From c89da49e8f064eb68d80c98e403d11728ce8d1a6 Mon Sep 17 00:00:00 2001 From: Daniel Dudzic <d.dudzic@syde.com> Date: Wed, 23 Oct 2024 18:45:33 +0200 Subject: [PATCH 1/7] Add PayPalInsightsLoader for Axo Block --- .../js/plugins/PayPalInsightsLoader.js | 35 ++++++++++++++++ modules/ppcp-axo-block/src/AxoBlockModule.php | 42 +++++++++++++++++++ 2 files changed, 77 insertions(+) create mode 100644 modules/ppcp-axo-block/resources/js/plugins/PayPalInsightsLoader.js diff --git a/modules/ppcp-axo-block/resources/js/plugins/PayPalInsightsLoader.js b/modules/ppcp-axo-block/resources/js/plugins/PayPalInsightsLoader.js new file mode 100644 index 000000000..9f114593b --- /dev/null +++ b/modules/ppcp-axo-block/resources/js/plugins/PayPalInsightsLoader.js @@ -0,0 +1,35 @@ +import { registerPlugin } from '@wordpress/plugins'; +import { useSelect } from '@wordpress/data'; +import { useEffect, useState } from '@wordpress/element'; +import PayPalInsights from '../../../../ppcp-axo/resources/js/Insights/PayPalInsights'; + +const PayPalInsightsLoader = () => { + + // Subscribe to checkout store data + const checkoutData = useSelect( ( select ) => { + return { + paymentMethod: select( 'wc/store/checkout' ).getPaymentMethod(), + orderTotal: select( 'wc/store/checkout' ).getOrderTotal(), + // Add other data points you need + }; + } ); + + // Watch for changes and trigger analytics events + useEffect( () => { + if ( isScriptLoaded && window.YourAnalyticsObject ) { + // Example tracking calls + window.YourAnalyticsObject.track( 'checkout_updated', { + payment_method: checkoutData.paymentMethod, + order_total: checkoutData.orderTotal, + } ); + } + }, [ isScriptLoaded, checkoutData ] ); + + return null; // This component doesn't render anything +}; + +// Register the plugin to run with the checkout block +registerPlugin( 'wc-ppcp-paypal-insights', { + render: PayPalInsightsLoader, + scope: 'woocommerce-checkout', +} ); diff --git a/modules/ppcp-axo-block/src/AxoBlockModule.php b/modules/ppcp-axo-block/src/AxoBlockModule.php index 669cc7cc5..2a7ee73b6 100644 --- a/modules/ppcp-axo-block/src/AxoBlockModule.php +++ b/modules/ppcp-axo-block/src/AxoBlockModule.php @@ -133,6 +133,15 @@ static function () use ( $c ) { wp_enqueue_style( 'wc-ppcp-axo-block' ); } ); + + // Enqueue the PayPal Insights script + add_action( + 'wp_enqueue_scripts', + function () use ($c) { + $this->enqueue_paypal_insights_script($c); + } + ); + return true; } @@ -166,4 +175,37 @@ private function add_sdk_client_token_to_script_data( return $localized_script_data; } + + /** + * Enqueues PayPal Insights analytics script for the Checkout block. + * + * @param ContainerInterface $c The service container. + * @return void + */ + private function enqueue_paypal_insights_script( ContainerInterface $c ): void { + if ( ! has_block( 'woocommerce/checkout' ) ) { + return; + } + + $module_url = $c->get( 'axoblock.url' ); + $asset_version = $c->get( 'ppcp.asset-version' ); + + wp_register_script( + 'wc-ppcp-paypal-insights', + untrailingslashit( $module_url ) . '/resources/js/plugins/PayPalInsights.js', + array( 'wp-plugins', 'wp-data', 'wp-element', 'wc-blocks-registry' ), + $asset_version, + true + ); + + wp_localize_script( + 'wc-ppcp-paypal-insights', + 'ppcpPayPalInsightsData', + array( + 'isAxoEnabled' => true, + ) + ); + + wp_enqueue_script( 'wc-ppcp-paypal-insights' ); + } } From 1204edb1ca1b7a1d4b9efa2412bc572aad94dc50 Mon Sep 17 00:00:00 2001 From: Daniel Dudzic <d.dudzic@syde.com> Date: Tue, 29 Oct 2024 11:04:41 +0100 Subject: [PATCH 2/7] Axo: Add PayPal Insights to the Block Checkout and fix the existing Classic Checkout integration --- .../js/plugins/PayPalInsightsLoader.js | 260 ++++++++++++++++-- .../resources/js/stores/axoStore.js | 16 +- modules/ppcp-axo-block/services.php | 1 + modules/ppcp-axo-block/src/AxoBlockModule.php | 10 +- .../src/AxoBlockPaymentMethod.php | 39 ++- modules/ppcp-axo-block/webpack.config.js | 5 +- modules/ppcp-axo/resources/js/AxoManager.js | 38 ++- .../js/Insights/EndCheckoutTracker.js | 99 +++++++ modules/ppcp-axo/services.php | 48 ++++ modules/ppcp-axo/src/Assets/AxoManager.php | 30 +- modules/ppcp-axo/src/AxoModule.php | 66 ++++- modules/ppcp-axo/webpack.config.js | 71 ++--- 12 files changed, 567 insertions(+), 116 deletions(-) create mode 100644 modules/ppcp-axo/resources/js/Insights/EndCheckoutTracker.js diff --git a/modules/ppcp-axo-block/resources/js/plugins/PayPalInsightsLoader.js b/modules/ppcp-axo-block/resources/js/plugins/PayPalInsightsLoader.js index 9f114593b..b831bd45b 100644 --- a/modules/ppcp-axo-block/resources/js/plugins/PayPalInsightsLoader.js +++ b/modules/ppcp-axo-block/resources/js/plugins/PayPalInsightsLoader.js @@ -1,35 +1,259 @@ import { registerPlugin } from '@wordpress/plugins'; +import { useEffect, useCallback, useState, useRef } from '@wordpress/element'; import { useSelect } from '@wordpress/data'; -import { useEffect, useState } from '@wordpress/element'; +import { PAYMENT_STORE_KEY } from '@woocommerce/block-data'; import PayPalInsights from '../../../../ppcp-axo/resources/js/Insights/PayPalInsights'; +import { STORE_NAME } from '../stores/axoStore'; +import usePayPalCommerceGateway from '../hooks/usePayPalCommerceGateway'; + +const GATEWAY_HANDLE = 'ppcp-axo-gateway'; + +const useEventTracking = () => { + const [ triggeredEvents, setTriggeredEvents ] = useState( { + initialized: false, + jsLoaded: false, + beginCheckout: false, + emailSubmitted: false, + } ); + + const currentPaymentMethod = useRef( null ); + + const setEventTriggered = useCallback( ( eventName, value = true ) => { + setTriggeredEvents( ( prev ) => ( { + ...prev, + [ eventName ]: value, + } ) ); + }, [] ); + + const isEventTriggered = useCallback( + ( eventName ) => triggeredEvents[ eventName ], + [ triggeredEvents ] + ); + + const setCurrentPaymentMethod = useCallback( ( methodName ) => { + currentPaymentMethod.current = methodName; + }, [] ); + + const getCurrentPaymentMethod = useCallback( + () => currentPaymentMethod.current, + [] + ); + + return { + setEventTriggered, + isEventTriggered, + setCurrentPaymentMethod, + getCurrentPaymentMethod, + }; +}; + +const waitForPayPalInsight = () => { + return new Promise( ( resolve, reject ) => { + // If already loaded, resolve immediately + if ( window.paypalInsight ) { + resolve( window.paypalInsight ); + return; + } + + // Set a reasonable timeout + const timeoutId = setTimeout( () => { + observer.disconnect(); + reject( new Error( 'PayPal Insights script load timeout' ) ); + }, 10000 ); + + // Create MutationObserver to watch for script initialization + const observer = new MutationObserver( () => { + if ( window.paypalInsight ) { + observer.disconnect(); + clearTimeout( timeoutId ); + resolve( window.paypalInsight ); + } + } ); + + // Start observing + observer.observe( document, { + childList: true, + subtree: true, + } ); + } ); +}; + +const usePayPalInsightsInit = ( axoConfig, ppcpConfig, eventTracking ) => { + const { setEventTriggered, isEventTriggered } = eventTracking; + const initialized = useRef( false ); + + useEffect( () => { + if ( + ! axoConfig?.insights?.enabled || + ! axoConfig?.insights?.client_id || + ! axoConfig?.insights?.session_id || + initialized.current || + isEventTriggered( 'initialized' ) + ) { + return; + } + + const initializePayPalInsights = async () => { + try { + await waitForPayPalInsight(); + + if ( initialized.current ) { + return; + } + + // Track JS load first + PayPalInsights.trackJsLoad(); + setEventTriggered( 'jsLoaded' ); + + PayPalInsights.config( axoConfig.insights.client_id, { + debug: axoConfig?.wp_debug === '1', + } ); + + PayPalInsights.setSessionId( axoConfig.insights.session_id ); + initialized.current = true; + setEventTriggered( 'initialized' ); + + if ( + isEventTriggered( 'jsLoaded' ) && + ! isEventTriggered( 'beginCheckout' ) + ) { + PayPalInsights.trackBeginCheckout( { + amount: axoConfig.insights.amount, + page_type: 'checkout', + user_data: { + country: 'US', + is_store_member: false, + }, + } ); + setEventTriggered( 'beginCheckout' ); + } + } catch ( error ) { + console.error( + 'PayPal Insights initialization failed:', + error + ); + } + }; + + initializePayPalInsights(); + + return () => { + initialized.current = false; + }; + }, [ axoConfig, ppcpConfig, setEventTriggered, isEventTriggered ] ); +}; + +const usePaymentMethodTracking = ( axoConfig, eventTracking ) => { + const { setCurrentPaymentMethod } = eventTracking; + const lastPaymentMethod = useRef( null ); + const isInitialMount = useRef( true ); + + const activePaymentMethod = useSelect( ( select ) => { + return select( PAYMENT_STORE_KEY )?.getActivePaymentMethod(); + }, [] ); + + const handlePaymentMethodChange = useCallback( + async ( paymentMethod ) => { + // Skip if no payment method or same as last one + if ( + ! paymentMethod || + paymentMethod === lastPaymentMethod.current + ) { + return; + } + + try { + await waitForPayPalInsight(); + + // Only track if it's not the initial mount, and we have a previous payment method + if ( ! isInitialMount.current && lastPaymentMethod.current ) { + PayPalInsights.trackSelectPaymentMethod( { + payment_method_selected: + axoConfig?.insights?.payment_method_selected_map[ + paymentMethod + ] || 'other', + page_type: 'checkout', + } ); + } + + lastPaymentMethod.current = paymentMethod; + setCurrentPaymentMethod( paymentMethod ); + } catch ( error ) { + console.error( 'Failed to track payment method:', error ); + } + }, + [ + axoConfig?.insights?.payment_method_selected_map, + setCurrentPaymentMethod, + ] + ); + + useEffect( () => { + if ( activePaymentMethod ) { + if ( isInitialMount.current ) { + // Just set the initial payment method without tracking + lastPaymentMethod.current = activePaymentMethod; + setCurrentPaymentMethod( activePaymentMethod ); + isInitialMount.current = false; + } else { + handlePaymentMethodChange( activePaymentMethod ); + } + } + }, [ + activePaymentMethod, + handlePaymentMethodChange, + setCurrentPaymentMethod, + ] ); + + useEffect( () => { + return () => { + lastPaymentMethod.current = null; + isInitialMount.current = true; + }; + }, [] ); +}; const PayPalInsightsLoader = () => { + const eventTracking = useEventTracking(); + const { setEventTriggered, isEventTriggered } = eventTracking; + + const initialConfig = + window?.wc?.wcSettings?.getSetting( `${ GATEWAY_HANDLE }_data` ) || {}; - // Subscribe to checkout store data - const checkoutData = useSelect( ( select ) => { + const { ppcpConfig } = usePayPalCommerceGateway( initialConfig ); + const axoConfig = window?.wc_ppcp_axo; + + const { isEmailSubmitted } = useSelect( ( select ) => { + const storeSelect = select( STORE_NAME ); return { - paymentMethod: select( 'wc/store/checkout' ).getPaymentMethod(), - orderTotal: select( 'wc/store/checkout' ).getOrderTotal(), - // Add other data points you need + isEmailSubmitted: storeSelect?.getIsEmailSubmitted?.() ?? false, }; - } ); + }, [] ); + + usePayPalInsightsInit( axoConfig, ppcpConfig, eventTracking ); + usePaymentMethodTracking( axoConfig, eventTracking ); - // Watch for changes and trigger analytics events useEffect( () => { - if ( isScriptLoaded && window.YourAnalyticsObject ) { - // Example tracking calls - window.YourAnalyticsObject.track( 'checkout_updated', { - payment_method: checkoutData.paymentMethod, - order_total: checkoutData.orderTotal, - } ); - } - }, [ isScriptLoaded, checkoutData ] ); + const trackEmail = async () => { + if ( isEmailSubmitted && ! isEventTriggered( 'emailSubmitted' ) ) { + try { + await waitForPayPalInsight(); + PayPalInsights.trackSubmitCheckoutEmail(); + setEventTriggered( 'emailSubmitted' ); + } catch ( error ) { + console.error( 'Failed to track email submission:', error ); + } + } + }; + trackEmail(); + }, [ isEmailSubmitted, setEventTriggered, isEventTriggered ] ); - return null; // This component doesn't render anything + return null; }; -// Register the plugin to run with the checkout block registerPlugin( 'wc-ppcp-paypal-insights', { render: PayPalInsightsLoader, scope: 'woocommerce-checkout', } ); + +export default PayPalInsightsLoader; diff --git a/modules/ppcp-axo-block/resources/js/stores/axoStore.js b/modules/ppcp-axo-block/resources/js/stores/axoStore.js index f779f983c..6235e58c2 100644 --- a/modules/ppcp-axo-block/resources/js/stores/axoStore.js +++ b/modules/ppcp-axo-block/resources/js/stores/axoStore.js @@ -1,4 +1,4 @@ -import { createReduxStore, register, dispatch } from '@wordpress/data'; +import { createReduxStore, register, dispatch, select } from '@wordpress/data'; export const STORE_NAME = 'woocommerce-paypal-payments/axo-block'; @@ -100,13 +100,15 @@ const selectors = { }; // Create and register the Redux store for the AXO block -const store = createReduxStore( STORE_NAME, { - reducer, - actions, - selectors, -} ); +if ( ! select( STORE_NAME ) ) { + const store = createReduxStore( STORE_NAME, { + reducer, + actions, + selectors, + } ); -register( store ); + register( store ); +} // Action dispatchers diff --git a/modules/ppcp-axo-block/services.php b/modules/ppcp-axo-block/services.php index 270b69260..fddf07f80 100644 --- a/modules/ppcp-axo-block/services.php +++ b/modules/ppcp-axo-block/services.php @@ -37,6 +37,7 @@ $container->get( 'wcgateway.settings' ), $container->get( 'wcgateway.configuration.dcc' ), $container->get( 'onboarding.environment' ), + $container->get( 'axo.payment_method_selected_map' ), $container->get( 'wcgateway.url' ) ); }, diff --git a/modules/ppcp-axo-block/src/AxoBlockModule.php b/modules/ppcp-axo-block/src/AxoBlockModule.php index 2a7ee73b6..1ebb068ed 100644 --- a/modules/ppcp-axo-block/src/AxoBlockModule.php +++ b/modules/ppcp-axo-block/src/AxoBlockModule.php @@ -134,11 +134,11 @@ static function () use ( $c ) { } ); - // Enqueue the PayPal Insights script + // Enqueue the PayPal Insights script. add_action( 'wp_enqueue_scripts', - function () use ($c) { - $this->enqueue_paypal_insights_script($c); + function () use ( $c ) { + $this->enqueue_paypal_insights_script( $c ); } ); @@ -183,7 +183,7 @@ private function add_sdk_client_token_to_script_data( * @return void */ private function enqueue_paypal_insights_script( ContainerInterface $c ): void { - if ( ! has_block( 'woocommerce/checkout' ) ) { + if ( ! has_block( 'woocommerce/checkout' ) || WC()->cart->is_empty() ) { return; } @@ -192,7 +192,7 @@ private function enqueue_paypal_insights_script( ContainerInterface $c ): void { wp_register_script( 'wc-ppcp-paypal-insights', - untrailingslashit( $module_url ) . '/resources/js/plugins/PayPalInsights.js', + untrailingslashit( $module_url ) . '/assets/js/PayPalInsightsLoader.js', array( 'wp-plugins', 'wp-data', 'wp-element', 'wc-blocks-registry' ), $asset_version, true diff --git a/modules/ppcp-axo-block/src/AxoBlockPaymentMethod.php b/modules/ppcp-axo-block/src/AxoBlockPaymentMethod.php index 136db8233..bee0c68fd 100644 --- a/modules/ppcp-axo-block/src/AxoBlockPaymentMethod.php +++ b/modules/ppcp-axo-block/src/AxoBlockPaymentMethod.php @@ -72,6 +72,13 @@ class AxoBlockPaymentMethod extends AbstractPaymentMethodType { */ private $environment; + /** + * Mapping of payment methods to the PayPal Insights 'payment_method_selected' types. + * + * @var array + */ + private array $payment_method_selected_map; + /** * The WcGateway module URL. * @@ -90,6 +97,7 @@ class AxoBlockPaymentMethod extends AbstractPaymentMethodType { * @param Settings $settings The settings. * @param DCCGatewayConfiguration $dcc_configuration The DCC gateway settings. * @param Environment $environment The environment object. + * @param array $payment_method_selected_map Mapping of payment methods to the PayPal Insights 'payment_method_selected' types. * @param string $wcgateway_module_url The WcGateway module URL. */ public function __construct( @@ -100,17 +108,19 @@ public function __construct( Settings $settings, DCCGatewayConfiguration $dcc_configuration, Environment $environment, + array $payment_method_selected_map, string $wcgateway_module_url ) { - $this->name = AxoGateway::ID; - $this->module_url = $module_url; - $this->version = $version; - $this->gateway = $gateway; - $this->smart_button = $smart_button; - $this->settings = $settings; - $this->dcc_configuration = $dcc_configuration; - $this->environment = $environment; - $this->wcgateway_module_url = $wcgateway_module_url; + $this->name = AxoGateway::ID; + $this->module_url = $module_url; + $this->version = $version; + $this->gateway = $gateway; + $this->smart_button = $smart_button; + $this->settings = $settings; + $this->dcc_configuration = $dcc_configuration; + $this->environment = $environment; + $this->payment_method_selected_map = $payment_method_selected_map; + $this->wcgateway_module_url = $wcgateway_module_url; } @@ -194,18 +204,19 @@ private function script_data() : array { 'email' => 'render', ), 'insights' => array( - 'enabled' => defined( 'WP_DEBUG' ) && WP_DEBUG, - 'client_id' => ( $this->settings->has( 'client_id' ) ? $this->settings->get( 'client_id' ) : null ), - 'session_id' => + 'enabled' => defined( 'WP_DEBUG' ) && WP_DEBUG, + 'client_id' => ( $this->settings->has( 'client_id' ) ? $this->settings->get( 'client_id' ) : null ), + 'session_id' => ( WC()->session && method_exists( WC()->session, 'get_customer_unique_id' ) ) ? substr( md5( WC()->session->get_customer_unique_id() ), 0, 16 ) : '', - 'amount' => array( + 'amount' => array( 'currency_code' => get_woocommerce_currency(), 'value' => ( WC()->cart && method_exists( WC()->cart, 'get_total' ) ) ? WC()->cart->get_total( 'numeric' ) - : null, // Set to null if WC()->cart is null or get_total doesn't exist. + : null, ), + 'payment_method_selected_map' => $this->payment_method_selected_map, ), 'style_options' => array( 'root' => array( diff --git a/modules/ppcp-axo-block/webpack.config.js b/modules/ppcp-axo-block/webpack.config.js index b5db53234..86e2e2087 100644 --- a/modules/ppcp-axo-block/webpack.config.js +++ b/modules/ppcp-axo-block/webpack.config.js @@ -9,7 +9,10 @@ module.exports = { target: 'web', plugins: [ new DependencyExtractionWebpackPlugin() ], entry: { - 'index': path.resolve( './resources/js/index.js' ), + index: path.resolve( './resources/js/index.js' ), + PayPalInsightsLoader: path.resolve( + './resources/js/plugins/PayPalInsightsLoader.js' + ), gateway: path.resolve( './resources/css/gateway.scss' ), }, output: { diff --git a/modules/ppcp-axo/resources/js/AxoManager.js b/modules/ppcp-axo/resources/js/AxoManager.js index 0bd204672..68667532a 100644 --- a/modules/ppcp-axo/resources/js/AxoManager.js +++ b/modules/ppcp-axo/resources/js/AxoManager.js @@ -53,7 +53,7 @@ class AxoManager { cardView = null; constructor( namespace, axoConfig, ppcpConfig ) { - this.namespace = namespace; + this.namespace = namespace; this.axoConfig = axoConfig; this.ppcpConfig = ppcpConfig; @@ -116,7 +116,7 @@ class AxoManager { this.axoConfig?.insights?.session_id ) { PayPalInsights.config( this.axoConfig?.insights?.client_id, { - debug: true, + debug: axoConfig?.wp_debug === '1', } ); PayPalInsights.setSessionId( this.axoConfig?.insights?.session_id ); PayPalInsights.trackJsLoad(); @@ -159,19 +159,25 @@ class AxoManager { } registerEventHandlers() { + // Payment method change tracking with duplicate prevention + let lastSelectedPaymentMethod = document.querySelector( + 'input[name=payment_method]:checked' + )?.value; this.$( document ).on( 'change', 'input[name=payment_method]', ( ev ) => { - const map = { - 'ppcp-axo-gateway': 'card', - 'ppcp-gateway': 'paypal', - }; - - PayPalInsights.trackSelectPaymentMethod( { - payment_method_selected: map[ ev.target.value ] || 'other', - page_type: 'checkout', - } ); + if ( lastSelectedPaymentMethod !== ev.target.value ) { + PayPalInsights.trackSelectPaymentMethod( { + payment_method_selected: + this.axoConfig?.insights + ?.payment_method_selected_map[ + ev.target.value + ] || 'other', + page_type: 'checkout', + } ); + lastSelectedPaymentMethod = ev.target.value; + } } ); @@ -1155,16 +1161,6 @@ class AxoManager { this.el.axoNonceInput.get().value = nonce; - PayPalInsights.trackEndCheckout( { - amount: this.axoConfig?.insights?.amount, - page_type: 'checkout', - payment_method_selected: 'card', - user_data: { - country: 'US', - is_store_member: false, - }, - } ); - if ( data ) { // Ryan flow. const form = document.querySelector( 'form.woocommerce-checkout' ); diff --git a/modules/ppcp-axo/resources/js/Insights/EndCheckoutTracker.js b/modules/ppcp-axo/resources/js/Insights/EndCheckoutTracker.js new file mode 100644 index 000000000..e21a9d0e4 --- /dev/null +++ b/modules/ppcp-axo/resources/js/Insights/EndCheckoutTracker.js @@ -0,0 +1,99 @@ +import PayPalInsights from '../../../../ppcp-axo/resources/js/Insights/PayPalInsights'; + +class EndCheckoutTracker { + constructor() { + this.initialize(); + } + + async initialize() { + const axoConfig = window.wc_ppcp_axo_insights_data || {}; + + if ( + axoConfig?.enabled === '1' && + axoConfig?.client_id && + axoConfig?.session_id && + axoConfig?.orderTotal && + axoConfig?.orderCurrency + ) { + try { + await this.waitForPayPalInsight(); + + PayPalInsights.config( axoConfig?.client_id, { + debug: axoConfig?.wp_debug === '1', + } ); + PayPalInsights.setSessionId( axoConfig.session_id ); + PayPalInsights.trackJsLoad(); + + const trackingData = { + amount: { + currency_code: axoConfig?.orderCurrency, + value: axoConfig?.orderTotal, + }, + page_type: 'checkout', + payment_method_selected: + axoConfig?.payment_method_selected_map[ + axoConfig?.paymentMethod + ] || 'other', + user_data: { + country: 'US', + is_store_member: false, + }, + order_id: axoConfig?.orderId, + order_key: axoConfig?.orderKey, + }; + + PayPalInsights.trackEndCheckout( trackingData ); + } catch ( error ) { + console.error( + 'EndCheckoutTracker: Error during tracking:', + error + ); + console.error( 'PayPalInsights object:', window.paypalInsight ); + } + } else { + console.warn( + 'EndCheckoutTracker: Missing required configuration', + { + enabled: axoConfig?.enabled, + hasClientId: !! axoConfig?.client_id, + hasSessionId: !! axoConfig?.session_id, + hasOrderTotal: !! axoConfig?.orderTotal, + hasOrderCurrency: !! axoConfig?.orderCurrency, + } + ); + } + } + + waitForPayPalInsight() { + return new Promise( ( resolve, reject ) => { + // If already loaded, resolve immediately + if ( window.paypalInsight ) { + resolve( window.paypalInsight ); + return; + } + + const timeoutId = setTimeout( () => { + observer.disconnect(); + reject( new Error( 'PayPal Insights script load timeout' ) ); + }, 10000 ); + + // Create MutationObserver to watch for script initialization + const observer = new MutationObserver( () => { + if ( window.paypalInsight ) { + observer.disconnect(); + clearTimeout( timeoutId ); + resolve( window.paypalInsight ); + } + } ); + + observer.observe( document, { + childList: true, + subtree: true, + } ); + } ); + } +} + +document.addEventListener( 'DOMContentLoaded', () => { + new EndCheckoutTracker(); +} ); diff --git a/modules/ppcp-axo/services.php b/modules/ppcp-axo/services.php index ebeb0f394..e92cbe3c9 100644 --- a/modules/ppcp-axo/services.php +++ b/modules/ppcp-axo/services.php @@ -18,6 +18,7 @@ use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayPalGateway; use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings; use WooCommerce\PayPalCommerce\WcGateway\Helper\DCCGatewayConfiguration; +use WooCommerce\PayPalCommerce\ApiClient\Helper\CurrencyGetter; return array( @@ -64,6 +65,7 @@ $container->get( 'session.handler' ), $container->get( 'wcgateway.settings' ), $container->get( 'onboarding.environment' ), + $container->get( 'axo.insights' ), $container->get( 'wcgateway.settings.status' ), $container->get( 'api.shop.currency.getter' ), $container->get( 'woocommerce.logger.woocommerce' ), @@ -89,6 +91,52 @@ ); }, + // Data needed for the PayPal Insights. + 'axo.insights' => static function ( ContainerInterface $container ): array { + $settings = $container->get( 'wcgateway.settings' ); + assert( $settings instanceof Settings ); + + $currency = $container->get( 'api.shop.currency.getter' ); + assert( $currency instanceof CurrencyGetter ); + + return array( + 'enabled' => defined( 'WP_DEBUG' ) && WP_DEBUG, + 'client_id' => ( $settings->has( 'client_id' ) ? $settings->get( 'client_id' ) : null ), + 'session_id' => substr( + method_exists( WC()->session, 'get_customer_unique_id' ) ? + md5( WC()->session->get_customer_unique_id() ) : + '', + 0, + 16 + ), + 'amount' => array( + 'currency_code' => $currency->get(), + ), + 'payment_method_selected_map' => $container->get( 'axo.payment_method_selected_map' ), + 'wp_debug' => defined( 'WP_DEBUG' ) && WP_DEBUG, + ); + }, + + // The mapping of payment methods to the PayPal Insights 'payment_method_selected' types. + 'axo.payment_method_selected_map' => static function ( ContainerInterface $container ): array { + return array( + 'ppcp-axo-gateway' => 'card', + 'ppcp-credit-card-gateway' => 'card', + 'ppcp-gateway' => 'paypal', + 'ppcp-googlepay' => 'google_pay', + 'ppcp-applepay' => 'apple_pay', + 'ppcp-multibanco' => 'other', + 'ppcp-trustly' => 'other', + 'ppcp-p24' => 'other', + 'ppcp-mybank' => 'other', + 'ppcp-ideal' => 'other', + 'ppcp-eps' => 'other', + 'ppcp-blik' => 'other', + 'ppcp-bancontact' => 'other', + 'ppcp-card-button-gateway' => 'card', + ); + }, + /** * The matrix which countries and currency combinations can be used for AXO. */ diff --git a/modules/ppcp-axo/src/Assets/AxoManager.php b/modules/ppcp-axo/src/Assets/AxoManager.php index 6fa196719..dbc7c5920 100644 --- a/modules/ppcp-axo/src/Assets/AxoManager.php +++ b/modules/ppcp-axo/src/Assets/AxoManager.php @@ -52,6 +52,13 @@ class AxoManager { */ private $environment; + /** + * Data needed for the PayPal Insights. + * + * @var array + */ + private array $insights_data; + /** * The Settings status helper. * @@ -95,6 +102,7 @@ class AxoManager { * @param SessionHandler $session_handler The Session handler. * @param Settings $settings The Settings. * @param Environment $environment The environment object. + * @param array $insights_data Data needed for the PayPal Insights. * @param SettingsStatus $settings_status The Settings status helper. * @param CurrencyGetter $currency The getter of the 3-letter currency code of the shop. * @param LoggerInterface $logger The logger. @@ -106,6 +114,7 @@ public function __construct( SessionHandler $session_handler, Settings $settings, Environment $environment, + array $insights_data, SettingsStatus $settings_status, CurrencyGetter $currency, LoggerInterface $logger, @@ -117,6 +126,7 @@ public function __construct( $this->session_handler = $session_handler; $this->settings = $settings; $this->environment = $environment; + $this->insights_data = $insights_data; $this->settings_status = $settings_status; $this->currency = $currency; $this->logger = $logger; @@ -161,7 +171,7 @@ public function enqueue() { * * @return array */ - private function script_data() { + private function script_data(): array { return array( 'environment' => array( 'is_sandbox' => $this->environment->current_environment() === 'sandbox', @@ -169,20 +179,10 @@ private function script_data() { 'widgets' => array( 'email' => 'render', ), - 'insights' => array( - 'enabled' => defined( 'WP_DEBUG' ) && WP_DEBUG, - 'client_id' => ( $this->settings->has( 'client_id' ) ? $this->settings->get( 'client_id' ) : null ), - 'session_id' => - substr( - method_exists( WC()->session, 'get_customer_unique_id' ) ? md5( WC()->session->get_customer_unique_id() ) : '', - 0, - 16 - ), - 'amount' => array( - 'currency_code' => $this->currency->get(), - 'value' => WC()->cart->get_total( 'numeric' ), - ), - ), + // The amount is not available when setting the insights data, so we need to merge it here. + 'insights' => ( function( array $data ): array { + $data['amount']['value'] = WC()->cart->get_total( 'numeric' ); + return $data; } )( $this->insights_data ), 'style_options' => array( 'root' => array( 'backgroundColor' => $this->settings->has( 'axo_style_root_bg_color' ) ? $this->settings->get( 'axo_style_root_bg_color' ) : '', diff --git a/modules/ppcp-axo/src/AxoModule.php b/modules/ppcp-axo/src/AxoModule.php index a0470aeac..c8c316780 100644 --- a/modules/ppcp-axo/src/AxoModule.php +++ b/modules/ppcp-axo/src/AxoModule.php @@ -318,6 +318,15 @@ static function () use ( $c ) { $endpoint->handle_request(); } ); + + // Enqueue the PayPal Insights script. + add_action( + 'wp_enqueue_scripts', + function () use ( $c ) { + $this->enqueue_paypal_insights_script_on_order_received( $c ); + } + ); + return true; } @@ -429,8 +438,8 @@ function () { * @return bool */ private function is_excluded_endpoint(): bool { - // Exclude the Order Pay endpoint. - return is_wc_endpoint_url( 'order-pay' ); + // Exclude the Order Pay and Order Received endpoints. + return is_wc_endpoint_url( 'order-pay' ) || is_wc_endpoint_url( 'order-received' ); } /** @@ -451,4 +460,57 @@ private function add_feature_detection_tag( bool $axo_enabled ) { $axo_enabled ? 'enabled' : 'disabled' ); } + + /** + * Enqueues PayPal Insights on the Order Received endpoint. + * + * @param ContainerInterface $c The service container. + * @return void + */ + private function enqueue_paypal_insights_script_on_order_received( ContainerInterface $c ): void { + global $wp; + + if ( ! isset( $wp->query_vars['order-received'] ) ) { + return; + } + + $order_id = absint( $wp->query_vars['order-received'] ); + if ( ! $order_id ) { + return; + } + + $order = wc_get_order( $order_id ); + if ( ! $order || ! $order instanceof \WC_Order ) { + return; + } + + $module_url = $c->get( 'axo.url' ); + $asset_version = $c->get( 'ppcp.asset-version' ); + $insights_data = $c->get( 'axo.insights' ); + + wp_register_script( + 'wc-ppcp-paypal-insights-end-checkout', + untrailingslashit( $module_url ) . '/assets/js/TrackEndCheckout.js', + array( 'wp-plugins', 'wp-data', 'wp-element', 'wc-blocks-registry' ), + $asset_version, + true + ); + + wp_localize_script( + 'wc-ppcp-paypal-insights-end-checkout', + 'wc_ppcp_axo_insights_data', + array_merge( + $insights_data, + array( + 'orderId' => $order_id, + 'orderTotal' => (string) $order->get_total(), + 'orderCurrency' => (string) $order->get_currency(), + 'paymentMethod' => (string) $order->get_payment_method(), + 'orderKey' => (string) $order->get_order_key(), + ) + ) + ); + + wp_enqueue_script( 'wc-ppcp-paypal-insights-end-checkout' ); + } } diff --git a/modules/ppcp-axo/webpack.config.js b/modules/ppcp-axo/webpack.config.js index 95c7f0fc6..1af3ed0bc 100644 --- a/modules/ppcp-axo/webpack.config.js +++ b/modules/ppcp-axo/webpack.config.js @@ -1,39 +1,44 @@ -const path = require('path'); +const path = require( 'path' ); const isProduction = process.env.NODE_ENV === 'production'; const DependencyExtractionWebpackPlugin = require( '@woocommerce/dependency-extraction-webpack-plugin' ); module.exports = { - devtool: isProduction ? 'source-map' : 'eval-source-map', - mode: isProduction ? 'production' : 'development', - target: 'web', - plugins: [ new DependencyExtractionWebpackPlugin() ], - entry: { - 'boot': path.resolve('./resources/js/boot.js'), - 'styles': path.resolve('./resources/css/styles.scss') - }, - output: { - path: path.resolve(__dirname, 'assets/'), - filename: 'js/[name].js', - }, - module: { - rules: [{ - test: /\.js?$/, - exclude: /node_modules/, - loader: 'babel-loader', - }, - { - test: /\.scss$/, - exclude: /node_modules/, - use: [ - { - loader: 'file-loader', - options: { - name: 'css/[name].css', - } - }, - {loader:'sass-loader'} - ] - }] - } + devtool: isProduction ? 'source-map' : 'eval-source-map', + mode: isProduction ? 'production' : 'development', + target: 'web', + plugins: [ new DependencyExtractionWebpackPlugin() ], + entry: { + boot: path.resolve( './resources/js/boot.js' ), + styles: path.resolve( './resources/css/styles.scss' ), + TrackEndCheckout: path.resolve( + './resources/js/Insights/TrackEndCheckout.js' + ), + }, + output: { + path: path.resolve( __dirname, 'assets/' ), + filename: 'js/[name].js', + }, + module: { + rules: [ + { + test: /\.js?$/, + exclude: /node_modules/, + loader: 'babel-loader', + }, + { + test: /\.scss$/, + exclude: /node_modules/, + use: [ + { + loader: 'file-loader', + options: { + name: 'css/[name].css', + }, + }, + { loader: 'sass-loader' }, + ], + }, + ], + }, }; From 4fe7117fc91320a1817286672e2bba6c4af883f2 Mon Sep 17 00:00:00 2001 From: Daniel Dudzic <d.dudzic@syde.com> Date: Wed, 13 Nov 2024 10:50:35 +0100 Subject: [PATCH 3/7] Fix PHPCS errors --- .../src/AxoBlockPaymentMethod.php | 40 +++++++++---------- modules/ppcp-axo/src/Assets/AxoManager.php | 22 +++++----- 2 files changed, 31 insertions(+), 31 deletions(-) diff --git a/modules/ppcp-axo-block/src/AxoBlockPaymentMethod.php b/modules/ppcp-axo-block/src/AxoBlockPaymentMethod.php index 6e1107698..f1ed91527 100644 --- a/modules/ppcp-axo-block/src/AxoBlockPaymentMethod.php +++ b/modules/ppcp-axo-block/src/AxoBlockPaymentMethod.php @@ -96,16 +96,16 @@ class AxoBlockPaymentMethod extends AbstractPaymentMethodType { /** * AdvancedCardPaymentMethod constructor. * - * @param string $module_url The URL of this module. - * @param string $version The assets version. - * @param WC_Payment_Gateway $gateway Credit card gateway. - * @param SmartButtonInterface|callable $smart_button The smart button script loading - * handler. - * @param Settings $settings The settings. - * @param DCCGatewayConfiguration $dcc_configuration The DCC gateway settings. - * @param Environment $environment The environment object. + * @param string $module_url The URL of this module. + * @param string $version The assets version. + * @param WC_Payment_Gateway $gateway Credit card gateway. + * @param SmartButtonInterface|callable $smart_button The smart button script loading + * handler. + * @param Settings $settings The settings. + * @param DCCGatewayConfiguration $dcc_configuration The DCC gateway settings. + * @param Environment $environment The environment object. + * @param string $wcgateway_module_url The WcGateway module URL. * @param array $payment_method_selected_map Mapping of payment methods to the PayPal Insights 'payment_method_selected' types. - * @param string $wcgateway_module_url The WcGateway module URL. * @param array $enabled_shipping_locations The list of WooCommerce enabled shipping locations. */ public function __construct( @@ -120,17 +120,17 @@ public function __construct( array $payment_method_selected_map, array $enabled_shipping_locations ) { - $this->name = AxoGateway::ID; - $this->module_url = $module_url; - $this->version = $version; - $this->gateway = $gateway; - $this->smart_button = $smart_button; - $this->settings = $settings; - $this->dcc_configuration = $dcc_configuration; - $this->environment = $environment; - $this->wcgateway_module_url = $wcgateway_module_url; + $this->name = AxoGateway::ID; + $this->module_url = $module_url; + $this->version = $version; + $this->gateway = $gateway; + $this->smart_button = $smart_button; + $this->settings = $settings; + $this->dcc_configuration = $dcc_configuration; + $this->environment = $environment; + $this->wcgateway_module_url = $wcgateway_module_url; $this->payment_method_selected_map = $payment_method_selected_map; - $this->enabled_shipping_locations = $enabled_shipping_locations; + $this->enabled_shipping_locations = $enabled_shipping_locations; } /** @@ -212,7 +212,7 @@ private function script_data() : array { 'widgets' => array( 'email' => 'render', ), - 'insights' => array( + 'insights' => array( 'enabled' => defined( 'WP_DEBUG' ) && WP_DEBUG, 'client_id' => ( $this->settings->has( 'client_id' ) ? $this->settings->get( 'client_id' ) : null ), 'session_id' => diff --git a/modules/ppcp-axo/src/Assets/AxoManager.php b/modules/ppcp-axo/src/Assets/AxoManager.php index 9ee15ffaa..8b9a59aeb 100644 --- a/modules/ppcp-axo/src/Assets/AxoManager.php +++ b/modules/ppcp-axo/src/Assets/AxoManager.php @@ -130,16 +130,16 @@ public function __construct( array $enabled_shipping_locations ) { - $this->module_url = $module_url; - $this->version = $version; - $this->session_handler = $session_handler; - $this->settings = $settings; - $this->environment = $environment; - $this->insights_data = $insights_data; - $this->settings_status = $settings_status; - $this->currency = $currency; - $this->logger = $logger; - $this->wcgateway_module_url = $wcgateway_module_url; + $this->module_url = $module_url; + $this->version = $version; + $this->session_handler = $session_handler; + $this->settings = $settings; + $this->environment = $environment; + $this->insights_data = $insights_data; + $this->settings_status = $settings_status; + $this->currency = $currency; + $this->logger = $logger; + $this->wcgateway_module_url = $wcgateway_module_url; $this->enabled_shipping_locations = $enabled_shipping_locations; } @@ -190,7 +190,7 @@ private function script_data(): array { 'email' => 'render', ), // The amount is not available when setting the insights data, so we need to merge it here. - 'insights' => ( function( array $data ): array { + 'insights' => ( function( array $data ): array { $data['amount']['value'] = WC()->cart->get_total( 'numeric' ); return $data; } )( $this->insights_data ), 'enabled_shipping_locations' => $this->enabled_shipping_locations, From e455561eb8bad3116f0e965634246f07faf6ff81 Mon Sep 17 00:00:00 2001 From: Daniel Dudzic <d.dudzic@syde.com> Date: Wed, 13 Nov 2024 11:24:31 +0100 Subject: [PATCH 4/7] Fix parameters being passed in the wrong order --- modules/ppcp-axo-block/services.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/ppcp-axo-block/services.php b/modules/ppcp-axo-block/services.php index 3d899ac03..810634586 100644 --- a/modules/ppcp-axo-block/services.php +++ b/modules/ppcp-axo-block/services.php @@ -37,8 +37,8 @@ $container->get( 'wcgateway.settings' ), $container->get( 'wcgateway.configuration.dcc' ), $container->get( 'onboarding.environment' ), - $container->get( 'axo.payment_method_selected_map' ), $container->get( 'wcgateway.url' ), + $container->get( 'axo.payment_method_selected_map' ), $container->get( 'axo.shipping-wc-enabled-locations' ) ); }, From 216e8056158395b56ef0858e6df27cdb9327874f Mon Sep 17 00:00:00 2001 From: Daniel Dudzic <d.dudzic@syde.com> Date: Fri, 15 Nov 2024 20:00:33 +0100 Subject: [PATCH 5/7] Fix the EndCheckoutTracker.js file name in the webpack config file --- modules/ppcp-axo/webpack.config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/ppcp-axo/webpack.config.js b/modules/ppcp-axo/webpack.config.js index 1af3ed0bc..e8638b564 100644 --- a/modules/ppcp-axo/webpack.config.js +++ b/modules/ppcp-axo/webpack.config.js @@ -12,7 +12,7 @@ module.exports = { boot: path.resolve( './resources/js/boot.js' ), styles: path.resolve( './resources/css/styles.scss' ), TrackEndCheckout: path.resolve( - './resources/js/Insights/TrackEndCheckout.js' + './resources/js/Insights/EndCheckoutTracker.js' ), }, output: { From f79fb048463787c4118013ae3fee701d68fea9c2 Mon Sep 17 00:00:00 2001 From: Daniel Dudzic <d.dudzic@syde.com> Date: Mon, 18 Nov 2024 20:01:00 +0100 Subject: [PATCH 6/7] Ensure the WC session exists before trying to access it --- modules/ppcp-axo/services.php | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/modules/ppcp-axo/services.php b/modules/ppcp-axo/services.php index db38334b0..9021f7088 100644 --- a/modules/ppcp-axo/services.php +++ b/modules/ppcp-axo/services.php @@ -100,16 +100,19 @@ $currency = $container->get( 'api.shop.currency.getter' ); assert( $currency instanceof CurrencyGetter ); + $session_id = ''; + if ( isset( WC()->session ) && method_exists( WC()->session, 'get_customer_unique_id' ) ) { + $session_id = substr( + md5( WC()->session->get_customer_unique_id() ), + 0, + 16 + ); + } + return array( 'enabled' => defined( 'WP_DEBUG' ) && WP_DEBUG, 'client_id' => ( $settings->has( 'client_id' ) ? $settings->get( 'client_id' ) : null ), - 'session_id' => substr( - method_exists( WC()->session, 'get_customer_unique_id' ) ? - md5( WC()->session->get_customer_unique_id() ) : - '', - 0, - 16 - ), + 'session_id' => $session_id, 'amount' => array( 'currency_code' => $currency->get(), ), From 55d98fdeb380b66b692be621c30af1e80d1b8ffe Mon Sep 17 00:00:00 2001 From: Daniel Dudzic <d.dudzic@syde.com> Date: Tue, 19 Nov 2024 11:23:38 +0100 Subject: [PATCH 7/7] PHPCS fixes --- modules/ppcp-axo-block/services.php | 22 +++++++++++----------- modules/ppcp-axo/src/Assets/AxoManager.php | 22 +++++++++++----------- 2 files changed, 22 insertions(+), 22 deletions(-) diff --git a/modules/ppcp-axo-block/services.php b/modules/ppcp-axo-block/services.php index 2e728ff58..f91cd738a 100644 --- a/modules/ppcp-axo-block/services.php +++ b/modules/ppcp-axo-block/services.php @@ -30,17 +30,17 @@ }, 'axoblock.method' => static function ( ContainerInterface $container ) : AxoBlockPaymentMethod { return new AxoBlockPaymentMethod( - $container->get('axoblock.url'), - $container->get('ppcp.asset-version'), - $container->get('axo.gateway'), - fn(): SmartButtonInterface => $container->get('button.smart-button'), - $container->get('wcgateway.settings'), - $container->get('wcgateway.configuration.dcc'), - $container->get('onboarding.environment'), - $container->get('wcgateway.url'), - $container->get('axo.payment_method_selected_map'), - $container->get('axo.supported-country-card-type-matrix'), - $container->get('axo.shipping-wc-enabled-locations') + $container->get( 'axoblock.url' ), + $container->get( 'ppcp.asset-version' ), + $container->get( 'axo.gateway' ), + fn(): SmartButtonInterface => $container->get( 'button.smart-button' ), + $container->get( 'wcgateway.settings' ), + $container->get( 'wcgateway.configuration.dcc' ), + $container->get( 'onboarding.environment' ), + $container->get( 'wcgateway.url' ), + $container->get( 'axo.payment_method_selected_map' ), + $container->get( 'axo.supported-country-card-type-matrix' ), + $container->get( 'axo.shipping-wc-enabled-locations' ) ); }, ); diff --git a/modules/ppcp-axo/src/Assets/AxoManager.php b/modules/ppcp-axo/src/Assets/AxoManager.php index 37e188072..6fafcb681 100644 --- a/modules/ppcp-axo/src/Assets/AxoManager.php +++ b/modules/ppcp-axo/src/Assets/AxoManager.php @@ -137,17 +137,17 @@ public function __construct( array $enabled_shipping_locations ) { - $this->module_url = $module_url; - $this->version = $version; - $this->session_handler = $session_handler; - $this->settings = $settings; - $this->environment = $environment; - $this->insights_data = $insights_data; - $this->settings_status = $settings_status; - $this->currency = $currency; - $this->logger = $logger; - $this->wcgateway_module_url = $wcgateway_module_url; - $this->enabled_shipping_locations = $enabled_shipping_locations; + $this->module_url = $module_url; + $this->version = $version; + $this->session_handler = $session_handler; + $this->settings = $settings; + $this->environment = $environment; + $this->insights_data = $insights_data; + $this->settings_status = $settings_status; + $this->currency = $currency; + $this->logger = $logger; + $this->wcgateway_module_url = $wcgateway_module_url; + $this->enabled_shipping_locations = $enabled_shipping_locations; $this->supported_country_card_type_matrix = $supported_country_card_type_matrix; }