wip: onboarding

This commit is contained in:
reya 2024-03-25 14:22:06 +07:00
parent a49b88ab35
commit 6e489f1c49
22 changed files with 299 additions and 422 deletions

View File

@ -24,11 +24,13 @@
"@tanstack/react-router": "^1.20.0",
"i18next": "^23.10.1",
"i18next-resources-to-backend": "^1.2.0",
"minidenticons": "^4.2.1",
"nanoid": "^5.0.6",
"nostr-tools": "^2.3.1",
"react": "^18.2.0",
"react-currency-input-field": "^3.8.0",
"react-dom": "^18.2.0",
"react-hook-form": "^7.51.0",
"react-hotkeys-hook": "^4.5.0",
"react-i18next": "^14.1.0",
"slate": "^0.102.0",

View File

@ -7,11 +7,11 @@
}
*::-webkit-scrollbar-track {
@apply bg-white dark:bg-black;
@apply bg-transparent;
}
*::-webkit-scrollbar-thumb {
@apply bg-black dark:bg-white rounded;
@apply rounded bg-black dark:bg-white;
}
@layer utilities {

View File

@ -0,0 +1,46 @@
import { useArk } from "@lume/ark";
import { LoaderIcon } from "@lume/icons";
import { Dispatch, SetStateAction, useState } from "react";
import { useTranslation } from "react-i18next";
import { toast } from "sonner";
export function AvatarUploader({
setPicture,
}: {
setPicture: Dispatch<SetStateAction<string>>;
}) {
const ark = useArk();
const [t] = useTranslation();
const [loading, setLoading] = useState(false);
const uploadAvatar = async () => {
// start loading
setLoading(true);
try {
const image = await ark.upload();
setPicture(image);
} catch (e) {
toast.error(String(e));
}
// stop loading
setLoading(false);
};
return (
<button
type="button"
onClick={() => uploadAvatar()}
className="inline-flex w-32 items-center justify-center rounded-lg border border-blue-200 bg-blue-100 px-2 py-1.5 text-sm font-medium text-blue-500 hover:border-blue-300 hover:bg-blue-200 dark:border-blue-800 dark:bg-blue-900 dark:text-blue-500 dark:hover:border-blue-800 dark:hover:bg-blue-800"
>
{loading ? (
<button type="button" className="size-4" disabled>
<LoaderIcon className="size-4 animate-spin" />
</button>
) : (
t("user.avatarButton")
)}
</button>
);
}

View File

@ -7,7 +7,6 @@ import { UnlistenFn } from "@tauri-apps/api/event";
import { getCurrent } from "@tauri-apps/api/window";
import { useEffect, useRef, useState } from "react";
import { VList, VListHandle } from "virtua";
import { useHotkeys } from "react-hotkeys-hook";
export const Route = createFileRoute("/$account/home")({
component: Screen,
@ -27,9 +26,6 @@ function Screen() {
const [selectedIndex, setSelectedIndex] = useState(-1);
const [isScroll, setIsScroll] = useState(false);
useHotkeys("left", () => goLeft());
useHotkeys("right", () => goRight());
const goLeft = () => {
const prevIndex = Math.max(selectedIndex - 1, 0);
setSelectedIndex(prevIndex);

View File

@ -1,4 +1,4 @@
import { ComposeFilledIcon, HorizontalDotsIcon, PlusIcon } from "@lume/icons";
import { ComposeFilledIcon, PlusIcon } from "@lume/icons";
import { Outlet, createFileRoute } from "@tanstack/react-router";
import { cn } from "@lume/utils";
import { Accounts } from "@/components/accounts";

View File

@ -0,0 +1,16 @@
import { Box, Container } from "@lume/ui";
import { Outlet, createLazyFileRoute } from "@tanstack/react-router";
export const Route = createLazyFileRoute("/auth")({
component: Screen,
});
function Screen() {
return (
<Container withDrag>
<Box className="px-3 pt-3">
<Outlet />
</Box>
</Container>
);
}

View File

@ -0,0 +1,25 @@
import { LoaderIcon } from "@lume/icons";
import { createFileRoute } from "@tanstack/react-router";
export const Route = createFileRoute("/auth/create")({
component: Screen,
loader: ({ context }) => {
return context.ark.create_keys();
},
pendingComponent: Pending,
});
function Screen() {
return <div className="px-5"></div>;
}
function Pending() {
return (
<div className="flex h-full w-full flex-col items-center gap-2">
<button type="button" className="size-5" disabled>
<LoaderIcon className="size-5 animate-spin" />
</button>
<p>Creating account</p>
</div>
);
}

View File

@ -1,108 +0,0 @@
import { LoaderIcon } from "@lume/icons";
import { cn } from "@lume/utils";
import { createLazyFileRoute, useNavigate } from "@tanstack/react-router";
import { useState } from "react";
import { useTranslation } from "react-i18next";
export const Route = createLazyFileRoute("/auth/create/")({
component: Screen,
});
function Screen() {
const navigate = useNavigate();
const [t] = useTranslation();
const [method, setMethod] = useState<"self" | "managed">("self");
const [loading, setLoading] = useState(false);
const next = () => {
setLoading(true);
if (method === "self") {
navigate({ to: "/auth/create/self" });
} else {
navigate({ to: "/auth/create/managed" });
}
};
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("signup.title")}</h1>
<p className="text-lg leading-snug text-neutral-600 dark:text-neutral-500">
{t("signup.subtitle")}
</p>
</div>
<div className="flex flex-col gap-4">
<button
type="button"
onClick={() => setMethod("self")}
className={cn(
"flex flex-col items-start rounded-xl bg-neutral-100 px-4 py-3.5 hover:bg-neutral-200 dark:bg-neutral-900 dark:hover:bg-neutral-800",
method === "self"
? "ring-1 ring-blue-500 ring-offset-2 ring-offset-white dark:ring-offset-black"
: "",
)}
>
<p className="font-semibold">{t("signup.selfManageMethod")}</p>
<p className="text-sm text-neutral-600 dark:text-neutral-500">
{t("signup.selfManageMethodDescription")}
</p>
</button>
<button
type="button"
onClick={() => setMethod("managed")}
className={cn(
"flex flex-col items-start rounded-xl bg-neutral-100 px-4 py-3.5 hover:bg-neutral-200 dark:bg-neutral-900 dark:hover:bg-neutral-800",
method === "managed"
? "ring-1 ring-blue-500 ring-offset-2 ring-offset-white dark:ring-offset-black"
: "",
)}
>
<p className="font-semibold">{t("signup.providerMethod")}</p>
<p className="text-sm text-neutral-600 dark:text-neutral-500">
{t("signup.providerMethodDescription")}
</p>
</button>
<div className="flex flex-col gap-3">
<button
type="button"
onClick={next}
className="inline-flex h-12 w-full items-center justify-center rounded-xl bg-blue-500 text-lg font-medium text-white hover:bg-blue-600"
>
{loading ? (
<LoaderIcon className="size-5 animate-spin" />
) : (
t("global.continue")
)}
</button>
{method === "managed" ? (
<div className="flex flex-col gap-1.5 rounded-xl border border-red-200 bg-red-100 p-2 text-sm text-red-800 dark:border-red-800 dark:bg-red-900 dark:text-red-200">
<p className="font-semibold text-red-900 dark:text-red-100">
Attention:
</p>
<p>
You're chosing Managed by Provider, this feature still in
"Beta".
</p>
<p>
Some functions still missing or not work as expected, you
shouldn't create your main account with this method
</p>
<a
href="https://github.com/kind-0/nsecbunkerd/blob/master/OAUTH-LIKE-FLOW.md"
target="_blank"
rel="noreferrer"
className="text-blue-500"
>
Learn more
</a>
</div>
) : null}
</div>
</div>
</div>
</div>
);
}

View File

@ -1,5 +0,0 @@
import { createLazyFileRoute } from '@tanstack/react-router'
export const Route = createLazyFileRoute('/auth/create/managed')({
component: () => <div>Hello /auth/create/managed!</div>
})

View File

@ -1,161 +0,0 @@
import { useArk } from "@lume/ark";
import { CheckIcon, EyeOffIcon, EyeOnIcon, LoaderIcon } from "@lume/icons";
import { Keys } from "@lume/types";
import * as Checkbox from "@radix-ui/react-checkbox";
import { createLazyFileRoute, useNavigate } from "@tanstack/react-router";
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { toast } from "sonner";
export const Route = createLazyFileRoute("/auth/create/self")({
component: Create,
});
function Create() {
const ark = useArk();
const navigate = useNavigate();
const [t] = useTranslation();
const [loading, setLoading] = useState(false);
const [showKey, setShowKey] = useState(false);
const [confirm, setConfirm] = useState({ c1: false, c2: false, c3: false });
const [keys, setKeys] = useState<Keys>(null);
const submit = async () => {
setLoading(true);
try {
await ark.save_account(keys);
navigate({
to: "/$account/home/local",
params: { account: keys.npub },
search: { onboarding: true },
replace: true,
});
} catch (e) {
setLoading(false);
toast.error(e);
}
};
useEffect(() => {
async function genKeys() {
const res = await ark.create_keys();
setKeys(res);
}
genKeys();
}, []);
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("signupWithSelfManage.title")}
</h1>
<p className="text-lg leading-snug text-neutral-600 dark:text-neutral-500">
{t("signupWithSelfManage.subtitle")}
</p>
</div>
<div className="mb-0 flex flex-col gap-6">
<div className="flex flex-col gap-4">
<div className="relative">
{keys ? (
<input
readOnly
value={keys.nsec}
type={showKey ? "text" : "password"}
className="h-12 w-full resize-none rounded-xl border-transparent bg-neutral-100 pl-3 pr-14 placeholder:text-neutral-600 focus:border-blue-500 focus:ring focus:ring-blue-100 dark:bg-neutral-900 dark:focus:ring-blue-900"
/>
) : null}
<button
type="button"
onClick={() => setShowKey((state) => !state)}
className="absolute right-2 top-2 inline-flex size-8 items-center justify-center rounded-lg bg-neutral-200 hover:bg-neutral-300 dark:bg-neutral-800 dark:hover:bg-neutral-700"
>
{showKey ? (
<EyeOnIcon className="size-4" />
) : (
<EyeOffIcon className="size-4" />
)}
</button>
</div>
<div className="flex flex-col gap-3">
<div className="flex items-center gap-2">
<Checkbox.Root
checked={confirm.c1}
onCheckedChange={() =>
setConfirm((state) => ({ ...state, c1: !state.c1 }))
}
className="flex size-7 appearance-none items-center justify-center rounded-lg bg-neutral-100 outline-none dark:bg-neutral-900"
id="confirm1"
>
<Checkbox.Indicator className="text-blue-500">
<CheckIcon className="size-4" />
</Checkbox.Indicator>
</Checkbox.Root>
<label
className="text-sm text-neutral-700 dark:text-neutral-400"
htmlFor="confirm1"
>
{t("signupWithSelfManage.confirm1")}
</label>
</div>
<div className="flex items-center gap-2">
<Checkbox.Root
checked={confirm.c3}
onCheckedChange={() =>
setConfirm((state) => ({ ...state, c3: !state.c3 }))
}
className="flex size-7 appearance-none items-center justify-center rounded-lg bg-neutral-100 outline-none dark:bg-neutral-900"
id="confirm3"
>
<Checkbox.Indicator className="text-blue-500">
<CheckIcon className="size-4" />
</Checkbox.Indicator>
</Checkbox.Root>
<label
className="text-sm text-neutral-700 dark:text-neutral-400"
htmlFor="confirm3"
>
{t("signupWithSelfManage.confirm3")}
</label>
</div>
<div className="flex items-center gap-2">
<Checkbox.Root
checked={confirm.c2}
onCheckedChange={() =>
setConfirm((state) => ({ ...state, c2: !state.c2 }))
}
className="flex size-7 appearance-none items-center justify-center rounded-lg bg-neutral-100 outline-none dark:bg-neutral-900"
id="confirm2"
>
<Checkbox.Indicator className="text-blue-500">
<CheckIcon className="size-4" />
</Checkbox.Indicator>
</Checkbox.Root>
<label
className="text-sm text-neutral-700 dark:text-neutral-400"
htmlFor="confirm2"
>
{t("signupWithSelfManage.confirm2")}
</label>
</div>
</div>
</div>
<button
type="button"
onClick={submit}
disabled={!confirm.c1 || !confirm.c2 || !confirm.c3}
className="inline-flex h-12 w-full items-center justify-center rounded-xl bg-blue-500 text-lg font-medium text-white hover:bg-blue-600 disabled:opacity-50"
>
{loading ? (
<LoaderIcon className="size-5 animate-spin" />
) : (
t("signupWithSelfManage.button")
)}
</button>
</div>
</div>
</div>
);
}

View File

@ -1,101 +0,0 @@
import { useArk } from "@lume/ark";
import { LoaderIcon } from "@lume/icons";
import { createLazyFileRoute, useNavigate } from "@tanstack/react-router";
import { invoke } from "@tauri-apps/api/core";
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { toast } from "sonner";
export const Route = createLazyFileRoute("/auth/import")({
component: Import,
});
function Import() {
const ark = useArk();
const navigate = useNavigate();
const [t] = useTranslation();
const [key, setKey] = useState("");
const [password, setPassword] = useState("");
const [loading, setLoading] = useState(false);
const submit = async () => {
if (!key.startsWith("nsec1")) return;
if (key.length < 30) return;
setLoading(true);
try {
const npub: string = await invoke("get_public_key", { nsec: key });
await ark.save_account({
npub,
nsec: key,
});
navigate({
to: "/$account/home/local",
params: { account: npub },
search: { onboarding: true },
replace: true,
});
} catch (e) {
setLoading(false);
toast.error(e);
}
};
const isNip05 = /^[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,4}$/.test(key);
const isNip49 = key.startsWith("ncryptsec");
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("login.title")}</h1>
<p className="text-lg leading-snug text-neutral-600 dark:text-neutral-500">
{t("login.subtitle")}
</p>
</div>
<div className="flex flex-col gap-6">
<div className="flex flex-col gap-4">
<div>
<input
value={key}
type="text"
onChange={(e) => setKey(e.target.value)}
className="h-12 w-full resize-none rounded-xl border-transparent bg-neutral-100 pl-3 pr-10 placeholder:text-neutral-600 focus:border-blue-500 focus:ring focus:ring-blue-100 dark:bg-neutral-900 dark:focus:ring-blue-900"
/>
</div>
{isNip05 || isNip49 ? (
<div className="flex flex-col gap-1.5">
<label
htmlFor="password"
className="font-medium text-neutral-900 dark:text-neutral-100"
>
Password *
</label>
<input
value={password}
name="password"
type="password"
onChange={(e) => setPassword(e.target.value)}
className="h-12 w-full resize-none rounded-xl border-transparent bg-neutral-100 pl-3 pr-10 placeholder:text-neutral-600 focus:border-blue-500 focus:ring focus:ring-blue-100 dark:bg-neutral-900 dark:focus:ring-blue-900"
/>
</div>
) : null}
</div>
<button
type="button"
onClick={submit}
className="inline-flex h-12 w-full items-center justify-center rounded-xl bg-blue-500 text-lg font-medium text-white hover:bg-blue-600 disabled:opacity-50"
>
{loading ? (
<LoaderIcon className="size-5 animate-spin" />
) : (
"Import"
)}
</button>
</div>
</div>
</div>
);
}

View File

@ -0,0 +1,102 @@
import { useArk } from "@lume/ark";
import { LoaderIcon } from "@lume/icons";
import { createLazyFileRoute, useNavigate } from "@tanstack/react-router";
import { invoke } from "@tauri-apps/api/core";
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { toast } from "sonner";
export const Route = createLazyFileRoute("/auth/privkey")({
component: Screen,
});
function Screen() {
const ark = useArk();
const navigate = useNavigate();
const [t] = useTranslation();
const [key, setKey] = useState("");
const [password, setPassword] = useState("");
const [loading, setLoading] = useState(false);
const submit = async () => {
if (!key.startsWith("nsec1"))
return toast.warning(
"You need to enter a valid private key starts with nsec or ncryptsec",
);
if (key.length < 30)
return toast.warning("You need to enter a valid private key");
setLoading(true);
try {
const npub = await ark.save_account(key, password);
navigate({
to: "/$account/home",
params: { account: npub },
search: { onboarding: true },
replace: true,
});
} catch (e) {
toast.error(e);
}
setLoading(false);
};
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 text-center">
<h1 className="text-2xl font-semibold">{t("login.title")}</h1>
<p className="text-lg leading-snug text-neutral-600 dark:text-neutral-500">
{t("login.subtitle")}
</p>
</div>
<div className="flex flex-col gap-6">
<div className="flex flex-col gap-4">
<div className="flex flex-col gap-1.5">
<label
htmlFor="key"
className="font-medium text-neutral-900 dark:text-neutral-100"
>
Private Key
</label>
<input
name="key"
type="text"
placeholder="nsec or ncryptsec..."
value={key}
onChange={(e) => setKey(e.target.value)}
className="h-11 w-full 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-1.5">
<label
htmlFor="password"
className="font-medium text-neutral-900 dark:text-neutral-100"
>
Password (Optional)
</label>
<input
name="password"
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
className="h-11 w-full 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>
<button
type="button"
onClick={submit}
disabled={loading}
className="inline-flex h-11 w-full items-center justify-center rounded-lg bg-blue-500 text-lg font-medium text-white hover:bg-blue-600"
>
{loading ? <LoaderIcon className="size-5 animate-spin" /> : "Login"}
</button>
</div>
</div>
</div>
);
}

View File

@ -0,0 +1,9 @@
import { createLazyFileRoute } from "@tanstack/react-router";
export const Route = createLazyFileRoute("/auth/remote")({
component: Screen,
});
function Screen() {
return <div>#todo</div>;
}

View File

@ -13,11 +13,8 @@ export const Route = createFileRoute("/")({
switch (accounts.length) {
// Guest account
case 0:
const guest = await ark.create_guest_account();
throw redirect({
to: "/$account/home",
params: { account: guest },
search: { guest: true },
to: "/landing",
replace: true,
});
// Only 1 account, skip account selection screen

View File

@ -1,3 +1,4 @@
import { KeyIcon, RemoteIcon } from "@lume/icons";
import { Link, createFileRoute } from "@tanstack/react-router";
import { useTranslation } from "react-i18next";
@ -6,12 +7,16 @@ export const Route = createFileRoute("/landing/")({
});
function Screen() {
const context = Route.useRouteContext();
const { t } = useTranslation();
const context = Route.useRouteContext();
return (
<div className="flex h-screen w-screen bg-black">
<div className="flex h-full w-full flex-col items-center justify-between">
<div className="relative flex h-screen w-screen bg-black">
<div
data-tauri-drag-region
className="absolute left-0 top-0 z-50 h-16 w-full"
/>
<div className="z-20 flex h-full w-full flex-col items-center justify-between">
<div />
<div className="mx-auto flex w-full max-w-4xl flex-col items-center gap-10">
<div className="flex flex-col items-center text-center">
@ -19,39 +24,59 @@ function Screen() {
src={`/heading-${context.locale}.png`}
srcSet={`/heading-${context.locale}@2x.png 2x`}
alt="lume"
className="w-2/3"
className="w-3/4 xl:w-2/3"
/>
<p className="mt-5 whitespace-pre-line text-lg font-medium leading-snug text-neutral-700">
<p className="mt-4 whitespace-pre-line text-lg font-medium leading-snug text-white/70">
{t("welcome.title")}
</p>
</div>
<div className="mx-auto flex w-full max-w-sm flex-col gap-2">
<div className="mx-auto flex w-full max-w-sm flex-col gap-4">
<Link
to="/auth/create"
className="inline-flex h-12 w-full items-center justify-center rounded-xl bg-blue-500 text-lg font-medium text-white hover:bg-blue-600"
className="inline-flex h-11 w-full items-center justify-center rounded-lg bg-white font-medium text-blue-500 backdrop-blur-lg hover:bg-white/90"
>
{t("welcome.signup")}
</Link>
<Link
to="/auth/import"
className="inline-flex h-12 w-full items-center justify-center rounded-xl bg-neutral-950 text-lg font-medium text-white hover:bg-neutral-900"
>
{t("welcome.login")}
</Link>
<div className="flex items-center gap-2">
<div className="h-px flex-1 bg-white/20" />
<span className="text-white">Or</span>
<div className="h-px flex-1 bg-white/20" />
</div>
<div className="flex flex-col gap-2">
<Link
to="/auth/remote"
className="inline-flex h-11 w-full items-center justify-center gap-2 rounded-lg bg-white/20 px-3 font-medium text-white backdrop-blur-md hover:bg-white/40"
>
<RemoteIcon className="size-5" />
Continue with Remote Signing
</Link>
<Link
to="/auth/privkey"
className="inline-flex h-11 w-full items-center justify-center gap-2 rounded-lg bg-white/20 px-3 font-medium text-white backdrop-blur-md hover:bg-white/40"
>
<KeyIcon className="size-5" />
Continue with Private Key
</Link>
</div>
</div>
</div>
<div className="flex h-11 items-center justify-center">
<p className="text-neutral-800">
{t("welcome.footer")}{" "}
<Link
to="https://nostr.com"
target="_blank"
className="text-blue-500"
>
here
</Link>
</p>
</div>
<div className="flex h-11 items-center justify-center"></div>
</div>
<div className="absolute z-10 h-full w-full bg-white/5 backdrop-blur-sm dark:bg-black/5" />
<div className="absolute inset-0 h-full w-full">
<img
src="/lock-screen.jpg"
srcSet="/lock-screen@2x.jpg 2x"
alt="Lock Screen Background"
className="h-full w-full object-cover"
/>
<a
href="https://njump.me/nprofile1qqs9tuz9jpn57djg7nxunhyvuvk69g5zqaxdpvpqt9hwqv7395u9rpg6zq5uw"
target="_blank"
className="absolute bottom-3 right-3 z-50 rounded-md bg-white/20 px-2 py-1 text-xs font-medium text-white backdrop-blur-md dark:bg-black/20"
>
Design by NoGood
</a>
</div>
</div>
);

View File

@ -40,11 +40,13 @@ export function Screen() {
<Column.Root>
<Column.Header id={id} name={name} />
<Column.Content>
{isLoading || isRefetching ? (
{isLoading ? (
<div className="flex h-20 w-full flex-col items-center justify-center gap-1">
<LoaderIcon className="size-5 animate-spin" />
<button type="button" className="size-5" disabled>
<LoaderIcon className="size-5 animate-spin" />
</button>
</div>
) : !data.length ? (
) : !data ? (
<div className="flex flex-col gap-3">
<div className="flex items-center gap-2 rounded-xl bg-neutral-50 p-5 dark:bg-neutral-950">
<InfoIcon className="size-6" />
@ -61,13 +63,11 @@ export function Screen() {
</Virtualizer>
)}
<div className="flex h-20 items-center justify-center">
{!isLoading && hasNextPage ? (
{data?.length && hasNextPage ? (
<button
type="button"
onClick={() => fetchNextPage()}
disabled={
!hasNextPage || isFetchingNextPage || isFetchingNextPage
}
disabled={isFetchingNextPage || isFetchingNextPage}
className="inline-flex h-12 w-36 items-center justify-center gap-2 rounded-full bg-neutral-100 px-3 font-medium hover:bg-neutral-200 focus:outline-none dark:bg-neutral-900 dark:hover:bg-neutral-800"
>
{isFetchingNextPage ? (

View File

@ -71,7 +71,7 @@ export class Ark {
public async save_account(nsec: string, password: string = "") {
try {
const cmd: boolean = await invoke("save_key", {
const cmd: string = await invoke("save_key", {
nsec,
password,
});

View File

@ -122,3 +122,5 @@ export * from "./src/infoCircle";
export * from "./src/cancelCircle";
export * from "./src/laurel";
export * from "./src/quote";
export * from "./src/key";
export * from "./src/remote";

View File

@ -0,0 +1,13 @@
export function KeyIcon(props: JSX.IntrinsicElements["svg"]) {
return (
<svg width="24" height="24" fill="none" viewBox="0 0 24 24" {...props}>
<path
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="1.5"
d="M13 12.815a7.68 7.68 0 0 0-1-.065c-3.448 0-6.05 2.231-7.07 5.364-.363 1.116.58 2.136 1.753 2.136H14M15.75 6.5a3.75 3.75 0 1 1-7.5 0 3.75 3.75 0 0 1 7.5 0Zm0 7.5a2.25 2.25 0 1 1 3.5 1.871V17l-.5.936.5 1.032V20L18 21l-1.25-1v-4.129a2.248 2.248 0 0 1-1-1.871Z"
/>
</svg>
);
}

View File

@ -0,0 +1,13 @@
export function RemoteIcon(props: JSX.IntrinsicElements["svg"]) {
return (
<svg width="24" height="24" fill="none" viewBox="0 0 24 24" {...props}>
<path
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="1.5"
d="M6.75 21.25h10.5m-5.25-4V21m0-3.75a7.25 7.25 0 1 0 0-14.5 7.25 7.25 0 0 0 0 14.5ZM15.25 10a3.25 3.25 0 1 1-6.5 0 3.25 3.25 0 0 1 6.5 0Z"
/>
</svg>
);
}

View File

@ -105,6 +105,9 @@ importers:
i18next-resources-to-backend:
specifier: ^1.2.0
version: 1.2.0
minidenticons:
specifier: ^4.2.1
version: 4.2.1
nanoid:
specifier: ^5.0.6
version: 5.0.6
@ -120,6 +123,9 @@ importers:
react-dom:
specifier: ^18.2.0
version: 18.2.0(react@18.2.0)
react-hook-form:
specifier: ^7.51.0
version: 7.51.0(react@18.2.0)
react-hotkeys-hook:
specifier: ^4.5.0
version: 4.5.0(react-dom@18.2.0)(react@18.2.0)

View File

@ -31,10 +31,10 @@ pub async fn save_key(
password: &str,
app_handle: tauri::AppHandle,
state: State<'_, Nostr>,
) -> Result<bool, String> {
) -> Result<String, String> {
let secret_key: Result<SecretKey, String>;
if nsec.starts_with("ncrypto") {
if nsec.starts_with("ncryptsec") {
let encrypted_key = EncryptedSecretKey::from_bech32(nsec).unwrap();
secret_key = match encrypted_key.to_secret_key(password) {
Ok(val) => Ok(val),
@ -68,7 +68,7 @@ pub async fn save_key(
// Update client's signer
client.set_signer(Some(signer)).await;
Ok(true)
Ok(npub)
}
Err(msg) => Err(msg.into()),
}