Skip to content

Commit

Permalink
feat: [LW-12162] rename wallet and enabled accounts (#1695)
Browse files Browse the repository at this point in the history
* feat: rename wallet and enabled accounts

* fix: deep copy accounts metadata

* fix: improve input validation

* fix: disable renaming shared wallet
  • Loading branch information
greatertomi authored Feb 11, 2025
1 parent 1c07517 commit 39af93b
Show file tree
Hide file tree
Showing 14 changed files with 251 additions and 27 deletions.
2 changes: 1 addition & 1 deletion apps/browser-extension-wallet/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@
"@cardano-sdk/wallet": "0.51.9",
"@cardano-sdk/web-extension": "0.38.10",
"@emurgo/cip14-js": "~3.0.1",
"@input-output-hk/lace-ui-toolkit": "1.21.0",
"@input-output-hk/lace-ui-toolkit": "3.2.1",
"@lace/cardano": "0.1.0",
"@lace/common": "0.1.0",
"@lace/core": "0.1.0",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
Links,
LockWallet,
NetworkChoise,
RenameWalletDrawer,
Separator,
SettingsLink,
SignMessageLink,
Expand Down Expand Up @@ -55,6 +56,7 @@ export const DropdownMenuOverlay: VFC<Props> = ({
const { environmentName, setManageAccountsWallet, walletType, isSharedWallet, isHardwareWallet } = useWalletStore();
const [namiMigration, setNamiMigration] = useState<BackgroundStorage['namiMigration']>();
const [modalOpen, setModalOpen] = useState(false);
const [isRenamingWallet, setIsRenamingWallet] = useState(false);
const useSwitchToNamiMode = posthog?.isFeatureFlagEnabled('use-switch-to-nami-mode');

useEffect(() => {
Expand All @@ -75,7 +77,15 @@ export const DropdownMenuOverlay: VFC<Props> = ({

const goBackToMainSection = useCallback(() => setCurrentSection(Sections.Main), []);

topSection = topSection ?? <UserInfo onOpenWalletAccounts={openWalletAccounts} />;
topSection = topSection ?? (
<UserInfo
onOpenWalletAccounts={openWalletAccounts}
onOpenEditWallet={(wallet: AnyBip32Wallet<Wallet.WalletMetadata, Wallet.AccountMetadata>) => {
setManageAccountsWallet(wallet);
setIsRenamingWallet(true);
}}
/>
);

const getSignMessageLink = () => (
<>
Expand Down Expand Up @@ -174,6 +184,9 @@ export const DropdownMenuOverlay: VFC<Props> = ({
)}
{currentSection === Sections.NetworkInfo && <NetworkInfo onBack={goBackToMainSection} />}
{currentSection === Sections.WalletAccounts && <WalletAccounts onBack={goBackToMainSection} isPopup={isPopup} />}
{isRenamingWallet && (
<RenameWalletDrawer open={isRenamingWallet} popupView={isPopup} onClose={() => setIsRenamingWallet(false)} />
)}
</Menu>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
import React, { ReactElement, useMemo, useState } from 'react';
import * as KeyManagement from '@cardano-sdk/key-management';
import { Drawer, DrawerHeader, DrawerNavigation, logger, PostHogAction, toast } from '@lace/common';
import { Box, Button, Flex, Text, TextBox } from '@input-output-hk/lace-ui-toolkit';
import { useWalletStore } from '@stores';
import { TOAST_DEFAULT_DURATION, useWalletManager } from '@hooks';
import { Bip32WalletAccount } from '@cardano-sdk/web-extension';
import { Wallet } from '@lace/cardano';
import { usePostHogClientContext } from '@providers/PostHogClientProvider';
import { useTranslation } from 'react-i18next';

const getDerivationPath = (accountIndex: number) =>
`m/${KeyManagement.KeyPurpose.STANDARD}'/${KeyManagement.CardanoKeyConst.COIN_TYPE}'/${accountIndex}'`;

const MAX_CHARACTER_LENGTH = 20;

interface RenameWalletDrawerProps {
open: boolean;
popupView: boolean;
onClose: () => void;
}

const getStandardAccountsInitValues = (accounts: Bip32WalletAccount<Wallet.AccountMetadata>[]) =>
accounts
.filter((account) => account.purpose !== KeyManagement.KeyPurpose.MULTI_SIG)
.map((account) => ({
value: {
accountIndex: account.accountIndex,
metadata: {
...account.metadata,
name: account.metadata.name
}
},
errorMessage: ''
}));

export const RenameWalletDrawer = ({ popupView, onClose, open }: RenameWalletDrawerProps): ReactElement => {
const posthog = usePostHogClientContext();
const { t } = useTranslation();
const { manageAccountsWallet: wallet } = useWalletStore();
const { walletRepository } = useWalletManager();
const [newWalletName, setNewWalletName] = useState({
value: wallet.metadata.name,
errorMessage: ''
});
const [accountsData, setAccountsData] = useState(getStandardAccountsInitValues(wallet.accounts));

const isInputValid = (name: string): string => {
if (!name.trim()) return t('browserView.renameWalletDrawer.inputEmptyError');
if (name.length > MAX_CHARACTER_LENGTH)
return `${t('browserView.renameWalletDrawer.inputLengthError', { length: MAX_CHARACTER_LENGTH })}`;
return '';
};

const isSaveButtonDisabled = useMemo(
() => !!newWalletName.errorMessage || !!accountsData.some((account) => account.errorMessage),
[accountsData, newWalletName.errorMessage]
);

const renameWallet = async () => {
try {
if (isSaveButtonDisabled) return;

const currentWalletId = wallet.walletId;
await walletRepository.updateWalletMetadata({
walletId: currentWalletId,
metadata: { ...wallet.metadata, name: newWalletName.value }
});

for (const account of accountsData) {
await walletRepository.updateAccountMetadata({
walletId: currentWalletId,
accountIndex: account.value.accountIndex,
metadata: { ...account.value.metadata, name: account.value.metadata.name }
});
}

void posthog.sendEvent(PostHogAction.RenameWalletSaveClick);
toast.notify({
duration: TOAST_DEFAULT_DURATION,
text: t('browserView.renameWalletDrawer.renameSuccessful')
});
} catch (error) {
logger.error(error);
toast.notify({
duration: TOAST_DEFAULT_DURATION,
text: t('browserView.renameWalletDrawer.renameFailed')
});
} finally {
onClose();
}
};

const handleOnChangeAccountData = (event: Readonly<React.ChangeEvent<HTMLInputElement>>, index: number) => {
setAccountsData((prev) => {
const currentAccountData = [...prev];
currentAccountData[index].value.metadata.name = event.target.value;
currentAccountData[index].errorMessage = isInputValid(event.target.value);
return currentAccountData;
});
};

const handleOnClose = async () => {
await posthog.sendEvent(PostHogAction.RenameWalletCancelClick);
onClose();
};

return (
<Drawer
open={open}
onClose={handleOnClose}
title={<DrawerHeader popupView={popupView} title={t('browserView.renameWalletDrawer.title')} />}
navigation={
<DrawerNavigation
title={t('browserView.renameWalletDrawer.walletSettings')}
onCloseIconClick={!popupView ? handleOnClose : undefined}
onArrowIconClick={popupView ? handleOnClose : undefined}
/>
}
footer={
<Flex flexDirection="column" gap="$16">
<Button.CallToAction
label={t('browserView.renameWalletDrawer.save')}
w="$fill"
onClick={renameWallet}
disabled={isSaveButtonDisabled}
/>
<Button.Secondary label={t('browserView.renameWalletDrawer.cancel')} w="$fill" onClick={handleOnClose} />
</Flex>
}
popupView={popupView}
>
<>
<Box mt="$44">
<Box mb="$24">
<Text.Body.Large color="secondary">{t('browserView.renameWalletDrawer.renameWallet')}</Text.Body.Large>
</Box>
<TextBox
label={t('browserView.renameWalletDrawer.walletName')}
value={newWalletName.value}
onChange={(e) => {
const errorMessage = isInputValid(e.target.value);
setNewWalletName({
value: e.target.value,
errorMessage
});
}}
w="$fill"
errorMessage={newWalletName.errorMessage}
/>
</Box>
<Box mt="$60" mb="$16">
<Box mb="$24">
<Text.Body.Large color="secondary">
{t('browserView.renameWalletDrawer.renameEnabledAccounts')}
</Text.Body.Large>
</Box>
<Flex flexDirection="column" gap="$16">
{accountsData.map((account, index: number) => (
<Box key={account.value.accountIndex} w="$fill">
<TextBox
label={getDerivationPath(account.value.accountIndex)}
value={accountsData[index]?.value.metadata.name}
errorMessage={accountsData[index]?.errorMessage}
w="$fill"
onChange={(e) => handleOnChangeAccountData(e, index)}
/>
</Box>
))}
</Flex>
</Box>
</>
</Drawer>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ const overlayInnerStyle = {
interface UserInfoProps {
avatarVisible?: boolean;
onOpenWalletAccounts?: (wallet: AnyBip32Wallet<Wallet.WalletMetadata, Wallet.AccountMetadata>) => void;
onOpenEditWallet?: (wallet: AnyBip32Wallet<Wallet.WalletMetadata, Wallet.AccountMetadata>) => void;
}

interface RenderWalletOptionsParams {
Expand All @@ -44,7 +45,11 @@ const NO_WALLETS: AnyWallet<Wallet.WalletMetadata, Wallet.AccountMetadata>[] = [
const shortenWalletName = (text: string, length: number) =>
text.length > length ? `${text.slice(0, length)}...` : text;

export const UserInfo = ({ onOpenWalletAccounts, avatarVisible = true }: UserInfoProps): React.ReactElement => {
export const UserInfo = ({
onOpenWalletAccounts,
avatarVisible = true,
onOpenEditWallet
}: UserInfoProps): React.ReactElement => {
const { t } = useTranslation();
const { walletInfo, cardanoWallet, setIsDropdownMenuOpen } = useWalletStore();
const { activateWallet, walletRepository } = useWalletManager();
Expand Down Expand Up @@ -105,7 +110,7 @@ export const UserInfo = ({ onOpenWalletAccounts, avatarVisible = true }: UserInf
if (activeWalletId === wallet.walletId) {
return;
}
analytics.sendEventToPostHog(PostHogAction.MultiWalletSwitchWallet);
void analytics.sendEventToPostHog(PostHogAction.MultiWalletSwitchWallet);

await activateWallet({
walletId: wallet.walletId,
Expand All @@ -127,7 +132,8 @@ export const UserInfo = ({ onOpenWalletAccounts, avatarVisible = true }: UserInf
: undefined
}
{...(wallet.type !== WalletType.Script && {
onOpenAccountsMenu: () => onOpenWalletAccounts(wallet)
onOpenAccountsMenu: () => onOpenWalletAccounts(wallet),
onOpenEditWallet: () => onOpenEditWallet(wallet)
})}
/>
);
Expand All @@ -138,6 +144,7 @@ export const UserInfo = ({ onOpenWalletAccounts, avatarVisible = true }: UserInf
analytics,
fullWalletName,
getAvatar,
onOpenEditWallet,
onOpenWalletAccounts,
setIsDropdownMenuOpen,
t
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@ export * from './UserAvatar';
export * from './NetworkChoise';
export * from './NetworkInfo';
export * from './AddNewWalletLink';
export * from './RenameWalletDrawer';
Original file line number Diff line number Diff line change
Expand Up @@ -85,13 +85,13 @@ test('fresh install', async () => {
accounts: [
{
accountIndex: 0,
metadata: { name: 'Account #0' },
metadata: { name: 'Nami' },
extendedAccountPublicKey:
'a5f18f73dde7b6f11df448913d60a86bbb397a435269e5024193b293f28892fd33d1225d468aac8f5a9d3cfedceacabe80192fcf0beb5c5c9b7988151f3353cc'
},
{
accountIndex: 1,
metadata: { name: 'Account #1' },
metadata: { name: 'xxx' },
extendedAccountPublicKey:
'5280ef1287dfa35605891eb788590dbfe43b59682ada939ee111f8667d4a0847b43c08b5dce7aab937e860626e95f05ef6cc12758fa9ee16a4fc394bd9f684e4'
}
Expand Down Expand Up @@ -213,13 +213,13 @@ test('lace already installed and has no conflict', async () => {
accounts: [
{
accountIndex: 0,
metadata: { name: 'Account #0' },
metadata: { name: 'Nami' },
extendedAccountPublicKey:
'a5f18f73dde7b6f11df448913d60a86bbb397a435269e5024193b293f28892fd33d1225d468aac8f5a9d3cfedceacabe80192fcf0beb5c5c9b7988151f3353cc'
},
{
accountIndex: 1,
metadata: { name: 'Account #1' },
metadata: { name: 'xxx' },
extendedAccountPublicKey:
'5280ef1287dfa35605891eb788590dbfe43b59682ada939ee111f8667d4a0847b43c08b5dce7aab937e860626e95f05ef6cc12758fa9ee16a4fc394bd9f684e4'
}
Expand Down Expand Up @@ -444,7 +444,7 @@ test('LW-12135 one conflicting hw wallet and one new in-memory wallet', async ()
accounts: [
{
accountIndex: 0,
metadata: { name: 'Account #0' },
metadata: { name: 'Nami' },
extendedAccountPublicKey:
'a5f18f73dde7b6f11df448913d60a86bbb397a435269e5024193b293f28892fd33d1225d468aac8f5a9d3cfedceacabe80192fcf0beb5c5c9b7988151f3353cc'
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ const freshInstall: FreshInstall = async ({
},
accounts: accounts.map((account) => ({
accountIndex: account.index,
metadata: { name: accountName(account) },
metadata: { name: account.name ?? accountName(account) },
extendedAccountPublicKey: Wallet.Crypto.Bip32PublicKeyHex(account.extendedAccountPublicKey)
})),
type: Extension.WalletType.InMemory
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -255,10 +255,9 @@ export const EnterPgpPrivateKey: VFC = () => {
<PasswordBox
onChange={async (e) => {
setValidation({ error: null, success: null });
setPgpInfo({ ...pgpInfo, pgpKeyPassphrase: e.target.value });
setPgpInfo({ ...pgpInfo, pgpKeyPassphrase: e.value });
}}
label={i18n.t('core.paperWallet.privatePgpKeyPassphraseLabel')}
value={pgpInfo.pgpKeyPassphrase || ''}
onSubmit={(event) => {
event.preventDefault();
}}
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@
"trim-off-newlines": "^1.0.3"
},
"dependencies": {
"@input-output-hk/lace-ui-toolkit": "1.21.0",
"@input-output-hk/lace-ui-toolkit": "3.2.1",
"normalize.css": "^8.0.1",
"uuid": "^8.3.2"
},
Expand Down
5 changes: 4 additions & 1 deletion packages/common/src/analytics/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -317,7 +317,10 @@ export enum PostHogAction {
// dapp explorer
DappExplorerCategoryClick = 'dapp explorer | category | click',
DappExplorerDappTileClick = 'dapp explorer | dapp tile | click',
DappExplorerDetailDrawerRedirectClick = 'dapp explorer | detail drawer | redirect | click'
DappExplorerDetailDrawerRedirectClick = 'dapp explorer | detail drawer | redirect | click',
// rename wallet
RenameWalletSaveClick = 'rename wallet | save | click',
RenameWalletCancelClick = 'rename wallet | cancel | click'
}

// eslint-disable-next-line @typescript-eslint/no-explicit-any
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
import React from 'react';
import { UncontrolledPasswordBox } from '@input-output-hk/lace-ui-toolkit';
import type { UncontrolledPasswordBoxProps } from '@input-output-hk/lace-ui-toolkit';
import { PasswordBox } from '@input-output-hk/lace-ui-toolkit';
import type { PasswordBoxProps } from '@input-output-hk/lace-ui-toolkit';
import styles from './namiPassword.module.scss';

type NamiPasswordProps = Omit<
UncontrolledPasswordBoxProps,
PasswordBoxProps,
'containerClassName'
>;

export const NamiPassword = (props: NamiPasswordProps) => {
return (
<UncontrolledPasswordBox
<PasswordBox
containerClassName={styles.namiPasswordContainer}
{...props}
/>
Expand Down
Loading

0 comments on commit 39af93b

Please sign in to comment.