diff --git a/web/src/app/providers.tsx b/web/src/app/providers.tsx index 67576f1..b3819f3 100644 --- a/web/src/app/providers.tsx +++ b/web/src/app/providers.tsx @@ -4,6 +4,7 @@ import { useEffect } from 'react'; import { usePathname } from 'next/navigation'; import { AuthProvider } from '@/lib/auth-context'; import { initTelemetry, trackPageView } from '@/lib/telemetry'; +import { initFeatureFlags } from '@/lib/feature-flags'; import type { ReactNode } from 'react'; export function Providers({ children }: { children: ReactNode }) { @@ -11,6 +12,7 @@ export function Providers({ children }: { children: ReactNode }) { useEffect(() => { initTelemetry(); + initFeatureFlags({ platform: 'web' }); }, []); useEffect(() => { diff --git a/web/src/lib/feature-flags.ts b/web/src/lib/feature-flags.ts new file mode 100644 index 0000000..7294d11 --- /dev/null +++ b/web/src/lib/feature-flags.ts @@ -0,0 +1,70 @@ +/** + * Feature flag client — polls platform-service /flags/poll endpoint. + * + * Flags are fetched on init and cached in memory. Re-polls on a configurable + * interval (default 5 min). Consumers call `isEnabled('flag_key')`. + * + * Privacy: sends userId + platform only. No PII. + */ + +import { PRODUCT_ID } from './auth-api'; + +const PLATFORM_URL = process.env.NEXT_PUBLIC_PLATFORM_SERVICE_URL ?? 'https://api.chronomind.app'; +const POLL_INTERVAL_MS = 5 * 60 * 1000; // 5 minutes + +let _flags: Record = {}; +let _initialized = false; +let _intervalId: ReturnType | null = null; + +interface PollParams { + userId?: string; + platform?: string; +} + +async function fetchFlags(params: PollParams): Promise> { + try { + const qs = new URLSearchParams(); + if (params.userId) qs.set('userId', params.userId); + if (params.platform) qs.set('platform', params.platform); + const res = await fetch(`${PLATFORM_URL}/api/flags/poll?${qs.toString()}`, { + headers: { 'x-product-id': PRODUCT_ID }, + }); + if (!res.ok) return _flags; + const data = await res.json(); + return (data as { flags: Record }).flags ?? {}; + } catch { + return _flags; + } +} + +/** + * Initialize the flag client. Call once on app startup. + * Fetches flags immediately and starts polling. + */ +export async function initFeatureFlags(params: PollParams = {}): Promise { + if (_initialized) return; + _initialized = true; + const pollParams: PollParams = { platform: 'web', ...params }; + _flags = await fetchFlags(pollParams); + _intervalId = setInterval(async () => { + _flags = await fetchFlags(pollParams); + }, POLL_INTERVAL_MS); +} + +/** Check if a feature flag is enabled. Returns false if not found or not initialized. */ +export function isEnabled(key: string): boolean { + return _flags[key] === true; +} + +/** Get all currently cached flags. */ +export function getAllFlags(): Readonly> { + return _flags; +} + +/** Stop polling and reset state. Useful for tests. */ +export function resetFeatureFlags(): void { + if (_intervalId) clearInterval(_intervalId); + _intervalId = null; + _flags = {}; + _initialized = false; +}