Skip to content
Snippets Groups Projects
Commit d4ef68af authored by mattwire's avatar mattwire
Browse files

Convert to stripe elements for credit card

parent e53889f4
Branches
Tags
No related merge requests found
......@@ -3,6 +3,8 @@
* https://civicrm.org/licensing
*/
use CRM_Stripe_ExtensionUtil as E;
/**
* Class CRM_Core_Payment_Stripe
*/
......@@ -262,11 +264,11 @@ class CRM_Core_Payment_Stripe extends CRM_Core_Payment {
*/
public function getPaymentFormFields() {
return array(
'credit_card_type',
'credit_card_number',
'cvv2',
'credit_card_exp_date',
'stripe_token',
//'credit_card_type',
//'credit_card_number',
//'cvv2',
//'credit_card_exp_date',
//'stripe_token',
'stripe_pub_key',
'stripe_id',
);
......@@ -402,6 +404,11 @@ class CRM_Core_Payment_Stripe extends CRM_Core_Payment {
'stripe_pub_key' => $publishableKey,
];
$form->setDefaults($defaults);
// Add help and javascript
CRM_Core_Region::instance('billing-block')->add(
['template' => 'CRM/Core/Payment/Stripe/Card.tpl', 'weight' => -1]);
CRM_Core_Resources::singleton()->addStyleFile(E::LONG_NAME, 'css/elements.css', 0, 'html-header');
}
/**
......@@ -422,13 +429,6 @@ class CRM_Core_Payment_Stripe extends CRM_Core_Payment {
* @throws \Civi\Payment\Exception\PaymentProcessorException
*/
public function doPayment(&$params, $component = 'contribute') {
if (array_key_exists('credit_card_number', $params)) {
$cc = $params['credit_card_number'];
if (!empty($cc) && substr($cc, 0, 8) != '00000000') {
Civi::log()->debug(ts('ALERT! Unmasked credit card received in back end. Please report this error to the site administrator.'));
}
}
$completedStatusId = CRM_Core_PseudoConstant::getKey('CRM_Contribute_BAO_Contribution', 'contribution_status_id', 'Completed');
$pendingStatusId = CRM_Core_PseudoConstant::getKey('CRM_Contribute_BAO_Contribution', 'contribution_status_id', 'Pending');
......@@ -456,11 +456,8 @@ class CRM_Core_Payment_Stripe extends CRM_Core_Payment {
$amount = self::getAmount($params);
// Use Stripe.js instead of raw card details.
if (!empty($params['stripe_token'])) {
$card_token = $params['stripe_token'];
}
else if(!empty(CRM_Utils_Array::value('stripe_token', $_POST, NULL))) {
$card_token = CRM_Utils_Array::value('stripe_token', $_POST, NULL);
if(!empty(CRM_Utils_Array::value('stripeToken', $_POST, NULL))) {
$cardToken = CRM_Utils_Array::value('stripeToken', $_POST, NULL);
}
else {
CRM_Core_Error::statusBounce(ts('Unable to complete payment! Please this to the site administrator with a description of what you were trying to do.'));
......@@ -473,7 +470,7 @@ class CRM_Core_Payment_Stripe extends CRM_Core_Payment {
// See if we already have a stripe customer
$customerParams = [
'contact_id' => $contactId,
'card_token' => $card_token,
'card_token' => $cardToken,
'processor_id' => $this->_paymentProcessor['id'],
'email' => $email,
// Include this to allow redirect within session on payment failure
......@@ -514,7 +511,7 @@ class CRM_Core_Payment_Stripe extends CRM_Core_Payment {
}
}
$stripeCustomer->card = $card_token;
$stripeCustomer->card = $cardToken;
try {
$stripeCustomer->save();
}
......@@ -554,7 +551,7 @@ class CRM_Core_Payment_Stripe extends CRM_Core_Payment {
$stripeChargeParams['customer'] = $stripeCustomer->id;
}
else {
$stripeChargeParams['card'] = $card_token;
$stripeChargeParams['card'] = $cardToken;
}
try {
......
#card-element {
padding: 2%;
margin: 2%;
background-color: ghostwhite;
-webkit-box-shadow: 10px 10px 7px -6px rgba(0,0,0,0.81);
-moz-box-shadow: 10px 10px 7px -6px rgba(0, 0, 0, 0.81);
box-shadow: 10px 10px 7px -6px rgba(0, 0, 0, 0.81);
}
#card-errors {
margin: 2%;
display: none;
}
......@@ -4,44 +4,41 @@
*/
CRM.$(function($) {
// Response from Stripe.createToken.
function stripeResponseHandler(status, response) {
$form = getBillingForm();
$submit = getBillingSubmit();
if (response.error) {
$('html, body').animate({scrollTop: 0}, 300);
// Show the errors on the form.
if ($(".messages.crm-error.stripe-message").length > 0) {
$(".messages.crm-error.stripe-message").slideUp();
$(".messages.crm-error.stripe-message:first").remove();
}
$form.prepend('<div class="messages alert alert-block alert-danger error crm-error stripe-message">'
+ '<strong>Payment Error Response:</strong>'
+ '<ul id="errorList">'
+ '<li>Error: ' + response.error.message + '</li>'
+ '</ul>'
+ '</div>');
removeCCDetails($form, true);
$form.data('submitted', false);
$submit.prop('disabled', false);
}
else {
var token = response['id'];
// Update form with the token & submit.
removeCCDetails($form, false);
$form.find("input#stripe-token").val(token);
// Disable unload event handler
window.onbeforeunload = null;
// Restore any onclickAction that was removed.
$submit.attr('onclick', onclickAction);
var stripe;
var card;
var form;
var submitButton;
function stripeTokenHandler(token) {
debugging('stripeTokenHandler');
// Insert the token ID into the form so it gets submitted to the server
var hiddenInput = document.createElement('input');
hiddenInput.setAttribute('type', 'hidden');
hiddenInput.setAttribute('name', 'stripeToken');
hiddenInput.setAttribute('value', token.id);
form.appendChild(hiddenInput);
// Submit the form
form.submit();
}
// This triggers submit without generating a submit event (so we don't run submit handler again)
$form.get(0).submit();
}
function createToken() {
debugging('createToken');
stripe.createToken(card).then(function(result) {
if (result.error) {
debugging('createToken failed');
// Inform the user if there was an error
var errorElement = document.getElementById('card-errors');
errorElement.style.display = 'block';
errorElement.textContent = result.error.message;
submitButton.removeAttribute('disabled');
document.querySelector('#billing-payment-block').scrollIntoView();
window.scrollBy(0, -50);
} else {
// Send the token to your server
stripeTokenHandler(result.token);
}
});
}
// Prepare the form.
......@@ -51,12 +48,11 @@ CRM.$(function($) {
window.onbeforeunload = null;
// Load Stripe onto the form.
loadStripeBillingBlock();
$submit = getBillingSubmit();
// Store and remove any onclick Action currently assigned to the form.
// We will re-add it if the transaction goes through.
onclickAction = $submit.attr('onclick');
$submit.removeAttr('onclick');
//onclickAction = submitButton.getAttribute('onclick');
//submitButton.removeAttribute('onclick');
// Quickform doesn't add hidden elements via standard method. On a form where payment processor may
// be loaded via initial form load AND ajax (eg. backend live contribution page with payproc dropdown)
......@@ -126,80 +122,82 @@ CRM.$(function($) {
// Setup Stripe.Js
var $stripePubKey = $('#stripe-pub-key');
if ($stripePubKey.length) {
if (!$().Stripe) {
$.getScript('https://js.stripe.com/v2/', function () {
Stripe.setPublishableKey($('#stripe-pub-key').val());
});
}
if (!$stripePubKey.length) {
return;
}
stripe = Stripe($('#stripe-pub-key').val());
var elements = stripe.elements();
var style = {
base: {
fontSize: '20px',
},
};
// Create an instance of the card Element.
card = elements.create('card', {style: style});
card.mount('#card-element');
// Get the form containing payment details
$form = getBillingForm();
if (!$form.length) {
form = getBillingForm();
if (!form.length) {
debugging('No billing form!');
return;
}
$submit = getBillingSubmit();
submitButton = getBillingSubmit();
// If another submit button on the form is pressed (eg. apply discount)
// add a flag that we can set to stop payment submission
$form.data('submit-dont-process', '0');
form.dataset.submitdontprocess = false;
var button = document.createElement("input");
button.type = "submit";
button.value = "im a button";
button.classList.add('cancel');
form.appendChild(button);
// Find submit buttons which should not submit payment
$form.find('[type="submit"][formnovalidate="1"], ' +
var nonPaymentSubmitButtons = form.querySelectorAll('[type="submit"][formnovalidate="1"], ' +
'[type="submit"][formnovalidate="formnovalidate"], ' +
'[type="submit"].cancel, ' +
'[type="submit"].webform-previous').click( function() {
debugging('adding submit-dont-process');
$form.data('submit-dont-process', 1);
});
'[type="submit"].webform-previous'), i;
for (i = 0; i < nonPaymentSubmitButtons.length; ++i) {
nonPaymentSubmitButtons[i].addEventListener('click', function () {
debugging('adding submitdontprocess');
form.dataset.submitdontprocess = true;
});
}
$submit.click( function(event) {
submitButton.addEventListener('click', function(event) {
// Take over the click function of the form.
debugging('clearing submit-dont-process');
$form.data('submit-dont-process', 0);
debugging('clearing submitdontprocess');
form.dataset.submitdontprocess = false;
// Run through our own submit, that executes Stripe submission if
// appropriate for this submit.
var ret = submit(event);
if (ret) {
// True means it's not our form. We are bailing and not trying to
// process Stripe.
// Restore any onclickAction that was removed.
$form = getBillingForm();
$submit = getBillingSubmit();
$submit.attr('onclick', onclickAction);
$form.get(0).submit();
return true;
}
// Otherwise, this is a stripe submission - don't handle normally.
// The code for completing the submission is all managed in the
// stripe handler (stripeResponseHandler) which gets execute after
// stripe finishes.
return false;
return submit(event);
});
// Add a keypress handler to set flag if enter is pressed
$form.find('input#discountcode').keypress( function(e) {
if (e.which === 13) {
$form.data('submit-dont-process', 1);
}
});
var isWebform = getIsWebform($form);
//form.querySelector('input#discountcode').keypress( function(e) {
// if (e.which === 13) {
// form.dataset.submitdontprocess = true;
// }
//});
// For CiviCRM Webforms.
if (isWebform) {
if (getIsDrupalWebform()) {
// We need the action field for back/submit to work and redirect properly after submission
if (!($('#action').length)) {
$form.append($('<input type="hidden" name="op" id="action" />'));
form.append($('<input type="hidden" name="op" id="action" />'));
}
var $actions = $form.find('[type=submit]');
var $actions = form.querySelector('[type=submit]');
$('[type=submit]').click(function() {
$('#action').val(this.value);
});
// If enter pressed, use our submit function
$form.keypress(function(event) {
form.keypress(function(event) {
if (event.which === 13) {
$('#action').val(this.value);
submit(event);
......@@ -208,41 +206,37 @@ CRM.$(function($) {
$('#billingcheckbox:input').hide();
$('label[for="billingcheckbox"]').hide();
}
else {
// As we use credit_card_number to pass token, make sure it is empty when shown
$form.find("input#credit_card_number").val('');
$form.find("input#cvv2").val('');
}
function submit(event) {
event.preventDefault();
debugging('submit handler');
if ($form.data('submitted') === true) {
if (form.dataset.submitted === true) {
debugging('form already submitted');
return false;
}
var isWebform = getIsWebform($form);
var stripeProcessorId;
var chosenProcessorId;
// Handle multiple payment options and Stripe not being chosen.
if (isWebform) {
var stripeProcessorId;
var chosenProcessorId;
if (getIsDrupalWebform()) {
stripeProcessorId = $('#stripe-id').val();
// this element may or may not exist on the webform, but we are dealing with a single (stripe) processor enabled.
if (!$('input[name="submitted[civicrm_1_contribution_1_contribution_payment_processor_id]"]').length) {
chosenProcessorId = stripeProcessorId;
} else {
chosenProcessorId = $form.find('input[name="submitted[civicrm_1_contribution_1_contribution_payment_processor_id]"]:checked').val();
chosenProcessorId = form.querySelector('input[name="submitted[civicrm_1_contribution_1_contribution_payment_processor_id]"]:checked').val();
}
}
else {
// Most forms have payment_processor-section but event registration has credit_card_info-section
if (($form.find(".crm-section.payment_processor-section").length > 0)
|| ($form.find(".crm-section.credit_card_info-section").length > 0)) {
if ((form.querySelector(".crm-section.payment_processor-section") !== null)
|| (form.querySelector(".crm-section.credit_card_info-section") !== null)) {
stripeProcessorId = $('#stripe-id').val();
chosenProcessorId = $form.find('input[name="payment_processor_id"]:checked').val();
if (form.querySelector('input[name="payment_processor_id"]:checked') !== null) {
chosenProcessorId = form.querySelector('input[name="payment_processor_id"]:checked').value;
}
}
}
......@@ -260,22 +254,18 @@ CRM.$(function($) {
debugging('Stripe is the selected payprocessor');
}
$form = getBillingForm();
// Don't handle submits generated by non-stripe processors
if (!$('input#stripe-pub-key').length || !($('input#stripe-pub-key').val())) {
debugging('submit missing stripe-pub-key element or value');
return true;
}
// Don't handle submits generated by the CiviDiscount button.
if ($form.data('submit-dont-process')) {
if (form.dataset.submitdontprocess === true) {
debugging('non-payment submit detected - not submitting payment');
return true;
}
$submit = getBillingSubmit();
if (isWebform) {
if (getIsDrupalWebform()) {
// If we have selected Stripe but amount is 0 we don't submit via Stripe
if ($('#billing-payment-block').is(':hidden')) {
debugging('no payment processor on webform');
......@@ -303,100 +293,64 @@ CRM.$(function($) {
}
}
// If there's no credit card field, no use in continuing (probably wrong
// context anyway)
if (!$form.find('#credit_card_number').length) {
debugging('No credit card field');
return true;
}
// Lock to prevent multiple submissions
if ($form.data('submitted') === true) {
if (form.dataset.submitted === true) {
// Previously submitted - don't submit again
alert('Form already submitted. Please wait.');
return false;
} else {
// Mark it so that the next submit can be ignored
// ADDED requirement that form be valid
if($form.valid()) {
$form.data('submitted', true);
}
form.dataset.submitted = true;
}
// Disable the submit button to prevent repeated clicks
$submit.prop('disabled', true);
var cc_month = $form.find('#credit_card_exp_date_M').val();
var cc_year = $form.find('#credit_card_exp_date_Y').val();
Stripe.card.createToken({
name: $form.find('#billing_first_name')
.val() + ' ' + $form.find('#billing_last_name').val(),
address_zip: $form.find('#billing_postal_code-5').val(),
number: $form.find('#credit_card_number').val(),
cvc: $form.find('#cvv2').val(),
exp_month: cc_month,
exp_year: cc_year
}, stripeResponseHandler);
submitButton.setAttribute('disabled', true);
// Create a token when the form is submitted.
createToken();
debugging('Created Stripe token');
return false;
return true;
}
}
function getIsWebform(form) {
// Pass in the billingForm object
// If the form has the webform-client-form (drupal 7) or webform-submission-form (drupal 8) class then it's a drupal webform!
return form.hasClass('webform-client-form') || form.hasClass('webform-submission-form');
function getIsDrupalWebform() {
// form class for drupal webform: webform-client-form (drupal 7); webform-submission-form (drupal 8)
if (form !== null) {
return form.classList.contains('webform-client-form') || form.classList.contains('webform-submission-form');
}
return false;
}
function getBillingForm() {
// If we have a stripe billing form on the page
var $billingForm = $('input#stripe-pub-key').closest('form');
//if (!$billingForm.length && getIsWebform()) {
// If we are in a webform
// $billingForm = $('.webform-client-form');
//}
if (!$billingForm.length) {
var billingFormID = $('input#stripe-pub-key').closest('form').prop('id');
if (!billingFormID.length) {
// If we have multiple payment processors to select and stripe is not currently loaded
$billingForm = $('input[name=hidden_processor]').closest('form');
billingFormID = $('input[name=hidden_processor]').closest('form').prop('id');
}
return $billingForm;
// We have to use document.getElementById here so we have the right elementtype for appendChild()
return document.getElementById(billingFormID);
}
function getBillingSubmit() {
$form = getBillingForm();
var isWebform = getIsWebform($form);
if (isWebform) {
$submit = $form.find('[type="submit"].webform-submit');
if (!$submit.length) {
var submit = null;
if (getIsDrupalWebform()) {
submit = form.querySelector('[type="submit"].webform-submit');
if (!submit.length) {
// drupal 8 webform
$submit = $form.find('[type="submit"].webform-button--submit');
submit = form.querySelector('[type="submit"].webform-button--submit');
}
}
else {
$submit = $form.find('[type="submit"].validate');
}
return $submit;
}
function removeCCDetails($form, $truncate) {
// Remove the "name" attribute so params are not submitted
var ccNumElement = $form.find("input#credit_card_number");
var cvv2Element = $form.find("input#cvv2");
if ($truncate) {
ccNumElement.val('');
cvv2Element.val('');
}
else {
var last4digits = ccNumElement.val().substr(12, 16);
ccNumElement.val('000000000000' + last4digits);
cvv2Element.val('000');
submit = form.querySelector('[type="submit"].validate');
}
return submit;
}
function debugging (errorCode) {
// Uncomment the following to debug unexpected returns.
//console.log(new Date().toISOString() + ' civicrm_stripe.js: ' + errorCode);
console.log(new Date().toISOString() + ' civicrm_stripe.js: ' + errorCode);
}
});
{* https://civicrm.org/licensing *}
<script src="https://js.stripe.com/v3/"></script>
<label for="card-element">
<legend>Credit or debit card</legend>
</label>
<div id="card-element">
<!-- a Stripe Element will be inserted here. -->
</div>
{* Area for Stripe to report errors *}
<div id="card-errors" role="alert" class="alert alert-danger"></div>
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment