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
|
@@ -23,13 +23,20 @@ interface TrackingConfig {
|
|
|
23
23
|
consentMessage?: string;
|
|
24
24
|
segment?: SegmentConfig;
|
|
25
25
|
}
|
|
26
|
+
interface PreviewConfig {
|
|
27
|
+
endpoint?: string;
|
|
28
|
+
revalidateEndpoint?: string;
|
|
29
|
+
clientSide?: boolean;
|
|
30
|
+
}
|
|
26
31
|
export interface Config {
|
|
27
32
|
form?: FormConfig;
|
|
28
33
|
storeModal?: StoreModalConfig;
|
|
29
34
|
typeTester?: TypeTesterConfig;
|
|
30
35
|
stripe?: StripeConfig;
|
|
31
36
|
tracking?: TrackingConfig;
|
|
37
|
+
preview?: PreviewConfig;
|
|
32
38
|
corsErrorModal?: boolean;
|
|
39
|
+
cdnUrl?: string;
|
|
33
40
|
}
|
|
34
41
|
export declare const mergeConfig: (base?: Config, override?: Config) => Config;
|
|
35
42
|
export declare const makeConfig: (config?: Config) => {
|
|
@@ -45,7 +52,7 @@ export declare const makeConfig: (config?: Config) => {
|
|
|
45
52
|
selectable: boolean;
|
|
46
53
|
priceBar: boolean;
|
|
47
54
|
textInput: boolean;
|
|
48
|
-
initialMode: "
|
|
55
|
+
initialMode: "group" | "local";
|
|
49
56
|
groupEdit: boolean;
|
|
50
57
|
bulletStyle: "round" | "square";
|
|
51
58
|
openTypeFeatures: {
|
|
@@ -102,7 +109,13 @@ export declare const makeConfig: (config?: Config) => {
|
|
|
102
109
|
consentMessage: string | undefined;
|
|
103
110
|
segment: SegmentConfig | undefined;
|
|
104
111
|
};
|
|
112
|
+
preview: {
|
|
113
|
+
endpoint: string;
|
|
114
|
+
revalidateEndpoint: string | undefined;
|
|
115
|
+
clientSide: boolean;
|
|
116
|
+
};
|
|
105
117
|
corsErrorModal: boolean;
|
|
118
|
+
cdnUrl: string;
|
|
106
119
|
};
|
|
107
120
|
declare const _default: React.Context<{
|
|
108
121
|
typeTester: {
|
|
@@ -117,7 +130,7 @@ declare const _default: React.Context<{
|
|
|
117
130
|
selectable: boolean;
|
|
118
131
|
priceBar: boolean;
|
|
119
132
|
textInput: boolean;
|
|
120
|
-
initialMode: "
|
|
133
|
+
initialMode: "group" | "local";
|
|
121
134
|
groupEdit: boolean;
|
|
122
135
|
bulletStyle: "round" | "square";
|
|
123
136
|
openTypeFeatures: {
|
|
@@ -174,6 +187,12 @@ declare const _default: React.Context<{
|
|
|
174
187
|
consentMessage: string | undefined;
|
|
175
188
|
segment: SegmentConfig | undefined;
|
|
176
189
|
};
|
|
190
|
+
preview: {
|
|
191
|
+
endpoint: string;
|
|
192
|
+
revalidateEndpoint: string | undefined;
|
|
193
|
+
clientSide: boolean;
|
|
194
|
+
};
|
|
177
195
|
corsErrorModal: boolean;
|
|
196
|
+
cdnUrl: string;
|
|
178
197
|
}>;
|
|
179
198
|
export default _default;
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import React from 'react';
|
|
4
|
+
import { PREVIEW_ENDPOINT } from '../preview/constants.js';
|
|
5
|
+
import { FONTDUE_CDN_URL } from '../data/unicodeNamesUrl.js';
|
|
4
6
|
const makeTypeTesterConfig = config => {
|
|
5
7
|
var _config$openTypeFeatu, _config$openTypeFeatu2, _config$openTypeFeatu3, _config$openTypeFeatu4, _config$size, _config$size2, _config$size3, _config$lineHeight, _config$lineHeight2, _config$lineHeight3, _config$letterSpacing, _config$letterSpacing2, _config$letterSpacing3, _config$columns, _config$columns2, _config$columns3;
|
|
6
8
|
let shy = (config === null || config === void 0 ? void 0 : config.shy) ?? false;
|
|
@@ -79,7 +81,7 @@ export const mergeConfig = (base, override) => {
|
|
|
79
81
|
return deepMerge(base, override);
|
|
80
82
|
};
|
|
81
83
|
export const makeConfig = config => {
|
|
82
|
-
var _config$form, _config$storeModal, _config$storeModal2, _config$storeModal3, _config$storeModal4, _config$stripe, _config$tracking, _config$tracking2, _config$tracking3, _config$tracking4;
|
|
84
|
+
var _config$form, _config$storeModal, _config$storeModal2, _config$storeModal3, _config$storeModal4, _config$stripe, _config$tracking, _config$tracking2, _config$tracking3, _config$tracking4, _config$preview, _config$preview2, _config$preview3;
|
|
83
85
|
return {
|
|
84
86
|
typeTester: makeTypeTesterConfig(config === null || config === void 0 ? void 0 : config.typeTester),
|
|
85
87
|
form: {
|
|
@@ -101,7 +103,15 @@ export const makeConfig = config => {
|
|
|
101
103
|
consentMessage: config === null || config === void 0 ? void 0 : (_config$tracking3 = config.tracking) === null || _config$tracking3 === void 0 ? void 0 : _config$tracking3.consentMessage,
|
|
102
104
|
segment: config === null || config === void 0 ? void 0 : (_config$tracking4 = config.tracking) === null || _config$tracking4 === void 0 ? void 0 : _config$tracking4.segment
|
|
103
105
|
},
|
|
104
|
-
|
|
106
|
+
preview: {
|
|
107
|
+
endpoint: (config === null || config === void 0 ? void 0 : (_config$preview = config.preview) === null || _config$preview === void 0 ? void 0 : _config$preview.endpoint) ?? PREVIEW_ENDPOINT,
|
|
108
|
+
// No default: an unset revalidate endpoint hides the toolbar's refresh
|
|
109
|
+
// button (frameworks without a client-callable purge leave it unset).
|
|
110
|
+
revalidateEndpoint: config === null || config === void 0 ? void 0 : (_config$preview2 = config.preview) === null || _config$preview2 === void 0 ? void 0 : _config$preview2.revalidateEndpoint,
|
|
111
|
+
clientSide: (config === null || config === void 0 ? void 0 : (_config$preview3 = config.preview) === null || _config$preview3 === void 0 ? void 0 : _config$preview3.clientSide) ?? false
|
|
112
|
+
},
|
|
113
|
+
corsErrorModal: (config === null || config === void 0 ? void 0 : config.corsErrorModal) ?? true,
|
|
114
|
+
cdnUrl: (config === null || config === void 0 ? void 0 : config.cdnUrl) ?? FONTDUE_CDN_URL
|
|
105
115
|
};
|
|
106
116
|
};
|
|
107
117
|
export default /*#__PURE__*/React.createContext(makeConfig());
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function renderConnectionErrorToolbar(origin: string, fontdueUrl: string): void;
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import { createRoot } from 'react-dom/client';
|
|
3
|
+
|
|
4
|
+
// Connection-error status, mounted in its own React root (like the modal it
|
|
5
|
+
// replaces) rather than inside the admin toolbar's component tree.
|
|
6
|
+
//
|
|
7
|
+
// A CORS/connection outage commonly makes the consumer's own Relay-driven
|
|
8
|
+
// components throw, and their error boundary can replace the whole route
|
|
9
|
+
// wholesale — which would take an in-tree status down with it (verified in a
|
|
10
|
+
// single-tree framework: the page became an "Oops!" error page and the toolbar
|
|
11
|
+
// vanished). An independent root keeps this status visible regardless, while
|
|
12
|
+
// reusing the admin toolbar's restrained styling so it reads as the same small
|
|
13
|
+
// platform chrome — not the old full-screen modal.
|
|
14
|
+
//
|
|
15
|
+
// It deliberately takes no contexts (Relay, config, url): a blocked connection
|
|
16
|
+
// can't confirm an admin anyway, so this shows to whoever is on the page, with
|
|
17
|
+
// just the origin + Fontdue URL the detector hands it.
|
|
18
|
+
|
|
19
|
+
// Deep link to where an admin adds allowed origins, derived from the blocked
|
|
20
|
+
// request's own URL. Null if it can't be parsed, so the link is simply omitted.
|
|
21
|
+
function settingsUrl(fontdueUrl) {
|
|
22
|
+
try {
|
|
23
|
+
return `${new URL(fontdueUrl).origin}/admin/settings/integration`;
|
|
24
|
+
} catch {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
function ConnectionErrorToolbar(_ref) {
|
|
29
|
+
let {
|
|
30
|
+
origin,
|
|
31
|
+
fontdueUrl
|
|
32
|
+
} = _ref;
|
|
33
|
+
const [open, setOpen] = useState(false);
|
|
34
|
+
const fixUrl = settingsUrl(fontdueUrl);
|
|
35
|
+
return /*#__PURE__*/React.createElement("div", {
|
|
36
|
+
className: "fontdue-admin-toolbar",
|
|
37
|
+
"data-connection-error": "true"
|
|
38
|
+
}, open && /*#__PURE__*/React.createElement("div", {
|
|
39
|
+
className: "fontdue-admin-toolbar__panel"
|
|
40
|
+
}, /*#__PURE__*/React.createElement("div", {
|
|
41
|
+
className: "fontdue-admin-toolbar__header"
|
|
42
|
+
}, /*#__PURE__*/React.createElement("span", {
|
|
43
|
+
className: "fontdue-admin-toolbar__title"
|
|
44
|
+
}, "Fontdue")), /*#__PURE__*/React.createElement("div", {
|
|
45
|
+
className: "fontdue-admin-toolbar__error-status",
|
|
46
|
+
role: "alert"
|
|
47
|
+
}, /*#__PURE__*/React.createElement("p", {
|
|
48
|
+
className: "fontdue-admin-toolbar__error-status-text"
|
|
49
|
+
}, /*#__PURE__*/React.createElement(WarningIcon, null), "Fontdue couldn\u2019t be reached from this site."), /*#__PURE__*/React.createElement("p", {
|
|
50
|
+
className: "fontdue-admin-toolbar__hint"
|
|
51
|
+
}, "This is most likely a cross-origin (CORS) setting \u2014 ", origin, " isn\u2019t on Fontdue\u2019s allowed list. Add it under Settings \u2192 Integration and this page will reload automatically."), /*#__PURE__*/React.createElement("p", {
|
|
52
|
+
className: "fontdue-admin-toolbar__hint"
|
|
53
|
+
}, "If it\u2019s already listed, the connection may be failing for another reason \u2014 a network problem or a temporary Fontdue outage."), fixUrl && /*#__PURE__*/React.createElement("a", {
|
|
54
|
+
className: "fontdue-admin-toolbar__action",
|
|
55
|
+
href: fixUrl,
|
|
56
|
+
target: "_blank",
|
|
57
|
+
rel: "noreferrer"
|
|
58
|
+
}, "Open integration settings \u2197")), /*#__PURE__*/React.createElement("p", {
|
|
59
|
+
className: "fontdue-admin-toolbar__meta"
|
|
60
|
+
}, "Shown because Fontdue couldn\u2019t load on this page.")), /*#__PURE__*/React.createElement("button", {
|
|
61
|
+
type: "button",
|
|
62
|
+
className: "fontdue-admin-toolbar__button",
|
|
63
|
+
onClick: () => setOpen(v => !v),
|
|
64
|
+
"aria-expanded": open
|
|
65
|
+
}, /*#__PURE__*/React.createElement(WarningIcon, null), "Fontdue \xB7 connection issue"));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Small inline warning glyph (a triangle with an exclamation), matching the
|
|
69
|
+
// admin toolbar's. Inherits the surrounding text color via currentColor.
|
|
70
|
+
function WarningIcon() {
|
|
71
|
+
return /*#__PURE__*/React.createElement("svg", {
|
|
72
|
+
className: "fontdue-admin-toolbar__warning-icon",
|
|
73
|
+
width: "13",
|
|
74
|
+
height: "13",
|
|
75
|
+
viewBox: "0 0 16 16",
|
|
76
|
+
fill: "none",
|
|
77
|
+
stroke: "currentColor",
|
|
78
|
+
strokeWidth: "1.4",
|
|
79
|
+
"aria-hidden": "true",
|
|
80
|
+
focusable: "false"
|
|
81
|
+
}, /*#__PURE__*/React.createElement("path", {
|
|
82
|
+
d: "M8 1.5 15 14H1L8 1.5Z",
|
|
83
|
+
strokeLinejoin: "miter"
|
|
84
|
+
}), /*#__PURE__*/React.createElement("path", {
|
|
85
|
+
d: "M8 6v3.5",
|
|
86
|
+
strokeLinecap: "square"
|
|
87
|
+
}), /*#__PURE__*/React.createElement("path", {
|
|
88
|
+
d: "M8 11.5v.5",
|
|
89
|
+
strokeLinecap: "square"
|
|
90
|
+
}));
|
|
91
|
+
}
|
|
92
|
+
function mount(origin, fontdueUrl) {
|
|
93
|
+
const container = document.createElement('div');
|
|
94
|
+
document.body.appendChild(container);
|
|
95
|
+
createRoot(container).render( /*#__PURE__*/React.createElement(ConnectionErrorToolbar, {
|
|
96
|
+
origin: origin,
|
|
97
|
+
fontdueUrl: fontdueUrl
|
|
98
|
+
}));
|
|
99
|
+
}
|
|
100
|
+
export function renderConnectionErrorToolbar(origin, fontdueUrl) {
|
|
101
|
+
if (document.body) {
|
|
102
|
+
mount(origin, fontdueUrl);
|
|
103
|
+
} else {
|
|
104
|
+
document.addEventListener('DOMContentLoaded', () => mount(origin, fontdueUrl));
|
|
105
|
+
}
|
|
106
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import React, { type JSX } from 'react';
|
|
2
|
+
import { VerticalMetrics } from '../useFontStyle.js';
|
|
2
3
|
import { FontStyle_fontStyle$key } from '../../__generated__/FontStyle_fontStyle.graphql.js';
|
|
3
4
|
interface FontStyleProps {
|
|
4
5
|
fontStyle: FontStyle_fontStyle$key;
|
|
@@ -7,6 +8,7 @@ interface FontStyleProps {
|
|
|
7
8
|
children: ((props: {
|
|
8
9
|
loaded: boolean;
|
|
9
10
|
style: React.CSSProperties;
|
|
11
|
+
verticalMetrics: VerticalMetrics | null;
|
|
10
12
|
}) => React.ReactNode) | React.ReactNode;
|
|
11
13
|
}
|
|
12
14
|
export default function FontStyle({ Component, fontStyle: fontStyleKey, style, children, ...rest }: Omit<React.HTMLAttributes<HTMLDivElement | HTMLSpanElement>, 'children'> & FontStyleProps): JSX.Element;
|
|
@@ -13,7 +13,8 @@ export default function FontStyle(_ref) {
|
|
|
13
13
|
const fontStyle = useFragment((_FontStyle_fontStyle.hash && _FontStyle_fontStyle.hash !== "7891603bea1a3e40f40297bf1697eb3c" && console.error("The definition of 'FontStyle_fontStyle' appears to have changed. Run `relay-compiler` to update the generated files to receive the expected data."), _FontStyle_fontStyle), fontStyleKey);
|
|
14
14
|
const {
|
|
15
15
|
style: fontStyleStyle,
|
|
16
|
-
loaded
|
|
16
|
+
loaded,
|
|
17
|
+
verticalMetrics
|
|
17
18
|
} = useFontStyle({
|
|
18
19
|
fontStyle
|
|
19
20
|
});
|
|
@@ -26,6 +27,7 @@ export default function FontStyle(_ref) {
|
|
|
26
27
|
};
|
|
27
28
|
return /*#__PURE__*/React.createElement(Component, renderProps, typeof children === 'function' ? children({
|
|
28
29
|
loaded,
|
|
29
|
-
style: fontStyleStyle
|
|
30
|
+
style: fontStyleStyle,
|
|
31
|
+
verticalMetrics
|
|
30
32
|
}) : children);
|
|
31
33
|
}
|
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import _FontdueAdminToolbarTokenMutation from "../../__generated__/FontdueAdminToolbarTokenMutation.graphql.js";
|
|
4
|
+
import _FontdueAdminToolbarQuery from "../../__generated__/FontdueAdminToolbarQuery.graphql.js";
|
|
5
|
+
import React, { useContext, useEffect, useState } from 'react';
|
|
6
|
+
import { commitMutation, fetchQuery, graphql, useRelayEnvironment } from 'react-relay';
|
|
7
|
+
import ConfigContext from '../ConfigContext.js';
|
|
8
|
+
import { useFontdueUrl } from '../UrlContext.js';
|
|
9
|
+
import { PREVIEW_ENDPOINT, DEFAULT_PREVIEW_TTL_MS, setPreviewMarkerCookie } from '../../preview/constants.js';
|
|
10
|
+
import { readPreviewState, expiresAtFromTokenResult, isSessionExpiredError } from './previewState.js';
|
|
11
|
+
import { fontdueBaseUrl, version } from '../../relay/environment.js';
|
|
12
|
+
// Admin-only affordance for logged-in foundry admins: reveal unpublished
|
|
13
|
+
// ("hidden") fonts across the whole storefront. Storefront pages are
|
|
14
|
+
// server-rendered sessionless and cached, and the provider's own query is
|
|
15
|
+
// preloaded server-side without the session — so `adminUser` can't ride it.
|
|
16
|
+
// Admin presence is detected here, client-side, where the request carries
|
|
17
|
+
// the session; the public never sees this control.
|
|
18
|
+
//
|
|
19
|
+
// The reveal itself is not done here: entering preview brokers a short-lived
|
|
20
|
+
// admin token and hands it to the framework's preview route, which switches
|
|
21
|
+
// the whole app to a live, token-bearing render. Exiting clears it. Public
|
|
22
|
+
// renders never touch any of this and stay static and cached.
|
|
23
|
+
const adminQuery = (_FontdueAdminToolbarQuery.hash && _FontdueAdminToolbarQuery.hash !== "8660b45f086137d249eee82459ad648d" && console.error("The definition of 'FontdueAdminToolbarQuery' appears to have changed. Run `relay-compiler` to update the generated files to receive the expected data."), _FontdueAdminToolbarQuery);
|
|
24
|
+
|
|
25
|
+
// createAdminToken mints a short-lived, stateless admin token from the admin
|
|
26
|
+
// session; it's handed to the preview route to enter draft mode.
|
|
27
|
+
const tokenMutation = (_FontdueAdminToolbarTokenMutation.hash && _FontdueAdminToolbarTokenMutation.hash !== "2b82f195747c86d50fd5884d76f3b709" && console.error("The definition of 'FontdueAdminToolbarTokenMutation' appears to have changed. Run `relay-compiler` to update the generated files to receive the expected data."), _FontdueAdminToolbarTokenMutation);
|
|
28
|
+
export default function FontdueAdminToolbar() {
|
|
29
|
+
const environment = useRelayEnvironment();
|
|
30
|
+
const previewConfig = useContext(ConfigContext).preview;
|
|
31
|
+
// The preview entry/exit route is part of the portable preview contract (see
|
|
32
|
+
// fontdue-js/preview); it defaults to '/api/preview' but is configurable so
|
|
33
|
+
// non-Next apps can mount it where their router expects. The revalidate route
|
|
34
|
+
// is opt-in (no default) — see config.preview.revalidateEndpoint.
|
|
35
|
+
const previewEndpoint = (previewConfig === null || previewConfig === void 0 ? void 0 : previewConfig.endpoint) ?? PREVIEW_ENDPOINT;
|
|
36
|
+
const revalidateEndpoint = previewConfig === null || previewConfig === void 0 ? void 0 : previewConfig.revalidateEndpoint;
|
|
37
|
+
// Client-only embeds (the script-tag CDN bundle, a client SPA) have no server
|
|
38
|
+
// preview route to broker a token through, so they enter/exit preview by
|
|
39
|
+
// toggling the readable marker cookie directly — see setPreviewMarkerCookie.
|
|
40
|
+
const clientSidePreview = (previewConfig === null || previewConfig === void 0 ? void 0 : previewConfig.clientSide) ?? false;
|
|
41
|
+
|
|
42
|
+
// Where the admin is signed in: the configured Fontdue origin. Used for the
|
|
43
|
+
// "signed in at" line and the deep link back to the admin. Falls back to the
|
|
44
|
+
// URL the embed was initialized with (the script-tag bundle sets no
|
|
45
|
+
// FONTDUE_URL env); undefined (e.g. multi-tenant) drops both gracefully.
|
|
46
|
+
const contextUrl = useFontdueUrl();
|
|
47
|
+
const fontdueUrl = fontdueBaseUrl() ?? (contextUrl || undefined);
|
|
48
|
+
const fontdueHost = fontdueUrl ? safeHost(fontdueUrl) : null;
|
|
49
|
+
const adminUrl = fontdueUrl ? `${fontdueUrl.replace(/\/+$/, '')}/admin` : null;
|
|
50
|
+
const [adminName, setAdminName] = useState(null);
|
|
51
|
+
const [ready, setReady] = useState(false);
|
|
52
|
+
const [open, setOpen] = useState(false);
|
|
53
|
+
const [previewState, setPreviewState] = useState('off');
|
|
54
|
+
const [busy, setBusy] = useState(false);
|
|
55
|
+
const [error, setError] = useState(null);
|
|
56
|
+
const [notice, setNotice] = useState(null);
|
|
57
|
+
const previewing = previewState === 'active';
|
|
58
|
+
const expired = previewState === 'expired';
|
|
59
|
+
useEffect(() => {
|
|
60
|
+
setPreviewState(readPreviewState());
|
|
61
|
+
// network-only: the provider's preloaded query already wrote a sessionless
|
|
62
|
+
// `viewer` to the store, so we must go to the network (with the session)
|
|
63
|
+
// rather than read that cached, admin-blind copy.
|
|
64
|
+
const subscription = fetchQuery(environment, adminQuery, {}, {
|
|
65
|
+
fetchPolicy: 'network-only'
|
|
66
|
+
}).subscribe({
|
|
67
|
+
next: data => {
|
|
68
|
+
var _data$viewer;
|
|
69
|
+
const admin = (_data$viewer = data.viewer) === null || _data$viewer === void 0 ? void 0 : _data$viewer.adminUser;
|
|
70
|
+
if (admin) setAdminName(admin.name ?? 'admin');
|
|
71
|
+
},
|
|
72
|
+
error: () => setReady(true),
|
|
73
|
+
complete: () => setReady(true)
|
|
74
|
+
});
|
|
75
|
+
return () => subscription.unsubscribe();
|
|
76
|
+
}, [environment]);
|
|
77
|
+
|
|
78
|
+
// Re-derive the state when the tab regains focus or becomes visible. A tab
|
|
79
|
+
// left open past the token's expiry won't otherwise notice — there's no
|
|
80
|
+
// event when a cookie's encoded expiry passes — so the warning would never
|
|
81
|
+
// appear until the next reload. Recovery stays user-initiated; we only
|
|
82
|
+
// refresh what state is shown.
|
|
83
|
+
useEffect(() => {
|
|
84
|
+
if (typeof window === 'undefined') return;
|
|
85
|
+
const recheck = () => setPreviewState(readPreviewState());
|
|
86
|
+
window.addEventListener('focus', recheck);
|
|
87
|
+
document.addEventListener('visibilitychange', recheck);
|
|
88
|
+
return () => {
|
|
89
|
+
window.removeEventListener('focus', recheck);
|
|
90
|
+
document.removeEventListener('visibilitychange', recheck);
|
|
91
|
+
};
|
|
92
|
+
}, []);
|
|
93
|
+
|
|
94
|
+
// Hidden until we've confirmed an admin session. The public always lands here.
|
|
95
|
+
if (!ready || !adminName) return null;
|
|
96
|
+
async function startPreview(token, expiresAt) {
|
|
97
|
+
const res = await fetch(previewEndpoint, {
|
|
98
|
+
method: 'POST',
|
|
99
|
+
headers: {
|
|
100
|
+
'content-type': 'application/json'
|
|
101
|
+
},
|
|
102
|
+
body: JSON.stringify({
|
|
103
|
+
token,
|
|
104
|
+
expiresAt
|
|
105
|
+
})
|
|
106
|
+
});
|
|
107
|
+
if (res.ok) {
|
|
108
|
+
window.location.reload();
|
|
109
|
+
} else {
|
|
110
|
+
setBusy(false);
|
|
111
|
+
setError('Could not start preview.');
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
function enterPreview() {
|
|
115
|
+
setBusy(true);
|
|
116
|
+
setError(null);
|
|
117
|
+
setNotice(null);
|
|
118
|
+
// Client-only: no server render needs a token, so just mark preview on and
|
|
119
|
+
// reload — the admin's session cookie rides the next fetches and reveals
|
|
120
|
+
// hidden fonts now that the marker makes the client send `fontdue-preview`.
|
|
121
|
+
// There's no token to mint, so the marker just gets the default 1h expiry.
|
|
122
|
+
if (clientSidePreview) {
|
|
123
|
+
setPreviewMarkerCookie(Date.now() + DEFAULT_PREVIEW_TTL_MS);
|
|
124
|
+
window.location.reload();
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
commitMutation(environment, {
|
|
128
|
+
mutation: tokenMutation,
|
|
129
|
+
variables: {},
|
|
130
|
+
onCompleted: (res, errors) => {
|
|
131
|
+
var _res$createAdminToken;
|
|
132
|
+
const token = (_res$createAdminToken = res.createAdminToken) === null || _res$createAdminToken === void 0 ? void 0 : _res$createAdminToken.token;
|
|
133
|
+
if (errors && errors.length > 0 || !token) {
|
|
134
|
+
setBusy(false);
|
|
135
|
+
// createAdminToken is gated on the admin session (current_user); a
|
|
136
|
+
// "Not authorized" error means the session itself has lapsed, which
|
|
137
|
+
// needs a fresh sign-in, not a retry. Surface that distinctly and
|
|
138
|
+
// clear the now-meaningless marker so the toolbar resets.
|
|
139
|
+
if (isSessionExpiredError(errors)) {
|
|
140
|
+
setPreviewMarkerCookie(false);
|
|
141
|
+
setPreviewState('off');
|
|
142
|
+
setError('Your session expired — sign in again.');
|
|
143
|
+
} else {
|
|
144
|
+
setError('Could not start preview.');
|
|
145
|
+
}
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
startPreview(token, expiresAtFromTokenResult(res.createAdminToken));
|
|
149
|
+
},
|
|
150
|
+
onError: () => {
|
|
151
|
+
setBusy(false);
|
|
152
|
+
setError('Could not start preview.');
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
async function exitPreview() {
|
|
157
|
+
setBusy(true);
|
|
158
|
+
setError(null);
|
|
159
|
+
setNotice(null);
|
|
160
|
+
if (clientSidePreview) {
|
|
161
|
+
setPreviewMarkerCookie(false);
|
|
162
|
+
} else {
|
|
163
|
+
await fetch(previewEndpoint, {
|
|
164
|
+
method: 'DELETE'
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
window.location.reload();
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Recover from the expired state: mint a fresh token and re-enter. This is the
|
|
171
|
+
// same enter flow — a successful POST overwrites the stale token + marker
|
|
172
|
+
// cookies and reloads, so there's nothing to clear first. If the session has
|
|
173
|
+
// also lapsed, enterPreview surfaces the distinct "sign in again" message.
|
|
174
|
+
function reEnterPreview() {
|
|
175
|
+
enterPreview();
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Purge the cached storefront so public visitors see freshly published (or
|
|
179
|
+
// newly hidden) content. Targets the configured revalidate route as-is.
|
|
180
|
+
async function refreshCache() {
|
|
181
|
+
if (!revalidateEndpoint) return;
|
|
182
|
+
setBusy(true);
|
|
183
|
+
setError(null);
|
|
184
|
+
setNotice(null);
|
|
185
|
+
try {
|
|
186
|
+
const res = await fetch(revalidateEndpoint, {
|
|
187
|
+
method: 'POST'
|
|
188
|
+
});
|
|
189
|
+
if (!res.ok) throw new Error(`status ${res.status}`);
|
|
190
|
+
setNotice('Public cache refreshed.');
|
|
191
|
+
} catch {
|
|
192
|
+
setError('Could not refresh the cache.');
|
|
193
|
+
} finally {
|
|
194
|
+
setBusy(false);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
return /*#__PURE__*/React.createElement("div", {
|
|
198
|
+
className: "fontdue-admin-toolbar",
|
|
199
|
+
"data-previewing": previewing,
|
|
200
|
+
"data-preview-state": previewState,
|
|
201
|
+
"data-testid": "fontdue-admin-toolbar"
|
|
202
|
+
}, open && /*#__PURE__*/React.createElement("div", {
|
|
203
|
+
className: "fontdue-admin-toolbar__panel"
|
|
204
|
+
}, /*#__PURE__*/React.createElement("div", {
|
|
205
|
+
className: "fontdue-admin-toolbar__header"
|
|
206
|
+
}, /*#__PURE__*/React.createElement("span", {
|
|
207
|
+
className: "fontdue-admin-toolbar__title"
|
|
208
|
+
}, "Fontdue"), /*#__PURE__*/React.createElement("span", {
|
|
209
|
+
className: "fontdue-admin-toolbar__user"
|
|
210
|
+
}, adminName)), expired ? /*#__PURE__*/React.createElement("div", {
|
|
211
|
+
className: "fontdue-admin-toolbar__expired",
|
|
212
|
+
role: "status",
|
|
213
|
+
"data-testid": "preview-expired"
|
|
214
|
+
}, /*#__PURE__*/React.createElement("p", {
|
|
215
|
+
className: "fontdue-admin-toolbar__expired-text"
|
|
216
|
+
}, /*#__PURE__*/React.createElement(WarningIcon, null), "Preview expired. Hidden fonts are no longer shown."), /*#__PURE__*/React.createElement("button", {
|
|
217
|
+
type: "button",
|
|
218
|
+
className: "fontdue-admin-toolbar__action",
|
|
219
|
+
onClick: reEnterPreview,
|
|
220
|
+
disabled: busy,
|
|
221
|
+
"data-testid": "reenter-preview-button"
|
|
222
|
+
}, "Re-enter preview")) : /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("label", {
|
|
223
|
+
className: "fontdue-admin-toolbar__toggle"
|
|
224
|
+
}, /*#__PURE__*/React.createElement("input", {
|
|
225
|
+
type: "checkbox",
|
|
226
|
+
checked: previewing,
|
|
227
|
+
disabled: busy,
|
|
228
|
+
onChange: e => e.target.checked ? enterPreview() : exitPreview(),
|
|
229
|
+
"data-testid": "preview-toggle"
|
|
230
|
+
}), /*#__PURE__*/React.createElement("span", null, "Preview hidden fonts")), /*#__PURE__*/React.createElement("p", {
|
|
231
|
+
className: "fontdue-admin-toolbar__hint"
|
|
232
|
+
}, previewing ? 'Unpublished fonts are visible across the site — only to you.' : 'Reveal unpublished fonts everywhere, only for you.')), (revalidateEndpoint || adminUrl) && /*#__PURE__*/React.createElement("div", {
|
|
233
|
+
className: "fontdue-admin-toolbar__actions"
|
|
234
|
+
}, revalidateEndpoint && /*#__PURE__*/React.createElement("button", {
|
|
235
|
+
type: "button",
|
|
236
|
+
className: "fontdue-admin-toolbar__action",
|
|
237
|
+
onClick: refreshCache,
|
|
238
|
+
disabled: busy,
|
|
239
|
+
"data-testid": "revalidate-button"
|
|
240
|
+
}, "Refresh public cache"), adminUrl && /*#__PURE__*/React.createElement("a", {
|
|
241
|
+
className: "fontdue-admin-toolbar__action",
|
|
242
|
+
href: adminUrl,
|
|
243
|
+
target: "_blank",
|
|
244
|
+
rel: "noreferrer",
|
|
245
|
+
"data-testid": "open-admin-link"
|
|
246
|
+
}, "Open Fontdue admin \u2197")), notice && /*#__PURE__*/React.createElement("p", {
|
|
247
|
+
className: "fontdue-admin-toolbar__notice"
|
|
248
|
+
}, notice), error && /*#__PURE__*/React.createElement("p", {
|
|
249
|
+
className: "fontdue-admin-toolbar__error"
|
|
250
|
+
}, error), /*#__PURE__*/React.createElement("p", {
|
|
251
|
+
className: "fontdue-admin-toolbar__meta"
|
|
252
|
+
}, fontdueHost ? `Shown because you’re signed in to Fontdue at ${fontdueHost}.` : 'Shown because you’re signed in to Fontdue.', /*#__PURE__*/React.createElement("span", {
|
|
253
|
+
className: "fontdue-admin-toolbar__version"
|
|
254
|
+
}, "fontdue-js ", version))), /*#__PURE__*/React.createElement("button", {
|
|
255
|
+
type: "button",
|
|
256
|
+
className: "fontdue-admin-toolbar__button",
|
|
257
|
+
onClick: () => setOpen(v => !v),
|
|
258
|
+
"aria-expanded": open,
|
|
259
|
+
"data-testid": "fontdue-admin-toolbar-button"
|
|
260
|
+
}, expired ? /*#__PURE__*/React.createElement(WarningIcon, null) : /*#__PURE__*/React.createElement("span", {
|
|
261
|
+
className: "fontdue-admin-toolbar__dot",
|
|
262
|
+
"aria-hidden": "true"
|
|
263
|
+
}), "Fontdue", previewing ? ' · previewing' : '', expired ? ' · preview expired' : ''));
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Small inline warning glyph (a triangle with an exclamation). Inherits the
|
|
267
|
+
// surrounding text color so it stays monochrome with the toolbar chrome.
|
|
268
|
+
function WarningIcon() {
|
|
269
|
+
return /*#__PURE__*/React.createElement("svg", {
|
|
270
|
+
className: "fontdue-admin-toolbar__warning-icon",
|
|
271
|
+
width: "13",
|
|
272
|
+
height: "13",
|
|
273
|
+
viewBox: "0 0 16 16",
|
|
274
|
+
fill: "none",
|
|
275
|
+
stroke: "currentColor",
|
|
276
|
+
strokeWidth: "1.4",
|
|
277
|
+
"aria-hidden": "true",
|
|
278
|
+
focusable: "false"
|
|
279
|
+
}, /*#__PURE__*/React.createElement("path", {
|
|
280
|
+
d: "M8 1.5 15 14H1L8 1.5Z",
|
|
281
|
+
strokeLinejoin: "miter"
|
|
282
|
+
}), /*#__PURE__*/React.createElement("path", {
|
|
283
|
+
d: "M8 6v3.5",
|
|
284
|
+
strokeLinecap: "square"
|
|
285
|
+
}), /*#__PURE__*/React.createElement("path", {
|
|
286
|
+
d: "M8 11.5v.5",
|
|
287
|
+
strokeLinecap: "square"
|
|
288
|
+
}));
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// Hostname of a base URL, or the raw value if it can't be parsed (so a
|
|
292
|
+
// misconfigured URL still shows something rather than throwing).
|
|
293
|
+
function safeHost(url) {
|
|
294
|
+
try {
|
|
295
|
+
return new URL(url).host;
|
|
296
|
+
} catch {
|
|
297
|
+
return url;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export type PreviewState = 'off' | 'active' | 'expired';
|
|
2
|
+
export declare function derivePreviewState(hasMarker: boolean, expiresAt: number | undefined, now?: number): PreviewState;
|
|
3
|
+
export declare function readPreviewState(): PreviewState;
|
|
4
|
+
export declare function expiresAtFromTokenResult(result: unknown): number;
|
|
5
|
+
export declare function isSessionExpiredError(errors: ReadonlyArray<{
|
|
6
|
+
message?: string | null;
|
|
7
|
+
}> | null | undefined): boolean;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// Pure preview-state logic for the admin toolbar, split out from the component
|
|
2
|
+
// so it can be unit-tested without a browser, a Relay environment, or the
|
|
3
|
+
// `graphql` Babel transform (importing the .tsx triggers the tag at load).
|
|
4
|
+
//
|
|
5
|
+
// The preview token is a stateless, time-only-expiring Phoenix.Token. There is
|
|
6
|
+
// no way to learn its validity client-side (it's httpOnly, and the toolbar's
|
|
7
|
+
// admin check rides the longer-lived *session* cookie, not the token), so the
|
|
8
|
+
// toolbar tracks the token's *expiry* instead and derives three states from the
|
|
9
|
+
// readable marker cookie:
|
|
10
|
+
//
|
|
11
|
+
// - 'off' — no marker cookie: not previewing.
|
|
12
|
+
// - 'active' — marker present and now < expiresAt: previewing.
|
|
13
|
+
// - 'expired' — marker present and now >= expiresAt: the token has lapsed,
|
|
14
|
+
// so server renders have silently fallen back to the public
|
|
15
|
+
// catalog. Show a warning and offer "re-enter".
|
|
16
|
+
|
|
17
|
+
import { DEFAULT_PREVIEW_TTL_MS, hasPreviewMarkerCookie, getPreviewExpiry } from '../../preview/constants.js';
|
|
18
|
+
// Pure derivation. `hasMarker` and `expiresAt` come from the marker cookie;
|
|
19
|
+
// `now` is injectable for tests.
|
|
20
|
+
export function derivePreviewState(hasMarker, expiresAt) {
|
|
21
|
+
let now = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : Date.now();
|
|
22
|
+
if (!hasMarker) return 'off';
|
|
23
|
+
// A marker with no/garbled expiry (e.g. a stale legacy `=1` cookie) is
|
|
24
|
+
// treated as expired: safer to prompt a re-enter than to imply it's live.
|
|
25
|
+
if (expiresAt === undefined || now >= expiresAt) return 'expired';
|
|
26
|
+
return 'active';
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Read the current preview state straight off the document's cookies.
|
|
30
|
+
export function readPreviewState() {
|
|
31
|
+
return derivePreviewState(hasPreviewMarkerCookie(), getPreviewExpiry());
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Pull an expiry out of the createAdminToken response. The backend will grow an
|
|
35
|
+
// `expiresAt` field (ISO-8601 or epoch-ms); until it does, the field is absent
|
|
36
|
+
// and we fall back to now + DEFAULT_PREVIEW_TTL_MS — the canonical 1h TTL lives
|
|
37
|
+
// in the backend `token.ex` `:admin_token` `max_age`. Read defensively so the
|
|
38
|
+
// client and backend halves can ship independently.
|
|
39
|
+
export function expiresAtFromTokenResult(result) {
|
|
40
|
+
const value = result === null || result === void 0 ? void 0 : result.expiresAt;
|
|
41
|
+
if (typeof value === 'number' && Number.isFinite(value)) return value;
|
|
42
|
+
if (typeof value === 'string' && value !== '') {
|
|
43
|
+
const asNumber = Number(value);
|
|
44
|
+
if (Number.isFinite(asNumber)) return asNumber;
|
|
45
|
+
const asDate = Date.parse(value);
|
|
46
|
+
if (!Number.isNaN(asDate)) return asDate;
|
|
47
|
+
}
|
|
48
|
+
return Date.now() + DEFAULT_PREVIEW_TTL_MS;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// True when the createAdminToken mutation failed because the admin *session*
|
|
52
|
+
// (not the preview token) has expired. The resolver returns a "Not authorized"
|
|
53
|
+
// GraphQL error when current_user is nil, so we match that message rather than
|
|
54
|
+
// treating every failure as a recoverable retry.
|
|
55
|
+
export function isSessionExpiredError(errors) {
|
|
56
|
+
if (!errors) return false;
|
|
57
|
+
return errors.some(e => /not authoriz/i.test(e.message ?? ''));
|
|
58
|
+
}
|
|
@@ -8,7 +8,7 @@ import ConfigContext, { makeConfig, mergeConfig } from '../ConfigContext.js';
|
|
|
8
8
|
import { createDefaultStore } from '../../reducer.js';
|
|
9
9
|
import ComponentsContext from '../ComponentsContext.js';
|
|
10
10
|
import UrlContext from '../UrlContext.js';
|
|
11
|
-
import {
|
|
11
|
+
import { setConnectionErrorUiEnabled } from '../../corsError.js';
|
|
12
12
|
import TypeTesterFamiliesProvider from '../TypeTester/TypeTesterFamilies.js';
|
|
13
13
|
|
|
14
14
|
// Marker context used by self-wrapping components to detect a parent
|
|
@@ -126,7 +126,9 @@ export default function FontdueContextProvider(_ref3) {
|
|
|
126
126
|
stripeIntegration
|
|
127
127
|
});
|
|
128
128
|
const configValue = makeConfig(config);
|
|
129
|
-
|
|
129
|
+
// `corsErrorModal` is the legacy config name; it now gates the toolbar's
|
|
130
|
+
// connection-error status rather than a modal.
|
|
131
|
+
setConnectionErrorUiEnabled(configValue.corsErrorModal);
|
|
130
132
|
const sharedStore = useSharedStore(store, config);
|
|
131
133
|
return /*#__PURE__*/React.createElement(FontdueContextMarker.Provider, {
|
|
132
134
|
value: true
|
|
@@ -13,6 +13,11 @@ import retryImport from '../../retryImport.js';
|
|
|
13
13
|
import useSerializablePreloadedQuery from '../../relay/useSerializablePreloadedQuery.js';
|
|
14
14
|
const ConsentBanner = /*#__PURE__*/lazy(() => retryImport(() => import('../ConsentBanner/index.js')));
|
|
15
15
|
const Tracking = /*#__PURE__*/lazy(() => retryImport(() => import('../Tracking/index.js')));
|
|
16
|
+
// Admin-only; self-gates to an admin session and renders nothing otherwise, so
|
|
17
|
+
// it's safe to mount for everyone. It detects admins client-side (storefront
|
|
18
|
+
// renders are sessionless), so it needs no fragment on the provider query.
|
|
19
|
+
// Lazy so it stays out of the provider's main chunk.
|
|
20
|
+
const FontdueAdminToolbar = /*#__PURE__*/lazy(() => retryImport(() => import('../FontdueAdminToolbar/index.js')));
|
|
16
21
|
|
|
17
22
|
// Single layout-level query that spreads every aux UI component's fragment.
|
|
18
23
|
// Components declare their own data needs via fragments — adding a new aux
|
|
@@ -72,6 +77,6 @@ function AuxUI(_ref4) {
|
|
|
72
77
|
codeConfig: codeConfig
|
|
73
78
|
}, /*#__PURE__*/React.createElement(Suspense, {
|
|
74
79
|
fallback: null
|
|
75
|
-
}, /*#__PURE__*/React.createElement(ConsentBanner, null), /*#__PURE__*/React.createElement(Tracking, null))));
|
|
80
|
+
}, /*#__PURE__*/React.createElement(ConsentBanner, null), /*#__PURE__*/React.createElement(Tracking, null), /*#__PURE__*/React.createElement(FontdueAdminToolbar, null))));
|
|
76
81
|
}
|
|
77
82
|
export default FontdueProvider;
|