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' );