From 5c8c3665dae015da361a95d81f5e1f11e68b87f0 Mon Sep 17 00:00:00 2001 From: Sergei Date: Mon, 12 Jan 2026 21:53:53 -0800 Subject: [PATCH] Profile avatar cloud upload + test bundle ID - Profile avatar now uploads to MinIO cloud storage via /auth/avatar endpoint - Added loading indicator (ActivityIndicator) during avatar upload - Avatar loads from cloud URL (user.avatarUrl) first, with SecureStore fallback - Changed iOS bundleIdentifier to com.serter2069.wellnuo.test (test account) --- app.json | 2 +- app/(tabs)/profile/index.tsx | 54 ++++++++++++++++++++++++++++++++++-- 2 files changed, 52 insertions(+), 4 deletions(-) diff --git a/app.json b/app.json index d2ad426..28df652 100644 --- a/app.json +++ b/app.json @@ -10,7 +10,7 @@ "newArchEnabled": true, "ios": { "supportsTablet": true, - "bundleIdentifier": "com.wellnuo.BluetoothScanner", + "bundleIdentifier": "com.serter2069.wellnuo.test", "appleTeamId": "UHLZD54ULZ", "deploymentTarget": "16.0", "infoPlist": { diff --git a/app/(tabs)/profile/index.tsx b/app/(tabs)/profile/index.tsx index 12e4099..2922f9a 100644 --- a/app/(tabs)/profile/index.tsx +++ b/app/(tabs)/profile/index.tsx @@ -7,7 +7,9 @@ import { Alert, Image, ScrollView, + ActivityIndicator, } from 'react-native'; +import { api } from '@/services/api'; import { Ionicons } from '@expo/vector-icons'; import { SafeAreaView } from 'react-native-safe-area-context'; import * as SecureStore from 'expo-secure-store'; @@ -61,6 +63,7 @@ export default function ProfileScreen() { // Avatar const [avatarUri, setAvatarUri] = useState(null); + const [isUploadingAvatar, setIsUploadingAvatar] = useState(false); useEffect(() => { loadAvatar(); @@ -68,6 +71,13 @@ export default function ProfileScreen() { const loadAvatar = async () => { try { + // First try to get cloud URL from user profile + if (user?.avatarUrl) { + setAvatarUri(user.avatarUrl); + await SecureStore.setItemAsync('userAvatar', user.avatarUrl); + return; + } + // Fallback to cached local avatar const uri = await SecureStore.getItemAsync('userAvatar'); if (uri) { setAvatarUri(uri); @@ -98,9 +108,30 @@ export default function ProfileScreen() { // Optimize image: resize to 400x400 and compress const optimizedUri = await optimizeAvatarImage(originalUri); + // Show optimistic update immediately setAvatarUri(optimizedUri); - await SecureStore.setItemAsync('userAvatar', optimizedUri); - toast.success('Avatar updated'); + + // Upload to cloud storage + setIsUploadingAvatar(true); + try { + const response = await api.updateProfileAvatar(optimizedUri); + if (response.ok && response.data?.avatarUrl) { + // Use cloud URL instead of local + setAvatarUri(response.data.avatarUrl); + await SecureStore.setItemAsync('userAvatar', response.data.avatarUrl); + toast.success('Avatar updated'); + } else { + // Fallback to local storage if cloud upload fails + await SecureStore.setItemAsync('userAvatar', optimizedUri); + toast.error(response.error?.message || 'Cloud upload failed, saved locally'); + } + } catch (error) { + console.error('Avatar upload error:', error); + await SecureStore.setItemAsync('userAvatar', optimizedUri); + toast.error('Upload failed, saved locally'); + } finally { + setIsUploadingAvatar(false); + } } }; @@ -180,14 +211,24 @@ export default function ProfileScreen() { {avatarUri ? ( ) : ( {userInitial} )} + {isUploadingAvatar && ( + + + + )} - + @@ -339,6 +380,13 @@ const styles = StyleSheet.create({ fontWeight: FontWeights.bold, color: AppColors.white, }, + avatarLoadingOverlay: { + ...StyleSheet.absoluteFillObject, + backgroundColor: 'rgba(0, 0, 0, 0.5)', + borderRadius: AvatarSizes.xl / 2, + justifyContent: 'center', + alignItems: 'center', + }, avatarEditBadge: { position: 'absolute', bottom: 0,