@envive-ai/react-hooks 0.2.7-arthur-1 → 0.2.7-arthur-2

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 (176) hide show
  1. package/dist/{NewOrgConfig-yptI2imS.js → NewOrgConfig-BVByiYPp.js} +2 -2
  2. package/dist/{NewOrgConfig-Bo1seKr6.cjs → NewOrgConfig-CInGtTV6.cjs} +2 -2
  3. package/dist/{SystemSettingsContext-EDpRMMt2.cjs → SystemSettingsContext-150LTxIk.cjs} +2 -2
  4. package/dist/{SystemSettingsContext-BY1BFgAQ.js → SystemSettingsContext-ei5B0dxO.js} +2 -2
  5. package/dist/{TrackComponentVisibleEvent-CgxCqrIt.cjs → TrackComponentVisibleEvent-C7-nnBks.cjs} +2 -2
  6. package/dist/{TrackComponentVisibleEvent-CXhKOwKQ.js → TrackComponentVisibleEvent-CuwSLbug.js} +2 -2
  7. package/dist/amplitudeContext-BBQ1ATA3.js +265 -0
  8. package/dist/amplitudeContext-C-0-DDk3.cjs +287 -0
  9. package/dist/{app-CflxT_xI.js → app-C_Y-57U5.js} +3 -4
  10. package/dist/{app-BbPSHefQ.cjs → app-XEFPotoH.cjs} +3 -4
  11. package/dist/application/utils/index.cjs +8 -8
  12. package/dist/application/utils/index.d.cts +2 -2
  13. package/dist/application/utils/index.d.ts +2 -2
  14. package/dist/application/utils/index.js +8 -8
  15. package/dist/{atomStore-CmZbgQHc.cjs → atomStore-8ppNkJ_n.cjs} +1 -1
  16. package/dist/{atomStore-DEcDhiLp.js → atomStore-BLYJ2ZoQ.js} +1 -1
  17. package/dist/atoms/app/index.cjs +4 -4
  18. package/dist/atoms/app/index.d.cts +7 -7
  19. package/dist/atoms/app/index.d.ts +7 -7
  20. package/dist/atoms/app/index.js +4 -4
  21. package/dist/atoms/atomStore/index.cjs +1 -1
  22. package/dist/atoms/atomStore/index.js +1 -1
  23. package/dist/atoms/chat/index.cjs +9 -9
  24. package/dist/atoms/chat/index.d.cts +26 -26
  25. package/dist/atoms/chat/index.d.ts +27 -27
  26. package/dist/atoms/chat/index.js +9 -9
  27. package/dist/atoms/globalSearch/index.d.cts +5 -5
  28. package/dist/atoms/globalSearch/index.d.ts +6 -6
  29. package/dist/atoms/org/index.cjs +1 -1
  30. package/dist/atoms/org/index.d.cts +16 -16
  31. package/dist/atoms/org/index.d.ts +16 -16
  32. package/dist/atoms/org/index.js +1 -1
  33. package/dist/atoms/search/index.cjs +11 -11
  34. package/dist/atoms/search/index.d.cts +1 -1
  35. package/dist/atoms/search/index.d.ts +1 -1
  36. package/dist/atoms/search/index.js +11 -11
  37. package/dist/atoms/search/utils.d.ts +1 -1
  38. package/dist/{cdnContext-CaDyQ_5p.cjs → cdnContext-Bzqk0s2M.cjs} +2 -2
  39. package/dist/{cdnContext-CtrIlAqX.js → cdnContext-CTC-zBtx.js} +2 -2
  40. package/dist/{chat-BjhQCyW_.js → chat-Bzay7QnI.js} +6 -6
  41. package/dist/{chat-BkPax29G.cjs → chat-Ckd1b_z_.cjs} +6 -6
  42. package/dist/{chatSearch-C3N3iIxu.cjs → chatSearch-Bev4ZI8Z.cjs} +3 -3
  43. package/dist/{chatSearch-BsYlFvpv.js → chatSearch-DNaGtQyx.js} +3 -3
  44. package/dist/{chatState-CXA1vF16.js → chatState-CcCvgmSM.js} +2 -2
  45. package/dist/{chatState-CJ52Ag_7.cjs → chatState-OkYPVghN.cjs} +2 -2
  46. package/dist/{commerce-api-rgj30eEp.js → commerce-api-ml5fkEuk.js} +6 -6
  47. package/dist/{commerce-api-DA1QGGMK.cjs → commerce-api-sQtLuwTh.cjs} +6 -6
  48. package/dist/contexts/amplitudeContext/index.cjs +7 -7
  49. package/dist/contexts/amplitudeContext/index.js +7 -7
  50. package/dist/contexts/cdnContext/index.cjs +3 -3
  51. package/dist/contexts/cdnContext/index.js +3 -3
  52. package/dist/contexts/chatContext/index.cjs +16 -16
  53. package/dist/contexts/chatContext/index.d.cts +2 -2
  54. package/dist/contexts/chatContext/index.d.ts +4 -4
  55. package/dist/contexts/chatContext/index.js +16 -16
  56. package/dist/contexts/enviveConfigContext/index.cjs +3 -3
  57. package/dist/contexts/enviveConfigContext/index.js +3 -3
  58. package/dist/contexts/enviveCssContext/index.cjs +6 -6
  59. package/dist/contexts/enviveCssContext/index.js +6 -6
  60. package/dist/contexts/featureFlagContext/index.cjs +4 -4
  61. package/dist/contexts/featureFlagContext/index.js +4 -4
  62. package/dist/contexts/graphqlContext/index.cjs +3 -3
  63. package/dist/contexts/graphqlContext/index.d.ts +1 -1
  64. package/dist/contexts/graphqlContext/index.js +3 -3
  65. package/dist/contexts/localStorageContext/index.cjs +1 -1
  66. package/dist/contexts/localStorageContext/index.js +1 -1
  67. package/dist/contexts/newOrgConfigContext/index.cjs +5 -5
  68. package/dist/contexts/newOrgConfigContext/index.d.ts +2 -2
  69. package/dist/contexts/newOrgConfigContext/index.js +5 -5
  70. package/dist/contexts/searchContext/index.cjs +10 -10
  71. package/dist/contexts/searchContext/index.js +10 -10
  72. package/dist/contexts/sessionStorageContext/index.cjs +1 -1
  73. package/dist/contexts/sessionStorageContext/index.js +1 -1
  74. package/dist/contexts/shopifyUrlContext/index.cjs +1 -1
  75. package/dist/contexts/shopifyUrlContext/index.js +1 -1
  76. package/dist/contexts/systemSettingsContext/index.cjs +3 -3
  77. package/dist/contexts/systemSettingsContext/index.d.cts +2 -2
  78. package/dist/contexts/systemSettingsContext/index.d.ts +2 -2
  79. package/dist/contexts/systemSettingsContext/index.js +3 -3
  80. package/dist/contexts/userIdentityContext/index.cjs +11 -11
  81. package/dist/contexts/userIdentityContext/index.js +11 -11
  82. package/dist/{enviveConfig-DZBohDpc.js → enviveConfig-DV8F12B9.js} +2 -2
  83. package/dist/{enviveConfig-Dv9-esGV.cjs → enviveConfig-DZHdtLsQ.cjs} +2 -2
  84. package/dist/{enviveConfigContext-DrDjCems.js → enviveConfigContext-BS7aNop5.js} +2 -2
  85. package/dist/{enviveConfigContext-D2OELZDR.cjs → enviveConfigContext-CTcHUIFP.cjs} +2 -2
  86. package/dist/frontendConfig-Cawh5iqv.d.ts +1 -1
  87. package/dist/frontendConfig-iZipB5FG.d.cts +1 -1
  88. package/dist/{graphqlContext-DP8T3-Kd.cjs → graphqlContext-CVbYIftg.cjs} +2 -2
  89. package/dist/{graphqlContext-CXQl0hq2.d.ts → graphqlContext-DgkS-UX1.d.ts} +3 -3
  90. package/dist/{graphqlContext-czH0kIZg.js → graphqlContext-DouNZbYo.js} +2 -2
  91. package/dist/hooks/AmplitudeOperations/index.cjs +9 -9
  92. package/dist/hooks/AmplitudeOperations/index.js +9 -9
  93. package/dist/hooks/AppDetails/index.cjs +7 -7
  94. package/dist/hooks/AppDetails/index.js +7 -7
  95. package/dist/hooks/CdnOperations/index.cjs +3 -3
  96. package/dist/hooks/CdnOperations/index.js +3 -3
  97. package/dist/hooks/ChatToggle/index.cjs +9 -9
  98. package/dist/hooks/ChatToggle/index.js +9 -9
  99. package/dist/hooks/ChatToggleAnalytics/index.cjs +10 -10
  100. package/dist/hooks/ChatToggleAnalytics/index.js +10 -10
  101. package/dist/hooks/GrabAndScroll/index.d.cts +2 -2
  102. package/dist/hooks/GrabAndScroll/index.d.ts +2 -2
  103. package/dist/hooks/GraphQLConfig/index.cjs +4 -4
  104. package/dist/hooks/GraphQLConfig/index.d.ts +1 -1
  105. package/dist/hooks/GraphQLConfig/index.js +4 -4
  106. package/dist/hooks/IdentifyUser/index.cjs +11 -11
  107. package/dist/hooks/IdentifyUser/index.js +11 -11
  108. package/dist/hooks/ImageResolver/index.cjs +2 -2
  109. package/dist/hooks/ImageResolver/index.js +2 -2
  110. package/dist/hooks/LocalStorageOperations/index.cjs +1 -1
  111. package/dist/hooks/LocalStorageOperations/index.js +1 -1
  112. package/dist/hooks/NewOrgConfig/index.cjs +6 -6
  113. package/dist/hooks/NewOrgConfig/index.d.ts +2 -2
  114. package/dist/hooks/NewOrgConfig/index.js +6 -6
  115. package/dist/hooks/Search/index.cjs +28 -1166
  116. package/dist/hooks/Search/index.d.cts +1 -1
  117. package/dist/hooks/Search/index.d.ts +1 -1
  118. package/dist/hooks/Search/index.js +26 -1165
  119. package/dist/hooks/SearchOperations/index.cjs +10 -10
  120. package/dist/hooks/SearchOperations/index.js +10 -10
  121. package/dist/hooks/SessionStorageOperations/index.cjs +1 -1
  122. package/dist/hooks/SessionStorageOperations/index.js +1 -1
  123. package/dist/hooks/ShopifyUrlOperations/index.cjs +1 -1
  124. package/dist/hooks/ShopifyUrlOperations/index.js +1 -1
  125. package/dist/hooks/SystemSettingsContext/index.cjs +4 -4
  126. package/dist/hooks/SystemSettingsContext/index.d.cts +2 -2
  127. package/dist/hooks/SystemSettingsContext/index.d.ts +2 -2
  128. package/dist/hooks/SystemSettingsContext/index.js +4 -4
  129. package/dist/hooks/TrackComponentVisibleEvent/index.cjs +8 -8
  130. package/dist/hooks/TrackComponentVisibleEvent/index.js +8 -8
  131. package/dist/hooks/UpdateAnalyticsProps/index.cjs +7 -7
  132. package/dist/hooks/UpdateAnalyticsProps/index.js +7 -7
  133. package/dist/{index-CUO68KG3.d.ts → index-CMJM-3zV.d.ts} +30 -30
  134. package/dist/{index-BSd8767K.d.cts → index-DpJzjjpi.d.cts} +30 -30
  135. package/dist/{localStorageContext-BPZ82q-G.js → localStorageContext-CqcSvg2H.js} +1 -1
  136. package/dist/{localStorageContext-NRP-CdmF.cjs → localStorageContext-DiLfSsqL.cjs} +1 -1
  137. package/dist/{newOrgConfigContext-Bet9CgKP.cjs → newOrgConfigContext-BIDz4ZuO.cjs} +3 -3
  138. package/dist/{newOrgConfigContext-I2qceBB4.d.ts → newOrgConfigContext-CKn7B2rj.d.ts} +2 -2
  139. package/dist/{newOrgConfigContext-Bi_dBNe5.js → newOrgConfigContext-u_9UPNcX.js} +3 -3
  140. package/dist/{orgAnalyticsConfig-Bm23fw4s.cjs → orgAnalyticsConfig-CGEQtAFs.cjs} +1 -1
  141. package/dist/{orgAnalyticsConfig-CpBmga08.js → orgAnalyticsConfig-i4jozLBB.js} +1 -1
  142. package/dist/{search-Csh2n66W.cjs → search-CTVX9gC6.cjs} +2 -2
  143. package/dist/{search-DkiqkogN.js → search-NgNrXNS9.js} +2 -2
  144. package/dist/{searchContext-DksJfC1s.cjs → searchContext-CnDXkawZ.cjs} +5 -5
  145. package/dist/{searchContext-BmgoAFMF.js → searchContext-DtRmshTA.js} +5 -5
  146. package/dist/{sessionStorageContext-B6FsNKjj.cjs → sessionStorageContext-1Ks_d4Z0.cjs} +1 -1
  147. package/dist/{sessionStorageContext-CLYCm83p.js → sessionStorageContext-CDcl7NVl.js} +1 -1
  148. package/dist/{shopifyUrlContext-ZOcARiMR.cjs → shopifyUrlContext-CxjV3qvH.cjs} +1 -1
  149. package/dist/{shopifyUrlContext-C-PkSgNC.js → shopifyUrlContext-D2btP_lY.js} +1 -1
  150. package/dist/{systemSettingsContext-dmE1v6w8.cjs → systemSettingsContext-BejoGzzB.cjs} +2 -2
  151. package/dist/{systemSettingsContext-DF0jSq9m.js → systemSettingsContext-C4dtZ0uZ.js} +2 -2
  152. package/dist/types-BegmH0S1.d.ts +1 -1
  153. package/dist/{unsupportedProductExceptions-DGENUnEA.cjs → unsupportedProductExceptions-B0yx2bHK.cjs} +1 -1
  154. package/dist/{unsupportedProductExceptions-uQuuelOs.js → unsupportedProductExceptions-Cs66ngs3.js} +1 -1
  155. package/dist/{useAmplitudeOperations-Bo6YNbTV.cjs → useAmplitudeOperations-BJXD9v2u.cjs} +2 -2
  156. package/dist/{useAmplitudeOperations-zIRSqmMW.js → useAmplitudeOperations-Dym0Ker8.js} +2 -2
  157. package/dist/{useAppDetails-B584gkCs.js → useAppDetails-Dmh16bWE.js} +4 -4
  158. package/dist/{useAppDetails-DczgqeLG.cjs → useAppDetails-DsAZ1xQn.cjs} +4 -4
  159. package/dist/{useGraphQLConfig-D_rF2Sun.cjs → useGraphQLConfig-B3DlwmGg.cjs} +2 -2
  160. package/dist/{useGraphQLConfig-7UxACM4n.js → useGraphQLConfig-DSRaDTdT.js} +2 -2
  161. package/dist/userIdentityContext-DF3atBFE.js +119 -0
  162. package/dist/userIdentityContext-DpQTduhF.cjs +136 -0
  163. package/dist/{utils-C1ErYSoW.js → utils-B7KTAEmV.js} +2 -2
  164. package/dist/utils-CBD4g2Nc.d.cts +1 -1
  165. package/dist/{utils-mqfncrhI.cjs → utils-CcC2jZRi.cjs} +2 -2
  166. package/package.json +3 -2
  167. package/src/atoms/app/index.ts +1 -1
  168. package/src/contexts/amplitudeContext/__tests__/amplitudeContext.test.tsx +525 -0
  169. package/src/contexts/amplitudeContext/amplitudeContext.tsx +5 -2
  170. package/src/contexts/userIdentityContext/userIdentityContext.tsx +7 -5
  171. package/dist/amplitudeContext-C8tT74Mi.cjs +0 -286
  172. package/dist/amplitudeContext-DCk6Va-j.js +0 -264
  173. package/dist/userIdentityContext-BqbNu7xu.cjs +0 -132
  174. package/dist/userIdentityContext-BxFH9FNQ.js +0 -115
  175. /package/dist/{AmplitudeOperations-ChZWcSsc.js → AmplitudeOperations-C-ieCm9m.js} +0 -0
  176. /package/dist/{AmplitudeOperations-JggIc1zD.cjs → AmplitudeOperations-p7APchq9.cjs} +0 -0
@@ -0,0 +1,525 @@
1
+ import React from "react";
2
+ import { render, screen, waitFor, act } from "@testing-library/react";
3
+ import { Provider, useStore } from "jotai";
4
+ import {
5
+ AmplitudeProvider,
6
+ useAmplitude,
7
+ SpiffyMetricsEventName,
8
+ } from "../amplitudeContext";
9
+ import {
10
+ UserIdentityProvider,
11
+ useUserIdentity,
12
+ } from "src/contexts/userIdentityContext/userIdentityContext";
13
+ import { LocalStorageProvider } from "src/contexts/localStorageContext";
14
+ import { EnviveConfigProvider } from "src/contexts/enviveConfigContext/enviveConfigContext";
15
+ import { FeatureFlagServiceProvider } from "src/contexts/featureFlagServiceContext/featureFlagServiceContext";
16
+ import { userIdAtom } from "src/atoms/app";
17
+ import Logger from "src/application/logging/logger";
18
+ import { createInstance } from "@amplitude/analytics-browser";
19
+
20
+ // Mock the Logger to avoid console output in tests
21
+ // vi.spyOn(Logger, "logInfo").mockImplementation(() => {});
22
+ // vi.spyOn(Logger, "logWarn").mockImplementation(() => {});
23
+ // vi.spyOn(Logger, "logError").mockImplementation(() => {});
24
+ // vi.spyOn(Logger, "logDebug").mockImplementation(() => {});
25
+
26
+ // Mock Amplitude
27
+ const mockTrack = vi.fn();
28
+ const mockInit = vi.fn();
29
+ const mockAdd = vi.fn();
30
+
31
+ vi.mock("@amplitude/analytics-browser", () => ({
32
+ createInstance: vi.fn(() => ({
33
+ track: mockTrack,
34
+ init: mockInit,
35
+ add: mockAdd,
36
+ })),
37
+ }));
38
+
39
+ // Mock EventsDispatcher
40
+ vi.mock("src/events", () => ({
41
+ EventsDispatcher: {
42
+ dispatch: vi.fn(),
43
+ },
44
+ SpiffyEvent: {
45
+ AMPLITUDE_EVENT: "AMPLITUDE_EVENT",
46
+ },
47
+ }));
48
+
49
+ // Mock crypto.subtle.digest for insert_id generation
50
+ const mockDigest = vi.fn().mockResolvedValue(new Uint8Array(32).fill(0));
51
+ Object.defineProperty(global, "crypto", {
52
+ value: {
53
+ subtle: {
54
+ digest: mockDigest,
55
+ },
56
+ },
57
+ writable: true,
58
+ });
59
+
60
+ // Component that uses the useAmplitude hook
61
+ const MockAmplitudeComponent: React.FC = () => {
62
+ const amplitude = useAmplitude();
63
+
64
+ return (
65
+ <div data-testid="amplitude-component">
66
+ <div data-testid="is-ready">{amplitude.isReady.toString()}</div>
67
+ <button
68
+ data-testid="track-event-button"
69
+ onClick={() =>
70
+ amplitude.trackEvent({
71
+ eventName: SpiffyMetricsEventName.ChatUserMessageInput,
72
+ eventProps: { message: "test message" },
73
+ })
74
+ }
75
+ >
76
+ Track Event
77
+ </button>
78
+ </div>
79
+ );
80
+ };
81
+
82
+ // Component that reads userIdAtom to verify it's set
83
+ const AtomReaderComponent: React.FC = () => {
84
+ const store = useStore();
85
+ const [userId, setUserId] = React.useState<string>("not-set");
86
+
87
+ React.useEffect(() => {
88
+ const unsubscribe = store.sub(userIdAtom, () => {
89
+ try {
90
+ const value = store.get(userIdAtom);
91
+ if (value) {
92
+ setUserId(value);
93
+ }
94
+ } catch {
95
+ // Still not set
96
+ }
97
+ });
98
+
99
+ const timer = setTimeout(() => {
100
+ try {
101
+ const value = store.get(userIdAtom);
102
+ if (value) {
103
+ setUserId(value);
104
+ }
105
+ } catch {
106
+ // Not set yet
107
+ }
108
+ }, 50);
109
+
110
+ return () => {
111
+ unsubscribe();
112
+ clearTimeout(timer);
113
+ };
114
+ }, [store]);
115
+
116
+ return <div data-testid="atom-reader">{userId}</div>;
117
+ };
118
+
119
+ // Component that uses both contexts
120
+ const CombinedContextComponent: React.FC = () => {
121
+ const userIdentity = useUserIdentity();
122
+ const amplitude = useAmplitude();
123
+
124
+ return (
125
+ <div data-testid="combined-component">
126
+ <div data-testid="user-id-from-identity">
127
+ {userIdentity.getUserIdOrDefault()}
128
+ </div>
129
+ <div data-testid="amplitude-ready">{amplitude.isReady.toString()}</div>
130
+ </div>
131
+ );
132
+ };
133
+
134
+ // Wrapper component with all required providers
135
+ const TestWrapper: React.FC<{
136
+ children: React.ReactNode;
137
+ amplitudeApiKey?: string;
138
+ userIdOverride?: string;
139
+ userIdDefault?: string;
140
+ }> = ({
141
+ children,
142
+ amplitudeApiKey = "test-amplitude-key",
143
+ userIdOverride,
144
+ userIdDefault,
145
+ }) => {
146
+ // Set up localStorage with user IDs if provided
147
+ React.useEffect(() => {
148
+ if (userIdOverride) {
149
+ localStorage.setItem("v1-spiffy-user-id-override", userIdOverride);
150
+ }
151
+ if (userIdDefault) {
152
+ localStorage.setItem("v1-spiffy-user-id-default", userIdDefault);
153
+ }
154
+ }, [userIdOverride, userIdDefault]);
155
+
156
+ return (
157
+ <Provider>
158
+ <EnviveConfigProvider
159
+ identifyingPrefix="test"
160
+ orgShortName="test-org"
161
+ amplitudeApiKey={amplitudeApiKey}
162
+ dataResidency="US"
163
+ env="test"
164
+ contextSource="app"
165
+ featureGates={[]}
166
+ >
167
+ <LocalStorageProvider>
168
+ <UserIdentityProvider>
169
+ <FeatureFlagServiceProvider featureGates={[]}>
170
+ <AmplitudeProvider>{children}</AmplitudeProvider>
171
+ </FeatureFlagServiceProvider>
172
+ </UserIdentityProvider>
173
+ </LocalStorageProvider>
174
+ </EnviveConfigProvider>
175
+ </Provider>
176
+ );
177
+ };
178
+
179
+ describe("AmplitudeProvider with UserIdentityContext", () => {
180
+ beforeEach(() => {
181
+ vi.clearAllMocks();
182
+ if (typeof localStorage !== "undefined") {
183
+ localStorage.clear();
184
+ }
185
+ // Reset mocks
186
+ mockTrack.mockClear();
187
+ mockInit.mockClear();
188
+ mockAdd.mockClear();
189
+ mockDigest.mockResolvedValue(new Uint8Array(32).fill(0));
190
+ });
191
+
192
+ describe("Integration with UserIdentityContext - userIdAtom", () => {
193
+ it("should be ready when UserIdentityContext provides a userId", async () => {
194
+ render(
195
+ <TestWrapper>
196
+ <CombinedContextComponent />
197
+ </TestWrapper>
198
+ );
199
+
200
+ await waitFor(() => {
201
+ const isReady = screen.getByTestId("amplitude-ready").textContent;
202
+ expect(isReady).toBe("true");
203
+ });
204
+ });
205
+
206
+ it("should use userId from UserIdentityContext in tracking events", async () => {
207
+ const testUserId = "test-user-id-123";
208
+ render(
209
+ <TestWrapper userIdOverride={testUserId}>
210
+ <MockAmplitudeComponent />
211
+ </TestWrapper>
212
+ );
213
+
214
+ await waitFor(() => {
215
+ expect(screen.getByTestId("is-ready").textContent).toBe("true");
216
+ });
217
+
218
+ await act(async () => {
219
+ screen.getByTestId("track-event-button").click();
220
+ });
221
+
222
+ await waitFor(() => {
223
+ expect(mockTrack).toHaveBeenCalled();
224
+ const callArgs = mockTrack.mock.calls[0];
225
+ expect(callArgs[0]).toBe("[Spiffy] Chat User Message Input");
226
+ expect(callArgs[1]).toHaveProperty("user.id", testUserId);
227
+ });
228
+ });
229
+
230
+ it("should use default userId from UserIdentityContext when override is not set", async () => {
231
+ const testUserId = "default-user-id-456";
232
+ render(
233
+ <TestWrapper userIdDefault={testUserId}>
234
+ <MockAmplitudeComponent />
235
+ </TestWrapper>
236
+ );
237
+
238
+ await waitFor(() => {
239
+ expect(screen.getByTestId("is-ready").textContent).toBe("true");
240
+ });
241
+
242
+ await act(async () => {
243
+ screen.getByTestId("track-event-button").click();
244
+ });
245
+
246
+ await waitFor(() => {
247
+ expect(mockTrack).toHaveBeenCalled();
248
+ const callArgs = mockTrack.mock.calls[0];
249
+ expect(callArgs[1]).toHaveProperty("user.id", testUserId);
250
+ });
251
+ });
252
+
253
+ it("should prioritize override userId over default userId", async () => {
254
+ const overrideUserId = "override-user-id-789";
255
+ const defaultUserId = "default-user-id-012";
256
+ render(
257
+ <TestWrapper
258
+ userIdOverride={overrideUserId}
259
+ userIdDefault={defaultUserId}
260
+ >
261
+ <MockAmplitudeComponent />
262
+ </TestWrapper>
263
+ );
264
+
265
+ await waitFor(() => {
266
+ expect(screen.getByTestId("is-ready").textContent).toBe("true");
267
+ });
268
+
269
+ await act(async () => {
270
+ screen.getByTestId("track-event-button").click();
271
+ });
272
+
273
+ await waitFor(() => {
274
+ expect(mockTrack).toHaveBeenCalled();
275
+ const callArgs = mockTrack.mock.calls[0];
276
+ expect(callArgs[1]).toHaveProperty("user.id", overrideUserId);
277
+ expect(callArgs[1]).not.toHaveProperty("user.id", defaultUserId);
278
+ });
279
+ });
280
+
281
+ it("should initialize Amplitude with userId from UserIdentityContext", async () => {
282
+ const testUserId = "init-user-id-345";
283
+ render(
284
+ <TestWrapper userIdOverride={testUserId}>
285
+ <MockAmplitudeComponent />
286
+ </TestWrapper>
287
+ );
288
+
289
+ await waitFor(() => {
290
+ expect(mockInit).toHaveBeenCalled();
291
+ const initArgs = mockInit.mock.calls[0];
292
+ expect(initArgs[1]).toBe(testUserId);
293
+ });
294
+ });
295
+ });
296
+
297
+ describe("userIdAtom synchronization", () => {
298
+ it("should sync userIdAtom with UserIdentityContext userId", async () => {
299
+ const testUserId = "sync-user-id-678";
300
+ render(
301
+ <TestWrapper userIdOverride={testUserId}>
302
+ <>
303
+ <CombinedContextComponent />
304
+ <AtomReaderComponent />
305
+ </>
306
+ </TestWrapper>
307
+ );
308
+
309
+ await waitFor(() => {
310
+ const identityUserId = screen.getByTestId(
311
+ "user-id-from-identity"
312
+ ).textContent;
313
+ const atomUserId = screen.getByTestId("atom-reader").textContent;
314
+ expect(identityUserId).toBe(testUserId);
315
+ expect(atomUserId).toBe(testUserId);
316
+ expect(identityUserId).toBe(atomUserId);
317
+ });
318
+ });
319
+ });
320
+
321
+ describe("Event tracking with UserIdentityContext userId", () => {
322
+ it("should include user.id in all tracked events", async () => {
323
+ const testUserId = "event-user-id-901";
324
+ render(
325
+ <TestWrapper userIdOverride={testUserId}>
326
+ <MockAmplitudeComponent />
327
+ </TestWrapper>
328
+ );
329
+
330
+ await waitFor(() => {
331
+ expect(screen.getByTestId("is-ready").textContent).toBe("true");
332
+ });
333
+
334
+ await act(async () => {
335
+ screen.getByTestId("track-event-button").click();
336
+ });
337
+
338
+ await waitFor(() => {
339
+ expect(mockTrack).toHaveBeenCalled();
340
+ const callArgs = mockTrack.mock.calls[0];
341
+ expect(callArgs[1]).toHaveProperty("user.id", testUserId);
342
+ });
343
+ });
344
+
345
+ it("should include user.id in default tracking props", async () => {
346
+ const testUserId = "default-props-user-id";
347
+ render(
348
+ <TestWrapper userIdOverride={testUserId}>
349
+ <MockAmplitudeComponent />
350
+ </TestWrapper>
351
+ );
352
+
353
+ await waitFor(() => {
354
+ expect(screen.getByTestId("is-ready").textContent).toBe("true");
355
+ });
356
+
357
+ await act(async () => {
358
+ screen.getByTestId("track-event-button").click();
359
+ });
360
+
361
+ await waitFor(() => {
362
+ expect(mockTrack).toHaveBeenCalled();
363
+ const callArgs = mockTrack.mock.calls[0];
364
+ const eventProps = callArgs[1];
365
+ expect(eventProps["user.id"]).toBe(testUserId);
366
+ expect(eventProps["cdp.user_id"]).toBe(null);
367
+ });
368
+ });
369
+
370
+ it("should track multiple events with correct userId", async () => {
371
+ const testUserId = "multi-event-user-id";
372
+ const MultiEventComponent: React.FC = () => {
373
+ const amplitude = useAmplitude();
374
+
375
+ const trackMultiple = () => {
376
+ amplitude.trackEvent({
377
+ eventName: SpiffyMetricsEventName.ChatUserMessageInput,
378
+ eventProps: { event1: true },
379
+ });
380
+ amplitude.trackEvent({
381
+ eventName: SpiffyMetricsEventName.ChatSuggestionClicked,
382
+ eventProps: { event2: true },
383
+ });
384
+ };
385
+
386
+ return (
387
+ <button data-testid="track-multiple" onClick={trackMultiple}>
388
+ Track Multiple
389
+ </button>
390
+ );
391
+ };
392
+
393
+ render(
394
+ <TestWrapper userIdOverride={testUserId}>
395
+ <MultiEventComponent />
396
+ </TestWrapper>
397
+ );
398
+
399
+ await waitFor(() => {
400
+ expect(screen.getByTestId("track-multiple")).toBeInTheDocument();
401
+ });
402
+
403
+ await act(async () => {
404
+ screen.getByTestId("track-multiple").click();
405
+ });
406
+
407
+ await waitFor(() => {
408
+ expect(mockTrack).toHaveBeenCalledTimes(2);
409
+ mockTrack.mock.calls.forEach((call: any[]) => {
410
+ expect(call[1]).toHaveProperty("user.id", testUserId);
411
+ });
412
+ });
413
+ });
414
+ });
415
+
416
+ describe("Context readiness and initialization", () => {
417
+ it("should wait for UserIdentityContext to be ready before becoming ready", async () => {
418
+ render(
419
+ <TestWrapper>
420
+ <CombinedContextComponent />
421
+ </TestWrapper>
422
+ );
423
+
424
+ // Both contexts should eventually be ready
425
+ await waitFor(() => {
426
+ const amplitudeReady = screen.getByTestId("amplitude-ready").textContent;
427
+ expect(amplitudeReady).toBe("true");
428
+ });
429
+ });
430
+
431
+ it("should initialize Amplitude client with correct userId from UserIdentityContext", async () => {
432
+ const testUserId = "init-client-user-id";
433
+ render(
434
+ <TestWrapper userIdOverride={testUserId}>
435
+ <MockAmplitudeComponent />
436
+ </TestWrapper>
437
+ );
438
+
439
+ await waitFor(() => {
440
+ expect(mockInit).toHaveBeenCalledWith(
441
+ "test-amplitude-key",
442
+ testUserId,
443
+ expect.objectContaining({
444
+ serverZone: "US",
445
+ })
446
+ );
447
+ });
448
+ });
449
+
450
+ it("should not render children when not ready", async () => {
451
+ // Create a provider without required dependencies
452
+ const IncompleteWrapper: React.FC<{ children: React.ReactNode }> = ({
453
+ children,
454
+ }) => {
455
+ return (
456
+ <Provider>
457
+ <EnviveConfigProvider
458
+ identifyingPrefix="test"
459
+ orgShortName="test-org"
460
+ // Missing amplitudeApiKey
461
+ featureGates={[]}
462
+ >
463
+ <LocalStorageProvider>
464
+ <UserIdentityProvider>
465
+ <FeatureFlagServiceProvider featureGates={[]}>
466
+ <AmplitudeProvider>{children}</AmplitudeProvider>
467
+ </FeatureFlagServiceProvider>
468
+ </UserIdentityProvider>
469
+ </LocalStorageProvider>
470
+ </EnviveConfigProvider>
471
+ </Provider>
472
+ );
473
+ };
474
+
475
+ const { container } = render(
476
+ <IncompleteWrapper>
477
+ <div data-testid="should-not-render">Should not render</div>
478
+ </IncompleteWrapper>
479
+ );
480
+
481
+ // AmplitudeProvider returns null when not ready, so children should not render
482
+ await waitFor(() => {
483
+ expect(
484
+ screen.queryByTestId("should-not-render")
485
+ ).not.toBeInTheDocument();
486
+ });
487
+ });
488
+ });
489
+
490
+ describe("Error handling", () => {
491
+ it("should handle tracking errors gracefully when userId is available", async () => {
492
+ const testUserId = "error-user-id";
493
+ mockTrack.mockImplementationOnce(() => {
494
+ throw new Error("Tracking error");
495
+ });
496
+
497
+ const logErrorSpy = vi.spyOn(Logger, "logError");
498
+
499
+ render(
500
+ <TestWrapper userIdOverride={testUserId}>
501
+ <MockAmplitudeComponent />
502
+ </TestWrapper>
503
+ );
504
+
505
+ await waitFor(() => {
506
+ expect(screen.getByTestId("is-ready").textContent).toBe("true");
507
+ });
508
+
509
+ await act(async () => {
510
+ screen.getByTestId("track-event-button").click();
511
+ });
512
+
513
+ await waitFor(() => {
514
+ expect(logErrorSpy).toHaveBeenCalledWith(
515
+ "[spiffy-ai] Error tracking event",
516
+ expect.any(Error),
517
+ expect.objectContaining({
518
+ eventName: SpiffyMetricsEventName.ChatUserMessageInput,
519
+ })
520
+ );
521
+ });
522
+ });
523
+ });
524
+ });
525
+
@@ -34,7 +34,6 @@ import { useEnviveConfig } from "src/contexts/enviveConfigContext/enviveConfigCo
34
34
  import {
35
35
  useFeatureFlagService,
36
36
  } from "src/contexts/featureFlagServiceContext/featureFlagServiceContext";
37
- import { useUserIdentity } from "src/contexts/userIdentityContext/userIdentityContext";
38
37
 
39
38
  export enum SpiffyMetricsEventName {
40
39
  BundleLoaded = "Bundle Loaded",
@@ -116,7 +115,7 @@ export const AmplitudeProvider: React.FC<{ children: React.ReactNode }> = ({
116
115
  React.useState<Record<string, unknown>>({});
117
116
 
118
117
  const isReady = Boolean(
119
- userId && featureFlagService && amplitudeApiKey && userId
118
+ userId && featureFlagService && amplitudeApiKey
120
119
  );
121
120
 
122
121
  const getDefaultTrackingProps = useCallback((): Record<string, unknown> => {
@@ -416,6 +415,10 @@ export const AmplitudeProvider: React.FC<{ children: React.ReactNode }> = ({
416
415
  [trackEvent, isReady, setSupplementalDefaultProps]
417
416
  );
418
417
 
418
+ if (!isReady) {
419
+ return null;
420
+ }
421
+
419
422
  return (
420
423
  <AmplitudeContext.Provider value={value}>
421
424
  {children}
@@ -12,7 +12,7 @@ import CommerceApiClient from "src/application/commerce-api";
12
12
  import { v4 as uuid } from "uuid";
13
13
  import { ClientDetails } from "src/application/models/clientDetails";
14
14
  import { useLocalStorage } from "src/contexts/localStorageContext";
15
- import { useSetAtom } from "jotai";
15
+ import { useAtom } from "jotai";
16
16
  import { userIdAtom } from "src/atoms/app";
17
17
 
18
18
  // Helper function from the original service
@@ -50,6 +50,7 @@ const UserIdentityContext = createContext<UserIdentityContextType | undefined>(
50
50
  export const UserIdentityProvider: React.FC<{ children: React.ReactNode }> = ({
51
51
  children,
52
52
  }) => {
53
+ const [userId, setUserId] = useAtom(userIdAtom);
53
54
  const {
54
55
  getItem,
55
56
  setItem,
@@ -180,6 +181,11 @@ export const UserIdentityProvider: React.FC<{ children: React.ReactNode }> = ({
180
181
  isReady,
181
182
  ]
182
183
  );
184
+ useEffect(() => {
185
+ if (isReady && !userId) {
186
+ setUserId(getUserIdOrDefault() ?? "");
187
+ }
188
+ }, [isReady, userId, setUserId]);
183
189
 
184
190
  return (
185
191
  <UserIdentityContext.Provider value={value}>
@@ -190,10 +196,6 @@ export const UserIdentityProvider: React.FC<{ children: React.ReactNode }> = ({
190
196
 
191
197
  export const useUserIdentity = () => {
192
198
  const context = useContext(UserIdentityContext);
193
- const setUserId = useSetAtom(userIdAtom);
194
- useEffect(() => {
195
- setUserId(context?.getUserIdOrDefault() ?? "");
196
- }, [context, setUserId]);
197
199
  if (!context) {
198
200
  throw new Error(
199
201
  "useUserIdentity must be used within a UserIdentityProvider"