Skip to content

Commit 7678ab8

Browse files
committed
Mess around with using react context to refactor config (WIP)
1 parent 71e3d1d commit 7678ab8

File tree

6 files changed

+294
-191
lines changed

6 files changed

+294
-191
lines changed

components/ChatWindow.tsx

+5-5
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import {
1313
shouldActivateGameMode,
1414
setupPostMessageHandlers,
1515
} from '../helpers/utils';
16-
import {Message} from '../helpers/types';
16+
import {CustomerMetadata, Message} from '../helpers/types';
1717
import {isDev, getWebsocketUrl} from '../helpers/config';
1818
import Logger from '../helpers/logger';
1919
import {
@@ -33,7 +33,7 @@ type Props = {
3333
newMessagesNotificationText?: string;
3434
shouldRequireEmail?: boolean;
3535
isMobile?: boolean;
36-
customer?: API.CustomerMetadata;
36+
customer?: CustomerMetadata;
3737
companyName?: string;
3838
agentAvailableText?: string;
3939
agentUnavailableText?: string;
@@ -320,7 +320,7 @@ class ChatWindow extends React.Component<Props, State> {
320320
// Check if we have a matching customer based on the `external_id` provided
321321
// in the customer metadata. Otherwise, fallback to the cached customer id.
322322
checkForExistingCustomer = async (
323-
metadata?: API.CustomerMetadata,
323+
metadata?: CustomerMetadata,
324324
defaultCustomerId?: string | null
325325
): Promise<string | null> => {
326326
if (!metadata || !metadata?.external_id) {
@@ -365,7 +365,7 @@ class ChatWindow extends React.Component<Props, State> {
365365
// until the customer initiates the first message to create the conversation.
366366
fetchLatestConversation = async (
367367
cachedCustomerId?: string | null,
368-
metadata?: API.CustomerMetadata
368+
metadata?: CustomerMetadata
369369
) => {
370370
const customerId = await this.checkForExistingCustomer(
371371
metadata,
@@ -477,7 +477,7 @@ class ChatWindow extends React.Component<Props, State> {
477477
// to make it easier to identify customers in the dashboard.
478478
updateExistingCustomer = async (
479479
customerId: string,
480-
metadata?: API.CustomerMetadata
480+
metadata?: CustomerMetadata
481481
) => {
482482
if (!metadata) {
483483
return;

components/ConfigProvider.tsx

+229
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,229 @@
1+
import React, {useContext} from 'react';
2+
import Logger from '../helpers/logger';
3+
import {CustomerMetadata} from '../helpers/types';
4+
import {isDev} from '../helpers/config';
5+
import {setupPostMessageHandlers} from '../helpers/utils';
6+
7+
type StringifiedConfig = {
8+
title?: string;
9+
subtitle?: string;
10+
primaryColor?: string;
11+
accountId?: string;
12+
baseUrl?: string;
13+
greeting?: string;
14+
customerId?: string;
15+
newMessagePlaceholder?: string;
16+
emailInputPlaceholder?: string;
17+
newMessagesNotificationText?: string;
18+
companyName?: string;
19+
agentAvailableText?: string;
20+
agentUnavailableText?: string;
21+
showAgentAvailability?: '0' | '1';
22+
defaultIsOpen?: '0' | '1';
23+
requireEmailUpfront?: '0' | '1';
24+
closeable?: '0' | '1';
25+
mobile?: '0' | '1';
26+
metadata?: string; // stringified CustomerMetadata JSON
27+
version?: string;
28+
};
29+
30+
export type Config = {
31+
accountId: string;
32+
customerId?: string;
33+
title?: string;
34+
subtitle?: string;
35+
primaryColor?: string;
36+
baseUrl?: string;
37+
greeting?: string;
38+
newMessagePlaceholder?: string;
39+
emailInputPlaceholder?: string;
40+
newMessagesNotificationText?: string;
41+
shouldRequireEmail?: boolean;
42+
isMobile?: boolean;
43+
customer?: CustomerMetadata;
44+
companyName?: string;
45+
agentAvailableText?: string;
46+
agentUnavailableText?: string;
47+
showAgentAvailability?: boolean;
48+
isCloseable?: boolean;
49+
version?: string;
50+
};
51+
52+
const parseCustomerMetadata = (str: string): CustomerMetadata => {
53+
try {
54+
return JSON.parse(str);
55+
} catch (err) {
56+
return {} as CustomerMetadata;
57+
}
58+
};
59+
60+
const sanitizeConfigPayload = (
61+
payload?: Record<string, any> | null
62+
): Partial<StringifiedConfig> => {
63+
if (!payload) {
64+
return {};
65+
}
66+
67+
const {
68+
accountId,
69+
title,
70+
subtitle,
71+
primaryColor,
72+
baseUrl,
73+
greeting,
74+
companyName,
75+
newMessagePlaceholder,
76+
emailInputPlaceholder,
77+
newMessagesNotificationText,
78+
agentAvailableText,
79+
agentUnavailableText,
80+
showAgentAvailability,
81+
closeable,
82+
version,
83+
} = payload;
84+
85+
return {
86+
accountId,
87+
title,
88+
subtitle,
89+
primaryColor,
90+
baseUrl,
91+
greeting,
92+
companyName,
93+
newMessagePlaceholder,
94+
emailInputPlaceholder,
95+
newMessagesNotificationText,
96+
agentAvailableText,
97+
agentUnavailableText,
98+
showAgentAvailability,
99+
closeable,
100+
version,
101+
};
102+
};
103+
104+
const parseStringifiedConfig = (payload: StringifiedConfig): Config => {
105+
const {
106+
accountId,
107+
customerId,
108+
greeting,
109+
companyName,
110+
agentAvailableText,
111+
agentUnavailableText,
112+
title = 'Welcome!',
113+
subtitle = 'How can we help you?',
114+
newMessagePlaceholder = 'Start typing...',
115+
emailInputPlaceholder = 'Enter your email',
116+
newMessagesNotificationText = 'View new messages',
117+
primaryColor = '1890ff',
118+
baseUrl = 'https://app.papercups.io',
119+
requireEmailUpfront = '0',
120+
showAgentAvailability = '0',
121+
closeable = '1',
122+
mobile = '0',
123+
metadata = '{}',
124+
version = '1.0.0',
125+
} = payload;
126+
127+
return {
128+
title,
129+
subtitle,
130+
accountId,
131+
customerId,
132+
greeting,
133+
primaryColor,
134+
companyName,
135+
newMessagePlaceholder,
136+
emailInputPlaceholder,
137+
newMessagesNotificationText,
138+
agentAvailableText,
139+
agentUnavailableText,
140+
baseUrl,
141+
version,
142+
shouldRequireEmail: !!Number(requireEmailUpfront),
143+
isMobile: !!Number(mobile),
144+
isCloseable: !!Number(closeable),
145+
showAgentAvailability: !!Number(showAgentAvailability),
146+
customer: parseCustomerMetadata(metadata),
147+
};
148+
};
149+
150+
export const ConfigurationContext = React.createContext<{
151+
config: Config;
152+
logger: Logger;
153+
}>({
154+
config: null,
155+
logger: null,
156+
});
157+
158+
export const useConfig = () => useContext(ConfigurationContext);
159+
160+
type Props = React.PropsWithChildren<{config: StringifiedConfig}>;
161+
type State = {config: StringifiedConfig};
162+
163+
class ConfigProvider extends React.Component<Props, State> {
164+
logger: Logger;
165+
unsubscribe: () => void;
166+
167+
constructor(props: Props) {
168+
super(props);
169+
170+
this.state = {
171+
config: props.config,
172+
};
173+
}
174+
175+
componentDidMount() {
176+
// TODO: make it possible to opt into debug mode
177+
const debugModeEnabled = isDev(window);
178+
179+
this.logger = new Logger(debugModeEnabled);
180+
this.unsubscribe = setupPostMessageHandlers(
181+
window,
182+
this.postMessageHandlers
183+
);
184+
}
185+
186+
componentWillUnmount() {
187+
this.unsubscribe && this.unsubscribe();
188+
}
189+
190+
postMessageHandlers = (msg: any) => {
191+
this.logger.debug('Handling in wrapper:', msg.data);
192+
const {event, payload = {}} = msg.data;
193+
194+
switch (event) {
195+
case 'config:update':
196+
return this.handleConfigUpdate(payload);
197+
default:
198+
return null;
199+
}
200+
};
201+
202+
handleConfigUpdate = (payload: any) => {
203+
const updates = sanitizeConfigPayload(payload);
204+
this.logger.debug('Updating widget config:', updates);
205+
206+
this.setState({config: {...this.state.config, ...updates}});
207+
};
208+
209+
render() {
210+
const {config = {}} = this.state;
211+
212+
if (Object.keys(config).length === 0) {
213+
return null;
214+
}
215+
216+
return (
217+
<ConfigurationContext.Provider
218+
value={{
219+
config: parseStringifiedConfig(config),
220+
logger: this.logger,
221+
}}
222+
>
223+
{this.props.children}
224+
</ConfigurationContext.Provider>
225+
);
226+
}
227+
}
228+
229+
export default ConfigProvider;

0 commit comments

Comments
 (0)