diff --git a/app/(tabs)/beneficiaries/[id]/index.tsx b/app/(tabs)/beneficiaries/[id]/index.tsx index 490d8ee..3296d25 100644 --- a/app/(tabs)/beneficiaries/[id]/index.tsx +++ b/app/(tabs)/beneficiaries/[id]/index.tsx @@ -42,6 +42,7 @@ import { shouldShowSubscriptionWarning, } from '@/services/BeneficiaryDetailController'; import { BeneficiaryMenu } from '@/components/ui/BeneficiaryMenu'; +import { ImageLightbox } from '@/components/ImageLightbox'; // WebView Dashboard URL - opens specific deployment directly const getDashboardUrl = (deploymentId?: number) => { @@ -74,6 +75,9 @@ export default function BeneficiaryDetailScreen() { const [isEditModalVisible, setIsEditModalVisible] = useState(false); const [editForm, setEditForm] = useState({ name: '', address: '', avatar: undefined as string | undefined }); + // Avatar lightbox state + const [lightboxVisible, setLightboxVisible] = useState(false); + const webViewRef = useRef(null); // Load legacy credentials for WebView dashboard @@ -335,15 +339,24 @@ export default function BeneficiaryDetailScreen() { {/* Avatar + Name + Role */} - {beneficiary.avatar && beneficiary.avatar.trim() !== '' && !beneficiary.avatar.includes('placeholder') ? ( - - ) : ( - - - {beneficiary.name.charAt(0).toUpperCase()} - - - )} + { + if (beneficiary.avatar && beneficiary.avatar.trim() !== '' && !beneficiary.avatar.includes('placeholder')) { + setLightboxVisible(true); + } + }} + disabled={!beneficiary.avatar || beneficiary.avatar.trim() === '' || beneficiary.avatar.includes('placeholder')} + > + {beneficiary.avatar && beneficiary.avatar.trim() !== '' && !beneficiary.avatar.includes('placeholder') ? ( + + ) : ( + + + {beneficiary.name.charAt(0).toUpperCase()} + + + )} + {beneficiary.name} @@ -516,6 +529,13 @@ export default function BeneficiaryDetailScreen() { + + {/* Avatar Lightbox */} + setLightboxVisible(false)} + /> ); } diff --git a/app/(tabs)/bug.tsx b/app/(tabs)/bug.tsx index 68347b5..0da93f3 100644 --- a/app/(tabs)/bug.tsx +++ b/app/(tabs)/bug.tsx @@ -4,201 +4,9 @@ import { WebView, WebViewMessageEvent } from 'react-native-webview'; import { useRouter } from 'expo-router'; import { AppColors } from '@/constants/theme'; -// Test HTML page with buttons that send messages to React Native -const TEST_HTML = ` - - - - - - - -
-

WebView Bridge Test

-

Test communication between Web and React Native

- -
-
Navigation Commands
- - - -
- -
-
Native Features
- - -
- -
-
Send Custom Data
- -
- -
-
Ready to communicate...
-
-
- - - - -`; +// Remote URL for the test page (loaded from server) +// Add cache-busting query param to force reload +const TEST_PAGE_URL = `https://wellnuo.smartlaunchhub.com/test-bridge.html?v=${Date.now()}`; export default function BugScreen() { const router = useRouter(); @@ -300,13 +108,15 @@ export default function BugScreen() { ); diff --git a/app/(tabs)/profile/index.tsx b/app/(tabs)/profile/index.tsx index 7bb7835..12e4099 100644 --- a/app/(tabs)/profile/index.tsx +++ b/app/(tabs)/profile/index.tsx @@ -13,8 +13,11 @@ import { SafeAreaView } from 'react-native-safe-area-context'; import * as SecureStore from 'expo-secure-store'; import * as ImagePicker from 'expo-image-picker'; import * as Clipboard from 'expo-clipboard'; +// ImageLightbox removed - tap on avatar directly opens picker +import { optimizeAvatarImage } from '@/utils/imageUtils'; import { router } from 'expo-router'; import { useAuth } from '@/contexts/AuthContext'; +import { useBeneficiary } from '@/contexts/BeneficiaryContext'; import { ProfileDrawer } from '@/components/ProfileDrawer'; import { useToast } from '@/components/ui/Toast'; import { @@ -43,6 +46,7 @@ const generateInviteCode = (identifier: string): string => { export default function ProfileScreen() { const { user, logout } = useAuth(); + const { clearAllBeneficiaryData } = useBeneficiary(); const toast = useToast(); // Drawer state @@ -73,7 +77,8 @@ export default function ProfileScreen() { } }; - const handleAvatarPress = async () => { + // Change avatar (tap on avatar or camera badge) + const handleAvatarChange = async () => { const { status } = await ImagePicker.requestMediaLibraryPermissionsAsync(); if (status !== 'granted') { Alert.alert('Permission needed', 'Please allow access to your photo library to change avatar.'); @@ -84,13 +89,18 @@ export default function ProfileScreen() { mediaTypes: ['images'], allowsEditing: true, aspect: [1, 1], - quality: 0.5, + quality: 0.8, }); if (!result.canceled && result.assets[0]) { - const uri = result.assets[0].uri; - setAvatarUri(uri); - await SecureStore.setItemAsync('userAvatar', uri); + const originalUri = result.assets[0].uri; + + // Optimize image: resize to 400x400 and compress + const optimizedUri = await optimizeAvatarImage(originalUri); + + setAvatarUri(optimizedUri); + await SecureStore.setItemAsync('userAvatar', optimizedUri); + toast.success('Avatar updated'); } }; @@ -112,6 +122,11 @@ export default function ProfileScreen() { text: 'Logout', style: 'destructive', onPress: async () => { + // Clear local UI state + setAvatarUri(null); + // Clear beneficiary context + await clearAllBeneficiaryData(); + // Logout (clears SecureStore and AsyncStorage) await logout(); router.replace('/(auth)/login'); }, @@ -161,18 +176,21 @@ export default function ProfileScreen() { > {/* Profile Card */} - - + + {avatarUri ? ( ) : ( {userInitial} )} - - - - - + + + + + {displayName} {user?.email || ''} @@ -247,6 +265,7 @@ export default function ProfileScreen() { settings={settings} onSettingChange={handleSettingChange} /> + ); } @@ -299,6 +318,7 @@ const styles = StyleSheet.create({ }, avatarSection: { marginBottom: Spacing.md, + position: 'relative', }, avatarContainer: { width: AvatarSizes.xl, @@ -307,7 +327,7 @@ const styles = StyleSheet.create({ backgroundColor: AppColors.primary, justifyContent: 'center', alignItems: 'center', - position: 'relative', + overflow: 'hidden', }, avatarImage: { width: AvatarSizes.xl, diff --git a/components/ImageLightbox.tsx b/components/ImageLightbox.tsx new file mode 100644 index 0000000..4e29497 --- /dev/null +++ b/components/ImageLightbox.tsx @@ -0,0 +1,87 @@ +import React from 'react'; +import { + Modal, + View, + Image, + StyleSheet, + TouchableOpacity, + Dimensions, + StatusBar, +} from 'react-native'; +import { Ionicons } from '@expo/vector-icons'; +import { AppColors } from '@/constants/theme'; + +interface ImageLightboxProps { + visible: boolean; + imageUri: string | null; + onClose: () => void; +} + +const { width: screenWidth, height: screenHeight } = Dimensions.get('window'); + +export function ImageLightbox({ visible, imageUri, onClose }: ImageLightboxProps) { + if (!imageUri) return null; + + return ( + + + + {/* Close button */} + + + + + {/* Image */} + + + + + + ); +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + backgroundColor: 'rgba(0, 0, 0, 0.95)', + justifyContent: 'center', + alignItems: 'center', + }, + closeButton: { + position: 'absolute', + top: 50, + right: 20, + width: 44, + height: 44, + borderRadius: 22, + backgroundColor: 'rgba(255, 255, 255, 0.2)', + justifyContent: 'center', + alignItems: 'center', + zIndex: 10, + }, + imageContainer: { + width: screenWidth, + height: screenHeight, + justifyContent: 'center', + alignItems: 'center', + }, + image: { + width: screenWidth - 40, + height: screenWidth - 40, + borderRadius: 20, + }, +}); diff --git a/contexts/BeneficiaryContext.tsx b/contexts/BeneficiaryContext.tsx index ceab046..74079c9 100644 --- a/contexts/BeneficiaryContext.tsx +++ b/contexts/BeneficiaryContext.tsx @@ -20,6 +20,8 @@ interface BeneficiaryContextType { addLocalBeneficiary: (data: string | AddBeneficiaryData) => Promise; updateLocalBeneficiary: (id: number, data: Partial) => Promise; removeLocalBeneficiary: (id: number) => Promise; + // Clear all data (used on logout) + clearAllBeneficiaryData: () => Promise; // Helper to format beneficiary context for AI getBeneficiaryContext: () => string; } @@ -116,6 +118,13 @@ export function BeneficiaryProvider({ children }: { children: React.ReactNode }) setCurrentBeneficiary(null); }, []); + // Clear all beneficiary data (called on logout) + const clearAllBeneficiaryData = useCallback(async () => { + setCurrentBeneficiary(null); + setLocalBeneficiaries([]); + await AsyncStorage.removeItem(LOCAL_BENEFICIARIES_KEY); + }, []); + const getBeneficiaryContext = useCallback(() => { if (!currentBeneficiary) { return ''; @@ -182,6 +191,7 @@ export function BeneficiaryProvider({ children }: { children: React.ReactNode }) addLocalBeneficiary, updateLocalBeneficiary, removeLocalBeneficiary, + clearAllBeneficiaryData, getBeneficiaryContext, }} > diff --git a/package-lock.json b/package-lock.json index 9e075e1..9d8f983 100644 --- a/package-lock.json +++ b/package-lock.json @@ -26,6 +26,7 @@ "expo-font": "~14.0.10", "expo-haptics": "~15.0.8", "expo-image": "~3.0.11", + "expo-image-manipulator": "^14.0.8", "expo-image-picker": "~17.0.10", "expo-linking": "~8.0.11", "expo-router": "~6.0.21", @@ -10769,6 +10770,18 @@ "expo": "*" } }, + "node_modules/expo-image-manipulator": { + "version": "14.0.8", + "resolved": "https://registry.npmjs.org/expo-image-manipulator/-/expo-image-manipulator-14.0.8.tgz", + "integrity": "sha512-sXsXjm7rIxLWZe0j2A41J/Ph53PpFJRdyzJ3EQ/qetxLUvS2m3K1sP5xy37px43qCf0l79N/i6XgFgenFV36/Q==", + "license": "MIT", + "dependencies": { + "expo-image-loader": "~6.0.0" + }, + "peerDependencies": { + "expo": "*" + } + }, "node_modules/expo-image-picker": { "version": "17.0.10", "resolved": "https://registry.npmjs.org/expo-image-picker/-/expo-image-picker-17.0.10.tgz", diff --git a/package.json b/package.json index eae84b3..e6cf09c 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ "expo-font": "~14.0.10", "expo-haptics": "~15.0.8", "expo-image": "~3.0.11", + "expo-image-manipulator": "^14.0.8", "expo-image-picker": "~17.0.10", "expo-linking": "~8.0.11", "expo-router": "~6.0.21", diff --git a/services/api.ts b/services/api.ts index 50659d3..1da041b 100644 --- a/services/api.ts +++ b/services/api.ts @@ -2,6 +2,7 @@ import type { ApiError, ApiResponse, AuthResponse, Beneficiary, BeneficiaryDashb import * as Crypto from 'expo-crypto'; import { File } from 'expo-file-system'; import * as SecureStore from 'expo-secure-store'; +import AsyncStorage from '@react-native-async-storage/async-storage'; // Callback for handling unauthorized responses (401) let onUnauthorizedCallback: (() => void) | null = null; @@ -158,6 +159,10 @@ class ApiService { await SecureStore.deleteItemAsync('legacyAccessToken'); await SecureStore.deleteItemAsync('privileges'); await SecureStore.deleteItemAsync('maxRole'); + // Clear user profile data (avatar, etc.) + await SecureStore.deleteItemAsync('userAvatar'); + // Clear local cached data (beneficiaries, etc.) + await AsyncStorage.removeItem('wellnuo_local_beneficiaries'); } // Save user email (for OTP auth flow)