S3 (a1): affiliate + partner plumbing

Zero out-of-pocket; everything config-flip-ready but DISABLED/organic.

- BOOK IT deep links: web/src/lib/bookLinks.js + affiliateConfig.js
  (all books enabled:false, Impact/Partnerize param shapes documented,
  empty params skipped). Wired into StatStrip BookItTeaser (real anchor
  now) + scan hand-off links. Every book anchor renders
  rel="sponsored noopener noreferrer" (BOOK_LINK_REL).
- Best-price marker: slateAdapter.detectBestBook (only when >=2 books
  post the SAME line and prices differ — absent beats wrong) + subtle
  signal-green dot in StatStrip. Slate.groupByGame threads the grouped
  per-book rows (books[]) onto PropRowProp instead of discarding them.
- Partner refs: ?ref=CODE -> vyndr_ref cookie (90d, first-touch,
  PartnerRefCapture in layout) -> signup metadata partner_ref ->
  internal GET /api/partners/report/:code (requireInternalAuth; honest
  zeros + note until the TODO migration in docs/PARTNERS.md adds
  user_profiles.partner_ref — NOT run). Stripe promo-code convention:
  partner code == promotion code, verbatim.
- Tests: +41 (2398 -> 2439, 209 suites); bookItTeaser + vyndrCoreScreens
  invariants updated to the new (stronger) rel contract. Web build exit 0.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Kev
2026-07-11 14:28:18 -04:00
parent e4d2e79f95
commit 0996320bd1
23 changed files with 1151 additions and 36 deletions
+3
View File
@@ -9,6 +9,7 @@ import Footer from '@/components/Footer';
import AuthGate from '@/components/AuthGate';
import HashRedirect from '@/components/vyndr/HashRedirect';
import GlobalHosts from '@/components/vyndr/GlobalHosts';
import PartnerRefCapture from '@/components/vyndr/PartnerRefCapture';
// Session 50 — the Parlay Lab supersedes the legacy ParlayTray.
import ParlayPanel from '@/components/vyndr/ParlayPanel';
import BottomTabBar from '@/components/BottomTabBar';
@@ -156,6 +157,8 @@ export default async function RootLayout({ children }: { children: React.ReactNo
<SentryInit />
{/* Session 38 — prefs apply + paywall/checkout/prefs globals (§9/§10/§12) */}
<GlobalHosts />
{/* A1 S3 — first-visit ?ref=CODE → vyndr_ref cookie (90d, first-touch) */}
<PartnerRefCapture />
</ParlayProvider>
</ExplainModeProvider>
</AuthProvider>
+19 -21
View File
@@ -17,6 +17,7 @@ import {
trackUpgradeClicked,
} from '@/lib/analytics';
import { getHeadshotUrl, PLAYER_SILHOUETTE, type HeadshotSport } from '@/lib/playerHeadshot';
import { buildBookLink, SUPPORTED_BOOKS, BOOK_LINK_REL } from '@/lib/bookLinks';
type Sport = 'NBA' | 'MLB' | 'WNBA';
@@ -103,15 +104,8 @@ const SPORT_ACCENT: Record<Sport, string> = {
WNBA: '#FFB347',
};
// Sportsbook deep-links — preserved from the legacy GradeCard so the new
// design keeps the book hand-off. target=_blank + noopener,noreferrer.
const SPORTSBOOKS = [
{ id: 'draftkings', label: 'DraftKings', host: 'sportsbook.draftkings.com' },
{ id: 'fanduel', label: 'FanDuel', host: 'sportsbook.fanduel.com' },
{ id: 'betmgm', label: 'BetMGM', host: 'sports.betmgm.com' },
{ id: 'caesars', label: 'Caesars', host: 'sportsbook.caesars.com' },
];
const deepLink = (host: string, player: string) => `https://${host}/?search=${encodeURIComponent(player)}`;
// Sportsbook deep-links — A1 S3: built by lib/bookLinks (organic until the
// affiliate config flips a book on). rel is BOOK_LINK_REL on every anchor.
export default function ScanPage() {
const router = useRouter();
@@ -775,18 +769,22 @@ export default function ScanPage() {
{/* Sportsbook hand-off (preserved feature) */}
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 8, justifyContent: 'center' }}>
{SPORTSBOOKS.map((b) => (
<a
key={b.id}
href={deepLink(b.host, selectedPlayer)}
target="_blank"
rel="noopener noreferrer"
className="mono"
style={{ padding: '7px 13px', fontSize: 11, fontWeight: 700, borderRadius: 6, border: '1px solid var(--border-hi)', color: 'var(--text-1)', textDecoration: 'none' }}
>
{b.label}
</a>
))}
{SUPPORTED_BOOKS.map((b) => {
const link = buildBookLink({ book: b.id, player: selectedPlayer, sport });
if (!link) return null;
return (
<a
key={b.id}
href={link.url}
target="_blank"
rel={BOOK_LINK_REL}
className="mono"
style={{ padding: '7px 13px', fontSize: 11, fontWeight: 700, borderRadius: 6, border: '1px solid var(--border-hi)', color: 'var(--text-1)', textDecoration: 'none' }}
>
{b.label}
</a>
);
})}
</div>
{/* Free-tier nudge — full paywall treatment returns in Phase G */}