@usethrottle/cart 3.3.0 → 3.5.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
@@ -2,7 +2,6 @@
2
2
  <img src="https://raw.githubusercontent.com/Epic-Design-Labs/app-throttle/main/packages/brand/assets/throttle-logo.png" alt="Throttle" height="56" />
3
3
  </p>
4
4
 
5
-
6
5
  # @usethrottle/cart
7
6
 
8
7
  Typed Node.js REST client for the Throttle Cart API.
@@ -62,6 +61,43 @@ are rejected by the API.
62
61
 
63
62
  All monetary values are integers in the smallest currency unit (cents for USD).
64
63
 
64
+ ## Browser carts (backend-less)
65
+
66
+ `CartClient` carries a secret `sk_` key and must run on your server. For
67
+ JAMstack / frontend-only storefronts that have no backend, use
68
+ `CartSessionClient` instead — it creates and owns a Throttle cart directly from
69
+ the browser, authorized by a publishable `pk_` quote token (the same token used
70
+ by `StorefrontQuoteClient`) plus your application's origin allowlist.
71
+
72
+ Set your allowed origins first (`PUT /api/v1/embed-config`), then:
73
+
74
+ ```ts
75
+ import { CartSessionClient } from '@usethrottle/cart';
76
+
77
+ const cart = new CartSessionClient({
78
+ applicationId: '7f9d4c8a-5b2e-4f16-9a73-2d1e5c8b6f40',
79
+ environmentId: 'a1b2c3d4-...',
80
+ quoteToken: 'pk_live_...',
81
+ });
82
+
83
+ // Create a new session (store cart.cartSessionId in localStorage),
84
+ // or resume one you saved: cart.resume(savedId)
85
+ await cart.create({ currency: 'USD' });
86
+
87
+ await cart.addItem({ name: 'Premium Widget', unitPrice: 2999, quantity: 2 });
88
+ await cart.applyDiscount('SAVE10');
89
+ await cart.selectShipping({ methodId: 'fedex_ground', displayName: 'FedEx Ground', rateAmount: 599 });
90
+
91
+ // Hand off to hosted/embedded checkout and redirect the buyer.
92
+ const { checkoutUrl } = await cart.checkout({
93
+ returnUrl: 'https://shop.example.com/thanks',
94
+ cancelUrl: 'https://shop.example.com/cart',
95
+ });
96
+ ```
97
+
98
+ The opaque `cart_…` session id is scoped to exactly one cart. See the
99
+ [cart sessions guide](https://docs.usethrottle.dev/developers/cart-sessions).
100
+
65
101
  ## Net-N invoice terms
66
102
 
67
103
  Cart-level invoice terms are optional. Pass `netN` on cart create or update to
@@ -223,12 +259,12 @@ resolvable base returns `400 image_url_unresolvable`.
223
259
 
224
260
  ## Client options
225
261
 
226
- | Option | Default | Description |
227
- | ----------- | ----------------------------- | ----------------------------------------------------------- |
228
- | `apiKey` | — | **Required.** Your Throttle secret key (`sk_…`). |
229
- | `baseUrl` | `https://api.usethrottle.dev` | Optional override for a dedicated Throttle API environment. |
230
- | `timeoutMs` | `30000` | Per-request abort timeout in ms. |
231
- | `fetch` | `globalThis.fetch` | Custom fetch implementation (e.g. `node-fetch`). |
262
+ | Option | Default | Description |
263
+ | ----------- | ----------------------------- | ------------------------------------------------------------------------------------------------------------------------- |
264
+ | `apiKey` | — | **Required.** Your Throttle secret key (`sk_…`). |
265
+ | `baseUrl` | `https://api.usethrottle.dev` | Optional Throttle API host override for self-hosted/proxy setups. Workspace environment selection comes from the API key. |
266
+ | `timeoutMs` | `30000` | Per-request abort timeout in ms. |
267
+ | `fetch` | `globalThis.fetch` | Custom fetch implementation (e.g. `node-fetch`). |
232
268
 
233
269
  ## See also
234
270
 
package/dist/index.cjs CHANGED
@@ -21,6 +21,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
21
21
  var index_exports = {};
22
22
  __export(index_exports, {
23
23
  CartClient: () => CartClient,
24
+ CartSessionClient: () => CartSessionClient,
24
25
  StorefrontQuoteClient: () => StorefrontQuoteClient,
25
26
  ThrottleApiError: () => ThrottleApiError,
26
27
  ThrottleError: () => import_errors2.ThrottleError,
@@ -187,9 +188,117 @@ var StorefrontQuoteClient = class {
187
188
  }
188
189
  }
189
190
  };
191
+
192
+ // src/cart-session.ts
193
+ var CartSessionClient = class {
194
+ applicationId;
195
+ environmentId;
196
+ quoteToken;
197
+ baseUrl;
198
+ fetchImpl;
199
+ timeoutMs;
200
+ origin;
201
+ sessionId = null;
202
+ constructor(opts) {
203
+ if (!opts.applicationId) throw new Error("CartSessionClient: applicationId is required");
204
+ if (!opts.environmentId) throw new Error("CartSessionClient: environmentId is required");
205
+ if (!opts.quoteToken?.startsWith("pk_")) {
206
+ throw new Error("CartSessionClient: quoteToken must be a publishable pk_ token");
207
+ }
208
+ this.applicationId = opts.applicationId;
209
+ this.environmentId = opts.environmentId;
210
+ this.quoteToken = opts.quoteToken;
211
+ this.baseUrl = (opts.baseUrl ?? "https://api.usethrottle.dev").replace(/\/+$/, "");
212
+ this.fetchImpl = opts.fetch ?? globalThis.fetch;
213
+ this.timeoutMs = opts.timeoutMs ?? 15e3;
214
+ this.origin = opts.origin;
215
+ }
216
+ /** The current bound session id (after `create()`/`resume()`), or null. */
217
+ get cartSessionId() {
218
+ return this.sessionId;
219
+ }
220
+ /** Bind to an existing session id — e.g. one restored from `localStorage`. */
221
+ resume(cartSessionId) {
222
+ this.sessionId = cartSessionId;
223
+ return this;
224
+ }
225
+ /** Create a new cart session and bind this client to it. */
226
+ async create(input = {}) {
227
+ const data = await this.request("POST", "/api/v1/cart-sessions", {
228
+ applicationId: this.applicationId,
229
+ environmentId: this.environmentId,
230
+ quoteToken: this.quoteToken,
231
+ ...input
232
+ });
233
+ this.sessionId = data.cartSessionId;
234
+ return data;
235
+ }
236
+ /** Read the current session + cart snapshot. */
237
+ get() {
238
+ return this.request("GET", this.basePath());
239
+ }
240
+ async addItem(input) {
241
+ return (await this.request("POST", `${this.basePath()}/items`, input)).cart;
242
+ }
243
+ async updateItem(itemId, input) {
244
+ return (await this.request("PATCH", `${this.basePath()}/items/${encodeURIComponent(itemId)}`, input)).cart;
245
+ }
246
+ async removeItem(itemId) {
247
+ return (await this.request("DELETE", `${this.basePath()}/items/${encodeURIComponent(itemId)}`)).cart;
248
+ }
249
+ async selectShipping(input) {
250
+ return (await this.request("POST", `${this.basePath()}/shipping`, input)).cart;
251
+ }
252
+ async clearShipping() {
253
+ return (await this.request("DELETE", `${this.basePath()}/shipping`)).cart;
254
+ }
255
+ async applyDiscount(code) {
256
+ return (await this.request("POST", `${this.basePath()}/discount`, { code })).cart;
257
+ }
258
+ async removeDiscount() {
259
+ return (await this.request("DELETE", `${this.basePath()}/discount`)).cart;
260
+ }
261
+ /** Hand off to hosted/embedded checkout. Marks the session converted (no more edits). */
262
+ checkout(input) {
263
+ return this.request("POST", `${this.basePath()}/checkout-session`, input);
264
+ }
265
+ basePath() {
266
+ if (!this.sessionId) {
267
+ throw new Error("CartSessionClient: no active session \u2014 call create() or resume(id) first");
268
+ }
269
+ return `/api/v1/cart-sessions/${encodeURIComponent(this.sessionId)}`;
270
+ }
271
+ async request(method, path, body) {
272
+ const ctrl = new AbortController();
273
+ const timer = setTimeout(() => ctrl.abort(), this.timeoutMs);
274
+ try {
275
+ const headers = { "content-type": "application/json" };
276
+ if (this.origin) headers.origin = this.origin;
277
+ const res = await this.fetchImpl(`${this.baseUrl}${path}`, {
278
+ method,
279
+ headers,
280
+ body: body !== void 0 ? JSON.stringify(body) : void 0,
281
+ signal: ctrl.signal
282
+ });
283
+ const json = await res.json().catch(() => ({}));
284
+ if (!res.ok) {
285
+ throw new ThrottleApiError({
286
+ code: json?.error?.code ?? "unknown_error",
287
+ message: json?.error?.message ?? `HTTP ${res.status}`,
288
+ statusCode: res.status,
289
+ details: json?.error?.details
290
+ });
291
+ }
292
+ return json.data;
293
+ } finally {
294
+ clearTimeout(timer);
295
+ }
296
+ }
297
+ };
190
298
  // Annotate the CommonJS export names for ESM import in node:
191
299
  0 && (module.exports = {
192
300
  CartClient,
301
+ CartSessionClient,
193
302
  StorefrontQuoteClient,
194
303
  ThrottleApiError,
195
304
  ThrottleError,
package/dist/index.d.cts CHANGED
@@ -119,6 +119,13 @@ interface AddLineItemInput {
119
119
  count: number | null;
120
120
  } | null;
121
121
  imageUrl?: string;
122
+ /**
123
+ * Whether this item is physically shippable. For external/ad-hoc catalogs
124
+ * with no Throttle product behind the item. Omit to infer from `type`
125
+ * (everything except `service` ships); set explicitly to force-include or
126
+ * force-exclude the item from `calculated` shipping.
127
+ */
128
+ requiresShipping?: boolean;
122
129
  metadata?: Record<string, unknown>;
123
130
  }
124
131
  interface UpdateLineItemInput {
@@ -199,6 +206,14 @@ interface ShippingTaxCartCalculateInput {
199
206
  kind?: 'cart_estimate' | 'checkout_final';
200
207
  selectedShippingMethodId?: string | null;
201
208
  persist?: boolean;
209
+ /**
210
+ * Inline shipping address. When set it is persisted to the cart (like
211
+ * `carts.update`) before calculation, so you can set the destination and get
212
+ * rates in a single request instead of a separate update + calculate.
213
+ */
214
+ shippingAddress?: ShippingTaxAddress | null;
215
+ /** Inline billing address; persisted to the cart before calculation. */
216
+ billingAddress?: ShippingTaxAddress | null;
202
217
  }
203
218
  interface ShippingTaxMethod {
204
219
  id: string;
@@ -362,6 +377,93 @@ declare class StorefrontQuoteClient {
362
377
  quote(input: ShippingTaxQuoteInput): Promise<ShippingTaxCalculationResponse>;
363
378
  }
364
379
 
380
+ /**
381
+ * Browser client for backend-less cart ownership. Unlike {@link CartClient}
382
+ * (which needs a server-side `sk_` key), `CartSessionClient` runs in the browser:
383
+ * it creates and mutates a Throttle cart authorized by a publishable quote token
384
+ * (`pk_…`, the same token used by {@link StorefrontQuoteClient}) plus the
385
+ * application origin allowlist, and an opaque `cart_…` session id scoped to that
386
+ * one cart.
387
+ *
388
+ * Typical use:
389
+ * ```ts
390
+ * const cart = new CartSessionClient({ applicationId, environmentId, quoteToken });
391
+ * await cart.create(); // or cart.resume(savedId)
392
+ * await cart.addItem({ name: 'Widget', unitPrice: 2999, quantity: 1 });
393
+ * const { checkoutUrl } = await cart.checkout({ returnUrl, cancelUrl });
394
+ * ```
395
+ */
396
+ interface CartSessionClientOptions {
397
+ /** Application UUID. */
398
+ applicationId: string;
399
+ /** Workspace environment UUID the publishable token was minted for. */
400
+ environmentId: string;
401
+ /** Publishable storefront quote token (`pk_…`). */
402
+ quoteToken: string;
403
+ baseUrl?: string;
404
+ fetch?: typeof globalThis.fetch;
405
+ timeoutMs?: number;
406
+ /**
407
+ * Origin to send. Browsers set the `Origin` header automatically (and it can't
408
+ * be overridden from JS), so this is only needed for non-browser callers
409
+ * (SSR, tests). It must be on the application's allowlist.
410
+ */
411
+ origin?: string;
412
+ }
413
+ interface CreateCartSessionInput {
414
+ currency?: string;
415
+ netN?: number;
416
+ metadata?: Record<string, unknown>;
417
+ }
418
+ interface CartSession {
419
+ cartSessionId: string;
420
+ status: string;
421
+ expiresAt: string;
422
+ cart: Cart;
423
+ }
424
+ interface CheckoutHandoffInput {
425
+ returnUrl: string;
426
+ cancelUrl: string;
427
+ customerEmail?: string;
428
+ allowedMethods?: string[];
429
+ }
430
+ interface CheckoutHandoff {
431
+ cartSessionId: string;
432
+ checkoutSessionId: string;
433
+ checkoutUrl: string;
434
+ expiresAt: string;
435
+ }
436
+ declare class CartSessionClient {
437
+ private readonly applicationId;
438
+ private readonly environmentId;
439
+ private readonly quoteToken;
440
+ private readonly baseUrl;
441
+ private readonly fetchImpl;
442
+ private readonly timeoutMs;
443
+ private readonly origin?;
444
+ private sessionId;
445
+ constructor(opts: CartSessionClientOptions);
446
+ /** The current bound session id (after `create()`/`resume()`), or null. */
447
+ get cartSessionId(): string | null;
448
+ /** Bind to an existing session id — e.g. one restored from `localStorage`. */
449
+ resume(cartSessionId: string): this;
450
+ /** Create a new cart session and bind this client to it. */
451
+ create(input?: CreateCartSessionInput): Promise<CartSession>;
452
+ /** Read the current session + cart snapshot. */
453
+ get(): Promise<CartSession>;
454
+ addItem(input: AddLineItemInput): Promise<Cart>;
455
+ updateItem(itemId: string, input: UpdateLineItemInput): Promise<Cart>;
456
+ removeItem(itemId: string): Promise<Cart>;
457
+ selectShipping(input: SelectShippingInput): Promise<Cart>;
458
+ clearShipping(): Promise<Cart>;
459
+ applyDiscount(code: string): Promise<Cart>;
460
+ removeDiscount(): Promise<Cart>;
461
+ /** Hand off to hosted/embedded checkout. Marks the session converted (no more edits). */
462
+ checkout(input: CheckoutHandoffInput): Promise<CheckoutHandoff>;
463
+ private basePath;
464
+ private request;
465
+ }
466
+
365
467
  /**
366
468
  * Thrown on a non-2xx response from the Throttle Cart API.
367
469
  *
@@ -379,4 +481,4 @@ declare class ThrottleApiError extends ThrottleError {
379
481
  });
380
482
  }
381
483
 
382
- export { type AddLineItemInput, type AppliedDiscount, type Cart, CartClient, type CartEvent, type CheckoutInput, type CreateCartInput, type ExternalShippingTaxSnapshotInput, type LineItem, type Order, type SelectShippingInput, type SelectedShipping, type ShippingTaxAddress, type ShippingTaxCalculationResponse, type ShippingTaxCartCalculateInput, type ShippingTaxMethod, type ShippingTaxQuoteInput, type ShippingTaxQuoteItem, StorefrontQuoteClient, type TaxLine, type TaxLineInput, ThrottleApiError, type UpdateCartInput, type UpdateLineItemInput };
484
+ export { type AddLineItemInput, type AppliedDiscount, type Cart, CartClient, type CartEvent, type CartSession, CartSessionClient, type CartSessionClientOptions, type CheckoutHandoff, type CheckoutHandoffInput, type CheckoutInput, type CreateCartInput, type CreateCartSessionInput, type ExternalShippingTaxSnapshotInput, type LineItem, type Order, type SelectShippingInput, type SelectedShipping, type ShippingTaxAddress, type ShippingTaxCalculationResponse, type ShippingTaxCartCalculateInput, type ShippingTaxMethod, type ShippingTaxQuoteInput, type ShippingTaxQuoteItem, StorefrontQuoteClient, type TaxLine, type TaxLineInput, ThrottleApiError, type UpdateCartInput, type UpdateLineItemInput };
package/dist/index.d.ts CHANGED
@@ -119,6 +119,13 @@ interface AddLineItemInput {
119
119
  count: number | null;
120
120
  } | null;
121
121
  imageUrl?: string;
122
+ /**
123
+ * Whether this item is physically shippable. For external/ad-hoc catalogs
124
+ * with no Throttle product behind the item. Omit to infer from `type`
125
+ * (everything except `service` ships); set explicitly to force-include or
126
+ * force-exclude the item from `calculated` shipping.
127
+ */
128
+ requiresShipping?: boolean;
122
129
  metadata?: Record<string, unknown>;
123
130
  }
124
131
  interface UpdateLineItemInput {
@@ -199,6 +206,14 @@ interface ShippingTaxCartCalculateInput {
199
206
  kind?: 'cart_estimate' | 'checkout_final';
200
207
  selectedShippingMethodId?: string | null;
201
208
  persist?: boolean;
209
+ /**
210
+ * Inline shipping address. When set it is persisted to the cart (like
211
+ * `carts.update`) before calculation, so you can set the destination and get
212
+ * rates in a single request instead of a separate update + calculate.
213
+ */
214
+ shippingAddress?: ShippingTaxAddress | null;
215
+ /** Inline billing address; persisted to the cart before calculation. */
216
+ billingAddress?: ShippingTaxAddress | null;
202
217
  }
203
218
  interface ShippingTaxMethod {
204
219
  id: string;
@@ -362,6 +377,93 @@ declare class StorefrontQuoteClient {
362
377
  quote(input: ShippingTaxQuoteInput): Promise<ShippingTaxCalculationResponse>;
363
378
  }
364
379
 
380
+ /**
381
+ * Browser client for backend-less cart ownership. Unlike {@link CartClient}
382
+ * (which needs a server-side `sk_` key), `CartSessionClient` runs in the browser:
383
+ * it creates and mutates a Throttle cart authorized by a publishable quote token
384
+ * (`pk_…`, the same token used by {@link StorefrontQuoteClient}) plus the
385
+ * application origin allowlist, and an opaque `cart_…` session id scoped to that
386
+ * one cart.
387
+ *
388
+ * Typical use:
389
+ * ```ts
390
+ * const cart = new CartSessionClient({ applicationId, environmentId, quoteToken });
391
+ * await cart.create(); // or cart.resume(savedId)
392
+ * await cart.addItem({ name: 'Widget', unitPrice: 2999, quantity: 1 });
393
+ * const { checkoutUrl } = await cart.checkout({ returnUrl, cancelUrl });
394
+ * ```
395
+ */
396
+ interface CartSessionClientOptions {
397
+ /** Application UUID. */
398
+ applicationId: string;
399
+ /** Workspace environment UUID the publishable token was minted for. */
400
+ environmentId: string;
401
+ /** Publishable storefront quote token (`pk_…`). */
402
+ quoteToken: string;
403
+ baseUrl?: string;
404
+ fetch?: typeof globalThis.fetch;
405
+ timeoutMs?: number;
406
+ /**
407
+ * Origin to send. Browsers set the `Origin` header automatically (and it can't
408
+ * be overridden from JS), so this is only needed for non-browser callers
409
+ * (SSR, tests). It must be on the application's allowlist.
410
+ */
411
+ origin?: string;
412
+ }
413
+ interface CreateCartSessionInput {
414
+ currency?: string;
415
+ netN?: number;
416
+ metadata?: Record<string, unknown>;
417
+ }
418
+ interface CartSession {
419
+ cartSessionId: string;
420
+ status: string;
421
+ expiresAt: string;
422
+ cart: Cart;
423
+ }
424
+ interface CheckoutHandoffInput {
425
+ returnUrl: string;
426
+ cancelUrl: string;
427
+ customerEmail?: string;
428
+ allowedMethods?: string[];
429
+ }
430
+ interface CheckoutHandoff {
431
+ cartSessionId: string;
432
+ checkoutSessionId: string;
433
+ checkoutUrl: string;
434
+ expiresAt: string;
435
+ }
436
+ declare class CartSessionClient {
437
+ private readonly applicationId;
438
+ private readonly environmentId;
439
+ private readonly quoteToken;
440
+ private readonly baseUrl;
441
+ private readonly fetchImpl;
442
+ private readonly timeoutMs;
443
+ private readonly origin?;
444
+ private sessionId;
445
+ constructor(opts: CartSessionClientOptions);
446
+ /** The current bound session id (after `create()`/`resume()`), or null. */
447
+ get cartSessionId(): string | null;
448
+ /** Bind to an existing session id — e.g. one restored from `localStorage`. */
449
+ resume(cartSessionId: string): this;
450
+ /** Create a new cart session and bind this client to it. */
451
+ create(input?: CreateCartSessionInput): Promise<CartSession>;
452
+ /** Read the current session + cart snapshot. */
453
+ get(): Promise<CartSession>;
454
+ addItem(input: AddLineItemInput): Promise<Cart>;
455
+ updateItem(itemId: string, input: UpdateLineItemInput): Promise<Cart>;
456
+ removeItem(itemId: string): Promise<Cart>;
457
+ selectShipping(input: SelectShippingInput): Promise<Cart>;
458
+ clearShipping(): Promise<Cart>;
459
+ applyDiscount(code: string): Promise<Cart>;
460
+ removeDiscount(): Promise<Cart>;
461
+ /** Hand off to hosted/embedded checkout. Marks the session converted (no more edits). */
462
+ checkout(input: CheckoutHandoffInput): Promise<CheckoutHandoff>;
463
+ private basePath;
464
+ private request;
465
+ }
466
+
365
467
  /**
366
468
  * Thrown on a non-2xx response from the Throttle Cart API.
367
469
  *
@@ -379,4 +481,4 @@ declare class ThrottleApiError extends ThrottleError {
379
481
  });
380
482
  }
381
483
 
382
- export { type AddLineItemInput, type AppliedDiscount, type Cart, CartClient, type CartEvent, type CheckoutInput, type CreateCartInput, type ExternalShippingTaxSnapshotInput, type LineItem, type Order, type SelectShippingInput, type SelectedShipping, type ShippingTaxAddress, type ShippingTaxCalculationResponse, type ShippingTaxCartCalculateInput, type ShippingTaxMethod, type ShippingTaxQuoteInput, type ShippingTaxQuoteItem, StorefrontQuoteClient, type TaxLine, type TaxLineInput, ThrottleApiError, type UpdateCartInput, type UpdateLineItemInput };
484
+ export { type AddLineItemInput, type AppliedDiscount, type Cart, CartClient, type CartEvent, type CartSession, CartSessionClient, type CartSessionClientOptions, type CheckoutHandoff, type CheckoutHandoffInput, type CheckoutInput, type CreateCartInput, type CreateCartSessionInput, type ExternalShippingTaxSnapshotInput, type LineItem, type Order, type SelectShippingInput, type SelectedShipping, type ShippingTaxAddress, type ShippingTaxCalculationResponse, type ShippingTaxCartCalculateInput, type ShippingTaxMethod, type ShippingTaxQuoteInput, type ShippingTaxQuoteItem, StorefrontQuoteClient, type TaxLine, type TaxLineInput, ThrottleApiError, type UpdateCartInput, type UpdateLineItemInput };
package/dist/index.js CHANGED
@@ -157,8 +157,116 @@ var StorefrontQuoteClient = class {
157
157
  }
158
158
  }
159
159
  };
160
+
161
+ // src/cart-session.ts
162
+ var CartSessionClient = class {
163
+ applicationId;
164
+ environmentId;
165
+ quoteToken;
166
+ baseUrl;
167
+ fetchImpl;
168
+ timeoutMs;
169
+ origin;
170
+ sessionId = null;
171
+ constructor(opts) {
172
+ if (!opts.applicationId) throw new Error("CartSessionClient: applicationId is required");
173
+ if (!opts.environmentId) throw new Error("CartSessionClient: environmentId is required");
174
+ if (!opts.quoteToken?.startsWith("pk_")) {
175
+ throw new Error("CartSessionClient: quoteToken must be a publishable pk_ token");
176
+ }
177
+ this.applicationId = opts.applicationId;
178
+ this.environmentId = opts.environmentId;
179
+ this.quoteToken = opts.quoteToken;
180
+ this.baseUrl = (opts.baseUrl ?? "https://api.usethrottle.dev").replace(/\/+$/, "");
181
+ this.fetchImpl = opts.fetch ?? globalThis.fetch;
182
+ this.timeoutMs = opts.timeoutMs ?? 15e3;
183
+ this.origin = opts.origin;
184
+ }
185
+ /** The current bound session id (after `create()`/`resume()`), or null. */
186
+ get cartSessionId() {
187
+ return this.sessionId;
188
+ }
189
+ /** Bind to an existing session id — e.g. one restored from `localStorage`. */
190
+ resume(cartSessionId) {
191
+ this.sessionId = cartSessionId;
192
+ return this;
193
+ }
194
+ /** Create a new cart session and bind this client to it. */
195
+ async create(input = {}) {
196
+ const data = await this.request("POST", "/api/v1/cart-sessions", {
197
+ applicationId: this.applicationId,
198
+ environmentId: this.environmentId,
199
+ quoteToken: this.quoteToken,
200
+ ...input
201
+ });
202
+ this.sessionId = data.cartSessionId;
203
+ return data;
204
+ }
205
+ /** Read the current session + cart snapshot. */
206
+ get() {
207
+ return this.request("GET", this.basePath());
208
+ }
209
+ async addItem(input) {
210
+ return (await this.request("POST", `${this.basePath()}/items`, input)).cart;
211
+ }
212
+ async updateItem(itemId, input) {
213
+ return (await this.request("PATCH", `${this.basePath()}/items/${encodeURIComponent(itemId)}`, input)).cart;
214
+ }
215
+ async removeItem(itemId) {
216
+ return (await this.request("DELETE", `${this.basePath()}/items/${encodeURIComponent(itemId)}`)).cart;
217
+ }
218
+ async selectShipping(input) {
219
+ return (await this.request("POST", `${this.basePath()}/shipping`, input)).cart;
220
+ }
221
+ async clearShipping() {
222
+ return (await this.request("DELETE", `${this.basePath()}/shipping`)).cart;
223
+ }
224
+ async applyDiscount(code) {
225
+ return (await this.request("POST", `${this.basePath()}/discount`, { code })).cart;
226
+ }
227
+ async removeDiscount() {
228
+ return (await this.request("DELETE", `${this.basePath()}/discount`)).cart;
229
+ }
230
+ /** Hand off to hosted/embedded checkout. Marks the session converted (no more edits). */
231
+ checkout(input) {
232
+ return this.request("POST", `${this.basePath()}/checkout-session`, input);
233
+ }
234
+ basePath() {
235
+ if (!this.sessionId) {
236
+ throw new Error("CartSessionClient: no active session \u2014 call create() or resume(id) first");
237
+ }
238
+ return `/api/v1/cart-sessions/${encodeURIComponent(this.sessionId)}`;
239
+ }
240
+ async request(method, path, body) {
241
+ const ctrl = new AbortController();
242
+ const timer = setTimeout(() => ctrl.abort(), this.timeoutMs);
243
+ try {
244
+ const headers = { "content-type": "application/json" };
245
+ if (this.origin) headers.origin = this.origin;
246
+ const res = await this.fetchImpl(`${this.baseUrl}${path}`, {
247
+ method,
248
+ headers,
249
+ body: body !== void 0 ? JSON.stringify(body) : void 0,
250
+ signal: ctrl.signal
251
+ });
252
+ const json = await res.json().catch(() => ({}));
253
+ if (!res.ok) {
254
+ throw new ThrottleApiError({
255
+ code: json?.error?.code ?? "unknown_error",
256
+ message: json?.error?.message ?? `HTTP ${res.status}`,
257
+ statusCode: res.status,
258
+ details: json?.error?.details
259
+ });
260
+ }
261
+ return json.data;
262
+ } finally {
263
+ clearTimeout(timer);
264
+ }
265
+ }
266
+ };
160
267
  export {
161
268
  CartClient,
269
+ CartSessionClient,
162
270
  StorefrontQuoteClient,
163
271
  ThrottleApiError,
164
272
  ThrottleError2 as ThrottleError,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@usethrottle/cart",
3
- "version": "3.3.0",
3
+ "version": "3.5.0",
4
4
  "description": "Typed REST client for the Throttle Cart API.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",