0996320bd1
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>
95 lines
3.5 KiB
JavaScript
95 lines
3.5 KiB
JavaScript
// A1 Session 3 — partner ref capture: ?ref=CODE → first-party vyndr_ref
|
|
// cookie (90d, first-touch) → signup metadata partner_ref.
|
|
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
const {
|
|
REF_COOKIE,
|
|
sanitizeRefCode,
|
|
parseRefFromSearch,
|
|
readRefCookie,
|
|
buildRefCookie,
|
|
captureRef,
|
|
} = require('../../web/src/lib/partnerRef');
|
|
|
|
const WEB = path.join(__dirname, '..', '..', 'web', 'src');
|
|
const read = (rel) => fs.readFileSync(path.join(WEB, rel), 'utf8');
|
|
|
|
describe('sanitizeRefCode', () => {
|
|
it('uppercases and accepts A-Z 0-9 - _', () => {
|
|
expect(sanitizeRefCode('hoopspod')).toBe('HOOPSPOD');
|
|
expect(sanitizeRefCode(' the-wire_22 ')).toBe('THE-WIRE_22');
|
|
});
|
|
it('rejects empty, too-long, and unsafe codes', () => {
|
|
expect(sanitizeRefCode('')).toBeNull();
|
|
expect(sanitizeRefCode(null)).toBeNull();
|
|
expect(sanitizeRefCode('a'.repeat(33))).toBeNull();
|
|
expect(sanitizeRefCode('bad code')).toBeNull();
|
|
expect(sanitizeRefCode('<script>')).toBeNull();
|
|
expect(sanitizeRefCode('a;b')).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe('parseRefFromSearch / readRefCookie / buildRefCookie', () => {
|
|
it('parses ?ref= out of a search string', () => {
|
|
expect(parseRefFromSearch('?ref=hoopspod&utm_source=x')).toBe('HOOPSPOD');
|
|
expect(parseRefFromSearch('?utm_source=x')).toBeNull();
|
|
expect(parseRefFromSearch('')).toBeNull();
|
|
});
|
|
it('builds a 90-day first-party cookie string', () => {
|
|
const c = buildRefCookie('hoopspod');
|
|
expect(c).toContain(`${REF_COOKIE}=HOOPSPOD`);
|
|
expect(c).toContain(`Max-Age=${90 * 24 * 60 * 60}`);
|
|
expect(c).toContain('Path=/');
|
|
expect(c).toContain('SameSite=Lax');
|
|
expect(c).not.toContain('Secure');
|
|
expect(buildRefCookie('hoopspod', { secure: true })).toContain('; Secure');
|
|
expect(buildRefCookie('bad code')).toBeNull();
|
|
});
|
|
it('reads the cookie back out of a document.cookie string', () => {
|
|
expect(readRefCookie('foo=1; vyndr_ref=HOOPSPOD; bar=2')).toBe('HOOPSPOD');
|
|
expect(readRefCookie('foo=1; bar=2')).toBeNull();
|
|
expect(readRefCookie('')).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe('captureRef — first touch wins', () => {
|
|
const fakeWindow = (search, cookie = '') => {
|
|
const doc = { cookie };
|
|
return { location: { search, protocol: 'https:' }, document: doc };
|
|
};
|
|
|
|
it('sets the cookie on a first visit with ?ref=', () => {
|
|
const w = fakeWindow('?ref=hoopspod');
|
|
expect(captureRef(w)).toBe('HOOPSPOD');
|
|
expect(w.document.cookie).toContain('vyndr_ref=HOOPSPOD');
|
|
expect(w.document.cookie).toContain('; Secure'); // https origin
|
|
});
|
|
|
|
it('does NOT overwrite an existing ref cookie (first touch)', () => {
|
|
const w = fakeWindow('?ref=newpartner', 'vyndr_ref=ORIGINAL');
|
|
expect(captureRef(w)).toBeNull();
|
|
expect(w.document.cookie).toBe('vyndr_ref=ORIGINAL');
|
|
});
|
|
|
|
it('no-ops without a ref param or without a window', () => {
|
|
const w = fakeWindow('?utm_source=x');
|
|
expect(captureRef(w)).toBeNull();
|
|
expect(w.document.cookie).toBe('');
|
|
expect(captureRef(null)).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe('wiring (source-asserted)', () => {
|
|
it('PartnerRefCapture is mounted in the layout (GlobalHosts pattern)', () => {
|
|
const layout = read('app/layout.tsx');
|
|
expect(layout).toContain('<PartnerRefCapture />');
|
|
expect(layout).toContain("from '@/components/vyndr/PartnerRefCapture'");
|
|
});
|
|
it('signUp forwards the cookie as partner_ref signup metadata', () => {
|
|
const auth = read('contexts/AuthContext.tsx');
|
|
expect(auth).toContain('readRefCookie(document.cookie)');
|
|
expect(auth).toContain('partner_ref: partnerRef');
|
|
});
|
|
});
|