Skip to content
Draft
Show file tree
Hide file tree
Changes from all 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
Original file line number Diff line number Diff line change
Expand Up @@ -1031,7 +1031,7 @@ export default function CheckoutMainContent( {
validatingButtonText={ validatingButtonText }
validatingButtonAriaLabel={ validatingButtonText }
onPageLoadError={ onPageLoadError }
waitForPaymentMethodIds={ [ 'apple-pay', 'google-pay' ] }
waitForPaymentMethodIds={ [ 'apple-pay', 'google-pay', 'stripe-wallet' ] }
{ ...( isMobileCheckoutStickySummary && {
/* Figma 3971:13237 — the active heading reads "Payment method"
under the experiment (vs. composite-checkout's default
Expand Down
3 changes: 3 additions & 0 deletions client/my-sites/checkout/src/components/checkout-main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ import payPalProcessor from '../lib/paypal-express-processor';
import { payPalJsProcessor } from '../lib/paypal-js-processor';
import { pixAutomaticoProcessor } from '../lib/pix-automatico-processor';
import { pixProcessor } from '../lib/pix-processor';
import stripeWalletProcessor from '../lib/stripe-wallet-processor';
import { translateResponseCartToWPCOMCart } from '../lib/translate-cart';
import upiProcessor from '../lib/upi-processor';
import weChatProcessor from '../lib/we-chat-processor';
Expand Down Expand Up @@ -591,6 +592,8 @@ export default function CheckoutMain( {
),
'stripe-blik': ( transactionData: unknown ) =>
blikProcessor( transactionData, dataForProcessor, translate ),
'stripe-wallet': ( transactionData: unknown ) =>
stripeWalletProcessor( transactionData, dataForProcessor ),
'existing-card': ( transactionData: unknown ) =>
existingCardProcessor( transactionData, dataForProcessor ),
'existing-card-ebanx': ( transactionData: unknown ) =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import {
createPixPaymentMethod,
createPixAutomaticoPaymentMethod,
} from '../../payment-methods/pix';
import { createStripeWalletMethod } from '../../payment-methods/stripe-wallet';
import { createWeChatMethod } from '../../payment-methods/wechat';
import useCreateExistingCards from './use-create-existing-cards';
import useCreateExistingPayPalPPCP from './use-create-existing-paypal-ppcp';
Expand Down Expand Up @@ -368,6 +369,33 @@ function useCreateStripeUpi( {
);
}

function useCreateStripeWallet( {
isStripeLoading,
stripeLoadingError,
stripeConfiguration,
stripe,
responseCart,
}: {
isStripeLoading: boolean;
stripeLoadingError: StripeLoadingError;
stripeConfiguration: StripeConfiguration | null;
stripe: Stripe | null;
responseCart: ReturnType< typeof useShoppingCart >[ 'responseCart' ];
} ): PaymentMethod | null {
const isReady =
! isStripeLoading &&
! stripeLoadingError &&
stripe &&
stripeConfiguration &&
isEnabled( 'checkout/stripe-wallet' );

return useMemo( () => {
return isReady && stripe && stripeConfiguration
? createStripeWalletMethod( { stripe, stripeConfiguration, responseCart } )
: null;
}, [ isReady, stripe, stripeConfiguration, responseCart ] );
}

/**
* Create all possible payment methods.
*
Expand Down Expand Up @@ -505,6 +533,14 @@ export default function useCreatePaymentMethods( {
stripeLoadingError,
} );

const stripeWalletMethod = useCreateStripeWallet( {
isStripeLoading,
stripeLoadingError,
stripeConfiguration,
stripe,
responseCart,
} );

// The order of this array is the order that Payment Methods will be
// displayed in Checkout, although not all payment methods here will be
// listed; the list of allowed payment methods is returned by the shopping
Expand All @@ -513,6 +549,7 @@ export default function useCreatePaymentMethods( {
let paymentMethods = [
...existingCardMethods,
...existingPayPalPPCPMethods,
stripeWalletMethod,
applePayMethod,
googlePayMethod,
stripeMethod,
Expand All @@ -538,6 +575,7 @@ export default function useCreatePaymentMethods( {
paymentMethods = [
...existingCardMethods,
...existingPayPalPPCPMethods,
stripeWalletMethod,
applePayMethod,
googlePayMethod,
paypalExpressMethod,
Expand Down
166 changes: 166 additions & 0 deletions client/my-sites/checkout/src/lib/stripe-wallet-processor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
import { makeSuccessResponse, makeErrorResponse } from '@automattic/composite-checkout';
import debugFactory from 'debug';
import { recordTracksEvent } from 'calypso/state/analytics/actions';
import { logStashEvent, recordTransactionBeginAnalytics } from './analytics';
import getDomainDetails from './get-domain-details';
import getPostalCode from './get-postal-code';
import { addUrlToPendingPageRedirect } from './pending-page';
import submitWpcomTransaction from './submit-wpcom-transaction';
import {
createTransactionEndpointRequestPayload,
createTransactionEndpointCartFromResponseCart,
} from './translate-cart';
import type { PaymentProcessorOptions } from '../types/payment-processors';
import type { PaymentProcessorResponse } from '@automattic/composite-checkout';
import type { StripeElements } from '@stripe/stripe-js';

const debug = debugFactory( 'calypso:composite-checkout:stripe-wallet-processor' );

type StripeWalletTransactionRequest = {
elements: StripeElements;
expressPaymentType: string;
};

export default async function stripeWalletProcessor(
submitData: unknown,
transactionOptions: PaymentProcessorOptions
): Promise< PaymentProcessorResponse > {
if ( ! isValidTransactionData( submitData ) ) {
throw new Error( 'Required purchase data is missing' );
}

const {
stripe,
stripeConfiguration,
responseCart,
contactDetails,
siteSlug,
siteId,
fromSiteSlug,
getThankYouUrl,
includeDomainDetails,
includeGSuiteDetails,
reduxDispatch,
} = transactionOptions;

if ( ! stripe ) {
throw new Error( 'Stripe is required for stripe-wallet payment' );
}

reduxDispatch( recordTransactionBeginAnalytics( { paymentMethodId: 'stripe-wallet' } ) );

const { elements, expressPaymentType } = submitData;

// Build the pending-page URL used as return_url for stripe.confirmPayment and
// as the success landing URL after an inline confirm.
const thankYouUrl = getThankYouUrl() || 'https://wordpress.com';
const successUrl = addUrlToPendingPageRedirect( thankYouUrl, {
siteSlug,
fromSiteSlug,
urlType: 'absolute',
} );

const formattedTransactionData = createTransactionEndpointRequestPayload( {
country: contactDetails?.countryCode?.value ?? '',
postalCode: getPostalCode( contactDetails ),
subdivisionCode: contactDetails?.state?.value,
domainDetails: getDomainDetails( contactDetails, {
includeDomainDetails,
includeGSuiteDetails,
} ),
cart: createTransactionEndpointCartFromResponseCart( {
siteId,
contactDetails:
getDomainDetails( contactDetails, { includeDomainDetails, includeGSuiteDetails } ) ?? null,
responseCart,
} ),
paymentMethodType: 'WPCOM_Billing_Stripe_Wallet',
paymentPartnerProcessorId: stripeConfiguration?.processor_id,
successUrl,
name: contactDetails?.firstName?.value ?? '',
email: contactDetails?.email?.value,
} );

debug( 'sending stripe-wallet transaction', formattedTransactionData );

let clientSecret: string;
let orderId: number | '' = '';

try {
const response = await submitWpcomTransaction( formattedTransactionData, transactionOptions );
const message = ( response as { message?: unknown } ).message;

if (
! message ||
typeof message !== 'object' ||
! ( 'payment_intent_client_secret' in message )
) {
throw new Error( 'Server did not return a payment intent client secret' );
}

clientSecret = String(
( message as { payment_intent_client_secret: string } ).payment_intent_client_secret
);
orderId = ( response as { order_id?: number | '' } ).order_id ?? '';
} catch ( error ) {
debug( 'transaction submission failed', error );
const errorMessage = ( error as Error ).message;
reduxDispatch(
recordTracksEvent( 'calypso_checkout_stripe_wallet_transaction_failed', {
express_payment_type: expressPaymentType,
error: errorMessage,
} )
);
logStashEvent( 'calypso_checkout_stripe_wallet_transaction_failed', {
express_payment_type: expressPaymentType,
tags: [ `express_payment_type:${ expressPaymentType }` ],
error: errorMessage,
} );
return makeErrorResponse( errorMessage );
}

// Confirm the PaymentIntent client-side. redirect:'if_required' means Stripe.js
// only navigates away when the payment method genuinely requires a browser redirect
// (e.g. 3DS); otherwise the promise resolves inline and we route to the pending page.
debug( 'confirming payment client-side', { orderId } );
const { error: confirmError } = await stripe.confirmPayment( {
elements,
clientSecret,
confirmParams: { return_url: successUrl },
redirect: 'if_required',
} );

if ( confirmError ) {
debug( 'stripe.confirmPayment failed', confirmError );
const errorMessage = confirmError.message ?? 'Payment confirmation failed';
reduxDispatch(
recordTracksEvent( 'calypso_checkout_stripe_wallet_transaction_failed', {
express_payment_type: expressPaymentType,
error: errorMessage,
} )
);
logStashEvent( 'calypso_checkout_stripe_wallet_transaction_failed', {
express_payment_type: expressPaymentType,
tags: [ `express_payment_type:${ expressPaymentType }` ],
error: errorMessage,
} );
return makeErrorResponse( errorMessage );
}

// Inline confirm succeeded — return success so composite-checkout routes to the
// pending page, which will poll for the webhook-driven provisioning.
return makeSuccessResponse( { order_id: orderId } );
}

function isValidTransactionData(
submitData: unknown
): submitData is StripeWalletTransactionRequest {
const data = submitData as StripeWalletTransactionRequest;
if ( ! data?.elements ) {
throw new Error( 'Transaction requires Stripe Elements and none was provided' );
}
if ( ! data?.expressPaymentType ) {
throw new Error( 'Transaction requires expressPaymentType and none was provided' );
}
return true;
}
Loading
Loading