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.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.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');
+
+ }
+}