fontdue-js 3.5.1 → 3.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +19 -0
- package/README.md +2 -0
- package/dist/__generated__/CheckoutUpdateCustomerMutation.graphql.d.ts +3 -1
- package/dist/__generated__/CheckoutUpdateCustomerMutation.graphql.js +1 -1
- package/dist/__generated__/CustomerSignInRequestLoginCodeMutation.graphql.d.ts +24 -0
- package/dist/__generated__/CustomerSignInRequestLoginCodeMutation.graphql.js +71 -0
- package/dist/__generated__/CustomerSignInVerifyLoginCodeMutation.graphql.d.ts +30 -0
- package/dist/__generated__/CustomerSignInVerifyLoginCodeMutation.graphql.js +98 -0
- package/dist/__generated__/FontFamiliesQuery.graphql.d.ts +4 -1
- package/dist/__generated__/FontFamiliesQuery.graphql.js +46 -39
- package/dist/__generated__/NewsletterSignupUpdateCustomerMutation.graphql.d.ts +3 -1
- package/dist/__generated__/NewsletterSignupUpdateCustomerMutation.graphql.js +1 -1
- package/dist/__generated__/PriceBar_node.graphql.d.ts +4 -1
- package/dist/__generated__/PriceBar_node.graphql.js +23 -16
- package/dist/__generated__/StoreModalFamily_collection.graphql.d.ts +3 -2
- package/dist/__generated__/StoreModalFamily_collection.graphql.js +3 -3
- package/dist/__generated__/StoreModalIndexPaginationQuery.graphql.d.ts +23 -0
- package/dist/__generated__/StoreModalIndexPaginationQuery.graphql.js +303 -0
- package/dist/__generated__/StoreModalIndexQuery.graphql.d.ts +4 -9
- package/dist/__generated__/StoreModalIndexQuery.graphql.js +90 -53
- package/dist/__generated__/StoreModalIndex_viewer.graphql.d.ts +24 -0
- package/dist/__generated__/StoreModalIndex_viewer.graphql.js +134 -0
- package/dist/__generated__/StoreModalProductContent_collection.graphql.d.ts +4 -1
- package/dist/__generated__/StoreModalProductContent_collection.graphql.js +12 -5
- package/dist/__generated__/StoreModalProductLicenseSelection_collection.graphql.d.ts +2 -1
- package/dist/__generated__/StoreModalProductLicenseSelection_collection.graphql.js +27 -24
- package/dist/__generated__/StoreModalProductQuery.graphql.d.ts +2 -19
- package/dist/__generated__/StoreModalProductQuery.graphql.js +143 -174
- package/dist/__generated__/StoreModalProductRefetchQuery.graphql.d.ts +1 -1
- package/dist/__generated__/StoreModalProductRefetchQuery.graphql.js +54 -53
- package/dist/__generated__/StoreModalProductSlugQuery.graphql.d.ts +38 -0
- package/dist/__generated__/StoreModalProductSlugQuery.graphql.js +750 -0
- package/dist/__generated__/StoreModalProductViewer.graphql.d.ts +34 -0
- package/dist/__generated__/StoreModalProductViewer.graphql.js +149 -0
- package/dist/__generated__/StoreModalUnifiedCheckoutUpdateCustomerMutation.graphql.d.ts +3 -1
- package/dist/__generated__/StoreModalUnifiedCheckoutUpdateCustomerMutation.graphql.js +1 -1
- package/dist/__generated__/TestFontsFormUpdateCustomerMutation.graphql.d.ts +3 -1
- package/dist/__generated__/TestFontsFormUpdateCustomerMutation.graphql.js +1 -1
- package/dist/__generated__/TypeTestersIDQuery.graphql.d.ts +1 -1
- package/dist/__generated__/TypeTestersIDQuery.graphql.js +17 -10
- package/dist/__generated__/TypeTestersRefetchQuery.graphql.d.ts +1 -1
- package/dist/__generated__/TypeTestersRefetchQuery.graphql.js +17 -10
- package/dist/__generated__/TypeTestersSlugQuery.graphql.d.ts +1 -1
- package/dist/__generated__/TypeTestersSlugQuery.graphql.js +17 -10
- package/dist/__generated__/storefrontEventsTrackStorefrontEventMutation.graphql.d.ts +41 -0
- package/dist/__generated__/storefrontEventsTrackStorefrontEventMutation.graphql.js +65 -0
- package/dist/__generated__/useCurrentCustomerQuery.graphql.d.ts +23 -0
- package/dist/__generated__/{ServerConfigProviderQuery.graphql.js → useCurrentCustomerQuery.graphql.js} +24 -39
- package/dist/__tests__/bundleSelectionOrder.test.js +92 -0
- package/dist/__tests__/collectionBundleSelection.test.js +441 -1446
- package/dist/__tests__/customerSession.test.js +456 -0
- package/dist/__tests__/infiniteScroll.test.js +180 -0
- package/dist/__tests__/licenseExclusions.test.js +2 -1
- package/dist/__tests__/storeModalRoutes.test.js +118 -0
- package/dist/__tests__/storefrontEvents.test.js +167 -0
- package/dist/__tests__/stripeElements.test.js +173 -0
- package/dist/__tests__/typeTesterAutofitFeatures.test.js +138 -0
- package/dist/__tests__/typeTesterInitialSize.test.js +82 -0
- package/dist/__tests__/typeTesterTruncate.test.js +218 -0
- package/dist/auth/session.d.ts +42 -0
- package/dist/auth/session.js +139 -0
- package/dist/auth/tokenStore.d.ts +61 -0
- package/dist/auth/tokenStore.js +162 -0
- package/dist/components/Cart/Checkout.js +19 -9
- package/dist/components/Cart/storefrontEvents.d.ts +34 -0
- package/dist/components/Cart/storefrontEvents.js +138 -0
- package/dist/components/ConfigContext.d.ts +2 -2
- package/dist/components/CustomerSignIn/index.d.ts +33 -0
- package/dist/components/CustomerSignIn/index.js +170 -0
- package/dist/components/FontFamilies/index.js +4 -3
- package/dist/components/NewsletterSignup/index.js +5 -1
- package/dist/components/Precart/index.js +6 -0
- package/dist/components/PriceBar/index.js +5 -3
- package/dist/components/Root/index.js +4 -34
- package/dist/components/Root/productState.d.ts +2 -8
- package/dist/components/Root/productState.js +46 -57
- package/dist/components/SKUPrice/index.js +7 -8
- package/dist/components/StoreModal/StoreModalFamily.js +4 -2
- package/dist/components/StoreModal/StoreModalIndex.js +48 -7
- package/dist/components/StoreModal/StoreModalProduct.d.ts +5 -1
- package/dist/components/StoreModal/StoreModalProduct.js +69 -16
- package/dist/components/StoreModal/index.js +14 -14
- package/dist/components/StoreModal/routes.js +9 -1
- package/dist/components/StoreModalProductLicenseSelection/StoreModalProductLicense.d.ts +2 -1
- package/dist/components/StoreModalProductLicenseSelection/StoreModalProductLicense.js +8 -3
- package/dist/components/StoreModalProductLicenseSelection/index.js +3 -2
- package/dist/components/TestFontsForm/index.js +5 -1
- package/dist/components/TypeTester/TypeTesterContent.js +9 -2
- package/dist/components/TypeTester/TypeTesterContext.js +17 -4
- package/dist/components/TypeTester/TypeTesterFitter.js +7 -2
- package/dist/components/TypeTester/TypeTesterStandalone.d.ts +2 -0
- package/dist/components/TypeTester/TypeTesterStandalone.js +2 -0
- package/dist/components/TypeTester/TypeTesterStandaloneElement.d.ts +1 -0
- package/dist/components/TypeTester/TypeTesterStandaloneElement.js +4 -1
- package/dist/components/TypeTester/TypeTesterVariableAxes.js +16 -10
- package/dist/components/TypeTester/index.d.ts +3 -1
- package/dist/components/TypeTester/index.js +2 -1
- package/dist/components/TypeTester/truncate.d.ts +12 -0
- package/dist/components/TypeTester/truncate.js +95 -0
- package/dist/components/TypeTester/useTypeTesterStyler.d.ts +3 -1
- package/dist/components/TypeTester/useTypeTesterStyler.js +36 -7
- package/dist/components/TypeTesters/TypeTestersElement.d.ts +2 -1
- package/dist/components/TypeTesters/TypeTestersElement.js +4 -1
- package/dist/components/TypeTesters/index.d.ts +2 -0
- package/dist/components/elements/StoreModalContainer/StoreModalContainerContext.d.ts +1 -0
- package/dist/components/elements/StoreModalContainer/index.js +9 -4
- package/dist/components/elements/StoreModalIndexLoadMore/index.d.ts +8 -0
- package/dist/components/elements/StoreModalIndexLoadMore/index.js +21 -0
- package/dist/components/elements/StoreModalUnifiedCheckout.d.ts +1 -1
- package/dist/components/elements/StoreModalUnifiedCheckout.js +20 -17
- package/dist/components/useInfiniteScroll.d.ts +28 -0
- package/dist/components/useInfiniteScroll.js +50 -0
- package/dist/fontdue.css +63 -0
- package/dist/hooks/useAutofit.d.ts +2 -1
- package/dist/hooks/useAutofit.js +10 -4
- package/dist/hooks/useCurrentCustomer.d.ts +34 -0
- package/dist/hooks/useCurrentCustomer.js +93 -0
- package/dist/reducer.d.ts +5 -5
- package/dist/reducer.js +9 -87
- package/dist/relay/environment.js +91 -50
- package/dist/selection.d.ts +40 -0
- package/dist/selection.js +494 -0
- package/dist/storeModalRoutes.d.ts +38 -0
- package/dist/storeModalRoutes.js +110 -0
- package/dist/stripeElements.d.ts +18 -0
- package/dist/stripeElements.js +49 -0
- package/dist/utils.d.ts +1 -12
- package/dist/utils.js +1 -36
- package/dist/version.js +1 -1
- package/package.json +1 -1
- package/dist/__generated__/NodePasswordFormIDQuery.graphql.d.ts +0 -21
- package/dist/__generated__/NodePasswordFormIDQuery.graphql.js +0 -86
- package/dist/__generated__/NodePasswordFormSlugQuery.graphql.d.ts +0 -25
- package/dist/__generated__/NodePasswordFormSlugQuery.graphql.js +0 -108
- package/dist/__generated__/ServerConfigProviderQuery.graphql.d.ts +0 -24
- package/dist/__generated__/TestModeBannerQuery.graphql.d.ts +0 -20
- package/dist/__generated__/TestModeBannerQuery.graphql.js +0 -87
- package/dist/__generated__/ThemeConfigQuery.graphql.d.ts +0 -20
- package/dist/__generated__/ThemeConfigQuery.graphql.js +0 -87
- package/dist/components/CartButton/index.server.d.ts +0 -3
- package/dist/components/CartButton/index.server.js +0 -18
- package/dist/components/CorsErrorModal.d.ts +0 -1
- package/dist/components/CorsErrorModal.js +0 -92
- package/dist/components/FontdueProvider/FontdueProviderClientComponent.d.ts +0 -14
- package/dist/components/FontdueProvider/FontdueProviderClientComponent.js +0 -61
- package/dist/components/ThemeConfig/index.server.d.ts +0 -2
- package/dist/components/ThemeConfig/index.server.js +0 -17
- package/dist/components/TypeTester/features.d.ts +0 -5
- package/dist/components/TypeTester/features.js +0 -143
- package/dist/data/unicodeData.d.ts +0 -4
- package/dist/data/unicodeData.js +0 -1
package/dist/fontdue.css
CHANGED
|
@@ -89,6 +89,33 @@
|
|
|
89
89
|
font-size: var(--cart_icon_size, 26px);
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
+
.customer-sign-in__errors {
|
|
93
|
+
color: var(--error_color);
|
|
94
|
+
margin-top: 30px;
|
|
95
|
+
margin-bottom: 30px;
|
|
96
|
+
}
|
|
97
|
+
.customer-sign-in__intro {
|
|
98
|
+
margin-bottom: 30px;
|
|
99
|
+
}
|
|
100
|
+
.customer-sign-in__section {
|
|
101
|
+
margin-bottom: 30px;
|
|
102
|
+
}
|
|
103
|
+
.customer-sign-in__actions {
|
|
104
|
+
display: flex;
|
|
105
|
+
align-items: center;
|
|
106
|
+
gap: 1em;
|
|
107
|
+
flex-wrap: wrap;
|
|
108
|
+
}
|
|
109
|
+
.customer-sign-in__back {
|
|
110
|
+
background: none;
|
|
111
|
+
border: 0;
|
|
112
|
+
padding: 0;
|
|
113
|
+
color: inherit;
|
|
114
|
+
font: inherit;
|
|
115
|
+
cursor: pointer;
|
|
116
|
+
text-decoration: underline;
|
|
117
|
+
}
|
|
118
|
+
|
|
92
119
|
.icon {
|
|
93
120
|
display: inline-flex;
|
|
94
121
|
justify-content: center;
|
|
@@ -860,6 +887,9 @@ fontdue-type-tester {
|
|
|
860
887
|
position: relative;
|
|
861
888
|
z-index: 0;
|
|
862
889
|
}
|
|
890
|
+
.type-tester__text__container[data-truncated] {
|
|
891
|
+
overflow: hidden;
|
|
892
|
+
}
|
|
863
893
|
.type-tester__text__container [data-contents=true] {
|
|
864
894
|
column-count: var(--type-tester--column-count, auto);
|
|
865
895
|
column-width: var(--type-tester--column-width, auto);
|
|
@@ -2008,6 +2038,39 @@ input:checked + .checkbox__icon {
|
|
|
2008
2038
|
font-size: 20px;
|
|
2009
2039
|
}
|
|
2010
2040
|
|
|
2041
|
+
.store-modal__index__load-more {
|
|
2042
|
+
grid-column: 1/-1;
|
|
2043
|
+
padding-bottom: 20px;
|
|
2044
|
+
text-align: center;
|
|
2045
|
+
}
|
|
2046
|
+
|
|
2047
|
+
.store-modal__index__load-more__button {
|
|
2048
|
+
background: none;
|
|
2049
|
+
color: inherit;
|
|
2050
|
+
font: inherit;
|
|
2051
|
+
text-decoration: inherit;
|
|
2052
|
+
text-align: inherit;
|
|
2053
|
+
border: 0;
|
|
2054
|
+
border-radius: 0;
|
|
2055
|
+
padding: 0;
|
|
2056
|
+
margin: 0;
|
|
2057
|
+
appearance: none;
|
|
2058
|
+
text-transform: inherit;
|
|
2059
|
+
}
|
|
2060
|
+
.store-modal__index__load-more__button:active, .store-modal__index__load-more__button:focus {
|
|
2061
|
+
outline: none;
|
|
2062
|
+
}
|
|
2063
|
+
.store-modal__index__load-more__button:not(:disabled) {
|
|
2064
|
+
cursor: pointer;
|
|
2065
|
+
}
|
|
2066
|
+
.store-modal__index__load-more__button {
|
|
2067
|
+
color: var(--secondary_text_color);
|
|
2068
|
+
text-decoration: underline;
|
|
2069
|
+
}
|
|
2070
|
+
.store-modal__index__load-more__button:disabled {
|
|
2071
|
+
text-decoration: none;
|
|
2072
|
+
}
|
|
2073
|
+
|
|
2011
2074
|
.store-modal__page__body:has(> .store-modal__index-item__button) {
|
|
2012
2075
|
display: grid;
|
|
2013
2076
|
grid-template-columns: auto 1fr auto;
|
|
@@ -8,6 +8,7 @@ export interface UseAutofitProps {
|
|
|
8
8
|
fontWeight?: React.CSSProperties['fontWeight'];
|
|
9
9
|
fontStyle?: string;
|
|
10
10
|
fontVariationSettings?: string;
|
|
11
|
+
fontFeatureSettings?: string;
|
|
11
12
|
enabled?: boolean;
|
|
12
13
|
padding?: number;
|
|
13
14
|
}
|
|
@@ -16,5 +17,5 @@ export interface UseAutofitResult {
|
|
|
16
17
|
fontSize: number;
|
|
17
18
|
ready: boolean;
|
|
18
19
|
}
|
|
19
|
-
declare const useAutofit: ({ text, fontFamily, fontSize: externalFontSize, min, max, letterSpacing, fontWeight, fontStyle, fontVariationSettings, enabled, padding, }: UseAutofitProps) => UseAutofitResult;
|
|
20
|
+
declare const useAutofit: ({ text, fontFamily, fontSize: externalFontSize, min, max, letterSpacing, fontWeight, fontStyle, fontVariationSettings, fontFeatureSettings, enabled, padding, }: UseAutofitProps) => UseAutofitResult;
|
|
20
21
|
export default useAutofit;
|
package/dist/hooks/useAutofit.js
CHANGED
|
@@ -9,7 +9,8 @@ function getCanvasContext() {
|
|
|
9
9
|
return sharedCanvas.getContext('2d');
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
-
// DOM measurement (fallback for variable fonts
|
|
12
|
+
// DOM measurement (fallback for variable fonts and OpenType features, which
|
|
13
|
+
// canvas can't express: `ctx.font` takes neither variation nor feature settings)
|
|
13
14
|
let measureEl = null;
|
|
14
15
|
function getMeasureElement() {
|
|
15
16
|
if (!measureEl) {
|
|
@@ -31,7 +32,7 @@ function measureWithCanvas(text, fontFamily, fontWeight, fontStyleValue, letterS
|
|
|
31
32
|
ctx.letterSpacing = '0px';
|
|
32
33
|
return width;
|
|
33
34
|
}
|
|
34
|
-
function measureWithDOM(text, fontFamily, fontWeight, fontStyleValue, letterSpacing, fontVariationSettings) {
|
|
35
|
+
function measureWithDOM(text, fontFamily, fontWeight, fontStyleValue, letterSpacing, fontVariationSettings, fontFeatureSettings) {
|
|
35
36
|
const el = getMeasureElement();
|
|
36
37
|
el.style.fontFamily = fontFamily;
|
|
37
38
|
el.style.fontWeight = `${fontWeight}`;
|
|
@@ -39,6 +40,7 @@ function measureWithDOM(text, fontFamily, fontWeight, fontStyleValue, letterSpac
|
|
|
39
40
|
el.style.fontSize = `${REF_SIZE}px`;
|
|
40
41
|
el.style.letterSpacing = `${letterSpacing}em`;
|
|
41
42
|
el.style.fontVariationSettings = fontVariationSettings;
|
|
43
|
+
el.style.fontFeatureSettings = fontFeatureSettings;
|
|
42
44
|
el.textContent = text;
|
|
43
45
|
return el.getBoundingClientRect().width;
|
|
44
46
|
}
|
|
@@ -53,6 +55,7 @@ const useAutofit = _ref => {
|
|
|
53
55
|
fontWeight = 400,
|
|
54
56
|
fontStyle = 'normal',
|
|
55
57
|
fontVariationSettings,
|
|
58
|
+
fontFeatureSettings,
|
|
56
59
|
enabled = true,
|
|
57
60
|
padding = 0
|
|
58
61
|
} = _ref;
|
|
@@ -83,7 +86,10 @@ const useAutofit = _ref => {
|
|
|
83
86
|
// Split by newlines and measure the widest line, since the rendered
|
|
84
87
|
// text preserves line breaks (Draft.js uses white-space: pre-wrap).
|
|
85
88
|
const lines = text.split('\n');
|
|
86
|
-
|
|
89
|
+
// Enabled features can change glyph widths (stylistic sets, small caps,
|
|
90
|
+
// ligatures), so the text has to be measured with them applied. The
|
|
91
|
+
// measure element is shared, so both settings are written on every call.
|
|
92
|
+
const measure = fontVariationSettings || fontFeatureSettings ? line => measureWithDOM(line, fontFamily, fontWeight, fontStyle, letterSpacing, fontVariationSettings || 'normal', fontFeatureSettings || 'normal') : line => measureWithCanvas(line, fontFamily, fontWeight, fontStyle, letterSpacing);
|
|
87
93
|
const refWidth = Math.max(...lines.map(measure));
|
|
88
94
|
if (refWidth <= 0) return;
|
|
89
95
|
|
|
@@ -92,7 +98,7 @@ const useAutofit = _ref => {
|
|
|
92
98
|
const newSize = Math.min(Math.max(min, Math.floor(availableWidth / (refWidth / REF_SIZE))), max);
|
|
93
99
|
setFontSize(newSize);
|
|
94
100
|
if (!measured) setMeasured(true);
|
|
95
|
-
}, [text, fontFamily, fontWeight, fontStyle, fontVariationSettings, letterSpacing, containerWidth, enabled, min, max, padding, fontVersion]);
|
|
101
|
+
}, [text, fontFamily, fontWeight, fontStyle, fontVariationSettings, fontFeatureSettings, letterSpacing, containerWidth, enabled, min, max, padding, fontVersion]);
|
|
96
102
|
if (!enabled) return {
|
|
97
103
|
ref,
|
|
98
104
|
fontSize: externalFontSize,
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { useCurrentCustomerQuery$data } from '../__generated__/useCurrentCustomerQuery.graphql.js';
|
|
2
|
+
export type CurrentCustomer = NonNullable<NonNullable<useCurrentCustomerQuery$data['viewer']>['currentCustomer']>;
|
|
3
|
+
export interface UseCurrentCustomer {
|
|
4
|
+
/** The signed-in customer, or `null` when signed out or still loading. */
|
|
5
|
+
customer: CurrentCustomer | null;
|
|
6
|
+
/** True while the customer is being fetched. Always false when signed out. */
|
|
7
|
+
loading: boolean;
|
|
8
|
+
/** Forgets the session in this browser. Every Fontdue component sees it. */
|
|
9
|
+
signOut: () => void;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Who is signed in, if anyone.
|
|
13
|
+
*
|
|
14
|
+
* ```tsx
|
|
15
|
+
* const { customer, loading, signOut } = useCurrentCustomer();
|
|
16
|
+
*
|
|
17
|
+
* if (loading) return null;
|
|
18
|
+
* if (!customer) return <CustomerSignIn />;
|
|
19
|
+
* return <button onClick={signOut}>Sign out, {customer.name}</button>;
|
|
20
|
+
* ```
|
|
21
|
+
*
|
|
22
|
+
* Deliberately does not suspend, so it can be called from a component that
|
|
23
|
+
* renders on the server: it reports "signed out, not loading" during SSR and on
|
|
24
|
+
* the first client render, then fetches. That also means it needs no Suspense
|
|
25
|
+
* boundary, unlike the components that use `useLazyLoadQuery`.
|
|
26
|
+
*
|
|
27
|
+
* The signed-in state comes from the token store, which is one module-level
|
|
28
|
+
* singleton for the whole page — so a sign-in in one Astro island updates a
|
|
29
|
+
* `useCurrentCustomer()` in another, and `signOut()` signs out everywhere.
|
|
30
|
+
*
|
|
31
|
+
* Requires a `<FontdueProvider>` (or `<FontdueContextProvider>`) ancestor for
|
|
32
|
+
* the Relay environment.
|
|
33
|
+
*/
|
|
34
|
+
export default function useCurrentCustomer(): UseCurrentCustomer;
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import _useCurrentCustomerQuery from "../__generated__/useCurrentCustomerQuery.graphql.js";
|
|
4
|
+
import { useCallback, useEffect, useState } from 'react';
|
|
5
|
+
import { fetchQuery, graphql, useRelayEnvironment } from 'react-relay';
|
|
6
|
+
import { clearTokens, getTokens, subscribeToTokens } from '../auth/tokenStore.js';
|
|
7
|
+
import { useFontdueUrl } from '../components/UrlContext.js';
|
|
8
|
+
const query = (_useCurrentCustomerQuery.hash && _useCurrentCustomerQuery.hash !== "bbd9b896d31a326af6b8525fdd4464c0" && console.error("The definition of 'useCurrentCustomerQuery' appears to have changed. Run `relay-compiler` to update the generated files to receive the expected data."), _useCurrentCustomerQuery);
|
|
9
|
+
/**
|
|
10
|
+
* Who is signed in, if anyone.
|
|
11
|
+
*
|
|
12
|
+
* ```tsx
|
|
13
|
+
* const { customer, loading, signOut } = useCurrentCustomer();
|
|
14
|
+
*
|
|
15
|
+
* if (loading) return null;
|
|
16
|
+
* if (!customer) return <CustomerSignIn />;
|
|
17
|
+
* return <button onClick={signOut}>Sign out, {customer.name}</button>;
|
|
18
|
+
* ```
|
|
19
|
+
*
|
|
20
|
+
* Deliberately does not suspend, so it can be called from a component that
|
|
21
|
+
* renders on the server: it reports "signed out, not loading" during SSR and on
|
|
22
|
+
* the first client render, then fetches. That also means it needs no Suspense
|
|
23
|
+
* boundary, unlike the components that use `useLazyLoadQuery`.
|
|
24
|
+
*
|
|
25
|
+
* The signed-in state comes from the token store, which is one module-level
|
|
26
|
+
* singleton for the whole page — so a sign-in in one Astro island updates a
|
|
27
|
+
* `useCurrentCustomer()` in another, and `signOut()` signs out everywhere.
|
|
28
|
+
*
|
|
29
|
+
* Requires a `<FontdueProvider>` (or `<FontdueContextProvider>`) ancestor for
|
|
30
|
+
* the Relay environment.
|
|
31
|
+
*/
|
|
32
|
+
export default function useCurrentCustomer() {
|
|
33
|
+
const environment = useRelayEnvironment();
|
|
34
|
+
const url = useFontdueUrl();
|
|
35
|
+
const [signedIn, setSignedIn] = useState(false);
|
|
36
|
+
const [customer, setCustomer] = useState(null);
|
|
37
|
+
const [loading, setLoading] = useState(false);
|
|
38
|
+
|
|
39
|
+
// Read the store in an effect rather than during render: reading it during
|
|
40
|
+
// render would have the first client render disagree with the server HTML for
|
|
41
|
+
// a signed-in visitor, which React reports as a hydration mismatch.
|
|
42
|
+
useEffect(() => {
|
|
43
|
+
const read = () => setSignedIn(getTokens(url) != null);
|
|
44
|
+
read();
|
|
45
|
+
return subscribeToTokens(read);
|
|
46
|
+
}, [url]);
|
|
47
|
+
useEffect(() => {
|
|
48
|
+
if (!signedIn) {
|
|
49
|
+
setCustomer(null);
|
|
50
|
+
setLoading(false);
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
let cancelled = false;
|
|
54
|
+
setLoading(true);
|
|
55
|
+
|
|
56
|
+
// network-only: the point of this query is who is signed in *now*, and a
|
|
57
|
+
// store hit would answer with the previous customer after a sign-out and
|
|
58
|
+
// back in.
|
|
59
|
+
const subscription = fetchQuery(environment, query, {}, {
|
|
60
|
+
fetchPolicy: 'network-only'
|
|
61
|
+
}).subscribe({
|
|
62
|
+
next: data => {
|
|
63
|
+
var _data$viewer;
|
|
64
|
+
if (!cancelled) setCustomer(((_data$viewer = data.viewer) === null || _data$viewer === void 0 ? void 0 : _data$viewer.currentCustomer) ?? null);
|
|
65
|
+
},
|
|
66
|
+
complete: () => {
|
|
67
|
+
if (!cancelled) setLoading(false);
|
|
68
|
+
},
|
|
69
|
+
error: () => {
|
|
70
|
+
// The network layer has already dealt with an expired or invalid
|
|
71
|
+
// token — renewed it, or cleared the session, which re-runs this
|
|
72
|
+
// effect. Anything left is a failed request, and reporting "signed
|
|
73
|
+
// out" for it is better than a spinner that never stops.
|
|
74
|
+
if (!cancelled) {
|
|
75
|
+
setCustomer(null);
|
|
76
|
+
setLoading(false);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
return () => {
|
|
81
|
+
cancelled = true;
|
|
82
|
+
subscription.unsubscribe();
|
|
83
|
+
};
|
|
84
|
+
}, [signedIn, environment]);
|
|
85
|
+
const signOut = useCallback(() => {
|
|
86
|
+
clearTokens(url);
|
|
87
|
+
}, [url]);
|
|
88
|
+
return {
|
|
89
|
+
customer,
|
|
90
|
+
loading,
|
|
91
|
+
signOut
|
|
92
|
+
};
|
|
93
|
+
}
|
package/dist/reducer.d.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { SkuStyleIds } from './components/Root/productState.js';
|
|
2
2
|
import { Reducer } from 'redux';
|
|
3
3
|
import { productState_Query$data } from './__generated__/productState_Query.graphql.js';
|
|
4
4
|
import type { RouteConfig } from './components/StoreModal/routes.js';
|
|
5
|
-
import {
|
|
5
|
+
import { isSelected } from './selection.js';
|
|
6
6
|
import { Config } from './components/ConfigContext.js';
|
|
7
|
-
export {
|
|
7
|
+
export { isSelected };
|
|
8
8
|
interface LicenseOptionSpec {
|
|
9
9
|
licenseId: string;
|
|
10
10
|
licenseOptions: {
|
|
@@ -22,8 +22,9 @@ export interface FontdueState {
|
|
|
22
22
|
selectedSkuIds: {
|
|
23
23
|
[skuId: string]: boolean;
|
|
24
24
|
};
|
|
25
|
+
requestedSkuIds: string[];
|
|
25
26
|
licenseOptions: LicenseOptionSpec[];
|
|
26
|
-
|
|
27
|
+
skuStyleIds: SkuStyleIds;
|
|
27
28
|
collectionSkus: {
|
|
28
29
|
[collectionSkuId: string]: string | null;
|
|
29
30
|
};
|
|
@@ -94,7 +95,6 @@ export type FontdueAction = {
|
|
|
94
95
|
type: 'UPDATE_LICENSEE_IS_BILLING_IDENTITY';
|
|
95
96
|
value: boolean;
|
|
96
97
|
};
|
|
97
|
-
export declare const collectionSkuIdWithDiscount: (state: FontdueState, skuId: string) => [string, number] | [null, null];
|
|
98
98
|
declare const reducer: Reducer<FontdueState, FontdueAction>;
|
|
99
99
|
export declare function createDefaultStore(config: Config | undefined): import("redux").Store<FontdueState, FontdueAction, unknown>;
|
|
100
100
|
export default reducer;
|
package/dist/reducer.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { populateProductStateReducer } from './components/Root/productState.js';
|
|
2
2
|
import { createStore } from 'redux';
|
|
3
3
|
import { shallowEqual } from 'react-redux';
|
|
4
|
-
import {
|
|
4
|
+
import { isSelected, repriceSelection, selectSku } from './selection.js';
|
|
5
5
|
import { makeConfig } from './components/ConfigContext.js';
|
|
6
|
-
export {
|
|
6
|
+
export { isSelected };
|
|
7
7
|
const unselectLicense = (state, action) => {
|
|
8
8
|
if (!(action.type === 'PRECART_UNSELECT_LICENSE' || action.type === 'PRECART_TOGGLE_LICENSE')) return state;
|
|
9
9
|
return {
|
|
@@ -61,96 +61,17 @@ const selectLicenseOption = (state, action) => {
|
|
|
61
61
|
})
|
|
62
62
|
};
|
|
63
63
|
};
|
|
64
|
-
export const collectionSkuIdWithDiscount = (state, skuId) => {
|
|
65
|
-
const differences = collectionSkuIdsDifferences(state, skuId);
|
|
66
|
-
const collectionId = Object.keys(differences).filter(collSkuId => differences[collSkuId] <= state.skuPrices[skuId]).reduce((smallest, key) => {
|
|
67
|
-
if (!smallest || differences[key] < differences[smallest]) return key;
|
|
68
|
-
if (differences[key] === differences[smallest]) {
|
|
69
|
-
// When differences are equal, prefer a collection that is not the
|
|
70
|
-
// SKU being selected — this ensures collection bundles are preferred
|
|
71
|
-
// over selecting the family itself at the same price
|
|
72
|
-
if (smallest === skuId && key !== skuId) return key;
|
|
73
|
-
// Among non-self candidates, prefer the one covering more unique
|
|
74
|
-
// font styles — this ensures the most comprehensive collection wins
|
|
75
|
-
if (smallest !== skuId && key !== skuId) {
|
|
76
|
-
var _state$collectionStyl, _state$collectionStyl2;
|
|
77
|
-
const smallestStyles = new Set((_state$collectionStyl = state.collectionStyleSkus[smallest]) === null || _state$collectionStyl === void 0 ? void 0 : _state$collectionStyl.fontStyleSkuIds).size;
|
|
78
|
-
const keyStyles = new Set((_state$collectionStyl2 = state.collectionStyleSkus[key]) === null || _state$collectionStyl2 === void 0 ? void 0 : _state$collectionStyl2.fontStyleSkuIds).size;
|
|
79
|
-
if (keyStyles > smallestStyles) return key;
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
return smallest;
|
|
83
|
-
}, null);
|
|
84
|
-
if (collectionId) return [collectionId, differences[collectionId]];
|
|
85
|
-
return [null, null];
|
|
86
|
-
};
|
|
87
|
-
const selectedCollectionSkuIdContainingSkuId = (state, skuId) => Object.keys(state.selectedSkuIds).filter(id => state.selectedSkuIds[id] === true).find(selectedSkuId => collTransitivelyContainsSkuId(state.collectionStyleSkus, selectedSkuId, skuId));
|
|
88
|
-
const setKeysFalse = (acc, key) => {
|
|
89
|
-
acc[key] = false;
|
|
90
|
-
return acc;
|
|
91
|
-
};
|
|
92
|
-
const unselectedMembersSkuIds = (state, action) => {
|
|
93
|
-
if (action.type !== 'SELECT_SKU_ID') throw new Error('Wrong action ${action.type}');
|
|
94
|
-
const {
|
|
95
|
-
skuId,
|
|
96
|
-
selected
|
|
97
|
-
} = action;
|
|
98
|
-
if (!selected) return {}; // only do this in case we are selecting something
|
|
99
|
-
|
|
100
|
-
// Recursively collect all descendant SKU IDs so that transitive
|
|
101
|
-
// selections (e.g., collection bundle → family → bundle) are all
|
|
102
|
-
// marked as false
|
|
103
|
-
const result = {};
|
|
104
|
-
const collectMembers = collSkuId => {
|
|
105
|
-
const members = state.collectionStyleSkus[collSkuId];
|
|
106
|
-
if (!members) return;
|
|
107
|
-
for (const id of members.fontStyleSkuIds) {
|
|
108
|
-
result[id] = false;
|
|
109
|
-
}
|
|
110
|
-
for (const id of members.childrenSkuIds) {
|
|
111
|
-
result[id] = false;
|
|
112
|
-
collectMembers(id);
|
|
113
|
-
}
|
|
114
|
-
};
|
|
115
|
-
collectMembers(skuId);
|
|
116
|
-
return result;
|
|
117
|
-
};
|
|
118
64
|
const selectSkuId = (state, action) => {
|
|
119
65
|
if (action.type !== 'SELECT_SKU_ID') return state;
|
|
120
|
-
const {
|
|
121
|
-
skuId,
|
|
122
|
-
selected
|
|
123
|
-
} = action;
|
|
124
|
-
let actualSkuId = skuId;
|
|
125
|
-
if (selected) {
|
|
126
|
-
// check if selecting any collections instead of this sku
|
|
127
|
-
// would reduce the selection price
|
|
128
|
-
const [collectionSkuId, _amount] = collectionSkuIdWithDiscount(state, skuId);
|
|
129
|
-
actualSkuId = collectionSkuId || actualSkuId;
|
|
130
|
-
} else if (state.selectedSkuIds[skuId] === false) {
|
|
131
|
-
// we're clicking "deselect" on an item that's not actually selected,
|
|
132
|
-
// it's a member of a collection which is selected. in this case
|
|
133
|
-
// we'll find the collection and deselect that collection instead
|
|
134
|
-
const parentId = selectedCollectionSkuIdContainingSkuId(state, skuId);
|
|
135
|
-
if (parentId) actualSkuId = parentId;
|
|
136
|
-
}
|
|
137
66
|
return {
|
|
138
|
-
...state,
|
|
139
|
-
precartOpen: true
|
|
140
|
-
selectedSkuIds: {
|
|
141
|
-
...state.selectedSkuIds,
|
|
142
|
-
[actualSkuId]: selected,
|
|
143
|
-
...unselectedMembersSkuIds(state, {
|
|
144
|
-
...action,
|
|
145
|
-
skuId: actualSkuId,
|
|
146
|
-
selected
|
|
147
|
-
})
|
|
148
|
-
}
|
|
67
|
+
...selectSku(state, action.skuId, action.selected),
|
|
68
|
+
precartOpen: true
|
|
149
69
|
};
|
|
150
70
|
};
|
|
151
71
|
const unselectSkus = state => ({
|
|
152
72
|
...state,
|
|
153
|
-
selectedSkuIds: {}
|
|
73
|
+
selectedSkuIds: {},
|
|
74
|
+
requestedSkuIds: []
|
|
154
75
|
});
|
|
155
76
|
const storeModalNavigate = (state, action) => {
|
|
156
77
|
if (action.type !== 'STORE_MODAL_NAVIGATE') return state;
|
|
@@ -197,7 +118,7 @@ const reducer = (state, action) => {
|
|
|
197
118
|
if (!state) throw 'Reducer called without state';
|
|
198
119
|
switch (action.type) {
|
|
199
120
|
case 'POPULATE_PRODUCT_STATE':
|
|
200
|
-
return populateProductStateReducer(state, action);
|
|
121
|
+
return repriceSelection(populateProductStateReducer(state, action));
|
|
201
122
|
case 'PRECART_TOGGLE_LICENSE':
|
|
202
123
|
return toggleLicense(state, action);
|
|
203
124
|
case 'PRECART_UNSELECT_LICENSE':
|
|
@@ -260,10 +181,11 @@ export function createDefaultStore(config) {
|
|
|
260
181
|
return createStore(reducer, {
|
|
261
182
|
selectedSkuIds: {},
|
|
262
183
|
// { [skuId]: Boolean }
|
|
184
|
+
requestedSkuIds: [],
|
|
263
185
|
licenseOptions: [],
|
|
264
186
|
cartOpen: false,
|
|
265
187
|
precartOpen: false,
|
|
266
|
-
|
|
188
|
+
skuStyleIds: {},
|
|
267
189
|
skuPrices: {},
|
|
268
190
|
collectionSkus: {},
|
|
269
191
|
fetchedCollectionIds: [],
|
|
@@ -4,6 +4,8 @@ import { resolveFontdueServerConfig } from './serverConfig.js';
|
|
|
4
4
|
import { PREVIEW_HEADER, hasPreviewMarkerCookie } from '../preview/constants.js';
|
|
5
5
|
import { NODE_ACCESS_HEADER } from '../nodeAccess.js';
|
|
6
6
|
import { CLIENT_VERSION_HEADER, version } from '../version.js';
|
|
7
|
+
import { applyCustomerAuthHeader, customerAuthErrorCode, refreshCustomerSession } from '../auth/session.js';
|
|
8
|
+
import { clearTokens } from '../auth/tokenStore.js';
|
|
7
9
|
|
|
8
10
|
// Re-exported so UI (the admin toolbar) can surface the package version
|
|
9
11
|
// without re-reading the build-time global in a 'use client' module.
|
|
@@ -174,65 +176,104 @@ export function createNetworkFetch(options) {
|
|
|
174
176
|
tags: ['graphql', ...((serverConfig === null || serverConfig === void 0 ? void 0 : serverConfig.cacheTags) ?? []), `operation:${request.name}`]
|
|
175
177
|
};
|
|
176
178
|
}
|
|
177
|
-
for (let attempt = 0; attempt <= 2; attempt++) {
|
|
178
|
-
try {
|
|
179
|
-
const resp = await fetch(url + `?queryName=${request.name}`, init);
|
|
180
179
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
180
|
+
// The request itself, retries and all. Named so the customer-session
|
|
181
|
+
// logic below can re-run it after renewing an expired access token.
|
|
182
|
+
async function send() {
|
|
183
|
+
for (let attempt = 0; attempt <= 2; attempt++) {
|
|
184
|
+
try {
|
|
185
|
+
const resp = await fetch(url + `?queryName=${request.name}`, init);
|
|
186
|
+
|
|
187
|
+
// A non-2xx response is a failed request, never data. Returning its
|
|
188
|
+
// body used to let a 5xx that happened to parse as JSON (Cloudflare
|
|
189
|
+
// answers an `Accept: application/json` client with one) reach Relay
|
|
190
|
+
// as a payload — and, on the server, get serialized into a prerendered
|
|
191
|
+
// page that then crashed every visitor on hydration.
|
|
192
|
+
if (!resp.ok) {
|
|
193
|
+
if (attempt < 2 && isRetryableStatus(resp.status)) {
|
|
194
|
+
await new Promise(resolve => setTimeout(resolve, RETRY_DELAY_MS));
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
const detail = await errorMessageOf(resp);
|
|
198
|
+
throw new FontdueResponseError(`fontdue-js: ${request.name} failed with HTTP ${resp.status}` + (detail ? `: ${detail}` : ''), resp.status, request.name);
|
|
199
|
+
}
|
|
200
|
+
const json = await resp.json();
|
|
201
|
+
if (!isGraphQLResponse(json)) {
|
|
202
|
+
throw new FontdueResponseError(`fontdue-js: ${request.name} returned HTTP ${resp.status} with a body that is not a GraphQL response`, resp.status, request.name);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// GraphQL returns exceptions (for example, a missing required variable) in the "errors"
|
|
206
|
+
// property of the response. If any exceptions occurred when processing the request,
|
|
207
|
+
// throw an error to indicate to the developer what went wrong.
|
|
208
|
+
const {
|
|
209
|
+
errors
|
|
210
|
+
} = json;
|
|
211
|
+
if (Array.isArray(errors) && errors[0]) {
|
|
212
|
+
var _error$extensions;
|
|
213
|
+
const error = errors[0];
|
|
214
|
+
console.error('GraphQL Error:', {
|
|
215
|
+
message: error.message,
|
|
216
|
+
code: (_error$extensions = error.extensions) === null || _error$extensions === void 0 ? void 0 : _error$extensions.code,
|
|
217
|
+
path: error.path
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
return json;
|
|
221
|
+
} catch (error) {
|
|
222
|
+
if (error instanceof FontdueResponseError) throw error;
|
|
223
|
+
// Retry on network errors (TypeError) before falling through to CORS detection
|
|
224
|
+
if (attempt < 2 && error instanceof TypeError) {
|
|
188
225
|
await new Promise(resolve => setTimeout(resolve, RETRY_DELAY_MS));
|
|
189
226
|
continue;
|
|
190
227
|
}
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
228
|
+
if (handlePossibleCorsError(error, url)) {
|
|
229
|
+
return {
|
|
230
|
+
data: undefined,
|
|
231
|
+
errors: [{
|
|
232
|
+
message: 'Cross-origin request blocked'
|
|
233
|
+
}]
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
throw error;
|
|
197
237
|
}
|
|
238
|
+
}
|
|
198
239
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
240
|
+
// Unreachable — final iteration either returns or throws
|
|
241
|
+
throw new Error('Unexpected: exhausted retries');
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// The signed-in customer's access token, in the browser only. Set *after*
|
|
245
|
+
// `previewing` is decided, so a customer session can never be mistaken for
|
|
246
|
+
// an admin preview, and only when nothing else has claimed the header —
|
|
247
|
+
// a forwarded preview token is the page author's credential and wins.
|
|
248
|
+
//
|
|
249
|
+
// `headers` is the same object `init.headers` holds, so renewing the token
|
|
250
|
+
// below and re-running the fetch sends the new one.
|
|
251
|
+
const authScope = IS_SERVER ? undefined : base;
|
|
252
|
+
const customerSession = !IS_SERVER && headers.Authorization == null && headers.authorization == null && applyCustomerAuthHeader(headers, authScope);
|
|
253
|
+
const json = await send();
|
|
254
|
+
if (!customerSession) return json;
|
|
255
|
+
switch (customerAuthErrorCode(json)) {
|
|
256
|
+
// The everyday case: 24 hours have passed since the last visit. Renew
|
|
257
|
+
// once and re-run the request, so the visitor never sees a signed-out
|
|
258
|
+
// page they have to sign back into.
|
|
259
|
+
case 'token:expired':
|
|
260
|
+
{
|
|
261
|
+
const renewed = await refreshCustomerSession(url, authScope, headers);
|
|
262
|
+
if (renewed == null) return json;
|
|
263
|
+
applyCustomerAuthHeader(headers, authScope);
|
|
264
|
+
return await send();
|
|
213
265
|
}
|
|
266
|
+
|
|
267
|
+
// Not renewable, and not a request the customer can retry into: forged,
|
|
268
|
+
// tampered with, or naming a customer who no longer exists. Forget it so
|
|
269
|
+
// the UI shows a signed-out state rather than retrying every request.
|
|
270
|
+
case 'token:invalid':
|
|
271
|
+
case 'customer:required':
|
|
272
|
+
clearTokens(authScope);
|
|
273
|
+
return json;
|
|
274
|
+
default:
|
|
214
275
|
return json;
|
|
215
|
-
} catch (error) {
|
|
216
|
-
if (error instanceof FontdueResponseError) throw error;
|
|
217
|
-
// Retry on network errors (TypeError) before falling through to CORS detection
|
|
218
|
-
if (attempt < 2 && error instanceof TypeError) {
|
|
219
|
-
await new Promise(resolve => setTimeout(resolve, RETRY_DELAY_MS));
|
|
220
|
-
continue;
|
|
221
|
-
}
|
|
222
|
-
if (handlePossibleCorsError(error, url)) {
|
|
223
|
-
return {
|
|
224
|
-
data: undefined,
|
|
225
|
-
errors: [{
|
|
226
|
-
message: 'Cross-origin request blocked'
|
|
227
|
-
}]
|
|
228
|
-
};
|
|
229
|
-
}
|
|
230
|
-
throw error;
|
|
231
|
-
}
|
|
232
276
|
}
|
|
233
|
-
|
|
234
|
-
// Unreachable — final iteration either returns or throws
|
|
235
|
-
throw new Error('Unexpected: exhausted retries');
|
|
236
277
|
};
|
|
237
278
|
}
|
|
238
279
|
export const networkFetch = createNetworkFetch();
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { FontdueState } from './reducer.js';
|
|
2
|
+
type Catalog = Pick<FontdueState, 'skuStyleIds' | 'skuPrices'>;
|
|
3
|
+
/**
|
|
4
|
+
* The cheapest set of SKUs in which each of `picks` sits inside one SKU
|
|
5
|
+
* granting all of its styles, sorted. Picks whose styles or price aren't known
|
|
6
|
+
* are left out.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* cheapestSkuIds(['sku:bold', 'sku:regular', 'sku:light'], catalog)
|
|
10
|
+
* // => ['sku:light+regular+bold'] rather than a pair plus a single
|
|
11
|
+
*/
|
|
12
|
+
export declare const cheapestSkuIds: (picks: readonly string[], catalog: Catalog) => string[];
|
|
13
|
+
/**
|
|
14
|
+
* Re-derives `selectedSkuIds` from the picks. A pick whose styles or price
|
|
15
|
+
* aren't known yet – selected from the host page before its product data
|
|
16
|
+
* arrived – is bought as it is until they are.
|
|
17
|
+
*/
|
|
18
|
+
export declare const repriceSelection: (state: FontdueState) => FontdueState;
|
|
19
|
+
/**
|
|
20
|
+
* Adds a pick, or takes out every pick sharing a style with the clicked SKU.
|
|
21
|
+
*
|
|
22
|
+
* If what's left is still cheapest bought with a SKU granting the clicked one's
|
|
23
|
+
* styles anyway – it's part of a bundle that costs no more without it – the
|
|
24
|
+
* click would change nothing, so the picks inside the selected SKUs granting
|
|
25
|
+
* them go too, as deselecting a member of a selected bundle always has.
|
|
26
|
+
*/
|
|
27
|
+
export declare const selectSku: (state: FontdueState, skuId: string, selected: boolean) => FontdueState;
|
|
28
|
+
/**
|
|
29
|
+
* Whether a SKU – or a style with no SKU of its own – is in the selection:
|
|
30
|
+
* selected itself, or every style it grants is granted by what is selected.
|
|
31
|
+
*/
|
|
32
|
+
export declare const isSelected: (skuIdOrStyleId: string | null) => (state: FontdueState) => boolean;
|
|
33
|
+
/**
|
|
34
|
+
* What picking a SKU would add to the selection's price: the cheapest
|
|
35
|
+
* combination with it, less the current one. Never more than the SKU's own
|
|
36
|
+
* price, and less when it completes a cheaper bundle – the Store Modal's
|
|
37
|
+
* “+$X”. Null when the SKU's styles or price aren't known.
|
|
38
|
+
*/
|
|
39
|
+
export declare const additionalCost: (state: FontdueState, skuId: string) => number | null;
|
|
40
|
+
export {};
|