|
|
|
import React from 'react';
|
|
|
|
import classNames from 'classnames';
|
|
|
|
|
|
|
|
import { SessionInput } from './SessionInput';
|
|
|
|
import {
|
|
|
|
SessionButton,
|
|
|
|
SessionButtonColor,
|
|
|
|
SessionButtonType,
|
|
|
|
} from './SessionButton';
|
|
|
|
import { trigger } from '../../shims/events';
|
|
|
|
import { SessionHtmlRenderer } from './SessionHTMLRenderer';
|
|
|
|
import { SessionIdEditable } from './SessionIdEditable';
|
|
|
|
import { SessionSpinner } from './SessionSpinner';
|
|
|
|
|
|
|
|
enum SignInMode {
|
|
|
|
Default,
|
|
|
|
UsingSeed,
|
|
|
|
LinkingDevice,
|
|
|
|
}
|
|
|
|
|
|
|
|
enum SignUpMode {
|
|
|
|
Default,
|
|
|
|
SessionIDShown,
|
|
|
|
EnterDetails,
|
|
|
|
}
|
|
|
|
|
|
|
|
enum TabType {
|
|
|
|
Create,
|
|
|
|
SignIn,
|
|
|
|
}
|
|
|
|
|
|
|
|
interface State {
|
|
|
|
selectedTab: TabType;
|
|
|
|
signInMode: SignInMode;
|
|
|
|
signUpMode: SignUpMode;
|
|
|
|
displayName: string;
|
|
|
|
password: string;
|
|
|
|
validatePassword: string;
|
|
|
|
passwordErrorString: string;
|
|
|
|
passwordFieldsMatch: boolean;
|
|
|
|
mnemonicSeed: string;
|
|
|
|
hexGeneratedPubKey: string;
|
|
|
|
primaryDevicePubKey: string;
|
|
|
|
mnemonicError: string | undefined;
|
|
|
|
displayNameError: string | undefined;
|
|
|
|
loading: boolean;
|
|
|
|
}
|
|
|
|
|
|
|
|
const Tab = ({
|
|
|
|
isSelected,
|
|
|
|
label,
|
|
|
|
onSelect,
|
|
|
|
type,
|
|
|
|
}: {
|
|
|
|
isSelected: boolean;
|
|
|
|
label: string;
|
|
|
|
onSelect?: (event: TabType) => void;
|
|
|
|
type: TabType;
|
|
|
|
}) => {
|
|
|
|
const handleClick = onSelect
|
|
|
|
? () => {
|
|
|
|
onSelect(type);
|
|
|
|
}
|
|
|
|
: undefined;
|
|
|
|
|
|
|
|
return (
|
|
|
|
<div
|
|
|
|
className={classNames(
|
|
|
|
'session-registration__tab',
|
|
|
|
isSelected ? 'session-registration__tab--active' : null
|
|
|
|
)}
|
|
|
|
onClick={handleClick}
|
|
|
|
role="tab"
|
|
|
|
>
|
|
|
|
{label}
|
|
|
|
</div>
|
|
|
|
);
|
|
|
|
};
|
|
|
|
|
|
|
|
export class RegistrationTabs extends React.Component<{}, State> {
|
|
|
|
private readonly accountManager: any;
|
|
|
|
|
|
|
|
constructor(props: any) {
|
|
|
|
super(props);
|
|
|
|
|
|
|
|
this.onSeedChanged = this.onSeedChanged.bind(this);
|
|
|
|
this.onDisplayNameChanged = this.onDisplayNameChanged.bind(this);
|
|
|
|
this.onPasswordChanged = this.onPasswordChanged.bind(this);
|
|
|
|
this.onPasswordVerifyChanged = this.onPasswordVerifyChanged.bind(this);
|
|
|
|
this.onSignUpGenerateSessionIDClick = this.onSignUpGenerateSessionIDClick.bind(
|
|
|
|
this
|
|
|
|
);
|
|
|
|
this.onSignUpGetStartedClick = this.onSignUpGetStartedClick.bind(this);
|
|
|
|
this.onSecondDeviceSessionIDChanged = this.onSecondDeviceSessionIDChanged.bind(
|
|
|
|
this
|
|
|
|
);
|
|
|
|
this.onSecondaryDeviceRegistered = this.onSecondaryDeviceRegistered.bind(
|
|
|
|
this
|
|
|
|
);
|
|
|
|
this.onCompleteSignUpClick = this.onCompleteSignUpClick.bind(this);
|
|
|
|
this.handlePressEnter = this.handlePressEnter.bind(this);
|
|
|
|
this.handleContinueYourSessionClick = this.handleContinueYourSessionClick.bind(
|
|
|
|
this
|
|
|
|
);
|
|
|
|
|
|
|
|
this.state = {
|
|
|
|
selectedTab: TabType.Create,
|
|
|
|
signInMode: SignInMode.Default,
|
|
|
|
signUpMode: SignUpMode.Default,
|
|
|
|
displayName: '',
|
|
|
|
password: '',
|
|
|
|
validatePassword: '',
|
|
|
|
passwordErrorString: '',
|
|
|
|
passwordFieldsMatch: false,
|
|
|
|
mnemonicSeed: '',
|
|
|
|
hexGeneratedPubKey: '',
|
|
|
|
primaryDevicePubKey: '',
|
|
|
|
mnemonicError: undefined,
|
|
|
|
displayNameError: undefined,
|
|
|
|
loading: false,
|
|
|
|
};
|
|
|
|
|
|
|
|
this.accountManager = window.getAccountManager();
|
|
|
|
// Clean status in case the app closed unexpectedly
|
|
|
|
window.textsecure.storage.remove('secondaryDeviceStatus');
|
|
|
|
}
|
|
|
|
|
|
|
|
public render() {
|
|
|
|
this.generateMnemonicAndKeyPair().ignore();
|
|
|
|
|
|
|
|
return this.renderTabs();
|
|
|
|
}
|
|
|
|
|
|
|
|
private async generateMnemonicAndKeyPair() {
|
|
|
|
if (this.state.mnemonicSeed === '') {
|
|
|
|
const language = 'english';
|
|
|
|
const mnemonic = await this.accountManager.generateMnemonic(language);
|
|
|
|
|
|
|
|
let seedHex = window.mnemonic.mn_decode(mnemonic, language);
|
|
|
|
// handle shorter than 32 bytes seeds
|
|
|
|
const privKeyHexLength = 32 * 2;
|
|
|
|
if (seedHex.length !== privKeyHexLength) {
|
|
|
|
seedHex = seedHex.concat(seedHex);
|
|
|
|
seedHex = seedHex.substring(0, privKeyHexLength);
|
|
|
|
}
|
|
|
|
const seed = window.dcodeIO.ByteBuffer.wrap(
|
|
|
|
seedHex,
|
|
|
|
'hex'
|
|
|
|
).toArrayBuffer();
|
|
|
|
const keyPair = await window.libsignal.Curve.async.createKeyPair(seed);
|
|
|
|
const hexGeneratedPubKey = Buffer.from(keyPair.pubKey).toString('hex');
|
|
|
|
|
|
|
|
this.setState({
|
|
|
|
mnemonicSeed: mnemonic,
|
|
|
|
hexGeneratedPubKey, // our 'frontend' sessionID
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private renderTabs() {
|
|
|
|
const { selectedTab } = this.state;
|
|
|
|
|
|
|
|
const createAccount = window.i18n('createAccount');
|
|
|
|
const signIn = window.i18n('signIn');
|
|
|
|
const isCreateSelected = selectedTab === TabType.Create;
|
|
|
|
const isSignInSelected = selectedTab === TabType.SignIn;
|
|
|
|
|
|
|
|
return (
|
|
|
|
<div className="session-registration-container">
|
|
|
|
<div className="session-registration__tab-container">
|
|
|
|
<Tab
|
|
|
|
label={createAccount}
|
|
|
|
type={TabType.Create}
|
|
|
|
isSelected={isCreateSelected}
|
|
|
|
onSelect={this.handleTabSelect}
|
|
|
|
/>
|
|
|
|
<Tab
|
|
|
|
label={signIn}
|
|
|
|
type={TabType.SignIn}
|
|
|
|
isSelected={isSignInSelected}
|
|
|
|
onSelect={this.handleTabSelect}
|
|
|
|
/>
|
|
|
|
</div>
|
|
|
|
{this.renderSections()}
|
|
|
|
</div>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
private readonly handleTabSelect = (tabType: TabType): void => {
|
|
|
|
if (tabType !== TabType.SignIn) {
|
|
|
|
this.cancelSecondaryDevice().ignore();
|
|
|
|
}
|
|
|
|
this.setState({
|
|
|
|
selectedTab: tabType,
|
|
|
|
signInMode: SignInMode.Default,
|
|
|
|
signUpMode: SignUpMode.Default,
|
|
|
|
displayName: '',
|
|
|
|
password: '',
|
|
|
|
validatePassword: '',
|
|
|
|
passwordErrorString: '',
|
|
|
|
passwordFieldsMatch: false,
|
|
|
|
mnemonicSeed: '',
|
|
|
|
hexGeneratedPubKey: '',
|
|
|
|
primaryDevicePubKey: '',
|
|
|
|
mnemonicError: undefined,
|
|
|
|
displayNameError: undefined,
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
|
|
|
private onSeedChanged(val: string) {
|
|
|
|
this.setState({
|
|
|
|
mnemonicSeed: val,
|
|
|
|
mnemonicError: !val ? window.i18n('mnemonicEmpty') : undefined,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
private onDisplayNameChanged(val: string) {
|
|
|
|
const sanitizedName = this.sanitiseNameInput(val);
|
|
|
|
const trimName = sanitizedName.trim();
|
|
|
|
|
|
|
|
this.setState({
|
|
|
|
displayName: sanitizedName,
|
|
|
|
displayNameError: !trimName ? window.i18n('displayNameEmpty') : undefined,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
private onPasswordChanged(val: string) {
|
|
|
|
this.setState({ password: val }, () => {
|
|
|
|
this.validatePassword();
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
private onPasswordVerifyChanged(val: string) {
|
|
|
|
this.setState({ validatePassword: val });
|
|
|
|
this.setState({ validatePassword: val }, () => {
|
|
|
|
this.validatePassword();
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
private renderSections() {
|
|
|
|
const { selectedTab } = this.state;
|
|
|
|
if (selectedTab === TabType.Create) {
|
|
|
|
return this.renderSignUp();
|
|
|
|
}
|
|
|
|
|
|
|
|
return this.renderSignIn();
|
|
|
|
}
|
|
|
|
|
|
|
|
private renderSignUp() {
|
|
|
|
const { signUpMode } = this.state;
|
|
|
|
switch (signUpMode) {
|
|
|
|
case SignUpMode.Default:
|
|
|
|
return (
|
|
|
|
<div className="session-registration__content">
|
|
|
|
{this.renderSignUpHeader()}
|
|
|
|
{this.renderSignUpButton()}
|
|
|
|
</div>
|
|
|
|
);
|
|
|
|
case SignUpMode.SessionIDShown:
|
|
|
|
return (
|
|
|
|
<div className="session-registration__content">
|
|
|
|
{this.renderSignUpHeader()}
|
|
|
|
<div className="session-registration__unique-session-id">
|
|
|
|
{window.i18n('yourUniqueSessionID')}
|
|
|
|
</div>
|
|
|
|
{this.renderEnterSessionID(false)}
|
|
|
|
{this.renderSignUpButton()}
|
|
|
|
{this.getRenderTermsConditionAgreement()}
|
|
|
|
</div>
|
|
|
|
);
|
|
|
|
|
|
|
|
default:
|
|
|
|
const {
|
|
|
|
passwordErrorString,
|
|
|
|
passwordFieldsMatch,
|
|
|
|
displayNameError,
|
|
|
|
displayName,
|
|
|
|
password,
|
|
|
|
} = this.state;
|
|
|
|
|
|
|
|
let enableCompleteSignUp = true;
|
|
|
|
const displayNameOK = !displayNameError && !!displayName; //display name required
|
|
|
|
const passwordsOK =
|
|
|
|
!password || (!passwordErrorString && passwordFieldsMatch); // password is valid if empty, or if no error and fields are matching
|
|
|
|
|
|
|
|
enableCompleteSignUp = displayNameOK && passwordsOK;
|
|
|
|
|
|
|
|
return (
|
|
|
|
<div className="session-registration__content">
|
|
|
|
<div className="session-registration__welcome-session">
|
|
|
|
{window.i18n('welcomeToYourSession')}
|
|
|
|
</div>
|
|
|
|
|
|
|
|
{this.renderRegistrationContent()}
|
|
|
|
<SessionButton
|
|
|
|
onClick={() => {
|
|
|
|
this.onCompleteSignUpClick();
|
|
|
|
}}
|
|
|
|
buttonType={SessionButtonType.Brand}
|
|
|
|
buttonColor={SessionButtonColor.Green}
|
|
|
|
text={window.i18n('getStarted')}
|
|
|
|
disabled={!enableCompleteSignUp}
|
|
|
|
/>
|
|
|
|
</div>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private getRenderTermsConditionAgreement() {
|
|
|
|
const { selectedTab, signInMode, signUpMode } = this.state;
|
|
|
|
if (selectedTab === TabType.Create) {
|
|
|
|
return signUpMode !== SignUpMode.Default
|
|
|
|
? this.renderTermsConditionAgreement()
|
|
|
|
: null;
|
|
|
|
} else {
|
|
|
|
return signInMode !== SignInMode.Default
|
|
|
|
? this.renderTermsConditionAgreement()
|
|
|
|
: null;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private renderSignUpHeader() {
|
|
|
|
const allUsersAreRandomly = window.i18n('allUsersAreRandomly...');
|
|
|
|
|
|
|
|
return (
|
|
|
|
<div className="session-description-long">{allUsersAreRandomly}</div>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
private renderSignUpButton() {
|
|
|
|
const { signUpMode } = this.state;
|
|
|
|
|
|
|
|
let buttonType: SessionButtonType;
|
|
|
|
let buttonColor: SessionButtonColor;
|
|
|
|
let buttonText: string;
|
|
|
|
if (signUpMode !== SignUpMode.Default) {
|
|
|
|
buttonType = SessionButtonType.Brand;
|
|
|
|
buttonColor = SessionButtonColor.Green;
|
|
|
|
buttonText = window.i18n('continue');
|
|
|
|
} else {
|
|
|
|
buttonType = SessionButtonType.BrandOutline;
|
|
|
|
buttonColor = SessionButtonColor.Green;
|
|
|
|
buttonText = window.i18n('generateSessionID');
|
|
|
|
}
|
|
|
|
|
|
|
|
return (
|
|
|
|
<SessionButton
|
|
|
|
onClick={() => {
|
|
|
|
if (signUpMode === SignUpMode.Default) {
|
|
|
|
this.onSignUpGenerateSessionIDClick().ignore();
|
|
|
|
} else {
|
|
|
|
this.onSignUpGetStartedClick();
|
|
|
|
}
|
|
|
|
}}
|
|
|
|
buttonType={buttonType}
|
|
|
|
buttonColor={buttonColor}
|
|
|
|
text={buttonText}
|
|
|
|
/>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
private async onSignUpGenerateSessionIDClick() {
|
|
|
|
this.setState(
|
|
|
|
{
|
|
|
|
signUpMode: SignUpMode.SessionIDShown,
|
|
|
|
},
|
|
|
|
() => {
|
|
|
|
window.Session.setNewSessionID(this.state.hexGeneratedPubKey);
|
|
|
|
}
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
private onSignUpGetStartedClick() {
|
|
|
|
this.setState({
|
|
|
|
signUpMode: SignUpMode.EnterDetails,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
private onCompleteSignUpClick() {
|
|
|
|
this.register('english').ignore();
|
|
|
|
}
|
|
|
|
|
|
|
|
private renderSignIn() {
|
|
|
|
return (
|
|
|
|
<div className="session-registration__content">
|
|
|
|
{this.renderRegistrationContent()}
|
|
|
|
|
|
|
|
{this.renderSignInButtons()}
|
|
|
|
{this.getRenderTermsConditionAgreement()}
|
|
|
|
</div>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
private renderRegistrationContent() {
|
|
|
|
const { signInMode, signUpMode } = this.state;
|
|
|
|
|
|
|
|
if (signInMode === SignInMode.UsingSeed) {
|
|
|
|
return (
|
|
|
|
<div className={classNames('session-registration__entry-fields')}>
|
|
|
|
<SessionInput
|
|
|
|
label={window.i18n('mnemonicSeed')}
|
|
|
|
type="password"
|
|
|
|
placeholder={window.i18n('enterSeed')}
|
|
|
|
enableShowHide={true}
|
|
|
|
onValueChanged={(val: string) => {
|
|
|
|
this.onSeedChanged(val);
|
|
|
|
}}
|
|
|
|
onEnterPressed={() => {
|
|
|
|
this.handlePressEnter();
|
|
|
|
}}
|
|
|
|
/>
|
|
|
|
{this.renderNamePasswordAndVerifyPasswordFields()}
|
|
|
|
</div>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
if (signInMode === SignInMode.LinkingDevice) {
|
|
|
|
return (
|
|
|
|
<div className="registration-content-centered">
|
|
|
|
<div className="session-signin-device-pairing-header">
|
|
|
|
{window.i18n('devicePairingHeader')}
|
|
|
|
</div>
|
|
|
|
{this.renderEnterSessionID(true)}
|
|
|
|
<SessionSpinner loading={this.state.loading} />
|
|
|
|
</div>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
if (signUpMode === SignUpMode.EnterDetails) {
|
|
|
|
return (
|
|
|
|
<div className={classNames('session-registration__entry-fields')}>
|
|
|
|
{this.renderNamePasswordAndVerifyPasswordFields()}
|
|
|
|
</div>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
private renderNamePasswordAndVerifyPasswordFields() {
|
|
|
|
const { password, passwordFieldsMatch } = this.state;
|
|
|
|
const passwordsDoNotMatch =
|
|
|
|
!passwordFieldsMatch && this.state.password
|
|
|
|
? window.i18n('passwordsDoNotMatch')
|
|
|
|
: undefined;
|
|
|
|
|
|
|
|
return (
|
|
|
|
<div className="inputfields">
|
|
|
|
<SessionInput
|
|
|
|
label={window.i18n('displayName')}
|
|
|
|
type="text"
|
|
|
|
placeholder={window.i18n('enterDisplayName')}
|
|
|
|
value={this.state.displayName}
|
|
|
|
maxLength={window.CONSTANTS.MAX_USERNAME_LENGTH}
|
|
|
|
onValueChanged={(val: string) => {
|
|
|
|
this.onDisplayNameChanged(val);
|
|
|
|
}}
|
|
|
|
onEnterPressed={() => {
|
|
|
|
this.handlePressEnter();
|
|
|
|
}}
|
|
|
|
/>
|
|
|
|
|
|
|
|
<SessionInput
|
|
|
|
label={window.i18n('optionalPassword')}
|
|
|
|
error={this.state.passwordErrorString}
|
|
|
|
type="password"
|
|
|
|
placeholder={window.i18n('enterOptionalPassword')}
|
|
|
|
maxLength={window.CONSTANTS.MAX_PASSWORD_LENGTH}
|
|
|
|
onValueChanged={(val: string) => {
|
|
|
|
this.onPasswordChanged(val);
|
|
|
|
}}
|
|
|
|
onEnterPressed={() => {
|
|
|
|
this.handlePressEnter();
|
|
|
|
}}
|
|
|
|
/>
|
|
|
|
|
|
|
|
{!!password && (
|
|
|
|
<SessionInput
|
|
|
|
label={window.i18n('verifyPassword')}
|
|
|
|
error={passwordsDoNotMatch}
|
|
|
|
type="password"
|
|
|
|
placeholder={window.i18n('verifyPassword')}
|
|
|
|
maxLength={window.CONSTANTS.MAX_PASSWORD_LENGTH}
|
|
|
|
onValueChanged={(val: string) => {
|
|
|
|
this.onPasswordVerifyChanged(val);
|
|
|
|
}}
|
|
|
|
onEnterPressed={() => {
|
|
|
|
this.handlePressEnter();
|
|
|
|
}}
|
|
|
|
/>
|
|
|
|
)}
|
|
|
|
</div>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
private renderEnterSessionID(contentEditable: boolean) {
|
|
|
|
const enterSessionIDHere = window.i18n('enterSessionIDHere');
|
|
|
|
|
|
|
|
return (
|
|
|
|
<SessionIdEditable
|
|
|
|
editable={contentEditable}
|
|
|
|
placeholder={enterSessionIDHere}
|
|
|
|
onChange={(value: string) => {
|
|
|
|
this.onSecondDeviceSessionIDChanged(value);
|
|
|
|
}}
|
|
|
|
/>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
private onSecondDeviceSessionIDChanged(value: string) {
|
|
|
|
this.setState({
|
|
|
|
primaryDevicePubKey: value,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
private renderSignInButtons() {
|
|
|
|
const { signInMode } = this.state;
|
|
|
|
|
|
|
|
const or = window.i18n('or');
|
|
|
|
|
|
|
|
if (signInMode === SignInMode.Default) {
|
|
|
|
return (
|
|
|
|
<div>
|
|
|
|
{this.renderRestoreUsingSeedButton(
|
|
|
|
SessionButtonType.BrandOutline,
|
|
|
|
SessionButtonColor.Green
|
|
|
|
)}
|
|
|
|
<h4>{or}</h4>
|
|
|
|
{this.renderLinkDeviceToExistingAccountButton()}
|
|
|
|
</div>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (signInMode === SignInMode.LinkingDevice) {
|
|
|
|
return (
|
|
|
|
<div>
|
|
|
|
{this.renderContinueYourSessionButton()}
|
|
|
|
<h4>{or}</h4>
|
|
|
|
{this.renderRestoreUsingSeedButton(
|
|
|
|
SessionButtonType.BrandOutline,
|
|
|
|
SessionButtonColor.White
|
|
|
|
)}
|
|
|
|
</div>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
return (
|
|
|
|
<div>
|
|
|
|
{this.renderContinueYourSessionButton()}
|
|
|
|
<h4>{or}</h4>
|
|
|
|
{this.renderLinkDeviceToExistingAccountButton()}
|
|
|
|
</div>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
private renderTermsConditionAgreement() {
|
|
|
|
// FIXME add link to our Terms and Conditions and privacy statement
|
|
|
|
|
|
|
|
return (
|
|
|
|
<div className="session-terms-conditions-agreement">
|
|
|
|
<SessionHtmlRenderer html={window.i18n('ByUsingThisService...')} />
|
|
|
|
</div>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
private handleContinueYourSessionClick() {
|
|
|
|
if (this.state.signInMode === SignInMode.UsingSeed) {
|
|
|
|
this.register('english').ignore();
|
|
|
|
} else {
|
|
|
|
this.registerSecondaryDevice().ignore();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private renderContinueYourSessionButton() {
|
|
|
|
const {
|
|
|
|
signUpMode,
|
|
|
|
signInMode,
|
|
|
|
passwordErrorString,
|
|
|
|
passwordFieldsMatch,
|
|
|
|
displayNameError,
|
|
|
|
mnemonicError,
|
|
|
|
primaryDevicePubKey,
|
|
|
|
displayName,
|
|
|
|
mnemonicSeed,
|
|
|
|
password,
|
|
|
|
} = this.state;
|
|
|
|
|
|
|
|
let enableContinue = true;
|
|
|
|
let text = window.i18n('continueYourSession');
|
|
|
|
const displayNameOK = !displayNameError && !!displayName; //display name required
|
|
|
|
const mnemonicOK = !mnemonicError && !!mnemonicSeed; //Mnemonic required
|
|
|
|
const passwordsOK =
|
|
|
|
!password || (!passwordErrorString && passwordFieldsMatch); // password is valid if empty, or if no error and fields are matching
|
|
|
|
if (signInMode === SignInMode.UsingSeed) {
|
|
|
|
enableContinue = displayNameOK && mnemonicOK && passwordsOK;
|
|
|
|
} else if (signInMode === SignInMode.LinkingDevice) {
|
|
|
|
enableContinue = !!primaryDevicePubKey;
|
|
|
|
text = window.i18n('linkDevice');
|
|
|
|
} else if (signUpMode === SignUpMode.EnterDetails) {
|
|
|
|
enableContinue = displayNameOK && passwordsOK;
|
|
|
|
}
|
|
|
|
|
|
|
|
return (
|
|
|
|
<SessionButton
|
|
|
|
onClick={() => {
|
|
|
|
this.handleContinueYourSessionClick();
|
|
|
|
}}
|
|
|
|
buttonType={SessionButtonType.Brand}
|
|
|
|
buttonColor={SessionButtonColor.Green}
|
|
|
|
text={text}
|
|
|
|
disabled={!enableContinue}
|
|
|
|
/>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
private renderRestoreUsingSeedButton(
|
|
|
|
buttonType: SessionButtonType,
|
|
|
|
buttonColor: SessionButtonColor
|
|
|
|
) {
|
|
|
|
return (
|
|
|
|
<SessionButton
|
|
|
|
onClick={() => {
|
|
|
|
this.cancelSecondaryDevice().ignore();
|
|
|
|
this.setState({
|
|
|
|
signInMode: SignInMode.UsingSeed,
|
|
|
|
primaryDevicePubKey: '',
|
|
|
|
mnemonicSeed: '',
|
|
|
|
displayName: '',
|
|
|
|
signUpMode: SignUpMode.Default,
|
|
|
|
});
|
|
|
|
}}
|
|
|
|
buttonType={buttonType}
|
|
|
|
buttonColor={buttonColor}
|
|
|
|
text={window.i18n('restoreUsingSeed')}
|
|
|
|
/>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
private renderLinkDeviceToExistingAccountButton() {
|
|
|
|
return (
|
|
|
|
<SessionButton
|
|
|
|
onClick={() => {
|
|
|
|
this.setState({
|
|
|
|
signInMode: SignInMode.LinkingDevice,
|
|
|
|
mnemonicSeed: '',
|
|
|
|
displayName: '',
|
|
|
|
signUpMode: SignUpMode.Default,
|
|
|
|
});
|
|
|
|
}}
|
|
|
|
buttonType={SessionButtonType.BrandOutline}
|
|
|
|
buttonColor={SessionButtonColor.White}
|
|
|
|
text={window.i18n('linkDeviceToExistingAccount')}
|
|
|
|
/>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
private handlePressEnter() {
|
|
|
|
const { signInMode, signUpMode } = this.state;
|
|
|
|
if (signUpMode === SignUpMode.EnterDetails) {
|
|
|
|
this.onCompleteSignUpClick();
|
|
|
|
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (signInMode === SignInMode.UsingSeed) {
|
|
|
|
this.handleContinueYourSessionClick();
|
|
|
|
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private trim(value: string) {
|
|
|
|
return value ? value.trim() : value;
|
|
|
|
}
|
|
|
|
|
|
|
|
private validatePassword() {
|
|
|
|
const input = this.trim(this.state.password);
|
|
|
|
const confirmationInput = this.trim(this.state.validatePassword);
|
|
|
|
|
|
|
|
// If user hasn't set a value then skip
|
|
|
|
if (!input && !confirmationInput) {
|
|
|
|
this.setState({
|
|
|
|
passwordErrorString: '',
|
|
|
|
passwordFieldsMatch: true,
|
|
|
|
});
|
|
|
|
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
const error = window.passwordUtil.validatePassword(input, window.i18n);
|
|
|
|
if (error) {
|
|
|
|
this.setState({
|
|
|
|
passwordErrorString: error,
|
|
|
|
passwordFieldsMatch: true,
|
|
|
|
});
|
|
|
|
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (input !== confirmationInput) {
|
|
|
|
this.setState({
|
|
|
|
passwordErrorString: '',
|
|
|
|
passwordFieldsMatch: false,
|
|
|
|
});
|
|
|
|
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
this.setState({
|
|
|
|
passwordErrorString: '',
|
|
|
|
passwordFieldsMatch: true,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
private sanitiseNameInput(val: string) {
|
|
|
|
return val.replace(window.displayNameRegex, '');
|
|
|
|
}
|
|
|
|
|
|
|
|
private async resetRegistration() {
|
|
|
|
await window.Signal.Data.removeAllIdentityKeys();
|
|
|
|
await window.Signal.Data.removeAllPrivateConversations();
|
|
|
|
window.Whisper.Registration.remove();
|
|
|
|
// Do not remove all items since they are only set
|
|
|
|
// at startup.
|
|
|
|
window.textsecure.storage.remove('identityKey');
|
|
|
|
window.textsecure.storage.remove('secondaryDeviceStatus');
|
|
|
|
window.ConversationController.reset();
|
|
|
|
await window.ConversationController.load();
|
|
|
|
window.Whisper.RotateSignedPreKeyListener.stop(window.Whisper.events);
|
|
|
|
}
|
|
|
|
|
|
|
|
private async register(language: string) {
|
|
|
|
const {
|
|
|
|
password,
|
|
|
|
mnemonicSeed,
|
|
|
|
displayName,
|
|
|
|
passwordErrorString,
|
|
|
|
passwordFieldsMatch,
|
|
|
|
} = this.state;
|
|
|
|
// Make sure the password is valid
|
|
|
|
|
|
|
|
const trimName = displayName.trim();
|
|
|
|
|
|
|
|
if (!trimName) {
|
|
|
|
window.pushToast({
|
|
|
|
title: window.i18n('displayNameEmpty'),
|
|
|
|
type: 'error',
|
|
|
|
id: 'invalidDisplayName',
|
|
|
|
});
|
|
|
|
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (passwordErrorString) {
|
|
|
|
window.pushToast({
|
|
|
|
title: window.i18n('invalidPassword'),
|
|
|
|
type: 'error',
|
|
|
|
id: 'invalidPassword',
|
|
|
|
});
|
|
|
|
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!!password && !passwordFieldsMatch) {
|
|
|
|
window.pushToast({
|
|
|
|
title: window.i18n('passwordsDoNotMatch'),
|
|
|
|
type: 'error',
|
|
|
|
id: 'invalidPassword',
|
|
|
|
});
|
|
|
|
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!mnemonicSeed) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Ensure we clear the secondary device registration status
|
|
|
|
window.textsecure.storage.remove('secondaryDeviceStatus');
|
|
|
|
|
|
|
|
try {
|
|
|
|
await this.resetRegistration();
|
|
|
|
|
|
|
|
await window.setPassword(password);
|
|
|
|
await this.accountManager.registerSingleDevice(
|
|
|
|
mnemonicSeed,
|
|
|
|
language,
|
|
|
|
trimName
|
|
|
|
);
|
|
|
|
trigger('openInbox');
|
|
|
|
} catch (e) {
|
|
|
|
if (typeof e === 'string') {
|
|
|
|
//this.showToast(e);
|
|
|
|
}
|
|
|
|
//this.log(e);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private async cancelSecondaryDevice() {
|
|
|
|
window.Whisper.events.off(
|
|
|
|
'secondaryDeviceRegistration',
|
|
|
|
this.onSecondaryDeviceRegistered
|
|
|
|
);
|
|
|
|
|
|
|
|
await this.resetRegistration();
|
|
|
|
}
|
|
|
|
|
|
|
|
private async registerSecondaryDevice() {
|
|
|
|
// tslint:disable-next-line: no-backbone-get-set-outside-model
|
|
|
|
if (window.textsecure.storage.get('secondaryDeviceStatus') === 'ongoing') {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
this.setState({
|
|
|
|
loading: true,
|
|
|
|
});
|
|
|
|
await this.resetRegistration();
|
|
|
|
window.textsecure.storage.put('secondaryDeviceStatus', 'ongoing');
|
|
|
|
|
|
|
|
const primaryPubKey = this.state.primaryDevicePubKey;
|
|
|
|
|
|
|
|
// Ensure only one listener
|
|
|
|
window.Whisper.events.off(
|
|
|
|
'secondaryDeviceRegistration',
|
|
|
|
this.onSecondaryDeviceRegistered
|
|
|
|
);
|
|
|
|
window.Whisper.events.once(
|
|
|
|
'secondaryDeviceRegistration',
|
|
|
|
this.onSecondaryDeviceRegistered
|
|
|
|
);
|
|
|
|
|
|
|
|
const onError = async (error: any) => {
|
|
|
|
window.log.error(error);
|
|
|
|
// clear the ... to make sure the user realize we're not doing anything
|
|
|
|
this.setState({
|
|
|
|
loading: false,
|
|
|
|
});
|
|
|
|
await this.resetRegistration();
|
|
|
|
};
|
|
|
|
|
|
|
|
const c = new window.Whisper.Conversation({
|
|
|
|
id: primaryPubKey,
|
|
|
|
type: 'private',
|
|
|
|
});
|
|
|
|
|
|
|
|
const validationError = c.validateNumber();
|
|
|
|
if (validationError) {
|
|
|
|
onError('Invalid public key').ignore();
|
|
|
|
window.pushToast({
|
|
|
|
title: window.i18n('invalidNumberError'),
|
|
|
|
type: 'error',
|
|
|
|
id: 'invalidNumberError',
|
|
|
|
});
|
|
|
|
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
try {
|
|
|
|
const fakeMnemonic = this.state.mnemonicSeed;
|
|
|
|
|
|
|
|
await this.accountManager.registerSingleDevice(
|
|
|
|
fakeMnemonic,
|
|
|
|
'english',
|
|
|
|
null
|
|
|
|
);
|
|
|
|
|
|
|
|
await this.accountManager.requestPairing(primaryPubKey);
|
|
|
|
const pubkey = window.textsecure.storage.user.getNumber();
|
|
|
|
const words = window.mnemonic.pubkey_to_secret_words(pubkey);
|
|
|
|
window.console.log(`Here is your secret:\n${words}`);
|
|
|
|
window.pushToast({
|
|
|
|
title: `${window.i18n('secretPrompt')}`,
|
|
|
|
description: words,
|
|
|
|
id: 'yourSecret',
|
|
|
|
shouldFade: false,
|
|
|
|
});
|
|
|
|
} catch (e) {
|
|
|
|
window.console.log(e);
|
|
|
|
this.setState({
|
|
|
|
loading: false,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private async onSecondaryDeviceRegistered() {
|
|
|
|
// Ensure the left menu is updated
|
|
|
|
this.setState({
|
|
|
|
loading: false,
|
|
|
|
});
|
|
|
|
trigger('userChanged', { isSecondaryDevice: true });
|
|
|
|
// will re-run the background initialisation
|
|
|
|
trigger('registration_done');
|
|
|
|
trigger('openInbox');
|
|
|
|
}
|
|
|
|
}
|