Skip to content

Commit 1fa80c7

Browse files
First attempt at adding chat
1 parent 8c95b76 commit 1fa80c7

File tree

1 file changed

+170
-3
lines changed

1 file changed

+170
-3
lines changed

src/components/HelpMenu.tsx

Lines changed: 170 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -105,8 +105,161 @@ export interface HelpMenuProps {
105105
children?: React.ReactNode;
106106
}
107107

108+
declare global {
109+
interface Window {
110+
embeddedservice_bootstrap?: any;
111+
}
112+
}
113+
export interface BusinessHours {
114+
startTime: number;
115+
endTime: number;
116+
}
117+
118+
export interface ChatContext extends BusinessHours {
119+
openChat: () => void;
120+
}
121+
122+
function useScript(src: string): boolean {
123+
const [ready, setReady] = React.useState(false);
124+
const scriptRef = React.useRef<HTMLScriptElement | null>(null);
125+
126+
React.useEffect(() => {
127+
// Already in the DOM? No need to add it again.
128+
if (document.querySelector(`script[src="${src}"]`)) {
129+
setReady(true);
130+
return;
131+
}
132+
133+
const script = document.createElement('script');
134+
script.src = src;
135+
script.async = true;
136+
script.onload = () => setReady(true);
137+
script.onerror = () => console.warn(`Failed to load ${src}`);
138+
document.head.appendChild(script);
139+
scriptRef.current = script;
140+
141+
return () => {
142+
scriptRef.current?.remove();
143+
};
144+
}, [src]);
145+
146+
return ready;
147+
}
148+
149+
export function useWebMessagingDeployment() {
150+
const [chatContext, setChatContext] = React.useState<ChatContext | null>(null);
151+
const orgId = '00DU0000000Kwch';
152+
const app = 'Web_Messaging_Deployment';
153+
const deployment = 'ESWWebMessagingDeployme1716235390398';
154+
const scrt2URL = 'https://openstax.my.salesforce-scrt.com';
155+
const scriptUrl =
156+
`https://openstax.my.site.com/${deployment}/assets/js/bootstrap.min.js`;
157+
158+
const scriptLoaded = useScript(scriptUrl);
159+
160+
React.useEffect(() => {
161+
if (!scriptLoaded || typeof window === 'undefined') return;
162+
163+
let cancelled = false;
164+
165+
const openChat = () => {
166+
const svc = window.embeddedservice_bootstrap
167+
svc.utilAPI.launchChat()
168+
.catch((err: Error) => {
169+
console.error(err)
170+
})
171+
}
172+
173+
// Get the business hours from salesforce
174+
const fetchHours = async () => {
175+
try {
176+
const res = await fetch(
177+
`${scrt2URL}/embeddedservice/v1/businesshours?orgId=${orgId}&esConfigName=${app}`
178+
);
179+
const { businessHoursInfo }: {
180+
businessHoursInfo: {
181+
isActive: boolean
182+
businessHours: BusinessHours[]
183+
}
184+
} = await res.json();
185+
186+
if (cancelled) return;
187+
188+
const today = (new Date()).toISOString().slice(0, 10);
189+
const todaysHours = businessHoursInfo.businessHours.find(
190+
(h: BusinessHours) => today === (new Date(h.startTime)).toISOString().slice(0, 10)
191+
);
192+
const newChatContext = !businessHoursInfo.isActive || todaysHours === undefined
193+
? null
194+
: { ...todaysHours, openChat }
195+
setChatContext(newChatContext);
196+
} catch (e) {
197+
if (!cancelled) {
198+
console.error('Error fetching business hours', e);
199+
setChatContext(null);
200+
}
201+
}
202+
};
203+
204+
const handleBusinessHours = () => {
205+
void fetchHours();
206+
};
207+
208+
// Don't try to set business hours until we know the chat is ready
209+
window.addEventListener('onEmbeddedMessagingReady', handleBusinessHours);
210+
window.addEventListener('onEmbeddedMessagingBusinessHoursStarted', handleBusinessHours);
211+
window.addEventListener('onEmbeddedMessagingBusinessHoursEnded', handleBusinessHours);
212+
213+
try {
214+
// `embeddedservice_bootstrap` is injected by the script we just added
215+
const svc = window.embeddedservice_bootstrap;
216+
svc.settings.language = 'en_US';
217+
svc.settings.hideChatButtonOnLoad = true;
218+
svc.init(orgId, app, `https://openstax.my.site.com/${deployment}`, { scrt2URL });
219+
} catch (e) {
220+
console.error('Error initializing Embedded Messaging', e);
221+
}
222+
223+
return () => {
224+
cancelled = true;
225+
window.removeEventListener('onEmbeddedMessagingReady', handleBusinessHours);
226+
window.removeEventListener('onEmbeddedMessagingBusinessHoursStarted', handleBusinessHours);
227+
window.removeEventListener('onEmbeddedMessagingBusinessHoursEnded', handleBusinessHours);
228+
};
229+
}, [scriptLoaded, orgId, app, scrt2URL]);
230+
231+
return { chatContext };
232+
}
233+
234+
const formatBusinessHoursRange = (startTime: number, endTime: number) => {
235+
// Ensure we are working with a real Date instance
236+
const startDate = new Date(startTime);
237+
const endDate = new Date(endTime);
238+
239+
// Bail if the timestamps are not valid numbers
240+
if (isNaN(startDate.getTime()) || isNaN(endDate.getTime())) return '';
241+
242+
try {
243+
const baseOptions: Parameters<typeof Intl.DateTimeFormat>[1] = {
244+
hour: 'numeric', hour12: true
245+
};
246+
const start = new Intl.DateTimeFormat(undefined, baseOptions).format(startDate);
247+
const end = new Intl.DateTimeFormat(undefined, {
248+
...baseOptions, timeZoneName: 'short'
249+
}).format(endDate);
250+
// Ex: 9 AM - 5 PM CDT
251+
return `${start} - ${end}`;
252+
} catch (e) {
253+
console.warn(
254+
'Intl.DateTimeFormat not available, falling back to simple hours.', e
255+
);
256+
return `${startDate.getHours()} - ${endDate.getHours()}`;
257+
}
258+
}
259+
108260
export const HelpMenu: React.FC<HelpMenuProps> = ({ contactFormParams, children }) => {
109261
const [showIframe, setShowIframe] = React.useState<string | undefined>();
262+
const { chatContext } = useWebMessagingDeployment();
110263

111264
const contactFormUrl = React.useMemo(() => {
112265
const formUrl = 'https://openstax.org/embedded/contact';
@@ -118,6 +271,12 @@ export const HelpMenu: React.FC<HelpMenuProps> = ({ contactFormParams, children
118271
return `${formUrl}?${params}`;
119272
}, [contactFormParams]);
120273

274+
const hoursRange = React.useMemo(
275+
() => chatContext == null ? '' : formatBusinessHoursRange(
276+
chatContext.startTime, chatContext.endTime
277+
), [chatContext]
278+
);
279+
121280
React.useEffect(() => {
122281
const closeIt = ({data}: MessageEvent) => {
123282
if (data === 'CONTACT_FORM_SUBMITTED') {
@@ -132,9 +291,17 @@ export const HelpMenu: React.FC<HelpMenuProps> = ({ contactFormParams, children
132291
return (
133292
<>
134293
<HelpMenuButton label='Help' aria-label='Help menu'>
135-
<HelpMenuItem onAction={() => setShowIframe(contactFormUrl)}>
136-
Report an issue
137-
</HelpMenuItem>
294+
{chatContext !== null
295+
? (
296+
<HelpMenuItem onAction={() => chatContext.openChat()}>
297+
Chat With Us ({hoursRange})
298+
</HelpMenuItem>
299+
) : (
300+
<HelpMenuItem onAction={() => setShowIframe(contactFormUrl)}>
301+
Report an issue
302+
</HelpMenuItem>
303+
)
304+
}
138305
{children}
139306
</HelpMenuButton>
140307

0 commit comments

Comments
 (0)