Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add ACSS payment tokenization #4013

Open
wants to merge 10 commits into
base: develop
Choose a base branch
from
1 change: 1 addition & 0 deletions changelog.txt
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
* Fix - Fix issue where Legacy Checkout settings get overwritten with old value.
* Add - Add WooCommerce Pre-Orders support to Bacs.
* Tweak - Fix background in express checkout settings.
* Add - Add ACSS payment tokenization.

= 9.2.0 - 2025-02-13 =
* Fix - Fix missing product_id parameter for the express checkout add-to-cart operation.
Expand Down
5 changes: 4 additions & 1 deletion client/api/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -110,10 +110,13 @@ export default class WCStripeAPI {
/**
* Creates a setup intent without confirming it.
*
* @param {string} paymentMethodType The type of payment method.
*
* @return {Promise} The final promise for the request to the server.
*/
initSetupIntent() {
initSetupIntent( paymentMethodType ) {
return this.request( this.getAjaxUrl( 'init_setup_intent' ), {
payment_method_type: paymentMethodType,
_ajax_nonce: this.options?.createSetupIntentNonce,
} )
.then( ( response ) => {
Expand Down
7 changes: 6 additions & 1 deletion client/classic/upe/deferred-intent.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,9 +53,14 @@ jQuery( function ( $ ) {
$( 'form#order_review' ).length
) {
maybeMountStripePaymentElement();

// For payment methods that don't support deferred intents, we mount the Payment Element only when the PM is selected.
$( 'input[name="payment_method"]' ).on( 'change', () => {
maybeMountStripePaymentElement();
} );
}

// For payment methods that don't support deferred intents, we mount the Payment Element only when it's selected.
// For payment methods that don't support deferred intents, we mount the Payment Element only when the PM is selected.
$( 'form.checkout' ).on( 'change', 'input[name="payment_method"]', () => {
maybeMountStripePaymentElement();
} );
Expand Down
11 changes: 10 additions & 1 deletion client/classic/upe/payment-processing.js
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,16 @@ async function createStripePaymentElement( api, paymentMethodType ) {
// If the payment method doesn't support deferred intent, the intent must be created here.
if ( ! supportsDeferredIntent ) {
try {
intent = await api.createIntent( null, paymentMethodType );
const isSetupIntent =
document.getElementById( 'add_payment_method' ) ||
! getStripeServerData()?.isPaymentNeeded ||
getStripeServerData()?.isChangingPayment;

if ( isSetupIntent ) {
intent = await api.initSetupIntent( paymentMethodType );
} else {
intent = await api.createIntent( null, paymentMethodType );
}
} catch ( error ) {
showErrorPaymentMethod(
error?.message ??
Expand Down
1 change: 1 addition & 0 deletions includes/class-wc-stripe-customer.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ class WC_Stripe_Customer {
WC_Stripe_UPE_Payment_Method_Sepa::STRIPE_ID,
WC_Stripe_UPE_Payment_Method_Cash_App_Pay::STRIPE_ID,
WC_Stripe_UPE_Payment_Method_ACH::STRIPE_ID,
WC_Stripe_UPE_Payment_Method_ACSS::STRIPE_ID,
WC_Stripe_UPE_Payment_Method_Bacs_Debit::STRIPE_ID,
];

Expand Down
45 changes: 32 additions & 13 deletions includes/class-wc-stripe-intent-controller.php
Original file line number Diff line number Diff line change
Expand Up @@ -506,7 +506,7 @@ public function update_payment_intent( $payment_intent_id = '', $order_id = null
* Handle AJAX requests for creating a setup intent without confirmation for Stripe UPE.
*
* @since 5.6.0
* @version 5.6.0
* @version x.x.x
*/
public function init_setup_intent_ajax() {
try {
Expand All @@ -515,7 +515,9 @@ public function init_setup_intent_ajax() {
throw new Exception( __( "We're not able to add this payment method. Please refresh the page and try again.", 'woocommerce-gateway-stripe' ) );
}

wp_send_json_success( $this->init_setup_intent(), 200 );
$payment_method_type = isset( $_POST['payment_method_type'] ) ? wc_clean( wp_unslash( $_POST['payment_method_type'] ) ) : '';

wp_send_json_success( $this->init_setup_intent( $payment_method_type ), 200 );
} catch ( Exception $e ) {
// Send back error, so it can be displayed to the customer.
wp_send_json_error(
Expand All @@ -532,11 +534,13 @@ public function init_setup_intent_ajax() {
* Creates a setup intent without confirmation.
*
* @since 5.6.0
* @version 5.6.0
* @version x.x.x
*
* @param string|null $payment_method_type The type of payment method to use for the intent.
* @return array
* @throws Exception If customer for the current user cannot be read/found.
*/
public function init_setup_intent() {
public function init_setup_intent( $payment_method_type = null ) {
// Determine the customer managing the payment methods, create one if we don't have one already.
$user = wp_get_current_user();
$customer = new WC_Stripe_Customer( $user->ID );
Expand All @@ -547,15 +551,19 @@ public function init_setup_intent() {
$customer_id = $customer->update_customer();
}

$gateway = $this->get_upe_gateway();
$payment_method_types = array_filter( $gateway->get_upe_enabled_payment_method_ids(), [ $gateway, 'is_enabled_for_saved_payments' ] );
$gateway = $this->get_upe_gateway();
$enabled_payment_methods = $payment_method_type ? [ $payment_method_type ] : array_filter( $gateway->get_upe_enabled_payment_method_ids(), [ $gateway, 'is_enabled_for_saved_payments' ] );

$request = [
'customer' => $customer_id,
'confirm' => 'false',
'payment_method_types' => $enabled_payment_methods,
];

$request = $this->maybe_add_mandate_options( $request, $payment_method_type, true );

$setup_intent = WC_Stripe_API::request(
[
'customer' => $customer_id,
'confirm' => 'false',
'payment_method_types' => array_values( $payment_method_types ),
],
$request,
'setup_intents'
);

Expand Down Expand Up @@ -763,7 +771,7 @@ public function create_and_confirm_payment_intent( $payment_information ) {
$request['statement_descriptor_suffix'] = $payment_information['statement_descriptor_suffix'];
}

if ( isset( $payment_information['payment_method_options'] ) ) {
if ( ! empty( $payment_information['payment_method_options'] ) ) {
$request['payment_method_options'] = $payment_information['payment_method_options'];
}

Expand Down Expand Up @@ -806,10 +814,11 @@ public function create_and_confirm_payment_intent( $payment_information ) {
*
* @param array $request The request array to add the mandate options to.
* @param string|null $payment_method_type The type of payment method to use for the intent.
* @param bool $is_setup_intent Whether the request is for a setup intent.
*
* @return array
*/
private function maybe_add_mandate_options( $request, $payment_method_type ) {
private function maybe_add_mandate_options( $request, $payment_method_type, $is_setup_intent = false ) {
// Add required mandate options for ACSS.
if ( WC_Stripe_UPE_Payment_Method_ACSS::STRIPE_ID === $payment_method_type ) {
$request['payment_method_options'] = [
Expand All @@ -821,6 +830,11 @@ private function maybe_add_mandate_options( $request, $payment_method_type ) {
],
],
];

// If it's a setup intent, add the CAD currency parameter.
if ( $is_setup_intent ) {
$request['payment_method_options'][ WC_Stripe_Payment_Methods::ACSS_DEBIT ]['currency'] = strtolower( WC_Stripe_Currency_Code::CANADIAN_DOLLAR );
}
}

return $request;
Expand Down Expand Up @@ -965,6 +979,9 @@ private function build_base_payment_intent_request_params( $payment_information
$request = WC_Stripe_Helper::add_mandate_data( $request );
}

// Add required mandate options for ACSS.
$request = $this->maybe_add_mandate_options( $request, $payment_information['selected_payment_type'] );

if ( $this->request_needs_redirection( $payment_method_types ) ) {
$request['return_url'] = $payment_information['return_url'];
}
Expand Down Expand Up @@ -1036,6 +1053,8 @@ public function create_and_confirm_setup_intent( $payment_information ) {
$request = WC_Stripe_Helper::add_mandate_data( $request );
}

$request = $this->maybe_add_mandate_options( $request, $payment_information['selected_payment_type'], true );

// For voucher payment methods type like Boleto, Oxxo, Multibanco, and Cash App, we shouldn't confirm the intent immediately as this is done on the front-end when displaying the voucher to the customer.
// When the intent is confirmed, Stripe sends a webhook to the store which puts the order on-hold, which we only want to happen after successfully displaying the voucher.
if ( $this->is_delayed_confirmation_required( $request['payment_method_types'] ) ) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,15 @@ public function __construct() {
$this->stripe_id = self::STRIPE_ID;
$this->title = __( 'Pre-Authorized Debit', 'woocommerce-gateway-stripe' );
$this->is_reusable = true;
$this->supported_currencies = [ WC_Stripe_Currency_Code::CANADIAN_DOLLAR ]; // The US dollar is supported, but has a high risk of failure since only a few Canadian bank accounts support it.
$this->supported_currencies = [ WC_Stripe_Currency_Code::CANADIAN_DOLLAR ]; // The US dollar is also supported, but has a high risk of failure since only a few Canadian bank accounts support it.
$this->supported_countries = [ 'CA' ];
$this->label = __( 'Pre-Authorized Debit', 'woocommerce-gateway-stripe' );
$this->description = __(
'Canadian Pre-Authorized Debit is a payment method that allows customers to pay using their Canadian bank account.',
'woocommerce-gateway-stripe'
);
$this->supports_deferred_intent = false;
$this->supports[] = 'tokenization';
}

/**
Expand All @@ -35,4 +36,25 @@ public function __construct() {
public function get_retrievable_type() {
return $this->get_id();
}

/**
* Creates an ACSS payment token for the customer.
*
* @param int $user_id The customer ID the payment token is associated with.
* @param stdClass $payment_method The payment method object.
*
* @return WC_Payment_Token_ACSS|null The payment token created.
*/
public function create_payment_token_for_user( $user_id, $payment_method ) {
$payment_token = new WC_Payment_Token_ACSS();
$payment_token->set_token( $payment_method->id );
$payment_token->set_gateway_id( WC_Stripe_Payment_Tokens::UPE_REUSABLE_GATEWAYS_BY_PAYMENT_METHOD[ self::STRIPE_ID ] );
$payment_token->set_user_id( $user_id );
$payment_token->set_last4( $payment_method->acss_debit->last4 );
$payment_token->set_bank_name( $payment_method->acss_debit->bank_name );
$payment_token->set_fingerprint( $payment_method->acss_debit->fingerprint );
$payment_token->save();

return $payment_token;
}
}
13 changes: 7 additions & 6 deletions includes/payment-methods/class-wc-stripe-upe-payment-method.php
Original file line number Diff line number Diff line change
Expand Up @@ -584,25 +584,26 @@ public function payment_fields() {
<?php if ( ! empty( $this->get_description() ) ) : ?>
<p><?php echo wp_kses_post( $this->get_description() ); ?></p>
<?php endif; ?>

<?php
if ( $display_tokenization ) {
$this->tokenization_script();
$this->saved_payment_methods();
}
?>
<fieldset id="wc-<?php echo esc_attr( $this->id ); ?>-upe-form" class="wc-upe-form wc-payment-form">
<div class="wc-stripe-upe-element" data-payment-method-type="<?php echo esc_attr( $this->stripe_id ); ?>"></div>
<div id="wc-<?php echo esc_attr( $this->id ); ?>-upe-errors" role="alert"></div>
<input type="hidden" class="wc-stripe-is-deferred-intent" name="wc-stripe-is-deferred-intent" value="1" />
</fieldset>
<?php

if ( $this->should_show_save_option() ) {
$force_save_payment = ( $display_tokenization && ! apply_filters( 'wc_stripe_display_save_payment_method_checkbox', $display_tokenization ) ) || is_add_payment_method_page();
if ( is_user_logged_in() ) {
$this->save_payment_method_checkbox( $force_save_payment );
}
}

if ( $display_tokenization ) {
$this->tokenization_script();
$this->saved_payment_methods();
}

do_action( 'wc_stripe_payment_fields_' . $this->id, $this->id );
} catch ( Exception $e ) {
// Output the error message.
Expand Down
133 changes: 133 additions & 0 deletions includes/payment-tokens/class-wc-stripe-acss-payment-token.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
<?php
if ( ! defined( 'ABSPATH' ) ) {
exit;
}

// phpcs:disable WordPress.Files.FileName

/**
* WooCommerce Stripe ACSS Payment Token.
*
* Token for ACSS.
*
* @since x.x.x
*/
class WC_Payment_Token_ACSS extends WC_Payment_Token implements WC_Stripe_Payment_Method_Comparison_Interface {
use WC_Stripe_Fingerprint_Trait;

/**
* Token Type.
*
* @var string
*/
protected $type = WC_Stripe_Payment_Methods::ACSS_DEBIT;

/**
* ACSS payment token data.
*
* @var array
*/
protected $extra_data = [
'bank_name' => '',
'last4' => '',
'payment_method_type' => WC_Stripe_Payment_Methods::ACSS_DEBIT,
'fingerprint' => '',
];

/**
* Checks if the payment method token is equal a provided payment method.
*
* @param object $payment_method Payment method object.
* @return bool
*/
public function is_equal_payment_method( $payment_method ): bool {
if ( WC_Stripe_Payment_Methods::ACSS_DEBIT === $payment_method->type
&& ( $payment_method->acss_debit->fingerprint ?? null ) === $this->get_fingerprint() ) {
return true;
}

return false;
}

/**
* Set the last four digits for the ACSS Debit Token.
*
* @param string $last4
*/
public function set_last4( $last4 ) {
$this->set_prop( 'last4', $last4 );
}

/**
* Returns the last four digits of the ACSS Token.
*
* @param string $context What the value is for. Valid values are view and edit.
* @return string The last 4 digits.
*/
public function get_last4( $context = 'view' ) {
return $this->get_prop( 'last4', $context );
}

/**
* Set Stripe payment method type.
*
* @param string $type Payment method type.
*/
public function set_payment_method_type( $type ) {
$this->set_prop( 'payment_method_type', $type );
}

/**
* Returns Stripe payment method type.
*
* @param string $context What the value is for. Valid values are view and edit.
* @return string $payment_method_type
*/
public function get_payment_method_type( $context = 'view' ) {
return $this->get_prop( 'payment_method_type', $context );
}

/**
* Set the bank name.
*
* @param string $bank_name
*/
public function set_bank_name( $bank_name ) {
$this->set_prop( 'bank_name', $bank_name );
}

/**
* Get the bank name.
*
* @param string $context What the value is for. Valid values are view and edit.
* @return string
*/
public function get_bank_name( $context = 'view' ) {
return $this->get_prop( 'bank_name', $context );
}

/**
* Returns the name of the token to display.
*
* @param string $deprecated Deprecated since WooCommerce 3.0
* @return string
*/
public function get_display_name( $deprecated = '' ) {
$display = sprintf(
/* translators: bank name, last 4 digits of account. */
__( '%1$s ending in %2$s', 'woocommerce-gateway-stripe' ),
$this->get_bank_name(),
$this->get_last4()
);

return $display;
}

/**
* Hook prefix.
*/
protected function get_hook_prefix() {
return 'woocommerce_payment_token_acss_get_';
}
}

Loading
Loading