fontdue-js 3.6.0 → 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.
Files changed (71) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/dist/__generated__/FontFamiliesQuery.graphql.d.ts +4 -1
  3. package/dist/__generated__/FontFamiliesQuery.graphql.js +46 -39
  4. package/dist/__generated__/PriceBar_node.graphql.d.ts +4 -1
  5. package/dist/__generated__/PriceBar_node.graphql.js +23 -16
  6. package/dist/__generated__/StoreModalFamily_collection.graphql.d.ts +3 -2
  7. package/dist/__generated__/StoreModalFamily_collection.graphql.js +3 -3
  8. package/dist/__generated__/StoreModalIndexPaginationQuery.graphql.d.ts +23 -0
  9. package/dist/__generated__/StoreModalIndexPaginationQuery.graphql.js +303 -0
  10. package/dist/__generated__/StoreModalIndexQuery.graphql.d.ts +4 -9
  11. package/dist/__generated__/StoreModalIndexQuery.graphql.js +90 -53
  12. package/dist/__generated__/StoreModalIndex_viewer.graphql.d.ts +24 -0
  13. package/dist/__generated__/StoreModalIndex_viewer.graphql.js +134 -0
  14. package/dist/__generated__/StoreModalProductContent_collection.graphql.d.ts +4 -1
  15. package/dist/__generated__/StoreModalProductContent_collection.graphql.js +12 -5
  16. package/dist/__generated__/StoreModalProductLicenseSelection_collection.graphql.d.ts +2 -1
  17. package/dist/__generated__/StoreModalProductLicenseSelection_collection.graphql.js +27 -24
  18. package/dist/__generated__/StoreModalProductQuery.graphql.d.ts +1 -1
  19. package/dist/__generated__/StoreModalProductQuery.graphql.js +78 -77
  20. package/dist/__generated__/StoreModalProductRefetchQuery.graphql.d.ts +1 -1
  21. package/dist/__generated__/StoreModalProductRefetchQuery.graphql.js +54 -53
  22. package/dist/__generated__/StoreModalProductSlugQuery.graphql.d.ts +1 -1
  23. package/dist/__generated__/StoreModalProductSlugQuery.graphql.js +78 -77
  24. package/dist/__generated__/TypeTestersIDQuery.graphql.d.ts +1 -1
  25. package/dist/__generated__/TypeTestersIDQuery.graphql.js +17 -10
  26. package/dist/__generated__/TypeTestersRefetchQuery.graphql.d.ts +1 -1
  27. package/dist/__generated__/TypeTestersRefetchQuery.graphql.js +17 -10
  28. package/dist/__generated__/TypeTestersSlugQuery.graphql.d.ts +1 -1
  29. package/dist/__generated__/TypeTestersSlugQuery.graphql.js +17 -10
  30. package/dist/__generated__/storefrontEventsTrackStorefrontEventMutation.graphql.d.ts +41 -0
  31. package/dist/__generated__/storefrontEventsTrackStorefrontEventMutation.graphql.js +65 -0
  32. package/dist/__tests__/bundleSelectionOrder.test.js +92 -0
  33. package/dist/__tests__/collectionBundleSelection.test.js +441 -1446
  34. package/dist/__tests__/infiniteScroll.test.js +180 -0
  35. package/dist/__tests__/licenseExclusions.test.js +2 -1
  36. package/dist/__tests__/storefrontEvents.test.js +167 -0
  37. package/dist/__tests__/typeTesterAutofitFeatures.test.js +138 -0
  38. package/dist/components/Cart/Checkout.js +11 -1
  39. package/dist/components/Cart/storefrontEvents.d.ts +34 -0
  40. package/dist/components/Cart/storefrontEvents.js +138 -0
  41. package/dist/components/FontFamilies/index.js +4 -3
  42. package/dist/components/Precart/index.js +6 -0
  43. package/dist/components/PriceBar/index.js +5 -3
  44. package/dist/components/Root/productState.d.ts +2 -8
  45. package/dist/components/Root/productState.js +46 -57
  46. package/dist/components/SKUPrice/index.js +7 -8
  47. package/dist/components/StoreModal/StoreModalFamily.js +4 -2
  48. package/dist/components/StoreModal/StoreModalIndex.js +48 -7
  49. package/dist/components/StoreModal/StoreModalProduct.js +16 -9
  50. package/dist/components/StoreModalProductLicenseSelection/StoreModalProductLicense.d.ts +2 -1
  51. package/dist/components/StoreModalProductLicenseSelection/StoreModalProductLicense.js +8 -3
  52. package/dist/components/StoreModalProductLicenseSelection/index.js +3 -2
  53. package/dist/components/TypeTester/useTypeTesterStyler.js +1 -0
  54. package/dist/components/elements/StoreModalContainer/StoreModalContainerContext.d.ts +1 -0
  55. package/dist/components/elements/StoreModalContainer/index.js +9 -4
  56. package/dist/components/elements/StoreModalIndexLoadMore/index.d.ts +8 -0
  57. package/dist/components/elements/StoreModalIndexLoadMore/index.js +21 -0
  58. package/dist/components/elements/StoreModalUnifiedCheckout.js +12 -0
  59. package/dist/components/useInfiniteScroll.d.ts +28 -0
  60. package/dist/components/useInfiniteScroll.js +50 -0
  61. package/dist/fontdue.css +33 -0
  62. package/dist/hooks/useAutofit.d.ts +2 -1
  63. package/dist/hooks/useAutofit.js +10 -4
  64. package/dist/reducer.d.ts +5 -5
  65. package/dist/reducer.js +9 -87
  66. package/dist/selection.d.ts +40 -0
  67. package/dist/selection.js +494 -0
  68. package/dist/utils.d.ts +1 -12
  69. package/dist/utils.js +1 -36
  70. package/dist/version.js +1 -1
  71. package/package.json +1 -1
@@ -0,0 +1,138 @@
1
+ import _storefrontEventsTrackStorefrontEventMutation from "../../__generated__/storefrontEventsTrackStorefrontEventMutation.graphql.js";
2
+ import { useCallback } from 'react';
3
+ import { commitMutation, graphql, useRelayEnvironment } from 'react-relay';
4
+ import { useStore } from 'react-redux';
5
+ import { orderTrackingInput } from './orderTracking.js';
6
+ /**
7
+ * The storefront moments that happen entirely in the browser – a collection
8
+ * opened in the store modal, its licences changed, the payment details
9
+ * completed. Nothing reaches the server at those moments on its own, so they
10
+ * are reported through `trackStorefrontEvent`, which records them as "Product
11
+ * Viewed", "Product Customized" and "Payment Info Entered" with the same
12
+ * consent and identifiers as the order events around them (Meta reads them as
13
+ * ViewContent, CustomizeProduct and AddPaymentInfo).
14
+ *
15
+ * Every call is fire-and-forget: tracking must never break the store.
16
+ */
17
+ function track(environment, input) {
18
+ const tracking = orderTrackingInput();
19
+ // Outside a browser there is no moment to report.
20
+ if (!tracking) return;
21
+ try {
22
+ commitMutation(environment, {
23
+ mutation: (_storefrontEventsTrackStorefrontEventMutation.hash && _storefrontEventsTrackStorefrontEventMutation.hash !== "340cc13477a15b926e2307d36c644053" && console.error("The definition of 'storefrontEventsTrackStorefrontEventMutation' appears to have changed. Run `relay-compiler` to update the generated files to receive the expected data."), _storefrontEventsTrackStorefrontEventMutation),
24
+ variables: {
25
+ input: {
26
+ ...input,
27
+ tracking
28
+ }
29
+ },
30
+ onCompleted: () => undefined,
31
+ onError: () => undefined
32
+ });
33
+ } catch {
34
+ // Ignore — see above.
35
+ }
36
+ }
37
+
38
+ // React can run an effect twice for one mount – StrictMode in development, a
39
+ // Suspense retry – and each run would count as a second look.
40
+ const VIEW_REPEAT_MS = 2000;
41
+ let lastView = null;
42
+
43
+ /** Reports the store modal opening on a collection. */
44
+ export function trackProductViewed(environment, collectionId) {
45
+ var _lastView;
46
+ const now = Date.now();
47
+ if (((_lastView = lastView) === null || _lastView === void 0 ? void 0 : _lastView.collectionId) === collectionId && now - lastView.at < VIEW_REPEAT_MS) return;
48
+ lastView = {
49
+ collectionId,
50
+ at: now
51
+ };
52
+ track(environment, {
53
+ event: 'PRODUCT_VIEWED',
54
+ collectionId
55
+ });
56
+ }
57
+ /**
58
+ * The licence choices in the store's state, in the shape `createOrderItems`
59
+ * takes them – one selection per licence variable, or one bare selection for a
60
+ * licence without variables. Sorted, so the same configuration reached in a
61
+ * different order reads the same.
62
+ */
63
+ export function licenseSelectionsFromOptions(licenseOptions) {
64
+ const selections = licenseOptions.flatMap(_ref => {
65
+ let {
66
+ licenseId,
67
+ licenseOptions: options
68
+ } = _ref;
69
+ const variableIds = Object.keys(options ?? {});
70
+ if (variableIds.length === 0) return [{
71
+ licenseId
72
+ }];
73
+ return variableIds.map(licenseVariableId => ({
74
+ licenseId,
75
+ licenseVariableId,
76
+ licenseOptionId: options[licenseVariableId]
77
+ }));
78
+ });
79
+ const key = s => `${s.licenseId}:${s.licenseVariableId ?? ''}`;
80
+ return selections.sort((a, b) => key(a).localeCompare(key(b)));
81
+ }
82
+
83
+ // A buyer clicks through a few licences and sizes before settling. Report the
84
+ // configuration they settle on rather than every step on the way, and only
85
+ // when it differs from the last one reported for that collection.
86
+ const CUSTOMIZE_SETTLE_MS = 1500;
87
+ const pendingCustomizations = new Map();
88
+ const reportedConfigurations = new Map();
89
+
90
+ /**
91
+ * Reports the buyer changing their licence choices for a collection, once the
92
+ * choices settle. Call it from the handler of the buyer's own click, never from
93
+ * an effect: licences a foundry marks as selected by default are applied on
94
+ * mount, and that isn't the buyer customizing anything.
95
+ *
96
+ * `currentLicenseOptions` is read when the report fires, so it sees the
97
+ * configuration the buyer settled on.
98
+ */
99
+ export function trackProductCustomized(environment, collectionId, currentLicenseOptions) {
100
+ clearTimeout(pendingCustomizations.get(collectionId));
101
+ pendingCustomizations.set(collectionId, setTimeout(() => {
102
+ pendingCustomizations.delete(collectionId);
103
+ const licenseSelections = licenseSelectionsFromOptions(currentLicenseOptions());
104
+ // Unticking everything configures nothing.
105
+ if (licenseSelections.length === 0) return;
106
+ const configuration = JSON.stringify(licenseSelections);
107
+ if (reportedConfigurations.get(collectionId) === configuration) return;
108
+ reportedConfigurations.set(collectionId, configuration);
109
+ track(environment, {
110
+ event: 'PRODUCT_CUSTOMIZED',
111
+ collectionId,
112
+ licenseSelections
113
+ });
114
+ }, CUSTOMIZE_SETTLE_MS));
115
+ }
116
+
117
+ /**
118
+ * `trackProductCustomized` bound to the store and Relay environment in
119
+ * context, for a licence control's click handlers.
120
+ */
121
+ export function useTrackProductCustomized(collectionId) {
122
+ const environment = useRelayEnvironment();
123
+ const store = useStore();
124
+ return useCallback(() => {
125
+ if (!collectionId) return;
126
+ trackProductCustomized(environment, collectionId, () => store.getState().licenseOptions);
127
+ }, [environment, store, collectionId]);
128
+ }
129
+
130
+ /**
131
+ * Reports the checkout's payment details as complete. The server records it
132
+ * once per order, however often this is called.
133
+ */
134
+ export function trackPaymentInfoEntered(environment) {
135
+ track(environment, {
136
+ event: 'PAYMENT_INFO_ENTERED'
137
+ });
138
+ }
@@ -24,20 +24,21 @@ function FontFamilies(_ref) {
24
24
  } = useLicenseAndOrderVariables();
25
25
  const {
26
26
  node
27
- } = useLazyLoadQuery((_FontFamiliesQuery.hash && _FontFamiliesQuery.hash !== "18eba6b750d3f714b964af66ecfcfa3b" && console.error("The definition of 'FontFamiliesQuery' appears to have changed. Run `relay-compiler` to update the generated files to receive the expected data."), _FontFamiliesQuery), {
27
+ } = useLazyLoadQuery((_FontFamiliesQuery.hash && _FontFamiliesQuery.hash !== "1ccff8f69fae99e65e6007bf25e4f8a3" && console.error("The definition of 'FontFamiliesQuery' appears to have changed. Run `relay-compiler` to update the generated files to receive the expected data."), _FontFamiliesQuery), {
28
28
  collectionId,
29
29
  licenseOptions: licenseOptionsSpecs,
30
30
  orderVariables: orderVariableSelections
31
31
  });
32
32
  if (!node) return null;
33
+ // Unlisted collections are only sold through an upgrade – see StoreModalProduct.
33
34
  return /*#__PURE__*/React.createElement("div", {
34
35
  className: "font-families"
35
36
  }, /*#__PURE__*/React.createElement("div", {
36
37
  className: "font-families__items"
37
- }, /*#__PURE__*/React.createElement(Family, {
38
+ }, node.visibility !== 'UNLISTED' && /*#__PURE__*/React.createElement(Family, {
38
39
  node: node,
39
40
  size: "wide"
40
- }), (_node$families = node.families) === null || _node$families === void 0 ? void 0 : _node$families.map((family, index) => /*#__PURE__*/React.createElement(Family, {
41
+ }), (_node$families = node.families) === null || _node$families === void 0 ? void 0 : _node$families.filter(family => family.visibility !== 'UNLISTED').map((family, index) => /*#__PURE__*/React.createElement(Family, {
41
42
  key: index,
42
43
  node: family
43
44
  }))));
@@ -13,6 +13,7 @@ import { Price } from '../Price/index.js';
13
13
  import { pluralize } from '../../utils.js';
14
14
  import ComponentsContext from '../ComponentsContext.js';
15
15
  import { orderTrackingInput } from '../Cart/orderTracking.js';
16
+ import { useTrackProductCustomized } from '../Cart/storefrontEvents.js';
16
17
  function skuName(sku) {
17
18
  if (sku.product && 'name' in sku.product) return sku.product.name;
18
19
  return null;
@@ -79,6 +80,7 @@ function Precart(_ref3) {
79
80
  let {
80
81
  viewer: viewerKey,
81
82
  collection: collectionKey,
83
+ collectionId,
82
84
  isLoading
83
85
  } = _ref3;
84
86
  const viewer = useFragment((_Precart_viewer.hash && _Precart_viewer.hash !== "fe66a5cdbee38cae7bfb11922f945fd5" && console.error("The definition of 'Precart_viewer' appears to have changed. Run `relay-compiler` to update the generated files to receive the expected data."), _Precart_viewer), viewerKey);
@@ -111,6 +113,7 @@ function Precart(_ref3) {
111
113
  }
112
114
  });
113
115
  }, [environment]);
116
+ const trackCustomized = useTrackProductCustomized(collectionId);
114
117
  const onSelectLicense = (id, checked) => {
115
118
  var _collection$licenses, _license$excludedLice;
116
119
  const license = collection === null || collection === void 0 ? void 0 : (_collection$licenses = collection.licenses) === null || _collection$licenses === void 0 ? void 0 : _collection$licenses.find(node => node.id === id);
@@ -123,6 +126,7 @@ function Precart(_ref3) {
123
126
  licenseOptionSpec,
124
127
  excludedLicenseIds: (_license$excludedLice = license.excludedLicenses) === null || _license$excludedLice === void 0 ? void 0 : _license$excludedLice.map(l => l.id)
125
128
  });
129
+ trackCustomized();
126
130
  };
127
131
  const onSelectLicenseOption = (licenseId, variableId, optionId) => {
128
132
  dispatch({
@@ -131,6 +135,7 @@ function Precart(_ref3) {
131
135
  variableId,
132
136
  optionId
133
137
  });
138
+ trackCustomized();
134
139
  };
135
140
  const selectedItemsArray = () => Object.keys(selectedSkuIds).filter(id => selectedSkuIds[id] === true);
136
141
  const licenseSelectionsArray = () => licenseOptions.reduce((res, _ref4) => {
@@ -340,6 +345,7 @@ function PrecartQueryRenderer(_ref6) {
340
345
  collectionId
341
346
  });
342
347
  return /*#__PURE__*/React.createElement(Precart, _extends({}, data, rest, {
348
+ collectionId: collectionId,
343
349
  isLoading: licenseOptionsSpecs !== deferredLicenseOptionsSpecs || selectedSkuIds !== deferredSelectedSkuIds
344
350
  }));
345
351
  }
@@ -6,7 +6,7 @@ function PriceBar(_ref) {
6
6
  let {
7
7
  node: nodeKey
8
8
  } = _ref;
9
- const node = useFragment((_PriceBar_node.hash && _PriceBar_node.hash !== "60c5de3c786db43860478f1b0e00f3df" && console.error("The definition of 'PriceBar_node' appears to have changed. Run `relay-compiler` to update the generated files to receive the expected data."), _PriceBar_node), nodeKey);
9
+ const node = useFragment((_PriceBar_node.hash && _PriceBar_node.hash !== "ccde4ddfe0fb771462c7a0eccf4db454" && console.error("The definition of 'PriceBar_node' appears to have changed. Run `relay-compiler` to update the generated files to receive the expected data."), _PriceBar_node), nodeKey);
10
10
  const [visible, setVisible] = useState(false);
11
11
  const $pricebar = useRef(null);
12
12
  useEffect(() => {
@@ -25,15 +25,17 @@ function PriceBar(_ref) {
25
25
  };
26
26
  }, [setVisible, $pricebar]);
27
27
  if (!node) return null;
28
+
29
+ // Unlisted collections are only sold through an upgrade – see StoreModalProduct.
28
30
  return /*#__PURE__*/React.createElement("div", {
29
31
  className: "price-bar",
30
32
  ref: $pricebar,
31
33
  "data-visible": visible
32
34
  }, /*#__PURE__*/React.createElement("div", {
33
35
  className: "price-bar__wrapper"
34
- }, node.parent && /*#__PURE__*/React.createElement(PriceBarSection, {
36
+ }, node.parent && node.parent.visibility !== 'UNLISTED' && /*#__PURE__*/React.createElement(PriceBarSection, {
35
37
  node: node.parent
36
- }), /*#__PURE__*/React.createElement(PriceBarSection, {
38
+ }), node.visibility !== 'UNLISTED' && /*#__PURE__*/React.createElement(PriceBarSection, {
37
39
  node: node
38
40
  })));
39
41
  }
@@ -2,13 +2,7 @@ import { Environment } from 'react-relay';
2
2
  import { Store } from 'redux';
3
3
  import { FontdueState, FontdueAction } from '../../reducer.js';
4
4
  export declare const populateProductState: (store: Store<FontdueState, FontdueAction>, environment: Environment, id: string) => import("relay-runtime").Subscription | undefined;
5
- export interface CollectionStyleSkus {
6
- fontStyleSkuIds: string[];
7
- fontStyleIds: string[];
8
- childrenSkuIds: string[];
9
- name: string;
10
- }
11
- export interface CollectionStyleSkusMap {
12
- [collectionSkuId: string]: CollectionStyleSkus | undefined;
5
+ export interface SkuStyleIds {
6
+ [skuId: string]: string[];
13
7
  }
14
8
  export declare const populateProductStateReducer: (state: FontdueState, action: FontdueAction) => FontdueState;
@@ -53,58 +53,50 @@ const allSkus = function (collection) {
53
53
  }
54
54
  return [includeCollections ? collection === null || collection === void 0 ? void 0 : collection.sku : null, ...(includeFontStyles && 'fontStyles' in collection ? (collection === null || collection === void 0 ? void 0 : (_collection$fontStyle = collection.fontStyles) === null || _collection$fontStyle === void 0 ? void 0 : _collection$fontStyle.map(style => style.sku)) ?? [] : []), ...bundleSkus, ...collectionBundleSkus, ...memberCollectionSkus, ...childrenSkus].filter(notEmpty);
55
55
  };
56
- const flattenSkuData = collection => {
57
- let bundles = {};
58
- if ('bundles' in collection && collection.bundles) {
59
- bundles = collection.bundles.reduce((res, bundle) => ({
60
- ...res,
61
- ...flattenSkuData(bundle)
62
- }), {});
63
- }
64
- let collectionBundles = {};
65
- if ('collectionBundles' in collection && collection.collectionBundles) {
66
- collectionBundles = collection.collectionBundles.reduce((res, collBundle) => ({
67
- ...res,
68
- ...flattenSkuData(collBundle)
69
- }), {});
70
- }
71
- let children = {};
72
- if ('children' in collection && collection.children) {
73
- children = collection.children.reduce((res, child) => ({
74
- ...res,
75
- ...flattenSkuData(child)
76
- }), {});
77
- }
78
- let collectionData = {};
79
- if (collection.sku) {
80
- var _collection$fontStyle2, _collection$children2;
81
- // For collection bundles, get font style IDs from member collections
82
- const fontStyleIds = 'memberCollections' in collection && collection.memberCollections ? collection.memberCollections.flatMap(member => {
83
- var _member$fontStyles2;
84
- return ((_member$fontStyles2 = member.fontStyles) === null || _member$fontStyles2 === void 0 ? void 0 : _member$fontStyles2.map(style => style.id)) ?? [];
85
- }) : 'fontStyles' in collection ? (((_collection$fontStyle2 = collection.fontStyles) === null || _collection$fontStyle2 === void 0 ? void 0 : _collection$fontStyle2.map(style => style.id)) ?? []).concat('children' in collection ? ((_collection$children2 = collection.children) === null || _collection$children2 === void 0 ? void 0 : _collection$children2.flatMap(child => {
86
- var _child$fontStyles;
87
- return ((_child$fontStyles = child.fontStyles) === null || _child$fontStyles === void 0 ? void 0 : _child$fontStyles.map(style => style.id)) ?? [];
88
- })) ?? [] : []) : [];
89
- collectionData = {
90
- [collection.sku.id]: {
91
- fontStyleIds,
92
- fontStyleSkuIds: allSkus(collection, false, true).map(sku => sku.id),
93
- childrenSkuIds: allSkus(collection, true, false).map(sku => sku.id).filter(skuId => {
94
- var _collection$sku;
95
- return skuId !== ((_collection$sku = collection.sku) === null || _collection$sku === void 0 ? void 0 : _collection$sku.id);
96
- }),
97
- // exclude self
98
- name: collection.name
99
- }
100
- };
56
+
57
+ // The styles each SKU grants, by SKU id: a style's SKU grants that style, a
58
+ // bundle its styles, a family its styles, a superfamily its families' styles
59
+ // and a collection bundle its member collections' styles. This is all the
60
+ // selection needs to price any combination of them (see selection.ts).
61
+
62
+ const styleIdsOf = collection => {
63
+ var _collection$fontStyle2, _collection$children2;
64
+ if ('memberCollections' in collection && collection.memberCollections) {
65
+ return collection.memberCollections.flatMap(member => member.fontStyles.map(s => s.id));
101
66
  }
102
- return {
103
- ...collectionData,
104
- ...children,
105
- ...bundles,
106
- ...collectionBundles
67
+ const own = 'fontStyles' in collection ? ((_collection$fontStyle2 = collection.fontStyles) === null || _collection$fontStyle2 === void 0 ? void 0 : _collection$fontStyle2.map(s => s.id)) ?? [] : [];
68
+ const children = 'children' in collection ? ((_collection$children2 = collection.children) === null || _collection$children2 === void 0 ? void 0 : _collection$children2.flatMap(child => {
69
+ var _child$fontStyles;
70
+ return ((_child$fontStyles = child.fontStyles) === null || _child$fontStyles === void 0 ? void 0 : _child$fontStyles.map(s => s.id)) ?? [];
71
+ })) ?? [] : [];
72
+ return [...new Set([...own, ...children])];
73
+ };
74
+ const flattenSkuStyleIds = collection => {
75
+ var _collection$sku;
76
+ const result = {};
77
+ const add = (skuId, styleIds) => {
78
+ // A collection that returns no styles of its own (a superfamily listed as
79
+ // a collection bundle member) says nothing about what it grants.
80
+ if (skuId && styleIds.length > 0) result[skuId] = styleIds;
107
81
  };
82
+ const addStyles = fontStyles => {
83
+ for (const style of fontStyles ?? []) {
84
+ var _style$sku;
85
+ add((_style$sku = style.sku) === null || _style$sku === void 0 ? void 0 : _style$sku.id, [style.id]);
86
+ }
87
+ };
88
+ add((_collection$sku = collection.sku) === null || _collection$sku === void 0 ? void 0 : _collection$sku.id, styleIdsOf(collection));
89
+ if ('fontStyles' in collection) addStyles(collection.fontStyles);
90
+ if ('memberCollections' in collection) {
91
+ for (const member of collection.memberCollections ?? []) {
92
+ var _member$sku;
93
+ add((_member$sku = member.sku) === null || _member$sku === void 0 ? void 0 : _member$sku.id, member.fontStyles.map(s => s.id));
94
+ addStyles(member.fontStyles);
95
+ }
96
+ }
97
+ const nested = [...('bundles' in collection ? collection.bundles ?? [] : []), ...('collectionBundles' in collection ? collection.collectionBundles ?? [] : []), ...('children' in collection ? collection.children ?? [] : [])];
98
+ for (const child of nested) Object.assign(result, flattenSkuStyleIds(child));
99
+ return result;
108
100
  };
109
101
  const flattenSkuPrices = collection => {
110
102
  return allSkus(collection).reduce((acc, sku) => {
@@ -122,14 +114,11 @@ export const populateProductStateReducer = (state, action) => {
122
114
  return {
123
115
  ...state,
124
116
  // {
125
- // [collectionSkuId]: {
126
- // childrenSkuIds: [skuId...],
127
- // fontStyleSkuIds: [skuId...],
128
- // }
117
+ // [skuId]: [styleId...]
129
118
  // }
130
- collectionStyleSkus: {
131
- ...state.collectionStyleSkus,
132
- ...flattenSkuData(data.node)
119
+ skuStyleIds: {
120
+ ...state.skuStyleIds,
121
+ ...flattenSkuStyleIds(data.node)
133
122
  },
134
123
  // {
135
124
  // [skuId]: amount,
@@ -3,20 +3,19 @@ import _SKUPrice_sku from "../../__generated__/SKUPrice_sku.graphql.js";
3
3
  import React from 'react';
4
4
  import { graphql, useFragment } from 'react-relay';
5
5
  import Price from '../Price/index.js';
6
- import { collectionSkuIdWithDiscount, isSelected } from '../../reducer.js';
6
+ import { isSelected } from '../../reducer.js';
7
+ import { additionalCost } from '../../selection.js';
7
8
  import { useSelector } from 'react-redux';
8
9
  const getSavingsMultiplier = (state, options) => {
9
10
  if (options.showCollectionSavings === false) return 1;
10
11
  if (!options.sku) return 1;
11
12
  const baseSkuPrice = state.skuPrices[options.sku.id];
12
- const [collectionWithSavings, savingsAmount] = collectionSkuIdWithDiscount(state, options.sku.id);
13
+ const cost = additionalCost(state, options.sku.id);
13
14
 
14
- // we make it a multiplier since the `collectionSkuIdsDifference`
15
- // helper doesnt take license multiplication into account
16
- if (collectionWithSavings && baseSkuPrice > 0) {
17
- return savingsAmount / baseSkuPrice;
18
- }
19
- return 1;
15
+ // A multiplier, since `additionalCost` works in unlicensed prices and the
16
+ // price shown here carries the license's multipliers
17
+ if (cost === null || !(baseSkuPrice > 0)) return 1;
18
+ return cost / baseSkuPrice;
20
19
  };
21
20
  function SKUPrice(_ref) {
22
21
  let {
@@ -38,7 +38,7 @@ export default function StoreModalFamily(_ref) {
38
38
  onSelect,
39
39
  title
40
40
  } = _ref;
41
- const collection = useFragment((_StoreModalFamily_collection.hash && _StoreModalFamily_collection.hash !== "d2d429ebada7593dd5debcce7a69317d" && console.error("The definition of 'StoreModalFamily_collection' appears to have changed. Run `relay-compiler` to update the generated files to receive the expected data."), _StoreModalFamily_collection), collectionKey);
41
+ const collection = useFragment((_StoreModalFamily_collection.hash && _StoreModalFamily_collection.hash !== "f136cd0050f0f75f561221fac75feb85" && console.error("The definition of 'StoreModalFamily_collection' appears to have changed. Run `relay-compiler` to update the generated files to receive the expected data."), _StoreModalFamily_collection), collectionKey);
42
42
  const fontStyles = useMemo(() => stylesGrouped(collection), [collection]);
43
43
  if ((fontStyles === null || fontStyles === void 0 ? void 0 : fontStyles.length) === 0 && collection.collectionType === 'superfamily' && !collection.sku) return null;
44
44
  return /*#__PURE__*/React.createElement(FamilyElement, null, {
@@ -47,7 +47,9 @@ export default function StoreModalFamily(_ref) {
47
47
  collection: collection,
48
48
  onSelectSku: onSelect
49
49
  }),
50
- bundles: collection === null || collection === void 0 ? void 0 : (_collection$bundles = collection.bundles) === null || _collection$bundles === void 0 ? void 0 : _collection$bundles.filter(bundle => !bundle.hidden).map(bundle => /*#__PURE__*/React.createElement(StoreModalBundleButton, {
50
+ // An unlisted bundle gets no button, but its price still applies as an
51
+ // upgrade: the product state reads every bundle.
52
+ bundles: collection === null || collection === void 0 ? void 0 : (_collection$bundles = collection.bundles) === null || _collection$bundles === void 0 ? void 0 : _collection$bundles.filter(bundle => bundle.visibility !== 'UNLISTED').map(bundle => /*#__PURE__*/React.createElement(StoreModalBundleButton, {
51
53
  key: bundle.id,
52
54
  bundle: bundle,
53
55
  onSelectSku: onSelect
@@ -1,20 +1,61 @@
1
+ import _StoreModalIndex_viewer from "../../__generated__/StoreModalIndex_viewer.graphql.js";
1
2
  import _StoreModalIndexQuery from "../../__generated__/StoreModalIndexQuery.graphql.js";
2
- import React from 'react';
3
- import { graphql, usePreloadedQuery } from 'react-relay';
3
+ import React, { useCallback, useContext } from 'react';
4
+ import { graphql, usePaginationFragment, usePreloadedQuery } from 'react-relay';
4
5
  import StoreModalPageContainer from '../elements/StoreModalPageContainer/index.js';
6
+ import StoreModalIndexLoadMore from '../elements/StoreModalIndexLoadMore/index.js';
7
+ import StoreModalContainerContext from '../elements/StoreModalContainer/StoreModalContainerContext.js';
5
8
  import StoreModalIndexItem from './StoreModalIndexItem.js';
9
+ import useInfiniteScroll from '../useInfiniteScroll.js';
10
+
11
+ // How many collections arrive at a time. Enough that a typical catalogue is one
12
+ // request, few enough that a large one doesn't make the visitor wait for every
13
+ // collection's feature style before anything renders. The GraphQL below can't
14
+ // interpolate, so the same number is written into the query's `$count` default
15
+ // and the fragment's argument definitions — keep the three in step.
16
+ const PAGE_SIZE = 25;
17
+
18
+ // Start the next page while the sentinel is still this far below the modal's
19
+ // visible area, so the pages join up without the visitor reaching a dead end.
20
+ const LOAD_MORE_MARGIN = '0px 0px 600px 0px';
6
21
  const StoreModalIndex = _ref => {
7
- var _viewer$rootCollectio;
22
+ var _data$rootCollections;
8
23
  let {
9
24
  prepared
10
25
  } = _ref;
11
- const data = usePreloadedQuery((_StoreModalIndexQuery.hash && _StoreModalIndexQuery.hash !== "099c77808b41054885365ebe3eda03c8" && console.error("The definition of 'StoreModalIndexQuery' appears to have changed. Run `relay-compiler` to update the generated files to receive the expected data."), _StoreModalIndexQuery), prepared.indexQuery);
12
- const viewer = data.viewer;
13
- const edges = (viewer === null || viewer === void 0 ? void 0 : (_viewer$rootCollectio = viewer.rootCollections) === null || _viewer$rootCollectio === void 0 ? void 0 : _viewer$rootCollectio.edges) ?? [];
26
+ const queryData = usePreloadedQuery((_StoreModalIndexQuery.hash && _StoreModalIndexQuery.hash !== "125a10155b690929f0eae6e9e6c7188c" && console.error("The definition of 'StoreModalIndexQuery' appears to have changed. Run `relay-compiler` to update the generated files to receive the expected data."), _StoreModalIndexQuery), prepared.indexQuery);
27
+
28
+ // The query type only carries the fragment spread, not its data type, so
29
+ // both sides are named explicitly for `data` to be typed.
30
+ const {
31
+ data,
32
+ loadNext,
33
+ hasNext,
34
+ isLoadingNext
35
+ } = usePaginationFragment((_StoreModalIndex_viewer.hash && _StoreModalIndex_viewer.hash !== "f4bd120deb8eea66de4257529cbb022e" && console.error("The definition of 'StoreModalIndex_viewer' appears to have changed. Run `relay-compiler` to update the generated files to receive the expected data."), _StoreModalIndex_viewer), queryData.viewer);
36
+ const {
37
+ scrollContainer
38
+ } = useContext(StoreModalContainerContext);
39
+ const handleLoadMore = useCallback(() => {
40
+ // Relay ignores this while a page is already in flight, so the observer
41
+ // firing repeatedly on the way down costs nothing.
42
+ loadNext(PAGE_SIZE);
43
+ }, [loadNext]);
44
+ const sentinelRef = useInfiniteScroll({
45
+ enabled: hasNext && !isLoadingNext,
46
+ onLoadMore: handleLoadMore,
47
+ rootRef: scrollContainer,
48
+ rootMargin: LOAD_MORE_MARGIN
49
+ });
50
+ const edges = (data === null || data === void 0 ? void 0 : (_data$rootCollections = data.rootCollections) === null || _data$rootCollections === void 0 ? void 0 : _data$rootCollections.edges) ?? [];
14
51
  return /*#__PURE__*/React.createElement(StoreModalPageContainer, null, {
15
- body: edges.map(edge => /*#__PURE__*/React.createElement(StoreModalIndexItem, {
52
+ body: /*#__PURE__*/React.createElement(React.Fragment, null, edges.map(edge => /*#__PURE__*/React.createElement(StoreModalIndexItem, {
16
53
  collection: edge.node,
17
54
  key: edge.node.id
55
+ })), hasNext && /*#__PURE__*/React.createElement(StoreModalIndexLoadMore, {
56
+ sentinelRef: sentinelRef,
57
+ onClick: handleLoadMore,
58
+ loading: isLoadingNext
18
59
  }))
19
60
  });
20
61
  };
@@ -16,6 +16,7 @@ import ConfigContext from '../ConfigContext.js';
16
16
  import { useRefetchOnLicenseChanges } from '../../hooks/useRefetchOnLicenseChanges.js';
17
17
  import StoreModalLicenseeIsBillingSelection from './StoreModalLicenseeIsBillingSelection.js';
18
18
  import { useLicenseAndOrderVariables } from '../../hooks/useLicenseAndOrderVariables.js';
19
+ import { trackProductViewed } from '../Cart/storefrontEvents.js';
19
20
 
20
21
  // A product is addressed by collection id or by slug — the same two
21
22
  // identifiers `BuyButton` takes. Two queries rather than one with `@skip`,
@@ -29,7 +30,7 @@ export const productIdQuery = (_StoreModalProductQuery.hash && _StoreModalProduc
29
30
  export const productSlugQuery = (_StoreModalProductSlugQuery.hash && _StoreModalProductSlugQuery.hash !== "f0bb35ed9f88ad214b40fd1f3b92fed0" && console.error("The definition of 'StoreModalProductSlugQuery' appears to have changed. Run `relay-compiler` to update the generated files to receive the expected data."), _StoreModalProductSlugQuery);
30
31
  const productViewerFragment = (_StoreModalProductViewer.hash && _StoreModalProductViewer.hash !== "3d2fc7da461822a44bb26e9b3f1715fb" && console.error("The definition of 'StoreModalProductViewer' appears to have changed. Run `relay-compiler` to update the generated files to receive the expected data."), _StoreModalProductViewer);
31
32
  const StoreModalProductContent = _ref => {
32
- var _config$storeModal, _viewer$orderVariable, _viewer$settings, _viewer$settings2, _viewer$settings3, _viewer$settings4, _viewer$settings5, _viewer$settings6, _viewer$settings7, _viewer$settings8, _collection$children;
33
+ var _config$storeModal, _viewer$orderVariable, _viewer$settings, _viewer$settings2, _viewer$settings3, _viewer$settings4, _viewer$settings5, _viewer$settings6, _viewer$settings7, _viewer$settings8;
33
34
  let {
34
35
  collection: collectionRef,
35
36
  viewer: viewerRef
@@ -37,7 +38,7 @@ const StoreModalProductContent = _ref => {
37
38
  const config = useContext(ConfigContext);
38
39
  const environment = useRelayEnvironment();
39
40
  const viewer = useFragment(productViewerFragment, viewerRef);
40
- const [data, refetch] = useRefetchableFragment((_StoreModalProductContent_collection.hash && _StoreModalProductContent_collection.hash !== "0684ec2633ccc5d4d0866e7780df28cc" && console.error("The definition of 'StoreModalProductContent_collection' appears to have changed. Run `relay-compiler` to update the generated files to receive the expected data."), _StoreModalProductContent_collection), collectionRef ?? null);
41
+ const [data, refetch] = useRefetchableFragment((_StoreModalProductContent_collection.hash && _StoreModalProductContent_collection.hash !== "f7a9e591b5e1596f4d518fc5d9d46e91" && console.error("The definition of 'StoreModalProductContent_collection' appears to have changed. Run `relay-compiler` to update the generated files to receive the expected data."), _StoreModalProductContent_collection), collectionRef ?? null);
41
42
  const collection = data;
42
43
  const refetchVariables = useMemo(() => collection !== null && collection !== void 0 && collection.id ? {
43
44
  id: collection.id
@@ -60,6 +61,9 @@ const StoreModalProductContent = _ref => {
60
61
  useEffect(() => {
61
62
  if (collection !== null && collection !== void 0 && collection.id) populateProductState(store, environment, collection.id);
62
63
  }, [collection === null || collection === void 0 ? void 0 : collection.id]);
64
+ useEffect(() => {
65
+ if (collection !== null && collection !== void 0 && collection.id) trackProductViewed(environment, collection.id);
66
+ }, [collection === null || collection === void 0 ? void 0 : collection.id]);
63
67
  useEffect(() => {
64
68
  return () => {
65
69
  // unselect the skus when this component unmounts,
@@ -126,6 +130,12 @@ const StoreModalProductContent = _ref => {
126
130
  __html: ((_viewer$settings8 = viewer.settings) === null || _viewer$settings8 === void 0 ? void 0 : _viewer$settings8.storeModalSelectFontsLabel) ?? ''
127
131
  }
128
132
  });
133
+
134
+ // An unlisted collection gets no block – no family button, bundles or
135
+ // styles – but stays in the product state, so selecting its siblings can
136
+ // still upgrade to it (an unlisted superfamily) or to a collection bundle
137
+ // containing it (an unlisted variable family).
138
+ const selectableFamilies = [collection, ...(collection.children ?? [])].filter(family => family.visibility !== 'UNLISTED');
129
139
  return /*#__PURE__*/React.createElement(StoreModalPageContainer, null, {
130
140
  body: /*#__PURE__*/React.createElement(Fragment, null, licensesPosition === 'top' && /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(StoreModalLicenseeIsBillingSelection, {
131
141
  disabled: false
@@ -148,13 +158,10 @@ const StoreModalProductContent = _ref => {
148
158
  })), /*#__PURE__*/React.createElement("div", {
149
159
  className: "store-modal__families",
150
160
  "data-disabled": licensesPosition === 'top' ? !anyLicenseOptionsSelected || !hasSelectionForLicenseeIsBillingIdentity || !!requiredOrderVariablesNotYetSelected : false
151
- }, /*#__PURE__*/React.createElement(StoreModalFamily, {
152
- title: selectFontsLabel,
153
- collection: collection,
154
- onSelect: handleSelectProduct
155
- }), (_collection$children = collection.children) === null || _collection$children === void 0 ? void 0 : _collection$children.map(child => /*#__PURE__*/React.createElement(StoreModalFamily, {
156
- key: child.id,
157
- collection: child,
161
+ }, selectableFamilies.map((family, index) => /*#__PURE__*/React.createElement(StoreModalFamily, {
162
+ key: family.id,
163
+ title: index === 0 ? selectFontsLabel : undefined,
164
+ collection: family,
158
165
  onSelect: handleSelectProduct
159
166
  }))), licensesPosition === 'bottom' && /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(StoreModalLicenseeIsBillingSelection, {
160
167
  disabled: !anySkusSelected
@@ -2,6 +2,7 @@ import React from 'react';
2
2
  import { StoreModalProductLicense_license$key } from '../../__generated__/StoreModalProductLicense_license.graphql.js';
3
3
  interface StoreModalProductLicenseProps {
4
4
  license: StoreModalProductLicense_license$key;
5
+ collectionId?: string;
5
6
  }
6
- export default function StoreModalProduceLicense({ license: licenseKey, }: StoreModalProductLicenseProps): React.JSX.Element;
7
+ export default function StoreModalProduceLicense({ license: licenseKey, collectionId, }: StoreModalProductLicenseProps): React.JSX.Element;
7
8
  export {};
@@ -6,6 +6,7 @@ import LicenseElement from './LicenseElement.js';
6
6
  import LicenseVariableElement from './LicenseVariableElement.js';
7
7
  import VariableTableAmounts from '../VariableTableAmounts/index.js';
8
8
  import LicenseVariableRadioElement from './LicenseVariableRadioElement.js';
9
+ import { useTrackProductCustomized } from '../Cart/storefrontEvents.js';
9
10
  const specFromLicense = license => {
10
11
  let licenseOptionSpec = {
11
12
  licenseId: license.id
@@ -31,7 +32,8 @@ const specFromLicense = license => {
31
32
  export default function StoreModalProduceLicense(_ref) {
32
33
  var _license$variables;
33
34
  let {
34
- license: licenseKey
35
+ license: licenseKey,
36
+ collectionId
35
37
  } = _ref;
36
38
  const license = useFragment((_StoreModalProductLicense_license.hash && _StoreModalProductLicense_license.hash !== "87cd41e76dcd8952d7096940a383b9f3" && console.error("The definition of 'StoreModalProductLicense_license' appears to have changed. Run `relay-compiler` to update the generated files to receive the expected data."), _StoreModalProductLicense_license), licenseKey);
37
39
  const licenseOptions = useSelector(state => state.licenseOptions);
@@ -43,6 +45,7 @@ export default function StoreModalProduceLicense(_ref) {
43
45
  });
44
46
  const isSelected = Boolean(selectedOptions);
45
47
  const dispatch = useDispatch();
48
+ const trackCustomized = useTrackProductCustomized(collectionId);
46
49
  const handleSelectLicense = useCallback(() => {
47
50
  var _license$excludedLice;
48
51
  dispatch({
@@ -52,7 +55,8 @@ export default function StoreModalProduceLicense(_ref) {
52
55
  licenseOptionSpec: specFromLicense(license),
53
56
  excludedLicenseIds: (_license$excludedLice = license.excludedLicenses) === null || _license$excludedLice === void 0 ? void 0 : _license$excludedLice.map(l => l.id)
54
57
  });
55
- }, [dispatch, license, isSelected]);
58
+ trackCustomized();
59
+ }, [dispatch, license, isSelected, trackCustomized]);
56
60
  const handleLicenseOptionChange = useCallback(_ref3 => {
57
61
  let {
58
62
  variableId,
@@ -72,7 +76,8 @@ export default function StoreModalProduceLicense(_ref) {
72
76
  variableId,
73
77
  optionId
74
78
  });
75
- }, [dispatch, license, isSelected]);
79
+ trackCustomized();
80
+ }, [dispatch, license, isSelected, trackCustomized]);
76
81
  useEffect(() => {
77
82
  if (license.defaultSelected) {
78
83
  var _license$excludedLice3;
@@ -11,7 +11,7 @@ export default function StoreModalProductLicenseSelection(_ref) {
11
11
  label
12
12
  }
13
13
  } = _ref;
14
- const collection = useFragment((_StoreModalProductLicenseSelection_collection.hash && _StoreModalProductLicenseSelection_collection.hash !== "2767ff3f8ddb4354051074bc3d0312e6" && console.error("The definition of 'StoreModalProductLicenseSelection_collection' appears to have changed. Run `relay-compiler` to update the generated files to receive the expected data."), _StoreModalProductLicenseSelection_collection), collectionKey);
14
+ const collection = useFragment((_StoreModalProductLicenseSelection_collection.hash && _StoreModalProductLicenseSelection_collection.hash !== "c3609e849ae8057e71542d32716f3ddc" && console.error("The definition of 'StoreModalProductLicenseSelection_collection' appears to have changed. Run `relay-compiler` to update the generated files to receive the expected data."), _StoreModalProductLicenseSelection_collection), collectionKey);
15
15
  return /*#__PURE__*/React.createElement("div", {
16
16
  className: "store-modal__license-selection__container",
17
17
  "data-disabled": disabled
@@ -21,6 +21,7 @@ export default function StoreModalProductLicenseSelection(_ref) {
21
21
  className: "store-modal__license-selection__licenses"
22
22
  }, collection === null || collection === void 0 ? void 0 : (_collection$licenses = collection.licenses) === null || _collection$licenses === void 0 ? void 0 : _collection$licenses.map(license => license && /*#__PURE__*/React.createElement(StoreModalProductLicense, {
23
23
  key: license.id,
24
- license: license
24
+ license: license,
25
+ collectionId: collection.id
25
26
  }))));
26
27
  }