@envive-ai/react-hooks 0.3.46 → 0.3.47

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.
Files changed (240) hide show
  1. package/dist/application/commerce-api.cjs +73 -3
  2. package/dist/application/commerce-api.d.cts +16 -1
  3. package/dist/application/commerce-api.d.ts +16 -1
  4. package/dist/application/commerce-api.js +73 -3
  5. package/dist/application/models/api/userEvent.d.cts +9 -2
  6. package/dist/application/models/api/userEvent.d.ts +9 -2
  7. package/dist/application/models/featureGates.cjs +2 -1
  8. package/dist/application/models/featureGates.d.cts +2 -1
  9. package/dist/application/models/featureGates.d.ts +2 -1
  10. package/dist/application/models/featureGates.js +2 -1
  11. package/dist/application/models/graphql/queries/getProductOptionsQuery.cjs +33 -0
  12. package/dist/application/models/graphql/queries/getProductOptionsQuery.d.cts +28 -0
  13. package/dist/application/models/graphql/queries/getProductOptionsQuery.d.ts +28 -0
  14. package/dist/application/models/graphql/queries/getProductOptionsQuery.js +32 -0
  15. package/dist/application/models/validators/validateResponse.cjs +2 -1
  16. package/dist/application/models/validators/validateResponse.js +2 -1
  17. package/dist/application/models/validators/validateUserEvent.cjs +3 -3
  18. package/dist/application/models/validators/validateUserEvent.js +3 -3
  19. package/dist/application/utils/elementObserver.d.cts +2 -2
  20. package/dist/application/utils/index.cjs +2 -0
  21. package/dist/application/utils/index.d.cts +2 -1
  22. package/dist/application/utils/index.d.ts +2 -1
  23. package/dist/application/utils/index.js +2 -1
  24. package/dist/application/utils/messageFromAddToCartEvent.cjs +32 -0
  25. package/dist/application/utils/messageFromAddToCartEvent.d.cts +19 -0
  26. package/dist/application/utils/messageFromAddToCartEvent.d.ts +19 -0
  27. package/dist/application/utils/messageFromAddToCartEvent.js +31 -0
  28. package/dist/atoms/app/index.cjs +1 -1
  29. package/dist/atoms/app/index.d.cts +7 -7
  30. package/dist/atoms/app/index.d.ts +1 -1
  31. package/dist/atoms/app/index.js +1 -1
  32. package/dist/atoms/app/variant.d.cts +6 -6
  33. package/dist/atoms/app/variant.d.ts +6 -6
  34. package/dist/atoms/chat/chatState.d.cts +19 -19
  35. package/dist/atoms/chat/chatState.d.ts +19 -19
  36. package/dist/atoms/chat/form.d.cts +2 -2
  37. package/dist/atoms/chat/form.d.ts +2 -2
  38. package/dist/atoms/chat/index.d.cts +2 -2
  39. package/dist/atoms/chat/index.d.ts +3 -3
  40. package/dist/atoms/chat/lastMessage.d.cts +2 -2
  41. package/dist/atoms/chat/messageQueue.d.cts +6 -6
  42. package/dist/atoms/chat/performanceMetrics.d.cts +6 -6
  43. package/dist/atoms/chat/performanceMetrics.d.ts +6 -6
  44. package/dist/atoms/chat/renderedWidgetRefs.d.cts +2 -2
  45. package/dist/atoms/chat/replies.d.cts +3 -3
  46. package/dist/atoms/chat/replies.d.ts +2 -2
  47. package/dist/atoms/chat/suggestions.d.cts +2 -2
  48. package/dist/atoms/chat/suggestions.d.ts +2 -2
  49. package/dist/atoms/envive/enviveConfig.cjs +7 -4
  50. package/dist/atoms/envive/enviveConfig.d.cts +4 -2
  51. package/dist/atoms/envive/enviveConfig.d.ts +16 -14
  52. package/dist/atoms/envive/enviveConfig.js +6 -4
  53. package/dist/atoms/globalSearch/globalSearch.d.cts +5 -5
  54. package/dist/atoms/globalSearch/globalSearch.d.ts +5 -5
  55. package/dist/atoms/org/customerService.d.cts +6 -6
  56. package/dist/atoms/org/customerService.d.ts +6 -6
  57. package/dist/atoms/org/graphqlConfig.cjs +1 -1
  58. package/dist/atoms/org/graphqlConfig.d.cts +6 -6
  59. package/dist/atoms/org/graphqlConfig.d.ts +6 -6
  60. package/dist/atoms/org/graphqlConfig.js +1 -1
  61. package/dist/atoms/org/newOrgConfigAtom.d.cts +2 -2
  62. package/dist/atoms/org/newOrgConfigAtom.d.ts +2 -2
  63. package/dist/atoms/org/orgAnalyticsConfig.d.cts +4 -4
  64. package/dist/atoms/org/orgAnalyticsConfig.d.ts +4 -4
  65. package/dist/atoms/search/chatSearch.d.cts +17 -17
  66. package/dist/atoms/search/chatSearch.d.ts +17 -17
  67. package/dist/atoms/search/searchAPI.d.cts +13 -13
  68. package/dist/atoms/search/searchAPI.d.ts +13 -13
  69. package/dist/atoms/widget/chatPreviewLoading.d.cts +2 -2
  70. package/dist/atoms/widget/chatPreviewLoading.d.ts +2 -2
  71. package/dist/contexts/enviveConfigContext/enviveConfigContext.cjs +6 -4
  72. package/dist/contexts/enviveConfigContext/enviveConfigContext.d.cts +3 -1
  73. package/dist/contexts/enviveConfigContext/enviveConfigContext.d.ts +3 -1
  74. package/dist/contexts/enviveConfigContext/enviveConfigContext.js +6 -4
  75. package/dist/contexts/enviveContext/enviveContext.cjs +7 -3
  76. package/dist/contexts/enviveContext/enviveContext.d.cts +5 -1
  77. package/dist/contexts/enviveContext/enviveContext.d.ts +5 -1
  78. package/dist/contexts/enviveContext/enviveContext.js +7 -3
  79. package/dist/contexts/featureFlagServiceContext/featureFlagServiceContext.cjs +2 -97
  80. package/dist/contexts/featureFlagServiceContext/featureFlagServiceContext.d.cts +3 -25
  81. package/dist/contexts/featureFlagServiceContext/featureFlagServiceContext.d.ts +3 -25
  82. package/dist/contexts/featureFlagServiceContext/featureFlagServiceContext.js +3 -97
  83. package/dist/contexts/featureFlagServiceContext/index.cjs +2 -1
  84. package/dist/contexts/featureFlagServiceContext/index.d.cts +2 -1
  85. package/dist/contexts/featureFlagServiceContext/index.d.ts +2 -1
  86. package/dist/contexts/featureFlagServiceContext/index.js +2 -1
  87. package/dist/contexts/graphqlContext/mockV3Config.cjs +1 -1
  88. package/dist/contexts/graphqlContext/mockV3Config.js +1 -1
  89. package/dist/contexts/localStorageContext/index.cjs +2 -1
  90. package/dist/contexts/localStorageContext/index.d.cts +2 -1
  91. package/dist/contexts/localStorageContext/index.d.ts +2 -1
  92. package/dist/contexts/localStorageContext/index.js +2 -1
  93. package/dist/contexts/localStorageContext/localStorageContext.cjs +5 -11
  94. package/dist/contexts/localStorageContext/localStorageContext.d.cts +3 -7
  95. package/dist/contexts/localStorageContext/localStorageContext.d.ts +3 -7
  96. package/dist/contexts/localStorageContext/localStorageContext.js +3 -8
  97. package/dist/contexts/localStorageContext/localStorageKeys.cjs +12 -0
  98. package/dist/contexts/localStorageContext/localStorageKeys.d.cts +9 -0
  99. package/dist/contexts/localStorageContext/localStorageKeys.d.ts +9 -0
  100. package/dist/contexts/localStorageContext/localStorageKeys.js +11 -0
  101. package/dist/contexts/salesAgentContext/chatAPI.cjs +13 -2
  102. package/dist/contexts/salesAgentContext/chatAPI.d.cts +4 -1
  103. package/dist/contexts/salesAgentContext/chatAPI.d.ts +4 -1
  104. package/dist/contexts/salesAgentContext/chatAPI.js +14 -3
  105. package/dist/contexts/salesAgentContext/salesAgentContext.cjs +12 -4
  106. package/dist/contexts/salesAgentContext/salesAgentContext.js +12 -4
  107. package/dist/contexts/systemSettingsContext/systemSettingsContext.d.cts +2 -2
  108. package/dist/contexts/types.cjs +1 -1
  109. package/dist/contexts/types.d.cts +3 -1
  110. package/dist/contexts/types.d.ts +3 -1
  111. package/dist/contexts/types.js +1 -1
  112. package/dist/contexts/typesV3.cjs +1 -1
  113. package/dist/contexts/typesV3.d.cts +11 -2
  114. package/dist/contexts/typesV3.d.ts +12 -3
  115. package/dist/contexts/typesV3.js +1 -1
  116. package/dist/hooks/AddToCart/addToCartErrorMessage.cjs +7 -0
  117. package/dist/hooks/AddToCart/addToCartErrorMessage.d.cts +5 -0
  118. package/dist/hooks/AddToCart/addToCartErrorMessage.d.ts +5 -0
  119. package/dist/hooks/AddToCart/addToCartErrorMessage.js +6 -0
  120. package/dist/hooks/AddToCart/createAddToCartUserEvent.cjs +20 -0
  121. package/dist/hooks/AddToCart/createAddToCartUserEvent.d.cts +17 -0
  122. package/dist/hooks/AddToCart/createAddToCartUserEvent.d.ts +17 -0
  123. package/dist/hooks/AddToCart/createAddToCartUserEvent.js +19 -0
  124. package/dist/hooks/AddToCart/index.cjs +9 -0
  125. package/dist/hooks/AddToCart/index.d.cts +5 -0
  126. package/dist/hooks/AddToCart/index.d.ts +5 -0
  127. package/dist/hooks/AddToCart/index.js +6 -0
  128. package/dist/hooks/AddToCart/parseAddToCartFunction.cjs +27 -0
  129. package/dist/hooks/AddToCart/parseAddToCartFunction.d.cts +13 -0
  130. package/dist/hooks/AddToCart/parseAddToCartFunction.d.ts +13 -0
  131. package/dist/hooks/AddToCart/parseAddToCartFunction.js +27 -0
  132. package/dist/hooks/AddToCart/useAddToCart.cjs +24 -0
  133. package/dist/hooks/AddToCart/useAddToCart.d.cts +14 -0
  134. package/dist/hooks/AddToCart/useAddToCart.d.ts +14 -0
  135. package/dist/hooks/AddToCart/useAddToCart.js +23 -0
  136. package/dist/hooks/GrabAndScroll/useGrabAndScroll.d.cts +2 -2
  137. package/dist/hooks/LocalStorageOperations/useLocalStorageOperations.cjs +6 -5
  138. package/dist/hooks/LocalStorageOperations/useLocalStorageOperations.js +3 -2
  139. package/dist/hooks/PageViewedEvent/usePageViewedEvent.cjs +5 -2
  140. package/dist/hooks/PageViewedEvent/usePageViewedEvent.js +5 -2
  141. package/dist/hooks/ProductDetails/index.cjs +9 -0
  142. package/dist/hooks/ProductDetails/index.d.cts +4 -0
  143. package/dist/hooks/ProductDetails/index.d.ts +4 -0
  144. package/dist/hooks/ProductDetails/index.js +4 -0
  145. package/dist/hooks/ProductDetails/useProductDetails.cjs +53 -0
  146. package/dist/hooks/ProductDetails/useProductDetails.d.cts +20 -0
  147. package/dist/hooks/ProductDetails/useProductDetails.d.ts +20 -0
  148. package/dist/hooks/ProductDetails/useProductDetails.js +52 -0
  149. package/dist/hooks/ProductDetails/variantSelection.cjs +42 -0
  150. package/dist/hooks/ProductDetails/variantSelection.d.cts +35 -0
  151. package/dist/hooks/ProductDetails/variantSelection.d.ts +35 -0
  152. package/dist/hooks/ProductDetails/variantSelection.js +38 -0
  153. package/dist/hooks/Search/useSearch.d.cts +1 -1
  154. package/dist/hooks/Search/useSearch.d.ts +1 -1
  155. package/dist/hooks/SystemSettingsContext/useSystemSettingsContext.d.cts +2 -2
  156. package/dist/hooks/TrackComponentVisibleEvent/useTrackComponentVisibleEvent.cjs +11 -2
  157. package/dist/hooks/TrackComponentVisibleEvent/useTrackComponentVisibleEvent.d.cts +1 -10
  158. package/dist/hooks/TrackComponentVisibleEvent/useTrackComponentVisibleEvent.d.ts +1 -10
  159. package/dist/hooks/TrackComponentVisibleEvent/useTrackComponentVisibleEvent.js +11 -2
  160. package/dist/hooks/WidgetInteraction/types.cjs +12 -2
  161. package/dist/hooks/WidgetInteraction/types.d.cts +28 -3
  162. package/dist/hooks/WidgetInteraction/types.d.ts +28 -3
  163. package/dist/hooks/WidgetInteraction/types.js +12 -2
  164. package/dist/hooks/utils.d.ts +1 -1
  165. package/dist/services/amplitudeService/amplitudeService.cjs +16 -10
  166. package/dist/services/amplitudeService/amplitudeService.d.cts +5 -2
  167. package/dist/services/amplitudeService/amplitudeService.d.ts +5 -2
  168. package/dist/services/amplitudeService/amplitudeService.js +14 -7
  169. package/dist/services/enviveConfigService/enviveConfigService.cjs +3 -4
  170. package/dist/services/enviveConfigService/enviveConfigService.d.cts +1 -1
  171. package/dist/services/enviveConfigService/enviveConfigService.d.ts +1 -1
  172. package/dist/services/enviveConfigService/enviveConfigService.js +2 -3
  173. package/dist/services/enviveConfigService/fetchGraphQLConfig.cjs +8 -7
  174. package/dist/services/enviveConfigService/fetchGraphQLConfig.js +8 -6
  175. package/dist/services/featureFlagService/index.cjs +101 -0
  176. package/dist/services/featureFlagService/index.d.cts +28 -0
  177. package/dist/services/featureFlagService/index.d.ts +28 -0
  178. package/dist/services/featureFlagService/index.js +101 -0
  179. package/dist/services/ga4ProjectionService/ga4EventSchema.cjs +8 -2
  180. package/dist/services/ga4ProjectionService/ga4EventSchema.js +8 -2
  181. package/dist/services/hardcopyService/hardcopyService.d.cts +1 -1
  182. package/dist/services/hardcopyService/hardcopyService.d.ts +1 -1
  183. package/dist/services/urlResolverWarmupService/index.cjs +54 -0
  184. package/dist/services/urlResolverWarmupService/index.d.cts +40 -0
  185. package/dist/services/urlResolverWarmupService/index.d.ts +40 -0
  186. package/dist/services/urlResolverWarmupService/index.js +54 -0
  187. package/dist/types/enviveConfig.d.cts +6 -2
  188. package/dist/types/enviveConfig.d.ts +6 -2
  189. package/dist/types/index.d.cts +2 -2
  190. package/dist/types/index.d.ts +2 -2
  191. package/package.json +17 -1
  192. package/src/application/__tests__/commerceApiAddToCartRehydration.test.ts +126 -0
  193. package/src/application/commerce-api.ts +142 -23
  194. package/src/application/models/api/userEvent.ts +8 -1
  195. package/src/application/models/featureGates.ts +5 -0
  196. package/src/application/models/graphql/queries/getProductOptionsQuery.ts +59 -0
  197. package/src/application/models/validators/__tests__/validateUserEvent.test.ts +41 -0
  198. package/src/application/models/validators/validateResponse.ts +1 -0
  199. package/src/application/models/validators/validateUserEvent.ts +7 -2
  200. package/src/application/utils/__tests__/messageFromAddToCartEvent.test.ts +109 -0
  201. package/src/application/utils/index.ts +1 -0
  202. package/src/application/utils/messageFromAddToCartEvent.ts +32 -0
  203. package/src/atoms/app/index.ts +1 -1
  204. package/src/atoms/envive/enviveConfig.ts +6 -1
  205. package/src/atoms/org/graphqlConfig.ts +1 -1
  206. package/src/contexts/enviveConfigContext/enviveConfigContext.tsx +5 -0
  207. package/src/contexts/enviveContext/enviveContext.tsx +10 -0
  208. package/src/contexts/featureFlagServiceContext/featureFlagServiceContext.tsx +6 -173
  209. package/src/contexts/localStorageContext/localStorageContext.tsx +2 -5
  210. package/src/contexts/localStorageContext/localStorageKeys.ts +8 -0
  211. package/src/contexts/salesAgentContext/__tests__/chatAPI.test.tsx +63 -0
  212. package/src/contexts/salesAgentContext/chatAPI.ts +20 -1
  213. package/src/contexts/salesAgentContext/salesAgentContext.tsx +10 -0
  214. package/src/contexts/types.ts +2 -0
  215. package/src/contexts/typesV3.ts +14 -0
  216. package/src/hooks/AddToCart/__tests__/parseAddToCartFunction.test.ts +31 -0
  217. package/src/hooks/AddToCart/__tests__/useAddToCart.test.tsx +204 -0
  218. package/src/hooks/AddToCart/addToCartErrorMessage.ts +2 -0
  219. package/src/hooks/AddToCart/createAddToCartUserEvent.ts +25 -0
  220. package/src/hooks/AddToCart/index.ts +4 -0
  221. package/src/hooks/AddToCart/parseAddToCartFunction.ts +31 -0
  222. package/src/hooks/AddToCart/useAddToCart.ts +35 -0
  223. package/src/hooks/PageViewedEvent/usePageViewedEvent.ts +4 -0
  224. package/src/hooks/ProductDetails/__tests__/useProductDetails.test.tsx +129 -0
  225. package/src/hooks/ProductDetails/__tests__/variantSelection.test.ts +145 -0
  226. package/src/hooks/ProductDetails/index.ts +2 -0
  227. package/src/hooks/ProductDetails/useProductDetails.ts +62 -0
  228. package/src/hooks/ProductDetails/variantSelection.ts +61 -0
  229. package/src/hooks/TrackComponentVisibleEvent/useTrackComponentVisibleEvent.ts +15 -0
  230. package/src/hooks/WidgetInteraction/types.ts +42 -1
  231. package/src/services/amplitudeService/__tests__/amplitudeService.test.ts +8 -8
  232. package/src/services/amplitudeService/amplitudeService.ts +42 -23
  233. package/src/services/enviveConfigService/enviveConfigService.ts +2 -2
  234. package/src/services/enviveConfigService/fetchGraphQLConfig.ts +17 -6
  235. package/src/services/featureFlagService/index.ts +176 -0
  236. package/src/services/ga4ProjectionService/__tests__/ga4ProjectionService.test.ts +36 -0
  237. package/src/services/ga4ProjectionService/ga4EventSchema.ts +7 -0
  238. package/src/services/urlResolverWarmupService/__tests__/urlResolverWarmupService.test.ts +85 -0
  239. package/src/services/urlResolverWarmupService/index.ts +89 -0
  240. package/src/types/enviveConfig.ts +3 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@envive-ai/react-hooks",
3
- "version": "0.3.46",
3
+ "version": "0.3.47",
4
4
  "description": "React hooks for connecting to Envive AI services.",
5
5
  "keywords": [
6
6
  "react",
@@ -274,6 +274,10 @@
274
274
  "import": "./dist/events/index.js",
275
275
  "require": "./dist/events/index.cjs"
276
276
  },
277
+ "./hooks/AddToCart": {
278
+ "import": "./dist/hooks/AddToCart/index.js",
279
+ "require": "./dist/hooks/AddToCart/index.cjs"
280
+ },
277
281
  "./hooks/AmplitudeOperations": {
278
282
  "import": "./dist/hooks/AmplitudeOperations/index.js",
279
283
  "require": "./dist/hooks/AmplitudeOperations/index.cjs"
@@ -350,6 +354,10 @@
350
354
  "import": "./dist/hooks/PageViewedEvent/index.js",
351
355
  "require": "./dist/hooks/PageViewedEvent/index.cjs"
352
356
  },
357
+ "./hooks/ProductDetails": {
358
+ "import": "./dist/hooks/ProductDetails/index.js",
359
+ "require": "./dist/hooks/ProductDetails/index.cjs"
360
+ },
353
361
  "./hooks/ProductImageUrl": {
354
362
  "import": "./dist/hooks/ProductImageUrl/index.js",
355
363
  "require": "./dist/hooks/ProductImageUrl/index.cjs"
@@ -422,10 +430,18 @@
422
430
  "import": "./dist/services/enviveConfigService/index.js",
423
431
  "require": "./dist/services/enviveConfigService/index.cjs"
424
432
  },
433
+ "./services/featureFlagService": {
434
+ "import": "./dist/services/featureFlagService/index.js",
435
+ "require": "./dist/services/featureFlagService/index.cjs"
436
+ },
425
437
  "./services/hardcopyService": {
426
438
  "import": "./dist/services/hardcopyService/index.js",
427
439
  "require": "./dist/services/hardcopyService/index.cjs"
428
440
  },
441
+ "./services/urlResolverWarmupService": {
442
+ "import": "./dist/services/urlResolverWarmupService/index.js",
443
+ "require": "./dist/services/urlResolverWarmupService/index.cjs"
444
+ },
429
445
  "./services/userIdentityService": {
430
446
  "import": "./dist/services/userIdentityService/index.js",
431
447
  "require": "./dist/services/userIdentityService/index.cjs"
@@ -0,0 +1,126 @@
1
+ import { ResponseCategory, UserEventCategory } from '@spiffy-ai/commerce-api-client';
2
+ import { getAtomStore } from 'src/atoms/atomStore/atomStore';
3
+ import { enviveConfigAtom } from 'src/atoms/envive/enviveConfig';
4
+ import { MessageType } from 'src/application/models';
5
+ import CommerceApiClient from '../commerce-api';
6
+
7
+ const { mockV1GetSessionMessages } = vi.hoisted(() => ({
8
+ mockV1GetSessionMessages: vi.fn(),
9
+ }));
10
+
11
+ vi.mock('@spiffy-ai/commerce-api-client', async (importOriginal: () => Promise<unknown>) => {
12
+ const actual = (await importOriginal()) as typeof import('@spiffy-ai/commerce-api-client');
13
+ return {
14
+ ...actual,
15
+ DefaultApi: vi.fn().mockImplementation(function DefaultApi() {
16
+ return { v1GetSessionMessages: mockV1GetSessionMessages };
17
+ }),
18
+ };
19
+ });
20
+
21
+ const makeSessionResponse = (overrides: Record<string, unknown> = {}) => ({
22
+ responses: [],
23
+ suggestions: [],
24
+ user_events: [],
25
+ ...overrides,
26
+ });
27
+
28
+ const productResponse = (id: string, title: string) => ({
29
+ id: `resp-${id}`,
30
+ created_at: '2026-07-20T00:00:00.000Z',
31
+ category: ResponseCategory.Product,
32
+ attributes: {
33
+ id,
34
+ title,
35
+ url: `https://example.com/${id}`,
36
+ },
37
+ });
38
+
39
+ const addToCartEvent = (eventId: string, productId: string) => ({
40
+ event_id: eventId,
41
+ created_at: '2026-07-20T00:00:01.000Z',
42
+ category: UserEventCategory.AddToCart,
43
+ attributes: {
44
+ product_id: productId,
45
+ parent_product_id: '',
46
+ url: '',
47
+ },
48
+ });
49
+
50
+ beforeEach(() => {
51
+ vi.clearAllMocks();
52
+ getAtomStore().set(enviveConfigAtom, {
53
+ baseUrl: 'https://commerce-api.test',
54
+ orgLevelApiKey: 'test-key',
55
+ orgId: 'org-1',
56
+ orgShortName: 'test-org',
57
+ env: 'dev',
58
+ contextSource: 'app',
59
+ });
60
+ });
61
+
62
+ describe('CommerceApiClient.getResponses — AddToCart title resolution on rehydration', () => {
63
+ it('resolves the title for free from a matching Product response, without a fallback fetch', async () => {
64
+ mockV1GetSessionMessages.mockResolvedValueOnce(
65
+ makeSessionResponse({
66
+ responses: [[productResponse('sku-tier1', 'Tier One Sweater')]],
67
+ user_events: [addToCartEvent('evt-tier1', 'sku-tier1')],
68
+ }),
69
+ );
70
+ const fetchSpy = vi.spyOn(global, 'fetch');
71
+
72
+ const { messages } = await CommerceApiClient.getResponses('org-1', 'chat-1', 'user-1');
73
+
74
+ const addToCartMessage = messages.flat().find(m => m.id === 'evt-tier1');
75
+ expect(addToCartMessage?.type).toBe(MessageType.QueryTyped);
76
+ if (addToCartMessage?.type === MessageType.QueryTyped) {
77
+ expect(addToCartMessage.metadata.content).toBe('Add Tier One Sweater to my cart');
78
+ }
79
+ expect(fetchSpy).not.toHaveBeenCalled();
80
+ });
81
+
82
+ it('falls back to a cached product-details fetch when no matching response exists', async () => {
83
+ mockV1GetSessionMessages.mockResolvedValueOnce(
84
+ makeSessionResponse({
85
+ user_events: [addToCartEvent('evt-tier2', 'sku-tier2')],
86
+ }),
87
+ );
88
+ const fetchMock = vi.fn().mockResolvedValue({
89
+ ok: true,
90
+ json: async () => ({ data: { me: { productOptions: { title: 'Tier Two Boots' } } } }),
91
+ });
92
+ vi.stubGlobal('fetch', fetchMock);
93
+
94
+ const { messages } = await CommerceApiClient.getResponses('org-1', 'chat-1', 'user-1');
95
+
96
+ const addToCartMessage = messages.flat().find(m => m.id === 'evt-tier2');
97
+ expect(addToCartMessage?.type).toBe(MessageType.QueryTyped);
98
+ if (addToCartMessage?.type === MessageType.QueryTyped) {
99
+ expect(addToCartMessage.metadata.content).toBe('Add Tier Two Boots to my cart');
100
+ }
101
+ expect(fetchMock).toHaveBeenCalledTimes(1);
102
+ });
103
+
104
+ it('degrades to the raw productId when neither tier resolves a title', async () => {
105
+ mockV1GetSessionMessages.mockResolvedValueOnce(
106
+ makeSessionResponse({
107
+ user_events: [addToCartEvent('evt-tier3', 'sku-tier3-unresolvable')],
108
+ }),
109
+ );
110
+ vi.stubGlobal(
111
+ 'fetch',
112
+ vi.fn().mockResolvedValue({
113
+ ok: true,
114
+ json: async () => ({ data: { me: { productOptions: null } } }),
115
+ }),
116
+ );
117
+
118
+ const { messages } = await CommerceApiClient.getResponses('org-1', 'chat-1', 'user-1');
119
+
120
+ const addToCartMessage = messages.flat().find(m => m.id === 'evt-tier3');
121
+ expect(addToCartMessage?.type).toBe(MessageType.QueryTyped);
122
+ if (addToCartMessage?.type === MessageType.QueryTyped) {
123
+ expect(addToCartMessage.metadata.content).toBe('Add sku-tier3-unresolvable to my cart');
124
+ }
125
+ });
126
+ });
@@ -28,6 +28,10 @@ import {
28
28
  UserEvent,
29
29
  } from 'src/application/models';
30
30
  import { OrgConfig } from 'src/application/models/api/orgConfigResults';
31
+ import {
32
+ ProductDetails,
33
+ getProductOptionsQuery,
34
+ } from 'src/application/models/graphql/queries/getProductOptionsQuery';
31
35
  import { ProductExperiment } from 'src/application/models/productExperiment';
32
36
  import { validateOrgConfigResults } from 'src/application/models/validators/validateOrgConfigResults';
33
37
  import { validateResponse } from 'src/application/models/validators/validateResponse';
@@ -35,6 +39,7 @@ import { validateSuggestion } from 'src/application/models/validators/validateSu
35
39
  import { validateUserEvent } from 'src/application/models/validators/validateUserEvent';
36
40
  import {
37
41
  getQueryParam,
42
+ messageFromAddToCartEvent,
38
43
  messageFromQueryEvent,
39
44
  messageFromResponse,
40
45
  messageFromSuggestionEvent,
@@ -146,7 +151,26 @@ class CommerceApiClient {
146
151
  this.productsApi = new ProductsApi(config);
147
152
  }
148
153
 
154
+ // Dedupes concurrent resolutions of the same URL: the injection bundle
155
+ // warms this call before React mounts, and PageProvider may request the
156
+ // same URL while the warm-up is still in flight.
157
+ private static inFlightUrlResolutions = new Map<string, Promise<unknown>>();
158
+
149
159
  static resolveUrl = async (url: string) => {
160
+ const inFlight = CommerceApiClient.inFlightUrlResolutions.get(url);
161
+ if (inFlight) {
162
+ return inFlight;
163
+ }
164
+ const request = CommerceApiClient.resolveUrlUncached(url);
165
+ CommerceApiClient.inFlightUrlResolutions.set(url, request);
166
+ try {
167
+ return await request;
168
+ } finally {
169
+ CommerceApiClient.inFlightUrlResolutions.delete(url);
170
+ }
171
+ };
172
+
173
+ private static resolveUrlUncached = async (url: string) => {
150
174
  const atomStore = getAtomStore();
151
175
  const orgShortName = atomStore.get(orgShortNameAtom);
152
176
  const orgId = atomStore.get(orgIdAtom);
@@ -405,33 +429,64 @@ class CommerceApiClient {
405
429
  )
406
430
  .filter(turn => turn.length > 0);
407
431
 
408
- const userMessages: Message[][] = userEvents
409
- .map(event => {
410
- if ([UserEventCategory.QueryTyped, UserEventCategory.Search].includes(event.category)) {
411
- return [messageFromQueryEvent(event)];
412
- }
432
+ const userMessages: Message[][] = (
433
+ await Promise.all(
434
+ userEvents.map(async event => {
435
+ if ([UserEventCategory.QueryTyped, UserEventCategory.Search].includes(event.category)) {
436
+ return [messageFromQueryEvent(event)];
437
+ }
413
438
 
414
- if (event.category === UserEventCategory.SuggestionClicked) {
415
- return [messageFromSuggestionEvent(event, suggestions)];
416
- }
439
+ if (event.category === UserEventCategory.SuggestionClicked) {
440
+ return [messageFromSuggestionEvent(event, suggestions)];
441
+ }
417
442
 
418
- if (event.category === UserEventCategory.FormSubmitted) {
419
- const formResponse = responses
420
- .flat()
421
- .find(
422
- response =>
423
- response.id === event.attributes.formResponseId &&
424
- event.attributes.formType !== FormType.Escalation,
425
- );
426
-
427
- if (formResponse && formResponse.category === ResponseCategory.Form) {
428
- return [messageFromFormSubmittedEvent(event, formResponse.attributes)];
443
+ if (event.category === UserEventCategory.AddToCart) {
444
+ // Historical events never carried a client-local productTitle (it's
445
+ // dropped before hitting the backend), so resolve it here: first for
446
+ // free from a matching Product response already fetched above, then
447
+ // as a fallback via the cached product-details call.
448
+ const matchedProduct = responses
449
+ .flat()
450
+ .find(
451
+ response =>
452
+ response.category === ResponseCategory.Product &&
453
+ response.attributes.id === event.attributes.productId,
454
+ );
455
+ let productTitle =
456
+ matchedProduct?.category === ResponseCategory.Product
457
+ ? matchedProduct.attributes.title
458
+ : undefined;
459
+ if (!productTitle) {
460
+ try {
461
+ const details = await CommerceApiClient.retrieveProductDetails(
462
+ event.attributes.productId,
463
+ );
464
+ productTitle = details?.title;
465
+ } catch {
466
+ productTitle = undefined;
467
+ }
468
+ }
469
+ return [messageFromAddToCartEvent(event, productTitle)];
470
+ }
471
+
472
+ if (event.category === UserEventCategory.FormSubmitted) {
473
+ const formResponse = responses
474
+ .flat()
475
+ .find(
476
+ response =>
477
+ response.id === event.attributes.formResponseId &&
478
+ event.attributes.formType !== FormType.Escalation,
479
+ );
480
+
481
+ if (formResponse && formResponse.category === ResponseCategory.Form) {
482
+ return [messageFromFormSubmittedEvent(event, formResponse.attributes)];
483
+ }
429
484
  }
430
- }
431
485
 
432
- return [];
433
- })
434
- .filter((message): message is Message[] => message.length > 0);
486
+ return [];
487
+ }),
488
+ )
489
+ ).filter((message): message is Message[] => message.length > 0);
435
490
 
436
491
  // Sort the messages chronologically to reconstruct the chat history
437
492
  const sortedMessages = [...assistantMessages, ...userMessages].sort(
@@ -494,6 +549,70 @@ class CommerceApiClient {
494
549
  }
495
550
  };
496
551
 
552
+ // Caches resolved product details by productId for the lifetime of the
553
+ // session: variant selection, the display of the add-to-cart chat message,
554
+ // and useProductDetails all resolve the same product repeatedly. Failed
555
+ // requests are evicted so a later call can retry.
556
+ private static productDetailsCache = new Map<string, Promise<ProductDetails | undefined>>();
557
+
558
+ static retrieveProductDetails = (productId: string): Promise<ProductDetails | undefined> => {
559
+ const cached = CommerceApiClient.productDetailsCache.get(productId);
560
+ if (cached) {
561
+ return cached;
562
+ }
563
+ const request = CommerceApiClient.retrieveProductDetailsUncached(productId).catch(err => {
564
+ CommerceApiClient.productDetailsCache.delete(productId);
565
+ throw err;
566
+ });
567
+ CommerceApiClient.productDetailsCache.set(productId, request);
568
+ return request;
569
+ };
570
+
571
+ /**
572
+ * Retrieves variant-level product details for a single product via the GraphQL
573
+ * `me.productOptions` query on POST /v1/graphql. Only meaningful for products
574
+ * that offer variations.
575
+ *
576
+ * @param productId - The product ID to retrieve details for
577
+ * @returns ProductDetails, or undefined when the product has no options data
578
+ * @throws on network/HTTP failure or GraphQL errors
579
+ */
580
+ private static retrieveProductDetailsUncached = async (
581
+ productId: string,
582
+ ): Promise<ProductDetails | undefined> => {
583
+ const atomStore = getAtomStore();
584
+ const baseUrl = atomStore.get(baseUrlAtom);
585
+ const apiKey = atomStore.get(orgLevelApiKeyAtom);
586
+
587
+ try {
588
+ const response = await fetch(`${baseUrl}/v1/graphql`, {
589
+ method: 'POST',
590
+ headers: {
591
+ 'Content-Type': 'application/json',
592
+ Authorization: `Bearer ${apiKey}`,
593
+ },
594
+ body: JSON.stringify({
595
+ query: getProductOptionsQuery(),
596
+ variables: { productId },
597
+ }),
598
+ });
599
+
600
+ if (!response.ok) {
601
+ throw new ResponseError(response, `GraphQL request failed: ${response.statusText}`);
602
+ }
603
+
604
+ const result = await response.json();
605
+ if (result.errors) {
606
+ throw new Error(`GraphQL errors: ${JSON.stringify(result.errors)}`);
607
+ }
608
+
609
+ return result.data?.me?.productOptions ?? undefined;
610
+ } catch (err) {
611
+ await throwSessionRestartRequiredIf('Failed to retrieve product details', err);
612
+ return undefined;
613
+ }
614
+ };
615
+
497
616
  /**
498
617
  *
499
618
  * @param payload
@@ -12,7 +12,14 @@ import {
12
12
  import { CamelCasedPropertiesDeep } from 'src/application/models/utilityTypes';
13
13
 
14
14
  // TODO flatten these out or explore new typescript openapi generator fork
15
- export interface PDPAttributes extends CamelCasedPropertiesDeep<ApiPDPAttributes> {}
15
+ export interface PDPAttributes extends CamelCasedPropertiesDeep<ApiPDPAttributes> {
16
+ /**
17
+ * Client-local only: never serialized to the backend (coreUserEventToApiUserEvent
18
+ * hand-picks fields and ignores this one). Lets the live add-to-cart chat message
19
+ * show a resolved product title instead of the raw productId.
20
+ */
21
+ productTitle?: string;
22
+ }
16
23
 
17
24
  export interface PLPAttributes extends CamelCasedPropertiesDeep<ApiPLPAttributes> {}
18
25
 
@@ -27,6 +27,11 @@ export enum FeatureGates {
27
27
  IsAiSuggestionsVariantEnabled = 'is_ai_suggestions_variant_enabled',
28
28
  IsAiSuggestionsVariantAEnabled = 'is_ai_suggestions_variant_a_enabled',
29
29
  IsAiSuggestionsVariantBEnabled = 'is_ai_suggestions_variant_b_enabled',
30
+ // Serves the agentic storefront (FullPageSalesAgentV3 welcome overlay)
31
+ // independently of the pdp/plp variant_b experiment. The backend serves the
32
+ // storefront under this gate and falls back to variant_b only until this
33
+ // ships, so an org can run the storefront without opting pdp/plp into variant_b.
34
+ IsAiSuggestionsStorefrontEnabled = 'is_ai_suggestions_storefront_enabled',
30
35
  IsVoiceInputEnabled = 'is_voice_input_enabled',
31
36
  // Statsig experiments / unified getWidgetConfig endpoint migration. When
32
37
  // true, EnviveConfigService calls getWidgetConfig (experiment-aware)
@@ -0,0 +1,59 @@
1
+ // Fetches variant-level product details (options, availability, pricing,
2
+ // images) for a single product. Only meaningful for products that offer
3
+ // variations — non-variant PDP surfaces should keep using the REST
4
+ // /v1/products/retrieve path (CommerceApiClient.retrieveProducts).
5
+ export const getProductOptionsQuery = () => `
6
+ query($productId: String!) {
7
+ me {
8
+ productOptions(productId: $productId) {
9
+ productId
10
+ virtualParentId
11
+ title
12
+ options {
13
+ name
14
+ values
15
+ }
16
+ variants {
17
+ variantId
18
+ available
19
+ originalPrice
20
+ salePrice
21
+ imageUrl
22
+ url
23
+ options {
24
+ name
25
+ value
26
+ }
27
+ }
28
+ }
29
+ }
30
+ }
31
+ `;
32
+
33
+ export interface ProductVariantOption {
34
+ name: string;
35
+ value: string;
36
+ }
37
+
38
+ export interface ProductVariant {
39
+ variantId: string;
40
+ available: boolean;
41
+ originalPrice?: number;
42
+ salePrice?: number;
43
+ imageUrl?: string;
44
+ url?: string;
45
+ options: ProductVariantOption[];
46
+ }
47
+
48
+ export interface ProductOption {
49
+ name: string;
50
+ values: string[];
51
+ }
52
+
53
+ export interface ProductDetails {
54
+ productId: string;
55
+ virtualParentId?: string;
56
+ title: string;
57
+ options: ProductOption[];
58
+ variants: ProductVariant[];
59
+ }
@@ -0,0 +1,41 @@
1
+ import { UserEventCategory } from '@spiffy-ai/commerce-api-client';
2
+ import { validateUserEvent } from 'src/application/models/validators/validateUserEvent';
3
+
4
+ describe('validateUserEvent', () => {
5
+ it('preserves productId, parentProductId, and url for an AddToCart event', () => {
6
+ const result = validateUserEvent({
7
+ event_id: 'event-1',
8
+ created_at: '2026-07-20T00:00:00.000Z',
9
+ category: UserEventCategory.AddToCart,
10
+ attributes: {
11
+ product_id: 'product-1',
12
+ parent_product_id: 'parent-1',
13
+ url: 'https://example.com/product-1',
14
+ },
15
+ });
16
+
17
+ expect(result).toEqual({
18
+ eventId: 'event-1',
19
+ createdAt: '2026-07-20T00:00:00.000Z',
20
+ category: UserEventCategory.AddToCart,
21
+ attributes: {
22
+ productId: 'product-1',
23
+ parentProductId: 'parent-1',
24
+ url: 'https://example.com/product-1',
25
+ },
26
+ });
27
+ });
28
+
29
+ it('does not fall through to AppLoaded for an AddToCart event', () => {
30
+ const result = validateUserEvent({
31
+ event_id: 'event-1',
32
+ created_at: '2026-07-20T00:00:00.000Z',
33
+ category: UserEventCategory.AddToCart,
34
+ attributes: {
35
+ product_id: 'product-1',
36
+ },
37
+ });
38
+
39
+ expect(result?.category).toBe(UserEventCategory.AddToCart);
40
+ });
41
+ });
@@ -48,6 +48,7 @@ export const validateResponse = (data: unknown): Response | undefined => {
48
48
  category: ResponseCategory.Product,
49
49
  attributes: {
50
50
  id: data.attributes.id,
51
+ itemGroupId: data.attributes.item_group_id,
51
52
  description: data.attributes.description,
52
53
  imageUrl: data.attributes.image_url,
53
54
  imageUrls: data.attributes.image_urls,
@@ -16,11 +16,16 @@ export const validateUserEvent = (data: unknown): UserEvent | undefined => {
16
16
  return undefined;
17
17
  }
18
18
 
19
- if (data.category === UserEventCategory.PdpVisit && isApiPDPAttributes(data.attributes)) {
19
+ // PdpVisit and AddToCart share the same attributes structure
20
+ if (
21
+ (data.category === UserEventCategory.PdpVisit ||
22
+ data.category === UserEventCategory.AddToCart) &&
23
+ isApiPDPAttributes(data.attributes)
24
+ ) {
20
25
  return {
21
26
  eventId: data.event_id,
22
27
  createdAt: data.created_at,
23
- category: UserEventCategory.PdpVisit,
28
+ category: data.category,
24
29
  attributes: {
25
30
  productId: data.attributes.product_id,
26
31
  parentProductId: data.attributes.parent_product_id,
@@ -0,0 +1,109 @@
1
+ import { UserEventCategory } from '@spiffy-ai/commerce-api-client';
2
+ import { MessageRole, MessageType, UserEvent } from 'src/application/models';
3
+ import { messageFromAddToCartEvent } from '../messageFromAddToCartEvent';
4
+
5
+ describe('messageFromAddToCartEvent', () => {
6
+ it('builds a user message from an AddToCart event', () => {
7
+ const event: UserEvent = {
8
+ eventId: 'event-1',
9
+ createdAt: '2026-07-20T00:00:00.000Z',
10
+ category: UserEventCategory.AddToCart,
11
+ attributes: {
12
+ productId: 'product-1',
13
+ parentProductId: '',
14
+ url: '',
15
+ },
16
+ };
17
+
18
+ expect(messageFromAddToCartEvent(event)).toEqual({
19
+ id: 'event-1',
20
+ role: MessageRole.User,
21
+ type: MessageType.QueryTyped,
22
+ createdAt: '2026-07-20T00:00:00.000Z',
23
+ metadata: {
24
+ content: 'Add product-1 to my cart',
25
+ },
26
+ });
27
+ });
28
+
29
+ it('returns undefined for a non-AddToCart event', () => {
30
+ const event: UserEvent = {
31
+ eventId: 'event-1',
32
+ createdAt: '2026-07-20T00:00:00.000Z',
33
+ category: UserEventCategory.AppLoaded,
34
+ };
35
+
36
+ expect(messageFromAddToCartEvent(event)).toBeUndefined();
37
+ });
38
+
39
+ it('prefers the explicit productTitle override parameter', () => {
40
+ const event: UserEvent = {
41
+ eventId: 'event-1',
42
+ createdAt: '2026-07-20T00:00:00.000Z',
43
+ category: UserEventCategory.AddToCart,
44
+ attributes: {
45
+ productId: 'product-1',
46
+ parentProductId: '',
47
+ url: '',
48
+ productTitle: 'Attribute Title',
49
+ },
50
+ };
51
+
52
+ expect(messageFromAddToCartEvent(event, 'Override Title')).toEqual({
53
+ id: 'event-1',
54
+ role: MessageRole.User,
55
+ type: MessageType.QueryTyped,
56
+ createdAt: '2026-07-20T00:00:00.000Z',
57
+ metadata: {
58
+ content: 'Add Override Title to my cart',
59
+ },
60
+ });
61
+ });
62
+
63
+ it('falls back to event.attributes.productTitle when no override is passed', () => {
64
+ const event: UserEvent = {
65
+ eventId: 'event-1',
66
+ createdAt: '2026-07-20T00:00:00.000Z',
67
+ category: UserEventCategory.AddToCart,
68
+ attributes: {
69
+ productId: 'product-1',
70
+ parentProductId: '',
71
+ url: '',
72
+ productTitle: 'Attribute Title',
73
+ },
74
+ };
75
+
76
+ expect(messageFromAddToCartEvent(event)).toEqual({
77
+ id: 'event-1',
78
+ role: MessageRole.User,
79
+ type: MessageType.QueryTyped,
80
+ createdAt: '2026-07-20T00:00:00.000Z',
81
+ metadata: {
82
+ content: 'Add Attribute Title to my cart',
83
+ },
84
+ });
85
+ });
86
+
87
+ it('falls back to the raw productId when neither a title override nor an attribute title is present', () => {
88
+ const event: UserEvent = {
89
+ eventId: 'event-1',
90
+ createdAt: '2026-07-20T00:00:00.000Z',
91
+ category: UserEventCategory.AddToCart,
92
+ attributes: {
93
+ productId: 'product-1',
94
+ parentProductId: '',
95
+ url: '',
96
+ },
97
+ };
98
+
99
+ expect(messageFromAddToCartEvent(event)).toEqual({
100
+ id: 'event-1',
101
+ role: MessageRole.User,
102
+ type: MessageType.QueryTyped,
103
+ createdAt: '2026-07-20T00:00:00.000Z',
104
+ metadata: {
105
+ content: 'Add product-1 to my cart',
106
+ },
107
+ });
108
+ });
109
+ });
@@ -6,6 +6,7 @@ export * from './divideArray';
6
6
  export * from './domObserver';
7
7
  export * from './elementObserver';
8
8
  export * from './imageFilter';
9
+ export * from './messageFromAddToCartEvent';
9
10
  export * from './messageFromFormSubmittedEvent';
10
11
  export * from './messageFromQueryEvent';
11
12
  export * from './messageFromResponse';