Move StandaloneRegistration to React
This commit is contained in:
parent
67b17ec317
commit
7c1ce3366d
20 changed files with 452 additions and 1358 deletions
|
@ -18,6 +18,13 @@ type PropsType = {
|
|||
appView: AppViewType;
|
||||
renderCallManager: () => JSX.Element;
|
||||
renderGlobalModalContainer: () => JSX.Element;
|
||||
openInbox: () => void;
|
||||
requestVerification: (
|
||||
type: 'sms' | 'voice',
|
||||
number: string,
|
||||
token: string
|
||||
) => Promise<void>;
|
||||
registerSingleDevice: (number: string, code: string) => Promise<void>;
|
||||
theme: ThemeType;
|
||||
} & ComponentProps<typeof Inbox>;
|
||||
|
||||
|
@ -34,6 +41,9 @@ export const App = ({
|
|||
renderCustomizingPreferredReactionsModal,
|
||||
renderGlobalModalContainer,
|
||||
renderSafetyNumber,
|
||||
openInbox,
|
||||
requestVerification,
|
||||
registerSingleDevice,
|
||||
theme,
|
||||
verifyConversationsStoppingMessageSend,
|
||||
}: PropsType): JSX.Element => {
|
||||
|
@ -42,7 +52,17 @@ export const App = ({
|
|||
if (appView === AppViewType.Installer) {
|
||||
contents = <Install />;
|
||||
} else if (appView === AppViewType.Standalone) {
|
||||
contents = <StandaloneRegistration />;
|
||||
const onComplete = () => {
|
||||
window.removeSetupMenuItems();
|
||||
openInbox();
|
||||
};
|
||||
contents = (
|
||||
<StandaloneRegistration
|
||||
onComplete={onComplete}
|
||||
requestVerification={requestVerification}
|
||||
registerSingleDevice={registerSingleDevice}
|
||||
/>
|
||||
);
|
||||
} else if (appView === AppViewType.Inbox) {
|
||||
contents = (
|
||||
<Inbox
|
||||
|
|
|
@ -1,14 +1,268 @@
|
|||
// Copyright 2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import React from 'react';
|
||||
import { BackboneHost } from './BackboneHost';
|
||||
import type { ChangeEvent } from 'react';
|
||||
import React, { useState, useEffect, useCallback, useRef } from 'react';
|
||||
import type { Plugin } from 'intl-tel-input';
|
||||
import intlTelInput from 'intl-tel-input';
|
||||
|
||||
import { strictAssert } from '../util/assert';
|
||||
import { getChallengeURL } from '../challenge';
|
||||
|
||||
const PhoneInput = ({
|
||||
onValidation,
|
||||
onNumberChange,
|
||||
}: {
|
||||
onValidation: (isValid: boolean) => void;
|
||||
onNumberChange: (number?: string) => void;
|
||||
}): JSX.Element => {
|
||||
const [isValid, setIsValid] = useState(false);
|
||||
const pluginRef = useRef<Plugin | undefined>();
|
||||
const elemRef = useRef<HTMLInputElement | null>(null);
|
||||
|
||||
const onRef = useCallback((elem: HTMLInputElement | null) => {
|
||||
elemRef.current = elem;
|
||||
|
||||
if (!elem) {
|
||||
return;
|
||||
}
|
||||
|
||||
pluginRef.current?.destroy();
|
||||
|
||||
const plugin = intlTelInput(elem);
|
||||
pluginRef.current = plugin;
|
||||
}, []);
|
||||
|
||||
const validateNumber = useCallback(
|
||||
(number: string) => {
|
||||
const { current: plugin } = pluginRef;
|
||||
if (!plugin) {
|
||||
return;
|
||||
}
|
||||
|
||||
const regionCode = plugin.getSelectedCountryData().iso2;
|
||||
|
||||
const parsedNumber = window.libphonenumber.util.parseNumber(
|
||||
number,
|
||||
regionCode
|
||||
);
|
||||
|
||||
setIsValid(parsedNumber.isValidNumber);
|
||||
onValidation(parsedNumber.isValidNumber);
|
||||
|
||||
onNumberChange(
|
||||
parsedNumber.isValidNumber ? parsedNumber.e164 : undefined
|
||||
);
|
||||
},
|
||||
[setIsValid, onNumberChange, onValidation]
|
||||
);
|
||||
|
||||
const onChange = useCallback(
|
||||
(_: ChangeEvent<HTMLInputElement>) => {
|
||||
if (elemRef.current) {
|
||||
validateNumber(elemRef.current.value);
|
||||
}
|
||||
},
|
||||
[validateNumber]
|
||||
);
|
||||
|
||||
const onKeyDown = useCallback(
|
||||
(event: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
// Pacify TypeScript and handle events bubbling up
|
||||
if (event.target instanceof HTMLInputElement) {
|
||||
validateNumber(event.target.value);
|
||||
}
|
||||
},
|
||||
[validateNumber]
|
||||
);
|
||||
|
||||
export const StandaloneRegistration = (): JSX.Element => {
|
||||
return (
|
||||
<BackboneHost
|
||||
className="full-screen-flow"
|
||||
View={window.Whisper.StandaloneRegistrationView}
|
||||
/>
|
||||
<div className="phone-input">
|
||||
<div className="phone-input-form">
|
||||
<div className={`number-container ${isValid ? 'valid' : 'invalid'}`}>
|
||||
<input
|
||||
className="number"
|
||||
type="tel"
|
||||
ref={onRef}
|
||||
onChange={onChange}
|
||||
onKeyDown={onKeyDown}
|
||||
placeholder="Phone Number"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const StandaloneRegistration = ({
|
||||
onComplete,
|
||||
requestVerification,
|
||||
registerSingleDevice,
|
||||
}: {
|
||||
onComplete: () => void;
|
||||
requestVerification: (
|
||||
type: 'sms' | 'voice',
|
||||
number: string,
|
||||
token: string
|
||||
) => Promise<void>;
|
||||
registerSingleDevice: (number: string, code: string) => Promise<void>;
|
||||
}): JSX.Element => {
|
||||
useEffect(() => {
|
||||
window.readyForUpdates();
|
||||
}, []);
|
||||
|
||||
const [isValidNumber, setIsValidNumber] = useState(false);
|
||||
const [isValidCode, setIsValidCode] = useState(false);
|
||||
const [number, setNumber] = useState<string | undefined>(undefined);
|
||||
const [code, setCode] = useState('');
|
||||
const [error, setError] = useState<string | undefined>(undefined);
|
||||
const [status, setStatus] = useState<string | undefined>(undefined);
|
||||
|
||||
const onRequestCode = useCallback(
|
||||
async (type: 'sms' | 'voice') => {
|
||||
if (!isValidNumber) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!number) {
|
||||
setIsValidNumber(false);
|
||||
setError(undefined);
|
||||
return;
|
||||
}
|
||||
|
||||
document.location.href = getChallengeURL();
|
||||
const token = await window.Signal.challengeHandler.requestCaptcha();
|
||||
|
||||
try {
|
||||
requestVerification(type, number, token);
|
||||
setError(undefined);
|
||||
} catch (err) {
|
||||
setError(err.message);
|
||||
}
|
||||
},
|
||||
[isValidNumber, setIsValidNumber, setError, requestVerification, number]
|
||||
);
|
||||
|
||||
const onSMSClick = useCallback(
|
||||
(e: React.MouseEvent<HTMLButtonElement>) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
onRequestCode('sms');
|
||||
},
|
||||
[onRequestCode]
|
||||
);
|
||||
|
||||
const onVoiceClick = useCallback(
|
||||
(e: React.MouseEvent<HTMLButtonElement>) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
onRequestCode('voice');
|
||||
},
|
||||
[onRequestCode]
|
||||
);
|
||||
|
||||
const onChangeCode = useCallback(
|
||||
(event: ChangeEvent<HTMLInputElement>) => {
|
||||
const { value } = event.target;
|
||||
|
||||
setIsValidCode(value.length === 6);
|
||||
setCode(value);
|
||||
},
|
||||
[setIsValidCode, setCode]
|
||||
);
|
||||
|
||||
const onVerifyCode = useCallback(
|
||||
async (event: React.MouseEvent<HTMLButtonElement>) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
if (!isValidNumber || !isValidCode) {
|
||||
return;
|
||||
}
|
||||
|
||||
strictAssert(number && code, 'Missing number or code');
|
||||
|
||||
try {
|
||||
await registerSingleDevice(number, code);
|
||||
onComplete();
|
||||
} catch (err) {
|
||||
setStatus(err.message);
|
||||
}
|
||||
},
|
||||
[
|
||||
registerSingleDevice,
|
||||
onComplete,
|
||||
number,
|
||||
code,
|
||||
setStatus,
|
||||
isValidNumber,
|
||||
isValidCode,
|
||||
]
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="full-screen-flow">
|
||||
<div className="module-title-bar-drag-area" />
|
||||
|
||||
<div className="step">
|
||||
<div className="inner">
|
||||
<div className="step-body">
|
||||
<div className="banner-image module-splash-screen__logo module-img--128" />
|
||||
<div className="header">Create your Signal Account</div>
|
||||
<div>
|
||||
<div className="phone-input-form">
|
||||
<PhoneInput
|
||||
onValidation={setIsValidNumber}
|
||||
onNumberChange={setNumber}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="clearfix">
|
||||
<button
|
||||
type="button"
|
||||
className="button"
|
||||
disabled={!isValidNumber}
|
||||
onClick={onSMSClick}
|
||||
>
|
||||
Send SMS
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="link"
|
||||
tabIndex={-1}
|
||||
disabled={!isValidNumber}
|
||||
onClick={onVoiceClick}
|
||||
>
|
||||
Call
|
||||
</button>
|
||||
</div>
|
||||
<input
|
||||
className={`form-control ${isValidCode ? 'valid' : 'invalid'}`}
|
||||
type="text"
|
||||
pattern="\s*[0-9]{3}-?[0-9]{3}\s*"
|
||||
title="Enter your 6-digit verification code. If you did not receive a code, click Call or Send SMS to request a new one"
|
||||
placeholder="Verification Code"
|
||||
autoComplete="off"
|
||||
value={code}
|
||||
onChange={onChangeCode}
|
||||
/>
|
||||
<div>{error}</div>
|
||||
<div>{status}</div>
|
||||
</div>
|
||||
<div className="nav">
|
||||
<button
|
||||
type="button"
|
||||
className="button"
|
||||
disabled={!isValidNumber || !isValidCode}
|
||||
onClick={onVerifyCode}
|
||||
>
|
||||
Register
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue