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

Amazon Pay ECE: fix payment method issues for subscriptions #4008

Merged
merged 12 commits into from
Mar 24, 2025
Merged
1 change: 1 addition & 0 deletions changelog.txt
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
* Add - Disable unsupported payment methods in Stripe settings
* Update - Update handling of PR as a country in the terminal locations endpoint.
* Fix - Hide Amazon Pay in settings when legacy checkout is enabled.
* Fix - Fix subscription renewal issues for Amazon Pay.

= 9.3.1 - 2025-03-14 =
* Fix - Temporarily disables the subscriptions detached notice feature due to long loading times on stores with many subscriptions.
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 @@ -26,6 +26,7 @@ class WC_Stripe_Customer {
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,
WC_Stripe_UPE_Payment_Method_Amazon_Pay::STRIPE_ID,
];

/**
Expand Down
1 change: 1 addition & 0 deletions includes/class-wc-stripe-intent-controller.php
Original file line number Diff line number Diff line change
Expand Up @@ -1010,6 +1010,7 @@ public function is_mandate_data_required( $selected_payment_type, $is_using_save
$payment_methods_with_mandates = [
WC_Stripe_Payment_Methods::ACH,
WC_Stripe_Payment_Methods::ACSS_DEBIT,
WC_Stripe_Payment_Methods::AMAZON_PAY,
WC_Stripe_Payment_Methods::BACS_DEBIT,
WC_Stripe_Payment_Methods::BECS_DEBIT,
WC_Stripe_Payment_Methods::SEPA_DEBIT,
Expand Down
4 changes: 4 additions & 0 deletions includes/compat/trait-wc-stripe-subscriptions.php
Original file line number Diff line number Diff line change
Expand Up @@ -1010,6 +1010,10 @@ public function maybe_render_subscription_payment_method( $payment_method_to_dis
/* 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;
}
}
}
Expand Down
102 changes: 71 additions & 31 deletions includes/payment-methods/class-wc-stripe-upe-payment-gateway.php
Original file line number Diff line number Diff line change
Expand Up @@ -1071,6 +1071,14 @@ private function process_payment_with_confirmation_token( int $order_id ) {
}
}

if ( $payment_information['save_payment_method_to_store'] ) {
$this->handle_saving_payment_method(
$order,
$payment_method,
$selected_payment_type
);
}

$return_url = $this->get_return_url( $order );

return [
Expand Down Expand Up @@ -2223,6 +2231,8 @@ protected function prepare_payment_information_from_request( WC_Order $order ) {

if ( is_a( $token, 'WC_Payment_Token_SEPA' ) ) {
$selected_payment_type = WC_Stripe_UPE_Payment_Method_Sepa::STRIPE_ID;
} elseif ( is_a( $token, 'WC_Payment_Token_Amazon_Pay' ) ) {
$selected_payment_type = WC_Stripe_UPE_Payment_Method_Amazon_Pay::STRIPE_ID;
}
} else {
$payment_method_id = sanitize_text_field( wp_unslash( $_POST['wc-stripe-payment-method'] ?? '' ) ); // phpcs:ignore WordPress.Security.NonceVerification.Missing
Expand Down Expand Up @@ -2253,56 +2263,86 @@ protected function prepare_payment_information_from_request( WC_Order $order ) {
'return_url' => $this->get_return_url_for_redirect( $order, $save_payment_method_to_store ),
'use_stripe_sdk' => 'true', // We want to use the SDK to handle next actions via the client payment elements. See https://docs.stripe.com/api/setup_intents/create#create_setup_intent-use_stripe_sdk
'has_subscription' => $this->has_subscription( $order->get_id() ),
'payment_method' => '',
'payment_method' => $payment_method_id,
'payment_method_details' => $payment_method_details,
'payment_type' => 'single', // single | recurring.
'save_payment_method_to_store' => $save_payment_method_to_store,
'capture_method' => $capture_method,
];

if ( WC_Stripe_Payment_Methods::ACH === $selected_payment_type ) {
WC_Stripe_API::attach_payment_method_to_customer( $payment_information['customer'], $payment_method_id );
}

if ( ! empty( $payment_method_id ) ) {
$payment_information['payment_method'] = $payment_method_id;
$payment_information['save_payment_method_to_store'] = $save_payment_method_to_store;
$payment_information['payment_method_options'] = $this->get_payment_method_options(
// Use the dynamic + short statement descriptor if enabled and it's a card payment.
$is_short_statement_descriptor_enabled = 'yes' === $this->get_option( 'is_short_statement_descriptor_enabled', 'no' );
if ( WC_Stripe_Payment_Methods::CARD === $selected_payment_type && $is_short_statement_descriptor_enabled ) {
$payment_information['statement_descriptor_suffix'] = WC_Stripe_Helper::get_dynamic_statement_descriptor_suffix( $order );
}

if ( empty( $payment_method_id ) && ! empty( $_POST['wc-stripe-confirmation-token'] ) ) {
// Add fields that are only set when using the confirmation token flow.
$payment_information = $this->prepare_payment_information_for_confirmation_token(
$payment_information,
$selected_payment_type,
$order,
$payment_method_details
$capture_method,
);
$payment_information['capture_method'] = $capture_method;
} else {
$confirmation_token_id = sanitize_text_field( wp_unslash( $_POST['wc-stripe-confirmation-token'] ?? '' ) );
$payment_information['confirmation_token'] = $confirmation_token_id;
$payment_information['save_payment_method_to_store'] = false;

// When using confirmation tokens with manual capture, we need to
// set the capture_method parameter under payment method options.
if ( 'manual' === $capture_method ) {
$payment_information['payment_method_options'] = [
$selected_payment_type => [
'capture_method' => 'manual',
],
];
} else {
$payment_information['capture_method'] = $capture_method;
}
// Add fields that are only set when using the payment method flow.
$payment_information = $this->prepare_payment_information_for_payment_method( $payment_information, $selected_payment_type, $order );
}

// When using confirmation tokens for subscriptions, we need to set the setup_future_usage parameter under payment method options.
if ( $payment_information['has_subscription'] ) {
$payment_information['payment_method_options'][ $selected_payment_type ]['setup_future_usage'] = 'off_session';
}
return $payment_information;
}

/**
* Add or remove payment information fields for the confirmation token flow.
*
* @param array $payment_information The base payment information.
* @param string $selected_payment_type The selected payment type.
* @param string $capture_method The capture method to be used.
* @return array The customized payment information for the confirmation token flow.
*/
private function prepare_payment_information_for_confirmation_token( $payment_information, $selected_payment_type, $capture_method ) {
// These fields should not be set when using confirmation tokens to create a payment intent.
unset( $payment_information['payment_method'] );
unset( $payment_information['payment_method_details'] );

$confirmation_token_id = sanitize_text_field( wp_unslash( $_POST['wc-stripe-confirmation-token'] ?? '' ) );
$payment_information['confirmation_token'] = $confirmation_token_id;

// Some payment methods such as Amazon Pay will only accept a capture_method of 'manual'
// under payment_method_options instead of at the top level.
if ( 'manual' === $capture_method ) {
unset( $payment_information['capture_method'] );
$payment_information['payment_method_options'][ $selected_payment_type ]['capture_method'] = 'manual';
}

// Use the dynamic + short statement descriptor if enabled and it's a card payment.
$is_short_statement_descriptor_enabled = 'yes' === $this->get_option( 'is_short_statement_descriptor_enabled', 'no' );
if ( WC_Stripe_Payment_Methods::CARD === $selected_payment_type && $is_short_statement_descriptor_enabled ) {
$payment_information['statement_descriptor_suffix'] = WC_Stripe_Helper::get_dynamic_statement_descriptor_suffix( $order );
if ( $payment_information['has_subscription'] ) {
$payment_information['payment_method_options'][ $selected_payment_type ]['setup_future_usage'] = 'off_session';
}

return $payment_information;
}

/**
* Add or remove payment information fields for the payment method flow.
*
* @param array $payment_information The base payment information.
* @param string $selected_payment_type The selected payment type.
* @param WC_Order $order The WC Order being processed.
* @return array The customized payment information for the payment method flow.
*/
private function prepare_payment_information_for_payment_method( $payment_information, $selected_payment_type, $order ) {
$payment_information['payment_method_options'] = $this->get_payment_method_options(
$selected_payment_type,
$order,
$payment_information['payment_method_details']
);

return $payment_information;
}

/**
* Returns the payment method options for the selected payment type.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
* Amazon Pay Payment Method class extending UPE base class
*/
class WC_Stripe_UPE_Payment_Method_Amazon_Pay extends WC_Stripe_UPE_Payment_Method {
use WC_Stripe_Subscriptions_Trait;

const STRIPE_ID = WC_Stripe_Payment_Methods::AMAZON_PAY;

Expand All @@ -24,6 +25,36 @@ public function __construct() {
'Amazon Pay is a payment method that allows customers to pay with their Amazon account.',
'woocommerce-gateway-stripe'
);
$this->supports[] = 'tokenization';

// Check if subscriptions are enabled and add support for them.
$this->maybe_init_subscriptions();
}

/**
* Returns string representing payment method type
* to query to retrieve saved payment methods from Stripe.
*/
public function get_retrievable_type() {
return $this->get_id();
}

/**
* Create new WC payment token and add to user.
*
* @param int $user_id WP_User ID
* @param object $payment_method Stripe payment method object
*
* @return WC_Payment_Token_Amazon_Pay
*/
public function create_payment_token_for_user( $user_id, $payment_method ) {
$token = new WC_Payment_Token_Amazon_Pay();
$token->set_email( $payment_method->billing_details->email ?? '' );
$token->set_gateway_id( WC_Stripe_Payment_Tokens::UPE_REUSABLE_GATEWAYS_BY_PAYMENT_METHOD[ self::STRIPE_ID ] );
$token->set_token( $payment_method->id );
$token->set_user_id( $user_id );
$token->save();
return $token;
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
<?php

if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}

// phpcs:disable WordPress.Files.FileName

/**
* WooCommerce Stripe Amazon Pay Payment Token.
*
* Representation of a payment token for Amazon Pay.
*
* @class WC_Payment_Token_Amazon_Pay
*/
class WC_Payment_Token_Amazon_Pay extends WC_Payment_Token implements WC_Stripe_Payment_Method_Comparison_Interface {
/**
* Stores payment type.
*
* @var string
*/
protected $type = WC_Stripe_Payment_Methods::AMAZON_PAY;

/**
* Stores Amazon Pay payment token data.
*
* @var array
*/
protected $extra_data = [
'email' => '',
];

/**
* Get type to display to user.
*
* @param string $deprecated Deprecated since WooCommerce 3.0
* @return string
*/
public function get_display_name( $deprecated = '' ) {
$display = sprintf(
/* translators: customer email */
__( 'Amazon Pay (%s)', 'woocommerce-gateway-stripe' ),
$this->get_email()
);

return $display;
}

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

/**
* Returns the customer email.
*
* @param string $context What the value is for. Valid values are view and edit.
*
* @return string Customer email.
*/
public function get_email( $context = 'view' ) {
return $this->get_prop( 'email', $context );
}

/**
* Set the customer email.
*
* @param string $email Customer email.
*/
public function set_email( $email ) {
$this->set_prop( 'email', $email );
}

/**
* Checks if the payment method token is equal a provided payment method.
*
* @inheritDoc
*/
public function is_equal_payment_method( $payment_method ): bool {
if ( WC_Stripe_Payment_Methods::AMAZON_PAY === $payment_method->type
&& ( $payment_method->billing_details->email ?? null ) === $this->get_email() ) {
return true;
}

return false;
}
}
13 changes: 13 additions & 0 deletions includes/payment-tokens/class-wc-stripe-payment-tokens.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ class WC_Stripe_Payment_Tokens {
const UPE_REUSABLE_GATEWAYS_BY_PAYMENT_METHOD = [
WC_Stripe_UPE_Payment_Method_CC::STRIPE_ID => WC_Stripe_UPE_Payment_Gateway::ID,
WC_Stripe_UPE_Payment_Method_Link::STRIPE_ID => WC_Stripe_UPE_Payment_Gateway::ID,
WC_Stripe_UPE_Payment_Method_Amazon_Pay::STRIPE_ID => WC_Stripe_UPE_Payment_Gateway::ID,
WC_Stripe_UPE_Payment_Method_ACH::STRIPE_ID => WC_Stripe_UPE_Payment_Gateway::ID . '_' . WC_Stripe_UPE_Payment_Method_ACH::STRIPE_ID,
WC_Stripe_UPE_Payment_Method_Bancontact::STRIPE_ID => WC_Stripe_UPE_Payment_Gateway::ID . '_' . WC_Stripe_UPE_Payment_Method_Bancontact::STRIPE_ID,
WC_Stripe_UPE_Payment_Method_Ideal::STRIPE_ID => WC_Stripe_UPE_Payment_Gateway::ID . '_' . WC_Stripe_UPE_Payment_Method_Ideal::STRIPE_ID,
Expand Down Expand Up @@ -431,6 +432,13 @@ public function get_account_saved_payment_methods_list_item( $item, $payment_tok
esc_html( $payment_token->get_email() )
);
break;
case WC_Stripe_Payment_Methods::AMAZON_PAY:
$item['method']['brand'] = sprintf(
/* translators: customer email */
esc_html__( 'Amazon Pay (%s)', 'woocommerce-gateway-stripe' ),
esc_html( $payment_token->get_email() )
);
break;
}

return $item;
Expand Down Expand Up @@ -555,6 +563,10 @@ private function add_token_to_user( $payment_method, WC_Stripe_Customer $custome
$token->set_email( $payment_method->link->email );
$token->set_payment_method_type( $payment_method_type );
break;
case WC_Stripe_UPE_Payment_Method_Amazon_Pay::STRIPE_ID:
$token = new WC_Payment_Token_Amazon_Pay();
$token->set_email( $payment_method->billing_details->email ?? '' );
break;
case WC_Stripe_UPE_Payment_Method_ACH::STRIPE_ID:
$token = new WC_Payment_Token_ACH();
if ( isset( $payment_method->us_bank_account->last4 ) ) {
Expand Down Expand Up @@ -643,6 +655,7 @@ public static function get_token_label_overrides_for_checkout() {
WC_Stripe_UPE_Payment_Method_Cash_App_Pay::STRIPE_ID,
WC_Stripe_UPE_Payment_Method_Link::STRIPE_ID,
WC_Stripe_UPE_Payment_Method_Bacs_Debit::STRIPE_ID,
WC_Stripe_UPE_Payment_Method_Amazon_Pay::STRIPE_ID,
];

foreach ( $payment_method_types as $stripe_id ) {
Expand Down
1 change: 1 addition & 0 deletions readme.txt
Original file line number Diff line number Diff line change
Expand Up @@ -137,5 +137,6 @@ If you get stuck, you can ask for help in the [Plugin Forum](https://wordpress.o
* Add - Disable unsupported payment methods in Stripe settings
* Update - Update handling of PR as a country in the terminal locations endpoint.
* Fix - Hide Amazon Pay in settings when legacy checkout is enabled.
* Fix - Fix subscription renewal issues for Amazon Pay.

[See changelog for all versions](https://raw.githubusercontent.com/woocommerce/woocommerce-gateway-stripe/trunk/changelog.txt).
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,17 @@ class WC_Stripe_UPE_Payment_Method_Test extends WP_UnitTestCase {
],
];

/**
* Base template for Stripe Amazon Pay payment method.
*/
const MOCK_AMAZON_PAY_PAYMENT_METHOD_TEMPLATE = [
'id' => 'pm_mock_payment_method_id',
'type' => 'amazon_pay',
'billing_details' => [
'email' => '[email protected]',
],
];

/**
* Base template for Stripe ACH payment method.
*/
Expand Down Expand Up @@ -787,6 +798,12 @@ public function test_create_payment_token_for_user() {
$this->assertTrue( WC_Payment_Token_Link::class === get_class( $token ) );
$this->assertSame( $token->get_email(), $link_payment_method_mock->link->email );
break;
case WC_Stripe_UPE_Payment_Method_Amazon_Pay::STRIPE_ID:
$amazon_payment_method_mock = $this->array_to_object( self::MOCK_AMAZON_PAY_PAYMENT_METHOD_TEMPLATE );
$token = $payment_method->create_payment_token_for_user( $user_id, $amazon_payment_method_mock );
$this->assertTrue( WC_Payment_Token_Amazon_Pay::class === get_class( $token ) );
$this->assertSame( $token->get_email(), $amazon_payment_method_mock->billing_details->email );
break;
case WC_Stripe_UPE_Payment_Method_Cash_App_Pay::STRIPE_ID:
$cash_app_payment_method_mock = $this->array_to_object( self::MOCK_CASH_APP_PAYMENT_METHOD_TEMPLATE );
$token = $payment_method->create_payment_token_for_user( $user_id, $cash_app_payment_method_mock );
Expand Down
Loading
Loading