diff --git a/modules/ppcp-axo-block/resources/js/hooks/useAxoSetup.js b/modules/ppcp-axo-block/resources/js/hooks/useAxoSetup.js index 629ce05d4..2a8c4a5ff 100644 --- a/modules/ppcp-axo-block/resources/js/hooks/useAxoSetup.js +++ b/modules/ppcp-axo-block/resources/js/hooks/useAxoSetup.js @@ -11,6 +11,7 @@ import { snapshotFields } from '../helpers/fieldHelpers'; import useCustomerData from './useCustomerData'; import useShippingAddressChange from './useShippingAddressChange'; import useCardChange from './useCardChange'; +import useSessionRestoration from './useSessionRestoration'; /** * Custom hook to set up AXO functionality. @@ -63,6 +64,9 @@ const useAxoSetup = ( // Set up phone sync handler usePhoneSyncHandler( paymentComponent ); + // Set up session restoration + useSessionRestoration( fastlaneSdk ); + // Initialize class toggles on mount useEffect( () => { initializeClassToggles(); @@ -104,6 +108,7 @@ const useAxoSetup = ( setShippingAddress, setCardDetails, paymentComponent, + setCardChangeHandler, ] ); return paypalLoaded; diff --git a/modules/ppcp-axo-block/resources/js/hooks/useSessionRestoration.js b/modules/ppcp-axo-block/resources/js/hooks/useSessionRestoration.js new file mode 100644 index 000000000..9c0fa4565 --- /dev/null +++ b/modules/ppcp-axo-block/resources/js/hooks/useSessionRestoration.js @@ -0,0 +1,87 @@ +import { useEffect, useRef } from '@wordpress/element'; +import { useDispatch } from '@wordpress/data'; +import { setIsEmailLookupCompleted, STORE_NAME } from '../stores/axoStore'; +import { log } from '../../../../ppcp-axo/resources/js/Helper/Debug'; + +/** + * Hook to restore Fastlane session after payment failures using triggerAuthenticationFlow + * Only runs when ppcp_fastlane_error=1 URL parameter is present + * @param {Object} fastlaneSdk - The Fastlane SDK instance + */ +const useSessionRestoration = ( fastlaneSdk ) => { + const { setShippingAddress, setCardDetails, setIsGuest } = + useDispatch( STORE_NAME ); + const hasProcessed = useRef( false ); + + useEffect( () => { + if ( ! fastlaneSdk || hasProcessed.current ) { + return; + } + + const urlParams = new URLSearchParams( window.location.search ); + const hasErrorParam = urlParams.get( 'ppcp_fastlane_error' ) === '1'; + + if ( ! hasErrorParam ) { + return; + } + + // Remove the error parameter from URL + urlParams.delete( 'ppcp_fastlane_error' ); + const newUrl = new URL( window.location ); + newUrl.search = urlParams.toString(); + window.history.replaceState( {}, '', newUrl ); + + hasProcessed.current = true; + + const restoreSession = async () => { + try { + const emailInput = document.getElementById( 'email' ); + + if ( emailInput?.value ) { + const lookupResult = + await fastlaneSdk.identity.lookupCustomerByEmail( + emailInput.value + ); + + wp.data.dispatch( STORE_NAME ).setIsEmailSubmitted( true ); + + if ( lookupResult?.customerContextId ) { + const customerContextId = + lookupResult.customerContextId; + + const authenticatedCustomerResult = + await fastlaneSdk.identity.triggerAuthenticationFlow( + customerContextId + ); + + if ( + authenticatedCustomerResult?.authenticationState === + 'succeeded' + ) { + const { profileData } = authenticatedCustomerResult; + setIsGuest( false ); + + if ( profileData?.shippingAddress ) { + setShippingAddress( + profileData.shippingAddress + ); + } + + if ( profileData?.card ) { + setCardDetails( profileData.card ); + } + + setIsEmailLookupCompleted( true ); + } + } + } + } catch ( error ) { + log( 'Failed to restore Fastlane session', 'warn' ); + } + }; + + restoreSession(); + }, [ fastlaneSdk, setShippingAddress, setCardDetails, setIsGuest ] ); +}; + +export default useSessionRestoration; diff --git a/modules/ppcp-axo/resources/js/AxoManager.js b/modules/ppcp-axo/resources/js/AxoManager.js index e93fa9527..1ffd8fede 100644 --- a/modules/ppcp-axo/resources/js/AxoManager.js +++ b/modules/ppcp-axo/resources/js/AxoManager.js @@ -60,6 +60,8 @@ class AxoManager { this.fastlane = new Fastlane( namespace ); this.$ = jQuery; + this.hasProcessedSessionRestore = false; + this.status = { active: false, validEmail: false, @@ -529,7 +531,15 @@ class AxoManager { log( `this.lastEmailCheckedIdentity: ${ this.lastEmailCheckedIdentity }` ); - if ( + + const urlParams = new URLSearchParams( window.location.search ); + const hasErrorParam = urlParams.get( 'ppcp_fastlane_error' ) === '1'; + + if ( hasErrorParam ) { + log( + 'Payment failure detected, session restoration will be attempted' + ); + } else if ( this.emailInput && this.lastEmailCheckedIdentity !== this.emailInput.value ) { @@ -662,6 +672,8 @@ class AxoManager { await this.renderWatermark(); this.renderEmailSubmitButton(); this.watchEmail(); + + await this.restoreSessionAfterFailure(); } async connect() { @@ -1383,6 +1395,94 @@ class AxoManager { this.$( '#billing_email_field input' ).on( 'input', reEnableInput ); this.$( '#billing_email_field input' ).on( 'click', reEnableInput ); } + + async restoreSessionAfterFailure() { + if ( ! this.fastlane || this.hasProcessedSessionRestore ) { + return; + } + + const urlParams = new URLSearchParams( window.location.search ); + const hasErrorParam = urlParams.get( 'ppcp_fastlane_error' ) === '1'; + + if ( ! hasErrorParam ) { + return; + } + + urlParams.delete( 'ppcp_fastlane_error' ); + const newUrl = new URL( window.location ); + newUrl.search = urlParams.toString(); + window.history.replaceState( {}, '', newUrl ); + + this.hasProcessedSessionRestore = true; + + try { + if ( this.emailInput?.value ) { + log( + `Restoring Fastlane session for email: ${ this.emailInput.value }` + ); + + const lookupResult = + await this.fastlane.identity.lookupCustomerByEmail( + this.emailInput.value + ); + + if ( lookupResult?.customerContextId ) { + const authenticatedCustomerResult = + await this.fastlane.identity.triggerAuthenticationFlow( + lookupResult.customerContextId + ); + + if ( + authenticatedCustomerResult?.authenticationState === + 'succeeded' + ) { + const { profileData } = authenticatedCustomerResult; + + if ( profileData?.shippingAddress ) { + this.setShipping( profileData.shippingAddress ); + } + + if ( profileData?.card ) { + this.setCard( profileData.card ); + this.setStatus( 'hasCard', true ); + + const cardBillingAddress = + profileData.card?.paymentSource?.card + ?.billingAddress; + if ( cardBillingAddress ) { + const billingData = { + address: cardBillingAddress, + }; + + const phoneNumber = + profileData.shippingAddress?.phoneNumber + ?.nationalNumber; + if ( phoneNumber ) { + billingData.phoneNumber = phoneNumber; + } + + this.setBilling( billingData ); + } + } + + this.setStatus( 'validEmail', true ); + this.setStatus( 'hasProfile', true ); + + this.hideGatewaySelection = true; + this.$( '.wc_payment_methods label' ).hide(); + this.$( '.wc_payment_methods input' ).hide(); + + await this.renderWatermark( false ); + + log( 'Fastlane session successfully restored' ); + } + } + } + } catch ( error ) { + log( 'Failed to restore Fastlane session', 'warn' ); + console.warn( 'Fastlane session restoration error:', error ); + } + } } export default AxoManager; diff --git a/modules/ppcp-wc-gateway/src/Endpoint/ReturnUrlEndpoint.php b/modules/ppcp-wc-gateway/src/Endpoint/ReturnUrlEndpoint.php index 17df86b8c..08170494b 100644 --- a/modules/ppcp-wc-gateway/src/Endpoint/ReturnUrlEndpoint.php +++ b/modules/ppcp-wc-gateway/src/Endpoint/ReturnUrlEndpoint.php @@ -82,7 +82,7 @@ class ReturnUrlEndpoint { // phpcs:disable WordPress.Security.NonceVerification.Recommended if ( ! isset( $_GET['token'] ) ) { wc_add_notice( __( 'Payment session expired. Please try placing your order again.', 'woocommerce-paypal-payments' ), 'error' ); - wp_safe_redirect( wc_get_checkout_url() ); + wp_safe_redirect( $this->get_checkout_url_with_error() ); exit(); } $token = sanitize_text_field( wp_unslash( $_GET['token'] ) ); @@ -93,7 +93,7 @@ class ReturnUrlEndpoint { } catch ( Exception $exception ) { $this->logger->warning( "Return URL endpoint failed to fetch order $token: " . $exception->getMessage() ); wc_add_notice( __( 'Could not retrieve payment information. Please try again.', 'woocommerce-paypal-payments' ), 'error' ); - wp_safe_redirect( wc_get_checkout_url() ); + wp_safe_redirect( $this->get_checkout_url_with_error() ); exit(); } @@ -104,7 +104,7 @@ class ReturnUrlEndpoint { } catch ( Exception $e ) { $this->logger->warning( "3DS completion failed for order $token: " . $e->getMessage() ); wc_add_notice( $this->get_3ds_error_message( $e ), 'error' ); - wp_safe_redirect( wc_get_checkout_url() ); + wp_safe_redirect( $this->get_checkout_url_with_error() ); exit(); } } @@ -128,7 +128,7 @@ class ReturnUrlEndpoint { $this->logger->warning( "Return URL endpoint $token: no WC order ID." ); wc_add_notice( __( 'Order information is missing. Please try placing your order again.', 'woocommerce-paypal-payments' ), 'error' ); - wp_safe_redirect( wc_get_checkout_url() ); + wp_safe_redirect( $this->get_checkout_url_with_error() ); exit(); } @@ -137,7 +137,7 @@ class ReturnUrlEndpoint { $this->logger->warning( "Return URL endpoint $token: WC order $wc_order_id not found." ); wc_add_notice( __( 'Order not found. Please try placing your order again.', 'woocommerce-paypal-payments' ), 'error' ); - wp_safe_redirect( wc_get_checkout_url() ); + wp_safe_redirect( $this->get_checkout_url_with_error() ); exit(); } @@ -150,7 +150,7 @@ class ReturnUrlEndpoint { $payment_gateway = $this->get_payment_gateway( $wc_order->get_payment_method() ); if ( ! $payment_gateway ) { wc_add_notice( __( 'Payment gateway is unavailable. Please try again or contact support.', 'woocommerce-paypal-payments' ), 'error' ); - wp_safe_redirect( wc_get_checkout_url() ); + wp_safe_redirect( $this->get_checkout_url_with_error() ); exit(); } @@ -170,10 +170,19 @@ class ReturnUrlEndpoint { } wc_add_notice( __( 'Payment processing failed. Please try again or contact support.', 'woocommerce-paypal-payments' ), 'error' ); - wp_safe_redirect( wc_get_checkout_url() ); + wp_safe_redirect( $this->get_checkout_url_with_error() ); exit(); } + /** + * Get checkout URL with Fastlane error parameter. + * + * @return string + */ + private function get_checkout_url_with_error(): string { + return add_query_arg( 'ppcp_fastlane_error', '1', wc_get_checkout_url() ); + } + /** * Check if order needs 3DS completion. *