fontdue-js 3.5.1 → 3.7.0
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 +19 -0
- package/README.md +2 -0
- package/dist/__generated__/CheckoutUpdateCustomerMutation.graphql.d.ts +3 -1
- package/dist/__generated__/CheckoutUpdateCustomerMutation.graphql.js +1 -1
- package/dist/__generated__/CustomerSignInRequestLoginCodeMutation.graphql.d.ts +24 -0
- package/dist/__generated__/CustomerSignInRequestLoginCodeMutation.graphql.js +71 -0
- package/dist/__generated__/CustomerSignInVerifyLoginCodeMutation.graphql.d.ts +30 -0
- package/dist/__generated__/CustomerSignInVerifyLoginCodeMutation.graphql.js +98 -0
- package/dist/__generated__/FontFamiliesQuery.graphql.d.ts +4 -1
- package/dist/__generated__/FontFamiliesQuery.graphql.js +46 -39
- package/dist/__generated__/NewsletterSignupUpdateCustomerMutation.graphql.d.ts +3 -1
- package/dist/__generated__/NewsletterSignupUpdateCustomerMutation.graphql.js +1 -1
- package/dist/__generated__/PriceBar_node.graphql.d.ts +4 -1
- package/dist/__generated__/PriceBar_node.graphql.js +23 -16
- package/dist/__generated__/StoreModalFamily_collection.graphql.d.ts +3 -2
- package/dist/__generated__/StoreModalFamily_collection.graphql.js +3 -3
- package/dist/__generated__/StoreModalIndexPaginationQuery.graphql.d.ts +23 -0
- package/dist/__generated__/StoreModalIndexPaginationQuery.graphql.js +303 -0
- package/dist/__generated__/StoreModalIndexQuery.graphql.d.ts +4 -9
- package/dist/__generated__/StoreModalIndexQuery.graphql.js +90 -53
- package/dist/__generated__/StoreModalIndex_viewer.graphql.d.ts +24 -0
- package/dist/__generated__/StoreModalIndex_viewer.graphql.js +134 -0
- package/dist/__generated__/StoreModalProductContent_collection.graphql.d.ts +4 -1
- package/dist/__generated__/StoreModalProductContent_collection.graphql.js +12 -5
- package/dist/__generated__/StoreModalProductLicenseSelection_collection.graphql.d.ts +2 -1
- package/dist/__generated__/StoreModalProductLicenseSelection_collection.graphql.js +27 -24
- package/dist/__generated__/StoreModalProductQuery.graphql.d.ts +2 -19
- package/dist/__generated__/StoreModalProductQuery.graphql.js +143 -174
- package/dist/__generated__/StoreModalProductRefetchQuery.graphql.d.ts +1 -1
- package/dist/__generated__/StoreModalProductRefetchQuery.graphql.js +54 -53
- package/dist/__generated__/StoreModalProductSlugQuery.graphql.d.ts +38 -0
- package/dist/__generated__/StoreModalProductSlugQuery.graphql.js +750 -0
- package/dist/__generated__/StoreModalProductViewer.graphql.d.ts +34 -0
- package/dist/__generated__/StoreModalProductViewer.graphql.js +149 -0
- package/dist/__generated__/StoreModalUnifiedCheckoutUpdateCustomerMutation.graphql.d.ts +3 -1
- package/dist/__generated__/StoreModalUnifiedCheckoutUpdateCustomerMutation.graphql.js +1 -1
- package/dist/__generated__/TestFontsFormUpdateCustomerMutation.graphql.d.ts +3 -1
- package/dist/__generated__/TestFontsFormUpdateCustomerMutation.graphql.js +1 -1
- package/dist/__generated__/TypeTestersIDQuery.graphql.d.ts +1 -1
- package/dist/__generated__/TypeTestersIDQuery.graphql.js +17 -10
- package/dist/__generated__/TypeTestersRefetchQuery.graphql.d.ts +1 -1
- package/dist/__generated__/TypeTestersRefetchQuery.graphql.js +17 -10
- package/dist/__generated__/TypeTestersSlugQuery.graphql.d.ts +1 -1
- package/dist/__generated__/TypeTestersSlugQuery.graphql.js +17 -10
- package/dist/__generated__/storefrontEventsTrackStorefrontEventMutation.graphql.d.ts +41 -0
- package/dist/__generated__/storefrontEventsTrackStorefrontEventMutation.graphql.js +65 -0
- package/dist/__generated__/useCurrentCustomerQuery.graphql.d.ts +23 -0
- package/dist/__generated__/{ServerConfigProviderQuery.graphql.js → useCurrentCustomerQuery.graphql.js} +24 -39
- package/dist/__tests__/bundleSelectionOrder.test.js +92 -0
- package/dist/__tests__/collectionBundleSelection.test.js +441 -1446
- package/dist/__tests__/customerSession.test.js +456 -0
- package/dist/__tests__/infiniteScroll.test.js +180 -0
- package/dist/__tests__/licenseExclusions.test.js +2 -1
- package/dist/__tests__/storeModalRoutes.test.js +118 -0
- package/dist/__tests__/storefrontEvents.test.js +167 -0
- package/dist/__tests__/stripeElements.test.js +173 -0
- package/dist/__tests__/typeTesterAutofitFeatures.test.js +138 -0
- package/dist/__tests__/typeTesterInitialSize.test.js +82 -0
- package/dist/__tests__/typeTesterTruncate.test.js +218 -0
- package/dist/auth/session.d.ts +42 -0
- package/dist/auth/session.js +139 -0
- package/dist/auth/tokenStore.d.ts +61 -0
- package/dist/auth/tokenStore.js +162 -0
- package/dist/components/Cart/Checkout.js +19 -9
- package/dist/components/Cart/storefrontEvents.d.ts +34 -0
- package/dist/components/Cart/storefrontEvents.js +138 -0
- package/dist/components/ConfigContext.d.ts +2 -2
- package/dist/components/CustomerSignIn/index.d.ts +33 -0
- package/dist/components/CustomerSignIn/index.js +170 -0
- package/dist/components/FontFamilies/index.js +4 -3
- package/dist/components/NewsletterSignup/index.js +5 -1
- package/dist/components/Precart/index.js +6 -0
- package/dist/components/PriceBar/index.js +5 -3
- package/dist/components/Root/index.js +4 -34
- package/dist/components/Root/productState.d.ts +2 -8
- package/dist/components/Root/productState.js +46 -57
- package/dist/components/SKUPrice/index.js +7 -8
- package/dist/components/StoreModal/StoreModalFamily.js +4 -2
- package/dist/components/StoreModal/StoreModalIndex.js +48 -7
- package/dist/components/StoreModal/StoreModalProduct.d.ts +5 -1
- package/dist/components/StoreModal/StoreModalProduct.js +69 -16
- package/dist/components/StoreModal/index.js +14 -14
- package/dist/components/StoreModal/routes.js +9 -1
- package/dist/components/StoreModalProductLicenseSelection/StoreModalProductLicense.d.ts +2 -1
- package/dist/components/StoreModalProductLicenseSelection/StoreModalProductLicense.js +8 -3
- package/dist/components/StoreModalProductLicenseSelection/index.js +3 -2
- package/dist/components/TestFontsForm/index.js +5 -1
- package/dist/components/TypeTester/TypeTesterContent.js +9 -2
- package/dist/components/TypeTester/TypeTesterContext.js +17 -4
- package/dist/components/TypeTester/TypeTesterFitter.js +7 -2
- package/dist/components/TypeTester/TypeTesterStandalone.d.ts +2 -0
- package/dist/components/TypeTester/TypeTesterStandalone.js +2 -0
- package/dist/components/TypeTester/TypeTesterStandaloneElement.d.ts +1 -0
- package/dist/components/TypeTester/TypeTesterStandaloneElement.js +4 -1
- package/dist/components/TypeTester/TypeTesterVariableAxes.js +16 -10
- package/dist/components/TypeTester/index.d.ts +3 -1
- package/dist/components/TypeTester/index.js +2 -1
- package/dist/components/TypeTester/truncate.d.ts +12 -0
- package/dist/components/TypeTester/truncate.js +95 -0
- package/dist/components/TypeTester/useTypeTesterStyler.d.ts +3 -1
- package/dist/components/TypeTester/useTypeTesterStyler.js +36 -7
- package/dist/components/TypeTesters/TypeTestersElement.d.ts +2 -1
- package/dist/components/TypeTesters/TypeTestersElement.js +4 -1
- package/dist/components/TypeTesters/index.d.ts +2 -0
- package/dist/components/elements/StoreModalContainer/StoreModalContainerContext.d.ts +1 -0
- package/dist/components/elements/StoreModalContainer/index.js +9 -4
- package/dist/components/elements/StoreModalIndexLoadMore/index.d.ts +8 -0
- package/dist/components/elements/StoreModalIndexLoadMore/index.js +21 -0
- package/dist/components/elements/StoreModalUnifiedCheckout.d.ts +1 -1
- package/dist/components/elements/StoreModalUnifiedCheckout.js +20 -17
- package/dist/components/useInfiniteScroll.d.ts +28 -0
- package/dist/components/useInfiniteScroll.js +50 -0
- package/dist/fontdue.css +63 -0
- package/dist/hooks/useAutofit.d.ts +2 -1
- package/dist/hooks/useAutofit.js +10 -4
- package/dist/hooks/useCurrentCustomer.d.ts +34 -0
- package/dist/hooks/useCurrentCustomer.js +93 -0
- package/dist/reducer.d.ts +5 -5
- package/dist/reducer.js +9 -87
- package/dist/relay/environment.js +91 -50
- package/dist/selection.d.ts +40 -0
- package/dist/selection.js +494 -0
- package/dist/storeModalRoutes.d.ts +38 -0
- package/dist/storeModalRoutes.js +110 -0
- package/dist/stripeElements.d.ts +18 -0
- package/dist/stripeElements.js +49 -0
- package/dist/utils.d.ts +1 -12
- package/dist/utils.js +1 -36
- package/dist/version.js +1 -1
- package/package.json +1 -1
- package/dist/__generated__/NodePasswordFormIDQuery.graphql.d.ts +0 -21
- package/dist/__generated__/NodePasswordFormIDQuery.graphql.js +0 -86
- package/dist/__generated__/NodePasswordFormSlugQuery.graphql.d.ts +0 -25
- package/dist/__generated__/NodePasswordFormSlugQuery.graphql.js +0 -108
- package/dist/__generated__/ServerConfigProviderQuery.graphql.d.ts +0 -24
- package/dist/__generated__/TestModeBannerQuery.graphql.d.ts +0 -20
- package/dist/__generated__/TestModeBannerQuery.graphql.js +0 -87
- package/dist/__generated__/ThemeConfigQuery.graphql.d.ts +0 -20
- package/dist/__generated__/ThemeConfigQuery.graphql.js +0 -87
- package/dist/components/CartButton/index.server.d.ts +0 -3
- package/dist/components/CartButton/index.server.js +0 -18
- package/dist/components/CorsErrorModal.d.ts +0 -1
- package/dist/components/CorsErrorModal.js +0 -92
- package/dist/components/FontdueProvider/FontdueProviderClientComponent.d.ts +0 -14
- package/dist/components/FontdueProvider/FontdueProviderClientComponent.js +0 -61
- package/dist/components/ThemeConfig/index.server.d.ts +0 -2
- package/dist/components/ThemeConfig/index.server.js +0 -17
- package/dist/components/TypeTester/features.d.ts +0 -5
- package/dist/components/TypeTester/features.js +0 -143
- package/dist/data/unicodeData.d.ts +0 -4
- package/dist/data/unicodeData.js +0 -1
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import { describe, it, expect, vi, afterEach } from 'vitest';
|
|
2
|
+
import { normalizeTruncate, parseTruncateAttribute, shouldTruncate } from "../components/TypeTester/truncate.js";
|
|
3
|
+
import { mergeConfig } from "../components/ConfigContext.js";
|
|
4
|
+
describe('normalizeTruncate', () => {
|
|
5
|
+
it('is off when unset or false', () => {
|
|
6
|
+
expect(normalizeTruncate()).toEqual({
|
|
7
|
+
lines: 0,
|
|
8
|
+
expandOnFocus: false
|
|
9
|
+
});
|
|
10
|
+
expect(normalizeTruncate(false)).toEqual({
|
|
11
|
+
lines: 0,
|
|
12
|
+
expandOnFocus: false
|
|
13
|
+
});
|
|
14
|
+
});
|
|
15
|
+
it('treats `true` as the legacy one-line preview that opens on focus', () => {
|
|
16
|
+
expect(normalizeTruncate(true)).toEqual({
|
|
17
|
+
lines: 1,
|
|
18
|
+
expandOnFocus: true
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
it('holds a numeric cap while the tester is focused', () => {
|
|
22
|
+
expect(normalizeTruncate(15)).toEqual({
|
|
23
|
+
lines: 15,
|
|
24
|
+
expandOnFocus: false
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
it('distinguishes `truncate: 1` from `truncate: true`', () => {
|
|
28
|
+
expect(normalizeTruncate(1).expandOnFocus).toBe(false);
|
|
29
|
+
expect(normalizeTruncate(true).expandOnFocus).toBe(true);
|
|
30
|
+
});
|
|
31
|
+
it('floors and clamps line counts', () => {
|
|
32
|
+
expect(normalizeTruncate(15.9).lines).toBe(15);
|
|
33
|
+
expect(normalizeTruncate(-3).lines).toBe(0);
|
|
34
|
+
expect(normalizeTruncate(NaN).lines).toBe(0);
|
|
35
|
+
expect(normalizeTruncate(Infinity).lines).toBe(0);
|
|
36
|
+
});
|
|
37
|
+
it('accepts the explicit object form', () => {
|
|
38
|
+
expect(normalizeTruncate({
|
|
39
|
+
lines: 20,
|
|
40
|
+
expandOnFocus: true
|
|
41
|
+
})).toEqual({
|
|
42
|
+
lines: 20,
|
|
43
|
+
expandOnFocus: true
|
|
44
|
+
});
|
|
45
|
+
expect(normalizeTruncate({
|
|
46
|
+
lines: 1
|
|
47
|
+
})).toEqual({
|
|
48
|
+
lines: 1,
|
|
49
|
+
expandOnFocus: false
|
|
50
|
+
});
|
|
51
|
+
expect(normalizeTruncate({
|
|
52
|
+
expandOnFocus: false
|
|
53
|
+
})).toEqual({
|
|
54
|
+
lines: 0,
|
|
55
|
+
expandOnFocus: false
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
// The one behaviour FD-191 changes. Everything else about truncation is
|
|
61
|
+
// geometry that only a browser can check.
|
|
62
|
+
describe('shouldTruncate', () => {
|
|
63
|
+
it('keeps a line count capped through focus', () => {
|
|
64
|
+
const resolved = normalizeTruncate(15);
|
|
65
|
+
expect(shouldTruncate(resolved, false)).toBe(true);
|
|
66
|
+
expect(shouldTruncate(resolved, true)).toBe(true);
|
|
67
|
+
});
|
|
68
|
+
it('still lifts the legacy one-line preview on focus', () => {
|
|
69
|
+
const resolved = normalizeTruncate(true);
|
|
70
|
+
expect(shouldTruncate(resolved, false)).toBe(true);
|
|
71
|
+
expect(shouldTruncate(resolved, true)).toBe(false);
|
|
72
|
+
});
|
|
73
|
+
it('lets the object form ask for the old jump-open behaviour', () => {
|
|
74
|
+
const resolved = normalizeTruncate({
|
|
75
|
+
lines: 20,
|
|
76
|
+
expandOnFocus: true
|
|
77
|
+
});
|
|
78
|
+
expect(shouldTruncate(resolved, false)).toBe(true);
|
|
79
|
+
expect(shouldTruncate(resolved, true)).toBe(false);
|
|
80
|
+
});
|
|
81
|
+
it('is off in both states when truncation is off', () => {
|
|
82
|
+
const resolved = normalizeTruncate(false);
|
|
83
|
+
expect(shouldTruncate(resolved, false)).toBe(false);
|
|
84
|
+
expect(shouldTruncate(resolved, true)).toBe(false);
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
describe('parseTruncateAttribute', () => {
|
|
88
|
+
afterEach(() => vi.restoreAllMocks());
|
|
89
|
+
it('inherits the site config when the attribute is absent', () => {
|
|
90
|
+
expect(parseTruncateAttribute(undefined, 'fontdue-type-tester')).toBeUndefined();
|
|
91
|
+
});
|
|
92
|
+
it('parses the written forms', () => {
|
|
93
|
+
expect(parseTruncateAttribute('15', 'fontdue-type-tester')).toBe(15);
|
|
94
|
+
expect(parseTruncateAttribute('true', 'fontdue-type-tester')).toBe(true);
|
|
95
|
+
expect(parseTruncateAttribute('false', 'fontdue-type-tester')).toBe(false);
|
|
96
|
+
expect(parseTruncateAttribute('0', 'fontdue-type-tester')).toBe(0);
|
|
97
|
+
});
|
|
98
|
+
it('warns and inherits on garbage rather than silently reading as off', () => {
|
|
99
|
+
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
100
|
+
expect(parseTruncateAttribute('fifteen', 'fontdue-type-tester')).toBeUndefined();
|
|
101
|
+
expect(parseTruncateAttribute('15.5', 'fontdue-type-tester')).toBeUndefined();
|
|
102
|
+
expect(parseTruncateAttribute('-2', 'fontdue-type-tester')).toBeUndefined();
|
|
103
|
+
expect(warn).toHaveBeenCalledTimes(3);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
// Root turns a valueless `truncate` into '', which is an authoring mistake
|
|
107
|
+
// rather than a request to inherit, so it has to be audible.
|
|
108
|
+
it('warns on a valueless attribute', () => {
|
|
109
|
+
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
110
|
+
expect(parseTruncateAttribute('', 'fontdue-type-tester')).toBeUndefined();
|
|
111
|
+
expect(warn).toHaveBeenCalledTimes(1);
|
|
112
|
+
});
|
|
113
|
+
it('names the element it failed on', () => {
|
|
114
|
+
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
115
|
+
parseTruncateAttribute('nope', 'fontdue-type-testers');
|
|
116
|
+
expect(warn.mock.calls[0][0]).toContain('fontdue-type-testers');
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
// The second word is the only way an attribute can reach `expandOnFocus`,
|
|
120
|
+
// which is the one combination the scalar forms cannot express: a line count
|
|
121
|
+
// that still opens on click.
|
|
122
|
+
it('reads a second word as expandOnFocus', () => {
|
|
123
|
+
expect(parseTruncateAttribute('15 expandable', 'fontdue-type-tester')).toEqual({
|
|
124
|
+
lines: 15,
|
|
125
|
+
expandOnFocus: true
|
|
126
|
+
});
|
|
127
|
+
expect(parseTruncateAttribute('15 fixed', 'fontdue-type-tester')).toEqual({
|
|
128
|
+
lines: 15,
|
|
129
|
+
expandOnFocus: false
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
it('fills the gap the scalar forms leave', () => {
|
|
133
|
+
const resolved = normalizeTruncate(parseTruncateAttribute('15 expandable', 'fontdue-type-tester'));
|
|
134
|
+
expect(resolved).toEqual({
|
|
135
|
+
lines: 15,
|
|
136
|
+
expandOnFocus: true
|
|
137
|
+
});
|
|
138
|
+
expect(shouldTruncate(resolved, false)).toBe(true);
|
|
139
|
+
expect(shouldTruncate(resolved, true)).toBe(false);
|
|
140
|
+
});
|
|
141
|
+
it('takes a second word after true and false too', () => {
|
|
142
|
+
// `true fixed` is the one-line preview that stays put, which was previously
|
|
143
|
+
// only reachable as the bare count "1".
|
|
144
|
+
expect(parseTruncateAttribute('true fixed', 'fontdue-type-tester')).toEqual({
|
|
145
|
+
lines: 1,
|
|
146
|
+
expandOnFocus: false
|
|
147
|
+
});
|
|
148
|
+
expect(parseTruncateAttribute('true expandable', 'fontdue-type-tester')).toEqual({
|
|
149
|
+
lines: 1,
|
|
150
|
+
expandOnFocus: true
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
it('tolerates the whitespace a hand-written attribute picks up', () => {
|
|
154
|
+
expect(parseTruncateAttribute(' 15 expandable ', 'fontdue-type-tester')).toEqual({
|
|
155
|
+
lines: 15,
|
|
156
|
+
expandOnFocus: true
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
it('warns on an unknown or extra word rather than ignoring it', () => {
|
|
160
|
+
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
161
|
+
expect(parseTruncateAttribute('15 open', 'fontdue-type-tester')).toBeUndefined();
|
|
162
|
+
expect(parseTruncateAttribute('15 Expandable', 'fontdue-type-tester')).toBeUndefined();
|
|
163
|
+
expect(parseTruncateAttribute('15 expandable now', 'fontdue-type-tester')).toBeUndefined();
|
|
164
|
+
expect(parseTruncateAttribute('expandable', 'fontdue-type-tester')).toBeUndefined();
|
|
165
|
+
expect(warn).toHaveBeenCalledTimes(4);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
// A word that is an Object member must be a typo like any other, not a
|
|
169
|
+
// truthy hit off the prototype chain.
|
|
170
|
+
it('does not treat inherited Object members as words', () => {
|
|
171
|
+
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
172
|
+
expect(parseTruncateAttribute('15 constructor', 'fontdue-type-tester')).toBeUndefined();
|
|
173
|
+
expect(warn).toHaveBeenCalledTimes(1);
|
|
174
|
+
});
|
|
175
|
+
it('tells the author which forms it accepts', () => {
|
|
176
|
+
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
177
|
+
parseTruncateAttribute('15 open', 'fontdue-type-tester');
|
|
178
|
+
expect(warn.mock.calls[0][0]).toContain('15 expandable');
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
describe('mergeConfig with the object truncate form', () => {
|
|
182
|
+
it('deep-merges a per-component line count onto a global expandOnFocus', () => {
|
|
183
|
+
var _merged$typeTester;
|
|
184
|
+
const merged = mergeConfig({
|
|
185
|
+
typeTester: {
|
|
186
|
+
truncate: {
|
|
187
|
+
expandOnFocus: true
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}, {
|
|
191
|
+
typeTester: {
|
|
192
|
+
truncate: {
|
|
193
|
+
lines: 15
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
expect((_merged$typeTester = merged.typeTester) === null || _merged$typeTester === void 0 ? void 0 : _merged$typeTester.truncate).toEqual({
|
|
198
|
+
expandOnFocus: true,
|
|
199
|
+
lines: 15
|
|
200
|
+
});
|
|
201
|
+
});
|
|
202
|
+
it('lets a scalar override replace an object base outright', () => {
|
|
203
|
+
var _merged$typeTester2;
|
|
204
|
+
const merged = mergeConfig({
|
|
205
|
+
typeTester: {
|
|
206
|
+
truncate: {
|
|
207
|
+
lines: 10,
|
|
208
|
+
expandOnFocus: true
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}, {
|
|
212
|
+
typeTester: {
|
|
213
|
+
truncate: 20
|
|
214
|
+
}
|
|
215
|
+
});
|
|
216
|
+
expect((_merged$typeTester2 = merged.typeTester) === null || _merged$typeTester2 === void 0 ? void 0 : _merged$typeTester2.truncate).toBe(20);
|
|
217
|
+
});
|
|
218
|
+
});
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The customer session, as the network layer sees it: putting the access token
|
|
3
|
+
* on a request, and renewing it when the server says it has expired.
|
|
4
|
+
*
|
|
5
|
+
* The access token lasts 24 hours and the refresh token 30 days, so a customer
|
|
6
|
+
* who signs in once and comes back the next week arrives with an expired access
|
|
7
|
+
* token and a good refresh token. That is the ordinary case, not an error —
|
|
8
|
+
* `token:expired` is renewed and the request retried, and the visitor never
|
|
9
|
+
* sees it. Only `token:invalid` (a forged, tampered or too-old credential, or
|
|
10
|
+
* one naming a customer who no longer exists) ends the session.
|
|
11
|
+
*/
|
|
12
|
+
import { AuthTokens } from './tokenStore.js';
|
|
13
|
+
/** Codes `FontageWeb.UserAuthPlugs` and the RequireCustomer middleware return. */
|
|
14
|
+
export type CustomerAuthErrorCode = 'token:expired' | 'token:invalid' | 'token:missing' | 'customer:required';
|
|
15
|
+
/**
|
|
16
|
+
* The customer-auth error code in a GraphQL response, if it carries one.
|
|
17
|
+
*
|
|
18
|
+
* These arrive as ordinary field errors rather than a failed request — a
|
|
19
|
+
* storefront query asks for public content and `viewer.currentCustomer` in one
|
|
20
|
+
* go, and the public half still resolves.
|
|
21
|
+
*/
|
|
22
|
+
export declare function customerAuthErrorCode(json: unknown): CustomerAuthErrorCode | undefined;
|
|
23
|
+
/**
|
|
24
|
+
* Puts the stored access token on `headers`, and reports whether it did.
|
|
25
|
+
*
|
|
26
|
+
* Returns false when the visitor is signed out, so the caller knows an auth
|
|
27
|
+
* error in the response is about somebody else's credential (an admin preview
|
|
28
|
+
* token, say) and none of its business.
|
|
29
|
+
*/
|
|
30
|
+
export declare function applyCustomerAuthHeader(headers: Record<string, string>, scope: string | undefined): boolean;
|
|
31
|
+
/**
|
|
32
|
+
* Trades the stored refresh token for a new pair.
|
|
33
|
+
*
|
|
34
|
+
* Deliberately a plain `fetch` rather than a Relay mutation: this runs from
|
|
35
|
+
* inside the Relay network layer, and going back through it would recurse.
|
|
36
|
+
*
|
|
37
|
+
* Returns the new tokens, or `null` — in which case the session has been
|
|
38
|
+
* cleared and the caller should let the original auth error stand.
|
|
39
|
+
*/
|
|
40
|
+
export declare function refreshCustomerSession(url: string, scope: string | undefined, extraHeaders?: Record<string, string>): Promise<AuthTokens | null>;
|
|
41
|
+
/** Test seam: forgets any refresh that is still in flight. */
|
|
42
|
+
export declare function resetRefreshState(): void;
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The customer session, as the network layer sees it: putting the access token
|
|
3
|
+
* on a request, and renewing it when the server says it has expired.
|
|
4
|
+
*
|
|
5
|
+
* The access token lasts 24 hours and the refresh token 30 days, so a customer
|
|
6
|
+
* who signs in once and comes back the next week arrives with an expired access
|
|
7
|
+
* token and a good refresh token. That is the ordinary case, not an error —
|
|
8
|
+
* `token:expired` is renewed and the request retried, and the visitor never
|
|
9
|
+
* sees it. Only `token:invalid` (a forged, tampered or too-old credential, or
|
|
10
|
+
* one naming a customer who no longer exists) ends the session.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { clearTokens, getTokens, setTokens } from './tokenStore.js';
|
|
14
|
+
|
|
15
|
+
/** Codes `FontageWeb.UserAuthPlugs` and the RequireCustomer middleware return. */
|
|
16
|
+
|
|
17
|
+
const AUTH_ERROR_CODES = ['token:expired', 'token:invalid', 'token:missing', 'customer:required'];
|
|
18
|
+
/**
|
|
19
|
+
* The customer-auth error code in a GraphQL response, if it carries one.
|
|
20
|
+
*
|
|
21
|
+
* These arrive as ordinary field errors rather than a failed request — a
|
|
22
|
+
* storefront query asks for public content and `viewer.currentCustomer` in one
|
|
23
|
+
* go, and the public half still resolves.
|
|
24
|
+
*/
|
|
25
|
+
export function customerAuthErrorCode(json) {
|
|
26
|
+
const errors = json === null || json === void 0 ? void 0 : json.errors;
|
|
27
|
+
if (!Array.isArray(errors)) return undefined;
|
|
28
|
+
for (const error of errors) {
|
|
29
|
+
var _error$extensions;
|
|
30
|
+
const code = error === null || error === void 0 ? void 0 : (_error$extensions = error.extensions) === null || _error$extensions === void 0 ? void 0 : _error$extensions.code;
|
|
31
|
+
if (typeof code === 'string' && AUTH_ERROR_CODES.includes(code)) {
|
|
32
|
+
return code;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return undefined;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Puts the stored access token on `headers`, and reports whether it did.
|
|
40
|
+
*
|
|
41
|
+
* Returns false when the visitor is signed out, so the caller knows an auth
|
|
42
|
+
* error in the response is about somebody else's credential (an admin preview
|
|
43
|
+
* token, say) and none of its business.
|
|
44
|
+
*/
|
|
45
|
+
export function applyCustomerAuthHeader(headers, scope) {
|
|
46
|
+
const tokens = getTokens(scope);
|
|
47
|
+
if (tokens == null) return false;
|
|
48
|
+
headers.Authorization = `Bearer ${tokens.accessToken}`;
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Renewal is idempotent but not free, and a page load fires several queries at
|
|
53
|
+
// once — all of which would come back `token:expired` together. One refresh per
|
|
54
|
+
// scope is shared between them.
|
|
55
|
+
const inFlight = new Map();
|
|
56
|
+
const REFRESH_MUTATION = `mutation FontdueRefreshAccessToken($refreshToken: String!) {
|
|
57
|
+
refreshAccessToken(input: { refreshToken: $refreshToken }) {
|
|
58
|
+
accessToken
|
|
59
|
+
refreshToken
|
|
60
|
+
}
|
|
61
|
+
}`;
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Trades the stored refresh token for a new pair.
|
|
65
|
+
*
|
|
66
|
+
* Deliberately a plain `fetch` rather than a Relay mutation: this runs from
|
|
67
|
+
* inside the Relay network layer, and going back through it would recurse.
|
|
68
|
+
*
|
|
69
|
+
* Returns the new tokens, or `null` — in which case the session has been
|
|
70
|
+
* cleared and the caller should let the original auth error stand.
|
|
71
|
+
*/
|
|
72
|
+
export function refreshCustomerSession(url, scope) {
|
|
73
|
+
let extraHeaders = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {};
|
|
74
|
+
const key = scope ?? 'default';
|
|
75
|
+
const existing = inFlight.get(key);
|
|
76
|
+
if (existing) return existing;
|
|
77
|
+
const pending = doRefresh(url, scope, extraHeaders).finally(() => {
|
|
78
|
+
inFlight.delete(key);
|
|
79
|
+
});
|
|
80
|
+
inFlight.set(key, pending);
|
|
81
|
+
return pending;
|
|
82
|
+
}
|
|
83
|
+
async function doRefresh(url, scope, extraHeaders) {
|
|
84
|
+
const tokens = getTokens(scope);
|
|
85
|
+
if (tokens == null) return null;
|
|
86
|
+
|
|
87
|
+
// The caller's headers carry the tenant-routing and telemetry headers this
|
|
88
|
+
// request wants too — but not its `Authorization`, which by now holds the
|
|
89
|
+
// very access token the server has just refused. The refresh token travels in
|
|
90
|
+
// the mutation's variables instead.
|
|
91
|
+
const {
|
|
92
|
+
Authorization: _access,
|
|
93
|
+
authorization: _lower,
|
|
94
|
+
...forwarded
|
|
95
|
+
} = extraHeaders;
|
|
96
|
+
try {
|
|
97
|
+
var _json$data;
|
|
98
|
+
const response = await fetch(`${url}?queryName=FontdueRefreshAccessToken`, {
|
|
99
|
+
method: 'POST',
|
|
100
|
+
credentials: 'include',
|
|
101
|
+
headers: {
|
|
102
|
+
...forwarded,
|
|
103
|
+
Accept: 'application/json',
|
|
104
|
+
'Content-Type': 'application/json'
|
|
105
|
+
},
|
|
106
|
+
body: JSON.stringify({
|
|
107
|
+
query: REFRESH_MUTATION,
|
|
108
|
+
variables: {
|
|
109
|
+
refreshToken: tokens.refreshToken
|
|
110
|
+
}
|
|
111
|
+
})
|
|
112
|
+
});
|
|
113
|
+
if (!response.ok) return null;
|
|
114
|
+
const json = await response.json();
|
|
115
|
+
const renewed = json === null || json === void 0 ? void 0 : (_json$data = json.data) === null || _json$data === void 0 ? void 0 : _json$data.refreshAccessToken;
|
|
116
|
+
if (typeof (renewed === null || renewed === void 0 ? void 0 : renewed.accessToken) === 'string' && typeof (renewed === null || renewed === void 0 ? void 0 : renewed.refreshToken) === 'string') {
|
|
117
|
+
const next = {
|
|
118
|
+
accessToken: renewed.accessToken,
|
|
119
|
+
refreshToken: renewed.refreshToken
|
|
120
|
+
};
|
|
121
|
+
setTokens(next, scope);
|
|
122
|
+
return next;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// The server answered, and it will not renew this credential: the refresh
|
|
126
|
+
// token is expired or invalid too. Sign out rather than retrying forever.
|
|
127
|
+
clearTokens(scope);
|
|
128
|
+
return null;
|
|
129
|
+
} catch {
|
|
130
|
+
// A network failure says nothing about the credential. Keep the tokens and
|
|
131
|
+
// let the original error surface — the next request will try again.
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** Test seam: forgets any refresh that is still in flight. */
|
|
137
|
+
export function resetRefreshState() {
|
|
138
|
+
inFlight.clear();
|
|
139
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where a signed-in customer's tokens live in the browser.
|
|
3
|
+
*
|
|
4
|
+
* Sign-in returns an access token (24 hours) and a refresh token (30 days).
|
|
5
|
+
* Both are bearer credentials the network layer has to put on an
|
|
6
|
+
* `Authorization` header, so they have to be readable by JavaScript — an
|
|
7
|
+
* httpOnly cookie, safer for a session the server reads, cannot be read back to
|
|
8
|
+
* build that header. `localStorage` is therefore no weaker here than a
|
|
9
|
+
* JS-readable cookie, and it does not ride along on every request to the
|
|
10
|
+
* foundry's own site.
|
|
11
|
+
*
|
|
12
|
+
* ## Why a module singleton, and why on globalThis
|
|
13
|
+
*
|
|
14
|
+
* Astro (and Vike, and Qwik) mount one React tree per island, each with its own
|
|
15
|
+
* reconciler and its own copy of every context — so a token held in React state
|
|
16
|
+
* would have the sign-in island signed in and the cart island signed out. Next's
|
|
17
|
+
* server build can also bundle one file into several chunks, whose module-level
|
|
18
|
+
* `let`s are separate variables. Anchoring the singleton on `globalThis` covers
|
|
19
|
+
* both: every island and every duplicated copy of this module reads one store.
|
|
20
|
+
* Same reasoning as `relay/serverConfig.ts` and the Relay environment.
|
|
21
|
+
*
|
|
22
|
+
* ## Scope
|
|
23
|
+
*
|
|
24
|
+
* Keyed by the Fontdue URL the tokens came from, so a page wired to two
|
|
25
|
+
* foundries — or a developer switching a local site between them — never sends
|
|
26
|
+
* one foundry's token to the other.
|
|
27
|
+
*
|
|
28
|
+
* ## SSR
|
|
29
|
+
*
|
|
30
|
+
* Every entry point returns as if signed out on the server. There is no browser
|
|
31
|
+
* storage to read there, and reading a *shared* store on the server would leak
|
|
32
|
+
* one visitor's session into another's render. The first client render
|
|
33
|
+
* therefore matches the server HTML, and the tokens appear on the render after
|
|
34
|
+
* hydration, driven by `subscribeToTokens`.
|
|
35
|
+
*/
|
|
36
|
+
export interface AuthTokens {
|
|
37
|
+
accessToken: string;
|
|
38
|
+
refreshToken: string;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* The `localStorage` key for a given Fontdue URL.
|
|
42
|
+
*
|
|
43
|
+
* Exported so a host app can clear it, and so the tests can assert on the exact
|
|
44
|
+
* key rather than on whatever the implementation happens to write.
|
|
45
|
+
*/
|
|
46
|
+
export declare function tokenStorageKey(url?: string): string;
|
|
47
|
+
/** The tokens held for `url`, or `null` when signed out (always on the server). */
|
|
48
|
+
export declare function getTokens(url?: string): AuthTokens | null;
|
|
49
|
+
/** Replaces the tokens held for `url`, or clears them when given `null`. */
|
|
50
|
+
export declare function setTokens(tokens: AuthTokens | null, url?: string): void;
|
|
51
|
+
/** Signs out of `url`: forgets the tokens here and in storage. */
|
|
52
|
+
export declare function clearTokens(url?: string): void;
|
|
53
|
+
/**
|
|
54
|
+
* Calls `listener` whenever the tokens change, and returns an unsubscribe.
|
|
55
|
+
*
|
|
56
|
+
* This is how a sign-in in one island reaches a `useCurrentCustomer()` in
|
|
57
|
+
* another — nothing else connects two React trees on the same page.
|
|
58
|
+
*/
|
|
59
|
+
export declare function subscribeToTokens(listener: () => void): () => void;
|
|
60
|
+
/** Test seam: drops every cached token and listener. */
|
|
61
|
+
export declare function resetTokenStore(): void;
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where a signed-in customer's tokens live in the browser.
|
|
3
|
+
*
|
|
4
|
+
* Sign-in returns an access token (24 hours) and a refresh token (30 days).
|
|
5
|
+
* Both are bearer credentials the network layer has to put on an
|
|
6
|
+
* `Authorization` header, so they have to be readable by JavaScript — an
|
|
7
|
+
* httpOnly cookie, safer for a session the server reads, cannot be read back to
|
|
8
|
+
* build that header. `localStorage` is therefore no weaker here than a
|
|
9
|
+
* JS-readable cookie, and it does not ride along on every request to the
|
|
10
|
+
* foundry's own site.
|
|
11
|
+
*
|
|
12
|
+
* ## Why a module singleton, and why on globalThis
|
|
13
|
+
*
|
|
14
|
+
* Astro (and Vike, and Qwik) mount one React tree per island, each with its own
|
|
15
|
+
* reconciler and its own copy of every context — so a token held in React state
|
|
16
|
+
* would have the sign-in island signed in and the cart island signed out. Next's
|
|
17
|
+
* server build can also bundle one file into several chunks, whose module-level
|
|
18
|
+
* `let`s are separate variables. Anchoring the singleton on `globalThis` covers
|
|
19
|
+
* both: every island and every duplicated copy of this module reads one store.
|
|
20
|
+
* Same reasoning as `relay/serverConfig.ts` and the Relay environment.
|
|
21
|
+
*
|
|
22
|
+
* ## Scope
|
|
23
|
+
*
|
|
24
|
+
* Keyed by the Fontdue URL the tokens came from, so a page wired to two
|
|
25
|
+
* foundries — or a developer switching a local site between them — never sends
|
|
26
|
+
* one foundry's token to the other.
|
|
27
|
+
*
|
|
28
|
+
* ## SSR
|
|
29
|
+
*
|
|
30
|
+
* Every entry point returns as if signed out on the server. There is no browser
|
|
31
|
+
* storage to read there, and reading a *shared* store on the server would leak
|
|
32
|
+
* one visitor's session into another's render. The first client render
|
|
33
|
+
* therefore matches the server HTML, and the tokens appear on the render after
|
|
34
|
+
* hydration, driven by `subscribeToTokens`.
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
const IS_SERVER = typeof window === 'undefined';
|
|
38
|
+
const STORE_KEY = '__fontdueAuthTokenStore__';
|
|
39
|
+
const STORAGE_PREFIX = 'fontdue:auth:';
|
|
40
|
+
function store() {
|
|
41
|
+
const g = globalThis;
|
|
42
|
+
let s = g[STORE_KEY];
|
|
43
|
+
if (!s) {
|
|
44
|
+
s = {
|
|
45
|
+
cache: new Map(),
|
|
46
|
+
listeners: new Set()
|
|
47
|
+
};
|
|
48
|
+
g[STORE_KEY] = s;
|
|
49
|
+
}
|
|
50
|
+
return s;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* The `localStorage` key for a given Fontdue URL.
|
|
55
|
+
*
|
|
56
|
+
* Exported so a host app can clear it, and so the tests can assert on the exact
|
|
57
|
+
* key rather than on whatever the implementation happens to write.
|
|
58
|
+
*/
|
|
59
|
+
export function tokenStorageKey(url) {
|
|
60
|
+
return STORAGE_PREFIX + scope(url);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// A trailing slash and a case difference are the same origin as far as the API
|
|
64
|
+
// is concerned, so they must not be two different sessions.
|
|
65
|
+
function scope(url) {
|
|
66
|
+
if (url == null || url === '') return 'default';
|
|
67
|
+
return url.trim().replace(/\/+$/, '').toLowerCase();
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Storage throws rather than returning null in more situations than people
|
|
71
|
+
// expect: Safari's private mode historically, a browser set to block site data,
|
|
72
|
+
// and any cross-origin iframe with third-party storage partitioned off. None of
|
|
73
|
+
// those should take the page down — a visitor who can't persist a session
|
|
74
|
+
// simply signs in again.
|
|
75
|
+
function readStorage(key) {
|
|
76
|
+
try {
|
|
77
|
+
const raw = window.localStorage.getItem(key);
|
|
78
|
+
if (!raw) return null;
|
|
79
|
+
const parsed = JSON.parse(raw);
|
|
80
|
+
if (typeof (parsed === null || parsed === void 0 ? void 0 : parsed.accessToken) !== 'string' || typeof (parsed === null || parsed === void 0 ? void 0 : parsed.refreshToken) !== 'string') {
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
return {
|
|
84
|
+
accessToken: parsed.accessToken,
|
|
85
|
+
refreshToken: parsed.refreshToken
|
|
86
|
+
};
|
|
87
|
+
} catch {
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
function writeStorage(key, tokens) {
|
|
92
|
+
try {
|
|
93
|
+
if (tokens == null) {
|
|
94
|
+
window.localStorage.removeItem(key);
|
|
95
|
+
} else {
|
|
96
|
+
window.localStorage.setItem(key, JSON.stringify(tokens));
|
|
97
|
+
}
|
|
98
|
+
} catch {
|
|
99
|
+
// Kept in memory only. The session lasts the tab rather than the month.
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** The tokens held for `url`, or `null` when signed out (always on the server). */
|
|
104
|
+
export function getTokens(url) {
|
|
105
|
+
if (IS_SERVER) return null;
|
|
106
|
+
const {
|
|
107
|
+
cache
|
|
108
|
+
} = store();
|
|
109
|
+
const key = scope(url);
|
|
110
|
+
if (!cache.has(key)) {
|
|
111
|
+
cache.set(key, readStorage(tokenStorageKey(url)));
|
|
112
|
+
}
|
|
113
|
+
return cache.get(key) ?? null;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** Replaces the tokens held for `url`, or clears them when given `null`. */
|
|
117
|
+
export function setTokens(tokens, url) {
|
|
118
|
+
if (IS_SERVER) return;
|
|
119
|
+
const s = store();
|
|
120
|
+
s.cache.set(scope(url), tokens);
|
|
121
|
+
writeStorage(tokenStorageKey(url), tokens);
|
|
122
|
+
notify(s);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** Signs out of `url`: forgets the tokens here and in storage. */
|
|
126
|
+
export function clearTokens(url) {
|
|
127
|
+
setTokens(null, url);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Calls `listener` whenever the tokens change, and returns an unsubscribe.
|
|
132
|
+
*
|
|
133
|
+
* This is how a sign-in in one island reaches a `useCurrentCustomer()` in
|
|
134
|
+
* another — nothing else connects two React trees on the same page.
|
|
135
|
+
*/
|
|
136
|
+
export function subscribeToTokens(listener) {
|
|
137
|
+
if (IS_SERVER) return () => {};
|
|
138
|
+
const {
|
|
139
|
+
listeners
|
|
140
|
+
} = store();
|
|
141
|
+
listeners.add(listener);
|
|
142
|
+
return () => {
|
|
143
|
+
listeners.delete(listener);
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
function notify(s) {
|
|
147
|
+
// A copy, so a listener that unsubscribes itself while being called doesn't
|
|
148
|
+
// change the set mid-iteration.
|
|
149
|
+
for (const listener of Array.from(s.listeners)) {
|
|
150
|
+
try {
|
|
151
|
+
listener();
|
|
152
|
+
} catch {
|
|
153
|
+
// One broken subscriber must not stop the others hearing about a sign-out.
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** Test seam: drops every cached token and listener. */
|
|
159
|
+
export function resetTokenStore() {
|
|
160
|
+
const g = globalThis;
|
|
161
|
+
delete g[STORE_KEY];
|
|
162
|
+
}
|
|
@@ -4,9 +4,10 @@ import _CheckoutUpdateOrderMutation from "../../__generated__/CheckoutUpdateOrde
|
|
|
4
4
|
import _Checkout_UpdateOrderErrors from "../../__generated__/Checkout_UpdateOrderErrors.graphql.js";
|
|
5
5
|
import _CheckoutUpdateCustomerMutation from "../../__generated__/CheckoutUpdateCustomerMutation.graphql.js";
|
|
6
6
|
import _Checkout_identity from "../../__generated__/Checkout_identity.graphql.js";
|
|
7
|
-
import React, { Fragment, useContext, useEffect, useState } from 'react';
|
|
7
|
+
import React, { Fragment, useContext, useEffect, useRef, useState } from 'react';
|
|
8
8
|
import { commitMutation, graphql, useFragment, useRelayEnvironment } from 'react-relay';
|
|
9
9
|
import { CardElement } from '@stripe/react-stripe-js';
|
|
10
|
+
import { trackPaymentInfoEntered } from './storefrontEvents.js';
|
|
10
11
|
import { CreditCard, User, Check, License, Receipt, X } from '../Icons/index.js';
|
|
11
12
|
import AddressFields from './AddressFields.js';
|
|
12
13
|
import CustomerFields from './CustomerFields.js';
|
|
@@ -14,6 +15,7 @@ import CheckoutSteps from './CheckoutSteps.js';
|
|
|
14
15
|
import { useElements, useStripe } from '@stripe/react-stripe-js';
|
|
15
16
|
import ConfigContext from '../ConfigContext.js';
|
|
16
17
|
import { parseCustomProperties } from '../../utils.js';
|
|
18
|
+
import { validateAddressElements } from '../../stripeElements.js';
|
|
17
19
|
const MOBILE = 'mobile';
|
|
18
20
|
const DESKTOP = 'desktop';
|
|
19
21
|
|
|
@@ -98,6 +100,7 @@ const Checkout = _ref => {
|
|
|
98
100
|
onScrollToTop();
|
|
99
101
|
}, [checkoutStep]);
|
|
100
102
|
const environment = useRelayEnvironment();
|
|
103
|
+
const paymentInfoTracked = useRef(false);
|
|
101
104
|
const [errorsObject, setErrorsObject] = useState(null);
|
|
102
105
|
const [customerErrorsObject, setCustomerErrorsObject] = useState(null);
|
|
103
106
|
const onCompleted = (res, errors, onSuccess) => {
|
|
@@ -153,14 +156,13 @@ const Checkout = _ref => {
|
|
|
153
156
|
elements = useElements();
|
|
154
157
|
}
|
|
155
158
|
const handleSubmit = async () => {
|
|
156
|
-
// Validate Stripe
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
}
|
|
159
|
+
// Validate the Stripe address first (for Billing and Licensee steps). Not
|
|
160
|
+
// `elements.submit()`: the order may not have a PaymentIntent yet, and
|
|
161
|
+
// without one Stripe.js throws.
|
|
162
|
+
if ((checkoutStep === CheckoutSteps.Billing || checkoutStep === CheckoutSteps.Licensee && !licenseeIsBillingIdentity) && !(await validateAddressElements(elements))) {
|
|
163
|
+
// Stripe shows the validation errors in the UI
|
|
164
|
+
onScrollToTop();
|
|
165
|
+
return;
|
|
164
166
|
}
|
|
165
167
|
setSubmitting(true);
|
|
166
168
|
setError(null);
|
|
@@ -454,6 +456,14 @@ const Checkout = _ref => {
|
|
|
454
456
|
style: inputStyles,
|
|
455
457
|
hidePostalCode: true
|
|
456
458
|
}
|
|
459
|
+
// The first time the card details are complete. The server
|
|
460
|
+
// keeps it to one per order; the ref only saves requests.
|
|
461
|
+
,
|
|
462
|
+
onChange: event => {
|
|
463
|
+
if (!event.complete || paymentInfoTracked.current) return;
|
|
464
|
+
paymentInfoTracked.current = true;
|
|
465
|
+
trackPaymentInfoEntered(environment);
|
|
466
|
+
}
|
|
457
467
|
})), errorsObject && errorsObject.stripePaymentMethodId && errorsObject.stripePaymentMethodId.length > 0 && /*#__PURE__*/React.createElement("p", {
|
|
458
468
|
className: "checkout-modal__section__errors"
|
|
459
469
|
}, errorsObject.stripePaymentMethodId[0]));
|