Skip to content

Commit 8e9aacf

Browse files
UX pass: consistent link coaching, smoother flow, light mode only
login.js: - unify the two "key isn't linked" outcomes (404, and a synthesized DID doc with no alsoKnownAs — nostr.social answers 200 for any valid key) behind the same coaching; skip the fallback-resolver link when already on nostr.social so it can't loop - auto-resume the flow when arriving with an explicit ?resolver= (e.g. via the fallback link) instead of asking for a second click - grace-poll up to 1.5s for window.nostr before declaring no signer (extensions can inject after page load); soft on-load hint when none announces itself - button reads "Signing in…" while the flow runs - redirect immediately to the user's own pod; hold 1.5s when an app-supplied ?next= is the destination so the user sees where they're going, and refuse non-HTTP destinations index.html: - drop the dark-mode palette; declare color-scheme: light
1 parent 32a970f commit 8e9aacf

2 files changed

Lines changed: 65 additions & 24 deletions

File tree

index.html

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
<meta charset="UTF-8">
55
<title>Sign in to Solid</title>
66
<meta name="viewport" content="width=device-width, initial-scale=1">
7+
<meta name="color-scheme" content="light">
78
<meta name="description" content="Universal one-click sign-in for Solid pods. Click the button — you're at your pod.">
89
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'%3E%3Ccircle cx='50' cy='50' r='45' fill='%236839e6'/%3E%3C/svg%3E">
910

@@ -14,22 +15,14 @@
1415

1516
<style>
1617
:root {
18+
color-scheme: light;
1719
--bg: #fafbfc;
1820
--fg: #1f2328;
1921
--accent: #6839e6;
2022
--accent-hover: #5328d4;
2123
--muted: #656d76;
2224
--error: #cf222e;
2325
}
24-
@media (prefers-color-scheme: dark) {
25-
:root {
26-
--bg: #0d1117;
27-
--fg: #e6edf3;
28-
--accent: #a78bfa;
29-
--accent-hover: #c4b5fd;
30-
--muted: #8b949e;
31-
}
32-
}
3326
* { box-sizing: border-box; }
3427
html, body {
3528
margin: 0; padding: 0; height: 100%;

login.js

Lines changed: 63 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
// ?next=<url> — destination after success (default pod root)
2828

2929
const DEFAULT_RESOLVER = 'https://solid.social';
30+
const FALLBACK_RESOLVER = 'https://nostr.social'; // did-nostr.com source
3031

3132
function readConfig() {
3233
const p = new URLSearchParams(location.search);
@@ -72,12 +73,40 @@ function podRootFromWebId(webId) {
7273
catch { return null; }
7374
}
7475

76+
// Both "not linked" outcomes (404, and a synthesized DID doc with no
77+
// alsoKnownAs — nostr.social answers 200 for any valid key) get the
78+
// same coaching. Don't offer the fallback when we're already on it.
79+
function linkCoaching(resolver) {
80+
const links = [
81+
{ label: 'How to link your Nostr key to a Solid pod', href: 'https://jss.live/docs/' },
82+
];
83+
if (resolver !== FALLBACK_RESOLVER) {
84+
const alt = new URLSearchParams(location.search);
85+
alt.set('resolver', FALLBACK_RESOLVER);
86+
links.push({ label: 'Try the fallback resolver (nostr.social)', href: `?${alt}` });
87+
}
88+
return links;
89+
}
90+
91+
// Signer extensions can inject window.nostr a beat after page load;
92+
// don't declare "no signer" on a fast click without a short grace poll.
93+
function waitForSigner(ms = 1500) {
94+
return new Promise((resolve) => {
95+
if (window.nostr) return resolve(true);
96+
const started = performance.now();
97+
const poll = setInterval(() => {
98+
if (window.nostr) { clearInterval(poll); resolve(true); }
99+
else if (performance.now() - started > ms) { clearInterval(poll); resolve(false); }
100+
}, 100);
101+
});
102+
}
103+
75104
async function flow() {
76105
const { resolver, next } = readConfig();
77106
setStatus('Reading your Nostr identity…');
78107

79108
// Step 1 — signer extension present?
80-
if (!window.nostr) {
109+
if (!(await waitForSigner())) {
81110
setStatus(help(
82111
['No Nostr signer detected. Install a browser extension that provides ',
83112
'window.nostr, then reload this page.'],
@@ -114,17 +143,9 @@ async function flow() {
114143
headers: { Accept: 'application/did+json, application/json' },
115144
});
116145
if (res.status === 404) {
117-
// Fallback resolver: nostr.social serves the did-nostr.com
118-
// source. Preserve the caller's other params (e.g. ?next=)
119-
// when offering the switch.
120-
const alt = new URLSearchParams(location.search);
121-
alt.set('resolver', 'https://nostr.social');
122146
setStatus(help(
123147
['Your Nostr key isn’t linked to a Solid pod at ', resolver, '. '],
124-
[
125-
{ label: 'How to link your Nostr key to a Solid pod', href: 'https://jss.live/docs/' },
126-
{ label: 'Try the fallback resolver (nostr.social)', href: `?${alt}` },
127-
],
148+
linkCoaching(resolver),
128149
), 'error');
129150
return false;
130151
}
@@ -145,8 +166,8 @@ async function flow() {
145166
const webId = aka.find((x) => typeof x === 'string' && /^https?:\/\//.test(x));
146167
if (!webId) {
147168
setStatus(help(
148-
[`Your did:nostr document at ${resolver} doesn’t include an alsoKnownAs WebID. `],
149-
[{ label: 'How to fix this', href: 'https://jss.live/docs/' }],
169+
[`Your Nostr key isn’t linked to a Solid pod at ${resolver} — the DID document has no alsoKnownAs WebID. `],
170+
linkCoaching(resolver),
150171
), 'error');
151172
return false;
152173
}
@@ -163,29 +184,56 @@ async function flow() {
163184
let dest;
164185
try {
165186
const url = new URL(base);
187+
if (!/^https?:$/.test(url.protocol)) {
188+
setStatus(`Refusing to redirect to a non-HTTP destination: ${base}`, 'error');
189+
return false;
190+
}
166191
url.searchParams.set('webid', `did:nostr:${pubkey}`);
167192
dest = url.href;
168193
} catch {
169194
setStatus(`Could not build redirect URL from: ${base}`, 'error');
170195
return false;
171196
}
172197
setStatus(`Found your WebID: ${webId}. Taking you to ${base}…`);
173-
setTimeout(() => { location.href = dest; }, 800);
198+
// Hopping to the user's own pod is instant; an app-supplied ?next=
199+
// destination stays on screen for a beat so the user sees where
200+
// they're being sent before leaving this origin.
201+
setTimeout(() => { location.href = dest; }, next ? 1500 : 0);
174202
return true;
175203
}
176204

177205
function wire() {
178206
const button = document.querySelector('#signin');
179207
if (!button) return;
180-
button.addEventListener('click', async () => {
208+
209+
async function run() {
181210
button.disabled = true;
182211
button.setAttribute('aria-busy', 'true');
212+
button.textContent = 'Signing in…';
183213
const ok = await flow();
184214
if (!ok) {
185215
button.disabled = false;
186216
button.removeAttribute('aria-busy');
217+
button.textContent = 'Sign in';
187218
}
188-
});
219+
}
220+
221+
button.addEventListener('click', run);
222+
223+
if (new URLSearchParams(location.search).has('resolver')) {
224+
// Arriving via a "Try the fallback resolver" link (or any explicit
225+
// ?resolver=) carries a click's worth of intent — resume the flow
226+
// instead of asking for a second click.
227+
run();
228+
} else {
229+
// Soft pre-check: coach before the first click if no signer
230+
// extension has announced itself.
231+
setTimeout(() => {
232+
if (!window.nostr && !button.disabled) {
233+
setStatus('No Nostr signer detected yet — you’ll need a signer extension to sign in.');
234+
}
235+
}, 1500);
236+
}
189237
}
190238

191239
wire();

0 commit comments

Comments
 (0)