423 lines
12 KiB
TypeScript
423 lines
12 KiB
TypeScript
import { useState, useEffect, useRef } from 'react';
|
||
import { StyleSheet, View, Text, FlatList, TouchableOpacity, PermissionsAndroid, Platform, ScrollView } from 'react-native';
|
||
import { SafeAreaView } from 'react-native-safe-area-context';
|
||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||
import { Stack, router } from 'expo-router';
|
||
import { useKeepAwake } from 'expo-keep-awake';
|
||
import io from 'socket.io-client';
|
||
import { SendDirectSms } from 'react-native-send-direct-sms';
|
||
|
||
const COLORS = {
|
||
background: '#121416',
|
||
surface: '#1e2022',
|
||
surfaceHigh: '#282a2c',
|
||
primary: '#fefcff',
|
||
onPrimary: '#213148',
|
||
text: '#e2e2e5',
|
||
textMuted: '#c5c6cd',
|
||
success: '#69fc9b',
|
||
successDim: '#003919',
|
||
error: '#ffb4ab',
|
||
border: '#44474d',
|
||
};
|
||
|
||
export default function DashboardScreen() {
|
||
useKeepAwake();
|
||
|
||
const [status, setStatus] = useState('Disconnected');
|
||
const [hasPermission, setHasPermission] = useState(false);
|
||
const [logs, setLogs] = useState<{id: string, time: string, msg: string, type: 'info' | 'success' | 'error'}[]>([]);
|
||
const [latestSms, setLatestSms] = useState<{phone: string, message: string} | null>(null);
|
||
const socketRef = useRef<any>(null);
|
||
|
||
useEffect(() => {
|
||
checkAndRequestPermission();
|
||
connectSocket();
|
||
|
||
return () => {
|
||
if (socketRef.current) {
|
||
socketRef.current.disconnect();
|
||
}
|
||
};
|
||
}, []);
|
||
|
||
const checkAndRequestPermission = async () => {
|
||
if (Platform.OS === 'android') {
|
||
try {
|
||
const granted = await PermissionsAndroid.request(
|
||
PermissionsAndroid.PERMISSIONS.SEND_SMS,
|
||
{
|
||
title: 'SMS Permission',
|
||
message: 'This app needs access to send SMS messages directly.',
|
||
buttonNeutral: 'Ask Me Later',
|
||
buttonNegative: 'Cancel',
|
||
buttonPositive: 'OK',
|
||
},
|
||
);
|
||
if (granted === PermissionsAndroid.RESULTS.GRANTED) {
|
||
setHasPermission(true);
|
||
addLog('SMS permission granted', 'success');
|
||
} else {
|
||
setHasPermission(false);
|
||
addLog('SMS permission denied', 'error');
|
||
}
|
||
} catch (err) {
|
||
console.warn(err);
|
||
addLog(`Permission error: ${err}`, 'error');
|
||
}
|
||
}
|
||
};
|
||
|
||
const addLog = (msg: string, type: 'info' | 'success' | 'error' = 'info') => {
|
||
setLogs(prev => [{
|
||
id: Math.random().toString(),
|
||
time: new Date().toLocaleTimeString('en-US', { hour12: false }),
|
||
msg,
|
||
type
|
||
}, ...prev].slice(0, 50));
|
||
};
|
||
|
||
const connectSocket = async () => {
|
||
try {
|
||
const endpoint = await AsyncStorage.getItem('endpoint');
|
||
const login = await AsyncStorage.getItem('login');
|
||
const password = await AsyncStorage.getItem('password');
|
||
|
||
if (!endpoint || !login || !password) {
|
||
setStatus('Missing Settings');
|
||
addLog('Please configure settings first', 'error');
|
||
return;
|
||
}
|
||
|
||
if (socketRef.current) {
|
||
socketRef.current.disconnect();
|
||
}
|
||
|
||
setStatus('Connecting...');
|
||
addLog('Attempting socket handshake', 'info');
|
||
|
||
const socket = io(endpoint, {
|
||
auth: { login, password },
|
||
reconnectionAttempts: Infinity,
|
||
reconnectionDelay: 1000,
|
||
reconnectionDelayMax: 5000,
|
||
});
|
||
|
||
socket.on('connect', () => {
|
||
setStatus('Connected');
|
||
addLog('Connected to backend', 'success');
|
||
});
|
||
|
||
socket.on('disconnect', () => {
|
||
setStatus('Disconnected');
|
||
addLog('Connection lost: Retrying...', 'error');
|
||
});
|
||
|
||
socket.on('connect_error', (err) => {
|
||
setStatus('Error');
|
||
addLog(`Connection error: ${err.message}`, 'error');
|
||
});
|
||
|
||
socket.on('send_sms', async (data) => {
|
||
addLog(`OTP received: ${data.message}`, 'info');
|
||
setLatestSms({ phone: data.phone || '+993 61 92 92 48', message: data.message });
|
||
|
||
try {
|
||
const targetPhone = data.phone || '+993 61 92 92 48';
|
||
|
||
if (Platform.OS === 'android') {
|
||
SendDirectSms(targetPhone, data.message)
|
||
.then((res: any) => {
|
||
addLog(`OTP forwarded successfully`, 'success');
|
||
socket.emit('sms_status', { id: data.id, status: 'sent' });
|
||
})
|
||
.catch((err: any) => {
|
||
addLog(`SMS send failed: ${err}`, 'error');
|
||
socket.emit('sms_status', { id: data.id, status: 'failed' });
|
||
});
|
||
} else {
|
||
addLog('Direct SMS only supported on Android', 'error');
|
||
socket.emit('sms_status', { id: data.id, status: 'failed' });
|
||
}
|
||
} catch (error) {
|
||
addLog(`Error sending SMS: ${error}`, 'error');
|
||
socket.emit('sms_status', { id: data.id, status: 'failed' });
|
||
}
|
||
});
|
||
|
||
socketRef.current = socket;
|
||
|
||
} catch (e) {
|
||
addLog(`Setup error: ${e}`, 'error');
|
||
}
|
||
};
|
||
|
||
return (
|
||
<SafeAreaView style={styles.container}>
|
||
<Stack.Screen options={{ headerShown: false }} />
|
||
|
||
<View style={styles.header}>
|
||
<View style={styles.headerLeft}>
|
||
<Text style={styles.headerIcon}>🛡️</Text>
|
||
<Text style={styles.headerTitle}>Secure Relay</Text>
|
||
</View>
|
||
<TouchableOpacity onPress={() => router.push('/settings')}>
|
||
<Text style={styles.settingsIcon}>⚙️</Text>
|
||
</TouchableOpacity>
|
||
</View>
|
||
|
||
<ScrollView style={styles.scrollContent} contentContainerStyle={styles.scrollContentContainer}>
|
||
|
||
{/* Status Card */}
|
||
<View style={styles.mainStatusCard}>
|
||
<View style={[styles.statusIconContainer, { backgroundColor: status === 'Connected' ? COLORS.successDim : (status === 'Connecting...' ? '#4a3600' : '#4a0005') }]}>
|
||
<Text style={styles.statusIcon}>{status === 'Connected' ? '✓' : (status === 'Connecting...' ? '⏳' : '✕')}</Text>
|
||
</View>
|
||
<Text style={[styles.statusValue, { color: status === 'Connected' ? COLORS.success : (status === 'Connecting...' ? '#ffb4ab' : COLORS.error) }]}>
|
||
{status} {status === 'Connected' && '✅'}
|
||
</Text>
|
||
<Text style={styles.statusDesc}>
|
||
{status === 'Connected' ? 'Service is active and monitoring incoming SMS.' : 'Service is currently unavailable.'}
|
||
</Text>
|
||
</View>
|
||
|
||
{/* System Status */}
|
||
<Text style={styles.sectionTitle}>System Status</Text>
|
||
<View style={styles.systemStatusRow}>
|
||
<TouchableOpacity style={styles.systemStatusCard} onPress={checkAndRequestPermission}>
|
||
<Text style={styles.systemStatusIcon}>💬</Text>
|
||
<View>
|
||
<Text style={styles.systemStatusLabel}>SMS Perm</Text>
|
||
<Text style={[styles.systemStatusValue, { color: hasPermission ? COLORS.success : COLORS.error }]}>
|
||
{hasPermission ? 'GRANTED' : 'DENIED'}
|
||
</Text>
|
||
</View>
|
||
</TouchableOpacity>
|
||
<View style={styles.systemStatusCard}>
|
||
<Text style={styles.systemStatusIcon}>📁</Text>
|
||
<View>
|
||
<Text style={styles.systemStatusLabel}>Storage</Text>
|
||
<Text style={[styles.systemStatusValue, { color: COLORS.success }]}>
|
||
GRANTED
|
||
</Text>
|
||
</View>
|
||
</View>
|
||
</View>
|
||
|
||
<TouchableOpacity style={styles.reconnectButton} onPress={connectSocket}>
|
||
<Text style={styles.reconnectText}>↻ Reconnect</Text>
|
||
</TouchableOpacity>
|
||
|
||
{/* Latest SMS */}
|
||
{latestSms && (
|
||
<>
|
||
<Text style={styles.sectionTitle}>Latest SMS Request</Text>
|
||
<View style={styles.latestSmsCard}>
|
||
<Text style={styles.latestSmsText}>📱 Phone: <Text style={styles.latestSmsHighlight}>{latestSms.phone}</Text></Text>
|
||
<Text style={styles.latestSmsText}>💬 Message: <Text style={styles.latestSmsHighlight}>'{latestSms.message}'</Text></Text>
|
||
</View>
|
||
</>
|
||
)}
|
||
|
||
{/* Logs */}
|
||
<View style={styles.logHeader}>
|
||
<Text style={styles.sectionTitle}>Recent Logs</Text>
|
||
<TouchableOpacity onPress={() => setLogs([])} style={styles.clearButton}>
|
||
<Text style={styles.clearButtonText}>Clear Logs</Text>
|
||
</TouchableOpacity>
|
||
</View>
|
||
|
||
<View style={styles.logsCard}>
|
||
{logs.map((item) => (
|
||
<View key={item.id} style={styles.logItem}>
|
||
<Text style={[styles.logTime, { color: item.type === 'error' ? COLORS.error : (item.type === 'success' ? COLORS.success : COLORS.textMuted) }]}>
|
||
[{item.time}]
|
||
</Text>
|
||
<Text style={[styles.logMsg, { color: item.type === 'error' ? COLORS.error : (item.type === 'success' ? COLORS.success : COLORS.textMuted) }]}>
|
||
{item.msg}
|
||
</Text>
|
||
</View>
|
||
))}
|
||
{logs.length === 0 && (
|
||
<Text style={styles.emptyLogs}>No recent activity.</Text>
|
||
)}
|
||
</View>
|
||
|
||
</ScrollView>
|
||
</SafeAreaView>
|
||
);
|
||
}
|
||
|
||
const styles = StyleSheet.create({
|
||
container: {
|
||
flex: 1,
|
||
backgroundColor: COLORS.background,
|
||
},
|
||
header: {
|
||
flexDirection: 'row',
|
||
justifyContent: 'space-between',
|
||
alignItems: 'center',
|
||
paddingHorizontal: 24,
|
||
paddingVertical: 16,
|
||
marginTop: 8,
|
||
},
|
||
headerLeft: {
|
||
flexDirection: 'row',
|
||
alignItems: 'center',
|
||
},
|
||
headerIcon: {
|
||
fontSize: 24,
|
||
marginRight: 8,
|
||
},
|
||
headerTitle: {
|
||
color: COLORS.text,
|
||
fontSize: 20,
|
||
fontWeight: 'bold',
|
||
},
|
||
settingsIcon: {
|
||
fontSize: 24,
|
||
color: COLORS.textMuted,
|
||
},
|
||
scrollContent: {
|
||
flex: 1,
|
||
},
|
||
scrollContentContainer: {
|
||
padding: 24,
|
||
paddingTop: 8,
|
||
},
|
||
mainStatusCard: {
|
||
backgroundColor: COLORS.surface,
|
||
borderRadius: 24,
|
||
padding: 32,
|
||
alignItems: 'center',
|
||
marginBottom: 24,
|
||
},
|
||
statusIconContainer: {
|
||
width: 48,
|
||
height: 48,
|
||
borderRadius: 24,
|
||
alignItems: 'center',
|
||
justifyContent: 'center',
|
||
marginBottom: 16,
|
||
},
|
||
statusIcon: {
|
||
fontSize: 24,
|
||
color: COLORS.success,
|
||
},
|
||
statusValue: {
|
||
fontSize: 28,
|
||
fontWeight: 'bold',
|
||
marginBottom: 8,
|
||
},
|
||
statusDesc: {
|
||
color: COLORS.textMuted,
|
||
fontSize: 14,
|
||
textAlign: 'center',
|
||
},
|
||
sectionTitle: {
|
||
color: COLORS.text,
|
||
fontSize: 16,
|
||
fontWeight: '600',
|
||
marginBottom: 12,
|
||
marginTop: 8,
|
||
},
|
||
systemStatusRow: {
|
||
flexDirection: 'row',
|
||
justifyContent: 'space-between',
|
||
marginBottom: 24,
|
||
},
|
||
systemStatusCard: {
|
||
backgroundColor: COLORS.surface,
|
||
borderRadius: 16,
|
||
padding: 16,
|
||
flexDirection: 'row',
|
||
alignItems: 'center',
|
||
flex: 0.48,
|
||
},
|
||
systemStatusIcon: {
|
||
fontSize: 20,
|
||
marginRight: 12,
|
||
},
|
||
systemStatusLabel: {
|
||
color: COLORS.text,
|
||
fontSize: 14,
|
||
fontWeight: '500',
|
||
},
|
||
systemStatusValue: {
|
||
fontSize: 12,
|
||
fontWeight: 'bold',
|
||
marginTop: 2,
|
||
},
|
||
reconnectButton: {
|
||
backgroundColor: COLORS.primary,
|
||
borderRadius: 9999,
|
||
paddingVertical: 16,
|
||
alignItems: 'center',
|
||
marginBottom: 24,
|
||
},
|
||
reconnectText: {
|
||
color: COLORS.onPrimary,
|
||
fontSize: 16,
|
||
fontWeight: 'bold',
|
||
},
|
||
latestSmsCard: {
|
||
backgroundColor: COLORS.surface,
|
||
borderRadius: 24,
|
||
padding: 20,
|
||
marginBottom: 24,
|
||
},
|
||
latestSmsText: {
|
||
color: COLORS.textMuted,
|
||
fontSize: 14,
|
||
marginBottom: 8,
|
||
lineHeight: 20,
|
||
},
|
||
latestSmsHighlight: {
|
||
color: COLORS.text,
|
||
fontWeight: 'bold',
|
||
fontFamily: Platform.OS === 'ios' ? 'Menlo' : 'monospace',
|
||
},
|
||
logHeader: {
|
||
flexDirection: 'row',
|
||
justifyContent: 'space-between',
|
||
alignItems: 'center',
|
||
},
|
||
clearButton: {
|
||
backgroundColor: COLORS.surfaceHigh,
|
||
paddingHorizontal: 12,
|
||
paddingVertical: 6,
|
||
borderRadius: 12,
|
||
},
|
||
clearButtonText: {
|
||
color: COLORS.text,
|
||
fontSize: 12,
|
||
fontWeight: '500',
|
||
},
|
||
logsCard: {
|
||
backgroundColor: COLORS.surface,
|
||
borderRadius: 24,
|
||
padding: 20,
|
||
minHeight: 150,
|
||
},
|
||
logItem: {
|
||
flexDirection: 'row',
|
||
marginBottom: 8,
|
||
},
|
||
logTime: {
|
||
fontSize: 13,
|
||
fontFamily: Platform.OS === 'ios' ? 'Menlo' : 'monospace',
|
||
marginRight: 8,
|
||
},
|
||
logMsg: {
|
||
flex: 1,
|
||
fontSize: 13,
|
||
fontFamily: Platform.OS === 'ios' ? 'Menlo' : 'monospace',
|
||
},
|
||
emptyLogs: {
|
||
color: COLORS.textMuted,
|
||
fontSize: 13,
|
||
fontStyle: 'italic',
|
||
textAlign: 'center',
|
||
marginTop: 20,
|
||
}
|
||
}); |