diff --git a/components/ChatWindow.tsx b/components/ChatWindow.tsx index 8fc3210..6955087 100644 --- a/components/ChatWindow.tsx +++ b/components/ChatWindow.tsx @@ -12,8 +12,9 @@ import { shorten, shouldActivateGameMode, setupPostMessageHandlers, + debounce, } from '../helpers/utils'; -import {Message} from '../helpers/types'; +import {Config, Message} from '../helpers/types'; import {isDev, getWebsocketUrl} from '../helpers/config'; import Logger from '../helpers/logger'; import { @@ -47,12 +48,14 @@ type State = { customerId: string; conversationId: string | null; availableAgents: Array; + isLoaded: boolean; isSending: boolean; isOpen: boolean; isTransitioning: boolean; isGameMode?: boolean; shouldDisplayNotifications: boolean; shouldDisplayBranding: boolean; + forceRequireEmailUpfront?: boolean; }; class ChatWindow extends React.Component { @@ -74,12 +77,14 @@ class ChatWindow extends React.Component { customerId: null, availableAgents: [], conversationId: null, + isLoaded: false, isSending: false, isOpen: false, isTransitioning: false, isGameMode: false, shouldDisplayNotifications: false, shouldDisplayBranding: false, + forceRequireEmailUpfront: false, }; } @@ -114,6 +119,7 @@ class ChatWindow extends React.Component { await this.fetchLatestConversation(customerId, metadata); this.emit('chat:loaded'); + this.setState({isLoaded: true}); if (this.isOnDeprecatedVersion()) { console.warn('You are currently on a deprecated version of Papercups.'); @@ -130,6 +136,31 @@ class ChatWindow extends React.Component { }); } + async componentDidUpdate(prevProps: Props) { + if (!this.state.isLoaded) { + return; + } + + const {greeting, shouldRequireEmail} = this.props; + + if (greeting !== prevProps.greeting) { + this.debouncedHandleGreetingUpdated(greeting, prevProps.greeting); + } + + // FIXME: how can we force email to show up correctly during the preview mode??? + if (shouldRequireEmail !== prevProps.shouldRequireEmail) { + this.setState({forceRequireEmailUpfront: shouldRequireEmail}); + } + } + + debouncedHandleGreetingUpdated = debounce(async () => { + const {customerId: cachedCustomerId, customer: metadata} = this.props; + const isValidCustomer = await this.isValidCustomer(cachedCustomerId); + const customerId = isValidCustomer ? cachedCustomerId : null; + + await this.fetchLatestConversation(customerId, metadata); + }, 800); + emit = (event: string, payload?: any) => { this.logger.debug('Sending event from iframe:', {event, payload}); @@ -155,6 +186,8 @@ class ChatWindow extends React.Component { return this.handlePapercupsPlan(payload); case 'papercups:ping': return this.logger.debug('Pong!'); + case 'config:update': + return this.logger.debug('Config updated dynamically:', payload); default: return null; } @@ -692,6 +725,10 @@ class ChatWindow extends React.Component { // If this is true, we don't allow the customer to send any messages // until they enter an email address in the chat widget. askForEmailUpfront = () => { + if (this.state.forceRequireEmailUpfront) { + return true; + } + const {customer, shouldRequireEmail} = this.props; const {customerId, messages = []} = this.state; @@ -722,14 +759,7 @@ class ChatWindow extends React.Component { }; isOnDeprecatedVersion = (): boolean => { - const {accountId, version = '1.0.0'} = this.props; - - // TODO: remove after testing - if (accountId === '873f5102-d267-4b09-9de0-d6e741e0e076') { - return false; - } - - return version < '1.1.2'; + return this.props.version < '1.1.2'; }; renderEmbeddedGame() { diff --git a/components/Widget.tsx b/components/Widget.tsx index 3fca787..f644ead 100644 --- a/components/Widget.tsx +++ b/components/Widget.tsx @@ -6,29 +6,7 @@ import {isDev} from '../helpers/config'; import {setupPostMessageHandlers} from '../helpers/utils'; import getThemeConfig from '../helpers/theme'; import Logger from '../helpers/logger'; - -type Config = { - title?: string; - subtitle?: string; - primaryColor?: string; - accountId?: string; - baseUrl?: string; - greeting?: string; - customerId?: string; - newMessagePlaceholder?: string; - emailInputPlaceholder?: string; - newMessagesNotificationText?: string; - companyName?: string; - agentAvailableText?: string; - agentUnavailableText?: string; - showAgentAvailability?: boolean; - defaultIsOpen?: boolean; - requireEmailUpfront?: boolean; - closeable?: boolean; - mobile?: boolean; - metadata?: string; // stringified CustomerMetadata JSON - version?: string; -}; +import {Config} from '../helpers/types'; const parseCustomerMetadata = (str: string): CustomerMetadata => { try { @@ -54,6 +32,7 @@ const sanitizeConfigPayload = (payload: any): Config => { newMessagePlaceholder, emailInputPlaceholder, newMessagesNotificationText, + requireEmailUpfront, agentAvailableText, agentUnavailableText, showAgentAvailability, @@ -61,7 +40,7 @@ const sanitizeConfigPayload = (payload: any): Config => { version, } = payload; - return { + const updates = { accountId, title, subtitle, @@ -72,12 +51,23 @@ const sanitizeConfigPayload = (payload: any): Config => { newMessagePlaceholder, emailInputPlaceholder, newMessagesNotificationText, + requireEmailUpfront, agentAvailableText, agentUnavailableText, showAgentAvailability, closeable, version, }; + + return Object.keys(updates).reduce((acc, key) => { + const value = updates[key]; + + if (typeof value === 'undefined') { + return acc; + } + + return {...acc, [key]: value}; + }, {}); }; type Props = {config: Config}; @@ -161,7 +151,7 @@ class Wrapper extends React.Component { const shouldRequireEmail = !!Number(requireEmailUpfront); const isMobile = !!Number(mobile); const isCloseable = !!Number(closeable); - const shouldHideAvailability = !!Number(showAgentAvailability); + const shouldShowAvailability = !!Number(showAgentAvailability); const theme = getThemeConfig({primary: primaryColor}); const customer = parseCustomerMetadata(metadata); @@ -179,7 +169,7 @@ class Wrapper extends React.Component { newMessagesNotificationText={newMessagesNotificationText} agentAvailableText={agentAvailableText} agentUnavailableText={agentUnavailableText} - showAgentAvailability={shouldHideAvailability} + showAgentAvailability={shouldShowAvailability} shouldRequireEmail={shouldRequireEmail} isMobile={isMobile} isCloseable={isCloseable} diff --git a/helpers/types.ts b/helpers/types.ts index b22edd7..73e735c 100644 --- a/helpers/types.ts +++ b/helpers/types.ts @@ -27,3 +27,26 @@ export type Attachment = { file_url: string; content_type: string; }; + +export type Config = { + title?: string; + subtitle?: string; + primaryColor?: string; + accountId?: string; + baseUrl?: string; + greeting?: string; + customerId?: string; + newMessagePlaceholder?: string; + emailInputPlaceholder?: string; + newMessagesNotificationText?: string; + companyName?: string; + agentAvailableText?: string; + agentUnavailableText?: string; + showAgentAvailability?: boolean; + defaultIsOpen?: boolean; + requireEmailUpfront?: boolean; + closeable?: boolean; + mobile?: boolean; + metadata?: string; // stringified CustomerMetadata JSON + version?: string; +}; diff --git a/helpers/utils.ts b/helpers/utils.ts index 84dc2d2..1659311 100644 --- a/helpers/utils.ts +++ b/helpers/utils.ts @@ -60,3 +60,13 @@ export function setupPostMessageHandlers(w: any, handler: (msg: any) => void) { return () => w.detachEvent('message', cb); } } + +export function debounce(fn: any, wait: number) { + let timeout: any; + + return function (...args: any) { + clearTimeout(timeout); + + timeout = setTimeout(() => fn.call(this, ...args), wait); + }; +}