284 lines
7.6 KiB
TypeScript
284 lines
7.6 KiB
TypeScript
// Copyright 2021 Signal Messenger, LLC
|
|
// SPDX-License-Identifier: AGPL-3.0-only
|
|
|
|
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 * as log from '../logging/log';
|
|
import { parseNumber } from '../util/libphonenumberUtil';
|
|
import { getChallengeURL } from '../challenge';
|
|
import { VerificationTransport } from '../types/VerificationTransport';
|
|
|
|
function 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 = 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]
|
|
);
|
|
|
|
return (
|
|
<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 function StandaloneRegistration({
|
|
onComplete,
|
|
requestVerification,
|
|
registerSingleDevice,
|
|
}: {
|
|
onComplete: () => void;
|
|
requestVerification: (
|
|
number: string,
|
|
captcha: string,
|
|
transport: VerificationTransport
|
|
) => Promise<{ sessionId: string }>;
|
|
registerSingleDevice: (
|
|
number: string,
|
|
code: string,
|
|
sessionId: string
|
|
) => Promise<void>;
|
|
}): JSX.Element {
|
|
useEffect(() => {
|
|
window.IPC.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 [sessionId, setSessionId] = useState<string | undefined>(undefined);
|
|
const [status, setStatus] = useState<string | undefined>(undefined);
|
|
|
|
const onRequestCode = useCallback(
|
|
async (transport: VerificationTransport) => {
|
|
if (!isValidNumber) {
|
|
return;
|
|
}
|
|
|
|
if (!number) {
|
|
setIsValidNumber(false);
|
|
setError(undefined);
|
|
return;
|
|
}
|
|
|
|
const url = getChallengeURL('registration');
|
|
log.info(`StandaloneRegistration: navigating to ${url}`);
|
|
document.location.href = url;
|
|
if (!window.Signal.challengeHandler) {
|
|
setError('Captcha handler is not ready!');
|
|
return;
|
|
}
|
|
const token = await window.Signal.challengeHandler.requestCaptcha({
|
|
reason: 'standalone registration',
|
|
});
|
|
|
|
try {
|
|
const result = await requestVerification(number, token, transport);
|
|
setSessionId(result.sessionId);
|
|
setError(undefined);
|
|
} catch (err) {
|
|
setError(err.message);
|
|
}
|
|
},
|
|
[isValidNumber, setIsValidNumber, setError, requestVerification, number]
|
|
);
|
|
|
|
const onSMSClick = useCallback(
|
|
(e: React.MouseEvent<HTMLButtonElement>) => {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
|
|
void onRequestCode(VerificationTransport.SMS);
|
|
},
|
|
[onRequestCode]
|
|
);
|
|
|
|
const onVoiceClick = useCallback(
|
|
(e: React.MouseEvent<HTMLButtonElement>) => {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
|
|
void onRequestCode(VerificationTransport.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 || !sessionId) {
|
|
return;
|
|
}
|
|
|
|
strictAssert(number != null && code.length > 0, 'Missing number or code');
|
|
|
|
try {
|
|
await registerSingleDevice(number, code, sessionId);
|
|
onComplete();
|
|
} catch (err) {
|
|
setStatus(err.message);
|
|
}
|
|
},
|
|
[
|
|
registerSingleDevice,
|
|
onComplete,
|
|
number,
|
|
code,
|
|
sessionId,
|
|
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"
|
|
dir="auto"
|
|
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>
|
|
);
|
|
}
|