From 118e49cfdef021935c70a0f3ea347300887b7bd8 Mon Sep 17 00:00:00 2001 From: Pedro Silva Date: Fri, 3 Nov 2023 10:30:31 +0000 Subject: [PATCH] Add ApplePay preview in admin settings. --- .../ppcp-applepay/resources/css/styles.scss | 10 +- .../resources/js/ApplepayButton.js | 16 +- .../js/Context/ContextHandlerFactory.js | 3 + .../resources/js/Context/PreviewHandler.js | 37 +++++ .../ppcp-applepay/resources/js/boot-admin.js | 144 ++++++++++++++++++ modules/ppcp-applepay/src/ApplepayModule.php | 39 +++++ .../src/Assets/ApplePayButton.php | 45 +++++- .../src/Assets/DataToAppleButtonScripts.php | 63 ++++++++ modules/ppcp-applepay/webpack.config.js | 1 + .../resources/js/Context/PreviewHandler.js | 4 +- 10 files changed, 343 insertions(+), 19 deletions(-) create mode 100644 modules/ppcp-applepay/resources/js/Context/PreviewHandler.js create mode 100644 modules/ppcp-applepay/resources/js/boot-admin.js diff --git a/modules/ppcp-applepay/resources/css/styles.scss b/modules/ppcp-applepay/resources/css/styles.scss index d3f834119..0abd19451 100644 --- a/modules/ppcp-applepay/resources/css/styles.scss +++ b/modules/ppcp-applepay/resources/css/styles.scss @@ -1,11 +1,11 @@ -#applepay-container { +#applepay-container, .ppcp-button-applepay { --apple-pay-button-height: 45px; --apple-pay-button-min-height: 40px; --apple-pay-button-width: 100%; --apple-pay-button-max-width: 750px; --apple-pay-button-border-radius: 4px; --apple-pay-button-overflow: hidden; - --apple-pay-button-margin:7px 0; + margin:7px 0; &.ppcp-button-pill { --apple-pay-button-border-radius: 50px; } @@ -17,7 +17,7 @@ } .woocommerce-checkout { - #applepay-container { + #applepay-container, .ppcp-button-applepay { margin-top: 0.5em; --apple-pay-button-border-radius: 4px; --apple-pay-button-height: 45px; @@ -30,7 +30,7 @@ .ppcp-has-applepay-block { .wp-block-woocommerce-checkout { - #applepay-container { + #applepay-container, .ppcp-button-applepay { --apple-pay-button-margin: 0; --apple-pay-button-height: 40px; &.ppcp-button-pill { @@ -40,7 +40,7 @@ } .wp-block-woocommerce-cart { - #applepay-container { + #applepay-container, .ppcp-button-applepay { --apple-pay-button-margin: 0; --apple-pay-button-height: 40px; } diff --git a/modules/ppcp-applepay/resources/js/ApplepayButton.js b/modules/ppcp-applepay/resources/js/ApplepayButton.js index 19c03c5d2..6cab7d5ca 100644 --- a/modules/ppcp-applepay/resources/js/ApplepayButton.js +++ b/modules/ppcp-applepay/resources/js/ApplepayButton.js @@ -4,6 +4,7 @@ import {setVisible} from '../../../ppcp-button/resources/js/modules/Helper/Hidin import {setEnabled} from '../../../ppcp-button/resources/js/modules/Helper/ButtonDisabler'; import FormValidator from "../../../ppcp-button/resources/js/modules/Helper/FormValidator"; import ErrorHandler from '../../../ppcp-button/resources/js/modules/ErrorHandler'; +import widgetBuilder from "../../../ppcp-button/resources/js/modules/Renderer/WidgetBuilder"; class ApplepayButton { @@ -48,7 +49,7 @@ class ApplepayButton { const isEligible = this.applePayConfig.isEligible; if (isEligible) { this.fetchTransactionInfo().then(() => { - const isSubscriptionProduct = this.ppcpConfig.data_client_id.has_subscriptions === true; + const isSubscriptionProduct = this.ppcpConfig?.data_client_id?.has_subscriptions === true; if (isSubscriptionProduct) { return; } @@ -145,13 +146,12 @@ class ApplepayButton { return session; } - - - /** * Add a Apple Pay purchase button */ addButton() { + this.log('addButton', this.context); + const wrapper = (this.context === 'mini-cart') ? this.buttonConfig.button.mini_cart_wrapper @@ -160,7 +160,7 @@ class ApplepayButton { (this.context === 'mini-cart') ? this.ppcpConfig.button.mini_cart_style.shape : this.ppcpConfig.button.style.shape; - const appleContainer = this.context === 'mini-cart' ? document.getElementById("applepay-container-minicart") : document.getElementById("applepay-container"); + const appleContainer = document.getElementById(wrapper); const type = this.buttonConfig.button.type; const language = this.buttonConfig.button.lang; const color = this.buttonConfig.button.color; @@ -195,7 +195,7 @@ class ApplepayButton { try { const formData = new FormData(document.querySelector(checkoutFormSelector)); this.form_saved = Object.fromEntries(formData.entries()); - // This line should be reviewed, the paypal.Applepay().confirmOrder fails if we add it. + // This line should be reviewed, the widgetBuilder.paypal.Applepay().confirmOrder fails if we add it. //this.update_request_data_with_form(paymentDataRequest); } catch (error) { console.error(error); @@ -274,7 +274,7 @@ class ApplepayButton { return (applePayValidateMerchantEvent) => { this.log('onvalidatemerchant call'); - paypal.Applepay().validateMerchant({ + widgetBuilder.paypal.Applepay().validateMerchant({ validationUrl: applePayValidateMerchantEvent.validationURL }) .then(validateResult => { @@ -501,7 +501,7 @@ class ApplepayButton { this.log('onpaymentauthorized paypal order ID', id, event.payment.token, event.payment.billingContact); try { - const confirmOrderResponse = await paypal.Applepay().confirmOrder({ + const confirmOrderResponse = await widgetBuilder.paypal.Applepay().confirmOrder({ orderId: id, token: event.payment.token, billingContact: event.payment.billingContact, diff --git a/modules/ppcp-applepay/resources/js/Context/ContextHandlerFactory.js b/modules/ppcp-applepay/resources/js/Context/ContextHandlerFactory.js index 72ee4f5fa..d94833daa 100644 --- a/modules/ppcp-applepay/resources/js/Context/ContextHandlerFactory.js +++ b/modules/ppcp-applepay/resources/js/Context/ContextHandlerFactory.js @@ -4,6 +4,7 @@ import CheckoutHandler from "./CheckoutHandler"; import CartBlockHandler from "./CartBlockHandler"; import CheckoutBlockHandler from "./CheckoutBlockHandler"; import MiniCartHandler from "./MiniCartHandler"; +import PreviewHandler from "./PreviewHandler"; class ContextHandlerFactory { @@ -22,6 +23,8 @@ class ContextHandlerFactory { return new CartBlockHandler(buttonConfig, ppcpConfig); case 'checkout-block': return new CheckoutBlockHandler(buttonConfig, ppcpConfig); + case 'preview': + return new PreviewHandler(buttonConfig, ppcpConfig); } } } diff --git a/modules/ppcp-applepay/resources/js/Context/PreviewHandler.js b/modules/ppcp-applepay/resources/js/Context/PreviewHandler.js new file mode 100644 index 000000000..6e0da8852 --- /dev/null +++ b/modules/ppcp-applepay/resources/js/Context/PreviewHandler.js @@ -0,0 +1,37 @@ +import BaseHandler from "./BaseHandler"; + +class PreviewHandler extends BaseHandler { + + constructor(buttonConfig, ppcpConfig, externalHandler) { + super(buttonConfig, ppcpConfig, externalHandler); + } + + transactionInfo() { + // We need to return something as ApplePay button initialization expects valid data. + return { + countryCode: "US", + currencyCode: "USD", + totalPrice: "10.00", + totalPriceStatus: "FINAL" + } + } + + createOrder() { + throw new Error('Create order fail. This is just a preview.'); + } + + approveOrder(data, actions) { + throw new Error('Approve order fail. This is just a preview.'); + } + + actionHandler() { + throw new Error('Action handler fail. This is just a preview.'); + } + + errorHandler() { + throw new Error('Error handler fail. This is just a preview.'); + } + +} + +export default PreviewHandler; diff --git a/modules/ppcp-applepay/resources/js/boot-admin.js b/modules/ppcp-applepay/resources/js/boot-admin.js new file mode 100644 index 000000000..c2fa7796a --- /dev/null +++ b/modules/ppcp-applepay/resources/js/boot-admin.js @@ -0,0 +1,144 @@ +import {loadCustomScript} from "@paypal/paypal-js"; +import ApplepayButton from "./ApplepayButton"; +import widgetBuilder from "../../../ppcp-button/resources/js/modules/Renderer/WidgetBuilder"; + +(function ({ + buttonConfig, + jQuery +}) { + + let applePayConfig; + 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 ApplePay config changes. + jQuery([ + '#ppcp-applepay_button_enabled', + '#ppcp-applepay_button_type', + '#ppcp-applepay_button_color', + '#ppcp-applepay_button_language' + ].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 applyConfigOptions = function (buttonConfig) { + buttonConfig.button = buttonConfig.button || {}; + buttonConfig.button.type = jQuery('#ppcp-applepay_button_type').val(); + buttonConfig.button.color = jQuery('#ppcp-applepay_button_color').val(); + buttonConfig.button.lang = jQuery('#ppcp-applepay_button_language').val(); + } + + const createButton = function (ppcpConfig) { + const selector = ppcpConfig.button.wrapper + 'ApplePay'; + + if (!jQuery('#ppcp-applepay_button_enabled').is(':checked')) { + jQuery(selector).remove(); + return; + } + + buttonConfig = JSON.parse(JSON.stringify(buttonConfig)); + buttonConfig.button.wrapper = selector.replace('#', ''); + applyConfigOptions(buttonConfig); + + const wrapperElement = `
`; + + if (!jQuery(selector).length) { + jQuery(ppcpConfig.button.wrapper).after(wrapperElement); + } else { + jQuery(selector).replaceWith(wrapperElement); + } + + const button = new ApplepayButton( + 'preview', + null, + buttonConfig, + ppcpConfig, + ); + + button.init(applePayConfig); + + activeButtons[selector] = ppcpConfig; + } + + const bootstrap = async function () { + if (!widgetBuilder.paypal) { + return; + } + + applePayConfig = await widgetBuilder.paypal.Applepay().config(); + + // We need to set bootstrapped here otherwise applePayConfig 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 applePayLoaded = false; + + const tryToBoot = () => { + if (!bootstrapped && paypalLoaded && applePayLoaded) { + bootstrap(); + } + } + + // Load ApplePay SDK + loadCustomScript({ url: buttonConfig.sdk_url }).then(() => { + applePayLoaded = 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_applepay_admin, + jQuery: window.jQuery +}); diff --git a/modules/ppcp-applepay/src/ApplepayModule.php b/modules/ppcp-applepay/src/ApplepayModule.php index b81329811..1abbf339b 100644 --- a/modules/ppcp-applepay/src/ApplepayModule.php +++ b/modules/ppcp-applepay/src/ApplepayModule.php @@ -89,6 +89,8 @@ class ApplepayModule implements ModuleInterface { $module->render_buttons( $c, $apple_payment_method ); $apple_payment_method->bootstrap_ajax_request(); + + $module->load_admin_assets( $c, $apple_payment_method ); } ); @@ -180,6 +182,43 @@ class ApplepayModule implements ModuleInterface { ); } + /** + * Registers and enqueues the assets. + * + * @param ContainerInterface $c The container. + * @param ApplePayButton $button The button. + * @return void + */ + public function load_admin_assets( ContainerInterface $c, ApplePayButton $button ): void { + // Enqueue backend scripts. + add_action( + 'admin_enqueue_scripts', + static function () use ( $c, $button ) { + if ( ! is_admin() ) { + return; + } + + /** + * Should add this to the ButtonInterface. + * + * @psalm-suppress UndefinedInterfaceMethod + */ + $button->enqueue_admin(); + } + ); + + // Adds ApplePay component to the backend button preview settings. + add_action( + 'woocommerce_paypal_payments_admin_gateway_settings', + function( array $settings ) use ( $c ): array { + if ( is_array( $settings['components'] ) ) { + $settings['components'][] = 'applepay'; + } + return $settings; + } + ); + } + /** * Renders the Apple Pay buttons in the enabled places. * diff --git a/modules/ppcp-applepay/src/Assets/ApplePayButton.php b/modules/ppcp-applepay/src/Assets/ApplePayButton.php index fe7313a85..3f1063684 100644 --- a/modules/ppcp-applepay/src/Assets/ApplePayButton.php +++ b/modules/ppcp-applepay/src/Assets/ApplePayButton.php @@ -942,7 +942,7 @@ class ApplePayButton implements ButtonInterface { } if ( $button_enabled_cart ) { $default_hook_name = 'woocommerce_paypal_payments_cart_button_render'; - $render_placeholder = apply_filters( 'woocommerce_paypal_payments_googlepay_cart_button_render_hook', $default_hook_name ); + $render_placeholder = apply_filters( 'woocommerce_paypal_payments_applepay_cart_button_render_hook', $default_hook_name ); $render_placeholder = is_string( $render_placeholder ) ? $render_placeholder : $default_hook_name; add_action( $render_placeholder, @@ -954,7 +954,7 @@ class ApplePayButton implements ButtonInterface { if ( $button_enabled_checkout ) { $default_hook_name = 'woocommerce_paypal_payments_checkout_button_render'; - $render_placeholder = apply_filters( 'woocommerce_paypal_payments_googlepay_checkout_button_render_hook', $default_hook_name ); + $render_placeholder = apply_filters( 'woocommerce_paypal_payments_applepay_checkout_button_render_hook', $default_hook_name ); $render_placeholder = is_string( $render_placeholder ) ? $render_placeholder : $default_hook_name; add_action( $render_placeholder, @@ -966,7 +966,7 @@ class ApplePayButton implements ButtonInterface { } if ( $button_enabled_payorder ) { $default_hook_name = 'woocommerce_paypal_payments_payorder_button_render'; - $render_placeholder = apply_filters( 'woocommerce_paypal_payments_googlepay_payorder_button_render_hook', $default_hook_name ); + $render_placeholder = apply_filters( 'woocommerce_paypal_payments_applepay_payorder_button_render_hook', $default_hook_name ); $render_placeholder = is_string( $render_placeholder ) ? $render_placeholder : $default_hook_name; add_action( $render_placeholder, @@ -979,7 +979,7 @@ class ApplePayButton implements ButtonInterface { if ( $button_enabled_minicart ) { $default_hook_name = 'woocommerce_paypal_payments_minicart_button_render'; - $render_placeholder = apply_filters( 'woocommerce_paypal_payments_googlepay_minicart_button_render_hook', $default_hook_name ); + $render_placeholder = apply_filters( 'woocommerce_paypal_payments_applepay_minicart_button_render_hook', $default_hook_name ); $render_placeholder = is_string( $render_placeholder ) ? $render_placeholder : $default_hook_name; add_action( $render_placeholder, @@ -1059,6 +1059,34 @@ class ApplePayButton implements ButtonInterface { wp_enqueue_style( 'wc-ppcp-applepay' ); } + /** + * Enqueues scripts/styles for admin. + */ + public function enqueue_admin(): void { + wp_register_style( + 'wc-ppcp-applepay-admin', + untrailingslashit( $this->module_url ) . '/assets/css/styles.css', + array(), + $this->version + ); + wp_enqueue_style( 'wc-ppcp-applepay-admin' ); + + wp_register_script( + 'wc-ppcp-applepay-admin', + untrailingslashit( $this->module_url ) . '/assets/js/boot-admin.js?x=' . time(), + array(), + $this->version, + true + ); + wp_enqueue_script( 'wc-ppcp-applepay-admin' ); + + wp_localize_script( + 'wc-ppcp-applepay-admin', + 'wc_ppcp_applepay_admin', + $this->script_data_for_admin() + ); + } + /** * Returns the script data. * @@ -1068,6 +1096,15 @@ class ApplePayButton implements ButtonInterface { return $this->script_data->apple_pay_script_data(); } + /** + * Returns the admin script data. + * + * @return array + */ + public function script_data_for_admin(): array { + return $this->script_data->apple_pay_script_data_for_admin(); + } + /** * Returns true if the module is enabled. * diff --git a/modules/ppcp-applepay/src/Assets/DataToAppleButtonScripts.php b/modules/ppcp-applepay/src/Assets/DataToAppleButtonScripts.php index 0b15fbe78..a530ba95f 100644 --- a/modules/ppcp-applepay/src/Assets/DataToAppleButtonScripts.php +++ b/modules/ppcp-applepay/src/Assets/DataToAppleButtonScripts.php @@ -69,6 +69,26 @@ class DataToAppleButtonScripts { ); } + + /** + * Returns the appropriate admin data to send to ApplePay script + * + * @return array + * @throws NotFoundException When the setting is not found. + */ + public function apple_pay_script_data_for_admin(): array { + $base_location = wc_get_base_location(); + $shop_country_code = $base_location['country']; + $currency_code = get_woocommerce_currency(); + $total_label = get_bloginfo( 'name' ); + + return $this->data_for_admin_page( + $shop_country_code, + $currency_code, + $total_label + ); + } + /** * Check if the product needs shipping * @@ -197,4 +217,47 @@ class DataToAppleButtonScripts { 'ajax_url' => admin_url( 'admin-ajax.php' ), ); } + + /** + * Prepares the data for the cart page. + * Consider refactoring this method along with data_for_cart_page() and data_for_product_page() methods. + * + * @param string $shop_country_code The shop country code. + * @param string $currency_code The currency code. + * @param string $total_label The label for the total amount. + * + * @return array + */ + protected function data_for_admin_page( + $shop_country_code, + $currency_code, + $total_label + ) { + $type = $this->settings->has( 'applepay_button_type' ) ? $this->settings->get( 'applepay_button_type' ) : ''; + $color = $this->settings->has( 'applepay_button_color' ) ? $this->settings->get( 'applepay_button_color' ) : ''; + $lang = $this->settings->has( 'applepay_button_language' ) ? $this->settings->get( 'applepay_button_language' ) : ''; + $lang = apply_filters( 'woocommerce_paypal_payments_applepay_button_language', $lang ); + + return array( + 'sdk_url' => $this->sdk_url, + 'is_debug' => defined( 'WP_DEBUG' ) && WP_DEBUG ? true : false, + 'button' => array( + 'wrapper' => 'applepay-container', + 'mini_cart_wrapper' => 'applepay-container-minicart', + 'type' => $type, + 'color' => $color, + 'lang' => $lang, + ), + 'product' => array( + 'needShipping' => false, + 'subtotal' => 0, + ), + 'shop' => array( + 'countryCode' => $shop_country_code, + 'currencyCode' => $currency_code, + 'totalLabel' => $total_label, + ), + 'ajax_url' => admin_url( 'admin-ajax.php' ), + ); + } } diff --git a/modules/ppcp-applepay/webpack.config.js b/modules/ppcp-applepay/webpack.config.js index 63834c8a6..94e7bfd8b 100644 --- a/modules/ppcp-applepay/webpack.config.js +++ b/modules/ppcp-applepay/webpack.config.js @@ -11,6 +11,7 @@ module.exports = { entry: { 'boot': path.resolve('./resources/js/boot.js'), 'boot-block': path.resolve('./resources/js/boot-block.js'), + 'boot-admin': path.resolve('./resources/js/boot-admin.js'), "styles": path.resolve('./resources/css/styles.scss') }, output: { diff --git a/modules/ppcp-googlepay/resources/js/Context/PreviewHandler.js b/modules/ppcp-googlepay/resources/js/Context/PreviewHandler.js index a637f078d..f4eeee486 100644 --- a/modules/ppcp-googlepay/resources/js/Context/PreviewHandler.js +++ b/modules/ppcp-googlepay/resources/js/Context/PreviewHandler.js @@ -1,6 +1,6 @@ import BaseHandler from "./BaseHandler"; -class CartHandler extends BaseHandler { +class PreviewHandler extends BaseHandler { constructor(buttonConfig, ppcpConfig, externalHandler) { super(buttonConfig, ppcpConfig, externalHandler); @@ -28,4 +28,4 @@ class CartHandler extends BaseHandler { } -export default CartHandler; +export default PreviewHandler;