import { useState, useEffect, useCallback } from 'react'; import { apiUrl, authHeaders, assertOk } from '../../lib/api'; import { SpinnerIcon } from '../ui/icons/SpinnerIcon'; import { CheckIcon } from '../ui/icons/CheckIcon'; import type { AuthUser } from '../../hooks/useAuth'; interface InvitePageProps { code: string; user: AuthUser | null; onLoginClick: () => void; onRegisterClick: () => void; onLicenseGranted: () => void; } interface InviteInfo { valid: boolean; invite_type: string; used: boolean; } export default function InvitePage({ code, user, onLoginClick, onRegisterClick, onLicenseGranted, }: InvitePageProps) { 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); 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('Failed to validate invite link'); } finally { if (!cancelled) setLoading(false); } })(); return () => { cancelled = true; }; }, [code]); 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 (loading) { return (
); } if (error && !invite) { return (

Invalid invite

{error}

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

{invite?.used ? 'Invite already used' : 'Invalid invite link'}

{invite?.used ? 'This invite link has already been redeemed.' : 'This invite link is invalid or has expired.'}

); } if (redeemed) { return (

License activated!

You now have full access to Perfect Postcode.

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

{isAdminInvite ? "You're invited!" : 'Special offer!'}

{isAdminInvite ? 'You have been invited to get free lifetime access.' : 'A friend has shared a 30% discount on lifetime access.'}

{isAdminInvite && (
Free lifetime access
)} {!isAdminInvite && pricePence !== null && pricePence > 0 && (
{`\u00A3${pricePence / 100}`} {`\u00A3${(Math.round(pricePence * 0.7) / 100).toFixed(2)}`} /once
)} {user ? ( ) : (
)} {error && (

{error}

)}
); }