mirror of
https://github.com/lumehq/lume.git
synced 2025-09-30 19:13:47 +02:00
feat: update login flow
This commit is contained in:
@@ -189,12 +189,35 @@ export default function Router() {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "import",
|
path: "login",
|
||||||
async lazy() {
|
async lazy() {
|
||||||
const { ImportAccountScreen } = await import(
|
const { LoginScreen } = await import("./routes/auth/login");
|
||||||
"./routes/auth/import"
|
return { Component: LoginScreen };
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "login-key",
|
||||||
|
async lazy() {
|
||||||
|
const { LoginWithKey } = await import("./routes/auth/login-key");
|
||||||
|
return { Component: LoginWithKey };
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "login-nsecbunker",
|
||||||
|
async lazy() {
|
||||||
|
const { LoginWithNsecbunker } = await import(
|
||||||
|
"./routes/auth/login-nsecbunker"
|
||||||
);
|
);
|
||||||
return { Component: ImportAccountScreen };
|
return { Component: LoginWithNsecbunker };
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "login-oauth",
|
||||||
|
async lazy() {
|
||||||
|
const { LoginWithOAuth } = await import(
|
||||||
|
"./routes/auth/login-oauth"
|
||||||
|
);
|
||||||
|
return { Component: LoginWithOAuth };
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@@ -89,31 +89,31 @@ export function CreateAccountScreen() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const onSubmit = async (data: { username: string; email: string }) => {
|
const onSubmit = async (data: { username: string; email: string }) => {
|
||||||
|
try {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
|
|
||||||
const domain = getDomainName(serviceId);
|
const domain = getDomainName(serviceId);
|
||||||
const service = services.find((ev) => ev.id === serviceId);
|
const service = services.find((ev) => ev.id === serviceId);
|
||||||
|
|
||||||
|
// generate ndk for nsecbunker
|
||||||
const localSigner = NDKPrivateKeySigner.generate();
|
const localSigner = NDKPrivateKeySigner.generate();
|
||||||
const localUser = await localSigner.user();
|
|
||||||
|
|
||||||
const bunker = new NDK({
|
const bunker = new NDK({
|
||||||
explicitRelayUrls: [
|
explicitRelayUrls: [
|
||||||
"wss://relay.nsecbunker.com/",
|
"wss://relay.nsecbunker.com/",
|
||||||
"wss://nostr.vulpem.com/",
|
"wss://nostr.vulpem.com/",
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
await bunker.connect(2000);
|
await bunker.connect(2000);
|
||||||
|
|
||||||
|
// generate tmp remote singer for create account
|
||||||
const remoteSigner = new NDKNip46Signer(
|
const remoteSigner = new NDKNip46Signer(
|
||||||
bunker,
|
bunker,
|
||||||
service.pubkey,
|
service.pubkey,
|
||||||
localSigner,
|
localSigner,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// handle auth url request
|
||||||
let authWindow: Window;
|
let authWindow: Window;
|
||||||
|
|
||||||
remoteSigner.addListener("authUrl", (authUrl: string) => {
|
remoteSigner.addListener("authUrl", (authUrl: string) => {
|
||||||
authWindow = new Window(`auth-${serviceId}`, {
|
authWindow = new Window(`auth-${serviceId}`, {
|
||||||
url: authUrl,
|
url: authUrl,
|
||||||
@@ -126,6 +126,7 @@ export function CreateAccountScreen() {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// create new account
|
||||||
const account = await remoteSigner.createAccount(
|
const account = await remoteSigner.createAccount(
|
||||||
data.username,
|
data.username,
|
||||||
domain,
|
domain,
|
||||||
@@ -139,13 +140,20 @@ export function CreateAccountScreen() {
|
|||||||
return toast.error("Failed to create new account, try again later");
|
return toast.error("Failed to create new account, try again later");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// add account to storage
|
||||||
await storage.createSetting("nsecbunker", "1");
|
await storage.createSetting("nsecbunker", "1");
|
||||||
await storage.createAccount({
|
await storage.createAccount({
|
||||||
pubkey: account,
|
pubkey: account,
|
||||||
privkey: localSigner.privateKey,
|
privkey: localSigner.privateKey,
|
||||||
});
|
});
|
||||||
|
|
||||||
ark.updateNostrSigner({ signer: remoteSigner });
|
// get final signer with newly created account
|
||||||
|
const finalSigner = new NDKNip46Signer(bunker, account, localSigner);
|
||||||
|
await finalSigner.blockUntilReady();
|
||||||
|
|
||||||
|
// update main ndk instance signer
|
||||||
|
ark.updateNostrSigner({ signer: finalSigner });
|
||||||
|
console.log(ark.ndk.signer);
|
||||||
|
|
||||||
// remove default nsecbunker profile and contact list
|
// remove default nsecbunker profile and contact list
|
||||||
await ark.createEvent({ kind: NDKKind.Metadata, content: "", tags: [] });
|
await ark.createEvent({ kind: NDKKind.Metadata, content: "", tags: [] });
|
||||||
@@ -157,6 +165,10 @@ export function CreateAccountScreen() {
|
|||||||
authWindow.close();
|
authWindow.close();
|
||||||
|
|
||||||
return navigate("/auth/onboarding");
|
return navigate("/auth/onboarding");
|
||||||
|
} catch (e) {
|
||||||
|
setIsLoading(false);
|
||||||
|
toast.error(String(e));
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@@ -1,259 +0,0 @@
|
|||||||
import { useArk } from "@lume/ark";
|
|
||||||
import {
|
|
||||||
ArrowRightIcon,
|
|
||||||
CancelIcon,
|
|
||||||
ChevronDownIcon,
|
|
||||||
LoaderIcon,
|
|
||||||
PlusIcon,
|
|
||||||
} from "@lume/icons";
|
|
||||||
import { User } from "@lume/ui";
|
|
||||||
import * as Accordion from "@radix-ui/react-accordion";
|
|
||||||
import { useQuery } from "@tanstack/react-query";
|
|
||||||
import { nip19 } from "nostr-tools";
|
|
||||||
import { useState } from "react";
|
|
||||||
import { useNavigate } from "react-router-dom";
|
|
||||||
import { toast } from "sonner";
|
|
||||||
import { twMerge } from "tailwind-merge";
|
|
||||||
|
|
||||||
const POPULAR_USERS = [
|
|
||||||
"npub180cvv07tjdrrgpa0j7j7tmnyl2yr6yr7l8j4s3evf6u64th6gkwsyjh6w6",
|
|
||||||
"npub1sg6plzptd64u62a878hep2kev88swjh3tw00gjsfl8f237lmu63q0uf63m",
|
|
||||||
"npub1xtscya34g58tk0z605fvr788k263gsu6cy9x0mhnm87echrgufzsevkk5s",
|
|
||||||
"npub1gcxzte5zlkncx26j68ez60fzkvtkm9e0vrwdcvsjakxf9mu9qewqlfnj5z",
|
|
||||||
"npub1az9xj85cmxv8e9j9y80lvqp97crsqdu2fpu3srwthd99qfu9qsgstam8y8",
|
|
||||||
"npub1a2cww4kn9wqte4ry70vyfwqyqvpswksna27rtxd8vty6c74era8sdcw83a",
|
|
||||||
"npub168ghgug469n4r2tuyw05dmqhqv5jcwm7nxytn67afmz8qkc4a4zqsu2dlc",
|
|
||||||
"npub133vj8ycevdle0cq8mtgddq0xtn34kxkwxvak983dx0u5vhqnycyqj6tcza",
|
|
||||||
"npub18ams6ewn5aj2n3wt2qawzglx9mr4nzksxhvrdc4gzrecw7n5tvjqctp424",
|
|
||||||
"npub1r0rs5q2gk0e3dk3nlc7gnu378ec6cnlenqp8a3cjhyzu6f8k5sgs4sq9ac",
|
|
||||||
"npub1prya33fnqerq0fljwjtp77ehtu7jlsjt5ydhwveuwmqdsdm6k8esk42xcv",
|
|
||||||
"npub19mduaf5569jx9xz555jcx3v06mvktvtpu0zgk47n4lcpjsz43zzqhj6vzk",
|
|
||||||
];
|
|
||||||
|
|
||||||
const LUME_USERS = [
|
|
||||||
"npub1zfss807aer0j26mwp2la0ume0jqde3823rmu97ra6sgyyg956e0s6xw445",
|
|
||||||
];
|
|
||||||
|
|
||||||
export function FollowsScreen() {
|
|
||||||
const ark = useArk();
|
|
||||||
const navigate = useNavigate();
|
|
||||||
|
|
||||||
const { status, data } = useQuery({
|
|
||||||
queryKey: ["trending-users"],
|
|
||||||
queryFn: async () => {
|
|
||||||
const res = await fetch("https://api.nostr.band/v0/trending/profiles");
|
|
||||||
if (!res.ok) {
|
|
||||||
throw new Error("Failed to fetch trending users from nostr.band API.");
|
|
||||||
}
|
|
||||||
return res.json();
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
const [follows, setFollows] = useState<string[]>([]);
|
|
||||||
|
|
||||||
// toggle follow state
|
|
||||||
const toggleFollow = (pubkey: string) => {
|
|
||||||
const arr = follows.includes(pubkey)
|
|
||||||
? follows.filter((i) => i !== pubkey)
|
|
||||||
: [...follows, pubkey];
|
|
||||||
setFollows(arr);
|
|
||||||
};
|
|
||||||
|
|
||||||
const submit = async () => {
|
|
||||||
try {
|
|
||||||
setLoading(true);
|
|
||||||
if (!follows.length) return navigate("/auth/finish");
|
|
||||||
|
|
||||||
const publish = await ark.newContactList({
|
|
||||||
tags: follows.map((item) => {
|
|
||||||
if (item.startsWith("npub1"))
|
|
||||||
return ["p", nip19.decode(item).data as string];
|
|
||||||
return ["p", item];
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (publish) {
|
|
||||||
setLoading(false);
|
|
||||||
return navigate("/auth/finish");
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
setLoading(false);
|
|
||||||
toast.error(e);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="relative flex h-full w-full items-center justify-center">
|
|
||||||
<div className="mx-auto flex w-full max-w-md flex-col gap-10">
|
|
||||||
<div className="flex flex-col gap-3">
|
|
||||||
<h1 className="text-2xl font-semibold text-center">
|
|
||||||
Dive into the nostrverse
|
|
||||||
</h1>
|
|
||||||
<p className="text-lg font-medium leading-snug text-center text-neutral-600 dark:text-neutral-500">
|
|
||||||
Try following some users that interest you
|
|
||||||
<br />
|
|
||||||
to build up your timeline.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<Accordion.Root type="single" defaultValue="recommended" collapsible>
|
|
||||||
<Accordion.Item
|
|
||||||
value="recommended"
|
|
||||||
className="mb-3 overflow-hidden rounded-xl"
|
|
||||||
>
|
|
||||||
<Accordion.Trigger className="flex h-14 w-full items-center justify-between rounded-t-xl px-3 font-medium bg-neutral-950">
|
|
||||||
Popular users
|
|
||||||
<ChevronDownIcon className="size-4" />
|
|
||||||
</Accordion.Trigger>
|
|
||||||
<Accordion.Content>
|
|
||||||
<div className="flex h-[400px] w-full flex-col overflow-y-auto rounded-b-xl px-3 bg-neutral-950">
|
|
||||||
{POPULAR_USERS.map((pubkey) => (
|
|
||||||
<div
|
|
||||||
key={pubkey}
|
|
||||||
className="flex h-max w-full shrink-0 flex-col mb-3 p-3 gap-4 overflow-hidden rounded-lg bg-neutral-900"
|
|
||||||
>
|
|
||||||
<User pubkey={pubkey} variant="large" />
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => toggleFollow(pubkey)}
|
|
||||||
className={twMerge(
|
|
||||||
"inline-flex h-9 shrink-0 w-full items-center justify-center gap-1 rounded-lg font-medium text-white",
|
|
||||||
follows.includes(pubkey)
|
|
||||||
? "bg-red-600 hover:bg-red-500"
|
|
||||||
: "bg-blue-600 hover:bg-blue-500",
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{follows.includes(pubkey) ? (
|
|
||||||
<>
|
|
||||||
<CancelIcon className="size-4" />
|
|
||||||
Unfollow
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<PlusIcon className="size-4" />
|
|
||||||
Follow
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</Accordion.Content>
|
|
||||||
</Accordion.Item>
|
|
||||||
<Accordion.Item
|
|
||||||
value="trending"
|
|
||||||
className="mb-3 overflow-hidden rounded-xl"
|
|
||||||
>
|
|
||||||
<Accordion.Trigger className="flex h-14 w-full items-center justify-between rounded-t-xl px-3 font-medium bg-neutral-950">
|
|
||||||
Trending users
|
|
||||||
<ChevronDownIcon className="size-4" />
|
|
||||||
</Accordion.Trigger>
|
|
||||||
<Accordion.Content>
|
|
||||||
<div className="flex h-[400px] w-full flex-col overflow-y-auto rounded-b-xl px-3 bg-neutral-950">
|
|
||||||
{status === "pending" ? (
|
|
||||||
<div className="flex h-full w-full items-center justify-center">
|
|
||||||
<LoaderIcon className="size-4 animate-spin" />
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
data?.profiles.map(
|
|
||||||
(item: {
|
|
||||||
pubkey: string;
|
|
||||||
profile: { content: string };
|
|
||||||
}) => (
|
|
||||||
<div
|
|
||||||
key={item.pubkey}
|
|
||||||
className="flex h-max w-full mb-3 p-3 gap-4 shrink-0 flex-col overflow-hidden rounded-lg bg-neutral-900"
|
|
||||||
>
|
|
||||||
<User pubkey={item.pubkey} variant="large" />
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => toggleFollow(item.pubkey)}
|
|
||||||
className={twMerge(
|
|
||||||
"inline-flex h-9 shrink-0 w-full items-center justify-center gap-1 rounded-lg font-medium text-white",
|
|
||||||
follows.includes(item.pubkey)
|
|
||||||
? "bg-red-600 hover:bg-red-500"
|
|
||||||
: "bg-blue-600 hover:bg-blue-500",
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{follows.includes(item.pubkey) ? (
|
|
||||||
<>
|
|
||||||
<CancelIcon className="size-4" />
|
|
||||||
Unfollow
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<PlusIcon className="size-4" />
|
|
||||||
Follow
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
)
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</Accordion.Content>
|
|
||||||
</Accordion.Item>
|
|
||||||
<Accordion.Item
|
|
||||||
value="lume"
|
|
||||||
className="mb-3 overflow-hidden rounded-xl"
|
|
||||||
>
|
|
||||||
<Accordion.Trigger className="flex h-14 w-full items-center justify-between rounded-t-xl px-3 font-medium bg-neutral-950">
|
|
||||||
Lume HQ
|
|
||||||
<ChevronDownIcon className="size-4" />
|
|
||||||
</Accordion.Trigger>
|
|
||||||
<Accordion.Content>
|
|
||||||
<div className="flex h-[400px] w-full flex-col gap-3 overflow-y-auto rounded-b-xl p-3 bg-neutral-950">
|
|
||||||
{LUME_USERS.map((pubkey) => (
|
|
||||||
<div
|
|
||||||
key={pubkey}
|
|
||||||
className="flex h-max w-full mb-3 p-3 gap-4 shrink-0 flex-col overflow-hidden rounded-lg bg-neutral-900"
|
|
||||||
>
|
|
||||||
<User pubkey={pubkey} variant="large" />
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => toggleFollow(pubkey)}
|
|
||||||
className={twMerge(
|
|
||||||
"inline-flex h-9 shrink-0 w-full items-center justify-center gap-1 rounded-lg font-medium text-white",
|
|
||||||
follows.includes(pubkey)
|
|
||||||
? "bg-red-600 hover:bg-red-500"
|
|
||||||
: "bg-blue-600 hover:bg-blue-500",
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{follows.includes(pubkey) ? (
|
|
||||||
<>
|
|
||||||
<CancelIcon className="size-4" />
|
|
||||||
Unfollow
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<PlusIcon className="size-4" />
|
|
||||||
Follow
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</Accordion.Content>
|
|
||||||
</Accordion.Item>
|
|
||||||
</Accordion.Root>
|
|
||||||
</div>
|
|
||||||
<div className="absolute bottom-4 right-4 flex w-full items-center justify-end gap-2">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={submit}
|
|
||||||
disabled={loading}
|
|
||||||
className="inline-flex h-12 w-max items-center justify-center gap-2 rounded-xl bg-blue-500 px-3 font-semibold text-white hover:bg-blue-600"
|
|
||||||
>
|
|
||||||
{loading ? (
|
|
||||||
<LoaderIcon className="size-4 animate-spin" />
|
|
||||||
) : (
|
|
||||||
<ArrowRightIcon className="size-4" />
|
|
||||||
)}
|
|
||||||
Continue
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
@@ -1,330 +0,0 @@
|
|||||||
import { useArk, useStorage } from "@lume/ark";
|
|
||||||
import { ArrowLeftIcon, LoaderIcon } from "@lume/icons";
|
|
||||||
import { User } from "@lume/ui";
|
|
||||||
import NDK, { NDKNip46Signer, NDKPrivateKeySigner } from "@nostr-dev-kit/ndk";
|
|
||||||
import { readText } from "@tauri-apps/plugin-clipboard-manager";
|
|
||||||
import { open } from "@tauri-apps/plugin-shell";
|
|
||||||
import { motion } from "framer-motion";
|
|
||||||
import { nip19 } from "nostr-tools";
|
|
||||||
import { useState } from "react";
|
|
||||||
import { useNavigate } from "react-router-dom";
|
|
||||||
import { toast } from "sonner";
|
|
||||||
import { twMerge } from "tailwind-merge";
|
|
||||||
|
|
||||||
export function ImportAccountScreen() {
|
|
||||||
const [npub, setNpub] = useState<string>("");
|
|
||||||
const [nsec, setNsec] = useState<string>("");
|
|
||||||
const [pubkey, setPubkey] = useState<undefined | string>(undefined);
|
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
const [created, setCreated] = useState({ ok: false, remote: false });
|
|
||||||
const [savedPrivkey, setSavedPrivkey] = useState(false);
|
|
||||||
|
|
||||||
const ark = useArk();
|
|
||||||
const storage = useStorage();
|
|
||||||
const navigate = useNavigate();
|
|
||||||
|
|
||||||
const submitNpub = async () => {
|
|
||||||
if (npub.length < 6) return toast.error("You must enter valid npub");
|
|
||||||
if (!npub.startsWith("npub1"))
|
|
||||||
return toast.error("npub must be starts with npub1");
|
|
||||||
|
|
||||||
try {
|
|
||||||
const pubkey = nip19.decode(npub).data as string;
|
|
||||||
setPubkey(pubkey);
|
|
||||||
} catch (e) {
|
|
||||||
return toast.error(`npub invalid: ${e}`);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const connectNsecBunker = async () => {
|
|
||||||
if (npub.length < 6) return toast.error("You must enter valid npub");
|
|
||||||
if (!npub.startsWith("npub1"))
|
|
||||||
return toast.error("npub must be starts with npub1");
|
|
||||||
|
|
||||||
try {
|
|
||||||
const pubkey = nip19.decode(npub.split("#")[0]).data as string;
|
|
||||||
const localSigner = NDKPrivateKeySigner.generate();
|
|
||||||
|
|
||||||
await storage.createSetting("nsecbunker", "1");
|
|
||||||
await storage.createPrivkey(`${npub}-nsecbunker`, localSigner.privateKey);
|
|
||||||
|
|
||||||
// open nsecbunker web app in default browser
|
|
||||||
await open("https://app.nsecbunker.com/keys");
|
|
||||||
|
|
||||||
const bunker = new NDK({
|
|
||||||
explicitRelayUrls: [
|
|
||||||
"wss://relay.nsecbunker.com",
|
|
||||||
"wss://nostr.vulpem.com",
|
|
||||||
],
|
|
||||||
});
|
|
||||||
await bunker.connect();
|
|
||||||
|
|
||||||
const remoteSigner = new NDKNip46Signer(bunker, npub, localSigner);
|
|
||||||
await remoteSigner.blockUntilReady();
|
|
||||||
ark.updateNostrSigner({ signer: remoteSigner });
|
|
||||||
|
|
||||||
setPubkey(pubkey);
|
|
||||||
setCreated({ ok: false, remote: true });
|
|
||||||
} catch (e) {
|
|
||||||
return toast.error(e);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const changeAccount = async () => {
|
|
||||||
setNpub("");
|
|
||||||
setPubkey("");
|
|
||||||
};
|
|
||||||
|
|
||||||
const createAccount = async () => {
|
|
||||||
try {
|
|
||||||
setLoading(true);
|
|
||||||
|
|
||||||
// add account to db
|
|
||||||
await storage.createAccount({ id: npub, pubkey });
|
|
||||||
|
|
||||||
// get account contacts
|
|
||||||
await ark.getUserContacts({ pubkey });
|
|
||||||
|
|
||||||
setCreated((prev) => ({ ...prev, ok: true }));
|
|
||||||
setLoading(false);
|
|
||||||
|
|
||||||
if (created.remote) navigate("/auth/onboarding");
|
|
||||||
} catch (e) {
|
|
||||||
setLoading(false);
|
|
||||||
return toast.error(e);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const pasteNsec = async () => {
|
|
||||||
const tempNsec = await readText();
|
|
||||||
setNsec(tempNsec);
|
|
||||||
};
|
|
||||||
|
|
||||||
const submitNsec = async () => {
|
|
||||||
if (savedPrivkey) return;
|
|
||||||
if (nsec.length > 50 && nsec.startsWith("nsec1")) {
|
|
||||||
try {
|
|
||||||
const privkey = nip19.decode(nsec).data as string;
|
|
||||||
await storage.createPrivkey(pubkey, privkey);
|
|
||||||
ark.updateNostrSigner({ signer: new NDKPrivateKeySigner(privkey) });
|
|
||||||
|
|
||||||
setSavedPrivkey(true);
|
|
||||||
} catch (e) {
|
|
||||||
return toast(`nsec invalid: ${e}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="relative flex h-full w-full items-center justify-center">
|
|
||||||
<div className="absolute left-[8px] top-2">
|
|
||||||
{!created ? (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => navigate(-1)}
|
|
||||||
className="inline-flex items-center gap-2 text-sm font-medium"
|
|
||||||
>
|
|
||||||
<div className="inline-flex h-8 w-8 items-center justify-center rounded-lg bg-neutral-200 text-neutral-800 dark:bg-neutral-800 dark:text-neutral-200">
|
|
||||||
<ArrowLeftIcon className="h-5 w-5" />
|
|
||||||
</div>
|
|
||||||
Back
|
|
||||||
</button>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
<div className="mx-auto flex w-full max-w-md flex-col gap-10">
|
|
||||||
<h1 className="text-center text-2xl font-semibold">
|
|
||||||
Import your account.
|
|
||||||
</h1>
|
|
||||||
<div className="flex flex-col gap-3">
|
|
||||||
<div className="rounded-xl bg-neutral-50 p-3 dark:bg-neutral-950">
|
|
||||||
<div className="flex flex-col gap-1.5">
|
|
||||||
<label htmlFor="npub" className="font-semibold">
|
|
||||||
Enter your public key:
|
|
||||||
</label>
|
|
||||||
<div className="flex w-full flex-col gap-2">
|
|
||||||
<input
|
|
||||||
readOnly={!!pubkey}
|
|
||||||
name="npub"
|
|
||||||
type="text"
|
|
||||||
value={npub}
|
|
||||||
onChange={(e) => setNpub(e.target.value)}
|
|
||||||
spellCheck={false}
|
|
||||||
autoComplete="off"
|
|
||||||
autoCorrect="off"
|
|
||||||
autoCapitalize="off"
|
|
||||||
placeholder="npub1"
|
|
||||||
className="h-11 rounded-lg border-transparent bg-neutral-100 px-3 placeholder:text-neutral-500 focus:border-blue-500 focus:ring focus:ring-blue-200 dark:bg-neutral-900 dark:placeholder:text-neutral-400 dark:focus:ring-blue-800"
|
|
||||||
/>
|
|
||||||
{!pubkey ? (
|
|
||||||
<div className="flex flex-col gap-2">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={submitNpub}
|
|
||||||
className="h-11 w-full shrink-0 rounded-lg bg-blue-500 font-semibold text-white hover:bg-blue-600"
|
|
||||||
>
|
|
||||||
Continue
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={connectNsecBunker}
|
|
||||||
className="h-11 w-full shrink-0 rounded-lg bg-neutral-200 font-medium hover:bg-neutral-300 dark:bg-neutral-800 dark:hover:bg-neutral-700"
|
|
||||||
>
|
|
||||||
Continue with nsecBunker
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
{npub.indexOf("#") > -1 ? (
|
|
||||||
<p className="text-sm text-neutral-600 dark:text-neutral-400">
|
|
||||||
You're using nsecbunker token, keep in mind it only can
|
|
||||||
redeem one-time, you need to login again in the next launch
|
|
||||||
</p>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{pubkey ? (
|
|
||||||
<motion.div
|
|
||||||
initial={{ opacity: 0, y: 50 }}
|
|
||||||
animate={{
|
|
||||||
opacity: 1,
|
|
||||||
y: 0,
|
|
||||||
}}
|
|
||||||
className="rounded-xl bg-neutral-50 p-3 dark:bg-neutral-950"
|
|
||||||
>
|
|
||||||
<h5 className="mb-1.5 font-semibold">Account found</h5>
|
|
||||||
<div className="flex w-full flex-col gap-2">
|
|
||||||
<div className="flex h-full w-full items-center justify-between rounded-lg bg-neutral-100 px-4 py-3 dark:bg-neutral-900">
|
|
||||||
<User pubkey={pubkey} variant="simple" />
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={changeAccount}
|
|
||||||
className="h-8 w-max shrink-0 rounded-lg bg-neutral-200 px-2.5 text-sm font-medium hover:bg-neutral-300 dark:bg-neutral-800 dark:hover:bg-neutral-700"
|
|
||||||
>
|
|
||||||
Change
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
{!created.ok ? (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={createAccount}
|
|
||||||
className="inline-flex h-11 w-full shrink-0 items-center justify-center rounded-lg bg-blue-500 font-semibold text-white hover:bg-blue-600"
|
|
||||||
>
|
|
||||||
{loading ? (
|
|
||||||
<LoaderIcon className="h-4 w-4 animate-spin" />
|
|
||||||
) : (
|
|
||||||
"Continue"
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
</motion.div>
|
|
||||||
) : null}
|
|
||||||
{created.ok ? (
|
|
||||||
<>
|
|
||||||
{!created.remote ? (
|
|
||||||
<motion.div
|
|
||||||
initial={{ opacity: 0, y: 50 }}
|
|
||||||
animate={{
|
|
||||||
opacity: 1,
|
|
||||||
y: 0,
|
|
||||||
}}
|
|
||||||
className="rounded-lg bg-neutral-100 p-3 text-neutral-800 dark:bg-neutral-900 dark:text-neutral-200"
|
|
||||||
>
|
|
||||||
<div className="flex flex-col gap-1.5">
|
|
||||||
<label htmlFor="nsec" className="font-semibold">
|
|
||||||
Enter your private key (optional):
|
|
||||||
</label>
|
|
||||||
<div className="inline-flex w-full items-center gap-2">
|
|
||||||
<div className="relative flex-1">
|
|
||||||
<input
|
|
||||||
name="nsec"
|
|
||||||
type="text"
|
|
||||||
value={nsec}
|
|
||||||
onChange={(e) => setNsec(e.target.value)}
|
|
||||||
spellCheck={false}
|
|
||||||
autoComplete="off"
|
|
||||||
autoCorrect="off"
|
|
||||||
autoCapitalize="off"
|
|
||||||
placeholder="nsec1"
|
|
||||||
className="h-11 w-full rounded-lg border-transparent bg-neutral-200 px-3 placeholder:text-neutral-500 focus:border-blue-500 focus:ring focus:ring-blue-200 dark:bg-neutral-800 dark:placeholder:text-neutral-400 dark:focus:ring-blue-800"
|
|
||||||
/>
|
|
||||||
{nsec.length < 5 ? (
|
|
||||||
<div className="absolute right-0 top-0 inline-flex h-11 items-center justify-center px-2">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={pasteNsec}
|
|
||||||
className="rounded-md bg-neutral-300 px-2 py-1 text-sm font-medium hover:bg-neutral-400 dark:bg-neutral-700 dark:hover:bg-neutral-600"
|
|
||||||
>
|
|
||||||
Paste
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
{nsec.length > 5 ? (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={submitNsec}
|
|
||||||
className={twMerge(
|
|
||||||
"h-11 w-24 shrink-0 rounded-lg font-semibold text-white",
|
|
||||||
!savedPrivkey
|
|
||||||
? "bg-blue-500 hover:bg-blue-600"
|
|
||||||
: "bg-teal-500 hover:bg-teal-600",
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{savedPrivkey ? "Saved" : "Save"}
|
|
||||||
</button>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="mt-3 select-text">
|
|
||||||
<p className="text-sm">
|
|
||||||
<b>Private Key</b> is used to sign your event. For
|
|
||||||
example, if you want to make a new post or send a message
|
|
||||||
to your contact, you need to use your private key to sign
|
|
||||||
this event.
|
|
||||||
</p>
|
|
||||||
<h5 className="mt-2 font-semibold">
|
|
||||||
1. In case you store private key in Lume
|
|
||||||
</h5>
|
|
||||||
<p className="text-sm">
|
|
||||||
Lume will put your private key to{" "}
|
|
||||||
<b>
|
|
||||||
{storage.platform === "macos"
|
|
||||||
? "Apple Keychain (macOS)"
|
|
||||||
: storage.platform === "windows"
|
|
||||||
? "Credential Manager (Windows)"
|
|
||||||
: "Secret Service (Linux)"}
|
|
||||||
</b>
|
|
||||||
, it will be secured by your OS
|
|
||||||
</p>
|
|
||||||
<h5 className="mt-2 font-semibold">
|
|
||||||
2. In case you do not store private key in Lume
|
|
||||||
</h5>
|
|
||||||
<p className="text-sm">
|
|
||||||
When you make an event that requires a sign by your
|
|
||||||
private key, Lume will show a prompt for you to enter
|
|
||||||
private key. It will be cleared after signing and not
|
|
||||||
stored anywhere.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</motion.div>
|
|
||||||
) : null}
|
|
||||||
<motion.button
|
|
||||||
initial={{ opacity: 0, y: 80 }}
|
|
||||||
animate={{
|
|
||||||
opacity: 1,
|
|
||||||
y: 0,
|
|
||||||
}}
|
|
||||||
className="inline-flex h-11 w-full shrink-0 items-center justify-center rounded-lg bg-blue-500 font-semibold text-white hover:bg-blue-600"
|
|
||||||
type="button"
|
|
||||||
onClick={() => navigate("/auth/onboarding")}
|
|
||||||
>
|
|
||||||
Continue
|
|
||||||
</motion.button>
|
|
||||||
</>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
115
apps/desktop/src/routes/auth/login-key.tsx
Normal file
115
apps/desktop/src/routes/auth/login-key.tsx
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
import { useStorage } from "@lume/ark";
|
||||||
|
import { EyeOffIcon, EyeOnIcon, LoaderIcon } from "@lume/icons";
|
||||||
|
import { getPublicKey, nip19 } from "nostr-tools";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
|
||||||
|
export function LoginWithKey() {
|
||||||
|
const storage = useStorage();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const [showKey, setShowKey] = useState(false);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
|
const {
|
||||||
|
register,
|
||||||
|
handleSubmit,
|
||||||
|
setError,
|
||||||
|
formState: { errors, isValid },
|
||||||
|
} = useForm();
|
||||||
|
|
||||||
|
const onSubmit = async (data: { nsec: string }) => {
|
||||||
|
try {
|
||||||
|
if (!data.nsec.startsWith("nsec1"))
|
||||||
|
return toast.error("You need to enter a private key start with nsec1");
|
||||||
|
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
const privkey = nip19.decode(data.nsec).data as string;
|
||||||
|
const pubkey = getPublicKey(privkey);
|
||||||
|
|
||||||
|
await storage.createAccount({
|
||||||
|
pubkey: pubkey,
|
||||||
|
privkey: privkey,
|
||||||
|
});
|
||||||
|
|
||||||
|
return navigate("/auth/onboarding");
|
||||||
|
} catch (e) {
|
||||||
|
setLoading(false);
|
||||||
|
setError("nsec", {
|
||||||
|
type: "manual",
|
||||||
|
message: String(e),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="relative flex items-center justify-center w-full h-full">
|
||||||
|
<div className="flex flex-col w-full max-w-md gap-8 mx-auto">
|
||||||
|
<div className="flex flex-col gap-1 text-center items-center">
|
||||||
|
<h1 className="text-2xl font-semibold">Enter your Private Key</h1>
|
||||||
|
<p className="text-lg font-medium leading-snug text-neutral-600 dark:text-neutral-500">
|
||||||
|
Lume will put your private key to{" "}
|
||||||
|
<span className="text-teal-500">
|
||||||
|
{storage.platform === "macos"
|
||||||
|
? "Apple Keychain"
|
||||||
|
: storage.platform === "windows"
|
||||||
|
? "Credential Manager"
|
||||||
|
: "Secret Service"}
|
||||||
|
</span>
|
||||||
|
.
|
||||||
|
<br />
|
||||||
|
It will be secured by your OS.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-6">
|
||||||
|
<form
|
||||||
|
onSubmit={handleSubmit(onSubmit)}
|
||||||
|
className="flex flex-col gap-4 mb-0"
|
||||||
|
>
|
||||||
|
<div className="relative flex flex-col gap-1">
|
||||||
|
<input
|
||||||
|
type={showKey ? "text" : "password"}
|
||||||
|
{...register("nsec", { required: false })}
|
||||||
|
spellCheck={false}
|
||||||
|
autoCapitalize="none"
|
||||||
|
autoCorrect="none"
|
||||||
|
placeholder="nsec1..."
|
||||||
|
className="pl-3 pr-11 text-xl border-transparent rounded-xl h-14 bg-neutral-950 placeholder:text-neutral-600 focus:border-blue-500 focus:ring focus:ring-blue-800"
|
||||||
|
/>
|
||||||
|
{errors.nsec && (
|
||||||
|
<p className="text-sm text-center text-red-600">
|
||||||
|
{errors.nsec.message as string}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setShowKey((state) => !state)}
|
||||||
|
className="absolute right-2 top-2 size-10 inline-flex items-center justify-center rounded-lg text-white bg-neutral-900 hover:bg-neutral-800"
|
||||||
|
>
|
||||||
|
{showKey ? (
|
||||||
|
<EyeOnIcon className="size-5" />
|
||||||
|
) : (
|
||||||
|
<EyeOffIcon className="size-5" />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={!isValid}
|
||||||
|
className="inline-flex items-center justify-center w-full text-lg h-12 font-medium text-white bg-blue-500 rounded-xl hover:bg-blue-600 disabled:opacity-50"
|
||||||
|
>
|
||||||
|
{loading ? (
|
||||||
|
<LoaderIcon className="size-5 animate-spin" />
|
||||||
|
) : (
|
||||||
|
"Continue"
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
110
apps/desktop/src/routes/auth/login-nsecbunker.tsx
Normal file
110
apps/desktop/src/routes/auth/login-nsecbunker.tsx
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
import { useArk, useStorage } from "@lume/ark";
|
||||||
|
import { LoaderIcon } from "@lume/icons";
|
||||||
|
import NDK, { NDKNip46Signer, NDKPrivateKeySigner } from "@nostr-dev-kit/ndk";
|
||||||
|
import { nip19 } from "nostr-tools";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
|
||||||
|
export function LoginWithNsecbunker() {
|
||||||
|
const ark = useArk();
|
||||||
|
const storage = useStorage();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
|
const {
|
||||||
|
register,
|
||||||
|
handleSubmit,
|
||||||
|
setError,
|
||||||
|
formState: { errors, isValid },
|
||||||
|
} = useForm();
|
||||||
|
|
||||||
|
const onSubmit = async (data: { npub: string }) => {
|
||||||
|
try {
|
||||||
|
if (!data.npub.startsWith("npub1"))
|
||||||
|
return toast.info("You need to enter a token start with npub1");
|
||||||
|
|
||||||
|
if (!data.npub.includes("#"))
|
||||||
|
return toast.info("Token must include #secret");
|
||||||
|
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
const bunker = new NDK({
|
||||||
|
explicitRelayUrls: [
|
||||||
|
"wss://relay.nsecbunker.com",
|
||||||
|
"wss://nostr.vulpem.com",
|
||||||
|
],
|
||||||
|
});
|
||||||
|
await bunker.connect(2000);
|
||||||
|
|
||||||
|
const pubkey = nip19.decode(data.npub.split("#")[0]).data as string;
|
||||||
|
const localSigner = NDKPrivateKeySigner.generate();
|
||||||
|
const remoteSigner = new NDKNip46Signer(bunker, data.npub, localSigner);
|
||||||
|
await remoteSigner.blockUntilReady();
|
||||||
|
|
||||||
|
ark.updateNostrSigner({ signer: remoteSigner });
|
||||||
|
|
||||||
|
await storage.createSetting("nsecbunker", "1");
|
||||||
|
await storage.createAccount({
|
||||||
|
pubkey,
|
||||||
|
privkey: localSigner.privateKey,
|
||||||
|
});
|
||||||
|
|
||||||
|
return navigate("/auth/onboarding");
|
||||||
|
} catch (e) {
|
||||||
|
setLoading(false);
|
||||||
|
setError("npub", {
|
||||||
|
type: "manual",
|
||||||
|
message: String(e),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="relative flex items-center justify-center w-full h-full">
|
||||||
|
<div className="flex flex-col w-full max-w-md gap-8 mx-auto">
|
||||||
|
<div className="flex flex-col gap-1 text-center items-center">
|
||||||
|
<h1 className="text-2xl font-semibold">
|
||||||
|
Enter your nsecbunker token
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-6">
|
||||||
|
<form
|
||||||
|
onSubmit={handleSubmit(onSubmit)}
|
||||||
|
className="flex flex-col gap-4 mb-0"
|
||||||
|
>
|
||||||
|
<div className="relative flex flex-col gap-1">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
{...register("npub", { required: false })}
|
||||||
|
spellCheck={false}
|
||||||
|
autoCapitalize="none"
|
||||||
|
autoCorrect="none"
|
||||||
|
placeholder="npub1...#..."
|
||||||
|
className="px-3 text-xl border-transparent rounded-xl h-14 bg-neutral-950 placeholder:text-neutral-600 focus:border-blue-500 focus:ring focus:ring-blue-800"
|
||||||
|
/>
|
||||||
|
{errors.npub && (
|
||||||
|
<p className="text-sm text-center text-red-600">
|
||||||
|
{errors.npub.message as string}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={!isValid}
|
||||||
|
className="inline-flex items-center justify-center w-full text-lg h-12 font-medium text-white bg-blue-500 rounded-xl hover:bg-blue-600 disabled:opacity-50"
|
||||||
|
>
|
||||||
|
{loading ? (
|
||||||
|
<LoaderIcon className="size-5 animate-spin" />
|
||||||
|
) : (
|
||||||
|
"Continue"
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
139
apps/desktop/src/routes/auth/login-oauth.tsx
Normal file
139
apps/desktop/src/routes/auth/login-oauth.tsx
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
import { useArk, useStorage } from "@lume/ark";
|
||||||
|
import { LoaderIcon } from "@lume/icons";
|
||||||
|
import { NIP05 } from "@lume/types";
|
||||||
|
import NDK, { NDKNip46Signer, NDKPrivateKeySigner } from "@nostr-dev-kit/ndk";
|
||||||
|
import { Window } from "@tauri-apps/api/window";
|
||||||
|
import { fetch } from "@tauri-apps/plugin-http";
|
||||||
|
import { nip19 } from "nostr-tools";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
|
||||||
|
const emailRegex = /^[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,4}$/;
|
||||||
|
|
||||||
|
export function LoginWithOAuth() {
|
||||||
|
const ark = useArk();
|
||||||
|
const storage = useStorage();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
|
const {
|
||||||
|
register,
|
||||||
|
handleSubmit,
|
||||||
|
setError,
|
||||||
|
formState: { errors, isValid },
|
||||||
|
} = useForm();
|
||||||
|
|
||||||
|
const onSubmit = async (data: { nip05: string }) => {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
const localPath = data.nip05.split("@")[0];
|
||||||
|
const service = data.nip05.split("@")[1];
|
||||||
|
|
||||||
|
const verifyURL = `https://${service}/.well-known/nostr.json?name=${localPath}`;
|
||||||
|
|
||||||
|
const req = await fetch(verifyURL, {
|
||||||
|
method: "GET",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json; charset=utf-8",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!req.ok)
|
||||||
|
return toast.error(
|
||||||
|
"Cannot verify your NIP-05 address, please try again later.",
|
||||||
|
);
|
||||||
|
|
||||||
|
const res: NIP05 = await req.json();
|
||||||
|
|
||||||
|
if (!res.names[localPath.toLowerCase()] || !res.names[localPath])
|
||||||
|
return toast.error(
|
||||||
|
"Cannot verify your NIP-05 address, please try again later.",
|
||||||
|
);
|
||||||
|
|
||||||
|
const pubkey =
|
||||||
|
(res.names[localPath] as string) ||
|
||||||
|
(res.names[localPath.toLowerCase()] as string);
|
||||||
|
|
||||||
|
if (!res.nip46[pubkey])
|
||||||
|
return toast.error("Cannot found NIP-46 with this address");
|
||||||
|
|
||||||
|
const nip46Relays = res.nip46[pubkey] as unknown as string[];
|
||||||
|
|
||||||
|
const bunker = new NDK({
|
||||||
|
explicitRelayUrls: nip46Relays || [
|
||||||
|
"wss://relay.nsecbunker.com",
|
||||||
|
"wss://nostr.vulpem.com",
|
||||||
|
],
|
||||||
|
});
|
||||||
|
await bunker.connect(2000);
|
||||||
|
|
||||||
|
const localSigner = NDKPrivateKeySigner.generate();
|
||||||
|
const remoteSigner = new NDKNip46Signer(bunker, pubkey, localSigner);
|
||||||
|
await remoteSigner.blockUntilReady();
|
||||||
|
|
||||||
|
ark.updateNostrSigner({ signer: remoteSigner });
|
||||||
|
|
||||||
|
await storage.createSetting("nsecbunker", "1");
|
||||||
|
await storage.createAccount({
|
||||||
|
pubkey,
|
||||||
|
privkey: localSigner.privateKey,
|
||||||
|
});
|
||||||
|
|
||||||
|
return navigate("/auth/onboarding");
|
||||||
|
} catch (e) {
|
||||||
|
setLoading(false);
|
||||||
|
setError("npub", {
|
||||||
|
type: "manual",
|
||||||
|
message: String(e),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="relative flex items-center justify-center w-full h-full">
|
||||||
|
<div className="flex flex-col w-full max-w-md gap-8 mx-auto">
|
||||||
|
<div className="flex flex-col gap-1 text-center items-center">
|
||||||
|
<h1 className="text-2xl font-semibold">Enter your NIP-05 address</h1>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-6">
|
||||||
|
<form
|
||||||
|
onSubmit={handleSubmit(onSubmit)}
|
||||||
|
className="flex flex-col gap-4 mb-0"
|
||||||
|
>
|
||||||
|
<div className="relative flex flex-col gap-1">
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
{...register("nip05", { required: false })}
|
||||||
|
spellCheck={false}
|
||||||
|
autoCapitalize="none"
|
||||||
|
autoCorrect="none"
|
||||||
|
placeholder="satoshi@nostr.me"
|
||||||
|
className="px-3 text-xl border-transparent rounded-xl h-14 bg-neutral-950 placeholder:text-neutral-600 focus:border-blue-500 focus:ring focus:ring-blue-800"
|
||||||
|
/>
|
||||||
|
{errors.nip05 && (
|
||||||
|
<p className="text-sm text-center text-red-600">
|
||||||
|
{errors.nip05.message as string}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={!isValid}
|
||||||
|
className="inline-flex items-center justify-center w-full text-lg h-12 font-medium text-white bg-blue-500 rounded-xl hover:bg-blue-600 disabled:opacity-50"
|
||||||
|
>
|
||||||
|
{loading ? (
|
||||||
|
<LoaderIcon className="size-5 animate-spin" />
|
||||||
|
) : (
|
||||||
|
"Continue"
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
55
apps/desktop/src/routes/auth/login.tsx
Normal file
55
apps/desktop/src/routes/auth/login.tsx
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
import { Link } from "react-router-dom";
|
||||||
|
|
||||||
|
export function LoginScreen() {
|
||||||
|
return (
|
||||||
|
<div className="relative flex items-center justify-center w-full h-full">
|
||||||
|
<div className="flex flex-col w-full max-w-md gap-8 mx-auto">
|
||||||
|
<div className="flex flex-col gap-1 text-center items-center">
|
||||||
|
<h1 className="text-2xl font-semibold">
|
||||||
|
Continue your experience on Nostr
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-6">
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<Link
|
||||||
|
to="/auth/login-oauth"
|
||||||
|
className="inline-flex items-center justify-center w-full h-12 text-lg font-medium text-white bg-blue-500 rounded-xl hover:bg-blue-600"
|
||||||
|
>
|
||||||
|
OAuth Login
|
||||||
|
</Link>
|
||||||
|
<Link
|
||||||
|
to="/auth/login-nsecbunker"
|
||||||
|
className="inline-flex items-center justify-center w-full h-12 text-lg font-medium text-neutral-50 rounded-xl bg-neutral-950 hover:bg-neutral-900"
|
||||||
|
>
|
||||||
|
Login with nsecbunker
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-6">
|
||||||
|
<div className="relative">
|
||||||
|
<div className="absolute inset-0 flex items-center">
|
||||||
|
<div className="w-full border-t border-neutral-900" />
|
||||||
|
</div>
|
||||||
|
<div className="relative flex justify-center">
|
||||||
|
<span className="px-2 font-medium bg-black text-neutral-600">
|
||||||
|
Or (Not recommend)
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Link
|
||||||
|
to="/auth/login-key"
|
||||||
|
className="mb-2 inline-flex items-center justify-center w-full h-12 text-lg font-medium text-neutral-50 rounded-xl bg-neutral-950 hover:bg-neutral-900"
|
||||||
|
>
|
||||||
|
Login with Private Key
|
||||||
|
</Link>
|
||||||
|
<p className="text-sm text-center text-neutral-500">
|
||||||
|
Lume will store your Private Key in{" "}
|
||||||
|
<span className="text-teal-600">OS Secure Storage</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@@ -1,7 +1,9 @@
|
|||||||
import { useStorage } from "@lume/ark";
|
import { useArk, useStorage } from "@lume/ark";
|
||||||
import { InfoIcon, LoaderIcon } from "@lume/icons";
|
import { InfoIcon, LoaderIcon } from "@lume/icons";
|
||||||
import { delay } from "@lume/utils";
|
import { FETCH_LIMIT } from "@lume/utils";
|
||||||
|
import { NDKKind } from "@nostr-dev-kit/ndk";
|
||||||
import * as Switch from "@radix-ui/react-switch";
|
import * as Switch from "@radix-ui/react-switch";
|
||||||
|
import { useQueryClient } from "@tanstack/react-query";
|
||||||
import {
|
import {
|
||||||
isPermissionGranted,
|
isPermissionGranted,
|
||||||
requestPermission,
|
requestPermission,
|
||||||
@@ -10,7 +12,9 @@ import { useEffect, useState } from "react";
|
|||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
export function OnboardingScreen() {
|
export function OnboardingScreen() {
|
||||||
|
const ark = useArk();
|
||||||
const storage = useStorage();
|
const storage = useStorage();
|
||||||
|
const queryClient = useQueryClient();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
@@ -33,7 +37,35 @@ export function OnboardingScreen() {
|
|||||||
|
|
||||||
const completeAuth = async () => {
|
const completeAuth = async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
await delay(1200);
|
|
||||||
|
// get account contacts
|
||||||
|
await ark.getUserContacts(storage.account.pubkey);
|
||||||
|
|
||||||
|
// refetch newsfeed
|
||||||
|
await queryClient.prefetchInfiniteQuery({
|
||||||
|
queryKey: ["timeline-9999"],
|
||||||
|
initialPageParam: 0,
|
||||||
|
queryFn: async ({
|
||||||
|
signal,
|
||||||
|
pageParam,
|
||||||
|
}: {
|
||||||
|
signal: AbortSignal;
|
||||||
|
pageParam: number;
|
||||||
|
}) => {
|
||||||
|
const events = await ark.getInfiniteEvents({
|
||||||
|
filter: {
|
||||||
|
kinds: [NDKKind.Text, NDKKind.Repost],
|
||||||
|
authors: storage.account.contacts,
|
||||||
|
},
|
||||||
|
limit: FETCH_LIMIT,
|
||||||
|
pageParam,
|
||||||
|
signal,
|
||||||
|
});
|
||||||
|
|
||||||
|
return events;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
navigate("/");
|
navigate("/");
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@@ -41,7 +41,7 @@ export function WelcomeScreen() {
|
|||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
<Link
|
<Link
|
||||||
to="/auth/import"
|
to="/auth/login"
|
||||||
className="inline-flex items-center justify-center w-full h-12 text-lg font-medium text-neutral-50 rounded-xl bg-neutral-950 hover:bg-neutral-900"
|
className="inline-flex items-center justify-center w-full h-12 text-lg font-medium text-neutral-50 rounded-xl bg-neutral-950 hover:bg-neutral-900"
|
||||||
>
|
>
|
||||||
Login
|
Login
|
||||||
|
@@ -9,7 +9,7 @@ export function ContactCard() {
|
|||||||
const { status, data } = useQuery({
|
const { status, data } = useQuery({
|
||||||
queryKey: ["contacts"],
|
queryKey: ["contacts"],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const contacts = await ark.getUserContacts({});
|
const contacts = await ark.getUserContacts();
|
||||||
return contacts;
|
return contacts;
|
||||||
},
|
},
|
||||||
refetchOnWindowFocus: false,
|
refetchOnWindowFocus: false,
|
||||||
|
@@ -8,7 +8,7 @@ export function EditContactScreen() {
|
|||||||
const { status, data } = useQuery({
|
const { status, data } = useQuery({
|
||||||
queryKey: ["contacts"],
|
queryKey: ["contacts"],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
return await ark.getUserContacts({});
|
return await ark.getUserContacts();
|
||||||
},
|
},
|
||||||
refetchOnWindowFocus: false,
|
refetchOnWindowFocus: false,
|
||||||
});
|
});
|
||||||
|
@@ -108,10 +108,11 @@ export class Ark {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getUserProfile({ pubkey }: { pubkey: string }) {
|
public async getUserProfile(pubkey?: string) {
|
||||||
try {
|
try {
|
||||||
// get clean pubkey without any special characters
|
// get clean pubkey without any special characters
|
||||||
let hexstring = pubkey.replace(/[^a-zA-Z0-9]/g, "").replace("nostr:", "");
|
let hexstring = pubkey.replace(/[^a-zA-Z0-9]/g, "").replace("nostr:", "");
|
||||||
|
const currentUserPubkey = this.#storage.account.pubkey;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
hexstring.startsWith("npub1") ||
|
hexstring.startsWith("npub1") ||
|
||||||
@@ -125,7 +126,9 @@ export class Ark {
|
|||||||
if (decoded.type === "naddr") hexstring = decoded.data.pubkey;
|
if (decoded.type === "naddr") hexstring = decoded.data.pubkey;
|
||||||
}
|
}
|
||||||
|
|
||||||
const user = this.ndk.getUser({ pubkey: hexstring });
|
const user = this.ndk.getUser({
|
||||||
|
pubkey: pubkey ? hexstring : currentUserPubkey,
|
||||||
|
});
|
||||||
|
|
||||||
const profile = await user.fetchProfile({
|
const profile = await user.fetchProfile({
|
||||||
cacheUsage: NDKSubscriptionCacheUsage.CACHE_FIRST,
|
cacheUsage: NDKSubscriptionCacheUsage.CACHE_FIRST,
|
||||||
@@ -138,23 +141,33 @@ export class Ark {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getUserContacts({
|
public async getUserContacts(pubkey?: string) {
|
||||||
pubkey = undefined,
|
|
||||||
outbox = undefined,
|
|
||||||
}: {
|
|
||||||
pubkey?: string;
|
|
||||||
outbox?: boolean;
|
|
||||||
}) {
|
|
||||||
try {
|
try {
|
||||||
const user = this.ndk.getUser({
|
// get clean pubkey without any special characters
|
||||||
pubkey: pubkey ? pubkey : this.#storage.account.pubkey,
|
let hexstring = pubkey.replace(/[^a-zA-Z0-9]/g, "").replace("nostr:", "");
|
||||||
});
|
const currentUserPubkey = this.#storage.account.pubkey;
|
||||||
const contacts = [...(await user.follows(undefined, outbox))].map(
|
|
||||||
(user) => user.pubkey,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (pubkey === this.#storage.account.pubkey)
|
if (
|
||||||
|
hexstring.startsWith("npub1") ||
|
||||||
|
hexstring.startsWith("nprofile1") ||
|
||||||
|
hexstring.startsWith("naddr1")
|
||||||
|
) {
|
||||||
|
const decoded = nip19.decode(hexstring);
|
||||||
|
|
||||||
|
if (decoded.type === "nprofile") hexstring = decoded.data.pubkey;
|
||||||
|
if (decoded.type === "npub") hexstring = decoded.data;
|
||||||
|
if (decoded.type === "naddr") hexstring = decoded.data.pubkey;
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = this.ndk.getUser({
|
||||||
|
pubkey: pubkey ? hexstring : currentUserPubkey,
|
||||||
|
});
|
||||||
|
|
||||||
|
const contacts = [...(await user.follows())].map((user) => user.pubkey);
|
||||||
|
|
||||||
|
if (!pubkey || pubkey === this.#storage.account.pubkey)
|
||||||
this.#storage.account.contacts = contacts;
|
this.#storage.account.contacts = contacts;
|
||||||
|
|
||||||
return contacts;
|
return contacts;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
throw new Error(e);
|
throw new Error(e);
|
||||||
@@ -507,7 +520,10 @@ export class Ark {
|
|||||||
signal,
|
signal,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!res.ok) throw new Error(`Failed to fetch NIP-05 service: ${nip05}`);
|
if (!res.ok) {
|
||||||
|
console.log(res);
|
||||||
|
throw new Error(`Failed to fetch NIP-05 service: ${nip05}`);
|
||||||
|
}
|
||||||
|
|
||||||
const data: NIP05 = await res.json();
|
const data: NIP05 = await res.json();
|
||||||
|
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { useArk } from '../provider';
|
import { useArk } from "../provider";
|
||||||
|
|
||||||
export function useProfile(pubkey: string) {
|
export function useProfile(pubkey: string) {
|
||||||
const ark = useArk();
|
const ark = useArk();
|
||||||
@@ -8,12 +8,12 @@ export function useProfile(pubkey: string) {
|
|||||||
isError,
|
isError,
|
||||||
data: user,
|
data: user,
|
||||||
} = useQuery({
|
} = useQuery({
|
||||||
queryKey: ['user', pubkey],
|
queryKey: ["user", pubkey],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const profile = await ark.getUserProfile({ pubkey });
|
const profile = await ark.getUserProfile(pubkey);
|
||||||
if (!profile)
|
if (!profile)
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Cannot get metadata for ${pubkey}, will be retry after 10 seconds`
|
`Cannot get metadata for ${pubkey}, will be retry after 10 seconds`,
|
||||||
);
|
);
|
||||||
return profile;
|
return profile;
|
||||||
},
|
},
|
||||||
|
@@ -366,6 +366,7 @@ export class LumeStorage {
|
|||||||
const currentSetting = await this.checkSettingValue(key);
|
const currentSetting = await this.checkSettingValue(key);
|
||||||
|
|
||||||
if (!currentSetting) {
|
if (!currentSetting) {
|
||||||
|
this.settings[key] === !!parseInt(value);
|
||||||
return await this.#db.execute(
|
return await this.#db.execute(
|
||||||
"INSERT OR IGNORE INTO settings (key, value) VALUES ($1, $2);",
|
"INSERT OR IGNORE INTO settings (key, value) VALUES ($1, $2);",
|
||||||
[key, value],
|
[key, value],
|
||||||
|
5
packages/types/index.d.ts
vendored
5
packages/types/index.d.ts
vendored
@@ -159,4 +159,9 @@ export interface NIP05 {
|
|||||||
names: {
|
names: {
|
||||||
[key: string]: string;
|
[key: string]: string;
|
||||||
};
|
};
|
||||||
|
nip46: {
|
||||||
|
[key: string]: {
|
||||||
|
[key: string]: string[];
|
||||||
|
};
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
@@ -32,9 +32,7 @@ export function OnboardingProfileSettingsScreen() {
|
|||||||
navigate("/follow");
|
navigate("/follow");
|
||||||
}
|
}
|
||||||
|
|
||||||
const oldProfile = await ark.getUserProfile({
|
const oldProfile = await ark.getUserProfile();
|
||||||
pubkey: storage.account.pubkey,
|
|
||||||
});
|
|
||||||
|
|
||||||
const profile: NDKUserProfile = {
|
const profile: NDKUserProfile = {
|
||||||
...data,
|
...data,
|
||||||
@@ -55,9 +53,6 @@ export function OnboardingProfileSettingsScreen() {
|
|||||||
if (publish) {
|
if (publish) {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
navigate("/follow");
|
navigate("/follow");
|
||||||
} else {
|
|
||||||
toast.error("Cannot publish your profile, please try again later.");
|
|
||||||
setLoading(false);
|
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
|
Reference in New Issue
Block a user