diff --git a/examples/hooks/11-Custom-Checkout.js b/examples/hooks/11-Custom-Checkout.js index 7485a7c..8627e17 100644 --- a/examples/hooks/11-Custom-Checkout.js +++ b/examples/hooks/11-Custom-Checkout.js @@ -2,11 +2,10 @@ import React from 'react'; import {loadStripe} from '@stripe/stripe-js'; import { PaymentElement, - useStripe, CheckoutProvider, useCheckout, - AddressElement, -} from '../../src'; + BillingAddressElement, +} from '../../src/checkout'; import '../styles/common.css'; @@ -45,23 +44,27 @@ const CustomerDetails = ({phoneNumber, setPhoneNumber, email, setEmail}) => { }; const CheckoutForm = () => { - const checkout = useCheckout(); + const checkoutState = useCheckout(); const [status, setStatus] = React.useState(); const [loading, setLoading] = React.useState(false); - const stripe = useStripe(); const [phoneNumber, setPhoneNumber] = React.useState(''); const [email, setEmail] = React.useState(''); const handleSubmit = async (event) => { event.preventDefault(); + setStatus(undefined); - if (!stripe || !checkout) { + if (checkoutState.type === 'loading') { + setStatus('Loading...'); + return; + } else if (checkoutState.type === 'error') { + setStatus(`Error: ${checkoutState.error.message}`); return; } try { setLoading(true); - await checkout.confirm({ + await checkoutState.checkout.confirm({ email, phoneNumber, returnUrl: window.location.href, @@ -73,7 +76,7 @@ const CheckoutForm = () => { } }; - const buttonDisabled = !stripe || !checkout || loading; + const buttonDisabled = checkoutState.type !== 'success' || loading; return (
@@ -86,7 +89,7 @@ const CheckoutForm = () => {

Payment Details

Billing Details

- + @@ -112,11 +115,7 @@ const App = () => { const handleSubmit = (e) => { e.preventDefault(); - setStripePromise( - loadStripe(pk, { - betas: ['custom_checkout_beta_6'], - }) - ); + setStripePromise(loadStripe(pk)); }; const handleThemeChange = (e) => { diff --git a/package.json b/package.json index 195051b..2ca5a35 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@stripe/react-stripe-js", - "version": "4.0.2", + "version": "5.0.0-rc.1", "description": "React components for Stripe.js and Stripe Elements", "main": "dist/react-stripe.js", "module": "dist/react-stripe.esm.mjs", @@ -8,7 +8,7 @@ "browser:min": "dist/react-stripe.umd.min.js", "browser": "dist/react-stripe.umd.js", "types": "dist/react-stripe.d.ts", - "releaseCandidate": false, + "releaseCandidate": true, "exports": { ".": { "require": "./dist/react-stripe.js", @@ -93,7 +93,7 @@ "@rollup/plugin-replace": "^5.0.5", "@rollup/plugin-terser": "^0.4.4", "@storybook/react": "^6.5.0-beta.8", - "@stripe/stripe-js": "^7.8.0", + "@stripe/stripe-js": "^8.0.0-rc.1", "@testing-library/jest-dom": "^5.16.4", "@testing-library/react": "^13.1.1", "@testing-library/react-hooks": "^8.0.0", @@ -132,7 +132,7 @@ "@types/react": "18.0.5" }, "peerDependencies": { - "@stripe/stripe-js": ">=1.44.1 <8.0.0", + "@stripe/stripe-js": ">=8.0.0-rc.1 <9.0.0", "react": ">=16.8.0 <20.0.0", "react-dom": ">=16.8.0 <20.0.0" } diff --git a/src/checkout/components/CheckoutProvider.test.tsx b/src/checkout/components/CheckoutProvider.test.tsx index c9ea6fe..5dc0568 100644 --- a/src/checkout/components/CheckoutProvider.test.tsx +++ b/src/checkout/components/CheckoutProvider.test.tsx @@ -12,22 +12,21 @@ describe('CheckoutProvider', () => { let mockStripe: any; let mockStripePromise: any; let mockCheckoutSdk: any; - let mockSession: any; let consoleError: any; let consoleWarn: any; - let mockCheckout: any; + let mockCheckoutActions: any; beforeEach(() => { - mockStripe = mocks.mockStripe(); - mockStripePromise = Promise.resolve(mockStripe); mockCheckoutSdk = mocks.mockCheckoutSdk(); - mockStripe.initCheckout.mockResolvedValue(mockCheckoutSdk); - mockSession = mocks.mockCheckoutSession(); - mockCheckoutSdk.session.mockReturnValue(mockSession); - - const {on: _on, session: _session, ...actions} = mockCheckoutSdk; + mockCheckoutActions = mocks.mockCheckoutActions(); + mockCheckoutSdk.loadActions.mockResolvedValue({ + type: 'success', + actions: mockCheckoutActions, + }); - mockCheckout = {...actions, ...mockSession}; + mockStripe = mocks.mockStripe(); + mockStripe.initCheckout.mockReturnValue(mockCheckoutSdk); + mockStripePromise = Promise.resolve(mockStripe); jest.spyOn(console, 'error'); jest.spyOn(console, 'warn'); @@ -39,15 +38,13 @@ describe('CheckoutProvider', () => { jest.restoreAllMocks(); }); - const makeFetchClientSecret = () => async () => { - return 'cs_123'; - }; + const fakeClientSecret = 'cs_123'; - const wrapper = ({stripe, fetchClientSecret, children}: any) => ( + const wrapper = ({stripe, clientSecret, children}: any) => ( {children} @@ -101,10 +98,15 @@ describe('CheckoutProvider', () => { }); describe('interaction with useCheckout()', () => { - it('works when initCheckout resolves', async () => { + it('works when loadActions resolves', async () => { const stripe: any = mocks.mockStripe(); const deferred = makeDeferred(); - stripe.initCheckout.mockReturnValue(deferred.promise); + const mockSdk = mocks.mockCheckoutSdk(); + const testMockCheckoutActions = mocks.mockCheckoutActions(); + const testMockSession = mocks.mockCheckoutSession(); + + mockSdk.loadActions.mockReturnValue(deferred.promise); + stripe.initCheckout.mockReturnValue(mockSdk); const {result} = renderHook(() => useCheckout(), { wrapper, @@ -114,19 +116,38 @@ describe('CheckoutProvider', () => { expect(result.current).toEqual({type: 'loading'}); expect(stripe.initCheckout).toHaveBeenCalledTimes(1); - await act(() => deferred.resolve(mockCheckoutSdk)); + await act(() => + deferred.resolve({ + type: 'success', + actions: testMockCheckoutActions, + }) + ); + + const {on: _on, loadActions: _loadActions, ...elementsMethods} = mockSdk; + const { + getSession: _getSession, + ...otherCheckoutActions + } = testMockCheckoutActions; + + const expectedCheckout = { + ...elementsMethods, + ...otherCheckoutActions, + ...testMockSession, + }; expect(result.current).toEqual({ type: 'success', - checkout: mockCheckout, + checkout: expectedCheckout, }); expect(stripe.initCheckout).toHaveBeenCalledTimes(1); }); - it('works when initCheckout rejects', async () => { + it('works when loadActions rejects', async () => { const stripe: any = mocks.mockStripe(); const deferred = makeDeferred(); - stripe.initCheckout.mockReturnValue(deferred.promise); + const mockSdk = mocks.mockCheckoutSdk(); + mockSdk.loadActions.mockReturnValue(deferred.promise); + stripe.initCheckout.mockReturnValue(mockSdk); const {result} = renderHook(() => useCheckout(), { wrapper, @@ -149,7 +170,7 @@ describe('CheckoutProvider', () => { const result = render( 'cs_123'}} + options={{clientSecret: Promise.resolve(fakeClientSecret)}} > {null} @@ -171,7 +192,7 @@ describe('CheckoutProvider', () => { render( 'cs_123'}} + options={{clientSecret: fakeClientSecret}} >
@@ -195,7 +216,12 @@ describe('CheckoutProvider', () => { it('when stripe prop changes from null to a Stripe instance', async () => { const stripe: any = mocks.mockStripe(); const deferred = makeDeferred(); - stripe.initCheckout.mockReturnValue(deferred.promise); + const mockSdk = mocks.mockCheckoutSdk(); + const testMockCheckoutActions = mocks.mockCheckoutActions(); + const testMockSession = mocks.mockCheckoutSession(); + + mockSdk.loadActions.mockReturnValue(deferred.promise); + stripe.initCheckout.mockReturnValue(mockSdk); const {result, rerender} = renderHook(() => useCheckout(), { wrapper, @@ -210,11 +236,28 @@ describe('CheckoutProvider', () => { expect(result.current).toEqual({type: 'loading'}); expect(stripe.initCheckout).toHaveBeenCalledTimes(1); - await act(() => deferred.resolve(mockCheckoutSdk)); + await act(() => + deferred.resolve({ + type: 'success', + actions: testMockCheckoutActions, + }) + ); + + const {on: _on, loadActions: _loadActions, ...elementsMethods} = mockSdk; + const { + getSession: _getSession, + ...otherCheckoutActions + } = testMockCheckoutActions; + + const expectedCheckout = { + ...elementsMethods, + ...otherCheckoutActions, + ...testMockSession, + }; expect(result.current).toEqual({ type: 'success', - checkout: mockCheckout, + checkout: expectedCheckout, }); expect(stripe.initCheckout).toHaveBeenCalledTimes(1); }); @@ -223,7 +266,12 @@ describe('CheckoutProvider', () => { const stripe: any = mocks.mockStripe(); const stripeDeferred = makeDeferred(); const deferred = makeDeferred(); - stripe.initCheckout.mockReturnValue(deferred.promise); + const mockSdk = mocks.mockCheckoutSdk(); + const testMockCheckoutActions = mocks.mockCheckoutActions(); + const testMockSession = mocks.mockCheckoutSession(); + + mockSdk.loadActions.mockReturnValue(deferred.promise); + stripe.initCheckout.mockReturnValue(mockSdk); const {result} = renderHook(() => useCheckout(), { wrapper, @@ -238,11 +286,28 @@ describe('CheckoutProvider', () => { expect(result.current).toEqual({type: 'loading'}); expect(stripe.initCheckout).toHaveBeenCalledTimes(1); - await act(() => deferred.resolve(mockCheckoutSdk)); + await act(() => + deferred.resolve({ + type: 'success', + actions: testMockCheckoutActions, + }) + ); + + const {on: _on, loadActions: _loadActions, ...elementsMethods} = mockSdk; + const { + getSession: _getSession, + ...otherCheckoutActions + } = testMockCheckoutActions; + + const expectedCheckout = { + ...elementsMethods, + ...otherCheckoutActions, + ...testMockSession, + }; expect(result.current).toEqual({ type: 'success', - checkout: mockCheckout, + checkout: expectedCheckout, }); expect(stripe.initCheckout).toHaveBeenCalledTimes(1); }); @@ -251,7 +316,12 @@ describe('CheckoutProvider', () => { const stripe: any = mocks.mockStripe(); const stripeDeferred = makeDeferred(); const deferred = makeDeferred(); - stripe.initCheckout.mockReturnValue(deferred.promise); + const mockSdk = mocks.mockCheckoutSdk(); + const testMockCheckoutActions = mocks.mockCheckoutActions(); + const testMockSession = mocks.mockCheckoutSession(); + + mockSdk.loadActions.mockReturnValue(deferred.promise); + stripe.initCheckout.mockReturnValue(mockSdk); const {result, rerender} = renderHook(() => useCheckout(), { wrapper, @@ -261,21 +331,38 @@ describe('CheckoutProvider', () => { expect(result.current).toEqual({type: 'loading'}); expect(stripe.initCheckout).toHaveBeenCalledTimes(0); - rerender({stripe}); + rerender({stripe: stripeDeferred.promise as any}); expect(result.current).toEqual({type: 'loading'}); - expect(stripe.initCheckout).toHaveBeenCalledTimes(1); + expect(stripe.initCheckout).toHaveBeenCalledTimes(0); await act(() => stripeDeferred.resolve(stripe)); expect(result.current).toEqual({type: 'loading'}); expect(stripe.initCheckout).toHaveBeenCalledTimes(1); - await act(() => deferred.resolve(mockCheckoutSdk)); + await act(() => + deferred.resolve({ + type: 'success', + actions: testMockCheckoutActions, + }) + ); + + const {on: _on, loadActions: _loadActions, ...elementsMethods} = mockSdk; + const { + getSession: _getSession, + ...otherCheckoutActions + } = testMockCheckoutActions; + + const expectedCheckout = { + ...elementsMethods, + ...otherCheckoutActions, + ...testMockSession, + }; expect(result.current).toEqual({ type: 'success', - checkout: mockCheckout, + checkout: expectedCheckout, }); expect(stripe.initCheckout).toHaveBeenCalledTimes(1); }); @@ -303,7 +390,7 @@ describe('CheckoutProvider', () => { result = render( 'cs_123'}} + options={{clientSecret: fakeClientSecret}} /> ); }); @@ -313,7 +400,7 @@ describe('CheckoutProvider', () => { result.rerender( 'cs_123'}} + options={{clientSecret: fakeClientSecret}} /> ); }); @@ -329,12 +416,11 @@ describe('CheckoutProvider', () => { }); it('only calls initCheckout once and allows changes to elementsOptions appearance after setting the Stripe object', async () => { - const fetchClientSecret = async () => 'cs_123'; const result = render( { await waitFor(() => { expect(mockStripe.initCheckout).toHaveBeenCalledWith({ - fetchClientSecret, + clientSecret: fakeClientSecret, elementsOptions: { appearance: {theme: 'stripe'}, }, @@ -356,7 +442,7 @@ describe('CheckoutProvider', () => { 'cs_123', + clientSecret: fakeClientSecret, elementsOptions: {appearance: {theme: 'night'}}, }} /> @@ -374,13 +460,12 @@ describe('CheckoutProvider', () => { test('it does not call loadFonts a 2nd time if they do not change', async () => { let result: any; - const fetchClientSecret = async () => 'cs_123'; act(() => { result = render( { await waitFor(() => expect(mockStripe.initCheckout).toHaveBeenCalledWith({ - fetchClientSecret, + clientSecret: fakeClientSecret, elementsOptions: { fonts: [ { @@ -411,7 +496,7 @@ describe('CheckoutProvider', () => { 'cs_123', + clientSecret: fakeClientSecret, elementsOptions: { fonts: [ { @@ -429,7 +514,7 @@ describe('CheckoutProvider', () => { 'cs_123', + clientSecret: fakeClientSecret, elementsOptions: { fonts: [ { @@ -445,20 +530,19 @@ describe('CheckoutProvider', () => { await waitFor(() => { expect(mockStripe.initCheckout).toHaveBeenCalledTimes(1); - // This is called once, due to the sdk having loaded. - expect(mockCheckoutSdk.loadFonts).toHaveBeenCalledTimes(1); + // This is not called, due to the fonts not changing. + expect(mockCheckoutSdk.loadFonts).toHaveBeenCalledTimes(0); }); }); test('allows changes to elementsOptions fonts', async () => { let result: any; - const fetchClientSecret = async () => 'cs_123'; act(() => { result = render( @@ -467,7 +551,7 @@ describe('CheckoutProvider', () => { await waitFor(() => expect(mockStripe.initCheckout).toHaveBeenCalledWith({ - fetchClientSecret, + clientSecret: fakeClientSecret, elementsOptions: {}, }) ); @@ -477,7 +561,7 @@ describe('CheckoutProvider', () => { 'cs_123', + clientSecret: fakeClientSecret, elementsOptions: { fonts: [ { @@ -502,12 +586,11 @@ describe('CheckoutProvider', () => { }); it('allows options changes before setting the Stripe object', async () => { - const fetchClientSecret = async () => 'cs_123'; const result = render( { @@ -535,7 +618,7 @@ describe('CheckoutProvider', () => { expect(console.warn).not.toHaveBeenCalled(); expect(mockStripe.initCheckout).toHaveBeenCalledTimes(1); expect(mockStripe.initCheckout).toHaveBeenCalledWith({ - fetchClientSecret, + clientSecret: fakeClientSecret, elementsOptions: { appearance: {theme: 'stripe'}, }, @@ -546,7 +629,7 @@ describe('CheckoutProvider', () => { describe('React.StrictMode', () => { test('initCheckout once in StrictMode', async () => { const TestComponent = () => { - const _ = useCheckout(); + useCheckout(); return
; }; @@ -555,7 +638,7 @@ describe('CheckoutProvider', () => { 'cs_123'}} + options={{clientSecret: fakeClientSecret}} > @@ -570,7 +653,7 @@ describe('CheckoutProvider', () => { test('initCheckout once with stripePromise in StrictMode', async () => { const TestComponent = () => { - const _ = useCheckout(); + useCheckout(); return
; }; @@ -579,7 +662,7 @@ describe('CheckoutProvider', () => { 'cs_123'}} + options={{clientSecret: fakeClientSecret}} > @@ -594,14 +677,13 @@ describe('CheckoutProvider', () => { test('allows changes to options via (mockCheckoutSdk.changeAppearance after setting the Stripe object in StrictMode', async () => { let result: any; - const fetchClientSecret = async () => 'cs_123'; act(() => { result = render( { await waitFor(() => { expect(mockStripe.initCheckout).toHaveBeenCalledTimes(1); expect(mockStripe.initCheckout).toHaveBeenCalledWith({ - fetchClientSecret, + clientSecret: fakeClientSecret, elementsOptions: { appearance: {theme: 'stripe'}, }, @@ -627,7 +709,7 @@ describe('CheckoutProvider', () => { 'cs_123', + clientSecret: fakeClientSecret, elementsOptions: {appearance: {theme: 'night'}}, }} /> @@ -666,7 +748,7 @@ describe('CheckoutProvider', () => { 'cs_123'}} + options={{clientSecret: fakeClientSecret}} > {children} @@ -686,7 +768,7 @@ describe('CheckoutProvider', () => { const wrapper = ({children}: any) => ( 'cs_123'}} + options={{clientSecret: fakeClientSecret}} > {children} diff --git a/src/checkout/components/CheckoutProvider.tsx b/src/checkout/components/CheckoutProvider.tsx index d75c3c7..798b39c 100644 --- a/src/checkout/components/CheckoutProvider.tsx +++ b/src/checkout/components/CheckoutProvider.tsx @@ -14,20 +14,22 @@ import { } from '../../components/Elements'; import {registerWithStripeJs} from '../../utils/registerWithStripeJs'; -export type CheckoutValue = StripeCheckoutActions & - stripeJs.StripeCheckoutSession; - -export type CheckoutState = - | {type: 'loading'} +type State = + | { + type: 'loading'; + sdk: stripeJs.StripeCheckout | null; + } | { type: 'success'; - checkout: CheckoutValue; + sdk: stripeJs.StripeCheckout; + checkoutActions: stripeJs.LoadActionsSuccess; + session: stripeJs.StripeCheckoutSession; } | {type: 'error'; error: {message: string}}; type CheckoutContextValue = { stripe: stripeJs.Stripe | null; - checkoutState: CheckoutState; + checkoutState: State; }; const CheckoutContext = React.createContext(null); @@ -45,30 +47,6 @@ const validateCheckoutContext = ( return ctx; }; -type StripeCheckoutActions = Omit< - Omit, - 'on' ->; - -const getContextValue = ( - stripe: stripeJs.Stripe | null, - state: State -): CheckoutContextValue => { - if (state.type === 'success') { - const {sdk, session} = state; - const {on: _on, session: _session, ...actions} = sdk; - return { - stripe, - checkoutState: { - type: 'success', - checkout: Object.assign({}, session, actions), - }, - }; - } else { - return {stripe, checkoutState: state}; - } -}; - interface CheckoutProviderProps { /** * A [Stripe object](https://stripe.com/docs/js/initializing) or a `Promise` resolving to a `Stripe` object. @@ -89,17 +67,8 @@ interface PrivateCheckoutProviderProps { const INVALID_STRIPE_ERROR = 'Invalid prop `stripe` supplied to `CheckoutProvider`. We recommend using the `loadStripe` utility from `@stripe/stripe-js`. See https://stripe.com/docs/stripe-js/react#elements-props-stripe for details.'; -type State = - | {type: 'loading'} - | { - type: 'success'; - sdk: stripeJs.StripeCheckout; - session: stripeJs.StripeCheckoutSession; - } - | {type: 'error'; error: {message: string}}; - const maybeSdk = (state: State): stripeJs.StripeCheckout | null => { - if (state.type === 'success') { + if (state.type === 'success' || state.type === 'loading') { return state.sdk; } else { return null; @@ -118,7 +87,7 @@ export const CheckoutProvider: FunctionComponent({type: 'loading'}); + const [state, setState] = React.useState({type: 'loading', sdk: null}); const [stripe, setStripe] = React.useState(null); // Ref used to avoid calling initCheckout multiple times when options changes @@ -133,27 +102,42 @@ export const CheckoutProvider: FunctionComponent { - setState({type: 'success', sdk, session: sdk.session()}); - sdk.on('change', (session) => { - setState((prevState) => { - if (prevState.type === 'success') { - return { - type: 'success', - sdk: prevState.sdk, - session, - }; - } else { - return prevState; - } + const sdk = stripe.initCheckout(options); + setState({type: 'loading', sdk}); + + sdk + .loadActions() + .then((result) => { + if (result.type === 'success') { + const {actions} = result; + setState({ + type: 'success', + sdk, + checkoutActions: actions, + session: actions.getSession(), }); - }); - }, - (error) => { + + sdk.on('change', (session) => { + setState((prevState) => { + if (prevState.type === 'success') { + return { + type: 'success', + sdk: prevState.sdk, + checkoutActions: prevState.checkoutActions, + session, + }; + } else { + return prevState; + } + }); + }); + } else { + setState({type: 'error', error: result.error}); + } + }) + .catch((error) => { setState({type: 'error', error}); - } - ); + }); } }; @@ -191,15 +175,12 @@ export const CheckoutProvider: FunctionComponent { // Ignore changes while checkout sdk is not initialized. if (!sdk) { return; } - const hasSdkLoaded = Boolean(!prevCheckoutSdk && sdk); - // Handle appearance changes const previousAppearance = prevOptions?.elementsOptions?.appearance; const currentAppearance = options?.elementsOptions?.appearance; @@ -207,7 +188,7 @@ export const CheckoutProvider: FunctionComponent { registerWithStripeJs(stripe); }, [stripe]); - const contextValue = React.useMemo(() => getContextValue(stripe, state), [ - stripe, - state, - ]); + // Use useMemo to prevent unnecessary re-renders of child components + // when the context value object reference changes but the actual values haven't + const contextValue = React.useMemo( + () => ({ + stripe, + checkoutState: state, + }), + [stripe, state] + ); return ( @@ -241,10 +227,13 @@ export const CheckoutProvider: FunctionComponent; export const useElementsOrCheckoutContextWithUseCase = ( useCaseString: string @@ -265,8 +254,55 @@ export const useElementsOrCheckoutContextWithUseCase = ( } }; -export const useCheckout = (): CheckoutState => { +type StripeCheckoutActions = Omit< + stripeJs.StripeCheckout, + 'on' | 'loadActions' +> & + Omit; + +export type StripeCheckoutValue = StripeCheckoutActions & + stripeJs.StripeCheckoutSession; + +export type StripeUseCheckoutResult = + | {type: 'loading'} + | { + type: 'success'; + checkout: StripeCheckoutValue; + } + | {type: 'error'; error: {message: string}}; + +const mapStateToUseCheckoutResult = ( + checkoutState: State +): StripeUseCheckoutResult => { + if (checkoutState.type === 'success') { + const {sdk, session, checkoutActions} = checkoutState; + const {on: _on, loadActions: _loadActions, ...elementsMethods} = sdk; + const {getSession: _getSession, ...otherCheckoutActions} = checkoutActions; + const actions = { + ...elementsMethods, + ...otherCheckoutActions, + }; + return { + type: 'success', + checkout: { + ...session, + ...actions, + }, + }; + } else if (checkoutState.type === 'loading') { + return { + type: 'loading', + }; + } else { + return { + type: 'error', + error: checkoutState.error, + }; + } +}; + +export const useCheckout = (): StripeUseCheckoutResult => { const ctx = React.useContext(CheckoutContext); const {checkoutState} = validateCheckoutContext(ctx, 'calls useCheckout()'); - return checkoutState; + return mapStateToUseCheckoutResult(checkoutState); }; diff --git a/src/checkout/index.ts b/src/checkout/index.ts index ecc66e5..5739edf 100644 --- a/src/checkout/index.ts +++ b/src/checkout/index.ts @@ -1,7 +1,7 @@ export { useCheckout, CheckoutProvider, - CheckoutState, + StripeUseCheckoutResult, } from './components/CheckoutProvider'; export * from './types'; import React from 'react'; diff --git a/src/components/createElementComponent.test.tsx b/src/components/createElementComponent.test.tsx index 442adce..72fd576 100644 --- a/src/components/createElementComponent.test.tsx +++ b/src/components/createElementComponent.test.tsx @@ -36,7 +36,7 @@ describe('createElementComponent', () => { mockElement = mocks.mockElement(); mockStripe.elements.mockReturnValue(mockElements); mockElements.create.mockReturnValue(mockElement); - mockStripe.initCheckout.mockResolvedValue(mockCheckoutSdk); + mockStripe.initCheckout.mockReturnValue(mockCheckoutSdk); mockCheckoutSdk.createPaymentElement.mockReturnValue(mockElement); mockCheckoutSdk.createBillingAddressElement.mockReturnValue(mockElement); mockCheckoutSdk.createShippingAddressElement.mockReturnValue(mockElement); @@ -94,10 +94,7 @@ describe('createElementComponent', () => { it('does not render anything', () => { const {container} = render( - ''}} - > + ); @@ -109,11 +106,7 @@ describe('createElementComponent', () => { describe.each([ ['Elements', Elements, {clientSecret: 'pi_123'}], - [ - 'CheckoutProvider', - CheckoutProvider, - {fetchClientSecret: async () => 'cs_123'} as any, - ], + ['CheckoutProvider', CheckoutProvider, {clientSecret: 'cs_123'} as any], ])( 'on the server with Provider - %s', (_providerName, Provider, providerOptions) => { @@ -913,7 +906,7 @@ describe('createElementComponent', () => { result = render( 'cs_123'}} + options={{clientSecret: 'cs_123'}} > @@ -927,7 +920,7 @@ describe('createElementComponent', () => { rerender( 'cs_123'}} + options={{clientSecret: 'cs_123'}} > @@ -943,7 +936,7 @@ describe('createElementComponent', () => { result = render( 'cs_123'}} + options={{clientSecret: 'cs_123'}} > @@ -961,7 +954,7 @@ describe('createElementComponent', () => { result = render( 'cs_123'}} + options={{clientSecret: 'cs_123'}} > @@ -980,7 +973,7 @@ describe('createElementComponent', () => { result = render( 'cs_123'}} + options={{clientSecret: 'cs_123'}} > @@ -1028,7 +1021,7 @@ describe('createElementComponent', () => { 'cs_123'}} + options={{clientSecret: 'cs_123'}} > @@ -1047,7 +1040,7 @@ describe('createElementComponent', () => { result = render( 'cs_123'}} + options={{clientSecret: 'cs_123'}} > @@ -1067,10 +1060,7 @@ describe('createElementComponent', () => { it('does not create and mount until CheckoutSdk has been instantiated', async () => { act(() => { result = render( - 'cs_123'}} - > + ); @@ -1085,7 +1075,7 @@ describe('createElementComponent', () => { rerender( 'cs_123'}} + options={{clientSecret: 'cs_123'}} > @@ -1104,7 +1094,7 @@ describe('createElementComponent', () => { result = render( 'cs_123'}} + options={{clientSecret: 'cs_123'}} > @@ -1123,10 +1113,7 @@ describe('createElementComponent', () => { // This won't create the element, since checkoutSdk is undefined on this render act(() => { result = render( - 'cs_123'}} - > + ); @@ -1140,7 +1127,7 @@ describe('createElementComponent', () => { result.rerender( 'cs_123'}} + options={{clientSecret: 'cs_123'}} > @@ -1164,7 +1151,7 @@ describe('createElementComponent', () => { result = render( 'cs_123'}} + options={{clientSecret: 'cs_123'}} > @@ -1180,7 +1167,7 @@ describe('createElementComponent', () => { rerender( 'cs_123'}} + options={{clientSecret: 'cs_123'}} > @@ -1197,7 +1184,7 @@ describe('createElementComponent', () => { result = render( 'cs_123'}} + options={{clientSecret: 'cs_123'}} > @@ -1213,7 +1200,7 @@ describe('createElementComponent', () => { rerender( 'cs_123'}} + options={{clientSecret: 'cs_123'}} > @@ -1232,7 +1219,7 @@ describe('createElementComponent', () => { result = render( 'cs_123'}} + options={{clientSecret: 'cs_123'}} > @@ -1247,7 +1234,7 @@ describe('createElementComponent', () => { rerender( 'cs_123'}} + options={{clientSecret: 'cs_123'}} > @@ -1267,7 +1254,7 @@ describe('createElementComponent', () => { result = render( 'cs_123'}} + options={{clientSecret: 'cs_123'}} > @@ -1279,7 +1266,7 @@ describe('createElementComponent', () => { rerender( 'cs_123'}} + options={{clientSecret: 'cs_123'}} > @@ -1301,7 +1288,7 @@ describe('createElementComponent', () => { result = render( 'cs_123'}} + options={{clientSecret: 'cs_123'}} > @@ -1314,7 +1301,7 @@ describe('createElementComponent', () => { rerender( 'cs_123'}} + options={{clientSecret: 'cs_123'}} > @@ -1335,7 +1322,7 @@ describe('createElementComponent', () => { result = render( 'cs_123'}} + options={{clientSecret: 'cs_123'}} > @@ -1348,7 +1335,7 @@ describe('createElementComponent', () => { rerender( 'cs_123'}} + options={{clientSecret: 'cs_123'}} > @@ -1368,7 +1355,7 @@ describe('createElementComponent', () => { result = render( 'cs_123'}} + options={{clientSecret: 'cs_123'}} > @@ -1381,7 +1368,7 @@ describe('createElementComponent', () => { rerender( 'cs_123'}} + options={{clientSecret: 'cs_123'}} > @@ -1401,7 +1388,7 @@ describe('createElementComponent', () => { result = render( 'cs_123'}} + options={{clientSecret: 'cs_123'}} > @@ -1414,7 +1401,7 @@ describe('createElementComponent', () => { rerender( 'cs_123'}} + options={{clientSecret: 'cs_123'}} > @@ -1434,7 +1421,7 @@ describe('createElementComponent', () => { result = render( 'cs_123'}} + options={{clientSecret: 'cs_123'}} > @@ -1447,7 +1434,7 @@ describe('createElementComponent', () => { rerender( 'cs_123'}} + options={{clientSecret: 'cs_123'}} > @@ -1467,7 +1454,7 @@ describe('createElementComponent', () => { result = render( 'cs_123'}} + options={{clientSecret: 'cs_123'}} > @@ -1480,7 +1467,7 @@ describe('createElementComponent', () => { rerender( 'cs_123'}} + options={{clientSecret: 'cs_123'}} > @@ -1498,7 +1485,7 @@ describe('createElementComponent', () => { result = render( 'cs_123'}} + options={{clientSecret: 'cs_123'}} > @@ -1511,7 +1498,7 @@ describe('createElementComponent', () => { rerender( 'cs_123'}} + options={{clientSecret: 'cs_123'}} > @@ -1529,7 +1516,7 @@ describe('createElementComponent', () => { result = render( 'cs_123'}} + options={{clientSecret: 'cs_123'}} > @@ -1542,7 +1529,7 @@ describe('createElementComponent', () => { rerender( 'cs_123'}} + options={{clientSecret: 'cs_123'}} > @@ -1558,7 +1545,7 @@ describe('createElementComponent', () => { result = render( 'cs_123'}} + options={{clientSecret: 'cs_123'}} > {/* @ts-expect-error */} @@ -1572,7 +1559,7 @@ describe('createElementComponent', () => { rerender( 'cs_123'}} + options={{clientSecret: 'cs_123'}} > @@ -1588,10 +1575,7 @@ describe('createElementComponent', () => { it('destroys an existing Element when the component unmounts', async () => { act(() => { result = render( - 'cs_123'}} - > + ); @@ -1606,7 +1590,7 @@ describe('createElementComponent', () => { result = render( 'cs_123'}} + options={{clientSecret: 'cs_123'}} > @@ -1626,7 +1610,7 @@ describe('createElementComponent', () => { result = render( 'cs_123'}} + options={{clientSecret: 'cs_123'}} > @@ -1646,7 +1630,7 @@ describe('createElementComponent', () => { 'cs_123'}} + options={{clientSecret: 'cs_123'}} > @@ -1670,7 +1654,7 @@ describe('createElementComponent', () => { render( 'cs_123'}} + options={{clientSecret: 'cs_123'}} > @@ -1693,7 +1677,7 @@ describe('createElementComponent', () => { render( 'cs_123'}} + options={{clientSecret: 'cs_123'}} > {/* @ts-expect-error Testing invalid mode */} @@ -1715,7 +1699,7 @@ describe('createElementComponent', () => { render( 'cs_123'}} + options={{clientSecret: 'cs_123'}} > {/* @ts-expect-error Testing missing mode */} diff --git a/src/components/createElementComponent.tsx b/src/components/createElementComponent.tsx index 1acaf98..ba54936 100644 --- a/src/components/createElementComponent.tsx +++ b/src/components/createElementComponent.tsx @@ -72,7 +72,9 @@ const createElementComponent = ( const elements = 'elements' in ctx ? ctx.elements : null; const checkoutState = 'checkoutState' in ctx ? ctx.checkoutState : null; const checkoutSdk = - checkoutState?.type === 'success' ? checkoutState.checkout : null; + checkoutState?.type === 'success' || checkoutState?.type === 'loading' + ? checkoutState.sdk + : null; const [element, setElement] = React.useState( null ); diff --git a/test/mocks.js b/test/mocks.js index ff0bdb9..3cc1514 100644 --- a/test/mocks.js +++ b/test/mocks.js @@ -37,6 +37,21 @@ export const mockCheckoutSession = () => { }; }; +export const mockCheckoutActions = () => { + return { + getSession: jest.fn(() => mockCheckoutSession()), + applyPromotionCode: jest.fn(), + removePromotionCode: jest.fn(), + updateShippingAddress: jest.fn(), + updateBillingAddress: jest.fn(), + updatePhoneNumber: jest.fn(), + updateEmail: jest.fn(), + updateLineItemQuantity: jest.fn(), + updateShippingOption: jest.fn(), + confirm: jest.fn(), + }; +}; + export const mockCheckoutSdk = () => { const elements = {}; @@ -71,17 +86,17 @@ export const mockCheckoutSdk = () => { getExpressCheckoutElement: jest.fn(() => { return elements.expressCheckout || null; }), - session: jest.fn(() => mockCheckoutSession()), - applyPromotionCode: jest.fn(), - removePromotionCode: jest.fn(), - updateShippingAddress: jest.fn(), - updateBillingAddress: jest.fn(), - updatePhoneNumber: jest.fn(), - updateEmail: jest.fn(), - updateLineItemQuantity: jest.fn(), - updateShippingOption: jest.fn(), - confirm: jest.fn(), - on: jest.fn(), + + on: jest.fn((event, callback) => { + if (event === 'change') { + // Simulate initial session call + setTimeout(() => callback(mockCheckoutSession()), 0); + } + }), + loadActions: jest.fn().mockResolvedValue({ + type: 'success', + actions: mockCheckoutActions(), + }), }; }; @@ -93,6 +108,7 @@ export const mockEmbeddedCheckout = () => ({ export const mockStripe = () => { const checkoutSdk = mockCheckoutSdk(); + return { elements: jest.fn(() => mockElements()), createToken: jest.fn(), @@ -103,7 +119,7 @@ export const mockStripe = () => { paymentRequest: jest.fn(), registerAppInfo: jest.fn(), _registerWrapper: jest.fn(), - initCheckout: jest.fn().mockResolvedValue(checkoutSdk), + initCheckout: jest.fn(() => checkoutSdk), initEmbeddedCheckout: jest.fn(() => Promise.resolve(mockEmbeddedCheckout()) ), diff --git a/yarn.lock b/yarn.lock index 387fe99..c0a0fc5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2350,10 +2350,10 @@ regenerator-runtime "^0.13.7" resolve-from "^5.0.0" -"@stripe/stripe-js@^7.8.0": - version "7.8.0" - resolved "https://registry.yarnpkg.com/@stripe/stripe-js/-/stripe-js-7.8.0.tgz#01390d31030f04b64a7ff13adcccbafd615c7b77" - integrity sha512-DNXRfYUgkZlrniQORbA/wH8CdFRhiBSE0R56gYU0V5vvpJ9WZwvGrz9tBAZmfq2aTgw6SK7mNpmTizGzLWVezw== +"@stripe/stripe-js@^8.0.0-rc.1": + version "8.0.0-rc.1" + resolved "https://registry.yarnpkg.com/@stripe/stripe-js/-/stripe-js-8.0.0-rc.1.tgz#8c61f167b610bcfa92b65828b5fabbee133b70c8" + integrity sha512-lj6BFGL4gqx8iiIZ0znZTIQFguERi53lOKP3ZqIfc9E1ui7eJ4S6ZyWejcuuohuiQFPT3bpag3E9xZRS6qSujg== "@testing-library/dom@^8.5.0": version "8.13.0"