mirror of
https://github.com/woocommerce/woocommerce-paypal-payments.git
synced 2025-09-05 08:59:14 +08:00
♻️ 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:
parent
d1a93a1b55
commit
9342086f33
3 changed files with 329 additions and 6 deletions
|
@ -11,7 +11,8 @@
|
||||||
],
|
],
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@paypal/paypal-js": "^6.0.0",
|
"@paypal/paypal-js": "^6.0.0",
|
||||||
"core-js": "^3.25.0"
|
"core-js": "^3.25.0",
|
||||||
|
"deepmerge": "^4.3.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.19",
|
"@babel/core": "^7.19",
|
||||||
|
|
|
@ -1,7 +1,327 @@
|
||||||
import {loadCustomScript} from "@paypal/paypal-js";
|
import {loadCustomScript} from "@paypal/paypal-js";
|
||||||
import GooglepayButton from "./GooglepayButton";
|
import GooglepayButton from "./GooglepayButton";
|
||||||
|
import merge from "deepmerge";
|
||||||
import widgetBuilder from "../../../ppcp-button/resources/js/modules/Renderer/WidgetBuilder";
|
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 ({
|
(function ({
|
||||||
buttonConfig,
|
buttonConfig,
|
||||||
jQuery
|
jQuery
|
||||||
|
@ -47,11 +367,6 @@ import widgetBuilder from "../../../ppcp-button/resources/js/modules/Renderer/Wi
|
||||||
}, 100);
|
}, 100);
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
|
||||||
* Decides, whether to display the Google Pay preview button.
|
|
||||||
*
|
|
||||||
* @return {boolean}
|
|
||||||
*/
|
|
||||||
const shouldDisplayPreviewButton = function() {
|
const shouldDisplayPreviewButton = function() {
|
||||||
// TODO - original condition, which is wrong.
|
// TODO - original condition, which is wrong.
|
||||||
return jQuery('#ppcp-googlepay_button_enabled').is(':checked');
|
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,
|
buttonConfig: window.wc_ppcp_googlepay_admin,
|
||||||
jQuery: window.jQuery
|
jQuery: window.jQuery
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// */
|
||||||
|
|
|
@ -1453,6 +1453,11 @@ debug@^4.1.0, debug@^4.1.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
ms "2.1.2"
|
ms "2.1.2"
|
||||||
|
|
||||||
|
deepmerge@^4.3.1:
|
||||||
|
version "4.3.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.3.1.tgz#44b5f2147cd3b00d4b56137685966f26fd25dd4a"
|
||||||
|
integrity sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==
|
||||||
|
|
||||||
electron-to-chromium@^1.4.477:
|
electron-to-chromium@^1.4.477:
|
||||||
version "1.4.496"
|
version "1.4.496"
|
||||||
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.496.tgz#a57534b70d2bdee7e1ad7dbd4c91e560cbd08db1"
|
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.496.tgz#a57534b70d2bdee7e1ad7dbd4c91e560cbd08db1"
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue