Add sign-up via Nstart

This commit is contained in:
dtonon 2026-08-25 22:24:46 +01:00
parent 03395adf85
commit bcbc70ecb8
3 changed files with 104 additions and 3 deletions

6
package-lock.json generated
View file

@ -11,6 +11,7 @@
"@djot/djot": "^0.3.2", "@djot/djot": "^0.3.2",
"@nostr/gadgets": "npm:@jsr/nostr__gadgets@^0.1.8", "@nostr/gadgets": "npm:@jsr/nostr__gadgets@^0.1.8",
"@nostr/tools": "npm:@jsr/nostr__tools@^2.23.3", "@nostr/tools": "npm:@jsr/nostr__tools@^2.23.3",
"nstart-modal": "^2.1.0",
"uqr": "^0.1.3" "uqr": "^0.1.3"
}, },
"devDependencies": { "devDependencies": {
@ -3170,6 +3171,11 @@
"integrity": "sha512-78BTryCLcLYv96ONU8Ws3Q1JzjlAt+43pWQhIl86xZmWeegYCNLPml7yQ+gG3vR6V5h4XGj+TxO+SS5dsThQIA==", "integrity": "sha512-78BTryCLcLYv96ONU8Ws3Q1JzjlAt+43pWQhIl86xZmWeegYCNLPml7yQ+gG3vR6V5h4XGj+TxO+SS5dsThQIA==",
"license": "MIT" "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": { "node_modules/obug": {
"version": "2.1.1", "version": "2.1.1",
"resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz", "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz",

View file

@ -42,6 +42,7 @@
"@djot/djot": "^0.3.2", "@djot/djot": "^0.3.2",
"@nostr/gadgets": "npm:@jsr/nostr__gadgets@^0.1.8", "@nostr/gadgets": "npm:@jsr/nostr__gadgets@^0.1.8",
"@nostr/tools": "npm:@jsr/nostr__tools@^2.23.3", "@nostr/tools": "npm:@jsr/nostr__tools@^2.23.3",
"nstart-modal": "^2.1.0",
"uqr": "^0.1.3" "uqr": "^0.1.3"
}, },
"overrides": { "overrides": {

View file

@ -7,6 +7,10 @@
loginWithNostrConnect, loginWithNostrConnect,
loginWithNsec, loginWithNsec,
} from "$lib/auth.svelte"; } 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 { onDestroy, tick } from "svelte";
import { renderSVG } from "uqr"; import { renderSVG } from "uqr";
@ -21,6 +25,19 @@
let nsecInput = $state<HTMLInputElement | null>(null); let nsecInput = $state<HTMLInputElement | null>(null);
let bunkerInput = $state<HTMLInputElement | null>(null); let bunkerInput = $state<HTMLInputElement | null>(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 // Client-initiated NIP-46 flow, alive only while the bunker view is shown
let connect = $state<ReturnType<typeof loginWithNostrConnect> | null>(null); let connect = $state<ReturnType<typeof loginWithNostrConnect> | null>(null);
const qrSvg = $derived( const qrSvg = $derived(
@ -131,6 +148,62 @@
reset(); 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() { async function showNsecView() {
stopConnect(); stopConnect();
view = "nsec"; view = "nsec";
@ -154,16 +227,19 @@
} }
function onKeydown(e: KeyboardEvent) { function onKeydown(e: KeyboardEvent) {
if (!auth.loginModalOpen) return; if (!auth.loginModalOpen || signingUp) return;
if (e.key === "Escape") onClose(); if (e.key === "Escape") onClose();
} }
onDestroy(stopConnect); onDestroy(() => {
stopConnect();
wizard?.destroy();
});
</script> </script>
<svelte:window onkeydown={onKeydown} /> <svelte:window onkeydown={onKeydown} />
{#if auth.loginModalOpen} {#if auth.loginModalOpen && !signingUp}
<div class="fixed inset-0 z-50 flex items-center justify-center p-4"> <div class="fixed inset-0 z-50 flex items-center justify-center p-4">
<button <button
type="button" type="button"
@ -230,6 +306,24 @@
> >
Or log in using your nsec Or log in using your nsec
</button> </button>
<div
class="my-5 border-t border-neutral-200 dark:border-neutral-700"
></div>
<p
class="mb-3 text-center text-sm text-neutral-600 dark:text-neutral-400"
>
Don't have an account yet?
</p>
<button
type="button"
onclick={handleSignup}
disabled={busy}
class="border-accent text-accent hover:bg-accent/10 w-full rounded border px-3 py-2 text-sm font-medium disabled:cursor-not-allowed disabled:opacity-50"
>
Sign up
</button>
{:else if view === "bunker"} {:else if view === "bunker"}
{#if connect} {#if connect}
<a <a