@shopify/hydrogen 1.5.0 → 1.6.1

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 (29) hide show
  1. package/dist/esnext/components/CartProvider/CartProvider.client.d.ts +20 -11
  2. package/dist/esnext/components/CartProvider/CartProvider.client.js +457 -477
  3. package/dist/esnext/components/CartProvider/cart-queries.d.ts +1 -1
  4. package/dist/esnext/components/CartProvider/cart-queries.js +4 -1
  5. package/dist/esnext/components/Seo/Seo.client.d.ts +1 -1
  6. package/dist/esnext/entry-server.js +12 -2
  7. package/dist/esnext/experimental.d.ts +0 -1
  8. package/dist/esnext/experimental.js +0 -1
  9. package/dist/esnext/foundation/Analytics/connectors/Shopify/ShopifyAnalytics.client.js +21 -14
  10. package/dist/esnext/foundation/Analytics/connectors/Shopify/ShopifyAnalytics.server.js +15 -9
  11. package/dist/esnext/foundation/Analytics/connectors/Shopify/const.d.ts +5 -0
  12. package/dist/esnext/foundation/Analytics/connectors/Shopify/const.js +5 -0
  13. package/dist/esnext/foundation/Analytics/connectors/Shopify/customer-events.client.d.ts +2 -0
  14. package/dist/esnext/foundation/Analytics/connectors/Shopify/customer-events.client.js +182 -0
  15. package/dist/esnext/foundation/Analytics/connectors/Shopify/utils.d.ts +3 -0
  16. package/dist/esnext/foundation/Analytics/connectors/Shopify/utils.js +69 -0
  17. package/dist/esnext/foundation/HydrogenRequest/HydrogenRequest.server.d.ts +1 -0
  18. package/dist/esnext/foundation/HydrogenRequest/HydrogenRequest.server.js +2 -8
  19. package/dist/esnext/foundation/Route/Route.server.d.ts +3 -1
  20. package/dist/esnext/foundation/Route/Route.server.js +2 -2
  21. package/dist/esnext/hooks/useShopQuery/hooks.js +10 -6
  22. package/dist/esnext/utilities/random.d.ts +1 -0
  23. package/dist/esnext/utilities/random.js +11 -0
  24. package/dist/esnext/utilities/tests/price.js +0 -1
  25. package/dist/esnext/version.d.ts +1 -1
  26. package/dist/esnext/version.js +1 -1
  27. package/package.json +2 -1
  28. package/dist/esnext/components/CartProvider/CartProviderV2.client.d.ts +0 -50
  29. package/dist/esnext/components/CartProvider/CartProviderV2.client.js +0 -513
@@ -1,118 +1,76 @@
1
- import React, { useEffect, useCallback, useReducer, useMemo, useRef, } from 'react';
2
- import { flattenConnection } from '../../utilities/flattenConnection/index.js';
3
- import { CartLineAdd, CartCreate, CartLineRemove, CartLineUpdate, CartNoteUpdate, CartBuyerIdentityUpdate, CartAttributesUpdate, CartDiscountCodesUpdate, CartQuery, defaultCartFragment, } from './cart-queries.js';
1
+ import React, { useCallback, useEffect, useMemo, useRef, useState, useTransition, } from 'react';
4
2
  import { CountryCode, } from '../../storefront-api-types.js';
5
- import { useCartFetch } from './hooks.client.js';
6
3
  import { CartContext } from './context.js';
4
+ import { useCartAPIStateMachine } from './useCartAPIStateMachine.client.js';
7
5
  import { CART_ID_STORAGE_KEY } from './constants.js';
8
6
  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();
7
+ export function CartProvider({ 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, }) {
8
+ if (countryCode)
9
+ countryCode = countryCode.toUpperCase();
10
+ const [prevCountryCode, setPrevCountryCode] = useState(countryCode);
11
+ const [prevCustomerAccessToken, setPrevCustomerAccessToken] = useState(customerAccessToken);
12
+ const customerOverridesCountryCode = useRef(false);
13
+ if (prevCountryCode !== countryCode ||
14
+ prevCustomerAccessToken !== customerAccessToken) {
15
+ setPrevCountryCode(countryCode);
16
+ setPrevCustomerAccessToken(customerAccessToken);
17
+ customerOverridesCountryCode.current = false;
29
18
  }
30
- })();
31
- function cartReducer(state, action) {
32
- switch (action.type) {
33
- case 'cartFetch': {
34
- if (state.status === 'uninitialized') {
35
- return {
36
- status: 'fetching',
37
- };
38
- }
39
- break;
40
- }
41
- case 'cartCreate': {
42
- if (state.status === 'uninitialized') {
43
- return {
44
- status: 'creating',
45
- };
46
- }
47
- break;
48
- }
49
- case 'resolve': {
50
- const resolvableStatuses = ['updating', 'fetching', 'creating'];
51
- if (resolvableStatuses.includes(state.status)) {
52
- return {
53
- status: 'idle',
54
- cart: action.cart,
55
- };
56
- }
57
- break;
58
- }
59
- case 'reject': {
60
- if (action.errors) {
61
- console.group('%cCart Error:', 'color:red');
62
- for (const [i, error] of action.errors.entries()) {
63
- console.log(`%c${i + 1}. ` + error.message, 'color:red');
64
- }
65
- console.groupEnd();
66
- }
67
- if (state.status === 'fetching' || state.status === 'creating') {
68
- return { status: 'uninitialized', error: action.errors };
69
- }
70
- else if (state.status === 'updating') {
71
- return {
72
- status: 'idle',
73
- cart: state.lastValidCart,
74
- error: action.errors,
75
- };
19
+ const onCartActionEntry = useCallback((context, event) => {
20
+ try {
21
+ switch (event.type) {
22
+ case 'CART_CREATE':
23
+ return onCreate?.();
24
+ case 'CARTLINE_ADD':
25
+ return onLineAdd?.();
26
+ case 'CARTLINE_REMOVE':
27
+ return onLineRemove?.();
28
+ case 'CARTLINE_UPDATE':
29
+ return onLineUpdate?.();
30
+ case 'NOTE_UPDATE':
31
+ return onNoteUpdate?.();
32
+ case 'BUYER_IDENTITY_UPDATE':
33
+ return onBuyerIdentityUpdate?.();
34
+ case 'CART_ATTRIBUTES_UPDATE':
35
+ return onAttributesUpdate?.();
36
+ case 'DISCOUNT_CODES_UPDATE':
37
+ return onDiscountCodesUpdate?.();
76
38
  }
77
- break;
78
39
  }
79
- case 'resetCart': {
80
- if (state.status === 'fetching') {
81
- return { status: 'uninitialized' };
82
- }
83
- break;
40
+ catch (error) {
41
+ console.error('Cart entry action failed', error);
84
42
  }
85
- case 'addLineItem': {
86
- if (state.status === 'idle') {
87
- return {
88
- status: 'updating',
89
- cart: state.cart,
90
- lastValidCart: state.cart,
91
- };
92
- }
93
- break;
94
- }
95
- case 'removeLineItem': {
96
- if (state.status === 'idle') {
43
+ }, [
44
+ onAttributesUpdate,
45
+ onBuyerIdentityUpdate,
46
+ onCreate,
47
+ onDiscountCodesUpdate,
48
+ onLineAdd,
49
+ onLineRemove,
50
+ onLineUpdate,
51
+ onNoteUpdate,
52
+ ]);
53
+ const onCartActionOptimisticUI = useCallback((context, event) => {
54
+ if (!context?.cart)
55
+ return { cart: undefined };
56
+ switch (event.type) {
57
+ case 'CARTLINE_REMOVE':
97
58
  return {
98
- status: 'updating',
59
+ ...context,
60
+ lastValidCart: context.cart,
99
61
  cart: {
100
- ...state.cart,
101
- lines: state.cart.lines.filter(({ id }) => !action.lines.includes(id)),
62
+ ...context.cart,
63
+ lines: context?.cart?.lines.filter(({ id }) => !event.payload.lines.includes(id)),
102
64
  },
103
- lastValidCart: state.cart,
104
65
  };
105
- }
106
- break;
107
- }
108
- case 'updateLineItem': {
109
- if (state.status === 'idle') {
66
+ case 'CARTLINE_UPDATE':
110
67
  return {
111
- status: 'updating',
68
+ ...context,
69
+ lastValidCart: context.cart,
112
70
  cart: {
113
- ...state.cart,
114
- lines: state.cart.lines.map((line) => {
115
- const updatedLine = action.lines.find(({ id }) => id === line.id);
71
+ ...context.cart,
72
+ lines: context.cart.lines.map((line) => {
73
+ const updatedLine = event.payload.lines.find(({ id }) => id === line.id);
116
74
  if (updatedLine && updatedLine.quantity) {
117
75
  return {
118
76
  ...line,
@@ -122,433 +80,455 @@ function cartReducer(state, action) {
122
80
  return line;
123
81
  }),
124
82
  },
125
- lastValidCart: state.cart,
126
- };
127
- }
128
- break;
129
- }
130
- case 'noteUpdate': {
131
- if (state.status === 'idle') {
132
- return {
133
- status: 'updating',
134
- cart: state.cart,
135
- lastValidCart: state.cart,
136
83
  };
137
- }
138
- break;
139
84
  }
140
- case 'buyerIdentityUpdate': {
141
- if (state.status === 'idle') {
142
- return {
143
- status: 'updating',
144
- cart: state.cart,
145
- lastValidCart: state.cart,
146
- };
85
+ return { cart: context.cart ? { ...context.cart } : undefined };
86
+ }, []);
87
+ const onCartActionComplete = useCallback((context, event) => {
88
+ const cartActionEvent = event.payload.cartActionEvent;
89
+ try {
90
+ switch (event.type) {
91
+ case 'RESOLVE':
92
+ switch (cartActionEvent.type) {
93
+ case 'CART_CREATE':
94
+ publishCreateAnalytics(context, cartActionEvent);
95
+ return onCreateComplete?.();
96
+ case 'CARTLINE_ADD':
97
+ publishLineAddAnalytics(context, cartActionEvent);
98
+ return onLineAddComplete?.();
99
+ case 'CARTLINE_REMOVE':
100
+ publishLineRemoveAnalytics(context, cartActionEvent);
101
+ return onLineRemoveComplete?.();
102
+ case 'CARTLINE_UPDATE':
103
+ publishLineUpdateAnalytics(context, cartActionEvent);
104
+ return onLineUpdateComplete?.();
105
+ case 'NOTE_UPDATE':
106
+ return onNoteUpdateComplete?.();
107
+ case 'BUYER_IDENTITY_UPDATE':
108
+ if (countryCodeNotUpdated(context, cartActionEvent)) {
109
+ customerOverridesCountryCode.current = true;
110
+ }
111
+ return onBuyerIdentityUpdateComplete?.();
112
+ case 'CART_ATTRIBUTES_UPDATE':
113
+ return onAttributesUpdateComplete?.();
114
+ case 'DISCOUNT_CODES_UPDATE':
115
+ publishDiscountCodesUpdateAnalytics(context, cartActionEvent);
116
+ return onDiscountCodesUpdateComplete?.();
117
+ }
147
118
  }
148
- break;
149
119
  }
150
- case 'cartAttributesUpdate': {
151
- if (state.status === 'idle') {
152
- return {
153
- status: 'updating',
154
- cart: state.cart,
155
- lastValidCart: state.cart,
156
- };
157
- }
158
- break;
120
+ catch (error) {
121
+ console.error('onCartActionComplete failed', error);
159
122
  }
160
- case 'discountCodesUpdate': {
161
- if (state.status === 'idle') {
162
- return {
163
- status: 'updating',
164
- cart: state.cart,
165
- lastValidCart: state.cart,
166
- };
123
+ }, [
124
+ onAttributesUpdateComplete,
125
+ onBuyerIdentityUpdateComplete,
126
+ onCreateComplete,
127
+ onDiscountCodesUpdateComplete,
128
+ onLineAddComplete,
129
+ onLineRemoveComplete,
130
+ onLineUpdateComplete,
131
+ onNoteUpdateComplete,
132
+ ]);
133
+ const [cartState, cartSend] = useCartAPIStateMachine({
134
+ numCartLines,
135
+ data: cart,
136
+ cartFragment,
137
+ countryCode,
138
+ onCartActionEntry,
139
+ onCartActionOptimisticUI,
140
+ onCartActionComplete,
141
+ });
142
+ const cartReady = useRef(false);
143
+ const cartCompleted = cartState.matches('cartCompleted');
144
+ const countryChanged = (cartState.value === 'idle' ||
145
+ cartState.value === 'error' ||
146
+ cartState.value === 'cartCompleted') &&
147
+ countryCode !== cartState?.context?.cart?.buyerIdentity?.countryCode &&
148
+ !cartState.context.errors;
149
+ const fetchingFromStorage = useRef(false);
150
+ /**
151
+ * Initializes cart with priority in this order:
152
+ * 1. cart props
153
+ * 2. localStorage cartId
154
+ */
155
+ useEffect(() => {
156
+ if (!cartReady.current && !fetchingFromStorage.current) {
157
+ if (!cart && storageAvailable('localStorage')) {
158
+ fetchingFromStorage.current = true;
159
+ try {
160
+ const cartId = window.localStorage.getItem(CART_ID_STORAGE_KEY);
161
+ if (cartId) {
162
+ cartSend({ type: 'CART_FETCH', payload: { cartId } });
163
+ }
164
+ }
165
+ catch (error) {
166
+ console.warn('error fetching cartId');
167
+ console.warn(error);
168
+ }
167
169
  }
168
- break;
170
+ cartReady.current = true;
169
171
  }
170
- }
171
- throw new Error(`Cannot dispatch event (${action.type}) for current cart state (${state.status})`);
172
- }
173
- /**
174
- * The `CartProvider` component creates a context for using a cart. It creates a cart object and callbacks
175
- * that can be accessed by any descendent component using the `useCart` hook and related hooks. It also carries out
176
- * any callback props when a relevant action is performed. For example, if a `onLineAdd` callback is provided,
177
- * then the callback will be called when a new line item is successfully added to the cart.
178
- *
179
- * The `CartProvider` component must be a descendent of the `ShopifyProvider` component.
180
- * You must use this component if you want to use the `useCart` hook or related hooks, or if you would like to use the `AddToCartButton` component.
181
- */
182
- export function CartProvider({ children, numCartLines, onCreate, onLineAdd, onLineRemove, onLineUpdate, onNoteUpdate, onBuyerIdentityUpdate, onAttributesUpdate, onDiscountCodesUpdate, data: cart, cartFragment = defaultCartFragment, customerAccessToken, countryCode = CountryCode.Us, }) {
183
- if (countryCode)
184
- countryCode = countryCode.toUpperCase();
185
- const initialStatus = cart
186
- ? { status: 'idle', cart: cartFromGraphQL(cart) }
187
- : { status: 'uninitialized' };
188
- const [state, dispatch] = useReducer((state, dispatch) => cartReducer(state, dispatch), initialStatus);
189
- const fetchCart = useCartFetch();
190
- const countryChanged = state.status === 'idle' &&
191
- countryCode !== state?.cart?.buyerIdentity?.countryCode &&
192
- !state.error;
193
- const cartFetch = useCallback(async (cartId) => {
194
- dispatch({ type: 'cartFetch' });
195
- const { data } = await fetchCart({
196
- query: CartQuery(cartFragment),
197
- variables: {
198
- id: cartId,
199
- numCartLines,
200
- country: countryCode,
201
- },
202
- });
203
- if (!data?.cart) {
204
- localStorage.removeItem(CART_ID_STORAGE_KEY);
205
- dispatch({ type: 'resetCart' });
172
+ }, [cart, cartReady, cartSend]);
173
+ // Update cart country code if cart and props countryCode's as different
174
+ useEffect(() => {
175
+ if (!countryChanged || customerOverridesCountryCode.current)
206
176
  return;
207
- }
208
- dispatch({ type: 'resolve', cart: cartFromGraphQL(data.cart) });
209
- }, [fetchCart, cartFragment, numCartLines, countryCode]);
210
- const cartCreate = useCallback(async (cart) => {
211
- dispatch({ type: 'cartCreate' });
212
- onCreate?.();
213
- if (countryCode && !cart.buyerIdentity?.countryCode) {
214
- if (cart.buyerIdentity == null) {
215
- cart.buyerIdentity = {};
216
- }
217
- cart.buyerIdentity.countryCode = countryCode;
218
- }
219
- if (customerAccessToken && !cart.buyerIdentity?.customerAccessToken) {
220
- if (cart.buyerIdentity == null) {
221
- cart.buyerIdentity = {};
222
- }
223
- cart.buyerIdentity.customerAccessToken = customerAccessToken;
224
- }
225
- const { data, errors } = await fetchCart({
226
- query: CartCreate(cartFragment),
227
- variables: {
228
- input: cart,
229
- numCartLines,
230
- country: countryCode,
231
- },
177
+ cartSend({
178
+ type: 'BUYER_IDENTITY_UPDATE',
179
+ payload: { buyerIdentity: { countryCode, customerAccessToken } },
232
180
  });
233
- if (errors) {
234
- dispatch({
235
- type: 'reject',
236
- errors,
237
- });
238
- }
239
- if (data?.cartCreate?.cart) {
240
- if (cart.lines) {
241
- ClientAnalytics.publish(ClientAnalytics.eventNames.ADD_TO_CART, true, {
242
- addedCartLines: cart.lines,
243
- cart: data.cartCreate.cart,
244
- prevCart: null,
245
- });
246
- }
247
- dispatch({
248
- type: 'resolve',
249
- cart: cartFromGraphQL(data.cartCreate.cart),
250
- });
251
- localStorage.setItem(CART_ID_STORAGE_KEY, data.cartCreate.cart.id);
252
- }
253
181
  }, [
254
- onCreate,
255
182
  countryCode,
256
- fetchCart,
257
- cartFragment,
258
- numCartLines,
259
183
  customerAccessToken,
184
+ countryChanged,
185
+ customerOverridesCountryCode,
186
+ cartSend,
260
187
  ]);
261
- const addLineItem = useCallback(async (lines, state) => {
262
- if (state.status === 'idle') {
263
- dispatch({ type: 'addLineItem' });
264
- onLineAdd?.();
265
- const { data, errors } = await fetchCart({
266
- query: CartLineAdd(cartFragment),
267
- variables: {
268
- cartId: state.cart.id,
269
- lines,
270
- numCartLines,
271
- country: countryCode,
272
- },
273
- });
274
- if (errors) {
275
- dispatch({
276
- type: 'reject',
277
- errors,
278
- });
279
- }
280
- if (data?.cartLinesAdd?.cart) {
281
- ClientAnalytics.publish(ClientAnalytics.eventNames.ADD_TO_CART, true, {
282
- addedCartLines: lines,
283
- cart: data.cartLinesAdd.cart,
284
- prevCart: state.cart,
285
- });
286
- dispatch({
287
- type: 'resolve',
288
- cart: cartFromGraphQL(data.cartLinesAdd.cart),
289
- });
290
- }
188
+ // send cart events when ready
189
+ const onCartReadySend = useCallback((cartEvent) => {
190
+ if (!cartReady.current) {
191
+ return console.warn("Cart isn't ready yet");
291
192
  }
292
- }, [onLineAdd, fetchCart, cartFragment, numCartLines, countryCode]);
293
- const removeLineItem = useCallback(async (lines, state) => {
294
- if (state.status === 'idle') {
295
- dispatch({ type: 'removeLineItem', lines });
296
- onLineRemove?.();
297
- const { data, errors } = await fetchCart({
298
- query: CartLineRemove(cartFragment),
299
- variables: {
300
- cartId: state.cart.id,
301
- lines,
302
- numCartLines,
303
- country: countryCode,
304
- },
305
- });
306
- if (errors) {
307
- dispatch({
308
- type: 'reject',
309
- errors,
310
- });
311
- }
312
- if (data?.cartLinesRemove?.cart) {
313
- ClientAnalytics.publish(ClientAnalytics.eventNames.REMOVE_FROM_CART, true, {
314
- removedCartLines: lines,
315
- cart: data.cartLinesRemove.cart,
316
- prevCart: state.cart,
317
- });
318
- dispatch({
319
- type: 'resolve',
320
- cart: cartFromGraphQL(data.cartLinesRemove.cart),
321
- });
322
- }
323
- }
324
- }, [onLineRemove, fetchCart, cartFragment, numCartLines, countryCode]);
325
- const updateLineItem = useCallback(async (lines, state) => {
326
- if (state.status === 'idle') {
327
- dispatch({ type: 'updateLineItem', lines });
328
- onLineUpdate?.();
329
- const { data, errors } = await fetchCart({
330
- query: CartLineUpdate(cartFragment),
331
- variables: {
332
- cartId: state.cart.id,
333
- lines,
334
- numCartLines,
335
- country: countryCode,
336
- },
337
- });
338
- if (errors) {
339
- dispatch({
340
- type: 'reject',
341
- errors,
342
- });
343
- }
344
- if (data?.cartLinesUpdate?.cart) {
345
- ClientAnalytics.publish(ClientAnalytics.eventNames.UPDATE_CART, true, {
346
- updatedCartLines: lines,
347
- oldCart: state.cart,
348
- cart: data.cartLinesUpdate.cart,
349
- prevCart: state.cart,
350
- });
351
- dispatch({
352
- type: 'resolve',
353
- cart: cartFromGraphQL(data.cartLinesUpdate.cart),
354
- });
355
- }
356
- }
357
- }, [onLineUpdate, fetchCart, cartFragment, numCartLines, countryCode]);
358
- const noteUpdate = useCallback(async (note, state) => {
359
- if (state.status === 'idle') {
360
- dispatch({ type: 'noteUpdate' });
361
- onNoteUpdate?.();
362
- const { data, errors } = await fetchCart({
363
- query: CartNoteUpdate(cartFragment),
364
- variables: {
365
- cartId: state.cart.id,
366
- note,
367
- numCartLines,
368
- country: countryCode,
369
- },
370
- });
371
- if (errors) {
372
- dispatch({
373
- type: 'reject',
374
- errors,
375
- });
193
+ cartSend(cartEvent);
194
+ }, [cartSend]);
195
+ // save cart id to local storage
196
+ useEffect(() => {
197
+ if (cartState?.context?.cart?.id && storageAvailable('localStorage')) {
198
+ try {
199
+ window.localStorage.setItem(CART_ID_STORAGE_KEY, cartState.context.cart?.id);
376
200
  }
377
- if (data?.cartNoteUpdate?.cart) {
378
- dispatch({
379
- type: 'resolve',
380
- cart: cartFromGraphQL(data.cartNoteUpdate.cart),
381
- });
201
+ catch (error) {
202
+ console.warn('Failed to save cartId to localStorage', error);
382
203
  }
383
204
  }
384
- }, [onNoteUpdate, fetchCart, cartFragment, numCartLines, countryCode]);
385
- const buyerIdentityUpdate = useCallback(async (buyerIdentity, state) => {
386
- if (state.status === 'idle') {
387
- dispatch({ type: 'buyerIdentityUpdate' });
388
- onBuyerIdentityUpdate?.();
389
- const { data, errors } = await fetchCart({
390
- query: CartBuyerIdentityUpdate(cartFragment),
391
- variables: {
392
- cartId: state.cart.id,
393
- buyerIdentity,
394
- numCartLines,
395
- country: countryCode,
396
- },
397
- });
398
- if (errors) {
399
- dispatch({
400
- type: 'reject',
401
- errors,
402
- });
205
+ }, [cartState?.context?.cart?.id]);
206
+ // delete cart from local storage if cart fetched has been completed
207
+ useEffect(() => {
208
+ if (cartCompleted && storageAvailable('localStorage')) {
209
+ try {
210
+ window.localStorage.removeItem(CART_ID_STORAGE_KEY);
403
211
  }
404
- if (data?.cartBuyerIdentityUpdate?.cart) {
405
- dispatch({
406
- type: 'resolve',
407
- cart: cartFromGraphQL(data.cartBuyerIdentityUpdate.cart),
408
- });
212
+ catch (error) {
213
+ console.warn('Failed to delete cartId from localStorage', error);
409
214
  }
410
215
  }
411
- }, [onBuyerIdentityUpdate, fetchCart, cartFragment, numCartLines, countryCode]);
412
- const cartAttributesUpdate = useCallback(async (attributes, state) => {
413
- if (state.status === 'idle') {
414
- dispatch({ type: 'cartAttributesUpdate' });
415
- onAttributesUpdate?.();
416
- const { data, errors } = await fetchCart({
417
- query: CartAttributesUpdate(cartFragment),
418
- variables: {
419
- cartId: state.cart.id,
420
- attributes,
421
- numCartLines,
422
- country: countryCode,
423
- },
424
- });
425
- if (errors) {
426
- dispatch({
427
- type: 'reject',
428
- errors,
429
- });
430
- }
431
- if (data?.cartAttributesUpdate?.cart) {
432
- dispatch({
433
- type: 'resolve',
434
- cart: cartFromGraphQL(data.cartAttributesUpdate.cart),
435
- });
216
+ }, [cartCompleted]);
217
+ const cartCreate = useCallback((cartInput) => {
218
+ if (countryCode && !cartInput.buyerIdentity?.countryCode) {
219
+ if (cartInput.buyerIdentity == null) {
220
+ cartInput.buyerIdentity = {};
436
221
  }
222
+ cartInput.buyerIdentity.countryCode = countryCode;
437
223
  }
438
- }, [onAttributesUpdate, fetchCart, cartFragment, numCartLines, countryCode]);
439
- const discountCodesUpdate = useCallback(async (discountCodes, state) => {
440
- if (state.status === 'idle') {
441
- dispatch({ type: 'discountCodesUpdate' });
442
- onDiscountCodesUpdate?.();
443
- const { data, errors } = await fetchCart({
444
- query: CartDiscountCodesUpdate(cartFragment),
445
- variables: {
446
- cartId: state.cart.id,
447
- discountCodes,
448
- numCartLines,
449
- country: countryCode,
450
- },
451
- });
452
- if (errors) {
453
- dispatch({
454
- type: 'reject',
455
- errors,
456
- });
457
- }
458
- if (data?.cartDiscountCodesUpdate?.cart) {
459
- ClientAnalytics.publish(ClientAnalytics.eventNames.DISCOUNT_CODE_UPDATED, true, {
460
- updatedDiscountCodes: discountCodes,
461
- cart: data.cartDiscountCodesUpdate.cart,
462
- prevCart: state.cart,
463
- });
464
- dispatch({
465
- type: 'resolve',
466
- cart: cartFromGraphQL(data.cartDiscountCodesUpdate.cart),
467
- });
224
+ if (customerAccessToken &&
225
+ !cartInput.buyerIdentity?.customerAccessToken) {
226
+ if (cartInput.buyerIdentity == null) {
227
+ cartInput.buyerIdentity = {};
468
228
  }
229
+ cartInput.buyerIdentity.customerAccessToken = customerAccessToken;
469
230
  }
470
- }, [onDiscountCodesUpdate, fetchCart, cartFragment, numCartLines, countryCode]);
471
- const didFetchCart = useRef(false);
472
- useEffect(() => {
473
- if (localStorage.getItem(CART_ID_STORAGE_KEY) &&
474
- state.status === 'uninitialized' &&
475
- !didFetchCart.current) {
476
- didFetchCart.current = true;
477
- cartFetch(localStorage.getItem(CART_ID_STORAGE_KEY));
478
- }
479
- }, [cartFetch, state]);
480
- useEffect(() => {
481
- if (!countryChanged)
482
- return;
483
- buyerIdentityUpdate({ countryCode, customerAccessToken }, state);
484
- }, [
485
- state,
486
- buyerIdentityUpdate,
487
- countryCode,
488
- customerAccessToken,
489
- countryChanged,
490
- ]);
231
+ onCartReadySend({
232
+ type: 'CART_CREATE',
233
+ payload: cartInput,
234
+ });
235
+ }, [countryCode, customerAccessToken, onCartReadySend]);
236
+ // Delays the cart state in the context if the page is hydrating
237
+ // preventing suspense boundary errors.
238
+ const cartDisplayState = useDelayedStateUntilHydration(cartState);
491
239
  const cartContextValue = useMemo(() => {
492
240
  return {
493
- ...('cart' in state
494
- ? state.cart
495
- : {
496
- lines: [],
497
- attributes: [],
498
- ...(cart ? cartFromGraphQL(cart) : {}),
499
- }),
500
- status: state.status,
501
- error: 'error' in state ? state.error : undefined,
502
- totalQuantity: 'cart' in state ? state?.cart?.totalQuantity ?? 0 : 0,
241
+ ...(cartDisplayState?.context?.cart ?? { lines: [], attributes: [] }),
242
+ status: transposeStatus(cartDisplayState.value),
243
+ error: cartDisplayState?.context?.errors,
244
+ totalQuantity: cartDisplayState?.context?.cart?.totalQuantity ?? 0,
503
245
  cartCreate,
504
246
  linesAdd(lines) {
505
- if ('cart' in state && state.cart.id) {
506
- addLineItem(lines, state);
247
+ if (cartDisplayState?.context?.cart?.id) {
248
+ onCartReadySend({
249
+ type: 'CARTLINE_ADD',
250
+ payload: { lines },
251
+ });
507
252
  }
508
253
  else {
509
254
  cartCreate({ lines });
510
255
  }
511
256
  },
512
257
  linesRemove(lines) {
513
- removeLineItem(lines, state);
258
+ onCartReadySend({
259
+ type: 'CARTLINE_REMOVE',
260
+ payload: {
261
+ lines,
262
+ },
263
+ });
514
264
  },
515
265
  linesUpdate(lines) {
516
- updateLineItem(lines, state);
266
+ onCartReadySend({
267
+ type: 'CARTLINE_UPDATE',
268
+ payload: {
269
+ lines,
270
+ },
271
+ });
517
272
  },
518
273
  noteUpdate(note) {
519
- noteUpdate(note, state);
274
+ onCartReadySend({
275
+ type: 'NOTE_UPDATE',
276
+ payload: {
277
+ note,
278
+ },
279
+ });
520
280
  },
521
281
  buyerIdentityUpdate(buyerIdentity) {
522
- buyerIdentityUpdate(buyerIdentity, state);
282
+ onCartReadySend({
283
+ type: 'BUYER_IDENTITY_UPDATE',
284
+ payload: {
285
+ buyerIdentity,
286
+ },
287
+ });
523
288
  },
524
289
  cartAttributesUpdate(attributes) {
525
- cartAttributesUpdate(attributes, state);
290
+ onCartReadySend({
291
+ type: 'CART_ATTRIBUTES_UPDATE',
292
+ payload: {
293
+ attributes,
294
+ },
295
+ });
526
296
  },
527
297
  discountCodesUpdate(discountCodes) {
528
- discountCodesUpdate(discountCodes, state);
298
+ onCartReadySend({
299
+ type: 'DISCOUNT_CODES_UPDATE',
300
+ payload: {
301
+ discountCodes,
302
+ },
303
+ });
529
304
  },
530
305
  cartFragment,
531
306
  };
532
307
  }, [
533
- state,
534
- cart,
535
308
  cartCreate,
309
+ cartDisplayState?.context?.cart,
310
+ cartDisplayState?.context?.errors,
311
+ cartDisplayState.value,
536
312
  cartFragment,
537
- addLineItem,
538
- removeLineItem,
539
- updateLineItem,
540
- noteUpdate,
541
- buyerIdentityUpdate,
542
- cartAttributesUpdate,
543
- discountCodesUpdate,
313
+ onCartReadySend,
544
314
  ]);
545
315
  return (React.createElement(CartContext.Provider, { value: cartContextValue }, children));
546
316
  }
547
- function cartFromGraphQL(cart) {
548
- return {
549
- ...cart,
550
- // @ts-expect-error While the cart still uses fragments, there will be a TS error here until we remove those fragments and get the type in-line
551
- lines: flattenConnection(cart.lines),
552
- note: cart.note ?? undefined,
553
- };
317
+ function transposeStatus(status) {
318
+ switch (status) {
319
+ case 'uninitialized':
320
+ case 'initializationError':
321
+ return 'uninitialized';
322
+ case 'idle':
323
+ case 'cartCompleted':
324
+ case 'error':
325
+ return 'idle';
326
+ case 'cartFetching':
327
+ return 'fetching';
328
+ case 'cartCreating':
329
+ return 'creating';
330
+ case 'cartLineAdding':
331
+ case 'cartLineRemoving':
332
+ case 'cartLineUpdating':
333
+ case 'noteUpdating':
334
+ case 'buyerIdentityUpdating':
335
+ case 'cartAttributesUpdating':
336
+ case 'discountCodesUpdating':
337
+ return 'updating';
338
+ }
339
+ }
340
+ /**
341
+ * Delays a state update until hydration finishes. Useful for preventing suspense boundaries errors when updating a context
342
+ * @remarks this uses startTransition and waits for it to finish.
343
+ */
344
+ function useDelayedStateUntilHydration(state) {
345
+ const [isPending, startTransition] = useTransition();
346
+ const [delayedState, setDelayedState] = useState(state);
347
+ const firstTimePending = useRef(false);
348
+ if (isPending) {
349
+ firstTimePending.current = true;
350
+ }
351
+ const firstTimePendingFinished = useRef(false);
352
+ if (!isPending && firstTimePending.current) {
353
+ firstTimePendingFinished.current = true;
354
+ }
355
+ useEffect(() => {
356
+ startTransition(() => {
357
+ if (!firstTimePendingFinished.current) {
358
+ setDelayedState(state);
359
+ }
360
+ });
361
+ }, [state]);
362
+ const displayState = firstTimePendingFinished.current ? state : delayedState;
363
+ return displayState;
364
+ }
365
+ /** Check for storage availability funciton obtained from
366
+ * https://developer.mozilla.org/en-US/docs/Web/API/Web_Storage_API/Using_the_Web_Storage_API
367
+ */
368
+ function storageAvailable(type) {
369
+ let storage;
370
+ try {
371
+ storage = window[type];
372
+ const x = '__storage_test__';
373
+ storage.setItem(x, x);
374
+ storage.removeItem(x);
375
+ return true;
376
+ }
377
+ catch (e) {
378
+ return (e instanceof DOMException &&
379
+ // everything except Firefox
380
+ (e.code === 22 ||
381
+ // Firefox
382
+ e.code === 1014 ||
383
+ // test name field too, because code might not be present
384
+ // everything except Firefox
385
+ e.name === 'QuotaExceededError' ||
386
+ // Firefox
387
+ e.name === 'NS_ERROR_DOM_QUOTA_REACHED') &&
388
+ // acknowledge QuotaExceededError only if there's something already stored
389
+ storage &&
390
+ storage.length !== 0);
391
+ }
392
+ }
393
+ function countryCodeNotUpdated(context, event) {
394
+ return (event.payload.buyerIdentity.countryCode &&
395
+ context.cart?.buyerIdentity?.countryCode !==
396
+ event.payload.buyerIdentity.countryCode);
397
+ }
398
+ // Cart Analytics
399
+ function publishCreateAnalytics(context, event) {
400
+ ClientAnalytics.publish(ClientAnalytics.eventNames.ADD_TO_CART, true, {
401
+ addedCartLines: event.payload.lines,
402
+ cart: context.rawCartResult,
403
+ prevCart: null,
404
+ });
405
+ }
406
+ function publishLineAddAnalytics(context, event) {
407
+ ClientAnalytics.publish(ClientAnalytics.eventNames.ADD_TO_CART, true, {
408
+ addedCartLines: event.payload.lines,
409
+ cart: context.rawCartResult,
410
+ prevCart: context.prevCart,
411
+ });
412
+ }
413
+ function publishLineUpdateAnalytics(context, event) {
414
+ ClientAnalytics.publish(ClientAnalytics.eventNames.UPDATE_CART, true, {
415
+ updatedCartLines: event.payload.lines,
416
+ oldCart: context.prevCart,
417
+ cart: context.rawCartResult,
418
+ prevCart: context.prevCart,
419
+ });
420
+ }
421
+ function publishLineRemoveAnalytics(context, event) {
422
+ ClientAnalytics.publish(ClientAnalytics.eventNames.REMOVE_FROM_CART, true, {
423
+ removedCartLines: event.payload.lines,
424
+ cart: context.rawCartResult,
425
+ prevCart: context.prevCart,
426
+ });
427
+ }
428
+ function publishDiscountCodesUpdateAnalytics(context, event) {
429
+ ClientAnalytics.publish(ClientAnalytics.eventNames.DISCOUNT_CODE_UPDATED, true, {
430
+ updatedDiscountCodes: event.payload.discountCodes,
431
+ cart: context.rawCartResult,
432
+ prevCart: context.prevCart,
433
+ });
434
+ }
435
+ export const defaultCartFragment = `
436
+ fragment CartFragment on Cart {
437
+ id
438
+ checkoutUrl
439
+ totalQuantity
440
+ buyerIdentity {
441
+ countryCode
442
+ customer {
443
+ id
444
+ email
445
+ firstName
446
+ lastName
447
+ displayName
448
+ }
449
+ email
450
+ phone
451
+ }
452
+ lines(first: $numCartLines) {
453
+ edges {
454
+ node {
455
+ id
456
+ quantity
457
+ attributes {
458
+ key
459
+ value
460
+ }
461
+ cost {
462
+ totalAmount {
463
+ amount
464
+ currencyCode
465
+ }
466
+ compareAtAmountPerQuantity {
467
+ amount
468
+ currencyCode
469
+ }
470
+ }
471
+ merchandise {
472
+ ... on ProductVariant {
473
+ id
474
+ availableForSale
475
+ compareAtPriceV2 {
476
+ ...MoneyFragment
477
+ }
478
+ priceV2 {
479
+ ...MoneyFragment
480
+ }
481
+ requiresShipping
482
+ title
483
+ image {
484
+ ...ImageFragment
485
+ }
486
+ product {
487
+ handle
488
+ title
489
+ }
490
+ selectedOptions {
491
+ name
492
+ value
493
+ }
494
+ }
495
+ }
496
+ }
497
+ }
498
+ }
499
+ cost {
500
+ subtotalAmount {
501
+ ...MoneyFragment
502
+ }
503
+ totalAmount {
504
+ ...MoneyFragment
505
+ }
506
+ totalDutyAmount {
507
+ ...MoneyFragment
508
+ }
509
+ totalTaxAmount {
510
+ ...MoneyFragment
511
+ }
512
+ }
513
+ note
514
+ attributes {
515
+ key
516
+ value
517
+ }
518
+ discountCodes {
519
+ code
520
+ }
521
+ }
522
+
523
+ fragment MoneyFragment on MoneyV2 {
524
+ currencyCode
525
+ amount
526
+ }
527
+ fragment ImageFragment on Image {
528
+ id
529
+ url
530
+ altText
531
+ width
532
+ height
554
533
  }
534
+ `;