1/** 2 * SB Auth â login form JS. 3 * 4 * State machine: IDLE â SUBMITTING â DONE. 5 * - bfcache restore (pageshow event.persisted) resets to IDLE. 6 * - Stale-nonce response triggers a transparent re-submit with the fresh 7 * nonce returned by the REST handler (one retry max). 8 * - After success, the form is hidden and a 60s countdown is shown before 9 * "Send a new link" becomes clickable. 10 * 11 * No localStorage / sessionStorage. No client-side validation gates beyond 12 * native HTML5 (server is canonical). Honeypot is read server-side only. 13 * 14 * @see docs/plans/2026-05-06-001-feat-sb-auth-v1-and-registration-form-plan.md 15 */ 16 17/** 18 * GA4 / GTM dataLayer push helper. Events are namespaced `sb_auth_*` so 19 * they can be filtered into a clean GA4 trigger group via GTM. PII (email, 20 * tokens) is NEVER passed in event params â only state machine values. 21 */ 22function sbAuthTrack(eventName, params) { 23 try { 24 window.dataLayer = window.dataLayer || []; 25 var payload = { event: eventName }; 26 if (params && typeof params === 'object') { 27 for (var key in params) { 28 if (Object.prototype.hasOwnProperty.call(params, key)) { 29 payload[key] = params[key]; 30 } 31 } 32 } 33 window.dataLayer.push(payload); 34 } catch (e) { 35 // Tracking should never break the auth flow. 36 } 37} 38 39(function () { 40 'use strict'; 41 42 var IDLE = 'idle'; 43 var SUBMITTING = 'submitting'; 44 var DONE = 'done'; 45 46 var wrapper = document.getElementById('sb-auth-form-wrapper'); 47 var form = document.getElementById('sb-auth-login-form'); 48 var success = document.getElementById('sb-auth-success'); 49 var errorEl = document.getElementById('sb-auth-form-error'); 50 var resendBtn = document.getElementById('sb-auth-resend'); 51 var cooldownEl = document.getElementById('sb-auth-cooldown-seconds'); 52 53 if (!wrapper || !form || !success) { 54 return; 55 } 56 57 var state = wrapper.dataset.state || IDLE; 58 var restUrl = (window.sbAuth && window.sbAuth.restUrl) || form.getAttribute('action'); 59 var cooldownSeconds = parseInt(wrapper.dataset.cooldownSeconds, 10) || 60; 60 var cooldownTimer = null; 61 62 function setState(next) { 63 state = next; 64 wrapper.dataset.state = next; 65 66 var btn = form.querySelector('button[type="submit"]'); 67 if (btn) { 68 if (next === SUBMITTING) { 69 btn.disabled = true; 70 btn.setAttribute('aria-busy', 'true'); 71 btn.textContent = btn.dataset.busyText || 'Sendingâ¦'; 72 } else { 73 btn.disabled = false; 74 btn.removeAttribute('aria-busy'); 75 btn.textContent = btn.dataset.idleText || 'Send sign-in link'; 76 } 77 } 78 } 79 80 function showError(message) { 81 if (!errorEl) return; 82 errorEl.textContent = message; 83 errorEl.classList.remove('hidden'); 84 } 85 86 function hideError() { 87 if (!errorEl) return; 88 errorEl.classList.add('hidden'); 89 errorEl.textContent = ''; 90 } 91 92 function showSuccess() { 93 form.classList.add('hidden'); 94 success.classList.remove('hidden'); 95 startCooldown(); 96 } 97 98 function showForm() { 99 success.classList.add('hidden'); 100 form.classList.remove('hidden'); 101 hideError(); 102 setState(IDLE); 103 } 104 105 function startCooldown() { 106 if (cooldownTimer) { 107 clearInterval(cooldownTimer); 108 } 109 if (resendBtn) resendBtn.classList.add('hidden'); 110 var remaining = cooldownSeconds; 111 if (cooldownEl) cooldownEl.textContent = String(remaining); 112 cooldownTimer = setInterval(function () { 113 remaining -= 1; 114 if (cooldownEl) cooldownEl.textContent = String(remaining); 115 if (remaining <= 0) { 116 clearInterval(cooldownTimer); 117 cooldownTimer = null; 118 if (resendBtn) resendBtn.classList.remove('hidden'); 119 } 120 }, 1000); 121 } 122 123 /** 124 * Submit the form to the REST endpoint. If the server returns 125 * expired_nonce + fresh_nonce, swap and retry ONCE. 126 */ 127 function submitForm(retryWithNonce) { 128 if (state !== IDLE) { 129 return; 130 } 131 hideError(); 132 setState(SUBMITTING); 133 134 var formData = new FormData(form); 135 if (retryWithNonce) { 136 formData.set('_wpnonce', retryWithNonce); 137 } 138 if (!retryWithNonce) { 139 sbAuthTrack('sb_auth_login_request_submitted', {}); 140 } 141 142 var headers = { 143 'Accept': 'application/json', 144 'X-WP-Nonce': formData.get('_wpnonce'), 145 }; 146 147 fetch(restUrl, { 148 method: 'POST', 149 credentials: 'same-origin', 150 headers: headers, 151 body: formData, 152 }) 153 .then(function (response) { 154 return response.json().catch(function () { return { ok: false }; }); 155 }) 156 .then(function (data) { 157 if (data && data.error === 'expired_nonce' && data.fresh_nonce && !retryWithNonce) { 158 // One-shot retry with the fresh nonce. 159 sbAuthTrack('sb_auth_login_request_response', { sb_auth_response_state: 'expired_nonce' }); 160 var nonceField = form.querySelector('input[name="_wpnonce"]'); 161 if (nonceField) nonceField.value = data.fresh_nonce; 162 setState(IDLE); 163 submitForm(data.fresh_nonce); 164 return; 165 } 166 167 if (data && data.ok) { 168 sbAuthTrack('sb_auth_login_request_response', { sb_auth_response_state: 'ok' }); 169 setState(DONE); 170 showSuccess(); 171 return; 172 } 173 174 // Non-OK response: surface a generic error and reset to idle so 175 // the user can retry. 176 sbAuthTrack('sb_auth_login_request_response', { sb_auth_response_state: 'error' }); 177 setState(IDLE); 178 showError('Something went wrong. Please try again.'); 179 }) 180 .catch(function () { 181 sbAuthTrack('sb_auth_login_request_response', { sb_auth_response_state: 'network_error' }); 182 setState(IDLE); 183 showError('Network error. Please check your connection and try again.'); 184 }); 185 } 186 187 // Form submit handler â gate on IDLE state to prevent double-submit. 188 form.addEventListener('submit', function (event) { 189 event.preventDefault(); 190 submitForm(null); 191 }); 192 193 // Resend after cooldown â re-arms the form, user can edit email, submit. 194 if (resendBtn) { 195 resendBtn.addEventListener('click', function (event) { 196 event.preventDefault(); 197 showForm(); 198 }); 199 } 200 201 // bfcache restore (mobile back button on Safari/Chrome) â reset state so 202 // the form isn't stuck in SUBMITTING with a spinner. 203 window.addEventListener('pageshow', function (event) { 204 if (!event.persisted) return; 205 if (state === SUBMITTING) { 206 setState(IDLE); 207 } 208 }); 209 210 // Initial state â if server rendered ?sent=1, kick off the cooldown timer 211 // so the resend button appears at the right moment. 212 if (state === DONE) { 213 startCooldown(); 214 } 215})(); 216 217/** 218 * Redeem form on /auth/redeem/. 219 * 220 * Default flow: form auto-submits ~350ms after page load (Stytch's 221 * protected_eml pattern, JS-flavored). The brief delay defends against 222 * email scanners that race-prefetch links â most scanners don't execute 223 * JS, so they hit the GET (which doesn't consume the token) and stop.
224 * The user sees "Signing you inâ¦", then lands on /profile/?welcome=1. 225 * 226 * Native POST goes to /wp-json/sb/v1/auth/redeem which sets the auth 227 * cookie on a 200 (defends against Cloudflare APO Set-Cookie strip on 228 * 302 â see workspace memory feedback_cloudflare_strips_set_cookie) and 229 * returns a 302 to /profile/?welcome=1 with the cookie attached. 230 * 231 * No-JS users see the form button labeled "Continue" and click manually. 232 */ 233(function () { 234 'use strict'; 235 236 var form = document.getElementById('sb-auth-redeem-form'); 237 if (!form) { 238 return; 239 } 240 var submitting = false; 241 242 function lockSubmitButton(method) { 243 if (submitting) return false; 244 submitting = true; 245 sbAuthTrack('sb_auth_redeem_submit', { sb_auth_method: method || 'manual' }); 246 var btn = form.querySelector('button[type="submit"]'); 247 if (btn) { 248 btn.disabled = true; 249 btn.setAttribute('aria-busy', 'true'); 250 btn.textContent = btn.dataset.busyText || 'Signing inâ¦'; 251 } 252 return true; 253 } 254 255 form.addEventListener('submit', function () { 256 // If the user manually clicks Continue before auto-submit fires, 257 // method is 'manual'; otherwise auto-submit will have set 'auto' 258 // before dispatching the submit event. 259 lockSubmitButton('manual'); 260 }); 261 262 // Auto-submit after a brief delay so the page can paint and the user 263 // sees "Signing you inâ¦". Delay is short enough to feel seamless, 264 // long enough for any synchronous scanner check to complete on the GET. 265 if (form.dataset.autoSubmit === '1') { 266 var delay = parseInt(form.dataset.autoSubmitDelay, 10) || 350; 267 setTimeout(function () { 268 if (submitting) return; 269 // Fire submit event so our handler (and the browser's native 270 // form POST) goes through. requestSubmit() respects the form's 271 // submit handlers; .submit() bypasses them. 272 sbAuthTrack('sb_auth_redeem_submit', { sb_auth_method: 'auto' }); 273 submitting = true; 274 var btn = form.querySelector('button[type="submit"]'); 275 if (btn) { 276 btn.disabled = true; 277 btn.setAttribute('aria-busy', 'true'); 278 btn.textContent = btn.dataset.busyText || 'Signing inâ¦'; 279 } 280 if (form.requestSubmit) { 281 form.requestSubmit(); 282 } else { 283 form.submit(); 284 } 285 }, delay); 286 } 287 288 // bfcache restore â reset to idle so the back-button doesn't leave the 289 // button stuck in "Signing inâ¦" forever. Auto-submit will re-fire if 290 // the form is still on the page after restore. 291 window.addEventListener('pageshow', function (event) { 292 if (!event.persisted) return; 293 submitting = false; 294 var btn = form.querySelector('button[type="submit"]'); 295 if (btn) { 296 btn.disabled = false; 297 btn.removeAttribute('aria-busy'); 298 btn.textContent = btn.dataset.idleText || 'Continue'; 299 } 300 }); 301})();
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.