diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000..6a87396 Binary files /dev/null and b/.DS_Store differ diff --git a/README.txt b/README.txt index 2e42089..2602c41 100644 --- a/README.txt +++ b/README.txt @@ -1,4 +1,8 @@ -This is an Ubercart payment gateway module for Stripe. +This is an Ubercart payment gateway module for Stripe. It maintains PCI SAQ A +compliance which allows Stripe, the payment processor, to handle prcoessing and +storing of payment card details. + +It is compliant with 3D Secure, 3D Secure 2, and Strong Customer Authentication (SCA) Versions of the Stripe PHP Library and Stripe API that this module currently supports are found in uc_stripe_libraries_info() in uc_stripe.module. @@ -15,9 +19,9 @@ section, and enable the gateway under the Payment Gateways. c) On that page, provide your Stripe API keys, from https://dashboard.stripe.com/account/apikeys -d) Download and install the Stripe PHP Library version 2.2.0 or >=3.13.0 -from https://github.com/stripe/stripe-php/releases. The recommended technique is -to use the command +d) Download and install the Stripe PHP Library version 6.38.0 with stripe api +2019-05-16 or newer from https://github.com/stripe/stripe-php/releases. The +recommended technique is to use the command drush ldl stripe @@ -25,8 +29,7 @@ If you don't use "drush ldl stripe", download and install the Stripe library in sites/all/libraries/stripe such that the path to VERSION is sites/all/libraries/stripe/VERSION. YOU MUST CLEAR THE CACHE AFTER CHANGING THE STRIPE PHP LIBRARY. The Libraries module caches its memory of -libraries like the Stripe Library. (Version 2.2.0 support is maintained for -existing users; version 3.13.0+ supports PHP 7 and will get ongoing support.) +libraries like the Stripe Library. (With the latest version of the libraries module you can use the command: e) If you are using recurring payments, install version 2.x @@ -48,14 +51,35 @@ disabled on admin/store/settings/payment/method/credit - uc_credit never sees the credit card number, so cannot properly validate it (and we don't want it to ever know the credit card number.) -Upgrading from uc_stripe 6.x-1.x or 7.x-1.x +i) uc_stripe creates it's own payment pane. Ensure the correct ordering by visiting +store->configuration->checkout (admin/store/settings/checkout). + +Upgrading from uc_stripe 7.x-2.x =========================================== -7.x-2.x does not use Stripe subscriptions for recurring payments, but instead -uses the uc_recurring module. This means you have control of recurring -transactions without having to manage them on the Stripe dashboard. (Credit -card numbers and sensitive data are *not* stored on your site; only the Stripe -customer ID is stored.) +7.x-3.x maintains PCI SAQ A compliance and has major implementation changes from +2.x. This version uses it's own payment pane in uc_cart to collect card info. +The card fields such as card number, expiration date, and cvc code have all been + hidden, and is handled entirely by Stripe's Element implementation. +Which means no credit card information gets processed at all by drupal. The last4, +and expiration date are sent back to drupal by Stripe's api. + +7.x-3.x no longer creates a new stripe customer for each order. If a drupal user +already has a stripe customer ID, this module will attach future orders to that +exisiting stripe customer ID. + +When upgrading from 2.x the ordering of the new stripe payment pane should be +verified at store->configuration->checkout (admin/store/settings/checkout). + +An upgrade of the stripe library is required. See installation step d from above. + +7.x-3.x Uses the uc_recurring module for recurring payments. It is also equipped +to handle recurring payments that require authentication (See the uc_recurring +steps below). Exisiting recurring payments set up with 7.x-2.x work without any +configuration changes. + +Upgrading from uc_stripe 6.x-1.x or 7.x-1.x +============================================ The upgrade hooks, however, must move the customer id stored in the obsolete uc_recurring_stripe table into the user table. When this happens the old @@ -84,6 +108,11 @@ Recurring payments require automatically triggered renewals using uc_recurring_trigger_renewals ("Enabled triggered renewals" must be enabled on admin/store/settings/payment/edit/recurring) +You should also set your email message for recurring payments that require +Authentication. The system will email your customers with a link so that they +can authenticate and have their payment processed. +(You can edit from here: admin/store/settings/payment/edit/gateways) + If you were using Stripe subscriptions in v1 of this module, you may have to disable those subscriptions in order to not double-charge your customers. diff --git a/css/uc_stripe.css b/css/uc_stripe.css index 0f5fa0a..140c888 100644 --- a/css/uc_stripe.css +++ b/css/uc_stripe.css @@ -30,3 +30,5 @@ a.poweredbylink:hover { #uc_stripe_messages.hidden {display: none;} .stripe-warning {color: red; font-style: oblique; } + +#edit-panes-payment-details-stripe-card-element{max-width: 600px} diff --git a/js/uc_stripe.js b/js/uc_stripe.js index d30b53a..f787c41 100644 --- a/js/uc_stripe.js +++ b/js/uc_stripe.js @@ -8,164 +8,186 @@ Drupal.behaviors.uc_stripe = { attach: function (context) { - + + // Once function prevents stripe from reloading. Any dom changes to stripe area will destroy element + // as a Stripe security feature + $('#uc-cart-checkout-form', context).once('uc_stripe', function(){ + + var stripe_card_element = '#stripe-card-element'; + + if (Drupal.settings && Drupal.settings.uc_stripe ) { + var apikey = Drupal.settings.uc_stripe.apikey; + + var stripe = Stripe(apikey); + var elements = stripe.elements(); + } + + // Map stripe names to (partial) Ubercart field names; Ubercart names add "billing_" or "shipping_" on the front. - const address_field_mapping = { - "address_line1": "street1", - "address_line2": "street2", - "address_city": "city", - "address_state": "zone", - "address_zip": "postal_code", - "address_country": "country" - }; - var submitButton = $('.uc-cart-checkout-form #edit-continue'); + const address_field_mapping = { + "address_line1": "street1", + "address_line2": "street2", + "address_city": "city", + "address_state": "zone", + "address_zip": "postal_code", + "address_country": "country" + }; + var submitButton = $('.uc-cart-checkout-form #edit-continue'); - var cc_container = $('.payment-details-credit'); - var cc_num = cc_container.find(':input[id*="edit-panes-payment-details-cc-numbe"]'); - var cc_cvv = cc_container.find(':input[id*="edit-panes-payment-details-cc-cv"]'); + // Load the js reference to these fields so that on the review page + // we can input the last 4 and expiration date which is returned to us by stripe paymentMethod call + var cc_container = $('.payment-details-credit'); + var cc_num = cc_container.find(':input[id*="edit-panes-payment-details-cc-numbe"]'); + var cc_cvv = cc_container.find(':input[id*="edit-panes-payment-details-cc-cv"]'); + var cc_exp_month = cc_container.find('#edit-panes-payment-details-cc-exp-month'); + var cc_exp_year = cc_container.find('#edit-panes-payment-details-cc-exp-year'); + + // Make sure that when the page is being loaded the paymentMethod value is reset + // Browser or other caching might do otherwise. + $("[name='panes[payment-stripe][details][stripe_payment_method]']").val('default'); - // Make sure that when the page is being loaded the token value is reset - // Browser or other caching might do otherwise. - $("[name='panes[payment][details][stripe_token]']").val('default'); + // JS must enable the button; otherwise form might disclose cc info. It starts disabled + submitButton.attr('disabled', false); - $('span#stripe-nojs-warning').parent().hide(); - - // JS must enable the button; otherwise form might disclose cc info. It starts disabled - submitButton.attr('disabled', false); - - // When this behavior fires, we can clean the form so it will behave properly, - // Remove 'name' from sensitive form elements so there's no way they can be submitted. - cc_num.removeAttr('name').removeAttr('disabled'); - $('div.form-item-panes-payment-details-cc-number').removeClass('form-disabled'); - cc_cvv.removeAttr('name').removeAttr('disabled'); - var cc_val_val = cc_num.val(); - if (cc_val_val && cc_val_val.indexOf('Last 4')) { - cc_num.val(''); - } - - submitButton.click(function (e) { - - // We must find the various fields again, because they may have been swapped - // in by ajax action of the form. - cc_container = $('.payment-details-credit'); - cc_num = cc_container.find(':input[id*="edit-panes-payment-details-cc-numbe"]'); - cc_cvv = cc_container.find(':input[id*="edit-panes-payment-details-cc-cv"]'); - - // If not credit card processing or no token field, just let the submit go on - // Also continue if we've received the tokenValue - var tokenField = $("[name='panes[payment][details][stripe_token]']"); - if (!$("div.payment-details-credit").length || !tokenField.length || tokenField.val().indexOf('tok_') == 0) { - return true; + // When this behavior fires, we can clean the form so it will behave properly, + // Remove 'name' from sensitive form elements so there's no way they can be submitted. + cc_num.removeAttr('name').removeAttr('disabled'); + $('div.form-item-panes-payment-details-cc-number').removeClass('form-disabled'); + cc_cvv.removeAttr('name').removeAttr('disabled'); + var cc_val_val = cc_num.val(); + if (cc_val_val && cc_val_val.indexOf('Last 4')) { + cc_num.val(''); } - - // If we've requested and are waiting for token, prevent any further submit - if (tokenField.val() == 'requested') { - return false; // Prevent any submit processing until token is received - } - - // Go ahead and request the token - tokenField.val('requested'); - - try { - var name = undefined; - - if ($(':input[name="panes[billing][billing_first_name]"]').length) { - name = $(':input[name="panes[billing][billing_first_name]"]').val() + " " + $(':input[name="panes[billing][billing_last_name]"]').val(); + + + // Custom styling can be passed to options when creating an Element. + var style = { + base: { + // Add your base input styles here. For example: + fontSize: '24px', + color: "#000000", + iconColor: "blue", } - if (typeof name === "undefined" && $(':input[name="panes[delivery][delivery_first_name]"]').length) { - name = $(':input[name="panes[delivery][delivery_first_name]"]').val() + " " + $(':input[name="panes[delivery][delivery_last_name]"]').val(); + }; + + // Create an instance of the card Element. + var card = elements.create('card', {style: style}); + + // Add an instance of the card Element into the #stripe-card-element
. + card.mount(stripe_card_element); + + // Display errors from stripe + card.addEventListener('change', function(event) { + var displayError = document.getElementById('uc_stripe_messages'); + if (event.error) { + displayError.textContent = event.error.message; + console.log(event.error.message) + } else { + displayError.textContent = ''; + } + }); + + submitButton.click(function (e) { + + // We must find the various fields again, because they may have been swapped + // in by ajax action of the form. + cc_container = $('.payment-details-credit'); + cc_num = cc_container.find(':input[id*="edit-panes-payment-details-cc-numbe"]'); + cc_cvv = cc_container.find(':input[id*="edit-panes-payment-details-cc-cv"]'); + cc_exp_year = cc_container.find('#edit-panes-payment-details-cc-exp-month'); + cc_exp_month = cc_container.find('#edit-panes-payment-details-cc-exp-year'); + + // If not credit card processing or no payment method field, just let the submit go on + // Also continue if we've received the tokenValue + var paymentMethodField = $("[name='panes[payment-stripe][details][stripe_payment_method]']"); + if (!$("div.payment-details-credit").length || !paymentMethodField.length || paymentMethodField.val().indexOf('pm_') == 0) { + return true; } - var params = { - number: cc_num.val(), - cvc: cc_cvv.val(), - exp_month: $(':input[name="panes[payment][details][cc_exp_month]"]').val(), - exp_year: $(':input[name="panes[payment][details][cc_exp_year]"]').val(), - name: name - }; + // If we've requested and are waiting for token, prevent any further submit + if (paymentMethodField.val() == 'requested') { + return false; // Prevent any submit processing until token is received + } - // Translate the Ubercart billing/shipping fields to Stripe values - for (var key in address_field_mapping) { - const prefixes = ['billing', 'delivery']; - for (var i = 0; i < prefixes.length; i++) { - var prefix = prefixes[i]; - var uc_field_name = prefix + '_' + address_field_mapping[key]; - var location = ':input[name="panes[' + prefix + '][' + uc_field_name + ']"]'; - if ($(location).length) { - params[key] = $(location).val(); - if ($(location).attr('type') == 'select-one') { - params[key] = $(location + " option:selected").text(); - } - break; // break out of billing/shipping loop because we got the info + // Go ahead and request the token + paymentMethodField.val('requested'); + + try { + + stripe.createPaymentMethod('card', card).then(function (response) { + + if (response.error) { + + // Show the errors on the form + $('#uc_stripe_messages') + .removeClass("hidden") + .text(response.error.message); + $('#edit-stripe-messages').val(response.error.message); + + // Make the fields visible again for retry + cc_num + .css('visibility', 'visible') + .val('') + .attr('name', 'panes[payment][details][cc_number]'); + cc_cvv + .css('visibility', 'visible') + .val('') + .attr('name', 'panes[payment][details][cc_cvv]'); + + + // Turn off the throbber + $('.ubercart-throbber').remove(); + // Remove the bogus copy of the submit button added in uc_cart.js ucSubmitOrderThrobber + submitButton.next().remove(); + // And show the hidden original button which has the behavior attached to it. + submitButton.show(); + + paymentMethodField.val('default'); // Make sure token field set back to default + + } else { + // token contains id, last4, and card type + var paymentMethodId = response.paymentMethod.id; + + + // Insert the token into the form so it gets submitted to the server + paymentMethodField.val(paymentMethodId); + + // set cc expiration date received from stripe so that it is available on checkout review + cc_exp_year.val(response.paymentMethod.card.exp_month); + cc_exp_month.val(response.paymentMethod.card.exp_year); + + // Since we're now submitting, make sure that uc_credit doesn't + // find values it objects to; after "fixing" set the name back on the + // form element. + // add dummy tweleve 5's and the last 4 of credit card so that last 4 show + cc_num + .css('visibility', 'hidden') + .val('555555555555' + response.paymentMethod.card.last4) + .attr('name', 'panes[payment][details][cc_number]'); + cc_cvv + .css('visibility', 'hidden') + .val('999') + .attr('name', 'panes[payment][details][cc_cvv]'); + + // now actually submit to Drupal. The only "real" things going + // are the token and the expiration date and last 4 of cc + submitButton.click(); } - } + }); + } catch (e) { + $('#uc_stripe_messages') + .removeClass("hidden") + .text(e.message); + $('#edit-stripe-messages').val(e.message); } - Stripe.createToken(params, function (status, response) { - - if (response.error) { - - // Show the errors on the form - $('#uc_stripe_messages') - .removeClass("hidden") - .text(response.error.message); - $('#edit-stripe-messages').val(response.error.message); - - // Make the fields visible again for retry - cc_num - .css('visibility', 'visible') - .val('') - .attr('name', 'panes[payment][details][cc_number]'); - cc_cvv - .css('visibility', 'visible') - .val('') - .attr('name', 'panes[payment][details][cc_cvv]'); - - - // Turn off the throbber - $('.ubercart-throbber').remove(); - // Remove the bogus copy of the submit button added in uc_cart.js ucSubmitOrderThrobber - submitButton.next().remove(); - // And show the hidden original button which has the behavior attached to it. - submitButton.show(); - - tokenField.val('default'); // Make sure token field set back to default - - } else { - // token contains id, last4, and card type - var token = response.id; - - // Insert the token into the form so it gets submitted to the server - tokenField.val(token); - - // Since we're now submitting, make sure that uc_credit doesn't - // find values it objects to; after "fixing" set the name back on the - // form element. - cc_num - .css('visibility', 'hidden') - .val('555555555555' + response.card.last4) - .attr('name', 'panes[payment][details][cc_number]'); - cc_cvv - .css('visibility', 'hidden') - .val('999') - .attr('name', 'panes[payment][details][cc_cvv]'); - - // now actually submit to Drupal. The only "real" things going - // are the token and the expiration date. - submitButton.click(); - } - }); - } catch (e) { - $('#uc_stripe_messages') - .removeClass("hidden") - .text(e.message); - $('#edit-stripe-messages').val(e.message); - } - - // Prevent processing until we get the token back - return false; + // Prevent processing until we get the token back + return false; + }); }); - } + + }, + }; }(jQuery)); diff --git a/js/uc_stripe_process_payment.js b/js/uc_stripe_process_payment.js new file mode 100644 index 0000000..23f9384 --- /dev/null +++ b/js/uc_stripe_process_payment.js @@ -0,0 +1,91 @@ +/** + * @file + * uc_stripe.js + * + * Handles all interactions with Stripe on the client side for PCI-DSS compliance + */ +(function ($) { + + Drupal.behaviors.uc_stripe_process_payment = { + attach: function (context) { + + $('#uc-cart-checkout-review-form, #uc-stripe-authenticate-payment-form', context).once('uc_stripe', function(){ + + if (Drupal.settings && Drupal.settings.uc_stripe ) { + var apikey = Drupal.settings.uc_stripe.apikey; + var methodId = Drupal.settings.uc_stripe.methodId; + var orderId = Drupal.settings.uc_stripe.orderId + var stripe = Stripe(apikey); + } + + var submitButton = $('#edit-submit'); + var processed = false; + + submitButton.click(function (e) { + if(!processed){ + e.preventDefault(); + + $.ajax({ + url: '/uc_stripe/ajax/confirm_payment', + type: "POST", + data: JSON.stringify({ payment_method_id: methodId, order_id: orderId }), + contentType: 'application/json;', + dataType: 'json', + success: function(result){ + handleServerResponse(result); + }, + error: function(result){ + handleServerResponse(result); + } + }) + + } + + }); + + function handleServerResponse(response) { + if (response.error) { + processed = true; + submitButton.click(); + // Show error from server on payment form + } else if (response.requires_action) { + // Use Stripe.js to handle required card action + stripe.handleCardAction( + response.payment_intent_client_secret + ).then(function(result) { + if (result.error) { + // Show error in payment form + processed = true; + submitButton.click(); + } else { + // The card action has been handled + // The PaymentIntent can be confirmed again on the server + $.ajax({ + url: '/uc_stripe/ajax/confirm_payment', + type: 'POST', + data: JSON.stringify({ payment_intent_id: result.paymentIntent.id, order_id: orderId }), + contentType: 'application/json;', + dataType: 'json', + success: function(confirmResult){ + return handleServerResponse(confirmResult); + }, + error: function(confirmResult){ + return handleServerResponse(confirmResult); + }, + }) + } + }); + } else { + // Show success message + processed = true; + submitButton.click(); + } + } + + }); + + }, + + }; + +}(jQuery)); diff --git a/uc_stripe.info b/uc_stripe.info index a7be141..ab65542 100644 --- a/uc_stripe.info +++ b/uc_stripe.info @@ -5,12 +5,4 @@ dependencies[] = uc_credit dependencies[] = libraries package = Ubercart - payment core = 7.x -php = 5.3 - - -; Information added by Drupal.org packaging script on 2017-05-19 -version = "7.x-2.2+2-dev" -core = "7.x" -project = "uc_stripe" -datestamp = "1495159090" - +php = 5.3 \ No newline at end of file diff --git a/uc_stripe.install b/uc_stripe.install index 95d8d0f..4f9fcc9 100644 --- a/uc_stripe.install +++ b/uc_stripe.install @@ -92,6 +92,57 @@ function uc_stripe_install() { variable_set('uc_credit_validate_numbers', FALSE); } +/** + * Implements hook_uninstall(). + */ +function uc_stripe_uninstall() { + variable_del('uc_stripe_authenticaiton_required_email'); +} + +/** + * Implements hook_schema(). + */ +function uc_stripe_schema() { + $schema['uc_stripe_pending_auth'] = array( + 'description' => 'Ubercart Stripe - Track orders pending authentication', + 'fields' => array( + 'id' => array( + 'description' => 'id of entry', + 'type' => 'serial', + 'not null' => TRUE + ), + 'order_id' => array( + 'description' => 'Order Id of pending order', + 'type' => 'int', + 'not null' => TRUE + ), + 'rfee_id' => array( + 'description' => 'Recurring Fee Id of pending order', + 'type' => 'int', + 'not null' => TRUE + ), + 'completed' => array( + 'description' => 'Competion status of this pending order', + 'type' => 'int', + 'not null' => TRUE + ), + 'hash' => array ( + 'description' => 'The unqiue has of order and payment id', + 'type' => 'varchar', + 'length' => '100', + 'not null' => TRUE + ), + ), + 'unique keys' => array( + 'hash' => array( + 'hash' + ) + ), + 'primary key' => array('id'), + ); + return $schema; +} + /** * Enable triggered renewals, as uc_recurring manages renewals with this version. */ @@ -181,3 +232,52 @@ function _uc_stripe_move_customer_id(&$sandbox) { $sandbox['#finished'] = $sandbox['progress'] / $sandbox['max']; } } + +/** + * + * create table to track orders that require extra authentication verification. + */ +function uc_stripe_update_7301() { + + $table = array( + 'description' => 'Ubercart Stripe - Track orders pending authentication', + 'fields' => array( + 'id' => array( + 'description' => 'id of entry', + 'type' => 'serial', + 'not null' => TRUE + ), + 'order_id' => array( + 'description' => 'Order Id of pending order', + 'type' => 'int', + 'not null' => TRUE + ), + 'rfee_id' => array( + 'description' => 'Recurring Fee Id of pending order', + 'type' => 'int', + 'not null' => TRUE + ), + 'completed' => array( + 'description' => 'Competion status of this pending order', + 'type' => 'int', + 'not null' => TRUE + ), + 'hash' => array( + 'description' => 'The unqiue has of order and payment id', + 'type' => 'varchar', + 'length' => '100', + 'not null' => TRUE + ) + ), + 'unique keys' => array( + 'hash' => array( + 'hash' + ) + ), + 'primary key' => array( + 'id' + ) + ); + + db_create_table('uc_stripe_pending_auth', $table); +} diff --git a/uc_stripe.mail.inc b/uc_stripe.mail.inc new file mode 100644 index 0000000..bc9f875 --- /dev/null +++ b/uc_stripe.mail.inc @@ -0,0 +1,62 @@ + $params['user'], 'authentication_key' => $params['hash']); + + $message['body'][]= token_replace($params['body'], $variables, array('language' => language_default(), 'callback' => 'uc_stripe_mail_tokens', 'sanitize' => FALSE, 'clear' => TRUE)); + + break; + } + +} \ No newline at end of file diff --git a/uc_stripe.module b/uc_stripe.module index de0f247..1473b36 100644 --- a/uc_stripe.module +++ b/uc_stripe.module @@ -14,83 +14,57 @@ * * @return mixed */ -function uc_stripe_libraries_info() { +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/v3.20.0.tar.gz', + '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( - '2.2.0' => array( - 'files' => array( - 'php' => array( - 'lib/Util/RequestOptions.php', - 'lib/Util/Set.php', - 'lib/Util/Util.php', - 'lib/Object.php', - 'lib/ApiResource.php', - 'lib/Account.php', - 'lib/ExternalAccount.php', - 'lib/AlipayAccount.php', - 'lib/ApiRequestor.php', - 'lib/ApplicationFee.php', - 'lib/ApplicationFeeRefund.php', - 'lib/AttachedObject.php', - 'lib/SingletonApiResource.php', - 'lib/Balance.php', - 'lib/BalanceTransaction.php', - 'lib/BankAccount.php', - 'lib/BitcoinReceiver.php', - 'lib/BitcoinTransaction.php', - 'lib/Card.php', - 'lib/Charge.php', - 'lib/Collection.php', - 'lib/Coupon.php', - 'lib/Customer.php', - 'lib/Error/Base.php', - 'lib/Error/Api.php', - 'lib/Error/ApiConnection.php', - 'lib/Error/Authentication.php', - 'lib/Error/Card.php', - 'lib/Error/InvalidRequest.php', - 'lib/Error/RateLimit.php', - 'lib/Event.php', - 'lib/FileUpload.php', - 'lib/HttpClient/ClientInterface.php', - 'lib/HttpClient/CurlClient.php', - 'lib/Invoice.php', - 'lib/InvoiceItem.php', - 'lib/Plan.php', - 'lib/Recipient.php', - 'lib/Refund.php', - 'lib/Stripe.php', - 'lib/Subscription.php', - 'lib/Token.php', - 'lib/Transfer.php', - 'lib/TransferReversal.php', - ), - ), - 'stripe_api_version' => '2015-06-15', - ), - '3.0' => array( + '6.38.0' => array( 'files' => array( 'php' => array( 'init.php', - ), + ) ), - 'stripe_api_version' => '2016-03-07', - ), + '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', + ); + + $items['stripe/authenticate-payment/%'] = array( + 'access callback' => true, + 'page callback' => 'drupal_get_form', + 'page arguments' => array('uc_stripe_authenticate_payment_form', 2), + 'file' => 'uc_stripe.pages.inc' + ); + + + return $items; +} + /** * Implements hook_payment_gateway to register this payment gateway * @return array @@ -131,6 +105,47 @@ function uc_stripe_recurring_info() { 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'){ + 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; + } + + // When a payment fails, remove the Submit Order button because it will most + // likely fail again. Instead, the customer should hit back to try again. + if(isset($_SESSION['stripe']['payment_failed'])){ + $form['actions']['submit']['#type'] = 'hidden'; + } + + $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 @@ -142,37 +157,49 @@ function uc_stripe_recurring_info() { */ function uc_stripe_form_uc_cart_checkout_form_alter(&$form, &$form_state) { + $form['panes']['payment-stripe']['#attached']['css'][] = array( + 'data' => '#payment-stripe-pane { display: none; }', + 'type' => 'inline', + ); + + $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']; - $payment_form['stripe_nojs_warning'] = array( - '#type' => 'item', - '#markup' => '' . t('Sorry, for security reasons your card cannot be processed because Javascript is disabled in your browser.') . '', - '#weight' => -1000, + // 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' => '
', + '#weight' => - 10, + '#markup' => '
' . 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.') . '
', + '#suffix' => '
', ); // Powered by Stripe (logo from https://stripe.com/about/resources) if (variable_get('uc_stripe_poweredby', FALSE)) { $payment_form['field_message'] = array( '#type' => 'item', - '#markup' => "Powered by Stripe", + '#markup' => "Powered by Stripe", '#weight' => 1, ); } - $payment_form['stripe_token'] = array( + // 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-payment-details-stripe-token', + 'id' => 'edit-panes-stripe-payment-details-stripe-payment-method', ), ); - // Prevent form Credit card fill and submission if javascript has not removed - // the "disabled" attributes.. - // If JS happens to be disabled, we don't want user to be able to enter CC data. - // Note that we can't use '#disabled', as it causes Form API to discard all input, - // so use the disabled attribute instead. - $form['panes']['payment']['details']['cc_number']['#attributes']['disabled'] = 'disabled'; if (empty($form['actions']['continue']['#attributes'])) { $form['actions']['continue']['#attributes'] = array(); } @@ -180,13 +207,38 @@ function uc_stripe_form_uc_cart_checkout_form_alter(&$form, &$form_state) { $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', '')); - // Add custom JS and CSS - $form['#attached']['js']['https://js.stripe.com/v2/'] = array('type' => 'external'); - $form['#attached']['js'][] = array('data' => "Stripe.setPublishableKey('$apikey')", 'type' => 'inline'); + $settings = array('apikey' => $apikey); + $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'; @@ -195,6 +247,11 @@ function uc_stripe_form_uc_cart_checkout_form_alter(&$form, &$form_state) { '#markup' => "", ); + //Clear any previous card payment failures + if(isset($_SESSION['stripe']['payment_failed'])){ + unset($_SESSION['stripe']['payment_failed']); + } + if (uc_credit_default_gateway() == 'uc_stripe') { if (variable_get('uc_stripe_testmode', TRUE)) { $form['panes']['testmode'] = array( @@ -226,6 +283,51 @@ function uc_stripe_uc_order_pane() { 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() * @@ -237,12 +339,15 @@ function uc_stripe_uc_order_pane() { function uc_stripe_uc_checkout_complete($order, $account) { if ($order->payment_method == "credit") { - // Pull the stripe customer ID from the session. + // Pull the stripe payment method ID from the session. // It got there in uc_stripe_checkout_form_customsubmit() - $stripe_customer_id = $_SESSION['stripe']['customer_id']; + $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))); } } @@ -309,6 +414,15 @@ function uc_stripe_settings_form() { '#description' => t('Your Live Stripe API Key. Must be the "publishable" key, not the "secret" one.'), ); + $email_text = _uc_stripe_get_authentication_required_email_text(); + + $form['uc_stripe_settings']['uc_stripe_authenticaiton_required_email'] = array( + '#type' => 'textarea', + '#title' => t('Email for Recurring payment authentication'), + '#default_value' => variable_get('uc_stripe_authenticaiton_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'), @@ -323,20 +437,6 @@ function uc_stripe_settings_form() { '#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; } @@ -410,8 +510,8 @@ function _uc_stripe_validate_key($key) { */ 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']['details']['stripe_token'])) { - $_SESSION['stripe']['token'] = $form_state['values']['panes']['payment']['details']['stripe_token']; + 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']; } } @@ -457,85 +557,6 @@ function uc_stripe_charge($order_id, $amount, $data) { // Format the amount in cents, which is what Stripe wants $amount = uc_currency_format($amount, FALSE, FALSE, FALSE); - $stripe_customer_id = FALSE; - - // If the user running the order is not the order's owner - // (like if an admin is processing an order on someone's behalf) - // then load the customer ID from the user object. - // Otherwise, make a brand new customer each time a user checks out. - if ($user->uid != $order->uid) { - $stripe_customer_id = _uc_stripe_get_customer_id($order->uid); - } - - - // Always Create a new customer in stripe for new orders - - if (!$stripe_customer_id) { - - try { - // If the token is not in the user's session, we can't set up a new customer - if (empty($_SESSION['stripe']['token'])) { - throw new Exception('Token not found'); - } - $stripe_token = $_SESSION['stripe']['token']; - - $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( - "source" => $stripe_token, - '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); - - // Store the customer ID in the session, - // We'll pick it up later to save it in the database since we might not have a $user object at this point anyway - $stripe_customer_id = $_SESSION['stripe']['customer_id'] = $customer->id; - - } catch (Exception $e) { - $result = array( - 'success' => FALSE, - 'comment' => $e->getCode(), - 'message' => t("Stripe Customer Creation 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', 'Failed stripe customer creation: @message', array('@message' => $result['message'])); - - return $result; - } - - } - // Charge the stripe customer the amount in the order //--Handle transactions for $0 @@ -553,20 +574,36 @@ function uc_stripe_charge($order_id, $amount, $data) { return $result; } - // Charge the customer + $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)) { + 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']; + + //Get item titles and models foreach($order->products as $item){ $titles[] = $item->title; $models[] = $item->model; } $metadata = array(); - if (!empty($models)) { $metadata['models'] = implode(";", $models); } @@ -578,22 +615,38 @@ function uc_stripe_charge($order_id, $amount, $data) { "currency" => strtolower($order->currency), "customer" => $stripe_customer_id, "description" => t("Order #@order_id", array("@order_id" => $order_id)), - "metadata" => $metadata, + "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 - $charge = \Stripe\Charge::create($params); + $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)), + '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, ); @@ -616,6 +669,8 @@ function uc_stripe_charge($order_id, $amount, $data) { 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'])); + $_SESSION['stripe']['payment_failed'] = TRUE; + return $result; } @@ -633,6 +688,8 @@ function uc_stripe_charge($order_id, $amount, $data) { watchdog('uc_stripe', 'Stripe gateway error for order @order_id', array('order_id' => $order_id)); + $_SESSION['stripe']['payment_failed'] = TRUE; + return $result; } @@ -656,33 +713,65 @@ function uc_stripe_renew($order, &$fee) { //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'); } - - //Create the charge $amount = $fee->fee_amount; $amount = $amount * 100; - $charge = \Stripe\Charge::create(array( - "amount" => $amount, - "currency" => strtolower($order->currency), - "customer" => $stripe_customer_id - ) + //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; + } - uc_payment_enter($order->order_id, $order->payment_method, $order->order_total, $fee->uid, $charge, "Success"); + $payment_intent = \Stripe\PaymentIntent::create($intent_params); + + 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, 'order', 'completed', FALSE); + uc_order_comment_save($fee->order_id, $order->uid, $message, 'admin'); return TRUE; - } catch (Exception $e) { + } 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_authenticaiton_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(), @@ -692,10 +781,14 @@ function uc_stripe_renew($order, &$fee) { )), ); + 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' => $result['comment'], '@message' => $result['message'])); + 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; } @@ -759,6 +852,13 @@ function _uc_stripe_prepare_api() { } 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; } @@ -789,6 +889,20 @@ function _uc_stripe_get_customer_id($uid) { 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 @@ -808,13 +922,222 @@ function uc_stripe_theme_registry_alter(&$theme_registry) { * @return string */ function uc_stripe_uc_payment_method_credit_form($form) { - $output = drupal_render($form['stripe_nojs_warning']); $output .= drupal_render($form['config_error']); $output .= theme('uc_payment_method_credit_form',$form); - $output .= drupal_render($form['stripe_token']); + $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'])) { + + $params = array( + 'payment_method' => $data['payment_method_id'], + "description" => t("Order #@order_id", array("@order_id" => $order_id)), + '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, $data['payment_method_id']); + if(!$customer){ + $message = 'Customer creation failed.'; + $_SESSION['stripe']['payment_failed'] = TRUE; + return ['error' => $message]; + } + $stripe_customer_id = $customer->id; + } + + $params['customer'] = $stripe_customer_id; + + # Create the PaymentIntent + $intent = \Stripe\PaymentIntent::create($params); + + 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; + } + +} + +/** + * + * @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; + } +} diff --git a/uc_stripe.pages.inc b/uc_stripe.pages.inc new file mode 100644 index 0000000..d80ce8d --- /dev/null +++ b/uc_stripe.pages.inc @@ -0,0 +1,149 @@ +fields('u', array('order_id', 'completed', 'rfee_id')) + ->condition('hash', $hash) + ->execute() + ->fetchObject(); + + + if(!$pending_order){ + $form['error'] = array( + '#markup' => t('Sorry, we could not verify your payment details. Please verify the link and try again. Contact support if the problem persists.'), + ); + return $form; + } + + $order_id = $pending_order->order_id; + $completed = $pending_order->completed; + $rfee_id = $pending_order->rfee_id; + + if ($completed) { + $form['error'] = array( + '#markup' => t('This payment has already been verified.'), + ); + return $form; + }; + + $form['heading'] = array( + '#markup' => t('

Your financial institution has requested additional verification to process your scheduled payment.

'), + ); + + $form['order_id'] = array( + '#type' => 'hidden', + '#value' => $order_id, + ); + + $form['rfee_id'] = array( + '#type' => 'hidden', + '#value' => $rfee_id, + ); + + $form['hash'] = array( + '#type' => 'hidden', + '#value' => $hash, + ); + + $form['submit'] = array( + '#type' => 'submit', + '#value' => t('Verify Payment') + ); + + $order = uc_order_load($order_id); + $user = user_load($order->uid); + $payment_method_id = _uc_stripe_get_payment_id($user->uid); + $stripe_customer_id = _uc_stripe_get_customer_id($user->uid); + + $order_id = $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('apikey' => $apikey, 'methodId' => $payment_method_id, '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'; + $form['#attached']['css'][] = drupal_get_path('module', 'uc_stripe') . '/css/uc_stripe.css'; + + return $form; +} + +function uc_stripe_authenticate_payment_form_submit($form, &$form_state){ + + $order_id = $form_state['values']['order_id']; + $rfee_id = $form_state['values']['rfee_id']; + $hash = $form_state['values']['hash']; + + $order = uc_order_load($order_id); + $intent_id = $order->data['payment_intent_id']; + + try{ + _uc_stripe_prepare_api(); + $payment_intent = \Stripe\PaymentIntent::retrieve($intent_id); + + if ($payment_intent->status != 'succeeded') { + throw new Exception('Payment intent failed'); + } + + $charge_id = $payment_intent->charges->data[0]['id']; + $amount = uc_currency_format($order->order_total, FALSE, FALSE, FALSE); + + $formatted_amount = $amount / 100; + $formatted_amount = number_format($formatted_amount, 2); + + $message = t('Payment of @amount processed successfully, Stripe transaction id @transaction_id.', array('@amount' => $formatted_amount, '@transaction_id' => $charge_id)); + $COMPLETED = 1; + + //Set all orders matching the order id and fee id to completed. This is incase + // there were multiple attempts to process the subscription. + db_update('uc_stripe_pending_auth') + ->fields(array( + 'completed' => $COMPLETED, + )) + ->condition('order_id', $order_id) + ->condition('rfee_id', $rfee_id) + ->execute(); + + $fee = uc_recurring_fee_user_load($rfee_id); + uc_payment_enter($order->order_id, $order->payment_method, $order->order_total, $fee->uid, $payment_intent, "Success"); + + // Since we have processed the payment here already, we'll temporarily change the fee + // handler to the the default uc_recurring fee handler that simply returns TRUE + // without any processing. + $fee->fee_handler = 'default'; + $id = uc_recurring_renew($fee); + + // We need to reset the fee handler for this order back to uc_stripe so that + // future subscriptions work. + $fee = uc_recurring_fee_user_load($fee->rfid); + $fee->fee_handler = 'uc_stripe'; + uc_recurring_fee_user_save($fee); + + uc_order_comment_save($order_id, $order->uid, $message, 'admin'); + uc_order_comment_save($order_id, $order->uid, $message, 'order', 'completed', FALSE); + + $form_state['redirect'] = '/'; + drupal_set_message('You have successfully completed your payment'); + + } catch (Exception $e) { + + $message = t("Stripe Charge Failed for order !order: !message", array( + "!order" => $order_id, + "!message" => $e->getMessage() + )); + + uc_order_comment_save($order_id, $order->uid, $message, 'admin'); + watchdog('uc_stripe', 'Stripe charge failed for order @order, message: @message', array('@order' => $order_id, '@message' => $message)); + $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.')); + drupal_set_message($fail_message, 'error'); + + } +}