// /lib/support/memberships.ts export type MembershipRow = { id: string | number; provider: "kofi" | "patreon" | "mighty" | string; status: "active" | "one_time" | "canceled" | "inactive" | string; tier?: string | null; started_at?: string | null; renews_at?: string | null; email?: string | null; username?: string | null; app_user?: string | null; }; export type SupportBadge = { provider: "kofi" | "patreon" | "mighty" | string; active: boolean; // true if currently entitled (status==="active" && renews_at >= now) kind: "member" | "one_time" | "inactive"; label: string; // e.g., "Ko-fi • Bronze" or "Ko-fi Supporter" tier?: string | null; renews_at?: string | null; started_at?: string | null; }; const DIRECTUS = (process.env.NEXT_PUBLIC_API_BASE_URL || "").replace(/\/$/, ""); /** Active if status==="active" and renews_at >= now */ function computeActive(row: MembershipRow): boolean { if (row.status !== "active") return false; if (!row.renews_at) return false; const due = new Date(row.renews_at).getTime(); return Number.isFinite(due) && due >= Date.now(); } function providerLabel(p: string) { if (p === "kofi") return "Ko-fi"; if (p === "patreon") return "Patreon"; if (p === "mighty") return "Mighty"; return p[0]?.toUpperCase() + p.slice(1); } /** Convert a membership row → displayable badge */ function rowToBadge(row: MembershipRow): SupportBadge { const active = computeActive(row); const prov = providerLabel(row.provider); let kind: SupportBadge["kind"] = "inactive"; let label = prov; if (active) { kind = "member"; label = row.tier ? `${prov} • ${row.tier}` : `${prov} Member`; } else if (row.status === "one_time") { kind = "one_time"; label = `${prov} Supporter`; } else { kind = "inactive"; label = `${prov} (inactive)`; } return { provider: row.provider, active, kind, label, tier: row.tier ?? null, renews_at: row.renews_at ?? null, started_at: row.started_at ?? null, }; } /** * Fetch all memberships for a user by app_user (preferred) or by email. * Pass either/both: { userId?, email? } */ export async function fetchMembershipBadges(opts: { userId?: string; email?: string }): Promise { const clauses: any[] = [{ provider: { _in: ["kofi", "patreon", "mighty"] } }]; if (opts.userId) clauses.push({ app_user: { _eq: opts.userId } }); if (opts.email) clauses.push({ email: { _eq: opts.email.toLowerCase() } }); // Build filter: provider IN (...) AND (app_user = userId OR email = email) const filter = encodeURIComponent( JSON.stringify({ _and: [ clauses[0], { _or: clauses.slice(1).length ? clauses.slice(1) : [{ id: { _neq: null } }] }, ], }) ); const res = await fetch(`${DIRECTUS}/items/user_memberships?filter=${filter}&limit=500`, { cache: "no-store" }); const json = await res.json().catch(() => ({} as any)); const rows: MembershipRow[] = json?.data || []; const badges = rows.map(rowToBadge); // Sort: active first, then provider name asc badges.sort((a, b) => { if (a.active !== b.active) return a.active ? -1 : 1; return a.provider.localeCompare(b.provider); }); return badges; } /** Quick boolean if any active membership exists */ export async function hasActiveMembership(opts: { userId?: string; email?: string }): Promise { const list = await fetchMembershipBadges(opts); return list.some(b => b.kind === "member" && b.active); }