@usethrottle/cart 3.7.0 → 3.9.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 +51 -5
- package/dist/index.cjs +92 -13
- package/dist/index.d.cts +151 -9
- package/dist/index.d.ts +151 -9
- package/dist/index.js +91 -13
- package/package.json +8 -8
package/README.md
CHANGED
|
@@ -88,15 +88,58 @@ await cart.addItem({ name: 'Premium Widget', unitPrice: 2999, quantity: 2 });
|
|
|
88
88
|
await cart.applyDiscount('SAVE10');
|
|
89
89
|
await cart.selectShipping({ methodId: 'fedex_ground', displayName: 'FedEx Ground', rateAmount: 599 });
|
|
90
90
|
|
|
91
|
+
// Set buyer-editable fields. `customerEmail` is what makes a guest cart
|
|
92
|
+
// visible to abandoned-cart recovery. `null` clears a field.
|
|
93
|
+
await cart.update({ customerEmail: 'buyer@example.com', notes: 'Leave at the door' });
|
|
94
|
+
|
|
91
95
|
// Hand off to hosted/embedded checkout and redirect the buyer.
|
|
92
96
|
const { checkoutUrl } = await cart.checkout({
|
|
93
97
|
returnUrl: 'https://shop.example.com/thanks',
|
|
94
98
|
cancelUrl: 'https://shop.example.com/cart',
|
|
99
|
+
// Leave the session `active` instead of `converted`, so a buyer who backs
|
|
100
|
+
// out of checkout returns to a live cart instead of an expired one.
|
|
101
|
+
keepOpen: true,
|
|
95
102
|
});
|
|
96
103
|
```
|
|
97
104
|
|
|
105
|
+
`create()` and `get()` also return a `branding` block (`CartBranding`:
|
|
106
|
+
`logoUrl`, `primaryColor`, `merchantName`, each nullable) so a storefront
|
|
107
|
+
rendering its own cart UI doesn't need a separate call to brand it.
|
|
108
|
+
|
|
109
|
+
To bind to an existing session without a publishable token — e.g. Throttle's
|
|
110
|
+
own hosted cart page, or a storefront restoring a saved session id — use
|
|
111
|
+
`CartSessionClient.forSession()` instead of `new CartSessionClient()`:
|
|
112
|
+
|
|
113
|
+
```ts
|
|
114
|
+
const cart = CartSessionClient.forSession(savedCartSessionId);
|
|
115
|
+
await cart.addItem({ name: 'Premium Widget', unitPrice: 2999, quantity: 1 });
|
|
116
|
+
```
|
|
117
|
+
|
|
98
118
|
The opaque `cart_…` session id is scoped to exactly one cart. See the
|
|
99
|
-
[cart sessions guide](https://
|
|
119
|
+
[cart sessions guide](https://usethrottle.dev/docs/developers/cart-sessions).
|
|
120
|
+
|
|
121
|
+
### Hosted cart page
|
|
122
|
+
|
|
123
|
+
Skip building cart UI entirely and send the buyer to Throttle's own hosted
|
|
124
|
+
cart page with `hostedCartUrl()`:
|
|
125
|
+
|
|
126
|
+
```ts
|
|
127
|
+
const session = await cart.create({
|
|
128
|
+
metadata: {
|
|
129
|
+
returnUrl: 'https://shop.example.com/thanks',
|
|
130
|
+
cancelUrl: 'https://shop.example.com/cart',
|
|
131
|
+
storefrontUrl: 'https://shop.example.com',
|
|
132
|
+
},
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
location.href = cart.hostedCartUrl(); // https://checkout.usethrottle.dev/cart/cart_...
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
`returnUrl`, `cancelUrl`, and `storefrontUrl` in `metadata` are all optional.
|
|
139
|
+
Omitting `storefrontUrl` just removes the hosted page's "Continue shopping"
|
|
140
|
+
button — checkout still works. Omitting `returnUrl` or `cancelUrl` falls back
|
|
141
|
+
to Throttle's own `/complete` page and this same hosted cart page,
|
|
142
|
+
respectively.
|
|
100
143
|
|
|
101
144
|
## Net-N invoice terms
|
|
102
145
|
|
|
@@ -179,7 +222,7 @@ await client.shippingTax.pushExternalSnapshot({
|
|
|
179
222
|
|
|
180
223
|
## Subscriptions
|
|
181
224
|
|
|
182
|
-
|
|
225
|
+
Mark a line item with `type: 'subscription'`:
|
|
183
226
|
|
|
184
227
|
```ts
|
|
185
228
|
await client.items.add(cart.id, {
|
|
@@ -187,12 +230,15 @@ await client.items.add(cart.id, {
|
|
|
187
230
|
name: 'Pro Plan',
|
|
188
231
|
unitPrice: 1900,
|
|
189
232
|
quantity: 1,
|
|
190
|
-
recurring: { interval: 'month', count: null }, // count: null = until cancelled
|
|
191
233
|
});
|
|
192
234
|
```
|
|
193
235
|
|
|
194
|
-
Then call `client.carts.checkout` as normal.
|
|
195
|
-
|
|
236
|
+
Then call `client.carts.checkout` as normal.
|
|
237
|
+
|
|
238
|
+
The billing schedule is **not** settable through this endpoint. The add-item
|
|
239
|
+
body is strict and has no `recurring` field, so sending one is rejected with
|
|
240
|
+
`Unsupported field(s): recurring` — the recurring plan is attached by the
|
|
241
|
+
subscription engine, not by the cart API.
|
|
196
242
|
|
|
197
243
|
## Events
|
|
198
244
|
|
package/dist/index.cjs
CHANGED
|
@@ -24,6 +24,7 @@ __export(index_exports, {
|
|
|
24
24
|
CartNotFoundError: () => CartNotFoundError,
|
|
25
25
|
CartNotOpenError: () => CartNotOpenError,
|
|
26
26
|
CartSessionClient: () => CartSessionClient,
|
|
27
|
+
CartTimeoutError: () => CartTimeoutError,
|
|
27
28
|
StorefrontQuoteClient: () => StorefrontQuoteClient,
|
|
28
29
|
ThrottleApiError: () => ThrottleApiError,
|
|
29
30
|
ThrottleError: () => import_errors2.ThrottleError,
|
|
@@ -53,6 +54,17 @@ var CartNotFoundError = class extends ThrottleApiError {
|
|
|
53
54
|
this.name = "CartNotFoundError";
|
|
54
55
|
}
|
|
55
56
|
};
|
|
57
|
+
var CartTimeoutError = class extends import_errors.ThrottleError {
|
|
58
|
+
constructor(args) {
|
|
59
|
+
super({
|
|
60
|
+
code: "request_timeout",
|
|
61
|
+
statusCode: 408,
|
|
62
|
+
message: `Request timed out after ${args.timeoutMs}ms`,
|
|
63
|
+
details: { timeoutMs: args.timeoutMs, method: args.method, path: args.path }
|
|
64
|
+
});
|
|
65
|
+
this.name = "CartTimeoutError";
|
|
66
|
+
}
|
|
67
|
+
};
|
|
56
68
|
function cartErrorFromResponse(args) {
|
|
57
69
|
if (args.statusCode === 409 && args.code === "cart_not_open") {
|
|
58
70
|
return new CartNotOpenError({ message: args.message, statusCode: args.statusCode, details: args.details });
|
|
@@ -76,7 +88,7 @@ var CartClient = class {
|
|
|
76
88
|
if (!opts.apiKey) throw new Error("CartClient: apiKey is required");
|
|
77
89
|
this.apiKey = opts.apiKey;
|
|
78
90
|
this.baseUrl = (opts.baseUrl ?? "https://api.usethrottle.dev").replace(/\/+$/, "");
|
|
79
|
-
this.fetchImpl = opts.fetch ?? globalThis.fetch;
|
|
91
|
+
this.fetchImpl = opts.fetch ?? globalThis.fetch.bind(globalThis);
|
|
80
92
|
this.timeoutMs = opts.timeoutMs ?? 3e4;
|
|
81
93
|
}
|
|
82
94
|
async request(method, path, body) {
|
|
@@ -171,13 +183,13 @@ var StorefrontQuoteClient = class {
|
|
|
171
183
|
origin;
|
|
172
184
|
constructor(opts) {
|
|
173
185
|
if (!opts.applicationId) throw new Error("StorefrontQuoteClient: applicationId is required");
|
|
174
|
-
if (
|
|
175
|
-
throw new Error("StorefrontQuoteClient: quoteToken must be a publishable pk_ token");
|
|
186
|
+
if (!/^(pk|qt)_/.test(opts.quoteToken ?? "")) {
|
|
187
|
+
throw new Error("StorefrontQuoteClient: quoteToken must be a publishable qt_ (or legacy pk_) token");
|
|
176
188
|
}
|
|
177
189
|
this.applicationId = opts.applicationId;
|
|
178
190
|
this.quoteToken = opts.quoteToken;
|
|
179
191
|
this.baseUrl = (opts.baseUrl ?? "https://api.usethrottle.dev").replace(/\/+$/, "");
|
|
180
|
-
this.fetchImpl = opts.fetch ?? globalThis.fetch;
|
|
192
|
+
this.fetchImpl = opts.fetch ?? globalThis.fetch.bind(globalThis);
|
|
181
193
|
this.timeoutMs = opts.timeoutMs ?? 15e3;
|
|
182
194
|
this.origin = opts.origin;
|
|
183
195
|
}
|
|
@@ -214,7 +226,7 @@ var StorefrontQuoteClient = class {
|
|
|
214
226
|
};
|
|
215
227
|
|
|
216
228
|
// src/cart-session.ts
|
|
217
|
-
var CartSessionClient = class {
|
|
229
|
+
var CartSessionClient = class _CartSessionClient {
|
|
218
230
|
applicationId;
|
|
219
231
|
environmentId;
|
|
220
232
|
quoteToken;
|
|
@@ -223,17 +235,18 @@ var CartSessionClient = class {
|
|
|
223
235
|
timeoutMs;
|
|
224
236
|
origin;
|
|
225
237
|
sessionId = null;
|
|
238
|
+
isForSessionClient = false;
|
|
226
239
|
constructor(opts) {
|
|
227
240
|
if (!opts.applicationId) throw new Error("CartSessionClient: applicationId is required");
|
|
228
241
|
if (!opts.environmentId) throw new Error("CartSessionClient: environmentId is required");
|
|
229
|
-
if (
|
|
230
|
-
throw new Error("CartSessionClient: quoteToken must be a publishable pk_ token");
|
|
242
|
+
if (!/^(pk|qt)_/.test(opts.quoteToken ?? "")) {
|
|
243
|
+
throw new Error("CartSessionClient: quoteToken must be a publishable qt_ (or legacy pk_) token");
|
|
231
244
|
}
|
|
232
245
|
this.applicationId = opts.applicationId;
|
|
233
246
|
this.environmentId = opts.environmentId;
|
|
234
247
|
this.quoteToken = opts.quoteToken;
|
|
235
248
|
this.baseUrl = (opts.baseUrl ?? "https://api.usethrottle.dev").replace(/\/+$/, "");
|
|
236
|
-
this.fetchImpl = opts.fetch ?? globalThis.fetch;
|
|
249
|
+
this.fetchImpl = opts.fetch ?? globalThis.fetch.bind(globalThis);
|
|
237
250
|
this.timeoutMs = opts.timeoutMs ?? 15e3;
|
|
238
251
|
this.origin = opts.origin;
|
|
239
252
|
}
|
|
@@ -248,6 +261,11 @@ var CartSessionClient = class {
|
|
|
248
261
|
}
|
|
249
262
|
/** Create a new cart session and bind this client to it. */
|
|
250
263
|
async create(input = {}) {
|
|
264
|
+
if (this.isForSessionClient) {
|
|
265
|
+
throw new Error(
|
|
266
|
+
"CartSessionClient: this client was created with forSession() and cannot call create(). Construct a CartSessionClient with a real pk_ publishable token instead."
|
|
267
|
+
);
|
|
268
|
+
}
|
|
251
269
|
const data = await this.request("POST", "/api/v1/cart-sessions", {
|
|
252
270
|
applicationId: this.applicationId,
|
|
253
271
|
environmentId: this.environmentId,
|
|
@@ -255,17 +273,38 @@ var CartSessionClient = class {
|
|
|
255
273
|
...input
|
|
256
274
|
});
|
|
257
275
|
this.sessionId = data.cartSessionId;
|
|
258
|
-
return
|
|
276
|
+
return {
|
|
277
|
+
...data,
|
|
278
|
+
branding: data.branding ?? { logoUrl: null, primaryColor: null, merchantName: null }
|
|
279
|
+
};
|
|
259
280
|
}
|
|
260
281
|
/** Read the current session + cart snapshot. */
|
|
261
|
-
get() {
|
|
262
|
-
|
|
282
|
+
async get() {
|
|
283
|
+
const data = await this.request("GET", this.basePath());
|
|
284
|
+
return {
|
|
285
|
+
...data,
|
|
286
|
+
branding: data.branding ?? { logoUrl: null, primaryColor: null, merchantName: null }
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
/**
|
|
290
|
+
* Unit count + total for a cart badge, without pulling the whole cart.
|
|
291
|
+
*
|
|
292
|
+
* Works on a client built with {@link CartSessionClient.forSession} — a
|
|
293
|
+
* storefront that restored a `cart_…` id from `localStorage` and holds no
|
|
294
|
+
* publishable token in the browser.
|
|
295
|
+
*/
|
|
296
|
+
async summary() {
|
|
297
|
+
return this.request("GET", `${this.basePath()}/summary`);
|
|
263
298
|
}
|
|
264
299
|
async addItem(input) {
|
|
265
300
|
return (await this.request("POST", `${this.basePath()}/items`, input)).cart;
|
|
266
301
|
}
|
|
267
302
|
async updateItem(itemId, input) {
|
|
268
|
-
return (await this.request(
|
|
303
|
+
return (await this.request(
|
|
304
|
+
"PATCH",
|
|
305
|
+
`${this.basePath()}/items/${encodeURIComponent(itemId)}`,
|
|
306
|
+
input
|
|
307
|
+
)).cart;
|
|
269
308
|
}
|
|
270
309
|
async removeItem(itemId) {
|
|
271
310
|
return (await this.request("DELETE", `${this.basePath()}/items/${encodeURIComponent(itemId)}`)).cart;
|
|
@@ -282,6 +321,38 @@ var CartSessionClient = class {
|
|
|
282
321
|
async removeDiscount() {
|
|
283
322
|
return (await this.request("DELETE", `${this.basePath()}/discount`)).cart;
|
|
284
323
|
}
|
|
324
|
+
/** Set buyer-editable cart fields. Pass `null` to clear one. */
|
|
325
|
+
async update(input) {
|
|
326
|
+
return (await this.request("PATCH", this.basePath(), input)).cart;
|
|
327
|
+
}
|
|
328
|
+
/**
|
|
329
|
+
* Bind to an existing session without a publishable token.
|
|
330
|
+
*
|
|
331
|
+
* `create()` needs a `pk_` token; every other route is authorized by the
|
|
332
|
+
* opaque `cart_…` id alone. A caller that only ever resumes — Throttle's own
|
|
333
|
+
* hosted cart page, or a storefront restoring from localStorage — would
|
|
334
|
+
* otherwise have to invent a token to satisfy the constructor.
|
|
335
|
+
*/
|
|
336
|
+
static forSession(cartSessionId, opts = {}) {
|
|
337
|
+
const client = new _CartSessionClient({
|
|
338
|
+
applicationId: "session",
|
|
339
|
+
environmentId: "session",
|
|
340
|
+
quoteToken: "pk_session",
|
|
341
|
+
...opts
|
|
342
|
+
});
|
|
343
|
+
client.isForSessionClient = true;
|
|
344
|
+
return client.resume(cartSessionId);
|
|
345
|
+
}
|
|
346
|
+
/**
|
|
347
|
+
* The Throttle-hosted cart page for this session. Send a buyer here to let
|
|
348
|
+
* Throttle run the cart UI instead of building one.
|
|
349
|
+
*/
|
|
350
|
+
hostedCartUrl(baseUrl = "https://checkout.usethrottle.dev") {
|
|
351
|
+
if (!this.sessionId) {
|
|
352
|
+
throw new Error("CartSessionClient: no active session \u2014 call create() or resume(id) first");
|
|
353
|
+
}
|
|
354
|
+
return `${baseUrl.replace(/\/+$/, "")}/cart/${encodeURIComponent(this.sessionId)}`;
|
|
355
|
+
}
|
|
285
356
|
/** Hand off to hosted/embedded checkout. Marks the session converted (no more edits). */
|
|
286
357
|
checkout(input) {
|
|
287
358
|
return this.request("POST", `${this.basePath()}/checkout-session`, input);
|
|
@@ -294,7 +365,11 @@ var CartSessionClient = class {
|
|
|
294
365
|
}
|
|
295
366
|
async request(method, path, body) {
|
|
296
367
|
const ctrl = new AbortController();
|
|
297
|
-
|
|
368
|
+
let timedOut = false;
|
|
369
|
+
const timer = setTimeout(() => {
|
|
370
|
+
timedOut = true;
|
|
371
|
+
ctrl.abort();
|
|
372
|
+
}, this.timeoutMs);
|
|
298
373
|
try {
|
|
299
374
|
const headers = { "content-type": "application/json" };
|
|
300
375
|
if (this.origin) headers.origin = this.origin;
|
|
@@ -314,6 +389,9 @@ var CartSessionClient = class {
|
|
|
314
389
|
});
|
|
315
390
|
}
|
|
316
391
|
return json.data;
|
|
392
|
+
} catch (err) {
|
|
393
|
+
if (timedOut) throw new CartTimeoutError({ timeoutMs: this.timeoutMs, method, path });
|
|
394
|
+
throw err;
|
|
317
395
|
} finally {
|
|
318
396
|
clearTimeout(timer);
|
|
319
397
|
}
|
|
@@ -325,6 +403,7 @@ var CartSessionClient = class {
|
|
|
325
403
|
CartNotFoundError,
|
|
326
404
|
CartNotOpenError,
|
|
327
405
|
CartSessionClient,
|
|
406
|
+
CartTimeoutError,
|
|
328
407
|
StorefrontQuoteClient,
|
|
329
408
|
ThrottleApiError,
|
|
330
409
|
ThrottleError,
|
package/dist/index.d.cts
CHANGED
|
@@ -11,6 +11,8 @@ export { ThrottleError, isThrottleError } from '@usethrottle/errors';
|
|
|
11
11
|
interface CartAddress {
|
|
12
12
|
firstName?: string;
|
|
13
13
|
lastName?: string;
|
|
14
|
+
/** Business name. Optional — B2B addresses use it, B2C addresses omit it. */
|
|
15
|
+
company?: string;
|
|
14
16
|
addressLine1: string;
|
|
15
17
|
addressLine2?: string;
|
|
16
18
|
city: string;
|
|
@@ -22,8 +24,14 @@ interface CartAddress {
|
|
|
22
24
|
email?: string;
|
|
23
25
|
}
|
|
24
26
|
interface Cart {
|
|
27
|
+
/**
|
|
28
|
+
* `getById`/`create`/etc. (the `carts` resource) return this shape. See
|
|
29
|
+
* {@link CartSessionCart} for the browser-facing `CartSessionClient` shape,
|
|
30
|
+
* which omits `workspaceId` and adds `cartId`.
|
|
31
|
+
*/
|
|
25
32
|
id: string;
|
|
26
33
|
workspaceId: string;
|
|
34
|
+
cartId?: string;
|
|
27
35
|
applicationId: string;
|
|
28
36
|
customerId: string | null;
|
|
29
37
|
/** Lightweight captured email (no full customer record), or null. */
|
|
@@ -39,6 +47,7 @@ interface Cart {
|
|
|
39
47
|
billingAddress: CartAddress | null;
|
|
40
48
|
shippingAddress: CartAddress | null;
|
|
41
49
|
notes: string | null;
|
|
50
|
+
metadata: Record<string, unknown>;
|
|
42
51
|
expiresAt: string | null;
|
|
43
52
|
externalRef?: {
|
|
44
53
|
connectionId: string;
|
|
@@ -52,6 +61,24 @@ interface Cart {
|
|
|
52
61
|
createdAt: string;
|
|
53
62
|
updatedAt: string;
|
|
54
63
|
}
|
|
64
|
+
/**
|
|
65
|
+
* The cart as the session (browser, opaque-token) surface returns it —
|
|
66
|
+
* `CartSessionClient`'s methods. There is no `workspaceId` on this surface
|
|
67
|
+
* (the browser never needs, and shouldn't see, the internal workspace id);
|
|
68
|
+
* `cartId` — the same value as `id` — is always present, kept for backward
|
|
69
|
+
* compatibility with `@usethrottle/cart`'s original (order-snapshot-shaped)
|
|
70
|
+
* cart-session response.
|
|
71
|
+
*/
|
|
72
|
+
interface CartSessionCart extends Omit<Cart, 'workspaceId'> {
|
|
73
|
+
cartId: string;
|
|
74
|
+
/**
|
|
75
|
+
* Sum of the line-item quantities — 1 stand + 2 notebooks + 1 keyboard is
|
|
76
|
+
* `4`, not `3`. Present only on this surface; the merchant `carts` resource
|
|
77
|
+
* does not return it. Named for what it counts because `itemCount` in the
|
|
78
|
+
* `cart.abandoned` webhook payload means the number of distinct lines.
|
|
79
|
+
*/
|
|
80
|
+
unitCount: number;
|
|
81
|
+
}
|
|
55
82
|
interface LineItem {
|
|
56
83
|
id: string;
|
|
57
84
|
cartId: string;
|
|
@@ -98,6 +125,12 @@ interface TaxLine {
|
|
|
98
125
|
jurisdictionCode: string;
|
|
99
126
|
jurisdictionName: string | null;
|
|
100
127
|
taxType: 'sales' | 'vat' | 'gst' | 'pst' | 'hst' | 'service' | 'excise' | 'other';
|
|
128
|
+
/** The VAT code the engine returned, verbatim — AvaTax's `vatCode`. */
|
|
129
|
+
vatCode?: string | null;
|
|
130
|
+
/** The engine's own name for the tax ('Standard'); `taxType` is our slug for it. */
|
|
131
|
+
taxName?: string | null;
|
|
132
|
+
/** Engine rate classification: 'S' standard, 'R' reduced, 'Z' zero. */
|
|
133
|
+
rateTypeCode?: string | null;
|
|
101
134
|
rate: number;
|
|
102
135
|
amount: number;
|
|
103
136
|
currency: string;
|
|
@@ -307,6 +340,20 @@ interface ShippingTaxCalculationResponse {
|
|
|
307
340
|
taxTotal: number;
|
|
308
341
|
total: number;
|
|
309
342
|
};
|
|
343
|
+
/**
|
|
344
|
+
* Verbatim messaging the tax engine returned about this transaction. In
|
|
345
|
+
* several EU member states the engine's own wording is what makes the
|
|
346
|
+
* invoice compliant, so it is carried through rather than paraphrased.
|
|
347
|
+
*/
|
|
348
|
+
providerMessages?: Array<{
|
|
349
|
+
summary: string;
|
|
350
|
+
details?: string;
|
|
351
|
+
/** The engine's identifier for the citation, e.g. 'Article138.1'. */
|
|
352
|
+
citationCode?: string;
|
|
353
|
+
refersTo?: string;
|
|
354
|
+
severity?: string;
|
|
355
|
+
source?: string;
|
|
356
|
+
}>;
|
|
310
357
|
prompts: Array<{
|
|
311
358
|
code: 'missing_info';
|
|
312
359
|
field: string;
|
|
@@ -441,17 +488,63 @@ interface CreateCartSessionInput {
|
|
|
441
488
|
netN?: number;
|
|
442
489
|
metadata?: Record<string, unknown>;
|
|
443
490
|
}
|
|
491
|
+
interface CartBranding {
|
|
492
|
+
logoUrl: string | null;
|
|
493
|
+
primaryColor: string | null;
|
|
494
|
+
merchantName: string | null;
|
|
495
|
+
}
|
|
496
|
+
/** Buyer-editable cart fields. `null` clears a value. */
|
|
497
|
+
interface UpdateCartSessionInput {
|
|
498
|
+
customerEmail?: string | null;
|
|
499
|
+
notes?: string | null;
|
|
500
|
+
}
|
|
444
501
|
interface CartSession {
|
|
445
502
|
cartSessionId: string;
|
|
446
503
|
status: string;
|
|
447
504
|
expiresAt: string;
|
|
448
|
-
|
|
505
|
+
branding: CartBranding;
|
|
506
|
+
/**
|
|
507
|
+
* Whatever the storefront passed to `create()`. Write-once — unlike the copy
|
|
508
|
+
* on `cart.metadata`, which the merchant API can change — so this is the one
|
|
509
|
+
* to read the buyer's return/cancel/storefront URLs from.
|
|
510
|
+
*/
|
|
511
|
+
metadata: Record<string, unknown>;
|
|
512
|
+
cart: CartSessionCart;
|
|
513
|
+
}
|
|
514
|
+
/**
|
|
515
|
+
* The badge-sized read: what a storefront header needs to draw a cart count,
|
|
516
|
+
* and nothing more. No line items, no addresses, no buyer email or notes.
|
|
517
|
+
*/
|
|
518
|
+
interface CartSessionSummary {
|
|
519
|
+
cartSessionId: string;
|
|
520
|
+
/**
|
|
521
|
+
* `active` while the cart can still be edited. `expired` (past its TTL) or
|
|
522
|
+
* `converted` (handed off to checkout) once the session is finished — those
|
|
523
|
+
* come back as a normal 200 with a zeroed summary, not an error, so a badge
|
|
524
|
+
* can render a zero without a try/catch.
|
|
525
|
+
*/
|
|
526
|
+
status: 'active' | 'expired' | 'converted';
|
|
527
|
+
/**
|
|
528
|
+
* Sum of the line-item quantities — 1 stand + 2 notebooks + 1 keyboard is
|
|
529
|
+
* `4`, not `3`. Named for what it counts because `itemCount` elsewhere in
|
|
530
|
+
* Throttle (the `cart.abandoned` payload) means the number of distinct lines.
|
|
531
|
+
*/
|
|
532
|
+
unitCount: number;
|
|
533
|
+
/** Cart total in minor units, or `0` on a finished session. */
|
|
534
|
+
total: number;
|
|
535
|
+
/** `null` on a finished session — there is no live cart to price. */
|
|
536
|
+
currency: string | null;
|
|
449
537
|
}
|
|
450
538
|
interface CheckoutHandoffInput {
|
|
451
539
|
returnUrl: string;
|
|
452
540
|
cancelUrl: string;
|
|
453
541
|
customerEmail?: string;
|
|
454
542
|
allowedMethods?: string[];
|
|
543
|
+
/**
|
|
544
|
+
* Leave the cart session open after handing off, so a buyer who backs out of
|
|
545
|
+
* checkout returns to a live cart. Throttle's own hosted cart page sets this.
|
|
546
|
+
*/
|
|
547
|
+
keepOpen?: boolean;
|
|
455
548
|
}
|
|
456
549
|
interface CheckoutHandoff {
|
|
457
550
|
cartSessionId: string;
|
|
@@ -468,6 +561,7 @@ declare class CartSessionClient {
|
|
|
468
561
|
private readonly timeoutMs;
|
|
469
562
|
private readonly origin?;
|
|
470
563
|
private sessionId;
|
|
564
|
+
private isForSessionClient;
|
|
471
565
|
constructor(opts: CartSessionClientOptions);
|
|
472
566
|
/** The current bound session id (after `create()`/`resume()`), or null. */
|
|
473
567
|
get cartSessionId(): string | null;
|
|
@@ -477,13 +571,42 @@ declare class CartSessionClient {
|
|
|
477
571
|
create(input?: CreateCartSessionInput): Promise<CartSession>;
|
|
478
572
|
/** Read the current session + cart snapshot. */
|
|
479
573
|
get(): Promise<CartSession>;
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
574
|
+
/**
|
|
575
|
+
* Unit count + total for a cart badge, without pulling the whole cart.
|
|
576
|
+
*
|
|
577
|
+
* Works on a client built with {@link CartSessionClient.forSession} — a
|
|
578
|
+
* storefront that restored a `cart_…` id from `localStorage` and holds no
|
|
579
|
+
* publishable token in the browser.
|
|
580
|
+
*/
|
|
581
|
+
summary(): Promise<CartSessionSummary>;
|
|
582
|
+
addItem(input: AddLineItemInput): Promise<CartSessionCart>;
|
|
583
|
+
updateItem(itemId: string, input: UpdateLineItemInput): Promise<CartSessionCart>;
|
|
584
|
+
removeItem(itemId: string): Promise<CartSessionCart>;
|
|
585
|
+
selectShipping(input: SelectShippingInput): Promise<CartSessionCart>;
|
|
586
|
+
clearShipping(): Promise<CartSessionCart>;
|
|
587
|
+
applyDiscount(code: string): Promise<CartSessionCart>;
|
|
588
|
+
removeDiscount(): Promise<CartSessionCart>;
|
|
589
|
+
/** Set buyer-editable cart fields. Pass `null` to clear one. */
|
|
590
|
+
update(input: UpdateCartSessionInput): Promise<CartSessionCart>;
|
|
591
|
+
/**
|
|
592
|
+
* Bind to an existing session without a publishable token.
|
|
593
|
+
*
|
|
594
|
+
* `create()` needs a `pk_` token; every other route is authorized by the
|
|
595
|
+
* opaque `cart_…` id alone. A caller that only ever resumes — Throttle's own
|
|
596
|
+
* hosted cart page, or a storefront restoring from localStorage — would
|
|
597
|
+
* otherwise have to invent a token to satisfy the constructor.
|
|
598
|
+
*/
|
|
599
|
+
static forSession(cartSessionId: string, opts?: {
|
|
600
|
+
baseUrl?: string;
|
|
601
|
+
fetch?: typeof globalThis.fetch;
|
|
602
|
+
timeoutMs?: number;
|
|
603
|
+
origin?: string;
|
|
604
|
+
}): CartSessionClient;
|
|
605
|
+
/**
|
|
606
|
+
* The Throttle-hosted cart page for this session. Send a buyer here to let
|
|
607
|
+
* Throttle run the cart UI instead of building one.
|
|
608
|
+
*/
|
|
609
|
+
hostedCartUrl(baseUrl?: string): string;
|
|
487
610
|
/** Hand off to hosted/embedded checkout. Marks the session converted (no more edits). */
|
|
488
611
|
checkout(input: CheckoutHandoffInput): Promise<CheckoutHandoff>;
|
|
489
612
|
private basePath;
|
|
@@ -534,6 +657,25 @@ declare class CartNotFoundError extends ThrottleApiError {
|
|
|
534
657
|
details?: Record<string, unknown>;
|
|
535
658
|
});
|
|
536
659
|
}
|
|
660
|
+
/**
|
|
661
|
+
* Thrown when a request is aborted by the client's own timeout — the server
|
|
662
|
+
* never answered within `timeoutMs`.
|
|
663
|
+
*
|
|
664
|
+
* It exists because the alternative is worse than useless: aborting an
|
|
665
|
+
* AbortController without a reason produces a DOMException whose message is
|
|
666
|
+
* "signal is aborted without reason", and that string was reaching shoppers as
|
|
667
|
+
* a page-level alert on the hosted cart.
|
|
668
|
+
*
|
|
669
|
+
* `statusCode` is 408 (Request Timeout) so the error keeps the same shape as
|
|
670
|
+
* every other Cart SDK error, even though no server sent it.
|
|
671
|
+
*/
|
|
672
|
+
declare class CartTimeoutError extends ThrottleError {
|
|
673
|
+
constructor(args: {
|
|
674
|
+
timeoutMs: number;
|
|
675
|
+
method?: string;
|
|
676
|
+
path?: string;
|
|
677
|
+
});
|
|
678
|
+
}
|
|
537
679
|
/**
|
|
538
680
|
* Map a raw API error envelope to the most specific Cart SDK error class.
|
|
539
681
|
* Falls back to {@link ThrottleApiError} for everything unrecognised.
|
|
@@ -545,4 +687,4 @@ declare function cartErrorFromResponse(args: {
|
|
|
545
687
|
details?: Record<string, unknown>;
|
|
546
688
|
}): ThrottleApiError;
|
|
547
689
|
|
|
548
|
-
export { type AddLineItemInput, type AppliedDiscount, type Cart, type CartAddress, CartClient, type CartEvent, CartNotFoundError, CartNotOpenError, 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, cartErrorFromResponse };
|
|
690
|
+
export { type AddLineItemInput, type AppliedDiscount, type Cart, type CartAddress, type CartBranding, CartClient, type CartEvent, CartNotFoundError, CartNotOpenError, type CartSession, type CartSessionCart, CartSessionClient, type CartSessionClientOptions, type CartSessionSummary, CartTimeoutError, 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 UpdateCartSessionInput, type UpdateLineItemInput, cartErrorFromResponse };
|
package/dist/index.d.ts
CHANGED
|
@@ -11,6 +11,8 @@ export { ThrottleError, isThrottleError } from '@usethrottle/errors';
|
|
|
11
11
|
interface CartAddress {
|
|
12
12
|
firstName?: string;
|
|
13
13
|
lastName?: string;
|
|
14
|
+
/** Business name. Optional — B2B addresses use it, B2C addresses omit it. */
|
|
15
|
+
company?: string;
|
|
14
16
|
addressLine1: string;
|
|
15
17
|
addressLine2?: string;
|
|
16
18
|
city: string;
|
|
@@ -22,8 +24,14 @@ interface CartAddress {
|
|
|
22
24
|
email?: string;
|
|
23
25
|
}
|
|
24
26
|
interface Cart {
|
|
27
|
+
/**
|
|
28
|
+
* `getById`/`create`/etc. (the `carts` resource) return this shape. See
|
|
29
|
+
* {@link CartSessionCart} for the browser-facing `CartSessionClient` shape,
|
|
30
|
+
* which omits `workspaceId` and adds `cartId`.
|
|
31
|
+
*/
|
|
25
32
|
id: string;
|
|
26
33
|
workspaceId: string;
|
|
34
|
+
cartId?: string;
|
|
27
35
|
applicationId: string;
|
|
28
36
|
customerId: string | null;
|
|
29
37
|
/** Lightweight captured email (no full customer record), or null. */
|
|
@@ -39,6 +47,7 @@ interface Cart {
|
|
|
39
47
|
billingAddress: CartAddress | null;
|
|
40
48
|
shippingAddress: CartAddress | null;
|
|
41
49
|
notes: string | null;
|
|
50
|
+
metadata: Record<string, unknown>;
|
|
42
51
|
expiresAt: string | null;
|
|
43
52
|
externalRef?: {
|
|
44
53
|
connectionId: string;
|
|
@@ -52,6 +61,24 @@ interface Cart {
|
|
|
52
61
|
createdAt: string;
|
|
53
62
|
updatedAt: string;
|
|
54
63
|
}
|
|
64
|
+
/**
|
|
65
|
+
* The cart as the session (browser, opaque-token) surface returns it —
|
|
66
|
+
* `CartSessionClient`'s methods. There is no `workspaceId` on this surface
|
|
67
|
+
* (the browser never needs, and shouldn't see, the internal workspace id);
|
|
68
|
+
* `cartId` — the same value as `id` — is always present, kept for backward
|
|
69
|
+
* compatibility with `@usethrottle/cart`'s original (order-snapshot-shaped)
|
|
70
|
+
* cart-session response.
|
|
71
|
+
*/
|
|
72
|
+
interface CartSessionCart extends Omit<Cart, 'workspaceId'> {
|
|
73
|
+
cartId: string;
|
|
74
|
+
/**
|
|
75
|
+
* Sum of the line-item quantities — 1 stand + 2 notebooks + 1 keyboard is
|
|
76
|
+
* `4`, not `3`. Present only on this surface; the merchant `carts` resource
|
|
77
|
+
* does not return it. Named for what it counts because `itemCount` in the
|
|
78
|
+
* `cart.abandoned` webhook payload means the number of distinct lines.
|
|
79
|
+
*/
|
|
80
|
+
unitCount: number;
|
|
81
|
+
}
|
|
55
82
|
interface LineItem {
|
|
56
83
|
id: string;
|
|
57
84
|
cartId: string;
|
|
@@ -98,6 +125,12 @@ interface TaxLine {
|
|
|
98
125
|
jurisdictionCode: string;
|
|
99
126
|
jurisdictionName: string | null;
|
|
100
127
|
taxType: 'sales' | 'vat' | 'gst' | 'pst' | 'hst' | 'service' | 'excise' | 'other';
|
|
128
|
+
/** The VAT code the engine returned, verbatim — AvaTax's `vatCode`. */
|
|
129
|
+
vatCode?: string | null;
|
|
130
|
+
/** The engine's own name for the tax ('Standard'); `taxType` is our slug for it. */
|
|
131
|
+
taxName?: string | null;
|
|
132
|
+
/** Engine rate classification: 'S' standard, 'R' reduced, 'Z' zero. */
|
|
133
|
+
rateTypeCode?: string | null;
|
|
101
134
|
rate: number;
|
|
102
135
|
amount: number;
|
|
103
136
|
currency: string;
|
|
@@ -307,6 +340,20 @@ interface ShippingTaxCalculationResponse {
|
|
|
307
340
|
taxTotal: number;
|
|
308
341
|
total: number;
|
|
309
342
|
};
|
|
343
|
+
/**
|
|
344
|
+
* Verbatim messaging the tax engine returned about this transaction. In
|
|
345
|
+
* several EU member states the engine's own wording is what makes the
|
|
346
|
+
* invoice compliant, so it is carried through rather than paraphrased.
|
|
347
|
+
*/
|
|
348
|
+
providerMessages?: Array<{
|
|
349
|
+
summary: string;
|
|
350
|
+
details?: string;
|
|
351
|
+
/** The engine's identifier for the citation, e.g. 'Article138.1'. */
|
|
352
|
+
citationCode?: string;
|
|
353
|
+
refersTo?: string;
|
|
354
|
+
severity?: string;
|
|
355
|
+
source?: string;
|
|
356
|
+
}>;
|
|
310
357
|
prompts: Array<{
|
|
311
358
|
code: 'missing_info';
|
|
312
359
|
field: string;
|
|
@@ -441,17 +488,63 @@ interface CreateCartSessionInput {
|
|
|
441
488
|
netN?: number;
|
|
442
489
|
metadata?: Record<string, unknown>;
|
|
443
490
|
}
|
|
491
|
+
interface CartBranding {
|
|
492
|
+
logoUrl: string | null;
|
|
493
|
+
primaryColor: string | null;
|
|
494
|
+
merchantName: string | null;
|
|
495
|
+
}
|
|
496
|
+
/** Buyer-editable cart fields. `null` clears a value. */
|
|
497
|
+
interface UpdateCartSessionInput {
|
|
498
|
+
customerEmail?: string | null;
|
|
499
|
+
notes?: string | null;
|
|
500
|
+
}
|
|
444
501
|
interface CartSession {
|
|
445
502
|
cartSessionId: string;
|
|
446
503
|
status: string;
|
|
447
504
|
expiresAt: string;
|
|
448
|
-
|
|
505
|
+
branding: CartBranding;
|
|
506
|
+
/**
|
|
507
|
+
* Whatever the storefront passed to `create()`. Write-once — unlike the copy
|
|
508
|
+
* on `cart.metadata`, which the merchant API can change — so this is the one
|
|
509
|
+
* to read the buyer's return/cancel/storefront URLs from.
|
|
510
|
+
*/
|
|
511
|
+
metadata: Record<string, unknown>;
|
|
512
|
+
cart: CartSessionCart;
|
|
513
|
+
}
|
|
514
|
+
/**
|
|
515
|
+
* The badge-sized read: what a storefront header needs to draw a cart count,
|
|
516
|
+
* and nothing more. No line items, no addresses, no buyer email or notes.
|
|
517
|
+
*/
|
|
518
|
+
interface CartSessionSummary {
|
|
519
|
+
cartSessionId: string;
|
|
520
|
+
/**
|
|
521
|
+
* `active` while the cart can still be edited. `expired` (past its TTL) or
|
|
522
|
+
* `converted` (handed off to checkout) once the session is finished — those
|
|
523
|
+
* come back as a normal 200 with a zeroed summary, not an error, so a badge
|
|
524
|
+
* can render a zero without a try/catch.
|
|
525
|
+
*/
|
|
526
|
+
status: 'active' | 'expired' | 'converted';
|
|
527
|
+
/**
|
|
528
|
+
* Sum of the line-item quantities — 1 stand + 2 notebooks + 1 keyboard is
|
|
529
|
+
* `4`, not `3`. Named for what it counts because `itemCount` elsewhere in
|
|
530
|
+
* Throttle (the `cart.abandoned` payload) means the number of distinct lines.
|
|
531
|
+
*/
|
|
532
|
+
unitCount: number;
|
|
533
|
+
/** Cart total in minor units, or `0` on a finished session. */
|
|
534
|
+
total: number;
|
|
535
|
+
/** `null` on a finished session — there is no live cart to price. */
|
|
536
|
+
currency: string | null;
|
|
449
537
|
}
|
|
450
538
|
interface CheckoutHandoffInput {
|
|
451
539
|
returnUrl: string;
|
|
452
540
|
cancelUrl: string;
|
|
453
541
|
customerEmail?: string;
|
|
454
542
|
allowedMethods?: string[];
|
|
543
|
+
/**
|
|
544
|
+
* Leave the cart session open after handing off, so a buyer who backs out of
|
|
545
|
+
* checkout returns to a live cart. Throttle's own hosted cart page sets this.
|
|
546
|
+
*/
|
|
547
|
+
keepOpen?: boolean;
|
|
455
548
|
}
|
|
456
549
|
interface CheckoutHandoff {
|
|
457
550
|
cartSessionId: string;
|
|
@@ -468,6 +561,7 @@ declare class CartSessionClient {
|
|
|
468
561
|
private readonly timeoutMs;
|
|
469
562
|
private readonly origin?;
|
|
470
563
|
private sessionId;
|
|
564
|
+
private isForSessionClient;
|
|
471
565
|
constructor(opts: CartSessionClientOptions);
|
|
472
566
|
/** The current bound session id (after `create()`/`resume()`), or null. */
|
|
473
567
|
get cartSessionId(): string | null;
|
|
@@ -477,13 +571,42 @@ declare class CartSessionClient {
|
|
|
477
571
|
create(input?: CreateCartSessionInput): Promise<CartSession>;
|
|
478
572
|
/** Read the current session + cart snapshot. */
|
|
479
573
|
get(): Promise<CartSession>;
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
574
|
+
/**
|
|
575
|
+
* Unit count + total for a cart badge, without pulling the whole cart.
|
|
576
|
+
*
|
|
577
|
+
* Works on a client built with {@link CartSessionClient.forSession} — a
|
|
578
|
+
* storefront that restored a `cart_…` id from `localStorage` and holds no
|
|
579
|
+
* publishable token in the browser.
|
|
580
|
+
*/
|
|
581
|
+
summary(): Promise<CartSessionSummary>;
|
|
582
|
+
addItem(input: AddLineItemInput): Promise<CartSessionCart>;
|
|
583
|
+
updateItem(itemId: string, input: UpdateLineItemInput): Promise<CartSessionCart>;
|
|
584
|
+
removeItem(itemId: string): Promise<CartSessionCart>;
|
|
585
|
+
selectShipping(input: SelectShippingInput): Promise<CartSessionCart>;
|
|
586
|
+
clearShipping(): Promise<CartSessionCart>;
|
|
587
|
+
applyDiscount(code: string): Promise<CartSessionCart>;
|
|
588
|
+
removeDiscount(): Promise<CartSessionCart>;
|
|
589
|
+
/** Set buyer-editable cart fields. Pass `null` to clear one. */
|
|
590
|
+
update(input: UpdateCartSessionInput): Promise<CartSessionCart>;
|
|
591
|
+
/**
|
|
592
|
+
* Bind to an existing session without a publishable token.
|
|
593
|
+
*
|
|
594
|
+
* `create()` needs a `pk_` token; every other route is authorized by the
|
|
595
|
+
* opaque `cart_…` id alone. A caller that only ever resumes — Throttle's own
|
|
596
|
+
* hosted cart page, or a storefront restoring from localStorage — would
|
|
597
|
+
* otherwise have to invent a token to satisfy the constructor.
|
|
598
|
+
*/
|
|
599
|
+
static forSession(cartSessionId: string, opts?: {
|
|
600
|
+
baseUrl?: string;
|
|
601
|
+
fetch?: typeof globalThis.fetch;
|
|
602
|
+
timeoutMs?: number;
|
|
603
|
+
origin?: string;
|
|
604
|
+
}): CartSessionClient;
|
|
605
|
+
/**
|
|
606
|
+
* The Throttle-hosted cart page for this session. Send a buyer here to let
|
|
607
|
+
* Throttle run the cart UI instead of building one.
|
|
608
|
+
*/
|
|
609
|
+
hostedCartUrl(baseUrl?: string): string;
|
|
487
610
|
/** Hand off to hosted/embedded checkout. Marks the session converted (no more edits). */
|
|
488
611
|
checkout(input: CheckoutHandoffInput): Promise<CheckoutHandoff>;
|
|
489
612
|
private basePath;
|
|
@@ -534,6 +657,25 @@ declare class CartNotFoundError extends ThrottleApiError {
|
|
|
534
657
|
details?: Record<string, unknown>;
|
|
535
658
|
});
|
|
536
659
|
}
|
|
660
|
+
/**
|
|
661
|
+
* Thrown when a request is aborted by the client's own timeout — the server
|
|
662
|
+
* never answered within `timeoutMs`.
|
|
663
|
+
*
|
|
664
|
+
* It exists because the alternative is worse than useless: aborting an
|
|
665
|
+
* AbortController without a reason produces a DOMException whose message is
|
|
666
|
+
* "signal is aborted without reason", and that string was reaching shoppers as
|
|
667
|
+
* a page-level alert on the hosted cart.
|
|
668
|
+
*
|
|
669
|
+
* `statusCode` is 408 (Request Timeout) so the error keeps the same shape as
|
|
670
|
+
* every other Cart SDK error, even though no server sent it.
|
|
671
|
+
*/
|
|
672
|
+
declare class CartTimeoutError extends ThrottleError {
|
|
673
|
+
constructor(args: {
|
|
674
|
+
timeoutMs: number;
|
|
675
|
+
method?: string;
|
|
676
|
+
path?: string;
|
|
677
|
+
});
|
|
678
|
+
}
|
|
537
679
|
/**
|
|
538
680
|
* Map a raw API error envelope to the most specific Cart SDK error class.
|
|
539
681
|
* Falls back to {@link ThrottleApiError} for everything unrecognised.
|
|
@@ -545,4 +687,4 @@ declare function cartErrorFromResponse(args: {
|
|
|
545
687
|
details?: Record<string, unknown>;
|
|
546
688
|
}): ThrottleApiError;
|
|
547
689
|
|
|
548
|
-
export { type AddLineItemInput, type AppliedDiscount, type Cart, type CartAddress, CartClient, type CartEvent, CartNotFoundError, CartNotOpenError, 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, cartErrorFromResponse };
|
|
690
|
+
export { type AddLineItemInput, type AppliedDiscount, type Cart, type CartAddress, type CartBranding, CartClient, type CartEvent, CartNotFoundError, CartNotOpenError, type CartSession, type CartSessionCart, CartSessionClient, type CartSessionClientOptions, type CartSessionSummary, CartTimeoutError, 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 UpdateCartSessionInput, type UpdateLineItemInput, cartErrorFromResponse };
|
package/dist/index.js
CHANGED
|
@@ -19,6 +19,17 @@ var CartNotFoundError = class extends ThrottleApiError {
|
|
|
19
19
|
this.name = "CartNotFoundError";
|
|
20
20
|
}
|
|
21
21
|
};
|
|
22
|
+
var CartTimeoutError = class extends ThrottleError {
|
|
23
|
+
constructor(args) {
|
|
24
|
+
super({
|
|
25
|
+
code: "request_timeout",
|
|
26
|
+
statusCode: 408,
|
|
27
|
+
message: `Request timed out after ${args.timeoutMs}ms`,
|
|
28
|
+
details: { timeoutMs: args.timeoutMs, method: args.method, path: args.path }
|
|
29
|
+
});
|
|
30
|
+
this.name = "CartTimeoutError";
|
|
31
|
+
}
|
|
32
|
+
};
|
|
22
33
|
function cartErrorFromResponse(args) {
|
|
23
34
|
if (args.statusCode === 409 && args.code === "cart_not_open") {
|
|
24
35
|
return new CartNotOpenError({ message: args.message, statusCode: args.statusCode, details: args.details });
|
|
@@ -42,7 +53,7 @@ var CartClient = class {
|
|
|
42
53
|
if (!opts.apiKey) throw new Error("CartClient: apiKey is required");
|
|
43
54
|
this.apiKey = opts.apiKey;
|
|
44
55
|
this.baseUrl = (opts.baseUrl ?? "https://api.usethrottle.dev").replace(/\/+$/, "");
|
|
45
|
-
this.fetchImpl = opts.fetch ?? globalThis.fetch;
|
|
56
|
+
this.fetchImpl = opts.fetch ?? globalThis.fetch.bind(globalThis);
|
|
46
57
|
this.timeoutMs = opts.timeoutMs ?? 3e4;
|
|
47
58
|
}
|
|
48
59
|
async request(method, path, body) {
|
|
@@ -137,13 +148,13 @@ var StorefrontQuoteClient = class {
|
|
|
137
148
|
origin;
|
|
138
149
|
constructor(opts) {
|
|
139
150
|
if (!opts.applicationId) throw new Error("StorefrontQuoteClient: applicationId is required");
|
|
140
|
-
if (
|
|
141
|
-
throw new Error("StorefrontQuoteClient: quoteToken must be a publishable pk_ token");
|
|
151
|
+
if (!/^(pk|qt)_/.test(opts.quoteToken ?? "")) {
|
|
152
|
+
throw new Error("StorefrontQuoteClient: quoteToken must be a publishable qt_ (or legacy pk_) token");
|
|
142
153
|
}
|
|
143
154
|
this.applicationId = opts.applicationId;
|
|
144
155
|
this.quoteToken = opts.quoteToken;
|
|
145
156
|
this.baseUrl = (opts.baseUrl ?? "https://api.usethrottle.dev").replace(/\/+$/, "");
|
|
146
|
-
this.fetchImpl = opts.fetch ?? globalThis.fetch;
|
|
157
|
+
this.fetchImpl = opts.fetch ?? globalThis.fetch.bind(globalThis);
|
|
147
158
|
this.timeoutMs = opts.timeoutMs ?? 15e3;
|
|
148
159
|
this.origin = opts.origin;
|
|
149
160
|
}
|
|
@@ -180,7 +191,7 @@ var StorefrontQuoteClient = class {
|
|
|
180
191
|
};
|
|
181
192
|
|
|
182
193
|
// src/cart-session.ts
|
|
183
|
-
var CartSessionClient = class {
|
|
194
|
+
var CartSessionClient = class _CartSessionClient {
|
|
184
195
|
applicationId;
|
|
185
196
|
environmentId;
|
|
186
197
|
quoteToken;
|
|
@@ -189,17 +200,18 @@ var CartSessionClient = class {
|
|
|
189
200
|
timeoutMs;
|
|
190
201
|
origin;
|
|
191
202
|
sessionId = null;
|
|
203
|
+
isForSessionClient = false;
|
|
192
204
|
constructor(opts) {
|
|
193
205
|
if (!opts.applicationId) throw new Error("CartSessionClient: applicationId is required");
|
|
194
206
|
if (!opts.environmentId) throw new Error("CartSessionClient: environmentId is required");
|
|
195
|
-
if (
|
|
196
|
-
throw new Error("CartSessionClient: quoteToken must be a publishable pk_ token");
|
|
207
|
+
if (!/^(pk|qt)_/.test(opts.quoteToken ?? "")) {
|
|
208
|
+
throw new Error("CartSessionClient: quoteToken must be a publishable qt_ (or legacy pk_) token");
|
|
197
209
|
}
|
|
198
210
|
this.applicationId = opts.applicationId;
|
|
199
211
|
this.environmentId = opts.environmentId;
|
|
200
212
|
this.quoteToken = opts.quoteToken;
|
|
201
213
|
this.baseUrl = (opts.baseUrl ?? "https://api.usethrottle.dev").replace(/\/+$/, "");
|
|
202
|
-
this.fetchImpl = opts.fetch ?? globalThis.fetch;
|
|
214
|
+
this.fetchImpl = opts.fetch ?? globalThis.fetch.bind(globalThis);
|
|
203
215
|
this.timeoutMs = opts.timeoutMs ?? 15e3;
|
|
204
216
|
this.origin = opts.origin;
|
|
205
217
|
}
|
|
@@ -214,6 +226,11 @@ var CartSessionClient = class {
|
|
|
214
226
|
}
|
|
215
227
|
/** Create a new cart session and bind this client to it. */
|
|
216
228
|
async create(input = {}) {
|
|
229
|
+
if (this.isForSessionClient) {
|
|
230
|
+
throw new Error(
|
|
231
|
+
"CartSessionClient: this client was created with forSession() and cannot call create(). Construct a CartSessionClient with a real pk_ publishable token instead."
|
|
232
|
+
);
|
|
233
|
+
}
|
|
217
234
|
const data = await this.request("POST", "/api/v1/cart-sessions", {
|
|
218
235
|
applicationId: this.applicationId,
|
|
219
236
|
environmentId: this.environmentId,
|
|
@@ -221,17 +238,38 @@ var CartSessionClient = class {
|
|
|
221
238
|
...input
|
|
222
239
|
});
|
|
223
240
|
this.sessionId = data.cartSessionId;
|
|
224
|
-
return
|
|
241
|
+
return {
|
|
242
|
+
...data,
|
|
243
|
+
branding: data.branding ?? { logoUrl: null, primaryColor: null, merchantName: null }
|
|
244
|
+
};
|
|
225
245
|
}
|
|
226
246
|
/** Read the current session + cart snapshot. */
|
|
227
|
-
get() {
|
|
228
|
-
|
|
247
|
+
async get() {
|
|
248
|
+
const data = await this.request("GET", this.basePath());
|
|
249
|
+
return {
|
|
250
|
+
...data,
|
|
251
|
+
branding: data.branding ?? { logoUrl: null, primaryColor: null, merchantName: null }
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* Unit count + total for a cart badge, without pulling the whole cart.
|
|
256
|
+
*
|
|
257
|
+
* Works on a client built with {@link CartSessionClient.forSession} — a
|
|
258
|
+
* storefront that restored a `cart_…` id from `localStorage` and holds no
|
|
259
|
+
* publishable token in the browser.
|
|
260
|
+
*/
|
|
261
|
+
async summary() {
|
|
262
|
+
return this.request("GET", `${this.basePath()}/summary`);
|
|
229
263
|
}
|
|
230
264
|
async addItem(input) {
|
|
231
265
|
return (await this.request("POST", `${this.basePath()}/items`, input)).cart;
|
|
232
266
|
}
|
|
233
267
|
async updateItem(itemId, input) {
|
|
234
|
-
return (await this.request(
|
|
268
|
+
return (await this.request(
|
|
269
|
+
"PATCH",
|
|
270
|
+
`${this.basePath()}/items/${encodeURIComponent(itemId)}`,
|
|
271
|
+
input
|
|
272
|
+
)).cart;
|
|
235
273
|
}
|
|
236
274
|
async removeItem(itemId) {
|
|
237
275
|
return (await this.request("DELETE", `${this.basePath()}/items/${encodeURIComponent(itemId)}`)).cart;
|
|
@@ -248,6 +286,38 @@ var CartSessionClient = class {
|
|
|
248
286
|
async removeDiscount() {
|
|
249
287
|
return (await this.request("DELETE", `${this.basePath()}/discount`)).cart;
|
|
250
288
|
}
|
|
289
|
+
/** Set buyer-editable cart fields. Pass `null` to clear one. */
|
|
290
|
+
async update(input) {
|
|
291
|
+
return (await this.request("PATCH", this.basePath(), input)).cart;
|
|
292
|
+
}
|
|
293
|
+
/**
|
|
294
|
+
* Bind to an existing session without a publishable token.
|
|
295
|
+
*
|
|
296
|
+
* `create()` needs a `pk_` token; every other route is authorized by the
|
|
297
|
+
* opaque `cart_…` id alone. A caller that only ever resumes — Throttle's own
|
|
298
|
+
* hosted cart page, or a storefront restoring from localStorage — would
|
|
299
|
+
* otherwise have to invent a token to satisfy the constructor.
|
|
300
|
+
*/
|
|
301
|
+
static forSession(cartSessionId, opts = {}) {
|
|
302
|
+
const client = new _CartSessionClient({
|
|
303
|
+
applicationId: "session",
|
|
304
|
+
environmentId: "session",
|
|
305
|
+
quoteToken: "pk_session",
|
|
306
|
+
...opts
|
|
307
|
+
});
|
|
308
|
+
client.isForSessionClient = true;
|
|
309
|
+
return client.resume(cartSessionId);
|
|
310
|
+
}
|
|
311
|
+
/**
|
|
312
|
+
* The Throttle-hosted cart page for this session. Send a buyer here to let
|
|
313
|
+
* Throttle run the cart UI instead of building one.
|
|
314
|
+
*/
|
|
315
|
+
hostedCartUrl(baseUrl = "https://checkout.usethrottle.dev") {
|
|
316
|
+
if (!this.sessionId) {
|
|
317
|
+
throw new Error("CartSessionClient: no active session \u2014 call create() or resume(id) first");
|
|
318
|
+
}
|
|
319
|
+
return `${baseUrl.replace(/\/+$/, "")}/cart/${encodeURIComponent(this.sessionId)}`;
|
|
320
|
+
}
|
|
251
321
|
/** Hand off to hosted/embedded checkout. Marks the session converted (no more edits). */
|
|
252
322
|
checkout(input) {
|
|
253
323
|
return this.request("POST", `${this.basePath()}/checkout-session`, input);
|
|
@@ -260,7 +330,11 @@ var CartSessionClient = class {
|
|
|
260
330
|
}
|
|
261
331
|
async request(method, path, body) {
|
|
262
332
|
const ctrl = new AbortController();
|
|
263
|
-
|
|
333
|
+
let timedOut = false;
|
|
334
|
+
const timer = setTimeout(() => {
|
|
335
|
+
timedOut = true;
|
|
336
|
+
ctrl.abort();
|
|
337
|
+
}, this.timeoutMs);
|
|
264
338
|
try {
|
|
265
339
|
const headers = { "content-type": "application/json" };
|
|
266
340
|
if (this.origin) headers.origin = this.origin;
|
|
@@ -280,6 +354,9 @@ var CartSessionClient = class {
|
|
|
280
354
|
});
|
|
281
355
|
}
|
|
282
356
|
return json.data;
|
|
357
|
+
} catch (err) {
|
|
358
|
+
if (timedOut) throw new CartTimeoutError({ timeoutMs: this.timeoutMs, method, path });
|
|
359
|
+
throw err;
|
|
283
360
|
} finally {
|
|
284
361
|
clearTimeout(timer);
|
|
285
362
|
}
|
|
@@ -290,6 +367,7 @@ export {
|
|
|
290
367
|
CartNotFoundError,
|
|
291
368
|
CartNotOpenError,
|
|
292
369
|
CartSessionClient,
|
|
370
|
+
CartTimeoutError,
|
|
293
371
|
StorefrontQuoteClient,
|
|
294
372
|
ThrottleApiError,
|
|
295
373
|
ThrottleError2 as ThrottleError,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@usethrottle/cart",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.9.0",
|
|
4
4
|
"description": "Typed REST client for the Throttle Cart API.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.cjs",
|
|
@@ -17,8 +17,13 @@
|
|
|
17
17
|
"dist",
|
|
18
18
|
"README.md"
|
|
19
19
|
],
|
|
20
|
+
"scripts": {
|
|
21
|
+
"build": "tsup",
|
|
22
|
+
"test": "vitest run",
|
|
23
|
+
"typecheck": "tsc --noEmit"
|
|
24
|
+
},
|
|
20
25
|
"dependencies": {
|
|
21
|
-
"@usethrottle/errors": "
|
|
26
|
+
"@usethrottle/errors": "workspace:^"
|
|
22
27
|
},
|
|
23
28
|
"devDependencies": {
|
|
24
29
|
"tsup": "^8.0.0",
|
|
@@ -27,10 +32,5 @@
|
|
|
27
32
|
},
|
|
28
33
|
"publishConfig": {
|
|
29
34
|
"access": "public"
|
|
30
|
-
},
|
|
31
|
-
"scripts": {
|
|
32
|
-
"build": "tsup",
|
|
33
|
-
"test": "vitest run",
|
|
34
|
-
"typecheck": "tsc --noEmit"
|
|
35
35
|
}
|
|
36
|
-
}
|
|
36
|
+
}
|