@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.
- package/README.md +653 -50
- package/dist/alertSettings.d.ts.map +1 -1
- package/dist/alertSettings.js +2 -0
- package/dist/alertSettings.js.map +1 -1
- package/dist/attribution.d.ts +21 -0
- package/dist/attribution.d.ts.map +1 -1
- package/dist/attribution.js +17 -0
- package/dist/attribution.js.map +1 -1
- package/dist/auth/pkce.d.ts +30 -0
- package/dist/auth/pkce.d.ts.map +1 -0
- package/dist/auth/pkce.js +128 -0
- package/dist/auth/pkce.js.map +1 -0
- package/dist/auth/session.d.ts +118 -0
- package/dist/auth/session.d.ts.map +1 -0
- package/dist/auth/session.js +896 -0
- package/dist/auth/session.js.map +1 -0
- package/dist/auth/storeCredit.d.ts +64 -0
- package/dist/auth/storeCredit.d.ts.map +1 -0
- package/dist/auth/storeCredit.js +274 -0
- package/dist/auth/storeCredit.js.map +1 -0
- package/dist/cart.d.ts +25 -1
- package/dist/cart.d.ts.map +1 -1
- package/dist/cart.js +95 -2
- package/dist/cart.js.map +1 -1
- package/dist/cartPolicy.js +4 -4
- package/dist/checkout.d.ts +123 -0
- package/dist/checkout.d.ts.map +1 -0
- package/dist/checkout.js +205 -0
- package/dist/checkout.js.map +1 -0
- package/dist/customer.d.ts.map +1 -1
- package/dist/customer.js +8 -0
- package/dist/customer.js.map +1 -1
- package/dist/index.d.ts +12 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +74 -2
- package/dist/index.js.map +1 -1
- package/dist/messages.d.ts.map +1 -1
- package/dist/messages.js +2 -0
- package/dist/messages.js.map +1 -1
- package/dist/money.d.ts +16 -0
- package/dist/money.d.ts.map +1 -1
- package/dist/money.js +55 -0
- package/dist/money.js.map +1 -1
- package/dist/orders.d.ts +99 -0
- package/dist/orders.d.ts.map +1 -0
- package/dist/orders.js +439 -0
- package/dist/orders.js.map +1 -0
- package/dist/productPage.d.ts +100 -0
- package/dist/productPage.d.ts.map +1 -0
- package/dist/productPage.js +298 -0
- package/dist/productPage.js.map +1 -0
- package/dist/productStore.d.ts +1 -1
- package/dist/productStore.d.ts.map +1 -1
- package/dist/productStore.js +63 -5
- package/dist/productStore.js.map +1 -1
- package/dist/products.d.ts.map +1 -1
- package/dist/products.js +15 -3
- package/dist/products.js.map +1 -1
- package/dist/purchaseRules.d.ts +74 -0
- package/dist/purchaseRules.d.ts.map +1 -0
- package/dist/purchaseRules.js +73 -0
- package/dist/purchaseRules.js.map +1 -0
- package/dist/queries.d.ts +15 -13
- package/dist/queries.d.ts.map +1 -1
- package/dist/queries.js +43 -7
- package/dist/queries.js.map +1 -1
- package/dist/react/ShopifyProvider.d.ts +167 -25
- package/dist/react/ShopifyProvider.d.ts.map +1 -1
- package/dist/react/ShopifyProvider.js +352 -207
- package/dist/react/ShopifyProvider.js.map +1 -1
- package/dist/react/appForeground.d.ts +6 -0
- package/dist/react/appForeground.d.ts.map +1 -0
- package/dist/react/appForeground.js +19 -0
- package/dist/react/appForeground.js.map +1 -0
- package/dist/react/appForeground.native.d.ts +2 -0
- package/dist/react/appForeground.native.d.ts.map +1 -0
- package/dist/react/appForeground.native.js +23 -0
- package/dist/react/appForeground.native.js.map +1 -0
- package/dist/react/index.d.ts +7 -1
- package/dist/react/index.d.ts.map +1 -1
- package/dist/react/index.js +23 -1
- package/dist/react/index.js.map +1 -1
- package/dist/react/tileCreditClient.d.ts +4 -0
- package/dist/react/tileCreditClient.d.ts.map +1 -0
- package/dist/react/tileCreditClient.js +27 -0
- package/dist/react/tileCreditClient.js.map +1 -0
- package/dist/react/useAppDiscountCode.d.ts +19 -0
- package/dist/react/useAppDiscountCode.d.ts.map +1 -0
- package/dist/react/useAppDiscountCode.js +44 -0
- package/dist/react/useAppDiscountCode.js.map +1 -0
- package/dist/react/useCartStoreCredit.d.ts +25 -0
- package/dist/react/useCartStoreCredit.d.ts.map +1 -0
- package/dist/react/useCartStoreCredit.js +292 -0
- package/dist/react/useCartStoreCredit.js.map +1 -0
- package/dist/react/useOrders.d.ts +104 -0
- package/dist/react/useOrders.d.ts.map +1 -0
- package/dist/react/useOrders.js +287 -0
- package/dist/react/useOrders.js.map +1 -0
- package/dist/react/useProduct.d.ts.map +1 -1
- package/dist/react/useProduct.js +1 -9
- package/dist/react/useProduct.js.map +1 -1
- package/dist/react/useProductFeed.d.ts.map +1 -1
- package/dist/react/useProductFeed.js +33 -5
- package/dist/react/useProductFeed.js.map +1 -1
- package/dist/react/useProductPage.d.ts +148 -0
- package/dist/react/useProductPage.d.ts.map +1 -0
- package/dist/react/useProductPage.js +178 -0
- package/dist/react/useProductPage.js.map +1 -0
- package/dist/react/useProductRecommendations.d.ts +27 -0
- package/dist/react/useProductRecommendations.d.ts.map +1 -0
- package/dist/react/useProductRecommendations.js +76 -0
- package/dist/react/useProductRecommendations.js.map +1 -0
- package/dist/react/useStoreCredit.d.ts +58 -0
- package/dist/react/useStoreCredit.d.ts.map +1 -0
- package/dist/react/useStoreCredit.js +209 -0
- package/dist/react/useStoreCredit.js.map +1 -0
- package/dist/react/useTileCredit.d.ts +4 -3
- package/dist/react/useTileCredit.d.ts.map +1 -1
- package/dist/react/useTileCredit.js +63 -55
- package/dist/react/useTileCredit.js.map +1 -1
- package/dist/shopify.d.ts.map +1 -1
- package/dist/shopify.js +2 -0
- package/dist/shopify.js.map +1 -1
- package/dist/storedList.d.ts +27 -0
- package/dist/storedList.d.ts.map +1 -0
- package/dist/storedList.js +152 -0
- package/dist/storedList.js.map +1 -0
- package/dist/tileCredit.d.ts +25 -10
- package/dist/tileCredit.d.ts.map +1 -1
- package/dist/tileCredit.js +97 -45
- package/dist/tileCredit.js.map +1 -1
- package/dist/types.d.ts +407 -34
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +1 -2
- package/dist/types.js.map +1 -1
- package/dist/variants.d.ts.map +1 -1
- package/dist/variants.js +4 -3
- package/dist/variants.js.map +1 -1
- package/dist/waitlist.d.ts +4 -0
- package/dist/waitlist.d.ts.map +1 -0
- package/dist/waitlist.js +213 -0
- package/dist/waitlist.js.map +1 -0
- package/dist/wishlist.d.ts.map +1 -1
- package/dist/wishlist.js +70 -47
- package/dist/wishlist.js.map +1 -1
- 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
|
|
182
|
-
* listener must not fail the write that triggered it —
|
|
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,
|
|
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
|
-
|
|
205
|
-
|
|
243
|
+
if (listener) {
|
|
244
|
+
try {
|
|
245
|
+
listener(event);
|
|
246
|
+
}
|
|
247
|
+
catch (listenerError) {
|
|
248
|
+
console.warn("[ShopifyProvider] onEvent threw", listenerError);
|
|
249
|
+
}
|
|
206
250
|
}
|
|
207
|
-
|
|
208
|
-
|
|
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
|
-
/**
|
|
317
|
-
|
|
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
|
|
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
|
-
|
|
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, ...(
|
|
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
|
-
|
|
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
|
-
|
|
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) => {
|
|
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
|
-
//
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
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
|
|
925
|
-
|
|
926
|
-
|
|
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
|
-
|
|
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:
|
|
1003
|
-
accessToken:
|
|
1004
|
-
loading:
|
|
1005
|
-
restoring,
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
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
|
-
|
|
1025
|
-
|
|
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
|
-
/**
|
|
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
|
-
|
|
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
|