Wave 1: kill three trust bugs (billing renewal + namesake collision + Desk copy)
FIX 1 — Honest billing renewal render. VYNDR tiers are monthly, so a `subscription_end` far in the future (the manually-seeded "RENEWS 6/9/2036" founder row) is a comped/lifetime/seed value, not a renewal. New web/src/lib/billingDisplay.js `classifyRenewal()` → date | none | lapsed | unknown (strict Date.parse guard, MONTHLY_RENEWAL_MAX_DAYS=60). Profile page renders the classified label for both the "Renews" stat and the cancel-scheduled "Access ends" line — no raw far-future date. No DB row mutated. FIX 2 — MLB namesake collision (James Wood → "Chicago Cubs"). searchPlayer now collects ALL exact-nameKey matches instead of first-`.find`; a ≥2 collision resolves ONLY via a confident teamHint (the prop's game participants, matched against the cached /teams list with ESPN↔statsapi abbr reconciliation), else refuses (null) — never guesses. The hint threads getPlayerStats → resolvePlayerStats → snapshotService (built from each prop's home/away team). Join invariant: a single-exact player whose team isn't in the hinted game has its team DROPPED (null), so streaks/rosterlogs never tag a foreign team. Full teamHint recovery shipped (not just the refuse fallback). FIX 3 — DeskShowcase headline "A $1M terminal." → deadpan value-showing copy "Every grade, every alt line, live." Prices ($44.99 / $34.99) unchanged. Tests: billingDisplay.test.js (7), mlbNamesakeResolve.test.js (12, disambiguation + join invariant + pure helpers), ds5PricingStates updated to assert the new headline and no "$1M". Full suite green (237 suites / 2863 tests); web `next build` exit 0. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -36,7 +36,7 @@ export default function DeskShowcase() {
|
||||
THE DESK · FLAGSHIP
|
||||
</div>
|
||||
<h2 className="text-balance" style={{ fontSize: 'clamp(30px, 4.4vw, 50px)', fontWeight: 800, letterSpacing: '-0.03em', lineHeight: 1.02, margin: 0 }}>
|
||||
A $1M terminal.{' '}
|
||||
Every grade, every alt line, live.{' '}
|
||||
<span className="mono" style={{ color: 'var(--g-a)' }}>$44.99</span>
|
||||
<span style={{ color: 'var(--text-tertiary)' }}>/mo.</span>
|
||||
</h2>
|
||||
|
||||
@@ -36,7 +36,7 @@ export const metadata: Metadata = {
|
||||
export default function PricingPage() {
|
||||
return (
|
||||
<main style={{ minHeight: '100vh' }}>
|
||||
{/* DS5 (#8) — Desk is the hero. The "$1M terminal" story leads, above the
|
||||
{/* DS5 (#8) — Desk is the hero. The value-showing story leads, above the
|
||||
grid, so the premium tier stops being an afterthought. */}
|
||||
<DeskShowcase />
|
||||
<Pricing />
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useEffect, useState } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useAuth } from '@/contexts/AuthContext';
|
||||
import { currentAccessToken } from '@/lib/authToken';
|
||||
import { classifyRenewal } from '@/lib/billingDisplay';
|
||||
|
||||
interface FullProfile {
|
||||
id: string;
|
||||
@@ -125,7 +126,7 @@ export default function ProfilePage() {
|
||||
{tier !== 'free' && (
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 8 }}>
|
||||
<Stat label="Status" value={profile.subscription_status} tone={profile.subscription_status === 'active' ? 'good' : 'warn'} />
|
||||
<Stat label="Renews" value={profile.subscription_end ? new Date(profile.subscription_end).toLocaleDateString() : '—'} />
|
||||
<Stat label="Renews" value={renewalLabel(profile.subscription_end)} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -170,7 +171,7 @@ export default function ProfilePage() {
|
||||
<section className="surface" style={{ padding: 20, marginBottom: 16, borderColor: 'var(--grade-c)' }}>
|
||||
<p style={{ fontSize: 13, color: 'var(--grade-c)' }}>
|
||||
Cancellation scheduled. Access ends{' '}
|
||||
{profile.subscription_end ? new Date(profile.subscription_end).toLocaleDateString() : 'at period end'}.
|
||||
{accessEndsLabel(profile.subscription_end)}.
|
||||
</p>
|
||||
</section>
|
||||
)}
|
||||
@@ -199,6 +200,25 @@ function Stat({ label, value, tone }: { label: string; value: string; tone?: 'go
|
||||
);
|
||||
}
|
||||
|
||||
// Renewal render is guarded (Wave 1 trust bug) — VYNDR tiers are monthly, so a
|
||||
// `subscription_end` far in the future is a comped/seed value, not a renewal.
|
||||
// classifyRenewal decides; we never print a raw date the cadence can't justify.
|
||||
function renewalLabel(subscriptionEnd: string | null): string {
|
||||
const r = classifyRenewal(subscriptionEnd);
|
||||
if (r.kind === 'date' && r.iso) return new Date(r.iso).toLocaleDateString();
|
||||
if (r.kind === 'none') return 'No scheduled renewal';
|
||||
if (r.kind === 'lapsed') return 'Lapsed';
|
||||
return '—';
|
||||
}
|
||||
|
||||
// The cancel-scheduled line only shows a real near date; anything else falls to
|
||||
// the honest "at period end" (never a fabricated 2036 access-end).
|
||||
function accessEndsLabel(subscriptionEnd: string | null): string {
|
||||
const r = classifyRenewal(subscriptionEnd);
|
||||
if (r.kind === 'date' && r.iso) return new Date(r.iso).toLocaleDateString();
|
||||
return 'at period end';
|
||||
}
|
||||
|
||||
function tierColor(tier: string): string {
|
||||
if (tier === 'desk') return 'var(--grade-a)';
|
||||
if (tier === 'analyst') return 'var(--grade-b)';
|
||||
|
||||
Reference in New Issue
Block a user