@viu/emporix-sdk-react 2.6.0 → 2.8.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/CHANGELOG.md CHANGED
@@ -1,5 +1,86 @@
1
1
  # @viu/emporix-sdk-react
2
2
 
3
+ ## 2.8.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [#98](https://github.com/viuteam/emporix-sdk/pull/98) [`108a724`](https://github.com/viuteam/emporix-sdk/commit/108a724f1d4342532ae8d575faa501d54d8c591f) Thanks [@amnael1](https://github.com/amnael1)! - Support partial cart-item updates. `client.carts.updateItem(cartId, itemId,
8
+ patch, auth, { partial: true })` now sends `?partial=true`, so a quantity-only
9
+ change can be `{ quantity }` instead of a full item replace (which otherwise
10
+ requires re-sending `itemYrn` + the `price` row). The React
11
+ `useCartMutations().updateItem` mutation accepts an optional `partial` flag in
12
+ its variables. Default behavior is unchanged.
13
+
14
+ - [#100](https://github.com/viuteam/emporix-sdk/pull/100) [`b4be158`](https://github.com/viuteam/emporix-sdk/commit/b4be1589b2fb0db44852233efe2a5d575a2e2795) Thanks [@amnael1](https://github.com/amnael1)! - `useCustomerSession()` now exposes the current `saasToken`. It was already
15
+ tracked internally (from `login` / `exchangeToken`) but not returned — so
16
+ consumers couldn't pass it to `useCheckout().placeOrder({ ..., saasToken })` for
17
+ customer checkout, or to saas-token-gated order reads.
18
+
19
+ ### Patch Changes
20
+
21
+ - [#101](https://github.com/viuteam/emporix-sdk/pull/101) [`e010a5a`](https://github.com/viuteam/emporix-sdk/commit/e010a5ab8f35c92ed946522558db33b2febff5de) Thanks [@amnael1](https://github.com/amnael1)! - fix(react): refresh the cart after a 204-only mutation
22
+
23
+ `useCartMutations` assumed every cart write echoes the full updated cart.
24
+ A partial quantity update (`updateItem(..., { partial: true })`) returns
25
+ `204 No Content`, which the SDK resolves to `undefined` — and
26
+ `setQueryData(key, undefined)` is a no-op in React Query, so the cart cache
27
+ stayed stale and the UI did not reflect the change. The mutation now adopts
28
+ a real cart body when one is returned and otherwise invalidates the cart
29
+ query so it refetches. This also makes coupon/address/remove mutations
30
+ reconcile with the server when they return no body.
31
+
32
+ - [#103](https://github.com/viuteam/emporix-sdk/pull/103) [`2e5c767`](https://github.com/viuteam/emporix-sdk/commit/2e5c76715f5d08358dd9342ef65d7c4c0d8b9aef) Thanks [@amnael1](https://github.com/amnael1)! - fix(react): drop the cart on logout and react to cart-id clearing
33
+
34
+ Two related cleanup gaps caused follow-up errors after logout and checkout:
35
+ - `useCustomerSession().logout()` cleared the customer token but left the
36
+ stored `cartId`. The cart belonged to the customer and isn't accessible
37
+ anonymously, so the cart query immediately refetched it and got a `403`.
38
+ Logout now clears `cartId` too.
39
+ - `useActiveCart` cached the cart id in local state and never reacted to
40
+ external `storage.setCartId(null)` (logout, or the post-order cleanup that
41
+ closes the cart). It kept fetching the dead cart id — a `403` after logout,
42
+ a `404` after checkout. It now subscribes to storage cart-id changes and
43
+ syncs, so clearing the id stops the fetch (and a logged-out cart page
44
+ bootstraps a fresh anonymous cart on demand).
45
+
46
+ - [#102](https://github.com/viuteam/emporix-sdk/pull/102) [`020722b`](https://github.com/viuteam/emporix-sdk/commit/020722b2cb696bdc347538205ea4fad884451d88) Thanks [@amnael1](https://github.com/amnael1)! - fix(react): share the customer session across hook instances
47
+
48
+ `useCustomerSession` kept its session in a per-instance `useState`. The
49
+ `token` slot was mirrored from storage (so `isAuthenticated` was consistent),
50
+ but the in-memory `saasToken` and `refreshToken` lived only in the component
51
+ instance that called `login()`. A different consumer — e.g. the checkout page
52
+ reading `saasToken` for the `saas-token` header — saw `null`, so customer
53
+ checkout failed with `401 "Saas TOKEN is invalid"`.
54
+
55
+ The session now lives in a shared, per-storage store consumed via
56
+ `useSyncExternalStore`, so every `useCustomerSession()` reads the same
57
+ `{ token, refreshToken, saasToken }`. A login in one component is immediately
58
+ visible to all others. The tokens remain in-memory only (still cleared on a
59
+ full reload, by design).
60
+
61
+ ## 2.7.0
62
+
63
+ ### Minor Changes
64
+
65
+ - [#96](https://github.com/viuteam/emporix-sdk/pull/96) [`da1113a`](https://github.com/viuteam/emporix-sdk/commit/da1113a07f70dceb9f1cb732b28462ccb3671f4a) Thanks [@amnael1](https://github.com/amnael1)! - Fix and extend the Category service for catalogue + hierarchy browsing. Several
66
+ methods targeted routes that don't exist on the deployed category service
67
+ (verified against a live tenant):
68
+ - **`categories.productsIn(...)`** requested a non-existent
69
+ `/categories/{id}/products` route (always 404). It now resolves products via
70
+ category **assignments** (`/categories/{id}/assignments` → keep `PRODUCT`
71
+ refs → `/products/search`), preserving its `PaginatedItems<Product>` contract;
72
+ categories with no products return an empty page instead of throwing.
73
+ - **`categories.tree()`** pointed at a non-existent `/categories/{...}Tree`
74
+ route. It now reads `/category-trees` and returns the catalogue's **root
75
+ categories** (`Promise<Category[]>`) for top-level navigation. (Return type
76
+ changed from the previous nested-node shape; the `rootId` argument is removed.)
77
+ - **New `categories.subcategories(categoryId)`** (+ React `useSubcategories`):
78
+ a category's direct child categories, resolved from `CATEGORY` assignment refs
79
+ (mirrors `productsIn`). Returns `[]` when there are none.
80
+
81
+ React `useCategoryTree()` now returns `Category[]` (root categories) and takes no
82
+ `rootId`.
83
+
3
84
  ## 2.6.0
4
85
 
5
86
  ### Minor Changes
@@ -1,5 +1,5 @@
1
1
  import { useEmporix, EmporixSiteContext, useActiveCompany } from './chunk-SDRV73LG.js';
2
- import { useContext, useState, useEffect, useCallback } from 'react';
2
+ import { useContext, useMemo, useSyncExternalStore, useEffect, useCallback, useState } from 'react';
3
3
  import { useQueryClient, useQuery, useMutation, useInfiniteQuery } from '@tanstack/react-query';
4
4
  import { auth, EmporixError } from '@viu/emporix-sdk';
5
5
 
@@ -29,6 +29,36 @@ async function bootstrapCart(opts) {
29
29
  });
30
30
  }
31
31
 
32
+ // src/hooks/internal/customer-session-store.ts
33
+ var stores = /* @__PURE__ */ new WeakMap();
34
+ function getCustomerSessionStore(storage) {
35
+ const existing = stores.get(storage);
36
+ if (existing) return existing;
37
+ let state = {
38
+ token: storage.getCustomerToken(),
39
+ refreshToken: null,
40
+ saasToken: null
41
+ };
42
+ const listeners = /* @__PURE__ */ new Set();
43
+ const store = {
44
+ getSnapshot: () => state,
45
+ setState: (next) => {
46
+ const resolved = typeof next === "function" ? next(state) : next;
47
+ if (resolved === state) return;
48
+ state = resolved;
49
+ for (const listener of listeners) listener();
50
+ },
51
+ subscribe: (listener) => {
52
+ listeners.add(listener);
53
+ return () => {
54
+ listeners.delete(listener);
55
+ };
56
+ }
57
+ };
58
+ stores.set(storage, store);
59
+ return store;
60
+ }
61
+
32
62
  // src/hooks/use-customer-session.ts
33
63
  var EMPTY_SESSION = {
34
64
  token: null,
@@ -39,14 +69,12 @@ function useCustomerSession() {
39
69
  const { client, storage } = useEmporix();
40
70
  const qc = useQueryClient();
41
71
  const siteCtx = useContext(EmporixSiteContext);
42
- const [session, setSession] = useState(() => ({
43
- token: storage.getCustomerToken(),
44
- refreshToken: null,
45
- saasToken: null
46
- }));
72
+ const store = useMemo(() => getCustomerSessionStore(storage), [storage]);
73
+ const session = useSyncExternalStore(store.subscribe, store.getSnapshot, store.getSnapshot);
74
+ const setSession = store.setState;
47
75
  useEffect(() => {
48
76
  return storage.subscribe?.((t) => setSession((s) => ({ ...s, token: t })));
49
- }, [storage]);
77
+ }, [storage, setSession]);
50
78
  const meQuery = useQuery({
51
79
  queryKey: ["emporix", "customer", "me", { tenant: client.tenant, hasToken: session.token !== null }],
52
80
  enabled: session.token !== null,
@@ -80,7 +108,7 @@ function useCustomerSession() {
80
108
  await qc.invalidateQueries({ queryKey: ["emporix", "customer"], refetchType: "none" });
81
109
  await qc.invalidateQueries({ queryKey: ["emporix", "cart"], refetchType: "none" });
82
110
  },
83
- [client, storage, qc, siteCtx]
111
+ [client, storage, qc, siteCtx, setSession]
84
112
  );
85
113
  const signup = useCallback(
86
114
  async (input) => {
@@ -112,7 +140,7 @@ function useCustomerSession() {
112
140
  await qc.invalidateQueries({ queryKey: ["emporix", "customer"], refetchType: "none" });
113
141
  await qc.invalidateQueries({ queryKey: ["emporix", "cart"], refetchType: "none" });
114
142
  },
115
- [client, storage, qc, siteCtx]
143
+ [client, storage, qc, siteCtx, setSession]
116
144
  );
117
145
  const socialLogin = useCallback(
118
146
  async (input) => {
@@ -136,10 +164,11 @@ function useCustomerSession() {
136
164
  storage.setCustomerToken(null);
137
165
  storage.setRefreshToken(null);
138
166
  storage.setActiveLegalEntityId(null);
167
+ storage.setCartId(null);
139
168
  setSession(EMPTY_SESSION);
140
169
  qc.removeQueries({ queryKey: ["emporix", "customer"] });
141
170
  qc.removeQueries({ queryKey: ["emporix", "cart"] });
142
- }, [client, session.token, storage, qc]);
171
+ }, [client, session.token, storage, qc, setSession]);
143
172
  const refresh = useCallback(async () => {
144
173
  await meQuery.refetch();
145
174
  }, [meQuery]);
@@ -158,10 +187,11 @@ function useCustomerSession() {
158
187
  }));
159
188
  await qc.invalidateQueries({ queryKey: ["emporix", "customer"] });
160
189
  await qc.invalidateQueries({ queryKey: ["emporix", "cart"] });
161
- }, [client, storage, qc, session.refreshToken, session.saasToken]);
190
+ }, [client, storage, qc, session.refreshToken, session.saasToken, setSession]);
162
191
  return {
163
192
  customerToken: session.token,
164
193
  refreshToken: session.refreshToken,
194
+ saasToken: session.saasToken,
165
195
  customer: meQuery.data ?? null,
166
196
  isAuthenticated: session.token !== null,
167
197
  isLoading: meQuery.isLoading && session.token !== null,
@@ -425,6 +455,17 @@ function useCategory(categoryId, options = {}) {
425
455
  staleTime: CATEGORIES_STALE_TIME
426
456
  });
427
457
  }
458
+ function useSubcategories(categoryId, params = {}, options = {}) {
459
+ const { client } = useEmporix();
460
+ const { ctx } = useReadAuth(options.auth);
461
+ const { siteCode } = useReadSite();
462
+ return useQuery({
463
+ queryKey: emporixKey("subcategories", [categoryId ?? null, params], { tenant: client.tenant, authKind: ctx.kind, siteCode }),
464
+ enabled: typeof categoryId === "string" && categoryId !== "",
465
+ queryFn: () => client.categories.subcategories(categoryId, params, ctx),
466
+ staleTime: CATEGORIES_STALE_TIME
467
+ });
468
+ }
428
469
  function useCategories(params = {}, options = {}) {
429
470
  const { client } = useEmporix();
430
471
  const { ctx } = useReadAuth(options.auth);
@@ -448,13 +489,13 @@ function useCategoriesInfinite(params = {}, options = {}) {
448
489
  staleTime: CATEGORIES_STALE_TIME
449
490
  });
450
491
  }
451
- function useCategoryTree(rootId, options = {}) {
492
+ function useCategoryTree(options = {}) {
452
493
  const { client } = useEmporix();
453
494
  const { ctx } = useReadAuth(options.auth);
454
495
  const { siteCode } = useReadSite();
455
496
  return useQuery({
456
- queryKey: emporixKey("category-tree", [rootId ?? null], { tenant: client.tenant, authKind: ctx.kind, siteCode }),
457
- queryFn: () => client.categories.tree(rootId, ctx),
497
+ queryKey: emporixKey("category-tree", [], { tenant: client.tenant, authKind: ctx.kind, siteCode }),
498
+ queryFn: () => client.categories.tree(ctx),
458
499
  staleTime: CATEGORIES_STALE_TIME
459
500
  });
460
501
  }
@@ -535,7 +576,12 @@ function useCartMutations(cartId) {
535
576
  if (c) qc.setQueryData(c.key, c.previous);
536
577
  },
537
578
  onSuccess: (cart, _v, c) => {
538
- if (c) qc.setQueryData(c.key, cart);
579
+ if (!c) return;
580
+ if (cart && Array.isArray(cart.items)) {
581
+ qc.setQueryData(c.key, cart);
582
+ } else {
583
+ void qc.invalidateQueries({ queryKey: c.key });
584
+ }
539
585
  }
540
586
  });
541
587
  }
@@ -554,7 +600,9 @@ function useCartMutations(cartId) {
554
600
  ]
555
601
  } : prev
556
602
  ),
557
- updateItem: make((id, v) => client.carts.updateItem(id, v.itemId, v.patch, ctx)),
603
+ updateItem: make(
604
+ (id, v) => client.carts.updateItem(id, v.itemId, v.patch, ctx, v.partial ? { partial: true } : {})
605
+ ),
558
606
  removeItem: make(
559
607
  (id, v) => client.carts.removeItem(id, v.itemId, ctx),
560
608
  (prev, v) => prev ? { ...prev, items: (prev.items ?? []).filter((i) => i.id !== v.itemId) } : prev
@@ -589,6 +637,14 @@ function useActiveCart(opts) {
589
637
  const { activeCompany } = useActiveCompany();
590
638
  const [cartId, setCartId] = useState(() => storage.getCartId());
591
639
  const effectiveLegalEntityId = opts?.legalEntityId ?? activeCompany?.id;
640
+ useEffect(() => {
641
+ if (!storage.subscribeAll) return;
642
+ return storage.subscribeAll((key) => {
643
+ if (key !== "cartId") return;
644
+ const next = storage.getCartId();
645
+ setCartId((prev) => prev === next ? prev : next);
646
+ });
647
+ }, [storage]);
592
648
  useEffect(() => {
593
649
  if (cartId !== null) return;
594
650
  if (!opts?.create) return;
@@ -1419,6 +1475,6 @@ function useUpdateApproval() {
1419
1475
  });
1420
1476
  }
1421
1477
 
1422
- export { useActiveCart, useAddToShoppingList, useAddressMutations, useApproval, useApprovals, useAssignContact, useAvailabilities, useAvailability, useCancelOrder, useCart, useCartMutations, useCategories, useCategoriesInfinite, useCategory, useCategoryTree, useChangePassword, useCheckout, useCompany, useCompanyContacts, useCompanyGroups, useCompanyLocations, useCompanySwitcher, useCreateApproval, useCreateCart, useCreateCompany, useCreateLocation, useCreateReturn, useCreateShoppingList, useCustomerAddresses, useCustomerSession, useDefaultSite, useDeleteCompany, useDeleteLocation, useDeleteShoppingList, useMatchPrices, useMatchPricesChunked, useMyCompanies, useMyOrders, useMyOrdersInfinite, useMyReturns, useMyRewardPoints, useMyRewardPointsSummary, useMySegmentCategories, useMySegmentCategoriesInfinite, useMySegmentCategoryTree, useMySegmentItems, useMySegmentProducts, useMySegmentProductsInfinite, useMySegments, useOrder, useOrderTransition, usePasswordReset, usePaymentModes, useProduct, useProductByCode, useProductMedia, useProductSearch, useProducts, useProductsByCodes, useProductsInCategory, useProductsInCategoryInfinite, useProductsInfinite, useRedeemCoupon, useRedeemOptions, useRedeemRewardPoints, useRemoveFromShoppingList, useReorder, useReturn, useSalesOrder, useSetShoppingListItemQuantity, useShoppingLists, useSiteContext, useSites, useUnassignContact, useUpdateApproval, useUpdateCompany, useUpdateContactAssignment, useUpdateCustomer, useUpdateLocation, useUpdateSalesOrder, useValidateCoupon, useVariantChildren };
1423
- //# sourceMappingURL=chunk-RCI7242Y.js.map
1424
- //# sourceMappingURL=chunk-RCI7242Y.js.map
1478
+ export { useActiveCart, useAddToShoppingList, useAddressMutations, useApproval, useApprovals, useAssignContact, useAvailabilities, useAvailability, useCancelOrder, useCart, useCartMutations, useCategories, useCategoriesInfinite, useCategory, useCategoryTree, useChangePassword, useCheckout, useCompany, useCompanyContacts, useCompanyGroups, useCompanyLocations, useCompanySwitcher, useCreateApproval, useCreateCart, useCreateCompany, useCreateLocation, useCreateReturn, useCreateShoppingList, useCustomerAddresses, useCustomerSession, useDefaultSite, useDeleteCompany, useDeleteLocation, useDeleteShoppingList, useMatchPrices, useMatchPricesChunked, useMyCompanies, useMyOrders, useMyOrdersInfinite, useMyReturns, useMyRewardPoints, useMyRewardPointsSummary, useMySegmentCategories, useMySegmentCategoriesInfinite, useMySegmentCategoryTree, useMySegmentItems, useMySegmentProducts, useMySegmentProductsInfinite, useMySegments, useOrder, useOrderTransition, usePasswordReset, usePaymentModes, useProduct, useProductByCode, useProductMedia, useProductSearch, useProducts, useProductsByCodes, useProductsInCategory, useProductsInCategoryInfinite, useProductsInfinite, useRedeemCoupon, useRedeemOptions, useRedeemRewardPoints, useRemoveFromShoppingList, useReorder, useReturn, useSalesOrder, useSetShoppingListItemQuantity, useShoppingLists, useSiteContext, useSites, useSubcategories, useUnassignContact, useUpdateApproval, useUpdateCompany, useUpdateContactAssignment, useUpdateCustomer, useUpdateLocation, useUpdateSalesOrder, useValidateCoupon, useVariantChildren };
1479
+ //# sourceMappingURL=chunk-GRGWUXNB.js.map
1480
+ //# sourceMappingURL=chunk-GRGWUXNB.js.map