Session 13: The Slate, Africa geo-restriction, OAuth providers, PropRow + GameCard (1311 tests)

This commit is contained in:
Kev
2026-06-11 03:48:07 -04:00
parent d957dee17b
commit 10159209fa
18 changed files with 1452 additions and 64 deletions
+117 -1
View File
@@ -4,7 +4,123 @@
2026-06-10
## Current Phase
SHIP BUILD v12.0 — i18n (10 languages) + Africa tier (Session 12)
SHIP BUILD v13.0 — The Slate (browse-first dashboard) + OAuth providers + Africa geo (Session 13)
## Session 13 (2026-06-11) — SHIPPED
### Phase 1 — Africa geo-restriction via CF-IPCountry
The Session 12 Africa tier was visible to anyone on a Swahili locale
(too narrow: most African users browse in English/French; too broad:
Swahili speakers anywhere got the discount). Session 13 swaps the
locale proxy for real Cloudflare IP geolocation.
- **`web/middleware.ts`** — reads `cf-ipcountry` (uppercase),
stamps `x-vyndr-country` on the request alongside the locale header.
Empty string when traffic bypasses Cloudflare (local dev).
- **`web/src/lib/locales.ts`** — `AFRICAN_COUNTRIES` set covering all
54 sovereign African nations (NG/KE/ZA/GH + sub-Saharan + MENA
overlap). `isAfricanCountry(code)` is case-insensitive and degrades
closed on empty/null inputs.
- **`LocaleContext`** — extended with `country`/`inAfrica` fields;
new `useRegion()` hook for components that gate by geography.
- **`Pricing.tsx`** — `inAfrica === false` filters the Africa tier
out of the render entirely. `inAfrica === true` puts it first.
Locale-based reorder removed.
- **Pricing grid CSS** — desktop column count now tracks the visible
tier count via a `--pricing-cols` CSS custom property on the grid
root (3 outside Africa, 4 inside). Sidesteps a styled-jsx
limitation with attribute selectors inside `:global()`.
### Phase 2 — OAuth: Google + Apple + X
- **`AuthContext`** — added generic `signInWithProvider(provider)`
alongside the legacy `signInWithGoogle()` (kept as an alias so
existing callers don't break). Translates Supabase OAuth errors
into a flat `{ error: string }` so the UI can surface a friendly
inline message when a provider isn't configured.
- **`login/page.tsx` + `signup/page.tsx`** — both pages now render
three OAuth buttons (Google, Apple, X). The `handleOAuth` helper
routes to `signInWithProvider` and shows an inline error when the
provider isn't configured ("apple login isn't available yet. Use
email or another method.").
- **External configuration required** (operator action, not code):
- Supabase Auth → Providers → Apple: needs an Apple Developer
Service ID + private key
- Supabase Auth → Providers → Twitter: needs an X Developer OAuth 2.0
client
- Google should already work — if it doesn't, verify Supabase
Auth → URL Configuration → Site URL = https://vyndr.app and
Redirect URLs include `https://vyndr.app/**`, and that the Google
Cloud Console OAuth consent screen has the Supabase callback URL
in Authorized redirect URIs.
### Phase 3 — The Slate (browse-first dashboard)
Generalizes the Session 8 `/soccer` page pattern across every sport.
- **`web/src/components/PropRow.tsx`** — single-prop UI with three
states (ungraded/grading/graded). Pure presentational — parent
owns the API call so there's one shared rate-limited grading queue.
Free-tier expansion shows blurred reasoning + Unlock CTA; paid tier
shows full reasoning + kill conditions. Exports `propRowKey()` for
stable Map keys.
- **`web/src/components/GameCard.tsx`** — game header + expandable
prop list. Sport emoji prefix (🏀 NBA/WNBA, ⚾ MLB, ⚽ soccer),
sport-accented left border, formatted local game time, `+ N more`
expander when props > defaultVisible.
- **`web/src/components/Slate.tsx`** — the orchestrator. Sport tabs
(ALL / NBA / WNBA / MLB / Soccer), sticky search input, group-by-game
pipeline, `gradedProps` Map, single-flight grading queue
(`gradingKey`). `Promise.allSettled` fan-out for the ALL tab so a
single sport failing doesn't blank the slate. `FETCH_URLS` is
null-aware — sports without an odds proxy yet (WNBA, MLB) render a
bottom-of-page "endpoint not configured yet" note rather than
spamming 404s.
- **Search filter + manual-scan fallback** — sticky search filters
game cards by team name and prop rows by player/stat. Empty result
shows a CTA linking to `/scan?q=<query>` so users land on a
partially-filled scan form.
- **`/dashboard`** — `<Slate />` mounted as the lead surface above
the existing Top Graded / Most Parlayed / Recent Reads sections.
Those sections stay as supplementary intelligence layers — not
removed.
- **`Nav.tsx`** — "Scan" link removed from primary nav. The Slate is
the scan surface; `/scan` stays reachable from the slate's
empty-state CTA.
### Tests added
| Suite | Tests |
|----------------------------------------|-------|
| `tests/unit/africaCountries.test.js` | 6 |
| **Session 13 total** | **6** |
### Quality gates
- `npm test`: **1311 / 1311 passing** (1305 + 6 new), 102 suites, 0 regressions
- `web/npm run build`: clean — Slate page + components prerender
- License audit: third-party deps remain permissive
### Honest gaps (documented, not bugs)
- I could not visually verify The Slate in a browser. Build/type
correctness is confirmed; "renders correctly with live odds data"
needs a deploy smoke test.
- Google/Apple/X OAuth: button wiring is complete. Whether the
buttons actually authenticate depends on external dashboard
configuration (Supabase + Google Cloud Console + Apple Developer +
X Developer Portal). Apple and X are guaranteed to show the
"isn't available yet" inline error until configured.
- WNBA + MLB don't have `/api/odds/*` proxies on the Next.js side
yet. The Slate degrades cleanly (footer note), but those tabs
return empty until the proxies exist. Session-14 work.
- Africa tier still can't be SOLD even when geo gates open it —
the Stripe price + the DB CHECK migration remain outstanding from
Session 12.
### Coolify env (Session 13 additions)
None. CF-IPCountry is set by Cloudflare automatically; no env-var
change required.
---
## Session 12 (2026-06-11) — SHIPPED