diff --git a/package-lock.json b/package-lock.json index 81958d3..57c431e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,7 @@ "@djot/djot": "^0.3.2", "@nostr/gadgets": "npm:@jsr/nostr__gadgets@^0.1.8", "@nostr/tools": "npm:@jsr/nostr__tools@^2.23.3", + "nstart-modal": "^2.1.0", "uqr": "^0.1.3" }, "devDependencies": { @@ -3170,6 +3171,11 @@ "integrity": "sha512-78BTryCLcLYv96ONU8Ws3Q1JzjlAt+43pWQhIl86xZmWeegYCNLPml7yQ+gG3vR6V5h4XGj+TxO+SS5dsThQIA==", "license": "MIT" }, + "node_modules/nstart-modal": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/nstart-modal/-/nstart-modal-2.1.0.tgz", + "integrity": "sha512-PolShYoWK07yJJbINtUn/IoOI5B0lmXRG9zOY9dirKKVjmMWFKjYLlafunNOl94EGcEndzAPWJAFDCt8flYRqg==" + }, "node_modules/obug": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz", diff --git a/package.json b/package.json index 961ad12..460340f 100644 --- a/package.json +++ b/package.json @@ -42,6 +42,7 @@ "@djot/djot": "^0.3.2", "@nostr/gadgets": "npm:@jsr/nostr__gadgets@^0.1.8", "@nostr/tools": "npm:@jsr/nostr__tools@^2.23.3", + "nstart-modal": "^2.1.0", "uqr": "^0.1.3" }, "overrides": { diff --git a/src/lib/components/LoginModal.svelte b/src/lib/components/LoginModal.svelte index fdcb025..6e05fc8 100644 --- a/src/lib/components/LoginModal.svelte +++ b/src/lib/components/LoginModal.svelte @@ -7,6 +7,10 @@ loginWithNostrConnect, loginWithNsec, } from "$lib/auth.svelte"; + import { TITLE, MODE } from "$lib/config"; + import { groupStore } from "$lib/group.svelte"; + import { themeState } from "$lib/theme.svelte"; + import type { NstartModal } from "nstart-modal"; import { onDestroy, tick } from "svelte"; import { renderSVG } from "uqr"; @@ -21,6 +25,19 @@ let nsecInput = $state(null); let bunkerInput = $state(null); + // Nstart sign-up wizard (an iframe overlay). Built lazily and reused; it is + // rebuilt only when the theme it was built for changes. + let wizard: NstartModal | null = null; + let wizardTheme: string | null = null; + // The login dialog hides while the wizard is up and comes back on cancel + let signingUp = $state(false); + + const appName = $derived( + TITLE || + (MODE === "simple" ? groupStore.data?.name : "") || + (typeof location !== "undefined" ? location.host : ""), + ); + // Client-initiated NIP-46 flow, alive only while the bunker view is shown let connect = $state | null>(null); const qrSvg = $derived( @@ -131,6 +148,62 @@ reset(); } + // The accent color as Nstart wants it (hex, no #), so the wizard matches the + // forum; env overrides are already applied to the CSS variable. + function accentHex(): string | undefined { + const v = getComputedStyle(document.documentElement) + .getPropertyValue("--color-accent") + .trim(); + return /^#[0-9a-fA-F]{6}$/.test(v) ? v.slice(1) : undefined; + } + + async function handleSignup() { + if (busy) return; + error = null; + stopConnect(); + const { NstartModal } = await import("nstart-modal"); + if (!wizard || wizardTheme !== themeState.theme) { + wizard?.destroy(); + wizardTheme = themeState.theme; + wizard = new NstartModal({ + baseUrl: "https://nstart.me", + an: appName, + aa: accentHex(), + am: themeState.theme, + // Only bunker:// or nsec come back; ncryptsec has no login here + aac: true, + onComplete: ({ nostrLogin }) => void completeSignup(nostrLogin), + onCancel: () => { + signingUp = false; + }, + }); + } + signingUp = true; + wizard.open(); + } + + async function completeSignup(nostrLogin: string | null) { + signingUp = false; + if (!nostrLogin) { + error = "Sign-up finished without a login credential"; + return; + } + busy = true; + try { + if (nostrLogin.startsWith("bunker://")) { + await loginWithBunker(nostrLogin); + } else { + await loginWithNsec(nostrLogin); + } + closeLogin(); + reset(); + } catch (e) { + error = e instanceof Error ? e.message : "Failed to login"; + } finally { + busy = false; + } + } + async function showNsecView() { stopConnect(); view = "nsec"; @@ -154,16 +227,19 @@ } function onKeydown(e: KeyboardEvent) { - if (!auth.loginModalOpen) return; + if (!auth.loginModalOpen || signingUp) return; if (e.key === "Escape") onClose(); } - onDestroy(stopConnect); + onDestroy(() => { + stopConnect(); + wizard?.destroy(); + }); -{#if auth.loginModalOpen} +{#if auth.loginModalOpen && !signingUp}
+ +
+ +

+ Don't have an account yet? +

+ {:else if view === "bunker"} {#if connect}