diff --git a/modules/ppcp-applepay/extensions.php b/modules/ppcp-applepay/extensions.php index 058c24904..86fe75c3a 100644 --- a/modules/ppcp-applepay/extensions.php +++ b/modules/ppcp-applepay/extensions.php @@ -20,6 +20,9 @@ use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings; return array( 'wcgateway.settings.fields' => function ( ContainerInterface $container, array $fields ): array { + // Used in various places to mark fields for the preview button. + $apm_name = 'ApplePay'; + // Eligibility check. if ( ! $container->has( 'applepay.eligible' ) || ! $container->get( 'applepay.eligible' ) ) { return $fields; @@ -171,7 +174,7 @@ return array( 'gateway' => 'dcc', 'requirements' => array(), 'custom_attributes' => array( - 'data-ppcp-display' => wp_json_encode( + 'data-ppcp-display' => wp_json_encode( array( $display_manager ->rule() @@ -183,10 +186,13 @@ return array( ->action_visible( 'applepay_button_type' ) ->action_visible( 'applepay_button_language' ) ->action_visible( 'applepay_checkout_data_mode' ) + ->action_visible( 'applepay_button_preview' ) ->action_class( 'applepay_button_enabled', 'active' ) ->to_array(), ) ), + 'data-ppcp-apm-name' => $apm_name, + 'data-ppcp-field-name' => 'is_enabled', ), 'classes' => array( 'ppcp-valign-label-middle', 'ppcp-align-label-center' ), ), @@ -253,56 +259,68 @@ return array( 'requirements' => array(), ), 'applepay_button_type' => array( - 'title' => __( 'Button Label', 'woocommerce-paypal-payments' ), - 'type' => 'select', - 'desc_tip' => true, - 'description' => __( + 'title' => __( 'Button Label', 'woocommerce-paypal-payments' ), + 'type' => 'select', + 'desc_tip' => true, + 'description' => __( 'This controls the label of the Apple Pay button.', 'woocommerce-paypal-payments' ), - 'classes' => array( 'ppcp-field-indent' ), - 'class' => array(), - 'input_class' => array( 'wc-enhanced-select' ), - 'default' => 'pay', - 'options' => PropertiesDictionary::button_types(), - 'screens' => array( State::STATE_ONBOARDED ), - 'gateway' => 'dcc', - 'requirements' => array(), + 'classes' => array( 'ppcp-field-indent' ), + 'class' => array(), + 'input_class' => array( 'wc-enhanced-select' ), + 'default' => 'pay', + 'options' => PropertiesDictionary::button_types(), + 'screens' => array( State::STATE_ONBOARDED ), + 'gateway' => 'dcc', + 'requirements' => array(), + 'custom_attributes' => array( + 'data-ppcp-apm-name' => $apm_name, + 'data-ppcp-field-name' => 'type', + ), ), 'applepay_button_color' => array( - 'title' => __( 'Button Color', 'woocommerce-paypal-payments' ), - 'type' => 'select', - 'desc_tip' => true, - 'description' => __( + 'title' => __( 'Button Color', 'woocommerce-paypal-payments' ), + 'type' => 'select', + 'desc_tip' => true, + 'description' => __( 'The Apple Pay Button may appear as a black button with white lettering, white button with black lettering, or a white button with black lettering and a black outline.', 'woocommerce-paypal-payments' ), - 'label' => '', - 'input_class' => array( 'wc-enhanced-select' ), - 'classes' => array( 'ppcp-field-indent' ), - 'class' => array(), - 'default' => 'black', - 'options' => PropertiesDictionary::button_colors(), - 'screens' => array( State::STATE_ONBOARDED ), - 'gateway' => 'dcc', - 'requirements' => array(), + 'label' => '', + 'input_class' => array( 'wc-enhanced-select' ), + 'classes' => array( 'ppcp-field-indent' ), + 'class' => array(), + 'default' => 'black', + 'options' => PropertiesDictionary::button_colors(), + 'screens' => array( State::STATE_ONBOARDED ), + 'gateway' => 'dcc', + 'requirements' => array(), + 'custom_attributes' => array( + 'data-ppcp-apm-name' => $apm_name, + 'data-ppcp-field-name' => 'color', + ), ), 'applepay_button_language' => array( - 'title' => __( 'Button Language', 'woocommerce-paypal-payments' ), - 'type' => 'select', - 'desc_tip' => true, - 'description' => __( + 'title' => __( 'Button Language', 'woocommerce-paypal-payments' ), + 'type' => 'select', + 'desc_tip' => true, + 'description' => __( 'The language and region used for the displayed Apple Pay button. The default value is the current language and region setting in a browser.', 'woocommerce-paypal-payments' ), - 'classes' => array( 'ppcp-field-indent' ), - 'class' => array(), - 'input_class' => array( 'wc-enhanced-select' ), - 'default' => 'en', - 'options' => PropertiesDictionary::button_languages(), - 'screens' => array( State::STATE_ONBOARDED ), - 'gateway' => 'dcc', - 'requirements' => array(), + 'classes' => array( 'ppcp-field-indent' ), + 'class' => array(), + 'input_class' => array( 'wc-enhanced-select' ), + 'default' => 'en', + 'options' => PropertiesDictionary::button_languages(), + 'screens' => array( State::STATE_ONBOARDED ), + 'gateway' => 'dcc', + 'requirements' => array(), + 'custom_attributes' => array( + 'data-ppcp-apm-name' => $apm_name, + 'data-ppcp-field-name' => 'language', + ), ), 'applepay_checkout_data_mode' => array( 'title' => __( 'Send checkout billing and shipping data to Apple Pay', 'woocommerce-paypal-payments' ), @@ -318,6 +336,22 @@ return array( 'gateway' => 'dcc', 'requirements' => array(), ), + 'applepay_button_preview' => array( + 'type' => 'ppcp-text', + 'text' => sprintf( + ' +
+

' . __( 'Button Styling Preview', 'woocommerce-paypal-payments' ) . '

+
+
', + $apm_name + ), + 'screens' => array( + State::STATE_ONBOARDED, + ), + 'requirements' => array(), + 'gateway' => 'dcc', + ), ) ); }, diff --git a/modules/ppcp-applepay/resources/js/ApplepayButton.js b/modules/ppcp-applepay/resources/js/ApplepayButton.js index c90f54932..49ccdaf88 100644 --- a/modules/ppcp-applepay/resources/js/ApplepayButton.js +++ b/modules/ppcp-applepay/resources/js/ApplepayButton.js @@ -135,6 +135,10 @@ class ApplepayButton { const { wrapper, ppcpButtonWrapper } = this.contextConfig(); const wrapper_id = '#' + wrapper; + if (wrapper_id === ppcpButtonWrapper) { + throw new Error(`[ApplePayButton] "wrapper" and "ppcpButtonWrapper" values must differ to avoid infinite loop. Current value: "${wrapper_id}"`); + } + const syncButtonVisibility = () => { if (!this.isEligible) { return; diff --git a/modules/ppcp-applepay/resources/js/boot-admin.js b/modules/ppcp-applepay/resources/js/boot-admin.js index f584ce41b..dffd0429b 100644 --- a/modules/ppcp-applepay/resources/js/boot-admin.js +++ b/modules/ppcp-applepay/resources/js/boot-admin.js @@ -1,148 +1,112 @@ -import {loadCustomScript} from "@paypal/paypal-js"; -import ApplepayButton from "./ApplepayButton"; -import widgetBuilder from "../../../ppcp-button/resources/js/modules/Renderer/WidgetBuilder"; +import ApplepayButton from './ApplepayButton'; +import PreviewButton from '../../../ppcp-button/resources/js/modules/Renderer/PreviewButton'; +import PreviewButtonManager from '../../../ppcp-button/resources/js/modules/Renderer/PreviewButtonManager'; -(function ({ - buttonConfig, - jQuery -}) { - - let applePayConfig; - let buttonQueue = []; - let activeButtons = {}; - let bootstrapped = false; - - // React to PayPal config changes. - jQuery(document).on('ppcp_paypal_render_preview', (ev, ppcpConfig) => { - if (bootstrapped) { - createButton(ppcpConfig); - } else { - buttonQueue.push({ - ppcpConfig: JSON.parse(JSON.stringify(ppcpConfig)) - }); - } - }); - - // React to ApplePay config changes. - jQuery([ - '#ppcp-applepay_button_enabled', - '#ppcp-applepay_button_type', - '#ppcp-applepay_button_color', - '#ppcp-applepay_button_language' - ].join(',')).on('change', () => { - for (const [selector, ppcpConfig] of Object.entries(activeButtons)) { - createButton(ppcpConfig); - } - }); - - // Maybe we can find a more elegant reload method when transitioning from styling modes. - jQuery([ - '#ppcp-smart_button_enable_styling_per_location' - ].join(',')).on('change', () => { - setTimeout(() => { - for (const [selector, ppcpConfig] of Object.entries(activeButtons)) { - createButton(ppcpConfig); - } - }, 100); - }); - - const applyConfigOptions = function (buttonConfig) { - buttonConfig.button = buttonConfig.button || {}; - buttonConfig.button.type = jQuery('#ppcp-applepay_button_type').val(); - buttonConfig.button.color = jQuery('#ppcp-applepay_button_color').val(); - buttonConfig.button.lang = jQuery('#ppcp-applepay_button_language').val(); +/** + * Accessor that creates and returns a single PreviewButtonManager instance. + */ +const buttonManager = () => { + if (!ApplePayPreviewButtonManager.instance) { + ApplePayPreviewButtonManager.instance = new ApplePayPreviewButtonManager(); } - const createButton = function (ppcpConfig) { - const selector = ppcpConfig.button.wrapper + 'ApplePay'; + return ApplePayPreviewButtonManager.instance; +}; - if (!jQuery('#ppcp-applepay_button_enabled').is(':checked')) { - jQuery(selector).remove(); - return; - } - buttonConfig = JSON.parse(JSON.stringify(buttonConfig)); - buttonConfig.button.wrapper = selector.replace('#', ''); - applyConfigOptions(buttonConfig); +/** + * Manages all Apple Pay preview buttons on this page. + */ +class ApplePayPreviewButtonManager extends PreviewButtonManager { + constructor() { + const args = { + methodName: 'ApplePay', + buttonConfig: window.wc_ppcp_applepay_admin, + }; - const wrapperElement = `
`; - - if (!jQuery(selector).length) { - jQuery(ppcpConfig.button.wrapper).after(wrapperElement); - } else { - jQuery(selector).replaceWith(wrapperElement); - } - - const button = new ApplepayButton( - 'preview', - null, - buttonConfig, - ppcpConfig, - ); - - button.init(applePayConfig); - - activeButtons[selector] = ppcpConfig; + super(args); } - const bootstrap = async function () { - if (!widgetBuilder.paypal) { - return; + /** + * 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 {}; } - applePayConfig = await widgetBuilder.paypal.Applepay().config(); + return await apiMethod(); + } - // We need to set bootstrapped here otherwise applePayConfig may not be set. - bootstrapped = true; + /** + * 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, + }); + } +} - let options; - while (options = buttonQueue.pop()) { - createButton(options.ppcpConfig); + +/** + * 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. + */ + 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; } + } +} - if (!window.ApplePaySession) { - jQuery('body').addClass('ppcp-non-ios-device') - } - }; - - document.addEventListener( - 'DOMContentLoaded', - () => { - - if (typeof (buttonConfig) === 'undefined') { - console.error('PayPal button could not be configured.'); - return; - } - - let paypalLoaded = false; - let applePayLoaded = false; - - const tryToBoot = () => { - if (!bootstrapped && paypalLoaded && applePayLoaded) { - bootstrap(); - } - } - - // Load ApplePay SDK - loadCustomScript({ url: buttonConfig.sdk_url }).then(() => { - applePayLoaded = true; - tryToBoot(); - }); - - // Wait for PayPal to be loaded externally - if (typeof widgetBuilder.paypal !== 'undefined') { - paypalLoaded = true; - tryToBoot(); - } - - jQuery(document).on('ppcp-paypal-loaded', () => { - paypalLoaded = true; - tryToBoot(); - }); - }, - ); - -})({ - buttonConfig: window.wc_ppcp_applepay_admin, - jQuery: window.jQuery -}); +// Initialize the preview button manager. +buttonManager(); diff --git a/modules/ppcp-applepay/src/ApplepayModule.php b/modules/ppcp-applepay/src/ApplepayModule.php index 5ac6a62cb..409a9c81e 100644 --- a/modules/ppcp-applepay/src/ApplepayModule.php +++ b/modules/ppcp-applepay/src/ApplepayModule.php @@ -205,7 +205,7 @@ class ApplepayModule implements ModuleInterface { add_action( 'admin_enqueue_scripts', static function () use ( $c, $button ) { - if ( ! is_admin() || ! $c->get( 'wcgateway.is-ppcp-settings-standard-payments-page' ) ) { + if ( ! is_admin() || ! $c->get( 'wcgateway.is-ppcp-settings-payment-methods-page' ) ) { return; } diff --git a/modules/ppcp-applepay/src/Assets/DataToAppleButtonScripts.php b/modules/ppcp-applepay/src/Assets/DataToAppleButtonScripts.php index cdbe2062b..e7317fc5d 100644 --- a/modules/ppcp-applepay/src/Assets/DataToAppleButtonScripts.php +++ b/modules/ppcp-applepay/src/Assets/DataToAppleButtonScripts.php @@ -69,14 +69,13 @@ class DataToAppleButtonScripts { ); } - /** * Returns the appropriate admin data to send to ApplePay script * * @return array * @throws NotFoundException When the setting is not found. */ - public function apple_pay_script_data_for_admin(): array { + public function apple_pay_script_data_for_admin() : array { $base_location = wc_get_base_location(); $shop_country_code = $base_location['country']; $currency_code = get_woocommerce_currency(); @@ -250,11 +249,13 @@ class DataToAppleButtonScripts { $lang = $this->settings->has( 'applepay_button_language' ) ? $this->settings->get( 'applepay_button_language' ) : ''; $lang = apply_filters( 'woocommerce_paypal_payments_applepay_button_language', $lang ); $checkout_data_mode = $this->settings->has( 'applepay_checkout_data_mode' ) ? $this->settings->get( 'applepay_checkout_data_mode' ) : PropertiesDictionary::BILLING_DATA_MODE_DEFAULT; + $is_enabled = $this->settings->has( 'applepay_button_enabled' ) && $this->settings->get( 'applepay_button_enabled' ); return array( 'sdk_url' => $this->sdk_url, - 'is_debug' => defined( 'WP_DEBUG' ) && WP_DEBUG ? true : false, + 'is_debug' => defined( 'WP_DEBUG' ) && WP_DEBUG, 'is_admin' => true, + 'is_enabled' => $is_enabled, 'preferences' => array( 'checkout_data_mode' => $checkout_data_mode, ), diff --git a/modules/ppcp-button/resources/js/modules/Renderer/PreviewButton.js b/modules/ppcp-button/resources/js/modules/Renderer/PreviewButton.js new file mode 100644 index 000000000..f2b7f0b4c --- /dev/null +++ b/modules/ppcp-button/resources/js/modules/Renderer/PreviewButton.js @@ -0,0 +1,154 @@ +import merge from 'deepmerge'; + +/** + * Base class for APM button previews, used on the plugin's settings page. + */ +class PreviewButton { + /** + * @param {string} selector - CSS ID of the wrapper, including the `#` + * @param {object} apiConfig - PayPal configuration object; retrieved via a + * widgetBuilder API method + */ + constructor({ + selector, + apiConfig, + }) { + this.apiConfig = apiConfig; + this.defaultAttributes = {}; + this.buttonConfig = {}; + this.ppcpConfig = {}; + this.isDynamic = true; + + // The selector is usually overwritten in constructor of derived class. + this.selector = selector; + this.wrapper = selector; + + this.domWrapper = null; + } + + /** + * Creates a new DOM node to contain the preview button. + * + * @return {jQuery} Always a single jQuery element with the new DOM node. + */ + createNewWrapper() { + const previewId = this.selector.replace('#', ''); + const previewClass = 'ppcp-button-apm'; + + return jQuery(`
`); + } + + /** + * 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. + */ + setDynamic(state) { + this.isDynamic = state; + return this; + } + + /** + * 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; + } + + /** + * Updates the button configuration with current details from the form. + * + * @return {this} Reference to self, for chaining. + */ + 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(previewConfig) { + throw new Error('The "createButton" method must be implemented by the derived class'); + } + + /** + * Refreshes the button in the DOM. + * Will always create a new button in the DOM. + */ + render() { + // The APM button is disabled and cannot be enabled on the current page: Do not render it. + if (!this.isDynamic && !this.buttonConfig.is_enabled) { + return; + } + + if (!this.domWrapper) { + if (!this.wrapper) { + console.error('Skip render, button is not configured yet'); + return; + } + this.domWrapper = this.createNewWrapper(); + 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.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); + + /* + * Unfortunately, a hacky way that is required to guarantee that this preview button is + * actually visible after calling the `render()` method. On some sites, we've noticed that + * certain JS events (like `ppcp-hidden`) do not fire in the expected order. This causes + * problems with preview buttons not being displayed instantly. + * + * Using a timeout here will make the button visible again at the end of the current + * event queue. + */ + setTimeout(() => this.domWrapper.show()); + } + + remove() { + this.isVisible = false; + + if (this.domWrapper) { + this.domWrapper.hide().empty(); + } + } +} + +export default PreviewButton; diff --git a/modules/ppcp-button/resources/js/modules/Renderer/PreviewButtonManager.js b/modules/ppcp-button/resources/js/modules/Renderer/PreviewButtonManager.js new file mode 100644 index 000000000..5277d9d8a --- /dev/null +++ b/modules/ppcp-button/resources/js/modules/Renderer/PreviewButtonManager.js @@ -0,0 +1,286 @@ +import { loadCustomScript } from '@paypal/paypal-js'; +import widgetBuilder from './WidgetBuilder'; +import { debounce } from '../../../../../ppcp-blocks/resources/js/Helper/debounce'; + +/** + * Manages all PreviewButton instances of a certain payment method on the page. + */ +class PreviewButtonManager { + /** + * Resolves the promise. + * Used by `this.boostrap()` to process enqueued initialization logic. + */ + #onInitResolver; + + /** + * A deferred Promise that is resolved once the page is ready. + * Deferred init logic can be added by using `this.#onInit.then(...)` + * + * @param {Promise|null} + */ + #onInit; + + constructor({ + methodName, + buttonConfig, + defaultAttributes, + }) { + // Define the payment method name in the derived class. + this.methodName = methodName; + + this.buttonConfig = buttonConfig; + this.defaultAttributes = defaultAttributes; + + this.isEnabled = true; + this.buttons = {}; + this.apiConfig = null; + + this.#onInit = new Promise(resolve => { + this.#onInitResolver = resolve; + }); + + this.bootstrap = this.bootstrap.bind(this); + this.renderPreview = this.renderPreview.bind(this); + + /** + * The "configureAllButtons" method applies ppcpConfig to all buttons that were created + * by this PreviewButtonManager instance. We debounce this method, as it should invoke + * only once, even if called multiple times in a row. + * + * This is required, as the `ppcp_paypal_render_preview` event does not fire for all + * buttons, but only a single time, passing in a random button's wrapper-ID; however, + * that event should always refresh all preview buttons, not only that single button. + */ + this._configureAllButtons = debounce(this._configureAllButtons.bind(this), 100); + + this.registerEventListeners(); + } + + /** + * Protected method that needs to be implemented by the derived class. + * 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) { + throw new Error('The "fetchConfig" method must be implemented by the derived class'); + } + + /** + * Protected method that needs to be implemented by the derived class. + * This method is responsible for creating a new PreviewButton instance and returning it. + * + * @param {string} wrapperId - CSS ID of the wrapper element. + * @return {PreviewButton} + */ + createButtonInstance(wrapperId) { + throw new Error('The "createButtonInstance" method must be implemented by the derived class'); + } + + registerEventListeners() { + jQuery(document).one('DOMContentLoaded', this.bootstrap); + + // General event that all APM buttons react to. + 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); + } + + /** + * Output an error message to the console, with a module-specific prefix. + */ + error(message, ...args) { + 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 + * used without limitation. + * + * @return {Promise} + */ + async bootstrap() { + const MAX_WAIT_TIME = 10000; // Fail, if PayPal SDK is unavailable after 10 seconds. + const RESOLVE_INTERVAL = 200; + + if (!this.buttonConfig || !widgetBuilder) { + this.error('Button could not be configured.'); + return; + } + + // This is a localization object of "gateway-settings.js". If it's missing, the script was + // not loaded. + if (!window.PayPalCommerceGatewaySettings) { + this.error( + 'PayPal settings are not fully loaded. Please clear the cache and reload the page.'); + return; + } + + // A helper function that clears the interval and resolves/rejects the promise. + const resolveOrReject = (resolve, reject, id, success = true) => { + clearInterval(id); + success ? resolve() : reject('Timeout while waiting for widgetBuilder.paypal'); + }; + + // Wait for the PayPal SDK to be ready. + const paypalPromise = new Promise((resolve, reject) => { + let elapsedTime = 0; + + const id = setInterval(() => { + if (widgetBuilder.paypal) { + resolveOrReject(resolve, reject, id); + } else if (elapsedTime >= MAX_WAIT_TIME) { + resolveOrReject(resolve, reject, id, false); + } + elapsedTime += RESOLVE_INTERVAL; + }, RESOLVE_INTERVAL); + }); + + // Load the custom SDK script. + const customScriptPromise = loadCustomScript({ url: this.buttonConfig.sdk_url }); + + // Wait for both promises to resolve before continuing. + await Promise + .all([customScriptPromise, paypalPromise]) + .catch(err => { + console.log(`Failed to load ${this.methodName} dependencies:`, err); + }); + + /* + The fetchConfig method requires two objects to succeed: + (a) the SDK custom-script + (b) the `widgetBuilder.paypal` object + */ + this.apiConfig = await this.fetchConfig(widgetBuilder.paypal); + await this.#onInitResolver(); + + this.#onInit = null; + } + + /** + * Event handler, fires on `ppcp_paypal_render_preview` + * + * @param ev - Ignored + * @param ppcpConfig - The button settings for the preview. + */ + renderPreview(ev, ppcpConfig) { + const id = ppcpConfig.button.wrapper; + + if (!id) { + this.error('Button did not provide a wrapper ID', ppcpConfig); + return; + } + + if (!this.buttons[id]) { + this._addButton(id, ppcpConfig); + } else { + // This is a debounced method, that fires after 100ms. + this._configureAllButtons(ppcpConfig); + } + } + + /** + * Applies a new configuration to an existing preview button. + */ + _configureButton(id, ppcpConfig) { + this.buttons[id] + .setDynamic(this.isDynamic()) + .setPpcpConfig(ppcpConfig) + .render(); + } + + /** + * Apples the provided configuration to all existing preview buttons. + */ + _configureAllButtons(ppcpConfig) { + Object.entries(this.buttons).forEach(([id, button]) => { + this._configureButton(id, { + ...ppcpConfig, + button: { + ...ppcpConfig.button, + + // The ppcpConfig object might refer to a different wrapper. + // Fix the selector, to avoid unintentionally hidden preview buttons. + wrapper: button.wrapper, + }, + }); + }); + } + + /** + * Creates a new preview button, that is rendered once the bootstrapping Promise resolves. + */ + _addButton(id, ppcpConfig) { + const createButton = () => { + if (!this.buttons[id]) { + this.buttons[id] = this.createButtonInstance(id).setButtonConfig(this.buttonConfig); + } + + this._configureButton(id, ppcpConfig); + }; + + if (this.#onInit) { + this.#onInit.then(createButton); + } else { + createButton(); + } + } + + /** + * Refreshes all buttons using the latest buttonConfig. + * + * @return {this} Reference to self, for chaining. + */ + renderButtons() { + if (this.isEnabled) { + Object.values(this.buttons).forEach(button => button.render()); + } else { + Object.values(this.buttons).forEach(button => button.remove()); + } + + return this; + } + + /** + * Enables this payment method, which re-creates or refreshes all buttons. + * + * @return {this} Reference to self, for chaining. + */ + enable() { + if (!this.isEnabled) { + this.isEnabled = true; + this.renderButtons(); + } + + return this; + } + + /** + * Disables this payment method, effectively removing all preview buttons. + * + * @return {this} Reference to self, for chaining. + */ + disable() { + if (!this.isEnabled) { + this.isEnabled = false; + this.renderButtons(); + } + + return this; + } +} + +export default PreviewButtonManager; diff --git a/modules/ppcp-googlepay/extensions.php b/modules/ppcp-googlepay/extensions.php index 31b65e6f1..92ff195b0 100644 --- a/modules/ppcp-googlepay/extensions.php +++ b/modules/ppcp-googlepay/extensions.php @@ -20,6 +20,9 @@ return array( 'wcgateway.settings.fields' => function ( ContainerInterface $container, array $fields ): array { + // Used in various places to mark fields for the preview button. + $apm_name = 'GooglePay'; + // Eligibility check. if ( ! $container->has( 'googlepay.eligible' ) || ! $container->get( 'googlepay.eligible' ) ) { return $fields; @@ -133,7 +136,7 @@ return array( 'gateway' => 'dcc', 'requirements' => array(), 'custom_attributes' => array( - 'data-ppcp-display' => wp_json_encode( + 'data-ppcp-display' => wp_json_encode( array( $display_manager ->rule() @@ -142,64 +145,79 @@ return array( ->action_visible( 'googlepay_button_color' ) ->action_visible( 'googlepay_button_language' ) ->action_visible( 'googlepay_button_shipping_enabled' ) + ->action_visible( 'googlepay_button_preview' ) ->action_class( 'googlepay_button_enabled', 'active' ) ->to_array(), ) ), + 'data-ppcp-apm-name' => $apm_name, + 'data-ppcp-field-name' => 'is_enabled', ), 'classes' => array( 'ppcp-valign-label-middle', 'ppcp-align-label-center' ), ), 'googlepay_button_type' => array( - 'title' => __( 'Button Label', 'woocommerce-paypal-payments' ), - 'type' => 'select', - 'desc_tip' => true, - 'description' => __( + 'title' => __( 'Button Label', 'woocommerce-paypal-payments' ), + 'type' => 'select', + 'desc_tip' => true, + 'description' => __( 'This controls the label of the Google Pay button.', 'woocommerce-paypal-payments' ), - 'classes' => array( 'ppcp-field-indent' ), - 'class' => array(), - 'input_class' => array( 'wc-enhanced-select' ), - 'default' => 'pay', - 'options' => PropertiesDictionary::button_types(), - 'screens' => array( State::STATE_ONBOARDED ), - 'gateway' => 'dcc', - 'requirements' => array(), + 'classes' => array( 'ppcp-field-indent' ), + 'class' => array(), + 'input_class' => array( 'wc-enhanced-select' ), + 'default' => 'pay', + 'options' => PropertiesDictionary::button_types(), + 'screens' => array( State::STATE_ONBOARDED ), + 'gateway' => 'dcc', + 'requirements' => array(), + 'custom_attributes' => array( + 'data-ppcp-apm-name' => $apm_name, + 'data-ppcp-field-name' => 'type', + ), ), 'googlepay_button_color' => array( - 'title' => __( 'Button Color', 'woocommerce-paypal-payments' ), - 'type' => 'select', - 'desc_tip' => true, - 'description' => __( + 'title' => __( 'Button Color', 'woocommerce-paypal-payments' ), + 'type' => 'select', + 'desc_tip' => true, + 'description' => __( 'Google Pay payment buttons exist in two styles: dark and light. To provide contrast, use dark buttons on light backgrounds and light buttons on dark or colorful backgrounds.', 'woocommerce-paypal-payments' ), - 'label' => '', - 'input_class' => array( 'wc-enhanced-select' ), - 'classes' => array( 'ppcp-field-indent' ), - 'class' => array(), - 'default' => 'black', - 'options' => PropertiesDictionary::button_colors(), - 'screens' => array( State::STATE_ONBOARDED ), - 'gateway' => 'dcc', - 'requirements' => array(), + 'label' => '', + 'input_class' => array( 'wc-enhanced-select' ), + 'classes' => array( 'ppcp-field-indent' ), + 'class' => array(), + 'default' => 'black', + 'options' => PropertiesDictionary::button_colors(), + 'screens' => array( State::STATE_ONBOARDED ), + 'gateway' => 'dcc', + 'requirements' => array(), + 'custom_attributes' => array( + 'data-ppcp-apm-name' => $apm_name, + 'data-ppcp-field-name' => 'color', + ), ), 'googlepay_button_language' => array( - 'title' => __( 'Button Language', 'woocommerce-paypal-payments' ), - 'type' => 'select', - 'desc_tip' => true, - 'description' => __( + 'title' => __( 'Button Language', 'woocommerce-paypal-payments' ), + 'type' => 'select', + 'desc_tip' => true, + 'description' => __( 'The language and region used for the displayed Google Pay button. The default value is the current language and region setting in a browser.', 'woocommerce-paypal-payments' ), - 'classes' => array( 'ppcp-field-indent' ), - 'class' => array(), - 'input_class' => array( 'wc-enhanced-select' ), - 'default' => 'en', - 'options' => PropertiesDictionary::button_languages(), - 'screens' => array( State::STATE_ONBOARDED ), - 'gateway' => 'dcc', - 'requirements' => array(), + 'classes' => array( 'ppcp-field-indent' ), + 'class' => array(), + 'input_class' => array( 'wc-enhanced-select' ), + 'default' => 'en', + 'options' => PropertiesDictionary::button_languages(), + 'screens' => array( State::STATE_ONBOARDED ), + 'gateway' => 'dcc', + 'requirements' => array(), + 'custom_attributes' => array( + 'data-ppcp-apm-name' => $apm_name, + 'data-ppcp-field-name' => 'language', + ), ), 'googlepay_button_shipping_enabled' => array( 'title' => __( 'Shipping Callback', 'woocommerce-paypal-payments' ), @@ -209,13 +227,29 @@ return array( 'Synchronizes your available shipping options with Google Pay. Enabling this may impact the buyer experience.', 'woocommerce-paypal-payments' ), - 'classes' => array( 'ppcp-field-indent' ), + 'classes' => array( 'ppcp-field-indent ppcp' ), 'label' => __( 'Enable Google Pay shipping callback', 'woocommerce-paypal-payments' ), 'default' => 'no', 'screens' => array( State::STATE_ONBOARDED ), 'gateway' => 'dcc', 'requirements' => array(), ), + 'googlepay_button_preview' => array( + 'type' => 'ppcp-text', + 'text' => sprintf( + ' +
+

' . __( 'Button Styling Preview', 'woocommerce-paypal-payments' ) . '

+
+
', + $apm_name + ), + 'screens' => array( + State::STATE_ONBOARDED, + ), + 'requirements' => array(), + 'gateway' => 'dcc', + ), ) ); }, diff --git a/modules/ppcp-googlepay/resources/js/GooglepayButton.js b/modules/ppcp-googlepay/resources/js/GooglepayButton.js index 00b8f7f7d..ea1387cea 100644 --- a/modules/ppcp-googlepay/resources/js/GooglepayButton.js +++ b/modules/ppcp-googlepay/resources/js/GooglepayButton.js @@ -139,6 +139,10 @@ class GooglepayButton { initEventHandlers() { const { wrapper, ppcpButtonWrapper } = this.contextConfig(); + if (wrapper === ppcpButtonWrapper) { + throw new Error(`[GooglePayButton] "wrapper" and "ppcpButtonWrapper" values must differ to avoid infinite loop. Current value: "${wrapper}"`); + } + const syncButtonVisibility = () => { const $ppcpButtonWrapper = jQuery(ppcpButtonWrapper); setVisible(wrapper, $ppcpButtonWrapper.is(':visible')); diff --git a/modules/ppcp-googlepay/resources/js/boot-admin.js b/modules/ppcp-googlepay/resources/js/boot-admin.js index 3b971986f..6010cec4c 100644 --- a/modules/ppcp-googlepay/resources/js/boot-admin.js +++ b/modules/ppcp-googlepay/resources/js/boot-admin.js @@ -1,146 +1,108 @@ -import {loadCustomScript} from "@paypal/paypal-js"; -import GooglepayButton from "./GooglepayButton"; -import widgetBuilder from "../../../ppcp-button/resources/js/modules/Renderer/WidgetBuilder"; +import GooglepayButton from './GooglepayButton'; +import PreviewButton from '../../../ppcp-button/resources/js/modules/Renderer/PreviewButton'; +import PreviewButtonManager from '../../../ppcp-button/resources/js/modules/Renderer/PreviewButtonManager'; -(function ({ - buttonConfig, - jQuery -}) { - - let googlePayConfig; - let buttonQueue = []; - let activeButtons = {}; - let bootstrapped = false; - - // React to PayPal config changes. - jQuery(document).on('ppcp_paypal_render_preview', (ev, ppcpConfig) => { - if (bootstrapped) { - createButton(ppcpConfig); - } else { - buttonQueue.push({ - ppcpConfig: JSON.parse(JSON.stringify(ppcpConfig)) - }); - } - }); - - // React to GooglePay config changes. - jQuery([ - '#ppcp-googlepay_button_enabled', - '#ppcp-googlepay_button_type', - '#ppcp-googlepay_button_color', - '#ppcp-googlepay_button_language', - '#ppcp-googlepay_button_shipping_enabled' - ].join(',')).on('change', () => { - for (const [selector, ppcpConfig] of Object.entries(activeButtons)) { - createButton(ppcpConfig); - } - }); - - // Maybe we can find a more elegant reload method when transitioning from styling modes. - jQuery([ - '#ppcp-smart_button_enable_styling_per_location' - ].join(',')).on('change', () => { - setTimeout(() => { - for (const [selector, ppcpConfig] of Object.entries(activeButtons)) { - createButton(ppcpConfig); - } - }, 100); - }); - - const applyConfigOptions = function (buttonConfig) { - buttonConfig.button = buttonConfig.button || {}; - buttonConfig.button.style = buttonConfig.button.style || {}; - buttonConfig.button.style.type = jQuery('#ppcp-googlepay_button_type').val(); - buttonConfig.button.style.color = jQuery('#ppcp-googlepay_button_color').val(); - buttonConfig.button.style.language = jQuery('#ppcp-googlepay_button_language').val(); +/** + * Accessor that creates and returns a single PreviewButtonManager instance. + */ +const buttonManager = () => { + if (!GooglePayPreviewButtonManager.instance) { + GooglePayPreviewButtonManager.instance = new GooglePayPreviewButtonManager(); } - const createButton = function (ppcpConfig) { - const selector = ppcpConfig.button.wrapper + 'GooglePay'; + return GooglePayPreviewButtonManager.instance; +}; - if (!jQuery('#ppcp-googlepay_button_enabled').is(':checked')) { - jQuery(selector).remove(); - return; - } - buttonConfig = JSON.parse(JSON.stringify(buttonConfig)); - buttonConfig.button.wrapper = selector; - applyConfigOptions(buttonConfig); +/** + * Manages all GooglePay preview buttons on this page. + */ +class GooglePayPreviewButtonManager extends PreviewButtonManager { + constructor() { + const args = { + methodName: 'GooglePay', + buttonConfig: window.wc_ppcp_googlepay_admin, + }; - const wrapperElement = `
`; - - if (!jQuery(selector).length) { - jQuery(ppcpConfig.button.wrapper).after(wrapperElement); - } else { - jQuery(selector).replaceWith(wrapperElement); - } - - const button = new GooglepayButton( - 'preview', - null, - buttonConfig, - ppcpConfig, - ); - - button.init(googlePayConfig); - - activeButtons[selector] = ppcpConfig; + super(args); } - const bootstrap = async function () { - if (!widgetBuilder.paypal) { - return; + /** + * 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 {}; } - googlePayConfig = await widgetBuilder.paypal.Googlepay().config(); + return await apiMethod(); + } - // We need to set bootstrapped here otherwise googlePayConfig may not be set. - bootstrapped = true; + /** + * 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, + }); + } +} - let options; - while (options = buttonQueue.pop()) { - createButton(options.ppcpConfig); + +/** + * 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. + */ + 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); } - }; + } +} - document.addEventListener( - 'DOMContentLoaded', - () => { - - if (typeof (buttonConfig) === 'undefined') { - console.error('PayPal button could not be configured.'); - return; - } - - let paypalLoaded = false; - let googlePayLoaded = false; - - const tryToBoot = () => { - if (!bootstrapped && paypalLoaded && googlePayLoaded) { - bootstrap(); - } - } - - // Load GooglePay SDK - loadCustomScript({ url: buttonConfig.sdk_url }).then(() => { - googlePayLoaded = true; - tryToBoot(); - }); - - // Wait for PayPal to be loaded externally - if (typeof widgetBuilder.paypal !== 'undefined') { - paypalLoaded = true; - tryToBoot(); - } - - jQuery(document).on('ppcp-paypal-loaded', () => { - paypalLoaded = true; - tryToBoot(); - }); - }, - ); - -})({ - buttonConfig: window.wc_ppcp_googlepay_admin, - jQuery: window.jQuery -}); +// Initialize the preview button manager. +buttonManager(); diff --git a/modules/ppcp-googlepay/src/Assets/Button.php b/modules/ppcp-googlepay/src/Assets/Button.php index 15f16e381..8ad4163ab 100644 --- a/modules/ppcp-googlepay/src/Assets/Button.php +++ b/modules/ppcp-googlepay/src/Assets/Button.php @@ -418,9 +418,12 @@ class Button implements ButtonInterface { $shipping['countries'] = array_keys( $this->wc_countries()->get_shipping_countries() ); } + $is_enabled = $this->settings->has( 'googlepay_button_enabled' ) && $this->settings->get( 'googlepay_button_enabled' ); + return array( 'environment' => $this->environment->current_environment_is( Environment::SANDBOX ) ? 'TEST' : 'PRODUCTION', - 'is_debug' => defined( 'WP_DEBUG' ) && WP_DEBUG ? true : false, + 'is_debug' => defined( 'WP_DEBUG' ) && WP_DEBUG, + 'is_enabled' => $is_enabled, 'sdk_url' => $this->sdk_url, 'button' => array( 'wrapper' => '#ppc-button-googlepay-container', diff --git a/modules/ppcp-googlepay/src/GooglepayModule.php b/modules/ppcp-googlepay/src/GooglepayModule.php index e478881ba..e2b0ea8c5 100644 --- a/modules/ppcp-googlepay/src/GooglepayModule.php +++ b/modules/ppcp-googlepay/src/GooglepayModule.php @@ -112,7 +112,7 @@ class GooglepayModule implements ModuleInterface { add_action( 'admin_enqueue_scripts', static function () use ( $c, $button ) { - if ( ! is_admin() || ! $c->get( 'wcgateway.is-ppcp-settings-standard-payments-page' ) ) { + if ( ! is_admin() || ! $c->get( 'wcgateway.is-ppcp-settings-payment-methods-page' ) ) { return; } diff --git a/modules/ppcp-wc-gateway/resources/css/gateway-settings.scss b/modules/ppcp-wc-gateway/resources/css/gateway-settings.scss index 0a4dc45cc..e915905bd 100644 --- a/modules/ppcp-wc-gateway/resources/css/gateway-settings.scss +++ b/modules/ppcp-wc-gateway/resources/css/gateway-settings.scss @@ -2,10 +2,24 @@ width: 350px; padding: 15px; border: 1px solid lightgray; + background: #eeeeef; border-radius: 15px; box-shadow: 0 2px 10px 1px #ddd; margin-right: -28px; + // Preview box showing a single button. + &[data-ppcp-apm-preview] { + height: 82px; + + @media (min-width: 1200px) { + margin-top: -149px; + } + + @media (min-width: 601px) and (max-width: 1399px) { + margin-right: 10px; + } + } + h4 { margin-top: 0; } diff --git a/modules/ppcp-wc-gateway/resources/js/gateway-settings.js b/modules/ppcp-wc-gateway/resources/js/gateway-settings.js index ed50902e6..5a81fca2f 100644 --- a/modules/ppcp-wc-gateway/resources/js/gateway-settings.js +++ b/modules/ppcp-wc-gateway/resources/js/gateway-settings.js @@ -1,5 +1,6 @@ import { loadScript } from "@paypal/paypal-js"; import {debounce} from "./helper/debounce"; +import { buttonRefreshTriggerFactory, buttonSettingsGetterFactory } from './helper/preview-button'; import Renderer from '../../../ppcp-button/resources/js/modules/Renderer/Renderer' import MessageRenderer from "../../../ppcp-button/resources/js/modules/Renderer/MessageRenderer"; import {setVisibleByClass, isVisible} from "../../../ppcp-button/resources/js/modules/Helper/Hiding"; @@ -312,6 +313,7 @@ document.addEventListener( const payLaterMessagingLocations = ['product', 'cart', 'checkout', 'shop', 'home', 'general']; const paypalButtonLocations = ['product', 'cart', 'checkout', 'mini-cart', 'cart-block', 'checkout-block-express', 'general']; + // Default preview buttons; on "Standard Payments" tab. paypalButtonLocations.forEach((location) => { const inputNamePrefix = location === 'checkout' ? '#ppcp-button' : '#ppcp-button_' + location; const wrapperName = location.split('-').map(s => s.charAt(0).toUpperCase() + s.slice(1)).join(''); @@ -330,6 +332,31 @@ document.addEventListener( createButtonPreview(() => getButtonSettings('#ppcp' + wrapperName + 'ButtonPreview', fields)); }); + /** + * Inspect DOM to find APM button previews; on tabs like "Advanced Card Payments". + * + * How it works: + * + * 1. Add a
to hold the preview button to the settings page: + * - `id="ppcp[NAME]ButtonPreview"` + * - `data-ppc-apm-preview="[NAME]"` + * 2. Mark all fields that are relevant for the preview button: + * - custom_attribute: `data-ppcp-apm-name="[NAME]"` + * - custom_attribute: `data-ppcp-field-name="[FIELD]"` + * + * This block will find all marked input fields and trigger a re-render of the + * preview button when one of those fields value changes. + * + * Example: See the ppcp-google-pay "extensions.php" file. + */ + document.querySelectorAll('[data-ppcp-apm-preview]').forEach(item => { + const apmName = item.dataset.ppcpApmPreview; + const getSettings = buttonSettingsGetterFactory(apmName) + const renderButtonPreview = buttonRefreshTriggerFactory(apmName); + + renderPreview(getSettings, renderButtonPreview) + }); + payLaterMessagingLocations.forEach((location) => { const inputNamePrefix = '#ppcp-pay_later_' + location + '_message'; const wrapperName = location.charAt(0).toUpperCase() + location.slice(1); diff --git a/modules/ppcp-wc-gateway/resources/js/helper/preview-button.js b/modules/ppcp-wc-gateway/resources/js/helper/preview-button.js new file mode 100644 index 000000000..50ff3b710 --- /dev/null +++ b/modules/ppcp-wc-gateway/resources/js/helper/preview-button.js @@ -0,0 +1,68 @@ +/** + * Returns a Map with all input fields that are relevant to render the preview of the + * given payment button. + * + * @param {string} apmName - Value of the custom attribute `data-ppcp-apm-name`. + * @return {Map} + */ +export function getButtonFormFields(apmName) { + const inputFields = document.querySelectorAll(`[data-ppcp-apm-name="${apmName}"]`); + + return [...inputFields].reduce((fieldMap, el) => { + const key = el.dataset.ppcpFieldName; + let getter = () => el.value; + + if ('LABEL' === el.tagName) { + el = el.querySelector('input[type="checkbox"]'); + getter = () => el.checked; + } + + return fieldMap.set(key, { + val: getter, + el, + }); + }, new Map()); +} + + +/** + * Returns a function that triggers an update of the specified preview button, when invoked. + + * @param {string} apmName + * @return {((object) => void)} + */ +export function buttonRefreshTriggerFactory(apmName) { + const eventName = `ppcp_paypal_render_preview_${apmName}`; + + return (settings) => { + jQuery(document).trigger(eventName, settings); + }; +} + +/** + * Returns a function that gets the current form values of the specified preview button. + * + * @param {string} apmName + * @return {() => {button: {wrapper:string, is_enabled:boolean, style:{}}}} + */ +export function buttonSettingsGetterFactory(apmName) { + const fields = getButtonFormFields(apmName); + + return () => { + const buttonConfig = { + wrapper: `#ppcp${apmName}ButtonPreview`, + 'is_enabled': true, + style: {}, + }; + + fields.forEach((item, name) => { + if ('is_enabled' === name) { + buttonConfig[name] = item.val(); + } else { + buttonConfig.style[name] = item.val(); + } + }); + + return { button: buttonConfig }; + }; +} diff --git a/modules/ppcp-wc-gateway/services.php b/modules/ppcp-wc-gateway/services.php index f2991d9ef..f8541e152 100644 --- a/modules/ppcp-wc-gateway/services.php +++ b/modules/ppcp-wc-gateway/services.php @@ -201,9 +201,23 @@ return array( ); }, - 'wcgateway.is-ppcp-settings-standard-payments-page' => static function ( ContainerInterface $container ): bool { - return $container->get( 'wcgateway.is-ppcp-settings-page' ) - && $container->get( 'wcgateway.current-ppcp-settings-page-id' ) === PayPalGateway::ID; + // Checks, if the current admin page contains settings for this plugin's payment methods. + 'wcgateway.is-ppcp-settings-payment-methods-page' => static function ( ContainerInterface $container ) : bool { + if ( ! $container->get( 'wcgateway.is-ppcp-settings-page' ) ) { + return false; + } + + $active_tab = $container->get( 'wcgateway.current-ppcp-settings-page-id' ); + + return in_array( + $active_tab, + array( + PayPalGateway::ID, + CreditCardGateway::ID, + CardButtonGateway::ID, + ), + true + ); }, 'wcgateway.current-ppcp-settings-page-id' => static function ( ContainerInterface $container ): string { diff --git a/modules/ppcp-wc-gateway/src/Assets/SettingsPageAssets.php b/modules/ppcp-wc-gateway/src/Assets/SettingsPageAssets.php index 50f453d9e..353d33827 100644 --- a/modules/ppcp-wc-gateway/src/Assets/SettingsPageAssets.php +++ b/modules/ppcp-wc-gateway/src/Assets/SettingsPageAssets.php @@ -112,6 +112,13 @@ class SettingsPageAssets { */ private $billing_agreements_endpoint; + /** + * Whether we're on a settings page for our plugin's payment methods. + * + * @var bool + */ + private $is_paypal_payment_method_page; + /** * Assets constructor. * @@ -128,6 +135,7 @@ class SettingsPageAssets { * @param bool $is_settings_page Whether it's a settings page of this plugin. * @param bool $is_acdc_enabled Whether the ACDC gateway is enabled. * @param BillingAgreementsEndpoint $billing_agreements_endpoint Billing Agreements endpoint. + * @param bool $is_paypal_payment_method_page Whether we're on a settings page for our plugin's payment methods. */ public function __construct( string $module_url, @@ -142,21 +150,23 @@ class SettingsPageAssets { array $all_funding_sources, bool $is_settings_page, bool $is_acdc_enabled, - BillingAgreementsEndpoint $billing_agreements_endpoint + BillingAgreementsEndpoint $billing_agreements_endpoint, + bool $is_paypal_payment_method_page ) { - $this->module_url = $module_url; - $this->version = $version; - $this->subscription_helper = $subscription_helper; - $this->client_id = $client_id; - $this->currency = $currency; - $this->country = $country; - $this->environment = $environment; - $this->is_pay_later_button_enabled = $is_pay_later_button_enabled; - $this->disabled_sources = $disabled_sources; - $this->all_funding_sources = $all_funding_sources; - $this->is_settings_page = $is_settings_page; - $this->is_acdc_enabled = $is_acdc_enabled; - $this->billing_agreements_endpoint = $billing_agreements_endpoint; + $this->module_url = $module_url; + $this->version = $version; + $this->subscription_helper = $subscription_helper; + $this->client_id = $client_id; + $this->currency = $currency; + $this->country = $country; + $this->environment = $environment; + $this->is_pay_later_button_enabled = $is_pay_later_button_enabled; + $this->disabled_sources = $disabled_sources; + $this->all_funding_sources = $all_funding_sources; + $this->is_settings_page = $is_settings_page; + $this->is_acdc_enabled = $is_acdc_enabled; + $this->billing_agreements_endpoint = $billing_agreements_endpoint; + $this->is_paypal_payment_method_page = $is_paypal_payment_method_page; } /** @@ -176,7 +186,7 @@ class SettingsPageAssets { $this->register_admin_assets(); } - if ( $this->is_paypal_payment_method_page() ) { + if ( $this->is_paypal_payment_method_page ) { $this->register_paypal_admin_assets(); } } @@ -184,30 +194,6 @@ class SettingsPageAssets { } - /** - * Check whether the current page is PayPal payment method settings. - * - * @return bool - */ - private function is_paypal_payment_method_page(): bool { - - if ( ! function_exists( 'get_current_screen' ) ) { - return false; - } - - $screen = get_current_screen(); - if ( ! $screen || $screen->id !== 'woocommerce_page_wc-settings' ) { - return false; - } - - // phpcs:disable WordPress.Security.NonceVerification.Recommended - $tab = wc_clean( wp_unslash( $_GET['tab'] ?? '' ) ); - $section = wc_clean( wp_unslash( $_GET['section'] ?? '' ) ); - // phpcs:enable WordPress.Security.NonceVerification.Recommended - - return 'checkout' === $tab && in_array( $section, array( PayPalGateway::ID, CardButtonGateway::ID ), true ); - } - /** * Register assets for PayPal admin pages. */ diff --git a/modules/ppcp-wc-gateway/src/WCGatewayModule.php b/modules/ppcp-wc-gateway/src/WCGatewayModule.php index 19f67ef53..76de0478c 100644 --- a/modules/ppcp-wc-gateway/src/WCGatewayModule.php +++ b/modules/ppcp-wc-gateway/src/WCGatewayModule.php @@ -183,7 +183,8 @@ class WCGatewayModule implements ModuleInterface { $c->get( 'wcgateway.settings.funding-sources' ), $c->get( 'wcgateway.is-ppcp-settings-page' ), $settings->has( 'dcc_enabled' ) && $settings->get( 'dcc_enabled' ), - $c->get( 'api.endpoint.billing-agreements' ) + $c->get( 'api.endpoint.billing-agreements' ), + $c->get( 'wcgateway.is-ppcp-settings-payment-methods-page' ) ); $assets->register_assets(); } diff --git a/tests/PHPUnit/WcGateway/Assets/SettingsPagesAssetsTest.php b/tests/PHPUnit/WcGateway/Assets/SettingsPagesAssetsTest.php index 5569c4158..931c55fcf 100644 --- a/tests/PHPUnit/WcGateway/Assets/SettingsPagesAssetsTest.php +++ b/tests/PHPUnit/WcGateway/Assets/SettingsPagesAssetsTest.php @@ -33,7 +33,8 @@ class SettingsPagesAssetsTest extends TestCase array(), true, false, - $billingAgreementEndpoint + $billingAgreementEndpoint, + true ); when('is_admin')