/** * Tier-gating helpers (Session 19). * * Shared, declarative answers to the questions every list-rendering * component asks: "should this user see everything, or a teaser?" * Centralized so we don't drift across components (one screen * showing 3 rows + upgrade hint, another showing 5 rows + lock * icon, a third showing all rows with a partial blur — that path * leads to chaos). * * The actual SECURITY boundary on tier-locked data lives on the * server (the API routes that produce these lists already filter by * the bearer token's tier). These helpers govern presentation only, * matching the server-supplied limit so the UI doesn't promise more * than the API will deliver. * * Free users see top 3. Paid (analyst + desk) see everything. * Africa tier (entry-level subscription) also sees everything for * lists; gradient features live behind canSeeGradeDetails. */ export type Tier = 'free' | 'africa' | 'analyst' | 'desk' | string; const PAID_TIERS: ReadonlySet = new Set(['analyst', 'desk']); const FULL_LIST_TIERS: ReadonlySet = new Set(['africa', 'analyst', 'desk']); const FREE_VISIBLE_COUNT = 3; export function canSeeFullLists(tier: Tier): boolean { if (!tier) return false; return FULL_LIST_TIERS.has(String(tier).toLowerCase()); } export function canSeeGradeDetails(tier: Tier): boolean { if (!tier) return false; return PAID_TIERS.has(String(tier).toLowerCase()); } export function getVisibleCount(tier: Tier, totalCount: number): number { if (totalCount <= 0) return 0; if (canSeeFullLists(tier)) return totalCount; return Math.min(FREE_VISIBLE_COUNT, totalCount); } /** * Inverse helper for UX copy — "Upgrade to see N more results." * Returns 0 when the tier already sees everything (so the upsell * line can short-circuit without arithmetic). */ export function getHiddenCount(tier: Tier, totalCount: number): number { if (canSeeFullLists(tier)) return 0; return Math.max(0, totalCount - FREE_VISIBLE_COUNT); }