const $ = s => document.querySelector(s); const fail = e => { $('#err').textContent = e && e.name === 'NotAllowedError' ? 'Cancelled. Try again.' : (e.message || String(e)); }; async function signIn() { $('#err').textContent = ''; try { await passkeyLogin(); location.replace('/'); } catch (e) { fail(e); } } // ---- Name tiles: on a remembered device (a family screen or a person's own device) the sign-in page shows who lives here. Tap your name, then use your PIN or passkey. const escH = t => String(t == null ? '' : t).replace(/[&<>"']/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c])); async function tilesLogin() { let r; try { r = await fetch('/api/login/people'); } catch { return false; } if (!r.ok) return false; const { people } = await r.json(); if (!people.length) return false; document.querySelector('.box').classList.add('wide'); $('#sub').textContent = "Who's signing in? Tap your name"; const av = p => `${escH((p.name.trim()[0] || '?').toUpperCase())}`; const home = () => { $('#err').textContent = ''; $('#sub').textContent = "Who's signing in? Tap your name"; $('#ui').innerHTML = `
${people.map(p => ``).join('')}

Sign in another way (any passkey, invite or recovery code)

`; $('#ui').querySelectorAll('.tile').forEach(t => t.onclick = () => who(people.find(p => p.id === t.dataset.id))); $('#other').onclick = e => { e.preventDefault(); classicLogin(); }; }; const who = p => { $('#err').textContent = ''; $('#sub').textContent = ''; const tile = `
${av(p)}${escH(p.name)}${p.kid ? 'KID' : ''}
`; const back = '

Not you? Back to the names

', passkeyBtn = p.passkey ? '

' : ''; const done = () => location.replace('/'), pkgo = async () => { $('#err').textContent = ''; try { await passkeyLogin(p.id); done(); } catch (e) { fail(e); } }; const wire = () => { const b = $('#back'); if (b) b.onclick = e => { e.preventDefault(); home(); }; const k = $('#pk'); if (k) k.onclick = pkgo; }; if (p.kid && !p.pin) { $('#ui').innerHTML = `${tile}

Sign in as ${escH(p.name)}?

${passkeyBtn}${back}`; wire(); $('#yes').onclick = async () => { try { await pinPost(p.id); done(); } catch (e) { fail(e); } }; return; } if (!p.pin) { $('#ui').innerHTML = `${tile}

${escH(p.name)} has not set a PIN yet.

${p.passkey ? '' : '

Set a PIN or add a passkey in Settings on a device where you are signed in.

'}${back}`; wire(); return; } let pin = ''; $('#ui').innerHTML = `${tile}

Enter your PIN

${[1, 2, 3, 4, 5, 6, 7, 8, 9, '⌫', 0, '✓'].map(k => ``).join('')}
${passkeyBtn}${back}`; wire(); const paint = () => { $('#dots').innerHTML = Array.from({ length: Math.max(4, pin.length) }, (_, i) => ``).join(''); }; paint(); $('#ui').querySelectorAll('.pad button').forEach(b => b.onclick = async () => { let k = b.dataset.k; $('#err').textContent = ''; if (k === '⌫') pin = pin.slice(0, -1); else if (k !== '✓' && pin.length < 8) { pin += k; if (p.pinLen && pin.length === p.pinLen) k = '✓'; } if (k === '✓') { try { await pinPost(p.id, pin); done(); return; } catch (e) { fail(e); pin = ''; } } paint(); }); }; const pinPost = async (uid, pin) => { const r = await fetch('/api/login/pin', { method: 'POST', body: JSON.stringify({ uid, pin }) }), j = await r.json().catch(() => ({})); if (!r.ok) throw new Error(j.error || 'Could not sign in'); }; home(); return true; } async function init() { const st = await (await fetch('/api/auth/state')).json(); if (st.authed) return location.replace('/'); if (st.registered && await tilesLogin()) return; // (a remembered device: name tiles, and the PIN works even where passkeys do not) if (!window.PublicKeyCredential) { $('#sub').textContent = ''; return fail(new Error('This browser does not support passkeys.')); } classicLogin(st); } async function classicLogin(st) { if (!st) st = await (await fetch('/api/auth/state')).json(); if (st.registered) { $('#sub').textContent = 'Sign in with your passkey'; $('#ui').innerHTML = ''; $('#go').onclick = signIn; $('#ui').insertAdjacentHTML('beforeend', '

New device or lost your devices? Use an invite or recovery code

'); $('#inv').onclick = e => { e.preventDefault(); $('#err').textContent = ''; $('#sub').textContent = 'Add this device'; $('#ui').innerHTML = `

On a device that is already signed in, open Settings and tap Invite a device. Lost every device? Use one of the recovery codes you made in Settings.

Back to sign in

`; $('#f').onsubmit = async ev => { ev.preventDefault(); $('#err').textContent = ''; try { await passkeyRegister('', $('#nm').value.trim() || 'Passkey', $('#ic').value.trim()); location.replace('/'); } catch (err) { fail(err); } }; }; // Offer the passkey straight away unless the user came here to use an invite code. signIn(); } else { $('#sub').textContent = 'First-time setup: create a passkey for this device'; $('#ui').innerHTML = `
`; $('#f').onsubmit = async e => { e.preventDefault(); $('#err').textContent = ''; try { await passkeyRegister($('#sc').value.trim(), $('#nm').value.trim() || 'Passkey'); location.replace('/'); } catch (err) { fail(err); } }; } } init().catch(fail);