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:
Emili Castells 2025-05-05 15:55:39 +02:00 committed by GitHub
commit 89e847bc52
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 336 additions and 173 deletions

View file

@ -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,

View file

@ -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 );
} );
};
};

View file

@ -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'
)
);
}
}
}

View file

@ -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;
}
}

View file

@ -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;
}
/**

View file

@ -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;
}
}