@envive-ai/react-hooks 0.3.45 → 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 (293) 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/api/widgetText.d.cts +2 -1
  8. package/dist/application/models/api/widgetText.d.ts +2 -1
  9. package/dist/application/models/featureGates.cjs +2 -1
  10. package/dist/application/models/featureGates.d.cts +2 -1
  11. package/dist/application/models/featureGates.d.ts +2 -1
  12. package/dist/application/models/featureGates.js +2 -1
  13. package/dist/application/models/graphql/queries/getProductOptionsQuery.cjs +33 -0
  14. package/dist/application/models/graphql/queries/getProductOptionsQuery.d.cts +28 -0
  15. package/dist/application/models/graphql/queries/getProductOptionsQuery.d.ts +28 -0
  16. package/dist/application/models/graphql/queries/getProductOptionsQuery.js +32 -0
  17. package/dist/application/models/index.cjs +1 -1
  18. package/dist/application/models/index.js +1 -1
  19. package/dist/application/models/validators/validateGraphQLFrontendConfig.cjs +1 -1
  20. package/dist/application/models/validators/validateGraphQLFrontendConfig.js +1 -1
  21. package/dist/application/models/validators/validateResponse.cjs +2 -1
  22. package/dist/application/models/validators/validateResponse.js +2 -1
  23. package/dist/application/models/validators/validateUserEvent.cjs +3 -3
  24. package/dist/application/models/validators/validateUserEvent.js +3 -3
  25. package/dist/application/utils/elementObserver.d.cts +2 -2
  26. package/dist/application/utils/elementObserver.d.ts +2 -2
  27. package/dist/application/utils/index.cjs +2 -0
  28. package/dist/application/utils/index.d.cts +2 -1
  29. package/dist/application/utils/index.d.ts +2 -1
  30. package/dist/application/utils/index.js +2 -1
  31. package/dist/application/utils/messageFromAddToCartEvent.cjs +32 -0
  32. package/dist/application/utils/messageFromAddToCartEvent.d.cts +19 -0
  33. package/dist/application/utils/messageFromAddToCartEvent.d.ts +19 -0
  34. package/dist/application/utils/messageFromAddToCartEvent.js +31 -0
  35. package/dist/application/utils/widgetTextFromApiWidgetTextResponse.cjs +3 -2
  36. package/dist/application/utils/widgetTextFromApiWidgetTextResponse.js +3 -2
  37. package/dist/atoms/app/index.cjs +1 -1
  38. package/dist/atoms/app/index.d.cts +7 -7
  39. package/dist/atoms/app/index.d.ts +7 -7
  40. package/dist/atoms/app/index.js +1 -1
  41. package/dist/atoms/app/variant.cjs +2 -2
  42. package/dist/atoms/app/variant.d.cts +7 -7
  43. package/dist/atoms/app/variant.d.ts +7 -7
  44. package/dist/atoms/app/variant.js +2 -2
  45. package/dist/atoms/chat/chatState.d.cts +19 -19
  46. package/dist/atoms/chat/chatState.d.ts +19 -19
  47. package/dist/atoms/chat/form.d.cts +2 -2
  48. package/dist/atoms/chat/form.d.ts +2 -2
  49. package/dist/atoms/chat/index.cjs +1 -1
  50. package/dist/atoms/chat/index.d.cts +2 -2
  51. package/dist/atoms/chat/index.d.ts +2 -2
  52. package/dist/atoms/chat/index.js +1 -1
  53. package/dist/atoms/chat/lastMessage.d.cts +2 -2
  54. package/dist/atoms/chat/lastMessage.d.ts +2 -2
  55. package/dist/atoms/chat/messageQueue.d.cts +6 -6
  56. package/dist/atoms/chat/messageQueue.d.ts +6 -6
  57. package/dist/atoms/chat/performanceMetrics.d.cts +6 -6
  58. package/dist/atoms/chat/performanceMetrics.d.ts +6 -6
  59. package/dist/atoms/chat/renderedWidgetRefs.d.cts +2 -2
  60. package/dist/atoms/chat/renderedWidgetRefs.d.ts +2 -2
  61. package/dist/atoms/chat/replies.d.cts +3 -3
  62. package/dist/atoms/chat/replies.d.ts +2 -2
  63. package/dist/atoms/chat/suggestions.d.cts +2 -2
  64. package/dist/atoms/chat/suggestions.d.ts +2 -2
  65. package/dist/atoms/envive/enviveConfig.cjs +7 -4
  66. package/dist/atoms/envive/enviveConfig.d.cts +16 -14
  67. package/dist/atoms/envive/enviveConfig.d.ts +16 -14
  68. package/dist/atoms/envive/enviveConfig.js +6 -4
  69. package/dist/atoms/globalSearch/globalSearch.d.cts +5 -5
  70. package/dist/atoms/globalSearch/globalSearch.d.ts +5 -5
  71. package/dist/atoms/org/customerService.d.cts +6 -6
  72. package/dist/atoms/org/customerService.d.ts +6 -6
  73. package/dist/atoms/org/graphqlConfig.cjs +1 -1
  74. package/dist/atoms/org/graphqlConfig.d.cts +6 -6
  75. package/dist/atoms/org/graphqlConfig.d.ts +6 -6
  76. package/dist/atoms/org/graphqlConfig.js +1 -1
  77. package/dist/atoms/org/index.cjs +1 -1
  78. package/dist/atoms/org/index.js +1 -1
  79. package/dist/atoms/org/newOrgConfigAtom.d.cts +2 -2
  80. package/dist/atoms/org/newOrgConfigAtom.d.ts +2 -2
  81. package/dist/atoms/org/orgAnalyticsConfig.d.cts +4 -4
  82. package/dist/atoms/org/orgAnalyticsConfig.d.ts +4 -4
  83. package/dist/atoms/search/chatSearch.cjs +1 -1
  84. package/dist/atoms/search/chatSearch.d.cts +17 -17
  85. package/dist/atoms/search/chatSearch.d.ts +17 -17
  86. package/dist/atoms/search/chatSearch.js +1 -1
  87. package/dist/atoms/search/searchAPI.cjs +1 -1
  88. package/dist/atoms/search/searchAPI.d.cts +13 -13
  89. package/dist/atoms/search/searchAPI.d.ts +13 -13
  90. package/dist/atoms/search/searchAPI.js +1 -1
  91. package/dist/atoms/widget/chatPreviewLoading.d.cts +2 -2
  92. package/dist/atoms/widget/chatPreviewLoading.d.ts +2 -2
  93. package/dist/contexts/enviveConfigContext/enviveConfigContext.cjs +6 -4
  94. package/dist/contexts/enviveConfigContext/enviveConfigContext.d.cts +3 -1
  95. package/dist/contexts/enviveConfigContext/enviveConfigContext.d.ts +3 -1
  96. package/dist/contexts/enviveConfigContext/enviveConfigContext.js +6 -4
  97. package/dist/contexts/enviveContext/enviveContext.cjs +7 -3
  98. package/dist/contexts/enviveContext/enviveContext.d.cts +5 -1
  99. package/dist/contexts/enviveContext/enviveContext.d.ts +5 -1
  100. package/dist/contexts/enviveContext/enviveContext.js +7 -3
  101. package/dist/contexts/enviveContext/types.d.cts +1 -1
  102. package/dist/contexts/enviveContext/types.d.ts +1 -1
  103. package/dist/contexts/featureFlagServiceContext/featureFlagServiceContext.cjs +2 -97
  104. package/dist/contexts/featureFlagServiceContext/featureFlagServiceContext.d.cts +3 -25
  105. package/dist/contexts/featureFlagServiceContext/featureFlagServiceContext.d.ts +3 -25
  106. package/dist/contexts/featureFlagServiceContext/featureFlagServiceContext.js +3 -97
  107. package/dist/contexts/featureFlagServiceContext/index.cjs +2 -1
  108. package/dist/contexts/featureFlagServiceContext/index.d.cts +2 -1
  109. package/dist/contexts/featureFlagServiceContext/index.d.ts +2 -1
  110. package/dist/contexts/featureFlagServiceContext/index.js +2 -1
  111. package/dist/contexts/graphqlContext/mockV3Config.cjs +1 -1
  112. package/dist/contexts/graphqlContext/mockV3Config.js +1 -1
  113. package/dist/contexts/localStorageContext/index.cjs +2 -1
  114. package/dist/contexts/localStorageContext/index.d.cts +2 -1
  115. package/dist/contexts/localStorageContext/index.d.ts +2 -1
  116. package/dist/contexts/localStorageContext/index.js +2 -1
  117. package/dist/contexts/localStorageContext/localStorageContext.cjs +5 -11
  118. package/dist/contexts/localStorageContext/localStorageContext.d.cts +3 -7
  119. package/dist/contexts/localStorageContext/localStorageContext.d.ts +3 -7
  120. package/dist/contexts/localStorageContext/localStorageContext.js +3 -8
  121. package/dist/contexts/localStorageContext/localStorageKeys.cjs +12 -0
  122. package/dist/contexts/localStorageContext/localStorageKeys.d.cts +9 -0
  123. package/dist/contexts/localStorageContext/localStorageKeys.d.ts +9 -0
  124. package/dist/contexts/localStorageContext/localStorageKeys.js +11 -0
  125. package/dist/contexts/pageContext/pageContext.cjs +2 -2
  126. package/dist/contexts/pageContext/pageContext.js +2 -2
  127. package/dist/contexts/salesAgentContext/chatAPI.cjs +15 -4
  128. package/dist/contexts/salesAgentContext/chatAPI.d.cts +4 -1
  129. package/dist/contexts/salesAgentContext/chatAPI.d.ts +4 -1
  130. package/dist/contexts/salesAgentContext/chatAPI.js +16 -5
  131. package/dist/contexts/salesAgentContext/salesAgentContext.cjs +15 -4
  132. package/dist/contexts/salesAgentContext/salesAgentContext.d.cts +2 -1
  133. package/dist/contexts/salesAgentContext/salesAgentContext.d.ts +2 -1
  134. package/dist/contexts/salesAgentContext/salesAgentContext.js +16 -5
  135. package/dist/contexts/salesAgentContext/salesAgentService.cjs +12 -4
  136. package/dist/contexts/salesAgentContext/salesAgentService.js +12 -4
  137. package/dist/contexts/searchContext/searchContext.cjs +1 -1
  138. package/dist/contexts/searchContext/searchContext.js +1 -1
  139. package/dist/contexts/systemSettingsContext/systemSettingsContext.d.cts +2 -2
  140. package/dist/contexts/systemSettingsContext/systemSettingsContext.d.ts +2 -2
  141. package/dist/contexts/types.cjs +1 -1
  142. package/dist/contexts/types.d.cts +3 -1
  143. package/dist/contexts/types.d.ts +3 -1
  144. package/dist/contexts/types.js +1 -1
  145. package/dist/contexts/typesV3.cjs +1 -1
  146. package/dist/contexts/typesV3.d.cts +11 -2
  147. package/dist/contexts/typesV3.d.ts +12 -3
  148. package/dist/contexts/typesV3.js +1 -1
  149. package/dist/contexts/widgetConfigContext/widgetConfigContext.cjs +1 -1
  150. package/dist/contexts/widgetConfigContext/widgetConfigContext.js +1 -1
  151. package/dist/hooks/AddToCart/addToCartErrorMessage.cjs +7 -0
  152. package/dist/hooks/AddToCart/addToCartErrorMessage.d.cts +5 -0
  153. package/dist/hooks/AddToCart/addToCartErrorMessage.d.ts +5 -0
  154. package/dist/hooks/AddToCart/addToCartErrorMessage.js +6 -0
  155. package/dist/hooks/AddToCart/createAddToCartUserEvent.cjs +20 -0
  156. package/dist/hooks/AddToCart/createAddToCartUserEvent.d.cts +17 -0
  157. package/dist/hooks/AddToCart/createAddToCartUserEvent.d.ts +17 -0
  158. package/dist/hooks/AddToCart/createAddToCartUserEvent.js +19 -0
  159. package/dist/hooks/AddToCart/index.cjs +9 -0
  160. package/dist/hooks/AddToCart/index.d.cts +5 -0
  161. package/dist/hooks/AddToCart/index.d.ts +5 -0
  162. package/dist/hooks/AddToCart/index.js +6 -0
  163. package/dist/hooks/AddToCart/parseAddToCartFunction.cjs +27 -0
  164. package/dist/hooks/AddToCart/parseAddToCartFunction.d.cts +13 -0
  165. package/dist/hooks/AddToCart/parseAddToCartFunction.d.ts +13 -0
  166. package/dist/hooks/AddToCart/parseAddToCartFunction.js +27 -0
  167. package/dist/hooks/AddToCart/useAddToCart.cjs +24 -0
  168. package/dist/hooks/AddToCart/useAddToCart.d.cts +14 -0
  169. package/dist/hooks/AddToCart/useAddToCart.d.ts +14 -0
  170. package/dist/hooks/AddToCart/useAddToCart.js +23 -0
  171. package/dist/hooks/ChatToggle/useChatToggle.cjs +1 -1
  172. package/dist/hooks/ChatToggle/useChatToggle.js +1 -1
  173. package/dist/hooks/LocalStorageOperations/useLocalStorageOperations.cjs +6 -5
  174. package/dist/hooks/LocalStorageOperations/useLocalStorageOperations.js +3 -2
  175. package/dist/hooks/PageViewedEvent/usePageViewedEvent.cjs +6 -3
  176. package/dist/hooks/PageViewedEvent/usePageViewedEvent.js +6 -3
  177. package/dist/hooks/ProductDetails/index.cjs +9 -0
  178. package/dist/hooks/ProductDetails/index.d.cts +4 -0
  179. package/dist/hooks/ProductDetails/index.d.ts +4 -0
  180. package/dist/hooks/ProductDetails/index.js +4 -0
  181. package/dist/hooks/ProductDetails/useProductDetails.cjs +53 -0
  182. package/dist/hooks/ProductDetails/useProductDetails.d.cts +20 -0
  183. package/dist/hooks/ProductDetails/useProductDetails.d.ts +20 -0
  184. package/dist/hooks/ProductDetails/useProductDetails.js +52 -0
  185. package/dist/hooks/ProductDetails/variantSelection.cjs +42 -0
  186. package/dist/hooks/ProductDetails/variantSelection.d.cts +35 -0
  187. package/dist/hooks/ProductDetails/variantSelection.d.ts +35 -0
  188. package/dist/hooks/ProductDetails/variantSelection.js +38 -0
  189. package/dist/hooks/Search/useSearch.cjs +3 -3
  190. package/dist/hooks/Search/useSearch.d.cts +1 -1
  191. package/dist/hooks/Search/useSearch.d.ts +1 -1
  192. package/dist/hooks/Search/useSearch.js +3 -3
  193. package/dist/hooks/SystemSettingsContext/useSystemSettingsContext.d.cts +2 -2
  194. package/dist/hooks/TrackComponentVisibleEvent/useTrackComponentVisibleEvent.cjs +11 -2
  195. package/dist/hooks/TrackComponentVisibleEvent/useTrackComponentVisibleEvent.d.cts +1 -10
  196. package/dist/hooks/TrackComponentVisibleEvent/useTrackComponentVisibleEvent.d.ts +1 -10
  197. package/dist/hooks/TrackComponentVisibleEvent/useTrackComponentVisibleEvent.js +11 -2
  198. package/dist/hooks/WidgetInteraction/types.cjs +13 -3
  199. package/dist/hooks/WidgetInteraction/types.d.cts +29 -4
  200. package/dist/hooks/WidgetInteraction/types.d.ts +29 -4
  201. package/dist/hooks/WidgetInteraction/types.js +13 -3
  202. package/dist/hooks/WidgetInteraction/utils.cjs +2 -2
  203. package/dist/hooks/WidgetInteraction/utils.js +2 -2
  204. package/dist/hooks/utils.d.cts +1 -1
  205. package/dist/hooks/utils.d.ts +1 -1
  206. package/dist/services/amplitudeService/amplitudeService.cjs +18 -11
  207. package/dist/services/amplitudeService/amplitudeService.d.cts +5 -2
  208. package/dist/services/amplitudeService/amplitudeService.d.ts +5 -2
  209. package/dist/services/amplitudeService/amplitudeService.js +16 -8
  210. package/dist/services/enviveConfigService/enviveConfigService.cjs +36 -4
  211. package/dist/services/enviveConfigService/enviveConfigService.d.cts +4 -1
  212. package/dist/services/enviveConfigService/enviveConfigService.d.ts +4 -1
  213. package/dist/services/enviveConfigService/enviveConfigService.js +35 -3
  214. package/dist/services/enviveConfigService/fetchGraphQLConfig.cjs +9 -8
  215. package/dist/services/enviveConfigService/fetchGraphQLConfig.js +9 -7
  216. package/dist/services/featureFlagService/index.cjs +101 -0
  217. package/dist/services/featureFlagService/index.d.cts +28 -0
  218. package/dist/services/featureFlagService/index.d.ts +28 -0
  219. package/dist/services/featureFlagService/index.js +101 -0
  220. package/dist/services/ga4ProjectionService/ga4EventSchema.cjs +8 -2
  221. package/dist/services/ga4ProjectionService/ga4EventSchema.js +8 -2
  222. package/dist/services/hardcopyService/hardcopyService.cjs +4 -3
  223. package/dist/services/hardcopyService/hardcopyService.d.cts +1 -1
  224. package/dist/services/hardcopyService/hardcopyService.d.ts +1 -1
  225. package/dist/services/hardcopyService/hardcopyService.js +4 -3
  226. package/dist/services/urlResolverWarmupService/index.cjs +54 -0
  227. package/dist/services/urlResolverWarmupService/index.d.cts +40 -0
  228. package/dist/services/urlResolverWarmupService/index.d.ts +40 -0
  229. package/dist/services/urlResolverWarmupService/index.js +54 -0
  230. package/dist/types/enviveConfig.d.cts +6 -2
  231. package/dist/types/enviveConfig.d.ts +6 -2
  232. package/dist/types/index.d.cts +2 -2
  233. package/dist/types/index.d.ts +2 -2
  234. package/package.json +18 -2
  235. package/src/application/__tests__/commerceApiAddToCartRehydration.test.ts +126 -0
  236. package/src/application/commerce-api.ts +142 -23
  237. package/src/application/models/api/userEvent.ts +8 -1
  238. package/src/application/models/api/widgetText.ts +3 -0
  239. package/src/application/models/featureGates.ts +5 -0
  240. package/src/application/models/graphql/queries/getProductOptionsQuery.ts +59 -0
  241. package/src/application/models/validators/__tests__/validateUserEvent.test.ts +41 -0
  242. package/src/application/models/validators/validateResponse.ts +1 -0
  243. package/src/application/models/validators/validateUserEvent.ts +7 -2
  244. package/src/application/utils/__tests__/messageFromAddToCartEvent.test.ts +109 -0
  245. package/src/application/utils/index.ts +1 -0
  246. package/src/application/utils/messageFromAddToCartEvent.ts +32 -0
  247. package/src/application/utils/widgetTextFromApiWidgetTextResponse.ts +1 -0
  248. package/src/atoms/app/index.ts +1 -1
  249. package/src/atoms/app/variant.ts +1 -1
  250. package/src/atoms/envive/enviveConfig.ts +6 -1
  251. package/src/atoms/org/graphqlConfig.ts +1 -1
  252. package/src/contexts/enviveConfigContext/enviveConfigContext.tsx +5 -0
  253. package/src/contexts/enviveContext/enviveContext.tsx +10 -0
  254. package/src/contexts/featureFlagServiceContext/featureFlagServiceContext.tsx +6 -173
  255. package/src/contexts/localStorageContext/localStorageContext.tsx +2 -5
  256. package/src/contexts/localStorageContext/localStorageKeys.ts +8 -0
  257. package/src/contexts/pageContext/pageContext.tsx +1 -1
  258. package/src/contexts/salesAgentContext/__tests__/chatAPI.test.tsx +63 -0
  259. package/src/contexts/salesAgentContext/chatAPI.ts +20 -1
  260. package/src/contexts/salesAgentContext/salesAgentContext.tsx +17 -0
  261. package/src/contexts/salesAgentContext/salesAgentService.ts +12 -3
  262. package/src/contexts/types.ts +2 -0
  263. package/src/contexts/typesV3.ts +14 -0
  264. package/src/hooks/AddToCart/__tests__/parseAddToCartFunction.test.ts +31 -0
  265. package/src/hooks/AddToCart/__tests__/useAddToCart.test.tsx +204 -0
  266. package/src/hooks/AddToCart/addToCartErrorMessage.ts +2 -0
  267. package/src/hooks/AddToCart/createAddToCartUserEvent.ts +25 -0
  268. package/src/hooks/AddToCart/index.ts +4 -0
  269. package/src/hooks/AddToCart/parseAddToCartFunction.ts +31 -0
  270. package/src/hooks/AddToCart/useAddToCart.ts +35 -0
  271. package/src/hooks/PageViewedEvent/__tests__/usePageViewedEvent.test.ts +4 -4
  272. package/src/hooks/PageViewedEvent/usePageViewedEvent.ts +5 -1
  273. package/src/hooks/ProductDetails/__tests__/useProductDetails.test.tsx +129 -0
  274. package/src/hooks/ProductDetails/__tests__/variantSelection.test.ts +145 -0
  275. package/src/hooks/ProductDetails/index.ts +2 -0
  276. package/src/hooks/ProductDetails/useProductDetails.ts +62 -0
  277. package/src/hooks/ProductDetails/variantSelection.ts +61 -0
  278. package/src/hooks/TrackComponentVisibleEvent/useTrackComponentVisibleEvent.ts +15 -0
  279. package/src/hooks/WidgetInteraction/types.ts +43 -2
  280. package/src/hooks/WidgetInteraction/utils.ts +1 -1
  281. package/src/services/amplitudeService/__tests__/amplitudeService.test.ts +8 -8
  282. package/src/services/amplitudeService/amplitudeService.ts +43 -23
  283. package/src/services/enviveConfigService/__tests__/enviveConfigService.test.ts +208 -0
  284. package/src/services/enviveConfigService/enviveConfigService.ts +62 -2
  285. package/src/services/enviveConfigService/fetchGraphQLConfig.ts +17 -6
  286. package/src/services/featureFlagService/index.ts +176 -0
  287. package/src/services/ga4ProjectionService/__tests__/ga4ProjectionService.test.ts +36 -0
  288. package/src/services/ga4ProjectionService/ga4EventSchema.ts +7 -0
  289. package/src/services/hardcopyService/__tests__/hardcopyService.test.ts +47 -1
  290. package/src/services/hardcopyService/hardcopyService.ts +5 -0
  291. package/src/services/urlResolverWarmupService/__tests__/urlResolverWarmupService.test.ts +85 -0
  292. package/src/services/urlResolverWarmupService/index.ts +89 -0
  293. package/src/types/enviveConfig.ts +3 -0
@@ -1,9 +1,21 @@
1
1
  import { OrgConfigExperimentAssignment, OrgConfigFeatureGate } from 'src/application/models';
2
2
  import { FeatureGates } from 'src/application/models/featureGates';
3
- import { FeatureFlagService } from 'src/contexts/featureFlagServiceContext';
4
- import { GraphQlConfigValues } from 'src/contexts/graphqlContext';
3
+ import { FeatureFlagService } from 'src/services/featureFlagService';
4
+ import type { GraphQlConfigValues } from 'src/contexts/graphqlContext';
5
+ import { configVersionOverride } from 'src/types/config-versions';
5
6
  import { fetchGraphQLConfig, fetchUnifiedGraphQLConfig } from './fetchGraphQLConfig';
6
7
 
8
+ // Short-lived so config changes (gates, experiments) still propagate within
9
+ // a session, while sparing every page navigation from re-fetching org
10
+ // config + GraphQL config, both of which sit on the critical path before
11
+ // first paint.
12
+ const CONFIG_CACHE_TTL_MS = 5 * 60 * 1000;
13
+
14
+ type CachedConfigEntry = {
15
+ value: EnviveServiceConfig;
16
+ expiresAt: number;
17
+ };
18
+
7
19
  type EnviveConfigServiceProps = {
8
20
  baseUrl: string;
9
21
  apiKey: string;
@@ -63,10 +75,57 @@ export class EnviveConfigService implements IEnviveConfigService {
63
75
  this.source = source;
64
76
  }
65
77
 
78
+ private getCacheKey(): string {
79
+ return `envive-config-cache:${this.baseUrl}:${this.apiKey}:${this.userId}:${this.namespace}:${this.source}`;
80
+ }
81
+
82
+ // `envive_config_version` / `spiffy_config_version` pin a specific config
83
+ // version for debugging/rollback. Any use of that override must always
84
+ // bypass the cache below — both reading a stale entry and writing one
85
+ // under the shared key would defeat the override's purpose.
86
+ private readCachedConfig(): EnviveServiceConfig | null {
87
+ if (configVersionOverride()) {
88
+ return null;
89
+ }
90
+ try {
91
+ const raw = sessionStorage.getItem(this.getCacheKey());
92
+ if (!raw) {
93
+ return null;
94
+ }
95
+ const entry = JSON.parse(raw) as CachedConfigEntry;
96
+ if (Date.now() >= entry.expiresAt) {
97
+ return null;
98
+ }
99
+ return entry.value;
100
+ } catch {
101
+ return null;
102
+ }
103
+ }
104
+
105
+ private writeCachedConfig(value: EnviveServiceConfig): void {
106
+ if (configVersionOverride()) {
107
+ return;
108
+ }
109
+ try {
110
+ const entry: CachedConfigEntry = { value, expiresAt: Date.now() + CONFIG_CACHE_TTL_MS };
111
+ sessionStorage.setItem(this.getCacheKey(), JSON.stringify(entry));
112
+ } catch {
113
+ // Storage disabled, full, or unavailable (private browsing, etc.) -
114
+ // fail open and let the next call re-fetch instead of caching.
115
+ }
116
+ }
117
+
66
118
  async getEnviveConfig(): Promise<EnviveServiceConfig> {
67
119
  if (this.response !== null) {
68
120
  return this.response;
69
121
  }
122
+
123
+ const cached = this.readCachedConfig();
124
+ if (cached !== null) {
125
+ this.response = cached;
126
+ return this.response;
127
+ }
128
+
70
129
  const queryParams = new URLSearchParams({
71
130
  namespace: this.namespace,
72
131
  source: this.source,
@@ -106,6 +165,7 @@ export class EnviveConfigService implements IEnviveConfigService {
106
165
  : await fetchGraphQLConfig(this.baseUrl, this.apiKey);
107
166
 
108
167
  this.response = { ...orgConfigResponse, orgConfig };
168
+ this.writeCachedConfig(this.response);
109
169
  return this.response;
110
170
  }
111
171
  }
@@ -7,13 +7,16 @@ import {
7
7
  } from 'src/application/models';
8
8
  import { FrontendConfigV3 } from 'src/application/models/frontendConfigV3';
9
9
  import { ColorMappingV3 } from 'src/application/models/colorsConfigV3';
10
- import { FloatingButtonLocation } from '@envive-ai/react-toolkit-v3/FloatingButton';
10
+ // Type-only: a value import would pull the FloatingButton component (and
11
+ // React) through the package's component index into the injection bundle's
12
+ // pre-config chunk.
13
+ import type { FloatingButtonLocation } from '@envive-ai/react-toolkit-v3/FloatingButton';
11
14
  import { getColorsAndFrontendQuery } from 'src/application/models/graphql/queries/getColorsAndFrontendQuery';
12
15
  import { getWidgetConfigQuery } from 'src/application/models/graphql/queries/getWidgetConfigQuery';
13
16
  import { mockV3ColorsConfig, mockV3FrontendConfig } from 'src/contexts/graphqlContext/mockV3Config';
14
17
  import { WidgetConfigV3 } from 'src/contexts/typesV3';
15
18
  import { PageVariantConfig, PageVariantTestType, WidgetMountingConfig } from 'src/contexts/types';
16
- import { GraphQlConfigValues } from 'src/contexts/graphqlContext';
19
+ import type { GraphQlConfigValues } from 'src/contexts/graphqlContext';
17
20
  import { configVersionOverride } from 'src/types/config-versions';
18
21
  import { ExperimentConfigResolutionMetadata } from 'src/application/models/api/orgConfigResults';
19
22
 
@@ -39,11 +42,19 @@ export const DEFAULT_PAGE_VARIANTS: PageVariantConfig[] = [
39
42
  },
40
43
  ];
41
44
 
45
+ // String-literal values of the FloatingButtonLocation enum; keep in sync
46
+ // with @envive-ai/react-toolkit-v3/FloatingButton/types/types.ts.
47
+ const FLOATING_BUTTON_POSITIONS = [
48
+ 'bottom-left',
49
+ 'middle-left',
50
+ 'middle-right',
51
+ 'bottom-right',
52
+ ] as const;
53
+
42
54
  const mapPositionToCamelCase = (position: string): FloatingButtonLocation => {
43
- if (position === 'bottom-left') return FloatingButtonLocation.BOTTOM_LEFT;
44
- if (position === 'middle-left') return FloatingButtonLocation.MIDDLE_LEFT;
45
- if (position === 'middle-right') return FloatingButtonLocation.MIDDLE_RIGHT;
46
- if (position === 'bottom-right') return FloatingButtonLocation.BOTTOM_RIGHT;
55
+ if ((FLOATING_BUTTON_POSITIONS as readonly string[]).includes(position)) {
56
+ return position as FloatingButtonLocation;
57
+ }
47
58
  throw new Error(`Invalid position: ${position}`);
48
59
  };
49
60
 
@@ -0,0 +1,176 @@
1
+ import { OrgConfigFeatureGate } from 'src/application/models/api/orgConfigResults';
2
+ import { FeatureGates } from 'src/application/models/featureGates';
3
+ import Logger from 'src/application/logging/logger';
4
+
5
+ const FEATURE_FLAGS_STORAGE_KEY = 'spiffy-feature-flags';
6
+
7
+ const logger = new Logger('featureFlagService');
8
+
9
+ export interface FullFlagValue {
10
+ resolved: boolean;
11
+ initial: boolean | undefined;
12
+ query_param_override: boolean | undefined;
13
+ window_param_override: boolean | undefined;
14
+ stored_override: boolean | undefined;
15
+ }
16
+
17
+ // Kept free of React imports: the injection bundle constructs this service
18
+ // before deciding whether to load the React app, and pulling React into
19
+ // that pre-config code path would put it on the eager critical chunk.
20
+ export class FeatureFlagService {
21
+ private featureGates: OrgConfigFeatureGate[];
22
+
23
+ constructor(featureGates: OrgConfigFeatureGate[]) {
24
+ this.featureGates = featureGates;
25
+ }
26
+
27
+ static getQueryParamFeatureGateOverrides(featureGate: FeatureGates): boolean | undefined {
28
+ const url = window.location?.href ?? '';
29
+ if (url.includes('?')) {
30
+ const urlObj = new URL(url);
31
+ const params = new URLSearchParams(urlObj.search);
32
+ const paramsObj = Object.fromEntries(params.entries());
33
+ const value = paramsObj[featureGate];
34
+
35
+ if (value != null) {
36
+ return value === 'true';
37
+ }
38
+ }
39
+
40
+ return undefined;
41
+ }
42
+
43
+ static getWindowFeatureGateOverrides(featureGate: FeatureGates): boolean | undefined {
44
+ if (typeof window === 'undefined') {
45
+ return undefined;
46
+ }
47
+ const windowOverride = window as Window & {
48
+ _envive?: { featureOverrides?: Record<string, boolean> };
49
+ _spiffy?: { featureOverrides?: Record<string, boolean> };
50
+ };
51
+ const featureOverrides =
52
+ windowOverride?._envive?.featureOverrides ?? windowOverride?._spiffy?.featureOverrides;
53
+ const value = featureOverrides?.[String(featureGate)];
54
+ if (value != null) {
55
+ return value;
56
+ }
57
+ return undefined;
58
+ }
59
+
60
+ static getStoredFeatureGateOverrides(featureGate: FeatureGates): boolean | undefined {
61
+ if (typeof window === 'undefined' || typeof window.localStorage === 'undefined') {
62
+ return undefined;
63
+ }
64
+ try {
65
+ const featureFlags = window.localStorage.getItem(FEATURE_FLAGS_STORAGE_KEY);
66
+ if (!featureFlags) {
67
+ return undefined;
68
+ }
69
+ const featureFlagsObj = JSON.parse(featureFlags) as Record<string, boolean>;
70
+ return featureFlagsObj[featureGate];
71
+ } catch (err) {
72
+ logger.logError('getStoredFeatureGateOverrides: error parsing feature flags', err);
73
+ return undefined;
74
+ }
75
+ }
76
+
77
+ static getFeatureFlagOverride(featureGate: FeatureGates): boolean | undefined {
78
+ const queryOverride = FeatureFlagService.getQueryParamFeatureGateOverrides(featureGate);
79
+ if (queryOverride != null) {
80
+ logger.logDebug(
81
+ `isFeatureGateEnabled featureGate:${featureGate} overridden by URL param. New value: ${queryOverride}`,
82
+ );
83
+ return queryOverride;
84
+ }
85
+
86
+ const windowOverride = FeatureFlagService.getWindowFeatureGateOverrides(featureGate);
87
+ if (windowOverride != null) {
88
+ logger.logDebug(
89
+ `isFeatureGateEnabled featureGate:${featureGate} overridden by window param. New value: ${windowOverride}`,
90
+ );
91
+ return windowOverride;
92
+ }
93
+
94
+ const storedOverride = FeatureFlagService.getStoredFeatureGateOverrides(featureGate);
95
+ if (storedOverride != null) {
96
+ logger.logDebug(
97
+ `isFeatureGateEnabled featureGate:${featureGate} overridden by stored value. New value: ${storedOverride}`,
98
+ );
99
+ return storedOverride;
100
+ }
101
+
102
+ return undefined;
103
+ }
104
+
105
+ isFeatureGateEnabled = (featureGate: FeatureGates): boolean => {
106
+ const gateValue = this.featureGates.find(gate => gate.name === featureGate);
107
+
108
+ const featureFlagOverride = FeatureFlagService.getFeatureFlagOverride(featureGate);
109
+ if (featureFlagOverride !== undefined) {
110
+ return featureFlagOverride;
111
+ }
112
+
113
+ if (gateValue == null || gateValue.value == null) {
114
+ logger.logDebug(
115
+ `isFeatureGateEnabled featureGate:${featureGate} value is undefined - returning false`,
116
+ );
117
+ return false;
118
+ }
119
+ return gateValue.value;
120
+ };
121
+
122
+ // Ensure that standard overrides work as expected
123
+ isClientSessionEnabled = (): boolean =>
124
+ this.isFeatureGateEnabled(FeatureGates.IsClientSessionEnabled);
125
+
126
+ getFeatureFlags = (): Record<string, boolean> => {
127
+ return Object.fromEntries(
128
+ Object.values(FeatureGates).map((featureGate: FeatureGates) => [
129
+ featureGate,
130
+ this.isFeatureGateEnabled(featureGate),
131
+ ]),
132
+ );
133
+ };
134
+
135
+ getFullFlagValue = (featureGate: FeatureGates): FullFlagValue => {
136
+ const gateValue = this.featureGates.find(gate => gate.name === featureGate);
137
+ return {
138
+ resolved: this.isFeatureGateEnabled(featureGate),
139
+ initial: gateValue?.value,
140
+ query_param_override: FeatureFlagService.getQueryParamFeatureGateOverrides(featureGate),
141
+ window_param_override: FeatureFlagService.getWindowFeatureGateOverrides(featureGate),
142
+ stored_override: FeatureFlagService.getStoredFeatureGateOverrides(featureGate),
143
+ };
144
+ };
145
+
146
+ getFullFlagValues = (): Record<string, FullFlagValue> => {
147
+ const featureGates = Object.values(FeatureGates).map((featureGate: FeatureGates) => [
148
+ featureGate,
149
+ this.getFullFlagValue(featureGate),
150
+ ]);
151
+
152
+ return featureGates.length > 0
153
+ ? featureGates.reduce<Record<string, FullFlagValue>>((acc, entry) => {
154
+ const [name, value] = entry as [FeatureGates, FullFlagValue];
155
+ if (name && value != null) {
156
+ return { ...acc, [`feature_gate.${name}`]: value };
157
+ }
158
+ return acc;
159
+ }, {})
160
+ : {};
161
+ };
162
+
163
+ static persistFeatureGateOverrides(): void {
164
+ if (typeof window === 'undefined' || typeof window.localStorage === 'undefined') {
165
+ return;
166
+ }
167
+ const overrides = Object.values(FeatureGates)
168
+ .map(
169
+ featureGate =>
170
+ [featureGate, FeatureFlagService.getFeatureFlagOverride(featureGate)] as const,
171
+ )
172
+ .filter(([, value]) => value !== undefined)
173
+ .reduce<Record<string, boolean>>((acc, [key, value]) => ({ ...acc, [key]: value! }), {});
174
+ window.localStorage.setItem(FEATURE_FLAGS_STORAGE_KEY, JSON.stringify(overrides));
175
+ }
176
+ }
@@ -229,6 +229,42 @@ describe('projectToGA4', () => {
229
229
  expect(window.dataLayer[0]).toHaveProperty('interaction_suggestion_id', 'sug-456');
230
230
  });
231
231
 
232
+ it('should extract product_id and option_name but not option_value for variant_option_selected', () => {
233
+ projectToGA4(EnviveMetricsEventName.WidgetInteraction, {
234
+ 'context.page_type': 'pdp',
235
+ 'context.page_id': 'product-123',
236
+ 'trigger.widget': 'floating_chat',
237
+ 'trigger.widget_interaction': 'variant_option_selected',
238
+ 'trigger.widget_interaction_data': {
239
+ variant_option_selected: {
240
+ product_id: 'sku-789',
241
+ option_name: 'Color',
242
+ option_value: 'Grey',
243
+ },
244
+ },
245
+ });
246
+
247
+ const pushed = window.dataLayer[0];
248
+ expect(pushed).toHaveProperty('interaction_product_id', 'sku-789');
249
+ expect(pushed).toHaveProperty('interaction_option_name', 'Color');
250
+ expect(pushed).not.toHaveProperty('interaction_option_value');
251
+ expect(pushed).not.toHaveProperty('option_value');
252
+ });
253
+
254
+ it('should extract interaction_product_id for variant selector open/dismiss', () => {
255
+ projectToGA4(EnviveMetricsEventName.WidgetInteraction, {
256
+ 'context.page_type': 'pdp',
257
+ 'context.page_id': 'product-123',
258
+ 'trigger.widget': 'floating_chat',
259
+ 'trigger.widget_interaction': 'variant_selector_dismissed',
260
+ 'trigger.widget_interaction_data': {
261
+ variant_selector_dismissed: { product_id: 'sku-789' },
262
+ },
263
+ });
264
+
265
+ expect(window.dataLayer[0]).toHaveProperty('interaction_product_id', 'sku-789');
266
+ });
267
+
232
268
  it('should drop widget_interaction_data for unwhitelisted interactions', () => {
233
269
  projectToGA4(EnviveMetricsEventName.WidgetInteraction, {
234
270
  'context.page_type': 'pdp',
@@ -52,6 +52,13 @@ const WIDGET_INTERACTION_DATA_PROJECTIONS: WidgetInteractionDataProjection = {
52
52
  widget_collapsed: { interaction_collapse_source: 'widget_collapsed.collapse_source' },
53
53
  product_card_clicked: { interaction_product_id: 'product_card_clicked.product_id' },
54
54
  suggestion_scrolled: { interaction_suggestion_id: 'suggestion_scrolled.suggestion_id' },
55
+ variant_selector_opened: { interaction_product_id: 'variant_selector_opened.product_id' },
56
+ // option_value is intentionally excluded — merchant-defined values are high cardinality
57
+ variant_option_selected: {
58
+ interaction_product_id: 'variant_option_selected.product_id',
59
+ interaction_option_name: 'variant_option_selected.option_name',
60
+ },
61
+ variant_selector_dismissed: { interaction_product_id: 'variant_selector_dismissed.product_id' },
55
62
  };
56
63
 
57
64
  export const GA4_EVENT_SCHEMA: Record<EnviveMetricsEventName, GA4EventSchemaEntry> = {
@@ -1,7 +1,7 @@
1
1
  import { WidgetTypeV3 } from 'src/contexts/typesV3';
2
2
  import type { WidgetText } from 'src/application/models/api/widgetText';
3
3
  import type { AppDetails } from 'src/atoms/app';
4
- import { EnviveMetricsEventName } from 'src/services/amplitudeService';
4
+ import { EnviveMetricsEventName, TrackEventParams } from 'src/services/amplitudeService';
5
5
  import { getAtomStore } from 'src/atoms/atomStore/atomStore';
6
6
  import { resolvedBaseConfigVersionAtom } from 'src/atoms/envive/resolvedBaseConfigVersion';
7
7
  import { HardcopyService, MOCK_HARDCOPY_RESPONSE, MockHardcopyService } from '../hardcopyService';
@@ -269,6 +269,52 @@ describe('HardcopyService', () => {
269
269
  );
270
270
  });
271
271
 
272
+ it('forwards ai_suggestions_attribution_by_slot on WidgetTextResponse when present', async () => {
273
+ const trackEvent = vi.fn();
274
+ const attribution = {
275
+ prompt_button_texts: {
276
+ served_method_id: 'variant_b',
277
+ served_method_type: 'bandit_pbm',
278
+ },
279
+ };
280
+ mockGetHardcopy.mockResolvedValueOnce(
281
+ makeWidgetTextResponse({ aiSuggestionsAttributionBySlot: attribution }),
282
+ );
283
+
284
+ await makeService(trackEvent).getHardcopy({
285
+ widgetType: WidgetTypeV3.ChatPreviewV3,
286
+ userEvent: {} as any,
287
+ });
288
+
289
+ expect(trackEvent).toHaveBeenCalledWith(
290
+ expect.objectContaining({
291
+ eventName: EnviveMetricsEventName.WidgetTextResponse,
292
+ eventProps: expect.objectContaining({
293
+ ai_suggestions_attribution_by_slot: attribution,
294
+ }),
295
+ }),
296
+ );
297
+ });
298
+
299
+ it('omits ai_suggestions_attribution_by_slot when the response carries none', async () => {
300
+ const trackEvent = vi.fn();
301
+ mockGetHardcopy.mockResolvedValueOnce(makeWidgetTextResponse());
302
+
303
+ await makeService(trackEvent).getHardcopy({
304
+ widgetType: WidgetTypeV3.ChatPreviewV3,
305
+ userEvent: {} as any,
306
+ });
307
+
308
+ const responseEvent = trackEvent.mock.calls
309
+ .map(([params]: [TrackEventParams]) => params)
310
+ .find(
311
+ (params: TrackEventParams) =>
312
+ params.eventName === EnviveMetricsEventName.WidgetTextResponse,
313
+ );
314
+ expect(responseEvent).toBeDefined();
315
+ expect(responseEvent!.eventProps).not.toHaveProperty('ai_suggestions_attribution_by_slot');
316
+ });
317
+
272
318
  it('does not crash when trackEvent is not provided', async () => {
273
319
  const service = new HardcopyService({
274
320
  getAppDetails: () => mockAppDetails,
@@ -237,6 +237,11 @@ export class HardcopyService implements IHardcopyService {
237
237
  response_id: response.responseId,
238
238
  widget_text: response.values,
239
239
  request_id: widgetTextRequest.requestId,
240
+ // Which method served each overridden slot — training/attribution
241
+ // reads this from the analytics event; passed through verbatim.
242
+ ...(response.aiSuggestionsAttributionBySlot
243
+ ? { ai_suggestions_attribution_by_slot: response.aiSuggestionsAttributionBySlot }
244
+ : {}),
240
245
  },
241
246
  });
242
247
  return convertedResponse;
@@ -0,0 +1,85 @@
1
+ import { createStore } from 'jotai';
2
+ import { getAtomStore, setAtomStoreForTesting } from 'src/atoms/atomStore/atomStore';
3
+ import { contextSourceAtom, envAtom, enviveConfigAtom } from 'src/atoms/envive/enviveConfig';
4
+ import { featureFlagServiceAtom } from 'src/atoms/org/graphqlConfig';
5
+ import { FeatureFlagService } from 'src/services/featureFlagService';
6
+ import type { EnviveServiceConfig } from 'src/services/enviveConfigService';
7
+ import { warmUrlResolver } from '../index';
8
+
9
+ const { mockResolveUrl } = vi.hoisted(() => ({ mockResolveUrl: vi.fn() }));
10
+
11
+ vi.mock('src/application/commerce-api', () => ({
12
+ default: { resolveUrl: mockResolveUrl },
13
+ }));
14
+
15
+ const enviveServiceConfig = {
16
+ org: { org: { short_name: 'test-org', id: 'org-1' } },
17
+ } as EnviveServiceConfig;
18
+
19
+ const baseParams = {
20
+ baseUrl: 'https://commerce-api.spiffy.ai',
21
+ apiKey: 'test-key',
22
+ enviveServiceConfig,
23
+ show: true,
24
+ enviveOn: false,
25
+ };
26
+
27
+ describe('warmUrlResolver', () => {
28
+ beforeEach(() => {
29
+ setAtomStoreForTesting(createStore());
30
+ window.sessionStorage.clear();
31
+ vi.clearAllMocks();
32
+ mockResolveUrl.mockResolvedValue({ ready: true });
33
+ });
34
+
35
+ it('seeds env, contextSource, and the feature flag service before resolving', async () => {
36
+ const featureFlagService = new FeatureFlagService([]);
37
+
38
+ let envAtCallTime: string | undefined;
39
+ let contextSourceAtCallTime: string | undefined;
40
+ let flagServiceAtCallTime: FeatureFlagService | undefined;
41
+ mockResolveUrl.mockImplementation(async () => {
42
+ const store = getAtomStore();
43
+ envAtCallTime = store.get(envAtom);
44
+ contextSourceAtCallTime = store.get(contextSourceAtom);
45
+ flagServiceAtCallTime = store.get(featureFlagServiceAtom)?.featureFlagService;
46
+ return { ready: true };
47
+ });
48
+
49
+ await warmUrlResolver({
50
+ ...baseParams,
51
+ env: 'prod',
52
+ contextSource: 'app',
53
+ featureFlagService,
54
+ });
55
+
56
+ expect(mockResolveUrl).toHaveBeenCalledTimes(1);
57
+ expect(envAtCallTime).toBe('prod');
58
+ expect(contextSourceAtCallTime).toBe('app');
59
+ expect(flagServiceAtCallTime).toBe(featureFlagService);
60
+ });
61
+
62
+ it('does not overwrite atoms a provider has already seeded', async () => {
63
+ const store = getAtomStore();
64
+ const existingFlagService = new FeatureFlagService([]);
65
+ store.set(enviveConfigAtom, { identifyingPrefix: 'test', env: 'dev' });
66
+ store.set(featureFlagServiceAtom, { featureFlagService: existingFlagService });
67
+
68
+ await warmUrlResolver({
69
+ ...baseParams,
70
+ env: 'prod',
71
+ contextSource: 'playground',
72
+ featureFlagService: new FeatureFlagService([]),
73
+ });
74
+
75
+ expect(store.get(envAtom)).toBe('dev');
76
+ expect(store.get(featureFlagServiceAtom)?.featureFlagService).toBe(existingFlagService);
77
+ });
78
+
79
+ it('leaves the feature flag service atom unset when none is provided', async () => {
80
+ await warmUrlResolver(baseParams);
81
+
82
+ expect(getAtomStore().get(featureFlagServiceAtom)).toBeUndefined();
83
+ expect(mockResolveUrl).toHaveBeenCalledTimes(1);
84
+ });
85
+ });
@@ -0,0 +1,89 @@
1
+ import CommerceApiClient from 'src/application/commerce-api';
2
+ import Logger from 'src/application/logging/logger';
3
+ import { UrlResolverResponse, urlResolverAtom } from 'src/atoms/app/variant';
4
+ import { getAtomStore } from 'src/atoms/atomStore/atomStore';
5
+ import { enviveConfigAtom } from 'src/atoms/envive/enviveConfig';
6
+ import { resolvedBaseConfigVersionAtom } from 'src/atoms/envive/resolvedBaseConfigVersion';
7
+ import { featureFlagServiceAtom } from 'src/atoms/org/graphqlConfig';
8
+ import { EnviveServiceConfig } from 'src/services/enviveConfigService';
9
+ import type { FeatureFlagService } from 'src/services/featureFlagService';
10
+
11
+ const logger = new Logger('urlResolverWarmupService');
12
+
13
+ export interface WarmUrlResolverParams {
14
+ baseUrl: string;
15
+ apiKey: string;
16
+ enviveServiceConfig: EnviveServiceConfig;
17
+ show?: boolean;
18
+ enviveOn?: boolean;
19
+ env?: string;
20
+ contextSource?: string;
21
+ featureFlagService?: FeatureFlagService;
22
+ }
23
+
24
+ /**
25
+ * Fires the /v1/url_resolving request as soon as the org config is available,
26
+ * instead of waiting for the React tree to mount and PageProvider's effect to
27
+ * run — that removes a full network round trip from the first-widget-paint
28
+ * critical path.
29
+ *
30
+ * Seeds the same atoms EnviveProvider/EnviveConfigProvider populate later
31
+ * (with identical values) so the request context matches the one
32
+ * PageProvider would send, then stores the response in the
33
+ * sessionStorage-backed urlResolverAtom that PageProvider consults before
34
+ * fetching. Concurrent calls for the same URL are deduped inside
35
+ * CommerceApiClient.resolveUrl.
36
+ */
37
+ export const warmUrlResolver = async ({
38
+ baseUrl,
39
+ apiKey,
40
+ enviveServiceConfig,
41
+ show,
42
+ enviveOn,
43
+ env,
44
+ contextSource,
45
+ featureFlagService,
46
+ }: WarmUrlResolverParams): Promise<void> => {
47
+ try {
48
+ const store = getAtomStore();
49
+ // Must match PageProvider's URL cleansing so the cache key lines up.
50
+ const cleansedUrl = window.location.href.toLowerCase().trim();
51
+ if (store.get(urlResolverAtom)[cleansedUrl]) {
52
+ return;
53
+ }
54
+
55
+ // The CommerceApiClient singleton captures baseUrl/apiKey at first
56
+ // instantiation, so these atoms must be seeded before any API call.
57
+ // Only seed when unset — providers own this atom once React mounts.
58
+ if (store.get(enviveConfigAtom) === undefined) {
59
+ store.set(enviveConfigAtom, {
60
+ baseUrl,
61
+ orgLevelApiKey: apiKey,
62
+ orgShortName: enviveServiceConfig.org.org.short_name,
63
+ orgId: enviveServiceConfig.org.org.id,
64
+ show,
65
+ enviveOn,
66
+ env,
67
+ contextSource,
68
+ });
69
+ }
70
+ // resolveUrl reads feature gates through featureFlagServiceAtom; without
71
+ // this seed the warm-up request is computed with feature_gates={} and
72
+ // that response is what gets cached for PageProvider.
73
+ if (featureFlagService && store.get(featureFlagServiceAtom) === undefined) {
74
+ store.set(featureFlagServiceAtom, { featureFlagService });
75
+ }
76
+ // Mirrors EnviveProvider: "deployed" is the no-pin sentinel.
77
+ const { baseVersion, baseVersionSource } = enviveServiceConfig.orgConfig?.resolution ?? {};
78
+ if (baseVersion && baseVersionSource && baseVersionSource !== 'deployed') {
79
+ store.set(resolvedBaseConfigVersionAtom, baseVersion);
80
+ }
81
+
82
+ const response = (await CommerceApiClient.resolveUrl(cleansedUrl)) as UrlResolverResponse;
83
+ store.set(urlResolverAtom, { url: cleansedUrl, response });
84
+ } catch (err) {
85
+ // Fail open: PageProvider issues its own resolveUrl call when the cache
86
+ // is empty, so a warm-up failure never blocks rendering.
87
+ logger.logDebug('URL resolver warm-up failed', err);
88
+ }
89
+ };
@@ -2,8 +2,11 @@ import { FrontendConfigOverrides, VariantInfo } from 'src/application/models';
2
2
  import type { HardcopyResponse } from 'src/contexts/hardcopyContext/hardcopyContext';
3
3
  import type { WidgetTypeV3 } from 'src/contexts/typesV3';
4
4
 
5
+ export type AddToCartCallback = (params: { product_id: string }) => void | Promise<void>;
6
+
5
7
  export interface EnviveConfig {
6
8
  publicKey?: string;
9
+ addToCart?: AddToCartCallback;
7
10
  userID?: string;
8
11
  env?: 'prod' | 'dev';
9
12
  clientID?: string;