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}
)}
);
}