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} ) : null}
<Flex container={true} width={width} justifyContent="space-between" alignItems="center"> <Flex container={true} width={width} justifyContent="space-between" alignItems="center">
{subtitle ? <StyledText>{subtitle}</StyledText> : null} {subtitle ? <StyledText>{subtitle}</StyledText> : null}
{showPercentage ? <StyledText>{progress}%</StyledText> : null} {showPercentage ? <StyledText>{Math.floor(progress)}%</StyledText> : null}
</Flex> </Flex>
{subtitle || showPercentage ? <SpacerXL /> : null} {subtitle || showPercentage ? <SpacerXL /> : null}
<ProgressContainer color={backgroundColor} style={{ width }}> <ProgressContainer color={backgroundColor} style={{ width }}>

@ -4,11 +4,12 @@ import { useMount } from 'react-use';
import styled from 'styled-components'; import styled from 'styled-components';
import { Data } from '../../data/data'; import { Data } from '../../data/data';
import { getSwarmPollingInstance } from '../../session/apis/snode_api'; import { getSwarmPollingInstance } from '../../session/apis/snode_api';
import { ONBOARDING_TIMES } from '../../session/constants';
import { getConversationController } from '../../session/conversations'; import { getConversationController } from '../../session/conversations';
import { InvalidWordsError, NotEnoughWordsError, mnDecode } from '../../session/crypto/mnemonic'; import { InvalidWordsError, NotEnoughWordsError, mnDecode } from '../../session/crypto/mnemonic';
import { PromiseUtils, StringUtils, ToastUtils } from '../../session/utils'; import { PromiseUtils, StringUtils, ToastUtils } from '../../session/utils';
import { fromHex } from '../../session/utils/String'; import { fromHex } from '../../session/utils/String';
import { trigger } from '../../shims/events'; import { NotFoundError } from '../../session/utils/errors';
import { import {
Onboarding, Onboarding,
setGeneratedRecoveryPhrase, setGeneratedRecoveryPhrase,
@ -29,7 +30,7 @@ import { Flex } from '../basic/Flex';
import { SpacerLG, SpacerSM } from '../basic/Text'; import { SpacerLG, SpacerSM } from '../basic/Text';
import { SessionIcon, SessionIconButton } from '../icon'; import { SessionIcon, SessionIconButton } from '../icon';
import { CreateAccount, RestoreAccount, Start } from './stages'; import { CreateAccount, RestoreAccount, Start } from './stages';
import { displayNameIsValid } from './stages/CreateAccount'; import { displayNameIsValid } from './utils';
const StyledRegistrationContainer = styled(Flex)` const StyledRegistrationContainer = styled(Flex)`
width: 348px; 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. * Ask for a display name, as we will drop incoming ConfigurationMessages if any are saved on the swarm.
* We will handle a ConfigurationMessage * We will handle a ConfigurationMessage
*/ */
export async function signInWithRecovery(signInDetails: SignInDetails) { export async function signInWithNewDisplayName(signInDetails: SignInDetails) {
const { displayName, userRecoveryPhrase } = 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; const trimName = displayName ? displayNameIsValid(displayName) : undefined;
// shows toast to user about the error
if (!trimName) { if (!trimName) {
return; return;
} }
try { try {
await resetRegistration(); await resetRegistration();
await registerSingleDevice(userRecoveryPhrase, 'english', trimName); await registerSingleDevice(userRecoveryPhrase, 'english', trimName);
await setSignWithRecoveryPhrase(true); await setSignWithRecoveryPhrase(true);
trigger('openInbox');
} catch (e) { } catch (e) {
await resetRegistration(); await resetRegistration();
ToastUtils.pushToastError('registrationError', `Error: ${e.message || 'Something went wrong'}`); 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. * This will try to sign in with the user recovery phrase.
* If no ConfigurationMessage is received in 60seconds, the loading will be canceled. * 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; const { userRecoveryPhrase, errorCallback } = signInDetails;
window?.log?.info('LINKING DEVICE'); window.log.debug(`WIP: [signInAndFetchDisplayName] starting sign in....`);
try { try {
throw new NotFoundError('Got a config message from network but without a displayName...');
await resetRegistration(); await resetRegistration();
await signInByLinkingDevice(userRecoveryPhrase, 'english'); await signInByLinkingDevice(userRecoveryPhrase, 'english');
let displayNameFromNetwork = ''; let displayNameFromNetwork = '';
@ -102,20 +100,23 @@ export async function signInWithLinking(signInDetails: SignInDetails) {
await setSignInByLinking(false); await setSignInByLinking(false);
await setSignWithRecoveryPhrase(true); await setSignWithRecoveryPhrase(true);
done(displayName); done(displayName);
displayNameFromNetwork = displayName; displayNameFromNetwork = displayName;
}); });
}, 60000); }, ONBOARDING_TIMES.RECOVERY_TIMEOUT);
if (displayNameFromNetwork.length) { if (displayNameFromNetwork.length) {
// display name, avatars, groups and contacts should already be handled when this event was triggered. // 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 { } else {
window?.log?.info('Got a config message from network but without a displayName...'); window.log.debug(
throw new Error('Got a config message from network but without a displayName...'); `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. // Do not set the lastProfileUpdateTimestamp.
// We expect to get a display name from a configuration message while we are loading messages of this user // 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) { } catch (e) {
await resetRegistration(); await resetRegistration();
if (errorCallback) { if (errorCallback) {
@ -127,7 +128,10 @@ export async function signInWithLinking(signInDetails: SignInDetails) {
void errorCallback(window.i18n('recoveryPasswordErrorMessageGeneric')); 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 { useDispatch } from 'react-redux';
import { SettingsKey } from '../../../data/settings-key'; import { SettingsKey } from '../../../data/settings-key';
import { ToastUtils } from '../../../session/utils'; import { ToastUtils } from '../../../session/utils';
import { sanitizeSessionUsername } from '../../../session/utils/String';
import { trigger } from '../../../shims/events'; import { trigger } from '../../../shims/events';
import { import {
AccountCreation, AccountCreation,
@ -22,45 +21,13 @@ import { SessionInput } from '../../inputs';
import { resetRegistration } from '../RegistrationStages'; import { resetRegistration } from '../RegistrationStages';
import { OnboardContainer, OnboardDescription, OnboardHeading } from '../components'; import { OnboardContainer, OnboardDescription, OnboardHeading } from '../components';
import { BackButtonWithininContainer } from '../components/BackButton'; import { BackButtonWithininContainer } from '../components/BackButton';
import { displayNameIsValid, sanitizeDisplayNameOrToast } from '../utils';
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;
};
async function signUp(signUpDetails: { displayName: string; generatedRecoveryPhrase: string }) { async function signUp(signUpDetails: { displayName: string; generatedRecoveryPhrase: string }) {
const { displayName, generatedRecoveryPhrase } = signUpDetails; const { displayName, generatedRecoveryPhrase } = signUpDetails;
window?.log?.info('SIGNING UP'); window?.log?.info('SIGNING UP');
const trimName = displayNameIsValid(displayName); const trimName = displayNameIsValid(displayName);
// shows toast to user about the error
if (!trimName) { if (!trimName) {
return; return;
} }

@ -1,5 +1,9 @@
import { isEmpty } from 'lodash';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { useDispatch } from 'react-redux'; import { useDispatch } from 'react-redux';
import { ONBOARDING_TIMES } from '../../../session/constants';
import { NotFoundError } from '../../../session/utils/errors';
import { trigger } from '../../../shims/events';
import { import {
AccountRestoration, AccountRestoration,
setAccountRestorationStep, setAccountRestorationStep,
@ -11,54 +15,219 @@ import { SpacerLG, SpacerSM } from '../../basic/Text';
import { SessionIcon } from '../../icon'; import { SessionIcon } from '../../icon';
import { SessionInput } from '../../inputs'; import { SessionInput } from '../../inputs';
import { SessionProgressBar } from '../../loading'; import { SessionProgressBar } from '../../loading';
import { signInWithLinking } from '../RegistrationStages'; import { signInAndFetchDisplayName, signInWithNewDisplayName } from '../RegistrationStages';
import { OnboardContainer, OnboardDescription, OnboardHeading } from '../components'; import { OnboardContainer, OnboardDescription, OnboardHeading } from '../components';
import { BackButtonWithininContainer } from '../components/BackButton'; import { BackButtonWithininContainer } from '../components/BackButton';
import { sanitizeDisplayNameOrToast } from '../utils';
export const RestoreAccount = () => { export const RestoreAccount = () => {
const step = useOnboardAccountRestorationStep(); const step = useOnboardAccountRestorationStep();
const [recoveryPhrase, setRecoveryPhrase] = useState(''); const [recoveryPhrase, setRecoveryPhrase] = useState('');
const [recoveryPhraseError, setRecoveryPhraseError] = useState(undefined as string | undefined); const [recoveryPhraseError, setRecoveryPhraseError] = useState(undefined as string | undefined);
const [displayName, setDisplayName] = useState('');
const [displayNameError, setDisplayNameError] = useState<undefined | string>('');
const [progress, setProgress] = useState(0); const [progress, setProgress] = useState(0);
const dispatch = useDispatch(); const dispatch = useDispatch();
// Seed is mandatory no matter which mode // Seed is mandatory no matter which mode
const seedOK = !!recoveryPhrase && !recoveryPhraseError; 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)); 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, userRecoveryPhrase: recoveryPhrase,
errorCallback: setRecoveryPhraseError,
}); });
dispatch(setAccountRestorationStep(AccountRestoration.Complete)); dispatch(setAccountRestorationStep(AccountRestoration.Complete));
}; };
useEffect(() => { useEffect(() => {
let interval: NodeJS.Timeout; let interval: NodeJS.Timeout;
if (step === AccountRestoration.Loading) { if (step === AccountRestoration.Loading) {
interval = setInterval(() => { interval = setInterval(() => {
setProgress(oldProgress => { if (progress < 100) {
if (oldProgress === 100) { setProgress(progress + 1);
clearInterval(interval); }
return 100; window.log.debug(
} `WIP: [continueYourSession] AccountRestoration.Loading Loading progress ${progress}%`
// Increment by 100 / 15 = 6.67 each second to complete in 15 seconds );
return Math.min(oldProgress + 100 / 15, 100);
}); if (progress >= 100) {
}, 1000); 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); return () => clearInterval(interval);
}, [step]); }, [dispatch, displayName, progress, step]);
return ( return (
<OnboardContainer> <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 <Flex
container={true} container={true}
flexDirection="column" flexDirection="column"
@ -76,51 +245,6 @@ export const RestoreAccount = () => {
showPercentage={true} showPercentage={true}
/> />
</Flex> </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> </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 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 { export class NotFoundError extends Error {
public error: any; public error: any;
constructor(message: string, error: any) { constructor(message: string, error?: any) {
// 'Error' breaks prototype chain here // 'Error' breaks prototype chain here
super(message); super(message);
this.error = error; this.error = error;

@ -19,14 +19,16 @@ export enum AccountCreation {
export enum AccountRestoration { export enum AccountRestoration {
/** starting screen */ /** starting screen */
RecoveryPassword, RecoveryPassword,
/** fetching account details */ /** fetching account details, so we increment progress to 100% over 15s */
Loading, 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, DisplayName,
/** show conversation screen */ /** we have restored successfuly, show the conversation screen */
Complete, Complete,
/** TODO to be removed */
LinkDevice,
} }
export type OnboardingState = { 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 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 mnemonicLanguage 'english' only is supported
*/ */
@ -97,7 +97,7 @@ export async function signInByLinkingDevice(mnemonic: string, mnemonicLanguage:
return pubKeyString; 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 mnemonic The mnemonic generated on first app loading and to use for this brand new user
* @param mnemonicLanguage only 'english' is supported * @param mnemonicLanguage only 'english' is supported
* @param profileName the display name to register, character limit is MAX_NAME_LENGTH_BYTES * @param profileName the display name to register, character limit is MAX_NAME_LENGTH_BYTES

Loading…
Cancel
Save