Session 8: Frontend Stripe cutover, soccer pages, sport selector, grade result cards, beta badge
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user