Various improvements and fixes
- Added ImageLightbox component for avatar viewing - Updated beneficiary detail page with lightbox support - Profile page improvements - Bug page cleanup - API and context updates 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
4c880190d5
commit
966d8e2aba
@ -42,6 +42,7 @@ import {
|
|||||||
shouldShowSubscriptionWarning,
|
shouldShowSubscriptionWarning,
|
||||||
} from '@/services/BeneficiaryDetailController';
|
} from '@/services/BeneficiaryDetailController';
|
||||||
import { BeneficiaryMenu } from '@/components/ui/BeneficiaryMenu';
|
import { BeneficiaryMenu } from '@/components/ui/BeneficiaryMenu';
|
||||||
|
import { ImageLightbox } from '@/components/ImageLightbox';
|
||||||
|
|
||||||
// WebView Dashboard URL - opens specific deployment directly
|
// WebView Dashboard URL - opens specific deployment directly
|
||||||
const getDashboardUrl = (deploymentId?: number) => {
|
const getDashboardUrl = (deploymentId?: number) => {
|
||||||
@ -74,6 +75,9 @@ export default function BeneficiaryDetailScreen() {
|
|||||||
const [isEditModalVisible, setIsEditModalVisible] = useState(false);
|
const [isEditModalVisible, setIsEditModalVisible] = useState(false);
|
||||||
const [editForm, setEditForm] = useState({ name: '', address: '', avatar: undefined as string | undefined });
|
const [editForm, setEditForm] = useState({ name: '', address: '', avatar: undefined as string | undefined });
|
||||||
|
|
||||||
|
// Avatar lightbox state
|
||||||
|
const [lightboxVisible, setLightboxVisible] = useState(false);
|
||||||
|
|
||||||
const webViewRef = useRef<WebView>(null);
|
const webViewRef = useRef<WebView>(null);
|
||||||
|
|
||||||
// Load legacy credentials for WebView dashboard
|
// Load legacy credentials for WebView dashboard
|
||||||
@ -335,6 +339,14 @@ export default function BeneficiaryDetailScreen() {
|
|||||||
|
|
||||||
{/* Avatar + Name + Role */}
|
{/* Avatar + Name + Role */}
|
||||||
<View style={styles.headerCenter}>
|
<View style={styles.headerCenter}>
|
||||||
|
<TouchableOpacity
|
||||||
|
onPress={() => {
|
||||||
|
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.avatar && beneficiary.avatar.trim() !== '' && !beneficiary.avatar.includes('placeholder') ? (
|
||||||
<Image source={{ uri: beneficiary.avatar }} style={styles.headerAvatarImage} />
|
<Image source={{ uri: beneficiary.avatar }} style={styles.headerAvatarImage} />
|
||||||
) : (
|
) : (
|
||||||
@ -344,6 +356,7 @@ export default function BeneficiaryDetailScreen() {
|
|||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
|
</TouchableOpacity>
|
||||||
<Text style={styles.headerTitle}>{beneficiary.name}</Text>
|
<Text style={styles.headerTitle}>{beneficiary.name}</Text>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
@ -516,6 +529,13 @@ export default function BeneficiaryDetailScreen() {
|
|||||||
</View>
|
</View>
|
||||||
</KeyboardAvoidingView>
|
</KeyboardAvoidingView>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
|
{/* Avatar Lightbox */}
|
||||||
|
<ImageLightbox
|
||||||
|
visible={lightboxVisible}
|
||||||
|
imageUri={beneficiary?.avatar || null}
|
||||||
|
onClose={() => setLightboxVisible(false)}
|
||||||
|
/>
|
||||||
</SafeAreaView>
|
</SafeAreaView>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,201 +4,9 @@ import { WebView, WebViewMessageEvent } from 'react-native-webview';
|
|||||||
import { useRouter } from 'expo-router';
|
import { useRouter } from 'expo-router';
|
||||||
import { AppColors } from '@/constants/theme';
|
import { AppColors } from '@/constants/theme';
|
||||||
|
|
||||||
// Test HTML page with buttons that send messages to React Native
|
// Remote URL for the test page (loaded from server)
|
||||||
const TEST_HTML = `
|
// Add cache-busting query param to force reload
|
||||||
<!DOCTYPE html>
|
const TEST_PAGE_URL = `https://wellnuo.smartlaunchhub.com/test-bridge.html?v=${Date.now()}`;
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<style>
|
|
||||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
||||||
body {
|
|
||||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
||||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
||||||
min-height: 100vh;
|
|
||||||
padding: 20px;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
.container {
|
|
||||||
max-width: 400px;
|
|
||||||
margin: 0 auto;
|
|
||||||
}
|
|
||||||
h1 {
|
|
||||||
font-size: 24px;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
.subtitle {
|
|
||||||
font-size: 14px;
|
|
||||||
opacity: 0.8;
|
|
||||||
text-align: center;
|
|
||||||
margin-bottom: 30px;
|
|
||||||
}
|
|
||||||
.card {
|
|
||||||
background: rgba(255,255,255,0.15);
|
|
||||||
backdrop-filter: blur(10px);
|
|
||||||
border-radius: 16px;
|
|
||||||
padding: 20px;
|
|
||||||
margin-bottom: 16px;
|
|
||||||
}
|
|
||||||
.card-title {
|
|
||||||
font-size: 16px;
|
|
||||||
font-weight: 600;
|
|
||||||
margin-bottom: 12px;
|
|
||||||
}
|
|
||||||
.btn {
|
|
||||||
display: block;
|
|
||||||
width: 100%;
|
|
||||||
padding: 16px;
|
|
||||||
border: none;
|
|
||||||
border-radius: 12px;
|
|
||||||
font-size: 16px;
|
|
||||||
font-weight: 600;
|
|
||||||
cursor: pointer;
|
|
||||||
margin-bottom: 12px;
|
|
||||||
transition: transform 0.1s, opacity 0.1s;
|
|
||||||
}
|
|
||||||
.btn:active {
|
|
||||||
transform: scale(0.98);
|
|
||||||
opacity: 0.9;
|
|
||||||
}
|
|
||||||
.btn-primary {
|
|
||||||
background: white;
|
|
||||||
color: #667eea;
|
|
||||||
}
|
|
||||||
.btn-secondary {
|
|
||||||
background: rgba(255,255,255,0.2);
|
|
||||||
color: white;
|
|
||||||
border: 2px solid rgba(255,255,255,0.3);
|
|
||||||
}
|
|
||||||
.btn-danger {
|
|
||||||
background: #ff6b6b;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
.log {
|
|
||||||
background: rgba(0,0,0,0.2);
|
|
||||||
border-radius: 8px;
|
|
||||||
padding: 12px;
|
|
||||||
font-family: monospace;
|
|
||||||
font-size: 12px;
|
|
||||||
max-height: 150px;
|
|
||||||
overflow-y: auto;
|
|
||||||
margin-top: 20px;
|
|
||||||
}
|
|
||||||
.log-entry {
|
|
||||||
margin-bottom: 4px;
|
|
||||||
opacity: 0.9;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div class="container">
|
|
||||||
<h1>WebView Bridge Test</h1>
|
|
||||||
<p class="subtitle">Test communication between Web and React Native</p>
|
|
||||||
|
|
||||||
<div class="card">
|
|
||||||
<div class="card-title">Navigation Commands</div>
|
|
||||||
<button class="btn btn-primary" onclick="navigateTo('beneficiaries')">
|
|
||||||
Open Beneficiaries
|
|
||||||
</button>
|
|
||||||
<button class="btn btn-secondary" onclick="navigateTo('chat')">
|
|
||||||
Open Chat
|
|
||||||
</button>
|
|
||||||
<button class="btn btn-secondary" onclick="navigateTo('profile')">
|
|
||||||
Open Profile
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="card">
|
|
||||||
<div class="card-title">Native Features</div>
|
|
||||||
<button class="btn btn-primary" onclick="requestNativeAction('bluetooth')">
|
|
||||||
Scan Bluetooth Devices
|
|
||||||
</button>
|
|
||||||
<button class="btn btn-secondary" onclick="requestNativeAction('camera')">
|
|
||||||
Open Camera
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="card">
|
|
||||||
<div class="card-title">Send Custom Data</div>
|
|
||||||
<button class="btn btn-danger" onclick="sendCustomMessage()">
|
|
||||||
Send Test Message
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="log" id="log">
|
|
||||||
<div class="log-entry">Ready to communicate...</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
function log(message) {
|
|
||||||
const logEl = document.getElementById('log');
|
|
||||||
const time = new Date().toLocaleTimeString();
|
|
||||||
logEl.innerHTML += '<div class="log-entry">[' + time + '] ' + message + '</div>';
|
|
||||||
logEl.scrollTop = logEl.scrollHeight;
|
|
||||||
}
|
|
||||||
|
|
||||||
function sendToRN(data) {
|
|
||||||
if (window.ReactNativeWebView) {
|
|
||||||
window.ReactNativeWebView.postMessage(JSON.stringify(data));
|
|
||||||
log('Sent: ' + JSON.stringify(data));
|
|
||||||
} else {
|
|
||||||
log('ERROR: Not in WebView');
|
|
||||||
alert('This page must be opened in the mobile app');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function navigateTo(screen) {
|
|
||||||
sendToRN({
|
|
||||||
action: 'NAVIGATE',
|
|
||||||
screen: screen
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function requestNativeAction(feature) {
|
|
||||||
sendToRN({
|
|
||||||
action: 'NATIVE_FEATURE',
|
|
||||||
feature: feature
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function sendCustomMessage() {
|
|
||||||
sendToRN({
|
|
||||||
action: 'CUSTOM',
|
|
||||||
payload: {
|
|
||||||
timestamp: Date.now(),
|
|
||||||
message: 'Hello from WebView!',
|
|
||||||
data: { foo: 'bar', count: 42 }
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Listen for messages FROM React Native
|
|
||||||
window.addEventListener('message', function(event) {
|
|
||||||
try {
|
|
||||||
const data = JSON.parse(event.data);
|
|
||||||
log('Received from RN: ' + JSON.stringify(data));
|
|
||||||
} catch (e) {
|
|
||||||
log('Received: ' + event.data);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Also handle React Native's onMessage format
|
|
||||||
document.addEventListener('message', function(event) {
|
|
||||||
try {
|
|
||||||
const data = JSON.parse(event.data);
|
|
||||||
log('Received from RN: ' + JSON.stringify(data));
|
|
||||||
} catch (e) {
|
|
||||||
log('Received: ' + event.data);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
log('WebView Bridge initialized');
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
`;
|
|
||||||
|
|
||||||
export default function BugScreen() {
|
export default function BugScreen() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@ -300,13 +108,15 @@ export default function BugScreen() {
|
|||||||
<SafeAreaView style={styles.container}>
|
<SafeAreaView style={styles.container}>
|
||||||
<WebView
|
<WebView
|
||||||
ref={webViewRef}
|
ref={webViewRef}
|
||||||
source={{ html: TEST_HTML }}
|
source={{ uri: TEST_PAGE_URL }}
|
||||||
style={styles.webview}
|
style={styles.webview}
|
||||||
onMessage={handleMessage}
|
onMessage={handleMessage}
|
||||||
javaScriptEnabled={true}
|
javaScriptEnabled={true}
|
||||||
domStorageEnabled={true}
|
domStorageEnabled={true}
|
||||||
startInLoadingState={true}
|
startInLoadingState={true}
|
||||||
scalesPageToFit={true}
|
scalesPageToFit={true}
|
||||||
|
cacheEnabled={false}
|
||||||
|
incognito={true}
|
||||||
/>
|
/>
|
||||||
</SafeAreaView>
|
</SafeAreaView>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -13,8 +13,11 @@ import { SafeAreaView } from 'react-native-safe-area-context';
|
|||||||
import * as SecureStore from 'expo-secure-store';
|
import * as SecureStore from 'expo-secure-store';
|
||||||
import * as ImagePicker from 'expo-image-picker';
|
import * as ImagePicker from 'expo-image-picker';
|
||||||
import * as Clipboard from 'expo-clipboard';
|
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 { router } from 'expo-router';
|
||||||
import { useAuth } from '@/contexts/AuthContext';
|
import { useAuth } from '@/contexts/AuthContext';
|
||||||
|
import { useBeneficiary } from '@/contexts/BeneficiaryContext';
|
||||||
import { ProfileDrawer } from '@/components/ProfileDrawer';
|
import { ProfileDrawer } from '@/components/ProfileDrawer';
|
||||||
import { useToast } from '@/components/ui/Toast';
|
import { useToast } from '@/components/ui/Toast';
|
||||||
import {
|
import {
|
||||||
@ -43,6 +46,7 @@ const generateInviteCode = (identifier: string): string => {
|
|||||||
|
|
||||||
export default function ProfileScreen() {
|
export default function ProfileScreen() {
|
||||||
const { user, logout } = useAuth();
|
const { user, logout } = useAuth();
|
||||||
|
const { clearAllBeneficiaryData } = useBeneficiary();
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
|
|
||||||
// Drawer state
|
// 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();
|
const { status } = await ImagePicker.requestMediaLibraryPermissionsAsync();
|
||||||
if (status !== 'granted') {
|
if (status !== 'granted') {
|
||||||
Alert.alert('Permission needed', 'Please allow access to your photo library to change avatar.');
|
Alert.alert('Permission needed', 'Please allow access to your photo library to change avatar.');
|
||||||
@ -84,13 +89,18 @@ export default function ProfileScreen() {
|
|||||||
mediaTypes: ['images'],
|
mediaTypes: ['images'],
|
||||||
allowsEditing: true,
|
allowsEditing: true,
|
||||||
aspect: [1, 1],
|
aspect: [1, 1],
|
||||||
quality: 0.5,
|
quality: 0.8,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!result.canceled && result.assets[0]) {
|
if (!result.canceled && result.assets[0]) {
|
||||||
const uri = result.assets[0].uri;
|
const originalUri = result.assets[0].uri;
|
||||||
setAvatarUri(uri);
|
|
||||||
await SecureStore.setItemAsync('userAvatar', 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',
|
text: 'Logout',
|
||||||
style: 'destructive',
|
style: 'destructive',
|
||||||
onPress: async () => {
|
onPress: async () => {
|
||||||
|
// Clear local UI state
|
||||||
|
setAvatarUri(null);
|
||||||
|
// Clear beneficiary context
|
||||||
|
await clearAllBeneficiaryData();
|
||||||
|
// Logout (clears SecureStore and AsyncStorage)
|
||||||
await logout();
|
await logout();
|
||||||
router.replace('/(auth)/login');
|
router.replace('/(auth)/login');
|
||||||
},
|
},
|
||||||
@ -161,18 +176,21 @@ export default function ProfileScreen() {
|
|||||||
>
|
>
|
||||||
{/* Profile Card */}
|
{/* Profile Card */}
|
||||||
<View style={styles.profileCard}>
|
<View style={styles.profileCard}>
|
||||||
<TouchableOpacity style={styles.avatarSection} onPress={handleAvatarPress}>
|
<View style={styles.avatarSection}>
|
||||||
<View style={styles.avatarContainer}>
|
<TouchableOpacity
|
||||||
|
style={styles.avatarContainer}
|
||||||
|
onPress={handleAvatarChange}
|
||||||
|
>
|
||||||
{avatarUri ? (
|
{avatarUri ? (
|
||||||
<Image source={{ uri: avatarUri }} style={styles.avatarImage} />
|
<Image source={{ uri: avatarUri }} style={styles.avatarImage} />
|
||||||
) : (
|
) : (
|
||||||
<Text style={styles.avatarText}>{userInitial}</Text>
|
<Text style={styles.avatarText}>{userInitial}</Text>
|
||||||
)}
|
)}
|
||||||
<View style={styles.avatarEditBadge}>
|
|
||||||
<Ionicons name="camera" size={14} color={AppColors.white} />
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
|
<TouchableOpacity style={styles.avatarEditBadge} onPress={handleAvatarChange}>
|
||||||
|
<Ionicons name="camera" size={14} color={AppColors.white} />
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
|
||||||
<Text style={styles.displayName}>{displayName}</Text>
|
<Text style={styles.displayName}>{displayName}</Text>
|
||||||
<Text style={styles.userEmail}>{user?.email || ''}</Text>
|
<Text style={styles.userEmail}>{user?.email || ''}</Text>
|
||||||
@ -247,6 +265,7 @@ export default function ProfileScreen() {
|
|||||||
settings={settings}
|
settings={settings}
|
||||||
onSettingChange={handleSettingChange}
|
onSettingChange={handleSettingChange}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
</SafeAreaView>
|
</SafeAreaView>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -299,6 +318,7 @@ const styles = StyleSheet.create({
|
|||||||
},
|
},
|
||||||
avatarSection: {
|
avatarSection: {
|
||||||
marginBottom: Spacing.md,
|
marginBottom: Spacing.md,
|
||||||
|
position: 'relative',
|
||||||
},
|
},
|
||||||
avatarContainer: {
|
avatarContainer: {
|
||||||
width: AvatarSizes.xl,
|
width: AvatarSizes.xl,
|
||||||
@ -307,7 +327,7 @@ const styles = StyleSheet.create({
|
|||||||
backgroundColor: AppColors.primary,
|
backgroundColor: AppColors.primary,
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
position: 'relative',
|
overflow: 'hidden',
|
||||||
},
|
},
|
||||||
avatarImage: {
|
avatarImage: {
|
||||||
width: AvatarSizes.xl,
|
width: AvatarSizes.xl,
|
||||||
|
|||||||
87
components/ImageLightbox.tsx
Normal file
87
components/ImageLightbox.tsx
Normal file
@ -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 (
|
||||||
|
<Modal
|
||||||
|
visible={visible}
|
||||||
|
transparent
|
||||||
|
animationType="fade"
|
||||||
|
statusBarTranslucent
|
||||||
|
onRequestClose={onClose}
|
||||||
|
>
|
||||||
|
<StatusBar barStyle="light-content" backgroundColor="rgba(0,0,0,0.95)" />
|
||||||
|
<View style={styles.container}>
|
||||||
|
{/* Close button */}
|
||||||
|
<TouchableOpacity style={styles.closeButton} onPress={onClose}>
|
||||||
|
<Ionicons name="close" size={28} color={AppColors.white} />
|
||||||
|
</TouchableOpacity>
|
||||||
|
|
||||||
|
{/* Image */}
|
||||||
|
<TouchableOpacity
|
||||||
|
style={styles.imageContainer}
|
||||||
|
activeOpacity={1}
|
||||||
|
onPress={onClose}
|
||||||
|
>
|
||||||
|
<Image
|
||||||
|
source={{ uri: imageUri }}
|
||||||
|
style={styles.image}
|
||||||
|
resizeMode="contain"
|
||||||
|
/>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
},
|
||||||
|
});
|
||||||
@ -20,6 +20,8 @@ interface BeneficiaryContextType {
|
|||||||
addLocalBeneficiary: (data: string | AddBeneficiaryData) => Promise<Beneficiary>;
|
addLocalBeneficiary: (data: string | AddBeneficiaryData) => Promise<Beneficiary>;
|
||||||
updateLocalBeneficiary: (id: number, data: Partial<Beneficiary>) => Promise<Beneficiary | null>;
|
updateLocalBeneficiary: (id: number, data: Partial<Beneficiary>) => Promise<Beneficiary | null>;
|
||||||
removeLocalBeneficiary: (id: number) => Promise<void>;
|
removeLocalBeneficiary: (id: number) => Promise<void>;
|
||||||
|
// Clear all data (used on logout)
|
||||||
|
clearAllBeneficiaryData: () => Promise<void>;
|
||||||
// Helper to format beneficiary context for AI
|
// Helper to format beneficiary context for AI
|
||||||
getBeneficiaryContext: () => string;
|
getBeneficiaryContext: () => string;
|
||||||
}
|
}
|
||||||
@ -116,6 +118,13 @@ export function BeneficiaryProvider({ children }: { children: React.ReactNode })
|
|||||||
setCurrentBeneficiary(null);
|
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(() => {
|
const getBeneficiaryContext = useCallback(() => {
|
||||||
if (!currentBeneficiary) {
|
if (!currentBeneficiary) {
|
||||||
return '';
|
return '';
|
||||||
@ -182,6 +191,7 @@ export function BeneficiaryProvider({ children }: { children: React.ReactNode })
|
|||||||
addLocalBeneficiary,
|
addLocalBeneficiary,
|
||||||
updateLocalBeneficiary,
|
updateLocalBeneficiary,
|
||||||
removeLocalBeneficiary,
|
removeLocalBeneficiary,
|
||||||
|
clearAllBeneficiaryData,
|
||||||
getBeneficiaryContext,
|
getBeneficiaryContext,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
13
package-lock.json
generated
13
package-lock.json
generated
@ -26,6 +26,7 @@
|
|||||||
"expo-font": "~14.0.10",
|
"expo-font": "~14.0.10",
|
||||||
"expo-haptics": "~15.0.8",
|
"expo-haptics": "~15.0.8",
|
||||||
"expo-image": "~3.0.11",
|
"expo-image": "~3.0.11",
|
||||||
|
"expo-image-manipulator": "^14.0.8",
|
||||||
"expo-image-picker": "~17.0.10",
|
"expo-image-picker": "~17.0.10",
|
||||||
"expo-linking": "~8.0.11",
|
"expo-linking": "~8.0.11",
|
||||||
"expo-router": "~6.0.21",
|
"expo-router": "~6.0.21",
|
||||||
@ -10769,6 +10770,18 @@
|
|||||||
"expo": "*"
|
"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": {
|
"node_modules/expo-image-picker": {
|
||||||
"version": "17.0.10",
|
"version": "17.0.10",
|
||||||
"resolved": "https://registry.npmjs.org/expo-image-picker/-/expo-image-picker-17.0.10.tgz",
|
"resolved": "https://registry.npmjs.org/expo-image-picker/-/expo-image-picker-17.0.10.tgz",
|
||||||
|
|||||||
@ -29,6 +29,7 @@
|
|||||||
"expo-font": "~14.0.10",
|
"expo-font": "~14.0.10",
|
||||||
"expo-haptics": "~15.0.8",
|
"expo-haptics": "~15.0.8",
|
||||||
"expo-image": "~3.0.11",
|
"expo-image": "~3.0.11",
|
||||||
|
"expo-image-manipulator": "^14.0.8",
|
||||||
"expo-image-picker": "~17.0.10",
|
"expo-image-picker": "~17.0.10",
|
||||||
"expo-linking": "~8.0.11",
|
"expo-linking": "~8.0.11",
|
||||||
"expo-router": "~6.0.21",
|
"expo-router": "~6.0.21",
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import type { ApiError, ApiResponse, AuthResponse, Beneficiary, BeneficiaryDashb
|
|||||||
import * as Crypto from 'expo-crypto';
|
import * as Crypto from 'expo-crypto';
|
||||||
import { File } from 'expo-file-system';
|
import { File } from 'expo-file-system';
|
||||||
import * as SecureStore from 'expo-secure-store';
|
import * as SecureStore from 'expo-secure-store';
|
||||||
|
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||||
|
|
||||||
// Callback for handling unauthorized responses (401)
|
// Callback for handling unauthorized responses (401)
|
||||||
let onUnauthorizedCallback: (() => void) | null = null;
|
let onUnauthorizedCallback: (() => void) | null = null;
|
||||||
@ -158,6 +159,10 @@ class ApiService {
|
|||||||
await SecureStore.deleteItemAsync('legacyAccessToken');
|
await SecureStore.deleteItemAsync('legacyAccessToken');
|
||||||
await SecureStore.deleteItemAsync('privileges');
|
await SecureStore.deleteItemAsync('privileges');
|
||||||
await SecureStore.deleteItemAsync('maxRole');
|
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)
|
// Save user email (for OTP auth flow)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user