@caldik/client 0.1.3 → 0.1.4
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 +119 -12
- package/dist/index.d.mts +39 -15
- package/dist/index.mjs +296 -121
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
# Caldik SDK
|
|
2
|
-
|
|
3
|
-
The official Caldik SDK for interacting with Caldik storefronts, products, carts, checkouts, and customer accounts.
|
|
2
|
+
TThe official Caldik SDK for interacting with Caldik storefronts, products, carts, checkouts, and customer accounts.
|
|
4
3
|
|
|
5
4
|
## Installation
|
|
6
5
|
|
|
@@ -37,9 +36,48 @@ const caldik = new CaldikClient({
|
|
|
37
36
|
|
|
38
37
|
> **Security:** Private API keys must only be used in trusted server-side environments. Never expose `sk_` keys in client-side code.
|
|
39
38
|
|
|
39
|
+
|
|
40
40
|
## Orders and Items
|
|
41
41
|
|
|
42
|
-
###
|
|
42
|
+
### List Products
|
|
43
|
+
|
|
44
|
+
Retrieve products associated with the current store.
|
|
45
|
+
|
|
46
|
+
**Basic usage**
|
|
47
|
+
|
|
48
|
+
```ts
|
|
49
|
+
const { products } = await caldik.products.list()
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
**Limit results**
|
|
53
|
+
|
|
54
|
+
Pass a number to limit the number of products returned.
|
|
55
|
+
|
|
56
|
+
```ts
|
|
57
|
+
const { products } = await caldik.products.list(20)
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### Get Product
|
|
61
|
+
|
|
62
|
+
Retrieve a specific product by its ID.
|
|
63
|
+
|
|
64
|
+
**Basic usage**
|
|
65
|
+
|
|
66
|
+
```ts
|
|
67
|
+
const { product } = await caldik.products.get("product_123")
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### Get Category
|
|
71
|
+
|
|
72
|
+
Retrieve a specific category by its ID.
|
|
73
|
+
|
|
74
|
+
**Basic usage**
|
|
75
|
+
|
|
76
|
+
```ts
|
|
77
|
+
const { category } = await caldik.categories.get("CAT_ID")
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### List Categories
|
|
43
81
|
|
|
44
82
|
Retrieve categories associated with the current store.
|
|
45
83
|
|
|
@@ -68,6 +106,8 @@ const { categories } = await caldik.categories.list({
|
|
|
68
106
|
})
|
|
69
107
|
```
|
|
70
108
|
|
|
109
|
+
**Optional fields**
|
|
110
|
+
|
|
71
111
|
The following fields are omitted by default unless explicitly requested using `include`:
|
|
72
112
|
|
|
73
113
|
| Field | Description |
|
|
@@ -75,16 +115,23 @@ The following fields are omitted by default unless explicitly requested using `i
|
|
|
75
115
|
| `productCount` | Number of products associated with the category. |
|
|
76
116
|
| `startingPrice` | Lowest available product price in the category, in cents. |
|
|
77
117
|
|
|
118
|
+
|
|
78
119
|
## Cart and Discounts
|
|
79
120
|
|
|
80
121
|
### Cart Management
|
|
81
122
|
|
|
82
|
-
|
|
123
|
+
**Get the cart**
|
|
124
|
+
|
|
125
|
+
Refresh the current items and applied discount from the server.
|
|
126
|
+
|
|
127
|
+
```ts
|
|
128
|
+
const { items, discount } = await caldik.cart.get()
|
|
129
|
+
```
|
|
83
130
|
|
|
84
131
|
**Add an item**
|
|
85
132
|
|
|
86
133
|
```ts
|
|
87
|
-
const cart = caldik.cart.add({
|
|
134
|
+
const cart = await caldik.cart.add({
|
|
88
135
|
productId: "PRODUCT_...",
|
|
89
136
|
productVariantId: "PRODUCT_VARIANT_...",
|
|
90
137
|
quantity: 1,
|
|
@@ -93,30 +140,90 @@ const cart = caldik.cart.add({
|
|
|
93
140
|
|
|
94
141
|
**Update item quantity**
|
|
95
142
|
|
|
143
|
+
Set an item's quantity. A quantity of zero removes it.
|
|
144
|
+
|
|
96
145
|
```ts
|
|
97
|
-
caldik.cart.updateQuantity(cart.items[0].id, 3)
|
|
146
|
+
await caldik.cart.updateQuantity(cart.items[0].id, 3)
|
|
98
147
|
```
|
|
99
148
|
|
|
100
149
|
**Remove an item**
|
|
101
150
|
|
|
102
151
|
```ts
|
|
103
|
-
caldik.cart.remove(cart.items[0].id)
|
|
152
|
+
await caldik.cart.remove(cart.items[0].id)
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
**Clear the cart**
|
|
156
|
+
|
|
157
|
+
Remove all items and the applied discount from the persisted cart.
|
|
158
|
+
|
|
159
|
+
```ts
|
|
160
|
+
await caldik.cart.clear()
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
**Check a discount**
|
|
164
|
+
|
|
165
|
+
Get a price and discount quote for the current items without applying the code.
|
|
166
|
+
|
|
167
|
+
```ts
|
|
168
|
+
const quote = await caldik.cart.checkDiscount("SAVE10")
|
|
104
169
|
```
|
|
105
170
|
|
|
106
|
-
|
|
171
|
+
**Apply a discount**
|
|
107
172
|
|
|
108
|
-
|
|
173
|
+
Validate the code and save it to the current cart.
|
|
109
174
|
|
|
110
175
|
```ts
|
|
111
|
-
await caldik.cart.applyDiscount("SAVE10")
|
|
176
|
+
const discountedCart = await caldik.cart.applyDiscount("SAVE10")
|
|
112
177
|
```
|
|
113
178
|
|
|
114
|
-
|
|
179
|
+
**Remove the discount**
|
|
115
180
|
|
|
116
|
-
|
|
181
|
+
Remove the applied code while keeping the cart's items.
|
|
182
|
+
|
|
183
|
+
```ts
|
|
184
|
+
await caldik.cart.removeDiscount()
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
**Checkout the cart**
|
|
188
|
+
|
|
189
|
+
Create an order using the persisted cart. The API revalidates prices and discounts.
|
|
117
190
|
|
|
118
191
|
```ts
|
|
119
192
|
const result = await caldik.cart.checkout({
|
|
193
|
+
paymentMethod: "stripe",
|
|
194
|
+
termsAccepted: acceptedTermsCheckbox,
|
|
195
|
+
successUrl: "https://shop.example/success",
|
|
196
|
+
cancelUrl: "https://shop.example/cart",
|
|
197
|
+
})
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
### Server-rendered Storefronts
|
|
201
|
+
|
|
202
|
+
Create a client for each incoming request so shopper sessions stay isolated.
|
|
203
|
+
Pass the request and forward returned session cookies to the outgoing response:
|
|
204
|
+
|
|
205
|
+
```ts
|
|
206
|
+
const responseHeaders = new Headers()
|
|
207
|
+
const caldik = new CaldikClient({
|
|
208
|
+
request,
|
|
209
|
+
onSetCookie: (cookie) => responseHeaders.append("Set-Cookie", cookie),
|
|
210
|
+
// External servers also supply storeId and their private apiKey.
|
|
211
|
+
})
|
|
212
|
+
const cart = await caldik.cart.get()
|
|
213
|
+
// Include responseHeaders in your framework's HTTP response.
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
Browser and server calls must share the same cookie context (for example through
|
|
217
|
+
a storefront API proxy). Cookies set only on a separate API hostname are not
|
|
218
|
+
included in requests to the storefront server. A private store key does not
|
|
219
|
+
replace a shopper's customer session.
|
|
220
|
+
|
|
221
|
+
### Checkout with Explicit Items
|
|
222
|
+
|
|
223
|
+
Use `caldik.checkout.create()` to create an order from explicitly provided items.
|
|
224
|
+
|
|
225
|
+
```ts
|
|
226
|
+
const result = await caldik.checkout.create({
|
|
120
227
|
paymentMethod: "stripe",
|
|
121
228
|
items: [
|
|
122
229
|
{
|
package/dist/index.d.mts
CHANGED
|
@@ -349,6 +349,8 @@ interface RequestOptions {
|
|
|
349
349
|
edgeCache?: boolean;
|
|
350
350
|
/** Internal authorization policy. Protected hosted calls are the default. */
|
|
351
351
|
access?: "public" | "runtime";
|
|
352
|
+
/** Scoped credentials used by stateful storefront resources. */
|
|
353
|
+
headers?: Record<string, string>;
|
|
352
354
|
}
|
|
353
355
|
//#endregion
|
|
354
356
|
//#region src/internal/client.d.ts
|
|
@@ -356,13 +358,22 @@ type HttpClientOptions = {
|
|
|
356
358
|
apiKey?: string;
|
|
357
359
|
apiUrl?: string;
|
|
358
360
|
storeId?: string;
|
|
361
|
+
request?: Request;
|
|
362
|
+
onSetCookie?: (cookie: string) => void | Promise<void>;
|
|
359
363
|
};
|
|
360
364
|
declare class CaldikHttpClient {
|
|
361
365
|
private readonly apiKey?;
|
|
362
366
|
private readonly apiUrl;
|
|
363
367
|
private readonly storeId?;
|
|
368
|
+
private readonly cookies;
|
|
369
|
+
private readonly onSetCookie?;
|
|
370
|
+
private readonly authenticationListeners;
|
|
364
371
|
constructor(options?: HttpClientOptions);
|
|
365
372
|
storeContextId(): string | null;
|
|
373
|
+
hasCartSession(): boolean;
|
|
374
|
+
onCustomerAuthentication(listener: () => Promise<unknown>): void;
|
|
375
|
+
customerAuthenticated(): Promise<void>;
|
|
376
|
+
private captureCookies;
|
|
366
377
|
private baseUrl;
|
|
367
378
|
private request;
|
|
368
379
|
get<T>(path: string, params?: Record<string, string | number>, options?: RequestOptions): Promise<T>;
|
|
@@ -476,10 +487,8 @@ declare class DiscountsApi {
|
|
|
476
487
|
}
|
|
477
488
|
//#endregion
|
|
478
489
|
//#region src/cart.d.ts
|
|
479
|
-
interface CartItem {
|
|
490
|
+
interface CartItem extends ProductCommerceItem {
|
|
480
491
|
id: string;
|
|
481
|
-
productId: string;
|
|
482
|
-
productVariantId?: string;
|
|
483
492
|
quantity: number;
|
|
484
493
|
}
|
|
485
494
|
interface CartDiscount {
|
|
@@ -494,24 +503,35 @@ interface CartSnapshot {
|
|
|
494
503
|
items: CartItem[];
|
|
495
504
|
discount: CartDiscount | null;
|
|
496
505
|
}
|
|
497
|
-
type CartCheckoutInput = Omit<CreateOrderInput, "items" | "discountCode">;
|
|
506
|
+
type CartCheckoutInput = Omit<CreateOrderInput, "items" | "discountCode" | "customerId">;
|
|
507
|
+
/** One persisted cart, automatically scoped to the current shopper session. */
|
|
498
508
|
declare class CartApi {
|
|
499
509
|
private client;
|
|
500
|
-
private
|
|
510
|
+
private cache;
|
|
511
|
+
private queue;
|
|
512
|
+
private initialized;
|
|
501
513
|
constructor(client: CaldikHttpClient);
|
|
502
|
-
private
|
|
503
|
-
private
|
|
514
|
+
private storage;
|
|
515
|
+
private key;
|
|
516
|
+
private readCache;
|
|
517
|
+
private persistCache;
|
|
504
518
|
private write;
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
private
|
|
519
|
+
private enqueue;
|
|
520
|
+
private options;
|
|
521
|
+
private resolve;
|
|
522
|
+
private reset;
|
|
523
|
+
private mutate;
|
|
524
|
+
private save;
|
|
525
|
+
private quote;
|
|
526
|
+
get(): Promise<CartSnapshot>;
|
|
527
|
+
add(item: ProductCommerceItem): Promise<CartSnapshot>;
|
|
528
|
+
updateQuantity(itemId: string, nextQuantity: number): Promise<CartSnapshot>;
|
|
529
|
+
remove(itemId: string): Promise<CartSnapshot>;
|
|
530
|
+
clear(): Promise<CartSnapshot>;
|
|
511
531
|
checkDiscount(code: string): Promise<DiscountQuoteResponse>;
|
|
512
532
|
applyDiscount(code: string): Promise<CartSnapshot>;
|
|
513
|
-
removeDiscount(): CartSnapshot
|
|
514
|
-
checkout(input
|
|
533
|
+
removeDiscount(): Promise<CartSnapshot>;
|
|
534
|
+
checkout(input?: CartCheckoutInput): Promise<OrderResponse>;
|
|
515
535
|
}
|
|
516
536
|
//#endregion
|
|
517
537
|
//#region src/index.d.ts
|
|
@@ -521,6 +541,10 @@ interface CaldikClientOptions {
|
|
|
521
541
|
/** Private API key for external server integrations. Never use in browsers. */
|
|
522
542
|
apiKey?: string;
|
|
523
543
|
apiUrl?: string;
|
|
544
|
+
/** Incoming storefront request. Create one client per server request to isolate shoppers. */
|
|
545
|
+
request?: Request;
|
|
546
|
+
/** Forward session cookies to the outgoing storefront response in server code. */
|
|
547
|
+
onSetCookie?: (cookie: string) => void | Promise<void>;
|
|
524
548
|
}
|
|
525
549
|
declare class CaldikClient {
|
|
526
550
|
readonly categories: CategoriesApi;
|
package/dist/index.mjs
CHANGED
|
@@ -26,6 +26,14 @@ function defaultApiUrl(browser) {
|
|
|
26
26
|
//#region src/internal/client.ts
|
|
27
27
|
const STORE_ID_PATTERN = /^STORE_[a-f0-9]{32}$/i;
|
|
28
28
|
const STOREFRONT_STORE_ID_ATTRIBUTE = "data-caldik-store-id";
|
|
29
|
+
var CaldikHttpError = class extends Error {
|
|
30
|
+
status;
|
|
31
|
+
constructor(message, status) {
|
|
32
|
+
super(message);
|
|
33
|
+
this.status = status;
|
|
34
|
+
this.name = "CaldikHttpError";
|
|
35
|
+
}
|
|
36
|
+
};
|
|
29
37
|
function isBrowserRuntime() {
|
|
30
38
|
return typeof window !== "undefined" && typeof document !== "undefined";
|
|
31
39
|
}
|
|
@@ -48,9 +56,17 @@ var CaldikHttpClient = class {
|
|
|
48
56
|
apiKey;
|
|
49
57
|
apiUrl;
|
|
50
58
|
storeId;
|
|
59
|
+
cookies = /* @__PURE__ */ new Map();
|
|
60
|
+
onSetCookie;
|
|
61
|
+
authenticationListeners = /* @__PURE__ */ new Set();
|
|
51
62
|
constructor(options = {}) {
|
|
52
63
|
this.apiKey = options.apiKey?.trim() || void 0;
|
|
53
64
|
this.apiUrl = options.apiUrl || defaultApiUrl(isBrowserRuntime());
|
|
65
|
+
this.onSetCookie = options.onSetCookie;
|
|
66
|
+
if (!isBrowserRuntime()) for (const part of (options.request?.headers.get("Cookie") || "").split(";")) {
|
|
67
|
+
const separator = part.indexOf("=");
|
|
68
|
+
if (separator > 0) this.cookies.set(part.slice(0, separator).trim(), part.slice(separator + 1).trim());
|
|
69
|
+
}
|
|
54
70
|
const configuredStoreId = options.storeId?.trim() || void 0;
|
|
55
71
|
if (this.apiKey && !this.apiKey.startsWith("sk_")) throw new Error("Caldik external integrations require a private sk_ API key");
|
|
56
72
|
if (configuredStoreId && !STORE_ID_PATTERN.test(configuredStoreId)) throw new Error("Caldik storeId is invalid");
|
|
@@ -60,6 +76,31 @@ var CaldikHttpClient = class {
|
|
|
60
76
|
storeContextId() {
|
|
61
77
|
return this.storeId || boundRuntimeStoreId();
|
|
62
78
|
}
|
|
79
|
+
hasCartSession() {
|
|
80
|
+
return this.cookies.has(`caldik_cart_session_${this.storeContextId()}`);
|
|
81
|
+
}
|
|
82
|
+
onCustomerAuthentication(listener) {
|
|
83
|
+
this.authenticationListeners.add(listener);
|
|
84
|
+
}
|
|
85
|
+
async customerAuthenticated() {
|
|
86
|
+
await Promise.allSettled([...this.authenticationListeners].map((listener) => listener()));
|
|
87
|
+
}
|
|
88
|
+
async captureCookies(response) {
|
|
89
|
+
for (const cookie of response.headers.getSetCookie()) {
|
|
90
|
+
const [pair = "", ...attributes] = cookie.split(";");
|
|
91
|
+
const separator = pair.indexOf("=");
|
|
92
|
+
if (separator < 1) continue;
|
|
93
|
+
const name = pair.slice(0, separator).trim();
|
|
94
|
+
if (attributes.some((attribute) => {
|
|
95
|
+
const [key, ...value] = attribute.trim().split("=");
|
|
96
|
+
if (key?.toLowerCase() === "max-age") return Number(value.join("=")) <= 0;
|
|
97
|
+
if (key?.toLowerCase() === "expires") return Date.parse(value.join("=")) <= Date.now();
|
|
98
|
+
return false;
|
|
99
|
+
})) this.cookies.delete(name);
|
|
100
|
+
else this.cookies.set(name, pair.slice(separator + 1).trim());
|
|
101
|
+
await this.onSetCookie?.(cookie);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
63
104
|
baseUrl() {
|
|
64
105
|
if (isBrowserRuntime()) {
|
|
65
106
|
const storeId = this.storeId;
|
|
@@ -76,8 +117,9 @@ var CaldikHttpClient = class {
|
|
|
76
117
|
async request(method, path, input, params, options) {
|
|
77
118
|
const url = new URL(`${this.baseUrl()}${path}`);
|
|
78
119
|
if (params) for (const [key, value] of Object.entries(params)) url.searchParams.set(key, String(value));
|
|
79
|
-
const headers = {};
|
|
120
|
+
const headers = { ...options?.headers };
|
|
80
121
|
if (!isBrowserRuntime()) headers["User-Agent"] = "CaldikClient/0.0.2";
|
|
122
|
+
if (!isBrowserRuntime() && options?.access === "public" && this.cookies.size) headers.Cookie = [...this.cookies].map(([name, value]) => `${name}=${value}`).join("; ");
|
|
81
123
|
if (this.apiKey) headers.Authorization = `Bearer ${this.apiKey}`;
|
|
82
124
|
else if (options?.access === "public" && (!isBrowserRuntime() || isLocalBrowserRuntime())) headers["X-Caldik-Public-Request"] = "1";
|
|
83
125
|
if (input !== void 0) headers["Content-Type"] = "application/json";
|
|
@@ -89,10 +131,10 @@ var CaldikHttpClient = class {
|
|
|
89
131
|
cache: "default",
|
|
90
132
|
credentials: "include"
|
|
91
133
|
});
|
|
134
|
+
if (!isBrowserRuntime() && options?.access === "public") await this.captureCookies(response);
|
|
92
135
|
if (!response.ok) {
|
|
93
136
|
const error = await response.clone().json().catch(() => null);
|
|
94
|
-
|
|
95
|
-
throw new Error(message);
|
|
137
|
+
throw new CaldikHttpError(typeof error?.message === "string" && error.message ? error.message : `${response.status} ${response.statusText}`.trim(), response.status);
|
|
96
138
|
}
|
|
97
139
|
return response.json();
|
|
98
140
|
}
|
|
@@ -207,7 +249,11 @@ var CustomerApi = class {
|
|
|
207
249
|
};
|
|
208
250
|
let pendingChallengeId;
|
|
209
251
|
async function login(input, options) {
|
|
210
|
-
if (typeof input !== "string" && "password" in input)
|
|
252
|
+
if (typeof input !== "string" && "password" in input) {
|
|
253
|
+
const response = await client.post("/customers/login/password", input, { access: "public" });
|
|
254
|
+
await client.customerAuthenticated();
|
|
255
|
+
return response;
|
|
256
|
+
}
|
|
211
257
|
const response = await client.post("/customers/login", typeof input === "string" ? {
|
|
212
258
|
email: input,
|
|
213
259
|
...options
|
|
@@ -225,6 +271,7 @@ verify: async (code, challengeId = pendingChallengeId) => {
|
|
|
225
271
|
challengeId
|
|
226
272
|
}, { access: "public" });
|
|
227
273
|
if (pendingChallengeId === challengeId) pendingChallengeId = void 0;
|
|
274
|
+
await client.customerAuthenticated();
|
|
228
275
|
return response;
|
|
229
276
|
} });
|
|
230
277
|
let pendingRegistrationId;
|
|
@@ -243,6 +290,7 @@ verify: async (code, challengeId = pendingRegistrationId) => {
|
|
|
243
290
|
challengeId
|
|
244
291
|
}, { access: "public" });
|
|
245
292
|
if (pendingRegistrationId === challengeId) pendingRegistrationId = void 0;
|
|
293
|
+
await client.customerAuthenticated();
|
|
246
294
|
return response;
|
|
247
295
|
} });
|
|
248
296
|
}
|
|
@@ -279,20 +327,18 @@ var DiscountsApi = class {
|
|
|
279
327
|
};
|
|
280
328
|
//#endregion
|
|
281
329
|
//#region src/cart.ts
|
|
282
|
-
const CART_VERSION = 1;
|
|
283
330
|
const MAX_QUANTITY = 999999;
|
|
284
|
-
function
|
|
285
|
-
return item.productVariantId ? `${item.productId}:${item.productVariantId}` : item.productId;
|
|
286
|
-
}
|
|
287
|
-
function quantity(value, allowZero = false) {
|
|
288
|
-
const normalized = value ?? 1;
|
|
331
|
+
function quantity(value = 1, allowZero = false) {
|
|
289
332
|
const minimum = allowZero ? 0 : 1;
|
|
290
|
-
if (!Number.isInteger(
|
|
291
|
-
return
|
|
333
|
+
if (!Number.isInteger(value) || value < minimum || value > MAX_QUANTITY) throw new Error(`Cart quantity must be an integer between ${minimum} and ${MAX_QUANTITY}`);
|
|
334
|
+
return value;
|
|
292
335
|
}
|
|
293
336
|
function cloneSnapshot(state) {
|
|
294
337
|
return {
|
|
295
|
-
items: state.items.map((item) => ({
|
|
338
|
+
items: state.items.map((item) => ({
|
|
339
|
+
...item,
|
|
340
|
+
...item.customFields ? { customFields: { ...item.customFields } } : {}
|
|
341
|
+
})),
|
|
296
342
|
discount: state.discount ? { ...state.discount } : null
|
|
297
343
|
};
|
|
298
344
|
}
|
|
@@ -303,140 +349,267 @@ function isCartItem(value) {
|
|
|
303
349
|
}
|
|
304
350
|
function isCartDiscount(value) {
|
|
305
351
|
if (!value || typeof value !== "object") return false;
|
|
306
|
-
|
|
307
|
-
|
|
352
|
+
return [
|
|
353
|
+
"code",
|
|
354
|
+
"percentage",
|
|
355
|
+
"subtotal",
|
|
356
|
+
"discountAmount",
|
|
357
|
+
"total",
|
|
358
|
+
"currency"
|
|
359
|
+
].every((key) => typeof value[key] === "string");
|
|
308
360
|
}
|
|
361
|
+
function isReference(value) {
|
|
362
|
+
if (!value || typeof value !== "object") return false;
|
|
363
|
+
const reference = value;
|
|
364
|
+
return typeof reference.id === "string" && /^CART_[a-f0-9]{32}$/.test(reference.id) && (reference.token == null || typeof reference.token === "string");
|
|
365
|
+
}
|
|
366
|
+
function commerceItems(items) {
|
|
367
|
+
return items.map(({ id: _id, ...item }) => item);
|
|
368
|
+
}
|
|
369
|
+
function discountFromQuote(quote) {
|
|
370
|
+
return {
|
|
371
|
+
...quote.discount,
|
|
372
|
+
subtotal: quote.subtotal,
|
|
373
|
+
discountAmount: quote.discountAmount,
|
|
374
|
+
total: quote.total,
|
|
375
|
+
currency: quote.currency
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
/** One persisted cart, automatically scoped to the current shopper session. */
|
|
309
379
|
var CartApi = class {
|
|
310
380
|
client;
|
|
311
|
-
|
|
312
|
-
version:
|
|
313
|
-
|
|
314
|
-
|
|
381
|
+
cache = {
|
|
382
|
+
version: 2,
|
|
383
|
+
reference: null,
|
|
384
|
+
snapshot: {
|
|
385
|
+
items: [],
|
|
386
|
+
discount: null
|
|
387
|
+
}
|
|
315
388
|
};
|
|
389
|
+
queue = Promise.resolve();
|
|
390
|
+
initialized = false;
|
|
316
391
|
constructor(client) {
|
|
317
392
|
this.client = client;
|
|
393
|
+
client.onCustomerAuthentication(() => {
|
|
394
|
+
this.readCache();
|
|
395
|
+
return this.initialized || this.cache.reference || this.cache.snapshot.items.length || client.hasCartSession() ? this.enqueue(async () => {
|
|
396
|
+
await this.resolve();
|
|
397
|
+
}) : Promise.resolve();
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
storage() {
|
|
401
|
+
try {
|
|
402
|
+
return typeof window !== "undefined" && typeof document !== "undefined" && typeof localStorage !== "undefined" ? localStorage : null;
|
|
403
|
+
} catch {
|
|
404
|
+
return null;
|
|
405
|
+
}
|
|
318
406
|
}
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
const
|
|
327
|
-
if (
|
|
328
|
-
const
|
|
329
|
-
if (
|
|
330
|
-
version:
|
|
407
|
+
key() {
|
|
408
|
+
return `caldik:cart:${this.client.storeContextId()}`;
|
|
409
|
+
}
|
|
410
|
+
readCache() {
|
|
411
|
+
const storage = this.storage();
|
|
412
|
+
if (!storage) return;
|
|
413
|
+
try {
|
|
414
|
+
const parsed = JSON.parse(storage.getItem(this.key()) || "null");
|
|
415
|
+
if (parsed?.version === 2) {
|
|
416
|
+
const snapshot = parsed.snapshot;
|
|
417
|
+
if (snapshot && Array.isArray(snapshot.items) && snapshot.items.every(isCartItem) && (parsed.reference === null || isReference(parsed.reference))) this.cache = {
|
|
418
|
+
version: 2,
|
|
419
|
+
reference: parsed.reference,
|
|
420
|
+
snapshot: cloneSnapshot({
|
|
421
|
+
items: snapshot.items,
|
|
422
|
+
discount: isCartDiscount(snapshot.discount) ? snapshot.discount : null
|
|
423
|
+
})
|
|
424
|
+
};
|
|
425
|
+
} else if (parsed?.version === 1 && Array.isArray(parsed.items) && parsed.items.every(isCartItem)) this.cache = {
|
|
426
|
+
version: 2,
|
|
427
|
+
reference: null,
|
|
428
|
+
snapshot: cloneSnapshot({
|
|
331
429
|
items: parsed.items,
|
|
332
430
|
discount: isCartDiscount(parsed.discount) ? parsed.discount : null
|
|
333
|
-
}
|
|
431
|
+
})
|
|
432
|
+
};
|
|
433
|
+
if (!this.cache.reference) {
|
|
434
|
+
const legacy = JSON.parse(storage.getItem(`caldik:server-cart:${this.client.storeContextId()}`) || "null");
|
|
435
|
+
if (isReference(legacy)) this.cache.reference = legacy;
|
|
334
436
|
}
|
|
335
437
|
} catch {}
|
|
336
|
-
return this.memory;
|
|
337
438
|
}
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
const key = this.storageKey();
|
|
344
|
-
if (key && typeof localStorage !== "undefined") try {
|
|
345
|
-
localStorage.setItem(key, JSON.stringify(this.memory));
|
|
439
|
+
persistCache() {
|
|
440
|
+
try {
|
|
441
|
+
const storage = this.storage();
|
|
442
|
+
storage?.setItem(this.key(), JSON.stringify(this.cache));
|
|
443
|
+
storage?.removeItem(`caldik:server-cart:${this.client.storeContextId()}`);
|
|
346
444
|
} catch {}
|
|
347
|
-
|
|
445
|
+
}
|
|
446
|
+
write(cart, token = this.cache.reference?.token, discount = null) {
|
|
447
|
+
this.cache = {
|
|
448
|
+
version: 2,
|
|
449
|
+
reference: {
|
|
450
|
+
id: cart.id,
|
|
451
|
+
token: cart.customerId ? null : token
|
|
452
|
+
},
|
|
453
|
+
snapshot: cloneSnapshot({
|
|
454
|
+
items: cart.items,
|
|
455
|
+
discount
|
|
456
|
+
})
|
|
457
|
+
};
|
|
458
|
+
this.persistCache();
|
|
459
|
+
return cloneSnapshot(this.cache.snapshot);
|
|
460
|
+
}
|
|
461
|
+
enqueue(operation) {
|
|
462
|
+
const run = () => {
|
|
463
|
+
const locks = this.storage() && typeof navigator !== "undefined" ? navigator.locks : void 0;
|
|
464
|
+
return locks ? locks.request(this.key(), operation) : operation();
|
|
465
|
+
};
|
|
466
|
+
const result = this.queue.then(run, run);
|
|
467
|
+
this.queue = result.catch(() => void 0);
|
|
468
|
+
return result;
|
|
469
|
+
}
|
|
470
|
+
options() {
|
|
471
|
+
return {
|
|
472
|
+
access: "public",
|
|
473
|
+
...this.cache.reference?.token ? { headers: { "X-Caldik-Cart-Token": this.cache.reference.token } } : {}
|
|
474
|
+
};
|
|
475
|
+
}
|
|
476
|
+
async resolve(checkout = false, seed = true) {
|
|
477
|
+
this.readCache();
|
|
478
|
+
const input = this.cache.reference ? { cartId: this.cache.reference.id } : seed && this.cache.snapshot.items.length ? {
|
|
479
|
+
items: commerceItems(this.cache.snapshot.items),
|
|
480
|
+
discountCode: this.cache.snapshot.discount?.code
|
|
481
|
+
} : {};
|
|
482
|
+
const response = await this.client.post("/carts/session", {
|
|
483
|
+
...input,
|
|
484
|
+
...checkout ? { checkout: true } : {}
|
|
485
|
+
}, this.options());
|
|
486
|
+
this.initialized = true;
|
|
487
|
+
this.write(response.cart, response.token ?? null);
|
|
488
|
+
if (!checkout && response.cart.status === "checked_out") {
|
|
489
|
+
this.reset();
|
|
490
|
+
return this.resolve(false, false);
|
|
491
|
+
}
|
|
492
|
+
return response.cart;
|
|
493
|
+
}
|
|
494
|
+
reset() {
|
|
495
|
+
this.cache = {
|
|
496
|
+
version: 2,
|
|
497
|
+
reference: null,
|
|
498
|
+
snapshot: {
|
|
499
|
+
items: [],
|
|
500
|
+
discount: null
|
|
501
|
+
}
|
|
502
|
+
};
|
|
503
|
+
this.persistCache();
|
|
504
|
+
}
|
|
505
|
+
async mutate(operation, seed = true) {
|
|
506
|
+
let cart = await this.resolve(false, seed);
|
|
507
|
+
let response;
|
|
508
|
+
try {
|
|
509
|
+
response = await operation(cart);
|
|
510
|
+
} catch (error) {
|
|
511
|
+
if (!(error instanceof CaldikHttpError) || error.status !== 409) throw error;
|
|
512
|
+
const refreshed = await this.resolve();
|
|
513
|
+
if (refreshed.id !== cart.id || refreshed.status !== "active") throw error;
|
|
514
|
+
cart = refreshed;
|
|
515
|
+
response = await operation(cart);
|
|
516
|
+
}
|
|
517
|
+
return this.write(response.cart);
|
|
518
|
+
}
|
|
519
|
+
save(cart, items, discountCode) {
|
|
520
|
+
return this.client.post(`/carts/${cart.id}`, {
|
|
521
|
+
items: commerceItems(items),
|
|
522
|
+
version: cart.version,
|
|
523
|
+
discountCode
|
|
524
|
+
}, this.options());
|
|
525
|
+
}
|
|
526
|
+
quote(cart, code) {
|
|
527
|
+
if (!cart.items.length) throw new Error("Cannot check a discount on an empty cart");
|
|
528
|
+
return this.client.post("/discounts/check", {
|
|
529
|
+
code,
|
|
530
|
+
items: commerceItems(cart.items)
|
|
531
|
+
}, { access: "public" });
|
|
348
532
|
}
|
|
349
533
|
get() {
|
|
350
|
-
return
|
|
534
|
+
return this.enqueue(async () => {
|
|
535
|
+
let cart = await this.resolve();
|
|
536
|
+
let discount = null;
|
|
537
|
+
if (cart.discountCode) try {
|
|
538
|
+
discount = discountFromQuote(await this.quote(cart, cart.discountCode));
|
|
539
|
+
} catch (error) {
|
|
540
|
+
if (!(error instanceof CaldikHttpError) || ![
|
|
541
|
+
400,
|
|
542
|
+
404,
|
|
543
|
+
422
|
|
544
|
+
].includes(error.status)) throw error;
|
|
545
|
+
cart = (await this.save(cart, cart.items, null)).cart;
|
|
546
|
+
}
|
|
547
|
+
return this.write(cart, this.cache.reference?.token, discount);
|
|
548
|
+
});
|
|
351
549
|
}
|
|
352
550
|
add(item) {
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
551
|
+
return this.enqueue(async () => {
|
|
552
|
+
if (!item.productId.trim()) throw new Error("Cart productId is required");
|
|
553
|
+
const normalized = {
|
|
554
|
+
...item,
|
|
555
|
+
productId: item.productId.trim(),
|
|
556
|
+
quantity: quantity(item.quantity)
|
|
557
|
+
};
|
|
558
|
+
return this.mutate((cart) => this.client.post(`/carts/${cart.id}/items`, {
|
|
559
|
+
item: normalized,
|
|
560
|
+
version: cart.version
|
|
561
|
+
}, this.options()));
|
|
360
562
|
});
|
|
361
|
-
const existing = state.items.find((entry) => entry.id === id);
|
|
362
|
-
const addedQuantity = quantity(item.quantity);
|
|
363
|
-
if (existing) existing.quantity = quantity(existing.quantity + addedQuantity);
|
|
364
|
-
else state.items.push({
|
|
365
|
-
id,
|
|
366
|
-
productId,
|
|
367
|
-
...productVariantId ? { productVariantId } : {},
|
|
368
|
-
quantity: addedQuantity
|
|
369
|
-
});
|
|
370
|
-
state.discount = null;
|
|
371
|
-
return this.write(state);
|
|
372
563
|
}
|
|
373
564
|
updateQuantity(itemId, nextQuantity) {
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
565
|
+
return this.enqueue(async () => {
|
|
566
|
+
const normalized = quantity(nextQuantity, true);
|
|
567
|
+
return this.mutate((cart) => {
|
|
568
|
+
if (!cart.items.some((item) => item.id === itemId)) throw new Error("Cart item not found");
|
|
569
|
+
const items = cart.items.map((item) => item.id === itemId ? {
|
|
570
|
+
...item,
|
|
571
|
+
quantity: normalized
|
|
572
|
+
} : item).filter((item) => item.quantity > 0);
|
|
573
|
+
return this.save(cart, items, null);
|
|
574
|
+
});
|
|
575
|
+
});
|
|
382
576
|
}
|
|
383
577
|
remove(itemId) {
|
|
384
|
-
|
|
385
|
-
const items = state.items.filter((item) => item.id !== itemId);
|
|
386
|
-
if (items.length === state.items.length) throw new Error("Cart item not found");
|
|
387
|
-
return this.write({
|
|
388
|
-
items,
|
|
389
|
-
discount: null
|
|
390
|
-
});
|
|
578
|
+
return this.updateQuantity(itemId, 0);
|
|
391
579
|
}
|
|
392
580
|
clear() {
|
|
393
|
-
return this.
|
|
394
|
-
|
|
395
|
-
|
|
581
|
+
return this.enqueue(() => this.mutate((cart) => this.save(cart, [], null), false));
|
|
582
|
+
}
|
|
583
|
+
checkDiscount(code) {
|
|
584
|
+
return this.enqueue(async () => this.quote(await this.resolve(), code));
|
|
585
|
+
}
|
|
586
|
+
applyDiscount(code) {
|
|
587
|
+
return this.enqueue(async () => {
|
|
588
|
+
let quote;
|
|
589
|
+
const snapshot = await this.mutate(async (cart) => {
|
|
590
|
+
quote = await this.quote(cart, code);
|
|
591
|
+
return this.save(cart, cart.items, quote.discount.code);
|
|
592
|
+
});
|
|
593
|
+
snapshot.discount = discountFromQuote(quote);
|
|
594
|
+
this.cache.snapshot = cloneSnapshot(snapshot);
|
|
595
|
+
this.persistCache();
|
|
596
|
+
return snapshot;
|
|
396
597
|
});
|
|
397
598
|
}
|
|
398
|
-
commerceItems() {
|
|
399
|
-
return this.get().items.map(({ productId, productVariantId, quantity }) => ({
|
|
400
|
-
productId,
|
|
401
|
-
...productVariantId ? { productVariantId } : {},
|
|
402
|
-
quantity
|
|
403
|
-
}));
|
|
404
|
-
}
|
|
405
|
-
async checkDiscount(code) {
|
|
406
|
-
const items = this.commerceItems();
|
|
407
|
-
if (items.length === 0) throw new Error("Cannot check a discount on an empty cart");
|
|
408
|
-
return this.client.post("/discounts/check", {
|
|
409
|
-
code,
|
|
410
|
-
items
|
|
411
|
-
}, { access: "public" });
|
|
412
|
-
}
|
|
413
|
-
async applyDiscount(code) {
|
|
414
|
-
const quote = await this.checkDiscount(code);
|
|
415
|
-
const state = this.get();
|
|
416
|
-
state.discount = {
|
|
417
|
-
...quote.discount,
|
|
418
|
-
subtotal: quote.subtotal,
|
|
419
|
-
discountAmount: quote.discountAmount,
|
|
420
|
-
total: quote.total,
|
|
421
|
-
currency: quote.currency
|
|
422
|
-
};
|
|
423
|
-
return this.write(state);
|
|
424
|
-
}
|
|
425
599
|
removeDiscount() {
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
return response;
|
|
600
|
+
return this.enqueue(() => this.mutate((cart) => this.save(cart, cart.items, null)));
|
|
601
|
+
}
|
|
602
|
+
checkout(input = {}) {
|
|
603
|
+
return this.enqueue(async () => {
|
|
604
|
+
const cart = await this.resolve(true);
|
|
605
|
+
if (!cart.items.length) throw new Error("Cannot checkout an empty cart");
|
|
606
|
+
const response = await this.client.post(`/carts/${cart.id}/checkout`, {
|
|
607
|
+
...input,
|
|
608
|
+
version: cart.version
|
|
609
|
+
}, this.options());
|
|
610
|
+
this.reset();
|
|
611
|
+
return response;
|
|
612
|
+
});
|
|
440
613
|
}
|
|
441
614
|
};
|
|
442
615
|
//#endregion
|
|
@@ -497,7 +670,9 @@ var CaldikClient = class {
|
|
|
497
670
|
const client = new CaldikHttpClient({
|
|
498
671
|
apiKey: options.apiKey,
|
|
499
672
|
apiUrl: options.apiUrl,
|
|
500
|
-
storeId: options.storeId
|
|
673
|
+
storeId: options.storeId,
|
|
674
|
+
request: options.request,
|
|
675
|
+
onSetCookie: options.onSetCookie
|
|
501
676
|
});
|
|
502
677
|
this.categories = new CategoriesApi(client);
|
|
503
678
|
this.products = new ProductsApi(client);
|