From 9342086f337e5b709de5710eb61e68969a5403b1 Mon Sep 17 00:00:00 2001
From: Philipp Stracker
Date: Tue, 4 Jun 2024 22:10:37 +0200
Subject: [PATCH] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Refactor=20Google=20Pay=20?=
=?UTF-8?q?preview=20button?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Mainly, separate the preview code into three main parts:
- Configuration
- Button
- ButtonManager
Remove dependency from DOM structure
---
modules/ppcp-googlepay/package.json | 3 +-
.../ppcp-googlepay/resources/js/boot-admin.js | 327 +++++++++++++++++-
modules/ppcp-googlepay/yarn.lock | 5 +
3 files changed, 329 insertions(+), 6 deletions(-)
diff --git a/modules/ppcp-googlepay/package.json b/modules/ppcp-googlepay/package.json
index c5b05bade..362598490 100644
--- a/modules/ppcp-googlepay/package.json
+++ b/modules/ppcp-googlepay/package.json
@@ -11,7 +11,8 @@
],
"dependencies": {
"@paypal/paypal-js": "^6.0.0",
- "core-js": "^3.25.0"
+ "core-js": "^3.25.0",
+ "deepmerge": "^4.3.1"
},
"devDependencies": {
"@babel/core": "^7.19",
diff --git a/modules/ppcp-googlepay/resources/js/boot-admin.js b/modules/ppcp-googlepay/resources/js/boot-admin.js
index bb5457ec8..e45cbfaf1 100644
--- a/modules/ppcp-googlepay/resources/js/boot-admin.js
+++ b/modules/ppcp-googlepay/resources/js/boot-admin.js
@@ -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(``)
+ }
+
+ 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}
+ */
+ 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
});
+
+// */
diff --git a/modules/ppcp-googlepay/yarn.lock b/modules/ppcp-googlepay/yarn.lock
index ec5854d6c..c239214a9 100644
--- a/modules/ppcp-googlepay/yarn.lock
+++ b/modules/ppcp-googlepay/yarn.lock
@@ -1453,6 +1453,11 @@ debug@^4.1.0, debug@^4.1.1:
dependencies:
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:
version "1.4.496"
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.496.tgz#a57534b70d2bdee7e1ad7dbd4c91e560cbd08db1"