feat: add login dialog

This commit is contained in:
reya 2024-03-06 09:42:44 +07:00
parent 86183d799a
commit 8eaf47f6d2
17 changed files with 336 additions and 167 deletions

View File

@ -15,6 +15,7 @@
"@lume/utils": "workspace:^", "@lume/utils": "workspace:^",
"@radix-ui/react-checkbox": "^1.0.4", "@radix-ui/react-checkbox": "^1.0.4",
"@radix-ui/react-collapsible": "^1.0.3", "@radix-ui/react-collapsible": "^1.0.3",
"@radix-ui/react-dialog": "^1.0.5",
"@radix-ui/react-dropdown-menu": "^2.0.6", "@radix-ui/react-dropdown-menu": "^2.0.6",
"@radix-ui/react-popover": "^1.0.7", "@radix-ui/react-popover": "^1.0.7",
"@tanstack/query-sync-storage-persister": "^5.24.1", "@tanstack/query-sync-storage-persister": "^5.24.1",

View File

@ -4,9 +4,9 @@ import { User } from "@lume/ui";
import { useNavigate, useParams, useSearch } from "@tanstack/react-router"; import { useNavigate, useParams, useSearch } from "@tanstack/react-router";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import * as Popover from "@radix-ui/react-popover"; import * as Popover from "@radix-ui/react-popover";
import { Link } from "@tanstack/react-router";
import { useTranslation } from "react-i18next";
import * as DropdownMenu from "@radix-ui/react-dropdown-menu"; import * as DropdownMenu from "@radix-ui/react-dropdown-menu";
import { BackupDialog } from "./backup";
import { LoginDialog } from "./login";
export function Accounts() { export function Accounts() {
const ark = useArk(); const ark = useArk();
@ -63,7 +63,6 @@ function Active({ pubkey }: { pubkey: string }) {
const [open, setOpen] = useState(true); const [open, setOpen] = useState(true);
// @ts-ignore, magic !!! // @ts-ignore, magic !!!
const { guest } = useSearch({ strict: false }); const { guest } = useSearch({ strict: false });
const { t } = useTranslation();
if (guest) { if (guest) {
return ( return (
@ -84,25 +83,17 @@ function Active({ pubkey }: { pubkey: string }) {
side="bottom" side="bottom"
> >
<div> <div>
<h1 className="mb-1 font-semibold">You're using guest account</h1> <h1 className="mb-1 font-semibold">
You're using random account
</h1>
<p className="text-sm text-neutral-500 dark:text-neutral-600"> <p className="text-sm text-neutral-500 dark:text-neutral-600">
You can continue by claim and backup this account, or you can You can continue by claim and backup this account, or you can
import your own account key. import your own account.
</p> </p>
</div> </div>
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<Link <BackupDialog />
to="/backup" <LoginDialog />
className="inline-flex h-9 w-full items-center justify-center rounded-lg bg-white text-sm font-medium leading-tight text-neutral-900 hover:bg-neutral-100"
>
Claim & Backup
</Link>
<Link
to="/login"
className="inline-flex h-9 w-full items-center justify-center rounded-lg bg-neutral-900 text-sm font-medium leading-tight text-neutral-100 hover:bg-neutral-800"
>
{t("welcome.login")}
</Link>
</div> </div>
<Popover.Arrow className="fill-black dark:fill-white" /> <Popover.Arrow className="fill-black dark:fill-white" />
</Popover.Content> </Popover.Content>

View File

@ -0,0 +1,85 @@
import { CancelIcon } from "@lume/icons";
import * as Dialog from "@radix-ui/react-dialog";
import { useState } from "react";
export function BackupDialog() {
const [key, setKey] = useState("");
const [passphase, setPassphase] = useState("");
const encryptKey = async () => {
console.log("****");
};
return (
<Dialog.Root>
<Dialog.Trigger asChild>
<button
type="button"
className="inline-flex h-9 w-full items-center justify-center rounded-lg bg-neutral-900 text-sm font-medium leading-tight text-neutral-100 hover:bg-neutral-800"
>
Claim & Backup
</button>
</Dialog.Trigger>
<Dialog.Portal>
<Dialog.Overlay className="fixed inset-0 z-50 bg-black/30 backdrop-blur dark:bg-white/30" />
<Dialog.Content className="fixed inset-0 z-50 flex min-h-full items-center justify-center">
<Dialog.Close className="absolute right-5 top-5 flex w-12 flex-col items-center justify-center gap-1 text-white">
<CancelIcon className="size-8" />
<span className="text-sm font-medium uppercase text-neutral-400 dark:text-neutral-600">
Esc
</span>
</Dialog.Close>
<div className="relative flex h-min w-full max-w-xl flex-col gap-8 rounded-xl bg-white p-5 dark:bg-black">
<div className="flex flex-col">
<h3 className="text-lg font-semibold">
This is your account key
</h3>
<p>
It's use for login to Lume or other Nostr clients. You will lost
access to your account if you lose this key.
</p>
</div>
<div className="flex flex-col gap-5">
<div className="flex flex-col gap-2">
<label htmlFor="nsec">
Copy this key and keep it in safe place
</label>
<input
name="nsec"
type="text"
className="h-11 w-full resize-none rounded-lg border-transparent bg-neutral-100 placeholder:text-neutral-600 focus:border-blue-500 focus:ring focus:ring-blue-100 dark:bg-neutral-900 dark:focus:ring-blue-900"
/>
</div>
<div className="flex flex-col gap-2">
<label htmlFor="nsec">
<span className="font-semibold">(Recommend)</span> Set a
passphase to secure your key
</label>
<div className="relative">
<input
name="passphase"
type="password"
value={passphase}
onChange={(e) => setPassphase(e.target.value)}
className="h-11 w-full resize-none rounded-lg border-transparent bg-neutral-100 placeholder:text-neutral-600 focus:border-blue-500 focus:ring focus:ring-blue-100 dark:bg-neutral-900 dark:focus:ring-blue-900"
/>
{passphase.length ? (
<div className="absolute right-2 top-0 h-11 py-2">
<button
type="button"
onClick={encryptKey}
className="inline-flex h-full items-center justify-center rounded-md bg-blue-500 px-3 text-sm font-medium text-white hover:bg-blue-600"
>
Update
</button>
</div>
) : null}
</div>
</div>
</div>
</div>
</Dialog.Content>
</Dialog.Portal>
</Dialog.Root>
);
}

View File

@ -0,0 +1,120 @@
import { useArk } from "@lume/ark";
import { ArrowRightIcon, CancelIcon } from "@lume/icons";
import * as Dialog from "@radix-ui/react-dialog";
import { useNavigate } from "@tanstack/react-router";
import { useState } from "react";
import { toast } from "sonner";
export function LoginDialog() {
const ark = useArk();
const navigate = useNavigate();
const [nsec, setNsec] = useState("");
const [passphase, setPassphase] = useState("");
const [loading, setLoading] = useState(false);
const login = async () => {
try {
setLoading(true);
const save = await ark.save_account(nsec, passphase);
if (save) {
navigate({ to: "/", search: { guest: false } });
}
} catch (e) {
setLoading(false);
toast.error(String(e));
}
};
return (
<Dialog.Root>
<Dialog.Trigger asChild>
<button
type="button"
className="inline-flex h-9 w-full items-center justify-center rounded-lg bg-neutral-900 text-sm font-medium leading-tight text-neutral-100 hover:bg-neutral-800"
>
Add account
</button>
</Dialog.Trigger>
<Dialog.Portal>
<Dialog.Overlay className="fixed inset-0 z-50 bg-black/30 backdrop-blur dark:bg-white/30" />
<Dialog.Content className="fixed inset-0 z-50 flex min-h-full items-center justify-center">
<Dialog.Close className="absolute right-5 top-5 flex w-12 flex-col items-center justify-center gap-1 text-white">
<CancelIcon className="size-8" />
<span className="text-sm font-medium uppercase text-neutral-400 dark:text-neutral-600">
Esc
</span>
</Dialog.Close>
<div className="relative flex h-min w-full max-w-xl flex-col gap-8 rounded-xl bg-white p-5 dark:bg-black">
<div className="flex flex-col gap-1.5">
<h3 className="text-lg font-semibold">Add new account with</h3>
<div className="flex h-11 items-center overflow-hidden rounded-lg bg-neutral-100 p-1 dark:bg-neutral-900">
<button
type="button"
className="h-full flex-1 rounded-md bg-white text-sm font-medium dark:bg-black"
>
nsec
</button>
<button
type="button"
className="flex h-full flex-1 flex-col items-center justify-center rounded-md text-sm font-medium"
>
<span className="leading-tight">nsecBunker</span>
<span className="text-xs font-normal leading-tight text-neutral-700 dark:text-neutral-300">
coming soon
</span>
</button>
<button
type="button"
className="flex h-full flex-1 flex-col items-center justify-center rounded-md text-sm font-medium"
>
<span className="leading-tight">Address</span>
<span className="text-xs font-normal leading-tight text-neutral-700 dark:text-neutral-300">
coming soon
</span>
</button>
</div>
</div>
<div className="flex flex-col gap-5">
<div className="flex flex-col gap-2">
<label htmlFor="nsec">
Enter sign in key start with nsec or ncrypto
</label>
<input
name="nsec"
type="text"
value={nsec}
onChange={(e) => setNsec(e.target.value)}
className="h-11 w-full resize-none rounded-lg border-transparent bg-neutral-100 placeholder:text-neutral-600 focus:border-blue-500 focus:ring focus:ring-blue-100 dark:bg-neutral-900 dark:focus:ring-blue-900"
/>
</div>
<div className="flex flex-col gap-2">
<label htmlFor="nsec">Passphase (optional)</label>
<input
name="passphase"
type="password"
value={passphase}
onChange={(e) => setPassphase(e.target.value)}
className="h-11 w-full resize-none rounded-lg border-transparent bg-neutral-100 placeholder:text-neutral-600 focus:border-blue-500 focus:ring focus:ring-blue-100 dark:bg-neutral-900 dark:focus:ring-blue-900"
/>
</div>
</div>
<div className="flex flex-col gap-3">
<button
type="button"
onClick={login}
className="inline-flex h-11 w-full items-center justify-between gap-1.5 rounded-lg bg-blue-500 px-5 font-medium text-white hover:bg-blue-600"
>
<div className="size-5" />
<div>Add account</div>
<ArrowRightIcon className="size-5" />
</button>
</div>
</div>
</Dialog.Content>
</Dialog.Portal>
</Dialog.Root>
);
}

View File

@ -5,6 +5,7 @@ import {
HomeFilledIcon, HomeFilledIcon,
HomeIcon, HomeIcon,
HorizontalDotsIcon, HorizontalDotsIcon,
SettingsIcon,
SpaceFilledIcon, SpaceFilledIcon,
SpaceIcon, SpaceIcon,
} from "@lume/icons"; } from "@lume/icons";
@ -43,6 +44,12 @@ function App() {
<ComposeFilledIcon className="size-4" /> <ComposeFilledIcon className="size-4" />
New post New post
</button> </button>
<button
type="button"
className="inline-flex size-8 items-center justify-center rounded-full bg-neutral-200 text-neutral-800 hover:bg-neutral-400 dark:bg-neutral-800 dark:text-neutral-200 dark:hover:bg-neutral-600"
>
<HorizontalDotsIcon className="size-5" />
</button>
</div> </div>
</div> </div>
<Box> <Box>

View File

@ -1,20 +0,0 @@
import { createLazyFileRoute } from "@tanstack/react-router";
import { useTranslation } from "react-i18next";
export const Route = createLazyFileRoute("/backup")({
component: Screen,
});
function Screen() {
const { t } = useTranslation();
return (
<div className="flex h-full w-full items-center justify-center">
<div className="mx-auto flex w-full max-w-md flex-col gap-8">
<div className="flex flex-col items-center gap-1 text-center">
<h1 className="text-2xl font-semibold">{t("backup.title")}</h1>
</div>
</div>
</div>
);
}

View File

@ -6,7 +6,7 @@ import { createFileRoute, redirect, useNavigate } from "@tanstack/react-router";
import { useState } from "react"; import { useState } from "react";
export const Route = createFileRoute("/")({ export const Route = createFileRoute("/")({
beforeLoad: async ({ location, context }) => { beforeLoad: async ({ context }) => {
const ark = context.ark; const ark = context.ark;
const accounts = await ark.get_all_accounts(); const accounts = await ark.get_all_accounts();

View File

@ -1,14 +0,0 @@
import { Outlet, createFileRoute } from "@tanstack/react-router";
export const Route = createFileRoute("/login")({
component: Screen,
});
function Screen() {
return (
<div>
<h1>Login</h1>
<Outlet />
</div>
);
}

View File

@ -31,7 +31,6 @@ export class Ark {
this.accounts = accounts; this.accounts = accounts;
return accounts; return accounts;
} catch (e) { } catch (e) {
console.error(e);
return []; return [];
} }
} }
@ -45,19 +44,18 @@ export class Ark {
return cmd; return cmd;
} catch (e) { } catch (e) {
console.error(e); throw new Error(String(e));
return false;
} }
} }
public async create_guest_account() { public async create_guest_account() {
try { try {
const keys = await this.create_keys(); const keys = await this.create_keys();
await this.save_account(keys); await this.save_account(keys.nsec, "");
return keys.npub; return keys.npub;
} catch (e) { } catch (e) {
console.error(e); throw new Error(String(e));
} }
} }
@ -70,17 +68,20 @@ export class Ark {
} }
} }
public async save_account(keys: Keys) { public async save_account(nsec: string, password: string = "") {
try { try {
const cmd: boolean = await invoke("save_key", { nsec: keys.nsec }); const cmd: boolean = await invoke("save_key", {
nsec,
password,
});
if (cmd) { if (cmd) {
await invoke("update_signer", { nsec: keys.nsec }); await invoke("update_signer", { nsec });
} }
return cmd; return cmd;
} catch (e) { } catch (e) {
console.error(String(e)); throw new Error(String(e));
} }
} }
@ -92,7 +93,7 @@ export class Ark {
}); });
return cmd; return cmd;
} catch (e) { } catch (e) {
console.error(String(e)); throw new Error(String(e));
} }
} }
@ -106,7 +107,7 @@ export class Ark {
const event: Event = JSON.parse(cmd); const event: Event = JSON.parse(cmd);
return event; return event;
} catch (e) { } catch (e) {
return null; throw new Error(String(e));
} }
} }
@ -210,8 +211,7 @@ export class Ark {
return cmd; return cmd;
} catch (e) { } catch (e) {
console.error(String(e)); throw new Error(String(e));
return false;
} }
} }
@ -220,7 +220,7 @@ export class Ark {
const cmd: string = await invoke("reply_to", { content, tags }); const cmd: string = await invoke("reply_to", { content, tags });
return cmd; return cmd;
} catch (e) { } catch (e) {
console.error(String(e)); throw new Error(String(e));
} }
} }
@ -229,7 +229,7 @@ export class Ark {
const cmd: string = await invoke("repost", { id, pubkey: author }); const cmd: string = await invoke("repost", { id, pubkey: author });
return cmd; return cmd;
} catch (e) { } catch (e) {
console.error(String(e)); throw new Error(String(e));
} }
} }
@ -238,7 +238,7 @@ export class Ark {
const cmd: string = await invoke("upvote", { id, pubkey: author }); const cmd: string = await invoke("upvote", { id, pubkey: author });
return cmd; return cmd;
} catch (e) { } catch (e) {
console.error(String(e)); throw new Error(String(e));
} }
} }
@ -247,7 +247,7 @@ export class Ark {
const cmd: string = await invoke("downvote", { id, pubkey: author }); const cmd: string = await invoke("downvote", { id, pubkey: author });
return cmd; return cmd;
} catch (e) { } catch (e) {
console.error(String(e)); throw new Error(String(e));
} }
} }
@ -366,8 +366,7 @@ export class Ark {
const cmd: string = await invoke("follow", { id, alias }); const cmd: string = await invoke("follow", { id, alias });
return cmd; return cmd;
} catch (e) { } catch (e) {
console.error(e); throw new Error(String(e));
return false;
} }
} }
@ -376,8 +375,7 @@ export class Ark {
const cmd: string = await invoke("unfollow", { id }); const cmd: string = await invoke("unfollow", { id });
return cmd; return cmd;
} catch (e) { } catch (e) {
console.error(e); throw new Error(String(e));
return false;
} }
} }
@ -389,7 +387,7 @@ export class Ark {
}); });
return cmd; return cmd;
} catch (e) { } catch (e) {
console.error(String(e)); throw new Error(String(e));
} }
} }

View File

@ -1,18 +1,13 @@
export function ArrowRightIcon(props: JSX.IntrinsicElements['svg']) { export function ArrowRightIcon(props: JSX.IntrinsicElements["svg"]) {
return ( return (
<svg <svg width="24" height="24" fill="none" viewBox="0 0 24 24" {...props}>
{...props} <path
xmlns="http://www.w3.org/2000/svg" stroke="currentColor"
viewBox="0 0 24 24" strokeLinecap="round"
width="24" strokeLinejoin="round"
height="24" strokeWidth="2"
fill="none" d="m14 6 6 6-6 6m5-6H4"
stroke="currentColor" />
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
>
<path d="M15.17 6a30.23 30.23 0 0 1 5.62 5.406c.14.174.21.384.21.594m-5.83 6a30.232 30.232 0 0 0 5.62-5.406A.949.949 0 0 0 21 12m0 0H3" />
</svg> </svg>
); );
} }

View File

@ -1,18 +1,12 @@
export function CancelIcon(props: JSX.IntrinsicElements['svg']) { export function CancelIcon(props: JSX.IntrinsicElements["svg"]) {
return ( return (
<svg <svg width="24" height="24" fill="none" viewBox="0 0 24 24" {...props}>
{...props} <path
xmlns="http://www.w3.org/2000/svg" stroke="currentColor"
viewBox="0 0 24 24" stroke-linecap="round"
width="24" stroke-width="2"
height="24" d="m5 5 14 14m0-14L5 19"
fill="none" />
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
>
<path d="m6 18 6-6m0 0 6-6m-6 6L6 6m6 6 6 6" />
</svg> </svg>
); );
} }

View File

@ -7,14 +7,16 @@ export function SettingsIcon(
<svg width="24" height="24" fill="none" viewBox="0 0 24 24" {...props}> <svg width="24" height="24" fill="none" viewBox="0 0 24 24" {...props}>
<path <path
stroke="currentColor" stroke="currentColor"
stroke-linejoin="round" strokeLinecap="square"
stroke-width="2" strokeLinejoin="round"
d="m7.99 5.398-.685-.158A1.722 1.722 0 0 0 5.24 7.305l.158.684a1.946 1.946 0 0 1-.817 2.057l-.832.555a1.682 1.682 0 0 0 0 2.798l.832.555c.673.449.999 1.268.817 2.057l-.158.684a1.722 1.722 0 0 0 2.065 2.065l.684-.158a1.946 1.946 0 0 1 2.057.817l.555.832a1.682 1.682 0 0 0 2.798 0l.555-.832a1.946 1.946 0 0 1 2.057-.817l.684.158a1.722 1.722 0 0 0 2.065-2.065l-.158-.684a1.946 1.946 0 0 1 .817-2.057l.832-.555a1.682 1.682 0 0 0 0-2.798l-.832-.555a1.946 1.946 0 0 1-.817-2.057l.158-.684a1.722 1.722 0 0 0-2.065-2.065l-.684.158a1.946 1.946 0 0 1-2.057-.817l-.555-.832a1.682 1.682 0 0 0-2.798 0l-.555.832a1.946 1.946 0 0 1-2.057.817Z" strokeWidth="2"
d="M11.02 3.552a2 2 0 0 1 1.96 0l6 3.374A2 2 0 0 1 20 8.67v6.66a2 2 0 0 1-1.02 1.743l-6 3.375a2 2 0 0 1-1.96 0l-6-3.374A2 2 0 0 1 4 15.33V8.67a2 2 0 0 1 1.02-1.744l6-3.374Z"
/> />
<path <path
stroke="currentColor" stroke="currentColor"
stroke-linejoin="round" strokeLinecap="square"
stroke-width="2" strokeLinejoin="round"
strokeWidth="2"
d="M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z" d="M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z"
/> />
</svg> </svg>

View File

@ -78,6 +78,9 @@ importers:
'@radix-ui/react-collapsible': '@radix-ui/react-collapsible':
specifier: ^1.0.3 specifier: ^1.0.3
version: 1.0.3(@types/react-dom@18.2.19)(@types/react@18.2.61)(react-dom@18.2.0)(react@18.2.0) version: 1.0.3(@types/react-dom@18.2.19)(@types/react@18.2.61)(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-dialog':
specifier: ^1.0.5
version: 1.0.5(@types/react-dom@18.2.19)(@types/react@18.2.61)(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-dropdown-menu': '@radix-ui/react-dropdown-menu':
specifier: ^2.0.6 specifier: ^2.0.6
version: 2.0.6(@types/react-dom@18.2.19)(@types/react@18.2.61)(react-dom@18.2.0)(react@18.2.0) version: 2.0.6(@types/react-dom@18.2.19)(@types/react@18.2.61)(react-dom@18.2.0)(react@18.2.0)
@ -226,7 +229,7 @@ importers:
version: 1.0.3(@types/react-dom@18.2.19)(@types/react@18.2.61)(react-dom@18.2.0)(react@18.2.0) version: 1.0.3(@types/react-dom@18.2.19)(@types/react@18.2.61)(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-dialog': '@radix-ui/react-dialog':
specifier: ^1.0.5 specifier: ^1.0.5
version: 1.0.5(@types/react@18.2.61)(react-dom@18.2.0)(react@18.2.0) version: 1.0.5(@types/react-dom@18.2.19)(@types/react@18.2.61)(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-dropdown-menu': '@radix-ui/react-dropdown-menu':
specifier: ^2.0.6 specifier: ^2.0.6
version: 2.0.6(@types/react-dom@18.2.19)(@types/react@18.2.61)(react-dom@18.2.0)(react@18.2.0) version: 2.0.6(@types/react-dom@18.2.19)(@types/react@18.2.61)(react-dom@18.2.0)(react@18.2.0)
@ -874,7 +877,7 @@ importers:
version: 1.0.3(@types/react-dom@18.2.19)(@types/react@18.2.61)(react-dom@18.2.0)(react@18.2.0) version: 1.0.3(@types/react-dom@18.2.19)(@types/react@18.2.61)(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-dialog': '@radix-ui/react-dialog':
specifier: ^1.0.5 specifier: ^1.0.5
version: 1.0.5(@types/react@18.2.61)(react-dom@18.2.0)(react@18.2.0) version: 1.0.5(@types/react-dom@18.2.19)(@types/react@18.2.61)(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-dropdown-menu': '@radix-ui/react-dropdown-menu':
specifier: ^2.0.6 specifier: ^2.0.6
version: 2.0.6(@types/react-dom@18.2.19)(@types/react@18.2.61)(react-dom@18.2.0)(react@18.2.0) version: 2.0.6(@types/react-dom@18.2.19)(@types/react@18.2.61)(react-dom@18.2.0)(react@18.2.0)
@ -1912,7 +1915,7 @@ packages:
'@radix-ui/primitive': 1.0.1 '@radix-ui/primitive': 1.0.1
'@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.61)(react@18.2.0) '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.61)(react@18.2.0)
'@radix-ui/react-context': 1.0.1(@types/react@18.2.61)(react@18.2.0) '@radix-ui/react-context': 1.0.1(@types/react@18.2.61)(react@18.2.0)
'@radix-ui/react-dialog': 1.0.5(@types/react@18.2.61)(react-dom@18.2.0)(react@18.2.0) '@radix-ui/react-dialog': 1.0.5(@types/react-dom@18.2.19)(@types/react@18.2.61)(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.19)(@types/react@18.2.61)(react-dom@18.2.0)(react@18.2.0) '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.19)(@types/react@18.2.61)(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-slot': 1.0.2(@types/react@18.2.61)(react@18.2.0) '@radix-ui/react-slot': 1.0.2(@types/react@18.2.61)(react@18.2.0)
'@types/react': 18.2.61 '@types/react': 18.2.61
@ -2072,7 +2075,7 @@ packages:
react: 18.2.0 react: 18.2.0
dev: false dev: false
/@radix-ui/react-dialog@1.0.5(@types/react@18.2.61)(react-dom@18.2.0)(react@18.2.0): /@radix-ui/react-dialog@1.0.5(@types/react-dom@18.2.19)(@types/react@18.2.61)(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-GjWJX/AUpB703eEBanuBnIWdIXg6NvJFCXcNlSZk4xdszCdhrJgBoUd1cGk67vFO+WdA2pfI/plOpqz/5GUP6Q==} resolution: {integrity: sha512-GjWJX/AUpB703eEBanuBnIWdIXg6NvJFCXcNlSZk4xdszCdhrJgBoUd1cGk67vFO+WdA2pfI/plOpqz/5GUP6Q==}
peerDependencies: peerDependencies:
'@types/react': '*' '@types/react': '*'
@ -2099,6 +2102,7 @@ packages:
'@radix-ui/react-slot': 1.0.2(@types/react@18.2.61)(react@18.2.0) '@radix-ui/react-slot': 1.0.2(@types/react@18.2.61)(react@18.2.0)
'@radix-ui/react-use-controllable-state': 1.0.1(@types/react@18.2.61)(react@18.2.0) '@radix-ui/react-use-controllable-state': 1.0.1(@types/react@18.2.61)(react@18.2.0)
'@types/react': 18.2.61 '@types/react': 18.2.61
'@types/react-dom': 18.2.19
aria-hidden: 1.2.3 aria-hidden: 1.2.3
react: 18.2.0 react: 18.2.0
react-dom: 18.2.0(react@18.2.0) react-dom: 18.2.0(react@18.2.0)

View File

@ -89,7 +89,6 @@ fn main() {
.invoke_handler(tauri::generate_handler![ .invoke_handler(tauri::generate_handler![
nostr::keys::create_keys, nostr::keys::create_keys,
nostr::keys::save_key, nostr::keys::save_key,
nostr::keys::get_public_key,
nostr::keys::update_signer, nostr::keys::update_signer,
nostr::keys::verify_signer, nostr::keys::verify_signer,
nostr::keys::load_selected_account, nostr::keys::load_selected_account,

View File

@ -30,54 +30,58 @@ pub fn create_keys() -> Result<CreateKeysResponse, ()> {
#[tauri::command] #[tauri::command]
pub async fn save_key( pub async fn save_key(
nsec: &str, nsec: &str,
password: &str,
app_handle: tauri::AppHandle, app_handle: tauri::AppHandle,
state: State<'_, Nostr>, state: State<'_, Nostr>,
) -> Result<bool, ()> { ) -> Result<bool, String> {
if let Ok(nostr_secret_key) = SecretKey::from_bech32(nsec) { let secret_key: Result<SecretKey, String>;
let nostr_keys = Keys::new(nostr_secret_key);
let nostr_npub = nostr_keys.public_key().to_bech32().unwrap();
let signer = NostrSigner::Keys(nostr_keys);
// Update client's signer if nsec.starts_with("ncrypto") {
let client = &state.client; let encrypted_key = EncryptedSecretKey::from_bech32(nsec).unwrap();
client.set_signer(Some(signer)).await; secret_key = match encrypted_key.to_secret_key(password) {
Ok(val) => Ok(val),
let keyring_entry = Entry::new("Lume Secret Storage", "AppKey").unwrap(); Err(_) => Err("Wrong passphase".into()),
let secret_key = keyring_entry.get_password().unwrap(); };
let app_key = age::x25519::Identity::from_str(&secret_key).unwrap();
let app_pubkey = app_key.to_public();
let config_dir = app_handle.path().app_config_dir().unwrap();
let encryptor =
age::Encryptor::with_recipients(vec![Box::new(app_pubkey)]).expect("we provided a recipient");
let file_ext = ".nsec".to_owned();
let file_path = nostr_npub + &file_ext;
let mut file = File::create(config_dir.join(file_path)).unwrap();
let mut writer = encryptor
.wrap_output(&mut file)
.expect("Init writer failed");
writer
.write_all(nsec.as_bytes())
.expect("Write nsec failed");
writer.finish().expect("Save nsec failed");
Ok(true)
} else { } else {
Ok(false) secret_key = match SecretKey::from_bech32(nsec) {
Ok(val) => Ok(val),
Err(_) => Err("nsec is not valid".into()),
}
} }
}
#[tauri::command] match secret_key {
pub fn get_public_key(nsec: &str) -> Result<String, ()> { Ok(val) => {
let secret_key = SecretKey::from_bech32(nsec).unwrap(); let nostr_keys = Keys::new(val);
let keys = Keys::new(secret_key); let nostr_npub = nostr_keys.public_key().to_bech32().unwrap();
Ok( let signer = NostrSigner::Keys(nostr_keys);
keys
.public_key() // Update client's signer
.to_bech32() let client = &state.client;
.expect("get public key failed"), client.set_signer(Some(signer)).await;
)
let keyring_entry = Entry::new("Lume Secret Storage", "AppKey").unwrap();
let master_key = keyring_entry.get_password().unwrap();
let app_key = age::x25519::Identity::from_str(&master_key).unwrap();
let app_pubkey = app_key.to_public();
let config_dir = app_handle.path().app_config_dir().unwrap();
let encryptor = age::Encryptor::with_recipients(vec![Box::new(app_pubkey)])
.expect("we provided a recipient");
let file_path = nostr_npub + ".nsec";
let mut file = File::create(config_dir.join(file_path)).unwrap();
let mut writer = encryptor
.wrap_output(&mut file)
.expect("Init writer failed");
writer
.write_all(nsec.as_bytes())
.expect("Write nsec failed");
writer.finish().expect("Save nsec failed");
Ok(true)
}
Err(msg) => Err(msg.into()),
}
} }
#[tauri::command] #[tauri::command]

View File

@ -1,22 +1,12 @@
use tauri::{tray::ClickType, Manager, Runtime}; use tauri::{tray::ClickType, Manager, Runtime};
pub fn create_tray<R: Runtime>(app: &tauri::AppHandle<R>) -> tauri::Result<()> { pub fn create_tray<R: Runtime>(app: &tauri::AppHandle<R>) -> tauri::Result<()> {
let tray = app.tray().unwrap();
let menu = tauri::menu::MenuBuilder::new(app) let menu = tauri::menu::MenuBuilder::new(app)
.item(&tauri::menu::MenuItem::with_id(app, "quit", "Quit", true, None::<&str>).unwrap()) .item(&tauri::menu::MenuItem::with_id(app, "quit", "Quit", true, None::<&str>).unwrap())
.build() .build()
.unwrap(); .unwrap();
let _ = tray.set_menu(Some(menu));
let tray = tauri::tray::TrayIconBuilder::with_id("main_tray")
.tooltip("Lume")
.icon(tauri::Icon::Rgba {
rgba: include_bytes!("../icons/icon.png").to_vec(),
width: 500,
height: 500,
})
.icon_as_template(true)
.menu(&menu)
.build(app)
.unwrap();
tray.on_menu_event(move |app, event| match event.id.0.as_str() { tray.on_menu_event(move |app, event| match event.id.0.as_str() {
"quit" => { "quit" => {

View File

@ -12,6 +12,11 @@
"app": { "app": {
"macOSPrivateApi": true, "macOSPrivateApi": true,
"withGlobalTauri": true, "withGlobalTauri": true,
"trayIcon": {
"id": "main_tray",
"iconPath": "./icons/tray.png",
"iconAsTemplate": true
},
"security": { "security": {
"assetProtocol": { "assetProtocol": {
"enable": true, "enable": true,
@ -92,7 +97,15 @@
"type": "downloadBootstrapper" "type": "downloadBootstrapper"
}, },
"wix": null "wix": null
} },
"fileAssociations": [
{
"name": "bech32",
"description": "Nostr Bech32",
"ext": ["nsec", "nprofile", "nevent", "naddr", "nrelay"],
"role": "Viewer"
}
]
}, },
"plugins": { "plugins": {
"updater": { "updater": {