Skip to content

Commit

Permalink
Add payment flow using confirmation tokens
Browse files Browse the repository at this point in the history
  • Loading branch information
annemirasol committed Jan 17, 2025
1 parent 8db4fde commit d5d7c44
Show file tree
Hide file tree
Showing 6 changed files with 290 additions and 99 deletions.
6 changes: 3 additions & 3 deletions client/blocks/express-checkout/hooks.js
Original file line number Diff line number Diff line change
Expand Up @@ -116,14 +116,14 @@ export const useExpressCheckout = ( {
);

const onConfirm = async ( event ) => {
await onConfirmHandler(
await onConfirmHandler( {
api,
stripe,
elements,
completePayment,
abortPayment,
event
);
event,
} );
};

return {
Expand Down
12 changes: 6 additions & 6 deletions client/entrypoints/express-checkout/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -234,15 +234,15 @@ jQuery( function ( $ ) {
eceButton.on( 'confirm', async ( event ) => {
const order = options.order ? options.order : 0;

return await onConfirmHandler(
return await onConfirmHandler( {
api,
api.getStripe(),
stripe: api.getStripe(),
elements,
wcStripeECE.completePayment,
wcStripeECE.abortPayment,
completePayment: wcStripeECE.completePayment,
abortPayment: wcStripeECE.abortPayment,
event,
order
);
order,
} );
} );

eceButton.on( 'cancel', () => {
Expand Down
119 changes: 102 additions & 17 deletions client/express-checkout/event-handler.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,20 +53,28 @@ export const shippingRateChangeHandler = async ( api, event, elements ) => {
}
};

export const onConfirmHandler = async (
const handlePaymentFlowException = ( event, exception, abortPayment ) => {
let errorMessage;
if ( exception.message ) {
errorMessage = exception.message;
} else {
errorMessage = __(
'There was a problem processing the order.',
'woocommerce-gateway-stripe'
);
}
return abortPayment( event, errorMessage );
};

const handleManualPaymentMethodFlow = async ( {
api,
stripe,
elements,
completePayment,
abortPayment,
event,
order = 0 // Order ID for the pay for order flow.
) => {
const submitResponse = await elements.submit();
if ( submitResponse?.error ) {
return abortPayment( event, submitResponse?.error?.message );
}

order,
} ) => {
const { paymentMethod, error } = await stripe.createPaymentMethod( {
elements,
} );
Expand All @@ -80,12 +88,18 @@ export const onConfirmHandler = async (
let orderResponse;
if ( ! order ) {
orderResponse = await api.expressCheckoutECECreateOrder(
normalizeOrderData( event, paymentMethod.id )
normalizeOrderData( {
event,
paymentMethodId: paymentMethod.id,
} )
);
} else {
orderResponse = await api.expressCheckoutECEPayForOrder(
order,
normalizePayForOrderData( event, paymentMethod.id )
normalizePayForOrderData( {
event,
paymentMethodId: paymentMethod.id,
} )
);
}

Expand All @@ -109,19 +123,90 @@ export const onConfirmHandler = async (
completePayment( redirectUrl );
}
} catch ( e ) {
let errorMessage;
if ( e.message ) {
errorMessage = e.message;
return handlePaymentFlowException( event, e, abortPayment );
}
};

const handleConfirmationTokenFlow = async ( {
api,
stripe,
elements,
completePayment,
abortPayment,
event,
order,
} ) => {
// Create a ConfirmationToken that we can use later to create and confirm the payment intent.
const { error, confirmationToken } = await stripe.createConfirmationToken( {
elements,
params: {
// Required by Amazon Pay, but is not used by express checkout
// as it uses a payment modal instead of redirection.
return_url: window.location.href,
},
} );

if ( error ) {
return abortPayment(
event,
getErrorMessageFromNotice( error.message ),
true
);
}

try {
let orderResponse;
if ( ! order ) {
orderResponse = await api.expressCheckoutECECreateOrder(
normalizeOrderData( {
event,
confirmationTokenId: confirmationToken.id,
} )
);
} else {
errorMessage = __(
'There was a problem processing the order.',
'woocommerce-gateway-stripe'
orderResponse = await api.expressCheckoutECEPayForOrder(
order,
normalizePayForOrderData( {
event,
confirmationTokenId: confirmationToken.id,
} )
);
}

if ( orderResponse.result !== 'success' ) {
return abortPayment(
event,
getErrorMessageFromNotice( orderResponse.messages ),
true
);
}
return abortPayment( event, errorMessage );

completePayment( orderResponse.redirect );
} catch ( e ) {
return handlePaymentFlowException( event, e, abortPayment );
}
};

export const onConfirmHandler = async ( params ) => {
const { abortPayment, elements, event } = params;

const submitResponse = await elements.submit();
if ( submitResponse?.error ) {
return abortPayment( event, submitResponse?.error?.message );
}

// Amazon Pay does not support manual payment method creation.
// TODO: GOOGLE PAY FOR TESTING ONLY. REMOVE BEFORE MERGE!
if (
event.expressPaymentType === 'amazon_pay' ||
event.expressPaymentType === 'google_pay'
) {
return handleConfirmationTokenFlow( params );
}

return handleManualPaymentMethodFlow( params );
};

export const onReadyHandler = function ( { availablePaymentMethods } ) {
if ( availablePaymentMethods ) {
const enabledMethods = Object.entries( availablePaymentMethods )
Expand Down
26 changes: 20 additions & 6 deletions client/express-checkout/utils/normalize.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,18 @@ export const normalizeLineItems = ( displayItems ) => {
/**
* Normalize order data from Stripe's object to the expected format for WC.
*
* @param {Object} event Stripe's event object.
* @param {string} paymentMethodId Stripe's payment method id.
* @param {Object} params
* @param {Object} params.event Stripe's event object.
* @param {string|null} params.paymentMethodId Payment method ID from Stripe, if using manual payment method flow.
* @param {string|null} params.confirmationTokenId Confirmation token ID from Stripe, if using confirmation token flow.
*
* @return {Object} Order object in the format WooCommerce expects.
*/
export const normalizeOrderData = ( event, paymentMethodId ) => {
export const normalizeOrderData = ( {
event,
paymentMethodId,
confirmationTokenId,
} ) => {
const name = event?.billingDetails?.name;
const email = event?.billingDetails?.email ?? '';
const billing = event?.billingDetails?.address ?? {};
Expand Down Expand Up @@ -71,6 +77,7 @@ export const normalizeOrderData = ( event, paymentMethodId ) => {
ship_to_different_address: 1,
terms: 1,
'wc-stripe-payment-method': paymentMethodId,
'wc-stripe-confirmation-token': confirmationTokenId,
express_checkout_type: event?.expressPaymentType,
express_payment_type: event?.expressPaymentType,
'wc-stripe-is-deferred-intent': true,
Expand All @@ -81,16 +88,23 @@ export const normalizeOrderData = ( event, paymentMethodId ) => {
/**
* Normalize Pay for Order data from Stripe's object to the expected format for WC.
*
* @param {Object} event Stripe's event object.
* @param {string} paymentMethodId Stripe's payment method id.
* @param {Object} params
* @param {Object} params.event Stripe's event object.
* @param {string|null} params.paymentMethodId Payment method IDfrom Stripe, if using manual payment method flow
* @param {string|null} params.confirmationTokenId Confirmation token ID from Stripe, if using confirmation token flow
*
* @return {Object} Order object in the format WooCommerce expects.
*/
export const normalizePayForOrderData = ( event, paymentMethodId ) => {
export const normalizePayForOrderData = ( {
event,
paymentMethodId,
confirmationTokenId,
} ) => {
return {
payment_method: 'stripe',
'wc-stripe-is-deferred-intent': true, // Set the deferred intent flag, so the deferred intent flow is used.
'wc-stripe-payment-method': paymentMethodId,
'wc-stripe-confirmation-token': confirmationTokenId,
express_payment_type: event?.expressPaymentType,
};
};
Expand Down
28 changes: 20 additions & 8 deletions includes/class-wc-stripe-intent-controller.php
Original file line number Diff line number Diff line change
Expand Up @@ -243,9 +243,9 @@ public function create_setup_intent() {
// 4. Generate the setup intent
$setup_intent = WC_Stripe_API::request(
[
'customer' => $customer->get_id(),
'confirm' => 'true',
'payment_method' => $source_id,
'customer' => $customer->get_id(),
'confirm' => 'true',
'payment_method' => $source_id,
'payment_method_types' => [ $source_object->type ],
],
'setup_intents'
Expand Down Expand Up @@ -702,12 +702,17 @@ public function create_and_confirm_payment_intent( $payment_information ) {
'level3',
'metadata',
'order',
'payment_method',
'save_payment_method_to_store',
'shipping',
];

$non_empty_params = [ 'payment_method' ];
$non_empty_params = [];

// The payment method is not required if we're using the confirmation token flow.
if ( empty( $payment_information['confirmation_token'] ) ) {
$required_params[] = 'payment_method';
$non_empty_params[] = 'payment_method';
}

$instance_params = [ 'order' => 'WC_Order' ];

Expand Down Expand Up @@ -916,12 +921,19 @@ private function build_base_payment_intent_request_params( $payment_information

$request = [
'capture_method' => $payment_information['capture_method'],
'payment_method' => $payment_information['payment_method'],
'shipping' => $payment_information['shipping'],
];

$is_using_confirmation_token = ! empty( $payment_information['confirmation_token'] );
if ( $is_using_confirmation_token ) {
$request['confirmation_token'] = $payment_information['confirmation_token'];
} else {
$request['payment_method'] = $payment_information['payment_method'];
}

// 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 ) ) {
// TODO "You cannot provide both a confirmation_token and mandate_data because they both contain payment method information."
if ( ! $is_using_confirmation_token && $this->is_mandate_data_required( $selected_payment_type ) ) {
$request = $this->add_mandate_data( $request );
}

Expand Down Expand Up @@ -1121,7 +1133,7 @@ public function confirm_change_payment_from_setup_intent_ajax() {

// Check if the subscription has the delayed update all flag and attempt to update all subscriptions after the intent has been confirmed. If successful, display the "updated all subscriptions" notice.
if ( WC_Subscriptions_Change_Payment_Gateway::will_subscription_update_all_payment_methods( $subscription ) && WC_Subscriptions_Change_Payment_Gateway::update_all_payment_methods_from_subscription( $subscription, $token->get_gateway_id() ) ) {
$notice = __( 'Payment method updated for all your current subscriptions.', 'woocommerce-gateway-stripe' );
$notice = __( 'Payment method updated for all your current subscriptions.', 'woocommerce-gateway-stripe' );
}

wc_add_notice( $notice );
Expand Down
Loading

0 comments on commit d5d7c44

Please sign in to comment.