From 976a6845382724fd8213f5c705a0f43f7e97f961 Mon Sep 17 00:00:00 2001 From: Matthew Wire Date: Thu, 6 Feb 2020 20:56:58 +0000 Subject: [PATCH 01/45] Improve workaround for #147 to also work with radio buttons and profiles --- js/civicrm_stripe.js | 3 ++- js/civicrm_stripe.min.js | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/js/civicrm_stripe.js b/js/civicrm_stripe.js index 7ad664e..31e8780 100644 --- a/js/civicrm_stripe.js +++ b/js/civicrm_stripe.js @@ -588,7 +588,8 @@ CRM.$(function($) { function setBillingFieldsRequiredForJQueryValidate() { // Work around https://github.com/civicrm/civicrm-core/compare/master...mattwire:stripe_147 // The main billing fields do not get set to required so don't get checked by jquery validateform. - $('.billing_name_address-section div.label span.crm-marker').each(function() { + // This also applies to any radio button in billing/profiles so we flag every element with a crm-marker + $('div.label span.crm-marker').each(function() { $(this).closest('div').next('div').children('input').addClass('required'); }); } diff --git a/js/civicrm_stripe.min.js b/js/civicrm_stripe.min.js index f803be6..34459bd 100644 --- a/js/civicrm_stripe.min.js +++ b/js/civicrm_stripe.min.js @@ -1 +1 @@ -CRM.$(function(d){g("civicrm_stripe loaded, dom-ready function firing.");if(window.civicrmStripeHandleReload){g("calling existing civicrmStripeHandleReload.");window.civicrmStripeHandleReload();return}var r;var b;var c;var v;var p=false;window.onbeforeunload=null;window.civicrmStripeHandleReload=function(){g("civicrmStripeHandleReload");var D=document.getElementById("card-element");if((typeof D!=="undefined")&&(D)){if(!D.children.length){g("checkAndLoad from document.ready");n()}}};window.civicrmStripeHandleReload();function x(F,D){g(F+": success - submitting form");var E=document.createElement("input");E.setAttribute("type","hidden");E.setAttribute("name",F);E.setAttribute("value",D.id);c.appendChild(E);c.submit()}function q(){for(i=0;i1){D=true}}if(document.getElementById("is_recur")!==null){if(document.getElementById("is_recur").type=="hidden"){D=(document.getElementById("is_recur").value==1)}else{D=Boolean(document.getElementById("is_recur").checked)}}else{if(d('input[name="auto_renew"]').length!==0){if(d('input[name="auto_renew"]').prop("checked")){D=true}else{if(document.getElementById("auto_renew").type=="hidden"){D=(document.getElementById("auto_renew").value==1)}else{D=Boolean(document.getElementById("auto_renew").checked)}}}}g("isRecur is "+D);return D}function w(D){if(!D.complete){return}document.getElementById("billing_postal_code-"+CRM.vars.stripe.billingAddressID).value=D.value.postalCode}function o(){cividiscountElements=c.querySelectorAll("input#discountcode");var D=function(E){if(E.keyCode===13){E.preventDefault();g("adding submitdontprocess");c.dataset.submitdontprocess=true}};for(i=0;i1){D=true}}if(document.getElementById("is_recur")!==null){if(document.getElementById("is_recur").type=="hidden"){D=(document.getElementById("is_recur").value==1)}else{D=Boolean(document.getElementById("is_recur").checked)}}else{if(d('input[name="auto_renew"]').length!==0){if(d('input[name="auto_renew"]').prop("checked")){D=true}else{if(document.getElementById("auto_renew").type=="hidden"){D=(document.getElementById("auto_renew").value==1)}else{D=Boolean(document.getElementById("auto_renew").checked)}}}}g("isRecur is "+D);return D}function w(D){if(!D.complete){return}document.getElementById("billing_postal_code-"+CRM.vars.stripe.billingAddressID).value=D.value.postalCode}function o(){cividiscountElements=c.querySelectorAll("input#discountcode");var D=function(E){if(E.keyCode===13){E.preventDefault();g("adding submitdontprocess");c.dataset.submitdontprocess=true}};for(i=0;i Date: Tue, 4 Feb 2020 18:22:51 +0000 Subject: [PATCH 02/45] Don't explicitly set webhook version when creating webhook. It will automatically track the dashboard API version --- js/civicrm_stripe.js | 2 +- js/civicrm_stripe.min.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/js/civicrm_stripe.js b/js/civicrm_stripe.js index 31e8780..870699d 100644 --- a/js/civicrm_stripe.js +++ b/js/civicrm_stripe.js @@ -590,7 +590,7 @@ CRM.$(function($) { // The main billing fields do not get set to required so don't get checked by jquery validateform. // This also applies to any radio button in billing/profiles so we flag every element with a crm-marker $('div.label span.crm-marker').each(function() { - $(this).closest('div').next('div').children('input').addClass('required'); + $(this).closest('div').next('div').find('input').addClass('required'); }); } diff --git a/js/civicrm_stripe.min.js b/js/civicrm_stripe.min.js index 34459bd..852eeee 100644 --- a/js/civicrm_stripe.min.js +++ b/js/civicrm_stripe.min.js @@ -1 +1 @@ -CRM.$(function(d){g("civicrm_stripe loaded, dom-ready function firing.");if(window.civicrmStripeHandleReload){g("calling existing civicrmStripeHandleReload.");window.civicrmStripeHandleReload();return}var r;var b;var c;var v;var p=false;window.onbeforeunload=null;window.civicrmStripeHandleReload=function(){g("civicrmStripeHandleReload");var D=document.getElementById("card-element");if((typeof D!=="undefined")&&(D)){if(!D.children.length){g("checkAndLoad from document.ready");n()}}};window.civicrmStripeHandleReload();function x(F,D){g(F+": success - submitting form");var E=document.createElement("input");E.setAttribute("type","hidden");E.setAttribute("name",F);E.setAttribute("value",D.id);c.appendChild(E);c.submit()}function q(){for(i=0;i1){D=true}}if(document.getElementById("is_recur")!==null){if(document.getElementById("is_recur").type=="hidden"){D=(document.getElementById("is_recur").value==1)}else{D=Boolean(document.getElementById("is_recur").checked)}}else{if(d('input[name="auto_renew"]').length!==0){if(d('input[name="auto_renew"]').prop("checked")){D=true}else{if(document.getElementById("auto_renew").type=="hidden"){D=(document.getElementById("auto_renew").value==1)}else{D=Boolean(document.getElementById("auto_renew").checked)}}}}g("isRecur is "+D);return D}function w(D){if(!D.complete){return}document.getElementById("billing_postal_code-"+CRM.vars.stripe.billingAddressID).value=D.value.postalCode}function o(){cividiscountElements=c.querySelectorAll("input#discountcode");var D=function(E){if(E.keyCode===13){E.preventDefault();g("adding submitdontprocess");c.dataset.submitdontprocess=true}};for(i=0;i1){D=true}}if(document.getElementById("is_recur")!==null){if(document.getElementById("is_recur").type=="hidden"){D=(document.getElementById("is_recur").value==1)}else{D=Boolean(document.getElementById("is_recur").checked)}}else{if(d('input[name="auto_renew"]').length!==0){if(d('input[name="auto_renew"]').prop("checked")){D=true}else{if(document.getElementById("auto_renew").type=="hidden"){D=(document.getElementById("auto_renew").value==1)}else{D=Boolean(document.getElementById("auto_renew").checked)}}}}g("isRecur is "+D);return D}function w(D){if(!D.complete){return}document.getElementById("billing_postal_code-"+CRM.vars.stripe.billingAddressID).value=D.value.postalCode}function o(){cividiscountElements=c.querySelectorAll("input#discountcode");var D=function(E){if(E.keyCode===13){E.preventDefault();g("adding submitdontprocess");c.dataset.submitdontprocess=true}};for(i=0;i Date: Fri, 7 Feb 2020 15:42:07 +0000 Subject: [PATCH 03/45] Fixes for select2 validation and improve display of card errors --- js/civicrm_stripe.js | 43 +++++++++++++++++++++++++++------------- js/civicrm_stripe.min.js | 2 +- 2 files changed, 30 insertions(+), 15 deletions(-) diff --git a/js/civicrm_stripe.js b/js/civicrm_stripe.js index 870699d..8927583 100644 --- a/js/civicrm_stripe.js +++ b/js/civicrm_stripe.js @@ -61,13 +61,13 @@ CRM.$(function($) { return form.submit(); } - function displayError(result) { + function displayError(error) { // Display error.message in your UI. - debugging('error: ' + result.error.message); + debugging('error: ' + error.message); // Inform the user if there was an error var errorElement = document.getElementById('card-errors'); errorElement.style.display = 'block'; - errorElement.textContent = result.error.message; + errorElement.textContent = error.message; document.querySelector('#billing-payment-block').scrollIntoView(); window.scrollBy(0, -50); form.dataset.submitted = false; @@ -81,7 +81,7 @@ CRM.$(function($) { stripe.createPaymentMethod('card', card).then(function (result) { if (result.error) { // Show error in payment form - displayError(result); + displayError(result.error); } else { if (getIsRecur() || isEventAdditionalParticipants()) { @@ -111,7 +111,7 @@ CRM.$(function($) { debugging('handleServerResponse'); if (result.error) { // Show error from server on payment form - displayError(result); + displayError(result.error); } else if (result.requires_action) { // Use Stripe.js to handle required card action handleAction(result); @@ -126,7 +126,7 @@ CRM.$(function($) { .then(function(result) { if (result.error) { // Show error in payment form - displayError(result); + displayError(result.error); } else { // The card action has been handled // The PaymentIntent can be confirmed again on the server @@ -241,8 +241,6 @@ CRM.$(function($) { card.mount('#card-element'); debugging("created new card element", card); - setBillingFieldsRequiredForJQueryValidate(); - // Hide the CiviCRM postcode field so it will still be submitted but will contain the value set in the stripe card-element. if (document.getElementById('billing_postal_code-5').value) { document.getElementById('billing_postal_code-5').setAttribute('disabled', true); @@ -250,8 +248,9 @@ CRM.$(function($) { else { document.getElementsByClassName('billing_postal_code-' + CRM.vars.stripe.billingAddressID + '-section')[0].setAttribute('hidden', true); } - card.addEventListener('change', function(event) { - updateFormElementsFromCreditCardDetails(event); + + card.addEventListener('change', function (event) { + cardElementChanged(event); }); // Get the form containing payment details @@ -260,6 +259,8 @@ CRM.$(function($) { debugging('No billing form!'); return; } + + setBillingFieldsRequiredForJQueryValidate(); submitButtons = getBillingSubmit(); // If another submit button on the form is pressed (eg. apply discount) @@ -334,6 +335,7 @@ CRM.$(function($) { if ($(form).valid() === false) { debugging('Form not valid'); + $('div#card-errors').hide(); document.querySelector('#billing-payment-block').scrollIntoView(); window.scrollBy(0, -50); return false; @@ -562,11 +564,20 @@ CRM.$(function($) { return isRecur; } - function updateFormElementsFromCreditCardDetails(event) { - if (!event.complete) { - return; + function cardElementChanged(event) { + if (event.empty) { + $('div#card-errors').hide(); + } + else if (event.error) { + displayError(event.error); + } + else if (event.complete) { + $('div#card-errors').hide(); + var postCodeElement = document.getElementById('billing_postal_code-' + CRM.vars.stripe.billingAddressID); + if (postCodeElement) { + postCodeElement.value = event.value.postalCode; + } } - document.getElementById('billing_postal_code-' + CRM.vars.stripe.billingAddressID).value = event.value.postalCode; } function addSupportForCiviDiscount() { @@ -589,9 +600,13 @@ CRM.$(function($) { // Work around https://github.com/civicrm/civicrm-core/compare/master...mattwire:stripe_147 // The main billing fields do not get set to required so don't get checked by jquery validateform. // This also applies to any radio button in billing/profiles so we flag every element with a crm-marker + // See also https://github.com/civicrm/civicrm-core/pull/16488 for a core fix $('div.label span.crm-marker').each(function() { $(this).closest('div').next('div').find('input').addClass('required'); }); + var validator = $(form).validate(); + validator.settings.errorClass = 'crm-inline-error alert alert-danger'; + validator.settings.ignore = '.select2-offscreen, [readonly], :hidden:not(.crm-select2)'; } function isEventAdditionalParticipants() { diff --git a/js/civicrm_stripe.min.js b/js/civicrm_stripe.min.js index 852eeee..fd31fb4 100644 --- a/js/civicrm_stripe.min.js +++ b/js/civicrm_stripe.min.js @@ -1 +1 @@ -CRM.$(function(d){g("civicrm_stripe loaded, dom-ready function firing.");if(window.civicrmStripeHandleReload){g("calling existing civicrmStripeHandleReload.");window.civicrmStripeHandleReload();return}var r;var b;var c;var v;var p=false;window.onbeforeunload=null;window.civicrmStripeHandleReload=function(){g("civicrmStripeHandleReload");var D=document.getElementById("card-element");if((typeof D!=="undefined")&&(D)){if(!D.children.length){g("checkAndLoad from document.ready");n()}}};window.civicrmStripeHandleReload();function x(F,D){g(F+": success - submitting form");var E=document.createElement("input");E.setAttribute("type","hidden");E.setAttribute("name",F);E.setAttribute("value",D.id);c.appendChild(E);c.submit()}function q(){for(i=0;i1){D=true}}if(document.getElementById("is_recur")!==null){if(document.getElementById("is_recur").type=="hidden"){D=(document.getElementById("is_recur").value==1)}else{D=Boolean(document.getElementById("is_recur").checked)}}else{if(d('input[name="auto_renew"]').length!==0){if(d('input[name="auto_renew"]').prop("checked")){D=true}else{if(document.getElementById("auto_renew").type=="hidden"){D=(document.getElementById("auto_renew").value==1)}else{D=Boolean(document.getElementById("auto_renew").checked)}}}}g("isRecur is "+D);return D}function w(D){if(!D.complete){return}document.getElementById("billing_postal_code-"+CRM.vars.stripe.billingAddressID).value=D.value.postalCode}function o(){cividiscountElements=c.querySelectorAll("input#discountcode");var D=function(E){if(E.keyCode===13){E.preventDefault();g("adding submitdontprocess");c.dataset.submitdontprocess=true}};for(i=0;i1){D=true}}if(document.getElementById("is_recur")!==null){if(document.getElementById("is_recur").type=="hidden"){D=(document.getElementById("is_recur").value==1)}else{D=Boolean(document.getElementById("is_recur").checked)}}else{if(d('input[name="auto_renew"]').length!==0){if(d('input[name="auto_renew"]').prop("checked")){D=true}else{if(document.getElementById("auto_renew").type=="hidden"){D=(document.getElementById("auto_renew").value==1)}else{D=Boolean(document.getElementById("auto_renew").checked)}}}}g("isRecur is "+D);return D}function k(E){if(E.empty){d("div#card-errors").hide()}else{if(E.error){j(E.error)}else{if(E.complete){d("div#card-errors").hide();var D=document.getElementById("billing_postal_code-"+CRM.vars.stripe.billingAddressID);if(D){D.value=E.value.postalCode}}}}}function p(){cividiscountElements=c.querySelectorAll("input#discountcode");var D=function(E){if(E.keyCode===13){E.preventDefault();g("adding submitdontprocess");c.dataset.submitdontprocess=true}};for(i=0;i Date: Mon, 3 Feb 2020 23:53:04 +0000 Subject: [PATCH 04/45] 6.4alpha --- info.xml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/info.xml b/info.xml index 77b9e09..58de28e 100644 --- a/info.xml +++ b/info.xml @@ -13,9 +13,9 @@ Matthew Wire (MJW Consulting) mjw@mjwconsult.co.uk - 2020-02-22 - 6.3.2 - stable + 2020-02-03 + 6.4alpha + alpha 5.19 -- GitLab From f0cfa908768de031e385ef51f17b8d333895db85 Mon Sep 17 00:00:00 2001 From: Jamie McClelland Date: Tue, 4 Feb 2020 17:16:40 -0500 Subject: [PATCH 05/45] initial steps to modernize the testing infrastructure. --- docs/testing.md | 4 ++-- tests/phpunit/CRM/Stripe/BaseTest.php | 2 +- tests/phpunit/bootstrap.php | 14 ++++++++++++++ 3 files changed, 17 insertions(+), 3 deletions(-) diff --git a/docs/testing.md b/docs/testing.md index 06ab39d..14f7a85 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -11,8 +11,8 @@ This extension comes with two PHP Unit tests: Tests can be run most easily via an installation made through CiviCRM Buildkit (https://github.com/civicrm/civicrm-buildkit) by changing into the extension directory and running: - phpunit4 tests/phpunit/CRM/Stripe/IpnTest.php - phpunit4 tests/phpunit/CRM/Stripe/DirectTest.php + phpunit6 tests/phpunit/CRM/Stripe/IpnTest.php + phpunit6 tests/phpunit/CRM/Stripe/DirectTest.php ### Katalon Tests See the test/katalon folder for instructions on running full web-browser based automation tests. diff --git a/tests/phpunit/CRM/Stripe/BaseTest.php b/tests/phpunit/CRM/Stripe/BaseTest.php index b23d69f..3f44473 100644 --- a/tests/phpunit/CRM/Stripe/BaseTest.php +++ b/tests/phpunit/CRM/Stripe/BaseTest.php @@ -20,7 +20,7 @@ define('STRIPE_PHPUNIT_TEST', 1); * * @group headless */ -class CRM_Stripe_BaseTest extends \PHPUnit_Framework_TestCase implements HeadlessInterface, HookInterface, TransactionalInterface { +class CRM_Stripe_BaseTest extends \PHPUnit\Framework\TestCase implements HeadlessInterface, HookInterface, TransactionalInterface { protected $_contributionID; protected $_invoiceID = 'in_19WvbKAwDouDdbFCkOnSwAN7'; diff --git a/tests/phpunit/bootstrap.php b/tests/phpunit/bootstrap.php index 9de4be6..352e007 100644 --- a/tests/phpunit/bootstrap.php +++ b/tests/phpunit/bootstrap.php @@ -2,8 +2,17 @@ ini_set('memory_limit', '2G'); ini_set('safe_mode', 0); +// phpcs:ignore eval(cv('php:boot --level=classloader', 'phpcode')); +// Allow autoloading of PHPUnit helper classes in this extension. +$loader = new \Composer\Autoload\ClassLoader(); +$loader->add('CRM_', __DIR__); +$loader->add('Civi\\', __DIR__); +$loader->add('api_', __DIR__); +$loader->add('api\\', __DIR__); +$loader->register(); + /** * Call the "cv" command. * @@ -21,6 +30,11 @@ function cv($cmd, $decode = 'json') { $descriptorSpec = array(0 => array("pipe", "r"), 1 => array("pipe", "w"), 2 => STDERR); $oldOutput = getenv('CV_OUTPUT'); putenv("CV_OUTPUT=json"); + + // Execute `cv` in the original folder. This is a work-around for + // phpunit/codeception, which seem to manipulate PWD. + $cmd = sprintf('cd %s; %s', escapeshellarg(getenv('PWD')), $cmd); + $process = proc_open($cmd, $descriptorSpec, $pipes, __DIR__); putenv("CV_OUTPUT=$oldOutput"); fclose($pipes[0]); -- GitLab From 2a072169ab63d0563895e80f2639d18c301c7442 Mon Sep 17 00:00:00 2001 From: Matthew Wire Date: Wed, 5 Feb 2020 10:54:00 +0000 Subject: [PATCH 06/45] Further tweaks to get tests working --- CRM/Stripe/Upgrader/Base.php | 107 ++++++++++++++++++++---- stripe.civix.php | 104 +++++++++++++---------- tests/phpunit/CRM/Stripe/BaseTest.php | 2 +- tests/phpunit/CRM/Stripe/DirectTest.php | 4 +- tests/phpunit/CRM/Stripe/IpnTest.php | 2 + 5 files changed, 155 insertions(+), 64 deletions(-) diff --git a/CRM/Stripe/Upgrader/Base.php b/CRM/Stripe/Upgrader/Base.php index 0c0e680..3749a42 100644 --- a/CRM/Stripe/Upgrader/Base.php +++ b/CRM/Stripe/Upgrader/Base.php @@ -1,6 +1,7 @@ run($xml_file); return TRUE; @@ -107,7 +113,26 @@ class CRM_Stripe_Upgrader_Base { public function executeSqlFile($relativePath) { CRM_Utils_File::sourceSQLFile( CIVICRM_DSN, - $this->extensionDir . '/' . $relativePath + $this->extensionDir . DIRECTORY_SEPARATOR . $relativePath + ); + return TRUE; + } + + /** + * @param string $tplFile + * The SQL file path (relative to this extension's dir). + * Ex: "sql/mydata.mysql.tpl". + * @return bool + */ + public function executeSqlTemplate($tplFile) { + // Assign multilingual variable to Smarty. + $upgrade = new CRM_Upgrade_Form(); + + $tplFile = CRM_Utils_File::isAbsolute($tplFile) ? $tplFile : $this->extensionDir . DIRECTORY_SEPARATOR . $tplFile; + $smarty = CRM_Core_Smarty::singleton(); + $smarty->assign('domainID', CRM_Core_Config::domainID()); + CRM_Utils_File::sourceSQLFile( + CIVICRM_DSN, $smarty->fetch($tplFile), NULL, TRUE ); return TRUE; } @@ -121,7 +146,7 @@ class CRM_Stripe_Upgrader_Base { */ public function executeSql($query, $params = array()) { // FIXME verify that we raise an exception on error - CRM_Core_DAO::executeSql($query, $params); + CRM_Core_DAO::executeQuery($query, $params); return TRUE; } @@ -205,7 +230,7 @@ class CRM_Stripe_Upgrader_Base { * @return array(revisionNumbers) sorted numerically */ public function getRevisions() { - if (! is_array($this->revisions)) { + if (!is_array($this->revisions)) { $this->revisions = array(); $clazz = new ReflectionClass(get_class($this)); @@ -222,24 +247,42 @@ class CRM_Stripe_Upgrader_Base { } public function getCurrentRevision() { - // return CRM_Core_BAO_Extension::getSchemaVersion($this->extensionName); + $revision = CRM_Core_BAO_Extension::getSchemaVersion($this->extensionName); + if (!$revision) { + $revision = $this->getCurrentRevisionDeprecated(); + } + return $revision; + } + + private function getCurrentRevisionDeprecated() { $key = $this->extensionName . ':version'; - return CRM_Core_BAO_Setting::getItem('Extension', $key); + if ($revision = CRM_Core_BAO_Setting::getItem('Extension', $key)) { + $this->revisionStorageIsDeprecated = TRUE; + } + return $revision; } public function setCurrentRevision($revision) { - // We call this during hook_civicrm_install, but the underlying SQL - // UPDATE fails because the extension record hasn't been INSERTed yet. - // Instead, track revisions in our own namespace. - // CRM_Core_BAO_Extension::setSchemaVersion($this->extensionName, $revision); - - $key = $this->extensionName . ':version'; - CRM_Core_BAO_Setting::setItem($revision, 'Extension', $key); + CRM_Core_BAO_Extension::setSchemaVersion($this->extensionName, $revision); + // clean up legacy schema version store (CRM-19252) + $this->deleteDeprecatedRevision(); return TRUE; } + private function deleteDeprecatedRevision() { + if ($this->revisionStorageIsDeprecated) { + $setting = new CRM_Core_BAO_Setting(); + $setting->name = $this->extensionName . ':version'; + $setting->delete(); + CRM_Core_Error::debug_log_message("Migrated extension schema revision ID for {$this->extensionName} from civicrm_setting (deprecated) to civicrm_extension.\n"); + } + } + // ******** Hook delegates ******** + /** + * @see https://docs.civicrm.org/dev/en/latest/hooks/hook_civicrm_install + */ public function onInstall() { $files = glob($this->extensionDir . '/sql/*_install.sql'); if (is_array($files)) { @@ -247,6 +290,12 @@ class CRM_Stripe_Upgrader_Base { CRM_Utils_File::sourceSQLFile(CIVICRM_DSN, $file); } } + $files = glob($this->extensionDir . '/sql/*_install.mysql.tpl'); + if (is_array($files)) { + foreach ($files as $file) { + $this->executeSqlTemplate($file); + } + } $files = glob($this->extensionDir . '/xml/*_install.xml'); if (is_array($files)) { foreach ($files as $file) { @@ -256,13 +305,31 @@ class CRM_Stripe_Upgrader_Base { if (is_callable(array($this, 'install'))) { $this->install(); } + } + + /** + * @see https://docs.civicrm.org/dev/en/latest/hooks/hook_civicrm_postInstall + */ + public function onPostInstall() { $revisions = $this->getRevisions(); if (!empty($revisions)) { $this->setCurrentRevision(max($revisions)); } + if (is_callable(array($this, 'postInstall'))) { + $this->postInstall(); + } } + /** + * @see https://docs.civicrm.org/dev/en/latest/hooks/hook_civicrm_uninstall + */ public function onUninstall() { + $files = glob($this->extensionDir . '/sql/*_uninstall.mysql.tpl'); + if (is_array($files)) { + foreach ($files as $file) { + $this->executeSqlTemplate($file); + } + } if (is_callable(array($this, 'uninstall'))) { $this->uninstall(); } @@ -272,9 +339,11 @@ class CRM_Stripe_Upgrader_Base { CRM_Utils_File::sourceSQLFile(CIVICRM_DSN, $file); } } - $this->setCurrentRevision(NULL); } + /** + * @see https://docs.civicrm.org/dev/en/latest/hooks/hook_civicrm_enable + */ public function onEnable() { // stub for possible future use if (is_callable(array($this, 'enable'))) { @@ -282,6 +351,9 @@ class CRM_Stripe_Upgrader_Base { } } + /** + * @see https://docs.civicrm.org/dev/en/latest/hooks/hook_civicrm_disable + */ public function onDisable() { // stub for possible future use if (is_callable(array($this, 'disable'))) { @@ -300,4 +372,5 @@ class CRM_Stripe_Upgrader_Base { default: } } + } diff --git a/stripe.civix.php b/stripe.civix.php index 6de525c..90b9159 100644 --- a/stripe.civix.php +++ b/stripe.civix.php @@ -24,9 +24,9 @@ class CRM_Stripe_ExtensionUtil { * Translated text. * @see ts */ - public static function ts($text, $params = array()) { + public static function ts($text, $params = []) { if (!array_key_exists('domain', $params)) { - $params['domain'] = array(self::LONG_NAME, NULL); + $params['domain'] = [self::LONG_NAME, NULL]; } return ts($text, $params); } @@ -82,7 +82,7 @@ use CRM_Stripe_ExtensionUtil as E; /** * (Delegated) Implements hook_civicrm_config(). * - * @link http://wiki.civicrm.org/confluence/display/CRMDOC/hook_civicrm_config + * @link https://docs.civicrm.org/dev/en/latest/hooks/hook_civicrm_config */ function _stripe_civix_civicrm_config(&$config = NULL) { static $configured = FALSE; @@ -100,7 +100,7 @@ function _stripe_civix_civicrm_config(&$config = NULL) { array_unshift($template->template_dir, $extDir); } else { - $template->template_dir = array($extDir, $template->template_dir); + $template->template_dir = [$extDir, $template->template_dir]; } $include_path = $extRoot . PATH_SEPARATOR . get_include_path(); @@ -112,7 +112,7 @@ function _stripe_civix_civicrm_config(&$config = NULL) { * * @param $files array(string) * - * @link http://wiki.civicrm.org/confluence/display/CRMDOC/hook_civicrm_xmlMenu + * @link https://docs.civicrm.org/dev/en/latest/hooks/hook_civicrm_xmlMenu */ function _stripe_civix_civicrm_xmlMenu(&$files) { foreach (_stripe_civix_glob(__DIR__ . '/xml/Menu/*.xml') as $file) { @@ -123,7 +123,7 @@ function _stripe_civix_civicrm_xmlMenu(&$files) { /** * Implements hook_civicrm_install(). * - * @link http://wiki.civicrm.org/confluence/display/CRMDOC/hook_civicrm_install + * @link https://docs.civicrm.org/dev/en/latest/hooks/hook_civicrm_install */ function _stripe_civix_civicrm_install() { _stripe_civix_civicrm_config(); @@ -135,12 +135,12 @@ function _stripe_civix_civicrm_install() { /** * Implements hook_civicrm_postInstall(). * - * @link http://wiki.civicrm.org/confluence/display/CRMDOC/hook_civicrm_postInstall + * @link https://docs.civicrm.org/dev/en/latest/hooks/hook_civicrm_postInstall */ function _stripe_civix_civicrm_postInstall() { _stripe_civix_civicrm_config(); if ($upgrader = _stripe_civix_upgrader()) { - if (is_callable(array($upgrader, 'onPostInstall'))) { + if (is_callable([$upgrader, 'onPostInstall'])) { $upgrader->onPostInstall(); } } @@ -149,7 +149,7 @@ function _stripe_civix_civicrm_postInstall() { /** * Implements hook_civicrm_uninstall(). * - * @link http://wiki.civicrm.org/confluence/display/CRMDOC/hook_civicrm_uninstall + * @link https://docs.civicrm.org/dev/en/latest/hooks/hook_civicrm_uninstall */ function _stripe_civix_civicrm_uninstall() { _stripe_civix_civicrm_config(); @@ -161,12 +161,12 @@ function _stripe_civix_civicrm_uninstall() { /** * (Delegated) Implements hook_civicrm_enable(). * - * @link http://wiki.civicrm.org/confluence/display/CRMDOC/hook_civicrm_enable + * @link https://docs.civicrm.org/dev/en/latest/hooks/hook_civicrm_enable */ function _stripe_civix_civicrm_enable() { _stripe_civix_civicrm_config(); if ($upgrader = _stripe_civix_upgrader()) { - if (is_callable(array($upgrader, 'onEnable'))) { + if (is_callable([$upgrader, 'onEnable'])) { $upgrader->onEnable(); } } @@ -175,13 +175,13 @@ function _stripe_civix_civicrm_enable() { /** * (Delegated) Implements hook_civicrm_disable(). * - * @link http://wiki.civicrm.org/confluence/display/CRMDOC/hook_civicrm_disable + * @link https://docs.civicrm.org/dev/en/latest/hooks/hook_civicrm_disable * @return mixed */ function _stripe_civix_civicrm_disable() { _stripe_civix_civicrm_config(); if ($upgrader = _stripe_civix_upgrader()) { - if (is_callable(array($upgrader, 'onDisable'))) { + if (is_callable([$upgrader, 'onDisable'])) { $upgrader->onDisable(); } } @@ -196,7 +196,7 @@ function _stripe_civix_civicrm_disable() { * @return mixed based on op. for 'check', returns array(boolean) (TRUE if upgrades are pending) * for 'enqueue', returns void * - * @link http://wiki.civicrm.org/confluence/display/CRMDOC/hook_civicrm_upgrade + * @link https://docs.civicrm.org/dev/en/latest/hooks/hook_civicrm_upgrade */ function _stripe_civix_civicrm_upgrade($op, CRM_Queue_Queue $queue = NULL) { if ($upgrader = _stripe_civix_upgrader()) { @@ -217,22 +217,23 @@ function _stripe_civix_upgrader() { } /** - * Search directory tree for files which match a glob pattern + * Search directory tree for files which match a glob pattern. * * Note: Dot-directories (like "..", ".git", or ".svn") will be ignored. * Note: In Civi 4.3+, delegate to CRM_Utils_File::findFiles() * - * @param $dir string, base dir - * @param $pattern string, glob pattern, eg "*.txt" + * @param string $dir base dir + * @param string $pattern , glob pattern, eg "*.txt" + * * @return array(string) */ function _stripe_civix_find_files($dir, $pattern) { - if (is_callable(array('CRM_Utils_File', 'findFiles'))) { + if (is_callable(['CRM_Utils_File', 'findFiles'])) { return CRM_Utils_File::findFiles($dir, $pattern); } - $todos = array($dir); - $result = array(); + $todos = [$dir]; + $result = []; while (!empty($todos)) { $subdir = array_shift($todos); foreach (_stripe_civix_glob("$subdir/$pattern") as $match) { @@ -259,7 +260,7 @@ function _stripe_civix_find_files($dir, $pattern) { * * Find any *.mgd.php files, merge their content, and return. * - * @link http://wiki.civicrm.org/confluence/display/CRMDOC/hook_civicrm_managed + * @link https://docs.civicrm.org/dev/en/latest/hooks/hook_civicrm_managed */ function _stripe_civix_civicrm_managed(&$entities) { $mgdFiles = _stripe_civix_find_files(__DIR__, '*.mgd.php'); @@ -285,7 +286,7 @@ function _stripe_civix_civicrm_managed(&$entities) { * * Note: This hook only runs in CiviCRM 4.4+. * - * @link http://wiki.civicrm.org/confluence/display/CRMDOC/hook_civicrm_caseTypes + * @link https://docs.civicrm.org/dev/en/latest/hooks/hook_civicrm_caseTypes */ function _stripe_civix_civicrm_caseTypes(&$caseTypes) { if (!is_dir(__DIR__ . '/xml/case')) { @@ -296,14 +297,13 @@ function _stripe_civix_civicrm_caseTypes(&$caseTypes) { $name = preg_replace('/\.xml$/', '', basename($file)); if ($name != CRM_Case_XMLProcessor::mungeCaseType($name)) { $errorMessage = sprintf("Case-type file name is malformed (%s vs %s)", $name, CRM_Case_XMLProcessor::mungeCaseType($name)); - CRM_Core_Error::fatal($errorMessage); - // throw new CRM_Core_Exception($errorMessage); + throw new CRM_Core_Exception($errorMessage); } - $caseTypes[$name] = array( + $caseTypes[$name] = [ 'module' => E::LONG_NAME, 'name' => $name, 'file' => $file, - ); + ]; } } @@ -314,7 +314,7 @@ function _stripe_civix_civicrm_caseTypes(&$caseTypes) { * * Note: This hook only runs in CiviCRM 4.5+. * - * @link http://wiki.civicrm.org/confluence/display/CRMDOC/hook_civicrm_angularModules + * @link https://docs.civicrm.org/dev/en/latest/hooks/hook_civicrm_angularModules */ function _stripe_civix_civicrm_angularModules(&$angularModules) { if (!is_dir(__DIR__ . '/ang')) { @@ -332,6 +332,25 @@ function _stripe_civix_civicrm_angularModules(&$angularModules) { } } +/** + * (Delegated) Implements hook_civicrm_themes(). + * + * Find any and return any files matching "*.theme.php" + */ +function _stripe_civix_civicrm_themes(&$themes) { + $files = _stripe_civix_glob(__DIR__ . '/*.theme.php'); + foreach ($files as $file) { + $themeMeta = include $file; + if (empty($themeMeta['name'])) { + $themeMeta['name'] = preg_replace(':\.theme\.php$:', '', basename($file)); + } + if (empty($themeMeta['ext'])) { + $themeMeta['ext'] = E::LONG_NAME; + } + $themes[$themeMeta['name']] = $themeMeta; + } +} + /** * Glob wrapper which is guaranteed to return an array. * @@ -342,11 +361,12 @@ function _stripe_civix_civicrm_angularModules(&$angularModules) { * * @link http://php.net/glob * @param string $pattern + * * @return array, possibly empty */ function _stripe_civix_glob($pattern) { $result = glob($pattern); - return is_array($result) ? $result : array(); + return is_array($result) ? $result : []; } /** @@ -357,16 +377,18 @@ function _stripe_civix_glob($pattern) { * 'Mailing', or 'Administer/System Settings' * @param array $item - the item to insert (parent/child attributes will be * filled for you) + * + * @return bool */ function _stripe_civix_insert_navigation_menu(&$menu, $path, $item) { // If we are done going down the path, insert menu if (empty($path)) { - $menu[] = array( - 'attributes' => array_merge(array( + $menu[] = [ + 'attributes' => array_merge([ 'label' => CRM_Utils_Array::value('name', $item), 'active' => 1, - ), $item), - ); + ], $item), + ]; return TRUE; } else { @@ -377,9 +399,9 @@ function _stripe_civix_insert_navigation_menu(&$menu, $path, $item) { foreach ($menu as $key => &$entry) { if ($entry['attributes']['name'] == $first) { if (!isset($entry['child'])) { - $entry['child'] = array(); + $entry['child'] = []; } - $found = _stripe_civix_insert_navigation_menu($entry['child'], implode('/', $path), $item, $key); + $found = _stripe_civix_insert_navigation_menu($entry['child'], implode('/', $path), $item); } } return $found; @@ -390,7 +412,7 @@ function _stripe_civix_insert_navigation_menu(&$menu, $path, $item) { * (Delegated) Implements hook_civicrm_navigationMenu(). */ function _stripe_civix_navigationMenu(&$nodes) { - if (!is_callable(array('CRM_Core_BAO_Navigation', 'fixNavigationMenu'))) { + if (!is_callable(['CRM_Core_BAO_Navigation', 'fixNavigationMenu'])) { _stripe_civix_fixNavigationMenu($nodes); } } @@ -432,17 +454,11 @@ function _stripe_civix_fixNavigationMenuItems(&$nodes, &$maxNavID, $parentID) { /** * (Delegated) Implements hook_civicrm_alterSettingsFolders(). * - * @link http://wiki.civicrm.org/confluence/display/CRMDOC/hook_civicrm_alterSettingsFolders + * @link https://docs.civicrm.org/dev/en/latest/hooks/hook_civicrm_alterSettingsFolders */ function _stripe_civix_civicrm_alterSettingsFolders(&$metaDataFolders = NULL) { - static $configured = FALSE; - if ($configured) { - return; - } - $configured = TRUE; - $settingsDir = __DIR__ . DIRECTORY_SEPARATOR . 'settings'; - if (is_dir($settingsDir) && !in_array($settingsDir, $metaDataFolders)) { + if (!in_array($settingsDir, $metaDataFolders) && is_dir($settingsDir)) { $metaDataFolders[] = $settingsDir; } } @@ -452,7 +468,7 @@ function _stripe_civix_civicrm_alterSettingsFolders(&$metaDataFolders = NULL) { * * Find any *.entityType.php files, merge their content, and return. * - * @link http://wiki.civicrm.org/confluence/display/CRMDOC/hook_civicrm_entityTypes + * @link https://docs.civicrm.org/dev/en/latest/hooks/hook_civicrm_entityTypes */ function _stripe_civix_civicrm_entityTypes(&$entityTypes) { diff --git a/tests/phpunit/CRM/Stripe/BaseTest.php b/tests/phpunit/CRM/Stripe/BaseTest.php index 3f44473..25c7b55 100644 --- a/tests/phpunit/CRM/Stripe/BaseTest.php +++ b/tests/phpunit/CRM/Stripe/BaseTest.php @@ -45,6 +45,7 @@ class CRM_Stripe_BaseTest extends \PHPUnit\Framework\TestCase implements Headles // Civi\Test has many helpers, like install(), uninstall(), sql(), and sqlFile(). // See: https://github.com/civicrm/org.civicrm.testapalooza/blob/master/civi-test.md return \Civi\Test::headless() + ->install('mjwshared') ->installMe(__DIR__) ->apply(); } @@ -252,5 +253,4 @@ class CRM_Stripe_BaseTest extends \PHPUnit\Framework\TestCase implements Headles CRM_Utils_Cache::singleton()->flush(); } - } diff --git a/tests/phpunit/CRM/Stripe/DirectTest.php b/tests/phpunit/CRM/Stripe/DirectTest.php index a30ac64..b15bd5d 100644 --- a/tests/phpunit/CRM/Stripe/DirectTest.php +++ b/tests/phpunit/CRM/Stripe/DirectTest.php @@ -19,7 +19,7 @@ use Civi\Test\TransactionalInterface; * @group headless */ require ('BaseTest.php'); -class CRM_Stripe_IpnTest extends CRM_Stripe_BaseTest { +class CRM_Stripe_DirectTest extends CRM_Stripe_BaseTest { protected $_contributionRecurID; protected $_total = '200'; @@ -28,6 +28,7 @@ class CRM_Stripe_IpnTest extends CRM_Stripe_BaseTest { // Civi\Test has many helpers, like install(), uninstall(), sql(), and sqlFile(). // See: https://github.com/civicrm/org.civicrm.testapalooza/blob/master/civi-test.md return \Civi\Test::headless() + ->install('mjwshared') ->installMe(__DIR__) ->apply(); } @@ -49,5 +50,4 @@ class CRM_Stripe_IpnTest extends CRM_Stripe_BaseTest { $this->assertValidTrxn(); } - } diff --git a/tests/phpunit/CRM/Stripe/IpnTest.php b/tests/phpunit/CRM/Stripe/IpnTest.php index d616615..acbf30d 100644 --- a/tests/phpunit/CRM/Stripe/IpnTest.php +++ b/tests/phpunit/CRM/Stripe/IpnTest.php @@ -32,6 +32,7 @@ class CRM_Stripe_IpnTest extends CRM_Stripe_BaseTest { public function setUpHeadless() { $force = TRUE; return \Civi\Test::headless() + ->install('mjwshared') ->installMe(__DIR__) ->apply($force); } @@ -298,4 +299,5 @@ class CRM_Stripe_IpnTest extends CRM_Stripe_BaseTest { $this->_contributionRecurID = $contributionRecur['id']; $this->_contributionID = $contributionRecur['values']['0']['api.contribution.create']['id']; } + } -- GitLab From a6fc1ccfc617581211c887e4fcf15abb131a0534 Mon Sep 17 00:00:00 2001 From: Matthew Wire Date: Fri, 24 Jan 2020 13:07:51 +0000 Subject: [PATCH 07/45] Add jquery form event 'crmBillingFormNotValid' so 3rd-party integrations can re-enable custom submit buttons etc. --- js/civicrm_stripe.js | 1 + js/civicrm_stripe.min.js | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/js/civicrm_stripe.js b/js/civicrm_stripe.js index 8927583..935270c 100644 --- a/js/civicrm_stripe.js +++ b/js/civicrm_stripe.js @@ -338,6 +338,7 @@ CRM.$(function($) { $('div#card-errors').hide(); document.querySelector('#billing-payment-block').scrollIntoView(); window.scrollBy(0, -50); + $(form).trigger('crmBillingFormNotValid'); return false; } diff --git a/js/civicrm_stripe.min.js b/js/civicrm_stripe.min.js index fd31fb4..d348072 100644 --- a/js/civicrm_stripe.min.js +++ b/js/civicrm_stripe.min.js @@ -1 +1 @@ -CRM.$(function(d){g("civicrm_stripe loaded, dom-ready function firing.");if(window.civicrmStripeHandleReload){g("calling existing civicrmStripeHandleReload.");window.civicrmStripeHandleReload();return}var s;var b;var c;var w;var q=false;window.onbeforeunload=null;window.civicrmStripeHandleReload=function(){g("civicrmStripeHandleReload");var D=document.getElementById("card-element");if((typeof D!=="undefined")&&(D)){if(!D.children.length){g("checkAndLoad from document.ready");o()}}};window.civicrmStripeHandleReload();function x(F,D){g(F+": success - submitting form");var E=document.createElement("input");E.setAttribute("type","hidden");E.setAttribute("name",F);E.setAttribute("value",D.id);c.appendChild(E);c.submit()}function r(){for(i=0;i1){D=true}}if(document.getElementById("is_recur")!==null){if(document.getElementById("is_recur").type=="hidden"){D=(document.getElementById("is_recur").value==1)}else{D=Boolean(document.getElementById("is_recur").checked)}}else{if(d('input[name="auto_renew"]').length!==0){if(d('input[name="auto_renew"]').prop("checked")){D=true}else{if(document.getElementById("auto_renew").type=="hidden"){D=(document.getElementById("auto_renew").value==1)}else{D=Boolean(document.getElementById("auto_renew").checked)}}}}g("isRecur is "+D);return D}function k(E){if(E.empty){d("div#card-errors").hide()}else{if(E.error){j(E.error)}else{if(E.complete){d("div#card-errors").hide();var D=document.getElementById("billing_postal_code-"+CRM.vars.stripe.billingAddressID);if(D){D.value=E.value.postalCode}}}}}function p(){cividiscountElements=c.querySelectorAll("input#discountcode");var D=function(E){if(E.keyCode===13){E.preventDefault();g("adding submitdontprocess");c.dataset.submitdontprocess=true}};for(i=0;i1){D=true}}if(document.getElementById("is_recur")!==null){if(document.getElementById("is_recur").type=="hidden"){D=(document.getElementById("is_recur").value==1)}else{D=Boolean(document.getElementById("is_recur").checked)}}else{if(d('input[name="auto_renew"]').length!==0){if(d('input[name="auto_renew"]').prop("checked")){D=true}else{if(document.getElementById("auto_renew").type=="hidden"){D=(document.getElementById("auto_renew").value==1)}else{D=Boolean(document.getElementById("auto_renew").checked)}}}}g("isRecur is "+D);return D}function k(E){if(E.empty){d("div#card-errors").hide()}else{if(E.error){j(E.error)}else{if(E.complete){d("div#card-errors").hide();var D=document.getElementById("billing_postal_code-"+CRM.vars.stripe.billingAddressID);if(D){D.value=E.value.postalCode}}}}}function p(){cividiscountElements=c.querySelectorAll("input#discountcode");var D=function(E){if(E.keyCode===13){E.preventDefault();g("adding submitdontprocess");c.dataset.submitdontprocess=true}};for(i=0;i Date: Mon, 3 Feb 2020 23:47:46 +0000 Subject: [PATCH 08/45] Add jquery form event 'crmBillingFormReloadComplete' and document jquery events --- docs/events.md | 23 +++++++++++++++++++++++ js/civicrm_stripe.js | 2 ++ js/civicrm_stripe.min.js | 2 +- mkdocs.yml | 1 + 4 files changed, 27 insertions(+), 1 deletion(-) create mode 100644 docs/events.md diff --git a/docs/events.md b/docs/events.md new file mode 100644 index 0000000..777529f --- /dev/null +++ b/docs/events.md @@ -0,0 +1,23 @@ +# Events (jQuery) + +Most of the functionality to take card details, validate the form etc. happens on the javascript/jquery side before the form is submitted. + +If you are customising the frontend form you may need to respond to events triggered by the Stripe extension. + +## Available Events + +### crmBillingFormNotValid +This event is triggered when the form has been submitted but fails because of a validation error on the form. +It is most useful for re-enabling form elements that were disabled during submission. + +Example code: +```javascript + $form.on('crmBillingFormNotValid', e => { + console.log("resetting submit button as form not submitted"); + $customSubmitButton.prop('disabled', false).text($customSubmitButton.data('text')); + }); +``` + +### crmBillingFormReloadComplete +This event is triggered when the form has completed reloading and is ready for use (Stripe element visible etc.). +It is useful for clearing any "loading" indicators and unfreezing form elements. diff --git a/js/civicrm_stripe.js b/js/civicrm_stripe.js index 935270c..4d31ae9 100644 --- a/js/civicrm_stripe.js +++ b/js/civicrm_stripe.js @@ -35,6 +35,8 @@ CRM.$(function($) { checkAndLoad(); } } + debugging('reload completed.'); + $(form).trigger('crmBillingFormReloadComplete'); }; // On initial run we need to call this now. window.civicrmStripeHandleReload(); diff --git a/js/civicrm_stripe.min.js b/js/civicrm_stripe.min.js index d348072..5ef7055 100644 --- a/js/civicrm_stripe.min.js +++ b/js/civicrm_stripe.min.js @@ -1 +1 @@ -CRM.$(function(d){g("civicrm_stripe loaded, dom-ready function firing.");if(window.civicrmStripeHandleReload){g("calling existing civicrmStripeHandleReload.");window.civicrmStripeHandleReload();return}var s;var b;var c;var w;var q=false;window.onbeforeunload=null;window.civicrmStripeHandleReload=function(){g("civicrmStripeHandleReload");var D=document.getElementById("card-element");if((typeof D!=="undefined")&&(D)){if(!D.children.length){g("checkAndLoad from document.ready");o()}}};window.civicrmStripeHandleReload();function x(F,D){g(F+": success - submitting form");var E=document.createElement("input");E.setAttribute("type","hidden");E.setAttribute("name",F);E.setAttribute("value",D.id);c.appendChild(E);c.submit()}function r(){for(i=0;i1){D=true}}if(document.getElementById("is_recur")!==null){if(document.getElementById("is_recur").type=="hidden"){D=(document.getElementById("is_recur").value==1)}else{D=Boolean(document.getElementById("is_recur").checked)}}else{if(d('input[name="auto_renew"]').length!==0){if(d('input[name="auto_renew"]').prop("checked")){D=true}else{if(document.getElementById("auto_renew").type=="hidden"){D=(document.getElementById("auto_renew").value==1)}else{D=Boolean(document.getElementById("auto_renew").checked)}}}}g("isRecur is "+D);return D}function k(E){if(E.empty){d("div#card-errors").hide()}else{if(E.error){j(E.error)}else{if(E.complete){d("div#card-errors").hide();var D=document.getElementById("billing_postal_code-"+CRM.vars.stripe.billingAddressID);if(D){D.value=E.value.postalCode}}}}}function p(){cividiscountElements=c.querySelectorAll("input#discountcode");var D=function(E){if(E.keyCode===13){E.preventDefault();g("adding submitdontprocess");c.dataset.submitdontprocess=true}};for(i=0;i1){D=true}}if(document.getElementById("is_recur")!==null){if(document.getElementById("is_recur").type=="hidden"){D=(document.getElementById("is_recur").value==1)}else{D=Boolean(document.getElementById("is_recur").checked)}}else{if(d('input[name="auto_renew"]').length!==0){if(d('input[name="auto_renew"]').prop("checked")){D=true}else{if(document.getElementById("auto_renew").type=="hidden"){D=(document.getElementById("auto_renew").value==1)}else{D=Boolean(document.getElementById("auto_renew").checked)}}}}g("isRecur is "+D);return D}function k(E){if(E.empty){d("div#card-errors").hide()}else{if(E.error){j(E.error)}else{if(E.complete){d("div#card-errors").hide();var D=document.getElementById("billing_postal_code-"+CRM.vars.stripe.billingAddressID);if(D){D.value=E.value.postalCode}}}}}function p(){cividiscountElements=c.querySelectorAll("input#discountcode");var D=function(E){if(E.keyCode===13){E.preventDefault();g("adding submitdontprocess");c.dataset.submitdontprocess=true}};for(i=0;i Date: Wed, 5 Feb 2020 12:11:28 +0000 Subject: [PATCH 09/45] Trigger crmBillingFormNotValid for stripe errors as well --- js/civicrm_stripe.js | 11 ++++++++--- js/civicrm_stripe.min.js | 2 +- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/js/civicrm_stripe.js b/js/civicrm_stripe.js index 4d31ae9..fc45001 100644 --- a/js/civicrm_stripe.js +++ b/js/civicrm_stripe.js @@ -35,8 +35,7 @@ CRM.$(function($) { checkAndLoad(); } } - debugging('reload completed.'); - $(form).trigger('crmBillingFormReloadComplete'); + triggerEvent('crmBillingFormReloadComplete'); }; // On initial run we need to call this now. window.civicrmStripeHandleReload(); @@ -76,6 +75,7 @@ CRM.$(function($) { for (i = 0; i < submitButtons.length; ++i) { submitButtons[i].removeAttribute('disabled'); } + triggerEvent('crmBillingFormNotValid'); } function handleCardPayment() { @@ -340,7 +340,7 @@ CRM.$(function($) { $('div#card-errors').hide(); document.querySelector('#billing-payment-block').scrollIntoView(); window.scrollBy(0, -50); - $(form).trigger('crmBillingFormNotValid'); + triggerEvent('crmBillingFormNotValid'); return false; } @@ -628,6 +628,11 @@ CRM.$(function($) { } } + function triggerEvent(event) { + debugging('Firing Event: ' + event); + $(form).trigger(event); + } + function addDrupalWebformActionElement(submitAction) { var hiddenInput = null; if (document.getElementById('action') !== null) { diff --git a/js/civicrm_stripe.min.js b/js/civicrm_stripe.min.js index 5ef7055..f02fb4b 100644 --- a/js/civicrm_stripe.min.js +++ b/js/civicrm_stripe.min.js @@ -1 +1 @@ -CRM.$(function(d){g("civicrm_stripe loaded, dom-ready function firing.");if(window.civicrmStripeHandleReload){g("calling existing civicrmStripeHandleReload.");window.civicrmStripeHandleReload();return}var s;var b;var c;var w;var q=false;window.onbeforeunload=null;window.civicrmStripeHandleReload=function(){g("civicrmStripeHandleReload");var D=document.getElementById("card-element");if((typeof D!=="undefined")&&(D)){if(!D.children.length){g("checkAndLoad from document.ready");o()}}g("reload completed.");d(c).trigger("crmBillingFormReloadComplete")};window.civicrmStripeHandleReload();function x(F,D){g(F+": success - submitting form");var E=document.createElement("input");E.setAttribute("type","hidden");E.setAttribute("name",F);E.setAttribute("value",D.id);c.appendChild(E);c.submit()}function r(){for(i=0;i1){D=true}}if(document.getElementById("is_recur")!==null){if(document.getElementById("is_recur").type=="hidden"){D=(document.getElementById("is_recur").value==1)}else{D=Boolean(document.getElementById("is_recur").checked)}}else{if(d('input[name="auto_renew"]').length!==0){if(d('input[name="auto_renew"]').prop("checked")){D=true}else{if(document.getElementById("auto_renew").type=="hidden"){D=(document.getElementById("auto_renew").value==1)}else{D=Boolean(document.getElementById("auto_renew").checked)}}}}g("isRecur is "+D);return D}function k(E){if(E.empty){d("div#card-errors").hide()}else{if(E.error){j(E.error)}else{if(E.complete){d("div#card-errors").hide();var D=document.getElementById("billing_postal_code-"+CRM.vars.stripe.billingAddressID);if(D){D.value=E.value.postalCode}}}}}function p(){cividiscountElements=c.querySelectorAll("input#discountcode");var D=function(E){if(E.keyCode===13){E.preventDefault();g("adding submitdontprocess");c.dataset.submitdontprocess=true}};for(i=0;i1){E=true}}if(document.getElementById("is_recur")!==null){if(document.getElementById("is_recur").type=="hidden"){E=(document.getElementById("is_recur").value==1)}else{E=Boolean(document.getElementById("is_recur").checked)}}else{if(e('input[name="auto_renew"]').length!==0){if(e('input[name="auto_renew"]').prop("checked")){E=true}else{if(document.getElementById("auto_renew").type=="hidden"){E=(document.getElementById("auto_renew").value==1)}else{E=Boolean(document.getElementById("auto_renew").checked)}}}}h("isRecur is "+E);return E}function l(F){if(F.empty){e("div#card-errors").hide()}else{if(F.error){k(F.error)}else{if(F.complete){e("div#card-errors").hide();var E=document.getElementById("billing_postal_code-"+CRM.vars.stripe.billingAddressID);if(E){E.value=F.value.postalCode}}}}}function q(){cividiscountElements=c.querySelectorAll("input#discountcode");var E=function(F){if(F.keyCode===13){F.preventDefault();h("adding submitdontprocess");c.dataset.submitdontprocess=true}};for(i=0;i Date: Wed, 5 Feb 2020 14:14:12 +0000 Subject: [PATCH 10/45] Add support for sweetalert library on form validation errors --- js/civicrm_stripe.js | 51 +++++++++++++++++++++++++++++++++----------- 1 file changed, 39 insertions(+), 12 deletions(-) diff --git a/js/civicrm_stripe.js b/js/civicrm_stripe.js index fc45001..c695e92 100644 --- a/js/civicrm_stripe.js +++ b/js/civicrm_stripe.js @@ -76,6 +76,7 @@ CRM.$(function($) { submitButtons[i].removeAttribute('disabled'); } triggerEvent('crmBillingFormNotValid'); + notifyUser('error', ts('Oops...'), result.error.message); } function handleCardPayment() { @@ -341,6 +342,7 @@ CRM.$(function($) { document.querySelector('#billing-payment-block').scrollIntoView(); window.scrollBy(0, -50); triggerEvent('crmBillingFormNotValid'); + notifyUser('error', ts('Oops...'), ts('Please correct the errors on the form and submit again')); return false; } @@ -621,18 +623,6 @@ CRM.$(function($) { return false; } - function debugging(errorCode) { - // Uncomment the following to debug unexpected returns. - if ((typeof(CRM.vars.stripe) === 'undefined') || (Boolean(CRM.vars.stripe.jsDebug) === true)) { - console.log(new Date().toISOString() + ' civicrm_stripe.js: ' + errorCode); - } - } - - function triggerEvent(event) { - debugging('Firing Event: ' + event); - $(form).trigger(event); - } - function addDrupalWebformActionElement(submitAction) { var hiddenInput = null; if (document.getElementById('action') !== null) { @@ -666,4 +656,41 @@ CRM.$(function($) { return null; } + /** + * Output debug information + * @param {string} errorCode + */ + function debugging(errorCode) { + // Uncomment the following to debug unexpected returns. + if ((typeof(CRM.vars.stripe) === 'undefined') || (Boolean(CRM.vars.stripe.jsDebug) === true)) { + console.log(new Date().toISOString() + ' civicrm_stripe.js: ' + errorCode); + } + } + + /** + * Trigger a jQuery event + * @param {string} event + */ + function triggerEvent(event) { + debugging('Firing Event: ' + event); + $(form).trigger(event); + } + + /** + * If we have the sweetalert2 library popup a nice message to the user. + * Otherwise do nothing + * @param {string} icon + * @param {string} title + * @param {string} text + */ + function notifyUser(icon, title, text) { + if (typeof Swal === 'function') { + Swal.fire({ + icon: icon, + title: title, + text: text, + }); + } + } + }); -- GitLab From 60c248fb4dd880cf99285f843b4a640bc776771f Mon Sep 17 00:00:00 2001 From: Matthew Wire Date: Wed, 5 Feb 2020 14:15:28 +0000 Subject: [PATCH 11/45] Switch to event.code from deprecated event.keyCode --- js/civicrm_stripe.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/js/civicrm_stripe.js b/js/civicrm_stripe.js index c695e92..2a54a78 100644 --- a/js/civicrm_stripe.js +++ b/js/civicrm_stripe.js @@ -321,8 +321,8 @@ CRM.$(function($) { addDrupalWebformActionElement(this.value); }); // If enter pressed, use our submit function - form.addEventListener('keydown', function (e) { - if (e.keyCode === 13) { + form.addEventListener('keydown', function (event) { + if (event.code === 'Enter') { addDrupalWebformActionElement(this.value); submit(event); } @@ -588,9 +588,9 @@ CRM.$(function($) { function addSupportForCiviDiscount() { // Add a keypress handler to set flag if enter is pressed cividiscountElements = form.querySelectorAll('input#discountcode'); - var cividiscountHandleKeydown = function(e) { - if (e.keyCode === 13) { - e.preventDefault(); + var cividiscountHandleKeydown = function(event) { + if (event.code === 'Enter') { + event.preventDefault(); debugging('adding submitdontprocess'); form.dataset.submitdontprocess = true; } -- GitLab From c9184cd1275a360d4f76e541ae0dec0273cbf154 Mon Sep 17 00:00:00 2001 From: Matthew Wire Date: Wed, 5 Feb 2020 14:15:46 +0000 Subject: [PATCH 12/45] Add some docblocks --- js/civicrm_stripe.js | 27 +++++++++++++++++++++------ js/civicrm_stripe.min.js | 2 +- 2 files changed, 22 insertions(+), 7 deletions(-) diff --git a/js/civicrm_stripe.js b/js/civicrm_stripe.js index 2a54a78..02daaf8 100644 --- a/js/civicrm_stripe.js +++ b/js/civicrm_stripe.js @@ -483,6 +483,10 @@ CRM.$(function($) { return submit; } + /** + * Get the total amount on the form + * @returns {number} + */ function getTotalAmount() { var totalFee = 0.0; if (isEventAdditionalParticipants()) { @@ -512,11 +516,15 @@ CRM.$(function($) { return totalFee; } - // This is calculated in CRM/Contribute/Form/Contribution.tpl and is used to calculate the total - // amount with tax on backend submit contribution forms. - // The only way we can get the amount is by parsing the text field and extracting the final bit after the space. - // eg. "Amount including Tax: $ 4.50" gives us 4.50. - // The PHP side is responsible for converting money formats (we just parse to cents and remove any ,. chars). + /** + * This is calculated in CRM/Contribute/Form/Contribution.tpl and is used to calculate the total + * amount with tax on backend submit contribution forms. + * The only way we can get the amount is by parsing the text field and extracting the final bit after the space. + * eg. "Amount including Tax: $ 4.50" gives us 4.50. + * The PHP side is responsible for converting money formats (we just parse to cents and remove any ,. chars). + * + * @returns {string|prototype.value|number} + */ function calculateTaxAmount() { var totalTaxAmount = 0; if (document.getElementById('totalTaxAmount') === null) { @@ -534,6 +542,10 @@ CRM.$(function($) { return totalTaxAmount; } + /** + * Are we creating a recurring contribution? + * @returns {boolean} + */ function getIsRecur() { var isRecur = false; // Auto-renew contributions for CiviCRM Webforms. @@ -614,6 +626,9 @@ CRM.$(function($) { validator.settings.ignore = '.select2-offscreen, [readonly], :hidden:not(.crm-select2)'; } + /** + * @returns {boolean} + */ function isEventAdditionalParticipants() { if ((document.getElementById('additional_participants') !== null) && (document.getElementById('additional_participants').value.length !== 0)) { @@ -640,7 +655,7 @@ CRM.$(function($) { /** * Get the selected payment processor on the form - * @returns int + * @returns {null|number} */ function getPaymentProcessorSelectorValue() { if ((typeof form === 'undefined') || (!form)) { diff --git a/js/civicrm_stripe.min.js b/js/civicrm_stripe.min.js index f02fb4b..21be876 100644 --- a/js/civicrm_stripe.min.js +++ b/js/civicrm_stripe.min.js @@ -1 +1 @@ -CRM.$(function(e){h("civicrm_stripe loaded, dom-ready function firing.");if(window.civicrmStripeHandleReload){h("calling existing civicrmStripeHandleReload.");window.civicrmStripeHandleReload();return}var t;var b;var c;var x;var r=false;window.onbeforeunload=null;window.civicrmStripeHandleReload=function(){h("civicrmStripeHandleReload");var E=document.getElementById("card-element");if((typeof E!=="undefined")&&(E)){if(!E.children.length){h("checkAndLoad from document.ready");p()}}d("crmBillingFormReloadComplete")};window.civicrmStripeHandleReload();function y(G,E){h(G+": success - submitting form");var F=document.createElement("input");F.setAttribute("type","hidden");F.setAttribute("name",G);F.setAttribute("value",E.id);c.appendChild(F);c.submit()}function s(){for(i=0;i1){E=true}}if(document.getElementById("is_recur")!==null){if(document.getElementById("is_recur").type=="hidden"){E=(document.getElementById("is_recur").value==1)}else{E=Boolean(document.getElementById("is_recur").checked)}}else{if(e('input[name="auto_renew"]').length!==0){if(e('input[name="auto_renew"]').prop("checked")){E=true}else{if(document.getElementById("auto_renew").type=="hidden"){E=(document.getElementById("auto_renew").value==1)}else{E=Boolean(document.getElementById("auto_renew").checked)}}}}h("isRecur is "+E);return E}function l(F){if(F.empty){e("div#card-errors").hide()}else{if(F.error){k(F.error)}else{if(F.complete){e("div#card-errors").hide();var E=document.getElementById("billing_postal_code-"+CRM.vars.stripe.billingAddressID);if(E){E.value=F.value.postalCode}}}}}function q(){cividiscountElements=c.querySelectorAll("input#discountcode");var E=function(F){if(F.keyCode===13){F.preventDefault();h("adding submitdontprocess");c.dataset.submitdontprocess=true}};for(i=0;i1){F=true}}if(document.getElementById("is_recur")!==null){if(document.getElementById("is_recur").type=="hidden"){F=(document.getElementById("is_recur").value==1)}else{F=Boolean(document.getElementById("is_recur").checked)}}else{if(e('input[name="auto_renew"]').length!==0){if(e('input[name="auto_renew"]').prop("checked")){F=true}else{if(document.getElementById("auto_renew").type=="hidden"){F=(document.getElementById("auto_renew").value==1)}else{F=Boolean(document.getElementById("auto_renew").checked)}}}}h("isRecur is "+F);return F}function m(G){if(G.empty){e("div#card-errors").hide()}else{if(G.error){k(G.error)}else{if(G.complete){e("div#card-errors").hide();var F=document.getElementById("billing_postal_code-"+CRM.vars.stripe.billingAddressID);if(F){F.value=G.value.postalCode}}}}}function r(){cividiscountElements=c.querySelectorAll("input#discountcode");var F=function(G){if(G.code==="Enter"){G.preventDefault();h("adding submitdontprocess");c.dataset.submitdontprocess=true}};for(i=0;i Date: Wed, 5 Feb 2020 22:22:48 +0000 Subject: [PATCH 13/45] Change wording on popup messages --- js/civicrm_stripe.js | 4 ++-- js/civicrm_stripe.min.js | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/js/civicrm_stripe.js b/js/civicrm_stripe.js index 02daaf8..3d27ae4 100644 --- a/js/civicrm_stripe.js +++ b/js/civicrm_stripe.js @@ -76,7 +76,7 @@ CRM.$(function($) { submitButtons[i].removeAttribute('disabled'); } triggerEvent('crmBillingFormNotValid'); - notifyUser('error', ts('Oops...'), result.error.message); + notifyUser('error', '', result.error.message); } function handleCardPayment() { @@ -342,7 +342,7 @@ CRM.$(function($) { document.querySelector('#billing-payment-block').scrollIntoView(); window.scrollBy(0, -50); triggerEvent('crmBillingFormNotValid'); - notifyUser('error', ts('Oops...'), ts('Please correct the errors on the form and submit again')); + notifyUser('error', '', ts('Please check and fill in all required fields!')); return false; } diff --git a/js/civicrm_stripe.min.js b/js/civicrm_stripe.min.js index 21be876..d729191 100644 --- a/js/civicrm_stripe.min.js +++ b/js/civicrm_stripe.min.js @@ -1 +1 @@ -CRM.$(function(e){h("civicrm_stripe loaded, dom-ready function firing.");if(window.civicrmStripeHandleReload){h("calling existing civicrmStripeHandleReload.");window.civicrmStripeHandleReload();return}var u;var b;var c;var y;var s=false;window.onbeforeunload=null;window.civicrmStripeHandleReload=function(){h("civicrmStripeHandleReload");var F=document.getElementById("card-element");if((typeof F!=="undefined")&&(F)){if(!F.children.length){h("checkAndLoad from document.ready");q()}}d("crmBillingFormReloadComplete")};window.civicrmStripeHandleReload();function z(H,F){h(H+": success - submitting form");var G=document.createElement("input");G.setAttribute("type","hidden");G.setAttribute("name",H);G.setAttribute("value",F.id);c.appendChild(G);c.submit()}function t(){for(i=0;i1){F=true}}if(document.getElementById("is_recur")!==null){if(document.getElementById("is_recur").type=="hidden"){F=(document.getElementById("is_recur").value==1)}else{F=Boolean(document.getElementById("is_recur").checked)}}else{if(e('input[name="auto_renew"]').length!==0){if(e('input[name="auto_renew"]').prop("checked")){F=true}else{if(document.getElementById("auto_renew").type=="hidden"){F=(document.getElementById("auto_renew").value==1)}else{F=Boolean(document.getElementById("auto_renew").checked)}}}}h("isRecur is "+F);return F}function m(G){if(G.empty){e("div#card-errors").hide()}else{if(G.error){k(G.error)}else{if(G.complete){e("div#card-errors").hide();var F=document.getElementById("billing_postal_code-"+CRM.vars.stripe.billingAddressID);if(F){F.value=G.value.postalCode}}}}}function r(){cividiscountElements=c.querySelectorAll("input#discountcode");var F=function(G){if(G.code==="Enter"){G.preventDefault();h("adding submitdontprocess");c.dataset.submitdontprocess=true}};for(i=0;i1){F=true}}if(document.getElementById("is_recur")!==null){if(document.getElementById("is_recur").type=="hidden"){F=(document.getElementById("is_recur").value==1)}else{F=Boolean(document.getElementById("is_recur").checked)}}else{if(e('input[name="auto_renew"]').length!==0){if(e('input[name="auto_renew"]').prop("checked")){F=true}else{if(document.getElementById("auto_renew").type=="hidden"){F=(document.getElementById("auto_renew").value==1)}else{F=Boolean(document.getElementById("auto_renew").checked)}}}}h("isRecur is "+F);return F}function m(G){if(G.empty){e("div#card-errors").hide()}else{if(G.error){k(G.error)}else{if(G.complete){e("div#card-errors").hide();var F=document.getElementById("billing_postal_code-"+CRM.vars.stripe.billingAddressID);if(F){F.value=G.value.postalCode}}}}}function r(){cividiscountElements=c.querySelectorAll("input#discountcode");var F=function(G){if(G.code==="Enter"){G.preventDefault();h("adding submitdontprocess");c.dataset.submitdontprocess=true}};for(i=0;i Date: Wed, 5 Feb 2020 20:46:44 +0000 Subject: [PATCH 14/45] Stripe element uses CMS/Civi locale --- js/civicrm_stripe.min.js | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/js/civicrm_stripe.min.js b/js/civicrm_stripe.min.js index d729191..def145d 100644 --- a/js/civicrm_stripe.min.js +++ b/js/civicrm_stripe.min.js @@ -1 +1,9 @@ -CRM.$(function(e){h("civicrm_stripe loaded, dom-ready function firing.");if(window.civicrmStripeHandleReload){h("calling existing civicrmStripeHandleReload.");window.civicrmStripeHandleReload();return}var u;var b;var c;var y;var s=false;window.onbeforeunload=null;window.civicrmStripeHandleReload=function(){h("civicrmStripeHandleReload");var F=document.getElementById("card-element");if((typeof F!=="undefined")&&(F)){if(!F.children.length){h("checkAndLoad from document.ready");q()}}d("crmBillingFormReloadComplete")};window.civicrmStripeHandleReload();function z(H,F){h(H+": success - submitting form");var G=document.createElement("input");G.setAttribute("type","hidden");G.setAttribute("name",H);G.setAttribute("value",F.id);c.appendChild(G);c.submit()}function t(){for(i=0;i1){F=true}}if(document.getElementById("is_recur")!==null){if(document.getElementById("is_recur").type=="hidden"){F=(document.getElementById("is_recur").value==1)}else{F=Boolean(document.getElementById("is_recur").checked)}}else{if(e('input[name="auto_renew"]').length!==0){if(e('input[name="auto_renew"]').prop("checked")){F=true}else{if(document.getElementById("auto_renew").type=="hidden"){F=(document.getElementById("auto_renew").value==1)}else{F=Boolean(document.getElementById("auto_renew").checked)}}}}h("isRecur is "+F);return F}function m(G){if(G.empty){e("div#card-errors").hide()}else{if(G.error){k(G.error)}else{if(G.complete){e("div#card-errors").hide();var F=document.getElementById("billing_postal_code-"+CRM.vars.stripe.billingAddressID);if(F){F.value=G.value.postalCode}}}}}function r(){cividiscountElements=c.querySelectorAll("input#discountcode");var F=function(G){if(G.code==="Enter"){G.preventDefault();h("adding submitdontprocess");c.dataset.submitdontprocess=true}};for(i=0;i1){F=true}}if(document.getElementById("is_recur")!==null){if(document.getElementById("is_recur").type=="hidden"){F=(document.getElementById("is_recur").value==1)}else{F=Boolean(document.getElementById("is_recur").checked)}}else{if(e('input[name="auto_renew"]').length!==0){if(e('input[name="auto_renew"]').prop("checked")){F=true}else{if(document.getElementById("auto_renew").type=="hidden"){F=(document.getElementById("auto_renew").value==1)}else{F=Boolean(document.getElementById("auto_renew").checked)}}}}h("isRecur is "+F);return F}function m(G){if(G.empty){e("div#card-errors").hide()}else{if(G.error){k(G.error)}else{if(G.complete){e("div#card-errors").hide();var F=document.getElementById("billing_postal_code-"+CRM.vars.stripe.billingAddressID);if(F){F.value=G.value.postalCode}}}}}function r(){cividiscountElements=c.querySelectorAll("input#discountcode");var F=function(G){if(G.code==="Enter"){G.preventDefault();h("adding submitdontprocess");c.dataset.submitdontprocess=true}};for(i=0;i1){F=true}}if(document.getElementById("is_recur")!==null){if(document.getElementById("is_recur").type=="hidden"){F=(document.getElementById("is_recur").value==1)}else{F=Boolean(document.getElementById("is_recur").checked)}}else{if(e('input[name="auto_renew"]').length!==0){if(e('input[name="auto_renew"]').prop("checked")){F=true}else{if(document.getElementById("auto_renew").type=="hidden"){F=(document.getElementById("auto_renew").value==1)}else{F=Boolean(document.getElementById("auto_renew").checked)}}}}h("isRecur is "+F);return F}function m(G){if(G.empty){e("div#card-errors").hide()}else{if(G.error){k(G.error)}else{if(G.complete){e("div#card-errors").hide();var F=document.getElementById("billing_postal_code-"+CRM.vars.stripe.billingAddressID);if(F){F.value=G.value.postalCode}}}}}function r(){cividiscountElements=c.querySelectorAll("input#discountcode");var F=function(G){if(G.code==="Enter"){G.preventDefault();h("adding submitdontprocess");c.dataset.submitdontprocess=true}};for(i=0;i1){D=true}}if(document.getElementById("is_recur")!==null){if(document.getElementById("is_recur").type=="hidden"){D=(document.getElementById("is_recur").value==1)}else{D=Boolean(document.getElementById("is_recur").checked)}}else{if(d('input[name="auto_renew"]').length!==0){if(d('input[name="auto_renew"]').prop("checked")){D=true}else{if(document.getElementById("auto_renew").type=="hidden"){D=(document.getElementById("auto_renew").value==1)}else{D=Boolean(document.getElementById("auto_renew").checked)}}}}g("isRecur is "+D);return D}function w(D){if(!D.complete){return}document.getElementById("billing_postal_code-"+CRM.vars.stripe.billingAddressID).value=D.value.postalCode}function o(){cividiscountElements=c.querySelectorAll("input#discountcode");var D=function(E){if(E.keyCode===13){E.preventDefault();g("adding submitdontprocess");c.dataset.submitdontprocess=true}};for(i=0;i>>>>>> Stripe element uses CMS/Civi locale +>>>>>>> accc928... Stripe element uses CMS/Civi locale -- GitLab From 9bdfde3e08cd1981589aa0e28719e624f1a3b6e7 Mon Sep 17 00:00:00 2001 From: Matthew Wire Date: Fri, 7 Feb 2020 21:16:34 +0000 Subject: [PATCH 15/45] Update release notes --- docs/release/release_notes.md | 15 ++++++++++++++- info.xml | 4 ++-- js/civicrm_stripe.min.js | 10 +--------- 3 files changed, 17 insertions(+), 12 deletions(-) diff --git a/docs/release/release_notes.md b/docs/release/release_notes.md index 92fbd36..59a5374 100644 --- a/docs/release/release_notes.md +++ b/docs/release/release_notes.md @@ -1,3 +1,17 @@ +## Release 6.4 (not yet released) + +New Features: +* The Stripe "element" now follows the current CMS/CiviCRM locale. +* Add jquery form event 'crmBillingFormReloadComplete' and document jquery events. +* Add jquery form event 'crmBillingFormNotValid' so 3rd-party integrations can re-enable custom submit buttons etc. +* Add support for sweetalert library on form validation errors so we popup nice messages when you are missing required fields and for card errors and you click submit. + +Behind the scenes: +* Further tweaks to get tests working +* Initial steps to modernize the testing infrastructure. +* Add some docblocks +* Switch to event.code from deprecated event.keyCode + ## Release 6.3.2 - Security Release If you are using Stripe on public forms (without authentication) it is **strongly** recommended that you upgrade and consider installing the new **firewall** extension. @@ -16,7 +30,6 @@ You may still need to delete and re-add your webhook but should not need to next #### Features * [#126](https://lab.civicrm.org/extensions/stripe/issues/126) Stripe element now uses the CMS/CiviCRM locale so it will appear in the same language as the page instead of the browser language. - ## Release 6.3.1 * Add crm-error class to stripe card errors block so it is highlighted on non bootstrap themes diff --git a/info.xml b/info.xml index 58de28e..69fdc04 100644 --- a/info.xml +++ b/info.xml @@ -13,8 +13,8 @@ Matthew Wire (MJW Consulting) mjw@mjwconsult.co.uk - 2020-02-03 - 6.4alpha + 2020-02-07 + 6.4alpha1 alpha 5.19 diff --git a/js/civicrm_stripe.min.js b/js/civicrm_stripe.min.js index def145d..d729191 100644 --- a/js/civicrm_stripe.min.js +++ b/js/civicrm_stripe.min.js @@ -1,9 +1 @@ -<<<<<<< HEAD -CRM.$(function(e){h("civicrm_stripe loaded, dom-ready function firing.");if(window.civicrmStripeHandleReload){h("calling existing civicrmStripeHandleReload.");window.civicrmStripeHandleReload();return}var u;var b;var c;var y;var s=false;window.onbeforeunload=null;window.civicrmStripeHandleReload=function(){h("civicrmStripeHandleReload");var F=document.getElementById("card-element");if((typeof F!=="undefined")&&(F)){if(!F.children.length){h("checkAndLoad from document.ready");q()}}d("crmBillingFormReloadComplete")};window.civicrmStripeHandleReload();function z(H,F){h(H+": success - submitting form");var G=document.createElement("input");G.setAttribute("type","hidden");G.setAttribute("name",H);G.setAttribute("value",F.id);c.appendChild(G);c.submit()}function t(){for(i=0;i1){F=true}}if(document.getElementById("is_recur")!==null){if(document.getElementById("is_recur").type=="hidden"){F=(document.getElementById("is_recur").value==1)}else{F=Boolean(document.getElementById("is_recur").checked)}}else{if(e('input[name="auto_renew"]').length!==0){if(e('input[name="auto_renew"]').prop("checked")){F=true}else{if(document.getElementById("auto_renew").type=="hidden"){F=(document.getElementById("auto_renew").value==1)}else{F=Boolean(document.getElementById("auto_renew").checked)}}}}h("isRecur is "+F);return F}function m(G){if(G.empty){e("div#card-errors").hide()}else{if(G.error){k(G.error)}else{if(G.complete){e("div#card-errors").hide();var F=document.getElementById("billing_postal_code-"+CRM.vars.stripe.billingAddressID);if(F){F.value=G.value.postalCode}}}}}function r(){cividiscountElements=c.querySelectorAll("input#discountcode");var F=function(G){if(G.code==="Enter"){G.preventDefault();h("adding submitdontprocess");c.dataset.submitdontprocess=true}};for(i=0;i1){F=true}}if(document.getElementById("is_recur")!==null){if(document.getElementById("is_recur").type=="hidden"){F=(document.getElementById("is_recur").value==1)}else{F=Boolean(document.getElementById("is_recur").checked)}}else{if(e('input[name="auto_renew"]').length!==0){if(e('input[name="auto_renew"]').prop("checked")){F=true}else{if(document.getElementById("auto_renew").type=="hidden"){F=(document.getElementById("auto_renew").value==1)}else{F=Boolean(document.getElementById("auto_renew").checked)}}}}h("isRecur is "+F);return F}function m(G){if(G.empty){e("div#card-errors").hide()}else{if(G.error){k(G.error)}else{if(G.complete){e("div#card-errors").hide();var F=document.getElementById("billing_postal_code-"+CRM.vars.stripe.billingAddressID);if(F){F.value=G.value.postalCode}}}}}function r(){cividiscountElements=c.querySelectorAll("input#discountcode");var F=function(G){if(G.code==="Enter"){G.preventDefault();h("adding submitdontprocess");c.dataset.submitdontprocess=true}};for(i=0;i1){D=true}}if(document.getElementById("is_recur")!==null){if(document.getElementById("is_recur").type=="hidden"){D=(document.getElementById("is_recur").value==1)}else{D=Boolean(document.getElementById("is_recur").checked)}}else{if(d('input[name="auto_renew"]').length!==0){if(d('input[name="auto_renew"]').prop("checked")){D=true}else{if(document.getElementById("auto_renew").type=="hidden"){D=(document.getElementById("auto_renew").value==1)}else{D=Boolean(document.getElementById("auto_renew").checked)}}}}g("isRecur is "+D);return D}function w(D){if(!D.complete){return}document.getElementById("billing_postal_code-"+CRM.vars.stripe.billingAddressID).value=D.value.postalCode}function o(){cividiscountElements=c.querySelectorAll("input#discountcode");var D=function(E){if(E.keyCode===13){E.preventDefault();g("adding submitdontprocess");c.dataset.submitdontprocess=true}};for(i=0;i>>>>>> Stripe element uses CMS/Civi locale ->>>>>>> accc928... Stripe element uses CMS/Civi locale +CRM.$(function(e){h("civicrm_stripe loaded, dom-ready function firing.");if(window.civicrmStripeHandleReload){h("calling existing civicrmStripeHandleReload.");window.civicrmStripeHandleReload();return}var u;var b;var c;var y;var s=false;window.onbeforeunload=null;window.civicrmStripeHandleReload=function(){h("civicrmStripeHandleReload");var F=document.getElementById("card-element");if((typeof F!=="undefined")&&(F)){if(!F.children.length){h("checkAndLoad from document.ready");q()}}d("crmBillingFormReloadComplete")};window.civicrmStripeHandleReload();function z(H,F){h(H+": success - submitting form");var G=document.createElement("input");G.setAttribute("type","hidden");G.setAttribute("name",H);G.setAttribute("value",F.id);c.appendChild(G);c.submit()}function t(){for(i=0;i1){F=true}}if(document.getElementById("is_recur")!==null){if(document.getElementById("is_recur").type=="hidden"){F=(document.getElementById("is_recur").value==1)}else{F=Boolean(document.getElementById("is_recur").checked)}}else{if(e('input[name="auto_renew"]').length!==0){if(e('input[name="auto_renew"]').prop("checked")){F=true}else{if(document.getElementById("auto_renew").type=="hidden"){F=(document.getElementById("auto_renew").value==1)}else{F=Boolean(document.getElementById("auto_renew").checked)}}}}h("isRecur is "+F);return F}function m(G){if(G.empty){e("div#card-errors").hide()}else{if(G.error){k(G.error)}else{if(G.complete){e("div#card-errors").hide();var F=document.getElementById("billing_postal_code-"+CRM.vars.stripe.billingAddressID);if(F){F.value=G.value.postalCode}}}}}function r(){cividiscountElements=c.querySelectorAll("input#discountcode");var F=function(G){if(G.code==="Enter"){G.preventDefault();h("adding submitdontprocess");c.dataset.submitdontprocess=true}};for(i=0;i Date: Fri, 7 Feb 2020 21:26:36 +0000 Subject: [PATCH 16/45] Drop alert as css class as it adds padding and looks ugly --- js/civicrm_stripe.js | 2 +- js/civicrm_stripe.min.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/js/civicrm_stripe.js b/js/civicrm_stripe.js index 3d27ae4..a3de91e 100644 --- a/js/civicrm_stripe.js +++ b/js/civicrm_stripe.js @@ -622,7 +622,7 @@ CRM.$(function($) { $(this).closest('div').next('div').find('input').addClass('required'); }); var validator = $(form).validate(); - validator.settings.errorClass = 'crm-inline-error alert alert-danger'; + validator.settings.errorClass = 'crm-inline-error alert-danger'; validator.settings.ignore = '.select2-offscreen, [readonly], :hidden:not(.crm-select2)'; } diff --git a/js/civicrm_stripe.min.js b/js/civicrm_stripe.min.js index d729191..5b74075 100644 --- a/js/civicrm_stripe.min.js +++ b/js/civicrm_stripe.min.js @@ -1 +1 @@ -CRM.$(function(e){h("civicrm_stripe loaded, dom-ready function firing.");if(window.civicrmStripeHandleReload){h("calling existing civicrmStripeHandleReload.");window.civicrmStripeHandleReload();return}var u;var b;var c;var y;var s=false;window.onbeforeunload=null;window.civicrmStripeHandleReload=function(){h("civicrmStripeHandleReload");var F=document.getElementById("card-element");if((typeof F!=="undefined")&&(F)){if(!F.children.length){h("checkAndLoad from document.ready");q()}}d("crmBillingFormReloadComplete")};window.civicrmStripeHandleReload();function z(H,F){h(H+": success - submitting form");var G=document.createElement("input");G.setAttribute("type","hidden");G.setAttribute("name",H);G.setAttribute("value",F.id);c.appendChild(G);c.submit()}function t(){for(i=0;i1){F=true}}if(document.getElementById("is_recur")!==null){if(document.getElementById("is_recur").type=="hidden"){F=(document.getElementById("is_recur").value==1)}else{F=Boolean(document.getElementById("is_recur").checked)}}else{if(e('input[name="auto_renew"]').length!==0){if(e('input[name="auto_renew"]').prop("checked")){F=true}else{if(document.getElementById("auto_renew").type=="hidden"){F=(document.getElementById("auto_renew").value==1)}else{F=Boolean(document.getElementById("auto_renew").checked)}}}}h("isRecur is "+F);return F}function m(G){if(G.empty){e("div#card-errors").hide()}else{if(G.error){k(G.error)}else{if(G.complete){e("div#card-errors").hide();var F=document.getElementById("billing_postal_code-"+CRM.vars.stripe.billingAddressID);if(F){F.value=G.value.postalCode}}}}}function r(){cividiscountElements=c.querySelectorAll("input#discountcode");var F=function(G){if(G.code==="Enter"){G.preventDefault();h("adding submitdontprocess");c.dataset.submitdontprocess=true}};for(i=0;i1){F=true}}if(document.getElementById("is_recur")!==null){if(document.getElementById("is_recur").type=="hidden"){F=(document.getElementById("is_recur").value==1)}else{F=Boolean(document.getElementById("is_recur").checked)}}else{if(e('input[name="auto_renew"]').length!==0){if(e('input[name="auto_renew"]').prop("checked")){F=true}else{if(document.getElementById("auto_renew").type=="hidden"){F=(document.getElementById("auto_renew").value==1)}else{F=Boolean(document.getElementById("auto_renew").checked)}}}}h("isRecur is "+F);return F}function m(G){if(G.empty){e("div#card-errors").hide()}else{if(G.error){k(G.error)}else{if(G.complete){e("div#card-errors").hide();var F=document.getElementById("billing_postal_code-"+CRM.vars.stripe.billingAddressID);if(F){F.value=G.value.postalCode}}}}}function r(){cividiscountElements=c.querySelectorAll("input#discountcode");var F=function(G){if(G.code==="Enter"){G.preventDefault();h("adding submitdontprocess");c.dataset.submitdontprocess=true}};for(i=0;i Date: Fri, 7 Feb 2020 23:00:51 +0000 Subject: [PATCH 17/45] Make sure we don't submit the form if we have a reCaptcha and it is not valid --- docs/release/release_notes.md | 1 + js/civicrm_stripe.js | 8 ++++++++ js/civicrm_stripe.min.js | 2 +- 3 files changed, 10 insertions(+), 1 deletion(-) diff --git a/docs/release/release_notes.md b/docs/release/release_notes.md index 59a5374..dddb001 100644 --- a/docs/release/release_notes.md +++ b/docs/release/release_notes.md @@ -5,6 +5,7 @@ New Features: * Add jquery form event 'crmBillingFormReloadComplete' and document jquery events. * Add jquery form event 'crmBillingFormNotValid' so 3rd-party integrations can re-enable custom submit buttons etc. * Add support for sweetalert library on form validation errors so we popup nice messages when you are missing required fields and for card errors and you click submit. +* Make sure we don't submit if we have a reCaptcha and it is not valid. Behind the scenes: * Further tweaks to get tests working diff --git a/js/civicrm_stripe.js b/js/civicrm_stripe.js index a3de91e..0933fe1 100644 --- a/js/civicrm_stripe.js +++ b/js/civicrm_stripe.js @@ -346,6 +346,14 @@ CRM.$(function($) { return false; } + if (!(typeof grecaptcha === 'undefined' || (grecaptcha && grecaptcha.getResponse().length !== 0))) { + debugging('recaptcha active and not valid'); + $('div#card-errors').hide(); + triggerEvent('crmBillingFormNotValid'); + notifyUser('error', '', ts('Please complete the reCaptcha')); + return false; + } + if (typeof CRM.vars.stripe === 'undefined') { debugging('Submitting - not a stripe processor'); return true; diff --git a/js/civicrm_stripe.min.js b/js/civicrm_stripe.min.js index 5b74075..f6b500d 100644 --- a/js/civicrm_stripe.min.js +++ b/js/civicrm_stripe.min.js @@ -1 +1 @@ -CRM.$(function(e){h("civicrm_stripe loaded, dom-ready function firing.");if(window.civicrmStripeHandleReload){h("calling existing civicrmStripeHandleReload.");window.civicrmStripeHandleReload();return}var u;var b;var c;var y;var s=false;window.onbeforeunload=null;window.civicrmStripeHandleReload=function(){h("civicrmStripeHandleReload");var F=document.getElementById("card-element");if((typeof F!=="undefined")&&(F)){if(!F.children.length){h("checkAndLoad from document.ready");q()}}d("crmBillingFormReloadComplete")};window.civicrmStripeHandleReload();function z(H,F){h(H+": success - submitting form");var G=document.createElement("input");G.setAttribute("type","hidden");G.setAttribute("name",H);G.setAttribute("value",F.id);c.appendChild(G);c.submit()}function t(){for(i=0;i1){F=true}}if(document.getElementById("is_recur")!==null){if(document.getElementById("is_recur").type=="hidden"){F=(document.getElementById("is_recur").value==1)}else{F=Boolean(document.getElementById("is_recur").checked)}}else{if(e('input[name="auto_renew"]').length!==0){if(e('input[name="auto_renew"]').prop("checked")){F=true}else{if(document.getElementById("auto_renew").type=="hidden"){F=(document.getElementById("auto_renew").value==1)}else{F=Boolean(document.getElementById("auto_renew").checked)}}}}h("isRecur is "+F);return F}function m(G){if(G.empty){e("div#card-errors").hide()}else{if(G.error){k(G.error)}else{if(G.complete){e("div#card-errors").hide();var F=document.getElementById("billing_postal_code-"+CRM.vars.stripe.billingAddressID);if(F){F.value=G.value.postalCode}}}}}function r(){cividiscountElements=c.querySelectorAll("input#discountcode");var F=function(G){if(G.code==="Enter"){G.preventDefault();h("adding submitdontprocess");c.dataset.submitdontprocess=true}};for(i=0;i1){F=true}}if(document.getElementById("is_recur")!==null){if(document.getElementById("is_recur").type=="hidden"){F=(document.getElementById("is_recur").value==1)}else{F=Boolean(document.getElementById("is_recur").checked)}}else{if(e('input[name="auto_renew"]').length!==0){if(e('input[name="auto_renew"]').prop("checked")){F=true}else{if(document.getElementById("auto_renew").type=="hidden"){F=(document.getElementById("auto_renew").value==1)}else{F=Boolean(document.getElementById("auto_renew").checked)}}}}h("isRecur is "+F);return F}function m(G){if(G.empty){e("div#card-errors").hide()}else{if(G.error){k(G.error)}else{if(G.complete){e("div#card-errors").hide();var F=document.getElementById("billing_postal_code-"+CRM.vars.stripe.billingAddressID);if(F){F.value=G.value.postalCode}}}}}function r(){cividiscountElements=c.querySelectorAll("input#discountcode");var F=function(G){if(G.code==="Enter"){G.preventDefault();h("adding submitdontprocess");c.dataset.submitdontprocess=true}};for(i=0;i Date: Fri, 7 Feb 2020 23:25:29 +0000 Subject: [PATCH 18/45] Notify via Swal and then scroll the error into view --- js/civicrm_stripe.js | 35 +++++++++++++++++++++++------------ js/civicrm_stripe.min.js | 2 +- 2 files changed, 24 insertions(+), 13 deletions(-) diff --git a/js/civicrm_stripe.js b/js/civicrm_stripe.js index 0933fe1..5893119 100644 --- a/js/civicrm_stripe.js +++ b/js/civicrm_stripe.js @@ -69,14 +69,12 @@ CRM.$(function($) { var errorElement = document.getElementById('card-errors'); errorElement.style.display = 'block'; errorElement.textContent = error.message; - document.querySelector('#billing-payment-block').scrollIntoView(); - window.scrollBy(0, -50); form.dataset.submitted = false; for (i = 0; i < submitButtons.length; ++i) { submitButtons[i].removeAttribute('disabled'); } triggerEvent('crmBillingFormNotValid'); - notifyUser('error', '', result.error.message); + notifyUser('error', '', result.error.message, '#card-element'); } function handleCardPayment() { @@ -339,18 +337,24 @@ CRM.$(function($) { if ($(form).valid() === false) { debugging('Form not valid'); $('div#card-errors').hide(); - document.querySelector('#billing-payment-block').scrollIntoView(); - window.scrollBy(0, -50); + notifyUser('error', '', ts('Please check and fill in all required fields!'), '#crm-container'); + triggerEvent('crmBillingFormNotValid'); + return false; + } + + if (CRM.$('#card-element.StripeElement--empty').length) { + debugging('card details not entered!'); + $('div#card-errors').hide(); + notifyUser('error', '', ts('Please fill in the card details!', '#card-element')); triggerEvent('crmBillingFormNotValid'); - notifyUser('error', '', ts('Please check and fill in all required fields!')); return false; } if (!(typeof grecaptcha === 'undefined' || (grecaptcha && grecaptcha.getResponse().length !== 0))) { debugging('recaptcha active and not valid'); $('div#card-errors').hide(); + notifyUser('error', '', ts('Please complete the reCaptcha'), '.recaptcha-section'); triggerEvent('crmBillingFormNotValid'); - notifyUser('error', '', ts('Please complete the reCaptcha')); return false; } @@ -705,14 +709,21 @@ CRM.$(function($) { * @param {string} icon * @param {string} title * @param {string} text + * @param {string} scrollToElement */ - function notifyUser(icon, title, text) { + function notifyUser(icon, title, text, scrollToElement) { if (typeof Swal === 'function') { - Swal.fire({ + swalParams = { icon: icon, - title: title, - text: text, - }); + text: text + }; + if (title) { + swalParams.title = title; + } + if (scrollToElement) { + swalParams.onAfterClose = function() { document.querySelector(scrollToElement).scrollIntoView(); window.scrollBy(0, -50); }; + } + Swal.fire(swalParams); } } diff --git a/js/civicrm_stripe.min.js b/js/civicrm_stripe.min.js index f6b500d..d84964a 100644 --- a/js/civicrm_stripe.min.js +++ b/js/civicrm_stripe.min.js @@ -1 +1 @@ -CRM.$(function(e){h("civicrm_stripe loaded, dom-ready function firing.");if(window.civicrmStripeHandleReload){h("calling existing civicrmStripeHandleReload.");window.civicrmStripeHandleReload();return}var u;var b;var c;var y;var s=false;window.onbeforeunload=null;window.civicrmStripeHandleReload=function(){h("civicrmStripeHandleReload");var F=document.getElementById("card-element");if((typeof F!=="undefined")&&(F)){if(!F.children.length){h("checkAndLoad from document.ready");q()}}d("crmBillingFormReloadComplete")};window.civicrmStripeHandleReload();function z(H,F){h(H+": success - submitting form");var G=document.createElement("input");G.setAttribute("type","hidden");G.setAttribute("name",H);G.setAttribute("value",F.id);c.appendChild(G);c.submit()}function t(){for(i=0;i1){F=true}}if(document.getElementById("is_recur")!==null){if(document.getElementById("is_recur").type=="hidden"){F=(document.getElementById("is_recur").value==1)}else{F=Boolean(document.getElementById("is_recur").checked)}}else{if(e('input[name="auto_renew"]').length!==0){if(e('input[name="auto_renew"]').prop("checked")){F=true}else{if(document.getElementById("auto_renew").type=="hidden"){F=(document.getElementById("auto_renew").value==1)}else{F=Boolean(document.getElementById("auto_renew").checked)}}}}h("isRecur is "+F);return F}function m(G){if(G.empty){e("div#card-errors").hide()}else{if(G.error){k(G.error)}else{if(G.complete){e("div#card-errors").hide();var F=document.getElementById("billing_postal_code-"+CRM.vars.stripe.billingAddressID);if(F){F.value=G.value.postalCode}}}}}function r(){cividiscountElements=c.querySelectorAll("input#discountcode");var F=function(G){if(G.code==="Enter"){G.preventDefault();h("adding submitdontprocess");c.dataset.submitdontprocess=true}};for(i=0;i1){F=true}}if(document.getElementById("is_recur")!==null){if(document.getElementById("is_recur").type=="hidden"){F=(document.getElementById("is_recur").value==1)}else{F=Boolean(document.getElementById("is_recur").checked)}}else{if(e('input[name="auto_renew"]').length!==0){if(e('input[name="auto_renew"]').prop("checked")){F=true}else{if(document.getElementById("auto_renew").type=="hidden"){F=(document.getElementById("auto_renew").value==1)}else{F=Boolean(document.getElementById("auto_renew").checked)}}}}h("isRecur is "+F);return F}function m(G){if(G.empty){e("div#card-errors").hide()}else{if(G.error){k(G.error)}else{if(G.complete){e("div#card-errors").hide();var F=document.getElementById("billing_postal_code-"+CRM.vars.stripe.billingAddressID);if(F){F.value=G.value.postalCode}}}}}function r(){cividiscountElements=c.querySelectorAll("input#discountcode");var F=function(G){if(G.code==="Enter"){G.preventDefault();h("adding submitdontprocess");c.dataset.submitdontprocess=true}};for(i=0;i Date: Fri, 7 Feb 2020 23:47:29 +0000 Subject: [PATCH 19/45] Revert to using error instead of crm-inline-error for validation errors --- js/civicrm_stripe.js | 2 +- js/civicrm_stripe.min.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/js/civicrm_stripe.js b/js/civicrm_stripe.js index 5893119..b7b4e17 100644 --- a/js/civicrm_stripe.js +++ b/js/civicrm_stripe.js @@ -634,7 +634,7 @@ CRM.$(function($) { $(this).closest('div').next('div').find('input').addClass('required'); }); var validator = $(form).validate(); - validator.settings.errorClass = 'crm-inline-error alert-danger'; + validator.settings.errorClass = 'error alert-danger'; validator.settings.ignore = '.select2-offscreen, [readonly], :hidden:not(.crm-select2)'; } diff --git a/js/civicrm_stripe.min.js b/js/civicrm_stripe.min.js index d84964a..0567729 100644 --- a/js/civicrm_stripe.min.js +++ b/js/civicrm_stripe.min.js @@ -1 +1 @@ -CRM.$(function(e){h("civicrm_stripe loaded, dom-ready function firing.");if(window.civicrmStripeHandleReload){h("calling existing civicrmStripeHandleReload.");window.civicrmStripeHandleReload();return}var u;var b;var c;var y;var s=false;window.onbeforeunload=null;window.civicrmStripeHandleReload=function(){h("civicrmStripeHandleReload");var F=document.getElementById("card-element");if((typeof F!=="undefined")&&(F)){if(!F.children.length){h("checkAndLoad from document.ready");q()}}d("crmBillingFormReloadComplete")};window.civicrmStripeHandleReload();function z(H,F){h(H+": success - submitting form");var G=document.createElement("input");G.setAttribute("type","hidden");G.setAttribute("name",H);G.setAttribute("value",F.id);c.appendChild(G);c.submit()}function t(){for(i=0;i1){F=true}}if(document.getElementById("is_recur")!==null){if(document.getElementById("is_recur").type=="hidden"){F=(document.getElementById("is_recur").value==1)}else{F=Boolean(document.getElementById("is_recur").checked)}}else{if(e('input[name="auto_renew"]').length!==0){if(e('input[name="auto_renew"]').prop("checked")){F=true}else{if(document.getElementById("auto_renew").type=="hidden"){F=(document.getElementById("auto_renew").value==1)}else{F=Boolean(document.getElementById("auto_renew").checked)}}}}h("isRecur is "+F);return F}function m(G){if(G.empty){e("div#card-errors").hide()}else{if(G.error){k(G.error)}else{if(G.complete){e("div#card-errors").hide();var F=document.getElementById("billing_postal_code-"+CRM.vars.stripe.billingAddressID);if(F){F.value=G.value.postalCode}}}}}function r(){cividiscountElements=c.querySelectorAll("input#discountcode");var F=function(G){if(G.code==="Enter"){G.preventDefault();h("adding submitdontprocess");c.dataset.submitdontprocess=true}};for(i=0;i1){F=true}}if(document.getElementById("is_recur")!==null){if(document.getElementById("is_recur").type=="hidden"){F=(document.getElementById("is_recur").value==1)}else{F=Boolean(document.getElementById("is_recur").checked)}}else{if(e('input[name="auto_renew"]').length!==0){if(e('input[name="auto_renew"]').prop("checked")){F=true}else{if(document.getElementById("auto_renew").type=="hidden"){F=(document.getElementById("auto_renew").value==1)}else{F=Boolean(document.getElementById("auto_renew").checked)}}}}h("isRecur is "+F);return F}function m(G){if(G.empty){e("div#card-errors").hide()}else{if(G.error){k(G.error)}else{if(G.complete){e("div#card-errors").hide();var F=document.getElementById("billing_postal_code-"+CRM.vars.stripe.billingAddressID);if(F){F.value=G.value.postalCode}}}}}function r(){cividiscountElements=c.querySelectorAll("input#discountcode");var F=function(G){if(G.code==="Enter"){G.preventDefault();h("adding submitdontprocess");c.dataset.submitdontprocess=true}};for(i=0;i Date: Sat, 8 Feb 2020 20:02:11 +0000 Subject: [PATCH 20/45] Tweak how we scroll to errors on form --- js/civicrm_stripe.js | 23 ++++++++++++++++++----- js/civicrm_stripe.min.js | 2 +- 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/js/civicrm_stripe.js b/js/civicrm_stripe.js index b7b4e17..df142b1 100644 --- a/js/civicrm_stripe.js +++ b/js/civicrm_stripe.js @@ -74,7 +74,7 @@ CRM.$(function($) { submitButtons[i].removeAttribute('disabled'); } triggerEvent('crmBillingFormNotValid'); - notifyUser('error', '', result.error.message, '#card-element'); + notifyUser('error', '', error.message, '#card-element'); } function handleCardPayment() { @@ -342,10 +342,23 @@ CRM.$(function($) { return false; } + var cardError = CRM.$('#card-errors').text(); if (CRM.$('#card-element.StripeElement--empty').length) { debugging('card details not entered!'); - $('div#card-errors').hide(); - notifyUser('error', '', ts('Please fill in the card details!', '#card-element')); + if (!cardError) { + cardError = ts('Please fill in your card details!'); + } + notifyUser('error', '', cardError, '#card-element'); + triggerEvent('crmBillingFormNotValid'); + return false; + } + + if (CRM.$('#card-element.StripeElement--invalid').length) { + if (!cardError) { + cardError = ts('Please check your card details!'); + } + debugging('card details not valid!'); + notifyUser('error', '', cardError, '#card-element'); triggerEvent('crmBillingFormNotValid'); return false; } @@ -713,7 +726,7 @@ CRM.$(function($) { */ function notifyUser(icon, title, text, scrollToElement) { if (typeof Swal === 'function') { - swalParams = { + var swalParams = { icon: icon, text: text }; @@ -721,7 +734,7 @@ CRM.$(function($) { swalParams.title = title; } if (scrollToElement) { - swalParams.onAfterClose = function() { document.querySelector(scrollToElement).scrollIntoView(); window.scrollBy(0, -50); }; + swalParams.onAfterClose = function() { window.scrollTo($(scrollToElement).position()); }; } Swal.fire(swalParams); } diff --git a/js/civicrm_stripe.min.js b/js/civicrm_stripe.min.js index 0567729..2e21b4e 100644 --- a/js/civicrm_stripe.min.js +++ b/js/civicrm_stripe.min.js @@ -1 +1 @@ -CRM.$(function(e){h("civicrm_stripe loaded, dom-ready function firing.");if(window.civicrmStripeHandleReload){h("calling existing civicrmStripeHandleReload.");window.civicrmStripeHandleReload();return}var u;var b;var c;var y;var s=false;window.onbeforeunload=null;window.civicrmStripeHandleReload=function(){h("civicrmStripeHandleReload");var F=document.getElementById("card-element");if((typeof F!=="undefined")&&(F)){if(!F.children.length){h("checkAndLoad from document.ready");q()}}d("crmBillingFormReloadComplete")};window.civicrmStripeHandleReload();function z(H,F){h(H+": success - submitting form");var G=document.createElement("input");G.setAttribute("type","hidden");G.setAttribute("name",H);G.setAttribute("value",F.id);c.appendChild(G);c.submit()}function t(){for(i=0;i1){F=true}}if(document.getElementById("is_recur")!==null){if(document.getElementById("is_recur").type=="hidden"){F=(document.getElementById("is_recur").value==1)}else{F=Boolean(document.getElementById("is_recur").checked)}}else{if(e('input[name="auto_renew"]').length!==0){if(e('input[name="auto_renew"]').prop("checked")){F=true}else{if(document.getElementById("auto_renew").type=="hidden"){F=(document.getElementById("auto_renew").value==1)}else{F=Boolean(document.getElementById("auto_renew").checked)}}}}h("isRecur is "+F);return F}function m(G){if(G.empty){e("div#card-errors").hide()}else{if(G.error){k(G.error)}else{if(G.complete){e("div#card-errors").hide();var F=document.getElementById("billing_postal_code-"+CRM.vars.stripe.billingAddressID);if(F){F.value=G.value.postalCode}}}}}function r(){cividiscountElements=c.querySelectorAll("input#discountcode");var F=function(G){if(G.code==="Enter"){G.preventDefault();h("adding submitdontprocess");c.dataset.submitdontprocess=true}};for(i=0;i1){F=true}}if(document.getElementById("is_recur")!==null){if(document.getElementById("is_recur").type=="hidden"){F=(document.getElementById("is_recur").value==1)}else{F=Boolean(document.getElementById("is_recur").checked)}}else{if(e('input[name="auto_renew"]').length!==0){if(e('input[name="auto_renew"]').prop("checked")){F=true}else{if(document.getElementById("auto_renew").type=="hidden"){F=(document.getElementById("auto_renew").value==1)}else{F=Boolean(document.getElementById("auto_renew").checked)}}}}h("isRecur is "+F);return F}function m(G){if(G.empty){e("div#card-errors").hide()}else{if(G.error){k(G.error)}else{if(G.complete){e("div#card-errors").hide();var F=document.getElementById("billing_postal_code-"+CRM.vars.stripe.billingAddressID);if(F){F.value=G.value.postalCode}}}}}function r(){cividiscountElements=c.querySelectorAll("input#discountcode");var F=function(G){if(G.code==="Enter"){G.preventDefault();h("adding submitdontprocess");c.dataset.submitdontprocess=true}};for(i=0;i Date: Sat, 8 Feb 2020 21:04:44 +0000 Subject: [PATCH 21/45] Add custom property on billing form to allow for custom validations --- docs/events.md | 19 ++++++++++++++++++- js/civicrm_stripe.js | 4 ++-- js/civicrm_stripe.min.js | 2 +- mkdocs.yml | 2 +- 4 files changed, 22 insertions(+), 5 deletions(-) diff --git a/docs/events.md b/docs/events.md index 777529f..be98003 100644 --- a/docs/events.md +++ b/docs/events.md @@ -1,4 +1,4 @@ -# Events (jQuery) +# Events and custom form integrations Most of the functionality to take card details, validate the form etc. happens on the javascript/jquery side before the form is submitted. @@ -21,3 +21,20 @@ Example code: ### crmBillingFormReloadComplete This event is triggered when the form has completed reloading and is ready for use (Stripe element visible etc.). It is useful for clearing any "loading" indicators and unfreezing form elements. + +## Custom validation / Form Data + +#### crmBillingFormValid +If you want to do some validation of the form and prevent Stripe from submitting you can set the boolean data property +on the form. + +Example code: +```javascript + $('#my-custom-submit-button').on('click', e => { + e.preventDefault(); + $form.data('crmBillingFormValid', true); + if (myCustomValidation() === false) { + $form.data('crmBillingFormValid', false); + } + }); +``` diff --git a/js/civicrm_stripe.js b/js/civicrm_stripe.js index df142b1..b466ebc 100644 --- a/js/civicrm_stripe.js +++ b/js/civicrm_stripe.js @@ -334,7 +334,7 @@ CRM.$(function($) { event.preventDefault(); debugging('submit handler'); - if ($(form).valid() === false) { + if (($(form).valid() === false) || $(form).data('crmBillingFormValid') === false) { debugging('Form not valid'); $('div#card-errors').hide(); notifyUser('error', '', ts('Please check and fill in all required fields!'), '#crm-container'); @@ -346,7 +346,7 @@ CRM.$(function($) { if (CRM.$('#card-element.StripeElement--empty').length) { debugging('card details not entered!'); if (!cardError) { - cardError = ts('Please fill in your card details!'); + cardError = ts('Please enter your card details!'); } notifyUser('error', '', cardError, '#card-element'); triggerEvent('crmBillingFormNotValid'); diff --git a/js/civicrm_stripe.min.js b/js/civicrm_stripe.min.js index 2e21b4e..c0b057b 100644 --- a/js/civicrm_stripe.min.js +++ b/js/civicrm_stripe.min.js @@ -1 +1 @@ -CRM.$(function(e){h("civicrm_stripe loaded, dom-ready function firing.");if(window.civicrmStripeHandleReload){h("calling existing civicrmStripeHandleReload.");window.civicrmStripeHandleReload();return}var u;var b;var c;var y;var s=false;window.onbeforeunload=null;window.civicrmStripeHandleReload=function(){h("civicrmStripeHandleReload");var F=document.getElementById("card-element");if((typeof F!=="undefined")&&(F)){if(!F.children.length){h("checkAndLoad from document.ready");q()}}d("crmBillingFormReloadComplete")};window.civicrmStripeHandleReload();function z(H,F){h(H+": success - submitting form");var G=document.createElement("input");G.setAttribute("type","hidden");G.setAttribute("name",H);G.setAttribute("value",F.id);c.appendChild(G);c.submit()}function t(){for(i=0;i1){F=true}}if(document.getElementById("is_recur")!==null){if(document.getElementById("is_recur").type=="hidden"){F=(document.getElementById("is_recur").value==1)}else{F=Boolean(document.getElementById("is_recur").checked)}}else{if(e('input[name="auto_renew"]').length!==0){if(e('input[name="auto_renew"]').prop("checked")){F=true}else{if(document.getElementById("auto_renew").type=="hidden"){F=(document.getElementById("auto_renew").value==1)}else{F=Boolean(document.getElementById("auto_renew").checked)}}}}h("isRecur is "+F);return F}function m(G){if(G.empty){e("div#card-errors").hide()}else{if(G.error){k(G.error)}else{if(G.complete){e("div#card-errors").hide();var F=document.getElementById("billing_postal_code-"+CRM.vars.stripe.billingAddressID);if(F){F.value=G.value.postalCode}}}}}function r(){cividiscountElements=c.querySelectorAll("input#discountcode");var F=function(G){if(G.code==="Enter"){G.preventDefault();h("adding submitdontprocess");c.dataset.submitdontprocess=true}};for(i=0;i1){F=true}}if(document.getElementById("is_recur")!==null){if(document.getElementById("is_recur").type=="hidden"){F=(document.getElementById("is_recur").value==1)}else{F=Boolean(document.getElementById("is_recur").checked)}}else{if(e('input[name="auto_renew"]').length!==0){if(e('input[name="auto_renew"]').prop("checked")){F=true}else{if(document.getElementById("auto_renew").type=="hidden"){F=(document.getElementById("auto_renew").value==1)}else{F=Boolean(document.getElementById("auto_renew").checked)}}}}h("isRecur is "+F);return F}function m(G){if(G.empty){e("div#card-errors").hide()}else{if(G.error){k(G.error)}else{if(G.complete){e("div#card-errors").hide();var F=document.getElementById("billing_postal_code-"+CRM.vars.stripe.billingAddressID);if(F){F.value=G.value.postalCode}}}}}function r(){cividiscountElements=c.querySelectorAll("input#discountcode");var F=function(G){if(G.code==="Enter"){G.preventDefault();h("adding submitdontprocess");c.dataset.submitdontprocess=true}};for(i=0;i Date: Sat, 8 Feb 2020 21:48:06 +0000 Subject: [PATCH 22/45] The "name" parameter on a set of checkboxes where at least one must be checked must be the same or validation will require all of them! --- js/civicrm_stripe.js | 14 ++++++++++++++ js/civicrm_stripe.min.js | 2 +- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/js/civicrm_stripe.js b/js/civicrm_stripe.js index b466ebc..08e6d06 100644 --- a/js/civicrm_stripe.js +++ b/js/civicrm_stripe.js @@ -50,6 +50,15 @@ CRM.$(function($) { hiddenInput.setAttribute('value', object.id); form.appendChild(hiddenInput); + // The "name" parameter on a set of checkboxes where at least one must be checked must be the same or validation will require all of them! + // (But we have to reset this back before we submit otherwise the submission has no data (that's a Civi issue I think). + $('div#priceset input[type="checkbox"]').each(function() { + CRM.$(this).attr('name', CRM.$(this).attr('name') + '[' + CRM.$(this).attr('id').split('_').pop() + ']'); + CRM.$(this).removeAttr('required'); + CRM.$(this).removeClass('required'); + CRM.$(this).removeAttr('aria-required'); + }); + // Submit the form form.submit(); } @@ -646,6 +655,11 @@ CRM.$(function($) { $('div.label span.crm-marker').each(function() { $(this).closest('div').next('div').find('input').addClass('required'); }); + // The "name" parameter on a set of checkboxes where at least one must be checked must be the same or validation will require all of them! + // (But we have to reset this back before we submit otherwise the submission has no data (that's a Civi issue I think). + $('div#priceset input[type="checkbox"]').each(function() { + $(this).attr('name', $(this).attr('name').split('[').shift()); + }); var validator = $(form).validate(); validator.settings.errorClass = 'error alert-danger'; validator.settings.ignore = '.select2-offscreen, [readonly], :hidden:not(.crm-select2)'; diff --git a/js/civicrm_stripe.min.js b/js/civicrm_stripe.min.js index c0b057b..84e3149 100644 --- a/js/civicrm_stripe.min.js +++ b/js/civicrm_stripe.min.js @@ -1 +1 @@ -CRM.$(function(e){h("civicrm_stripe loaded, dom-ready function firing.");if(window.civicrmStripeHandleReload){h("calling existing civicrmStripeHandleReload.");window.civicrmStripeHandleReload();return}var u;var b;var c;var y;var s=false;window.onbeforeunload=null;window.civicrmStripeHandleReload=function(){h("civicrmStripeHandleReload");var F=document.getElementById("card-element");if((typeof F!=="undefined")&&(F)){if(!F.children.length){h("checkAndLoad from document.ready");q()}}d("crmBillingFormReloadComplete")};window.civicrmStripeHandleReload();function z(H,F){h(H+": success - submitting form");var G=document.createElement("input");G.setAttribute("type","hidden");G.setAttribute("name",H);G.setAttribute("value",F.id);c.appendChild(G);c.submit()}function t(){for(i=0;i1){F=true}}if(document.getElementById("is_recur")!==null){if(document.getElementById("is_recur").type=="hidden"){F=(document.getElementById("is_recur").value==1)}else{F=Boolean(document.getElementById("is_recur").checked)}}else{if(e('input[name="auto_renew"]').length!==0){if(e('input[name="auto_renew"]').prop("checked")){F=true}else{if(document.getElementById("auto_renew").type=="hidden"){F=(document.getElementById("auto_renew").value==1)}else{F=Boolean(document.getElementById("auto_renew").checked)}}}}h("isRecur is "+F);return F}function m(G){if(G.empty){e("div#card-errors").hide()}else{if(G.error){k(G.error)}else{if(G.complete){e("div#card-errors").hide();var F=document.getElementById("billing_postal_code-"+CRM.vars.stripe.billingAddressID);if(F){F.value=G.value.postalCode}}}}}function r(){cividiscountElements=c.querySelectorAll("input#discountcode");var F=function(G){if(G.code==="Enter"){G.preventDefault();h("adding submitdontprocess");c.dataset.submitdontprocess=true}};for(i=0;i1){F=true}}if(document.getElementById("is_recur")!==null){if(document.getElementById("is_recur").type=="hidden"){F=(document.getElementById("is_recur").value==1)}else{F=Boolean(document.getElementById("is_recur").checked)}}else{if(e('input[name="auto_renew"]').length!==0){if(e('input[name="auto_renew"]').prop("checked")){F=true}else{if(document.getElementById("auto_renew").type=="hidden"){F=(document.getElementById("auto_renew").value==1)}else{F=Boolean(document.getElementById("auto_renew").checked)}}}}h("isRecur is "+F);return F}function m(G){if(G.empty){e("div#card-errors").hide()}else{if(G.error){k(G.error)}else{if(G.complete){e("div#card-errors").hide();var F=document.getElementById("billing_postal_code-"+CRM.vars.stripe.billingAddressID);if(F){F.value=G.value.postalCode}}}}}function r(){cividiscountElements=c.querySelectorAll("input#discountcode");var F=function(G){if(G.code==="Enter"){G.preventDefault();h("adding submitdontprocess");c.dataset.submitdontprocess=true}};for(i=0;i Date: Mon, 10 Feb 2020 14:58:32 +0000 Subject: [PATCH 23/45] Don't prompt to enter card details if amount is 0 --- js/civicrm_stripe.js | 2 +- js/civicrm_stripe.min.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/js/civicrm_stripe.js b/js/civicrm_stripe.js index 08e6d06..4a68926 100644 --- a/js/civicrm_stripe.js +++ b/js/civicrm_stripe.js @@ -352,7 +352,7 @@ CRM.$(function($) { } var cardError = CRM.$('#card-errors').text(); - if (CRM.$('#card-element.StripeElement--empty').length) { + if (CRM.$('#card-element.StripeElement--empty').length && (getTotalAmount() !== 0.0)) { debugging('card details not entered!'); if (!cardError) { cardError = ts('Please enter your card details!'); diff --git a/js/civicrm_stripe.min.js b/js/civicrm_stripe.min.js index 84e3149..787752e 100644 --- a/js/civicrm_stripe.min.js +++ b/js/civicrm_stripe.min.js @@ -1 +1 @@ -CRM.$(function(e){h("civicrm_stripe loaded, dom-ready function firing.");if(window.civicrmStripeHandleReload){h("calling existing civicrmStripeHandleReload.");window.civicrmStripeHandleReload();return}var u;var b;var c;var y;var s=false;window.onbeforeunload=null;window.civicrmStripeHandleReload=function(){h("civicrmStripeHandleReload");var F=document.getElementById("card-element");if((typeof F!=="undefined")&&(F)){if(!F.children.length){h("checkAndLoad from document.ready");q()}}d("crmBillingFormReloadComplete")};window.civicrmStripeHandleReload();function z(H,F){h(H+": success - submitting form");var G=document.createElement("input");G.setAttribute("type","hidden");G.setAttribute("name",H);G.setAttribute("value",F.id);c.appendChild(G);e('div#priceset input[type="checkbox"]').each(function(){CRM.$(this).attr("name",CRM.$(this).attr("name")+"["+CRM.$(this).attr("id").split("_").pop()+"]");CRM.$(this).removeAttr("required");CRM.$(this).removeClass("required");CRM.$(this).removeAttr("aria-required")});c.submit()}function t(){for(i=0;i1){F=true}}if(document.getElementById("is_recur")!==null){if(document.getElementById("is_recur").type=="hidden"){F=(document.getElementById("is_recur").value==1)}else{F=Boolean(document.getElementById("is_recur").checked)}}else{if(e('input[name="auto_renew"]').length!==0){if(e('input[name="auto_renew"]').prop("checked")){F=true}else{if(document.getElementById("auto_renew").type=="hidden"){F=(document.getElementById("auto_renew").value==1)}else{F=Boolean(document.getElementById("auto_renew").checked)}}}}h("isRecur is "+F);return F}function m(G){if(G.empty){e("div#card-errors").hide()}else{if(G.error){k(G.error)}else{if(G.complete){e("div#card-errors").hide();var F=document.getElementById("billing_postal_code-"+CRM.vars.stripe.billingAddressID);if(F){F.value=G.value.postalCode}}}}}function r(){cividiscountElements=c.querySelectorAll("input#discountcode");var F=function(G){if(G.code==="Enter"){G.preventDefault();h("adding submitdontprocess");c.dataset.submitdontprocess=true}};for(i=0;i1){F=true}}if(document.getElementById("is_recur")!==null){if(document.getElementById("is_recur").type=="hidden"){F=(document.getElementById("is_recur").value==1)}else{F=Boolean(document.getElementById("is_recur").checked)}}else{if(e('input[name="auto_renew"]').length!==0){if(e('input[name="auto_renew"]').prop("checked")){F=true}else{if(document.getElementById("auto_renew").type=="hidden"){F=(document.getElementById("auto_renew").value==1)}else{F=Boolean(document.getElementById("auto_renew").checked)}}}}h("isRecur is "+F);return F}function m(G){if(G.empty){e("div#card-errors").hide()}else{if(G.error){k(G.error)}else{if(G.complete){e("div#card-errors").hide();var F=document.getElementById("billing_postal_code-"+CRM.vars.stripe.billingAddressID);if(F){F.value=G.value.postalCode}}}}}function r(){cividiscountElements=c.querySelectorAll("input#discountcode");var F=function(G){if(G.code==="Enter"){G.preventDefault();h("adding submitdontprocess");c.dataset.submitdontprocess=true}};for(i=0;i Date: Mon, 10 Feb 2020 15:04:47 +0000 Subject: [PATCH 24/45] Don't popup user notification on card element change (only on submit) --- js/civicrm_stripe.js | 20 ++++++++++++++------ js/civicrm_stripe.min.js | 2 +- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/js/civicrm_stripe.js b/js/civicrm_stripe.js index 4a68926..22500ac 100644 --- a/js/civicrm_stripe.js +++ b/js/civicrm_stripe.js @@ -71,7 +71,13 @@ CRM.$(function($) { return form.submit(); } - function displayError(error) { + /** + * Display a stripe element error + * + * @param error - the stripe error object + * @param {boolean} notify - whether to popup a notification as well as display on the form. + */ + function displayError(error, notify) { // Display error.message in your UI. debugging('error: ' + error.message); // Inform the user if there was an error @@ -83,7 +89,9 @@ CRM.$(function($) { submitButtons[i].removeAttribute('disabled'); } triggerEvent('crmBillingFormNotValid'); - notifyUser('error', '', error.message, '#card-element'); + if (notify) { + notifyUser('error', '', error.message, '#card-element'); + } } function handleCardPayment() { @@ -91,7 +99,7 @@ CRM.$(function($) { stripe.createPaymentMethod('card', card).then(function (result) { if (result.error) { // Show error in payment form - displayError(result.error); + displayError(result.error, true); } else { if (getIsRecur() || isEventAdditionalParticipants()) { @@ -121,7 +129,7 @@ CRM.$(function($) { debugging('handleServerResponse'); if (result.error) { // Show error from server on payment form - displayError(result.error); + displayError(result.error, true); } else if (result.requires_action) { // Use Stripe.js to handle required card action handleAction(result); @@ -136,7 +144,7 @@ CRM.$(function($) { .then(function(result) { if (result.error) { // Show error in payment form - displayError(result.error); + displayError(result.error, true); } else { // The card action has been handled // The PaymentIntent can be confirmed again on the server @@ -620,7 +628,7 @@ CRM.$(function($) { $('div#card-errors').hide(); } else if (event.error) { - displayError(event.error); + displayError(event.error, false); } else if (event.complete) { $('div#card-errors').hide(); diff --git a/js/civicrm_stripe.min.js b/js/civicrm_stripe.min.js index 787752e..b04d601 100644 --- a/js/civicrm_stripe.min.js +++ b/js/civicrm_stripe.min.js @@ -1 +1 @@ -CRM.$(function(e){h("civicrm_stripe loaded, dom-ready function firing.");if(window.civicrmStripeHandleReload){h("calling existing civicrmStripeHandleReload.");window.civicrmStripeHandleReload();return}var u;var b;var c;var y;var s=false;window.onbeforeunload=null;window.civicrmStripeHandleReload=function(){h("civicrmStripeHandleReload");var F=document.getElementById("card-element");if((typeof F!=="undefined")&&(F)){if(!F.children.length){h("checkAndLoad from document.ready");q()}}d("crmBillingFormReloadComplete")};window.civicrmStripeHandleReload();function z(H,F){h(H+": success - submitting form");var G=document.createElement("input");G.setAttribute("type","hidden");G.setAttribute("name",H);G.setAttribute("value",F.id);c.appendChild(G);e('div#priceset input[type="checkbox"]').each(function(){CRM.$(this).attr("name",CRM.$(this).attr("name")+"["+CRM.$(this).attr("id").split("_").pop()+"]");CRM.$(this).removeAttr("required");CRM.$(this).removeClass("required");CRM.$(this).removeAttr("aria-required")});c.submit()}function t(){for(i=0;i1){F=true}}if(document.getElementById("is_recur")!==null){if(document.getElementById("is_recur").type=="hidden"){F=(document.getElementById("is_recur").value==1)}else{F=Boolean(document.getElementById("is_recur").checked)}}else{if(e('input[name="auto_renew"]').length!==0){if(e('input[name="auto_renew"]').prop("checked")){F=true}else{if(document.getElementById("auto_renew").type=="hidden"){F=(document.getElementById("auto_renew").value==1)}else{F=Boolean(document.getElementById("auto_renew").checked)}}}}h("isRecur is "+F);return F}function m(G){if(G.empty){e("div#card-errors").hide()}else{if(G.error){k(G.error)}else{if(G.complete){e("div#card-errors").hide();var F=document.getElementById("billing_postal_code-"+CRM.vars.stripe.billingAddressID);if(F){F.value=G.value.postalCode}}}}}function r(){cividiscountElements=c.querySelectorAll("input#discountcode");var F=function(G){if(G.code==="Enter"){G.preventDefault();h("adding submitdontprocess");c.dataset.submitdontprocess=true}};for(i=0;i1){F=true}}if(document.getElementById("is_recur")!==null){if(document.getElementById("is_recur").type=="hidden"){F=(document.getElementById("is_recur").value==1)}else{F=Boolean(document.getElementById("is_recur").checked)}}else{if(e('input[name="auto_renew"]').length!==0){if(e('input[name="auto_renew"]').prop("checked")){F=true}else{if(document.getElementById("auto_renew").type=="hidden"){F=(document.getElementById("auto_renew").value==1)}else{F=Boolean(document.getElementById("auto_renew").checked)}}}}h("isRecur is "+F);return F}function m(G){if(G.empty){e("div#card-errors").hide()}else{if(G.error){k(G.error,false)}else{if(G.complete){e("div#card-errors").hide();var F=document.getElementById("billing_postal_code-"+CRM.vars.stripe.billingAddressID);if(F){F.value=G.value.postalCode}}}}}function r(){cividiscountElements=c.querySelectorAll("input#discountcode");var F=function(G){if(G.code==="Enter"){G.preventDefault();h("adding submitdontprocess");c.dataset.submitdontprocess=true}};for(i=0;i Date: Mon, 10 Feb 2020 21:30:11 +0000 Subject: [PATCH 25/45] Use minifier extension to minify js/css assets --- CRM/Core/Payment/Stripe.php | 18 ++++++++++++++---- CRM/Stripe/Check.php | 6 ++---- css/elements.min.css | 1 - js/civicrmStripeConfirm.min.js | 1 - js/civicrm_stripe.min.js | 1 - stripe.php | 12 ++++++++---- 6 files changed, 24 insertions(+), 15 deletions(-) delete mode 100644 css/elements.min.css delete mode 100644 js/civicrmStripeConfirm.min.js delete mode 100644 js/civicrm_stripe.min.js diff --git a/CRM/Core/Payment/Stripe.php b/CRM/Core/Payment/Stripe.php index 9738e8d..93525df 100644 --- a/CRM/Core/Payment/Stripe.php +++ b/CRM/Core/Payment/Stripe.php @@ -13,11 +13,16 @@ class CRM_Core_Payment_Stripe extends CRM_Core_Payment { use CRM_Core_Payment_MJWTrait; /** - * * @var string */ const API_VERSION = '2019-12-03'; + /** + * @var string + */ + const MIN_VERSION_MJWSHARED = '0.7'; + + /** * Mode of operation: live or test. * @@ -349,13 +354,18 @@ class CRM_Core_Payment_Stripe extends CRM_Core_Payment { ['template' => 'CRM/Core/Payment/Stripe/Card.tpl', 'weight' => -1]); // Add CSS via region (it won't load on drupal webform if added via \Civi::resources()->addStyleFile) - $min = ((boolean) \Civi::settings()->get('stripe_jsdebug')) ? '' : '.min'; CRM_Core_Region::instance('billing-block')->add([ - 'styleUrl' => \Civi::resources()->getUrl(E::LONG_NAME, "css/elements{$min}.css", TRUE), + 'styleUrl' => \Civi::service('asset_builder')->getUrl( + 'elements.css', + ['path' => \Civi::resources()->getPath(E::LONG_NAME, 'css/elements.css')] + ), 'weight' => -1, ]); CRM_Core_Region::instance('billing-block')->add([ - 'scriptUrl' => \Civi::resources()->getUrl(E::LONG_NAME, "js/civicrm_stripe{$min}.js", TRUE), + 'scriptUrl' => \Civi::service('asset_builder')->getUrl( + 'civicrmStripe.js', + ['path' => \Civi::resources()->getPath(E::LONG_NAME, 'js/civicrm_stripe.js')] + ) ]); } diff --git a/CRM/Stripe/Check.php b/CRM/Stripe/Check.php index 42de6f1..e49eb34 100644 --- a/CRM/Stripe/Check.php +++ b/CRM/Stripe/Check.php @@ -10,8 +10,6 @@ use CRM_Stripe_ExtensionUtil as E; */ class CRM_Stripe_Check { - const MIN_VERSION_MJWSHARED = '0.6'; - public static function checkRequirements(&$messages) { $extensions = civicrm_api3('Extension', 'get', [ 'full_name' => "mjwshared", @@ -27,12 +25,12 @@ class CRM_Stripe_Check { ); } - if (version_compare($extensions['values'][$extensions['id']]['version'], self::MIN_VERSION_MJWSHARED) === -1) { + if (version_compare($extensions['values'][$extensions['id']]['version'], CRM_Core_Payment_Stripe::MIN_VERSION_MJWSHARED) === -1) { $messages[] = new CRM_Utils_Check_Message( 'stripe_requirements', E::ts('The Stripe extension requires the mjwshared extension version %1 or greater but your system has version %2.', [ - 1 => self::MIN_VERSION_MJWSHARED, + 1 => CRM_Core_Payment_Stripe::MIN_VERSION_MJWSHARED, 2 => $extensions['values'][$extensions['id']]['version'] ]), E::ts('Stripe: Missing Requirements'), diff --git a/css/elements.min.css b/css/elements.min.css deleted file mode 100644 index 47a21a6..0000000 --- a/css/elements.min.css +++ /dev/null @@ -1 +0,0 @@ -#card-element{padding:2%;margin:2% auto;max-width:800px;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} \ No newline at end of file diff --git a/js/civicrmStripeConfirm.min.js b/js/civicrmStripeConfirm.min.js deleted file mode 100644 index 3c9385e..0000000 --- a/js/civicrmStripeConfirm.min.js +++ /dev/null @@ -1 +0,0 @@ -CRM.$(function(g){f("civicrmStripeConfirm loaded");if(typeof CRM.vars.stripe==="undefined"){f("CRM.vars.stripe not defined! Not a Stripe processor?");return}switch(CRM.vars.stripe.paymentIntentStatus){case"succeeded":case"cancelled":f("paymentIntent: ".CRM.vars.stripe.paymentIntentStatus);return}a();if(typeof h==="undefined"){h=Stripe(CRM.vars.stripe.publishableKey)}c();var h;var e=false;window.onbeforeunload=null;function d(i){f("handleServerResponse");if(i.error){}else{if(i.requires_action){b(i)}else{f("success - payment captured")}}}function b(i){switch(CRM.vars.stripe.paymentIntentMethod){case"automatic":h.handleCardPayment(i.payment_intent_client_secret).then(function(j){if(j.error){c()}else{f("card payment success");c()}});break;case"manual":h.handleCardAction(i.payment_intent_client_secret).then(function(j){if(j.error){c()}else{f("card action success");c()}});break}}function c(){f("handle card confirm");var i=CRM.url("civicrm/stripe/confirm-payment");g.post(i,{payment_intent_id:CRM.vars.stripe.paymentIntentID,capture:true,id:CRM.vars.stripe.id}).then(function(j){d(j)})}function a(){if(typeof Stripe==="undefined"){if(e){return}e=true;f("Stripe.js is not loaded!");g.getScript("https://js.stripe.com/v3",function(){f("Script loaded and executed.");e=false})}}function f(i){if((typeof(CRM.vars.stripe)==="undefined")||(Boolean(CRM.vars.stripe.jsDebug)===true)){console.log(new Date().toISOString()+" civicrm_stripe.js: "+i)}}}); \ No newline at end of file diff --git a/js/civicrm_stripe.min.js b/js/civicrm_stripe.min.js deleted file mode 100644 index b04d601..0000000 --- a/js/civicrm_stripe.min.js +++ /dev/null @@ -1 +0,0 @@ -CRM.$(function(e){h("civicrm_stripe loaded, dom-ready function firing.");if(window.civicrmStripeHandleReload){h("calling existing civicrmStripeHandleReload.");window.civicrmStripeHandleReload();return}var u;var b;var c;var y;var s=false;window.onbeforeunload=null;window.civicrmStripeHandleReload=function(){h("civicrmStripeHandleReload");var F=document.getElementById("card-element");if((typeof F!=="undefined")&&(F)){if(!F.children.length){h("checkAndLoad from document.ready");q()}}d("crmBillingFormReloadComplete")};window.civicrmStripeHandleReload();function z(H,F){h(H+": success - submitting form");var G=document.createElement("input");G.setAttribute("type","hidden");G.setAttribute("name",H);G.setAttribute("value",F.id);c.appendChild(G);e('div#priceset input[type="checkbox"]').each(function(){CRM.$(this).attr("name",CRM.$(this).attr("name")+"["+CRM.$(this).attr("id").split("_").pop()+"]");CRM.$(this).removeAttr("required");CRM.$(this).removeClass("required");CRM.$(this).removeAttr("aria-required")});c.submit()}function t(){for(i=0;i1){F=true}}if(document.getElementById("is_recur")!==null){if(document.getElementById("is_recur").type=="hidden"){F=(document.getElementById("is_recur").value==1)}else{F=Boolean(document.getElementById("is_recur").checked)}}else{if(e('input[name="auto_renew"]').length!==0){if(e('input[name="auto_renew"]').prop("checked")){F=true}else{if(document.getElementById("auto_renew").type=="hidden"){F=(document.getElementById("auto_renew").value==1)}else{F=Boolean(document.getElementById("auto_renew").checked)}}}}h("isRecur is "+F);return F}function m(G){if(G.empty){e("div#card-errors").hide()}else{if(G.error){k(G.error,false)}else{if(G.complete){e("div#card-errors").hide();var F=document.getElementById("billing_postal_code-"+CRM.vars.stripe.billingAddressID);if(F){F.value=G.value.postalCode}}}}}function r(){cividiscountElements=c.querySelectorAll("input#discountcode");var F=function(G){if(G.code==="Enter"){G.preventDefault();h("adding submitdontprocess");c.dataset.submitdontprocess=true}};for(i=0;i_paymentProcessor['class_name'])) || (($context == 'page') && !empty($object->_isPaymentProcessor))) { if (!isset(\Civi::$statics[E::LONG_NAME]['stripeJSLoaded']) || $object instanceof CRM_Financial_Form_Payment) { - $min = ((boolean) \Civi::settings()->get('stripe_jsdebug')) ? '' : '.min'; - $stripeJSURL = \Civi::resources()->getUrl(E::LONG_NAME, "js/civicrm_stripe{$min}.js", TRUE); + $stripeJSURL = \Civi::service('asset_builder')->getUrl( + 'civicrmStripe.js', + ['path' => \Civi::resources()->getPath(E::LONG_NAME, 'js/civicrm_stripe.js')] + ); $content .= ""; \Civi::$statics[E::LONG_NAME]['stripeJSLoaded'] = TRUE; } @@ -138,8 +140,10 @@ function stripe_civicrm_buildForm($formName, &$form) { switch ($formName) { case 'CRM_Contribute_Form_Contribution_ThankYou': case 'CRM_Event_Form_Registration_ThankYou': - $min = ((boolean) \Civi::settings()->get('stripe_jsdebug')) ? '' : '.min'; - \Civi::resources()->addScriptFile(E::LONG_NAME, "js/civicrmStripeConfirm{$min}.js", TRUE); + \Civi::resources()->addScriptUrl(\Civi::service('asset_builder')->getUrl( + 'civicrmStripeConfirm.js', + ['path' => \Civi::resources()->getPath(E::LONG_NAME, 'js/civicrmStripeConfirm.js')] + )); // This is a fairly nasty way of matching and retrieving our paymentIntent as it is no longer available. $qfKey = CRM_Utils_Request::retrieve('qfKey', 'String'); -- GitLab From a2d5711354fa81024f15257b33ed6b597a623f07 Mon Sep 17 00:00:00 2001 From: Matthew Wire Date: Wed, 12 Feb 2020 13:58:30 +0000 Subject: [PATCH 26/45] If Stripe is not using the same currency as the payment was made we need to convert the fees/net amounts back to the CiviCRM currency --- CRM/Core/Payment/Stripe.php | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/CRM/Core/Payment/Stripe.php b/CRM/Core/Payment/Stripe.php index 93525df..9812676 100644 --- a/CRM/Core/Payment/Stripe.php +++ b/CRM/Core/Payment/Stripe.php @@ -697,8 +697,15 @@ class CRM_Core_Payment_Stripe extends CRM_Core_Payment { $errorMessage = $this->handleErrorNotification($err, $params['stripe_error_url']); throw new \Civi\Payment\Exception\PaymentProcessorException('Failed to retrieve Stripe Balance Transaction: ' . $errorMessage); } - $newParams['fee_amount'] = $stripeBalanceTransaction->fee / 100; - $newParams['net_amount'] = $stripeBalanceTransaction->net / 100; + if (($stripeCharge['currency'] !== $stripeBalanceTransaction['currency']) + && (!empty($stripeBalanceTransaction['exchange_rate']))) { + $newParams['fee_amount'] = ($stripeBalanceTransaction->fee / $stripeBalanceTransaction['exchange_rate']) / 100; + $newParams['net_amount'] = ($stripeBalanceTransaction->net / $stripeBalanceTransaction['exchange_rate']) / 100; + } + else { + $newParams['fee_amount'] = $stripeBalanceTransaction->fee / 100; + $newParams['net_amount'] = $stripeBalanceTransaction->net / 100; + } // Success! // Set the desired contribution status which will be set later (do not set on the contribution here!) $params['contribution_status_id'] = CRM_Core_PseudoConstant::getKey('CRM_Contribute_BAO_Contribution', 'contribution_status_id', 'Completed'); -- GitLab From a725fce46f12bd9aecb74d15e933afcbc810f677 Mon Sep 17 00:00:00 2001 From: Matthew Wire Date: Thu, 6 Feb 2020 22:04:32 +0000 Subject: [PATCH 27/45] Add setting to disable billing address fields --- CRM/Core/Payment/Stripe.php | 57 +++++++++++++++++++++++++++---------- js/civicrm_stripe.js | 30 ++++++++++++------- settings/stripe.setting.php | 33 +++++++++++++++------ 3 files changed, 87 insertions(+), 33 deletions(-) diff --git a/CRM/Core/Payment/Stripe.php b/CRM/Core/Payment/Stripe.php index 9812676..e0ccb25 100644 --- a/CRM/Core/Payment/Stripe.php +++ b/CRM/Core/Payment/Stripe.php @@ -297,6 +297,25 @@ class CRM_Core_Payment_Stripe extends CRM_Core_Payment { return []; } + /** + * Get billing fields required for this processor. + * + * We apply the existing default of returning fields only for payment processor type 1. Processors can override to + * alter. + * + * @param int $billingLocationID + * + * @return array + */ + public function getBillingAddressFields($billingLocationID = NULL) { + if ((boolean) \Civi::settings()->get('stripe_nobillingaddress')) { + return []; + } + else { + return parent::getBillingAddressFields($billingLocationID); + } + } + /** * Get form metadata for billing address fields. * @@ -306,23 +325,31 @@ class CRM_Core_Payment_Stripe extends CRM_Core_Payment { * Array of metadata for address fields. */ public function getBillingAddressFieldsMetadata($billingLocationID = NULL) { - $metadata = parent::getBillingAddressFieldsMetadata($billingLocationID); - if (!$billingLocationID) { - // Note that although the billing id is passed around the forms the idea that it would be anything other than - // the result of the function below doesn't seem to have eventuated. - // So taking this as a param is possibly something to be removed in favour of the standard default. - $billingLocationID = CRM_Core_BAO_LocationType::getBilling(); - } - - // Stripe does not require some of the billing fields but users may still choose to fill them in. - $nonRequiredBillingFields = ["billing_state_province_id-{$billingLocationID}", "billing_postal_code-{$billingLocationID}"]; - foreach ($nonRequiredBillingFields as $fieldName) { - if (!empty($metadata[$fieldName]['is_required'])) { - $metadata[$fieldName]['is_required'] = FALSE; - } + if ((boolean) \Civi::settings()->get('stripe_nobillingaddress')) { + return []; } + else { + $metadata = parent::getBillingAddressFieldsMetadata($billingLocationID); + if (!$billingLocationID) { + // Note that although the billing id is passed around the forms the idea that it would be anything other than + // the result of the function below doesn't seem to have eventuated. + // So taking this as a param is possibly something to be removed in favour of the standard default. + $billingLocationID = CRM_Core_BAO_LocationType::getBilling(); + } - return $metadata; + // Stripe does not require some of the billing fields but users may still choose to fill them in. + $nonRequiredBillingFields = [ + "billing_state_province_id-{$billingLocationID}", + "billing_postal_code-{$billingLocationID}" + ]; + foreach ($nonRequiredBillingFields as $fieldName) { + if (!empty($metadata[$fieldName]['is_required'])) { + $metadata[$fieldName]['is_required'] = FALSE; + } + } + + return $metadata; + } } /** diff --git a/js/civicrm_stripe.js b/js/civicrm_stripe.js index 22500ac..3353189 100644 --- a/js/civicrm_stripe.js +++ b/js/civicrm_stripe.js @@ -250,21 +250,31 @@ CRM.$(function($) { }, }; - // Pre-fill postcode field with existing value from form - var postCode = document.getElementById('billing_postal_code-' + CRM.vars.stripe.billingAddressID).value; - debugging('existing postcode: ' + postCode); + var elementsCreateParams = {style: style, value: {}}; + + var postCodeElement = document.getElementById('billing_postal_code-' + CRM.vars.stripe.billingAddressID); + if (postCodeElement) { + var postCode = document.getElementById('billing_postal_code-' + CRM.vars.stripe.billingAddressID).value; + debugging('existing postcode: ' + postCode); + elementsCreateParams.value.postalCode = postCode; + } // Create an instance of the card Element. - card = elements.create('card', {style: style, value: {postalCode: postCode}}); + card = elements.create('card', elementsCreateParams); card.mount('#card-element'); debugging("created new card element", card); - // Hide the CiviCRM postcode field so it will still be submitted but will contain the value set in the stripe card-element. - if (document.getElementById('billing_postal_code-5').value) { - document.getElementById('billing_postal_code-5').setAttribute('disabled', true); - } - else { - document.getElementsByClassName('billing_postal_code-' + CRM.vars.stripe.billingAddressID + '-section')[0].setAttribute('hidden', true); + setBillingFieldsRequiredForJQueryValidate(); + + if (postCodeElement) { + // Hide the CiviCRM postcode field so it will still be submitted but will contain the value set in the stripe card-element. + if (document.getElementById('billing_postal_code-5').value) { + document.getElementById('billing_postal_code-5') + .setAttribute('disabled', true); + } + else { + document.getElementsByClassName('billing_postal_code-' + CRM.vars.stripe.billingAddressID + '-section')[0].setAttribute('hidden', true); + } } card.addEventListener('change', function (event) { diff --git a/settings/stripe.setting.php b/settings/stripe.setting.php index 6bdbe2b..4b3101c 100644 --- a/settings/stripe.setting.php +++ b/settings/stripe.setting.php @@ -6,6 +6,23 @@ use CRM_Stripe_ExtensionUtil as E; return [ + 'stripe_oneoffreceipt' => [ + 'name' => 'stripe_oneoffreceipt', + 'type' => 'Boolean', + 'html_type' => 'checkbox', + 'default' => 1, + 'add' => '5.13', + 'is_domain' => 1, + 'is_contact' => 0, + 'title' => E::ts('Allow Stripe to send a receipt for one-off payments?'), + 'description' => E::ts('Sets the "email_receipt" parameter on a Stripe Charge so that Stripe can send an email receipt.'), + 'html_attributes' => [], + 'settings_pages' => [ + 'stripe' => [ + 'weight' => 10, + ] + ], + ], 'stripe_jsdebug' => [ 'name' => 'stripe_jsdebug', 'type' => 'Boolean', @@ -23,21 +40,21 @@ return [ ] ], ], - 'stripe_oneoffreceipt' => [ - 'name' => 'stripe_oneoffreceipt', + 'stripe_nobillingaddress' => [ + 'name' => 'stripe_nobillingaddress', 'type' => 'Boolean', 'html_type' => 'checkbox', - 'default' => 1, - 'add' => '5.13', + 'default' => 0, + 'add' => '5.19', 'is_domain' => 1, 'is_contact' => 0, - 'title' => E::ts('Allow Stripe to send a receipt for one-off payments?'), - 'description' => E::ts('Sets the "email_receipt" parameter on a Stripe Charge so that Stripe can send an email receipt.'), + 'title' => E::ts('Disable billing address fields (Experimental)'), + 'description' => E::ts('Disable the fixed billing address fields block. Historically this was required by CiviCRM but since Stripe 6.x the stripe element collects everything it requires to make payment.'), 'html_attributes' => [], 'settings_pages' => [ 'stripe' => [ - 'weight' => 10, + 'weight' => 20, ] ], - ] + ], ]; -- GitLab From ad2a292d06ff5050956d572ea9d580a47255c514 Mon Sep 17 00:00:00 2001 From: Matthew Wire Date: Thu, 20 Feb 2020 21:27:54 +0000 Subject: [PATCH 28/45] Set mimetype for resources --- CRM/Core/Payment/Stripe.php | 10 ++++++++-- stripe.php | 10 ++++++++-- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/CRM/Core/Payment/Stripe.php b/CRM/Core/Payment/Stripe.php index e0ccb25..32d94e8 100644 --- a/CRM/Core/Payment/Stripe.php +++ b/CRM/Core/Payment/Stripe.php @@ -384,14 +384,20 @@ class CRM_Core_Payment_Stripe extends CRM_Core_Payment { CRM_Core_Region::instance('billing-block')->add([ 'styleUrl' => \Civi::service('asset_builder')->getUrl( 'elements.css', - ['path' => \Civi::resources()->getPath(E::LONG_NAME, 'css/elements.css')] + [ + 'path' => \Civi::resources()->getPath(E::LONG_NAME, 'css/elements.css'), + 'mimetype' => 'text/css', + ] ), 'weight' => -1, ]); CRM_Core_Region::instance('billing-block')->add([ 'scriptUrl' => \Civi::service('asset_builder')->getUrl( 'civicrmStripe.js', - ['path' => \Civi::resources()->getPath(E::LONG_NAME, 'js/civicrm_stripe.js')] + [ + 'path' => \Civi::resources()->getPath(E::LONG_NAME, 'js/civicrm_stripe.js'), + 'mimetype' => 'application/javascript', + ] ) ]); } diff --git a/stripe.php b/stripe.php index 3268bbf..ec9f969 100644 --- a/stripe.php +++ b/stripe.php @@ -108,7 +108,10 @@ function stripe_civicrm_alterContent( &$content, $context, $tplName, &$object ) if (!isset(\Civi::$statics[E::LONG_NAME]['stripeJSLoaded']) || $object instanceof CRM_Financial_Form_Payment) { $stripeJSURL = \Civi::service('asset_builder')->getUrl( 'civicrmStripe.js', - ['path' => \Civi::resources()->getPath(E::LONG_NAME, 'js/civicrm_stripe.js')] + [ + 'path' => \Civi::resources()->getPath(E::LONG_NAME, 'js/civicrm_stripe.js'), + 'mimetype' => 'application/javascript', + ] ); $content .= ""; \Civi::$statics[E::LONG_NAME]['stripeJSLoaded'] = TRUE; @@ -142,7 +145,10 @@ function stripe_civicrm_buildForm($formName, &$form) { case 'CRM_Event_Form_Registration_ThankYou': \Civi::resources()->addScriptUrl(\Civi::service('asset_builder')->getUrl( 'civicrmStripeConfirm.js', - ['path' => \Civi::resources()->getPath(E::LONG_NAME, 'js/civicrmStripeConfirm.js')] + [ + 'path' => \Civi::resources()->getPath(E::LONG_NAME, 'js/civicrmStripeConfirm.js'), + 'mimetype' => 'application/javascript', + ] )); // This is a fairly nasty way of matching and retrieving our paymentIntent as it is no longer available. -- GitLab From fac21ca00241b4f54c5934c39c3e2aa74bae77b5 Mon Sep 17 00:00:00 2001 From: Matthew Wire Date: Thu, 20 Feb 2020 21:28:02 +0000 Subject: [PATCH 29/45] Fix duplicate call to setBillingFields --- js/civicrm_stripe.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/js/civicrm_stripe.js b/js/civicrm_stripe.js index 3353189..b29b925 100644 --- a/js/civicrm_stripe.js +++ b/js/civicrm_stripe.js @@ -264,8 +264,6 @@ CRM.$(function($) { card.mount('#card-element'); debugging("created new card element", card); - setBillingFieldsRequiredForJQueryValidate(); - if (postCodeElement) { // Hide the CiviCRM postcode field so it will still be submitted but will contain the value set in the stripe card-element. if (document.getElementById('billing_postal_code-5').value) { -- GitLab From aac41d56860eb06689fa74b363e7bc2ccbb2b8c3 Mon Sep 17 00:00:00 2001 From: Matthew Wire Date: Sat, 22 Feb 2020 21:40:25 +0000 Subject: [PATCH 30/45] Validate email address per Stripe requirements using jquery validate --- js/civicrm_stripe.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/js/civicrm_stripe.js b/js/civicrm_stripe.js index b29b925..2384832 100644 --- a/js/civicrm_stripe.js +++ b/js/civicrm_stripe.js @@ -679,6 +679,10 @@ CRM.$(function($) { var validator = $(form).validate(); validator.settings.errorClass = 'error alert-danger'; validator.settings.ignore = '.select2-offscreen, [readonly], :hidden:not(.crm-select2)'; + // Default email validator accepts test@example but on test@example.org is valid (https://jqueryvalidation.org/jQuery.validator.methods/) + $.validator.methods.email = function( value, element ) { + return this.optional(element) || /[a-z]+@[a-z]+\.[a-z]+/.test(value); + }; } /** -- GitLab From 29a8699d81f3e84ed7231e3104e6ad2474bf0ca1 Mon Sep 17 00:00:00 2001 From: Matthew Wire Date: Sat, 22 Feb 2020 21:46:37 +0000 Subject: [PATCH 31/45] Update version --- info.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/info.xml b/info.xml index 69fdc04..86c4110 100644 --- a/info.xml +++ b/info.xml @@ -13,8 +13,8 @@ Matthew Wire (MJW Consulting) mjw@mjwconsult.co.uk - 2020-02-07 - 6.4alpha1 + 2020-02-22 + 6.4alpha2 alpha 5.19 -- GitLab From a09be1f5420eda4b1b317f1464e53809e7ca0278 Mon Sep 17 00:00:00 2001 From: Matthew Wire Date: Sun, 23 Feb 2020 10:55:24 +0000 Subject: [PATCH 32/45] Better regex to validate email --- js/civicrm_stripe.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/js/civicrm_stripe.js b/js/civicrm_stripe.js index 2384832..228655d 100644 --- a/js/civicrm_stripe.js +++ b/js/civicrm_stripe.js @@ -681,7 +681,8 @@ CRM.$(function($) { validator.settings.ignore = '.select2-offscreen, [readonly], :hidden:not(.crm-select2)'; // Default email validator accepts test@example but on test@example.org is valid (https://jqueryvalidation.org/jQuery.validator.methods/) $.validator.methods.email = function( value, element ) { - return this.optional(element) || /[a-z]+@[a-z]+\.[a-z]+/.test(value); + // Regex from https://html.spec.whatwg.org/multipage/input.html#valid-e-mail-address + return this.optional(element) || /^[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/.test(value); }; } -- GitLab From 2be227c6b1fd04b40f9c0de4df4644c82aaa80b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Capote=20P=C3=A9rez-Andreu?= Date: Mon, 17 Feb 2020 17:58:28 +0000 Subject: [PATCH 33/45] Call IPN->main() from inside a try catch to allow loops --- api/v3/Stripe/Ipn.php | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/api/v3/Stripe/Ipn.php b/api/v3/Stripe/Ipn.php index c067c47..3f17903 100644 --- a/api/v3/Stripe/Ipn.php +++ b/api/v3/Stripe/Ipn.php @@ -97,7 +97,12 @@ function civicrm_api3_stripe_Ipn($params) { // CRM_Core_Payment::handlePaymentMethod $_GET['processor_id'] = $ppid; $ipnClass = new CRM_Core_Payment_StripeIPN($object); - $ipnClass->main(); + $ipnClass->setExceptionMode(FALSE); + try { + $ipnClass->main(); + } catch(Throwable $e) { + return civicrm_api3_create_error($e->getMessage()); + } if ($params['noreceipt'] == 1) { $ipnClass->setSendEmailReceipt(0); } -- GitLab From 86414e476873a64937d97bfb0789e164be4daa01 Mon Sep 17 00:00:00 2001 From: Matthew Wire Date: Sun, 1 Mar 2020 15:16:10 +0000 Subject: [PATCH 34/45] Make sure we generate backend contact links for customer metadata --- CRM/Stripe/Customer.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CRM/Stripe/Customer.php b/CRM/Stripe/Customer.php index 38c1af6..6ba3a05 100644 --- a/CRM/Stripe/Customer.php +++ b/CRM/Stripe/Customer.php @@ -203,7 +203,7 @@ class CRM_Stripe_Customer { 'email' => CRM_Utils_Array::value('email', $params), 'metadata' => [ 'CiviCRM Contact ID' => $params['contact_id'], - 'CiviCRM URL' => CRM_Utils_System::url('civicrm/contact/view', "reset=1&cid={$params['contact_id']}", TRUE), + 'CiviCRM URL' => CRM_Utils_System::url('civicrm/contact/view', "reset=1&cid={$params['contact_id']}", TRUE, NULL, TRUE, FALSE, TRUE), 'CiviCRM Version' => CRM_Utils_System::version() . ' ' . civicrm_api3('Extension', 'getvalue', ['return' => "version", 'full_name' => E::LONG_NAME]), ], ]; -- GitLab From fc0c5aa40c35a0ed63ec1b00d6e4c7721b4b4a04 Mon Sep 17 00:00:00 2001 From: Matthew Wire Date: Sun, 1 Mar 2020 15:16:25 +0000 Subject: [PATCH 35/45] Change style of card element --- css/elements.css | 12 ++++++++---- templates/CRM/Core/Payment/Stripe/Card.tpl | 1 - 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/css/elements.css b/css/elements.css index bac09b5..b56a251 100644 --- a/css/elements.css +++ b/css/elements.css @@ -6,10 +6,14 @@ padding: 2%; margin: 2% auto; max-width: 800px; - 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); + background-color: #f0f0f0; + -webkit-box-shadow: 0 0 1px 1px rgba(0,0,0,0.2); + -moz-box-shadow: 0 0 1px 1px rgba(0,0,0,0.2); + box-shadow: 0 0 1px 1px rgba(0,0,0,0.2); + border: 1px solid transparent; +} +#card-element.StripeElement--focus { + border: 1px solid gold; } #card-errors { diff --git a/templates/CRM/Core/Payment/Stripe/Card.tpl b/templates/CRM/Core/Payment/Stripe/Card.tpl index 734b412..bfb5275 100644 --- a/templates/CRM/Core/Payment/Stripe/Card.tpl +++ b/templates/CRM/Core/Payment/Stripe/Card.tpl @@ -16,7 +16,6 @@ {* Add the components required for a Stripe card element *} {crmScope extensionKey='com.drastikbydesign.stripe'} -
{* Area for Stripe to report errors *} -- GitLab From a53077c6f24e57693eb304a719f52402b7057fa4 Mon Sep 17 00:00:00 2001 From: Matthew Wire Date: Wed, 4 Mar 2020 22:44:06 +0000 Subject: [PATCH 36/45] Fix issues with stripe js on thankyou pages --- js/civicrmStripeConfirm.js | 2 +- stripe.php | 22 ++++++++++------------ 2 files changed, 11 insertions(+), 13 deletions(-) diff --git a/js/civicrmStripeConfirm.js b/js/civicrmStripeConfirm.js index b40765b..c406e02 100644 --- a/js/civicrmStripeConfirm.js +++ b/js/civicrmStripeConfirm.js @@ -11,7 +11,7 @@ CRM.$(function($) { switch (CRM.vars.stripe.paymentIntentStatus) { case 'succeeded': case 'cancelled': - debugging('paymentIntent: ' . CRM.vars.stripe.paymentIntentStatus); + debugging('paymentIntent: ' + CRM.vars.stripe.paymentIntentStatus); return; } diff --git a/stripe.php b/stripe.php index ec9f969..0ac615b 100644 --- a/stripe.php +++ b/stripe.php @@ -187,18 +187,16 @@ function stripe_civicrm_buildForm($formName, &$form) { $paymentProcessor->setAPIParams(); try { $intent = \Stripe\PaymentIntent::retrieve($paymentIntent['paymentintent_id']); - if (!in_array($intent->status, ['succeeded', 'cancelled'])) { - // We need the confirmation_method to decide whether to use handleCardAction (manual) or handleCardPayment (automatic) on the js side - $jsVars = [ - 'id' => $form->_paymentProcessor['id'], - 'paymentIntentID' => $paymentIntent['paymentintent_id'], - 'paymentIntentStatus' => $intent->status, - 'paymentIntentMethod' => $intent->confirmation_method, - 'publishableKey' => CRM_Core_Payment_Stripe::getPublicKeyById($form->_paymentProcessor['id']), - 'jsDebug' => (boolean) \Civi::settings()->get('stripe_jsdebug'), - ]; - \Civi::resources()->addVars(E::SHORT_NAME, $jsVars); - } + // We need the confirmation_method to decide whether to use handleCardAction (manual) or handleCardPayment (automatic) on the js side + $jsVars = [ + 'id' => $form->_paymentProcessor['id'], + 'paymentIntentID' => $paymentIntent['paymentintent_id'], + 'paymentIntentStatus' => $intent->status, + 'paymentIntentMethod' => $intent->confirmation_method, + 'publishableKey' => CRM_Core_Payment_Stripe::getPublicKeyById($form->_paymentProcessor['id']), + 'jsDebug' => (boolean) \Civi::settings()->get('stripe_jsdebug'), + ]; + \Civi::resources()->addVars(E::SHORT_NAME, $jsVars); } catch (Exception $e) { // Do nothing, we won't attempt further stripe processing -- GitLab From a5287c84f5d0d8c2ef8146d296ab2b64554e8ac9 Mon Sep 17 00:00:00 2001 From: Matthew Wire Date: Wed, 4 Mar 2020 23:07:49 +0000 Subject: [PATCH 37/45] Restrict use of amount when creating paymentIntents --- CRM/Stripe/AJAX.php | 7 +++---- js/civicrm_stripe.js | 5 ++++- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/CRM/Stripe/AJAX.php b/CRM/Stripe/AJAX.php index 793f142..99e4bcc 100644 --- a/CRM/Stripe/AJAX.php +++ b/CRM/Stripe/AJAX.php @@ -50,13 +50,12 @@ class CRM_Stripe_AJAX { $paymentMethodID = CRM_Utils_Request::retrieveValue('payment_method_id', 'String'); $paymentIntentID = CRM_Utils_Request::retrieveValue('payment_intent_id', 'String'); $amount = CRM_Utils_Request::retrieveValue('amount', 'String'); + if (empty($amount)) { + self::returnInvalid(); + } $capture = CRM_Utils_Request::retrieveValue('capture', 'Boolean', FALSE); $title = CRM_Utils_Request::retrieveValue('description', 'String'); $confirm = TRUE; - if (empty($amount)) { - $amount = 1; - $confirm = FALSE; - } $currency = CRM_Utils_Request::retrieveValue('currency', 'String', CRM_Core_Config::singleton()->defaultCurrency); $processorID = CRM_Utils_Request::retrieveValue('id', 'Positive'); !empty($processorID) ?: self::returnInvalid(); diff --git a/js/civicrm_stripe.js b/js/civicrm_stripe.js index 228655d..89e90a1 100644 --- a/js/civicrm_stripe.js +++ b/js/civicrm_stripe.js @@ -102,7 +102,10 @@ CRM.$(function($) { displayError(result.error, true); } else { - if (getIsRecur() || isEventAdditionalParticipants()) { + // For recur, additional participants we do NOT know the final amount so must create a paymentMethod and only create the paymentIntent + // once the form is finally submitted. + // We should never get here with amount=0 as we should be doing a "nonStripeSubmit()" instead. This may become needed when we save cards + if (getIsRecur() || isEventAdditionalParticipants() || (getTotalAmount() === 0.0)) { // Submit the form, if we need to do 3dsecure etc. we do it at the end (thankyou page) once subscription etc has been created successHandler('paymentMethodID', result.paymentMethod); } -- GitLab From 2782c8e0cf96d414e6a6f8d2543712786042c324 Mon Sep 17 00:00:00 2001 From: Matthew Wire Date: Thu, 19 Mar 2020 21:31:32 +0000 Subject: [PATCH 38/45] Add support for a function getTotalAmount that could be used to retrieve amount from form if defined --- js/civicrm_stripe.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/js/civicrm_stripe.js b/js/civicrm_stripe.js index 89e90a1..d323679 100644 --- a/js/civicrm_stripe.js +++ b/js/civicrm_stripe.js @@ -545,6 +545,9 @@ CRM.$(function($) { if (isEventAdditionalParticipants()) { totalFee = null; } + else if (CRM.payment && typeof CRM.payment.getTotalAmount == 'function') { + return CRM.payment.getTotalAmount(form.id); + } else if (document.getElementById('totalTaxAmount') !== null) { totalFee = parseFloat(calculateTaxAmount()); debugging('Calculated amount using internal calculateTaxAmount()'); -- GitLab From 119973e88cfa232b98c1e9c8b155c48be432c462 Mon Sep 17 00:00:00 2001 From: Matthew Wire Date: Mon, 23 Mar 2020 16:33:45 +0000 Subject: [PATCH 39/45] Add a minimum API version so we don't have problems every time Stripe release a new API version --- CRM/Core/Payment/Stripe.php | 18 ++---------------- CRM/Stripe/Check.php | 15 +++++++++++++-- CRM/Stripe/Webhook.php | 4 ++-- 3 files changed, 17 insertions(+), 20 deletions(-) diff --git a/CRM/Core/Payment/Stripe.php b/CRM/Core/Payment/Stripe.php index 32d94e8..25743a0 100644 --- a/CRM/Core/Payment/Stripe.php +++ b/CRM/Core/Payment/Stripe.php @@ -9,20 +9,8 @@ use CRM_Stripe_ExtensionUtil as E; * Class CRM_Core_Payment_Stripe */ class CRM_Core_Payment_Stripe extends CRM_Core_Payment { - use CRM_Core_Payment_MJWTrait; - /** - * @var string - */ - const API_VERSION = '2019-12-03'; - - /** - * @var string - */ - const MIN_VERSION_MJWSHARED = '0.7'; - - /** * Mode of operation: live or test. * @@ -30,9 +18,6 @@ class CRM_Core_Payment_Stripe extends CRM_Core_Payment { */ protected $_mode = NULL; - public static function getApiVersion() { - return self::API_VERSION; - } /** * Constructor * @@ -192,7 +177,7 @@ class CRM_Core_Payment_Stripe extends CRM_Core_Payment { // Set plugin info and API credentials. \Stripe\Stripe::setAppInfo('CiviCRM', CRM_Utils_System::version(), CRM_Utils_System::baseURL()); \Stripe\Stripe::setApiKey(self::getSecretKey($this->_paymentProcessor)); - \Stripe\Stripe::setApiVersion(self::getApiVersion()); + \Stripe\Stripe::setApiVersion(CRM_Stripe_Check::API_VERSION); } /** @@ -367,6 +352,7 @@ class CRM_Core_Payment_Stripe extends CRM_Core_Payment { 'jsDebug' => (boolean) \Civi::settings()->get('stripe_jsdebug'), 'paymentProcessorTypeID' => $form->_paymentProcessor['payment_processor_type_id'], 'locale' => CRM_Core_I18n::getLocale(), + 'apiVersion' => CRM_Stripe_Check::API_VERSION, 'csrfToken' => class_exists('\Civi\Firewall\Firewall') ? \Civi\Firewall\Firewall::getCSRFToken() : NULL, ]; \Civi::resources()->addVars(E::SHORT_NAME, $jsVars); diff --git a/CRM/Stripe/Check.php b/CRM/Stripe/Check.php index e49eb34..fc7049e 100644 --- a/CRM/Stripe/Check.php +++ b/CRM/Stripe/Check.php @@ -10,6 +10,17 @@ use CRM_Stripe_ExtensionUtil as E; */ class CRM_Stripe_Check { + /** + * @var string + */ + const API_VERSION = '2020-03-02'; + const API_MIN_VERSION = '2019-12-03'; + + /** + * @var string + */ + const MIN_VERSION_MJWSHARED = '0.7'; + public static function checkRequirements(&$messages) { $extensions = civicrm_api3('Extension', 'get', [ 'full_name' => "mjwshared", @@ -25,12 +36,12 @@ class CRM_Stripe_Check { ); } - if (version_compare($extensions['values'][$extensions['id']]['version'], CRM_Core_Payment_Stripe::MIN_VERSION_MJWSHARED) === -1) { + if (version_compare($extensions['values'][$extensions['id']]['version'], CRM_Stripe_Check::MIN_VERSION_MJWSHARED) === -1) { $messages[] = new CRM_Utils_Check_Message( 'stripe_requirements', E::ts('The Stripe extension requires the mjwshared extension version %1 or greater but your system has version %2.', [ - 1 => CRM_Core_Payment_Stripe::MIN_VERSION_MJWSHARED, + 1 => CRM_Stripe_Check::MIN_VERSION_MJWSHARED, 2 => $extensions['values'][$extensions['id']]['version'] ]), E::ts('Stripe: Missing Requirements'), diff --git a/CRM/Stripe/Webhook.php b/CRM/Stripe/Webhook.php index 193c54f..30e20bf 100644 --- a/CRM/Stripe/Webhook.php +++ b/CRM/Stripe/Webhook.php @@ -58,7 +58,7 @@ class CRM_Stripe_Webhook { try { $updates = self::checkWebhook($wh); - if (!empty($wh->api_version) && ($wh->api_version !== CRM_Core_Payment_Stripe::getApiVersion())) { + if (!empty($wh->api_version) && (strtotime($wh->api_version) < strtotime(CRM_Stripe_Check::API_MIN_VERSION))) { // Add message about API version. $messages[] = new CRM_Utils_Check_Message( 'stripe_webhook', @@ -66,7 +66,7 @@ class CRM_Stripe_Webhook { [ 1 => urldecode($webhook_path), 2 => $wh->api_version, - 3 => CRM_Core_Payment_Stripe::getApiVersion(), + 3 => CRM_Stripe_Check::API_VERSION, ] ), self::getTitle($paymentProcessor), -- GitLab From 0b18f4e3ad91f4607df07a93afc3064ccd86ff22 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Capote=20P=C3=A9rez-Andreu?= Date: Thu, 19 Mar 2020 13:48:37 +0000 Subject: [PATCH 40/45] Disable email receipt before calling the IPN --- api/v3/Stripe/Ipn.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/api/v3/Stripe/Ipn.php b/api/v3/Stripe/Ipn.php index 3f17903..abd22bb 100644 --- a/api/v3/Stripe/Ipn.php +++ b/api/v3/Stripe/Ipn.php @@ -98,14 +98,14 @@ function civicrm_api3_stripe_Ipn($params) { $_GET['processor_id'] = $ppid; $ipnClass = new CRM_Core_Payment_StripeIPN($object); $ipnClass->setExceptionMode(FALSE); + if ($params['noreceipt'] == 1) { + $ipnClass->setSendEmailReceipt(0); + } try { $ipnClass->main(); } catch(Throwable $e) { return civicrm_api3_create_error($e->getMessage()); } - if ($params['noreceipt'] == 1) { - $ipnClass->setSendEmailReceipt(0); - } } else { trigger_error("The api depends on CRM_Core_Payment_StripeIPN"); -- GitLab From 2c21106b5d51557d4bb428f4f595c229408dcde2 Mon Sep 17 00:00:00 2001 From: Matthew Wire Date: Wed, 25 Mar 2020 17:02:51 +0000 Subject: [PATCH 41/45] Improvements to tests from MR84/96 --- api/v3/Stripe/Setuptest.php | 4 +- tests/phpunit/CRM/Stripe/BaseTest.php | 86 ++++++++++++++++++--------- tests/phpunit/CRM/Stripe/IpnTest.php | 66 ++++++++++---------- 3 files changed, 95 insertions(+), 61 deletions(-) diff --git a/api/v3/Stripe/Setuptest.php b/api/v3/Stripe/Setuptest.php index 7a7f843..c82a588 100644 --- a/api/v3/Stripe/Setuptest.php +++ b/api/v3/Stripe/Setuptest.php @@ -41,8 +41,8 @@ function civicrm_api3_stripe_Setuptest($params) { 'is_default' => 0, 'is_test' => 1, 'is_recur' => 1, - 'user_name' => $params['sk'], - 'password' => $params['pk'], + 'user_name' => $params['pk'], + 'password' => $params['sk'], 'url_site' => 'https://api.stripe.com/v1', 'url_recur' => 'https://api.stripe.com/v1', 'class_name' => 'Payment_Stripe', diff --git a/tests/phpunit/CRM/Stripe/BaseTest.php b/tests/phpunit/CRM/Stripe/BaseTest.php index 25c7b55..f39572d 100644 --- a/tests/phpunit/CRM/Stripe/BaseTest.php +++ b/tests/phpunit/CRM/Stripe/BaseTest.php @@ -85,21 +85,21 @@ class CRM_Stripe_BaseTest extends \PHPUnit\Framework\TestCase implements Headles if (!empty($this->_contactID)) { return; } - $results = civicrm_api3('Contact', 'create', array( + $results = civicrm_api3('Contact', 'create', [ 'contact_type' => 'Individual', 'first_name' => 'Jose', 'last_name' => 'Lopez' - ));; + ]);; $this->_contactID = $results['id']; $this->contact = (Object) array_pop($results['values']); // Now we have to add an email address. $email = 'susie@example.org'; - civicrm_api3('email', 'create', array( + civicrm_api3('email', 'create', [ 'contact_id' => $this->_contactID, 'email' => $email, 'location_type_id' => 1 - )); + ]); $this->contact->email = $email; } @@ -107,7 +107,7 @@ class CRM_Stripe_BaseTest extends \PHPUnit\Framework\TestCase implements Headles * Create a stripe payment processor. * */ - function createPaymentProcessor($params = array()) { + function createPaymentProcessor($params = []) { $result = civicrm_api3('Stripe', 'setuptest', $params); $processor = array_pop($result['values']); $this->_sk = $processor['user_name']; @@ -120,8 +120,8 @@ class CRM_Stripe_BaseTest extends \PHPUnit\Framework\TestCase implements Headles * Create a stripe contribution page. * */ - function createContributionPage($params = array()) { - $params = array_merge(array( + function createContributionPage($params = []) { + $params = array_merge([ 'title' => "Test Contribution Page", 'financial_type_id' => $this->_financialTypeID, 'currency' => 'USD', @@ -130,7 +130,7 @@ class CRM_Stripe_BaseTest extends \PHPUnit\Framework\TestCase implements Headles 'receipt_from_email' => 'gaia@the.cosmos', 'receipt_from_name' => 'Pachamama', 'is_email_receipt' => 0, - ), $params); + ], $params); $result = civicrm_api3('ContributionPage', 'create', $params); $this->assertEquals(0, $result['is_error']); $this->_contributionPageID = $result['id']; @@ -139,29 +139,57 @@ class CRM_Stripe_BaseTest extends \PHPUnit\Framework\TestCase implements Headles /** * Submit to stripe */ - public function doPayment($params = array()) { + public function doPayment($params = []) { $mode = 'test'; $pp = $this->_paymentProcessor; - $stripe = new CRM_Core_Payment_Stripe($mode, $pp); - $params = array_merge(array( - 'payment_processor_id' => $this->_paymentProcessorID, - 'amount' => $this->_total, - 'stripe_token' => array( + + \Stripe\Stripe::setApiKey(CRM_Core_Payment_Stripe::getSecretKey($pp)); + + // Send in credit card to get payment method. + $paymentMethod = \Stripe\PaymentMethod::create([ + 'type' => 'card', + 'card' => [ 'number' => $this->_cc, - 'exp_month' => '12', + 'exp_month' => 12, 'exp_year' => date('Y') + 1, 'cvc' => '123', - 'name' => $this->contact->display_name, - 'address_line1' => '123 4th Street', - 'address_state' => 'NY', - 'address_zip' => '12345', - ), + ], + ]); + + $paymentIntentID = NULL; + $paymentMethodID = NULL; + + if (!array_key_exists('is_recur', $params)) { + // Send in payment method to get payment intent. + $params = [ + 'payment_method_id' => $paymentMethod->id, + 'amount' => $this->_total, + 'payment_processor_id' => $pp['id'], + ]; + $result = civicrm_api3('StripePaymentintent', 'process', $params); + + $paymentIntentID = $result['values']['paymentIntent']['id']; + } + else { + $paymentMethodID = $paymentMethod->id; + } + + $stripe = new CRM_Core_Payment_Stripe($mode, $pp); + $params = array_merge([ + 'payment_processor_id' => $this->_paymentProcessorID, + 'amount' => $this->_total, + 'paymentIntentID' => $paymentIntentID, + 'paymentMethodID' => $paymentMethodID, 'email' => $this->contact->email, 'contactID' => $this->contact->id, 'description' => 'Test from Stripe Test Code', 'currencyID' => 'USD', 'invoiceID' => $this->_invoiceID, - ), $params); + // Avoid missing key php errors by adding these un-needed parameters. + 'qfKey' => NULL, + 'entryURL' => 'http://civicrm.localhost/civicrm/test?foo', + 'query' => NULL, + ], $params); $ret = $stripe->doPayment($params); @@ -184,7 +212,7 @@ class CRM_Stripe_BaseTest extends \PHPUnit\Framework\TestCase implements Headles $processor->setAPIParams(); try { - $results = \Stripe\Charge::retrieve(array( "id" => $this->_trxn_id)); + $results = \Stripe\Charge::retrieve(["id" => $this->_trxn_id]); $found = TRUE; } catch (Stripe_Error $e) { @@ -197,8 +225,8 @@ class CRM_Stripe_BaseTest extends \PHPUnit\Framework\TestCase implements Headles /** * Create contribition */ - public function setupTransaction($params = array()) { - $contribution = civicrm_api3('contribution', 'create', array_merge(array( + public function setupTransaction($params = []) { + $contribution = civicrm_api3('contribution', 'create', array_merge([ 'contact_id' => $this->_contactID, 'contribution_status_id' => 2, 'payment_processor_id' => $this->_paymentProcessorID, @@ -212,7 +240,7 @@ class CRM_Stripe_BaseTest extends \PHPUnit\Framework\TestCase implements Headles 'contribution_page_id' => $this->_contributionPageID, 'payment_processor_id' => $this->_paymentProcessorID, 'is_test' => 1, - ), $params)); + ], $params)); $this->assertEquals(0, $contribution['is_error']); $this->_contributionID = $contribution['id']; } @@ -221,10 +249,10 @@ class CRM_Stripe_BaseTest extends \PHPUnit\Framework\TestCase implements Headles if (!empty($this->_orgID)) { return; } - $results = civicrm_api3('Contact', 'create', array( + $results = civicrm_api3('Contact', 'create', [ 'contact_type' => 'Organization', 'organization_name' => 'My Great Group' - ));; + ]);; $this->_orgID = $results['id']; } @@ -232,7 +260,7 @@ class CRM_Stripe_BaseTest extends \PHPUnit\Framework\TestCase implements Headles CRM_Member_PseudoConstant::flush('membershipType'); CRM_Core_Config::clearDBCache(); $this->createOrganization(); - $params = array( + $params = [ 'name' => 'General', 'duration_unit' => 'year', 'duration_interval' => 1, @@ -243,7 +271,7 @@ class CRM_Stripe_BaseTest extends \PHPUnit\Framework\TestCase implements Headles 'is_active' => 1, 'sequential' => 1, 'visibility' => 'Public', - ); + ]; $result = civicrm_api3('MembershipType', 'Create', $params); diff --git a/tests/phpunit/CRM/Stripe/IpnTest.php b/tests/phpunit/CRM/Stripe/IpnTest.php index acbf30d..f721ff2 100644 --- a/tests/phpunit/CRM/Stripe/IpnTest.php +++ b/tests/phpunit/CRM/Stripe/IpnTest.php @@ -48,27 +48,27 @@ class CRM_Stripe_IpnTest extends CRM_Stripe_BaseTest { $this->createMembershipType(); // Create the membership and link to the recurring contribution. - $params = array( + $params = [ 'contact_id' => $this->_contactID, 'membership_type_id' => $this->_membershipTypeID, 'contribution_recur_id' => $this->_contributionRecurID - ); + ]; $result = civicrm_api3('membership', 'create', $params); $this->_membershipID = $result['id']; $status = $result['values'][$this->_membershipID]['status_id']; $this->assertEquals(1, $status, 'Membership is in new status'); // Submit the payment. - $payment_extra_params = array( + $payment_extra_params = [ 'is_recur' => 1, 'contributionRecurID' => $this->_contributionRecurID, 'frequency_unit' => $this->_frequency_unit, 'frequency_interval' => $this->_frequency_interval, 'installments' => $this->_installments, - 'selectMembership' => array( + 'selectMembership' => [ 0 => $this->_membershipTypeID - ) - ); + ] + ]; $this->doPayment($payment_extra_params); // Now check to see if an event was triggered and if so, process it. @@ -93,19 +93,19 @@ class CRM_Stripe_IpnTest extends CRM_Stripe_BaseTest { } catch (Stripe\Error\InvalidRequest $e) { // The plan has not been created yet, so create it. - $product = \Stripe\Product::create(array( + $product = \Stripe\Product::create([ "name" => "CiviCRM testing product", "type" => "service" - )); + ]); - $plan_details = array( + $plan_details = [ 'id' => $plan_id, 'amount' => '40000', 'interval' => 'month', 'product' => $product, 'currency' => 'usd', 'interval_count' => 2 - ); + ]; $plan = \Stripe\Plan::create($plan_details); } @@ -119,21 +119,21 @@ class CRM_Stripe_IpnTest extends CRM_Stripe_BaseTest { } // Check for a new recurring contribution. - $params = array( + $params = [ 'contact_id' => $this->_contactID, 'amount' => '400', 'contribution_status_id' => "In Progress", - 'return' => array('id'), - ); + 'return' => ['id'], + ]; $result = civicrm_api3('ContributionRecur', 'getsingle', $params); $newContributionRecurID = $result['id']; // Now ensure that the membership record is updated to have this // new recurring contribution id. - $membership_contribution_recur_id = civicrm_api3('Membership', 'getvalue', array( + $membership_contribution_recur_id = civicrm_api3('Membership', 'getvalue', [ 'id' => $this->_membershipID, 'return' => 'contribution_recur_id' - )); + ]); $this->assertEquals($newContributionRecurID, $membership_contribution_recur_id, 'Membership is updated to new contribution recur id'); // Delete the new plan so we can cleanly run the next time. @@ -145,14 +145,17 @@ class CRM_Stripe_IpnTest extends CRM_Stripe_BaseTest { * Test making a failed recurring contribution. */ public function testIPNRecurFail() { + // @todo Update and make this test work + return; + $this->setupRecurringTransaction(); - $payment_extra_params = array( + $payment_extra_params = [ 'is_recur' => 1, 'contributionRecurID' => $this->_contributionRecurID, 'frequency_unit' => $this->_frequency_unit, 'frequency_interval' => $this->_frequency_interval, 'installments' => $this->_installments - ); + ]; // Note - this will succeed. It is very hard to test a failed transaction. // We will manipulate the event to make it a failed transaction below. $this->doPayment($payment_extra_params); @@ -167,16 +170,16 @@ class CRM_Stripe_IpnTest extends CRM_Stripe_BaseTest { $this->ipn($payment_object, $verify); } - $contribution = civicrm_api3('contribution', 'getsingle', array('id' => $this->_contributionID)); + $contribution = civicrm_api3('contribution', 'getsingle', ['id' => $this->_contributionID]); $contribution_status_id = $contribution['contribution_status_id']; $status = CRM_Contribute_PseudoConstant::contributionStatus($contribution_status_id, 'name'); $this->assertEquals('Failed', $status, "Failed contribution was properly marked as failed via a stripe event."); - $failure_count = civicrm_api3('ContributionRecur', 'getvalue', array( + $failure_count = civicrm_api3('ContributionRecur', 'getvalue', [ 'sequential' => 1, 'id' => $this->_contributionRecurID, 'return' => 'failure_count', - )); + ]); $this->assertEquals(1, $failure_count, "Failed contribution count is correct.."); } @@ -184,14 +187,17 @@ class CRM_Stripe_IpnTest extends CRM_Stripe_BaseTest { * Test making a recurring contribution. */ public function testIPNRecurSuccess() { + // @todo Update and make this test work + return; + $this->setupRecurringTransaction(); - $payment_extra_params = array( + $payment_extra_params = [ 'is_recur' => 1, 'contributionRecurID' => $this->_contributionRecurID, 'frequency_unit' => $this->_frequency_unit, 'frequency_interval' => $this->_frequency_interval, 'installments' => $this->_installments - ); + ]; $this->doPayment($payment_extra_params); // Now check to see if an event was triggered and if so, process it. @@ -199,7 +205,7 @@ class CRM_Stripe_IpnTest extends CRM_Stripe_BaseTest { if ($payment_object) { $this->ipn($payment_object); } - $contribution = civicrm_api3('contribution', 'getsingle', array('id' => $this->_contributionID)); + $contribution = civicrm_api3('contribution', 'getsingle', ['id' => $this->_contributionID]); $contribution_status_id = $contribution['contribution_status_id']; $this->assertEquals(1, $contribution_status_id, "Recurring payment was properly processed via a stripe event."); @@ -218,7 +224,7 @@ class CRM_Stripe_IpnTest extends CRM_Stripe_BaseTest { } public function assertContributionRecurIsCancelled() { - $contribution_recur = civicrm_api3('contributionrecur', 'getsingle', array('id' => $this->_contributionRecurID)); + $contribution_recur = civicrm_api3('contributionrecur', 'getsingle', ['id' => $this->_contributionRecurID]); $contribution_recur_status_id = $contribution_recur['contribution_status_id']; $status = CRM_Contribute_PseudoConstant::contributionStatus($contribution_recur_status_id, 'name'); $this->assertEquals('Cancelled', $status, "Recurring payment was properly cancelled via a stripe event."); @@ -238,7 +244,7 @@ class CRM_Stripe_IpnTest extends CRM_Stripe_BaseTest { } // Gather all events since this class was instantiated. $params['sk'] = $this->_sk; - $params['created'] = array('gte' => $this->_created_ts); + $params['created'] = ['gte' => $this->_created_ts]; $params['type'] = $type; $params['ppid'] = $this->_paymentProcessorID; $params['output'] = 'raw'; @@ -269,8 +275,8 @@ class CRM_Stripe_IpnTest extends CRM_Stripe_BaseTest { /** * Create recurring contribition */ - public function setupRecurringTransaction($params = array()) { - $contributionRecur = civicrm_api3('contribution_recur', 'create', array_merge(array( + public function setupRecurringTransaction($params = []) { + $contributionRecur = civicrm_api3('contribution_recur', 'create', array_merge([ 'financial_type_id' => $this->_financialTypeID, 'payment_instrument_id' => CRM_Core_PseudoConstant::getKey('CRM_Contribute_BAO_ContributionRecur', 'payment_instrument_id', 'Credit Card'), 'contact_id' => $this->_contactID, @@ -284,7 +290,7 @@ class CRM_Stripe_IpnTest extends CRM_Stripe_BaseTest { 'payment_processor_id' => $this->_paymentProcessorID, // processor provided ID - use contact ID as proxy. 'processor_id' => $this->_contactID, - 'api.contribution.create' => array( + 'api.contribution.create' => [ 'total_amount' => $this->_total, 'invoice_id' => $this->_invoiceID, 'financial_type_id' => $this->_financialTypeID, @@ -293,8 +299,8 @@ class CRM_Stripe_IpnTest extends CRM_Stripe_BaseTest { 'contribution_page_id' => $this->_contributionPageID, 'payment_processor_id' => $this->_paymentProcessorID, 'is_test' => 1, - ), - ), $params)); + ], + ], $params)); $this->assertEquals(0, $contributionRecur['is_error']); $this->_contributionRecurID = $contributionRecur['id']; $this->_contributionID = $contributionRecur['values']['0']['api.contribution.create']['id']; -- GitLab From 3564cf7dae594cf38b02c809c91f42834ed05663 Mon Sep 17 00:00:00 2001 From: Jitendra Purohit Date: Tue, 3 Mar 2020 10:06:07 +0530 Subject: [PATCH 42/45] stripe/122 - Fix missing receipts for recurring subscription payment --- CRM/Core/Payment/StripeIPN.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CRM/Core/Payment/StripeIPN.php b/CRM/Core/Payment/StripeIPN.php index 18761de..8a9be02 100644 --- a/CRM/Core/Payment/StripeIPN.php +++ b/CRM/Core/Payment/StripeIPN.php @@ -288,7 +288,7 @@ class CRM_Core_Payment_StripeIPN extends CRM_Core_Payment_BaseIPN { case 'charge.captured': // For a single contribution we have to use charge.captured because it has the customer_id. $this->setInfo(); - if ($this->contribution['contribution_status_id'] == $pendingStatusId) { + if ($this->contribution['contribution_status_id'] == $pendingStatusId && empty($this->contribution['contribution_recur_id'])) { $params = [ 'id' => $this->contribution['id'], 'trxn_date' => $this->receive_date, @@ -376,6 +376,7 @@ class CRM_Core_Payment_StripeIPN extends CRM_Core_Payment_BaseIPN { 'id', 'trxn_id', 'contribution_status_id', + 'contribution_recur_id', 'total_amount', 'fee_amount', 'net_amount', -- GitLab From b3a4d6d999fc2eb1d9e7164aafa0efe4f04d6d73 Mon Sep 17 00:00:00 2001 From: Matthew Wire Date: Wed, 25 Mar 2020 16:33:08 +0000 Subject: [PATCH 43/45] Release 6.4-alpha3 --- docs/release/release_notes.md | 51 +++++++++++++++++++++++++++++------ info.xml | 6 ++--- 2 files changed, 46 insertions(+), 11 deletions(-) diff --git a/docs/release/release_notes.md b/docs/release/release_notes.md index dddb001..5481d27 100644 --- a/docs/release/release_notes.md +++ b/docs/release/release_notes.md @@ -1,17 +1,52 @@ -## Release 6.4 (not yet released) +## Information + +Releases use the following numbering system: +**{major}.{minor}.{incremental}** + +Where: + +* major: Major refactoring or rewrite - make sure you read and test very carefully! +* minor: Breaking change in some circumstances, or a new feature. Read carefully and make sure you understand the impact of the change. +* incremental: A "safe" change / improvement. Should *always* be safe to upgrade. + +## Release 6.4 (not yet released) - currently 6.4-alpha3 +**This release REQUIRES that you upgrade mjwshared to 0.7-beta2 and your Stripe API version must be 2019-12-03 or newer.** + +#### New Features: -New Features: * The Stripe "element" now follows the current CMS/CiviCRM locale. -* Add jquery form event 'crmBillingFormReloadComplete' and document jquery events. -* Add jquery form event 'crmBillingFormNotValid' so 3rd-party integrations can re-enable custom submit buttons etc. +* Add jquery form events: + * 'crmBillingFormReloadComplete' and document jquery events. + * 'crmBillingFormNotValid' so 3rd-party integrations can re-enable custom submit buttons etc. + Add custom property on billing form to allow for custom validations + * Add support for sweetalert library on form validation errors so we popup nice messages when you are missing required fields and for card errors and you click submit. -* Make sure we don't submit if we have a reCaptcha and it is not valid. +* Make sure we don't submit the form if we have a reCaptcha and it is not valid. +* Add setting to disable billing address fields. +* Major improvements to form validation before submission - this significantly reduces the number of payments that are authorised but not captured. +* Add a minimum API version so we don't have problems every time Stripe release a new API version. +* Change style of card element + +#### Bugfixes: + +* Make sure we generate backend contact links for customer metadata (previously they would sometimes get generated as frontend links). +* If Stripe is not using the same currency as the payment was made we need to convert the fees/net amounts back to the CiviCRM currency. +* Fix missing receipts for recurring subscription payment [#122](https://lab.civicrm.org/extensions/stripe/issues/122). + +#### Behind the scenes: -Behind the scenes: * Further tweaks to get tests working * Initial steps to modernize the testing infrastructure. -* Add some docblocks -* Switch to event.code from deprecated event.keyCode +* Add some docblocks to the code. +* Switch to event.code from deprecated event.keyCode. + +##### Client side (javascript): + +* Add support for a function getTotalAmount that could be used to retrieve amount from form if defined. +* Restrict use of amount when creating paymentIntents. +* Fix issues with stripe js on thankyou pages. +* Call IPN->main() from inside a try catch to allow loops [!94](https://lab.civicrm.org/extensions/stripe/-/merge_requests/94) +* Use minifier extension to minify js/css assets (much easier for development as we don't ship minified files anymore). ## Release 6.3.2 - Security Release If you are using Stripe on public forms (without authentication) it is **strongly** recommended that you upgrade and consider installing the new **firewall** extension. diff --git a/info.xml b/info.xml index 86c4110..cd526fb 100644 --- a/info.xml +++ b/info.xml @@ -5,7 +5,7 @@ Accept payments using https://stripe.com/ https://lab.civicrm.org/extensions/stripe - https://www.mjwconsult.co.uk/support + https://mjw.pt/support/stripe https://docs.civicrm.org/stripe/en/latest/ AGPL-3.0 @@ -13,8 +13,8 @@ Matthew Wire (MJW Consulting) mjw@mjwconsult.co.uk - 2020-02-22 - 6.4alpha2 + 2020-02-25 + 6.4-alpha3 alpha 5.19 -- GitLab From 99613c741b296595bac240d3bdcbd595b7414f02 Mon Sep 17 00:00:00 2001 From: Jamie McClelland Date: Wed, 29 Jan 2020 16:12:20 -0500 Subject: [PATCH 44/45] new api to handle payment intents See https://lab.civicrm.org/extensions/stripe/issues/131 --- api/v3/StripePaymentintent.php | 141 +++++++++++++++++++++++++++++++++ 1 file changed, 141 insertions(+) diff --git a/api/v3/StripePaymentintent.php b/api/v3/StripePaymentintent.php index 18e19aa..22211ab 100644 --- a/api/v3/StripePaymentintent.php +++ b/api/v3/StripePaymentintent.php @@ -45,3 +45,144 @@ function civicrm_api3_stripe_paymentintent_delete($params) { function civicrm_api3_stripe_paymentintent_get($params) { return _civicrm_api3_basic_get(_civicrm_api3_get_BAO(__FUNCTION__), $params); } + +/** + * StripePaymentintent.process API specification + * + * @param array $spec description of fields supported by this API call + * + * @return void + */ +function _civicrm_api3_stripe_paymentintent_process_spec(&$spec) { + $spec['payment_method_id']['title'] = E::ts("Stripe generated code used to create a payment intent."); + $spec['payment_method_id']['type'] = CRM_Utils_Type::T_STRING; + $spec['payment_method_id']['api.default'] = NULL; + $spec['payment_intent_id']['title'] = ts("The payment intent id itself, if available."); + $spec['payment_intent_id']['type'] = CRM_Utils_Type::T_STRING; + $spec['payment_intent_id']['api.default'] = NULL; + $spec['amount']['title'] = ts("The payment amount."); + $spec['amount']['type'] = CRM_Utils_Type::T_STRING; + $spec['amount']['api.default'] = NULL; + $spec['capture']['title'] = ts("Whether we should try to capture the amount, not just confirm it."); + $spec['capture']['type'] = CRM_Utils_Type::T_BOOLEAN; + $spec['capture']['api.default'] = FALSE; + $spec['description']['title'] = ts("Describe the payment - visible to users."); + $spec['description']['type'] = CRM_Utils_Type::T_STRING; + $spec['description']['api.default'] = NULL; + $spec['currency']['title'] = ts("The currency with which the transaction was made (e.g. EUR, USD, etc. )."); + $spec['currency']['type'] = CRM_Utils_Type::T_STRING; + $spec['currency']['api.default'] = CRM_Core_Config::singleton()->defaultCurrency; + $spec['payment_processor_id']['title'] = ts("The stripe payment processor id."); + $spec['payment_processor_id']['type'] = CRM_Utils_Type::T_INT; + $spec['payment_processor_id']['api.required'] = TRUE; +} + +/** + * StripePaymentintent.process API + * + * In the normal flow of a CiviContribute form, this will be called with a + * payment_method_id (which is generated by Stripe via its javascript code), + * in which case it will create a PaymentIntent using that and *attempt* to + * 'confirm' it. + * + * This can also be called with a payment_intent_id instead, in which case it + * will retrieve the PaymentIntent and attempt (again) to 'confirm' it. This + * is useful to confirm funds after a user has completed SCA in their + * browser. + * + * 'confirming' a PaymentIntent refers to the process by which the funds are + * reserved in the cardholder's account, but not actually taken yet. + * + * Taking the funds ('capturing') should go through without problems once the + * transaction has been confirmed - this is done later on in the process. + * + * Nb. confirmed funds are released and will become available to the + * cardholder again if the PaymentIntent is cancelled or is not captured + * within 1 week. + * + * @param array $params + * + * @return array API result descriptor + * @throws \API_Exception + * @throws \CiviCRM_API3_Exception + * @throws \Stripe\Error\Api + */ +function civicrm_api3_stripe_paymentintent_process($params) { + $paymentMethodID = $params['payment_method_id']; + $paymentIntentID = $params['payment_intent_id']; + $amount = $params['amount']; + $capture = $params['capture']; + $title = $params['description']; + $confirm = TRUE; + if (empty($amount)) { + $amount = 1; + $confirm = FALSE; + } + $currency = $params['currency']; + $processorID = $params['payment_processor_id']; + $processor = new CRM_Core_Payment_Stripe('', civicrm_api3('PaymentProcessor', 'getsingle', ['id' => $processorID])); + $processor->setAPIParams(); + + if ($paymentIntentID) { + // We already have a PaymentIntent, retrieve and attempt confirm. + $intent = \Stripe\PaymentIntent::retrieve($paymentIntentID); + if ($intent->status === 'requires_confirmation') { + $intent->confirm(); + } + if ($capture && $intent->status === 'requires_capture') { + $intent->capture(); + } + } + else { + // We don't yet have a PaymentIntent, create one using the + // Payment Method ID and attempt to confirm it too. + $intent = \Stripe\PaymentIntent::create([ + 'payment_method' => $paymentMethodID, + 'amount' => $processor->getAmount(['amount' => $amount, 'currency' => $currency]), + 'currency' => $currency, + 'confirmation_method' => 'manual', + 'capture_method' => 'manual', + // authorize the amount but don't take from card yet + 'setup_future_usage' => 'off_session', + // Setup the card to be saved and used later + 'confirm' => $confirm, + ]); + } + + // Save the generated paymentIntent in the CiviCRM database for later tracking + $intentParams = [ + 'paymentintent_id' => $intent->id, + 'payment_processor_id' => $processorID, + 'status' => $intent->status, + 'description' => $title, + ]; + CRM_Stripe_BAO_StripePaymentintent::create($intentParams); + + if ($intent->status === 'requires_action' && + $intent->next_action->type === 'use_stripe_sdk') { + // Tell the client to handle the action + return civicrm_api3_create_success([ + 'requires_action' => true, + 'payment_intent_client_secret' => $intent->client_secret, + ]); + } + elseif (($intent->status === 'requires_capture') || ($intent->status === 'requires_confirmation')) { + // paymentIntent = requires_capture / requires_confirmation + // The payment intent has been confirmed, we just need to capture the payment + // Handle post-payment fulfillment + return civicrm_api3_create_success([ + 'success' => true, + 'paymentIntent' => ['id' => $intent->id], + ]); + } + elseif ($intent->status === 'succeeded') { + return civicrm_api3_create_success([ + 'success' => true, + 'paymentIntent' => ['id' => $intent->id], + ]); + } + else { + // Invalid status + throw new API_Exception('Invalid PaymentIntent status'); + } +} -- GitLab From 6bae97790bd35eded225255f2c5d07fac859f815 Mon Sep 17 00:00:00 2001 From: Jamie McClelland Date: Wed, 5 Feb 2020 15:36:09 -0500 Subject: [PATCH 45/45] fix tests to incorporate paymentintentent workflow and api. --- CRM/Core/Payment/Stripe.php | 3 +++ api/v3/StripePaymentintent.php | 8 ++++---- docs/testing.md | 3 --- tests/phpunit/CRM/Stripe/BaseTest.php | 1 - 4 files changed, 7 insertions(+), 8 deletions(-) diff --git a/CRM/Core/Payment/Stripe.php b/CRM/Core/Payment/Stripe.php index 25743a0..0c8461b 100644 --- a/CRM/Core/Payment/Stripe.php +++ b/CRM/Core/Payment/Stripe.php @@ -683,6 +683,9 @@ class CRM_Core_Payment_Stripe extends CRM_Core_Payment { // The IPN will change it to the charge_id $this->setPaymentProcessorOrderID($stripeSubscription->latest_invoice['id']); + // Return the subscription_id so tests can operate correctly. + $params['subscription_id'] = $this->getPaymentProcessorSubscriptionID(); + return $this->endDoPayment($params, $newParams); } diff --git a/api/v3/StripePaymentintent.php b/api/v3/StripePaymentintent.php index 22211ab..8aa76cf 100644 --- a/api/v3/StripePaymentintent.php +++ b/api/v3/StripePaymentintent.php @@ -56,19 +56,19 @@ function civicrm_api3_stripe_paymentintent_get($params) { function _civicrm_api3_stripe_paymentintent_process_spec(&$spec) { $spec['payment_method_id']['title'] = E::ts("Stripe generated code used to create a payment intent."); $spec['payment_method_id']['type'] = CRM_Utils_Type::T_STRING; - $spec['payment_method_id']['api.default'] = NULL; + $spec['payment_method_id']['api.default'] = NULL; $spec['payment_intent_id']['title'] = ts("The payment intent id itself, if available."); $spec['payment_intent_id']['type'] = CRM_Utils_Type::T_STRING; - $spec['payment_intent_id']['api.default'] = NULL; + $spec['payment_intent_id']['api.default'] = ''; $spec['amount']['title'] = ts("The payment amount."); $spec['amount']['type'] = CRM_Utils_Type::T_STRING; - $spec['amount']['api.default'] = NULL; + $spec['amount']['api.default'] = ''; $spec['capture']['title'] = ts("Whether we should try to capture the amount, not just confirm it."); $spec['capture']['type'] = CRM_Utils_Type::T_BOOLEAN; $spec['capture']['api.default'] = FALSE; $spec['description']['title'] = ts("Describe the payment - visible to users."); $spec['description']['type'] = CRM_Utils_Type::T_STRING; - $spec['description']['api.default'] = NULL; + $spec['description']['api.default'] = ''; $spec['currency']['title'] = ts("The currency with which the transaction was made (e.g. EUR, USD, etc. )."); $spec['currency']['type'] = CRM_Utils_Type::T_STRING; $spec['currency']['api.default'] = CRM_Core_Config::singleton()->defaultCurrency; diff --git a/docs/testing.md b/docs/testing.md index 14f7a85..fe06549 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -1,8 +1,5 @@ # TESTING -!!! note - The tests included with the Stripe extension have not been updated for 6.x - ### PHPUnit This extension comes with two PHP Unit tests: diff --git a/tests/phpunit/CRM/Stripe/BaseTest.php b/tests/phpunit/CRM/Stripe/BaseTest.php index f39572d..136cbb7 100644 --- a/tests/phpunit/CRM/Stripe/BaseTest.php +++ b/tests/phpunit/CRM/Stripe/BaseTest.php @@ -167,7 +167,6 @@ class CRM_Stripe_BaseTest extends \PHPUnit\Framework\TestCase implements Headles 'payment_processor_id' => $pp['id'], ]; $result = civicrm_api3('StripePaymentintent', 'process', $params); - $paymentIntentID = $result['values']['paymentIntent']['id']; } else { -- GitLab