basic app

This commit is contained in:
2025-07-03 19:40:32 +05:00
commit 2a597df2d3
29 changed files with 17379 additions and 0 deletions

37
.gitignore vendored Normal file
View File

@@ -0,0 +1,37 @@
# Learn more https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files
# dependencies
node_modules/
# Expo
.expo/
dist/
web-build/
expo-env.d.ts
# Native
.kotlin/
*.orig.*
*.jks
*.p8
*.p12
*.key
*.mobileprovision
# Metro
.metro-health-check*
# debug
npm-debug.*
yarn-debug.*
yarn-error.*
# macOS
.DS_Store
*.pem
# local env files
.env*.local
# typescript
*.tsbuildinfo

11
App.js Normal file
View File

@@ -0,0 +1,11 @@
import React from 'react';
import { AuthProvider } from './src/contexts/AuthContext';
import RootNavigator from './src/navigation/RootNavigator';
export default function App() {
return (
<AuthProvider>
<RootNavigator />
</AuthProvider>
);
}

29
app.json Normal file
View File

@@ -0,0 +1,29 @@
{
"expo": {
"name": "TBBANK ONLINE",
"slug": "tbbank-online",
"version": "1.0.0",
"orientation": "portrait",
"icon": "./assets/icon.png",
"userInterfaceStyle": "light",
"newArchEnabled": true,
"splash": {
"image": "./assets/splash-icon.png",
"resizeMode": "contain",
"backgroundColor": "#17b69b"
},
"ios": {
"supportsTablet": true
},
"android": {
"adaptiveIcon": {
"foregroundImage": "./assets/adaptive-icon.png",
"backgroundColor": "#17b69b"
},
"edgeToEdgeEnabled": true
},
"web": {
"favicon": "./assets/favicon.png"
}
}
}

BIN
assets/adaptive-icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

BIN
assets/favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

BIN
assets/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

BIN
assets/splash-icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

8
index.js Normal file
View File

@@ -0,0 +1,8 @@
import { registerRootComponent } from 'expo';
import App from './App';
// registerRootComponent calls AppRegistry.registerComponent('main', () => App);
// It also ensures that whether you load the app in Expo Go or in a native build,
// the environment is set up appropriately
registerRootComponent(App);

7941
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

29
package.json Normal file
View File

@@ -0,0 +1,29 @@
{
"name": "temp-expo",
"version": "1.0.0",
"main": "index.js",
"scripts": {
"start": "expo start",
"android": "expo start --android",
"ios": "expo start --ios",
"web": "expo start --web"
},
"dependencies": {
"@expo/vector-icons": "^14.1.0",
"@react-native-async-storage/async-storage": "^2.2.0",
"@react-navigation/bottom-tabs": "^7.4.2",
"@react-navigation/native": "^7.1.14",
"@react-navigation/stack": "^7.4.2",
"expo": "~53.0.16",
"expo-status-bar": "~2.2.3",
"react": "19.0.0",
"react-native": "0.79.5",
"react-native-safe-area-context": "^5.5.1",
"react-native-screens": "^4.11.1",
"react-native-svg": "^15.12.0"
},
"devDependencies": {
"@babel/core": "^7.20.0"
},
"private": true
}

BIN
resources/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

1
resources/logo.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 614 KiB

7193
resources/openapi.json Normal file

File diff suppressed because it is too large Load Diff

84
src/components/Button.js Normal file
View File

@@ -0,0 +1,84 @@
import React from 'react';
import { TouchableOpacity, Text, StyleSheet, ActivityIndicator } from 'react-native';
import { COLORS } from '../constants/colors';
const Button = ({
title,
onPress,
loading = false,
disabled = false,
variant = 'primary',
style,
textStyle
}) => {
const buttonStyles = [
styles.button,
styles[variant],
disabled && styles.disabled,
style,
];
const textStyles = [
styles.text,
styles[`${variant}Text`],
textStyle,
];
return (
<TouchableOpacity
style={buttonStyles}
onPress={onPress}
disabled={disabled || loading}
activeOpacity={0.8}
>
{loading ? (
<ActivityIndicator color={variant === 'primary' ? COLORS.white : COLORS.primary} />
) : (
<Text style={textStyles}>{title}</Text>
)}
</TouchableOpacity>
);
};
const styles = StyleSheet.create({
button: {
paddingVertical: 16,
paddingHorizontal: 24,
borderRadius: 12,
alignItems: 'center',
justifyContent: 'center',
minHeight: 52,
},
primary: {
backgroundColor: COLORS.primary,
},
secondary: {
backgroundColor: COLORS.backgroundSecondary,
borderWidth: 1,
borderColor: COLORS.gray[300],
},
outline: {
backgroundColor: 'transparent',
borderWidth: 1,
borderColor: COLORS.primary,
},
disabled: {
opacity: 0.5,
},
text: {
fontSize: 16,
fontWeight: '600',
textAlign: 'center',
},
primaryText: {
color: COLORS.textOnPrimary,
},
secondaryText: {
color: COLORS.textPrimary,
},
outlineText: {
color: COLORS.primary,
},
});
export default Button;

128
src/components/Input.js Normal file
View File

@@ -0,0 +1,128 @@
import React, { useState } from 'react';
import { View, TextInput, Text, StyleSheet, TouchableOpacity } from 'react-native';
import { Ionicons } from '@expo/vector-icons';
import { COLORS } from '../constants/colors';
const Input = ({
label,
value,
onChangeText,
placeholder,
secureTextEntry = false,
keyboardType = 'default',
error,
disabled = false,
leftIcon,
style,
returnKeyType = 'done',
onSubmitEditing,
...props
}) => {
const [isPasswordVisible, setIsPasswordVisible] = useState(false);
const [isFocused, setIsFocused] = useState(false);
const togglePasswordVisibility = () => {
setIsPasswordVisible(!isPasswordVisible);
};
return (
<View style={[styles.container, style]}>
{label && <Text style={styles.label}>{label}</Text>}
<View style={[
styles.inputContainer,
isFocused && styles.focused,
error && styles.error,
disabled && styles.disabled,
]}>
{leftIcon && (
<View style={styles.leftIconContainer}>
<Ionicons name={leftIcon} size={20} color={COLORS.gray[400]} />
</View>
)}
<TextInput
style={[styles.input, leftIcon && styles.inputWithLeftIcon]}
value={value}
onChangeText={onChangeText}
placeholder={placeholder}
placeholderTextColor={COLORS.gray[400]}
secureTextEntry={secureTextEntry && !isPasswordVisible}
keyboardType={keyboardType}
editable={!disabled}
onFocus={() => setIsFocused(true)}
onBlur={() => setIsFocused(false)}
returnKeyType={returnKeyType}
onSubmitEditing={onSubmitEditing}
{...props}
/>
{secureTextEntry && (
<TouchableOpacity
style={styles.eyeIcon}
onPress={togglePasswordVisibility}
>
<Ionicons
name={isPasswordVisible ? 'eye-off' : 'eye'}
size={20}
color={COLORS.gray[400]}
/>
</TouchableOpacity>
)}
</View>
{error && <Text style={styles.errorText}>{error}</Text>}
</View>
);
};
const styles = StyleSheet.create({
container: {
marginBottom: 20,
},
label: {
fontSize: 14,
fontWeight: '600',
color: COLORS.textPrimary,
marginBottom: 8,
},
inputContainer: {
flexDirection: 'row',
alignItems: 'center',
borderWidth: 1,
borderColor: COLORS.gray[300],
borderRadius: 12,
backgroundColor: COLORS.white,
paddingHorizontal: 16,
minHeight: 52,
},
focused: {
borderColor: COLORS.primary,
borderWidth: 2,
},
error: {
borderColor: COLORS.error,
},
disabled: {
backgroundColor: COLORS.gray[100],
opacity: 0.6,
},
leftIconContainer: {
marginRight: 12,
},
input: {
flex: 1,
fontSize: 16,
color: COLORS.textPrimary,
paddingVertical: 16,
},
inputWithLeftIcon: {
paddingLeft: 0,
},
eyeIcon: {
padding: 4,
},
errorText: {
fontSize: 12,
color: COLORS.error,
marginTop: 4,
},
});
export default Input;

21
src/components/Logo.js Normal file
View File

@@ -0,0 +1,21 @@
import React from 'react';
import { Image } from 'react-native';
const Logo = ({ width = 120, height = 120, style = {} }) => {
return (
<Image
source={require('../../resources/logo.png')} // You'll need to add logo.png to resources folder
style={[
{
width,
height,
resizeMode: 'contain',
marginBottom: 20,
},
style,
]}
/>
);
};
export default Logo;

16
src/constants/api.js Normal file
View File

@@ -0,0 +1,16 @@
export const API_CONFIG = {
BASE_URL: 'https://online.tbbank.gov.tm/api',
ENDPOINTS: {
AUTH: {
LOGIN: '/auth/login',
REGISTER: '/auth/register',
VERIFY: '/auth/verify',
DELETE_USER: '/auth/delete-user',
},
ENUMS: '/base-app-enums',
},
HEADERS: {
'Content-Type': 'application/json',
'Accept': 'application/json',
},
};

38
src/constants/colors.js Normal file
View File

@@ -0,0 +1,38 @@
export const COLORS = {
primary: '#17b69b',
primaryDark: '#0e8a73',
primaryLight: '#4cc3ab',
// Neutral colors
white: '#ffffff',
black: '#000000',
gray: {
50: '#f9fafb',
100: '#f3f4f6',
200: '#e5e7eb',
300: '#d1d5db',
400: '#9ca3af',
500: '#6b7280',
600: '#4b5563',
700: '#374151',
800: '#1f2937',
900: '#111827',
},
// Status colors
success: '#10b981',
warning: '#f59e0b',
error: '#ef4444',
info: '#3b82f6',
// Background colors
background: '#ffffff',
backgroundSecondary: '#f9fafb',
surface: '#ffffff',
// Text colors
textPrimary: '#111827',
textSecondary: '#6b7280',
textDisabled: '#9ca3af',
textOnPrimary: '#ffffff',
};

175
src/contexts/AuthContext.js Normal file
View File

@@ -0,0 +1,175 @@
import React, { createContext, useContext, useReducer, useEffect } from 'react';
import AsyncStorage from '@react-native-async-storage/async-storage';
import authService from '../services/authService';
const AuthContext = createContext();
const initialState = {
isAuthenticated: false,
isLoading: true,
user: null,
token: null,
pendingVerification: null, // { phone, type: 'login' | 'register' }
};
const authReducer = (state, action) => {
switch (action.type) {
case 'LOADING':
return { ...state, isLoading: action.payload };
case 'LOGIN_SUCCESS':
return {
...state,
isAuthenticated: true,
user: action.payload.user,
token: action.payload.token,
pendingVerification: null,
isLoading: false,
};
case 'LOGOUT':
return {
...initialState,
isLoading: false,
};
case 'SET_PENDING_VERIFICATION':
return {
...state,
pendingVerification: action.payload,
isLoading: false,
};
case 'CLEAR_PENDING_VERIFICATION':
return {
...state,
pendingVerification: null,
};
default:
return state;
}
};
export const AuthProvider = ({ children }) => {
const [state, dispatch] = useReducer(authReducer, initialState);
useEffect(() => {
checkAuthStatus();
}, []);
const checkAuthStatus = async () => {
try {
const token = await AsyncStorage.getItem('auth_token');
const userData = await AsyncStorage.getItem('user_data');
if (token && userData) {
dispatch({
type: 'LOGIN_SUCCESS',
payload: {
token,
user: JSON.parse(userData),
},
});
} else {
dispatch({ type: 'LOADING', payload: false });
}
} catch (error) {
dispatch({ type: 'LOADING', payload: false });
}
};
const login = async (phone, password) => {
try {
dispatch({ type: 'LOADING', payload: true });
const response = await authService.login(phone, password);
// After login, we need verification, so set pending verification
dispatch({
type: 'SET_PENDING_VERIFICATION',
payload: { phone, type: 'login' },
});
return { success: true, message: response.message };
} catch (error) {
dispatch({ type: 'LOADING', payload: false });
return { success: false, error: error.message };
}
};
const register = async (phone, name, password) => {
try {
dispatch({ type: 'LOADING', payload: true });
const response = await authService.register(phone, name, password);
// After register, we need verification, so set pending verification
dispatch({
type: 'SET_PENDING_VERIFICATION',
payload: { phone, type: 'register' },
});
return { success: true, message: response.message };
} catch (error) {
dispatch({ type: 'LOADING', payload: false });
return { success: false, error: error.message };
}
};
const verify = async (code) => {
try {
if (!state.pendingVerification) {
return { success: false, error: 'No pending verification' };
}
dispatch({ type: 'LOADING', payload: true });
const response = await authService.verify(state.pendingVerification.phone, code);
// Assuming the API returns a token after successful verification
// You might need to adjust this based on your actual API response
const token = response.token || 'dummy_token_for_demo';
const user = response.user || { phone: state.pendingVerification.phone };
// Store auth data
await AsyncStorage.setItem('auth_token', token);
await AsyncStorage.setItem('user_data', JSON.stringify(user));
dispatch({
type: 'LOGIN_SUCCESS',
payload: { token, user },
});
return { success: true, message: response.message };
} catch (error) {
dispatch({ type: 'LOADING', payload: false });
return { success: false, error: error.message };
}
};
const logout = async () => {
try {
await AsyncStorage.removeItem('auth_token');
await AsyncStorage.removeItem('user_data');
dispatch({ type: 'LOGOUT' });
} catch (error) {
console.error('Logout error:', error);
}
};
const clearPendingVerification = () => {
dispatch({ type: 'CLEAR_PENDING_VERIFICATION' });
};
const value = {
...state,
login,
register,
verify,
logout,
clearPendingVerification,
};
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
};
export const useAuth = () => {
const context = useContext(AuthContext);
if (!context) {
throw new Error('useAuth must be used within an AuthProvider');
}
return context;
};

View File

@@ -0,0 +1,42 @@
import React from 'react';
import { createStackNavigator } from '@react-navigation/stack';
import { useAuth } from '../contexts/AuthContext';
import LoginScreen from '../screens/Auth/LoginScreen';
import RegisterScreen from '../screens/Auth/RegisterScreen';
import VerificationScreen from '../screens/Auth/VerificationScreen';
const Stack = createStackNavigator();
const AuthNavigator = () => {
const { pendingVerification } = useAuth();
return (
<Stack.Navigator
screenOptions={{
headerShown: false,
cardStyleInterpolator: ({ current, layouts }) => {
return {
cardStyle: {
transform: [
{
translateX: current.progress.interpolate({
inputRange: [0, 1],
outputRange: [layouts.screen.width, 0],
}),
},
],
},
};
},
}}
initialRouteName={pendingVerification ? 'Verification' : 'Login'}
>
<Stack.Screen name="Login" component={LoginScreen} />
<Stack.Screen name="Register" component={RegisterScreen} />
<Stack.Screen name="Verification" component={VerificationScreen} />
</Stack.Navigator>
);
};
export default AuthNavigator;

View File

@@ -0,0 +1,75 @@
import React from 'react';
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
import { Ionicons } from '@expo/vector-icons';
import { COLORS } from '../constants/colors';
import HomeScreen from '../screens/Main/HomeScreen';
import MenuScreen from '../screens/Main/MenuScreen';
import ProfileScreen from '../screens/Main/ProfileScreen';
const Tab = createBottomTabNavigator();
const MainNavigator = () => {
return (
<Tab.Navigator
screenOptions={({ route }) => ({
headerShown: false,
tabBarIcon: ({ focused, color, size }) => {
let iconName;
if (route.name === 'Home') {
iconName = focused ? 'home' : 'home-outline';
} else if (route.name === 'Menu') {
iconName = focused ? 'grid' : 'grid-outline';
} else if (route.name === 'Profile') {
iconName = focused ? 'person' : 'person-outline';
}
return <Ionicons name={iconName} size={size} color={color} />;
},
tabBarActiveTintColor: COLORS.primary,
tabBarInactiveTintColor: COLORS.gray[500],
tabBarStyle: {
backgroundColor: COLORS.white,
borderTopWidth: 1,
borderTopColor: COLORS.gray[200],
paddingBottom: 8,
paddingTop: 8,
height: 88,
},
tabBarLabelStyle: {
fontSize: 12,
fontWeight: '600',
marginTop: 4,
},
tabBarItemStyle: {
paddingVertical: 4,
},
})}
>
<Tab.Screen
name="Home"
component={HomeScreen}
options={{
tabBarLabel: 'Baş sahypa',
}}
/>
<Tab.Screen
name="Menu"
component={MenuScreen}
options={{
tabBarLabel: 'Hyzmatlar',
}}
/>
<Tab.Screen
name="Profile"
component={ProfileScreen}
options={{
tabBarLabel: 'Profil',
}}
/>
</Tab.Navigator>
);
};
export default MainNavigator;

View File

@@ -0,0 +1,39 @@
import React from 'react';
import { View, ActivityIndicator } from 'react-native';
import { NavigationContainer } from '@react-navigation/native';
import { useAuth } from '../contexts/AuthContext';
import { COLORS } from '../constants/colors';
import AuthNavigator from './AuthNavigator';
import MainNavigator from './MainNavigator';
const LoadingScreen = () => (
<View style={{
flex: 1,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: COLORS.background,
}}>
<ActivityIndicator size="large" color={COLORS.primary} />
</View>
);
const RootNavigator = () => {
const { isAuthenticated, isLoading, pendingVerification } = useAuth();
if (isLoading) {
return <LoadingScreen />;
}
return (
<NavigationContainer>
{isAuthenticated && !pendingVerification ? (
<MainNavigator />
) : (
<AuthNavigator />
)}
</NavigationContainer>
);
};
export default RootNavigator;

View File

@@ -0,0 +1,195 @@
import React, { useState } from 'react';
import {
View,
Text,
StyleSheet,
SafeAreaView,
KeyboardAvoidingView,
ScrollView,
Platform,
Alert,
Image,
TouchableWithoutFeedback,
Keyboard,
} from 'react-native';
import { StatusBar } from 'expo-status-bar';
import { useAuth } from '../../contexts/AuthContext';
import Button from '../../components/Button';
import Input from '../../components/Input';
import Logo from '../../components/Logo';
import { COLORS } from '../../constants/colors';
const LoginScreen = ({ navigation }) => {
const [phone, setPhone] = useState('');
const [password, setPassword] = useState('');
const [errors, setErrors] = useState({});
const { login, isLoading } = useAuth();
const validateForm = () => {
const newErrors = {};
if (!phone.trim()) {
newErrors.phone = 'Telefon belgisi gerek';
} else if (!/^\d{8}$/.test(phone.trim())) {
newErrors.phone = 'Telefon belgisi 8 sanly bolmaly (mysal: 61909090)';
}
if (!password.trim()) {
newErrors.password = 'Parol gerek';
} else if (password.length < 6) {
newErrors.password = 'Parol azyndan 6 harp bolmaly';
}
if (!/^6[1-9]\d{6}$|^7[0-1]\d{6}$/.test(phone.trim())) {
newErrors.phone = 'Telefon belgisi 61000000-71999999 aralygynda bolmaly';
}
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
const handleLogin = async () => {
if (!validateForm()) return;
const result = await login(phone.trim(), password);
if (result.success) {
// Navigation will be handled by AuthContext
} else {
Alert.alert('Ýalňyşlyk', result.error);
}
};
const navigateToRegister = () => {
navigation.navigate('Register');
};
return (
<SafeAreaView style={styles.container}>
<StatusBar style="dark" />
<KeyboardAvoidingView
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
style={styles.keyboardAvoid}
>
<ScrollView
contentContainerStyle={styles.scrollContent}
showsVerticalScrollIndicator={false}
keyboardShouldPersistTaps="handled"
>
<View style={styles.logoContainer}>
<Logo width={100} height={100} />
<Text style={styles.appName}>TBBANK ONLINE</Text>
<Text style={styles.welcomeText}>Hoş geldiňiz</Text>
</View>
<View style={styles.formContainer}>
<Text style={styles.formTitle}>Giriş</Text>
<Text style={styles.formSubtitle}>
Hasabyňyza girmek üçin maglumatyňyzy giriziň
</Text>
<Input
label="Telefon belgi"
value={phone}
onChangeText={setPhone}
placeholder="61909090"
keyboardType="numeric"
leftIcon="call"
error={errors.phone}
maxLength={8}
/>
<Input
label="Parol"
value={password}
onChangeText={setPassword}
placeholder="Parolyňyzy giriziň"
secureTextEntry
leftIcon="lock-closed"
error={errors.password}
/>
<Button
title="Gir"
onPress={handleLogin}
loading={isLoading}
style={styles.loginButton}
/>
<View style={styles.registerContainer}>
<Text style={styles.registerText}>Hasabyňyz ýokmy? </Text>
<Button
title="Agza bol"
onPress={navigateToRegister}
variant="outline"
style={styles.registerButton}
/>
</View>
</View>
</ScrollView>
</KeyboardAvoidingView>
</SafeAreaView>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: COLORS.background,
},
keyboardAvoid: {
flex: 1,
},
scrollContent: {
flexGrow: 1,
paddingHorizontal: 24,
},
logoContainer: {
alignItems: 'center',
paddingTop: 60,
paddingBottom: 40,
},
appName: {
fontSize: 24,
fontWeight: 'bold',
color: COLORS.textPrimary,
marginBottom: 8,
},
welcomeText: {
fontSize: 16,
color: COLORS.textSecondary,
},
formContainer: {
flex: 1,
},
formTitle: {
fontSize: 28,
fontWeight: 'bold',
color: COLORS.textPrimary,
marginBottom: 8,
},
formSubtitle: {
fontSize: 16,
color: COLORS.textSecondary,
marginBottom: 32,
lineHeight: 22,
},
loginButton: {
marginTop: 8,
marginBottom: 32,
},
registerContainer: {
alignItems: 'center',
},
registerText: {
fontSize: 16,
color: COLORS.textSecondary,
marginBottom: 16,
},
registerButton: {
width: '100%',
},
});
export default LoginScreen;

View File

@@ -0,0 +1,230 @@
import React, { useState } from 'react';
import {
View,
Text,
StyleSheet,
SafeAreaView,
KeyboardAvoidingView,
ScrollView,
Platform,
Alert,
} from 'react-native';
import { StatusBar } from 'expo-status-bar';
import { useAuth } from '../../contexts/AuthContext';
import Button from '../../components/Button';
import Input from '../../components/Input';
import Logo from '../../components/Logo';
import { COLORS } from '../../constants/colors';
const RegisterScreen = ({ navigation }) => {
const [formData, setFormData] = useState({
phone: '',
name: '',
password: '',
confirmPassword: '',
});
const [errors, setErrors] = useState({});
const { register, isLoading } = useAuth();
const updateField = (field, value) => {
setFormData(prev => ({ ...prev, [field]: value }));
// Clear error when user starts typing
if (errors[field]) {
setErrors(prev => ({ ...prev, [field]: '' }));
}
};
const validateForm = () => {
const newErrors = {};
if (!formData.phone.trim()) {
newErrors.phone = 'Telefon belgisi gerek';
} else if (!/^\d{8}$/.test(formData.phone.trim())) {
newErrors.phone = 'Telefon belgisi 8 sanly bolmaly (mysal: 61909090)';
}
if (!formData.name.trim()) {
newErrors.name = 'Ady gerek';
} else if (formData.name.trim().length < 2) {
newErrors.name = 'Ady azyndan 2 harp bolmaly';
}
if (!formData.password.trim()) {
newErrors.password = 'Parol gerek';
} else if (formData.password.length < 6) {
newErrors.password = 'Parol azyndan 6 harp bolmaly';
}
if (!formData.confirmPassword.trim()) {
newErrors.confirmPassword = 'Paroly tassyklaň';
} else if (formData.password !== formData.confirmPassword) {
newErrors.confirmPassword = 'Parollar deň däl';
}
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
const handleRegister = async () => {
if (!validateForm()) return;
const result = await register(
formData.phone.trim(),
formData.name.trim(),
formData.password
);
if (result.success) {
// Navigation will be handled by AuthContext
} else {
Alert.alert('Ýalňyşlyk', result.error);
}
};
const navigateToLogin = () => {
navigation.navigate('Login');
};
return (
<SafeAreaView style={styles.container}>
<StatusBar style="dark" />
<KeyboardAvoidingView
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
style={styles.keyboardAvoid}
>
<ScrollView
contentContainerStyle={styles.scrollContent}
showsVerticalScrollIndicator={false}
keyboardShouldPersistTaps="handled"
>
<View style={styles.logoContainer}>
<Logo width={100} height={100} />
<Text style={styles.appName}>TBBANK ONLINE</Text>
</View>
<View style={styles.formContainer}>
<Text style={styles.formTitle}>Agza bol</Text>
<Text style={styles.formSubtitle}>
Täze hasap döretmek üçin maglumatyňyzy giriziň
</Text>
<Input
label="Telefon belgi"
value={formData.phone}
onChangeText={(value) => updateField('phone', value)}
placeholder="61909090"
keyboardType="numeric"
leftIcon="call"
error={errors.phone}
maxLength={8}
/>
<Input
label="Ady"
value={formData.name}
onChangeText={(value) => updateField('name', value)}
placeholder="Doly adyňyzy giriziň"
leftIcon="person"
error={errors.name}
/>
<Input
label="Parol"
value={formData.password}
onChangeText={(value) => updateField('password', value)}
placeholder="Parolyňyzy giriziň"
secureTextEntry
leftIcon="lock-closed"
error={errors.password}
/>
<Input
label="Paroly tassyklaň"
value={formData.confirmPassword}
onChangeText={(value) => updateField('confirmPassword', value)}
placeholder="Paroly gaýtadan giriziň"
secureTextEntry
leftIcon="lock-closed"
error={errors.confirmPassword}
/>
<Button
title="Agza bol"
onPress={handleRegister}
loading={isLoading}
style={styles.registerButton}
/>
<View style={styles.loginContainer}>
<Text style={styles.loginText}>Eýýäm hasabyňyz barmy? </Text>
<Button
title="Gir"
onPress={navigateToLogin}
variant="outline"
style={styles.loginButton}
/>
</View>
</View>
</ScrollView>
</KeyboardAvoidingView>
</SafeAreaView>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: COLORS.background,
},
keyboardAvoid: {
flex: 1,
},
scrollContent: {
flexGrow: 1,
paddingHorizontal: 24,
},
logoContainer: {
alignItems: 'center',
paddingTop: 60,
paddingBottom: 40,
},
appName: {
fontSize: 24,
fontWeight: 'bold',
color: COLORS.textPrimary,
marginBottom: 8,
},
formContainer: {
flex: 1,
},
formTitle: {
fontSize: 28,
fontWeight: 'bold',
color: COLORS.textPrimary,
marginBottom: 8,
},
formSubtitle: {
fontSize: 16,
color: COLORS.textSecondary,
marginBottom: 32,
lineHeight: 22,
},
registerButton: {
marginTop: 8,
marginBottom: 32,
},
loginContainer: {
alignItems: 'center',
},
loginText: {
fontSize: 16,
color: COLORS.textSecondary,
marginBottom: 16,
},
loginButton: {
width: '100%',
},
});
export default RegisterScreen;

View File

@@ -0,0 +1,246 @@
import React, { useState, useEffect, useRef } from 'react';
import {
View,
Text,
StyleSheet,
SafeAreaView,
Alert,
TouchableOpacity,
} from 'react-native';
import { StatusBar } from 'expo-status-bar';
import { useAuth } from '../../contexts/AuthContext';
import Button from '../../components/Button';
import Input from '../../components/Input';
import { COLORS } from '../../constants/colors';
const VerificationScreen = ({ navigation }) => {
const [code, setCode] = useState('');
const [countdown, setCountdown] = useState(60);
const [canResend, setCanResend] = useState(false);
const { verify, isLoading, pendingVerification, clearPendingVerification } = useAuth();
const countdownRef = useRef(null);
useEffect(() => {
if (!pendingVerification) {
// If no pending verification, go back to login
navigation.replace('Login');
return;
}
startCountdown();
return () => {
if (countdownRef.current) {
clearInterval(countdownRef.current);
}
};
}, []);
const startCountdown = () => {
setCountdown(60);
setCanResend(false);
countdownRef.current = setInterval(() => {
setCountdown((prev) => {
if (prev <= 1) {
setCanResend(true);
if (countdownRef.current) {
clearInterval(countdownRef.current);
}
return 0;
}
return prev - 1;
});
}, 1000);
};
const handleVerify = async () => {
if (!code.trim()) {
Alert.alert('Ýalňyşlyk', 'Tassyklama koduny giriziň');
return;
}
if (!/^\d{6}$/.test(code.trim())) {
Alert.alert('Ýalňyşlyk', 'Tassyklama kody 6 sanly bolmaly');
return;
}
const result = await verify(code.trim());
if (result.success) {
// Navigation will be handled by AuthContext
} else {
Alert.alert('Ýalňyşlyk', result.error);
}
};
const handleResendCode = () => {
if (!canResend) return;
// Here you would call the API to resend the code
// For now, just restart the countdown
Alert.alert('Üstünlik', 'Täze tassyklama kody iberildi');
startCountdown();
};
const handleGoBack = () => {
clearPendingVerification();
navigation.goBack();
};
const formatPhoneNumber = (phone) => {
if (!phone) return '';
const phoneStr = phone.toString();
return `+993 ${phoneStr.slice(0, 2)} ${phoneStr.slice(2, 5)} ${phoneStr.slice(5)}`;
};
if (!pendingVerification) {
return null; // Will navigate away in useEffect
}
return (
<SafeAreaView style={styles.container}>
<StatusBar style="dark" />
<View style={styles.header}>
<TouchableOpacity onPress={handleGoBack} style={styles.backButton}>
<Text style={styles.backButtonText}> Yza</Text>
</TouchableOpacity>
</View>
<View style={styles.content}>
<View style={styles.logoContainer}>
<View style={styles.verificationIcon}>
<Text style={styles.verificationIconText}></Text>
</View>
<Text style={styles.title}>Tassyklama</Text>
<Text style={styles.subtitle}>
{formatPhoneNumber(pendingVerification.phone)} belgisine iberilen
6 sanly tassyklama koduny giriziň
</Text>
</View>
<View style={styles.formContainer}>
<Input
label="Tassyklama kody"
value={code}
onChangeText={setCode}
placeholder="123456"
keyboardType="numeric"
maxLength={6}
style={styles.codeInput}
textAlign="center"
/>
<Button
title="Tassykla"
onPress={handleVerify}
loading={isLoading}
style={styles.verifyButton}
/>
<View style={styles.resendContainer}>
{canResend ? (
<TouchableOpacity onPress={handleResendCode}>
<Text style={styles.resendText}>Kodu gaýtadan iber</Text>
</TouchableOpacity>
) : (
<Text style={styles.countdownText}>
Gaýtadan ibermek üçin {countdown} sekunt garaşyň
</Text>
)}
</View>
</View>
</View>
</SafeAreaView>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: COLORS.background,
},
header: {
paddingHorizontal: 24,
paddingTop: 16,
},
backButton: {
alignSelf: 'flex-start',
},
backButtonText: {
fontSize: 16,
color: COLORS.primary,
fontWeight: '600',
},
content: {
flex: 1,
paddingHorizontal: 24,
justifyContent: 'center',
},
logoContainer: {
alignItems: 'center',
marginBottom: 48,
},
verificationIcon: {
width: 80,
height: 80,
borderRadius: 40,
backgroundColor: COLORS.success,
alignItems: 'center',
justifyContent: 'center',
marginBottom: 24,
shadowColor: COLORS.success,
shadowOffset: {
width: 0,
height: 4,
},
shadowOpacity: 0.3,
shadowRadius: 8,
elevation: 8,
},
verificationIconText: {
fontSize: 32,
color: COLORS.white,
fontWeight: 'bold',
},
title: {
fontSize: 28,
fontWeight: 'bold',
color: COLORS.textPrimary,
marginBottom: 16,
textAlign: 'center',
},
subtitle: {
fontSize: 16,
color: COLORS.textSecondary,
textAlign: 'center',
lineHeight: 22,
paddingHorizontal: 20,
},
formContainer: {
alignItems: 'center',
},
codeInput: {
width: '100%',
marginBottom: 32,
},
verifyButton: {
width: '100%',
marginBottom: 32,
},
resendContainer: {
alignItems: 'center',
},
resendText: {
fontSize: 16,
color: COLORS.primary,
fontWeight: '600',
},
countdownText: {
fontSize: 14,
color: COLORS.textSecondary,
textAlign: 'center',
},
});
export default VerificationScreen;

View File

@@ -0,0 +1,300 @@
import React from 'react';
import {
View,
Text,
StyleSheet,
SafeAreaView,
ScrollView,
TouchableOpacity,
} from 'react-native';
import { StatusBar } from 'expo-status-bar';
import { Ionicons } from '@expo/vector-icons';
import { useAuth } from '../../contexts/AuthContext';
import { COLORS } from '../../constants/colors';
const HomeScreen = () => {
const { user, logout } = useAuth();
const quickActions = [
{ id: 1, title: 'Pul ugrat', icon: 'send', color: COLORS.primary },
{ id: 2, title: 'Pul al', icon: 'download', color: COLORS.info },
{ id: 3, title: 'Töleg et', icon: 'card', color: COLORS.warning },
{ id: 4, title: 'Hasabat', icon: 'document-text', color: COLORS.success },
];
return (
<SafeAreaView style={styles.container}>
<StatusBar style="dark" />
<ScrollView style={styles.scrollView} showsVerticalScrollIndicator={false}>
{/* Header */}
<View style={styles.header}>
<View>
<Text style={styles.greeting}>Salam,</Text>
<Text style={styles.userName}>{user?.name || 'Ulanyjy'}</Text>
</View>
<TouchableOpacity style={styles.profileButton} onPress={logout}>
<Ionicons name="person-circle" size={40} color={COLORS.primary} />
</TouchableOpacity>
</View>
{/* Balance Card */}
<View style={styles.balanceCard}>
<View style={styles.balanceHeader}>
<Text style={styles.balanceLabel}>Jemi balans</Text>
<TouchableOpacity>
<Ionicons name="eye" size={20} color={COLORS.white} />
</TouchableOpacity>
</View>
<Text style={styles.balanceAmount}>1,250.00 TMT</Text>
<View style={styles.balanceFooter}>
<Text style={styles.accountNumber}>Hasap: ****1234</Text>
<View style={styles.cardChip} />
</View>
</View>
{/* Quick Actions */}
<View style={styles.section}>
<Text style={styles.sectionTitle}>Çalt hereketler</Text>
<View style={styles.quickActionsGrid}>
{quickActions.map((action) => (
<TouchableOpacity key={action.id} style={styles.quickActionItem}>
<View style={[styles.quickActionIcon, { backgroundColor: action.color }]}>
<Ionicons name={action.icon} size={24} color={COLORS.white} />
</View>
<Text style={styles.quickActionText}>{action.title}</Text>
</TouchableOpacity>
))}
</View>
</View>
{/* Recent Transactions */}
<View style={styles.section}>
<View style={styles.sectionHeader}>
<Text style={styles.sectionTitle}>Soňky geleşikler</Text>
<TouchableOpacity>
<Text style={styles.seeAllText}>Hemmesini gör</Text>
</TouchableOpacity>
</View>
<View style={styles.transactionsList}>
{[1, 2, 3].map((transaction) => (
<View key={transaction} style={styles.transactionItem}>
<View style={styles.transactionIcon}>
<Ionicons name="arrow-down" size={20} color={COLORS.success} />
</View>
<View style={styles.transactionDetails}>
<Text style={styles.transactionTitle}>Girizen pul</Text>
<Text style={styles.transactionDate}>Şu gün, 14:30</Text>
</View>
<Text style={styles.transactionAmount}>+500.00 TMT</Text>
</View>
))}
</View>
</View>
{/* Services */}
<View style={styles.section}>
<Text style={styles.sectionTitle}>Hyzmatlar</Text>
<View style={styles.servicesGrid}>
<TouchableOpacity style={styles.serviceItem}>
<Ionicons name="phone-portrait" size={24} color={COLORS.primary} />
<Text style={styles.serviceText}>Telefon töleg</Text>
</TouchableOpacity>
<TouchableOpacity style={styles.serviceItem}>
<Ionicons name="flash" size={24} color={COLORS.warning} />
<Text style={styles.serviceText}>Elektrik töleg</Text>
</TouchableOpacity>
<TouchableOpacity style={styles.serviceItem}>
<Ionicons name="water" size={24} color={COLORS.info} />
<Text style={styles.serviceText}>Suw töleg</Text>
</TouchableOpacity>
<TouchableOpacity style={styles.serviceItem}>
<Ionicons name="wifi" size={24} color={COLORS.success} />
<Text style={styles.serviceText}>Internet töleg</Text>
</TouchableOpacity>
</View>
</View>
</ScrollView>
</SafeAreaView>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: COLORS.backgroundSecondary,
},
scrollView: {
flex: 1,
},
header: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
paddingHorizontal: 24,
paddingTop: 16,
paddingBottom: 24,
backgroundColor: COLORS.white,
},
greeting: {
fontSize: 16,
color: COLORS.textSecondary,
},
userName: {
fontSize: 24,
fontWeight: 'bold',
color: COLORS.textPrimary,
},
profileButton: {
padding: 4,
},
balanceCard: {
backgroundColor: COLORS.primary,
marginHorizontal: 24,
marginBottom: 24,
borderRadius: 16,
padding: 24,
shadowColor: COLORS.primary,
shadowOffset: {
width: 0,
height: 4,
},
shadowOpacity: 0.3,
shadowRadius: 8,
elevation: 8,
},
balanceHeader: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: 8,
},
balanceLabel: {
fontSize: 14,
color: COLORS.white,
opacity: 0.8,
},
balanceAmount: {
fontSize: 32,
fontWeight: 'bold',
color: COLORS.white,
marginBottom: 24,
},
balanceFooter: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
},
accountNumber: {
fontSize: 14,
color: COLORS.white,
opacity: 0.8,
},
cardChip: {
width: 32,
height: 24,
backgroundColor: COLORS.white,
borderRadius: 4,
opacity: 0.8,
},
section: {
backgroundColor: COLORS.white,
marginHorizontal: 24,
marginBottom: 16,
borderRadius: 12,
padding: 20,
},
sectionHeader: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: 16,
},
sectionTitle: {
fontSize: 18,
fontWeight: 'bold',
color: COLORS.textPrimary,
marginBottom: 16,
},
seeAllText: {
fontSize: 14,
color: COLORS.primary,
fontWeight: '600',
},
quickActionsGrid: {
flexDirection: 'row',
justifyContent: 'space-between',
},
quickActionItem: {
alignItems: 'center',
flex: 1,
},
quickActionIcon: {
width: 48,
height: 48,
borderRadius: 24,
alignItems: 'center',
justifyContent: 'center',
marginBottom: 8,
},
quickActionText: {
fontSize: 12,
color: COLORS.textSecondary,
textAlign: 'center',
},
transactionsList: {
gap: 16,
},
transactionItem: {
flexDirection: 'row',
alignItems: 'center',
},
transactionIcon: {
width: 40,
height: 40,
borderRadius: 20,
backgroundColor: COLORS.backgroundSecondary,
alignItems: 'center',
justifyContent: 'center',
marginRight: 12,
},
transactionDetails: {
flex: 1,
},
transactionTitle: {
fontSize: 16,
fontWeight: '600',
color: COLORS.textPrimary,
},
transactionDate: {
fontSize: 14,
color: COLORS.textSecondary,
},
transactionAmount: {
fontSize: 16,
fontWeight: 'bold',
color: COLORS.success,
},
servicesGrid: {
flexDirection: 'row',
flexWrap: 'wrap',
justifyContent: 'space-between',
},
serviceItem: {
width: '48%',
padding: 16,
backgroundColor: COLORS.backgroundSecondary,
borderRadius: 12,
alignItems: 'center',
marginBottom: 12,
},
serviceText: {
fontSize: 12,
color: COLORS.textSecondary,
textAlign: 'center',
marginTop: 8,
},
});
export default HomeScreen;

View File

@@ -0,0 +1,182 @@
import React from 'react';
import {
View,
Text,
StyleSheet,
SafeAreaView,
ScrollView,
TouchableOpacity,
} from 'react-native';
import { StatusBar } from 'expo-status-bar';
import { Ionicons } from '@expo/vector-icons';
import { COLORS } from '../../constants/colors';
const MenuScreen = () => {
const menuSections = [
{
title: 'Pul hereketleri',
items: [
{ id: 1, title: 'Pul ugrat', icon: 'send', description: 'Başga hasaplara pul ugrat' },
{ id: 2, title: 'Pul al', icon: 'download', description: 'Pul al we hasabyňa geçir' },
{ id: 3, title: 'Geleşikler taryhy', icon: 'time', description: 'Geçen geleşikleri gör' },
],
},
{
title: 'Tölegler',
items: [
{ id: 4, title: 'Telefon töleg', icon: 'phone-portrait', description: 'Mobil telefon töleg' },
{ id: 5, title: 'Elektrik töleg', icon: 'flash', description: 'Elektrik töleg et' },
{ id: 6, title: 'Suw töleg', icon: 'water', description: 'Suw töleg et' },
{ id: 7, title: 'Internet töleg', icon: 'wifi', description: 'Internet töleg et' },
{ id: 8, title: 'Gaz töleg', icon: 'flame', description: 'Gaz töleg et' },
],
},
{
title: 'Kartlar',
items: [
{ id: 9, title: 'Meniň kartlarym', icon: 'card', description: 'Kartlaryňyzy dolandyr' },
{ id: 10, title: 'Täze kart sargyt', icon: 'add-circle', description: 'Täze kart sargyt ediň' },
{ id: 11, title: 'Kart bloklat', icon: 'lock-closed', description: 'Karty blokla' },
],
},
{
title: 'Karzlar',
items: [
{ id: 12, title: 'Meniň karzlarym', icon: 'document-text', description: 'Karzlaryňyzy gör' },
{ id: 13, title: 'Täze karz sargyt', icon: 'trending-up', description: 'Täze karz al' },
],
},
];
const handleMenuItemPress = (item) => {
console.log('Menu item pressed:', item.title);
// Handle navigation or action here
};
return (
<SafeAreaView style={styles.container}>
<StatusBar style="dark" />
<View style={styles.header}>
<Text style={styles.headerTitle}>Hyzmatlar</Text>
</View>
<ScrollView style={styles.scrollView} showsVerticalScrollIndicator={false}>
{menuSections.map((section, sectionIndex) => (
<View key={sectionIndex} style={styles.section}>
<Text style={styles.sectionTitle}>{section.title}</Text>
<View style={styles.sectionContent}>
{section.items.map((item, itemIndex) => (
<TouchableOpacity
key={item.id}
style={[
styles.menuItem,
itemIndex === section.items.length - 1 && styles.lastMenuItem,
]}
onPress={() => handleMenuItemPress(item)}
>
<View style={styles.menuItemLeft}>
<View style={styles.menuItemIcon}>
<Ionicons name={item.icon} size={24} color={COLORS.primary} />
</View>
<View style={styles.menuItemTextContainer}>
<Text style={styles.menuItemTitle}>{item.title}</Text>
<Text style={styles.menuItemDescription}>{item.description}</Text>
</View>
</View>
<Ionicons name="chevron-forward" size={20} color={COLORS.gray[400]} />
</TouchableOpacity>
))}
</View>
</View>
))}
<View style={styles.bottomSpacing} />
</ScrollView>
</SafeAreaView>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: COLORS.backgroundSecondary,
},
header: {
backgroundColor: COLORS.white,
paddingHorizontal: 24,
paddingTop: 16,
paddingBottom: 24,
borderBottomWidth: 1,
borderBottomColor: COLORS.gray[200],
},
headerTitle: {
fontSize: 28,
fontWeight: 'bold',
color: COLORS.textPrimary,
},
scrollView: {
flex: 1,
},
section: {
marginTop: 24,
marginHorizontal: 24,
},
sectionTitle: {
fontSize: 18,
fontWeight: 'bold',
color: COLORS.textPrimary,
marginBottom: 12,
paddingHorizontal: 4,
},
sectionContent: {
backgroundColor: COLORS.white,
borderRadius: 12,
overflow: 'hidden',
},
menuItem: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingHorizontal: 20,
paddingVertical: 16,
borderBottomWidth: 1,
borderBottomColor: COLORS.gray[200],
},
lastMenuItem: {
borderBottomWidth: 0,
},
menuItemLeft: {
flexDirection: 'row',
alignItems: 'center',
flex: 1,
},
menuItemIcon: {
width: 48,
height: 48,
borderRadius: 24,
backgroundColor: COLORS.backgroundSecondary,
alignItems: 'center',
justifyContent: 'center',
marginRight: 16,
},
menuItemTextContainer: {
flex: 1,
},
menuItemTitle: {
fontSize: 16,
fontWeight: '600',
color: COLORS.textPrimary,
marginBottom: 4,
},
menuItemDescription: {
fontSize: 14,
color: COLORS.textSecondary,
lineHeight: 18,
},
bottomSpacing: {
height: 24,
},
});
export default MenuScreen;

View File

@@ -0,0 +1,304 @@
import React from 'react';
import {
View,
Text,
StyleSheet,
SafeAreaView,
ScrollView,
TouchableOpacity,
Alert,
} from 'react-native';
import { StatusBar } from 'expo-status-bar';
import { Ionicons } from '@expo/vector-icons';
import { useAuth } from '../../contexts/AuthContext';
import { COLORS } from '../../constants/colors';
const ProfileScreen = () => {
const { user, logout } = useAuth();
const profileSections = [
{
title: 'Hasap maglumatlary',
items: [
{ id: 1, title: 'Şahsy maglumatlar', icon: 'person', hasArrow: true },
{ id: 2, title: 'Habarlaşmak maglumatlary', icon: 'mail', hasArrow: true },
{ id: 3, title: 'Paroly üýtget', icon: 'lock-closed', hasArrow: true },
],
},
{
title: 'Sazlamalar',
items: [
{ id: 4, title: 'Bildirişler', icon: 'notifications', hasArrow: true },
{ id: 5, title: 'Howpsuzlyk', icon: 'shield-checkmark', hasArrow: true },
{ id: 6, title: 'Dil', icon: 'language', value: 'Türkmençe', hasArrow: true },
{ id: 7, title: 'Tema', icon: 'color-palette', value: 'Ýeňil', hasArrow: true },
],
},
{
title: 'Kömek',
items: [
{ id: 8, title: 'Kömek merkezi', icon: 'help-circle', hasArrow: true },
{ id: 9, title: 'Habarlaş', icon: 'chatbubble', hasArrow: true },
{ id: 10, title: 'Baha ber', icon: 'star', hasArrow: true },
],
},
{
title: 'Goşmaça',
items: [
{ id: 11, title: 'Ulanmak düzgünleri', icon: 'document-text', hasArrow: true },
{ id: 12, title: 'Gizlinlik syýasaty', icon: 'lock-open', hasArrow: true },
{ id: 13, title: 'Programma barada', icon: 'information-circle', value: 'v1.0.0', hasArrow: true },
],
},
];
const handleProfileItemPress = (item) => {
console.log('Profile item pressed:', item.title);
// Handle navigation or action here
};
const handleLogout = () => {
Alert.alert(
'Çykmak',
'Hasabdan çykjak bolýarsyňyzmy?',
[
{
text: 'Ýok',
style: 'cancel',
},
{
text: 'Hawa',
style: 'destructive',
onPress: logout,
},
]
);
};
const formatPhoneNumber = (phone) => {
if (!phone) return '';
const phoneStr = phone.toString();
return `+993 ${phoneStr.slice(0, 2)} ${phoneStr.slice(2, 5)} ${phoneStr.slice(5)}`;
};
return (
<SafeAreaView style={styles.container}>
<StatusBar style="dark" />
<View style={styles.header}>
<Text style={styles.headerTitle}>Profil</Text>
</View>
<ScrollView style={styles.scrollView} showsVerticalScrollIndicator={false}>
{/* User Info Card */}
<View style={styles.userCard}>
<View style={styles.userAvatar}>
<Text style={styles.userInitials}>
{user?.name ? user.name.split(' ').map(n => n[0]).join('').toUpperCase() : 'U'}
</Text>
</View>
<View style={styles.userInfo}>
<Text style={styles.userName}>{user?.name || 'Ulanyjy'}</Text>
<Text style={styles.userPhone}>{formatPhoneNumber(user?.phone)}</Text>
</View>
<TouchableOpacity style={styles.editButton}>
<Ionicons name="pencil" size={20} color={COLORS.primary} />
</TouchableOpacity>
</View>
{/* Profile Sections */}
{profileSections.map((section, sectionIndex) => (
<View key={sectionIndex} style={styles.section}>
<Text style={styles.sectionTitle}>{section.title}</Text>
<View style={styles.sectionContent}>
{section.items.map((item, itemIndex) => (
<TouchableOpacity
key={item.id}
style={[
styles.profileItem,
itemIndex === section.items.length - 1 && styles.lastProfileItem,
]}
onPress={() => handleProfileItemPress(item)}
>
<View style={styles.profileItemLeft}>
<View style={styles.profileItemIcon}>
<Ionicons name={item.icon} size={20} color={COLORS.primary} />
</View>
<Text style={styles.profileItemTitle}>{item.title}</Text>
</View>
<View style={styles.profileItemRight}>
{item.value && (
<Text style={styles.profileItemValue}>{item.value}</Text>
)}
{item.hasArrow && (
<Ionicons name="chevron-forward" size={16} color={COLORS.gray[400]} />
)}
</View>
</TouchableOpacity>
))}
</View>
</View>
))}
{/* Logout Button */}
<View style={styles.logoutSection}>
<TouchableOpacity style={styles.logoutButton} onPress={handleLogout}>
<Ionicons name="log-out" size={20} color={COLORS.error} />
<Text style={styles.logoutText}>Hasabdan çyk</Text>
</TouchableOpacity>
</View>
<View style={styles.bottomSpacing} />
</ScrollView>
</SafeAreaView>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: COLORS.backgroundSecondary,
},
header: {
backgroundColor: COLORS.white,
paddingHorizontal: 24,
paddingTop: 16,
paddingBottom: 24,
borderBottomWidth: 1,
borderBottomColor: COLORS.gray[200],
},
headerTitle: {
fontSize: 28,
fontWeight: 'bold',
color: COLORS.textPrimary,
},
scrollView: {
flex: 1,
},
userCard: {
backgroundColor: COLORS.white,
marginHorizontal: 24,
marginTop: 24,
borderRadius: 16,
padding: 20,
flexDirection: 'row',
alignItems: 'center',
shadowColor: COLORS.black,
shadowOffset: {
width: 0,
height: 2,
},
shadowOpacity: 0.1,
shadowRadius: 4,
elevation: 4,
},
userAvatar: {
width: 60,
height: 60,
borderRadius: 30,
backgroundColor: COLORS.primary,
alignItems: 'center',
justifyContent: 'center',
marginRight: 16,
},
userInitials: {
fontSize: 24,
fontWeight: 'bold',
color: COLORS.white,
},
userInfo: {
flex: 1,
},
userName: {
fontSize: 20,
fontWeight: 'bold',
color: COLORS.textPrimary,
marginBottom: 4,
},
userPhone: {
fontSize: 16,
color: COLORS.textSecondary,
},
editButton: {
padding: 8,
},
section: {
marginTop: 24,
marginHorizontal: 24,
},
sectionTitle: {
fontSize: 16,
fontWeight: 'bold',
color: COLORS.textPrimary,
marginBottom: 12,
paddingHorizontal: 4,
},
sectionContent: {
backgroundColor: COLORS.white,
borderRadius: 12,
overflow: 'hidden',
},
profileItem: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingHorizontal: 16,
paddingVertical: 16,
borderBottomWidth: 1,
borderBottomColor: COLORS.gray[200],
},
lastProfileItem: {
borderBottomWidth: 0,
},
profileItemLeft: {
flexDirection: 'row',
alignItems: 'center',
flex: 1,
},
profileItemIcon: {
width: 32,
height: 32,
borderRadius: 16,
backgroundColor: COLORS.backgroundSecondary,
alignItems: 'center',
justifyContent: 'center',
marginRight: 12,
},
profileItemTitle: {
fontSize: 16,
color: COLORS.textPrimary,
flex: 1,
},
profileItemRight: {
flexDirection: 'row',
alignItems: 'center',
},
profileItemValue: {
fontSize: 14,
color: COLORS.textSecondary,
marginRight: 8,
},
logoutSection: {
marginTop: 24,
marginHorizontal: 24,
},
logoutButton: {
backgroundColor: COLORS.white,
borderRadius: 12,
padding: 16,
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
},
logoutText: {
fontSize: 16,
fontWeight: '600',
color: COLORS.error,
marginLeft: 8,
},
bottomSpacing: {
height: 24,
},
});
export default ProfileScreen;

View File

@@ -0,0 +1,55 @@
import { API_CONFIG } from '../constants/api';
class AuthService {
async makeRequest(endpoint, data, token = null) {
try {
const headers = {
...API_CONFIG.HEADERS,
};
if (token) {
headers.Authorization = `Bearer ${token}`;
}
const response = await fetch(`${API_CONFIG.BASE_URL}${endpoint}`, {
method: 'POST',
headers,
body: JSON.stringify(data),
});
const result = await response.json();
if (!response.ok) {
throw new Error(result.message || 'An error occurred');
}
return result;
} catch (error) {
throw new Error(error.message || 'Network error');
}
}
async login(phone, password) {
return this.makeRequest(API_CONFIG.ENDPOINTS.AUTH.LOGIN, {
phone: parseInt(phone),
password,
});
}
async register(phone, name, password) {
return this.makeRequest(API_CONFIG.ENDPOINTS.AUTH.REGISTER, {
phone: parseInt(phone),
name,
password,
});
}
async verify(phone, code) {
return this.makeRequest(API_CONFIG.ENDPOINTS.AUTH.VERIFY, {
phone: parseInt(phone),
code: parseInt(code),
});
}
}
export default new AuthService();