diff --git a/modules/ppcp-blocks/resources/js/checkout-block.js b/modules/ppcp-blocks/resources/js/checkout-block.js index bec1827c3..c8441c624 100644 --- a/modules/ppcp-blocks/resources/js/checkout-block.js +++ b/modules/ppcp-blocks/resources/js/checkout-block.js @@ -2,6 +2,7 @@ import {useEffect, useState} from '@wordpress/element'; import {registerExpressPaymentMethod, registerPaymentMethod} from '@woocommerce/blocks-registry'; import {paypalAddressToWc, paypalOrderToWcAddresses} from "./Helper/Address"; import {loadPaypalScript} from '../../../ppcp-button/resources/js/modules/Helper/ScriptLoading' +import buttonModuleWatcher from "../../../ppcp-button/resources/js/modules/ButtonModuleWatcher"; const config = wc.wcSettings.getSetting('ppcp-gateway_data'); @@ -28,6 +29,7 @@ const PayPalComponent = ({ if (!loaded) { loadPaypalScript(config.scriptData, () => { setLoaded(true); + buttonModuleWatcher.registerContextBootstrap(config.scriptData.context, this); }); } }, [loaded]); diff --git a/modules/ppcp-button/resources/js/button.js b/modules/ppcp-button/resources/js/button.js index 8b63b26bb..e93232e2d 100644 --- a/modules/ppcp-button/resources/js/button.js +++ b/modules/ppcp-button/resources/js/button.js @@ -19,6 +19,7 @@ import FreeTrialHandler from "./modules/ActionHandler/FreeTrialHandler"; import FormSaver from './modules/Helper/FormSaver'; import FormValidator from "./modules/Helper/FormValidator"; import {loadPaypalScript} from "./modules/Helper/ScriptLoading"; +import buttonModuleWatcher from "./modules/ButtonModuleWatcher"; // TODO: could be a good idea to have a separate spinner for each gateway, // but I think we care mainly about the script loading, so one spinner should be enough. @@ -60,6 +61,11 @@ const bootstrap = () => { } }); + const hasMessages = () => { + return PayPalCommerceGateway.messages.is_hidden === false + && document.querySelector(PayPalCommerceGateway.messages.wrapper); + } + const onSmartButtonClick = async (data, actions) => { window.ppcpFundingSource = data.fundingSource; const requiredFields = jQuery('form.woocommerce-checkout .validate-required:visible :input'); @@ -146,6 +152,7 @@ const bootstrap = () => { ); miniCartBootstrap.init(); + buttonModuleWatcher.registerContextBootstrap('mini-cart', miniCartBootstrap); } if ( @@ -163,6 +170,7 @@ const bootstrap = () => { ); singleProductBootstrap.init(); + buttonModuleWatcher.registerContextBootstrap('product', singleProductBootstrap); } if (context === 'cart') { @@ -174,6 +182,7 @@ const bootstrap = () => { ); cartBootstrap.init(); + buttonModuleWatcher.registerContextBootstrap('cart', cartBootstrap); } if (context === 'checkout') { @@ -186,6 +195,7 @@ const bootstrap = () => { ); checkoutBootstap.init(); + buttonModuleWatcher.registerContextBootstrap('checkout', checkoutBootstap); } if (context === 'pay-now' ) { @@ -197,15 +207,11 @@ const bootstrap = () => { errorHandler, ); payNowBootstrap.init(); + buttonModuleWatcher.registerContextBootstrap('pay-now', payNowBootstrap); } }; -const hasMessages = () => { - return PayPalCommerceGateway.messages.is_hidden === false - && document.querySelector(PayPalCommerceGateway.messages.wrapper); -} - document.addEventListener( 'DOMContentLoaded', () => { diff --git a/modules/ppcp-button/resources/js/modules/ButtonModuleWatcher.js b/modules/ppcp-button/resources/js/modules/ButtonModuleWatcher.js new file mode 100644 index 000000000..3fccca178 --- /dev/null +++ b/modules/ppcp-button/resources/js/modules/ButtonModuleWatcher.js @@ -0,0 +1,31 @@ + +class ButtonModuleWatcher { + + constructor() { + this.contextBootstrapRegistry = {}; + this.contextBootstrapWatchers = []; + } + + watchContextBootstrap(callable) { + this.contextBootstrapWatchers.push(callable); + Object.values(this.contextBootstrapRegistry).forEach(callable); + } + + registerContextBootstrap(context, handler) { + this.contextBootstrapRegistry[context] = { + context: context, + handler: handler + } + + // Call registered watchers + for (const callable of this.contextBootstrapWatchers) { + callable(this.contextBootstrapRegistry[context]); + } + } + +} + +window.ppcpResources = window.ppcpResources || {}; +const buttonModuleWatcher = window.ppcpResources['ButtonModuleWatcher'] = window.ppcpResources['ButtonModuleWatcher'] || new ButtonModuleWatcher(); + +export default buttonModuleWatcher; diff --git a/modules/ppcp-googlepay/resources/js/Context/CartBlockHandler.js b/modules/ppcp-googlepay/resources/js/Context/CartBlockHandler.js new file mode 100644 index 000000000..dfbd711dc --- /dev/null +++ b/modules/ppcp-googlepay/resources/js/Context/CartBlockHandler.js @@ -0,0 +1,75 @@ +import ErrorHandler from "../../../../ppcp-button/resources/js/modules/ErrorHandler"; +import CartActionHandler + from "../../../../ppcp-button/resources/js/modules/ActionHandler/CartActionHandler"; +import onApprove + from "../../../../ppcp-button/resources/js/modules/OnApproveHandler/onApproveForContinue"; + +class CartHandler { + + constructor(buttonConfig, ppcpConfig) { + console.log('NEW CartHandler'); + + this.buttonConfig = buttonConfig; + this.ppcpConfig = ppcpConfig; + } + + transactionInfo() { + return new Promise((resolve, reject) => { + + fetch( + this.ppcpConfig.ajax.cart_script_params.endpoint, + { + method: 'GET', + credentials: 'same-origin', + } + ) + .then(result => result.json()) + .then(result => { + if (! result.success) { + return; + } + + // handle script reload + const data = result.data; + + resolve({ + countryCode: 'US', // data.country_code, + currencyCode: 'USD', // data.currency_code, + totalPriceStatus: 'FINAL', + totalPrice: data.amount // Your amount + }); + + }); + }); + } + + createOrder() { + const errorHandler = new ErrorHandler( + this.ppcpConfig.labels.error.generic, + document.querySelector('.woocommerce-notices-wrapper') + ); + + const actionHandler = new CartActionHandler( + this.ppcpConfig, + errorHandler, + ); + + return actionHandler.configuration().createOrder(null, null); + } + + approveOrderForContinue(data, actions) { + const errorHandler = new ErrorHandler( + this.ppcpConfig.labels.error.generic, + document.querySelector('.woocommerce-notices-wrapper') + ); + + let onApproveHandler = onApprove({ + config: this.ppcpConfig + }, errorHandler); + + return onApproveHandler(data, actions); + } + +} + +export default CartHandler; diff --git a/modules/ppcp-googlepay/resources/js/Context/CartHandler.js b/modules/ppcp-googlepay/resources/js/Context/CartHandler.js new file mode 100644 index 000000000..dfbd711dc --- /dev/null +++ b/modules/ppcp-googlepay/resources/js/Context/CartHandler.js @@ -0,0 +1,75 @@ +import ErrorHandler from "../../../../ppcp-button/resources/js/modules/ErrorHandler"; +import CartActionHandler + from "../../../../ppcp-button/resources/js/modules/ActionHandler/CartActionHandler"; +import onApprove + from "../../../../ppcp-button/resources/js/modules/OnApproveHandler/onApproveForContinue"; + +class CartHandler { + + constructor(buttonConfig, ppcpConfig) { + console.log('NEW CartHandler'); + + this.buttonConfig = buttonConfig; + this.ppcpConfig = ppcpConfig; + } + + transactionInfo() { + return new Promise((resolve, reject) => { + + fetch( + this.ppcpConfig.ajax.cart_script_params.endpoint, + { + method: 'GET', + credentials: 'same-origin', + } + ) + .then(result => result.json()) + .then(result => { + if (! result.success) { + return; + } + + // handle script reload + const data = result.data; + + resolve({ + countryCode: 'US', // data.country_code, + currencyCode: 'USD', // data.currency_code, + totalPriceStatus: 'FINAL', + totalPrice: data.amount // Your amount + }); + + }); + }); + } + + createOrder() { + const errorHandler = new ErrorHandler( + this.ppcpConfig.labels.error.generic, + document.querySelector('.woocommerce-notices-wrapper') + ); + + const actionHandler = new CartActionHandler( + this.ppcpConfig, + errorHandler, + ); + + return actionHandler.configuration().createOrder(null, null); + } + + approveOrderForContinue(data, actions) { + const errorHandler = new ErrorHandler( + this.ppcpConfig.labels.error.generic, + document.querySelector('.woocommerce-notices-wrapper') + ); + + let onApproveHandler = onApprove({ + config: this.ppcpConfig + }, errorHandler); + + return onApproveHandler(data, actions); + } + +} + +export default CartHandler; diff --git a/modules/ppcp-googlepay/resources/js/Context/CheckoutBlockHandler.js b/modules/ppcp-googlepay/resources/js/Context/CheckoutBlockHandler.js new file mode 100644 index 000000000..dfbd711dc --- /dev/null +++ b/modules/ppcp-googlepay/resources/js/Context/CheckoutBlockHandler.js @@ -0,0 +1,75 @@ +import ErrorHandler from "../../../../ppcp-button/resources/js/modules/ErrorHandler"; +import CartActionHandler + from "../../../../ppcp-button/resources/js/modules/ActionHandler/CartActionHandler"; +import onApprove + from "../../../../ppcp-button/resources/js/modules/OnApproveHandler/onApproveForContinue"; + +class CartHandler { + + constructor(buttonConfig, ppcpConfig) { + console.log('NEW CartHandler'); + + this.buttonConfig = buttonConfig; + this.ppcpConfig = ppcpConfig; + } + + transactionInfo() { + return new Promise((resolve, reject) => { + + fetch( + this.ppcpConfig.ajax.cart_script_params.endpoint, + { + method: 'GET', + credentials: 'same-origin', + } + ) + .then(result => result.json()) + .then(result => { + if (! result.success) { + return; + } + + // handle script reload + const data = result.data; + + resolve({ + countryCode: 'US', // data.country_code, + currencyCode: 'USD', // data.currency_code, + totalPriceStatus: 'FINAL', + totalPrice: data.amount // Your amount + }); + + }); + }); + } + + createOrder() { + const errorHandler = new ErrorHandler( + this.ppcpConfig.labels.error.generic, + document.querySelector('.woocommerce-notices-wrapper') + ); + + const actionHandler = new CartActionHandler( + this.ppcpConfig, + errorHandler, + ); + + return actionHandler.configuration().createOrder(null, null); + } + + approveOrderForContinue(data, actions) { + const errorHandler = new ErrorHandler( + this.ppcpConfig.labels.error.generic, + document.querySelector('.woocommerce-notices-wrapper') + ); + + let onApproveHandler = onApprove({ + config: this.ppcpConfig + }, errorHandler); + + return onApproveHandler(data, actions); + } + +} + +export default CartHandler; diff --git a/modules/ppcp-googlepay/resources/js/Context/CheckoutHandler.js b/modules/ppcp-googlepay/resources/js/Context/CheckoutHandler.js new file mode 100644 index 000000000..34c44fd1f --- /dev/null +++ b/modules/ppcp-googlepay/resources/js/Context/CheckoutHandler.js @@ -0,0 +1,79 @@ +import Spinner from "../../../../ppcp-button/resources/js/modules/Helper/Spinner"; +import CheckoutActionHandler + from "../../../../ppcp-button/resources/js/modules/ActionHandler/CheckoutActionHandler"; +import ErrorHandler from "../../../../ppcp-button/resources/js/modules/ErrorHandler"; +import onApprove + from "../../../../ppcp-button/resources/js/modules/OnApproveHandler/onApproveForContinue"; + +class CheckoutHandler { + + constructor(buttonConfig, ppcpConfig) { + console.log('NEW CheckoutHandler'); + + this.buttonConfig = buttonConfig; + this.ppcpConfig = ppcpConfig; + } + + transactionInfo() { + return new Promise((resolve, reject) => { + + fetch( + this.ppcpConfig.ajax.cart_script_params.endpoint, + { + method: 'GET', + credentials: 'same-origin', + } + ) + .then(result => result.json()) + .then(result => { + if (! result.success) { + return; + } + + // handle script reload + const data = result.data; + + resolve({ + countryCode: 'US', // data.country_code, + currencyCode: 'USD', // data.currency_code, + totalPriceStatus: 'FINAL', + totalPrice: data.amount // Your amount + }); + + }); + }); + } + + createOrder() { + const errorHandler = new ErrorHandler( + this.ppcpConfig.labels.error.generic, + document.querySelector('.woocommerce-notices-wrapper') + ); + + const spinner = new Spinner(); + + const actionHandler = new CheckoutActionHandler( + this.ppcpConfig, + errorHandler, + spinner + ); + + return actionHandler.configuration().createOrder(null, null); + } + + approveOrderForContinue(data, actions) { + const errorHandler = new ErrorHandler( + this.ppcpConfig.labels.error.generic, + document.querySelector('.woocommerce-notices-wrapper') + ); + + let onApproveHandler = onApprove({ + config: this.ppcpConfig + }, errorHandler); + + return onApproveHandler(data, actions); + } + +} + +export default CheckoutHandler; diff --git a/modules/ppcp-googlepay/resources/js/Context/ContextHandlerFactory.js b/modules/ppcp-googlepay/resources/js/Context/ContextHandlerFactory.js new file mode 100644 index 000000000..04500f0d2 --- /dev/null +++ b/modules/ppcp-googlepay/resources/js/Context/ContextHandlerFactory.js @@ -0,0 +1,31 @@ +import SingleProductHandler from "./SingleProductHandler"; +import CartHandler from "./CartHandler"; +import CheckoutHandler from "./CheckoutHandler"; +import CartBlockHandler from "./CartBlockHandler"; +import CheckoutBlockHandler from "./CheckoutBlockHandler"; + +class ContextHandlerFactory { + + static create(context, buttonConfig, ppcpConfig) { + switch (context) { + case 'product': + return new SingleProductHandler(buttonConfig, ppcpConfig); + case 'cart': + return new CartHandler(buttonConfig, ppcpConfig); + case 'checkout': + return new CheckoutHandler(buttonConfig, ppcpConfig); + case 'pay-now': + // todo + return null; + case 'mini-cart': + // todo + return null; + case 'cart-block': + return new CartBlockHandler(buttonConfig, ppcpConfig); + case 'checkout-block': + return new CheckoutBlockHandler(buttonConfig, ppcpConfig); + } + } +} + +export default ContextHandlerFactory; diff --git a/modules/ppcp-googlepay/resources/js/Context/SingleProductHandler.js b/modules/ppcp-googlepay/resources/js/Context/SingleProductHandler.js new file mode 100644 index 000000000..9f5614bf9 --- /dev/null +++ b/modules/ppcp-googlepay/resources/js/Context/SingleProductHandler.js @@ -0,0 +1,93 @@ +import SingleProductActionHandler + from "../../../../ppcp-button/resources/js/modules/ActionHandler/SingleProductActionHandler"; +import SimulateCart from "../../../../ppcp-button/resources/js/modules/Helper/SimulateCart"; +import ErrorHandler from "../../../../ppcp-button/resources/js/modules/ErrorHandler"; +import UpdateCart from "../../../../ppcp-button/resources/js/modules/Helper/UpdateCart"; +import onApprove + from "../../../../ppcp-button/resources/js/modules/OnApproveHandler/onApproveForContinue"; + +class SingleProductHandler { + + constructor(buttonConfig, ppcpConfig) { + console.log('NEW SingleProductHandler'); + + this.buttonConfig = buttonConfig; + this.ppcpConfig = ppcpConfig; + } + + transactionInfo() { + const errorHandler = new ErrorHandler( + this.ppcpConfig.labels.error.generic, + document.querySelector('.woocommerce-notices-wrapper') + ); + + function form() { + return document.querySelector('form.cart'); + } + + const actionHandler = new SingleProductActionHandler( + null, + null, + form(), + errorHandler, + ); + + const hasSubscriptions = PayPalCommerceGateway.data_client_id.has_subscriptions + && PayPalCommerceGateway.data_client_id.paypal_subscriptions_enabled; + + const products = hasSubscriptions + ? actionHandler.getSubscriptionProducts() + : actionHandler.getProducts(); + + return new Promise((resolve, reject) => { + (new SimulateCart( + this.ppcpConfig.ajax.simulate_cart.endpoint, + this.ppcpConfig.ajax.simulate_cart.nonce, + )).simulate((data) => { + + resolve({ + countryCode: data.country_code, + currencyCode: data.currency_code, + totalPriceStatus: 'FINAL', + totalPrice: data.total_str // Your amount + }); + + }, products); + }); + } + + createOrder() { + const errorHandler = new ErrorHandler( + this.ppcpConfig.labels.error.generic, + document.querySelector('.woocommerce-notices-wrapper') + ); + + const actionHandler = new SingleProductActionHandler( + this.ppcpConfig, + new UpdateCart( + this.ppcpConfig.ajax.change_cart.endpoint, + this.ppcpConfig.ajax.change_cart.nonce, + ), + document.querySelector('form.cart'), + errorHandler, + ); + + return actionHandler.configuration().createOrder(); + } + + approveOrderForContinue(data, actions) { + const errorHandler = new ErrorHandler( + this.ppcpConfig.labels.error.generic, + document.querySelector('.woocommerce-notices-wrapper') + ); + + let onApproveHandler = onApprove({ + config: this.ppcpConfig + }, errorHandler); + + return onApproveHandler(data, actions); + } + +} + +export default SingleProductHandler; diff --git a/modules/ppcp-googlepay/resources/js/GooglepayManager.js b/modules/ppcp-googlepay/resources/js/GooglepayManager.js index bd7482fc5..524957d8b 100644 --- a/modules/ppcp-googlepay/resources/js/GooglepayManager.js +++ b/modules/ppcp-googlepay/resources/js/GooglepayManager.js @@ -1,12 +1,5 @@ -import SingleProductActionHandler from '../../../ppcp-button/resources/js/modules/ActionHandler/SingleProductActionHandler'; -import CartActionHandler from '../../../ppcp-button/resources/js/modules/ActionHandler/CartActionHandler'; -import UpdateCart from '../../../ppcp-button/resources/js/modules/Helper/UpdateCart'; -import ErrorHandler from '../../../ppcp-button/resources/js/modules/ErrorHandler'; -import SimulateCart from "../../../ppcp-button/resources/js/modules/Helper/SimulateCart"; -import onApprove from '../../../ppcp-button/resources/js/modules/OnApproveHandler/onApproveForContinue'; -import CheckoutActionHandler - from "../../../ppcp-button/resources/js/modules/ActionHandler/CheckoutActionHandler"; -import Spinner from "../../../ppcp-button/resources/js/modules/Helper/Spinner"; +import ContextHandlerFactory from "./Context/ContextHandlerFactory"; +import buttonModuleWatcher from "../../../ppcp-button/resources/js/modules/ButtonModuleWatcher"; class GooglepayManager { @@ -21,6 +14,20 @@ class GooglepayManager { this.isReadyToPayRequest = null; this.baseCardPaymentMethod = null; + + // Wait for buttons context handler to be loaded + this.contextHandler = null; + + buttonModuleWatcher.watchContextBootstrap((bootstrap) => { + if (bootstrap.context === 'mini-cart') { + return; + } + this.contextHandler = ContextHandlerFactory.create( + bootstrap.context, + buttonConfig, + ppcpConfig + ); + }); } init() { @@ -151,80 +158,7 @@ class GooglepayManager { } async transactionInfo() { - -//------------- - const errorHandler = new ErrorHandler( - this.ppcpConfig.labels.error.generic, - document.querySelector('.woocommerce-notices-wrapper') - ); - - //== cart & checkout page == - - return new Promise((resolve, reject) => { - - fetch( - this.ppcpConfig.ajax.cart_script_params.endpoint, - { - method: 'GET', - credentials: 'same-origin', - } - ) - .then(result => result.json()) - .then(result => { - if (! result.success) { - return; - } - - // handle script reload - const data = result.data; - - resolve({ - countryCode: 'US', // data.country_code, - currencyCode: 'USD', // data.currency_code, - totalPriceStatus: 'FINAL', - totalPrice: data.amount // Your amount - }); - - }); - }); - - - // == product page == - // function form() { - // return document.querySelector('form.cart'); - // } - // - // const actionHandler = new SingleProductActionHandler( - // null, - // null, - // form(), - // errorHandler, - // ); - // - // const hasSubscriptions = PayPalCommerceGateway.data_client_id.has_subscriptions - // && PayPalCommerceGateway.data_client_id.paypal_subscriptions_enabled; - // - // const products = hasSubscriptions - // ? actionHandler.getSubscriptionProducts() - // : actionHandler.getProducts(); - // - // return new Promise((resolve, reject) => { - // (new SimulateCart( - // this.ppcpConfig.ajax.simulate_cart.endpoint, - // this.ppcpConfig.ajax.simulate_cart.nonce, - // )).simulate((data) => { - // - // resolve({ - // countryCode: data.country_code, - // currencyCode: data.currency_code, - // totalPriceStatus: 'FINAL', - // totalPrice: data.total_str // Your amount - // }); - // - // }, products); - // }); -//------------- - + return this.contextHandler.transactionInfo(); } //------------------------ @@ -252,56 +186,11 @@ class GooglepayManager { return new Promise(async (resolve, reject) => { try { - -//------------- console.log('ppcpConfig:', this.ppcpConfig); - const errorHandler = new ErrorHandler( - this.ppcpConfig.labels.error.generic, - document.querySelector('.woocommerce-notices-wrapper') - ); - - // == checkout page == - // const spinner = new Spinner(); - // - // const actionHandler = new CheckoutActionHandler( - // this.ppcpConfig, - // errorHandler, - // spinner - // ); - // - // let id = await actionHandler.configuration().createOrder(null, null); - - // == cart page == - const actionHandler = new CartActionHandler( - this.ppcpConfig, - errorHandler, - ); - - let id = await actionHandler.configuration().createOrder(null, null); - - // == product page == - // function form() { - // return document.querySelector('form.cart'); - // } - // const actionHandler = new SingleProductActionHandler( - // this.ppcpConfig, - // new UpdateCart( - // this.ppcpConfig.ajax.change_cart.endpoint, - // this.ppcpConfig.ajax.change_cart.nonce, - // ), - // form(), - // errorHandler, - // ); - // - // const createOrderInPayPal = actionHandler.createOrder(); - // - // let id = await createOrderInPayPal(null, null); + let id = await this.contextHandler.createOrder(); console.log('PayPal Order ID:', id); - -//------------- - console.log('paypal.Googlepay().confirmOrder : paymentData', { orderId: id, paymentMethodData: paymentData.paymentMethodData @@ -315,20 +204,10 @@ class GooglepayManager { /** Capture the Order on your Server */ if (confirmOrderResponse.status === "APPROVED") { - console.log('onApprove', this.ppcpConfig); let approveFailed = false; - - const approveOrderAndContinue = onApprove({ - config: this.ppcpConfig - }, errorHandler); - - console.log('approveOrderAndContinue', { - orderID: id - }); - - await approveOrderAndContinue({ + await this.contextHandler.approveOrderForContinue({ orderID: id }, { restart: () => new Promise((resolve, reject) => { @@ -351,17 +230,6 @@ class GooglepayManager { resolve({transactionState: 'SUCCESS'}); - // if (response.capture.status === "COMPLETED") - // resolve({transactionState: 'SUCCESS'}); - // else - // resolve({ - // transactionState: 'ERROR', - // error: { - // intent: 'PAYMENT_AUTHORIZATION', - // message: 'TRANSACTION FAILED', - // } - // }) - } else { resolve({ transactionState: 'ERROR', diff --git a/modules/ppcp-googlepay/resources/js/boot-block.js b/modules/ppcp-googlepay/resources/js/boot-block.js index 00ab91aeb..d1c8656ef 100644 --- a/modules/ppcp-googlepay/resources/js/boot-block.js +++ b/modules/ppcp-googlepay/resources/js/boot-block.js @@ -26,11 +26,11 @@ const GooglePayComponent = () => { const [paypalLoaded, setPaypalLoaded] = useState(false); const [googlePayLoaded, setGooglePayLoaded] = useState(false); - const manager = new GooglepayManager(buttonConfig, ppcpConfig); - const bootstrap = function () { const manager = new GooglepayManager(buttonConfig, ppcpConfig); manager.init(); + + }; useEffect(() => { @@ -43,7 +43,7 @@ const GooglePayComponent = () => { loadPaypalScript(ppcpConfig, () => { setPaypalLoaded(true); }); - }); + }, []); useEffect(() => { if (!bootstrapped && paypalLoaded && googlePayLoaded) {