@envive-ai/react-widgets-v3 0.3.39 → 0.3.40

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 (93) hide show
  1. package/dist/CXIntegration/hooks/useUnifiedCXButton.cjs +1 -1
  2. package/dist/CXIntegration/hooks/useUnifiedCXButton.js +1 -1
  3. package/dist/CXIntegration/implementations/useFreshChatUnifiedCXButton.cjs +76 -0
  4. package/dist/CXIntegration/implementations/useFreshChatUnifiedCXButton.js +75 -0
  5. package/dist/CXIntegration/implementations/useHelpScoutUnifiedCXButton.cjs +110 -25
  6. package/dist/CXIntegration/implementations/useHelpScoutUnifiedCXButton.js +110 -25
  7. package/dist/CXIntegration/types.cjs +1 -0
  8. package/dist/CXIntegration/types.js +1 -0
  9. package/dist/CXIntegration/utils/functions.cjs +2 -0
  10. package/dist/CXIntegration/utils/functions.js +2 -0
  11. package/dist/debug/MessageContent.cjs +1 -1
  12. package/dist/debug/MessageContent.js +1 -1
  13. package/dist/debug/chatEmbed.cjs +4 -4
  14. package/dist/debug/chatEmbed.js +3 -3
  15. package/dist/debug/reportIssue.cjs +3 -3
  16. package/dist/debug/reportIssue.js +3 -3
  17. package/dist/hocs/withBaseWidget/types.d.ts +1 -1
  18. package/dist/hocs/withBaseWidget/withBaseWidget.cjs +1 -1
  19. package/dist/hocs/withBaseWidget/withBaseWidget.d.cts +2 -2
  20. package/dist/hocs/withBaseWidget/withBaseWidget.d.ts +2 -2
  21. package/dist/hocs/withBaseWidget/withBaseWidget.js +1 -1
  22. package/dist/packages/widgets/dist/SearchResults/SearchResults.d.ts +2 -2
  23. package/dist/packages/widgets/dist/SearchZeroState/SearchZeroStateWidget.d.ts +2 -2
  24. package/dist/packages/widgets/dist/SuggestionBar/SuggestionBar.d.ts +2 -2
  25. package/dist/widgets/ChatPreviewComparisonWidget/ChatPreviewComparisonWidget.cjs +9 -9
  26. package/dist/widgets/ChatPreviewComparisonWidget/ChatPreviewComparisonWidget.d.cts +3 -3
  27. package/dist/widgets/ChatPreviewComparisonWidget/ChatPreviewComparisonWidget.d.ts +3 -3
  28. package/dist/widgets/ChatPreviewComparisonWidget/ChatPreviewComparisonWidget.js +9 -9
  29. package/dist/widgets/ChatPreviewLoadingWidget/ChatPreviewLoadingWidget.cjs +1 -1
  30. package/dist/widgets/ChatPreviewLoadingWidget/ChatPreviewLoadingWidget.d.cts +3 -3
  31. package/dist/widgets/ChatPreviewLoadingWidget/ChatPreviewLoadingWidget.d.ts +3 -3
  32. package/dist/widgets/ChatPreviewLoadingWidget/ChatPreviewLoadingWidget.js +1 -1
  33. package/dist/widgets/ChatPreviewWidget/ChatPreviewWidget.cjs +7 -7
  34. package/dist/widgets/ChatPreviewWidget/ChatPreviewWidget.d.cts +3 -3
  35. package/dist/widgets/ChatPreviewWidget/ChatPreviewWidget.d.ts +3 -3
  36. package/dist/widgets/ChatPreviewWidget/ChatPreviewWidget.js +7 -7
  37. package/dist/widgets/FloatingChatWidget/FloatingChatOverlay.cjs +3 -0
  38. package/dist/widgets/FloatingChatWidget/FloatingChatOverlay.js +3 -0
  39. package/dist/widgets/FloatingChatWidget/FloatingChatWidget.cjs +7 -7
  40. package/dist/widgets/FloatingChatWidget/FloatingChatWidget.d.cts +2 -2
  41. package/dist/widgets/FloatingChatWidget/FloatingChatWidget.d.ts +2 -2
  42. package/dist/widgets/FloatingChatWidget/FloatingChatWidget.js +7 -7
  43. package/dist/widgets/FloatingChatWidget/hooks/useAutoPopup.cjs +2 -2
  44. package/dist/widgets/FloatingChatWidget/hooks/useAutoPopup.js +2 -2
  45. package/dist/widgets/FloatingChatWidget/hooks/useFloatingButtonVisibility.cjs +1 -1
  46. package/dist/widgets/FloatingChatWidget/hooks/useFloatingButtonVisibility.js +1 -1
  47. package/dist/widgets/FullPageSalesAgentWidget/FullPageSalesAgentWidget.cjs +1 -1
  48. package/dist/widgets/FullPageSalesAgentWidget/FullPageSalesAgentWidget.d.cts +2 -2
  49. package/dist/widgets/FullPageSalesAgentWidget/FullPageSalesAgentWidget.d.ts +2 -2
  50. package/dist/widgets/FullPageSalesAgentWidget/FullPageSalesAgentWidget.js +1 -1
  51. package/dist/widgets/ProductCardWidget/ProductCardWidget.cjs +6 -6
  52. package/dist/widgets/ProductCardWidget/ProductCardWidget.d.cts +2 -2
  53. package/dist/widgets/ProductCardWidget/ProductCardWidget.d.ts +2 -2
  54. package/dist/widgets/ProductCardWidget/ProductCardWidget.js +6 -6
  55. package/dist/widgets/PromptButtonCarouselWithImageWidget/PromptButtonCarouselWithImageWidget.cjs +8 -8
  56. package/dist/widgets/PromptButtonCarouselWithImageWidget/PromptButtonCarouselWithImageWidget.d.cts +3 -3
  57. package/dist/widgets/PromptButtonCarouselWithImageWidget/PromptButtonCarouselWithImageWidget.d.ts +3 -3
  58. package/dist/widgets/PromptButtonCarouselWithImageWidget/PromptButtonCarouselWithImageWidget.js +8 -8
  59. package/dist/widgets/PromptCarouselWidget/PromptCarouselWidget.cjs +6 -6
  60. package/dist/widgets/PromptCarouselWidget/PromptCarouselWidget.d.cts +2 -2
  61. package/dist/widgets/PromptCarouselWidget/PromptCarouselWidget.js +6 -6
  62. package/dist/widgets/SocialProofFlowWidget/SocialProofFlowWidget.cjs +2 -2
  63. package/dist/widgets/SocialProofFlowWidget/SocialProofFlowWidget.d.cts +2 -2
  64. package/dist/widgets/SocialProofFlowWidget/SocialProofFlowWidget.d.ts +2 -2
  65. package/dist/widgets/SocialProofFlowWidget/SocialProofFlowWidget.js +2 -2
  66. package/dist/widgets/SocialProofWidget/SocialProofWidget.cjs +9 -9
  67. package/dist/widgets/SocialProofWidget/SocialProofWidget.d.cts +3 -3
  68. package/dist/widgets/SocialProofWidget/SocialProofWidget.d.ts +3 -3
  69. package/dist/widgets/SocialProofWidget/SocialProofWidget.js +9 -9
  70. package/dist/widgets/TitledPromptCarouselWidget/TitledPromptCarouselWidget.cjs +5 -5
  71. package/dist/widgets/TitledPromptCarouselWidget/TitledPromptCarouselWidget.d.cts +2 -2
  72. package/dist/widgets/TitledPromptCarouselWidget/TitledPromptCarouselWidget.d.ts +2 -2
  73. package/dist/widgets/TitledPromptCarouselWidget/TitledPromptCarouselWidget.js +5 -5
  74. package/dist/widgets/TypingAnimationFlowWidget/TypingAnimationFlowWidget.cjs +2 -2
  75. package/dist/widgets/TypingAnimationFlowWidget/TypingAnimationFlowWidget.d.ts +2 -2
  76. package/dist/widgets/TypingAnimationFlowWidget/TypingAnimationFlowWidget.js +2 -2
  77. package/dist/widgets/TypingAnimationWidget/TypingAnimationWidget.cjs +6 -6
  78. package/dist/widgets/TypingAnimationWidget/TypingAnimationWidget.d.cts +3 -3
  79. package/dist/widgets/TypingAnimationWidget/TypingAnimationWidget.d.ts +3 -3
  80. package/dist/widgets/TypingAnimationWidget/TypingAnimationWidget.js +6 -6
  81. package/dist/widgets/dist/SearchResults/SearchResults.d.cts +2 -2
  82. package/dist/widgets/dist/SearchZeroState/SearchZeroStateWidget.d.cts +2 -2
  83. package/dist/widgets/dist/SuggestionBar/SuggestionBar.d.cts +2 -2
  84. package/dist/widgets/hooks/useGetWidgetStatus.cjs +2 -2
  85. package/dist/widgets/hooks/useGetWidgetStatus.js +2 -2
  86. package/dist/widgets/utils/functions.cjs +1 -1
  87. package/dist/widgets/utils/functions.js +1 -1
  88. package/package.json +1 -1
  89. package/src/CXIntegration/implementations/useFreshChatUnifiedCXButton.ts +132 -0
  90. package/src/CXIntegration/implementations/useHelpScoutUnifiedCXButton.ts +184 -54
  91. package/src/CXIntegration/types.ts +1 -0
  92. package/src/CXIntegration/utils/functions.ts +4 -0
  93. package/src/widgets/FloatingChatWidget/FloatingChatOverlay.tsx +3 -0
@@ -1,9 +1,9 @@
1
1
  import { BaseWidgetProps } from "../../hocs/withBaseWidget/types.js";
2
- import * as react_jsx_runtime2 from "react/jsx-runtime";
2
+ import * as react_jsx_runtime0 from "react/jsx-runtime";
3
3
 
4
4
  //#region src/widgets/TypingAnimationWidget/TypingAnimationWidget.d.ts
5
5
  declare const TypingAnimationWithBaseWidget: {
6
- (props: BaseWidgetProps): react_jsx_runtime2.JSX.Element;
6
+ (props: BaseWidgetProps): react_jsx_runtime0.JSX.Element;
7
7
  displayName: string;
8
8
  };
9
9
  interface TypingAnimationWidgetProps {
@@ -12,7 +12,7 @@ interface TypingAnimationWidgetProps {
12
12
  declare const TypingAnimationWidget: {
13
13
  ({
14
14
  widgetConfigId
15
- }: TypingAnimationWidgetProps): react_jsx_runtime2.JSX.Element;
15
+ }: TypingAnimationWidgetProps): react_jsx_runtime0.JSX.Element;
16
16
  displayName: string;
17
17
  };
18
18
  //#endregion
@@ -1,17 +1,17 @@
1
1
  import { withBaseWidget } from "../../hocs/withBaseWidget/withBaseWidget.js";
2
2
  import "../../hocs/withBaseWidget/index.js";
3
3
  import { getStringIdForText } from "../utils/functions.js";
4
- import { useSalesAgent } from "@envive-ai/react-hooks/contexts/salesAgentContext";
4
+ import { useCallback } from "react";
5
+ import { EnviveMetricsEventName, useAmplitude } from "@envive-ai/react-hooks/contexts/amplitudeContext";
6
+ import { jsx } from "react/jsx-runtime";
5
7
  import { WidgetTypeV3 } from "@envive-ai/react-hooks/contexts/typesV3";
6
- import { useChatToggle } from "@envive-ai/react-hooks/hooks/ChatToggle";
7
8
  import { useFeatureFlagService } from "@envive-ai/react-hooks/contexts/featureFlagServiceContext";
8
9
  import { ChatElementDisplayLocationV3, FeatureGates } from "@envive-ai/react-hooks/application/models";
9
- import { EnviveMetricsEventName, useAmplitude } from "@envive-ai/react-hooks/contexts/amplitudeContext";
10
- import { useWidgetInteraction } from "@envive-ai/react-hooks/hooks/WidgetInteraction";
10
+ import { useSalesAgent } from "@envive-ai/react-hooks/contexts/salesAgentContext";
11
+ import { useChatToggle } from "@envive-ai/react-hooks/hooks/ChatToggle";
11
12
  import { WidgetInteractionComponent, WidgetInteractionType } from "@envive-ai/react-hooks/hooks/WidgetInteraction/types";
12
13
  import { PromptCarouselRows, usePromptCarouselAnalytics } from "@envive-ai/react-toolkit-v3/PromptCarousel";
13
- import { useCallback } from "react";
14
- import { jsx } from "react/jsx-runtime";
14
+ import { useWidgetInteraction } from "@envive-ai/react-hooks/hooks/WidgetInteraction";
15
15
  import { PromptButtonVariant } from "@envive-ai/react-toolkit-v3/PromptButton/types";
16
16
  import { TypingAnimation } from "@envive-ai/react-toolkit-v3/TypingAnimation";
17
17
  import { WidgetWrapperVariant } from "@envive-ai/react-toolkit-v3/WidgetWrapper";
@@ -1,5 +1,5 @@
1
1
  import { SearchResultsEntryPointWidgetConfig } from "./types.cjs";
2
- import * as react_jsx_runtime1 from "react/jsx-runtime";
2
+ import * as react_jsx_runtime0 from "react/jsx-runtime";
3
3
  import { SearchResultsHocProps } from "@envive-ai/react-hooks/hooks/Search";
4
4
 
5
5
  //#region ../widgets/dist/SearchResults/SearchResults.d.ts
@@ -7,7 +7,7 @@ import { SearchResultsHocProps } from "@envive-ai/react-hooks/hooks/Search";
7
7
  interface SearchResultsProps extends SearchResultsHocProps {
8
8
  widgetConfig: SearchResultsEntryPointWidgetConfig;
9
9
  }
10
- declare const SearchResults: (props: Omit<SearchResultsProps, string | number | symbol>) => react_jsx_runtime1.JSX.Element;
10
+ declare const SearchResults: (props: Omit<SearchResultsProps, string | number | symbol>) => react_jsx_runtime0.JSX.Element;
11
11
  //#endregion
12
12
 
13
13
  //#endregion
@@ -1,4 +1,4 @@
1
- import * as react_jsx_runtime3 from "react/jsx-runtime";
1
+ import * as react_jsx_runtime2 from "react/jsx-runtime";
2
2
 
3
3
  //#region ../widgets/dist/SearchZeroState/SearchZeroStateWidget.d.ts
4
4
  //#region src/SearchZeroState/SearchZeroStateWidget.d.ts
@@ -11,7 +11,7 @@ declare const SearchZeroStateWidget: ({
11
11
  initialIsOpen,
12
12
  widgetConfigId,
13
13
  entryPointRef
14
- }: SearchZeroStateWidgetProps) => react_jsx_runtime3.JSX.Element;
14
+ }: SearchZeroStateWidgetProps) => react_jsx_runtime2.JSX.Element;
15
15
  //#endregion
16
16
 
17
17
  //#endregion
@@ -1,5 +1,5 @@
1
1
  import { SuggestionBarLocationForMetrics } from "./types.cjs";
2
- import * as react_jsx_runtime0 from "react/jsx-runtime";
2
+ import * as react_jsx_runtime3 from "react/jsx-runtime";
3
3
  import { SuggestionButtonVariant } from "@envive-ai/react-hooks/contexts/types";
4
4
  import { Message } from "postcss";
5
5
 
@@ -30,7 +30,7 @@ declare function SuggestionBar({
30
30
  buttonBorderRadius,
31
31
  handleReply,
32
32
  dataTestId
33
- }: Readonly<SuggestionBarProps>): react_jsx_runtime0.JSX.Element;
33
+ }: Readonly<SuggestionBarProps>): react_jsx_runtime3.JSX.Element;
34
34
  //#endregion
35
35
  //#endregion
36
36
  export { SuggestionBar };
@@ -1,8 +1,8 @@
1
1
  const require_rolldown_runtime = require('../../_virtual/rolldown_runtime.cjs');
2
2
  const require_functions = require('../utils/functions.cjs');
3
- let __envive_ai_react_hooks_atoms_chat = require("@envive-ai/react-hooks/atoms/chat");
4
- let jotai = require("jotai");
5
3
  let react = require("react");
4
+ let jotai = require("jotai");
5
+ let __envive_ai_react_hooks_atoms_chat = require("@envive-ai/react-hooks/atoms/chat");
6
6
  let __envive_ai_react_hooks_atoms_chat_chatState = require("@envive-ai/react-hooks/atoms/chat/chatState");
7
7
 
8
8
  //#region src/widgets/hooks/useGetWidgetStatus.tsx
@@ -1,7 +1,7 @@
1
1
  import { isLoading, isProductComparison, userHasInteracted, userHasNotInteracted } from "../utils/functions.js";
2
- import { chatAtom, lastAssistantMessageAtom } from "@envive-ai/react-hooks/atoms/chat";
3
- import { useAtomValue } from "jotai";
4
2
  import { useMemo } from "react";
3
+ import { useAtomValue } from "jotai";
4
+ import { chatAtom, lastAssistantMessageAtom } from "@envive-ai/react-hooks/atoms/chat";
5
5
  import { responseStreamingAtom } from "@envive-ai/react-hooks/atoms/chat/chatState";
6
6
 
7
7
  //#region src/widgets/hooks/useGetWidgetStatus.tsx
@@ -1,6 +1,6 @@
1
1
  const require_rolldown_runtime = require('../../_virtual/rolldown_runtime.cjs');
2
- let __envive_ai_react_hooks_atoms_chat = require("@envive-ai/react-hooks/atoms/chat");
3
2
  let __envive_ai_react_hooks_application_models = require("@envive-ai/react-hooks/application/models");
3
+ let __envive_ai_react_hooks_atoms_chat = require("@envive-ai/react-hooks/atoms/chat");
4
4
  let __envive_ai_react_hooks_atoms_atomStore = require("@envive-ai/react-hooks/atoms/atomStore");
5
5
 
6
6
  //#region src/widgets/utils/functions.ts
@@ -1,5 +1,5 @@
1
- import { chatAtom, lastAssistantMessageAtom } from "@envive-ai/react-hooks/atoms/chat";
2
1
  import { MessageRole, MessageType } from "@envive-ai/react-hooks/application/models";
2
+ import { chatAtom, lastAssistantMessageAtom } from "@envive-ai/react-hooks/atoms/chat";
3
3
  import { getAtomStore } from "@envive-ai/react-hooks/atoms/atomStore";
4
4
 
5
5
  //#region src/widgets/utils/functions.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@envive-ai/react-widgets-v3",
3
- "version": "0.3.39",
3
+ "version": "0.3.40",
4
4
  "description": "React widget library v3 for Envive services.",
5
5
  "keywords": [
6
6
  "react",
@@ -0,0 +1,132 @@
1
+ import { useRef } from 'react';
2
+ import { SelectorFactory } from '@envive-ai/react-hooks/application/utils';
3
+ import { useElementObserver } from '@envive-ai/react-hooks/hooks/ElementObserver';
4
+ import { FLOATING_BUTTON_ID } from '../../widgets/FloatingChatWidget/constants';
5
+ import { CustomerServiceImplProps, UnifiedCXButton } from '../types';
6
+
7
+ const FRESHCHAT_FRAME_ID = 'fc_frame';
8
+ const FRESHCHAT_WIDGET_ID = 'fc_widget';
9
+ const FRESHCHAT_OPEN_CLASS = 'fc-open';
10
+ const FRESHCHAT_LAUNCHER_SELECTOR = '.d-hotline';
11
+
12
+ interface FreshChatApi {
13
+ open: () => void;
14
+ close: () => void;
15
+ show: () => void;
16
+ hide: () => void;
17
+ isOpen?: () => boolean;
18
+ isLoaded?: () => boolean;
19
+ on?: (event: string, callback: () => void) => void;
20
+ }
21
+
22
+ declare global {
23
+ interface Window {
24
+ fcWidget?: FreshChatApi;
25
+ }
26
+ }
27
+
28
+ interface UseFreshChatUnifiedCXButtonProps extends CustomerServiceImplProps {}
29
+
30
+ const isFreshChatOpen = (el: HTMLElement | null): boolean => {
31
+ if (!el) return false;
32
+ if (el.classList.contains(FRESHCHAT_OPEN_CLASS)) return true;
33
+ // Fallback: when open, the iframe expands well beyond launcher dimensions
34
+ const iframe = document.getElementById(FRESHCHAT_WIDGET_ID) as HTMLIFrameElement | null;
35
+ if (!iframe) return false;
36
+ const rect = iframe.getBoundingClientRect();
37
+ return rect.width > 80 && rect.height > 80;
38
+ };
39
+
40
+ export const useFreshChatUnifiedCXButton = ({
41
+ onSwitchToAgent,
42
+ onCXClose,
43
+ suppressMerchantButton,
44
+ }: UseFreshChatUnifiedCXButtonProps): UnifiedCXButton => {
45
+ const freshchatFrame = useElementObserver(SelectorFactory.id(FRESHCHAT_FRAME_ID));
46
+ const enviveFloatingButton = useElementObserver(SelectorFactory.id(FLOATING_BUTTON_ID));
47
+ // Tracks the previous open/closed state so onChange (which fires on every DOM
48
+ // mutation inside #fc_frame) only acts on real transitions — otherwise the
49
+ // floating button flickers while the chat panel is in use.
50
+ const wasOpenRef = useRef(false);
51
+
52
+ const toggle = () => {
53
+ onSwitchToAgent();
54
+ enviveFloatingButton.hide();
55
+
56
+ if (typeof window.fcWidget !== 'undefined') {
57
+ window.fcWidget.show();
58
+ window.fcWidget.open();
59
+ } else {
60
+ freshchatFrame.show();
61
+ const iframe = document.getElementById(FRESHCHAT_WIDGET_ID) as HTMLIFrameElement | null;
62
+ const launcher = iframe?.contentDocument?.querySelector(
63
+ FRESHCHAT_LAUNCHER_SELECTOR,
64
+ ) as HTMLElement | null;
65
+ launcher?.click();
66
+ }
67
+ };
68
+
69
+ const isFreshChatAvailable = () => {
70
+ const frame = document.getElementById(FRESHCHAT_FRAME_ID);
71
+ const hasApi = typeof window.fcWidget !== 'undefined';
72
+ return !!(frame || hasApi);
73
+ };
74
+
75
+ const isSwitchEnabled = () => isFreshChatAvailable();
76
+
77
+ freshchatFrame.onAdd(el => {
78
+ if (!el) return;
79
+
80
+ const isOpen = isFreshChatOpen(el);
81
+ wasOpenRef.current = isOpen;
82
+
83
+ if (isOpen) {
84
+ enviveFloatingButton.hide();
85
+ } else {
86
+ enviveFloatingButton.show();
87
+ }
88
+
89
+ if (suppressMerchantButton) {
90
+ if (isOpen) {
91
+ freshchatFrame.show();
92
+ } else {
93
+ freshchatFrame.hide();
94
+ }
95
+ }
96
+ });
97
+
98
+ freshchatFrame.onChange(el => {
99
+ if (!el) return;
100
+
101
+ const isOpen = isFreshChatOpen(el);
102
+
103
+ if (isOpen === wasOpenRef.current) return;
104
+ wasOpenRef.current = isOpen;
105
+
106
+ if (isOpen) {
107
+ enviveFloatingButton.hide();
108
+ if (suppressMerchantButton) {
109
+ freshchatFrame.show();
110
+ }
111
+ } else {
112
+ onCXClose?.();
113
+ enviveFloatingButton.show();
114
+ if (suppressMerchantButton) {
115
+ freshchatFrame.hide();
116
+ }
117
+ }
118
+ });
119
+
120
+ freshchatFrame.onRemove(() => {
121
+ if (wasOpenRef.current) {
122
+ onCXClose?.();
123
+ }
124
+ wasOpenRef.current = false;
125
+ enviveFloatingButton.show();
126
+ });
127
+
128
+ return {
129
+ toggle,
130
+ isSwitchEnabled,
131
+ };
132
+ };
@@ -1,102 +1,232 @@
1
1
  import { SelectorFactory } from '@envive-ai/react-hooks/application/utils';
2
2
  import { useElementObserver } from '@envive-ai/react-hooks/hooks/ElementObserver';
3
+ import { useEffect, useRef } from 'react';
3
4
  import { FLOATING_BUTTON_ID } from '../../widgets/FloatingChatWidget/constants';
4
5
  import { CustomerServiceImplProps, UnifiedCXButton } from '../types';
5
6
 
6
7
  const BEACON_CONTAINER_ID = 'beacon-container';
8
+ const FAB_BUTTON_SELECTOR = '.BeaconFabButtonFrame';
9
+ const BEACON_WIDGET_SELECTOR = '.BeaconContainer';
10
+ // HelpScout's container may render the widget either as a styled `.BeaconContainer` or
11
+ // (e.g. in the "Instant Answers" view) inside an iframe. We treat either as "open".
12
+ const BEACON_OPEN_CHILD_SELECTOR = '.BeaconContainer, iframe, [class*="Beacon"][class*="ontainer"]';
7
13
 
8
14
  interface UseHelpScoutUnifiedCXButtonProps extends CustomerServiceImplProps {}
9
15
 
16
+ type BeaconApi = (command: string, ...args: unknown[]) => void;
17
+
18
+ const getBeaconApi = (): BeaconApi | undefined => {
19
+ const beacon = (window as unknown as { Beacon?: BeaconApi }).Beacon;
20
+ return typeof beacon === 'function' ? beacon : undefined;
21
+ };
22
+
23
+ const getBeaconContainer = (): HTMLElement | null => document.getElementById(BEACON_CONTAINER_ID);
24
+
25
+ const getFabButton = (): HTMLElement | null => {
26
+ return (getBeaconContainer()?.querySelector(FAB_BUTTON_SELECTOR) as HTMLElement) ?? null;
27
+ };
28
+
29
+ const getFloatingButtonEl = (): HTMLElement | null => document.getElementById(FLOATING_BUTTON_ID);
30
+
31
+ /**
32
+ * Detects whether the HelpScout beacon is currently visible/open.
33
+ * We use multiple signals because HelpScout has different "views"
34
+ * (chat, instant answers) that may use different class names.
35
+ */
36
+ const isBeaconCurrentlyOpen = (): boolean => {
37
+ const container = getBeaconContainer();
38
+ if (!container) return false;
39
+
40
+ // Signal 1: a `.BeaconContainer` exists with an enter-* class
41
+ const beaconWidget = container.querySelector(BEACON_WIDGET_SELECTOR);
42
+ if (beaconWidget) {
43
+ const cl = beaconWidget.classList;
44
+ if (
45
+ cl.contains('BeaconContainer-enter-done') ||
46
+ cl.contains('BeaconContainer-enter-active') ||
47
+ cl.contains('BeaconContainer-enter')
48
+ ) {
49
+ return true;
50
+ }
51
+ if (
52
+ cl.contains('BeaconContainer-exit') ||
53
+ cl.contains('BeaconContainer-exit-active') ||
54
+ cl.contains('BeaconContainer-exit-done')
55
+ ) {
56
+ return false;
57
+ }
58
+ }
59
+
60
+ // Signal 2: any non-FAB visible child in the container means the panel is rendered
61
+ const children = Array.from(container.children);
62
+ return children.some(child => {
63
+ if (child.matches(FAB_BUTTON_SELECTOR)) return false;
64
+ const rect = (child as HTMLElement).getBoundingClientRect();
65
+ return rect.width > 50 && rect.height > 50;
66
+ });
67
+ };
68
+
69
+ /**
70
+ * Force-hides the Envive floating button via direct DOM manipulation. This is a
71
+ * safety net for cases where React state alone doesn't unmount the element fast
72
+ * enough (or at all), so we never get a flash of the floating button on top of
73
+ * the merchant's CX UI.
74
+ */
75
+ const forceHideFloatingButton = () => {
76
+ const el = getFloatingButtonEl();
77
+ if (el) el.style.display = 'none';
78
+ };
79
+
80
+ /**
81
+ * Restores the Envive floating button display, letting React's render logic take
82
+ * over again.
83
+ */
84
+ const forceShowFloatingButton = () => {
85
+ const el = getFloatingButtonEl();
86
+ if (el && el.style.display === 'none') el.style.display = '';
87
+ };
88
+
10
89
  export const useHelpScoutUnifiedCXButton = ({
11
90
  onSwitchToAgent,
91
+ onCXClose,
12
92
  suppressMerchantButton,
13
93
  }: UseHelpScoutUnifiedCXButtonProps): UnifiedCXButton => {
14
94
  const beaconContainer = useElementObserver(SelectorFactory.id(BEACON_CONTAINER_ID));
15
95
  const enviveFloatingButton = useElementObserver(SelectorFactory.id(FLOATING_BUTTON_ID));
16
96
 
97
+ // Track previous open state so we only fire onCXClose on a real open→closed
98
+ // transition, not on initial render or during the open animation.
99
+ const wasOpenRef = useRef(false);
100
+
101
+ // Keep latest callbacks/flags in refs so the Beacon API effect can read them
102
+ // without re-binding listeners on every render.
103
+ const onCXCloseRef = useRef(onCXClose);
104
+ const suppressMerchantButtonRef = useRef(suppressMerchantButton);
105
+ useEffect(() => {
106
+ onCXCloseRef.current = onCXClose;
107
+ suppressMerchantButtonRef.current = suppressMerchantButton;
108
+ }, [onCXClose, suppressMerchantButton]);
109
+
110
+ const handleBeaconOpened = () => {
111
+ wasOpenRef.current = true;
112
+ // Always force-hide the Envive floating button when the merchant CX opens.
113
+ // This is the safety net regardless of suppressMerchantButton.
114
+ forceHideFloatingButton();
115
+ if (suppressMerchantButtonRef.current) {
116
+ const fabButton = getFabButton();
117
+ if (fabButton) fabButton.style.display = '';
118
+ enviveFloatingButton.hide();
119
+ }
120
+ };
121
+
122
+ const handleBeaconClosed = () => {
123
+ if (!wasOpenRef.current) return;
124
+ wasOpenRef.current = false;
125
+ onCXCloseRef.current?.();
126
+ forceShowFloatingButton();
127
+ if (suppressMerchantButtonRef.current) {
128
+ const fabButton = getFabButton();
129
+ if (fabButton) fabButton.style.display = 'none';
130
+ enviveFloatingButton.show();
131
+ }
132
+ };
133
+
17
134
  const toggle = () => {
18
135
  onSwitchToAgent();
136
+ // Force hide BEFORE opening HelpScout so there's no flash of the floating
137
+ // button next to the merchant's CX panel.
138
+ wasOpenRef.current = true;
139
+ forceHideFloatingButton();
140
+ if (suppressMerchantButton) {
141
+ enviveFloatingButton.hide();
142
+ }
19
143
 
20
- // Try using the Help Scout Beacon API first
21
- if (typeof (window as any).Beacon !== 'undefined') {
22
- (window as any).Beacon('open');
144
+ const beacon = getBeaconApi();
145
+ if (beacon) {
146
+ beacon('open');
23
147
  } else {
24
- // Fallback: Find and click the FAB button
25
- const container = document.getElementById(BEACON_CONTAINER_ID);
26
- const fabButton = container?.querySelector('.BeaconFabButtonFrame') as HTMLElement;
148
+ // Fallback: find and click the FAB button
149
+ const fabButton = getFabButton();
27
150
  if (fabButton) {
28
151
  fabButton.style.display = '';
29
152
  fabButton.click();
30
153
  }
31
154
  }
32
-
33
- if (suppressMerchantButton) {
34
- enviveFloatingButton.hide();
35
- }
36
155
  };
37
156
 
38
157
  const isHelpScoutButtonEnabled = () => {
39
- // Check for the main Help Scout Beacon container
40
- const beaconContainerEl = document.getElementById('beacon-container');
158
+ const beaconContainerEl = getBeaconContainer();
41
159
  if (!beaconContainerEl) return false;
42
-
43
- // Check for the FAB button (Floating Action Button) or the global Beacon API
44
- const fabButton = beaconContainerEl.querySelector('.BeaconFabButtonFrame');
45
- const hasBeaconAPI = typeof (window as any).Beacon !== 'undefined';
46
-
160
+ const fabButton = beaconContainerEl.querySelector(FAB_BUTTON_SELECTOR);
161
+ const hasBeaconAPI = !!getBeaconApi();
47
162
  return !!(fabButton || hasBeaconAPI);
48
163
  };
49
164
 
50
165
  const isSwitchEnabled = () => isHelpScoutButtonEnabled();
51
166
 
52
- // Hide Help Scout FAB button when beacon container is added
167
+ // Use the official HelpScout Beacon API events for the most reliable open/close
168
+ // detection. We use empty deps + refs so this binds exactly once per mount, not
169
+ // every render — important because the Beacon listener registry doesn't dedupe.
170
+ useEffect(() => {
171
+ const beacon = getBeaconApi();
172
+ if (!beacon) return undefined;
173
+
174
+ try {
175
+ beacon('on', 'open', handleBeaconOpened);
176
+ beacon('on', 'close', handleBeaconClosed);
177
+ } catch {
178
+ // Beacon may not be initialized yet — the MutationObserver fallback will
179
+ // still detect the open/close transitions below.
180
+ }
181
+
182
+ return () => {
183
+ try {
184
+ beacon('off', 'open', handleBeaconOpened);
185
+ beacon('off', 'close', handleBeaconClosed);
186
+ } catch {
187
+ // ignore — beacon may already be torn down
188
+ }
189
+ };
190
+ // eslint-disable-next-line react-hooks/exhaustive-deps
191
+ }, []);
192
+
193
+ // Hide the merchant's FAB button when the beacon container is added. Do NOT
194
+ // blindly re-show the Envive floating button here — if `Beacon('open')` creates
195
+ // the container in response to a "live support" click, this would un-hide the
196
+ // floating button the user just hid via toggle().
53
197
  beaconContainer.onAdd(() => {
54
198
  if (suppressMerchantButton) {
55
- const fabButton = document
56
- .getElementById(BEACON_CONTAINER_ID)
57
- ?.querySelector('.BeaconFabButtonFrame') as HTMLElement;
58
- if (fabButton) {
59
- fabButton.style.display = 'none';
60
- }
199
+ const fabButton = getFabButton();
200
+ if (fabButton) fabButton.style.display = 'none';
201
+ }
202
+ // If the beacon happens to mount in an already-open state, treat it like a
203
+ // proper open transition so the floating button is hidden.
204
+ if (isBeaconCurrentlyOpen()) {
205
+ handleBeaconOpened();
206
+ } else if (suppressMerchantButton) {
61
207
  enviveFloatingButton.show();
62
208
  }
63
209
  });
64
210
 
65
- // Observe the beacon container for visibility changes
66
- // Help Scout Beacon adds/removes classes to show/hide the widget
211
+ // MutationObserver fallback for cases where the Beacon API events don't fire
212
+ // (older Beacon versions, opened via direct DOM manipulation, etc.). The logic
213
+ // here is broader than just checking enter/exit classes because the "Instant
214
+ // Answers" view can use different DOM markers.
67
215
  beaconContainer.onChange(el => {
68
- if (el && suppressMerchantButton) {
69
- // Check if the BeaconContainer widget is open
70
- const beaconWidget = el.querySelector('.BeaconContainer');
71
- const fabButton = el.querySelector('.BeaconFabButtonFrame') as HTMLElement;
72
-
73
- if (beaconWidget && fabButton) {
74
- const { classList } = beaconWidget;
75
-
76
- // When beacon is open (has enter-done class)
77
- if (
78
- classList.contains('BeaconContainer-enter-done') ||
79
- classList.contains('BeaconContainer-enter-active')
80
- ) {
81
- fabButton.style.display = '';
82
- enviveFloatingButton.hide();
83
- }
84
-
85
- // When beacon is closed (has exit class)
86
- if (
87
- classList.contains('BeaconContainer-exit') ||
88
- classList.contains('BeaconContainer-exit-active')
89
- ) {
90
- fabButton.style.display = 'none';
91
- enviveFloatingButton.show();
92
- }
93
- }
216
+ if (!el) return;
217
+ const open = isBeaconCurrentlyOpen();
218
+ if (open && !wasOpenRef.current) {
219
+ handleBeaconOpened();
220
+ } else if (!open && wasOpenRef.current) {
221
+ handleBeaconClosed();
94
222
  }
95
223
  });
96
224
 
97
- // Cleanup when container is removed
225
+ // Cleanup when the entire container is removed.
98
226
  beaconContainer.onRemove(() => {
99
- if (suppressMerchantButton) {
227
+ if (wasOpenRef.current) {
228
+ handleBeaconClosed();
229
+ } else if (suppressMerchantButton) {
100
230
  enviveFloatingButton.show();
101
231
  }
102
232
  });
@@ -12,6 +12,7 @@ export enum CustomerServiceType {
12
12
  helpscout = 'helpscout',
13
13
  talkdesk = 'talkdesk',
14
14
  eightByEight = '8x8',
15
+ freshchat = 'freshchat',
15
16
  unsupported = 'unsupported',
16
17
  }
17
18
 
@@ -13,6 +13,7 @@ import { useKustomerUnifiedCXButton } from '../implementations/useKustomerUnifie
13
13
  import { useHelpScoutUnifiedCXButton } from '../implementations/useHelpScoutUnifiedCXButton';
14
14
  import { useTalkdeskUnifiedCXButton } from '../implementations/useTalkdeskUnifiedCXButton';
15
15
  import { useEightByEightUnifiedCXButton } from '../implementations/useEightByEightUnifiedCXButton';
16
+ import { useFreshChatUnifiedCXButton } from '../implementations/useFreshChatUnifiedCXButton';
16
17
  import { useDefaultUnifiedCXButton } from '../implementations/useDefaultUnifiedCXButton';
17
18
 
18
19
  export const findCustomerServiceImpl = (
@@ -57,6 +58,9 @@ export const findCustomerServiceImpl = (
57
58
  if (provider === CustomerServiceType.eightByEight) {
58
59
  return useEightByEightUnifiedCXButton;
59
60
  }
61
+ if (provider === CustomerServiceType.freshchat) {
62
+ return useFreshChatUnifiedCXButton;
63
+ }
60
64
 
61
65
  return useDefaultUnifiedCXButton;
62
66
  };
@@ -137,6 +137,9 @@ export const FloatingChatOverlay = ({
137
137
  }}
138
138
  className="envive-tw-shadow-md"
139
139
  onClick={e => e.stopPropagation()}
140
+ role="dialog"
141
+ aria-modal="true"
142
+ aria-label="Envive Chat Window"
140
143
  >
141
144
  {children}
142
145
  </motion.div>