@tiledev/sdk-shopify 0.9.1 → 0.10.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 (146) hide show
  1. package/README.md +653 -50
  2. package/dist/alertSettings.d.ts.map +1 -1
  3. package/dist/alertSettings.js +2 -0
  4. package/dist/alertSettings.js.map +1 -1
  5. package/dist/attribution.d.ts +21 -0
  6. package/dist/attribution.d.ts.map +1 -1
  7. package/dist/attribution.js +17 -0
  8. package/dist/attribution.js.map +1 -1
  9. package/dist/auth/pkce.d.ts +30 -0
  10. package/dist/auth/pkce.d.ts.map +1 -0
  11. package/dist/auth/pkce.js +128 -0
  12. package/dist/auth/pkce.js.map +1 -0
  13. package/dist/auth/session.d.ts +118 -0
  14. package/dist/auth/session.d.ts.map +1 -0
  15. package/dist/auth/session.js +896 -0
  16. package/dist/auth/session.js.map +1 -0
  17. package/dist/auth/storeCredit.d.ts +64 -0
  18. package/dist/auth/storeCredit.d.ts.map +1 -0
  19. package/dist/auth/storeCredit.js +274 -0
  20. package/dist/auth/storeCredit.js.map +1 -0
  21. package/dist/cart.d.ts +25 -1
  22. package/dist/cart.d.ts.map +1 -1
  23. package/dist/cart.js +95 -2
  24. package/dist/cart.js.map +1 -1
  25. package/dist/cartPolicy.js +4 -4
  26. package/dist/checkout.d.ts +123 -0
  27. package/dist/checkout.d.ts.map +1 -0
  28. package/dist/checkout.js +205 -0
  29. package/dist/checkout.js.map +1 -0
  30. package/dist/customer.d.ts.map +1 -1
  31. package/dist/customer.js +8 -0
  32. package/dist/customer.js.map +1 -1
  33. package/dist/index.d.ts +12 -4
  34. package/dist/index.d.ts.map +1 -1
  35. package/dist/index.js +74 -2
  36. package/dist/index.js.map +1 -1
  37. package/dist/messages.d.ts.map +1 -1
  38. package/dist/messages.js +2 -0
  39. package/dist/messages.js.map +1 -1
  40. package/dist/money.d.ts +16 -0
  41. package/dist/money.d.ts.map +1 -1
  42. package/dist/money.js +55 -0
  43. package/dist/money.js.map +1 -1
  44. package/dist/orders.d.ts +99 -0
  45. package/dist/orders.d.ts.map +1 -0
  46. package/dist/orders.js +439 -0
  47. package/dist/orders.js.map +1 -0
  48. package/dist/productPage.d.ts +100 -0
  49. package/dist/productPage.d.ts.map +1 -0
  50. package/dist/productPage.js +298 -0
  51. package/dist/productPage.js.map +1 -0
  52. package/dist/productStore.d.ts +1 -1
  53. package/dist/productStore.d.ts.map +1 -1
  54. package/dist/productStore.js +63 -5
  55. package/dist/productStore.js.map +1 -1
  56. package/dist/products.d.ts.map +1 -1
  57. package/dist/products.js +15 -3
  58. package/dist/products.js.map +1 -1
  59. package/dist/purchaseRules.d.ts +74 -0
  60. package/dist/purchaseRules.d.ts.map +1 -0
  61. package/dist/purchaseRules.js +73 -0
  62. package/dist/purchaseRules.js.map +1 -0
  63. package/dist/queries.d.ts +15 -13
  64. package/dist/queries.d.ts.map +1 -1
  65. package/dist/queries.js +43 -7
  66. package/dist/queries.js.map +1 -1
  67. package/dist/react/ShopifyProvider.d.ts +167 -25
  68. package/dist/react/ShopifyProvider.d.ts.map +1 -1
  69. package/dist/react/ShopifyProvider.js +352 -207
  70. package/dist/react/ShopifyProvider.js.map +1 -1
  71. package/dist/react/appForeground.d.ts +6 -0
  72. package/dist/react/appForeground.d.ts.map +1 -0
  73. package/dist/react/appForeground.js +19 -0
  74. package/dist/react/appForeground.js.map +1 -0
  75. package/dist/react/appForeground.native.d.ts +2 -0
  76. package/dist/react/appForeground.native.d.ts.map +1 -0
  77. package/dist/react/appForeground.native.js +23 -0
  78. package/dist/react/appForeground.native.js.map +1 -0
  79. package/dist/react/index.d.ts +7 -1
  80. package/dist/react/index.d.ts.map +1 -1
  81. package/dist/react/index.js +23 -1
  82. package/dist/react/index.js.map +1 -1
  83. package/dist/react/tileCreditClient.d.ts +4 -0
  84. package/dist/react/tileCreditClient.d.ts.map +1 -0
  85. package/dist/react/tileCreditClient.js +27 -0
  86. package/dist/react/tileCreditClient.js.map +1 -0
  87. package/dist/react/useAppDiscountCode.d.ts +19 -0
  88. package/dist/react/useAppDiscountCode.d.ts.map +1 -0
  89. package/dist/react/useAppDiscountCode.js +44 -0
  90. package/dist/react/useAppDiscountCode.js.map +1 -0
  91. package/dist/react/useCartStoreCredit.d.ts +25 -0
  92. package/dist/react/useCartStoreCredit.d.ts.map +1 -0
  93. package/dist/react/useCartStoreCredit.js +292 -0
  94. package/dist/react/useCartStoreCredit.js.map +1 -0
  95. package/dist/react/useOrders.d.ts +104 -0
  96. package/dist/react/useOrders.d.ts.map +1 -0
  97. package/dist/react/useOrders.js +287 -0
  98. package/dist/react/useOrders.js.map +1 -0
  99. package/dist/react/useProduct.d.ts.map +1 -1
  100. package/dist/react/useProduct.js +1 -9
  101. package/dist/react/useProduct.js.map +1 -1
  102. package/dist/react/useProductFeed.d.ts.map +1 -1
  103. package/dist/react/useProductFeed.js +33 -5
  104. package/dist/react/useProductFeed.js.map +1 -1
  105. package/dist/react/useProductPage.d.ts +148 -0
  106. package/dist/react/useProductPage.d.ts.map +1 -0
  107. package/dist/react/useProductPage.js +178 -0
  108. package/dist/react/useProductPage.js.map +1 -0
  109. package/dist/react/useProductRecommendations.d.ts +27 -0
  110. package/dist/react/useProductRecommendations.d.ts.map +1 -0
  111. package/dist/react/useProductRecommendations.js +76 -0
  112. package/dist/react/useProductRecommendations.js.map +1 -0
  113. package/dist/react/useStoreCredit.d.ts +58 -0
  114. package/dist/react/useStoreCredit.d.ts.map +1 -0
  115. package/dist/react/useStoreCredit.js +209 -0
  116. package/dist/react/useStoreCredit.js.map +1 -0
  117. package/dist/react/useTileCredit.d.ts +4 -3
  118. package/dist/react/useTileCredit.d.ts.map +1 -1
  119. package/dist/react/useTileCredit.js +63 -55
  120. package/dist/react/useTileCredit.js.map +1 -1
  121. package/dist/shopify.d.ts.map +1 -1
  122. package/dist/shopify.js +2 -0
  123. package/dist/shopify.js.map +1 -1
  124. package/dist/storedList.d.ts +27 -0
  125. package/dist/storedList.d.ts.map +1 -0
  126. package/dist/storedList.js +152 -0
  127. package/dist/storedList.js.map +1 -0
  128. package/dist/tileCredit.d.ts +25 -10
  129. package/dist/tileCredit.d.ts.map +1 -1
  130. package/dist/tileCredit.js +97 -45
  131. package/dist/tileCredit.js.map +1 -1
  132. package/dist/types.d.ts +407 -34
  133. package/dist/types.d.ts.map +1 -1
  134. package/dist/types.js +1 -2
  135. package/dist/types.js.map +1 -1
  136. package/dist/variants.d.ts.map +1 -1
  137. package/dist/variants.js +4 -3
  138. package/dist/variants.js.map +1 -1
  139. package/dist/waitlist.d.ts +4 -0
  140. package/dist/waitlist.d.ts.map +1 -0
  141. package/dist/waitlist.js +213 -0
  142. package/dist/waitlist.js.map +1 -0
  143. package/dist/wishlist.d.ts.map +1 -1
  144. package/dist/wishlist.js +70 -47
  145. package/dist/wishlist.js.map +1 -1
  146. package/package.json +2 -2
@@ -4,18 +4,24 @@ exports.ShopifyProvider = ShopifyProvider;
4
4
  exports.useShopify = useShopify;
5
5
  exports.useCart = useCart;
6
6
  exports.useWishlist = useWishlist;
7
+ exports.useWaitlist = useWaitlist;
7
8
  exports.useCustomer = useCustomer;
8
9
  exports.useCheckout = useCheckout;
10
+ exports.useShopifyEvents = useShopifyEvents;
9
11
  exports.useShopifyMessage = useShopifyMessage;
10
12
  const jsx_runtime_1 = require("react/jsx-runtime");
11
13
  const react_1 = require("react");
12
14
  const shopify_1 = require("../shopify");
13
15
  const client_1 = require("../client");
14
16
  const cart_1 = require("../cart");
17
+ const checkout_1 = require("../checkout");
15
18
  const attributionRecorder_1 = require("./attributionRecorder");
16
19
  const cartPolicy_1 = require("../cartPolicy");
20
+ const productPage_1 = require("../productPage");
17
21
  const errors_1 = require("../errors");
18
22
  const messages_1 = require("../messages");
23
+ const types_1 = require("../types");
24
+ const session_1 = require("../auth/session");
19
25
  /**
20
26
  * Shopify rejecting the *contents* of a mutation rather than failing to answer it: only
21
27
  * `userErrors` populate `ShopifyError.errors`, so transport and GraphQL failures arrive empty.
@@ -40,7 +46,6 @@ function lineFor(cart, input) {
40
46
  }
41
47
  const ShopifyContext = (0, react_1.createContext)(undefined);
42
48
  const CART_STORAGE_KEY = "shopify:cart-id:v1";
43
- const CUSTOMER_TOKEN_KEY = "shopify:customer-token:v1";
44
49
  const CART_LINES_KEY = "shopify:cart-lines:v1";
45
50
  const CHECKOUT_STARTED_KEY = "shopify:checkout-started-cart-id:v1";
46
51
  // Best-effort: the cart write that triggered it has already landed.
@@ -130,14 +135,34 @@ async function createFromSnapshot(lines, attributes) {
130
135
  }
131
136
  return next;
132
137
  }
138
+ /**
139
+ * What an add changed: each line the inputs landed on, once, with the units added to it (two
140
+ * inputs that landed on one line, same variant and attributes, are counted together).
141
+ */
142
+ function changesForAdds(cart, inputs) {
143
+ const changes = [];
144
+ for (const input of inputs) {
145
+ const line = lineFor(cart, input);
146
+ if (!line)
147
+ continue;
148
+ const sameLine = changes.find((change) => change.line.id === line.id);
149
+ if (sameLine)
150
+ sameLine.quantityChange += input.quantity;
151
+ else
152
+ changes.push({ line, quantityChange: input.quantity });
153
+ }
154
+ return changes;
155
+ }
133
156
  /** Alert-carrying events and the message key each resolves. */
134
157
  const EVENT_MESSAGE = {
135
158
  "cart:add": "cart.added",
136
159
  "cart:remove": "cart.removed",
137
160
  "cart:limitExceeded": "cart.limitExceeded",
161
+ "cart:stockLimit": "cart.noMoreStock",
138
162
  "cart:outOfStock": "cart.outOfStock",
139
163
  "wishlist:add": "wishlist.added",
140
164
  "wishlist:remove": "wishlist.removed",
165
+ "waitlist:add": "waitlist.added",
141
166
  "auth:loginSuccess": "auth.loginSuccess",
142
167
  "auth:loginFailed": "auth.loginFailed",
143
168
  "auth:logout": "auth.loggedOut",
@@ -147,6 +172,7 @@ const EVENT_MESSAGE = {
147
172
  };
148
173
  const ERROR_EVENTS = new Set([
149
174
  "cart:limitExceeded",
175
+ "cart:stockLimit",
150
176
  "cart:outOfStock",
151
177
  "auth:loginFailed",
152
178
  "checkout:paymentFailed",
@@ -157,17 +183,19 @@ function defaultStorage() {
157
183
  }
158
184
  return null;
159
185
  }
160
- function ShopifyProvider({ children, config, storage, onEvent, cartGuard, wishlistKeepDeleted, messages, translate, cartPolicy, cartAttributes, attribution, }) {
186
+ function ShopifyProvider({ children, config, storage, onEvent, cartGuard, wishlistKeepDeleted, wishlistStorageKey, wishlistMigrateFrom, waitlistStorageKey, waitlistMigrateFrom, messages, translate, cartPolicy, cartAttributes, auth, storeCredit, attribution, }) {
187
+ var _a, _b;
161
188
  const [ready, setReady] = (0, react_1.useState)(false);
162
189
  const [error, setError] = (0, react_1.useState)(null);
163
190
  const [cart, setCart] = (0, react_1.useState)(null);
191
+ // The cart as of the newest write, for a write that waited in the queue behind others.
192
+ const latestCart = (0, react_1.useRef)(null);
193
+ latestCart.current = cart;
164
194
  const [cartLoading, setCartLoad] = (0, react_1.useState)(false);
165
195
  const [wlItems, setWlItems] = (0, react_1.useState)([]);
166
- const [customer, setCustomer] = (0, react_1.useState)(null);
167
- const [token, setToken] = (0, react_1.useState)(null);
168
- const [authLoading, setAuthLoad] = (0, react_1.useState)(false);
169
- const [restoring, setRestoring] = (0, react_1.useState)(true);
170
196
  const wlUnsubRef = (0, react_1.useRef)(null);
197
+ const [wtItems, setWtItems] = (0, react_1.useState)([]);
198
+ const wtUnsubRef = (0, react_1.useRef)(null);
171
199
  const storageRef = (0, react_1.useRef)(null);
172
200
  storageRef.current = storage !== null && storage !== void 0 ? storage : defaultStorage();
173
201
  // In a ref so the cart callbacks keep their identity when the host passes a new guard object.
@@ -177,16 +205,23 @@ function ShopifyProvider({ children, config, storage, onEvent, cartGuard, wishli
177
205
  // every cart callback on each render.
178
206
  const onEventRef = (0, react_1.useRef)(onEvent);
179
207
  onEventRef.current = onEvent;
208
+ /** Listeners added with `useShopifyEvents`, told after `onEvent`, in the order they subscribed. */
209
+ const eventListeners = (0, react_1.useRef)(new Set());
210
+ /** Declared before `emit`, which reads the signed-in customer from it for `auth:loginSuccess`. */
211
+ const sessionRef = (0, react_1.useRef)(null);
180
212
  /**
181
- * Resolves the copy for `type` and hands the event to the host. A throwing
182
- * listener must not fail the write that triggered it — the mutation already
183
- * landed, so swallowing here is the only honest option.
213
+ * Resolves the copy for `type` and hands the event to the host's `onEvent`, then to every
214
+ * `useShopifyEvents` listener. A throwing listener must not fail the write that triggered it —
215
+ * the mutation already landed, so swallowing here is the only honest option.
184
216
  */
185
- const emit = (0, react_1.useCallback)((type, error) => {
217
+ const emit = (0, react_1.useCallback)((type, details) => {
218
+ var _a;
186
219
  const listener = onEventRef.current;
187
- if (!listener)
220
+ if (!listener && eventListeners.current.size === 0)
188
221
  return;
189
222
  const key = EVENT_MESSAGE[type];
223
+ // The session has already taken the new customer when it announces the sign-in.
224
+ const signedIn = type === "auth:loginSuccess" ? (_a = sessionRef.current) === null || _a === void 0 ? void 0 : _a.getState() : undefined;
190
225
  const max = (0, cartPolicy_1.maxLineItems)();
191
226
  const event = {
192
227
  type,
@@ -199,15 +234,61 @@ function ShopifyProvider({ children, config, storage, onEvent, cartGuard, wishli
199
234
  : (0, messages_1.message)(key),
200
235
  }
201
236
  : {}),
202
- ...(error === undefined ? {} : { error }),
237
+ ...((details === null || details === void 0 ? void 0 : details.error) === undefined ? {} : { error: details.error }),
238
+ ...((details === null || details === void 0 ? void 0 : details.cart) ? { cart: details.cart } : {}),
239
+ ...((details === null || details === void 0 ? void 0 : details.changedLines) ? { changedLines: details.changedLines } : {}),
240
+ ...((details === null || details === void 0 ? void 0 : details.productId) ? { productId: details.productId } : {}),
241
+ ...(signedIn ? { customer: signedIn.customer, sessionKind: signedIn.kind } : {}),
203
242
  };
204
- try {
205
- listener(event);
243
+ if (listener) {
244
+ try {
245
+ listener(event);
246
+ }
247
+ catch (listenerError) {
248
+ console.warn("[ShopifyProvider] onEvent threw", listenerError);
249
+ }
206
250
  }
207
- catch (listenerError) {
208
- console.warn("[ShopifyProvider] onEvent threw", listenerError);
251
+ for (const eventListener of [...eventListeners.current]) {
252
+ try {
253
+ eventListener(event);
254
+ }
255
+ catch (listenerError) {
256
+ console.warn("[ShopifyProvider] a useShopifyEvents listener threw", listenerError);
257
+ }
209
258
  }
210
259
  }, []);
260
+ const subscribeToEvents = (0, react_1.useCallback)((eventListener) => {
261
+ eventListeners.current.add(eventListener);
262
+ return () => {
263
+ eventListeners.current.delete(eventListener);
264
+ };
265
+ }, []);
266
+ const events = (0, react_1.useMemo)(() => ({ subscribe: subscribeToEvents }), [subscribeToEvents]);
267
+ // ── Customer session ────────────────────────────────────────────────────────
268
+ // One per provider, created on first render. `auth` is read through a ref on every use, so a Live
269
+ // Layer publish that flips `auth.method` applies at once and leaves the current session alone.
270
+ const authRef = (0, react_1.useRef)(auth);
271
+ authRef.current = auth;
272
+ const configRef = (0, react_1.useRef)(config);
273
+ configRef.current = config;
274
+ if (!sessionRef.current) {
275
+ sessionRef.current = (0, session_1.createCustomerSession)({
276
+ storage: () => { var _a, _b; return (_b = (_a = authRef.current) === null || _a === void 0 ? void 0 : _a.secureStorage) !== null && _b !== void 0 ? _b : storageRef.current; },
277
+ legacyStorage: () => storageRef.current,
278
+ options: () => authRef.current,
279
+ apiVersion: () => configRef.current.apiVersion,
280
+ emit: (type, error) => emit(type, error === undefined ? undefined : { error }),
281
+ });
282
+ }
283
+ const session = sessionRef.current;
284
+ const [sessionState, setSessionState] = (0, react_1.useState)(session.getState);
285
+ (0, react_1.useEffect)(() => {
286
+ setSessionState(session.getState());
287
+ const unsubscribe = session.subscribe(() => setSessionState(session.getState()));
288
+ // Not gated on the shop loading: a stored session is restored offline too.
289
+ void session.restore();
290
+ return unsubscribe;
291
+ }, [session]);
211
292
  /**
212
293
  * Alert copy and cart rules, re-applied whenever the host passes new ones so a
213
294
  * Live Layer publish lands without a re-init. `config` values are the base;
@@ -305,16 +386,19 @@ function ShopifyProvider({ children, config, storage, onEvent, cartGuard, wishli
305
386
  return fallback;
306
387
  }
307
388
  }, []);
308
- const approveAdd = (0, react_1.useCallback)(async (input) => {
389
+ const approveAdd = (0, react_1.useCallback)(async (input, options) => {
309
390
  const guard = guardRef.current;
310
391
  if (!(guard === null || guard === void 0 ? void 0 : guard.beforeAdd))
311
392
  return input;
312
- const approved = await runGuard(() => guard.beforeAdd(input), input);
393
+ const approved = await runGuard(() => guard.beforeAdd(input, options), input);
313
394
  // A guard that rebuilds the input must not lose where it came from.
314
395
  return approved && !approved.source && input.source ? { ...approved, source: input.source } : approved;
315
396
  }, [runGuard]);
316
- /** Hands back units the guard approved for a write that then failed. */
317
- const releaseRejected = (0, react_1.useCallback)((input) => {
397
+ /**
398
+ * Hands back units the guard approved for a write that then didn't land. An add passes its approved
399
+ * input (the guard's receipt rides on its attributes); an increase passes the line it was to grow.
400
+ */
401
+ const releaseRejected = (0, react_1.useCallback)((input, line = null) => {
318
402
  const guard = guardRef.current;
319
403
  if (!(guard === null || guard === void 0 ? void 0 : guard.onReleased))
320
404
  return;
@@ -322,7 +406,8 @@ function ShopifyProvider({ children, config, storage, onEvent, cartGuard, wishli
322
406
  variantId: input.merchandiseId,
323
407
  quantity: input.quantity,
324
408
  reason: "rejected",
325
- line: null,
409
+ line,
410
+ ...(line ? {} : { input }),
326
411
  cart: null,
327
412
  }), undefined);
328
413
  }, [runGuard]);
@@ -340,10 +425,46 @@ function ShopifyProvider({ children, config, storage, onEvent, cartGuard, wishli
340
425
  (0, react_1.useEffect)(() => {
341
426
  let mounted = true;
342
427
  (async () => {
343
- var _a;
428
+ var _a, _b;
344
429
  try {
345
430
  await shopify_1.shopify.init(config);
346
431
  reapplyAlertConfig();
432
+ // The wishlist first, on its own: it is on the device, so it loads with no network, and a
433
+ // cart that can't load (offline) must not take it down with it.
434
+ try {
435
+ const initialItems = await shopify_1.shopify.wishlist.init({
436
+ storage: (_a = storageRef.current) !== null && _a !== void 0 ? _a : undefined,
437
+ storageKey: wishlistStorageKey,
438
+ migrateFrom: wishlistMigrateFrom,
439
+ keepDeleted: wishlistKeepDeleted,
440
+ });
441
+ if (mounted)
442
+ setWlItems(initialItems);
443
+ wlUnsubRef.current = shopify_1.shopify.wishlist.onChange((items) => {
444
+ if (mounted)
445
+ setWlItems(items);
446
+ });
447
+ }
448
+ catch (e) {
449
+ console.error("[ShopifyProvider] wishlist init failed", e);
450
+ }
451
+ // The waitlist the same way, for the same reason.
452
+ try {
453
+ const initialWaitlist = await shopify_1.shopify.waitlist.init({
454
+ storage: (_b = storageRef.current) !== null && _b !== void 0 ? _b : undefined,
455
+ storageKey: waitlistStorageKey,
456
+ migrateFrom: waitlistMigrateFrom,
457
+ });
458
+ if (mounted)
459
+ setWtItems(initialWaitlist);
460
+ wtUnsubRef.current = shopify_1.shopify.waitlist.onChange((items) => {
461
+ if (mounted)
462
+ setWtItems(items);
463
+ });
464
+ }
465
+ catch (e) {
466
+ console.error("[ShopifyProvider] waitlist init failed", e);
467
+ }
347
468
  setCartLoad(true);
348
469
  try {
349
470
  const s = storageRef.current;
@@ -373,40 +494,6 @@ function ShopifyProvider({ children, config, storage, onEvent, cartGuard, wishli
373
494
  if (mounted)
374
495
  setCartLoad(false);
375
496
  }
376
- const initialItems = await shopify_1.shopify.wishlist.init({
377
- storage: (_a = storageRef.current) !== null && _a !== void 0 ? _a : undefined,
378
- keepDeleted: wishlistKeepDeleted,
379
- });
380
- if (mounted)
381
- setWlItems(initialItems);
382
- wlUnsubRef.current = shopify_1.shopify.wishlist.onChange((items) => {
383
- if (mounted)
384
- setWlItems(items);
385
- });
386
- // Restore a stored session. A token that no longer resolves is dropped
387
- // silently — an expired login is not a failed one, and firing
388
- // `auth:loginFailed` on app open would toast at a shopper who did nothing.
389
- try {
390
- const s = storageRef.current;
391
- const savedToken = s ? await Promise.resolve(s.getItem(CUSTOMER_TOKEN_KEY)) : null;
392
- if (savedToken) {
393
- const profile = await shopify_1.shopify.customer.profile(savedToken);
394
- if (mounted && profile) {
395
- setCustomer(profile);
396
- setToken(savedToken);
397
- }
398
- else if (s && !profile) {
399
- await Promise.resolve(s.removeItem(CUSTOMER_TOKEN_KEY));
400
- }
401
- }
402
- }
403
- catch (sessionError) {
404
- console.warn("[ShopifyProvider] session restore failed", sessionError);
405
- }
406
- finally {
407
- if (mounted)
408
- setRestoring(false);
409
- }
410
497
  if (mounted)
411
498
  setReady(true);
412
499
  }
@@ -414,16 +501,16 @@ function ShopifyProvider({ children, config, storage, onEvent, cartGuard, wishli
414
501
  console.error("[ShopifyProvider] init failed", e);
415
502
  if (mounted) {
416
503
  setError(e instanceof Error ? e.message : String(e));
417
- setRestoring(false);
418
504
  // Ready so downstream UIs render an error state rather than an indefinite spinner.
419
505
  setReady(true);
420
506
  }
421
507
  }
422
508
  })();
423
509
  return () => {
424
- var _a;
510
+ var _a, _b;
425
511
  mounted = false;
426
512
  (_a = wlUnsubRef.current) === null || _a === void 0 ? void 0 : _a.call(wlUnsubRef);
513
+ (_b = wtUnsubRef.current) === null || _b === void 0 ? void 0 : _b.call(wtUnsubRef);
427
514
  };
428
515
  // eslint-disable-next-line react-hooks/exhaustive-deps
429
516
  }, []);
@@ -494,6 +581,7 @@ function ShopifyProvider({ children, config, storage, onEvent, cartGuard, wishli
494
581
  }
495
582
  }, [config.country]);
496
583
  const persistCart = (0, react_1.useCallback)(async (next) => {
584
+ latestCart.current = next;
497
585
  setCart(next);
498
586
  cartRef.current = next;
499
587
  const synced = recorder.sync(next);
@@ -523,18 +611,28 @@ function ShopifyProvider({ children, config, storage, onEvent, cartGuard, wishli
523
611
  */
524
612
  const reportCartFailure = (0, react_1.useCallback)((failure) => {
525
613
  if ((0, errors_1.isOutOfStockError)(failure)) {
526
- emit("cart:outOfStock", failure);
614
+ emit("cart:outOfStock", { error: failure });
527
615
  return true;
528
616
  }
529
617
  return false;
530
618
  }, [emit]);
531
619
  const cartAddLine = (0, react_1.useCallback)(async (input) => {
620
+ // The variant's stock, when the caller knows it: Shopify would accept the
621
+ // add and clamp it silently, so the shopper would be told it worked. Before
622
+ // the guard, so a guard that reserves stock (Cart Hold) never reserves for an
623
+ // add that is then refused.
624
+ if (input.maxQuantity != null && !(0, productPage_1.withinCeiling)(input.maxQuantity, (0, productPage_1.quantityInCart)(cart, input.merchandiseId), input.quantity)) {
625
+ emit("cart:stockLimit");
626
+ return { ok: false, reason: "stock", message: (0, messages_1.message)("cart.noMoreStock"), cart };
627
+ }
532
628
  const approved = await approveAdd(input);
533
629
  if (!approved)
534
630
  return { ok: false, reason: "guard", cart };
535
631
  // Checked before the write so a refusal costs no round trip, and so the
536
632
  // shopper reads the configured message rather than a Shopify error.
537
633
  if ((0, cartPolicy_1.wouldExceedLineLimit)(cart, [approved])) {
634
+ // The guard may have reserved stock for this add; it won't land, so that goes back.
635
+ releaseRejected(approved);
538
636
  emit("cart:limitExceeded");
539
637
  const max = (0, cartPolicy_1.maxLineItems)();
540
638
  return {
@@ -546,9 +644,10 @@ function ShopifyProvider({ children, config, storage, onEvent, cartGuard, wishli
546
644
  }
547
645
  setCartLoad(true);
548
646
  try {
549
- const id = await ensureCartId();
550
647
  let next;
551
648
  try {
649
+ // Inside the catch: a cart that can't be created is an add that didn't land.
650
+ const id = await ensureCartId();
552
651
  next = await serialize(() => shopify_1.shopify.cart.addLines(id, [approved]));
553
652
  }
554
653
  catch (addError) {
@@ -559,7 +658,7 @@ function ShopifyProvider({ children, config, storage, onEvent, cartGuard, wishli
559
658
  await persistCart(next);
560
659
  await clearCheckoutStarted(storageRef.current);
561
660
  recorder.addInput(next.id, approved);
562
- emit("cart:add");
661
+ emit("cart:add", { cart: next, changedLines: changesForAdds(next, [approved]) });
563
662
  announceLanded(approved, next);
564
663
  return { ok: true, cart: next };
565
664
  }
@@ -567,22 +666,31 @@ function ShopifyProvider({ children, config, storage, onEvent, cartGuard, wishli
567
666
  setCartLoad(false);
568
667
  }
569
668
  }, [cart, approveAdd, releaseRejected, announceLanded, ensureCartId, persistCart, emit, reportCartFailure, serialize, recorder]);
570
- const cartAddLines = (0, react_1.useCallback)(async (requested) => {
669
+ const cartAddLines = (0, react_1.useCallback)(async (requested, options) => {
571
670
  if (requested.length === 0)
572
671
  return cart;
573
672
  // Vetted up front so a guard's veto does not cost the other lines their fast path.
574
- const inputs = (await Promise.all(requested.map(approveAdd))).filter((input) => input !== null);
673
+ const inputs = (await Promise.all(requested.map((input) => approveAdd(input, options)))).filter((input) => input !== null);
575
674
  if (inputs.length === 0)
576
675
  return null;
577
676
  // The whole batch is judged against the limit, not line by line — landing
578
677
  // half a "add these 5" is worse than refusing it with the message.
579
678
  if ((0, cartPolicy_1.wouldExceedLineLimit)(cart, inputs)) {
679
+ inputs.forEach((input) => releaseRejected(input));
580
680
  emit("cart:limitExceeded");
581
681
  return null;
582
682
  }
583
683
  setCartLoad(true);
584
684
  try {
585
- const id = await ensureCartId();
685
+ let id;
686
+ try {
687
+ id = await ensureCartId();
688
+ }
689
+ catch (createError) {
690
+ inputs.forEach((input) => releaseRejected(input));
691
+ reportCartFailure(createError);
692
+ throw createError;
693
+ }
586
694
  let next = null;
587
695
  // Which inputs landed, so the guard hears about exactly those.
588
696
  let landed = [];
@@ -594,7 +702,7 @@ function ShopifyProvider({ children, config, storage, onEvent, cartGuard, wishli
594
702
  // A transport or GraphQL failure would fail all N the same way, so retrying it would turn
595
703
  // one dead request into N and still end at null. Only line rejections are worth retrying.
596
704
  if (!isLineRejection(batchError)) {
597
- inputs.forEach(releaseRejected);
705
+ inputs.forEach((input) => releaseRejected(input));
598
706
  reportCartFailure(batchError);
599
707
  throw batchError;
600
708
  }
@@ -613,16 +721,16 @@ function ShopifyProvider({ children, config, storage, onEvent, cartGuard, wishli
613
721
  anyOutOfStock = true;
614
722
  // Anything but a rejection means the retry itself is failing, so stop.
615
723
  if (!isLineRejection(lineError)) {
616
- inputs.slice(i + 1).forEach(releaseRejected);
724
+ inputs.slice(i + 1).forEach((rest) => releaseRejected(rest));
617
725
  reportCartFailure(lineError);
618
726
  throw lineError;
619
727
  }
620
728
  }
621
729
  }
622
730
  // One alert for the batch: N unsellable lines is one thing that went
623
- // wrong from the shopper's side, not N toasts.
624
- if (anyOutOfStock)
625
- emit("cart:outOfStock", batchError);
731
+ // wrong from the shopper's side, not N toasts. None when quiet: the caller reports it.
732
+ if (anyOutOfStock && !(options === null || options === void 0 ? void 0 : options.quiet))
733
+ emit("cart:outOfStock", { error: batchError });
626
734
  // Every line individually rejected. Surface the original rather than an empty success,
627
735
  // which reads as "nothing to add" instead of "none of this is sellable".
628
736
  if (landed.length === 0)
@@ -633,8 +741,8 @@ function ShopifyProvider({ children, config, storage, onEvent, cartGuard, wishli
633
741
  await clearCheckoutStarted(storageRef.current);
634
742
  const cartId = next.id;
635
743
  landed.forEach((input) => recorder.addInput(cartId, input));
636
- emit("cart:add");
637
744
  const settled = next;
745
+ emit("cart:add", { cart: settled, changedLines: changesForAdds(settled, landed) });
638
746
  landed.forEach((input) => announceLanded(input, settled));
639
747
  }
640
748
  return next;
@@ -659,20 +767,28 @@ function ShopifyProvider({ children, config, storage, onEvent, cartGuard, wishli
659
767
  }
660
768
  }, [cart, persistCart, emit, serialize]);
661
769
  const cartUpdateLine = (0, react_1.useCallback)(async (lineId, quantity, attributes, opts) => {
662
- var _a;
770
+ var _a, _b;
663
771
  if (!(cart === null || cart === void 0 ? void 0 : cart.id))
664
772
  return false;
665
773
  const guard = guardRef.current;
666
774
  const previous = (_a = cart.lines.find((line) => line.id === lineId)) !== null && _a !== void 0 ? _a : null;
667
775
  const delta = previous ? quantity - previous.quantity : 0;
776
+ // Attributes REPLACE the line's set, so the caller's new set keeps the line's private
777
+ // (`_`-prefixed) attributes it didn't mention: those belong to the app's integrations (Cart
778
+ // Hold's expiry stamp is one), not to whoever is editing the visible ones.
779
+ const kept = attributes && previous
780
+ ? previous.attributes.filter((old) => old.key.startsWith("_") && !attributes.some((next) => next.key === old.key))
781
+ : [];
782
+ const nextAttributes = attributes ? [...attributes, ...kept] : undefined;
668
783
  // Only an increase can be vetted; a decrease is reported after the fact, once Shopify has
669
784
  // actually taken the units back.
670
- let update = { id: lineId, quantity, ...(attributes ? { attributes } : {}) };
785
+ let update = { id: lineId, quantity, ...(nextAttributes ? { attributes: nextAttributes } : {}) };
671
786
  if (previous && delta > 0 && (guard === null || guard === void 0 ? void 0 : guard.beforeIncrease)) {
672
787
  const approved = await runGuard(() => guard.beforeIncrease(previous, quantity), update);
673
788
  if (!approved)
674
789
  return false;
675
- update = approved;
790
+ // The guard's answer wins, but attributes it didn't return are the caller's, not dropped.
791
+ update = { ...approved, ...(approved.attributes === undefined && nextAttributes ? { attributes: nextAttributes } : {}) };
676
792
  }
677
793
  setCartLoad(true);
678
794
  try {
@@ -682,7 +798,7 @@ function ShopifyProvider({ children, config, storage, onEvent, cartGuard, wishli
682
798
  }
683
799
  catch (updateError) {
684
800
  if (previous && delta > 0) {
685
- releaseRejected({ merchandiseId: previous.merchandise.id, quantity: delta });
801
+ releaseRejected({ merchandiseId: previous.merchandise.id, quantity: delta }, previous);
686
802
  }
687
803
  reportCartFailure(updateError);
688
804
  throw updateError;
@@ -692,7 +808,12 @@ function ShopifyProvider({ children, config, storage, onEvent, cartGuard, wishli
692
808
  recorder.changeLine(next.id, previous, previous ? update.quantity - previous.quantity : 0, opts === null || opts === void 0 ? void 0 : opts.source);
693
809
  // A quantity change is not one of the panel's alerts — emitted as a state
694
810
  // signal so a host can refresh a badge, with no copy attached.
695
- emit("cart:update");
811
+ // The line as it is now; gone at 0, so the line as it was.
812
+ const lineNow = (_b = next.lines.find((line) => line.id === lineId)) !== null && _b !== void 0 ? _b : previous;
813
+ emit("cart:update", {
814
+ cart: next,
815
+ changedLines: previous && lineNow ? [{ line: lineNow, quantityChange: update.quantity - previous.quantity }] : [],
816
+ });
696
817
  if (previous && delta < 0 && (guard === null || guard === void 0 ? void 0 : guard.onReleased)) {
697
818
  void runGuard(() => guard.onReleased({
698
819
  variantId: previous.merchandise.id,
@@ -722,7 +843,10 @@ function ShopifyProvider({ children, config, storage, onEvent, cartGuard, wishli
722
843
  recorder.changeLine(next.id, previous, -((_b = previous === null || previous === void 0 ? void 0 : previous.quantity) !== null && _b !== void 0 ? _b : 0));
723
844
  // The panel's "Removed From Cart" alert. Previously nothing fired here, so
724
845
  // the configured copy had no trigger at all.
725
- emit("cart:remove");
846
+ emit("cart:remove", {
847
+ cart: next,
848
+ changedLines: previous ? [{ line: previous, quantityChange: -previous.quantity }] : [],
849
+ });
726
850
  if (previous && (guard === null || guard === void 0 ? void 0 : guard.onReleased)) {
727
851
  void runGuard(() => guard.onReleased({
728
852
  variantId: previous.merchandise.id,
@@ -763,6 +887,47 @@ function ShopifyProvider({ children, config, storage, onEvent, cartGuard, wishli
763
887
  setCartLoad(false);
764
888
  }
765
889
  }, [cart, persistCart, serialize]);
890
+ const cartAddGiftCardCodes = (0, react_1.useCallback)(async (codes) => {
891
+ if (!(cart === null || cart === void 0 ? void 0 : cart.id) || codes.length === 0)
892
+ return cart !== null && cart !== void 0 ? cart : null;
893
+ setCartLoad(true);
894
+ try {
895
+ const { cart: next, notApplied } = await serialize(async () => {
896
+ var _a;
897
+ const current = (_a = latestCart.current) !== null && _a !== void 0 ? _a : cart;
898
+ const signedIn = session.getState().kind !== null;
899
+ return (0, cart_1.addGiftCardsKeepingBuyer)(current, codes, {
900
+ countryCode: async () => config.country || (await shopify_1.shopify.shop.countryCode()) || "US",
901
+ // Signed in: the identity write keeps the cart linked to the shopper (it replaces the identity).
902
+ customerAccessToken: signedIn ? await session.getAccessToken().catch(() => null) : null,
903
+ });
904
+ });
905
+ await persistCart(next);
906
+ if (notApplied.length) {
907
+ const message = `Shopify didn't apply the gift card ending in ${notApplied.map((code) => code.slice(-4)).join(", ")}`;
908
+ throw new types_1.ShopifyError(message, [{ field: ["giftCardCodes"], message, code: "GIFT_CARD_NOT_APPLIED" }]);
909
+ }
910
+ return next;
911
+ }
912
+ finally {
913
+ setCartLoad(false);
914
+ }
915
+ }, [cart, config.country, persistCart, serialize, session]);
916
+ const cartRemoveGiftCards = (0, react_1.useCallback)(async (appliedGiftCardIds) => {
917
+ if (!(cart === null || cart === void 0 ? void 0 : cart.id))
918
+ return null;
919
+ if (appliedGiftCardIds.length === 0)
920
+ return cart;
921
+ setCartLoad(true);
922
+ try {
923
+ const next = await serialize(() => shopify_1.shopify.cart.removeGiftCardCodes(cart.id, appliedGiftCardIds));
924
+ await persistCart(next);
925
+ return next;
926
+ }
927
+ finally {
928
+ setCartLoad(false);
929
+ }
930
+ }, [cart, persistCart, serialize]);
766
931
  const cartRefresh = (0, react_1.useCallback)(async () => {
767
932
  if (!(cart === null || cart === void 0 ? void 0 : cart.id))
768
933
  return null;
@@ -806,137 +971,39 @@ function ShopifyProvider({ children, config, storage, onEvent, cartGuard, wishli
806
971
  }, [persistCart]);
807
972
  // The wishlist module owns storage and hydration; these just wrap the mutating methods and
808
973
  // rely on onChange to keep React in sync.
809
- const wlAdd = (0, react_1.useCallback)(async (p) => { await shopify_1.shopify.wishlist.add(p); emit("wishlist:add"); }, [emit]);
974
+ const wlAdd = (0, react_1.useCallback)(async (p) => {
975
+ await shopify_1.shopify.wishlist.add(p);
976
+ emit("wishlist:add", { productId: typeof p === "string" ? p : p.id });
977
+ }, [emit]);
810
978
  // `remove` emits too. It used not to, so "Removed From Wishlist" only ever
811
979
  // fired via `toggle` — a dedicated remove button showed nothing.
812
980
  const wlRemove = (0, react_1.useCallback)(async (id) => {
813
981
  const removed = await shopify_1.shopify.wishlist.remove(id);
814
982
  if (removed)
815
- emit("wishlist:remove");
983
+ emit("wishlist:remove", { productId: id });
816
984
  }, [emit]);
817
985
  const wlToggle = (0, react_1.useCallback)(async (p) => {
818
986
  const nowSaved = await shopify_1.shopify.wishlist.toggle(p);
819
- emit(nowSaved ? "wishlist:add" : "wishlist:remove");
987
+ emit(nowSaved ? "wishlist:add" : "wishlist:remove", { productId: typeof p === "string" ? p : p.id });
820
988
  return nowSaved;
821
989
  }, [emit]);
822
990
  const wlClear = (0, react_1.useCallback)(async () => { await shopify_1.shopify.wishlist.clear(); }, []);
823
991
  const wlRefresh = (0, react_1.useCallback)(async () => { await shopify_1.shopify.wishlist.refresh({ keepDeleted: wishlistKeepDeleted }); }, [wishlistKeepDeleted]);
824
992
  const wlHas = (0, react_1.useCallback)((id) => shopify_1.shopify.wishlist.has(id), []);
825
993
  const wlIds = (0, react_1.useMemo)(() => wlItems.map((item) => item.productId), [wlItems]);
826
- // ── Customer session ────────────────────────────────────────────────────────
827
- // Wraps `shopify.customer` with token persistence and the four Login alerts.
828
- // Bad credentials resolve `false` rather than throwing: it is an expected
829
- // answer to a login form, and every caller would otherwise need a try/catch to
830
- // tell it apart from the store being unreachable (which still throws).
831
- const persistToken = (0, react_1.useCallback)(async (next) => {
832
- setToken(next);
833
- const s = storageRef.current;
834
- if (!s)
835
- return;
836
- if (next)
837
- await Promise.resolve(s.setItem(CUSTOMER_TOKEN_KEY, next));
838
- else
839
- await Promise.resolve(s.removeItem(CUSTOMER_TOKEN_KEY));
840
- }, []);
841
- const authLogin = (0, react_1.useCallback)(async (email, password) => {
842
- setAuthLoad(true);
843
- try {
844
- let accessToken;
845
- try {
846
- const minted = await shopify_1.shopify.customer.login({ email, password });
847
- accessToken = minted.accessToken;
848
- }
849
- catch (loginError) {
850
- // Only a credential problem is a "Login Failed" toast; a network failure
851
- // is not the shopper's mistake, so it propagates instead.
852
- if ((0, errors_1.classifyAuthFailure)(loginError) === "unknown")
853
- throw loginError;
854
- emit("auth:loginFailed", loginError);
855
- return false;
856
- }
857
- const profile = await shopify_1.shopify.customer.profile(accessToken);
858
- await persistToken(accessToken);
859
- setCustomer(profile);
860
- emit("auth:loginSuccess");
861
- return true;
862
- }
863
- finally {
864
- setAuthLoad(false);
865
- }
866
- }, [emit, persistToken]);
867
- const authSignup = (0, react_1.useCallback)(async (input) => {
868
- setAuthLoad(true);
869
- try {
870
- let created;
871
- try {
872
- created = await shopify_1.shopify.customer.signup(input);
873
- }
874
- catch (signupError) {
875
- if ((0, errors_1.classifyAuthFailure)(signupError) === "unknown")
876
- throw signupError;
877
- emit("auth:loginFailed", signupError);
878
- return false;
879
- }
880
- await persistToken(created.accessToken.accessToken);
881
- setCustomer(created.customer);
882
- // Signup mints a token, so the shopper IS logged in — both events fire, and
883
- // a host that only toasts on `auth:loginSuccess` still says the right thing.
884
- emit("auth:signup");
885
- emit("auth:loginSuccess");
886
- return true;
887
- }
888
- finally {
889
- setAuthLoad(false);
890
- }
891
- }, [emit, persistToken]);
892
- const authLogout = (0, react_1.useCallback)(async () => {
893
- const current = token;
894
- setAuthLoad(true);
895
- try {
896
- if (current) {
897
- try {
898
- await shopify_1.shopify.customer.logout(current);
899
- }
900
- catch (logoutError) {
901
- // The local session ends regardless — leaving a shopper "logged in"
902
- // because Shopify was unreachable is the worse failure.
903
- console.warn("[ShopifyProvider] token delete failed; clearing locally", logoutError);
904
- }
905
- }
906
- await persistToken(null);
907
- setCustomer(null);
908
- emit("auth:logout");
909
- }
910
- finally {
911
- setAuthLoad(false);
912
- }
913
- }, [token, emit, persistToken]);
914
- const authRecover = (0, react_1.useCallback)(async (email) => {
915
- setAuthLoad(true);
916
- try {
917
- await shopify_1.shopify.customer.recoverPassword(email);
918
- emit("auth:recoverSent");
919
- }
920
- finally {
921
- setAuthLoad(false);
922
- }
994
+ // The waitlist module owns storage and fetching, as the wishlist's does.
995
+ const wtAdd = (0, react_1.useCallback)(async (entry) => { await shopify_1.shopify.waitlist.add(entry); emit("waitlist:add"); }, [emit]);
996
+ const wtRemove = (0, react_1.useCallback)(async (variantId) => {
997
+ if (await shopify_1.shopify.waitlist.remove(variantId))
998
+ emit("waitlist:remove");
923
999
  }, [emit]);
924
- const authRefresh = (0, react_1.useCallback)(async () => {
925
- if (!token)
926
- return null;
927
- const profile = await shopify_1.shopify.customer.profile(token);
928
- if (!profile) {
929
- // Token expired or revoked. Clear it silently — see the mount restore.
930
- await persistToken(null);
931
- setCustomer(null);
932
- return null;
933
- }
934
- setCustomer(profile);
935
- return profile;
936
- }, [token, persistToken]);
1000
+ const wtRefresh = (0, react_1.useCallback)(async () => { await shopify_1.shopify.waitlist.refresh(); }, []);
1001
+ const wtHas = (0, react_1.useCallback)((variantId) => shopify_1.shopify.waitlist.has(variantId), []);
1002
+ const wtIds = (0, react_1.useMemo)(() => wtItems.map((item) => item.variantId), [wtItems]);
937
1003
  // ── Checkout outcome ────────────────────────────────────────────────────────
938
1004
  const checkoutOrderPlaced = (0, react_1.useCallback)(async (details) => {
939
- emit("checkout:orderPlaced", details);
1005
+ // The order's details travel in `error`, where hosts have always read them.
1006
+ emit("checkout:orderPlaced", { error: details });
940
1007
  // The old cart is spent once an order exists; keeping it would show the
941
1008
  // shopper their purchased items still sitting in the bag.
942
1009
  try {
@@ -961,13 +1028,35 @@ function ShopifyProvider({ children, config, storage, onEvent, cartGuard, wishli
961
1028
  }
962
1029
  }, []);
963
1030
  const checkoutPaymentFailed = (0, react_1.useCallback)((paymentError) => {
964
- emit("checkout:paymentFailed", paymentError);
1031
+ emit("checkout:paymentFailed", { error: paymentError });
965
1032
  }, [emit]);
1033
+ // Every step handed in from here, so `prepareCheckout` is tested without React (test/checkout-prepare).
1034
+ const checkoutPrepare = (0, react_1.useCallback)((options) => {
1035
+ var _a, _b;
1036
+ return (0, checkout_1.prepareCheckout)({
1037
+ readCartAgain: cartRefresh,
1038
+ // A read that fails carries on with the cart in hand ("Carry on", 2026-10-06).
1039
+ cartAlreadyLoaded: () => latestCart.current,
1040
+ signedIn: sessionState.kind !== null,
1041
+ email: (_b = (_a = sessionState.customer) === null || _a === void 0 ? void 0 : _a.email) !== null && _b !== void 0 ? _b : null,
1042
+ getAccessToken: session.getAccessToken,
1043
+ setBuyerIdentity: cartSetBuyerIdentity,
1044
+ reportCheckoutStarted: checkoutStarted,
1045
+ // The cart's attribution lands first, then the provider's own cartAttributes (Freckled Poppy's
1046
+ // labelCartForCheckout). Each queued behind pending cart writes.
1047
+ flushAttribution: cartFlushAttribution,
1048
+ ensureCartAttributes: () => { var _a; return (((_a = cartAttrsRef.current) === null || _a === void 0 ? void 0 : _a.length) ? cartEnsureAttributes(cartAttrsRef.current) : Promise.resolve(true)); },
1049
+ }, options);
1050
+ }, [cartRefresh, cartSetBuyerIdentity, cartFlushAttribution, cartEnsureAttributes, checkoutStarted, sessionState.kind, (_a = sessionState.customer) === null || _a === void 0 ? void 0 : _a.email, session]);
1051
+ const authMethod = (_b = auth === null || auth === void 0 ? void 0 : auth.method) !== null && _b !== void 0 ? _b : "password";
1052
+ // By its fields, so a host passing a fresh object each render doesn't rebuild the context.
1053
+ const storeCreditOptions = (0, react_1.useMemo)(() => (storeCredit ? { source: storeCredit.source, tileCreditBaseUrl: storeCredit.tileCreditBaseUrl } : null), [storeCredit === null || storeCredit === void 0 ? void 0 : storeCredit.source, storeCredit === null || storeCredit === void 0 ? void 0 : storeCredit.tileCreditBaseUrl]);
966
1054
  const value = (0, react_1.useMemo)(() => {
967
1055
  var _a, _b;
968
1056
  return ({
969
1057
  ready, error,
970
1058
  message: messages_1.message,
1059
+ events,
971
1060
  cart: {
972
1061
  cart, loading: cartLoading, itemCount: (_a = cart === null || cart === void 0 ? void 0 : cart.totalQuantity) !== null && _a !== void 0 ? _a : 0,
973
1062
  lineCount: (_b = cart === null || cart === void 0 ? void 0 : cart.lines.length) !== null && _b !== void 0 ? _b : 0,
@@ -979,6 +1068,8 @@ function ShopifyProvider({ children, config, storage, onEvent, cartGuard, wishli
979
1068
  applyDiscountCodes: cartApplyDiscounts,
980
1069
  updateNote: cartUpdateNote,
981
1070
  setBuyerIdentity: cartSetBuyerIdentity,
1071
+ addGiftCardCodes: cartAddGiftCardCodes,
1072
+ removeGiftCards: cartRemoveGiftCards,
982
1073
  refresh: cartRefresh,
983
1074
  adopt: cartAdopt,
984
1075
  reset: cartReset,
@@ -997,32 +1088,52 @@ function ShopifyProvider({ children, config, storage, onEvent, cartGuard, wishli
997
1088
  clear: wlClear,
998
1089
  refresh: wlRefresh,
999
1090
  },
1091
+ waitlist: {
1092
+ items: wtItems,
1093
+ ids: wtIds,
1094
+ count: wtItems.length,
1095
+ has: wtHas,
1096
+ add: wtAdd,
1097
+ remove: wtRemove,
1098
+ refresh: wtRefresh,
1099
+ },
1000
1100
  customer: {
1001
- customer,
1002
- loggedIn: !!customer,
1003
- accessToken: token,
1004
- loading: authLoading,
1005
- restoring,
1006
- login: authLogin,
1007
- signup: authSignup,
1008
- logout: authLogout,
1009
- recoverPassword: authRecover,
1010
- refresh: authRefresh,
1101
+ customer: sessionState.customer,
1102
+ loggedIn: sessionState.kind !== null,
1103
+ accessToken: sessionState.accessToken,
1104
+ loading: sessionState.loading,
1105
+ restoring: sessionState.restoring,
1106
+ method: authMethod,
1107
+ sessionKind: sessionState.kind,
1108
+ login: session.login,
1109
+ signup: session.signup,
1110
+ logout: session.logout,
1111
+ recoverPassword: session.recoverPassword,
1112
+ refresh: session.refresh,
1113
+ updateProfile: session.updateProfile,
1114
+ signIn: session.signIn,
1115
+ startSignIn: session.startSignIn,
1116
+ getAccessToken: session.getAccessToken,
1117
+ renewAccessToken: session.renewAccessToken,
1118
+ request: session.customerAccountRequest,
1011
1119
  },
1012
1120
  checkout: {
1121
+ prepare: checkoutPrepare,
1013
1122
  reportCheckoutStarted: checkoutStarted,
1014
1123
  reportOrderPlaced: checkoutOrderPlaced,
1015
1124
  reportPaymentFailed: checkoutPaymentFailed,
1016
1125
  },
1126
+ storeCredit: storeCreditOptions,
1017
1127
  });
1018
1128
  }, [
1019
- ready, error,
1129
+ ready, error, events,
1020
1130
  // A new policy changes `cart.maxLineItems`, so the memo must see it.
1021
1131
  resolvedPolicy,
1022
- cart, cartLoading, cartAddLine, cartAddLines, cartUpdateLine, cartRemoveLine, cartApplyDiscounts, cartUpdateNote, cartSetBuyerIdentity, cartRefresh, cartAdopt, cartReset, cartFlushAttribution, cartEnsureAttributes,
1132
+ cart, cartLoading, cartAddLine, cartAddLines, cartUpdateLine, cartRemoveLine, cartApplyDiscounts, cartUpdateNote, cartSetBuyerIdentity, cartAddGiftCardCodes, cartRemoveGiftCards, cartRefresh, cartAdopt, cartReset, cartFlushAttribution, cartEnsureAttributes,
1023
1133
  wlItems, wlIds, wlHas, wlAdd, wlRemove, wlToggle, wlClear, wlRefresh,
1024
- customer, token, authLoading, restoring, authLogin, authSignup, authLogout, authRecover, authRefresh,
1025
- checkoutStarted, checkoutOrderPlaced, checkoutPaymentFailed,
1134
+ wtItems, wtIds, wtHas, wtAdd, wtRemove, wtRefresh,
1135
+ sessionState, session, authMethod, storeCreditOptions,
1136
+ checkoutStarted, checkoutOrderPlaced, checkoutPaymentFailed, checkoutPrepare,
1026
1137
  ]);
1027
1138
  return (0, jsx_runtime_1.jsx)(ShopifyContext.Provider, { value: value, children: children });
1028
1139
  }
@@ -1038,13 +1149,47 @@ function useCart() {
1038
1149
  function useWishlist() {
1039
1150
  return useShopify().wishlist;
1040
1151
  }
1152
+ function useWaitlist() {
1153
+ return useShopify().waitlist;
1154
+ }
1041
1155
  /** Customer session — the source of the four Login alerts. */
1042
1156
  function useCustomer() {
1043
1157
  return useShopify().customer;
1044
1158
  }
1045
- /** Reports a hosted-checkout outcome so the two Checkout alerts can fire. */
1159
+ /**
1160
+ * Gets the cart ready before checkout opens (`prepare`, with this caller's own `preparing`), and
1161
+ * reports a hosted-checkout outcome so the two Checkout alerts can fire.
1162
+ */
1046
1163
  function useCheckout() {
1047
- return useShopify().checkout;
1164
+ const checkout = useShopify().checkout;
1165
+ // Each caller's own, so only the button that started it shows a spinner.
1166
+ const [preparing, setPreparing] = (0, react_1.useState)(false);
1167
+ // Read when called, so `prepare` keeps one identity while the cart changes.
1168
+ const latestPrepare = (0, react_1.useRef)(checkout.prepare);
1169
+ latestPrepare.current = checkout.prepare;
1170
+ const prepare = (0, react_1.useCallback)(async (options) => {
1171
+ setPreparing(true);
1172
+ try {
1173
+ return await latestPrepare.current(options);
1174
+ }
1175
+ finally {
1176
+ setPreparing(false);
1177
+ }
1178
+ }, []);
1179
+ return (0, react_1.useMemo)(() => ({ ...checkout, prepare, preparing }), [checkout, prepare, preparing]);
1180
+ }
1181
+ /**
1182
+ * Hears every event the provider raises, from anywhere inside it (0.10): the same events `onEvent`
1183
+ * gets, told right after it, in the order they happen. Cart events carry the cart and the lines they
1184
+ * changed, so a listener reports from the event rather than waiting for `useCart()` to catch up. The
1185
+ * listener is read when an event fires, so a new function each render is fine. It starts hearing once
1186
+ * the component has mounted and stops when it unmounts.
1187
+ */
1188
+ function useShopifyEvents(listener) {
1189
+ const { events } = useShopify();
1190
+ const latestListener = (0, react_1.useRef)(listener);
1191
+ latestListener.current = listener;
1192
+ (0, react_1.useEffect)(() => events.subscribe((event) => latestListener.current(event)), [events]);
1048
1193
  }
1049
1194
  /**
1050
1195
  * Resolved alert copy, for UI that isn't a toast — the empty-wishlist