wip: improve onboarding
This commit is contained in:
@@ -207,9 +207,6 @@ export default function Router() {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "create",
|
path: "create",
|
||||||
loader: async () => {
|
|
||||||
return await ark.getOAuthServices();
|
|
||||||
},
|
|
||||||
async lazy() {
|
async lazy() {
|
||||||
const { CreateAccountScreen } = await import(
|
const { CreateAccountScreen } = await import(
|
||||||
"./routes/auth/create"
|
"./routes/auth/create"
|
||||||
@@ -217,6 +214,27 @@ export default function Router() {
|
|||||||
return { Component: CreateAccountScreen };
|
return { Component: CreateAccountScreen };
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "create-keys",
|
||||||
|
async lazy() {
|
||||||
|
const { CreateAccountKeys } = await import(
|
||||||
|
"./routes/auth/create-keys"
|
||||||
|
);
|
||||||
|
return { Component: CreateAccountKeys };
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "create-address",
|
||||||
|
loader: async () => {
|
||||||
|
return await ark.getOAuthServices();
|
||||||
|
},
|
||||||
|
async lazy() {
|
||||||
|
const { CreateAccountAddress } = await import(
|
||||||
|
"./routes/auth/create-address"
|
||||||
|
);
|
||||||
|
return { Component: CreateAccountAddress };
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: "login",
|
path: "login",
|
||||||
async lazy() {
|
async lazy() {
|
||||||
|
|||||||
261
apps/desktop/src/routes/auth/create-address.tsx
Normal file
261
apps/desktop/src/routes/auth/create-address.tsx
Normal file
@@ -0,0 +1,261 @@
|
|||||||
|
import { useArk } from "@lume/ark";
|
||||||
|
import { CheckIcon, ChevronDownIcon, LoaderIcon } from "@lume/icons";
|
||||||
|
import { useStorage } from "@lume/storage";
|
||||||
|
import { onboardingAtom } from "@lume/utils";
|
||||||
|
import NDK, {
|
||||||
|
NDKEvent,
|
||||||
|
NDKKind,
|
||||||
|
NDKNip46Signer,
|
||||||
|
NDKPrivateKeySigner,
|
||||||
|
} from "@nostr-dev-kit/ndk";
|
||||||
|
import * as Select from "@radix-ui/react-select";
|
||||||
|
import { UnlistenFn } from "@tauri-apps/api/event";
|
||||||
|
import { Window } from "@tauri-apps/api/window";
|
||||||
|
import { useSetAtom } from "jotai";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
import { useLoaderData, useNavigate } from "react-router-dom";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
|
||||||
|
const Item = ({ event }: { event: NDKEvent }) => {
|
||||||
|
const domain = JSON.parse(event.content).nip05.replace("_@", "");
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Select.Item
|
||||||
|
value={event.id}
|
||||||
|
className="relative flex items-center pr-10 leading-none rounded-md select-none text-neutral-100 rounded-mg h-9 pl-7"
|
||||||
|
>
|
||||||
|
<Select.ItemText>@{domain}</Select.ItemText>
|
||||||
|
<Select.ItemIndicator className="absolute left-0 inline-flex items-center justify-center transform h-7">
|
||||||
|
<CheckIcon className="size-4" />
|
||||||
|
</Select.ItemIndicator>
|
||||||
|
</Select.Item>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export function CreateAccountAddress() {
|
||||||
|
const ark = useArk();
|
||||||
|
const storage = useStorage();
|
||||||
|
const services = useLoaderData() as NDKEvent[];
|
||||||
|
const setOnboarding = useSetAtom(onboardingAtom);
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const [serviceId, setServiceId] = useState(services?.[0]?.id);
|
||||||
|
const [loading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
|
const {
|
||||||
|
register,
|
||||||
|
handleSubmit,
|
||||||
|
formState: { isValid },
|
||||||
|
} = useForm();
|
||||||
|
|
||||||
|
const getDomainName = (id: string) => {
|
||||||
|
const event = services.find((ev) => ev.id === id);
|
||||||
|
return JSON.parse(event.content).nip05.replace("_@", "") as string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const onSubmit = async (data: { username: string; email: string }) => {
|
||||||
|
try {
|
||||||
|
setIsLoading(true);
|
||||||
|
|
||||||
|
const domain = getDomainName(serviceId);
|
||||||
|
const service = services.find((ev) => ev.id === serviceId);
|
||||||
|
|
||||||
|
// generate ndk for nsecbunker
|
||||||
|
const localSigner = NDKPrivateKeySigner.generate();
|
||||||
|
const bunker = new NDK({
|
||||||
|
explicitRelayUrls: [
|
||||||
|
"wss://relay.nsecbunker.com/",
|
||||||
|
"wss://nostr.vulpem.com/",
|
||||||
|
],
|
||||||
|
});
|
||||||
|
await bunker.connect(2000);
|
||||||
|
|
||||||
|
// generate tmp remote singer for create account
|
||||||
|
const remoteSigner = new NDKNip46Signer(
|
||||||
|
bunker,
|
||||||
|
service.pubkey,
|
||||||
|
localSigner,
|
||||||
|
);
|
||||||
|
|
||||||
|
// handle auth url request
|
||||||
|
let unlisten: UnlistenFn;
|
||||||
|
let authWindow: Window;
|
||||||
|
let account: string = undefined;
|
||||||
|
|
||||||
|
remoteSigner.addListener("authUrl", async (authUrl: string) => {
|
||||||
|
authWindow = new Window(`auth-${serviceId}`, {
|
||||||
|
url: authUrl,
|
||||||
|
title: domain,
|
||||||
|
titleBarStyle: "overlay",
|
||||||
|
width: 600,
|
||||||
|
height: 650,
|
||||||
|
center: true,
|
||||||
|
closable: false,
|
||||||
|
});
|
||||||
|
unlisten = await authWindow.onCloseRequested(() => {
|
||||||
|
if (!account) {
|
||||||
|
setIsLoading(false);
|
||||||
|
unlisten();
|
||||||
|
|
||||||
|
return authWindow.close();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// create new account
|
||||||
|
account = await remoteSigner.createAccount(
|
||||||
|
data.username,
|
||||||
|
domain,
|
||||||
|
data.email,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!account) {
|
||||||
|
unlisten();
|
||||||
|
setIsLoading(false);
|
||||||
|
|
||||||
|
authWindow.close();
|
||||||
|
|
||||||
|
return toast.error("Failed to create new account, try again later");
|
||||||
|
}
|
||||||
|
|
||||||
|
unlisten();
|
||||||
|
authWindow.close();
|
||||||
|
|
||||||
|
// add account to storage
|
||||||
|
await storage.createSetting("nsecbunker", "1");
|
||||||
|
const dbAccount = await storage.createAccount({
|
||||||
|
pubkey: account,
|
||||||
|
privkey: localSigner.privateKey,
|
||||||
|
});
|
||||||
|
ark.account = dbAccount;
|
||||||
|
|
||||||
|
// 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 });
|
||||||
|
|
||||||
|
// remove default nsecbunker profile and contact list
|
||||||
|
// await ark.createEvent({ kind: NDKKind.Metadata, content: "", tags: [] });
|
||||||
|
await ark.createEvent({ kind: NDKKind.Contacts, content: "", tags: [] });
|
||||||
|
|
||||||
|
setIsLoading(false);
|
||||||
|
setOnboarding({ open: true, newUser: true });
|
||||||
|
|
||||||
|
return navigate("/auth/onboarding", { replace: true });
|
||||||
|
} catch (e) {
|
||||||
|
setIsLoading(false);
|
||||||
|
toast.error(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-16 mx-auto">
|
||||||
|
<div className="flex flex-col gap-1 text-center items-center">
|
||||||
|
<h1 className="text-2xl font-semibold">Create Account</h1>
|
||||||
|
</div>
|
||||||
|
{!services ? (
|
||||||
|
<div className="flex items-center justify-center w-full">
|
||||||
|
<LoaderIcon className="size-5 animate-spin" />
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<form
|
||||||
|
onSubmit={handleSubmit(onSubmit)}
|
||||||
|
className="flex flex-col gap-3 mb-0"
|
||||||
|
>
|
||||||
|
<div className="flex flex-col gap-6 p-5 bg-neutral-950 rounded-2xl">
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<label
|
||||||
|
htmlFor="username"
|
||||||
|
className="text-sm font-semibold uppercase text-neutral-600"
|
||||||
|
>
|
||||||
|
Username *
|
||||||
|
</label>
|
||||||
|
<div className="flex flex-col gap-1.5">
|
||||||
|
<div className="flex items-center justify-between w-full gap-2 bg-neutral-900 rounded-xl">
|
||||||
|
<input
|
||||||
|
type={"text"}
|
||||||
|
{...register("username", {
|
||||||
|
required: true,
|
||||||
|
minLength: 1,
|
||||||
|
})}
|
||||||
|
spellCheck={false}
|
||||||
|
autoComplete="off"
|
||||||
|
autoCorrect="off"
|
||||||
|
autoCapitalize="off"
|
||||||
|
placeholder="alice"
|
||||||
|
className="flex-1 min-w-0 text-xl bg-transparent border-transparent outline-none focus:outline-none focus:ring-0 focus:border-none h-14 ring-0 placeholder:text-neutral-600"
|
||||||
|
/>
|
||||||
|
<Select.Root value={serviceId} onValueChange={setServiceId}>
|
||||||
|
<Select.Trigger className="inline-flex items-center justify-end gap-2 pr-3 text-xl font-semibold text-blue-500 w-max shrink-0">
|
||||||
|
<Select.Value>@{getDomainName(serviceId)}</Select.Value>
|
||||||
|
<Select.Icon>
|
||||||
|
<ChevronDownIcon className="size-5" />
|
||||||
|
</Select.Icon>
|
||||||
|
</Select.Trigger>
|
||||||
|
<Select.Portal>
|
||||||
|
<Select.Content className="rounded-lg border border-white/20 bg-white/10 backdrop-blur-xl">
|
||||||
|
<Select.Viewport className="p-3">
|
||||||
|
<Select.Group>
|
||||||
|
<Select.Label className="mb-2 text-sm font-medium uppercase px-7 text-neutral-600">
|
||||||
|
Choose a Provider
|
||||||
|
</Select.Label>
|
||||||
|
{services.map((service) => (
|
||||||
|
<Item key={service.id} event={service} />
|
||||||
|
))}
|
||||||
|
</Select.Group>
|
||||||
|
</Select.Viewport>
|
||||||
|
</Select.Content>
|
||||||
|
</Select.Portal>
|
||||||
|
</Select.Root>
|
||||||
|
</div>
|
||||||
|
<span className="text-sm text-neutral-600">
|
||||||
|
Use to login to Lume and other Nostr apps. You can choose
|
||||||
|
provider you trust to manage your account
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-1.5">
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<label
|
||||||
|
htmlFor="email"
|
||||||
|
className="text-sm font-semibold uppercase text-neutral-600"
|
||||||
|
>
|
||||||
|
Backup Email (optional)
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type={"email"}
|
||||||
|
{...register("email", { required: false })}
|
||||||
|
spellCheck={false}
|
||||||
|
autoCapitalize="none"
|
||||||
|
autoCorrect="none"
|
||||||
|
className="px-3 text-xl border-transparent rounded-xl h-14 bg-neutral-900 placeholder:text-neutral-600 focus:border-blue-500 focus:ring focus:ring-blue-800"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<span className="text-sm text-neutral-600">
|
||||||
|
Use for recover your account if you lose your password
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
91
apps/desktop/src/routes/auth/create-keys.tsx
Normal file
91
apps/desktop/src/routes/auth/create-keys.tsx
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
import { useArk } from "@lume/ark";
|
||||||
|
import { useStorage } from "@lume/storage";
|
||||||
|
import { onboardingAtom } from "@lume/utils";
|
||||||
|
import { NDKPrivateKeySigner } from "@nostr-dev-kit/ndk";
|
||||||
|
import { desktopDir } from "@tauri-apps/api/path";
|
||||||
|
import { save } from "@tauri-apps/plugin-dialog";
|
||||||
|
import { writeTextFile } from "@tauri-apps/plugin-fs";
|
||||||
|
import { useSetAtom } from "jotai";
|
||||||
|
import { nanoid } from "nanoid";
|
||||||
|
import { getPublicKey, nip19 } from "nostr-tools";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
|
||||||
|
export function CreateAccountKeys() {
|
||||||
|
const ark = useArk();
|
||||||
|
const storage = useStorage();
|
||||||
|
const setOnboarding = useSetAtom(onboardingAtom);
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const generateNostrKeys = async () => {
|
||||||
|
const signer = NDKPrivateKeySigner.generate();
|
||||||
|
const pubkey = getPublicKey(signer.privateKey);
|
||||||
|
|
||||||
|
const npub = nip19.npubEncode(pubkey);
|
||||||
|
const nsec = nip19.nsecEncode(signer.privateKey);
|
||||||
|
|
||||||
|
ark.updateNostrSigner({ signer });
|
||||||
|
|
||||||
|
const downloadPath = await desktopDir();
|
||||||
|
const fileName = `nostr_keys_${nanoid(4)}.txt`;
|
||||||
|
const filePath = await save({
|
||||||
|
defaultPath: `${downloadPath}/${fileName}`,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!filePath) {
|
||||||
|
return toast.info("You need to save account keys before continue.");
|
||||||
|
}
|
||||||
|
|
||||||
|
await writeTextFile(
|
||||||
|
filePath,
|
||||||
|
`Nostr Account\nGenerated by Lume (lume.nu)\n---\nPublic key: ${npub}\nPrivate key: ${nsec}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
await storage.createAccount({
|
||||||
|
pubkey: pubkey,
|
||||||
|
privkey: signer.privateKey,
|
||||||
|
});
|
||||||
|
|
||||||
|
setOnboarding({ open: true, newUser: true });
|
||||||
|
|
||||||
|
return navigate("/auth/onboarding", { replace: true });
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="relative flex items-center justify-center w-full h-full">
|
||||||
|
<div className="flex flex-col w-full max-w-md gap-16 mx-auto">
|
||||||
|
<div className="flex flex-col gap-1 text-center items-center">
|
||||||
|
<h1 className="text-2xl font-semibold">Create Account</h1>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-3 mb-0">
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<label
|
||||||
|
htmlFor="npub"
|
||||||
|
className="text-sm font-semibold uppercase text-neutral-600"
|
||||||
|
>
|
||||||
|
Public Key
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
readOnly
|
||||||
|
type="text"
|
||||||
|
className="px-3 text-xl border-transparent rounded-xl h-14 bg-neutral-900 placeholder:text-neutral-600 focus:border-blue-500 focus:ring focus:ring-blue-800"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<label
|
||||||
|
htmlFor="nsec"
|
||||||
|
className="text-sm font-semibold uppercase text-neutral-600"
|
||||||
|
>
|
||||||
|
Private Key
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
readOnly
|
||||||
|
type="text"
|
||||||
|
className="px-3 text-xl border-transparent rounded-xl h-14 bg-neutral-900 placeholder:text-neutral-600 focus:border-blue-500 focus:ring focus:ring-blue-800"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,342 +1,74 @@
|
|||||||
import { useArk } from "@lume/ark";
|
import { LoaderIcon } from "@lume/icons";
|
||||||
import { CheckIcon, ChevronDownIcon, LoaderIcon } from "@lume/icons";
|
import { cn } from "@lume/utils";
|
||||||
import { useStorage } from "@lume/storage";
|
|
||||||
import { onboardingAtom } from "@lume/utils";
|
|
||||||
import NDK, {
|
|
||||||
NDKEvent,
|
|
||||||
NDKKind,
|
|
||||||
NDKNip46Signer,
|
|
||||||
NDKPrivateKeySigner,
|
|
||||||
} from "@nostr-dev-kit/ndk";
|
|
||||||
import * as Select from "@radix-ui/react-select";
|
|
||||||
import { UnlistenFn } from "@tauri-apps/api/event";
|
|
||||||
import { desktopDir } from "@tauri-apps/api/path";
|
|
||||||
import { Window } from "@tauri-apps/api/window";
|
|
||||||
import { save } from "@tauri-apps/plugin-dialog";
|
|
||||||
import { writeTextFile } from "@tauri-apps/plugin-fs";
|
|
||||||
import { useSetAtom } from "jotai";
|
|
||||||
import { nanoid } from "nanoid";
|
|
||||||
import { getPublicKey, nip19 } from "nostr-tools";
|
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { Link, useNavigate } from "react-router-dom";
|
||||||
import { useLoaderData, useNavigate } from "react-router-dom";
|
|
||||||
import { toast } from "sonner";
|
|
||||||
|
|
||||||
const Item = ({ event }: { event: NDKEvent }) => {
|
|
||||||
const domain = JSON.parse(event.content).nip05.replace("_@", "");
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Select.Item
|
|
||||||
value={event.id}
|
|
||||||
className="relative flex items-center pr-10 leading-none rounded-md select-none text-neutral-100 rounded-mg h-9 pl-7"
|
|
||||||
>
|
|
||||||
<Select.ItemText>@{domain}</Select.ItemText>
|
|
||||||
<Select.ItemIndicator className="absolute left-0 inline-flex items-center justify-center transform h-7">
|
|
||||||
<CheckIcon className="size-4" />
|
|
||||||
</Select.ItemIndicator>
|
|
||||||
</Select.Item>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export function CreateAccountScreen() {
|
export function CreateAccountScreen() {
|
||||||
const ark = useArk();
|
|
||||||
const storage = useStorage();
|
|
||||||
const services = useLoaderData() as NDKEvent[];
|
|
||||||
const setOnboarding = useSetAtom(onboardingAtom);
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const [serviceId, setServiceId] = useState(services?.[0]?.id);
|
const [method, setMethod] = useState<"self" | "managed">("self");
|
||||||
const [loading, setIsLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
const {
|
const next = () => {
|
||||||
register,
|
setLoading(true);
|
||||||
handleSubmit,
|
|
||||||
formState: { isValid },
|
|
||||||
} = useForm();
|
|
||||||
|
|
||||||
const getDomainName = (id: string) => {
|
if (method === "self") {
|
||||||
const event = services.find((ev) => ev.id === id);
|
navigate("/auth/create-keys");
|
||||||
return JSON.parse(event.content).nip05.replace("_@", "") as string;
|
} else {
|
||||||
};
|
navigate("/auth/create-address");
|
||||||
|
|
||||||
const generateNostrKeys = async () => {
|
|
||||||
const signer = NDKPrivateKeySigner.generate();
|
|
||||||
const pubkey = getPublicKey(signer.privateKey);
|
|
||||||
|
|
||||||
const npub = nip19.npubEncode(pubkey);
|
|
||||||
const nsec = nip19.nsecEncode(signer.privateKey);
|
|
||||||
|
|
||||||
ark.updateNostrSigner({ signer });
|
|
||||||
|
|
||||||
const downloadPath = await desktopDir();
|
|
||||||
const fileName = `nostr_keys_${nanoid(4)}.txt`;
|
|
||||||
const filePath = await save({
|
|
||||||
defaultPath: `${downloadPath}/${fileName}`,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!filePath) {
|
|
||||||
return toast.info("You need to save account keys before continue.");
|
|
||||||
}
|
|
||||||
|
|
||||||
await writeTextFile(
|
|
||||||
filePath,
|
|
||||||
`Nostr Account\nGenerated by Lume (lume.nu)\n---\nPublic key: ${npub}\nPrivate key: ${nsec}`,
|
|
||||||
);
|
|
||||||
|
|
||||||
await storage.createAccount({
|
|
||||||
pubkey: pubkey,
|
|
||||||
privkey: signer.privateKey,
|
|
||||||
});
|
|
||||||
|
|
||||||
setOnboarding({ open: true, newUser: true });
|
|
||||||
|
|
||||||
return navigate("/auth/onboarding", { replace: true });
|
|
||||||
};
|
|
||||||
|
|
||||||
const onSubmit = async (data: { username: string; email: string }) => {
|
|
||||||
try {
|
|
||||||
setIsLoading(true);
|
|
||||||
|
|
||||||
const domain = getDomainName(serviceId);
|
|
||||||
const service = services.find((ev) => ev.id === serviceId);
|
|
||||||
|
|
||||||
// generate ndk for nsecbunker
|
|
||||||
const localSigner = NDKPrivateKeySigner.generate();
|
|
||||||
const bunker = new NDK({
|
|
||||||
explicitRelayUrls: [
|
|
||||||
"wss://relay.nsecbunker.com/",
|
|
||||||
"wss://nostr.vulpem.com/",
|
|
||||||
],
|
|
||||||
});
|
|
||||||
await bunker.connect(2000);
|
|
||||||
|
|
||||||
// generate tmp remote singer for create account
|
|
||||||
const remoteSigner = new NDKNip46Signer(
|
|
||||||
bunker,
|
|
||||||
service.pubkey,
|
|
||||||
localSigner,
|
|
||||||
);
|
|
||||||
|
|
||||||
// handle auth url request
|
|
||||||
let unlisten: UnlistenFn;
|
|
||||||
let authWindow: Window;
|
|
||||||
let account: string = undefined;
|
|
||||||
|
|
||||||
remoteSigner.addListener("authUrl", async (authUrl: string) => {
|
|
||||||
authWindow = new Window(`auth-${serviceId}`, {
|
|
||||||
url: authUrl,
|
|
||||||
title: domain,
|
|
||||||
titleBarStyle: "overlay",
|
|
||||||
width: 600,
|
|
||||||
height: 650,
|
|
||||||
center: true,
|
|
||||||
closable: false,
|
|
||||||
});
|
|
||||||
unlisten = await authWindow.onCloseRequested(() => {
|
|
||||||
if (!account) {
|
|
||||||
setIsLoading(false);
|
|
||||||
unlisten();
|
|
||||||
|
|
||||||
return authWindow.close();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// create new account
|
|
||||||
account = await remoteSigner.createAccount(
|
|
||||||
data.username,
|
|
||||||
domain,
|
|
||||||
data.email,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!account) {
|
|
||||||
unlisten();
|
|
||||||
setIsLoading(false);
|
|
||||||
|
|
||||||
authWindow.close();
|
|
||||||
|
|
||||||
return toast.error("Failed to create new account, try again later");
|
|
||||||
}
|
|
||||||
|
|
||||||
unlisten();
|
|
||||||
authWindow.close();
|
|
||||||
|
|
||||||
// add account to storage
|
|
||||||
await storage.createSetting("nsecbunker", "1");
|
|
||||||
const dbAccount = await storage.createAccount({
|
|
||||||
pubkey: account,
|
|
||||||
privkey: localSigner.privateKey,
|
|
||||||
});
|
|
||||||
ark.account = dbAccount;
|
|
||||||
|
|
||||||
// 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 });
|
|
||||||
|
|
||||||
// remove default nsecbunker profile and contact list
|
|
||||||
// await ark.createEvent({ kind: NDKKind.Metadata, content: "", tags: [] });
|
|
||||||
await ark.createEvent({ kind: NDKKind.Contacts, content: "", tags: [] });
|
|
||||||
|
|
||||||
setIsLoading(false);
|
|
||||||
setOnboarding({ open: true, newUser: true });
|
|
||||||
|
|
||||||
return navigate("/auth/onboarding", { replace: true });
|
|
||||||
} catch (e) {
|
|
||||||
setIsLoading(false);
|
|
||||||
toast.error(String(e));
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative flex items-center justify-center w-full h-full">
|
<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 w-full max-w-md gap-16 mx-auto">
|
||||||
<div className="flex flex-col gap-1 text-center items-center">
|
<div className="flex flex-col gap-1 text-center items-center">
|
||||||
<h1 className="text-2xl font-semibold">
|
<h1 className="text-2xl font-semibold">
|
||||||
Let's get you set up on Nostr.
|
Let's get you set up on Nostr.
|
||||||
</h1>
|
</h1>
|
||||||
|
<p className="text-lg font-medium leading-snug text-neutral-600 dark:text-neutral-500">
|
||||||
|
Choose one of methods below to create your account
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
{!services ? (
|
<div className="flex flex-col gap-4">
|
||||||
<div className="flex items-center justify-center w-full">
|
|
||||||
<LoaderIcon className="size-5 animate-spin" />
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="flex flex-col gap-6">
|
|
||||||
<form
|
|
||||||
onSubmit={handleSubmit(onSubmit)}
|
|
||||||
className="flex flex-col gap-3 mb-0"
|
|
||||||
>
|
|
||||||
<div className="flex flex-col gap-6 p-5 bg-neutral-950 rounded-2xl">
|
|
||||||
<div className="flex flex-col gap-2">
|
|
||||||
<label
|
|
||||||
htmlFor="username"
|
|
||||||
className="text-sm font-semibold uppercase text-neutral-600"
|
|
||||||
>
|
|
||||||
Username *
|
|
||||||
</label>
|
|
||||||
<div className="flex flex-col gap-1.5">
|
|
||||||
<div className="flex items-center justify-between w-full gap-2 bg-neutral-900 rounded-xl">
|
|
||||||
<input
|
|
||||||
type={"text"}
|
|
||||||
{...register("username", {
|
|
||||||
required: true,
|
|
||||||
minLength: 1,
|
|
||||||
})}
|
|
||||||
spellCheck={false}
|
|
||||||
autoComplete="off"
|
|
||||||
autoCorrect="off"
|
|
||||||
autoCapitalize="off"
|
|
||||||
placeholder="alice"
|
|
||||||
className="flex-1 min-w-0 text-xl bg-transparent border-transparent outline-none focus:outline-none focus:ring-0 focus:border-none h-14 ring-0 placeholder:text-neutral-600"
|
|
||||||
/>
|
|
||||||
<Select.Root
|
|
||||||
value={serviceId}
|
|
||||||
onValueChange={setServiceId}
|
|
||||||
>
|
|
||||||
<Select.Trigger className="inline-flex items-center justify-end gap-2 pr-3 text-xl font-semibold text-blue-500 w-max shrink-0">
|
|
||||||
<Select.Value>
|
|
||||||
@{getDomainName(serviceId)}
|
|
||||||
</Select.Value>
|
|
||||||
<Select.Icon>
|
|
||||||
<ChevronDownIcon className="size-5" />
|
|
||||||
</Select.Icon>
|
|
||||||
</Select.Trigger>
|
|
||||||
<Select.Portal>
|
|
||||||
<Select.Content className="rounded-lg border border-white/20 bg-white/10 backdrop-blur-xl">
|
|
||||||
<Select.Viewport className="p-3">
|
|
||||||
<Select.Group>
|
|
||||||
<Select.Label className="mb-2 text-sm font-medium uppercase px-7 text-neutral-600">
|
|
||||||
Choose a Provider
|
|
||||||
</Select.Label>
|
|
||||||
{services.map((service) => (
|
|
||||||
<Item key={service.id} event={service} />
|
|
||||||
))}
|
|
||||||
</Select.Group>
|
|
||||||
</Select.Viewport>
|
|
||||||
</Select.Content>
|
|
||||||
</Select.Portal>
|
|
||||||
</Select.Root>
|
|
||||||
</div>
|
|
||||||
<span className="text-sm text-neutral-600">
|
|
||||||
Use to login to Lume and other Nostr apps. You can choose
|
|
||||||
provider you trust to manage your account
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col gap-1.5">
|
|
||||||
<div className="flex flex-col gap-2">
|
|
||||||
<label
|
|
||||||
htmlFor="email"
|
|
||||||
className="text-sm font-semibold uppercase text-neutral-600"
|
|
||||||
>
|
|
||||||
Backup Email (optional)
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type={"email"}
|
|
||||||
{...register("email", { required: false })}
|
|
||||||
spellCheck={false}
|
|
||||||
autoCapitalize="none"
|
|
||||||
autoCorrect="none"
|
|
||||||
className="px-3 text-xl border-transparent rounded-xl h-14 bg-neutral-900 placeholder:text-neutral-600 focus:border-blue-500 focus:ring focus:ring-blue-800"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<span className="text-sm text-neutral-600">
|
|
||||||
Use for recover your account if you lose your password
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="button"
|
||||||
disabled={!isValid}
|
onClick={() => setMethod("managed")}
|
||||||
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"
|
className={cn(
|
||||||
|
"flex flex-col items-start px-4 py-3.5 bg-neutral-900 rounded-xl hover:bg-neutral-800",
|
||||||
|
method === "managed" ? "ring-1 ring-teal-500" : "",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<p className="font-semibold">Managed by Provider</p>
|
||||||
|
<p className="text-sm font-medium text-neutral-500">
|
||||||
|
A 3rd party provider will handle your sign in keys for you.
|
||||||
|
</p>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setMethod("self")}
|
||||||
|
className={cn(
|
||||||
|
"flex flex-col items-start px-4 py-3.5 bg-neutral-900 rounded-xl hover:bg-neutral-800",
|
||||||
|
method === "self" ? "ring-1 ring-teal-500" : "",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<p className="font-semibold">Self-Managed</p>
|
||||||
|
<p className="text-sm font-medium text-neutral-500">
|
||||||
|
You create your keys and keep them safe.
|
||||||
|
</p>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={next}
|
||||||
|
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"
|
||||||
>
|
>
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<LoaderIcon className="size-5 animate-spin" />
|
<LoaderIcon className="size-5 animate-spin" />
|
||||||
) : (
|
) : (
|
||||||
"Create Account"
|
"Continue"
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
|
||||||
<div className="flex flex-col gap-6">
|
|
||||||
<div className="flex flex-col">
|
|
||||||
<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-500">
|
|
||||||
Or manage your own keys
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<span className="mx-auto text-xs font-medium bg-black text-neutral-600">
|
|
||||||
Mostly compatible with other Nostr clients
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={generateNostrKeys}
|
|
||||||
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"
|
|
||||||
>
|
|
||||||
Generate Nostr Keys
|
|
||||||
</button>
|
|
||||||
<p className="text-sm text-center text-neutral-500">
|
|
||||||
If you are using this option, please make sure to store your
|
|
||||||
keys safely. You{" "}
|
|
||||||
<span className="text-red-600">cannot recover</span> them if
|
|
||||||
they're lost, and will be{" "}
|
|
||||||
<span className="text-red-600">unable</span> to access your
|
|
||||||
account.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ export function LoginWithKey() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative flex items-center justify-center w-full h-full">
|
<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 w-full max-w-md gap-16 mx-auto">
|
||||||
<div className="flex flex-col gap-1 text-center items-center">
|
<div className="flex flex-col gap-1 text-center items-center">
|
||||||
<h1 className="text-2xl font-semibold">Enter your Private Key</h1>
|
<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">
|
<p className="text-lg font-medium leading-snug text-neutral-600 dark:text-neutral-500">
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ export function LoginWithNsecbunker() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative flex items-center justify-center w-full h-full">
|
<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 w-full max-w-md gap-16 mx-auto">
|
||||||
<div className="flex flex-col gap-1 text-center items-center">
|
<div className="flex flex-col gap-1 text-center items-center">
|
||||||
<h1 className="text-2xl font-semibold">
|
<h1 className="text-2xl font-semibold">
|
||||||
Enter your nsecbunker token
|
Enter your nsecbunker token
|
||||||
|
|||||||
@@ -128,9 +128,9 @@ export function LoginWithOAuth() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative flex items-center justify-center w-full h-full">
|
<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 w-full max-w-md gap-16 mx-auto">
|
||||||
<div className="flex flex-col gap-1 text-center items-center">
|
<div className="flex flex-col gap-1 text-center items-center">
|
||||||
<h1 className="text-2xl font-semibold">Enter your NIP-05 address</h1>
|
<h1 className="text-2xl font-semibold">Enter your Nostr Address</h1>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-6">
|
<div className="flex flex-col gap-6">
|
||||||
<form
|
<form
|
||||||
|
|||||||
@@ -3,11 +3,9 @@ import { Link } from "react-router-dom";
|
|||||||
export function LoginScreen() {
|
export function LoginScreen() {
|
||||||
return (
|
return (
|
||||||
<div className="relative flex items-center justify-center w-full h-full">
|
<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 w-full max-w-md gap-16 mx-auto">
|
||||||
<div className="flex flex-col gap-1 text-center items-center">
|
<div className="flex flex-col gap-1 text-center items-center">
|
||||||
<h1 className="text-2xl font-semibold">
|
<h1 className="text-2xl font-semibold">Welcome back, anon!</h1>
|
||||||
Continue your experience on Nostr
|
|
||||||
</h1>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-6">
|
<div className="flex flex-col gap-6">
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
@@ -15,13 +13,13 @@ export function LoginScreen() {
|
|||||||
to="/auth/login-oauth"
|
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"
|
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"
|
||||||
>
|
>
|
||||||
Login with Address
|
Login with Nostr Address
|
||||||
</Link>
|
</Link>
|
||||||
<Link
|
<Link
|
||||||
to="/auth/login-nsecbunker"
|
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"
|
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
|
Login with nsecBunker
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-6">
|
<div className="flex flex-col gap-6">
|
||||||
@@ -31,7 +29,7 @@ export function LoginScreen() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="relative flex justify-center">
|
<div className="relative flex justify-center">
|
||||||
<span className="px-2 font-medium bg-black text-neutral-600">
|
<span className="px-2 font-medium bg-black text-neutral-600">
|
||||||
Or (Not recommended)
|
Or continue with
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -43,8 +41,10 @@ export function LoginScreen() {
|
|||||||
Login with Private Key
|
Login with Private Key
|
||||||
</Link>
|
</Link>
|
||||||
<p className="text-sm text-center text-neutral-500">
|
<p className="text-sm text-center text-neutral-500">
|
||||||
Lume will store your Private Key in{" "}
|
Lume will put your Private Key in{" "}
|
||||||
<span className="text-teal-600">OS Secure Storage</span>
|
<span className="text-teal-600">Secure Storage</span> depended
|
||||||
|
on your OS Platform. It will be secured by Password or Biometric
|
||||||
|
ID
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,16 +1,6 @@
|
|||||||
import { LoaderIcon } from "@lume/icons";
|
import { Link } from "react-router-dom";
|
||||||
import { useState } from "react";
|
|
||||||
import { Link, useNavigate } from "react-router-dom";
|
|
||||||
|
|
||||||
export function WelcomeScreen() {
|
export function WelcomeScreen() {
|
||||||
const navigate = useNavigate();
|
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
|
|
||||||
const gotoCreateAccount = () => {
|
|
||||||
setLoading(true);
|
|
||||||
navigate("/auth/create");
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col items-center justify-between w-full h-full">
|
<div className="flex flex-col items-center justify-between w-full h-full">
|
||||||
<div />
|
<div />
|
||||||
@@ -29,17 +19,12 @@ export function WelcomeScreen() {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col w-full max-w-xs gap-2 mx-auto">
|
<div className="flex flex-col w-full max-w-xs gap-2 mx-auto">
|
||||||
<button
|
<Link
|
||||||
type="button"
|
to="/auth/create"
|
||||||
onClick={gotoCreateAccount}
|
|
||||||
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"
|
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"
|
||||||
>
|
>
|
||||||
{loading ? (
|
Join Nostr
|
||||||
<LoaderIcon className="size-5 animate-spin" />
|
</Link>
|
||||||
) : (
|
|
||||||
"Create New Account"
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
<Link
|
<Link
|
||||||
to="/auth/login"
|
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"
|
||||||
@@ -49,7 +34,7 @@ export function WelcomeScreen() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center justify-center h-11">
|
<div className="flex items-center justify-center h-11">
|
||||||
<p className="text-neutral-800">
|
<p className="text-neutral-700">
|
||||||
Before joining Nostr, you can take time to learn more about Nostr{" "}
|
Before joining Nostr, you can take time to learn more about Nostr{" "}
|
||||||
<Link
|
<Link
|
||||||
to="https://nostr.com"
|
to="https://nostr.com"
|
||||||
|
|||||||
Reference in New Issue
Block a user