Skip to content

Commit

Permalink
feat: implement rename/delete key
Browse files Browse the repository at this point in the history
  • Loading branch information
brusherru committed Sep 5, 2024
1 parent 3fd43ad commit ebccf35
Show file tree
Hide file tree
Showing 5 changed files with 286 additions and 8 deletions.
24 changes: 21 additions & 3 deletions src/components/FormKeySelect.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
FieldValues,
get,
Path,
PathValue,
UseFormRegister,
UseFormUnregister,
} from 'react-hook-form';
Expand Down Expand Up @@ -35,6 +36,8 @@ type Props<
errors: FieldErrors<T>;
isSubmitted?: boolean;
isRequired?: boolean;
defaultForeign?: boolean;
value?: string | null;
}>;

enum KeyType {
Expand All @@ -51,11 +54,25 @@ function FormKeySelect<T extends FieldValues, FieldName extends Path<T>>({
isSubmitted = false,
isRequired = false,
children = '',
value = null,
}: Props<T, FieldName>): JSX.Element {
const [keyType, setKeyType] = useState(KeyType.Local);
const isLocalValue = !value || keys.some((key) => key.publicKey === value);
const [keyType, setKeyType] = useState(
isLocalValue ? KeyType.Local : KeyType.Foreign
);
const formValue = (value || '') as PathValue<T, FieldName>;

useEffect(() => {
if (keys.some((key) => key.publicKey === value)) {
setKeyType(KeyType.Local);
}
if (!value) {
setKeyType(KeyType.Foreign);
}
}, [keys, value]);
useEffect(
() => () => unregister(fieldName),
[unregister, fieldName, keyType]
[unregister, fieldName, keyType, value]
);

const error = get(errors, fieldName) as FieldError | undefined;
Expand All @@ -65,6 +82,7 @@ function FormKeySelect<T extends FieldValues, FieldName extends Path<T>>({
return (
<Input
{...register(fieldName, {
value: formValue,
required: isRequired ? 'Please pick the key' : false,
validate: (val) =>
isHexString(val) ? true : 'Invalid Hex format',
Expand All @@ -74,7 +92,7 @@ function FormKeySelect<T extends FieldValues, FieldName extends Path<T>>({
case KeyType.Local:
default:
return (
<Select {...register(fieldName)}>
<Select {...register(fieldName, { value: formValue })}>
{keys.map((key) => (
<option key={key.publicKey} value={key.publicKey}>
{key.displayName} ({getAbbreviatedHexString(key.publicKey)})
Expand Down
35 changes: 32 additions & 3 deletions src/components/FormMultiKeySelect.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useCallback } from 'react';
import { useCallback, useEffect } from 'react';
import {
ArrayPath,
Control,
Expand All @@ -16,6 +16,7 @@ import { IconPlus, IconTrash } from '@tabler/icons-react';

import { SafeKey } from '../types/wallet';
import { BUTTON_ICON_SIZE, MAX_MULTISIG_AMOUNT } from '../utils/constants';
import { noop } from '../utils/func';

import FormKeySelect from './FormKeySelect';

Expand All @@ -27,6 +28,7 @@ type Props<T extends FieldValues, FieldName extends ArrayPath<T>> = {
unregister: UseFormUnregister<T>;
errors: FieldErrors<T>;
isSubmitted?: boolean;
values?: string[];
};

function FormMultiKeySelect<
Expand All @@ -40,16 +42,42 @@ function FormMultiKeySelect<
unregister,
errors,
isSubmitted = false,
values = [],
}: Props<T, FieldName>): JSX.Element {
const { fields, append, remove } = useFieldArray({
control,
name: fieldName,
});
const addEmptyField = useCallback(
() => append((keys[0]?.publicKey ?? '0x01') as FieldArray<T, FieldName>),
[append, keys]
() =>
append(
(keys[fields.length]?.publicKey ||
`0x${String(fields.length).padStart(2, '0')}`) as FieldArray<
T,
FieldName
>
),
[append, fields.length, keys]
);

useEffect(() => {
// Have at least one field by default
if (!values) {
append(keys[0]?.publicKey as FieldArray<T, FieldName>);
}
}, [append, keys, values]);

useEffect(() => {
if (values.length === 0) {
return noop;
}
// In case there are some values — restore them in the form
values.forEach((_, idx) => append(idx as FieldArray<T, FieldName>));
return () => {
values.forEach((_, idx) => remove(idx));
};
}, [values, append, remove]);

const rootError = errors[fieldName]?.message;
return (
<>
Expand All @@ -68,6 +96,7 @@ function FormMultiKeySelect<
errors={errors}
isSubmitted={isSubmitted}
isRequired
value={values[index] || keys[index]?.publicKey}
>
<IconButton
icon={<IconTrash size={BUTTON_ICON_SIZE} />}
Expand Down
59 changes: 57 additions & 2 deletions src/components/KeyManager.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import fileDownload from 'js-file-download';
import { useState } from 'react';

import {
Badge,
Expand All @@ -10,6 +11,7 @@ import {
DrawerContent,
DrawerOverlay,
Flex,
IconButton,
Tab,
TabList,
TabPanel,
Expand All @@ -23,15 +25,19 @@ import { StdPublicKeys } from '@spacemesh/sm-codec';
import {
IconDeviceDesktop,
IconDeviceUsb,
IconEdit,
IconEyeglass2,
IconFileImport,
IconKey,
IconPlus,
IconTrash,
} from '@tabler/icons-react';

import useConfirmation from '../hooks/useConfirmation';
import { useCurrentHRP } from '../hooks/useNetworkSelectors';
import useRevealSecretKey from '../hooks/useRevealSecretKey';
import { useAccountsList } from '../hooks/useWalletSelectors';
import usePassword from '../store/usePassword';
import useWallet from '../store/useWallet';
import {
AccountWithAddress,
Expand All @@ -55,6 +61,7 @@ import ExplorerButton from './ExplorerButton';
import ImportAccountModal from './ImportAccountModal';
import ImportKeyFromLedgerModal from './ImportKeyFromLedgerModal';
import ImportKeyPairModal from './ImportKeyPairModal';
import RenameKeyModal from './RenameKeyModal';
import RevealSecretKeyModal from './RevealSecretKeyModal';

type KeyManagerProps = {
Expand Down Expand Up @@ -94,22 +101,47 @@ const renderSingleKey = (key: SafeKeyWithType): JSX.Element =>
);

function KeyManager({ isOpen, onClose }: KeyManagerProps): JSX.Element {
const { wallet } = useWallet();
const { wallet, deleteKey } = useWallet();
const hrp = useCurrentHRP();
const accounts = useAccountsList(hrp);

const { revealSecretKey } = useRevealSecretKey();
const { withConfirmation } = useConfirmation();
const { withPassword } = usePassword();

const createKeyPairModal = useDisclosure();
const importKeyPairModal = useDisclosure();
const renameKeyModal = useDisclosure();
const importFromLedgerModal = useDisclosure();
const createAccountModal = useDisclosure();
const importAccountModal = useDisclosure();

const [renameKeyIdx, setRenameKeyIdx] = useState(0);

const closeHandler = () => {
onClose();
};

const onRenameKey = (idx: number) => {
setRenameKeyIdx(idx);
renameKeyModal.onOpen();
};
const onDeleteKey = (idx: number) =>
withConfirmation(
() =>
withPassword(
(pass) => deleteKey(idx, pass),
'Delete Key',
// eslint-disable-next-line max-len
'Please type in the password to delete the key and store the wallet secrets without it'
),
'Delete Key',
'Are you sure you want to delete this key?',
// eslint-disable-next-line max-len
'You cannot undo this action, but you always can import the key again or derive it if you know the path.',
true
);

const exportAccount = (acc: AccountWithAddress) =>
fileDownload(
JSON.stringify(acc, null, 2),
Expand Down Expand Up @@ -202,7 +234,7 @@ function KeyManager({ isOpen, onClose }: KeyManagerProps): JSX.Element {
</Button>
</Flex>
<Box flex={1}>
{(wallet?.keychain ?? []).map((key) => (
{(wallet?.keychain ?? []).map((key, idx) => (
<Box
key={key.publicKey}
mb={2}
Expand Down Expand Up @@ -245,6 +277,24 @@ function KeyManager({ isOpen, onClose }: KeyManagerProps): JSX.Element {
)}
<Text fontWeight="bold" mb={1}>
{key.displayName}
<IconButton
ml={2}
aria-label="Rename key"
onClick={() => onRenameKey(idx)}
icon={<IconEdit size={12} />}
variant="whiteOutline"
borderWidth={1}
size="xs"
/>
<IconButton
ml={2}
aria-label="Delete key"
onClick={() => onDeleteKey(idx)}
icon={<IconTrash size={12} />}
variant="dangerOutline"
borderWidth={1}
size="xs"
/>
</Text>

<Text fontSize="xx-small">Public Key</Text>
Expand Down Expand Up @@ -395,6 +445,11 @@ function KeyManager({ isOpen, onClose }: KeyManagerProps): JSX.Element {
isOpen={importFromLedgerModal.isOpen}
onClose={importFromLedgerModal.onClose}
/>
<RenameKeyModal
keyIndex={renameKeyIdx ?? 0}
isOpen={renameKeyModal.isOpen}
onClose={renameKeyModal.onClose}
/>
<RevealSecretKeyModal />
<CreateAccountModal
isOpen={createAccountModal.isOpen}
Expand Down
120 changes: 120 additions & 0 deletions src/components/RenameKeyModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import { useEffect } from 'react';
import { Form, useForm } from 'react-hook-form';

import {
Button,
Modal,
ModalBody,
ModalCloseButton,
ModalContent,
ModalFooter,
ModalHeader,
ModalOverlay,
Text,
} from '@chakra-ui/react';

import usePassword from '../store/usePassword';
import useWallet from '../store/useWallet';

import FormInput from './FormInput';

type RenameKeyModalProps = {
keyIndex: number;
isOpen: boolean;
onClose: () => void;
};

type FormValues = {
displayName: string;
};

function RenameKeyModal({
keyIndex,
isOpen,
onClose,
}: RenameKeyModalProps): JSX.Element {
const { renameKey, wallet } = useWallet();
const { withPassword } = usePassword();
const {
register,
reset,
control,
handleSubmit,
setValue,
formState: { errors, isSubmitted },
} = useForm<FormValues>();

const close = () => {
reset();
onClose();
};

const key = wallet?.keychain[keyIndex];
if (!key) {
throw new Error(`Key with index ${keyIndex} not found`);
}

const submit = handleSubmit(async ({ displayName }) => {
const success = await withPassword(
async (password) => {
await renameKey(keyIndex, displayName, password);
return true;
},
'Rename',
// eslint-disable-next-line max-len
`Please enter the password to change the name of key "${key.displayName}" (${key.path}) to "${displayName}":`
);
if (success) {
close();
}
});

useEffect(() => {
setValue('displayName', key.displayName);
}, [key, setValue]);

return (
<Modal isOpen={isOpen} onClose={close} isCentered size="lg">
<Form control={control}>
<ModalOverlay />
<ModalContent>
<ModalCloseButton />
<ModalHeader textAlign="center">
Change the name of Key Pair
</ModalHeader>
<ModalBody>
<Text mb={4}>
Please specify the new name for the key pair with the path:{' '}
{key.path}
</Text>
<FormInput
label="Name"
register={register('displayName', {
required: 'Name is required',
minLength: {
value: 2,
message: 'Give your account a meaningful name',
},
})}
errors={errors}
isSubmitted={isSubmitted}
/>
</ModalBody>
<ModalFooter>
<Button
colorScheme="blue"
onClick={submit}
ml={2}
variant="whiteModal"
px={10}
>
Rename
</Button>
</ModalFooter>
</ModalContent>
</Form>
</Modal>
);
}

export default RenameKeyModal;
Loading

0 comments on commit ebccf35

Please sign in to comment.