import { useState, useEffect, useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { apiUrl, authHeaders, assertOk } from '../../lib/api'; import { SpinnerIcon } from '../ui/icons/SpinnerIcon'; import { CheckIcon } from '../ui/icons/CheckIcon'; import HexCanvas from '../home/HexCanvas'; import type { AuthUser } from '../../hooks/useAuth'; interface InvitePageProps { code: string; user: AuthUser | null; theme: 'light' | 'dark'; screenshotMode?: boolean; onLoginClick: () => void; onRegisterClick: () => void; onLicenseGranted: () => void; } interface InviteInfo { valid: boolean; invite_type: string; used: boolean; invited_by: string | null; } const CONFETTI_COLORS = ['#10b981', '#06b6d4', '#f59e0b', '#ef4444', '#8b5cf6', '#ec4899']; function Confetti() { const particles = useMemo( () => Array.from({ length: 40 }, (_, i) => ({ id: i, left: Math.random() * 100, delay: Math.random() * 2, duration: 2 + Math.random() * 2, color: CONFETTI_COLORS[Math.floor(Math.random() * 6)], size: 6 + Math.random() * 6, isCircle: Math.random() > 0.5, })), [] ); return (
{particles.map((p) => (
))}
); } export default function InvitePage({ code, user, theme, screenshotMode = false, onLoginClick, onRegisterClick, onLicenseGranted, }: InvitePageProps) { const { t } = useTranslation(); const [invite, setInvite] = useState(null); const [loading, setLoading] = useState(true); const [redeeming, setRedeeming] = useState(false); const [error, setError] = useState(null); const [redeemed, setRedeemed] = useState(false); const [pricePence, setPricePence] = useState(null); const isDark = theme === 'dark'; // Signal screenshot readiness once loading completes and a frame has painted useEffect(() => { if (screenshotMode && !loading) { requestAnimationFrame(() => { requestAnimationFrame(() => { window.__screenshot_ready = true; }); }); } }, [screenshotMode, loading]); useEffect(() => { let cancelled = false; (async () => { try { const [inviteRes, pricingRes] = await Promise.all([ fetch(apiUrl(`invite/${encodeURIComponent(code)}`)), fetch(apiUrl('pricing')), ]); if (!inviteRes.ok) throw new Error('Failed to validate invite'); const data: InviteInfo = await inviteRes.json(); if (!cancelled) setInvite(data); if (pricingRes.ok) { const pricing = await pricingRes.json(); if (!cancelled) setPricePence(pricing.current_price_pence); } } catch { if (!cancelled) setError(t('invitePage.failedToValidate')); } finally { if (!cancelled) setLoading(false); } })(); return () => { cancelled = true; }; }, [code, t]); const handleRedeem = useCallback(async () => { if (!user) return; setRedeeming(true); setError(null); try { const res = await fetch(apiUrl('redeem-invite'), { method: 'POST', ...authHeaders({ headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ code }), }), }); assertOk(res, 'Redeem invite'); const data = await res.json(); if (data.result === 'licensed') { setRedeemed(true); onLicenseGranted(); } else if (data.checkout_url) { window.location.href = data.checkout_url; } } catch (err) { setError(err instanceof Error ? err.message : 'Failed to redeem invite'); } finally { setRedeeming(false); } }, [code, user, onLicenseGranted]); if (screenshotMode && loading) { return (
); } if (screenshotMode) { const isAdminInvite = invite?.valid && !invite.used && invite.invite_type === 'admin'; const isValid = invite?.valid && !invite.used; return (

{isValid ? isAdminInvite ? t('invitePage.youreInvited') : t('invitePage.specialOffer') : t('header.appName')}

{isValid && invite.invited_by ? isAdminInvite ? t('invitePage.invitedByFree', { name: invite.invited_by }) : t('invitePage.invitedByDiscount', { name: invite.invited_by }) : isValid ? isAdminInvite ? t('invitePage.genericFreeInvite') : t('invitePage.genericDiscount') : t('invitePage.exploreEvery')}

{isValid && !isAdminInvite && pricePence !== null && pricePence > 0 && (
{`\u00A3${pricePence / 100}`} {`\u00A3${(Math.round(pricePence * 0.7) / 100).toFixed(2)}`} {t('upgrade.once')}
)}

{t('invitePage.propertyInfo')}

); } if (loading) { return (
); } if (error && !invite) { return (

{t('invitePage.invalidInvite')}

{error}

); } if (!invite?.valid || invite.used) { return (

{invite?.used ? t('invitePage.inviteAlreadyUsed') : t('invitePage.invalidInviteLink')}

{invite?.used ? t('invitePage.inviteAlreadyUsedDesc') : t('invitePage.invalidInviteLinkDesc')}

); } if (redeemed) { return (

{t('invitePage.licenseActivated')}

{t('invitePage.fullAccessGranted')}

); } const isAdminInvite = invite.invite_type === 'admin'; return (

{isAdminInvite ? t('invitePage.youreInvited') : t('invitePage.specialOffer')}

{invite.invited_by ? isAdminInvite ? t('invitePage.invitedByFree', { name: invite.invited_by }) : t('invitePage.invitedByDiscount', { name: invite.invited_by }) : isAdminInvite ? t('invitePage.genericFreeInvite') : t('invitePage.genericDiscount')}

{!isAdminInvite && pricePence !== null && pricePence > 0 && (
{`\u00A3${pricePence / 100}`} {`\u00A3${(Math.round(pricePence * 0.7) / 100).toFixed(2)}`} {t('upgrade.once')}
)} {user?.subscription === 'licensed' ? (

{t('invitePage.youAlreadyHaveLicense')}

{t('invitePage.accountHasFullAccess')}

) : user ? ( ) : (
)} {error && (

{error}

)}
); }