@shopify/hydrogen 1.4.1 → 1.4.3

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.
@@ -17,7 +17,7 @@ export declare function useCartActions({ numCartLines, cartFragment, countryCode
17
17
  /** Maximum number of cart lines to fetch. Defaults to 250 cart lines. */
18
18
  numCartLines?: number;
19
19
  /** A fragment used to query the Storefront API's [Cart object](https://shopify.dev/api/storefront/latest/objects/cart) for all queries and mutations. A default value is used if no argument is provided. */
20
- cartFragment?: string;
20
+ cartFragment: string;
21
21
  /** The ISO country code for i18n. */
22
22
  countryCode?: CountryCode;
23
23
  }): {
@@ -59,4 +59,3 @@ export declare function useCartActions({ numCartLines, cartFragment, countryCode
59
59
  }>;
60
60
  cartFragment: string;
61
61
  };
62
- export declare const defaultCartFragment = "\nfragment CartFragment on Cart {\n id\n checkoutUrl\n totalQuantity\n buyerIdentity {\n countryCode\n customer {\n id\n email\n firstName\n lastName\n displayName\n }\n email\n phone\n }\n lines(first: $numCartLines) {\n edges {\n node {\n id\n quantity\n attributes {\n key\n value\n }\n cost {\n totalAmount {\n amount\n currencyCode\n }\n compareAtAmountPerQuantity {\n amount\n currencyCode\n }\n }\n merchandise {\n ... on ProductVariant {\n id\n availableForSale\n compareAtPriceV2 {\n ...MoneyFragment\n }\n priceV2 {\n ...MoneyFragment\n }\n requiresShipping\n title\n image {\n ...ImageFragment\n }\n product {\n handle\n title\n }\n selectedOptions {\n name\n value\n }\n }\n }\n }\n }\n }\n cost {\n subtotalAmount {\n ...MoneyFragment\n }\n totalAmount {\n ...MoneyFragment\n }\n totalDutyAmount {\n ...MoneyFragment\n }\n totalTaxAmount {\n ...MoneyFragment\n }\n }\n note\n attributes {\n key\n value\n }\n discountCodes {\n code\n }\n}\n\nfragment MoneyFragment on MoneyV2 {\n currencyCode\n amount\n}\nfragment ImageFragment on Image {\n id\n url\n altText\n width\n height\n}\n";
@@ -7,7 +7,7 @@ import { useCartFetch } from './hooks.client.js';
7
7
  *
8
8
  * See [cart API graphql mutations](https://shopify.dev/api/storefront/2022-07/objects/Cart)
9
9
  */
10
- export function useCartActions({ numCartLines, cartFragment = defaultCartFragment, countryCode = CountryCode.Us, }) {
10
+ export function useCartActions({ numCartLines, cartFragment, countryCode = CountryCode.Us, }) {
11
11
  const fetchCart = useCartFetch();
12
12
  const cartFetch = useCallback((cartId) => {
13
13
  return fetchCart({
@@ -130,103 +130,3 @@ export function useCartActions({ numCartLines, cartFragment = defaultCartFragmen
130
130
  cartFragment,
131
131
  ]);
132
132
  }
133
- export const defaultCartFragment = `
134
- fragment CartFragment on Cart {
135
- id
136
- checkoutUrl
137
- totalQuantity
138
- buyerIdentity {
139
- countryCode
140
- customer {
141
- id
142
- email
143
- firstName
144
- lastName
145
- displayName
146
- }
147
- email
148
- phone
149
- }
150
- lines(first: $numCartLines) {
151
- edges {
152
- node {
153
- id
154
- quantity
155
- attributes {
156
- key
157
- value
158
- }
159
- cost {
160
- totalAmount {
161
- amount
162
- currencyCode
163
- }
164
- compareAtAmountPerQuantity {
165
- amount
166
- currencyCode
167
- }
168
- }
169
- merchandise {
170
- ... on ProductVariant {
171
- id
172
- availableForSale
173
- compareAtPriceV2 {
174
- ...MoneyFragment
175
- }
176
- priceV2 {
177
- ...MoneyFragment
178
- }
179
- requiresShipping
180
- title
181
- image {
182
- ...ImageFragment
183
- }
184
- product {
185
- handle
186
- title
187
- }
188
- selectedOptions {
189
- name
190
- value
191
- }
192
- }
193
- }
194
- }
195
- }
196
- }
197
- cost {
198
- subtotalAmount {
199
- ...MoneyFragment
200
- }
201
- totalAmount {
202
- ...MoneyFragment
203
- }
204
- totalDutyAmount {
205
- ...MoneyFragment
206
- }
207
- totalTaxAmount {
208
- ...MoneyFragment
209
- }
210
- }
211
- note
212
- attributes {
213
- key
214
- value
215
- }
216
- discountCodes {
217
- code
218
- }
219
- }
220
-
221
- fragment MoneyFragment on MoneyV2 {
222
- currencyCode
223
- amount
224
- }
225
- fragment ImageFragment on Image {
226
- id
227
- url
228
- altText
229
- width
230
- height
231
- }
232
- `;
@@ -6,6 +6,28 @@ import { useCartFetch } from './hooks.client.js';
6
6
  import { CartContext } from './context.js';
7
7
  import { CART_ID_STORAGE_KEY } from './constants.js';
8
8
  import { ClientAnalytics } from '../../foundation/Analytics/ClientAnalytics.js';
9
+ function getLocalStoragePolyfill() {
10
+ const storage = {};
11
+ return {
12
+ removeItem(key) {
13
+ delete storage[key];
14
+ },
15
+ setItem(key, value) {
16
+ storage[key] = value;
17
+ },
18
+ getItem(key) {
19
+ return storage[key];
20
+ },
21
+ };
22
+ }
23
+ const localStorage = (function () {
24
+ try {
25
+ return window.localStorage || getLocalStoragePolyfill();
26
+ }
27
+ catch (e) {
28
+ return getLocalStoragePolyfill();
29
+ }
30
+ })();
9
31
  function cartReducer(state, action) {
10
32
  switch (action.type) {
11
33
  case 'cartFetch': {
@@ -179,7 +201,7 @@ export function CartProvider({ children, numCartLines, onCreate, onLineAdd, onLi
179
201
  },
180
202
  });
181
203
  if (!data?.cart) {
182
- window.localStorage.removeItem(CART_ID_STORAGE_KEY);
204
+ localStorage.removeItem(CART_ID_STORAGE_KEY);
183
205
  dispatch({ type: 'resetCart' });
184
206
  return;
185
207
  }
@@ -226,7 +248,7 @@ export function CartProvider({ children, numCartLines, onCreate, onLineAdd, onLi
226
248
  type: 'resolve',
227
249
  cart: cartFromGraphQL(data.cartCreate.cart),
228
250
  });
229
- window.localStorage.setItem(CART_ID_STORAGE_KEY, data.cartCreate.cart.id);
251
+ localStorage.setItem(CART_ID_STORAGE_KEY, data.cartCreate.cart.id);
230
252
  }
231
253
  }, [
232
254
  onCreate,
@@ -1,7 +1,7 @@
1
1
  import React from 'react';
2
2
  import { CartFragmentFragment } from './graphql/CartFragment.js';
3
3
  import { CartBuyerIdentityInput, CountryCode } from '../../storefront-api-types.js';
4
- export declare function CartProviderV2({ children, numCartLines, onCreate, onLineAdd, onLineRemove, onLineUpdate, onNoteUpdate, onBuyerIdentityUpdate, onAttributesUpdate, onDiscountCodesUpdate, onCreateComplete, onLineAddComplete, onLineRemoveComplete, onLineUpdateComplete, onNoteUpdateComplete, onBuyerIdentityUpdateComplete, onAttributesUpdateComplete, onDiscountCodesUpdateComplete, data, cartFragment, customerAccessToken, countryCode, }: {
4
+ export declare function CartProviderV2({ children, numCartLines, onCreate, onLineAdd, onLineRemove, onLineUpdate, onNoteUpdate, onBuyerIdentityUpdate, onAttributesUpdate, onDiscountCodesUpdate, onCreateComplete, onLineAddComplete, onLineRemoveComplete, onLineUpdateComplete, onNoteUpdateComplete, onBuyerIdentityUpdateComplete, onAttributesUpdateComplete, onDiscountCodesUpdateComplete, data: cart, cartFragment, customerAccessToken, countryCode, }: {
5
5
  /** Any `ReactNode` elements. */
6
6
  children: React.ReactNode;
7
7
  /** Maximum number of cart lines to fetch. Defaults to 250 cart lines. */
@@ -47,3 +47,4 @@ export declare function CartProviderV2({ children, numCartLines, onCreate, onLin
47
47
  /** The ISO country code for i18n. */
48
48
  countryCode?: CountryCode;
49
49
  }): JSX.Element;
50
+ export declare const defaultCartFragment = "\nfragment CartFragment on Cart {\n id\n checkoutUrl\n totalQuantity\n buyerIdentity {\n countryCode\n customer {\n id\n email\n firstName\n lastName\n displayName\n }\n email\n phone\n }\n lines(first: $numCartLines) {\n edges {\n node {\n id\n quantity\n attributes {\n key\n value\n }\n cost {\n totalAmount {\n amount\n currencyCode\n }\n compareAtAmountPerQuantity {\n amount\n currencyCode\n }\n }\n merchandise {\n ... on ProductVariant {\n id\n availableForSale\n compareAtPriceV2 {\n ...MoneyFragment\n }\n priceV2 {\n ...MoneyFragment\n }\n requiresShipping\n title\n image {\n ...ImageFragment\n }\n product {\n handle\n title\n }\n selectedOptions {\n name\n value\n }\n }\n }\n }\n }\n }\n cost {\n subtotalAmount {\n ...MoneyFragment\n }\n totalAmount {\n ...MoneyFragment\n }\n totalDutyAmount {\n ...MoneyFragment\n }\n totalTaxAmount {\n ...MoneyFragment\n }\n }\n note\n attributes {\n key\n value\n }\n discountCodes {\n code\n }\n}\n\nfragment MoneyFragment on MoneyV2 {\n currencyCode\n amount\n}\nfragment ImageFragment on Image {\n id\n url\n altText\n width\n height\n}\n";
@@ -1,11 +1,10 @@
1
1
  import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
2
2
  import { CountryCode, } from '../../storefront-api-types.js';
3
3
  import { CartContext } from './context.js';
4
- import { useCartActions } from './CartActions.client.js';
5
4
  import { useCartAPIStateMachine } from './useCartAPIStateMachine.client.js';
6
5
  import { CART_ID_STORAGE_KEY } from './constants.js';
7
6
  import { ClientAnalytics } from '../../foundation/Analytics/ClientAnalytics.js';
8
- export function CartProviderV2({ children, numCartLines, onCreate, onLineAdd, onLineRemove, onLineUpdate, onNoteUpdate, onBuyerIdentityUpdate, onAttributesUpdate, onDiscountCodesUpdate, onCreateComplete, onLineAddComplete, onLineRemoveComplete, onLineUpdateComplete, onNoteUpdateComplete, onBuyerIdentityUpdateComplete, onAttributesUpdateComplete, onDiscountCodesUpdateComplete, data: cart, cartFragment, customerAccessToken, countryCode = CountryCode.Us, }) {
7
+ export function CartProviderV2({ children, numCartLines, onCreate, onLineAdd, onLineRemove, onLineUpdate, onNoteUpdate, onBuyerIdentityUpdate, onAttributesUpdate, onDiscountCodesUpdate, onCreateComplete, onLineAddComplete, onLineRemoveComplete, onLineUpdateComplete, onNoteUpdateComplete, onBuyerIdentityUpdateComplete, onAttributesUpdateComplete, onDiscountCodesUpdateComplete, data: cart, cartFragment = defaultCartFragment, customerAccessToken, countryCode = CountryCode.Us, }) {
9
8
  if (countryCode)
10
9
  countryCode = countryCode.toUpperCase();
11
10
  const [prevCountryCode, setPrevCountryCode] = useState(countryCode);
@@ -17,32 +16,34 @@ export function CartProviderV2({ children, numCartLines, onCreate, onLineAdd, on
17
16
  setPrevCustomerAccessToken(customerAccessToken);
18
17
  customerOverridesCountryCode.current = false;
19
18
  }
20
- const { cartFragment: usedCartFragment } = useCartActions({
21
- numCartLines,
22
- cartFragment,
23
- countryCode,
24
- });
25
19
  const [cartState, cartSend] = useCartAPIStateMachine({
26
20
  numCartLines,
21
+ data: cart,
27
22
  cartFragment,
23
+ countryCode,
28
24
  onCartActionEntry(context, event) {
29
- switch (event.type) {
30
- case 'CART_CREATE':
31
- return onCreate?.();
32
- case 'CARTLINE_ADD':
33
- return onLineAdd?.();
34
- case 'CARTLINE_REMOVE':
35
- return onLineRemove?.();
36
- case 'CARTLINE_UPDATE':
37
- return onLineUpdate?.();
38
- case 'NOTE_UPDATE':
39
- return onNoteUpdate?.();
40
- case 'BUYER_IDENTITY_UPDATE':
41
- return onBuyerIdentityUpdate?.();
42
- case 'CART_ATTRIBUTES_UPDATE':
43
- return onAttributesUpdate?.();
44
- case 'DISCOUNT_CODES_UPDATE':
45
- return onDiscountCodesUpdate?.();
25
+ try {
26
+ switch (event.type) {
27
+ case 'CART_CREATE':
28
+ return onCreate?.();
29
+ case 'CARTLINE_ADD':
30
+ return onLineAdd?.();
31
+ case 'CARTLINE_REMOVE':
32
+ return onLineRemove?.();
33
+ case 'CARTLINE_UPDATE':
34
+ return onLineUpdate?.();
35
+ case 'NOTE_UPDATE':
36
+ return onNoteUpdate?.();
37
+ case 'BUYER_IDENTITY_UPDATE':
38
+ return onBuyerIdentityUpdate?.();
39
+ case 'CART_ATTRIBUTES_UPDATE':
40
+ return onAttributesUpdate?.();
41
+ case 'DISCOUNT_CODES_UPDATE':
42
+ return onDiscountCodesUpdate?.();
43
+ }
44
+ }
45
+ catch (error) {
46
+ console.error('Cart entry action failed', error);
46
47
  }
47
48
  },
48
49
  onCartActionOptimisticUI(context, event) {
@@ -81,44 +82,72 @@ export function CartProviderV2({ children, numCartLines, onCreate, onLineAdd, on
81
82
  },
82
83
  onCartActionComplete(context, event) {
83
84
  const cartActionEvent = event.payload.cartActionEvent;
84
- switch (event.type) {
85
- case 'RESOLVE':
86
- switch (cartActionEvent.type) {
87
- case 'CART_CREATE':
88
- publishCreateAnalytics(context, cartActionEvent);
89
- return onCreateComplete?.();
90
- case 'CARTLINE_ADD':
91
- publishLineAddAnalytics(context, cartActionEvent);
92
- return onLineAddComplete?.();
93
- case 'CARTLINE_REMOVE':
94
- publishLineRemoveAnalytics(context, cartActionEvent);
95
- return onLineRemoveComplete?.();
96
- case 'CARTLINE_UPDATE':
97
- publishLineUpdateAnalytics(context, cartActionEvent);
98
- return onLineUpdateComplete?.();
99
- case 'NOTE_UPDATE':
100
- return onNoteUpdateComplete?.();
101
- case 'BUYER_IDENTITY_UPDATE':
102
- if (countryCodeNotUpdated(context, cartActionEvent)) {
103
- customerOverridesCountryCode.current = true;
104
- }
105
- return onBuyerIdentityUpdateComplete?.();
106
- case 'CART_ATTRIBUTES_UPDATE':
107
- return onAttributesUpdateComplete?.();
108
- case 'DISCOUNT_CODES_UPDATE':
109
- publishDiscountCodesUpdateAnalytics(context, cartActionEvent);
110
- return onDiscountCodesUpdateComplete?.();
111
- }
85
+ try {
86
+ switch (event.type) {
87
+ case 'RESOLVE':
88
+ switch (cartActionEvent.type) {
89
+ case 'CART_CREATE':
90
+ publishCreateAnalytics(context, cartActionEvent);
91
+ return onCreateComplete?.();
92
+ case 'CARTLINE_ADD':
93
+ publishLineAddAnalytics(context, cartActionEvent);
94
+ return onLineAddComplete?.();
95
+ case 'CARTLINE_REMOVE':
96
+ publishLineRemoveAnalytics(context, cartActionEvent);
97
+ return onLineRemoveComplete?.();
98
+ case 'CARTLINE_UPDATE':
99
+ publishLineUpdateAnalytics(context, cartActionEvent);
100
+ return onLineUpdateComplete?.();
101
+ case 'NOTE_UPDATE':
102
+ return onNoteUpdateComplete?.();
103
+ case 'BUYER_IDENTITY_UPDATE':
104
+ if (countryCodeNotUpdated(context, cartActionEvent)) {
105
+ customerOverridesCountryCode.current = true;
106
+ }
107
+ return onBuyerIdentityUpdateComplete?.();
108
+ case 'CART_ATTRIBUTES_UPDATE':
109
+ return onAttributesUpdateComplete?.();
110
+ case 'DISCOUNT_CODES_UPDATE':
111
+ publishDiscountCodesUpdateAnalytics(context, cartActionEvent);
112
+ return onDiscountCodesUpdateComplete?.();
113
+ }
114
+ }
115
+ }
116
+ catch (error) {
117
+ console.error('onCartActionComplete failed', error);
112
118
  }
113
119
  },
114
120
  });
115
- const [cartReady, setCartReady] = useState(false);
121
+ const cartReady = useRef(false);
116
122
  const cartCompleted = cartState.matches('cartCompleted');
117
123
  const countryChanged = (cartState.value === 'idle' ||
118
124
  cartState.value === 'error' ||
119
125
  cartState.value === 'cartCompleted') &&
120
126
  countryCode !== cartState?.context?.cart?.buyerIdentity?.countryCode &&
121
127
  !cartState.context.errors;
128
+ /**
129
+ * Initializes cart with priority in this order:
130
+ * 1. cart props
131
+ * 2. localStorage cartId
132
+ */
133
+ useEffect(() => {
134
+ if (!cartReady.current) {
135
+ if (!cart && storageAvailable('localStorage')) {
136
+ try {
137
+ const cartId = window.localStorage.getItem(CART_ID_STORAGE_KEY);
138
+ if (cartId) {
139
+ cartSend({ type: 'CART_FETCH', payload: { cartId } });
140
+ }
141
+ }
142
+ catch (error) {
143
+ console.warn('error fetching cartId');
144
+ console.warn(error);
145
+ }
146
+ }
147
+ cartReady.current = true;
148
+ }
149
+ }, [cart, cartReady, cartSend]);
150
+ // Update cart country code if cart and props countryCode's as different
122
151
  useEffect(() => {
123
152
  if (!countryChanged || customerOverridesCountryCode.current)
124
153
  return;
@@ -135,11 +164,11 @@ export function CartProviderV2({ children, numCartLines, onCreate, onLineAdd, on
135
164
  ]);
136
165
  // send cart events when ready
137
166
  const onCartReadySend = useCallback((cartEvent) => {
138
- if (!cartReady) {
167
+ if (!cartReady.current) {
139
168
  return console.warn("Cart isn't ready yet");
140
169
  }
141
170
  cartSend(cartEvent);
142
- }, [cartReady, cartSend]);
171
+ }, [cartSend]);
143
172
  // save cart id to local storage
144
173
  useEffect(() => {
145
174
  if (cartState?.context?.cart?.id && storageAvailable('localStorage')) {
@@ -162,22 +191,6 @@ export function CartProviderV2({ children, numCartLines, onCreate, onLineAdd, on
162
191
  }
163
192
  }
164
193
  }, [cartCompleted]);
165
- // fetch cart from local storage if cart id present and set cart as ready for use
166
- useEffect(() => {
167
- if (!cartReady && storageAvailable('localStorage')) {
168
- try {
169
- const cartId = window.localStorage.getItem(CART_ID_STORAGE_KEY);
170
- if (cartId) {
171
- cartSend({ type: 'CART_FETCH', payload: { cartId } });
172
- }
173
- }
174
- catch (error) {
175
- console.warn('error fetching cartId');
176
- console.warn(error);
177
- }
178
- setCartReady(true);
179
- }
180
- }, [cartReady, cartSend]);
181
194
  const cartCreate = useCallback((cartInput) => {
182
195
  if (countryCode && !cartInput.buyerIdentity?.countryCode) {
183
196
  if (cartInput.buyerIdentity == null) {
@@ -263,15 +276,15 @@ export function CartProviderV2({ children, numCartLines, onCreate, onLineAdd, on
263
276
  },
264
277
  });
265
278
  },
266
- cartFragment: usedCartFragment,
279
+ cartFragment,
267
280
  };
268
281
  }, [
269
282
  cartCreate,
283
+ cartFragment,
270
284
  cartState?.context?.cart,
271
285
  cartState?.context?.errors,
272
286
  cartState.value,
273
287
  onCartReadySend,
274
- usedCartFragment,
275
288
  ]);
276
289
  return (React.createElement(CartContext.Provider, { value: cartContextValue }, children));
277
290
  }
@@ -368,3 +381,103 @@ function publishDiscountCodesUpdateAnalytics(context, event) {
368
381
  prevCart: context.prevCart,
369
382
  });
370
383
  }
384
+ export const defaultCartFragment = `
385
+ fragment CartFragment on Cart {
386
+ id
387
+ checkoutUrl
388
+ totalQuantity
389
+ buyerIdentity {
390
+ countryCode
391
+ customer {
392
+ id
393
+ email
394
+ firstName
395
+ lastName
396
+ displayName
397
+ }
398
+ email
399
+ phone
400
+ }
401
+ lines(first: $numCartLines) {
402
+ edges {
403
+ node {
404
+ id
405
+ quantity
406
+ attributes {
407
+ key
408
+ value
409
+ }
410
+ cost {
411
+ totalAmount {
412
+ amount
413
+ currencyCode
414
+ }
415
+ compareAtAmountPerQuantity {
416
+ amount
417
+ currencyCode
418
+ }
419
+ }
420
+ merchandise {
421
+ ... on ProductVariant {
422
+ id
423
+ availableForSale
424
+ compareAtPriceV2 {
425
+ ...MoneyFragment
426
+ }
427
+ priceV2 {
428
+ ...MoneyFragment
429
+ }
430
+ requiresShipping
431
+ title
432
+ image {
433
+ ...ImageFragment
434
+ }
435
+ product {
436
+ handle
437
+ title
438
+ }
439
+ selectedOptions {
440
+ name
441
+ value
442
+ }
443
+ }
444
+ }
445
+ }
446
+ }
447
+ }
448
+ cost {
449
+ subtotalAmount {
450
+ ...MoneyFragment
451
+ }
452
+ totalAmount {
453
+ ...MoneyFragment
454
+ }
455
+ totalDutyAmount {
456
+ ...MoneyFragment
457
+ }
458
+ totalTaxAmount {
459
+ ...MoneyFragment
460
+ }
461
+ }
462
+ note
463
+ attributes {
464
+ key
465
+ value
466
+ }
467
+ discountCodes {
468
+ code
469
+ }
470
+ }
471
+
472
+ fragment MoneyFragment on MoneyV2 {
473
+ currencyCode
474
+ amount
475
+ }
476
+ fragment ImageFragment on Image {
477
+ id
478
+ url
479
+ altText
480
+ width
481
+ height
482
+ }
483
+ `;
@@ -121,6 +121,12 @@ export declare type CartCreateEvent = {
121
121
  type: 'CART_CREATE';
122
122
  payload: CartInput;
123
123
  };
124
+ export declare type CartSetEvent = {
125
+ type: 'CART_SET';
126
+ payload: {
127
+ cart: CartFragmentFragment;
128
+ };
129
+ };
124
130
  export declare type CartLineAddEvent = {
125
131
  type: 'CARTLINE_ADD';
126
132
  payload: {
@@ -163,7 +169,7 @@ export declare type DiscountCodesUpdateEvent = {
163
169
  discountCodes: string[];
164
170
  };
165
171
  };
166
- export declare type CartMachineActionEvent = CartFetchEvent | CartCreateEvent | CartLineAddEvent | CartLineRemoveEvent | CartLineUpdateEvent | NoteUpdateEvent | BuyerIdentityUpdateEvent | CartAttributesUpdateEvent | DiscountCodesUpdateEvent;
172
+ export declare type CartMachineActionEvent = CartFetchEvent | CartCreateEvent | CartSetEvent | CartLineAddEvent | CartLineRemoveEvent | CartLineUpdateEvent | NoteUpdateEvent | BuyerIdentityUpdateEvent | CartAttributesUpdateEvent | DiscountCodesUpdateEvent;
167
173
  export declare type CartMachineFetchResultEvent = {
168
174
  type: 'CART_COMPLETED';
169
175
  payload: {
@@ -2,7 +2,7 @@ import { StateMachine } from '@xstate/fsm';
2
2
  import { CartFragmentFragment } from './graphql/CartFragment.js';
3
3
  import { Cart, CartMachineActionEvent, CartMachineContext, CartMachineEvent, CartMachineFetchResultEvent, CartMachineTypeState } from './types.js';
4
4
  import { CountryCode } from '../../storefront-api-types.js';
5
- export declare function useCartAPIStateMachine({ numCartLines, onCartActionEntry, onCartActionOptimisticUI, onCartActionComplete, data, cartFragment, countryCode, }: {
5
+ export declare function useCartAPIStateMachine({ numCartLines, onCartActionEntry, onCartActionOptimisticUI, onCartActionComplete, data: cart, cartFragment, countryCode, }: {
6
6
  /** Maximum number of cart lines to fetch. Defaults to 250 cart lines. */
7
7
  numCartLines?: number;
8
8
  /** A callback that is invoked just before a Cart API action executes. */
@@ -11,12 +11,11 @@ export declare function useCartAPIStateMachine({ numCartLines, onCartActionEntry
11
11
  onCartActionOptimisticUI?: (context: CartMachineContext, event: CartMachineEvent) => Partial<CartMachineContext>;
12
12
  /** A callback that is invoked after a Cart API completes. */
13
13
  onCartActionComplete?: (context: CartMachineContext, event: CartMachineFetchResultEvent) => void;
14
- /** A callback that is invoked after a Cart API completes. */
15
14
  /** An object with fields that correspond to the Storefront API's [Cart object](https://shopify.dev/api/storefront/latest/objects/cart). */
16
15
  data?: CartFragmentFragment;
17
16
  /** A fragment used to query the Storefront API's [Cart object](https://shopify.dev/api/storefront/latest/objects/cart) for all queries and mutations. A default value is used if no argument is provided. */
18
- cartFragment?: string;
17
+ cartFragment: string;
19
18
  /** The ISO country code for i18n. */
20
19
  countryCode?: CountryCode;
21
- }): readonly [StateMachine.State<CartMachineContext, CartMachineEvent, CartMachineTypeState>, (event: "CART_FETCH" | "CART_CREATE" | "CARTLINE_ADD" | "CARTLINE_REMOVE" | "CARTLINE_UPDATE" | "NOTE_UPDATE" | "BUYER_IDENTITY_UPDATE" | "CART_ATTRIBUTES_UPDATE" | "DISCOUNT_CODES_UPDATE" | "CART_COMPLETED" | "RESOLVE" | "ERROR" | CartMachineEvent) => void, StateMachine.Service<CartMachineContext, CartMachineEvent, CartMachineTypeState>];
20
+ }): readonly [StateMachine.State<CartMachineContext, CartMachineEvent, CartMachineTypeState>, (event: "CART_FETCH" | "CART_CREATE" | "CART_SET" | "CARTLINE_ADD" | "CARTLINE_REMOVE" | "CARTLINE_UPDATE" | "NOTE_UPDATE" | "BUYER_IDENTITY_UPDATE" | "CART_ATTRIBUTES_UPDATE" | "DISCOUNT_CODES_UPDATE" | "CART_COMPLETED" | "RESOLVE" | "ERROR" | CartMachineEvent) => void, StateMachine.Service<CartMachineContext, CartMachineEvent, CartMachineTypeState>];
22
21
  export declare function cartFromGraphQL(cart: CartFragmentFragment): Cart;
@@ -53,6 +53,15 @@ const INITIALIZING_CART_EVENTS = {
53
53
  CART_CREATE: {
54
54
  target: 'cartCreating',
55
55
  },
56
+ CART_SET: {
57
+ target: 'idle',
58
+ actions: [
59
+ assign({
60
+ rawCartResult: (_, event) => event.payload.cart,
61
+ cart: (_, event) => cartFromGraphQL(event.payload.cart),
62
+ }),
63
+ ],
64
+ },
56
65
  };
57
66
  const UPDATING_CART_EVENTS = {
58
67
  CARTLINE_ADD: {
@@ -77,46 +86,52 @@ const UPDATING_CART_EVENTS = {
77
86
  target: 'discountCodesUpdating',
78
87
  },
79
88
  };
80
- const cartMachine = createMachine({
81
- id: 'Cart',
82
- initial: 'uninitialized',
83
- states: {
84
- uninitialized: {
85
- on: INITIALIZING_CART_EVENTS,
86
- },
87
- cartCompleted: {
88
- on: INITIALIZING_CART_EVENTS,
89
+ function createCartMachine(initialCart) {
90
+ return createMachine({
91
+ id: 'Cart',
92
+ initial: initialCart ? 'idle' : 'uninitialized',
93
+ context: {
94
+ cart: initialCart && cartFromGraphQL(initialCart),
89
95
  },
90
- initializationError: {
91
- on: INITIALIZING_CART_EVENTS,
92
- },
93
- idle: {
94
- on: UPDATING_CART_EVENTS,
95
- },
96
- error: {
97
- on: UPDATING_CART_EVENTS,
96
+ states: {
97
+ uninitialized: {
98
+ on: INITIALIZING_CART_EVENTS,
99
+ },
100
+ cartCompleted: {
101
+ on: INITIALIZING_CART_EVENTS,
102
+ },
103
+ initializationError: {
104
+ on: INITIALIZING_CART_EVENTS,
105
+ },
106
+ idle: {
107
+ on: { ...INITIALIZING_CART_EVENTS, ...UPDATING_CART_EVENTS },
108
+ },
109
+ error: {
110
+ on: { ...INITIALIZING_CART_EVENTS, ...UPDATING_CART_EVENTS },
111
+ },
112
+ cartFetching: invokeCart('cartFetchAction', {
113
+ errorTarget: 'initializationError',
114
+ }),
115
+ cartCreating: invokeCart('cartCreateAction', {
116
+ errorTarget: 'initializationError',
117
+ }),
118
+ cartLineRemoving: invokeCart('cartLineRemoveAction'),
119
+ cartLineUpdating: invokeCart('cartLineUpdateAction'),
120
+ cartLineAdding: invokeCart('cartLineAddAction'),
121
+ noteUpdating: invokeCart('noteUpdateAction'),
122
+ buyerIdentityUpdating: invokeCart('buyerIdentityUpdateAction'),
123
+ cartAttributesUpdating: invokeCart('cartAttributesUpdateAction'),
124
+ discountCodesUpdating: invokeCart('discountCodesUpdateAction'),
98
125
  },
99
- cartFetching: invokeCart('cartFetchAction', {
100
- errorTarget: 'initializationError',
101
- }),
102
- cartCreating: invokeCart('cartCreateAction', {
103
- errorTarget: 'initializationError',
104
- }),
105
- cartLineRemoving: invokeCart('cartLineRemoveAction'),
106
- cartLineUpdating: invokeCart('cartLineUpdateAction'),
107
- cartLineAdding: invokeCart('cartLineAddAction'),
108
- noteUpdating: invokeCart('noteUpdateAction'),
109
- buyerIdentityUpdating: invokeCart('buyerIdentityUpdateAction'),
110
- cartAttributesUpdating: invokeCart('cartAttributesUpdateAction'),
111
- discountCodesUpdating: invokeCart('discountCodesUpdateAction'),
112
- },
113
- });
126
+ });
127
+ }
114
128
  export function useCartAPIStateMachine({ numCartLines, onCartActionEntry, onCartActionOptimisticUI, onCartActionComplete, data: cart, cartFragment, countryCode, }) {
115
129
  const { cartFetch, cartCreate, cartLineAdd, cartLineUpdate, cartLineRemove, noteUpdate, buyerIdentityUpdate, cartAttributesUpdate, discountCodesUpdate, } = useCartActions({
116
130
  numCartLines,
117
131
  cartFragment,
118
132
  countryCode,
119
133
  });
134
+ const cartMachine = useMemo(() => createCartMachine(cart), [cart]);
120
135
  const [state, send, service] = useMachine(cartMachine, {
121
136
  actions: {
122
137
  cartFetchAction: async (_, event) => {
@@ -152,9 +152,12 @@ async function processRequest(handleRequest, App, url, request, sessionApi, opti
152
152
  const rsc = runRSC({ App, state, log, request, response });
153
153
  if (isRSCRequest) {
154
154
  const buffered = await bufferReadableStream(rsc.readable.getReader());
155
- postRequestTasks('rsc', 200, request, response);
156
- response.headers.set('cache-control', response.cacheControlHeader);
157
- cacheResponse(response, request, [buffered], revalidate);
155
+ const rscDidError = !!rsc.didError();
156
+ postRequestTasks('rsc', rscDidError ? 500 : 200, request, response, rscDidError);
157
+ if (rscDidError) {
158
+ response.headers.set('cache-control', response.cacheControlHeader);
159
+ cacheResponse(response, request, [buffered], revalidate);
160
+ }
158
161
  return new Response(buffered, {
159
162
  headers: response.headers,
160
163
  });
@@ -319,7 +322,7 @@ async function runSSR({ rsc, state, request, response, nodeResponse, nonce, dev,
319
322
  // Last SSR write might be pending, delay closing the writable one tick
320
323
  setTimeout(() => {
321
324
  writable.close();
322
- postRequestTasks('str', responseOptions.status, request, response);
325
+ postRequestTasks('str', responseOptions.status, request, response, !!didError());
323
326
  response.status = responseOptions.status;
324
327
  cacheResponse(response, request, savedChunks, revalidate);
325
328
  }, 0);
@@ -328,7 +331,7 @@ async function runSSR({ rsc, state, request, response, nodeResponse, nonce, dev,
328
331
  else {
329
332
  // Redirects do not write body
330
333
  writable.close();
331
- postRequestTasks('str', responseOptions.status, request, response);
334
+ postRequestTasks('str', responseOptions.status, request, response, !!didError());
332
335
  }
333
336
  if (response.canStream()) {
334
337
  return new Response(transform.readable, responseOptions);
@@ -376,7 +379,7 @@ async function runSSR({ rsc, state, request, response, nodeResponse, nonce, dev,
376
379
  log.trace('node complete ssr');
377
380
  if (!revalidate &&
378
381
  (response.canStream() || nodeResponse.writableEnded)) {
379
- postRequestTasks('str', nodeResponse.statusCode, request, response);
382
+ postRequestTasks('str', nodeResponse.statusCode, request, response, !!didError());
380
383
  return;
381
384
  }
382
385
  writeHeadToNodeResponse(nodeResponse, response, log, didError());
@@ -396,8 +399,8 @@ async function runSSR({ rsc, state, request, response, nodeResponse, nonce, dev,
396
399
  let html = template;
397
400
  if (!error) {
398
401
  html = assembleHtml({ ssrHtml, rscPayload, request, template });
399
- postRequestTasks('ssr', nodeResponse.statusCode, request, response);
400
402
  }
403
+ postRequestTasks('ssr', nodeResponse.statusCode, request, response, !!didError());
401
404
  if (!nodeResponse.writableEnded) {
402
405
  nodeResponse.write(html);
403
406
  nodeResponse.end();
@@ -506,8 +509,8 @@ function isRedirect(response) {
506
509
  function flightContainer(chunk) {
507
510
  return `<meta data-flight="${htmlEncode(chunk)}" />`;
508
511
  }
509
- function postRequestTasks(type, status, request, response) {
510
- logServerResponse(type, request, status);
512
+ function postRequestTasks(type, status, request, response, didError) {
513
+ logServerResponse(type, request, status, didError);
511
514
  logCacheControlHeaders(type, request, response);
512
515
  logQueryTimings(type, request);
513
516
  request.savePreloadQueries();
@@ -74,7 +74,9 @@ function getCookieDomain(cookieDomain) {
74
74
  function trackPageView(payload) {
75
75
  microSessionCount += 1;
76
76
  try {
77
- sendToServer(storefrontPageViewSchema(payload));
77
+ payload &&
78
+ payload.shopify &&
79
+ sendToServer(storefrontPageViewSchema(payload));
78
80
  }
79
81
  catch (error) {
80
82
  console.error(`Error Shopify analytics: ${ClientAnalytics.eventNames.PAGE_VIEW}`, error);
@@ -94,7 +96,7 @@ function buildStorefrontPageViewPayload(payload) {
94
96
  const shopify = payload.shopify;
95
97
  let formattedData = {
96
98
  appClientId: '6167201',
97
- hydrogenSubchannelId: shopify.storefrontId,
99
+ hydrogenSubchannelId: shopify.storefrontId || '0',
98
100
  isPersistentCookie: shopify.isPersistentCookie,
99
101
  uniqToken: shopify.userId,
100
102
  visitToken: shopify.sessionId,
@@ -10,7 +10,7 @@ import { CacheLong } from '../../../Cache/strategies/index.js';
10
10
  import { gql } from '../../../../utilities/graphql-tag.js';
11
11
  import { SHOPIFY_Y, SHOPIFY_S } from '../../../../constants.js';
12
12
  export function ShopifyAnalytics({ cookieDomain }) {
13
- const { storeDomain } = useShop();
13
+ const { storeDomain, storefrontId } = useShop();
14
14
  const request = useServerRequest();
15
15
  const cookies = parse(request.headers.get('Cookie') || '');
16
16
  const domain = cookieDomain || storeDomain;
@@ -23,7 +23,7 @@ export function ShopifyAnalytics({ cookieDomain }) {
23
23
  shopify: {
24
24
  shopId: id,
25
25
  currency: currencyCode,
26
- storefrontId: globalThis.Oxygen?.env?.SHOPIFY_STOREFRONT_ID || '0',
26
+ storefrontId,
27
27
  acceptedLanguage: request.headers.get('Accept-Language')?.replace(/-.*/, '') || 'en',
28
28
  isPersistentCookie: !!cookies[SHOPIFY_S] || !!cookies[SHOPIFY_Y],
29
29
  },
@@ -16,7 +16,8 @@ export const CLIENT_CONTEXT_ALLOW_LIST = [
16
16
  function makeShopifyContext(shopifyConfig) {
17
17
  const countryCode = shopifyConfig.defaultCountryCode ?? DEFAULT_COUNTRY;
18
18
  const languageCode = shopifyConfig.defaultLanguageCode ?? DEFAULT_LANGUAGE;
19
- const storefrontId = getOxygenVariable(SHOPIFY_STOREFRONT_ID_VARIABLE);
19
+ const storefrontId = shopifyConfig.storefrontId ??
20
+ getOxygenVariable(SHOPIFY_STOREFRONT_ID_VARIABLE);
20
21
  const shopifyProviderServerValue = {
21
22
  defaultCountryCode: countryCode.toUpperCase(),
22
23
  defaultLanguageCode: languageCode.toUpperCase(),
@@ -40,5 +40,5 @@ export { CartQuery } from './components/CartProvider/cart-queries.js';
40
40
  export { fetchSync } from './foundation/fetchSync/server/fetchSync.js';
41
41
  export { type HydrogenRequest } from './foundation/HydrogenRequest/HydrogenRequest.server.js';
42
42
  export { type HydrogenResponse } from './foundation/HydrogenResponse/HydrogenResponse.server.js';
43
- export { type HydrogenRouteProps } from './types.js';
43
+ export { type HydrogenRouteProps, type CachingStrategy } from './types.js';
44
44
  export { type ResourceGetter as HydrogenApiRoute, RequestOptions as HydrogenApiRouteOptions, } from './utilities/apiRoutes.js';
@@ -161,7 +161,7 @@ export async function renderApiRoute(request, route, hydrogenConfig, { session,
161
161
  });
162
162
  }
163
163
  if (!suppressLog) {
164
- logServerResponse('api', request, response.status ?? 200);
164
+ logServerResponse('api', request, response.status ?? 200, false);
165
165
  }
166
166
  if (response instanceof Request) {
167
167
  const url = new URL(request.url);
@@ -24,5 +24,5 @@ export declare type RenderType = 'str' | 'rsc' | 'ssr' | 'api';
24
24
  export declare function getLoggerWithContext(context: Partial<HydrogenRequest>): Logger;
25
25
  export declare const log: Logger;
26
26
  export declare function setLogger(config?: LoggerConfig): void;
27
- export declare function logServerResponse(type: RenderType, request: HydrogenRequest, responseStatus: number): void;
27
+ export declare function logServerResponse(type: RenderType, request: HydrogenRequest, responseStatus: number, didError: boolean): void;
28
28
  export {};
@@ -72,7 +72,7 @@ const SERVER_RESPONSE_MAP = {
72
72
  rsc: 'Server Components',
73
73
  ssr: 'buffered SSR',
74
74
  };
75
- export function logServerResponse(type, request, responseStatus) {
75
+ export function logServerResponse(type, request, responseStatus, didError) {
76
76
  const log = getLoggerWithContext(request);
77
77
  const coloredResponseStatus = responseStatus >= 500
78
78
  ? red(responseStatus)
@@ -85,5 +85,5 @@ export function logServerResponse(type, request, responseStatus) {
85
85
  const styledType = italic(fullType.padEnd(17));
86
86
  const paddedTiming = ((getTime() - request.time).toFixed(2) + ' ms').padEnd(10);
87
87
  const url = parseUrl(type, request.url);
88
- log.debug(`${request.method} ${styledType} ${coloredResponseStatus} ${paddedTiming} ${url}`);
88
+ log.debug(`${request.method} ${styledType} ${coloredResponseStatus} ${didError || responseStatus >= 400 ? red('error') : green('ok ')} ${paddedTiming} ${url}`);
89
89
  }
@@ -13,7 +13,7 @@ export function getStorefrontApiRequestHeaders({ buyerIp, publicStorefrontToken,
13
13
  log.error('No secret Shopify storefront API token was defined. This means your app will be rate limited!\nSee how to add the token: ');
14
14
  }
15
15
  else if (privateStorefrontToken) {
16
- log.warn('The private shopify storefront API token was loaded implicitly by an environment variable. This is deprecated, and instead the variable should be defined directly in the Hydrogen Config.\nFor more information: ');
16
+ log.warn('The private shopify storefront API token was loaded implicitly by an environment variable. This is deprecated, and instead the variable should be defined directly in the Hydrogen Config.\nFor more information: https://shopify.dev/custom-storefronts/hydrogen/framework/hydrogen-config');
17
17
  }
18
18
  }
19
19
  }
@@ -21,11 +21,8 @@ export function getStorefrontApiRequestHeaders({ buyerIp, publicStorefrontToken,
21
21
  storefrontId = getOxygenVariable(SHOPIFY_STOREFRONT_ID_VARIABLE);
22
22
  if (!storefrontIdWarned) {
23
23
  storefrontIdWarned = true;
24
- if (!storefrontId && !__HYDROGEN_DEV__) {
25
- log.warn('No storefrontId was defined. This means the analytics on your admin dashboard will be broken!\nSee how to fix it: ');
26
- }
27
- else if (storefrontId) {
28
- log.warn('The storefrontId was loaded implicitly by an environment variable. This is deprecated, and instead the variable should be defined directly in the Hydrogen Config.\nFor more information: ');
24
+ if (storefrontId) {
25
+ log.warn('The storefrontId was loaded implicitly by an environment variable. This is deprecated, and instead the variable should be defined directly in the Hydrogen Config.\nFor more information: https://shopify.dev/custom-storefronts/hydrogen/framework/hydrogen-config');
29
26
  }
30
27
  }
31
28
  }
@@ -1 +1 @@
1
- export declare const LIB_VERSION = "1.4.1";
1
+ export declare const LIB_VERSION = "1.4.3";
@@ -1 +1 @@
1
- export const LIB_VERSION = '1.4.1';
1
+ export const LIB_VERSION = '1.4.3';
package/package.json CHANGED
@@ -7,7 +7,7 @@
7
7
  "engines": {
8
8
  "node": ">=14"
9
9
  },
10
- "version": "1.4.1",
10
+ "version": "1.4.3",
11
11
  "description": "Modern custom Shopify storefronts",
12
12
  "license": "MIT",
13
13
  "main": "dist/esnext/index.js",