This commit is contained in:
Mekan1206
2026-06-02 12:10:45 +05:00
parent 01b8d5c988
commit 3cac2d4051
7 changed files with 8598 additions and 269 deletions

View File

@@ -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

File diff suppressed because it is too large Load Diff

View File

@@ -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"
},

View File

@@ -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&apos;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',
},
});

View File

@@ -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';
} 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;
}
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 (
<ThemedText type="small">
press <ThemedText type="code">{shortcut}</ThemedText>
</ThemedText>
);
}
<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>
export default function HomeScreen() {
return (
<ThemedView style={styles.container}>
<SafeAreaView style={styles.safeArea}>
<ThemedView style={styles.heroSection}>
<AnimatedIcon />
<ThemedText type="title" style={styles.title}>
Welcome to&nbsp;Expo
</ThemedText>
</ThemedView>
{!hasPermission && (
<TouchableOpacity style={[styles.reconnectButton, { backgroundColor: '#ffebee', marginLeft: 10 }]} onPress={checkAndRequestPermission}>
<Text style={[styles.reconnectText, { color: '#c62828' }]}>Req Permission</Text>
</TouchableOpacity>
)}
</View>
</View>
<ThemedText type="code" style={styles.code}>
get started
</ThemedText>
<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
View 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,
}
});

View File

@@ -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"