@envive-ai/react-widgets-v3 0.3.12 → 0.3.14

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 (91) hide show
  1. package/dist/CXIntegration/implementations/useEightByEightUnifiedCXButton.cjs +54 -0
  2. package/dist/CXIntegration/implementations/useEightByEightUnifiedCXButton.js +53 -0
  3. package/dist/CXIntegration/implementations/useHelpScoutUnifiedCXButton.cjs +65 -0
  4. package/dist/CXIntegration/implementations/useHelpScoutUnifiedCXButton.js +64 -0
  5. package/dist/CXIntegration/implementations/useTalkdeskUnifiedCXButton.cjs +64 -0
  6. package/dist/CXIntegration/implementations/useTalkdeskUnifiedCXButton.js +63 -0
  7. package/dist/CXIntegration/implementations/useZendeskUnifiedCXButton.cjs +2 -2
  8. package/dist/CXIntegration/implementations/useZendeskUnifiedCXButton.js +2 -2
  9. package/dist/CXIntegration/types.cjs +3 -0
  10. package/dist/CXIntegration/types.js +3 -0
  11. package/dist/CXIntegration/utils/functions.cjs +6 -0
  12. package/dist/CXIntegration/utils/functions.js +6 -0
  13. package/dist/hocs/withBaseWidget/withBaseWidget.d.cts +2 -2
  14. package/dist/packages/widgets/dist/SearchResults/SearchResults.d.ts +2 -2
  15. package/dist/packages/widgets/dist/SearchResults/SearchResultsWidget.d.ts +2 -2
  16. package/dist/packages/widgets/dist/SearchZeroState/SearchZeroStateWidget.d.ts +2 -2
  17. package/dist/packages/widgets/dist/SuggestionBar/SuggestionBar.d.ts +2 -2
  18. package/dist/widgets/ChatPreviewComparisonWidget/ChatPreviewComparisonWidget.d.cts +3 -3
  19. package/dist/widgets/ChatPreviewComparisonWidget/ChatPreviewComparisonWidget.d.ts +3 -3
  20. package/dist/widgets/ChatPreviewLoadingWidget/ChatPreviewLoadingWidget.d.cts +3 -3
  21. package/dist/widgets/ChatPreviewWidget/ChatPreviewWidget.d.cts +3 -3
  22. package/dist/widgets/ChatPreviewWidget/ChatPreviewWidget.d.ts +3 -3
  23. package/dist/widgets/FloatingChatWidget/FloatingChatWidget.d.cts +2 -2
  24. package/dist/widgets/FloatingChatWidget/FloatingChatWidget.d.ts +2 -2
  25. package/dist/widgets/FullPageSalesAgentWidget/FullPageSalesAgentWidget.d.cts +2 -2
  26. package/dist/widgets/FullPageSalesAgentWidget/FullPageSalesAgentWidget.d.ts +2 -2
  27. package/dist/widgets/ProductCardWidget/ProductCardWidget.cjs +19 -3
  28. package/dist/widgets/ProductCardWidget/ProductCardWidget.d.cts +2 -2
  29. package/dist/widgets/ProductCardWidget/ProductCardWidget.d.ts +2 -2
  30. package/dist/widgets/ProductCardWidget/ProductCardWidget.js +20 -4
  31. package/dist/widgets/PromptButtonCarouselWithImageWidget/PromptButtonCarouselWithImageWidget.cjs +10 -0
  32. package/dist/widgets/PromptButtonCarouselWithImageWidget/PromptButtonCarouselWithImageWidget.d.cts +3 -3
  33. package/dist/widgets/PromptButtonCarouselWithImageWidget/PromptButtonCarouselWithImageWidget.d.ts +3 -3
  34. package/dist/widgets/PromptButtonCarouselWithImageWidget/PromptButtonCarouselWithImageWidget.js +12 -2
  35. package/dist/widgets/PromptCarouselWidget/PromptCarouselWidget.cjs +2 -9
  36. package/dist/widgets/PromptCarouselWidget/PromptCarouselWidget.d.cts +2 -2
  37. package/dist/widgets/PromptCarouselWidget/PromptCarouselWidget.d.ts +2 -2
  38. package/dist/widgets/PromptCarouselWidget/PromptCarouselWidget.js +1 -8
  39. package/dist/widgets/SocialProofFlowWidget/SocialProofFlowWidget.cjs +8 -8
  40. package/dist/widgets/SocialProofFlowWidget/SocialProofFlowWidget.d.cts +2 -2
  41. package/dist/widgets/SocialProofFlowWidget/SocialProofFlowWidget.d.ts +2 -2
  42. package/dist/widgets/SocialProofFlowWidget/SocialProofFlowWidget.js +8 -8
  43. package/dist/widgets/SocialProofWidget/SocialProofWidget.cjs +10 -0
  44. package/dist/widgets/SocialProofWidget/SocialProofWidget.d.cts +3 -3
  45. package/dist/widgets/SocialProofWidget/SocialProofWidget.d.ts +3 -3
  46. package/dist/widgets/SocialProofWidget/SocialProofWidget.js +12 -2
  47. package/dist/widgets/TitledPromptCarouselWidget/TitledPromptCarouselWidget.cjs +11 -0
  48. package/dist/widgets/TitledPromptCarouselWidget/TitledPromptCarouselWidget.d.cts +2 -2
  49. package/dist/widgets/TitledPromptCarouselWidget/TitledPromptCarouselWidget.d.ts +2 -2
  50. package/dist/widgets/TitledPromptCarouselWidget/TitledPromptCarouselWidget.js +12 -1
  51. package/dist/widgets/TypingAnimationFlowWidget/TypingAnimationFlowWidget.cjs +8 -8
  52. package/dist/widgets/TypingAnimationFlowWidget/TypingAnimationFlowWidget.d.cts +2 -2
  53. package/dist/widgets/TypingAnimationFlowWidget/TypingAnimationFlowWidget.d.ts +2 -2
  54. package/dist/widgets/TypingAnimationFlowWidget/TypingAnimationFlowWidget.js +8 -8
  55. package/dist/widgets/TypingAnimationWidget/TypingAnimationWidget.cjs +23 -3
  56. package/dist/widgets/TypingAnimationWidget/TypingAnimationWidget.d.cts +3 -3
  57. package/dist/widgets/TypingAnimationWidget/TypingAnimationWidget.d.ts +3 -3
  58. package/dist/widgets/TypingAnimationWidget/TypingAnimationWidget.js +24 -4
  59. package/dist/widgets/dist/SearchResults/SearchResults.d.cts +2 -2
  60. package/dist/widgets/dist/SearchResults/SearchResultsWidget.d.cts +2 -2
  61. package/dist/widgets/dist/SearchZeroState/SearchZeroStateWidget.d.cts +2 -2
  62. package/dist/widgets/dist/SuggestionBar/SuggestionBar.d.cts +2 -2
  63. package/dist/widgets/utils/functions.cjs +9 -0
  64. package/dist/widgets/utils/functions.js +9 -1
  65. package/package.json +1 -1
  66. package/src/CXIntegration/implementations/useEightByEightUnifiedCXButton.ts +91 -0
  67. package/src/CXIntegration/implementations/useHelpScoutUnifiedCXButton.ts +108 -0
  68. package/src/CXIntegration/implementations/useTalkdeskUnifiedCXButton.ts +94 -0
  69. package/src/CXIntegration/implementations/useZendeskUnifiedCXButton.ts +4 -2
  70. package/src/CXIntegration/types.ts +3 -0
  71. package/src/CXIntegration/utils/functions.ts +12 -0
  72. package/src/hocs/withBaseWidget/__tests__/withBaseWidget.test.tsx +15 -3
  73. package/src/widgets/ChatPreviewWidget/__tests__/ChatPreviewWidget.test.tsx +114 -0
  74. package/src/widgets/FloatingChatWidget/__tests__/FloatingChatWidget.test.tsx +119 -0
  75. package/src/widgets/ProductCardWidget/ProductCardWidget.tsx +15 -3
  76. package/src/widgets/ProductCardWidget/__tests__/ProductCardWidget.test.tsx +144 -0
  77. package/src/widgets/PromptButtonCarouselWithImageWidget/PromptButtonCarouselWithImageWidget.tsx +12 -1
  78. package/src/widgets/PromptButtonCarouselWithImageWidget/__tests__/PromptButtonCarouselWithImageWidget.test.tsx +179 -0
  79. package/src/widgets/PromptCarouselWidget/PromptCarouselWidget.tsx +1 -14
  80. package/src/widgets/PromptCarouselWidget/__tests__/PromptCarouselWidget.test.tsx +150 -0
  81. package/src/widgets/SocialProofFlowWidget/SocialProofFlowWidget.tsx +12 -12
  82. package/src/widgets/SocialProofWidget/SocialProofWidget.tsx +12 -1
  83. package/src/widgets/SocialProofWidget/__tests__/SocialProofWidget.test.tsx +184 -0
  84. package/src/widgets/TitledPromptCarouselWidget/TitledPromptCarouselWidget.tsx +12 -0
  85. package/src/widgets/TitledPromptCarouselWidget/__tests__/TitledPromptCarouselWidget.test.tsx +150 -0
  86. package/src/widgets/TypingAnimationFlowWidget/TypingAnimationFlowWidget.tsx +12 -12
  87. package/src/widgets/TypingAnimationWidget/TypingAnimationWidget.tsx +19 -2
  88. package/src/widgets/TypingAnimationWidget/__tests__/TypingAnimationWidget.test.tsx +163 -0
  89. package/src/widgets/__tests__/testUtils.tsx +63 -0
  90. package/src/widgets/__tests__/trackEventCanary.test.ts +45 -0
  91. package/src/widgets/utils/functions.ts +16 -0
@@ -0,0 +1,94 @@
1
+ import { SelectorFactory } from '@envive-ai/react-hooks/application/utils';
2
+ import { useElementObserver } from '@envive-ai/react-hooks/hooks/ElementObserver';
3
+ import { FLOATING_BUTTON_ID } from '../../widgets/FloatingChatWidget/constants';
4
+ import { CustomerServiceImplProps, UnifiedCXButton } from '../types';
5
+
6
+ interface UseTalkdeskUnifiedCXButtonProps extends CustomerServiceImplProps {}
7
+
8
+ export const useTalkdeskUnifiedCXButton = ({
9
+ onSwitchToAgent,
10
+ suppressMerchantButton,
11
+ }: UseTalkdeskUnifiedCXButtonProps): UnifiedCXButton => {
12
+ const talkdeskButton = useElementObserver(SelectorFactory.id('talkdesk-chat-widget-trigger'));
13
+ const talkdeskWidget = useElementObserver(SelectorFactory.id('talkdesk-chat-widget'));
14
+ const talkdeskContainer = useElementObserver(SelectorFactory.id('tdWebchat'));
15
+ const enviveFloatingButton = useElementObserver(SelectorFactory.id(FLOATING_BUTTON_ID));
16
+
17
+ const toggle = () => {
18
+ onSwitchToAgent();
19
+ if (suppressMerchantButton) {
20
+ enviveFloatingButton.hide();
21
+ }
22
+ talkdeskButton.show();
23
+ talkdeskButton.fire('click');
24
+ };
25
+
26
+ const isTalkdeskButtonEnabled = () => {
27
+ // Check for the main Talkdesk container
28
+ const talkdeskMainContainer = document.getElementById('tdWebchat');
29
+ if (!talkdeskMainContainer) return false;
30
+
31
+ // Check for the trigger button (it's directly in the DOM, not inside an iframe)
32
+ const talkdeskTriggerButton = document.getElementById('talkdesk-chat-widget-trigger');
33
+ return !!talkdeskTriggerButton;
34
+ };
35
+
36
+ const isSwitchEnabled = () => isTalkdeskButtonEnabled();
37
+
38
+ // Hide Talkdesk button when it's added to DOM (if suppressMerchantButton is enabled)
39
+ talkdeskButton.onAdd(() => {
40
+ if (suppressMerchantButton) {
41
+ talkdeskButton.hide();
42
+ enviveFloatingButton.show();
43
+ }
44
+ });
45
+
46
+ // Observe the widget container (NOT the button) to avoid infinite loops
47
+ talkdeskWidget.onAdd(el => {
48
+ if (el && suppressMerchantButton) {
49
+ const ariaHidden = el.getAttribute('aria-hidden');
50
+
51
+ // Widget is open initially (aria-hidden="false")
52
+ if (ariaHidden === 'false') {
53
+ talkdeskButton.show();
54
+ enviveFloatingButton.hide();
55
+ } else {
56
+ // Widget is closed initially
57
+ talkdeskButton.hide();
58
+ enviveFloatingButton.show();
59
+ }
60
+ }
61
+ });
62
+
63
+ // Track widget visibility changes
64
+ talkdeskWidget.onChange(el => {
65
+ if (el && suppressMerchantButton) {
66
+ const ariaHidden = el.getAttribute('aria-hidden');
67
+
68
+ // Widget is open (aria-hidden="false")
69
+ if (ariaHidden === 'false') {
70
+ talkdeskButton.show();
71
+ enviveFloatingButton.hide();
72
+ }
73
+
74
+ // Widget is closed (aria-hidden="true")
75
+ if (ariaHidden === 'true') {
76
+ talkdeskButton.hide();
77
+ enviveFloatingButton.show();
78
+ }
79
+ }
80
+ });
81
+
82
+ // Cleanup when container is removed
83
+ talkdeskContainer.onRemove(() => {
84
+ if (suppressMerchantButton) {
85
+ talkdeskButton.hide();
86
+ enviveFloatingButton.show();
87
+ }
88
+ });
89
+
90
+ return {
91
+ toggle,
92
+ isSwitchEnabled,
93
+ };
94
+ };
@@ -10,10 +10,12 @@ export const useZendeskUnifiedCXButton = ({
10
10
  onCXClose,
11
11
  suppressMerchantButton,
12
12
  }: UseZendeskUnifiedCXButtonProps): UnifiedCXButton => {
13
- const zendeskButton = useElementObserver(SelectorFactory.chain('id|launcher @ query|button'));
13
+ const zendeskButton = useElementObserver(
14
+ SelectorFactory.chain('query|iframe#launcher @ query|button'),
15
+ );
14
16
  const zendeskAlternativeIframe = useElementObserver(SelectorFactory.chain('id|webWidget'));
15
17
  const enviveFloatingButton = useElementObserver(SelectorFactory.id(FLOATING_BUTTON_ID));
16
- const zendeskIframe = useElementObserver(SelectorFactory.id('launcher'));
18
+ const zendeskIframe = useElementObserver(SelectorFactory.query('iframe#launcher'));
17
19
 
18
20
  const toggle = () => {
19
21
  zendeskIframe.hide();
@@ -9,6 +9,9 @@ export enum CustomerServiceType {
9
9
  gladly = 'gladly',
10
10
  richpanel = 'richpanel',
11
11
  zendesk = 'zendesk',
12
+ helpscout = 'helpscout',
13
+ talkdesk = 'talkdesk',
14
+ eightByEight = '8x8',
12
15
  unsupported = 'unsupported',
13
16
  }
14
17
 
@@ -10,6 +10,9 @@ import { useReDoUnifiedCXButton } from '../implementations/useReDoUnifiedCXButto
10
10
  import { useRichpanelUnifiedCXButton } from '../implementations/useRichpanelUnifiedCXButton';
11
11
  import { useZendeskUnifiedCXButton } from '../implementations/useZendeskUnifiedCXButton';
12
12
  import { useKustomerUnifiedCXButton } from '../implementations/useKustomerUnifiedCXButton';
13
+ import { useHelpScoutUnifiedCXButton } from '../implementations/useHelpScoutUnifiedCXButton';
14
+ import { useTalkdeskUnifiedCXButton } from '../implementations/useTalkdeskUnifiedCXButton';
15
+ import { useEightByEightUnifiedCXButton } from '../implementations/useEightByEightUnifiedCXButton';
13
16
  import { useDefaultUnifiedCXButton } from '../implementations/useDefaultUnifiedCXButton';
14
17
 
15
18
  export const findCustomerServiceImpl = (
@@ -45,6 +48,15 @@ export const findCustomerServiceImpl = (
45
48
  if (provider === CustomerServiceType.zendesk) {
46
49
  return useZendeskUnifiedCXButton;
47
50
  }
51
+ if (provider === CustomerServiceType.helpscout) {
52
+ return useHelpScoutUnifiedCXButton;
53
+ }
54
+ if (provider === CustomerServiceType.talkdesk) {
55
+ return useTalkdeskUnifiedCXButton;
56
+ }
57
+ if (provider === CustomerServiceType.eightByEight) {
58
+ return useEightByEightUnifiedCXButton;
59
+ }
48
60
 
49
61
  return useDefaultUnifiedCXButton;
50
62
  };
@@ -66,7 +66,7 @@ class MockIntersectionObserver implements IntersectionObserver {
66
66
  const mockTrackEvent = vi.fn();
67
67
  const mockGetHardcopy = vi.fn();
68
68
 
69
- vi.mock('src/contexts/amplitudeContext/amplitudeContext', () => ({
69
+ vi.mock('@envive-ai/react-hooks/contexts/amplitudeContext', () => ({
70
70
  useAmplitude: () => ({
71
71
  trackEvent: mockTrackEvent,
72
72
  isReady: true,
@@ -77,13 +77,13 @@ vi.mock('src/contexts/amplitudeContext/amplitudeContext', () => ({
77
77
  },
78
78
  }));
79
79
 
80
- vi.mock('src/contexts/hardcopyContext', () => ({
80
+ vi.mock('@envive-ai/react-hooks/contexts/hardcopyContext', () => ({
81
81
  useHardcopy: () => ({
82
82
  getHardcopy: mockGetHardcopy,
83
83
  }),
84
84
  }));
85
85
 
86
- vi.mock('src/contexts/pageContext', () => ({
86
+ vi.mock('@envive-ai/react-hooks/contexts/pageContext', () => ({
87
87
  usePage: () => ({
88
88
  userEvent: {
89
89
  id: 'test-user-event-id',
@@ -94,6 +94,18 @@ vi.mock('src/contexts/pageContext', () => ({
94
94
  }),
95
95
  }));
96
96
 
97
+ vi.mock('@envive-ai/react-hooks/contexts/widgetConfigContext', () => ({
98
+ useWidgetConfig: () => ({
99
+ getWidgetConfig: vi.fn().mockResolvedValue({}),
100
+ }),
101
+ }));
102
+
103
+ vi.mock('@envive-ai/react-hooks/contexts/uiConfigContext', () => ({
104
+ useUiConfig: () => ({
105
+ getUiConfig: vi.fn().mockResolvedValue({}),
106
+ }),
107
+ }));
108
+
97
109
  // Test widget component
98
110
  interface TestWidgetProps extends BaseWidgetProps {
99
111
  testId?: string;
@@ -0,0 +1,114 @@
1
+ import { render, waitFor } from '@testing-library/react';
2
+ import { SpiffyMetricsEventName } from '@envive-ai/react-hooks/contexts/amplitudeContext';
3
+ import { WidgetTypeV3 } from '@envive-ai/react-hooks/contexts/typesV3';
4
+ import { UserEvent, UserEventCategory } from '@spiffy-ai/commerce-api-client';
5
+ import { ChatPreviewWidget } from '../ChatPreviewWidget';
6
+
7
+ const mockTrackEvent = vi.fn();
8
+ const mockGetHardcopy = vi.fn();
9
+
10
+ vi.mock('@envive-ai/react-toolkit-v3/ChatPreview', () => ({
11
+ ChatPreview: () => <div data-testid="chat-preview-mock">Chat Preview</div>,
12
+ }));
13
+
14
+ vi.mock('@envive-ai/react-hooks/contexts/amplitudeContext', () => ({
15
+ useAmplitude: () => ({
16
+ trackEvent: mockTrackEvent,
17
+ isReady: true,
18
+ }),
19
+ SpiffyMetricsEventName: {
20
+ ChatComponentVisible: 'Chat Component Visible',
21
+ SearchComponentVisible: 'Search Component Visible',
22
+ },
23
+ }));
24
+
25
+ vi.mock('@envive-ai/react-hooks/contexts/hardcopyContext', () => ({
26
+ useHardcopy: () => ({
27
+ getHardcopy: mockGetHardcopy,
28
+ }),
29
+ }));
30
+
31
+ vi.mock('@envive-ai/react-hooks/contexts/pageContext', () => ({
32
+ usePage: () => ({
33
+ userEvent: {
34
+ id: 'test-user-event-id',
35
+ category: UserEventCategory.PageVisit,
36
+ created_at: '2025-01-01T00:00:00Z',
37
+ event_id: 'test-event-id',
38
+ } as UserEvent,
39
+ }),
40
+ }));
41
+
42
+ vi.mock('@envive-ai/react-hooks/contexts/widgetConfigContext', () => ({
43
+ useWidgetConfig: () => ({
44
+ getWidgetConfig: vi.fn().mockResolvedValue({}),
45
+ }),
46
+ }));
47
+
48
+ vi.mock('@envive-ai/react-hooks/contexts/uiConfigContext', () => ({
49
+ useUiConfig: () => ({
50
+ getUiConfig: vi.fn().mockResolvedValue({}),
51
+ }),
52
+ }));
53
+
54
+ vi.mock('@envive-ai/react-hooks/contexts/salesAgentContext', () => ({
55
+ useSalesAgent: () => ({
56
+ onSuggestionClicked: vi.fn(),
57
+ }),
58
+ }));
59
+
60
+ vi.mock('@envive-ai/react-hooks/hooks/ChatToggle', () => ({
61
+ useChatToggle: () => ({
62
+ openChat: vi.fn(),
63
+ }),
64
+ }));
65
+
66
+ vi.mock('@envive-ai/react-hooks/atoms/chat', () => ({
67
+ chatAtom: {},
68
+ lastAssistantMessageAtom: {},
69
+ }));
70
+
71
+ vi.mock('jotai', async importOriginal => {
72
+ const actual = await importOriginal();
73
+ return {
74
+ ...actual,
75
+ useAtomValue: vi
76
+ .fn()
77
+ .mockReturnValueOnce([])
78
+ .mockReturnValueOnce({ messages: [], suggestions: [] })
79
+ .mockReturnValue([]),
80
+ };
81
+ });
82
+
83
+ describe('ChatPreviewWidget analytics', () => {
84
+ const defaultHardcopy = {
85
+ responseId: 'test-response-id',
86
+ language: 'en',
87
+ values: {
88
+ titleLabel: 'Chat Preview',
89
+ textFieldPlaceholderText: 'Type here',
90
+ },
91
+ };
92
+
93
+ beforeEach(() => {
94
+ vi.clearAllMocks();
95
+ mockGetHardcopy.mockResolvedValue(defaultHardcopy);
96
+ });
97
+
98
+ it('should track ChatComponentVisible when widget mounts', async () => {
99
+ render(<ChatPreviewWidget widgetConfigId="test-config-1" />);
100
+
101
+ await waitFor(
102
+ () => {
103
+ expect(mockTrackEvent).toHaveBeenCalledWith({
104
+ eventName: SpiffyMetricsEventName.ChatComponentVisible,
105
+ eventProps: {
106
+ widget_config_id: 'test-config-1',
107
+ widget_type: WidgetTypeV3.ChatPreviewV3,
108
+ },
109
+ });
110
+ },
111
+ { timeout: 3000 },
112
+ );
113
+ }, 5000);
114
+ });
@@ -0,0 +1,119 @@
1
+ import { render, waitFor } from '@testing-library/react';
2
+ import { SpiffyMetricsEventName } from '@envive-ai/react-hooks/contexts/amplitudeContext';
3
+ import { WidgetTypeV3 } from '@envive-ai/react-hooks/contexts/typesV3';
4
+ import { UserEvent, UserEventCategory } from '@spiffy-ai/commerce-api-client';
5
+ import { FloatingChatWidget } from '../FloatingChatWidget';
6
+
7
+ const mockTrackEvent = vi.fn();
8
+ const mockGetHardcopy = vi.fn();
9
+
10
+ vi.mock('@envive-ai/react-toolkit-v3/FloatingButton', () => ({
11
+ FloatingButton: () => <div data-testid="floating-button">Floating Button</div>,
12
+ }));
13
+
14
+ vi.mock('@envive-ai/react-hooks/contexts/amplitudeContext', () => ({
15
+ useAmplitude: () => ({
16
+ trackEvent: mockTrackEvent,
17
+ isReady: true,
18
+ }),
19
+ SpiffyMetricsEventName: {
20
+ ChatComponentVisible: 'Chat Component Visible',
21
+ SearchComponentVisible: 'Search Component Visible',
22
+ },
23
+ }));
24
+
25
+ vi.mock('@envive-ai/react-hooks/contexts/hardcopyContext', () => ({
26
+ useHardcopy: () => ({
27
+ getHardcopy: mockGetHardcopy,
28
+ }),
29
+ }));
30
+
31
+ vi.mock('@envive-ai/react-hooks/contexts/pageContext', () => ({
32
+ usePage: () => ({
33
+ userEvent: {
34
+ id: 'test-user-event-id',
35
+ category: UserEventCategory.PageVisit,
36
+ created_at: '2025-01-01T00:00:00Z',
37
+ event_id: 'test-event-id',
38
+ } as UserEvent,
39
+ }),
40
+ }));
41
+
42
+ vi.mock('@envive-ai/react-hooks/contexts/widgetConfigContext', () => ({
43
+ useWidgetConfig: () => ({
44
+ getWidgetConfig: vi.fn().mockResolvedValue({}),
45
+ }),
46
+ }));
47
+
48
+ vi.mock('@envive-ai/react-hooks/contexts/uiConfigContext', () => ({
49
+ useUiConfig: () => ({
50
+ getUiConfig: vi.fn().mockResolvedValue({
51
+ floatingButton: { showOption: 'always', position: 'bottom-right' },
52
+ floatingChat: {},
53
+ lookAndFeel: {},
54
+ }),
55
+ }),
56
+ }));
57
+
58
+ vi.mock('@envive-ai/react-hooks/contexts/salesAgentContext', () => ({
59
+ useSalesAgent: () => ({}),
60
+ }));
61
+
62
+ vi.mock('@envive-ai/react-hooks/hooks/ChatToggle', () => ({
63
+ useChatToggle: () => ({
64
+ isOpen: false,
65
+ openChat: vi.fn(),
66
+ closeChat: vi.fn(),
67
+ }),
68
+ }));
69
+
70
+ vi.mock('../hooks/useFloatingButtonVisibility', () => ({
71
+ useFloatingButtonVisibility: () => ({
72
+ shouldShowFloatingButton: true,
73
+ }),
74
+ }));
75
+
76
+ vi.mock('../hooks/useAutoPopup', () => ({
77
+ useAutoPopup: () => {},
78
+ }));
79
+
80
+ vi.mock('../../hooks/useGetWidgetStatus', () => ({
81
+ default: () => ({
82
+ userHasInteractedValue: false,
83
+ }),
84
+ }));
85
+
86
+ vi.mock('src/debug/debugBar', () => ({
87
+ DebugBar: () => null,
88
+ }));
89
+
90
+ vi.mock('../../CXIntegration/hooks/useUnifiedCXButton', () => ({
91
+ useUnifiedCXButton: () => ({
92
+ isSwitchEnabled: () => false,
93
+ toggle: vi.fn(),
94
+ }),
95
+ }));
96
+
97
+ describe('FloatingChatWidget analytics', () => {
98
+ beforeEach(() => {
99
+ vi.clearAllMocks();
100
+ mockGetHardcopy.mockResolvedValue({ language: 'en', values: {} });
101
+ });
102
+
103
+ it('should track ChatComponentVisible when floating button is shown', async () => {
104
+ render(<FloatingChatWidget previewButtonOnly />);
105
+
106
+ await waitFor(
107
+ () => {
108
+ expect(mockTrackEvent).toHaveBeenCalledWith({
109
+ eventName: SpiffyMetricsEventName.ChatComponentVisible,
110
+ eventProps: {
111
+ widget_config_id: 'floating-button',
112
+ widget_type: WidgetTypeV3.FloatingButtonV3,
113
+ },
114
+ });
115
+ },
116
+ { timeout: 3000 },
117
+ );
118
+ }, 5000);
119
+ });
@@ -2,6 +2,7 @@ import { ProductCardWidgetV3Config, WidgetTypeV3 } from '@envive-ai/react-hooks/
2
2
  import { PromptButtonVariant } from '@envive-ai/react-toolkit-v3/PromptButton/types';
3
3
  import { useCallback, useEffect } from 'react';
4
4
  import {
5
+ EnviveMetricsEventName,
5
6
  SpiffyMetricsEventName,
6
7
  useAmplitude,
7
8
  } from '@envive-ai/react-hooks/contexts/amplitudeContext';
@@ -11,6 +12,7 @@ import { useChatToggle } from '@envive-ai/react-hooks/hooks/ChatToggle';
11
12
  import { ProductCard } from '@envive-ai/react-toolkit-v3/ProductCard';
12
13
  import { Theme } from '@envive-ai/react-toolkit-v3/Tokens';
13
14
  import { BaseWidgetProps, withBaseWidget } from '../../hocs/withBaseWidget';
15
+ import { RawValues, getStringIdForText } from '../utils/functions';
14
16
 
15
17
  const mockPrompts = [
16
18
  'Loading prompt 1',
@@ -63,15 +65,25 @@ const ProductCardWidgetHandler = (props: BaseWidgetProps) => {
63
65
  }, [trackEvent, widgetConfigId]);
64
66
 
65
67
  const handleSelect = useCallback(
66
- (prompt: string) => {
68
+ (text: string) => {
69
+ const rawValues = (hardcopyContent as { rawValues?: RawValues } | undefined)?.rawValues;
70
+ const stringId = getStringIdForText(rawValues, text);
71
+ trackEvent({
72
+ eventName: EnviveMetricsEventName.WidgetTextClicked,
73
+ eventProps: {
74
+ response_id: hardcopyContent?.responseId,
75
+ string_id: stringId,
76
+ text,
77
+ },
78
+ });
67
79
  onTypedMessageSubmitted({
68
- query: prompt,
80
+ query: text,
69
81
  userTyped: false,
70
82
  displayLocation: ChatElementDisplayLocationV3.PRODUCT_CARD_PROMPT_BUTTON,
71
83
  });
72
84
  openChat(ChatElementDisplayLocationV3.PRODUCT_CARD_PROMPT_BUTTON);
73
85
  },
74
- [onTypedMessageSubmitted, openChat],
86
+ [hardcopyContent, onTypedMessageSubmitted, openChat, trackEvent],
75
87
  );
76
88
 
77
89
  const handleInputClick = useCallback(() => {
@@ -0,0 +1,144 @@
1
+ import { render, screen, waitFor } from '@testing-library/react';
2
+ import {
3
+ EnviveMetricsEventName,
4
+ SpiffyMetricsEventName,
5
+ } from '@envive-ai/react-hooks/contexts/amplitudeContext';
6
+ import { WidgetTypeV3 } from '@envive-ai/react-hooks/contexts/typesV3';
7
+ import { UserEvent, UserEventCategory } from '@spiffy-ai/commerce-api-client';
8
+ import { ProductCardWidget } from '../ProductCardWidget';
9
+
10
+ const mockTrackEvent = vi.fn();
11
+ const mockGetHardcopy = vi.fn();
12
+
13
+ vi.mock('@envive-ai/react-toolkit-v3/ProductCard', () => ({
14
+ ProductCard: ({ prompts, onSelect }: { prompts: string[]; onSelect: (text: string) => void }) => (
15
+ <div data-testid="product-card-mock">
16
+ {prompts.map(text => (
17
+ <button
18
+ key={text}
19
+ type="button"
20
+ onClick={() => onSelect(text)}
21
+ >
22
+ {text}
23
+ </button>
24
+ ))}
25
+ </div>
26
+ ),
27
+ }));
28
+
29
+ vi.mock('@envive-ai/react-hooks/contexts/amplitudeContext', () => ({
30
+ useAmplitude: () => ({
31
+ trackEvent: mockTrackEvent,
32
+ isReady: true,
33
+ }),
34
+ SpiffyMetricsEventName: {
35
+ ChatComponentVisible: 'Chat Component Visible',
36
+ SearchComponentVisible: 'Search Component Visible',
37
+ },
38
+ EnviveMetricsEventName: {
39
+ WidgetTextClicked: 'Widget Text Clicked',
40
+ },
41
+ }));
42
+
43
+ vi.mock('@envive-ai/react-hooks/contexts/hardcopyContext', () => ({
44
+ useHardcopy: () => ({
45
+ getHardcopy: mockGetHardcopy,
46
+ }),
47
+ }));
48
+
49
+ vi.mock('@envive-ai/react-hooks/contexts/pageContext', () => ({
50
+ usePage: () => ({
51
+ userEvent: {
52
+ id: 'test-user-event-id',
53
+ category: UserEventCategory.PageVisit,
54
+ created_at: '2025-01-01T00:00:00Z',
55
+ event_id: 'test-event-id',
56
+ } as UserEvent,
57
+ }),
58
+ }));
59
+
60
+ vi.mock('@envive-ai/react-hooks/contexts/widgetConfigContext', () => ({
61
+ useWidgetConfig: () => ({
62
+ getWidgetConfig: vi.fn().mockResolvedValue({}),
63
+ }),
64
+ }));
65
+
66
+ vi.mock('@envive-ai/react-hooks/contexts/uiConfigContext', () => ({
67
+ useUiConfig: () => ({
68
+ getUiConfig: vi.fn().mockResolvedValue({}),
69
+ }),
70
+ }));
71
+
72
+ vi.mock('@envive-ai/react-hooks/contexts/salesAgentContext', () => ({
73
+ useSalesAgent: () => ({
74
+ onTypedMessageSubmitted: vi.fn(),
75
+ }),
76
+ }));
77
+
78
+ vi.mock('@envive-ai/react-hooks/hooks/ChatToggle', () => ({
79
+ useChatToggle: () => ({
80
+ openChat: vi.fn(),
81
+ }),
82
+ }));
83
+
84
+ describe('ProductCardWidget analytics', () => {
85
+ const defaultHardcopy = {
86
+ responseId: 'test-response-id',
87
+ language: 'en',
88
+ values: {
89
+ prompts: ['Prompt 1', 'Prompt 2', 'Prompt 3'],
90
+ },
91
+ rawValues: {
92
+ prompts: [
93
+ { id: 'id-1', value: 'Prompt 1' },
94
+ { id: 'id-2', value: 'Prompt 2' },
95
+ ],
96
+ },
97
+ };
98
+
99
+ beforeEach(() => {
100
+ vi.clearAllMocks();
101
+ mockGetHardcopy.mockResolvedValue(defaultHardcopy);
102
+ });
103
+
104
+ it('should track ChatComponentVisible when widget mounts', async () => {
105
+ render(<ProductCardWidget widgetConfigId="test-config-1" />);
106
+
107
+ await waitFor(
108
+ () => {
109
+ expect(mockTrackEvent).toHaveBeenCalledWith({
110
+ eventName: SpiffyMetricsEventName.ChatComponentVisible,
111
+ eventProps: {
112
+ widget_config_id: 'test-config-1',
113
+ widget_type: WidgetTypeV3.ProductCardV3,
114
+ },
115
+ });
116
+ },
117
+ { timeout: 3000 },
118
+ );
119
+ }, 5000);
120
+
121
+ it('should track WidgetTextClicked when prompt is selected', async () => {
122
+ render(<ProductCardWidget widgetConfigId="test-config-2" />);
123
+
124
+ await waitFor(
125
+ () => {
126
+ expect(screen.getByText('Prompt 1')).toBeInTheDocument();
127
+ },
128
+ { timeout: 3000 },
129
+ );
130
+
131
+ mockTrackEvent.mockClear();
132
+
133
+ screen.getByText('Prompt 1').click();
134
+
135
+ expect(mockTrackEvent).toHaveBeenCalledWith({
136
+ eventName: EnviveMetricsEventName.WidgetTextClicked,
137
+ eventProps: {
138
+ response_id: 'test-response-id',
139
+ string_id: 'id-1',
140
+ text: 'Prompt 1',
141
+ },
142
+ });
143
+ }, 5000);
144
+ });
@@ -8,6 +8,7 @@ import { useChatToggle } from '@envive-ai/react-hooks/hooks/ChatToggle';
8
8
  import { Theme } from '@envive-ai/react-toolkit-v3/Tokens';
9
9
  import { useCallback, useEffect, useMemo } from 'react';
10
10
  import {
11
+ EnviveMetricsEventName,
11
12
  SpiffyMetricsEventName,
12
13
  useAmplitude,
13
14
  } from '@envive-ai/react-hooks/contexts/amplitudeContext';
@@ -24,7 +25,7 @@ import { useAtomValue } from 'jotai';
24
25
  import { variantInfoAtom } from '@envive-ai/react-hooks/atoms/app';
25
26
  import { BaseWidgetProps } from '../../hocs/withBaseWidget/types';
26
27
  import { withBaseWidget } from '../../hocs/withBaseWidget/withBaseWidget';
27
- import { getRecentProductImageUrls } from '../utils/functions';
28
+ import { RawValues, getRecentProductImageUrls, getStringIdForText } from '../utils/functions';
28
29
 
29
30
  const PromptButtonCarouselWithImageWidgetHandler = (props: BaseWidgetProps) => {
30
31
  const { onTypedMessageSubmitted } = useSalesAgent();
@@ -60,6 +61,16 @@ const PromptButtonCarouselWithImageWidgetHandler = (props: BaseWidgetProps) => {
60
61
 
61
62
  const handlePromptButtonClick = useCallback(
62
63
  (text: string) => {
64
+ const rawValues = (hardcopyContent as { rawValues?: RawValues } | undefined)?.rawValues;
65
+ const stringId = getStringIdForText(rawValues, text);
66
+ trackEvent({
67
+ eventName: EnviveMetricsEventName.WidgetTextClicked,
68
+ eventProps: {
69
+ response_id: hardcopyContent?.responseId,
70
+ string_id: stringId,
71
+ text,
72
+ },
73
+ });
63
74
  onTypedMessageSubmitted({
64
75
  query: text,
65
76
  userTyped: false,