diff --git a/modules/ppcp-applepay/resources/js/boot-admin.js b/modules/ppcp-applepay/resources/js/boot-admin.js index 151459b07..b9e71f99d 100644 --- a/modules/ppcp-applepay/resources/js/boot-admin.js +++ b/modules/ppcp-applepay/resources/js/boot-admin.js @@ -20,17 +20,10 @@ const buttonManager = () => { */ class ApplePayPreviewButtonManager extends PreviewButtonManager { constructor() { - const defaultButton = { - type: 'pay', - color: 'black', - lang: 'en' - }; - const args = { methodName: 'ApplePay', buttonConfig: window.wc_ppcp_applepay_admin, widgetBuilder, - defaultAttributes: {button: defaultButton} }; super(args); @@ -62,8 +55,7 @@ class ApplePayPreviewButtonManager extends PreviewButtonManager { createButtonInst(wrapperId) { return new ApplePayPreviewButton({ selector: wrapperId, - configResponse: this.configResponse, - defaultAttributes: this.defaultAttributes + apiConfig: this.apiConfig }); } } @@ -77,6 +69,13 @@ class ApplePayPreviewButton extends PreviewButton { super(args); this.selector = `${args.selector}ApplePay` + this.defaultAttributes = { + button: { + type: 'pay', + color: 'black', + lang: 'en' + } + }; } createNewWrapper() { @@ -86,38 +85,30 @@ class ApplePayPreviewButton extends PreviewButton { return element; } - createButton() { + createButton(buttonConfig) { const button = new ApplepayButton( 'preview', null, - this.buttonConfig, + buttonConfig, this.ppcpConfig, ); - button.init(this.configResponse); + button.init(this.apiConfig); } /** - * Some style details need to be copied from the ppcpConfig object to buttonConfig. - * - * - ppcpConfig: Generated by JS, containing the current form values. - * - buttonConfig: Generated on server side, contains the full (saved) button details. + * Merge form details into the config object for preview. + * Mutates the previewConfig object; no return value. */ - applyPreviewConfig() { + dynamicPreviewConfig(buttonConfig, ppcpConfig) { // The Apple Pay button expects the "wrapper" to be an ID without `#` prefix! - if (this.buttonConfig?.button?.wrapper) { - this.buttonConfig.button.wrapper = this.buttonConfig.button.wrapper.replace(/^#/, ''); - } + buttonConfig.button.wrapper = buttonConfig.button.wrapper.replace(/^#/, ''); - // Apple Pay expects the param "lang" instead of "language" - if (this.ppcpConfig?.button?.style?.language) { - this.ppcpConfig.button.style.lang = this.ppcpConfig.button.style.language; - } - - if (this.ppcpConfig && this.buttonConfig) { - this.buttonConfig.button.type = this.ppcpConfig.button.style.type; - this.buttonConfig.button.color = this.ppcpConfig.button.style.color; - this.buttonConfig.button.lang = this.ppcpConfig.button.style.lang; + // 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-button/resources/js/modules/Renderer/PreviewButton.js b/modules/ppcp-button/resources/js/modules/Renderer/PreviewButton.js index 71cce9d9b..4b46b850f 100644 --- a/modules/ppcp-button/resources/js/modules/Renderer/PreviewButton.js +++ b/modules/ppcp-button/resources/js/modules/Renderer/PreviewButton.js @@ -6,18 +6,19 @@ import merge from "deepmerge"; class PreviewButton { /** * @param {string} selector - CSS ID of the wrapper, including the `#` - * @param {object} configResponse - PayPal configuration object; retrieved via a + * @param {object} apiConfig - PayPal configuration object; retrieved via a * widgetBuilder API method - * @param {object} defaultAttributes - Optional. */ - constructor({selector, configResponse, defaultAttributes = {}}) { - this.configResponse = configResponse; - this.defaultAttributes = defaultAttributes; + constructor({selector, apiConfig}) { + this.apiConfig = apiConfig; + this.defaultAttributes = {}; this.buttonConfig = {}; this.ppcpConfig = {}; + this.isDynamic = true; - // Usually overwritten in constructor of derived class. + // The selector is usually overwritten in constructor of derived class. this.selector = selector; + this.wrapper = selector; this.domWrapper = null; } @@ -35,40 +36,53 @@ class PreviewButton { } /** - * Updates the internal button configuration. Does not trigger a redraw. + * Toggle the "dynamic" nature of the preview. + * When the button is dynamic, it will reflect current form values. A static button always + * uses the settings that were provided via PHP. * * @return {this} Reference to self, for chaining. */ - config({buttonConfig, ppcpConfig}) { - if (ppcpConfig) { - this.ppcpConfig = merge({}, ppcpConfig); - } + setDynamic(state) { + this.isDynamic = state; + return this; + } - if (buttonConfig) { - this.buttonConfig = merge(this.defaultAttributes, buttonConfig) - this.buttonConfig.button.wrapper = this.selector - } - - this.applyPreviewConfig(); + /** + * Sets server-side configuration for the button. + * + * @return {this} Reference to self, for chaining. + */ + setButtonConfig(config) { + this.buttonConfig = merge(this.defaultAttributes, config) + this.buttonConfig.button.wrapper = this.selector return this; } /** - * Some style details need to be copied from the ppcpConfig object to buttonConfig. + * Updates the button configuration with current details from the form. * - * - ppcpConfig: Generated by JS, containing the current form values. - * - buttonConfig: Generated on server side, contains the full (saved) button details. + * @return {this} Reference to self, for chaining. */ - applyPreviewConfig() { - // Implement in the derived class. + setPpcpConfig(config) { + this.ppcpConfig = merge({}, config); + + return this; + } + + /** + * Merge form details into the config object for preview. + * Mutates the previewConfig object; no return value. + */ + dynamicPreviewConfig(previewConfig, formConfig) { + // Implement in derived class. } /** * Responsible for creating the actual payment button preview. * Called by the `render()` method, after the wrapper DOM element is ready. */ - createButton() { + createButton(previewConfig) { throw new Error('The "createButton" method must be implemented by the derived class'); } @@ -78,20 +92,35 @@ class PreviewButton { */ render() { if (!this.domWrapper) { - if (!this.buttonConfig?.button?.wrapper) { + if (!this.wrapper) { console.error('Skip render, button is not configured yet'); return; } this.domWrapper = this.createNewWrapper(); - - this.domWrapper.insertAfter(this.ppcpConfig.button.wrapper) + this.domWrapper.insertAfter(this.wrapper) } else { this.domWrapper.empty().show(); } this.isVisible = true; + const previewButtonConfig = merge({}, this.buttonConfig); + const previewPpcpConfig = this.isDynamic ? merge({}, this.ppcpConfig) : {}; + previewButtonConfig.button.wrapper = this.selector; - this.createButton() + this.dynamicPreviewConfig(previewButtonConfig, previewPpcpConfig); + + /* + * previewButtonConfig.button.wrapper must be different from this.ppcpConfig.button.wrapper! + * If both selectors point to the same element, an infinite loop is triggered. + */ + const buttonWrapper = previewButtonConfig.button.wrapper.replace(/^#/, '') + const ppcpWrapper = this.ppcpConfig.button.wrapper.replace(/^#/, '') + +if (buttonWrapper === ppcpWrapper) { +throw new Error(`[APM Preview Button] Infinite loop detected. Provide different selectors for the button/ppcp wrapper elements! Selector: "#${buttonWrapper}"`); +} + + this.createButton(previewButtonConfig) } remove() { diff --git a/modules/ppcp-button/resources/js/modules/Renderer/PreviewButtonManager.js b/modules/ppcp-button/resources/js/modules/Renderer/PreviewButtonManager.js index f59e1252b..fcb61fbe4 100644 --- a/modules/ppcp-button/resources/js/modules/Renderer/PreviewButtonManager.js +++ b/modules/ppcp-button/resources/js/modules/Renderer/PreviewButtonManager.js @@ -1,5 +1,4 @@ import {loadCustomScript} from "@paypal/paypal-js"; -import merge from "deepmerge"; /** * Manages all PreviewButton instances of a certain payment method on the page. @@ -29,12 +28,15 @@ class PreviewButtonManager { this.isEnabled = true this.buttons = {}; - this.configResponse = null; + this.apiConfig = null; this.#onInit = new Promise(resolve => { this.#onInitResolver = resolve; }); + this.bootstrap = this.bootstrap.bind(this); + this.renderPreview = this.renderPreview.bind(this); + this.registerEventListeners(); } @@ -61,13 +63,13 @@ class PreviewButtonManager { } registerEventListeners() { - jQuery(document).on('DOMContentLoaded', this.bootstrap.bind(this)); + jQuery(document).one('DOMContentLoaded', this.bootstrap); // General event that all APM buttons react to. - jQuery(document).on('ppcp_paypal_render_preview', this.renderPreview.bind(this)); + jQuery(document).on('ppcp_paypal_render_preview', this.renderPreview); // Specific event to only (re)render the current APM button type. - jQuery(document).on(`ppcp_paypal_render_preview_${this.methodName}`, this.renderPreview.bind(this)); + jQuery(document).on(`ppcp_paypal_render_preview_${this.methodName}`, this.renderPreview); } /** @@ -77,6 +79,15 @@ class PreviewButtonManager { console.error(`${this.methodName} ${message}`, ...args) } + /** + * Whether this is a dynamic preview of the APM button. + * A dynamic preview adjusts to the current form settings, while a static preview uses the + * style settings that were provided from server-side. + */ + isDynamic() { + return !!document.querySelector(`[data-ppcp-apm-name="${this.methodName}"]`) + } + /** * Load dependencies and bootstrap the module. * Returns a Promise that resolves once all dependencies were loaded and the module can be @@ -104,8 +115,7 @@ class PreviewButtonManager { await Promise.all([customScriptPromise, paypalPromise]); - this.configResponse = await this.fetchConfig(); - + this.apiConfig = await this.fetchConfig(); await this.#onInitResolver() this.#onInit = null; @@ -126,38 +136,41 @@ class PreviewButtonManager { } if (!this.buttons[id]) { - this.addButton(id, ppcpConfig); + this.#addButton(id, ppcpConfig); } else { - this.buttons[id].config({ - buttonConfig: this.buttonConfig, - ppcpConfig - }).render() + this.#configureButton(id, ppcpConfig); } } + /** + * Applies a new configuration to an existing preview button. + */ + #configureButton(id, ppcpConfig) { + this.buttons[id] + .setDynamic(this.isDynamic()) + .setPpcpConfig(ppcpConfig) + .render() + } + /** * Creates a new preview button, that is rendered once the bootstrapping Promise resolves. */ - addButton(id, ppcpConfig) { - const createOrUpdateButton = () => { + #addButton(id, ppcpConfig) { + const createButton = () => { if (!this.buttons[id]) { - this.buttons[id] = this.createButtonInst(id); + this.buttons[id] = this.createButtonInst(id).setButtonConfig(this.buttonConfig); } - this.buttons[id].config({ - buttonConfig: this.buttonConfig, - ppcpConfig - }).render() + this.#configureButton(id, ppcpConfig); } if (this.#onInit) { - this.#onInit.then(createOrUpdateButton); + this.#onInit.then(createButton); } else { - createOrUpdateButton(); + createButton(); } } - /** * Refreshes all buttons using the latest buttonConfig. * diff --git a/modules/ppcp-googlepay/resources/js/boot-admin.js b/modules/ppcp-googlepay/resources/js/boot-admin.js index bb1e07d91..edd7af729 100644 --- a/modules/ppcp-googlepay/resources/js/boot-admin.js +++ b/modules/ppcp-googlepay/resources/js/boot-admin.js @@ -20,19 +20,10 @@ const buttonManager = () => { */ class GooglePayPreviewButtonManager extends PreviewButtonManager { constructor() { - const defaultButton = { - style: { - type: 'pay', - color: 'black', - language: 'en' - } - }; - const args = { methodName: 'GooglePay', buttonConfig: window.wc_ppcp_googlepay_admin, - widgetBuilder, - defaultAttributes: {button: defaultButton} + widgetBuilder }; super(args); @@ -64,8 +55,7 @@ class GooglePayPreviewButtonManager extends PreviewButtonManager { createButtonInst(wrapperId) { return new GooglePayPreviewButton({ selector: wrapperId, - configResponse: this.configResponse, - defaultAttributes: this.defaultAttributes + apiConfig: this.apiConfig }); } } @@ -79,6 +69,15 @@ class GooglePayPreviewButton extends PreviewButton { super(args); this.selector = `${args.selector}GooglePay` + this.defaultAttributes = { + button: { + style: { + type: 'pay', + color: 'black', + language: 'en' + } + } + }; } createNewWrapper() { @@ -88,26 +87,25 @@ class GooglePayPreviewButton extends PreviewButton { return element; } - createButton() { + createButton(buttonConfig) { const button = new GooglepayButton( 'preview', null, - this.buttonConfig, + buttonConfig, this.ppcpConfig, ); - button.init(this.configResponse); + button.init(this.apiConfig); } /** - * Some style details need to be copied from the ppcpConfig object to buttonConfig. - * - * - ppcpConfig: Generated by JS, containing the current form values. - * - buttonConfig: Generated on server side, contains the full (saved) button details. + * Merge form details into the config object for preview. + * Mutates the previewConfig object; no return value. */ - applyPreviewConfig() { - if (this.ppcpConfig && this.buttonConfig) { - this.buttonConfig.button.style = this.ppcpConfig.button.style; + 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); } } }