Session 8: Frontend Stripe cutover, soccer pages, sport selector, grade result cards, beta badge

This commit is contained in:
Kev
2026-06-10 15:34:23 -04:00
parent ad5ea8d5a8
commit 4db1c1c539
15 changed files with 1583 additions and 161 deletions
+39
View File
@@ -0,0 +1,39 @@
import Link from 'next/link';
/**
* Stripe cancel landing — Stripe sends users here when they bail out
* of checkout. We don't gate, judge, or guilt; just acknowledge and
* point them back to pricing.
*/
export default function UpgradeCancelPage() {
return (
<main style={{ minHeight: '100vh', display: 'flex', alignItems: 'center', justifyContent: 'center', padding: 24 }}>
<article
className="surface diagonal-cut"
style={{
maxWidth: 560,
width: '100%',
padding: 40,
textAlign: 'center',
border: '1px solid var(--border)',
background: 'var(--bg-surface)',
}}
>
<h1 style={{ fontSize: 28, fontWeight: 700, letterSpacing: '-0.02em', marginBottom: 12 }}>
Checkout cancelled.
</h1>
<p style={{ fontSize: 15, color: 'var(--text-secondary)', marginBottom: 32 }}>
Your account is unchanged. No card was charged.
</p>
<div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
<Link href="/#pricing" className="btn-primary" style={{ padding: 14, width: '100%' }}>
Back to pricing
</Link>
<Link href="/scan" className="btn-ghost" style={{ padding: 14, width: '100%' }}>
Keep using the free tier
</Link>
</div>
</article>
</main>
);
}
+106
View File
@@ -0,0 +1,106 @@
'use client';
import { Suspense, useEffect } from 'react';
import Link from 'next/link';
import { useSearchParams } from 'next/navigation';
import { useAuth } from '@/contexts/AuthContext';
/**
* Stripe checkout success landing.
*
* Stripe redirects to /upgrade/success?session_id={CHECKOUT_SESSION_ID}
* after a completed checkout. Webhook events (handled server-side in
* `src/services/stripeService.handleWebhookEvent`) write the user's
* new tier into Supabase — there's nothing for the client to do here
* besides confirm the redirect and refresh the auth context so the
* new tier shows up on subsequent reads.
*
* We deliberately do NOT verify the session against Stripe from the
* client (no secret key on the browser). The webhook is the source of
* truth; this page just acknowledges the user landed.
*/
function SuccessInner() {
const search = useSearchParams();
const sessionId = search.get('session_id');
const { refresh, profile } = useAuth();
useEffect(() => {
// Force a profile re-read so the new tier flips in the UI without
// requiring a manual sign-out/in.
refresh();
}, [refresh]);
const tierLabel = profile?.tier === 'desk' ? 'Desk' : profile?.tier === 'analyst' ? 'Analyst' : '';
return (
<main style={{ minHeight: '100vh', display: 'flex', alignItems: 'center', justifyContent: 'center', padding: '24px' }}>
<article
className="surface diagonal-cut"
style={{
maxWidth: 560,
width: '100%',
padding: 40,
textAlign: 'center',
border: '1px solid var(--grade-a)',
background: 'var(--bg-elevated)',
boxShadow: '0 16px 48px var(--accent-glow)',
}}
>
<div
className="mono"
style={{
display: 'inline-block',
padding: '4px 12px',
background: 'var(--grade-a)',
color: 'var(--bg-primary)',
fontSize: 10,
fontWeight: 800,
letterSpacing: '0.08em',
borderRadius: 999,
textTransform: 'uppercase',
marginBottom: 24,
}}
>
Founder Pricing Locked
</div>
<h1 style={{ fontSize: 32, fontWeight: 700, letterSpacing: '-0.02em', marginBottom: 12 }}>
Welcome to VYNDR{tierLabel ? ` ${tierLabel}` : ''}.
</h1>
<p style={{ fontSize: 16, color: 'var(--text-secondary)', marginBottom: 8 }}>
Your beta pricing is locked for as long as you stay subscribed.
</p>
<p style={{ fontSize: 14, color: 'var(--text-tertiary)', marginBottom: 32 }}>
Cancel anytime in your profile.
</p>
{sessionId && (
<p
className="mono"
style={{ fontSize: 11, color: 'var(--text-tertiary)', marginBottom: 24, wordBreak: 'break-all' }}
>
Session: {sessionId}
</p>
)}
<div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
<Link href="/scan" className="btn-primary" style={{ padding: 14, width: '100%' }}>
Start scanning
</Link>
<Link href="/profile" className="btn-ghost" style={{ padding: 14, width: '100%' }}>
View account
</Link>
</div>
</article>
</main>
);
}
export default function UpgradeSuccessPage() {
return (
<Suspense fallback={<main style={{ padding: 40, textAlign: 'center' }}>Loading</main>}>
<SuccessInner />
</Suspense>
);
}