mirror of
https://github.com/woocommerce/woocommerce-paypal-payments.git
synced 2025-09-04 08:47:23 +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
|
@ -483,6 +483,7 @@ class OrderEndpoint {
|
||||||
}
|
}
|
||||||
$json = json_decode( $response['body'] );
|
$json = json_decode( $response['body'] );
|
||||||
$status_code = (int) wp_remote_retrieve_response_code( $response );
|
$status_code = (int) wp_remote_retrieve_response_code( $response );
|
||||||
|
|
||||||
if ( 404 === $status_code || empty( $response['body'] ) ) {
|
if ( 404 === $status_code || empty( $response['body'] ) ) {
|
||||||
$error = new RuntimeException(
|
$error = new RuntimeException(
|
||||||
__( 'Could not retrieve order.', 'woocommerce-paypal-payments' ),
|
__( 'Could not retrieve order.', 'woocommerce-paypal-payments' ),
|
||||||
|
@ -498,6 +499,7 @@ class OrderEndpoint {
|
||||||
);
|
);
|
||||||
throw $error;
|
throw $error;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ( 200 !== $status_code ) {
|
if ( 200 !== $status_code ) {
|
||||||
$error = new PayPalApiException(
|
$error = new PayPalApiException(
|
||||||
$json,
|
$json,
|
||||||
|
|
|
@ -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 ) => {
|
const onApprove = ( context, errorHandler ) => {
|
||||||
return ( data, actions ) => {
|
return ( data, actions ) => {
|
||||||
const canCreateOrder =
|
const canCreateOrder =
|
||||||
|
@ -28,24 +43,13 @@ const onApprove = ( context, errorHandler ) => {
|
||||||
.then( ( approveData ) => {
|
.then( ( approveData ) => {
|
||||||
if ( ! approveData.success ) {
|
if ( ! approveData.success ) {
|
||||||
errorHandler.genericError();
|
errorHandler.genericError();
|
||||||
return actions.restart().catch( ( err ) => {
|
return actions.restart().catch( () => {
|
||||||
errorHandler.genericError();
|
errorHandler.genericError();
|
||||||
} );
|
} );
|
||||||
}
|
}
|
||||||
|
|
||||||
const orderReceivedUrl = approveData.data?.order_received_url;
|
const orderReceivedUrl = approveData.data?.order_received_url;
|
||||||
|
initiateRedirect( orderReceivedUrl || context.config.redirect );
|
||||||
/**
|
|
||||||
* 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;
|
|
||||||
} );
|
} );
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
@ -6,13 +6,14 @@
|
||||||
* @package WooCommerce\PayPalCommerce\Button\Endpoint
|
* @package WooCommerce\PayPalCommerce\Button\Endpoint
|
||||||
*/
|
*/
|
||||||
|
|
||||||
declare(strict_types=1);
|
declare( strict_types = 1 );
|
||||||
|
|
||||||
namespace WooCommerce\PayPalCommerce\Button\Endpoint;
|
namespace WooCommerce\PayPalCommerce\Button\Endpoint;
|
||||||
|
|
||||||
use Exception;
|
use Exception;
|
||||||
use Psr\Log\LoggerInterface;
|
use Psr\Log\LoggerInterface;
|
||||||
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\OrderEndpoint;
|
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\OrderEndpoint;
|
||||||
|
use WooCommerce\PayPalCommerce\ApiClient\Entity\Order;
|
||||||
use WooCommerce\PayPalCommerce\ApiClient\Entity\OrderStatus;
|
use WooCommerce\PayPalCommerce\ApiClient\Entity\OrderStatus;
|
||||||
use WooCommerce\PayPalCommerce\ApiClient\Exception\PayPalApiException;
|
use WooCommerce\PayPalCommerce\ApiClient\Exception\PayPalApiException;
|
||||||
use WooCommerce\PayPalCommerce\ApiClient\Helper\DccApplies;
|
use WooCommerce\PayPalCommerce\ApiClient\Helper\DccApplies;
|
||||||
|
@ -24,7 +25,6 @@ use WooCommerce\PayPalCommerce\Button\Helper\WooCommerceOrderCreator;
|
||||||
use WooCommerce\PayPalCommerce\Session\SessionHandler;
|
use WooCommerce\PayPalCommerce\Session\SessionHandler;
|
||||||
use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayPalGateway;
|
use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayPalGateway;
|
||||||
use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings;
|
use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings;
|
||||||
use WooCommerce\PayPalCommerce\WcSubscriptions\Helper\SubscriptionHelper;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class ApproveOrderEndpoint
|
* Class ApproveOrderEndpoint
|
||||||
|
@ -115,17 +115,17 @@ class ApproveOrderEndpoint implements EndpointInterface {
|
||||||
/**
|
/**
|
||||||
* ApproveOrderEndpoint constructor.
|
* ApproveOrderEndpoint constructor.
|
||||||
*
|
*
|
||||||
* @param RequestData $request_data The request data helper.
|
* @param RequestData $request_data The request data helper.
|
||||||
* @param OrderEndpoint $order_endpoint The order endpoint.
|
* @param OrderEndpoint $order_endpoint The order endpoint.
|
||||||
* @param SessionHandler $session_handler The session handler.
|
* @param SessionHandler $session_handler The session handler.
|
||||||
* @param ThreeDSecure $three_d_secure The 3d secure helper object.
|
* @param ThreeDSecure $three_d_secure The 3d secure helper object.
|
||||||
* @param Settings $settings The settings.
|
* @param Settings $settings The settings.
|
||||||
* @param DccApplies $dcc_applies The DCC applies object.
|
* @param DccApplies $dcc_applies The DCC applies object.
|
||||||
* @param OrderHelper $order_helper The order helper.
|
* @param OrderHelper $order_helper The order helper.
|
||||||
* @param bool $final_review_enabled Whether the final review is enabled.
|
* @param bool $final_review_enabled Whether the final review is enabled.
|
||||||
* @param PayPalGateway $gateway The WC gateway.
|
* @param PayPalGateway $gateway The WC gateway.
|
||||||
* @param WooCommerceOrderCreator $wc_order_creator The WooCommerce order creator.
|
* @param WooCommerceOrderCreator $wc_order_creator The WooCommerce order creator.
|
||||||
* @param LoggerInterface $logger The logger.
|
* @param LoggerInterface $logger The logger.
|
||||||
*/
|
*/
|
||||||
public function __construct(
|
public function __construct(
|
||||||
RequestData $request_data,
|
RequestData $request_data,
|
||||||
|
@ -159,7 +159,7 @@ class ApproveOrderEndpoint implements EndpointInterface {
|
||||||
*
|
*
|
||||||
* @return string
|
* @return string
|
||||||
*/
|
*/
|
||||||
public static function nonce(): string {
|
public static function nonce() : string {
|
||||||
return self::ENDPOINT;
|
return self::ENDPOINT;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -169,9 +169,9 @@ class ApproveOrderEndpoint implements EndpointInterface {
|
||||||
* @return bool
|
* @return bool
|
||||||
* @throws RuntimeException When order not found or handling failed.
|
* @throws RuntimeException When order not found or handling failed.
|
||||||
*/
|
*/
|
||||||
public function handle_request(): bool {
|
public function handle_request() : bool {
|
||||||
try {
|
try {
|
||||||
$data = $this->request_data->read_request( $this->nonce() );
|
$data = $this->request_data->read_request( self::nonce() );
|
||||||
if ( ! isset( $data['order_id'] ) ) {
|
if ( ! isset( $data['order_id'] ) ) {
|
||||||
throw new RuntimeException(
|
throw new RuntimeException(
|
||||||
__( 'No order id given', 'woocommerce-paypal-payments' )
|
__( 'No order id given', 'woocommerce-paypal-payments' )
|
||||||
|
@ -181,6 +181,7 @@ class ApproveOrderEndpoint implements EndpointInterface {
|
||||||
$order = $this->api_endpoint->order( $data['order_id'] );
|
$order = $this->api_endpoint->order( $data['order_id'] );
|
||||||
|
|
||||||
$payment_source = $order->payment_source();
|
$payment_source = $order->payment_source();
|
||||||
|
|
||||||
if ( $payment_source && $payment_source->name() === 'card' ) {
|
if ( $payment_source && $payment_source->name() === 'card' ) {
|
||||||
if ( $this->settings->has( 'disable_cards' ) ) {
|
if ( $this->settings->has( 'disable_cards' ) ) {
|
||||||
$disabled_cards = (array) $this->settings->get( '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 ) {
|
// This check will either pass, or throw an exception.
|
||||||
throw new RuntimeException(
|
$this->verify_three_d_secure( $order );
|
||||||
__(
|
|
||||||
'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->session_handler->replace_order( $order );
|
$this->session_handler->replace_order( $order );
|
||||||
|
|
||||||
|
// Exit the request early.
|
||||||
wp_send_json_success();
|
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(
|
$message = sprintf(
|
||||||
// translators: %s is the id of the order.
|
// translators: %s is the id of the order.
|
||||||
__( 'Order %s is not ready for processing yet.', 'woocommerce-paypal-payments' ),
|
__( '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( array( 'order_received_url' => $order_received_url ) );
|
||||||
}
|
}
|
||||||
wp_send_json_success();
|
wp_send_json_success();
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
} catch ( Exception $error ) {
|
} catch ( Exception $error ) {
|
||||||
$this->logger->error( 'Order approve failed: ' . $error->getMessage() );
|
$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(),
|
'details' => is_a( $error, PayPalApiException::class ) ? $error->details() : array(),
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -271,10 +268,79 @@ class ApproveOrderEndpoint implements EndpointInterface {
|
||||||
*
|
*
|
||||||
* @return void
|
* @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.
|
// 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' );
|
$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->set( 'blocks_final_review_enabled', ! $final_review_enabled_setting );
|
||||||
$this->settings->persist();
|
$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
|
* @package WooCommerce\PayPalCommerce\Button\Helper
|
||||||
*/
|
*/
|
||||||
|
|
||||||
declare(strict_types=1);
|
declare( strict_types = 1 );
|
||||||
|
|
||||||
namespace WooCommerce\PayPalCommerce\Button\Helper;
|
namespace WooCommerce\PayPalCommerce\Button\Helper;
|
||||||
|
|
||||||
|
@ -19,49 +19,49 @@ use WooCommerce\PayPalCommerce\ApiClient\Factory\CardAuthenticationResultFactory
|
||||||
*/
|
*/
|
||||||
class ThreeDSecure {
|
class ThreeDSecure {
|
||||||
|
|
||||||
const NO_DECISION = 0;
|
public const NO_DECISION = 0;
|
||||||
const PROCEED = 1;
|
public const PROCEED = 1;
|
||||||
const REJECT = 2;
|
public const REJECT = 2;
|
||||||
const RETRY = 3;
|
public const RETRY = 3;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Card authentication result factory.
|
* Card authentication result factory.
|
||||||
*
|
*
|
||||||
* @var CardAuthenticationResultFactory
|
* @var CardAuthenticationResultFactory
|
||||||
*/
|
*/
|
||||||
private $card_authentication_result_factory;
|
private CardAuthenticationResultFactory $authentication_result;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The logger.
|
* The logger.
|
||||||
*
|
*
|
||||||
* @var LoggerInterface
|
* @var LoggerInterface
|
||||||
*/
|
*/
|
||||||
protected $logger;
|
protected LoggerInterface $logger;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ThreeDSecure constructor.
|
* ThreeDSecure constructor.
|
||||||
*
|
*
|
||||||
* @param CardAuthenticationResultFactory $card_authentication_result_factory Card authentication result factory.
|
* @param CardAuthenticationResultFactory $authentication_factory Card authentication result factory.
|
||||||
* @param LoggerInterface $logger The logger.
|
* @param LoggerInterface $logger The logger.
|
||||||
*/
|
*/
|
||||||
public function __construct(
|
public function __construct(
|
||||||
CardAuthenticationResultFactory $card_authentication_result_factory,
|
CardAuthenticationResultFactory $authentication_factory,
|
||||||
LoggerInterface $logger
|
LoggerInterface $logger
|
||||||
) {
|
) {
|
||||||
$this->logger = $logger;
|
$this->logger = $logger;
|
||||||
$this->card_authentication_result_factory = $card_authentication_result_factory;
|
$this->authentication_result = $authentication_factory;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Determine, how we proceed with a given order.
|
* 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.
|
* @param Order $order The order for which the decision is needed.
|
||||||
*
|
*
|
||||||
* @return int
|
* @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 );
|
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 );
|
return $this->return_decision( self::NO_DECISION, $order );
|
||||||
}
|
}
|
||||||
|
|
||||||
if ( ! ( $payment_source->properties()->brand ?? '' ) ) {
|
if ( isset( $payment_source->properties()->card ) ) {
|
||||||
return $this->return_decision( self::NO_DECISION, $order );
|
/**
|
||||||
|
* 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 );
|
return $this->return_decision( self::NO_DECISION, $order );
|
||||||
}
|
}
|
||||||
|
|
||||||
$authentication_result = $payment_source->properties()->authentication_result ?? null;
|
if ( empty( $card_properties->authentication_result ) ) {
|
||||||
if ( $authentication_result ) {
|
return $this->return_decision( self::NO_DECISION, $order );
|
||||||
$result = $this->card_authentication_result_factory->from_paypal_response( $authentication_result );
|
}
|
||||||
|
|
||||||
$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 ) {
|
$this->logger->info( '3DS Authentication Result: ' . wc_print_r( $result->to_array(), true ) );
|
||||||
return $this->return_decision( self::PROCEED, $order );
|
|
||||||
}
|
|
||||||
|
|
||||||
if ( $result->liability_shift() === AuthResult::LIABILITY_SHIFT_UNKNOWN ) {
|
if ( $liability === AuthResult::LIABILITY_SHIFT_POSSIBLE ) {
|
||||||
return $this->return_decision( self::RETRY, $order );
|
return $this->return_decision( self::PROCEED, $order );
|
||||||
}
|
}
|
||||||
if ( $result->liability_shift() === AuthResult::LIABILITY_SHIFT_NO ) {
|
|
||||||
return $this->return_decision( $this->no_liability_shift( $result ), $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 );
|
return $this->return_decision( self::NO_DECISION, $order );
|
||||||
|
@ -102,12 +117,13 @@ class ThreeDSecure {
|
||||||
* Processes and returns a ThreeD secure decision.
|
* Processes and returns a ThreeD secure decision.
|
||||||
*
|
*
|
||||||
* @param int $decision The 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
|
* @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 );
|
$decision = apply_filters( 'woocommerce_paypal_payments_three_d_secure_decision', $decision, $order );
|
||||||
do_action( 'woocommerce_paypal_payments_three_d_secure_after_check', $order, $decision );
|
do_action( 'woocommerce_paypal_payments_three_d_secure_after_check', $order, $decision );
|
||||||
|
|
||||||
return $decision;
|
return $decision;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -118,42 +134,40 @@ class ThreeDSecure {
|
||||||
*
|
*
|
||||||
* @return int
|
* @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 (
|
if ( ! $authentication ) {
|
||||||
$result->enrollment_status() === AuthResult::ENROLLMENT_STATUS_BYPASS
|
if ( $enrollment === AuthResult::ENROLLMENT_STATUS_BYPASS ) {
|
||||||
&& ! $result->authentication_result()
|
return self::PROCEED;
|
||||||
) {
|
}
|
||||||
return self::PROCEED;
|
|
||||||
}
|
if ( $enrollment === AuthResult::ENROLLMENT_STATUS_UNAVAILABLE ) {
|
||||||
if (
|
return self::PROCEED;
|
||||||
$result->enrollment_status() === AuthResult::ENROLLMENT_STATUS_UNAVAILABLE
|
}
|
||||||
&& ! $result->authentication_result()
|
|
||||||
) {
|
if ( $enrollment === AuthResult::ENROLLMENT_STATUS_NO ) {
|
||||||
return self::PROCEED;
|
return self::PROCEED;
|
||||||
}
|
}
|
||||||
if (
|
|
||||||
$result->enrollment_status() === AuthResult::ENROLLMENT_STATUS_NO
|
|
||||||
&& ! $result->authentication_result()
|
|
||||||
) {
|
|
||||||
return self::PROCEED;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if ( $result->authentication_result() === AuthResult::AUTHENTICATION_RESULT_REJECTED ) {
|
if ( $authentication === AuthResult::AUTHENTICATION_RESULT_REJECTED ) {
|
||||||
return self::REJECT;
|
return self::REJECT;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ( $result->authentication_result() === AuthResult::AUTHENTICATION_RESULT_NO ) {
|
if ( $authentication === AuthResult::AUTHENTICATION_RESULT_NO ) {
|
||||||
return self::REJECT;
|
return self::REJECT;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ( $result->authentication_result() === AuthResult::AUTHENTICATION_RESULT_UNABLE ) {
|
if ( $authentication === AuthResult::AUTHENTICATION_RESULT_UNABLE ) {
|
||||||
return self::RETRY;
|
return self::RETRY;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ( ! $result->authentication_result() ) {
|
if ( ! $authentication ) {
|
||||||
return self::RETRY;
|
return self::RETRY;
|
||||||
}
|
}
|
||||||
|
|
||||||
return self::NO_DECISION;
|
return self::NO_DECISION;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -44,7 +44,6 @@ import moduleStorage from './Helper/GooglePayStorage';
|
||||||
* @property {Function} createButton - The convenience method is used to generate a Google Pay payment button styled with the latest Google Pay branding for insertion into a webpage.
|
* @property {Function} createButton - The convenience method is used to generate a Google Pay payment button styled with the latest Google Pay branding for insertion into a webpage.
|
||||||
* @property {Function} isReadyToPay - Use the isReadyToPay(isReadyToPayRequest) method to determine a user's ability to return a form of payment from the Google Pay API.
|
* @property {Function} isReadyToPay - Use the isReadyToPay(isReadyToPayRequest) method to determine a user's ability to return a form of payment from the Google Pay API.
|
||||||
* @property {(Object) => Promise} loadPaymentData - This method presents a Google Pay payment sheet that allows selection of a payment method and optionally configured parameters
|
* @property {(Object) => Promise} loadPaymentData - This method presents a Google Pay payment sheet that allows selection of a payment method and optionally configured parameters
|
||||||
* @property {Function} onPaymentAuthorized - This method is called when a payment is authorized in the payment sheet.
|
|
||||||
* @property {Function} onPaymentDataChanged - This method handles payment data changes in the payment sheet such as shipping address and shipping options.
|
* @property {Function} onPaymentDataChanged - This method handles payment data changes in the payment sheet such as shipping address and shipping options.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
@ -63,6 +62,21 @@ import moduleStorage from './Helper/GooglePayStorage';
|
||||||
* @property {string} checkoutOption - Optional. Affects the submit button text displayed in the Google Pay payment sheet.
|
* @property {string} checkoutOption - Optional. Affects the submit button text displayed in the Google Pay payment sheet.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates that the payment was approved by PayPal and can be processed.
|
||||||
|
*/
|
||||||
|
const ORDER_APPROVED = 'approved';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* We should not process this order, as it failed for some reason.
|
||||||
|
*/
|
||||||
|
const ORDER_FAILED = 'failed';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The order is still pending, and we need to request 3DS details from the customer.
|
||||||
|
*/
|
||||||
|
const ORDER_INCOMPLETE = 'payerAction';
|
||||||
|
|
||||||
function payerDataFromPaymentResponse( response ) {
|
function payerDataFromPaymentResponse( response ) {
|
||||||
const raw = response?.paymentMethodData?.info?.billingAddress;
|
const raw = response?.paymentMethodData?.info?.billingAddress;
|
||||||
|
|
||||||
|
@ -190,7 +204,6 @@ class GooglepayButton extends PaymentButton {
|
||||||
);
|
);
|
||||||
|
|
||||||
this.init = this.init.bind( this );
|
this.init = this.init.bind( this );
|
||||||
this.onPaymentAuthorized = this.onPaymentAuthorized.bind( this );
|
|
||||||
this.onPaymentDataChanged = this.onPaymentDataChanged.bind( this );
|
this.onPaymentDataChanged = this.onPaymentDataChanged.bind( this );
|
||||||
this.onButtonClick = this.onButtonClick.bind( this );
|
this.onButtonClick = this.onButtonClick.bind( this );
|
||||||
|
|
||||||
|
@ -411,8 +424,6 @@ class GooglepayButton extends PaymentButton {
|
||||||
return callbacks;
|
return callbacks;
|
||||||
}
|
}
|
||||||
|
|
||||||
callbacks.onPaymentAuthorized = this.onPaymentAuthorized;
|
|
||||||
|
|
||||||
if ( this.requiresShipping ) {
|
if ( this.requiresShipping ) {
|
||||||
callbacks.onPaymentDataChanged = this.onPaymentDataChanged;
|
callbacks.onPaymentDataChanged = this.onPaymentDataChanged;
|
||||||
}
|
}
|
||||||
|
@ -536,10 +547,10 @@ class GooglepayButton extends PaymentButton {
|
||||||
/**
|
/**
|
||||||
* Show Google Pay payment sheet when Google Pay payment button is clicked
|
* Show Google Pay payment sheet when Google Pay payment button is clicked
|
||||||
*/
|
*/
|
||||||
onButtonClick() {
|
async onButtonClick() {
|
||||||
this.log( 'onButtonClick' );
|
this.logGroup( 'onButtonClick' );
|
||||||
|
|
||||||
const initiatePaymentRequest = () => {
|
const initiatePaymentRequest = async () => {
|
||||||
window.ppcpFundingSource = 'googlepay';
|
window.ppcpFundingSource = 'googlepay';
|
||||||
const paymentDataRequest = this.paymentDataRequest();
|
const paymentDataRequest = this.paymentDataRequest();
|
||||||
|
|
||||||
|
@ -549,10 +560,19 @@ class GooglepayButton extends PaymentButton {
|
||||||
this.context
|
this.context
|
||||||
);
|
);
|
||||||
|
|
||||||
return this.paymentsClient.loadPaymentData( paymentDataRequest );
|
return this.paymentsClient
|
||||||
|
.loadPaymentData( paymentDataRequest )
|
||||||
|
.then( ( paymentData ) => {
|
||||||
|
this.log( 'loadPaymentData response:', paymentData );
|
||||||
|
return paymentData;
|
||||||
|
} )
|
||||||
|
.catch( ( error ) => {
|
||||||
|
this.error( 'loadPaymentData failed:', error );
|
||||||
|
throw error;
|
||||||
|
} );
|
||||||
};
|
};
|
||||||
|
|
||||||
const validateForm = () => {
|
const validateForm = async () => {
|
||||||
if ( 'function' !== typeof this.contextHandler.validateForm ) {
|
if ( 'function' !== typeof this.contextHandler.validateForm ) {
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
}
|
}
|
||||||
|
@ -563,7 +583,7 @@ class GooglepayButton extends PaymentButton {
|
||||||
} );
|
} );
|
||||||
};
|
};
|
||||||
|
|
||||||
const getTransactionInfo = () => {
|
const getTransactionInfo = async () => {
|
||||||
if ( 'function' !== typeof this.contextHandler.transactionInfo ) {
|
if ( 'function' !== typeof this.contextHandler.transactionInfo ) {
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
}
|
}
|
||||||
|
@ -579,9 +599,18 @@ class GooglepayButton extends PaymentButton {
|
||||||
} );
|
} );
|
||||||
};
|
};
|
||||||
|
|
||||||
validateForm()
|
const paymentData = await validateForm()
|
||||||
.then( getTransactionInfo )
|
.then( getTransactionInfo )
|
||||||
.then( initiatePaymentRequest );
|
.then( initiatePaymentRequest );
|
||||||
|
|
||||||
|
this.logGroup();
|
||||||
|
|
||||||
|
// If something failed above, stop here. Only continue if we have the paymentData.
|
||||||
|
if ( ! paymentData ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.processPayment( paymentData );
|
||||||
}
|
}
|
||||||
|
|
||||||
paymentDataRequest() {
|
paymentDataRequest() {
|
||||||
|
@ -591,7 +620,7 @@ class GooglepayButton extends PaymentButton {
|
||||||
};
|
};
|
||||||
|
|
||||||
const useShippingCallback = this.requiresShipping;
|
const useShippingCallback = this.requiresShipping;
|
||||||
const callbackIntents = [ 'PAYMENT_AUTHORIZATION' ];
|
const callbackIntents = [];
|
||||||
|
|
||||||
if ( useShippingCallback ) {
|
if ( useShippingCallback ) {
|
||||||
callbackIntents.push( 'SHIPPING_ADDRESS', 'SHIPPING_OPTION' );
|
callbackIntents.push( 'SHIPPING_ADDRESS', 'SHIPPING_OPTION' );
|
||||||
|
@ -791,25 +820,31 @@ class GooglepayButton extends PaymentButton {
|
||||||
// Payment process
|
// Payment process
|
||||||
//------------------------
|
//------------------------
|
||||||
|
|
||||||
onPaymentAuthorized( paymentData ) {
|
|
||||||
this.log( 'onPaymentAuthorized', paymentData );
|
|
||||||
|
|
||||||
return this.processPayment( paymentData );
|
|
||||||
}
|
|
||||||
|
|
||||||
async processPayment( paymentData ) {
|
async processPayment( paymentData ) {
|
||||||
this.logGroup( 'processPayment' );
|
this.logGroup( 'processPayment' );
|
||||||
|
|
||||||
|
let result;
|
||||||
|
|
||||||
const payer = payerDataFromPaymentResponse( paymentData );
|
const payer = payerDataFromPaymentResponse( paymentData );
|
||||||
|
|
||||||
|
const paymentResponse = ( state, intent = null, message = null ) => {
|
||||||
|
const response = {
|
||||||
|
transactionState: state,
|
||||||
|
};
|
||||||
|
|
||||||
|
if ( intent || message ) {
|
||||||
|
response.error = { intent, message };
|
||||||
|
}
|
||||||
|
|
||||||
|
this.log( 'processPaymentResponse', response );
|
||||||
|
|
||||||
|
return response;
|
||||||
|
};
|
||||||
|
|
||||||
const paymentError = ( reason ) => {
|
const paymentError = ( reason ) => {
|
||||||
this.error( reason );
|
this.error( reason );
|
||||||
|
|
||||||
return this.processPaymentResponse(
|
return paymentResponse( 'ERROR', 'PAYMENT_AUTHORIZATION', reason );
|
||||||
'ERROR',
|
|
||||||
'PAYMENT_AUTHORIZATION',
|
|
||||||
reason
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const checkPayPalApproval = async ( orderId ) => {
|
const checkPayPalApproval = async ( orderId ) => {
|
||||||
|
@ -824,15 +859,41 @@ class GooglepayButton extends PaymentButton {
|
||||||
|
|
||||||
this.log( 'confirmOrder', confirmOrderResponse );
|
this.log( 'confirmOrder', confirmOrderResponse );
|
||||||
|
|
||||||
return 'APPROVED' === confirmOrderResponse?.status;
|
switch ( confirmOrderResponse?.status ) {
|
||||||
|
case 'APPROVED':
|
||||||
|
return ORDER_APPROVED;
|
||||||
|
|
||||||
|
case 'PAYER_ACTION_REQUIRED':
|
||||||
|
return ORDER_INCOMPLETE;
|
||||||
|
|
||||||
|
default:
|
||||||
|
return ORDER_FAILED;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initiates payer action and handles the 3DS contingency.
|
||||||
|
*
|
||||||
|
* @param {string} orderID
|
||||||
|
*/
|
||||||
|
const initiatePayerAction = ( orderID ) => {
|
||||||
|
this.log( 'initiatePayerAction', orderID );
|
||||||
|
|
||||||
|
return widgetBuilder.paypal
|
||||||
|
.Googlepay()
|
||||||
|
.initiatePayerAction( { orderId: orderID } );
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This approval mainly confirms that the orderID is valid.
|
* This approval mainly confirms that the orderID is valid.
|
||||||
*
|
*
|
||||||
* It's still needed because this handler redirects to the checkout page if the server-side
|
* It's still needed because this handler REDIRECTS to the checkout page if the server-side
|
||||||
* approval was successful.
|
* approval was successful.
|
||||||
*
|
*
|
||||||
|
* I.e. on success, the approveOrder handler initiates a browser navigation; this means
|
||||||
|
* it should be the last action that happens in the payment process.
|
||||||
|
*
|
||||||
|
* @see onApproveForContinue.js
|
||||||
* @param {string} orderID
|
* @param {string} orderID
|
||||||
*/
|
*/
|
||||||
const approveOrderServerSide = async ( orderID ) => {
|
const approveOrderServerSide = async ( orderID ) => {
|
||||||
|
@ -860,61 +921,40 @@ class GooglepayButton extends PaymentButton {
|
||||||
return isApproved;
|
return isApproved;
|
||||||
};
|
};
|
||||||
|
|
||||||
const processPaymentPromise = async ( resolve ) => {
|
// Add billing data to session.
|
||||||
const id = await this.contextHandler.createOrder();
|
moduleStorage.setPayer( payer );
|
||||||
|
setPayerData( payer );
|
||||||
|
|
||||||
this.log( 'createOrder', id );
|
try {
|
||||||
|
const orderId = await this.contextHandler.createOrder();
|
||||||
|
this.log( 'createOrder', orderId );
|
||||||
|
|
||||||
const isApprovedByPayPal = await checkPayPalApproval( id );
|
const orderState = await checkPayPalApproval( orderId );
|
||||||
|
|
||||||
if ( ! isApprovedByPayPal ) {
|
if ( ORDER_FAILED === orderState ) {
|
||||||
resolve( paymentError( 'TRANSACTION FAILED' ) );
|
result = paymentError( 'TRANSACTION FAILED' );
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// This must be the last step in the process, as it initiates a redirect.
|
|
||||||
const success = await approveOrderServerSide( id );
|
|
||||||
|
|
||||||
if ( success ) {
|
|
||||||
resolve( this.processPaymentResponse( 'SUCCESS' ) );
|
|
||||||
} else {
|
} else {
|
||||||
resolve( paymentError( 'FAILED TO APPROVE' ) );
|
// This payment requires a 3DS verification before we can process the order.
|
||||||
|
if ( ORDER_INCOMPLETE === orderState ) {
|
||||||
|
const response = await initiatePayerAction( orderId );
|
||||||
|
this.log( '3DS verification completed', response );
|
||||||
|
}
|
||||||
|
|
||||||
|
const success = await approveOrderServerSide( orderId );
|
||||||
|
|
||||||
|
if ( success ) {
|
||||||
|
result = paymentResponse( 'SUCCESS' );
|
||||||
|
} else {
|
||||||
|
result = paymentError( 'FAILED TO APPROVE' );
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
} catch ( err ) {
|
||||||
|
result = paymentError( err.message );
|
||||||
const addBillingDataToSession = () => {
|
|
||||||
moduleStorage.setPayer( payer );
|
|
||||||
setPayerData( payer );
|
|
||||||
};
|
|
||||||
|
|
||||||
return new Promise( async ( resolve ) => {
|
|
||||||
try {
|
|
||||||
addBillingDataToSession();
|
|
||||||
await processPaymentPromise( resolve );
|
|
||||||
} catch ( err ) {
|
|
||||||
resolve( paymentError( err.message ) );
|
|
||||||
}
|
|
||||||
|
|
||||||
this.logGroup();
|
|
||||||
} );
|
|
||||||
}
|
|
||||||
|
|
||||||
processPaymentResponse( state, intent = null, message = null ) {
|
|
||||||
const response = {
|
|
||||||
transactionState: state,
|
|
||||||
};
|
|
||||||
|
|
||||||
if ( intent || message ) {
|
|
||||||
response.error = {
|
|
||||||
intent,
|
|
||||||
message,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.log( 'processPaymentResponse', response );
|
this.logGroup();
|
||||||
|
|
||||||
return response;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -22,6 +22,7 @@ use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ModuleClassNameI
|
||||||
use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ServiceModule;
|
use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ServiceModule;
|
||||||
use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;
|
use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;
|
||||||
use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings;
|
use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings;
|
||||||
|
use WooCommerce\PayPalCommerce\WcGateway\Gateway\CreditCardGateway;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class GooglepayModule
|
* Class GooglepayModule
|
||||||
|
@ -248,6 +249,42 @@ class GooglepayModule implements ServiceModule, ExtendingModule, ExecutableModul
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
add_filter(
|
||||||
|
'ppcp_create_order_request_body_data',
|
||||||
|
static function ( array $data, string $payment_method, array $request ) use ( $c ) : array {
|
||||||
|
|
||||||
|
$funding_source = $request['funding_source'];
|
||||||
|
if ( $payment_method !== GooglePayGateway::ID && $funding_source !== 'googlepay' ) {
|
||||||
|
return $data;
|
||||||
|
}
|
||||||
|
|
||||||
|
$settings = $c->get( 'wcgateway.settings' );
|
||||||
|
assert( $settings instanceof Settings );
|
||||||
|
|
||||||
|
$three_d_secure_contingency =
|
||||||
|
$settings->has( '3d_secure_contingency' )
|
||||||
|
? apply_filters( 'woocommerce_paypal_payments_three_d_secure_contingency', $settings->get( '3d_secure_contingency' ) )
|
||||||
|
: '';
|
||||||
|
|
||||||
|
if (
|
||||||
|
$three_d_secure_contingency === 'SCA_ALWAYS'
|
||||||
|
|| $three_d_secure_contingency === 'SCA_WHEN_REQUIRED'
|
||||||
|
) {
|
||||||
|
$data['payment_source']['google_pay'] = array(
|
||||||
|
'attributes' => array(
|
||||||
|
'verification' => array(
|
||||||
|
'method' => $three_d_secure_contingency,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $data;
|
||||||
|
},
|
||||||
|
10,
|
||||||
|
3
|
||||||
|
);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue