Skip to content

Commit

Permalink
feat(extension,common,core): LW-10193 LW-10332 LW-10073 revamp multi-…
Browse files Browse the repository at this point in the history
…wallet restore and create flows (#1100)

* refactor(core): generate options of the mnemonic length picker

* refactor(extension): apply review sugestions

* test(extension): fix multi-wallet create and restore tests

* refactor(extension): move wallet cration and wallet add analytics code to a hook

* refactor(extension): variables renames

* fix: prevent skiping input mnemonic step when going back

* fix: post-review changes

* fix: styling of the onboarding components
  • Loading branch information
szymonmaslowski committed May 3, 2024
1 parent 0bde5f5 commit 0bac9f6
Show file tree
Hide file tree
Showing 26 changed files with 446 additions and 292 deletions.
6 changes: 3 additions & 3 deletions apps/browser-extension-wallet/src/hooks/useWalletManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ const { AVAILABLE_CHAINS, CHAIN } = config();
const DEFAULT_CHAIN_ID = Wallet.Cardano.ChainIds[CHAIN];
export const LOCK_VALUE = Buffer.from(JSON.stringify({ lock: 'lock' }), 'utf8');

export interface CreateWallet {
export interface CreateWalletParams {
name: string;
mnemonic: string[];
password: string;
Expand Down Expand Up @@ -79,7 +79,7 @@ export interface UseWalletManager {
wallets: AnyWallet<Wallet.WalletMetadata, Wallet.AccountMetadata>[],
activeWalletProps: WalletManagerActivateProps | null
) => Promise<Wallet.CardanoWallet | null>;
createWallet: (args: CreateWallet) => Promise<Wallet.CardanoWallet>;
createWallet: (args: CreateWalletParams) => Promise<Wallet.CardanoWallet>;
activateWallet: (args: Omit<WalletManagerActivateProps, 'chainId'>) => Promise<void>;
createHardwareWallet: (args: CreateHardwareWallet) => Promise<Wallet.CardanoWallet>;
createHardwareWalletRevamped: CreateHardwareWalletRevamped;
Expand Down Expand Up @@ -459,7 +459,7 @@ export const useWalletManager = (): UseWalletManager => {
name,
password,
chainId = getCurrentChainId()
}: CreateWallet): Promise<Wallet.CardanoWallet> => {
}: CreateWalletParams): Promise<Wallet.CardanoWallet> => {
const accountIndex = 0;
const passphrase = Buffer.from(password, 'utf8');
const keyAgent = await Wallet.KeyManagement.InMemoryKeyAgent.fromBip39MnemonicWords(
Expand Down
6 changes: 2 additions & 4 deletions apps/browser-extension-wallet/src/routes/wallet-paths.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,7 @@ export const walletRoutePaths = {
create: {
root: '/new-wallet/create',
setup: '/new-wallet/create/setup',
recoveryPhrase: '/new-wallet/create/recovery-phrase',
allDone: '/new-wallet/create/all-done'
recoveryPhrase: '/new-wallet/create/recovery-phrase'
},
hardware: {
root: '/new-wallet/hardware',
Expand All @@ -39,8 +38,7 @@ export const walletRoutePaths = {
restore: {
root: '/new-wallet/restore',
setup: '/new-wallet/restore/setup',
enterRecoveryPhrase: '/new-wallet/restore/enter-recovery-phrase',
allDone: '/new-wallet/restore/all-done'
enterRecoveryPhrase: '/new-wallet/restore/enter-recovery-phrase'
}
},
sharedWallet: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,15 +35,15 @@ export const Home = (): JSX.Element => {
<WalletSetupOptionsStep
onNewWalletRequest={() => {
analytics.sendEventToPostHog(PostHogAction.MultiWalletCreateClick);
history.push(walletRoutePaths.newWallet.create.setup);
history.push(walletRoutePaths.newWallet.create.root);
}}
onHardwareWalletRequest={() => {
analytics.sendEventToPostHog(PostHogAction.MultiWalletHWClick);
history.push(walletRoutePaths.newWallet.hardware.connect);
history.push(walletRoutePaths.newWallet.hardware.root);
}}
onRestoreWalletRequest={() => {
analytics.sendEventToPostHog(PostHogAction.MultiWalletRestoreClick);
history.push(walletRoutePaths.newWallet.restore.setup);
history.push(walletRoutePaths.newWallet.restore.root);
}}
translations={walletSetupOptionsStepTranslations}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ import {
fillMnemonic,
getNextButton,
mnemonicWords,
setupStep
setupStep,
getBackButton
} from '../tests/utils';
import { StoreProvider } from '@src/stores';
import { APP_MODE_BROWSER } from '@src/utils/constants';
Expand All @@ -37,20 +38,22 @@ import { AnalyticsTracker } from '@providers/AnalyticsProvider/analyticsTracker'
import { CreateWallet } from './CreateWallet';

jest.mock('@providers/AnalyticsProvider', () => ({
useAnalyticsContext: jest.fn<Pick<AnalyticsTracker, 'sendMergeEvent' | 'sendEventToPostHog'>, []>().mockReturnValue({
sendMergeEvent: jest.fn().mockReturnValue(''),
sendEventToPostHog: jest.fn().mockReturnValue('')
})
useAnalyticsContext: jest
.fn<Pick<AnalyticsTracker, 'sendMergeEvent' | 'sendEventToPostHog' | 'sendAliasEvent'>, []>()
.mockReturnValue({
sendMergeEvent: jest.fn().mockReturnValue(''),
sendEventToPostHog: jest.fn().mockReturnValue(''),
sendAliasEvent: jest.fn().mockReturnValue('')
})
}));

const recoveryPhraseStep = async () => {
const nextButton = getNextButton();

let nextButton = getNextButton();
fireEvent.click(nextButton);

await fillMnemonic(0, DEFAULT_MNEMONIC_LENGTH);

await screen.findByText('Total wallet balance');
nextButton = getNextButton();
fireEvent.click(nextButton);
await screen.findByText("Let's set up your new wallet");
};

describe('Multi Wallet Setup/Create Wallet', () => {
Expand Down Expand Up @@ -80,7 +83,7 @@ describe('Multi Wallet Setup/Create Wallet', () => {
<AppSettingsProvider>
<DatabaseProvider>
<StoreProvider appMode={APP_MODE_BROWSER}>
<MemoryRouter initialEntries={[walletRoutePaths.newWallet.create.setup]}>
<MemoryRouter initialEntries={[walletRoutePaths.newWallet.create.root]}>
<CreateWallet providers={providers as Providers} />
{createAssetsRoute()}
</MemoryRouter>
Expand All @@ -89,16 +92,18 @@ describe('Multi Wallet Setup/Create Wallet', () => {
</AppSettingsProvider>
);

await setupStep();
await recoveryPhraseStep();
await setupStep();
});

test('should emit correct value for shouldShowDialog', async () => {
providers.generateMnemonicWords.mockReturnValue(mnemonicWords);

render(
<AppSettingsProvider>
<DatabaseProvider>
<StoreProvider appMode={APP_MODE_BROWSER}>
<MemoryRouter initialEntries={[walletRoutePaths.newWallet.create.setup]}>
<MemoryRouter initialEntries={[walletRoutePaths.newWallet.create.root]}>
<CreateWallet providers={providers as Providers} />
{createAssetsRoute()}
</MemoryRouter>
Expand All @@ -107,14 +112,14 @@ describe('Multi Wallet Setup/Create Wallet', () => {
</AppSettingsProvider>
);

const nameInput = screen.getByTestId('wallet-name-input');

fireEvent.change(nameInput, { target: { value: 'My X Wallet' } });
expect(await firstValueFrom(providers.confirmationDialog.shouldShowDialog$)).toBe(false);

const nextButton = getNextButton();
fireEvent.click(nextButton);
expect(await firstValueFrom(providers.confirmationDialog.shouldShowDialog$)).toBe(true);

fireEvent.change(nameInput, { target: { value: '' } });

const backButton = getBackButton();
fireEvent.click(backButton);
expect(await firstValueFrom(providers.confirmationDialog.shouldShowDialog$)).toBe(false);
});
});
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React from 'react';
import { Route, Switch } from 'react-router-dom';
import { Redirect, Route, Switch } from 'react-router-dom';
import { Setup } from './steps/Setup';
import { NewRecoveryPhrase } from './steps/NewRecoveryPhrase';
import { CreateWalletProvider } from './context';
Expand All @@ -17,8 +17,9 @@ interface Props {
export const CreateWallet = ({ providers }: Props): JSX.Element => (
<CreateWalletProvider providers={providers}>
<Switch>
<Route path={create.setup} component={Setup} />
<Route path={create.recoveryPhrase} component={NewRecoveryPhrase} />
<Route path={create.setup} component={Setup} />
<Redirect from={create.root} to={create.recoveryPhrase} />
</Switch>
</CreateWalletProvider>
);
Original file line number Diff line number Diff line change
@@ -1,18 +1,30 @@
import { CreateWalletParams } from '@hooks';
import { PostHogAction } from '@lace/common';
import { walletRoutePaths } from '@routes';
import React, { createContext, useContext, useState } from 'react';
import { Data, Providers } from './types';
import { useHistory } from 'react-router';
import { useHotWalletCreation } from '../useHotWalletCreation';
import { Providers } from './types';

interface Props {
children: React.ReactNode;
providers: Providers;
}

type OnNameAndPasswordChange = (state: { name: string; password: string }) => void;

export enum WalletCreateStep {
RecoveryPhraseWriteDown = 'RecoveryPhraseWriteDown',
RecoveryPhraseInput = 'RecoveryPhraseInput',
Setup = 'Setup'
}

interface State {
data: Data;
setMnemonic: (mnemonic: string[]) => void;
setName: (name: string) => void;
setPassword: (password: string) => void;
generatedMnemonic: () => void;
onChange: (state: { name: string; password: string }) => void;
back: () => void;
createWalletData: CreateWalletParams;
next: () => Promise<void>;
onNameAndPasswordChange: OnNameAndPasswordChange;
step: WalletCreateStep;
}

// eslint-disable-next-line unicorn/no-null
Expand All @@ -25,41 +37,87 @@ export const useCreateWallet = (): State => {
};

export const CreateWalletProvider = ({ children, providers }: Props): React.ReactElement => {
const [state, setState] = useState<Data>({
mnemonic: providers.generateMnemonicWords(),
name: '',
password: ''
const history = useHistory();
const {
clearSecrets,
createWallet: createHotWallet,
createWalletData,
sendPostWalletAddAnalytics,
setCreateWalletData
} = useHotWalletCreation({
initialMnemonic: providers.generateMnemonicWords()
});
const [step, setStep] = useState<WalletCreateStep>(WalletCreateStep.RecoveryPhraseWriteDown);

const generateMnemonic = () => {
setCreateWalletData((prevState) => ({ ...prevState, mnemonic: providers.generateMnemonicWords() }));
};

const setMnemonic = (mnemonic: string[]) => {
setState((prevState) => ({ ...prevState, mnemonic }));
const onNameAndPasswordChange: OnNameAndPasswordChange = ({ name, password }) => {
setCreateWalletData((prevState) => ({ ...prevState, name, password }));
};

const setName = (name: string) => {
setState((prevState) => ({ ...prevState, name }));
const setFormDirty = (dirty: boolean) => {
providers.confirmationDialog.shouldShowDialog$.next(dirty);
};

const setPassword = (password: string) => {
setState((prevState) => ({ ...prevState, password }));
const finalizeWalletCreation = async () => {
const wallet = await createHotWallet();
await sendPostWalletAddAnalytics({
extendedAccountPublicKey: wallet.source.account.extendedAccountPublicKey,
walletAddedPostHogAction: PostHogAction.MultiWalletCreateAdded
});
clearSecrets();
};

const generatedMnemonic = () => {
setState((prevState) => ({ ...prevState, mnemonic: providers.generateMnemonicWords() }));
const next = async () => {
switch (step) {
case WalletCreateStep.RecoveryPhraseWriteDown: {
setStep(WalletCreateStep.RecoveryPhraseInput);
break;
}
case WalletCreateStep.RecoveryPhraseInput: {
setStep(WalletCreateStep.Setup);
setFormDirty(true);
history.push(walletRoutePaths.newWallet.create.setup);
break;
}
case WalletCreateStep.Setup: {
await finalizeWalletCreation();
history.push(walletRoutePaths.assets);
break;
}
}
};

const onChange = ({ name, password }: { name: string; password: string }) => {
providers.confirmationDialog.shouldShowDialog$.next(Boolean(name || password));
const back = () => {
switch (step) {
case WalletCreateStep.RecoveryPhraseWriteDown: {
setFormDirty(false);
history.push(walletRoutePaths.newWallet.root);
break;
}
case WalletCreateStep.RecoveryPhraseInput: {
generateMnemonic();
setStep(WalletCreateStep.RecoveryPhraseWriteDown);
break;
}
case WalletCreateStep.Setup: {
setStep(WalletCreateStep.RecoveryPhraseInput);
history.push(walletRoutePaths.newWallet.create.recoveryPhrase);
break;
}
}
};

return (
<CreateWalletContext.Provider
value={{
data: state,
setMnemonic,
setName,
setPassword,
generatedMnemonic,
onChange
back,
createWalletData,
next,
onNameAndPasswordChange,
step
}}
>
{children}
Expand Down

0 comments on commit 0bac9f6

Please sign in to comment.