@managesales/storefront 1.0.2 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -3,6 +3,67 @@
3
3
  All notable changes to `@managesales/storefront`. This project adheres to
4
4
  [Semantic Versioning](https://semver.org).
5
5
 
6
+ ## 1.2.0 — 2026-07-21
7
+
8
+ ### Added
9
+ - **`discounts` namespace** — `client.discounts.validate(code, { subtotal | items, customerId? })`
10
+ previews a discount code against a cart *before* creating a checkout session. Returns
11
+ `{ valid: true, discountId, code, label, amount, freeShipping }` or
12
+ `{ valid: false, reason }` with a customer-safe explanation — no try/catch needed for
13
+ "invalid code" UI states. Validation never consumes coupon usage.
14
+ - **`client.discounts.automatic()`** — lists the store's active automatic promotions
15
+ (`AutomaticPromotion[]`: title, type, value, minimums, end date) for display, e.g. promo
16
+ banners. Coupon codes are never exposed by this endpoint.
17
+ - **`checkout.applyGiftCard(checkoutId, code)`** — applies a gift card to an open checkout
18
+ session (the backend endpoint existed; the SDK now exposes it).
19
+ - **`couponCode` option at checkout creation** — `checkout.create(items, { couponCode })` and
20
+ `cart.checkout({ couponCode })` apply the code server-side in the same round-trip as session
21
+ creation.
22
+
23
+ ### Fixed
24
+ - **`checkout.applyCoupon` sent the wrong field name and always failed.** The request body was
25
+ `{ couponCode }`, but the API accepts `{ code }` and rejects unknown fields — every
26
+ `applyCoupon` call returned a 400. It now sends `{ code }`.
27
+ - **`CheckoutSession` type now matches the actual API response.** The session's real fields are
28
+ `lines`, `discountAmount`, `discountCode`, `discountLabel`, `taxAmount`, `shippingAmount`,
29
+ `giftCardAmount`, `availableShippingMethods`, etc. The previously-declared `items`, `tax`,
30
+ `discount`, and `couponCode` members were never populated at runtime; they remain as optional
31
+ `@deprecated` aliases so existing code still compiles. `shipping` was previously typed as a
32
+ number but is actually the shipping *address object* on the wire — it is now typed correctly
33
+ (`CheckoutShippingAddress`).
34
+
35
+ ## 1.1.0 — 2026-07-11
36
+
37
+ ### Added
38
+ - **Stateful cart** — `client.cart.create()` / `client.cart.current()` return a `StorefrontCart` with
39
+ `addLineItem` (auto-enriches price/name from `products.get`, merges duplicate product+variant
40
+ lines), `updateLineItem`, `removeLineItem`, `clear`, and `checkout()` to materialize the cart
41
+ straight into a checkout session. Persisted to `localStorage` by default (`persistCart: false` to
42
+ opt out); safe to import under SSR/Node with no DOM. `client.cart.recover(token)` is unchanged.
43
+ - **`orders` and `customers` alias namespaces** — `client.orders.list()` and
44
+ `client.customers.{login,register,loginWithGoogle,logout,isAuthenticated,setTokens,me,orders,addresses}`
45
+ are convenience references onto the existing `auth`/`customer` methods (no duplicated logic, no new
46
+ backend calls).
47
+ - **Configurable request retries** — network errors and 5xx/408/425/429 responses on idempotent
48
+ (`GET`/`HEAD`) requests are now retried automatically with exponential backoff, honoring a
49
+ `Retry-After` response header as a delay floor. Configure via
50
+ `createStorefrontClient({ retry: { retries, baseDelayMs, maxDelayMs, retryOnStatus } })`, or pass
51
+ `retry: false` to disable. Non-idempotent requests (`POST`/`PUT`/`PATCH`/`DELETE`) are never
52
+ retried once a response is received from the server; a network failure that never reached the
53
+ server (e.g. `fetch` throwing) is retried regardless of method, since no side effect could have
54
+ occurred yet.
55
+
56
+ ### Fixed
57
+ - `checkout.create` now sends line items under the `lines` key (matching the `/checkout` endpoint's
58
+ actual request shape) instead of `items`. Note this only corrects the request envelope — direct
59
+ callers must still supply every field the backend requires (see next item).
60
+ - `CheckoutLineItem` now includes the backend-required `productName` (string) and `unitPrice`
61
+ (number) fields, plus optional `imageUrl`. The previous `{ productId, variantId?, quantity }`
62
+ shape type-checked but was always rejected by the real `/checkout` endpoint's validation for
63
+ direct `client.checkout.create()` callers; `cart.checkout()` was unaffected since `CartLine`
64
+ already carried both fields. Callers who don't want to supply `productName`/`unitPrice` by hand
65
+ should use the stateful cart (`cart.addLineItem`), which auto-enriches them from `products.get`.
66
+
6
67
  ## 1.0.2 — 2026-07-11
7
68
 
8
69
  ### Docs
package/README.md CHANGED
@@ -30,8 +30,10 @@ like Shopify's Storefront client, for ManageSales. Everything you need to ship a
30
30
  - [Products & search](#products--search)
31
31
  - [Collections](#collections)
32
32
  - [Cart & checkout](#cart--checkout)
33
+ - [Stateful cart](#stateful-cart)
33
34
  - [Authentication](#authentication)
34
35
  - [Customer account](#customer-account)
36
+ - [Retries](#retries)
35
37
  - [Error handling](#error-handling)
36
38
  - [TypeScript support](#typescript-support)
37
39
  - [Framework examples](#framework-examples)
@@ -78,12 +80,19 @@ client.products.search(query, params?) // full-text search
78
80
  client.collections.list(params?)
79
81
  client.collections.get(idOrSlug) // collection + its products
80
82
 
83
+ // Discounts (public)
84
+ client.discounts.validate(code, { subtotal | items }) // preview a code before checkout
85
+ client.discounts.automatic() // active automatic promotions (for banners)
86
+
81
87
  // Cart & checkout (public)
82
- client.checkout.create(lineItems) // start a checkout from line items
88
+ client.checkout.create(lineItems, { couponCode? }) // start a checkout from line items
83
89
  client.checkout.get(checkoutId)
84
90
  client.checkout.applyCoupon(checkoutId, code)
91
+ client.checkout.applyGiftCard(checkoutId, code)
85
92
  client.checkout.complete(checkoutId, payment?)
93
+ client.cart.create() / client.cart.current() // stateful cart — see Stateful cart
86
94
  client.cart.recover(token) // rebuild a checkout from a recovery link
95
+ client.orders.list(params?) // alias for client.customer.orders(params?)
87
96
 
88
97
  // Auth & customer (customer session)
89
98
  client.auth.register({ email, password, name })
@@ -95,6 +104,8 @@ client.auth.setTokens(access, refresh) // restore a session (e.g. from cookies)
95
104
  client.customer.me()
96
105
  client.customer.orders(params?) // order history
97
106
  client.customer.addresses()
107
+ // client.customers.* — alias namespace bundling auth + customer under one name
108
+ // (client.customers.login === client.auth.login, client.customers.me === client.customer.me, ...)
98
109
 
99
110
  // Content (public)
100
111
  client.blog.list(params?) / client.blog.get(slug)
@@ -155,26 +166,56 @@ const summer = await client.collections.get("summer-sale");
155
166
  console.log(summer.name, summer.products.length); // collection + its products
156
167
  ```
157
168
 
169
+ ## Discounts
170
+
171
+ Preview a discount code against the customer's cart **before** creating a checkout session — ideal
172
+ for a "promo code" field with instant feedback. Validation never consumes the coupon's usage:
173
+
174
+ ```ts
175
+ const check = await client.discounts.validate("SAVE10", { subtotal: 68 });
176
+ // or derive the subtotal from line items:
177
+ // await client.discounts.validate("SAVE10", { items: [{ quantity: 2, unitPrice: 24.99 }] });
178
+
179
+ if (check.valid) {
180
+ console.log(`${check.label} — saves ${check.amount}`); // "Summer Sale (SAVE10) — saves 6.80"
181
+ } else {
182
+ console.log(check.reason); // customer-safe, e.g. "Minimum order amount of 50 required"
183
+ }
184
+ ```
185
+
186
+ Show the store's running automatic promotions (no code needed to redeem — great for banners):
187
+
188
+ ```ts
189
+ const promos = await client.discounts.automatic();
190
+ // [{ id, title: "Free shipping over $100", type: "free_shipping", value: 0, minOrderAmount: 100, ... }]
191
+ ```
192
+
158
193
  ## Cart & checkout
159
194
 
160
- Assemble line items in your UI, create a checkout session, optionally apply a coupon, then complete
161
- it:
195
+ Assemble line items in your UI, create a checkout session, optionally apply a coupon or gift card,
196
+ then complete it:
162
197
 
163
198
  ```ts
164
- // 1. Create a checkout from line items
165
- const session = await client.checkout.create([
166
- { productId: "prod_123", variantId: "var_456", quantity: 2 },
167
- { productId: "prod_789", quantity: 1 },
168
- ]);
199
+ // 1. Create a checkout from line items — optionally applying a coupon in the same call
200
+ const session = await client.checkout.create(
201
+ [
202
+ { productId: "prod_123", variantId: "var_456", productName: "Blue T-Shirt", quantity: 2, unitPrice: 24.99 },
203
+ { productId: "prod_789", productName: "Canvas Tote", quantity: 1, unitPrice: 18 },
204
+ ],
205
+ { couponCode: "SAVE10" },
206
+ );
169
207
 
170
208
  // 2. Re-fetch a session (e.g. after a reload)
171
209
  const current = await client.checkout.get(session.id);
172
210
 
173
- // 3. Apply a coupon
211
+ // 3. Apply (or change) a coupon on an open session
174
212
  const discounted = await client.checkout.applyCoupon(session.id, "SAVE10");
175
- console.log(discounted.discount, discounted.total);
213
+ console.log(discounted.discountAmount, discounted.total);
214
+
215
+ // 4. Apply a gift card
216
+ const withGiftCard = await client.checkout.applyGiftCard(session.id, "GC-XXXX-XXXX");
176
217
 
177
- // 4. Complete the purchase
218
+ // 5. Complete the purchase
178
219
  const order = await client.checkout.complete(session.id, {
179
220
  paymentMethod: "card",
180
221
  token: "tok_visa_xxx",
@@ -182,6 +223,12 @@ const order = await client.checkout.complete(session.id, {
182
223
  console.log("Order placed:", order.orderNumber);
183
224
  ```
184
225
 
226
+ > `productName` and `unitPrice` are required on every line item passed directly to
227
+ > `client.checkout.create()` — the backend validates them. If you'd rather not look these up
228
+ > yourself, use the [stateful cart](#stateful-cart) instead: `cart.addLineItem` auto-enriches
229
+ > `productName`/`unitPrice` (and `imageUrl`) via `products.get`, so cart callers only ever supply
230
+ > `productId`/`variantId`/`quantity`.
231
+
185
232
  **Abandoned-cart recovery** — turn a recovery link back into a checkout session:
186
233
 
187
234
  ```ts
@@ -189,6 +236,39 @@ const token = new URLSearchParams(location.search).get("token")!;
189
236
  const recovered = await client.cart.recover(token);
190
237
  ```
191
238
 
239
+ ## Stateful cart
240
+
241
+ For a persistent, imperative cart — add/update/remove lines, then check out in one call — use
242
+ `client.cart` instead of assembling line items by hand:
243
+
244
+ ```ts
245
+ // Create a fresh cart, or resume one already persisted for this workspace
246
+ const cart = client.cart.create(); // client.cart.current() resumes a persisted cart instead
247
+
248
+ // Add items — price, name, and image are looked up automatically via products.get()
249
+ await cart.addLineItem({ productId: "prod_123", variantId: "var_456", quantity: 2 });
250
+ await cart.addLineItem({ productId: "prod_123", variantId: "var_456", quantity: 1 }); // merges into the existing line
251
+
252
+ console.log(cart.itemCount); // 3
253
+ console.log(cart.subtotal); // sum of unitPrice * quantity across all lines
254
+
255
+ cart.updateLineItem(cart.lines[0].id, 5); // set a line's quantity (quantity <= 0 removes it)
256
+ cart.removeLineItem(cart.lines[0].id);
257
+ cart.clear();
258
+
259
+ // Turn the cart directly into a checkout session — optionally with a coupon
260
+ const session = await cart.checkout({ clearOnSuccess: true, couponCode: "SAVE10" });
261
+ console.log(session.id, session.discountAmount, session.total);
262
+ ```
263
+
264
+ By default the cart is persisted to `localStorage` under a per-workspace key, so `client.cart.current()`
265
+ resumes it after a page reload. Pass `persistCart: false` to `createStorefrontClient` to keep it
266
+ in-memory only (tests, SSR, multi-cart UIs). All storage access is guarded, so the SDK still imports
267
+ and runs fine under Node/SSR where `localStorage` doesn't exist — the cart simply won't persist.
268
+
269
+ > `cart.checkout()` forwards the cart's line items to `client.checkout.create()` under the hood, so
270
+ > coupons/payment on the returned session work exactly as in [Cart & checkout](#cart--checkout).
271
+
192
272
  ## Authentication
193
273
 
194
274
  Most pages need **no auth**. When a customer logs in, the SDK stores the access + refresh tokens and
@@ -235,6 +315,37 @@ const { data: orders } = await client.customer.orders({ page: 1 }); // order his
235
315
  const addresses = await client.customer.addresses(); // saved addresses
236
316
  ```
237
317
 
318
+ `client.customers` mirrors `auth` + `customer` under one namespace, and `client.orders.list()` is a
319
+ shorthand for `client.customer.orders()` — same functions, same requests, just organized differently:
320
+
321
+ ```ts
322
+ await client.customers.login("jane@example.com", "SecurePass123!"); // === client.auth.login
323
+ const { data: orders } = await client.orders.list({ page: 1 }); // === client.customer.orders({ page: 1 })
324
+ ```
325
+
326
+ ## Retries
327
+
328
+ Network failures and `408` / `425` / `429` / `5xx` responses to idempotent (`GET`/`HEAD`) requests are
329
+ retried automatically with exponential backoff, honoring a `Retry-After` response header as a delay
330
+ floor when the server sends one. Non-idempotent requests (`POST`/`PUT`/`PATCH`/`DELETE`) are retried
331
+ **only** when the request never reached the server (e.g. `fetch` itself throws) — never after a server
332
+ response, since the side effect may already have happened.
333
+
334
+ ```ts
335
+ const client = createStorefrontClient({
336
+ workspaceId: "ws_123",
337
+ retry: {
338
+ retries: 2, // default: 2 additional attempts after the first
339
+ baseDelayMs: 300, // default: 300 — base for exponential backoff
340
+ maxDelayMs: 4000, // default: 4000 — cap on any single backoff delay
341
+ retryOnStatus: [408, 425, 429, 500, 502, 503, 504], // default shown
342
+ },
343
+ });
344
+
345
+ // Disable retries entirely
346
+ const noRetryClient = createStorefrontClient({ workspaceId: "ws_123", retry: false });
347
+ ```
348
+
238
349
  ## Error handling
239
350
 
240
351
  Every non-2xx response throws a `StorefrontError` with structured fields:
@@ -257,8 +368,8 @@ try {
257
368
  ```
258
369
 
259
370
  > **Resilience:** the client automatically retries a request **once** with a refreshed token when a
260
- > `401` indicates an expired access token. It does not retry on network/5xx errors — wrap calls in
261
- > your own retry policy if you need that.
371
+ > `401` indicates an expired access token, and separately retries transient network/5xx failures per
372
+ > the [Retries](#retries) config.
262
373
 
263
374
  ## TypeScript support
264
375
 
package/dist/index.cjs CHANGED
@@ -15,6 +15,18 @@ function createStorefrontClient(config) {
15
15
  const fetchFn = config.customFetch ?? globalThis.fetch.bind(globalThis);
16
16
  let accessToken = null;
17
17
  let refreshToken = null;
18
+ const RETRY_DEFAULTS = {
19
+ retries: 2,
20
+ baseDelayMs: 300,
21
+ maxDelayMs: 4e3,
22
+ retryOnStatus: [408, 425, 429, 500, 502, 503, 504]
23
+ };
24
+ const rc = config.retry === false ? { ...RETRY_DEFAULTS, retries: 0 } : { ...RETRY_DEFAULTS, ...config.retry || {} };
25
+ const IDEMPOTENT = /* @__PURE__ */ new Set(["GET", "HEAD"]);
26
+ const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
27
+ function backoff(attempt) {
28
+ return Math.min(rc.maxDelayMs, Math.round(Math.random() * rc.baseDelayMs * Math.pow(2, attempt)));
29
+ }
18
30
  async function request(method, path, options) {
19
31
  const url = new URL(`${apiUrl}${path}`);
20
32
  if (options?.params) {
@@ -31,11 +43,32 @@ function createStorefrontClient(config) {
31
43
  if (options?.body) {
32
44
  headers["Content-Type"] = "application/json";
33
45
  }
34
- const res = await fetchFn(url.toString(), {
46
+ const fetchOptions = {
35
47
  method,
36
48
  headers,
37
49
  body: options?.body ? JSON.stringify(options.body) : void 0
38
- });
50
+ };
51
+ let attempt = 0;
52
+ let res;
53
+ while (true) {
54
+ try {
55
+ res = await fetchFn(url.toString(), fetchOptions);
56
+ } catch (err) {
57
+ if (attempt < rc.retries) {
58
+ await sleep(backoff(attempt));
59
+ attempt++;
60
+ continue;
61
+ }
62
+ throw err;
63
+ }
64
+ if (attempt < rc.retries && rc.retryOnStatus.includes(res.status) && IDEMPOTENT.has(method)) {
65
+ const ra = Number(res.headers.get("retry-after"));
66
+ await sleep(Number.isFinite(ra) && ra > 0 ? ra * 1e3 : backoff(attempt));
67
+ attempt++;
68
+ continue;
69
+ }
70
+ break;
71
+ }
39
72
  if (res.status === 401 && options?.auth && refreshToken) {
40
73
  const refreshed = await refreshSession();
41
74
  if (refreshed) {
@@ -119,10 +152,22 @@ function createStorefrontClient(config) {
119
152
  return request("GET", `/storefront-api/collections/${idOrSlug}`);
120
153
  }
121
154
  };
155
+ const discounts = {
156
+ /** Preview a discount code against a cart before checkout. Never consumes usage. */
157
+ async validate(code, input) {
158
+ return request("POST", "/storefront-api/discounts/validate", { body: { code, ...input || {} } });
159
+ },
160
+ /** Active automatic promotions for display (codes are never exposed) */
161
+ async automatic() {
162
+ return request("GET", "/storefront-api/discounts/automatic");
163
+ }
164
+ };
122
165
  const checkout = {
123
166
  /** Create a checkout session from cart items */
124
- async create(items) {
125
- return request("POST", "/checkout", { body: { items } });
167
+ async create(items, opts) {
168
+ const body = { lines: items };
169
+ if (opts && opts.couponCode) body.couponCode = opts.couponCode;
170
+ return request("POST", "/checkout", { body });
126
171
  },
127
172
  /** Retrieve an existing checkout session */
128
173
  async get(checkoutId) {
@@ -130,7 +175,11 @@ function createStorefrontClient(config) {
130
175
  },
131
176
  /** Apply a coupon or discount code */
132
177
  async applyCoupon(checkoutId, couponCode) {
133
- return request("POST", `/checkout/${checkoutId}/coupon`, { body: { couponCode } });
178
+ return request("POST", `/checkout/${checkoutId}/coupon`, { body: { code: couponCode } });
179
+ },
180
+ /** Apply a gift card to the checkout */
181
+ async applyGiftCard(checkoutId, code) {
182
+ return request("POST", `/checkout/${checkoutId}/gift-card`, { body: { code } });
134
183
  },
135
184
  /** Complete checkout — process payment and create order */
136
185
  async complete(checkoutId, payment) {
@@ -254,23 +303,167 @@ function createStorefrontClient(config) {
254
303
  return res.json();
255
304
  }
256
305
  };
306
+ const CART_KEY = "managesales_cart_" + config.workspaceId;
307
+ const hasLS = () => {
308
+ try {
309
+ return config.persistCart !== false && typeof localStorage !== "undefined";
310
+ } catch {
311
+ return false;
312
+ }
313
+ };
314
+ const loadCart = () => {
315
+ if (!hasLS()) return null;
316
+ try {
317
+ const s = localStorage.getItem(CART_KEY);
318
+ return s ? JSON.parse(s) : null;
319
+ } catch {
320
+ return null;
321
+ }
322
+ };
323
+ const saveCart = (st) => {
324
+ if (!hasLS()) return;
325
+ try {
326
+ localStorage.setItem(CART_KEY, JSON.stringify(st));
327
+ } catch {
328
+ }
329
+ };
330
+ let __lineSeq = 0;
331
+ const genId = (p) => typeof crypto !== "undefined" && crypto.randomUUID ? p + crypto.randomUUID() : p + Date.now() + "_" + ++__lineSeq;
332
+ const nowIso = () => (/* @__PURE__ */ new Date()).toISOString();
333
+ const freshCart = () => ({ id: genId("cart_"), lines: [], createdAt: nowIso(), updatedAt: nowIso() });
334
+ function makeCart(state) {
335
+ const persist = () => {
336
+ state.updatedAt = nowIso();
337
+ saveCart(state);
338
+ };
339
+ const cartObj = {
340
+ get id() {
341
+ return state.id;
342
+ },
343
+ get lines() {
344
+ return state.lines;
345
+ },
346
+ get itemCount() {
347
+ return state.lines.reduce((n, l) => n + l.quantity, 0);
348
+ },
349
+ get subtotal() {
350
+ return state.lines.reduce((s, l) => s + l.unitPrice * l.quantity, 0);
351
+ },
352
+ // Supplying unitPrice alone does not skip the lookup — productName must also be
353
+ // provided, otherwise this falls through to the products.get() network lookup below.
354
+ async addLineItem(input) {
355
+ let line;
356
+ if (input.unitPrice != null && input.productName) {
357
+ line = {
358
+ id: genId("line_"),
359
+ productId: input.productId,
360
+ variantId: input.variantId,
361
+ quantity: input.quantity,
362
+ unitPrice: input.unitPrice,
363
+ productName: input.productName,
364
+ imageUrl: input.imageUrl
365
+ };
366
+ } else {
367
+ const p = await products.get(input.productId);
368
+ const v = input.variantId && Array.isArray(p.variants) ? p.variants.find((x) => x.id === input.variantId) : void 0;
369
+ line = {
370
+ id: genId("line_"),
371
+ productId: input.productId,
372
+ variantId: input.variantId,
373
+ quantity: input.quantity,
374
+ unitPrice: v && v.price != null ? v.price : p.price,
375
+ productName: p.name,
376
+ imageUrl: Array.isArray(p.images) && p.images.length > 0 ? p.images[0].url : void 0
377
+ };
378
+ }
379
+ const existing = state.lines.find((l) => l.productId === line.productId && l.variantId === line.variantId);
380
+ if (existing) existing.quantity += line.quantity;
381
+ else state.lines.push(line);
382
+ persist();
383
+ return cartObj;
384
+ },
385
+ updateLineItem(lineId, quantity) {
386
+ const l = state.lines.find((x) => x.id === lineId);
387
+ if (l) {
388
+ if (quantity <= 0) state.lines = state.lines.filter((x) => x.id !== lineId);
389
+ else l.quantity = quantity;
390
+ persist();
391
+ }
392
+ return cartObj;
393
+ },
394
+ removeLineItem(lineId) {
395
+ state.lines = state.lines.filter((x) => x.id !== lineId);
396
+ persist();
397
+ return cartObj;
398
+ },
399
+ clear() {
400
+ state.lines = [];
401
+ persist();
402
+ return cartObj;
403
+ },
404
+ async checkout(opts) {
405
+ const session = await checkout.create(
406
+ state.lines.map((l) => ({
407
+ productId: l.productId,
408
+ variantId: l.variantId,
409
+ productName: l.productName,
410
+ quantity: l.quantity,
411
+ unitPrice: l.unitPrice,
412
+ imageUrl: l.imageUrl
413
+ })),
414
+ opts && opts.couponCode ? { couponCode: opts.couponCode } : void 0
415
+ );
416
+ if (opts && opts.clearOnSuccess) cartObj.clear();
417
+ return session;
418
+ }
419
+ };
420
+ return cartObj;
421
+ }
257
422
  const cart = {
423
+ /** Create a new, empty cart (replaces any persisted cart) */
424
+ create() {
425
+ const st = freshCart();
426
+ saveCart(st);
427
+ return makeCart(st);
428
+ },
429
+ /** Get the current cart — loaded from storage if persisted, otherwise a fresh one */
430
+ current() {
431
+ return makeCart(loadCart() || freshCart());
432
+ },
258
433
  /** Recover an abandoned cart by recovery token */
259
434
  async recover(token) {
260
435
  return request("GET", `/abandoned-carts/recover/${token}`);
261
436
  }
262
437
  };
438
+ const orders = {
439
+ /** Get customer's order history (delegates to `customer.orders`) */
440
+ list: (params) => customer.orders(params)
441
+ };
442
+ const customers = {
443
+ login: auth.login,
444
+ register: auth.register,
445
+ loginWithGoogle: auth.loginWithGoogle,
446
+ logout: auth.logout,
447
+ isAuthenticated: auth.isAuthenticated,
448
+ setTokens: auth.setTokens,
449
+ me: customer.me,
450
+ orders: customer.orders,
451
+ addresses: customer.addresses
452
+ };
263
453
  return {
264
454
  store,
265
455
  products,
266
456
  collections,
457
+ discounts,
267
458
  checkout,
268
459
  auth,
269
460
  customer,
270
461
  blog,
271
462
  bookings,
272
463
  forms,
273
- cart
464
+ cart,
465
+ orders,
466
+ customers
274
467
  };
275
468
  }
276
469