diff --git a/includes/class-wc-stripe-feature-flags.php b/includes/class-wc-stripe-feature-flags.php index 17a036c142..02211b5393 100644 --- a/includes/class-wc-stripe-feature-flags.php +++ b/includes/class-wc-stripe-feature-flags.php @@ -4,9 +4,9 @@ } class WC_Stripe_Feature_Flags { - const UPE_CHECKOUT_FEATURE_ATTRIBUTE_NAME = 'upe_checkout_experience_enabled'; - const AMAZON_PAY_FEATURE_FLAG_NAME = '_wcstripe_feature_amazon_pay'; - + const UPE_CHECKOUT_FEATURE_ATTRIBUTE_NAME = 'upe_checkout_experience_enabled'; + const AMAZON_PAY_FEATURE_FLAG_NAME = '_wcstripe_feature_amazon_pay'; + const SHARED_PAYMENT_TOKEN_FEATURE_FLAG_NAME = '_wcstripe_feature_shared_payment_token'; /** * Feature flag for Stripe ECE (Express Checkout Element). * This feature flag controls whether the new Express Checkout Element (ECE) or the legacy Payment Request Button (PRB) is used to render express checkout buttons. @@ -33,9 +33,10 @@ class WC_Stripe_Feature_Flags { * @var array */ protected static $feature_flags = [ - '_wcstripe_feature_upe' => 'yes', - self::AMAZON_PAY_FEATURE_FLAG_NAME => 'no', - self::OC_FEATURE_FLAG_NAME => 'no', + '_wcstripe_feature_upe' => 'yes', + self::AMAZON_PAY_FEATURE_FLAG_NAME => 'no', + self::OC_FEATURE_FLAG_NAME => 'no', + self::SHARED_PAYMENT_TOKEN_FEATURE_FLAG_NAME => 'no', ]; /** @@ -80,6 +81,15 @@ public static function is_stripe_ece_enabled() { return true; } + /** + * Checks whether Shared Payment Token feature flag is enabled. + * + * @return bool + */ + public static function is_shared_payment_token_available(): bool { + return 'yes' === self::get_option_with_default( self::SHARED_PAYMENT_TOKEN_FEATURE_FLAG_NAME ); + } + /** * Checks whether UPE "preview" feature flag is enabled. * This allows the merchant to enable/disable UPE checkout. diff --git a/includes/class-wc-stripe-intent-controller.php b/includes/class-wc-stripe-intent-controller.php index b7067b232b..7e1a231f44 100644 --- a/includes/class-wc-stripe-intent-controller.php +++ b/includes/class-wc-stripe-intent-controller.php @@ -818,8 +818,12 @@ public function create_and_confirm_payment_intent( $payment_information ) { $non_empty_params = []; - // The payment method is not required if we're using the confirmation token flow. - if ( empty( $payment_information['confirmation_token'] ) ) { + if ( WC_Stripe_Payment_Methods::SHARED_PAYMENT_TOKEN === ( $payment_information['selected_payment_type'] ?? null ) ) { + $required_params[] = 'shared_payment_granted_token'; + + $non_empty_params[] = 'shared_payment_granted_token'; + } elseif ( empty( $payment_information['confirmation_token'] ) ) { + // The payment method is not required if we're using the confirmation token flow. $required_params[] = 'payment_method'; $required_params[] = 'capture_method'; @@ -832,7 +836,7 @@ public function create_and_confirm_payment_intent( $payment_information ) { $order = $payment_information['order']; $selected_payment_type = $payment_information['selected_payment_type']; - $payment_method_types = $payment_information['payment_method_types']; + $payment_method_types = $payment_information['payment_method_types'] ?? []; $is_using_saved_token = $payment_information['is_using_saved_payment_method'] ?? false; $request = $this->build_base_payment_intent_request_params( $payment_information ); @@ -847,10 +851,13 @@ public function create_and_confirm_payment_intent( $payment_information ) { /* translators: 1) blog name 2) order number */ 'description' => sprintf( __( '%1$s - Order %2$s', 'woocommerce-gateway-stripe' ), wp_specialchars_decode( get_bloginfo( 'name' ), ENT_QUOTES ), $order->get_order_number() ), 'metadata' => $payment_information['metadata'], - 'payment_method_types' => $payment_method_types, ] ); + if ( [] !== $payment_method_types ) { + $request['payment_method_types'] = $payment_method_types; + } + if ( isset( $payment_information['statement_descriptor_suffix'] ) ) { $request['statement_descriptor_suffix'] = $payment_information['statement_descriptor_suffix']; } @@ -884,8 +891,12 @@ public function create_and_confirm_payment_intent( $payment_information ) { $order ); - // Only update the payment_type if we have a reference to the payment type the customer selected. - if ( '' !== $selected_payment_type ) { + // TODO: Verify this works as expected. + // If we're processing a shared payment token, we need to look at the intent that was actually created. + if ( WC_Stripe_Payment_Methods::SHARED_PAYMENT_TOKEN === $selected_payment_type ) { + $order->update_meta_data( '_stripe_upe_payment_type', $payment_intent->type ); + } elseif ( '' !== $selected_payment_type ) { + // Only update the payment_type if we have a reference to the payment type the customer selected. WC_Stripe_Order_Helper::get_instance()->update_stripe_upe_payment_type( $order, $selected_payment_type ); } @@ -1053,18 +1064,20 @@ private function validate_payment_intent_required_params( $required_params, $non */ private function build_base_payment_intent_request_params( $payment_information ) { $selected_payment_type = $payment_information['selected_payment_type']; - if ( $this->get_upe_gateway()->is_oc_enabled() && isset( $payment_information['payment_method_details']->type ) ) { + if ( WC_Stripe_Payment_Methods::SHARED_PAYMENT_TOKEN !== $selected_payment_type && $this->get_upe_gateway()->is_oc_enabled() && isset( $payment_information['payment_method_details']->type ) ) { $selected_payment_type = $payment_information['payment_method_details']->type; } - $payment_method_types = $payment_information['payment_method_types']; + $payment_method_types = $payment_information['payment_method_types'] ?? []; $request = [ 'shipping' => $payment_information['shipping'], ]; $is_using_confirmation_token = ! empty( $payment_information['confirmation_token'] ); - if ( $is_using_confirmation_token ) { + if ( WC_Stripe_Payment_Methods::SHARED_PAYMENT_TOKEN === $selected_payment_type ) { + $request['shared_payment_granted_token'] = $payment_information['shared_payment_granted_token']; + } elseif ( $is_using_confirmation_token ) { $request['confirmation_token'] = $payment_information['confirmation_token']; } else { $request['payment_method'] = $payment_information['payment_method']; @@ -1089,7 +1102,7 @@ private function build_base_payment_intent_request_params( $payment_information // For confirmations tokens, the setup_future_usage is set within the payment method. $payment_method = WC_Stripe_UPE_Payment_Gateway::get_payment_method_instance( $selected_payment_type ); $has_auto_renewing_subscription = ! empty( $payment_information['has_subscription'] ) && ! $this->is_manual_renewal_required( $payment_method->is_reusable() ); - if ( ! $is_using_confirmation_token && ( $payment_information['save_payment_method_to_store'] || $has_auto_renewing_subscription ) ) { + if ( ! $is_using_confirmation_token && WC_Stripe_Payment_Methods::SHARED_PAYMENT_TOKEN !== $selected_payment_type && ( $payment_information['save_payment_method_to_store'] || $has_auto_renewing_subscription ) ) { $request['setup_future_usage'] = 'off_session'; } diff --git a/includes/class-wc-stripe.php b/includes/class-wc-stripe.php index 4cae23488c..be9a76faca 100644 --- a/includes/class-wc-stripe.php +++ b/includes/class-wc-stripe.php @@ -186,6 +186,7 @@ public function init() { require_once WC_STRIPE_PLUGIN_PATH . '/includes/payment-methods/class-wc-stripe-upe-payment-method-acss.php'; require_once WC_STRIPE_PLUGIN_PATH . '/includes/payment-methods/class-wc-stripe-upe-payment-method-amazon-pay.php'; require_once WC_STRIPE_PLUGIN_PATH . '/includes/payment-methods/class-wc-stripe-upe-payment-method-oc.php'; + require_once WC_STRIPE_PLUGIN_PATH . '/includes/payment-methods/class-wc-stripe-upe-payment-method-shared-payment-token.php'; require_once WC_STRIPE_PLUGIN_PATH . '/includes/payment-methods/class-wc-gateway-stripe-bancontact.php'; require_once WC_STRIPE_PLUGIN_PATH . '/includes/payment-methods/class-wc-gateway-stripe-sofort.php'; require_once WC_STRIPE_PLUGIN_PATH . '/includes/payment-methods/class-wc-gateway-stripe-giropay.php'; diff --git a/includes/constants/class-wc-stripe-payment-methods.php b/includes/constants/class-wc-stripe-payment-methods.php index a42936405a..063f65a793 100644 --- a/includes/constants/class-wc-stripe-payment-methods.php +++ b/includes/constants/class-wc-stripe-payment-methods.php @@ -9,31 +9,34 @@ */ class WC_Stripe_Payment_Methods { // Standard payment method constants - const ACH = 'us_bank_account'; - const ACSS_DEBIT = 'acss_debit'; - const AFFIRM = 'affirm'; - const AFTERPAY_CLEARPAY = 'afterpay_clearpay'; - const ALIPAY = 'alipay'; - const BACS_DEBIT = 'bacs_debit'; - const BECS_DEBIT = 'au_becs_debit'; - const BANCONTACT = 'bancontact'; - const BLIK = 'blik'; - const BOLETO = 'boleto'; - const CARD = 'card'; - const CARD_PRESENT = 'card_present'; - const CASHAPP_PAY = 'cashapp'; - const EPS = 'eps'; - const GIROPAY = 'giropay'; - const IDEAL = 'ideal'; - const KLARNA = 'klarna'; - const MULTIBANCO = 'multibanco'; - const OXXO = 'oxxo'; - const P24 = 'p24'; - const SEPA = 'sepa'; - const SEPA_DEBIT = 'sepa_debit'; - const SOFORT = 'sofort'; - const WECHAT_PAY = 'wechat_pay'; - const OC = 'card'; // This is a special case for the Optimized Checkout + const ACH = 'us_bank_account'; + const ACSS_DEBIT = 'acss_debit'; + const AFFIRM = 'affirm'; + const AFTERPAY_CLEARPAY = 'afterpay_clearpay'; + const ALIPAY = 'alipay'; + const BACS_DEBIT = 'bacs_debit'; + const BECS_DEBIT = 'au_becs_debit'; + const BANCONTACT = 'bancontact'; + const BLIK = 'blik'; + const BOLETO = 'boleto'; + const CARD = 'card'; + const CARD_PRESENT = 'card_present'; + const CASHAPP_PAY = 'cashapp'; + const EPS = 'eps'; + const GIROPAY = 'giropay'; + const IDEAL = 'ideal'; + const KLARNA = 'klarna'; + const MULTIBANCO = 'multibanco'; + const OXXO = 'oxxo'; + const P24 = 'p24'; + const SEPA = 'sepa'; + const SEPA_DEBIT = 'sepa_debit'; + const SOFORT = 'sofort'; + const WECHAT_PAY = 'wechat_pay'; + + // TODO: We need to move the constants for OC and Shared Payment Token, as they are not true payment methods. + const OC = 'card'; // This is a special case for the Optimized Checkout + const SHARED_PAYMENT_TOKEN = 'shared_payment_token'; // Express method constants const AMAZON_PAY = 'amazon_pay'; diff --git a/includes/payment-methods/class-wc-stripe-upe-payment-gateway.php b/includes/payment-methods/class-wc-stripe-upe-payment-gateway.php index 7d2040708b..4d3733cf1d 100644 --- a/includes/payment-methods/class-wc-stripe-upe-payment-gateway.php +++ b/includes/payment-methods/class-wc-stripe-upe-payment-gateway.php @@ -181,6 +181,13 @@ class WC_Stripe_UPE_Payment_Gateway extends WC_Gateway_Stripe { */ public $payment_methods = []; + /** + * Whether Shared Payment Token is enabled. + * + * @var bool + */ + public bool $shared_payment_token_enabled = false; + /** * Constructor */ @@ -196,6 +203,8 @@ public function __construct() { 'tokenization', 'add_payment_method', ]; + // TODO: Add option to enable/disable shared payment token. + $this->shared_payment_token_enabled = WC_Stripe_Feature_Flags::is_shared_payment_token_available(); $enabled_payment_methods = $this->get_upe_enabled_payment_method_ids(); $is_sofort_enabled = in_array( WC_Stripe_Payment_Methods::SOFORT, $enabled_payment_methods, true ); @@ -221,6 +230,12 @@ public function __construct() { $this->payment_methods[ $payment_method->get_id() ] = $payment_method; } + if ( $this->shared_payment_token_enabled ) { + $shared_payment_token_method = new WC_Stripe_UPE_Payment_Method_Shared_Payment_Token(); + + $this->payment_methods[ $shared_payment_token_method->get_id() ] = $shared_payment_token_method; + } + $this->intent_controller = new WC_Stripe_Intent_Controller(); $this->action_scheduler_service = new WC_Stripe_Action_Scheduler_Service(); @@ -619,9 +634,12 @@ private function get_enabled_payment_method_config() { $original_method_ids = $enabled_payment_methods; // For OC, keep the original methods to control availability $payment_methods = $this->payment_methods; - // If the Optimized Checkout is enabled, we need to return just the card payment method + express methods. - // All payment methods are rendered inside the card container. - if ( $this->oc_enabled ) { + if ( $this->shared_payment_token_enabled ) { + $enabled_payment_methods[] = WC_Stripe_UPE_Payment_Method_Shared_Payment_Token::STRIPE_ID; + $payment_methods[ WC_Stripe_UPE_Payment_Method_Shared_Payment_Token::STRIPE_ID ] = new WC_Stripe_UPE_Payment_Method_Shared_Payment_Token(); + } elseif ( $this->oc_enabled ) { + // If the Optimized Checkout is enabled, we need to return just the card payment method + express methods. + // All payment methods are rendered inside the card container. $oc_method_id = WC_Stripe_UPE_Payment_Method_OC::STRIPE_ID; $enabled_express_methods = array_intersect( $enabled_payment_methods, @@ -877,10 +895,10 @@ public function payment_fields() { * @return array|null An array with result of payment and redirect URL, or nothing. */ public function process_payment( $order_id, $retry = true, $force_save_source = false, $previous_error = false, $use_order_source = false ) { - $payment_intent_id = isset( $_POST['wc_payment_intent_id'] ) ? wc_clean( wp_unslash( $_POST['wc_payment_intent_id'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Missing - $order = wc_get_order( $order_id ); - $selected_payment_type = $this->get_selected_payment_method_type_from_request(); - $save_payment_method = $this->should_save_payment_method_from_request( $order_id, $selected_payment_type ); + $payment_intent_id = isset( $_POST['wc_payment_intent_id'] ) ? wc_clean( wp_unslash( $_POST['wc_payment_intent_id'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Missing + $order = wc_get_order( $order_id ); + $selected_payment_type = $this->get_selected_payment_method_type_from_request( $order ); + $save_payment_method = $this->should_save_payment_method_from_request( $order_id, $selected_payment_type ); if ( $payment_intent_id && ! $this->payment_methods[ $selected_payment_type ]->supports_deferred_intent() ) { // Adds customer and metadata to PaymentIntent. @@ -906,6 +924,10 @@ public function process_payment( $order_id, $retry = true, $force_save_source = return $this->process_payment_with_saved_payment_method( $order_id ); } + if ( WC_Stripe_Payment_Methods::SHARED_PAYMENT_TOKEN === $selected_payment_type ) { + return $this->process_payment_with_payment_method( $order_id ); + } + $payment_needed = $this->is_payment_needed( $order_id ); $selected_upe_payment_type = ! empty( $_POST['wc_stripe_selected_upe_payment_type'] ) ? wc_clean( wp_unslash( $_POST['wc_stripe_selected_upe_payment_type'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Missing @@ -1070,19 +1092,21 @@ private function process_payment_with_payment_method( int $order_id ) { } $payment_needed = $this->is_payment_needed( $order->get_id() ); - $payment_method_id = $payment_information['payment_method']; - $payment_method_details = $payment_information['payment_method_details']; + $payment_method_id = $payment_information['payment_method'] ?? null; + $payment_method_details = $payment_information['payment_method_details'] ?? null; $selected_payment_type = $payment_information['selected_payment_type']; $is_using_saved_payment_method = $payment_information['is_using_saved_payment_method']; $upe_payment_method = $this->payment_methods[ $selected_payment_type ] ?? null; $response_args = []; - if ( $this->oc_enabled && isset( $payment_method_details->type ) ) { + if ( $this->oc_enabled && isset( $payment_method_details->type ) && WC_Stripe_Payment_Methods::SHARED_PAYMENT_TOKEN !== $selected_payment_type ) { $upe_payment_method = self::get_payment_method_instance( $payment_method_details->type ); } // Make sure that we attach the payment method and the customer ID to the order meta data. - $this->set_payment_method_id_for_order( $order, $payment_method_id ); + if ( $payment_method_id ) { + $this->set_payment_method_id_for_order( $order, $payment_method_id ); + } $this->set_customer_id_for_order( $order, $payment_information['customer'] ); // Only update the payment_type if we have a reference to the payment type the customer selected. @@ -1091,10 +1115,15 @@ private function process_payment_with_payment_method( int $order_id ) { } // Retrieve the payment method object from Stripe. - $payment_method = $this->stripe_request( 'payment_methods/' . $payment_method_id ); + $payment_method = null; + if ( $payment_method_id ) { + $payment_method = $this->stripe_request( 'payment_methods/' . $payment_method_id ); - // Throw an exception when the payment method is a prepaid card and it's disallowed. - $this->maybe_disallow_prepaid_card( $payment_method ); + if ( $payment_method ) { + // Throw an exception when the payment method is a prepaid card and it's disallowed. + $this->maybe_disallow_prepaid_card( $payment_method ); + } + } // Until we know other payment methods need this, let's just set for BLIK. if ( WC_Stripe_Payment_Methods::BLIK === $selected_payment_type ) { @@ -1106,7 +1135,7 @@ private function process_payment_with_payment_method( int $order_id ) { } // Update saved payment method to include billing details. - if ( $is_using_saved_payment_method ) { + if ( $is_using_saved_payment_method && $payment_method_id ) { $this->update_saved_payment_method( $payment_method_id, $order ); } @@ -2509,7 +2538,7 @@ protected function process_setup_intent_for_order( WC_Order $order, array $payme * @throws WC_Stripe_Exception When there's an error retrieving the payment information. */ protected function prepare_payment_information_from_request( WC_Order $order ) { - $selected_payment_type = $this->get_selected_payment_method_type_from_request(); + $selected_payment_type = $this->get_selected_payment_method_type_from_request( $order ); $capture_method = $this->is_automatic_capture_enabled() ? 'automatic' : 'manual'; // automatic | manual. $currency = strtolower( $order->get_currency() ); $amount = WC_Stripe_Helper::get_stripe_amount( $order->get_total(), $currency ); @@ -2547,20 +2576,6 @@ protected function prepare_payment_information_from_request( WC_Order $order ) { $payment_method_id = sanitize_text_field( wp_unslash( $_POST['wc-stripe-payment-method'] ?? '' ) ); // phpcs:ignore WordPress.Security.NonceVerification.Missing } - $payment_method_details = ! empty( $payment_method_id ) ? WC_Stripe_API::get_payment_method( $payment_method_id ) : (object) []; - - // Override the payment method type with the API value when OC is enabled - if ( $this->oc_enabled ) { - $selected_payment_type = $payment_method_details->type ?? null; - $payment_method_types = [ $selected_payment_type ]; - } else { - $payment_method_types = $this->get_payment_method_types_for_intent_creation( - $selected_payment_type, - $order->get_id(), - $this->get_express_payment_type_from_request() - ); - } - $payment_information = [ 'amount' => $amount, 'currency' => $currency, @@ -2571,19 +2586,41 @@ protected function prepare_payment_information_from_request( WC_Order $order ) { 'order' => $order, 'payment_initiated_by' => 'initiated_by_customer', // initiated_by_merchant | initiated_by_customer. 'selected_payment_type' => $selected_payment_type, - 'payment_method_types' => $payment_method_types, 'shipping' => $shipping_details, 'token' => $token, '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_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, ]; + // Agentic commerce uses a shared payment token rather than a payment method. + if ( WC_Stripe_Payment_Methods::SHARED_PAYMENT_TOKEN === $selected_payment_type ) { + $shared_payment_token = sanitize_text_field( wp_unslash( $_POST['wc-agentic_commerce-token'] ?? '' ) ); // phpcs:ignore WordPress.Security.NonceVerification.Missing + + $payment_information['shared_payment_granted_token'] = $shared_payment_token; + } else { + $payment_method_details = ! empty( $payment_method_id ) ? WC_Stripe_API::get_payment_method( $payment_method_id ) : (object) []; + + // Override the payment method type with the API value when OC is enabled + if ( $this->oc_enabled ) { + $selected_payment_type = $payment_method_details->type ?? null; + $payment_method_types = [ $selected_payment_type ]; + } else { + $payment_method_types = $this->get_payment_method_types_for_intent_creation( + $selected_payment_type, + $order->get_id(), + $this->get_express_payment_type_from_request() + ); + } + + $payment_information['payment_method_details'] = $payment_method_details; + $payment_information['payment_method_types'] = $payment_method_types; + $payment_information['payment_method'] = $payment_method_id; + } + if ( WC_Stripe_Payment_Methods::ACH === $selected_payment_type ) { WC_Stripe_API::attach_payment_method_to_customer( $payment_information['customer'], $payment_method_id ); } @@ -2795,10 +2832,17 @@ private function should_save_payment_method_from_request( $order_id, $payment_me /** * Gets the selected payment method type from the request and normalizes its slug for internal use. * + * @param WC_Order|false|null $order The current order. * @return string */ - private function get_selected_payment_method_type_from_request() { - // phpcs:ignore WordPress.Security.NonceVerification.Missing + private function get_selected_payment_method_type_from_request( $order = null ) { + if ( $order ) { + $agentic_commerce_payment_token = $this->get_agentic_commerce_payment_token_from_request( $order ); + if ( null !== $agentic_commerce_payment_token ) { + return WC_Stripe_Payment_Methods::SHARED_PAYMENT_TOKEN; + } + } + if ( ! isset( $_POST['payment_method'] ) ) { return ''; } @@ -2821,6 +2865,43 @@ private function get_selected_payment_method_type_from_request() { return substr( $payment_method_type, 0, 7 ) === 'stripe_' ? substr( $payment_method_type, 7 ) : 'card'; } + /** + * Helper function to get a shared payment token from an incoming request for a specific order. + * + * @param WC_Order|null $order The order. + * @return string|null The shared payment token, or null if it is not found. + */ + private function get_agentic_commerce_payment_token_from_request( ?WC_Order $order = null ): ?string { + if ( ! $order ) { + return null; + } + + if ( ! isset( $_POST['wc-agentic_commerce-token'] ) || ! isset( $_POST['wc-agentic_commerce-provider'] ) ) { + return null; + } + + if ( WC_Stripe_UPE_Payment_Method_Shared_Payment_Token::STRIPE_AGENTIC_PROVIDER_ID !== sanitize_text_field( wp_unslash( $_POST['wc-agentic_commerce-provider'] ) ) ) { + return null; + } + + $shared_payment_token = sanitize_text_field( wp_unslash( $_POST['wc-agentic_commerce-token'] ) ); + if ( empty( $shared_payment_token ) || ! str_starts_with( $shared_payment_token, 'spt_' ) ) { + return null; + } + + if ( ! class_exists( '\Automattic\WooCommerce\StoreApi\Routes\V1\Agentic\Enums\OrderMetaKey' ) || + ! defined( \Automattic\WooCommerce\StoreApi\Routes\V1\Agentic\Enums\OrderMetaKey::class . '::AGENTIC_CHECKOUT_SESSION_ID' ) + ) { + return null; + } + + if ( empty( $order->get_meta( \Automattic\WooCommerce\StoreApi\Routes\V1\Agentic\Enums\OrderMetaKey::AGENTIC_CHECKOUT_SESSION_ID, true ) ) ) { + return null; + } + + return $shared_payment_token; + } + /** * Gets the express payment type, e.g. google_pay, apple_pay, from the request, * if applicable. diff --git a/includes/payment-methods/class-wc-stripe-upe-payment-method-shared-payment-token.php b/includes/payment-methods/class-wc-stripe-upe-payment-method-shared-payment-token.php new file mode 100644 index 0000000000..468e4d73e1 --- /dev/null +++ b/includes/payment-methods/class-wc-stripe-upe-payment-method-shared-payment-token.php @@ -0,0 +1,188 @@ +shared_payment_token_enabled && + [] !== $this->get_enabled_agentic_commerce_payment_methods(); + + $this->enabled = $shared_payment_token_enabled ? 'yes' : 'no'; + $this->id = WC_Gateway_Stripe::ID; // Force the ID to be the same as the main payment gateway. + $this->stripe_id = self::STRIPE_ID; + $this->title = __( 'Stripe Agentic Commerce', 'woocommerce-gateway-stripe' ); + $this->is_reusable = false; + $this->supports[] = 'subscriptions'; + $this->supports[] = 'tokenization'; + $this->supports[] = 'agentic_commerce'; + } + + /** + * Get the supported agentic commerce payment methods. + * + * @return string[] Array of supported payment methods for agentic commerce. + */ + public static function get_supported_agentic_commerce_payment_methods(): array { + return [ WC_Stripe_Payment_Methods::CARD ]; + } + + /** + * Get the enabled agentic commerce payment methods. + * + * @return string[] Array of enabled payment methods for agentic commerce. + */ + public function get_enabled_agentic_commerce_payment_methods(): array { + return array_intersect( + self::get_supported_agentic_commerce_payment_methods(), + $this->get_upe_enabled_payment_method_ids() + ); + } + + /** + * Get the agentic commerce payment provider identifier. + * + * @return string Payment provider identifier. + */ + public function get_agentic_commerce_provider() { + return self::STRIPE_AGENTIC_PROVIDER_ID; + } + + /** + * Get the supported payment methods for agentic commerce. + * + * @return array Array of supported payment methods. + */ + public function get_agentic_commerce_payment_methods() { + return self::get_supported_agentic_commerce_payment_methods(); + } + + /** + * Returns payment method title + * + * @param stdClass|array|bool $payment_details Optional payment details from charge object. + * + * @return string + */ + public function get_title( $payment_details = false ) { + // Wallet type + if ( $payment_details && ! empty( $payment_details->card->wallet->type ) ) { + return $this->get_card_wallet_type_title( $payment_details->card->wallet->type ); + } + + if ( $payment_details && ! empty( $payment_details->type ) ) { // Setting title for the order details page / thank you page. + $payment_method = WC_Stripe_UPE_Payment_Gateway::get_payment_method_instance( $payment_details->type ); + + // Avoid potential recursion by checking instance type. This fixes the title on pay for order confirmation page. + return $payment_method instanceof self ? parent::get_title() : $payment_method->get_title(); + } + + // Block checkout and pay for order (checkout) page. + if ( ( has_block( 'woocommerce/checkout' ) || ! empty( $_GET['pay_for_order'] ) ) && ! is_wc_endpoint_url( 'order-received' ) ) { // phpcs:ignore WordPress.Security.NonceVerification + return __( 'Stripe Agentic Commerce', 'woocommerce-gateway-stripe' ); + } + + return parent::get_title(); + } + + /** + * Returns true if the UPE method is available. + * + * @inheritDoc + */ + public function is_available() { + if ( ! $this->shared_payment_token_enabled ) { + return false; + } + + if ( [] === $this->get_enabled_agentic_commerce_payment_methods() ) { + return false; + } + + return parent::is_available(); + } + + /** + * Returns string representing payment method type + * to query to retrieve saved payment methods from Stripe. + * + * @inheritDoc + */ + public function get_retrievable_type() { + return WC_Stripe_Payment_Methods::CARD; + } + + /** + * Returns boolean dependent on whether capability + * for site account is enabled for payment method. + * + * @inheritDoc + */ + public function is_capability_active() { + return true; + } + + /** + * The Optimized Checkout method allows automatic capture. + * + * @inheritDoc + */ + public function requires_automatic_capture() { + return false; + } + + /** + * Returns testing credentials to be printed at checkout in test mode. + * + * @return string + */ + public function get_testing_instructions( $show_optimized_checkout_instruction = false ) { + if ( false !== $show_optimized_checkout_instruction ) { + _deprecated_argument( + __FUNCTION__, + '9.9.0' + ); + } + + $instructions = ''; + $base_instruction_html = '
'; + + foreach ( $this->get_enabled_agentic_commerce_payment_methods() as $payment_method_id ) { + $payment_method = WC_Stripe_UPE_Payment_Gateway::get_payment_method_instance( $payment_method_id ); + if ( ! $payment_method ) { + continue; + } + + $payment_method_instructions = $payment_method->get_testing_instructions(); + if ( $payment_method_instructions ) { + $instructions .= sprintf( $base_instruction_html, $payment_method::STRIPE_ID, $payment_method_instructions ); + } + } + + return $instructions; + } +} diff --git a/includes/payment-methods/class-wc-stripe-upe-payment-method.php b/includes/payment-methods/class-wc-stripe-upe-payment-method.php index 9253b2f6e8..07f1994fa0 100644 --- a/includes/payment-methods/class-wc-stripe-upe-payment-method.php +++ b/includes/payment-methods/class-wc-stripe-upe-payment-method.php @@ -119,6 +119,13 @@ abstract class WC_Stripe_UPE_Payment_Method extends WC_Payment_Gateway { */ protected $oc_enabled; + /** + * Whether Shared Payment Token is enabled. + * + * @var bool + */ + protected bool $shared_payment_token_enabled; + /** * Create instance of payment method */ @@ -126,13 +133,15 @@ public function __construct() { $main_settings = WC_Stripe_Helper::get_stripe_settings(); $is_stripe_enabled = ! empty( $main_settings['enabled'] ) && 'yes' === $main_settings['enabled']; - $this->enabled = $is_stripe_enabled && in_array( static::STRIPE_ID, $this->get_upe_enabled_payment_method_ids(), true ) ? 'yes' : 'no'; // @phpstan-ignore-line (STRIPE_ID is defined in classes using this class) - $this->id = WC_Stripe_UPE_Payment_Gateway::ID . '_' . static::STRIPE_ID; // @phpstan-ignore-line (STRIPE_ID is defined in classes using this class) - $this->has_fields = true; - $this->testmode = WC_Stripe_Mode::is_test(); - $this->supports = [ 'products', 'refunds' ]; - $this->supports_deferred_intent = true; - $this->oc_enabled = WC_Stripe_Feature_Flags::is_oc_available() && 'yes' === $this->get_option( 'optimized_checkout_element' ); + $this->enabled = $is_stripe_enabled && in_array( static::STRIPE_ID, $this->get_upe_enabled_payment_method_ids(), true ) ? 'yes' : 'no'; // @phpstan-ignore-line (STRIPE_ID is defined in classes using this class) + $this->id = WC_Stripe_UPE_Payment_Gateway::ID . '_' . static::STRIPE_ID; // @phpstan-ignore-line (STRIPE_ID is defined in classes using this class) + $this->has_fields = true; + $this->testmode = WC_Stripe_Mode::is_test(); + $this->supports = [ 'products', 'refunds' ]; + $this->supports_deferred_intent = true; + $this->oc_enabled = WC_Stripe_Feature_Flags::is_oc_available() && 'yes' === $this->get_option( 'optimized_checkout_element' ); + // TODO: Add option to enable/disable shared payment token. + $this->shared_payment_token_enabled = WC_Stripe_Feature_Flags::is_shared_payment_token_available(); } /**