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'] );
|
||||
$status_code = (int) wp_remote_retrieve_response_code( $response );
|
||||
|
||||
if ( 404 === $status_code || empty( $response['body'] ) ) {
|
||||
$error = new RuntimeException(
|
||||
__( 'Could not retrieve order.', 'woocommerce-paypal-payments' ),
|
||||
|
@ -498,6 +499,7 @@ class OrderEndpoint {
|
|||
);
|
||||
throw $error;
|
||||
}
|
||||
|
||||
if ( 200 !== $status_code ) {
|
||||
$error = new PayPalApiException(
|
||||
$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 ) => {
|
||||
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
|
||||
|
@ -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 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->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,30 +70,45 @@ 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 );
|
||||
}
|
||||
|
||||
$result = $this->authentication_result->from_paypal_response( $card_properties->authentication_result );
|
||||
$liability = $result->liability_shift();
|
||||
|
||||
$this->logger->info( '3DS Authentication Result: ' . wc_print_r( $result->to_array(), true ) );
|
||||
|
||||
if ( $result->liability_shift() === AuthResult::LIABILITY_SHIFT_POSSIBLE ) {
|
||||
if ( $liability === AuthResult::LIABILITY_SHIFT_POSSIBLE ) {
|
||||
return $this->return_decision( self::PROCEED, $order );
|
||||
}
|
||||
|
||||
if ( $result->liability_shift() === AuthResult::LIABILITY_SHIFT_UNKNOWN ) {
|
||||
if ( $liability === AuthResult::LIABILITY_SHIFT_UNKNOWN ) {
|
||||
return $this->return_decision( self::RETRY, $order );
|
||||
}
|
||||
if ( $result->liability_shift() === AuthResult::LIABILITY_SHIFT_NO ) {
|
||||
|
||||
if ( $liability === AuthResult::LIABILITY_SHIFT_NO ) {
|
||||
return $this->return_decision( $this->no_liability_shift( $result ), $order );
|
||||
}
|
||||
}
|
||||
|
||||
return $this->return_decision( self::NO_DECISION, $order );
|
||||
}
|
||||
|
@ -105,9 +120,10 @@ class ThreeDSecure {
|
|||
* @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()
|
||||
) {
|
||||
if ( ! $authentication ) {
|
||||
if ( $enrollment === AuthResult::ENROLLMENT_STATUS_BYPASS ) {
|
||||
return self::PROCEED;
|
||||
}
|
||||
|
||||
if ( $result->authentication_result() === AuthResult::AUTHENTICATION_RESULT_REJECTED ) {
|
||||
if ( $enrollment === AuthResult::ENROLLMENT_STATUS_UNAVAILABLE ) {
|
||||
return self::PROCEED;
|
||||
}
|
||||
|
||||
if ( $enrollment === AuthResult::ENROLLMENT_STATUS_NO ) {
|
||||
return self::PROCEED;
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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} 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 {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.
|
||||
*/
|
||||
|
||||
|
@ -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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* 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 ) {
|
||||
const raw = response?.paymentMethodData?.info?.billingAddress;
|
||||
|
||||
|
@ -190,7 +204,6 @@ class GooglepayButton extends PaymentButton {
|
|||
);
|
||||
|
||||
this.init = this.init.bind( this );
|
||||
this.onPaymentAuthorized = this.onPaymentAuthorized.bind( this );
|
||||
this.onPaymentDataChanged = this.onPaymentDataChanged.bind( this );
|
||||
this.onButtonClick = this.onButtonClick.bind( this );
|
||||
|
||||
|
@ -411,8 +424,6 @@ class GooglepayButton extends PaymentButton {
|
|||
return callbacks;
|
||||
}
|
||||
|
||||
callbacks.onPaymentAuthorized = this.onPaymentAuthorized;
|
||||
|
||||
if ( this.requiresShipping ) {
|
||||
callbacks.onPaymentDataChanged = this.onPaymentDataChanged;
|
||||
}
|
||||
|
@ -536,10 +547,10 @@ class GooglepayButton extends PaymentButton {
|
|||
/**
|
||||
* Show Google Pay payment sheet when Google Pay payment button is clicked
|
||||
*/
|
||||
onButtonClick() {
|
||||
this.log( 'onButtonClick' );
|
||||
async onButtonClick() {
|
||||
this.logGroup( 'onButtonClick' );
|
||||
|
||||
const initiatePaymentRequest = () => {
|
||||
const initiatePaymentRequest = async () => {
|
||||
window.ppcpFundingSource = 'googlepay';
|
||||
const paymentDataRequest = this.paymentDataRequest();
|
||||
|
||||
|
@ -549,10 +560,19 @@ class GooglepayButton extends PaymentButton {
|
|||
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 ) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
@ -563,7 +583,7 @@ class GooglepayButton extends PaymentButton {
|
|||
} );
|
||||
};
|
||||
|
||||
const getTransactionInfo = () => {
|
||||
const getTransactionInfo = async () => {
|
||||
if ( 'function' !== typeof this.contextHandler.transactionInfo ) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
@ -579,9 +599,18 @@ class GooglepayButton extends PaymentButton {
|
|||
} );
|
||||
};
|
||||
|
||||
validateForm()
|
||||
const paymentData = await validateForm()
|
||||
.then( getTransactionInfo )
|
||||
.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() {
|
||||
|
@ -591,7 +620,7 @@ class GooglepayButton extends PaymentButton {
|
|||
};
|
||||
|
||||
const useShippingCallback = this.requiresShipping;
|
||||
const callbackIntents = [ 'PAYMENT_AUTHORIZATION' ];
|
||||
const callbackIntents = [];
|
||||
|
||||
if ( useShippingCallback ) {
|
||||
callbackIntents.push( 'SHIPPING_ADDRESS', 'SHIPPING_OPTION' );
|
||||
|
@ -791,25 +820,31 @@ class GooglepayButton extends PaymentButton {
|
|||
// Payment process
|
||||
//------------------------
|
||||
|
||||
onPaymentAuthorized( paymentData ) {
|
||||
this.log( 'onPaymentAuthorized', paymentData );
|
||||
|
||||
return this.processPayment( paymentData );
|
||||
}
|
||||
|
||||
async processPayment( paymentData ) {
|
||||
this.logGroup( 'processPayment' );
|
||||
|
||||
let result;
|
||||
|
||||
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 ) => {
|
||||
this.error( reason );
|
||||
|
||||
return this.processPaymentResponse(
|
||||
'ERROR',
|
||||
'PAYMENT_AUTHORIZATION',
|
||||
reason
|
||||
);
|
||||
return paymentResponse( 'ERROR', 'PAYMENT_AUTHORIZATION', reason );
|
||||
};
|
||||
|
||||
const checkPayPalApproval = async ( orderId ) => {
|
||||
|
@ -824,15 +859,41 @@ class GooglepayButton extends PaymentButton {
|
|||
|
||||
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.
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
* 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
|
||||
*/
|
||||
const approveOrderServerSide = async ( orderID ) => {
|
||||
|
@ -860,61 +921,40 @@ class GooglepayButton extends PaymentButton {
|
|||
return isApproved;
|
||||
};
|
||||
|
||||
const processPaymentPromise = async ( resolve ) => {
|
||||
const id = await this.contextHandler.createOrder();
|
||||
|
||||
this.log( 'createOrder', id );
|
||||
|
||||
const isApprovedByPayPal = await checkPayPalApproval( id );
|
||||
|
||||
if ( ! isApprovedByPayPal ) {
|
||||
resolve( 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 {
|
||||
resolve( paymentError( 'FAILED TO APPROVE' ) );
|
||||
}
|
||||
};
|
||||
|
||||
const addBillingDataToSession = () => {
|
||||
// Add billing data to session.
|
||||
moduleStorage.setPayer( payer );
|
||||
setPayerData( payer );
|
||||
};
|
||||
|
||||
return new Promise( async ( resolve ) => {
|
||||
try {
|
||||
addBillingDataToSession();
|
||||
await processPaymentPromise( resolve );
|
||||
const orderId = await this.contextHandler.createOrder();
|
||||
this.log( 'createOrder', orderId );
|
||||
|
||||
const orderState = await checkPayPalApproval( orderId );
|
||||
|
||||
if ( ORDER_FAILED === orderState ) {
|
||||
result = paymentError( 'TRANSACTION FAILED' );
|
||||
} else {
|
||||
// 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 ) {
|
||||
resolve( paymentError( err.message ) );
|
||||
result = 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 );
|
||||
|
||||
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\Psr\Container\ContainerInterface;
|
||||
use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings;
|
||||
use WooCommerce\PayPalCommerce\WcGateway\Gateway\CreditCardGateway;
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue