@usethrottle/cart 3.2.0 → 3.4.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 +70 -7
- package/dist/index.cjs +119 -10
- package/dist/index.d.cts +106 -4
- package/dist/index.d.ts +106 -4
- package/dist/index.js +115 -9
- package/package.json +4 -1
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
|
|
@@ -194,14 +230,41 @@ try {
|
|
|
194
230
|
}
|
|
195
231
|
```
|
|
196
232
|
|
|
233
|
+
`ThrottleApiError` extends the shared **`ThrottleError`** from
|
|
234
|
+
[`@usethrottle/errors`](https://www.npmjs.com/package/@usethrottle/errors),
|
|
235
|
+
re-exported here. If you use more than one Throttle SDK you can catch all of
|
|
236
|
+
their errors with a single check — `instanceof ThrottleApiError` keeps working:
|
|
237
|
+
|
|
238
|
+
```ts
|
|
239
|
+
import { ThrottleError } from '@usethrottle/cart'; // same class across every SDK
|
|
240
|
+
|
|
241
|
+
try {
|
|
242
|
+
await cart.items.add(cartId, item);
|
|
243
|
+
await checkout.completeSession(sessionId, payment);
|
|
244
|
+
} catch (e) {
|
|
245
|
+
if (e instanceof ThrottleError) {
|
|
246
|
+
console.error(e.statusCode, e.code, e.message);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
### Line item `imageUrl`
|
|
252
|
+
|
|
253
|
+
`imageUrl` accepts an absolute `http(s)` URL **or** a relative path like
|
|
254
|
+
`/images/x.png`. Relative paths are resolved to an absolute URL against your
|
|
255
|
+
application's storefront base URL so the image renders on the hosted checkout —
|
|
256
|
+
set it with `PUT /v1/embed-config { "storefrontBaseUrl": "https://yourstore.com" }`
|
|
257
|
+
(the first `allowedOrigin` is used as a fallback). A relative path with no
|
|
258
|
+
resolvable base returns `400 image_url_unresolvable`.
|
|
259
|
+
|
|
197
260
|
## Client options
|
|
198
261
|
|
|
199
|
-
| Option | Default | Description
|
|
200
|
-
| ----------- | ----------------------------- |
|
|
201
|
-
| `apiKey` | — | **Required.** Your Throttle secret key (`sk_…`).
|
|
202
|
-
| `baseUrl` | `https://api.usethrottle.dev` | Optional override for
|
|
203
|
-
| `timeoutMs` | `30000` | Per-request abort timeout in ms.
|
|
204
|
-
| `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`). |
|
|
205
268
|
|
|
206
269
|
## See also
|
|
207
270
|
|
package/dist/index.cjs
CHANGED
|
@@ -21,22 +21,21 @@ 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
|
-
ThrottleApiError: () => ThrottleApiError
|
|
26
|
+
ThrottleApiError: () => ThrottleApiError,
|
|
27
|
+
ThrottleError: () => import_errors2.ThrottleError,
|
|
28
|
+
isThrottleError: () => import_errors2.isThrottleError
|
|
26
29
|
});
|
|
27
30
|
module.exports = __toCommonJS(index_exports);
|
|
28
31
|
|
|
29
32
|
// src/errors.ts
|
|
30
|
-
var
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
details;
|
|
33
|
+
var import_errors = require("@usethrottle/errors");
|
|
34
|
+
var import_errors2 = require("@usethrottle/errors");
|
|
35
|
+
var ThrottleApiError = class extends import_errors.ThrottleError {
|
|
34
36
|
constructor(args) {
|
|
35
|
-
super(args
|
|
37
|
+
super(args);
|
|
36
38
|
this.name = "ThrottleApiError";
|
|
37
|
-
this.code = args.code;
|
|
38
|
-
this.statusCode = args.statusCode;
|
|
39
|
-
this.details = args.details;
|
|
40
39
|
}
|
|
41
40
|
};
|
|
42
41
|
|
|
@@ -189,9 +188,119 @@ var StorefrontQuoteClient = class {
|
|
|
189
188
|
}
|
|
190
189
|
}
|
|
191
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
|
+
};
|
|
192
298
|
// Annotate the CommonJS export names for ESM import in node:
|
|
193
299
|
0 && (module.exports = {
|
|
194
300
|
CartClient,
|
|
301
|
+
CartSessionClient,
|
|
195
302
|
StorefrontQuoteClient,
|
|
196
|
-
ThrottleApiError
|
|
303
|
+
ThrottleApiError,
|
|
304
|
+
ThrottleError,
|
|
305
|
+
isThrottleError
|
|
197
306
|
});
|
package/dist/index.d.cts
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { ThrottleError } from '@usethrottle/errors';
|
|
2
|
+
export { ThrottleError, isThrottleError } from '@usethrottle/errors';
|
|
3
|
+
|
|
1
4
|
interface Cart {
|
|
2
5
|
id: string;
|
|
3
6
|
workspaceId: string;
|
|
@@ -116,6 +119,13 @@ interface AddLineItemInput {
|
|
|
116
119
|
count: number | null;
|
|
117
120
|
} | null;
|
|
118
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;
|
|
119
129
|
metadata?: Record<string, unknown>;
|
|
120
130
|
}
|
|
121
131
|
interface UpdateLineItemInput {
|
|
@@ -359,9 +369,101 @@ declare class StorefrontQuoteClient {
|
|
|
359
369
|
quote(input: ShippingTaxQuoteInput): Promise<ShippingTaxCalculationResponse>;
|
|
360
370
|
}
|
|
361
371
|
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
372
|
+
/**
|
|
373
|
+
* Browser client for backend-less cart ownership. Unlike {@link CartClient}
|
|
374
|
+
* (which needs a server-side `sk_` key), `CartSessionClient` runs in the browser:
|
|
375
|
+
* it creates and mutates a Throttle cart authorized by a publishable quote token
|
|
376
|
+
* (`pk_…`, the same token used by {@link StorefrontQuoteClient}) plus the
|
|
377
|
+
* application origin allowlist, and an opaque `cart_…` session id scoped to that
|
|
378
|
+
* one cart.
|
|
379
|
+
*
|
|
380
|
+
* Typical use:
|
|
381
|
+
* ```ts
|
|
382
|
+
* const cart = new CartSessionClient({ applicationId, environmentId, quoteToken });
|
|
383
|
+
* await cart.create(); // or cart.resume(savedId)
|
|
384
|
+
* await cart.addItem({ name: 'Widget', unitPrice: 2999, quantity: 1 });
|
|
385
|
+
* const { checkoutUrl } = await cart.checkout({ returnUrl, cancelUrl });
|
|
386
|
+
* ```
|
|
387
|
+
*/
|
|
388
|
+
interface CartSessionClientOptions {
|
|
389
|
+
/** Application UUID. */
|
|
390
|
+
applicationId: string;
|
|
391
|
+
/** Workspace environment UUID the publishable token was minted for. */
|
|
392
|
+
environmentId: string;
|
|
393
|
+
/** Publishable storefront quote token (`pk_…`). */
|
|
394
|
+
quoteToken: string;
|
|
395
|
+
baseUrl?: string;
|
|
396
|
+
fetch?: typeof globalThis.fetch;
|
|
397
|
+
timeoutMs?: number;
|
|
398
|
+
/**
|
|
399
|
+
* Origin to send. Browsers set the `Origin` header automatically (and it can't
|
|
400
|
+
* be overridden from JS), so this is only needed for non-browser callers
|
|
401
|
+
* (SSR, tests). It must be on the application's allowlist.
|
|
402
|
+
*/
|
|
403
|
+
origin?: string;
|
|
404
|
+
}
|
|
405
|
+
interface CreateCartSessionInput {
|
|
406
|
+
currency?: string;
|
|
407
|
+
netN?: number;
|
|
408
|
+
metadata?: Record<string, unknown>;
|
|
409
|
+
}
|
|
410
|
+
interface CartSession {
|
|
411
|
+
cartSessionId: string;
|
|
412
|
+
status: string;
|
|
413
|
+
expiresAt: string;
|
|
414
|
+
cart: Cart;
|
|
415
|
+
}
|
|
416
|
+
interface CheckoutHandoffInput {
|
|
417
|
+
returnUrl: string;
|
|
418
|
+
cancelUrl: string;
|
|
419
|
+
customerEmail?: string;
|
|
420
|
+
allowedMethods?: string[];
|
|
421
|
+
}
|
|
422
|
+
interface CheckoutHandoff {
|
|
423
|
+
cartSessionId: string;
|
|
424
|
+
checkoutSessionId: string;
|
|
425
|
+
checkoutUrl: string;
|
|
426
|
+
expiresAt: string;
|
|
427
|
+
}
|
|
428
|
+
declare class CartSessionClient {
|
|
429
|
+
private readonly applicationId;
|
|
430
|
+
private readonly environmentId;
|
|
431
|
+
private readonly quoteToken;
|
|
432
|
+
private readonly baseUrl;
|
|
433
|
+
private readonly fetchImpl;
|
|
434
|
+
private readonly timeoutMs;
|
|
435
|
+
private readonly origin?;
|
|
436
|
+
private sessionId;
|
|
437
|
+
constructor(opts: CartSessionClientOptions);
|
|
438
|
+
/** The current bound session id (after `create()`/`resume()`), or null. */
|
|
439
|
+
get cartSessionId(): string | null;
|
|
440
|
+
/** Bind to an existing session id — e.g. one restored from `localStorage`. */
|
|
441
|
+
resume(cartSessionId: string): this;
|
|
442
|
+
/** Create a new cart session and bind this client to it. */
|
|
443
|
+
create(input?: CreateCartSessionInput): Promise<CartSession>;
|
|
444
|
+
/** Read the current session + cart snapshot. */
|
|
445
|
+
get(): Promise<CartSession>;
|
|
446
|
+
addItem(input: AddLineItemInput): Promise<Cart>;
|
|
447
|
+
updateItem(itemId: string, input: UpdateLineItemInput): Promise<Cart>;
|
|
448
|
+
removeItem(itemId: string): Promise<Cart>;
|
|
449
|
+
selectShipping(input: SelectShippingInput): Promise<Cart>;
|
|
450
|
+
clearShipping(): Promise<Cart>;
|
|
451
|
+
applyDiscount(code: string): Promise<Cart>;
|
|
452
|
+
removeDiscount(): Promise<Cart>;
|
|
453
|
+
/** Hand off to hosted/embedded checkout. Marks the session converted (no more edits). */
|
|
454
|
+
checkout(input: CheckoutHandoffInput): Promise<CheckoutHandoff>;
|
|
455
|
+
private basePath;
|
|
456
|
+
private request;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
/**
|
|
460
|
+
* Thrown on a non-2xx response from the Throttle Cart API.
|
|
461
|
+
*
|
|
462
|
+
* Extends the shared {@link ThrottleError} so a single
|
|
463
|
+
* `catch (e) { if (e instanceof ThrottleError) … }` covers errors from every
|
|
464
|
+
* Throttle SDK. `instanceof ThrottleApiError` keeps working for existing code.
|
|
465
|
+
*/
|
|
466
|
+
declare class ThrottleApiError extends ThrottleError {
|
|
365
467
|
readonly details?: Record<string, unknown>;
|
|
366
468
|
constructor(args: {
|
|
367
469
|
code: string;
|
|
@@ -371,4 +473,4 @@ declare class ThrottleApiError extends Error {
|
|
|
371
473
|
});
|
|
372
474
|
}
|
|
373
475
|
|
|
374
|
-
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 };
|
|
476
|
+
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
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { ThrottleError } from '@usethrottle/errors';
|
|
2
|
+
export { ThrottleError, isThrottleError } from '@usethrottle/errors';
|
|
3
|
+
|
|
1
4
|
interface Cart {
|
|
2
5
|
id: string;
|
|
3
6
|
workspaceId: string;
|
|
@@ -116,6 +119,13 @@ interface AddLineItemInput {
|
|
|
116
119
|
count: number | null;
|
|
117
120
|
} | null;
|
|
118
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;
|
|
119
129
|
metadata?: Record<string, unknown>;
|
|
120
130
|
}
|
|
121
131
|
interface UpdateLineItemInput {
|
|
@@ -359,9 +369,101 @@ declare class StorefrontQuoteClient {
|
|
|
359
369
|
quote(input: ShippingTaxQuoteInput): Promise<ShippingTaxCalculationResponse>;
|
|
360
370
|
}
|
|
361
371
|
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
372
|
+
/**
|
|
373
|
+
* Browser client for backend-less cart ownership. Unlike {@link CartClient}
|
|
374
|
+
* (which needs a server-side `sk_` key), `CartSessionClient` runs in the browser:
|
|
375
|
+
* it creates and mutates a Throttle cart authorized by a publishable quote token
|
|
376
|
+
* (`pk_…`, the same token used by {@link StorefrontQuoteClient}) plus the
|
|
377
|
+
* application origin allowlist, and an opaque `cart_…` session id scoped to that
|
|
378
|
+
* one cart.
|
|
379
|
+
*
|
|
380
|
+
* Typical use:
|
|
381
|
+
* ```ts
|
|
382
|
+
* const cart = new CartSessionClient({ applicationId, environmentId, quoteToken });
|
|
383
|
+
* await cart.create(); // or cart.resume(savedId)
|
|
384
|
+
* await cart.addItem({ name: 'Widget', unitPrice: 2999, quantity: 1 });
|
|
385
|
+
* const { checkoutUrl } = await cart.checkout({ returnUrl, cancelUrl });
|
|
386
|
+
* ```
|
|
387
|
+
*/
|
|
388
|
+
interface CartSessionClientOptions {
|
|
389
|
+
/** Application UUID. */
|
|
390
|
+
applicationId: string;
|
|
391
|
+
/** Workspace environment UUID the publishable token was minted for. */
|
|
392
|
+
environmentId: string;
|
|
393
|
+
/** Publishable storefront quote token (`pk_…`). */
|
|
394
|
+
quoteToken: string;
|
|
395
|
+
baseUrl?: string;
|
|
396
|
+
fetch?: typeof globalThis.fetch;
|
|
397
|
+
timeoutMs?: number;
|
|
398
|
+
/**
|
|
399
|
+
* Origin to send. Browsers set the `Origin` header automatically (and it can't
|
|
400
|
+
* be overridden from JS), so this is only needed for non-browser callers
|
|
401
|
+
* (SSR, tests). It must be on the application's allowlist.
|
|
402
|
+
*/
|
|
403
|
+
origin?: string;
|
|
404
|
+
}
|
|
405
|
+
interface CreateCartSessionInput {
|
|
406
|
+
currency?: string;
|
|
407
|
+
netN?: number;
|
|
408
|
+
metadata?: Record<string, unknown>;
|
|
409
|
+
}
|
|
410
|
+
interface CartSession {
|
|
411
|
+
cartSessionId: string;
|
|
412
|
+
status: string;
|
|
413
|
+
expiresAt: string;
|
|
414
|
+
cart: Cart;
|
|
415
|
+
}
|
|
416
|
+
interface CheckoutHandoffInput {
|
|
417
|
+
returnUrl: string;
|
|
418
|
+
cancelUrl: string;
|
|
419
|
+
customerEmail?: string;
|
|
420
|
+
allowedMethods?: string[];
|
|
421
|
+
}
|
|
422
|
+
interface CheckoutHandoff {
|
|
423
|
+
cartSessionId: string;
|
|
424
|
+
checkoutSessionId: string;
|
|
425
|
+
checkoutUrl: string;
|
|
426
|
+
expiresAt: string;
|
|
427
|
+
}
|
|
428
|
+
declare class CartSessionClient {
|
|
429
|
+
private readonly applicationId;
|
|
430
|
+
private readonly environmentId;
|
|
431
|
+
private readonly quoteToken;
|
|
432
|
+
private readonly baseUrl;
|
|
433
|
+
private readonly fetchImpl;
|
|
434
|
+
private readonly timeoutMs;
|
|
435
|
+
private readonly origin?;
|
|
436
|
+
private sessionId;
|
|
437
|
+
constructor(opts: CartSessionClientOptions);
|
|
438
|
+
/** The current bound session id (after `create()`/`resume()`), or null. */
|
|
439
|
+
get cartSessionId(): string | null;
|
|
440
|
+
/** Bind to an existing session id — e.g. one restored from `localStorage`. */
|
|
441
|
+
resume(cartSessionId: string): this;
|
|
442
|
+
/** Create a new cart session and bind this client to it. */
|
|
443
|
+
create(input?: CreateCartSessionInput): Promise<CartSession>;
|
|
444
|
+
/** Read the current session + cart snapshot. */
|
|
445
|
+
get(): Promise<CartSession>;
|
|
446
|
+
addItem(input: AddLineItemInput): Promise<Cart>;
|
|
447
|
+
updateItem(itemId: string, input: UpdateLineItemInput): Promise<Cart>;
|
|
448
|
+
removeItem(itemId: string): Promise<Cart>;
|
|
449
|
+
selectShipping(input: SelectShippingInput): Promise<Cart>;
|
|
450
|
+
clearShipping(): Promise<Cart>;
|
|
451
|
+
applyDiscount(code: string): Promise<Cart>;
|
|
452
|
+
removeDiscount(): Promise<Cart>;
|
|
453
|
+
/** Hand off to hosted/embedded checkout. Marks the session converted (no more edits). */
|
|
454
|
+
checkout(input: CheckoutHandoffInput): Promise<CheckoutHandoff>;
|
|
455
|
+
private basePath;
|
|
456
|
+
private request;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
/**
|
|
460
|
+
* Thrown on a non-2xx response from the Throttle Cart API.
|
|
461
|
+
*
|
|
462
|
+
* Extends the shared {@link ThrottleError} so a single
|
|
463
|
+
* `catch (e) { if (e instanceof ThrottleError) … }` covers errors from every
|
|
464
|
+
* Throttle SDK. `instanceof ThrottleApiError` keeps working for existing code.
|
|
465
|
+
*/
|
|
466
|
+
declare class ThrottleApiError extends ThrottleError {
|
|
365
467
|
readonly details?: Record<string, unknown>;
|
|
366
468
|
constructor(args: {
|
|
367
469
|
code: string;
|
|
@@ -371,4 +473,4 @@ declare class ThrottleApiError extends Error {
|
|
|
371
473
|
});
|
|
372
474
|
}
|
|
373
475
|
|
|
374
|
-
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 };
|
|
476
|
+
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
|
@@ -1,14 +1,10 @@
|
|
|
1
1
|
// src/errors.ts
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
details;
|
|
2
|
+
import { ThrottleError } from "@usethrottle/errors";
|
|
3
|
+
import { ThrottleError as ThrottleError2, isThrottleError } from "@usethrottle/errors";
|
|
4
|
+
var ThrottleApiError = class extends ThrottleError {
|
|
6
5
|
constructor(args) {
|
|
7
|
-
super(args
|
|
6
|
+
super(args);
|
|
8
7
|
this.name = "ThrottleApiError";
|
|
9
|
-
this.code = args.code;
|
|
10
|
-
this.statusCode = args.statusCode;
|
|
11
|
-
this.details = args.details;
|
|
12
8
|
}
|
|
13
9
|
};
|
|
14
10
|
|
|
@@ -161,8 +157,118 @@ var StorefrontQuoteClient = class {
|
|
|
161
157
|
}
|
|
162
158
|
}
|
|
163
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
|
+
};
|
|
164
267
|
export {
|
|
165
268
|
CartClient,
|
|
269
|
+
CartSessionClient,
|
|
166
270
|
StorefrontQuoteClient,
|
|
167
|
-
ThrottleApiError
|
|
271
|
+
ThrottleApiError,
|
|
272
|
+
ThrottleError2 as ThrottleError,
|
|
273
|
+
isThrottleError
|
|
168
274
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@usethrottle/cart",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.4.0",
|
|
4
4
|
"description": "Typed REST client for the Throttle Cart API.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.cjs",
|
|
@@ -17,6 +17,9 @@
|
|
|
17
17
|
"dist",
|
|
18
18
|
"README.md"
|
|
19
19
|
],
|
|
20
|
+
"dependencies": {
|
|
21
|
+
"@usethrottle/errors": "^1.0.0"
|
|
22
|
+
},
|
|
20
23
|
"devDependencies": {
|
|
21
24
|
"tsup": "^8.0.0",
|
|
22
25
|
"vitest": "^2.1.9",
|