Initial commit: libp2p-native-bridge package
- Extracted libp2p component from main app - Created modular package structure with interfaces and implementations - Added dependency injection for NativeModules - Configured for IOR loading from Gitea - Added comprehensive README and documentation
This commit is contained in:
496
implementations/SettingsUI.tsx
Normal file
496
implementations/SettingsUI.tsx
Normal file
@@ -0,0 +1,496 @@
|
||||
/**
|
||||
* 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<SettingsUIProps> = ({
|
||||
initialSettings,
|
||||
onSettingsChange,
|
||||
onPortsSaved,
|
||||
onReset,
|
||||
}) => {
|
||||
const loggerRef = useRef<LoggerComponent | null>(null);
|
||||
if (!loggerRef.current) {
|
||||
loggerRef.current = new LoggerComponent('SettingsUI');
|
||||
}
|
||||
const logger = loggerRef.current;
|
||||
const [settings, setSettings] = useState<AppSettings>(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 (
|
||||
<KeyboardAvoidingView
|
||||
style={styles.container}
|
||||
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
|
||||
keyboardVerticalOffset={Platform.OS === 'ios' ? 0 : 0}
|
||||
>
|
||||
<ScrollView
|
||||
style={styles.scrollView}
|
||||
contentContainerStyle={styles.scrollContent}
|
||||
showsVerticalScrollIndicator={true}
|
||||
keyboardShouldPersistTaps="handled"
|
||||
>
|
||||
<View style={styles.section}>
|
||||
<Text style={styles.sectionTitle}>Network Configuration</Text>
|
||||
|
||||
<View style={styles.setting}>
|
||||
<Text style={styles.settingLabel}>Use Custom Ports</Text>
|
||||
<Switch
|
||||
value={settings.useCustomPorts}
|
||||
onValueChange={handleToggleCustomPorts}
|
||||
trackColor={{ false: '#767577', true: '#2196F3' }}
|
||||
thumbColor={settings.useCustomPorts ? '#fff' : '#f4f3f4'}
|
||||
ios_backgroundColor="#3e3e3e"
|
||||
/>
|
||||
</View>
|
||||
|
||||
{settings.useCustomPorts && (
|
||||
<>
|
||||
<View style={styles.inputSetting}>
|
||||
<Text style={styles.settingLabel}>TCP Port</Text>
|
||||
<TextInput
|
||||
style={styles.input}
|
||||
value={tcpPortInput}
|
||||
onChangeText={(text) => {
|
||||
setTcpPortInput(text);
|
||||
setIsDirty(true);
|
||||
}}
|
||||
keyboardType="number-pad"
|
||||
placeholder="10000"
|
||||
placeholderTextColor="#666"
|
||||
maxLength={5}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<View style={styles.inputSetting}>
|
||||
<Text style={styles.settingLabel}>WebSocket Port</Text>
|
||||
<TextInput
|
||||
style={styles.input}
|
||||
value={wsPortInput}
|
||||
onChangeText={(text) => {
|
||||
setWsPortInput(text);
|
||||
setIsDirty(true);
|
||||
}}
|
||||
keyboardType="number-pad"
|
||||
placeholder="10005"
|
||||
placeholderTextColor="#666"
|
||||
maxLength={5}
|
||||
/>
|
||||
</View>
|
||||
</>
|
||||
)}
|
||||
</View>
|
||||
|
||||
<View style={styles.section}>
|
||||
<Text style={styles.sectionTitle}>Discovery Configuration</Text>
|
||||
|
||||
<View style={styles.inputSetting}>
|
||||
<Text style={styles.settingLabel}>Timeout (seconds)</Text>
|
||||
<TextInput
|
||||
style={styles.input}
|
||||
value={discoveryTimeoutInput}
|
||||
onChangeText={(text) => {
|
||||
setDiscoveryTimeoutInput(text);
|
||||
setIsDirty(true);
|
||||
}}
|
||||
keyboardType="number-pad"
|
||||
placeholder="30"
|
||||
placeholderTextColor="#666"
|
||||
maxLength={3}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<View style={styles.inputSetting}>
|
||||
<Text style={styles.settingLabel}>DHT Server URL</Text>
|
||||
<TextInput
|
||||
style={styles.input}
|
||||
value={dhtServerUrlInput}
|
||||
onChangeText={(text) => {
|
||||
setDhtServerUrlInput(text);
|
||||
setIsDirty(true);
|
||||
}}
|
||||
placeholder="ws://192.168.188.40:3000/ws"
|
||||
placeholderTextColor="#666"
|
||||
selectionColor="#2196F3"
|
||||
autoCapitalize="none"
|
||||
autoCorrect={false}
|
||||
underlineColorAndroid="transparent"
|
||||
/>
|
||||
<Text style={styles.settingDescription}>
|
||||
WebSocket URL of the DHT server for global peer discovery
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{isDirty && (
|
||||
<View style={styles.section}>
|
||||
<TouchableOpacity style={styles.saveButton} onPress={saveSettings}>
|
||||
<Text style={styles.saveButtonText}>Save Settings</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
)}
|
||||
|
||||
<View style={[styles.section, styles.lastSection]}>
|
||||
<TouchableOpacity style={styles.dangerButton} onPress={resetApp}>
|
||||
<Text style={styles.dangerButtonText}>🗑 Reset All App Data</Text>
|
||||
</TouchableOpacity>
|
||||
|
||||
<Text style={styles.warningText}>
|
||||
This will delete all stored data and reset the app to its initial state
|
||||
</Text>
|
||||
</View>
|
||||
</ScrollView>
|
||||
</KeyboardAvoidingView>
|
||||
);
|
||||
};
|
||||
|
||||
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;
|
||||
Reference in New Issue
Block a user