mirror of
https://github.com/woocommerce/woocommerce-paypal-payments.git
synced 2025-09-01 07:02:48 +08:00
Merge pull request #3163 from woocommerce/PCP-4156-implement-3ds-for-google-pay
Implement 3D secure check for Google Pay (4156)
This commit is contained in:
commit
89e847bc52
6 changed files with 336 additions and 173 deletions
|
@ -1,3 +1,18 @@
|
|||
const initiateRedirect = ( successUrl ) => {
|
||||
/**
|
||||
* Notice how this step initiates a redirect to a new page using a plain
|
||||
* URL as new location. This process does not send any details about the
|
||||
* approved order or billed customer.
|
||||
*
|
||||
* The redirect will start after a short delay, giving the calling method
|
||||
* time to process the return value of the `await onApprove()` call.
|
||||
*/
|
||||
|
||||
setTimeout( () => {
|
||||
window.location.href = successUrl;
|
||||
}, 200 );
|
||||
};
|
||||
|
||||
const onApprove = ( context, errorHandler ) => {
|
||||
return ( data, actions ) => {
|
||||
const canCreateOrder =
|
||||
|
@ -28,24 +43,13 @@ const onApprove = ( context, errorHandler ) => {
|
|||
.then( ( approveData ) => {
|
||||
if ( ! approveData.success ) {
|
||||
errorHandler.genericError();
|
||||
return actions.restart().catch( ( err ) => {
|
||||
return actions.restart().catch( () => {
|
||||
errorHandler.genericError();
|
||||
} );
|
||||
}
|
||||
|
||||
const orderReceivedUrl = approveData.data?.order_received_url;
|
||||
|
||||
/**
|
||||
* Notice how this step initiates a redirect to a new page using a plain
|
||||
* URL as new location. This process does not send any details about the
|
||||
* approved order or billed customer.
|
||||
* Also, due to the redirect starting _instantly_ there should be no other
|
||||
* logic scheduled after calling `await onApprove()`;
|
||||
*/
|
||||
|
||||
window.location.href = orderReceivedUrl
|
||||
? orderReceivedUrl
|
||||
: context.config.redirect;
|
||||
initiateRedirect( orderReceivedUrl || context.config.redirect );
|
||||
} );
|
||||
};
|
||||
};
|
||||
|
|
|
@ -6,13 +6,14 @@
|
|||
* @package WooCommerce\PayPalCommerce\Button\Endpoint
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
declare( strict_types = 1 );
|
||||
|
||||
namespace WooCommerce\PayPalCommerce\Button\Endpoint;
|
||||
|
||||
use Exception;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\OrderEndpoint;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Entity\Order;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Entity\OrderStatus;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Exception\PayPalApiException;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Helper\DccApplies;
|
||||
|
@ -24,7 +25,6 @@ use WooCommerce\PayPalCommerce\Button\Helper\WooCommerceOrderCreator;
|
|||
use WooCommerce\PayPalCommerce\Session\SessionHandler;
|
||||
use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayPalGateway;
|
||||
use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings;
|
||||
use WooCommerce\PayPalCommerce\WcSubscriptions\Helper\SubscriptionHelper;
|
||||
|
||||
/**
|
||||
* Class ApproveOrderEndpoint
|
||||
|
@ -115,17 +115,17 @@ class ApproveOrderEndpoint implements EndpointInterface {
|
|||
/**
|
||||
* ApproveOrderEndpoint constructor.
|
||||
*
|
||||
* @param RequestData $request_data The request data helper.
|
||||
* @param OrderEndpoint $order_endpoint The order endpoint.
|
||||
* @param SessionHandler $session_handler The session handler.
|
||||
* @param ThreeDSecure $three_d_secure The 3d secure helper object.
|
||||
* @param Settings $settings The settings.
|
||||
* @param DccApplies $dcc_applies The DCC applies object.
|
||||
* @param OrderHelper $order_helper The order helper.
|
||||
* @param RequestData $request_data The request data helper.
|
||||
* @param OrderEndpoint $order_endpoint The order endpoint.
|
||||
* @param SessionHandler $session_handler The session handler.
|
||||
* @param ThreeDSecure $three_d_secure The 3d secure helper object.
|
||||
* @param Settings $settings The settings.
|
||||
* @param DccApplies $dcc_applies The DCC applies object.
|
||||
* @param OrderHelper $order_helper The order helper.
|
||||
* @param bool $final_review_enabled Whether the final review is enabled.
|
||||
* @param PayPalGateway $gateway The WC gateway.
|
||||
* @param WooCommerceOrderCreator $wc_order_creator The WooCommerce order creator.
|
||||
* @param LoggerInterface $logger The logger.
|
||||
* @param PayPalGateway $gateway The WC gateway.
|
||||
* @param WooCommerceOrderCreator $wc_order_creator The WooCommerce order creator.
|
||||
* @param LoggerInterface $logger The logger.
|
||||
*/
|
||||
public function __construct(
|
||||
RequestData $request_data,
|
||||
|
@ -159,7 +159,7 @@ class ApproveOrderEndpoint implements EndpointInterface {
|
|||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function nonce(): string {
|
||||
public static function nonce() : string {
|
||||
return self::ENDPOINT;
|
||||
}
|
||||
|
||||
|
@ -169,9 +169,9 @@ class ApproveOrderEndpoint implements EndpointInterface {
|
|||
* @return bool
|
||||
* @throws RuntimeException When order not found or handling failed.
|
||||
*/
|
||||
public function handle_request(): bool {
|
||||
public function handle_request() : bool {
|
||||
try {
|
||||
$data = $this->request_data->read_request( $this->nonce() );
|
||||
$data = $this->request_data->read_request( self::nonce() );
|
||||
if ( ! isset( $data['order_id'] ) ) {
|
||||
throw new RuntimeException(
|
||||
__( 'No order id given', 'woocommerce-paypal-payments' )
|
||||
|
@ -181,6 +181,7 @@ class ApproveOrderEndpoint implements EndpointInterface {
|
|||
$order = $this->api_endpoint->order( $data['order_id'] );
|
||||
|
||||
$payment_source = $order->payment_source();
|
||||
|
||||
if ( $payment_source && $payment_source->name() === 'card' ) {
|
||||
if ( $this->settings->has( 'disable_cards' ) ) {
|
||||
$disabled_cards = (array) $this->settings->get( 'disable_cards' );
|
||||
|
@ -199,29 +200,23 @@ class ApproveOrderEndpoint implements EndpointInterface {
|
|||
);
|
||||
}
|
||||
}
|
||||
$proceed = $this->threed_secure->proceed_with_order( $order );
|
||||
if ( ThreeDSecure::RETRY === $proceed ) {
|
||||
throw new RuntimeException(
|
||||
__(
|
||||
'Something went wrong. Please try again.',
|
||||
'woocommerce-paypal-payments'
|
||||
)
|
||||
);
|
||||
}
|
||||
if ( ThreeDSecure::REJECT === $proceed ) {
|
||||
throw new RuntimeException(
|
||||
__(
|
||||
'Unfortunately, we can\'t accept your card. Please choose a different payment method.',
|
||||
'woocommerce-paypal-payments'
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// This check will either pass, or throw an exception.
|
||||
$this->verify_three_d_secure( $order );
|
||||
|
||||
$this->session_handler->replace_order( $order );
|
||||
|
||||
// Exit the request early.
|
||||
wp_send_json_success();
|
||||
}
|
||||
|
||||
if ( $this->order_helper->contains_physical_goods( $order ) && ! $order->status()->is( OrderStatus::APPROVED ) && ! $order->status()->is( OrderStatus::CREATED ) ) {
|
||||
// Verify 3DS details. Throws an error when security check fails.
|
||||
$this->verify_three_d_secure( $order );
|
||||
|
||||
$is_ready = $order->status()->is( OrderStatus::APPROVED )
|
||||
|| $order->status()->is( OrderStatus::CREATED );
|
||||
|
||||
if ( ! $is_ready && $this->order_helper->contains_physical_goods( $order ) ) {
|
||||
$message = sprintf(
|
||||
// translators: %s is the id of the order.
|
||||
__( 'Order %s is not ready for processing yet.', 'woocommerce-paypal-payments' ),
|
||||
|
@ -250,6 +245,7 @@ class ApproveOrderEndpoint implements EndpointInterface {
|
|||
wp_send_json_success( array( 'order_received_url' => $order_received_url ) );
|
||||
}
|
||||
wp_send_json_success();
|
||||
|
||||
return true;
|
||||
} catch ( Exception $error ) {
|
||||
$this->logger->error( 'Order approve failed: ' . $error->getMessage() );
|
||||
|
@ -262,6 +258,7 @@ class ApproveOrderEndpoint implements EndpointInterface {
|
|||
'details' => is_a( $error, PayPalApiException::class ) ? $error->details() : array(),
|
||||
)
|
||||
);
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
@ -271,10 +268,79 @@ class ApproveOrderEndpoint implements EndpointInterface {
|
|||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function toggle_final_review_enabled_setting(): void {
|
||||
protected function toggle_final_review_enabled_setting() : void {
|
||||
// TODO new-ux: This flag must also be updated in the new settings.
|
||||
$final_review_enabled_setting = $this->settings->has( 'blocks_final_review_enabled' ) && $this->settings->get( 'blocks_final_review_enabled' );
|
||||
$this->settings->set( 'blocks_final_review_enabled', ! $final_review_enabled_setting );
|
||||
$this->settings->persist();
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs a 3DS check to verify the payment is not rejected from PayPal side.
|
||||
*
|
||||
* This method only checks, if the payment was rejected:
|
||||
*
|
||||
* - No 3DS details are present: The payment can proceed.
|
||||
* - 3DS details present but no rejected: Payment can proceed.
|
||||
* - 3DS details with a clear rejected: Payment fails.
|
||||
*
|
||||
* @param Order $order The PayPal order to inspect.
|
||||
* @throws RuntimeException When the 3DS check was rejected.
|
||||
*/
|
||||
protected function verify_three_d_secure( Order $order ) : void {
|
||||
$payment_source = $order->payment_source();
|
||||
|
||||
if ( ! $payment_source ) {
|
||||
// Missing 3DS details.
|
||||
return;
|
||||
}
|
||||
|
||||
$proceed = ThreeDSecure::NO_DECISION;
|
||||
$order_status = $order->status();
|
||||
$source_name = $payment_source->name();
|
||||
|
||||
/**
|
||||
* For GooglePay (and possibly other payment sources) we check the order
|
||||
* status, as it will clearly indicate if verification is needed.
|
||||
*
|
||||
* Note: PayPal is currently investigating this case.
|
||||
* Maybe the order status is wrong and should be ACCEPTED, in that case,
|
||||
* we could drop the condition and always run proceed_with_order().
|
||||
*/
|
||||
if ( $order_status->is( OrderStatus::PAYER_ACTION_REQUIRED ) ) {
|
||||
$proceed = $this->threed_secure->proceed_with_order( $order );
|
||||
} elseif ( 'card' === $source_name ) {
|
||||
// For credit cards, we also check the 3DS response.
|
||||
$proceed = $this->threed_secure->proceed_with_order( $order );
|
||||
}
|
||||
|
||||
// Handle the verification result based on the proceed value.
|
||||
switch ( $proceed ) {
|
||||
case ThreeDSecure::PROCEED:
|
||||
// Check was successful.
|
||||
return;
|
||||
|
||||
case ThreeDSecure::NO_DECISION:
|
||||
// No rejection. Let's proceed with the payment.
|
||||
return;
|
||||
|
||||
case ThreeDSecure::RETRY:
|
||||
// Rejection case 1, verification can be retried.
|
||||
throw new RuntimeException(
|
||||
__(
|
||||
'Something went wrong. Please try again.',
|
||||
'woocommerce-paypal-payments'
|
||||
)
|
||||
);
|
||||
|
||||
case ThreeDSecure::REJECT:
|
||||
// Rejection case 2, payment was rejected.
|
||||
throw new RuntimeException(
|
||||
__(
|
||||
'Unfortunately, we can\'t accept your card. Please choose a different payment method.',
|
||||
'woocommerce-paypal-payments'
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
* @package WooCommerce\PayPalCommerce\Button\Helper
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
declare( strict_types = 1 );
|
||||
|
||||
namespace WooCommerce\PayPalCommerce\Button\Helper;
|
||||
|
||||
|
@ -19,49 +19,49 @@ use WooCommerce\PayPalCommerce\ApiClient\Factory\CardAuthenticationResultFactory
|
|||
*/
|
||||
class ThreeDSecure {
|
||||
|
||||
const NO_DECISION = 0;
|
||||
const PROCEED = 1;
|
||||
const REJECT = 2;
|
||||
const RETRY = 3;
|
||||
public const NO_DECISION = 0;
|
||||
public const PROCEED = 1;
|
||||
public const REJECT = 2;
|
||||
public const RETRY = 3;
|
||||
|
||||
/**
|
||||
* Card authentication result factory.
|
||||
*
|
||||
* @var CardAuthenticationResultFactory
|
||||
*/
|
||||
private $card_authentication_result_factory;
|
||||
private CardAuthenticationResultFactory $authentication_result;
|
||||
|
||||
/**
|
||||
* The logger.
|
||||
*
|
||||
* @var LoggerInterface
|
||||
*/
|
||||
protected $logger;
|
||||
protected LoggerInterface $logger;
|
||||
|
||||
/**
|
||||
* ThreeDSecure constructor.
|
||||
*
|
||||
* @param CardAuthenticationResultFactory $card_authentication_result_factory Card authentication result factory.
|
||||
* @param LoggerInterface $logger The logger.
|
||||
* @param CardAuthenticationResultFactory $authentication_factory Card authentication result factory.
|
||||
* @param LoggerInterface $logger The logger.
|
||||
*/
|
||||
public function __construct(
|
||||
CardAuthenticationResultFactory $card_authentication_result_factory,
|
||||
CardAuthenticationResultFactory $authentication_factory,
|
||||
LoggerInterface $logger
|
||||
) {
|
||||
$this->logger = $logger;
|
||||
$this->card_authentication_result_factory = $card_authentication_result_factory;
|
||||
$this->logger = $logger;
|
||||
$this->authentication_result = $authentication_factory;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine, how we proceed with a given order.
|
||||
*
|
||||
* @link https://developer.paypal.com/docs/business/checkout/add-capabilities/3d-secure/#authenticationresult
|
||||
* @link https://developer.paypal.com/docs/checkout/advanced/customize/3d-secure/response-parameters/
|
||||
*
|
||||
* @param Order $order The order for which the decision is needed.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function proceed_with_order( Order $order ): int {
|
||||
public function proceed_with_order( Order $order ) : int {
|
||||
|
||||
do_action( 'woocommerce_paypal_payments_three_d_secure_before_check', $order );
|
||||
|
||||
|
@ -70,29 +70,44 @@ class ThreeDSecure {
|
|||
return $this->return_decision( self::NO_DECISION, $order );
|
||||
}
|
||||
|
||||
if ( ! ( $payment_source->properties()->brand ?? '' ) ) {
|
||||
return $this->return_decision( self::NO_DECISION, $order );
|
||||
if ( isset( $payment_source->properties()->card ) ) {
|
||||
/**
|
||||
* GooglePay provides the credit-card details and authentication-result
|
||||
* via the "cards" attribute. We assume, that this structure is also
|
||||
* used for other payment methods that support 3DS.
|
||||
*/
|
||||
$card_properties = $payment_source->properties()->card;
|
||||
} else {
|
||||
/**
|
||||
* For regular credit card payments (via PayPal) we get all details
|
||||
* directly in the payment_source properties.
|
||||
*/
|
||||
$card_properties = $payment_source->properties();
|
||||
}
|
||||
if ( ! ( $payment_source->properties()->authentication_result ?? '' ) ) {
|
||||
|
||||
if ( empty( $card_properties->brand ) ) {
|
||||
return $this->return_decision( self::NO_DECISION, $order );
|
||||
}
|
||||
|
||||
$authentication_result = $payment_source->properties()->authentication_result ?? null;
|
||||
if ( $authentication_result ) {
|
||||
$result = $this->card_authentication_result_factory->from_paypal_response( $authentication_result );
|
||||
if ( empty( $card_properties->authentication_result ) ) {
|
||||
return $this->return_decision( self::NO_DECISION, $order );
|
||||
}
|
||||
|
||||
$this->logger->info( '3DS Authentication Result: ' . wc_print_r( $result->to_array(), true ) );
|
||||
$result = $this->authentication_result->from_paypal_response( $card_properties->authentication_result );
|
||||
$liability = $result->liability_shift();
|
||||
|
||||
if ( $result->liability_shift() === AuthResult::LIABILITY_SHIFT_POSSIBLE ) {
|
||||
return $this->return_decision( self::PROCEED, $order );
|
||||
}
|
||||
$this->logger->info( '3DS Authentication Result: ' . wc_print_r( $result->to_array(), true ) );
|
||||
|
||||
if ( $result->liability_shift() === AuthResult::LIABILITY_SHIFT_UNKNOWN ) {
|
||||
return $this->return_decision( self::RETRY, $order );
|
||||
}
|
||||
if ( $result->liability_shift() === AuthResult::LIABILITY_SHIFT_NO ) {
|
||||
return $this->return_decision( $this->no_liability_shift( $result ), $order );
|
||||
}
|
||||
if ( $liability === AuthResult::LIABILITY_SHIFT_POSSIBLE ) {
|
||||
return $this->return_decision( self::PROCEED, $order );
|
||||
}
|
||||
|
||||
if ( $liability === AuthResult::LIABILITY_SHIFT_UNKNOWN ) {
|
||||
return $this->return_decision( self::RETRY, $order );
|
||||
}
|
||||
|
||||
if ( $liability === AuthResult::LIABILITY_SHIFT_NO ) {
|
||||
return $this->return_decision( $this->no_liability_shift( $result ), $order );
|
||||
}
|
||||
|
||||
return $this->return_decision( self::NO_DECISION, $order );
|
||||
|
@ -102,12 +117,13 @@ class ThreeDSecure {
|
|||
* Processes and returns a ThreeD secure decision.
|
||||
*
|
||||
* @param int $decision The ThreeD secure decision.
|
||||
* @param Order $order The PayPal Order object.
|
||||
* @param Order $order The PayPal Order object.
|
||||
* @return int
|
||||
*/
|
||||
public function return_decision( int $decision, Order $order ) {
|
||||
public function return_decision( int $decision, Order $order ) : int {
|
||||
$decision = apply_filters( 'woocommerce_paypal_payments_three_d_secure_decision', $decision, $order );
|
||||
do_action( 'woocommerce_paypal_payments_three_d_secure_after_check', $order, $decision );
|
||||
|
||||
return $decision;
|
||||
}
|
||||
|
||||
|
@ -118,42 +134,40 @@ class ThreeDSecure {
|
|||
*
|
||||
* @return int
|
||||
*/
|
||||
private function no_liability_shift( AuthResult $result ): int {
|
||||
private function no_liability_shift( AuthResult $result ) : int {
|
||||
$enrollment = $result->enrollment_status();
|
||||
$authentication = $result->authentication_result();
|
||||
|
||||
if (
|
||||
$result->enrollment_status() === AuthResult::ENROLLMENT_STATUS_BYPASS
|
||||
&& ! $result->authentication_result()
|
||||
) {
|
||||
return self::PROCEED;
|
||||
}
|
||||
if (
|
||||
$result->enrollment_status() === AuthResult::ENROLLMENT_STATUS_UNAVAILABLE
|
||||
&& ! $result->authentication_result()
|
||||
) {
|
||||
return self::PROCEED;
|
||||
}
|
||||
if (
|
||||
$result->enrollment_status() === AuthResult::ENROLLMENT_STATUS_NO
|
||||
&& ! $result->authentication_result()
|
||||
) {
|
||||
return self::PROCEED;
|
||||
if ( ! $authentication ) {
|
||||
if ( $enrollment === AuthResult::ENROLLMENT_STATUS_BYPASS ) {
|
||||
return self::PROCEED;
|
||||
}
|
||||
|
||||
if ( $enrollment === AuthResult::ENROLLMENT_STATUS_UNAVAILABLE ) {
|
||||
return self::PROCEED;
|
||||
}
|
||||
|
||||
if ( $enrollment === AuthResult::ENROLLMENT_STATUS_NO ) {
|
||||
return self::PROCEED;
|
||||
}
|
||||
}
|
||||
|
||||
if ( $result->authentication_result() === AuthResult::AUTHENTICATION_RESULT_REJECTED ) {
|
||||
if ( $authentication === AuthResult::AUTHENTICATION_RESULT_REJECTED ) {
|
||||
return self::REJECT;
|
||||
}
|
||||
|
||||
if ( $result->authentication_result() === AuthResult::AUTHENTICATION_RESULT_NO ) {
|
||||
if ( $authentication === AuthResult::AUTHENTICATION_RESULT_NO ) {
|
||||
return self::REJECT;
|
||||
}
|
||||
|
||||
if ( $result->authentication_result() === AuthResult::AUTHENTICATION_RESULT_UNABLE ) {
|
||||
if ( $authentication === AuthResult::AUTHENTICATION_RESULT_UNABLE ) {
|
||||
return self::RETRY;
|
||||
}
|
||||
|
||||
if ( ! $result->authentication_result() ) {
|
||||
if ( ! $authentication ) {
|
||||
return self::RETRY;
|
||||
}
|
||||
|
||||
return self::NO_DECISION;
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue