@@ -105,8 +105,161 @@ export interface HelpMenuProps {
105
105
children ?: React . ReactNode ;
106
106
}
107
107
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
+
108
260
export const HelpMenu : React . FC < HelpMenuProps > = ( { contactFormParams, children } ) => {
109
261
const [ showIframe , setShowIframe ] = React . useState < string | undefined > ( ) ;
262
+ const { chatContext } = useWebMessagingDeployment ( ) ;
110
263
111
264
const contactFormUrl = React . useMemo ( ( ) => {
112
265
const formUrl = 'https://openstax.org/embedded/contact' ;
@@ -118,6 +271,12 @@ export const HelpMenu: React.FC<HelpMenuProps> = ({ contactFormParams, children
118
271
return `${ formUrl } ?${ params } ` ;
119
272
} , [ contactFormParams ] ) ;
120
273
274
+ const hoursRange = React . useMemo (
275
+ ( ) => chatContext == null ? '' : formatBusinessHoursRange (
276
+ chatContext . startTime , chatContext . endTime
277
+ ) , [ chatContext ]
278
+ ) ;
279
+
121
280
React . useEffect ( ( ) => {
122
281
const closeIt = ( { data} : MessageEvent ) => {
123
282
if ( data === 'CONTACT_FORM_SUBMITTED' ) {
@@ -132,9 +291,17 @@ export const HelpMenu: React.FC<HelpMenuProps> = ({ contactFormParams, children
132
291
return (
133
292
< >
134
293
< 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
+ }
138
305
{ children }
139
306
</ HelpMenuButton >
140
307
0 commit comments