From 77e3ca0f18560c1a583be765b2ec6498e41c260c Mon Sep 17 00:00:00 2001 From: Nurmuhammet Allanov Date: Thu, 3 Jul 2025 22:36:28 +0500 Subject: [PATCH] sync profile api --- src/contexts/AuthContext.js | 153 ++++++++++++++++++++++++--- src/screens/Main/ProfileScreen.js | 170 ++++++++++++++++++++++++++++-- src/services/apiService.js | 82 ++++++++++++++ src/services/authService.js | 123 +++++++++++++++++++-- 4 files changed, 498 insertions(+), 30 deletions(-) create mode 100644 src/services/apiService.js diff --git a/src/contexts/AuthContext.js b/src/contexts/AuthContext.js index 01cd1e9..af41643 100644 --- a/src/contexts/AuthContext.js +++ b/src/contexts/AuthContext.js @@ -59,17 +59,58 @@ export const AuthProvider = ({ children }) => { const userData = await AsyncStorage.getItem('user_data'); if (token && userData) { + const user = JSON.parse(userData); + console.log('🔄 Found cached auth data, loading user...'); + console.log('🎫 Token:', `${token.slice(0, 20)}...`); + console.log('👤 Cached user:', user); + dispatch({ type: 'LOGIN_SUCCESS', - payload: { - token, - user: JSON.parse(userData), - }, + payload: { token, user }, }); + + // Try to refresh profile data in background + console.log('🔄 Refreshing profile in background...'); + try { + const freshProfileData = await authService.getProfile(); + console.log('👤 Fresh profile data:', freshProfileData); + + // Validate fresh data + if (freshProfileData.name && freshProfileData.phone) { + // Update cached data + await AsyncStorage.setItem('user_data', JSON.stringify(freshProfileData)); + + console.log('✅ Profile refreshed successfully on app startup'); + + // Update state with fresh data + dispatch({ + type: 'LOGIN_SUCCESS', + payload: { token, user: freshProfileData }, + }); + } else { + console.warn('⚠️ Fresh profile data is invalid, keeping cached data'); + } + } catch (profileError) { + console.warn('⚠️ Could not refresh profile on startup:', profileError.message); + + // Check if it's a token expiration error + if (profileError.message.includes('Session expired')) { + console.log('🔓 Token expired, clearing auth data'); + await AsyncStorage.removeItem('auth_token'); + await AsyncStorage.removeItem('user_data'); + dispatch({ type: 'LOGOUT' }); + return; + } + + // Otherwise, continue with cached data + console.log('📦 Using cached user data'); + } } else { + console.log('🔍 No cached auth data found'); dispatch({ type: 'LOADING', payload: false }); } } catch (error) { + console.error('💥 Error checking auth status:', error.message); dispatch({ type: 'LOADING', payload: false }); } }; @@ -117,23 +158,47 @@ export const AuthProvider = ({ children }) => { } 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 }; + // Step 1: Verify the code and get the token + const verificationResponse = await authService.verify(state.pendingVerification.phone, code); - // Store auth data + console.log('🔍 Verification Response:', verificationResponse); + + // Extract data from your API response structure + if (!verificationResponse.success) { + dispatch({ type: 'LOADING', payload: false }); + return { success: false, error: verificationResponse.message || 'Verification failed' }; + } + + const token = verificationResponse.token; + const basicUserData = verificationResponse.user; + + if (!token) { + dispatch({ type: 'LOADING', payload: false }); + return { success: false, error: 'No access token received from verification' }; + } + + console.log('✅ Token received:', `${token.slice(0, 20)}...`); + console.log('👤 Basic user data:', basicUserData); + + // Step 2: Cache auth data (token and user data from verification) await AsyncStorage.setItem('auth_token', token); - await AsyncStorage.setItem('user_data', JSON.stringify(user)); + await AsyncStorage.setItem('user_data', JSON.stringify(basicUserData)); + + console.log('✅ Auth data cached successfully'); + console.log('🎫 Token stored:', `${token.slice(0, 20)}...`); + console.log('👤 User data cached:', basicUserData); dispatch({ type: 'LOGIN_SUCCESS', - payload: { token, user }, + payload: { token, user: basicUserData }, }); - return { success: true, message: response.message }; + return { + success: true, + message: verificationResponse.message, + user: basicUserData + }; } catch (error) { dispatch({ type: 'LOADING', payload: false }); return { success: false, error: error.message }; @@ -154,6 +219,66 @@ export const AuthProvider = ({ children }) => { dispatch({ type: 'CLEAR_PENDING_VERIFICATION' }); }; + const updateUserProfile = async (profileData) => { + try { + const response = await authService.updateProfile(profileData); + const updatedUser = { ...state.user, ...profileData }; + + // Update AsyncStorage + await AsyncStorage.setItem('user_data', JSON.stringify(updatedUser)); + + // Update state + dispatch({ + type: 'LOGIN_SUCCESS', + payload: { + token: state.token, + user: updatedUser, + }, + }); + + return { success: true, message: response.message }; + } catch (error) { + return { success: false, error: error.message }; + } + }; + + const fetchUserProfile = async () => { + try { + console.log('🔄 Manually fetching user profile...'); + const profileData = await authService.getProfile(); + console.log('👤 Fetched profile data:', profileData); + + // Validate profile data structure + if (!profileData.name || !profileData.phone) { + throw new Error('Invalid profile data structure received'); + } + + // Update AsyncStorage + await AsyncStorage.setItem('user_data', JSON.stringify(profileData)); + + // Update state + dispatch({ + type: 'LOGIN_SUCCESS', + payload: { + token: state.token, + user: profileData, + }, + }); + + console.log('✅ Profile updated successfully'); + return { success: true, user: profileData }; + } catch (error) { + console.error('💥 Failed to fetch profile:', error.message); + + // If unauthorized, logout user + if (error.message.includes('Session expired')) { + console.log('🔓 Session expired during profile fetch, logging out'); + await logout(); + } + return { success: false, error: error.message }; + } + }; + const value = { ...state, login, @@ -161,6 +286,8 @@ export const AuthProvider = ({ children }) => { verify, logout, clearPendingVerification, + updateUserProfile, + fetchUserProfile, }; return {children}; diff --git a/src/screens/Main/ProfileScreen.js b/src/screens/Main/ProfileScreen.js index df60d6d..b63c7e3 100644 --- a/src/screens/Main/ProfileScreen.js +++ b/src/screens/Main/ProfileScreen.js @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useState, useEffect } from 'react'; import { View, Text, @@ -7,14 +7,47 @@ import { ScrollView, TouchableOpacity, Alert, + ActivityIndicator, + RefreshControl, } from 'react-native'; import { StatusBar } from 'expo-status-bar'; import { Ionicons } from '@expo/vector-icons'; import { useAuth } from '../../contexts/AuthContext'; +import apiService from '../../services/apiService'; import { COLORS } from '../../constants/colors'; const ProfileScreen = () => { - const { user, logout } = useAuth(); + const { user, logout, fetchUserProfile } = useAuth(); + const [isLoading, setIsLoading] = useState(false); + const [isRefreshing, setIsRefreshing] = useState(false); + const [profileData, setProfileData] = useState(null); + + useEffect(() => { + loadProfileData(); + }, []); + + const loadProfileData = async () => { + if (isLoading) return; + + setIsLoading(true); + try { + // Fetch fresh profile data from API + const result = await fetchUserProfile(); + if (result.success) { + setProfileData(result.user); + } + } catch (error) { + console.error('Error loading profile:', error); + } finally { + setIsLoading(false); + } + }; + + const handleRefresh = async () => { + setIsRefreshing(true); + await loadProfileData(); + setIsRefreshing(false); + }; const profileSections = [ { @@ -53,8 +86,100 @@ const ProfileScreen = () => { ]; const handleProfileItemPress = (item) => { - console.log('Profile item pressed:', item.title); - // Handle navigation or action here + switch (item.id) { + case 1: // Personal Information + handleEditPersonalInfo(); + break; + case 2: // Contact Information + handleEditContactInfo(); + break; + case 3: // Change Password + handleChangePassword(); + break; + case 4: // Notifications + handleNotificationSettings(); + break; + case 5: // Security + handleSecuritySettings(); + break; + default: + Alert.alert('Üns beriň', 'Bu funksiýa entek işlenok'); + } + }; + + const handleEditPersonalInfo = () => { + // Navigate to edit personal info screen + Alert.alert('Şahsy maglumatlar', 'Şahsy maglumatlar sahypasy açylýar...'); + }; + + const handleEditContactInfo = () => { + // Navigate to edit contact info screen + Alert.alert('Aragatnaşyk', 'Aragatnaşyk maglumatlar sahypasy açylýar...'); + }; + + const handleChangePassword = () => { + Alert.prompt( + 'Paroly üýtget', + 'Häzirki parolyňyzy giriziň', + [ + { text: 'Ýatyr', style: 'cancel' }, + { + text: 'Dowam et', + onPress: (currentPassword) => { + if (currentPassword) { + promptForNewPassword(currentPassword); + } + } + } + ], + 'secure-text' + ); + }; + + const promptForNewPassword = (currentPassword) => { + Alert.prompt( + 'Täze parol', + 'Täze parolyňyzy giriziň', + [ + { text: 'Ýatyr', style: 'cancel' }, + { + text: 'Üýtget', + onPress: (newPassword) => { + if (newPassword && newPassword.length >= 6) { + changePassword(currentPassword, newPassword); + } else { + Alert.alert('Ýalňyşlyk', 'Parol azyndan 6 harp bolmaly'); + } + } + } + ], + 'secure-text' + ); + }; + + const changePassword = async (currentPassword, newPassword) => { + try { + setIsLoading(true); + const result = await apiService.changePassword(currentPassword, newPassword); + + if (result.success) { + Alert.alert('Üstünlik', 'Parol üstünlikli üýtgedildi'); + } else { + Alert.alert('Ýalňyşlyk', result.error || 'Paroly üýtgetmek amala aşmady'); + } + } catch (error) { + Alert.alert('Ýalňyşlyk', error.message || 'Paroly üýtgetmek amala aşmady'); + } finally { + setIsLoading(false); + } + }; + + const handleNotificationSettings = () => { + Alert.alert('Bildirişler', 'Bildiriş sazlamalary sahypasy açylýar...'); + }; + + const handleSecuritySettings = () => { + Alert.alert('Howpsuzlyk', 'Howpsuzlyk sazlamalary sahypasy açylýar...'); }; const handleLogout = () => { @@ -78,9 +203,12 @@ const ProfileScreen = () => { const formatPhoneNumber = (phone) => { if (!phone) return ''; const phoneStr = phone.toString(); - return `+993 ${phoneStr.slice(0, 2)} ${phoneStr.slice(2, 5)} ${phoneStr.slice(5)}`; + return `+993 ${phoneStr.slice(0, 2)} ${phoneStr.slice(2, 4)}-${phoneStr.slice(4, 6)}-${phoneStr.slice(6)}`; }; + // Use profile data from API or fallback to cached user data + const currentUser = profileData || user; + return ( @@ -89,19 +217,36 @@ const ProfileScreen = () => { Profil - + + } + > {/* User Info Card */} - {user?.name ? user.name.split(' ').map(n => n[0]).join('').toUpperCase() : 'U'} + {currentUser?.name ? currentUser.name.split(' ').map(n => n[0]).join('').toUpperCase() : 'U'} - {user?.name || 'Ulanyjy'} - {formatPhoneNumber(user?.phone)} + + {currentUser?.name || 'Ulanyjy'} + {isLoading && } + + {formatPhoneNumber(currentUser?.phone)} + {currentUser?.email && ( + {currentUser.email} + )} - + @@ -219,6 +364,11 @@ const styles = StyleSheet.create({ fontSize: 16, color: COLORS.textSecondary, }, + userEmail: { + fontSize: 14, + color: COLORS.textSecondary, + marginTop: 2, + }, editButton: { padding: 8, }, diff --git a/src/services/apiService.js b/src/services/apiService.js new file mode 100644 index 0000000..b5d7ed3 --- /dev/null +++ b/src/services/apiService.js @@ -0,0 +1,82 @@ +import authService from './authService'; + +class ApiService { + // Profile methods + async getProfile() { + return authService.getProfile(); + } + + async updateProfile(data) { + return authService.updateProfile(data); + } + + async changePassword(currentPassword, newPassword) { + return authService.changePassword(currentPassword, newPassword); + } + + async deleteAccount() { + return authService.deleteAccount(); + } + + // Balance and transactions + async getBalance() { + return authService.getBalance(); + } + + async getTransactions(page = 1, limit = 20) { + return authService.getTransactions(page, limit); + } + + // Transfer and payments + async transferMoney(data) { + return authService.transferMoney(data); + } + + async payBill(data) { + return authService.payBill(data); + } + + // Cards management + async getCards() { + return authService.getCards(); + } + + async addCard(data) { + return authService.addCard(data); + } + + async blockCard(cardId) { + return authService.blockCard(cardId); + } + + async unblockCard(cardId) { + return authService.unblockCard(cardId); + } + + // Utility methods for common operations + async getUserDashboardData() { + try { + const [balance, transactions, cards] = await Promise.all([ + this.getBalance().catch(() => ({ balance: 0 })), + this.getTransactions(1, 5).catch(() => ({ transactions: [] })), + this.getCards().catch(() => ({ cards: [] })) + ]); + + return { + success: true, + data: { + balance: balance.balance || 0, + recentTransactions: transactions.transactions || [], + cards: cards.cards || [] + } + }; + } catch (error) { + return { + success: false, + error: error.message + }; + } + } +} + +export default new ApiService(); \ No newline at end of file diff --git a/src/services/authService.js b/src/services/authService.js index d406f1c..8ff515b 100644 --- a/src/services/authService.js +++ b/src/services/authService.js @@ -1,30 +1,87 @@ +import AsyncStorage from '@react-native-async-storage/async-storage'; import { API_CONFIG } from '../constants/api'; class AuthService { - async makeRequest(endpoint, data, token = null) { + async getStoredToken() { + try { + return await AsyncStorage.getItem('auth_token'); + } catch (error) { + return null; + } + } + + async makeRequest(endpoint, data, requiresAuth = false, method = 'POST') { + const fullUrl = `${API_CONFIG.BASE_URL}${endpoint}`; + const requestId = Math.random().toString(36).substr(2, 9); + try { const headers = { ...API_CONFIG.HEADERS, }; - if (token) { - headers.Authorization = `Bearer ${token}`; + // Auto-include token for authenticated requests + if (requiresAuth) { + const token = await this.getStoredToken(); + if (token) { + headers.Authorization = `Bearer ${token}`; + } } - const response = await fetch(`${API_CONFIG.BASE_URL}${endpoint}`, { - method: 'POST', - headers, - body: JSON.stringify(data), + // Log request details + console.log(`\n🚀 [${requestId}] API REQUEST`); + console.log(`📍 URL: ${method} ${fullUrl}`); + console.log(`🔐 Auth Required: ${requiresAuth}`); + console.log(`📝 Headers:`, { + ...headers, + Authorization: headers.Authorization ? `Bearer ${headers.Authorization.slice(-10)}...` : 'None' }); + if (data) { + console.log(`📦 Body:`, data); + } + console.log(`⏰ Time: ${new Date().toISOString()}`); + + const startTime = Date.now(); + + const response = await fetch(fullUrl, { + method, + headers, + body: data ? JSON.stringify(data) : undefined, + }); + + const endTime = Date.now(); + const duration = endTime - startTime; const result = await response.json(); + // Log response details + console.log(`\n✅ [${requestId}] API RESPONSE`); + console.log(`📊 Status: ${response.status} ${response.statusText}`); + console.log(`⏱️ Duration: ${duration}ms`); + console.log(`📄 Response:`, result); + if (!response.ok) { + console.log(`❌ [${requestId}] API ERROR`); + console.log(`🔴 Status: ${response.status}`); + console.log(`💬 Error Message:`, result.message || 'Unknown error'); + + // Handle token expiration + if (response.status === 401 && requiresAuth) { + console.log(`🔓 [${requestId}] TOKEN EXPIRED - Clearing storage`); + await AsyncStorage.removeItem('auth_token'); + await AsyncStorage.removeItem('user_data'); + throw new Error('Session expired. Please login again.'); + } throw new Error(result.message || 'An error occurred'); } + console.log(`✨ [${requestId}] REQUEST COMPLETED SUCCESSFULLY\n`); return result; } catch (error) { + console.log(`\n💥 [${requestId}] API REQUEST FAILED`); + console.log(`📍 URL: ${method} ${fullUrl}`); + console.log(`🔴 Error:`, error.message); + console.log(`📅 Time: ${new Date().toISOString()}\n`); + throw new Error(error.message || 'Network error'); } } @@ -50,6 +107,58 @@ class AuthService { code: parseInt(code), }); } + + // Authenticated API methods + async getProfile() { + return this.makeRequest('/profile', null, true, 'GET'); + } + + async updateProfile(data) { + return this.makeRequest('/profile', data, true, 'PUT'); + } + + async getTransactions(page = 1, limit = 20) { + return this.makeRequest(`/user/transactions?page=${page}&limit=${limit}`, null, true, 'GET'); + } + + async getBalance() { + return this.makeRequest('/user/balance', null, true, 'GET'); + } + + async transferMoney(data) { + return this.makeRequest('/user/transfer', data, true); + } + + async payBill(data) { + return this.makeRequest('/user/pay-bill', data, true); + } + + async getCards() { + return this.makeRequest('/user/cards', null, true, 'GET'); + } + + async addCard(data) { + return this.makeRequest('/user/cards', data, true); + } + + async blockCard(cardId) { + return this.makeRequest(`/user/cards/${cardId}/block`, null, true); + } + + async unblockCard(cardId) { + return this.makeRequest(`/user/cards/${cardId}/unblock`, null, true); + } + + async changePassword(currentPassword, newPassword) { + return this.makeRequest('/user/change-password', { + current_password: currentPassword, + new_password: newPassword, + }, true); + } + + async deleteAccount() { + return this.makeRequest('/user/delete-account', null, true, 'DELETE'); + } } export default new AuthService(); \ No newline at end of file