PageSourceSearch

https://styleblueprint.com/wp-content/plugins/sb-auth/assets/js/sb-auth.js?ver=1778213300

js styleblueprint.com collected 2026-10-02 04:00:32 UTC 10,898 bytes, 301 lines download raw bytes

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.