diff --git a/src/components/EmbeddedCheckout.client.test.tsx b/src/components/EmbeddedCheckout.client.test.tsx index a6749bd..60faafb 100644 --- a/src/components/EmbeddedCheckout.client.test.tsx +++ b/src/components/EmbeddedCheckout.client.test.tsx @@ -13,7 +13,8 @@ describe('EmbeddedCheckout on the client', () => { let mockEmbeddedCheckout: any; let mockEmbeddedCheckoutPromise: any; const fakeClientSecret = 'cs_123_secret_abc'; - const fakeOptions = {clientSecret: fakeClientSecret}; + const fetchClientSecret = () => Promise.resolve(fakeClientSecret); + const fakeOptions = {fetchClientSecret}; beforeEach(() => { mockStripe = mocks.mockStripe(); @@ -73,10 +74,13 @@ describe('EmbeddedCheckout on the client', () => { expect(mockEmbeddedCheckout.mount).toBeCalledWith(container.firstChild); }); - it('does not mount until Embedded Checkouts has been initialized', async () => { + it('does not mount until Embedded Checkout has been initialized', async () => { // Render with no stripe instance and client secret const {container, rerender} = render( - + ); @@ -86,18 +90,18 @@ describe('EmbeddedCheckout on the client', () => { rerender( ); expect(mockEmbeddedCheckout.mount).not.toBeCalled(); - // Set client secret + // Set fetchClientSecret rerender( @@ -154,4 +158,19 @@ describe('EmbeddedCheckout on the client', () => { ); }).not.toThrow(); }); + + it('still works with clientSecret param (deprecated)', async () => { + const {container} = render( + + + + ); + + await act(() => mockEmbeddedCheckoutPromise); + + expect(mockEmbeddedCheckout.mount).toBeCalledWith(container.firstChild); + }); }); diff --git a/src/components/EmbeddedCheckoutProvider.test.tsx b/src/components/EmbeddedCheckoutProvider.test.tsx index 38eaccb..ff1c731 100644 --- a/src/components/EmbeddedCheckoutProvider.test.tsx +++ b/src/components/EmbeddedCheckoutProvider.test.tsx @@ -14,7 +14,8 @@ describe('EmbeddedCheckoutProvider', () => { let mockEmbeddedCheckout: any; let mockEmbeddedCheckoutPromise: any; const fakeClientSecret = 'cs_123_secret_abc'; - const fakeOptions = {clientSecret: fakeClientSecret}; + const fetchClientSecret = () => Promise.resolve(fakeClientSecret); + const fakeOptions = {fetchClientSecret}; let consoleWarn: any; let consoleError: any; @@ -87,26 +88,6 @@ describe('EmbeddedCheckoutProvider', () => { expect(result.current.embeddedCheckout).toBe(mockEmbeddedCheckout); }); - it('allows a transition from null to a valid client secret', async () => { - let optionsProp: any = {clientSecret: null}; - const wrapper = ({children}: {children?: React.ReactNode}) => ( - - {children} - - ); - - const {result, rerender} = renderHook(() => useEmbeddedCheckoutContext(), { - wrapper, - }); - expect(result.current.embeddedCheckout).toBe(null); - - optionsProp = {clientSecret: fakeClientSecret}; - rerender(); - - await act(() => mockEmbeddedCheckoutPromise); - expect(result.current.embeddedCheckout).toBe(mockEmbeddedCheckout); - }); - it('works with a Promise resolving to a valid Stripe object', async () => { const wrapper = ({children}: {children?: React.ReactNode}) => ( { ); }); - it('does not allow changes to clientSecret option', async () => { - const optionsProp1 = {clientSecret: 'cs_123_secret_abc'}; - const optionsProp2 = {clientSecret: 'cs_abc_secret_123'}; + describe('clientSecret param (deprecated)', () => { + it('allows a transition from null to a valid client secret', async () => { + let optionsProp: any = {clientSecret: null}; + const wrapper = ({children}: {children?: React.ReactNode}) => ( + + {children} + + ); + + const {result, rerender} = renderHook( + () => useEmbeddedCheckoutContext(), + { + wrapper, + } + ); + expect(result.current.embeddedCheckout).toBe(null); + + optionsProp = {clientSecret: fakeClientSecret}; + rerender(); + + await act(() => mockEmbeddedCheckoutPromise); + expect(result.current.embeddedCheckout).toBe(mockEmbeddedCheckout); + }); + + it('does not allow changes to clientSecret option', async () => { + const optionsProp1 = {clientSecret: 'cs_123_secret_abc'}; + const optionsProp2 = {clientSecret: 'cs_abc_secret_123'}; + + // Silence console output so test output is less noisy + consoleWarn.mockImplementation(() => {}); + + const {rerender} = render( + + ); + await act(() => mockEmbeddedCheckoutPromise); + + rerender( + + ); + + expect(consoleWarn).toHaveBeenCalledWith( + 'Unsupported prop change on EmbeddedCheckoutProvider: You cannot change the client secret after setting it. Unmount and create a new instance of EmbeddedCheckoutProvider instead.' + ); + }); + }); + + describe('fetchClientSecret param', () => { + it('allows a transition from null to a valid fetchClientSecret', async () => { + let optionsProp: any = {fetchClientSecret: null}; + const wrapper = ({children}: {children?: React.ReactNode}) => ( + + {children} + + ); + + const {result, rerender} = renderHook( + () => useEmbeddedCheckoutContext(), + { + wrapper, + } + ); + expect(result.current.embeddedCheckout).toBe(null); + + optionsProp = {fetchClientSecret}; + rerender(); + + await act(() => mockEmbeddedCheckoutPromise); + expect(result.current.embeddedCheckout).toBe(mockEmbeddedCheckout); + }); + + it('does not allow changes to fetchClientSecret option', async () => { + const optionsProp1 = {fetchClientSecret}; + const optionsProp2 = { + fetchClientSecret: () => Promise.resolve('cs_abc_secret_123'), + }; + + // Silence console output so test output is less noisy + consoleWarn.mockImplementation(() => {}); + + const {rerender} = render( + + ); + await act(() => mockEmbeddedCheckoutPromise); + + rerender( + + ); + + expect(consoleWarn).toHaveBeenCalledWith( + 'Unsupported prop change on EmbeddedCheckoutProvider: You cannot change fetchClientSecret after setting it. Unmount and create a new instance of EmbeddedCheckoutProvider instead.' + ); + }); + }); + it('errors if both clientSecret and fetchClientSecret are undefined', async () => { // Silence console output so test output is less noisy consoleWarn.mockImplementation(() => {}); - const {rerender} = render( - - ); - await act(() => mockEmbeddedCheckoutPromise); - - rerender( + render( ); expect(consoleWarn).toHaveBeenCalledWith( - 'Unsupported prop change on EmbeddedCheckoutProvider: You cannot change the client secret after setting it. Unmount and create a new instance of EmbeddedCheckoutProvider instead.' + 'Invalid props passed to EmbeddedCheckoutProvider: You must provide one of either `options.fetchClientSecret` or `options.clientSecret`.' ); }); it('does not allow changes to onComplete option', async () => { const optionsProp1 = { - clientSecret: 'cs_123_secret_abc', + fetchClientSecret, onComplete: () => 'foo', }; const optionsProp2 = { - clientSecret: 'cs_123_secret_abc', + fetchClientSecret, onComplete: () => 'bar', }; // Silence console output so test output is less noisy diff --git a/src/components/EmbeddedCheckoutProvider.tsx b/src/components/EmbeddedCheckoutProvider.tsx index dbdc1f9..98b3efb 100644 --- a/src/components/EmbeddedCheckoutProvider.tsx +++ b/src/components/EmbeddedCheckoutProvider.tsx @@ -46,11 +46,13 @@ interface EmbeddedCheckoutProviderProps { stripe: PromiseLike | stripeJs.Stripe | null; /** * Embedded Checkout configuration options. - * You can initially pass in `null` as `options.clientSecret` if you are - * performing an initial server-side render or when generating a static site. + * You can initially pass in `null` to `options.clientSecret` or + * `options.fetchClientSecret` if you are performing an initial server-side + * render or when generating a static site. */ options: { - clientSecret: string | null; + clientSecret?: string | null; + fetchClientSecret?: (() => Promise) | null; onComplete?: () => void; }; } @@ -102,7 +104,7 @@ export const EmbeddedCheckoutProvider: FunctionComponent { if (stripe) { @@ -112,7 +114,7 @@ export const EmbeddedCheckoutProvider: FunctionComponent populated setStripeAndInitEmbeddedCheckout(parsed.stripe); @@ -171,6 +173,15 @@ export const EmbeddedCheckoutProvider: FunctionComponent