fontdue-js 3.0.0-alpha8 → 3.0.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 +23 -0
- package/README.md +253 -1
- package/dist/__generated__/CartOrderCompleteOrderMutation.graphql.d.ts +1 -1
- package/dist/__generated__/CartOrderCompleteOrderMutation.graphql.js +9 -3
- package/dist/__generated__/CartOrderRemoveDiscountMutation.graphql.d.ts +1 -1
- package/dist/__generated__/CartOrderRemoveDiscountMutation.graphql.js +9 -3
- package/dist/__generated__/CartOrderUpdateMutation.graphql.d.ts +1 -1
- package/dist/__generated__/CartOrderUpdateMutation.graphql.js +9 -3
- package/dist/__generated__/CartQuery.graphql.d.ts +1 -1
- package/dist/__generated__/CartQuery.graphql.js +9 -3
- package/dist/__generated__/CartStateUpdateMutation.graphql.d.ts +1 -1
- package/dist/__generated__/CartStateUpdateMutation.graphql.js +9 -3
- package/dist/__generated__/CharacterViewerIDQuery.graphql.d.ts +1 -1
- package/dist/__generated__/CharacterViewerIDQuery.graphql.js +9 -3
- package/dist/__generated__/CharacterViewerSlugQuery.graphql.d.ts +1 -1
- package/dist/__generated__/CharacterViewerSlugQuery.graphql.js +9 -3
- package/dist/__generated__/CharacterViewerStyleRefetchQuery.graphql.d.ts +1 -1
- package/dist/__generated__/CharacterViewerStyleRefetchQuery.graphql.js +9 -3
- package/dist/__generated__/CheckoutUpdateCustomerMutation.graphql.d.ts +1 -1
- package/dist/__generated__/CheckoutUpdateCustomerMutation.graphql.js +9 -3
- package/dist/__generated__/CheckoutUpdateOrderMutation.graphql.d.ts +1 -1
- package/dist/__generated__/CheckoutUpdateOrderMutation.graphql.js +9 -3
- package/dist/__generated__/CollectionAa_Query.graphql.d.ts +1 -1
- package/dist/__generated__/CollectionAa_Query.graphql.js +9 -3
- package/dist/__generated__/FontFamiliesQuery.graphql.d.ts +1 -1
- package/dist/__generated__/FontFamiliesQuery.graphql.js +9 -3
- package/dist/__generated__/FontdueAdminToolbarQuery.graphql.d.ts +20 -0
- package/dist/__generated__/FontdueAdminToolbarQuery.graphql.js +80 -0
- package/dist/__generated__/FontdueAdminToolbarTokenMutation.graphql.d.ts +18 -0
- package/dist/__generated__/FontdueAdminToolbarTokenMutation.graphql.js +56 -0
- package/dist/__generated__/PrecartAddToCartMutation.graphql.d.ts +1 -1
- package/dist/__generated__/PrecartAddToCartMutation.graphql.js +9 -3
- package/dist/__generated__/StoreModalCartQuery.graphql.d.ts +1 -1
- package/dist/__generated__/StoreModalCartQuery.graphql.js +9 -3
- package/dist/__generated__/StoreModalContainerQuery.graphql.d.ts +1 -1
- package/dist/__generated__/StoreModalContainerQuery.graphql.js +9 -3
- package/dist/__generated__/StoreModalIndexQuery.graphql.d.ts +1 -1
- package/dist/__generated__/StoreModalIndexQuery.graphql.js +9 -3
- package/dist/__generated__/StoreModalProductQuery.graphql.d.ts +1 -1
- package/dist/__generated__/StoreModalProductQuery.graphql.js +9 -3
- package/dist/__generated__/StoreModalProductRefetchQuery.graphql.d.ts +1 -1
- package/dist/__generated__/StoreModalProductRefetchQuery.graphql.js +9 -3
- package/dist/__generated__/TestFontsFormUpdateCustomerMutation.graphql.d.ts +1 -1
- package/dist/__generated__/TestFontsFormUpdateCustomerMutation.graphql.js +9 -3
- package/dist/__generated__/TypeTesterStandaloneChangedStylesQuery.graphql.d.ts +1 -1
- package/dist/__generated__/TypeTesterStandaloneChangedStylesQuery.graphql.js +9 -3
- package/dist/__generated__/TypeTesterStandaloneQuery.graphql.d.ts +1 -1
- package/dist/__generated__/TypeTesterStandaloneQuery.graphql.js +9 -3
- package/dist/__generated__/TypeTestersChangedStylesQuery.graphql.d.ts +1 -1
- package/dist/__generated__/TypeTestersChangedStylesQuery.graphql.js +9 -3
- package/dist/__generated__/TypeTestersIDQuery.graphql.d.ts +1 -1
- package/dist/__generated__/TypeTestersIDQuery.graphql.js +9 -3
- package/dist/__generated__/TypeTestersRefetchQuery.graphql.d.ts +1 -1
- package/dist/__generated__/TypeTestersRefetchQuery.graphql.js +9 -3
- package/dist/__generated__/TypeTestersSlugQuery.graphql.d.ts +1 -1
- package/dist/__generated__/TypeTestersSlugQuery.graphql.js +9 -3
- package/dist/__generated__/orderTrackingUpdateOrderTrackingMutation.graphql.js +1 -8
- package/dist/__generated__/useFontStyle_fontStyle.graphql.d.ts +2 -1
- package/dist/__generated__/useFontStyle_fontStyle.graphql.js +8 -2
- package/dist/__tests__/createFontdueFetch.test.js +276 -0
- package/dist/__tests__/imageLoader.test.js +62 -0
- package/dist/__tests__/metricFallback.test.js +74 -0
- package/dist/__tests__/networkFetch.test.js +188 -0
- package/dist/__tests__/nextAdapter.test.js +273 -18
- package/dist/__tests__/preview.test.js +217 -0
- package/dist/__tests__/previewServer.test.js +118 -0
- package/dist/__tests__/previewState.test.js +63 -0
- package/dist/__tests__/serverConfig.test.js +62 -0
- package/dist/components/BuyButton/index.d.ts +2 -2
- package/dist/components/BuyButton/index.js +3 -3
- package/dist/components/Cart/CartOrder.js +9 -1
- package/dist/components/Cart/orderTracking.js +8 -15
- package/dist/components/CharacterViewer/index.d.ts +2 -2
- package/dist/components/CharacterViewer/index.js +20 -11
- package/dist/components/ConfigContext.d.ts +21 -2
- package/dist/components/ConfigContext.js +12 -2
- package/dist/components/ConnectionErrorToolbar.d.ts +1 -0
- package/dist/components/ConnectionErrorToolbar.js +106 -0
- package/dist/components/FontStyle/index.d.ts +2 -0
- package/dist/components/FontStyle/index.js +4 -2
- package/dist/components/FontdueAdminToolbar/index.d.ts +2 -0
- package/dist/components/FontdueAdminToolbar/index.js +299 -0
- package/dist/components/FontdueAdminToolbar/previewState.d.ts +7 -0
- package/dist/components/FontdueAdminToolbar/previewState.js +58 -0
- package/dist/components/FontdueContextProvider/index.js +4 -2
- package/dist/components/FontdueProvider/index.js +6 -1
- package/dist/components/FontdueProvider/index.server.d.ts +1 -0
- package/dist/components/FontdueProvider/index.server.js +10 -0
- package/dist/components/NewsletterSignup/index.d.ts +2 -2
- package/dist/components/NewsletterSignup/index.js +2 -2
- package/dist/components/Root/index.js +16 -2
- package/dist/components/TestFontsForm/index.d.ts +2 -2
- package/dist/components/TestFontsForm/index.js +2 -2
- package/dist/components/TypeTester/TypeTesterStandalone.d.ts +2 -2
- package/dist/components/TypeTester/TypeTesterStandalone.js +2 -2
- package/dist/components/TypeTester/index.js +3 -1
- package/dist/components/TypeTester/useTypeTesterStyler.d.ts +3 -1
- package/dist/components/TypeTester/useTypeTesterStyler.js +70 -20
- package/dist/components/TypeTesters/index.d.ts +2 -2
- package/dist/components/TypeTesters/index.js +3 -3
- package/dist/components/elements/StoreModalUnifiedCheckout.js +8 -0
- package/dist/components/useFontStyle.d.ts +8 -0
- package/dist/components/useFontStyle.js +14 -4
- package/dist/corsError.d.ts +1 -5
- package/dist/corsError.js +23 -13
- package/dist/data/unicodeNamesUrl.d.ts +2 -0
- package/dist/data/unicodeNamesUrl.js +18 -0
- package/dist/data/unicodeNamesVersion.d.ts +1 -0
- package/dist/data/unicodeNamesVersion.js +4 -0
- package/dist/fallbackFontData.d.ts +2 -0
- package/dist/fallbackFontData.js +10 -0
- package/dist/fontdue.css +231 -4
- package/dist/loadFontdueProviderQuery.d.ts +2 -1
- package/dist/loadFontdueProviderQuery.js +5 -2
- package/dist/metricFallback.d.ts +48 -0
- package/dist/metricFallback.js +98 -0
- package/dist/next/config.d.ts +1 -5
- package/dist/next/config.js +14 -1
- package/dist/next/image-loader.js +22 -3
- package/dist/next/index.d.ts +1 -2
- package/dist/next/index.js +14 -6
- package/dist/next/registerSingleTenantResolver.d.ts +1 -0
- package/dist/next/registerSingleTenantResolver.js +35 -0
- package/dist/next/revalidate.js +1 -1
- package/dist/next/tenant.d.ts +10 -2
- package/dist/next/tenant.js +111 -16
- package/dist/preview/constants.d.ts +9 -0
- package/dist/preview/constants.js +117 -0
- package/dist/preview/index.d.ts +53 -0
- package/dist/preview/index.js +190 -0
- package/dist/preview/server.d.ts +20 -0
- package/dist/preview/server.js +89 -0
- package/dist/relay/environment.d.ts +8 -0
- package/dist/relay/environment.js +81 -25
- package/dist/relay/loadSerializableQuery.d.ts +13 -3
- package/dist/relay/loadSerializableQuery.js +2 -0
- package/dist/relay/serverConfig.d.ts +5 -1
- package/dist/relay/serverConfig.js +83 -8
- package/dist/scripts/publishUnicodeData.js +68 -0
- package/dist/scripts/updateUnicodeData.js +41 -6
- package/dist/server/index.d.ts +37 -0
- package/dist/server/index.js +160 -0
- package/package.json +5 -1
- package/types/next-headers.d.ts +9 -0
- package/types/next-navigation.d.ts +10 -0
- package/vitest.config.ts +9 -0
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import { describe, it, expect, vi, afterEach } from 'vitest';
|
|
2
|
+
import { handlePreviewRequest, readPreviewToken, previewAuthHeaders, PREVIEW_TOKEN_COOKIE, PREVIEW_MARKER_COOKIE } from '../preview/index.js';
|
|
3
|
+
import { setPreviewMarkerCookie, hasPreviewMarkerCookie, getPreviewExpiry } from '../preview/constants.js';
|
|
4
|
+
function postPreview(token) {
|
|
5
|
+
let url = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 'https://site.test/api/preview';
|
|
6
|
+
return handlePreviewRequest(new Request(url, {
|
|
7
|
+
method: 'POST',
|
|
8
|
+
headers: {
|
|
9
|
+
'content-type': 'application/json'
|
|
10
|
+
},
|
|
11
|
+
body: JSON.stringify({
|
|
12
|
+
token
|
|
13
|
+
})
|
|
14
|
+
}));
|
|
15
|
+
}
|
|
16
|
+
describe('handlePreviewRequest', () => {
|
|
17
|
+
it('enters preview: sets an httpOnly token cookie + a readable marker', async () => {
|
|
18
|
+
const before = Date.now();
|
|
19
|
+
const res = await postPreview('admin.tok.123');
|
|
20
|
+
expect(res.status).toBe(200);
|
|
21
|
+
const body = await res.json();
|
|
22
|
+
expect(body.ok).toBe(true);
|
|
23
|
+
expect(body.preview).toBe(true);
|
|
24
|
+
// No expiresAt in the body → defaults to now + the 1h TTL.
|
|
25
|
+
expect(body.expiresAt).toBeGreaterThanOrEqual(before + 60 * 60 * 1000 - 5_000);
|
|
26
|
+
const cookies = res.headers.getSetCookie();
|
|
27
|
+
const tokenCookie = cookies.find(c => c.startsWith(`${PREVIEW_TOKEN_COOKIE}=`));
|
|
28
|
+
const markerCookie = cookies.find(c => c.startsWith(`${PREVIEW_MARKER_COOKIE}=`));
|
|
29
|
+
expect(tokenCookie).toContain(`${PREVIEW_TOKEN_COOKIE}=admin.tok.123`);
|
|
30
|
+
expect(tokenCookie).toContain('HttpOnly');
|
|
31
|
+
expect(tokenCookie).toContain('SameSite=Lax');
|
|
32
|
+
expect(tokenCookie).toContain('Path=/');
|
|
33
|
+
|
|
34
|
+
// The marker now carries the token's expiry (epoch-ms), and outlives the
|
|
35
|
+
// token via a grace window so the toolbar can reach the "expired" warning.
|
|
36
|
+
expect(markerCookie).toContain(`${PREVIEW_MARKER_COOKIE}=${body.expiresAt}`);
|
|
37
|
+
expect(markerCookie).toMatch(/Max-Age=\d+/);
|
|
38
|
+
// The marker must be readable by the client toolbar.
|
|
39
|
+
expect(markerCookie).not.toContain('HttpOnly');
|
|
40
|
+
});
|
|
41
|
+
it('threads an explicit expiresAt (ISO + epoch-ms) into the marker + body', async () => {
|
|
42
|
+
const iso = '2030-01-01T00:00:00.000Z';
|
|
43
|
+
const epoch = Date.parse(iso);
|
|
44
|
+
const fromIso = await handlePreviewRequest(new Request('https://site.test/api/preview', {
|
|
45
|
+
method: 'POST',
|
|
46
|
+
headers: {
|
|
47
|
+
'content-type': 'application/json'
|
|
48
|
+
},
|
|
49
|
+
body: JSON.stringify({
|
|
50
|
+
token: 't',
|
|
51
|
+
expiresAt: iso
|
|
52
|
+
})
|
|
53
|
+
}));
|
|
54
|
+
expect((await fromIso.json()).expiresAt).toBe(epoch);
|
|
55
|
+
const fromEpoch = await handlePreviewRequest(new Request('https://site.test/api/preview', {
|
|
56
|
+
method: 'POST',
|
|
57
|
+
headers: {
|
|
58
|
+
'content-type': 'application/json'
|
|
59
|
+
},
|
|
60
|
+
body: JSON.stringify({
|
|
61
|
+
token: 't',
|
|
62
|
+
expiresAt: epoch
|
|
63
|
+
})
|
|
64
|
+
}));
|
|
65
|
+
expect((await fromEpoch.json()).expiresAt).toBe(epoch);
|
|
66
|
+
const marker = fromEpoch.headers.getSetCookie().find(c => c.startsWith(`${PREVIEW_MARKER_COOKIE}=`));
|
|
67
|
+
expect(marker).toContain(`${PREVIEW_MARKER_COOKIE}=${epoch}`);
|
|
68
|
+
});
|
|
69
|
+
it('marks cookies Secure on https and not on http (local dev)', async () => {
|
|
70
|
+
const httpsCookies = (await postPreview('t')).headers.getSetCookie();
|
|
71
|
+
expect(httpsCookies.every(c => c.includes('Secure'))).toBe(true);
|
|
72
|
+
const httpCookies = (await postPreview('t', 'http://localhost:4321/api/preview')).headers.getSetCookie();
|
|
73
|
+
expect(httpCookies.some(c => c.includes('Secure'))).toBe(false);
|
|
74
|
+
});
|
|
75
|
+
it('rejects a POST with no token', async () => {
|
|
76
|
+
const res = await postPreview('');
|
|
77
|
+
expect(res.status).toBe(400);
|
|
78
|
+
expect(res.headers.getSetCookie()).toHaveLength(0);
|
|
79
|
+
});
|
|
80
|
+
it('exits preview: clears both cookies', async () => {
|
|
81
|
+
const res = await handlePreviewRequest(new Request('https://site.test/api/preview', {
|
|
82
|
+
method: 'DELETE'
|
|
83
|
+
}));
|
|
84
|
+
expect(res.status).toBe(200);
|
|
85
|
+
expect(await res.json()).toEqual({
|
|
86
|
+
ok: true,
|
|
87
|
+
preview: false
|
|
88
|
+
});
|
|
89
|
+
const cookies = res.headers.getSetCookie();
|
|
90
|
+
expect(cookies).toHaveLength(2);
|
|
91
|
+
// Expiry via Max-Age=0 clears the cookie.
|
|
92
|
+
expect(cookies.every(c => c.includes('Max-Age=0'))).toBe(true);
|
|
93
|
+
});
|
|
94
|
+
it('rejects unsupported methods', async () => {
|
|
95
|
+
const res = await handlePreviewRequest(new Request('https://site.test/api/preview', {
|
|
96
|
+
method: 'GET'
|
|
97
|
+
}));
|
|
98
|
+
expect(res.status).toBe(405);
|
|
99
|
+
});
|
|
100
|
+
it('round-trips a token with cookie-unsafe characters', async () => {
|
|
101
|
+
const token = 'tok+en/with=specials';
|
|
102
|
+
const res = await postPreview(token);
|
|
103
|
+
const setCookie = res.headers.getSetCookie().find(c => c.startsWith(`${PREVIEW_TOKEN_COOKIE}=`));
|
|
104
|
+
// Reconstruct the Cookie header the browser would send back (name=value).
|
|
105
|
+
const cookieHeader = setCookie.split(';')[0];
|
|
106
|
+
expect(readPreviewToken(cookieHeader)).toBe(token);
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
describe('readPreviewToken', () => {
|
|
110
|
+
it('extracts the token from a multi-cookie header', () => {
|
|
111
|
+
expect(readPreviewToken(`a=1; ${PREVIEW_TOKEN_COOKIE}=abc; ${PREVIEW_MARKER_COOKIE}=1`)).toBe('abc');
|
|
112
|
+
});
|
|
113
|
+
it('returns undefined when there is no token cookie', () => {
|
|
114
|
+
expect(readPreviewToken('a=1; b=2')).toBeUndefined();
|
|
115
|
+
expect(readPreviewToken('')).toBeUndefined();
|
|
116
|
+
expect(readPreviewToken(null)).toBeUndefined();
|
|
117
|
+
expect(readPreviewToken(undefined)).toBeUndefined();
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
describe('setPreviewMarkerCookie (client-only embeds)', () => {
|
|
121
|
+
afterEach(() => vi.unstubAllGlobals());
|
|
122
|
+
|
|
123
|
+
// Captures the last `document.cookie = ...` write through a setter, the way
|
|
124
|
+
// the browser would receive it.
|
|
125
|
+
function stubBrowser(protocol) {
|
|
126
|
+
const state = {
|
|
127
|
+
written: ''
|
|
128
|
+
};
|
|
129
|
+
vi.stubGlobal('document', {
|
|
130
|
+
set cookie(value) {
|
|
131
|
+
state.written = value;
|
|
132
|
+
},
|
|
133
|
+
get cookie() {
|
|
134
|
+
return state.written;
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
vi.stubGlobal('location', {
|
|
138
|
+
protocol
|
|
139
|
+
});
|
|
140
|
+
return state;
|
|
141
|
+
}
|
|
142
|
+
it('is a no-op during server rendering (no document)', () => {
|
|
143
|
+
// The node test env has no `document`, so this must stay inert, not throw.
|
|
144
|
+
expect(() => setPreviewMarkerCookie(true)).not.toThrow();
|
|
145
|
+
});
|
|
146
|
+
it('sets a JS-readable marker carrying the expiry, Secure on https', () => {
|
|
147
|
+
const state = stubBrowser('https:');
|
|
148
|
+
const expiresAt = Date.now() + 60 * 60 * 1000;
|
|
149
|
+
setPreviewMarkerCookie(expiresAt);
|
|
150
|
+
expect(state.written).toContain(`${PREVIEW_MARKER_COOKIE}=${expiresAt}`);
|
|
151
|
+
expect(state.written).toContain('Path=/');
|
|
152
|
+
expect(state.written).toContain('SameSite=Lax');
|
|
153
|
+
expect(state.written).toContain('Secure');
|
|
154
|
+
// A positive Max-Age (not a clear) that outlives the token.
|
|
155
|
+
expect(state.written).toMatch(/Max-Age=\d+/);
|
|
156
|
+
expect(state.written).not.toContain('Max-Age=0');
|
|
157
|
+
});
|
|
158
|
+
it('defaults to the 1h TTL when passed a bare true', () => {
|
|
159
|
+
const state = stubBrowser('https:');
|
|
160
|
+
const before = Date.now();
|
|
161
|
+
setPreviewMarkerCookie(true);
|
|
162
|
+
const match = state.written.match(new RegExp(`${PREVIEW_MARKER_COOKIE}=(\\d+)`));
|
|
163
|
+
expect(match).not.toBeNull();
|
|
164
|
+
expect(Number(match[1])).toBeGreaterThanOrEqual(before + 60 * 60 * 1000 - 5_000);
|
|
165
|
+
});
|
|
166
|
+
it('omits Secure on http so it persists in local dev', () => {
|
|
167
|
+
const state = stubBrowser('http:');
|
|
168
|
+
setPreviewMarkerCookie(Date.now() + 60 * 60 * 1000);
|
|
169
|
+
expect(state.written).not.toContain('Secure');
|
|
170
|
+
});
|
|
171
|
+
it('clears the marker with Max-Age=0 on exit', () => {
|
|
172
|
+
const state = stubBrowser('https:');
|
|
173
|
+
setPreviewMarkerCookie(false);
|
|
174
|
+
expect(state.written).toContain(`${PREVIEW_MARKER_COOKIE}=`);
|
|
175
|
+
expect(state.written).toContain('Max-Age=0');
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
describe('previewAuthHeaders', () => {
|
|
179
|
+
it('builds a Bearer header when a token is present', () => {
|
|
180
|
+
expect(previewAuthHeaders('abc')).toEqual({
|
|
181
|
+
authorization: 'Bearer abc'
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
it('returns an empty object when there is no token (safe to spread)', () => {
|
|
185
|
+
expect(previewAuthHeaders(undefined)).toEqual({});
|
|
186
|
+
expect(previewAuthHeaders(null)).toEqual({});
|
|
187
|
+
expect(previewAuthHeaders('')).toEqual({});
|
|
188
|
+
});
|
|
189
|
+
});
|
|
190
|
+
describe('marker cookie readers (hasPreviewMarkerCookie / getPreviewExpiry)', () => {
|
|
191
|
+
afterEach(() => vi.unstubAllGlobals());
|
|
192
|
+
function stubCookies(cookie) {
|
|
193
|
+
vi.stubGlobal('document', {
|
|
194
|
+
cookie
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
it('reads presence + expiry from an expiry-encoded marker', () => {
|
|
198
|
+
stubCookies(`a=1; ${PREVIEW_MARKER_COOKIE}=1781700000000; b=2`);
|
|
199
|
+
expect(hasPreviewMarkerCookie()).toBe(true);
|
|
200
|
+
expect(getPreviewExpiry()).toBe(1781700000000);
|
|
201
|
+
});
|
|
202
|
+
it('treats a legacy `=1` marker as present, expiry 1ms (always expired)', () => {
|
|
203
|
+
stubCookies(`${PREVIEW_MARKER_COOKIE}=1`);
|
|
204
|
+
expect(hasPreviewMarkerCookie()).toBe(true);
|
|
205
|
+
expect(getPreviewExpiry()).toBe(1);
|
|
206
|
+
});
|
|
207
|
+
it('reports absent when the marker cookie is missing', () => {
|
|
208
|
+
stubCookies('a=1; b=2');
|
|
209
|
+
expect(hasPreviewMarkerCookie()).toBe(false);
|
|
210
|
+
expect(getPreviewExpiry()).toBeUndefined();
|
|
211
|
+
});
|
|
212
|
+
it('returns undefined expiry for a non-numeric marker value', () => {
|
|
213
|
+
stubCookies(`${PREVIEW_MARKER_COOKIE}=bogus`);
|
|
214
|
+
expect(hasPreviewMarkerCookie()).toBe(true);
|
|
215
|
+
expect(getPreviewExpiry()).toBeUndefined();
|
|
216
|
+
});
|
|
217
|
+
});
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
2
|
+
import { runWithPreview, isPreviewing, ambientPreviewHeaders } from '../preview/server.js';
|
|
3
|
+
import { createFontdueFetch } from '../server/index.js';
|
|
4
|
+
import { PREVIEW_TOKEN_COOKIE } from '../preview/constants.js';
|
|
5
|
+
beforeEach(() => {
|
|
6
|
+
vi.unstubAllGlobals();
|
|
7
|
+
});
|
|
8
|
+
function req(token) {
|
|
9
|
+
return new Request('https://store.test/', {
|
|
10
|
+
headers: token ? {
|
|
11
|
+
cookie: `${PREVIEW_TOKEN_COOKIE}=${token}`
|
|
12
|
+
} : {}
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
describe('runWithPreview', () => {
|
|
16
|
+
it('passes public (no-token) requests through untouched and cacheable', async () => {
|
|
17
|
+
const response = await runWithPreview(req(), async () => {
|
|
18
|
+
expect(isPreviewing()).toBe(false);
|
|
19
|
+
return new Response('ok', {
|
|
20
|
+
headers: {
|
|
21
|
+
'Netlify-CDN-Cache-Control': 'public, durable, s-maxage=300'
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
expect(response.headers.get('Cache-Control')).toBeNull();
|
|
26
|
+
expect(response.headers.get('Netlify-CDN-Cache-Control')).toBe('public, durable, s-maxage=300');
|
|
27
|
+
});
|
|
28
|
+
it('exposes the token in ambient context while previewing', async () => {
|
|
29
|
+
await runWithPreview(req('tok123'), async () => {
|
|
30
|
+
expect(isPreviewing()).toBe(true);
|
|
31
|
+
expect(ambientPreviewHeaders()).toEqual({
|
|
32
|
+
authorization: 'Bearer tok123'
|
|
33
|
+
});
|
|
34
|
+
return new Response('ok');
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
// Context unwinds after the request.
|
|
38
|
+
expect(isPreviewing()).toBe(false);
|
|
39
|
+
expect(ambientPreviewHeaders()).toEqual({});
|
|
40
|
+
});
|
|
41
|
+
it('forces preview responses out of any shared/CDN cache', async () => {
|
|
42
|
+
const response = await runWithPreview(req('tok123'), async () => new Response('secret', {
|
|
43
|
+
headers: {
|
|
44
|
+
'Netlify-CDN-Cache-Control': 'public, durable, s-maxage=31536000',
|
|
45
|
+
'CDN-Cache-Control': 'public, s-maxage=300',
|
|
46
|
+
'Cache-Control': 'public, max-age=0, must-revalidate'
|
|
47
|
+
}
|
|
48
|
+
}));
|
|
49
|
+
expect(response.headers.get('Cache-Control')).toBe('private, no-store');
|
|
50
|
+
expect(response.headers.get('Netlify-CDN-Cache-Control')).toBeNull();
|
|
51
|
+
expect(response.headers.get('CDN-Cache-Control')).toBeNull();
|
|
52
|
+
});
|
|
53
|
+
it('isolates concurrent preview contexts (no cross-request leak)', async () => {
|
|
54
|
+
const seen = {};
|
|
55
|
+
await Promise.all([runWithPreview(req('tokA'), async () => {
|
|
56
|
+
await new Promise(r => setTimeout(r, 15));
|
|
57
|
+
seen.a = ambientPreviewHeaders().authorization;
|
|
58
|
+
return new Response('a');
|
|
59
|
+
}), runWithPreview(req('tokB'), async () => {
|
|
60
|
+
seen.b = ambientPreviewHeaders().authorization;
|
|
61
|
+
return new Response('b');
|
|
62
|
+
}), runWithPreview(req(), async () => {
|
|
63
|
+
seen.public = ambientPreviewHeaders().authorization;
|
|
64
|
+
return new Response('public');
|
|
65
|
+
})]);
|
|
66
|
+
expect(seen.a).toBe('Bearer tokA');
|
|
67
|
+
expect(seen.b).toBe('Bearer tokB');
|
|
68
|
+
expect(seen.public).toBeUndefined();
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
describe('createFontdueFetch + ambient preview', () => {
|
|
72
|
+
function mockFetch() {
|
|
73
|
+
const fetchMock = vi.fn(async () => ({
|
|
74
|
+
status: 200,
|
|
75
|
+
json: async () => ({
|
|
76
|
+
data: {}
|
|
77
|
+
})
|
|
78
|
+
}));
|
|
79
|
+
vi.stubGlobal('fetch', fetchMock);
|
|
80
|
+
return fetchMock;
|
|
81
|
+
}
|
|
82
|
+
it('a module-level fetcher forwards the ambient token, no per-call plumbing', async () => {
|
|
83
|
+
const fetchMock = mockFetch();
|
|
84
|
+
const fetchGraphql = createFontdueFetch({
|
|
85
|
+
url: 'https://acme.fontdue.com'
|
|
86
|
+
});
|
|
87
|
+
await runWithPreview(req('admin-tok'), async () => {
|
|
88
|
+
await fetchGraphql('Q', 'query Q { __typename }');
|
|
89
|
+
return new Response('ok');
|
|
90
|
+
});
|
|
91
|
+
const init = fetchMock.mock.calls[0][1];
|
|
92
|
+
expect(init.headers.authorization).toBe('Bearer admin-tok');
|
|
93
|
+
});
|
|
94
|
+
it('does not forward a token for public requests', async () => {
|
|
95
|
+
const fetchMock = mockFetch();
|
|
96
|
+
const fetchGraphql = createFontdueFetch({
|
|
97
|
+
url: 'https://acme.fontdue.com'
|
|
98
|
+
});
|
|
99
|
+
await fetchGraphql('Q', 'query Q { __typename }');
|
|
100
|
+
const init = fetchMock.mock.calls[0][1];
|
|
101
|
+
expect(init.headers.authorization).toBeUndefined();
|
|
102
|
+
});
|
|
103
|
+
it('explicit per-fetcher headers override the ambient context', async () => {
|
|
104
|
+
const fetchMock = mockFetch();
|
|
105
|
+
const fetchGraphql = createFontdueFetch({
|
|
106
|
+
url: 'https://acme.fontdue.com',
|
|
107
|
+
headers: {
|
|
108
|
+
authorization: 'Bearer explicit'
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
await runWithPreview(req('ambient-tok'), async () => {
|
|
112
|
+
await fetchGraphql('Q', 'query Q { __typename }');
|
|
113
|
+
return new Response('ok');
|
|
114
|
+
});
|
|
115
|
+
const init = fetchMock.mock.calls[0][1];
|
|
116
|
+
expect(init.headers.authorization).toBe('Bearer explicit');
|
|
117
|
+
});
|
|
118
|
+
});
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { derivePreviewState, expiresAtFromTokenResult, isSessionExpiredError } from '../components/FontdueAdminToolbar/previewState.js';
|
|
3
|
+
|
|
4
|
+
// The three-state derivation is the heart of the expired-preview fix, so it's
|
|
5
|
+
// tested in isolation (the full toolbar needs a browser + Relay environment +
|
|
6
|
+
// admin session to render, which isn't available here).
|
|
7
|
+
describe('derivePreviewState', () => {
|
|
8
|
+
const NOW = 1_000_000;
|
|
9
|
+
it('is "off" with no marker cookie', () => {
|
|
10
|
+
expect(derivePreviewState(false, undefined, NOW)).toBe('off');
|
|
11
|
+
expect(derivePreviewState(false, NOW + 1000, NOW)).toBe('off');
|
|
12
|
+
});
|
|
13
|
+
it('is "active" while the marker is present and the token is still valid', () => {
|
|
14
|
+
expect(derivePreviewState(true, NOW + 1, NOW)).toBe('active');
|
|
15
|
+
});
|
|
16
|
+
it('is "expired" once now has reached the stored expiry', () => {
|
|
17
|
+
expect(derivePreviewState(true, NOW, NOW)).toBe('expired');
|
|
18
|
+
expect(derivePreviewState(true, NOW - 1, NOW)).toBe('expired');
|
|
19
|
+
});
|
|
20
|
+
it('is "expired" for a marker with no/garbled expiry (stale legacy cookie)', () => {
|
|
21
|
+
expect(derivePreviewState(true, undefined, NOW)).toBe('expired');
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
describe('expiresAtFromTokenResult', () => {
|
|
25
|
+
it('accepts an epoch-ms number', () => {
|
|
26
|
+
expect(expiresAtFromTokenResult({
|
|
27
|
+
expiresAt: 123_456
|
|
28
|
+
})).toBe(123_456);
|
|
29
|
+
});
|
|
30
|
+
it('accepts an ISO-8601 string', () => {
|
|
31
|
+
const iso = '2030-01-01T00:00:00.000Z';
|
|
32
|
+
expect(expiresAtFromTokenResult({
|
|
33
|
+
expiresAt: iso
|
|
34
|
+
})).toBe(Date.parse(iso));
|
|
35
|
+
});
|
|
36
|
+
it('accepts a numeric string', () => {
|
|
37
|
+
expect(expiresAtFromTokenResult({
|
|
38
|
+
expiresAt: '987654321'
|
|
39
|
+
})).toBe(987654321);
|
|
40
|
+
});
|
|
41
|
+
it('falls back to now + 1h TTL when the field is absent (backend not yet shipped)', () => {
|
|
42
|
+
const before = Date.now();
|
|
43
|
+
expect(expiresAtFromTokenResult({
|
|
44
|
+
token: 'abc'
|
|
45
|
+
})).toBeGreaterThanOrEqual(before + 60 * 60 * 1000 - 5_000);
|
|
46
|
+
expect(expiresAtFromTokenResult(null)).toBeGreaterThanOrEqual(before + 60 * 60 * 1000 - 5_000);
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
describe('isSessionExpiredError', () => {
|
|
50
|
+
it('matches the resolver’s "Not authorized" message', () => {
|
|
51
|
+
expect(isSessionExpiredError([{
|
|
52
|
+
message: 'Not authorized: you must be signed in as an admin.'
|
|
53
|
+
}])).toBe(true);
|
|
54
|
+
});
|
|
55
|
+
it('is false for unrelated errors and empty/absent error lists', () => {
|
|
56
|
+
expect(isSessionExpiredError([{
|
|
57
|
+
message: 'Something else failed'
|
|
58
|
+
}])).toBe(false);
|
|
59
|
+
expect(isSessionExpiredError([])).toBe(false);
|
|
60
|
+
expect(isSessionExpiredError(null)).toBe(false);
|
|
61
|
+
expect(isSessionExpiredError(undefined)).toBe(false);
|
|
62
|
+
});
|
|
63
|
+
});
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
2
|
+
|
|
3
|
+
// The render-scoped slot and the ambient resolver are anchored on globalThis so
|
|
4
|
+
// they survive this module being bundled into more than one Next.js server
|
|
5
|
+
// chunk (the app/provider chunk vs. a per-embed chunk), which would otherwise
|
|
6
|
+
// give each chunk its own copy of the module-level state. Re-importing the
|
|
7
|
+
// module after vi.resetModules() faithfully simulates that second chunk: a
|
|
8
|
+
// fresh module instance with its own scope but the same globalThis.
|
|
9
|
+
const STORE_KEY = '__fontdueServerConfigStore__';
|
|
10
|
+
function clearStore() {
|
|
11
|
+
delete globalThis[STORE_KEY];
|
|
12
|
+
}
|
|
13
|
+
beforeEach(() => {
|
|
14
|
+
vi.resetModules();
|
|
15
|
+
clearStore();
|
|
16
|
+
});
|
|
17
|
+
afterEach(clearStore);
|
|
18
|
+
describe('serverConfig store (chunk-duplication safety, FD-712)', () => {
|
|
19
|
+
it('shares the ambient resolver across module instances', async () => {
|
|
20
|
+
var _await$a$resolveFontd, _await$a$resolveFontd2, _await$b$resolveFontd, _await$b$resolveFontd2;
|
|
21
|
+
// First "chunk": register a resolver (what fontdue-js/next does when
|
|
22
|
+
// <FontdueProvider> mounts in single-tenant mode).
|
|
23
|
+
const a = await import("../relay/serverConfig.js");
|
|
24
|
+
a.registerAmbientConfigResolver(async () => ({
|
|
25
|
+
url: 'https://acme.fontdue.com',
|
|
26
|
+
headers: {
|
|
27
|
+
authorization: 'Bearer admin-tok'
|
|
28
|
+
}
|
|
29
|
+
}));
|
|
30
|
+
expect((_await$a$resolveFontd = await a.resolveFontdueServerConfig()) === null || _await$a$resolveFontd === void 0 ? void 0 : (_await$a$resolveFontd2 = _await$a$resolveFontd.headers) === null || _await$a$resolveFontd2 === void 0 ? void 0 : _await$a$resolveFontd2.authorization).toBe('Bearer admin-tok');
|
|
31
|
+
|
|
32
|
+
// Second "chunk": a fresh module instance. Before the fix its module-level
|
|
33
|
+
// `ambientResolver` was undefined here, so an embed preloading from this
|
|
34
|
+
// chunk fetched without the preview token and a hidden-font reveal failed.
|
|
35
|
+
vi.resetModules();
|
|
36
|
+
const b = await import("../relay/serverConfig.js");
|
|
37
|
+
expect(b).not.toBe(a); // genuinely a re-evaluated module
|
|
38
|
+
expect((_await$b$resolveFontd = await b.resolveFontdueServerConfig()) === null || _await$b$resolveFontd === void 0 ? void 0 : (_await$b$resolveFontd2 = _await$b$resolveFontd.headers) === null || _await$b$resolveFontd2 === void 0 ? void 0 : _await$b$resolveFontd2.authorization).toBe('Bearer admin-tok');
|
|
39
|
+
});
|
|
40
|
+
it('reuses one shared store object across re-imports', async () => {
|
|
41
|
+
const a = await import("../relay/serverConfig.js");
|
|
42
|
+
// The store is created lazily on first use, not on import.
|
|
43
|
+
a.registerAmbientConfigResolver(() => undefined);
|
|
44
|
+
const first = globalThis[STORE_KEY];
|
|
45
|
+
expect(first).toBeDefined();
|
|
46
|
+
vi.resetModules();
|
|
47
|
+
const b = await import("../relay/serverConfig.js");
|
|
48
|
+
await b.resolveFontdueServerConfig();
|
|
49
|
+
const second = globalThis[STORE_KEY];
|
|
50
|
+
|
|
51
|
+
// Same store (slot factory + resolver), not a per-instance copy — so the
|
|
52
|
+
// React.cache-backed slot multi-tenant uses (setFontdueServerConfig via
|
|
53
|
+
// __prepareFontdueRender) is one shared cell every chunk reads/writes.
|
|
54
|
+
expect(second).toBe(first);
|
|
55
|
+
});
|
|
56
|
+
it('starts with no resolver (resolution unchanged until something registers)', async () => {
|
|
57
|
+
const {
|
|
58
|
+
resolveFontdueServerConfig
|
|
59
|
+
} = await import("../relay/serverConfig.js");
|
|
60
|
+
expect(await resolveFontdueServerConfig()).toBeUndefined();
|
|
61
|
+
});
|
|
62
|
+
});
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import { BuyButtonIDQuery } from '../../__generated__/BuyButtonIDQuery.graphql.js';
|
|
3
3
|
import { BuyButtonSlugQuery } from '../../__generated__/BuyButtonSlugQuery.graphql.js';
|
|
4
|
-
import { SerializablePreloadedQuery } from '../../relay/loadSerializableQuery.js';
|
|
4
|
+
import { SerializablePreloadedQuery, type LoadQueryOptions } from '../../relay/loadSerializableQuery.js';
|
|
5
5
|
import { Config } from '../ConfigContext.js';
|
|
6
6
|
export interface BuyButton_props {
|
|
7
7
|
collectionName?: string;
|
|
@@ -15,7 +15,7 @@ export type LoadBuyButtonQueryVariables = {
|
|
|
15
15
|
collectionId?: never;
|
|
16
16
|
collectionSlug: string;
|
|
17
17
|
};
|
|
18
|
-
export declare function loadBuyButtonQuery(variables: LoadBuyButtonQueryVariables): Promise<BuyButtonPreloadedQuery>;
|
|
18
|
+
export declare function loadBuyButtonQuery(variables: LoadBuyButtonQueryVariables, options?: LoadQueryOptions): Promise<BuyButtonPreloadedQuery>;
|
|
19
19
|
export declare function BuyButtonPreloadedIDQueryRenderer({ preloadedQuery, ...rest }: BuyButton_props & {
|
|
20
20
|
preloadedQuery: SerializablePreloadedQuery<BuyButtonIDQuery>;
|
|
21
21
|
}): React.JSX.Element;
|
|
@@ -57,16 +57,16 @@ function BuyButtonComponent(_ref) {
|
|
|
57
57
|
}
|
|
58
58
|
const idQuery = (_BuyButtonIDQuery.hash && _BuyButtonIDQuery.hash !== "4fbf1dbf9e6c530a5d38c697b174d8b0" && console.error("The definition of 'BuyButtonIDQuery' appears to have changed. Run `relay-compiler` to update the generated files to receive the expected data."), _BuyButtonIDQuery);
|
|
59
59
|
const slugQuery = (_BuyButtonSlugQuery.hash && _BuyButtonSlugQuery.hash !== "6e750adea09698f7cb61f435cd88fd26" && console.error("The definition of 'BuyButtonSlugQuery' appears to have changed. Run `relay-compiler` to update the generated files to receive the expected data."), _BuyButtonSlugQuery);
|
|
60
|
-
export async function loadBuyButtonQuery(variables) {
|
|
60
|
+
export async function loadBuyButtonQuery(variables, options) {
|
|
61
61
|
if (variables.collectionId) {
|
|
62
62
|
return loadSerializableQuery(BuyButtonIDQueryNode, {
|
|
63
63
|
collectionId: variables.collectionId
|
|
64
|
-
});
|
|
64
|
+
}, options);
|
|
65
65
|
}
|
|
66
66
|
if (variables.collectionSlug) {
|
|
67
67
|
return loadSerializableQuery(BuyButtonSlugQueryNode, {
|
|
68
68
|
collectionSlug: variables.collectionSlug
|
|
69
|
-
});
|
|
69
|
+
}, options);
|
|
70
70
|
}
|
|
71
71
|
throw new Error('loadBuyButtonQuery expected either a collectionId or collectionSlug');
|
|
72
72
|
}
|
|
@@ -4,7 +4,7 @@ import _CartOrderRemoveDiscountMutation from "../../__generated__/CartOrderRemov
|
|
|
4
4
|
import _CartOrderUpdateMutation from "../../__generated__/CartOrderUpdateMutation.graphql.js";
|
|
5
5
|
import _CartOrder_UpdateErrors from "../../__generated__/CartOrder_UpdateErrors.graphql.js";
|
|
6
6
|
import _CartOrderCompleteOrderMutation from "../../__generated__/CartOrderCompleteOrderMutation.graphql.js";
|
|
7
|
-
import React, { Fragment, useRef, useState } from 'react';
|
|
7
|
+
import React, { Fragment, useEffect, useRef, useState } from 'react';
|
|
8
8
|
import { commitMutation, graphql, useFragment, useRelayEnvironment } from 'react-relay';
|
|
9
9
|
import Checkout from './Checkout.js';
|
|
10
10
|
import CheckoutSteps from './CheckoutSteps.js';
|
|
@@ -21,6 +21,7 @@ import { textVariablesAllHaveText } from './utils.js';
|
|
|
21
21
|
import { useStripe } from '@stripe/react-stripe-js';
|
|
22
22
|
import { useDispatch } from 'react-redux';
|
|
23
23
|
import CartState from './CartState.js';
|
|
24
|
+
import { sendOrderTracking } from './orderTracking.js';
|
|
24
25
|
|
|
25
26
|
// note here we're only updating the order (by id).
|
|
26
27
|
// careful not to update the current customer's reference
|
|
@@ -44,6 +45,13 @@ const CartOrder = _ref => {
|
|
|
44
45
|
const environment = useRelayEnvironment();
|
|
45
46
|
const stripe = useStripe();
|
|
46
47
|
const dispatch = useDispatch();
|
|
48
|
+
|
|
49
|
+
// Capture analytics consent + attribution on the order for the purchase
|
|
50
|
+
// event the server emits on completion (from a Stripe webhook, where no
|
|
51
|
+
// browser context exists).
|
|
52
|
+
useEffect(() => {
|
|
53
|
+
if (open) sendOrderTracking(environment);
|
|
54
|
+
}, [open, environment]);
|
|
47
55
|
const order = useFragment((_CartOrder_order.hash && _CartOrder_order.hash !== "25ef000c40000c4f76f973ac6ac7f593" && console.error("The definition of 'CartOrder_order' appears to have changed. Run `relay-compiler` to update the generated files to receive the expected data."), _CartOrder_order), orderKey);
|
|
48
56
|
const viewer = useFragment((_CartOrder_viewer.hash && _CartOrder_viewer.hash !== "8cf5ab5a33a474483a6c231cc44b0df1" && console.error("The definition of 'CartOrder_viewer' appears to have changed. Run `relay-compiler` to update the generated files to receive the expected data."), _CartOrder_viewer), viewerKey);
|
|
49
57
|
|
|
@@ -1,13 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
value: true
|
|
5
|
-
});
|
|
6
|
-
exports.sendOrderTracking = sendOrderTracking;
|
|
7
|
-
var _orderTrackingUpdateOrderTrackingMutation2 = _interopRequireDefault(require("../../__generated__/orderTrackingUpdateOrderTrackingMutation.graphql"));
|
|
8
|
-
var _reactRelay = require("react-relay");
|
|
9
|
-
var _consent = require("../ConsentBanner/consent");
|
|
10
|
-
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
|
|
1
|
+
import _orderTrackingUpdateOrderTrackingMutation from "../../__generated__/orderTrackingUpdateOrderTrackingMutation.graphql.js";
|
|
2
|
+
import { commitMutation, graphql } from 'react-relay';
|
|
3
|
+
import { hasConsent, getClientAnonymousId } from '../ConsentBanner/consent.js';
|
|
11
4
|
function readCookie(name) {
|
|
12
5
|
const match = document.cookie.match(new RegExp('(?:^|;\\s*)' + name + '=([^;]*)'));
|
|
13
6
|
return match ? decodeURIComponent(match[1]) : undefined;
|
|
@@ -21,14 +14,14 @@ function readCookie(name) {
|
|
|
21
14
|
*
|
|
22
15
|
* Fire-and-forget: tracking must never break checkout.
|
|
23
16
|
*/
|
|
24
|
-
function sendOrderTracking(environment) {
|
|
17
|
+
export function sendOrderTracking(environment) {
|
|
25
18
|
try {
|
|
26
|
-
|
|
27
|
-
mutation: (
|
|
19
|
+
commitMutation(environment, {
|
|
20
|
+
mutation: (_orderTrackingUpdateOrderTrackingMutation.hash && _orderTrackingUpdateOrderTrackingMutation.hash !== "d59a127a7f140424f507ae549731bac7" && console.error("The definition of 'orderTrackingUpdateOrderTrackingMutation' appears to have changed. Run `relay-compiler` to update the generated files to receive the expected data."), _orderTrackingUpdateOrderTrackingMutation),
|
|
28
21
|
variables: {
|
|
29
22
|
input: {
|
|
30
|
-
analyticsConsent:
|
|
31
|
-
anonymousId:
|
|
23
|
+
analyticsConsent: hasConsent('analytics'),
|
|
24
|
+
anonymousId: getClientAnonymousId(),
|
|
32
25
|
fbp: readCookie('_fbp'),
|
|
33
26
|
fbc: readCookie('_fbc'),
|
|
34
27
|
url: window.location.href
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
import { SerializablePreloadedQuery } from '../../relay/loadSerializableQuery.js';
|
|
2
|
+
import { SerializablePreloadedQuery, type LoadQueryOptions } from '../../relay/loadSerializableQuery.js';
|
|
3
3
|
import { CharacterViewerIDQuery } from '../../__generated__/CharacterViewerIDQuery.graphql.js';
|
|
4
4
|
import { CharacterViewerSlugQuery } from '../../__generated__/CharacterViewerSlugQuery.graphql.js';
|
|
5
5
|
import { Config } from '../ConfigContext.js';
|
|
@@ -13,7 +13,7 @@ export type LoadCharacterViewerQueryVariables = {
|
|
|
13
13
|
collectionId?: never;
|
|
14
14
|
collectionSlug: string;
|
|
15
15
|
};
|
|
16
|
-
export declare function loadCharacterViewerQuery(variables: LoadCharacterViewerQueryVariables): Promise<CharacterViewerPreloadedQuery>;
|
|
16
|
+
export declare function loadCharacterViewerQuery(variables: LoadCharacterViewerQueryVariables, options?: LoadQueryOptions): Promise<CharacterViewerPreloadedQuery>;
|
|
17
17
|
export declare function CharacterViewerPreloadedIDQueryRenderer({ preloadedQuery, ...rest }: CharacterViewer_props & {
|
|
18
18
|
preloadedQuery: SerializablePreloadedQuery<CharacterViewerIDQuery>;
|
|
19
19
|
}): React.JSX.Element | null;
|
|
@@ -6,7 +6,7 @@ import _CharacterViewerIDQuery from "../../__generated__/CharacterViewerIDQuery.
|
|
|
6
6
|
import _CharacterViewer_style from "../../__generated__/CharacterViewer_style.graphql.js";
|
|
7
7
|
import _CharacterViewer_collection from "../../__generated__/CharacterViewer_collection.graphql.js";
|
|
8
8
|
import _CharacterViewer_family from "../../__generated__/CharacterViewer_family.graphql.js";
|
|
9
|
-
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
9
|
+
import React, { useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react';
|
|
10
10
|
import { fetchQuery, graphql, useFragment, useLazyLoadQuery, usePreloadedQuery, useRefetchableFragment, useRelayEnvironment } from 'react-relay';
|
|
11
11
|
import useResizeObserver from '../../hooks/useResizeObserver.js';
|
|
12
12
|
import retryImport from '../../retryImport.js';
|
|
@@ -20,6 +20,8 @@ import StyleSelect from './StyleSelect.js';
|
|
|
20
20
|
import Checkbox from '../Checkbox/index.js';
|
|
21
21
|
import CharacterViewerStyleRefetchQueryNode from '../../__generated__/CharacterViewerStyleRefetchQuery.graphql.js';
|
|
22
22
|
import { EnsureFontdueContext } from '../FontdueContextProvider/index.js';
|
|
23
|
+
import ConfigContext from '../ConfigContext.js';
|
|
24
|
+
import { unicodeNamesUrl } from '../../data/unicodeNamesUrl.js';
|
|
23
25
|
function useSize() {
|
|
24
26
|
const ref = useRef(null);
|
|
25
27
|
const [size, setSize] = React.useState();
|
|
@@ -74,20 +76,27 @@ function GlyphMeta(_ref) {
|
|
|
74
76
|
onChange: e => setFeaturesChecked(e.target.checked)
|
|
75
77
|
}), glyph.features.join(', ')))));
|
|
76
78
|
}
|
|
79
|
+
|
|
80
|
+
// Glyph unicode names (~1.2 MB) are fetched lazily from the Fontdue CDN rather
|
|
81
|
+
// than bundled — see data/unicodeNamesUrl.ts. Purely cosmetic (the “Unicode
|
|
82
|
+
// name” label), so a failed fetch degrades silently. `retryImport` is reused
|
|
83
|
+
// here as a generic retry wrapper for transient network blips.
|
|
77
84
|
function useUnicodeData() {
|
|
85
|
+
const config = useContext(ConfigContext);
|
|
86
|
+
const url = unicodeNamesUrl(config.cdnUrl);
|
|
78
87
|
const [data, setData] = useState();
|
|
79
88
|
useEffect(() => {
|
|
80
|
-
function fetchData() {
|
|
81
|
-
retryImport(() => import('../../data/unicodeData.js')).then(data => {
|
|
82
|
-
if (!ignore) setData(data.default);
|
|
83
|
-
}).catch(() => {});
|
|
84
|
-
}
|
|
85
89
|
let ignore = false;
|
|
86
|
-
|
|
90
|
+
retryImport(() => fetch(url).then(res => {
|
|
91
|
+
if (!res.ok) throw new Error(`unicode names request failed: ${res.status}`);
|
|
92
|
+
return res.json();
|
|
93
|
+
})).then(json => {
|
|
94
|
+
if (!ignore) setData(json);
|
|
95
|
+
}).catch(() => {});
|
|
87
96
|
return () => {
|
|
88
97
|
ignore = true;
|
|
89
98
|
};
|
|
90
|
-
}, []);
|
|
99
|
+
}, [url]);
|
|
91
100
|
return data;
|
|
92
101
|
}
|
|
93
102
|
function compareGlyphs(a, b) {
|
|
@@ -416,16 +425,16 @@ function CharacterViewerComponent(_ref3) {
|
|
|
416
425
|
}
|
|
417
426
|
const idQuery = (_CharacterViewerIDQuery.hash && _CharacterViewerIDQuery.hash !== "f90b09a4df6d95307b0a5d5fda487cdc" && console.error("The definition of 'CharacterViewerIDQuery' appears to have changed. Run `relay-compiler` to update the generated files to receive the expected data."), _CharacterViewerIDQuery);
|
|
418
427
|
const slugQuery = (_CharacterViewerSlugQuery.hash && _CharacterViewerSlugQuery.hash !== "afa08a8f050e0434308892fea6e3c267" && console.error("The definition of 'CharacterViewerSlugQuery' appears to have changed. Run `relay-compiler` to update the generated files to receive the expected data."), _CharacterViewerSlugQuery);
|
|
419
|
-
export async function loadCharacterViewerQuery(variables) {
|
|
428
|
+
export async function loadCharacterViewerQuery(variables, options) {
|
|
420
429
|
if (variables.collectionId) {
|
|
421
430
|
return loadSerializableQuery(CharacterViewerIDQueryNode, {
|
|
422
431
|
collectionId: variables.collectionId
|
|
423
|
-
});
|
|
432
|
+
}, options);
|
|
424
433
|
}
|
|
425
434
|
if (variables.collectionSlug) {
|
|
426
435
|
return loadSerializableQuery(CharacterViewerSlugQueryNode, {
|
|
427
436
|
collectionSlug: variables.collectionSlug
|
|
428
|
-
});
|
|
437
|
+
}, options);
|
|
429
438
|
}
|
|
430
439
|
throw new Error('loadCharacterViewerQuery expected either collectionId or collectionSlug');
|
|
431
440
|
}
|