- react-native-platform-sdk: add tsconfig.json + 13 source files (core, auth, telemetry, feature-flags, kill-switch, broadcasts, surveys) - react-native-platform-sdk: React hooks + providers wrapping platform-service APIs via fetch - admin-web: fix ThemeEditor.tsx + themes/active/route.ts lysnrai token type access - tracker-web: product-context import path fix (pre-existing)
84 lines
2.2 KiB
TypeScript
84 lines
2.2 KiB
TypeScript
/**
|
|
* Feature Flags module — React context + hook for feature flags in React Native apps.
|
|
*/
|
|
|
|
import React, { createContext, useContext, useState, useCallback, useEffect } from 'react';
|
|
import type { PlatformSDK } from '../core.js';
|
|
|
|
export interface FeatureFlag {
|
|
key: string;
|
|
enabled: boolean;
|
|
value?: unknown;
|
|
}
|
|
|
|
interface FeatureFlagContextType {
|
|
flags: Map<string, FeatureFlag>;
|
|
isEnabled: (key: string) => boolean;
|
|
getValue: <T = unknown>(key: string, fallback: T) => T;
|
|
refresh: () => Promise<void>;
|
|
}
|
|
|
|
const FeatureFlagContext = createContext<FeatureFlagContextType | null>(null);
|
|
|
|
export function useFeatureFlags(): FeatureFlagContextType {
|
|
const ctx = useContext(FeatureFlagContext);
|
|
if (!ctx) throw new Error('useFeatureFlags must be used within a FeatureFlagProvider');
|
|
return ctx;
|
|
}
|
|
|
|
interface FeatureFlagProviderProps {
|
|
sdk: PlatformSDK;
|
|
/** Poll interval in ms (default: 60000) */
|
|
pollInterval?: number;
|
|
children: React.ReactNode;
|
|
}
|
|
|
|
export function FeatureFlagProvider({
|
|
sdk,
|
|
pollInterval = 60_000,
|
|
children,
|
|
}: FeatureFlagProviderProps): React.JSX.Element {
|
|
const [flags, setFlags] = useState<Map<string, FeatureFlag>>(new Map());
|
|
|
|
const refresh = useCallback(async () => {
|
|
try {
|
|
const res = await sdk.fetch('/api/flags/poll');
|
|
if (res.ok) {
|
|
const data = (await res.json()) as FeatureFlag[];
|
|
const map = new Map<string, FeatureFlag>();
|
|
for (const flag of data) {
|
|
map.set(flag.key, flag);
|
|
}
|
|
setFlags(map);
|
|
}
|
|
} catch {
|
|
/* fail-open: keep existing flags */
|
|
}
|
|
}, [sdk]);
|
|
|
|
const isEnabled = useCallback(
|
|
(key: string): boolean => {
|
|
return flags.get(key)?.enabled ?? false;
|
|
},
|
|
[flags]
|
|
);
|
|
|
|
const getValue = useCallback(
|
|
<T = unknown>(key: string, fallback: T): T => {
|
|
const flag = flags.get(key);
|
|
if (!flag?.enabled) return fallback;
|
|
return (flag.value as T) ?? fallback;
|
|
},
|
|
[flags]
|
|
);
|
|
|
|
useEffect(() => {
|
|
refresh();
|
|
const id = setInterval(refresh, pollInterval);
|
|
return () => clearInterval(id);
|
|
}, [refresh, pollInterval]);
|
|
|
|
const value: FeatureFlagContextType = { flags, isEnabled, getValue, refresh };
|
|
return React.createElement(FeatureFlagContext.Provider, { value }, children);
|
|
}
|