Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions modules/ppcp-axo-block/resources/js/hooks/useAxoSetup.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -104,6 +108,7 @@ const useAxoSetup = (
setShippingAddress,
setCardDetails,
paymentComponent,
setCardChangeHandler,
] );

return paypalLoaded;
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
102 changes: 101 additions & 1 deletion modules/ppcp-axo/resources/js/AxoManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@ class AxoManager {
this.fastlane = new Fastlane( namespace );
this.$ = jQuery;

this.hasProcessedSessionRestore = false;

this.status = {
active: false,
validEmail: false,
Expand Down Expand Up @@ -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
) {
Expand Down Expand Up @@ -662,6 +672,8 @@ class AxoManager {
await this.renderWatermark();
this.renderEmailSubmitButton();
this.watchEmail();

await this.restoreSessionAfterFailure();
}

async connect() {
Expand Down Expand Up @@ -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;
23 changes: 16 additions & 7 deletions modules/ppcp-wc-gateway/src/Endpoint/ReturnUrlEndpoint.php
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ public function handle_request(): void {
// 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'] ) );
Expand All @@ -93,7 +93,7 @@ public function handle_request(): void {
} 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();
}

Expand All @@ -104,7 +104,7 @@ public function handle_request(): void {
} 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();
}
}
Expand All @@ -128,7 +128,7 @@ public function handle_request(): void {

$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();
}

Expand All @@ -137,7 +137,7 @@ public function handle_request(): void {
$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();
}

Expand All @@ -150,7 +150,7 @@ public function handle_request(): void {
$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();
}

Expand All @@ -170,10 +170,19 @@ function( $allowed_hosts ) : array {
}

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.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -248,7 +248,7 @@ public function init_form_fields() {
public function process_payment( $order_id ) {
$wc_order = wc_get_order( $order_id );
// phpcs:disable WordPress.Security.NonceVerification
$birth_date = wc_clean( wp_unslash( $_POST['billing_birth_date'] ?? '' ) );
$birth_date = wc_clean( wp_unslash( $_POST['billing_birth_date'] ?? '' ) );
$pay_for_order = wc_clean( wp_unslash( $_GET['pay_for_order'] ?? '' ) );
if ( 'true' === $pay_for_order ) {
if ( ! $this->checkout_helper->validate_birth_date( $birth_date ) ) {
Expand Down
Loading