Skip to content

Commit 08d0b83

Browse files
Add setLoadParameters API (stripe#53)
1 parent 8f6aa5b commit 08d0b83

File tree

8 files changed

+343
-41
lines changed

8 files changed

+343
-41
lines changed

README.md

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -63,9 +63,9 @@ declarations. These changes will not affect Stripe.js itself.
6363
## Ensuring Stripe.js is available everywhere
6464

6565
To best leverage Stripe’s advanced fraud functionality, ensure that Stripe.js is
66-
loaded on every page, not just your checkout page. This allows Stripe to detect
67-
anomalous behavior that may be indicative of fraud as customers browse your
68-
website.
66+
loaded on every page, not just your checkout page. This
67+
[allows Stripe to detect suspicious behavior](/docs/disputes/prevention/advanced-fraud-detection)
68+
that may be indicative of fraud as customers browse your website.
6969

7070
By default, this module will insert a `<script>` tag that loads Stripe.js from
7171
`https://js.stripe.com`. This happens as a side effect immediately upon
@@ -105,7 +105,24 @@ Stripe.js script until `loadStripe` is first called, use the alternative
105105
import {loadStripe} from '@stripe/stripe-js/pure';
106106
107107
// Stripe.js will not be loaded until `loadStripe` is called
108+
const stripe = await loadStripe('pk_test_TYooMQauvdEDq54NiTphI7jx');
109+
```
110+
111+
### Disabling advanced fraud detection signals
112+
113+
If you would like to
114+
[disable advanced fraud detection](https://stripe.com/docs/disputes/prevention/advanced-fraud-detection#disabling-advanced-fraud-detection)
115+
altogether, use `loadStripe.setLoadParameters`:
116+
108117
```
118+
import {loadStripe} from '@stripe/stripe-js/pure';
119+
120+
loadStripe.setLoadParameters({advancedFraudSignals: false})
121+
const stripe = await loadStripe('pk_test_TYooMQauvdEDq54NiTphI7jx');
122+
```
123+
124+
The `loadStripe.setLoadParameters` function is only available when importing
125+
`loadStripe` from `@stripe/stripe-js/pure`.
109126

110127
## Stripe.js Documentation
111128

pure.d.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
11
///<reference path='./types/index.d.ts' />
22

3-
export const loadStripe: typeof import('@stripe/stripe-js').loadStripe;
3+
export const loadStripe: typeof import('@stripe/stripe-js').loadStripe & {
4+
setLoadParameters: (params: {advancedFraudSignals: boolean}) => void;
5+
};

src/index.test.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,30 @@ describe('Stripe module loader', () => {
8282
).toHaveLength(1);
8383
});
8484
});
85+
86+
test('ignores non-Stripe.js scripts that start with the v3 url', async () => {
87+
const script = document.createElement('script');
88+
script.src = 'https://js.stripe.com/v3/futureBundle.js';
89+
document.body.appendChild(script);
90+
91+
require('./index');
92+
93+
await Promise.resolve();
94+
95+
expect(
96+
document.querySelectorAll('script[src^="https://js.stripe.com/v3"]')
97+
).toHaveLength(2);
98+
99+
expect(
100+
document.querySelector(
101+
'script[src="https://js.stripe.com/v3/futureBundle.js"]'
102+
)
103+
).not.toBe(null);
104+
105+
expect(
106+
document.querySelector('script[src="https://js.stripe.com/v3"]')
107+
).not.toBe(null);
108+
});
85109
});
86110

87111
describe.each(['./index', './pure'])('loadStripe (%s.ts)', (requirePath) => {

src/index.ts

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,18 @@
1-
///<reference path='../types/index.d.ts' />
2-
import {Stripe as StripeInstance, StripeConstructor} from '@stripe/stripe-js';
3-
import {loadScript, initStripe} from './shared';
1+
import {loadScript, initStripe, LoadStripe} from './shared';
42

53
// Execute our own script injection after a tick to give users time to do their
64
// own script injection.
7-
const stripePromise = Promise.resolve().then(loadScript);
5+
const stripePromise = Promise.resolve().then(() => loadScript(null));
86

97
let loadCalled = false;
108

11-
stripePromise.catch((err) => {
9+
stripePromise.catch((err: Error) => {
1210
if (!loadCalled) {
1311
console.warn(err);
1412
}
1513
});
1614

17-
export const loadStripe = (
18-
...args: Parameters<StripeConstructor>
19-
): Promise<StripeInstance | null> => {
15+
export const loadStripe: LoadStripe = (...args) => {
2016
loadCalled = true;
2117

2218
return stripePromise.then((maybeStripe) => initStripe(maybeStripe, args));

src/pure.test.ts

Lines changed: 109 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,25 @@
11
/* eslint-disable @typescript-eslint/no-var-requires */
22

3-
const SCRIPT_SELECTOR =
4-
'script[src="https://js.stripe.com/v3"], script[src="https://js.stripe.com/v3/"]';
3+
const SCRIPT_SELECTOR = 'script[src^="https://js.stripe.com/v3"]';
54

65
describe('pure module', () => {
6+
beforeEach(() => {
7+
jest.spyOn(console, 'warn').mockReturnValue();
8+
});
9+
710
afterEach(() => {
8-
const script = document.querySelector(SCRIPT_SELECTOR);
9-
if (script && script.parentElement) {
10-
script.parentElement.removeChild(script);
11+
const scripts = Array.from(document.querySelectorAll(SCRIPT_SELECTOR));
12+
13+
for (const script of scripts) {
14+
if (script.parentElement) {
15+
script.parentElement.removeChild(script);
16+
}
1117
}
1218

1319
delete window.Stripe;
20+
1421
jest.resetModules();
22+
jest.restoreAllMocks();
1523
});
1624

1725
test('does not inject the script if loadStripe is not called', async () => {
@@ -26,4 +34,100 @@ describe('pure module', () => {
2634

2735
expect(document.querySelector(SCRIPT_SELECTOR)).not.toBe(null);
2836
});
37+
38+
test('it can load the script with advanced fraud signals disabled', () => {
39+
const {loadStripe} = require('./pure');
40+
41+
loadStripe.setLoadParameters({advancedFraudSignals: false});
42+
loadStripe('pk_test_foo');
43+
44+
expect(
45+
document.querySelector(
46+
'script[src^="https://js.stripe.com/v3?advancedFraudSignals=false"]'
47+
)
48+
).not.toBe(null);
49+
});
50+
51+
test('it should throw when setting invalid load parameters', () => {
52+
const {loadStripe} = require('./pure');
53+
54+
expect(() => {
55+
loadStripe.setLoadParameters({howdy: true});
56+
}).toThrow('invalid load parameters');
57+
});
58+
59+
test('it should warn when calling loadStripe if a script already exists when parameters are set', () => {
60+
const script = document.createElement('script');
61+
script.src = 'https://js.stripe.com/v3';
62+
document.body.appendChild(script);
63+
64+
const {loadStripe} = require('./pure');
65+
loadStripe.setLoadParameters({advancedFraudSignals: true});
66+
loadStripe('pk_test_123');
67+
68+
expect(console.warn).toHaveBeenCalledTimes(1);
69+
expect(console.warn).toHaveBeenLastCalledWith(
70+
'loadStripe.setLoadParameters was called but an existing Stripe.js script already exists in the document; existing script parameters will be used'
71+
);
72+
});
73+
74+
test('it should warn when calling loadStripe if a script is added after parameters are set', () => {
75+
const {loadStripe} = require('./pure');
76+
loadStripe.setLoadParameters({advancedFraudSignals: true});
77+
78+
const script = document.createElement('script');
79+
script.src = 'https://js.stripe.com/v3';
80+
document.body.appendChild(script);
81+
82+
loadStripe('pk_test_123');
83+
84+
expect(console.warn).toHaveBeenCalledTimes(1);
85+
expect(console.warn).toHaveBeenLastCalledWith(
86+
'loadStripe.setLoadParameters was called but an existing Stripe.js script already exists in the document; existing script parameters will be used'
87+
);
88+
});
89+
90+
test('it should warn when window.Stripe already exists if parameters are set', () => {
91+
window.Stripe = jest.fn((key) => ({key})) as any;
92+
93+
const {loadStripe} = require('./pure');
94+
loadStripe.setLoadParameters({advancedFraudSignals: true});
95+
loadStripe('pk_test_123');
96+
97+
expect(console.warn).toHaveBeenCalledTimes(1);
98+
expect(console.warn).toHaveBeenLastCalledWith(
99+
'loadStripe.setLoadParameters was called but an existing Stripe.js script already exists in the document; existing script parameters will be used'
100+
);
101+
});
102+
103+
test('it should not warn when a script already exists if parameters are not set', () => {
104+
const script = document.createElement('script');
105+
script.src = 'https://js.stripe.com/v3';
106+
document.body.appendChild(script);
107+
108+
const {loadStripe} = require('./pure');
109+
loadStripe('pk_test_123');
110+
111+
expect(console.warn).toHaveBeenCalledTimes(0);
112+
});
113+
114+
test('it should not warn when window.Stripe already exists if parameters are not set', () => {
115+
window.Stripe = jest.fn((key) => ({key})) as any;
116+
117+
const {loadStripe} = require('./pure');
118+
loadStripe('pk_test_123');
119+
120+
expect(console.warn).toHaveBeenCalledTimes(0);
121+
});
122+
123+
test('throws an error if calling setLoadParameters after loadStripe', () => {
124+
const {loadStripe} = require('./pure');
125+
126+
loadStripe.setLoadParameters({advancedFraudSignals: false});
127+
loadStripe('pk_foo');
128+
129+
expect(() => {
130+
loadStripe.setLoadParameters({advancedFraudSignals: false});
131+
}).toThrow('cannot change load parameters');
132+
});
29133
});

src/pure.ts

Lines changed: 32 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,32 @@
1-
///<reference path='../types/index.d.ts' />
2-
import {Stripe as StripeInstance, StripeConstructor} from '@stripe/stripe-js';
3-
import {loadScript, initStripe} from './shared';
4-
5-
export const loadStripe = (
6-
...args: Parameters<StripeConstructor>
7-
): Promise<StripeInstance | null> =>
8-
loadScript().then((maybeStripe) => initStripe(maybeStripe, args));
1+
import {
2+
validateLoadParams,
3+
loadScript,
4+
initStripe,
5+
LoadStripe,
6+
LoadParams,
7+
} from './shared';
8+
9+
type SetLoadParams = (params: LoadParams) => void;
10+
11+
let loadParams: null | LoadParams;
12+
let loadStripeCalled = false;
13+
14+
export const loadStripe: LoadStripe & {setLoadParameters: SetLoadParams} = (
15+
...args
16+
) => {
17+
loadStripeCalled = true;
18+
19+
return loadScript(loadParams).then((maybeStripe) =>
20+
initStripe(maybeStripe, args)
21+
);
22+
};
23+
24+
loadStripe.setLoadParameters = (params): void => {
25+
if (loadStripeCalled) {
26+
throw new Error(
27+
'You cannot change load parameters after calling loadStripe'
28+
);
29+
}
30+
31+
loadParams = validateLoadParams(params);
32+
};

src/shared.test.ts

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import {validateLoadParams, findScript} from './shared';
2+
3+
describe('validateLoadParams', () => {
4+
const INVALID_INPUTS: any[] = [
5+
[undefined],
6+
[false],
7+
[null],
8+
[true],
9+
[{}],
10+
[8],
11+
[{advancedFraud: true}],
12+
[{advancedFraudSignals: true, someOtherKey: true}],
13+
[{advancedFraudSignals: 'true'}],
14+
];
15+
16+
test.each(INVALID_INPUTS)('throws on invalid input: %p', (input) => {
17+
expect(() => validateLoadParams(input)).toThrow('invalid load parameters');
18+
});
19+
20+
test('validates valid input', () => {
21+
expect(validateLoadParams({advancedFraudSignals: true})).toEqual({
22+
advancedFraudSignals: true,
23+
});
24+
25+
expect(validateLoadParams({advancedFraudSignals: false})).toEqual({
26+
advancedFraudSignals: false,
27+
});
28+
});
29+
});
30+
31+
describe('findScript', () => {
32+
const CASES: Array<[string, boolean]> = [
33+
['https://js.stripe.com/v3?advancedFraudSignals=true', true],
34+
['https://js.stripe.com/v3', true],
35+
['https://js.stripe.com/v3/', true],
36+
['https://js.stripe.com/v3?advancedFraudSignals=false', true],
37+
['https://js.stripe.com/v3?ab=cd', true],
38+
['https://js.stripe.com/v3/something.js', false],
39+
['https://js.stripe.com/v3/something.js?advancedFraudSignals=false', false],
40+
['https://js.stripe.com/v3/something.js?ab=cd', false],
41+
];
42+
43+
afterEach(() => {
44+
for (const [url] of CASES) {
45+
const script = document.querySelector(`script[src="${url}"]`);
46+
47+
if (script && script.parentElement) {
48+
script.parentElement.removeChild(script);
49+
}
50+
}
51+
52+
delete window.Stripe;
53+
});
54+
55+
test.each(CASES)(
56+
'findScript with <script src="%s"></script>',
57+
(url, shouldBeFound) => {
58+
const script = document.createElement('script');
59+
script.src = url;
60+
document.body.appendChild(script);
61+
62+
expect(!!findScript()).toBe(shouldBeFound);
63+
}
64+
);
65+
});

0 commit comments

Comments
 (0)