From 534e41252476c65512992c162719c38353957c87 Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Fri, 12 Jul 2024 17:32:59 +0200 Subject: [PATCH 01/80] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Extract=20preview=20?= =?UTF-8?q?classes=20to=20own=20files?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../js/Preview/ApplePayPreviewButton.js | 61 ++++++++++ .../Preview/ApplePayPreviewButtonManager.js | 49 ++++++++ .../ppcp-applepay/resources/js/boot-admin.js | 110 +----------------- .../js/modules/Renderer/PreviewButton.js | 2 +- .../modules/Renderer/PreviewButtonManager.js | 2 +- .../js/Preview/GooglePayPreviewButton.js | 53 +++++++++ .../Preview/GooglePayPreviewButtonManager.js | 56 +++++++++ .../ppcp-googlepay/resources/js/boot-admin.js | 109 +---------------- 8 files changed, 223 insertions(+), 219 deletions(-) create mode 100644 modules/ppcp-applepay/resources/js/Preview/ApplePayPreviewButton.js create mode 100644 modules/ppcp-applepay/resources/js/Preview/ApplePayPreviewButtonManager.js create mode 100644 modules/ppcp-googlepay/resources/js/Preview/GooglePayPreviewButton.js create mode 100644 modules/ppcp-googlepay/resources/js/Preview/GooglePayPreviewButtonManager.js diff --git a/modules/ppcp-applepay/resources/js/Preview/ApplePayPreviewButton.js b/modules/ppcp-applepay/resources/js/Preview/ApplePayPreviewButton.js new file mode 100644 index 000000000..6e3f721da --- /dev/null +++ b/modules/ppcp-applepay/resources/js/Preview/ApplePayPreviewButton.js @@ -0,0 +1,61 @@ +import ApplepayButton from '../ApplepayButton'; +import PreviewButton from '../../../../ppcp-button/resources/js/modules/Renderer/PreviewButton'; + +/** + * A single Apple Pay preview button instance. + */ +export default class ApplePayPreviewButton extends PreviewButton { + constructor( args ) { + super( args ); + + this.selector = `${ args.selector }ApplePay`; + this.defaultAttributes = { + button: { + type: 'pay', + color: 'black', + lang: 'en', + }, + }; + } + + createNewWrapper() { + const element = super.createNewWrapper(); + element.addClass( 'ppcp-button-apm ppcp-button-applepay' ); + + return element; + } + + createButton( buttonConfig ) { + const button = new ApplepayButton( + 'preview', + null, + buttonConfig, + this.ppcpConfig + ); + + button.init( this.apiConfig ); + } + + /** + * Merge form details into the config object for preview. + * Mutates the previewConfig object; no return value. + * @param buttonConfig + * @param ppcpConfig + */ + dynamicPreviewConfig( buttonConfig, ppcpConfig ) { + // The Apple Pay button expects the "wrapper" to be an ID without `#` prefix! + buttonConfig.button.wrapper = buttonConfig.button.wrapper.replace( + /^#/, + '' + ); + + // Merge the current form-values into the preview-button configuration. + if ( ppcpConfig.button ) { + buttonConfig.button.type = ppcpConfig.button.style.type; + buttonConfig.button.color = ppcpConfig.button.style.color; + buttonConfig.button.lang = + ppcpConfig.button.style?.lang || + ppcpConfig.button.style.language; + } + } +} diff --git a/modules/ppcp-applepay/resources/js/Preview/ApplePayPreviewButtonManager.js b/modules/ppcp-applepay/resources/js/Preview/ApplePayPreviewButtonManager.js new file mode 100644 index 000000000..43ecc37b8 --- /dev/null +++ b/modules/ppcp-applepay/resources/js/Preview/ApplePayPreviewButtonManager.js @@ -0,0 +1,49 @@ +import PreviewButtonManager from '../../../../ppcp-button/resources/js/modules/Renderer/PreviewButtonManager'; +import ApplePayPreviewButton from './ApplePayPreviewButton'; + +/** + * Manages all Apple Pay preview buttons on this page. + */ +export default class ApplePayPreviewButtonManager extends PreviewButtonManager { + constructor() { + const args = { + methodName: 'ApplePay', + buttonConfig: window.wc_ppcp_applepay_admin, + }; + + super( args ); + } + + /** + * Responsible for fetching and returning the PayPal configuration object for this payment + * method. + * + * @param {{}} payPal - The PayPal SDK object provided by WidgetBuilder. + * @return {Promise<{}>} + */ + async fetchConfig( payPal ) { + const apiMethod = payPal?.Applepay()?.config; + + if ( ! apiMethod ) { + this.error( + 'configuration object cannot be retrieved from PayPal' + ); + return {}; + } + + return await apiMethod(); + } + + /** + * This method is responsible for creating a new PreviewButton instance and returning it. + * + * @param {string} wrapperId - CSS ID of the wrapper element. + * @return {ApplePayPreviewButton} + */ + createButtonInstance( wrapperId ) { + return new ApplePayPreviewButton( { + selector: wrapperId, + apiConfig: this.apiConfig, + } ); + } +} diff --git a/modules/ppcp-applepay/resources/js/boot-admin.js b/modules/ppcp-applepay/resources/js/boot-admin.js index 080d7c8aa..e92c03cdf 100644 --- a/modules/ppcp-applepay/resources/js/boot-admin.js +++ b/modules/ppcp-applepay/resources/js/boot-admin.js @@ -1,6 +1,4 @@ -import ApplepayButton from './ApplepayButton'; -import PreviewButton from '../../../ppcp-button/resources/js/modules/Renderer/PreviewButton'; -import PreviewButtonManager from '../../../ppcp-button/resources/js/modules/Renderer/PreviewButtonManager'; +import ApplePayPreviewButtonManager from './Preview/ApplePayPreviewButtonManager'; /** * Accessor that creates and returns a single PreviewButtonManager instance. @@ -14,111 +12,5 @@ const buttonManager = () => { return ApplePayPreviewButtonManager.instance; }; -/** - * Manages all Apple Pay preview buttons on this page. - */ -class ApplePayPreviewButtonManager extends PreviewButtonManager { - constructor() { - const args = { - methodName: 'ApplePay', - buttonConfig: window.wc_ppcp_applepay_admin, - }; - - super( args ); - } - - /** - * Responsible for fetching and returning the PayPal configuration object for this payment - * method. - * - * @param {{}} payPal - The PayPal SDK object provided by WidgetBuilder. - * @return {Promise<{}>} - */ - async fetchConfig( payPal ) { - const apiMethod = payPal?.Applepay()?.config; - - if ( ! apiMethod ) { - this.error( - 'configuration object cannot be retrieved from PayPal' - ); - return {}; - } - - return await apiMethod(); - } - - /** - * This method is responsible for creating a new PreviewButton instance and returning it. - * - * @param {string} wrapperId - CSS ID of the wrapper element. - * @return {ApplePayPreviewButton} - */ - createButtonInstance( wrapperId ) { - return new ApplePayPreviewButton( { - selector: wrapperId, - apiConfig: this.apiConfig, - } ); - } -} - -/** - * A single Apple Pay preview button instance. - */ -class ApplePayPreviewButton extends PreviewButton { - constructor( args ) { - super( args ); - - this.selector = `${ args.selector }ApplePay`; - this.defaultAttributes = { - button: { - type: 'pay', - color: 'black', - lang: 'en', - }, - }; - } - - createNewWrapper() { - const element = super.createNewWrapper(); - element.addClass( 'ppcp-button-applepay' ); - - return element; - } - - createButton( buttonConfig ) { - const button = new ApplepayButton( - 'preview', - null, - buttonConfig, - this.ppcpConfig - ); - - button.init( this.apiConfig ); - } - - /** - * Merge form details into the config object for preview. - * Mutates the previewConfig object; no return value. - * @param buttonConfig - * @param ppcpConfig - */ - dynamicPreviewConfig( buttonConfig, ppcpConfig ) { - // The Apple Pay button expects the "wrapper" to be an ID without `#` prefix! - buttonConfig.button.wrapper = buttonConfig.button.wrapper.replace( - /^#/, - '' - ); - - // Merge the current form-values into the preview-button configuration. - if ( ppcpConfig.button ) { - buttonConfig.button.type = ppcpConfig.button.style.type; - buttonConfig.button.color = ppcpConfig.button.style.color; - buttonConfig.button.lang = - ppcpConfig.button.style?.lang || - ppcpConfig.button.style.language; - } - } -} - // Initialize the preview button manager. buttonManager(); diff --git a/modules/ppcp-button/resources/js/modules/Renderer/PreviewButton.js b/modules/ppcp-button/resources/js/modules/Renderer/PreviewButton.js index cec951927..ac189e89e 100644 --- a/modules/ppcp-button/resources/js/modules/Renderer/PreviewButton.js +++ b/modules/ppcp-button/resources/js/modules/Renderer/PreviewButton.js @@ -30,7 +30,7 @@ class PreviewButton { */ createNewWrapper() { const previewId = this.selector.replace( '#', '' ); - const previewClass = 'ppcp-button-apm'; + const previewClass = 'ppcp-preview-button'; return jQuery( `
` ); } diff --git a/modules/ppcp-button/resources/js/modules/Renderer/PreviewButtonManager.js b/modules/ppcp-button/resources/js/modules/Renderer/PreviewButtonManager.js index 42e715904..9cb9fa81f 100644 --- a/modules/ppcp-button/resources/js/modules/Renderer/PreviewButtonManager.js +++ b/modules/ppcp-button/resources/js/modules/Renderer/PreviewButtonManager.js @@ -95,7 +95,7 @@ class PreviewButtonManager { */ createDummy( wrapperId ) { const elButton = document.createElement( 'div' ); - elButton.classList.add( 'ppcp-button-apm', 'ppcp-button-dummy' ); + elButton.classList.add( 'ppcp-preview-button', 'ppcp-button-dummy' ); elButton.innerHTML = `${ this.apiError ?? 'Not Available' }`; diff --git a/modules/ppcp-googlepay/resources/js/Preview/GooglePayPreviewButton.js b/modules/ppcp-googlepay/resources/js/Preview/GooglePayPreviewButton.js new file mode 100644 index 000000000..7bd3ffa7f --- /dev/null +++ b/modules/ppcp-googlepay/resources/js/Preview/GooglePayPreviewButton.js @@ -0,0 +1,53 @@ +import GooglepayButton from '../GooglepayButton'; +import PreviewButton from '../../../../ppcp-button/resources/js/modules/Renderer/PreviewButton'; + +/** + * A single GooglePay preview button instance. + */ +export default class GooglePayPreviewButton extends PreviewButton { + constructor( args ) { + super( args ); + + this.selector = `${ args.selector }GooglePay`; + this.defaultAttributes = { + button: { + style: { + type: 'pay', + color: 'black', + language: 'en', + }, + }, + }; + } + + createNewWrapper() { + const element = super.createNewWrapper(); + element.addClass( 'ppcp-button-apm ppcp-button-googlepay' ); + + return element; + } + + createButton( buttonConfig ) { + const button = new GooglepayButton( + 'preview', + null, + buttonConfig, + this.ppcpConfig + ); + + button.init( this.apiConfig ); + } + + /** + * Merge form details into the config object for preview. + * Mutates the previewConfig object; no return value. + * @param buttonConfig + * @param ppcpConfig + */ + dynamicPreviewConfig( buttonConfig, ppcpConfig ) { + // Merge the current form-values into the preview-button configuration. + if ( ppcpConfig.button && buttonConfig.button ) { + Object.assign( buttonConfig.button.style, ppcpConfig.button.style ); + } + } +} diff --git a/modules/ppcp-googlepay/resources/js/Preview/GooglePayPreviewButtonManager.js b/modules/ppcp-googlepay/resources/js/Preview/GooglePayPreviewButtonManager.js new file mode 100644 index 000000000..0aaef0396 --- /dev/null +++ b/modules/ppcp-googlepay/resources/js/Preview/GooglePayPreviewButtonManager.js @@ -0,0 +1,56 @@ +import PreviewButtonManager from '../../../../ppcp-button/resources/js/modules/Renderer/PreviewButtonManager'; +import GooglePayPreviewButton from './GooglePayPreviewButton'; + +/** + * Manages all GooglePay preview buttons on this page. + */ +export default class GooglePayPreviewButtonManager extends PreviewButtonManager { + constructor() { + const args = { + methodName: 'GooglePay', + buttonConfig: window.wc_ppcp_googlepay_admin, + }; + + super( args ); + } + + /** + * Responsible for fetching and returning the PayPal configuration object for this payment + * method. + * + * @param {{}} payPal - The PayPal SDK object provided by WidgetBuilder. + * @return {Promise<{}>} + */ + async fetchConfig( payPal ) { + const apiMethod = payPal?.Googlepay()?.config; + + if ( ! apiMethod ) { + this.error( + 'configuration object cannot be retrieved from PayPal' + ); + return {}; + } + + try { + return await apiMethod(); + } catch ( error ) { + if ( error.message.includes( 'Not Eligible' ) ) { + this.apiError = 'Not Eligible'; + } + return null; + } + } + + /** + * This method is responsible for creating a new PreviewButton instance and returning it. + * + * @param {string} wrapperId - CSS ID of the wrapper element. + * @return {GooglePayPreviewButton} + */ + createButtonInstance( wrapperId ) { + return new GooglePayPreviewButton( { + selector: wrapperId, + apiConfig: this.apiConfig, + } ); + } +} diff --git a/modules/ppcp-googlepay/resources/js/boot-admin.js b/modules/ppcp-googlepay/resources/js/boot-admin.js index 41cace0c0..953a6088e 100644 --- a/modules/ppcp-googlepay/resources/js/boot-admin.js +++ b/modules/ppcp-googlepay/resources/js/boot-admin.js @@ -1,6 +1,4 @@ -import GooglepayButton from './GooglepayButton'; -import PreviewButton from '../../../ppcp-button/resources/js/modules/Renderer/PreviewButton'; -import PreviewButtonManager from '../../../ppcp-button/resources/js/modules/Renderer/PreviewButtonManager'; +import GooglePayPreviewButtonManager from './Preview/GooglePayPreviewButtonManager'; /** * Accessor that creates and returns a single PreviewButtonManager instance. @@ -14,110 +12,5 @@ const buttonManager = () => { return GooglePayPreviewButtonManager.instance; }; -/** - * Manages all GooglePay preview buttons on this page. - */ -class GooglePayPreviewButtonManager extends PreviewButtonManager { - constructor() { - const args = { - methodName: 'GooglePay', - buttonConfig: window.wc_ppcp_googlepay_admin, - }; - - super( args ); - } - - /** - * Responsible for fetching and returning the PayPal configuration object for this payment - * method. - * - * @param {{}} payPal - The PayPal SDK object provided by WidgetBuilder. - * @return {Promise<{}>} - */ - async fetchConfig( payPal ) { - const apiMethod = payPal?.Googlepay()?.config; - - if ( ! apiMethod ) { - this.error( - 'configuration object cannot be retrieved from PayPal' - ); - return {}; - } - - try { - return await apiMethod(); - } catch ( error ) { - if ( error.message.includes( 'Not Eligible' ) ) { - this.apiError = 'Not Eligible'; - } - return null; - } - } - - /** - * This method is responsible for creating a new PreviewButton instance and returning it. - * - * @param {string} wrapperId - CSS ID of the wrapper element. - * @return {GooglePayPreviewButton} - */ - createButtonInstance( wrapperId ) { - return new GooglePayPreviewButton( { - selector: wrapperId, - apiConfig: this.apiConfig, - } ); - } -} - -/** - * A single GooglePay preview button instance. - */ -class GooglePayPreviewButton extends PreviewButton { - constructor( args ) { - super( args ); - - this.selector = `${ args.selector }GooglePay`; - this.defaultAttributes = { - button: { - style: { - type: 'pay', - color: 'black', - language: 'en', - }, - }, - }; - } - - createNewWrapper() { - const element = super.createNewWrapper(); - element.addClass( 'ppcp-button-googlepay' ); - - return element; - } - - createButton( buttonConfig ) { - const button = new GooglepayButton( - 'preview', - null, - buttonConfig, - this.ppcpConfig - ); - - button.init( this.apiConfig ); - } - - /** - * Merge form details into the config object for preview. - * Mutates the previewConfig object; no return value. - * @param buttonConfig - * @param ppcpConfig - */ - dynamicPreviewConfig( buttonConfig, ppcpConfig ) { - // Merge the current form-values into the preview-button configuration. - if ( ppcpConfig.button && buttonConfig.button ) { - Object.assign( buttonConfig.button.style, ppcpConfig.button.style ); - } - } -} - // Initialize the preview button manager. buttonManager(); From 2da8b516ff37f6f9e2999e8cee617a1f1befd513 Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Fri, 12 Jul 2024 19:02:21 +0200 Subject: [PATCH 02/80] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Code=20organization?= =?UTF-8?q?=20and=20cleanup?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Move base classes for the preview buttons into “Preview” folder - Remove jQuery use inside those base classes - Extract dummy button to own class --- .../js/Preview/ApplePayPreviewButton.js | 8 ++-- .../Preview/ApplePayPreviewButtonManager.js | 2 +- .../js/modules/Preview/DummyPreviewButton.js | 31 +++++++++++++++ .../{Renderer => Preview}/PreviewButton.js | 39 ++++++++++++++++--- .../PreviewButtonManager.js | 34 ++++++---------- .../js/Preview/GooglePayPreviewButton.js | 8 ++-- .../Preview/GooglePayPreviewButtonManager.js | 2 +- 7 files changed, 85 insertions(+), 39 deletions(-) create mode 100644 modules/ppcp-button/resources/js/modules/Preview/DummyPreviewButton.js rename modules/ppcp-button/resources/js/modules/{Renderer => Preview}/PreviewButton.js (84%) rename modules/ppcp-button/resources/js/modules/{Renderer => Preview}/PreviewButtonManager.js (92%) diff --git a/modules/ppcp-applepay/resources/js/Preview/ApplePayPreviewButton.js b/modules/ppcp-applepay/resources/js/Preview/ApplePayPreviewButton.js index 6e3f721da..de4807f78 100644 --- a/modules/ppcp-applepay/resources/js/Preview/ApplePayPreviewButton.js +++ b/modules/ppcp-applepay/resources/js/Preview/ApplePayPreviewButton.js @@ -1,5 +1,5 @@ import ApplepayButton from '../ApplepayButton'; -import PreviewButton from '../../../../ppcp-button/resources/js/modules/Renderer/PreviewButton'; +import PreviewButton from '../../../../ppcp-button/resources/js/modules/Preview/PreviewButton'; /** * A single Apple Pay preview button instance. @@ -19,10 +19,10 @@ export default class ApplePayPreviewButton extends PreviewButton { } createNewWrapper() { - const element = super.createNewWrapper(); - element.addClass( 'ppcp-button-apm ppcp-button-applepay' ); + const wrapper = super.createNewWrapper(); + wrapper.classList.add( 'ppcp-button-apm', 'ppcp-button-applepay' ); - return element; + return wrapper; } createButton( buttonConfig ) { diff --git a/modules/ppcp-applepay/resources/js/Preview/ApplePayPreviewButtonManager.js b/modules/ppcp-applepay/resources/js/Preview/ApplePayPreviewButtonManager.js index 43ecc37b8..741b800db 100644 --- a/modules/ppcp-applepay/resources/js/Preview/ApplePayPreviewButtonManager.js +++ b/modules/ppcp-applepay/resources/js/Preview/ApplePayPreviewButtonManager.js @@ -1,4 +1,4 @@ -import PreviewButtonManager from '../../../../ppcp-button/resources/js/modules/Renderer/PreviewButtonManager'; +import PreviewButtonManager from '../../../../ppcp-button/resources/js/modules/Preview/PreviewButtonManager'; import ApplePayPreviewButton from './ApplePayPreviewButton'; /** diff --git a/modules/ppcp-button/resources/js/modules/Preview/DummyPreviewButton.js b/modules/ppcp-button/resources/js/modules/Preview/DummyPreviewButton.js new file mode 100644 index 000000000..e7fe3e3ff --- /dev/null +++ b/modules/ppcp-button/resources/js/modules/Preview/DummyPreviewButton.js @@ -0,0 +1,31 @@ +import PreviewButton from './PreviewButton'; + +/** + * Dummy preview button, to use in case an APM button cannot be rendered + */ +export default class DummyPreviewButton extends PreviewButton { + #innerEl; + + constructor( args ) { + super( args ); + + this.selector = `${ args.selector }Dummy`; + this.label = args.label || 'Not Available'; + } + + createNewWrapper() { + const wrapper = super.createNewWrapper(); + wrapper.classList.add( 'ppcp-button-apm', 'ppcp-button-dummy' ); + + return wrapper; + } + + createButton( buttonConfig ) { + this.#innerEl?.remove(); + + this.#innerEl = document.createElement( 'div' ); + this.#innerEl.innerHTML = `
${ this.label }
`; + + this.domWrapper.appendChild( this.#innerEl ); + } +} diff --git a/modules/ppcp-button/resources/js/modules/Renderer/PreviewButton.js b/modules/ppcp-button/resources/js/modules/Preview/PreviewButton.js similarity index 84% rename from modules/ppcp-button/resources/js/modules/Renderer/PreviewButton.js rename to modules/ppcp-button/resources/js/modules/Preview/PreviewButton.js index ac189e89e..72f8c07df 100644 --- a/modules/ppcp-button/resources/js/modules/Renderer/PreviewButton.js +++ b/modules/ppcp-button/resources/js/modules/Preview/PreviewButton.js @@ -26,13 +26,16 @@ class PreviewButton { /** * Creates a new DOM node to contain the preview button. * - * @return {jQuery} Always a single jQuery element with the new DOM node. + * @return {HTMLElement} Always a single jQuery element with the new DOM node. */ createNewWrapper() { + const wrapper = document.createElement( 'div' ); const previewId = this.selector.replace( '#', '' ); const previewClass = 'ppcp-preview-button'; - return jQuery( `
` ); + wrapper.setAttribute( 'id', previewId ); + wrapper.setAttribute( 'class', previewClass ); + return wrapper; } /** @@ -109,10 +112,12 @@ class PreviewButton { console.error( 'Skip render, button is not configured yet' ); return; } + this.domWrapper = this.createNewWrapper(); - this.domWrapper.insertAfter( this.wrapper ); + this._insertWrapper(); } else { - this.domWrapper.empty().show(); + this._emptyWrapper(); + this._showWrapper(); } this.isVisible = true; @@ -151,16 +156,38 @@ class PreviewButton { * Using a timeout here will make the button visible again at the end of the current * event queue. */ - setTimeout( () => this.domWrapper.show() ); + setTimeout( () => this._showWrapper() ); } remove() { this.isVisible = false; if ( this.domWrapper ) { - this.domWrapper.hide().empty(); + this._hideWrapper(); + this._emptyWrapper(); } } + + _showWrapper() { + this.domWrapper.style.display = ''; + } + + _hideWrapper() { + this.domWrapper.style.display = 'none'; + } + + _emptyWrapper() { + this.domWrapper.innerHTML = ''; + } + + _insertWrapper() { + const wrapperElement = document.querySelector( this.wrapper ); + + wrapperElement.parentNode.insertBefore( + this.domWrapper, + wrapperElement.nextSibling + ); + } } export default PreviewButton; diff --git a/modules/ppcp-button/resources/js/modules/Renderer/PreviewButtonManager.js b/modules/ppcp-button/resources/js/modules/Preview/PreviewButtonManager.js similarity index 92% rename from modules/ppcp-button/resources/js/modules/Renderer/PreviewButtonManager.js rename to modules/ppcp-button/resources/js/modules/Preview/PreviewButtonManager.js index 9cb9fa81f..c2df2b839 100644 --- a/modules/ppcp-button/resources/js/modules/Renderer/PreviewButtonManager.js +++ b/modules/ppcp-button/resources/js/modules/Preview/PreviewButtonManager.js @@ -1,6 +1,7 @@ import { loadCustomScript } from '@paypal/paypal-js'; -import widgetBuilder from './WidgetBuilder'; import { debounce } from '../../../../../ppcp-blocks/resources/js/Helper/debounce'; +import widgetBuilder from '../Renderer/WidgetBuilder'; +import DummyPreviewButton from './DummyPreviewButton'; /** * Manages all PreviewButton instances of a certain payment method on the page. @@ -89,27 +90,14 @@ class PreviewButtonManager { * * This dummy is only visible on the admin side, and not rendered on the front-end. * - * @todo Consider refactoring this into a new class that extends the PreviewButton class. * @param wrapperId * @return {any} */ - createDummy( wrapperId ) { - const elButton = document.createElement( 'div' ); - elButton.classList.add( 'ppcp-preview-button', 'ppcp-button-dummy' ); - elButton.innerHTML = `${ - this.apiError ?? 'Not Available' - }`; - - document.querySelector( wrapperId ).appendChild( elButton ); - - const instDummy = { - setDynamic: () => instDummy, - setPpcpConfig: () => instDummy, - render: () => {}, - remove: () => {}, - }; - - return instDummy; + createDummyButtonInstance( wrapperId ) { + return new DummyPreviewButton( { + selector: wrapperId, + label: this.apiError, + } ); } registerEventListeners() { @@ -309,13 +297,13 @@ class PreviewButtonManager { const createButton = () => { if ( ! this.buttons[ id ] ) { let newInst; + if ( this.apiConfig && 'object' === typeof this.apiConfig ) { - newInst = this.createButtonInstance( id ).setButtonConfig( - this.buttonConfig - ); + newInst = this.createButtonInstance( id ); } else { - newInst = this.createDummy( id ); + newInst = this.createDummyButtonInstance( id ); } + newInst.setButtonConfig( this.buttonConfig ); this.buttons[ id ] = newInst; } diff --git a/modules/ppcp-googlepay/resources/js/Preview/GooglePayPreviewButton.js b/modules/ppcp-googlepay/resources/js/Preview/GooglePayPreviewButton.js index 7bd3ffa7f..97bb62ea4 100644 --- a/modules/ppcp-googlepay/resources/js/Preview/GooglePayPreviewButton.js +++ b/modules/ppcp-googlepay/resources/js/Preview/GooglePayPreviewButton.js @@ -1,5 +1,5 @@ import GooglepayButton from '../GooglepayButton'; -import PreviewButton from '../../../../ppcp-button/resources/js/modules/Renderer/PreviewButton'; +import PreviewButton from '../../../../ppcp-button/resources/js/modules/Preview/PreviewButton'; /** * A single GooglePay preview button instance. @@ -21,10 +21,10 @@ export default class GooglePayPreviewButton extends PreviewButton { } createNewWrapper() { - const element = super.createNewWrapper(); - element.addClass( 'ppcp-button-apm ppcp-button-googlepay' ); + const wrapper = super.createNewWrapper(); + wrapper.classList.add( 'ppcp-button-apm', 'ppcp-button-googlepay' ); - return element; + return wrapper; } createButton( buttonConfig ) { diff --git a/modules/ppcp-googlepay/resources/js/Preview/GooglePayPreviewButtonManager.js b/modules/ppcp-googlepay/resources/js/Preview/GooglePayPreviewButtonManager.js index 0aaef0396..0406c1cc4 100644 --- a/modules/ppcp-googlepay/resources/js/Preview/GooglePayPreviewButtonManager.js +++ b/modules/ppcp-googlepay/resources/js/Preview/GooglePayPreviewButtonManager.js @@ -1,4 +1,4 @@ -import PreviewButtonManager from '../../../../ppcp-button/resources/js/modules/Renderer/PreviewButtonManager'; +import PreviewButtonManager from '../../../../ppcp-button/resources/js/modules/Preview/PreviewButtonManager'; import GooglePayPreviewButton from './GooglePayPreviewButton'; /** From 7f9cbd6f58da6b66d0b51c761e47c36f0ee92b88 Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Mon, 15 Jul 2024 13:16:08 +0200 Subject: [PATCH 03/80] =?UTF-8?q?=F0=9F=90=9B=20Fix=20general=20preview=20?= =?UTF-8?q?button=20shape?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../resources/js/ApplepayButton.js | 1 + .../js/modules/Preview/DummyPreviewButton.js | 17 +++++++++++++++++ .../resources/js/GooglepayButton.js | 9 ++++++--- 3 files changed, 24 insertions(+), 3 deletions(-) diff --git a/modules/ppcp-applepay/resources/js/ApplepayButton.js b/modules/ppcp-applepay/resources/js/ApplepayButton.js index 2bcac16fc..e60af5e79 100644 --- a/modules/ppcp-applepay/resources/js/ApplepayButton.js +++ b/modules/ppcp-applepay/resources/js/ApplepayButton.js @@ -216,6 +216,7 @@ class ApplepayButton { } const $wrapper = jQuery( '#' + wrapper ); + $wrapper.removeClass( 'ppcp-button-rect ppcp-button-pill' ); $wrapper.addClass( 'ppcp-button-' + ppcpStyle.shape ); if ( ppcpStyle.height ) { diff --git a/modules/ppcp-button/resources/js/modules/Preview/DummyPreviewButton.js b/modules/ppcp-button/resources/js/modules/Preview/DummyPreviewButton.js index e7fe3e3ff..ed5f3bee7 100644 --- a/modules/ppcp-button/resources/js/modules/Preview/DummyPreviewButton.js +++ b/modules/ppcp-button/resources/js/modules/Preview/DummyPreviewButton.js @@ -26,6 +26,23 @@ export default class DummyPreviewButton extends PreviewButton { this.#innerEl = document.createElement( 'div' ); this.#innerEl.innerHTML = `
${ this.label }
`; + this._applyShape( this.ppcpConfig?.button?.style?.shape ); + this.domWrapper.appendChild( this.#innerEl ); } + + /** + * Applies the button shape (rect/pill) to the dummy button + * + * @param {string|null} shape + * @private + */ + _applyShape( shape = 'rect' ) { + this.domWrapper.classList.remove( + 'ppcp-button-pill', + 'ppcp-button-rect' + ); + + this.domWrapper.classList.add( `ppcp-button-${ shape }` ); + } } diff --git a/modules/ppcp-googlepay/resources/js/GooglepayButton.js b/modules/ppcp-googlepay/resources/js/GooglepayButton.js index 973648af3..8316c42e5 100644 --- a/modules/ppcp-googlepay/resources/js/GooglepayButton.js +++ b/modules/ppcp-googlepay/resources/js/GooglepayButton.js @@ -202,10 +202,13 @@ class GooglepayButton { const { wrapper, ppcpStyle, buttonStyle } = this.contextConfig(); this.waitForWrapper( wrapper, () => { - jQuery( wrapper ).addClass( 'ppcp-button-' + ppcpStyle.shape ); + const $wrapper = jQuery( wrapper ); + + $wrapper.removeClass( 'ppcp-button-rect ppcp-button-pill' ); + $wrapper.addClass( 'ppcp-button-' + ppcpStyle.shape ); if ( ppcpStyle.height ) { - jQuery( wrapper ).css( 'height', `${ ppcpStyle.height }px` ); + $wrapper.css( 'height', `${ ppcpStyle.height }px` ); } const button = this.paymentsClient.createButton( { @@ -217,7 +220,7 @@ class GooglepayButton { buttonSizeMode: 'fill', } ); - jQuery( wrapper ).append( button ); + $wrapper.append( button ); } ); } From 4cde024320263150d9ff39626d42914b4062630d Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Mon, 15 Jul 2024 13:18:07 +0200 Subject: [PATCH 04/80] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Consolidate=20the=20?= =?UTF-8?q?preview=20button=E2=80=99s=20CSS=20classes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Class names are now generated by the base class. This allows to remove a method from all derived classes --- .../resources/js/Preview/ApplePayPreviewButton.js | 7 ------- .../js/Preview/ApplePayPreviewButtonManager.js | 1 + .../resources/js/modules/Preview/PreviewButton.js | 15 ++++++++++----- .../js/modules/Preview/PreviewButtonManager.js | 8 ++++++++ .../js/Preview/GooglePayPreviewButton.js | 7 ------- .../js/Preview/GooglePayPreviewButtonManager.js | 1 + 6 files changed, 20 insertions(+), 19 deletions(-) diff --git a/modules/ppcp-applepay/resources/js/Preview/ApplePayPreviewButton.js b/modules/ppcp-applepay/resources/js/Preview/ApplePayPreviewButton.js index de4807f78..2ad3e23f5 100644 --- a/modules/ppcp-applepay/resources/js/Preview/ApplePayPreviewButton.js +++ b/modules/ppcp-applepay/resources/js/Preview/ApplePayPreviewButton.js @@ -18,13 +18,6 @@ export default class ApplePayPreviewButton extends PreviewButton { }; } - createNewWrapper() { - const wrapper = super.createNewWrapper(); - wrapper.classList.add( 'ppcp-button-apm', 'ppcp-button-applepay' ); - - return wrapper; - } - createButton( buttonConfig ) { const button = new ApplepayButton( 'preview', diff --git a/modules/ppcp-applepay/resources/js/Preview/ApplePayPreviewButtonManager.js b/modules/ppcp-applepay/resources/js/Preview/ApplePayPreviewButtonManager.js index 741b800db..be15166e6 100644 --- a/modules/ppcp-applepay/resources/js/Preview/ApplePayPreviewButtonManager.js +++ b/modules/ppcp-applepay/resources/js/Preview/ApplePayPreviewButtonManager.js @@ -44,6 +44,7 @@ export default class ApplePayPreviewButtonManager extends PreviewButtonManager { return new ApplePayPreviewButton( { selector: wrapperId, apiConfig: this.apiConfig, + methodName: this.methodName, } ); } } diff --git a/modules/ppcp-button/resources/js/modules/Preview/PreviewButton.js b/modules/ppcp-button/resources/js/modules/Preview/PreviewButton.js index 72f8c07df..418bb57f5 100644 --- a/modules/ppcp-button/resources/js/modules/Preview/PreviewButton.js +++ b/modules/ppcp-button/resources/js/modules/Preview/PreviewButton.js @@ -5,16 +5,21 @@ import merge from 'deepmerge'; */ class PreviewButton { /** - * @param {string} selector - CSS ID of the wrapper, including the `#` - * @param {Object} apiConfig - PayPal configuration object; retrieved via a - * widgetBuilder API method + * @param {string} selector - CSS ID of the wrapper, including the `#` + * @param {Object} apiConfig - PayPal configuration object; retrieved via a + * widgetBuilder API method + * @param {string} methodName - Name of the payment method, e.g. "Google Pay" */ - constructor( { selector, apiConfig } ) { + constructor( { selector, apiConfig, methodName = '' } ) { this.apiConfig = apiConfig; this.defaultAttributes = {}; this.buttonConfig = {}; this.ppcpConfig = {}; this.isDynamic = true; + this.methodName = methodName; + this.methodSlug = this.methodName + .toLowerCase() + .replace( /[^a-z]+/g, '' ); // The selector is usually overwritten in constructor of derived class. this.selector = selector; @@ -31,7 +36,7 @@ class PreviewButton { createNewWrapper() { const wrapper = document.createElement( 'div' ); const previewId = this.selector.replace( '#', '' ); - const previewClass = 'ppcp-preview-button'; + const previewClass = `ppcp-preview-button ppcp-button-apm ppcp-button-${ this.methodSlug }`; wrapper.setAttribute( 'id', previewId ); wrapper.setAttribute( 'class', previewClass ); diff --git a/modules/ppcp-button/resources/js/modules/Preview/PreviewButtonManager.js b/modules/ppcp-button/resources/js/modules/Preview/PreviewButtonManager.js index c2df2b839..186cf7c45 100644 --- a/modules/ppcp-button/resources/js/modules/Preview/PreviewButtonManager.js +++ b/modules/ppcp-button/resources/js/modules/Preview/PreviewButtonManager.js @@ -21,6 +21,13 @@ class PreviewButtonManager { */ #onInit; + /** + * Initialize the new PreviewButtonManager. + * + * @param {string} methodName - Name of the payment method, e.g. "Google Pay" + * @param {Object} buttonConfig + * @param {Object} defaultAttributes + */ constructor( { methodName, buttonConfig, defaultAttributes } ) { // Define the payment method name in the derived class. this.methodName = methodName; @@ -97,6 +104,7 @@ class PreviewButtonManager { return new DummyPreviewButton( { selector: wrapperId, label: this.apiError, + methodName: this.methodName, } ); } diff --git a/modules/ppcp-googlepay/resources/js/Preview/GooglePayPreviewButton.js b/modules/ppcp-googlepay/resources/js/Preview/GooglePayPreviewButton.js index 97bb62ea4..e6b27ee55 100644 --- a/modules/ppcp-googlepay/resources/js/Preview/GooglePayPreviewButton.js +++ b/modules/ppcp-googlepay/resources/js/Preview/GooglePayPreviewButton.js @@ -20,13 +20,6 @@ export default class GooglePayPreviewButton extends PreviewButton { }; } - createNewWrapper() { - const wrapper = super.createNewWrapper(); - wrapper.classList.add( 'ppcp-button-apm', 'ppcp-button-googlepay' ); - - return wrapper; - } - createButton( buttonConfig ) { const button = new GooglepayButton( 'preview', diff --git a/modules/ppcp-googlepay/resources/js/Preview/GooglePayPreviewButtonManager.js b/modules/ppcp-googlepay/resources/js/Preview/GooglePayPreviewButtonManager.js index 0406c1cc4..a3e9f66af 100644 --- a/modules/ppcp-googlepay/resources/js/Preview/GooglePayPreviewButtonManager.js +++ b/modules/ppcp-googlepay/resources/js/Preview/GooglePayPreviewButtonManager.js @@ -51,6 +51,7 @@ export default class GooglePayPreviewButtonManager extends PreviewButtonManager return new GooglePayPreviewButton( { selector: wrapperId, apiConfig: this.apiConfig, + methodName: this.methodName, } ); } } From 6081b9ddf9b2aa1fa32a507f4adb7576d64ac289 Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Mon, 15 Jul 2024 13:19:37 +0200 Subject: [PATCH 05/80] =?UTF-8?q?=F0=9F=A9=B9=20Adjust=20the=20height=20of?= =?UTF-8?q?=20the=20Dummy=20preview-button?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Dummy button now better aligns with the button settings --- .../js/modules/Preview/DummyPreviewButton.js | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/modules/ppcp-button/resources/js/modules/Preview/DummyPreviewButton.js b/modules/ppcp-button/resources/js/modules/Preview/DummyPreviewButton.js index ed5f3bee7..c780d188d 100644 --- a/modules/ppcp-button/resources/js/modules/Preview/DummyPreviewButton.js +++ b/modules/ppcp-button/resources/js/modules/Preview/DummyPreviewButton.js @@ -26,7 +26,7 @@ export default class DummyPreviewButton extends PreviewButton { this.#innerEl = document.createElement( 'div' ); this.#innerEl.innerHTML = `
${ this.label }
`; - this._applyShape( this.ppcpConfig?.button?.style?.shape ); + this._applyStyles( this.ppcpConfig?.button?.style ); this.domWrapper.appendChild( this.#innerEl ); } @@ -34,15 +34,19 @@ export default class DummyPreviewButton extends PreviewButton { /** * Applies the button shape (rect/pill) to the dummy button * - * @param {string|null} shape + * @param {{shape: string, height: number|null}} style * @private */ - _applyShape( shape = 'rect' ) { + _applyStyles( style ) { this.domWrapper.classList.remove( 'ppcp-button-pill', 'ppcp-button-rect' ); - this.domWrapper.classList.add( `ppcp-button-${ shape }` ); + this.domWrapper.classList.add( `ppcp-button-${ style.shape }` ); + + if ( style.height ) { + this.domWrapper.style.height = `${ style.height }px`; + } } } From 07ec924c77cbcfa05d2807d5ae4eb4c4a59908ee Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Mon, 15 Jul 2024 13:43:09 +0200 Subject: [PATCH 06/80] =?UTF-8?q?=F0=9F=92=84=20Display=20the=20APM=20logo?= =?UTF-8?q?=20in=20dummy=20preview-buttons?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- modules/ppcp-applepay/resources/css/styles.scss | 5 +++++ modules/ppcp-googlepay/resources/css/styles.scss | 5 +++++ .../ppcp-wc-gateway/resources/css/common.scss | 16 ++++++++++++++++ 3 files changed, 26 insertions(+) diff --git a/modules/ppcp-applepay/resources/css/styles.scss b/modules/ppcp-applepay/resources/css/styles.scss index 3818b8db5..1cf632fd2 100644 --- a/modules/ppcp-applepay/resources/css/styles.scss +++ b/modules/ppcp-applepay/resources/css/styles.scss @@ -23,6 +23,11 @@ &.ppcp-button-minicart { --apple-pay-button-display: block; } + + &.ppcp-preview-button.ppcp-button-dummy { + /* URL must specify the correct module-folder! */ + --apm-button-dummy-background: url(../../../ppcp-applepay/assets/images/applepay.png); + } } .wp-block-woocommerce-checkout, .wp-block-woocommerce-cart { diff --git a/modules/ppcp-googlepay/resources/css/styles.scss b/modules/ppcp-googlepay/resources/css/styles.scss index c60212a2e..ad638b0b3 100644 --- a/modules/ppcp-googlepay/resources/css/styles.scss +++ b/modules/ppcp-googlepay/resources/css/styles.scss @@ -6,6 +6,11 @@ outline-offset: -1px; border-radius: var(--apm-button-border-radius); } + + &.ppcp-preview-button.ppcp-button-dummy { + /* URL must specify the correct module-folder! */ + --apm-button-dummy-background: url(../../../ppcp-googlepay/assets/images/googlepay.png); + } } .wp-block-woocommerce-checkout, .wp-block-woocommerce-cart { diff --git a/modules/ppcp-wc-gateway/resources/css/common.scss b/modules/ppcp-wc-gateway/resources/css/common.scss index b694547c8..9071e9e5a 100644 --- a/modules/ppcp-wc-gateway/resources/css/common.scss +++ b/modules/ppcp-wc-gateway/resources/css/common.scss @@ -31,9 +31,25 @@ $background-ident-color: #fbfbfb; &.ppcp-button-dummy { display: flex; + min-height: 25px; align-items: center; justify-content: center; background: #0001; + position: relative; + + &:before { + content: ''; + position: absolute; + left: 12px; + top: 50%; + transform: translateY(-50%); + width: 42px; + height: 24px; + background-image: var(--apm-button-dummy-background, none); + background-repeat: no-repeat; + background-size: contain; + background-position: center left; + } } } From a4a4f80a9935a21f5ba3247964957593c9141cca Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Mon, 15 Jul 2024 13:58:58 +0200 Subject: [PATCH 07/80] =?UTF-8?q?=F0=9F=90=9B=20Isolate=20preview=20change?= =?UTF-8?q?s=20to=20relevant=20sections?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Before this, any style change updated all preview buttons on the current page. Now, most changes are limited to the current section. For exaple, changing the height of “Mini Cart Buttons” only updates the mini cart button preview, and no other preview sections. --- .../js/modules/Preview/PreviewButtonManager.js | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/modules/ppcp-button/resources/js/modules/Preview/PreviewButtonManager.js b/modules/ppcp-button/resources/js/modules/Preview/PreviewButtonManager.js index 186cf7c45..2afafad23 100644 --- a/modules/ppcp-button/resources/js/modules/Preview/PreviewButtonManager.js +++ b/modules/ppcp-button/resources/js/modules/Preview/PreviewButtonManager.js @@ -283,6 +283,17 @@ class PreviewButtonManager { */ _configureAllButtons( ppcpConfig ) { Object.entries( this.buttons ).forEach( ( [ id, button ] ) => { + const limitWrapper = ppcpConfig.button?.wrapper; + + /** + * When the ppcpConfig object specifies a button wrapper, then ensure to limit preview + * changes to this individual wrapper. If no button wrapper is defined, the + * configuration is relevant for all buttons on the page. + */ + if ( limitWrapper && button.wrapper !== limitWrapper ) { + return; + } + this._configureButton( id, { ...ppcpConfig, button: { From 651549dbb01c2db35723ca30e7eceb4850dc9995 Mon Sep 17 00:00:00 2001 From: George Burduli Date: Tue, 16 Jul 2024 15:10:09 +0400 Subject: [PATCH 08/80] Display notice if Checkout Blocks is not active --- .../ppcp-wc-gateway/resources/css/common.scss | 8 +++++ modules/ppcp-wc-gateway/services.php | 30 +++++++++++++++++++ 2 files changed, 38 insertions(+) diff --git a/modules/ppcp-wc-gateway/resources/css/common.scss b/modules/ppcp-wc-gateway/resources/css/common.scss index b694547c8..7a65a701c 100644 --- a/modules/ppcp-wc-gateway/resources/css/common.scss +++ b/modules/ppcp-wc-gateway/resources/css/common.scss @@ -82,6 +82,14 @@ $background-ident-color: #fbfbfb; } } +.ppcp-notice-success { + border-left-color: #00a32a; + + .highlight { + color: #00a32a; + } +} + .ppcp-notice-warning { border-left-color: #dba617; diff --git a/modules/ppcp-wc-gateway/services.php b/modules/ppcp-wc-gateway/services.php index f0d848216..45a93e346 100644 --- a/modules/ppcp-wc-gateway/services.php +++ b/modules/ppcp-wc-gateway/services.php @@ -26,6 +26,7 @@ use WooCommerce\PayPalCommerce\Onboarding\State; use WooCommerce\PayPalCommerce\WcGateway\Admin\RenderReauthorizeAction; use WooCommerce\PayPalCommerce\WcGateway\Endpoint\CaptureCardPayment; use WooCommerce\PayPalCommerce\WcGateway\Endpoint\RefreshFeatureStatusEndpoint; +use WooCommerce\PayPalCommerce\WcGateway\Helper\CartCheckoutDetector; use WooCommerce\PayPalCommerce\WcSubscriptions\Helper\SubscriptionHelper; use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface; use WooCommerce\PayPalCommerce\WcGateway\Admin\FeesRenderer; @@ -292,6 +293,24 @@ return array( static function ( ContainerInterface $container ): AuthorizeOrderActionNotice { return new AuthorizeOrderActionNotice(); }, + 'wcgateway.notice.checkout-blocks' => + static function ( ContainerInterface $container ): string { + $instructions_link = 'https://woocommerce.com/document/cart-checkout-blocks-status/#using-the-cart-and-checkout-blocks'; + if ( ! CartCheckoutDetector::has_block_checkout() ) { + $notice_content = sprintf( + /* translators: %1$s: URL to the instructions page. */ + __( + 'Your store\'s checkout page currently uses a classic checkout layout or a custom checkout widget. Advanced Card Processing now also supports the new & improved Checkout block. See this page for instructions on upgrading to the new Checkout layout.', + 'woocommerce-paypal-payments' + ), + esc_url( $instructions_link ) + ); + } else { + return ''; + } + + return '

' . $notice_content . '

'; + }, 'wcgateway.settings.sections-renderer' => static function ( ContainerInterface $container ): SectionsRenderer { return new SectionsRenderer( $container->get( 'wcgateway.current-ppcp-settings-page-id' ), @@ -533,6 +552,17 @@ return array( 'requirements' => array(), 'gateway' => 'paypal', ), + 'dcc_block_checkout_notice' => array( + 'heading' => '', + 'html' => $container->get( 'wcgateway.notice.checkout-blocks' ), + 'type' => 'ppcp-html', + 'classes' => array(), + 'screens' => array( + State::STATE_ONBOARDED, + ), + 'requirements' => array( 'dcc' ), + 'gateway' => 'dcc', + ), 'dcc_enabled' => array( 'title' => __( 'Enable/Disable', 'woocommerce-paypal-payments' ), 'desc_tip' => true, From 5d429c75e00f6711e312edd2f04e095bee87f7eb Mon Sep 17 00:00:00 2001 From: George Burduli Date: Tue, 16 Jul 2024 15:36:18 +0400 Subject: [PATCH 09/80] Improve notice format and text --- modules/ppcp-wc-gateway/services.php | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/modules/ppcp-wc-gateway/services.php b/modules/ppcp-wc-gateway/services.php index 45a93e346..5a5ee4d26 100644 --- a/modules/ppcp-wc-gateway/services.php +++ b/modules/ppcp-wc-gateway/services.php @@ -295,14 +295,16 @@ return array( }, 'wcgateway.notice.checkout-blocks' => static function ( ContainerInterface $container ): string { + $checkout_page_link = esc_url( get_edit_post_link( wc_get_page_id( 'checkout' ) ) ?? '' ); $instructions_link = 'https://woocommerce.com/document/cart-checkout-blocks-status/#using-the-cart-and-checkout-blocks'; - if ( ! CartCheckoutDetector::has_block_checkout() ) { + if ( CartCheckoutDetector::has_block_checkout() ) { $notice_content = sprintf( /* translators: %1$s: URL to the instructions page. */ __( - 'Your store\'s checkout page currently uses a classic checkout layout or a custom checkout widget. Advanced Card Processing now also supports the new & improved Checkout block. See this page for instructions on upgrading to the new Checkout layout.', + 'Info: The Checkout page of your store currently uses a classic checkout layout or a custom checkout widget. Advanced Card Processing supports the new Checkout block which improves conversion rates. See this page for instructions on how to upgrade to the new Checkout layout.', 'woocommerce-paypal-payments' ), + esc_url( $checkout_page_link ), esc_url( $instructions_link ) ); } else { From e1a93b373d0dc2065fc618880aa445421722bfcc Mon Sep 17 00:00:00 2001 From: George Burduli Date: Tue, 16 Jul 2024 15:38:19 +0400 Subject: [PATCH 10/80] Fix condition, translation instructions --- modules/ppcp-wc-gateway/services.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/ppcp-wc-gateway/services.php b/modules/ppcp-wc-gateway/services.php index 5a5ee4d26..0adc2dbad 100644 --- a/modules/ppcp-wc-gateway/services.php +++ b/modules/ppcp-wc-gateway/services.php @@ -297,9 +297,9 @@ return array( static function ( ContainerInterface $container ): string { $checkout_page_link = esc_url( get_edit_post_link( wc_get_page_id( 'checkout' ) ) ?? '' ); $instructions_link = 'https://woocommerce.com/document/cart-checkout-blocks-status/#using-the-cart-and-checkout-blocks'; - if ( CartCheckoutDetector::has_block_checkout() ) { + if ( ! CartCheckoutDetector::has_block_checkout() ) { $notice_content = sprintf( - /* translators: %1$s: URL to the instructions page. */ + /* translators: %1$s: URL to the Checkout edit page. %2$s: URL to the WooCommerce Checkout instructions. */ __( 'Info: The Checkout page of your store currently uses a classic checkout layout or a custom checkout widget. Advanced Card Processing supports the new Checkout block which improves conversion rates. See this page for instructions on how to upgrade to the new Checkout layout.', 'woocommerce-paypal-payments' From cb9785993f005c5dd0bccb293cc34b1e221c6040 Mon Sep 17 00:00:00 2001 From: George Burduli Date: Tue, 16 Jul 2024 15:55:59 +0400 Subject: [PATCH 11/80] Only display notice if Fastlane is disabled --- modules/ppcp-wc-gateway/services.php | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/modules/ppcp-wc-gateway/services.php b/modules/ppcp-wc-gateway/services.php index 0adc2dbad..4286872cc 100644 --- a/modules/ppcp-wc-gateway/services.php +++ b/modules/ppcp-wc-gateway/services.php @@ -297,6 +297,14 @@ return array( static function ( ContainerInterface $container ): string { $checkout_page_link = esc_url( get_edit_post_link( wc_get_page_id( 'checkout' ) ) ?? '' ); $instructions_link = 'https://woocommerce.com/document/cart-checkout-blocks-status/#using-the-cart-and-checkout-blocks'; + + $settings = $container->get( 'wcgateway.settings' ); + assert( $settings instanceof Settings ); + + if ( $settings->has( 'axo_enabled' ) && $settings->get( 'axo_enabled' ) ) { + return ''; + } + if ( ! CartCheckoutDetector::has_block_checkout() ) { $notice_content = sprintf( /* translators: %1$s: URL to the Checkout edit page. %2$s: URL to the WooCommerce Checkout instructions. */ From fc8b1dd6a68371306c47ce0123aee554b612e64b Mon Sep 17 00:00:00 2001 From: George Burduli Date: Tue, 16 Jul 2024 15:58:16 +0400 Subject: [PATCH 12/80] Reformat --- modules/ppcp-wc-gateway/services.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/modules/ppcp-wc-gateway/services.php b/modules/ppcp-wc-gateway/services.php index 4286872cc..575477bb4 100644 --- a/modules/ppcp-wc-gateway/services.php +++ b/modules/ppcp-wc-gateway/services.php @@ -295,9 +295,6 @@ return array( }, 'wcgateway.notice.checkout-blocks' => static function ( ContainerInterface $container ): string { - $checkout_page_link = esc_url( get_edit_post_link( wc_get_page_id( 'checkout' ) ) ?? '' ); - $instructions_link = 'https://woocommerce.com/document/cart-checkout-blocks-status/#using-the-cart-and-checkout-blocks'; - $settings = $container->get( 'wcgateway.settings' ); assert( $settings instanceof Settings ); @@ -305,6 +302,9 @@ return array( return ''; } + $checkout_page_link = esc_url( get_edit_post_link( wc_get_page_id( 'checkout' ) ) ?? '' ); + $instructions_link = 'https://woocommerce.com/document/cart-checkout-blocks-status/#using-the-cart-and-checkout-blocks'; + if ( ! CartCheckoutDetector::has_block_checkout() ) { $notice_content = sprintf( /* translators: %1$s: URL to the Checkout edit page. %2$s: URL to the WooCommerce Checkout instructions. */ From a65d599ec9ca7f163c4a0e9bb6d76346eb72fd88 Mon Sep 17 00:00:00 2001 From: George Burduli Date: Wed, 17 Jul 2024 13:01:11 +0400 Subject: [PATCH 13/80] Remove redundant 'else' condition --- modules/ppcp-wc-gateway/services.php | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/modules/ppcp-wc-gateway/services.php b/modules/ppcp-wc-gateway/services.php index 575477bb4..3bdaefe30 100644 --- a/modules/ppcp-wc-gateway/services.php +++ b/modules/ppcp-wc-gateway/services.php @@ -305,20 +305,20 @@ return array( $checkout_page_link = esc_url( get_edit_post_link( wc_get_page_id( 'checkout' ) ) ?? '' ); $instructions_link = 'https://woocommerce.com/document/cart-checkout-blocks-status/#using-the-cart-and-checkout-blocks'; - if ( ! CartCheckoutDetector::has_block_checkout() ) { - $notice_content = sprintf( - /* translators: %1$s: URL to the Checkout edit page. %2$s: URL to the WooCommerce Checkout instructions. */ - __( - 'Info: The Checkout page of your store currently uses a classic checkout layout or a custom checkout widget. Advanced Card Processing supports the new Checkout block which improves conversion rates. See this page for instructions on how to upgrade to the new Checkout layout.', - 'woocommerce-paypal-payments' - ), - esc_url( $checkout_page_link ), - esc_url( $instructions_link ) - ); - } else { + if ( CartCheckoutDetector::has_block_checkout() ) { return ''; } + $notice_content = sprintf( + /* translators: %1$s: URL to the Checkout edit page. %2$s: URL to the WooCommerce Checkout instructions. */ + __( + 'Info: The Checkout page of your store currently uses a classic checkout layout or a custom checkout widget. Advanced Card Processing supports the new Checkout block which improves conversion rates. See this page for instructions on how to upgrade to the new Checkout layout.', + 'woocommerce-paypal-payments' + ), + esc_url( $checkout_page_link ), + esc_url( $instructions_link ) + ); + return '

' . $notice_content . '

'; }, 'wcgateway.settings.sections-renderer' => static function ( ContainerInterface $container ): SectionsRenderer { From c664a8308b4b70500c828c387643e41d1ad2fecb Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Wed, 17 Jul 2024 14:05:18 +0200 Subject: [PATCH 14/80] =?UTF-8?q?=E2=9C=8F=EF=B8=8F=20Fix=20incorrect=20=20tags?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ppcp-wc-gateway/src/Settings/Fields/card-button-fields.php | 2 +- .../src/Settings/Fields/pay-later-tab-fields.php | 2 +- .../src/Settings/Fields/paypal-smart-button-fields.php | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/modules/ppcp-wc-gateway/src/Settings/Fields/card-button-fields.php b/modules/ppcp-wc-gateway/src/Settings/Fields/card-button-fields.php index 1b8d13d9c..1bb6c24b8 100644 --- a/modules/ppcp-wc-gateway/src/Settings/Fields/card-button-fields.php +++ b/modules/ppcp-wc-gateway/src/Settings/Fields/card-button-fields.php @@ -32,7 +32,7 @@ return function ( ContainerInterface $container, array $fields ): array { ), '', '', - '' + '
' ), 'type' => 'ppcp-heading', 'screens' => array( diff --git a/modules/ppcp-wc-gateway/src/Settings/Fields/pay-later-tab-fields.php b/modules/ppcp-wc-gateway/src/Settings/Fields/pay-later-tab-fields.php index 06b4f51ce..0090b6632 100644 --- a/modules/ppcp-wc-gateway/src/Settings/Fields/pay-later-tab-fields.php +++ b/modules/ppcp-wc-gateway/src/Settings/Fields/pay-later-tab-fields.php @@ -847,7 +847,7 @@ return function ( ContainerInterface $container, array $fields ): array { __( 'When enabled, a %1$sPay Later button%2$s is displayed for eligible customers.%3$sPayPal buttons must be enabled to display the Pay Later button.', 'woocommerce-paypal-payments' ), '', '', - '' + '
' ), ), 'pay_later_button_enabled' => array( diff --git a/modules/ppcp-wc-gateway/src/Settings/Fields/paypal-smart-button-fields.php b/modules/ppcp-wc-gateway/src/Settings/Fields/paypal-smart-button-fields.php index 3a2513e98..011cfd192 100644 --- a/modules/ppcp-wc-gateway/src/Settings/Fields/paypal-smart-button-fields.php +++ b/modules/ppcp-wc-gateway/src/Settings/Fields/paypal-smart-button-fields.php @@ -233,7 +233,7 @@ return function ( ContainerInterface $container, array $fields ): array { ), '', '', - '' + '
' ), 'type' => 'ppcp-heading', 'screens' => array( From 7ed92c8fc26352aebf615132618d55bce7b11e23 Mon Sep 17 00:00:00 2001 From: George Burduli Date: Thu, 18 Jul 2024 15:10:15 +0400 Subject: [PATCH 15/80] Fix Fastlane condition --- modules/ppcp-wc-gateway/services.php | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/modules/ppcp-wc-gateway/services.php b/modules/ppcp-wc-gateway/services.php index 3bdaefe30..7bcfc4abb 100644 --- a/modules/ppcp-wc-gateway/services.php +++ b/modules/ppcp-wc-gateway/services.php @@ -298,17 +298,22 @@ return array( $settings = $container->get( 'wcgateway.settings' ); assert( $settings instanceof Settings ); - if ( $settings->has( 'axo_enabled' ) && $settings->get( 'axo_enabled' ) ) { + if ( + $container->has( 'axo.available' ) && + $container->get( 'axo.available' ) && + $settings->has( 'axo_enabled' ) && + $settings->get( 'axo_enabled' ) + ) { + return ''; + } + + if ( CartCheckoutDetector::has_block_checkout() ) { return ''; } $checkout_page_link = esc_url( get_edit_post_link( wc_get_page_id( 'checkout' ) ) ?? '' ); $instructions_link = 'https://woocommerce.com/document/cart-checkout-blocks-status/#using-the-cart-and-checkout-blocks'; - if ( CartCheckoutDetector::has_block_checkout() ) { - return ''; - } - $notice_content = sprintf( /* translators: %1$s: URL to the Checkout edit page. %2$s: URL to the WooCommerce Checkout instructions. */ __( From a829c5b50d5017bd3ae1d79eccf550fccc5255e8 Mon Sep 17 00:00:00 2001 From: George Burduli Date: Thu, 18 Jul 2024 15:17:19 +0400 Subject: [PATCH 16/80] Simplify Fastlane condition --- modules/ppcp-wc-gateway/services.php | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/modules/ppcp-wc-gateway/services.php b/modules/ppcp-wc-gateway/services.php index 7bcfc4abb..d91aa80d6 100644 --- a/modules/ppcp-wc-gateway/services.php +++ b/modules/ppcp-wc-gateway/services.php @@ -298,12 +298,10 @@ return array( $settings = $container->get( 'wcgateway.settings' ); assert( $settings instanceof Settings ); - if ( - $container->has( 'axo.available' ) && - $container->get( 'axo.available' ) && - $settings->has( 'axo_enabled' ) && - $settings->get( 'axo_enabled' ) - ) { + $axo_available = $container->has( 'axo.available' ) && $container->get( 'axo.available' ); + $axo_enabled = $settings->has( 'axo_enabled' ) && $settings->get( 'axo_enabled' ); + + if ( $axo_available && $axo_enabled ) { return ''; } From 9fbc28506fcaceec2c4ecdf774e5fe5dd0483043 Mon Sep 17 00:00:00 2001 From: George Burduli Date: Tue, 13 Aug 2024 11:41:32 +0400 Subject: [PATCH 17/80] Update Google Pay icon with SVG --- modules/ppcp-googlepay/extensions.php | 4 ++-- modules/ppcp-googlepay/resources/css/styles.scss | 6 ++++++ modules/ppcp-googlepay/src/GooglePayGateway.php | 2 +- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/modules/ppcp-googlepay/extensions.php b/modules/ppcp-googlepay/extensions.php index e0a01901f..516c4937e 100644 --- a/modules/ppcp-googlepay/extensions.php +++ b/modules/ppcp-googlepay/extensions.php @@ -72,7 +72,7 @@ return array( 'googlepay_button_enabled' => array( 'title' => __( 'Google Pay Button', 'woocommerce-paypal-payments' ), 'title_html' => sprintf( - '%s', + '%s', $module_url, __( 'Google Pay', 'woocommerce-paypal-payments' ) ), @@ -117,7 +117,7 @@ return array( 'googlepay_button_enabled' => array( 'title' => __( 'Google Pay Button', 'woocommerce-paypal-payments' ), 'title_html' => sprintf( - '%s', + '%s', $module_url, __( 'Google Pay', 'woocommerce-paypal-payments' ) ), diff --git a/modules/ppcp-googlepay/resources/css/styles.scss b/modules/ppcp-googlepay/resources/css/styles.scss index 6cf2119a0..c1eae1563 100644 --- a/modules/ppcp-googlepay/resources/css/styles.scss +++ b/modules/ppcp-googlepay/resources/css/styles.scss @@ -17,3 +17,9 @@ #ppc-button-ppcp-googlepay { display: none; } + +label[for='payment_method_ppcp-googlepay'] { + img { + max-height: 25px; + } +} diff --git a/modules/ppcp-googlepay/src/GooglePayGateway.php b/modules/ppcp-googlepay/src/GooglePayGateway.php index 26a84f326..eafe983de 100644 --- a/modules/ppcp-googlepay/src/GooglePayGateway.php +++ b/modules/ppcp-googlepay/src/GooglePayGateway.php @@ -99,7 +99,7 @@ class GooglePayGateway extends WC_Payment_Gateway { $this->description = $this->get_option( 'description', '' ); $this->module_url = $module_url; - $this->icon = esc_url( $this->module_url ) . 'assets/images/googlepay.png'; + $this->icon = esc_url( $this->module_url ) . 'assets/images/googlepay.svg'; $this->init_form_fields(); $this->init_settings(); From 95487b69e1c360ce3db438c5d9cbd57431094039 Mon Sep 17 00:00:00 2001 From: George Burduli Date: Tue, 13 Aug 2024 11:45:53 +0400 Subject: [PATCH 18/80] Update Apple Pay icon with SVG --- modules/ppcp-applepay/extensions.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/ppcp-applepay/extensions.php b/modules/ppcp-applepay/extensions.php index daf5cf2ea..b126290f2 100644 --- a/modules/ppcp-applepay/extensions.php +++ b/modules/ppcp-applepay/extensions.php @@ -101,7 +101,7 @@ return array( 'applepay_button_enabled' => array( 'title' => __( 'Apple Pay Button', 'woocommerce-paypal-payments' ), 'title_html' => sprintf( - '%s', + '%s', $module_url, __( 'Apple Pay', 'woocommerce-paypal-payments' ) ), @@ -155,7 +155,7 @@ return array( 'applepay_button_enabled' => array( 'title' => __( 'Apple Pay Button', 'woocommerce-paypal-payments' ), 'title_html' => sprintf( - '%s', + '%s', $module_url, __( 'Apple Pay', 'woocommerce-paypal-payments' ) ), From 42c2e4c9275944a4138a1b3356ba5e25de7dfcec Mon Sep 17 00:00:00 2001 From: George Burduli Date: Tue, 13 Aug 2024 11:47:22 +0400 Subject: [PATCH 19/80] Remove redundant PNGs --- .../ppcp-applepay/assets/images/applepay.png | Bin 13805 -> 0 bytes .../ppcp-googlepay/assets/images/googlepay.png | Bin 19520 -> 0 bytes 2 files changed, 0 insertions(+), 0 deletions(-) delete mode 100644 modules/ppcp-applepay/assets/images/applepay.png delete mode 100644 modules/ppcp-googlepay/assets/images/googlepay.png diff --git a/modules/ppcp-applepay/assets/images/applepay.png b/modules/ppcp-applepay/assets/images/applepay.png deleted file mode 100644 index d46807d77f7c31c33d8ea4b3cb7f5f2ee927d2fb..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 13805 zcmeHrby$?$y7$l}9fH&_gdjCAba!`2!!QgmbeDj1i2?$GG!g;=5(0uC-Q5k+Qc9PA z-{`ydyW^bmowKj+JO6F2>tWWjo_qb)x_@`9b*~sL4Mlw12e<$L0AE>2P6zp0fPCz* zF_HhD=9s(y07y^#^$b0ApgurXcNZIkBOK`I=L!eHeGxVQfbaZJ`eSeEPw5dWJh61B z?vw;;#y*%nm$yy{FUbR6><8KPfer1weSeQPB02PN7xW*t zQvz>Y-)u*O+HCk$Wk0kZA;tK4*($C%N#W1Cz|o>5*)2%^8zry;ywz@apoX zVf3;}6IIhn8^!<(djtplR-ROZ8x(&%yLMUj<(~bFpXrKQ{L>EE;c*XOf0_BuV|u?ZC%-`HgMMqJ~=jTvw6^;Nk^riu;_=$zXWKx3A)iXN25}Utu`(oo;?!k zQu=nEBs}A5Yis98zL}}f2q_VWD=RGy&pNoQA;HwU&etN7F(C!rt4IUwN*dEtC5T6v zUbc6m$lokoHujTj;Bk|aFFlx%l+9YC>}8%twUyeWW=zV1UABTWo&J)7bmLv&jWpfi z2I~+j?mRhNoxZ|kW8)qENkiQsplx~i`IZTQPj{gF!@TicmV@{3qHrT5_hMC%(v1mn z%$ol~;JP5dCKIt`WRvA|zWLbjOHcp1vc~z{1t%3mPnP#Gd}hg8GI<_nt20SMVsmmr zU#Lutb$0`08`{?Eud!F=bSAQ*Z4G_?GlB9cX-%LS7L5+ z6FcqVRUwK!0X`2igN+f`GtDV#Rm03J-k=*XMAzySK0P4JWMh;4$th`sHK*B3EwVp- z6>J~;CcKxV8)9UBxz?;}=zV*PeR3lES)%>sCgFWN_-SKuykeT9S$_}X;7Y{h4Dr@? znVUWAlc$OPw9udcrKKb7a!)_KS`GV?d!&!Bx}N}PtE^r}Oyg!2rr_$PR4K^wrSPjR zwV{a*m6Hu92i`{s$?;5sC=GdamJJPMaV+^8yM6#X6pm4iZx{BMli)7M&k6hvz`lbKJ7`N?QM|mPPSQQHYDKSqXxx#_+=9HHm+B^b<0!SAUh#LvfiKFf=IF z`W8bVOWgahm}ya08{5u&rzCf?R_)FAoFPg4OP>*mw8(Be2gRH?H^1PZCGZrpD6tuyegjk@!rs`(toN zf+7VjsvXz0(%Ns$if8Hj=blTOyf<=cM#=T<2%MU}kHYK^-h!u^?ZlI)o9v*65fW4| zjFv?140s`53?@4(gg}H=UuuN8pnZOv(1W|tr(8Q(vBOwU7(YCiv=P6Sq=ZVvs5l~w zhK?^_x5{Rh-awKsXzNWf`b<>`M^ki>koCyqYC^fviH`PUXvr-!S@#na0JOx?n~tf!NU65otJC(WGQtg6LmAi`W-58c9Pugdll zz`lKZMv#W?0}M!I`M{vr zkIKDIe&V#I_vk}p&zv3fASMCiXd#RyJxju)vS}qHo}h}2Pub`tU-_U+Rz_s3*BOoqOB%+hSi9(lH8yvFqzt@@G`Ti{WTN}7vvXhrM4 zgl^pKFB98!ko^*kF<&B|0&}e&1&i@J7~;7BISiF9*PV#q5aFB*DA2+$kNPh9(fH9@ zD7+0(o3DqDU);HwVs`d|6mU^I8dE1)i}E(xxYiH9rBS*0qVM%~=i6(B)VT7(Trpl~ zinI|Zr&#?;0r!=W1}>YO4=x4qjvusKx}1y58pWc{0E^pzD`o4OpO-Ln>s#%^nEY=A z{gHs%1BMDW=tALc>D=UMv_C_xDW)wBrL4<=?=(5F$46E9Yji@0%DR=BW?5S)fkH@u zsg&4#4D?%cF}-os z=7n)qS%#^D>6BtWM;#jkYu<090?HO f9GcMSv>+i6?F+b@lXEo6-N+8z#dJTt^- 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 From 9002688f699aef96bf02c4ed43c54cd79c82b636 Mon Sep 17 00:00:00 2001 From: George Burduli Date: Tue, 13 Aug 2024 11:50:36 +0400 Subject: [PATCH 20/80] Add SVGs to git --- .../ppcp-applepay/assets/images/applepay.svg | 83 +++++++++++++++++++ .../assets/images/googlepay.svg | 36 ++++++++ 2 files changed, 119 insertions(+) create mode 100644 modules/ppcp-applepay/assets/images/applepay.svg create mode 100644 modules/ppcp-googlepay/assets/images/googlepay.svg diff --git a/modules/ppcp-applepay/assets/images/applepay.svg b/modules/ppcp-applepay/assets/images/applepay.svg new file mode 100644 index 000000000..42e09634d --- /dev/null +++ b/modules/ppcp-applepay/assets/images/applepay.svg @@ -0,0 +1,83 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/modules/ppcp-googlepay/assets/images/googlepay.svg b/modules/ppcp-googlepay/assets/images/googlepay.svg new file mode 100644 index 000000000..0abef7bce --- /dev/null +++ b/modules/ppcp-googlepay/assets/images/googlepay.svg @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + From b380350a75f25e84fe6cbe4028f9ac8ca3372d86 Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Tue, 13 Aug 2024 15:10:42 +0200 Subject: [PATCH 21/80] =?UTF-8?q?=E2=9C=A8=20Include=20numeric=20shipping?= =?UTF-8?q?=20cost=20in=20ShippingOptions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ppcp-googlepay/src/Endpoint/UpdatePaymentDataEndpoint.php | 1 + 1 file changed, 1 insertion(+) diff --git a/modules/ppcp-googlepay/src/Endpoint/UpdatePaymentDataEndpoint.php b/modules/ppcp-googlepay/src/Endpoint/UpdatePaymentDataEndpoint.php index 27da5ef48..1ca438c16 100644 --- a/modules/ppcp-googlepay/src/Endpoint/UpdatePaymentDataEndpoint.php +++ b/modules/ppcp-googlepay/src/Endpoint/UpdatePaymentDataEndpoint.php @@ -146,6 +146,7 @@ class UpdatePaymentDataEndpoint { wc_price( (float) $rate->get_cost(), array( 'currency' => get_woocommerce_currency() ) ) ) ), + 'cost' => $rate->get_cost(), ); } From 6b5421a2b1b0c5c3321b39b06d421b49018b24f8 Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Tue, 13 Aug 2024 15:41:10 +0200 Subject: [PATCH 22/80] =?UTF-8?q?=F0=9F=90=9B=20Include=20shipping=20costs?= =?UTF-8?q?=20in=20Google=20Pay=20dialog?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../resources/js/GooglepayButton.js | 98 ++++++++++++++++--- 1 file changed, 84 insertions(+), 14 deletions(-) diff --git a/modules/ppcp-googlepay/resources/js/GooglepayButton.js b/modules/ppcp-googlepay/resources/js/GooglepayButton.js index ac477404b..77c94b3fe 100644 --- a/modules/ppcp-googlepay/resources/js/GooglepayButton.js +++ b/modules/ppcp-googlepay/resources/js/GooglepayButton.js @@ -396,20 +396,21 @@ class GooglepayButton { return; } - switch ( paymentData.callbackTrigger ) { - case 'INITIALIZE': - case 'SHIPPING_ADDRESS': - paymentDataRequestUpdate.newShippingOptionParameters = - updatedData.shipping_options; - paymentDataRequestUpdate.newTransactionInfo = - this.calculateNewTransactionInfo( updatedData ); - break; - case 'SHIPPING_OPTION': - paymentDataRequestUpdate.newTransactionInfo = - this.calculateNewTransactionInfo( updatedData ); - break; + if ( + [ 'INITIALIZE', 'SHIPPING_ADDRESS' ].includes( + paymentData.callbackTrigger + ) + ) { + paymentDataRequestUpdate.newShippingOptionParameters = + updatedData.shipping_options; } + paymentDataRequestUpdate.newTransactionInfo = + this.calculateNewTransactionInfo( + updatedData, + paymentData?.shippingOptionData?.id + ); + resolve( paymentDataRequestUpdate ); } catch ( error ) { console.error( 'Error during onPaymentDataChanged:', error ); @@ -418,6 +419,68 @@ class GooglepayButton { } ); } + /** + * Returns the shipping costs as numeric value. + * + * TODO - Move this to the PaymentButton base class + * + * @param {string} shippingId - The shipping method ID. + * @param {Object} shippingData - The PaymentDataRequest object that contains shipping options. + * @param {Array} shippingData.shippingOptions + * @param {string} shippingData.defaultSelectedOptionId + * + * @return {number} The shipping costs. + */ + getShippingCosts( + shippingId, + { shippingOptions = [], defaultSelectedOptionId = '' } = {} + ) { + if ( ! shippingOptions?.length ) { + this.log( 'Cannot calculate shipping cost: No Shipping Options' ); + return 0; + } + + const findOptionById = ( id ) => + shippingOptions.find( ( option ) => option.id === id ); + + const getValidShippingId = () => { + if ( + 'shipping_option_unselected' === shippingId || + ! findOptionById( shippingId ) + ) { + // Entered on initial call, and when changing the shipping country. + return defaultSelectedOptionId; + } + + return shippingId; + }; + + const currentOption = findOptionById( getValidShippingId() ); + + return currentOption?.cost ? this.toAmount( currentOption.cost ) : 0; + } + + /** + * Converts the provided value to a number with configurable precision. + * + * TODO - Move this to the PaymentButton base class + * + * @param {any} value - The value to convert. + * @param {number} [precision=2] - The number of decimal places. + * @return {number} Always a numeric value with the specified precision. + */ + toAmount( value, precision = 2 ) { + const number = Number( value ); + + if ( isNaN( number ) ) { + return 0; + } + + const multiplier = Math.pow( 10, precision ); + + return Math.round( number * multiplier ) / multiplier; + } + unserviceableShippingAddressError() { return { reason: 'SHIPPING_ADDRESS_UNSERVICEABLE', @@ -426,12 +489,19 @@ class GooglepayButton { }; } - calculateNewTransactionInfo( updatedData ) { + calculateNewTransactionInfo( updatedData, shippingId ) { + const shippingCost = this.getShippingCosts( + shippingId, + updatedData.shipping_options + ); + const subTotal = this.toAmount( updatedData.total_str ); + const totalPrice = shippingCost + subTotal; + return { countryCode: updatedData.country_code, currencyCode: updatedData.currency_code, totalPriceStatus: 'FINAL', - totalPrice: updatedData.total_str, + totalPrice: totalPrice.toFixed( 2 ), }; } From 7946150f5b975ed228cb7ff8fc5b28f8fe5b5921 Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Tue, 13 Aug 2024 18:41:42 +0200 Subject: [PATCH 23/80] =?UTF-8?q?=E2=9C=A8=20New=20TransactionInfo=20DTO?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../resources/js/Context/BaseHandler.js | 15 ++++--- .../resources/js/Context/PayNowHandler.js | 15 ++++--- .../js/Context/SingleProductHandler.js | 15 ++++--- .../resources/js/GooglepayButton.js | 4 +- .../resources/js/Helper/TransactionInfo.js | 43 +++++++++++++++++++ 5 files changed, 72 insertions(+), 20 deletions(-) create mode 100644 modules/ppcp-googlepay/resources/js/Helper/TransactionInfo.js diff --git a/modules/ppcp-googlepay/resources/js/Context/BaseHandler.js b/modules/ppcp-googlepay/resources/js/Context/BaseHandler.js index d61b674a7..014a6f2ce 100644 --- a/modules/ppcp-googlepay/resources/js/Context/BaseHandler.js +++ b/modules/ppcp-googlepay/resources/js/Context/BaseHandler.js @@ -1,5 +1,6 @@ import ErrorHandler from '../../../../ppcp-button/resources/js/modules/ErrorHandler'; import CartActionHandler from '../../../../ppcp-button/resources/js/modules/ActionHandler/CartActionHandler'; +import TransactionInfo from '../Helper/TransactionInfo'; class BaseHandler { constructor( buttonConfig, ppcpConfig, externalHandler ) { @@ -35,12 +36,14 @@ class BaseHandler { // handle script reload const data = result.data; - resolve( { - countryCode: data.country_code, - currencyCode: data.currency_code, - totalPriceStatus: 'FINAL', - totalPrice: data.total_str, - } ); + resolve( + new TransactionInfo( + data.total, + data.currency_code, + data.country_code, + true + ) + ); } ); } ); } diff --git a/modules/ppcp-googlepay/resources/js/Context/PayNowHandler.js b/modules/ppcp-googlepay/resources/js/Context/PayNowHandler.js index 79de6b39d..29e91d39b 100644 --- a/modules/ppcp-googlepay/resources/js/Context/PayNowHandler.js +++ b/modules/ppcp-googlepay/resources/js/Context/PayNowHandler.js @@ -1,6 +1,7 @@ import Spinner from '../../../../ppcp-button/resources/js/modules/Helper/Spinner'; import BaseHandler from './BaseHandler'; import CheckoutActionHandler from '../../../../ppcp-button/resources/js/modules/ActionHandler/CheckoutActionHandler'; +import TransactionInfo from '../Helper/TransactionInfo'; class PayNowHandler extends BaseHandler { validateContext() { @@ -14,12 +15,14 @@ class PayNowHandler extends BaseHandler { return new Promise( async ( resolve, reject ) => { const data = this.ppcpConfig.pay_now; - resolve( { - countryCode: data.country_code, - currencyCode: data.currency_code, - totalPriceStatus: 'FINAL', - totalPrice: data.total_str, - } ); + resolve( + new TransactionInfo( + data.total, + data.currency_code, + data.country_code, + true + ) + ); } ); } diff --git a/modules/ppcp-googlepay/resources/js/Context/SingleProductHandler.js b/modules/ppcp-googlepay/resources/js/Context/SingleProductHandler.js index a8aa6e8bd..6066b9eca 100644 --- a/modules/ppcp-googlepay/resources/js/Context/SingleProductHandler.js +++ b/modules/ppcp-googlepay/resources/js/Context/SingleProductHandler.js @@ -3,6 +3,7 @@ import SimulateCart from '../../../../ppcp-button/resources/js/modules/Helper/Si import ErrorHandler from '../../../../ppcp-button/resources/js/modules/ErrorHandler'; import UpdateCart from '../../../../ppcp-button/resources/js/modules/Helper/UpdateCart'; import BaseHandler from './BaseHandler'; +import TransactionInfo from '../Helper/TransactionInfo'; class SingleProductHandler extends BaseHandler { validateContext() { @@ -42,12 +43,14 @@ class SingleProductHandler extends BaseHandler { this.ppcpConfig.ajax.simulate_cart.endpoint, this.ppcpConfig.ajax.simulate_cart.nonce ).simulate( ( data ) => { - resolve( { - countryCode: data.country_code, - currencyCode: data.currency_code, - totalPriceStatus: 'FINAL', - totalPrice: data.total_str, - } ); + resolve( + new TransactionInfo( + data.total, + data.currency_code, + data.country_code, + true + ) + ); }, products ); } ); } diff --git a/modules/ppcp-googlepay/resources/js/GooglepayButton.js b/modules/ppcp-googlepay/resources/js/GooglepayButton.js index 77c94b3fe..cd468b93e 100644 --- a/modules/ppcp-googlepay/resources/js/GooglepayButton.js +++ b/modules/ppcp-googlepay/resources/js/GooglepayButton.js @@ -331,7 +331,7 @@ class GooglepayButton { const paymentDataRequest = Object.assign( {}, baseRequest ); paymentDataRequest.allowedPaymentMethods = googlePayConfig.allowedPaymentMethods; - paymentDataRequest.transactionInfo = this.transactionInfo; + paymentDataRequest.transactionInfo = this.transactionInfo.dataObject; paymentDataRequest.merchantInfo = googlePayConfig.merchantInfo; if ( @@ -376,7 +376,7 @@ class GooglepayButton { const updatedData = await new UpdatePaymentData( this.buttonConfig.ajax.update_payment_data ).update( paymentData ); - const transactionInfo = this.transactionInfo; + const transactionInfo = this.transactionInfo.dataObject; this.log( 'onPaymentDataChanged:updatedData', updatedData ); this.log( diff --git a/modules/ppcp-googlepay/resources/js/Helper/TransactionInfo.js b/modules/ppcp-googlepay/resources/js/Helper/TransactionInfo.js new file mode 100644 index 000000000..66e198374 --- /dev/null +++ b/modules/ppcp-googlepay/resources/js/Helper/TransactionInfo.js @@ -0,0 +1,43 @@ +export default class TransactionInfo { + #country = ''; + #currency = ''; + #isFinal = false; + #amount = 0; + + constructor( amount, currency, country, isFinal ) { + this.#country = country; + this.#currency = currency; + this.#isFinal = isFinal; + + this.amount = amount; + } + + set amount( newAmount ) { + this.#amount = Number( newAmount ) || 0; + } + + get currencyCode() { + return this.#currency; + } + + get countryCode() { + return this.#country; + } + + get totalPriceStatus() { + return this.#isFinal ? 'FINAL' : 'DRAFT'; + } + + get totalPrice() { + return this.#amount.toFixed( 2 ); + } + + get dataObject() { + return { + countryCode: this.countryCode, + currencyCode: this.currencyCode, + totalPriceStatus: this.totalPriceStatus, + totalPrice: this.totalPrice, + }; + } +} From 611ab4c5a7077bababc255ab9071e4705ff4c3c5 Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Tue, 13 Aug 2024 18:53:39 +0200 Subject: [PATCH 24/80] =?UTF-8?q?=E2=9C=A8=20New=20TransactionInfo=20DTO?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../resources/js/Context/BaseHandler.js | 15 +++++++-------- .../resources/js/Context/PayNowHandler.js | 14 +++++++------- .../resources/js/Context/SingleProductHandler.js | 14 +++++++------- 3 files changed, 21 insertions(+), 22 deletions(-) diff --git a/modules/ppcp-googlepay/resources/js/Context/BaseHandler.js b/modules/ppcp-googlepay/resources/js/Context/BaseHandler.js index 014a6f2ce..eebfd7518 100644 --- a/modules/ppcp-googlepay/resources/js/Context/BaseHandler.js +++ b/modules/ppcp-googlepay/resources/js/Context/BaseHandler.js @@ -35,15 +35,14 @@ class BaseHandler { // handle script reload const data = result.data; - - resolve( - new TransactionInfo( - data.total, - data.currency_code, - data.country_code, - true - ) + const transaction = new TransactionInfo( + data.total, + data.currency_code, + data.country_code, + true ); + + resolve( transaction ); } ); } ); } diff --git a/modules/ppcp-googlepay/resources/js/Context/PayNowHandler.js b/modules/ppcp-googlepay/resources/js/Context/PayNowHandler.js index 29e91d39b..9d35da16e 100644 --- a/modules/ppcp-googlepay/resources/js/Context/PayNowHandler.js +++ b/modules/ppcp-googlepay/resources/js/Context/PayNowHandler.js @@ -15,14 +15,14 @@ class PayNowHandler extends BaseHandler { return new Promise( async ( resolve, reject ) => { const data = this.ppcpConfig.pay_now; - resolve( - new TransactionInfo( - data.total, - data.currency_code, - data.country_code, - true - ) + const transaction = new TransactionInfo( + data.total, + data.currency_code, + data.country_code, + true ); + + resolve( transaction ); } ); } diff --git a/modules/ppcp-googlepay/resources/js/Context/SingleProductHandler.js b/modules/ppcp-googlepay/resources/js/Context/SingleProductHandler.js index 6066b9eca..61b7997c2 100644 --- a/modules/ppcp-googlepay/resources/js/Context/SingleProductHandler.js +++ b/modules/ppcp-googlepay/resources/js/Context/SingleProductHandler.js @@ -43,14 +43,14 @@ class SingleProductHandler extends BaseHandler { this.ppcpConfig.ajax.simulate_cart.endpoint, this.ppcpConfig.ajax.simulate_cart.nonce ).simulate( ( data ) => { - resolve( - new TransactionInfo( - data.total, - data.currency_code, - data.country_code, - true - ) + const transaction = new TransactionInfo( + data.total, + data.currency_code, + data.country_code, + true ); + + resolve( transaction ); }, products ); } ); } From 3b92eaac813325f358da9be4072ce9a7110f188a Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Tue, 13 Aug 2024 19:31:51 +0200 Subject: [PATCH 25/80] =?UTF-8?q?=E2=9C=A8=20Store=20shipping=20cost=20in?= =?UTF-8?q?=20the=20DTO?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/Endpoint/SimulateCartEndpoint.php | 5 ++-- .../resources/js/Context/BaseHandler.js | 1 + .../resources/js/Context/PayNowHandler.js | 1 + .../js/Context/SingleProductHandler.js | 1 + .../resources/js/Helper/TransactionInfo.js | 24 ++++++++++++++++--- .../Endpoint/UpdatePaymentDataEndpoint.php | 5 ++-- 6 files changed, 30 insertions(+), 7 deletions(-) diff --git a/modules/ppcp-button/src/Endpoint/SimulateCartEndpoint.php b/modules/ppcp-button/src/Endpoint/SimulateCartEndpoint.php index 36069e463..a96302e65 100644 --- a/modules/ppcp-button/src/Endpoint/SimulateCartEndpoint.php +++ b/modules/ppcp-button/src/Endpoint/SimulateCartEndpoint.php @@ -85,7 +85,8 @@ class SimulateCartEndpoint extends AbstractCartEndpoint { $this->add_products( $products ); $this->cart->calculate_totals(); - $total = (float) $this->cart->get_total( 'numeric' ); + $total = (float) $this->cart->get_total( 'numeric' ); + $shipping_fee = (float) $this->cart->get_shipping_total(); $this->restore_real_cart(); @@ -113,7 +114,7 @@ class SimulateCartEndpoint extends AbstractCartEndpoint { wp_send_json_success( array( 'total' => $total, - 'total_str' => ( new Money( $total, $currency_code ) )->value_str(), + 'shipping_fee' => $shipping_fee, 'currency_code' => $currency_code, 'country_code' => $shop_country_code, 'funding' => array( diff --git a/modules/ppcp-googlepay/resources/js/Context/BaseHandler.js b/modules/ppcp-googlepay/resources/js/Context/BaseHandler.js index eebfd7518..a9297fca6 100644 --- a/modules/ppcp-googlepay/resources/js/Context/BaseHandler.js +++ b/modules/ppcp-googlepay/resources/js/Context/BaseHandler.js @@ -37,6 +37,7 @@ class BaseHandler { const data = result.data; const transaction = new TransactionInfo( data.total, + data.shipping_fee, data.currency_code, data.country_code, true diff --git a/modules/ppcp-googlepay/resources/js/Context/PayNowHandler.js b/modules/ppcp-googlepay/resources/js/Context/PayNowHandler.js index 9d35da16e..8516d8809 100644 --- a/modules/ppcp-googlepay/resources/js/Context/PayNowHandler.js +++ b/modules/ppcp-googlepay/resources/js/Context/PayNowHandler.js @@ -17,6 +17,7 @@ class PayNowHandler extends BaseHandler { const transaction = new TransactionInfo( data.total, + data.shipping_fee, data.currency_code, data.country_code, true diff --git a/modules/ppcp-googlepay/resources/js/Context/SingleProductHandler.js b/modules/ppcp-googlepay/resources/js/Context/SingleProductHandler.js index 61b7997c2..3502e7701 100644 --- a/modules/ppcp-googlepay/resources/js/Context/SingleProductHandler.js +++ b/modules/ppcp-googlepay/resources/js/Context/SingleProductHandler.js @@ -45,6 +45,7 @@ class SingleProductHandler extends BaseHandler { ).simulate( ( data ) => { const transaction = new TransactionInfo( data.total, + data.shipping_fee, data.currency_code, data.country_code, true diff --git a/modules/ppcp-googlepay/resources/js/Helper/TransactionInfo.js b/modules/ppcp-googlepay/resources/js/Helper/TransactionInfo.js index 66e198374..012b024b5 100644 --- a/modules/ppcp-googlepay/resources/js/Helper/TransactionInfo.js +++ b/modules/ppcp-googlepay/resources/js/Helper/TransactionInfo.js @@ -3,19 +3,35 @@ export default class TransactionInfo { #currency = ''; #isFinal = false; #amount = 0; + #shippingFee = 0; - constructor( amount, currency, country, isFinal ) { + constructor( total, shippingFee, currency, country, isFinal ) { this.#country = country; this.#currency = currency; this.#isFinal = isFinal; - this.amount = amount; + this.shippingFee = shippingFee; + this.amount = total - shippingFee; } set amount( newAmount ) { this.#amount = Number( newAmount ) || 0; } + set shippingFee( newCost ) { + this.#shippingFee = Number( newCost ) || 0; + } + + set total( newTotal ) { + newTotal = Number( newTotal ) || 0; + + if ( ! newTotal ) { + return; + } + + this.#amount = newTotal - this.#shippingFee; + } + get currencyCode() { return this.#currency; } @@ -29,7 +45,9 @@ export default class TransactionInfo { } get totalPrice() { - return this.#amount.toFixed( 2 ); + const total = this.#amount + this.#shippingFee; + + return total.toFixed( 2 ); } get dataObject() { diff --git a/modules/ppcp-googlepay/src/Endpoint/UpdatePaymentDataEndpoint.php b/modules/ppcp-googlepay/src/Endpoint/UpdatePaymentDataEndpoint.php index 1ca438c16..e489bc771 100644 --- a/modules/ppcp-googlepay/src/Endpoint/UpdatePaymentDataEndpoint.php +++ b/modules/ppcp-googlepay/src/Endpoint/UpdatePaymentDataEndpoint.php @@ -90,7 +90,8 @@ class UpdatePaymentDataEndpoint { WC()->cart->calculate_fees(); WC()->cart->calculate_totals(); - $total = (float) WC()->cart->get_total( 'numeric' ); + $total = (float) WC()->cart->get_total( 'numeric' ); + $shipping_fee = (float) WC()->cart->get_shipping_total(); // Shop settings. $base_location = wc_get_base_location(); @@ -100,7 +101,7 @@ class UpdatePaymentDataEndpoint { wp_send_json_success( array( 'total' => $total, - 'total_str' => ( new Money( $total, $currency_code ) )->value_str(), + 'shipping_fee' => $shipping_fee, 'currency_code' => $currency_code, 'country_code' => $shop_country_code, 'shipping_options' => $this->get_shipping_options(), From 179c1d6831adf8bf81a967f3242bd6ce777dd790 Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Tue, 13 Aug 2024 19:34:13 +0200 Subject: [PATCH 26/80] =?UTF-8?q?=F0=9F=94=A5=20Minor=20clean-up?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../resources/js/GooglepayButton.js | 25 ++----------------- 1 file changed, 2 insertions(+), 23 deletions(-) diff --git a/modules/ppcp-googlepay/resources/js/GooglepayButton.js b/modules/ppcp-googlepay/resources/js/GooglepayButton.js index cd468b93e..6ad65c6d5 100644 --- a/modules/ppcp-googlepay/resources/js/GooglepayButton.js +++ b/modules/ppcp-googlepay/resources/js/GooglepayButton.js @@ -386,7 +386,6 @@ class GooglepayButton { updatedData.country_code = transactionInfo.countryCode; updatedData.currency_code = transactionInfo.currencyCode; - updatedData.total_str = transactionInfo.totalPrice; // Handle unserviceable address. if ( ! updatedData.shipping_options?.shippingOptions?.length ) { @@ -425,7 +424,8 @@ class GooglepayButton { * TODO - Move this to the PaymentButton base class * * @param {string} shippingId - The shipping method ID. - * @param {Object} shippingData - The PaymentDataRequest object that contains shipping options. + * @param {Object} shippingData - The PaymentDataRequest object that + * contains shipping options. * @param {Array} shippingData.shippingOptions * @param {string} shippingData.defaultSelectedOptionId * @@ -460,27 +460,6 @@ class GooglepayButton { return currentOption?.cost ? this.toAmount( currentOption.cost ) : 0; } - /** - * Converts the provided value to a number with configurable precision. - * - * TODO - Move this to the PaymentButton base class - * - * @param {any} value - The value to convert. - * @param {number} [precision=2] - The number of decimal places. - * @return {number} Always a numeric value with the specified precision. - */ - toAmount( value, precision = 2 ) { - const number = Number( value ); - - if ( isNaN( number ) ) { - return 0; - } - - const multiplier = Math.pow( 10, precision ); - - return Math.round( number * multiplier ) / multiplier; - } - unserviceableShippingAddressError() { return { reason: 'SHIPPING_ADDRESS_UNSERVICEABLE', From 8f191c2d490d6b68bf790eca28c7203ca1d79e60 Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Tue, 13 Aug 2024 19:36:37 +0200 Subject: [PATCH 27/80] =?UTF-8?q?=F0=9F=90=9B=20Fix=20the=20price=20calcul?= =?UTF-8?q?ation=20on=20product=20page?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../resources/js/GooglepayButton.js | 37 +++++++++---------- 1 file changed, 17 insertions(+), 20 deletions(-) diff --git a/modules/ppcp-googlepay/resources/js/GooglepayButton.js b/modules/ppcp-googlepay/resources/js/GooglepayButton.js index 6ad65c6d5..b0272cc9b 100644 --- a/modules/ppcp-googlepay/resources/js/GooglepayButton.js +++ b/modules/ppcp-googlepay/resources/js/GooglepayButton.js @@ -3,6 +3,7 @@ import { setEnabled } from '../../../ppcp-button/resources/js/modules/Helper/But import widgetBuilder from '../../../ppcp-button/resources/js/modules/Renderer/WidgetBuilder'; import UpdatePaymentData from './Helper/UpdatePaymentData'; import { apmButtonsInit } from '../../../ppcp-button/resources/js/modules/Helper/ApmButtons'; +import TransactionInfo from './Helper/TransactionInfo'; class GooglepayButton { constructor( @@ -376,7 +377,7 @@ class GooglepayButton { const updatedData = await new UpdatePaymentData( this.buttonConfig.ajax.update_payment_data ).update( paymentData ); - const transactionInfo = this.transactionInfo.dataObject; + const transactionInfo = this.transactionInfo; this.log( 'onPaymentDataChanged:updatedData', updatedData ); this.log( @@ -404,12 +405,14 @@ class GooglepayButton { updatedData.shipping_options; } - paymentDataRequestUpdate.newTransactionInfo = - this.calculateNewTransactionInfo( - updatedData, - paymentData?.shippingOptionData?.id + transactionInfo.shippingFee = this.getShippingCosts( + paymentData?.shippingOptionData?.id, + updatedData.shipping_options ); + paymentDataRequestUpdate.newTransactionInfo = + this.calculateNewTransactionInfo( transactionInfo ); + resolve( paymentDataRequestUpdate ); } catch ( error ) { console.error( 'Error during onPaymentDataChanged:', error ); @@ -457,7 +460,7 @@ class GooglepayButton { const currentOption = findOptionById( getValidShippingId() ); - return currentOption?.cost ? this.toAmount( currentOption.cost ) : 0; + return Number( currentOption?.cost ) || 0; } unserviceableShippingAddressError() { @@ -468,20 +471,14 @@ class GooglepayButton { }; } - calculateNewTransactionInfo( updatedData, shippingId ) { - const shippingCost = this.getShippingCosts( - shippingId, - updatedData.shipping_options - ); - const subTotal = this.toAmount( updatedData.total_str ); - const totalPrice = shippingCost + subTotal; - - return { - countryCode: updatedData.country_code, - currencyCode: updatedData.currency_code, - totalPriceStatus: 'FINAL', - totalPrice: totalPrice.toFixed( 2 ), - }; + /** + * Recalculates and returns the plain transaction info object. + * + * @param {TransactionInfo} transactionInfo - Internal transactionInfo instance. + * @return {{totalPrice: string, countryCode: string, totalPriceStatus: string, currencyCode: string}} Updated details. + */ + calculateNewTransactionInfo( transactionInfo ) { + return transactionInfo.dataObject; } //------------------------ From f2301a467478c60d0496ac0cea2d99a09dde3e4c Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Tue, 13 Aug 2024 20:02:49 +0200 Subject: [PATCH 28/80] =?UTF-8?q?=F0=9F=90=9B=20Fix=20price=20calculation?= =?UTF-8?q?=20on=20pages=20with=20cart?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This covers: Mini-Cart, Classic Cart, Block Cart, Classic Checkout, Block Checkout, Pay-Now page --- .../resources/js/GooglepayButton.js | 28 +++++++++-- .../resources/js/Helper/TransactionInfo.js | 47 +++++++++++++++---- 2 files changed, 62 insertions(+), 13 deletions(-) diff --git a/modules/ppcp-googlepay/resources/js/GooglepayButton.js b/modules/ppcp-googlepay/resources/js/GooglepayButton.js index b0272cc9b..9f8524997 100644 --- a/modules/ppcp-googlepay/resources/js/GooglepayButton.js +++ b/modules/ppcp-googlepay/resources/js/GooglepayButton.js @@ -6,6 +6,11 @@ import { apmButtonsInit } from '../../../ppcp-button/resources/js/modules/Helper import TransactionInfo from './Helper/TransactionInfo'; class GooglepayButton { + /** + * @type {TransactionInfo} + */ + transactionInfo; + constructor( context, externalHandler, @@ -379,6 +384,16 @@ class GooglepayButton { ).update( paymentData ); const transactionInfo = this.transactionInfo; + // Check, if the current context uses the WC cart. + const hasRealCart = [ + 'checkout-block', + 'checkout', + 'cart-block', + 'cart', + 'mini-cart', + 'pay-now', + ].includes( this.context ); + this.log( 'onPaymentDataChanged:updatedData', updatedData ); this.log( 'onPaymentDataChanged:transactionInfo', @@ -405,10 +420,17 @@ class GooglepayButton { updatedData.shipping_options; } - transactionInfo.shippingFee = this.getShippingCosts( - paymentData?.shippingOptionData?.id, - updatedData.shipping_options + if ( updatedData.total && hasRealCart ) { + transactionInfo.setTotal( + updatedData.total, + updatedData.shipping_fee ); + } else { + transactionInfo.shippingFee = this.getShippingCosts( + paymentData?.shippingOptionData?.id, + updatedData.shipping_options + ); + } paymentDataRequestUpdate.newTransactionInfo = this.calculateNewTransactionInfo( transactionInfo ); diff --git a/modules/ppcp-googlepay/resources/js/Helper/TransactionInfo.js b/modules/ppcp-googlepay/resources/js/Helper/TransactionInfo.js index 012b024b5..93ffe2e6e 100644 --- a/modules/ppcp-googlepay/resources/js/Helper/TransactionInfo.js +++ b/modules/ppcp-googlepay/resources/js/Helper/TransactionInfo.js @@ -15,21 +15,19 @@ export default class TransactionInfo { } set amount( newAmount ) { - this.#amount = Number( newAmount ) || 0; + this.#amount = this.toAmount( newAmount ); + } + + get amount() { + return this.#amount; } set shippingFee( newCost ) { - this.#shippingFee = Number( newCost ) || 0; + this.#shippingFee = this.toAmount( newCost ); } - set total( newTotal ) { - newTotal = Number( newTotal ) || 0; - - if ( ! newTotal ) { - return; - } - - this.#amount = newTotal - this.#shippingFee; + get shippingFee() { + return this.#shippingFee; } get currencyCode() { @@ -58,4 +56,33 @@ export default class TransactionInfo { totalPrice: this.totalPrice, }; } + + /** + * Converts the value to a number and rounds to a precision of 2 digits. + * + * @param {any} value - The value to sanitize. + * @return {number} Numeric value. + */ + toAmount( value ) { + value = Number( value ) || 0; + return Math.round( value * 100 ) / 100; + } + + setTotal( totalPrice, shippingFee ) { + totalPrice = this.toAmount( totalPrice ); + + if ( totalPrice ) { + this.shippingFee = shippingFee; + this.amount = totalPrice - this.shippingFee; + + console.log( + 'New Total Price:', + totalPrice, + '=', + this.amount, + '+', + this.shippingFee + ); + } + } } From ac98600b8fc085e468ed157afc2be16efa958601 Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Tue, 13 Aug 2024 20:23:51 +0200 Subject: [PATCH 29/80] =?UTF-8?q?=F0=9F=94=A5=20Simplify=20code,=20remove?= =?UTF-8?q?=20console=20output?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../resources/js/Context/BaseHandler.js | 3 +-- .../resources/js/Context/PayNowHandler.js | 3 +-- .../js/Context/SingleProductHandler.js | 3 +-- .../resources/js/GooglepayButton.js | 4 ++-- .../resources/js/Helper/TransactionInfo.js | 21 +++---------------- 5 files changed, 8 insertions(+), 26 deletions(-) diff --git a/modules/ppcp-googlepay/resources/js/Context/BaseHandler.js b/modules/ppcp-googlepay/resources/js/Context/BaseHandler.js index a9297fca6..d49bee615 100644 --- a/modules/ppcp-googlepay/resources/js/Context/BaseHandler.js +++ b/modules/ppcp-googlepay/resources/js/Context/BaseHandler.js @@ -39,8 +39,7 @@ class BaseHandler { data.total, data.shipping_fee, data.currency_code, - data.country_code, - true + data.country_code ); resolve( transaction ); diff --git a/modules/ppcp-googlepay/resources/js/Context/PayNowHandler.js b/modules/ppcp-googlepay/resources/js/Context/PayNowHandler.js index 8516d8809..81d60b078 100644 --- a/modules/ppcp-googlepay/resources/js/Context/PayNowHandler.js +++ b/modules/ppcp-googlepay/resources/js/Context/PayNowHandler.js @@ -19,8 +19,7 @@ class PayNowHandler extends BaseHandler { data.total, data.shipping_fee, data.currency_code, - data.country_code, - true + data.country_code ); resolve( transaction ); diff --git a/modules/ppcp-googlepay/resources/js/Context/SingleProductHandler.js b/modules/ppcp-googlepay/resources/js/Context/SingleProductHandler.js index 3502e7701..670b9a7c0 100644 --- a/modules/ppcp-googlepay/resources/js/Context/SingleProductHandler.js +++ b/modules/ppcp-googlepay/resources/js/Context/SingleProductHandler.js @@ -47,8 +47,7 @@ class SingleProductHandler extends BaseHandler { data.total, data.shipping_fee, data.currency_code, - data.country_code, - true + data.country_code ); resolve( transaction ); diff --git a/modules/ppcp-googlepay/resources/js/GooglepayButton.js b/modules/ppcp-googlepay/resources/js/GooglepayButton.js index 9f8524997..d626a172b 100644 --- a/modules/ppcp-googlepay/resources/js/GooglepayButton.js +++ b/modules/ppcp-googlepay/resources/js/GooglepayButton.js @@ -337,7 +337,7 @@ class GooglepayButton { const paymentDataRequest = Object.assign( {}, baseRequest ); paymentDataRequest.allowedPaymentMethods = googlePayConfig.allowedPaymentMethods; - paymentDataRequest.transactionInfo = this.transactionInfo.dataObject; + paymentDataRequest.transactionInfo = this.transactionInfo.finalObject; paymentDataRequest.merchantInfo = googlePayConfig.merchantInfo; if ( @@ -500,7 +500,7 @@ class GooglepayButton { * @return {{totalPrice: string, countryCode: string, totalPriceStatus: string, currencyCode: string}} Updated details. */ calculateNewTransactionInfo( transactionInfo ) { - return transactionInfo.dataObject; + return transactionInfo.finalObject; } //------------------------ diff --git a/modules/ppcp-googlepay/resources/js/Helper/TransactionInfo.js b/modules/ppcp-googlepay/resources/js/Helper/TransactionInfo.js index 93ffe2e6e..9216ad7c9 100644 --- a/modules/ppcp-googlepay/resources/js/Helper/TransactionInfo.js +++ b/modules/ppcp-googlepay/resources/js/Helper/TransactionInfo.js @@ -1,14 +1,12 @@ export default class TransactionInfo { #country = ''; #currency = ''; - #isFinal = false; #amount = 0; #shippingFee = 0; - constructor( total, shippingFee, currency, country, isFinal ) { + constructor( total, shippingFee, currency, country ) { this.#country = country; this.#currency = currency; - this.#isFinal = isFinal; this.shippingFee = shippingFee; this.amount = total - shippingFee; @@ -38,21 +36,17 @@ export default class TransactionInfo { return this.#country; } - get totalPriceStatus() { - return this.#isFinal ? 'FINAL' : 'DRAFT'; - } - get totalPrice() { const total = this.#amount + this.#shippingFee; return total.toFixed( 2 ); } - get dataObject() { + get finalObject() { return { countryCode: this.countryCode, currencyCode: this.currencyCode, - totalPriceStatus: this.totalPriceStatus, + totalPriceStatus: 'FINAL', totalPrice: this.totalPrice, }; } @@ -74,15 +68,6 @@ export default class TransactionInfo { if ( totalPrice ) { this.shippingFee = shippingFee; this.amount = totalPrice - this.shippingFee; - - console.log( - 'New Total Price:', - totalPrice, - '=', - this.amount, - '+', - this.shippingFee - ); } } } From 00e29597002c7aed3add7b863a97b0e7e392ab69 Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Fri, 16 Aug 2024 15:41:05 +0200 Subject: [PATCH 30/80] =?UTF-8?q?=E2=9C=A8=20Get=20payee=20details=20witho?= =?UTF-8?q?ut=20shipping=20callback?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../resources/js/modules/Helper/PayerData.js | 168 +++++++++---- .../resources/js/GooglepayButton.js | 230 ++++++++++-------- 2 files changed, 257 insertions(+), 141 deletions(-) diff --git a/modules/ppcp-button/resources/js/modules/Helper/PayerData.js b/modules/ppcp-button/resources/js/modules/Helper/PayerData.js index b9b84d14f..da7eca4d6 100644 --- a/modules/ppcp-button/resources/js/modules/Helper/PayerData.js +++ b/modules/ppcp-button/resources/js/modules/Helper/PayerData.js @@ -1,59 +1,135 @@ +/** + * Name details. + * + * @typedef {Object} NameDetails + * @property {?string} given_name - First name, e.g. "John". + * @property {?string} surname - Last name, e.g. "Doe". + */ + +/** + * Postal address details. + * + * @typedef {Object} AddressDetails + * @property {?string} country_code - Country code (2-letter). + * @property {?string} address_line_1 - Address details, line 1 (street, house number). + * @property {?string} address_line_2 - Address details, line 2. + * @property {?string} admin_area_1 - State or region. + * @property {?string} admin_area_2 - State or region. + * @property {?string} postal_code - Zip code. + */ + +/** + * Phone details. + * + * @typedef {Object} PhoneDetails + * @property {?string} phone_type - Type, usually 'HOME' + * @property {?{national_number: string}} phone_number - Phone number details. + */ + +/** + * Payer details. + * + * @typedef {Object} PayerDetails + * @property {?string} email_address - Email address for billing communication. + * @property {?PhoneDetails} phone - Phone number for billing communication. + * @property {?NameDetails} name - Payer's name. + * @property {?AddressDetails} address - Postal billing address. + */ + +// Map checkout fields to PayerData object properties. +const FIELD_MAP = { + '#billing_email': [ 'email_address' ], + '#billing_last_name': [ 'name', 'surname' ], + '#billing_first_name': [ 'name', 'given_name' ], + '#billing_country': [ 'address', 'country_code' ], + '#billing_address_1': [ 'address', 'address_line_1' ], + '#billing_address_2': [ 'address', 'address_line_2' ], + '#billing_state': [ 'address', 'admin_area_1' ], + '#billing_city': [ 'address', 'admin_area_2' ], + '#billing_postcode': [ 'address', 'postal_code' ], + '#billing_phone': [ 'phone' ], +}; + +/** + * Returns billing details from the checkout form or global JS object. + * + * @return {?PayerDetails} Full billing details, or null on failure. + */ export const payerData = () => { - const payer = PayPalCommerceGateway.payer; + const payer = window.PayPalCommerceGateway?.payer; if ( ! payer ) { return null; } - const phone = - document.querySelector( '#billing_phone' ) || - typeof payer.phone !== 'undefined' - ? { - phone_type: 'HOME', - phone_number: { - national_number: document.querySelector( - '#billing_phone' - ) - ? document.querySelector( '#billing_phone' ).value - : payer.phone.phone_number.national_number, - }, - } - : null; - const payerData = { - email_address: document.querySelector( '#billing_email' ) - ? document.querySelector( '#billing_email' ).value - : payer.email_address, + const getElementValue = ( selector ) => + document.querySelector( selector )?.value; + + // Initialize data with existing payer values. + const data = { + email_address: payer.email_address, + phone: payer.phone, name: { - surname: document.querySelector( '#billing_last_name' ) - ? document.querySelector( '#billing_last_name' ).value - : payer.name.surname, - given_name: document.querySelector( '#billing_first_name' ) - ? document.querySelector( '#billing_first_name' ).value - : payer.name.given_name, + surname: payer.name?.surname, + given_name: payer.name?.given_name, }, address: { - country_code: document.querySelector( '#billing_country' ) - ? document.querySelector( '#billing_country' ).value - : payer.address.country_code, - address_line_1: document.querySelector( '#billing_address_1' ) - ? document.querySelector( '#billing_address_1' ).value - : payer.address.address_line_1, - address_line_2: document.querySelector( '#billing_address_2' ) - ? document.querySelector( '#billing_address_2' ).value - : payer.address.address_line_2, - admin_area_1: document.querySelector( '#billing_state' ) - ? document.querySelector( '#billing_state' ).value - : payer.address.admin_area_1, - admin_area_2: document.querySelector( '#billing_city' ) - ? document.querySelector( '#billing_city' ).value - : payer.address.admin_area_2, - postal_code: document.querySelector( '#billing_postcode' ) - ? document.querySelector( '#billing_postcode' ).value - : payer.address.postal_code, + country_code: payer.address?.country_code, + address_line_1: payer.address?.address_line_1, + address_line_2: payer.address?.address_line_2, + admin_area_1: payer.address?.admin_area_1, + admin_area_2: payer.address?.admin_area_2, + postal_code: payer.address?.postal_code, }, }; - if ( phone ) { - payerData.phone = phone; + // Update data with DOM values where they exist. + Object.entries( FIELD_MAP ).forEach( ( [ selector, path ] ) => { + const value = getElementValue( selector ); + if ( value ) { + let current = data; + path.slice( 0, -1 ).forEach( ( key ) => { + current = current[ key ] = current[ key ] || {}; + } ); + current[ path[ path.length - 1 ] ] = value; + } + } ); + + // Handle phone separately due to its nested structure. + const phoneNumber = data.phone; + if ( phoneNumber && typeof phoneNumber === 'string' ) { + data.phone = { + phone_type: 'HOME', + phone_number: { national_number: phoneNumber }, + }; } - return payerData; + + return data; +}; + +/** + * Updates the DOM with specific payer details. + * + * @param {PayerDetails} newData - New payer details. + */ +export const setPayerData = ( newData ) => { + const setValue = ( path, field, value ) => { + if ( null === value || undefined === value || ! field ) { + return; + } + + if ( path[ 0 ] === 'phone' && typeof value === 'object' ) { + value = value.phone_number?.national_number; + } + + if ( field.value !== value ) { + field.value = value; + } + }; + + Object.entries( FIELD_MAP ).forEach( ( [ selector, path ] ) => { + const value = path.reduce( ( obj, key ) => obj?.[ key ], newData ); + const element = document.querySelector( selector ); + + setValue( path, element, value ); + } ); }; diff --git a/modules/ppcp-googlepay/resources/js/GooglepayButton.js b/modules/ppcp-googlepay/resources/js/GooglepayButton.js index 4b267c4e7..a9bfd693e 100644 --- a/modules/ppcp-googlepay/resources/js/GooglepayButton.js +++ b/modules/ppcp-googlepay/resources/js/GooglepayButton.js @@ -6,6 +6,7 @@ import PaymentButton from '../../../ppcp-button/resources/js/modules/Renderer/Pa import widgetBuilder from '../../../ppcp-button/resources/js/modules/Renderer/WidgetBuilder'; import UpdatePaymentData from './Helper/UpdatePaymentData'; import { PaymentMethods } from '../../../ppcp-button/resources/js/modules/Helper/CheckoutMethodState'; +import { setPayerData } from '../../../ppcp-button/resources/js/modules/Helper/PayerData'; /** * Plugin-specific styling. @@ -39,11 +40,17 @@ import { PaymentMethods } from '../../../ppcp-button/resources/js/modules/Helper * * @see https://developers.google.com/pay/api/web/reference/client * @typedef {Object} PaymentsClient - * @property {Function} createButton - The convenience method is used to generate a Google Pay payment button styled with the latest Google Pay branding for insertion into a webpage. - * @property {Function} isReadyToPay - Use the isReadyToPay(isReadyToPayRequest) method to determine a user's ability to return a form of payment from the Google Pay API. - * @property {Function} loadPaymentData - This method presents a Google Pay payment sheet that allows selection of a payment method and optionally configured parameters - * @property {Function} onPaymentAuthorized - This method is called when a payment is authorized in the payment sheet. - * @property {Function} onPaymentDataChanged - This method handles payment data changes in the payment sheet such as shipping address and shipping options. + * @property {Function} createButton - The convenience method is used to + * generate a Google Pay payment button styled with the latest Google Pay branding for + * insertion into a webpage. + * @property {Function} isReadyToPay - Use the isReadyToPay(isReadyToPayRequest) + * method to determine a user's ability to return a form of payment from the Google Pay API. + * @property {(Object) => Promise} loadPaymentData - This method presents a Google Pay payment + * sheet that allows selection of a payment method and optionally configured parameters + * @property {Function} onPaymentAuthorized - This method is called when a payment is + * authorized in the payment sheet. + * @property {Function} onPaymentDataChanged - This method handles payment data changes + * in the payment sheet such as shipping address and shipping options. */ /** @@ -53,12 +60,18 @@ import { PaymentMethods } from '../../../ppcp-button/resources/js/modules/Helper * @typedef {Object} TransactionInfo * @property {string} currencyCode - Required. The ISO 4217 alphabetic currency code. * @property {string} countryCode - Optional. required for EEA countries, - * @property {string} transactionId - Optional. A unique ID that identifies a facilitation attempt. Highly encouraged for troubleshooting. - * @property {string} totalPriceStatus - Required. [ESTIMATED|FINAL] The status of the total price used: - * @property {string} totalPrice - Required. Total monetary value of the transaction with an optional decimal precision of two decimal places. - * @property {Array} displayItems - Optional. A list of cart items shown in the payment sheet (e.g. subtotals, sales taxes, shipping charges, discounts etc.). - * @property {string} totalPriceLabel - Optional. Custom label for the total price within the display items. - * @property {string} checkoutOption - Optional. Affects the submit button text displayed in the Google Pay payment sheet. + * @property {string} transactionId - Optional. A unique ID that identifies a facilitation + * attempt. Highly encouraged for troubleshooting. + * @property {string} totalPriceStatus - Required. [ESTIMATED|FINAL] The status of the total price + * used: + * @property {string} totalPrice - Required. Total monetary value of the transaction with an + * optional decimal precision of two decimal places. + * @property {Array} displayItems - Optional. A list of cart items shown in the payment sheet + * (e.g. subtotals, sales taxes, shipping charges, discounts etc.). + * @property {string} totalPriceLabel - Optional. Custom label for the total price within the + * display items. + * @property {string} checkoutOption - Optional. Affects the submit button text displayed in the + * Google Pay payment sheet. */ class GooglepayButton extends PaymentButton { @@ -78,7 +91,7 @@ class GooglepayButton extends PaymentButton { #paymentsClient = null; /** - * Details about the processed transaction. + * Details about the processed transaction, provided to the Google SDK. * * @type {?TransactionInfo} */ @@ -388,12 +401,14 @@ class GooglepayButton extends PaymentButton { const initiatePaymentRequest = () => { window.ppcpFundingSource = 'googlepay'; const paymentDataRequest = this.paymentDataRequest(); + this.log( 'onButtonClick: paymentDataRequest', paymentDataRequest, this.context ); - this.paymentsClient.loadPaymentData( paymentDataRequest ); + + return this.paymentsClient.loadPaymentData( paymentDataRequest ); }; const validateForm = () => { @@ -434,28 +449,24 @@ class GooglepayButton extends PaymentButton { apiVersionMinor: 0, }; - const googlePayConfig = this.googlePayConfig; - const paymentDataRequest = Object.assign( {}, baseRequest ); - paymentDataRequest.allowedPaymentMethods = - googlePayConfig.allowedPaymentMethods; - paymentDataRequest.transactionInfo = this.transactionInfo; - paymentDataRequest.merchantInfo = googlePayConfig.merchantInfo; + const useShippingCallback = this.requiresShipping; + const callbackIntents = [ 'PAYMENT_AUTHORIZATION' ]; - if ( this.requiresShipping ) { - paymentDataRequest.callbackIntents = [ - 'SHIPPING_ADDRESS', - 'SHIPPING_OPTION', - 'PAYMENT_AUTHORIZATION', - ]; - paymentDataRequest.shippingAddressRequired = true; - paymentDataRequest.shippingAddressParameters = - this.shippingAddressParameters(); - paymentDataRequest.shippingOptionRequired = true; - } else { - paymentDataRequest.callbackIntents = [ 'PAYMENT_AUTHORIZATION' ]; + if ( useShippingCallback ) { + callbackIntents.push( 'SHIPPING_ADDRESS', 'SHIPPING_OPTION' ); } - return paymentDataRequest; + return { + ...baseRequest, + allowedPaymentMethods: this.googlePayConfig.allowedPaymentMethods, + transactionInfo: this.transactionInfo, + merchantInfo: this.googlePayConfig.merchantInfo, + callbackIntents, + emailRequired: true, + shippingAddressRequired: useShippingCallback, + shippingOptionRequired: useShippingCallback, + shippingAddressParameters: this.shippingAddressParameters(), + }; } //------------------------ @@ -543,82 +554,111 @@ class GooglepayButton extends PaymentButton { //------------------------ onPaymentAuthorized( paymentData ) { - this.log( 'onPaymentAuthorized' ); + this.log( 'onPaymentAuthorized', paymentData ); + return this.processPayment( paymentData ); } async processPayment( paymentData ) { this.log( 'processPayment' ); - return new Promise( async ( resolve, reject ) => { - try { - const id = await this.contextHandler.createOrder(); + const paymentError = ( reason ) => { + this.error( reason ); - this.log( 'processPayment: createOrder', id ); + return this.processPaymentResponse( + 'ERROR', + 'PAYMENT_AUTHORIZATION', + reason + ); + }; - const confirmOrderResponse = await widgetBuilder.paypal - .Googlepay() - .confirmOrder( { - orderId: id, - paymentMethodData: paymentData.paymentMethodData, - } ); + const checkPayPalApproval = async ( orderId ) => { + const confirmOrderResponse = await widgetBuilder.paypal + .Googlepay() + .confirmOrder( { + orderId, + paymentMethodData: paymentData.paymentMethodData, + } ); - this.log( - 'processPayment: confirmOrder', - confirmOrderResponse - ); + this.log( 'confirmOrder', confirmOrderResponse ); - /** Capture the Order on the Server */ - if ( confirmOrderResponse.status === 'APPROVED' ) { - let approveFailed = false; - await this.contextHandler.approveOrder( - { - orderID: id, - }, - { - // actions mock object. - restart: () => - new Promise( ( resolve, reject ) => { - approveFailed = true; - resolve(); - } ), - order: { - get: () => - new Promise( ( resolve, reject ) => { - resolve( null ); - } ), - }, - } - ); + return 'APPROVE' === confirmOrderResponse?.status; + }; - if ( ! approveFailed ) { - resolve( this.processPaymentResponse( 'SUCCESS' ) ); - } else { - resolve( - this.processPaymentResponse( - 'ERROR', - 'PAYMENT_AUTHORIZATION', - 'FAILED TO APPROVE' - ) - ); - } - } else { - resolve( - this.processPaymentResponse( - 'ERROR', - 'PAYMENT_AUTHORIZATION', - 'TRANSACTION FAILED' - ) - ); + const approveOrderServerSide = async ( orderID ) => { + let isApproved = true; + + this.log( 'approveOrder', orderID ); + + await this.contextHandler.approveOrder( + { orderID }, + { + restart: () => + new Promise( ( resolve ) => { + isApproved = false; + resolve(); + } ), + order: { + get: () => + new Promise( ( resolve ) => { + resolve( null ); + } ), + }, } + ); + + return isApproved; + }; + + const processPaymentPromise = async ( resolve ) => { + const id = await this.contextHandler.createOrder(); + + this.log( 'createOrder', id ); + + const isApprovedByPayPal = await checkPayPalApproval( id ); + + if ( ! isApprovedByPayPal ) { + resolve( paymentError( 'TRANSACTION FAILED' ) ); + + return; + } + + const success = await approveOrderServerSide( id ); + + if ( success ) { + resolve( this.processPaymentResponse( 'SUCCESS' ) ); + } else { + resolve( paymentError( 'FAILED TO APPROVE' ) ); + } + }; + + const propagatePayerDataToForm = () => { + const raw = paymentData?.paymentMethodData?.info?.billingAddress; + + const payer = { + name: { + given_name: raw.name.split( ' ' )[ 0 ], // Assuming first name is the first part + surname: raw.name.split( ' ' ).slice( 1 ).join( ' ' ), // Assuming last name is the rest + }, + address: { + country_code: raw.countryCode, + address_line_1: raw.address1, + address_line_2: raw.address2, + admin_area_1: raw.administrativeArea, + admin_area_2: raw.locality, + postal_code: raw.postalCode, + }, + }; + + setPayerData( payer ); + }; + + return new Promise( async ( resolve ) => { + try { + propagatePayerDataToForm(); + await processPaymentPromise( resolve ); } catch ( err ) { - resolve( - this.processPaymentResponse( - 'ERROR', - 'PAYMENT_AUTHORIZATION', - err.message - ) - ); + resolve( paymentError( err.message ) ); } } ); } From 63e9c8bf27729d0616f2f8249130d0c107adb274 Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Fri, 16 Aug 2024 18:15:59 +0200 Subject: [PATCH 31/80] =?UTF-8?q?=E2=9C=A8=20New=20ConsoleLogger=20`group`?= =?UTF-8?q?=20method?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Groups subsequent console.log output, for cleaner console output --- .../js/modules/Renderer/PaymentButton.js | 9 ++++++ .../resources/js/GooglepayButton.js | 16 +++++----- .../resources/js/helper/ConsoleLogger.js | 31 +++++++++++++++++++ 3 files changed, 49 insertions(+), 7 deletions(-) diff --git a/modules/ppcp-button/resources/js/modules/Renderer/PaymentButton.js b/modules/ppcp-button/resources/js/modules/Renderer/PaymentButton.js index 11fe31f79..143265bfb 100644 --- a/modules/ppcp-button/resources/js/modules/Renderer/PaymentButton.js +++ b/modules/ppcp-button/resources/js/modules/Renderer/PaymentButton.js @@ -599,6 +599,15 @@ export default class PaymentButton { this.#logger.error( ...args ); } + /** + * Open or close a log-group + * + * @param {?string} [label=null] Group label. + */ + logGroup( label = null ) { + this.#logger.group( label ); + } + /** * Determines if the current button instance has valid and complete configuration details. * Used during initialization to decide if the button can be initialized or should be skipped. diff --git a/modules/ppcp-googlepay/resources/js/GooglepayButton.js b/modules/ppcp-googlepay/resources/js/GooglepayButton.js index a9bfd693e..cfd5d4b07 100644 --- a/modules/ppcp-googlepay/resources/js/GooglepayButton.js +++ b/modules/ppcp-googlepay/resources/js/GooglepayButton.js @@ -560,7 +560,7 @@ class GooglepayButton extends PaymentButton { } async processPayment( paymentData ) { - this.log( 'processPayment' ); + this.logGroup( 'processPayment' ); const paymentError = ( reason ) => { this.error( reason ); @@ -623,13 +623,13 @@ class GooglepayButton extends PaymentButton { return; } - const success = await approveOrderServerSide( id ); + const success = await approveOrderServerSide( id ); - if ( success ) { - resolve( this.processPaymentResponse( 'SUCCESS' ) ); - } else { - resolve( paymentError( 'FAILED TO APPROVE' ) ); - } + if ( success ) { + resolve( this.processPaymentResponse( 'SUCCESS' ) ); + } else { + resolve( paymentError( 'FAILED TO APPROVE' ) ); + } }; const propagatePayerDataToForm = () => { @@ -660,6 +660,8 @@ class GooglepayButton extends PaymentButton { } catch ( err ) { resolve( paymentError( err.message ) ); } + + this.logGroup(); } ); } diff --git a/modules/ppcp-wc-gateway/resources/js/helper/ConsoleLogger.js b/modules/ppcp-wc-gateway/resources/js/helper/ConsoleLogger.js index c76aa8960..a7b61b32a 100644 --- a/modules/ppcp-wc-gateway/resources/js/helper/ConsoleLogger.js +++ b/modules/ppcp-wc-gateway/resources/js/helper/ConsoleLogger.js @@ -18,6 +18,13 @@ export default class ConsoleLogger { */ #enabled = false; + /** + * Tracks the current log-group that was started using `this.group()` + * + * @type {?string} + */ + #openGroup = null; + constructor( ...prefixes ) { if ( prefixes.length ) { this.#prefix = `[${ prefixes.join( ' | ' ) }]`; @@ -55,4 +62,28 @@ export default class ConsoleLogger { error( ...args ) { console.error( this.#prefix, ...args ); } + + /** + * Starts or ends a group in the browser console. + * + * @param {string} [label=null] - The group label. Omit to end the current group. + */ + group( label = null ) { + if ( ! this.#enabled ) { + return; + } + + if ( ! label || this.#openGroup ) { + // eslint-disable-next-line + console.groupEnd(); + this.#openGroup = null; + } + + if ( label ) { + // eslint-disable-next-line + console.group( label ); + + this.#openGroup = label; + } + } } From c007d7909c2793b266eb733c565a7ec52b87af4b Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Fri, 16 Aug 2024 18:45:49 +0200 Subject: [PATCH 32/80] =?UTF-8?q?=E2=9C=A8=20Option=20to=20only=20set=20mi?= =?UTF-8?q?ssing=20billing=20details?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../resources/js/modules/Helper/PayerData.js | 10 +++++++--- modules/ppcp-googlepay/resources/js/GooglepayButton.js | 2 +- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/modules/ppcp-button/resources/js/modules/Helper/PayerData.js b/modules/ppcp-button/resources/js/modules/Helper/PayerData.js index da7eca4d6..1f97309c1 100644 --- a/modules/ppcp-button/resources/js/modules/Helper/PayerData.js +++ b/modules/ppcp-button/resources/js/modules/Helper/PayerData.js @@ -109,9 +109,13 @@ export const payerData = () => { /** * Updates the DOM with specific payer details. * - * @param {PayerDetails} newData - New payer details. + * @param {PayerDetails} newData - New payer details. + * @param {boolean} [overwriteExisting=false] - If set to true, all provided values will replace existing details. If false, or omitted, only undefined fields are updated. */ -export const setPayerData = ( newData ) => { +export const setPayerData = ( newData, overwriteExisting = false ) => { + // TODO: Check if we can add some kind of "filter" to allow customization of the data. + // Or add JS flags like "onlyUpdateMissing". + const setValue = ( path, field, value ) => { if ( null === value || undefined === value || ! field ) { return; @@ -121,7 +125,7 @@ export const setPayerData = ( newData ) => { value = value.phone_number?.national_number; } - if ( field.value !== value ) { + if ( overwriteExisting || ! field.value ) { field.value = value; } }; diff --git a/modules/ppcp-googlepay/resources/js/GooglepayButton.js b/modules/ppcp-googlepay/resources/js/GooglepayButton.js index cfd5d4b07..d518c1850 100644 --- a/modules/ppcp-googlepay/resources/js/GooglepayButton.js +++ b/modules/ppcp-googlepay/resources/js/GooglepayButton.js @@ -634,8 +634,8 @@ class GooglepayButton extends PaymentButton { const propagatePayerDataToForm = () => { const raw = paymentData?.paymentMethodData?.info?.billingAddress; - const payer = { + email_address: paymentData?.email, name: { given_name: raw.name.split( ' ' )[ 0 ], // Assuming first name is the first part surname: raw.name.split( ' ' ).slice( 1 ).join( ' ' ), // Assuming last name is the rest From cf83d2ba6efa6e254ba508939f675121f4cf2401 Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Fri, 16 Aug 2024 18:54:55 +0200 Subject: [PATCH 33/80] =?UTF-8?q?=F0=9F=90=9B=20Fix=20critical=20typo?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ppcp-googlepay/resources/js/GooglepayButton.js | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/modules/ppcp-googlepay/resources/js/GooglepayButton.js b/modules/ppcp-googlepay/resources/js/GooglepayButton.js index d518c1850..d8a910f34 100644 --- a/modules/ppcp-googlepay/resources/js/GooglepayButton.js +++ b/modules/ppcp-googlepay/resources/js/GooglepayButton.js @@ -573,16 +573,18 @@ class GooglepayButton extends PaymentButton { }; const checkPayPalApproval = async ( orderId ) => { + const confirmationData = { + orderId, + paymentMethodData: paymentData.paymentMethodData, + }; + const confirmOrderResponse = await widgetBuilder.paypal .Googlepay() - .confirmOrder( { - orderId, - paymentMethodData: paymentData.paymentMethodData, - } ); + .confirmOrder( confirmationData ); this.log( 'confirmOrder', confirmOrderResponse ); - return 'APPROVE' === confirmOrderResponse?.status; + return 'APPROVED' === confirmOrderResponse?.status; }; const approveOrderServerSide = async ( orderID ) => { From 451dc842ed506fe6b48b10e7fad4e128fe050fef Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Tue, 20 Aug 2024 13:47:00 +0200 Subject: [PATCH 34/80] =?UTF-8?q?=E2=9C=A8=20New=20LocalStorage=20module?= =?UTF-8?q?=20for=20Google=20Pay?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../js/modules/Helper/LocalStorage.js | 179 ++++++++++++++++++ .../resources/js/Helper/GooglePayStorage.js | 31 +++ 2 files changed, 210 insertions(+) create mode 100644 modules/ppcp-button/resources/js/modules/Helper/LocalStorage.js create mode 100644 modules/ppcp-googlepay/resources/js/Helper/GooglePayStorage.js diff --git a/modules/ppcp-button/resources/js/modules/Helper/LocalStorage.js b/modules/ppcp-button/resources/js/modules/Helper/LocalStorage.js new file mode 100644 index 000000000..65494369e --- /dev/null +++ b/modules/ppcp-button/resources/js/modules/Helper/LocalStorage.js @@ -0,0 +1,179 @@ +/* global localStorage */ + +function checkLocalStorageAvailability() { + try { + const testKey = '__ppcp_test__'; + localStorage.setItem( testKey, 'test' ); + localStorage.removeItem( testKey ); + return true; + } catch ( e ) { + return false; + } +} + +function sanitizeKey( name ) { + return name + .toLowerCase() + .trim() + .replace( /[^a-z0-9_-]/g, '_' ); +} + +function deserializeEntry( serialized ) { + try { + const payload = JSON.parse( serialized ); + + return { + data: payload.data, + expires: payload.expires || 0, + }; + } catch ( e ) { + return null; + } +} + +function serializeEntry( data, timeToLive ) { + const payload = { + data, + expires: calculateExpiration( timeToLive ), + }; + + return JSON.stringify( payload ); +} + +function calculateExpiration( timeToLive ) { + return timeToLive ? Date.now() + timeToLive * 1000 : 0; +} + +/** + * A reusable class for handling data storage in the browser's local storage, + * with optional expiration. + * + * Can be extended for module specific logic. + * + * @see GooglePaySession + */ +export class LocalStorage { + /** + * @type {string} + */ + #group = ''; + + /** + * @type {null|boolean} + */ + #canUseLocalStorage = null; + + /** + * @param {string} group - Group name for all storage keys managed by this instance. + */ + constructor( group ) { + this.#group = sanitizeKey( group ) + ':'; + this.#removeExpired(); + } + + /** + * Removes all items in the current group that have reached the expiry date. + */ + #removeExpired() { + if ( ! this.canUseLocalStorage ) { + return; + } + + Object.keys( localStorage ).forEach( ( key ) => { + if ( ! key.startsWith( this.#group ) ) { + return; + } + + const entry = deserializeEntry( localStorage.getItem( key ) ); + if ( entry && entry.expires > 0 && entry.expires < Date.now() ) { + localStorage.removeItem( key ); + } + } ); + } + + /** + * Sanitizes the given entry name and adds the group prefix. + * + * @throws {Error} If the name is empty after sanitization. + * @param {string} name - Entry name. + * @return {string} Prefixed and sanitized entry name. + */ + #entryKey( name ) { + const sanitizedName = sanitizeKey( name ); + + if ( sanitizedName.length === 0 ) { + throw new Error( 'Name cannot be empty after sanitization' ); + } + + return `${ this.#group }${ sanitizedName }`; + } + + /** + * Indicates, whether localStorage is available. + * + * @return {boolean} True means the localStorage API is available. + */ + get canUseLocalStorage() { + if ( null === this.#canUseLocalStorage ) { + this.#canUseLocalStorage = checkLocalStorageAvailability(); + } + + return this.#canUseLocalStorage; + } + + /** + * Stores data in the browser's local storage, with an optional timeout. + * + * @param {string} name - Name of the item in the storage. + * @param {any} data - The data to store. + * @param {number} [timeToLive=0] - Lifespan in seconds. 0 means the data won't expire. + * @throws {Error} If local storage is not available. + */ + set( name, data, timeToLive = 0 ) { + if ( ! this.canUseLocalStorage ) { + throw new Error( 'Local storage is not available' ); + } + + const entry = serializeEntry( data, timeToLive ); + const entryKey = this.#entryKey( name ); + + localStorage.setItem( entryKey, entry ); + } + + /** + * Retrieves previously stored data from the browser's local storage. + * + * @param {string} name - Name of the stored item. + * @return {any|null} The stored data, or null when no valid entry is found or it has expired. + * @throws {Error} If local storage is not available. + */ + get( name ) { + if ( ! this.canUseLocalStorage ) { + throw new Error( 'Local storage is not available' ); + } + + const itemKey = this.#entryKey( name ); + const entry = deserializeEntry( localStorage.getItem( itemKey ) ); + + if ( ! entry ) { + return null; + } + + return entry.data; + } + + /** + * Removes the specified entry from the browser's local storage. + * + * @param {string} name - Name of the stored item. + * @throws {Error} If local storage is not available. + */ + clear( name ) { + if ( ! this.canUseLocalStorage ) { + throw new Error( 'Local storage is not available' ); + } + + const itemKey = this.#entryKey( name ); + localStorage.removeItem( itemKey ); + } +} diff --git a/modules/ppcp-googlepay/resources/js/Helper/GooglePayStorage.js b/modules/ppcp-googlepay/resources/js/Helper/GooglePayStorage.js new file mode 100644 index 000000000..faa2520e5 --- /dev/null +++ b/modules/ppcp-googlepay/resources/js/Helper/GooglePayStorage.js @@ -0,0 +1,31 @@ +import { LocalStorage } from '../../../../ppcp-button/resources/js/modules/Helper/LocalStorage'; + +export class GooglePayStorage extends LocalStorage { + static PAYER = 'payer'; + static PAYER_TTL = 900; // 15 minutes in seconds + + constructor() { + super( 'ppcp-googlepay' ); + } + + getPayer() { + return this.get( GooglePayStorage.PAYER ); + } + + setPayer( data ) { + /* + * The payer details are deleted on successful checkout, or after the TTL is reached. + * This helps to remove stale data from the browser, in case the customer chooses to + * use a different method to complete the purchase. + */ + this.set( GooglePayStorage.PAYER, data, GooglePayStorage.PAYER_TTL ); + } + + clearPayer() { + this.clear( GooglePayStorage.PAYER ); + } +} + +const moduleStorage = new GooglePayStorage(); + +export default moduleStorage; From 63b505590326c10174c0e0c54db12e82cb57b33d Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Tue, 20 Aug 2024 14:14:30 +0200 Subject: [PATCH 35/80] =?UTF-8?q?=F0=9F=92=A1=20Document=20payment=20workf?= =?UTF-8?q?low?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../OnApproveHandler/onApproveForContinue.js | 16 ++++++++++++---- .../resources/js/GooglepayButton.js | 9 +++++++++ 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/modules/ppcp-button/resources/js/modules/OnApproveHandler/onApproveForContinue.js b/modules/ppcp-button/resources/js/modules/OnApproveHandler/onApproveForContinue.js index c60c163fd..0d699c170 100644 --- a/modules/ppcp-button/resources/js/modules/OnApproveHandler/onApproveForContinue.js +++ b/modules/ppcp-button/resources/js/modules/OnApproveHandler/onApproveForContinue.js @@ -18,17 +18,25 @@ const onApprove = ( context, errorHandler ) => { .then( ( res ) => { return res.json(); } ) - .then( ( data ) => { - if ( ! data.success ) { + .then( ( approveData ) => { + if ( ! approveData.success ) { errorHandler.genericError(); return actions.restart().catch( ( err ) => { errorHandler.genericError(); } ); } - const orderReceivedUrl = data.data?.order_received_url; + const orderReceivedUrl = approveData.data?.order_received_url; - location.href = orderReceivedUrl + /** + * Notice how this step initiates a redirect to a new page using a plain + * URL as new location. This process does not send any details about the + * approved order or billed customer. + * Also, due to the redirect starting _instantly_ there should be no other + * logic scheduled after calling `await onApprove()`; + */ + + window.location.href = orderReceivedUrl ? orderReceivedUrl : context.config.redirect; } ); diff --git a/modules/ppcp-googlepay/resources/js/GooglepayButton.js b/modules/ppcp-googlepay/resources/js/GooglepayButton.js index d8a910f34..a68e22f4a 100644 --- a/modules/ppcp-googlepay/resources/js/GooglepayButton.js +++ b/modules/ppcp-googlepay/resources/js/GooglepayButton.js @@ -587,6 +587,14 @@ class GooglepayButton extends PaymentButton { return 'APPROVED' === confirmOrderResponse?.status; }; + /** + * This approval mainly confirms that the orderID is valid. + * + * It's still needed because this handler redirects to the checkout page if the server-side + * approval was successful. + * + * @param {string} orderID + */ const approveOrderServerSide = async ( orderID ) => { let isApproved = true; @@ -625,6 +633,7 @@ class GooglepayButton extends PaymentButton { return; } + // This must be the last step in the process, as it initiates a redirect. const success = await approveOrderServerSide( id ); if ( success ) { From a51748f805416b51828c38f6600b38401d75dd7e Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Tue, 20 Aug 2024 14:18:30 +0200 Subject: [PATCH 36/80] =?UTF-8?q?=F0=9F=92=A1=20Improve=20comments=20in=20?= =?UTF-8?q?PayerData=20module?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../resources/js/modules/Helper/PayerData.js | 49 +++++++++++-------- 1 file changed, 29 insertions(+), 20 deletions(-) diff --git a/modules/ppcp-button/resources/js/modules/Helper/PayerData.js b/modules/ppcp-button/resources/js/modules/Helper/PayerData.js index 1f97309c1..59af8dc85 100644 --- a/modules/ppcp-button/resources/js/modules/Helper/PayerData.js +++ b/modules/ppcp-button/resources/js/modules/Helper/PayerData.js @@ -10,30 +10,30 @@ * Postal address details. * * @typedef {Object} AddressDetails - * @property {?string} country_code - Country code (2-letter). - * @property {?string} address_line_1 - Address details, line 1 (street, house number). - * @property {?string} address_line_2 - Address details, line 2. - * @property {?string} admin_area_1 - State or region. - * @property {?string} admin_area_2 - State or region. - * @property {?string} postal_code - Zip code. + * @property {undefined|string} country_code - Country code (2-letter). + * @property {undefined|string} address_line_1 - Address details, line 1 (street, house number). + * @property {undefined|string} address_line_2 - Address details, line 2. + * @property {undefined|string} admin_area_1 - State or region. + * @property {undefined|string} admin_area_2 - State or region. + * @property {undefined|string} postal_code - Zip code. */ /** * Phone details. * * @typedef {Object} PhoneDetails - * @property {?string} phone_type - Type, usually 'HOME' - * @property {?{national_number: string}} phone_number - Phone number details. + * @property {undefined|string} phone_type - Type, usually 'HOME' + * @property {undefined|{national_number: string}} phone_number - Phone number details. */ /** * Payer details. * * @typedef {Object} PayerDetails - * @property {?string} email_address - Email address for billing communication. - * @property {?PhoneDetails} phone - Phone number for billing communication. - * @property {?NameDetails} name - Payer's name. - * @property {?AddressDetails} address - Postal billing address. + * @property {undefined|string} email_address - Email address for billing communication. + * @property {undefined|PhoneDetails} phone - Phone number for billing communication. + * @property {undefined|NameDetails} name - Payer's name. + * @property {undefined|AddressDetails} address - Postal billing address. */ // Map checkout fields to PayerData object properties. @@ -55,7 +55,14 @@ const FIELD_MAP = { * * @return {?PayerDetails} Full billing details, or null on failure. */ -export const payerData = () => { +export function payerData() { + /** + * PayPalCommerceGateway.payer can be set from server-side or via JS: + * - Server-side: Set by PHP when a WC customer is known. + * - Dynamic JS: When a payment method provided billing data. + * + * @see {setPayerData} + */ const payer = window.PayPalCommerceGateway?.payer; if ( ! payer ) { return null; @@ -104,18 +111,20 @@ export const payerData = () => { } return data; -}; +} /** * Updates the DOM with specific payer details. * + * Used by payment method callbacks that provide dedicated billing details, like Google Pay. + * Note: This code only works on classic checkout + * * @param {PayerDetails} newData - New payer details. - * @param {boolean} [overwriteExisting=false] - If set to true, all provided values will replace existing details. If false, or omitted, only undefined fields are updated. + * @param {boolean} [overwriteExisting=false] - If set to true, all provided values will + * replace existing details. If false, or omitted, + * only undefined fields are updated. */ -export const setPayerData = ( newData, overwriteExisting = false ) => { - // TODO: Check if we can add some kind of "filter" to allow customization of the data. - // Or add JS flags like "onlyUpdateMissing". - +export function setPayerData( newData, overwriteExisting = false ) { const setValue = ( path, field, value ) => { if ( null === value || undefined === value || ! field ) { return; @@ -136,4 +145,4 @@ export const setPayerData = ( newData, overwriteExisting = false ) => { setValue( path, element, value ); } ); -}; +} From c48c94e09d7030d86a4dfc6b3afc1fad25cd6596 Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Tue, 20 Aug 2024 14:19:41 +0200 Subject: [PATCH 37/80] =?UTF-8?q?=E2=9C=A8=20New=20CheckoutBootstrap=20for?= =?UTF-8?q?=20GooglePay?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This new module uses previously stored payer details to populate the checkout form on the classic checkout page. --- .../js/ContextBootstrap/CheckoutBootstrap.js | 64 +++++++++++++++++++ .../resources/js/GooglepayButton.js | 43 +++++++------ modules/ppcp-googlepay/resources/js/boot.js | 18 ++++-- 3 files changed, 101 insertions(+), 24 deletions(-) create mode 100644 modules/ppcp-googlepay/resources/js/ContextBootstrap/CheckoutBootstrap.js diff --git a/modules/ppcp-googlepay/resources/js/ContextBootstrap/CheckoutBootstrap.js b/modules/ppcp-googlepay/resources/js/ContextBootstrap/CheckoutBootstrap.js new file mode 100644 index 000000000..b5e0c1a48 --- /dev/null +++ b/modules/ppcp-googlepay/resources/js/ContextBootstrap/CheckoutBootstrap.js @@ -0,0 +1,64 @@ +import { GooglePayStorage } from '../Helper/GooglePayStorage'; +import { setPayerData } from '../../../../ppcp-button/resources/js/modules/Helper/PayerData'; + +const CHECKOUT_FORM_SELECTOR = 'form.woocommerce-checkout'; + +export class CheckoutBootstrap { + /** + * @type {GooglePayStorage} + */ + #storage; + + /** + * @type {null|HTMLFormElement} + */ + #checkoutForm = null; + + constructor( storage ) { + this.#storage = storage; + + this.onFormSubmit = this.onFormSubmit.bind( this ); + } + + /** + * Returns the WooCommerce checkout form element. + * + * @return {HTMLFormElement|null} The form, or null if not a checkout page. + */ + get checkoutForm() { + if ( null === this.#checkoutForm ) { + this.#checkoutForm = document.querySelector( + CHECKOUT_FORM_SELECTOR + ); + } + + return this.#checkoutForm; + } + + /** + * Indicates, if the current page contains a checkout form. + * + * @return {boolean} True, if a checkout form is present. + */ + get isPageWithCheckoutForm() { + return this.checkoutForm instanceof HTMLElement; + } + + init() { + if ( ! this.isPageWithCheckoutForm ) { + return; + } + + const billingData = this.#storage.getPayer(); + + if ( billingData ) { + setPayerData( billingData ); + + this.checkoutForm.addEventListener( 'submit', this.onFormSubmit ); + } + } + + onFormSubmit() { + this.#storage.clearPayer(); + } +} diff --git a/modules/ppcp-googlepay/resources/js/GooglepayButton.js b/modules/ppcp-googlepay/resources/js/GooglepayButton.js index a68e22f4a..26acd0139 100644 --- a/modules/ppcp-googlepay/resources/js/GooglepayButton.js +++ b/modules/ppcp-googlepay/resources/js/GooglepayButton.js @@ -7,6 +7,7 @@ import widgetBuilder from '../../../ppcp-button/resources/js/modules/Renderer/Wi import UpdatePaymentData from './Helper/UpdatePaymentData'; import { PaymentMethods } from '../../../ppcp-button/resources/js/modules/Helper/CheckoutMethodState'; import { setPayerData } from '../../../ppcp-button/resources/js/modules/Helper/PayerData'; +import moduleStorage from './Helper/GooglePayStorage'; /** * Plugin-specific styling. @@ -74,6 +75,26 @@ import { setPayerData } from '../../../ppcp-button/resources/js/modules/Helper/P * Google Pay payment sheet. */ +function payerDataFromPaymentResponse( response ) { + const raw = response?.paymentMethodData?.info?.billingAddress; + + return { + email_address: response?.email, + name: { + given_name: raw.name.split( ' ' )[ 0 ], // Assuming first name is the first part + surname: raw.name.split( ' ' ).slice( 1 ).join( ' ' ), // Assuming last name is the rest + }, + address: { + country_code: raw.countryCode, + address_line_1: raw.address1, + address_line_2: raw.address2, + admin_area_1: raw.administrativeArea, + admin_area_2: raw.locality, + postal_code: raw.postalCode, + }, + }; +} + class GooglepayButton extends PaymentButton { /** * @inheritDoc @@ -643,30 +664,16 @@ class GooglepayButton extends PaymentButton { } }; - const propagatePayerDataToForm = () => { - const raw = paymentData?.paymentMethodData?.info?.billingAddress; - const payer = { - email_address: paymentData?.email, - name: { - given_name: raw.name.split( ' ' )[ 0 ], // Assuming first name is the first part - surname: raw.name.split( ' ' ).slice( 1 ).join( ' ' ), // Assuming last name is the rest - }, - address: { - country_code: raw.countryCode, - address_line_1: raw.address1, - address_line_2: raw.address2, - admin_area_1: raw.administrativeArea, - admin_area_2: raw.locality, - postal_code: raw.postalCode, - }, - }; + const addBillingDataToSession = () => { + const payer = payerDataFromPaymentResponse( paymentData ); + moduleStorage.setPayer( payer ); setPayerData( payer ); }; return new Promise( async ( resolve ) => { try { - propagatePayerDataToForm(); + addBillingDataToSession(); await processPaymentPromise( resolve ); } catch ( err ) { resolve( paymentError( err.message ) ); diff --git a/modules/ppcp-googlepay/resources/js/boot.js b/modules/ppcp-googlepay/resources/js/boot.js index 99dd414f5..3071998a9 100644 --- a/modules/ppcp-googlepay/resources/js/boot.js +++ b/modules/ppcp-googlepay/resources/js/boot.js @@ -2,13 +2,23 @@ import { loadCustomScript } from '@paypal/paypal-js'; import { loadPaypalScript } from '../../../ppcp-button/resources/js/modules/Helper/ScriptLoading'; import GooglepayManager from './GooglepayManager'; import { setupButtonEvents } from '../../../ppcp-button/resources/js/modules/Helper/ButtonRefreshHelper'; +import { CheckoutBootstrap } from './ContextBootstrap/CheckoutBootstrap'; +import moduleStorage from './Helper/GooglePayStorage'; + +( function ( { buttonConfig, ppcpConfig } ) { + const context = ppcpConfig.context; -( function ( { buttonConfig, ppcpConfig, jQuery } ) { let manager; const bootstrap = function () { manager = new GooglepayManager( buttonConfig, ppcpConfig ); manager.init(); + + if ( 'continuation' === context || 'checkout' === context ) { + const checkoutBootstap = new CheckoutBootstrap( moduleStorage ); + + checkoutBootstap.init(); + } }; setupButtonEvents( function () { @@ -18,10 +28,7 @@ import { setupButtonEvents } from '../../../ppcp-button/resources/js/modules/Hel } ); document.addEventListener( 'DOMContentLoaded', () => { - if ( - typeof buttonConfig === 'undefined' || - typeof ppcpConfig === 'undefined' - ) { + if ( ! buttonConfig || ! ppcpConfig ) { // No PayPal buttons present on this page. return; } @@ -52,5 +59,4 @@ import { setupButtonEvents } from '../../../ppcp-button/resources/js/modules/Hel } )( { buttonConfig: window.wc_ppcp_googlepay, ppcpConfig: window.PayPalCommerceGateway, - jQuery: window.jQuery, } ); From 97379628d441602c3ca3425b7f6df253765b09f8 Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Tue, 20 Aug 2024 15:37:26 +0200 Subject: [PATCH 38/80] =?UTF-8?q?=E2=9C=A8=20Send=20payer=20details=20with?= =?UTF-8?q?=20order=20approval=20request?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit First step to integrate payer details in the payment flow without final confirmation. --- .../resources/js/modules/Helper/PayerData.js | 24 +++++++++++++++++++ .../OnApproveHandler/onApproveForContinue.js | 23 +++++++++++------- .../resources/js/GooglepayButton.js | 6 ++--- 3 files changed, 42 insertions(+), 11 deletions(-) diff --git a/modules/ppcp-button/resources/js/modules/Helper/PayerData.js b/modules/ppcp-button/resources/js/modules/Helper/PayerData.js index 59af8dc85..358b1587f 100644 --- a/modules/ppcp-button/resources/js/modules/Helper/PayerData.js +++ b/modules/ppcp-button/resources/js/modules/Helper/PayerData.js @@ -145,4 +145,28 @@ export function setPayerData( newData, overwriteExisting = false ) { setValue( path, element, value ); } ); + + /* + * Persist the payer details to the global JS object, to make it available in other modules + * via tha `payerData()` accessor. + */ + window.PayPalCommerceGateway.payer = + window.PayPalCommerceGateway.payer || {}; + const currentPayerData = payerData(); + + if ( currentPayerData ) { + Object.entries( newData ).forEach( ( [ key, value ] ) => { + if ( + overwriteExisting || + null !== currentPayerData[ key ] || + undefined !== currentPayerData[ key ] + ) { + currentPayerData[ key ] = value; + } + } ); + + window.PayPalCommerceGateway.payer = currentPayerData; + } else { + window.PayPalCommerceGateway.payer = newData; + } } diff --git a/modules/ppcp-button/resources/js/modules/OnApproveHandler/onApproveForContinue.js b/modules/ppcp-button/resources/js/modules/OnApproveHandler/onApproveForContinue.js index 0d699c170..d492802f1 100644 --- a/modules/ppcp-button/resources/js/modules/OnApproveHandler/onApproveForContinue.js +++ b/modules/ppcp-button/resources/js/modules/OnApproveHandler/onApproveForContinue.js @@ -1,19 +1,26 @@ const onApprove = ( context, errorHandler ) => { return ( data, actions ) => { + const canCreateOrder = + ! context.config.vaultingEnabled || data.paymentSource !== 'venmo'; + + const payload = { + nonce: context.config.ajax.approve_order.nonce, + order_id: data.orderID, + funding_source: window.ppcpFundingSource, + should_create_wc_order: canCreateOrder, + }; + + if ( canCreateOrder && data.payer ) { + payload.payer = data.payer; + } + return fetch( context.config.ajax.approve_order.endpoint, { method: 'POST', headers: { 'Content-Type': 'application/json', }, credentials: 'same-origin', - body: JSON.stringify( { - nonce: context.config.ajax.approve_order.nonce, - order_id: data.orderID, - funding_source: window.ppcpFundingSource, - should_create_wc_order: - ! context.config.vaultingEnabled || - data.paymentSource !== 'venmo', - } ), + body: JSON.stringify( payload ), } ) .then( ( res ) => { return res.json(); diff --git a/modules/ppcp-googlepay/resources/js/GooglepayButton.js b/modules/ppcp-googlepay/resources/js/GooglepayButton.js index 26acd0139..e9ca1bcc8 100644 --- a/modules/ppcp-googlepay/resources/js/GooglepayButton.js +++ b/modules/ppcp-googlepay/resources/js/GooglepayButton.js @@ -583,6 +583,8 @@ class GooglepayButton extends PaymentButton { async processPayment( paymentData ) { this.logGroup( 'processPayment' ); + const payer = payerDataFromPaymentResponse( paymentData ); + const paymentError = ( reason ) => { this.error( reason ); @@ -622,7 +624,7 @@ class GooglepayButton extends PaymentButton { this.log( 'approveOrder', orderID ); await this.contextHandler.approveOrder( - { orderID }, + { orderID, payer }, { restart: () => new Promise( ( resolve ) => { @@ -665,8 +667,6 @@ class GooglepayButton extends PaymentButton { }; const addBillingDataToSession = () => { - const payer = payerDataFromPaymentResponse( paymentData ); - moduleStorage.setPayer( payer ); setPayerData( payer ); }; From 915754799371e7e919289e4a547be8196377df55 Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Wed, 21 Aug 2024 13:51:40 +0200 Subject: [PATCH 39/80] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Improve=20PayerData?= =?UTF-8?q?=20module?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../resources/js/modules/Helper/PayerData.js | 212 ++++++++++-------- 1 file changed, 118 insertions(+), 94 deletions(-) diff --git a/modules/ppcp-button/resources/js/modules/Helper/PayerData.js b/modules/ppcp-button/resources/js/modules/Helper/PayerData.js index 358b1587f..6b899bc9e 100644 --- a/modules/ppcp-button/resources/js/modules/Helper/PayerData.js +++ b/modules/ppcp-button/resources/js/modules/Helper/PayerData.js @@ -2,38 +2,38 @@ * Name details. * * @typedef {Object} NameDetails - * @property {?string} given_name - First name, e.g. "John". - * @property {?string} surname - Last name, e.g. "Doe". + * @property {string} [given_name] - First name, e.g. "John". + * @property {string} [surname] - Last name, e.g. "Doe". */ /** * Postal address details. * * @typedef {Object} AddressDetails - * @property {undefined|string} country_code - Country code (2-letter). - * @property {undefined|string} address_line_1 - Address details, line 1 (street, house number). - * @property {undefined|string} address_line_2 - Address details, line 2. - * @property {undefined|string} admin_area_1 - State or region. - * @property {undefined|string} admin_area_2 - State or region. - * @property {undefined|string} postal_code - Zip code. + * @property {string} [country_code] - Country code (2-letter). + * @property {string} [address_line_1] - Address details, line 1 (street, house number). + * @property {string} [address_line_2] - Address details, line 2. + * @property {string} [admin_area_1] - State or region. + * @property {string} [admin_area_2] - State or region. + * @property {string} [postal_code] - Zip code. */ /** * Phone details. * * @typedef {Object} PhoneDetails - * @property {undefined|string} phone_type - Type, usually 'HOME' - * @property {undefined|{national_number: string}} phone_number - Phone number details. + * @property {string} [phone_type] - Type, usually 'HOME' + * @property {{national_number: string}} [phone_number] - Phone number details. */ /** * Payer details. * * @typedef {Object} PayerDetails - * @property {undefined|string} email_address - Email address for billing communication. - * @property {undefined|PhoneDetails} phone - Phone number for billing communication. - * @property {undefined|NameDetails} name - Payer's name. - * @property {undefined|AddressDetails} address - Postal billing address. + * @property {string} [email_address] - Email address for billing communication. + * @property {PhoneDetails} [phone] - Phone number for billing communication. + * @property {NameDetails} [name] - Payer's name. + * @property {AddressDetails} [address] - Postal billing address. */ // Map checkout fields to PayerData object properties. @@ -50,123 +50,147 @@ const FIELD_MAP = { '#billing_phone': [ 'phone' ], }; -/** - * Returns billing details from the checkout form or global JS object. - * - * @return {?PayerDetails} Full billing details, or null on failure. - */ -export function payerData() { - /** - * PayPalCommerceGateway.payer can be set from server-side or via JS: - * - Server-side: Set by PHP when a WC customer is known. - * - Dynamic JS: When a payment method provided billing data. - * - * @see {setPayerData} - */ - const payer = window.PayPalCommerceGateway?.payer; - if ( ! payer ) { - return null; - } +function normalizePayerDetails( details ) { + return { + email_address: details.email_address, + phone: details.phone, + name: { + surname: details.name?.surname, + given_name: details.name?.given_name, + }, + address: { + country_code: details.address?.country_code, + address_line_1: details.address?.address_line_1, + address_line_2: details.address?.address_line_2, + admin_area_1: details.address?.admin_area_1, + admin_area_2: details.address?.admin_area_2, + postal_code: details.address?.postal_code, + }, + }; +} +function mergePayerDetails( firstPayer, secondPayer ) { + const mergeNestedObjects = ( target, source ) => { + for ( const [ key, value ] of Object.entries( source ) ) { + if ( null !== value && undefined !== value ) { + if ( 'object' === typeof value ) { + target[ key ] = mergeNestedObjects( + target[ key ] || {}, + value + ); + } else { + target[ key ] = value; + } + } + } + return target; + }; + + return mergeNestedObjects( + normalizePayerDetails( firstPayer ), + normalizePayerDetails( secondPayer ) + ); +} + +function getCheckoutBillingDetails() { const getElementValue = ( selector ) => document.querySelector( selector )?.value; - // Initialize data with existing payer values. - const data = { - email_address: payer.email_address, - phone: payer.phone, - name: { - surname: payer.name?.surname, - given_name: payer.name?.given_name, - }, - address: { - country_code: payer.address?.country_code, - address_line_1: payer.address?.address_line_1, - address_line_2: payer.address?.address_line_2, - admin_area_1: payer.address?.admin_area_1, - admin_area_2: payer.address?.admin_area_2, - postal_code: payer.address?.postal_code, - }, + const setNestedValue = ( obj, path, value ) => { + let current = obj; + for ( let i = 0; i < path.length - 1; i++ ) { + current = current[ path[ i ] ] = current[ path[ i ] ] || {}; + } + current[ path[ path.length - 1 ] ] = value; }; - // Update data with DOM values where they exist. + const data = {}; + Object.entries( FIELD_MAP ).forEach( ( [ selector, path ] ) => { const value = getElementValue( selector ); if ( value ) { - let current = data; - path.slice( 0, -1 ).forEach( ( key ) => { - current = current[ key ] = current[ key ] || {}; - } ); - current[ path[ path.length - 1 ] ] = value; + setNestedValue( data, path, value ); } } ); - // Handle phone separately due to its nested structure. - const phoneNumber = data.phone; - if ( phoneNumber && typeof phoneNumber === 'string' ) { + if ( data.phone && 'string' === typeof data.phone ) { data.phone = { phone_type: 'HOME', - phone_number: { national_number: phoneNumber }, + phone_number: { national_number: data.phone }, }; } return data; } -/** - * Updates the DOM with specific payer details. - * - * Used by payment method callbacks that provide dedicated billing details, like Google Pay. - * Note: This code only works on classic checkout - * - * @param {PayerDetails} newData - New payer details. - * @param {boolean} [overwriteExisting=false] - If set to true, all provided values will - * replace existing details. If false, or omitted, - * only undefined fields are updated. - */ -export function setPayerData( newData, overwriteExisting = false ) { +function setCheckoutBillingDetails( payer ) { const setValue = ( path, field, value ) => { if ( null === value || undefined === value || ! field ) { return; } - if ( path[ 0 ] === 'phone' && typeof value === 'object' ) { + if ( 'phone' === path[ 0 ] && 'object' === typeof value ) { value = value.phone_number?.national_number; } - if ( overwriteExisting || ! field.value ) { - field.value = value; - } + field.value = value; }; + const getNestedValue = ( obj, path ) => + path.reduce( ( current, key ) => current?.[ key ], obj ); + Object.entries( FIELD_MAP ).forEach( ( [ selector, path ] ) => { - const value = path.reduce( ( obj, key ) => obj?.[ key ], newData ); + const value = getNestedValue( payer, path ); const element = document.querySelector( selector ); setValue( path, element, value ); } ); +} - /* - * Persist the payer details to the global JS object, to make it available in other modules - * via tha `payerData()` accessor. - */ - window.PayPalCommerceGateway.payer = - window.PayPalCommerceGateway.payer || {}; - const currentPayerData = payerData(); +export function getWooCommerceCustomerDetails() { + // Populated on server-side with details about the current WooCommerce customer. + return window.PayPalCommerceGateway?.payer ?? null; +} - if ( currentPayerData ) { - Object.entries( newData ).forEach( ( [ key, value ] ) => { - if ( - overwriteExisting || - null !== currentPayerData[ key ] || - undefined !== currentPayerData[ key ] - ) { - currentPayerData[ key ] = value; - } - } ); +export function getSessionCustomerDetails() { + // Populated by JS via `setSessionCustomerDetails()` + return window.PayPalCommerceGateway?.sessionPayer ?? null; +} - window.PayPalCommerceGateway.payer = currentPayerData; - } else { - window.PayPalCommerceGateway.payer = newData; +/** + * Stores customer details in the current JS context for use in the same request. + * Details that are set are not persisted during navigation. + * + * @param {unknown} details - New payer details + */ +export function setSessionCustomerDetails( details ) { + if ( details && 'object' === typeof details ) { + window.PayPalCommerceGateway.sessionPayer = + normalizePayerDetails( details ); + } +} + +export function payerData() { + const payer = + getWooCommerceCustomerDetails() ?? getSessionCustomerDetails(); + + if ( ! payer ) { + return null; + } + + const formData = getCheckoutBillingDetails(); + + if ( formData ) { + return mergePayerDetails( payer, formData ); + } + + return normalizePayerDetails( payer ); +} + +export function setPayerData( newData, updateCheckout = false ) { + setSessionCustomerDetails( newData ); + + if ( updateCheckout ) { + setCheckoutBillingDetails( newData ); } } From eba92e6b81c48b1745d4ffbf68623ea878f976e1 Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Wed, 21 Aug 2024 13:52:11 +0200 Subject: [PATCH 40/80] =?UTF-8?q?=E2=9C=A8=20Detect=20logged=20in=20custom?= =?UTF-8?q?er=20in=20checkout=20module?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../js/ContextBootstrap/CheckoutBootstrap.js | 22 ++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/modules/ppcp-googlepay/resources/js/ContextBootstrap/CheckoutBootstrap.js b/modules/ppcp-googlepay/resources/js/ContextBootstrap/CheckoutBootstrap.js index b5e0c1a48..d913b0b3f 100644 --- a/modules/ppcp-googlepay/resources/js/ContextBootstrap/CheckoutBootstrap.js +++ b/modules/ppcp-googlepay/resources/js/ContextBootstrap/CheckoutBootstrap.js @@ -1,5 +1,8 @@ import { GooglePayStorage } from '../Helper/GooglePayStorage'; -import { setPayerData } from '../../../../ppcp-button/resources/js/modules/Helper/PayerData'; +import { + getWooCommerceCustomerDetails, + setPayerData, +} from '../../../../ppcp-button/resources/js/modules/Helper/PayerData'; const CHECKOUT_FORM_SELECTOR = 'form.woocommerce-checkout'; @@ -49,16 +52,29 @@ export class CheckoutBootstrap { return; } + this.#populateCheckoutFields(); + } + + #populateCheckoutFields() { + const loggedInData = getWooCommerceCustomerDetails(); + + // If customer is logged in, we use the details from the customer profile. + if ( loggedInData ) { + return; + } + const billingData = this.#storage.getPayer(); if ( billingData ) { setPayerData( billingData ); - this.checkoutForm.addEventListener( 'submit', this.onFormSubmit ); + this.checkoutForm.addEventListener( 'submit', () => + this.#onFormSubmit() + ); } } - onFormSubmit() { + #onFormSubmit() { this.#storage.clearPayer(); } } From 734951adcb2d5250d5c768d29d6eb34936193350 Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Wed, 21 Aug 2024 14:21:45 +0200 Subject: [PATCH 41/80] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Minor=20cleanup?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../resources/js/modules/Helper/PayerData.js | 28 +++++++++---------- .../js/ContextBootstrap/CheckoutBootstrap.js | 17 ++++++----- 2 files changed, 22 insertions(+), 23 deletions(-) diff --git a/modules/ppcp-button/resources/js/modules/Helper/PayerData.js b/modules/ppcp-button/resources/js/modules/Helper/PayerData.js index 6b899bc9e..df13ef92f 100644 --- a/modules/ppcp-button/resources/js/modules/Helper/PayerData.js +++ b/modules/ppcp-button/resources/js/modules/Helper/PayerData.js @@ -149,12 +149,12 @@ function setCheckoutBillingDetails( payer ) { export function getWooCommerceCustomerDetails() { // Populated on server-side with details about the current WooCommerce customer. - return window.PayPalCommerceGateway?.payer ?? null; + return window.PayPalCommerceGateway?.payer; } -export function getSessionCustomerDetails() { - // Populated by JS via `setSessionCustomerDetails()` - return window.PayPalCommerceGateway?.sessionPayer ?? null; +export function getSessionBillingDetails() { + // Populated by JS via `setSessionBillingDetails()` + return window.PayPalCommerceGateway?.tempPayer; } /** @@ -163,16 +163,16 @@ export function getSessionCustomerDetails() { * * @param {unknown} details - New payer details */ -export function setSessionCustomerDetails( details ) { - if ( details && 'object' === typeof details ) { - window.PayPalCommerceGateway.sessionPayer = - normalizePayerDetails( details ); +export function setSessionBillingDetails( details ) { + if ( ! details || 'object' !== typeof details ) { + return; } + + window.PayPalCommerceGateway.tempPayer = normalizePayerDetails( details ); } export function payerData() { - const payer = - getWooCommerceCustomerDetails() ?? getSessionCustomerDetails(); + const payer = getWooCommerceCustomerDetails() ?? getSessionBillingDetails(); if ( ! payer ) { return null; @@ -187,10 +187,10 @@ export function payerData() { return normalizePayerDetails( payer ); } -export function setPayerData( newData, updateCheckout = false ) { - setSessionCustomerDetails( newData ); +export function setPayerData( payerDetails, updateCheckoutForm = false ) { + setSessionBillingDetails( payerDetails ); - if ( updateCheckout ) { - setCheckoutBillingDetails( newData ); + if ( updateCheckoutForm ) { + setCheckoutBillingDetails( payerDetails ); } } diff --git a/modules/ppcp-googlepay/resources/js/ContextBootstrap/CheckoutBootstrap.js b/modules/ppcp-googlepay/resources/js/ContextBootstrap/CheckoutBootstrap.js index d913b0b3f..9e1c30e9f 100644 --- a/modules/ppcp-googlepay/resources/js/ContextBootstrap/CheckoutBootstrap.js +++ b/modules/ppcp-googlepay/resources/js/ContextBootstrap/CheckoutBootstrap.js @@ -19,8 +19,6 @@ export class CheckoutBootstrap { constructor( storage ) { this.#storage = storage; - - this.onFormSubmit = this.onFormSubmit.bind( this ); } /** @@ -44,7 +42,7 @@ export class CheckoutBootstrap { * @return {boolean} True, if a checkout form is present. */ get isPageWithCheckoutForm() { - return this.checkoutForm instanceof HTMLElement; + return null !== this.checkoutForm; } init() { @@ -65,13 +63,14 @@ export class CheckoutBootstrap { const billingData = this.#storage.getPayer(); - if ( billingData ) { - setPayerData( billingData ); - - this.checkoutForm.addEventListener( 'submit', () => - this.#onFormSubmit() - ); + if ( ! billingData ) { + return; } + + setPayerData( billingData, true ); + this.checkoutForm.addEventListener( 'submit', () => + this.#onFormSubmit() + ); } #onFormSubmit() { From b4cd6bb121e0041c1a91d57bcd840309d89deb04 Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Fri, 23 Aug 2024 11:52:00 +0200 Subject: [PATCH 42/80] =?UTF-8?q?=F0=9F=90=9B=20Fix=20critical=20Google=20?= =?UTF-8?q?Pay=20error?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../resources/js/GooglepayButton.js | 28 ++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/modules/ppcp-googlepay/resources/js/GooglepayButton.js b/modules/ppcp-googlepay/resources/js/GooglepayButton.js index f6ceafc61..6b67dab72 100644 --- a/modules/ppcp-googlepay/resources/js/GooglepayButton.js +++ b/modules/ppcp-googlepay/resources/js/GooglepayButton.js @@ -515,7 +515,9 @@ class GooglepayButton extends PaymentButton { ) ) { paymentDataRequestUpdate.newShippingOptionParameters = - updatedData.shipping_options; + this.sanitizeShippingOptions( + updatedData.shipping_options + ); } if ( updatedData.total && hasRealCart ) { @@ -541,6 +543,30 @@ class GooglepayButton extends PaymentButton { } ); } + /** + * Google Pay throws an error, when the shippingOptions entries contain + * custom properties. This function strips unsupported properties from the + * provided ajax response. + * + * @param {Object} responseData Data returned from the ajax endpoint. + * @return {Object} Sanitized object. + */ + sanitizeShippingOptions( responseData ) { + const cleanOptions = []; + + responseData.shippingOptions.forEach( ( item ) => { + cleanOptions.push( { + id: item.id, + label: item.label, + description: item.description, + } ); + } ); + + responseData.shippingOptions = cleanOptions; + + return { ...responseData, shippingOptions: cleanOptions }; + } + /** * Returns the shipping costs as numeric value. * From a467533ba65a40992db6156345e17e34c6172b40 Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Mon, 26 Aug 2024 17:10:51 +0200 Subject: [PATCH 43/80] =?UTF-8?q?=F0=9F=90=9B=20Sync=20WC=20shipping=20det?= =?UTF-8?q?ails=20with=20Google=20Pay?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This change keeps the checkout & cart forms in-sync with the Google Pay form, to ensure the form submits the same details that the user can see inside Google Pay --- .../resources/js/GooglepayButton.js | 54 +++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/modules/ppcp-googlepay/resources/js/GooglepayButton.js b/modules/ppcp-googlepay/resources/js/GooglepayButton.js index 6b67dab72..3e1fc2eb4 100644 --- a/modules/ppcp-googlepay/resources/js/GooglepayButton.js +++ b/modules/ppcp-googlepay/resources/js/GooglepayButton.js @@ -525,6 +525,11 @@ class GooglepayButton extends PaymentButton { updatedData.total, updatedData.shipping_fee ); + + // This page contains a real cart and potentially a form for shipping options. + this.syncShippingOptionWithForm( + paymentData?.shippingOptionData?.id + ); } else { transactionInfo.shippingFee = this.getShippingCosts( paymentData?.shippingOptionData?.id, @@ -728,6 +733,55 @@ class GooglepayButton extends PaymentButton { return response; } + + /** + * Updates the shipping option in the checkout form, if a form with shipping options is + * detected. + * + * @param {string} shippingOption - The shipping option ID, e.g. "flat_rate:4". + * @return {boolean} - True if a shipping option was found and selected, false otherwise. + */ + syncShippingOptionWithForm( shippingOption ) { + const wrappers = [ + // Classic checkout, Classic cart. + '.woocommerce-shipping-methods', + // Block checkout. + '.wc-block-components-shipping-rates-control', + // Block cart. + '.wc-block-components-totals-shipping', + ]; + + const sanitizedShippingOption = shippingOption.replace( /"/g, '' ); + + // Check for radio buttons with shipping options. + for ( const wrapper of wrappers ) { + const selector = `${ wrapper } input[type="radio"][value="${ sanitizedShippingOption }"]`; + const radioInput = document.querySelector( selector ); + + if ( radioInput ) { + radioInput.click(); + return true; + } + } + + // Check for select list with shipping options. + for ( const wrapper of wrappers ) { + const selector = `${ wrapper } select option[value="${ sanitizedShippingOption }"]`; + const selectOption = document.querySelector( selector ); + + if ( selectOption ) { + const selectElement = selectOption.closest( 'select' ); + + if ( selectElement ) { + selectElement.value = sanitizedShippingOption; + selectElement.dispatchEvent( new Event( 'change' ) ); + return true; + } + } + } + + return false; + } } export default GooglepayButton; From 3e0a44ca1f57b7a954ed1702cfc8a03bd0a0c411 Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Mon, 26 Aug 2024 18:22:42 +0200 Subject: [PATCH 44/80] =?UTF-8?q?=F0=9F=9A=A7=20Enqueue=20missing=20script?= =?UTF-8?q?=20for=20billing=20data=20logic?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When PayLater is disabled and we're in "continuation" context, then the new billing data logic is not working: The relevant JS script is not enqueued. --- modules/ppcp-googlepay/src/GooglepayModule.php | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/modules/ppcp-googlepay/src/GooglepayModule.php b/modules/ppcp-googlepay/src/GooglepayModule.php index b7feedc07..9079a4b9a 100644 --- a/modules/ppcp-googlepay/src/GooglepayModule.php +++ b/modules/ppcp-googlepay/src/GooglepayModule.php @@ -93,6 +93,12 @@ class GooglepayModule implements ModuleInterface { static function () use ( $c, $button ) { $smart_button = $c->get( 'button.smart-button' ); assert( $smart_button instanceof SmartButtonInterface ); + + /* + * TODO: When PayLater is disabled and we're in "continuation" context, then no JS is enqueued. + * Find a solution to enqueue the CheckoutBootstrap module in that situation. + */ + if ( $smart_button->should_load_ppcp_script() ) { $button->enqueue(); return; From 15a09d9722f8f9d1e1a862d110bed61d768373fb Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Tue, 27 Aug 2024 12:32:00 +0200 Subject: [PATCH 45/80] =?UTF-8?q?=E2=9C=A8=20Decouple=20PayerData=20from?= =?UTF-8?q?=20global=20PPCP=20object?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ppcp-button/resources/js/modules/Helper/PayerData.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/modules/ppcp-button/resources/js/modules/Helper/PayerData.js b/modules/ppcp-button/resources/js/modules/Helper/PayerData.js index df13ef92f..5695facb0 100644 --- a/modules/ppcp-button/resources/js/modules/Helper/PayerData.js +++ b/modules/ppcp-button/resources/js/modules/Helper/PayerData.js @@ -149,12 +149,12 @@ function setCheckoutBillingDetails( payer ) { export function getWooCommerceCustomerDetails() { // Populated on server-side with details about the current WooCommerce customer. - return window.PayPalCommerceGateway?.payer; + return window?.PayPalCommerceGateway?.payer; } export function getSessionBillingDetails() { // Populated by JS via `setSessionBillingDetails()` - return window.PayPalCommerceGateway?.tempPayer; + return window._PpcpPayerSessionDetails; } /** @@ -168,7 +168,7 @@ export function setSessionBillingDetails( details ) { return; } - window.PayPalCommerceGateway.tempPayer = normalizePayerDetails( details ); + window._PpcpPayerSessionDetails = normalizePayerDetails( details ); } export function payerData() { From 07c73985e3ece321284e271138822c7b0c9fdb40 Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Tue, 27 Aug 2024 12:32:53 +0200 Subject: [PATCH 46/80] =?UTF-8?q?=E2=9C=A8=20Always=20load=20GooglePay=20s?= =?UTF-8?q?cripts=20on=20checkout=20page?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- modules/ppcp-googlepay/src/GooglepayModule.php | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/modules/ppcp-googlepay/src/GooglepayModule.php b/modules/ppcp-googlepay/src/GooglepayModule.php index 9079a4b9a..114fe7f3d 100644 --- a/modules/ppcp-googlepay/src/GooglepayModule.php +++ b/modules/ppcp-googlepay/src/GooglepayModule.php @@ -94,16 +94,20 @@ class GooglepayModule implements ModuleInterface { $smart_button = $c->get( 'button.smart-button' ); assert( $smart_button instanceof SmartButtonInterface ); - /* - * TODO: When PayLater is disabled and we're in "continuation" context, then no JS is enqueued. - * Find a solution to enqueue the CheckoutBootstrap module in that situation. - */ - if ( $smart_button->should_load_ppcp_script() ) { $button->enqueue(); return; } + /* + * Checkout page, but no PPCP scripts were loaded. Most likely in continuation mode. + * Need to enqueue some Google Pay scripts to populate the billing form with details + * provided by Google Pay. + */ + if ( is_checkout() ) { + $button->enqueue(); + } + if ( has_block( 'woocommerce/checkout' ) || has_block( 'woocommerce/cart' ) ) { /** * Should add this to the ButtonInterface. From 5b054581036fab0486bf08ac9cf00f84d23671ae Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Tue, 27 Aug 2024 12:34:50 +0200 Subject: [PATCH 47/80] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Decouple=20init=20lo?= =?UTF-8?q?gic=20from=20global=20PPCP=20config?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Allow Google Pay logic to initialize on pages that do not provide a global PayPalCommerceGateway object. Required to use CheckoutBootstrap to popuplate billing fields in continuation mode. --- .../js/ContextBootstrap/CheckoutBootstrap.js | 61 +++++++++++++------ modules/ppcp-googlepay/resources/js/boot.js | 56 ++++++++++++----- 2 files changed, 83 insertions(+), 34 deletions(-) diff --git a/modules/ppcp-googlepay/resources/js/ContextBootstrap/CheckoutBootstrap.js b/modules/ppcp-googlepay/resources/js/ContextBootstrap/CheckoutBootstrap.js index 9e1c30e9f..1e1933a10 100644 --- a/modules/ppcp-googlepay/resources/js/ContextBootstrap/CheckoutBootstrap.js +++ b/modules/ppcp-googlepay/resources/js/ContextBootstrap/CheckoutBootstrap.js @@ -13,12 +13,34 @@ export class CheckoutBootstrap { #storage; /** - * @type {null|HTMLFormElement} + * @type {HTMLFormElement|null} */ - #checkoutForm = null; + #checkoutForm; + /** + * @param {GooglePayStorage} storage + */ constructor( storage ) { this.#storage = storage; + this.#checkoutForm = CheckoutBootstrap.getCheckoutForm(); + } + + /** + * Indicates if the current page contains a checkout form. + * + * @return {boolean} True if a checkout form is present. + */ + static isPageWithCheckoutForm() { + return null !== CheckoutBootstrap.getCheckoutForm(); + } + + /** + * Retrieves the WooCommerce checkout form element. + * + * @return {HTMLFormElement|null} The form, or null if not a checkout page. + */ + static getCheckoutForm() { + return document.querySelector( CHECKOUT_FORM_SELECTOR ); } /** @@ -27,37 +49,32 @@ export class CheckoutBootstrap { * @return {HTMLFormElement|null} The form, or null if not a checkout page. */ get checkoutForm() { - if ( null === this.#checkoutForm ) { - this.#checkoutForm = document.querySelector( - CHECKOUT_FORM_SELECTOR - ); - } - return this.#checkoutForm; } /** - * Indicates, if the current page contains a checkout form. + * Initializes the checkout process. * - * @return {boolean} True, if a checkout form is present. + * @throws {Error} If called on a page without a checkout form. */ - get isPageWithCheckoutForm() { - return null !== this.checkoutForm; - } - init() { - if ( ! this.isPageWithCheckoutForm ) { - return; + if ( ! this.#checkoutForm ) { + throw new Error( + 'Checkout form not found. Cannot initialize CheckoutBootstrap.' + ); } this.#populateCheckoutFields(); } + /** + * Populates checkout fields with stored or customer data. + */ #populateCheckoutFields() { const loggedInData = getWooCommerceCustomerDetails(); - // If customer is logged in, we use the details from the customer profile. if ( loggedInData ) { + // If customer is logged in, we use the details from the customer profile. return; } @@ -68,11 +85,17 @@ export class CheckoutBootstrap { } setPayerData( billingData, true ); - this.checkoutForm.addEventListener( 'submit', () => - this.#onFormSubmit() + this.checkoutForm.addEventListener( + 'submit', + this.#onFormSubmit.bind( this ) ); } + /** + * Clean-up when checkout form is submitted. + * + * Immediately removes the payer details from the localStorage. + */ #onFormSubmit() { this.#storage.clearPayer(); } diff --git a/modules/ppcp-googlepay/resources/js/boot.js b/modules/ppcp-googlepay/resources/js/boot.js index 3071998a9..666286ed4 100644 --- a/modules/ppcp-googlepay/resources/js/boot.js +++ b/modules/ppcp-googlepay/resources/js/boot.js @@ -1,3 +1,12 @@ +/** + * Initialize the GooglePay module in the front end. + * In some cases, this module is loaded when the `window.PayPalCommerceGateway` object is not + * present. In that case, the page does not contain a Google Pay button, but some other logic + * that is related to Google Pay (e.g., the CheckoutBootstrap module) + * + * @file + */ + import { loadCustomScript } from '@paypal/paypal-js'; import { loadPaypalScript } from '../../../ppcp-button/resources/js/modules/Helper/ScriptLoading'; import GooglepayManager from './GooglepayManager'; @@ -5,31 +14,48 @@ import { setupButtonEvents } from '../../../ppcp-button/resources/js/modules/Hel import { CheckoutBootstrap } from './ContextBootstrap/CheckoutBootstrap'; import moduleStorage from './Helper/GooglePayStorage'; -( function ( { buttonConfig, ppcpConfig } ) { +( function ( { buttonConfig, ppcpConfig = {} } ) { const context = ppcpConfig.context; - let manager; + function bootstrapPayButton() { + if ( ! buttonConfig || ! ppcpConfig ) { + return; + } - const bootstrap = function () { - manager = new GooglepayManager( buttonConfig, ppcpConfig ); + const manager = new GooglepayManager( buttonConfig, ppcpConfig ); manager.init(); - if ( 'continuation' === context || 'checkout' === context ) { - const checkoutBootstap = new CheckoutBootstrap( moduleStorage ); - - checkoutBootstap.init(); - } - }; - - setupButtonEvents( function () { - if ( manager ) { + setupButtonEvents( function () { manager.reinit(); + } ); + } + + function bootstrapCheckout() { + if ( context && ! [ 'continuation', 'checkout' ].includes( context ) ) { + // Context must be missing/empty, or "continuation"/"checkout" to proceed. + return; } - } ); + if ( ! CheckoutBootstrap.isPageWithCheckoutForm() ) { + return; + } + + const checkoutBootstrap = new CheckoutBootstrap( moduleStorage ); + checkoutBootstrap.init(); + } + + function bootstrap() { + bootstrapPayButton(); + bootstrapCheckout(); + } document.addEventListener( 'DOMContentLoaded', () => { if ( ! buttonConfig || ! ppcpConfig ) { - // No PayPal buttons present on this page. + /* + * No PayPal buttons present on this page, but maybe a bootstrap module needs to be + * initialized. Run bootstrap without trying to load an SDK or payment configuration. + */ + bootstrap(); + return; } From 813f24da1cc717f09bf4a7437bb0c4b42d46a1e8 Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Tue, 27 Aug 2024 12:46:15 +0200 Subject: [PATCH 48/80] =?UTF-8?q?=F0=9F=92=A1=20Update=20comment?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- modules/ppcp-googlepay/resources/js/boot.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/modules/ppcp-googlepay/resources/js/boot.js b/modules/ppcp-googlepay/resources/js/boot.js index 666286ed4..fb9e8e313 100644 --- a/modules/ppcp-googlepay/resources/js/boot.js +++ b/modules/ppcp-googlepay/resources/js/boot.js @@ -52,7 +52,8 @@ import moduleStorage from './Helper/GooglePayStorage'; if ( ! buttonConfig || ! ppcpConfig ) { /* * No PayPal buttons present on this page, but maybe a bootstrap module needs to be - * initialized. Run bootstrap without trying to load an SDK or payment configuration. + * initialized. Skip loading the SDK or gateway configuration, and directly initialize + * the module. */ bootstrap(); From 3f1b9981183ae2292c86f97eae6bebea9ae36bdb Mon Sep 17 00:00:00 2001 From: Narek Zakarian Date: Wed, 28 Aug 2024 16:52:17 +0400 Subject: [PATCH 49/80] Only remove the gateway on the WooCommerce Settings Payments tab. Additionally introduce the function to check if is WooCommerce Settings Payments tab screen (/wp-admin/admin.php?page=wc-settings&tab=checkout). --- modules/ppcp-axo/src/AxoModule.php | 8 +++++++- modules/ppcp-button/src/Helper/ContextTrait.php | 16 ++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/modules/ppcp-axo/src/AxoModule.php b/modules/ppcp-axo/src/AxoModule.php index 9b5a6985d..827918e14 100644 --- a/modules/ppcp-axo/src/AxoModule.php +++ b/modules/ppcp-axo/src/AxoModule.php @@ -16,6 +16,7 @@ use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException; use WooCommerce\PayPalCommerce\Axo\Assets\AxoManager; use WooCommerce\PayPalCommerce\Axo\Gateway\AxoGateway; use WooCommerce\PayPalCommerce\Button\Assets\SmartButtonInterface; +use WooCommerce\PayPalCommerce\Button\Helper\ContextTrait; use WooCommerce\PayPalCommerce\Onboarding\Render\OnboardingOptionsRenderer; use WooCommerce\PayPalCommerce\Vendor\Dhii\Container\ServiceProvider; use WooCommerce\PayPalCommerce\Vendor\Dhii\Modular\Module\ModuleInterface; @@ -30,6 +31,9 @@ use WooCommerce\PayPalCommerce\WcSubscriptions\Helper\SubscriptionHelper; * Class AxoModule */ class AxoModule implements ModuleInterface { + + use ContextTrait; + /** * {@inheritDoc} */ @@ -66,7 +70,9 @@ class AxoModule implements ModuleInterface { // Add the gateway in admin area. if ( is_admin() ) { - // $methods[] = $gateway; - Temporarily remove Fastlane from the payment gateway list in admin area. + if ( ! $this->is_wc_settings_payments_tab() ) { + $methods[] = $gateway; + } return $methods; } diff --git a/modules/ppcp-button/src/Helper/ContextTrait.php b/modules/ppcp-button/src/Helper/ContextTrait.php index 0c1bb4201..ec583c2f7 100644 --- a/modules/ppcp-button/src/Helper/ContextTrait.php +++ b/modules/ppcp-button/src/Helper/ContextTrait.php @@ -243,4 +243,20 @@ trait ContextTrait { $screen = get_current_screen(); return $screen && $screen->is_block_editor(); } + + /** + * Checks if is WooCommerce Settings Payments tab screen (/wp-admin/admin.php?page=wc-settings&tab=checkout). + * + * @return bool + */ + protected function is_wc_settings_payments_tab(): bool { + if ( ! is_admin() || isset( $_GET['section'] ) ) { + return false; + } + + $page = $_GET['page'] ?? ''; + $tab = $_GET['tab'] ?? ''; + + return $page === 'wc-settings' && $tab === 'checkout'; + } } From 1e9d9ec29e60923404ed614b5971a51797908b7b Mon Sep 17 00:00:00 2001 From: Narek Zakarian Date: Wed, 28 Aug 2024 17:07:50 +0400 Subject: [PATCH 50/80] Fix the coding styles --- modules/ppcp-button/src/Helper/ContextTrait.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/modules/ppcp-button/src/Helper/ContextTrait.php b/modules/ppcp-button/src/Helper/ContextTrait.php index ec583c2f7..9f58cbcdf 100644 --- a/modules/ppcp-button/src/Helper/ContextTrait.php +++ b/modules/ppcp-button/src/Helper/ContextTrait.php @@ -250,12 +250,12 @@ trait ContextTrait { * @return bool */ protected function is_wc_settings_payments_tab(): bool { - if ( ! is_admin() || isset( $_GET['section'] ) ) { + if ( ! is_admin() || isset( $_GET['section'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification return false; } - $page = $_GET['page'] ?? ''; - $tab = $_GET['tab'] ?? ''; + $page = wc_clean( wp_unslash( $_GET['page'] ?? '' ) ); // phpcs:ignore WordPress.Security.NonceVerification + $tab = wc_clean( wp_unslash( $_GET['tab'] ?? '' ) ); // phpcs:ignore WordPress.Security.NonceVerification return $page === 'wc-settings' && $tab === 'checkout'; } From ac31cfe47621bbff61842e946629531ee0f03724 Mon Sep 17 00:00:00 2001 From: Narek Zakarian Date: Wed, 28 Aug 2024 17:32:59 +0400 Subject: [PATCH 51/80] Fix the coding styles --- modules/ppcp-axo/src/AxoModule.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/ppcp-axo/src/AxoModule.php b/modules/ppcp-axo/src/AxoModule.php index 827918e14..cce8fbd14 100644 --- a/modules/ppcp-axo/src/AxoModule.php +++ b/modules/ppcp-axo/src/AxoModule.php @@ -84,7 +84,7 @@ class AxoModule implements ModuleInterface { assert( $settings instanceof Settings ); $is_paypal_enabled = $settings->has( 'enabled' ) && $settings->get( 'enabled' ) ?? false; - $is_dcc_enabled = $settings->has( 'dcc_enabled' ) && $settings->get( 'dcc_enabled' ) ?? false; + $is_dcc_enabled = $settings->has( 'dcc_enabled' ) && $settings->get( 'dcc_enabled' ) ?? false; if ( ! $is_paypal_enabled || ! $is_dcc_enabled ) { return $methods; From 381041138c9b741e95d56a9cda348c30c76b1ab5 Mon Sep 17 00:00:00 2001 From: "Alex P." Date: Sat, 31 Aug 2024 14:03:04 +0300 Subject: [PATCH 52/80] Allow to override the list of Pay Later supported countries --- modules/ppcp-api-client/services.php | 15 +++++++++++++ modules/ppcp-button/services.php | 1 + .../ppcp-button/src/Helper/MessagesApply.php | 22 +++++++------------ 3 files changed, 24 insertions(+), 14 deletions(-) diff --git a/modules/ppcp-api-client/services.php b/modules/ppcp-api-client/services.php index 6049b050b..8b2ff9af0 100644 --- a/modules/ppcp-api-client/services.php +++ b/modules/ppcp-api-client/services.php @@ -1642,6 +1642,21 @@ return array( 'SE', ); }, + + 'api.paylater-countries' => static function ( ContainerInterface $container ) : array { + return apply_filters( + 'woocommerce_paypal_payments_supported_paylater_countries', + array( + 'US', + 'DE', + 'GB', + 'FR', + 'AU', + 'IT', + 'ES', + ) + ); + }, 'api.order-helper' => static function( ContainerInterface $container ): OrderHelper { return new OrderHelper(); }, diff --git a/modules/ppcp-button/services.php b/modules/ppcp-button/services.php index 069355da6..891d4c0a9 100644 --- a/modules/ppcp-button/services.php +++ b/modules/ppcp-button/services.php @@ -320,6 +320,7 @@ return array( }, 'button.helper.messages-apply' => static function ( ContainerInterface $container ): MessagesApply { return new MessagesApply( + $container->get( 'api.paylater-countries' ), $container->get( 'api.shop.country' ) ); }, diff --git a/modules/ppcp-button/src/Helper/MessagesApply.php b/modules/ppcp-button/src/Helper/MessagesApply.php index c0d0b1d6b..4f4a3d9f6 100644 --- a/modules/ppcp-button/src/Helper/MessagesApply.php +++ b/modules/ppcp-button/src/Helper/MessagesApply.php @@ -18,17 +18,9 @@ class MessagesApply { /** * In which countries credit messaging is available. * - * @var array + * @var string[] */ - private $countries = array( - 'US', - 'DE', - 'GB', - 'FR', - 'AU', - 'IT', - 'ES', - ); + private $allowed_countries; /** * 2-letter country code of the shop. @@ -40,10 +32,12 @@ class MessagesApply { /** * MessagesApply constructor. * - * @param string $country 2-letter country code of the shop. + * @param string[] $allowed_countries In which countries credit messaging is available. + * @param string $country 2-letter country code of the shop. */ - public function __construct( string $country ) { - $this->country = $country; + public function __construct( array $allowed_countries, string $country ) { + $this->allowed_countries = $allowed_countries; + $this->country = $country; } /** @@ -52,6 +46,6 @@ class MessagesApply { * @return bool */ public function for_country(): bool { - return in_array( $this->country, $this->countries, true ); + return in_array( $this->country, $this->allowed_countries, true ); } } From 65a4837935343992fdebab5b420712557827fa75 Mon Sep 17 00:00:00 2001 From: "Alex P." Date: Mon, 2 Sep 2024 12:03:08 +0300 Subject: [PATCH 53/80] Add more feature statuses into system report --- .../src/StatusReportModule.php | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/modules/ppcp-status-report/src/StatusReportModule.php b/modules/ppcp-status-report/src/StatusReportModule.php index a7e810abe..eea23a9e9 100644 --- a/modules/ppcp-status-report/src/StatusReportModule.php +++ b/modules/ppcp-status-report/src/StatusReportModule.php @@ -175,6 +175,38 @@ class StatusReportModule implements ModuleInterface { $subscriptions_mode_settings ), ), + array( + 'label' => esc_html__( 'PayPal Shipping Callback', 'woocommerce-paypal-payments' ), + 'exported_label' => 'PayPal Shipping Callback', + 'description' => esc_html__( 'Whether the "Require final confirmation on checkout" setting is enabled.', 'woocommerce-paypal-payments' ), + 'value' => $this->bool_to_html( + $settings->has( 'blocks_final_review_enabled' ) && $settings->get( 'blocks_final_review_enabled' ) + ), + ), + array( + 'label' => esc_html__( 'Apple Pay', 'woocommerce-paypal-payments' ), + 'exported_label' => 'Apple Pay', + 'description' => esc_html__( 'Whether Apple Pay is enabled.', 'woocommerce-paypal-payments' ), + 'value' => $this->bool_to_html( + $settings->has( 'applepay_button_enabled' ) && $settings->get( 'applepay_button_enabled' ) + ), + ), + array( + 'label' => esc_html__( 'Google Pay', 'woocommerce-paypal-payments' ), + 'exported_label' => 'Google Pay', + 'description' => esc_html__( 'Whether Google Pay is enabled.', 'woocommerce-paypal-payments' ), + 'value' => $this->bool_to_html( + $settings->has( 'googlepay_button_enabled' ) && $settings->get( 'googlepay_button_enabled' ) + ), + ), + array( + 'label' => esc_html__( 'Fastlane', 'woocommerce-paypal-payments' ), + 'exported_label' => 'Fastlane', + 'description' => esc_html__( 'Whether Fastlane is enabled.', 'woocommerce-paypal-payments' ), + 'value' => $this->bool_to_html( + $settings->has( 'axo_enabled' ) && $settings->get( 'axo_enabled' ) + ), + ), ); echo wp_kses_post( From c3c9beb7223404d0454325a178f84f4f598c27da Mon Sep 17 00:00:00 2001 From: Narek Zakarian Date: Mon, 2 Sep 2024 16:24:01 +0400 Subject: [PATCH 54/80] Extend list of fastlane incompatible plugins. --- .../src/Helper/SettingsNoticeGenerator.php | 21 ++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/modules/ppcp-axo/src/Helper/SettingsNoticeGenerator.php b/modules/ppcp-axo/src/Helper/SettingsNoticeGenerator.php index 503e135cb..713c9bc2d 100644 --- a/modules/ppcp-axo/src/Helper/SettingsNoticeGenerator.php +++ b/modules/ppcp-axo/src/Helper/SettingsNoticeGenerator.php @@ -117,9 +117,24 @@ class SettingsNoticeGenerator { * @return string */ public function generate_incompatible_plugins_notice(): string { - $incompatible_plugins = array( - 'Elementor' => did_action( 'elementor/loaded' ), - 'CheckoutWC' => defined( 'CFW_NAME' ), + /** + * Filters the list of Fastlane incompatible plugins. + */ + $incompatible_plugins = apply_filters( + 'woocommerce_paypal_payments_fastlane_incompatible_plugins', + array( + 'Elementor' => did_action( 'elementor/loaded' ), + 'CheckoutWC' => defined( 'CFW_NAME' ), + 'WCDirectCheckout' => defined( 'QLWCDC_PLUGIN_NAME' ), + 'WPMultiStepCheckout' => class_exists( 'WPMultiStepCheckout' ), + 'FluidCheckout' => class_exists( 'FluidCheckout' ), + 'THWMSCF_Multistep_Checkout' => class_exists( 'THWMSCF_Multistep_Checkout' ), + 'WC_Subscriptions' => class_exists( 'WC_Subscriptions' ), + 'Cartflows' => class_exists( 'Cartflows_Loader' ), + 'FunnelKitFunnelBuilder' => class_exists( 'WFFN_Core' ), + 'WCAllProductsForSubscriptions' => class_exists( 'WCS_ATT' ), + 'WCOnePageCheckout' => class_exists( 'PP_One_Page_Checkout' ), + ) ); $active_plugins_list = array_filter( $incompatible_plugins ); From 901774b8cc0a574b9a37d6ced5d82d6afea5bd7c Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Mon, 2 Sep 2024 16:40:14 +0200 Subject: [PATCH 55/80] =?UTF-8?q?=E2=9C=A8=20Add=20phone-observer=20for=20?= =?UTF-8?q?Gary?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Push the phone number from the Woo checkout form to the Fastlane component. --- modules/ppcp-axo/resources/js/AxoManager.js | 82 +++++++++++++++++++-- 1 file changed, 75 insertions(+), 7 deletions(-) diff --git a/modules/ppcp-axo/resources/js/AxoManager.js b/modules/ppcp-axo/resources/js/AxoManager.js index 34b8d24c5..a195dc71e 100644 --- a/modules/ppcp-axo/resources/js/AxoManager.js +++ b/modules/ppcp-axo/resources/js/AxoManager.js @@ -11,7 +11,23 @@ import { } from '../../../ppcp-button/resources/js/modules/Helper/ButtonDisabler'; import { getCurrentPaymentMethod } from '../../../ppcp-button/resources/js/modules/Helper/CheckoutMethodState'; +/** + * Internal customer details. + * + * @typedef {Object} CustomerDetails + * @property {null|string} email - Customer email. + * @property {null|string} phone - Fastlane phone number. + * @property {null|Object} billing - Billing details object. + * @property {null|Object} shipping - Shipping details object. + * @property {null|Object} card - Payment details object. + */ + class AxoManager { + /** + * @type {CustomerDetails} + */ + data = {}; + constructor( axoConfig, ppcpConfig ) { this.axoConfig = axoConfig; this.ppcpConfig = ppcpConfig; @@ -30,12 +46,7 @@ class AxoManager { hasCard: false, }; - this.data = { - email: null, - billing: null, - shipping: null, - card: null, - }; + this.clearData(); this.states = this.axoConfig.woocommerce.states; @@ -104,6 +115,17 @@ class AxoManager { this.triggerGatewayChange(); } + /** + * Checks if the current flow is the "Ryan flow": Ryan is a known customer who created a + * Fastlane profile before. Ryan can leverage all benefits of the accelerated 1-click checkout. + * + * @return {boolean} True means, the Fastlane could link the customer's email to an existing + * account. + */ + get isRyanFlow() { + return !! this.data.card; + } + registerEventHandlers() { this.$( document ).on( 'change', @@ -860,6 +882,8 @@ class AxoManager { ) ).render( this.el.paymentContainer.selector + '-form' ); } + + this.syncPhoneFromWooToFastlane(); } disableGatewaySelection() { @@ -873,6 +897,7 @@ class AxoManager { clearData() { this.data = { email: null, + phone: null, billing: null, shipping: null, card: null, @@ -935,7 +960,7 @@ class AxoManager { } cardComponentData() { - return { + const config = { fields: { cardholderName: { enabled: this.axoConfig.name_on_card === '1', @@ -945,6 +970,28 @@ class AxoManager { this.axoConfig.style_options ), }; + + if ( this.data.phone ) { + config.fields.phoneNumber = { + placeholder: this.data.phone, + prefill: this.data.phone, + }; + } + + return config; + } + + /** + * Refreshes the Fastlane UI component, using configuration provided by the `cardComponentData()` method. + * + * @return {Promise<*>} Resolves when the component was refreshed. + */ + async refreshFastlaneComponent() { + const elem = this.el.paymentContainer.selector + '-form'; + const config = this.cardComponentData(); + + const component = await this.fastlane.FastlaneCardComponent( config ); + return component.render( elem ); } tokenizeData() { @@ -1169,6 +1216,27 @@ class AxoManager { this.$( '#billing_email_field input' ).on( 'input', reEnableInput ); this.$( '#billing_email_field input' ).on( 'click', reEnableInput ); } + + syncPhoneFromWooToFastlane() { + // Ryan is a known customer, we do not need his phone number. + if ( this.isRyanFlow ) { + return; + } + + const phoneEl = document.querySelector( '#billing_phone' ); + + if ( ! phoneEl ) { + return; + } + + const onWooPhoneChanged = async ( ev ) => { + this.data.phone = ev.target.value; + + await this.refreshFastlaneComponent(); + }; + + phoneEl.addEventListener( 'change', onWooPhoneChanged ); + } } export default AxoManager; From b0359c9de81d29abf6cf8ac079cbcdb4a432356b Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Mon, 2 Sep 2024 16:42:04 +0200 Subject: [PATCH 56/80] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Minor=20code=20clean?= =?UTF-8?q?up?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Define all properties on class level (instead of implicit declaration in constructor) - Use the new `isRyanFlow` check - Use the new `this.refreshFastlaneComponent()` method --- modules/ppcp-axo/resources/js/AxoManager.js | 48 +++++++++++---------- 1 file changed, 25 insertions(+), 23 deletions(-) diff --git a/modules/ppcp-axo/resources/js/AxoManager.js b/modules/ppcp-axo/resources/js/AxoManager.js index a195dc71e..8fb2f3e4d 100644 --- a/modules/ppcp-axo/resources/js/AxoManager.js +++ b/modules/ppcp-axo/resources/js/AxoManager.js @@ -23,21 +23,36 @@ import { getCurrentPaymentMethod } from '../../../ppcp-button/resources/js/modul */ class AxoManager { + axoConfig = null; + ppcpConfig = null; + $ = null; + + initialized = false; + hideGatewaySelection = false; + fastlane = null; + phoneNumber = null; + /** * @type {CustomerDetails} */ data = {}; + status = {}; + styles = {}; + locale = 'en_us'; + + el = null; + emailInput = null; + shippingView = null; + billingView = null; + cardView = null; constructor( axoConfig, ppcpConfig ) { this.axoConfig = axoConfig; this.ppcpConfig = ppcpConfig; - this.initialized = false; this.fastlane = new Fastlane(); this.$ = jQuery; - this.hideGatewaySelection = false; - this.status = { active: false, validEmail: false, @@ -48,6 +63,7 @@ class AxoManager { this.clearData(); + // TODO - Do we need a public `states` property for this? this.states = this.axoConfig.woocommerce.states; this.el = new DomElementCollection(); @@ -62,8 +78,6 @@ class AxoManager { }, }; - this.locale = 'en_us'; - this.registerEventHandlers(); this.shippingView = new ShippingView( @@ -814,11 +828,7 @@ class AxoManager { if ( authResponse.profileData.card ) { this.setStatus( 'hasCard', true ); } else { - this.cardComponent = ( - await this.fastlane.FastlaneCardComponent( - this.cardComponentData() - ) - ).render( this.el.paymentContainer.selector + '-form' ); + this.cardComponent = await this.refreshFastlaneComponent(); } const cardBillingAddress = @@ -860,11 +870,7 @@ class AxoManager { await this.renderWatermark( true ); - this.cardComponent = ( - await this.fastlane.FastlaneCardComponent( - this.cardComponentData() - ) - ).render( this.el.paymentContainer.selector + '-form' ); + this.cardComponent = await this.refreshFastlaneComponent(); } } else { // No profile found with this email address. @@ -876,11 +882,7 @@ class AxoManager { await this.renderWatermark( true ); - this.cardComponent = ( - await this.fastlane.FastlaneCardComponent( - this.cardComponentData() - ) - ).render( this.el.paymentContainer.selector + '-form' ); + this.cardComponent = await this.refreshFastlaneComponent(); } this.syncPhoneFromWooToFastlane(); @@ -922,7 +924,7 @@ class AxoManager { onClickSubmitButton() { // TODO: validate data. - if ( this.data.card ) { + if ( this.isRyanFlow ) { // Ryan flow log( 'Starting Ryan flow.' ); @@ -1128,8 +1130,8 @@ class AxoManager { ensureBillingPhoneNumber( data ) { if ( data.billing_phone === '' ) { let phone = ''; - const cc = this.data?.shipping?.phoneNumber?.countryCode; - const number = this.data?.shipping?.phoneNumber?.nationalNumber; + const cc = this.data.shipping?.phoneNumber?.countryCode; + const number = this.data.shipping?.phoneNumber?.nationalNumber; if ( cc ) { phone = `+${ cc } `; From a4c2569a87b671d1bd3a567d243258767fe7b2f6 Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Mon, 2 Sep 2024 18:27:29 +0200 Subject: [PATCH 57/80] =?UTF-8?q?=E2=9C=A8=20Sanitize=20number,=20only=20s?= =?UTF-8?q?ync=20valid=20US=20numbers?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Invalid phone numbers do not trigger a Fastlane refresh --- modules/ppcp-axo/resources/js/AxoManager.js | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/modules/ppcp-axo/resources/js/AxoManager.js b/modules/ppcp-axo/resources/js/AxoManager.js index 8fb2f3e4d..8f2e4afc6 100644 --- a/modules/ppcp-axo/resources/js/AxoManager.js +++ b/modules/ppcp-axo/resources/js/AxoManager.js @@ -975,7 +975,6 @@ class AxoManager { if ( this.data.phone ) { config.fields.phoneNumber = { - placeholder: this.data.phone, prefill: this.data.phone, }; } @@ -1231,9 +1230,22 @@ class AxoManager { return; } - const onWooPhoneChanged = async ( ev ) => { - this.data.phone = ev.target.value; + const sanitizePhoneNumber = ( number ) => { + const localNumber = number.replace( /^\+1/, '' ); + const cleanNumber = localNumber.replace( /\D/g, '' ); + // All valid US mobile numbers have exactly 10 digits. + return cleanNumber.length === 10 ? cleanNumber : null; + }; + + const onWooPhoneChanged = async ( ev ) => { + const cleanPhoneNumber = sanitizePhoneNumber( ev.target.value ); + + if ( ! cleanPhoneNumber ) { + return; + } + + this.data.phone = cleanPhoneNumber; await this.refreshFastlaneComponent(); }; From c2400429cdb1226fd5397af0569d14e379f10156 Mon Sep 17 00:00:00 2001 From: "Alex P." Date: Tue, 3 Sep 2024 08:33:17 +0300 Subject: [PATCH 58/80] Remove 3.0 upgrade notice no longer needed --- woocommerce-paypal-payments.php | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/woocommerce-paypal-payments.php b/woocommerce-paypal-payments.php index afae61309..9514f9f04 100644 --- a/woocommerce-paypal-payments.php +++ b/woocommerce-paypal-payments.php @@ -223,22 +223,6 @@ define( 'PPCP_PAYPAL_BN_CODE', 'Woo_PPCP' ); } ); - add_action( - 'in_plugin_update_message-woocommerce-paypal-payments/woocommerce-paypal-payments.php', - static function( array $plugin_data, \stdClass $new_data ) { - if ( version_compare( $plugin_data['Version'], '3.0.0', '<' ) && - version_compare( $new_data->new_version, '3.0.0', '>=' ) ) { - printf( - '
%s: %s', - esc_html__( 'Warning', 'woocommerce-paypal-payments' ), - esc_html__( 'WooCommerce PayPal Payments version 3.0.0 contains significant changes that may impact your website. We strongly recommend reviewing the changes and testing the update on a staging site before updating it on your production environment.', 'woocommerce-paypal-payments' ) - ); - } - }, - 10, - 2 - ); - /** * Check if WooCommerce is active. * From 1efb3a0087bd509cf3571cbeb0f6b75b938167c6 Mon Sep 17 00:00:00 2001 From: Narek Zakarian Date: Tue, 3 Sep 2024 15:05:23 +0400 Subject: [PATCH 59/80] Refactor the configuration to include full names --- modules/ppcp-axo/services.php | 116 +++++++++++++++--- .../src/Helper/SettingsNoticeGenerator.php | 51 +++----- 2 files changed, 118 insertions(+), 49 deletions(-) diff --git a/modules/ppcp-axo/services.php b/modules/ppcp-axo/services.php index 5b468835f..890029ad5 100644 --- a/modules/ppcp-axo/services.php +++ b/modules/ppcp-axo/services.php @@ -21,14 +21,14 @@ use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings; return array( // If AXO can be configured. - 'axo.eligible' => static function ( ContainerInterface $container ): bool { + 'axo.eligible' => static function ( ContainerInterface $container ): bool { $apm_applies = $container->get( 'axo.helpers.apm-applies' ); assert( $apm_applies instanceof ApmApplies ); return $apm_applies->for_country_currency(); }, - 'axo.helpers.apm-applies' => static function ( ContainerInterface $container ) : ApmApplies { + 'axo.helpers.apm-applies' => static function ( ContainerInterface $container ) : ApmApplies { return new ApmApplies( $container->get( 'axo.supported-country-currency-matrix' ), $container->get( 'api.shop.currency' ), @@ -36,16 +36,16 @@ return array( ); }, - 'axo.helpers.settings-notice-generator' => static function ( ContainerInterface $container ) : SettingsNoticeGenerator { - return new SettingsNoticeGenerator(); + 'axo.helpers.settings-notice-generator' => static function ( ContainerInterface $container ) : SettingsNoticeGenerator { + return new SettingsNoticeGenerator( $container->get( 'axo.fastlane-incompatible-plugin-names' ) ); }, // If AXO is configured and onboarded. - 'axo.available' => static function ( ContainerInterface $container ): bool { + 'axo.available' => static function ( ContainerInterface $container ): bool { return true; }, - 'axo.url' => static function ( ContainerInterface $container ): string { + 'axo.url' => static function ( ContainerInterface $container ): string { $path = realpath( __FILE__ ); if ( false === $path ) { return ''; @@ -56,7 +56,7 @@ return array( ); }, - 'axo.manager' => static function ( ContainerInterface $container ): AxoManager { + 'axo.manager' => static function ( ContainerInterface $container ): AxoManager { return new AxoManager( $container->get( 'axo.url' ), $container->get( 'ppcp.asset-version' ), @@ -70,7 +70,7 @@ return array( ); }, - 'axo.gateway' => static function ( ContainerInterface $container ): AxoGateway { + 'axo.gateway' => static function ( ContainerInterface $container ): AxoGateway { return new AxoGateway( $container->get( 'wcgateway.settings.render' ), $container->get( 'wcgateway.settings' ), @@ -87,7 +87,7 @@ return array( ); }, - 'axo.card_icons' => static function ( ContainerInterface $container ): array { + 'axo.card_icons' => static function ( ContainerInterface $container ): array { return array( array( 'title' => 'Visa', @@ -108,7 +108,7 @@ return array( ); }, - 'axo.card_icons.axo' => static function ( ContainerInterface $container ): array { + 'axo.card_icons.axo' => static function ( ContainerInterface $container ): array { return array( array( 'title' => 'Visa', @@ -144,7 +144,7 @@ return array( /** * The matrix which countries and currency combinations can be used for AXO. */ - 'axo.supported-country-currency-matrix' => static function ( ContainerInterface $container ) : array { + 'axo.supported-country-currency-matrix' => static function ( ContainerInterface $container ) : array { /** * Returns which countries and currency combinations can be used for AXO. */ @@ -163,7 +163,7 @@ return array( ); }, - 'axo.settings-conflict-notice' => static function ( ContainerInterface $container ) : string { + 'axo.settings-conflict-notice' => static function ( ContainerInterface $container ) : string { $settings_notice_generator = $container->get( 'axo.helpers.settings-notice-generator' ); assert( $settings_notice_generator instanceof SettingsNoticeGenerator ); @@ -173,28 +173,28 @@ return array( return $settings_notice_generator->generate_settings_conflict_notice( $settings ); }, - 'axo.checkout-config-notice' => static function ( ContainerInterface $container ) : string { + 'axo.checkout-config-notice' => static function ( ContainerInterface $container ) : string { $settings_notice_generator = $container->get( 'axo.helpers.settings-notice-generator' ); assert( $settings_notice_generator instanceof SettingsNoticeGenerator ); return $settings_notice_generator->generate_checkout_notice(); }, - 'axo.shipping-config-notice' => static function ( ContainerInterface $container ) : string { + 'axo.shipping-config-notice' => static function ( ContainerInterface $container ) : string { $settings_notice_generator = $container->get( 'axo.helpers.settings-notice-generator' ); assert( $settings_notice_generator instanceof SettingsNoticeGenerator ); return $settings_notice_generator->generate_shipping_notice(); }, - 'axo.incompatible-plugins-notice' => static function ( ContainerInterface $container ) : string { + 'axo.incompatible-plugins-notice' => static function ( ContainerInterface $container ) : string { $settings_notice_generator = $container->get( 'axo.helpers.settings-notice-generator' ); assert( $settings_notice_generator instanceof SettingsNoticeGenerator ); return $settings_notice_generator->generate_incompatible_plugins_notice(); }, - 'axo.smart-button-location-notice' => static function ( ContainerInterface $container ) : string { + 'axo.smart-button-location-notice' => static function ( ContainerInterface $container ) : string { $settings = $container->get( 'wcgateway.settings' ); assert( $settings instanceof Settings ); @@ -222,10 +222,92 @@ return array( return '

' . $notice_content . '

'; }, - 'axo.endpoint.frontend-logger' => static function ( ContainerInterface $container ): FrontendLoggerEndpoint { + 'axo.endpoint.frontend-logger' => static function ( ContainerInterface $container ): FrontendLoggerEndpoint { return new FrontendLoggerEndpoint( $container->get( 'button.request-data' ), $container->get( 'woocommerce.logger.woocommerce' ) ); }, + + /** + * The list of Fastlane incompatible plugins. + * + * @returns array + */ + 'axo.fastlane-incompatible-plugins' => static function () : array { + /** + * Filters the list of Fastlane incompatible plugins. + */ + return apply_filters( + 'woocommerce_paypal_payments_fastlane_incompatible_plugins', + array( + array( + 'name' => 'Elementor', + 'isActive' => did_action( 'elementor/loaded' ), + ), + array( + 'name' => 'CheckoutWC', + 'isActive' => defined( 'CFW_NAME' ), + ), + array( + 'name' => 'Direct Checkout for WooCommerce', + 'isActive' => defined( 'QLWCDC_PLUGIN_NAME' ), + ), + array( + 'name' => 'Multi-Step Checkout for WooCommerce', + 'isActive' => class_exists( 'WPMultiStepCheckout' ), + ), + array( + 'name' => 'Fluid Checkout for WooCommerce', + 'isActive' => class_exists( 'FluidCheckout' ), + ), + array( + 'name' => 'MultiStep Checkout for WooCommerce', + 'isActive' => class_exists( 'THWMSCF_Multistep_Checkout' ), + ), + array( + 'name' => 'WooCommerce Subscriptions', + 'isActive' => class_exists( 'WC_Subscriptions' ), + ), + array( + 'name' => 'CartFlows', + 'isActive' => class_exists( 'Cartflows_Loader' ), + ), + array( + 'name' => 'FunnelKit Funnel Builder', + 'isActive' => class_exists( 'WFFN_Core' ), + ), + array( + 'name' => 'WooCommerce One Page Checkout', + 'isActive' => class_exists( 'PP_One_Page_Checkout' ), + ), + array( + 'name' => 'All Products for Woo Subscriptions', + 'isActive' => class_exists( 'WCS_ATT' ), + ), + ) + ); + }, + + 'axo.fastlane-incompatible-plugin-names' => static function ( ContainerInterface $container ) : array { + $incompatible_plugins = $container->get( 'axo.fastlane-incompatible-plugins' ); + + $active_plugins_list = array_filter( + $incompatible_plugins, + function( $plugin ) { + return $plugin['isActive']; + } + ); + + if ( empty( $active_plugins_list ) ) { + return []; + } + + return array_map( + function ( array $plugin ): string { + return "
  • {$plugin['name']}
  • "; + }, + $active_plugins_list + ); + }, ); diff --git a/modules/ppcp-axo/src/Helper/SettingsNoticeGenerator.php b/modules/ppcp-axo/src/Helper/SettingsNoticeGenerator.php index 713c9bc2d..72208d566 100644 --- a/modules/ppcp-axo/src/Helper/SettingsNoticeGenerator.php +++ b/modules/ppcp-axo/src/Helper/SettingsNoticeGenerator.php @@ -18,6 +18,22 @@ use WooCommerce\PayPalCommerce\WcGateway\Exception\NotFoundException; * Class SettingsNoticeGenerator */ class SettingsNoticeGenerator { + /** + * The list of Fastlane incompatible plugin names. + * + * @var string[] + */ + protected $incompatible_plugin_names; + + /** + * SettingsNoticeGenerator constructor. + * + * @param string[] $incompatible_plugin_names The list of Fastlane incompatible plugin names. + */ + public function __construct( array $incompatible_plugin_names ) { + $this->incompatible_plugin_names = $incompatible_plugin_names; + } + /** * Generates the full HTML of the notification. * @@ -95,7 +111,7 @@ class SettingsNoticeGenerator { public function generate_shipping_notice(): string { $shipping_settings_link = admin_url( 'admin.php?page=wc-settings&tab=shipping§ion=options' ); - $notice_content = ''; + $notice_content = 'asd'; if ( wc_shipping_enabled() && wc_ship_to_billing_address_only() ) { $notice_content = sprintf( @@ -117,39 +133,10 @@ class SettingsNoticeGenerator { * @return string */ public function generate_incompatible_plugins_notice(): string { - /** - * Filters the list of Fastlane incompatible plugins. - */ - $incompatible_plugins = apply_filters( - 'woocommerce_paypal_payments_fastlane_incompatible_plugins', - array( - 'Elementor' => did_action( 'elementor/loaded' ), - 'CheckoutWC' => defined( 'CFW_NAME' ), - 'WCDirectCheckout' => defined( 'QLWCDC_PLUGIN_NAME' ), - 'WPMultiStepCheckout' => class_exists( 'WPMultiStepCheckout' ), - 'FluidCheckout' => class_exists( 'FluidCheckout' ), - 'THWMSCF_Multistep_Checkout' => class_exists( 'THWMSCF_Multistep_Checkout' ), - 'WC_Subscriptions' => class_exists( 'WC_Subscriptions' ), - 'Cartflows' => class_exists( 'Cartflows_Loader' ), - 'FunnelKitFunnelBuilder' => class_exists( 'WFFN_Core' ), - 'WCAllProductsForSubscriptions' => class_exists( 'WCS_ATT' ), - 'WCOnePageCheckout' => class_exists( 'PP_One_Page_Checkout' ), - ) - ); - - $active_plugins_list = array_filter( $incompatible_plugins ); - - if ( empty( $active_plugins_list ) ) { + if ( empty( $this->incompatible_plugin_names ) ) { return ''; } - $incompatible_plugin_items = array_map( - function ( $plugin ) { - return "
  • {$plugin}
  • "; - }, - array_keys( $active_plugins_list ) - ); - $plugins_settings_link = esc_url( admin_url( 'plugins.php' ) ); $notice_content = sprintf( /* translators: %1$s: URL to the plugins settings page. %2$s: List of incompatible plugins. */ @@ -158,7 +145,7 @@ class SettingsNoticeGenerator { 'woocommerce-paypal-payments' ), $plugins_settings_link, - implode( '', $incompatible_plugin_items ) + implode( '', $this->incompatible_plugin_names ) ); return '

    ' . $notice_content . '

    '; From cc06b0101ab760021678d98a64d0955231414fad Mon Sep 17 00:00:00 2001 From: Narek Zakarian Date: Tue, 3 Sep 2024 15:05:58 +0400 Subject: [PATCH 60/80] Fix the coding styles --- modules/ppcp-axo/services.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/ppcp-axo/services.php b/modules/ppcp-axo/services.php index 890029ad5..074e5e63e 100644 --- a/modules/ppcp-axo/services.php +++ b/modules/ppcp-axo/services.php @@ -300,7 +300,7 @@ return array( ); if ( empty( $active_plugins_list ) ) { - return []; + return array(); } return array_map( From 1398c4ac97d9fdf6b8e624969db76a43d48adb1d Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Tue, 3 Sep 2024 13:16:43 +0200 Subject: [PATCH 61/80] =?UTF-8?q?=F0=9F=90=9B=20Refactor=20code=20to=20add?= =?UTF-8?q?ress=20some=20edge=20cases?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When user selects a different gateway, enters a phone number, and then switches back to Fastlane: The existing phone number is correctly synced to Fastlane on initial component render. --- modules/ppcp-axo/resources/js/AxoManager.js | 108 +++++++++++++------- 1 file changed, 71 insertions(+), 37 deletions(-) diff --git a/modules/ppcp-axo/resources/js/AxoManager.js b/modules/ppcp-axo/resources/js/AxoManager.js index 8f2e4afc6..f676ac486 100644 --- a/modules/ppcp-axo/resources/js/AxoManager.js +++ b/modules/ppcp-axo/resources/js/AxoManager.js @@ -42,6 +42,7 @@ class AxoManager { el = null; emailInput = null; + phoneInput = null; shippingView = null; billingView = null; cardView = null; @@ -126,6 +127,9 @@ class AxoManager { } } + this.onChangePhone = this.onChangePhone.bind( this ); + this.initPhoneSyncWooToFastlane(); + this.triggerGatewayChange(); } @@ -494,6 +498,7 @@ class AxoManager { this.initPlacements(); this.initFastlane(); this.setStatus( 'active', true ); + this.setPhoneFromWoo(); log( `Attempt on activation - emailInput: ${ this.emailInput.value }` ); log( @@ -726,6 +731,48 @@ class AxoManager { } } + /** + * Locates the WooCommerce checkout "billing phone" field and adds event listeners to it. + */ + initPhoneSyncWooToFastlane() { + this.phoneInput = document.querySelector( '#billing_phone' ); + this.phoneInput?.addEventListener( 'change', this.onChangePhone ); + } + + /** + * Strips the country prefix and non-numeric characters from the phone number. If the remaining + * phone number is valid, it's returned. Otherwise, the function returns null. + * + * @param {string} number - Phone number to sanitize. + * @return {string|null} A valid US phone number, or null if the number is invalid. + */ + sanitizePhoneNumber( number ) { + const localNumber = number.replace( /^\+1/, '' ); + const cleanNumber = localNumber.replace( /\D/g, '' ); + + // All valid US mobile numbers have exactly 10 digits. + return cleanNumber.length === 10 ? cleanNumber : null; + } + + /** + * Reads the phone number from the WooCommerce checkout form, sanitizes it, and (if valid) + * stores it in the internal customer details object. + */ + setPhoneFromWoo() { + if ( ! this.phoneInput ) { + return; + } + + const phoneNumber = this.phoneInput.value; + const cleanPhoneNumber = this.sanitizePhoneNumber( phoneNumber ); + + if ( ! cleanPhoneNumber ) { + return; + } + + this.data.phone = cleanPhoneNumber; + } + async onChangeEmail() { this.clearData(); @@ -769,6 +816,8 @@ class AxoManager { this.data.email = this.emailInput.value; this.billingView.setData( this.data ); + this.setPhoneFromWoo(); + if ( ! this.fastlane.identity ) { log( 'Not initialized.' ); return; @@ -793,6 +842,22 @@ class AxoManager { this.enableGatewaySelection(); } + /** + * Event handler that fires when the customer changed the phone number in the WooCommerce + * checkout form. If Fastlane is active, the component is refreshed. + * + * @return {Promise} + */ + async onChangePhone() { + this.setPhoneFromWoo(); + + if ( this.status.active ) { + await this.refreshFastlaneComponent(); + } + + return Promise.resolve(); + } + async lookupCustomerByEmail() { const lookupResponse = await this.fastlane.identity.lookupCustomerByEmail( @@ -884,8 +949,6 @@ class AxoManager { this.cardComponent = await this.refreshFastlaneComponent(); } - - this.syncPhoneFromWooToFastlane(); } disableGatewaySelection() { @@ -973,7 +1036,8 @@ class AxoManager { ), }; - if ( this.data.phone ) { + // Ryan is a known customer, we do not need his phone number. + if ( this.data.phone && ! this.isRyanFlow ) { config.fields.phoneNumber = { prefill: this.data.phone, }; @@ -988,6 +1052,10 @@ class AxoManager { * @return {Promise<*>} Resolves when the component was refreshed. */ async refreshFastlaneComponent() { + if ( ! this.status.active ) { + return Promise.resolve(); + } + const elem = this.el.paymentContainer.selector + '-form'; const config = this.cardComponentData(); @@ -1217,40 +1285,6 @@ class AxoManager { this.$( '#billing_email_field input' ).on( 'input', reEnableInput ); this.$( '#billing_email_field input' ).on( 'click', reEnableInput ); } - - syncPhoneFromWooToFastlane() { - // Ryan is a known customer, we do not need his phone number. - if ( this.isRyanFlow ) { - return; - } - - const phoneEl = document.querySelector( '#billing_phone' ); - - if ( ! phoneEl ) { - return; - } - - const sanitizePhoneNumber = ( number ) => { - const localNumber = number.replace( /^\+1/, '' ); - const cleanNumber = localNumber.replace( /\D/g, '' ); - - // All valid US mobile numbers have exactly 10 digits. - return cleanNumber.length === 10 ? cleanNumber : null; - }; - - const onWooPhoneChanged = async ( ev ) => { - const cleanPhoneNumber = sanitizePhoneNumber( ev.target.value ); - - if ( ! cleanPhoneNumber ) { - return; - } - - this.data.phone = cleanPhoneNumber; - await this.refreshFastlaneComponent(); - }; - - phoneEl.addEventListener( 'change', onWooPhoneChanged ); - } } export default AxoManager; From a803467b9634464fcea9aacb5da494fbd1f3a177 Mon Sep 17 00:00:00 2001 From: Narek Zakarian Date: Tue, 3 Sep 2024 15:19:38 +0400 Subject: [PATCH 62/80] Fix the coding styles --- modules/ppcp-axo/services.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/ppcp-axo/services.php b/modules/ppcp-axo/services.php index 074e5e63e..1ad8cfe39 100644 --- a/modules/ppcp-axo/services.php +++ b/modules/ppcp-axo/services.php @@ -294,8 +294,8 @@ return array( $active_plugins_list = array_filter( $incompatible_plugins, - function( $plugin ) { - return $plugin['isActive']; + function( array $plugin ): bool { + return (bool) $plugin['isActive']; } ); From ed98033d104451be3c6c6c380d053d67c7133ecf Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Tue, 3 Sep 2024 13:21:25 +0200 Subject: [PATCH 63/80] =?UTF-8?q?=F0=9F=92=AC=20Minor=20changes=20in=20com?= =?UTF-8?q?ment=20&=20variable=20name?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- modules/ppcp-axo/resources/js/AxoManager.js | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/modules/ppcp-axo/resources/js/AxoManager.js b/modules/ppcp-axo/resources/js/AxoManager.js index f676ac486..a56348e12 100644 --- a/modules/ppcp-axo/resources/js/AxoManager.js +++ b/modules/ppcp-axo/resources/js/AxoManager.js @@ -137,8 +137,7 @@ class AxoManager { * Checks if the current flow is the "Ryan flow": Ryan is a known customer who created a * Fastlane profile before. Ryan can leverage all benefits of the accelerated 1-click checkout. * - * @return {boolean} True means, the Fastlane could link the customer's email to an existing - * account. + * @return {boolean} True means, Fastlane could link the customer's email to an existing account. */ get isRyanFlow() { return !! this.data.card; @@ -764,13 +763,13 @@ class AxoManager { } const phoneNumber = this.phoneInput.value; - const cleanPhoneNumber = this.sanitizePhoneNumber( phoneNumber ); + const validPhoneNumber = this.sanitizePhoneNumber( phoneNumber ); - if ( ! cleanPhoneNumber ) { + if ( ! validPhoneNumber ) { return; } - this.data.phone = cleanPhoneNumber; + this.data.phone = validPhoneNumber; } async onChangeEmail() { From 99101640446abe41a8248006a8370506ab582845 Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Tue, 3 Sep 2024 14:07:52 +0200 Subject: [PATCH 64/80] =?UTF-8?q?=E2=9A=A1=EF=B8=8F=20Only=20re-render=20t?= =?UTF-8?q?he=20component=20when=20phone=20changed?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- modules/ppcp-axo/resources/js/AxoManager.js | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/modules/ppcp-axo/resources/js/AxoManager.js b/modules/ppcp-axo/resources/js/AxoManager.js index a56348e12..f71fc595e 100644 --- a/modules/ppcp-axo/resources/js/AxoManager.js +++ b/modules/ppcp-axo/resources/js/AxoManager.js @@ -756,20 +756,23 @@ class AxoManager { /** * Reads the phone number from the WooCommerce checkout form, sanitizes it, and (if valid) * stores it in the internal customer details object. + * + * @return {boolean} True, if the internal phone number was updated. */ setPhoneFromWoo() { if ( ! this.phoneInput ) { - return; + return false; } const phoneNumber = this.phoneInput.value; const validPhoneNumber = this.sanitizePhoneNumber( phoneNumber ); if ( ! validPhoneNumber ) { - return; + return false; } this.data.phone = validPhoneNumber; + return true; } async onChangeEmail() { @@ -848,9 +851,9 @@ class AxoManager { * @return {Promise} */ async onChangePhone() { - this.setPhoneFromWoo(); + const hasChanged = this.setPhoneFromWoo(); - if ( this.status.active ) { + if ( hasChanged && this.status.active ) { await this.refreshFastlaneComponent(); } From 8514f3a49cf752d74b41935a1d3850960586fe04 Mon Sep 17 00:00:00 2001 From: Narek Zakarian Date: Tue, 3 Sep 2024 16:35:58 +0400 Subject: [PATCH 65/80] Fix the coding styles --- modules/ppcp-axo/services.php | 48 +++++++++---------- .../src/Helper/SettingsNoticeGenerator.php | 2 +- 2 files changed, 25 insertions(+), 25 deletions(-) diff --git a/modules/ppcp-axo/services.php b/modules/ppcp-axo/services.php index 1ad8cfe39..b860eab15 100644 --- a/modules/ppcp-axo/services.php +++ b/modules/ppcp-axo/services.php @@ -232,7 +232,7 @@ return array( /** * The list of Fastlane incompatible plugins. * - * @returns array + * @returns array */ 'axo.fastlane-incompatible-plugins' => static function () : array { /** @@ -242,48 +242,48 @@ return array( 'woocommerce_paypal_payments_fastlane_incompatible_plugins', array( array( - 'name' => 'Elementor', - 'isActive' => did_action( 'elementor/loaded' ), + 'name' => 'Elementor', + 'is_active' => did_action( 'elementor/loaded' ), ), array( - 'name' => 'CheckoutWC', - 'isActive' => defined( 'CFW_NAME' ), + 'name' => 'CheckoutWC', + 'is_active' => defined( 'CFW_NAME' ), ), array( - 'name' => 'Direct Checkout for WooCommerce', - 'isActive' => defined( 'QLWCDC_PLUGIN_NAME' ), + 'name' => 'Direct Checkout for WooCommerce', + 'is_active' => defined( 'QLWCDC_PLUGIN_NAME' ), ), array( - 'name' => 'Multi-Step Checkout for WooCommerce', - 'isActive' => class_exists( 'WPMultiStepCheckout' ), + 'name' => 'Multi-Step Checkout for WooCommerce', + 'is_active' => class_exists( 'WPMultiStepCheckout' ), ), array( - 'name' => 'Fluid Checkout for WooCommerce', - 'isActive' => class_exists( 'FluidCheckout' ), + 'name' => 'Fluid Checkout for WooCommerce', + 'is_active' => class_exists( 'FluidCheckout' ), ), array( - 'name' => 'MultiStep Checkout for WooCommerce', - 'isActive' => class_exists( 'THWMSCF_Multistep_Checkout' ), + 'name' => 'MultiStep Checkout for WooCommerce', + 'is_active' => class_exists( 'THWMSCF_Multistep_Checkout' ), ), array( - 'name' => 'WooCommerce Subscriptions', - 'isActive' => class_exists( 'WC_Subscriptions' ), + 'name' => 'WooCommerce Subscriptions', + 'is_active' => class_exists( 'WC_Subscriptions' ), ), array( - 'name' => 'CartFlows', - 'isActive' => class_exists( 'Cartflows_Loader' ), + 'name' => 'CartFlows', + 'is_active' => class_exists( 'Cartflows_Loader' ), ), array( - 'name' => 'FunnelKit Funnel Builder', - 'isActive' => class_exists( 'WFFN_Core' ), + 'name' => 'FunnelKit Funnel Builder', + 'is_active' => class_exists( 'WFFN_Core' ), ), array( - 'name' => 'WooCommerce One Page Checkout', - 'isActive' => class_exists( 'PP_One_Page_Checkout' ), + 'name' => 'WooCommerce One Page Checkout', + 'is_active' => class_exists( 'PP_One_Page_Checkout' ), ), array( - 'name' => 'All Products for Woo Subscriptions', - 'isActive' => class_exists( 'WCS_ATT' ), + 'name' => 'All Products for Woo Subscriptions', + 'is_active' => class_exists( 'WCS_ATT' ), ), ) ); @@ -295,7 +295,7 @@ return array( $active_plugins_list = array_filter( $incompatible_plugins, function( array $plugin ): bool { - return (bool) $plugin['isActive']; + return (bool) $plugin['is_active']; } ); diff --git a/modules/ppcp-axo/src/Helper/SettingsNoticeGenerator.php b/modules/ppcp-axo/src/Helper/SettingsNoticeGenerator.php index 72208d566..d4d4cb824 100644 --- a/modules/ppcp-axo/src/Helper/SettingsNoticeGenerator.php +++ b/modules/ppcp-axo/src/Helper/SettingsNoticeGenerator.php @@ -111,7 +111,7 @@ class SettingsNoticeGenerator { public function generate_shipping_notice(): string { $shipping_settings_link = admin_url( 'admin.php?page=wc-settings&tab=shipping§ion=options' ); - $notice_content = 'asd'; + $notice_content = ''; if ( wc_shipping_enabled() && wc_ship_to_billing_address_only() ) { $notice_content = sprintf( From d7f0b693e33c486365ec509247b2684371ed7bbd Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Tue, 3 Sep 2024 17:35:36 +0200 Subject: [PATCH 66/80] =?UTF-8?q?=E2=9C=A8=20Define=20generic=20size=20for?= =?UTF-8?q?=20payment=20gateway=20icons?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- modules/ppcp-button/resources/css/hosted-fields.scss | 9 ++++----- modules/ppcp-googlepay/resources/css/styles.scss | 6 ------ 2 files changed, 4 insertions(+), 11 deletions(-) diff --git a/modules/ppcp-button/resources/css/hosted-fields.scss b/modules/ppcp-button/resources/css/hosted-fields.scss index 578a70a09..54482b573 100644 --- a/modules/ppcp-button/resources/css/hosted-fields.scss +++ b/modules/ppcp-button/resources/css/hosted-fields.scss @@ -1,11 +1,10 @@ -#payment ul.payment_methods li img.ppcp-card-icon { - padding: 0 0 3px 3px; - max-height: 25px; - display: inline-block; +#payment ul.payment_methods [class*="payment_method_ppcp-"] label img { + max-height: 25px; + display: inline-block; } .payments-sdk-contingency-handler { - z-index: 1000 !important; + z-index: 1000 !important; } .ppcp-credit-card-gateway-form-field-disabled { diff --git a/modules/ppcp-googlepay/resources/css/styles.scss b/modules/ppcp-googlepay/resources/css/styles.scss index c1eae1563..6cf2119a0 100644 --- a/modules/ppcp-googlepay/resources/css/styles.scss +++ b/modules/ppcp-googlepay/resources/css/styles.scss @@ -17,9 +17,3 @@ #ppc-button-ppcp-googlepay { display: none; } - -label[for='payment_method_ppcp-googlepay'] { - img { - max-height: 25px; - } -} From 27e8b06b29268773e3c8af3b7aca6d9e76bc4a23 Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Tue, 3 Sep 2024 18:16:10 +0200 Subject: [PATCH 67/80] =?UTF-8?q?=F0=9F=90=9B=20Update=20ApplePay=20logo?= =?UTF-8?q?=20in=20gateway?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- modules/ppcp-applepay/src/ApplePayGateway.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/ppcp-applepay/src/ApplePayGateway.php b/modules/ppcp-applepay/src/ApplePayGateway.php index 7c87258f9..3ee57ee96 100644 --- a/modules/ppcp-applepay/src/ApplePayGateway.php +++ b/modules/ppcp-applepay/src/ApplePayGateway.php @@ -112,7 +112,7 @@ class ApplePayGateway extends WC_Payment_Gateway { $this->description = $this->get_option( 'description', '' ); $this->module_url = $module_url; - $this->icon = esc_url( $this->module_url ) . 'assets/images/applepay.png'; + $this->icon = esc_url( $this->module_url ) . 'assets/images/applepay.svg'; $this->init_form_fields(); $this->init_settings(); From 38adb9bb2184cc91fe37e5633033444fc762e288 Mon Sep 17 00:00:00 2001 From: "Alex P." Date: Wed, 4 Sep 2024 08:19:33 +0300 Subject: [PATCH 68/80] Remove disabled before triggering save button click --- modules/ppcp-onboarding/resources/js/onboarding.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/modules/ppcp-onboarding/resources/js/onboarding.js b/modules/ppcp-onboarding/resources/js/onboarding.js index 905c66c65..5a6ab333a 100644 --- a/modules/ppcp-onboarding/resources/js/onboarding.js +++ b/modules/ppcp-onboarding/resources/js/onboarding.js @@ -326,7 +326,9 @@ window.ppcp_onboarding_productionCallback = function ( ...args ) { isDisconnecting = true; - document.querySelector( '.woocommerce-save-button' ).click(); + const saveButton = document.querySelector( '.woocommerce-save-button' ); + saveButton.removeAttribute( 'disabled' ); + saveButton.click(); }; // Prevent the message about unsaved checkbox/radiobutton when reloading the page. From b3a05ae3d8667d56ebe1e8f98ecf3e0ec096406f Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Wed, 4 Sep 2024 12:02:23 +0200 Subject: [PATCH 69/80] =?UTF-8?q?=F0=9F=90=9B=20Fix=20bug=20with=20default?= =?UTF-8?q?ShippingId?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../resources/js/GooglepayButton.js | 26 +++++++++++-------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/modules/ppcp-googlepay/resources/js/GooglepayButton.js b/modules/ppcp-googlepay/resources/js/GooglepayButton.js index 3e1fc2eb4..98e22f55b 100644 --- a/modules/ppcp-googlepay/resources/js/GooglepayButton.js +++ b/modules/ppcp-googlepay/resources/js/GooglepayButton.js @@ -557,19 +557,23 @@ class GooglepayButton extends PaymentButton { * @return {Object} Sanitized object. */ sanitizeShippingOptions( responseData ) { - const cleanOptions = []; + // Sanitize the shipping options. + const cleanOptions = responseData.shippingOptions.map( ( item ) => ( { + id: item.id, + label: item.label, + description: item.description, + } ) ); - responseData.shippingOptions.forEach( ( item ) => { - cleanOptions.push( { - id: item.id, - label: item.label, - description: item.description, - } ); - } ); + // Ensure that the default option is valid. + let defaultOptionId = responseData.defaultSelectedOptionId; + if ( ! cleanOptions.some( ( item ) => item.id === defaultOptionId ) ) { + defaultOptionId = cleanOptions[ 0 ].id; + } - responseData.shippingOptions = cleanOptions; - - return { ...responseData, shippingOptions: cleanOptions }; + return { + defaultSelectedOptionId: defaultOptionId, + shippingOptions: cleanOptions, + }; } /** From c72c97f8218dcbb54d612f64a5eddf512c7430e3 Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Wed, 4 Sep 2024 15:06:50 +0200 Subject: [PATCH 70/80] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Rename=20a=20functio?= =?UTF-8?q?n?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- modules/ppcp-axo/resources/js/AxoManager.js | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/modules/ppcp-axo/resources/js/AxoManager.js b/modules/ppcp-axo/resources/js/AxoManager.js index f71fc595e..be8d2beac 100644 --- a/modules/ppcp-axo/resources/js/AxoManager.js +++ b/modules/ppcp-axo/resources/js/AxoManager.js @@ -27,9 +27,10 @@ class AxoManager { ppcpConfig = null; $ = null; + fastlane = null; + initialized = false; hideGatewaySelection = false; - fastlane = null; phoneNumber = null; /** @@ -497,7 +498,7 @@ class AxoManager { this.initPlacements(); this.initFastlane(); this.setStatus( 'active', true ); - this.setPhoneFromWoo(); + this.readPhoneFromWoo(); log( `Attempt on activation - emailInput: ${ this.emailInput.value }` ); log( @@ -759,7 +760,7 @@ class AxoManager { * * @return {boolean} True, if the internal phone number was updated. */ - setPhoneFromWoo() { + readPhoneFromWoo() { if ( ! this.phoneInput ) { return false; } @@ -818,7 +819,7 @@ class AxoManager { this.data.email = this.emailInput.value; this.billingView.setData( this.data ); - this.setPhoneFromWoo(); + this.readPhoneFromWoo(); if ( ! this.fastlane.identity ) { log( 'Not initialized.' ); @@ -851,7 +852,7 @@ class AxoManager { * @return {Promise} */ async onChangePhone() { - const hasChanged = this.setPhoneFromWoo(); + const hasChanged = this.readPhoneFromWoo(); if ( hasChanged && this.status.active ) { await this.refreshFastlaneComponent(); From 5e08e415f006b8650a5acc6d53ddc0e53b329cea Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Wed, 4 Sep 2024 15:11:03 +0200 Subject: [PATCH 71/80] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Consolidate=20logic?= =?UTF-8?q?=20to=20initialize=20Fastlane=20UI?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- modules/ppcp-axo/resources/js/AxoManager.js | 27 ++++++++++++--------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/modules/ppcp-axo/resources/js/AxoManager.js b/modules/ppcp-axo/resources/js/AxoManager.js index be8d2beac..e7856b661 100644 --- a/modules/ppcp-axo/resources/js/AxoManager.js +++ b/modules/ppcp-axo/resources/js/AxoManager.js @@ -28,6 +28,10 @@ class AxoManager { $ = null; fastlane = null; + /** + * @type {FastlaneCardComponent} + */ + cardComponent = null; initialized = false; hideGatewaySelection = false; @@ -855,7 +859,7 @@ class AxoManager { const hasChanged = this.readPhoneFromWoo(); if ( hasChanged && this.status.active ) { - await this.refreshFastlaneComponent(); + await this.initializeFastlaneComponent(); } return Promise.resolve(); @@ -896,7 +900,7 @@ class AxoManager { if ( authResponse.profileData.card ) { this.setStatus( 'hasCard', true ); } else { - this.cardComponent = await this.refreshFastlaneComponent(); + await this.initializeFastlaneComponent(); } const cardBillingAddress = @@ -937,8 +941,7 @@ class AxoManager { this.setStatus( 'hasProfile', false ); await this.renderWatermark( true ); - - this.cardComponent = await this.refreshFastlaneComponent(); + await this.initializeFastlaneComponent(); } } else { // No profile found with this email address. @@ -949,8 +952,7 @@ class AxoManager { this.setStatus( 'hasProfile', false ); await this.renderWatermark( true ); - - this.cardComponent = await this.refreshFastlaneComponent(); + await this.initializeFastlaneComponent(); } } @@ -1050,11 +1052,12 @@ class AxoManager { } /** - * Refreshes the Fastlane UI component, using configuration provided by the `cardComponentData()` method. + * Initializes the Fastlane UI component, using configuration provided by the + * `cardComponentData()` method. If the UI component was already initialized, nothing happens. * - * @return {Promise<*>} Resolves when the component was refreshed. + * @return {Promise<*>} Resolves when the component was rendered. */ - async refreshFastlaneComponent() { + async initializeFastlaneComponent() { if ( ! this.status.active ) { return Promise.resolve(); } @@ -1062,8 +1065,10 @@ class AxoManager { const elem = this.el.paymentContainer.selector + '-form'; const config = this.cardComponentData(); - const component = await this.fastlane.FastlaneCardComponent( config ); - return component.render( elem ); + this.cardComponent = + await this.fastlane.FastlaneCardComponent( config ); + + return this.cardComponent.render( elem ); } tokenizeData() { From ad2181fd0963eb8b951ceb3f9677a6988454197d Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Wed, 4 Sep 2024 15:11:46 +0200 Subject: [PATCH 72/80] =?UTF-8?q?=E2=9C=A8=20Add=20logic=20to=20update=20p?= =?UTF-8?q?refill-values?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- modules/ppcp-axo/resources/js/AxoManager.js | 27 +++++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/modules/ppcp-axo/resources/js/AxoManager.js b/modules/ppcp-axo/resources/js/AxoManager.js index e7856b661..3144d28a1 100644 --- a/modules/ppcp-axo/resources/js/AxoManager.js +++ b/modules/ppcp-axo/resources/js/AxoManager.js @@ -513,6 +513,8 @@ class AxoManager { this.lastEmailCheckedIdentity !== this.emailInput.value ) { this.onChangeEmail(); + } else { + this.refreshFastlanePrefills(); } } @@ -859,7 +861,7 @@ class AxoManager { const hasChanged = this.readPhoneFromWoo(); if ( hasChanged && this.status.active ) { - await this.initializeFastlaneComponent(); + await this.refreshFastlanePrefills(); } return Promise.resolve(); @@ -1058,7 +1060,7 @@ class AxoManager { * @return {Promise<*>} Resolves when the component was rendered. */ async initializeFastlaneComponent() { - if ( ! this.status.active ) { + if ( ! this.status.active || this.cardComponent ) { return Promise.resolve(); } @@ -1071,6 +1073,27 @@ class AxoManager { return this.cardComponent.render( elem ); } + /** + * Updates the prefill-values in the UI component. This method only updates empty fields. + * + * @return {Promise<*>} Resolves when the component was refreshed. + */ + async refreshFastlanePrefills() { + if ( ! this.cardComponent ) { + return Promise.resolve(); + } + + const { fields } = this.cardComponentData(); + const prefills = Object.keys( fields ).reduce( ( result, key ) => { + if ( fields[ key ].hasOwnProperty( 'prefill' ) ) { + result[ key ] = fields[ key ].prefill; + } + return result; + }, {} ); + + return this.cardComponent.updatePrefills( prefills ); + } + tokenizeData() { return { cardholderName: { From 503ce63975114d835bd818040f794281a1088612 Mon Sep 17 00:00:00 2001 From: "Alex P." Date: Fri, 6 Sep 2024 09:01:19 +0300 Subject: [PATCH 73/80] Do not exclude block.json in .distignore --- .distignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.distignore b/.distignore index 836792c75..614705af1 100644 --- a/.distignore +++ b/.distignore @@ -14,7 +14,8 @@ tests .phpunit.result.cache babel.config.json node_modules -modules/*/resources +modules/*/resources/css +modules/*/resources/js/**/*.js *.lock webpack.config.js wp-cli.yml From d78201634b18168d468e6e1e077bae8793c54a3b Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Fri, 6 Sep 2024 15:24:06 +0200 Subject: [PATCH 74/80] =?UTF-8?q?=F0=9F=92=A1=20Add=20todo=20about=20missi?= =?UTF-8?q?n=20refactoring?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- modules/ppcp-applepay/resources/js/ApplepayButton.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/modules/ppcp-applepay/resources/js/ApplepayButton.js b/modules/ppcp-applepay/resources/js/ApplepayButton.js index 858495389..0f217f606 100644 --- a/modules/ppcp-applepay/resources/js/ApplepayButton.js +++ b/modules/ppcp-applepay/resources/js/ApplepayButton.js @@ -57,6 +57,8 @@ const CONTEXT = { * On a single page, multiple Apple Pay buttons can be displayed, which also means multiple * ApplePayButton instances exist. A typical case is on the product page, where one Apple Pay button * is located inside the minicart-popup, and another pay-now button is in the product context. + * + * TODO - extend from PaymentButton (same as we do in GooglepayButton.js) */ class ApplePayButton { /** From 9b3ec4692a4eba8458562792125699714775b61d Mon Sep 17 00:00:00 2001 From: "Alex P." Date: Sat, 7 Sep 2024 13:05:54 +0300 Subject: [PATCH 75/80] Improve card fields hiding --- modules/ppcp-card-fields/resources/js/Render.js | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/modules/ppcp-card-fields/resources/js/Render.js b/modules/ppcp-card-fields/resources/js/Render.js index 9a35ff449..ee891edf7 100644 --- a/modules/ppcp-card-fields/resources/js/Render.js +++ b/modules/ppcp-card-fields/resources/js/Render.js @@ -1,4 +1,5 @@ import { cardFieldStyles } from './CardFieldsHelper'; +import { hide } from '../../../ppcp-button/resources/js/modules/Helper/Hiding'; export function renderFields( cardFields ) { const nameField = document.getElementById( @@ -9,7 +10,7 @@ export function renderFields( cardFields ) { cardFields .NameField( { style: { input: styles } } ) .render( nameField.parentNode ); - nameField.hidden = true; + hide( nameField, true ); } const numberField = document.getElementById( @@ -20,7 +21,7 @@ export function renderFields( cardFields ) { cardFields .NumberField( { style: { input: styles } } ) .render( numberField.parentNode ); - numberField.hidden = true; + hide( numberField, true ); } const expiryField = document.getElementById( @@ -31,7 +32,7 @@ export function renderFields( cardFields ) { cardFields .ExpiryField( { style: { input: styles } } ) .render( expiryField.parentNode ); - expiryField.hidden = true; + hide( expiryField, true ); } const cvvField = document.getElementById( @@ -42,6 +43,6 @@ export function renderFields( cardFields ) { cardFields .CVVField( { style: { input: styles } } ) .render( cvvField.parentNode ); - cvvField.hidden = true; + hide( cvvField, true ); } } From 36522f685c0f7c21f17aae8f8323a652b671b57b Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Mon, 9 Sep 2024 12:10:47 +0200 Subject: [PATCH 76/80] Adjust logic to additionally set the hidden-attrib --- modules/ppcp-card-fields/resources/js/Render.js | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/modules/ppcp-card-fields/resources/js/Render.js b/modules/ppcp-card-fields/resources/js/Render.js index ee891edf7..892b88138 100644 --- a/modules/ppcp-card-fields/resources/js/Render.js +++ b/modules/ppcp-card-fields/resources/js/Render.js @@ -10,7 +10,8 @@ export function renderFields( cardFields ) { cardFields .NameField( { style: { input: styles } } ) .render( nameField.parentNode ); - hide( nameField, true ); + hide( nameField, true ); + nameField.hidden = true; } const numberField = document.getElementById( @@ -21,7 +22,8 @@ export function renderFields( cardFields ) { cardFields .NumberField( { style: { input: styles } } ) .render( numberField.parentNode ); - hide( numberField, true ); + hide( numberField, true ); + numberField.hidden = true; } const expiryField = document.getElementById( @@ -32,7 +34,8 @@ export function renderFields( cardFields ) { cardFields .ExpiryField( { style: { input: styles } } ) .render( expiryField.parentNode ); - hide( expiryField, true ); + hide( expiryField, true ); + expiryField.hidden = true; } const cvvField = document.getElementById( @@ -43,6 +46,7 @@ export function renderFields( cardFields ) { cardFields .CVVField( { style: { input: styles } } ) .render( cvvField.parentNode ); - hide( cvvField, true ); + hide( cvvField, true ); + cvvField.hidden = true; } } From fc4d2baa6703c9040bc01486879b452988fd7db8 Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Mon, 9 Sep 2024 12:16:02 +0200 Subject: [PATCH 77/80] Extract repeat logic into new helper method --- modules/ppcp-card-fields/resources/js/Render.js | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/modules/ppcp-card-fields/resources/js/Render.js b/modules/ppcp-card-fields/resources/js/Render.js index 892b88138..b899cb788 100644 --- a/modules/ppcp-card-fields/resources/js/Render.js +++ b/modules/ppcp-card-fields/resources/js/Render.js @@ -1,6 +1,20 @@ import { cardFieldStyles } from './CardFieldsHelper'; import { hide } from '../../../ppcp-button/resources/js/modules/Helper/Hiding'; +function renderField( cardField, inputField ) { + if ( ! inputField || inputField.hidden || ! cardField ) { + return; + } + + // Insert the PayPal card field after the original input field. + const styles = cardFieldStyles( inputField ); + cardField( { style: { input: styles } } ).render( inputField.parentNode ); + + // Hide the original input field. + hide( inputField, true ); + inputField.hidden = true; +} + export function renderFields( cardFields ) { const nameField = document.getElementById( 'ppcp-credit-card-gateway-card-name' From e00a4a302c47d782ec5d98160feeb59eb4ce0618 Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Mon, 9 Sep 2024 12:16:27 +0200 Subject: [PATCH 78/80] Replace existing logic with new helper method --- .../ppcp-card-fields/resources/js/Render.js | 55 ++++--------------- 1 file changed, 12 insertions(+), 43 deletions(-) diff --git a/modules/ppcp-card-fields/resources/js/Render.js b/modules/ppcp-card-fields/resources/js/Render.js index b899cb788..146396288 100644 --- a/modules/ppcp-card-fields/resources/js/Render.js +++ b/modules/ppcp-card-fields/resources/js/Render.js @@ -16,51 +16,20 @@ function renderField( cardField, inputField ) { } export function renderFields( cardFields ) { - const nameField = document.getElementById( - 'ppcp-credit-card-gateway-card-name' + renderField( + cardFields.NameField, + document.getElementById( 'ppcp-credit-card-gateway-card-name' ) ); - if ( nameField && nameField.hidden !== true ) { - const styles = cardFieldStyles( nameField ); - cardFields - .NameField( { style: { input: styles } } ) - .render( nameField.parentNode ); - hide( nameField, true ); - nameField.hidden = true; - } - - const numberField = document.getElementById( - 'ppcp-credit-card-gateway-card-number' + renderField( + cardFields.NumberField, + document.getElementById( 'ppcp-credit-card-gateway-card-number' ) ); - if ( numberField && numberField.hidden !== true ) { - const styles = cardFieldStyles( numberField ); - cardFields - .NumberField( { style: { input: styles } } ) - .render( numberField.parentNode ); - hide( numberField, true ); - numberField.hidden = true; - } - - const expiryField = document.getElementById( - 'ppcp-credit-card-gateway-card-expiry' + renderField( + cardFields.ExpiryField, + document.getElementById( 'ppcp-credit-card-gateway-card-expiry' ) ); - if ( expiryField && expiryField.hidden !== true ) { - const styles = cardFieldStyles( expiryField ); - cardFields - .ExpiryField( { style: { input: styles } } ) - .render( expiryField.parentNode ); - hide( expiryField, true ); - expiryField.hidden = true; - } - - const cvvField = document.getElementById( - 'ppcp-credit-card-gateway-card-cvc' + renderField( + cardFields.CVVField, + document.getElementById( 'ppcp-credit-card-gateway-card-cvc' ) ); - if ( cvvField && cvvField.hidden !== true ) { - const styles = cardFieldStyles( cvvField ); - cardFields - .CVVField( { style: { input: styles } } ) - .render( cvvField.parentNode ); - hide( cvvField, true ); - cvvField.hidden = true; - } } From 9bafceddec1f68e0dddbc1836675ae12aa503353 Mon Sep 17 00:00:00 2001 From: "Alex P." Date: Tue, 10 Sep 2024 16:41:53 +0300 Subject: [PATCH 79/80] Fix shipping callback condition in status report --- modules/ppcp-status-report/src/StatusReportModule.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/ppcp-status-report/src/StatusReportModule.php b/modules/ppcp-status-report/src/StatusReportModule.php index 8cff26b17..da015d586 100644 --- a/modules/ppcp-status-report/src/StatusReportModule.php +++ b/modules/ppcp-status-report/src/StatusReportModule.php @@ -184,9 +184,9 @@ class StatusReportModule implements ServiceModule, ExtendingModule, ExecutableMo array( 'label' => esc_html__( 'PayPal Shipping Callback', 'woocommerce-paypal-payments' ), 'exported_label' => 'PayPal Shipping Callback', - 'description' => esc_html__( 'Whether the "Require final confirmation on checkout" setting is enabled.', 'woocommerce-paypal-payments' ), + 'description' => esc_html__( 'Whether the "Require final confirmation on checkout" setting is disabled.', 'woocommerce-paypal-payments' ), 'value' => $this->bool_to_html( - $settings->has( 'blocks_final_review_enabled' ) && $settings->get( 'blocks_final_review_enabled' ) + $settings->has( 'blocks_final_review_enabled' ) && ! $settings->get( 'blocks_final_review_enabled' ) ), ), array( From c24b753c383f70a95b1043ef83b28961d400de68 Mon Sep 17 00:00:00 2001 From: Daniel Dudzic Date: Tue, 10 Sep 2024 19:12:14 +0200 Subject: [PATCH 80/80] AXO: Remove restriction for the Force shipping to the customer billing address shipping config --- modules/ppcp-axo/extensions.php | 1 - modules/ppcp-axo/services.php | 7 ------ modules/ppcp-axo/src/AxoModule.php | 16 +------------ .../src/Helper/SettingsNoticeGenerator.php | 24 ------------------- 4 files changed, 1 insertion(+), 47 deletions(-) diff --git a/modules/ppcp-axo/extensions.php b/modules/ppcp-axo/extensions.php index fe67bb3d5..9d26cffd5 100644 --- a/modules/ppcp-axo/extensions.php +++ b/modules/ppcp-axo/extensions.php @@ -120,7 +120,6 @@ return array( '', array( $container->get( 'axo.settings-conflict-notice' ), - $container->get( 'axo.shipping-config-notice' ), $container->get( 'axo.checkout-config-notice' ), $container->get( 'axo.incompatible-plugins-notice' ), ) diff --git a/modules/ppcp-axo/services.php b/modules/ppcp-axo/services.php index b860eab15..0519e9209 100644 --- a/modules/ppcp-axo/services.php +++ b/modules/ppcp-axo/services.php @@ -180,13 +180,6 @@ return array( return $settings_notice_generator->generate_checkout_notice(); }, - 'axo.shipping-config-notice' => static function ( ContainerInterface $container ) : string { - $settings_notice_generator = $container->get( 'axo.helpers.settings-notice-generator' ); - assert( $settings_notice_generator instanceof SettingsNoticeGenerator ); - - return $settings_notice_generator->generate_shipping_notice(); - }, - 'axo.incompatible-plugins-notice' => static function ( ContainerInterface $container ) : string { $settings_notice_generator = $container->get( 'axo.helpers.settings-notice-generator' ); assert( $settings_notice_generator instanceof SettingsNoticeGenerator ); diff --git a/modules/ppcp-axo/src/AxoModule.php b/modules/ppcp-axo/src/AxoModule.php index 2b554d1bb..069860550 100644 --- a/modules/ppcp-axo/src/AxoModule.php +++ b/modules/ppcp-axo/src/AxoModule.php @@ -99,10 +99,6 @@ class AxoModule implements ServiceModule, ExtendingModule, ExecutableModule { return $methods; } - if ( ! $this->is_compatible_shipping_config() ) { - return $methods; - } - $methods[] = $gateway; return $methods; }, @@ -173,7 +169,7 @@ class AxoModule implements ServiceModule, ExtendingModule, ExecutableModule { || ! $c->get( 'axo.eligible' ) || 'continuation' === $c->get( 'button.context' ) || $subscription_helper->cart_contains_subscription() - || ! $this->is_compatible_shipping_config() ) { + ) { return; } @@ -357,7 +353,6 @@ class AxoModule implements ServiceModule, ExtendingModule, ExecutableModule { return ! is_user_logged_in() && CartCheckoutDetector::has_classic_checkout() - && $this->is_compatible_shipping_config() && $is_axo_enabled && $is_dcc_enabled && ! $this->is_excluded_endpoint(); @@ -414,15 +409,6 @@ class AxoModule implements ServiceModule, ExtendingModule, ExecutableModule { return is_wc_endpoint_url( 'order-pay' ); } - /** - * Condition to evaluate if the shipping configuration is compatible. - * - * @return bool - */ - private function is_compatible_shipping_config(): bool { - return ! wc_shipping_enabled() || ( wc_shipping_enabled() && ! wc_ship_to_billing_address_only() ); - } - /** * Outputs a meta tag to allow feature detection on certain pages. * diff --git a/modules/ppcp-axo/src/Helper/SettingsNoticeGenerator.php b/modules/ppcp-axo/src/Helper/SettingsNoticeGenerator.php index d4d4cb824..aec8848a0 100644 --- a/modules/ppcp-axo/src/Helper/SettingsNoticeGenerator.php +++ b/modules/ppcp-axo/src/Helper/SettingsNoticeGenerator.php @@ -103,30 +103,6 @@ class SettingsNoticeGenerator { return $notice_content ? '

    ' . $notice_content . '

    ' : ''; } - /** - * Generates the shipping notice. - * - * @return string - */ - public function generate_shipping_notice(): string { - $shipping_settings_link = admin_url( 'admin.php?page=wc-settings&tab=shipping§ion=options' ); - - $notice_content = ''; - - if ( wc_shipping_enabled() && wc_ship_to_billing_address_only() ) { - $notice_content = sprintf( - /* translators: %1$s: URL to the Shipping destination settings page. */ - __( - 'Warning: The Shipping destination of your store is currently configured to Force shipping to the customer billing address. To enable Fastlane and accelerate payments, the shipping destination must be configured either to Default to customer shipping address or Default to customer billing address so buyers can set separate billing and shipping details.', - 'woocommerce-paypal-payments' - ), - esc_url( $shipping_settings_link ) - ); - } - - return $notice_content ? '

    ' . $notice_content . '

    ' : ''; - } - /** * Generates the incompatible plugins notice. *