@viu/emporix-sdk-react 2.7.0 → 2.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,120 @@
1
1
  # @viu/emporix-sdk-react
2
2
 
3
+ ## 2.9.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [#108](https://github.com/viuteam/emporix-sdk/pull/108) [`056cb62`](https://github.com/viuteam/emporix-sdk/commit/056cb622106fa5854ec9ebbee6e91c4820e62b29) Thanks [@amnael1](https://github.com/amnael1)! - feat(sdk): generate customer-management types from the real OpenAPI spec
8
+
9
+ Replaces the hand-written customer-management mirror (B2B legal-entities /
10
+ contact-assignments / locations) with codegen output from the vendored
11
+ "Customer Management Service" spec, so Companies/Contacts/Locations return the
12
+ real API shape. The `update` methods (and the matching `useUpdateCompany` /
13
+ `useUpdateContactAssignment` / `useUpdateLocation` hooks) now type their PATCH
14
+ body as `Partial<*Update>` to reflect the partial-update endpoint. `LegalEntity.id`
15
+ and sibling ids are optional in the generated shape, matching the wire contract.
16
+
17
+ - [#109](https://github.com/viuteam/emporix-sdk/pull/109) [`f90e05b`](https://github.com/viuteam/emporix-sdk/commit/f90e05b97f6c022660bc36ac3656e2f48bf78e69) Thanks [@amnael1](https://github.com/amnael1)! - feat(sdk): generate IAM types, add group member mutations
18
+
19
+ Replaces the last hand-written `generated/` mirror (`iam`) with codegen from the
20
+ vendored "IAM Service" spec, so `customerGroups.listForCompany` returns the real
21
+ group shape (`GroupsQueryDocument` — note: the wire uses `code`/`userType`, not
22
+ the previously-mirrored `role`, which never existed on the API). Ships the
23
+ previously-deferred group member mutations now that the endpoints are confirmed:
24
+ `customerGroups.addMember` / `removeMember`, plus the `useAddGroupMember` /
25
+ `useRemoveGroupMember` React hooks. No hand-written generated mirrors remain.
26
+
27
+ - [#107](https://github.com/viuteam/emporix-sdk/pull/107) [`975290c`](https://github.com/viuteam/emporix-sdk/commit/975290c7bd6129754d82e131186cade633394836) Thanks [@amnael1](https://github.com/amnael1)! - feat(product): add searchByName free-text helper + useProductNameSearch
28
+
29
+ `products.searchByName(term)` builds the Emporix `name:(~<term>)` regex filter
30
+ (escaping metacharacters) and delegates to `search`, so consumers no longer
31
+ hand-build the `q` DSL — a bare free-text term otherwise 400s with
32
+ "No value for key …". Adds the `useProductNameSearch` React hook (disabled on
33
+ empty/whitespace).
34
+
35
+ ### Patch Changes
36
+
37
+ - [#106](https://github.com/viuteam/emporix-sdk/pull/106) [`04b95ea`](https://github.com/viuteam/emporix-sdk/commit/04b95eab1fbf6b09ca29b0e3a98605e5ef938c6c) Thanks [@amnael1](https://github.com/amnael1)! - feat(sdk): generate order-v2 types from the real OpenAPI spec
38
+
39
+ Replaces the hand-written `order-v2` type mirror (which invented `items`,
40
+ `{amount,currency}` totals and a top-level `orderNumber`) with codegen output
41
+ from the vendored Emporix Order Service spec. `OrdersService` and
42
+ `SalesOrdersService` now return the real API shape:
43
+ - line items are `entries` (not `items`); each entry has `itemYrn`,
44
+ `orderedAmount`/`amount`, and a nested `product`
45
+ - `totalPrice`/`subTotalPrice` are numbers + a top-level `currency`; rich
46
+ net/gross/tax lives in `calculatedPrice`
47
+ - `orderNumber` is under `mixins.generalAttributes`
48
+ - `SalesOrderPatch` is now `Partial<OrderUpdateDto>` (the real PATCH body)
49
+
50
+ Public type surface: `Order`, `OrderEntry`, `OrderStatus`, `SalesOrder`,
51
+ `Transition`, `SalesOrderPatch`. The unused fictional re-exports (`OrderItem`,
52
+ `OrderMoney`, `OrderCustomer`, `OrderAddress`, `OrderPayment`, `OrderDelivery`,
53
+ `OrderTaxLine`, `OrderMetadata`, `OrderTransition`) are removed — they had no
54
+ runtime counterpart.
55
+
56
+ `useReorder` now reads `entries` and re-adds each with its `itemYrn` + price row
57
+ (`priceId`/amounts/currency) — the cart requires a price, so the previous
58
+ `{ product: { id } }` body always failed; reorder now actually works.
59
+
60
+ ## 2.8.0
61
+
62
+ ### Minor Changes
63
+
64
+ - [#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,
65
+ patch, auth, { partial: true })` now sends `?partial=true`, so a quantity-only
66
+ change can be `{ quantity }` instead of a full item replace (which otherwise
67
+ requires re-sending `itemYrn` + the `price` row). The React
68
+ `useCartMutations().updateItem` mutation accepts an optional `partial` flag in
69
+ its variables. Default behavior is unchanged.
70
+
71
+ - [#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
72
+ tracked internally (from `login` / `exchangeToken`) but not returned — so
73
+ consumers couldn't pass it to `useCheckout().placeOrder({ ..., saasToken })` for
74
+ customer checkout, or to saas-token-gated order reads.
75
+
76
+ ### Patch Changes
77
+
78
+ - [#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
79
+
80
+ `useCartMutations` assumed every cart write echoes the full updated cart.
81
+ A partial quantity update (`updateItem(..., { partial: true })`) returns
82
+ `204 No Content`, which the SDK resolves to `undefined` — and
83
+ `setQueryData(key, undefined)` is a no-op in React Query, so the cart cache
84
+ stayed stale and the UI did not reflect the change. The mutation now adopts
85
+ a real cart body when one is returned and otherwise invalidates the cart
86
+ query so it refetches. This also makes coupon/address/remove mutations
87
+ reconcile with the server when they return no body.
88
+
89
+ - [#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
90
+
91
+ Two related cleanup gaps caused follow-up errors after logout and checkout:
92
+ - `useCustomerSession().logout()` cleared the customer token but left the
93
+ stored `cartId`. The cart belonged to the customer and isn't accessible
94
+ anonymously, so the cart query immediately refetched it and got a `403`.
95
+ Logout now clears `cartId` too.
96
+ - `useActiveCart` cached the cart id in local state and never reacted to
97
+ external `storage.setCartId(null)` (logout, or the post-order cleanup that
98
+ closes the cart). It kept fetching the dead cart id — a `403` after logout,
99
+ a `404` after checkout. It now subscribes to storage cart-id changes and
100
+ syncs, so clearing the id stops the fetch (and a logged-out cart page
101
+ bootstraps a fresh anonymous cart on demand).
102
+
103
+ - [#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
104
+
105
+ `useCustomerSession` kept its session in a per-instance `useState`. The
106
+ `token` slot was mirrored from storage (so `isAuthenticated` was consistent),
107
+ but the in-memory `saasToken` and `refreshToken` lived only in the component
108
+ instance that called `login()`. A different consumer — e.g. the checkout page
109
+ reading `saasToken` for the `saas-token` header — saw `null`, so customer
110
+ checkout failed with `401 "Saas TOKEN is invalid"`.
111
+
112
+ The session now lives in a shared, per-storage store consumed via
113
+ `useSyncExternalStore`, so every `useCustomerSession()` reads the same
114
+ `{ token, refreshToken, saasToken }`. A login in one component is immediately
115
+ visible to all others. The tokens remain in-memory only (still cleared on a
116
+ full reload, by design).
117
+
3
118
  ## 2.7.0
4
119
 
5
120
  ### Minor Changes
@@ -1,5 +1,5 @@
1
- import { useEmporix, EmporixSiteContext, useActiveCompany } from './chunk-SDRV73LG.js';
2
- import { useContext, useState, useEffect, useCallback } from 'react';
1
+ import { useEmporix, EmporixSiteContext, useActiveCompany } from './chunk-JZRSYM3W.js';
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,
@@ -318,6 +348,17 @@ function useProductSearch(query, params = {}, options = {}) {
318
348
  staleTime: PRODUCTS_STALE_TIME
319
349
  });
320
350
  }
351
+ function useProductNameSearch(term, params = {}, options = {}) {
352
+ const { client } = useEmporix();
353
+ const { ctx } = useReadAuth(options.auth);
354
+ const { siteCode } = useReadSite();
355
+ return useQuery({
356
+ queryKey: emporixKey("product-name-search", [term, params], { tenant: client.tenant, authKind: ctx.kind, siteCode }),
357
+ enabled: typeof term === "string" && term.trim() !== "",
358
+ queryFn: () => client.products.searchByName(term, params, ctx),
359
+ staleTime: PRODUCTS_STALE_TIME
360
+ });
361
+ }
321
362
  function useProductsByCodes(codes, options = {}) {
322
363
  const { client } = useEmporix();
323
364
  const { ctx } = useReadAuth(options.auth);
@@ -546,7 +587,12 @@ function useCartMutations(cartId) {
546
587
  if (c) qc.setQueryData(c.key, c.previous);
547
588
  },
548
589
  onSuccess: (cart, _v, c) => {
549
- if (c) qc.setQueryData(c.key, cart);
590
+ if (!c) return;
591
+ if (cart && Array.isArray(cart.items)) {
592
+ qc.setQueryData(c.key, cart);
593
+ } else {
594
+ void qc.invalidateQueries({ queryKey: c.key });
595
+ }
550
596
  }
551
597
  });
552
598
  }
@@ -565,7 +611,9 @@ function useCartMutations(cartId) {
565
611
  ]
566
612
  } : prev
567
613
  ),
568
- updateItem: make((id, v) => client.carts.updateItem(id, v.itemId, v.patch, ctx)),
614
+ updateItem: make(
615
+ (id, v) => client.carts.updateItem(id, v.itemId, v.patch, ctx, v.partial ? { partial: true } : {})
616
+ ),
569
617
  removeItem: make(
570
618
  (id, v) => client.carts.removeItem(id, v.itemId, ctx),
571
619
  (prev, v) => prev ? { ...prev, items: (prev.items ?? []).filter((i) => i.id !== v.itemId) } : prev
@@ -600,6 +648,14 @@ function useActiveCart(opts) {
600
648
  const { activeCompany } = useActiveCompany();
601
649
  const [cartId, setCartId] = useState(() => storage.getCartId());
602
650
  const effectiveLegalEntityId = opts?.legalEntityId ?? activeCompany?.id;
651
+ useEffect(() => {
652
+ if (!storage.subscribeAll) return;
653
+ return storage.subscribeAll((key) => {
654
+ if (key !== "cartId") return;
655
+ const next = storage.getCartId();
656
+ setCartId((prev) => prev === next ? prev : next);
657
+ });
658
+ }, [storage]);
603
659
  useEffect(() => {
604
660
  if (cartId !== null) return;
605
661
  if (!opts?.create) return;
@@ -1066,6 +1122,24 @@ function useDeleteLocation() {
1066
1122
  onSuccess: () => qc.invalidateQueries({ predicate: (q) => q.queryKey.includes("locations") })
1067
1123
  });
1068
1124
  }
1125
+ function useAddGroupMember() {
1126
+ const { client } = useEmporix();
1127
+ const resolveAuth = useCustomerAuthResolver();
1128
+ const qc = useQueryClient();
1129
+ return useMutation({
1130
+ mutationFn: ({ groupId, member }) => client.customerGroups.addMember(groupId, member, resolveAuth()),
1131
+ onSuccess: () => qc.invalidateQueries({ predicate: (q) => q.queryKey.includes("groups") })
1132
+ });
1133
+ }
1134
+ function useRemoveGroupMember() {
1135
+ const { client } = useEmporix();
1136
+ const resolveAuth = useCustomerAuthResolver();
1137
+ const qc = useQueryClient();
1138
+ return useMutation({
1139
+ mutationFn: ({ groupId, userId }) => client.customerGroups.removeMember(groupId, userId, resolveAuth()),
1140
+ onSuccess: () => qc.invalidateQueries({ predicate: (q) => q.queryKey.includes("groups") })
1141
+ });
1142
+ }
1069
1143
  function useCompanySwitcher() {
1070
1144
  const ctx = useActiveCompany();
1071
1145
  const switchFn = useCallback(
@@ -1201,11 +1275,23 @@ function useReorder() {
1201
1275
  });
1202
1276
  const cartId = storage.getCartId();
1203
1277
  if (!cartId) throw new Error("useReorder: no active cart id in storage");
1204
- if (order.items.length === 0) return { added: 0, errors: [] };
1205
- const batchBody = order.items.map((item) => ({
1206
- product: { id: item.productId },
1207
- quantity: item.quantity
1208
- }));
1278
+ const batchBody = (order.entries ?? []).map((entry) => {
1279
+ const p = entry.price;
1280
+ if (!entry.itemYrn || !p?.priceId || p.originalAmount === void 0 || p.effectiveAmount === void 0 || !p.currency) {
1281
+ return null;
1282
+ }
1283
+ return {
1284
+ itemYrn: entry.itemYrn,
1285
+ quantity: entry.orderedAmount ?? entry.amount,
1286
+ price: {
1287
+ priceId: p.priceId,
1288
+ originalAmount: p.originalAmount,
1289
+ effectiveAmount: p.effectiveAmount,
1290
+ currency: p.currency
1291
+ }
1292
+ };
1293
+ }).filter((x) => x !== null);
1294
+ if (batchBody.length === 0) return { added: 0, errors: [] };
1209
1295
  const res = await client.carts.addItemsBatch(cartId, batchBody, ctx);
1210
1296
  let added = 0;
1211
1297
  const errors = [];
@@ -1430,6 +1516,6 @@ function useUpdateApproval() {
1430
1516
  });
1431
1517
  }
1432
1518
 
1433
- 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 };
1434
- //# sourceMappingURL=chunk-TJXNTSXN.js.map
1435
- //# sourceMappingURL=chunk-TJXNTSXN.js.map
1519
+ export { useActiveCart, useAddGroupMember, 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, useProductNameSearch, useProductSearch, useProducts, useProductsByCodes, useProductsInCategory, useProductsInCategoryInfinite, useProductsInfinite, useRedeemCoupon, useRedeemOptions, useRedeemRewardPoints, useRemoveFromShoppingList, useRemoveGroupMember, useReorder, useReturn, useSalesOrder, useSetShoppingListItemQuantity, useShoppingLists, useSiteContext, useSites, useSubcategories, useUnassignContact, useUpdateApproval, useUpdateCompany, useUpdateContactAssignment, useUpdateCustomer, useUpdateLocation, useUpdateSalesOrder, useValidateCoupon, useVariantChildren };
1520
+ //# sourceMappingURL=chunk-3W3TIKRV.js.map
1521
+ //# sourceMappingURL=chunk-3W3TIKRV.js.map