@shopify/hydrogen 1.4.0 → 1.4.2

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.
@@ -1,4 +1,4 @@
1
- import { AttributeInput, CartBuyerIdentityInput, CartInput, CartLineInput, CartLineUpdateInput } from '../../storefront-api-types.js';
1
+ import { AttributeInput, CartBuyerIdentityInput, CartInput, CartLineInput, CartLineUpdateInput, CountryCode } from '../../storefront-api-types.js';
2
2
  import { CartAttributesUpdateMutation } from './graphql/CartAttributesUpdateMutation.js';
3
3
  import { CartBuyerIdentityUpdateMutation } from './graphql/CartBuyerIdentityUpdateMutation.js';
4
4
  import { CartCreateMutation } from './graphql/CartCreateMutation.js';
@@ -13,11 +13,13 @@ import { CartQueryQuery } from './graphql/CartQuery.js';
13
13
  *
14
14
  * See [cart API graphql mutations](https://shopify.dev/api/storefront/2022-07/objects/Cart)
15
15
  */
16
- export declare function useCartActions({ numCartLines, cartFragment, }: {
16
+ 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
+ /** The ISO country code for i18n. */
22
+ countryCode?: CountryCode;
21
23
  }): {
22
24
  cartFetch: (cartId: string) => Promise<{
23
25
  data: CartQueryQuery | undefined;
@@ -57,4 +59,3 @@ export declare function useCartActions({ numCartLines, cartFragment, }: {
57
59
  }>;
58
60
  cartFragment: string;
59
61
  };
60
- 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, }) {
10
+ export function useCartActions({ numCartLines, cartFragment, countryCode = CountryCode.Us, }) {
11
11
  const fetchCart = useCartFetch();
12
12
  const cartFetch = useCallback((cartId) => {
13
13
  return fetchCart({
@@ -15,20 +15,20 @@ export function useCartActions({ numCartLines, cartFragment = defaultCartFragmen
15
15
  variables: {
16
16
  id: cartId,
17
17
  numCartLines,
18
- country: CountryCode.Us,
18
+ country: countryCode,
19
19
  },
20
20
  });
21
- }, [fetchCart, cartFragment, numCartLines]);
21
+ }, [fetchCart, cartFragment, numCartLines, countryCode]);
22
22
  const cartCreate = useCallback((cart) => {
23
23
  return fetchCart({
24
24
  query: CartCreate(cartFragment),
25
25
  variables: {
26
26
  input: cart,
27
27
  numCartLines,
28
- country: CountryCode.Us,
28
+ country: countryCode,
29
29
  },
30
30
  });
31
- }, [cartFragment, fetchCart, numCartLines]);
31
+ }, [cartFragment, countryCode, fetchCart, numCartLines]);
32
32
  const cartLineAdd = useCallback((cartId, lines) => {
33
33
  return fetchCart({
34
34
  query: CartLineAdd(cartFragment),
@@ -36,10 +36,10 @@ export function useCartActions({ numCartLines, cartFragment = defaultCartFragmen
36
36
  cartId,
37
37
  lines,
38
38
  numCartLines,
39
- country: CountryCode.Us,
39
+ country: countryCode,
40
40
  },
41
41
  });
42
- }, [cartFragment, fetchCart, numCartLines]);
42
+ }, [cartFragment, countryCode, fetchCart, numCartLines]);
43
43
  const cartLineUpdate = useCallback((cartId, lines) => {
44
44
  return fetchCart({
45
45
  query: CartLineUpdate(cartFragment),
@@ -47,10 +47,10 @@ export function useCartActions({ numCartLines, cartFragment = defaultCartFragmen
47
47
  cartId,
48
48
  lines,
49
49
  numCartLines,
50
- country: CountryCode.Us,
50
+ country: countryCode,
51
51
  },
52
52
  });
53
- }, [cartFragment, fetchCart, numCartLines]);
53
+ }, [cartFragment, countryCode, fetchCart, numCartLines]);
54
54
  const cartLineRemove = useCallback((cartId, lines) => {
55
55
  return fetchCart({
56
56
  query: CartLineRemove(cartFragment),
@@ -58,10 +58,10 @@ export function useCartActions({ numCartLines, cartFragment = defaultCartFragmen
58
58
  cartId,
59
59
  lines,
60
60
  numCartLines,
61
- country: CountryCode.Us,
61
+ country: countryCode,
62
62
  },
63
63
  });
64
- }, [cartFragment, fetchCart, numCartLines]);
64
+ }, [cartFragment, countryCode, fetchCart, numCartLines]);
65
65
  const noteUpdate = useCallback((cartId, note) => {
66
66
  return fetchCart({
67
67
  query: CartNoteUpdate(cartFragment),
@@ -69,10 +69,10 @@ export function useCartActions({ numCartLines, cartFragment = defaultCartFragmen
69
69
  cartId,
70
70
  note,
71
71
  numCartLines,
72
- country: CountryCode.Us,
72
+ country: countryCode,
73
73
  },
74
74
  });
75
- }, [fetchCart, cartFragment, numCartLines]);
75
+ }, [fetchCart, cartFragment, numCartLines, countryCode]);
76
76
  const buyerIdentityUpdate = useCallback((cartId, buyerIdentity) => {
77
77
  return fetchCart({
78
78
  query: CartBuyerIdentityUpdate(cartFragment),
@@ -80,10 +80,10 @@ export function useCartActions({ numCartLines, cartFragment = defaultCartFragmen
80
80
  cartId,
81
81
  buyerIdentity,
82
82
  numCartLines,
83
- country: CountryCode.Us,
83
+ country: countryCode,
84
84
  },
85
85
  });
86
- }, [cartFragment, fetchCart, numCartLines]);
86
+ }, [cartFragment, countryCode, fetchCart, numCartLines]);
87
87
  const cartAttributesUpdate = useCallback((cartId, attributes) => {
88
88
  return fetchCart({
89
89
  query: CartAttributesUpdate(cartFragment),
@@ -91,10 +91,10 @@ export function useCartActions({ numCartLines, cartFragment = defaultCartFragmen
91
91
  cartId,
92
92
  attributes,
93
93
  numCartLines,
94
- country: CountryCode.Us,
94
+ country: countryCode,
95
95
  },
96
96
  });
97
- }, [cartFragment, fetchCart, numCartLines]);
97
+ }, [cartFragment, countryCode, fetchCart, numCartLines]);
98
98
  const discountCodesUpdate = useCallback((cartId, discountCodes) => {
99
99
  return fetchCart({
100
100
  query: CartDiscountCodesUpdate(cartFragment),
@@ -102,10 +102,10 @@ export function useCartActions({ numCartLines, cartFragment = defaultCartFragmen
102
102
  cartId,
103
103
  discountCodes,
104
104
  numCartLines,
105
- country: CountryCode.Us,
105
+ country: countryCode,
106
106
  },
107
107
  });
108
- }, [cartFragment, fetchCart, numCartLines]);
108
+ }, [cartFragment, countryCode, fetchCart, numCartLines]);
109
109
  return useMemo(() => ({
110
110
  cartFetch,
111
111
  cartCreate,
@@ -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
- `;
@@ -1,6 +1,7 @@
1
1
  import React from 'react';
2
2
  import { CartFragmentFragment } from './graphql/CartFragment.js';
3
- export declare function CartProviderV2({ children, numCartLines, onCreate, onLineAdd, onLineRemove, onLineUpdate, onNoteUpdate, onBuyerIdentityUpdate, onAttributesUpdate, onDiscountCodesUpdate, onCreateComplete, onLineAddComplete, onLineRemoveComplete, onLineUpdateComplete, onNoteUpdateComplete, onBuyerIdentityUpdateComplete, onAttributesUpdateComplete, onDiscountCodesUpdateComplete, data, cartFragment, }: {
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: cart, cartFragment, customerAccessToken, countryCode, }: {
4
5
  /** Any `ReactNode` elements. */
5
6
  children: React.ReactNode;
6
7
  /** Maximum number of cart lines to fetch. Defaults to 250 cart lines. */
@@ -41,4 +42,9 @@ export declare function CartProviderV2({ children, numCartLines, onCreate, onLin
41
42
  data?: CartFragmentFragment;
42
43
  /** 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. */
43
44
  cartFragment?: string;
45
+ /** A customer access token that's accessible on the server if there's a customer login. */
46
+ customerAccessToken?: CartBuyerIdentityInput['customerAccessToken'];
47
+ /** The ISO country code for i18n. */
48
+ countryCode?: CountryCode;
44
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,69 +1,176 @@
1
- import React, { useCallback, useEffect, useMemo, useState } from 'react';
1
+ import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
2
+ import { CountryCode, } from '../../storefront-api-types.js';
2
3
  import { CartContext } from './context.js';
3
- import { useCartActions } from './CartActions.client.js';
4
4
  import { useCartAPIStateMachine } from './useCartAPIStateMachine.client.js';
5
5
  import { CART_ID_STORAGE_KEY } from './constants.js';
6
- export function CartProviderV2({ children, numCartLines, onCreate, onLineAdd, onLineRemove, onLineUpdate, onNoteUpdate, onBuyerIdentityUpdate, onAttributesUpdate, onDiscountCodesUpdate, onCreateComplete, onLineAddComplete, onLineRemoveComplete, onLineUpdateComplete, onNoteUpdateComplete, onBuyerIdentityUpdateComplete, onAttributesUpdateComplete, onDiscountCodesUpdateComplete, data: cart, cartFragment, }) {
7
- const { cartFragment: usedCartFragment } = useCartActions({
8
- numCartLines,
9
- cartFragment,
10
- });
6
+ import { ClientAnalytics } from '../../foundation/Analytics/ClientAnalytics.js';
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, }) {
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;
18
+ }
11
19
  const [cartState, cartSend] = useCartAPIStateMachine({
12
20
  numCartLines,
13
21
  cartFragment,
22
+ countryCode,
14
23
  onCartActionEntry(context, event) {
24
+ try {
25
+ switch (event.type) {
26
+ case 'CART_CREATE':
27
+ return onCreate?.();
28
+ case 'CARTLINE_ADD':
29
+ return onLineAdd?.();
30
+ case 'CARTLINE_REMOVE':
31
+ return onLineRemove?.();
32
+ case 'CARTLINE_UPDATE':
33
+ return onLineUpdate?.();
34
+ case 'NOTE_UPDATE':
35
+ return onNoteUpdate?.();
36
+ case 'BUYER_IDENTITY_UPDATE':
37
+ return onBuyerIdentityUpdate?.();
38
+ case 'CART_ATTRIBUTES_UPDATE':
39
+ return onAttributesUpdate?.();
40
+ case 'DISCOUNT_CODES_UPDATE':
41
+ return onDiscountCodesUpdate?.();
42
+ }
43
+ }
44
+ catch (error) {
45
+ console.error('Cart entry action failed', error);
46
+ }
47
+ },
48
+ onCartActionOptimisticUI(context, event) {
49
+ if (!context?.cart)
50
+ return { cart: undefined };
15
51
  switch (event.type) {
16
- case 'CART_CREATE':
17
- return onCreate?.();
18
- case 'CARTLINE_ADD':
19
- return onLineAdd?.();
20
52
  case 'CARTLINE_REMOVE':
21
- return onLineRemove?.();
53
+ return {
54
+ ...context,
55
+ lastValidCart: context.cart,
56
+ cart: {
57
+ ...context.cart,
58
+ lines: context?.cart?.lines.filter(({ id }) => !event.payload.lines.includes(id)),
59
+ },
60
+ };
22
61
  case 'CARTLINE_UPDATE':
23
- return onLineUpdate?.();
24
- case 'NOTE_UPDATE':
25
- return onNoteUpdate?.();
26
- case 'BUYER_IDENTITY_UPDATE':
27
- return onBuyerIdentityUpdate?.();
28
- case 'CART_ATTRIBUTES_UPDATE':
29
- return onAttributesUpdate?.();
30
- case 'DISCOUNT_CODES_UPDATE':
31
- return onDiscountCodesUpdate?.();
62
+ return {
63
+ ...context,
64
+ lastValidCart: context.cart,
65
+ cart: {
66
+ ...context.cart,
67
+ lines: context.cart.lines.map((line) => {
68
+ const updatedLine = event.payload.lines.find(({ id }) => id === line.id);
69
+ if (updatedLine && updatedLine.quantity) {
70
+ return {
71
+ ...line,
72
+ quantity: updatedLine.quantity,
73
+ };
74
+ }
75
+ return line;
76
+ }),
77
+ },
78
+ };
32
79
  }
80
+ return { cart: context.cart ? { ...context.cart } : undefined };
33
81
  },
34
82
  onCartActionComplete(context, event) {
35
- switch (event.type) {
36
- case 'RESOLVE':
37
- switch (event.payload.cartActionEvent.type) {
38
- case 'CART_CREATE':
39
- return onCreateComplete?.();
40
- case 'CARTLINE_ADD':
41
- return onLineAddComplete?.();
42
- case 'CARTLINE_REMOVE':
43
- return onLineRemoveComplete?.();
44
- case 'CARTLINE_UPDATE':
45
- return onLineUpdateComplete?.();
46
- case 'NOTE_UPDATE':
47
- return onNoteUpdateComplete?.();
48
- case 'BUYER_IDENTITY_UPDATE':
49
- return onBuyerIdentityUpdateComplete?.();
50
- case 'CART_ATTRIBUTES_UPDATE':
51
- return onAttributesUpdateComplete?.();
52
- case 'DISCOUNT_CODES_UPDATE':
53
- return onDiscountCodesUpdateComplete?.();
54
- }
83
+ const cartActionEvent = event.payload.cartActionEvent;
84
+ try {
85
+ switch (event.type) {
86
+ case 'RESOLVE':
87
+ switch (cartActionEvent.type) {
88
+ case 'CART_CREATE':
89
+ publishCreateAnalytics(context, cartActionEvent);
90
+ return onCreateComplete?.();
91
+ case 'CARTLINE_ADD':
92
+ publishLineAddAnalytics(context, cartActionEvent);
93
+ return onLineAddComplete?.();
94
+ case 'CARTLINE_REMOVE':
95
+ publishLineRemoveAnalytics(context, cartActionEvent);
96
+ return onLineRemoveComplete?.();
97
+ case 'CARTLINE_UPDATE':
98
+ publishLineUpdateAnalytics(context, cartActionEvent);
99
+ return onLineUpdateComplete?.();
100
+ case 'NOTE_UPDATE':
101
+ return onNoteUpdateComplete?.();
102
+ case 'BUYER_IDENTITY_UPDATE':
103
+ if (countryCodeNotUpdated(context, cartActionEvent)) {
104
+ customerOverridesCountryCode.current = true;
105
+ }
106
+ return onBuyerIdentityUpdateComplete?.();
107
+ case 'CART_ATTRIBUTES_UPDATE':
108
+ return onAttributesUpdateComplete?.();
109
+ case 'DISCOUNT_CODES_UPDATE':
110
+ publishDiscountCodesUpdateAnalytics(context, cartActionEvent);
111
+ return onDiscountCodesUpdateComplete?.();
112
+ }
113
+ }
114
+ }
115
+ catch (error) {
116
+ console.error('onCartActionComplete failed', error);
55
117
  }
56
118
  },
57
119
  });
58
- const [cartReady, setCartReady] = useState(false);
120
+ const cartReady = useRef(false);
59
121
  const cartCompleted = cartState.matches('cartCompleted');
122
+ const countryChanged = (cartState.value === 'idle' ||
123
+ cartState.value === 'error' ||
124
+ cartState.value === 'cartCompleted') &&
125
+ countryCode !== cartState?.context?.cart?.buyerIdentity?.countryCode &&
126
+ !cartState.context.errors;
127
+ /**
128
+ * Initializes cart with priority in this order:
129
+ * 1. cart props
130
+ * 2. localStorage cartId
131
+ */
132
+ useEffect(() => {
133
+ if (!cartReady.current) {
134
+ if (cart) {
135
+ cartSend({ type: 'CART_SET', payload: { cart } });
136
+ }
137
+ else if (storageAvailable('localStorage')) {
138
+ try {
139
+ const cartId = window.localStorage.getItem(CART_ID_STORAGE_KEY);
140
+ if (cartId) {
141
+ cartSend({ type: 'CART_FETCH', payload: { cartId } });
142
+ }
143
+ }
144
+ catch (error) {
145
+ console.warn('error fetching cartId');
146
+ console.warn(error);
147
+ }
148
+ }
149
+ cartReady.current = true;
150
+ }
151
+ }, [cart, cartReady, cartSend]);
152
+ // Update cart country code if cart and props countryCode's as different
153
+ useEffect(() => {
154
+ if (!countryChanged || customerOverridesCountryCode.current)
155
+ return;
156
+ cartSend({
157
+ type: 'BUYER_IDENTITY_UPDATE',
158
+ payload: { buyerIdentity: { countryCode, customerAccessToken } },
159
+ });
160
+ }, [
161
+ countryCode,
162
+ customerAccessToken,
163
+ countryChanged,
164
+ customerOverridesCountryCode,
165
+ cartSend,
166
+ ]);
60
167
  // send cart events when ready
61
168
  const onCartReadySend = useCallback((cartEvent) => {
62
- if (!cartReady) {
169
+ if (!cartReady.current) {
63
170
  return console.warn("Cart isn't ready yet");
64
171
  }
65
172
  cartSend(cartEvent);
66
- }, [cartReady, cartSend]);
173
+ }, [cartSend]);
67
174
  // save cart id to local storage
68
175
  useEffect(() => {
69
176
  if (cartState?.context?.cart?.id && storageAvailable('localStorage')) {
@@ -86,39 +193,42 @@ export function CartProviderV2({ children, numCartLines, onCreate, onLineAdd, on
86
193
  }
87
194
  }
88
195
  }, [cartCompleted]);
89
- // fetch cart from local storage if cart id present and set cart as ready for use
90
- useEffect(() => {
91
- if (!cartReady && storageAvailable('localStorage')) {
92
- try {
93
- const cartId = window.localStorage.getItem(CART_ID_STORAGE_KEY);
94
- if (cartId) {
95
- cartSend({ type: 'CART_FETCH', payload: { cartId } });
96
- }
196
+ const cartCreate = useCallback((cartInput) => {
197
+ if (countryCode && !cartInput.buyerIdentity?.countryCode) {
198
+ if (cartInput.buyerIdentity == null) {
199
+ cartInput.buyerIdentity = {};
97
200
  }
98
- catch (error) {
99
- console.warn('error fetching cartId');
100
- console.warn(error);
201
+ cartInput.buyerIdentity.countryCode = countryCode;
202
+ }
203
+ if (customerAccessToken &&
204
+ !cartInput.buyerIdentity?.customerAccessToken) {
205
+ if (cartInput.buyerIdentity == null) {
206
+ cartInput.buyerIdentity = {};
101
207
  }
102
- setCartReady(true);
208
+ cartInput.buyerIdentity.customerAccessToken = customerAccessToken;
103
209
  }
104
- }, [cartReady, cartSend]);
210
+ onCartReadySend({
211
+ type: 'CART_CREATE',
212
+ payload: cartInput,
213
+ });
214
+ }, [countryCode, customerAccessToken, onCartReadySend]);
105
215
  const cartContextValue = useMemo(() => {
106
216
  return {
107
217
  ...(cartState?.context?.cart ?? { lines: [], attributes: [] }),
108
218
  status: transposeStatus(cartState.value),
109
219
  error: cartState?.context?.errors,
110
220
  totalQuantity: cartState?.context?.cart?.totalQuantity ?? 0,
111
- cartCreate(cartInput) {
112
- onCartReadySend({
113
- type: 'CART_CREATE',
114
- payload: cartInput,
115
- });
116
- },
221
+ cartCreate,
117
222
  linesAdd(lines) {
118
- onCartReadySend({
119
- type: 'CARTLINE_ADD',
120
- payload: { lines },
121
- });
223
+ if (cartState?.context?.cart?.id) {
224
+ onCartReadySend({
225
+ type: 'CARTLINE_ADD',
226
+ payload: { lines },
227
+ });
228
+ }
229
+ else {
230
+ cartCreate({ lines });
231
+ }
122
232
  },
123
233
  linesRemove(lines) {
124
234
  onCartReadySend({
@@ -168,25 +278,26 @@ export function CartProviderV2({ children, numCartLines, onCreate, onLineAdd, on
168
278
  },
169
279
  });
170
280
  },
171
- cartFragment: usedCartFragment,
281
+ cartFragment,
172
282
  };
173
283
  }, [
284
+ cartCreate,
285
+ cartFragment,
174
286
  cartState?.context?.cart,
175
287
  cartState?.context?.errors,
176
288
  cartState.value,
177
289
  onCartReadySend,
178
- usedCartFragment,
179
290
  ]);
180
291
  return (React.createElement(CartContext.Provider, { value: cartContextValue }, children));
181
292
  }
182
293
  function transposeStatus(status) {
183
294
  switch (status) {
184
295
  case 'uninitialized':
296
+ case 'initializationError':
185
297
  return 'uninitialized';
186
298
  case 'idle':
187
299
  case 'cartCompleted':
188
300
  case 'error':
189
- case 'initializationError':
190
301
  return 'idle';
191
302
  case 'cartFetching':
192
303
  return 'fetching';
@@ -230,3 +341,145 @@ function storageAvailable(type) {
230
341
  storage.length !== 0);
231
342
  }
232
343
  }
344
+ function countryCodeNotUpdated(context, event) {
345
+ return (event.payload.buyerIdentity.countryCode &&
346
+ context.cart?.buyerIdentity?.countryCode !==
347
+ event.payload.buyerIdentity.countryCode);
348
+ }
349
+ // Cart Analytics
350
+ function publishCreateAnalytics(context, event) {
351
+ ClientAnalytics.publish(ClientAnalytics.eventNames.ADD_TO_CART, true, {
352
+ addedCartLines: event.payload.lines,
353
+ cart: context.rawCartResult,
354
+ prevCart: null,
355
+ });
356
+ }
357
+ function publishLineAddAnalytics(context, event) {
358
+ ClientAnalytics.publish(ClientAnalytics.eventNames.ADD_TO_CART, true, {
359
+ addedCartLines: event.payload.lines,
360
+ cart: context.rawCartResult,
361
+ prevCart: context.prevCart,
362
+ });
363
+ }
364
+ function publishLineUpdateAnalytics(context, event) {
365
+ ClientAnalytics.publish(ClientAnalytics.eventNames.UPDATE_CART, true, {
366
+ updatedCartLines: event.payload.lines,
367
+ oldCart: context.prevCart,
368
+ cart: context.rawCartResult,
369
+ prevCart: context.prevCart,
370
+ });
371
+ }
372
+ function publishLineRemoveAnalytics(context, event) {
373
+ ClientAnalytics.publish(ClientAnalytics.eventNames.REMOVE_FROM_CART, true, {
374
+ removedCartLines: event.payload.lines,
375
+ cart: context.rawCartResult,
376
+ prevCart: context.prevCart,
377
+ });
378
+ }
379
+ function publishDiscountCodesUpdateAnalytics(context, event) {
380
+ ClientAnalytics.publish(ClientAnalytics.eventNames.DISCOUNT_CODE_UPDATED, true, {
381
+ updatedDiscountCodes: event.payload.discountCodes,
382
+ cart: context.rawCartResult,
383
+ prevCart: context.prevCart,
384
+ });
385
+ }
386
+ export const defaultCartFragment = `
387
+ fragment CartFragment on Cart {
388
+ id
389
+ checkoutUrl
390
+ totalQuantity
391
+ buyerIdentity {
392
+ countryCode
393
+ customer {
394
+ id
395
+ email
396
+ firstName
397
+ lastName
398
+ displayName
399
+ }
400
+ email
401
+ phone
402
+ }
403
+ lines(first: $numCartLines) {
404
+ edges {
405
+ node {
406
+ id
407
+ quantity
408
+ attributes {
409
+ key
410
+ value
411
+ }
412
+ cost {
413
+ totalAmount {
414
+ amount
415
+ currencyCode
416
+ }
417
+ compareAtAmountPerQuantity {
418
+ amount
419
+ currencyCode
420
+ }
421
+ }
422
+ merchandise {
423
+ ... on ProductVariant {
424
+ id
425
+ availableForSale
426
+ compareAtPriceV2 {
427
+ ...MoneyFragment
428
+ }
429
+ priceV2 {
430
+ ...MoneyFragment
431
+ }
432
+ requiresShipping
433
+ title
434
+ image {
435
+ ...ImageFragment
436
+ }
437
+ product {
438
+ handle
439
+ title
440
+ }
441
+ selectedOptions {
442
+ name
443
+ value
444
+ }
445
+ }
446
+ }
447
+ }
448
+ }
449
+ }
450
+ cost {
451
+ subtotalAmount {
452
+ ...MoneyFragment
453
+ }
454
+ totalAmount {
455
+ ...MoneyFragment
456
+ }
457
+ totalDutyAmount {
458
+ ...MoneyFragment
459
+ }
460
+ totalTaxAmount {
461
+ ...MoneyFragment
462
+ }
463
+ }
464
+ note
465
+ attributes {
466
+ key
467
+ value
468
+ }
469
+ discountCodes {
470
+ code
471
+ }
472
+ }
473
+
474
+ fragment MoneyFragment on MoneyV2 {
475
+ currencyCode
476
+ amount
477
+ }
478
+ fragment ImageFragment on Image {
479
+ id
480
+ url
481
+ altText
482
+ width
483
+ height
484
+ }
485
+ `;
@@ -97,6 +97,7 @@ export declare type CartAction = {
97
97
  } | {
98
98
  type: 'resolve';
99
99
  cart: Cart;
100
+ rawCartResult?: CartFragmentFragment;
100
101
  } | {
101
102
  type: 'reject';
102
103
  errors: any;
@@ -105,6 +106,9 @@ export declare type CartAction = {
105
106
  };
106
107
  export declare type CartMachineContext = {
107
108
  cart?: Cart;
109
+ lastValidCart?: Cart;
110
+ rawCartResult?: CartFragmentFragment;
111
+ prevCart?: Cart;
108
112
  errors?: any;
109
113
  };
110
114
  export declare type CartFetchEvent = {
@@ -117,6 +121,12 @@ export declare type CartCreateEvent = {
117
121
  type: 'CART_CREATE';
118
122
  payload: CartInput;
119
123
  };
124
+ export declare type CartSetEvent = {
125
+ type: 'CART_SET';
126
+ payload: {
127
+ cart: CartFragmentFragment;
128
+ };
129
+ };
120
130
  export declare type CartLineAddEvent = {
121
131
  type: 'CARTLINE_ADD';
122
132
  payload: {
@@ -159,7 +169,7 @@ export declare type DiscountCodesUpdateEvent = {
159
169
  discountCodes: string[];
160
170
  };
161
171
  };
162
- 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;
163
173
  export declare type CartMachineFetchResultEvent = {
164
174
  type: 'CART_COMPLETED';
165
175
  payload: {
@@ -170,6 +180,7 @@ export declare type CartMachineFetchResultEvent = {
170
180
  payload: {
171
181
  cartActionEvent: CartMachineActionEvent;
172
182
  cart: Cart;
183
+ rawCartResult: CartFragmentFragment;
173
184
  };
174
185
  } | {
175
186
  type: 'ERROR';
@@ -183,30 +194,40 @@ export declare type CartMachineTypeState = {
183
194
  value: 'uninitialized';
184
195
  context: CartMachineContext & {
185
196
  cart: undefined;
197
+ lastValidCart: undefined;
198
+ prevCart: undefined;
186
199
  errors?: any;
187
200
  };
188
201
  } | {
189
202
  value: 'initializationError';
190
203
  context: CartMachineContext & {
191
204
  cart: undefined;
205
+ lastValidCart: undefined;
206
+ prevCart: undefined;
192
207
  errors: any;
193
208
  };
194
209
  } | {
195
210
  value: 'cartCompleted';
196
211
  context: CartMachineContext & {
197
212
  cart: undefined;
213
+ prevCart?: Cart;
214
+ lastValidCart: undefined;
198
215
  errors: any;
199
216
  };
200
217
  } | {
201
218
  value: 'idle';
202
219
  context: CartMachineContext & {
203
220
  cart: Cart;
221
+ prevCart?: Cart;
222
+ lastValidCart?: Cart;
204
223
  errors?: any;
205
224
  };
206
225
  } | {
207
226
  value: 'error';
208
227
  context: CartMachineContext & {
209
228
  cart?: Cart;
229
+ prevCart?: Cart;
230
+ lastValidCart?: Cart;
210
231
  errors: any;
211
232
  };
212
233
  } | {
@@ -249,5 +270,6 @@ export declare type CartMachineActions = {
249
270
  cartAttributesUpdateAction: CartMachineAction;
250
271
  discountCodesUpdateAction: CartMachineAction;
251
272
  onCartActionEntry?: CartMachineAction;
273
+ onCartActionOptimisticUI?: StateMachine.AssignActionObject<CartMachineContext, CartMachineEvent>;
252
274
  onCartActionComplete?: CartMachineAction;
253
275
  };
@@ -1,16 +1,21 @@
1
1
  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
- export declare function useCartAPIStateMachine({ numCartLines, onCartActionEntry, onCartActionComplete, data, cartFragment, }: {
4
+ import { CountryCode } from '../../storefront-api-types.js';
5
+ export declare function useCartAPIStateMachine({ numCartLines, onCartActionEntry, onCartActionOptimisticUI, onCartActionComplete, data, cartFragment, countryCode, }: {
5
6
  /** Maximum number of cart lines to fetch. Defaults to 250 cart lines. */
6
7
  numCartLines?: number;
7
8
  /** A callback that is invoked just before a Cart API action executes. */
8
9
  onCartActionEntry?: (context: CartMachineContext, event: CartMachineActionEvent) => void;
10
+ /** A callback that is invoked after executing the entry actions for optimistic UI changes. */
11
+ onCartActionOptimisticUI?: (context: CartMachineContext, event: CartMachineEvent) => Partial<CartMachineContext>;
9
12
  /** A callback that is invoked after a Cart API completes. */
10
13
  onCartActionComplete?: (context: CartMachineContext, event: CartMachineFetchResultEvent) => void;
11
14
  /** An object with fields that correspond to the Storefront API's [Cart object](https://shopify.dev/api/storefront/latest/objects/cart). */
12
15
  data?: CartFragmentFragment;
13
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. */
14
- cartFragment?: string;
15
- }): 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>];
17
+ cartFragment: string;
18
+ /** The ISO country code for i18n. */
19
+ countryCode?: CountryCode;
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>];
16
21
  export declare function cartFromGraphQL(cart: CartFragmentFragment): Cart;
@@ -5,28 +5,41 @@ import { useCartActions } from './CartActions.client.js';
5
5
  import { useMemo } from 'react';
6
6
  function invokeCart(action, options) {
7
7
  return {
8
- entry: [...(options?.entryActions || []), 'onCartActionEntry', action],
8
+ entry: [
9
+ ...(options?.entryActions || []),
10
+ 'onCartActionEntry',
11
+ 'onCartActionOptimisticUI',
12
+ action,
13
+ ],
9
14
  on: {
10
15
  RESOLVE: {
11
16
  target: options?.resolveTarget || 'idle',
12
17
  actions: [
13
18
  assign({
19
+ prevCart: (context) => context?.cart,
14
20
  cart: (_, event) => event?.payload?.cart,
15
- errors: (_, event) => undefined,
21
+ rawCartResult: (_, event) => event?.payload?.rawCartResult,
22
+ errors: (_) => undefined,
16
23
  }),
17
24
  ],
18
25
  },
19
26
  ERROR: {
20
27
  target: options?.errorTarget || 'error',
21
- actions: assign({
22
- errors: (_, event) => event?.payload?.errors,
23
- }),
28
+ actions: [
29
+ assign({
30
+ prevCart: (context) => context?.cart,
31
+ cart: (context, _) => context?.lastValidCart,
32
+ errors: (_, event) => event?.payload?.errors,
33
+ }),
34
+ ],
24
35
  },
25
36
  CART_COMPLETED: {
26
37
  target: 'cartCompleted',
27
38
  actions: assign({
28
- cart: (_, event) => undefined,
29
- errors: (_, event) => undefined,
39
+ prevCart: (_) => undefined,
40
+ cart: (_) => undefined,
41
+ lastValidCart: (_) => undefined,
42
+ errors: (_) => undefined,
30
43
  }),
31
44
  },
32
45
  },
@@ -40,8 +53,14 @@ const INITIALIZING_CART_EVENTS = {
40
53
  CART_CREATE: {
41
54
  target: 'cartCreating',
42
55
  },
43
- CARTLINE_ADD: {
44
- target: 'cartCreating',
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
+ ],
45
64
  },
46
65
  };
47
66
  const UPDATING_CART_EVENTS = {
@@ -81,10 +100,10 @@ const cartMachine = createMachine({
81
100
  on: INITIALIZING_CART_EVENTS,
82
101
  },
83
102
  idle: {
84
- on: UPDATING_CART_EVENTS,
103
+ on: { ...INITIALIZING_CART_EVENTS, ...UPDATING_CART_EVENTS },
85
104
  },
86
105
  error: {
87
- on: UPDATING_CART_EVENTS,
106
+ on: { ...INITIALIZING_CART_EVENTS, ...UPDATING_CART_EVENTS },
88
107
  },
89
108
  cartFetching: invokeCart('cartFetchAction', {
90
109
  errorTarget: 'initializationError',
@@ -101,10 +120,11 @@ const cartMachine = createMachine({
101
120
  discountCodesUpdating: invokeCart('discountCodesUpdateAction'),
102
121
  },
103
122
  });
104
- export function useCartAPIStateMachine({ numCartLines, onCartActionEntry, onCartActionComplete, data: cart, cartFragment, }) {
123
+ export function useCartAPIStateMachine({ numCartLines, onCartActionEntry, onCartActionOptimisticUI, onCartActionComplete, data: cart, cartFragment, countryCode, }) {
105
124
  const { cartFetch, cartCreate, cartLineAdd, cartLineUpdate, cartLineRemove, noteUpdate, buyerIdentityUpdate, cartAttributesUpdate, discountCodesUpdate, } = useCartActions({
106
125
  numCartLines,
107
126
  cartFragment,
127
+ countryCode,
108
128
  });
109
129
  const [state, send, service] = useMachine(cartMachine, {
110
130
  actions: {
@@ -116,7 +136,7 @@ export function useCartAPIStateMachine({ numCartLines, onCartActionEntry, onCart
116
136
  send(resultEvent);
117
137
  },
118
138
  cartCreateAction: async (_, event) => {
119
- if (event.type !== 'CART_CREATE' && event.type !== 'CARTLINE_ADD')
139
+ if (event.type !== 'CART_CREATE')
120
140
  return;
121
141
  const { data, errors } = await cartCreate(event?.payload);
122
142
  const resultEvent = eventFromFetchResult(event, data?.cartCreate?.cart, errors);
@@ -178,6 +198,11 @@ export function useCartAPIStateMachine({ numCartLines, onCartActionEntry, onCart
178
198
  }
179
199
  },
180
200
  }),
201
+ ...(onCartActionOptimisticUI && {
202
+ onCartActionOptimisticUI: assign((context, event) => {
203
+ return onCartActionOptimisticUI(context, event);
204
+ }),
205
+ }),
181
206
  ...(onCartActionComplete && {
182
207
  onCartActionComplete: (context, event) => {
183
208
  if (isCartFetchResultEvent(event)) {
@@ -211,7 +236,11 @@ function eventFromFetchResult(cartActionEvent, cart, errors) {
211
236
  }
212
237
  return {
213
238
  type: 'RESOLVE',
214
- payload: { cart: cartFromGraphQL(cart), cartActionEvent },
239
+ payload: {
240
+ cart: cartFromGraphQL(cart),
241
+ rawCartResult: cart,
242
+ cartActionEvent,
243
+ },
215
244
  };
216
245
  }
217
246
  function isCartActionEvent(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
  }
@@ -5,24 +5,25 @@ let secretTokenWarned = false;
5
5
  let storefrontIdWarned = false;
6
6
  export function getStorefrontApiRequestHeaders({ buyerIp, publicStorefrontToken, privateStorefrontToken, storefrontId, }) {
7
7
  const headers = {};
8
- if (!privateStorefrontToken && !secretTokenWarned) {
9
- secretTokenWarned = true;
8
+ if (!privateStorefrontToken) {
10
9
  privateStorefrontToken = getOxygenVariable(OXYGEN_SECRET_TOKEN_ENVIRONMENT_VARIABLE);
11
- if (!privateStorefrontToken && !__HYDROGEN_DEV__) {
12
- log.error('No secret Shopify storefront API token was defined. This means your app will be rate limited!\nSee how to add the token: ');
13
- }
14
- else if (privateStorefrontToken) {
15
- 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: ');
10
+ if (!secretTokenWarned) {
11
+ secretTokenWarned = true;
12
+ if (!privateStorefrontToken && !__HYDROGEN_DEV__) {
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
+ }
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: https://shopify.dev/custom-storefronts/hydrogen/framework/hydrogen-config');
17
+ }
16
18
  }
17
19
  }
18
- if (!storefrontId && !storefrontIdWarned) {
19
- storefrontIdWarned = true;
20
+ if (!storefrontId) {
20
21
  storefrontId = getOxygenVariable(SHOPIFY_STOREFRONT_ID_VARIABLE);
21
- if (!storefrontId && !__HYDROGEN_DEV__) {
22
- log.warn('No storefrontId was defined. This means the analytics on your admin dashboard will be broken!\nSee how to fix it: ');
23
- }
24
- else 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: ');
22
+ if (!storefrontIdWarned) {
23
+ storefrontIdWarned = true;
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');
26
+ }
26
27
  }
27
28
  }
28
29
  /**
@@ -1 +1 @@
1
- export declare const LIB_VERSION = "1.4.0";
1
+ export declare const LIB_VERSION = "1.4.2";
@@ -1 +1 @@
1
- export const LIB_VERSION = '1.4.0';
1
+ export const LIB_VERSION = '1.4.2';
package/package.json CHANGED
@@ -7,7 +7,7 @@
7
7
  "engines": {
8
8
  "node": ">=14"
9
9
  },
10
- "version": "1.4.0",
10
+ "version": "1.4.2",
11
11
  "description": "Modern custom Shopify storefronts",
12
12
  "license": "MIT",
13
13
  "main": "dist/esnext/index.js",