diff --git a/modules.php b/modules.php index 6d3f714ef..33a15fc3b 100644 --- a/modules.php +++ b/modules.php @@ -53,6 +53,14 @@ return function ( string $root_dir ): iterable { $modules[] = ( require "$modules_dir/ppcp-saved-payment-checker/module.php" )(); } + if ( apply_filters( + //phpcs:disable WordPress.NamingConventions.ValidHookName.UseUnderscores + 'woocommerce.feature-flags.woocommerce_paypal_payments.card_fields_enabled', + getenv( 'PCP_CARD_FIELDS_ENABLED' ) === '1' + ) ) { + $modules[] = ( require "$modules_dir/ppcp-card-fields/module.php" )(); + } + if ( apply_filters( //phpcs:disable WordPress.NamingConventions.ValidHookName.UseUnderscores 'woocommerce.feature-flags.woocommerce_paypal_payments.save_payment_methods_enabled', diff --git a/modules/ppcp-button/resources/js/button.js b/modules/ppcp-button/resources/js/button.js index 7d3812fc5..4366b6592 100644 --- a/modules/ppcp-button/resources/js/button.js +++ b/modules/ppcp-button/resources/js/button.js @@ -5,7 +5,8 @@ import CheckoutBootstap from './modules/ContextBootstrap/CheckoutBootstap'; import PayNowBootstrap from "./modules/ContextBootstrap/PayNowBootstrap"; import Renderer from './modules/Renderer/Renderer'; import ErrorHandler from './modules/ErrorHandler'; -import CreditCardRenderer from "./modules/Renderer/CreditCardRenderer"; +import HostedFieldsRenderer from "./modules/Renderer/HostedFieldsRenderer"; +import CardFieldsRenderer from "./modules/Renderer/CardFieldsRenderer"; import MessageRenderer from "./modules/Renderer/MessageRenderer"; import Spinner from "./modules/Helper/Spinner"; import { @@ -37,7 +38,11 @@ const bootstrap = () => { document.querySelector(checkoutFormSelector) ?? document.querySelector('.woocommerce-notices-wrapper') ); const spinner = new Spinner(); - const creditCardRenderer = new CreditCardRenderer(PayPalCommerceGateway, errorHandler, spinner); + + let creditCardRenderer = new HostedFieldsRenderer(PayPalCommerceGateway, errorHandler, spinner); + if (typeof paypal.CardFields !== 'undefined') { + creditCardRenderer = new CardFieldsRenderer(PayPalCommerceGateway, errorHandler, spinner); + } const formSaver = new FormSaver( PayPalCommerceGateway.ajax.save_checkout_form.endpoint, diff --git a/modules/ppcp-button/resources/js/modules/Renderer/CardFieldsRenderer.js b/modules/ppcp-button/resources/js/modules/Renderer/CardFieldsRenderer.js new file mode 100644 index 000000000..29514186a --- /dev/null +++ b/modules/ppcp-button/resources/js/modules/Renderer/CardFieldsRenderer.js @@ -0,0 +1,155 @@ +import {show} from "../Helper/Hiding"; + +class CardFieldsRenderer { + + constructor(defaultConfig, errorHandler, spinner) { + this.defaultConfig = defaultConfig; + this.errorHandler = errorHandler; + this.spinner = spinner; + this.cardValid = false; + this.formValid = false; + this.emptyFields = new Set(['number', 'cvv', 'expirationDate']); + this.currentHostedFieldsInstance = null; + } + + render(wrapper, contextConfig) { + if ( + ( + this.defaultConfig.context !== 'checkout' + && this.defaultConfig.context !== 'pay-now' + ) + || wrapper === null + || document.querySelector(wrapper) === null + ) { + return; + } + + const buttonSelector = wrapper + ' button'; + + const gateWayBox = document.querySelector('.payment_box.payment_method_ppcp-credit-card-gateway'); + if (!gateWayBox) { + return + } + + const oldDisplayStyle = gateWayBox.style.display; + gateWayBox.style.display = 'block'; + + const hideDccGateway = document.querySelector('#ppcp-hide-dcc'); + if (hideDccGateway) { + hideDccGateway.parentNode.removeChild(hideDccGateway); + } + + const cardField = paypal.CardFields({ + createOrder: contextConfig.createOrder, + onApprove: function (data) { + return contextConfig.onApprove(data); + }, + onError: function (error) { + console.error(error) + this.spinner.unblock(); + } + }); + + if (cardField.isEligible()) { + const nameField = document.getElementById('ppcp-credit-card-gateway-card-name'); + if (nameField) { + let styles = this.cardFieldStyles(nameField); + cardField.NameField({style: {'input': styles}}).render(nameField.parentNode); + nameField.remove(); + } + + const numberField = document.getElementById('ppcp-credit-card-gateway-card-number'); + if (numberField) { + let styles = this.cardFieldStyles(numberField); + cardField.NumberField({style: {'input': styles}}).render(numberField.parentNode); + numberField.remove(); + } + + const expiryField = document.getElementById('ppcp-credit-card-gateway-card-expiry'); + if (expiryField) { + let styles = this.cardFieldStyles(expiryField); + cardField.ExpiryField({style: {'input': styles}}).render(expiryField.parentNode); + expiryField.remove(); + } + + const cvvField = document.getElementById('ppcp-credit-card-gateway-card-cvc'); + if (cvvField) { + let styles = this.cardFieldStyles(cvvField); + cardField.CVVField({style: {'input': styles}}).render(cvvField.parentNode); + cvvField.remove(); + } + + document.dispatchEvent(new CustomEvent("hosted_fields_loaded")); + } + + gateWayBox.style.display = oldDisplayStyle; + + show(buttonSelector); + + document.querySelector(buttonSelector).addEventListener("click", (event) => { + event.preventDefault(); + this.spinner.block(); + this.errorHandler.clear(); + + cardField.submit() + .catch((error) => { + this.spinner.unblock(); + console.error(error) + this.errorHandler.message(this.defaultConfig.hosted_fields.labels.fields_not_valid); + }) + }); + } + + cardFieldStyles(field) { + const allowedProperties = [ + 'appearance', + 'color', + 'direction', + 'font', + 'font-family', + 'font-size', + 'font-size-adjust', + 'font-stretch', + 'font-style', + 'font-variant', + 'font-variant-alternates', + 'font-variant-caps', + 'font-variant-east-asian', + 'font-variant-ligatures', + 'font-variant-numeric', + 'font-weight', + 'letter-spacing', + 'line-height', + 'opacity', + 'outline', + 'padding', + 'padding-bottom', + 'padding-left', + 'padding-right', + 'padding-top', + 'text-shadow', + 'transition', + '-moz-appearance', + '-moz-osx-font-smoothing', + '-moz-tap-highlight-color', + '-moz-transition', + '-webkit-appearance', + '-webkit-osx-font-smoothing', + '-webkit-tap-highlight-color', + '-webkit-transition', + ]; + + const stylesRaw = window.getComputedStyle(field); + const styles = {}; + Object.values(stylesRaw).forEach((prop) => { + if (!stylesRaw[prop] || !allowedProperties.includes(prop)) { + return; + } + styles[prop] = '' + stylesRaw[prop]; + }); + + return styles; + } +} + +export default CardFieldsRenderer; diff --git a/modules/ppcp-button/resources/js/modules/Renderer/CreditCardRenderer.js b/modules/ppcp-button/resources/js/modules/Renderer/CreditCardRenderer.js deleted file mode 100644 index 9cd6e345c..000000000 --- a/modules/ppcp-button/resources/js/modules/Renderer/CreditCardRenderer.js +++ /dev/null @@ -1,275 +0,0 @@ -import dccInputFactory from "../Helper/DccInputFactory"; -import {show} from "../Helper/Hiding"; -import Product from "../Entity/Product"; - -class CreditCardRenderer { - - constructor(defaultConfig, errorHandler, spinner) { - this.defaultConfig = defaultConfig; - this.errorHandler = errorHandler; - this.spinner = spinner; - this.cardValid = false; - this.formValid = false; - this.emptyFields = new Set(['number', 'cvv', 'expirationDate']); - this.currentHostedFieldsInstance = null; - } - - render(wrapper, contextConfig) { - if ( - ( - this.defaultConfig.context !== 'checkout' - && this.defaultConfig.context !== 'pay-now' - ) - || wrapper === null - || document.querySelector(wrapper) === null - ) { - return; - } - if ( - typeof paypal.HostedFields === 'undefined' - || ! paypal.HostedFields.isEligible() - ) { - const wrapperElement = document.querySelector(wrapper); - wrapperElement.parentNode.removeChild(wrapperElement); - return; - } - - const buttonSelector = wrapper + ' button'; - - if (this.currentHostedFieldsInstance) { - this.currentHostedFieldsInstance.teardown() - .catch(err => console.error(`Hosted fields teardown error: ${err}`)); - this.currentHostedFieldsInstance = null; - } - - const gateWayBox = document.querySelector('.payment_box.payment_method_ppcp-credit-card-gateway'); - if(! gateWayBox) { - return - } - const oldDisplayStyle = gateWayBox.style.display; - gateWayBox.style.display = 'block'; - - const hideDccGateway = document.querySelector('#ppcp-hide-dcc'); - if (hideDccGateway) { - hideDccGateway.parentNode.removeChild(hideDccGateway); - } - - const cardNumberField = document.querySelector('#ppcp-credit-card-gateway-card-number'); - - const stylesRaw = window.getComputedStyle(cardNumberField); - let styles = {}; - Object.values(stylesRaw).forEach( (prop) => { - if (! stylesRaw[prop]) { - return; - } - styles[prop] = '' + stylesRaw[prop]; - }); - - const cardNumber = dccInputFactory(cardNumberField); - cardNumberField.parentNode.replaceChild(cardNumber, cardNumberField); - - const cardExpiryField = document.querySelector('#ppcp-credit-card-gateway-card-expiry'); - const cardExpiry = dccInputFactory(cardExpiryField); - cardExpiryField.parentNode.replaceChild(cardExpiry, cardExpiryField); - - const cardCodeField = document.querySelector('#ppcp-credit-card-gateway-card-cvc'); - const cardCode = dccInputFactory(cardCodeField); - cardCodeField.parentNode.replaceChild(cardCode, cardCodeField); - - gateWayBox.style.display = oldDisplayStyle; - - const formWrapper = '.payment_box payment_method_ppcp-credit-card-gateway'; - if ( - this.defaultConfig.enforce_vault - && document.querySelector(formWrapper + ' .ppcp-credit-card-vault') - ) { - document.querySelector(formWrapper + ' .ppcp-credit-card-vault').checked = true; - document.querySelector(formWrapper + ' .ppcp-credit-card-vault').setAttribute('disabled', true); - } - paypal.HostedFields.render({ - createOrder: contextConfig.createOrder, - styles: { - 'input': styles - }, - fields: { - number: { - selector: '#ppcp-credit-card-gateway-card-number', - placeholder: this.defaultConfig.hosted_fields.labels.credit_card_number, - }, - cvv: { - selector: '#ppcp-credit-card-gateway-card-cvc', - placeholder: this.defaultConfig.hosted_fields.labels.cvv, - }, - expirationDate: { - selector: '#ppcp-credit-card-gateway-card-expiry', - placeholder: this.defaultConfig.hosted_fields.labels.mm_yy, - } - } - }).then(hostedFields => { - document.dispatchEvent(new CustomEvent("hosted_fields_loaded")); - this.currentHostedFieldsInstance = hostedFields; - - hostedFields.on('inputSubmitRequest', () => { - this._submit(contextConfig); - }); - hostedFields.on('cardTypeChange', (event) => { - if ( ! event.cards.length ) { - this.cardValid = false; - return; - } - const validCards = this.defaultConfig.hosted_fields.valid_cards; - this.cardValid = validCards.indexOf(event.cards[0].type) !== -1; - - const className = this._cardNumberFiledCLassNameByCardType(event.cards[0].type); - this._recreateElementClassAttribute(cardNumber, cardNumberField.className); - if (event.cards.length === 1) { - cardNumber.classList.add(className); - } - }) - hostedFields.on('validityChange', (event) => { - this.formValid = Object.keys(event.fields).every(function (key) { - return event.fields[key].isValid; - }); - }); - hostedFields.on('empty', (event) => { - this._recreateElementClassAttribute(cardNumber, cardNumberField.className); - this.emptyFields.add(event.emittedBy); - }); - hostedFields.on('notEmpty', (event) => { - this.emptyFields.delete(event.emittedBy); - }); - - show(buttonSelector); - - if (document.querySelector(wrapper).getAttribute('data-ppcp-subscribed') !== true) { - document.querySelector(buttonSelector).addEventListener( - 'click', - event => { - event.preventDefault(); - this._submit(contextConfig); - } - ); - - document.querySelector(wrapper).setAttribute('data-ppcp-subscribed', true); - } - }); - - document.querySelector('#payment_method_ppcp-credit-card-gateway').addEventListener( - 'click', - () => { - document.querySelector('label[for=ppcp-credit-card-gateway-card-number]').click(); - } - ) - } - - disableFields() { - if (this.currentHostedFieldsInstance) { - this.currentHostedFieldsInstance.setAttribute({ - field: 'number', - attribute: 'disabled' - }) - this.currentHostedFieldsInstance.setAttribute({ - field: 'cvv', - attribute: 'disabled' - }) - this.currentHostedFieldsInstance.setAttribute({ - field: 'expirationDate', - attribute: 'disabled' - }) - } - } - - enableFields() { - if (this.currentHostedFieldsInstance) { - this.currentHostedFieldsInstance.removeAttribute({ - field: 'number', - attribute: 'disabled' - }) - this.currentHostedFieldsInstance.removeAttribute({ - field: 'cvv', - attribute: 'disabled' - }) - this.currentHostedFieldsInstance.removeAttribute({ - field: 'expirationDate', - attribute: 'disabled' - }) - } - } - - _submit(contextConfig) { - this.spinner.block(); - this.errorHandler.clear(); - - if (this.formValid && this.cardValid) { - const save_card = this.defaultConfig.can_save_vault_token ? true : false; - let vault = document.getElementById('ppcp-credit-card-vault') ? - document.getElementById('ppcp-credit-card-vault').checked : save_card; - if (this.defaultConfig.enforce_vault) { - vault = true; - } - const contingency = this.defaultConfig.hosted_fields.contingency; - const hostedFieldsData = { - vault: vault - }; - if (contingency !== 'NO_3D_SECURE') { - hostedFieldsData.contingencies = [contingency]; - } - - if (this.defaultConfig.payer) { - hostedFieldsData.cardholderName = this.defaultConfig.payer.name.given_name + ' ' + this.defaultConfig.payer.name.surname; - } - if (!hostedFieldsData.cardholderName) { - const firstName = document.getElementById('billing_first_name') ? document.getElementById('billing_first_name').value : ''; - const lastName = document.getElementById('billing_last_name') ? document.getElementById('billing_last_name').value : ''; - - hostedFieldsData.cardholderName = firstName + ' ' + lastName; - } - - this.currentHostedFieldsInstance.submit(hostedFieldsData).then((payload) => { - payload.orderID = payload.orderId; - this.spinner.unblock(); - return contextConfig.onApprove(payload); - }).catch(err => { - this.spinner.unblock(); - this.errorHandler.clear(); - - if (err.data?.details?.length) { - this.errorHandler.message(err.data.details.map(d => `${d.issue} ${d.description}`).join('
')); - } else if (err.details?.length) { - this.errorHandler.message(err.details.map(d => `${d.issue} ${d.description}`).join('
')); - } else if (err.data?.errors?.length > 0) { - this.errorHandler.messages(err.data.errors); - } else if (err.data?.message) { - this.errorHandler.message(err.data.message); - } else if (err.message) { - this.errorHandler.message(err.message); - } else { - this.errorHandler.genericError(); - } - }); - } else { - this.spinner.unblock(); - - let message = this.defaultConfig.labels.error.generic; - if (this.emptyFields.size > 0) { - message = this.defaultConfig.hosted_fields.labels.fields_empty; - } else if (!this.cardValid) { - message = this.defaultConfig.hosted_fields.labels.card_not_supported; - } else if (!this.formValid) { - message = this.defaultConfig.hosted_fields.labels.fields_not_valid; - } - - this.errorHandler.message(message); - } - } - - _cardNumberFiledCLassNameByCardType(cardType) { - return cardType === 'american-express' ? 'amex' : cardType.replace('-', ''); - } - - _recreateElementClassAttribute(element, newClassName) { - element.removeAttribute('class') - element.setAttribute('class', newClassName); - } -} -export default CreditCardRenderer; diff --git a/modules/ppcp-button/resources/js/modules/Renderer/HostedFieldsRenderer.js b/modules/ppcp-button/resources/js/modules/Renderer/HostedFieldsRenderer.js new file mode 100644 index 000000000..7a2c5043e --- /dev/null +++ b/modules/ppcp-button/resources/js/modules/Renderer/HostedFieldsRenderer.js @@ -0,0 +1,274 @@ +import dccInputFactory from "../Helper/DccInputFactory"; +import {show} from "../Helper/Hiding"; + +class HostedFieldsRenderer { + + constructor(defaultConfig, errorHandler, spinner) { + this.defaultConfig = defaultConfig; + this.errorHandler = errorHandler; + this.spinner = spinner; + this.cardValid = false; + this.formValid = false; + this.emptyFields = new Set(['number', 'cvv', 'expirationDate']); + this.currentHostedFieldsInstance = null; + } + + render(wrapper, contextConfig) { + if ( + ( + this.defaultConfig.context !== 'checkout' + && this.defaultConfig.context !== 'pay-now' + ) + || wrapper === null + || document.querySelector(wrapper) === null + ) { + return; + } + + if (typeof paypal.HostedFields !== 'undefined' && paypal.HostedFields.isEligible()) { + const buttonSelector = wrapper + ' button'; + + if (this.currentHostedFieldsInstance) { + this.currentHostedFieldsInstance.teardown() + .catch(err => console.error(`Hosted fields teardown error: ${err}`)); + this.currentHostedFieldsInstance = null; + } + + const gateWayBox = document.querySelector('.payment_box.payment_method_ppcp-credit-card-gateway'); + if (!gateWayBox) { + return + } + const oldDisplayStyle = gateWayBox.style.display; + gateWayBox.style.display = 'block'; + + const hideDccGateway = document.querySelector('#ppcp-hide-dcc'); + if (hideDccGateway) { + hideDccGateway.parentNode.removeChild(hideDccGateway); + } + + const cardNumberField = document.querySelector('#ppcp-credit-card-gateway-card-number'); + + const stylesRaw = window.getComputedStyle(cardNumberField); + let styles = {}; + Object.values(stylesRaw).forEach((prop) => { + if (!stylesRaw[prop]) { + return; + } + styles[prop] = '' + stylesRaw[prop]; + }); + + const cardNumber = dccInputFactory(cardNumberField); + cardNumberField.parentNode.replaceChild(cardNumber, cardNumberField); + + const cardExpiryField = document.querySelector('#ppcp-credit-card-gateway-card-expiry'); + const cardExpiry = dccInputFactory(cardExpiryField); + cardExpiryField.parentNode.replaceChild(cardExpiry, cardExpiryField); + + const cardCodeField = document.querySelector('#ppcp-credit-card-gateway-card-cvc'); + const cardCode = dccInputFactory(cardCodeField); + cardCodeField.parentNode.replaceChild(cardCode, cardCodeField); + + gateWayBox.style.display = oldDisplayStyle; + + const formWrapper = '.payment_box payment_method_ppcp-credit-card-gateway'; + if ( + this.defaultConfig.enforce_vault + && document.querySelector(formWrapper + ' .ppcp-credit-card-vault') + ) { + document.querySelector(formWrapper + ' .ppcp-credit-card-vault').checked = true; + document.querySelector(formWrapper + ' .ppcp-credit-card-vault').setAttribute('disabled', true); + } + paypal.HostedFields.render({ + createOrder: contextConfig.createOrder, + styles: { + 'input': styles + }, + fields: { + number: { + selector: '#ppcp-credit-card-gateway-card-number', + placeholder: this.defaultConfig.hosted_fields.labels.credit_card_number, + }, + cvv: { + selector: '#ppcp-credit-card-gateway-card-cvc', + placeholder: this.defaultConfig.hosted_fields.labels.cvv, + }, + expirationDate: { + selector: '#ppcp-credit-card-gateway-card-expiry', + placeholder: this.defaultConfig.hosted_fields.labels.mm_yy, + } + } + }).then(hostedFields => { + document.dispatchEvent(new CustomEvent("hosted_fields_loaded")); + this.currentHostedFieldsInstance = hostedFields; + + hostedFields.on('inputSubmitRequest', () => { + this._submit(contextConfig); + }); + hostedFields.on('cardTypeChange', (event) => { + if (!event.cards.length) { + this.cardValid = false; + return; + } + const validCards = this.defaultConfig.hosted_fields.valid_cards; + this.cardValid = validCards.indexOf(event.cards[0].type) !== -1; + + const className = this._cardNumberFiledCLassNameByCardType(event.cards[0].type); + this._recreateElementClassAttribute(cardNumber, cardNumberField.className); + if (event.cards.length === 1) { + cardNumber.classList.add(className); + } + }) + hostedFields.on('validityChange', (event) => { + this.formValid = Object.keys(event.fields).every(function (key) { + return event.fields[key].isValid; + }); + }); + hostedFields.on('empty', (event) => { + this._recreateElementClassAttribute(cardNumber, cardNumberField.className); + this.emptyFields.add(event.emittedBy); + }); + hostedFields.on('notEmpty', (event) => { + this.emptyFields.delete(event.emittedBy); + }); + + show(buttonSelector); + + if (document.querySelector(wrapper).getAttribute('data-ppcp-subscribed') !== true) { + document.querySelector(buttonSelector).addEventListener( + 'click', + event => { + event.preventDefault(); + this._submit(contextConfig); + } + ); + + document.querySelector(wrapper).setAttribute('data-ppcp-subscribed', true); + } + }); + + document.querySelector('#payment_method_ppcp-credit-card-gateway').addEventListener( + 'click', + () => { + document.querySelector('label[for=ppcp-credit-card-gateway-card-number]').click(); + } + ) + + return; + } + + const wrapperElement = document.querySelector(wrapper); + wrapperElement.parentNode.removeChild(wrapperElement); + } + + disableFields() { + if (this.currentHostedFieldsInstance) { + this.currentHostedFieldsInstance.setAttribute({ + field: 'number', + attribute: 'disabled' + }) + this.currentHostedFieldsInstance.setAttribute({ + field: 'cvv', + attribute: 'disabled' + }) + this.currentHostedFieldsInstance.setAttribute({ + field: 'expirationDate', + attribute: 'disabled' + }) + } + } + + enableFields() { + if (this.currentHostedFieldsInstance) { + this.currentHostedFieldsInstance.removeAttribute({ + field: 'number', + attribute: 'disabled' + }) + this.currentHostedFieldsInstance.removeAttribute({ + field: 'cvv', + attribute: 'disabled' + }) + this.currentHostedFieldsInstance.removeAttribute({ + field: 'expirationDate', + attribute: 'disabled' + }) + } + } + + _submit(contextConfig) { + this.spinner.block(); + this.errorHandler.clear(); + + if (this.formValid && this.cardValid) { + const save_card = this.defaultConfig.can_save_vault_token ? true : false; + let vault = document.getElementById('ppcp-credit-card-vault') ? + document.getElementById('ppcp-credit-card-vault').checked : save_card; + if (this.defaultConfig.enforce_vault) { + vault = true; + } + const contingency = this.defaultConfig.hosted_fields.contingency; + const hostedFieldsData = { + vault: vault + }; + if (contingency !== 'NO_3D_SECURE') { + hostedFieldsData.contingencies = [contingency]; + } + + if (this.defaultConfig.payer) { + hostedFieldsData.cardholderName = this.defaultConfig.payer.name.given_name + ' ' + this.defaultConfig.payer.name.surname; + } + if (!hostedFieldsData.cardholderName) { + const firstName = document.getElementById('billing_first_name') ? document.getElementById('billing_first_name').value : ''; + const lastName = document.getElementById('billing_last_name') ? document.getElementById('billing_last_name').value : ''; + + hostedFieldsData.cardholderName = firstName + ' ' + lastName; + } + + this.currentHostedFieldsInstance.submit(hostedFieldsData).then((payload) => { + payload.orderID = payload.orderId; + this.spinner.unblock(); + return contextConfig.onApprove(payload); + }).catch(err => { + this.spinner.unblock(); + this.errorHandler.clear(); + + if (err.data?.details?.length) { + this.errorHandler.message(err.data.details.map(d => `${d.issue} ${d.description}`).join('
')); + } else if (err.details?.length) { + this.errorHandler.message(err.details.map(d => `${d.issue} ${d.description}`).join('
')); + } else if (err.data?.errors?.length > 0) { + this.errorHandler.messages(err.data.errors); + } else if (err.data?.message) { + this.errorHandler.message(err.data.message); + } else if (err.message) { + this.errorHandler.message(err.message); + } else { + this.errorHandler.genericError(); + } + }); + } else { + this.spinner.unblock(); + + let message = this.defaultConfig.labels.error.generic; + if (this.emptyFields.size > 0) { + message = this.defaultConfig.hosted_fields.labels.fields_empty; + } else if (!this.cardValid) { + message = this.defaultConfig.hosted_fields.labels.card_not_supported; + } else if (!this.formValid) { + message = this.defaultConfig.hosted_fields.labels.fields_not_valid; + } + + this.errorHandler.message(message); + } + } + + _cardNumberFiledCLassNameByCardType(cardType) { + return cardType === 'american-express' ? 'amex' : cardType.replace('-', ''); + } + + _recreateElementClassAttribute(element, newClassName) { + element.removeAttribute('class') + element.setAttribute('class', newClassName); + } +} + +export default HostedFieldsRenderer; diff --git a/modules/ppcp-card-fields/composer.json b/modules/ppcp-card-fields/composer.json new file mode 100644 index 000000000..ad57a2296 --- /dev/null +++ b/modules/ppcp-card-fields/composer.json @@ -0,0 +1,17 @@ +{ + "name": "woocommerce/ppcp-card-fields", + "type": "dhii-mod", + "description": "Advanced Checkout Card Fields module", + "license": "GPL-2.0", + "require": { + "php": "^7.2 | ^8.0", + "dhii/module-interface": "^0.3.0-alpha1" + }, + "autoload": { + "psr-4": { + "WooCommerce\\PayPalCommerce\\CardFields\\": "src" + } + }, + "minimum-stability": "dev", + "prefer-stable": true +} diff --git a/modules/ppcp-card-fields/extensions.php b/modules/ppcp-card-fields/extensions.php new file mode 100644 index 000000000..f11d8f633 --- /dev/null +++ b/modules/ppcp-card-fields/extensions.php @@ -0,0 +1,12 @@ + static function ( ContainerInterface $container ): bool { + $save_payment_methods_applies = $container->get( 'card-fields.helpers.save-payment-methods-applies' ); + assert( $save_payment_methods_applies instanceof CardFieldsApplies ); + + return $save_payment_methods_applies->for_country_currency(); + }, + 'card-fields.helpers.save-payment-methods-applies' => static function ( ContainerInterface $container ) : CardFieldsApplies { + return new CardFieldsApplies( + $container->get( 'card-fields.supported-country-currency-matrix' ), + $container->get( 'api.shop.currency' ), + $container->get( 'api.shop.country' ) + ); + }, + 'card-fields.supported-country-currency-matrix' => static function ( ContainerInterface $container ) : array { + return apply_filters( + 'woocommerce_paypal_payments_card_fields_supported_country_currency_matrix', + array( + 'US' => array( + 'AUD', + 'CAD', + 'EUR', + 'GBP', + 'JPY', + 'USD', + ), + ) + ); + }, +); diff --git a/modules/ppcp-card-fields/src/CardFieldsModule.php b/modules/ppcp-card-fields/src/CardFieldsModule.php new file mode 100644 index 000000000..4471485aa --- /dev/null +++ b/modules/ppcp-card-fields/src/CardFieldsModule.php @@ -0,0 +1,115 @@ +get( 'card-fields.eligible' ) ) { + return; + } + + /** + * Param types removed to avoid third-party issues. + * + * @psalm-suppress MissingClosureParamType + */ + add_filter( + 'woocommerce_paypal_payments_sdk_components_hook', + function( $components ) { + if ( in_array( 'hosted-fields', $components, true ) ) { + $key = array_search( 'hosted-fields', $components, true ); + if ( $key !== false ) { + unset( $components[ $key ] ); + } + } + $components[] = 'card-fields'; + + return $components; + } + ); + + add_filter( + 'woocommerce_credit_card_form_fields', + /** + * Return/Param types removed to avoid third-party issues. + * + * @psalm-suppress MissingClosureReturnType + * @psalm-suppress MissingClosureParamType + */ + function( $default_fields, $id ) { + if ( CreditCardGateway::ID === $id && apply_filters( 'woocommerce_paypal_payments_enable_cardholder_name_field', false ) ) { + $default_fields['card-name-field'] = '

+ + +

'; + + // Moves new item to first position. + $new_field = $default_fields['card-name-field']; + unset( $default_fields['card-name-field'] ); + array_unshift( $default_fields, $new_field ); + } + + return $default_fields; + }, + 10, + 2 + ); + + add_filter( + 'ppcp_create_order_request_body_data', + function( array $data ) use ( $c ): array { + $settings = $c->get( 'wcgateway.settings' ); + assert( $settings instanceof Settings ); + + if ( + $settings->has( '3d_secure_contingency' ) + && ( + $settings->get( '3d_secure_contingency' ) === 'SCA_ALWAYS' + || $settings->get( '3d_secure_contingency' ) === 'SCA_WHEN_REQUIRED' + ) + ) { + $data['payment_source']['card'] = array( + 'attributes' => array( + 'verification' => array( + 'method' => $settings->get( '3d_secure_contingency' ), + ), + ), + ); + } + + return $data; + } + ); + } +} diff --git a/modules/ppcp-card-fields/src/Helper/CardFieldsApplies.php b/modules/ppcp-card-fields/src/Helper/CardFieldsApplies.php new file mode 100644 index 000000000..c5111b9bb --- /dev/null +++ b/modules/ppcp-card-fields/src/Helper/CardFieldsApplies.php @@ -0,0 +1,66 @@ +allowed_country_currency_matrix = $allowed_country_currency_matrix; + $this->currency = $currency; + $this->country = $country; + } + + /** + * Returns whether Card Fields can be used in the current country and the current currency. + * + * @return bool + */ + public function for_country_currency(): bool { + if ( ! in_array( $this->country, array_keys( $this->allowed_country_currency_matrix ), true ) ) { + return false; + } + return in_array( $this->currency, $this->allowed_country_currency_matrix[ $this->country ], true ); + } +} diff --git a/modules/ppcp-compat/src/CompatModule.php b/modules/ppcp-compat/src/CompatModule.php index edb4f0e67..44cfd1988 100644 --- a/modules/ppcp-compat/src/CompatModule.php +++ b/modules/ppcp-compat/src/CompatModule.php @@ -273,7 +273,12 @@ class CompatModule implements ModuleInterface { add_action( 'init', function() { - if ( $this->is_elementor_pro_active() || $this->is_divi_theme_active() ) { + if ( + $this->is_block_theme_active() + || $this->is_elementor_pro_active() + || $this->is_divi_theme_active() + || $this->is_divi_child_theme_active() + ) { add_filter( 'woocommerce_paypal_payments_single_product_renderer_hook', function(): string { @@ -286,6 +291,15 @@ class CompatModule implements ModuleInterface { ); } + /** + * Checks whether the current theme is a blocks theme. + * + * @return bool + */ + protected function is_block_theme_active(): bool { + return function_exists( 'wp_is_block_theme' ) && wp_is_block_theme(); + } + /** * Checks whether the Elementor Pro plugins (allowing integrations with WC) is active. * @@ -304,4 +318,15 @@ class CompatModule implements ModuleInterface { $theme = wp_get_theme(); return $theme->get( 'Name' ) === 'Divi'; } + + /** + * Checks whether a Divi child theme is currently used. + * + * @return bool + */ + protected function is_divi_child_theme_active(): bool { + $theme = wp_get_theme(); + $parent = $theme->parent(); + return ( $parent && $parent->get( 'Name' ) === 'Divi' ); + } } diff --git a/modules/ppcp-order-tracking/services.php b/modules/ppcp-order-tracking/services.php index 37154cd65..8b52fa2fe 100644 --- a/modules/ppcp-order-tracking/services.php +++ b/modules/ppcp-order-tracking/services.php @@ -21,16 +21,16 @@ use WooCommerce\PayPalCommerce\OrderTracking\Assets\OrderEditPageAssets; use WooCommerce\PayPalCommerce\OrderTracking\Endpoint\OrderTrackingEndpoint; return array( - 'order-tracking.assets' => function( ContainerInterface $container ) : OrderEditPageAssets { + 'order-tracking.assets' => function( ContainerInterface $container ) : OrderEditPageAssets { return new OrderEditPageAssets( $container->get( 'order-tracking.module.url' ), $container->get( 'ppcp.asset-version' ) ); }, - 'order-tracking.shipment.factory' => static function ( ContainerInterface $container ) : ShipmentFactoryInterface { + 'order-tracking.shipment.factory' => static function ( ContainerInterface $container ) : ShipmentFactoryInterface { return new ShipmentFactory(); }, - 'order-tracking.endpoint.controller' => static function ( ContainerInterface $container ) : OrderTrackingEndpoint { + 'order-tracking.endpoint.controller' => static function ( ContainerInterface $container ) : OrderTrackingEndpoint { return new OrderTrackingEndpoint( $container->get( 'api.host' ), $container->get( 'api.bearer' ), @@ -38,10 +38,10 @@ return array( $container->get( 'button.request-data' ), $container->get( 'order-tracking.shipment.factory' ), $container->get( 'order-tracking.allowed-shipping-statuses' ), - $container->get( 'order-tracking.is-merchant-country-us' ) + $container->get( 'order-tracking.should-use-second-version-of-api' ) ); }, - 'order-tracking.module.url' => static function ( ContainerInterface $container ): string { + 'order-tracking.module.url' => static function ( ContainerInterface $container ): string { /** * The path cannot be false. * @@ -52,15 +52,15 @@ return array( dirname( realpath( __FILE__ ), 3 ) . '/woocommerce-paypal-payments.php' ); }, - 'order-tracking.meta-box.renderer' => static function ( ContainerInterface $container ): MetaBoxRenderer { + 'order-tracking.meta-box.renderer' => static function ( ContainerInterface $container ): MetaBoxRenderer { return new MetaBoxRenderer( $container->get( 'order-tracking.allowed-shipping-statuses' ), $container->get( 'order-tracking.available-carriers' ), $container->get( 'order-tracking.endpoint.controller' ), - $container->get( 'order-tracking.is-merchant-country-us' ) + $container->get( 'order-tracking.should-use-second-version-of-api' ) ); }, - 'order-tracking.allowed-shipping-statuses' => static function ( ContainerInterface $container ): array { + 'order-tracking.allowed-shipping-statuses' => static function ( ContainerInterface $container ): array { return (array) apply_filters( 'woocommerce_paypal_payments_tracking_statuses', array( @@ -71,10 +71,10 @@ return array( ) ); }, - 'order-tracking.allowed-carriers' => static function ( ContainerInterface $container ): array { + 'order-tracking.allowed-carriers' => static function ( ContainerInterface $container ): array { return require __DIR__ . '/carriers.php'; }, - 'order-tracking.available-carriers' => static function ( ContainerInterface $container ): array { + 'order-tracking.available-carriers' => static function ( ContainerInterface $container ): array { $api_shop_country = $container->get( 'api.shop.country' ); $allowed_carriers = $container->get( 'order-tracking.allowed-carriers' ); $selected_country_carriers = $allowed_carriers[ $api_shop_country ] ?? array(); @@ -90,10 +90,26 @@ return array( ), ); }, - 'order-tracking.is-merchant-country-us' => static function ( ContainerInterface $container ): bool { - return $container->get( 'api.shop.country' ) === 'US'; + + /** + * The list of country codes, for which the 2nd version of PayPal tracking API is supported. + */ + 'order-tracking.second-version-api-supported-countries' => static function (): array { + /** + * Returns codes of countries, for which the 2nd version of PayPal tracking API is supported. + */ + return apply_filters( + 'woocommerce_paypal_payments_supported_country_codes_for_second_version_of_tracking_api', + array( 'US', 'AU', 'CA', 'FR', 'DE', 'IT', 'ES' ) + ); }, - 'order-tracking.integrations' => static function ( ContainerInterface $container ): array { + 'order-tracking.should-use-second-version-of-api' => static function ( ContainerInterface $container ): bool { + $supported_county_codes = $container->get( 'order-tracking.second-version-api-supported-countries' ); + $selected_country_code = $container->get( 'api.shop.country' ); + + return in_array( $selected_country_code, $supported_county_codes, true ); + }, + 'order-tracking.integrations' => static function ( ContainerInterface $container ): array { $shipment_factory = $container->get( 'order-tracking.shipment.factory' ); $logger = $container->get( 'woocommerce.logger.woocommerce' ); $endpoint = $container->get( 'order-tracking.endpoint.controller' );