/* global React, AppShell, Eyebrow, foyerApi, foyerLoad, useFoyerData */
// Two surfaces:
// /#/kiosk → agent-facing preview + "Launch kiosk" button.
// /#/kiosk-live → guest-facing form, no sidebar, no nav, hardened
// against back-button + hash navigation, designed to
// be handed to a walk-in guest in a separate tab.
// ============================================================
// /#/kiosk — agent-facing setup
// ============================================================
const KioskForm = () => {
const { user, summaries } = useFoyerData();
const recentAddress = pickRecentAddress(summaries);
const launch = () => {
const url = '/#/kiosk-live';
// Open in a new tab so the agent's dashboard stays put. The guest's
// tab is fully sandboxed by the live route's hash trap.
window.open(url, '_blank', 'noopener');
};
return (
Hand to a guest
Kiosk sign-in.
Launches a locked-down, full-screen sign-in in a new tab. Guests can fill
in their info but can't navigate back to your dashboard — no sidebar, no
back button, no link out.
Address shown to guests
{recentAddress || No recent session — guests will see a generic welcome.}
Pulled from your most recent recorded session. To change it, start a new
session at a different address from the iOS app first.
);
};
function Tip({ num, title, body }) {
return (
);
}
function pickRecentAddress(summaries) {
const recorded = (summaries || [])
.filter(s => (s.kind || 'recorded') !== 'manual')
.sort((a, b) => (b.created_at || '').localeCompare(a.created_at || ''));
return recorded[0]?.address || '';
}
// ============================================================
// /#/kiosk-live — guest-facing, locked-down full-screen form
// ============================================================
//
// Hardened so a walk-in can't escape:
// - No AppShell sidebar, no route-nav, no Open House Copilot-crest link out
// - popstate listener pushes state forward whenever back is pressed
// - hashchange listener forces the hash back to #/kiosk-live
// - No internal links other than the Sign in button
//
// The agent's session cookie still rides along automatically (same origin),
// so leads land under their account.
const KioskLive = () => {
const { user, summaries } = useFoyerData();
const recentAddress = pickRecentAddress(summaries);
// Trap back-button + URL hash changes so the guest can't navigate out.
React.useEffect(() => {
if (typeof window === 'undefined') return;
// Push an extra entry so the first Back press just re-fires popstate
// here instead of leaving the page.
try { window.history.pushState({ kiosk: true }, '', '#/kiosk-live'); } catch (e) {}
const onPopState = () => {
try { window.history.pushState({ kiosk: true }, '', '#/kiosk-live'); } catch (e) {}
};
const onHash = () => {
if (window.location.hash !== '#/kiosk-live') {
window.location.hash = '#/kiosk-live';
}
};
window.addEventListener('popstate', onPopState);
window.addEventListener('hashchange', onHash);
return () => {
window.removeEventListener('popstate', onPopState);
window.removeEventListener('hashchange', onHash);
};
}, []);
const [name, setName] = React.useState('');
const [email, setEmail] = React.useState('');
const [phone, setPhone] = React.useState('');
const [tag, setTag] = React.useState('Buyer');
const [submitting, setSubmitting] = React.useState(false);
const [thanksFor, setThanksFor] = React.useState(null);
const [err, setErr] = React.useState(null);
const [signedInCount, setSignedInCount] = React.useState(0);
const reset = () => {
setName(''); setEmail(''); setPhone(''); setTag('Buyer'); setErr(null);
};
const submit = async (e) => {
e?.preventDefault?.();
if (!name.trim()) { setErr('Name is required.'); return; }
setSubmitting(true); setErr(null);
try {
await foyerApi.post('/leads', {
name: name.trim(),
email: email.trim(),
phone: phone.trim(),
tag,
address: recentAddress || undefined,
});
// Don't bother refreshing foyerLoad here — this tab won't show
// the inbox anyway, and refreshing would slow each sign-in down.
const first = name.trim().split(' ')[0];
setThanksFor({ name: first, at: Date.now() });
setSignedInCount(c => c + 1);
reset();
setTimeout(() => setThanksFor(null), 2200);
} catch (e2) {
setErr(e2.message || String(e2));
} finally {
setSubmitting(false);
}
};
return (
{/* Decorative gold radial behind the form. */}
{/* Brand mark — text only, NOT a link, so guests can't tap to home. */}
F
Open House Copilot
{signedInCount > 0 ? `${signedInCount} SIGNED IN TODAY` : 'OPEN HOUSE · GUEST SIGN-IN'}
{/* Recording disclosure ribbon — surfaced before any contact-info
fields so guests have a clear notice that the open house is
being recorded. Required for two-party-consent states; safe
everywhere. */}
This open house is being recorded
{thanksFor && (
Thanks
See you around, {thanksFor.name}.
Hand the laptop back when you're ready.
)}
OPEN HOUSE COPILOT · RECORDED FOR THE AGENT'S NOTES · NEVER SOLD
);
};
function KioskField({ label, value, onChange, placeholder, type = 'text', autoFocus = false, required = false }) {
return (
);
}
Object.assign(window, { KioskForm, KioskLive });