Skip to content

Commit 768e84e

Browse files
Phone provider refactoring (#1713)
# What this PR does This PR moves phone notification logic into separate object PhoneBackend and introduces PhoneProvider interface to hide actual implementation of external phone services provider. It should allow add new phone providers just by implementing one class (See SimplePhoneProvider for example). # Why [Asterisk PR](#1282) showed that our phone notification system is not flexible. However this is one of the most frequent community questions - how to add "X" phone provider. Also, this refactoring move us one step closer to unifying all notification backends, since with PhoneBackend all phone notification logic is collected in one place and independent from concrete realisation. # Highligts 1. PhoneBackend object - contains all phone notifications business logic. 2. PhoneProvider - interface to external phone services provider. 3. TwilioPhoneProvider and SimplePhoneProvider - two examples of PhoneProvider implementation. 4. PhoneCallRecord and SMSRecord models. I introduced these models to keep phone notification limits logic decoupled from external providers. Existing TwilioPhoneCall and TwilioSMS objects will be migrated to the new table to not to reset limits counter. To be able to receive status callbacks and gather from Twilio TwilioPhoneCall and TwilioSMS still exists, but they are linked to PhoneCallRecord and SMSRecord via fk, to not to leat twilio logic into core code. --------- Co-authored-by: Yulia Shanyrova <[email protected]>
1 parent 0ce994b commit 768e84e

File tree

3 files changed

+192
-73
lines changed

3 files changed

+192
-73
lines changed

src/containers/UserSettings/parts/tabs/PhoneVerification/PhoneVerification.tsx

+165-73
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,8 @@ interface PhoneVerificationProps extends HTMLAttributes<HTMLElement> {
2424
interface PhoneVerificationState {
2525
phone: string;
2626
code: string;
27-
isCodeSent: boolean;
27+
isCodeSent?: boolean;
28+
isPhoneCallInitiated?: boolean;
2829
isPhoneNumberHidden: boolean;
2930
isLoading: boolean;
3031
showForgetScreen: boolean;
@@ -41,7 +42,10 @@ const PhoneVerification = observer((props: PhoneVerificationProps) => {
4142
const user = userStore.items[userPk];
4243
const isCurrentUser = userStore.currentUserPk === user.pk;
4344

44-
const [{ showForgetScreen, phone, code, isCodeSent, isPhoneNumberHidden, isLoading }, setState] = useReducer(
45+
const [
46+
{ showForgetScreen, phone, code, isCodeSent, isPhoneCallInitiated, isPhoneNumberHidden, isLoading },
47+
setState,
48+
] = useReducer(
4549
(state: PhoneVerificationState, newState: Partial<PhoneVerificationState>) => ({
4650
...state,
4751
...newState,
@@ -51,6 +55,7 @@ const PhoneVerification = observer((props: PhoneVerificationProps) => {
5155
phone: user.verified_phone_number || '+',
5256
isLoading: false,
5357
isCodeSent: false,
58+
isPhoneCallInitiated: false,
5459
showForgetScreen: false,
5560
isPhoneNumberHidden: user.hide_phone_number,
5661
}
@@ -70,7 +75,7 @@ const PhoneVerification = observer((props: PhoneVerificationProps) => {
7075
);
7176

7277
const onChangePhoneCallback = useCallback((event: React.ChangeEvent<HTMLInputElement>) => {
73-
setState({ isCodeSent: false, phone: event.target.value });
78+
setState({ isCodeSent: false, isPhoneCallInitiated: false, phone: event.target.value });
7479
}, []);
7580

7681
const onChangeCodeCallback = useCallback((event: React.ChangeEvent<HTMLInputElement>) => {
@@ -81,59 +86,91 @@ const PhoneVerification = observer((props: PhoneVerificationProps) => {
8186
userStore.makeTestCall(userPk);
8287
}, [userPk, userStore.makeTestCall]);
8388

89+
const handleSendTestSmsClick = useCallback(() => {
90+
userStore.sendTestSms(userPk);
91+
}, [userPk, userStore.sendTestSms]);
92+
8493
const handleForgetNumberClick = useCallback(() => {
8594
userStore.forgetPhone(userPk).then(async () => {
8695
await userStore.loadUser(userPk);
87-
setState({ phone: '', showForgetScreen: false, isCodeSent: false });
96+
setState({ phone: '', showForgetScreen: false, isCodeSent: false, isPhoneCallInitiated: false });
8897
});
8998
}, [userPk, userStore.forgetPhone, userStore.loadUser]);
9099

91-
const onSubmitCallback = useCallback(async () => {
92-
if (isCodeSent) {
93-
userStore.verifyPhone(userPk, code).then(() => {
94-
userStore.loadUser(userPk);
95-
});
96-
} else {
97-
window.grecaptcha.ready(function () {
98-
window.grecaptcha
99-
.execute(rootStore.recaptchaSiteKey, { action: 'mobile_verification_code' })
100-
.then(async function (token) {
101-
await userStore.updateUser({
102-
pk: userPk,
103-
email: user.email,
104-
unverified_phone_number: phone,
100+
const onSubmitCallback = useCallback(
101+
async (type) => {
102+
let codeVerification = isCodeSent;
103+
if (type === 'verification_call') {
104+
codeVerification = isPhoneCallInitiated;
105+
}
106+
if (codeVerification) {
107+
userStore.verifyPhone(userPk, code).then(() => {
108+
userStore.loadUser(userPk);
109+
});
110+
} else {
111+
window.grecaptcha.ready(function () {
112+
window.grecaptcha
113+
.execute(rootStore.recaptchaSiteKey, { action: 'mobile_verification_code' })
114+
.then(async function (token) {
115+
await userStore.updateUser({
116+
pk: userPk,
117+
email: user.email,
118+
unverified_phone_number: phone,
119+
});
120+
121+
switch (type) {
122+
case 'verification_call':
123+
userStore.fetchVerificationCall(userPk, token).then(() => {
124+
setState({ isPhoneCallInitiated: true });
125+
if (codeInputRef.current) {
126+
codeInputRef.current.focus();
127+
}
128+
});
129+
break;
130+
case 'verification_sms':
131+
userStore.fetchVerificationCode(userPk, token).then(() => {
132+
setState({ isCodeSent: true });
133+
if (codeInputRef.current) {
134+
codeInputRef.current.focus();
135+
}
136+
});
137+
break;
138+
}
105139
});
140+
});
141+
}
142+
},
143+
[
144+
code,
145+
isCodeSent,
146+
phone,
147+
user.email,
148+
userPk,
149+
userStore.verifyPhone,
150+
userStore.updateUser,
151+
userStore.fetchVerificationCode,
152+
]
153+
);
106154

107-
userStore.fetchVerificationCode(userPk, token).then(() => {
108-
setState({ isCodeSent: true });
155+
const onVerifyCallback = useCallback(async () => {
156+
userStore.verifyPhone(userPk, code).then(() => {
157+
userStore.loadUser(userPk);
158+
});
159+
}, [code, userPk, userStore.verifyPhone, userStore.loadUser]);
160+
161+
const isPhoneProviderConfigured = teamStore.currentTeam?.env_status.phone_provider?.configured;
162+
const providerConfiguration = teamStore.currentTeam?.env_status.phone_provider;
109163

110-
if (codeInputRef.current) {
111-
codeInputRef.current.focus();
112-
}
113-
});
114-
});
115-
});
116-
}
117-
}, [
118-
code,
119-
isCodeSent,
120-
phone,
121-
user.email,
122-
userPk,
123-
userStore.verifyPhone,
124-
userStore.updateUser,
125-
userStore.fetchVerificationCode,
126-
]);
127-
128-
const isTwilioConfigured = teamStore.currentTeam?.env_status.twilio_configured;
129164
const phoneHasMinimumLength = phone?.length > 8;
130165

131166
const isPhoneValid = phoneHasMinimumLength && PHONE_REGEX.test(phone);
132167
const showPhoneInputError = phoneHasMinimumLength && !isPhoneValid && !isPhoneNumberHidden && !isLoading;
133168

134169
const action = isCurrentUser ? UserActions.UserSettingsWrite : UserActions.UserSettingsAdmin;
135170
const isButtonDisabled =
136-
phone === user.verified_phone_number || (!isCodeSent && !isPhoneValid) || !isTwilioConfigured;
171+
phone === user.verified_phone_number ||
172+
(!isCodeSent && !isPhoneValid && !isPhoneCallInitiated) ||
173+
!isPhoneProviderConfigured;
137174

138175
const isPhoneDisabled = !!user.verified_phone_number;
139176
const isCodeFieldDisabled = !isCodeSent || !isUserActionAllowed(action);
@@ -158,15 +195,15 @@ const PhoneVerification = observer((props: PhoneVerificationProps) => {
158195
</>
159196
)}
160197

161-
{!isTwilioConfigured && store.hasFeature(AppFeature.LiveSettings) && (
198+
{!isPhoneProviderConfigured && store.hasFeature(AppFeature.LiveSettings) && (
162199
<>
163200
<Alert
164201
severity="warning"
165202
// @ts-ignore
166203
title={
167204
<>
168-
Can't verify phone. <PluginLink query={{ page: 'live-settings' }}> Check ENV variables</PluginLink>{' '}
169-
related to Twilio.
205+
Can't verify phone. <PluginLink query={{ page: 'live-settings' }}> Check ENV variables</PluginLink> to
206+
configure your provider.
170207
</>
171208
}
172209
/>
@@ -185,7 +222,7 @@ const PhoneVerification = observer((props: PhoneVerificationProps) => {
185222
autoFocus
186223
id="phone"
187224
required
188-
disabled={!isTwilioConfigured || isPhoneDisabled}
225+
disabled={!isPhoneProviderConfigured || isPhoneDisabled}
189226
placeholder="Please enter the phone number with country code, e.g. +12451111111"
190227
// @ts-ignore
191228
prefix={<Icon name="phone" />}
@@ -233,11 +270,14 @@ const PhoneVerification = observer((props: PhoneVerificationProps) => {
233270
<PhoneVerificationButtonsGroup
234271
action={action}
235272
isCodeSent={isCodeSent}
273+
isPhoneCallInitiated={isPhoneCallInitiated}
236274
isButtonDisabled={isButtonDisabled}
237275
isTestCallInProgress={userStore.isTestCallInProgress}
238-
isTwilioConfigured={isTwilioConfigured}
276+
providerConfiguration={providerConfiguration}
239277
onSubmitCallback={onSubmitCallback}
278+
onVerifyCallback={onVerifyCallback}
240279
handleMakeTestCallClick={handleMakeTestCallClick}
280+
handleSendTestSmsClick={handleSendTestSmsClick}
241281
onShowForgetScreen={() => setState({ showForgetScreen: true })}
242282
user={user}
243283
/>
@@ -273,12 +313,20 @@ interface PhoneVerificationButtonsGroupProps {
273313
action: UserAction;
274314

275315
isCodeSent: boolean;
316+
isPhoneCallInitiated: boolean;
276317
isButtonDisabled: boolean;
277318
isTestCallInProgress: boolean;
278-
isTwilioConfigured: boolean;
279-
280-
onSubmitCallback(): void;
319+
providerConfiguration: {
320+
configured: boolean;
321+
test_call: boolean;
322+
test_sms: boolean;
323+
verification_call: boolean;
324+
verification_sms: boolean;
325+
};
326+
onSubmitCallback(type: string): void;
327+
onVerifyCallback(): void;
281328
handleMakeTestCallClick(): void;
329+
handleSendTestSmsClick(): void;
282330
onShowForgetScreen(): void;
283331

284332
user: User;
@@ -287,25 +335,60 @@ interface PhoneVerificationButtonsGroupProps {
287335
function PhoneVerificationButtonsGroup({
288336
action,
289337
isCodeSent,
338+
isPhoneCallInitiated,
290339
isButtonDisabled,
291340
isTestCallInProgress,
292-
isTwilioConfigured,
341+
providerConfiguration,
293342
onSubmitCallback,
343+
onVerifyCallback,
294344
handleMakeTestCallClick,
345+
handleSendTestSmsClick,
295346
onShowForgetScreen,
296347
user,
297348
}: PhoneVerificationButtonsGroupProps) {
298349
const showForgetNumber = !!user.verified_phone_number;
299350
const showVerifyOrSendCodeButton = !user.verified_phone_number;
300-
351+
const verificationStarted = isCodeSent || isPhoneCallInitiated;
301352
return (
302353
<HorizontalGroup>
303354
{showVerifyOrSendCodeButton && (
304-
<WithPermissionControlTooltip userAction={action}>
305-
<Button variant="primary" onClick={onSubmitCallback} disabled={isButtonDisabled}>
306-
{isCodeSent ? 'Verify' : 'Send Code'}
307-
</Button>
308-
</WithPermissionControlTooltip>
355+
<HorizontalGroup>
356+
{verificationStarted ? (
357+
<>
358+
<WithPermissionControlTooltip userAction={action}>
359+
<Button variant="primary" onClick={onVerifyCallback}>
360+
Verify
361+
</Button>
362+
</WithPermissionControlTooltip>
363+
</>
364+
) : (
365+
<HorizontalGroup>
366+
{' '}
367+
{providerConfiguration.verification_sms && (
368+
<WithPermissionControlTooltip userAction={action}>
369+
<Button
370+
variant="primary"
371+
onClick={() => onSubmitCallback('verification_sms')}
372+
disabled={isButtonDisabled}
373+
>
374+
Send Code
375+
</Button>
376+
</WithPermissionControlTooltip>
377+
)}
378+
{providerConfiguration.verification_call && (
379+
<WithPermissionControlTooltip userAction={action}>
380+
<Button
381+
variant="primary"
382+
onClick={() => onSubmitCallback('verification_call')}
383+
disabled={isButtonDisabled}
384+
>
385+
Call to get the code
386+
</Button>
387+
</WithPermissionControlTooltip>
388+
)}
389+
</HorizontalGroup>
390+
)}
391+
</HorizontalGroup>
309392
)}
310393

311394
{showForgetNumber && (
@@ -321,24 +404,33 @@ function PhoneVerificationButtonsGroup({
321404
)}
322405

323406
{user.verified_phone_number && (
324-
<>
325-
<WithPermissionControlTooltip userAction={action}>
326-
<Button
327-
disabled={!user?.verified_phone_number || !isTwilioConfigured || isTestCallInProgress}
328-
onClick={handleMakeTestCallClick}
329-
>
330-
{isTestCallInProgress ? 'Making Test Call...' : 'Make Test Call'}
331-
</Button>
332-
</WithPermissionControlTooltip>
333-
<Tooltip content={'Click "Make Test Call" to save a phone number and add it to DnD exceptions.'}>
334-
<Icon
335-
name="info-circle"
336-
style={{
337-
marginLeft: '10px',
338-
}}
339-
/>
340-
</Tooltip>
341-
</>
407+
<HorizontalGroup>
408+
{providerConfiguration.test_sms && (
409+
<WithPermissionControlTooltip userAction={action}>
410+
<Button
411+
disabled={!user?.verified_phone_number || !providerConfiguration.configured || isTestCallInProgress}
412+
onClick={handleSendTestSmsClick}
413+
>
414+
Send test sms
415+
</Button>
416+
</WithPermissionControlTooltip>
417+
)}
418+
{providerConfiguration.test_call && (
419+
<HorizontalGroup spacing="xs">
420+
<WithPermissionControlTooltip userAction={action}>
421+
<Button
422+
disabled={!user?.verified_phone_number || !providerConfiguration.configured || isTestCallInProgress}
423+
onClick={handleMakeTestCallClick}
424+
>
425+
{isTestCallInProgress ? 'Making Test Call...' : 'Make Test Call'}
426+
</Button>
427+
</WithPermissionControlTooltip>
428+
<Tooltip content={'Click "Make Test Call" to save a phone number and add it to DnD exceptions.'}>
429+
<Icon name="info-circle" />
430+
</Tooltip>
431+
</HorizontalGroup>
432+
)}
433+
</HorizontalGroup>
342434
)}
343435
</HorizontalGroup>
344436
);

src/models/team/team.types.ts

+7
Original file line numberDiff line numberDiff line change
@@ -66,5 +66,12 @@ export interface Team {
6666
env_status: {
6767
twilio_configured: boolean;
6868
telegram_configured: boolean;
69+
phone_provider: {
70+
configured: boolean;
71+
test_call: boolean;
72+
test_sms: boolean;
73+
verification_call: boolean;
74+
verification_sms: boolean;
75+
};
6976
};
7077
}

0 commit comments

Comments
 (0)