'use client'; import { useState } from 'react'; import { useRouter } from 'next/navigation'; import { useAuth } from '@/contexts/AuthContext'; import { useT, useRegion } from '@/contexts/LocaleContext'; type TierId = 'free' | 'africa' | 'analyst' | 'desk'; interface TierConfig { id: TierId; name: string; price: string; originalPrice?: string; cadence: string; badge?: string; headline: string; cta: string; features: string[]; locked: string[]; highlight: boolean; } const TIERS: TierConfig[] = [ { id: 'free', name: 'Free', price: '$0', cadence: '/mo', headline: 'Try the model. No card required.', cta: 'Start Free', features: [ '3 reads per day', 'Grade letter + projection', 'Cross-book line comparison', 'Confidence indicator', ], locked: [ 'Factor analysis (blurred)', 'Kill conditions (blurred)', 'Alt line ladder (locked)', ], highlight: false, }, // Session 12 — VYNDR Africa tier ($4.99/mo). Slotted between Free // and Analyst. Pricing component reorders dynamically based on // locale (African-language users see this first). { id: 'africa', name: 'VYNDR Africa', price: '$4.99', cadence: '/mo', headline: 'Built for African mobile bettors.', cta: 'Unlock Africa Pricing', features: [ '10 reads per day', 'Full factor analysis (40+ signals)', 'Kill conditions surfaced inline', 'Grade + reasoning visible', 'World Cup soccer intelligence', ], locked: [ 'Cascade alerts (Analyst+)', 'Alt line ladder (Desk only)', ], highlight: false, }, { id: 'analyst', name: 'Analyst', price: '$14.99', originalPrice: '$24.99', cadence: '/mo', badge: 'Founder Access', headline: 'The full intelligence layer.', cta: 'Lock Founder Price', features: [ 'Unlimited reads', 'Full factor analysis (40+ signals)', 'Kill conditions surfaced inline', 'Cascade alerts when lineups shift', 'Parlay leg history with grades', 'Sportsbook deep links', ], locked: [ 'Alt line ladder (Desk only)', 'Kelly sizing (Desk only)', ], highlight: true, }, { id: 'desk', name: 'Desk', price: '$44.99', originalPrice: '$49.99', cadence: '/mo', headline: 'Everything. The professional setup.', cta: 'Go Desk', features: [ 'Everything in Analyst', 'Alt line ladder + edge ranking', 'Quarter-Kelly sizing recommendations', 'Real-time intelligence feed', 'Parlay correlation analysis (phi)', 'Consensus vs model comparison', ], locked: [], highlight: false, }, ]; export default function Pricing() { const router = useRouter(); const { session, loading: authLoading } = useAuth(); const { inAfrica } = useRegion(); const t = useT(); const [pending, setPending] = useState(null); const [error, setError] = useState(null); // Session 13 — Africa tier visibility + order is now driven by // REAL IP geolocation via Cloudflare's CF-IPCountry header (stamped // onto x-vyndr-country by the middleware). The previous locale- // based proxy (Swahili speakers everywhere) was both too narrow // (most African users browse in English/French) and too broad // (Swahili speakers outside Africa got the discount). // // Inside Africa: VYNDR Africa renders first, then Free, then Analyst, Desk. // Outside Africa: the Africa tier card is filtered out of the render // entirely — no path for non-African users to even // see the $4.99 option. // Unknown country (local dev, non-Cloudflare): degrades closed → // Africa tier hidden (same as outside Africa). const orderedTiers = inAfrica ? [TIERS.find((x) => x.id === 'africa')!, TIERS.find((x) => x.id === 'free')!, TIERS.find((x) => x.id === 'analyst')!, TIERS.find((x) => x.id === 'desk')!] : TIERS.filter((x) => x.id !== 'africa'); async function startCheckout(tier: TierId) { setError(null); // Free tier short-circuits — no checkout, just signup. if (tier === 'free') { router.push('/signup'); return; } // Session 15 — Africa short-circuit removed. The Session 14 // backend now handles 'africa' end-to-end: validation accepts // it, and when STRIPE_PRICE_AFRICA isn't configured the route // returns 503 { code: 'tier_unconfigured', error: '...' } which // the existing error-display path below surfaces inline. No // special-case needed at the UI layer. // Anonymous → bounce to signup with a returnTo back to /#pricing. if (!session) { router.push('/signup?return=/%23pricing'); return; } setPending(tier); try { const res = await fetch('/api/checkout', { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${session.access_token}`, }, body: JSON.stringify({ tier }), }); const data = (await res.json().catch(() => ({}))) as { url?: string; error?: string }; if (!res.ok || !data.url) { setError(data.error || 'Checkout creation failed. Try again in a moment.'); setPending(null); return; } // Hand off to Stripe. The success_url returns the user to // /upgrade/success?session_id=… — no further client work needed. window.location.assign(data.url); } catch { setError('Network error. Try again.'); setPending(null); } } return (

Pricing built for bettors. Not for SaaS investors.

First 100 users lock $14.99/mo for life. Beta pricing — this price dies at user 101.

{error && (
{error}
)}
{orderedTiers.map((tier, i) => { const isPending = pending === tier.id; const isDisabled = authLoading || (pending !== null && !isPending); return (
{tier.badge && (
{tier.badge}
)}

{tier.name}

{tier.price} {tier.cadence} {tier.originalPrice && ( {tier.originalPrice} )}

{tier.headline}

    {tier.features.map((f) => (
  • + {f}
  • ))} {tier.locked.map((f) => (
  • {f}
  • ))}
); })}

Cancel anytime. No contracts. Card / Apple Pay / Google Pay — payments processed by Stripe. First 100 users lock $14.99/mo Analyst for life.

); }