import { Show, createSignal, createEffect, onCleanup } from "solid-js" import { useNavigate, type RouteSectionProps } from "@solidjs/router" import { ExtensionAccount, NostrConnectAccount, PasswordAccount, PrivateKeyAccount } from "applesauce-accounts/accounts" import { PasswordSigner } from "applesauce-signers" import QrScanner from "qr-scanner" import QRCode from "qrcode" import { accountManager, identity, PLATFORM_NAME } from "@/lib/state" import useMinLoading from "@/components/useMinLoading" const NIP46_RELAYS = ['wss://bucket.coracle.social', 'wss://ephemeral.snowflare.cc'] type Tab = "nip07" | "nip46" | "key" type Screen = "tabs" | "nip46" | "key" type SignerTab = "qr" | "paste" type KeyTab = "plaintext" | "encrypted" type LoginProps = { inModal?: boolean onClose?: () => void onAuthenticated?: () => void | Promise } function normalizeBunkerUrl(value: string): string { const trimmed = value.trim() if (!trimmed) return "" if (trimmed.startsWith("nostrconnect://")) { const url = new URL(trimmed) const remote = url.host || url.pathname.replace(/^\/+/, "") const relays = url.searchParams.getAll("relay") const secret = url.searchParams.get("secret") const params = new URLSearchParams() for (const relay of relays) params.append("relay", relay) if (secret) params.set("secret", secret) return `bunker://${remote}?${params.toString()}` } return trimmed } async function loadNostrConnectSigner() { return import("applesauce-signers").then((m) => m.NostrConnectSigner) } type LoginPageProps = LoginProps & Partial> export default function Login(props: LoginPageProps = {}) { const navigate = useNavigate() const [tab, setTab] = createSignal(window.nostr ? "nip07" : "nip46") const [rawLoading, setRawLoading] = createSignal(false) const loading = useMinLoading(() => rawLoading()) const [error, setError] = createSignal("") const [screen, setScreen] = createSignal("tabs") const [signerTab, setSignerTab] = createSignal("qr") const [keyTab, setKeyTab] = createSignal("plaintext") const [nostrConnectUri, setNostrConnectUri] = createSignal("") const [qrDataUrl, setQrDataUrl] = createSignal("") const [showScanner, setShowScanner] = createSignal(false) const [bunkerUrl, setBunkerUrl] = createSignal("") const [nsecValue, setNsecValue] = createSignal("") const [ncryptsecValue, setNcryptsecValue] = createSignal("") const [password, setPassword] = createSignal("") let scannerVideo: HTMLVideoElement | undefined let scanner: QrScanner | undefined let abortController: AbortController | undefined async function completeLogin(account: ExtensionAccount | NostrConnectAccount | PrivateKeyAccount | PasswordAccount) { accountManager.addAccount(account) accountManager.setActive(account) await props.onAuthenticated?.() } async function loginWithNip07() { setError("") setRawLoading(true) try { await completeLogin(await ExtensionAccount.fromExtension()) } catch (e) { setError(e instanceof Error ? e.message : "Failed to login with extension") } finally { setRawLoading(false) } } async function startNostrConnect() { setError("") setRawLoading(true) try { const NostrConnectSigner = await loadNostrConnectSigner() const signer = new NostrConnectSigner({ relays: NIP46_RELAYS }) const uri = signer.getNostrConnectURI({ name: PLATFORM_NAME, url: window.location.origin, }) setNostrConnectUri(uri) setQrDataUrl(await QRCode.toDataURL(uri, { width: 256, margin: 2 })) abortController = new AbortController() const timeout = window.setTimeout(() => abortController?.abort(), 60_000) try { await signer.waitForSigner(abortController.signal) const pubkey = await signer.getPublicKey() const account = new NostrConnectAccount(pubkey, signer) await completeLogin(account) } finally { window.clearTimeout(timeout) } } catch (e) { setError(e instanceof Error ? e.message : "Failed to connect signer") } finally { setRawLoading(false) } } async function loginWithBunker() { setError("") setRawLoading(true) try { const uri = normalizeBunkerUrl(bunkerUrl()) const NostrConnectSigner = await loadNostrConnectSigner() const signer = await NostrConnectSigner.fromBunkerURI(uri) const pubkey = await signer.getPublicKey() const account = new NostrConnectAccount(pubkey, signer) await completeLogin(account) } catch (e) { setError(e instanceof Error ? e.message : "Invalid bunker URL") } finally { setRawLoading(false) } } async function loginWithKeyMaterial() { setError("") setRawLoading(true) try { if (ncryptsecValue().trim()) { if (!password().trim()) { throw new Error("Password is required for ncryptsec") } const signer = await PasswordSigner.fromNcryptsec(ncryptsecValue().trim(), password()) const pubkey = await signer.getPublicKey() const account = new PasswordAccount(pubkey, signer) await completeLogin(account) return } const key = nsecValue().trim() if (!key) throw new Error("Enter an nsec or ncryptsec key") const account = PrivateKeyAccount.fromKey(key) await completeLogin(account) } catch (e) { setError(e instanceof Error ? e.message : "Invalid key") } finally { setRawLoading(false) } } function closeScanner() { setShowScanner(false) scanner?.destroy() scanner = undefined } async function openScanner() { setError("") setShowScanner(true) // Wait a tick for the modal video element to mount await new Promise(r => setTimeout(r, 0)) if (!scannerVideo) return scanner = new QrScanner( scannerVideo, (result) => { setBunkerUrl(result.data) closeScanner() }, { returnDetailedScanResult: true }, ) try { await scanner.start() } catch (e) { setError(e instanceof Error ? e.message : "Unable to access camera") closeScanner() } } function enterSignerScreen() { setError("") setScreen("nip46") setSignerTab("qr") if (!nostrConnectUri()) void startNostrConnect() } function copyUri() { void navigator.clipboard.writeText(nostrConnectUri()) } createEffect(() => { if (!props.inModal && identity()) { navigate("/relays") } }) onCleanup(() => { abortController?.abort() scanner?.destroy() }) return (
Secure Nostr Login

Welcome!

Connect your Nostr account to manage relay hosting, billing, and access in one place.

Own your identity with cryptographic sign-in.

No passwords or email required.

Get fast access to your relays.

Log in / Sign up

Use any Nostr signer method. New users are automatically onboarded.

Log in with signer

{loading() ? "Generating..." : "Loading QR code..."}
}> Nostrconnect QR code
setBunkerUrl(e.currentTarget.value)} placeholder="bunker://..." class="min-w-0 flex-1 rounded-l-lg border-0 px-3 py-2 text-sm bg-transparent focus:outline-none" />

Log in with key

{ e.preventDefault() void loginWithKeyMaterial() }} > setNsecValue(e.currentTarget.value)} placeholder="nsec1..." class="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm" /> setNcryptsecValue(e.currentTarget.value)} placeholder="ncryptsec1..." class="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm" /> setPassword(e.currentTarget.value)} placeholder="Password" class="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm" />

{error()}

Having trouble? Make sure your signer is unlocked and connected.

e.stopPropagation()}>

Scan QR Code

) }