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:^",
"@radix-ui/react-checkbox": "^1.0.4",
"@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-popover": "^1.0.7",
"@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 { useEffect, useState } from "react";
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 { BackupDialog } from "./backup";
import { LoginDialog } from "./login";
export function Accounts() {
const ark = useArk();
@ -63,7 +63,6 @@ function Active({ pubkey }: { pubkey: string }) {
const [open, setOpen] = useState(true);
// @ts-ignore, magic !!!
const { guest } = useSearch({ strict: false });
const { t } = useTranslation();
if (guest) {
return (
@ -84,25 +83,17 @@ function Active({ pubkey }: { pubkey: string }) {
side="bottom"
>
<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">
You can continue by claim and backup this account, or you can
import your own account key.
import your own account.
</p>
</div>
<div className="flex flex-col gap-2">
<Link
to="/backup"
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>
<BackupDialog />
<LoginDialog />
</div>
<Popover.Arrow className="fill-black dark:fill-white" />
</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,
HomeIcon,
HorizontalDotsIcon,
SettingsIcon,
SpaceFilledIcon,
SpaceIcon,
} from "@lume/icons";
@ -43,6 +44,12 @@ function App() {
<ComposeFilledIcon className="size-4" />
New post
</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>
<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";
export const Route = createFileRoute("/")({
beforeLoad: async ({ location, context }) => {
beforeLoad: async ({ context }) => {
const ark = context.ark;
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;
return accounts;
} catch (e) {
console.error(e);
return [];
}
}
@ -45,19 +44,18 @@ export class Ark {
return cmd;
} catch (e) {
console.error(e);
return false;
throw new Error(String(e));
}
}
public async create_guest_account() {
try {
const keys = await this.create_keys();
await this.save_account(keys);
await this.save_account(keys.nsec, "");
return keys.npub;
} 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 {
const cmd: boolean = await invoke("save_key", { nsec: keys.nsec });
const cmd: boolean = await invoke("save_key", {
nsec,
password,
});
if (cmd) {
await invoke("update_signer", { nsec: keys.nsec });
await invoke("update_signer", { nsec });
}
return cmd;
} catch (e) {
console.error(String(e));
throw new Error(String(e));
}
}
@ -92,7 +93,7 @@ export class Ark {
});
return cmd;
} catch (e) {
console.error(String(e));
throw new Error(String(e));
}
}
@ -106,7 +107,7 @@ export class Ark {
const event: Event = JSON.parse(cmd);
return event;
} catch (e) {
return null;
throw new Error(String(e));
}
}
@ -210,8 +211,7 @@ export class Ark {
return cmd;
} catch (e) {
console.error(String(e));
return false;
throw new Error(String(e));
}
}
@ -220,7 +220,7 @@ export class Ark {
const cmd: string = await invoke("reply_to", { content, tags });
return cmd;
} 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 });
return cmd;
} 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 });
return cmd;
} 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 });
return cmd;
} 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 });
return cmd;
} catch (e) {
console.error(e);
return false;
throw new Error(String(e));
}
}
@ -376,8 +375,7 @@ export class Ark {
const cmd: string = await invoke("unfollow", { id });
return cmd;
} catch (e) {
console.error(e);
return false;
throw new Error(String(e));
}
}
@ -389,7 +387,7 @@ export class Ark {
});
return cmd;
} 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 (
<svg
{...props}
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
width="24"
height="24"
fill="none"
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 width="24" height="24" fill="none" viewBox="0 0 24 24" {...props}>
<path
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="m14 6 6 6-6 6m5-6H4"
/>
</svg>
);
}

View File

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

View File

@ -7,14 +7,16 @@ export function SettingsIcon(
<svg width="24" height="24" fill="none" viewBox="0 0 24 24" {...props}>
<path
stroke="currentColor"
stroke-linejoin="round"
stroke-width="2"
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"
strokeLinecap="square"
strokeLinejoin="round"
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
stroke="currentColor"
stroke-linejoin="round"
stroke-width="2"
strokeLinecap="square"
strokeLinejoin="round"
strokeWidth="2"
d="M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z"
/>
</svg>

View File

@ -78,6 +78,9 @@ importers:
'@radix-ui/react-collapsible':
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)
'@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':
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)
@ -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)
'@radix-ui/react-dialog':
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':
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)
@ -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)
'@radix-ui/react-dialog':
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':
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)
@ -1912,7 +1915,7 @@ packages:
'@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-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-slot': 1.0.2(@types/react@18.2.61)(react@18.2.0)
'@types/react': 18.2.61
@ -2072,7 +2075,7 @@ packages:
react: 18.2.0
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==}
peerDependencies:
'@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-use-controllable-state': 1.0.1(@types/react@18.2.61)(react@18.2.0)
'@types/react': 18.2.61
'@types/react-dom': 18.2.19
aria-hidden: 1.2.3
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![
nostr::keys::create_keys,
nostr::keys::save_key,
nostr::keys::get_public_key,
nostr::keys::update_signer,
nostr::keys::verify_signer,
nostr::keys::load_selected_account,

View File

@ -30,54 +30,58 @@ pub fn create_keys() -> Result<CreateKeysResponse, ()> {
#[tauri::command]
pub async fn save_key(
nsec: &str,
password: &str,
app_handle: tauri::AppHandle,
state: State<'_, Nostr>,
) -> Result<bool, ()> {
if let Ok(nostr_secret_key) = SecretKey::from_bech32(nsec) {
let nostr_keys = Keys::new(nostr_secret_key);
let nostr_npub = nostr_keys.public_key().to_bech32().unwrap();
let signer = NostrSigner::Keys(nostr_keys);
) -> Result<bool, String> {
let secret_key: Result<SecretKey, String>;
// Update client's signer
let client = &state.client;
client.set_signer(Some(signer)).await;
let keyring_entry = Entry::new("Lume Secret Storage", "AppKey").unwrap();
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)
if nsec.starts_with("ncrypto") {
let encrypted_key = EncryptedSecretKey::from_bech32(nsec).unwrap();
secret_key = match encrypted_key.to_secret_key(password) {
Ok(val) => Ok(val),
Err(_) => Err("Wrong passphase".into()),
};
} else {
Ok(false)
secret_key = match SecretKey::from_bech32(nsec) {
Ok(val) => Ok(val),
Err(_) => Err("nsec is not valid".into()),
}
}
}
#[tauri::command]
pub fn get_public_key(nsec: &str) -> Result<String, ()> {
let secret_key = SecretKey::from_bech32(nsec).unwrap();
let keys = Keys::new(secret_key);
Ok(
keys
.public_key()
.to_bech32()
.expect("get public key failed"),
)
match secret_key {
Ok(val) => {
let nostr_keys = Keys::new(val);
let nostr_npub = nostr_keys.public_key().to_bech32().unwrap();
let signer = NostrSigner::Keys(nostr_keys);
// Update client's signer
let client = &state.client;
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]

View File

@ -1,22 +1,12 @@
use tauri::{tray::ClickType, Manager, Runtime};
pub fn create_tray<R: Runtime>(app: &tauri::AppHandle<R>) -> tauri::Result<()> {
let tray = app.tray().unwrap();
let menu = tauri::menu::MenuBuilder::new(app)
.item(&tauri::menu::MenuItem::with_id(app, "quit", "Quit", true, None::<&str>).unwrap())
.build()
.unwrap();
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();
let _ = tray.set_menu(Some(menu));
tray.on_menu_event(move |app, event| match event.id.0.as_str() {
"quit" => {

View File

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