Skip to content

Commit 17d95c8

Browse files
authored
Add Financial Account for Platforms disclosure React component (stripe#630)
Add FinancialAccountDisclosure React component
1 parent e31c012 commit 17d95c8

File tree

2 files changed

+219
-0
lines changed

2 files changed

+219
-0
lines changed
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
import React from 'react';
2+
import {render} from '@testing-library/react';
3+
import FinancialAccountDisclosure from './FinancialAccountDisclosure';
4+
import {StripeErrorType} from '@stripe/stripe-js';
5+
import {mockStripe as baseMockStripe} from '../../test/mocks';
6+
7+
const apiError: StripeErrorType = 'api_error';
8+
9+
const mockSuccessfulStripeJsCall = () => {
10+
return {
11+
...baseMockStripe(),
12+
createFinancialAccountDisclosure: jest.fn(() =>
13+
Promise.resolve({
14+
htmlElement: document.createElement('div'),
15+
})
16+
),
17+
};
18+
};
19+
20+
const mockStripeJsWithError = () => {
21+
return {
22+
...baseMockStripe(),
23+
createFinancialAccountDisclosure: jest.fn(() =>
24+
Promise.resolve({
25+
error: {
26+
type: apiError,
27+
message: 'This is a test error',
28+
},
29+
})
30+
),
31+
};
32+
};
33+
34+
describe('FinancialAccountDisclosure', () => {
35+
let mockStripe: any;
36+
37+
beforeEach(() => {
38+
mockStripe = mockSuccessfulStripeJsCall();
39+
});
40+
41+
afterEach(() => {
42+
jest.restoreAllMocks();
43+
});
44+
45+
it('should render', () => {
46+
render(<FinancialAccountDisclosure stripe={mockStripe} />);
47+
});
48+
49+
it('should render with options', () => {
50+
const options = {
51+
businessName: 'Test Business',
52+
learnMoreLink: 'https://test.com',
53+
};
54+
render(
55+
<FinancialAccountDisclosure stripe={mockStripe} options={options} />
56+
);
57+
});
58+
59+
it('should render when there is an error', () => {
60+
mockStripe = mockStripeJsWithError();
61+
render(<FinancialAccountDisclosure stripe={mockStripe} />);
62+
});
63+
64+
it('should render with an onLoad callback', async () => {
65+
const onLoad = jest.fn();
66+
render(<FinancialAccountDisclosure stripe={mockStripe} onLoad={onLoad} />);
67+
await new Promise((resolve) => setTimeout(resolve, 0));
68+
expect(onLoad).toHaveBeenCalled();
69+
});
70+
71+
it('should not call onLoad if there is an error', async () => {
72+
const onLoad = jest.fn();
73+
mockStripe = mockStripeJsWithError();
74+
render(<FinancialAccountDisclosure stripe={mockStripe} onLoad={onLoad} />);
75+
await new Promise((resolve) => setTimeout(resolve, 0));
76+
expect(onLoad).not.toHaveBeenCalled();
77+
});
78+
79+
it('should render with an onError callback', async () => {
80+
const onError = jest.fn();
81+
mockStripe = mockStripeJsWithError();
82+
render(
83+
<FinancialAccountDisclosure stripe={mockStripe} onError={onError} />
84+
);
85+
await new Promise((resolve) => setTimeout(resolve, 0));
86+
expect(onError).toHaveBeenCalled();
87+
});
88+
89+
it('should not call onError if there is no error', async () => {
90+
const onError = jest.fn();
91+
render(
92+
<FinancialAccountDisclosure stripe={mockStripe} onError={onError} />
93+
);
94+
await new Promise((resolve) => setTimeout(resolve, 0));
95+
expect(onError).not.toHaveBeenCalled();
96+
});
97+
});
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
import * as stripeJs from '@stripe/stripe-js';
2+
import React from 'react';
3+
import {parseStripeProp} from '../utils/parseStripeProp';
4+
import {registerWithStripeJs} from '../utils/registerWithStripeJs';
5+
import {StripeError} from '@stripe/stripe-js';
6+
import {usePrevious} from '../utils/usePrevious';
7+
8+
interface FinancialAccountDisclosureProps {
9+
/**
10+
* A [Stripe object](https://stripe.com/docs/js/initializing) or a `Promise` resolving to a `Stripe` object.
11+
* The easiest way to initialize a `Stripe` object is with the the [Stripe.js wrapper module](https://github.com/stripe/stripe-js/blob/master/README.md#readme).
12+
* Once this prop has been set, it can not be changed.
13+
*
14+
* You can also pass in `null` or a `Promise` resolving to `null` if you are performing an initial server-side render or when generating a static site.
15+
*/
16+
17+
stripe: PromiseLike<stripeJs.Stripe | null> | stripeJs.Stripe | null;
18+
19+
/**
20+
* Callback function called when the disclosure content is loading.
21+
*/
22+
onLoad?: () => void;
23+
24+
/**
25+
* Callback function called when an error occurs during disclosure creation.
26+
*/
27+
onError?: (error: StripeError) => void;
28+
29+
/**
30+
* Optional Financial Account Disclosure configuration options.
31+
*
32+
* businessName: The name of your business as you would like it to appear in the disclosure. If not provided, the business name will be inferred from the Stripe account.
33+
* learnMoreLink: A supplemental link to for your users to learn more about Financial Accounts for platforms or any other relevant information included in the disclosure.
34+
*/
35+
options?: {
36+
businessName?: string;
37+
learnMoreLink?: string;
38+
};
39+
}
40+
41+
const FinancialAccountDisclosure = ({
42+
stripe: rawStripeProp,
43+
onLoad,
44+
onError,
45+
options,
46+
}: FinancialAccountDisclosureProps) => {
47+
const businessName = options?.businessName;
48+
const learnMoreLink = options?.learnMoreLink;
49+
const containerRef = React.useRef<HTMLDivElement>(null);
50+
const parsed = React.useMemo(() => parseStripeProp(rawStripeProp), [
51+
rawStripeProp,
52+
]);
53+
const [stripeState, setStripeState] = React.useState<stripeJs.Stripe | null>(
54+
parsed.tag === 'sync' ? parsed.stripe : null
55+
);
56+
57+
React.useEffect(() => {
58+
let isMounted = true;
59+
60+
if (parsed.tag === 'async') {
61+
parsed.stripePromise.then((stripePromise: stripeJs.Stripe | null) => {
62+
if (stripePromise && isMounted) {
63+
setStripeState(stripePromise);
64+
}
65+
});
66+
} else if (parsed.tag === 'sync') {
67+
setStripeState(parsed.stripe);
68+
}
69+
70+
return () => {
71+
isMounted = false;
72+
};
73+
}, [parsed]);
74+
75+
// Warn on changes to stripe prop
76+
const prevStripe = usePrevious(rawStripeProp);
77+
React.useEffect(() => {
78+
if (prevStripe !== null && prevStripe !== rawStripeProp) {
79+
console.warn(
80+
'Unsupported prop change on FinancialAccountDisclosure: You cannot change the `stripe` prop after setting it.'
81+
);
82+
}
83+
}, [prevStripe, rawStripeProp]);
84+
85+
// Attach react-stripe-js version to stripe.js instance
86+
React.useEffect(() => {
87+
registerWithStripeJs(stripeState);
88+
}, [stripeState]);
89+
90+
React.useEffect(() => {
91+
const createDisclosure = async () => {
92+
if (!stripeState || !containerRef.current) {
93+
return;
94+
}
95+
96+
const {
97+
htmlElement: disclosureContent,
98+
error,
99+
} = await (stripeState as any).createFinancialAccountDisclosure({
100+
businessName,
101+
learnMoreLink,
102+
});
103+
104+
if (error && onError) {
105+
onError(error);
106+
} else if (disclosureContent) {
107+
const container = containerRef.current;
108+
container.innerHTML = '';
109+
container.appendChild(disclosureContent);
110+
if (onLoad) {
111+
onLoad();
112+
}
113+
}
114+
};
115+
116+
createDisclosure();
117+
}, [stripeState, businessName, learnMoreLink, onLoad, onError]);
118+
119+
return React.createElement('div', {ref: containerRef});
120+
};
121+
122+
export default FinancialAccountDisclosure;

0 commit comments

Comments
 (0)