From 5151846a886f909dc8625c483905ef7571587373 Mon Sep 17 00:00:00 2001 From: FragginWagon Date: Wed, 28 Jan 2026 22:53:43 +0000 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20Add=20composable=20for=20managing?= =?UTF-8?q?=20feature=20flags?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/composables/useFeatureFlags.js | 188 ++++++++++++++++++ 1 file changed, 188 insertions(+) create mode 100644 code/websites/pokedex.online/src/composables/useFeatureFlags.js diff --git a/code/websites/pokedex.online/src/composables/useFeatureFlags.js b/code/websites/pokedex.online/src/composables/useFeatureFlags.js new file mode 100644 index 0000000..5e259bc --- /dev/null +++ b/code/websites/pokedex.online/src/composables/useFeatureFlags.js @@ -0,0 +1,188 @@ +/** + * Feature Flags Composable + * + * Manages runtime feature flag state with support for: + * - Local overrides (developer mode) + * - Permission-based flags (requires auth) + * - Backend feature flag queries (future) + * + * Usage: + * ```javascript + * const { isEnabled, toggle, getFlags } = useFeatureFlags(); + * if (isEnabled('experimental-search')) { ... } + * ``` + */ + +import { ref, computed, readonly } from 'vue'; +import { useAuth } from './useAuth.js'; +import { FEATURE_FLAGS, getFlag, getFlagPermission } from '../config/feature-flags.js'; + +// Local storage key for overrides +const LOCAL_OVERRIDES_KEY = 'feature_flag_overrides'; + +/** + * Load flag overrides from localStorage + * @returns {Object} Overrides object + */ +function loadLocalOverrides() { + try { + const stored = localStorage.getItem(LOCAL_OVERRIDES_KEY); + return stored ? JSON.parse(stored) : {}; + } catch { + return {}; + } +} + +/** + * Save flag overrides to localStorage + * @param {Object} overrides - Overrides to save + */ +function saveLocalOverrides(overrides) { + try { + localStorage.setItem(LOCAL_OVERRIDES_KEY, JSON.stringify(overrides)); + } catch { + console.warn('Failed to save feature flag overrides'); + } +} + +// Shared state +const localOverrides = ref(loadLocalOverrides()); +const backendFlags = ref({}); + +/** + * useFeatureFlags Composable + * + * @returns {Object} Feature flags interface + * - isEnabled(flagName) - Check if flag is enabled + * - toggle(flagName) - Toggle flag override in dev mode + * - reset(flagName) - Clear override for specific flag + * - resetAll() - Clear all overrides + * - getFlags() - Get all flags with status + * - setBackendFlags(flags) - Set flags from backend response + */ +export function useFeatureFlags() { + const { user, hasPermission } = useAuth(); + + /** + * Check if a feature flag is enabled + * Checks in order: local override, permission requirement, default value + */ + const isEnabled = computed(() => { + return (flagName) => { + // Check local override first + if (flagName in localOverrides.value) { + return localOverrides.value[flagName]; + } + + // Get flag definition + const flag = getFlag(flagName); + if (!flag) { + console.warn(`Feature flag not found: ${flagName}`); + return false; + } + + // Check permission requirement + if (flag.requiredPermission && !hasPermission(flag.requiredPermission)) { + return false; + } + + // Check backend flag (if available) + if (flagName in backendFlags.value) { + return backendFlags.value[flagName]; + } + + // Use default + return flag.enabled; + }; + }); + + /** + * Toggle a flag override (dev mode only) + * Only works in development mode + */ + const toggle = (flagName) => { + if (process.env.NODE_ENV !== 'development') { + console.warn('Feature flag overrides only available in development mode'); + return false; + } + + const current = localOverrides.value[flagName] !== undefined + ? localOverrides.value[flagName] + : getFlag(flagName)?.enabled ?? false; + + localOverrides.value[flagName] = !current; + saveLocalOverrides(localOverrides.value); + + return !current; + }; + + /** + * Reset a specific flag override + */ + const reset = (flagName) => { + if (process.env.NODE_ENV !== 'development') { + console.warn('Feature flag overrides only available in development mode'); + return; + } + + delete localOverrides.value[flagName]; + saveLocalOverrides(localOverrides.value); + }; + + /** + * Reset all flag overrides + */ + const resetAll = () => { + if (process.env.NODE_ENV !== 'development') { + console.warn('Feature flag overrides only available in development mode'); + return; + } + + localOverrides.value = {}; + saveLocalOverrides({}); + }; + + /** + * Get all flags with their current status + */ + const getFlags = computed(() => { + return () => { + return Object.values(FEATURE_FLAGS).map(flag => ({ + ...flag, + isEnabled: isEnabled.value(flag.name), + hasOverride: flag.name in localOverrides.value, + override: localOverrides.value[flag.name], + requiresPermission: !!flag.requiredPermission, + hasPermission: !flag.requiredPermission || hasPermission(flag.requiredPermission) + })); + }; + }); + + /** + * Set flags from backend response + * Called after fetching flags from backend + */ + const setBackendFlags = (flags) => { + backendFlags.value = flags; + }; + + /** + * Fetch flags from backend (requires admin permission) + * Future: Implement backend endpoint + */ + const fetchFromBackend = async () => { + // TODO: Implement backend endpoint + // const response = await apiClient.get('/api/feature-flags'); + // setBackendFlags(response.flags); + }; + + return { + isEnabled, + toggle, + reset, + resetAll, + getFlags, + setBackendFlags, + fetchFromBackend + }; +}