fix: error handling now works correctly for all stages

buttons also correctly disable themselves
pull/3056/head
William Grant 1 year ago
parent b87c265404
commit 2c83d41ccd

@ -3,13 +3,10 @@ import { useDispatch } from 'react-redux';
import { useMount } from 'react-use';
import styled from 'styled-components';
import { Data } from '../../data/data';
import { getSwarmPollingInstance } from '../../session/apis/snode_api';
import { ONBOARDING_TIMES } from '../../session/constants';
import { getConversationController } from '../../session/conversations';
import { InvalidWordsError, NotEnoughWordsError, mnDecode } from '../../session/crypto/mnemonic';
import { PromiseUtils, StringUtils, ToastUtils } from '../../session/utils';
import { mnDecode } from '../../session/crypto/mnemonic';
import { StringUtils } from '../../session/utils';
import { fromHex } from '../../session/utils/String';
import { NotFoundError } from '../../session/utils/errors';
import {
Onboarding,
setGeneratedRecoveryPhrase,
@ -19,27 +16,12 @@ import {
useOnboardGeneratedRecoveryPhrase,
useOnboardStep,
} from '../../state/onboarding/selectors/registration';
import {
generateMnemonic,
registerSingleDevice,
sessionGenerateKeyPair,
signInByLinkingDevice,
} from '../../util/accountManager';
import { Storage, setSignInByLinking, setSignWithRecoveryPhrase } from '../../util/storage';
import { generateMnemonic, sessionGenerateKeyPair } from '../../util/accountManager';
import { Storage } from '../../util/storage';
import { Flex } from '../basic/Flex';
import { SpacerLG, SpacerSM } from '../basic/Text';
import { SessionIcon, SessionIconButton } from '../icon';
import { CreateAccount, RestoreAccount, Start } from './stages';
import { displayNameIsValid } from './utils';
const StyledRegistrationContainer = styled(Flex)`
width: 348px;
.session-button {
width: 100%;
margin: 0;
}
`;
export async function resetRegistration() {
await Data.removeAll();
@ -49,91 +31,20 @@ export async function resetRegistration() {
await getConversationController().load();
}
type SignInDetails = {
userRecoveryPhrase: string;
export type RecoverDetails = {
recoveryPassword: string;
errorCallback: (error: Error) => void;
displayName?: string;
errorCallback?: (error: string) => void;
};
/**
* Sign in/restore from seed.
* Ask for a display name, as we will drop incoming ConfigurationMessages if any are saved on the swarm.
* We will handle a ConfigurationMessage
*/
export async function signInWithNewDisplayName(signInDetails: SignInDetails) {
const { displayName, userRecoveryPhrase } = signInDetails;
window.log.debug(`WIP: [signInWithNewDisplayName] starting sign in with new display name....`);
const trimName = displayName ? displayNameIsValid(displayName) : undefined;
if (!trimName) {
return;
}
try {
await resetRegistration();
await registerSingleDevice(userRecoveryPhrase, 'english', trimName);
await setSignWithRecoveryPhrase(true);
} catch (e) {
await resetRegistration();
ToastUtils.pushToastError('registrationError', `Error: ${e.message || 'Something went wrong'}`);
window?.log?.warn('exception during registration:', e);
}
}
/**
* This will try to sign in with the user recovery phrase.
* If no ConfigurationMessage is received within ONBOARDING_RECOVERY_TIMEOUT, the user will be asked to enter a display name.
*/
export async function signInAndFetchDisplayName(signInDetails: SignInDetails) {
const { userRecoveryPhrase, errorCallback } = signInDetails;
window.log.debug(`WIP: [signInAndFetchDisplayName] starting sign in....`);
try {
throw new NotFoundError('Got a config message from network but without a displayName...');
await resetRegistration();
await signInByLinkingDevice(userRecoveryPhrase, 'english');
let displayNameFromNetwork = '';
await getSwarmPollingInstance().start();
const StyledRegistrationContainer = styled(Flex)`
width: 348px;
await PromiseUtils.waitForTask(done => {
window.Whisper.events.on('configurationMessageReceived', async (displayName: string) => {
window.Whisper.events.off('configurationMessageReceived');
await setSignInByLinking(false);
await setSignWithRecoveryPhrase(true);
done(displayName);
displayNameFromNetwork = displayName;
});
}, ONBOARDING_TIMES.RECOVERY_TIMEOUT);
if (displayNameFromNetwork.length) {
// display name, avatars, groups and contacts should already be handled when this event was triggered.
window.log.debug(
`WIP: [signInAndFetchDisplayName] we got a displayName from network: "${displayNameFromNetwork}"`
);
} else {
window.log.debug(
`WIP: [signInAndFetchDisplayName] Got a config message from network but without a displayName...`
);
throw new NotFoundError('Got a config message from network but without a displayName...');
}
// Do not set the lastProfileUpdateTimestamp.
// We expect to get a display name from a configuration message while we are loading messages of this user
return displayNameFromNetwork;
} catch (e) {
await resetRegistration();
if (errorCallback) {
if (e instanceof NotEnoughWordsError) {
void errorCallback(window.i18n('recoveryPasswordErrorMessageShort'));
} else if (e instanceof InvalidWordsError) {
void errorCallback(window.i18n('recoveryPasswordErrorMessageIncorrect'));
} else {
void errorCallback(window.i18n('recoveryPasswordErrorMessageGeneric'));
}
}
window.log.debug(
`WIP: [signInAndFetchDisplayName] exception during registration: ${e.message || e}`
);
return '';
.session-button {
width: 100%;
margin: 0;
}
}
`;
export const RegistrationStages = () => {
const generatedRecoveryPhrase = useOnboardGeneratedRecoveryPhrase();

@ -1,7 +1,6 @@
import { useEffect, useState } from 'react';
import { useDispatch } from 'react-redux';
import { SettingsKey } from '../../../data/settings-key';
import { ToastUtils } from '../../../session/utils';
import { trigger } from '../../../shims/events';
import {
AccountCreation,
@ -18,37 +17,33 @@ import { Flex } from '../../basic/Flex';
import { SessionButton, SessionButtonColor } from '../../basic/SessionButton';
import { SpacerLG, SpacerSM } from '../../basic/Text';
import { SessionInput } from '../../inputs';
import { resetRegistration } from '../RegistrationStages';
import { RecoverDetails, resetRegistration } from '../RegistrationStages';
import { OnboardContainer, OnboardDescription, OnboardHeading } from '../components';
import { BackButtonWithininContainer } from '../components/BackButton';
import { displayNameIsValid, sanitizeDisplayNameOrToast } from '../utils';
async function signUp(signUpDetails: { displayName: string; generatedRecoveryPhrase: string }) {
const { displayName, generatedRecoveryPhrase } = signUpDetails;
window?.log?.info('SIGNING UP');
const trimName = displayNameIsValid(displayName);
if (!trimName) {
return;
}
async function signUp(signUpDetails: RecoverDetails) {
const { displayName, recoveryPassword, errorCallback } = signUpDetails;
window.log.debug(`WIP: [signUp] starting sign up....`);
try {
const trimName = displayNameIsValid(displayName);
await resetRegistration();
await registerSingleDevice(generatedRecoveryPhrase, 'english', trimName);
await registerSingleDevice(recoveryPassword, 'english', trimName);
await Storage.put(SettingsKey.hasSyncedInitialConfigurationItem, Date.now());
await setSignWithRecoveryPhrase(false);
trigger('openInbox');
} catch (e) {
await resetRegistration();
ToastUtils.pushToastError('registrationError', `Error: ${e.message || 'Something went wrong'}`);
window?.log?.warn('exception during registration:', e);
void errorCallback(e);
window.log.debug(`WIP: [signUp] exception during registration: ${e.message || e}`);
}
}
export const CreateAccount = () => {
const step = useOnboardAccountCreationStep();
const generatedRecoveryPhrase = useOnboardGeneratedRecoveryPhrase();
const recoveryPassword = useOnboardGeneratedRecoveryPhrase();
const hexGeneratedPubKey = useOnboardHexGeneratedPubKey();
const dispatch = useDispatch();
@ -62,18 +57,24 @@ export const CreateAccount = () => {
}
}, [step, hexGeneratedPubKey]);
const displayNameOK = !!displayName && !displayNameError;
const signUpWithDetails = () => {
if (!displayNameOK) {
return;
}
void signUp({
displayName,
generatedRecoveryPhrase,
});
const signUpWithDetails = async () => {
try {
await signUp({
displayName,
recoveryPassword,
errorCallback: e => {
setDisplayNameError(e.message || String(e));
throw e;
},
});
dispatch(setAccountCreationStep(AccountCreation.Done));
dispatch(setAccountCreationStep(AccountCreation.Done));
} catch (e) {
window.log.debug(
`WIP: [recoverAndFetchDisplayName] AccountRestoration.RecoveryPassword failed to fetch display name so we need to enter it manually. Error: ${e}`
);
dispatch(setAccountCreationStep(AccountCreation.DisplayName));
}
};
return (
@ -107,6 +108,7 @@ export const CreateAccount = () => {
buttonColor={SessionButtonColor.White}
onClick={signUpWithDetails}
text={window.i18n('continue')}
disabled={!(!!displayName && !displayNameError)}
/>
</Flex>
</BackButtonWithininContainer>

@ -1,28 +1,108 @@
import { useState } from 'react';
import { useDispatch } from 'react-redux';
import { getSwarmPollingInstance } from '../../../session/apis/snode_api';
import { ONBOARDING_TIMES } from '../../../session/constants';
import { InvalidWordsError, NotEnoughWordsError } from '../../../session/crypto/mnemonic';
import { PromiseUtils } from '../../../session/utils';
import { NotFoundError } from '../../../session/utils/errors';
import {
AccountRestoration,
setAccountRestorationStep,
} from '../../../state/onboarding/ducks/registration';
import { useOnboardAccountRestorationStep } from '../../../state/onboarding/selectors/registration';
import { registerSingleDevice, signInByLinkingDevice } from '../../../util/accountManager';
import { setSignInByLinking, setSignWithRecoveryPhrase } from '../../../util/storage';
import { Flex } from '../../basic/Flex';
import { SessionButton, SessionButtonColor } from '../../basic/SessionButton';
import { SpacerLG, SpacerSM } from '../../basic/Text';
import { SessionIcon } from '../../icon';
import { SessionInput } from '../../inputs';
import { SessionProgressBar } from '../../loading';
import { signInAndFetchDisplayName, signInWithNewDisplayName } from '../RegistrationStages';
import { RecoverDetails, resetRegistration } from '../RegistrationStages';
import { OnboardContainer, OnboardDescription, OnboardHeading } from '../components';
import { BackButtonWithininContainer } from '../components/BackButton';
import { useRecoveryProgressEffect } from '../hooks';
import { sanitizeDisplayNameOrToast } from '../utils';
import { displayNameIsValid, sanitizeDisplayNameOrToast } from '../utils';
/**
* Sign in/restore from seed.
* Ask for a display name, as we will drop incoming ConfigurationMessages if any are saved on the swarm.
* We will handle a ConfigurationMessage
*/
async function signInWithNewDisplayName(signInDetails: RecoverDetails) {
const { displayName, recoveryPassword, errorCallback } = signInDetails;
window.log.debug(`WIP: [signInWithNewDisplayName] starting sign in with new display name....`);
try {
const trimName = displayNameIsValid(displayName);
await resetRegistration();
await registerSingleDevice(recoveryPassword, 'english', trimName);
await setSignWithRecoveryPhrase(true);
} catch (e) {
await resetRegistration();
void errorCallback(e);
window.log.debug(
`WIP: [signInWithNewDisplayName] exception during registration: ${e.message || e}`
);
}
}
/**
* This will try to sign in with the user recovery phrase.
* If no ConfigurationMessage is received within ONBOARDING_RECOVERY_TIMEOUT, the user will be asked to enter a display name.
*/
async function signInAndFetchDisplayName(
signInDetails: RecoverDetails & {
/** this is used to trigger the loading animation further down the registration pipeline */
loadingAnimationCallback: () => void;
}
) {
const { recoveryPassword, errorCallback, loadingAnimationCallback } = signInDetails;
window.log.debug(`WIP: [signInAndFetchDisplayName] starting sign in....`);
let displayNameFromNetwork = '';
try {
// throw new NotFoundError('Got a config message from network but without a displayName...');
await resetRegistration();
await signInByLinkingDevice(recoveryPassword, 'english', loadingAnimationCallback);
await getSwarmPollingInstance().start();
await PromiseUtils.waitForTask(done => {
window.Whisper.events.on('configurationMessageReceived', async (displayName: string) => {
window.Whisper.events.off('configurationMessageReceived');
await setSignInByLinking(false);
await setSignWithRecoveryPhrase(true);
done(displayName);
displayNameFromNetwork = displayName;
});
}, ONBOARDING_TIMES.RECOVERY_TIMEOUT);
if (!displayNameFromNetwork.length) {
throw new NotFoundError('Got a config message from network but without a displayName...');
}
} catch (e) {
await resetRegistration();
errorCallback(e);
}
// display name, avatars, groups and contacts should already be handled when this event was triggered.
window.log.debug(
`WIP: [signInAndFetchDisplayName] we got a displayName from network: "${displayNameFromNetwork}"`
);
// Do not set the lastProfileUpdateTimestamp.
// We expect to get a display name from a configuration message while we are loading messages of this user
return displayNameFromNetwork;
}
export const RestoreAccount = () => {
const step = useOnboardAccountRestorationStep();
const [recoveryPhrase, setRecoveryPhrase] = useState('');
const [recoveryPhraseError, setRecoveryPhraseError] = useState(undefined as string | undefined);
const [recoveryPassword, setRecoveryPassword] = useState('');
const [recoveryPasswordError, setRecoveryPasswordError] = useState(
undefined as string | undefined
);
const [displayName, setDisplayName] = useState('');
const [displayNameError, setDisplayNameError] = useState<undefined | string>('');
@ -35,37 +115,58 @@ export const RestoreAccount = () => {
const recoverAndFetchDisplayName = async () => {
setProgress(0);
dispatch(setAccountRestorationStep(AccountRestoration.Loading));
try {
const displayNameFromNetwork = await signInAndFetchDisplayName({
userRecoveryPhrase: recoveryPhrase,
errorCallback: setRecoveryPhraseError,
recoveryPassword,
errorCallback: e => {
throw e;
},
loadingAnimationCallback: () => {
dispatch(setAccountRestorationStep(AccountRestoration.Loading));
},
});
setDisplayName(displayNameFromNetwork);
dispatch(setAccountRestorationStep(AccountRestoration.Finishing));
} catch (e) {
if (e instanceof NotFoundError) {
window.log.debug(
`WIP: [continueYourSession] AccountRestoration.DisplayName failed to fetch display name so we need to enter it manually. Error: ${e}`
`WIP: [recoverAndFetchDisplayName] AccountRestoration.RecoveryPassword failed to fetch display name so we need to enter it manually. Error: ${e}`
);
dispatch(setAccountRestorationStep(AccountRestoration.DisplayName));
return;
}
if (e instanceof NotEnoughWordsError) {
setRecoveryPasswordError(window.i18n('recoveryPasswordErrorMessageShort'));
} else if (e instanceof InvalidWordsError) {
setRecoveryPasswordError(window.i18n('recoveryPasswordErrorMessageIncorrect'));
} else {
dispatch(setAccountRestorationStep(AccountRestoration.RecoveryPassword));
setRecoveryPasswordError(window.i18n('recoveryPasswordErrorMessageGeneric'));
}
window.log.debug(
`WIP: [recoverAndFetchDisplayName] exception during registration: ${e.message || e}`
);
dispatch(setAccountRestorationStep(AccountRestoration.RecoveryPassword));
}
};
const recoverAndEnterDisplayName = async () => {
if (!(!!displayName && !displayNameError)) {
return;
setProgress(0);
try {
await signInWithNewDisplayName({
displayName,
recoveryPassword,
errorCallback: e => {
setDisplayNameError(e.message || String(e));
throw e;
},
});
dispatch(setAccountRestorationStep(AccountRestoration.Complete));
} catch (e) {
window.log.debug(
`WIP: [recoverAndEnterDisplayName] AccountRestoration.DisplayName failed to set the display name. Error: ${e}`
);
dispatch(setAccountRestorationStep(AccountRestoration.DisplayName));
}
await signInWithNewDisplayName({
displayName,
userRecoveryPhrase: recoveryPhrase,
});
dispatch(setAccountRestorationStep(AccountRestoration.Complete));
};
return (
@ -98,13 +199,15 @@ export const RestoreAccount = () => {
autoFocus={true}
type="password"
placeholder={window.i18n('enterRecoveryPhrase')}
value={recoveryPhrase}
value={recoveryPassword}
onValueChanged={(seed: string) => {
setRecoveryPhrase(seed);
setRecoveryPhraseError(!seed ? window.i18n('recoveryPhraseEmpty') : undefined);
setRecoveryPassword(seed);
setRecoveryPasswordError(
!seed ? window.i18n('recoveryPhraseEmpty') : undefined
);
}}
onEnterPressed={recoverAndFetchDisplayName}
error={recoveryPhraseError}
error={recoveryPasswordError}
enableShowHide={true}
inputDataTestId="recovery-phrase-input"
/>
@ -113,7 +216,7 @@ export const RestoreAccount = () => {
buttonColor={SessionButtonColor.White}
onClick={recoverAndFetchDisplayName}
text={window.i18n('continue')}
disabled={!(!!recoveryPhrase && !recoveryPhraseError)}
disabled={!(!!recoveryPassword && !recoveryPasswordError)}
dataTestId="continue-session-button"
/>
</>
@ -142,12 +245,8 @@ export const RestoreAccount = () => {
text={window.i18n('continue')}
disabled={
// TODO Fix that even if there is an error we only care if there is something in the input check Create Account
!(
!!recoveryPhrase &&
!recoveryPhraseError &&
!!displayName &&
!displayNameError
)
!(!!recoveryPassword && !recoveryPasswordError) ||
!(!!displayName && !displayNameError)
}
dataTestId="continue-session-button"
/>

@ -1,4 +1,3 @@
import { ToastUtils } from '../../../session/utils';
import { sanitizeSessionUsername } from '../../../session/utils/String';
export function sanitizeDisplayNameOrToast(
@ -22,13 +21,15 @@ export function sanitizeDisplayNameOrToast(
*
* Be sure to use the trimmed userName for creating the account.
*/
export const displayNameIsValid = (displayName: string): undefined | string => {
const trimName = displayName.trim();
export const displayNameIsValid = (displayName?: string): string => {
if (!displayName) {
throw new Error(window.i18n('displayNameEmpty'));
}
const trimName = displayName.trim();
if (!trimName) {
window?.log?.warn('invalid trimmed name for registration');
ToastUtils.pushToastError('invalidDisplayName', window.i18n('displayNameEmpty'));
return undefined;
throw new Error(window.i18n('displayNameEmpty'));
}
return trimName;
};

@ -24,14 +24,6 @@ export class InvalidWordsError extends MnemonicError {
}
}
export class LastWordMissingError extends MnemonicError {
constructor() {
super('You seem to be missing the last word in your private key, please try again');
// restore prototype chain
Object.setPrototypeOf(this, LastWordMissingError.prototype);
}
}
export class DecodingError extends MnemonicError {
constructor() {
super('Something went wrong when decoding your private key, please try again');
@ -114,7 +106,10 @@ export function mnDecode(str: string, wordsetName: string = MN_DEFAULT_WORDSET):
throw new NotEnoughWordsError();
}
if (wordset.prefixLen > 0 && wlist.length % 3 === 0) {
throw new LastWordMissingError();
window.log.error(
'mnDecode(): You seem to be missing the last word in your private key, please try again'
);
throw new NotEnoughWordsError();
}
if (wordset.prefixLen > 0) {
// Pop checksum from mnemonic

@ -77,8 +77,13 @@ export async function signInWithRecovery(
* Sign in with a recovery phrase and try to recover display name and avatar from the first encountered configuration message.
* @param mnemonic the mnemonic the user duly saved in a safe place. We will restore his sessionID based on this.
* @param mnemonicLanguage 'english' only is supported
* @param loadingAnimationCallback a callback to trigger a loading animation
*/
export async function signInByLinkingDevice(mnemonic: string, mnemonicLanguage: string) {
export async function signInByLinkingDevice(
mnemonic: string,
mnemonicLanguage: string,
loadingAnimationCallback: () => void
) {
if (!mnemonic) {
throw new Error('Session always needs a mnemonic. Either generated or given by the user');
}
@ -88,6 +93,7 @@ export async function signInByLinkingDevice(mnemonic: string, mnemonicLanguage:
const identityKeyPair = await generateKeypair(mnemonic, mnemonicLanguage);
await setSignInByLinking(true);
loadingAnimationCallback();
await createAccount(identityKeyPair);
await saveRecoveryPhrase(mnemonic);
const pubKeyString = toHex(identityKeyPair.pubKey);

Loading…
Cancel
Save