You are here

uc_stripe.module in Ubercart Stripe 7.3

A stripe.js PCI-compliant payment gateway Forked from Bitcookie's work (thanks!) which was posted at http://bitcookie.com/blog/pci-compliant-ubercart-and-stripe-js from discussion in the uc_stripe issue queue, https://www.drupal.org/node/1467886

File

uc_stripe.module
View source
<?php

/**
 * @file
 * A stripe.js PCI-compliant payment gateway
 * Forked from Bitcookie's work (thanks!) which was posted at
 * http://bitcookie.com/blog/pci-compliant-ubercart-and-stripe-js
 * from discussion in the uc_stripe issue queue,
 * https://www.drupal.org/node/1467886
 */

/**
 * Settings value for basic stripe styles.
 */
define('UC_STRIPE_BASIC_STYLES', 0);

/**
 * Settings value for advanced stripe styles.
 */
define('UC_STRIPE_ADVANCED_STYLES', 1);

/**
 * Implements hook_libraries_info() to define what files should be loaded.
 *
 * @return mixed
 */
module_load_include('inc', 'uc_stripe', 'uc_stripe.mail');
function uc_stripe_libraries_info() {
  $libraries['stripe'] = array(
    'name' => 'Stripe PHP Library',
    'vendor url' => 'http://stripe.com',
    'download url' => 'https://github.com/stripe/stripe-php/releases',
    'download file url' => 'https://github.com/stripe/stripe-php/archive/v6.38.0.tar.gz',
    'version arguments' => array(
      'file' => 'VERSION',
      'pattern' => '/(\\d+\\.\\d+\\.\\d+)/',
    ),
    'versions' => array(
      '6.38.0' => array(
        'files' => array(
          'php' => array(
            'init.php',
          ),
        ),
        'stripe_api_version' => '2019-05-16',
      ),
    ),
  );
  return $libraries;
}

/**
 * Implements hook_menu().
 */
function uc_stripe_menu() {
  $items = array();
  $items['uc_stripe/ajax/confirm_payment'] = array(
    'access callback' => true,
    'page callback' => '_uc_stripe_confirm_payment',
    'delivery callback' => 'drupal_json_output',
    'type' => MENU_CALLBACK,
  );
  $items['stripe/authenticate-payment/%'] = array(
    'access callback' => true,
    'page callback' => 'drupal_get_form',
    'page arguments' => array(
      'uc_stripe_authenticate_payment_form',
      2,
    ),
    'type' => MENU_CALLBACK,
    'file' => 'uc_stripe.pages.inc',
  );
  return $items;
}

/**
 * Implements hook_payment_gateway to register this payment gateway
 * @return array
 */
function uc_stripe_uc_payment_gateway() {
  $gateways = array();
  $gateways[] = array(
    'id' => 'uc_stripe',
    'title' => t('Stripe Gateway'),
    'description' => t('Process card payments using Stripe JS.'),
    'settings' => 'uc_stripe_settings_form',
    'credit' => 'uc_stripe_charge',
  );
  return $gateways;
}

/**
 * Implements hook_recurring_info() to integrate with uc_recurring
 *
 * @return mixed
 */
function uc_stripe_recurring_info() {
  $items['uc_stripe'] = array(
    'name' => t('Stripe'),
    'payment method' => 'credit',
    'module' => 'uc_recurring',
    'fee handler' => 'uc_stripe',
    'process callback' => 'uc_stripe_process',
    'renew callback' => 'uc_stripe_renew',
    'cancel callback' => 'uc_stripe_cancel',
    'own handler' => FALSE,
    'menu' => array(
      'charge' => UC_RECURRING_MENU_DEFAULT,
      'edit' => UC_RECURRING_MENU_DEFAULT,
      'cancel' => UC_RECURRING_MENU_DEFAULT,
    ),
  );
  return $items;
}

/**
 * Implements hook_form_FORMID_alter() to do JS Stripe processing when processing
 * from the order review page
 *
 * @param unknown $form
 * @param unknown $form_state
 * @param unknown $form_id
 *
 */
function uc_stripe_form_uc_cart_checkout_review_form_alter(&$form, &$form_state, $form_id) {

  //This alter hook should only take action when payment method is credit.
  if ($form_state['uc_order']->payment_method != 'credit' || uc_credit_default_gateway() != 'uc_stripe') {
    return;
  }

  // If payment method is not found, hide submit button, and show error to user
  if (empty($_SESSION['stripe']['payment_method'])) {
    $form['actions']['submit']['#type'] = 'hidden';
    $fail_message = variable_get('uc_credit_fail_message', t('We were unable to process your credit card payment. Please verify your details and try again.  If the problem persists, contact us to complete your order.'));
    watchdog('uc_stripe', 'Stripe charge failed for order @order, message: @message', array(
      '@order' => $form_state['uc_order']->order_id,
      '@message' => 'Payment method not found',
    ));
    drupal_set_message($fail_message, 'error');
    return;
  }
  $stripe_payment_method_id = $_SESSION['stripe']['payment_method'];
  $order_id = $form_state['uc_order']->order_id;
  $apikey = variable_get('uc_stripe_testmode', TRUE) ? check_plain(variable_get('uc_stripe_api_key_test_publishable', '')) : check_plain(variable_get('uc_stripe_api_key_live_publishable', ''));
  $settings = array(
    'methodId' => $stripe_payment_method_id,
    'apikey' => $apikey,
    'orderId' => $order_id,
  );

  //Attach Stripe v3 JS library and JS for processing payment
  $form['#attached']['js']['https://js.stripe.com/v3/'] = array(
    'type' => 'external',
  );
  $form['#attached']['js'][] = array(
    'data' => array(
      'uc_stripe' => $settings,
    ),
    'type' => 'setting',
  );
  $form['#attached']['js'][] = drupal_get_path('module', 'uc_stripe') . '/js/uc_stripe_process_payment.js';
}

/**
 * Implements hook_form_FORMID_alter() to change the checkout form
 * All work as a result is done in JS, the ordinary post does not happen.
 *
 * @param $form
 * @param $form_state
 * @param $form_id
 */
function uc_stripe_form_uc_cart_checkout_form_alter(&$form, &$form_state) {

  // By default, hide the stripe payment pane. We will show it later if
  // stripe is being used to checkout.
  $form['panes']['payment-stripe']['#attached']['css'][] = array(
    'data' => '#payment-stripe-pane { display: none; }',
    'type' => 'inline',
  );
  if (uc_credit_default_gateway() != 'uc_stripe') {
    return;
  }
  $form['panes']['payment-stripe']['#states'] = array(
    'visible' => array(
      ':input[name="panes[payment][payment_method]"]' => array(
        'value' => 'credit',
      ),
    ),
  );
  $stripe_payment_form =& $form['panes']['payment-stripe']['details'];
  $payment_form =& $form['panes']['payment']['details'];

  // Markup text will not be displayed when JS and stripe are functioning properly
  // since Stripe Elements will replace the contents of this div
  $stripe_payment_form['stripe_card_element'] = array(
    '#prefix' => '<div id="stripe-card-element">',
    '#weight' => -10,
    '#markup' => '<div class="stripe-warning">' . t('Sorry, for security reasons your card cannot be processed. Please refresh this page and try again. If the problem persists please check that Javascript is enabled your browser.') . '</div>',
    '#suffix' => '</div>',
  );

  // Powered by Stripe (logo from https://stripe.com/about/resources)
  if (variable_get('uc_stripe_poweredby', FALSE)) {
    $stripe_payment_form['powered_by_stripe'] = array(
      '#type' => 'item',
      '#markup' => "<a target='_blank' href='http://stripe.com'><img src=" . '/' . drupal_get_path('module', 'uc_stripe') . '/images/solid-dark.svg' . " alt='Powered by Stripe'></a>",
      '#weight' => -20,
    );
  }

  // Used for payment method Id when retrieved from stripe.
  $stripe_payment_form['stripe_payment_method'] = array(
    '#type' => 'hidden',
    '#default_value' => 'default',
    '#attributes' => array(
      'id' => 'edit-panes-stripe-payment-details-stripe-payment-method',
    ),
  );
  if (empty($form['actions']['continue']['#attributes'])) {
    $form['actions']['continue']['#attributes'] = array();
  }
  $form['actions']['continue']['#attributes']['disabled'] = 'disabled';
  $apikey = variable_get('uc_stripe_testmode', TRUE) ? check_plain(variable_get('uc_stripe_api_key_test_publishable', '')) : check_plain(variable_get('uc_stripe_api_key_live_publishable', ''));

  // Retrieve the basic or advanced styling to pass to uc_stripe.js
  if (variable_get('uc_stripe_element_styles_mode', UC_STRIPE_BASIC_STYLES) == UC_STRIPE_ADVANCED_STYLES) {
    $element_styles = variable_get('uc_stripe_element_styles_json', '{}');
  }
  else {

    // Simple style string needs to be JSON encoded first.
    $element_styles = variable_get('uc_stripe_element_styles', 'fontSize: 16px, color: black, iconColor: blue');
    if (!empty($element_styles)) {
      $element_styles = '{"base":' . json_encode(_uc_stripe_get_element_styles_array()) . '}';
    }
    else {
      $element_styles = '{}';
    }
  }

  // Add custom JS and CSS
  $settings = array(
    'apikey' => $apikey,
    'element_styles' => $element_styles,
  );
  $form['#attached']['js']['https://js.stripe.com/v3/'] = array(
    'type' => 'external',
  );
  $form['#attached']['js'][] = array(
    'data' => array(
      'uc_stripe' => $settings,
    ),
    'type' => 'setting',
  );
  $form['#attached']['js'][] = drupal_get_path('module', 'uc_stripe') . '/js/uc_stripe.js';
  $form['#attached']['css'][] = drupal_get_path('module', 'uc_stripe') . '/css/uc_stripe.css';

  // hide cc fields and set defaults since we rely fully on stripe's dynamic cc fields
  $payment_form['cc_number']['#type'] = 'hidden';
  $payment_form['cc_number']['#default_value'] = '';
  $payment_form['cc_number']['#attributes']['id'] = 'edit-panes-payment-details-cc-number';
  $payment_form['cc_cvv']['#type'] = 'hidden';
  $payment_form['cc_cvv']['#default_value'] = '';
  $payment_form['cc_cvv']['#attributes']['id'] = 'edit-panes-payment-details-cc-cvv';
  $payment_form['cc_exp_year']['#type'] = 'hidden';
  $payment_form['cc_exp_year']['#attributes']['id'] = 'edit-panes-payment-details-cc-exp-year';

  //Stripe CC expiration can be up to 50 years in future. The normal ubercart  select

  // options only go up to 20 years in the future.
  $min = intval(date('Y'));
  $max = intval(date('Y')) + 50;
  $default = intval(date('Y'));
  $payment_form['cc_exp_year']['#options'] = drupal_map_assoc(range($min, $max));
  $payment_form['cc_exp_year']['#default_value'] = $default;
  $payment_form['cc_exp_month']['#type'] = 'hidden';
  $payment_form['cc_exp_month']['#default_value'] = 1;
  $payment_form['cc_exp_month']['#attributes']['id'] = 'edit-panes-payment-details-cc-exp-month';

  // Add custom submit which will do saving away of token during submit.
  $form['#submit'][] = 'uc_stripe_checkout_form_customsubmit';

  // Add a section for stripe.js error messages (CC validation, etc.)
  $form['panes']['messages'] = array(
    '#markup' => "<div id='uc_stripe_messages' class='messages error hidden'></div>",
  );
  if (uc_credit_default_gateway() == 'uc_stripe') {
    if (variable_get('uc_stripe_testmode', TRUE)) {
      $form['panes']['testmode'] = array(
        '#prefix' => "<div class='messages' style='background-color:#BEEBBF'>",
        '#markup' => t("Test mode is <strong>ON</strong> for the Stripe Payment Gateway. Your  card will not be charged. To change this setting, edit the !link", array(
          '!link' => l("Stripe settings", "admin/store/settings/payment/method/credit"),
        )),
        '#suffix' => "</div>",
      );
    }
  }
}

/**
 * Implements hook_order_pane to provide the stripe customer info
 *
 * @return array
 */
function uc_stripe_uc_order_pane() {
  $panes[] = array(
    'id' => 'uc_stripe',
    'callback' => 'uc_stripe_order_pane_stripe',
    'title' => t('Stripe Customer Info'),
    'desc' => t("Stripe Information"),
    'class' => 'pos-left',
    'weight' => 3,
    'show' => array(
      'view',
      'edit',
    ),
  );
  return $panes;
}

/**
 * Implements hook_uc_checkout_pane to add checkout pane for stripe payment details
 *
 * @return array
 */
function uc_stripe_uc_checkout_pane() {
  $panes['payment-stripe'] = array(
    'callback' => '_uc_stripe_payment_pane_callback',
    'title' => t('Payment Information'),
    'desc' => t("Accept stripe payment from customer."),
    'weight' => 6,
    'process' => FALSE,
    'collapsible' => FALSE,
  );
  return $panes;
}

/**
 * Implements uc_checkout_pane_callback() specified in 'callback' of
 * uc_stripe_uc_checkout_pane()
 *
 * Provides empty pane for stripe elements to be added
 * @param $op
 * @param $order
 * @param $form
 * @param $form_state
 * @return array
 */
function _uc_stripe_payment_pane_callback($op, $order, $form = NULL, &$form_state = NULL) {

  // Create separate payment pane for stripe because the normal payment pane is refreshed many times
  // by ajax, by country changes, etc.. Refreshing the payment section triggers Stripe Api's security feature
  // and destroys the Stripe Element in the DOM.
  // Emtpy values needed so that pane still appears.
  switch ($op) {
    case 'view':
      $description = t('');
      $contents['stripe_card_element'] = array(
        '#markup' => '',
      );
      return array(
        'description' => $description,
        'contents' => $contents,
      );
  }
}

/**
 * Implements hook_uc_checkout_complete()
 *
 * Saves stripe customer_id into the user->data object
 *
 * @param $order
 * @param $account
 */
function uc_stripe_uc_checkout_complete($order, $account) {
  if ($order->payment_method == "credit" && uc_credit_default_gateway() == 'uc_stripe') {

    // Pull the stripe payment method ID from the session.
    // It got there in uc_stripe_checkout_form_customsubmit()
    $stripe_payment_id = $_SESSION['stripe']['payment_method'];
    $stripe_customer_id = $order->data['stripe_customer_id'];
    $loaded_user = user_load($account->uid);
    user_save($loaded_user, array(
      'data' => array(
        'uc_stripe_customer_id' => $stripe_customer_id,
      ),
    ));
    user_save($loaded_user, array(
      'data' => array(
        'uc_stripe_payment_id' => $stripe_payment_id,
      ),
    ));
  }
}

/**
 * Implements uc_order_pane_callback() specified in 'callback' of
 * uc_stripe_uc_order_pane()
 *
 * Returns text for customer id for order pane.
 *
 * @param $op
 * @param $order
 * @param $form
 * @param $form_state
 * @return array
 */
function uc_stripe_order_pane_stripe($op, $order, &$form = NULL, &$form_state = NULL) {
  switch ($op) {
    case 'view':
      $stripe_customer_id = _uc_stripe_get_customer_id($order->uid);
      $output = t("Customer ID: ") . $stripe_customer_id;
      return array(
        '#markup' => $output,
      );
    default:
      return;
  }
}

/**
 * Provide configuration form for uc_stripe
 *
 * @return mixed
 */
function uc_stripe_settings_form() {
  $form['uc_stripe_settings'] = array(
    '#type' => 'fieldset',
    '#title' => t('Stripe settings'),
  );
  $form['uc_stripe_settings']['uc_stripe_api_key_test_secret'] = array(
    '#type' => 'textfield',
    '#title' => t('Test Secret Key'),
    '#default_value' => variable_get('uc_stripe_api_key_test_secret', ''),
    '#description' => t('Your Development Stripe API Key. Must be the "secret" key, not the "publishable" one.'),
  );
  $form['uc_stripe_settings']['uc_stripe_api_key_test_publishable'] = array(
    '#type' => 'textfield',
    '#title' => t('Test Publishable Key'),
    '#default_value' => variable_get('uc_stripe_api_key_test_publishable', ''),
    '#description' => t('Your Development Stripe API Key. Must be the "publishable" key, not the "secret" one.'),
  );
  $form['uc_stripe_settings']['uc_stripe_api_key_live_secret'] = array(
    '#type' => 'textfield',
    '#title' => t('Live Secret Key'),
    '#default_value' => variable_get('uc_stripe_api_key_live_secret', ''),
    '#description' => t('Your Live Stripe API Key. Must be the "secret" key, not the "publishable" one.'),
  );
  $form['uc_stripe_settings']['uc_stripe_api_key_live_publishable'] = array(
    '#type' => 'textfield',
    '#title' => t('Live Publishable Key'),
    '#default_value' => variable_get('uc_stripe_api_key_live_publishable', ''),
    '#description' => t('Your Live Stripe API Key. Must be the "publishable" key, not the "secret" one.'),
  );
  $form['uc_stripe_settings']['uc_stripe_element_styles_mode'] = array(
    '#type' => 'radios',
    '#title' => t('Basic / Advanced Stripe Card Element Styling'),
    '#options' => array(
      UC_STRIPE_BASIC_STYLES => t('Basic'),
      UC_STRIPE_ADVANCED_STYLES => t('Advanced'),
    ),
    '#default_value' => variable_get('uc_stripe_element_styles_mode', UC_STRIPE_BASIC_STYLES),
    '#description' => t('Basic styling allows simple modifications while advanced allows greater customization.'),
  );
  $form['uc_stripe_settings']['uc_stripe_element_styles'] = array(
    '#type' => 'textfield',
    '#title' => t('Style Settings for Stripe Card Element (Basic)'),
    '#description' => t('Enter your style settings as a simple comma-separated list (no quoted commas or pseudo-elements allowed). Example: fontSize: 16px, color: black, iconColor: blue (These styles modify the Stripe "base" styles, see <a target="_blank" href="@stripe-documentation">Stripe documentation</a>)', array(
      '@stripe-documentation' => 'https://stripe.com/docs/js/appendix/style',
    )),
    '#default_value' => variable_get('uc_stripe_element_styles', 'fontSize: 16px, color: black, iconColor: blue'),
    '#states' => array(
      'visible' => array(
        ':input[name="uc_stripe_element_styles_mode"]' => array(
          'value' => UC_STRIPE_BASIC_STYLES,
        ),
      ),
    ),
  );
  $form['uc_stripe_settings']['uc_stripe_element_styles_json'] = array(
    '#type' => 'textarea',
    '#title' => t('Style Settings for Stripe Card Element (Advanced)'),
    '#description' => t('Enter a value for the "style" element as a valid JSON-encoded string. Example: {"base":{"fontFamily":"Roboto, Open Sans, Segoe UI, sans-serif","fontSize":"16px","color":"#333333","iconColor":"blue","::placeholder":{"color":"#333333"}}}. Newlines and whitespace are allowed. See <a target="_blank" href="@stripe-documentation">Stripe documentation</a> for all variants.', array(
      '@stripe-documentation' => 'https://stripe.com/docs/js/appendix/style',
    )),
    '#default_value' => variable_get('uc_stripe_element_styles_json', '{}'),
    '#states' => array(
      'visible' => array(
        ':input[name="uc_stripe_element_styles_mode"]' => array(
          'value' => UC_STRIPE_ADVANCED_STYLES,
        ),
      ),
    ),
  );
  $email_text = _uc_stripe_get_authentication_required_email_text();
  $form['uc_stripe_settings']['uc_stripe_authentication_required_email'] = array(
    '#type' => 'textarea',
    '#title' => t('Email for Recurring payment authentication'),
    '#default_value' => variable_get('uc_stripe_authentication_required_email', $email_text),
    '#description' => t('If your site uses recurring payments, some transactions will require the customer to return to the site and authenticate before the subscrption payment can be processed.'),
  );
  $form['uc_stripe_settings']['uc_stripe_testmode'] = array(
    '#type' => 'checkbox',
    '#title' => t('Test mode'),
    '#description' => 'Testing Mode: Stripe will use the development API key to process the transaction so the card will not actually be charged.',
    '#default_value' => variable_get('uc_stripe_testmode', TRUE),
  );
  $form['uc_stripe_settings']['uc_stripe_poweredby'] = array(
    '#type' => 'checkbox',
    '#title' => t('Powered by Stripe'),
    '#description' => 'Show "powered by Stripe" in checkout.',
    '#default_value' => variable_get('uc_stripe_poweredby', FALSE),
  );
  $form['uc_stripe_settings']['uc_stripe_metadata_titles'] = array(
    '#type' => 'checkbox',
    '#title' => t('Metadata: Title'),
    '#description' => t('Include order item title(s) in Stripe metadata.'),
    '#default_value' => variable_get('uc_stripe_metadata_titles', FALSE),
  );
  $form['uc_stripe_settings']['uc_stripe_metadata_models'] = array(
    '#type' => 'checkbox',
    '#title' => t('Metadata: Model'),
    '#description' => t('Include item model(s) (SKU(s)) in Stripe metadata.'),
    '#default_value' => variable_get('uc_stripe_metadata_models', FALSE),
  );
  return $form;
}

/**
 * Implements hook_form_FORMID_alter()
 *
 * Add validation function for stripe settings
 *
 * @param $form
 * @param $form_state
 */
function uc_stripe_form_uc_payment_method_settings_form_alter(&$form, &$form_state) {
  if ($form_state['build_info']['args'][0] == 'credit') {
    $form['#validate'][] = 'uc_stripe_settings_form_validate';
  }
}

/**
 * Validation function and normalize keys (trim spaces)
 *
 * @param $form
 * @param $form_state
 */
function uc_stripe_settings_form_validate($form, &$form_state) {
  $elements = array(
    'uc_stripe_api_key_test_secret',
    'uc_stripe_api_key_test_publishable',
    'uc_stripe_api_key_live_secret',
    'uc_stripe_api_key_live_publishable',
  );
  if ($form_state['values']['uc_pg_uc_stripe_enabled']) {
    foreach ($elements as $element_name) {
      $form_state['values'][$element_name] = _uc_stripe_sanitize_key($form_state['values'][$element_name]);
      if (!_uc_stripe_validate_key($form_state['values'][$element_name])) {
        form_set_error($element_name, t('@name does not appear to be a valid stripe key', array(
          '@name' => $element_name,
        )));
      }
    }
  }

  // Make sure they haven't tried to validate credit card numbers, as uc_stripe will not provide a real one.
  if (!empty($form_state['values']['uc_credit_validate_numbers'])) {
    form_set_error('uc_credit_validate_numbers', t('When used with Ubercart Stripe, "Validate credit card number at checkout" must be unchecked.'));
  }

  // Validate advanced or basic styling.
  if ($form_state['values']['uc_stripe_element_styles_mode']) {

    // Advanced - Validate JSON-encoded style settings.
    $element_styles = $form_state['values']['uc_stripe_element_styles_json'];
    if (empty($element_styles)) {
      form_set_error('uc_stripe_element_styles_json', t('Empty Style Settings for Stripe Card Element (Advanced) is not a valid JSON. Set to {} or switch off Advanced styling.'));
    }
    else {
      json_decode($element_styles);
      if (json_last_error() !== JSON_ERROR_NONE) {
        form_set_error('uc_stripe_element_styles_json', t('Style Settings for Stripe Card Element (Advanced) JSON does not validate:') . ' ' . json_last_error_msg());
      }
    }
  }
  else {

    // Basic - Make sure that stripe style settings do not have an extra comma at the end.
    $element_styles = $form_state['values']['uc_stripe_element_styles'];
    if (!empty($element_styles)) {
      if (substr($element_styles, -1) == ',') {
        form_set_error('uc_stripe_element_styles', t('Stripe Element Styles list should not end with a comma.'));
      }
    }
  }
}

/**
 * Sanitize and strip whitespace from Stripe keys
 *
 * @param $key
 */
function _uc_stripe_sanitize_key($key) {
  $key = trim($key);
  $key = check_plain($key);
  return $key;
}

/**
 * Validate Stripe key
 *
 * @param $key
 * @return boolean
 */
function _uc_stripe_validate_key($key) {
  $valid = preg_match('/^[a-zA-Z0-9_]+$/', $key);
  return $valid;
}

/**
 * Custom submit function to store the stripe token
 *
 * Since we don't have a user account at this step, we're going to store the token
 * in the session. We'll grab the token in the charge callback and use it to charge
 *
 * @param $form
 * @param $form_state
 */
function uc_stripe_checkout_form_customsubmit($form, &$form_state) {

  // This submit may be entered on another payment type, so don't set session in that case.
  if (!empty($form_state['values']['panes']['payment-stripe']['details']['stripe_payment_method'])) {
    $_SESSION['stripe']['payment_method'] = $form_state['values']['panes']['payment-stripe']['details']['stripe_payment_method'];
  }
}

/**
 * Generic "charge" callback that runs on checkout and via the order's "card" terminal
 *
 * @param $order_id
 * @param $amount
 * @param $data
 * @return array
 */
function uc_stripe_charge($order_id, $amount, $data) {
  global $user;

  //  Load the stripe PHP API
  if (!_uc_stripe_prepare_api()) {
    $result = array(
      'success' => FALSE,
      'comment' => t('Stripe API not found.'),
      'message' => t('Stripe API not found. Contact the site administrator.'),
      'uid' => $user->uid,
      'order_id' => $order_id,
    );
    return $result;
  }
  $order = uc_order_load($order_id);
  $context = array(
    'revision' => 'formatted-original',
    'type' => 'amount',
  );
  $options = array(
    'sign' => FALSE,
    'thou' => FALSE,
    'dec' => FALSE,
    'prec' => 2,
  );

  // Format the amount in cents, which is what Stripe wants
  $amount = uc_currency_format($amount, FALSE, FALSE, FALSE);

  //  Charge the stripe customer the amount in the order

  //--Handle transactions for $0

  // Stripe can't handle transactions < $0.50, but $0 is a common value
  // so we will just return a positive result when the amount is $0.
  if ($amount == 0) {
    $result = array(
      'success' => TRUE,
      'message' => t('Payment of $0 approved'),
      'uid' => $user->uid,
      'trans_id' => md5(uniqid(rand())),
    );
    uc_order_comment_save($order_id, $user->uid, $result['message'], 'admin');
    return $result;
  }
  $stripe_customer_id = null;
  if (key_exists('stripe_customer_id', $order->data)) {
    $stripe_customer_id = $order->data['stripe_customer_id'];
  }

  // Rexamine Payment Intent and Record payment or failure to the customer
  try {
    if (!key_exists('payment_intent_id', $order->data)) {
      throw new Exception('The payment Intent has failed.');
    }

    //Bail if there's no customer ID
    if (empty($stripe_customer_id) || is_null($stripe_customer_id)) {
      throw new Exception('No customer ID found');
    }

    //Bail if there's no payment method
    if (empty($_SESSION['stripe']['payment_method'])) {
      throw new Exception('Token not found');
    }
    $stripe_payment_method_id = $_SESSION['stripe']['payment_method'];
    $params = array(
      "amount" => $amount,
      "currency" => strtolower($order->currency),
      "customer" => $stripe_customer_id,
      "description" => t("Order #@order_id", array(
        "@order_id" => $order_id,
      )),
      "metadata" => get_order_metadata($order),
      "payment_method" => $stripe_payment_method_id,
      "payment_method_types" => [
        'card',
      ],
      "confirm" => true,
    );
    $intent_id = $order->data['payment_intent_id'];
    if (!empty($shipping_info)) {
      $params['shipping'] = $shipping_info;
      \Stripe\PaymentIntent::update($intent_id, [
        'shipping' => $shipping_info,
      ]);
    }

    // charge the Customer the amount in the order
    $payment_intent = \Stripe\PaymentIntent::retrieve($intent_id);
    if ($payment_intent->status != 'succeeded') {
      throw new Exception($payment_intent['last_payment_error']['message']);
    }
    $charge_id = $payment_intent->charges->data[0]['id'];
    $formatted_amount = $amount / 100;
    $formatted_amount = number_format($formatted_amount, 2);

    //     $payment_method = \Stripe\PaymentMethod::retrieve($payment_intent->payment_method);
    //     $payment_method->attach(['customer' => $stripe_customer_id]);
    $result = array(
      'success' => TRUE,
      'message' => t('Payment of @amount processed successfully, Stripe transaction id @transaction_id.', array(
        '@amount' => $formatted_amount,
        '@transaction_id' => $charge_id,
      )),
      'comment' => t('Stripe transaction ID: @transaction_id', array(
        '@transaction_id' => $charge_id,
      )),
      'uid' => $user->uid,
    );
    uc_order_comment_save($order_id, $user->uid, $result['message'], 'admin');
    return $result;
  } catch (Exception $e) {
    $result = array(
      'success' => FALSE,
      'comment' => $e
        ->getCode(),
      'message' => t("Stripe Charge Failed for order !order: !message", array(
        "!order" => $order_id,
        "!message" => $e
          ->getMessage(),
      )),
      'uid' => $user->uid,
      'order_id' => $order_id,
    );
    uc_order_comment_save($order_id, $user->uid, $result['message'], 'admin');
    watchdog('uc_stripe', 'Stripe charge failed for order @order, message: @message', array(
      '@order' => $order_id,
      '@message' => $result['message'],
    ));
    return $result;
  }

  //  Default / Fallback procedure to fail if the above conditions aren't met
  $result = array(
    'success' => FALSE,
    'comment' => "Stripe Gateway Error",
    'message' => "Stripe Gateway Error",
    'uid' => $user->uid,
    'order_id' => $order_id,
  );
  uc_order_comment_save($order_id, $user->uid, $result['message'], 'admin');
  watchdog('uc_stripe', 'Stripe gateway error for order @order_id', array(
    'order_id' => $order_id,
  ));
  return $result;
}

/**
 * Handle renewing a recurring fee, called by uc_recurring
 *
 * Runs when the subscription interval is hit. So once a month or whatever.
 * This just charges the stripe customer whatever amount ubercart wants. It does
 * not use the Stripe subscription feature.
 *
 * @param $order
 * @param $fee
 * @return bool
 */
function uc_stripe_renew($order, &$fee) {
  try {

    //Load the API
    _uc_stripe_prepare_api();

    //Get the customer ID
    $stripe_customer_id = _uc_stripe_get_customer_id($order->uid);
    $stripe_payment_id = _uc_stripe_get_payment_id($order->uid);
    if (empty($stripe_customer_id)) {
      throw new Exception('No stripe customer ID found');
    }
    $amount = $fee->fee_amount;
    $amount = $amount * 100;

    //create intent Array
    $intent_params = array(
      'amount' => $amount,
      'currency' => strtolower($order->currency),
      'payment_method_types' => [
        'card',
      ],
      'customer' => $stripe_customer_id,
      'off_session' => true,
      'confirm' => true,
    );

    // Payment methods added with Stripe PaymentIntent API will be saved to customer
    // object in drupal. Payment cards saved with 2.x tokens will not have a value
    // saved to customer object, but the payment Intent will still continue because
    // Stipe will use default payment in those situations.
    if ($stripe_payment_id) {
      $intent_params['payment_method'] = $stripe_payment_id;
    }

    // Idempotency key to mark unique requests in Stripe API.
    $idempotency_key = _uc_stripe_create_idempotency_key($order->order_id . $amount . $stripe_payment_id);

    //Allow other modules to alter recurring $intent params array
    drupal_alter('uc_stripe_recurring_intent', $intent_params, $order);
    $payment_intent = \Stripe\PaymentIntent::create($intent_params, [
      'idempotency_key' => $idempotency_key,
    ]);
    uc_payment_enter($order->order_id, $order->payment_method, $order->order_total, $fee->uid, $payment_intent, "Success");
    $formatted_amount = number_format($fee->fee_amount, 2);
    $message = t('Card renewal payment of @amount processed successfully.', array(
      '@amount' => $formatted_amount,
    ));
    uc_order_comment_save($fee->order_id, $order->uid, $message, 'admin');
    return TRUE;
  } catch (\Stripe\Error\Card $e) {
    if ($e
      ->getDeclineCode() === 'authentication_required') {
      $NOT_COMPLETED = 0;

      // Create and store hash so that we can prompt user to authenticate payment.
      $hash = drupal_hmac_base64(REQUEST_TIME . $order->order_id, drupal_get_hash_salt() . $stripe_payment_id);
      db_insert('uc_stripe_pending_auth')
        ->fields(array(
        'order_id' => $order->order_id,
        'completed' => $NOT_COMPLETED,
        'rfee_id' => $fee->rfid,
        'hash' => $hash,
      ))
        ->execute();

      // Prepare email to alert user that authentication is required.
      $params['body'] = variable_get('uc_stripe_authentication_required_email', _uc_stripe_get_authentication_required_email_text());
      $params['user'] = user_load($order->uid);
      $params['hash'] = $hash;
      drupal_mail('uc_stripe', 'authentication_required', $params['user']->mail, language_default(), $params);
    }
    $result = array(
      'success' => FALSE,
      'comment' => $e
        ->getCode(),
      'message' => t("Renewal Failed for order !order: !message", array(
        "!order" => $order->order_id,
        "!message" => $e
          ->getMessage(),
      )),
    );
    uc_order_comment_save($order->order_id, $order->uid, $result['message'], 'admin');
    watchdog('uc_stripe', 'Renewal failed for order @order_id, code=@code, message: @message', array(
      '@order_id' => $order->order_id,
      '@code' => $e
        ->getCode(),
      '@message' => $e
        ->getMessage(),
    ));
    return FALSE;
  } catch (Exception $e) {
    watchdog('uc_stripe', 'Renewal failed for order @order_id, code=@code, message: @message', array(
      '@order_id' => $order->order_id,
      '@code' => $e
        ->getCode(),
      '@message' => $e
        ->getMessage(),
    ));
    return FALSE;
  }
}

/**
 * UC Recurring: Process a new recurring fee.
 * This runs when subscriptions are "set up" for the first time.
 * There is no action to be taken here except returning TRUE because the customer
 * ID is already stored with the user, where it can be accessed when next charge
 * takes place.
 *
 * @param $order
 * @param $fee
 * @return bool
 */
function uc_stripe_process($order, &$fee) {
  return TRUE;
}

/**
 * UC Recurring: Cancel a recurring fee.
 * This runs when subscriptions are cancelled
 * Since we're handling charge intervals in ubercart, this doesn't need to do anything.
 *
 * @param $order
 * @param $op
 * @return bool
 */
function uc_stripe_cancel($order, $op) {
  $message = t("Subscription Canceled");
  uc_order_comment_save($order->order_id, $order->uid, $message, 'order', 'completed', FALSE);
  return TRUE;
}

/**
 * Load stripe API
 *
 * @return bool
 */
function _uc_stripe_prepare_api() {
  module_load_include('install', 'uc_stripe');
  if (!_uc_stripe_load_library()) {
    return FALSE;
  }
  if (!_uc_stripe_check_api_keys()) {
    watchdog('uc_stripe', 'Stripe API keys are not configured. Payments cannot be made without them.', array(), WATCHDOG_ERROR);
    return FALSE;
  }
  $secret_key = variable_get('uc_stripe_testmode', TRUE) ? variable_get('uc_stripe_api_key_test_secret', '') : variable_get('uc_stripe_api_key_live_secret', '');
  try {
    $library = libraries_load('stripe');
    \Stripe\Stripe::setApiKey($secret_key);
    \Stripe\Stripe::setApiVersion($library['stripe_api_version']);
  } catch (Exception $e) {
    watchdog('uc_stripe', 'Error setting the Stripe API Key. Payments will not be processed: %error', array(
      '%error' => $e
        ->getMessage(),
    ));
  }
  try {
    $module_info = system_get_info('module', "uc_stripe");
    $uc_stripe_version = is_null($module_info['version']) ? 'dev-unknown' : $module_info['version'];
    \Stripe\Stripe::setAppInfo("Drupal Ubercart Stripe", $uc_stripe_version, "https://www.drupal.org/project/uc_stripe");
  } catch (Exception $e) {
    watchdog('uc_stripe', 'Error setting Stripe plugin information: %error', array(
      '%error' => $e
        ->getMessage(),
    ));
  }
  return TRUE;
}

/**
 * Check that all API keys are configured.
 *
 * @return bool
 *   TRUE if all 4 keys have a value.
 */
function _uc_stripe_check_api_keys() {
  return variable_get('uc_stripe_api_key_live_publishable', FALSE) && variable_get('uc_stripe_api_key_live_secret', FALSE) && variable_get('uc_stripe_api_key_test_publishable', FALSE) && variable_get('uc_stripe_api_key_test_secret', FALSE);
}

/**
 * Retrieve the Stripe customer id for a user
 *
 * @param $uid
 * @return bool
 */
function _uc_stripe_get_customer_id($uid) {
  $account = user_load($uid);
  $id = !empty($account->data['uc_stripe_customer_id']) ? $account->data['uc_stripe_customer_id'] : FALSE;
  return $id;
}

/**
 * Retrieve the Stripe payment id for a user
 *
 * @param $uid
 * @return bool
 */
function _uc_stripe_get_payment_id($uid) {
  $account = user_load($uid);
  $id = !empty($account->data['uc_stripe_payment_id']) ? $account->data['uc_stripe_payment_id'] : FALSE;
  return $id;
}

/**
 * Implements hook_theme_registry_alter() to make sure that we render
 * the entire credit form, including the key returned by JS.
 *
 * @param $theme_registry
 */
function uc_stripe_theme_registry_alter(&$theme_registry) {
  if (!empty($theme_registry['uc_payment_method_credit_form'])) {
    $theme_registry['uc_payment_method_credit_form']['function'] = 'uc_stripe_uc_payment_method_credit_form';
  }
}

/**
 * Replace uc_credit's form themeing with our own - adds stripe_token.
 * @param $form
 * @return string
 */
function uc_stripe_uc_payment_method_credit_form($form) {
  $output .= drupal_render($form['config_error']);
  $output .= theme('uc_payment_method_credit_form', $form);
  $output .= drupal_render($form['stripe_payment_method']);
  $output .= drupal_render($form['dummy_image_load']);
  return $output;
}

/**
 * Used to return the appropriate response after checking Stripe Payment Intent
 * status
 * @param Object $intent
 * @return string response
 */
function _generatePaymentResponse($intent) {
  if ($intent->status == 'requires_action' && $intent->next_action->type == 'use_stripe_sdk') {

    # Tell the client to handle the action
    $response = [
      'requires_action' => true,
      'payment_intent_client_secret' => $intent->client_secret,
    ];
  }
  else {
    if ($intent->status == 'succeeded') {

      # The payment didn’t need any additional actions and completed!

      # Handle post-payment fulfillment
      $response = [
        'success' => true,
      ];
    }
    else {

      # Invalid status
      http_response_code(500);
      $response = [
        'error' => 'Invalid PaymentIntent status',
      ];
    }
  }
  return $response;
}

/**
 * Ajax page callback for callback uc_stripe/ajax/confirm_payment page
 * This is used to send payment and intent status back to JS client
 * @return string Json response
 */
function _uc_stripe_confirm_payment() {
  global $user;

  # retrieve json from POST body
  $received_json = file_get_contents("php://input", TRUE);
  $data = drupal_json_decode($received_json, TRUE);
  $order_id = $data['order_id'];
  $order = uc_order_load($order_id);
  if (!_uc_stripe_prepare_api()) {
    $message = 'Stripe API not found.';
    watchdog('uc_stripe', 'Error in Stripe API: @message', array(
      '@message' => $message,
    ));
    return [
      'error' => $message,
    ];
  }

  // Format the amount in cents, which is what Stripe wants
  $amount = uc_currency_format($order->order_total, FALSE, FALSE, FALSE);
  $stripe_customer_id = False;
  $order_has_stripe_id = key_exists('stripe_customer_id', $order->data) ? True : False;

  // Check various places to get the stripe_customer_id. If not found we'll create
  // a new stripe user.
  if ($order_has_stripe_id) {
    $stripe_customer_id = $order->data['stripe_customer_id'];
  }
  else {
    if ($user->uid != $order->uid) {
      $stripe_customer_id = _uc_stripe_get_customer_id($order->uid);
    }
    else {
      $stripe_customer_id = _uc_stripe_get_customer_id($user->uid);
    }
  }

  // In the case where the stored customer_id is not a valid customer in Stripe
  // then we'll need to create a new stripe customer. see #3071712
  if ($stripe_customer_id && !_uc_stripe_is_stripe_id_valid($stripe_customer_id)) {
    watchdog('uc_stripe', 'Stripe customer: @customer is not valid in this instance of Stripe. A new customer will be created.', array(
      '@customer' => $stripe_customer_id,
    ));
    $stripe_customer_id = false;
  }
  $intent = null;
  try {
    if (isset($data['payment_method_id'])) {
      $payment_method_id = $data['payment_method_id'];
      $params = array(
        'payment_method' => $payment_method_id,
        "description" => t("Order #@order_id", array(
          "@order_id" => $order_id,
        )),
        "metadata" => get_order_metadata($order),
        'amount' => $amount,
        'currency' => strtolower($order->currency),
        'confirmation_method' => 'manual',
        'confirm' => true,
        'setup_future_usage' => 'off_session',
        'save_payment_method' => true,
      );
      if (!$stripe_customer_id) {
        $customer = _uc_stripe_create_stripe_customer($order, $payment_method_id);
        if (!$customer) {
          $message = 'Customer creation failed.';
          return [
            'error' => $message,
          ];
        }
        $stripe_customer_id = $customer->id;
      }
      $params['customer'] = $stripe_customer_id;

      // Idempotency key to mark unique requests in Stripe API.
      $idempotency_key = _uc_stripe_create_idempotency_key($order_id . $amount . $payment_method_id);

      // Allow other modules to alter payment $intent params array
      drupal_alter('uc_stripe_payment_intent', $params, $order);

      # Create the PaymentIntent
      $intent = \Stripe\PaymentIntent::create($params, [
        'idempotency_key' => $idempotency_key,
      ]);
      if (!$order_has_stripe_id) {
        $order->data['stripe_customer_id'] = $stripe_customer_id;
      }
      $order->data['payment_intent_id'] = $intent->id;
      uc_order_save($order);
    }
    if (isset($data['payment_intent_id'])) {
      $intent = \Stripe\PaymentIntent::retrieve($data['payment_intent_id']);
      $intent
        ->confirm();
      $order->data['payment_intent_id'] = $data['payment_intent_id'];
      uc_order_save($order);
    }
    return _generatePaymentResponse($intent);
  } catch (Exception $e) {
    watchdog('uc_stripe', 'Payment could not be processed: @message', array(
      '@message' => $e
        ->getMessage(),
    ));
    return [
      'error' => $e
        ->getMessage(),
    ];
  }
}
function _uc_stripe_create_stripe_customer($order, $payment_method_id = NULL) {
  $stripe_customer_id = FALSE;
  try {

    // If the token is not in the user's session, we can't set up a new customer
    $shipping_info = array();
    if (!empty($order->delivery_postal_code)) {
      $shipping_info = array(
        'name' => @"{$order->delivery_first_name} {$order->delivery_last_name}",
        'phone' => @$order->delivery_phone,
      );
      $delivery_country = uc_get_country_data(array(
        'country_id' => $order->delivery_country,
      ));
      if ($delivery_country === FALSE) {
        $delivery_country = array(
          0 => array(
            'country_iso_code_2' => 'US',
          ),
        );
      }
      $shipping_info['address'] = array(
        'city' => @$order->delivery_city,
        'country' => @$delivery_country[0]['country_iso_code_2'],
        'line1' => @$order->delivery_street1,
        'line2' => @$order->delivery_street2,
        'postal_code' => @$order->delivery_postal_code,
      );
    }
    $params = array(
      'description' => "OrderID: {$order->order_id}",
      'email' => "{$order->primary_email}",
    );
    if (!empty($shipping_info)) {
      $params['shipping'] = $shipping_info;
    }

    //Create the customer in stripe
    $customer = \Stripe\Customer::create($params);
    return $customer;
  } catch (Exception $e) {
    $message = t("Stripe Customer Creation Failed for order !order: !message", array(
      "!order" => $order_id,
      "!message" => $e
        ->getMessage(),
    ));
    uc_order_comment_save($order_id, $user->uid, $message, 'admin');
    watchdog('uc_stripe', 'Failed stripe customer creation: @message', array(
      '@message' => $message,
    ));
    return false;
  }
}
function get_order_metadata($order) {

  //Get item titles and models
  foreach ($order->products as $item) {
    $titles[] = $item->title;
    $models[] = $item->model;
  }
  $metadata = array();
  if (variable_get('uc_stripe_metadata_models', FALSE) && !empty($models)) {
    $metadata['models'] = implode(";", $models);
  }
  if (variable_get('uc_stripe_metadata_titles', FALSE) && !empty($titles)) {
    $metadata['titles'] = implode(";", $titles);
  }
  return $metadata;
}

/**
 *
 * @param string $stripe_id
 * @return boolean result - if stripe_id is valid based on stripe api customer call
 */
function _uc_stripe_is_stripe_id_valid($stripe_id) {
  try {
    if (!_uc_stripe_prepare_api()) {
      $message = 'Stripe API not found.';
      watchdog('uc_stripe', 'Error in Stripe API: @message', array(
        '@message' => $message,
      ));
      return false;
    }
    $customer = \Stripe\Customer::retrieve($stripe_id);

    // Count deleted stripe customers as invalid
    return !$customer->deleted;
  } catch (Exception $e) {

    // IF customer is not found, an exception is thrown.
    return false;
  }
}

/**
 *
 * @return array styles - An associative array of style overrides from
 * the settings page.
 */
function _uc_stripe_get_element_styles_array() {
  $styles = array();
  $settings = variable_get('uc_stripe_element_styles', 'fontSize: 16px, color: black, iconColor: blue');
  $pairs = explode(',', $settings);
  foreach ($pairs as $pair) {
    $keyValue = explode(':', $pair);
    $key = trim($keyValue[0]);
    $value = trim($keyValue[1]);
    $styles[$key] = $value;
  }
  return $styles;
}

/**
 * Creates an idempotency key to prevent duplicate requests in Stripe API.
 *
 * @param string $key
 *   The string used to make a key.
 *
 * @return string
 *   The idempotency key.
 */
function _uc_stripe_create_idempotency_key($key) {
  $idempotency_key = hash('sha256', $key);
  return $idempotency_key;
}

Functions

Namesort descending Description
get_order_metadata
uc_stripe_cancel UC Recurring: Cancel a recurring fee. This runs when subscriptions are cancelled Since we're handling charge intervals in ubercart, this doesn't need to do anything.
uc_stripe_charge Generic "charge" callback that runs on checkout and via the order's "card" terminal
uc_stripe_checkout_form_customsubmit Custom submit function to store the stripe token
uc_stripe_form_uc_cart_checkout_form_alter Implements hook_form_FORMID_alter() to change the checkout form All work as a result is done in JS, the ordinary post does not happen.
uc_stripe_form_uc_cart_checkout_review_form_alter Implements hook_form_FORMID_alter() to do JS Stripe processing when processing from the order review page
uc_stripe_form_uc_payment_method_settings_form_alter Implements hook_form_FORMID_alter()
uc_stripe_libraries_info
uc_stripe_menu Implements hook_menu().
uc_stripe_order_pane_stripe Implements uc_order_pane_callback() specified in 'callback' of uc_stripe_uc_order_pane()
uc_stripe_process UC Recurring: Process a new recurring fee. This runs when subscriptions are "set up" for the first time. There is no action to be taken here except returning TRUE because the customer ID is already stored with the user, where it can be…
uc_stripe_recurring_info Implements hook_recurring_info() to integrate with uc_recurring
uc_stripe_renew Handle renewing a recurring fee, called by uc_recurring
uc_stripe_settings_form Provide configuration form for uc_stripe
uc_stripe_settings_form_validate Validation function and normalize keys (trim spaces)
uc_stripe_theme_registry_alter Implements hook_theme_registry_alter() to make sure that we render the entire credit form, including the key returned by JS.
uc_stripe_uc_checkout_complete Implements hook_uc_checkout_complete()
uc_stripe_uc_checkout_pane Implements hook_uc_checkout_pane to add checkout pane for stripe payment details
uc_stripe_uc_order_pane Implements hook_order_pane to provide the stripe customer info
uc_stripe_uc_payment_gateway Implements hook_payment_gateway to register this payment gateway
uc_stripe_uc_payment_method_credit_form Replace uc_credit's form themeing with our own - adds stripe_token.
_generatePaymentResponse Used to return the appropriate response after checking Stripe Payment Intent status
_uc_stripe_check_api_keys Check that all API keys are configured.
_uc_stripe_confirm_payment Ajax page callback for callback uc_stripe/ajax/confirm_payment page This is used to send payment and intent status back to JS client
_uc_stripe_create_idempotency_key Creates an idempotency key to prevent duplicate requests in Stripe API.
_uc_stripe_create_stripe_customer
_uc_stripe_get_customer_id Retrieve the Stripe customer id for a user
_uc_stripe_get_element_styles_array
_uc_stripe_get_payment_id Retrieve the Stripe payment id for a user
_uc_stripe_is_stripe_id_valid
_uc_stripe_payment_pane_callback Implements uc_checkout_pane_callback() specified in 'callback' of uc_stripe_uc_checkout_pane()
_uc_stripe_prepare_api Load stripe API
_uc_stripe_sanitize_key Sanitize and strip whitespace from Stripe keys
_uc_stripe_validate_key Validate Stripe key

Constants

Namesort descending Description
UC_STRIPE_ADVANCED_STYLES Settings value for advanced stripe styles.
UC_STRIPE_BASIC_STYLES Settings value for basic stripe styles.