1const API = 'https://api.peeksolve.com'; 2// A referral link points straight at the sign-in page. Remember the code, send 3// the person to the plan picker after sign-in, and show what they get. 4const inviteRef = (() => { try { const ref = new URLSearchParams(location.search).get('ref'); return (ref && /^peek-[a-z2-9]{4}$/i.test(ref.trim())) ? ref.trim().toUpperCase() : null; } catch (e) { return null; } })(); 5if (inviteRef) { try { localStorage.setItem('ps_ref', inviteRef); localStorage.setItem('ps_next', '/account.html?checkout=1'); } catch (e) {} } 6// PeekBench (peekbench.com) signs in here and gets the session handed back in 7// the URL fragment. The wish expires after 30 minutes so a later, ordinary 8// sign-in isn't sent there. 9if (new URLSearchParams(location.search).get('to') === 'peekbench') { try { localStorage.setItem('ps_bench', String(Date.now())); } catch (e) {} } 10function benchHandoff() { 11 try { 12 const at = Number(localStorage.getItem('ps_bench') || 0); 13 const jwt = localStorage.getItem('peeksolve_jwt'); 14 if (!at || Date.now() - at > 30 * 60 * 1000 || !jwt) return null; 15 localStorage.removeItem('ps_bench'); 16 let email = ''; 17 try { email = (JSON.parse(localStorage.getItem('peeksolve_user') || '{}') || {}).email || ''; } catch (e) {} 18 return `https://peekbench.com/auth.html#jwt=${encodeURIComponent(jwt)}&email=${encodeURIComponent(email)}`; 19 } catch (e) { return null; } 20} 21function nextUrl() { const bench = benchHandoff(); if (bench) return bench; try { const n = localStorage.getItem('ps_next'); if (n && n.startsWith('/account.html')) return n; } catch (e) {} return '/account.html'; } 22const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; 23 24// If already signed in, jump to account. 25if (localStorage.getItem('peeksolve_jwt')) { 26 location.href = nextUrl(); 27} 28 29const form = document.getElementById('magicForm'); 30const email = document.getElementById('email'); 31const emailErr = document.getElementById('emailErr'); 32const status = document.getElementById('status'); 33const submitBtn = document.getElementById('submitBtn'); 34const googleBtn = document.getElementById('googleSignIn'); 35const googleDivider = document.getElementById('googleDivider'); 36const formDefault = document.getElementById('formDefault'); 37const sentState = document.getElementById('sentState'); 38const sentEmail = document.getElementById('sentEmail'); 39const changeEmail = document.getElementById('changeEmail'); 40const resendTimer = document.getElementById('resendTimer'); 41 42googleBtn.href = `${API}/auth/google`; 43 44const inviteBanner = document.getElementById('inviteBanner'); 45if (inviteRef && inviteBanner) { 46 inviteBanner.hidden = false; 47 fetch(`${API}/referral/check?code=${encodeURIComponent(inviteRef)}`).then((r) => r.json()).then((d) => { 48 if (!d.valid) { inviteBanner.hidden = true; try { localStorage.removeItem('ps_ref'); localStorage.removeItem('ps_next'); } catch (e) {} } 49 }).catch(() => {}); 50} 51 52// Show Google button only if backend reports it configured. 53fetch(`${API}/auth/providers`) 54 .then((r) => (r.ok ? r.json() : { google: false })) 55 .then((providers) => { 56 if (providers.google) { 57 googleDivider.style.display = ''; 58 googleBtn.style.display = ''; 59 } 60 }) 61 .catch(() => { 62 // Backend unreachable â leave Google hidden, magic link still works. 63 }); 64 65function showError(msg) { 66 status.textContent = msg; 67 status.classList.add('show'); 68 status.classList.remove('ok'); 69} 70function clearError() { 71 status.textContent = ''; 72 status.classList.remove('show'); 73 email.classList.remove('bad'); 74 emailErr.classList.remove('show'); 75} 76 77email.addEventListener('input', clearError); 78 79async function sendMagicLink(addr) { 80 const res = await fetch(`${API}/auth/magic-link`, { 81 method: 'POST', 82 headers: { 'Content-Type': 'application/json' }, 83 body: JSON.stringify({ email: addr }) 84 }); 85 if (!res.ok) throw new Error(`HTTP ${res.status}`); 86 return res.json().catch(() => ({})); 87} 88 89// Cross-device: poll until the emailed link is clicked (on ANY device), then log 90// THIS browser in (localStorage + postMessage so the extension picks it up) and 91// go to the account. That's what makes "clicked on phone â desktop logs in" work. 92let pollTimer; 93function startPolling(pollId, secret) { 94 if (!pollId || !secret) return; 95 clearInterval(pollTimer); 96 pollTimer = setInterval(async () => { 97 try { 98 const r = await fetch(`${API}/auth/magic-link/poll?id=${encodeURIComponent(pollId)}&secret=${encodeURIComponent(secret)}`); 99 if (!r.ok) return; 100 const d = await r.json(); 101 if (d.status === 'ok' && d.jwt) { 102 clearInterval(pollTimer); 103 localStorage.setItem('peeksolve_jwt', d.jwt); 104 localStorage.setItem('peeksolve_user', JSON.stringify(d.user || {})); 105 window.postMessage({ source: 'peeksolve-auth', jwt: d.jwt, user: d.user || {} }, location.origin); 106 setTimeout(() => { location.href = nextUrl(); }, 400); 107 } else if (d.status === 'expired') { 108 clearInterval(pollTimer); 109 } 110 } catch { /* transient â keep polling */ } 111 }, 2500); 112} 113 114form.addEventListener('submit', async (e) => { 115 e.preventDefault(); 116 const addr = email.value.trim(); 117 if (!EMAIL_RE.test(addr)) { 118 email.classList.add('bad'); 119 emailErr.classList.add('show'); 120 email.focus(); 121 return; 122 } 123 clearError(); 124 submitBtn.disabled = true; 125 submitBtn.textContent = 'Sendingâ¦'; 126 try { 127 const data = await sendMagicLink(addr); 128 sentEmail.textContent = addr; 129 formDefault.classList.add('hide'); 130 sentState.classList.add('show'); 131 startResend(addr); 132 startPolling(data.pollId, data.secret); 133 } catch (err) {
134 showError('Failed to send. Try again or contact [email protected].'); 135 submitBtn.disabled = false; 136 submitBtn.textContent = 'Send magic link'; 137 } 138}); 139 140changeEmail.addEventListener('click', () => { 141 sentState.classList.remove('show'); 142 formDefault.classList.remove('hide'); 143 submitBtn.disabled = false; 144 submitBtn.textContent = 'Send magic link'; 145 email.focus(); 146 email.select(); 147}); 148 149let resendInterval; 150function startResend(addr) { 151 let n = 30; 152 resendTimer.classList.add('dim'); 153 resendTimer.textContent = n + 's'; 154 resendTimer.onclick = null; 155 clearInterval(resendInterval); 156 resendInterval = setInterval(() => { 157 n--; 158 if (n <= 0) { 159 clearInterval(resendInterval); 160 resendTimer.classList.remove('dim'); 161 resendTimer.textContent = 'Resend now'; 162 resendTimer.onclick = async () => { 163 resendTimer.onclick = null; 164 try { 165 const data = await sendMagicLink(addr); 166 startResend(addr); 167 startPolling(data.pollId, data.secret); 168 } catch { 169 resendTimer.textContent = 'Resend now';
170 resendTimer.onclick = () => startResend(addr); 171 } 172 }; 173 } else { 174 resendTimer.textContent = n + 's'; 175 } 176 }, 1000); 177}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.