Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

LW-10244: Port HW onboarding revamp to multi wallet #1130

Merged
Original file line number Diff line number Diff line change
Expand Up @@ -56,17 +56,26 @@ export const postHogMultiWalletActions: PostHogMultiWalletActionsType = {
RECOVERY_PHRASE_INTRO_VIDEO_GOTIT_CLICK: PostHogAction.MultiWalletCreateKeepWalletSecureGotItClick,
RECOVERY_PHRASE_COPY_TO_CLIPBOARD_CLICK: PostHogAction.MultiWalletCreateSaveRecoveryPhraseCopyToClipboardClick,
RECOVERY_PHRASE_PASTE_FROM_CLIPBOARD_CLICK:
PostHogAction.MultiWalletCreateEnterRecoveryPhrasePasteFromClipboardClick
PostHogAction.MultiWalletCreateEnterRecoveryPhrasePasteFromClipboardClick,
WALLET_ADDED: PostHogAction.MultiWalletCreateAdded
},
restore: {
SETUP_OPTION_CLICK: PostHogAction.MultiWalletRestoreClick,
ENTER_RECOVERY_PHRASE_NEXT_CLICK: PostHogAction.MultiWalletRestoreEnterRecoveryPhraseNextClick,
ENTER_WALLET: PostHogAction.MultiWalletRestoreEnterWalletClick,
RECOVERY_PHRASE_PASTE_FROM_CLIPBOARD_CLICK:
PostHogAction.MultiWalletRestoreEnterRecoveryPhrasePasteFromClipboardClick,
RECOVERY_PHRASE_PASTE_READ_MORE_CLICK: PostHogAction.MultiWalletCreateSaveRecoveryPhrasePasteReadMoreClick
RECOVERY_PHRASE_PASTE_READ_MORE_CLICK: PostHogAction.MultiWalletCreateSaveRecoveryPhrasePasteReadMoreClick,
WALLET_ADDED: PostHogAction.MultiWalletRestoreAdded,
HD_WALLET: PostHogAction.MultiWalletRestoreHdWallet
},
hw: {
SETUP_OPTION_CLICK: PostHogAction.MultiWalletHWClick
SETUP_OPTION_CLICK: PostHogAction.MultiWalletHWClick,
CONNECT_HW_VIEW: PostHogAction.MultiWalletHWConnectView,
HW_POPUP_CONNECT_CLICK: PostHogAction.MultiWalletHWPopupConnectClick,
CONNECT_HW_TRY_AGAIN_CLICK: PostHogAction.MultiWalletHWConnectTryAgainClick,
SETUP_HW_ACCOUNT_NO_CLICK: PostHogAction.MultiWalletHWSetupWalletAccountNoClick,
ENTER_WALLET: PostHogAction.MultiWalletHWEnterWalletClick,
WALLET_ADDED: PostHogAction.MultiWalletHWAdded
}
};
Original file line number Diff line number Diff line change
Expand Up @@ -61,10 +61,12 @@ export type PostHogActionsKeys =
| 'RECOVERY_PHRASE_PASTE_FROM_CLIPBOARD_CLICK'
| 'RECOVERY_PASSPHRASE_VERIFICATION_NEXT_CLICK'
| 'RECOVERY_PHRASE_COPY_READ_MORE_CLICK'
| 'RECOVERY_PHRASE_PASTE_READ_MORE_CLICK';
| 'RECOVERY_PHRASE_PASTE_READ_MORE_CLICK'
| 'WALLET_ADDED'
| 'HD_WALLET';
export type PostHogOnboardingActionsValueType = Partial<Record<PostHogActionsKeys, PostHogAction>>;
export type PostHogOnboardingActionsType = Record<OnboardingFlows, PostHogOnboardingActionsValueType>;
export type PostHogMultiWalletActionsValueType = Partial<Record<PostHogActionsKeys, PostHogAction>>;
export type PostHogOnboardingActionsType = Partial<Record<OnboardingFlows, PostHogOnboardingActionsValueType>>;
export type PostHogMultiWalletActionsType = Record<MultiWalletFlows, PostHogMultiWalletActionsValueType>;
export type PostHogPersonProperties = {
$set: {
Expand Down
5 changes: 2 additions & 3 deletions apps/browser-extension-wallet/src/routes/wallet-paths.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,8 @@ export const walletRoutePaths = {
hardware: {
root: '/new-wallet/hardware',
connect: '/new-wallet/hardware/connect',
select: '/new-wallet/hardware/select',
name: '/new-wallet/hardware/name',
allDone: '/new-wallet/hardware/all-done'
setup: '/new-wallet/hardware/setup',
create: '/new-wallet/hardware/create'
},
restore: {
root: '/new-wallet/restore',
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/* eslint-disable unicorn/no-null */
/* eslint-disable react/no-multi-comp */
import React, { useEffect, useMemo } from 'react';
import React from 'react';
import { Route, Switch, useHistory, useRouteMatch } from 'react-router-dom';
import { Modal } from 'antd';

Expand All @@ -18,101 +18,38 @@ import { CreateWallet } from './create-wallet';
import { HardwareWallet } from './hardware-wallet';
import { RestoreWallet } from './restore-wallet';
import { walletRoutePaths } from '@routes';
import { useWalletManager } from '@hooks';
import { Subject } from 'rxjs';
import { Wallet } from '@lace/cardano';
import { NavigationButton, PostHogAction, toast } from '@lace/common';
import { NavigationButton } from '@lace/common';
import { useBackgroundPage } from '@providers/BackgroundPageProvider';
import { Providers } from './hardware-wallet/types';
import { TOAST_DEFAULT_DURATION } from '@hooks/useActionExecution';
import { useTranslation } from 'react-i18next';
import { WalletConflictError } from '@cardano-sdk/web-extension';
import { useAnalyticsContext } from '@providers';
import { getWalletAccountsQtyString } from '@src/utils/get-wallet-count-string';

const { newWallet } = walletRoutePaths;

const createWallet = (): Promise<void> => Promise.resolve(void 0);

interface ConfirmationDialog {
shouldShowDialog$: Subject<boolean>;
interface Props {
shouldShowConfirmationDialog$: Subject<boolean>;
}

export const SetupHardwareWallet = ({ shouldShowDialog$ }: ConfirmationDialog): JSX.Element => {
const { t } = useTranslation();
const { connectHardwareWallet, createHardwareWallet, walletRepository } = useWalletManager();
const analytics = useAnalyticsContext();
const disconnectHardwareWallet$ = useMemo(() => new Subject<USBConnectionEvent>(), []);

const hardwareWalletProviders = useMemo(
(): Providers => ({
connectHardwareWallet,
disconnectHardwareWallet$,
shouldShowDialog$,
createWallet: async ({ account, connection, model, name }) => {
try {
const { source } = await createHardwareWallet({
connectedDevice: model,
deviceConnection: connection,
name,
accountIndex: account
});
await analytics.sendEventToPostHog(PostHogAction.MultiWalletHWAdded, {
// eslint-disable-next-line camelcase
$set: { wallet_accounts_quantity: await getWalletAccountsQtyString(walletRepository) }
});
await analytics.sendMergeEvent(source.account.extendedAccountPublicKey);
} catch (error) {
if (error instanceof WalletConflictError) {
toast.notify({ duration: TOAST_DEFAULT_DURATION, text: t('multiWallet.walletAlreadyExists') });
} else {
throw error;
}
}
}
}),
[
connectHardwareWallet,
createHardwareWallet,
disconnectHardwareWallet$,
shouldShowDialog$,
t,
analytics,
walletRepository
]
);

useEffect(() => {
const onHardwareWalletDisconnect = (event: USBConnectionEvent) => {
disconnectHardwareWallet$.next(event);
};

navigator.usb.addEventListener('disconnect', onHardwareWalletDisconnect);

return () => {
navigator.usb.removeEventListener('disconnect', onHardwareWalletDisconnect);
disconnectHardwareWallet$.complete();
};
}, [disconnectHardwareWallet$]);

return <HardwareWallet providers={hardwareWalletProviders} />;
};
export const SetupHardwareWallet = ({ shouldShowConfirmationDialog$ }: Props): JSX.Element => (
<HardwareWallet
providers={{
shouldShowConfirmationDialog$
}}
/>
);

export const SetupCreateWallet = (confirmationDialog: ConfirmationDialog): JSX.Element => (
export const SetupCreateWallet = ({ shouldShowConfirmationDialog$ }: Props): JSX.Element => (
<CreateWallet
providers={{
createWallet,
generateMnemonicWords: Wallet.KeyManagement.util.generateMnemonicWords,
confirmationDialog
shouldShowConfirmationDialog$
}}
/>
);

export const SetupRestoreWallet = (confirmationDialog: ConfirmationDialog): JSX.Element => (
export const SetupRestoreWallet = ({ shouldShowConfirmationDialog$ }: Props): JSX.Element => (
<RestoreWallet
providers={{
createWallet,
confirmationDialog
shouldShowConfirmationDialog$
}}
/>
);
Expand All @@ -136,15 +73,15 @@ const Component = (): JSX.Element => {
<Switch>
<Route
path={newWallet.create.root}
render={() => <SetupCreateWallet shouldShowDialog$={shouldShowDialog$} />}
render={() => <SetupCreateWallet shouldShowConfirmationDialog$={shouldShowDialog$} />}
/>
<Route
path={newWallet.hardware.root}
render={() => <SetupHardwareWallet shouldShowDialog$={shouldShowDialog$} />}
render={() => <SetupHardwareWallet shouldShowConfirmationDialog$={shouldShowDialog$} />}
/>
<Route
path={newWallet.restore.root}
render={() => <SetupRestoreWallet shouldShowDialog$={shouldShowDialog$} />}
render={() => <SetupRestoreWallet shouldShowConfirmationDialog$={shouldShowDialog$} />}
/>
<Route exact path={`${path}/`} component={Home} />
</Switch>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,18 +60,14 @@ describe('Multi Wallet Setup/Create Wallet', () => {
let providers = {} as {
createWallet: jest.Mock;
generateMnemonicWords: jest.Mock;
confirmationDialog: {
shouldShowDialog$: BehaviorSubject<boolean>;
};
shouldShowConfirmationDialog$: BehaviorSubject<boolean>;
};

beforeEach(() => {
providers = {
createWallet: jest.fn(),
generateMnemonicWords: jest.fn(),
confirmationDialog: {
shouldShowDialog$: new BehaviorSubject(false)
}
shouldShowConfirmationDialog$: new BehaviorSubject(false)
};
});

Expand Down Expand Up @@ -112,15 +108,15 @@ describe('Multi Wallet Setup/Create Wallet', () => {
</AppSettingsProvider>
);

expect(await firstValueFrom(providers.confirmationDialog.shouldShowDialog$)).toBe(false);
expect(await firstValueFrom(providers.shouldShowConfirmationDialog$)).toBe(false);

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

const backButton = getBackButton();
fireEvent.click(backButton);
fireEvent.click(screen.queryByTestId('delete-address-modal-confirm'));
expect(await firstValueFrom(providers.confirmationDialog.shouldShowDialog$)).toBe(false);
expect(await firstValueFrom(providers.shouldShowConfirmationDialog$)).toBe(false);
});
});
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { CreateWalletParams } from '@hooks';
import { PostHogAction } from '@lace/common';
import { walletRoutePaths } from '@routes';
import { postHogMultiWalletActions } from '@providers/AnalyticsProvider/analyticsTracker';
import React, { createContext, useContext, useState } from 'react';
import { useHistory } from 'react-router';
import { useHotWalletCreation } from '../useHotWalletCreation';
Expand Down Expand Up @@ -58,14 +58,14 @@ export const CreateWalletProvider = ({ children, providers }: Props): React.Reac
};

const setFormDirty = (dirty: boolean) => {
providers.confirmationDialog.shouldShowDialog$.next(dirty);
providers.shouldShowConfirmationDialog$.next(dirty);
};

const finalizeWalletCreation = async () => {
const wallet = await createHotWallet();
await sendPostWalletAddAnalytics({
extendedAccountPublicKey: wallet.source.account.extendedAccountPublicKey,
walletAddedPostHogAction: PostHogAction.MultiWalletCreateAdded
walletAddedPostHogAction: postHogMultiWalletActions.create.WALLET_ADDED
});
clearSecrets();
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,6 @@ export interface Data {
}

export interface Providers {
createWallet: (params: Data) => Promise<void>;
generateMnemonicWords: () => string[];
confirmationDialog: {
shouldShowDialog$: Subject<boolean>;
};
shouldShowConfirmationDialog$: Subject<boolean>;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { makeErrorDialog } from '@views/browser/features/wallet-setup/components/HardwareWalletFlow';
import { ErrorDialogCode } from './types';

const commonErrorDialogTranslationKeys = {
title: 'browserView.onboarding.errorDialog.title' as const,
confirm: 'browserView.onboarding.errorDialog.cta' as const
};
export const ErrorDialog = makeErrorDialog<ErrorDialogCode>({
szymonmaslowski marked this conversation as resolved.
Show resolved Hide resolved
[ErrorDialogCode.DeviceDisconnected]: {
...commonErrorDialogTranslationKeys,
description: 'browserView.onboarding.errorDialog.messageDeviceDisconnected'
},
[ErrorDialogCode.PublicKeyExportRejected]: {
...commonErrorDialogTranslationKeys,
description: 'browserView.onboarding.errorDialog.messagePublicKeyExportRejected'
},
[ErrorDialogCode.Generic]: {
...commonErrorDialogTranslationKeys,
description: 'browserView.onboarding.errorDialog.messageGeneric'
}
});
Original file line number Diff line number Diff line change
Expand Up @@ -60,15 +60,15 @@ describe('Multi Wallet Setup/Hardware Wallet', () => {
connectHardwareWallet: jest.Mock;
createWallet: jest.Mock;
disconnectHardwareWallet$: Subject<USBConnectionEvent>;
shouldShowDialog$: Subject<boolean>;
shouldShowConfirmationDialog$: Subject<boolean>;
};

beforeEach(() => {
providers = {
connectHardwareWallet: jest.fn(),
createWallet: jest.fn(),
disconnectHardwareWallet$: new Subject<USBConnectionEvent>(),
shouldShowDialog$: new Subject()
shouldShowConfirmationDialog$: new Subject()
};
});

Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { walletRoutePaths } from '@routes';
import { StartOverDialog } from '@views/browser/features/wallet-setup/components/StartOverDialog';
import React from 'react';
import { Redirect, Route, Switch } from 'react-router-dom';
import { Connect } from './steps/Connect';
import { SelectAccount } from './steps/SelectAccount';
import { NameWallet } from './steps/NameWallet';
import { walletRoutePaths } from '@routes';
import { HardwareWalletProvider } from './context';
import { ErrorDialog } from './ErrorDialog';
import { Connect } from './steps/Connect';
import { Setup } from './steps/Setup';
import { Create } from './steps/Create';
import { Providers } from './types';

const {
Expand All @@ -17,11 +19,21 @@ interface Props {

export const HardwareWallet = ({ providers }: Props): JSX.Element => (
<HardwareWalletProvider providers={providers}>
<Switch>
<Route path={hardware.connect} component={Connect} />
<Route path={hardware.select} component={SelectAccount} />
<Route path={hardware.name} component={NameWallet} />
<Redirect from={hardware.root} to={hardware.connect} />
</Switch>
{({ errorDialogCode, onErrorDialogRetry, isStartOverDialogVisible, onStartOverDialogAction }) => (
<>
{!!errorDialogCode && <ErrorDialog visible onRetry={onErrorDialogRetry} errorCode={errorDialogCode} />}
<StartOverDialog
visible={isStartOverDialogVisible}
onStartOver={() => onStartOverDialogAction(true)}
onClose={() => onStartOverDialogAction(false)}
/>
<Switch>
<Route path={hardware.connect} component={Connect} />
<Route path={hardware.setup} component={Setup} />
<Route path={hardware.create} component={Create} />
<Redirect from={hardware.root} to={hardware.connect} />
</Switch>
</>
)}
</HardwareWalletProvider>
);
Loading
Loading