@develemit/storefront 0.3.0 → 0.6.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/README.md CHANGED
@@ -131,6 +131,12 @@ sold or held by another buyer; its `productIds` lists them so the cart can drop
131
131
  them. Send only ids and quantities: the server prices every line itself.
132
132
  Redirect the shopper to the returned `url`.
133
133
 
134
+ Discount codes: `client.previewDiscount({ code, items })` returns
135
+ `{ code, discountCents, subtotalCents, currency }` or throws
136
+ `DiscountInvalidError` with a `reason` (`not_found`, `not_started`, `expired`,
137
+ `min_subtotal`, `exhausted`, `currency`). `createCheckout(items, { discountCode })`
138
+ sends the code and throws the same error if it was rejected at checkout.
139
+
134
140
  ## Images in production
135
141
 
136
142
  The default image adapter is a plain `<img loading="lazy" decoding="async">`
@@ -235,6 +241,58 @@ touches `window` during render.
235
241
  one-of-a-kind items can only be added once. Every label is overridable
236
242
  through a `labels` prop.
237
243
 
244
+ ### Checkout
245
+
246
+ `useCheckout({ startCheckout, onRedirect?, onDiscountInvalid?, labels? })`
247
+ wraps the host's checkout call (a server action or a fetch) and returns
248
+ `{ checkout, busy, message, clearMessage }`. `startCheckout(items, { discountCode })`
249
+ resolves to a `CheckoutResult`: `{ url }`, `{ error: 'out_of_stock', productIds }`
250
+ or `{ error: 'discount_invalid', reason }`. On the server, `toCheckoutResult(error)`
251
+ from `@develemit/storefront/client` maps `CheckoutConflictError` and
252
+ `DiscountInvalidError` to those results (other errors are rethrown). The hook
253
+ removes out-of-stock lines, sets `message`, and redirects with
254
+ `window.location.assign` unless `onRedirect` is given. Copy is overridable
255
+ through `labels` (`outOfStock`, `unavailable`, `discountReasons`).
256
+
257
+ ### Success page
258
+
259
+ `<CheckoutSuccessView order={order} locale? labels? />` renders an order from
260
+ `getOrderBySession`: lines, subtotal, discount row (only when applied),
261
+ shipping, total, ship-to address (when the order includes `shippingAddress`),
262
+ tracking, and a message per `OrderStatus` (`pending` tells the buyer to expect
263
+ an email). It has no hooks, so a server component can render it. Labels live in
264
+ `labels` (including `labels.status`). `<ClearCartOnMount />` empties the cart
265
+ once, even under React strict mode. Styles are `.sf-checkout-success*`.
266
+
267
+ ### Discount codes
268
+
269
+ `CartSummary` (and `CartDrawer` through `summary`) takes an optional `discount`
270
+ prop. The kit stays framework-agnostic, so the host performs the API call (a
271
+ server action in Next):
272
+
273
+ ```tsx
274
+ <CartDrawer
275
+ summary={{
276
+ onCheckout: checkout,
277
+ discount: {
278
+ onPreview: async (code, items) => {
279
+ // call client.previewDiscount; map DiscountInvalidError to a reason
280
+ return { ok: true, code, discountCents: 480 };
281
+ // or { ok: false, reason: 'exhausted' }
282
+ },
283
+ onApply: (code) => setDiscountCode(code), // null when removed or dropped
284
+ },
285
+ }}
286
+ />
287
+ ```
288
+
289
+ It renders a collapsible "Have a code?" field, a "Discount (CODE) −$4.80" row
290
+ with Remove, and a message for a rejected code. When the cart changes
291
+ (debounced, `debounceMs`, default 300) the code is previewed again and dropped
292
+ with its reason if it no longer applies. Pass the applied code to
293
+ `createCheckout`. Override copy with `labels.discount` (`toggle`, `field`,
294
+ `apply`, `remove`, `line`, `unavailable`, and `reasons` per rejection reason).
295
+
238
296
  ### Polish hooks
239
297
 
240
298
  - `CartDrawer` `slots={{ empty, summaryNote }}`: `empty` replaces the
@@ -0,0 +1 @@
1
+ export declare function ClearCartOnMount(): null;
@@ -0,0 +1,27 @@
1
+ import { DiscountRejectionReason } from '@develemit/commerce-contracts';
2
+ import { CheckoutResult } from '../client/checkout-result';
3
+ export interface CheckoutLabels {
4
+ outOfStock: string;
5
+ unavailable: string;
6
+ discountReasons?: Partial<Record<DiscountRejectionReason, string>>;
7
+ }
8
+ export declare const defaultCheckoutLabels: CheckoutLabels;
9
+ export interface StartCheckoutOptions {
10
+ discountCode?: string;
11
+ }
12
+ export interface UseCheckoutOptions {
13
+ startCheckout: (items: {
14
+ productId: string;
15
+ quantity: number;
16
+ }[], options: StartCheckoutOptions) => Promise<CheckoutResult>;
17
+ onRedirect?: (url: string) => void;
18
+ onDiscountInvalid?: (reason: DiscountRejectionReason) => void;
19
+ labels?: Partial<CheckoutLabels>;
20
+ }
21
+ export interface UseCheckoutValue {
22
+ checkout: (options?: StartCheckoutOptions) => Promise<void>;
23
+ busy: boolean;
24
+ message: string | null;
25
+ clearMessage: () => void;
26
+ }
27
+ export declare function useCheckout({ startCheckout, onRedirect, onDiscountInvalid, labels, }: UseCheckoutOptions): UseCheckoutValue;
@@ -0,0 +1,12 @@
1
+ import { DiscountRejectionReason } from '@develemit/commerce-contracts';
2
+ export type CheckoutFailure = {
3
+ error: 'out_of_stock';
4
+ productIds: string[];
5
+ } | {
6
+ error: 'discount_invalid';
7
+ reason: DiscountRejectionReason;
8
+ };
9
+ export type CheckoutResult = {
10
+ url: string;
11
+ } | CheckoutFailure;
12
+ export declare function toCheckoutResult(error: unknown): CheckoutFailure;
@@ -1,4 +1,4 @@
1
- import { Category, CheckoutRequest, CheckoutResponse, OrderSummary, ProductCardPage, Product, ProductListQuery } from '@develemit/commerce-contracts';
1
+ import { Category, CheckoutRequest, CheckoutResponse, DiscountPreview, OrderSummary, ProductCardPage, Product, ProductListQuery } from '@develemit/commerce-contracts';
2
2
  export interface CommerceClientOptions {
3
3
  baseUrl: string;
4
4
  fetch?: typeof fetch;
@@ -7,7 +7,14 @@ export interface CommerceClient {
7
7
  listProducts(query?: ProductListQuery): Promise<ProductCardPage>;
8
8
  getProduct(slug: string): Promise<Product>;
9
9
  listCategories(): Promise<Category[]>;
10
- createCheckout(items: CheckoutRequest['items']): Promise<CheckoutResponse>;
10
+ createCheckout(items: CheckoutRequest['items'], options?: CreateCheckoutOptions): Promise<CheckoutResponse>;
11
+ previewDiscount(input: {
12
+ code: string;
13
+ items: CheckoutRequest['items'];
14
+ }): Promise<DiscountPreview>;
11
15
  getOrderBySession(sessionId: string): Promise<OrderSummary>;
12
16
  }
17
+ export interface CreateCheckoutOptions {
18
+ discountCode?: string;
19
+ }
13
20
  export declare function createCommerceClient({ baseUrl, fetch: fetchImpl, }: CommerceClientOptions): CommerceClient;
@@ -1,3 +1,4 @@
1
+ import { DiscountRejectionReason } from '@develemit/commerce-contracts';
1
2
  export declare class CommerceApiError extends Error {
2
3
  readonly status: number;
3
4
  readonly url: string;
@@ -10,3 +11,7 @@ export declare class CheckoutConflictError extends CommerceApiError {
10
11
  readonly productIds: string[];
11
12
  constructor(url: string, productIds: string[]);
12
13
  }
14
+ export declare class DiscountInvalidError extends CommerceApiError {
15
+ readonly reason: DiscountRejectionReason;
16
+ constructor(url: string, reason: DiscountRejectionReason);
17
+ }
@@ -1,2 +1,3 @@
1
1
  export * from './commerce-client';
2
2
  export * from './errors';
3
+ export * from './checkout-result';
@@ -1,64 +1,97 @@
1
- import { categorySchema as e, checkoutConflictSchema as t, checkoutResponseSchema as n, orderSummarySchema as r, productCardPageSchema as i, productSchema as a } from "@develemit/commerce-contracts";
1
+ import { categorySchema as e, checkoutConflictSchema as t, checkoutResponseSchema as n, discountPreviewSchema as r, discountRejectionSchema as i, orderSummarySchema as a, productCardPageSchema as o, productSchema as s } from "@develemit/commerce-contracts";
2
2
  // src/client/errors.ts
3
- var o = class extends Error {
3
+ var c = class extends Error {
4
4
  status;
5
5
  url;
6
6
  constructor(e, t, n) {
7
7
  super(n ?? `Commerce API request failed (${e}): ${t}`), this.status = e, this.url = t, this.name = "CommerceApiError";
8
8
  }
9
- }, s = class extends o {
9
+ }, l = class extends c {
10
10
  constructor(e) {
11
11
  super(404, e, `Not found: ${e}`), this.name = "NotFoundError";
12
12
  }
13
- }, c = class extends o {
13
+ }, u = class extends c {
14
14
  productIds;
15
15
  constructor(e, t) {
16
16
  super(409, e, `Some items are no longer available: ${t.join(", ")}`), this.productIds = t, this.name = "CheckoutConflictError";
17
17
  }
18
+ }, d = class extends c {
19
+ reason;
20
+ constructor(e, t) {
21
+ super(422, e, `Discount code rejected: ${t}`), this.reason = t, this.name = "DiscountInvalidError";
22
+ }
18
23
  };
19
24
  // src/client/commerce-client.ts
20
- function l({ baseUrl: l, fetch: u = (...e) => globalThis.fetch(...e) }) {
21
- let d = l.replace(/\/+$/, "");
22
- async function f(e, t, n) {
23
- if (t.status === 404) throw new s(e);
24
- if (!t.ok) throw new o(t.status, e);
25
+ function f({ baseUrl: f, fetch: p = (...e) => globalThis.fetch(...e) }) {
26
+ let m = f.replace(/\/+$/, "");
27
+ async function h(e, t, n) {
28
+ if (t.status === 404) throw new l(e);
29
+ if (!t.ok) throw new c(t.status, e);
25
30
  return n.parse(await t.json());
26
31
  }
27
- async function p(e, t) {
28
- let n = `${d}${e}`;
29
- return f(n, await u(n, { headers: { accept: "application/json" } }), t);
32
+ async function g(e, t) {
33
+ let n = `${m}${e}`;
34
+ return h(n, await p(n, { headers: { accept: "application/json" } }), t);
30
35
  }
31
- async function m(e) {
32
- let r = `${d}/checkout`, i = await u(r, {
36
+ function _(e, t) {
37
+ return p(e, {
33
38
  method: "POST",
34
39
  headers: {
35
40
  accept: "application/json",
36
41
  "content-type": "application/json"
37
42
  },
38
- body: JSON.stringify({ items: e })
43
+ body: JSON.stringify(t)
39
44
  });
40
- if (i.status === 409) throw new c(r, t.parse(await i.json()).unavailableProductIds);
41
- return f(r, i, n);
45
+ }
46
+ async function v(e, t) {
47
+ if (t.status !== 422) return;
48
+ let n = i.safeParse(await t.json().catch(() => null));
49
+ if (n.success) throw new d(e, n.data.reason);
50
+ }
51
+ async function y(e, { discountCode: r } = {}) {
52
+ let i = `${m}/checkout`, a = await _(i, r ? {
53
+ items: e,
54
+ discountCode: r
55
+ } : { items: e });
56
+ if (a.status === 409) throw new u(i, t.parse(await a.json()).unavailableProductIds);
57
+ return await v(i, a), h(i, a, n);
58
+ }
59
+ async function b(e) {
60
+ let t = `${m}/checkout/discount/preview`, n = await _(t, e);
61
+ return await v(t, n), h(t, n, r);
42
62
  }
43
63
  return {
44
64
  async listProducts(e = {}) {
45
65
  let t = new URLSearchParams();
46
66
  e.category && t.set("category", e.category), e.featured !== void 0 && t.set("featured", String(e.featured)), e.limit !== void 0 && t.set("limit", String(e.limit)), e.cursor && t.set("cursor", e.cursor);
47
67
  let n = t.toString();
48
- return p(`/catalog/products${n ? `?${n}` : ""}`, i);
68
+ return g(`/catalog/products${n ? `?${n}` : ""}`, o);
49
69
  },
50
70
  getProduct(e) {
51
- return p(`/catalog/products/${encodeURIComponent(e)}`, a);
71
+ return g(`/catalog/products/${encodeURIComponent(e)}`, s);
52
72
  },
53
73
  listCategories() {
54
- return p("/catalog/categories", e.array());
74
+ return g("/catalog/categories", e.array());
55
75
  },
56
- createCheckout: m,
76
+ createCheckout: y,
77
+ previewDiscount: b,
57
78
  getOrderBySession(e) {
58
- return p(`/orders/session/${encodeURIComponent(e)}`, r);
79
+ return g(`/orders/session/${encodeURIComponent(e)}`, a);
59
80
  }
60
81
  };
61
82
  }
62
- export { c as CheckoutConflictError, o as CommerceApiError, s as NotFoundError, l as createCommerceClient };
83
+ // src/client/checkout-result.ts
84
+ function p(e) {
85
+ if (e instanceof u) return {
86
+ error: "out_of_stock",
87
+ productIds: e.productIds
88
+ };
89
+ if (e instanceof d) return {
90
+ error: "discount_invalid",
91
+ reason: e.reason
92
+ };
93
+ throw e;
94
+ }
95
+ export { u as CheckoutConflictError, c as CommerceApiError, d as DiscountInvalidError, l as NotFoundError, f as createCommerceClient, p as toCheckoutResult };
63
96
 
64
97
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":["CommerceApiError","message","status","url","NotFoundError","CheckoutConflictError","productIds","createCommerceClient","fetchImpl","args","root","baseUrl","readJson","res","schema","get","path","createCheckout","checkoutConflictSchema","checkoutResponseSchema","params","query","qs","productCardPageSchema","slug","productSchema","categorySchema","sessionId","orderSummarySchema"],"sources":["../../src/client/errors.ts","../../src/client/commerce-client.ts"],"sourcesContent":["export class CommerceApiError extends Error {\n constructor(\n readonly status: number,\n readonly url: string,\n message?: string,\n ) {\n super(message ?? `Commerce API request failed (${status}): ${url}`);\n this.name = 'CommerceApiError';\n }\n}\n\nexport class NotFoundError extends CommerceApiError {\n constructor(url: string) {\n super(404, url, `Not found: ${url}`);\n this.name = 'NotFoundError';\n }\n}\n\nexport class CheckoutConflictError extends CommerceApiError {\n constructor(\n url: string,\n readonly productIds: string[],\n ) {\n super(\n 409,\n url,\n `Some items are no longer available: ${productIds.join(', ')}`,\n );\n this.name = 'CheckoutConflictError';\n }\n}\n","import {\n categorySchema,\n checkoutConflictSchema,\n checkoutResponseSchema,\n orderSummarySchema,\n productCardPageSchema,\n productSchema,\n type Category,\n type CheckoutRequest,\n type CheckoutResponse,\n type OrderSummary,\n type ProductCardPage,\n type Product,\n type ProductListQuery,\n} from '@develemit/commerce-contracts';\nimport {\n CheckoutConflictError,\n CommerceApiError,\n NotFoundError,\n} from './errors';\n\nexport interface CommerceClientOptions {\n baseUrl: string;\n fetch?: typeof fetch;\n}\n\nexport interface CommerceClient {\n listProducts(query?: ProductListQuery): Promise<ProductCardPage>;\n getProduct(slug: string): Promise<Product>;\n listCategories(): Promise<Category[]>;\n createCheckout(items: CheckoutRequest['items']): Promise<CheckoutResponse>;\n getOrderBySession(sessionId: string): Promise<OrderSummary>;\n}\n\ntype Schema<T> = { parse(data: unknown): T };\n\nexport function createCommerceClient({\n baseUrl,\n fetch: fetchImpl = (...args) => globalThis.fetch(...args),\n}: CommerceClientOptions): CommerceClient {\n const root = baseUrl.replace(/\\/+$/, '');\n\n async function readJson<T>(\n url: string,\n res: Response,\n schema: Schema<T>,\n ): Promise<T> {\n if (res.status === 404) throw new NotFoundError(url);\n if (!res.ok) throw new CommerceApiError(res.status, url);\n return schema.parse(await res.json());\n }\n\n async function get<T>(path: string, schema: Schema<T>): Promise<T> {\n const url = `${root}${path}`;\n const res = await fetchImpl(url, {\n headers: { accept: 'application/json' },\n });\n return readJson(url, res, schema);\n }\n\n async function createCheckout(\n items: CheckoutRequest['items'],\n ): Promise<CheckoutResponse> {\n const url = `${root}/checkout`;\n const res = await fetchImpl(url, {\n method: 'POST',\n headers: {\n accept: 'application/json',\n 'content-type': 'application/json',\n },\n body: JSON.stringify({ items }),\n });\n if (res.status === 409) {\n const conflict = checkoutConflictSchema.parse(await res.json());\n throw new CheckoutConflictError(url, conflict.unavailableProductIds);\n }\n return readJson(url, res, checkoutResponseSchema);\n }\n\n return {\n async listProducts(query = {}) {\n const params = new URLSearchParams();\n if (query.category) params.set('category', query.category);\n if (query.featured !== undefined) {\n params.set('featured', String(query.featured));\n }\n if (query.limit !== undefined) params.set('limit', String(query.limit));\n if (query.cursor) params.set('cursor', query.cursor);\n const qs = params.toString();\n return get(\n `/catalog/products${qs ? `?${qs}` : ''}`,\n productCardPageSchema,\n );\n },\n getProduct(slug) {\n return get(\n `/catalog/products/${encodeURIComponent(slug)}`,\n productSchema,\n );\n },\n listCategories() {\n return get('/catalog/categories', categorySchema.array());\n },\n createCheckout,\n getOrderBySession(sessionId) {\n return get(\n `/orders/session/${encodeURIComponent(sessionId)}`,\n orderSummarySchema,\n );\n },\n };\n}\n"],"mappings":";;AAAA,IAAaA,IAAb,cAAsC,MAAM;CAE/B;CACA;CAFX,YACE,GACA,GACA,GACA;EAEA,AADA,MAAMC,KAAW,gCAAgCC,EAAO,KAAKC,GAAK,GAJzD,KAAA,SAAAD,GACA,KAAA,MAAAC,GAIT,KAAK,OAAO;CACd;AACF,GAEaC,IAAb,cAAmCJ,EAAiB;CAClD,YAAY,GAAa;EAEvB,AADA,MAAM,KAAKG,GAAK,cAAcA,GAAK,GACnC,KAAK,OAAO;CACd;AACF,GAEaE,IAAb,cAA2CL,EAAiB;CAG/C;CAFX,YACE,GACA,GACA;EAMA,AALA,MACE,KACAG,GACA,uCAAuCG,EAAW,KAAK,IAAI,GAC7D,GANS,KAAA,aAAAA,GAOT,KAAK,OAAO;CACd;AACF;;ACMA,SAAgBC,EAAqB,EACnC,YACA,OAAOC,KAAa,GAAGC,MAAS,WAAW,MAAM,GAAGA,CAAI,KAChB;CACxC,IAAMC,IAAOC,EAAQ,QAAQ,QAAQ,EAAE;CAEvC,eAAeC,EACb,GACA,GACA,GACY;EACZ,IAAIC,EAAI,WAAW,KAAK,MAAM,IAAIT,EAAcD,CAAG;EACnD,IAAI,CAACU,EAAI,IAAI,MAAM,IAAIb,EAAiBa,EAAI,QAAQV,CAAG;EACvD,OAAOW,EAAO,MAAM,MAAMD,EAAI,KAAK,CAAC;CACtC;CAEA,eAAeE,EAAO,GAAc,GAA+B;EACjE,IAAMZ,IAAM,GAAGO,IAAOM;EAItB,OAAOJ,EAAST,GAAK,MAHHK,EAAUL,GAAK,EAC/B,SAAS,EAAE,QAAQ,mBAAmB,EACxC,CAAC,GACyBW,CAAM;CAClC;CAEA,eAAeG,EACb,GAC2B;EAC3B,IAAMd,IAAM,GAAGO,EAAK,YACdG,IAAM,MAAML,EAAUL,GAAK;GAC/B,QAAQ;GACR,SAAS;IACP,QAAQ;IACR,gBAAgB;GAClB;GACA,MAAM,KAAK,UAAU,EAAE,SAAM,CAAC;EAChC,CAAC;EACD,IAAIU,EAAI,WAAW,KAEjB,MAAM,IAAIR,EAAsBF,GADfe,EAAuB,MAAM,MAAML,EAAI,KAAK,CACxB,CAAA,CAAS,qBAAqB;EAErE,OAAOD,EAAST,GAAKU,GAAKM,CAAsB;CAClD;CAEA,OAAO;EACL,MAAM,aAAa,IAAQ,CAAC,GAAG;GAC7B,IAAMC,IAAS,IAAI,gBAAgB;GAMnC,AALIC,EAAM,YAAUD,EAAO,IAAI,YAAYC,EAAM,QAAQ,GACrDA,EAAM,aAAa,KAAA,KACrBD,EAAO,IAAI,YAAY,OAAOC,EAAM,QAAQ,CAAC,GAE3CA,EAAM,UAAU,KAAA,KAAWD,EAAO,IAAI,SAAS,OAAOC,EAAM,KAAK,CAAC,GAClEA,EAAM,UAAQD,EAAO,IAAI,UAAUC,EAAM,MAAM;GACnD,IAAMC,IAAKF,EAAO,SAAS;GAC3B,OAAOL,EACL,oBAAoBO,IAAK,IAAIA,MAAO,MACpCC,CACF;EACF;EACA,WAAW,GAAM;GACf,OAAOR,EACL,qBAAqB,mBAAmBS,CAAI,KAC5CC,CACF;EACF;EACA,iBAAiB;GACf,OAAOV,EAAI,uBAAuBW,EAAe,MAAM,CAAC;EAC1D;EACA;EACA,kBAAkB,GAAW;GAC3B,OAAOX,EACL,mBAAmB,mBAAmBY,CAAS,KAC/CC,CACF;EACF;CACF;AACF"}
1
+ {"version":3,"file":"index.js","names":["CommerceApiError","message","status","url","NotFoundError","CheckoutConflictError","productIds","DiscountInvalidError","reason","createCommerceClient","fetchImpl","args","root","baseUrl","readJson","res","schema","get","path","postJson","body","throwIfDiscountInvalid","parsed","discountRejectionSchema","createCheckout","discountCode","checkoutConflictSchema","checkoutResponseSchema","previewDiscount","input","discountPreviewSchema","params","query","qs","productCardPageSchema","slug","productSchema","categorySchema","sessionId","orderSummarySchema","toCheckoutResult","error"],"sources":["../../src/client/errors.ts","../../src/client/commerce-client.ts","../../src/client/checkout-result.ts"],"sourcesContent":["import type { DiscountRejectionReason } from '@develemit/commerce-contracts';\n\nexport class CommerceApiError extends Error {\n constructor(\n readonly status: number,\n readonly url: string,\n message?: string,\n ) {\n super(message ?? `Commerce API request failed (${status}): ${url}`);\n this.name = 'CommerceApiError';\n }\n}\n\nexport class NotFoundError extends CommerceApiError {\n constructor(url: string) {\n super(404, url, `Not found: ${url}`);\n this.name = 'NotFoundError';\n }\n}\n\nexport class CheckoutConflictError extends CommerceApiError {\n constructor(\n url: string,\n readonly productIds: string[],\n ) {\n super(\n 409,\n url,\n `Some items are no longer available: ${productIds.join(', ')}`,\n );\n this.name = 'CheckoutConflictError';\n }\n}\n\nexport class DiscountInvalidError extends CommerceApiError {\n constructor(\n url: string,\n readonly reason: DiscountRejectionReason,\n ) {\n super(422, url, `Discount code rejected: ${reason}`);\n this.name = 'DiscountInvalidError';\n }\n}\n","import {\n categorySchema,\n checkoutConflictSchema,\n checkoutResponseSchema,\n discountPreviewSchema,\n discountRejectionSchema,\n orderSummarySchema,\n productCardPageSchema,\n productSchema,\n type Category,\n type CheckoutRequest,\n type CheckoutResponse,\n type DiscountPreview,\n type OrderSummary,\n type ProductCardPage,\n type Product,\n type ProductListQuery,\n} from '@develemit/commerce-contracts';\nimport {\n CheckoutConflictError,\n CommerceApiError,\n DiscountInvalidError,\n NotFoundError,\n} from './errors';\n\nexport interface CommerceClientOptions {\n baseUrl: string;\n fetch?: typeof fetch;\n}\n\nexport interface CommerceClient {\n listProducts(query?: ProductListQuery): Promise<ProductCardPage>;\n getProduct(slug: string): Promise<Product>;\n listCategories(): Promise<Category[]>;\n createCheckout(\n items: CheckoutRequest['items'],\n options?: CreateCheckoutOptions,\n ): Promise<CheckoutResponse>;\n previewDiscount(input: {\n code: string;\n items: CheckoutRequest['items'];\n }): Promise<DiscountPreview>;\n getOrderBySession(sessionId: string): Promise<OrderSummary>;\n}\n\nexport interface CreateCheckoutOptions {\n discountCode?: string;\n}\n\ntype Schema<T> = { parse(data: unknown): T };\n\nexport function createCommerceClient({\n baseUrl,\n fetch: fetchImpl = (...args) => globalThis.fetch(...args),\n}: CommerceClientOptions): CommerceClient {\n const root = baseUrl.replace(/\\/+$/, '');\n\n async function readJson<T>(\n url: string,\n res: Response,\n schema: Schema<T>,\n ): Promise<T> {\n if (res.status === 404) throw new NotFoundError(url);\n if (!res.ok) throw new CommerceApiError(res.status, url);\n return schema.parse(await res.json());\n }\n\n async function get<T>(path: string, schema: Schema<T>): Promise<T> {\n const url = `${root}${path}`;\n const res = await fetchImpl(url, {\n headers: { accept: 'application/json' },\n });\n return readJson(url, res, schema);\n }\n\n function postJson(url: string, body: unknown): Promise<Response> {\n return fetchImpl(url, {\n method: 'POST',\n headers: {\n accept: 'application/json',\n 'content-type': 'application/json',\n },\n body: JSON.stringify(body),\n });\n }\n\n async function throwIfDiscountInvalid(url: string, res: Response) {\n if (res.status !== 422) return;\n const parsed = discountRejectionSchema.safeParse(\n await res.json().catch(() => null),\n );\n if (parsed.success) throw new DiscountInvalidError(url, parsed.data.reason);\n }\n\n async function createCheckout(\n items: CheckoutRequest['items'],\n { discountCode }: CreateCheckoutOptions = {},\n ): Promise<CheckoutResponse> {\n const url = `${root}/checkout`;\n const res = await postJson(\n url,\n discountCode ? { items, discountCode } : { items },\n );\n if (res.status === 409) {\n const conflict = checkoutConflictSchema.parse(await res.json());\n throw new CheckoutConflictError(url, conflict.unavailableProductIds);\n }\n await throwIfDiscountInvalid(url, res);\n return readJson(url, res, checkoutResponseSchema);\n }\n\n async function previewDiscount(input: {\n code: string;\n items: CheckoutRequest['items'];\n }): Promise<DiscountPreview> {\n const url = `${root}/checkout/discount/preview`;\n const res = await postJson(url, input);\n await throwIfDiscountInvalid(url, res);\n return readJson(url, res, discountPreviewSchema);\n }\n\n return {\n async listProducts(query = {}) {\n const params = new URLSearchParams();\n if (query.category) params.set('category', query.category);\n if (query.featured !== undefined) {\n params.set('featured', String(query.featured));\n }\n if (query.limit !== undefined) params.set('limit', String(query.limit));\n if (query.cursor) params.set('cursor', query.cursor);\n const qs = params.toString();\n return get(\n `/catalog/products${qs ? `?${qs}` : ''}`,\n productCardPageSchema,\n );\n },\n getProduct(slug) {\n return get(\n `/catalog/products/${encodeURIComponent(slug)}`,\n productSchema,\n );\n },\n listCategories() {\n return get('/catalog/categories', categorySchema.array());\n },\n createCheckout,\n previewDiscount,\n getOrderBySession(sessionId) {\n return get(\n `/orders/session/${encodeURIComponent(sessionId)}`,\n orderSummarySchema,\n );\n },\n };\n}\n","import type { DiscountRejectionReason } from '@develemit/commerce-contracts';\nimport { CheckoutConflictError, DiscountInvalidError } from './errors';\n\nexport type CheckoutFailure =\n | { error: 'out_of_stock'; productIds: string[] }\n | { error: 'discount_invalid'; reason: DiscountRejectionReason };\n\nexport type CheckoutResult = { url: string } | CheckoutFailure;\n\nexport function toCheckoutResult(error: unknown): CheckoutFailure {\n if (error instanceof CheckoutConflictError) {\n return { error: 'out_of_stock', productIds: error.productIds };\n }\n if (error instanceof DiscountInvalidError) {\n return { error: 'discount_invalid', reason: error.reason };\n }\n throw error;\n}\n"],"mappings":"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"}
@@ -1,10 +1,17 @@
1
+ import { CartSummaryDiscount, DiscountLabels } from './discount-types';
2
+ export type { CartSummaryDiscount, DiscountPreviewResult, } from './discount-types';
3
+ export { discountReasonMessage } from './discount-types';
1
4
  export interface CartSummaryLabels {
2
5
  subtotal: string;
6
+ discount: Partial<Omit<DiscountLabels, 'reasons'>> & {
7
+ reasons?: Partial<DiscountLabels['reasons']>;
8
+ };
3
9
  }
4
10
  export interface CartSummaryProps {
5
11
  onCheckout?: () => void;
6
12
  checkoutLabel?: string;
7
13
  checkoutDisabled?: boolean;
8
14
  labels?: Partial<CartSummaryLabels>;
15
+ discount?: CartSummaryDiscount;
9
16
  }
10
- export declare function CartSummary({ onCheckout, checkoutLabel, checkoutDisabled, labels, }: CartSummaryProps): import('react').JSX.Element;
17
+ export declare function CartSummary({ onCheckout, checkoutLabel, checkoutDisabled, labels, discount, }: CartSummaryProps): import('react').JSX.Element;
@@ -0,0 +1,13 @@
1
+ import { DiscountLabels } from './discount-types';
2
+ import { AppliedDiscount, DiscountError } from './use-discount';
3
+ export interface DiscountFieldProps {
4
+ labels: DiscountLabels;
5
+ applied: AppliedDiscount | null;
6
+ error: DiscountError | null;
7
+ pending: boolean;
8
+ currency: string;
9
+ locale: string;
10
+ onApply: (code: string) => void;
11
+ onRemove: () => void;
12
+ }
13
+ export declare function DiscountField({ labels, applied, error, pending, currency, locale, onApply, onRemove, }: DiscountFieldProps): import('react').JSX.Element;
@@ -0,0 +1,28 @@
1
+ import { DiscountRejectionReason } from '@develemit/commerce-contracts';
2
+ export type DiscountPreviewResult = {
3
+ ok: true;
4
+ code: string;
5
+ discountCents: number;
6
+ } | {
7
+ ok: false;
8
+ reason: DiscountRejectionReason;
9
+ };
10
+ export interface CartSummaryDiscount {
11
+ onPreview: (code: string, items: {
12
+ productId: string;
13
+ quantity: number;
14
+ }[]) => Promise<DiscountPreviewResult>;
15
+ onApply?: (code: string | null) => void;
16
+ debounceMs?: number;
17
+ }
18
+ export interface DiscountLabels {
19
+ toggle: string;
20
+ field: string;
21
+ apply: string;
22
+ remove: string;
23
+ line: string;
24
+ reasons: Record<DiscountRejectionReason, string>;
25
+ unavailable: string;
26
+ }
27
+ export declare const defaultDiscountLabels: DiscountLabels;
28
+ export declare function discountReasonMessage(reason: DiscountRejectionReason, overrides?: Partial<DiscountLabels['reasons']>): string;
@@ -0,0 +1,14 @@
1
+ import { DiscountRejectionReason } from '@develemit/commerce-contracts';
2
+ import { CartSummaryDiscount } from './discount-types';
3
+ export interface AppliedDiscount {
4
+ code: string;
5
+ discountCents: number;
6
+ }
7
+ export type DiscountError = DiscountRejectionReason | 'unavailable';
8
+ export declare function useDiscount(discount: CartSummaryDiscount | undefined): {
9
+ applied: AppliedDiscount | null;
10
+ error: DiscountError | null;
11
+ pending: boolean;
12
+ apply: (code: string) => Promise<void>;
13
+ remove: () => void;
14
+ };
@@ -0,0 +1,13 @@
1
+ import { OrderAddress, OrderSummary } from '@develemit/commerce-contracts';
2
+ import { CheckoutSuccessLabels } from './labels';
3
+ export interface CheckoutSuccessViewProps {
4
+ order: OrderSummary & {
5
+ shippingAddress?: OrderAddress | null;
6
+ };
7
+ locale?: string;
8
+ labels?: Partial<Omit<CheckoutSuccessLabels, 'status'>> & {
9
+ status?: Partial<CheckoutSuccessLabels['status']>;
10
+ };
11
+ className?: string;
12
+ }
13
+ export declare function CheckoutSuccessView({ order, locale, labels, className, }: CheckoutSuccessViewProps): import('react').JSX.Element;
@@ -0,0 +1,12 @@
1
+ import { OrderStatus } from '@develemit/commerce-contracts';
2
+ export interface CheckoutSuccessLabels {
3
+ heading: string;
4
+ subtotal: string;
5
+ discount: string;
6
+ shipping: string;
7
+ total: string;
8
+ shipTo: string;
9
+ tracking: string;
10
+ status: Record<OrderStatus, string>;
11
+ }
12
+ export declare const defaultCheckoutSuccessLabels: CheckoutSuccessLabels;
package/dist/index.d.ts CHANGED
@@ -15,8 +15,12 @@ export * from './views/sort-for-display';
15
15
  export * from './cart/cart-reducer';
16
16
  export * from './cart/cart-storage';
17
17
  export * from './cart/cart-provider';
18
+ export * from './cart/use-checkout';
18
19
  export * from './components/add-to-cart-button/add-to-cart-button';
19
20
  export * from './components/cart-drawer/cart-drawer';
20
21
  export * from './components/cart-line-item/cart-line-item';
21
22
  export * from './components/cart-summary/cart-summary';
22
23
  export * from './components/cart-button/cart-button';
24
+ export * from './components/checkout-success/checkout-success-view';
25
+ export * from './components/checkout-success/labels';
26
+ export * from './cart/clear-cart-on-mount';