fontdue-js 3.0.0-alpha6 → 3.0.0-alpha8
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/dist/__generated__/orderTrackingUpdateOrderTrackingMutation.graphql.d.ts +27 -0
- package/dist/__generated__/orderTrackingUpdateOrderTrackingMutation.graphql.js +72 -0
- package/dist/__tests__/nextAdapter.test.js +307 -0
- package/dist/components/BuyButton/index.js +8 -2
- package/dist/components/Cart/orderTracking.d.ts +10 -0
- package/dist/components/Cart/orderTracking.js +43 -0
- package/dist/components/CartButton/index.js +16 -4
- package/dist/components/CharacterViewer/index.js +8 -2
- package/dist/components/CustomerLoginForm/index.js +17 -9
- package/dist/components/FontdueProvider/index.d.ts +10 -1
- package/dist/components/FontdueProvider/index.js +1 -0
- package/dist/components/FontdueProvider/index.server.d.ts +2 -1
- package/dist/components/FontdueProvider/index.server.js +16 -0
- package/dist/components/NewsletterSignup/index.js +4 -1
- package/dist/components/TestFontsForm/index.js +4 -1
- package/dist/components/TypeTesters/index.js +8 -2
- package/dist/next/config.d.ts +45 -0
- package/dist/next/config.js +180 -0
- package/dist/next/image-loader.d.ts +7 -0
- package/dist/next/image-loader.js +39 -0
- package/dist/next/index.d.ts +2 -0
- package/dist/next/index.js +10 -0
- package/dist/next/revalidate.d.ts +1 -0
- package/dist/next/revalidate.js +37 -0
- package/dist/next/tenant.d.ts +18 -0
- package/dist/next/tenant.js +105 -0
- package/dist/relay/environment.js +10 -4
- package/dist/relay/loadSerializableQuery.d.ts +3 -1
- package/dist/relay/loadSerializableQuery.js +2 -2
- package/dist/relay/serverConfig.d.ts +10 -0
- package/dist/relay/serverConfig.js +38 -0
- package/dist/vite.js +2 -0
- package/package.json +5 -1
- package/types/next-cache.d.ts +6 -0
- package/dist/__generated__/TypeTesterStyleSelectData_viewer.graphql.d.ts +0 -42
- package/dist/__generated__/TypeTesterStyleSelectData_viewer.graphql.js +0 -166
- package/dist/__generated__/TypeTester_viewer.graphql.d.ts +0 -17
- package/dist/__generated__/TypeTester_viewer.graphql.js +0 -40
- package/dist/__generated__/TypeTesters_viewer.graphql.d.ts +0 -17
- package/dist/__generated__/TypeTesters_viewer.graphql.js +0 -40
- package/dist/components/FontdueContextProvider/index.server.d.ts +0 -4
- package/dist/components/FontdueContextProvider/index.server.js +0 -7
- package/dist/components/FontdueProvider/useAuxUIOwner.d.ts +0 -1
- package/dist/components/FontdueProvider/useAuxUIOwner.js +0 -28
- package/dist/components/TypeTester/TypeTesterStandalone.preload.d.ts +0 -14
- package/dist/components/TypeTester/TypeTesterStandalone.preload.js +0 -20
- package/dist/config.d.ts +0 -7
- package/dist/config.js +0 -31
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @generated SignedSource<<017a8a724b3a0fd0918153ce04d07f68>>
|
|
3
|
+
* @lightSyntaxTransform
|
|
4
|
+
* @nogrep
|
|
5
|
+
*/
|
|
6
|
+
import { ConcreteRequest } from 'relay-runtime';
|
|
7
|
+
export type UpdateOrderTrackingInput = {
|
|
8
|
+
analyticsConsent?: boolean | null;
|
|
9
|
+
anonymousId?: string | null;
|
|
10
|
+
fbc?: string | null;
|
|
11
|
+
fbp?: string | null;
|
|
12
|
+
url?: string | null;
|
|
13
|
+
};
|
|
14
|
+
export type orderTrackingUpdateOrderTrackingMutation$variables = {
|
|
15
|
+
input: UpdateOrderTrackingInput;
|
|
16
|
+
};
|
|
17
|
+
export type orderTrackingUpdateOrderTrackingMutation$data = {
|
|
18
|
+
readonly updateOrderTracking: {
|
|
19
|
+
readonly success: boolean | null;
|
|
20
|
+
} | null;
|
|
21
|
+
};
|
|
22
|
+
export type orderTrackingUpdateOrderTrackingMutation = {
|
|
23
|
+
response: orderTrackingUpdateOrderTrackingMutation$data;
|
|
24
|
+
variables: orderTrackingUpdateOrderTrackingMutation$variables;
|
|
25
|
+
};
|
|
26
|
+
declare const node: ConcreteRequest;
|
|
27
|
+
export default node;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.default = void 0;
|
|
7
|
+
/**
|
|
8
|
+
* @generated SignedSource<<017a8a724b3a0fd0918153ce04d07f68>>
|
|
9
|
+
* @lightSyntaxTransform
|
|
10
|
+
* @nogrep
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/* tslint:disable */
|
|
14
|
+
/* eslint-disable */
|
|
15
|
+
// @ts-nocheck
|
|
16
|
+
|
|
17
|
+
const node = function () {
|
|
18
|
+
var v0 = [{
|
|
19
|
+
"defaultValue": null,
|
|
20
|
+
"kind": "LocalArgument",
|
|
21
|
+
"name": "input"
|
|
22
|
+
}],
|
|
23
|
+
v1 = [{
|
|
24
|
+
"alias": null,
|
|
25
|
+
"args": [{
|
|
26
|
+
"kind": "Variable",
|
|
27
|
+
"name": "input",
|
|
28
|
+
"variableName": "input"
|
|
29
|
+
}],
|
|
30
|
+
"concreteType": "UpdateOrderTrackingPayload",
|
|
31
|
+
"kind": "LinkedField",
|
|
32
|
+
"name": "updateOrderTracking",
|
|
33
|
+
"plural": false,
|
|
34
|
+
"selections": [{
|
|
35
|
+
"alias": null,
|
|
36
|
+
"args": null,
|
|
37
|
+
"kind": "ScalarField",
|
|
38
|
+
"name": "success",
|
|
39
|
+
"storageKey": null
|
|
40
|
+
}],
|
|
41
|
+
"storageKey": null
|
|
42
|
+
}];
|
|
43
|
+
return {
|
|
44
|
+
"fragment": {
|
|
45
|
+
"argumentDefinitions": v0 /*: any*/,
|
|
46
|
+
"kind": "Fragment",
|
|
47
|
+
"metadata": null,
|
|
48
|
+
"name": "orderTrackingUpdateOrderTrackingMutation",
|
|
49
|
+
"selections": v1 /*: any*/,
|
|
50
|
+
"type": "RootMutationType",
|
|
51
|
+
"abstractKey": null
|
|
52
|
+
},
|
|
53
|
+
"kind": "Request",
|
|
54
|
+
"operation": {
|
|
55
|
+
"argumentDefinitions": v0 /*: any*/,
|
|
56
|
+
"kind": "Operation",
|
|
57
|
+
"name": "orderTrackingUpdateOrderTrackingMutation",
|
|
58
|
+
"selections": v1 /*: any*/
|
|
59
|
+
},
|
|
60
|
+
"params": {
|
|
61
|
+
"cacheID": "c6d6059215c6ed30315c881dff80e9a7",
|
|
62
|
+
"id": null,
|
|
63
|
+
"metadata": {},
|
|
64
|
+
"name": "orderTrackingUpdateOrderTrackingMutation",
|
|
65
|
+
"operationKind": "mutation",
|
|
66
|
+
"text": "mutation orderTrackingUpdateOrderTrackingMutation(\n $input: UpdateOrderTrackingInput!\n) {\n updateOrderTracking(input: $input) {\n success\n }\n}\n"
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
}();
|
|
70
|
+
node.hash = "d59a127a7f140424f507ae549731bac7";
|
|
71
|
+
var _default = node;
|
|
72
|
+
exports.default = _default;
|
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
2
|
+
|
|
3
|
+
// The next adapter modules read process.env at module load, so each test
|
|
4
|
+
// group stubs the env and re-imports them fresh.
|
|
5
|
+
async function importTenant() {
|
|
6
|
+
return await import("../next/tenant.js");
|
|
7
|
+
}
|
|
8
|
+
async function importConfig() {
|
|
9
|
+
return await import("../next/config.js");
|
|
10
|
+
}
|
|
11
|
+
async function importRevalidate() {
|
|
12
|
+
return await import("../next/revalidate.js");
|
|
13
|
+
}
|
|
14
|
+
const revalidateTag = vi.fn();
|
|
15
|
+
vi.mock('next/cache', () => ({
|
|
16
|
+
revalidateTag: tag => revalidateTag(tag)
|
|
17
|
+
}));
|
|
18
|
+
beforeEach(() => {
|
|
19
|
+
vi.resetModules();
|
|
20
|
+
revalidateTag.mockClear();
|
|
21
|
+
vi.unstubAllEnvs();
|
|
22
|
+
});
|
|
23
|
+
function stubSingleTenant() {
|
|
24
|
+
let url = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 'https://acme.fontdue.com';
|
|
25
|
+
vi.stubEnv('NEXT_PUBLIC_FONTDUE_URL', url);
|
|
26
|
+
vi.stubEnv('FONTDUE_MULTI_TENANT', '');
|
|
27
|
+
vi.stubEnv('FONTDUE_ORIGIN', '');
|
|
28
|
+
vi.stubEnv('FONTDUE_PROXY_SECRET', '');
|
|
29
|
+
}
|
|
30
|
+
function stubMultiTenant() {
|
|
31
|
+
let {
|
|
32
|
+
origin = '',
|
|
33
|
+
secret = ''
|
|
34
|
+
} = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
|
|
35
|
+
vi.stubEnv('NEXT_PUBLIC_FONTDUE_URL', '');
|
|
36
|
+
vi.stubEnv('FONTDUE_MULTI_TENANT', '1');
|
|
37
|
+
vi.stubEnv('FONTDUE_ORIGIN', origin);
|
|
38
|
+
vi.stubEnv('FONTDUE_PROXY_SECRET', secret);
|
|
39
|
+
}
|
|
40
|
+
describe('isValidDomain', () => {
|
|
41
|
+
it('accepts plain dotted hostnames and rejects everything else', async () => {
|
|
42
|
+
stubMultiTenant();
|
|
43
|
+
const {
|
|
44
|
+
isValidDomain
|
|
45
|
+
} = await importTenant();
|
|
46
|
+
expect(isValidDomain('acme.fontdue.com')).toBe(true);
|
|
47
|
+
expect(isValidDomain('a-b.example')).toBe(true);
|
|
48
|
+
expect(isValidDomain('localhost')).toBe(false); // no dot
|
|
49
|
+
expect(isValidDomain('acme.fontdue.com:3000')).toBe(false); // port
|
|
50
|
+
expect(isValidDomain('acme.fontdue.com/x')).toBe(false); // path
|
|
51
|
+
expect(isValidDomain('-bad.example')).toBe(false);
|
|
52
|
+
expect(isValidDomain('a'.repeat(254) + '.example')).toBe(false);
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
describe('fontdueEndpoint', () => {
|
|
56
|
+
it('single-tenant: targets NEXT_PUBLIC_FONTDUE_URL with no headers', async () => {
|
|
57
|
+
stubSingleTenant('https://acme.fontdue.com');
|
|
58
|
+
const {
|
|
59
|
+
fontdueEndpoint
|
|
60
|
+
} = await importTenant();
|
|
61
|
+
expect(fontdueEndpoint('acme.fontdue.com')).toEqual({
|
|
62
|
+
origin: 'https://acme.fontdue.com',
|
|
63
|
+
headers: {},
|
|
64
|
+
tags: ['graphql', 'graphql:acme.fontdue.com']
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
it('single-tenant: throws when NEXT_PUBLIC_FONTDUE_URL is missing', async () => {
|
|
68
|
+
vi.stubEnv('NEXT_PUBLIC_FONTDUE_URL', '');
|
|
69
|
+
vi.stubEnv('FONTDUE_MULTI_TENANT', '');
|
|
70
|
+
const {
|
|
71
|
+
fontdueEndpoint
|
|
72
|
+
} = await importTenant();
|
|
73
|
+
expect(() => fontdueEndpoint('acme.fontdue.com')).toThrow(/NEXT_PUBLIC_FONTDUE_URL/);
|
|
74
|
+
});
|
|
75
|
+
it('multi-tenant: forwards the host to FONTDUE_ORIGIN with the proxy secret', async () => {
|
|
76
|
+
stubMultiTenant({
|
|
77
|
+
origin: 'http://app:4000',
|
|
78
|
+
secret: 's3cret'
|
|
79
|
+
});
|
|
80
|
+
const {
|
|
81
|
+
fontdueEndpoint
|
|
82
|
+
} = await importTenant();
|
|
83
|
+
expect(fontdueEndpoint('acme.fontdue.com')).toEqual({
|
|
84
|
+
origin: 'http://app:4000',
|
|
85
|
+
headers: {
|
|
86
|
+
'x-forwarded-host': 'acme.fontdue.com',
|
|
87
|
+
'x-fontdue-proxy-secret': 's3cret'
|
|
88
|
+
},
|
|
89
|
+
tags: ['graphql', 'graphql:acme.fontdue.com']
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
it('multi-tenant: omits the secret header when unset', async () => {
|
|
93
|
+
stubMultiTenant({
|
|
94
|
+
origin: 'http://app:4000'
|
|
95
|
+
});
|
|
96
|
+
const {
|
|
97
|
+
fontdueEndpoint
|
|
98
|
+
} = await importTenant();
|
|
99
|
+
expect(fontdueEndpoint('acme.fontdue.com').headers).toEqual({
|
|
100
|
+
'x-forwarded-host': 'acme.fontdue.com'
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
it('multi-tenant: falls back to the tenant public URL without FONTDUE_ORIGIN', async () => {
|
|
104
|
+
stubMultiTenant();
|
|
105
|
+
const {
|
|
106
|
+
fontdueEndpoint
|
|
107
|
+
} = await importTenant();
|
|
108
|
+
expect(fontdueEndpoint('acme.fontdue.com').origin).toBe('https://acme.fontdue.com');
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
describe('configureFontdueRender', () => {
|
|
112
|
+
it('rejects invalid domains with null and sets no config', async () => {
|
|
113
|
+
stubMultiTenant({
|
|
114
|
+
origin: 'http://app:4000'
|
|
115
|
+
});
|
|
116
|
+
const {
|
|
117
|
+
configureFontdueRender
|
|
118
|
+
} = await importTenant();
|
|
119
|
+
const {
|
|
120
|
+
getFontdueServerConfig
|
|
121
|
+
} = await import("../relay/serverConfig.js");
|
|
122
|
+
expect(configureFontdueRender('not a domain')).toBeNull();
|
|
123
|
+
expect(getFontdueServerConfig()).toBeUndefined();
|
|
124
|
+
});
|
|
125
|
+
it('sets the per-render server config and returns the endpoint', async () => {
|
|
126
|
+
stubMultiTenant({
|
|
127
|
+
origin: 'http://app:4000',
|
|
128
|
+
secret: 's3cret'
|
|
129
|
+
});
|
|
130
|
+
const {
|
|
131
|
+
configureFontdueRender
|
|
132
|
+
} = await importTenant();
|
|
133
|
+
const {
|
|
134
|
+
getFontdueServerConfig
|
|
135
|
+
} = await import("../relay/serverConfig.js");
|
|
136
|
+
const endpoint = configureFontdueRender('acme.fontdue.com');
|
|
137
|
+
expect(endpoint === null || endpoint === void 0 ? void 0 : endpoint.origin).toBe('http://app:4000');
|
|
138
|
+
// Outside an RSC render React.cache doesn't memoize, so the write is a
|
|
139
|
+
// no-op here — but the config passed must still be shaped correctly, so
|
|
140
|
+
// assert via fontdueServerConfig instead.
|
|
141
|
+
const {
|
|
142
|
+
fontdueServerConfig
|
|
143
|
+
} = await importTenant();
|
|
144
|
+
expect(fontdueServerConfig('acme.fontdue.com')).toEqual({
|
|
145
|
+
url: 'http://app:4000',
|
|
146
|
+
headers: {
|
|
147
|
+
'x-forwarded-host': 'acme.fontdue.com',
|
|
148
|
+
'x-fontdue-proxy-secret': 's3cret'
|
|
149
|
+
},
|
|
150
|
+
cacheTags: ['graphql:acme.fontdue.com']
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
describe('withFontdue', () => {
|
|
155
|
+
it('throws when neither mode is configured', async () => {
|
|
156
|
+
vi.stubEnv('NEXT_PUBLIC_FONTDUE_URL', '');
|
|
157
|
+
vi.stubEnv('FONTDUE_MULTI_TENANT', '');
|
|
158
|
+
const {
|
|
159
|
+
withFontdue
|
|
160
|
+
} = await importConfig();
|
|
161
|
+
expect(() => withFontdue({})).toThrow(/NEXT_PUBLIC_FONTDUE_URL/);
|
|
162
|
+
});
|
|
163
|
+
it('single-tenant: rewrites every path under the constant domain', async () => {
|
|
164
|
+
stubSingleTenant('https://acme.fontdue.com');
|
|
165
|
+
const {
|
|
166
|
+
withFontdue
|
|
167
|
+
} = await importConfig();
|
|
168
|
+
const rewrites = await withFontdue({}).rewrites();
|
|
169
|
+
expect(rewrites.afterFiles).toEqual([]);
|
|
170
|
+
expect(rewrites.fallback).toEqual([]);
|
|
171
|
+
const destinations = rewrites.beforeFiles.map(r => r.destination);
|
|
172
|
+
expect(destinations).toEqual(['/acme.fontdue.com', '/acme.fontdue.com/robots.txt', '/acme.fontdue.com/sitemap.xml', '/acme.fontdue.com/:path']);
|
|
173
|
+
});
|
|
174
|
+
it('multi-tenant: emits forwarded-host rules before host rules, mutually exclusive', async () => {
|
|
175
|
+
stubMultiTenant();
|
|
176
|
+
const {
|
|
177
|
+
withFontdue
|
|
178
|
+
} = await importConfig();
|
|
179
|
+
const rewrites = await withFontdue({}).rewrites();
|
|
180
|
+
expect(rewrites.beforeFiles).toHaveLength(8);
|
|
181
|
+
const [forwarded, hostBased] = [rewrites.beforeFiles.slice(0, 4), rewrites.beforeFiles.slice(4)];
|
|
182
|
+
for (const rule of forwarded) {
|
|
183
|
+
var _rule$has;
|
|
184
|
+
expect((_rule$has = rule.has) === null || _rule$has === void 0 ? void 0 : _rule$has[0]).toMatchObject({
|
|
185
|
+
type: 'header',
|
|
186
|
+
key: 'x-forwarded-host'
|
|
187
|
+
});
|
|
188
|
+
expect(rule.missing).toBeUndefined();
|
|
189
|
+
}
|
|
190
|
+
for (const rule of hostBased) {
|
|
191
|
+
var _rule$has2, _rule$missing;
|
|
192
|
+
expect((_rule$has2 = rule.has) === null || _rule$has2 === void 0 ? void 0 : _rule$has2[0]).toMatchObject({
|
|
193
|
+
type: 'host'
|
|
194
|
+
});
|
|
195
|
+
expect((_rule$missing = rule.missing) === null || _rule$missing === void 0 ? void 0 : _rule$missing[0]).toMatchObject({
|
|
196
|
+
type: 'header',
|
|
197
|
+
key: 'x-forwarded-host'
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
});
|
|
201
|
+
it("chains the app's beforeFiles rules ahead of the tenant rules", async () => {
|
|
202
|
+
stubSingleTenant();
|
|
203
|
+
const {
|
|
204
|
+
withFontdue
|
|
205
|
+
} = await importConfig();
|
|
206
|
+
const userRule = {
|
|
207
|
+
source: '/old',
|
|
208
|
+
destination: '/new'
|
|
209
|
+
};
|
|
210
|
+
const config = withFontdue({
|
|
211
|
+
rewrites: async () => ({
|
|
212
|
+
beforeFiles: [userRule]
|
|
213
|
+
})
|
|
214
|
+
});
|
|
215
|
+
const rewrites = await config.rewrites();
|
|
216
|
+
expect(rewrites.beforeFiles[0]).toEqual(userRule);
|
|
217
|
+
expect(rewrites.beforeFiles).toHaveLength(5);
|
|
218
|
+
});
|
|
219
|
+
it("treats a plain-array rewrites() result as afterFiles, per Next's contract", async () => {
|
|
220
|
+
stubSingleTenant();
|
|
221
|
+
const {
|
|
222
|
+
withFontdue
|
|
223
|
+
} = await importConfig();
|
|
224
|
+
const userRule = {
|
|
225
|
+
source: '/old',
|
|
226
|
+
destination: '/new'
|
|
227
|
+
};
|
|
228
|
+
const rewrites = await withFontdue({
|
|
229
|
+
rewrites: async () => [userRule]
|
|
230
|
+
}).rewrites();
|
|
231
|
+
expect(rewrites.afterFiles).toEqual([userRule]);
|
|
232
|
+
expect(rewrites.beforeFiles).toHaveLength(4);
|
|
233
|
+
});
|
|
234
|
+
it('merges image settings, keeping app remotePatterns and overrides', async () => {
|
|
235
|
+
stubMultiTenant();
|
|
236
|
+
const {
|
|
237
|
+
withFontdue
|
|
238
|
+
} = await importConfig();
|
|
239
|
+
const config = withFontdue({
|
|
240
|
+
images: {
|
|
241
|
+
dangerouslyAllowSVG: false,
|
|
242
|
+
remotePatterns: [{
|
|
243
|
+
protocol: 'https',
|
|
244
|
+
hostname: 'cdn.example'
|
|
245
|
+
}]
|
|
246
|
+
}
|
|
247
|
+
});
|
|
248
|
+
expect(config.images.dangerouslyAllowSVG).toBe(false);
|
|
249
|
+
// NODE_ENV is 'test' here, i.e. not production → dev workaround active.
|
|
250
|
+
expect(config.images.unoptimized).toBe(true);
|
|
251
|
+
const hostnames = config.images.remotePatterns.map(p => p.hostname);
|
|
252
|
+
expect(hostnames).toContain('cdn.example');
|
|
253
|
+
expect(hostnames).toContain('*.fontdue.com');
|
|
254
|
+
expect(hostnames).toContain('**');
|
|
255
|
+
});
|
|
256
|
+
it('passes unrelated config through and defaults htmlLimitedBots', async () => {
|
|
257
|
+
stubSingleTenant();
|
|
258
|
+
const {
|
|
259
|
+
withFontdue
|
|
260
|
+
} = await importConfig();
|
|
261
|
+
const config = withFontdue({
|
|
262
|
+
reactStrictMode: true
|
|
263
|
+
});
|
|
264
|
+
expect(config.reactStrictMode).toBe(true);
|
|
265
|
+
expect(config.htmlLimitedBots).toEqual(/.*/);
|
|
266
|
+
expect(withFontdue({
|
|
267
|
+
htmlLimitedBots: /Googlebot/
|
|
268
|
+
}).htmlLimitedBots).toEqual(/Googlebot/);
|
|
269
|
+
});
|
|
270
|
+
});
|
|
271
|
+
describe('revalidate POST', () => {
|
|
272
|
+
it('multi-tenant: purges only the tenant tag', async () => {
|
|
273
|
+
stubMultiTenant();
|
|
274
|
+
const {
|
|
275
|
+
POST
|
|
276
|
+
} = await importRevalidate();
|
|
277
|
+
const response = await POST(new Request('http://internal/api/revalidate?domain=Acme.Fontdue.com', {
|
|
278
|
+
method: 'POST'
|
|
279
|
+
}));
|
|
280
|
+
expect(response.status).toBe(200);
|
|
281
|
+
expect(revalidateTag).toHaveBeenCalledExactlyOnceWith('graphql:acme.fontdue.com');
|
|
282
|
+
});
|
|
283
|
+
it('multi-tenant: 400s on a missing or invalid domain', async () => {
|
|
284
|
+
stubMultiTenant();
|
|
285
|
+
const {
|
|
286
|
+
POST
|
|
287
|
+
} = await importRevalidate();
|
|
288
|
+
for (const url of ['http://internal/api/revalidate', 'http://internal/api/revalidate?domain=not%20a%20domain']) {
|
|
289
|
+
const response = await POST(new Request(url, {
|
|
290
|
+
method: 'POST'
|
|
291
|
+
}));
|
|
292
|
+
expect(response.status).toBe(400);
|
|
293
|
+
}
|
|
294
|
+
expect(revalidateTag).not.toHaveBeenCalled();
|
|
295
|
+
});
|
|
296
|
+
it('single-tenant: purges the global graphql tag', async () => {
|
|
297
|
+
stubSingleTenant();
|
|
298
|
+
const {
|
|
299
|
+
POST
|
|
300
|
+
} = await importRevalidate();
|
|
301
|
+
const response = await POST(new Request('http://internal/api/revalidate', {
|
|
302
|
+
method: 'POST'
|
|
303
|
+
}));
|
|
304
|
+
expect(response.status).toBe(200);
|
|
305
|
+
expect(revalidateTag).toHaveBeenCalledExactlyOnceWith('graphql');
|
|
306
|
+
});
|
|
307
|
+
});
|
|
@@ -85,7 +85,10 @@ export function BuyButtonPreloadedIDQueryRenderer(_ref3) {
|
|
|
85
85
|
preloadedQuery,
|
|
86
86
|
...rest
|
|
87
87
|
} = _ref3;
|
|
88
|
-
|
|
88
|
+
// The query node lets the hook commit the payload into the store, so
|
|
89
|
+
// usePreloadedQuery resolves synchronously during SSR instead of
|
|
90
|
+
// refetching (the response cache only exists in the browser).
|
|
91
|
+
const queryRef = useSerializablePreloadedQuery(preloadedQuery, 'store-or-network', idQuery);
|
|
89
92
|
const data = usePreloadedQuery(idQuery, queryRef);
|
|
90
93
|
return /*#__PURE__*/React.createElement(BuyButtonComponent, _extends({}, rest, data));
|
|
91
94
|
}
|
|
@@ -108,7 +111,10 @@ export function BuyButtonPreloadedSlugQueryRenderer(_ref5) {
|
|
|
108
111
|
preloadedQuery,
|
|
109
112
|
...rest
|
|
110
113
|
} = _ref5;
|
|
111
|
-
|
|
114
|
+
// The query node lets the hook commit the payload into the store, so
|
|
115
|
+
// usePreloadedQuery resolves synchronously during SSR instead of
|
|
116
|
+
// refetching (the response cache only exists in the browser).
|
|
117
|
+
const queryRef = useSerializablePreloadedQuery(preloadedQuery, 'store-or-network', slugQuery);
|
|
112
118
|
const data = usePreloadedQuery(slugQuery, queryRef);
|
|
113
119
|
return /*#__PURE__*/React.createElement(BuyButtonComponent, _extends({}, rest, {
|
|
114
120
|
collection: ((_data$viewer2 = data.viewer) === null || _data$viewer2 === void 0 ? void 0 : (_data$viewer2$slug = _data$viewer2.slug) === null || _data$viewer2$slug === void 0 ? void 0 : _data$viewer2$slug.collection) ?? null
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { Environment } from 'relay-runtime';
|
|
2
|
+
/**
|
|
3
|
+
* Stores the buyer's analytics context (cookie consent, Meta browser IDs,
|
|
4
|
+
* checkout page URL) on the current order. The server emits the purchase
|
|
5
|
+
* event from a Stripe webhook — outside the browser — so this is how that
|
|
6
|
+
* event respects the consent banner and carries attribution.
|
|
7
|
+
*
|
|
8
|
+
* Fire-and-forget: tracking must never break checkout.
|
|
9
|
+
*/
|
|
10
|
+
export declare function sendOrderTracking(environment: Environment): void;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
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 }; }
|
|
11
|
+
function readCookie(name) {
|
|
12
|
+
const match = document.cookie.match(new RegExp('(?:^|;\\s*)' + name + '=([^;]*)'));
|
|
13
|
+
return match ? decodeURIComponent(match[1]) : undefined;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Stores the buyer's analytics context (cookie consent, Meta browser IDs,
|
|
18
|
+
* checkout page URL) on the current order. The server emits the purchase
|
|
19
|
+
* event from a Stripe webhook — outside the browser — so this is how that
|
|
20
|
+
* event respects the consent banner and carries attribution.
|
|
21
|
+
*
|
|
22
|
+
* Fire-and-forget: tracking must never break checkout.
|
|
23
|
+
*/
|
|
24
|
+
function sendOrderTracking(environment) {
|
|
25
|
+
try {
|
|
26
|
+
(0, _reactRelay.commitMutation)(environment, {
|
|
27
|
+
mutation: (_orderTrackingUpdateOrderTrackingMutation2.default.hash && _orderTrackingUpdateOrderTrackingMutation2.default.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."), _orderTrackingUpdateOrderTrackingMutation2.default),
|
|
28
|
+
variables: {
|
|
29
|
+
input: {
|
|
30
|
+
analyticsConsent: (0, _consent.hasConsent)('analytics'),
|
|
31
|
+
anonymousId: (0, _consent.getClientAnonymousId)(),
|
|
32
|
+
fbp: readCookie('_fbp'),
|
|
33
|
+
fbc: readCookie('_fbc'),
|
|
34
|
+
url: window.location.href
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
onCompleted: () => undefined,
|
|
38
|
+
onError: () => undefined
|
|
39
|
+
});
|
|
40
|
+
} catch {
|
|
41
|
+
// Ignore — see above.
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -3,7 +3,7 @@
|
|
|
3
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
4
|
import _CartButtonQuery from "../../__generated__/CartButtonQuery.graphql.js";
|
|
5
5
|
import _CartButton_order from "../../__generated__/CartButton_order.graphql.js";
|
|
6
|
-
import React, { useEffect, useCallback, useContext, useState } from 'react';
|
|
6
|
+
import React, { Suspense, useEffect, useCallback, useContext, useState } from 'react';
|
|
7
7
|
import { useDispatch } from 'react-redux';
|
|
8
8
|
import { graphql, useFragment, useLazyLoadQuery } from 'react-relay';
|
|
9
9
|
import ComponentsContext from '../ComponentsContext.js';
|
|
@@ -101,14 +101,26 @@ function CartButtonLazyQueryRenderer(props) {
|
|
|
101
101
|
}
|
|
102
102
|
// Cart contents are per-customer-session, so a server-side preload from a
|
|
103
103
|
// CDN-cached SSG/prerender environment (no session cookies) just caches an
|
|
104
|
-
// empty cart and delays the real one. CartButton
|
|
105
|
-
//
|
|
104
|
+
// empty cart and delays the real one. CartButton lazy-fetches on hydration —
|
|
105
|
+
// and only after mount: useLazyLoadQuery would otherwise also run during the
|
|
106
|
+
// SSR pass, where the fetch is sessionless (and in multi-tenant setups may
|
|
107
|
+
// have no resolvable GraphQL URL at all). Until mount it renders the empty
|
|
108
|
+
// cart state, which is also the Suspense fallback while the real fetch runs,
|
|
109
|
+
// so server HTML and first client render agree.
|
|
106
110
|
export default function CartButton(_ref2) {
|
|
107
111
|
let {
|
|
108
112
|
config,
|
|
109
113
|
...props
|
|
110
114
|
} = _ref2;
|
|
115
|
+
const [mounted, setMounted] = useState(false);
|
|
116
|
+
useEffect(() => setMounted(true), []);
|
|
111
117
|
return /*#__PURE__*/React.createElement(EnsureFontdueContext, {
|
|
112
118
|
config: config
|
|
113
|
-
}, /*#__PURE__*/React.createElement(
|
|
119
|
+
}, mounted ? /*#__PURE__*/React.createElement(Suspense, {
|
|
120
|
+
fallback: /*#__PURE__*/React.createElement(CartButtonComponent, _extends({}, props, {
|
|
121
|
+
order: null
|
|
122
|
+
}))
|
|
123
|
+
}, /*#__PURE__*/React.createElement(CartButtonLazyQueryRenderer, props)) : /*#__PURE__*/React.createElement(CartButtonComponent, _extends({}, props, {
|
|
124
|
+
order: null
|
|
125
|
+
})));
|
|
114
126
|
}
|
|
@@ -446,7 +446,10 @@ export function CharacterViewerPreloadedIDQueryRenderer(_ref8) {
|
|
|
446
446
|
preloadedQuery,
|
|
447
447
|
...rest
|
|
448
448
|
} = _ref8;
|
|
449
|
-
|
|
449
|
+
// The query node lets the hook commit the payload into the store, so
|
|
450
|
+
// usePreloadedQuery resolves synchronously during SSR instead of
|
|
451
|
+
// refetching (the response cache only exists in the browser).
|
|
452
|
+
const queryRef = useSerializablePreloadedQuery(preloadedQuery, 'store-or-network', idQuery);
|
|
450
453
|
const data = usePreloadedQuery(idQuery, queryRef);
|
|
451
454
|
if (!data.node) return null;
|
|
452
455
|
return /*#__PURE__*/React.createElement(CharacterViewerComponent, _extends({}, rest, {
|
|
@@ -473,7 +476,10 @@ export function CharacterViewerPreloadedSlugQueryRenderer(_ref10) {
|
|
|
473
476
|
preloadedQuery,
|
|
474
477
|
...rest
|
|
475
478
|
} = _ref10;
|
|
476
|
-
|
|
479
|
+
// The query node lets the hook commit the payload into the store, so
|
|
480
|
+
// usePreloadedQuery resolves synchronously during SSR instead of
|
|
481
|
+
// refetching (the response cache only exists in the browser).
|
|
482
|
+
const queryRef = useSerializablePreloadedQuery(preloadedQuery, 'store-or-network', slugQuery);
|
|
477
483
|
const data = usePreloadedQuery(slugQuery, queryRef);
|
|
478
484
|
const collection = data === null || data === void 0 ? void 0 : (_data$viewer2 = data.viewer) === null || _data$viewer2 === void 0 ? void 0 : (_data$viewer2$slug = _data$viewer2.slug) === null || _data$viewer2$slug === void 0 ? void 0 : _data$viewer2$slug.fontCollection;
|
|
479
485
|
if (!collection) return null;
|
|
@@ -9,7 +9,6 @@ const loginMutation = (_CustomerLoginFormLoginMutation.hash && _CustomerLoginFor
|
|
|
9
9
|
const query = (_CustomerLoginFormQuery.hash && _CustomerLoginFormQuery.hash !== "f32c7b0b485a2b2a02ddf2cbc5b87a3c" && console.error("The definition of 'CustomerLoginFormQuery' appears to have changed. Run `relay-compiler` to update the generated files to receive the expected data."), _CustomerLoginFormQuery);
|
|
10
10
|
const DEFAULT_SUBMITTED_LABEL = '<p>Submitted!</p><p>Please check your email inbox for a link to log in.</p><p>If you don\u2019t receive an email, please contact us to retrieve your order information.</p>';
|
|
11
11
|
const CustomerLoginForm = _ref => {
|
|
12
|
-
var _data$viewer, _data$viewer$settings;
|
|
13
12
|
let {
|
|
14
13
|
submitLabel = 'Submit'
|
|
15
14
|
} = _ref;
|
|
@@ -18,8 +17,6 @@ const CustomerLoginForm = _ref => {
|
|
|
18
17
|
const [submitting, setSubmitting] = useState(false);
|
|
19
18
|
const [submitted, setSubmitted] = useState(false);
|
|
20
19
|
const environment = useRelayEnvironment();
|
|
21
|
-
const data = useLazyLoadQuery(query, {});
|
|
22
|
-
const submittedLabel = ((_data$viewer = data.viewer) === null || _data$viewer === void 0 ? void 0 : (_data$viewer$settings = _data$viewer.settings) === null || _data$viewer$settings === void 0 ? void 0 : _data$viewer$settings.customerLoginSubmittedLabel) || DEFAULT_SUBMITTED_LABEL;
|
|
23
20
|
const handleSubmit = e => {
|
|
24
21
|
e.preventDefault();
|
|
25
22
|
setSubmitting(true);
|
|
@@ -54,12 +51,7 @@ const CustomerLoginForm = _ref => {
|
|
|
54
51
|
className: "login-form"
|
|
55
52
|
}, error && /*#__PURE__*/React.createElement("div", {
|
|
56
53
|
className: "login-form__errors"
|
|
57
|
-
}, error), submitted ? /*#__PURE__*/React.createElement("
|
|
58
|
-
className: "login-form__submitted",
|
|
59
|
-
dangerouslySetInnerHTML: {
|
|
60
|
-
__html: submittedLabel
|
|
61
|
-
}
|
|
62
|
-
}) : /*#__PURE__*/React.createElement("form", {
|
|
54
|
+
}, error), submitted ? /*#__PURE__*/React.createElement(SubmittedMessage, null) : /*#__PURE__*/React.createElement("form", {
|
|
63
55
|
className: "login-form__form",
|
|
64
56
|
onSubmit: handleSubmit
|
|
65
57
|
}, /*#__PURE__*/React.createElement("div", {
|
|
@@ -79,4 +71,20 @@ const CustomerLoginForm = _ref => {
|
|
|
79
71
|
className: "submit-button__arrow"
|
|
80
72
|
}, " \u2192")))));
|
|
81
73
|
};
|
|
74
|
+
|
|
75
|
+
// The settings label is only shown after a submit, which can only happen in
|
|
76
|
+
// the browser — fetching it here (not in the form component) keeps the lazy
|
|
77
|
+
// query out of the SSR pass, where it has no session and, in multi-tenant
|
|
78
|
+
// setups, possibly no resolvable GraphQL URL.
|
|
79
|
+
function SubmittedMessage() {
|
|
80
|
+
var _data$viewer, _data$viewer$settings;
|
|
81
|
+
const data = useLazyLoadQuery(query, {});
|
|
82
|
+
const submittedLabel = ((_data$viewer = data.viewer) === null || _data$viewer === void 0 ? void 0 : (_data$viewer$settings = _data$viewer.settings) === null || _data$viewer$settings === void 0 ? void 0 : _data$viewer$settings.customerLoginSubmittedLabel) || DEFAULT_SUBMITTED_LABEL;
|
|
83
|
+
return /*#__PURE__*/React.createElement("div", {
|
|
84
|
+
className: "login-form__submitted",
|
|
85
|
+
dangerouslySetInnerHTML: {
|
|
86
|
+
__html: submittedLabel
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
}
|
|
82
90
|
export default CustomerLoginForm;
|
|
@@ -2,11 +2,20 @@ import React from 'react';
|
|
|
2
2
|
export { default as loadFontdueProviderQuery } from '../../loadFontdueProviderQuery.js';
|
|
3
3
|
import type { FontdueContextProvider_props } from '../FontdueContextProvider/index.js';
|
|
4
4
|
import { SerializablePreloadedQuery } from '../../relay/loadSerializableQuery.js';
|
|
5
|
+
import type { FontdueServerConfig } from '../../relay/serverConfig.js';
|
|
5
6
|
import { FontdueProviderQuery } from '../../__generated__/FontdueProviderQuery.graphql.js';
|
|
6
7
|
export declare const fontdueProviderQuery: import("react-relay").GraphQLTaggedNode;
|
|
7
8
|
export type FontdueProviderPreloadedQuery = SerializablePreloadedQuery<FontdueProviderQuery>;
|
|
8
9
|
export interface FontdueProvider_props extends FontdueContextProvider_props {
|
|
9
10
|
preloadedQuery?: FontdueProviderPreloadedQuery;
|
|
11
|
+
/**
|
|
12
|
+
* Per-render config for server-side fetches (url, headers, cacheTags).
|
|
13
|
+
* Only honoured by the React Server Components entrypoint, which writes it
|
|
14
|
+
* to the render-scoped config store and strips it before this client
|
|
15
|
+
* component — it may carry internal headers that must never reach the
|
|
16
|
+
* browser. Ignored when the client provider is used directly.
|
|
17
|
+
*/
|
|
18
|
+
serverConfig?: FontdueServerConfig;
|
|
10
19
|
}
|
|
11
|
-
declare const FontdueProvider: ({ children, preloadedQuery, ...rest }: FontdueProvider_props) => React.JSX.Element;
|
|
20
|
+
declare const FontdueProvider: ({ children, preloadedQuery, serverConfig: _serverConfig, ...rest }: FontdueProvider_props) => React.JSX.Element;
|
|
12
21
|
export default FontdueProvider;
|
|
@@ -2,5 +2,6 @@ import React from 'react';
|
|
|
2
2
|
import type { FontdueProvider_props } from './index.js';
|
|
3
3
|
export type { FontdueProvider_props } from './index.js';
|
|
4
4
|
export type { FontdueProviderPreloadedQuery } from '../../loadFontdueProviderQuery.js';
|
|
5
|
+
export type { FontdueServerConfig } from '../../relay/serverConfig.js';
|
|
5
6
|
export declare function loadFontdueProviderQuery(): never;
|
|
6
|
-
export default function FontdueProviderServer({ preloadedQuery, ...rest }: FontdueProvider_props): Promise<React.JSX.Element>;
|
|
7
|
+
export default function FontdueProviderServer({ preloadedQuery, serverConfig, ...rest }: FontdueProvider_props): Promise<React.JSX.Element>;
|