feat: added error messages to recovery password input

fixed remaining styling, improved errors for mnemonic code
pull/3056/head
William Grant 1 year ago
parent 5e836b9fbc
commit 7cabdba00e

@ -592,5 +592,8 @@
"onboardingAccountCreate": "Create account",
"onboardingAccountExists": "I have an account",
"sessionRecoveryPassword": "Recovery Password",
"onboardingRecoveryPassword": "Enter your recovery password to load your account. If you haven't saved it, you can find it in your app settings."
"onboardingRecoveryPassword": "Enter your recovery password to load your account. If you haven't saved it, you can find it in your app settings.",
"recoveryPasswordErrorMessageShort": "The Recovery Password you entered is not long enough. Please check and try again.",
"recoveryPasswordErrorMessageIncorrect": "Some of the words in your Recovery Password are incorrect. Please check and try again.",
"recoveryPasswordErrorMessageGeneric": "Please check your recovery password and try again."
}

@ -18,6 +18,7 @@ const StyledInputContainer = styled(Flex)<{ error: boolean }>`
color: var(--text-primary-color);
opacity: 0;
transition: opacity var(--default-duration);
text-align: center;
&.filled {
opacity: 1;
@ -67,7 +68,7 @@ const ErrorItem = (props: { id: string; error: string }) => {
);
};
const ShowHideButton = (props: { forceShow: boolean; toggleForceShow: Noop }) => {
const ShowHideButton = (props: { forceShow: boolean; toggleForceShow: Noop; error: boolean }) => {
const htmlDirection = useHTMLDirection();
const position =
htmlDirection === 'ltr' ? { right: 'var(--margins-md)' } : { left: 'var(--margins-md)' };
@ -76,7 +77,7 @@ const ShowHideButton = (props: { forceShow: boolean; toggleForceShow: Noop }) =>
return (
<SessionIconButton
iconType={'eyeDisabled'}
iconColor={'var(--text-primary-color)'}
iconColor={props.error ? 'var(--danger-color)' : 'var(--text-primary-color)'}
iconSize="huge"
iconPadding="1.25px"
onClick={props.toggleForceShow}
@ -95,7 +96,7 @@ const ShowHideButton = (props: { forceShow: boolean; toggleForceShow: Noop }) =>
return (
<SessionIconButton
iconType={'eye'}
iconColor={'var(--text-primary-color)'}
iconColor={props.error ? 'var(--danger-color)' : 'var(--text-primary-color)'}
iconSize="medium"
onClick={props.toggleForceShow}
style={{ position: 'absolute', top: '50%', transform: 'translateY(-50%)', ...position }}
@ -177,7 +178,7 @@ export const SessionInput = (props: Props) => {
autoFocus={autoFocus}
data-testid={inputDataTestId}
onChange={updateInputValue}
style={{ paddingInlineEnd: enableShowHide ? '30px' : undefined }}
style={{ paddingInlineEnd: enableShowHide ? '40px' : undefined }}
// just in case onChange isn't triggered
onBlur={updateInputValue}
onKeyDown={event => {
@ -200,6 +201,7 @@ export const SessionInput = (props: Props) => {
toggleForceShow={() => {
setForceShow(!forceShow);
}}
error={Boolean(errorString)}
/>
)}
</Flex>

@ -5,9 +5,8 @@ import styled from 'styled-components';
import { Data } from '../../data/data';
import { getSwarmPollingInstance } from '../../session/apis/snode_api';
import { getConversationController } from '../../session/conversations';
import { mnDecode } from '../../session/crypto/mnemonic';
import { InvalidWordsError, NotEnoughWordsError, mnDecode } from '../../session/crypto/mnemonic';
import { PromiseUtils, StringUtils, ToastUtils } from '../../session/utils';
import { TaskTimedOutError } from '../../session/utils/Promise';
import { fromHex } from '../../session/utils/String';
import { trigger } from '../../shims/events';
import {
@ -49,18 +48,21 @@ export async function resetRegistration() {
await getConversationController().load();
}
type SignInDetails = {
userRecoveryPhrase: string;
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 signInWithRecovery(signInDetails: {
displayName: string;
userRecoveryPhrase: string;
}) {
export async function signInWithRecovery(signInDetails: SignInDetails) {
const { displayName, userRecoveryPhrase } = signInDetails;
window?.log?.info('RESTORING FROM SEED');
const trimName = displayNameIsValid(displayName);
const trimName = displayName ? displayNameIsValid(displayName) : undefined;
// shows toast to user about the error
if (!trimName) {
return;
@ -84,8 +86,8 @@ export async function signInWithRecovery(signInDetails: {
* This is will try to sign in with the user recovery phrase.
* If no ConfigurationMessage is received in 60seconds, the loading will be canceled.
*/
export async function signInWithLinking(signInDetails: { userRecoveryPhrase: string }) {
const { userRecoveryPhrase } = signInDetails;
export async function signInWithLinking(signInDetails: SignInDetails) {
const { userRecoveryPhrase, errorCallback } = signInDetails;
window?.log?.info('LINKING DEVICE');
try {
@ -116,16 +118,14 @@ export async function signInWithLinking(signInDetails: { userRecoveryPhrase: str
trigger('openInbox');
} catch (e) {
await resetRegistration();
if (e instanceof TaskTimedOutError) {
ToastUtils.pushToastError(
'registrationError',
'Could not find your display name. Please Sign In by Restoring Your Account instead.'
);
if (errorCallback) {
if (e instanceof NotEnoughWordsError) {
void errorCallback(window.i18n('recoveryPasswordErrorMessageShort'));
} else if (e instanceof InvalidWordsError) {
void errorCallback(window.i18n('recoveryPasswordErrorMessageIncorrect'));
} else {
ToastUtils.pushToastError(
'registrationError',
`Error: ${e.message || 'Something went wrong'}`
);
void errorCallback(window.i18n('recoveryPasswordErrorMessageGeneric'));
}
}
window?.log?.warn('exception during registration:', e);
}

@ -21,17 +21,16 @@ export const RestoreAccount = () => {
const activateContinueButton = seedOK && !loading;
const continueYourSession = () => {
// TODO better error handling
// if (isRecovery) {
// void signInWithRecovery({
// displayName,
// userRecoveryPhrase: recoveryPhrase,
// });
// }
// else if (isLinking) {
setIsLoading(true);
void signInWithLinking({
userRecoveryPhrase: recoveryPhrase,
errorCallback: setRecoveryPhraseError,
});
setIsLoading(false);
};

@ -1,14 +1,59 @@
/* eslint-disable camelcase */
import crc32 from 'buffer-crc32';
class MnemonicError extends Error {}
class MnemonicError extends Error {
constructor(message: string) {
super(message);
// restore prototype chain
Object.setPrototypeOf(this, MnemonicError.prototype);
}
}
export class NotEnoughWordsError extends MnemonicError {
constructor() {
super("You've entered too few words, please try again");
// restore prototype chain
Object.setPrototypeOf(this, NotEnoughWordsError.prototype);
}
}
export class InvalidWordsError extends MnemonicError {
constructor() {
super('invalid word in mnemonic');
// restore prototype chain
Object.setPrototypeOf(this, InvalidWordsError.prototype);
}
}
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');
// restore prototype chain
Object.setPrototypeOf(this, DecodingError.prototype);
}
}
export class VerificationError extends MnemonicError {
constructor() {
super('Your private key could not be verified, please verify the checksum word');
// restore prototype chain
Object.setPrototypeOf(this, VerificationError.prototype);
}
}
/*
mnemonic.js : Converts between 4-byte aligned strings and a human-readable
mnemonic.ts : Converts between 4-byte aligned strings and a human-readable
sequence of words. Uses 1626 common words taken from wikipedia article:
http://en.wiktionary.org/wiki/Wiktionary:Frequency_lists/Contemporary_poetry
Originally written in python special for Electrum (lightweight Bitcoin client).
This version has been reimplemented in javascript and placed in public domain.
This version has been reimplemented in javascript and placed in public domain and has further been converted to TypeScript as part of the Session project.
*/
const MN_DEFAULT_WORDSET = 'english';
@ -60,18 +105,16 @@ export function mnDecode(str: string, wordsetName: string = MN_DEFAULT_WORDSET):
const wlist = str.split(' ');
let checksumWord = '';
if (wlist.length < 12) {
throw new MnemonicError("You've entered too few words, please try again");
throw new NotEnoughWordsError();
}
if (
(wordset.prefixLen === 0 && wlist.length % 3 !== 0) ||
(wordset.prefixLen > 0 && wlist.length % 3 === 2)
) {
throw new MnemonicError("You've entered too few words, please try again");
throw new NotEnoughWordsError();
}
if (wordset.prefixLen > 0 && wlist.length % 3 === 0) {
throw new MnemonicError(
'You seem to be missing the last word in your private key, please try again'
);
throw new LastWordMissingError();
}
if (wordset.prefixLen > 0) {
// Pop checksum from mnemonic
@ -92,14 +135,12 @@ export function mnDecode(str: string, wordsetName: string = MN_DEFAULT_WORDSET):
w3 = wordset.truncWords.indexOf(wlist[i + 2].slice(0, wordset.prefixLen));
}
if (w1 === -1 || w2 === -1 || w3 === -1) {
throw new MnemonicError('invalid word in mnemonic');
throw new InvalidWordsError();
}
const x = w1 + n * ((n - w1 + w2) % n) + n * n * ((n - w2 + w3) % n);
if (x % n !== w1) {
throw new MnemonicError(
'Something went wrong when decoding your private key, please try again'
);
throw new DecodingError();
}
out += mn_swap_endian_4byte(`0000000${x.toString(16)}`.slice(-8));
}
@ -110,9 +151,7 @@ export function mnDecode(str: string, wordsetName: string = MN_DEFAULT_WORDSET):
if (
expectedChecksumWord.slice(0, wordset.prefixLen) !== checksumWord.slice(0, wordset.prefixLen)
) {
throw new MnemonicError(
'Your private key could not be verified, please verify the checksum word'
);
throw new VerificationError();
}
}
return out;

@ -418,6 +418,9 @@ export type LocalizerKeys =
| 'readReceiptSettingDescription'
| 'readReceiptSettingTitle'
| 'received'
| 'recoveryPasswordErrorMessageGeneric'
| 'recoveryPasswordErrorMessageIncorrect'
| 'recoveryPasswordErrorMessageShort'
| 'recoveryPhrase'
| 'recoveryPhraseEmpty'
| 'recoveryPhraseSavePromptMain'

Loading…
Cancel
Save