Skip to content

Commit

Permalink
Merge branch 'master' into danwang/bump-stripe-js
Browse files Browse the repository at this point in the history
  • Loading branch information
danwang-stripe committed Mar 8, 2024
2 parents 305d099 + 04982fc commit 776705b
Show file tree
Hide file tree
Showing 4 changed files with 165 additions and 50 deletions.
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,18 +54,18 @@
}
},
"dependencies": {
"@rollup/plugin-babel": "^6.0.4",
"@rollup/plugin-terser": "^0.4.4",
"prop-types": "^15.7.2"
},
"devDependencies": {
"@babel/cli": "^7.7.0",
"@babel/core": "^7.7.2",
"@babel/preset-env": "^7.7.1",
"@babel/preset-react": "^7.7.0",
"@rollup/plugin-babel": "^6.0.4",
"@rollup/plugin-commonjs": "^25.0.7",
"@rollup/plugin-node-resolve": "^15.2.3",
"@rollup/plugin-replace": "^5.0.5",
"@rollup/plugin-terser": "^0.4.4",
"@storybook/react": "^6.5.0-beta.8",
"@stripe/stripe-js": "^3.0.7",
"@testing-library/jest-dom": "^5.16.4",
Expand Down
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 &&
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

0 comments on commit 776705b

Please sign in to comment.