diff --git a/CRM/Core/Payment/OmnipayMultiProcessor.php b/CRM/Core/Payment/OmnipayMultiProcessor.php index 8db2f5710..0afa3a19a 100644 --- a/CRM/Core/Payment/OmnipayMultiProcessor.php +++ b/CRM/Core/Payment/OmnipayMultiProcessor.php @@ -162,6 +162,12 @@ public function doPayment(&$params, $component = 'contribute') { $this->initialize($params); $this->saveBillingAddressIfRequired($params); + // "token" only gets set when coming in via a contribution page. + // Otherwise we need to set it from the actual parameter that's set on the form + if (!empty($params['paymentToken']) && empty($params['token'])) { + $params['token'] = $params['paymentToken']; + } + try { if (!empty($params['token'])) { $response = $this->doTokenPayment($params); @@ -325,7 +331,13 @@ public function buildForm(&$form) { $jsVariables[$clientSideKey] = $this->_paymentProcessor[$key]; } } - CRM_Core_Resources::singleton()->addVars('omnipay', $jsVariables); + + \Civi::resources()->addVars('omnipay', $jsVariables); + // Assign to smarty so we can add via tpl for drupal webform / default processor on contribution page because addVars doesn't work in that context + // until https://github.com/civicrm/civicrm-core/pull/18141 is merged + // Then we can set 'billing-block' above and remove the assign/tpl + $form->assign('omnipayJSVars', $jsVariables); + if (is_array($regions)) { foreach ($regions as $region => $additions) { foreach ($additions as $addition) { diff --git a/CRM/Omnipaymultiprocessor/Check.php b/CRM/Omnipaymultiprocessor/Check.php new file mode 100644 index 000000000..0957703f1 --- /dev/null +++ b/CRM/Omnipaymultiprocessor/Check.php @@ -0,0 +1,124 @@ +messages = $messages; + } + + public function checkRequirements() { + $this->checkExtensionMjwshared(); + return $this->messages; + } + + /** + * @param string $extensionName + * @param string $minVersion + * @param string $actualVersion + */ + private function requireExtensionMinVersion($extensionName, $minVersion, $actualVersion) { + $actualVersionModified = $actualVersion; + if (substr($actualVersion, -4) === '-dev') { + $message = new CRM_Utils_Check_Message( + __FUNCTION__ . $extensionName . E::SHORT_NAME . '_requirements_dev', + E::ts('You are using a development version of %1 extension.', + [1 => $extensionName]), + E::ts('%1: Development version', [1 => $extensionName]), + \Psr\Log\LogLevel::WARNING, + 'fa-code' + ); + $this->messages[] = $message; + $actualVersionModified = substr($actualVersion, 0, -4); + } + + if (version_compare($actualVersionModified, $minVersion) === -1) { + $message = new CRM_Utils_Check_Message( + __FUNCTION__ . $extensionName . E::SHORT_NAME . '_requirements', + E::ts('The %1 extension requires the %2 extension version %3 or greater but your system has version %4.', + [ + 1 => ucfirst(E::SHORT_NAME), + 2 => $extensionName, + 3 => $minVersion, + 4 => $actualVersion + ]), + E::ts('%1: Missing Requirements', [1 => ucfirst(E::SHORT_NAME)]), + \Psr\Log\LogLevel::ERROR, + 'fa-exclamation-triangle' + ); + $message->addAction( + E::ts('Upgrade now'), + NULL, + 'href', + ['path' => 'civicrm/admin/extensions', 'query' => ['action' => 'update', 'id' => $extensionName, 'key' => $extensionName]] + ); + $this->messages[] = $message; + } + } + + /** + * @throws \CiviCRM_API3_Exception + */ + private function checkExtensionMjwshared() { + // mjwshared: required. Requires min version + $extensionName = 'mjwshared'; + $extensions = civicrm_api3('Extension', 'get', [ + 'full_name' => $extensionName, + ]); + + if (empty($extensions['count']) || ($extensions['values'][$extensions['id']]['status'] !== 'installed')) { + $message = new CRM_Utils_Check_Message( + __FUNCTION__ . E::SHORT_NAME . '_requirements', + E::ts('The %1 extension requires the Payment Shared extension which is not installed. See details for more information.', + [ + 1 => ucfirst(E::SHORT_NAME), + 2 => 'https://civicrm.org/extensions/mjwshared', + ] + ), + E::ts('%1: Missing Requirements', [1 => ucfirst(E::SHORT_NAME)]), + \Psr\Log\LogLevel::ERROR, + 'fa-money' + ); + $message->addAction( + E::ts('Install now'), + NULL, + 'href', + ['path' => 'civicrm/admin/extensions', 'query' => ['action' => 'update', 'id' => $extensionName, 'key' => $extensionName]] + ); + $this->messages[] = $message; + return; + } + if (isset($extensions['id']) && $extensions['values'][$extensions['id']]['status'] === 'installed') { + $this->requireExtensionMinVersion($extensionName, self::MIN_VERSION_MJWSHARED, $extensions['values'][$extensions['id']]['version']); + } + } + +} diff --git a/Metadata/js/omnipay_PaypalRest.js b/Metadata/js/omnipay_PaypalRest.js index 53dd9670b..7c1a93e7e 100644 --- a/Metadata/js/omnipay_PaypalRest.js +++ b/Metadata/js/omnipay_PaypalRest.js @@ -1,24 +1,62 @@ // @see https://developer.paypal.com/docs/checkout/integrate/ (function($) { - var form = $('#billing-payment-block').closest('form'); - var qfKey = $('[name=qfKey]', form).val(); + var scriptName = 'omnipayPaypal'; + + if (typeof CRM.vars.omnipay === 'undefined') { + CRM.payment.debugging(scriptName, 'CRM.vars.omnipay not defined! Not a Omnipay processor?'); + return; + } function renderPaypal() { paypal.Buttons({ - onInit: function(data, actions) { + + // On webform, hide the submit button as it's triggered automatically + if (CRM.$('[type="submit"].webform-submit').length !== 0) { + $('[type="submit"].webform-submit').hide(); + } + + $('[type="submit"][formnovalidate="1"]', + '[type="submit"][formnovalidate="formnovalidate"]', + '[type="submit"].cancel', + '[type="submit"].webform-previous' + ).on('click', function() { + CRM.payment.debugging(scriptName, 'adding submitdontprocess: ' + this.id); + CRM.payment.form.dataset.submitdontprocess = 'true'; + }); + + $(CRM.payment.getBillingSubmit()).on('click', function() { + CRM.payment.debugging(scriptName, 'clearing submitdontprocess'); + CRM.payment.form.dataset.submitdontprocess = 'false'; + }); + + $(CRM.payment.form).on('submit', function(event) { + if (CRM.payment.form.dataset.submitdontprocess === 'true') { + CRM.payment.debugging(scriptName, 'non-payment submit detected - not submitting payment'); + event.preventDefault(); + return true; + } + if (document.getElementById('payment_token') && (document.getElementById('payment_token').value !== 'Authorisation token') && + document.getElementById('PayerID') && (document.getElementById('PayerID').value !== 'Payer ID')) { + return true; + } + CRM.payment.debugging(scriptName, 'Unable to submit - paypal not executed'); + event.preventDefault(); + return true; + }); + // Set up the buttons. - if (form.valid()) { - actions.enable() + if ($(CRM.payment.form).valid()) { + actions.enable(); } else { actions.disable(); } - form.on('blur keyup change', 'input', function (event) { - if (form.valid()) { - actions.enable() + $(CRM.payment.form).on('blur keyup change', 'input', function (event) { + if ($(CRM.payment.form).valid()) { + actions.enable(); } else { actions.disable(); @@ -28,23 +66,41 @@ createBillingAgreement: function (data, actions) { + // CRM.payment.getTotalAmount is implemented by webform_civicrm and mjwshared. The plan is to + // add CRM.payment.getTotalAmount() into CiviCRM core. This code allows it to work under any of + // these circumstances as well as if CRM.payment does not exist. + var totalAmount = 0.0; + if ((typeof CRM.payment !== 'undefined') && (CRM.payment.hasOwnProperty('getTotalAmount'))) { + totalAmount = CRM.payment.getTotalAmount(); + } + else if (typeof calculateTotalFee == 'function') { + // This is ONLY triggered in the following circumstances on a CiviCRM contribution page: + // - With a priceset that allows a 0 amount to be selected. + // - When we are the ONLY payment processor configured on the page. + totalAmount = parseFloat(calculateTotalFee()); + } + else if (document.getElementById('total_amount')) { + // The input#total_amount field exists on backend contribution forms + totalAmount = parseFloat(document.getElementById('total_amount').value); + } + var frequencyInterval = $('#frequency_interval').val() || 1; var frequencyUnit = $('#frequency_unit').val() ? $('#frequency_interval').val() : CRM.vars.omnipay.frequency_unit; - var paymentAmount = calculateTotalFee(); var isRecur = $('#is_recur').is(":checked"); var recurText = isRecur ? ' recurring' : ''; + var qfKey = $('[name=qfKey]', $(CRM.payment.form)).val(); return new Promise(function (resolve, reject) { CRM.api3('PaymentProcessor', 'preapprove', { 'payment_processor_id': CRM.vars.omnipay.paymentProcessorId, - 'amount': paymentAmount, + 'amount': totalAmount, 'currencyID' : CRM.vars.omnipay.currency, 'qf_key': qfKey, 'is_recur' : isRecur, 'installments' : $('#installments').val(), 'frequency_unit' : frequencyUnit, 'frequency_interval' : frequencyInterval, - 'description' : CRM.vars.omnipay.title + ' ' + CRM.formatMoney(paymentAmount) + recurText, + 'description' : CRM.vars.omnipay.title + ' ' + CRM.formatMoney(totalAmount) + recurText, } ).then(function (result) { if (result['is_error'] === 1) { @@ -63,21 +119,25 @@ onApprove: function (data, actions) { var isRecur = 1; - var paymentToken = data['billingToken']; + var paymentToken = data.billingToken; if (!paymentToken) { - paymentToken = data['paymentID']; + paymentToken = data.paymentID; isRecur = 0; } document.getElementById('paypal-button-container').style.visibility = "hidden"; - document.getElementById('crm-submit-buttons').style.display = 'block'; - document.getElementById('PayerID').value = data['payerID']; + var crmSubmitButtons = document.getElementById('crm-submit-buttons'); + if (crmSubmitButtons) { + crmSubmitButtons.style.display = 'block'; + } + document.getElementById('PayerID').value = data.payerID; document.getElementById('payment_token').value = paymentToken; - form.submit(); + + CRM.$(CRM.payment.getBillingSubmit()).click(); }, onError: function(err) { - console.log(err); + CRM.payment.debugging(scriptName, err); alert('Site is not correctly configured to process payments'); } diff --git a/Metadata/omnipay_PaypalRest.mgd.php b/Metadata/omnipay_PaypalRest.mgd.php index 4cc26e3bb..c32977c40 100644 --- a/Metadata/omnipay_PaypalRest.mgd.php +++ b/Metadata/omnipay_PaypalRest.mgd.php @@ -53,6 +53,9 @@ * database as appropriate. For more details, see "hook_civicrm_managed" at: * http://wiki.civicrm.org/confluence/display/CRMDOC/Hook+Reference */ + +use CRM_Omnipaymultiprocessor_ExtensionUtil as E; + return [ [ 'name' => 'PayPal Checkout', @@ -71,13 +74,14 @@ 'url_site_test_default' => 'http://unused.com', 'url_recur_test_default' => 'http://unused.com', 'url_api_test_default' => 'http://unused.com', - 'billing_mode' => 4, + 'billing_mode' => 1, 'payment_type' => 1, 'is_recur' => 1, ], 'metadata' => [ 'suppress_submit_button' => 1, 'supports_preapproval' => 1, + 'fields' => ['billing_fields' => []], 'payment_fields' => ['payment_token', 'PayerID', 'post_authorize'], 'pass_through_fields' => [ 'referrerCode' => 'CiviCRM_SP', @@ -114,16 +118,9 @@ 'regions' => [ //'billing-block-post' => [], 'billing-block' => [ - [ - 'markup' => '
', - 'name' => 'paypal_button', - 'weight' => 400, - ], - [ - 'name' => 'paypal_script', - 'weight' => 500, - 'script' => file_get_contents(__DIR__ . '/js/omnipay_PaypalRest.js'), - ], + ['markup' => '
', 'name' => 'paypal_button', 'weight' => 400], + ['template' => E::path('templates/CRM/Omnipaymultiprocessor/Form/OmnipayResource.tpl'), 'region' => 'billing-block', 'weight' => -1], + ['name' => 'paypal_script', 'weight' => 500, 'scriptUrl' => \Civi::resources()->addCacheCode(E::url('Metadata/js/omnipay_PaypalRest.js'))] ], ], ], diff --git a/omnipaymultiprocessor.php b/omnipaymultiprocessor.php index 55af69157..974a87e68 100644 --- a/omnipaymultiprocessor.php +++ b/omnipaymultiprocessor.php @@ -104,7 +104,7 @@ function omnipaymultiprocessor_civicrm_preProcess($formName, &$form) { $form->assign('isJsValidate', TRUE); if (!empty($form->_values['is_recur'])) { $recurOptions = [ - 'is_recur_interval' => $form->_values['is_recur_interval'], + 'is_recur_interval' => $form->_values['is_recur_interval'], 'frequency_unit' => $form->_values['recur_frequency_unit'], 'is_recur_installments' => $form->_values['is_recur_installments'], ]; @@ -112,10 +112,19 @@ function omnipaymultiprocessor_civicrm_preProcess($formName, &$form) { if (!$recurOptions['is_recur_interval']) { $recurOptions['frequency_interval'] = 1; } - CRM_Core_Resources::singleton()->addVars( + CRM_Core_Resources::singleton()->addVars( 'omnipay', $recurOptions - ); + ); } } +} +/** + * Implements hook_civicrm_check(). + * + * @throws \CiviCRM_API3_Exception + */ +function omnipaymultiprocessor_civicrm_check(&$messages) { + $checks = new CRM_Omnipaymultiprocessor_Check($messages); + $messages = $checks->checkRequirements(); } diff --git a/templates/CRM/Omnipaymultiprocessor/Form/OmnipayResource.tpl b/templates/CRM/Omnipaymultiprocessor/Form/OmnipayResource.tpl new file mode 100644 index 000000000..bb2489db8 --- /dev/null +++ b/templates/CRM/Omnipaymultiprocessor/Form/OmnipayResource.tpl @@ -0,0 +1,23 @@ +{* + +--------------------------------------------------------------------+ + | Copyright CiviCRM LLC. All rights reserved. | + | | + | This work is published under the GNU AGPLv3 license with some | + | permitted exceptions and without any warranty. For full license | + | and copyright information, see https://civicrm.org/licensing | + +--------------------------------------------------------------------+ +*} +{* Manually create the CRM.vars.omnipay here for cases where \Civi::resources()->addVars() does not work (eg. drupal webform_civicrm, contribution page paypal checkout is default *} +{literal} + +{/literal}