Skip to content

Refactor WC_Stripe_Subscriptions_Trait->maybe_render_subscription_payment_method() #4568

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

Open
wants to merge 22 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
91d638b
Basic get_all_payment_methods() implementation
daledupreez Aug 7, 2025
35837db
Ensure we clear the new _all payment methods cache
daledupreez Aug 8, 2025
9cfccaf
Always send expand flags for SEPA
daledupreez Aug 8, 2025
68bee81
Refactor logic to get customer UPE tokens
daledupreez Aug 8, 2025
c622d76
Refactor WC_Stripe_Subscriptions_Trait->maybe_render_subscription_pay…
daledupreez Aug 8, 2025
2e20c28
Revert "Refactor WC_Stripe_Subscriptions_Trait->maybe_render_subscrip…
daledupreez Aug 8, 2025
5dc3c1e
Reapply "Refactor WC_Stripe_Subscriptions_Trait->maybe_render_subscri…
daledupreez Aug 8, 2025
80bfbbf
Update unit test to use new cache
daledupreez Aug 11, 2025
0ddecfb
Merge branch 'develop' into refactor/subscriptions-trait-payment-meth…
daledupreez Aug 11, 2025
58e88e5
Merge branch 'develop' into refactor/subscriptions-trait-payment-meth…
daledupreez Aug 12, 2025
e87b5c9
Merge branch 'develop' into refactor/subscriptions-trait-payment-meth…
daledupreez Aug 15, 2025
7511081
Switch to single call to fetch payment method and add static variable…
daledupreez Aug 15, 2025
9af152c
Make API construction more declarative
daledupreez Aug 15, 2025
83cd681
Merge branch 'develop' into refactor/subscriptions-trait-payment-meth…
daledupreez Aug 15, 2025
5aabff5
Changelog
daledupreez Aug 15, 2025
b0f5b3a
Update and expand unit tests
daledupreez Aug 15, 2025
3a2a111
Merge branch 'develop' into refactor/subscriptions-trait-payment-meth…
daledupreez Aug 18, 2025
9e6ebdd
Change parameter to $payment_method instead of $source
daledupreez Aug 18, 2025
2920871
Move payment method fetch to WC_Stripe_Subscriptions_Helper
daledupreez Aug 18, 2025
3aecb6c
Switch to WC_Stripe_API::get_payment_method() to support src_ IDs
daledupreez Aug 18, 2025
8d2077e
Update unit tests
daledupreez Aug 18, 2025
7522668
Use counter for IDs
daledupreez Aug 18, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
96 changes: 95 additions & 1 deletion includes/class-wc-stripe-customer.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,11 @@ class WC_Stripe_Customer {
WC_Stripe_UPE_Payment_Method_Becs_Debit::STRIPE_ID,
];

/**
* The maximum value for the `limit` argument in the Stripe payment_methods API.
*/
protected const PAYMENT_METHODS_API_LIMIT = 100;

/**
* Stripe customer ID
*
Expand Down Expand Up @@ -712,7 +717,7 @@ public function get_payment_methods( $payment_method_type ) {
[
'customer' => $this->get_id(),
'type' => $payment_method_type,
'limit' => 100, // Maximum allowed value.
'limit' => self::PAYMENT_METHODS_API_LIMIT,
],
'payment_methods' . $params,
'GET'
Expand Down Expand Up @@ -741,6 +746,94 @@ public function get_payment_methods( $payment_method_type ) {
return empty( $payment_methods ) ? [] : $payment_methods;
}

/**
* Get all payment methods for a customer.
*
* @param string[] $payment_method_types The payment method types to look for using Stripe method IDs. If the array is empty, it implies all payment method types.
* @param int $limit The maximum number of payment methods to return. If the value is -1, no limit is applied.
* @return array
*/
public function get_all_payment_methods( array $payment_method_types = [], int $limit = -1 ) {
if ( ! $this->get_id() ) {
return [];
}

$cache_key = self::PAYMENT_METHODS_TRANSIENT_KEY . '__all_' . $this->get_id();
$all_payment_methods = get_transient( $cache_key );

if ( false === $all_payment_methods || ! is_array( $all_payment_methods ) ) {
$all_payment_methods = [];
$last_payment_method_id = null;

do {
$request_params = [
'customer' => $this->get_id(),
'limit' => self::PAYMENT_METHODS_API_LIMIT,
];

if ( $last_payment_method_id ) {
$request_params['starting_after'] = $last_payment_method_id;
}

$response = WC_Stripe_API::request( $request_params, 'payment_methods?expand[]=data.sepa_debit.generated_from.charge&expand[]=data.sepa_debit.generated_from.setup_attempt', 'GET' );

if ( ! empty( $response->error ) ) {
if (
isset( $response->error->code )
&& isset( $response->error->param )
&& 'customer' === $response->error->param
&& 'resource_missing' === $response->error->code
) {
// If the customer doesn't exist, cache an empty array.
set_transient( $cache_key, [], DAY_IN_SECONDS );
}
return [];
}

if ( ! is_array( $response->data ) || [] === $response->data ) {
break;
}

$all_payment_methods = array_merge( $all_payment_methods, $response->data );

// Reset the last payment method ID so we can paginate correctly.
$last_payment_method_id = null;
if ( isset( $response->has_more ) && $response->has_more ) {
$last_payment_method = end( $response->data );

if ( $last_payment_method && ! empty( $last_payment_method->id ) ) {
$last_payment_method_id = $last_payment_method->id;
}
}
} while ( null !== $last_payment_method_id );

// Always cache the result without any filters applied.
set_transient( $cache_key, $all_payment_methods, DAY_IN_SECONDS );
}

// If there are no payment methods, no need to apply any filters below.
if ( [] === $all_payment_methods ) {
return $all_payment_methods;
}

// Only apply the limit and type filters after fetching and caching all payment methods.
$filtered_payment_methods = $all_payment_methods;
if ( [] !== $payment_method_types ) {
$filtered_payment_methods = array_filter(
$filtered_payment_methods,
function ( $payment_method ) use ( $payment_method_types ) {
return in_array( $payment_method->type, $payment_method_types, true );
}
);
}

if ( $limit > 0 ) {
return array_slice( $filtered_payment_methods, 0, $limit );
}

return $filtered_payment_methods;
}

/**
* Delete a source from stripe.
*
Expand Down Expand Up @@ -846,6 +939,7 @@ public function clear_cache( $payment_method_id = null ) {
foreach ( self::STRIPE_PAYMENT_METHODS as $payment_method_type ) {
delete_transient( self::PAYMENT_METHODS_TRANSIENT_KEY . $payment_method_type . $this->get_id() );
}
delete_transient( self::PAYMENT_METHODS_TRANSIENT_KEY . '__all_' . $this->get_id() );
// Clear cache for the specific payment method if provided.
if ( $payment_method_id ) {
WC_Stripe_Database_Cache::delete( 'payment_method_for_source_' . $payment_method_id );
Expand Down
135 changes: 70 additions & 65 deletions includes/compat/trait-wc-stripe-subscriptions.php
Original file line number Diff line number Diff line change
Expand Up @@ -1013,80 +1013,85 @@ public function maybe_render_subscription_payment_method( $payment_method_to_dis

$stripe_customer->set_id( $stripe_customer_id );

$payment_method_to_display = __( 'N/A', 'woocommerce-gateway-stripe' );

try {
// Retrieve all possible payment methods for subscriptions.
foreach ( WC_Stripe_Customer::STRIPE_PAYMENT_METHODS as $payment_method_type ) {
foreach ( $stripe_customer->get_payment_methods( $payment_method_type ) as $source ) {
if ( $source->id !== $stripe_source_id ) {
continue;
}
$customer_payment_methods = $stripe_customer->get_all_payment_methods();

// Legacy handling for Stripe Card objects. ref: https://docs.stripe.com/api/cards/object
if ( isset( $source->object ) && WC_Stripe_Payment_Methods::CARD === $source->object ) {
/* translators: 1) card brand 2) last 4 digits */
$payment_method_to_display = sprintf( __( 'Via %1$s card ending in %2$s', 'woocommerce-gateway-stripe' ), ( isset( $source->brand ) ? wc_get_credit_card_type_label( $source->brand ) : __( 'N/A', 'woocommerce-gateway-stripe' ) ), $source->last4 );
break 2;
}

switch ( $source->type ) {
case WC_Stripe_Payment_Methods::CARD:
/* translators: 1) card brand 2) last 4 digits */
$payment_method_to_display = sprintf( __( 'Via %1$s card ending in %2$s', 'woocommerce-gateway-stripe' ), ( isset( $source->card->brand ) ? wc_get_credit_card_type_label( $source->card->brand ) : __( 'N/A', 'woocommerce-gateway-stripe' ) ), $source->card->last4 );
break 3;
case WC_Stripe_Payment_Methods::SEPA_DEBIT:
/* translators: 1) last 4 digits of SEPA Direct Debit */
$payment_method_to_display = sprintf( __( 'Via SEPA Direct Debit ending in %1$s', 'woocommerce-gateway-stripe' ), $source->sepa_debit->last4 );
break 3;
case WC_Stripe_Payment_Methods::CASHAPP_PAY:
/* translators: 1) Cash App Cashtag */
$payment_method_to_display = sprintf( __( 'Via Cash App Pay (%1$s)', 'woocommerce-gateway-stripe' ), $source->cashapp->cashtag );
break 3;
case WC_Stripe_Payment_Methods::LINK:
/* translators: 1) email address associated with the Stripe Link payment method */
$payment_method_to_display = sprintf( __( 'Via Stripe Link (%1$s)', 'woocommerce-gateway-stripe' ), $source->link->email );
break 3;
case WC_Stripe_Payment_Methods::ACH:
$payment_method_to_display = sprintf(
/* translators: 1) account type (checking, savings), 2) last 4 digits of account. */
__( 'Via %1$s Account ending in %2$s', 'woocommerce-gateway-stripe' ),
ucfirst( $source->us_bank_account->account_type ),
$source->us_bank_account->last4
);
break 3;
case WC_Stripe_Payment_Methods::BECS_DEBIT:
$payment_method_to_display = sprintf(
/* translators: last 4 digits of account. */
__( 'BECS Direct Debit ending in %s', 'woocommerce-gateway-stripe' ),
$source->au_becs_debit->last4
);
break 3;
case WC_Stripe_Payment_Methods::ACSS_DEBIT:
$payment_method_to_display = sprintf(
/* translators: 1) bank name, 2) last 4 digits of account. */
__( 'Via %1$s ending in %2$s', 'woocommerce-gateway-stripe' ),
$source->acss_debit->bank_name,
$source->acss_debit->last4
);
break 3;
case WC_Stripe_Payment_Methods::BACS_DEBIT:
/* translators: 1) the Bacs Direct Debit payment method's last 4 numbers */
$payment_method_to_display = sprintf( __( 'Via Bacs Direct Debit ending in (%1$s)', 'woocommerce-gateway-stripe' ), $source->bacs_debit->last4 );
break 3;
case WC_Stripe_Payment_Methods::AMAZON_PAY:
/* translators: 1) the Amazon Pay payment method's email */
$payment_method_to_display = sprintf( __( 'Via Amazon Pay (%1$s)', 'woocommerce-gateway-stripe' ), $source->billing_details->email ?? '' );
break 3;
}
foreach ( $customer_payment_methods as $payment_method ) {
if ( $payment_method->id === $stripe_source_id ) {
return $this->get_payment_method_to_display_for_payment_method( $payment_method );
}
}
} catch ( WC_Stripe_Exception $e ) {
wc_add_notice( $e->getLocalizedMessage(), 'error' );
WC_Stripe_Logger::log( 'Error: ' . $e->getMessage() );
}

return $payment_method_to_display;
return __( 'N/A', 'woocommerce-gateway-stripe' );
}

/**
* Helper function to get the descriptive text for a payment method or source.
*
* @param object $source The payment method or source object.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Optional: My preference would be to call it a payment_method in all the new methods.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good call. Updated in 9e6ebdd

* @return string The descriptive text for the payment method or source.
*/
protected function get_payment_method_to_display_for_payment_method( object $source ): string {
// Legacy handling for Stripe Card objects. ref: https://docs.stripe.com/api/cards/object
if ( isset( $source->object ) && WC_Stripe_Payment_Methods::CARD === $source->object ) {
return sprintf(
/* translators: 1) card brand 2) last 4 digits */
__( 'Via %1$s card ending in %2$s', 'woocommerce-gateway-stripe' ),
( isset( $source->brand ) ? wc_get_credit_card_type_label( $source->brand ) : __( 'N/A', 'woocommerce-gateway-stripe' ) ),
$source->last4
);
}

switch ( $source->type ) {
case WC_Stripe_Payment_Methods::CARD:
return sprintf(
/* translators: 1) card brand 2) last 4 digits */
__( 'Via %1$s card ending in %2$s', 'woocommerce-gateway-stripe' ),
( isset( $source->card->brand ) ? wc_get_credit_card_type_label( $source->card->brand ) : __( 'N/A', 'woocommerce-gateway-stripe' ) ),
$source->card->last4
);
case WC_Stripe_Payment_Methods::SEPA_DEBIT:
/* translators: 1) last 4 digits of SEPA Direct Debit */
return sprintf( __( 'Via SEPA Direct Debit ending in %1$s', 'woocommerce-gateway-stripe' ), $source->sepa_debit->last4 );
case WC_Stripe_Payment_Methods::CASHAPP_PAY:
/* translators: 1) Cash App Cashtag */
return sprintf( __( 'Via Cash App Pay (%1$s)', 'woocommerce-gateway-stripe' ), $source->cashapp->cashtag );
case WC_Stripe_Payment_Methods::LINK:
/* translators: 1) email address associated with the Stripe Link payment method */
return sprintf( __( 'Via Stripe Link (%1$s)', 'woocommerce-gateway-stripe' ), $source->link->email );
case WC_Stripe_Payment_Methods::ACH:
return sprintf(
/* translators: 1) account type (checking, savings), 2) last 4 digits of account. */
__( 'Via %1$s Account ending in %2$s', 'woocommerce-gateway-stripe' ),
ucfirst( $source->us_bank_account->account_type ),
$source->us_bank_account->last4
);
case WC_Stripe_Payment_Methods::BECS_DEBIT:
return sprintf(
/* translators: last 4 digits of account. */
__( 'BECS Direct Debit ending in %s', 'woocommerce-gateway-stripe' ),
$source->au_becs_debit->last4
);
case WC_Stripe_Payment_Methods::ACSS_DEBIT:
return sprintf(
/* translators: 1) bank name, 2) last 4 digits of account. */
__( 'Via %1$s ending in %2$s', 'woocommerce-gateway-stripe' ),
$source->acss_debit->bank_name,
$source->acss_debit->last4
);
case WC_Stripe_Payment_Methods::BACS_DEBIT:
/* translators: 1) the Bacs Direct Debit payment method's last 4 numbers */
return sprintf( __( 'Via Bacs Direct Debit ending in (%1$s)', 'woocommerce-gateway-stripe' ), $source->bacs_debit->last4 );
case WC_Stripe_Payment_Methods::AMAZON_PAY:
/* translators: 1) the Amazon Pay payment method's email */
return sprintf( __( 'Via Amazon Pay (%1$s)', 'woocommerce-gateway-stripe' ), $source->billing_details->email ?? '' );
}

return __( 'N/A', 'woocommerce-gateway-stripe' );
}

/**
Expand Down
29 changes: 10 additions & 19 deletions includes/payment-tokens/class-wc-stripe-payment-tokens.php
Original file line number Diff line number Diff line change
Expand Up @@ -301,42 +301,33 @@ public function woocommerce_get_customer_upe_payment_tokens( $tokens, $user_id,

// Retrieve the payment methods for the enabled reusable gateways.
$payment_methods = [];

$reusable_payment_method_types = array_keys( self::UPE_REUSABLE_GATEWAYS_BY_PAYMENT_METHOD );
$active_payment_method_types = [];

if ( $gateway->is_oc_enabled() ) {
// For OC, get all available payment method types
foreach ( self::UPE_REUSABLE_GATEWAYS_BY_PAYMENT_METHOD as $payment_method_type => $reausable_gateway_id ) {
// For Optimized Checkout, get all available payment method types.
foreach ( $reusable_payment_method_types as $payment_method_type ) {
$payment_method_instance = WC_Stripe_UPE_Payment_Gateway::get_payment_method_instance( $payment_method_type );
if ( $payment_method_instance ) {
$retrieved_methods = $customer->get_payment_methods( $payment_method_type );
if ( ! empty( $retrieved_methods ) ) {
$payment_methods[] = $retrieved_methods;
}
$active_payment_method_types[] = $payment_method_type;
}
}
} else {
foreach ( self::UPE_REUSABLE_GATEWAYS_BY_PAYMENT_METHOD as $payment_method_type => $reausable_gateway_id ) {
foreach ( $reusable_payment_method_types as $payment_method_type ) {
// The payment method type doesn't match the ones we use. Nothing to do here.
if ( ! isset( $gateway->payment_methods[ $payment_method_type ] ) ) {
continue;
}

$payment_method_instance = $gateway->payment_methods[ $payment_method_type ];
if ( $payment_method_instance->is_enabled() ) {
$payment_methods[] = $customer->get_payment_methods( $payment_method_type );
$active_payment_method_types[] = $payment_method_type;
}
}
}

// Add SEPA if it is disabled and iDEAL or Bancontact are enabled. iDEAL and Bancontact tokens are saved as SEPA tokens.
if ( $gateway->is_sepa_tokens_for_other_methods_enabled() ) {
if ( $gateway->is_oc_enabled() ) {
$payment_methods[] = $customer->get_payment_methods( WC_Stripe_UPE_Payment_Method_Sepa::STRIPE_ID );
} elseif ( ! $gateway->payment_methods[ WC_Stripe_UPE_Payment_Method_Sepa::STRIPE_ID ]->is_enabled()
&& ( $gateway->payment_methods[ WC_Stripe_UPE_Payment_Method_Ideal::STRIPE_ID ]->is_enabled()
|| $gateway->payment_methods[ WC_Stripe_UPE_Payment_Method_Bancontact::STRIPE_ID ]->is_enabled() ) ) {

$payment_methods[] = $customer->get_payment_methods( WC_Stripe_UPE_Payment_Method_Sepa::STRIPE_ID );
}
}
$payment_methods = $customer->get_all_payment_methods( $active_payment_method_types );

$payment_methods = array_merge( ...$payment_methods );
$payment_method_ids = array_map(
Expand Down
Loading