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:
Philipp Stracker 2024-06-07 21:14:12 +02:00
parent 93f3e4e7ac
commit 6f73e82d3e
No known key found for this signature in database
4 changed files with 131 additions and 100 deletions

View file

@ -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;
}
}
}

View file

@ -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() {

View file

@ -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.
*

View file

@ -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);
}
}
}