fontdue-js 3.12.0 → 3.12.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +5 -0
- package/dist/__generated__/CustomerSignInElementQuery.graphql.d.ts +22 -0
- package/dist/__generated__/CustomerSignInElementQuery.graphql.js +87 -0
- package/dist/__tests__/customerSignInElement.test.js +155 -0
- package/dist/__tests__/customerSignInRequest.test.js +85 -0
- package/dist/__tests__/stripeCheckoutError.test.js +110 -0
- package/dist/__tests__/typeTesterPrefetchStyles.test.js +108 -0
- package/dist/components/CustomerSignIn/CustomerSignInElement.d.ts +22 -0
- package/dist/components/CustomerSignIn/CustomerSignInElement.js +132 -0
- package/dist/components/CustomerSignIn/index.js +4 -2
- package/dist/components/Root/index.js +2 -0
- package/dist/components/StripeProvider/index.js +19 -2
- package/dist/components/TypeTester/TypeTesterStyleSelect.js +6 -1
- package/dist/components/TypeTester/usePrefetchWhenNear.d.ts +14 -0
- package/dist/components/TypeTester/usePrefetchWhenNear.js +69 -0
- package/dist/fontdue.css +2 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
## 3.12.1
|
|
2
|
+
|
|
3
|
+
- Fixed a `TypeTester`’s style menu showing only the current style on the first click, most visibly in Safari. The style list now loads as the type tester nears the viewport.
|
|
4
|
+
- Fixed the `StoreModal` checkout showing Stripe’s generic error when Stripe refuses to start a checkout, such as for a total under its minimum charge. It now shows Stripe’s reason.
|
|
5
|
+
|
|
1
6
|
## 3.12.0
|
|
2
7
|
|
|
3
8
|
- `CustomerLoginForm` runs an invisible reCAPTCHA check when *Enable reCAPTCHA* is on under *Settings → Integration*, giving the sign-in form the same bot protection as `NewsletterSignup` and `TestFontsForm`.
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @generated SignedSource<<115e004f4831c3eb09ea98ab9cd0934a>>
|
|
3
|
+
* @lightSyntaxTransform
|
|
4
|
+
* @nogrep
|
|
5
|
+
*/
|
|
6
|
+
import { ConcreteRequest } from 'relay-runtime';
|
|
7
|
+
export type CustomerSignInElementQuery$variables = Record<PropertyKey, never>;
|
|
8
|
+
export type CustomerSignInElementQuery$data = {
|
|
9
|
+
readonly viewer: {
|
|
10
|
+
readonly currentCustomer: {
|
|
11
|
+
readonly email: string | null;
|
|
12
|
+
readonly id: string;
|
|
13
|
+
readonly name: string | null;
|
|
14
|
+
} | null;
|
|
15
|
+
};
|
|
16
|
+
};
|
|
17
|
+
export type CustomerSignInElementQuery = {
|
|
18
|
+
response: CustomerSignInElementQuery$data;
|
|
19
|
+
variables: CustomerSignInElementQuery$variables;
|
|
20
|
+
};
|
|
21
|
+
declare const node: ConcreteRequest;
|
|
22
|
+
export default node;
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @generated SignedSource<<115e004f4831c3eb09ea98ab9cd0934a>>
|
|
3
|
+
* @lightSyntaxTransform
|
|
4
|
+
* @nogrep
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/* tslint:disable */
|
|
8
|
+
/* eslint-disable */
|
|
9
|
+
// @ts-nocheck
|
|
10
|
+
|
|
11
|
+
const node = function () {
|
|
12
|
+
var v0 = {
|
|
13
|
+
"alias": null,
|
|
14
|
+
"args": null,
|
|
15
|
+
"kind": "ScalarField",
|
|
16
|
+
"name": "id",
|
|
17
|
+
"storageKey": null
|
|
18
|
+
},
|
|
19
|
+
v1 = {
|
|
20
|
+
"alias": null,
|
|
21
|
+
"args": null,
|
|
22
|
+
"concreteType": "Customer",
|
|
23
|
+
"kind": "LinkedField",
|
|
24
|
+
"name": "currentCustomer",
|
|
25
|
+
"plural": false,
|
|
26
|
+
"selections": [v0 /*: any*/, {
|
|
27
|
+
"alias": null,
|
|
28
|
+
"args": null,
|
|
29
|
+
"kind": "ScalarField",
|
|
30
|
+
"name": "name",
|
|
31
|
+
"storageKey": null
|
|
32
|
+
}, {
|
|
33
|
+
"alias": null,
|
|
34
|
+
"args": null,
|
|
35
|
+
"kind": "ScalarField",
|
|
36
|
+
"name": "email",
|
|
37
|
+
"storageKey": null
|
|
38
|
+
}],
|
|
39
|
+
"storageKey": null
|
|
40
|
+
};
|
|
41
|
+
return {
|
|
42
|
+
"fragment": {
|
|
43
|
+
"argumentDefinitions": [],
|
|
44
|
+
"kind": "Fragment",
|
|
45
|
+
"metadata": null,
|
|
46
|
+
"name": "CustomerSignInElementQuery",
|
|
47
|
+
"selections": [{
|
|
48
|
+
"alias": null,
|
|
49
|
+
"args": null,
|
|
50
|
+
"concreteType": "Viewer",
|
|
51
|
+
"kind": "LinkedField",
|
|
52
|
+
"name": "viewer",
|
|
53
|
+
"plural": false,
|
|
54
|
+
"selections": [v1 /*: any*/],
|
|
55
|
+
"storageKey": null
|
|
56
|
+
}],
|
|
57
|
+
"type": "RootQueryType",
|
|
58
|
+
"abstractKey": null
|
|
59
|
+
},
|
|
60
|
+
"kind": "Request",
|
|
61
|
+
"operation": {
|
|
62
|
+
"argumentDefinitions": [],
|
|
63
|
+
"kind": "Operation",
|
|
64
|
+
"name": "CustomerSignInElementQuery",
|
|
65
|
+
"selections": [{
|
|
66
|
+
"alias": null,
|
|
67
|
+
"args": null,
|
|
68
|
+
"concreteType": "Viewer",
|
|
69
|
+
"kind": "LinkedField",
|
|
70
|
+
"name": "viewer",
|
|
71
|
+
"plural": false,
|
|
72
|
+
"selections": [v1 /*: any*/, v0 /*: any*/],
|
|
73
|
+
"storageKey": null
|
|
74
|
+
}]
|
|
75
|
+
},
|
|
76
|
+
"params": {
|
|
77
|
+
"cacheID": "3f3148c6d2bf1f2b90edfb3f217f6e50",
|
|
78
|
+
"id": null,
|
|
79
|
+
"metadata": {},
|
|
80
|
+
"name": "CustomerSignInElementQuery",
|
|
81
|
+
"operationKind": "query",
|
|
82
|
+
"text": "query CustomerSignInElementQuery {\n viewer {\n currentCustomer {\n id\n name\n email\n }\n id\n }\n}\n"
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
}();
|
|
86
|
+
node.hash = "e7d4a8fb8d19581d6bc36f29090230af";
|
|
87
|
+
export default node;
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
// @vitest-environment happy-dom
|
|
2
|
+
|
|
3
|
+
// `<fontdue-customer-sign-in>`: a script-tag page has no React of its own, so
|
|
4
|
+
// the element shows who is signed in and tells the page about sign-in and
|
|
5
|
+
// sign-out with window events.
|
|
6
|
+
|
|
7
|
+
import React, { act } from 'react';
|
|
8
|
+
import { createRoot } from 'react-dom/client';
|
|
9
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
10
|
+
const {
|
|
11
|
+
relay
|
|
12
|
+
} = vi.hoisted(() => ({
|
|
13
|
+
relay: {
|
|
14
|
+
currentCustomer: null,
|
|
15
|
+
lookups: 0
|
|
16
|
+
}
|
|
17
|
+
}));
|
|
18
|
+
vi.mock('react-relay', () => ({
|
|
19
|
+
graphql: () => null,
|
|
20
|
+
useLazyLoadQuery: () => {
|
|
21
|
+
relay.lookups += 1;
|
|
22
|
+
return {
|
|
23
|
+
viewer: {
|
|
24
|
+
currentCustomer: relay.currentCustomer
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
}));
|
|
29
|
+
|
|
30
|
+
// The form itself is CustomerSignIn's business; stand in a button that signs
|
|
31
|
+
// in the way it does – tokens stored first, then `onSignedIn`.
|
|
32
|
+
vi.mock('../components/CustomerSignIn/index.js', async () => {
|
|
33
|
+
const {
|
|
34
|
+
setTokens
|
|
35
|
+
} = await import('../auth/tokenStore.js');
|
|
36
|
+
return {
|
|
37
|
+
default: _ref => {
|
|
38
|
+
let {
|
|
39
|
+
onSignedIn
|
|
40
|
+
} = _ref;
|
|
41
|
+
return /*#__PURE__*/React.createElement("button", {
|
|
42
|
+
"data-testid": "form",
|
|
43
|
+
onClick: () => {
|
|
44
|
+
setTokens({
|
|
45
|
+
accessToken: 'fresh',
|
|
46
|
+
refreshToken: 'r'
|
|
47
|
+
});
|
|
48
|
+
onSignedIn({
|
|
49
|
+
id: 'C2',
|
|
50
|
+
name: 'Nina',
|
|
51
|
+
email: 'nina@example.com'
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
});
|
|
58
|
+
import CustomerSignInElement from '../components/CustomerSignIn/CustomerSignInElement.js';
|
|
59
|
+
import { getTokens, resetTokenStore, setTokens } from '../auth/tokenStore.js';
|
|
60
|
+
globalThis.IS_REACT_ACT_ENVIRONMENT = true;
|
|
61
|
+
let container;
|
|
62
|
+
let root;
|
|
63
|
+
let events;
|
|
64
|
+
const record = event => events.push({
|
|
65
|
+
type: event.type,
|
|
66
|
+
detail: event.detail
|
|
67
|
+
});
|
|
68
|
+
beforeEach(() => {
|
|
69
|
+
window.localStorage.clear();
|
|
70
|
+
resetTokenStore();
|
|
71
|
+
relay.currentCustomer = null;
|
|
72
|
+
relay.lookups = 0;
|
|
73
|
+
events = [];
|
|
74
|
+
window.addEventListener('fontdue:customer-signed-in', record);
|
|
75
|
+
window.addEventListener('fontdue:customer-signed-out', record);
|
|
76
|
+
container = document.createElement('div');
|
|
77
|
+
document.body.append(container);
|
|
78
|
+
root = createRoot(container);
|
|
79
|
+
});
|
|
80
|
+
afterEach(() => {
|
|
81
|
+
act(() => root.unmount());
|
|
82
|
+
container.remove();
|
|
83
|
+
window.removeEventListener('fontdue:customer-signed-in', record);
|
|
84
|
+
window.removeEventListener('fontdue:customer-signed-out', record);
|
|
85
|
+
});
|
|
86
|
+
const render = async () => {
|
|
87
|
+
await act(async () => root.render( /*#__PURE__*/React.createElement(CustomerSignInElement, null)));
|
|
88
|
+
};
|
|
89
|
+
const click = async selector => {
|
|
90
|
+
await act(async () => container.querySelector(selector).click());
|
|
91
|
+
};
|
|
92
|
+
describe('<fontdue-customer-sign-in>', () => {
|
|
93
|
+
it('shows the form to a visitor with no session, without a lookup', async () => {
|
|
94
|
+
await render();
|
|
95
|
+
expect(container.querySelector('[data-testid="form"]')).not.toBeNull();
|
|
96
|
+
expect(relay.lookups).toBe(0);
|
|
97
|
+
});
|
|
98
|
+
it('shows a returning customer who is signed in, not the form', async () => {
|
|
99
|
+
setTokens({
|
|
100
|
+
accessToken: 'stored',
|
|
101
|
+
refreshToken: 'r'
|
|
102
|
+
});
|
|
103
|
+
relay.currentCustomer = {
|
|
104
|
+
id: 'C1',
|
|
105
|
+
name: 'Ada',
|
|
106
|
+
email: 'ada@example.com'
|
|
107
|
+
};
|
|
108
|
+
await render();
|
|
109
|
+
expect(container.querySelector('[data-testid="form"]')).toBeNull();
|
|
110
|
+
expect(container.textContent).toContain('Signed in as ada@example.com.');
|
|
111
|
+
});
|
|
112
|
+
it('offers the form when the session names nobody', async () => {
|
|
113
|
+
setTokens({
|
|
114
|
+
accessToken: 'stored',
|
|
115
|
+
refreshToken: 'r'
|
|
116
|
+
});
|
|
117
|
+
await render();
|
|
118
|
+
expect(container.querySelector('[data-testid="form"]')).not.toBeNull();
|
|
119
|
+
});
|
|
120
|
+
it('shows the customer it just signed in at once and tells the page', async () => {
|
|
121
|
+
await render();
|
|
122
|
+
await click('[data-testid="form"]');
|
|
123
|
+
expect(container.textContent).toContain('Signed in as nina@example.com.');
|
|
124
|
+
expect(relay.lookups).toBe(0);
|
|
125
|
+
expect(events).toEqual([{
|
|
126
|
+
type: 'fontdue:customer-signed-in',
|
|
127
|
+
detail: {
|
|
128
|
+
customer: {
|
|
129
|
+
id: 'C2',
|
|
130
|
+
name: 'Nina',
|
|
131
|
+
email: 'nina@example.com'
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}]);
|
|
135
|
+
});
|
|
136
|
+
it('signs out: forgets the session, shows the form again and tells the page', async () => {
|
|
137
|
+
setTokens({
|
|
138
|
+
accessToken: 'stored',
|
|
139
|
+
refreshToken: 'r'
|
|
140
|
+
});
|
|
141
|
+
relay.currentCustomer = {
|
|
142
|
+
id: 'C1',
|
|
143
|
+
name: 'Ada',
|
|
144
|
+
email: 'ada@example.com'
|
|
145
|
+
};
|
|
146
|
+
await render();
|
|
147
|
+
await click('[data-step="signed-in"] button');
|
|
148
|
+
expect(getTokens()).toBeNull();
|
|
149
|
+
expect(container.querySelector('[data-testid="form"]')).not.toBeNull();
|
|
150
|
+
expect(events).toEqual([{
|
|
151
|
+
type: 'fontdue:customer-signed-out',
|
|
152
|
+
detail: null
|
|
153
|
+
}]);
|
|
154
|
+
});
|
|
155
|
+
});
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
// @vitest-environment happy-dom
|
|
2
|
+
|
|
3
|
+
// Asking for a sign-in code: the only unsuccessful *answer* is the rate limit,
|
|
4
|
+
// but a request can also fail outright, and that must not be reported as one.
|
|
5
|
+
|
|
6
|
+
import React, { act } from 'react';
|
|
7
|
+
import { createRoot } from 'react-dom/client';
|
|
8
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
9
|
+
const {
|
|
10
|
+
relay
|
|
11
|
+
} = vi.hoisted(() => ({
|
|
12
|
+
relay: {
|
|
13
|
+
respond: null
|
|
14
|
+
}
|
|
15
|
+
}));
|
|
16
|
+
vi.mock('react-relay', () => ({
|
|
17
|
+
graphql: () => null,
|
|
18
|
+
useMutation: () => [config => {
|
|
19
|
+
var _relay$respond;
|
|
20
|
+
return (_relay$respond = relay.respond) === null || _relay$respond === void 0 ? void 0 : _relay$respond.call(relay, config.onCompleted);
|
|
21
|
+
}, false]
|
|
22
|
+
}));
|
|
23
|
+
vi.mock('../components/FontdueContextProvider/index.js', () => ({
|
|
24
|
+
EnsureFontdueContext: _ref => {
|
|
25
|
+
let {
|
|
26
|
+
children
|
|
27
|
+
} = _ref;
|
|
28
|
+
return /*#__PURE__*/React.createElement(React.Fragment, null, children);
|
|
29
|
+
}
|
|
30
|
+
}));
|
|
31
|
+
import CustomerSignIn from '../components/CustomerSignIn/index.js';
|
|
32
|
+
globalThis.IS_REACT_ACT_ENVIRONMENT = true;
|
|
33
|
+
let container;
|
|
34
|
+
let root;
|
|
35
|
+
beforeEach(() => {
|
|
36
|
+
container = document.createElement('div');
|
|
37
|
+
document.body.append(container);
|
|
38
|
+
root = createRoot(container);
|
|
39
|
+
});
|
|
40
|
+
afterEach(() => {
|
|
41
|
+
act(() => root.unmount());
|
|
42
|
+
container.remove();
|
|
43
|
+
});
|
|
44
|
+
const requestCode = async () => {
|
|
45
|
+
await act(async () => root.render( /*#__PURE__*/React.createElement(CustomerSignIn, {
|
|
46
|
+
email: "reader@example.com"
|
|
47
|
+
})));
|
|
48
|
+
await act(async () => {
|
|
49
|
+
container.querySelector('form').dispatchEvent(new Event('submit', {
|
|
50
|
+
bubbles: true,
|
|
51
|
+
cancelable: true
|
|
52
|
+
}));
|
|
53
|
+
});
|
|
54
|
+
};
|
|
55
|
+
describe('CustomerSignIn asking for a code', () => {
|
|
56
|
+
it('moves to the code step when the code is on its way', async () => {
|
|
57
|
+
relay.respond = done => done({
|
|
58
|
+
requestLoginCode: {
|
|
59
|
+
success: true
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
await requestCode();
|
|
63
|
+
expect(container.querySelector('[data-step="code"]')).not.toBeNull();
|
|
64
|
+
});
|
|
65
|
+
it('reports the rate limit', async () => {
|
|
66
|
+
relay.respond = done => done({
|
|
67
|
+
requestLoginCode: {
|
|
68
|
+
success: false
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
await requestCode();
|
|
72
|
+
expect(container.textContent).toContain('Too many codes requested');
|
|
73
|
+
});
|
|
74
|
+
it('reports a failed request as itself, not as the rate limit', async () => {
|
|
75
|
+
relay.respond = done => done({
|
|
76
|
+
requestLoginCode: null
|
|
77
|
+
}, [{
|
|
78
|
+
message: 'Something went wrong'
|
|
79
|
+
}]);
|
|
80
|
+
await requestCode();
|
|
81
|
+
expect(container.querySelector('[data-step="email"]')).not.toBeNull();
|
|
82
|
+
expect(container.textContent).toContain('Something went wrong');
|
|
83
|
+
expect(container.textContent).not.toContain('Too many codes requested');
|
|
84
|
+
});
|
|
85
|
+
});
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
// @vitest-environment happy-dom
|
|
2
|
+
|
|
3
|
+
// When Stripe refuses to create the Checkout Session (a total under its $0.50
|
|
4
|
+
// minimum, say), the embedded checkout only says “Something went wrong”. The
|
|
5
|
+
// store modal shows Stripe's reason in its place instead.
|
|
6
|
+
|
|
7
|
+
import React, { act, useEffect } from 'react';
|
|
8
|
+
import { createRoot } from 'react-dom/client';
|
|
9
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
10
|
+
const {
|
|
11
|
+
relay
|
|
12
|
+
} = vi.hoisted(() => ({
|
|
13
|
+
relay: {
|
|
14
|
+
response: null,
|
|
15
|
+
errors: undefined,
|
|
16
|
+
networkError: null
|
|
17
|
+
}
|
|
18
|
+
}));
|
|
19
|
+
vi.mock('react-relay', () => ({
|
|
20
|
+
graphql: () => null,
|
|
21
|
+
useRelayEnvironment: () => ({}),
|
|
22
|
+
useFragment: () => ({
|
|
23
|
+
stripeAccountId: 'acct_test',
|
|
24
|
+
stripePublishableKey: 'pk_test'
|
|
25
|
+
}),
|
|
26
|
+
commitMutation: (_env, config) => {
|
|
27
|
+
if (relay.networkError) config.onError(relay.networkError);else config.onCompleted(relay.response, relay.errors);
|
|
28
|
+
}
|
|
29
|
+
}));
|
|
30
|
+
vi.mock('@stripe/stripe-js', () => ({
|
|
31
|
+
loadStripe: () => Promise.resolve({})
|
|
32
|
+
}));
|
|
33
|
+
|
|
34
|
+
// react-stripe-js asks for the client secret as soon as Stripe.js has loaded.
|
|
35
|
+
vi.mock('@stripe/react-stripe-js', () => ({
|
|
36
|
+
Elements: _ref => {
|
|
37
|
+
let {
|
|
38
|
+
children
|
|
39
|
+
} = _ref;
|
|
40
|
+
return /*#__PURE__*/React.createElement(React.Fragment, null, children);
|
|
41
|
+
},
|
|
42
|
+
EmbeddedCheckoutProvider: _ref2 => {
|
|
43
|
+
let {
|
|
44
|
+
options,
|
|
45
|
+
children
|
|
46
|
+
} = _ref2;
|
|
47
|
+
useEffect(() => {
|
|
48
|
+
options.fetchClientSecret().catch(() => {});
|
|
49
|
+
}, []);
|
|
50
|
+
return /*#__PURE__*/React.createElement(React.Fragment, null, children);
|
|
51
|
+
}
|
|
52
|
+
}));
|
|
53
|
+
import StripeProvider from '../components/StripeProvider/index.js';
|
|
54
|
+
globalThis.IS_REACT_ACT_ENVIRONMENT = true;
|
|
55
|
+
let container;
|
|
56
|
+
let root;
|
|
57
|
+
beforeEach(() => {
|
|
58
|
+
relay.response = {
|
|
59
|
+
createOrderCheckoutSession: null
|
|
60
|
+
};
|
|
61
|
+
relay.errors = undefined;
|
|
62
|
+
relay.networkError = null;
|
|
63
|
+
container = document.createElement('div');
|
|
64
|
+
document.body.append(container);
|
|
65
|
+
root = createRoot(container);
|
|
66
|
+
});
|
|
67
|
+
afterEach(() => {
|
|
68
|
+
act(() => root.unmount());
|
|
69
|
+
container.remove();
|
|
70
|
+
});
|
|
71
|
+
const render = async () => {
|
|
72
|
+
await act(async () => root.render( /*#__PURE__*/React.createElement(StripeProvider, {
|
|
73
|
+
viewer: {},
|
|
74
|
+
providerType: "checkout"
|
|
75
|
+
}, /*#__PURE__*/React.createElement("div", {
|
|
76
|
+
"data-testid": "embedded-checkout"
|
|
77
|
+
}))));
|
|
78
|
+
};
|
|
79
|
+
describe('StripeProvider embedded checkout errors', () => {
|
|
80
|
+
it('shows Stripe’s reason when the Checkout Session is refused', async () => {
|
|
81
|
+
var _container$querySelec;
|
|
82
|
+
const message = "The Checkout Session's total amount due must add up to at least $0.50 usd";
|
|
83
|
+
relay.errors = [{
|
|
84
|
+
message
|
|
85
|
+
}];
|
|
86
|
+
await render();
|
|
87
|
+
expect((_container$querySelec = container.querySelector('[role="alert"]')) === null || _container$querySelec === void 0 ? void 0 : _container$querySelec.textContent).toBe(message);
|
|
88
|
+
expect(container.querySelector('[data-testid="embedded-checkout"]')).toBeNull();
|
|
89
|
+
});
|
|
90
|
+
it('falls back to a generic message when the request itself fails', async () => {
|
|
91
|
+
var _container$querySelec2;
|
|
92
|
+
relay.networkError = new Error('fontdue-js: request failed with HTTP 502');
|
|
93
|
+
await render();
|
|
94
|
+
expect((_container$querySelec2 = container.querySelector('[role="alert"]')) === null || _container$querySelec2 === void 0 ? void 0 : _container$querySelec2.textContent).toBe('We couldn’t start the checkout – please try again');
|
|
95
|
+
});
|
|
96
|
+
it('mounts the embedded checkout when the session is created', async () => {
|
|
97
|
+
relay.response = {
|
|
98
|
+
createOrderCheckoutSession: {
|
|
99
|
+
order: {
|
|
100
|
+
stripeCheckoutSession: {
|
|
101
|
+
clientSecret: 'cs_test_secret'
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
await render();
|
|
107
|
+
expect(container.querySelector('[role="alert"]')).toBeNull();
|
|
108
|
+
expect(container.querySelector('[data-testid="embedded-checkout"]')).not.toBeNull();
|
|
109
|
+
});
|
|
110
|
+
});
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
// @vitest-environment happy-dom
|
|
2
|
+
import React, { act, useRef } from 'react';
|
|
3
|
+
import { createRoot } from 'react-dom/client';
|
|
4
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
5
|
+
import usePrefetchWhenNear from '../components/TypeTester/usePrefetchWhenNear.js';
|
|
6
|
+
globalThis.IS_REACT_ACT_ENVIRONMENT = true;
|
|
7
|
+
|
|
8
|
+
// A controllable IntersectionObserver: tests decide when the element is near.
|
|
9
|
+
let observers = [];
|
|
10
|
+
class FakeIntersectionObserver {
|
|
11
|
+
constructor(cb) {
|
|
12
|
+
this.entry = {
|
|
13
|
+
cb,
|
|
14
|
+
disconnected: false
|
|
15
|
+
};
|
|
16
|
+
observers.push(this.entry);
|
|
17
|
+
}
|
|
18
|
+
observe() {}
|
|
19
|
+
disconnect() {
|
|
20
|
+
this.entry.disconnected = true;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
const intersect = isIntersecting => act(() => {
|
|
24
|
+
observers.filter(o => !o.disconnected).forEach(o => o.cb([{
|
|
25
|
+
isIntersecting
|
|
26
|
+
}], {}));
|
|
27
|
+
});
|
|
28
|
+
function Select(_ref) {
|
|
29
|
+
let {
|
|
30
|
+
prefetch
|
|
31
|
+
} = _ref;
|
|
32
|
+
const ref = useRef(null);
|
|
33
|
+
usePrefetchWhenNear(ref, prefetch);
|
|
34
|
+
return /*#__PURE__*/React.createElement("div", {
|
|
35
|
+
ref: ref
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Native <select> menus snapshot their options on open (Safari never updates
|
|
40
|
+
// an open menu), so the type tester's style list has to be fetched before the
|
|
41
|
+
// first click, without competing with the initial render.
|
|
42
|
+
describe('type tester style list prefetch', () => {
|
|
43
|
+
let root;
|
|
44
|
+
let container;
|
|
45
|
+
let prefetch;
|
|
46
|
+
beforeEach(() => {
|
|
47
|
+
vi.useFakeTimers();
|
|
48
|
+
observers = [];
|
|
49
|
+
vi.stubGlobal('IntersectionObserver', FakeIntersectionObserver);
|
|
50
|
+
// Safari: no requestIdleCallback, so the timeout fallback is exercised.
|
|
51
|
+
vi.stubGlobal('requestIdleCallback', undefined);
|
|
52
|
+
container = document.createElement('div');
|
|
53
|
+
document.body.append(container);
|
|
54
|
+
root = createRoot(container);
|
|
55
|
+
prefetch = vi.fn();
|
|
56
|
+
});
|
|
57
|
+
afterEach(() => {
|
|
58
|
+
act(() => root.unmount());
|
|
59
|
+
container.remove();
|
|
60
|
+
vi.unstubAllGlobals();
|
|
61
|
+
vi.useRealTimers();
|
|
62
|
+
});
|
|
63
|
+
const render = () => act(() => root.render( /*#__PURE__*/React.createElement(Select, {
|
|
64
|
+
prefetch: prefetch
|
|
65
|
+
})));
|
|
66
|
+
it('waits until the tester is near the viewport', async () => {
|
|
67
|
+
await render();
|
|
68
|
+
await act(() => vi.advanceTimersByTime(5000));
|
|
69
|
+
expect(prefetch).not.toHaveBeenCalled();
|
|
70
|
+
await intersect(false);
|
|
71
|
+
await act(() => vi.advanceTimersByTime(5000));
|
|
72
|
+
expect(prefetch).not.toHaveBeenCalled();
|
|
73
|
+
});
|
|
74
|
+
it('prefetches at idle once near, not during render', async () => {
|
|
75
|
+
await render();
|
|
76
|
+
await intersect(true);
|
|
77
|
+
expect(prefetch).not.toHaveBeenCalled();
|
|
78
|
+
await act(() => vi.advanceTimersByTime(1000));
|
|
79
|
+
expect(prefetch).toHaveBeenCalledTimes(1);
|
|
80
|
+
});
|
|
81
|
+
it('prefetches only once', async () => {
|
|
82
|
+
await render();
|
|
83
|
+
await intersect(true);
|
|
84
|
+
await act(() => vi.advanceTimersByTime(1000));
|
|
85
|
+
await intersect(true);
|
|
86
|
+
await act(() => vi.advanceTimersByTime(1000));
|
|
87
|
+
expect(prefetch).toHaveBeenCalledTimes(1);
|
|
88
|
+
});
|
|
89
|
+
it('cancels a pending prefetch on unmount', async () => {
|
|
90
|
+
await render();
|
|
91
|
+
await intersect(true);
|
|
92
|
+
act(() => root.render( /*#__PURE__*/React.createElement(React.Fragment, null)));
|
|
93
|
+
await act(() => vi.advanceTimersByTime(1000));
|
|
94
|
+
expect(prefetch).not.toHaveBeenCalled();
|
|
95
|
+
});
|
|
96
|
+
it('uses requestIdleCallback where the browser has it', async () => {
|
|
97
|
+
const idle = vi.fn(cb => setTimeout(cb, 10));
|
|
98
|
+
vi.stubGlobal('requestIdleCallback', idle);
|
|
99
|
+
vi.stubGlobal('cancelIdleCallback', vi.fn());
|
|
100
|
+
await render();
|
|
101
|
+
await intersect(true);
|
|
102
|
+
expect(idle).toHaveBeenCalledWith(expect.any(Function), {
|
|
103
|
+
timeout: 2000
|
|
104
|
+
});
|
|
105
|
+
await act(() => vi.advanceTimersByTime(10));
|
|
106
|
+
expect(prefetch).toHaveBeenCalledTimes(1);
|
|
107
|
+
});
|
|
108
|
+
});
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { CustomerSignIn_props } from './index.js';
|
|
3
|
+
/** Dispatched on `window` once the customer is signed in; `detail.customer`. */
|
|
4
|
+
export declare const SIGNED_IN_EVENT = "fontdue:customer-signed-in";
|
|
5
|
+
/** Dispatched on `window` when the customer clicks the element's sign-out button. */
|
|
6
|
+
export declare const SIGNED_OUT_EVENT = "fontdue:customer-signed-out";
|
|
7
|
+
interface CustomerSignInElement_props extends Omit<CustomerSignIn_props, 'onSignedIn'> {
|
|
8
|
+
/** Label on the button that signs the customer out. */
|
|
9
|
+
signOutLabel?: string;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* `<fontdue-customer-sign-in>`: the script-tag wrapper around `CustomerSignIn`.
|
|
13
|
+
*
|
|
14
|
+
* A page has no React of its own to read `useCurrentCustomer()` with, so the
|
|
15
|
+
* element shows the signed-in state itself – who is signed in and a way out –
|
|
16
|
+
* and tells the page about both changes with window events, as `setConsent`
|
|
17
|
+
* does with `fontdue:consent`.
|
|
18
|
+
*
|
|
19
|
+
* Not in the npm package's `exports`: sign-in is script-tag only for now.
|
|
20
|
+
*/
|
|
21
|
+
export default function CustomerSignInElement({ signOutLabel, ...props }: CustomerSignInElement_props): React.JSX.Element;
|
|
22
|
+
export {};
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
function _extends() { return _extends = Object.assign ? Object.assign.bind() : function (n) { for (var e = 1; e < arguments.length; e++) { var t = arguments[e]; for (var r in t) ({}).hasOwnProperty.call(t, r) && (n[r] = t[r]); } return n; }, _extends.apply(null, arguments); }
|
|
4
|
+
import _CustomerSignInElementQuery from "../../__generated__/CustomerSignInElementQuery.graphql.js";
|
|
5
|
+
import React, { Suspense, useCallback, useState, useSyncExternalStore } from 'react';
|
|
6
|
+
import { ErrorBoundary } from 'react-error-boundary';
|
|
7
|
+
import { graphql, useLazyLoadQuery } from 'react-relay';
|
|
8
|
+
import CustomerSignIn from './index.js';
|
|
9
|
+
import { clearTokens, getTokens, subscribeToTokens } from '../../auth/tokenStore.js';
|
|
10
|
+
import { useFontdueUrl } from '../UrlContext.js';
|
|
11
|
+
const query = (_CustomerSignInElementQuery.hash && _CustomerSignInElementQuery.hash !== "e7d4a8fb8d19581d6bc36f29090230af" && console.error("The definition of 'CustomerSignInElementQuery' appears to have changed. Run `relay-compiler` to update the generated files to receive the expected data."), _CustomerSignInElementQuery);
|
|
12
|
+
|
|
13
|
+
/** Dispatched on `window` once the customer is signed in; `detail.customer`. */
|
|
14
|
+
export const SIGNED_IN_EVENT = 'fontdue:customer-signed-in';
|
|
15
|
+
/** Dispatched on `window` when the customer clicks the element's sign-out button. */
|
|
16
|
+
export const SIGNED_OUT_EVENT = 'fontdue:customer-signed-out';
|
|
17
|
+
/**
|
|
18
|
+
* `<fontdue-customer-sign-in>`: the script-tag wrapper around `CustomerSignIn`.
|
|
19
|
+
*
|
|
20
|
+
* A page has no React of its own to read `useCurrentCustomer()` with, so the
|
|
21
|
+
* element shows the signed-in state itself – who is signed in and a way out –
|
|
22
|
+
* and tells the page about both changes with window events, as `setConsent`
|
|
23
|
+
* does with `fontdue:consent`.
|
|
24
|
+
*
|
|
25
|
+
* Not in the npm package's `exports`: sign-in is script-tag only for now.
|
|
26
|
+
*/
|
|
27
|
+
export default function CustomerSignInElement(_ref) {
|
|
28
|
+
let {
|
|
29
|
+
signOutLabel = 'Sign out',
|
|
30
|
+
...props
|
|
31
|
+
} = _ref;
|
|
32
|
+
const url = useFontdueUrl();
|
|
33
|
+
// The script tag never renders on a server, so reading the store during
|
|
34
|
+
// render can't cause a hydration mismatch – and it means a returning
|
|
35
|
+
// customer never sees the form flash before their session is read.
|
|
36
|
+
const accessToken = useSyncExternalStore(subscribeToTokens, () => {
|
|
37
|
+
var _getTokens;
|
|
38
|
+
return ((_getTokens = getTokens(url)) === null || _getTokens === void 0 ? void 0 : _getTokens.accessToken) ?? null;
|
|
39
|
+
}, () => null);
|
|
40
|
+
// The customer `verifyLoginCode` just returned, tied to the token it came
|
|
41
|
+
// with, so the signed-in state shows at once instead of after a lookup. Any
|
|
42
|
+
// other session – a page load, a sign-in in another element, a renewed
|
|
43
|
+
// token – is looked up.
|
|
44
|
+
const [justSignedIn, setJustSignedIn] = useState(null);
|
|
45
|
+
const handleSignedIn = useCallback(customer => {
|
|
46
|
+
if (customer) {
|
|
47
|
+
var _getTokens2;
|
|
48
|
+
setJustSignedIn({
|
|
49
|
+
accessToken: (_getTokens2 = getTokens(url)) === null || _getTokens2 === void 0 ? void 0 : _getTokens2.accessToken,
|
|
50
|
+
customer
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
window.dispatchEvent(new CustomEvent(SIGNED_IN_EVENT, {
|
|
54
|
+
detail: {
|
|
55
|
+
customer
|
|
56
|
+
}
|
|
57
|
+
}));
|
|
58
|
+
}, [url]);
|
|
59
|
+
const signOut = useCallback(() => {
|
|
60
|
+
clearTokens(url);
|
|
61
|
+
setJustSignedIn(null);
|
|
62
|
+
window.dispatchEvent(new CustomEvent(SIGNED_OUT_EVENT));
|
|
63
|
+
}, [url]);
|
|
64
|
+
const form = /*#__PURE__*/React.createElement(CustomerSignIn, _extends({}, props, {
|
|
65
|
+
onSignedIn: handleSignedIn
|
|
66
|
+
}));
|
|
67
|
+
if (accessToken == null) return form;
|
|
68
|
+
if (justSignedIn && justSignedIn.accessToken === accessToken) {
|
|
69
|
+
return /*#__PURE__*/React.createElement(SignedIn, {
|
|
70
|
+
customer: justSignedIn.customer,
|
|
71
|
+
signOut: signOut,
|
|
72
|
+
signOutLabel: signOutLabel
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// A lookup that fails leaves the visitor able to sign in again rather than
|
|
77
|
+
// facing an empty element.
|
|
78
|
+
return /*#__PURE__*/React.createElement(ErrorBoundary, {
|
|
79
|
+
fallback: form,
|
|
80
|
+
resetKeys: [accessToken]
|
|
81
|
+
}, /*#__PURE__*/React.createElement(Suspense, {
|
|
82
|
+
fallback: null
|
|
83
|
+
}, /*#__PURE__*/React.createElement(SignedInLookup, {
|
|
84
|
+
key: accessToken,
|
|
85
|
+
form: form,
|
|
86
|
+
signOut: signOut,
|
|
87
|
+
signOutLabel: signOutLabel
|
|
88
|
+
})));
|
|
89
|
+
}
|
|
90
|
+
function SignedInLookup(_ref2) {
|
|
91
|
+
var _data$viewer;
|
|
92
|
+
let {
|
|
93
|
+
form,
|
|
94
|
+
signOut,
|
|
95
|
+
signOutLabel
|
|
96
|
+
} = _ref2;
|
|
97
|
+
// network-only: who is signed in *now*, never a store hit left over from the
|
|
98
|
+
// previous customer.
|
|
99
|
+
const data = useLazyLoadQuery(query, {}, {
|
|
100
|
+
fetchPolicy: 'network-only'
|
|
101
|
+
});
|
|
102
|
+
const customer = (_data$viewer = data.viewer) === null || _data$viewer === void 0 ? void 0 : _data$viewer.currentCustomer;
|
|
103
|
+
|
|
104
|
+
// A token the server no longer honours is cleared by the network layer,
|
|
105
|
+
// which re-renders the element signed out. A null here without that is a
|
|
106
|
+
// session that names nobody; offer the form.
|
|
107
|
+
if (!customer) return form;
|
|
108
|
+
return /*#__PURE__*/React.createElement(SignedIn, {
|
|
109
|
+
customer: customer,
|
|
110
|
+
signOut: signOut,
|
|
111
|
+
signOutLabel: signOutLabel
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
function SignedIn(_ref3) {
|
|
115
|
+
let {
|
|
116
|
+
customer,
|
|
117
|
+
signOut,
|
|
118
|
+
signOutLabel
|
|
119
|
+
} = _ref3;
|
|
120
|
+
return /*#__PURE__*/React.createElement("div", {
|
|
121
|
+
className: "customer-sign-in",
|
|
122
|
+
"data-step": "signed-in"
|
|
123
|
+
}, /*#__PURE__*/React.createElement("p", {
|
|
124
|
+
className: "customer-sign-in__intro"
|
|
125
|
+
}, "Signed in as ", customer.email ?? customer.name, "."), /*#__PURE__*/React.createElement("div", {
|
|
126
|
+
className: "customer-sign-in__actions"
|
|
127
|
+
}, /*#__PURE__*/React.createElement("button", {
|
|
128
|
+
className: "customer-sign-in__back",
|
|
129
|
+
type: "button",
|
|
130
|
+
onClick: signOut
|
|
131
|
+
}, signOutLabel)));
|
|
132
|
+
}
|
|
@@ -54,9 +54,11 @@ function CustomerSignInFields(_ref) {
|
|
|
54
54
|
email
|
|
55
55
|
}
|
|
56
56
|
},
|
|
57
|
-
onCompleted: res => {
|
|
57
|
+
onCompleted: (res, errors) => {
|
|
58
58
|
var _res$requestLoginCode;
|
|
59
|
-
if (
|
|
59
|
+
if (errors && errors.length > 0) {
|
|
60
|
+
setError(errors[0].message);
|
|
61
|
+
} else if ((_res$requestLoginCode = res.requestLoginCode) !== null && _res$requestLoginCode !== void 0 && _res$requestLoginCode.success) {
|
|
60
62
|
setStep('code');
|
|
61
63
|
} else {
|
|
62
64
|
// The only unsuccessful answer is the rate limit, and it is about the
|
|
@@ -25,6 +25,8 @@ const customElementMap = {
|
|
|
25
25
|
'fontdue-cookie-notification': /*#__PURE__*/lazy(() => retryImport(() => import('../CookieNotification/index.js'))),
|
|
26
26
|
'fontdue-font-families': /*#__PURE__*/lazy(() => retryImport(() => import('../FontFamilies/index.js'))),
|
|
27
27
|
'fontdue-customer-login-form': /*#__PURE__*/lazy(() => retryImport(() => import('../CustomerLoginForm/index.js'))),
|
|
28
|
+
// Script tag only: CustomerSignIn isn't in the npm package's `exports` yet.
|
|
29
|
+
'fontdue-customer-sign-in': /*#__PURE__*/lazy(() => retryImport(() => import('../CustomerSignIn/CustomerSignInElement.js'))),
|
|
28
30
|
'fontdue-newsletter-signup': /*#__PURE__*/lazy(() => retryImport(() => import('../NewsletterSignup/NewsletterSignupElement.js'))),
|
|
29
31
|
'fontdue-node-password-form': /*#__PURE__*/lazy(() => retryImport(() => import('../NodePasswordForm/NodePasswordFormElement.js'))),
|
|
30
32
|
// @ts-ignore
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import _StripeProviderCreateCheckoutSessionMutation from "../../__generated__/StripeProviderCreateCheckoutSessionMutation.graphql.js";
|
|
2
2
|
import _StripeProvider_viewer from "../../__generated__/StripeProvider_viewer.graphql.js";
|
|
3
|
-
import React, { useCallback, useContext, useRef } from 'react';
|
|
3
|
+
import React, { useCallback, useContext, useRef, useState } from 'react';
|
|
4
4
|
import { commitMutation, graphql, useFragment, useRelayEnvironment } from 'react-relay';
|
|
5
5
|
import { Elements, EmbeddedCheckoutProvider } from '@stripe/react-stripe-js';
|
|
6
6
|
import { loadStripe } from '@stripe/stripe-js';
|
|
7
7
|
import { parseCustomProperties } from '../../utils.js';
|
|
8
8
|
import ConfigContext from '../ConfigContext.js';
|
|
9
|
+
import StoreModalPageContainer from '../elements/StoreModalPageContainer/index.js';
|
|
10
|
+
const CHECKOUT_ERROR = 'We couldn’t start the checkout – please try again';
|
|
9
11
|
const StripeProvider = _ref => {
|
|
10
12
|
var _data$currentOrder, _data$settings, _data$settings$uiFont, _data$settings$uiFont2, _data$settings$uiFont3, _data$settings2, _data$currentOrder2, _data$currentOrder2$s, _data$themeConfig;
|
|
11
13
|
let {
|
|
@@ -26,6 +28,7 @@ const StripeProvider = _ref => {
|
|
|
26
28
|
const {
|
|
27
29
|
stripe: config
|
|
28
30
|
} = useContext(ConfigContext);
|
|
31
|
+
const [checkoutError, setCheckoutError] = useState(null);
|
|
29
32
|
let customFontSource;
|
|
30
33
|
const woffSrc = (_data$settings = data.settings) === null || _data$settings === void 0 ? void 0 : (_data$settings$uiFont = _data$settings.uiFontStyle) === null || _data$settings$uiFont === void 0 ? void 0 : (_data$settings$uiFont2 = _data$settings$uiFont.webfontSources) === null || _data$settings$uiFont2 === void 0 ? void 0 : (_data$settings$uiFont3 = _data$settings$uiFont2.find(source => (source === null || source === void 0 ? void 0 : source.format) === 'woff2')) === null || _data$settings$uiFont3 === void 0 ? void 0 : _data$settings$uiFont3.url;
|
|
31
34
|
let fontFamily = (_data$settings2 = data.settings) !== null && _data$settings2 !== void 0 && _data$settings2.uiFontStyle ? `${data.settings.uiFontStyle.cssFamily} ${data.settings.uiFontStyle.name}` : undefined;
|
|
@@ -50,16 +53,22 @@ const StripeProvider = _ref => {
|
|
|
50
53
|
commitMutation(environment, {
|
|
51
54
|
mutation: (_StripeProviderCreateCheckoutSessionMutation.hash && _StripeProviderCreateCheckoutSessionMutation.hash !== "2ca9a7fcfed1292117e3cd6d0d95cca6" && console.error("The definition of 'StripeProviderCreateCheckoutSessionMutation' appears to have changed. Run `relay-compiler` to update the generated files to receive the expected data."), _StripeProviderCreateCheckoutSessionMutation),
|
|
52
55
|
variables: {},
|
|
53
|
-
onCompleted: response => {
|
|
56
|
+
onCompleted: (response, errors) => {
|
|
54
57
|
var _response$createOrder, _response$createOrder2, _response$createOrder3;
|
|
55
58
|
const clientSecret = (_response$createOrder = response.createOrderCheckoutSession) === null || _response$createOrder === void 0 ? void 0 : (_response$createOrder2 = _response$createOrder.order) === null || _response$createOrder2 === void 0 ? void 0 : (_response$createOrder3 = _response$createOrder2.stripeCheckoutSession) === null || _response$createOrder3 === void 0 ? void 0 : _response$createOrder3.clientSecret;
|
|
56
59
|
if (clientSecret) {
|
|
57
60
|
resolve(clientSecret);
|
|
58
61
|
} else {
|
|
62
|
+
var _errors$;
|
|
63
|
+
// Stripe refused the session (e.g. a total under its minimum
|
|
64
|
+
// charge): show its reason, which the embedded checkout would
|
|
65
|
+
// otherwise swallow into a generic error.
|
|
66
|
+
setCheckoutError((errors === null || errors === void 0 ? void 0 : (_errors$ = errors[0]) === null || _errors$ === void 0 ? void 0 : _errors$.message) ?? CHECKOUT_ERROR);
|
|
59
67
|
reject("Didn't find client secret");
|
|
60
68
|
}
|
|
61
69
|
},
|
|
62
70
|
onError: error => {
|
|
71
|
+
setCheckoutError(CHECKOUT_ERROR);
|
|
63
72
|
reject(`Error creating checkout session: ${error.message}`);
|
|
64
73
|
}
|
|
65
74
|
});
|
|
@@ -147,6 +156,14 @@ const StripeProvider = _ref => {
|
|
|
147
156
|
syncAddressCheckbox: 'none'
|
|
148
157
|
};
|
|
149
158
|
if (providerType === 'checkout') {
|
|
159
|
+
if (checkoutError) {
|
|
160
|
+
return /*#__PURE__*/React.createElement(StoreModalPageContainer, null, {
|
|
161
|
+
body: /*#__PURE__*/React.createElement("div", {
|
|
162
|
+
className: "store-modal__cart__error",
|
|
163
|
+
role: "alert"
|
|
164
|
+
}, checkoutError)
|
|
165
|
+
});
|
|
166
|
+
}
|
|
150
167
|
return /*#__PURE__*/React.createElement(EmbeddedCheckoutProvider, {
|
|
151
168
|
stripe: stripePromiseRef.current,
|
|
152
169
|
options: {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import React from 'react';
|
|
1
|
+
import React, { useRef } from 'react';
|
|
2
2
|
import { compareVariableSettings, variableInstanceSettings } from '../../utils.js';
|
|
3
3
|
import Select from '../Select/index.js';
|
|
4
|
+
import usePrefetchWhenNear from './usePrefetchWhenNear.js';
|
|
4
5
|
const VARIABLE_AXES = 'VARIABLE_AXES';
|
|
5
6
|
const LOADING = '__loading__';
|
|
6
7
|
const TypeTesterStyleSelect = _ref => {
|
|
@@ -19,6 +20,8 @@ const TypeTesterStyleSelect = _ref => {
|
|
|
19
20
|
onSelectFamilyId,
|
|
20
21
|
includeVariableAxesOption
|
|
21
22
|
} = _ref;
|
|
23
|
+
const containerRef = useRef(null);
|
|
24
|
+
usePrefetchWhenNear(containerRef, onOpen);
|
|
22
25
|
const handleSelectFontStyle = selection => {
|
|
23
26
|
if (selection === VARIABLE_AXES) {
|
|
24
27
|
onSelectVariableAxes();
|
|
@@ -90,7 +93,9 @@ const TypeTesterStyleSelect = _ref => {
|
|
|
90
93
|
cb(e.target.value);
|
|
91
94
|
};
|
|
92
95
|
return /*#__PURE__*/React.createElement("div", {
|
|
96
|
+
ref: containerRef,
|
|
93
97
|
className: "type-tester-select",
|
|
98
|
+
onPointerEnter: () => onOpen === null || onOpen === void 0 ? void 0 : onOpen(),
|
|
94
99
|
"data-loading": loading || undefined,
|
|
95
100
|
"aria-busy": loading || undefined,
|
|
96
101
|
style: {
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export declare const PREFETCH_ROOT_MARGIN = "300px";
|
|
2
|
+
export declare const PREFETCH_IDLE_TIMEOUT = 2000;
|
|
3
|
+
/**
|
|
4
|
+
* Calls `prefetch` once, at idle time, after the element first comes near the
|
|
5
|
+
* viewport. Native `<select>` menus snapshot their options when they open (in
|
|
6
|
+
* Safari the open menu never updates), so data a dropdown needs has to be
|
|
7
|
+
* there before the first click – fetching on mousedown is too late. Waiting
|
|
8
|
+
* for idle keeps the request off the initial render, and waiting for the
|
|
9
|
+
* viewport keeps a long page of testers from fetching for every one at once.
|
|
10
|
+
*
|
|
11
|
+
* const ref = useRef<HTMLDivElement>(null);
|
|
12
|
+
* usePrefetchWhenNear(ref, loadOptions);
|
|
13
|
+
*/
|
|
14
|
+
export default function usePrefetchWhenNear(ref: React.RefObject<Element | null>, prefetch: (() => void) | undefined): void;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { useEffect, useRef } from 'react';
|
|
2
|
+
|
|
3
|
+
// How far outside the viewport a tester counts as "near".
|
|
4
|
+
export const PREFETCH_ROOT_MARGIN = '300px';
|
|
5
|
+
// Upper bound on waiting for the browser to go idle.
|
|
6
|
+
export const PREFETCH_IDLE_TIMEOUT = 2000;
|
|
7
|
+
function onIdle(cb) {
|
|
8
|
+
if (typeof window.requestIdleCallback === 'function') {
|
|
9
|
+
const id = window.requestIdleCallback(cb, {
|
|
10
|
+
timeout: PREFETCH_IDLE_TIMEOUT
|
|
11
|
+
});
|
|
12
|
+
return {
|
|
13
|
+
cancel: () => window.cancelIdleCallback(id)
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
// Safari has no requestIdleCallback.
|
|
17
|
+
const id = window.setTimeout(cb, 200);
|
|
18
|
+
return {
|
|
19
|
+
cancel: () => window.clearTimeout(id)
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Calls `prefetch` once, at idle time, after the element first comes near the
|
|
25
|
+
* viewport. Native `<select>` menus snapshot their options when they open (in
|
|
26
|
+
* Safari the open menu never updates), so data a dropdown needs has to be
|
|
27
|
+
* there before the first click – fetching on mousedown is too late. Waiting
|
|
28
|
+
* for idle keeps the request off the initial render, and waiting for the
|
|
29
|
+
* viewport keeps a long page of testers from fetching for every one at once.
|
|
30
|
+
*
|
|
31
|
+
* const ref = useRef<HTMLDivElement>(null);
|
|
32
|
+
* usePrefetchWhenNear(ref, loadOptions);
|
|
33
|
+
*/
|
|
34
|
+
export default function usePrefetchWhenNear(ref, prefetch) {
|
|
35
|
+
const prefetchRef = useRef(prefetch);
|
|
36
|
+
prefetchRef.current = prefetch;
|
|
37
|
+
useEffect(() => {
|
|
38
|
+
const el = ref.current;
|
|
39
|
+
if (!el) return;
|
|
40
|
+
let idle = null;
|
|
41
|
+
const schedule = () => {
|
|
42
|
+
idle ??= onIdle(() => {
|
|
43
|
+
var _prefetchRef$current;
|
|
44
|
+
return (_prefetchRef$current = prefetchRef.current) === null || _prefetchRef$current === void 0 ? void 0 : _prefetchRef$current.call(prefetchRef);
|
|
45
|
+
});
|
|
46
|
+
};
|
|
47
|
+
if (typeof IntersectionObserver !== 'function') {
|
|
48
|
+
schedule();
|
|
49
|
+
return () => {
|
|
50
|
+
var _idle;
|
|
51
|
+
return (_idle = idle) === null || _idle === void 0 ? void 0 : _idle.cancel();
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
const observer = new IntersectionObserver(entries => {
|
|
55
|
+
if (entries.some(entry => entry.isIntersecting)) {
|
|
56
|
+
observer.disconnect();
|
|
57
|
+
schedule();
|
|
58
|
+
}
|
|
59
|
+
}, {
|
|
60
|
+
rootMargin: PREFETCH_ROOT_MARGIN
|
|
61
|
+
});
|
|
62
|
+
observer.observe(el);
|
|
63
|
+
return () => {
|
|
64
|
+
var _idle2;
|
|
65
|
+
observer.disconnect();
|
|
66
|
+
(_idle2 = idle) === null || _idle2 === void 0 ? void 0 : _idle2.cancel();
|
|
67
|
+
};
|
|
68
|
+
}, [ref]);
|
|
69
|
+
}
|
package/dist/fontdue.css
CHANGED
package/dist/version.js
CHANGED
|
@@ -8,4 +8,4 @@
|
|
|
8
8
|
// (Fontage.ClientVersions) to spot sites that need to upgrade before a
|
|
9
9
|
// feature that depends on a newer client can work for them.
|
|
10
10
|
export const CLIENT_VERSION_HEADER = 'fontdue-client-version';
|
|
11
|
-
export const version = "3.12.
|
|
11
|
+
export const version = "3.12.1";
|