// ============================================================================ // Presenter passcode gate — protects ONLY the mini-CRM (not the whole app). // ---------------------------------------------------------------------------- // The Studio boots straight into its normal home/experience with NO passcode — // a salesperson presenting does not sign in just to use the tablet. // // The mini-CRM (today's walk-ins = sensitive customer data) opens by TRIPLE- // CLICKING the centre Aurora One logo on Home. On triple-click, if no valid // presenter is stored, this on-brand 4-digit PIN pad appears FIRST. The PIN is // validated by the relay (POST /api/studio/validate → studio_validate_passcode // RPC). On success the presenter {user_id, name, passcode} is stored in // sessionStorage (uni_presenter_v1) and the mini-CRM opens. A stored presenter // means triple-click opens the mini-CRM directly (no re-ask this session); a // "lock/switch presenter" affordance in the StudioBar clears it. // // There is NO mobile-number entry anywhere — selecting a customer card in the // mini-CRM and starting their journey is what links them to the salesperson. // data-crm="1" so the presenter's own taps never pollute the customer journey. // ============================================================================ (function () { const SKEY = 'uni_presenter_v1'; // Same-origin: the relay is proxied at /api/studio/* on whichever host serves // this page (vega.khosha.tech, century.thephygital.studio, …). Never hard-code a host. const VALIDATE_URL = '/api/studio/validate'; function loadPresenter() { try { return JSON.parse(sessionStorage.getItem(SKEY)) || null; } catch (e) { return null; } } function savePresenter(p) { try { sessionStorage.setItem(SKEY, JSON.stringify(p)); } catch (e) {} } function clearPresenter() { try { sessionStorage.removeItem(SKEY); } catch (e) {} // "Lock / switch presenter" was used → this session wants the real PIN pad, // so the demo auto-unlock below must stop re-opening the door behind them. try { sessionStorage.setItem(MKEY, '1'); } catch (e) {} } function signedIn() { const p = loadPresenter(); return !!(p && p.passcode); } // ── Demo auto-unlock ────────────────────────────────────────────────────── // The demo link goes to prospects who click through on their own, so a cold // visitor must never meet the PIN wall. On load, with no presenter stored, we // sign in with the demo PIN through EXACTLY the path a human uses: POST it to // /api/studio/validate and store whatever that success flow stores. We never // fabricate a local session — the relay re-checks the passcode server-side on // every /api/studio/* call, so a faked one would 401 on walkins and look // broken. If this fails (relay down, PIN not seeded) nothing is stored and the // manual PIN screen below takes over as before. const MKEY = 'uni_presenter_manual_v1'; // set once the user asks to switch presenter const DEMO_PIN = '1111'; let autoInflight = null; function manualOnly() { try { return sessionStorage.getItem(MKEY) === '1'; } catch (e) { return false; } } function validate(pin) { return fetch(VALIDATE_URL, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ passcode: pin }), }).then((res) => res.json().catch(() => null)); } // Resolves with the presenter on success, or null (never rejects) so every // caller can simply fall back to the manual gate. function autoUnlock() { const cur = loadPresenter(); if (cur && cur.passcode) return Promise.resolve(cur); if (manualOnly()) return Promise.resolve(null); if (autoInflight) return autoInflight; autoInflight = validate(DEMO_PIN) .then((data) => { if (!data || !data.ok) return null; const info = { user_id: data.user_id, name: data.name || 'Presenter', passcode: DEMO_PIN, at: Date.now(), auto: true }; savePresenter(info); // Nudge the StudioBar (it re-renders on 'storage') so the presenter chip // appears immediately — same-tab sessionStorage writes fire no event. try { window.dispatchEvent(new Event('storage')); } catch (e) {} return info; }) .catch(() => null) .then((info) => { autoInflight = null; return info; }); // allow one retry later return autoInflight; } // Shared helper store so home.jsx / minicrm.jsx agree on the presenter. window.UNI_PRESENTER = { get: loadPresenter, save: savePresenter, clear: clearPresenter, signedIn, autoUnlock, passcode() { const p = loadPresenter(); return p && p.passcode ? p.passcode : ''; }, }; // Kick the auto-unlock off at boot so the session is usually ready long before // anyone triple-taps the logo; openMiniCrm() awaits this same promise anyway. window.UNI_PRESENTER.ready = autoUnlock(); function Keypad({ value, onChange, onSubmit, disabled }) { const press = (d) => { if (disabled) return; if (d === 'back') { onChange(value.slice(0, -1)); return; } if (d === 'clear') { onChange(''); return; } if (value.length >= 4) return; const next = value + d; onChange(next); if (next.length === 4) setTimeout(() => onSubmit(next), 120); }; const keys = ['1','2','3','4','5','6','7','8','9','clear','0','back']; return (