5 Commits

Author SHA1 Message Date
01a5a1b15f Update from remote. 2019-10-04 21:05:43 -05:00
017b4b731c 7.x-3.x-dev 2019-10-03 21:18:09 -05:00
90baa97458 Remove extra lines. 2017-05-19 16:12:14 -05:00
562e2a3698 Replace lost brackets. 2017-05-19 14:51:38 -05:00
ee03f3243d Clean up. 2017-05-19 11:31:59 -05:00
9 changed files with 1134 additions and 371 deletions

View File

@ -1,10 +1,8 @@
Based on the 7.x-2.x-dev branch at https://www.drupal.org/project/uc_stripe. Using This is an Ubercart payment gateway module for Stripe. It maintains PCI SAQ A
this as a working repo to track changes. Im using this on some production sites, compliance which allows Stripe, the payment processor, to handle prcoessing and
but no guarantees are made for anyone else. Look through the code! storing of payment card details.
—— It is compliant with 3D Secure, 3D Secure 2, and Strong Customer Authentication (SCA)
This is an Ubercart payment gateway module for Stripe.
Versions of the Stripe PHP Library and Stripe API that this module currently 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. supports are found in uc_stripe_libraries_info() in uc_stripe.module.
@ -21,9 +19,9 @@ section, and enable the gateway under the Payment Gateways.
c) On that page, provide your Stripe API keys, from c) On that page, provide your Stripe API keys, from
https://dashboard.stripe.com/account/apikeys https://dashboard.stripe.com/account/apikeys
d) Download and install the Stripe PHP Library version 2.2.0 or >=3.13.0 d) Download and install the Stripe PHP Library version 6.38.0 with stripe api
from https://github.com/stripe/stripe-php/releases. The recommended technique is 2019-05-16 or newer from https://github.com/stripe/stripe-php/releases. The
to use the command recommended technique is to use the command
drush ldl stripe drush ldl stripe
@ -31,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 sites/all/libraries/stripe such that the path to VERSION
is sites/all/libraries/stripe/VERSION. YOU MUST CLEAR THE CACHE AFTER is sites/all/libraries/stripe/VERSION. YOU MUST CLEAR THE CACHE AFTER
CHANGING THE STRIPE PHP LIBRARY. The Libraries module caches its memory of 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 libraries like the Stripe Library.
existing users; version 3.13.0+ supports PHP 7 and will get ongoing support.)
(With the latest version of the libraries module you can use the command: (With the latest version of the libraries module you can use the command:
e) If you are using recurring payments, install version 2.x e) If you are using recurring payments, install version 2.x
@ -54,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 the credit card number, so cannot properly validate it (and we don't want it to
ever know the credit card number.) 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 7.x-3.x maintains PCI SAQ A compliance and has major implementation changes from
uses the uc_recurring module. This means you have control of recurring 2.x. This version uses it's own payment pane in uc_cart to collect card info.
transactions without having to manage them on the Stripe dashboard. (Credit The card fields such as card number, expiration date, and cvc code have all been
card numbers and sensitive data are *not* stored on your site; only the Stripe hidden, and is handled entirely by Stripe's Element implementation.
customer ID is stored.) 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 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 uc_recurring_stripe table into the user table. When this happens the old
@ -90,6 +108,11 @@ Recurring payments require automatically triggered renewals using
uc_recurring_trigger_renewals ("Enabled triggered renewals" must be enabled uc_recurring_trigger_renewals ("Enabled triggered renewals" must be enabled
on admin/store/settings/payment/edit/recurring) 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 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. disable those subscriptions in order to not double-charge your customers.

View File

@ -30,3 +30,5 @@ a.poweredbylink:hover {
#uc_stripe_messages.hidden {display: none;} #uc_stripe_messages.hidden {display: none;}
.stripe-warning {color: red; font-style: oblique; } .stripe-warning {color: red; font-style: oblique; }
#edit-panes-payment-details-stripe-card-element{max-width: 600px}

View File

@ -9,163 +9,185 @@
Drupal.behaviors.uc_stripe = { Drupal.behaviors.uc_stripe = {
attach: function (context) { 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. // Map stripe names to (partial) Ubercart field names; Ubercart names add "billing_" or "shipping_" on the front.
const address_field_mapping = { const address_field_mapping = {
"address_line1": "street1", "address_line1": "street1",
"address_line2": "street2", "address_line2": "street2",
"address_city": "city", "address_city": "city",
"address_state": "zone", "address_state": "zone",
"address_zip": "postal_code", "address_zip": "postal_code",
"address_country": "country" "address_country": "country"
}; };
var submitButton = $('.uc-cart-checkout-form #edit-continue'); var submitButton = $('.uc-cart-checkout-form #edit-continue');
var cc_container = $('.payment-details-credit'); // Load the js reference to these fields so that on the review page
var cc_num = cc_container.find(':input[id*="edit-panes-payment-details-cc-numbe"]'); // we can input the last 4 and expiration date which is returned to us by stripe paymentMethod call
var cc_cvv = cc_container.find(':input[id*="edit-panes-payment-details-cc-cv"]'); 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 token value is reset // Make sure that when the page is being loaded the paymentMethod value is reset
// Browser or other caching might do otherwise. // Browser or other caching might do otherwise.
$("[name='panes[payment][details][stripe_token]']").val('default'); $("[name='panes[payment-stripe][details][stripe_payment_method]']").val('default');
$('span#stripe-nojs-warning').parent().hide(); // JS must enable the button; otherwise form might disclose cc info. It starts disabled
submitButton.attr('disabled', false);
// JS must enable the button; otherwise form might disclose cc info. It starts disabled // When this behavior fires, we can clean the form so it will behave properly,
submitButton.attr('disabled', false); // Remove 'name' from sensitive form elements so there's no way they can be submitted.
cc_num.removeAttr('name').removeAttr('disabled');
// When this behavior fires, we can clean the form so it will behave properly, $('div.form-item-panes-payment-details-cc-number').removeClass('form-disabled');
// Remove 'name' from sensitive form elements so there's no way they can be submitted. cc_cvv.removeAttr('name').removeAttr('disabled');
cc_num.removeAttr('name').removeAttr('disabled'); var cc_val_val = cc_num.val();
$('div.form-item-panes-payment-details-cc-number').removeClass('form-disabled'); if (cc_val_val && cc_val_val.indexOf('Last 4')) {
cc_cvv.removeAttr('name').removeAttr('disabled'); cc_num.val('');
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;
} }
// 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 // Custom styling can be passed to options when creating an Element.
tokenField.val('requested'); var style = {
base: {
try { // Add your base input styles here. For example:
var name = undefined; fontSize: '24px',
color: "#000000",
if ($(':input[name="panes[billing][billing_first_name]"]').length) { iconColor: "blue",
name = $(':input[name="panes[billing][billing_first_name]"]').val() + " " + $(':input[name="panes[billing][billing_last_name]"]').val();
} }
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 <div>.
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 = { // If we've requested and are waiting for token, prevent any further submit
number: cc_num.val(), if (paymentMethodField.val() == 'requested') {
cvc: cc_cvv.val(), return false; // Prevent any submit processing until token is received
exp_month: $(':input[name="panes[payment][details][cc_exp_month]"]').val(), }
exp_year: $(':input[name="panes[payment][details][cc_exp_year]"]').val(),
name: name
};
// Translate the Ubercart billing/shipping fields to Stripe values // Go ahead and request the token
for (var key in address_field_mapping) { paymentMethodField.val('requested');
const prefixes = ['billing', 'delivery'];
for (var i = 0; i < prefixes.length; i++) { try {
var prefix = prefixes[i];
var uc_field_name = prefix + '_' + address_field_mapping[key]; stripe.createPaymentMethod('card', card).then(function (response) {
var location = ':input[name="panes[' + prefix + '][' + uc_field_name + ']"]';
if ($(location).length) { if (response.error) {
params[key] = $(location).val();
if ($(location).attr('type') == 'select-one') { // Show the errors on the form
params[key] = $(location + " option:selected").text(); $('#uc_stripe_messages')
} .removeClass("hidden")
break; // break out of billing/shipping loop because we got the info .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) { // Prevent processing until we get the token back
return false;
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;
}); });
}
},
}; };
}(jQuery)); }(jQuery));

View File

@ -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));

View File

@ -8,9 +8,8 @@ core = 7.x
php = 5.3 php = 5.3
; Information added by Drupal.org packaging script on 2016-10-03 ; Information added by Drupal.org packaging script on 2019-09-13
version = "7.x-2.2+1-dev" version = "7.x-3.1+1-dev"
core = "7.x" core = "7.x"
project = "uc_stripe" project = "uc_stripe"
datestamp = "1475516941" datestamp = "1568380086"

View File

@ -92,6 +92,57 @@ function uc_stripe_install() {
variable_set('uc_credit_validate_numbers', FALSE); variable_set('uc_credit_validate_numbers', FALSE);
} }
/**
* Implements hook_uninstall().
*/
function uc_stripe_uninstall() {
variable_del('uc_stripe_authentication_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. * Enable triggered renewals, as uc_recurring manages renewals with this version.
*/ */
@ -181,3 +232,61 @@ function _uc_stripe_move_customer_id(&$sandbox) {
$sandbox['#finished'] = $sandbox['progress'] / $sandbox['max']; $sandbox['#finished'] = $sandbox['progress'] / $sandbox['max'];
} }
} }
/**
* Creates 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);
}
/**
* Changes typo in variable uc_stripe_authenticaiton_required_email.
*/
function uc_stripe_update_7302() {
$typo_var_name = 'uc_stripe_authenticaiton_required_email';
$value = variable_get($typo_var_name, '');
if (!empty($value)) {
variable_set('uc_stripe_authentication_required_email', $value);
variable_del($typo_var_name);
}
}

62
uc_stripe.mail.inc Normal file
View File

@ -0,0 +1,62 @@
<?php
/**
* This function returns the default off session authention email text.
* @return $text - Email text
*/
function _uc_stripe_get_authentication_required_email_text(){
$text = t("Dear [user:name],
We were unable to process your subscription payment.
Your financial institution is requesting additional verification before your subscription can be renewed.
Please visit this link to return to our site and complete the verification step.
[uc_stripe:verification-link]
-- [site:name] team
");
return $text;
}
/**
*
* Token callback that adds the authentication link to user mails.
*
* This function is used by the token_replace() call in uc_stripe_mail() to add
* the url to verify payment information
*
* @param $replacements
* An associative array variable containing mappings from token names to
* values (for use with strtr()).
* @param $data
* An associative array of token replacement values.
* @param $options
* Unused parameter required by the token_replace() function. */
function uc_stripe_mail_tokens(&$replacements, $data, $options) {
global $base_url;
$replacements['[uc_stripe:verification-link]'] = $base_url.'/stripe/authenticate-payment/'.$data['authentication_key'];
}
/**
* Implements hook_mail().
*
* Send mail and replace token with authenticaion link.
*/
function uc_stripe_mail($key, &$message, $params) {
switch ($key) {
case 'authentication_required' :
$message['subject'] = t('Additional Verification Required to Process Subscription.');
$variables = array('user' => $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;
}
}

View File

@ -14,83 +14,57 @@
* *
* @return mixed * @return mixed
*/ */
function uc_stripe_libraries_info() {
module_load_include('inc', 'uc_stripe', 'uc_stripe.mail');
function uc_stripe_libraries_info() {
$libraries['stripe'] = array( $libraries['stripe'] = array(
'name' => 'Stripe PHP Library', 'name' => 'Stripe PHP Library',
'vendor url' => 'http://stripe.com', 'vendor url' => 'http://stripe.com',
'download url' => 'https://github.com/stripe/stripe-php/releases', '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( 'version arguments' => array(
'file' => 'VERSION', 'file' => 'VERSION',
'pattern' => '/(\d+\.\d+\.\d+)/', 'pattern' => '/(\d+\.\d+\.\d+)/',
), ),
'versions' => array( 'versions' => array(
'2.2.0' => array( '6.38.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(
'files' => array( 'files' => array(
'php' => array( 'php' => array(
'init.php', 'init.php',
), )
), ),
'stripe_api_version' => '2016-03-07', 'stripe_api_version' => '2019-05-16'
), )
), ),
); );
return $libraries; 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 * Implements hook_payment_gateway to register this payment gateway
* @return array * @return array
@ -131,6 +105,47 @@ function uc_stripe_recurring_info() {
return $items; 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 * 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) { 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 = &$form['panes']['payment']['details'];
$payment_form['stripe_nojs_warning'] = array( // Markup text will not be displayed when JS and stripe are functioning properly
'#type' => 'item', // since Stripe Elements will replace the contents of this div
'#markup' => '<span id="stripe-nojs-warning" class="stripe-warning">' . t('Sorry, for security reasons your card cannot be processed because Javascript is disabled in your browser.') . '</span>', $stripe_payment_form['stripe_card_element'] = array(
'#weight' => -1000, '#prefix' => '<div id="stripe-card-element">',
'#weight' => - 10,
'#markup' => '<div class="stripe-warning">' . t('Sorry, for security reasons your card cannot be processed. Please refresh this page and try again. If the problem persists please check that Javascript is enabled your browser.') . '</div>',
'#suffix' => '</div>',
); );
// Powered by Stripe (logo from https://stripe.com/about/resources) // Powered by Stripe (logo from https://stripe.com/about/resources)
if (variable_get('uc_stripe_poweredby', FALSE)) { if (variable_get('uc_stripe_poweredby', FALSE)) {
$payment_form['field_message'] = array( $payment_form['field_message'] = array(
'#type' => 'item', '#type' => 'item',
'#markup' => "<a href='http://stripe.com'><img src=" . '/' . drupal_get_path('module', 'uc_stripe') . '/images/solid-dark.svg' . " alt='Powered by Stripe'></a>", '#markup' => "<a target='_blank' href='http://stripe.com'><img src=" . '/' . drupal_get_path('module', 'uc_stripe') . '/images/solid-dark.svg' . " alt='Powered by Stripe'></a>",
'#weight' => 1, '#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', '#type' => 'hidden',
'#default_value' => 'default', '#default_value' => 'default',
'#attributes' => array( '#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'])) { if (empty($form['actions']['continue']['#attributes'])) {
$form['actions']['continue']['#attributes'] = array(); $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', '')); $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 // Add custom JS and CSS
$form['#attached']['js']['https://js.stripe.com/v2/'] = array('type' => 'external'); $settings = array('apikey' => $apikey);
$form['#attached']['js'][] = array('data' => "Stripe.setPublishableKey('$apikey')", 'type' => 'inline'); $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']['js'][] = drupal_get_path('module', 'uc_stripe') . '/js/uc_stripe.js';
$form['#attached']['css'][] = drupal_get_path('module', 'uc_stripe') . '/css/uc_stripe.css'; $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. // Add custom submit which will do saving away of token during submit.
$form['#submit'][] = 'uc_stripe_checkout_form_customsubmit'; $form['#submit'][] = 'uc_stripe_checkout_form_customsubmit';
@ -195,6 +247,11 @@ function uc_stripe_form_uc_cart_checkout_form_alter(&$form, &$form_state) {
'#markup' => "<div id='uc_stripe_messages' class='messages error hidden'></div>", '#markup' => "<div id='uc_stripe_messages' class='messages error hidden'></div>",
); );
//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 (uc_credit_default_gateway() == 'uc_stripe') {
if (variable_get('uc_stripe_testmode', TRUE)) { if (variable_get('uc_stripe_testmode', TRUE)) {
$form['panes']['testmode'] = array( $form['panes']['testmode'] = array(
@ -226,6 +283,51 @@ function uc_stripe_uc_order_pane() {
return $panes; 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() * Implements hook_uc_checkout_complete()
* *
@ -237,12 +339,15 @@ function uc_stripe_uc_order_pane() {
function uc_stripe_uc_checkout_complete($order, $account) { function uc_stripe_uc_checkout_complete($order, $account) {
if ($order->payment_method == "credit") { 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() // 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); $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_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.'), '#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_authentication_required_email'] = array(
'#type' => 'textarea',
'#title' => t('Email for Recurring payment authentication'),
'#default_value' => variable_get('uc_stripe_authentication_required_email', $email_text),
'#description' => t('If your site uses recurring payments, some transactions will require the customer to return to the site and authenticate before the subscrption payment can be processed.')
);
$form['uc_stripe_settings']['uc_stripe_testmode'] = array( $form['uc_stripe_settings']['uc_stripe_testmode'] = array(
'#type' => 'checkbox', '#type' => 'checkbox',
'#title' => t('Test mode'), '#title' => t('Test mode'),
@ -323,20 +437,6 @@ function uc_stripe_settings_form() {
'#default_value' => variable_get('uc_stripe_poweredby', FALSE), '#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; return $form;
} }
@ -410,8 +510,8 @@ function _uc_stripe_validate_key($key) {
*/ */
function uc_stripe_checkout_form_customsubmit($form, &$form_state) { function uc_stripe_checkout_form_customsubmit($form, &$form_state) {
// This submit may be entered on another payment type, so don't set session in that case. // 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'])) { if (!empty($form_state['values']['panes']['payment-stripe']['details']['stripe_payment_method'])) {
$_SESSION['stripe']['token'] = $form_state['values']['panes']['payment']['details']['stripe_token']; $_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 // Format the amount in cents, which is what Stripe wants
$amount = uc_currency_format($amount, FALSE, FALSE, FALSE); $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 // Charge the stripe customer the amount in the order
//--Handle transactions for $0 //--Handle transactions for $0
@ -553,52 +574,67 @@ function uc_stripe_charge($order_id, $amount, $data) {
return $result; 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 { try {
//Bail if there's no customer ID if(!key_exists('payment_intent_id', $order->data)){
if (empty($stripe_customer_id)) { throw new Exception('The payment Intent has failed.');
throw new Exception('No customer ID found');
} }
// Set up titles and SKUs //Bail if there's no customer ID
$titles = variable_get('uc_stripe_metadata_titles', FALSE); if (empty($stripe_customer_id) || is_null($stripe_customer_id)) {
$models = variable_get('uc_stripe_metadata_models', FALSE); throw new Exception('No customer ID found');
$metadata = array(); }
foreach($order->products as $item){ //Bail if there's no payment method
$titles[] = $item->title; if (empty($_SESSION['stripe']['payment_method'])) {
$models[] = $item->model; throw new Exception('Token not found');
} }
$stripe_payment_method_id = $_SESSION['stripe']['payment_method'];
if (!empty($models)) {
$metadata['models'] = implode(";", $models);
}
if (!empty($titles)) {
$metadata['titles'] = implode(";", $titles);
}
$params = array( $params = array(
"amount" => $amount, "amount" => $amount,
"currency" => strtolower($order->currency), "currency" => strtolower($order->currency),
"customer" => $stripe_customer_id, "customer" => $stripe_customer_id,
"description" => t("Order #@order_id", array("@order_id" => $order_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)) { if (!empty($shipping_info)) {
$params['shipping'] = $shipping_info; $params['shipping'] = $shipping_info;
\Stripe\PaymentIntent::update($intent_id, ['shipping' => $shipping_info]);
} }
// charge the Customer the amount in the order // 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 = $amount / 100;
$formatted_amount = number_format($formatted_amount, 2); $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( $result = array(
'success' => TRUE, 'success' => TRUE,
'message' => t('Payment of @amount processed successfully, Stripe transaction id @transaction_id.', array('@amount' => $formatted_amount, '@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)), 'comment' => t('Stripe transaction ID: @transaction_id', array('@transaction_id' => $charge_id)),
'uid' => $user->uid, 'uid' => $user->uid,
); );
@ -621,6 +657,8 @@ function uc_stripe_charge($order_id, $amount, $data) {
uc_order_comment_save($order_id, $user->uid, $result['message'], 'admin'); 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'])); 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; return $result;
} }
@ -638,6 +676,8 @@ function uc_stripe_charge($order_id, $amount, $data) {
watchdog('uc_stripe', 'Stripe gateway error for order @order_id', array('order_id' => $order_id)); watchdog('uc_stripe', 'Stripe gateway error for order @order_id', array('order_id' => $order_id));
$_SESSION['stripe']['payment_failed'] = TRUE;
return $result; return $result;
} }
@ -661,33 +701,65 @@ function uc_stripe_renew($order, &$fee) {
//Get the customer ID //Get the customer ID
$stripe_customer_id = _uc_stripe_get_customer_id($order->uid); $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)) { if (empty($stripe_customer_id)) {
throw new Exception('No stripe customer ID found'); throw new Exception('No stripe customer ID found');
} }
//Create the charge
$amount = $fee->fee_amount; $amount = $fee->fee_amount;
$amount = $amount * 100; $amount = $amount * 100;
$charge = \Stripe\Charge::create(array( //create intent Array
"amount" => $amount, $intent_params = array(
"currency" => strtolower($order->currency), 'amount' => $amount,
"customer" => $stripe_customer_id '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); $formatted_amount = number_format($fee->fee_amount, 2);
$message = t('Card renewal payment of @amount processed successfully.', array('@amount' => $formatted_amount)); $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, 'order', 'completed', FALSE);
uc_order_comment_save($fee->order_id, $order->uid, $message, 'admin');
return TRUE; 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_authentication_required_email', _uc_stripe_get_authentication_required_email_text());
$params['user'] = user_load($order->uid);
$params['hash'] = $hash;
drupal_mail('uc_stripe', 'authentication_required', $params['user']->mail,language_default(), $params);
};
$result = array( $result = array(
'success' => FALSE, 'success' => FALSE,
'comment' => $e->getCode(), 'comment' => $e->getCode(),
@ -697,10 +769,14 @@ function uc_stripe_renew($order, &$fee) {
)), )),
); );
uc_order_comment_save($order->order_id, $order->uid, $result['message'], 'admin'); 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; return FALSE;
} }
@ -764,6 +840,13 @@ function _uc_stripe_prepare_api() {
} catch (Exception $e) { } catch (Exception $e) {
watchdog('uc_stripe', 'Error setting the Stripe API Key. Payments will not be processed: %error', array('%error' => $e->getMessage())); 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; return TRUE;
} }
@ -794,6 +877,20 @@ function _uc_stripe_get_customer_id($uid) {
return $id; 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 * Implements hook_theme_registry_alter() to make sure that we render
@ -813,13 +910,222 @@ function uc_stripe_theme_registry_alter(&$theme_registry) {
* @return string * @return string
*/ */
function uc_stripe_uc_payment_method_credit_form($form) { function uc_stripe_uc_payment_method_credit_form($form) {
$output = drupal_render($form['stripe_nojs_warning']);
$output .= drupal_render($form['config_error']); $output .= drupal_render($form['config_error']);
$output .= theme('uc_payment_method_credit_form',$form); $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']); $output .= drupal_render($form['dummy_image_load']);
return $output; 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 didnt 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;
}
}

149
uc_stripe.pages.inc Normal file
View File

@ -0,0 +1,149 @@
<?php
/**
* Implements hook_form().
*
* This form allows the user to authenticate in order for their recurring payment
* to be processed.
*/
function uc_stripe_authenticate_payment_form($form, &$form_state, $hash) {
$form = array();
$pending_order = db_select('uc_stripe_pending_auth', 'u')
->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('<p>Your financial institution has requested additional verification to process your scheduled payment.</p>'),
);
$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');
}
}