@nymbalhq/sdk 0.0.0-stage → 0.2.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/dist/index.d.ts +372 -0
- package/dist/index.js +906 -0
- package/dist/index.js.map +1 -0
- package/package.json +38 -3
- package/README.md +0 -3
package/dist/index.js
ADDED
|
@@ -0,0 +1,906 @@
|
|
|
1
|
+
// src/adapter/fetch-client.ts
|
|
2
|
+
var NymbalApiError = class extends Error {
|
|
3
|
+
constructor(code, message, status, context) {
|
|
4
|
+
super(message);
|
|
5
|
+
this.code = code;
|
|
6
|
+
this.status = status;
|
|
7
|
+
this.context = context;
|
|
8
|
+
this.name = "NymbalApiError";
|
|
9
|
+
}
|
|
10
|
+
code;
|
|
11
|
+
status;
|
|
12
|
+
context;
|
|
13
|
+
};
|
|
14
|
+
var CSRF_HEADER = "x-csrf-token";
|
|
15
|
+
var UNSAFE_METHODS = /* @__PURE__ */ new Set(["POST", "PUT", "PATCH", "DELETE"]);
|
|
16
|
+
var FetchClient = class {
|
|
17
|
+
baseUrl;
|
|
18
|
+
credentials;
|
|
19
|
+
getAccessToken;
|
|
20
|
+
getCartToken;
|
|
21
|
+
onUnauthorized;
|
|
22
|
+
handlingUnauthorized = false;
|
|
23
|
+
/** The token `GET /api/csrf` answered: the API's `nymbal.csrf` cookie, which this origin may not be able to read. */
|
|
24
|
+
csrfToken = null;
|
|
25
|
+
constructor(options) {
|
|
26
|
+
this.baseUrl = options.baseUrl.replace(/\/+$/, "");
|
|
27
|
+
this.credentials = options.credentials ?? "include";
|
|
28
|
+
this.getAccessToken = options.getAccessToken ?? (() => null);
|
|
29
|
+
this.getCartToken = options.getCartToken;
|
|
30
|
+
this.onUnauthorized = options.onUnauthorized;
|
|
31
|
+
}
|
|
32
|
+
async request(method, path, body) {
|
|
33
|
+
return (await this.requestWithMeta(method, path, body)).data;
|
|
34
|
+
}
|
|
35
|
+
/** Like `request`, keeping the envelope's `meta` (pagination cursors live there). */
|
|
36
|
+
async requestWithMeta(method, path, body) {
|
|
37
|
+
let response = await this.doFetch(method, path, body);
|
|
38
|
+
if (await this.isCsrfRejection(response)) {
|
|
39
|
+
this.csrfToken = null;
|
|
40
|
+
response = await this.doFetch(method, path, body);
|
|
41
|
+
}
|
|
42
|
+
if (response.status === 401 && this.onUnauthorized && !this.handlingUnauthorized) {
|
|
43
|
+
this.handlingUnauthorized = true;
|
|
44
|
+
try {
|
|
45
|
+
await this.onUnauthorized();
|
|
46
|
+
} finally {
|
|
47
|
+
this.handlingUnauthorized = false;
|
|
48
|
+
}
|
|
49
|
+
const retry = await this.doFetch(method, path, body);
|
|
50
|
+
return this.parseResponse(retry);
|
|
51
|
+
}
|
|
52
|
+
return this.parseResponse(response);
|
|
53
|
+
}
|
|
54
|
+
csrf() {
|
|
55
|
+
this.csrfToken ??= this.fetchCsrfToken().catch((err) => {
|
|
56
|
+
this.csrfToken = null;
|
|
57
|
+
throw err;
|
|
58
|
+
});
|
|
59
|
+
return this.csrfToken;
|
|
60
|
+
}
|
|
61
|
+
async fetchCsrfToken() {
|
|
62
|
+
const response = await globalThis.fetch(`${this.baseUrl}/api/csrf`, {
|
|
63
|
+
method: "GET",
|
|
64
|
+
credentials: this.credentials
|
|
65
|
+
});
|
|
66
|
+
const { data } = await this.parseResponse(response);
|
|
67
|
+
return data.token;
|
|
68
|
+
}
|
|
69
|
+
async isCsrfRejection(response) {
|
|
70
|
+
if (response.status !== 403) return false;
|
|
71
|
+
const text = await response.clone().text();
|
|
72
|
+
try {
|
|
73
|
+
return JSON.parse(text).error?.code === "csrf.invalid";
|
|
74
|
+
} catch {
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
async doFetch(method, path, body) {
|
|
79
|
+
const headers = {};
|
|
80
|
+
const cookies = [];
|
|
81
|
+
if (UNSAFE_METHODS.has(method.toUpperCase())) {
|
|
82
|
+
const csrf = await this.csrf();
|
|
83
|
+
headers[CSRF_HEADER] = csrf;
|
|
84
|
+
cookies.push(`nymbal.csrf=${csrf}`);
|
|
85
|
+
}
|
|
86
|
+
if (body !== void 0) {
|
|
87
|
+
headers["Content-Type"] = "application/json";
|
|
88
|
+
}
|
|
89
|
+
const token = this.getAccessToken();
|
|
90
|
+
if (token) {
|
|
91
|
+
headers["Authorization"] = `Bearer ${token}`;
|
|
92
|
+
}
|
|
93
|
+
const cartToken = this.getCartToken?.();
|
|
94
|
+
if (cartToken) {
|
|
95
|
+
cookies.unshift(`nymbal.cart=${cartToken}`);
|
|
96
|
+
}
|
|
97
|
+
if (cookies.length > 0) {
|
|
98
|
+
headers["Cookie"] = cookies.join("; ");
|
|
99
|
+
}
|
|
100
|
+
return globalThis.fetch(`${this.baseUrl}${path}`, {
|
|
101
|
+
method,
|
|
102
|
+
headers,
|
|
103
|
+
credentials: this.credentials,
|
|
104
|
+
...body !== void 0 && { body: JSON.stringify(body) }
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
async parseResponse(response) {
|
|
108
|
+
const text = await response.text();
|
|
109
|
+
if (!text) {
|
|
110
|
+
return { data: void 0, meta: {} };
|
|
111
|
+
}
|
|
112
|
+
const envelope = JSON.parse(text);
|
|
113
|
+
if (envelope.error) {
|
|
114
|
+
throw new NymbalApiError(
|
|
115
|
+
envelope.error.code,
|
|
116
|
+
envelope.error.message,
|
|
117
|
+
response.status,
|
|
118
|
+
envelope.error.context
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
return { data: envelope.data, meta: envelope.meta ?? {} };
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
// src/adapter/nymbal-api-adapter.ts
|
|
126
|
+
function createNymbalApiAdapter(fetchClient) {
|
|
127
|
+
return {
|
|
128
|
+
cart: {
|
|
129
|
+
get() {
|
|
130
|
+
return fetchClient.request("GET", "/api/cart");
|
|
131
|
+
},
|
|
132
|
+
addItem(variantId, qty) {
|
|
133
|
+
return fetchClient.request("POST", "/api/cart/items", { variantId, qty });
|
|
134
|
+
},
|
|
135
|
+
removeItem(variantId) {
|
|
136
|
+
return fetchClient.request(
|
|
137
|
+
"DELETE",
|
|
138
|
+
`/api/cart/items/${encodeURIComponent(variantId)}`
|
|
139
|
+
);
|
|
140
|
+
},
|
|
141
|
+
updateQuantity(variantId, qty) {
|
|
142
|
+
return fetchClient.request(
|
|
143
|
+
"PATCH",
|
|
144
|
+
`/api/cart/items/${encodeURIComponent(variantId)}`,
|
|
145
|
+
{ qty }
|
|
146
|
+
);
|
|
147
|
+
},
|
|
148
|
+
clear() {
|
|
149
|
+
return fetchClient.request("DELETE", "/api/cart");
|
|
150
|
+
}
|
|
151
|
+
},
|
|
152
|
+
products: {
|
|
153
|
+
getBySlug(slug) {
|
|
154
|
+
return fetchClient.request(
|
|
155
|
+
"GET",
|
|
156
|
+
`/api/products/${encodeURIComponent(slug)}`
|
|
157
|
+
);
|
|
158
|
+
},
|
|
159
|
+
list(params) {
|
|
160
|
+
const search = new URLSearchParams();
|
|
161
|
+
if (params?.limit !== void 0) search.set("limit", String(params.limit));
|
|
162
|
+
if (params?.cursor !== void 0) search.set("cursor", params.cursor);
|
|
163
|
+
if (params?.category !== void 0) search.set("category", params.category);
|
|
164
|
+
if (params?.sort !== void 0) search.set("sort", params.sort);
|
|
165
|
+
const qs = search.toString();
|
|
166
|
+
return fetchClient.request("GET", `/api/products${qs ? `?${qs}` : ""}`);
|
|
167
|
+
},
|
|
168
|
+
search(query, params) {
|
|
169
|
+
const search = new URLSearchParams();
|
|
170
|
+
search.set("q", query);
|
|
171
|
+
if (params?.limit !== void 0) search.set("limit", String(params.limit));
|
|
172
|
+
if (params?.cursor !== void 0) search.set("cursor", params.cursor);
|
|
173
|
+
return fetchClient.request(
|
|
174
|
+
"GET",
|
|
175
|
+
`/api/products/search?${search.toString()}`
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
},
|
|
179
|
+
checkout: {
|
|
180
|
+
create(details) {
|
|
181
|
+
return fetchClient.request("POST", "/api/checkout", details);
|
|
182
|
+
}
|
|
183
|
+
},
|
|
184
|
+
customers: {
|
|
185
|
+
register(params) {
|
|
186
|
+
return fetchClient.request("POST", "/api/auth/register", params);
|
|
187
|
+
},
|
|
188
|
+
login(email, password) {
|
|
189
|
+
return fetchClient.request("POST", "/api/auth/login", { email, password });
|
|
190
|
+
},
|
|
191
|
+
async logout() {
|
|
192
|
+
await fetchClient.request("POST", "/api/auth/logout");
|
|
193
|
+
},
|
|
194
|
+
refresh() {
|
|
195
|
+
return fetchClient.request(
|
|
196
|
+
"POST",
|
|
197
|
+
"/api/auth/refresh"
|
|
198
|
+
);
|
|
199
|
+
},
|
|
200
|
+
getProfile() {
|
|
201
|
+
return fetchClient.request("GET", "/api/customers/me");
|
|
202
|
+
},
|
|
203
|
+
updateProfile(updates) {
|
|
204
|
+
return fetchClient.request("PATCH", "/api/customers/me", updates);
|
|
205
|
+
},
|
|
206
|
+
listAddresses() {
|
|
207
|
+
return fetchClient.request("GET", "/api/customers/me/addresses");
|
|
208
|
+
},
|
|
209
|
+
addAddress(address) {
|
|
210
|
+
return fetchClient.request("POST", "/api/customers/me/addresses", address);
|
|
211
|
+
},
|
|
212
|
+
async removeAddress(id) {
|
|
213
|
+
await fetchClient.request(
|
|
214
|
+
"DELETE",
|
|
215
|
+
`/api/customers/me/addresses/${encodeURIComponent(id)}`
|
|
216
|
+
);
|
|
217
|
+
},
|
|
218
|
+
async listOrders(params) {
|
|
219
|
+
const qs = params?.cursor !== void 0 ? `?cursor=${encodeURIComponent(params.cursor)}` : "";
|
|
220
|
+
const { data, meta } = await fetchClient.requestWithMeta("GET", `/api/orders${qs}`);
|
|
221
|
+
const nextCursor = typeof meta.nextCursor === "string" ? meta.nextCursor : null;
|
|
222
|
+
return { items: data ?? [], nextCursor };
|
|
223
|
+
},
|
|
224
|
+
getOrder(orderNumber) {
|
|
225
|
+
return fetchClient.request("GET", `/api/orders/${encodeURIComponent(orderNumber)}`);
|
|
226
|
+
}
|
|
227
|
+
},
|
|
228
|
+
reviews: {
|
|
229
|
+
getForProduct(productId, params) {
|
|
230
|
+
const search = new URLSearchParams();
|
|
231
|
+
if (params?.limit !== void 0) search.set("limit", String(params.limit));
|
|
232
|
+
if (params?.cursor !== void 0) search.set("cursor", params.cursor);
|
|
233
|
+
const qs = search.toString();
|
|
234
|
+
return fetchClient.request(
|
|
235
|
+
"GET",
|
|
236
|
+
`/api/products/${encodeURIComponent(productId)}/reviews${qs ? `?${qs}` : ""}`
|
|
237
|
+
);
|
|
238
|
+
},
|
|
239
|
+
submit(review) {
|
|
240
|
+
return fetchClient.request("POST", "/api/reviews", review);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// src/auth/token-manager.ts
|
|
247
|
+
var DEFAULT_STORAGE_KEY = "nymbal.auth.accessToken";
|
|
248
|
+
function createTokenManager(options) {
|
|
249
|
+
const storageMode = options?.storage ?? "memory";
|
|
250
|
+
const storageKey = options?.storageKey ?? DEFAULT_STORAGE_KEY;
|
|
251
|
+
let memoryToken = null;
|
|
252
|
+
let refreshTimer = null;
|
|
253
|
+
let refreshFn = null;
|
|
254
|
+
function getAccessToken() {
|
|
255
|
+
if (storageMode === "localStorage" && typeof globalThis.localStorage !== "undefined") {
|
|
256
|
+
return globalThis.localStorage.getItem(storageKey);
|
|
257
|
+
}
|
|
258
|
+
return memoryToken;
|
|
259
|
+
}
|
|
260
|
+
function setAccessToken(token) {
|
|
261
|
+
memoryToken = token;
|
|
262
|
+
if (storageMode === "localStorage" && typeof globalThis.localStorage !== "undefined") {
|
|
263
|
+
if (token) {
|
|
264
|
+
globalThis.localStorage.setItem(storageKey, token);
|
|
265
|
+
} else {
|
|
266
|
+
globalThis.localStorage.removeItem(storageKey);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
function setTokens(accessToken, expiresIn) {
|
|
271
|
+
setAccessToken(accessToken);
|
|
272
|
+
scheduleAutoRefresh(expiresIn);
|
|
273
|
+
}
|
|
274
|
+
function clear() {
|
|
275
|
+
setAccessToken(null);
|
|
276
|
+
if (refreshTimer !== null) {
|
|
277
|
+
clearTimeout(refreshTimer);
|
|
278
|
+
refreshTimer = null;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
function scheduleAutoRefresh(expiresIn) {
|
|
282
|
+
if (refreshTimer !== null) {
|
|
283
|
+
clearTimeout(refreshTimer);
|
|
284
|
+
}
|
|
285
|
+
const delayMs = Math.max(1e3, expiresIn * 0.8 * 1e3);
|
|
286
|
+
refreshTimer = setTimeout(async () => {
|
|
287
|
+
if (!refreshFn) return;
|
|
288
|
+
try {
|
|
289
|
+
const result = await refreshFn();
|
|
290
|
+
setTokens(result.accessToken, result.expiresIn);
|
|
291
|
+
} catch (err) {
|
|
292
|
+
console.error("[TokenManager] auto-refresh failed, clearing session", err);
|
|
293
|
+
clear();
|
|
294
|
+
}
|
|
295
|
+
}, delayMs);
|
|
296
|
+
}
|
|
297
|
+
function scheduleRefresh(fn) {
|
|
298
|
+
refreshFn = fn;
|
|
299
|
+
}
|
|
300
|
+
function destroy() {
|
|
301
|
+
clear();
|
|
302
|
+
refreshFn = null;
|
|
303
|
+
}
|
|
304
|
+
return { getAccessToken, setTokens, clear, scheduleRefresh, destroy };
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// src/store/create-store.ts
|
|
308
|
+
function createStore(initialState) {
|
|
309
|
+
let state = initialState;
|
|
310
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
311
|
+
function getState() {
|
|
312
|
+
return state;
|
|
313
|
+
}
|
|
314
|
+
function setState(partial) {
|
|
315
|
+
const next = typeof partial === "function" ? partial(state) : partial;
|
|
316
|
+
state = { ...state, ...next };
|
|
317
|
+
for (const listener of listeners) {
|
|
318
|
+
listener();
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
function subscribe(listener) {
|
|
322
|
+
listeners.add(listener);
|
|
323
|
+
return () => {
|
|
324
|
+
listeners.delete(listener);
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
function destroy() {
|
|
328
|
+
listeners.clear();
|
|
329
|
+
}
|
|
330
|
+
return { getState, setState, subscribe, destroy };
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// src/store/cart-store.ts
|
|
334
|
+
function deriveItemCount(items) {
|
|
335
|
+
let count = 0;
|
|
336
|
+
for (const item of items) {
|
|
337
|
+
count += item.qty;
|
|
338
|
+
}
|
|
339
|
+
return count;
|
|
340
|
+
}
|
|
341
|
+
var INITIAL_STATE = {
|
|
342
|
+
items: [],
|
|
343
|
+
subtotalMinor: 0,
|
|
344
|
+
currency: "USD",
|
|
345
|
+
itemCount: 0,
|
|
346
|
+
loading: false,
|
|
347
|
+
error: null
|
|
348
|
+
};
|
|
349
|
+
function createCartStore(adapter) {
|
|
350
|
+
const store = createStore(INITIAL_STATE);
|
|
351
|
+
async function load() {
|
|
352
|
+
store.setState({ loading: true, error: null });
|
|
353
|
+
try {
|
|
354
|
+
const cart = await adapter.cart.get();
|
|
355
|
+
store.setState({
|
|
356
|
+
items: cart.items,
|
|
357
|
+
subtotalMinor: cart.subtotalMinor,
|
|
358
|
+
currency: cart.currency,
|
|
359
|
+
itemCount: deriveItemCount(cart.items),
|
|
360
|
+
loading: false
|
|
361
|
+
});
|
|
362
|
+
} catch (err) {
|
|
363
|
+
store.setState({
|
|
364
|
+
loading: false,
|
|
365
|
+
error: err instanceof Error ? err.message : String(err)
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
async function addItem(variantId, qty = 1) {
|
|
370
|
+
store.setState({ loading: true, error: null });
|
|
371
|
+
try {
|
|
372
|
+
const cart = await adapter.cart.addItem(variantId, qty);
|
|
373
|
+
store.setState({
|
|
374
|
+
items: cart.items,
|
|
375
|
+
subtotalMinor: cart.subtotalMinor,
|
|
376
|
+
currency: cart.currency,
|
|
377
|
+
itemCount: deriveItemCount(cart.items),
|
|
378
|
+
loading: false
|
|
379
|
+
});
|
|
380
|
+
} catch (err) {
|
|
381
|
+
store.setState({
|
|
382
|
+
loading: false,
|
|
383
|
+
error: err instanceof Error ? err.message : String(err)
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
async function removeItem(variantId) {
|
|
388
|
+
store.setState({ loading: true, error: null });
|
|
389
|
+
try {
|
|
390
|
+
const cart = await adapter.cart.removeItem(variantId);
|
|
391
|
+
store.setState({
|
|
392
|
+
items: cart.items,
|
|
393
|
+
subtotalMinor: cart.subtotalMinor,
|
|
394
|
+
currency: cart.currency,
|
|
395
|
+
itemCount: deriveItemCount(cart.items),
|
|
396
|
+
loading: false
|
|
397
|
+
});
|
|
398
|
+
} catch (err) {
|
|
399
|
+
store.setState({
|
|
400
|
+
loading: false,
|
|
401
|
+
error: err instanceof Error ? err.message : String(err)
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
async function updateQuantity(variantId, qty) {
|
|
406
|
+
store.setState({ loading: true, error: null });
|
|
407
|
+
try {
|
|
408
|
+
const cart = await adapter.cart.updateQuantity(variantId, qty);
|
|
409
|
+
store.setState({
|
|
410
|
+
items: cart.items,
|
|
411
|
+
subtotalMinor: cart.subtotalMinor,
|
|
412
|
+
currency: cart.currency,
|
|
413
|
+
itemCount: deriveItemCount(cart.items),
|
|
414
|
+
loading: false
|
|
415
|
+
});
|
|
416
|
+
} catch (err) {
|
|
417
|
+
store.setState({
|
|
418
|
+
loading: false,
|
|
419
|
+
error: err instanceof Error ? err.message : String(err)
|
|
420
|
+
});
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
async function clear() {
|
|
424
|
+
store.setState({ loading: true, error: null });
|
|
425
|
+
try {
|
|
426
|
+
await adapter.cart.clear();
|
|
427
|
+
store.setState({
|
|
428
|
+
items: [],
|
|
429
|
+
subtotalMinor: 0,
|
|
430
|
+
itemCount: 0,
|
|
431
|
+
loading: false
|
|
432
|
+
});
|
|
433
|
+
} catch (err) {
|
|
434
|
+
store.setState({
|
|
435
|
+
loading: false,
|
|
436
|
+
error: err instanceof Error ? err.message : String(err)
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
return {
|
|
441
|
+
getState: store.getState,
|
|
442
|
+
subscribe: store.subscribe,
|
|
443
|
+
addItem,
|
|
444
|
+
removeItem,
|
|
445
|
+
updateQuantity,
|
|
446
|
+
clear,
|
|
447
|
+
load,
|
|
448
|
+
destroy: store.destroy
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// src/store/product-store.ts
|
|
453
|
+
var INITIAL_STATE2 = {
|
|
454
|
+
product: null,
|
|
455
|
+
selectedVariant: null,
|
|
456
|
+
loading: false,
|
|
457
|
+
error: null
|
|
458
|
+
};
|
|
459
|
+
function createProductStore(adapter) {
|
|
460
|
+
const store = createStore(INITIAL_STATE2);
|
|
461
|
+
async function loadBySlug(slug) {
|
|
462
|
+
store.setState({ loading: true, error: null });
|
|
463
|
+
try {
|
|
464
|
+
const product = await adapter.products.getBySlug(slug);
|
|
465
|
+
const firstVariant = product.variants[0] ?? null;
|
|
466
|
+
store.setState({
|
|
467
|
+
product,
|
|
468
|
+
selectedVariant: firstVariant,
|
|
469
|
+
loading: false
|
|
470
|
+
});
|
|
471
|
+
} catch (err) {
|
|
472
|
+
store.setState({
|
|
473
|
+
loading: false,
|
|
474
|
+
error: err instanceof Error ? err.message : String(err)
|
|
475
|
+
});
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
function selectVariant(variant) {
|
|
479
|
+
store.setState({ selectedVariant: variant });
|
|
480
|
+
}
|
|
481
|
+
return {
|
|
482
|
+
getState: store.getState,
|
|
483
|
+
subscribe: store.subscribe,
|
|
484
|
+
loadBySlug,
|
|
485
|
+
selectVariant,
|
|
486
|
+
destroy: store.destroy
|
|
487
|
+
};
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
// src/store/product-list-store.ts
|
|
491
|
+
import { DEFAULT_PRODUCT_SORT } from "@nymbalhq/types";
|
|
492
|
+
var INITIAL_STATE3 = {
|
|
493
|
+
products: [],
|
|
494
|
+
facets: [],
|
|
495
|
+
filters: {},
|
|
496
|
+
sort: DEFAULT_PRODUCT_SORT,
|
|
497
|
+
pagination: { cursor: null, hasMore: false },
|
|
498
|
+
loading: false,
|
|
499
|
+
error: null
|
|
500
|
+
};
|
|
501
|
+
function createProductListStore(adapter) {
|
|
502
|
+
const store = createStore(INITIAL_STATE3);
|
|
503
|
+
let lastLimit;
|
|
504
|
+
let generation = 0;
|
|
505
|
+
function buildParams() {
|
|
506
|
+
const state = store.getState();
|
|
507
|
+
const params = { sort: state.sort };
|
|
508
|
+
if (lastLimit !== void 0) params.limit = lastLimit;
|
|
509
|
+
const category = state.filters["category"];
|
|
510
|
+
if (typeof category === "string") {
|
|
511
|
+
params.category = category;
|
|
512
|
+
}
|
|
513
|
+
return params;
|
|
514
|
+
}
|
|
515
|
+
async function load(params) {
|
|
516
|
+
if (params !== void 0) {
|
|
517
|
+
if (params.limit !== void 0) lastLimit = params.limit;
|
|
518
|
+
const filters = { ...store.getState().filters };
|
|
519
|
+
if (params.category !== void 0) filters["category"] = params.category;
|
|
520
|
+
else delete filters["category"];
|
|
521
|
+
store.setState({ filters, sort: params.sort ?? DEFAULT_PRODUCT_SORT });
|
|
522
|
+
}
|
|
523
|
+
const request = buildParams();
|
|
524
|
+
if (params?.cursor !== void 0) request.cursor = params.cursor;
|
|
525
|
+
const current = ++generation;
|
|
526
|
+
store.setState({ loading: true });
|
|
527
|
+
try {
|
|
528
|
+
const result = await adapter.products.list(request);
|
|
529
|
+
if (current !== generation) return;
|
|
530
|
+
store.setState({
|
|
531
|
+
products: result.items,
|
|
532
|
+
facets: result.facets ?? store.getState().facets,
|
|
533
|
+
pagination: { cursor: result.nextCursor, hasMore: result.nextCursor !== null },
|
|
534
|
+
loading: false,
|
|
535
|
+
error: null
|
|
536
|
+
});
|
|
537
|
+
} catch (err) {
|
|
538
|
+
if (current !== generation) return;
|
|
539
|
+
console.error("[ProductListStore] load failed", err);
|
|
540
|
+
store.setState({ loading: false, error: err instanceof Error ? err.message : String(err) });
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
async function loadMore() {
|
|
544
|
+
const state = store.getState();
|
|
545
|
+
if (!state.pagination.hasMore || state.loading) return;
|
|
546
|
+
const current = ++generation;
|
|
547
|
+
store.setState({ loading: true });
|
|
548
|
+
try {
|
|
549
|
+
const params = buildParams();
|
|
550
|
+
if (state.pagination.cursor !== null) {
|
|
551
|
+
params.cursor = state.pagination.cursor;
|
|
552
|
+
}
|
|
553
|
+
const result = await adapter.products.list(params);
|
|
554
|
+
if (current !== generation) return;
|
|
555
|
+
store.setState({
|
|
556
|
+
products: [...state.products, ...result.items],
|
|
557
|
+
pagination: { cursor: result.nextCursor, hasMore: result.nextCursor !== null },
|
|
558
|
+
loading: false,
|
|
559
|
+
error: null
|
|
560
|
+
});
|
|
561
|
+
} catch (err) {
|
|
562
|
+
if (current !== generation) return;
|
|
563
|
+
console.error("[ProductListStore] loadMore failed", err);
|
|
564
|
+
store.setState({ loading: false, error: err instanceof Error ? err.message : String(err) });
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
async function applyFilter(name, value) {
|
|
568
|
+
const filters = { ...store.getState().filters, [name]: value };
|
|
569
|
+
store.setState({ filters });
|
|
570
|
+
await load();
|
|
571
|
+
}
|
|
572
|
+
async function removeFilter(name) {
|
|
573
|
+
const filters = { ...store.getState().filters };
|
|
574
|
+
delete filters[name];
|
|
575
|
+
store.setState({ filters });
|
|
576
|
+
await load();
|
|
577
|
+
}
|
|
578
|
+
async function setSort(sort) {
|
|
579
|
+
store.setState({ sort });
|
|
580
|
+
await load();
|
|
581
|
+
}
|
|
582
|
+
return {
|
|
583
|
+
getState: store.getState,
|
|
584
|
+
subscribe: store.subscribe,
|
|
585
|
+
load,
|
|
586
|
+
loadMore,
|
|
587
|
+
applyFilter,
|
|
588
|
+
removeFilter,
|
|
589
|
+
setSort,
|
|
590
|
+
destroy: store.destroy
|
|
591
|
+
};
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
// src/store/search-store.ts
|
|
595
|
+
var INITIAL_STATE4 = {
|
|
596
|
+
query: "",
|
|
597
|
+
results: [],
|
|
598
|
+
total: 0,
|
|
599
|
+
loading: false
|
|
600
|
+
};
|
|
601
|
+
function createSearchStore(adapter) {
|
|
602
|
+
const store = createStore(INITIAL_STATE4);
|
|
603
|
+
async function search(query) {
|
|
604
|
+
if (!query.trim()) {
|
|
605
|
+
store.setState({ query: "", results: [], total: 0, loading: false });
|
|
606
|
+
return;
|
|
607
|
+
}
|
|
608
|
+
store.setState({ query, loading: true });
|
|
609
|
+
try {
|
|
610
|
+
const result = await adapter.products.search(query);
|
|
611
|
+
store.setState({
|
|
612
|
+
results: result.items,
|
|
613
|
+
total: result.items.length,
|
|
614
|
+
loading: false
|
|
615
|
+
});
|
|
616
|
+
} catch (err) {
|
|
617
|
+
console.error("[SearchStore] search failed", err);
|
|
618
|
+
store.setState({ loading: false });
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
function clearSearch() {
|
|
622
|
+
store.setState({ query: "", results: [], total: 0, loading: false });
|
|
623
|
+
}
|
|
624
|
+
return {
|
|
625
|
+
getState: store.getState,
|
|
626
|
+
subscribe: store.subscribe,
|
|
627
|
+
search,
|
|
628
|
+
clearSearch,
|
|
629
|
+
destroy: store.destroy
|
|
630
|
+
};
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
// src/store/checkout-store.ts
|
|
634
|
+
var INITIAL_STATE5 = {
|
|
635
|
+
step: "contact",
|
|
636
|
+
email: "",
|
|
637
|
+
shippingAddress: null,
|
|
638
|
+
billingAddress: null,
|
|
639
|
+
shippingMethod: null,
|
|
640
|
+
paymentStatus: "idle",
|
|
641
|
+
error: null,
|
|
642
|
+
order: null,
|
|
643
|
+
placedOrderNumber: null
|
|
644
|
+
};
|
|
645
|
+
function namedOrder(err) {
|
|
646
|
+
if (!(err instanceof NymbalApiError)) return null;
|
|
647
|
+
const orderNumber = err.context?.["orderNumber"];
|
|
648
|
+
return typeof orderNumber === "string" ? orderNumber : null;
|
|
649
|
+
}
|
|
650
|
+
function createCheckoutStore(adapter, cart) {
|
|
651
|
+
const store = createStore(INITIAL_STATE5);
|
|
652
|
+
function setEmail(email) {
|
|
653
|
+
store.setState({ email, step: "shipping" });
|
|
654
|
+
}
|
|
655
|
+
function setShippingAddress(address) {
|
|
656
|
+
store.setState({ shippingAddress: address });
|
|
657
|
+
}
|
|
658
|
+
function setBillingAddress(address) {
|
|
659
|
+
store.setState({ billingAddress: address });
|
|
660
|
+
}
|
|
661
|
+
function setShippingMethod(method) {
|
|
662
|
+
store.setState({ shippingMethod: method, step: "payment" });
|
|
663
|
+
}
|
|
664
|
+
async function submitPayment() {
|
|
665
|
+
const state = store.getState();
|
|
666
|
+
if (!state.shippingAddress || !state.billingAddress) {
|
|
667
|
+
store.setState({ error: "Shipping and billing addresses are required" });
|
|
668
|
+
return null;
|
|
669
|
+
}
|
|
670
|
+
store.setState({ paymentStatus: "processing", error: null, placedOrderNumber: null });
|
|
671
|
+
try {
|
|
672
|
+
const result = await adapter.checkout.create({
|
|
673
|
+
email: state.email,
|
|
674
|
+
billingAddress: state.billingAddress,
|
|
675
|
+
shippingAddress: state.shippingAddress
|
|
676
|
+
});
|
|
677
|
+
const { status, reason } = result.payment;
|
|
678
|
+
store.setState({
|
|
679
|
+
paymentStatus: status,
|
|
680
|
+
order: {
|
|
681
|
+
orderId: result.orderId,
|
|
682
|
+
orderNumber: result.orderNumber,
|
|
683
|
+
paymentIntent: result.paymentIntent
|
|
684
|
+
},
|
|
685
|
+
error: status === "failed" ? `Your payment did not go through${reason ? ` (${reason})` : ""}. Please try again.` : null,
|
|
686
|
+
...status === "succeeded" && { step: "confirmation" }
|
|
687
|
+
});
|
|
688
|
+
await cart?.load();
|
|
689
|
+
return result;
|
|
690
|
+
} catch (err) {
|
|
691
|
+
const placedOrderNumber = namedOrder(err);
|
|
692
|
+
store.setState({
|
|
693
|
+
paymentStatus: "failed",
|
|
694
|
+
error: err instanceof Error ? err.message : String(err),
|
|
695
|
+
placedOrderNumber
|
|
696
|
+
});
|
|
697
|
+
if (placedOrderNumber !== null) await cart?.load();
|
|
698
|
+
return null;
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
function markConfirmed() {
|
|
702
|
+
store.setState({ paymentStatus: "succeeded", step: "confirmation" });
|
|
703
|
+
}
|
|
704
|
+
function reset() {
|
|
705
|
+
store.setState(INITIAL_STATE5);
|
|
706
|
+
}
|
|
707
|
+
return {
|
|
708
|
+
getState: store.getState,
|
|
709
|
+
subscribe: store.subscribe,
|
|
710
|
+
setEmail,
|
|
711
|
+
setShippingAddress,
|
|
712
|
+
setBillingAddress,
|
|
713
|
+
setShippingMethod,
|
|
714
|
+
submitPayment,
|
|
715
|
+
markConfirmed,
|
|
716
|
+
reset,
|
|
717
|
+
destroy: store.destroy
|
|
718
|
+
};
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
// src/store/auth-store.ts
|
|
722
|
+
var INITIAL_STATE6 = {
|
|
723
|
+
customer: null,
|
|
724
|
+
isAuthenticated: false,
|
|
725
|
+
loading: false,
|
|
726
|
+
error: null
|
|
727
|
+
};
|
|
728
|
+
function createAuthStore(adapter, tokenManager) {
|
|
729
|
+
const store = createStore(INITIAL_STATE6);
|
|
730
|
+
async function login(email, password) {
|
|
731
|
+
store.setState({ loading: true, error: null });
|
|
732
|
+
try {
|
|
733
|
+
const result = await adapter.customers.login(email, password);
|
|
734
|
+
tokenManager.setTokens(result.accessToken, result.expiresIn);
|
|
735
|
+
store.setState({
|
|
736
|
+
customer: result.customer,
|
|
737
|
+
isAuthenticated: true,
|
|
738
|
+
loading: false
|
|
739
|
+
});
|
|
740
|
+
} catch (err) {
|
|
741
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
742
|
+
store.setState({ loading: false, error: message });
|
|
743
|
+
throw err;
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
async function register(params) {
|
|
747
|
+
store.setState({ loading: true, error: null });
|
|
748
|
+
try {
|
|
749
|
+
const result = await adapter.customers.register(params);
|
|
750
|
+
tokenManager.setTokens(result.accessToken, result.expiresIn);
|
|
751
|
+
store.setState({
|
|
752
|
+
customer: result.customer,
|
|
753
|
+
isAuthenticated: true,
|
|
754
|
+
loading: false
|
|
755
|
+
});
|
|
756
|
+
} catch (err) {
|
|
757
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
758
|
+
store.setState({ loading: false, error: message });
|
|
759
|
+
throw err;
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
async function logout() {
|
|
763
|
+
store.setState({ loading: true, error: null });
|
|
764
|
+
try {
|
|
765
|
+
await adapter.customers.logout();
|
|
766
|
+
tokenManager.clear();
|
|
767
|
+
store.setState({
|
|
768
|
+
customer: null,
|
|
769
|
+
isAuthenticated: false,
|
|
770
|
+
loading: false
|
|
771
|
+
});
|
|
772
|
+
} catch (err) {
|
|
773
|
+
tokenManager.clear();
|
|
774
|
+
store.setState({
|
|
775
|
+
customer: null,
|
|
776
|
+
isAuthenticated: false,
|
|
777
|
+
loading: false,
|
|
778
|
+
error: err instanceof Error ? err.message : String(err)
|
|
779
|
+
});
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
async function refreshToken() {
|
|
783
|
+
try {
|
|
784
|
+
const result = await adapter.customers.refresh();
|
|
785
|
+
tokenManager.setTokens(result.accessToken, result.expiresIn);
|
|
786
|
+
} catch (err) {
|
|
787
|
+
console.error("[AuthStore] token refresh failed, logging out", err);
|
|
788
|
+
tokenManager.clear();
|
|
789
|
+
store.setState({
|
|
790
|
+
customer: null,
|
|
791
|
+
isAuthenticated: false
|
|
792
|
+
});
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
async function updateProfile(updates) {
|
|
796
|
+
store.setState({ loading: true, error: null });
|
|
797
|
+
try {
|
|
798
|
+
const customer = await adapter.customers.updateProfile(updates);
|
|
799
|
+
store.setState({ customer, loading: false });
|
|
800
|
+
} catch (err) {
|
|
801
|
+
store.setState({
|
|
802
|
+
loading: false,
|
|
803
|
+
error: err instanceof Error ? err.message : String(err)
|
|
804
|
+
});
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
async function loadProfile() {
|
|
808
|
+
if (!tokenManager.getAccessToken()) return;
|
|
809
|
+
store.setState({ loading: true, error: null });
|
|
810
|
+
try {
|
|
811
|
+
const customer = await adapter.customers.getProfile();
|
|
812
|
+
store.setState({
|
|
813
|
+
customer,
|
|
814
|
+
isAuthenticated: true,
|
|
815
|
+
loading: false
|
|
816
|
+
});
|
|
817
|
+
} catch (err) {
|
|
818
|
+
store.setState({
|
|
819
|
+
loading: false,
|
|
820
|
+
error: err instanceof Error ? err.message : String(err)
|
|
821
|
+
});
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
return {
|
|
825
|
+
getState: store.getState,
|
|
826
|
+
subscribe: store.subscribe,
|
|
827
|
+
login,
|
|
828
|
+
register,
|
|
829
|
+
logout,
|
|
830
|
+
refreshToken,
|
|
831
|
+
updateProfile,
|
|
832
|
+
loadProfile,
|
|
833
|
+
destroy: store.destroy
|
|
834
|
+
};
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
// src/client.ts
|
|
838
|
+
function createNymbalClient(options) {
|
|
839
|
+
const tokenManager = createTokenManager({
|
|
840
|
+
...options.tokenStorage !== void 0 && { storage: options.tokenStorage }
|
|
841
|
+
});
|
|
842
|
+
let adapter;
|
|
843
|
+
if (options.adapter) {
|
|
844
|
+
adapter = options.adapter;
|
|
845
|
+
} else {
|
|
846
|
+
const fetchClient = new FetchClient({
|
|
847
|
+
baseUrl: options.baseUrl,
|
|
848
|
+
...options.credentials !== void 0 && { credentials: options.credentials },
|
|
849
|
+
getAccessToken: () => tokenManager.getAccessToken(),
|
|
850
|
+
...options.getCartToken !== void 0 && { getCartToken: options.getCartToken },
|
|
851
|
+
onUnauthorized: async () => {
|
|
852
|
+
try {
|
|
853
|
+
const result = await adapter.customers.refresh();
|
|
854
|
+
tokenManager.setTokens(result.accessToken, result.expiresIn);
|
|
855
|
+
} catch (err) {
|
|
856
|
+
console.warn("[NymbalClient] token refresh on unauthorized failed", err);
|
|
857
|
+
tokenManager.clear();
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
});
|
|
861
|
+
adapter = createNymbalApiAdapter(fetchClient);
|
|
862
|
+
}
|
|
863
|
+
const cart = createCartStore(adapter);
|
|
864
|
+
const product = createProductStore(adapter);
|
|
865
|
+
const productList = createProductListStore(adapter);
|
|
866
|
+
const search = createSearchStore(adapter);
|
|
867
|
+
const checkout = createCheckoutStore(adapter, cart);
|
|
868
|
+
const auth = createAuthStore(adapter, tokenManager);
|
|
869
|
+
const customer = {
|
|
870
|
+
addresses: {
|
|
871
|
+
list: () => adapter.customers.listAddresses(),
|
|
872
|
+
create: (address) => adapter.customers.addAddress(address),
|
|
873
|
+
remove: (id) => adapter.customers.removeAddress(id)
|
|
874
|
+
},
|
|
875
|
+
orders: {
|
|
876
|
+
list: (opts) => adapter.customers.listOrders(opts),
|
|
877
|
+
get: (orderNumber) => adapter.customers.getOrder(orderNumber)
|
|
878
|
+
}
|
|
879
|
+
};
|
|
880
|
+
tokenManager.scheduleRefresh(() => adapter.customers.refresh());
|
|
881
|
+
function destroy() {
|
|
882
|
+
cart.destroy();
|
|
883
|
+
product.destroy();
|
|
884
|
+
productList.destroy();
|
|
885
|
+
search.destroy();
|
|
886
|
+
checkout.destroy();
|
|
887
|
+
auth.destroy();
|
|
888
|
+
tokenManager.destroy();
|
|
889
|
+
}
|
|
890
|
+
return { cart, product, productList, search, checkout, auth, customer, adapter, destroy };
|
|
891
|
+
}
|
|
892
|
+
export {
|
|
893
|
+
FetchClient,
|
|
894
|
+
NymbalApiError,
|
|
895
|
+
createAuthStore,
|
|
896
|
+
createCartStore,
|
|
897
|
+
createCheckoutStore,
|
|
898
|
+
createNymbalApiAdapter,
|
|
899
|
+
createNymbalClient,
|
|
900
|
+
createProductListStore,
|
|
901
|
+
createProductStore,
|
|
902
|
+
createSearchStore,
|
|
903
|
+
createStore,
|
|
904
|
+
createTokenManager
|
|
905
|
+
};
|
|
906
|
+
//# sourceMappingURL=index.js.map
|