@defra/flood-webchat 0.0.1-beta.2 → 0.0.1-beta.21

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 (39) hide show
  1. package/README.md +83 -1
  2. package/dist/client.js +1607 -212
  3. package/dist/client.js.map +1 -1
  4. package/dist/server.js +181 -240
  5. package/dist/server.js.map +1 -1
  6. package/main.scss +4 -3
  7. package/package.json +6 -2
  8. package/src/client/components/availability/availability.jsx +44 -46
  9. package/src/client/components/availability/availability.scss +105 -5
  10. package/src/client/components/chat/chat.jsx +169 -0
  11. package/src/client/components/chat/chat.scss +143 -0
  12. package/src/client/components/errorSummary/error-summary.jsx +34 -0
  13. package/src/client/components/message/message.jsx +20 -0
  14. package/src/client/components/panel/panel-footer.jsx +9 -0
  15. package/src/client/components/panel/panel-header.jsx +41 -0
  16. package/src/client/components/panel/panel.jsx +119 -0
  17. package/src/client/components/panel/panel.scss +72 -0
  18. package/src/client/components/screens/end-chat.jsx +17 -0
  19. package/src/client/components/screens/pre-chat.jsx +65 -0
  20. package/src/client/components/screens/request-chat.jsx +156 -0
  21. package/src/client/components/screens/settings.jsx +67 -0
  22. package/src/client/components/screens/unavailable.jsx +23 -0
  23. package/src/client/hooks/useFocusedElements.js +71 -0
  24. package/src/client/hooks/useTextareaAutosize.js +13 -0
  25. package/src/client/index.jsx +15 -1
  26. package/src/client/lib/check-availability.js +1 -0
  27. package/src/client/lib/message-notification.js +36 -0
  28. package/src/client/lib/transform-messages.js +35 -0
  29. package/src/client/store/AppProvider.jsx +139 -0
  30. package/src/client/store/actions-map.js +119 -0
  31. package/src/client/store/constants.js +3 -0
  32. package/src/client/store/reducer.js +28 -0
  33. package/src/client/store/useApp.js +5 -0
  34. package/src/client/store/useChatSdk.js +37 -0
  35. package/src/server/index.js +15 -6
  36. package/src/server/lib/client.js +31 -35
  37. package/src/server/lib/utils.js +18 -9
  38. package/src/client/lib/external-stores.js +0 -4
  39. package/src/client/lib/external-sync-store.js +0 -42
@@ -0,0 +1,67 @@
1
+ import React, { useState } from 'react'
2
+ import { PanelHeader } from '../panel/panel-header.jsx'
3
+
4
+ import { useApp } from '../../store/useApp.js'
5
+
6
+ export function Settings ({ onCancel }) {
7
+ const { settings, setSettings } = useApp()
8
+
9
+ const [optionAudio, setOptionAudio] = useState(settings.audio)
10
+ const [optionScroll, setOptionScroll] = useState(settings.scroll)
11
+
12
+ const onSave = e => {
13
+ e.preventDefault()
14
+
15
+ setSettings({ audio: optionAudio, scroll: optionScroll })
16
+ onCancel(e)
17
+ }
18
+
19
+ return (
20
+ <>
21
+ <PanelHeader />
22
+
23
+ <div className='wc-body'>
24
+ <fieldset className='govuk-fieldset govuk-!-margin-bottom-4'>
25
+ <legend className='govuk-fieldset__legend govuk-fieldset__legend'>
26
+ <h3 id='wc-subtitle' className='govuk-heading-s'>Change settings</h3>
27
+ </legend>
28
+ <div className='govuk-checkboxes govuk-checkboxes--small' data-module='govuk-checkboxes'>
29
+ <div className='govuk-checkboxes__item'>
30
+ <input
31
+ id='audio'
32
+ name='audio'
33
+ type='checkbox'
34
+ value='audio'
35
+ className='govuk-checkboxes__input'
36
+ defaultChecked={optionAudio}
37
+ onChange={() => setOptionAudio(!optionAudio)}
38
+ />
39
+ <label className='govuk-label govuk-checkboxes__label govuk-!-font-size-16' htmlFor='audio'>
40
+ Play a sound when receiving a new message
41
+ </label>
42
+ </div>
43
+ <div className='govuk-checkboxes__item'>
44
+ <input
45
+ id='scroll'
46
+ name='scroll'
47
+ type='checkbox'
48
+ value='scroll'
49
+ className='govuk-checkboxes__input'
50
+ defaultChecked={optionScroll}
51
+ onChange={() => setOptionScroll(!optionScroll)}
52
+ />
53
+ <label className='govuk-label govuk-checkboxes__label govuk-!-font-size-16' htmlFor='scroll'>
54
+ Scroll automatically to a new message
55
+ </label>
56
+ </div>
57
+ </div>
58
+ </fieldset>
59
+
60
+ <div className='govuk-button-group'>
61
+ <a id='settings-save' href='#' className='govuk-button govuk-!-font-size-16' data-module='govuk-button' onClick={onSave}>Save</a>
62
+ <a id='settings-cancel' href='#' className='govuk-link govuk-!-font-size-16' data-module='govuk-button' onClick={onCancel}>Cancel</a>
63
+ </div>
64
+ </div>
65
+ </>
66
+ )
67
+ }
@@ -0,0 +1,23 @@
1
+ import React from 'react'
2
+ import { PanelHeader } from '../panel/panel-header.jsx'
3
+
4
+ export function Unavailable () {
5
+ return (
6
+ <>
7
+ <PanelHeader />
8
+
9
+ <div className='wc-body'>
10
+ <h3 id='wc-subtitle' className='govuk-heading-m'>Webchat is currently not available</h3>
11
+ <p className='govuk-body-s'>Try again later, or call Floodline:</p>
12
+ <p className='govuk-body-s'>
13
+ <strong>Floodline helpline</strong>
14
+ <br />Telephone: 0345 988 1188
15
+ <br />Textphone: 0345 602 6340
16
+ <br />Open 24 hours a day, 7 days a week
17
+ <br /><a className='govuk-link' href='https://gov.uk/call-charges'>Find out more about call charges</a>
18
+ </p>
19
+ <p className='govuk-body-s'>We're running webchat as a trial, it will not always be available.</p>
20
+ </div>
21
+ </>
22
+ )
23
+ }
@@ -0,0 +1,71 @@
1
+ import { useState, useEffect, useCallback } from 'react'
2
+
3
+ const setAriaHidden = bool => {
4
+ for (const node of document.body.children) {
5
+ if (node.id !== 'wc-panel') {
6
+ (bool) ? node.setAttribute('aria-hidden', 'true') : node.removeAttribute('aria-hidden')
7
+ }
8
+ }
9
+ }
10
+
11
+ export const getFocusableElements = () => {
12
+ const selectors = [
13
+ '#wc-panel a:not([disabled])',
14
+ '#wc-panel button:not([disabled])',
15
+ '#wc-panel select:not([disabled])',
16
+ '#wc-panel input:not([disabled])',
17
+ '#wc-panel textarea:not([disabled])',
18
+ '#wc-panel *[tabindex="0"]:not([disabled])'
19
+ ]
20
+
21
+ const elements = document.body.querySelectorAll(selectors.join(','))
22
+ return Array.from(elements).filter(e => !e.closest('[hidden]') && !e.closest('[aria-hidden="true"]'))
23
+ }
24
+
25
+ const useFocusedElements = screen => {
26
+ const [panelElements, setPanelElements] = useState([])
27
+
28
+ const onKeyDown = useCallback(e => {
29
+ const webchatPanelElement = document.querySelector('#wc-panel')
30
+
31
+ if (e.key === 'Tab') {
32
+ if (webchatPanelElement && !document.activeElement.closest('#wc-panel')) {
33
+ webchatPanelElement.focus()
34
+ }
35
+
36
+ if (e.shiftKey) {
37
+ if (document.activeElement === panelElements[0]) {
38
+ panelElements[panelElements.length - 1].focus()
39
+ e.preventDefault()
40
+ } else if (document.activeElement === document.querySelector('#wc-panel')) {
41
+ panelElements[panelElements.length - 1]?.focus()
42
+ e.preventDefault()
43
+ }
44
+ } else if (document.activeElement === panelElements[panelElements.length - 1]) {
45
+ panelElements[0].focus()
46
+ e.preventDefault()
47
+ }
48
+ }
49
+ }, [panelElements])
50
+
51
+ useEffect(() => {
52
+ setPanelElements(getFocusableElements())
53
+ }, [screen])
54
+
55
+ useEffect(() => {
56
+ setAriaHidden(true)
57
+
58
+ const panelElement = document.querySelector('#wc-panel')
59
+ panelElement.focus()
60
+
61
+ document.addEventListener('keydown', onKeyDown)
62
+
63
+ return () => {
64
+ setAriaHidden()
65
+ document.body.querySelector('.wc-availability__link')?.focus()
66
+ document.removeEventListener('keydown', onKeyDown)
67
+ }
68
+ }, [panelElements])
69
+ }
70
+
71
+ export { useFocusedElements }
@@ -0,0 +1,13 @@
1
+ import { useEffect } from 'react'
2
+
3
+ export const useTextareaAutosize = (textAreaRef, value) => {
4
+ useEffect(() => {
5
+ if (textAreaRef) {
6
+ textAreaRef.style.height = '0px'
7
+
8
+ const scrollHeight = textAreaRef.scrollHeight
9
+
10
+ textAreaRef.style.height = `${scrollHeight}px`
11
+ }
12
+ }, [textAreaRef, value])
13
+ }
@@ -1,9 +1,19 @@
1
1
  import React from 'react'
2
2
  import { createRoot } from 'react-dom/client'
3
+ import { ChatSdk } from '@nice-devone/nice-cxone-chat-web-sdk'
3
4
  import { Availability } from './components/availability/availability.jsx'
4
5
  import { checkAvailability } from './lib/check-availability'
6
+ import { AppProvider } from './store/AppProvider.jsx'
7
+ import { CUSTOMER_ID_STORAGE_KEY } from './store/constants.js'
5
8
 
6
9
  export async function init (container, options) {
10
+ const sdk = new ChatSdk({
11
+ brandId: options.brandId,
12
+ channelId: options.channelId,
13
+ customerId: window.localStorage.getItem(CUSTOMER_ID_STORAGE_KEY) || '',
14
+ environment: options.environment
15
+ })
16
+
7
17
  const root = createRoot(container)
8
18
  let availability
9
19
  try {
@@ -12,5 +22,9 @@ export async function init (container, options) {
12
22
  } catch (e) {
13
23
  availability = 'UNAVAILABLE'
14
24
  }
15
- root.render(<Availability availability={availability} />)
25
+ root.render(
26
+ <AppProvider sdk={sdk} availability={availability} options={options}>
27
+ <Availability />
28
+ </AppProvider>
29
+ )
16
30
  }
@@ -1,6 +1,7 @@
1
1
  export async function checkAvailability (endpoint) {
2
2
  const response = await fetch(endpoint)
3
3
  const data = await response.json()
4
+
4
5
  return {
5
6
  availability: data.availability || 'UNAVAILABLE'
6
7
  }
@@ -0,0 +1,36 @@
1
+ export const messageNotification = audioUrl => {
2
+ let buffer
3
+
4
+ const context = new (window.AudioContext || window.webkitAudioContext)()
5
+
6
+ if (context.state === 'suspended') {
7
+ const events = ['touchstart', 'touchend', 'mousedown', 'wheel', 'keydown', 'click']
8
+
9
+ const unlock = _e => {
10
+ events.forEach(event => {
11
+ document.body.removeEventListener(event, unlock)
12
+ })
13
+
14
+ context.resume()
15
+ }
16
+
17
+ events.forEach(event => {
18
+ document.body.addEventListener(event, unlock, false)
19
+ })
20
+ }
21
+
22
+ fetch(audioUrl)
23
+ .then(response => response.arrayBuffer())
24
+ .then(data => context.decodeAudioData(data))
25
+ .then(decodedData => {
26
+ buffer = decodedData
27
+ })
28
+ .catch(console.error)
29
+
30
+ return () => {
31
+ const source = context.createBufferSource()
32
+ source.buffer = buffer
33
+ source.connect(context.destination)
34
+ source.start()
35
+ }
36
+ }
@@ -0,0 +1,35 @@
1
+ import * as uuid from 'uuid'
2
+ import { DateTime } from 'luxon'
3
+
4
+ export const transformMessage = message => {
5
+ return {
6
+ id: message.id,
7
+ text: message.messageContent?.text,
8
+ createdAt: new Date(message.createdAt),
9
+ user: message.authorEndUserIdentity?.fullName?.trim() || null,
10
+ assignee: message.authorUser?.firstName || null,
11
+ direction: message.direction
12
+ }
13
+ }
14
+
15
+ export const transformMessages = messages => messages.map(message => transformMessage(message))
16
+
17
+ export const formatTranscript = messages => {
18
+ const now = DateTime.local()
19
+ now.setZone('Europe/London')
20
+
21
+ let string = `Floodline webchat transcript, ${now.toFormat('HH:mm:ss a, dd LLLL yyyy')}, (ID: ${uuid.v4().split('-')[0]})\n\n`
22
+
23
+ for (const message of messages) {
24
+ const { text, direction, user, assignee, createdAt } = message
25
+
26
+ const author = direction === 'inbound' ? user : `${assignee} (Floodline adviser)`
27
+ const date = DateTime.fromJSDate(new Date(createdAt)).setZone('Europe/London').toFormat('HH:mm:ss a, dd LLLL yyyy')
28
+
29
+ string += `[${date}] ${author}: \n${text}\n\n`
30
+ }
31
+
32
+ string = string.replace(/<a\b[^>]*>/i, '').replace(/<\/a>/i, '')
33
+
34
+ return encodeURIComponent(string)
35
+ }
@@ -0,0 +1,139 @@
1
+ import React, { createContext, useEffect, useReducer, useMemo } from 'react'
2
+ import { ChatEvent } from '@nice-devone/nice-cxone-chat-web-sdk'
3
+
4
+ import { messageNotification } from '../lib/message-notification.js'
5
+
6
+ import { initialState, reducer } from './reducer.js'
7
+ import { CUSTOMER_ID_STORAGE_KEY, THREAD_ID_STORAGE_KEY, SETTINGS_STORAGE_KEY } from './constants.js'
8
+
9
+ export const AppContext = createContext(initialState)
10
+
11
+ export const AppProvider = ({ sdk, availability, options, children }) => {
12
+ const [state, dispatch] = useReducer(reducer, initialState)
13
+
14
+ const playSound = messageNotification(options.audioUrl)
15
+
16
+ /**
17
+ * SDK event handlers
18
+ */
19
+ const onLiveChatRecovered = e => {
20
+ dispatch({ type: 'SET_AGENT', payload: e.detail.data.inboxAssignee })
21
+ dispatch({ type: 'SET_AGENT_STATUS', payload: e.detail.data.contact.status })
22
+ dispatch({ type: 'SET_UNSEEN_COUNT', payload: e.detail.data.contact.customerStatistics.unseenMessagesCount })
23
+ }
24
+
25
+ const onAssignedAgentChanged = e => {
26
+ dispatch({ type: 'SET_AGENT', payload: e.detail.data.inboxAssignee })
27
+ dispatch({ type: 'SET_AGENT_STATUS', payload: e.detail.data.case.status })
28
+ }
29
+
30
+ const onAgentTypingStarted = () => {
31
+ dispatch({ type: 'SET_AGENT_TYPING', payload: true })
32
+ }
33
+
34
+ const onAgentTypingEnded = () => {
35
+ dispatch({ type: 'SET_AGENT_TYPING', payload: false })
36
+ }
37
+
38
+ const onMessageCreated = e => {
39
+ dispatch({ type: 'SET_MESSAGE', payload: e.detail.data.message })
40
+ dispatch({ type: 'SET_AGENT_STATUS', payload: e.detail.data.case.status })
41
+ dispatch({ type: 'SET_UNSEEN_COUNT', payload: e.detail.data.case.customerStatistics.unseenMessagesCount })
42
+
43
+ const isAudioOn = JSON.parse(window.localStorage.getItem(SETTINGS_STORAGE_KEY)).audio
44
+
45
+ if (isAudioOn && e.detail.data.message.direction === 'outbound') {
46
+ playSound()
47
+ }
48
+ }
49
+
50
+ const onContactStatusChanged = e => {
51
+ dispatch({ type: 'SET_AGENT_STATUS', payload: e.detail.data.case.status })
52
+ }
53
+
54
+ useEffect(() => {
55
+ sdk.onChatEvent(ChatEvent.LIVECHAT_RECOVERED, onLiveChatRecovered)
56
+ sdk.onChatEvent(ChatEvent.MESSAGE_CREATED, onMessageCreated)
57
+ sdk.onChatEvent(ChatEvent.AGENT_TYPING_STARTED, onAgentTypingStarted)
58
+ sdk.onChatEvent(ChatEvent.AGENT_TYPING_ENDED, onAgentTypingEnded)
59
+ sdk.onChatEvent(ChatEvent.ASSIGNED_AGENT_CHANGED, onAssignedAgentChanged)
60
+ sdk.onChatEvent(ChatEvent.CONTACT_STATUS_CHANGED, onContactStatusChanged)
61
+ }, [sdk])
62
+
63
+ /**
64
+ * Initialize customerId, threadId and whether the webchat should be open
65
+ */
66
+ useEffect(() => {
67
+ dispatch({ type: 'SET_AVAILABILITY', payload: availability })
68
+
69
+ setCustomerId(window.localStorage.getItem(CUSTOMER_ID_STORAGE_KEY))
70
+ setThreadId(window.localStorage.getItem(THREAD_ID_STORAGE_KEY))
71
+ setSettings(JSON.parse(window.localStorage.getItem(SETTINGS_STORAGE_KEY)) || state.settings)
72
+
73
+ if (window.location.hash === '#webchat') {
74
+ setChatVisibility(true)
75
+ }
76
+ }, [])
77
+
78
+ /**
79
+ * State update functions
80
+ */
81
+ const setChatVisibility = payload => {
82
+ if (!payload) {
83
+ window.location.hash = ''
84
+ }
85
+
86
+ dispatch({ type: 'SET_CHAT_VISIBILITY', payload })
87
+ }
88
+
89
+ const setCustomerId = customerId => {
90
+ dispatch({ type: 'SET_CUSTOMER_ID', payload: customerId })
91
+ }
92
+
93
+ const setThreadId = threadId => {
94
+ dispatch({ type: 'SET_THREAD_ID', payload: threadId })
95
+ }
96
+
97
+ const setThread = thread => {
98
+ dispatch({ type: 'SET_THREAD', payload: thread })
99
+ }
100
+
101
+ const setMessages = messages => {
102
+ dispatch({ type: 'SET_MESSAGES', payload: messages })
103
+ }
104
+
105
+ const setSettings = data => {
106
+ dispatch({ type: 'SET_SETTINGS', payload: data })
107
+ }
108
+
109
+ const setUnseenCount = unseenCount => {
110
+ dispatch({ type: 'SET_UNSEEN_COUNT', payload: unseenCount })
111
+ }
112
+
113
+ /**
114
+ * Application-wide state and state functions
115
+ */
116
+ const store = useMemo(() => ({
117
+ ...state,
118
+ sdk,
119
+ setSettings,
120
+ setCustomerId,
121
+ setThreadId,
122
+ setThread,
123
+ setMessages,
124
+ setUnseenCount,
125
+ setChatVisibility,
126
+ onLiveChatRecovered,
127
+ onAssignedAgentChanged,
128
+ onAgentTypingStarted,
129
+ onAgentTypingEnded,
130
+ onMessageCreated,
131
+ onContactStatusChanged
132
+ }))
133
+
134
+ return (
135
+ <AppContext.Provider value={store}>
136
+ {children}
137
+ </AppContext.Provider>
138
+ )
139
+ }
@@ -0,0 +1,119 @@
1
+ import { transformMessages, transformMessage } from '../lib/transform-messages'
2
+ import { CUSTOMER_ID_STORAGE_KEY, THREAD_ID_STORAGE_KEY, SETTINGS_STORAGE_KEY } from './constants'
3
+
4
+ const setSettings = (state, payload) => {
5
+ if (payload) {
6
+ window.localStorage.setItem(SETTINGS_STORAGE_KEY, JSON.stringify(payload))
7
+ } else {
8
+ window.localStorage.removeItem(SETTINGS_STORAGE_KEY)
9
+ }
10
+
11
+ return {
12
+ ...state,
13
+ settings: payload
14
+ }
15
+ }
16
+
17
+ const setCustomerId = (state, payload) => {
18
+ if (payload) {
19
+ window.localStorage.setItem(CUSTOMER_ID_STORAGE_KEY, payload)
20
+ } else {
21
+ window.localStorage.removeItem(CUSTOMER_ID_STORAGE_KEY)
22
+ }
23
+
24
+ return {
25
+ ...state,
26
+ customerId: payload
27
+ }
28
+ }
29
+
30
+ const setThreadId = (state, payload) => {
31
+ if (payload) {
32
+ window.localStorage.setItem(THREAD_ID_STORAGE_KEY, payload)
33
+ } else {
34
+ window.localStorage.removeItem(THREAD_ID_STORAGE_KEY)
35
+ }
36
+
37
+ return {
38
+ ...state,
39
+ threadId: payload
40
+ }
41
+ }
42
+
43
+ const setChatVisibility = (state, payload) => {
44
+ return {
45
+ ...state,
46
+ isChatOpen: payload
47
+ }
48
+ }
49
+
50
+ const setAvailability = (state, payload) => {
51
+ return {
52
+ ...state,
53
+ availability: payload
54
+ }
55
+ }
56
+
57
+ const setThread = (state, payload) => {
58
+ return {
59
+ ...state,
60
+ thread: payload
61
+ }
62
+ }
63
+
64
+ const setMessage = (state, payload) => {
65
+ return {
66
+ ...state,
67
+ messages: [...state.messages, transformMessage(payload)]
68
+ }
69
+ }
70
+
71
+ const setMessages = (state, payload) => {
72
+ return {
73
+ ...state,
74
+ messages: transformMessages(payload).reverse()
75
+ }
76
+ }
77
+
78
+ const setAgent = (state, payload) => {
79
+ return {
80
+ ...state,
81
+ agent: payload
82
+ }
83
+ }
84
+
85
+ const setAgentIsTyping = (state, payload) => {
86
+ return {
87
+ ...state,
88
+ isAgentTyping: payload
89
+ }
90
+ }
91
+
92
+ const setAgentStatus = (state, payload) => {
93
+ return {
94
+ ...state,
95
+ agentStatus: payload
96
+ }
97
+ }
98
+
99
+ const setUnseenCount = (state, payload) => {
100
+ return {
101
+ ...state,
102
+ unseenCount: payload
103
+ }
104
+ }
105
+
106
+ export const actionsMap = {
107
+ SET_CHAT_VISIBILITY: setChatVisibility,
108
+ SET_AVAILABILITY: setAvailability,
109
+ SET_SETTINGS: setSettings,
110
+ SET_CUSTOMER_ID: setCustomerId,
111
+ SET_THREAD_ID: setThreadId,
112
+ SET_THREAD: setThread,
113
+ SET_MESSAGE: setMessage,
114
+ SET_MESSAGES: setMessages,
115
+ SET_AGENT: setAgent,
116
+ SET_AGENT_TYPING: setAgentIsTyping,
117
+ SET_AGENT_STATUS: setAgentStatus,
118
+ SET_UNSEEN_COUNT: setUnseenCount
119
+ }
@@ -0,0 +1,3 @@
1
+ export const SETTINGS_STORAGE_KEY = 'webchat_settings'
2
+ export const CUSTOMER_ID_STORAGE_KEY = 'webchat_customer_id'
3
+ export const THREAD_ID_STORAGE_KEY = 'webchat_thread_id'
@@ -0,0 +1,28 @@
1
+ import { actionsMap } from './actions-map'
2
+
3
+ export const initialState = {
4
+ availability: null,
5
+ customerId: null,
6
+ threadId: null,
7
+ thread: null,
8
+ messages: [],
9
+ unseenCount: 0,
10
+ agent: null,
11
+ agentStatus: null,
12
+ isAgentTyping: false,
13
+ isChatOpen: false,
14
+ settings: { audio: true, scroll: true }
15
+ }
16
+
17
+ export const reducer = (state, action) => {
18
+ const { type, payload } = action
19
+
20
+ const fn = actionsMap[type]
21
+
22
+ if (fn) {
23
+ const actionFunction = fn.bind(this, state, payload)
24
+ return actionFunction()
25
+ }
26
+
27
+ return state
28
+ }
@@ -0,0 +1,5 @@
1
+ import { useContext } from 'react'
2
+
3
+ import { AppContext } from './AppProvider.jsx'
4
+
5
+ export const useApp = () => useContext(AppContext)
@@ -0,0 +1,37 @@
1
+ export const useChatSdk = sdk => {
2
+ const connect = async () => sdk.authorize()
3
+
4
+ const fetchCustomerId = async () => {
5
+ const response = await connect()
6
+ return response?.consumerIdentity.idOnExternalPlatform
7
+ }
8
+
9
+ const fetchThread = async threadId => {
10
+ await connect()
11
+ return sdk.getThread(threadId)
12
+ }
13
+
14
+ const fetchMessages = async (thread, threadId) => {
15
+ await connect()
16
+
17
+ const recovered = await thread.recover(threadId)
18
+
19
+ const allMessages = []
20
+ let fetchedMessages = recovered.messages
21
+
22
+ while (fetchedMessages.length) {
23
+ fetchedMessages.map(msg => allMessages.push(msg))
24
+
25
+ try {
26
+ const response = await thread.loadMoreMessages()
27
+ fetchedMessages = response.data.messages
28
+ } catch (err) {
29
+ fetchedMessages = []
30
+ }
31
+ }
32
+
33
+ return allMessages
34
+ }
35
+
36
+ return { connect, fetchCustomerId, fetchThread, fetchMessages }
37
+ }
@@ -1,4 +1,4 @@
1
- const { authenticate, getHost, getIsOpen, getActivity } = require('./lib/client.js')
1
+ const { authenticate, getApiBaseUrl, getIsOpen, getActivity } = require('./lib/client.js')
2
2
 
3
3
  /**
4
4
  * Returns webchat availability
@@ -9,6 +9,8 @@ const { authenticate, getHost, getIsOpen, getActivity } = require('./lib/client.
9
9
  * @param options.accessSecret {string}
10
10
  * @param options.skillEndpoint {string}
11
11
  * @param options.hoursEndpoint {string}
12
+ * @param options.authenticationUri {string}
13
+ * @param options.wellKnownUri {string}
12
14
  * @param options.maxQueueCount {string}
13
15
  * @returns {Promise<{date: Date, availability: (string)}>}
14
16
  */
@@ -17,20 +19,27 @@ module.exports = async function getAvailability ({
17
19
  clientSecret,
18
20
  accessKey,
19
21
  accessSecret,
22
+ maxQueueCount,
20
23
  skillEndpoint,
21
24
  hoursEndpoint,
22
- maxQueueCount
25
+ wellKnownUri = 'https://cxone.niceincontact.com/.well-known/cxone-configuration',
26
+ authenticationUri = 'https://cxone.niceincontact.com/auth/token'
23
27
  }) {
24
28
  const authorisation = 'Basic ' + Buffer.from(`${encodeURIComponent(clientId)}:${encodeURIComponent(clientSecret)}`).toString('base64')
25
29
 
26
30
  // Cache authentication and re-authenticate when needed (lasts 1 hour?)
27
- const { tenantId, token, tokenType } = await authenticate({ authorisation, accessKey, accessSecret })
31
+ const { tenantId, token, tokenType } = await authenticate({
32
+ authenticationUri,
33
+ authorisation,
34
+ accessKey,
35
+ accessSecret
36
+ })
28
37
 
29
- const host = await getHost({ tenantId })
38
+ const apiBaseUrl = await getApiBaseUrl({ wellKnownUri, tenantId })
30
39
 
31
40
  const [{ hasCapacity, hasAgentsAvailable }, isOpen] = await Promise.all([
32
- getActivity({ tokenType, token, host, skillEndpoint, maxQueueCount }),
33
- getIsOpen({ token, tokenType, host, hoursEndpoint })
41
+ getActivity({ baseUrl: apiBaseUrl, tokenType, token, skillEndpoint, maxQueueCount }),
42
+ getIsOpen({ baseUrl: apiBaseUrl, token, tokenType, hoursEndpoint })
34
43
  ])
35
44
 
36
45
  const isAvailable = isOpen && hasAgentsAvailable && hasCapacity