feat: animated progress loader to spec

timeout display name fetch to 15 seconds, added display name inital screen still needs work
pull/3056/head
William Grant 1 year ago
parent eaa2ee1887
commit 6f84d5bede

@ -65,7 +65,7 @@ export function SessionProgressBar(props: Props) {
) : null}
<Flex container={true} width={width} justifyContent="space-between" alignItems="center">
{subtitle ? <StyledText>{subtitle}</StyledText> : null}
{showPercentage ? <StyledText>{progress}%</StyledText> : null}
{showPercentage ? <StyledText>{Math.floor(progress)}%</StyledText> : null}
</Flex>
{subtitle || showPercentage ? <SpacerXL /> : null}
<ProgressContainer color={backgroundColor} style={{ width }}>

@ -4,11 +4,12 @@ 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 { fromHex } from '../../session/utils/String';
import { trigger } from '../../shims/events';
import { NotFoundError } from '../../session/utils/errors';
import {
Onboarding,
setGeneratedRecoveryPhrase,
@ -29,7 +30,7 @@ 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 './stages/CreateAccount';
import { displayNameIsValid } from './utils';
const StyledRegistrationContainer = styled(Flex)`
width: 348px;
@ -59,22 +60,18 @@ type SignInDetails = {
* 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: SignInDetails) {
export async function signInWithNewDisplayName(signInDetails: SignInDetails) {
const { displayName, userRecoveryPhrase } = signInDetails;
window?.log?.info('RESTORING FROM SEED');
window.log.debug(`WIP: [signInWithNewDisplayName] starting sign in with new display name....`);
const trimName = displayName ? displayNameIsValid(displayName) : undefined;
// shows toast to user about the error
if (!trimName) {
return;
}
try {
await resetRegistration();
await registerSingleDevice(userRecoveryPhrase, 'english', trimName);
await setSignWithRecoveryPhrase(true);
trigger('openInbox');
} catch (e) {
await resetRegistration();
ToastUtils.pushToastError('registrationError', `Error: ${e.message || 'Something went wrong'}`);
@ -83,14 +80,15 @@ export async function signInWithRecovery(signInDetails: 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.
* 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 signInWithLinking(signInDetails: SignInDetails) {
export async function signInAndFetchDisplayName(signInDetails: SignInDetails) {
const { userRecoveryPhrase, errorCallback } = signInDetails;
window?.log?.info('LINKING DEVICE');
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 = '';
@ -102,20 +100,23 @@ export async function signInWithLinking(signInDetails: SignInDetails) {
await setSignInByLinking(false);
await setSignWithRecoveryPhrase(true);
done(displayName);
displayNameFromNetwork = displayName;
});
}, 60000);
}, ONBOARDING_TIMES.RECOVERY_TIMEOUT);
if (displayNameFromNetwork.length) {
// display name, avatars, groups and contacts should already be handled when this event was triggered.
window?.log?.info(`We got a displayName from network: "${displayNameFromNetwork}"`);
window.log.debug(
`WIP: [signInAndFetchDisplayName] we got a displayName from network: "${displayNameFromNetwork}"`
);
} else {
window?.log?.info('Got a config message from network but without a displayName...');
throw new Error('Got a config message from network but without a displayName...');
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
trigger('openInbox');
return displayNameFromNetwork;
} catch (e) {
await resetRegistration();
if (errorCallback) {
@ -127,7 +128,10 @@ export async function signInWithLinking(signInDetails: SignInDetails) {
void errorCallback(window.i18n('recoveryPasswordErrorMessageGeneric'));
}
}
window?.log?.warn('exception during registration:', e);
window.log.debug(
`WIP: [signInAndFetchDisplayName] exception during registration: ${e.message || e}`
);
return '';
}
}

@ -2,7 +2,6 @@ import { useEffect, useState } from 'react';
import { useDispatch } from 'react-redux';
import { SettingsKey } from '../../../data/settings-key';
import { ToastUtils } from '../../../session/utils';
import { sanitizeSessionUsername } from '../../../session/utils/String';
import { trigger } from '../../../shims/events';
import {
AccountCreation,
@ -22,45 +21,13 @@ import { SessionInput } from '../../inputs';
import { resetRegistration } from '../RegistrationStages';
import { OnboardContainer, OnboardDescription, OnboardHeading } from '../components';
import { BackButtonWithininContainer } from '../components/BackButton';
function sanitizeDisplayNameOrToast(
displayName: string,
setDisplayName: (sanitized: string) => void,
setDisplayNameError: (error: string | undefined) => void
) {
try {
const sanitizedName = sanitizeSessionUsername(displayName);
const trimName = sanitizedName.trim();
setDisplayName(sanitizedName);
setDisplayNameError(!trimName ? window.i18n('displayNameEmpty') : undefined);
} catch (e) {
setDisplayName(displayName);
setDisplayNameError(window.i18n('displayNameErrorDescriptionShorter'));
}
}
/**
* Returns undefined if an error happened, or the trim userName.
*
* Be sure to use the trimmed userName for creating the account.
*/
export const displayNameIsValid = (displayName: string): undefined | string => {
const trimName = displayName.trim();
if (!trimName) {
window?.log?.warn('invalid trimmed name for registration');
ToastUtils.pushToastError('invalidDisplayName', window.i18n('displayNameEmpty'));
return undefined;
}
return trimName;
};
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);
// shows toast to user about the error
if (!trimName) {
return;
}

@ -1,5 +1,9 @@
import { isEmpty } from 'lodash';
import { useEffect, useState } from 'react';
import { useDispatch } from 'react-redux';
import { ONBOARDING_TIMES } from '../../../session/constants';
import { NotFoundError } from '../../../session/utils/errors';
import { trigger } from '../../../shims/events';
import {
AccountRestoration,
setAccountRestorationStep,
@ -11,54 +15,219 @@ import { SpacerLG, SpacerSM } from '../../basic/Text';
import { SessionIcon } from '../../icon';
import { SessionInput } from '../../inputs';
import { SessionProgressBar } from '../../loading';
import { signInWithLinking } from '../RegistrationStages';
import { signInAndFetchDisplayName, signInWithNewDisplayName } from '../RegistrationStages';
import { OnboardContainer, OnboardDescription, OnboardHeading } from '../components';
import { BackButtonWithininContainer } from '../components/BackButton';
import { sanitizeDisplayNameOrToast } from '../utils';
export const RestoreAccount = () => {
const step = useOnboardAccountRestorationStep();
const [recoveryPhrase, setRecoveryPhrase] = useState('');
const [recoveryPhraseError, setRecoveryPhraseError] = useState(undefined as string | undefined);
const [displayName, setDisplayName] = useState('');
const [displayNameError, setDisplayNameError] = useState<undefined | string>('');
const [progress, setProgress] = useState(0);
const dispatch = useDispatch();
// Seed is mandatory no matter which mode
const seedOK = !!recoveryPhrase && !recoveryPhraseError;
const displayNameOK = !!displayName && !displayNameError;
const activateContinueButton = seedOK && !(step === AccountRestoration.Loading);
const activateContinueButton =
seedOK &&
!(
step ===
(AccountRestoration.Loading || AccountRestoration.Finishing || AccountRestoration.Finished)
);
const continueYourSession = async () => {
const recoverWithoutDisplayName = async () => {
setProgress(0);
dispatch(setAccountRestorationStep(AccountRestoration.Loading));
await signInWithLinking({
try {
const displayNameFromNetwork = await signInAndFetchDisplayName({
userRecoveryPhrase: recoveryPhrase,
errorCallback: setRecoveryPhraseError,
});
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.message ||
e}`
);
dispatch(setAccountRestorationStep(AccountRestoration.DisplayName));
} else {
dispatch(setAccountRestorationStep(AccountRestoration.RecoveryPassword));
}
}
};
const recoverWithDisplayName = async () => {
if (!displayNameOK) {
return;
}
void signInWithNewDisplayName({
displayName,
userRecoveryPhrase: recoveryPhrase,
errorCallback: setRecoveryPhraseError,
});
dispatch(setAccountRestorationStep(AccountRestoration.Complete));
};
useEffect(() => {
let interval: NodeJS.Timeout;
if (step === AccountRestoration.Loading) {
interval = setInterval(() => {
setProgress(oldProgress => {
if (oldProgress === 100) {
clearInterval(interval);
return 100;
}
// Increment by 100 / 15 = 6.67 each second to complete in 15 seconds
return Math.min(oldProgress + 100 / 15, 100);
});
}, 1000);
if (progress < 100) {
setProgress(progress + 1);
}
window.log.debug(
`WIP: [continueYourSession] AccountRestoration.Loading Loading progress ${progress}%`
);
if (progress >= 100) {
clearInterval(interval);
// if we didn't get the display name in time, we need to enter it manually
window.log.debug(
`WIP: [continueYourSession] AccountRestoration.Loading We didn't get the display name in time, we need to enter it manually`
);
dispatch(setAccountRestorationStep(AccountRestoration.DisplayName));
}
}, ONBOARDING_TIMES.RECOVERY_TIMEOUT / 100);
}
if (step === AccountRestoration.Finishing) {
interval = setInterval(() => {
if (progress < 100) {
setProgress(progress + 1);
}
window.log.debug(
`WIP: [continueYourSession] AccountRestoration. Finishing progress ${progress}%`
);
if (progress >= 100) {
clearInterval(interval);
dispatch(setAccountRestorationStep(AccountRestoration.Finished));
}
}, ONBOARDING_TIMES.RECOVERY_FINISHING / 100);
}
if (step === AccountRestoration.Finished) {
interval = setInterval(() => {
clearInterval(interval);
if (!isEmpty(displayName)) {
window.log.debug(
`WIP: [continueYourSession] AccountRestoration.Complete Finished progress`
);
dispatch(setAccountRestorationStep(AccountRestoration.Complete));
} else {
dispatch(setAccountRestorationStep(AccountRestoration.RecoveryPassword));
window.log.debug(
`WIP: [continueYourSession] AccountRestoration.DisplayName failed to fetch display name so we need to enter it manually`
);
}
}, ONBOARDING_TIMES.RECOVERY_FINISHED);
}
if (step === AccountRestoration.Complete) {
if (!isEmpty(displayName)) {
window.log.debug(
`WIP: [continueYourSession] AccountRestoration.Complete opening inbox for ${displayName}`
);
trigger('openInbox');
}
}
return () => clearInterval(interval);
}, [step]);
}, [dispatch, displayName, progress, step]);
return (
<OnboardContainer>
{step === AccountRestoration.Loading ? (
{step === AccountRestoration.RecoveryPassword || step === AccountRestoration.DisplayName ? (
<BackButtonWithininContainer margin={'2px 0 0 -36px'}>
<Flex
container={true}
width="100%"
flexDirection="column"
alignItems="flex-start"
margin={'0 0 0 8px'}
>
{step === AccountRestoration.RecoveryPassword ? (
<>
<Flex container={true} width={'100%'} alignItems="center">
<OnboardHeading>{window.i18n('sessionRecoveryPassword')}</OnboardHeading>
<SessionIcon
iconType="recoveryPasswordOutline"
iconSize="large"
iconColor="var(--text-primary-color)"
style={{ margin: '-4px 0 0 8px' }}
/>
</Flex>
<SpacerSM />
<OnboardDescription>{window.i18n('onboardingRecoveryPassword')}</OnboardDescription>
<SpacerLG />
<SessionInput
autoFocus={true}
type="password"
placeholder={window.i18n('enterRecoveryPhrase')}
value={recoveryPhrase}
onValueChanged={(seed: string) => {
setRecoveryPhrase(seed);
setRecoveryPhraseError(!seed ? window.i18n('recoveryPhraseEmpty') : undefined);
}}
onEnterPressed={recoverWithoutDisplayName}
error={recoveryPhraseError}
enableShowHide={true}
inputDataTestId="recovery-phrase-input"
/>
<SpacerLG />
<SessionButton
buttonColor={SessionButtonColor.White}
onClick={recoverWithoutDisplayName}
text={window.i18n('continue')}
disabled={!activateContinueButton}
dataTestId="continue-session-button"
/>
</>
) : (
<>
{/* TODO this doesn't load for some reason */}
<Flex container={true} width="100%" flexDirection="column" alignItems="flex-start">
<OnboardHeading>{window.i18n('displayNamePick')}</OnboardHeading>
<SpacerSM />
<OnboardDescription>{window.i18n('displayNameDescription')}</OnboardDescription>
<SpacerLG />
<SessionInput
autoFocus={true}
type="text"
placeholder={window.i18n('enterDisplayName')}
value={displayName}
onValueChanged={(name: string) => {
sanitizeDisplayNameOrToast(name, setDisplayName, setDisplayNameError);
}}
onEnterPressed={recoverWithDisplayName}
error={displayNameError}
inputDataTestId="display-name-input"
/>
<SpacerLG />
<SessionButton
buttonColor={SessionButtonColor.White}
onClick={recoverWithDisplayName}
text={window.i18n('continue')}
/>
</Flex>
</>
)}
</Flex>
</BackButtonWithininContainer>
) : (
<Flex
container={true}
flexDirection="column"
@ -76,51 +245,6 @@ export const RestoreAccount = () => {
showPercentage={true}
/>
</Flex>
) : (
<BackButtonWithininContainer margin={'2px 0 0 -36px'}>
<Flex
container={true}
width="100%"
flexDirection="column"
alignItems="flex-start"
margin={'0 0 0 8px'}
>
<Flex container={true} width={'100%'} alignItems="center">
<OnboardHeading>{window.i18n('sessionRecoveryPassword')}</OnboardHeading>
<SessionIcon
iconType="recoveryPasswordOutline"
iconSize="large"
iconColor="var(--text-primary-color)"
style={{ margin: '-4px 0 0 8px' }}
/>
</Flex>
<SpacerSM />
<OnboardDescription>{window.i18n('onboardingRecoveryPassword')}</OnboardDescription>
<SpacerLG />
<SessionInput
autoFocus={true}
type="password"
placeholder={window.i18n('enterRecoveryPhrase')}
value={recoveryPhrase}
onValueChanged={(seed: string) => {
setRecoveryPhrase(seed);
setRecoveryPhraseError(!seed ? window.i18n('recoveryPhraseEmpty') : undefined);
}}
onEnterPressed={continueYourSession}
error={recoveryPhraseError}
enableShowHide={true}
inputDataTestId="recovery-phrase-input"
/>
<SpacerLG />
<SessionButton
buttonColor={SessionButtonColor.White}
onClick={continueYourSession}
text={window.i18n('continue')}
disabled={!activateContinueButton}
dataTestId="continue-session-button"
/>
</Flex>
</BackButtonWithininContainer>
)}
</OnboardContainer>
);

@ -0,0 +1,34 @@
import { ToastUtils } from '../../../session/utils';
import { sanitizeSessionUsername } from '../../../session/utils/String';
export function sanitizeDisplayNameOrToast(
displayName: string,
setDisplayName: (sanitized: string) => void,
setDisplayNameError: (error: string | undefined) => void
) {
try {
const sanitizedName = sanitizeSessionUsername(displayName);
const trimName = sanitizedName.trim();
setDisplayName(sanitizedName);
setDisplayNameError(!trimName ? window.i18n('displayNameEmpty') : undefined);
} catch (e) {
setDisplayName(displayName);
setDisplayNameError(window.i18n('displayNameErrorDescriptionShorter'));
}
}
/**
* Returns undefined if an error happened, or the trim userName.
*
* Be sure to use the trimmed userName for creating the account.
*/
export const displayNameIsValid = (displayName: string): undefined | string => {
const trimName = displayName.trim();
if (!trimName) {
window?.log?.warn('invalid trimmed name for registration');
ToastUtils.pushToastError('invalidDisplayName', window.i18n('displayNameEmpty'));
return undefined;
}
return trimName;
};

@ -80,3 +80,12 @@ export const FEATURE_RELEASE_TIMESTAMPS = {
USER_CONFIG: 1690761600000, // Monday July 31st at 10am Melbourne time
};
export const ONBOARDING_TIMES = {
/** 15 seconds */
RECOVERY_TIMEOUT: 15 * DURATION.SECONDS,
/** 0.3 seconds */
RECOVERY_FINISHING: 0.3 * DURATION.SECONDS,
/** 0.2 seconds */
RECOVERY_FINISHED: 0.2 * DURATION.SECONDS,
};

@ -25,7 +25,7 @@ export class EmptySwarmError extends Error {
export class NotFoundError extends Error {
public error: any;
constructor(message: string, error: any) {
constructor(message: string, error?: any) {
// 'Error' breaks prototype chain here
super(message);
this.error = error;

@ -19,14 +19,16 @@ export enum AccountCreation {
export enum AccountRestoration {
/** starting screen */
RecoveryPassword,
/** fetching account details */
/** fetching account details, so we increment progress to 100% over 15s */
Loading,
/** we failed to fetch a display name in time so we choose a new one */
/** found account details, so we increment the remaining progress to 100% over 0.3s */
Finishing,
/** found the account details and the progress is now 100%, so we wait for 0.2s */
Finished,
/** we failed to fetch account details in time, so we enter it manually */
DisplayName,
/** show conversation screen */
/** we have restored successfuly, show the conversation screen */
Complete,
/** TODO to be removed */
LinkDevice,
}
export type OnboardingState = {

@ -74,7 +74,7 @@ export async function signInWithRecovery(
}
/**
* Sign in with a recovery phrase but trying to recover display name and avatar from the first encountered configuration message.
* 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
*/
@ -97,7 +97,7 @@ export async function signInByLinkingDevice(mnemonic: string, mnemonicLanguage:
return pubKeyString;
}
/**
* This is a signup. User has no recovery and does not try to link a device
* This signs up a new user account. User has no recovery and does not try to link a device
* @param mnemonic The mnemonic generated on first app loading and to use for this brand new user
* @param mnemonicLanguage only 'english' is supported
* @param profileName the display name to register, character limit is MAX_NAME_LENGTH_BYTES

Loading…
Cancel
Save