From 2d550c6e6825b91b7e64c62475fff4b29759040a Mon Sep 17 00:00:00 2001 From: smalot Date: Thu, 25 Jan 2018 15:43:40 +0100 Subject: [PATCH] Issue #2717383 by Sebastien @Actualys: Add support for 3D Secure --- commerce_stripe.api.php | 60 +++- commerce_stripe.elements.js | 46 ++- commerce_stripe.module | 644 +++++++++++++++++++++++++++++++++++-- includes/commerce_stripe.admin.inc | 2 +- 4 files changed, 718 insertions(+), 34 deletions(-) diff --git a/commerce_stripe.api.php b/commerce_stripe.api.php index ead8ab3..a8f7cdf 100644 --- a/commerce_stripe.api.php +++ b/commerce_stripe.api.php @@ -1,4 +1,5 @@ commerce_order_total->value(); + $total_decimal = commerce_currency_amount_to_decimal($total_amount['amount'], $total_amount['currency_code']); + + // Disable 3D Secure check for order lower than 20. + if ($total_decimal < 20) { + $preference = STRIPE_3DSECURE_DISABLED; + } + + // Force 3D Secure check for order higher than 1000. + if ($total_decimal >= 1000) { + $preference = STRIPE_3DSECURE_ALWAYS; + } +} + +/** + * Alter 3DSecure requirment to define if 3DSecure is required depending of + * payment source. + * Define if 3DSecure is required depending of the source card and payment + * method settings. + * + * @param boolean $required + * The data to alter which define if 3DSecure is required. + * @param $source + * The source object from Stripe containing card data. + * + * @return bool + * Does 3DSecure is required for this payment and Source. + */ +function hook_commerce_stripe_3dsecure_required_alter(&$stripe_3dsecure_required, $source) { + $stripe_settings = _commerce_stripe_load_settings(); + // 3DSecure is not supported by the card or not required by module settings, + // so not required. + if ($source->card->three_d_secure === STRIPE_3DSECURE_CARD_NOT_SUPPORTED || $stripe_settings['elements_settings']['3d_secure'] === STRIPE_3DSECURE_DISABLED) { + return $required = FALSE; + } + + // Module settings define 3DSecure as required. Only left at this state cards + // allowing 3DSecure, so checking is required. + if ($stripe_settings['elements_settings']['3d_secure'] === STRIPE_3DSECURE_ALWAYS) { + return $required = TRUE; + } + + // The only other state for module settings is + // STRIPE_3DSECURE_ONLY_IF_REQUIRED. + // Require 3DSecure only if card requires it. + if ($source->card->three_d_secure === STRIPE_3DSECURE_CARD_REQUIRED) { + return $required = TRUE; + } + + // Other cases should not require 3DSecure. + return $required = FALSE; +} diff --git a/commerce_stripe.elements.js b/commerce_stripe.elements.js index 1a24996..85b216a 100644 --- a/commerce_stripe.elements.js +++ b/commerce_stripe.elements.js @@ -1,5 +1,9 @@ (function ($) { + String.prototype.capitalize = function() { + return this.charAt(0).toUpperCase() + this.slice(1); + }; + Drupal.behaviors.commerce_stripe_elements = { /** * Attach Stripe behavior to form elements. @@ -11,6 +15,16 @@ */ attach: function (context, settings) { var self = this; + + var add3DSecure = function() { + // Enable 3DSecure redirection if the feature is enabled by settings. + if (settings.stripe.secure_3d) { + self.card.redirect = settings.stripe.secure_3d.redirect; + self.card.type = 'three_d_secure'; + self.creationType = 'source'; + } + }; + if (typeof settings.stripe !== 'undefined') { // Create an instance of Stripe Elements var stripe = Stripe(settings.stripe.publicKey); @@ -26,7 +40,7 @@ $("#card-element").once('elements', function() { // Create an instance of the card Element - self.card = elements.create('card', {style: style}); + self.card = elements.create('card', {style: style, hidePostalCode: settings.stripe.hide_postal_code}); self.card.mount(this); // Attach the JS behaviors just once to the available card element. @@ -82,7 +96,10 @@ return; } } - stripe.createToken(self.card, Drupal.behaviors.commerce_stripe_elements.extractTokenData(form$)).then(function (result) { + + add3DSecure(); + + stripe['create' + self.creationType.capitalize()](self.card, Drupal.behaviors.commerce_stripe_elements.extractTokenData(form$)).then(function (result) { if (result.error) { // console.log('There was an error'); // console.log(result.error.message); @@ -105,9 +122,10 @@ Drupal.attachBehaviors(context); } else { + $("#card-errors").text(''); // Send the token to your server - $('#stripe_token').val(result.token.id); + $('#stripe_token').val(result[self.creationType].id); var submitButton$ = $('.checkout-buttons #edit-continue'); var submit_text = submitButton$.val(); // Set a triggering element for the form. @@ -162,7 +180,10 @@ return; } } - stripe.createToken(self.card, Drupal.behaviors.commerce_stripe_elements.extractTokenData(form$)).then(function(result) { + + add3DSecure(); + + stripe['create' + self.creationType.capitalize()](self.card, Drupal.behaviors.commerce_stripe_elements.extractTokenData(form$)).then(function(result) { if (result.error) { // console.log('There was an error'); // console.log(result.error.message); @@ -227,7 +248,9 @@ name: 'edit-payment-details-credit-card-owner' }; - stripe.createToken(self.card, Drupal.behaviors.commerce_stripe_elements.extractTokenData(form$)).then(function(result) { + add3DSecure(); + + stripe['create' + self.creationType.capitalize()](self.card, Drupal.behaviors.commerce_stripe_elements.extractTokenData(form$)).then(function(result) { if (result.error) { // console.log('There was an error'); // console.log(result.error.message); @@ -312,6 +335,11 @@ */ extractTokenData: function(form) { var data = {}; + // Do not extract token data if creation type is not token. + if (this.creationType !== 'token') { + return data; + } + $(':input[data-stripe]').not('[data-stripe="token"]').each(function() { var input = $(this); data[input.attr('data-stripe')] = input.val(); @@ -336,11 +364,17 @@ else if (typeof Drupal.settings.commerce_stripe_address !== 'undefined') { // Load the values from settings if the billing address isn't on // the same checkout pane as the address form. - data[stripeName] = Drupal.settings.commerce_stripe_address[stripeName]; + if (Drupal.settings.commerce_stripe_address[stripeName]) { + data[stripeName] = Drupal.settings.commerce_stripe_address[stripeName]; + } } } } return data; }, + /** + * Define stripe creation type to call: source or token. + */ + creationType: 'token' }; })(jQuery); diff --git a/commerce_stripe.module b/commerce_stripe.module index 95eda94..95f7678 100755 --- a/commerce_stripe.module +++ b/commerce_stripe.module @@ -12,6 +12,15 @@ define('COMMERCE_STRIPE_API_LATEST_TESTED', '2017-08-15'); define('COMMERCE_STRIPE_API_ACCOUNT_DEFAULT', 'Account Default'); define('COMMERCE_STRIPE_API_VERSION_CUSTOM', 'Custom'); +define('STRIPE_3DSECURE_DEFAULT', ''); +define('STRIPE_3DSECURE_DISABLED', ''); +define('STRIPE_3DSECURE_ALWAYS', 'always'); +define('STRIPE_3DSECURE_ONLY_IF_REQUIRED', 'only_if_required'); + +define('STRIPE_3DSECURE_CARD_NOT_SUPPORTED', 'not_supported'); +define('STRIPE_3DSECURE_CARD_OPTIONAL', 'optional'); +define('STRIPE_3DSECURE_CARD_REQUIRED', 'required'); + /** * Implements hook_init(). */ @@ -62,6 +71,20 @@ function commerce_stripe_menu() { 'file' => 'includes/commerce_stripe.admin.inc', ); + $items['commerce_stripe/redirect/%commerce_order'] = array( + 'page callback' => 'commerce_stripe_3dsecure_redirect', + 'page arguments' => array(2), + 'access arguments' => array('access checkout'), + 'type' => MENU_CALLBACK, + ); + + $items['commerce_stripe/confirm/%commerce_order'] = array( + 'page callback' => 'commerce_stripe_3dsecure_confirm', + 'page arguments' => array(2), + 'access arguments' => array('access checkout'), + 'type' => MENU_CALLBACK, + ); + return $items; } @@ -287,6 +310,41 @@ function commerce_stripe_settings_form($settings) { '#options' => array('elements' => t('Elements'), 'stripejs' => t('stripe.js'), 'checkout' => t('checkout')), '#default_value' => !empty($settings['integration_type']) ? $settings['integration_type'] : COMMERCE_STRIPE_DEFAULT_INTEGRATION, ); + + $form['elements_settings'] = array( + '#type' => 'fieldset', + '#title' => t('These settings are specific to "elements" integration type.'), + '#states' => array( + 'visible' => array( + ':input[name$="[integration_type]"]' => array('value' => 'elements'), + ), + ), + ); + // 3D Secure + $form['elements_settings']['3d_secure'] = array( + '#type' => 'select', + '#title' => t('3D Secure'), + '#description' => t( + 'If enabled, you can indicate when you would like 3D Secure to be used. '. + 'Contact Stripe support to enable the 3D Secure feature.' . + 'More details on this page (needs to be loggued): @url.
', + array('@url' => 'https://stripe.com/docs/3d-secure') + ), + '#options' => array( + STRIPE_3DSECURE_DISABLED => t('Disabled'), + STRIPE_3DSECURE_ALWAYS => t('Always'), + STRIPE_3DSECURE_ONLY_IF_REQUIRED => t('Only if required'), + ), + '#default_value' => !empty($settings['elements_settings']['3d_secure']) ? $settings['elements_settings']['3d_secure'] : STRIPE_3DSECURE_DEFAULT, + ); + // Postal Code + $form['elements_settings']['hide_postal_code'] = array( + '#type' => 'checkbox', + '#title' => t('Hide Postal code'), + '#description' => t('If enabled, postal code field will be hidden from form elements.'), + '#default_value' => !empty($settings['elements_settings']['hide_postal_code']) ? $settings['elements_settings']['hide_postal_code'] : FALSE, + ); + // Stripe Checkout specific settings. // @see: https://stripe.com/docs/checkout#integration-custom $form['checkout_settings'] = array( @@ -512,13 +570,12 @@ function commerce_stripe_submit_form($payment_method, $pane_values, $checkout_pa // Store them in Drupal.settings for easier access. drupal_add_js(array('commerce_stripe_address' => $address), array('type' => 'setting')); + // Add pay button. + $order_total = $order_wrapper->commerce_order_total->value(); + if ($integration_type === 'checkout' && isset($checkout_pane)) { $form = array(); - // Add pay button. - $order_wrapper = entity_metadata_wrapper('commerce_order', $order); - $order_total = $order_wrapper->commerce_order_total->value(); - // Add Checkout settings. $checkout_settings = $payment_method['settings']['checkout_settings']; @@ -555,6 +612,22 @@ function commerce_stripe_submit_form($payment_method, $pane_values, $checkout_pa } elseif ($integration_type === 'elements' && (empty($pane_values) || !is_numeric($pane_values['payment_details']['cardonfile']))) { $form = _commerce_stripe_elements_form(); + + // Initiating 3D Secure transaction. + if ($preference = commerce_stripe_3dsecure_order($order, $payment_method)) { + // Redirect requested. + $secure_3d_settings = [ + 'amount' => $order_total['amount'], + 'currency' => $payment_method['settings']['stripe_currency'], + 'redirect' => [ + 'return_url' => url('commerce_stripe/confirm/' . $order->order_id, ['absolute' => TRUE]), + ], + 'preference' => $preference, + ]; + + // Javascript var starting with a number is not supported. + drupal_add_js(['stripe' => ['secure_3d' => $secure_3d_settings]], 'setting'); + } } elseif ($integration_type === 'stripejs' && (empty($pane_values) || !is_numeric($pane_values['payment_details']['cardonfile']))) { $form = _commerce_stripe_credit_card_form(); @@ -578,11 +651,278 @@ function _commerce_stripe_credit_card_field_remove_name($content, $element) { return preg_replace($name_pattern, '', $content); } +/** + * Define if 3DSecure has to be enabled for this order depending of payment + * settings and 3DSecure support for the current card. + * + * @param stdClass $order + * The order to pay. + * @param array $payment_method + * The payment method used for the payement. + * @param string $card_supported + * Define if the card support 3DSecure or not. + * + * @return string + */ +function commerce_stripe_3dsecure_order($order, $payment_method, $card_supported = STRIPE_3DSECURE_CARD_OPTIONAL) { + $preference = STRIPE_3DSECURE_DISABLED; + + // If disable, we don't allow to change it. + if ($payment_method['settings']['integration_type'] == 'elements' && + !empty($payment_method['settings']['elements_settings']['3d_secure'])) { + $preference = $payment_method['settings']['elements_settings']['3d_secure']; + + if ($card_supported === STRIPE_3DSECURE_CARD_NOT_SUPPORTED) { + $preference = STRIPE_3DSECURE_DISABLED; + } + + // Let a chance to other modules to alter this preference. + drupal_alter('commerce_stripe_3dsecure_order', $preference, $order, $payment_method, $card_supported); + } + + return $preference; +} + +/** + * Page callback for 3D Secure redirect form. + */ +function commerce_stripe_3dsecure_redirect($order) { + // If the user does not have access to checkout the order, return a 404. We + // could return a 403, but then the user would know they've identified a + // potentially valid checkout URL. + if (!commerce_checkout_access($order)) { + return drupal_access_denied(); + } + + return drupal_get_form('commerce_stripe_3dsecure_redirect_form', $order); +} + +/** + * Payment method callback: form redirect to 3D Secure page. + */ +function commerce_stripe_3dsecure_redirect_form($form, &$form_state, $order, $token = '') { + if (!empty($order->data['stripe']['panel_values']['stripe_redirect'])) { + $form['#action'] = $order->data['stripe']['panel_values']['stripe_redirect']; + $form['#prefix'] = '
'; + $form['#suffix'] = '
'; + $form['#attached']['js'][] = drupal_get_path('module', 'commerce_payment') . '/commerce_payment.js'; + + $form['help']['#markup'] = '
' . t('Please wait while you are redirected to the payment server. If nothing happens within 10 seconds, please click on the button below.') . '
'; + + $form['pass_through'] = array( + '#type' => 'hidden', + '#value' => $token, + ); + + $form['submit'] = array( + '#type' => 'submit', + '#value' => t('Proceed with payment'), + ); + } + + return $form; +} + +/** + * @param $order + * @return mixed + */ +function commerce_stripe_3dsecure_confirm($order) { + // If the user does not have access to checkout the order, return a 404. We + // could return a 403, but then the user would know they've identified a + // potentially valid checkout URL. + if (!commerce_checkout_access($order)) { + return drupal_access_denied(); + } + + $status = (isset($_GET['status']) ? $_GET['status'] : 'failure'); + $error_message = (isset($_GET['error_message']) ? $_GET['error_message'] : ''); + + // Check client_secret to validate confirmation + if ($order->data['stripe']['3dsecure']['client_secret'] !== check_plain($_GET['client_secret'])) { + drupal_access_denied(); + return MENU_NOT_FOUND; + } + + if (!commerce_stripe_load_library()) { + return FALSE; + } + + // Get source object to check validation status + try { + $source = \Stripe\Source::retrieve(check_plain($_GET['source'])); + } + catch (Exception $e) { + // Display error to customer. + drupal_set_message(t('We received the following error validating 3DSecure card: :error + Please enter your information again or try a different card.', array(':error' => $e->getMessage())), 'error'); + // Log error. + watchdog('commerce_stripe', 'Following error received when validating 3DSecure card @stripe_error.', array('@stripe_error' => $e->getMessage()), WATCHDOG_NOTICE); + $order->revision = TRUE; + $order->log = t('Card processing error: @stripe_error', array('@stripe_error' => $e->getMessage())); + commerce_order_save($order); + + drupal_goto(commerce_checkout_order_uri($order)); + return FALSE; + } + + // Validation fail. + // Abort and display message. + if ($source->redirect->status !== 'succeeded' && !empty($source->redirect->failure_reason)) { + // Display error to customer. + drupal_set_message(t('We received the following error validating 3DSecure card: :error + Please enter your information again or try a different card.', array(':error' => $source->redirect->failure_reason)), 'error'); + // Log error. + watchdog('commerce_stripe', 'Following error received when validating 3DSecure card @stripe_error.', array('@stripe_error' => $source->redirect->failure_reason), WATCHDOG_NOTICE); + $order->revision = TRUE; + $order->log = t('Card processing error: @stripe_error', array('@stripe_error' => $source->redirect->failure_reason)); + commerce_order_save($order); + + drupal_goto(commerce_checkout_order_uri($order)); + } + + // Validation succeed, charge and complete the order. + if (isset($order->data['stripe']['panel_values']) && $source->redirect->status === 'succeeded') { + $payment_method = commerce_payment_method_instance_load($order->data['payment_method']); + $pane_form = array(); + $pane_values = $order->data['stripe']['panel_values']; + $charge = $order->data['stripe']['charge']; + + // Indicate 3D Secure has been called. + $pane_values['stripe_3dsecure'] = TRUE; + + // Generate customer as Source to be able to charge the Source later. + try { + $source = check_plain($_GET['source']); + $customer = \Stripe\Customer::create(array( + 'email' => $order->mail, + 'source' => $source, + )); + + $pane_values['stripe']['3dsecure_customer'] = $customer->id; + $pane_values['stripe']['3dsecure_source'] = $source; + // Empty stripe_token to next use $pane_values['stripe']['3dsecure_customer']. + $pane_values['stripe_token'] = ''; + } + catch (Exception $e) { + // Display error to customer. + drupal_set_message(t('We received the following error validating 3DSecure card: :error + Please enter your information again or try a different card.', array(':error' => $e->getMessage())), 'error'); + // Log error. + watchdog('commerce_stripe', 'Following error received when creating customer during 3DSecure card validation: @stripe_error.', array('@stripe_error' => $e->getMessage()), WATCHDOG_NOTICE); + $order->revision = TRUE; + $order->log = t('Card processing error during 3DSecure customer creation: @stripe_error', array('@stripe_error' => $e->getMessage())); + commerce_order_save($order); + + drupal_goto(commerce_checkout_order_uri($order)); + return FALSE; + } + + if (commerce_stripe_submit_form_submit($payment_method, $pane_form, $pane_values, $order, $charge) !== FALSE) { + // Current checkout page. + $order_status = commerce_order_status_load($order->status); + $checkout_page = commerce_checkout_page_load($order_status['checkout_page']); + + // If there is another checkout page... + if ($checkout_page['next_page']) { + // Update the order status to reflect the next checkout page. + $order = commerce_order_status_update($order, 'checkout_' . $checkout_page['next_page'], FALSE, NULL, t('Customer continued to the next checkout page after 3D Secure success.')); + + // If it happens to be the complete page, process completion now. + if ($checkout_page['next_page'] == 'complete') { + commerce_checkout_complete($order); + } + } + } + } + else { + watchdog('commerce_stripe', 'Following error received when processing 3D Secure response: "@stripe_error".', array('@stripe_error' => $error_message), WATCHDOG_NOTICE); + drupal_set_message(check_plain($error_message), 'error'); + } + + drupal_goto(commerce_checkout_order_uri($order)); +} + +/** + * Implements hook_form_alter(). + */ +function commerce_stripe_form_alter(&$form, &$form_state, $form_id) { + // Exit if the current form ID is for a checkout page form. + if (strpos($form_id, 'commerce_checkout_form_') !== 0 || + !commerce_checkout_page_load(substr($form_id, 23))) { + return; + } + + // Exit if the current page's form does no include the payment checkout pane. + if (empty($form['commerce_payment'])) { + return; + } + + // Exit if no payment method instance id. + if (empty($form['commerce_payment']['payment_method']['#default_value'])) { + return; + } + + // Exit if not using a new card. + if (module_exists('commerce_cardonfile')) { + if (isset($form_state['values']['commerce_payment']['payment_details']['cardonfile'])) { + if ($form_state['values']['commerce_payment']['payment_details']['cardonfile'] != 'new') { + // On radio change. + return; + } + } + elseif (isset($form['commerce_payment']['payment_details']['cardonfile']['#default_value']) && + $form['commerce_payment']['payment_details']['cardonfile']['#default_value'] != 'new') { + // Initial page load. + return; + } + } + + // Extract payment method instance id. + $payment_method = commerce_payment_method_instance_load($form['commerce_payment']['payment_method']['#default_value']); + + // Add submit handler. + if ($payment_method['base'] == 'commerce_stripe') { + if (isset($form['buttons']['continue'])) { + // Allow to bypass the automatic redirection to next checkout step. + // Replace the default callback 'commerce_checkout_form_submit' with a lighter version. + if (($position = array_search('commerce_checkout_form_submit', $form['buttons']['continue']['#submit'])) !== FALSE) { + $form['buttons']['continue']['#submit'][$position] = 'commerce_stripe_commerce_checkout_form_submit'; + } + else { + array_unshift($form['buttons']['continue']['#submit'], 'commerce_stripe_commerce_checkout_form_submit'); + } + } + } +} + +/** + * Define redirect on the global checkout form. + */ +function commerce_stripe_commerce_checkout_form_submit($form, &$form_state) { + $commerce_payment_values = $form_state['values']['commerce_payment']; + + if (!empty($commerce_payment_values['payment_details']['stripe_redirect'])) { + // Redirect to the 3D Secure autosubmit form. + $form_state['redirect'] = 'commerce_stripe/redirect/' . $form_state['order']->order_id; + } + else { + // Fallback on default form submit. + commerce_checkout_form_submit($form, $form_state); + } +} /** * Payment method callback: checkout form submission. */ function commerce_stripe_submit_form_submit($payment_method, $pane_form, $pane_values, $order, $charge) { + if (!commerce_stripe_load_library()) { + return FALSE; + } + + // Remove any previous submitted values. + unset($order->data['stripe']['panel_values'], $order->data['stripe']['charge']); + // If instructed to do so, try using the specified card on file. if (module_exists('commerce_cardonfile') && !empty($payment_method['settings']['cardonfile']) && !empty($pane_values['cardonfile']) && $pane_values['cardonfile'] !== 'new') { @@ -597,6 +937,65 @@ function commerce_stripe_submit_form_submit($payment_method, $pane_form, $pane_v return commerce_stripe_cardonfile_charge($payment_method, $card_data, $order, $charge); } + // In case we just validate 3Dsecure card, store it in stripe token and pass + // redirection. + if (!empty($pane_values['stripe']['3dsecure_customer'])) { + $pane_values['stripe_token'] = $pane_values['stripe']['3dsecure_customer']; + } + elseif (strpos($pane_values['stripe_token'], 'src_') === 0) { + $pane_values['stripe_redirect'] = url('commerce_stripe/confirm/' . $order->order_id, ['absolute' => TRUE]); + } + + // Store current pane to retry submission after 3D Secure confirmation. + if (!empty($pane_values['stripe_redirect']) && empty($pane_values['stripe_3dsecure'])) { + try { + // Get the current 3DSecure option for this card. + $source = \Stripe\Source::retrieve($pane_values['stripe_token']); + // Depending of module settings, define if we need to validate 3DSecure. + $stripe_3dsecure_required = FALSE; + drupal_alter('commerce_stripe_3dsecure_required', $stripe_3dsecure_required, $source); + + // 3Dsecure validation is needed for this payment. + if ($stripe_3dsecure_required) { + // 3DSecure is required, so create source to be redirected to 3DSecure + // validation process. + $source = \Stripe\Source::create([ + 'amount' => $charge['amount'], + 'currency' => $charge['currency_code'], + "type" => "three_d_secure", + "three_d_secure" => [ + "card" => $pane_values['stripe_token'], + ], + "redirect" => [ + "return_url" => url('commerce_stripe/confirm/' . $order->order_id, ['absolute' => TRUE]), + ], + ]); + + $order->data['stripe']['panel_values'] = $pane_values; + $order->data['stripe']['charge'] = $charge; + $order->data['stripe']['3dsecure']['client_secret'] = $source->client_secret; + commerce_order_save($order); + + drupal_goto($source->redirect->url, array('absolute' => TRUE)); + return TRUE; + } + } + catch (Exception $e) { + // Display error to customer. + drupal_set_message(t('We received the following error processing your card: :error + Please enter your information again or try a different card.', array(':error' => $e->getMessage())), 'error'); + // Log error. + watchdog('commerce_stripe', 'Following error received when processing card @stripe_error.', array('@stripe_error' => $e->getMessage()), WATCHDOG_NOTICE); + $order->revision = TRUE; + $order->log = t('Card processing error: @stripe_error', array('@stripe_error' => $e->getMessage())); + commerce_order_save($order); + + drupal_goto(commerce_checkout_order_uri($order)); + return FALSE; + + } + } + // The card is new. Either charge and forget, or charge and save. if (!commerce_stripe_load_library()) { drupal_set_message(t('Error making the payment. Please contact shop admin to proceed.'), 'error'); @@ -618,11 +1017,22 @@ function commerce_stripe_submit_form_submit($payment_method, $pane_form, $pane_v $c = array( 'amount' => $charge['amount'], 'currency' => $currency_code, - 'card' => $pane_values['stripe_token'], 'capture' => $txn_capture_bool, 'description' => $description, ); + // In case of 3Dsecure transaction and if card is chargeable, use it. + if (substr($pane_values['stripe_token'], 0, 4) === 'cus_') { + $c['customer'] = $pane_values['stripe']['3dsecure_customer']; + $c['source'] = $pane_values['stripe']['3dsecure_source']; + } + else if (substr($pane_values['stripe_token'], 0, 4) === 'src_') { + $c['source'] = $pane_values['stripe_token']; + } + else { + $c['card'] = $pane_values['stripe_token']; + } + // Specify that we want to send a receipt email if we are configured to do so. if (!empty($payment_method['settings']['receipt_email'])) { $c['receipt_email'] = $order->mail; @@ -639,14 +1049,29 @@ function commerce_stripe_submit_form_submit($payment_method, $pane_form, $pane_v if (module_exists('commerce_cardonfile') && !empty($payment_method['settings']['cardonfile']) && !empty($pane_values['credit_card']['cardonfile_store'])) { + $objects_tokens = array( + 'token' => $pane_values['stripe_token'], + ); + // In case of 3DSecure, we already have a source card object. + if (!empty($pane_values['stripe']['3dsecure_customer'])) { + $objects_tokens = array( + 'customer' => $pane_values['stripe']['3dsecure_customer'], + 'source' => $pane_values['stripe']['3dsecure_source'], + ); + } $user = user_load($order->uid); - $card = _commerce_stripe_create_card($pane_values['stripe_token'], $user, $payment_method); + $card = _commerce_stripe_create_card_from_tokens($objects_tokens, $user, $payment_method); // If the card is not declined or otherwise is error-free, we can save it. if ($card && !empty($card->id)) { $stripe_card_id = $card->id; $stripe_customer_id = $card->customer; - $c['card'] = $stripe_card_id; + if (substr($stripe_card_id, 0, 4) === 'src_') { + $c['source'] = $stripe_card_id; + } + else { + $c['card'] = $stripe_card_id; + } $c['customer'] = $stripe_customer_id; $save_card = TRUE; } @@ -657,14 +1082,12 @@ function commerce_stripe_submit_form_submit($payment_method, $pane_form, $pane_v $transaction->amount = $charge['amount']; $transaction->currency_code = $currency_code; - /* - * save the transaction as pending. this will cause an exception to be thrown - * if the transaction cannot be saved. this prevents the scenario where it - * can go all the way through the try/catch below with success in stripe but - * failing to ever save the transaction. saving the transaction here also acts as - * an early catch to prevent the stripe charge from going through if the Drupal - * side will be unable to save it for some reason. - */ + // Save the transaction as pending. this will cause an exception to be thrown + // if the transaction cannot be saved. this prevents the scenario where it + // can go all the way through the try/catch below with success in stripe but + // failing to ever save the transaction. saving the transaction here also acts as + // an early catch to prevent the stripe charge from going through if the Drupal + // side will be unable to save it for some reason. $transaction->status = COMMERCE_PAYMENT_STATUS_PENDING; if (!_commerce_stripe_commerce_payment_transaction_save($transaction)) { return FALSE; @@ -680,6 +1103,45 @@ function commerce_stripe_submit_form_submit($payment_method, $pane_form, $pane_v $transaction->status = commerce_stripe_get_txn_status($txn_capture_bool); $transaction->remote_status = commerce_stripe_get_remote_status($txn_capture_bool); _commerce_stripe_commerce_payment_transaction_save($transaction); + + // Set common attributes. + $transaction->remote_id = $response->id; + $transaction->payload[REQUEST_TIME] = $response->__toJSON(); + + // Does the card is 3DSecure and if 3DSecure is optional or required. + $three_d_secure_card_support = STRIPE_3DSECURE_CARD_NOT_SUPPORTED; + if (isset($response->source->{'three_d_secure'}->supported)) { + $three_d_secure_card_support = $response->source->{'three_d_secure'}->supported; + } + + // Check if 3D Secure is validated on this transaction (if required). + if (commerce_stripe_3dsecure_order($order, $payment_method, $three_d_secure_card_support)) { + // Refund if not succeeded. + if (!isset($response->source->{'3d_secure'}->status) || $response->source->{'3d_secure'}->status != 'succeeded') { + // Create the refund object. + $data = array( + 'amount' => $charge['amount'], + 'reason' => 'fraudulent', + ); + $initial_charge = Stripe\Charge::retrieve($response->id); + $refund = $initial_charge->refunds->create($data); + + // Add refund response to transaction logs. + $transaction->payload[REQUEST_TIME . '-refund'] = $refund->__toJSON(); + $transaction->message = t("Transaction rejected because 3D Secure hasn't been confirmed."); + $transaction->status = COMMERCE_PAYMENT_STATUS_FAILURE; + + drupal_set_message(check_plain($transaction->message), 'error'); + + _commerce_stripe_commerce_payment_transaction_save($transaction); + return FALSE; + } + } + + // Set success attributes. + $transaction->message = t('Payment completed successfully.'); + $transaction->status = COMMERCE_PAYMENT_STATUS_SUCCESS; + _commerce_stripe_commerce_payment_transaction_save($transaction); } // If the total is $0 we just put the card on file. } @@ -793,9 +1255,75 @@ function _commerce_stripe_create_card($stripe_token, $account, $payment_method, } } -function _commerce_stripe_save_cardonfile($card, $uid, $payment_method, $set_default, $billing_profile = NULL) { +/** + * Call Stripe to create card for a user depending of received tokens. + * + * @param array $tokens + * Tokens of objects. + * @param int $uid + * User identifier of the card owner. + * @param array $payment_method + * Array containing payment_method informations. + * + * @return bool + */ +function _commerce_stripe_create_card_from_tokens($tokens, $account, $payment_method, $throw_exceptions = FALSE) { + if (isset($tokens['token'])) { + return _commerce_stripe_create_card($tokens['token'], $account, $payment_method, $throw_exceptions); + } + + if (!commerce_stripe_load_library()) { + return FALSE; + } + + try { + $customer = Stripe\Customer::retrieve($tokens['customer']); + $card = $customer->sources->retrieve($tokens['source']); + return $card; + } + catch (Exception $e) { + drupal_set_message(t('We received the following error processing your card: %error. Please enter your information again or try a different card.', array('%error' => $e->getMessage())), 'error'); + watchdog('commerce_stripe', 'Following error received when adding a card to customer: @stripe_error.', array('@stripe_error' => $e->getMessage()), WATCHDOG_NOTICE); + if ($throw_exceptions) { + throw $e; + } + return FALSE; + } +} + +function _commerce_stripe_save_cardonfile($card_object, $uid, $payment_method, $set_default, $billing_profile = NULL) { + commerce_stripe_load_library(); + + $customer_id = (string) $card_object->customer; + $card_id = (string) $card_object->id; + if ($card_object->type === 'three_d_secure') { + // Set the real card as the source card. 3DSecure source is a single use. + $source = clone($card_object); + $source_card = \Stripe\Source::retrieve($source->three_d_secure->card); + // We cannot save 3DSecure card with required option since this source will + // be removed. + if ($source_card->card->three_d_secure === STRIPE_3DSECURE_CARD_REQUIRED) { + drupal_set_message(t('Your card cannot be saved since it requires 3DSecure.'), 'error'); + return FALSE; + } + $card_id = $source_card->id; + // Attach this source card to the customer. + $customer = Stripe\Customer::retrieve($customer_id); + $customer->sources->create(array('source' => $card_id)); + + // Finally get card information to store informations like expiration date + // in commerce_cardonfile entity. + $card = $source_card->card; + + } + elseif ($card_object->object === 'source') { + $card = $card_object->card; + } + else { + $card = $card_object; + } // Store the Stripe customer and card ids in the remote id field of {commerce_cardonfile} table - $remote_id = (string) $card->customer . '|' . (string) $card->id; + $remote_id = (string) $customer_id . '|' . (string) $card_id; // Populate and save the card $card_data = commerce_cardonfile_new(); @@ -822,10 +1350,9 @@ function _commerce_stripe_save_cardonfile($card, $uid, $payment_method, $set_def } if (!empty($set_default)) { - commerce_stripe_load_library(); try { - $customer = Stripe\Customer::retrieve($card->customer); - $customer->default_source = $card->id; + $customer = Stripe\Customer::retrieve($customer_id); + $customer->default_source = $card_id; $customer->save(); } catch (Exception $e) { @@ -833,7 +1360,7 @@ function _commerce_stripe_save_cardonfile($card, $uid, $payment_method, $set_def return FALSE; } } - watchdog('commerce_stripe', 'Stripe Customer Profile @profile_id created and saved to user @uid.', array('@profile_id' => (string) $card->customer, '@uid' => $uid)); + watchdog('commerce_stripe', 'Stripe Customer Profile @profile_id created and saved to user @uid.', array('@profile_id' => (string) $customer_id, '@uid' => $uid)); } /** @@ -924,10 +1451,18 @@ function commerce_stripe_cardonfile_charge($payment_method, $card_data, $order, 'amount' => $charge['amount'], 'currency' => $currency_code, 'customer' => $customer_id, - 'card' => $card_id, 'capture' => $txn_capture_bool, 'description' => $description, ); + // Act different if card is a Source Object or a Card object. + // Card object is herited from previous versions. Now Stripe deals with + // source. This is required for 3DSecure. + if (substr($card_id, 0, 4) == 'src_') { + $c['source'] = $card_id; + } + else { + $c['card'] = $card_id; + } commerce_stripe_add_metadata($c, $order); @@ -1161,6 +1696,7 @@ function _commerce_stripe_form_configure_stripe_checkout($checkout_settings, $ch */ function _commerce_stripe_form_configure_stripe_common(&$form, $stripe_token, $integration_type = COMMERCE_STRIPE_DEFAULT_INTEGRATION) { $public_key = _commerce_stripe_load_setting('public_key'); + $stripe_settings = array(); // Add stripe token field. This field is a container for token received from // Stripe API's response handler. @@ -1170,11 +1706,22 @@ function _commerce_stripe_form_configure_stripe_common(&$form, $stripe_token, $i '#default_value' => !empty($stripe_token) ? $stripe_token : '', ); + // This field is a container for 3D Secure redirect url. + $form['stripe_redirect'] = array( + '#type' => 'hidden', + '#attributes' => array('id' => 'stripe_redirect'), + '#default_value' => '', + ); + if ($integration_type === 'elements') { + $settings = _commerce_stripe_load_setting('elements_settings'); $form['#attached']['js'] = array( drupal_get_path('module', 'commerce_stripe') . '/commerce_stripe.elements.js' => array('preprocess' => FALSE, 'cache' => FALSE), ); + $stripe_settings = array( + 'hide_postal_code' => (bool) $settings['hide_postal_code'], + ); } elseif ($integration_type === 'checkout') { // Add external checkout.js library. @@ -1191,7 +1738,7 @@ function _commerce_stripe_form_configure_stripe_common(&$form, $stripe_token, $i $form['#attached']['js'][] = array( 'data' => array( - 'stripe' => array( + 'stripe' => $stripe_settings + array( 'publicKey' => trim($public_key), 'integration_type' => $integration_type, ), @@ -1482,16 +2029,24 @@ function commerce_stripe_cardonfile_update($form, &$form_state, $payment_method, * Card on file callback: deletes the associated customer payment profile. */ function commerce_stripe_cardonfile_delete($form, &$form_state, $payment_method, $card_data) { - if (!commerce_stripe_load_library()) { + if (!$library = commerce_stripe_load_library()) { return FALSE; } // Fetch the customer id and card id from $card_data->remote_id list($customer_id, $card_id) = explode('|', $card_data->remote_id); + if (empty($card_id) || empty($customer_id)) { + return TRUE; + } + try { $customer = Stripe\Customer::retrieve($customer_id); - $customer->sources->retrieve($card_id)->delete(); + // Delete is only available since v4.5.0 for source objects. + list($major, $minor,) = explode('.', $library['version']); + if (substr($card_id, 0, 4) !== 'src_' || ($major >= 4 && $minor >= 5)) { + $customer->sources->retrieve($card_id)->delete(); + } return TRUE; } catch (Exception $e) { @@ -1510,7 +2065,7 @@ function commerce_stripe_load_library() { watchdog('commerce_stripe', 'Failure to load Stripe API PHP Client Library. Please see the Status Report for more.', array(), WATCHDOG_CRITICAL); return FALSE; } - return TRUE; + return $library; } /** @@ -1859,3 +2414,40 @@ function commerce_stripe_get_txn_message_success($txn_capture_bool) { function commerce_stripe_get_txn_status($txn_capture_bool) { return ($txn_capture_bool) ? COMMERCE_PAYMENT_STATUS_SUCCESS : COMMERCE_PAYMENT_STATUS_PENDING; } + +/** + * Define if 3DSecure is required depending of the source card and payment + * method settings. + * + * @param boolean $required + * The data to alter which define if 3DSecure is required. + * @param $source + * The source object from Stripe containing card data. + * + * @return bool + * Does 3DSecure is required for this payment and Source. + */ +function commerce_stripe_commerce_stripe_3dsecure_required_alter(&$required, $source) { + $stripe_settings = _commerce_stripe_load_settings(); + // 3DSecure is not supported by the card or not required by module settings, + // so not required. + if ($source->card->three_d_secure === STRIPE_3DSECURE_CARD_NOT_SUPPORTED || $stripe_settings['elements_settings']['3d_secure'] === STRIPE_3DSECURE_DISABLED) { + return $required = FALSE; + } + + // Module settings define 3DSecure as required. Only left at this state cards + // allowing 3DSecure, so checking is required. + if ($stripe_settings['elements_settings']['3d_secure'] === STRIPE_3DSECURE_ALWAYS) { + return $required = TRUE; + } + + // The only other state for module settings is + // STRIPE_3DSECURE_ONLY_IF_REQUIRED. + // Require 3DSecure only if card requires it. + if ($source->card->three_d_secure === STRIPE_3DSECURE_CARD_REQUIRED) { + return $required = TRUE; + } + + // Other cases should not require 3DSecure. + return $required = FALSE; +} diff --git a/includes/commerce_stripe.admin.inc b/includes/commerce_stripe.admin.inc index ab15708..1e62d77 100644 --- a/includes/commerce_stripe.admin.inc +++ b/includes/commerce_stripe.admin.inc @@ -23,7 +23,7 @@ function commerce_stripe_refund_form($form, &$form_state, $order, $transaction) // Make sure we can load the original charge object. try { - \Stripe\Stripe::setApiKey(trim($payment_method['settings']['secret_key'])); + Stripe\Stripe::setApiKey(trim($payment_method['settings']['secret_key'])); $charge = Stripe\Charge::retrieve($transaction->remote_id); $form_state['stripe_charge'] = $charge; } -- 2.14.3 (Apple Git-98)