react-shopwave-connect 0.1.2 → 0.3.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.
@@ -24,89 +24,180 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
24
24
  let react = require("react");
25
25
  react = __toESM(react);
26
26
 
27
+ //#region src/core/errors.ts
28
+ /**
29
+ * Thrown by every `core` function when a request fails, either at the HTTP
30
+ * level (`status` is the HTTP status) or because the Shopwave response carried
31
+ * `api.message.errors` (then `status` is the HTTP status of that response,
32
+ * usually 200/201, and `errors` holds the API errors).
33
+ *
34
+ * Branch on `status` (e.g. `isUnauthorized` → send the user to log in) rather
35
+ * than parsing `message`.
36
+ */
37
+ var ShopwaveApiError = class extends Error {
38
+ /** HTTP status of the response (0 when the request never got a response). */
39
+ status;
40
+ /** Shopwave API errors, when the response carried any. */
41
+ errors;
42
+ /** Parsed response body, when there was one. */
43
+ body;
44
+ constructor(status, message, details = {}) {
45
+ super(message, details.cause !== void 0 ? { cause: details.cause } : void 0);
46
+ this.name = "ShopwaveApiError";
47
+ this.status = status;
48
+ this.errors = details.errors ?? null;
49
+ this.body = details.body;
50
+ }
51
+ /** 401, or the Shopwave "token expired / invalid" error 908. */
52
+ get isUnauthorized() {
53
+ if (this.status === 401) return true;
54
+ return !!this.errors && Object.entries(this.errors).some(([k, v]) => k === "908" || Number(v?.id) === 908);
55
+ }
56
+ get isNotFound() {
57
+ return this.status === 404;
58
+ }
59
+ };
60
+ /** The `api.message.errors` (or `api.message.error`) map of a Shopwave response, or `null` when there are none. */
61
+ function getApiErrorMap(body) {
62
+ const message = body?.api?.message;
63
+ const errors = message?.errors ?? message?.error;
64
+ if (errors && typeof errors === "object" && Object.keys(errors).length > 0) return errors;
65
+ return null;
66
+ }
67
+ function describeErrors(errors) {
68
+ return Object.entries(errors).map(([code, e]) => {
69
+ const text = [e?.title, e?.details].filter(Boolean).join(": ");
70
+ return text ? `${code} ${text}` : code;
71
+ }).join("; ");
72
+ }
73
+ /**
74
+ * Builds a {@link ShopwaveApiError} for a response. Understands the Shopwave
75
+ * envelope (`api.message.errors`) and the `{ error, message }` bodies returned
76
+ * by the `react-shopwave-connect/next` routes.
77
+ */
78
+ function toShopwaveApiError(status, body, fallback) {
79
+ const errors = getApiErrorMap(body);
80
+ let detail = errors ? describeErrors(errors) : "";
81
+ if (!detail && body && typeof body === "object") {
82
+ const b = body;
83
+ if (typeof b.message === "string") detail = b.message;
84
+ else if (typeof b.error === "string") detail = b.error;
85
+ }
86
+ if (!detail) detail = fallback ?? (status >= 400 ? "request failed" : "API returned errors");
87
+ return new ShopwaveApiError(status, `Shopwave API error (${status}): ${detail}`, {
88
+ errors,
89
+ body
90
+ });
91
+ }
92
+ /** Throws a {@link ShopwaveApiError} when the response body carries API errors. */
93
+ function assertNoApiErrors(body, status) {
94
+ if (getApiErrorMap(body)) throw toShopwaveApiError(status, body);
95
+ }
96
+
97
+ //#endregion
27
98
  //#region src/core/request.ts
28
99
  function resolveFetch(options) {
29
100
  const f = options.fetch ?? (typeof fetch !== "undefined" ? fetch : void 0);
30
101
  if (!f) throw new Error("No fetch implementation available. Pass `options.fetch` (e.g. node-fetch / undici) when running outside the browser.");
31
102
  return f;
32
103
  }
33
- function buildExtras(extras, options) {
104
+ /** Options with `token` filled in from a legacy per-call `params.token`, unless `options.token` is set. */
105
+ function withToken(options, token) {
106
+ return token && !options.token ? {
107
+ ...options,
108
+ token
109
+ } : options;
110
+ }
111
+ /** `Authorization` header value for a token (`OAuth <token>` unless it already has a scheme). */
112
+ function authorizationFor(token) {
113
+ const t = token.trim();
114
+ return /^(oauth|bearer)\s+\S/i.test(t) ? t : `OAuth ${t}`;
115
+ }
116
+ /** Headers shared by every request: Accept + the token, when one is given. */
117
+ function baseHeaders(options) {
118
+ const headers = { Accept: "application/json" };
119
+ if (options.token) headers.Authorization = authorizationFor(options.token);
120
+ return headers;
121
+ }
122
+ function buildExtras(extras) {
34
123
  const merged = {
35
124
  "Content-Type": "application/json",
36
125
  ...extras
37
126
  };
38
- if (options.token != null && merged.token == null) merged.token = options.token;
127
+ delete merged.token;
39
128
  return merged;
40
129
  }
41
- /**
42
- * Returns a JSON string of API errors if the response carries any, otherwise
43
- * `null`. Mirrors the original `!responseJson.api.message.errors` checks.
44
- */
45
- function getApiErrors(api) {
46
- const errors = api?.message?.errors;
47
- if (errors && Object.keys(errors).length > 0) return JSON.stringify(errors);
48
- return null;
130
+ /** Parses a response body as JSON; `null` for an empty body, `{ raw }` for non-JSON. */
131
+ async function readBody(response) {
132
+ const text = await response.text();
133
+ if (!text) return null;
134
+ try {
135
+ return JSON.parse(text);
136
+ } catch {
137
+ return { raw: text };
138
+ }
139
+ }
140
+ async function send(path, init, options) {
141
+ const doFetch = resolveFetch(options);
142
+ const url = (options.baseUrl ?? "") + path;
143
+ let response;
144
+ try {
145
+ response = await doFetch(url, {
146
+ ...init,
147
+ signal: options.signal
148
+ });
149
+ } catch (error) {
150
+ if (error?.name === "AbortError") throw error;
151
+ throw new ShopwaveApiError(0, `Shopwave API request failed: ${error?.message ?? error}`, { cause: error });
152
+ }
153
+ const body = await readBody(response);
154
+ if (!response.ok) throw toShopwaveApiError(response.status, body, response.statusText || void 0);
155
+ return {
156
+ status: response.status,
157
+ body
158
+ };
49
159
  }
50
160
  /**
51
- * GET helper. Sends request metadata via the `extras` header exactly like the
52
- * original hooks did, parses JSON, and returns the typed payload.
161
+ * GET helper. Sends request metadata via the `extras` header, parses JSON and
162
+ * returns the payload. Throws {@link ShopwaveApiError} for non-2xx responses.
53
163
  */
54
164
  async function apiGet(path, extras, options = {}) {
55
- const doFetch = resolveFetch(options);
56
- const url = (options.baseUrl ?? "") + path;
57
- const headers = { extras: JSON.stringify(buildExtras(extras, options)) };
58
- console.log("[API GET]", {
59
- url,
60
- headers
61
- });
62
- const response = await doFetch(url, {
165
+ const { body } = await send(path, {
63
166
  method: "GET",
64
- headers,
65
- signal: options.signal
66
- });
67
- if (!response.ok) throw new Error(`Network response was not ok (${response.status})`);
68
- return await response.json();
167
+ headers: {
168
+ ...baseHeaders(options),
169
+ extras: JSON.stringify(buildExtras(extras))
170
+ }
171
+ }, options);
172
+ return body;
69
173
  }
70
174
  /**
71
- * JSON body helper for POST/PUT mutations.
175
+ * JSON body helper for POST/PUT mutations. Returns the parsed body (`null` for
176
+ * an empty one). Throws {@link ShopwaveApiError} for non-2xx responses.
72
177
  */
73
178
  async function apiSend(path, method, payload, options = {}) {
74
- const doFetch = resolveFetch(options);
75
- const url = (options.baseUrl ?? "") + path;
76
- const headers = { "Content-Type": "application/json" };
77
- if (options.token != null) headers.token = options.token;
78
- console.log(`[API ${method}]`, {
79
- url,
80
- headers,
81
- payload
82
- });
83
- const response = await doFetch(url, {
179
+ const { body } = await send(path, {
84
180
  method,
85
- headers,
86
- body: JSON.stringify(payload),
87
- signal: options.signal
88
- });
89
- if (!response.ok) throw new Error(`Failed to ${method.toLowerCase()}: ${response.statusText}`);
90
- return await response.json();
181
+ headers: {
182
+ ...baseHeaders(options),
183
+ "Content-Type": "application/json"
184
+ },
185
+ body: JSON.stringify(payload)
186
+ }, options);
187
+ return body;
91
188
  }
92
189
  /**
93
- * DELETE helper.
190
+ * DELETE helper. Shopwave answers deletes with 205 and an empty body; if a body
191
+ * does come back and carries API errors, they are thrown. Returns the parsed
192
+ * body (`null` when empty).
94
193
  */
95
194
  async function apiDelete(path, options = {}) {
96
- const doFetch = resolveFetch(options);
97
- const url = (options.baseUrl ?? "") + path;
98
- const headers = {};
99
- if (options.token != null) headers.token = options.token;
100
- console.log("[API DELETE]", {
101
- url,
102
- headers
103
- });
104
- const response = await doFetch(url, {
195
+ const { status, body } = await send(path, {
105
196
  method: "DELETE",
106
- headers,
107
- signal: options.signal
108
- });
109
- if (!response.ok) throw new Error(`Failed to delete: ${response.statusText}`);
197
+ headers: baseHeaders(options)
198
+ }, options);
199
+ if (getApiErrorMap(body)) throw toShopwaveApiError(status, body);
200
+ return body;
110
201
  }
111
202
 
112
203
  //#endregion
@@ -118,11 +209,9 @@ async function apiDelete(path, options = {}) {
118
209
  async function fetchCategories(params = {}, options = {}) {
119
210
  const extras = { deleted: params.deleted ?? false };
120
211
  if (params.categoryIds) extras.categoryIds = params.categoryIds;
121
- if (params.token) extras.token = params.token;
122
- const json = await apiGet("/api/categories", extras, options);
123
- const error = getApiErrors(json.api);
124
- if (error) throw new Error(error);
125
- return Object.values(json.categories ?? {});
212
+ const json = await apiGet("/api/categories", extras, withToken(options, params.token));
213
+ assertNoApiErrors(json, 200);
214
+ return Object.values(json?.categories ?? {});
126
215
  }
127
216
 
128
217
  //#endregion
@@ -134,9 +223,8 @@ async function fetchCategories(params = {}, options = {}) {
134
223
  async function fetchConsumers(consumerIds, options = {}) {
135
224
  if (!consumerIds || consumerIds.length === 0) return [];
136
225
  const json = await apiGet("/api/consumer", { ids: consumerIds.join(",") }, options);
137
- const error = getApiErrors(json.api);
138
- if (error) throw new Error(error);
139
- return Object.values(json.consumers ?? {});
226
+ assertNoApiErrors(json, 200);
227
+ return Object.values(json?.consumers ?? {});
140
228
  }
141
229
 
142
230
  //#endregion
@@ -147,10 +235,9 @@ async function fetchConsumers(consumerIds, options = {}) {
147
235
  async function fetchEmployees(params = {}, options = {}) {
148
236
  const extras = { deleted: params.deleted ?? false };
149
237
  if (params.employeeIds) extras.employeeIds = params.employeeIds;
150
- const json = await apiGet("/api/employees", extras, options);
151
- const error = getApiErrors(json.api);
152
- if (error) throw new Error(error);
153
- return Object.values(json.employees ?? {});
238
+ const json = await apiGet("/api/employees", extras, withToken(options, params.token));
239
+ assertNoApiErrors(json, 200);
240
+ return Object.values(json?.employees ?? {});
154
241
  }
155
242
 
156
243
  //#endregion
@@ -162,11 +249,9 @@ async function fetchProducts(params = {}, options = {}) {
162
249
  const extras = { deleted: params.deleted ?? false };
163
250
  if (params.productIds) extras.productIds = params.productIds;
164
251
  if (params.storeId) extras.storeId = params.storeId;
165
- if (params.token) extras.token = params.token;
166
- const json = await apiGet("/api/products", extras, options);
167
- const error = getApiErrors(json.api);
168
- if (error) throw new Error(error);
169
- return Object.values(json.products ?? {});
252
+ const json = await apiGet("/api/products", extras, withToken(options, params.token));
253
+ assertNoApiErrors(json, 200);
254
+ return Object.values(json?.products ?? {});
170
255
  }
171
256
  /**
172
257
  * Fetches products by id and returns them keyed by id, transparently batching
@@ -195,11 +280,9 @@ async function fetchPromotions(params = {}, options = {}) {
195
280
  if (params.imminent !== void 0) extras.imminent = params.imminent;
196
281
  if (params.active !== void 0) extras.active = params.active;
197
282
  if (params.expired !== void 0) extras.expired = params.expired;
198
- if (params.token) extras.token = params.token;
199
- const json = await apiGet("/api/promotions", extras, options);
200
- const error = getApiErrors(json.api);
201
- if (error) throw new Error(error);
202
- return Object.values(json.promotions ?? {});
283
+ const json = await apiGet("/api/promotions", extras, withToken(options, params.token));
284
+ assertNoApiErrors(json, 200);
285
+ return Object.values(json?.promotions ?? {});
203
286
  }
204
287
 
205
288
  //#endregion
@@ -208,10 +291,11 @@ async function fetchPromotions(params = {}, options = {}) {
208
291
  * Fetches stores. Returns the flattened `Store[]`.
209
292
  */
210
293
  async function fetchStores(params = {}, options = {}) {
211
- const json = await apiGet("/api/stores", { deleted: params.deleted ?? false }, options);
212
- const error = getApiErrors(json.api);
213
- if (error) throw new Error(error);
214
- return Object.values(json.stores ?? {});
294
+ const extras = { deleted: params.deleted ?? false };
295
+ if (params.storeIds) extras.storeIds = params.storeIds;
296
+ const json = await apiGet("/api/stores", extras, withToken(options, params.token));
297
+ assertNoApiErrors(json, 200);
298
+ return Object.values(json?.stores ?? {});
215
299
  }
216
300
 
217
301
  //#endregion
@@ -223,20 +307,43 @@ async function fetchStores(params = {}, options = {}) {
223
307
  */
224
308
  async function fetchReport(query, options = {}) {
225
309
  const json = await apiGet("/api/report", { query: JSON.stringify(query) }, options);
226
- const error = getApiErrors(json.api);
227
- if (error) throw new Error(error);
228
- return json.reports ?? {};
310
+ assertNoApiErrors(json, 200);
311
+ return json?.reports ?? {};
229
312
  }
230
313
 
231
314
  //#endregion
232
315
  //#region src/core/session.ts
233
316
  /**
234
- * Ends the current server session.
317
+ * Reads the current login status from `GET /api/session`.
318
+ *
319
+ * Works with the new `react-shopwave-connect/next` session route
320
+ * (`{ loggedIn }`) and with older routes that returned the raw session
321
+ * (`{ token: {...} }`) — in that case only a boolean is surfaced.
322
+ */
323
+ async function fetchSession(options = {}) {
324
+ const doFetch = options.fetch ?? (typeof fetch !== "undefined" ? fetch : void 0);
325
+ if (!doFetch) throw new Error("No fetch implementation available. Pass `options.fetch`.");
326
+ const response = await doFetch(`${options.baseUrl ?? ""}/api/session`, {
327
+ method: "GET",
328
+ headers: { Accept: "application/json" },
329
+ cache: "no-store",
330
+ credentials: "same-origin",
331
+ signal: options.signal
332
+ });
333
+ if (response.status === 401) return { loggedIn: false };
334
+ if (!response.ok) throw new Error(`Session request failed (${response.status})`);
335
+ const json = await response.json();
336
+ if (json && typeof json.loggedIn === "boolean") return {
337
+ loggedIn: json.loggedIn,
338
+ expiresAt: typeof json.expiresAt === "number" ? json.expiresAt : void 0
339
+ };
340
+ return { loggedIn: Boolean(json && json.token) };
341
+ }
342
+ /**
343
+ * Ends the current server session (no redirect).
235
344
  *
236
- * This is the framework-agnostic half of the original `useLogout` hook. The
237
- * Next.js-specific redirect logic (router.push to the auth server logout URL,
238
- * built from server actions) is intentionally NOT included here because it is
239
- * tied to `next/navigation` and `'use server'` actions — see the README.
345
+ * Prefer navigating to {@link logoutPath} when using
346
+ * `react-shopwave-connect/next`: it also signs the user out of the auth server.
240
347
  */
241
348
  async function logout(options = {}) {
242
349
  await apiDelete("/api/session?action=logout", options);
@@ -245,10 +352,12 @@ async function logout(options = {}) {
245
352
  //#endregion
246
353
  //#region src/core/entity.ts
247
354
  /**
248
- * Generic CRUD-style mutations extracted from `useHandleDelete` /
249
- * `useHandleSubmit`. The original hooks also surfaced notistack snackbars; that
250
- * UI concern is deliberately left to the hooks/host layer so core stays
251
- * dependency-free.
355
+ * Low-level, untyped mutations: the caller supplies the route and payload.
356
+ *
357
+ * Prefer the typed functions in `./entities` (`saveProduct`, `deleteCategory`,
358
+ * …): they know each route, build the request envelope, check the response and
359
+ * return the saved entity with its id. Keep these for routes that have no typed
360
+ * function yet (e.g. `/api/merchant`).
252
361
  */
253
362
  /**
254
363
  * Deletes `/api/{resourcePath}/{entityId}`.
@@ -258,11 +367,15 @@ async function deleteEntity(resourcePath, entityId, options = {}) {
258
367
  await apiDelete(`/api/${resourcePath}/${entityId}`, options);
259
368
  }
260
369
  /**
261
- * Sends a create/update mutation and returns the parsed `result` field
262
- * (matching the original `result.result` access).
370
+ * Sends a create/update mutation. Returns the response's `result` field when it
371
+ * has one (older app routes wrapped their answer that way), otherwise the whole
372
+ * parsed response body. Throws `ShopwaveApiError` on HTTP or API errors.
263
373
  */
264
374
  async function submitEntity({ endpoint, method = "POST", payload }, options = {}) {
265
- return (await apiSend(endpoint, method, payload, options)).result;
375
+ const json = await apiSend(endpoint, method, payload, options);
376
+ assertNoApiErrors(json, 200);
377
+ if (json && typeof json === "object" && "result" in json) return json.result;
378
+ return json ?? void 0;
266
379
  }
267
380
 
268
381
  //#endregion
@@ -415,50 +528,127 @@ function toMessage(e) {
415
528
  if (e instanceof Error) return e.message;
416
529
  return String(e);
417
530
  }
531
+ function toStatus(e) {
532
+ return e instanceof ShopwaveApiError ? e.status : null;
533
+ }
534
+ function queryReducer(state, action) {
535
+ switch (action.type) {
536
+ case "start":
537
+ if (action.key !== state.key) return {
538
+ data: null,
539
+ key: action.key,
540
+ fetching: true,
541
+ error: null,
542
+ errorStatus: null
543
+ };
544
+ return {
545
+ ...state,
546
+ fetching: true,
547
+ error: null,
548
+ errorStatus: null
549
+ };
550
+ case "success":
551
+ if (action.key !== state.key) return state;
552
+ return {
553
+ data: action.data,
554
+ key: action.key,
555
+ fetching: false,
556
+ error: null,
557
+ errorStatus: null
558
+ };
559
+ case "failure":
560
+ if (action.key !== state.key) return state;
561
+ return {
562
+ ...state,
563
+ fetching: false,
564
+ error: action.error,
565
+ errorStatus: action.errorStatus
566
+ };
567
+ case "idle": return state.fetching ? {
568
+ ...state,
569
+ fetching: false
570
+ } : state;
571
+ default: return state;
572
+ }
573
+ }
574
+ /** Public view of the internal state for the params currently being rendered. */
575
+ function selectQueryView(state, key, enabled) {
576
+ if (state.key !== key) return {
577
+ data: null,
578
+ loading: enabled,
579
+ fetching: enabled,
580
+ error: null,
581
+ errorStatus: null
582
+ };
583
+ return {
584
+ data: state.data,
585
+ loading: state.fetching && state.data === null,
586
+ fetching: state.fetching,
587
+ error: state.error,
588
+ errorStatus: state.errorStatus
589
+ };
590
+ }
591
+ function depsKey(deps) {
592
+ try {
593
+ return JSON.stringify(deps);
594
+ } catch {
595
+ return String(deps);
596
+ }
597
+ }
418
598
  /**
419
599
  * Internal primitive powering the auto-fetch hooks. Runs `fetcher` on mount and
420
600
  * whenever `deps` change, tracks loading/error/data, cancels in-flight requests
421
- * on unmount or dep change, and exposes `refetch`.
601
+ * on unmount or dep change, and exposes `refetch` (which keeps the current data
602
+ * visible until the new data arrives).
422
603
  *
423
604
  * When `enabled` is false the fetch is skipped (used for hooks that need
424
605
  * arguments before they can run, e.g. consumers-by-id).
425
606
  */
426
607
  function useQuery(fetcher, deps, enabled = true) {
427
- const [data, setData] = (0, react.useState)(null);
428
- const [loading, setLoading] = (0, react.useState)(enabled);
429
- const [error, setError] = (0, react.useState)(null);
608
+ const key = depsKey(deps);
609
+ const [state, dispatch] = (0, react.useReducer)(queryReducer, {
610
+ data: null,
611
+ key: enabled ? key : null,
612
+ fetching: enabled,
613
+ error: null,
614
+ errorStatus: null
615
+ });
430
616
  const [tick, setTick] = (0, react.useState)(0);
431
617
  const refetch = (0, react.useCallback)(() => setTick((t) => t + 1), []);
432
618
  (0, react.useEffect)(() => {
433
619
  if (!enabled) {
434
- setLoading(false);
620
+ dispatch({ type: "idle" });
435
621
  return;
436
622
  }
437
623
  const controller = new AbortController();
438
- let active = true;
439
- setData(null);
440
- setError(null);
441
- setLoading(true);
624
+ dispatch({
625
+ type: "start",
626
+ key
627
+ });
442
628
  fetcher(controller.signal).then((result) => {
443
- if (active) setData(result);
629
+ if (!controller.signal.aborted) dispatch({
630
+ type: "success",
631
+ key,
632
+ data: result
633
+ });
444
634
  }).catch((e) => {
445
- if (active && e?.name !== "AbortError") setError(toMessage(e));
446
- }).finally(() => {
447
- if (active) setLoading(false);
635
+ if (!controller.signal.aborted && e?.name !== "AbortError") dispatch({
636
+ type: "failure",
637
+ key,
638
+ error: toMessage(e),
639
+ errorStatus: toStatus(e)
640
+ });
448
641
  });
449
642
  return () => {
450
- active = false;
451
643
  controller.abort();
452
644
  };
453
645
  }, [
454
- ...deps,
646
+ key,
455
647
  tick,
456
648
  enabled
457
649
  ]);
458
650
  return {
459
- data,
460
- loading,
461
- error,
651
+ ...selectQueryView(state, key, enabled),
462
652
  refetch
463
653
  };
464
654
  }
@@ -469,16 +659,19 @@ function useMutation(action, deps) {
469
659
  const [data, setData] = (0, react.useState)(null);
470
660
  const [loading, setLoading] = (0, react.useState)(false);
471
661
  const [error, setError] = (0, react.useState)(null);
662
+ const [errorStatus, setErrorStatus] = (0, react.useState)(null);
472
663
  return {
473
664
  mutate: (0, react.useCallback)(async (...args) => {
474
665
  setLoading(true);
475
666
  setError(null);
667
+ setErrorStatus(null);
476
668
  try {
477
669
  const result = await action(...args);
478
670
  setData(result);
479
671
  return result;
480
672
  } catch (e) {
481
673
  setError(toMessage(e));
674
+ setErrorStatus(toStatus(e));
482
675
  return null;
483
676
  } finally {
484
677
  setLoading(false);
@@ -486,7 +679,8 @@ function useMutation(action, deps) {
486
679
  }, deps),
487
680
  data,
488
681
  loading,
489
- error
682
+ error,
683
+ errorStatus
490
684
  };
491
685
  }
492
686
 
@@ -668,6 +862,23 @@ function useLogout(options) {
668
862
  return useMutation(() => logout(options), [options?.baseUrl, options?.token]);
669
863
  }
670
864
 
865
+ //#endregion
866
+ //#region src/hooks/useSession.ts
867
+ /**
868
+ * Current login status (`{ loggedIn, expiresAt }`) from `GET /api/session`.
869
+ * Tokens are never exposed to the browser.
870
+ *
871
+ * @example
872
+ * const { data: session, loading } = useSession();
873
+ * if (!loading && !session?.loggedIn) window.location.href = loginPath(location.pathname);
874
+ */
875
+ function useSession(options) {
876
+ return useQuery((signal) => fetchSession({
877
+ ...options,
878
+ signal
879
+ }), [options?.baseUrl]);
880
+ }
881
+
671
882
  //#endregion
672
883
  //#region src/hooks/useBasketReport.ts
673
884
  /**
@@ -781,6 +992,7 @@ exports.useLogout = useLogout;
781
992
  exports.useProduct = useProduct;
782
993
  exports.usePromotion = usePromotion;
783
994
  exports.useReport = useReport;
995
+ exports.useSession = useSession;
784
996
  exports.useStore = useStore;
785
997
  exports.useSubmit = useSubmit;
786
998
  //# sourceMappingURL=index.cjs.map