/** * Settings UI Component * * Provides configuration options for the libp2p node including: * - Port configuration * - Network settings * - Data management (reset app) * - Debug options */ import { LoggerComponent } from 'ior:gitea:gitea.metatrom.net:universal-components/logger@1.0.0'; import AsyncStorage from '@react-native-async-storage/async-storage'; import type React from 'react'; import { useEffect, useRef, useState } from 'react'; import { Alert, KeyboardAvoidingView, NativeModules, Platform, ScrollView, StyleSheet, Switch, Text, TextInput, TouchableOpacity, View, } from 'react-native'; // SecureStorage removed - not needed in external package import { DEFAULT_SETTINGS, type AppSettings } from './SettingsService'; const { Libp2pModule, DiscoveryModule, SecureStorageModule } = NativeModules; // Settings storage keys const SETTINGS_STORAGE_KEY = '@libp2p_settings'; // Use AppSettings from SettingsService // The LibP2PSettings type is now replaced by AppSettings interface SettingsUIProps { initialSettings?: AppSettings; onSettingsChange?: (settings: AppSettings) => void; onPortsSaved?: () => void; onReset?: () => void; } export const SettingsUI: React.FC = ({ initialSettings, onSettingsChange, onPortsSaved, onReset, }) => { const loggerRef = useRef(null); if (!loggerRef.current) { loggerRef.current = new LoggerComponent('SettingsUI'); } const logger = loggerRef.current; const [settings, setSettings] = useState(initialSettings || DEFAULT_SETTINGS); const [isDirty, setIsDirty] = useState(false); const [tcpPortInput, setTcpPortInput] = useState( String((initialSettings || DEFAULT_SETTINGS).tcpPort), ); const [wsPortInput, setWsPortInput] = useState( String((initialSettings || DEFAULT_SETTINGS).wsPort), ); const [discoveryTimeoutInput, setDiscoveryTimeoutInput] = useState( String((initialSettings || DEFAULT_SETTINGS).discoveryTimeout / 1000), ); const [dhtServerUrlInput, setDhtServerUrlInput] = useState( (initialSettings || DEFAULT_SETTINGS).dhtServerUrl, ); // Update state when initialSettings changes useEffect(() => { if (initialSettings) { setSettings(initialSettings); setTcpPortInput(String(initialSettings.tcpPort)); setWsPortInput(String(initialSettings.wsPort)); setDiscoveryTimeoutInput(String(initialSettings.discoveryTimeout / 1000)); setDhtServerUrlInput(initialSettings.dhtServerUrl); } else { loadSettings(); } }, [initialSettings]); /** * Load settings from AsyncStorage */ const loadSettings = async () => { try { const stored = await AsyncStorage.getItem(SETTINGS_STORAGE_KEY); let loadedSettings: LibP2PSettings; if (stored) { const parsed = JSON.parse(stored); loadedSettings = { ...DEFAULT_SETTINGS, ...parsed }; } else { // No stored settings, use defaults loadedSettings = DEFAULT_SETTINGS; } setSettings(loadedSettings); setTcpPortInput(String(loadedSettings.tcpPort)); setWsPortInput(String(loadedSettings.wsPort)); setDiscoveryTimeoutInput(String(loadedSettings.discoveryTimeout / 1000)); setDhtServerUrlInput(loadedSettings.dhtServerUrl); // Don't call onSettingsChange during initial load // Parent component should handle initial settings } catch (error) { logger.error('[Settings] Failed to load settings:', error); // On error, still load defaults setSettings(DEFAULT_SETTINGS); setTcpPortInput(String(DEFAULT_SETTINGS.tcpPort)); setWsPortInput(String(DEFAULT_SETTINGS.wsPort)); setDiscoveryTimeoutInput(String(DEFAULT_SETTINGS.discoveryTimeout / 1000)); setDhtServerUrlInput(DEFAULT_SETTINGS.dhtServerUrl); } }; /** * Save settings to AsyncStorage */ const saveSettings = async () => { try { // Validate ports const tcpPort = Number.parseInt(tcpPortInput, 10); const wsPort = Number.parseInt(wsPortInput, 10); const discoveryTimeout = Number.parseInt(discoveryTimeoutInput, 10) * 1000; if (Number.isNaN(tcpPort) || tcpPort < 1024 || tcpPort > 65535) { Alert.alert('Invalid TCP Port', 'Please enter a valid port number (1024-65535)'); return; } if (Number.isNaN(wsPort) || wsPort < 1024 || wsPort > 65535) { Alert.alert('Invalid WebSocket Port', 'Please enter a valid port number (1024-65535)'); return; } if (Number.isNaN(discoveryTimeout) || discoveryTimeout < 5000 || discoveryTimeout > 300000) { Alert.alert('Invalid Discovery Timeout', 'Please enter a value between 5 and 300 seconds'); return; } const newSettings = { ...settings, tcpPort, wsPort, discoveryTimeout, dhtServerUrl: dhtServerUrlInput, }; await AsyncStorage.setItem(SETTINGS_STORAGE_KEY, JSON.stringify(newSettings)); setSettings(newSettings); setIsDirty(false); if (onSettingsChange) { onSettingsChange(newSettings); } // Call the onPortsSaved callback to trigger libp2p restart if (onPortsSaved) { onPortsSaved(); } } catch (error) { logger.error('[Settings] Failed to save settings:', error); Alert.alert('Error', 'Failed to save settings'); } }; /** * Reset all app data */ const resetApp = () => { Alert.alert( 'Reset App Data', 'This will delete all stored data including:\n\n• Your peer identity\n• All settings\n• Stored devices\n• Chat history\n\nThe app will be like freshly installed. Continue?', [ { text: 'Cancel', style: 'cancel' }, { text: 'Reset', style: 'destructive', onPress: async () => { try { // Show progress alert Alert.alert('Resetting...', 'Please wait while we clear all data'); // Clear all AsyncStorage await AsyncStorage.clear(); // Clear all secure storage (identities, paired devices, etc.) try { // Clear secure storage if available if (SecureStorageModule?.clearAll) { await SecureStorageModule.clearAll(); } logger.info('[SettingsUI] Cleared all secure storage data'); } catch (error) { logger.error('[SettingsUI] Failed to clear secure storage:', error); } // Clear native module stored data if (Platform.OS === 'ios') { // iOS uses UserDefaults if (Libp2pModule.clearStoredData) { await Libp2pModule.clearStoredData(); } if (DiscoveryModule.clearStoredData) { await DiscoveryModule.clearStoredData(); } // SecureStorage handled above } else if (Platform.OS === 'android') { // Android uses SharedPreferences if (Libp2pModule.clearStoredData) { await Libp2pModule.clearStoredData(); } if (DiscoveryModule.clearStoredData) { await DiscoveryModule.clearStoredData(); } if (SecureStorageModule?.clearAll) { await SecureStorageModule.clearAll(); } } // Reset settings to defaults setSettings(DEFAULT_SETTINGS); setTcpPortInput(String(DEFAULT_SETTINGS.tcpPort)); setWsPortInput(String(DEFAULT_SETTINGS.wsPort)); setDiscoveryTimeoutInput(String(DEFAULT_SETTINGS.discoveryTimeout / 1000)); setDhtServerUrlInput(DEFAULT_SETTINGS.dhtServerUrl); if (onReset) { onReset(); } Alert.alert( 'Reset Complete', 'All app data has been cleared. Please restart the app for a fresh start.', [ { text: 'OK', onPress: () => { // Optionally restart the app // RNRestart.Restart(); // Requires react-native-restart package }, }, ], ); } catch (error) { logger.error('[Settings] Failed to reset app:', error); Alert.alert('Error', 'Failed to reset app data'); } }, }, ], ); }; const handleToggleCustomPorts = async () => { const newSettings = { ...settings, useCustomPorts: !settings.useCustomPorts }; setSettings(newSettings); // Auto-save switch changes immediately try { await AsyncStorage.setItem(SETTINGS_STORAGE_KEY, JSON.stringify(newSettings)); if (onSettingsChange) { onSettingsChange(newSettings); } } catch (error) { logger.error('[Settings] Failed to save setting:', error); } }; return ( Network Configuration Use Custom Ports {settings.useCustomPorts && ( <> TCP Port { setTcpPortInput(text); setIsDirty(true); }} keyboardType="number-pad" placeholder="10000" placeholderTextColor="#666" maxLength={5} /> WebSocket Port { setWsPortInput(text); setIsDirty(true); }} keyboardType="number-pad" placeholder="10005" placeholderTextColor="#666" maxLength={5} /> )} Discovery Configuration Timeout (seconds) { setDiscoveryTimeoutInput(text); setIsDirty(true); }} keyboardType="number-pad" placeholder="30" placeholderTextColor="#666" maxLength={3} /> DHT Server URL { setDhtServerUrlInput(text); setIsDirty(true); }} placeholder="ws://192.168.188.40:3000/ws" placeholderTextColor="#666" selectionColor="#2196F3" autoCapitalize="none" autoCorrect={false} underlineColorAndroid="transparent" /> WebSocket URL of the DHT server for global peer discovery {isDirty && ( Save Settings )} 🗑 Reset All App Data This will delete all stored data and reset the app to its initial state ); }; const styles = StyleSheet.create({ container: { flex: 1, backgroundColor: 'transparent', }, scrollView: { flex: 1, }, scrollContent: { paddingVertical: 20, paddingBottom: 100, // Increased to ensure all content is visible }, section: { paddingHorizontal: 20, marginBottom: 24, }, sectionTitle: { fontSize: 16, fontWeight: '600', color: '#fff', marginBottom: 16, }, setting: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'flex-start', paddingVertical: 16, }, settingInfo: { flex: 1, marginRight: 12, }, settingLabel: { fontSize: 16, fontWeight: '600', color: '#fff', marginBottom: 4, }, settingDescription: { fontSize: 12, color: '#999', marginTop: 2, }, inputSetting: { paddingVertical: 12, }, input: { marginTop: 8, paddingHorizontal: 12, paddingVertical: Platform.OS === 'ios' ? 10 : 8, backgroundColor: '#2a2a2a', borderRadius: 8, borderWidth: 1, borderColor: '#333', fontSize: 14, color: '#fff', textAlignVertical: 'center', minHeight: Platform.OS === 'android' ? 40 : undefined, }, dangerButton: { marginTop: 12, paddingVertical: 12, paddingHorizontal: 20, backgroundColor: '#5c1e1e', borderRadius: 8, alignItems: 'center', }, dangerButtonText: { fontSize: 16, fontWeight: '600', color: '#ff9999', }, saveButton: { marginTop: 16, paddingVertical: 12, paddingHorizontal: 20, backgroundColor: '#2563eb', borderRadius: 8, alignItems: 'center', }, saveButtonText: { fontSize: 16, fontWeight: '600', color: '#fff', }, warningText: { fontSize: 12, color: '#999', textAlign: 'center', marginTop: 8, fontStyle: 'italic', }, lastSection: { marginBottom: 40, }, }); export default SettingsUI;