import React, { useState, useEffect, useCallback, useMemo } from 'react'; import { View, Text, FlatList, TouchableOpacity, Switch, TextInput, StyleSheet, ScrollView, } from 'react-native'; import type { PluginComponentProps, FeatureFlag, DebuggerPlugin } from '../../core/types'; import { flagStore } from './flagStore'; const FeatureFlagsPanel: React.FC = ({ theme }) => { const [flags, setFlags] = useState([]); const [searchQuery, setSearchQuery] = useState(''); const [groupFilter, setGroupFilter] = useState(null); const [editingFlag, setEditingFlag] = useState(null); const [editValue, setEditValue] = useState(''); useEffect(() => { const unsubscribe = flagStore.subscribe(setFlags); return unsubscribe; }, []); const groups = useMemo(() => { const groupSet = new Set(); for (const flag of flags) { if (flag.group) groupSet.add(flag.group); } return Array.from(groupSet).sort(); }, [flags]); const filteredFlags = useMemo(() => { let filtered = flags; if (groupFilter) { filtered = filtered.filter((f) => f.group === groupFilter); } if (searchQuery) { const q = searchQuery.toLowerCase(); filtered = filtered.filter( (f) => f.key.toLowerCase().includes(q) || f.label?.toLowerCase().includes(q) || f.description?.toLowerCase().includes(q), ); } return filtered; }, [flags, searchQuery, groupFilter]); const handleToggle = useCallback((key: string, currentValue: boolean) => { flagStore.override(key, !currentValue); }, []); const handleSaveEdit = useCallback( (flag: FeatureFlag) => { let parsed: FeatureFlag['currentValue'] = editValue; if (flag.type === 'number') { parsed = Number(editValue); if (isNaN(parsed as number)) return; } else if (flag.type === 'json') { try { parsed = JSON.parse(editValue); } catch { return; } } flagStore.override(flag.key, parsed); setEditingFlag(null); }, [editValue], ); const handleResetAll = useCallback(() => { flagStore.resetAll(); }, []); const isOverridden = useCallback((flag: FeatureFlag) => { return JSON.stringify(flag.currentValue) !== JSON.stringify(flag.defaultValue); }, []); if (flags.length === 0) { return ( 🚩 No Feature Flags Register flags using:{'\n\n'} {'registerFlag({\n key: "dark_mode",\n type: "boolean",\n defaultValue: false\n})'} ); } return ( {/* Header */} {flags.length} flags Reset All {/* Groups */} {groups.length > 0 && ( setGroupFilter(null)} activeOpacity={0.7} > All {groups.map((group) => ( setGroupFilter(groupFilter === group ? null : group)} activeOpacity={0.7} > {group} ))} )} {/* Search */} {/* Flags List */} item.key} renderItem={({ item }) => ( {item.label || item.key} {isOverridden(item) && ( flagStore.reset(item.key)} activeOpacity={0.7}> Reset )} {item.description && ( {item.description} )} {item.key} • {item.type} {item.group ? ` • ${item.group}` : ''} {item.type === 'boolean' ? ( handleToggle(item.key, item.currentValue as boolean)} trackColor={{ false: theme.border, true: theme.accent }} thumbColor="#FFFFFF" /> ) : editingFlag === item.key ? ( handleSaveEdit(item)} activeOpacity={0.7}> Save setEditingFlag(null)} activeOpacity={0.7}> Cancel ) : ( { setEditingFlag(item.key); setEditValue( typeof item.currentValue === 'object' ? JSON.stringify(item.currentValue, null, 2) : String(item.currentValue), ); }} style={[styles.valueButton, { backgroundColor: theme.surfaceAlt }]} activeOpacity={0.7} > {typeof item.currentValue === 'object' ? JSON.stringify(item.currentValue) : String(item.currentValue)} )} )} /> ); }; const styles = StyleSheet.create({ container: { flex: 1 }, emptyContainer: { flex: 1, alignItems: 'center', justifyContent: 'center', paddingHorizontal: 32, }, emptyIcon: { fontSize: 48, marginBottom: 16 }, emptyTitle: { fontSize: 16, fontWeight: '700', marginBottom: 8 }, emptyDesc: { fontSize: 13, textAlign: 'center', lineHeight: 20 }, headerBar: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', paddingHorizontal: 12, paddingVertical: 8, borderBottomWidth: StyleSheet.hairlineWidth, }, headerText: { fontSize: 13, fontWeight: '600' }, resetText: { fontSize: 12, fontWeight: '700' }, groupBar: { maxHeight: 44 }, groupBarContent: { paddingHorizontal: 12, paddingVertical: 8, gap: 6 }, groupChip: { paddingHorizontal: 10, paddingVertical: 4, borderRadius: 12, borderWidth: 1 }, groupChipText: { fontSize: 11, fontWeight: '600' }, searchContainer: { paddingHorizontal: 12, paddingBottom: 8 }, searchInput: { height: 36, borderRadius: 8, paddingHorizontal: 12, fontSize: 13, borderWidth: 1 }, flagRow: { paddingHorizontal: 12, paddingVertical: 10, borderBottomWidth: StyleSheet.hairlineWidth, }, flagInfo: { marginBottom: 8 }, flagNameRow: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center' }, flagKey: { fontSize: 14, fontWeight: '600' }, resetFlag: { fontSize: 11, fontWeight: '600' }, flagDesc: { fontSize: 12, marginTop: 2 }, flagMeta: { fontSize: 10, marginTop: 2, fontFamily: 'monospace' }, flagControl: { alignItems: 'flex-end' }, editContainer: { width: '100%' }, editInput: { height: 36, borderRadius: 8, paddingHorizontal: 12, fontSize: 13, borderWidth: 1, marginBottom: 8, }, editButtons: { flexDirection: 'row', gap: 12, justifyContent: 'flex-end' }, editSave: { fontSize: 13, fontWeight: '700' }, editCancel: { fontSize: 13 }, valueButton: { paddingHorizontal: 12, paddingVertical: 6, borderRadius: 8, maxWidth: 200 }, valueText: { fontSize: 12, fontFamily: 'monospace' }, }); export function createFeatureFlagsPlugin(): DebuggerPlugin { return { id: 'feature-flags', name: 'Flags', icon: '🚩', component: FeatureFlagsPanel, order: 40, }; }