mirror of
https://github.com/woocommerce/woocommerce-paypal-payments.git
synced 2025-09-01 07:02:48 +08:00
🔄 Add session-based redirect flow handling post 3ds verification
This commit is contained in:
parent
27af1020ba
commit
2804b4157d
5 changed files with 376 additions and 62 deletions
|
@ -14,6 +14,7 @@ use WooCommerce\PayPalCommerce\ApiClient\Authentication\SdkClientToken;
|
|||
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\OrderEndpoint;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Exception\PayPalApiException;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Factory\ExperienceContextBuilder;
|
||||
use WooCommerce\PayPalCommerce\Axo\Assets\AxoManager;
|
||||
use WooCommerce\PayPalCommerce\Axo\Gateway\AxoGateway;
|
||||
use WooCommerce\PayPalCommerce\Button\Assets\SmartButtonInterface;
|
||||
|
@ -398,6 +399,19 @@ class AxoModule implements ServiceModule, ExtendingModule, ExecutableModule {
|
|||
'method' => $three_d_secure,
|
||||
),
|
||||
);
|
||||
|
||||
$experience_context_builder = $c->get( 'wcgateway.builder.experience-context' );
|
||||
assert( $experience_context_builder instanceof ExperienceContextBuilder );
|
||||
|
||||
$data['experience_context'] = $experience_context_builder
|
||||
->with_endpoint_return_urls()
|
||||
->with_current_brand_name()
|
||||
->with_current_locale()
|
||||
->build()->to_array();
|
||||
|
||||
$data['transaction_context'] = array(
|
||||
'soft_descriptor' => __( 'Card verification hold', 'woocommerce-paypal-payments' ),
|
||||
);
|
||||
}
|
||||
|
||||
return $data;
|
||||
|
|
|
@ -11,13 +11,18 @@ namespace WooCommerce\PayPalCommerce\Axo\Gateway;
|
|||
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Exception;
|
||||
use WC_AJAX;
|
||||
use WC_Order;
|
||||
use WC_Payment_Gateway;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\OrderEndpoint;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Entity\PaymentSource;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Entity\OrderStatus;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Exception\PayPalApiException;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Factory\PurchaseUnitFactory;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Factory\ShippingPreferenceFactory;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Entity\Order;
|
||||
use WooCommerce\PayPalCommerce\WcGateway\Endpoint\ReturnUrlEndpoint;
|
||||
use WooCommerce\PayPalCommerce\WcGateway\Exception\PayPalOrderMissingException;
|
||||
use WooCommerce\PayPalCommerce\WcGateway\Helper\Environment;
|
||||
use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;
|
||||
use WooCommerce\PayPalCommerce\WcGateway\Gateway\GatewaySettingsRendererTrait;
|
||||
|
@ -29,6 +34,8 @@ use WooCommerce\PayPalCommerce\WcGateway\Gateway\ProcessPaymentTrait;
|
|||
use WooCommerce\PayPalCommerce\WcGateway\Exception\GatewayGenericException;
|
||||
use WooCommerce\PayPalCommerce\Session\SessionHandler;
|
||||
use WooCommerce\PayPalCommerce\WcGateway\Helper\CardPaymentsConfiguration;
|
||||
use WooCommerce\PayPalCommerce\WcGateway\Gateway\Messages;
|
||||
use DomainException;
|
||||
|
||||
/**
|
||||
* Class AXOGateway.
|
||||
|
@ -232,12 +239,65 @@ class AxoGateway extends WC_Payment_Gateway {
|
|||
$wc_order = wc_get_order( $order_id );
|
||||
|
||||
if ( ! is_a( $wc_order, WC_Order::class ) ) {
|
||||
return $this->handle_payment_failure(
|
||||
null,
|
||||
new GatewayGenericException( new Exception( 'WC order was not found.' ) )
|
||||
return array(
|
||||
'result' => 'failure',
|
||||
'message' => __( 'Order not found. Please try again.', 'woocommerce-paypal-payments' ),
|
||||
);
|
||||
}
|
||||
|
||||
// Check for stored 3DS errors.
|
||||
$stored_error = $this->get_and_clear_stored_error();
|
||||
if ( $stored_error ) {
|
||||
return array(
|
||||
'result' => 'failure',
|
||||
'message' => $stored_error['message'],
|
||||
);
|
||||
}
|
||||
|
||||
$existing_order = $this->session_handler->order();
|
||||
|
||||
if ( $existing_order ) {
|
||||
// Check if this session order belongs to current WC order and we're in 3DS context.
|
||||
$session_order_belongs_to_current_wc_order = $this->session_order_matches_wc_order( $existing_order, $wc_order );
|
||||
$is_3ds_context = $this->is_3ds_context();
|
||||
|
||||
if ( $session_order_belongs_to_current_wc_order && $is_3ds_context ) {
|
||||
// This is a legitimate 3DS return for the current order.
|
||||
try {
|
||||
/**
|
||||
* This filter controls if the method 'process()' from OrderProcessor will be called.
|
||||
*/
|
||||
$process = apply_filters( 'woocommerce_paypal_payments_before_order_process', true, $this, $wc_order );
|
||||
if ( $process ) {
|
||||
$this->order_processor->process_captured_and_authorized( $wc_order, $existing_order );
|
||||
}
|
||||
|
||||
// Clear session after successful processing.
|
||||
$this->session_handler->destroy_session_data();
|
||||
WC()->cart->empty_cart();
|
||||
|
||||
return array(
|
||||
'result' => 'success',
|
||||
'redirect' => $this->get_return_url( $wc_order ),
|
||||
);
|
||||
|
||||
} catch ( Exception $exception ) {
|
||||
// Handle 3DS processing failures with universal error approach.
|
||||
// Clear session data since payment failed.
|
||||
$this->session_handler->destroy_session_data();
|
||||
|
||||
return array(
|
||||
'result' => 'failure',
|
||||
'message' => $this->get_user_friendly_error_message( $exception ),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// Session order doesn't belong to current WC order OR not 3DS context.
|
||||
$this->session_handler->destroy_session_data();
|
||||
}
|
||||
}
|
||||
|
||||
// No existing order or cleared session - this is an initial payment.
|
||||
try {
|
||||
// phpcs:ignore WordPress.Security.NonceVerification.Missing
|
||||
$fastlane_member = wc_clean( wp_unslash( $_POST['fastlane_member'] ?? '' ) );
|
||||
|
@ -251,38 +311,28 @@ class AxoGateway extends WC_Payment_Gateway {
|
|||
// phpcs:ignore WordPress.Security.NonceVerification.Missing
|
||||
$token = wc_clean( wp_unslash( $_POST['axo_nonce'] ?? '' ) );
|
||||
|
||||
$order = $this->create_paypal_order( $wc_order, $token );
|
||||
|
||||
// Check if 3DS verification is required
|
||||
$payer_action = '';
|
||||
foreach ( $order->links() as $link ) {
|
||||
if ( $link->rel === 'payer-action' ) {
|
||||
$payer_action = $link->href;
|
||||
}
|
||||
// Enhanced token validation with universal error handling.
|
||||
if ( empty( $token ) ) {
|
||||
// Universal error return.
|
||||
return array(
|
||||
'result' => 'failure',
|
||||
'message' => $this->is_3ds_context()
|
||||
? __( 'Payment session expired. Please try your payment again.', 'woocommerce-paypal-payments' )
|
||||
: __( 'No payment token provided. Please try again.', 'woocommerce-paypal-payments' ),
|
||||
);
|
||||
}
|
||||
|
||||
$this->logger->debug(
|
||||
'AXO order created',
|
||||
array(
|
||||
'result' => 'success',
|
||||
'return_url' => $this->get_return_url( $wc_order ),
|
||||
'payer_action' => $payer_action,
|
||||
)
|
||||
);
|
||||
$order = $this->create_paypal_order( $wc_order, $token );
|
||||
|
||||
// If 3DS verification is required, append the redirect_uri and return the redirect URL
|
||||
$this->logger->debug(
|
||||
'AXO order created',
|
||||
array(
|
||||
'result' => 'success',
|
||||
'return_url' => $this->get_return_url( $wc_order ),
|
||||
'payer_action' => $payer_action,
|
||||
)
|
||||
);
|
||||
// Check if 3DS verification is required.
|
||||
$payer_action = $this->get_payer_action_url( $order );
|
||||
|
||||
// If 3DS verification is required, append the redirect_uri and return the redirect URL
|
||||
// If 3DS verification is required, store order and redirect.
|
||||
if ( $payer_action ) {
|
||||
$return_url = $this->get_return_url( $wc_order );
|
||||
// Store the order in session before 3DS redirect.
|
||||
$this->session_handler->replace_order( $order );
|
||||
|
||||
$return_url = home_url( WC_AJAX::get_endpoint( ReturnUrlEndpoint::ENDPOINT ) );
|
||||
|
||||
$redirect_url = add_query_arg(
|
||||
'redirect_uri',
|
||||
|
@ -309,7 +359,11 @@ class AxoGateway extends WC_Payment_Gateway {
|
|||
$this->order_processor->process_captured_and_authorized( $wc_order, $order );
|
||||
}
|
||||
} catch ( Exception $exception ) {
|
||||
return $this->handle_payment_failure( $wc_order, $exception );
|
||||
// Error handling for initial payment failures.
|
||||
return array(
|
||||
'result' => 'failure',
|
||||
'message' => $this->get_user_friendly_error_message( $exception ),
|
||||
);
|
||||
}
|
||||
|
||||
WC()->cart->empty_cart();
|
||||
|
@ -320,6 +374,105 @@ class AxoGateway extends WC_Payment_Gateway {
|
|||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get and clear stored payment errors.
|
||||
*/
|
||||
private function get_and_clear_stored_error() {
|
||||
if ( ! WC()->session ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$stored_error = WC()->session->get( 'ppcp_payment_error' );
|
||||
if ( $stored_error ) {
|
||||
WC()->session->__unset( 'ppcp_payment_error' );
|
||||
return $stored_error;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert exceptions to user-friendly messages.
|
||||
*/
|
||||
private function get_user_friendly_error_message( Exception $exception ) {
|
||||
$error_message = $exception->getMessage();
|
||||
|
||||
// Handle specific error types with user-friendly messages.
|
||||
if ( $exception instanceof DomainException ) {
|
||||
if ( strpos( $error_message, 'Could not capture' ) !== false ) {
|
||||
return __( '3D Secure authentication was unavailable or failed. Please try a different payment method or contact your bank.', 'woocommerce-paypal-payments' );
|
||||
}
|
||||
}
|
||||
|
||||
if ( strpos( $error_message, 'declined' ) !== false ||
|
||||
strpos( $error_message, 'PAYMENT_DENIED' ) !== false ||
|
||||
strpos( $error_message, 'INSTRUMENT_DECLINED' ) !== false ||
|
||||
strpos( $error_message, 'Payment provider declined' ) !== false ) {
|
||||
return __( 'Your payment was declined after 3D Secure verification. Please try a different payment method or contact your bank.', 'woocommerce-paypal-payments' );
|
||||
}
|
||||
|
||||
if ( strpos( $error_message, 'session' ) !== false ||
|
||||
strpos( $error_message, 'expired' ) !== false ) {
|
||||
return __( 'Payment session expired. Please try your payment again.', 'woocommerce-paypal-payments' );
|
||||
}
|
||||
|
||||
return __( 'There was an error processing your payment. Please try again or contact support.', 'woocommerce-paypal-payments' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract payer action URL from PayPal order.
|
||||
*/
|
||||
private function get_payer_action_url( Order $order ) {
|
||||
foreach ( $order->links() as $link ) {
|
||||
if ( $link->rel === 'payer-action' ) {
|
||||
return $link->href;
|
||||
}
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if session order belongs to current WC order.
|
||||
*
|
||||
* @param Order $paypal_order The PayPal order from session.
|
||||
* @param WC_Order $wc_order The current WooCommerce order.
|
||||
* @return bool
|
||||
*/
|
||||
private function session_order_matches_wc_order( Order $paypal_order, WC_Order $wc_order ): bool {
|
||||
$paypal_custom_id = $paypal_order->purchase_units()[0]->custom_id() ?? '';
|
||||
$wc_order_id = (string) $wc_order->get_id();
|
||||
|
||||
return $paypal_custom_id === $wc_order_id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if we're in a 3DS return context.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
private function is_3ds_context(): bool {
|
||||
// Check referer for PayPal.
|
||||
$referer = $_SERVER['HTTP_REFERER'] ?? '';
|
||||
if ( strpos( $referer, 'paypal.com' ) !== false ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check for 3DS-specific parameters.
|
||||
$three_ds_params = array( 'liability_shift', 'state', 'code', 'authentication_state' );
|
||||
foreach ( $three_ds_params as $param ) {
|
||||
if ( isset( $_GET[ $param ] ) ) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if we're being called by ReturnUrlEndpoint.
|
||||
if ( isset( $_GET['wc-ajax'] ) && $_GET['wc-ajax'] === 'ppc-return-url' ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new PayPal order from the existing WC_Order instance.
|
||||
*
|
||||
|
@ -346,15 +499,6 @@ class AxoGateway extends WC_Payment_Gateway {
|
|||
$payment_source_properties
|
||||
);
|
||||
|
||||
$this->logger->debug(
|
||||
'AXO full order endpoint data',
|
||||
array(
|
||||
'purchase_unit' => $purchase_unit,
|
||||
'shipping_preference' => $shipping_preference,
|
||||
'payment_source' => $payment_source,
|
||||
)
|
||||
);
|
||||
|
||||
return $this->order_endpoint->create(
|
||||
array( $purchase_unit ),
|
||||
$shipping_preference,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue