/** * CLIENT-SAFE referral-code capture. * * ## Why a cookie, and why it outlives the visit * * A referral link is `https://your-store.com/?ref=` (the shape the * dashboard builds around `LoyaltyStatus.referralCode`). It lands on the home * page, and almost nobody registers on that first page view: they browse, they * leave, they come back. If the code lives in component state it is gone the * moment they click a product, the referrer never gets credited, and the * shopper never gets the welcome reward they were promised. So it is persisted * the instant it arrives and read back at registration, however much later. * * Same reasoning and same shape as `region.ts`: not `httpOnly`, because a * client component both writes it and reads it, and it holds a public share * code that was already sitting in the URL. */ import type { ReferralInfo } from 'brainerce'; export const REFERRAL_COOKIE = 'brainerce_referral'; /** * Thirty days. Long enough to cover browse-then-return-later, short enough that * a code does not silently attach itself to an account created months later by * a different person on a shared machine. */ const REFERRAL_COOKIE_MAX_AGE = 60 * 60 * 24 * 30; /** Codes are `REF-XXXXXXXX` today; the cap leaves room for that to change. */ const MAX_REFERRAL_CODE_LENGTH = 64; /** * Accept only a plausible share code. * * The value comes from the query string, which anybody can write anything into. * It ends up in a cookie and in a registration payload, so it is charset- and * length-checked here rather than trusted: `encodeURIComponent` already stops a * `;` from splitting the cookie, and this stops the rest of the nonsense from * being stored at all. An unknown-but-well-formed code is not rejected here on * purpose; `getReferralInfo()` is the authority on whether it is real. */ export function normalizeReferralCode(raw: string | null | undefined): string | null { if (!raw) return null; const code = raw.trim(); if (!code || code.length > MAX_REFERRAL_CODE_LENGTH) return null; if (!/^[A-Za-z0-9_-]+$/.test(code)) return null; return code; } /** Read the captured referral code in the browser. Returns null during SSR. */ export function readReferralCookie(): string | null { if (typeof document === 'undefined') return null; const match = document.cookie.match(new RegExp(`(?:^|;\\s*)${REFERRAL_COOKIE}=([^;]*)`)); return match ? normalizeReferralCode(decodeURIComponent(match[1])) : null; } /** * Persist a captured referral code. `SameSite=Lax` so the code survives the * click in from an email, a social post or a messaging app, which is where * referral links actually live; `Secure` only off localhost, where there is no * https to attach it to. */ export function writeReferralCookie(code: string): void { if (typeof document === 'undefined') return; const safe = normalizeReferralCode(code); if (!safe) return; const secure = typeof location !== 'undefined' && location.protocol === 'https:'; document.cookie = `${REFERRAL_COOKIE}=${encodeURIComponent(safe)}; path=/; max-age=${REFERRAL_COOKIE_MAX_AGE}; SameSite=Lax` + (secure ? '; Secure' : ''); } /** * Drop the captured code. Called once registration succeeds: the code has been * spent, and leaving it behind would attach the same referrer to the next * account created in this browser. */ export function clearReferralCookie(): void { if (typeof document === 'undefined') return; document.cookie = `${REFERRAL_COOKIE}=; path=/; max-age=0; SameSite=Lax`; } /** * Whether a referral lookup is worth greeting the visitor about. * * `getReferralInfo()` answers `{ valid: false }` for an unknown code, a * disabled referral program and an inactive program alike, so this is the only * gate that matters after the call. A valid referral with no configured reward * is still worth showing: the referrer's name is the reason the visitor * clicked. */ export function isGreetableReferral(info: ReferralInfo | null): info is ReferralInfo { return info !== null && info.valid; }