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