From 8aca8f7a937c11c3bda112c7f235e31492090c95 Mon Sep 17 00:00:00 2001 From: Samer Alabi Date: Fri, 4 Oct 2024 11:49:51 -0400 Subject: [PATCH] Add Customer Session to server & playground --- example/package.json | 2 +- example/server/index.ts | 170 ++++++++++++---- .../src/screens/PaymentsUICompleteScreen.tsx | 191 ++++++++++++------ example/yarn.lock | 8 +- 4 files changed, 270 insertions(+), 101 deletions(-) diff --git a/example/package.json b/example/package.json index d33734244..300fd3fc2 100644 --- a/example/package.json +++ b/example/package.json @@ -37,7 +37,7 @@ "nodemon": "^2.0.19", "path": "^0.12.7", "react-test-renderer": "18.0.0", - "stripe": "^11.0.0", + "stripe": "^14.18.0", "typescript": "^4.5.5" } } diff --git a/example/server/index.ts b/example/server/index.ts index 33c06e01b..256bdbe72 100644 --- a/example/server/index.ts +++ b/example/server/index.ts @@ -117,7 +117,7 @@ app.post( const { secret_key } = getKeys(payment_method_types[0]); const stripe = new Stripe(secret_key as string, { - apiVersion: '2022-11-15', + apiVersion: '2023-10-16', typescript: true, }); @@ -177,7 +177,7 @@ app.post( const { secret_key } = getKeys(); const stripe = new Stripe(secret_key as string, { - apiVersion: '2022-11-15', + apiVersion: '2023-10-16', typescript: true, }); const customers = await stripe.customers.list({ @@ -255,7 +255,7 @@ app.post( const { secret_key } = getKeys(); const stripe = new Stripe(secret_key as string, { - apiVersion: '2022-11-15', + apiVersion: '2023-10-16', typescript: true, }); @@ -344,7 +344,7 @@ app.post('/create-setup-intent', async (req, res) => { const { secret_key } = getKeys(payment_method_types[0]); const stripe = new Stripe(secret_key as string, { - apiVersion: '2022-11-15', + apiVersion: '2023-10-16', typescript: true, }); const customer = await stripe.customers.create({ email }); @@ -401,7 +401,7 @@ app.post( const { secret_key } = getKeys(); const stripe = new Stripe(secret_key as string, { - apiVersion: '2022-11-15', + apiVersion: '2023-10-16', typescript: true, }); // console.log('webhook!', req); @@ -464,7 +464,7 @@ app.post('/charge-card-off-session', async (req, res) => { const { secret_key } = getKeys(); const stripe = new Stripe(secret_key as string, { - apiVersion: '2022-11-15', + apiVersion: '2023-10-16', typescript: true, }); @@ -534,11 +534,27 @@ app.post('/charge-card-off-session', async (req, res) => { // This example sets up an endpoint using the Express framework. // Watch this video to get started: https://youtu.be/rPR2aJ6XnAc. -app.post('/payment-sheet', async (_, res) => { +app.post('/payment-sheet', async (req, res) => { + const { + customer_key_type, + }: { + customer_key_type?: string; + } = req.body; + + if ( + customer_key_type !== 'legacy_ephemeral_key' && + customer_key_type !== 'customer_session' + ) { + return res.send({ + error: + '`customer_key_type` is not valid! Please pass either "customer_session" or "legacy_ephemeral_key"', + }); + } + const { secret_key } = getKeys(); const stripe = new Stripe(secret_key as string, { - apiVersion: '2022-11-15', + apiVersion: '2023-10-16', typescript: true, }); @@ -553,10 +569,6 @@ app.post('/payment-sheet', async (_, res) => { }); } - const ephemeralKey = await stripe.ephemeralKeys.create( - { customer: customer.id }, - { apiVersion: '2022-11-15' } - ); const paymentIntent = await stripe.paymentIntents.create({ amount: 5099, currency: 'usd', @@ -577,18 +589,72 @@ app.post('/payment-sheet', async (_, res) => { // 'us_bank_account', ], }); - return res.json({ - paymentIntent: paymentIntent.client_secret, - ephemeralKey: ephemeralKey.secret, - customer: customer.id, - }); + + if (customer_key_type === 'legacy_ephemeral_key') { + const ephemeralKey = await stripe.ephemeralKeys.create( + { customer: customer.id }, + { apiVersion: '2023-10-16' } + ); + + return res.json({ + paymentIntent: paymentIntent.client_secret, + ephemeralKey: ephemeralKey.secret, + customer: customer.id, + }); + } else { + const customerSessionClientSecret = await stripe.customerSessions.create( + { + customer: customer.id, + components: { + // This needs to be ignored because `mobile_payment_element` is not specified as a type in `stripe-node` yet. + // @ts-ignore + mobile_payment_element: { + enabled: true, + features: { + payment_method_save: 'disabled', + payment_method_remove: 'enabled', + payment_method_redisplay: 'enabled', + payment_method_allow_redisplay_filters: [ + 'unspecified', + 'limited', + 'always', + ], + }, + }, + }, + }, + { apiVersion: '2023-10-16' } + ); + + return res.json({ + paymentIntent: paymentIntent.client_secret, + customerSessionClientSecret: customerSessionClientSecret.client_secret, + customer: customer.id, + }); + } }); -app.post('/payment-sheet-subscription', async (_, res) => { +app.post('/payment-sheet-subscription', async (req, res) => { + const { + customer_key_type, + }: { + customer_key_type?: string; + } = req.body; + + if ( + customer_key_type !== 'legacy_ephemeral_key' && + customer_key_type !== 'customer_session' + ) { + return res.send({ + error: + '`customer_key_type` is not valid! Please pass either "customer_session" or "legacy_ephemeral_key"', + }); + } + const { secret_key } = getKeys(); const stripe = new Stripe(secret_key as string, { - apiVersion: '2022-11-15', + apiVersion: '2023-10-16', typescript: true, }); @@ -603,10 +669,6 @@ app.post('/payment-sheet-subscription', async (_, res) => { }); } - const ephemeralKey = await stripe.ephemeralKeys.create( - { customer: customer.id }, - { apiVersion: '2022-11-15' } - ); const subscription = await stripe.subscriptions.create({ customer: customer.id, items: [{ price: 'price_1L3hcFLu5o3P18Zp9GDQEnqe' }], @@ -618,11 +680,49 @@ app.post('/payment-sheet-subscription', async (_, res) => { subscription.pending_setup_intent ); - return res.json({ - setupIntent: setupIntent.client_secret, - ephemeralKey: ephemeralKey.secret, - customer: customer.id, - }); + if (customer_key_type === 'legacy_ephemeral_key') { + const ephemeralKey = await stripe.ephemeralKeys.create( + { customer: customer.id }, + { apiVersion: '2023-10-16' } + ); + + return res.json({ + setupIntent: setupIntent.client_secret, + ephemeralKey: ephemeralKey.secret, + customer: customer.id, + }); + } else { + const customerSessionClientSecret = await stripe.customerSessions.create( + { + customer: customer.id, + components: { + // This needs to be ignored because `mobile_payment_element` is not specified as a type in `stripe-node` yet. + // @ts-ignore + mobile_payment_element: { + enabled: true, + features: { + payment_method_save: true, + payment_method_remove: true, + payment_method_redisplay: true, + payment_method_save_allow_redisplay_override: true, + payment_method_allow_redisplay_filters: [ + 'unspecified', + 'limited', + 'always', + ], + }, + }, + }, + }, + { apiVersion: '2023-10-16' } + ); + + return res.json({ + setupIntent: setupIntent.client_secret, + customerSessionClientSecret: customerSessionClientSecret.client_secret, + customer: customer.id, + }); + } } else { throw new Error( 'Expected response type string, but received: ' + @@ -655,7 +755,7 @@ app.post('/issuing-card-details', async (req, res) => { const { secret_key } = getKeys(); const stripe = new Stripe(secret_key as string, { - apiVersion: '2022-11-15', + apiVersion: '2023-10-16', typescript: true, }); @@ -678,7 +778,7 @@ app.post('/financial-connections-sheet', async (_, res) => { const { secret_key } = getKeys(); const stripe = new Stripe(secret_key as string, { - apiVersion: '2022-11-15', + apiVersion: '2023-10-16', typescript: true, }); @@ -704,7 +804,7 @@ app.post('/payment-intent-for-payment-sheet', async (req, res) => { const { secret_key } = getKeys(); const stripe = new Stripe(secret_key as string, { - apiVersion: '2022-11-15', + apiVersion: '2023-10-16', typescript: true, }); @@ -726,7 +826,7 @@ app.post('/customer-sheet', async (_, res) => { const { secret_key } = getKeys(); const stripe = new Stripe(secret_key as string, { - apiVersion: '2022-11-15', + apiVersion: '2023-10-16', typescript: true, }); @@ -754,7 +854,7 @@ app.post('/fetch-payment-methods', async (req, res) => { const { secret_key } = getKeys(); const stripe = new Stripe(secret_key as string, { - apiVersion: '2022-11-15', + apiVersion: '2023-10-16', typescript: true, }); @@ -771,7 +871,7 @@ app.post('/attach-payment-method', async (req, res) => { const { secret_key } = getKeys(); const stripe = new Stripe(secret_key as string, { - apiVersion: '2022-11-15', + apiVersion: '2023-10-16', typescript: true, }); console.log({ customer: req.body.customerId }); @@ -789,7 +889,7 @@ app.post('/detach-payment-method', async (req, res) => { const { secret_key } = getKeys(); const stripe = new Stripe(secret_key as string, { - apiVersion: '2022-11-15', + apiVersion: '2023-10-16', typescript: true, }); diff --git a/example/src/screens/PaymentsUICompleteScreen.tsx b/example/src/screens/PaymentsUICompleteScreen.tsx index 00956c149..8ac98ac5c 100644 --- a/example/src/screens/PaymentsUICompleteScreen.tsx +++ b/example/src/screens/PaymentsUICompleteScreen.tsx @@ -1,5 +1,13 @@ -import React, { useState } from 'react'; -import { Alert } from 'react-native'; +import React, { useCallback, useEffect, useState } from 'react'; +import { + Alert, + StyleProp, + Switch, + Text, + TextStyle, + View, + ViewStyle, +} from 'react-native'; import { AddressDetails, useStripe, @@ -23,20 +31,39 @@ export default function PaymentsUICompleteScreen() { const [addressSheetVisible, setAddressSheetVisible] = useState(false); const [clientSecret, setClientSecret] = useState(); - const fetchPaymentSheetParams = async () => { + const [customerKeyType, setCustomerKeyType] = useState( + 'legacy_ephemeral_key' + ); + + const fetchPaymentSheetParams = async (customer_key_type: string) => { const response = await fetch(`${API_URL}/payment-sheet`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, + body: JSON.stringify({ + customer_key_type, + }), }); - const { paymentIntent, ephemeralKey, customer } = await response.json(); - setClientSecret(paymentIntent); - return { - paymentIntent, - ephemeralKey, - customer, - }; + + if (customer_key_type === 'customer_session') { + const { paymentIntent, customerSessionClientSecret, customer } = + await response.json(); + setClientSecret(paymentIntent); + return { + paymentIntent, + customerSessionClientSecret, + customer, + }; + } else { + const { paymentIntent, ephemeralKey, customer } = await response.json(); + setClientSecret(paymentIntent); + return { + paymentIntent, + ephemeralKey, + customer, + }; + } }; const openPaymentSheet = async () => { @@ -74,65 +101,89 @@ export default function PaymentsUICompleteScreen() { setLoading(false); }; - const initialisePaymentSheet = async (shippingDetails?: AddressDetails) => { - const { paymentIntent, ephemeralKey, customer } = - await fetchPaymentSheetParams(); + const initialisePaymentSheet = useCallback( + async (shippingDetails?: AddressDetails) => { + const { paymentIntent, customer, ...remainingParams } = + await fetchPaymentSheetParams(customerKeyType); - const address: Address = { - city: 'San Francisco', - country: 'AT', - line1: '510 Townsend St.', - line2: '123 Street', - postalCode: '94102', - state: 'California', - }; - const billingDetails: BillingDetails = { - name: 'Jane Doe', - email: 'foo@bar.com', - phone: '555-555-555', - address: address, - }; + const clientSecretParams = + customerKeyType === 'customer_session' + ? { + customerSessionClientSecret: + remainingParams.customerSessionClientSecret, + } + : { customerEphemeralKeySecret: remainingParams.ephemeralKey }; - const { error } = await initPaymentSheet({ - customerId: customer, - customerEphemeralKeySecret: ephemeralKey, - paymentIntentClientSecret: paymentIntent, - customFlow: false, - merchantDisplayName: 'Example Inc.', - applePay: { merchantCountryCode: 'US' }, - style: 'automatic', - googlePay: { - merchantCountryCode: 'US', - testEnv: true, - }, - returnURL: 'stripe-example://stripe-redirect', - defaultBillingDetails: billingDetails, - defaultShippingDetails: shippingDetails, - allowsDelayedPaymentMethods: true, - appearance, - primaryButtonLabel: 'purchase!', - removeSavedPaymentMethodMessage: 'remove this payment method?', - preferredNetworks: [CardBrand.Amex, CardBrand.Visa], - }); - if (!error) { - setPaymentSheetEnabled(true); - } else if (error.code === PaymentSheetError.Failed) { - Alert.alert( - `PaymentSheet init failed with error code: ${error.code}`, - error.message - ); - } else if (error.code === PaymentSheetError.Canceled) { - Alert.alert( - `PaymentSheet init was canceled with code: ${error.code}`, - error.message - ); + const address: Address = { + city: 'San Francisco', + country: 'AT', + line1: '510 Townsend St.', + line2: '123 Street', + postalCode: '94102', + state: 'California', + }; + const billingDetails: BillingDetails = { + name: 'Jane Doe', + email: 'foo@bar.com', + phone: '555-555-555', + address: address, + }; + + const { error } = await initPaymentSheet({ + customerId: customer, + paymentIntentClientSecret: paymentIntent, + customFlow: false, + merchantDisplayName: 'Example Inc.', + applePay: { merchantCountryCode: 'US' }, + style: 'automatic', + googlePay: { + merchantCountryCode: 'US', + testEnv: true, + }, + returnURL: 'stripe-example://stripe-redirect', + defaultBillingDetails: billingDetails, + defaultShippingDetails: shippingDetails, + allowsDelayedPaymentMethods: true, + appearance, + primaryButtonLabel: 'purchase!', + removeSavedPaymentMethodMessage: 'remove this payment method?', + preferredNetworks: [CardBrand.Amex, CardBrand.Visa], + ...clientSecretParams, + }); + if (!error) { + setPaymentSheetEnabled(true); + } else if (error.code === PaymentSheetError.Failed) { + Alert.alert( + `PaymentSheet init failed with error code: ${error.code}`, + error.message + ); + } else if (error.code === PaymentSheetError.Canceled) { + Alert.alert( + `PaymentSheet init was canceled with code: ${error.code}`, + error.message + ); + } + }, + [customerKeyType, initPaymentSheet] + ); + + const toggleCustomerKeyType = (value: boolean) => { + if (value) { + setCustomerKeyType('customer_session'); + } else { + setCustomerKeyType('legacy_ephemeral_key'); } }; + useEffect(() => { + setPaymentSheetEnabled(false); + initialisePaymentSheet().catch((err) => console.log(err)); + }, [customerKeyType, initialisePaymentSheet]); + return ( // In your app’s checkout, make a network request to the backend and initialize PaymentSheet. // To reduce loading time, make this request before the Checkout button is tapped, e.g. when the screen is loaded. - +