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 APM support for Subscriptions and tokens with split UPE with deferred intent #2883

Merged
merged 23 commits into from
Feb 15, 2024
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
bfa3f24
Add support flags to Stripe UPE methods
james-allan Feb 7, 2024
992ccec
Set the wc-stripe-is-deferred-intent flag when processing payments vi…
james-allan Feb 7, 2024
e56c418
Add bancontact and ideal to the list of gateways that require a mandate
james-allan Feb 7, 2024
6333f2a
Set the subscriptions payment method ID when setting the payment toke…
james-allan Feb 7, 2024
36fda59
Fix saved tokens for APMs like iDEAL, Bancontact and SEPA
james-allan Feb 7, 2024
744f4d6
fix typo and fetch the payment method object
james-allan Feb 7, 2024
c46f9e6
Make sure to pass offsession flag to block payment elements if the ca…
james-allan Feb 7, 2024
0d84171
Set the payment method ID on the subscription when the payment method…
james-allan Feb 9, 2024
80c0e81
Set the order and subscription payment method to the upe gateway's ID
james-allan Feb 9, 2024
580f399
Map payment method to the gateway ID should be recorded on the subscr…
james-allan Feb 9, 2024
5d92694
Set payment method title and ID to the APM used
james-allan Feb 13, 2024
5f76377
Make sure UPE methods can call methods on the main instance of the UP…
james-allan Feb 13, 2024
7e0569d
Fix undefined $name error
james-allan Feb 13, 2024
9463060
Merge branch 'add/deferred-intent' into fix/2872-dPE-APM-subscription…
james-allan Feb 13, 2024
6e06360
Reinstate the process payment function for upe methods
james-allan Feb 13, 2024
6911378
Only attach hooks in the subscriptions trait once
james-allan Feb 14, 2024
52a741d
Merge branch 'add/deferred-intent' into fix/2872-dPE-APM-subscription…
james-allan Feb 14, 2024
b13c162
Process UPE payment method refunds via the UPE gateway
james-allan Feb 14, 2024
b99580b
Always use the source object's payment type when processing off sessi…
james-allan Feb 14, 2024
d1f7541
Use new helper function for determining the payment method id of a up…
james-allan Feb 14, 2024
9516976
Remove APM payment methods in Stripe when their tokens are deleted lo…
a-danae Feb 15, 2024
d6690c9
Add subscription support for Sofort
james-allan Feb 15, 2024
e1e3926
Add multiple subscription support flags to Bancontact, iDEAL, Sofort …
james-allan Feb 15, 2024
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
6 changes: 4 additions & 2 deletions client/blocks/upe/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,13 +32,15 @@ Object.entries( getBlocksConfiguration()?.paymentMethodsConfig )
upeName,
upeMethods,
api,
upeConfig.testingInstructions
upeConfig.testingInstructions,
upeConfig.showSaveOption ?? false
),
edit: getDeferredIntentCreationUPEFields(
upeName,
upeMethods,
api,
upeConfig.testingInstructions
upeConfig.testingInstructions,
upeConfig.showSaveOption ?? false
),
savedTokenComponent: <SavedTokenHandler api={ api } />,
canMakePayment: () => !! api.getStripe(),
Expand Down
35 changes: 21 additions & 14 deletions client/blocks/upe/upe-deferred-intent-creation/payment-elements.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,21 +24,25 @@ const PaymentElements = ( { api, ...props } ) => {
const amount = Number( getBlocksConfiguration()?.cartTotal );
const currency = getBlocksConfiguration()?.currency.toLowerCase();
const appearance = initializeUPEAppearance();
const options = {
mode: amount < 1 ? 'setup' : 'payment',
amount,
currency,
paymentMethodCreation: 'manual',
paymentMethodTypes: getPaymentMethodTypes( props.paymentMethodId ),
appearance,
};

// If the cart contains a subscription or the payment method supports saving, we need to use off_session setup so Stripe can display appropriate terms and conditions.
if (
getBlocksConfiguration()?.cartContainsSubscription ||
props.showSaveOption
) {
options.setupFutureUsage = 'off_session';
}
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This change makes sure that when a subscription is in the cart, or if saving the method is allowed these terms are displayed to the user.

Screenshot 2024-02-09 at 2 28 40 pm


return (
<Elements
stripe={ stripe }
options={ {
mode: amount < 1 ? 'setup' : 'payment',
amount,
currency,
paymentMethodCreation: 'manual',
paymentMethodTypes: getPaymentMethodTypes(
props.paymentMethodId
),
appearance,
} }
>
<Elements stripe={ stripe } options={ options }>
<PaymentProcessor api={ api } { ...props } />
</Elements>
);
Expand All @@ -51,21 +55,24 @@ const PaymentElements = ( { api, ...props } ) => {
* @param {Array} upeMethods
* @param {WCStripeAPI} api
* @param {string} testingInstructions
* @param {boolean} showSaveOption
*
* @return {JSX.Element} Rendered Payment elements.
*/
export const getDeferredIntentCreationUPEFields = (
paymentMethodId,
upeMethods,
api,
testingInstructions
testingInstructions,
showSaveOption
) => {
return (
<PaymentElements
paymentMethodId={ paymentMethodId }
upeMethods={ upeMethods }
api={ api }
testingInstructions={ testingInstructions }
showSaveOption={ showSaveOption }
/>
);
};
8 changes: 8 additions & 0 deletions includes/class-wc-stripe-blocks-support.php
Original file line number Diff line number Diff line change
Expand Up @@ -364,6 +364,14 @@ public function add_payment_request_order_meta( PaymentContext $context, Payment
// Strip "Stripe_" from the payment method name to get the payment method type.
$payment_method_type = substr( $context->payment_method, strlen( $this->name ) + 1 );
$is_stripe_payment_method = isset( $main_gateway->payment_methods[ $payment_method_type ] );

/**
* When using the block checkout and a saved token is being used, we need to set a flag
* to indicate that deferred intent should be used.
*/
if ( $is_stripe_payment_method && isset( $data['issavedtoken'] ) && $data['issavedtoken'] ) {
$context->set_payment_data( array_merge( $data, [ 'wc-stripe-is-deferred-intent' => true ] ) );
}
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Using a saved payment token on the block checkout doesn't go via PaymentProcessor function and so this flag that we use to determine if we should process the request via the deferred intent flow isn't set.

This change makes sure we set this flag early in the request to process the block checkout.

}
}

Expand Down
18 changes: 18 additions & 0 deletions includes/class-wc-stripe-helper.php
Original file line number Diff line number Diff line change
Expand Up @@ -1105,4 +1105,22 @@ public static function get_intent_id_from_order( $order ) {

return $intent_id ?? false;
}

/**
* Fetches a list of all Stripe gateway IDs.
*
* @return array An array of all Stripe gateway IDs.
*/
public static function get_stripe_gateway_ids() {
$main_gateway = WC_Stripe::get_instance()->get_main_stripe_gateway();
$gateway_ids = [ 'stripe' => $main_gateway->id ];

if ( is_a( $main_gateway, 'WC_Stripe_UPE_Payment_Gateway' ) ) {
$gateways = $main_gateway->payment_methods;
} else {
$gateways = self::get_legacy_payment_methods();
}

return array_merge( $gateway_ids, wp_list_pluck( $gateways, 'id', 'id' ) );
}
}
11 changes: 6 additions & 5 deletions includes/class-wc-stripe-intent-controller.php
Original file line number Diff line number Diff line change
Expand Up @@ -893,12 +893,12 @@ private function get_payment_method_types_for_intent_creation( string $selected_
* @return bool True if a mandate must be shown and acknowledged by customer before deferred intent UPE payment can be processed, false otherwise.
*/
public function is_mandate_data_required( $selected_payment_type ) {
$gateway = $this->get_upe_gateway();

$is_stripe_link_enabled = 'card' === $selected_payment_type && in_array( 'link', $gateway->get_upe_enabled_payment_method_ids(), true );
$is_sepa_debit_payment = 'sepa_debit' === $selected_payment_type;
if ( in_array( $selected_payment_type, [ 'sepa_debit', 'bancontact', 'ideal' ], true ) ) {
return true;
}

return $is_stripe_link_enabled || $is_sepa_debit_payment;
return 'card' === $selected_payment_type && in_array( 'link', $this->get_upe_gateway()->get_upe_enabled_payment_method_ids(), true );
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Because Bancontact and iDEAL resolve to SEPA debit payment tokens, Stripe also requires that mandate data be set when attempting to use Bancontact and save it.

}

/**
Expand All @@ -911,11 +911,12 @@ public function is_mandate_data_required( $selected_payment_type ) {
* @return array
*/
public function create_and_confirm_setup_intent( $payment_information ) {
$request = [
$request = [
'payment_method' => $payment_information['payment_method'],
'payment_method_types' => [ $payment_information['selected_payment_type'] ],
'customer' => $payment_information['customer'],
'confirm' => 'true',
'return_url' => $payment_information['return_url'],
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Now that Bancontact and iDEAL can be used for set up intents, we need to pass a return URL here.

];

// SEPA setup intents require mandate data.
Expand Down
86 changes: 43 additions & 43 deletions includes/class-wc-stripe-payment-tokens.php
Original file line number Diff line number Diff line change
Expand Up @@ -230,7 +230,8 @@ public function woocommerce_get_customer_payment_tokens_legacy( $tokens, $custom
* @return array
*/
public function woocommerce_get_customer_upe_payment_tokens( $tokens, $user_id, $gateway_id ) {
if ( ( ! empty( $gateway_id ) && WC_Stripe_UPE_Payment_Gateway::ID !== $gateway_id ) || ! is_user_logged_in() ) {

if ( ! is_user_logged_in() || ( ! empty( $gateway_id ) && ! in_array( $gateway_id, WC_Stripe_Helper::get_stripe_gateway_ids(), true ) ) ) {
return $tokens;
}

Expand All @@ -240,62 +241,61 @@ public function woocommerce_get_customer_upe_payment_tokens( $tokens, $user_id,
return $tokens;
}

$gateway = new WC_Stripe_UPE_Payment_Gateway();
$reusable_payment_methods = array_filter( $gateway->get_upe_enabled_payment_method_ids(), [ $gateway, 'is_enabled_for_saved_payments' ] );
$customer = new WC_Stripe_Customer( $user_id );
$remaining_tokens = [];
$gateway = WC_Stripe::get_instance()->get_main_stripe_gateway();
$upe_gateway = null;

foreach ( $tokens as $token ) {
if ( WC_Stripe_UPE_Payment_Gateway::ID === $token->get_gateway_id() ) {
$payment_method_type = $this->get_payment_method_type_from_token( $token );
if ( ! in_array( $payment_method_type, $reusable_payment_methods, true ) ) {
// Remove saved token from list, if payment method is not enabled.
unset( $tokens[ $token->get_id() ] );
} else {
// Store relevant existing tokens here.
// We will use this list to check whether these methods still exist on Stripe's side.
$remaining_tokens[ $token->get_token() ] = $token;
}
foreach ( $gateway->payment_methods as $payment_gateway ) {
if ( $payment_gateway->id === $gateway_id ) {
$upe_gateway = $payment_gateway;
break;
}
}

$retrievable_payment_method_types = [];
foreach ( $reusable_payment_methods as $payment_method_id ) {
$upe_payment_method = $gateway->payment_methods[ $payment_method_id ];
if ( ! in_array( $upe_payment_method->get_retrievable_type(), $retrievable_payment_method_types, true ) ) {
$retrievable_payment_method_types[] = $upe_payment_method->get_retrievable_type();
}
if ( ! $upe_gateway || ! $upe_gateway->is_reusable() ) {
return $tokens;
}

$customer = new WC_Stripe_Customer( $user_id );
$current_tokens = [];

foreach ( $tokens as $token ) {
// Store relevant existing tokens here.
// We will use this list to check whether these methods still exist on Stripe's side.
$current_tokens[ $token->get_token() ] = $token;
}

try {
foreach ( $retrievable_payment_method_types as $payment_method_id ) {
$payment_methods = $customer->get_payment_methods( $payment_method_id );

// Prevent unnecessary recursion, WC_Payment_Token::save() ends up calling 'woocommerce_get_customer_payment_tokens' in some cases.
remove_action( 'woocommerce_get_customer_payment_tokens', [ $this, 'woocommerce_get_customer_payment_tokens' ], 10, 3 );
foreach ( $payment_methods as $payment_method ) {
if ( ! isset( $remaining_tokens[ $payment_method->id ] ) ) {
$payment_method_type = $this->get_original_payment_method_type( $payment_method );
if ( ! in_array( $payment_method_type, $reusable_payment_methods, true ) ) {
continue;
}
// Create new token for new payment method and add to list.
$upe_payment_method = $gateway->payment_methods[ $payment_method_type ];
$token = $upe_payment_method->create_payment_token_for_user( $user_id, $payment_method );
$tokens[ $token->get_id() ] = $token;
} else {
// Count that existing token for payment method is still present on Stripe.
// Remaining IDs in $remaining_tokens no longer exist with Stripe and will be eliminated.
unset( $remaining_tokens[ $payment_method->id ] );
// If this UPE method uses a different payment method type as a token, we don't want to retrieve tokens for it. ie Bancontact uses SEPA tokens.
$payment_methods = $customer->get_payment_methods( $upe_gateway->get_retrievable_type() );

// Prevent unnecessary recursion, WC_Payment_Token::save() ends up calling 'woocommerce_get_customer_payment_tokens' in some cases.
remove_action( 'woocommerce_get_customer_payment_tokens', [ $this, 'woocommerce_get_customer_payment_tokens' ], 10, 3 );

foreach ( $payment_methods as $payment_method ) {
if ( ! isset( $current_tokens[ $payment_method->id ] ) ) {
$payment_method_type = $this->get_original_payment_method_type( $payment_method );

if ( $payment_method_type !== $upe_gateway::STRIPE_ID ) {
continue;
}

// Create new token for new payment method and add to list.
$token = $upe_gateway->create_payment_token_for_user( $user_id, $payment_method );
$tokens[ $token->get_id() ] = $token;
} else {
// Count that existing token for payment method is still present on Stripe.
// Remaining IDs in $remaining_tokens no longer exist with Stripe and will be eliminated.
unset( $current_tokens[ $payment_method->id ] );
}
add_action( 'woocommerce_get_customer_payment_tokens', [ $this, 'woocommerce_get_customer_payment_tokens' ], 10, 3 );
}

add_action( 'woocommerce_get_customer_payment_tokens', [ $this, 'woocommerce_get_customer_payment_tokens' ], 10, 3 );

// Eliminate remaining payment methods no longer known by Stripe.
// Prevent unnecessary recursion, when deleting tokens.
remove_action( 'woocommerce_payment_token_deleted', [ $this, 'woocommerce_payment_token_deleted' ], 10, 2 );
foreach ( $remaining_tokens as $token ) {

foreach ( $current_tokens as $token ) {
unset( $tokens[ $token->get_id() ] );
$token->delete();
}
Expand Down
12 changes: 10 additions & 2 deletions includes/compat/trait-wc-stripe-subscriptions.php
Original file line number Diff line number Diff line change
Expand Up @@ -386,8 +386,12 @@ public function process_subscription_payment( $amount, $renewal_order, $retry =
* Updates other subscription sources.
*
* @since 5.6.0
*
* @param WC_Order $order The order object.
* @param string $source_id The source ID.
* @param string $payment_gateway_id The payment method ID. eg 'stripe.
*/
public function maybe_update_source_on_subscription_order( $order, $source ) {
public function maybe_update_source_on_subscription_order( $order, $source, $payment_gateway_id = '' ) {
if ( ! $this->is_subscriptions_enabled() ) {
return;
}
Expand All @@ -404,7 +408,6 @@ public function maybe_update_source_on_subscription_order( $order, $source ) {
}

foreach ( $subscriptions as $subscription ) {
$subscription_id = $subscription->get_id();
$subscription->update_meta_data( '_stripe_customer_id', $source->customer );

if ( ! empty( $source->payment_method ) ) {
Expand All @@ -413,6 +416,11 @@ public function maybe_update_source_on_subscription_order( $order, $source ) {
$subscription->update_meta_data( '_stripe_source_id', $source->source );
}

// Update the payment method.
if ( ! empty( $payment_gateway_id ) ) {
$subscription->set_payment_method( $payment_gateway_id );
}

$subscription->save();
}
}
Expand Down
Loading
Loading