♻️ Refactor Google Pay preview button

Mainly, separate the preview code into three main parts:
- Configuration
- Button
- ButtonManager

Remove dependency from DOM structure
This commit is contained in:
Philipp Stracker 2024-06-04 22:10:37 +02:00
parent d1a93a1b55
commit 9342086f33
No known key found for this signature in database
3 changed files with 329 additions and 6 deletions

View file

@ -1,7 +1,327 @@
import {loadCustomScript} from "@paypal/paypal-js";
import GooglepayButton from "./GooglepayButton";
import merge from "deepmerge";
import widgetBuilder from "../../../ppcp-button/resources/js/modules/Renderer/WidgetBuilder";
/**
* Button manager instance; we usually only need a single instance of this object.
*
* @see buttonManager()
*/
let managerInstance = null;
/**
* Default attributes for new buttons.
*/
const defaultAttributes = {
button: {
style: {
type: 'pay',
color: 'black',
language: 'en'
}
}
};
/**
* Accessor that creates and returns a single PreviewButtonManager instance.
*/
const buttonManager = () => {
if (!managerInstance) {
managerInstance = new PreviewButtonManager({
// Internal, for logging.
name: 'GooglePay',
// WooCommerce configuration object.
buttonConfig: window.wc_ppcp_googlepay_admin,
// Internal widgetBuilder instance.
widgetBuilder,
// Default button styles.
defaultAttributes,
// Returns an async function that fetches the button configuration.
cbFetchConfig: () => widgetBuilder?.paypal?.Googlepay()?.config,
// Returns the CSS selector to render the given button.
cbInitButton: (button) => {
button.selector = `${button.wrapper}GooglePay`
button.customClasses = 'ppcp-button-googlepay'
}
});
}
return managerInstance;
}
// ----------------------------------------------------------------------------
/**
* Returns a new object which contains a copy of all public properties of the
* input object.
*/
const cloneObject = obj => JSON.parse(JSON.stringify(obj));
/**
* Convenience function to attach an event handler to the document node.
* The callback handler will not receive the first argument (event), as this
* argument is not used by our module.
*/
const onDocumentEvent = (name, handler) => jQuery(document).on(name, (ev, ...args) => handler(...args));
// ----------------------------------------------------------------------------
/**
* A single GooglePay preview button instance.
*/
class PreviewButton {
constructor({wrapperSelector, configResponse, defaultAttributes, cbInitButton}) {
// Do not clone this object. It's generated by an API and might contain methods.
this.configResponse = configResponse;
this.defaultAttributes = cloneObject(defaultAttributes);
this.buttonConfig = {};
this.ppcpConfig = {};
this.wrapper = wrapperSelector;
this.selector = wrapperSelector + 'Button';
this.customClasses = '';
cbInitButton(this);
this.domWrapper = jQuery(this.selector);
this.payButton = null;
}
createNewWrapper() {
const previewId = this.selector.replace('#', '')
const previewClass = `ppcp-button-apm ${this.customClasses}`;
return jQuery(`<div id="${previewId}" class="${previewClass}">`)
}
config(buttonConfig, ppcpConfig) {
if (ppcpConfig) {
this.ppcpConfig = cloneObject(ppcpConfig);
}
if (buttonConfig) {
this.buttonConfig = merge(this.defaultAttributes, buttonConfig)
this.buttonConfig.button.wrapper = this.selector
}
return this;
}
render() {
this.remove();
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 = new GooglepayButton(
'preview',
null,
this.buttonConfig,
this.ppcpConfig,
);
this.payButton.init(this.configResponse);
return this;
}
remove() {
// The payButton has no remove/cleanup function.
this.payButton = null;
if (this.domWrapper) {
this.domWrapper.remove();
this.domWrapper = null;
}
}
}
// ----------------------------------------------------------------------------
/**
* Manages all GooglePay preview buttons on this page.
*/
class PreviewButtonManager {
constructor({
name,
buttonConfig,
widgetBuilder,
defaultAttributes,
cbFetchConfig,
cbInitButton
}) {
this.name = name;
this.buttonConfig = buttonConfig;
this.widgetBuilder = widgetBuilder;
this.defaultAttributes = defaultAttributes;
this.cbInitButton = cbInitButton;
this.cbFetchConfig = cbFetchConfig;
this.state = 'enabled'
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 createButton().
this.bootstrapping = this.bootstrapping.then(() => this.bootstrap());
this.registerEventListeners();
}
registerEventListeners() {
onDocumentEvent('ppcp_paypal_render_preview', this.createButton.bind(this));
onDocumentEvent('DOMContentLoaded', () => this.bootstrapping);
}
/**
* Output an error message to the console, with a module-specific prefix.
*/
error(message, ...args) {
console.error(`${this.name} ${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 {
onDocumentEvent('ppcp-paypal-loaded', resolve);
}
});
await Promise.all([customScriptPromise, paypalPromise]);
const fetchConfig = this.cbFetchConfig()
if (!fetchConfig) {
this.error('Button could not be initialized');
return;
}
this.configResponse = await fetchConfig();
}
/**
* Creates a new preview button, that is rendered once the bootstrapping Promise resolves.
*/
createButton(ppcpConfig) {
if (!ppcpConfig.button.wrapper) {
this.error('Button did not provide a selector', ppcpConfig)
return;
}
const wrapper = ppcpConfig.button.wrapper;
const createButtonInst = () => {
if (!this.buttons[wrapper]) {
this.buttons[wrapper] = new PreviewButton({
wrapperSelector: wrapper,
configResponse: this.configResponse,
defaultAttributes: this.defaultAttributes,
cbInitButton: this.cbInitButton
});
}
this.buttons[wrapper].config(
this.buttonConfig,
ppcpConfig
).render()
}
if (this.bootstrapping) {
this.bootstrapping.then(createButtonInst);
} else {
createButtonInst();
}
}
/**
* Changes the button configuration, and re-renders all buttons.
*/
updateConfig(newConfig) {
if (!newConfig || 'object' !== typeof newConfig) {
return;
}
this.buttonConfig = merge(this.buttonConfig, newConfig)
Object.values(this.buttons).forEach(button => button.config(this.buttonConfig))
this.renderButtons()
}
/**
* Refreshes all buttons using the latest buttonConfig.
*/
renderButtons() {
if ('enabled' === this.state) {
Object.values(this.buttons).forEach(button => button.render())
} else {
Object.values(this.buttons).forEach(button => button.remove())
}
}
/**
* Enables this payment method, which re-creates or refreshes all buttons.
*/
enable() {
if ('enabled' === this.state) {
return;
}
this.state = 'enabled';
this.renderButtons()
}
/**
* Disables this payment method, effectively removing all preview buttons.
*/
disable() {
if ('disabled' === this.state) {
return;
}
this.state = 'disabled';
this.renderButtons()
}
}
// Initialize the preview button manager.
buttonManager().enable()
// todo - Expose button manager for testing. Remove this!
window.gpay = buttonManager()
/**
(function ({
buttonConfig,
jQuery
@ -47,11 +367,6 @@ import widgetBuilder from "../../../ppcp-button/resources/js/modules/Renderer/Wi
}, 100);
});
/**
* Decides, whether to display the Google Pay preview button.
*
* @return {boolean}
*/
const shouldDisplayPreviewButton = function() {
// TODO - original condition, which is wrong.
return jQuery('#ppcp-googlepay_button_enabled').is(':checked');
@ -154,3 +469,5 @@ import widgetBuilder from "../../../ppcp-button/resources/js/modules/Renderer/Wi
buttonConfig: window.wc_ppcp_googlepay_admin,
jQuery: window.jQuery
});
// */