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 }) { let googlePayConfig; let buttonQueue = []; let activeButtons = {}; let bootstrapped = false; // React to PayPal config changes. jQuery(document).on('ppcp_paypal_render_preview', (ev, ppcpConfig) => { if (bootstrapped) { createButton(ppcpConfig); } else { buttonQueue.push({ ppcpConfig: JSON.parse(JSON.stringify(ppcpConfig)) }); } }); // React to GooglePay config changes. jQuery([ '#ppcp-googlepay_button_enabled', '#ppcp-googlepay_button_type', '#ppcp-googlepay_button_color', '#ppcp-googlepay_button_language', '#ppcp-googlepay_button_shipping_enabled' ].join(',')).on('change', () => { for (const [selector, ppcpConfig] of Object.entries(activeButtons)) { createButton(ppcpConfig); } }); // Maybe we can find a more elegant reload method when transitioning from styling modes. jQuery([ '#ppcp-smart_button_enable_styling_per_location' ].join(',')).on('change', () => { setTimeout(() => { for (const [selector, ppcpConfig] of Object.entries(activeButtons)) { createButton(ppcpConfig); } }, 100); }); const shouldDisplayPreviewButton = function() { // TODO - original condition, which is wrong. return jQuery('#ppcp-googlepay_button_enabled').is(':checked'); } const applyConfigOptions = function (buttonConfig) { buttonConfig.button = buttonConfig.button || {}; buttonConfig.button.style = buttonConfig.button.style || {}; buttonConfig.button.style.type = jQuery('#ppcp-googlepay_button_type').val(); buttonConfig.button.style.color = jQuery('#ppcp-googlepay_button_color').val(); buttonConfig.button.style.language = jQuery('#ppcp-googlepay_button_language').val(); } const createButton = function (ppcpConfig) { const selector = ppcpConfig.button.wrapper + 'GooglePay'; if (!shouldDisplayPreviewButton()) { jQuery(selector).remove(); return; } buttonConfig = JSON.parse(JSON.stringify(buttonConfig)); buttonConfig.button.wrapper = selector; applyConfigOptions(buttonConfig); const wrapperElement = `
`; if (!jQuery(selector).length) { jQuery(ppcpConfig.button.wrapper).after(wrapperElement); } else { jQuery(selector).replaceWith(wrapperElement); } const button = new GooglepayButton( 'preview', null, buttonConfig, ppcpConfig, ); button.init(googlePayConfig); activeButtons[selector] = ppcpConfig; } const bootstrap = async function () { if (!widgetBuilder.paypal) { return; } googlePayConfig = await widgetBuilder.paypal.Googlepay().config(); // We need to set bootstrapped here otherwise googlePayConfig may not be set. bootstrapped = true; let options; while (options = buttonQueue.pop()) { createButton(options.ppcpConfig); } }; document.addEventListener( 'DOMContentLoaded', () => { if (typeof (buttonConfig) === 'undefined') { console.error('PayPal button could not be configured.'); return; } let paypalLoaded = false; let googlePayLoaded = false; const tryToBoot = () => { if (!bootstrapped && paypalLoaded && googlePayLoaded) { bootstrap(); } } // Load GooglePay SDK loadCustomScript({ url: buttonConfig.sdk_url }).then(() => { googlePayLoaded = true; tryToBoot(); }); // Wait for PayPal to be loaded externally if (typeof widgetBuilder.paypal !== 'undefined') { paypalLoaded = true; tryToBoot(); } jQuery(document).on('ppcp-paypal-loaded', () => { paypalLoaded = true; tryToBoot(); }); }, ); })({ buttonConfig: window.wc_ppcp_googlepay_admin, jQuery: window.jQuery }); // */