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' => "
 . '/images/solid-dark.svg' . )
",
+ '#markup' => "
 . '/images/solid-dark.svg' . )
",
'#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');
+
+ }
+}