diff --git a/.changeset/angry-flowers-turn.md b/.changeset/angry-flowers-turn.md new file mode 100644 index 000000000..d49d7cb64 --- /dev/null +++ b/.changeset/angry-flowers-turn.md @@ -0,0 +1,5 @@ +--- +"nostrudel": minor +--- + +Add "Migrate to signing device" option in account manager diff --git a/package.json b/package.json index c16a078b6..b2eeec6ee 100644 --- a/package.json +++ b/package.json @@ -49,7 +49,7 @@ "applesauce-lists": "^0.10.0", "applesauce-net": "^0.10.0", "applesauce-react": "^0.10.0", - "applesauce-signer": "^0.10.0", + "applesauce-signer": "0.0.0-next-20241218172722", "bech32": "^2.0.0", "blossom-client-sdk": "^2.1.0", "blossom-drive-sdk": "^0.4.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index be7cb55e7..d71c3ad83 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -112,8 +112,8 @@ importers: specifier: ^0.10.0 version: 0.10.0(typescript@5.7.2) applesauce-signer: - specifier: ^0.10.0 - version: 0.10.0(typescript@5.7.2) + specifier: 0.0.0-next-20241218172722 + version: 0.0.0-next-20241218172722(typescript@5.7.2) bech32: specifier: ^2.0.0 version: 2.0.0 @@ -1927,8 +1927,8 @@ packages: applesauce-react@0.10.0: resolution: {integrity: sha512-du4EC4cBM9bWbyRVllciPCNwwwKTEsTqSjsB5/h/tktfG/2ubiZfUsDD9o55o0eWx/drDabIN4sjnv3OEwvXww==} - applesauce-signer@0.10.0: - resolution: {integrity: sha512-2Cn2ZUxk47cBJBFoUl9DB37mjgg8/8GwTAG7csPqtooJ4nUh0ylO4Gh1Mr/401Lc8tjZJvpVBySWpCeXrnG1rQ==} + applesauce-signer@0.0.0-next-20241218172722: + resolution: {integrity: sha512-0DvkuVLCiFr7CjLeUr+AHxtiLBwOf7TiDBmpbVTdDgmNYo/hlvYOvgVDfISkpB8DRE1/44BlWBXFxbKmfVWkxg==} argparse@1.0.10: resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} @@ -6469,7 +6469,7 @@ snapshots: - supports-color - typescript - applesauce-signer@0.10.0(typescript@5.7.2): + applesauce-signer@0.0.0-next-20241218172722(typescript@5.7.2): dependencies: '@noble/hashes': 1.6.1 '@noble/secp256k1': 1.7.1 diff --git a/src/views/settings/accounts/components/migrate-to-device.tsx b/src/views/settings/accounts/components/migrate-to-device.tsx new file mode 100644 index 000000000..69f4c902d --- /dev/null +++ b/src/views/settings/accounts/components/migrate-to-device.tsx @@ -0,0 +1,62 @@ +import { Button, Flex, Heading, Link, useToast } from "@chakra-ui/react"; +import { PasswordSigner, SerialPortSigner, SimpleSigner } from "applesauce-signer"; +import { useState } from "react"; + +import useAsyncErrorHandler from "../../../../hooks/use-async-error-handler"; +import useCurrentAccount from "../../../../hooks/use-current-account"; +import SerialPortAccount from "../../../../classes/accounts/serial-port-account"; +import accountService from "../../../../services/account"; + +export default function MigrateAccountToDevice() { + if (!SerialPortSigner.SUPPORTED) return null; + + const toast = useToast(); + const current = useCurrentAccount(); + const [loading, setLoading] = useState(false); + + const migrate = useAsyncErrorHandler(async () => { + try { + setLoading(true); + if (!current?.signer) throw new Error("Account missing signer"); + const device = new SerialPortSigner(); + + if (current.signer instanceof SimpleSigner) { + // send key to device + await device.restore(current.signer.key); + } else if (current.signer instanceof PasswordSigner) { + // unlock the signer first + if (!current.signer.unlocked) { + const password = window.prompt("Decryption password"); + if (password === null) throw new Error("Password required"); + await current.signer.unlock(password); + } + + await device.restore(current.signer.key!); + } else throw new Error("Unsupported signer type"); + + // replace existing account + const deviceAccount = new SerialPortAccount(current.pubkey); + accountService.replaceAccount(current.pubkey, deviceAccount); + accountService.switchAccount(deviceAccount.pubkey); + } catch (error) { + if (error instanceof Error) toast({ description: error.message, status: "error" }); + } + setLoading(false); + }, [setLoading, current]); + + return ( + + + Migrate to{" "} + + nostr-signing-device + + + + + + + ); +} diff --git a/src/views/settings/accounts/password-signer-backup.tsx b/src/views/settings/accounts/components/password-signer-backup.tsx similarity index 92% rename from src/views/settings/accounts/password-signer-backup.tsx rename to src/views/settings/accounts/components/password-signer-backup.tsx index 058b679ed..2207fcd80 100644 --- a/src/views/settings/accounts/password-signer-backup.tsx +++ b/src/views/settings/accounts/components/password-signer-backup.tsx @@ -13,10 +13,10 @@ import { import { useForm } from "react-hook-form"; import { PasswordSigner } from "applesauce-signer"; -import useCurrentAccount from "../../../hooks/use-current-account"; -import EyeOff from "../../../components/icons/eye-off"; -import Eye from "../../../components/icons/eye"; -import { CopyIconButton } from "../../../components/copy-icon-button"; +import useCurrentAccount from "../../../../hooks/use-current-account"; +import EyeOff from "../../../../components/icons/eye-off"; +import Eye from "../../../../components/icons/eye"; +import { CopyIconButton } from "../../../../components/copy-icon-button"; const fake = Array(48).fill("x"); diff --git a/src/views/settings/accounts/simple-signer-backup.tsx b/src/views/settings/accounts/components/simple-signer-backup.tsx similarity index 88% rename from src/views/settings/accounts/simple-signer-backup.tsx rename to src/views/settings/accounts/components/simple-signer-backup.tsx index ab7a3acda..5fd4873b1 100644 --- a/src/views/settings/accounts/simple-signer-backup.tsx +++ b/src/views/settings/accounts/components/simple-signer-backup.tsx @@ -15,12 +15,12 @@ import { encrypt } from "nostr-tools/nip49"; import { useForm } from "react-hook-form"; import { SimpleSigner } from "applesauce-signer"; -import useCurrentAccount from "../../../hooks/use-current-account"; -import EyeOff from "../../../components/icons/eye-off"; -import Eye from "../../../components/icons/eye"; -import { CopyIconButton } from "../../../components/copy-icon-button"; -import accountService from "../../../services/account"; -import PasswordAccount from "../../../classes/accounts/password-account"; +import useCurrentAccount from "../../../../hooks/use-current-account"; +import EyeOff from "../../../../components/icons/eye-off"; +import Eye from "../../../../components/icons/eye"; +import { CopyIconButton } from "../../../../components/copy-icon-button"; +import accountService from "../../../../services/account"; +import PasswordAccount from "../../../../classes/accounts/password-account"; const fake = Array(48).fill("x"); diff --git a/src/views/settings/accounts/index.tsx b/src/views/settings/accounts/index.tsx index 25d22f8ae..12651238b 100644 --- a/src/views/settings/accounts/index.tsx +++ b/src/views/settings/accounts/index.tsx @@ -1,6 +1,6 @@ import { Box, Button, ButtonGroup, Divider, Flex, Heading, Text } from "@chakra-ui/react"; import { useNavigate } from "react-router-dom"; -import { PasswordSigner, SimpleSigner } from "applesauce-signer"; +import { PasswordSigner, SerialPortSigner, SimpleSigner } from "applesauce-signer"; import { useObservable } from "applesauce-react/hooks"; import VerticalPageLayout from "../../../components/vertical-page-layout"; @@ -10,21 +10,22 @@ import UserName from "../../../components/user/user-name"; import UserDnsIdentity from "../../../components/user/user-dns-identity"; import accountService from "../../../services/account"; import AccountTypeBadge from "../../../components/account-info-badge"; -import SimpleSignerBackup from "./simple-signer-backup"; -import PasswordSignerBackup from "./password-signer-backup"; +import SimpleSignerBackup from "./components/simple-signer-backup"; +import PasswordSignerBackup from "./components/password-signer-backup"; +import { ReactNode } from "react"; +import MigrateAccountToDevice from "./components/migrate-to-device"; function AccountBackup() { const account = useCurrentAccount()!; - if (account.signer instanceof PasswordSigner && account.signer.ncryptsec) { - return ; - } - - if (account.signer instanceof SimpleSigner && account.signer.key) { - return ; - } - - return null; + return ( + <> + {account.signer instanceof SimpleSigner && account.signer.key && } + {account.signer instanceof PasswordSigner && account.signer.ncryptsec && } + {(account.signer instanceof SimpleSigner || account.signer instanceof PasswordSigner) && + SerialPortSigner.SUPPORTED && } + + ); } export default function AccountSettings() {