Accept profile name during standalone registration
This commit is contained in:
parent
8f2061e11d
commit
12b57601ac
5 changed files with 447 additions and 103 deletions
|
@ -631,6 +631,10 @@ $loading-height: 16px;
|
||||||
float: inline-start;
|
float: inline-start;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.StandaloneRegistration__error {
|
||||||
|
color: $color-accent-red;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//yellow border fix
|
//yellow border fix
|
||||||
|
|
|
@ -17,14 +17,20 @@ import { useReducedMotion } from '../hooks/useReducedMotion';
|
||||||
type PropsType = {
|
type PropsType = {
|
||||||
appView: AppViewType;
|
appView: AppViewType;
|
||||||
openInbox: () => void;
|
openInbox: () => void;
|
||||||
|
getCaptchaToken: () => Promise<string>;
|
||||||
registerSingleDevice: (
|
registerSingleDevice: (
|
||||||
number: string,
|
number: string,
|
||||||
code: string,
|
code: string,
|
||||||
sessionId: string
|
sessionId: string
|
||||||
) => Promise<void>;
|
) => Promise<void>;
|
||||||
|
uploadProfile: (opts: {
|
||||||
|
firstName: string;
|
||||||
|
lastName: string;
|
||||||
|
}) => Promise<void>;
|
||||||
renderCallManager: () => JSX.Element;
|
renderCallManager: () => JSX.Element;
|
||||||
renderGlobalModalContainer: () => JSX.Element;
|
renderGlobalModalContainer: () => JSX.Element;
|
||||||
hasSelectedStoryData: boolean;
|
hasSelectedStoryData: boolean;
|
||||||
|
readyForUpdates: () => void;
|
||||||
renderStoryViewer: (closeView: () => unknown) => JSX.Element;
|
renderStoryViewer: (closeView: () => unknown) => JSX.Element;
|
||||||
renderLightbox: () => JSX.Element | null;
|
renderLightbox: () => JSX.Element | null;
|
||||||
requestVerification: (
|
requestVerification: (
|
||||||
|
@ -44,11 +50,13 @@ type PropsType = {
|
||||||
|
|
||||||
export function App({
|
export function App({
|
||||||
appView,
|
appView,
|
||||||
|
getCaptchaToken,
|
||||||
hasSelectedStoryData,
|
hasSelectedStoryData,
|
||||||
isFullScreen,
|
isFullScreen,
|
||||||
isMaximized,
|
isMaximized,
|
||||||
openInbox,
|
openInbox,
|
||||||
osClassName,
|
osClassName,
|
||||||
|
readyForUpdates,
|
||||||
registerSingleDevice,
|
registerSingleDevice,
|
||||||
renderCallManager,
|
renderCallManager,
|
||||||
renderGlobalModalContainer,
|
renderGlobalModalContainer,
|
||||||
|
@ -57,6 +65,7 @@ export function App({
|
||||||
renderStoryViewer,
|
renderStoryViewer,
|
||||||
requestVerification,
|
requestVerification,
|
||||||
theme,
|
theme,
|
||||||
|
uploadProfile,
|
||||||
viewStory,
|
viewStory,
|
||||||
}: PropsType): JSX.Element {
|
}: PropsType): JSX.Element {
|
||||||
let contents;
|
let contents;
|
||||||
|
@ -71,8 +80,11 @@ export function App({
|
||||||
contents = (
|
contents = (
|
||||||
<StandaloneRegistration
|
<StandaloneRegistration
|
||||||
onComplete={onComplete}
|
onComplete={onComplete}
|
||||||
|
getCaptchaToken={getCaptchaToken}
|
||||||
|
readyForUpdates={readyForUpdates}
|
||||||
requestVerification={requestVerification}
|
requestVerification={requestVerification}
|
||||||
registerSingleDevice={registerSingleDevice}
|
registerSingleDevice={registerSingleDevice}
|
||||||
|
uploadProfile={uploadProfile}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
} else if (appView === AppViewType.Inbox) {
|
} else if (appView === AppViewType.Inbox) {
|
||||||
|
|
40
ts/components/StandaloneRegistration.stories.tsx
Normal file
40
ts/components/StandaloneRegistration.stories.tsx
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
// Copyright 2023 Signal Messenger, LLC
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import type { Meta, StoryFn } from '@storybook/react';
|
||||||
|
import { fn } from '@storybook/test';
|
||||||
|
|
||||||
|
import { StandaloneRegistration } from './StandaloneRegistration';
|
||||||
|
import type { PropsType } from './StandaloneRegistration';
|
||||||
|
import { SECOND } from '../util/durations';
|
||||||
|
import { sleep } from '../util/sleep';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
title: 'Components/StandaloneRegistration',
|
||||||
|
args: {
|
||||||
|
getCaptchaToken: fn(async () => {
|
||||||
|
await sleep(SECOND);
|
||||||
|
return 'captcha-token';
|
||||||
|
}),
|
||||||
|
requestVerification: fn(async () => {
|
||||||
|
await sleep(SECOND);
|
||||||
|
return { sessionId: 'fake-session-id' };
|
||||||
|
}),
|
||||||
|
registerSingleDevice: fn(async () => {
|
||||||
|
await sleep(SECOND);
|
||||||
|
}),
|
||||||
|
uploadProfile: fn(async () => {
|
||||||
|
await sleep(SECOND);
|
||||||
|
}),
|
||||||
|
onComplete: fn(),
|
||||||
|
readyForUpdates: fn(),
|
||||||
|
},
|
||||||
|
} satisfies Meta<PropsType & { daysAgo?: number }>;
|
||||||
|
|
||||||
|
// eslint-disable-next-line react/function-component-definition
|
||||||
|
const Template: StoryFn<PropsType> = args => {
|
||||||
|
return <StandaloneRegistration {...args} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Default = Template.bind({});
|
|
@ -7,15 +7,16 @@ import type { Plugin } from 'intl-tel-input';
|
||||||
import intlTelInput from 'intl-tel-input';
|
import intlTelInput from 'intl-tel-input';
|
||||||
|
|
||||||
import { strictAssert } from '../util/assert';
|
import { strictAssert } from '../util/assert';
|
||||||
import * as log from '../logging/log';
|
|
||||||
import { parseNumber } from '../util/libphonenumberUtil';
|
import { parseNumber } from '../util/libphonenumberUtil';
|
||||||
import { getChallengeURL } from '../challenge';
|
import { missingCaseError } from '../util/missingCaseError';
|
||||||
import { VerificationTransport } from '../types/VerificationTransport';
|
import { VerificationTransport } from '../types/VerificationTransport';
|
||||||
|
|
||||||
function PhoneInput({
|
function PhoneInput({
|
||||||
|
initialValue,
|
||||||
onValidation,
|
onValidation,
|
||||||
onNumberChange,
|
onNumberChange,
|
||||||
}: {
|
}: {
|
||||||
|
initialValue: string | undefined;
|
||||||
onValidation: (isValid: boolean) => void;
|
onValidation: (isValid: boolean) => void;
|
||||||
onNumberChange: (number?: string) => void;
|
onNumberChange: (number?: string) => void;
|
||||||
}): JSX.Element {
|
}): JSX.Element {
|
||||||
|
@ -23,18 +24,26 @@ function PhoneInput({
|
||||||
const pluginRef = useRef<Plugin | undefined>();
|
const pluginRef = useRef<Plugin | undefined>();
|
||||||
const elemRef = useRef<HTMLInputElement | null>(null);
|
const elemRef = useRef<HTMLInputElement | null>(null);
|
||||||
|
|
||||||
const onRef = useCallback((elem: HTMLInputElement | null) => {
|
const onRef = useCallback(
|
||||||
elemRef.current = elem;
|
(elem: HTMLInputElement | null) => {
|
||||||
|
elemRef.current = elem;
|
||||||
|
|
||||||
if (!elem) {
|
if (!elem) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
pluginRef.current?.destroy();
|
if (initialValue !== undefined) {
|
||||||
|
// eslint-disable-next-line no-param-reassign
|
||||||
|
elem.value = initialValue;
|
||||||
|
}
|
||||||
|
|
||||||
const plugin = intlTelInput(elem);
|
pluginRef.current?.destroy();
|
||||||
pluginRef.current = plugin;
|
|
||||||
}, []);
|
const plugin = intlTelInput(elem);
|
||||||
|
pluginRef.current = plugin;
|
||||||
|
},
|
||||||
|
[initialValue]
|
||||||
|
);
|
||||||
|
|
||||||
const validateNumber = useCallback(
|
const validateNumber = useCallback(
|
||||||
(number: string) => {
|
(number: string) => {
|
||||||
|
@ -94,34 +103,45 @@ function PhoneInput({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function StandaloneRegistration({
|
enum Stage {
|
||||||
onComplete,
|
PhoneNumber,
|
||||||
|
VerificationCode,
|
||||||
|
ProfileName,
|
||||||
|
}
|
||||||
|
|
||||||
|
type StageData =
|
||||||
|
| {
|
||||||
|
stage: Stage.PhoneNumber;
|
||||||
|
initialNumber: string | undefined;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
stage: Stage.VerificationCode;
|
||||||
|
number: string;
|
||||||
|
sessionId: string;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
stage: Stage.ProfileName;
|
||||||
|
};
|
||||||
|
|
||||||
|
function PhoneNumberStage({
|
||||||
|
initialNumber,
|
||||||
|
getCaptchaToken,
|
||||||
requestVerification,
|
requestVerification,
|
||||||
registerSingleDevice,
|
onNext,
|
||||||
}: {
|
}: {
|
||||||
onComplete: () => void;
|
initialNumber: string | undefined;
|
||||||
|
getCaptchaToken: () => Promise<string>;
|
||||||
requestVerification: (
|
requestVerification: (
|
||||||
number: string,
|
number: string,
|
||||||
captcha: string,
|
captcha: string,
|
||||||
transport: VerificationTransport
|
transport: VerificationTransport
|
||||||
) => Promise<{ sessionId: string }>;
|
) => Promise<{ sessionId: string }>;
|
||||||
registerSingleDevice: (
|
onNext: (result: { number: string; sessionId: string }) => void;
|
||||||
number: string,
|
|
||||||
code: string,
|
|
||||||
sessionId: string
|
|
||||||
) => Promise<void>;
|
|
||||||
}): JSX.Element {
|
}): JSX.Element {
|
||||||
useEffect(() => {
|
const [number, setNumber] = useState<string | undefined>(initialNumber);
|
||||||
window.IPC.readyForUpdates();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const [isValidNumber, setIsValidNumber] = useState(false);
|
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 [error, setError] = useState<string | undefined>(undefined);
|
||||||
const [sessionId, setSessionId] = useState<string | undefined>(undefined);
|
|
||||||
const [status, setStatus] = useState<string | undefined>(undefined);
|
|
||||||
|
|
||||||
const onRequestCode = useCallback(
|
const onRequestCode = useCallback(
|
||||||
async (transport: VerificationTransport) => {
|
async (transport: VerificationTransport) => {
|
||||||
|
@ -135,26 +155,25 @@ export function StandaloneRegistration({
|
||||||
return;
|
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 {
|
try {
|
||||||
|
const token = await getCaptchaToken();
|
||||||
const result = await requestVerification(number, token, transport);
|
const result = await requestVerification(number, token, transport);
|
||||||
setSessionId(result.sessionId);
|
|
||||||
setError(undefined);
|
setError(undefined);
|
||||||
|
|
||||||
|
onNext({ number, sessionId: result.sessionId });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err.message);
|
setError(err.message);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[isValidNumber, setIsValidNumber, setError, requestVerification, number]
|
[
|
||||||
|
getCaptchaToken,
|
||||||
|
isValidNumber,
|
||||||
|
setIsValidNumber,
|
||||||
|
setError,
|
||||||
|
requestVerification,
|
||||||
|
number,
|
||||||
|
onNext,
|
||||||
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
const onSMSClick = useCallback(
|
const onSMSClick = useCallback(
|
||||||
|
@ -177,6 +196,65 @@ export function StandaloneRegistration({
|
||||||
[onRequestCode]
|
[onRequestCode]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<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
|
||||||
|
initialValue={initialNumber}
|
||||||
|
onValidation={setIsValidNumber}
|
||||||
|
onNumberChange={setNumber}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="StandaloneRegistration__error">{error}</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>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function VerificationCodeStage({
|
||||||
|
number,
|
||||||
|
sessionId,
|
||||||
|
registerSingleDevice,
|
||||||
|
onNext,
|
||||||
|
onBack,
|
||||||
|
}: {
|
||||||
|
number: string;
|
||||||
|
sessionId: string;
|
||||||
|
registerSingleDevice: (
|
||||||
|
number: string,
|
||||||
|
code: string,
|
||||||
|
sessionId: string
|
||||||
|
) => Promise<void>;
|
||||||
|
onNext: () => void;
|
||||||
|
onBack: () => void;
|
||||||
|
}): JSX.Element {
|
||||||
|
const [code, setCode] = useState('');
|
||||||
|
const [isValidCode, setIsValidCode] = useState(false);
|
||||||
|
const [error, setError] = useState<string | undefined>(undefined);
|
||||||
|
|
||||||
const onChangeCode = useCallback(
|
const onChangeCode = useCallback(
|
||||||
(event: ChangeEvent<HTMLInputElement>) => {
|
(event: ChangeEvent<HTMLInputElement>) => {
|
||||||
const { value } = event.target;
|
const { value } = event.target;
|
||||||
|
@ -187,12 +265,21 @@ export function StandaloneRegistration({
|
||||||
[setIsValidCode, setCode]
|
[setIsValidCode, setCode]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const onBackClick = useCallback(
|
||||||
|
(event: React.MouseEvent<HTMLButtonElement>) => {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
onBack();
|
||||||
|
},
|
||||||
|
[onBack]
|
||||||
|
);
|
||||||
|
|
||||||
const onVerifyCode = useCallback(
|
const onVerifyCode = useCallback(
|
||||||
async (event: React.MouseEvent<HTMLButtonElement>) => {
|
async (event: React.MouseEvent<HTMLButtonElement>) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
|
|
||||||
if (!isValidNumber || !isValidCode || !sessionId) {
|
if (!isValidCode || !sessionId) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -200,84 +287,246 @@ export function StandaloneRegistration({
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await registerSingleDevice(number, code, sessionId);
|
await registerSingleDevice(number, code, sessionId);
|
||||||
onComplete();
|
onNext();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setStatus(err.message);
|
setError(err.message);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[
|
[
|
||||||
registerSingleDevice,
|
registerSingleDevice,
|
||||||
onComplete,
|
onNext,
|
||||||
number,
|
number,
|
||||||
code,
|
code,
|
||||||
sessionId,
|
sessionId,
|
||||||
setStatus,
|
setError,
|
||||||
isValidNumber,
|
|
||||||
isValidCode,
|
isValidCode,
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="step-body">
|
||||||
|
<div className="banner-image module-splash-screen__logo module-img--128" />
|
||||||
|
<div className="header">Create your Signal Account</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 className="StandaloneRegistration__error">{error}</div>
|
||||||
|
</div>
|
||||||
|
<div className="nav">
|
||||||
|
<button type="button" className="button" onClick={onBackClick}>
|
||||||
|
Back
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="button"
|
||||||
|
disabled={!isValidCode}
|
||||||
|
onClick={onVerifyCode}
|
||||||
|
>
|
||||||
|
Register
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ProfileNameStage({
|
||||||
|
uploadProfile,
|
||||||
|
onNext,
|
||||||
|
}: {
|
||||||
|
uploadProfile: (opts: {
|
||||||
|
firstName: string;
|
||||||
|
lastName: string;
|
||||||
|
}) => Promise<void>;
|
||||||
|
onNext: () => void;
|
||||||
|
}): JSX.Element {
|
||||||
|
const [firstName, setFirstName] = useState('');
|
||||||
|
const [lastName, setLastName] = useState('');
|
||||||
|
const [error, setError] = useState<string | undefined>(undefined);
|
||||||
|
|
||||||
|
const onChangeFirstName = useCallback(
|
||||||
|
(event: ChangeEvent<HTMLInputElement>) => setFirstName(event.target.value),
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
const onChangeLastName = useCallback(
|
||||||
|
(event: ChangeEvent<HTMLInputElement>) => setLastName(event.target.value),
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
const onNextClick = useCallback(
|
||||||
|
async (event: React.MouseEvent<HTMLButtonElement>) => {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
try {
|
||||||
|
await uploadProfile({ firstName, lastName });
|
||||||
|
onNext();
|
||||||
|
} catch (err) {
|
||||||
|
setError(err.message);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[onNext, firstName, lastName, uploadProfile]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="step-body">
|
||||||
|
<div className="banner-image module-splash-screen__logo module-img--128" />
|
||||||
|
<div className="header">Select Profile Name</div>
|
||||||
|
|
||||||
|
<input
|
||||||
|
className={`form-control ${firstName ? 'valid' : 'invalid'}`}
|
||||||
|
type="text"
|
||||||
|
dir="auto"
|
||||||
|
pattern="\s*[0-9]{3}-?[0-9]{3}\s*"
|
||||||
|
title="Enter your first name"
|
||||||
|
placeholder="First name"
|
||||||
|
autoComplete="off"
|
||||||
|
value={firstName}
|
||||||
|
onChange={onChangeFirstName}
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
className="form-control"
|
||||||
|
type="text"
|
||||||
|
dir="auto"
|
||||||
|
pattern="\s*[0-9]{3}-?[0-9]{3}\s*"
|
||||||
|
title="Enter your last name"
|
||||||
|
placeholder="First name"
|
||||||
|
autoComplete="off"
|
||||||
|
value={lastName}
|
||||||
|
onChange={onChangeLastName}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* TODO(indutny): highlight error */}
|
||||||
|
<div>{error}</div>
|
||||||
|
</div>
|
||||||
|
<div className="nav">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="button"
|
||||||
|
disabled={!firstName}
|
||||||
|
onClick={onNextClick}
|
||||||
|
>
|
||||||
|
Finish
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export type PropsType = Readonly<{
|
||||||
|
onComplete: () => void;
|
||||||
|
getCaptchaToken: () => Promise<string>;
|
||||||
|
requestVerification: (
|
||||||
|
number: string,
|
||||||
|
captcha: string,
|
||||||
|
transport: VerificationTransport
|
||||||
|
) => Promise<{ sessionId: string }>;
|
||||||
|
registerSingleDevice: (
|
||||||
|
number: string,
|
||||||
|
code: string,
|
||||||
|
sessionId: string
|
||||||
|
) => Promise<void>;
|
||||||
|
uploadProfile: (opts: {
|
||||||
|
firstName: string;
|
||||||
|
lastName: string;
|
||||||
|
}) => Promise<void>;
|
||||||
|
readyForUpdates: () => void;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
export function StandaloneRegistration({
|
||||||
|
onComplete,
|
||||||
|
getCaptchaToken,
|
||||||
|
requestVerification,
|
||||||
|
registerSingleDevice,
|
||||||
|
uploadProfile,
|
||||||
|
readyForUpdates,
|
||||||
|
}: PropsType): JSX.Element {
|
||||||
|
useEffect(() => {
|
||||||
|
readyForUpdates();
|
||||||
|
}, [readyForUpdates]);
|
||||||
|
|
||||||
|
const [stageData, setStageData] = useState<StageData>({
|
||||||
|
stage: Stage.PhoneNumber,
|
||||||
|
initialNumber: undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
const onPhoneNumber = useCallback(
|
||||||
|
({ number, sessionId }: { number: string; sessionId: string }) => {
|
||||||
|
setStageData({
|
||||||
|
stage: Stage.VerificationCode,
|
||||||
|
number,
|
||||||
|
sessionId,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
const onBackToPhoneNumber = useCallback(() => {
|
||||||
|
setStageData(data => {
|
||||||
|
if (data.stage !== Stage.VerificationCode) {
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
stage: Stage.PhoneNumber,
|
||||||
|
initialNumber: data.number,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const onRegistered = useCallback(() => {
|
||||||
|
setStageData({
|
||||||
|
stage: Stage.ProfileName,
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
let body: JSX.Element;
|
||||||
|
if (stageData.stage === Stage.PhoneNumber) {
|
||||||
|
body = (
|
||||||
|
<PhoneNumberStage
|
||||||
|
{...stageData}
|
||||||
|
getCaptchaToken={getCaptchaToken}
|
||||||
|
requestVerification={requestVerification}
|
||||||
|
onNext={onPhoneNumber}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
} else if (stageData.stage === Stage.VerificationCode) {
|
||||||
|
body = (
|
||||||
|
<VerificationCodeStage
|
||||||
|
{...stageData}
|
||||||
|
registerSingleDevice={registerSingleDevice}
|
||||||
|
onNext={onRegistered}
|
||||||
|
onBack={onBackToPhoneNumber}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
} else if (stageData.stage === Stage.ProfileName) {
|
||||||
|
body = (
|
||||||
|
<ProfileNameStage
|
||||||
|
{...stageData}
|
||||||
|
uploadProfile={uploadProfile}
|
||||||
|
onNext={onComplete}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
throw missingCaseError(stageData);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="full-screen-flow">
|
<div className="full-screen-flow">
|
||||||
<div className="module-title-bar-drag-area" />
|
<div className="module-title-bar-drag-area" />
|
||||||
|
|
||||||
<div className="step">
|
<div className="step">
|
||||||
<div className="inner">
|
<div className="inner">{body}</div>
|
||||||
<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>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -5,6 +5,9 @@ import { useSelector } from 'react-redux';
|
||||||
import type { VerificationTransport } from '../../types/VerificationTransport';
|
import type { VerificationTransport } from '../../types/VerificationTransport';
|
||||||
import { App } from '../../components/App';
|
import { App } from '../../components/App';
|
||||||
import OS from '../../util/os/osMain';
|
import OS from '../../util/os/osMain';
|
||||||
|
import { getConversation } from '../../util/getConversation';
|
||||||
|
import { getChallengeURL } from '../../challenge';
|
||||||
|
import { writeProfile } from '../../services/writeProfile';
|
||||||
import { strictAssert } from '../../util/assert';
|
import { strictAssert } from '../../util/assert';
|
||||||
import { SmartCallManager } from './CallManager';
|
import { SmartCallManager } from './CallManager';
|
||||||
import { SmartGlobalModalContainer } from './GlobalModalContainer';
|
import { SmartGlobalModalContainer } from './GlobalModalContainer';
|
||||||
|
@ -52,6 +55,17 @@ function renderStoryViewer(closeView: () => unknown): JSX.Element {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function getCaptchaToken(): Promise<string> {
|
||||||
|
const url = getChallengeURL('registration');
|
||||||
|
document.location.href = url;
|
||||||
|
if (!window.Signal.challengeHandler) {
|
||||||
|
throw new Error('Captcha handler is not ready!');
|
||||||
|
}
|
||||||
|
return window.Signal.challengeHandler.requestCaptcha({
|
||||||
|
reason: 'standalone registration',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function requestVerification(
|
function requestVerification(
|
||||||
number: string,
|
number: string,
|
||||||
captcha: string,
|
captcha: string,
|
||||||
|
@ -72,6 +86,28 @@ function registerSingleDevice(
|
||||||
.registerSingleDevice(number, code, sessionId);
|
.registerSingleDevice(number, code, sessionId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function readyForUpdates(): void {
|
||||||
|
window.IPC.readyForUpdates();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function uploadProfile({
|
||||||
|
firstName,
|
||||||
|
lastName,
|
||||||
|
}: {
|
||||||
|
firstName: string;
|
||||||
|
lastName: string;
|
||||||
|
}): Promise<void> {
|
||||||
|
const us = window.ConversationController.getOurConversationOrThrow();
|
||||||
|
us.set('profileName', firstName);
|
||||||
|
us.set('profileFamilyName', lastName);
|
||||||
|
us.captureChange('standaloneProfile');
|
||||||
|
await window.Signal.Data.updateConversation(us.attributes);
|
||||||
|
|
||||||
|
await writeProfile(getConversation(us), {
|
||||||
|
keepAvatar: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export const SmartApp = memo(function SmartApp() {
|
export const SmartApp = memo(function SmartApp() {
|
||||||
const appView = useSelector(getAppView);
|
const appView = useSelector(getAppView);
|
||||||
const isMaximized = useSelector(getIsMainWindowMaximized);
|
const isMaximized = useSelector(getIsMainWindowMaximized);
|
||||||
|
@ -90,15 +126,18 @@ export const SmartApp = memo(function SmartApp() {
|
||||||
appView={appView}
|
appView={appView}
|
||||||
isMaximized={isMaximized}
|
isMaximized={isMaximized}
|
||||||
isFullScreen={isFullScreen}
|
isFullScreen={isFullScreen}
|
||||||
|
getCaptchaToken={getCaptchaToken}
|
||||||
osClassName={osClassName}
|
osClassName={osClassName}
|
||||||
renderCallManager={renderCallManager}
|
renderCallManager={renderCallManager}
|
||||||
renderGlobalModalContainer={renderGlobalModalContainer}
|
renderGlobalModalContainer={renderGlobalModalContainer}
|
||||||
renderLightbox={renderLightbox}
|
renderLightbox={renderLightbox}
|
||||||
hasSelectedStoryData={hasSelectedStoryData}
|
hasSelectedStoryData={hasSelectedStoryData}
|
||||||
|
readyForUpdates={readyForUpdates}
|
||||||
renderStoryViewer={renderStoryViewer}
|
renderStoryViewer={renderStoryViewer}
|
||||||
renderInbox={renderInbox}
|
renderInbox={renderInbox}
|
||||||
requestVerification={requestVerification}
|
requestVerification={requestVerification}
|
||||||
registerSingleDevice={registerSingleDevice}
|
registerSingleDevice={registerSingleDevice}
|
||||||
|
uploadProfile={uploadProfile}
|
||||||
theme={theme}
|
theme={theme}
|
||||||
openInbox={openInbox}
|
openInbox={openInbox}
|
||||||
scrollToMessage={scrollToMessage}
|
scrollToMessage={scrollToMessage}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue