Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for fetchClientSecret param to Embedded Checkout #481

Merged
merged 2 commits into from
Mar 8, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
31 changes: 25 additions & 6 deletions src/components/EmbeddedCheckout.client.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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(
<EmbeddedCheckoutProvider stripe={null} options={{clientSecret: null}}>
<EmbeddedCheckoutProvider
stripe={null}
options={{fetchClientSecret: null}}
>
<EmbeddedCheckout />
</EmbeddedCheckoutProvider>
);
Expand All @@ -86,18 +90,18 @@ describe('EmbeddedCheckout on the client', () => {
rerender(
<EmbeddedCheckoutProvider
stripe={mockStripe}
options={{clientSecret: null}}
options={{fetchClientSecret: null}}
>
<EmbeddedCheckout />
</EmbeddedCheckoutProvider>
);
expect(mockEmbeddedCheckout.mount).not.toBeCalled();

// Set client secret
// Set fetchClientSecret
rerender(
<EmbeddedCheckoutProvider
stripe={mockStripe}
options={{clientSecret: fakeClientSecret}}
options={{fetchClientSecret}}
>
<EmbeddedCheckout />
</EmbeddedCheckoutProvider>
Expand Down Expand Up @@ -154,4 +158,19 @@ describe('EmbeddedCheckout on the client', () => {
);
}).not.toThrow();
});

it('still works with clientSecret param (deprecated)', async () => {
const {container} = render(
<EmbeddedCheckoutProvider
stripe={mockStripe}
options={{clientSecret: 'cs_123_456'}}
>
<EmbeddedCheckout />
</EmbeddedCheckoutProvider>
);

await act(() => mockEmbeddedCheckoutPromise);

expect(mockEmbeddedCheckout.mount).toBeCalledWith(container.firstChild);
});
});
150 changes: 113 additions & 37 deletions src/components/EmbeddedCheckoutProvider.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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}) => (
<EmbeddedCheckoutProvider stripe={mockStripe} options={optionsProp}>
{children}
</EmbeddedCheckoutProvider>
);

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}) => (
<EmbeddedCheckoutProvider
Expand Down Expand Up @@ -247,40 +228,135 @@ describe('EmbeddedCheckoutProvider', () => {
);
});

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}) => (
<EmbeddedCheckoutProvider stripe={mockStripe} options={optionsProp}>
{children}
</EmbeddedCheckoutProvider>
);

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(
<EmbeddedCheckoutProvider
stripe={mockStripe}
options={optionsProp1}
></EmbeddedCheckoutProvider>
);
await act(() => mockEmbeddedCheckoutPromise);

rerender(
<EmbeddedCheckoutProvider
stripe={mockStripe}
options={optionsProp2}
></EmbeddedCheckoutProvider>
);

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}) => (
<EmbeddedCheckoutProvider stripe={mockStripe} options={optionsProp}>
{children}
</EmbeddedCheckoutProvider>
);

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(
<EmbeddedCheckoutProvider
stripe={mockStripe}
options={optionsProp1}
></EmbeddedCheckoutProvider>
);
await act(() => mockEmbeddedCheckoutPromise);

rerender(
<EmbeddedCheckoutProvider
stripe={mockStripe}
options={optionsProp2}
></EmbeddedCheckoutProvider>
);

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(
<EmbeddedCheckoutProvider
stripe={mockStripe}
options={optionsProp1}
></EmbeddedCheckoutProvider>
);
await act(() => mockEmbeddedCheckoutPromise);

rerender(
render(
<EmbeddedCheckoutProvider
stripe={mockStripe}
options={optionsProp2}
options={{}}
></EmbeddedCheckoutProvider>
);

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
Expand Down
30 changes: 25 additions & 5 deletions src/components/EmbeddedCheckoutProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,11 +46,13 @@ interface EmbeddedCheckoutProviderProps {
stripe: PromiseLike<stripeJs.Stripe | null> | 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<string>) | null;
onComplete?: () => void;
};
}
Expand Down Expand Up @@ -102,7 +104,7 @@ export const EmbeddedCheckoutProvider: FunctionComponent<PropsWithChildren<
if (
parsed.tag === 'async' &&
!loadedStripe.current &&
options.clientSecret
(options.clientSecret || options.fetchClientSecret)
) {
parsed.stripePromise.then((stripe) => {
if (stripe) {
Expand All @@ -112,7 +114,7 @@ export const EmbeddedCheckoutProvider: FunctionComponent<PropsWithChildren<
} else if (
parsed.tag === 'sync' &&
!loadedStripe.current &&
options.clientSecret
(options.clientSecret || options.fetchClientSecret)
) {
// Or, handle a sync stripe instance going from null -> populated
setStripeAndInitEmbeddedCheckout(parsed.stripe);
Expand Down Expand Up @@ -171,6 +173,15 @@ export const EmbeddedCheckoutProvider: FunctionComponent<PropsWithChildren<
return;
}

if (
options.clientSecret === undefined ||
tiff-stripe marked this conversation as resolved.
Show resolved Hide resolved
options.fetchClientSecret === undefined
) {
console.warn(
'Invalid props passed to EmbeddedCheckoutProvider: You must provide one of either `options.fetchClientSecret` or `options.clientSecret`.'
);
}

if (
prevOptions.clientSecret != null &&
options.clientSecret !== prevOptions.clientSecret
Expand All @@ -180,6 +191,15 @@ export const EmbeddedCheckoutProvider: FunctionComponent<PropsWithChildren<
);
}

if (
prevOptions.fetchClientSecret != null &&
options.fetchClientSecret !== prevOptions.fetchClientSecret
) {
console.warn(
'Unsupported prop change on EmbeddedCheckoutProvider: You cannot change fetchClientSecret after setting it. Unmount and create a new instance of EmbeddedCheckoutProvider instead.'
);
}

if (
prevOptions.onComplete != null &&
options.onComplete !== prevOptions.onComplete
Expand Down
Loading