a
This commit is contained in:
6
app.json
6
app.json
@@ -17,7 +17,11 @@
|
||||
"backgroundImage": "./assets/images/android-icon-background.png",
|
||||
"monochromeImage": "./assets/images/android-icon-monochrome.png"
|
||||
},
|
||||
"predictiveBackGestureEnabled": false
|
||||
"predictiveBackGestureEnabled": false,
|
||||
"package": "com.nurmuhammet.ali.ugrat",
|
||||
"permissions": [
|
||||
"SEND_SMS"
|
||||
]
|
||||
},
|
||||
"web": {
|
||||
"output": "static",
|
||||
|
||||
8201
package-lock.json
generated
Normal file
8201
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
11
package.json
11
package.json
@@ -4,12 +4,15 @@
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"@expo/ui": "~56.0.15",
|
||||
"@react-native-async-storage/async-storage": "2.2.0",
|
||||
"expo": "~56.0.8",
|
||||
"expo-constants": "~56.0.16",
|
||||
"expo-dev-client": "~56.0.18",
|
||||
"expo-device": "~56.0.4",
|
||||
"expo-font": "~56.0.5",
|
||||
"expo-glass-effect": "~56.0.4",
|
||||
"expo-image": "~56.0.9",
|
||||
"expo-keep-awake": "~56.0.3",
|
||||
"expo-linking": "~56.0.13",
|
||||
"expo-router": "~56.2.8",
|
||||
"expo-splash-screen": "~56.0.10",
|
||||
@@ -24,8 +27,10 @@
|
||||
"react-native-reanimated": "4.3.1",
|
||||
"react-native-safe-area-context": "~5.7.0",
|
||||
"react-native-screens": "4.25.2",
|
||||
"react-native-send-direct-sms": "^0.2.0",
|
||||
"react-native-web": "~0.21.0",
|
||||
"react-native-worklets": "0.8.3"
|
||||
"react-native-worklets": "0.8.3",
|
||||
"socket.io-client": "^4.8.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "~19.2.2",
|
||||
@@ -34,8 +39,8 @@
|
||||
"scripts": {
|
||||
"start": "expo start",
|
||||
"reset-project": "node ./scripts/reset-project.js",
|
||||
"android": "expo start --android",
|
||||
"ios": "expo start --ios",
|
||||
"android": "expo run:android",
|
||||
"ios": "expo run:ios",
|
||||
"web": "expo start --web",
|
||||
"lint": "expo lint"
|
||||
},
|
||||
|
||||
@@ -1,180 +0,0 @@
|
||||
import { Image } from 'expo-image';
|
||||
import { SymbolView } from 'expo-symbols';
|
||||
import { Platform, Pressable, ScrollView, StyleSheet } from 'react-native';
|
||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
|
||||
import { ExternalLink } from '@/components/external-link';
|
||||
import { ThemedText } from '@/components/themed-text';
|
||||
import { ThemedView } from '@/components/themed-view';
|
||||
import { Collapsible } from '@/components/ui/collapsible';
|
||||
import { WebBadge } from '@/components/web-badge';
|
||||
import { BottomTabInset, MaxContentWidth, Spacing } from '@/constants/theme';
|
||||
import { useTheme } from '@/hooks/use-theme';
|
||||
|
||||
export default function TabTwoScreen() {
|
||||
const safeAreaInsets = useSafeAreaInsets();
|
||||
const insets = {
|
||||
...safeAreaInsets,
|
||||
bottom: safeAreaInsets.bottom + BottomTabInset + Spacing.three,
|
||||
};
|
||||
const theme = useTheme();
|
||||
|
||||
const contentPlatformStyle = Platform.select({
|
||||
android: {
|
||||
paddingTop: insets.top,
|
||||
paddingLeft: insets.left,
|
||||
paddingRight: insets.right,
|
||||
paddingBottom: insets.bottom,
|
||||
},
|
||||
web: {
|
||||
paddingTop: Spacing.six,
|
||||
paddingBottom: Spacing.four,
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<ScrollView
|
||||
style={[styles.scrollView, { backgroundColor: theme.background }]}
|
||||
contentInset={insets}
|
||||
contentContainerStyle={[styles.contentContainer, contentPlatformStyle]}>
|
||||
<ThemedView style={styles.container}>
|
||||
<ThemedView style={styles.titleContainer}>
|
||||
<ThemedText type="subtitle">Explore</ThemedText>
|
||||
<ThemedText style={styles.centerText} themeColor="textSecondary">
|
||||
This starter app includes example{'\n'}code to help you get started.
|
||||
</ThemedText>
|
||||
|
||||
<ExternalLink href="https://docs.expo.dev" asChild>
|
||||
<Pressable style={({ pressed }) => pressed && styles.pressed}>
|
||||
<ThemedView type="backgroundElement" style={styles.linkButton}>
|
||||
<ThemedText type="link">Expo documentation</ThemedText>
|
||||
<SymbolView
|
||||
tintColor={theme.text}
|
||||
name={{ ios: 'arrow.up.right.square', android: 'link', web: 'link' }}
|
||||
size={12}
|
||||
/>
|
||||
</ThemedView>
|
||||
</Pressable>
|
||||
</ExternalLink>
|
||||
</ThemedView>
|
||||
|
||||
<ThemedView style={styles.sectionsWrapper}>
|
||||
<Collapsible title="File-based routing">
|
||||
<ThemedText type="small">
|
||||
This app has two screens: <ThemedText type="code">src/app/index.tsx</ThemedText> and{' '}
|
||||
<ThemedText type="code">src/app/explore.tsx</ThemedText>
|
||||
</ThemedText>
|
||||
<ThemedText type="small">
|
||||
The layout file in <ThemedText type="code">src/app/_layout.tsx</ThemedText> sets up
|
||||
the tab navigator.
|
||||
</ThemedText>
|
||||
<ExternalLink href="https://docs.expo.dev/router/introduction">
|
||||
<ThemedText type="linkPrimary">Learn more</ThemedText>
|
||||
</ExternalLink>
|
||||
</Collapsible>
|
||||
|
||||
<Collapsible title="Android, iOS, and web support">
|
||||
<ThemedView type="backgroundElement" style={styles.collapsibleContent}>
|
||||
<ThemedText type="small">
|
||||
You can open this project on Android, iOS, and the web. To open the web version,
|
||||
press <ThemedText type="smallBold">w</ThemedText> in the terminal running this
|
||||
project.
|
||||
</ThemedText>
|
||||
<Image
|
||||
source={require('@/assets/images/tutorial-web.png')}
|
||||
style={styles.imageTutorial}
|
||||
/>
|
||||
</ThemedView>
|
||||
</Collapsible>
|
||||
|
||||
<Collapsible title="Images">
|
||||
<ThemedText type="small">
|
||||
For static images, you can use the <ThemedText type="code">@2x</ThemedText> and{' '}
|
||||
<ThemedText type="code">@3x</ThemedText> suffixes to provide files for different
|
||||
screen densities.
|
||||
</ThemedText>
|
||||
<Image source={require('@/assets/images/react-logo.png')} style={styles.imageReact} />
|
||||
<ExternalLink href="https://reactnative.dev/docs/images">
|
||||
<ThemedText type="linkPrimary">Learn more</ThemedText>
|
||||
</ExternalLink>
|
||||
</Collapsible>
|
||||
|
||||
<Collapsible title="Light and dark mode components">
|
||||
<ThemedText type="small">
|
||||
This template has light and dark mode support. The{' '}
|
||||
<ThemedText type="code">useColorScheme()</ThemedText> hook lets you inspect what the
|
||||
user's current color scheme is, and so you can adjust UI colors accordingly.
|
||||
</ThemedText>
|
||||
<ExternalLink href="https://docs.expo.dev/develop/user-interface/color-themes/">
|
||||
<ThemedText type="linkPrimary">Learn more</ThemedText>
|
||||
</ExternalLink>
|
||||
</Collapsible>
|
||||
|
||||
<Collapsible title="Animations">
|
||||
<ThemedText type="small">
|
||||
This template includes an example of an animated component. The{' '}
|
||||
<ThemedText type="code">src/components/ui/collapsible.tsx</ThemedText> component uses
|
||||
the powerful <ThemedText type="code">react-native-reanimated</ThemedText> library to
|
||||
animate opening this hint.
|
||||
</ThemedText>
|
||||
</Collapsible>
|
||||
</ThemedView>
|
||||
{Platform.OS === 'web' && <WebBadge />}
|
||||
</ThemedView>
|
||||
</ScrollView>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
scrollView: {
|
||||
flex: 1,
|
||||
},
|
||||
contentContainer: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
container: {
|
||||
maxWidth: MaxContentWidth,
|
||||
flexGrow: 1,
|
||||
},
|
||||
titleContainer: {
|
||||
gap: Spacing.three,
|
||||
alignItems: 'center',
|
||||
paddingHorizontal: Spacing.four,
|
||||
paddingVertical: Spacing.six,
|
||||
},
|
||||
centerText: {
|
||||
textAlign: 'center',
|
||||
},
|
||||
pressed: {
|
||||
opacity: 0.7,
|
||||
},
|
||||
linkButton: {
|
||||
flexDirection: 'row',
|
||||
paddingHorizontal: Spacing.four,
|
||||
paddingVertical: Spacing.two,
|
||||
borderRadius: Spacing.five,
|
||||
justifyContent: 'center',
|
||||
gap: Spacing.one,
|
||||
alignItems: 'center',
|
||||
},
|
||||
sectionsWrapper: {
|
||||
gap: Spacing.five,
|
||||
paddingHorizontal: Spacing.four,
|
||||
paddingTop: Spacing.three,
|
||||
},
|
||||
collapsibleContent: {
|
||||
alignItems: 'center',
|
||||
},
|
||||
imageTutorial: {
|
||||
width: '100%',
|
||||
aspectRatio: 296 / 171,
|
||||
borderRadius: Spacing.three,
|
||||
marginTop: Spacing.two,
|
||||
},
|
||||
imageReact: {
|
||||
width: 100,
|
||||
height: 100,
|
||||
alignSelf: 'center',
|
||||
},
|
||||
});
|
||||
@@ -1,98 +1,252 @@
|
||||
import * as Device from 'expo-device';
|
||||
import { Platform, StyleSheet } from 'react-native';
|
||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
import { StyleSheet, View, Text, FlatList, TouchableOpacity, PermissionsAndroid, Platform } from 'react-native';
|
||||
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';
|
||||
|
||||
import { AnimatedIcon } from '@/components/animated-icon';
|
||||
import { HintRow } from '@/components/hint-row';
|
||||
import { ThemedText } from '@/components/themed-text';
|
||||
import { ThemedView } from '@/components/themed-view';
|
||||
import { WebBadge } from '@/components/web-badge';
|
||||
import { BottomTabInset, MaxContentWidth, Spacing } from '@/constants/theme';
|
||||
export default function DashboardScreen() {
|
||||
useKeepAwake(); // Keep the screen awake
|
||||
|
||||
function getDevMenuHint() {
|
||||
if (Platform.OS === 'web') {
|
||||
return <ThemedText type="small">use browser devtools</ThemedText>;
|
||||
const [status, setStatus] = useState('Disconnected');
|
||||
const [hasPermission, setHasPermission] = useState(false);
|
||||
const [logs, setLogs] = useState<{id: string, time: string, msg: string}[]>([]);
|
||||
const socketRef = useRef<any>(null);
|
||||
|
||||
useEffect(() => {
|
||||
checkAndRequestPermission();
|
||||
connectSocket();
|
||||
|
||||
return () => {
|
||||
if (socketRef.current) {
|
||||
socketRef.current.disconnect();
|
||||
}
|
||||
if (Device.isDevice) {
|
||||
return (
|
||||
<ThemedText type="small">
|
||||
shake device or press <ThemedText type="code">m</ThemedText> in terminal
|
||||
</ThemedText>
|
||||
};
|
||||
}, []);
|
||||
|
||||
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');
|
||||
} else {
|
||||
setHasPermission(false);
|
||||
addLog('SMS permission denied');
|
||||
}
|
||||
const shortcut = Platform.OS === 'android' ? 'cmd+m (or ctrl+m)' : 'cmd+d';
|
||||
return (
|
||||
<ThemedText type="small">
|
||||
press <ThemedText type="code">{shortcut}</ThemedText>
|
||||
</ThemedText>
|
||||
);
|
||||
} catch (err) {
|
||||
console.warn(err);
|
||||
addLog(`Permission error: ${err}`);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const addLog = (msg: string) => {
|
||||
setLogs(prev => [{
|
||||
id: Math.random().toString(),
|
||||
time: new Date().toLocaleTimeString(),
|
||||
msg
|
||||
}, ...prev].slice(0, 50)); // Keep last 50 logs
|
||||
};
|
||||
|
||||
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');
|
||||
return;
|
||||
}
|
||||
|
||||
export default function HomeScreen() {
|
||||
if (socketRef.current) {
|
||||
socketRef.current.disconnect();
|
||||
}
|
||||
|
||||
setStatus('Connecting...');
|
||||
|
||||
const socket = io(endpoint, {
|
||||
auth: { login, password },
|
||||
reconnectionAttempts: Infinity, // Keep trying forever
|
||||
reconnectionDelay: 1000,
|
||||
reconnectionDelayMax: 5000,
|
||||
});
|
||||
|
||||
socket.on('connect', () => {
|
||||
setStatus('Connected');
|
||||
addLog('Connected to backend');
|
||||
});
|
||||
|
||||
socket.on('disconnect', () => {
|
||||
setStatus('Disconnected');
|
||||
addLog('Disconnected from backend');
|
||||
});
|
||||
|
||||
socket.on('connect_error', (err) => {
|
||||
setStatus('Error');
|
||||
addLog(`Connection error: ${err.message}`);
|
||||
});
|
||||
|
||||
socket.on('send_sms', async (data) => {
|
||||
addLog(`Received SMS request ID: ${data.id} to ${data.phone}`);
|
||||
|
||||
try {
|
||||
// Default to rule phone if empty, or use the one provided
|
||||
const targetPhone = data.phone || '+993 61 92 92 48';
|
||||
|
||||
if (Platform.OS === 'android') {
|
||||
SendDirectSms.sendDirectSms(targetPhone, data.message)
|
||||
.then((res: any) => {
|
||||
addLog(`SMS sent successfully to ${targetPhone}`);
|
||||
socket.emit('sms_status', { id: data.id, status: 'sent' });
|
||||
})
|
||||
.catch((err: any) => {
|
||||
addLog(`SMS send failed: ${err}`);
|
||||
socket.emit('sms_status', { id: data.id, status: 'failed' });
|
||||
});
|
||||
} else {
|
||||
addLog('Direct SMS only supported on Android');
|
||||
socket.emit('sms_status', { id: data.id, status: 'failed' });
|
||||
}
|
||||
} catch (error) {
|
||||
addLog(`Error sending SMS: ${error}`);
|
||||
socket.emit('sms_status', { id: data.id, status: 'failed' });
|
||||
}
|
||||
});
|
||||
|
||||
socketRef.current = socket;
|
||||
|
||||
} catch (e) {
|
||||
addLog(`Setup error: ${e}`);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<ThemedView style={styles.container}>
|
||||
<SafeAreaView style={styles.safeArea}>
|
||||
<ThemedView style={styles.heroSection}>
|
||||
<AnimatedIcon />
|
||||
<ThemedText type="title" style={styles.title}>
|
||||
Welcome to Expo
|
||||
</ThemedText>
|
||||
</ThemedView>
|
||||
<View style={styles.container}>
|
||||
<View style={styles.statusCard}>
|
||||
<Text style={styles.statusLabel}>Backend Status:</Text>
|
||||
<Text style={[
|
||||
styles.statusValue,
|
||||
{ color: status === 'Connected' ? 'green' : (status === 'Error' || status === 'Disconnected' ? 'red' : 'orange') }
|
||||
]}>
|
||||
{status}
|
||||
</Text>
|
||||
<View style={styles.buttonRow}>
|
||||
<TouchableOpacity style={styles.reconnectButton} onPress={connectSocket}>
|
||||
<Text style={styles.reconnectText}>Reconnect</Text>
|
||||
</TouchableOpacity>
|
||||
|
||||
<ThemedText type="code" style={styles.code}>
|
||||
get started
|
||||
</ThemedText>
|
||||
{!hasPermission && (
|
||||
<TouchableOpacity style={[styles.reconnectButton, { backgroundColor: '#ffebee', marginLeft: 10 }]} onPress={checkAndRequestPermission}>
|
||||
<Text style={[styles.reconnectText, { color: '#c62828' }]}>Req Permission</Text>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<ThemedView type="backgroundElement" style={styles.stepContainer}>
|
||||
<HintRow
|
||||
title="Try editing"
|
||||
hint={<ThemedText type="code">src/app/index.tsx</ThemedText>}
|
||||
<Text style={styles.logTitle}>Activity Log</Text>
|
||||
<FlatList
|
||||
data={logs}
|
||||
keyExtractor={item => item.id}
|
||||
style={styles.logList}
|
||||
renderItem={({item}) => (
|
||||
<View style={styles.logItem}>
|
||||
<Text style={styles.logTime}>{item.time}</Text>
|
||||
<Text style={styles.logMsg}>{item.msg}</Text>
|
||||
</View>
|
||||
)}
|
||||
/>
|
||||
<HintRow title="Dev tools" hint={getDevMenuHint()} />
|
||||
<HintRow
|
||||
title="Fresh start"
|
||||
hint={<ThemedText type="code">npm run reset-project</ThemedText>}
|
||||
/>
|
||||
</ThemedView>
|
||||
|
||||
{Platform.OS === 'web' && <WebBadge />}
|
||||
</SafeAreaView>
|
||||
</ThemedView>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: '#f5f5f5',
|
||||
},
|
||||
headerButton: {
|
||||
marginRight: 15,
|
||||
},
|
||||
headerButtonText: {
|
||||
color: '#208AEF',
|
||||
fontSize: 16,
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
statusCard: {
|
||||
backgroundColor: '#fff',
|
||||
margin: 15,
|
||||
padding: 20,
|
||||
borderRadius: 10,
|
||||
alignItems: 'center',
|
||||
shadowColor: '#000',
|
||||
shadowOffset: { width: 0, height: 2 },
|
||||
shadowOpacity: 0.1,
|
||||
shadowRadius: 4,
|
||||
elevation: 3,
|
||||
},
|
||||
statusLabel: {
|
||||
fontSize: 16,
|
||||
color: '#666',
|
||||
},
|
||||
statusValue: {
|
||||
fontSize: 24,
|
||||
fontWeight: 'bold',
|
||||
marginTop: 5,
|
||||
},
|
||||
reconnectButton: {
|
||||
paddingVertical: 8,
|
||||
paddingHorizontal: 20,
|
||||
backgroundColor: '#eee',
|
||||
borderRadius: 20,
|
||||
},
|
||||
buttonRow: {
|
||||
flexDirection: 'row',
|
||||
marginTop: 15,
|
||||
justifyContent: 'center',
|
||||
},
|
||||
reconnectText: {
|
||||
color: '#333',
|
||||
fontWeight: '600',
|
||||
},
|
||||
logTitle: {
|
||||
fontSize: 18,
|
||||
fontWeight: 'bold',
|
||||
marginLeft: 15,
|
||||
marginBottom: 10,
|
||||
color: '#333',
|
||||
},
|
||||
logList: {
|
||||
flex: 1,
|
||||
backgroundColor: '#fff',
|
||||
},
|
||||
logItem: {
|
||||
padding: 15,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: '#eee',
|
||||
flexDirection: 'row',
|
||||
},
|
||||
safeArea: {
|
||||
logTime: {
|
||||
color: '#888',
|
||||
fontSize: 12,
|
||||
width: 80,
|
||||
},
|
||||
logMsg: {
|
||||
flex: 1,
|
||||
paddingHorizontal: Spacing.four,
|
||||
alignItems: 'center',
|
||||
gap: Spacing.three,
|
||||
paddingBottom: BottomTabInset + Spacing.three,
|
||||
maxWidth: MaxContentWidth,
|
||||
},
|
||||
heroSection: {
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
flex: 1,
|
||||
paddingHorizontal: Spacing.four,
|
||||
gap: Spacing.four,
|
||||
},
|
||||
title: {
|
||||
textAlign: 'center',
|
||||
},
|
||||
code: {
|
||||
textTransform: 'uppercase',
|
||||
},
|
||||
stepContainer: {
|
||||
gap: Spacing.three,
|
||||
alignSelf: 'stretch',
|
||||
paddingHorizontal: Spacing.three,
|
||||
paddingVertical: Spacing.four,
|
||||
borderRadius: Spacing.four,
|
||||
},
|
||||
color: '#333',
|
||||
fontSize: 14,
|
||||
}
|
||||
});
|
||||
|
||||
145
src/app/settings.tsx
Normal file
145
src/app/settings.tsx
Normal file
@@ -0,0 +1,145 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { StyleSheet, View, Text, TextInput, TouchableOpacity, Switch, ScrollView } from 'react-native';
|
||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||
import { Stack } from 'expo-router';
|
||||
import { useKeepAwake } from 'expo-keep-awake';
|
||||
// We will import socket logic later
|
||||
|
||||
export default function SettingsScreen() {
|
||||
useKeepAwake(); // Keep the screen awake
|
||||
|
||||
const [endpoint, setEndpoint] = useState('http://192.168.1.x:3001');
|
||||
const [login, setLogin] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [isSaved, setIsSaved] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
loadSettings();
|
||||
}, []);
|
||||
|
||||
const loadSettings = async () => {
|
||||
try {
|
||||
const savedEndpoint = await AsyncStorage.getItem('endpoint');
|
||||
const savedLogin = await AsyncStorage.getItem('login');
|
||||
const savedPassword = await AsyncStorage.getItem('password');
|
||||
|
||||
if (savedEndpoint) setEndpoint(savedEndpoint);
|
||||
if (savedLogin) setLogin(savedLogin);
|
||||
if (savedPassword) setPassword(savedPassword);
|
||||
} catch (e) {
|
||||
console.error('Failed to load settings', e);
|
||||
}
|
||||
};
|
||||
|
||||
const saveSettings = async () => {
|
||||
try {
|
||||
await AsyncStorage.setItem('endpoint', endpoint);
|
||||
await AsyncStorage.setItem('login', login);
|
||||
await AsyncStorage.setItem('password', password);
|
||||
setIsSaved(true);
|
||||
setTimeout(() => setIsSaved(false), 2000);
|
||||
|
||||
// In a real app, we would trigger a reconnect here
|
||||
} catch (e) {
|
||||
console.error('Failed to save settings', e);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<ScrollView style={styles.container}>
|
||||
<View style={styles.section}>
|
||||
<Text style={styles.label}>Backend Endpoint</Text>
|
||||
<TextInput
|
||||
style={styles.input}
|
||||
value={endpoint}
|
||||
onChangeText={setEndpoint}
|
||||
placeholder="http://192.168.1.x:3001"
|
||||
autoCapitalize="none"
|
||||
keyboardType="url"
|
||||
/>
|
||||
</View>
|
||||
|
||||
<View style={styles.section}>
|
||||
<Text style={styles.label}>Login</Text>
|
||||
<TextInput
|
||||
style={styles.input}
|
||||
value={login}
|
||||
onChangeText={setLogin}
|
||||
placeholder="admin"
|
||||
autoCapitalize="none"
|
||||
/>
|
||||
</View>
|
||||
|
||||
<View style={styles.section}>
|
||||
<Text style={styles.label}>Password</Text>
|
||||
<TextInput
|
||||
style={styles.input}
|
||||
value={password}
|
||||
onChangeText={setPassword}
|
||||
placeholder="admin123"
|
||||
secureTextEntry
|
||||
/>
|
||||
</View>
|
||||
|
||||
<TouchableOpacity style={styles.button} onPress={saveSettings}>
|
||||
<Text style={styles.buttonText}>{isSaved ? 'Saved!' : 'Save Settings'}</Text>
|
||||
</TouchableOpacity>
|
||||
|
||||
<View style={styles.infoBox}>
|
||||
<Text style={styles.infoText}>
|
||||
Note: This app will keep the screen awake while running to ensure background SMS sending works reliably.
|
||||
</Text>
|
||||
</View>
|
||||
</ScrollView>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
padding: 20,
|
||||
backgroundColor: '#f5f5f5',
|
||||
},
|
||||
section: {
|
||||
marginBottom: 20,
|
||||
},
|
||||
label: {
|
||||
fontSize: 16,
|
||||
fontWeight: 'bold',
|
||||
marginBottom: 8,
|
||||
color: '#333',
|
||||
},
|
||||
input: {
|
||||
backgroundColor: '#fff',
|
||||
borderWidth: 1,
|
||||
borderColor: '#ddd',
|
||||
borderRadius: 8,
|
||||
padding: 12,
|
||||
fontSize: 16,
|
||||
},
|
||||
button: {
|
||||
backgroundColor: '#208AEF',
|
||||
padding: 15,
|
||||
borderRadius: 8,
|
||||
alignItems: 'center',
|
||||
marginTop: 10,
|
||||
},
|
||||
buttonText: {
|
||||
color: '#fff',
|
||||
fontSize: 16,
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
infoBox: {
|
||||
marginTop: 30,
|
||||
padding: 15,
|
||||
backgroundColor: '#e8f4fd',
|
||||
borderRadius: 8,
|
||||
borderLeftWidth: 4,
|
||||
borderLeftColor: '#208AEF',
|
||||
},
|
||||
infoText: {
|
||||
color: '#555',
|
||||
fontSize: 14,
|
||||
lineHeight: 20,
|
||||
}
|
||||
});
|
||||
@@ -20,8 +20,8 @@ export default function AppTabs() {
|
||||
/>
|
||||
</NativeTabs.Trigger>
|
||||
|
||||
<NativeTabs.Trigger name="explore">
|
||||
<NativeTabs.Trigger.Label>Explore</NativeTabs.Trigger.Label>
|
||||
<NativeTabs.Trigger name="settings">
|
||||
<NativeTabs.Trigger.Label>Settings</NativeTabs.Trigger.Label>
|
||||
<NativeTabs.Trigger.Icon
|
||||
src={require('@/assets/images/tabIcons/explore.png')}
|
||||
renderingMode="template"
|
||||
|
||||
Reference in New Issue
Block a user