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() {