Merge pull request #2325 from woocommerce/PCP-3179-apple-pay-google-pay-buttons-no-longer-visible-in-standard-payments-button-previews-after-moving-them-to-advanced-card-processing-tab

Apple Pay & Google Pay buttons no longer visible in Standard Payments button previews after moving them to Advanced Card Processing tab (3179)
This commit is contained in:
Emili Castells 2024-06-17 09:30:47 +02:00 committed by GitHub
commit f5803f1c99
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
19 changed files with 950 additions and 393 deletions

View file

@ -20,6 +20,9 @@ use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings;
return array( return array(
'wcgateway.settings.fields' => function ( ContainerInterface $container, array $fields ): 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. // Eligibility check.
if ( ! $container->has( 'applepay.eligible' ) || ! $container->get( 'applepay.eligible' ) ) { if ( ! $container->has( 'applepay.eligible' ) || ! $container->get( 'applepay.eligible' ) ) {
return $fields; return $fields;
@ -171,7 +174,7 @@ return array(
'gateway' => 'dcc', 'gateway' => 'dcc',
'requirements' => array(), 'requirements' => array(),
'custom_attributes' => array( 'custom_attributes' => array(
'data-ppcp-display' => wp_json_encode( 'data-ppcp-display' => wp_json_encode(
array( array(
$display_manager $display_manager
->rule() ->rule()
@ -183,10 +186,13 @@ return array(
->action_visible( 'applepay_button_type' ) ->action_visible( 'applepay_button_type' )
->action_visible( 'applepay_button_language' ) ->action_visible( 'applepay_button_language' )
->action_visible( 'applepay_checkout_data_mode' ) ->action_visible( 'applepay_checkout_data_mode' )
->action_visible( 'applepay_button_preview' )
->action_class( 'applepay_button_enabled', 'active' ) ->action_class( 'applepay_button_enabled', 'active' )
->to_array(), ->to_array(),
) )
), ),
'data-ppcp-apm-name' => $apm_name,
'data-ppcp-field-name' => 'is_enabled',
), ),
'classes' => array( 'ppcp-valign-label-middle', 'ppcp-align-label-center' ), 'classes' => array( 'ppcp-valign-label-middle', 'ppcp-align-label-center' ),
), ),
@ -253,56 +259,68 @@ return array(
'requirements' => array(), 'requirements' => array(),
), ),
'applepay_button_type' => array( 'applepay_button_type' => array(
'title' => __( 'Button Label', 'woocommerce-paypal-payments' ), 'title' => __( 'Button Label', 'woocommerce-paypal-payments' ),
'type' => 'select', 'type' => 'select',
'desc_tip' => true, 'desc_tip' => true,
'description' => __( 'description' => __(
'This controls the label of the Apple Pay button.', 'This controls the label of the Apple Pay button.',
'woocommerce-paypal-payments' 'woocommerce-paypal-payments'
), ),
'classes' => array( 'ppcp-field-indent' ), 'classes' => array( 'ppcp-field-indent' ),
'class' => array(), 'class' => array(),
'input_class' => array( 'wc-enhanced-select' ), 'input_class' => array( 'wc-enhanced-select' ),
'default' => 'pay', 'default' => 'pay',
'options' => PropertiesDictionary::button_types(), 'options' => PropertiesDictionary::button_types(),
'screens' => array( State::STATE_ONBOARDED ), 'screens' => array( State::STATE_ONBOARDED ),
'gateway' => 'dcc', 'gateway' => 'dcc',
'requirements' => array(), 'requirements' => array(),
'custom_attributes' => array(
'data-ppcp-apm-name' => $apm_name,
'data-ppcp-field-name' => 'type',
),
), ),
'applepay_button_color' => array( 'applepay_button_color' => array(
'title' => __( 'Button Color', 'woocommerce-paypal-payments' ), 'title' => __( 'Button Color', 'woocommerce-paypal-payments' ),
'type' => 'select', 'type' => 'select',
'desc_tip' => true, 'desc_tip' => true,
'description' => __( '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.', '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' 'woocommerce-paypal-payments'
), ),
'label' => '', 'label' => '',
'input_class' => array( 'wc-enhanced-select' ), 'input_class' => array( 'wc-enhanced-select' ),
'classes' => array( 'ppcp-field-indent' ), 'classes' => array( 'ppcp-field-indent' ),
'class' => array(), 'class' => array(),
'default' => 'black', 'default' => 'black',
'options' => PropertiesDictionary::button_colors(), 'options' => PropertiesDictionary::button_colors(),
'screens' => array( State::STATE_ONBOARDED ), 'screens' => array( State::STATE_ONBOARDED ),
'gateway' => 'dcc', 'gateway' => 'dcc',
'requirements' => array(), 'requirements' => array(),
'custom_attributes' => array(
'data-ppcp-apm-name' => $apm_name,
'data-ppcp-field-name' => 'color',
),
), ),
'applepay_button_language' => array( 'applepay_button_language' => array(
'title' => __( 'Button Language', 'woocommerce-paypal-payments' ), 'title' => __( 'Button Language', 'woocommerce-paypal-payments' ),
'type' => 'select', 'type' => 'select',
'desc_tip' => true, 'desc_tip' => true,
'description' => __( '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.', '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' 'woocommerce-paypal-payments'
), ),
'classes' => array( 'ppcp-field-indent' ), 'classes' => array( 'ppcp-field-indent' ),
'class' => array(), 'class' => array(),
'input_class' => array( 'wc-enhanced-select' ), 'input_class' => array( 'wc-enhanced-select' ),
'default' => 'en', 'default' => 'en',
'options' => PropertiesDictionary::button_languages(), 'options' => PropertiesDictionary::button_languages(),
'screens' => array( State::STATE_ONBOARDED ), 'screens' => array( State::STATE_ONBOARDED ),
'gateway' => 'dcc', 'gateway' => 'dcc',
'requirements' => array(), 'requirements' => array(),
'custom_attributes' => array(
'data-ppcp-apm-name' => $apm_name,
'data-ppcp-field-name' => 'language',
),
), ),
'applepay_checkout_data_mode' => array( 'applepay_checkout_data_mode' => array(
'title' => __( 'Send checkout billing and shipping data to Apple Pay', 'woocommerce-paypal-payments' ), 'title' => __( 'Send checkout billing and shipping data to Apple Pay', 'woocommerce-paypal-payments' ),
@ -318,6 +336,22 @@ return array(
'gateway' => 'dcc', 'gateway' => 'dcc',
'requirements' => array(), 'requirements' => array(),
), ),
'applepay_button_preview' => array(
'type' => 'ppcp-text',
'text' => sprintf(
'
<div class="ppcp-preview ppcp-button-preview" data-ppcp-apm-preview="%1$s">
<h4>' . __( 'Button Styling Preview', 'woocommerce-paypal-payments' ) . '</h4>
<div id="ppcp%1$sButtonPreview" class="ppcp-button-preview-inner"></div>
</div>',
$apm_name
),
'screens' => array(
State::STATE_ONBOARDED,
),
'requirements' => array(),
'gateway' => 'dcc',
),
) )
); );
}, },

View file

@ -135,6 +135,10 @@ class ApplepayButton {
const { wrapper, ppcpButtonWrapper } = this.contextConfig(); const { wrapper, ppcpButtonWrapper } = this.contextConfig();
const wrapper_id = '#' + wrapper; 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 = () => { const syncButtonVisibility = () => {
if (!this.isEligible) { if (!this.isEligible) {
return; return;

View file

@ -1,148 +1,112 @@
import {loadCustomScript} from "@paypal/paypal-js"; import ApplepayButton from './ApplepayButton';
import ApplepayButton from "./ApplepayButton"; import PreviewButton from '../../../ppcp-button/resources/js/modules/Renderer/PreviewButton';
import widgetBuilder from "../../../ppcp-button/resources/js/modules/Renderer/WidgetBuilder"; import PreviewButtonManager from '../../../ppcp-button/resources/js/modules/Renderer/PreviewButtonManager';
(function ({ /**
buttonConfig, * Accessor that creates and returns a single PreviewButtonManager instance.
jQuery */
}) { const buttonManager = () => {
if (!ApplePayPreviewButtonManager.instance) {
let applePayConfig; ApplePayPreviewButtonManager.instance = new ApplePayPreviewButtonManager();
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();
} }
const createButton = function (ppcpConfig) { return ApplePayPreviewButtonManager.instance;
const selector = ppcpConfig.button.wrapper + 'ApplePay'; };
if (!jQuery('#ppcp-applepay_button_enabled').is(':checked')) {
jQuery(selector).remove();
return;
}
buttonConfig = JSON.parse(JSON.stringify(buttonConfig)); /**
buttonConfig.button.wrapper = selector.replace('#', ''); * Manages all Apple Pay preview buttons on this page.
applyConfigOptions(buttonConfig); */
class ApplePayPreviewButtonManager extends PreviewButtonManager {
constructor() {
const args = {
methodName: 'ApplePay',
buttonConfig: window.wc_ppcp_applepay_admin,
};
const wrapperElement = `<div id="${selector.replace('#', '')}" class="ppcp-button-apm ppcp-button-applepay"></div>`; super(args);
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;
} }
const bootstrap = async function () { /**
if (!widgetBuilder.paypal) { * Responsible for fetching and returning the PayPal configuration object for this payment
return; * 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) { // Initialize the preview button manager.
jQuery('body').addClass('ppcp-non-ios-device') buttonManager();
}
};
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
});

View file

@ -205,7 +205,7 @@ class ApplepayModule implements ModuleInterface {
add_action( add_action(
'admin_enqueue_scripts', 'admin_enqueue_scripts',
static function () use ( $c, $button ) { 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; return;
} }

View file

@ -69,14 +69,13 @@ class DataToAppleButtonScripts {
); );
} }
/** /**
* Returns the appropriate admin data to send to ApplePay script * Returns the appropriate admin data to send to ApplePay script
* *
* @return array * @return array
* @throws NotFoundException When the setting is not found. * @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(); $base_location = wc_get_base_location();
$shop_country_code = $base_location['country']; $shop_country_code = $base_location['country'];
$currency_code = get_woocommerce_currency(); $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 = $this->settings->has( 'applepay_button_language' ) ? $this->settings->get( 'applepay_button_language' ) : '';
$lang = apply_filters( 'woocommerce_paypal_payments_applepay_button_language', $lang ); $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; $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( return array(
'sdk_url' => $this->sdk_url, '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_admin' => true,
'is_enabled' => $is_enabled,
'preferences' => array( 'preferences' => array(
'checkout_data_mode' => $checkout_data_mode, 'checkout_data_mode' => $checkout_data_mode,
), ),

View file

@ -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(`<div id='${previewId}' class='${previewClass}'>`);
}
/**
* 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;

View file

@ -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<void>|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<void>}
*/
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;

View file

@ -20,6 +20,9 @@ return array(
'wcgateway.settings.fields' => function ( ContainerInterface $container, array $fields ): 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. // Eligibility check.
if ( ! $container->has( 'googlepay.eligible' ) || ! $container->get( 'googlepay.eligible' ) ) { if ( ! $container->has( 'googlepay.eligible' ) || ! $container->get( 'googlepay.eligible' ) ) {
return $fields; return $fields;
@ -133,7 +136,7 @@ return array(
'gateway' => 'dcc', 'gateway' => 'dcc',
'requirements' => array(), 'requirements' => array(),
'custom_attributes' => array( 'custom_attributes' => array(
'data-ppcp-display' => wp_json_encode( 'data-ppcp-display' => wp_json_encode(
array( array(
$display_manager $display_manager
->rule() ->rule()
@ -142,64 +145,79 @@ return array(
->action_visible( 'googlepay_button_color' ) ->action_visible( 'googlepay_button_color' )
->action_visible( 'googlepay_button_language' ) ->action_visible( 'googlepay_button_language' )
->action_visible( 'googlepay_button_shipping_enabled' ) ->action_visible( 'googlepay_button_shipping_enabled' )
->action_visible( 'googlepay_button_preview' )
->action_class( 'googlepay_button_enabled', 'active' ) ->action_class( 'googlepay_button_enabled', 'active' )
->to_array(), ->to_array(),
) )
), ),
'data-ppcp-apm-name' => $apm_name,
'data-ppcp-field-name' => 'is_enabled',
), ),
'classes' => array( 'ppcp-valign-label-middle', 'ppcp-align-label-center' ), 'classes' => array( 'ppcp-valign-label-middle', 'ppcp-align-label-center' ),
), ),
'googlepay_button_type' => array( 'googlepay_button_type' => array(
'title' => __( 'Button Label', 'woocommerce-paypal-payments' ), 'title' => __( 'Button Label', 'woocommerce-paypal-payments' ),
'type' => 'select', 'type' => 'select',
'desc_tip' => true, 'desc_tip' => true,
'description' => __( 'description' => __(
'This controls the label of the Google Pay button.', 'This controls the label of the Google Pay button.',
'woocommerce-paypal-payments' 'woocommerce-paypal-payments'
), ),
'classes' => array( 'ppcp-field-indent' ), 'classes' => array( 'ppcp-field-indent' ),
'class' => array(), 'class' => array(),
'input_class' => array( 'wc-enhanced-select' ), 'input_class' => array( 'wc-enhanced-select' ),
'default' => 'pay', 'default' => 'pay',
'options' => PropertiesDictionary::button_types(), 'options' => PropertiesDictionary::button_types(),
'screens' => array( State::STATE_ONBOARDED ), 'screens' => array( State::STATE_ONBOARDED ),
'gateway' => 'dcc', 'gateway' => 'dcc',
'requirements' => array(), 'requirements' => array(),
'custom_attributes' => array(
'data-ppcp-apm-name' => $apm_name,
'data-ppcp-field-name' => 'type',
),
), ),
'googlepay_button_color' => array( 'googlepay_button_color' => array(
'title' => __( 'Button Color', 'woocommerce-paypal-payments' ), 'title' => __( 'Button Color', 'woocommerce-paypal-payments' ),
'type' => 'select', 'type' => 'select',
'desc_tip' => true, 'desc_tip' => true,
'description' => __( '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.', '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' 'woocommerce-paypal-payments'
), ),
'label' => '', 'label' => '',
'input_class' => array( 'wc-enhanced-select' ), 'input_class' => array( 'wc-enhanced-select' ),
'classes' => array( 'ppcp-field-indent' ), 'classes' => array( 'ppcp-field-indent' ),
'class' => array(), 'class' => array(),
'default' => 'black', 'default' => 'black',
'options' => PropertiesDictionary::button_colors(), 'options' => PropertiesDictionary::button_colors(),
'screens' => array( State::STATE_ONBOARDED ), 'screens' => array( State::STATE_ONBOARDED ),
'gateway' => 'dcc', 'gateway' => 'dcc',
'requirements' => array(), 'requirements' => array(),
'custom_attributes' => array(
'data-ppcp-apm-name' => $apm_name,
'data-ppcp-field-name' => 'color',
),
), ),
'googlepay_button_language' => array( 'googlepay_button_language' => array(
'title' => __( 'Button Language', 'woocommerce-paypal-payments' ), 'title' => __( 'Button Language', 'woocommerce-paypal-payments' ),
'type' => 'select', 'type' => 'select',
'desc_tip' => true, 'desc_tip' => true,
'description' => __( '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.', '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' 'woocommerce-paypal-payments'
), ),
'classes' => array( 'ppcp-field-indent' ), 'classes' => array( 'ppcp-field-indent' ),
'class' => array(), 'class' => array(),
'input_class' => array( 'wc-enhanced-select' ), 'input_class' => array( 'wc-enhanced-select' ),
'default' => 'en', 'default' => 'en',
'options' => PropertiesDictionary::button_languages(), 'options' => PropertiesDictionary::button_languages(),
'screens' => array( State::STATE_ONBOARDED ), 'screens' => array( State::STATE_ONBOARDED ),
'gateway' => 'dcc', 'gateway' => 'dcc',
'requirements' => array(), 'requirements' => array(),
'custom_attributes' => array(
'data-ppcp-apm-name' => $apm_name,
'data-ppcp-field-name' => 'language',
),
), ),
'googlepay_button_shipping_enabled' => array( 'googlepay_button_shipping_enabled' => array(
'title' => __( 'Shipping Callback', 'woocommerce-paypal-payments' ), '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.', 'Synchronizes your available shipping options with Google Pay. Enabling this may impact the buyer experience.',
'woocommerce-paypal-payments' 'woocommerce-paypal-payments'
), ),
'classes' => array( 'ppcp-field-indent' ), 'classes' => array( 'ppcp-field-indent ppcp' ),
'label' => __( 'Enable Google Pay shipping callback', 'woocommerce-paypal-payments' ), 'label' => __( 'Enable Google Pay shipping callback', 'woocommerce-paypal-payments' ),
'default' => 'no', 'default' => 'no',
'screens' => array( State::STATE_ONBOARDED ), 'screens' => array( State::STATE_ONBOARDED ),
'gateway' => 'dcc', 'gateway' => 'dcc',
'requirements' => array(), 'requirements' => array(),
), ),
'googlepay_button_preview' => array(
'type' => 'ppcp-text',
'text' => sprintf(
'
<div class="ppcp-preview ppcp-button-preview" data-ppcp-apm-preview="%1$s">
<h4>' . __( 'Button Styling Preview', 'woocommerce-paypal-payments' ) . '</h4>
<div id="ppcp%1$sButtonPreview" class="ppcp-button-preview-inner"></div>
</div>',
$apm_name
),
'screens' => array(
State::STATE_ONBOARDED,
),
'requirements' => array(),
'gateway' => 'dcc',
),
) )
); );
}, },

View file

@ -139,6 +139,10 @@ class GooglepayButton {
initEventHandlers() { initEventHandlers() {
const { wrapper, ppcpButtonWrapper } = this.contextConfig(); 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 syncButtonVisibility = () => {
const $ppcpButtonWrapper = jQuery(ppcpButtonWrapper); const $ppcpButtonWrapper = jQuery(ppcpButtonWrapper);
setVisible(wrapper, $ppcpButtonWrapper.is(':visible')); setVisible(wrapper, $ppcpButtonWrapper.is(':visible'));

View file

@ -1,146 +1,108 @@
import {loadCustomScript} from "@paypal/paypal-js"; import GooglepayButton from './GooglepayButton';
import GooglepayButton from "./GooglepayButton"; import PreviewButton from '../../../ppcp-button/resources/js/modules/Renderer/PreviewButton';
import widgetBuilder from "../../../ppcp-button/resources/js/modules/Renderer/WidgetBuilder"; import PreviewButtonManager from '../../../ppcp-button/resources/js/modules/Renderer/PreviewButtonManager';
(function ({ /**
buttonConfig, * Accessor that creates and returns a single PreviewButtonManager instance.
jQuery */
}) { const buttonManager = () => {
if (!GooglePayPreviewButtonManager.instance) {
let googlePayConfig; GooglePayPreviewButtonManager.instance = new GooglePayPreviewButtonManager();
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();
} }
const createButton = function (ppcpConfig) { return GooglePayPreviewButtonManager.instance;
const selector = ppcpConfig.button.wrapper + 'GooglePay'; };
if (!jQuery('#ppcp-googlepay_button_enabled').is(':checked')) {
jQuery(selector).remove();
return;
}
buttonConfig = JSON.parse(JSON.stringify(buttonConfig)); /**
buttonConfig.button.wrapper = selector; * Manages all GooglePay preview buttons on this page.
applyConfigOptions(buttonConfig); */
class GooglePayPreviewButtonManager extends PreviewButtonManager {
constructor() {
const args = {
methodName: 'GooglePay',
buttonConfig: window.wc_ppcp_googlepay_admin,
};
const wrapperElement = `<div id="${selector.replace('#', '')}" class="ppcp-button-apm ppcp-button-googlepay"></div>`; super(args);
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;
} }
const bootstrap = async function () { /**
if (!widgetBuilder.paypal) { * Responsible for fetching and returning the PayPal configuration object for this payment
return; * 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( // Initialize the preview button manager.
'DOMContentLoaded', buttonManager();
() => {
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
});

View file

@ -418,9 +418,12 @@ class Button implements ButtonInterface {
$shipping['countries'] = array_keys( $this->wc_countries()->get_shipping_countries() ); $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( return array(
'environment' => $this->environment->current_environment_is( Environment::SANDBOX ) ? 'TEST' : 'PRODUCTION', '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, 'sdk_url' => $this->sdk_url,
'button' => array( 'button' => array(
'wrapper' => '#ppc-button-googlepay-container', 'wrapper' => '#ppc-button-googlepay-container',

View file

@ -112,7 +112,7 @@ class GooglepayModule implements ModuleInterface {
add_action( add_action(
'admin_enqueue_scripts', 'admin_enqueue_scripts',
static function () use ( $c, $button ) { 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; return;
} }

View file

@ -2,10 +2,24 @@
width: 350px; width: 350px;
padding: 15px; padding: 15px;
border: 1px solid lightgray; border: 1px solid lightgray;
background: #eeeeef;
border-radius: 15px; border-radius: 15px;
box-shadow: 0 2px 10px 1px #ddd; box-shadow: 0 2px 10px 1px #ddd;
margin-right: -28px; 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 { h4 {
margin-top: 0; margin-top: 0;
} }

View file

@ -1,5 +1,6 @@
import { loadScript } from "@paypal/paypal-js"; import { loadScript } from "@paypal/paypal-js";
import {debounce} from "./helper/debounce"; import {debounce} from "./helper/debounce";
import { buttonRefreshTriggerFactory, buttonSettingsGetterFactory } from './helper/preview-button';
import Renderer from '../../../ppcp-button/resources/js/modules/Renderer/Renderer' import Renderer from '../../../ppcp-button/resources/js/modules/Renderer/Renderer'
import MessageRenderer from "../../../ppcp-button/resources/js/modules/Renderer/MessageRenderer"; import MessageRenderer from "../../../ppcp-button/resources/js/modules/Renderer/MessageRenderer";
import {setVisibleByClass, isVisible} from "../../../ppcp-button/resources/js/modules/Helper/Hiding"; 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 payLaterMessagingLocations = ['product', 'cart', 'checkout', 'shop', 'home', 'general'];
const paypalButtonLocations = ['product', 'cart', 'checkout', 'mini-cart', 'cart-block', 'checkout-block-express', 'general']; const paypalButtonLocations = ['product', 'cart', 'checkout', 'mini-cart', 'cart-block', 'checkout-block-express', 'general'];
// Default preview buttons; on "Standard Payments" tab.
paypalButtonLocations.forEach((location) => { paypalButtonLocations.forEach((location) => {
const inputNamePrefix = location === 'checkout' ? '#ppcp-button' : '#ppcp-button_' + location; const inputNamePrefix = location === 'checkout' ? '#ppcp-button' : '#ppcp-button_' + location;
const wrapperName = location.split('-').map(s => s.charAt(0).toUpperCase() + s.slice(1)).join(''); 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)); 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 <div> 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) => { payLaterMessagingLocations.forEach((location) => {
const inputNamePrefix = '#ppcp-pay_later_' + location + '_message'; const inputNamePrefix = '#ppcp-pay_later_' + location + '_message';
const wrapperName = location.charAt(0).toUpperCase() + location.slice(1); const wrapperName = location.charAt(0).toUpperCase() + location.slice(1);

View file

@ -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<string, {val:Function, el:HTMLInputElement}>}
*/
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 };
};
}

View file

@ -201,9 +201,23 @@ return array(
); );
}, },
'wcgateway.is-ppcp-settings-standard-payments-page' => static function ( ContainerInterface $container ): bool { // Checks, if the current admin page contains settings for this plugin's payment methods.
return $container->get( 'wcgateway.is-ppcp-settings-page' ) 'wcgateway.is-ppcp-settings-payment-methods-page' => static function ( ContainerInterface $container ) : bool {
&& $container->get( 'wcgateway.current-ppcp-settings-page-id' ) === PayPalGateway::ID; 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 { 'wcgateway.current-ppcp-settings-page-id' => static function ( ContainerInterface $container ): string {

View file

@ -112,6 +112,13 @@ class SettingsPageAssets {
*/ */
private $billing_agreements_endpoint; 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. * 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_settings_page Whether it's a settings page of this plugin.
* @param bool $is_acdc_enabled Whether the ACDC gateway is enabled. * @param bool $is_acdc_enabled Whether the ACDC gateway is enabled.
* @param BillingAgreementsEndpoint $billing_agreements_endpoint Billing Agreements endpoint. * @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( public function __construct(
string $module_url, string $module_url,
@ -142,21 +150,23 @@ class SettingsPageAssets {
array $all_funding_sources, array $all_funding_sources,
bool $is_settings_page, bool $is_settings_page,
bool $is_acdc_enabled, bool $is_acdc_enabled,
BillingAgreementsEndpoint $billing_agreements_endpoint BillingAgreementsEndpoint $billing_agreements_endpoint,
bool $is_paypal_payment_method_page
) { ) {
$this->module_url = $module_url; $this->module_url = $module_url;
$this->version = $version; $this->version = $version;
$this->subscription_helper = $subscription_helper; $this->subscription_helper = $subscription_helper;
$this->client_id = $client_id; $this->client_id = $client_id;
$this->currency = $currency; $this->currency = $currency;
$this->country = $country; $this->country = $country;
$this->environment = $environment; $this->environment = $environment;
$this->is_pay_later_button_enabled = $is_pay_later_button_enabled; $this->is_pay_later_button_enabled = $is_pay_later_button_enabled;
$this->disabled_sources = $disabled_sources; $this->disabled_sources = $disabled_sources;
$this->all_funding_sources = $all_funding_sources; $this->all_funding_sources = $all_funding_sources;
$this->is_settings_page = $is_settings_page; $this->is_settings_page = $is_settings_page;
$this->is_acdc_enabled = $is_acdc_enabled; $this->is_acdc_enabled = $is_acdc_enabled;
$this->billing_agreements_endpoint = $billing_agreements_endpoint; $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(); $this->register_admin_assets();
} }
if ( $this->is_paypal_payment_method_page() ) { if ( $this->is_paypal_payment_method_page ) {
$this->register_paypal_admin_assets(); $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. * Register assets for PayPal admin pages.
*/ */

View file

@ -183,7 +183,8 @@ class WCGatewayModule implements ModuleInterface {
$c->get( 'wcgateway.settings.funding-sources' ), $c->get( 'wcgateway.settings.funding-sources' ),
$c->get( 'wcgateway.is-ppcp-settings-page' ), $c->get( 'wcgateway.is-ppcp-settings-page' ),
$settings->has( 'dcc_enabled' ) && $settings->get( 'dcc_enabled' ), $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(); $assets->register_assets();
} }

View file

@ -33,7 +33,8 @@ class SettingsPagesAssetsTest extends TestCase
array(), array(),
true, true,
false, false,
$billingAgreementEndpoint $billingAgreementEndpoint,
true
); );
when('is_admin') when('is_admin')