@growupsell/commerce 0.0.0-stage → 1.0.0-alpha.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,2980 @@
1
+ import {
2
+ GrowupsellApiError,
3
+ createClient
4
+ } from "./chunk-AI2CWQ22.js";
5
+ import {
6
+ DEFAULT_LISTING_CRITERIA,
7
+ LISTING_PAGE_SIZE,
8
+ categoryAndDescendants,
9
+ checkoutPreview,
10
+ defaultSelection,
11
+ formatSelectedVariants,
12
+ imagesFor,
13
+ isSelectionComplete,
14
+ listingQuery,
15
+ listingSearchOptions,
16
+ parseListingQuery,
17
+ paymentOptions,
18
+ productPricing,
19
+ requiresShippingAddress,
20
+ resolveSku,
21
+ selectOption,
22
+ specificationsFor,
23
+ variantChoices,
24
+ withCategory
25
+ } from "./chunk-W2CJSBV4.js";
26
+
27
+ // src/stores/runtime/concurrency.ts
28
+ function start(run) {
29
+ try {
30
+ return Promise.resolve(run());
31
+ } catch (error) {
32
+ return Promise.reject(error);
33
+ }
34
+ }
35
+ function createLatest() {
36
+ let generation = 0;
37
+ return async (run) => {
38
+ const mine = ++generation;
39
+ try {
40
+ const value = await start(run);
41
+ return mine === generation ? { current: true, value } : { current: false };
42
+ } catch (error) {
43
+ if (mine !== generation) return { current: false };
44
+ throw error;
45
+ }
46
+ };
47
+ }
48
+ function createSingleFlight() {
49
+ const inFlight = /* @__PURE__ */ new Map();
50
+ return (key, run) => {
51
+ const existing = inFlight.get(key);
52
+ if (existing) return existing;
53
+ const promise = start(run).finally(() => {
54
+ if (inFlight.get(key) === promise) inFlight.delete(key);
55
+ });
56
+ inFlight.set(key, promise);
57
+ return promise;
58
+ };
59
+ }
60
+ function createSerialQueue() {
61
+ let tail = Promise.resolve();
62
+ return (run) => {
63
+ const next = tail.then(
64
+ () => start(run),
65
+ () => start(run)
66
+ );
67
+ tail = next.catch(() => {
68
+ });
69
+ return next;
70
+ };
71
+ }
72
+ function createCoalescer() {
73
+ const slots = /* @__PURE__ */ new Map();
74
+ function launch(key, slot, job) {
75
+ const sent = start(() => job.send(job.value));
76
+ job.settle(
77
+ sent.then(
78
+ (value) => slot.waiting ? { current: false } : { current: true, value },
79
+ (error) => {
80
+ if (slot.waiting) return { current: false };
81
+ throw error;
82
+ }
83
+ )
84
+ );
85
+ void sent.catch(() => {
86
+ }).then(() => {
87
+ const next = slot.waiting;
88
+ slot.waiting = null;
89
+ if (next) launch(key, slot, next);
90
+ else slots.delete(key);
91
+ });
92
+ }
93
+ return (key, value, send) => new Promise((resolve, reject) => {
94
+ const job = {
95
+ value,
96
+ send,
97
+ settle: (outcome) => outcome.then(resolve, reject)
98
+ };
99
+ const slot = slots.get(key);
100
+ if (!slot) {
101
+ const fresh = { waiting: null };
102
+ slots.set(key, fresh);
103
+ launch(key, fresh, job);
104
+ return;
105
+ }
106
+ slot.waiting?.settle(Promise.resolve({ current: false }));
107
+ slot.waiting = job;
108
+ });
109
+ }
110
+ async function runInEpoch(epoch, run) {
111
+ const started = epoch();
112
+ try {
113
+ const value = await start(run);
114
+ return epoch() === started ? { current: true, value } : { current: false };
115
+ } catch (error) {
116
+ if (epoch() !== started) return { current: false };
117
+ throw error;
118
+ }
119
+ }
120
+
121
+ // src/stores/runtime/errors.ts
122
+ var ok = (value) => ({ ok: true, value });
123
+ var fail = (error) => ({ ok: false, error });
124
+ function storeError(kind, message, extra = {}) {
125
+ return { kind, message, status: extra.status ?? null, fieldErrors: extra.fieldErrors ?? {} };
126
+ }
127
+ var KINDS = /* @__PURE__ */ new Set([
128
+ "http",
129
+ "network",
130
+ "timeout",
131
+ "parse",
132
+ "validation",
133
+ "signed-out",
134
+ "session",
135
+ "superseded",
136
+ "unknown"
137
+ ]);
138
+ function isStoreError(value) {
139
+ if (!value || typeof value !== "object" || value instanceof Error) return false;
140
+ const v = value;
141
+ return typeof v.kind === "string" && KINDS.has(v.kind) && typeof v.message === "string";
142
+ }
143
+ function toStoreError(error, context = {}) {
144
+ if (isStoreError(error)) return error;
145
+ if (error instanceof GrowupsellApiError) {
146
+ const deadSession = context.sentSession === true && error.kind === "http" && error.status === 404 && typeof error.body === "string";
147
+ const converted = storeError(deadSession ? "session" : error.kind, error.message, {
148
+ status: error.status ?? null,
149
+ fieldErrors: Object.fromEntries(
150
+ Object.entries(error.fieldErrors).map(([field, messages]) => [field, [...messages]])
151
+ )
152
+ });
153
+ if (error.retryAfterSeconds != null) converted.retryAfterSeconds = error.retryAfterSeconds;
154
+ return converted;
155
+ }
156
+ if (error instanceof TypeError || error instanceof RangeError) {
157
+ return storeError("validation", error.message);
158
+ }
159
+ if (error instanceof Error) return storeError("unknown", error.message);
160
+ if (typeof error === "string") return storeError("unknown", error);
161
+ return storeError("unknown", "Unknown error");
162
+ }
163
+
164
+ // src/stores/runtime/store.ts
165
+ function readOnly(store) {
166
+ return {
167
+ getSnapshot: store.getSnapshot,
168
+ getServerSnapshot: store.getServerSnapshot,
169
+ subscribe: store.subscribe
170
+ };
171
+ }
172
+ var runtime = globalThis;
173
+ function rethrowAsync(error) {
174
+ runtime.setTimeout(() => {
175
+ throw error;
176
+ }, 0);
177
+ }
178
+ function createStore(initial, options = {}) {
179
+ const onListenerError = options.onListenerError ?? rethrowAsync;
180
+ let snapshot = initial;
181
+ let listeners = [];
182
+ function set(next) {
183
+ if (Object.is(next, snapshot)) return;
184
+ snapshot = next;
185
+ for (const entry of listeners.slice()) {
186
+ try {
187
+ entry.listener();
188
+ } catch (error) {
189
+ onListenerError(error);
190
+ }
191
+ }
192
+ }
193
+ return {
194
+ getSnapshot: () => snapshot,
195
+ getServerSnapshot: () => initial,
196
+ subscribe(listener) {
197
+ const entry = { listener };
198
+ listeners = [...listeners, entry];
199
+ return () => {
200
+ listeners = listeners.filter((e) => e !== entry);
201
+ };
202
+ },
203
+ set,
204
+ update: (fn) => set(fn(snapshot))
205
+ };
206
+ }
207
+
208
+ // src/stores/auth.ts
209
+ var SENDS_SESSION = /* @__PURE__ */ new Set(["login", "register", "verify"]);
210
+ var SUPERSEDED = () => storeError("superseded", "The shopper changed while the request was running.");
211
+ function createAuthStore(ctx) {
212
+ const store = createStore(
213
+ {
214
+ status: "unknown",
215
+ customer: null,
216
+ rewardPoints: null,
217
+ verification: null,
218
+ settings: null,
219
+ pending: null,
220
+ error: null
221
+ },
222
+ { onListenerError: ctx.onListenerError }
223
+ );
224
+ const patch = (next) => store.update((s) => ({ ...s, ...next }));
225
+ const flight = createSingleFlight();
226
+ let remembered = true;
227
+ let savedAt = 0;
228
+ ctx.onHydrate(({ auth, remembered: fromSecure }) => {
229
+ remembered = fromSecure;
230
+ savedAt = auth?.savedAt ?? 0;
231
+ patch(
232
+ auth ? {
233
+ status: "signed-in",
234
+ customer: auth.customer,
235
+ rewardPoints: auth.rewardPoints,
236
+ verification: null
237
+ } : { status: "signed-out", customer: null, rewardPoints: null }
238
+ );
239
+ });
240
+ function action(name, run) {
241
+ return flight(name, async () => {
242
+ patch({ pending: name, error: null });
243
+ let result;
244
+ try {
245
+ result = await run();
246
+ } catch (error) {
247
+ result = fail(toStoreError(error, { sentSession: SENDS_SESSION.has(name) }));
248
+ }
249
+ if (result.ok) {
250
+ if (store.getSnapshot().pending === name) patch({ pending: null });
251
+ } else {
252
+ failed(name, result.error);
253
+ }
254
+ return result;
255
+ });
256
+ }
257
+ function failed(name, error) {
258
+ const pending = store.getSnapshot().pending;
259
+ patch({ pending: pending === name ? null : pending, error });
260
+ ctx.events.emit({ type: "error", store: "auth", action: name, error });
261
+ }
262
+ function refuse(name, kind, message) {
263
+ const error = storeError(kind, message);
264
+ patch({ error });
265
+ ctx.events.emit({ type: "error", store: "auth", action: name, error });
266
+ return Promise.resolve(fail(error));
267
+ }
268
+ const signedIn = () => store.getSnapshot().status === "signed-in";
269
+ function signIn(session, via, remember) {
270
+ const rotated = ctx.hasSession();
271
+ ctx.tokens.setSessionToken(null);
272
+ ctx.records.session.remove();
273
+ ctx.tokens.setCustomerToken(session.token);
274
+ remembered = remember;
275
+ savedAt = Date.now();
276
+ persist(session.customer, session.token, session.rewardPoints);
277
+ patch({
278
+ status: "signed-in",
279
+ customer: session.customer,
280
+ rewardPoints: session.rewardPoints,
281
+ verification: null,
282
+ pending: null,
283
+ error: null
284
+ });
285
+ ctx.setIdentity({ kind: "customer", customerId: session.customer.id });
286
+ if (rotated) ctx.events.emit({ type: "session:replaced", reason: "rotated" });
287
+ ctx.events.emit({ type: "auth:signed-in", customerId: session.customer.id, via });
288
+ }
289
+ function persist(customer, token, rewardPoints) {
290
+ const record = { token, customer, rewardPoints, savedAt };
291
+ (remembered ? ctx.records.auth : ctx.records.ephemeralAuth).write(record);
292
+ (remembered ? ctx.records.ephemeralAuth : ctx.records.auth).remove();
293
+ }
294
+ function signOut(reason) {
295
+ const rotated = ctx.hasSession();
296
+ ctx.tokens.setCustomerToken(null);
297
+ ctx.tokens.setSessionToken(null);
298
+ ctx.records.auth.remove();
299
+ ctx.records.ephemeralAuth.remove();
300
+ ctx.records.session.remove();
301
+ patch({ status: "signed-out", customer: null, rewardPoints: null, verification: null });
302
+ ctx.setIdentity({ kind: "guest", customerId: null });
303
+ if (rotated) ctx.events.emit({ type: "session:replaced", reason: "rotated" });
304
+ ctx.events.emit({ type: "auth:signed-out", reason });
305
+ }
306
+ ctx.hooks.customerExpired = () => signOut("expired");
307
+ ctx.events.on("checkout:points-spent", ({ points }) => {
308
+ const { status, customer, rewardPoints } = store.getSnapshot();
309
+ const token = ctx.tokens.getCustomerToken();
310
+ if (status !== "signed-in" || !customer || rewardPoints === null || !token) return;
311
+ const estimate = Math.max(rewardPoints - points, 0);
312
+ persist(customer, token, estimate);
313
+ patch({ rewardPoints: estimate });
314
+ });
315
+ function authenticate(name, remember, send) {
316
+ return action(name, async () => {
317
+ await ctx.ready();
318
+ if (signedIn()) return fail(storeError("validation", "Already signed in; log out first."));
319
+ await ctx.ensureSession();
320
+ const answer = await ctx.inEpoch(send);
321
+ if (!answer.current) return fail(SUPERSEDED());
322
+ const result = answer.value;
323
+ if (result.status === "signed-in") {
324
+ signIn(result.session, name, remember !== false);
325
+ return ok({ status: "signed-in" });
326
+ }
327
+ patch({ verification: { customerId: result.customerId, contact: result.contact } });
328
+ return ok({ status: "verification-required", contact: result.contact });
329
+ });
330
+ }
331
+ function login({ remember, ...input }) {
332
+ return authenticate("login", remember, () => ctx.client.auth.login(input));
333
+ }
334
+ function register({ remember, ...input }) {
335
+ return authenticate("register", remember, () => ctx.client.auth.register(input));
336
+ }
337
+ function verifyOtp(otp) {
338
+ const verification = store.getSnapshot().verification;
339
+ if (!verification) {
340
+ return refuse("verify", "validation", "There is no verification in progress.");
341
+ }
342
+ return action("verify", async () => {
343
+ await ctx.ready();
344
+ await ctx.ensureSession();
345
+ const answer = await ctx.inEpoch(
346
+ () => ctx.client.auth.verifyOtp({ customerId: verification.customerId, otp })
347
+ );
348
+ if (!answer.current) return fail(SUPERSEDED());
349
+ if (!answer.value.customer.isActive) {
350
+ return fail(storeError("validation", "The account is still not verified."));
351
+ }
352
+ signIn(answer.value, "otp", true);
353
+ return ok({ status: "signed-in" });
354
+ });
355
+ }
356
+ function resendOtp() {
357
+ const contact = store.getSnapshot().verification?.contact;
358
+ if (!contact) {
359
+ return refuse("resend", "validation", "There is no contact to send a new OTP to.");
360
+ }
361
+ return action("resend", async () => {
362
+ await ctx.client.auth.sendOtp(contact);
363
+ return ok(void 0);
364
+ });
365
+ }
366
+ function cancelVerification() {
367
+ if (store.getSnapshot().verification) patch({ verification: null });
368
+ }
369
+ async function logout() {
370
+ await ctx.ready();
371
+ if (signedIn() || ctx.tokens.getCustomerToken() !== null) signOut("logout");
372
+ return ok(void 0);
373
+ }
374
+ function loadSettings() {
375
+ return action("settings", async () => {
376
+ const settings = await ctx.client.auth.settings();
377
+ patch({ settings });
378
+ return ok(settings);
379
+ });
380
+ }
381
+ async function updateProfile(input) {
382
+ await ctx.ready();
383
+ const customer = store.getSnapshot().customer;
384
+ if (!signedIn() || !customer) {
385
+ return refuse("profile", "signed-out", "Log in to change the profile.");
386
+ }
387
+ return action("profile", async () => {
388
+ const answer = await ctx.inEpoch(() => ctx.client.customer.updateProfile(customer.id, input));
389
+ if (!answer.current) return fail(SUPERSEDED());
390
+ const token = ctx.tokens.getCustomerToken();
391
+ if (token) persist(answer.value, token, store.getSnapshot().rewardPoints);
392
+ patch({ customer: answer.value });
393
+ return ok(answer.value);
394
+ });
395
+ }
396
+ return {
397
+ getSnapshot: store.getSnapshot,
398
+ getServerSnapshot: store.getServerSnapshot,
399
+ subscribe: store.subscribe,
400
+ loadSettings,
401
+ login,
402
+ register,
403
+ verifyOtp,
404
+ resendOtp,
405
+ cancelVerification,
406
+ logout,
407
+ updateProfile
408
+ };
409
+ }
410
+
411
+ // src/stores/business.ts
412
+ var SECTIONS = ["settings", "themeSettings", "media", "socialLinks"];
413
+ var NO_FEATURES = Object.freeze([]);
414
+ function derive(state) {
415
+ return {
416
+ ...state,
417
+ currency: state.settings?.currency ?? null,
418
+ themeId: state.themeSettings?.id ?? null,
419
+ features: state.business?.features ?? NO_FEATURES
420
+ };
421
+ }
422
+ function overall(sections, current) {
423
+ const values = SECTIONS.map((section) => sections[section]);
424
+ if (values.every((v) => v === "error")) return "error";
425
+ if (values.every((v) => v === "ready" || v === "error")) return "ready";
426
+ if (current === "ready") return "ready";
427
+ return values.includes("loading") ? "loading" : "idle";
428
+ }
429
+ function createBusinessStore(ctx) {
430
+ const seed = ctx.seeds.business;
431
+ const sections = Object.fromEntries(
432
+ SECTIONS.map((section) => [section, seed?.[section] !== void 0 ? "ready" : "idle"])
433
+ );
434
+ const store = createStore(
435
+ derive({
436
+ status: overall(sections, "idle"),
437
+ refreshing: false,
438
+ error: null,
439
+ business: seed?.business ?? null,
440
+ settings: seed?.settings ?? null,
441
+ themeSettings: seed?.themeSettings ?? null,
442
+ media: seed?.media ?? null,
443
+ socialLinks: seed?.socialLinks ?? null,
444
+ sections,
445
+ pending: { themeSettings: false }
446
+ }),
447
+ { onListenerError: ctx.onListenerError }
448
+ );
449
+ const fetchers = {
450
+ settings: () => ctx.client.business.settings(),
451
+ themeSettings: () => ctx.client.business.themeSettings(),
452
+ media: () => ctx.client.business.media(),
453
+ socialLinks: () => ctx.client.business.socialLinks()
454
+ };
455
+ const generation = {
456
+ settings: 0,
457
+ themeSettings: 0,
458
+ media: 0,
459
+ socialLinks: 0
460
+ };
461
+ const flight = createSingleFlight();
462
+ const saves = createSerialQueue();
463
+ let loadsRunning = 0;
464
+ let savesWaiting = 0;
465
+ function set(next) {
466
+ store.set(derive(next));
467
+ }
468
+ function failed(action, error) {
469
+ ctx.events.emit({ type: "error", store: "business", action, error });
470
+ return fail(error);
471
+ }
472
+ async function run(wanted) {
473
+ const mine = wanted.map((section) => ++generation[section]);
474
+ loadsRunning += 1;
475
+ const before = store.getSnapshot();
476
+ const starting = { ...before.sections };
477
+ for (const section of wanted) if (starting[section] !== "ready") starting[section] = "loading";
478
+ set({
479
+ ...before,
480
+ sections: starting,
481
+ status: overall(starting, before.status),
482
+ refreshing: before.status === "ready",
483
+ error: null
484
+ });
485
+ const results = await Promise.allSettled(wanted.map((section) => fetchers[section]()));
486
+ loadsRunning -= 1;
487
+ if (ctx.isDisposed()) return fail(storeError("superseded", "The stores were disposed."));
488
+ const now = store.getSnapshot();
489
+ const next = { ...now, sections: { ...now.sections } };
490
+ const writable = next;
491
+ const statuses = next.sections;
492
+ let error = null;
493
+ for (const [i, result] of results.entries()) {
494
+ const section = wanted[i];
495
+ if (generation[section] !== mine[i]) continue;
496
+ if (result.status === "fulfilled") {
497
+ writable[section] = result.value;
498
+ statuses[section] = "ready";
499
+ } else {
500
+ error = toStoreError(result.reason);
501
+ if (statuses[section] !== "ready") statuses[section] = "error";
502
+ }
503
+ }
504
+ const status = overall(statuses, now.status);
505
+ set({ ...next, status, refreshing: loadsRunning > 0 && status === "ready", error });
506
+ return error ? failed("load", error) : ok(void 0);
507
+ }
508
+ return {
509
+ ...readOnly(store),
510
+ load(options = {}) {
511
+ if (options.force) return run(SECTIONS);
512
+ const sectionsNow = store.getSnapshot().sections;
513
+ const missing = SECTIONS.filter((section) => sectionsNow[section] !== "ready");
514
+ if (missing.length === 0) return Promise.resolve(ok(void 0));
515
+ return flight("load", () => run(missing));
516
+ },
517
+ updateThemeSettings(changes) {
518
+ savesWaiting += 1;
519
+ const s = store.getSnapshot();
520
+ set({ ...s, pending: { themeSettings: true }, error: null });
521
+ return saves(async () => {
522
+ let outcome;
523
+ try {
524
+ const saved = await ctx.client.business.updateThemeSettings(changes);
525
+ outcome = ok(saved);
526
+ } catch (error) {
527
+ outcome = fail(toStoreError(error));
528
+ }
529
+ savesWaiting -= 1;
530
+ if (ctx.isDisposed()) return fail(storeError("superseded", "The stores were disposed."));
531
+ const now = store.getSnapshot();
532
+ const pending = { themeSettings: savesWaiting > 0 };
533
+ if (!outcome.ok) {
534
+ set({ ...now, pending, error: outcome.error });
535
+ return failed("updateThemeSettings", outcome.error);
536
+ }
537
+ generation.themeSettings += 1;
538
+ const statuses = { ...now.sections, themeSettings: "ready" };
539
+ set({
540
+ ...now,
541
+ themeSettings: outcome.value,
542
+ sections: statuses,
543
+ status: overall(statuses, now.status),
544
+ pending,
545
+ error: null
546
+ });
547
+ return outcome;
548
+ });
549
+ },
550
+ hasFeature: (name) => store.getSnapshot().features.includes(name)
551
+ };
552
+ }
553
+
554
+ // src/stores/cart.ts
555
+ var NO_PENDING = {
556
+ adding: [],
557
+ lines: {},
558
+ orderType: false,
559
+ discount: false,
560
+ addresses: false,
561
+ taxDetails: false
562
+ };
563
+ var INITIAL = {
564
+ status: "idle",
565
+ refreshing: false,
566
+ cart: null,
567
+ totalsStale: false,
568
+ pending: NO_PENDING,
569
+ delivery: { status: "idle", estimate: null, error: null },
570
+ mergeWarning: null,
571
+ error: null
572
+ };
573
+ var SUPERSEDED2 = storeError(
574
+ "superseded",
575
+ "A newer change, or a change of shopper, replaced this result."
576
+ );
577
+ function withLine(pending, lineId, kind, only) {
578
+ if (only && pending.lines[lineId] !== only) return pending;
579
+ if (!kind && !(lineId in pending.lines)) return pending;
580
+ const lines = { ...pending.lines };
581
+ if (kind) lines[lineId] = kind;
582
+ else delete lines[lineId];
583
+ return { ...pending, lines };
584
+ }
585
+ function withoutOne(list, value) {
586
+ const index = list.indexOf(value);
587
+ return index < 0 ? list : [...list.slice(0, index), ...list.slice(index + 1)];
588
+ }
589
+ function createCartStore(ctx) {
590
+ const store = createStore(INITIAL, { onListenerError: ctx.onListenerError });
591
+ const latest = createLatest();
592
+ const latestQuote = createLatest();
593
+ const serial = createSerialQueue();
594
+ const coalesce = createCoalescer();
595
+ let server = null;
596
+ const quantities = /* @__PURE__ */ new Map();
597
+ const removals = /* @__PURE__ */ new Map();
598
+ let orderType = null;
599
+ const gone = /* @__PURE__ */ new Set();
600
+ let needsLoad = false;
601
+ let mergeCheck = null;
602
+ let version = 0;
603
+ let memo = null;
604
+ const touch = () => {
605
+ version++;
606
+ };
607
+ function view() {
608
+ if (memo?.version === version) return memo.value;
609
+ const stale = quantities.size > 0 || removals.size > 0 || orderType !== null;
610
+ let value = { cart: server, totalsStale: false };
611
+ if (server && stale) {
612
+ const lines = server.lines.filter((line) => !removals.has(line.id)).map((line) => {
613
+ const quantity = quantities.get(line.id)?.value;
614
+ return quantity === void 0 || quantity === line.quantity ? line : { ...line, quantity };
615
+ });
616
+ value = {
617
+ cart: { ...server, lines, type: orderType?.value ?? server.type },
618
+ totalsStale: true
619
+ };
620
+ }
621
+ memo = { version, value };
622
+ return value;
623
+ }
624
+ function update(patch) {
625
+ const current = store.getSnapshot();
626
+ const keys = Object.keys(patch);
627
+ if (keys.every((key) => Object.is(patch[key], current[key]))) return;
628
+ store.set({ ...current, ...patch });
629
+ }
630
+ const render = (patch = {}) => update({ ...view(), ...patch });
631
+ const pending = () => store.getSnapshot().pending;
632
+ function reset(patch) {
633
+ server = null;
634
+ quantities.clear();
635
+ removals.clear();
636
+ orderType = null;
637
+ gone.clear();
638
+ touch();
639
+ store.set({ ...INITIAL, ...patch });
640
+ }
641
+ const epoch = () => ctx.identity.getSnapshot().epoch;
642
+ function act(name, keepError = false) {
643
+ const at = epoch();
644
+ const sentSession = ctx.hasSession();
645
+ if (!keepError && store.getSnapshot().error) update({ error: null });
646
+ const alive = () => !ctx.isDisposed() && epoch() === at;
647
+ const self = {
648
+ alive,
649
+ fail(cause, patch = {}) {
650
+ const error = toStoreError(cause, { sentSession });
651
+ if (!alive()) return fail(error.kind === "session" ? error : SUPERSEDED2);
652
+ if (error.kind === "superseded") return fail(error);
653
+ update({ ...patch, error });
654
+ ctx.events.emit({ type: "error", store: "cart", action: name, error });
655
+ return fail(error);
656
+ },
657
+ refuse: (kind, message) => self.fail(storeError(kind, message))
658
+ };
659
+ return self;
660
+ }
661
+ async function guard(action, run) {
662
+ try {
663
+ const value = await run();
664
+ return action.alive() ? { current: true, value } : { current: false };
665
+ } catch (error) {
666
+ if (action.alive() || toStoreError(error, { sentSession: true }).kind === "session") {
667
+ throw error;
668
+ }
669
+ return { current: false };
670
+ }
671
+ }
672
+ const isCustomer = () => ctx.identity.getSnapshot().kind === "customer";
673
+ const findLine = (lineId) => view().cart?.lines.find((line) => line.id === lineId);
674
+ async function load() {
675
+ await ctx.ready();
676
+ const action = act("refresh", true);
677
+ if (!isCustomer() && !ctx.hasSession()) {
678
+ void latest(() => Promise.resolve(null));
679
+ needsLoad = false;
680
+ server = null;
681
+ touch();
682
+ render({ status: "ready", refreshing: false });
683
+ return ok(null);
684
+ }
685
+ update(store.getSnapshot().status === "ready" ? { refreshing: true } : { status: "loading" });
686
+ const settled = settledChanges();
687
+ try {
688
+ const result = await guard(action, () => latest(() => ctx.client.cart.get()));
689
+ if (!result.current || !result.value.current) return fail(SUPERSEDED2);
690
+ server = result.value.value;
691
+ for (const forget of settled) forget();
692
+ touch();
693
+ needsLoad = false;
694
+ const missing = checkMerge(server);
695
+ render({ status: "ready", refreshing: false, ...missing && { mergeWarning: missing } });
696
+ if (missing) ctx.events.emit({ type: "cart:merge-incomplete", ...missing });
697
+ return ok(view().cart);
698
+ } catch (error) {
699
+ const status = store.getSnapshot().status === "ready" && server ? "ready" : "error";
700
+ return action.fail(error, { status, refreshing: false });
701
+ }
702
+ }
703
+ function checkMerge(cart) {
704
+ const guestSkus = mergeCheck;
705
+ mergeCheck = null;
706
+ if (!guestSkus) return null;
707
+ const present = new Set(cart.lines.map((line) => line.skuId));
708
+ const missingSkuIds = guestSkus.filter((skuId) => !present.has(skuId));
709
+ return missingSkuIds.length ? { missingSkuIds } : null;
710
+ }
711
+ function settledChanges() {
712
+ const forget = [];
713
+ for (const [map, entries] of [
714
+ [quantities, [...quantities]],
715
+ [removals, [...removals]]
716
+ ]) {
717
+ for (const [lineId, change] of entries) {
718
+ if (!change.settled) continue;
719
+ forget.push(() => {
720
+ if (map.get(lineId) === change) map.delete(lineId);
721
+ });
722
+ }
723
+ }
724
+ const type = orderType;
725
+ if (type?.settled) {
726
+ forget.push(() => {
727
+ if (orderType === type) orderType = null;
728
+ });
729
+ }
730
+ return forget;
731
+ }
732
+ async function refresh() {
733
+ await ctx.ready();
734
+ act("refresh");
735
+ return load();
736
+ }
737
+ async function add(input) {
738
+ await ctx.ready();
739
+ const action = act("add");
740
+ update({ pending: { ...pending(), adding: [...pending().adding, input.skuId] } });
741
+ try {
742
+ if (!isCustomer()) await ctx.ensureSession();
743
+ const result = await guard(action, () => serial(() => ctx.client.cart.addItem(input)));
744
+ if (!result.current) return fail(SUPERSEDED2);
745
+ update({ pending: { ...pending(), adding: withoutOne(pending().adding, input.skuId) } });
746
+ await load();
747
+ return ok({ lineId: result.value.lineId });
748
+ } catch (error) {
749
+ return action.fail(error, {
750
+ pending: { ...pending(), adding: withoutOne(pending().adding, input.skuId) }
751
+ });
752
+ }
753
+ }
754
+ async function setQuantity(lineId, quantity) {
755
+ await ctx.ready();
756
+ if (quantity === 0) return remove(lineId);
757
+ const action = act("setQuantity");
758
+ if (!Number.isInteger(quantity) || quantity < 0) {
759
+ return action.refuse("validation", `Cart quantity must be a whole number: ${quantity}.`);
760
+ }
761
+ const line = findLine(lineId);
762
+ if (!line) return action.refuse("validation", `There is no cart line ${lineId}.`);
763
+ if (line.maxQuantity !== null && quantity > line.maxQuantity) {
764
+ return action.refuse(
765
+ "validation",
766
+ `At most ${line.maxQuantity} of this item can be ordered.`
767
+ );
768
+ }
769
+ const change = { value: quantity, settled: false };
770
+ quantities.set(lineId, change);
771
+ touch();
772
+ render({ pending: withLine(pending(), lineId, "quantity") });
773
+ try {
774
+ const result = await guard(
775
+ action,
776
+ () => coalesce(
777
+ lineId,
778
+ quantity,
779
+ (value) => serial(async () => {
780
+ if (gone.has(lineId) || removals.has(lineId)) return;
781
+ await ctx.client.cart.setQuantity(lineId, value);
782
+ })
783
+ )
784
+ );
785
+ if (!result.current || !result.value.current) return fail(SUPERSEDED2);
786
+ change.settled = true;
787
+ update({ pending: withLine(pending(), lineId, null, "quantity") });
788
+ await load();
789
+ return ok(void 0);
790
+ } catch (error) {
791
+ if (quantities.get(lineId) === change) quantities.delete(lineId);
792
+ touch();
793
+ const failed = action.fail(error, {
794
+ ...view(),
795
+ pending: withLine(pending(), lineId, null, "quantity")
796
+ });
797
+ if (action.alive()) await load();
798
+ return failed;
799
+ }
800
+ }
801
+ async function remove(lineId) {
802
+ await ctx.ready();
803
+ const action = act("remove");
804
+ if (!findLine(lineId)) return action.refuse("validation", `There is no cart line ${lineId}.`);
805
+ const change = { value: true, settled: false };
806
+ removals.set(lineId, change);
807
+ touch();
808
+ render({ pending: withLine(pending(), lineId, "remove") });
809
+ try {
810
+ const result = await guard(action, () => serial(() => ctx.client.cart.removeItem(lineId)));
811
+ if (!result.current) return fail(SUPERSEDED2);
812
+ change.settled = true;
813
+ gone.add(lineId);
814
+ update({ pending: withLine(pending(), lineId, null) });
815
+ await load();
816
+ return ok(void 0);
817
+ } catch (error) {
818
+ if (removals.get(lineId) === change) removals.delete(lineId);
819
+ touch();
820
+ const failed = action.fail(error, { ...view(), pending: withLine(pending(), lineId, null) });
821
+ if (action.alive()) await load();
822
+ return failed;
823
+ }
824
+ }
825
+ async function setOrderType(type) {
826
+ await ctx.ready();
827
+ const action = act("setOrderType");
828
+ const cart = view().cart;
829
+ if (!cart) return action.refuse("validation", "There is no cart yet.");
830
+ if (cart.type === type) return ok(void 0);
831
+ const change = { value: type, settled: false };
832
+ orderType = change;
833
+ touch();
834
+ render({ pending: { ...pending(), orderType: true } });
835
+ try {
836
+ const result = await guard(action, () => serial(() => ctx.client.cart.setOrderType(type)));
837
+ if (!result.current) return fail(SUPERSEDED2);
838
+ change.settled = true;
839
+ update({ pending: { ...pending(), orderType: false } });
840
+ await load();
841
+ return ok(void 0);
842
+ } catch (error) {
843
+ if (orderType === change) orderType = null;
844
+ touch();
845
+ const failed = action.fail(error, { ...view(), pending: { ...pending(), orderType: false } });
846
+ if (action.alive()) await load();
847
+ return failed;
848
+ }
849
+ }
850
+ async function setAddresses(input) {
851
+ await ctx.ready();
852
+ const action = act("setAddresses");
853
+ if (!isCustomer()) return action.refuse("signed-out", "Sign in to choose addresses.");
854
+ update({ pending: { ...pending(), addresses: true } });
855
+ try {
856
+ const result = await guard(action, () => serial(() => ctx.client.cart.setAddresses(input)));
857
+ if (!result.current) return fail(SUPERSEDED2);
858
+ update({ pending: { ...pending(), addresses: false } });
859
+ await load();
860
+ return ok(void 0);
861
+ } catch (error) {
862
+ return action.fail(error, { pending: { ...pending(), addresses: false } });
863
+ }
864
+ }
865
+ async function setTaxDetails(input) {
866
+ await ctx.ready();
867
+ const action = act("setTaxDetails");
868
+ update({ pending: { ...pending(), taxDetails: true } });
869
+ try {
870
+ await ctx.ensureSession();
871
+ const result = await guard(action, () => serial(() => ctx.client.cart.setTaxDetails(input)));
872
+ if (!result.current) return fail(SUPERSEDED2);
873
+ update({ pending: { ...pending(), taxDetails: false } });
874
+ return ok(result.value);
875
+ } catch (error) {
876
+ return action.fail(error, { pending: { ...pending(), taxDetails: false } });
877
+ }
878
+ }
879
+ async function discount(action, run) {
880
+ update({ pending: { ...pending(), discount: true } });
881
+ let result;
882
+ try {
883
+ if (!isCustomer()) await ctx.ensureSession();
884
+ result = await run();
885
+ } catch (error) {
886
+ result = action.fail(error);
887
+ }
888
+ if (action.alive()) update({ pending: { ...pending(), discount: false } });
889
+ return result;
890
+ }
891
+ async function sendDiscount(action, send) {
892
+ let sent;
893
+ try {
894
+ sent = await guard(action, () => serial(send));
895
+ } catch (error) {
896
+ const failed = action.fail(error);
897
+ if (action.alive()) await load();
898
+ return failed;
899
+ }
900
+ if (!sent.current) return fail(SUPERSEDED2);
901
+ await load();
902
+ return ok(void 0);
903
+ }
904
+ async function applyOffer(offerId) {
905
+ await ctx.ready();
906
+ const action = act("applyOffer");
907
+ return discount(action, () => sendDiscount(action, () => ctx.client.cart.applyOffer(offerId)));
908
+ }
909
+ async function applyPromoCode(code) {
910
+ await ctx.ready();
911
+ const action = act("applyPromoCode");
912
+ if (code !== null && !code.trim()) return action.refuse("validation", "Enter a promo code.");
913
+ return discount(action, async () => {
914
+ if (code === null) {
915
+ const removed = await sendDiscount(action, () => ctx.client.cart.applyPromoCode(null));
916
+ return removed.ok ? ok(null) : removed;
917
+ }
918
+ const found = await guard(action, () => ctx.client.promotions.promoCode(code));
919
+ if (!found.current) return fail(SUPERSEDED2);
920
+ const promo = found.value;
921
+ if (!promo) return action.refuse("validation", "This promo code does not exist.");
922
+ if (promo.status !== "active") {
923
+ return action.fail(
924
+ storeError("validation", `This promo code is ${promo.status}.`, {
925
+ fieldErrors: { code: [promo.status] }
926
+ })
927
+ );
928
+ }
929
+ const applied = await sendDiscount(action, () => ctx.client.cart.applyPromoCode(promo.id));
930
+ return applied.ok ? ok(promo) : applied;
931
+ });
932
+ }
933
+ async function estimateDelivery(pincode) {
934
+ await ctx.ready();
935
+ const action = act("estimateDelivery");
936
+ const delivery = store.getSnapshot().delivery;
937
+ const refuse = (message) => {
938
+ const error = storeError("validation", message);
939
+ update({ delivery: { ...delivery, error } });
940
+ return fail(error);
941
+ };
942
+ const cart = view().cart;
943
+ if (!cart) return refuse("There is no cart yet.");
944
+ if (cart.deliveryChargeType !== "as_per_delivery_partner") {
945
+ return refuse("This business does not quote delivery charges.");
946
+ }
947
+ update({ delivery: { ...delivery, status: "loading", error: null } });
948
+ try {
949
+ const result = await guard(
950
+ action,
951
+ () => latestQuote(() => ctx.client.cart.deliveryEstimate(cart.id, { pincode }))
952
+ );
953
+ if (!result.current || !result.value.current) return fail(SUPERSEDED2);
954
+ const estimate = result.value.value;
955
+ update({ delivery: { status: "ready", estimate, error: null } });
956
+ await load();
957
+ return ok(estimate);
958
+ } catch (error) {
959
+ const failed = action.fail(error);
960
+ if (failed.ok || !action.alive() || failed.error.kind === "superseded") return failed;
961
+ const { estimate } = store.getSnapshot().delivery;
962
+ update({ delivery: { status: "error", estimate, error: failed.error } });
963
+ if (storedPayable(error)) await load();
964
+ return failed;
965
+ }
966
+ }
967
+ function dismissMergeWarning() {
968
+ update({ mergeWarning: null });
969
+ }
970
+ async function moveToWishlist(lineId) {
971
+ await ctx.ready();
972
+ const action = act("moveToWishlist");
973
+ if (!isCustomer()) return action.refuse("signed-out", "Sign in to save items.");
974
+ const skuId = findLine(lineId)?.skuId;
975
+ if (skuId == null) {
976
+ return action.refuse("validation", `Cart line ${lineId} has no item to save.`);
977
+ }
978
+ update({ pending: withLine(pending(), lineId, "wishlist") });
979
+ const wishlist = ctx.stores.wishlist;
980
+ if (!wishlist.has(skuId)) {
981
+ const added = await wishlist.add(skuId);
982
+ if (!action.alive()) return fail(SUPERSEDED2);
983
+ if (!added.ok) {
984
+ return action.fail(added.error, { pending: withLine(pending(), lineId, null) });
985
+ }
986
+ }
987
+ update({ pending: withLine(pending(), lineId, null) });
988
+ return remove(lineId);
989
+ }
990
+ ctx.autoLoad(() => void load());
991
+ ctx.onHydrate((read) => {
992
+ if (read.phase === "rehydrate") reset({});
993
+ });
994
+ ctx.events.on("auth:signed-in", () => {
995
+ const guestSkus = /* @__PURE__ */ new Set();
996
+ for (const line of server?.lines ?? []) if (line.skuId !== null) guestSkus.add(line.skuId);
997
+ mergeCheck = guestSkus.size ? [...guestSkus] : null;
998
+ needsLoad = true;
999
+ reset({ status: "loading" });
1000
+ void load();
1001
+ });
1002
+ ctx.events.on("auth:signed-out", () => {
1003
+ needsLoad = false;
1004
+ mergeCheck = null;
1005
+ reset({ status: "ready" });
1006
+ });
1007
+ ctx.events.on("session:replaced", ({ reason }) => {
1008
+ if (isCustomer()) {
1009
+ if (needsLoad && ctx.tokens.getCustomerToken()) void load();
1010
+ return;
1011
+ }
1012
+ if (reason === "dead") {
1013
+ reset({ status: "ready" });
1014
+ void load();
1015
+ }
1016
+ });
1017
+ ctx.events.on("order:placed", () => {
1018
+ reset({ status: "loading" });
1019
+ void load();
1020
+ });
1021
+ return {
1022
+ ...readOnly(store),
1023
+ refresh,
1024
+ add,
1025
+ setQuantity,
1026
+ remove,
1027
+ setOrderType,
1028
+ setAddresses,
1029
+ setTaxDetails,
1030
+ applyOffer,
1031
+ applyPromoCode,
1032
+ estimateDelivery,
1033
+ moveToWishlist,
1034
+ dismissMergeWarning
1035
+ };
1036
+ }
1037
+ function storedPayable(error) {
1038
+ return error instanceof GrowupsellApiError && error.status === 400 && typeof error.body === "object" && error.body !== null && "final_payable_amount" in error.body;
1039
+ }
1040
+
1041
+ // src/stores/catalog.ts
1042
+ function createCatalogStore(ctx) {
1043
+ const seed = ctx.seeds.catalog;
1044
+ const loaded = {
1045
+ categories: seed?.categories !== void 0,
1046
+ settings: seed?.settings !== void 0
1047
+ };
1048
+ const complete = () => loaded.categories && loaded.settings;
1049
+ const store = createStore(
1050
+ {
1051
+ status: complete() ? "ready" : "idle",
1052
+ refreshing: false,
1053
+ error: null,
1054
+ categories: seed?.categories ?? [],
1055
+ settings: seed?.settings ?? null
1056
+ },
1057
+ { onListenerError: ctx.onListenerError }
1058
+ );
1059
+ const flight = createSingleFlight();
1060
+ const latest = createLatest();
1061
+ async function run(parts) {
1062
+ store.update(
1063
+ (s) => s.status === "ready" ? { ...s, refreshing: true, error: null } : { ...s, status: "loading", error: null }
1064
+ );
1065
+ const outcome = await latest(
1066
+ () => Promise.allSettled([
1067
+ parts.includes("categories") ? ctx.client.catalog.categories() : void 0,
1068
+ parts.includes("settings") ? ctx.client.catalog.settings() : void 0
1069
+ ])
1070
+ );
1071
+ if (!outcome.current || ctx.isDisposed()) {
1072
+ return fail(storeError("superseded", "A newer catalog load replaced this one."));
1073
+ }
1074
+ const [categories, settings] = outcome.value;
1075
+ let error = null;
1076
+ const next = { ...store.getSnapshot() };
1077
+ if (categories.status === "fulfilled" && parts.includes("categories")) {
1078
+ next.categories = categories.value;
1079
+ loaded.categories = true;
1080
+ } else if (categories.status === "rejected") error = toStoreError(categories.reason);
1081
+ if (settings.status === "fulfilled" && parts.includes("settings")) {
1082
+ next.settings = settings.value;
1083
+ loaded.settings = true;
1084
+ } else if (settings.status === "rejected") error = toStoreError(settings.reason);
1085
+ store.set({
1086
+ ...next,
1087
+ status: complete() ? "ready" : "error",
1088
+ refreshing: false,
1089
+ error
1090
+ });
1091
+ if (!error) return ok(void 0);
1092
+ ctx.events.emit({ type: "error", store: "catalog", action: "load", error });
1093
+ return fail(error);
1094
+ }
1095
+ return {
1096
+ ...readOnly(store),
1097
+ load(options = {}) {
1098
+ if (options.force) return run(["categories", "settings"]);
1099
+ const missing = ["categories", "settings"].filter((part) => !loaded[part]);
1100
+ if (missing.length === 0) return Promise.resolve(ok(void 0));
1101
+ return flight("load", () => run(missing));
1102
+ }
1103
+ };
1104
+ }
1105
+
1106
+ // src/stores/selectors.ts
1107
+ function cartItemCount(state) {
1108
+ return state.cart?.lines.reduce((sum, line) => sum + line.quantity, 0) ?? 0;
1109
+ }
1110
+ function cartLineForSku(state, skuId) {
1111
+ return state.cart?.lines.find((line) => line.skuId === skuId && line.customData == null) ?? null;
1112
+ }
1113
+ function isInWishlist(state, skuId) {
1114
+ return state.items.some((item) => item.sku?.id === skuId);
1115
+ }
1116
+ function checkoutPreviewFor(cart, checkout, auth) {
1117
+ if (!cart.cart) return null;
1118
+ const { rewardSettings } = checkout;
1119
+ return checkoutPreview({
1120
+ payable: cart.cart.payable,
1121
+ subtotal: cart.cart.subtotal,
1122
+ giftCards: checkout.giftCards,
1123
+ rewardPoints: checkout.useRewardPoints && rewardSettings ? { available: auth.rewardPoints ?? 0, currencyPerPoint: rewardSettings.currencyPerPoint } : void 0
1124
+ });
1125
+ }
1126
+ function paymentOptionsFor(cart, checkout, auth, features) {
1127
+ if (!cart.cart || !checkout.settings || !checkout.rewardSettings) return null;
1128
+ return paymentOptions({
1129
+ cart: cart.cart,
1130
+ settings: checkout.settings,
1131
+ rewardPoints: checkout.rewardSettings,
1132
+ availablePoints: auth.rewardPoints ?? 0,
1133
+ features,
1134
+ giftCardsSelected: checkout.giftCards.length > 0
1135
+ });
1136
+ }
1137
+
1138
+ // src/stores/checkout.ts
1139
+ var INITIAL2 = {
1140
+ settings: null,
1141
+ rewardSettings: null,
1142
+ settingsStatus: "idle",
1143
+ giftCards: [],
1144
+ useRewardPoints: false,
1145
+ phase: "idle",
1146
+ order: null,
1147
+ pending: { settings: false, giftCard: false },
1148
+ error: null
1149
+ };
1150
+ var ORDER_IN_PROGRESS = /* @__PURE__ */ new Set([
1151
+ "placing",
1152
+ "awaiting-payment",
1153
+ "confirming",
1154
+ "outcome-unknown"
1155
+ ]);
1156
+ var superseded = () => storeError("superseded", "The shopper changed while checkout ran.");
1157
+ function paymentStatusOf(detail, paymentId) {
1158
+ const entry = paymentId != null && detail.payments.find((p) => p.id === paymentId) || detail.payments.at(-1);
1159
+ return entry?.status ?? null;
1160
+ }
1161
+ function createCheckoutStore(ctx) {
1162
+ const store = createStore(INITIAL2, { onListenerError: ctx.onListenerError });
1163
+ const flight = createSingleFlight();
1164
+ const currentCart = () => ctx.stores.cart.getSnapshot().cart;
1165
+ const rewardPoints = () => ctx.stores.auth.getSnapshot().rewardPoints;
1166
+ const features = () => ctx.stores.business.getSnapshot().business?.features ?? null;
1167
+ const whenReady = () => ctx.identity.getSnapshot().kind === "unknown" ? ctx.ready() : void 0;
1168
+ const patch = (changes) => store.update((s) => ({ ...s, ...changes }));
1169
+ const setPending = (key, value) => store.update((s) => ({ ...s, pending: { ...s.pending, [key]: value } }));
1170
+ function failWith(action, error, changes = {}) {
1171
+ if (error.kind !== "superseded") {
1172
+ patch({ ...changes, error });
1173
+ ctx.events.emit({ type: "error", store: "checkout", action, error });
1174
+ } else if (Object.keys(changes).length) {
1175
+ patch(changes);
1176
+ }
1177
+ return fail(error);
1178
+ }
1179
+ const refuse = (action, message, kind = "validation") => failWith(action, storeError(kind, message));
1180
+ const startAction = () => {
1181
+ if (store.getSnapshot().error) patch({ error: null });
1182
+ };
1183
+ let watching = false;
1184
+ function watchCart() {
1185
+ if (watching) return;
1186
+ watching = true;
1187
+ const unsubscribe = ctx.stores.cart.subscribe(() => {
1188
+ const s = store.getSnapshot();
1189
+ if (currentCart()?.type === "cod" && (s.giftCards.length > 0 || s.useRewardPoints)) {
1190
+ patch({ giftCards: [], useRewardPoints: false });
1191
+ }
1192
+ });
1193
+ ctx.onDispose(unsubscribe);
1194
+ }
1195
+ ctx.events.on("auth:signed-out", () => {
1196
+ const { settings, rewardSettings, settingsStatus } = store.getSnapshot();
1197
+ store.set({ ...INITIAL2, settings, rewardSettings, settingsStatus });
1198
+ });
1199
+ function preview() {
1200
+ return checkoutPreviewFor({ cart: currentCart() }, store.getSnapshot(), {
1201
+ rewardPoints: rewardPoints()
1202
+ });
1203
+ }
1204
+ function loadSettings() {
1205
+ return flight("settings", async () => {
1206
+ startAction();
1207
+ store.update((s) => ({
1208
+ ...s,
1209
+ settingsStatus: s.settingsStatus === "ready" ? "ready" : "loading",
1210
+ pending: { ...s.pending, settings: true }
1211
+ }));
1212
+ try {
1213
+ await ctx.ready();
1214
+ const [settings, rewardSettings] = await Promise.all([
1215
+ ctx.client.checkout.settings(),
1216
+ ctx.client.promotions.rewardPointSettings()
1217
+ ]);
1218
+ store.update((s) => ({
1219
+ ...s,
1220
+ settings,
1221
+ rewardSettings,
1222
+ settingsStatus: "ready",
1223
+ pending: { ...s.pending, settings: false }
1224
+ }));
1225
+ return ok(void 0);
1226
+ } catch (error) {
1227
+ setPending("settings", false);
1228
+ const failed = toStoreError(error);
1229
+ return failWith("loadSettings", failed, {
1230
+ settingsStatus: store.getSnapshot().settingsStatus === "ready" ? "ready" : "error"
1231
+ });
1232
+ }
1233
+ });
1234
+ }
1235
+ function giftCardRefusal() {
1236
+ const s = store.getSnapshot();
1237
+ if (s.useRewardPoints) {
1238
+ return "Gift cards and reward points cannot be combined; turn reward points off first.";
1239
+ }
1240
+ if (currentCart()?.type === "cod") return "Gift cards cannot pay a cash-on-delivery order.";
1241
+ if (s.giftCards.length > 0 && preview()?.toPay === 0) {
1242
+ return "The gift cards already pay the whole order.";
1243
+ }
1244
+ return null;
1245
+ }
1246
+ async function addGiftCard(input) {
1247
+ await ctx.ready();
1248
+ startAction();
1249
+ const code = input.code?.trim() ?? "";
1250
+ const pin = input.pin?.trim() ?? "";
1251
+ if (!code || !pin) return refuse("addGiftCard", "Enter the gift card code and PIN.");
1252
+ const held = store.getSnapshot().giftCards;
1253
+ if (held.some((c) => c.code.toUpperCase() === code.toUpperCase())) {
1254
+ return refuse("addGiftCard", "This gift card is already applied.");
1255
+ }
1256
+ const refusal = giftCardRefusal();
1257
+ if (refusal) return refuse("addGiftCard", refusal);
1258
+ watchCart();
1259
+ setPending("giftCard", true);
1260
+ let card;
1261
+ try {
1262
+ const result = await ctx.inEpoch(() => ctx.client.promotions.validateGiftCard({ code, pin }));
1263
+ if (!result.current) {
1264
+ setPending("giftCard", false);
1265
+ return fail(superseded());
1266
+ }
1267
+ card = result.value;
1268
+ } catch (error) {
1269
+ setPending("giftCard", false);
1270
+ return failWith("addGiftCard", toStoreError(error));
1271
+ }
1272
+ setPending("giftCard", false);
1273
+ if (store.getSnapshot().giftCards.some((c) => c.id === card.id)) {
1274
+ return refuse("addGiftCard", "This gift card is already applied.");
1275
+ }
1276
+ const late = giftCardRefusal();
1277
+ if (late) return refuse("addGiftCard", late);
1278
+ store.update((s) => ({ ...s, giftCards: [...s.giftCards, card] }));
1279
+ return ok(card);
1280
+ }
1281
+ function removeGiftCard(giftCardId) {
1282
+ store.update(
1283
+ (s) => s.giftCards.some((c) => c.id === giftCardId) ? { ...s, giftCards: s.giftCards.filter((c) => c.id !== giftCardId) } : s
1284
+ );
1285
+ }
1286
+ function setUseRewardPoints(use) {
1287
+ startAction();
1288
+ const s = store.getSnapshot();
1289
+ if (!use) {
1290
+ if (s.useRewardPoints) patch({ useRewardPoints: false });
1291
+ return ok(void 0);
1292
+ }
1293
+ if (s.giftCards.length > 0) {
1294
+ return refuse(
1295
+ "setUseRewardPoints",
1296
+ "Gift cards and reward points cannot be combined; remove the gift cards first."
1297
+ );
1298
+ }
1299
+ const cart = currentCart();
1300
+ if (!cart) return refuse("setUseRewardPoints", "The cart is not loaded.");
1301
+ if (cart.type === "cod") {
1302
+ return refuse("setUseRewardPoints", "Reward points cannot pay a cash-on-delivery order.");
1303
+ }
1304
+ if (!s.settings || !s.rewardSettings) {
1305
+ return refuse("setUseRewardPoints", "Load the checkout settings first.");
1306
+ }
1307
+ const options = paymentOptionsFor(
1308
+ { cart },
1309
+ s,
1310
+ { rewardPoints: rewardPoints() },
1311
+ features() ?? ["giftcard", "reward-point"]
1312
+ );
1313
+ if (!options?.rewardPoints) {
1314
+ return refuse("setUseRewardPoints", "Reward points cannot be used for this order.");
1315
+ }
1316
+ watchCart();
1317
+ patch({ useRewardPoints: true });
1318
+ return ok(void 0);
1319
+ }
1320
+ function orderPlaced(order, pointsUsed, payment) {
1321
+ store.update((s) => ({
1322
+ ...s,
1323
+ phase: payment === "none" ? "placed" : "awaiting-payment",
1324
+ order,
1325
+ giftCards: [],
1326
+ useRewardPoints: false,
1327
+ error: null
1328
+ }));
1329
+ if (pointsUsed > 0) ctx.events.emit({ type: "checkout:points-spent", points: pointsUsed });
1330
+ ctx.events.emit({ type: "order:placed", orderId: order.orderId, payment });
1331
+ }
1332
+ async function place() {
1333
+ const ready = whenReady();
1334
+ if (ready) await ready;
1335
+ startAction();
1336
+ const s = store.getSnapshot();
1337
+ if (ctx.identity.getSnapshot().kind !== "customer") {
1338
+ return refuse("placeOrder", "Log in to place the order.", "signed-out");
1339
+ }
1340
+ if (ORDER_IN_PROGRESS.has(s.phase)) {
1341
+ return refuse("placeOrder", "This order is already placed; finish or check its payment.");
1342
+ }
1343
+ const cart = currentCart();
1344
+ if (!cart || cart.lines.length === 0) return refuse("placeOrder", "The cart is empty.");
1345
+ if (s.order && s.order.orderId === cart.id) {
1346
+ return refuse("placeOrder", "This cart has already been placed as an order.");
1347
+ }
1348
+ if (requiresShippingAddress(cart.lines) && cart.shippingAddressId == null) {
1349
+ return refuse("placeOrder", "Choose a shipping address.");
1350
+ }
1351
+ if (cart.type === "cod" && (s.giftCards.length > 0 || s.useRewardPoints)) {
1352
+ return refuse("placeOrder", "Gift cards and reward points cannot pay cash on delivery.");
1353
+ }
1354
+ const pointsUsed = s.useRewardPoints ? preview()?.pointsUsed ?? 0 : 0;
1355
+ const giftCardIds = s.giftCards.map((c) => c.id);
1356
+ const useRewardPoints = s.useRewardPoints;
1357
+ patch({ phase: "placing" });
1358
+ let outcome;
1359
+ try {
1360
+ outcome = await ctx.inEpoch(
1361
+ () => ctx.client.checkout.placeOrder(cart.id, { giftCardIds, useRewardPoints })
1362
+ );
1363
+ } catch (error) {
1364
+ const failed = toStoreError(error);
1365
+ if (failed.kind === "timeout" || failed.kind === "network") {
1366
+ return afterUnknownOutcome(cart.id, failed, pointsUsed);
1367
+ }
1368
+ return failWith("placeOrder", failed, { phase: "idle" });
1369
+ }
1370
+ if (!outcome.current) return failWith("placeOrder", superseded(), { phase: "idle" });
1371
+ const result = outcome.value;
1372
+ if (result.status === "placed") {
1373
+ orderPlaced(
1374
+ { orderId: result.orderId, payment: null, paymentStatus: null },
1375
+ pointsUsed,
1376
+ "none"
1377
+ );
1378
+ } else {
1379
+ orderPlaced(
1380
+ { orderId: result.orderId, payment: result.payment, paymentStatus: null },
1381
+ pointsUsed,
1382
+ "required"
1383
+ );
1384
+ }
1385
+ return ok(result);
1386
+ }
1387
+ async function afterUnknownOutcome(cartId, cause, pointsUsed) {
1388
+ patch({ phase: "outcome-unknown" });
1389
+ let read;
1390
+ try {
1391
+ read = await ctx.inEpoch(() => ctx.client.orders.detail(cartId));
1392
+ } catch {
1393
+ return failWith("placeOrder", cause, { phase: "outcome-unknown" });
1394
+ }
1395
+ if (!read.current) return failWith("placeOrder", superseded(), { phase: "idle" });
1396
+ const detail = read.value;
1397
+ if (detail.status === "cart") return failWith("placeOrder", cause, { phase: "idle" });
1398
+ orderPlaced(
1399
+ { orderId: cartId, payment: null, paymentStatus: paymentStatusOf(detail) },
1400
+ pointsUsed,
1401
+ "none"
1402
+ );
1403
+ return ok({ status: "placed", orderId: cartId });
1404
+ }
1405
+ function confirm(action, method, send, options = {}) {
1406
+ return flight("confirm", async () => {
1407
+ const ready = whenReady();
1408
+ if (ready) await ready;
1409
+ startAction();
1410
+ const s = store.getSnapshot();
1411
+ const order = s.order;
1412
+ if (s.phase !== "awaiting-payment" || !order?.payment) {
1413
+ return refuse(action, "There is no payment waiting for confirmation.");
1414
+ }
1415
+ if (method && order.payment.method !== method) {
1416
+ return refuse(action, `The pending payment is not a ${method} payment.`);
1417
+ }
1418
+ return runConfirmation(
1419
+ action,
1420
+ order.orderId,
1421
+ order.payment,
1422
+ () => send(order.payment),
1423
+ options
1424
+ );
1425
+ });
1426
+ }
1427
+ async function runConfirmation(action, orderId, payment, send, options = {}) {
1428
+ const paymentId = options.paymentId ?? payment?.id;
1429
+ patch({
1430
+ phase: "confirming",
1431
+ order: { orderId, payment, paymentStatus: store.getSnapshot().order?.paymentStatus ?? null }
1432
+ });
1433
+ let sendError = null;
1434
+ try {
1435
+ const sent = await ctx.inEpoch(send);
1436
+ if (!sent.current) return failWith(action, superseded());
1437
+ } catch (error) {
1438
+ sendError = toStoreError(error);
1439
+ }
1440
+ let status;
1441
+ try {
1442
+ const read = await ctx.inEpoch(() => ctx.client.orders.detail(orderId));
1443
+ if (!read.current) return failWith(action, superseded());
1444
+ status = paymentStatusOf(read.value, paymentId);
1445
+ } catch (error) {
1446
+ return failWith(action, sendError ?? toStoreError(error), { phase: "awaiting-payment" });
1447
+ }
1448
+ const done = status === "paid" || options.endPlaced === true && sendError == null;
1449
+ store.update((st) => ({
1450
+ ...st,
1451
+ phase: done ? "placed" : "awaiting-payment",
1452
+ order: { orderId, payment, paymentStatus: status }
1453
+ }));
1454
+ if (done) {
1455
+ ctx.events.emit({ type: "payment:confirmed", orderId, paymentStatus: status ?? "unpaid" });
1456
+ return ok(status ?? "unpaid");
1457
+ }
1458
+ if (sendError) return failWith(action, sendError);
1459
+ return ok(status ?? "unpaid");
1460
+ }
1461
+ function confirmPhonePe(input) {
1462
+ return flight("confirm", async () => {
1463
+ const ready = whenReady();
1464
+ if (ready) await ready;
1465
+ startAction();
1466
+ const s = store.getSnapshot();
1467
+ if (s.phase === "placing") {
1468
+ return refuse("confirmPhonePe", "The order is still being placed.");
1469
+ }
1470
+ const known = s.order?.payment?.id === input.paymentId ? s.order.payment : null;
1471
+ return runConfirmation(
1472
+ "confirmPhonePe",
1473
+ input.orderId,
1474
+ known,
1475
+ () => ctx.client.payments.confirmPhonePe(input.paymentId),
1476
+ { paymentId: input.paymentId }
1477
+ );
1478
+ });
1479
+ }
1480
+ function reset() {
1481
+ const { settings, rewardSettings, settingsStatus } = store.getSnapshot();
1482
+ store.set({ ...INITIAL2, settings, rewardSettings, settingsStatus });
1483
+ }
1484
+ return {
1485
+ ...readOnly(store),
1486
+ loadSettings,
1487
+ addGiftCard,
1488
+ removeGiftCard,
1489
+ setUseRewardPoints,
1490
+ placeOrder: () => flight("place", place),
1491
+ confirmStripe: () => confirm("confirmStripe", "stripe", (payment) => ctx.client.payments.confirmStripe(payment)),
1492
+ confirmRazorpay: (result) => confirm(
1493
+ "confirmRazorpay",
1494
+ "razorpay",
1495
+ (payment) => ctx.client.payments.confirmRazorpay(payment, result)
1496
+ ),
1497
+ confirmPhonePe,
1498
+ submitTransferProof: (screenshot) => confirm(
1499
+ "submitTransferProof",
1500
+ "direct-transfer",
1501
+ (payment) => ctx.client.payments.submitTransferProof(payment.id, screenshot),
1502
+ { endPlaced: true }
1503
+ ),
1504
+ paymentDismissed: () => {
1505
+ },
1506
+ reset
1507
+ };
1508
+ }
1509
+
1510
+ // src/stores/listing.ts
1511
+ var runtime2 = globalThis;
1512
+ function derive2(base) {
1513
+ return {
1514
+ ...base,
1515
+ pageCount: Math.max(1, Math.ceil(base.total / base.pageSize)),
1516
+ query: listingQuery(base.criteria)
1517
+ };
1518
+ }
1519
+ var sameCriteria = (a, b) => JSON.stringify(a) === JSON.stringify(b);
1520
+ var toggle = (list, value) => list.includes(value) ? list.filter((v) => v !== value) : [...list, value];
1521
+ var emptyFacets = {
1522
+ status: "idle",
1523
+ price: null,
1524
+ specifications: [],
1525
+ brands: [],
1526
+ variantGroups: []
1527
+ };
1528
+ function initialCriteria(input) {
1529
+ const criteria = parseListingQuery(input.query ?? {});
1530
+ if (criteria.categoryId === null && input.categoryId != null) {
1531
+ return { ...criteria, categoryId: input.categoryId };
1532
+ }
1533
+ return criteria;
1534
+ }
1535
+ function createListingStore(ctx, input = {}) {
1536
+ const pageSize = input.pageSize && Number.isInteger(input.pageSize) && input.pageSize > 0 ? input.pageSize : LISTING_PAGE_SIZE;
1537
+ const store = createStore(
1538
+ derive2({
1539
+ criteria: initialCriteria(input),
1540
+ status: "idle",
1541
+ refreshing: false,
1542
+ items: [],
1543
+ total: 0,
1544
+ pageSize,
1545
+ error: null,
1546
+ facets: emptyFacets
1547
+ }),
1548
+ { onListenerError: ctx.onListenerError }
1549
+ );
1550
+ const searches = createLatest();
1551
+ const facetLoads = createLatest();
1552
+ let disposed = false;
1553
+ let started = false;
1554
+ let scheduled = false;
1555
+ let facetsFor;
1556
+ const set = (changes) => {
1557
+ if (!disposed) store.set(derive2({ ...store.getSnapshot(), ...changes }));
1558
+ };
1559
+ async function expand(categoryId) {
1560
+ const { catalog } = ctx.stores;
1561
+ await catalog.load();
1562
+ return categoryAndDescendants(catalog.getSnapshot().categories, categoryId);
1563
+ }
1564
+ async function attempt(criteria2) {
1565
+ await ctx.ready();
1566
+ const epoch = ctx.identity.getSnapshot().epoch;
1567
+ const changed = () => ctx.identity.getSnapshot().epoch !== epoch;
1568
+ try {
1569
+ await ctx.ensureSession();
1570
+ const ids = criteria2.categoryId === null ? [] : await expand(criteria2.categoryId);
1571
+ const page = await ctx.client.products.search(listingSearchOptions(criteria2, ids, pageSize));
1572
+ return { page, epochChanged: changed() };
1573
+ } catch (caught) {
1574
+ return { error: toStoreError(caught, { sentSession: true }), epochChanged: changed() };
1575
+ }
1576
+ }
1577
+ async function fetchPage(criteria2) {
1578
+ let result = await attempt(criteria2);
1579
+ if (result.epochChanged && !disposed) result = await attempt(criteria2);
1580
+ if ("error" in result) throw result.error;
1581
+ return result.page;
1582
+ }
1583
+ async function search() {
1584
+ if (disposed) return fail(storeError("superseded", "listing: the store was disposed."));
1585
+ const { criteria: criteria2, status } = store.getSnapshot();
1586
+ set(status === "ready" ? { refreshing: true } : { status: "loading", refreshing: false });
1587
+ try {
1588
+ const result = await searches(() => fetchPage(criteria2));
1589
+ if (!result.current || disposed) {
1590
+ return fail(storeError("superseded", "listing: a newer search replaced this one."));
1591
+ }
1592
+ set({
1593
+ status: "ready",
1594
+ refreshing: false,
1595
+ items: result.value.items,
1596
+ total: result.value.total,
1597
+ error: null
1598
+ });
1599
+ return ok(void 0);
1600
+ } catch (caught) {
1601
+ const error = toStoreError(caught);
1602
+ if (disposed) return fail(error);
1603
+ const shown = store.getSnapshot().status === "ready";
1604
+ set(shown ? { refreshing: false, error } : { status: "error", refreshing: false, error });
1605
+ ctx.events.emit({ type: "error", store: "listing", action: "search", error });
1606
+ return fail(error);
1607
+ }
1608
+ }
1609
+ async function loadFacets(categoryId) {
1610
+ facetsFor = categoryId;
1611
+ set({
1612
+ facets: {
1613
+ ...categoryId === null ? emptyFacets : store.getSnapshot().facets,
1614
+ status: "loading"
1615
+ }
1616
+ });
1617
+ const result = await facetLoads(async () => {
1618
+ const ids = categoryId === null ? [] : await expand(categoryId);
1619
+ return Promise.allSettled([
1620
+ ctx.client.products.filterConfig(categoryId === null ? {} : { categoryId }),
1621
+ categoryId === null ? [] : ctx.client.catalog.brands({ categoryIds: ids }),
1622
+ categoryId === null ? [] : ctx.client.products.variantFilters(ids)
1623
+ ]);
1624
+ });
1625
+ if (!result.current || disposed) return;
1626
+ const [config, brands, variants] = result.value;
1627
+ const failed = [config, brands, variants].find((r) => r.status === "rejected");
1628
+ const facets = {
1629
+ status: failed ? "error" : "ready",
1630
+ price: config.status === "fulfilled" ? config.value.price : null,
1631
+ specifications: config.status === "fulfilled" ? config.value.specifications : [],
1632
+ brands: brands.status === "fulfilled" ? brands.value : [],
1633
+ variantGroups: variants.status === "fulfilled" ? variants.value : []
1634
+ };
1635
+ if (failed) {
1636
+ ctx.events.emit({
1637
+ type: "error",
1638
+ store: "listing",
1639
+ action: "facets",
1640
+ error: toStoreError(failed.reason)
1641
+ });
1642
+ }
1643
+ const { criteria: criteria2 } = store.getSnapshot();
1644
+ if (config.status === "fulfilled" && criteria2.specifications.length > 0) {
1645
+ const offered = new Set(config.value.specifications.map((s) => s.id));
1646
+ const kept = criteria2.specifications.filter((s) => offered.has(s.id));
1647
+ if (kept.length !== criteria2.specifications.length) {
1648
+ set({ facets, criteria: { ...criteria2, specifications: kept, page: 1 } });
1649
+ schedule();
1650
+ return;
1651
+ }
1652
+ }
1653
+ set({ facets });
1654
+ }
1655
+ function run() {
1656
+ started = true;
1657
+ const { criteria: criteria2, facets } = store.getSnapshot();
1658
+ if (facetsFor === void 0 || facetsFor !== criteria2.categoryId || facets.status === "error") {
1659
+ void loadFacets(criteria2.categoryId);
1660
+ }
1661
+ return search();
1662
+ }
1663
+ function schedule() {
1664
+ started = true;
1665
+ if (scheduled || disposed) return;
1666
+ scheduled = true;
1667
+ runtime2.queueMicrotask(() => {
1668
+ scheduled = false;
1669
+ if (!disposed) void run();
1670
+ });
1671
+ }
1672
+ function change(next) {
1673
+ if (disposed) return;
1674
+ const { criteria: criteria2 } = store.getSnapshot();
1675
+ if (sameCriteria(next, criteria2)) return;
1676
+ if (next.categoryId !== criteria2.categoryId) {
1677
+ set({
1678
+ criteria: next,
1679
+ items: [],
1680
+ total: 0,
1681
+ status: "loading",
1682
+ refreshing: false,
1683
+ error: null
1684
+ });
1685
+ } else {
1686
+ set({ criteria: next });
1687
+ }
1688
+ schedule();
1689
+ }
1690
+ const filter = (changes) => change({ ...store.getSnapshot().criteria, ...changes, page: 1 });
1691
+ const criteria = () => store.getSnapshot().criteria;
1692
+ function dispose() {
1693
+ if (disposed) return;
1694
+ disposed = true;
1695
+ unregister();
1696
+ }
1697
+ const unregister = ctx.onDispose(dispose);
1698
+ return {
1699
+ getSnapshot: store.getSnapshot,
1700
+ getServerSnapshot: store.getServerSnapshot,
1701
+ subscribe(listener) {
1702
+ if (disposed) return () => {
1703
+ };
1704
+ const unsubscribe = store.subscribe(() => {
1705
+ if (!disposed) listener();
1706
+ });
1707
+ if (!started) schedule();
1708
+ return unsubscribe;
1709
+ },
1710
+ setCategory: (id) => change(withCategory(criteria(), id)),
1711
+ setSearch: (text) => filter({ search: text?.trim() || null }),
1712
+ toggleBrand: (id) => filter({ brandIds: toggle(criteria().brandIds, id).sort((a, b) => a - b) }),
1713
+ toggleVariant: (label) => filter({ variantLabels: toggle(criteria().variantLabels, label) }),
1714
+ toggleSpecification(id, value) {
1715
+ const current = criteria().specifications;
1716
+ const found = current.find((s) => s.id === id);
1717
+ const specifications = found ? current.map((s) => s.id === id ? { id, values: toggle(s.values, value) } : s).filter((s) => s.values.length > 0) : [...current, { id, values: [value] }];
1718
+ filter({ specifications });
1719
+ },
1720
+ setPriceRange(min, max) {
1721
+ const [minPrice, maxPrice] = min !== null && max !== null && min > max ? [max, min] : [min, max];
1722
+ filter({ minPrice, maxPrice });
1723
+ },
1724
+ setMinDiscount: (percent) => filter({ minDiscountPercent: percent }),
1725
+ setMinRating: (rating) => filter({ minRating: rating }),
1726
+ setSort: (sortBy) => filter({ sortBy }),
1727
+ setPage(page) {
1728
+ if (Number.isInteger(page) && page >= 1) change({ ...criteria(), page });
1729
+ },
1730
+ clearFilters() {
1731
+ const { categoryId, search: search2, sortBy } = criteria();
1732
+ change({ ...DEFAULT_LISTING_CRITERIA, categoryId, search: search2, sortBy });
1733
+ },
1734
+ applyQuery: (query) => change(parseListingQuery(query)),
1735
+ retry: () => disposed ? search() : run(),
1736
+ dispose
1737
+ };
1738
+ }
1739
+
1740
+ // src/stores/productDetail.ts
1741
+ function derive3(base) {
1742
+ const { product, selection } = base;
1743
+ if (!product) {
1744
+ return {
1745
+ ...base,
1746
+ sku: null,
1747
+ price: null,
1748
+ mrp: null,
1749
+ discountPercent: 0,
1750
+ complete: false,
1751
+ purchasable: false,
1752
+ choices: [],
1753
+ specifications: [],
1754
+ images: [],
1755
+ selectedVariantsParam: formatSelectedVariants(selection)
1756
+ };
1757
+ }
1758
+ const complete = isSelectionComplete(product.variantGroups, selection);
1759
+ const sku = complete ? resolveSku(product.skus, selection) : null;
1760
+ const pricing = sku ? productPricing(sku) : null;
1761
+ const scope = { skuId: sku?.id ?? null, variantIds: selection };
1762
+ return {
1763
+ ...base,
1764
+ sku,
1765
+ price: pricing?.price ?? null,
1766
+ mrp: pricing?.compareAtPrice ?? null,
1767
+ discountPercent: pricing?.discountPercent ?? 0,
1768
+ complete,
1769
+ purchasable: sku !== null && sku.active && sku.stockQty > 0,
1770
+ choices: variantChoices(product, selection),
1771
+ specifications: specificationsFor(product.specifications, scope),
1772
+ images: imagesFor(product.images, scope),
1773
+ selectedVariantsParam: formatSelectedVariants(selection)
1774
+ };
1775
+ }
1776
+ function selectionFor(product, current) {
1777
+ return defaultSelection(product, current);
1778
+ }
1779
+ function createProductDetailStore(ctx, input) {
1780
+ const { productId, initial } = input;
1781
+ const preferred = [...input.selectedVariantIds ?? []];
1782
+ const store = createStore(
1783
+ derive3({
1784
+ productId,
1785
+ status: initial ? "ready" : "idle",
1786
+ refreshing: false,
1787
+ product: initial ?? null,
1788
+ error: null,
1789
+ selection: initial ? defaultSelection(initial, preferred) : []
1790
+ }),
1791
+ { onListenerError: ctx.onListenerError }
1792
+ );
1793
+ const latest = createLatest();
1794
+ let disposed = false;
1795
+ let started = false;
1796
+ const base = () => {
1797
+ const { productId: productId2, status, refreshing, product, error, selection } = store.getSnapshot();
1798
+ return { productId: productId2, status, refreshing, product, error, selection };
1799
+ };
1800
+ const set = (changes) => {
1801
+ if (!disposed) store.set(derive3({ ...base(), ...changes }));
1802
+ };
1803
+ async function reload() {
1804
+ started = true;
1805
+ if (disposed) return fail(storeError("superseded", "productDetail: the store was disposed."));
1806
+ const shown = store.getSnapshot().product !== null;
1807
+ set(shown ? { refreshing: true } : { status: "loading", error: null });
1808
+ try {
1809
+ const result = await latest(() => ctx.client.products.detail(productId));
1810
+ if (!result.current || disposed) {
1811
+ return fail(storeError("superseded", "productDetail: a newer load replaced this one."));
1812
+ }
1813
+ const product = result.value;
1814
+ const current = store.getSnapshot();
1815
+ set({
1816
+ status: "ready",
1817
+ refreshing: false,
1818
+ error: null,
1819
+ product,
1820
+ selection: selectionFor(product, current.product ? current.selection : preferred)
1821
+ });
1822
+ return ok(void 0);
1823
+ } catch (caught) {
1824
+ const error = toStoreError(caught);
1825
+ if (disposed) return fail(error);
1826
+ set(shown ? { refreshing: false, error } : { status: "error", refreshing: false, error });
1827
+ ctx.events.emit({ type: "error", store: "productDetail", action: "reload", error });
1828
+ return fail(error);
1829
+ }
1830
+ }
1831
+ function select(groupId, optionId) {
1832
+ const { product, selection } = store.getSnapshot();
1833
+ const group = product?.variantGroups.find((g) => g.id === groupId);
1834
+ if (!group?.options.some((option) => option.id === optionId)) {
1835
+ return fail(
1836
+ storeError("validation", `productDetail: group ${groupId} has no option ${optionId}.`)
1837
+ );
1838
+ }
1839
+ set({ selection: selectOption(product.variantGroups, selection, optionId) });
1840
+ return ok(void 0);
1841
+ }
1842
+ function dispose() {
1843
+ if (disposed) return;
1844
+ disposed = true;
1845
+ unregister();
1846
+ }
1847
+ const unregister = ctx.onDispose(dispose);
1848
+ return {
1849
+ getSnapshot: store.getSnapshot,
1850
+ getServerSnapshot: store.getServerSnapshot,
1851
+ subscribe(listener) {
1852
+ if (disposed) return () => {
1853
+ };
1854
+ const unsubscribe = store.subscribe(() => {
1855
+ if (!disposed) listener();
1856
+ });
1857
+ if (!started) void reload();
1858
+ return unsubscribe;
1859
+ },
1860
+ select,
1861
+ reload,
1862
+ dispose
1863
+ };
1864
+ }
1865
+
1866
+ // src/stores/referral.ts
1867
+ var MAX_LENGTH = 64;
1868
+ function isReferralRecord(raw) {
1869
+ if (!raw || typeof raw !== "object") return false;
1870
+ const r = raw;
1871
+ return typeof r.code === "string" && r.code.trim() !== "" && r.code.length <= MAX_LENGTH && typeof r.capturedAt === "number" && Number.isFinite(r.capturedAt);
1872
+ }
1873
+ function createReferralStore(ctx, options = {}) {
1874
+ const ttlMs = options.ttlMs ?? ctx.config.referralTtlMs;
1875
+ const record = ctx.persisted("referral", 1, isReferralRecord);
1876
+ const store = createStore(
1877
+ { status: "idle", code: null, capturedAt: null },
1878
+ { onListenerError: ctx.onListenerError }
1879
+ );
1880
+ let changes = 0;
1881
+ let reading = Promise.resolve();
1882
+ const expired = (capturedAt) => Date.now() - capturedAt >= ttlMs;
1883
+ function forget() {
1884
+ changes += 1;
1885
+ record.remove();
1886
+ store.update((s) => ({ ...s, code: null, capturedAt: null }));
1887
+ }
1888
+ function restore() {
1889
+ const startedAt = changes;
1890
+ store.update((s) => s.status === "ready" ? s : { ...s, status: "loading" });
1891
+ reading = record.read().then((read) => {
1892
+ if (ctx.isDisposed()) return;
1893
+ if (changes !== startedAt) {
1894
+ store.update((s) => ({ ...s, status: "ready" }));
1895
+ return;
1896
+ }
1897
+ let stored = read;
1898
+ if (stored && expired(stored.capturedAt)) {
1899
+ record.remove();
1900
+ stored = null;
1901
+ }
1902
+ store.set({
1903
+ status: "ready",
1904
+ code: stored?.code ?? null,
1905
+ capturedAt: stored?.capturedAt ?? null
1906
+ });
1907
+ });
1908
+ }
1909
+ async function settled() {
1910
+ await ctx.ready();
1911
+ await reading;
1912
+ }
1913
+ ctx.onHydrate(restore);
1914
+ ctx.events.on("auth:signed-in", (event) => {
1915
+ if (event.via === "register" || event.via === "otp") forget();
1916
+ });
1917
+ return {
1918
+ ...readOnly(store),
1919
+ async capture(code) {
1920
+ const trimmed = typeof code === "string" ? code.trim() : "";
1921
+ if (!trimmed || trimmed.length > MAX_LENGTH) {
1922
+ const error = storeError(
1923
+ "validation",
1924
+ trimmed ? `A referral code has at most ${MAX_LENGTH} characters.` : "A referral code is required."
1925
+ );
1926
+ ctx.events.emit({ type: "error", store: "referral", action: "capture", error });
1927
+ return fail(error);
1928
+ }
1929
+ changes += 1;
1930
+ const capturedAt = Date.now();
1931
+ store.update((s) => ({ ...s, code: trimmed, capturedAt }));
1932
+ record.write({ code: trimmed, capturedAt });
1933
+ await settled();
1934
+ return ok(void 0);
1935
+ },
1936
+ current() {
1937
+ const { code, capturedAt } = store.getSnapshot();
1938
+ if (code === null || capturedAt === null) return null;
1939
+ if (!expired(capturedAt)) return code;
1940
+ forget();
1941
+ return null;
1942
+ },
1943
+ async clear() {
1944
+ forget();
1945
+ await settled();
1946
+ return ok(void 0);
1947
+ }
1948
+ };
1949
+ }
1950
+
1951
+ // src/stores/runtime/events.ts
1952
+ function createEventBus(onListenerError) {
1953
+ const listeners = /* @__PURE__ */ new Map();
1954
+ return {
1955
+ on(type, listener) {
1956
+ const entry = { listener };
1957
+ listeners.set(type, [...listeners.get(type) ?? [], entry]);
1958
+ return () => {
1959
+ const current = listeners.get(type);
1960
+ if (current)
1961
+ listeners.set(
1962
+ type,
1963
+ current.filter((e) => e !== entry)
1964
+ );
1965
+ };
1966
+ },
1967
+ emit(event) {
1968
+ for (const entry of listeners.get(event.type)?.slice() ?? []) {
1969
+ try {
1970
+ entry.listener(event);
1971
+ } catch (error) {
1972
+ onListenerError(error);
1973
+ }
1974
+ }
1975
+ }
1976
+ };
1977
+ }
1978
+
1979
+ // src/stores/runtime/storage.ts
1980
+ function createMemoryStorage() {
1981
+ const data = /* @__PURE__ */ new Map();
1982
+ return {
1983
+ getItem: (key) => data.get(key) ?? null,
1984
+ setItem: (key, value) => {
1985
+ data.set(key, value);
1986
+ },
1987
+ removeItem: (key) => {
1988
+ data.delete(key);
1989
+ }
1990
+ };
1991
+ }
1992
+ function resolveStorage(options) {
1993
+ const storage = options.storage ?? createMemoryStorage();
1994
+ return {
1995
+ storage,
1996
+ secure: options.secureStorage ?? storage,
1997
+ ephemeral: options.ephemeralStorage ?? createMemoryStorage()
1998
+ };
1999
+ }
2000
+ var ID_PART = /^[A-Za-z0-9_-]+$/;
2001
+ var NAME_PART = /^[A-Za-z0-9._-]+$/;
2002
+ function storageKey(businessId, name) {
2003
+ const id = String(businessId);
2004
+ if (!ID_PART.test(id)) {
2005
+ throw new TypeError(`businessId must contain only letters, digits, "_" and "-": "${id}".`);
2006
+ }
2007
+ if (!NAME_PART.test(name)) {
2008
+ throw new TypeError(`storage name must contain only letters, digits, ".", "_" and "-".`);
2009
+ }
2010
+ return `gs.${id}.${name}`;
2011
+ }
2012
+ function createPersisted(adapter, key, version, validate, onError) {
2013
+ let tail = Promise.resolve();
2014
+ function enqueue(operation, run, fallback) {
2015
+ const next = tail.then(run).catch((error) => {
2016
+ onError(error, operation);
2017
+ return fallback;
2018
+ });
2019
+ tail = next;
2020
+ return next;
2021
+ }
2022
+ async function removeQuietly() {
2023
+ try {
2024
+ await adapter.removeItem(key);
2025
+ } catch (error) {
2026
+ onError(error, "remove");
2027
+ }
2028
+ }
2029
+ async function readNow() {
2030
+ const raw = await adapter.getItem(key);
2031
+ if (raw == null) return null;
2032
+ let parsed;
2033
+ try {
2034
+ parsed = JSON.parse(raw);
2035
+ } catch {
2036
+ await removeQuietly();
2037
+ return null;
2038
+ }
2039
+ const record = parsed;
2040
+ if (!record || typeof record !== "object" || record.v !== version || !validate(record.data)) {
2041
+ await removeQuietly();
2042
+ return null;
2043
+ }
2044
+ return record.data;
2045
+ }
2046
+ return {
2047
+ read: () => enqueue("read", readNow, null),
2048
+ write(value) {
2049
+ void enqueue(
2050
+ "write",
2051
+ async () => adapter.setItem(key, JSON.stringify({ v: version, data: value })),
2052
+ void 0
2053
+ );
2054
+ },
2055
+ remove() {
2056
+ void enqueue("remove", async () => adapter.removeItem(key), void 0);
2057
+ }
2058
+ };
2059
+ }
2060
+
2061
+ // src/stores/runtime/context.ts
2062
+ var HOUR = 60 * 60 * 1e3;
2063
+ var DEFAULT_CUSTOMER_TOKEN_TTL_MS = 24 * HOUR;
2064
+ var REFERRAL_TTL_MS = 7 * 24 * HOUR;
2065
+ function isObject(raw) {
2066
+ return !!raw && typeof raw === "object" && !Array.isArray(raw);
2067
+ }
2068
+ function isSessionRecord(raw) {
2069
+ return isObject(raw) && typeof raw.id === "number" && typeof raw.token === "string" && !!raw.token;
2070
+ }
2071
+ function isAuthRecord(raw) {
2072
+ return isObject(raw) && typeof raw.token === "string" && !!raw.token && isObject(raw.customer) && typeof raw.customer.id === "number" && (raw.rewardPoints === null || typeof raw.rewardPoints === "number") && typeof raw.savedAt === "number";
2073
+ }
2074
+ function validateOptions(options) {
2075
+ if (typeof options?.baseURL !== "string" || !options.baseURL) {
2076
+ throw new TypeError("createGrowupsellStores: baseURL is required.");
2077
+ }
2078
+ if (options.businessId == null || options.businessId === "") {
2079
+ throw new TypeError(
2080
+ "createGrowupsellStores: businessId is required (resolve the business first)."
2081
+ );
2082
+ }
2083
+ storageKey(options.businessId, "check");
2084
+ if (options.platform !== "website" && options.platform !== "app") {
2085
+ throw new TypeError("createGrowupsellStores: platform must be 'website' or 'app'.");
2086
+ }
2087
+ }
2088
+ function dropSession(ctx, reason) {
2089
+ if (!ctx.tokens.getSessionToken()) return false;
2090
+ ctx.tokens.setSessionToken(null);
2091
+ ctx.records.session.remove();
2092
+ const { kind, customerId } = ctx.identity.getSnapshot();
2093
+ ctx.setIdentity({ kind: kind === "customer" ? "customer" : "guest", customerId });
2094
+ ctx.events.emit({ type: "session:replaced", reason });
2095
+ return true;
2096
+ }
2097
+ function clearCustomerCredentials(ctx) {
2098
+ ctx.tokens.setCustomerToken(null);
2099
+ ctx.records.auth.remove();
2100
+ ctx.records.ephemeralAuth.remove();
2101
+ dropSession(ctx, "rotated");
2102
+ ctx.setIdentity({ kind: "guest", customerId: null });
2103
+ }
2104
+ function createRuntime(options) {
2105
+ validateOptions(options);
2106
+ const onListenerError = options.onError ?? rethrowAsync;
2107
+ const config = {
2108
+ customerTokenTtlMs: options.customerTokenTtlMs ?? DEFAULT_CUSTOMER_TOKEN_TTL_MS,
2109
+ referralTtlMs: options.referralTtlMs ?? REFERRAL_TTL_MS,
2110
+ autoLoad: options.autoLoad ?? true
2111
+ };
2112
+ const adapters = resolveStorage(options);
2113
+ const seeds = { ...options.seeds };
2114
+ let disposed = false;
2115
+ let customerToken = null;
2116
+ let sessionToken = null;
2117
+ const tokens = {
2118
+ getCustomerToken: () => customerToken,
2119
+ setCustomerToken: (token) => {
2120
+ customerToken = token;
2121
+ },
2122
+ getSessionToken: () => sessionToken,
2123
+ setSessionToken: (token) => {
2124
+ sessionToken = token;
2125
+ }
2126
+ };
2127
+ const bus = createEventBus(onListenerError);
2128
+ const events = {
2129
+ on: bus.on,
2130
+ emit: (event) => {
2131
+ if (!disposed) bus.emit(event);
2132
+ }
2133
+ };
2134
+ const identity = createStore(
2135
+ { kind: "unknown", customerId: null, epoch: 0 },
2136
+ { onListenerError }
2137
+ );
2138
+ const hydrated = createStore(false, { onListenerError });
2139
+ const persistedByAdapter = /* @__PURE__ */ new Map();
2140
+ function persisted(name, version, validate, where = "storage") {
2141
+ const adapter = adapters[where];
2142
+ const key = storageKey(options.businessId, name);
2143
+ let byKey = persistedByAdapter.get(adapter);
2144
+ if (!byKey) persistedByAdapter.set(adapter, byKey = /* @__PURE__ */ new Map());
2145
+ let record = byKey.get(key);
2146
+ if (!record) {
2147
+ record = createPersisted(adapter, key, version, validate, (error, operation) => {
2148
+ events.emit({
2149
+ type: "error",
2150
+ store: "storage",
2151
+ action: operation,
2152
+ error: toStoreError(error)
2153
+ });
2154
+ onListenerError(error);
2155
+ });
2156
+ byKey.set(key, record);
2157
+ }
2158
+ return record;
2159
+ }
2160
+ const records = {
2161
+ session: persisted("session", 1, isSessionRecord, "secure"),
2162
+ auth: persisted("auth", 1, isAuthRecord, "secure"),
2163
+ ephemeralAuth: persisted("auth", 1, isAuthRecord, "ephemeral")
2164
+ };
2165
+ let hydration = null;
2166
+ let rehydration = null;
2167
+ const hydrateListeners = [];
2168
+ const loaders = [];
2169
+ function safely(run) {
2170
+ try {
2171
+ run();
2172
+ } catch (error) {
2173
+ onListenerError(error);
2174
+ }
2175
+ }
2176
+ async function readRecords(phase) {
2177
+ const [session, secureAuth, ephemeralAuth] = await Promise.all([
2178
+ records.session.read(),
2179
+ records.auth.read(),
2180
+ records.ephemeralAuth.read()
2181
+ ]);
2182
+ let auth = secureAuth ?? ephemeralAuth;
2183
+ const remembered = !!secureAuth;
2184
+ if (auth && Date.now() - auth.savedAt > config.customerTokenTtlMs) {
2185
+ (remembered ? records.auth : records.ephemeralAuth).remove();
2186
+ auth = null;
2187
+ }
2188
+ return { phase, session, auth, remembered: !!auth && remembered };
2189
+ }
2190
+ function apply(read, epoch) {
2191
+ tokens.setSessionToken(read.session?.token ?? null);
2192
+ tokens.setCustomerToken(read.auth?.token ?? null);
2193
+ for (const listener of hydrateListeners.slice()) safely(() => listener(read));
2194
+ identity.set({
2195
+ kind: read.auth ? "customer" : "guest",
2196
+ customerId: read.auth?.customer.id ?? null,
2197
+ epoch
2198
+ });
2199
+ hydrated.set(true);
2200
+ }
2201
+ function runLoaders() {
2202
+ if (!config.autoLoad) return;
2203
+ for (const load of loaders.slice()) safely(load);
2204
+ }
2205
+ function hydrate() {
2206
+ hydration ??= readRecords("hydrate").then((read) => {
2207
+ if (disposed) return;
2208
+ apply(read, 1);
2209
+ runLoaders();
2210
+ }, onListenerError);
2211
+ return hydration;
2212
+ }
2213
+ function rehydrate() {
2214
+ if (!hydrated.getSnapshot()) return hydrate();
2215
+ rehydration ??= readRecords("rehydrate").then((read) => {
2216
+ if (disposed) return;
2217
+ const sameSession = (read.session?.token ?? null) === tokens.getSessionToken();
2218
+ const sameCustomer = (read.auth?.token ?? null) === tokens.getCustomerToken();
2219
+ if (sameSession && sameCustomer) return;
2220
+ const wasCustomer = tokens.getCustomerToken() !== null;
2221
+ apply(read, identity.getSnapshot().epoch + 1);
2222
+ if (wasCustomer && !read.auth) events.emit({ type: "auth:signed-out", reason: "logout" });
2223
+ runLoaders();
2224
+ }, onListenerError).finally(() => {
2225
+ rehydration = null;
2226
+ });
2227
+ return rehydration;
2228
+ }
2229
+ const flight = createSingleFlight();
2230
+ const disposedError2 = () => storeError("superseded", "The stores were disposed.");
2231
+ const reported = /* @__PURE__ */ new Set();
2232
+ function routeUnauthorized({ kind, token }) {
2233
+ if (disposed) return;
2234
+ if (kind === "customer") {
2235
+ if (token !== tokens.getCustomerToken()) return;
2236
+ safely(() => ctx.hooks.customerExpired());
2237
+ } else if (kind === "session") {
2238
+ if (token !== tokens.getSessionToken()) return;
2239
+ safely(() => ctx.hooks.sessionDead());
2240
+ }
2241
+ const key = `${kind}\0${token}`;
2242
+ if (reported.has(key)) return;
2243
+ reported.add(key);
2244
+ safely(() => options.onUnauthorized?.({ kind }));
2245
+ }
2246
+ const credentials = () => ({
2247
+ businessId: options.businessId,
2248
+ customerToken: tokens.getCustomerToken(),
2249
+ sessionToken: tokens.getSessionToken(),
2250
+ merchantToken: options.merchantToken?.() ?? null
2251
+ });
2252
+ const client = createClient({
2253
+ baseURL: options.baseURL,
2254
+ fetch: options.fetch,
2255
+ timeoutMs: options.timeoutMs,
2256
+ // Synchronous once hydrated; a request made earlier waits for (and starts) hydration.
2257
+ credentials: () => hydrated.getSnapshot() || disposed ? credentials() : hydrate().then(credentials),
2258
+ onUnauthorized: routeUnauthorized
2259
+ });
2260
+ let registry = null;
2261
+ const cleanups = /* @__PURE__ */ new Set();
2262
+ const ctx = {
2263
+ client,
2264
+ businessId: options.businessId,
2265
+ platform: options.platform,
2266
+ events,
2267
+ identity: readOnly(identity),
2268
+ seeds,
2269
+ ready: hydrate,
2270
+ async ensureSession() {
2271
+ await hydrate();
2272
+ if (disposed) throw disposedError2();
2273
+ const held = tokens.getSessionToken();
2274
+ if (held) return held;
2275
+ return flight("session", () => ctx.hooks.createSession());
2276
+ },
2277
+ hasSession: () => tokens.getSessionToken() !== null,
2278
+ persisted,
2279
+ get stores() {
2280
+ if (!registry) {
2281
+ throw new Error("ctx.stores is not available during construction: read it inside actions.");
2282
+ }
2283
+ return registry;
2284
+ },
2285
+ onListenerError,
2286
+ config,
2287
+ tokens,
2288
+ records,
2289
+ hooks: {
2290
+ async createSession() {
2291
+ const session = await client.session.create();
2292
+ if (disposed) throw disposedError2();
2293
+ tokens.setSessionToken(session.token);
2294
+ records.session.write({ id: session.id, token: session.token });
2295
+ return session.token;
2296
+ },
2297
+ customerExpired() {
2298
+ clearCustomerCredentials(ctx);
2299
+ events.emit({ type: "auth:signed-out", reason: "expired" });
2300
+ },
2301
+ sessionDead() {
2302
+ dropSession(ctx, "dead");
2303
+ }
2304
+ },
2305
+ setIdentity(next) {
2306
+ identity.set({ ...next, epoch: identity.getSnapshot().epoch + 1 });
2307
+ },
2308
+ inEpoch: (run) => runInEpoch(() => disposed ? -1 : identity.getSnapshot().epoch, run),
2309
+ onHydrate(listener) {
2310
+ hydrateListeners.push(listener);
2311
+ },
2312
+ autoLoad(load) {
2313
+ loaders.push(load);
2314
+ },
2315
+ onDispose(cleanup) {
2316
+ const entry = { run: cleanup };
2317
+ cleanups.add(entry);
2318
+ return () => {
2319
+ cleanups.delete(entry);
2320
+ };
2321
+ },
2322
+ isDisposed: () => disposed
2323
+ };
2324
+ return {
2325
+ ctx,
2326
+ hydrated: readOnly(hydrated),
2327
+ hydrate,
2328
+ rehydrate,
2329
+ dispose() {
2330
+ if (disposed) return;
2331
+ disposed = true;
2332
+ for (const entry of [...cleanups]) safely(entry.run);
2333
+ cleanups.clear();
2334
+ },
2335
+ attach(stores) {
2336
+ registry = stores;
2337
+ }
2338
+ };
2339
+ }
2340
+
2341
+ // src/stores/searchSuggestions.ts
2342
+ var runtime3 = globalThis;
2343
+ var DEFAULT_DEBOUNCE_MS = 300;
2344
+ function createSearchSuggestionsStore(ctx, options = {}) {
2345
+ const debounceMs = options.debounceMs ?? DEFAULT_DEBOUNCE_MS;
2346
+ const store = createStore(
2347
+ { query: "", status: "idle", items: [], error: null },
2348
+ { onListenerError: ctx.onListenerError }
2349
+ );
2350
+ let timer = null;
2351
+ let generation = 0;
2352
+ let disposed = false;
2353
+ const update = (changes) => {
2354
+ if (!disposed) store.set({ ...store.getSnapshot(), ...changes });
2355
+ };
2356
+ function cancelTimer() {
2357
+ if (timer !== null) runtime3.clearTimeout(timer);
2358
+ timer = null;
2359
+ }
2360
+ async function ask(word) {
2361
+ const mine = ++generation;
2362
+ update({ status: "loading", error: null });
2363
+ try {
2364
+ const items = await ctx.client.products.suggestions(word);
2365
+ if (mine === generation) update({ status: "ready", items, error: null });
2366
+ } catch (caught) {
2367
+ if (mine !== generation) return;
2368
+ const error = toStoreError(caught);
2369
+ update({ status: "error", items: [], error });
2370
+ if (!disposed) {
2371
+ ctx.events.emit({ type: "error", store: "searchSuggestions", action: "setQuery", error });
2372
+ }
2373
+ }
2374
+ }
2375
+ function setQuery(text) {
2376
+ if (disposed) return;
2377
+ cancelTimer();
2378
+ const word = text.trim();
2379
+ if (!word) {
2380
+ generation += 1;
2381
+ update({ query: text, status: "idle", items: [], error: null });
2382
+ return;
2383
+ }
2384
+ update({ query: text });
2385
+ timer = runtime3.setTimeout(() => {
2386
+ timer = null;
2387
+ void ask(word);
2388
+ }, debounceMs);
2389
+ }
2390
+ function clear() {
2391
+ if (disposed) return;
2392
+ cancelTimer();
2393
+ generation += 1;
2394
+ update({ query: "", status: "idle", items: [], error: null });
2395
+ }
2396
+ function dispose() {
2397
+ if (disposed) return;
2398
+ cancelTimer();
2399
+ generation += 1;
2400
+ disposed = true;
2401
+ unregister();
2402
+ }
2403
+ const unregister = ctx.onDispose(dispose);
2404
+ return {
2405
+ getSnapshot: store.getSnapshot,
2406
+ getServerSnapshot: store.getServerSnapshot,
2407
+ subscribe: store.subscribe,
2408
+ setQuery,
2409
+ clear,
2410
+ dispose
2411
+ };
2412
+ }
2413
+
2414
+ // src/stores/session.ts
2415
+ var LOOP_LIMIT = 3;
2416
+ var LOOP_WINDOW_MS = 6e4;
2417
+ function createSessionStore(ctx) {
2418
+ const store = createStore(
2419
+ { status: "unknown", creating: false, error: null },
2420
+ { onListenerError: ctx.onListenerError }
2421
+ );
2422
+ const patch = (next) => store.update((s) => ({ ...s, ...next }));
2423
+ const status = () => ctx.hasSession() ? "active" : "none";
2424
+ function failed(action, error) {
2425
+ patch({ status: status(), creating: false, error });
2426
+ ctx.events.emit({ type: "error", store: "session", action, error });
2427
+ }
2428
+ ctx.onHydrate(() => patch({ status: status() }));
2429
+ ctx.events.on("session:replaced", () => patch({ status: status() }));
2430
+ const create = ctx.hooks.createSession;
2431
+ ctx.hooks.createSession = async () => {
2432
+ patch({ creating: true, error: null });
2433
+ try {
2434
+ const token = await create();
2435
+ patch({ status: status(), creating: false });
2436
+ return token;
2437
+ } catch (error) {
2438
+ patch({ status: status(), creating: false, error: toStoreError(error) });
2439
+ throw error;
2440
+ }
2441
+ };
2442
+ let replacements = [];
2443
+ let stopped = false;
2444
+ ctx.hooks.sessionDead = () => {
2445
+ dropSession(ctx, "dead");
2446
+ const now = Date.now();
2447
+ replacements = replacements.filter((at) => now - at < LOOP_WINDOW_MS);
2448
+ if (stopped || replacements.length >= LOOP_LIMIT) {
2449
+ stopped = true;
2450
+ failed(
2451
+ "replace",
2452
+ storeError("session", "The guest session keeps being rejected; call ensure() to retry.")
2453
+ );
2454
+ return;
2455
+ }
2456
+ replacements.push(now);
2457
+ ctx.ensureSession().then(
2458
+ () => {
2459
+ },
2460
+ (error) => failed("replace", toStoreError(error))
2461
+ );
2462
+ };
2463
+ async function ensure() {
2464
+ stopped = false;
2465
+ replacements = [];
2466
+ try {
2467
+ await ctx.ensureSession();
2468
+ patch({ status: status(), error: null });
2469
+ return ok(void 0);
2470
+ } catch (error) {
2471
+ const storeErr = toStoreError(error);
2472
+ failed("ensure", storeErr);
2473
+ return fail(storeErr);
2474
+ }
2475
+ }
2476
+ return {
2477
+ getSnapshot: store.getSnapshot,
2478
+ getServerSnapshot: store.getServerSnapshot,
2479
+ subscribe: store.subscribe,
2480
+ ensure
2481
+ };
2482
+ }
2483
+
2484
+ // src/stores/themeConfig.ts
2485
+ var IDLE = Object.freeze({
2486
+ status: "idle",
2487
+ content: null,
2488
+ saving: false,
2489
+ error: null
2490
+ });
2491
+ var superseded2 = () => storeError("superseded", "A newer save of this section replaced it.");
2492
+ var disposedError = () => storeError("superseded", "The stores were disposed.");
2493
+ function createThemeConfigStore(ctx) {
2494
+ const confirmed = /* @__PURE__ */ new Map();
2495
+ const sections = {};
2496
+ for (const [slug, content] of Object.entries(ctx.seeds.themeContent ?? {})) {
2497
+ confirmed.set(slug, content);
2498
+ sections[slug] = { status: "ready", content, saving: false, error: null };
2499
+ }
2500
+ const store = createStore(
2501
+ {
2502
+ platform: ctx.platform,
2503
+ themeId: ctx.seeds.business?.themeSettings?.id ?? null,
2504
+ sections
2505
+ },
2506
+ { onListenerError: ctx.onListenerError }
2507
+ );
2508
+ const flight = createSingleFlight();
2509
+ const generation = /* @__PURE__ */ new Map();
2510
+ const slots = /* @__PURE__ */ new Map();
2511
+ const section = (slug) => store.getSnapshot().sections[slug] ?? IDLE;
2512
+ function patch(changes, themeId) {
2513
+ const s = store.getSnapshot();
2514
+ const next = { ...s.sections };
2515
+ for (const [slug, change] of Object.entries(changes)) {
2516
+ next[slug] = { ...next[slug] ?? IDLE, ...change };
2517
+ }
2518
+ store.set({ ...s, sections: next, themeId: themeId ?? s.themeId });
2519
+ }
2520
+ function failed(action, error) {
2521
+ ctx.events.emit({ type: "error", store: "themeConfig", action, error });
2522
+ return fail(error);
2523
+ }
2524
+ async function resolveThemeId() {
2525
+ const business = ctx.stores.business;
2526
+ let id = business.getSnapshot().themeId;
2527
+ if (id == null) {
2528
+ await business.load();
2529
+ id = business.getSnapshot().themeId;
2530
+ }
2531
+ if (id == null) return storeError("validation", "themeConfig: theme settings are not loaded");
2532
+ return id;
2533
+ }
2534
+ function checkSlug(slug) {
2535
+ const key = typeof slug === "string" ? slug.trim() : "";
2536
+ if (!key) return storeError("validation", "themeConfig: a section slug is required.");
2537
+ if (key.includes(",")) {
2538
+ return storeError("validation", `themeConfig: slug "${key}" contains a comma.`);
2539
+ }
2540
+ return key;
2541
+ }
2542
+ async function runLoad(slugs) {
2543
+ const mine = new Map(slugs.map((slug) => [slug, (generation.get(slug) ?? 0) + 1]));
2544
+ for (const [slug, gen] of mine) generation.set(slug, gen);
2545
+ const isMine = (slug) => generation.get(slug) === mine.get(slug);
2546
+ patch(
2547
+ Object.fromEntries(
2548
+ slugs.map((slug) => [
2549
+ slug,
2550
+ { status: section(slug).status === "ready" ? "ready" : "loading", error: null }
2551
+ ])
2552
+ )
2553
+ );
2554
+ const settle = (error) => {
2555
+ patch(
2556
+ Object.fromEntries(
2557
+ slugs.filter(isMine).map((slug) => [
2558
+ slug,
2559
+ { status: section(slug).status === "ready" ? "ready" : "error", error }
2560
+ ])
2561
+ )
2562
+ );
2563
+ return failed("load", error);
2564
+ };
2565
+ const themeId = await resolveThemeId();
2566
+ if (ctx.isDisposed()) return fail(disposedError());
2567
+ if (typeof themeId !== "number") return settle(themeId);
2568
+ let contents;
2569
+ try {
2570
+ contents = await ctx.client.themeContent.getMany(themeId, slugs);
2571
+ } catch (error) {
2572
+ if (ctx.isDisposed()) return fail(disposedError());
2573
+ return settle(toStoreError(error));
2574
+ }
2575
+ if (ctx.isDisposed()) return fail(disposedError());
2576
+ const changes = {};
2577
+ slugs.forEach((slug, i) => {
2578
+ if (!isMine(slug)) return;
2579
+ const content = contents[i] ?? null;
2580
+ confirmed.set(slug, content);
2581
+ changes[slug] = section(slug).saving ? { status: "ready", error: null } : { status: "ready", content, error: null };
2582
+ });
2583
+ patch(changes, themeId);
2584
+ return ok(void 0);
2585
+ }
2586
+ async function send(slug, slot, job) {
2587
+ slot.running = true;
2588
+ let outcome;
2589
+ const themeId = await resolveThemeId();
2590
+ if (typeof themeId !== "number") outcome = fail(themeId);
2591
+ else {
2592
+ try {
2593
+ const saved = await ctx.client.themeContent.update(themeId, slug, {
2594
+ platform: ctx.platform,
2595
+ data: job.data
2596
+ });
2597
+ outcome = ok(saved);
2598
+ } catch (error) {
2599
+ outcome = fail(toStoreError(error));
2600
+ }
2601
+ }
2602
+ if (ctx.isDisposed()) {
2603
+ slot.running = false;
2604
+ slot.waiting?.resolve(fail(disposedError()));
2605
+ slot.waiting = null;
2606
+ job.resolve(fail(disposedError()));
2607
+ return;
2608
+ }
2609
+ const next = slot.waiting;
2610
+ slot.waiting = null;
2611
+ if (outcome.ok) {
2612
+ confirmed.set(slug, outcome.value);
2613
+ generation.set(slug, (generation.get(slug) ?? 0) + 1);
2614
+ if (!next) {
2615
+ patch(
2616
+ { [slug]: { status: "ready", content: outcome.value, saving: false, error: null } },
2617
+ outcome.value.themeId
2618
+ );
2619
+ }
2620
+ job.resolve(outcome);
2621
+ } else {
2622
+ if (!next) {
2623
+ patch({
2624
+ [slug]: { content: confirmed.get(slug) ?? null, saving: false, error: outcome.error }
2625
+ });
2626
+ }
2627
+ job.resolve(failed("save", outcome.error));
2628
+ }
2629
+ if (next) void send(slug, slot, next);
2630
+ else slot.running = false;
2631
+ }
2632
+ return {
2633
+ ...readOnly(store),
2634
+ load(slugs, options = {}) {
2635
+ const keys = [];
2636
+ for (const slug of slugs) {
2637
+ const key = checkSlug(slug);
2638
+ if (typeof key !== "string") return Promise.resolve(fail(key));
2639
+ if (!keys.includes(key)) keys.push(key);
2640
+ }
2641
+ const wanted = options.force ? keys : keys.filter((key) => section(key).status !== "ready");
2642
+ if (wanted.length === 0) return Promise.resolve(ok(void 0));
2643
+ return flight(
2644
+ `${options.force ? "force" : "load"}:${wanted.join(",")}`,
2645
+ () => runLoad(wanted)
2646
+ );
2647
+ },
2648
+ save(slug, data) {
2649
+ const key = checkSlug(slug);
2650
+ if (typeof key !== "string") {
2651
+ return Promise.resolve(fail(key));
2652
+ }
2653
+ const s = store.getSnapshot();
2654
+ const base = section(key).content;
2655
+ const optimistic = base ? { ...base, platform: ctx.platform, data } : { id: 0, themeId: s.themeId ?? 0, slug: key, platform: ctx.platform, data };
2656
+ patch({ [key]: { content: optimistic, saving: true, error: null } });
2657
+ return new Promise((resolve) => {
2658
+ const job = { data, resolve };
2659
+ let slot = slots.get(key);
2660
+ if (!slot) slots.set(key, slot = { running: false, waiting: null });
2661
+ if (slot.running) {
2662
+ slot.waiting?.resolve(fail(superseded2()));
2663
+ slot.waiting = job;
2664
+ return;
2665
+ }
2666
+ void send(key, slot, job);
2667
+ });
2668
+ },
2669
+ section: (slug) => section(slug)
2670
+ };
2671
+ }
2672
+
2673
+ // src/stores/wishlist.ts
2674
+ var INITIAL3 = {
2675
+ status: "idle",
2676
+ refreshing: false,
2677
+ items: [],
2678
+ pending: { adding: [], removing: [], moving: [] },
2679
+ error: null
2680
+ };
2681
+ var isId = (value) => Number.isInteger(value) && value > 0;
2682
+ var without = (list, value) => list.filter((v) => v !== value);
2683
+ var superseded3 = () => fail(storeError("superseded", "The shopper changed while the wishlist was being updated."));
2684
+ function createWishlistStore(ctx) {
2685
+ const store = createStore(INITIAL3, { onListenerError: ctx.onListenerError });
2686
+ const latest = createLatest();
2687
+ const queue = createSerialQueue();
2688
+ const adds = /* @__PURE__ */ new Map();
2689
+ const removes = /* @__PURE__ */ new Map();
2690
+ const moves = /* @__PURE__ */ new Map();
2691
+ const customerId = () => {
2692
+ const identity = ctx.identity.getSnapshot();
2693
+ return identity.kind === "customer" ? identity.customerId : null;
2694
+ };
2695
+ const epoch = () => ctx.isDisposed() ? -1 : ctx.identity.getSnapshot().epoch;
2696
+ const snapshot = () => store.getSnapshot();
2697
+ const has = (skuId) => isInWishlist(snapshot(), skuId);
2698
+ function report(action, error, started = null) {
2699
+ if (error.kind === "superseded" || started !== null && epoch() !== started) return;
2700
+ store.update((s) => ({ ...s, error }));
2701
+ ctx.events.emit({ type: "error", store: "wishlist", action, error });
2702
+ }
2703
+ function refuse(action, error) {
2704
+ report(action, error);
2705
+ return fail(error);
2706
+ }
2707
+ async function guard(action) {
2708
+ await ctx.ready();
2709
+ if (customerId() !== null) return null;
2710
+ return refuse(action, storeError("signed-out", "Sign in to use the wishlist."));
2711
+ }
2712
+ function send(started, run) {
2713
+ return queue(
2714
+ async () => epoch() === started ? ctx.inEpoch(run) : { current: false }
2715
+ );
2716
+ }
2717
+ function once(flights, key, run) {
2718
+ const existing = flights.get(key);
2719
+ if (existing) return existing;
2720
+ const promise = run().finally(() => flights.delete(key));
2721
+ flights.set(key, promise);
2722
+ return promise;
2723
+ }
2724
+ let owner = null;
2725
+ ctx.onDispose(
2726
+ ctx.identity.subscribe(() => {
2727
+ const id = customerId();
2728
+ if (id === owner) return;
2729
+ owner = id;
2730
+ store.set(INITIAL3);
2731
+ })
2732
+ );
2733
+ ctx.onDispose(ctx.events.on("auth:signed-in", () => void refresh()));
2734
+ ctx.autoLoad(() => {
2735
+ if (customerId() !== null) void refresh();
2736
+ });
2737
+ async function refresh() {
2738
+ const refused = await guard("refresh");
2739
+ if (refused) return refused;
2740
+ const loadedFor = customerId();
2741
+ store.update((s) => ({
2742
+ ...s,
2743
+ status: s.status === "ready" ? "ready" : "loading",
2744
+ refreshing: s.status === "ready",
2745
+ error: null
2746
+ }));
2747
+ let outcome;
2748
+ try {
2749
+ outcome = await latest(() => ctx.inEpoch(() => ctx.client.wishlist.list()));
2750
+ } catch (caught) {
2751
+ const error = toStoreError(caught);
2752
+ store.update((s) => ({
2753
+ ...s,
2754
+ status: s.status === "ready" ? "ready" : "error",
2755
+ refreshing: false
2756
+ }));
2757
+ report("refresh", error);
2758
+ return fail(error);
2759
+ }
2760
+ if (!outcome.current) return superseded3();
2761
+ if (!outcome.value.current) {
2762
+ if (!ctx.isDisposed() && customerId() === loadedFor) return refresh();
2763
+ return superseded3();
2764
+ }
2765
+ const items = outcome.value.value;
2766
+ store.update((s) => ({
2767
+ ...s,
2768
+ status: "ready",
2769
+ refreshing: false,
2770
+ // An entry being removed stays hidden, although the server listed it a moment ago.
2771
+ items: items.filter((item) => !s.pending.removing.includes(item.id))
2772
+ }));
2773
+ return ok(items);
2774
+ }
2775
+ function addNow(skuId) {
2776
+ return once(adds, skuId, async () => {
2777
+ if (has(skuId)) {
2778
+ if (snapshot().error) store.update((s) => ({ ...s, error: null }));
2779
+ return ok(void 0);
2780
+ }
2781
+ const started = epoch();
2782
+ store.update((s) => ({
2783
+ ...s,
2784
+ error: null,
2785
+ pending: { ...s.pending, adding: [...s.pending.adding, skuId] }
2786
+ }));
2787
+ const done = () => {
2788
+ if (epoch() === started) {
2789
+ store.update((s) => ({
2790
+ ...s,
2791
+ pending: { ...s.pending, adding: without(s.pending.adding, skuId) }
2792
+ }));
2793
+ }
2794
+ };
2795
+ try {
2796
+ const sent = await send(started, () => ctx.client.wishlist.add(skuId));
2797
+ if (!sent.current) return superseded3();
2798
+ await refresh();
2799
+ done();
2800
+ return epoch() === started ? ok(void 0) : superseded3();
2801
+ } catch (caught) {
2802
+ const error = toStoreError(caught);
2803
+ if (error.kind === "http" && error.status === 400) {
2804
+ await refresh();
2805
+ if (epoch() === started && has(skuId)) {
2806
+ done();
2807
+ return ok(void 0);
2808
+ }
2809
+ }
2810
+ done();
2811
+ report("add", error, started);
2812
+ return epoch() === started ? fail(error) : superseded3();
2813
+ }
2814
+ });
2815
+ }
2816
+ function removeNow(wishlistId, action) {
2817
+ return once(removes, wishlistId, async () => {
2818
+ const state = snapshot();
2819
+ const index = state.items.findIndex((item2) => item2.id === wishlistId);
2820
+ if (index < 0) {
2821
+ return refuse(action, storeError("validation", "This entry is not in the wishlist."));
2822
+ }
2823
+ const item = state.items[index];
2824
+ const started = epoch();
2825
+ store.set({
2826
+ ...state,
2827
+ error: null,
2828
+ items: state.items.filter((i) => i !== item),
2829
+ pending: { ...state.pending, removing: [...state.pending.removing, wishlistId] }
2830
+ });
2831
+ const settle = (items) => store.update((s) => ({
2832
+ ...s,
2833
+ items: items(s),
2834
+ pending: { ...s.pending, removing: without(s.pending.removing, wishlistId) }
2835
+ }));
2836
+ try {
2837
+ const sent = await send(started, () => ctx.client.wishlist.remove(wishlistId));
2838
+ if (!sent.current) return superseded3();
2839
+ settle((s) => s.items);
2840
+ return ok(void 0);
2841
+ } catch (caught) {
2842
+ const error = toStoreError(caught);
2843
+ settle(
2844
+ (s) => s.items.some((i) => i.id === wishlistId) ? s.items : [...s.items.slice(0, index), item, ...s.items.slice(index)]
2845
+ );
2846
+ report(action, error);
2847
+ return fail(error);
2848
+ }
2849
+ });
2850
+ }
2851
+ async function add(skuId) {
2852
+ const refused = await guard("add");
2853
+ if (refused) return refused;
2854
+ if (!isId(skuId)) {
2855
+ return refuse("add", storeError("validation", "A SKU id is a positive integer."));
2856
+ }
2857
+ return addNow(skuId);
2858
+ }
2859
+ async function remove(wishlistId) {
2860
+ const refused = await guard("remove");
2861
+ return refused ?? removeNow(wishlistId, "remove");
2862
+ }
2863
+ async function toggle2(skuId) {
2864
+ const refused = await guard("toggle");
2865
+ if (refused) return refused;
2866
+ if (!isId(skuId)) {
2867
+ return refuse("toggle", storeError("validation", "A SKU id is a positive integer."));
2868
+ }
2869
+ const adding = adds.get(skuId);
2870
+ if (adding) {
2871
+ const added2 = await adding;
2872
+ if (!added2.ok) return added2;
2873
+ }
2874
+ const saved = snapshot().items.find((item) => item.sku?.id === skuId);
2875
+ if (saved) {
2876
+ const removed = await removeNow(saved.id, "toggle");
2877
+ return removed.ok ? ok({ saved: false }) : removed;
2878
+ }
2879
+ const added = await addNow(skuId);
2880
+ return added.ok ? ok({ saved: true }) : added;
2881
+ }
2882
+ async function moveToCart(wishlistId, quantity = 1) {
2883
+ const refused = await guard("moveToCart");
2884
+ if (refused) return refused;
2885
+ const moving = moves.get(wishlistId);
2886
+ if (moving) return moving;
2887
+ const item = snapshot().items.find((i) => i.id === wishlistId);
2888
+ if (!item) {
2889
+ return refuse("moveToCart", storeError("validation", "This entry is not in the wishlist."));
2890
+ }
2891
+ if (!item.available || !item.sku) {
2892
+ return refuse("moveToCart", storeError("validation", "This item is no longer available."));
2893
+ }
2894
+ if (!isId(quantity)) {
2895
+ return refuse("moveToCart", storeError("validation", "The quantity is a positive integer."));
2896
+ }
2897
+ const skuId = item.sku.id;
2898
+ return once(moves, wishlistId, async () => {
2899
+ const started = epoch();
2900
+ store.update((s) => ({
2901
+ ...s,
2902
+ error: null,
2903
+ pending: { ...s.pending, moving: [...s.pending.moving, wishlistId] }
2904
+ }));
2905
+ const done = () => {
2906
+ if (epoch() === started) {
2907
+ store.update((s) => ({
2908
+ ...s,
2909
+ pending: { ...s.pending, moving: without(s.pending.moving, wishlistId) }
2910
+ }));
2911
+ }
2912
+ };
2913
+ const added = await ctx.stores.cart.add({ skuId, quantity });
2914
+ if (epoch() !== started) return superseded3();
2915
+ if (!added.ok) {
2916
+ done();
2917
+ report("moveToCart", added.error, started);
2918
+ return fail(added.error);
2919
+ }
2920
+ const removed = await removeNow(wishlistId, "moveToCart");
2921
+ done();
2922
+ return removed;
2923
+ });
2924
+ }
2925
+ return {
2926
+ ...readOnly(store),
2927
+ refresh,
2928
+ add,
2929
+ remove,
2930
+ toggle: toggle2,
2931
+ moveToCart,
2932
+ has
2933
+ };
2934
+ }
2935
+
2936
+ // src/stores/create.ts
2937
+ function createGrowupsellStores(options) {
2938
+ const runtime4 = createRuntime(options);
2939
+ const { ctx } = runtime4;
2940
+ const registry = {
2941
+ session: createSessionStore(ctx),
2942
+ auth: createAuthStore(ctx),
2943
+ cart: createCartStore(ctx),
2944
+ wishlist: createWishlistStore(ctx),
2945
+ checkout: createCheckoutStore(ctx),
2946
+ business: createBusinessStore(ctx),
2947
+ catalog: createCatalogStore(ctx),
2948
+ themeConfig: createThemeConfigStore(ctx),
2949
+ referral: createReferralStore(ctx)
2950
+ };
2951
+ runtime4.attach(registry);
2952
+ const screens = {
2953
+ createListing: (input) => createListingStore(ctx, input),
2954
+ createProductDetail: (input) => createProductDetailStore(ctx, input),
2955
+ createSearchSuggestions: (options2) => createSearchSuggestionsStore(ctx, options2)
2956
+ };
2957
+ return {
2958
+ ...registry,
2959
+ ...screens,
2960
+ client: ctx.client,
2961
+ identity: ctx.identity,
2962
+ hydrated: runtime4.hydrated,
2963
+ events: ctx.events,
2964
+ hydrate: runtime4.hydrate,
2965
+ rehydrate: runtime4.rehydrate,
2966
+ dispose: runtime4.dispose
2967
+ };
2968
+ }
2969
+
2970
+ export {
2971
+ cartItemCount,
2972
+ cartLineForSku,
2973
+ isInWishlist,
2974
+ checkoutPreviewFor,
2975
+ paymentOptionsFor,
2976
+ createMemoryStorage,
2977
+ REFERRAL_TTL_MS,
2978
+ createGrowupsellStores
2979
+ };
2980
+ //# sourceMappingURL=chunk-DHBRITDW.js.map