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(
+ '
+
',
+ $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(
+ '
+',
+ $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')