mirror of
https://github.com/woocommerce/woocommerce-paypal-payments.git
synced 2025-09-05 08:59:14 +08:00
♻️ Create generic base classes for preview buttons
This commit is contained in:
parent
9342086f33
commit
ebb1f6962f
3 changed files with 343 additions and 241 deletions
|
@ -0,0 +1,106 @@
|
|||
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} configResponse - PayPal configuration object; retrieved via a
|
||||
* widgetBuilder API method
|
||||
* @param {object} defaultAttributes - Optional.
|
||||
*/
|
||||
constructor({selector, configResponse, defaultAttributes = {}}) {
|
||||
this.configResponse = configResponse;
|
||||
this.defaultAttributes = defaultAttributes;
|
||||
this.buttonConfig = {};
|
||||
this.ppcpConfig = {};
|
||||
|
||||
// Usually overwritten in constructor of derived class.
|
||||
this.selector = selector;
|
||||
|
||||
this.domWrapper = null;
|
||||
this.payButton = 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}">`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the internal button configuration. Does not trigger a redraw.
|
||||
*
|
||||
* @return {this} Reference to self, for chaining.
|
||||
*/
|
||||
config({buttonConfig, ppcpConfig}) {
|
||||
if (ppcpConfig) {
|
||||
this.ppcpConfig = merge({}, ppcpConfig);
|
||||
}
|
||||
|
||||
if (buttonConfig) {
|
||||
this.buttonConfig = merge(this.defaultAttributes, buttonConfig)
|
||||
this.buttonConfig.button.wrapper = this.selector
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Responsible for creating the actual payment button preview.
|
||||
* Called by the `render()` method, after the wrapper DOM element is ready.
|
||||
*
|
||||
* @return {any} Return value is assigned to `this.payButton`
|
||||
*/
|
||||
createButton() {
|
||||
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() {
|
||||
this.remove();
|
||||
|
||||
if (!this.buttonConfig?.button?.wrapper) {
|
||||
console.error('Skip render, button is not configured yet');
|
||||
return;
|
||||
}
|
||||
|
||||
this.isVisible = true;
|
||||
|
||||
const newDomWrapper = this.createNewWrapper();
|
||||
|
||||
if (this.domWrapper?.length) {
|
||||
this.domWrapper.replaceWith(newDomWrapper);
|
||||
} else {
|
||||
jQuery(this.ppcpConfig.button.wrapper).after(newDomWrapper);
|
||||
}
|
||||
this.domWrapper = newDomWrapper;
|
||||
|
||||
this.payButton = this.createButton();
|
||||
}
|
||||
|
||||
remove() {
|
||||
this.isVisible = false;
|
||||
|
||||
// The current payButtons have no remove/cleanup function.
|
||||
this.payButton = null;
|
||||
|
||||
if (this.domWrapper?.remove) {
|
||||
this.domWrapper.remove();
|
||||
}
|
||||
|
||||
this.domWrapper = null;
|
||||
}
|
||||
}
|
||||
|
||||
export default PreviewButton;
|
|
@ -0,0 +1,185 @@
|
|||
import {loadCustomScript} from "@paypal/paypal-js";
|
||||
import merge from "deepmerge";
|
||||
|
||||
/**
|
||||
* Manages all PreviewButton instances of a certain payment method on the page.
|
||||
*/
|
||||
class PreviewButtonManager {
|
||||
constructor({buttonConfig, widgetBuilder, defaultAttributes}) {
|
||||
// Define the payment method name in the derived class.
|
||||
this.methodName = 'UNDEFINED';
|
||||
|
||||
this.buttonConfig = buttonConfig;
|
||||
this.widgetBuilder = widgetBuilder;
|
||||
this.defaultAttributes = defaultAttributes;
|
||||
|
||||
this.isEnabled = true
|
||||
this.buttons = {};
|
||||
this.configResponse = null;
|
||||
|
||||
// Empty promise that resolves instantly when called.
|
||||
this.bootstrapping = Promise.resolve();
|
||||
|
||||
// Add the bootstrap logic to the Promise chain. More `then`s are added by addButton().
|
||||
this.bootstrapping = this.bootstrapping.then(() => this.bootstrap());
|
||||
|
||||
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.
|
||||
*
|
||||
* @return {Promise<{}>}
|
||||
*/
|
||||
async fetchConfig() {
|
||||
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}
|
||||
*/
|
||||
createButtonInst(wrapperId) {
|
||||
throw new Error('The "createButtonInst" method must be implemented by the derived class');
|
||||
}
|
||||
|
||||
registerEventListeners() {
|
||||
jQuery(document).on('ppcp_paypal_render_preview', (ev, ppcpConfig) => this.addButton(ppcpConfig));
|
||||
jQuery(document).on('DOMContentLoaded', () => this.bootstrapping);
|
||||
}
|
||||
|
||||
/**
|
||||
* Output an error message to the console, with a module-specific prefix.
|
||||
*/
|
||||
error(message, ...args) {
|
||||
console.error(`${this.methodName} ${message}`, ...args)
|
||||
}
|
||||
|
||||
/**
|
||||
* 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() {
|
||||
if (!this.buttonConfig || !this.widgetBuilder) {
|
||||
this.error('Button could not be configured.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Load the custom SDK script.
|
||||
const customScriptPromise = loadCustomScript({url: this.buttonConfig.sdk_url});
|
||||
|
||||
// Wait until PayPal is ready.
|
||||
const paypalPromise = new Promise(resolve => {
|
||||
if (this.widgetBuilder.paypal) {
|
||||
resolve();
|
||||
} else {
|
||||
jQuery(document).on('ppcp-paypal-loaded', resolve);
|
||||
}
|
||||
});
|
||||
|
||||
await Promise.all([customScriptPromise, paypalPromise]);
|
||||
|
||||
this.configResponse = await this.fetchConfig();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new preview button, that is rendered once the bootstrapping Promise resolves.
|
||||
*/
|
||||
addButton(ppcpConfig) {
|
||||
if (!ppcpConfig.button.wrapper) {
|
||||
this.error('Button did not provide a wrapper ID', ppcpConfig)
|
||||
return;
|
||||
}
|
||||
|
||||
const createOrUpdateButton = () => {
|
||||
const id = ppcpConfig.button.wrapper;
|
||||
|
||||
if (!this.buttons[id]) {
|
||||
this.buttons[id] = this.createButtonInst(id);
|
||||
}
|
||||
|
||||
this.buttons[id].config({
|
||||
buttonConfig: this.buttonConfig,
|
||||
ppcpConfig
|
||||
}).render()
|
||||
}
|
||||
|
||||
if (this.bootstrapping) {
|
||||
this.bootstrapping.then(createOrUpdateButton);
|
||||
} else {
|
||||
createOrUpdateButton();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Changes the button configuration and re-renders all buttons.
|
||||
*
|
||||
* @return {this} Reference to self, for chaining.
|
||||
*/
|
||||
updateConfig(newConfig) {
|
||||
if (!newConfig || 'object' !== typeof newConfig) {
|
||||
return this;
|
||||
}
|
||||
|
||||
this.buttonConfig = merge(this.buttonConfig, newConfig)
|
||||
|
||||
Object.values(this.buttons).forEach(button => button.config({buttonConfig: this.buttonConfig}))
|
||||
this.renderButtons();
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 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;
|
Loading…
Add table
Add a link
Reference in a new issue