DS1 (design): speed + trust bugs
Fixes the three DESIGN-SPEC Part 4 + #17 audit findings. 1. React #418 hydration mismatch (landing → dashboard entry). The `maybeSignedIn` value was computed in a useState INITIALIZER that reads localStorage during render: server (no window) → false → emits the marketing tree; a signed-in visitor's first CLIENT render → true → emits the loading placeholder. Whole-subtree server/client mismatch → React discarded and re-rendered the page. Deferred behind a mounted flag so the first client render matches the server; the stored-session check flips post-mount. SSR HTML is no longer discarded. 2. Loading walls → skeletons. New tokenized Skeleton primitive (.vyndr-skeleton, reduced-motion-safe via the global rule). Swapped into every text-wall loader: dashboard slate load ("Loading the slate…"), /desk ("Assembling the pack…"), /ledger ("Loading…"), scan ("Loading the model…"), and the landing redirect placeholder. No bare text loader remains. 3. scan→ledger persistence. Root cause: the scan page read its bearer token from localStorage['sb-token'] — a key written ONLY by the OAuth callback — so email/password users posted /api/scan anonymously and the ledger write (gated on an authed user) was silently skipped. Now uses the authoritative session.access_token (matching the ledger read path). Extracted the row builder to web/src/lib/ledgerRow.js (shared, testable). Tests: +17 (scanLedgerPersistence write→mine round-trip + scope + idempotency; ds1SpeedTrust hydration/skeleton/persistence source invariants). Full suite 233 suites / 2793 green; web build exit 0. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -4,7 +4,7 @@ import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import ProcessingGrade from '@/components/vyndr/ProcessingGrade';
|
||||
import PriorReads from '@/components/vyndr/PriorReads';
|
||||
import { AccuracyBadge } from '@/components/vyndr';
|
||||
import { AccuracyBadge, Skeleton, SkeletonList } from '@/components/vyndr';
|
||||
import type { GradeResultData } from '@/components/vyndr/GradeResultCard';
|
||||
import { mapScanToGradeResult } from '@/lib/gradeAdapter';
|
||||
import { normalizeName, nameKey } from '@/lib/playerName';
|
||||
@@ -110,7 +110,7 @@ const SPORT_ACCENT: Record<Sport, string> = {
|
||||
|
||||
export default function ScanPage() {
|
||||
const router = useRouter();
|
||||
const { user, tier, scansRemaining, canScan, loading: authLoading, bumpScanCount } = useAuth();
|
||||
const { user, session, tier, scansRemaining, canScan, loading: authLoading, bumpScanCount } = useAuth();
|
||||
const { addLeg, legCount, open } = useParlay();
|
||||
|
||||
const [sport, setSport] = useState<Sport>('NBA');
|
||||
@@ -241,7 +241,16 @@ export default function ScanPage() {
|
||||
setError('');
|
||||
setResult(null);
|
||||
try {
|
||||
const token = typeof window !== 'undefined' ? localStorage.getItem('sb-token') : null;
|
||||
// DS1 (§17 — scan→ledger persistence). The authoritative bearer token is
|
||||
// the live Supabase session's access_token (set for EVERY sign-in method).
|
||||
// The legacy `localStorage['sb-token']` key is written ONLY by the OAuth
|
||||
// callback — so email/password users sent NO Authorization header, the
|
||||
// /api/scan route saw an anonymous request, and the completed read was
|
||||
// silently dropped from the ledger (the write is gated on an authed user).
|
||||
// Prefer the session token; keep the legacy key as a fallback.
|
||||
const token =
|
||||
session?.access_token ||
|
||||
(typeof window !== 'undefined' ? localStorage.getItem('sb-token') : null);
|
||||
const res = await fetch('/api/scan', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
@@ -301,9 +310,13 @@ export default function ScanPage() {
|
||||
};
|
||||
|
||||
if (authLoading || !user) {
|
||||
// DS1 (§4) — layout-matched skeleton of the scan form, not a text wall.
|
||||
return (
|
||||
<section style={{ minHeight: '80vh', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
||||
<p className="mono" style={{ color: 'var(--text-tertiary)' }}>Loading the model…</p>
|
||||
<section style={{ maxWidth: 720, margin: '0 auto', padding: '32px 16px 96px' }} aria-busy="true">
|
||||
<Skeleton height={30} width={200} radius={8} style={{ marginBottom: 8 }} />
|
||||
<Skeleton height={16} width={320} radius={6} style={{ marginBottom: 28 }} />
|
||||
<SkeletonList count={3} height={64} />
|
||||
<Skeleton height={52} radius={12} style={{ marginTop: 20 }} />
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user