@envive-ai/react-widgets-v3 0.3.14 → 0.3.15-alpha.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 (168) hide show
  1. package/dist/debug/chatEmbed.cjs +1 -1
  2. package/dist/debug/chatEmbed.js +1 -1
  3. package/dist/debug/reportIssue.cjs +1 -1
  4. package/dist/debug/reportIssue.js +1 -1
  5. package/dist/hocs/withBaseWidget/types.d.cts +7 -8
  6. package/dist/hocs/withBaseWidget/types.d.ts +8 -8
  7. package/dist/hocs/withBaseWidget/withBaseWidget.cjs +37 -43
  8. package/dist/hocs/withBaseWidget/withBaseWidget.d.cts +2 -2
  9. package/dist/hocs/withBaseWidget/withBaseWidget.d.ts +2 -2
  10. package/dist/hocs/withBaseWidget/withBaseWidget.js +39 -45
  11. package/dist/hooks/dist/application/models/api/widgetText.d.cts +8 -0
  12. package/dist/hooks/dist/contexts/hardcopyContext/hardcopyContext.d.cts +12 -0
  13. package/dist/hooks/dist/contexts/types.d.cts +38 -0
  14. package/dist/hooks/dist/contexts/typesV3.d.cts +230 -0
  15. package/dist/hooks/dist/types/customerService.d.cts +21 -0
  16. package/dist/node_modules/@spiffy-ai/commerce-api-client/dist/models/WidgetType.cjs +1 -0
  17. package/dist/packages/hooks/dist/application/models/api/orgConfigResults.d.ts +2 -0
  18. package/dist/packages/hooks/dist/application/models/api/userEvent.d.ts +1 -0
  19. package/dist/packages/hooks/dist/application/models/api/widgetText.d.ts +8 -0
  20. package/dist/packages/hooks/dist/application/models/frontendConfig.d.ts +1 -0
  21. package/dist/packages/hooks/dist/contexts/hardcopyContext/hardcopyContext.d.ts +15 -0
  22. package/dist/packages/hooks/dist/contexts/hardcopyContext/index.d.ts +1 -0
  23. package/dist/packages/hooks/dist/contexts/types.d.ts +44 -0
  24. package/dist/packages/hooks/dist/contexts/typesV3.d.ts +230 -0
  25. package/dist/packages/hooks/dist/types/customerService.d.ts +21 -0
  26. package/dist/packages/hooks/dist/types/custservice-types.d.ts +1 -0
  27. package/dist/packages/icons/dist/IconCloseVariant.cjs +5 -6
  28. package/dist/packages/icons/dist/IconCloseVariant.js +4 -6
  29. package/dist/packages/widgets/dist/SearchResults/SearchResults.d.ts +4 -3
  30. package/dist/packages/widgets/dist/SearchResults/SearchResultsWidget.d.ts +2 -2
  31. package/dist/packages/widgets/dist/SearchZeroState/SearchZeroStateWidget.d.ts +2 -2
  32. package/dist/packages/widgets/dist/SearchZeroState/index.d.ts +2 -1
  33. package/dist/packages/widgets/dist/SearchZeroState/types.d.ts +2 -2
  34. package/dist/packages/widgets/dist/SuggestionBar/SuggestionBar.d.ts +3 -3
  35. package/dist/packages/widgets/dist/SuggestionButtonContainer/index.d.ts +2 -0
  36. package/dist/packages/widgets/dist/SuggestionButtonContainer/types.d.ts +3 -2
  37. package/dist/packages/widgets/dist/packages/hooks/dist/application/models/api/response.d.ts +14 -0
  38. package/dist/packages/widgets/dist/packages/hooks/dist/application/models/api/search.d.ts +15 -0
  39. package/dist/packages/widgets/dist/packages/hooks/dist/application/models/utilityTypes/camelCase.d.ts +73 -0
  40. package/dist/packages/widgets/dist/packages/hooks/dist/application/models/utilityTypes/camelCasedPropertiesDeep.d.ts +61 -0
  41. package/dist/packages/widgets/dist/packages/hooks/dist/application/models/utilityTypes/internal.d.ts +25 -0
  42. package/dist/packages/widgets/dist/packages/hooks/dist/application/models/utilityTypes/splitWords.d.ts +35 -0
  43. package/dist/packages/widgets/dist/packages/hooks/dist/application/models/utilityTypes/trim.d.ts +32 -0
  44. package/dist/packages/widgets/dist/packages/hooks/dist/application/models/utilityTypes/unknownArray.d.ts +32 -0
  45. package/dist/packages/widgets/dist/packages/hooks/dist/application/models/variantInfo/pageVisitInfo.d.ts +1 -0
  46. package/dist/packages/widgets/dist/packages/hooks/dist/application/models/variantInfo/variantInfo.d.ts +2 -0
  47. package/dist/packages/widgets/dist/packages/hooks/dist/atoms/search/searchAPI.d.ts +15 -0
  48. package/dist/packages/widgets/dist/packages/hooks/dist/contexts/hardcopyContext/hardcopyContext.d.ts +1 -0
  49. package/dist/packages/widgets/dist/packages/hooks/dist/contexts/types.d.ts +64 -0
  50. package/dist/packages/widgets/dist/packages/hooks/dist/hooks/Search/useSearch.d.ts +60 -0
  51. package/dist/packages/widgets/dist/packages/hooks/dist/hooks/utils.d.ts +13 -0
  52. package/dist/packages/widgets/dist/packages/hooks/dist/types/OrgInfo.d.ts +1 -0
  53. package/dist/packages/widgets/dist/packages/hooks/dist/types/index.d.ts +1 -0
  54. package/dist/packages/widgets/dist/packages/hooks/dist/types/search-filter-types.d.ts +28 -0
  55. package/dist/packages/widgets/dist/packages/hooks/dist/types/test-types.d.ts +10 -0
  56. package/dist/widgets/ChatPreviewComparisonWidget/ChatPreviewComparisonWidget.cjs +35 -4
  57. package/dist/widgets/ChatPreviewComparisonWidget/ChatPreviewComparisonWidget.d.cts +3 -3
  58. package/dist/widgets/ChatPreviewComparisonWidget/ChatPreviewComparisonWidget.d.ts +3 -3
  59. package/dist/widgets/ChatPreviewComparisonWidget/ChatPreviewComparisonWidget.js +35 -4
  60. package/dist/widgets/ChatPreviewLoadingWidget/ChatPreviewLoadingWidget.d.cts +3 -3
  61. package/dist/widgets/ChatPreviewLoadingWidget/ChatPreviewLoadingWidget.d.ts +3 -3
  62. package/dist/widgets/ChatPreviewWidget/ChatPreviewWidget.cjs +33 -12
  63. package/dist/widgets/ChatPreviewWidget/ChatPreviewWidget.d.cts +3 -3
  64. package/dist/widgets/ChatPreviewWidget/ChatPreviewWidget.d.ts +3 -3
  65. package/dist/widgets/ChatPreviewWidget/ChatPreviewWidget.js +35 -14
  66. package/dist/widgets/FloatingChatWidget/FloatingChatOverlay.cjs +44 -23
  67. package/dist/widgets/FloatingChatWidget/FloatingChatOverlay.js +44 -23
  68. package/dist/widgets/FloatingChatWidget/FloatingChatWidget.cjs +62 -38
  69. package/dist/widgets/FloatingChatWidget/FloatingChatWidget.d.cts +5 -3
  70. package/dist/widgets/FloatingChatWidget/FloatingChatWidget.d.ts +5 -3
  71. package/dist/widgets/FloatingChatWidget/FloatingChatWidget.js +63 -39
  72. package/dist/widgets/FullPageSalesAgentWidget/FullPageSalesAgentWidget.d.cts +2 -2
  73. package/dist/widgets/FullPageSalesAgentWidget/FullPageSalesAgentWidget.d.ts +2 -2
  74. package/dist/widgets/ProductCardWidget/ProductCardWidget.cjs +24 -12
  75. package/dist/widgets/ProductCardWidget/ProductCardWidget.d.cts +2 -2
  76. package/dist/widgets/ProductCardWidget/ProductCardWidget.d.ts +2 -2
  77. package/dist/widgets/ProductCardWidget/ProductCardWidget.js +26 -14
  78. package/dist/widgets/PromptButtonCarouselWithImageWidget/PromptButtonCarouselWithImageWidget.cjs +22 -12
  79. package/dist/widgets/PromptButtonCarouselWithImageWidget/PromptButtonCarouselWithImageWidget.d.cts +3 -3
  80. package/dist/widgets/PromptButtonCarouselWithImageWidget/PromptButtonCarouselWithImageWidget.d.ts +3 -3
  81. package/dist/widgets/PromptButtonCarouselWithImageWidget/PromptButtonCarouselWithImageWidget.js +24 -14
  82. package/dist/widgets/PromptCarouselWidget/PromptCarouselWidget.cjs +14 -11
  83. package/dist/widgets/PromptCarouselWidget/PromptCarouselWidget.d.cts +2 -2
  84. package/dist/widgets/PromptCarouselWidget/PromptCarouselWidget.d.ts +2 -2
  85. package/dist/widgets/PromptCarouselWidget/PromptCarouselWidget.js +18 -15
  86. package/dist/widgets/SocialProofFlowWidget/SocialProofFlowWidget.cjs +1 -1
  87. package/dist/widgets/SocialProofFlowWidget/SocialProofFlowWidget.d.ts +2 -2
  88. package/dist/widgets/SocialProofFlowWidget/SocialProofFlowWidget.js +1 -1
  89. package/dist/widgets/SocialProofWidget/SocialProofWidget.cjs +53 -15
  90. package/dist/widgets/SocialProofWidget/SocialProofWidget.d.cts +3 -3
  91. package/dist/widgets/SocialProofWidget/SocialProofWidget.d.ts +3 -3
  92. package/dist/widgets/SocialProofWidget/SocialProofWidget.js +56 -18
  93. package/dist/widgets/TitledPromptCarouselWidget/TitledPromptCarouselWidget.cjs +10 -10
  94. package/dist/widgets/TitledPromptCarouselWidget/TitledPromptCarouselWidget.d.cts +2 -2
  95. package/dist/widgets/TitledPromptCarouselWidget/TitledPromptCarouselWidget.d.ts +2 -2
  96. package/dist/widgets/TitledPromptCarouselWidget/TitledPromptCarouselWidget.js +13 -13
  97. package/dist/widgets/TypingAnimationFlowWidget/TypingAnimationFlowWidget.cjs +1 -1
  98. package/dist/widgets/TypingAnimationFlowWidget/TypingAnimationFlowWidget.d.cts +2 -2
  99. package/dist/widgets/TypingAnimationFlowWidget/TypingAnimationFlowWidget.d.ts +2 -2
  100. package/dist/widgets/TypingAnimationFlowWidget/TypingAnimationFlowWidget.js +1 -1
  101. package/dist/widgets/TypingAnimationWidget/TypingAnimationWidget.cjs +23 -11
  102. package/dist/widgets/TypingAnimationWidget/TypingAnimationWidget.d.cts +3 -3
  103. package/dist/widgets/TypingAnimationWidget/TypingAnimationWidget.d.ts +3 -3
  104. package/dist/widgets/TypingAnimationWidget/TypingAnimationWidget.js +26 -14
  105. package/dist/widgets/dist/SearchResults/SearchResults.d.cts +4 -3
  106. package/dist/widgets/dist/SearchResults/SearchResultsWidget.d.cts +2 -2
  107. package/dist/widgets/dist/SearchZeroState/SearchZeroStateWidget.d.cts +2 -2
  108. package/dist/widgets/dist/SearchZeroState/types.d.cts +2 -2
  109. package/dist/widgets/dist/SuggestionBar/SuggestionBar.d.cts +3 -3
  110. package/dist/widgets/dist/SuggestionButtonContainer/types.d.cts +3 -2
  111. package/dist/widgets/dist/packages/hooks/dist/application/models/api/response.d.cts +14 -0
  112. package/dist/widgets/dist/packages/hooks/dist/application/models/api/search.d.cts +15 -0
  113. package/dist/widgets/dist/packages/hooks/dist/application/models/utilityTypes/camelCase.d.cts +73 -0
  114. package/dist/widgets/dist/packages/hooks/dist/application/models/utilityTypes/camelCasedPropertiesDeep.d.cts +61 -0
  115. package/dist/widgets/dist/packages/hooks/dist/application/models/utilityTypes/internal.d.cts +25 -0
  116. package/dist/widgets/dist/packages/hooks/dist/application/models/utilityTypes/splitWords.d.cts +35 -0
  117. package/dist/widgets/dist/packages/hooks/dist/application/models/utilityTypes/trim.d.cts +32 -0
  118. package/dist/widgets/dist/packages/hooks/dist/application/models/utilityTypes/unknownArray.d.cts +32 -0
  119. package/dist/widgets/dist/packages/hooks/dist/atoms/search/searchAPI.d.cts +14 -0
  120. package/dist/widgets/dist/packages/hooks/dist/contexts/types.d.cts +61 -0
  121. package/dist/widgets/dist/packages/hooks/dist/hooks/Search/useSearch.d.cts +60 -0
  122. package/dist/widgets/dist/packages/hooks/dist/hooks/utils.d.cts +12 -0
  123. package/dist/widgets/dist/packages/hooks/dist/types/search-filter-types.d.cts +28 -0
  124. package/dist/widgets/dist/packages/hooks/dist/types/test-types.d.cts +10 -0
  125. package/dist/widgets/hooks/useGetWidgetStatus.cjs +1 -1
  126. package/dist/widgets/hooks/useGetWidgetStatus.js +1 -1
  127. package/dist/widgets/utils/functions.cjs +0 -14
  128. package/dist/widgets/utils/functions.js +1 -14
  129. package/dist/widgets-v2/SearchZeroState/index.d.cts +2 -1
  130. package/dist/widgets-v2/SearchZeroState/index.d.ts +2 -1
  131. package/dist/widgets-v2/SuggestionButtonContainer/index.d.ts +1 -0
  132. package/package.json +4 -4
  133. package/src/hocs/withBaseWidget/__tests__/withBaseWidget.test.tsx +68 -302
  134. package/src/hocs/withBaseWidget/types.ts +6 -6
  135. package/src/hocs/withBaseWidget/withBaseWidget.tsx +46 -61
  136. package/src/widgets/ChatPreviewComparisonWidget/ChatPreviewComparisonWidget.tsx +59 -8
  137. package/src/widgets/ChatPreviewWidget/ChatPreviewWidget.tsx +54 -18
  138. package/src/widgets/ChatPreviewWidget/__tests__/ChatPreviewWidget.test.tsx +11 -5
  139. package/src/widgets/FloatingChatWidget/FloatingChatOverlay.tsx +48 -19
  140. package/src/widgets/FloatingChatWidget/FloatingChatWidget.tsx +100 -52
  141. package/src/widgets/FloatingChatWidget/__tests__/FloatingChatWidget.test.tsx +11 -5
  142. package/src/widgets/ProductCardWidget/ProductCardWidget.tsx +37 -16
  143. package/src/widgets/ProductCardWidget/__tests__/ProductCardWidget.test.tsx +12 -8
  144. package/src/widgets/PromptButtonCarouselWithImageWidget/PromptButtonCarouselWithImageWidget.tsx +38 -19
  145. package/src/widgets/PromptButtonCarouselWithImageWidget/__tests__/PromptButtonCarouselWithImageWidget.test.tsx +19 -8
  146. package/src/widgets/PromptCarouselWidget/PromptCarouselWidget.tsx +32 -19
  147. package/src/widgets/PromptCarouselWidget/__tests__/PromptCarouselWidget.test.tsx +21 -8
  148. package/src/widgets/SocialProofWidget/SocialProofWidget.tsx +77 -18
  149. package/src/widgets/SocialProofWidget/__tests__/SocialProofWidget.test.tsx +19 -8
  150. package/src/widgets/TitledPromptCarouselWidget/TitledPromptCarouselWidget.tsx +23 -19
  151. package/src/widgets/TitledPromptCarouselWidget/__tests__/TitledPromptCarouselWidget.test.tsx +19 -8
  152. package/src/widgets/TypingAnimationWidget/TypingAnimationWidget.tsx +45 -21
  153. package/src/widgets/TypingAnimationWidget/__tests__/TypingAnimationWidget.test.tsx +19 -8
  154. package/src/widgets/__tests__/trackEventCanary.test.ts +1 -1
  155. package/dist/packages/icons/dist/_virtual/rolldown_runtime.cjs +0 -27
  156. package/dist/packages/icons/dist/_virtual/rolldown_runtime.js +0 -25
  157. package/dist/packages/icons/dist/node_modules/react/cjs/react-jsx-runtime.development.cjs +0 -696
  158. package/dist/packages/icons/dist/node_modules/react/cjs/react-jsx-runtime.development.js +0 -696
  159. package/dist/packages/icons/dist/node_modules/react/cjs/react-jsx-runtime.production.min.cjs +0 -43
  160. package/dist/packages/icons/dist/node_modules/react/cjs/react-jsx-runtime.production.min.js +0 -43
  161. package/dist/packages/icons/dist/node_modules/react/cjs/react.development.cjs +0 -1528
  162. package/dist/packages/icons/dist/node_modules/react/cjs/react.development.js +0 -1528
  163. package/dist/packages/icons/dist/node_modules/react/cjs/react.production.min.cjs +0 -329
  164. package/dist/packages/icons/dist/node_modules/react/cjs/react.production.min.js +0 -329
  165. package/dist/packages/icons/dist/node_modules/react/index.cjs +0 -13
  166. package/dist/packages/icons/dist/node_modules/react/index.js +0 -13
  167. package/dist/packages/icons/dist/node_modules/react/jsx-runtime.cjs +0 -13
  168. package/dist/packages/icons/dist/node_modules/react/jsx-runtime.js +0 -13
@@ -1,70 +1,15 @@
1
1
  import { render, screen, waitFor } from '@testing-library/react';
2
2
  import { RefObject } from 'react';
3
- import { SpiffyMetricsEventName } from '@envive-ai/react-hooks/contexts/amplitudeContext';
4
3
  import { HardcopyResponse } from '@envive-ai/react-hooks/contexts/hardcopyContext';
5
4
  import { WidgetTypeV3 } from '@envive-ai/react-hooks/contexts/typesV3';
6
5
  import { UserEvent, UserEventCategory } from '@spiffy-ai/commerce-api-client';
7
6
  import { BaseWidgetProps } from '../types';
8
7
  import { withBaseWidget } from '../withBaseWidget';
9
8
 
10
- // Mock IntersectionObserver
11
- class MockIntersectionObserver implements IntersectionObserver {
12
- observe = vi.fn();
13
-
14
- disconnect = vi.fn();
15
-
16
- unobserve = vi.fn();
17
-
18
- root: Element | null = null;
19
-
20
- rootMargin: string = '';
21
-
22
- thresholds: ReadonlyArray<number> = [];
23
-
24
- private callback: (entries: IntersectionObserverEntry[]) => void;
25
-
26
- private options?: IntersectionObserverInit;
27
-
28
- constructor(
29
- callback: (entries: IntersectionObserverEntry[]) => void,
30
- options?: IntersectionObserverInit,
31
- ) {
32
- this.callback = callback;
33
- this.options = options;
34
- if (options) {
35
- this.rootMargin = options.rootMargin || '';
36
- }
37
- }
38
-
39
- // Helper method to trigger intersection
40
- triggerIntersection(isIntersecting: boolean, target?: Element) {
41
- const entry = {
42
- isIntersecting,
43
- intersectionRatio: isIntersecting ? 1 : 0,
44
- boundingClientRect: {} as DOMRectReadOnly,
45
- rootBounds: {} as DOMRectReadOnly,
46
- target: target || ({} as Element),
47
- time: Date.now(),
48
- } as IntersectionObserverEntry;
49
-
50
- this.callback([entry]);
51
- }
52
-
53
- // Helper method to get options
54
- getOptions() {
55
- return this.options;
56
- }
57
-
58
- // Required by IntersectionObserver interface but not used in tests
59
- // eslint-disable-next-line class-methods-use-this
60
- takeRecords(): IntersectionObserverEntry[] {
61
- return [];
62
- }
63
- }
64
-
65
9
  // Mock the contexts
66
10
  const mockTrackEvent = vi.fn();
67
11
  const mockGetHardcopy = vi.fn();
12
+ const mockUseTrackComponentVisibleEvent = vi.fn();
68
13
 
69
14
  vi.mock('@envive-ai/react-hooks/contexts/amplitudeContext', () => ({
70
15
  useAmplitude: () => ({
@@ -106,6 +51,10 @@ vi.mock('@envive-ai/react-hooks/contexts/uiConfigContext', () => ({
106
51
  }),
107
52
  }));
108
53
 
54
+ vi.mock('@envive-ai/react-hooks/hooks/TrackComponentVisibleEvent', () => ({
55
+ useTrackComponentVisibleEvent: (...args: unknown[]) => mockUseTrackComponentVisibleEvent(...args),
56
+ }));
57
+
109
58
  // Test widget component
110
59
  interface TestWidgetProps extends BaseWidgetProps {
111
60
  testId?: string;
@@ -145,44 +94,18 @@ const TestWidget: React.FC<TestWidgetProps> = ({
145
94
  };
146
95
 
147
96
  describe('withBaseWidget', () => {
148
- let mockObserver: MockIntersectionObserver;
149
- let mockObservers: MockIntersectionObserver[];
150
- let originalIntersectionObserver: typeof IntersectionObserver;
151
-
152
97
  beforeEach(() => {
153
98
  vi.clearAllMocks();
154
- mockObservers = [];
155
99
 
156
100
  // Set default mock for getHardcopy to return a resolved promise
157
101
  mockGetHardcopy.mockResolvedValue({
158
102
  language: 'en',
159
103
  values: {},
160
104
  });
161
-
162
- // Store original IntersectionObserver
163
- originalIntersectionObserver = global.IntersectionObserver;
164
-
165
- // Replace IntersectionObserver with mock class
166
- global.IntersectionObserver = class extends MockIntersectionObserver {
167
- constructor(
168
- callback: (entries: IntersectionObserverEntry[]) => void,
169
- options?: IntersectionObserverInit,
170
- ) {
171
- super(callback, options);
172
- // eslint-disable-next-line @typescript-eslint/no-this-alias
173
- mockObserver = this;
174
- mockObservers.push(this);
175
- }
176
- } as unknown as typeof IntersectionObserver;
177
105
  });
178
106
 
179
- afterEach(() => {
180
- // Restore original IntersectionObserver
181
- global.IntersectionObserver = originalIntersectionObserver;
182
- });
183
-
184
- describe('IntersectionObserver setup', () => {
185
- it('should create an IntersectionObserver with default rootMargin', () => {
107
+ describe('Visibility tracking', () => {
108
+ it('should call useTrackComponentVisibleEvent with default event', () => {
186
109
  const WrappedWidget = withBaseWidget(TestWidget);
187
110
  render(
188
111
  <WrappedWidget
@@ -191,14 +114,19 @@ describe('withBaseWidget', () => {
191
114
  />,
192
115
  );
193
116
 
194
- expect(mockObserver).toBeDefined();
195
- expect(mockObserver.rootMargin).toBe('0px');
117
+ expect(mockUseTrackComponentVisibleEvent).toHaveBeenCalledWith(
118
+ expect.any(Object),
119
+ {
120
+ widget_config_id: 'test-config-1',
121
+ widget_type: WidgetTypeV3.PromptCarouselV3,
122
+ },
123
+ '0px',
124
+ true,
125
+ );
196
126
  });
197
127
 
198
- it('should create an IntersectionObserver with custom rootMargin', () => {
199
- const WrappedWidget = withBaseWidget(TestWidget, {
200
- rootMargin: '100px',
201
- });
128
+ it('should pass custom rootMargin to useTrackComponentVisibleEvent', () => {
129
+ const WrappedWidget = withBaseWidget(TestWidget, { rootMargin: '100px' });
202
130
  render(
203
131
  <WrappedWidget
204
132
  widgetConfigId="test-config-2"
@@ -206,168 +134,42 @@ describe('withBaseWidget', () => {
206
134
  />,
207
135
  );
208
136
 
209
- expect(mockObserver).toBeDefined();
210
- expect(mockObserver.rootMargin).toBe('100px');
211
- });
212
-
213
- it('should observe the widget element', () => {
214
- const WrappedWidget = withBaseWidget(TestWidget);
215
- render(
216
- <WrappedWidget
217
- widgetConfigId="test-config-3"
218
- widgetType={WidgetTypeV3.PromptCarouselV3}
219
- />,
220
- );
221
-
222
- expect(mockObserver.observe).toHaveBeenCalled();
223
- });
224
- });
225
-
226
- describe('Amplitude event tracking', () => {
227
- it('should track default event when widget becomes visible', async () => {
228
- const WrappedWidget = withBaseWidget(TestWidget);
229
- const { container } = render(
230
- <WrappedWidget
231
- widgetConfigId="test-config-4"
232
- widgetType={WidgetTypeV3.PromptCarouselV3}
233
- />,
234
- );
235
-
236
- const widgetElement = container.querySelector('[data-testid="test-widget"]');
237
-
238
- // Trigger intersection with the actual element
239
- mockObserver.triggerIntersection(true, widgetElement as Element);
240
-
241
- await waitFor(() => {
242
- expect(mockTrackEvent).toHaveBeenCalledWith({
243
- eventName: SpiffyMetricsEventName.ChatComponentVisible,
244
- eventProps: {
245
- widget_config_id: 'test-config-4',
246
- widget_type: WidgetTypeV3.PromptCarouselV3,
247
- },
248
- });
249
- });
250
- });
251
-
252
- it('should track custom event when widget becomes visible', async () => {
253
- const WrappedWidget = withBaseWidget(TestWidget, {
254
- visibilityEventName: SpiffyMetricsEventName.SearchComponentVisible,
255
- });
256
- const { container } = render(
257
- <WrappedWidget
258
- widgetConfigId="test-config-5"
259
- widgetType={WidgetTypeV3.ChatPreviewV3}
260
- />,
137
+ expect(mockUseTrackComponentVisibleEvent).toHaveBeenCalledWith(
138
+ expect.any(Object),
139
+ {
140
+ widget_config_id: 'test-config-2',
141
+ widget_type: WidgetTypeV3.ChatPreviewV3,
142
+ },
143
+ '100px',
144
+ true,
261
145
  );
262
-
263
- const widgetElement = container.querySelector('[data-testid="test-widget"]');
264
-
265
- // Trigger intersection
266
- mockObserver.triggerIntersection(true, widgetElement as Element);
267
-
268
- await waitFor(() => {
269
- expect(mockTrackEvent).toHaveBeenCalledWith({
270
- eventName: SpiffyMetricsEventName.SearchComponentVisible,
271
- eventProps: {
272
- widget_config_id: 'test-config-5',
273
- widget_type: WidgetTypeV3.ChatPreviewV3,
274
- },
275
- });
276
- });
277
146
  });
278
147
 
279
- it('should include custom event props when tracking', async () => {
148
+ it('should include custom event props', () => {
280
149
  const WrappedWidget = withBaseWidget(TestWidget, {
281
150
  visibilityEventProps: {
282
151
  custom_prop: 'custom_value',
283
152
  another_prop: 123,
284
153
  },
285
154
  });
286
- const { container } = render(
155
+ render(
287
156
  <WrappedWidget
288
- widgetConfigId="test-config-6"
157
+ widgetConfigId="test-config-4"
289
158
  widgetType={WidgetTypeV3.SocialProofV3}
290
159
  />,
291
160
  );
292
161
 
293
- const widgetElement = container.querySelector('[data-testid="test-widget"]');
294
-
295
- // Trigger intersection
296
- mockObserver.triggerIntersection(true, widgetElement as Element);
297
-
298
- await waitFor(() => {
299
- expect(mockTrackEvent).toHaveBeenCalledWith({
300
- eventName: SpiffyMetricsEventName.ChatComponentVisible,
301
- eventProps: {
302
- widget_config_id: 'test-config-6',
303
- widget_type: WidgetTypeV3.SocialProofV3,
304
- custom_prop: 'custom_value',
305
- another_prop: 123,
306
- },
307
- });
308
- });
309
- });
310
-
311
- it('should only track event once even if intersection happens multiple times', async () => {
312
- const WrappedWidget = withBaseWidget(TestWidget);
313
- const { container } = render(
314
- <WrappedWidget
315
- widgetConfigId="test-config-7"
316
- widgetType={WidgetTypeV3.PromptCarouselV3}
317
- />,
318
- );
319
-
320
- const widgetElement = container.querySelector('[data-testid="test-widget"]');
321
-
322
- // Trigger intersection multiple times
323
- mockObserver.triggerIntersection(true, widgetElement as Element);
324
- mockObserver.triggerIntersection(false, widgetElement as Element);
325
- mockObserver.triggerIntersection(true, widgetElement as Element);
326
-
327
- await waitFor(() => {
328
- expect(mockTrackEvent).toHaveBeenCalledTimes(1);
329
- });
330
- });
331
-
332
- it('should disconnect observer after tracking event', async () => {
333
- const WrappedWidget = withBaseWidget(TestWidget);
334
- const { container } = render(
335
- <WrappedWidget
336
- widgetConfigId="test-config-8"
337
- widgetType={WidgetTypeV3.PromptCarouselV3}
338
- />,
339
- );
340
-
341
- const widgetElement = container.querySelector('[data-testid="test-widget"]');
342
-
343
- // Trigger intersection
344
- mockObserver.triggerIntersection(true, widgetElement as Element);
345
-
346
- await waitFor(() => {
347
- expect(mockObserver.disconnect).toHaveBeenCalled();
348
- });
349
- });
350
-
351
- it('should not track event if widget is not intersecting', async () => {
352
- const WrappedWidget = withBaseWidget(TestWidget);
353
- const { container } = render(
354
- <WrappedWidget
355
- widgetConfigId="test-config-9"
356
- widgetType={WidgetTypeV3.PromptCarouselV3}
357
- />,
162
+ expect(mockUseTrackComponentVisibleEvent).toHaveBeenCalledWith(
163
+ expect.any(Object),
164
+ {
165
+ widget_config_id: 'test-config-4',
166
+ widget_type: WidgetTypeV3.SocialProofV3,
167
+ custom_prop: 'custom_value',
168
+ another_prop: 123,
169
+ },
170
+ '0px',
171
+ true,
358
172
  );
359
-
360
- const widgetElement = container.querySelector('[data-testid="test-widget"]');
361
-
362
- // Trigger non-intersection
363
- mockObserver.triggerIntersection(false, widgetElement as Element);
364
-
365
- // Wait a bit to ensure no event is tracked
366
- await new Promise(resolve => {
367
- setTimeout(resolve, 100);
368
- });
369
-
370
- expect(mockTrackEvent).not.toHaveBeenCalled();
371
173
  });
372
174
  });
373
175
 
@@ -553,7 +355,6 @@ describe('withBaseWidget', () => {
553
355
  mockGetHardcopy.mockResolvedValue(mockHardcopy);
554
356
 
555
357
  const WrappedWidget = withBaseWidget(TestWidget, {
556
- visibilityEventName: SpiffyMetricsEventName.SearchComponentVisible,
557
358
  visibilityEventProps: {
558
359
  test_integration: true,
559
360
  },
@@ -573,20 +374,17 @@ describe('withBaseWidget', () => {
573
374
 
574
375
  const widgetElement = screen.getByTestId('test-widget');
575
376
 
576
- // Trigger intersection to track event
577
- mockObserver.triggerIntersection(true, widgetElement);
578
-
579
- // Verify both hardcopy and event tracking worked
580
- await waitFor(() => {
581
- expect(mockTrackEvent).toHaveBeenCalledWith({
582
- eventName: SpiffyMetricsEventName.SearchComponentVisible,
583
- eventProps: {
584
- widget_config_id: 'test-config-integration',
585
- widget_type: WidgetTypeV3.PromptCarouselV3,
586
- test_integration: true,
587
- },
588
- });
589
- });
377
+ expect(widgetElement).toBeInTheDocument();
378
+ expect(mockUseTrackComponentVisibleEvent).toHaveBeenCalledWith(
379
+ expect.any(Object),
380
+ {
381
+ widget_config_id: 'test-config-integration',
382
+ widget_type: WidgetTypeV3.PromptCarouselV3,
383
+ test_integration: true,
384
+ },
385
+ '0px',
386
+ true,
387
+ );
590
388
 
591
389
  expect(screen.getByTestId('hardcopy-language')).toHaveTextContent('en');
592
390
  expect(screen.getByTestId('hardcopy-values')).toHaveTextContent(
@@ -652,56 +450,24 @@ describe('withBaseWidget', () => {
652
450
  userEvent: expect.any(Object),
653
451
  });
654
452
 
655
- // Verify first widget can trigger intersection and track event
656
- // When multiple widgets are rendered, each creates its own IntersectionObserver
657
- // We need to find the observer that's observing widget-1's element
658
- const widget1Element = screen.getByTestId('widget-1');
659
- const widget2Element = screen.getByTestId('widget-2');
660
-
661
- // Clear any previous calls to isolate this test
662
- mockTrackEvent.mockClear();
663
-
664
- // Find which observer is observing widget-1 by checking which one was called with widget1Element
665
- // In our mock setup, the first observer should be for widget-1, second for widget-2
666
- const widget1Observer = mockObservers[0];
667
- const widget2Observer = mockObservers[1];
668
-
669
- // Verify we have both observers
670
- expect(widget1Observer).toBeDefined();
671
- expect(widget2Observer).toBeDefined();
672
-
673
- // Trigger intersection for widget-1 using its observer
674
- widget1Observer.triggerIntersection(true, widget1Element);
675
-
676
- await waitFor(() => {
677
- // Verify that an event was tracked for widget-1
678
- expect(mockTrackEvent).toHaveBeenCalled();
679
- const { calls } = mockTrackEvent.mock;
680
- // Find any call that matches widget-1's properties
681
- const widget1Call = calls.find(
682
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
683
- (call: any) =>
684
- call[0]?.eventProps?.widget_config_id === 'widget-1' &&
685
- call[0]?.eventProps?.widget_type === WidgetTypeV3.PromptCarouselV3,
686
- );
687
- expect(widget1Call).toBeDefined();
688
- });
689
-
690
- // Verify widget-2 can also track independently
691
- mockTrackEvent.mockClear();
692
- widget2Observer.triggerIntersection(true, widget2Element);
693
-
694
- await waitFor(() => {
695
- expect(mockTrackEvent).toHaveBeenCalled();
696
- const { calls } = mockTrackEvent.mock;
697
- const widget2Call = calls.find(
698
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
699
- (call: any) =>
700
- call[0]?.eventProps?.widget_config_id === 'widget-2' &&
701
- call[0]?.eventProps?.widget_type === WidgetTypeV3.ChatPreviewV3,
702
- );
703
- expect(widget2Call).toBeDefined();
704
- });
453
+ expect(mockUseTrackComponentVisibleEvent).toHaveBeenCalledWith(
454
+ expect.any(Object),
455
+ {
456
+ widget_config_id: 'widget-1',
457
+ widget_type: WidgetTypeV3.PromptCarouselV3,
458
+ },
459
+ '0px',
460
+ true,
461
+ );
462
+ expect(mockUseTrackComponentVisibleEvent).toHaveBeenCalledWith(
463
+ expect.any(Object),
464
+ {
465
+ widget_config_id: 'widget-2',
466
+ widget_type: WidgetTypeV3.ChatPreviewV3,
467
+ },
468
+ '0px',
469
+ true,
470
+ );
705
471
  });
706
472
  });
707
473
  });
@@ -1,5 +1,4 @@
1
1
  import { RefObject } from 'react';
2
- import { SpiffyMetricsEventName } from '@envive-ai/react-hooks/contexts/amplitudeContext';
3
2
  import { HardcopyResponse } from '@envive-ai/react-hooks/contexts/hardcopyContext';
4
3
  import {
5
4
  OrgUIConfigV3,
@@ -22,11 +21,6 @@ export interface BaseWidgetProps {
22
21
  }
23
22
 
24
23
  export interface WithBaseWidgetOptions {
25
- /**
26
- * The Amplitude event name to track when the widget becomes visible.
27
- * Defaults to a generic component visible event if not provided.
28
- */
29
- visibilityEventName?: SpiffyMetricsEventName;
30
24
  /**
31
25
  * Additional properties to include with the visibility event.
32
26
  */
@@ -36,4 +30,10 @@ export interface WithBaseWidgetOptions {
36
30
  * Defaults to '0px'.
37
31
  */
38
32
  rootMargin?: string;
33
+
34
+ /**
35
+ * Whether to defer loading of the widget until it is visible.
36
+ * Defaults to true
37
+ */
38
+ deferLoading?: boolean;
39
39
  }
@@ -1,15 +1,12 @@
1
- import { useEffect, useRef, useState } from 'react';
1
+ import { useEffect, useMemo, useRef, useState } from 'react';
2
2
 
3
- import {
4
- SpiffyMetricsEventName,
5
- useAmplitude,
6
- } from '@envive-ai/react-hooks/contexts/amplitudeContext';
3
+ import { useAmplitude } from '@envive-ai/react-hooks/contexts/amplitudeContext';
4
+ import { useTrackComponentVisibleEvent } from '@envive-ai/react-hooks/hooks/TrackComponentVisibleEvent';
7
5
  import { HardcopyResponse, useHardcopy } from '@envive-ai/react-hooks/contexts/hardcopyContext';
8
6
  import { usePage } from '@envive-ai/react-hooks/contexts/pageContext';
9
7
  import { useWidgetConfig } from '@envive-ai/react-hooks/contexts/widgetConfigContext';
10
8
  import { OrgUIConfigV3, WidgetConfigV3 } from '@envive-ai/react-hooks/contexts/typesV3';
11
9
  import { useUiConfig } from '@envive-ai/react-hooks/contexts/uiConfigContext';
12
-
13
10
  import { BaseWidgetProps, WithBaseWidgetOptions } from './types';
14
11
 
15
12
  export function withBaseWidget<P extends BaseWidgetProps>(
@@ -19,10 +16,10 @@ export function withBaseWidget<P extends BaseWidgetProps>(
19
16
  const WrappedWidget = (props: P) => {
20
17
  const { widgetConfigId, widgetType } = props;
21
18
  const widgetRef = useRef<HTMLDivElement>(null);
22
- const hasTrackedEvent = useRef(false);
23
- const { trackEvent, isReady } = useAmplitude();
19
+ const { isReady } = useAmplitude();
24
20
 
25
21
  const [hardcopyContent, setHardcopyContent] = useState<HardcopyResponse | undefined>(undefined);
22
+ const hardcopyFetchedRef = useRef(false);
26
23
  const { getHardcopy, isLoading: isHardcopyLoading } = useHardcopy();
27
24
 
28
25
  const { userEvent, isLoading: isPageLoading } = usePage();
@@ -36,87 +33,75 @@ export function withBaseWidget<P extends BaseWidgetProps>(
36
33
  const isLoading =
37
34
  isHardcopyLoading || isPageLoading || isWidgetConfigLoading || isUiConfigLoading;
38
35
 
39
- useEffect(() => {
40
- if (!isReady || hasTrackedEvent.current || !widgetRef.current) {
41
- return undefined;
36
+ const visibilityEventProps = {
37
+ widget_config_id: widgetConfigId,
38
+ widget_type: widgetType,
39
+ ...options?.visibilityEventProps,
40
+ };
41
+ const { isVisible } = useTrackComponentVisibleEvent(
42
+ widgetRef,
43
+ visibilityEventProps,
44
+ options?.rootMargin || '0px',
45
+ isReady,
46
+ );
47
+ const startLoading: boolean = useMemo(() => {
48
+ if (options?.deferLoading === false) {
49
+ return true;
42
50
  }
43
-
44
- const observer = new IntersectionObserver(
45
- ([entry]) => {
46
- if (entry.isIntersecting && !hasTrackedEvent.current) {
47
- const eventName =
48
- options?.visibilityEventName || SpiffyMetricsEventName.ChatComponentVisible;
49
-
50
- trackEvent({
51
- eventName,
52
- eventProps: {
53
- widget_config_id: widgetConfigId,
54
- widget_type: widgetType,
55
- ...options?.visibilityEventProps,
56
- },
57
- });
58
-
59
- hasTrackedEvent.current = true;
60
- observer.disconnect();
61
- }
62
- },
63
- {
64
- rootMargin: options?.rootMargin || '0px',
65
- },
66
- );
67
-
68
- observer.observe(widgetRef.current);
69
-
70
- return () => {
71
- observer.disconnect();
72
- };
73
- }, [isReady, widgetConfigId, widgetType, trackEvent]);
51
+ // Defer loading by default until the widget is visible
52
+ return isVisible && !!widgetConfigId && !!widgetType;
53
+ }, [isVisible, widgetConfigId, widgetType]);
74
54
 
75
55
  useEffect(() => {
76
- if (!widgetType || !userEvent) {
56
+ if (!startLoading || hardcopyFetchedRef.current) {
77
57
  return;
78
58
  }
59
+ hardcopyFetchedRef.current = true;
79
60
  getHardcopy({ widgetType, userEvent })
80
61
  .then(setHardcopyContent)
81
62
  .catch(() => {
82
63
  // Silently handle errors - widget will render without hardcopy content
83
64
  });
84
- }, [widgetConfigId, widgetType, getHardcopy, userEvent]);
65
+ }, [startLoading, widgetType, getHardcopy, userEvent, widgetConfigId]);
85
66
 
86
67
  useEffect(() => {
68
+ if (!startLoading) {
69
+ return;
70
+ }
87
71
  getUiConfig()
88
72
  .then(setUiConfig)
89
73
  .catch(() => {
90
74
  // Silently handle errors - widget will render without ui config
91
75
  });
92
- }, [getUiConfig, widgetConfigId, widgetType]);
76
+ }, [startLoading, getUiConfig, widgetConfigId, widgetType]);
93
77
 
94
78
  useEffect(() => {
95
- if (!widgetConfigId || !widgetType) {
79
+ if (!startLoading) {
96
80
  return;
97
81
  }
98
82
 
99
83
  getWidgetConfig({ widgetConfigId, widgetType })
100
84
  .then(setWidgetConfig)
101
- .catch(e => {
102
- console.error('Error getting widget config', widgetConfigId, widgetType, e);
85
+ .catch(() => {
103
86
  // Silently handle errors - widget will render without widget config
104
87
  });
105
- }, [widgetConfigId, widgetType, getWidgetConfig]);
88
+ }, [startLoading, widgetConfigId, widgetType, getWidgetConfig]);
106
89
 
107
90
  return (
108
- <Widget
109
- {...props}
110
- isLoading={isLoading}
111
- isHardcopyLoading={isHardcopyLoading}
112
- isPageLoading={isPageLoading}
113
- isWidgetConfigLoading={isWidgetConfigLoading}
114
- isUiConfigLoading={isUiConfigLoading}
115
- uiConfig={uiConfig}
116
- widgetConfig={widgetConfig}
117
- hardcopyContent={hardcopyContent}
118
- observedWidget={widgetRef}
119
- />
91
+ <span ref={widgetRef}>
92
+ <Widget
93
+ {...props}
94
+ isLoading={isLoading}
95
+ isHardcopyLoading={isHardcopyLoading}
96
+ isPageLoading={isPageLoading}
97
+ isWidgetConfigLoading={isWidgetConfigLoading}
98
+ isUiConfigLoading={isUiConfigLoading}
99
+ uiConfig={uiConfig}
100
+ widgetConfig={widgetConfig}
101
+ hardcopyContent={hardcopyContent}
102
+ observedWidget={widgetRef}
103
+ />
104
+ </span>
120
105
  );
121
106
  };
122
107