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

Split PE - Fixes Woo Subscriptions support when using card payments with dPE #2866

Merged
merged 14 commits into from
Feb 6, 2024
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
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
9 changes: 6 additions & 3 deletions client/api/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -166,16 +166,19 @@ export default class WCStripeAPI {
/**
* Creates and confirms a setup intent.
*
* @param {string} paymentMethodId The id of the payment method.
* @param {Object} paymentMethod Payment method data.
* @param {string} paymentMethod.id The ID of the payment method.
* @param {string} paymentMethod.type The type of the payment method.
*
* @return {Promise} Promise containing the setup intent.
*/
setupIntent( paymentMethodId ) {
setupIntent( paymentMethod ) {
return this.request(
this.getAjaxUrl( 'create_and_confirm_setup_intent' ),
{
action: 'create_and_confirm_setup_intent',
'wc-stripe-payment-method': paymentMethodId,
'wc-stripe-payment-method': paymentMethod.id,
'wc-stripe-payment-type': paymentMethod.type,
_ajax_nonce: this.options?.createAndConfirmSetupIntentNonce,
}
).then( ( response ) => {
Expand Down
13 changes: 8 additions & 5 deletions client/classic/upe/deferred-intent.js
Original file line number Diff line number Diff line change
Expand Up @@ -63,11 +63,14 @@ jQuery( function ( $ ) {

// Pay for Order page submit.
$( '#order_review' ).on( 'submit', () => {
return processPayment(
api,
$( '#order_review' ),
getSelectedUPEGatewayPaymentMethod()
);
const paymentMethodType = getSelectedUPEGatewayPaymentMethod();
if ( ! isUsingSavedPaymentMethod( paymentMethodType ) ) {
return processPayment(
api,
$( '#order_review' ),
paymentMethodType
);
}
} );

// If the card element selector doesn't exist, then do nothing.
Expand Down
2 changes: 1 addition & 1 deletion client/classic/upe/payment-processing.js
Original file line number Diff line number Diff line change
Expand Up @@ -269,7 +269,7 @@ export const createAndConfirmSetupIntent = (
api
) => {
return api
.setupIntent( paymentMethod.id )
.setupIntent( paymentMethod )
.then( function ( confirmedSetupIntent ) {
appendSetupIntentToForm( jQueryForm, confirmedSetupIntent );
return confirmedSetupIntent;
Expand Down
17 changes: 17 additions & 0 deletions includes/class-wc-stripe-helper.php
Original file line number Diff line number Diff line change
Expand Up @@ -880,4 +880,21 @@ public static function get_payment_method_from_intent( $intent ) {

return null;
}

/**
* Returns the payment intent or setup intent id method ID from a given intent object.
*
* @param WC_Order $order The order to fetch the Stripe intent from.
*
* @return string|bool The intent ID if found, false otherwise.
*/
public static function get_intent_id_from_order( $order ) {
$intent_id = $order->get_meta( '_stripe_intent_id' );

if ( ! $intent_id ) {
$intent_id = $order->get_meta( '_stripe_setup_intent' );
}

return $intent_id ?? false;
}
}
77 changes: 51 additions & 26 deletions includes/class-wc-stripe-intent-controller.php
Original file line number Diff line number Diff line change
Expand Up @@ -562,7 +562,7 @@ public function update_order_status_ajax() {
throw new WC_Stripe_Exception( 'order_not_found', __( "We're not able to process this payment. Please try again later.", 'woocommerce-gateway-stripe' ) );
}

$intent_id = $order->get_meta( '_stripe_intent_id' );
$intent_id = WC_Stripe_Helper::get_intent_id_from_order( $order );
$intent_id_received = isset( $_POST['intent_id'] ) ? wc_clean( wp_unslash( $_POST['intent_id'] ) ) : null;
if ( empty( $intent_id_received ) || $intent_id_received !== $intent_id ) {
$note = sprintf(
Expand Down Expand Up @@ -714,15 +714,7 @@ public function create_and_confirm_payment_intent( $payment_information ) {

// For Stripe Link & SEPA with deferred intent UPE, we must create mandate to acknowledge that terms have been shown to customer.
if ( $this->is_mandate_data_required( $selected_payment_type ) ) {
$request['mandate_data'] = [
'customer_acceptance' => [
'type' => 'online',
'online' => [
'ip_address' => WC_Geolocation::get_ip_address(),
'user_agent' => 'WooCommerce Stripe Gateway' . WC_STRIPE_VERSION . '; ' . get_bloginfo( 'url' ),
],
],
];
$request = $this->add_mandate_data( $request );
}

if ( $this->request_needs_redirection( $payment_method_types ) ) {
Expand Down Expand Up @@ -762,6 +754,27 @@ public function create_and_confirm_payment_intent( $payment_information ) {
return $payment_intent;
}

/**
* Adds mandate data to the request.
*
* @param array $request The request to add mandate data to.
*
* @return array The request with mandate data added.
*/
private function add_mandate_data( $request ) {
$request['mandate_data'] = [
'customer_acceptance' => [
'type' => 'online',
'online' => [
'ip_address' => WC_Geolocation::get_ip_address(),
'user_agent' => 'WooCommerce Stripe Gateway' . WC_STRIPE_VERSION . '; ' . get_bloginfo( 'url' ),
],
],
];

return $request;
}

/**
* Validate the provided information for creating and confirming a payment intent.
*
Expand Down Expand Up @@ -875,7 +888,7 @@ private function get_payment_method_types_for_intent_creation( string $selected_
* This applies to SEPA and Link payment methods.
* https://stripe.com/docs/payments/finalize-payments-on-the-server
*
* @param $selected_payment_type The name of the selected UPE payment type.
* @param string $selected_payment_type The name of the selected UPE payment type.
*
* @return bool True if a mandate must be shown and acknowledged by customer before deferred intent UPE payment can be processed, false otherwise.
*/
Expand All @@ -891,26 +904,26 @@ public function is_mandate_data_required( $selected_payment_type ) {
/**
* Creates and confirm a setup intent with the given payment method ID.
*
* @param string $payment_method The payment method ID (pm_).
* @param array $payment_information The payment information to be used for the setup intent.
*
* @throws WC_Stripe_Exception If the create intent call returns with an error.
*
* @return array
*/
public function create_and_confirm_setup_intent( $payment_method ) {
// 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 );
$customer_id = $customer->update_or_create_customer();
public function create_and_confirm_setup_intent( $payment_information ) {
$request = [
'payment_method' => $payment_information['payment_method'],
'payment_method_types' => [ $payment_information['selected_payment_type'] ],
'customer' => $payment_information['customer'],
'confirm' => 'true',
];

$setup_intent = WC_Stripe_API::request(
[
'customer' => $customer_id,
'confirm' => 'true',
'payment_method' => $payment_method,
],
'setup_intents'
);
// SEPA setup intents require mandate data.
if ( $this->is_mandate_data_required( $payment_information['selected_payment_type'] ) ) {
$request = $this->add_mandate_data( $request );
}

$setup_intent = WC_Stripe_API::request( $request, 'setup_intents' );

if ( ! empty( $setup_intent->error ) ) {
throw new WC_Stripe_Exception( print_r( $setup_intent->error, true ), $setup_intent->error->message );
Expand All @@ -935,12 +948,24 @@ public function create_and_confirm_setup_intent_ajax() {
}

$payment_method = sanitize_text_field( wp_unslash( $_POST['wc-stripe-payment-method'] ?? '' ) );
$payment_type = sanitize_text_field( wp_unslash( $_POST['wc-stripe-payment-type'] ?? 'card' ) );

if ( ! $payment_method ) {
throw new WC_Stripe_Exception( 'Payment method missing from request.', __( "We're not able to add this payment method. Please refresh the page and try again.", 'woocommerce-gateway-stripe' ) );
}

$setup_intent = $this->create_and_confirm_setup_intent( $payment_method );
// 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 );

// Manually create the payment information array to create & confirm the setup intent.
$payment_information = [
'payment_method' => $payment_method,
'customer' => $customer->update_or_create_customer(),
'selected_payment_type' => $payment_type,
];

$setup_intent = $this->create_and_confirm_setup_intent( $payment_information );

if ( empty( $setup_intent->status ) || ! in_array( $setup_intent->status, [ 'succeeded', 'processing', 'requires_action' ], true ) ) {
throw new WC_Stripe_Exception( 'Response from Stripe: ' . print_r( $setup_intent, true ), __( 'There was an error adding this payment method. Please refresh the page and try again', 'woocommerce-gateway-stripe' ) );
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ public function has_subscription( $order_id ) {
* @return bool
*/
public function is_changing_payment_method_for_subscription() {
if ( isset( $_GET['change_payment_method'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification
if ( isset( $_GET['change_payment_method'] ) && function_exists( 'wcs_is_subscription' ) ) { // phpcs:ignore WordPress.Security.NonceVerification
return wcs_is_subscription( wc_clean( wp_unslash( $_GET['change_payment_method'] ) ) ); // phpcs:ignore WordPress.Security.NonceVerification
}
return false;
Expand Down
107 changes: 84 additions & 23 deletions includes/payment-methods/class-wc-stripe-upe-payment-gateway.php
Original file line number Diff line number Diff line change
Expand Up @@ -703,32 +703,31 @@ private function process_payment_with_deferred_intent( int $order_id ) {

// Create a payment intent, or update an existing one associated with the order.
$payment_intent = $this->process_payment_intent_for_order( $order, $payment_information );

// Handle saving the payment method in the store.
// It's already attached to the Stripe customer at this point.
if ( $payment_information['save_payment_method_to_store'] ) {
$this->handle_saving_payment_method(
$order,
$payment_information['payment_method'],
$selected_payment_type
);
}

// Use the last charge within the intent to proceed.
$charge = end( $payment_intent->charges->data );

// Only process the response if it contains a charge object. Intents with no charge require further action like 3DS and will be processed later.
if ( $charge ) {
$this->process_response( $charge, $order );
}

// Set the selected UPE payment method type title in the WC order.
$this->set_payment_method_title_for_order( $order, $selected_payment_type );
} else {
// It's a setup intent. To be handled.
return [ 'result' => 'failure' ];
$payment_intent = $this->process_setup_intent_for_order( $order, $payment_information );
}

// Handle saving the payment method in the store.
// It's already attached to the Stripe customer at this point.
if ( $payment_information['save_payment_method_to_store'] ) {
$this->handle_saving_payment_method(
$order,
$payment_information['payment_method'],
$selected_payment_type
);
} elseif ( $payment_information['is_using_saved_payment_method'] ) {
$this->maybe_update_source_on_subscription_order(
$order,
(object) [
'payment_method' => $payment_information['payment_method'],
'customer' => $payment_information['customer'],
]
);
}

// Set the selected UPE payment method type title in the WC order.
$this->set_payment_method_title_for_order( $order, $selected_payment_type );

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

/**
Expand Down Expand Up @@ -761,6 +760,31 @@ private function process_payment_with_deferred_intent( int $order_id ) {
}
}

if ( $payment_needed ) {
// Use the last charge within the intent to proceed.
$charge = end( $payment_intent->charges->data );

// Only process the response if it contains a charge object. Intents with no charge require further action like 3DS and will be processed later.
if ( $charge ) {
$this->process_response( $charge, $order );
}
} elseif ( $this->is_changing_payment_method_for_subscription() ) {
// Trigger wc_stripe_change_subs_payment_method_success action hook to preserve backwards compatibility, see process_change_subscription_payment_method().
do_action(
'wc_stripe_change_subs_payment_method_success',
$payment_information['payment_method'],
(object) [
'token_id' => false !== $payment_information['token'] ? $payment_information['token']->get_id() : false,
'customer' => $payment_information['customer'],
'source' => null,
'source_object' => $payment_method,
'payment_method' => $payment_information['payment_method'],
]
);
} elseif ( in_array( $payment_intent->status, self::SUCCESSFUL_INTENT_STATUS, true ) ) {
$order->payment_complete();
}

return [
'result' => 'success',
'redirect' => $redirect,
Expand Down Expand Up @@ -1700,6 +1724,41 @@ private function process_payment_intent_for_order( WC_Order $order, array $payme
return $payment_intent;
}

/**
* Create a setup intent for the order.
*
* @param WC_Order $order The WC Order for which we're handling a setup intent.
* @param array $payment_information The payment information to be used for the setup intent.
*
* @throws WC_Stripe_Exception When there's an error creating the setup intent.
*
* @return stdClass
*/
private function process_setup_intent_for_order( WC_Order $order, array $payment_information ) {
$setup_intent = $this->intent_controller->create_and_confirm_setup_intent( $payment_information );

if ( ! empty( $setup_intent->error ) ) {

// Add the setup intent information to the order meta, if one was created despite the error.
if ( ! empty( $setup_intent->error->payment_intent ) ) {
$this->save_intent_to_order( $order, $setup_intent->error->payment_intent );
}

$this->maybe_remove_non_existent_customer( $setup_intent->error, $order );

throw new WC_Stripe_Exception(
// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_print_r
print_r( $setup_intent, true ),
__( 'Sorry, we are unable to process your payment at this time. Please retry later.', 'woocommerce-gateway-stripe' )
);
}

// Add the payment intent information to the order meta.
$this->save_intent_to_order( $order, $setup_intent );

return $setup_intent;
}

/**
* Collects the payment information needed for processing a payment intent.
*
Expand All @@ -1712,6 +1771,7 @@ private function prepare_payment_information_from_request( WC_Order $order ) {
$currency = strtolower( $order->get_currency() );
$amount = WC_Stripe_Helper::get_stripe_amount( $order->get_total(), $currency );
$shipping_details = null;
$token = false;

$save_payment_method_to_store = $this->should_save_payment_method_from_request( $order->get_id(), $selected_payment_type );
$is_using_saved_payment_method = $this->is_using_saved_payment_method();
Expand Down Expand Up @@ -1755,6 +1815,7 @@ private function prepare_payment_information_from_request( WC_Order $order ) {
'selected_payment_type' => $selected_payment_type,
'shipping' => $shipping_details,
'statement_descriptor' => $this->get_statement_descriptor( $order, $selected_payment_type ),
'token' => $token,
'return_url' => $this->get_return_url_for_redirect( $order, $save_payment_method_to_store ),
];

Expand Down
Loading