Session 12: i18n (10 languages, cookie-based), Africa tier .99, locale switcher, RTL Arabic (1305 tests)
This commit is contained in:
@@ -3,8 +3,10 @@
|
||||
import { useState } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useAuth } from '@/contexts/AuthContext';
|
||||
import { useT, useLocale } from '@/contexts/LocaleContext';
|
||||
import { AFRICA_LOCALES } from '@/lib/locales';
|
||||
|
||||
type TierId = 'free' | 'analyst' | 'desk';
|
||||
type TierId = 'free' | 'africa' | 'analyst' | 'desk';
|
||||
|
||||
interface TierConfig {
|
||||
id: TierId;
|
||||
@@ -29,7 +31,7 @@ const TIERS: TierConfig[] = [
|
||||
headline: 'Try the model. No card required.',
|
||||
cta: 'Start Free',
|
||||
features: [
|
||||
'5 reads per month',
|
||||
'3 reads per day',
|
||||
'Grade letter + projection',
|
||||
'Cross-book line comparison',
|
||||
'Confidence indicator',
|
||||
@@ -41,6 +43,29 @@ const TIERS: TierConfig[] = [
|
||||
],
|
||||
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',
|
||||
@@ -88,9 +113,22 @@ const TIERS: TierConfig[] = [
|
||||
export default function Pricing() {
|
||||
const router = useRouter();
|
||||
const { session, loading: authLoading } = useAuth();
|
||||
const { locale } = useLocale();
|
||||
const t = useT();
|
||||
const [pending, setPending] = useState<TierId | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
// Session 12 — Africa-language users see VYNDR Africa first. The
|
||||
// tier order is stable per locale (no flicker between renders).
|
||||
// Browser region (NG / KE / ZA / GH) isn't available server-side
|
||||
// without IP geolocation, so we use the locale as a proxy. Users
|
||||
// outside the locale set can still pick the Africa tier; it just
|
||||
// doesn't lead the card grid for them.
|
||||
const orderedTiers = AFRICA_LOCALES.has(locale)
|
||||
? [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;
|
||||
|
||||
async function startCheckout(tier: TierId) {
|
||||
setError(null);
|
||||
|
||||
@@ -100,6 +138,16 @@ export default function Pricing() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Session 12 — Africa tier: Stripe product + backend validation
|
||||
// not yet wired (intentional this session). Show an honest
|
||||
// "coming soon" instead of a 400. When STRIPE_PRICE_AFRICA is
|
||||
// configured AND the backend accepts the tier, this short-circuit
|
||||
// gets removed and the standard checkout path takes over.
|
||||
if (tier === 'africa') {
|
||||
setError('VYNDR Africa launches once Stripe regional processing is finalized. Email support@vyndr.app to lock the $4.99/mo founder price.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Anonymous → bounce to signup with a returnTo back to /#pricing.
|
||||
if (!session) {
|
||||
router.push('/signup?return=/%23pricing');
|
||||
@@ -171,7 +219,7 @@ export default function Pricing() {
|
||||
)}
|
||||
|
||||
<div className="pricing-grid" style={{ display: 'grid', gap: 24 }}>
|
||||
{TIERS.map((tier, i) => {
|
||||
{orderedTiers.map((tier, i) => {
|
||||
const isPending = pending === tier.id;
|
||||
const isDisabled = authLoading || (pending !== null && !isPending);
|
||||
return (
|
||||
@@ -270,7 +318,15 @@ export default function Pricing() {
|
||||
}
|
||||
@media (min-width: 768px) {
|
||||
:global(.pricing-grid) {
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
/* Session 12 — Africa tier brings the count to 4. On
|
||||
tablet we stay 2-up so cards don't squeeze; desktop
|
||||
unfolds to 4-up at >=1100px. */
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
}
|
||||
@media (min-width: 1100px) {
|
||||
:global(.pricing-grid) {
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
}
|
||||
}
|
||||
`}</style>
|
||||
|
||||
Reference in New Issue
Block a user