'use client'; import { useEffect, useState } from 'react'; import { useRouter } from 'next/navigation'; import { useAuth } from '@/contexts/AuthContext'; import { currentAccessToken } from '@/lib/authToken'; interface FullProfile { id: string; email: string; tier: 'free' | 'analyst' | 'desk'; scan_count: number; scan_reset_date: string; subscription_status: string; subscription_end: string | null; founder_pricing: boolean; cancel_at_period_end: boolean; } export default function ProfilePage() { const router = useRouter(); const { user, tier: authTier, signOut, loading: authLoading } = useAuth(); const [profile, setProfile] = useState(null); const [working, setWorking] = useState(false); const [error, setError] = useState(''); useEffect(() => { if (!authLoading && !user) router.replace('/login?next=/profile'); }, [authLoading, user, router]); useEffect(() => { if (!user) return; const token = currentAccessToken(); fetch('/api/user/profile', { headers: token ? { Authorization: `Bearer ${token}` } : {}, }) .then((r) => r.json()) .then(setProfile) .catch(() => setProfile(null)); }, [user]); const handleCancel = async () => { if (!confirm('Cancel your subscription at the end of the current period?')) return; setWorking(true); setError(''); const token = currentAccessToken(); const res = await fetch('/api/user/profile', { method: 'PUT', headers: { 'Content-Type': 'application/json', ...(token ? { Authorization: `Bearer ${token}` } : {}), }, body: JSON.stringify({ cancel_at_period_end: true }), }); setWorking(false); if (res.ok) { const updated = await res.json(); setProfile((p) => (p ? { ...p, ...updated } : p)); } else { const body = await res.json().catch(() => ({})); setError(body.error || 'Could not update — try again in a moment.'); } }; if (authLoading || !user || !profile) { return (

Loading profile…

); } // Session 41 — tier is read from useAuth (the same live-session source the // nav uses) so the two can't disagree. The /api/user/profile fetch can lag // or return a stale/null tier; the auth context is authoritative. Profile // still supplies the other fields (scan_count, subscription_*, founder). const tier = authTier || profile.tier || 'free'; return (

Profile

{profile.email}

{/* Tier card */}

YOUR TIER

{/* Session 27 — always render a tier label. When the profile API returns null/undefined tier (free users sometimes do), fall back to 'free' so the field is never blank. */}

{tier} {profile.founder_pricing && ( FOUNDER )}

{tier === 'free' && ( Upgrade )}
{tier !== 'free' && (
)} {tier === 'free' && ( )}
{/* Founder pricing promo for free users */} {tier === 'free' && (

FOUNDER ACCESS

Lock $14.99/mo for life before it's gone.

First 100 users keep founder pricing even after the regular price jumps to $24.99.

Lock founder price →
)} {/* Subscription actions */} {tier !== 'free' && !profile.cancel_at_period_end && (

Cancel subscription

Your access continues through the end of the current billing period. No refunds for partial months.

{error &&

{error}

}
)} {profile.cancel_at_period_end && (

Cancellation scheduled. Access ends{' '} {profile.subscription_end ? new Date(profile.subscription_end).toLocaleDateString() : 'at period end'}.

)} {/* Sign out */}
); } function Stat({ label, value, tone }: { label: string; value: string; tone?: 'good' | 'warn' }) { const color = tone === 'good' ? 'var(--grade-a)' : tone === 'warn' ? 'var(--grade-c)' : 'var(--text-primary)'; return (
{label.toUpperCase()}
{value}
); } function tierColor(tier: string): string { if (tier === 'desk') return 'var(--grade-a)'; if (tier === 'analyst') return 'var(--grade-b)'; return 'var(--text-primary)'; }