Full project sync - app updates and profile screens

Changes:
- Updated app.json, eas.json configurations
- Modified login, chat, profile, dashboard screens
- Added profile subpages (about, edit, help, language,
  notifications, privacy, subscription, support, terms)
- Updated BeneficiaryContext
- Updated API service and types
- Updated discussion questions scheme
- Added .history to gitignore

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Sergei 2025-12-12 16:26:13 -08:00
parent 68b89d2565
commit 48384f07c5
23 changed files with 4658 additions and 238 deletions

1
.gitignore vendored
View File

@ -42,3 +42,4 @@ app-example
/ios /ios
/android /android
.git-credentials .git-credentials
wellnuoSheme/.history/

View File

@ -10,7 +10,7 @@
"newArchEnabled": true, "newArchEnabled": true,
"ios": { "ios": {
"supportsTablet": true, "supportsTablet": true,
"bundleIdentifier": "com.wellnuo.app", "bundleIdentifier": "com.kosyakorel1.wellnuo",
"infoPlist": { "infoPlist": {
"ITSAppUsesNonExemptEncryption": false "ITSAppUsesNonExemptEncryption": false
} }

View File

@ -58,9 +58,11 @@ export default function LoginScreen() {
> >
{/* Logo / Header */} {/* Logo / Header */}
<View style={styles.header}> <View style={styles.header}>
<View style={styles.logoContainer}> <Image
<Text style={styles.logoText}>WellNuo</Text> source={require('@/assets/images/icon.png')}
</View> style={styles.logo}
resizeMode="contain"
/>
<Text style={styles.title}>Welcome Back</Text> <Text style={styles.title}>Welcome Back</Text>
<Text style={styles.subtitle}>Sign in to continue monitoring your loved ones</Text> <Text style={styles.subtitle}>Sign in to continue monitoring your loved ones</Text>
</View> </View>
@ -149,20 +151,11 @@ const styles = StyleSheet.create({
alignItems: 'center', alignItems: 'center',
marginBottom: Spacing.xl, marginBottom: Spacing.xl,
}, },
logoContainer: { logo: {
width: 80, width: 180,
height: 80, height: 100,
borderRadius: BorderRadius.xl,
backgroundColor: AppColors.primary,
justifyContent: 'center',
alignItems: 'center',
marginBottom: Spacing.lg, marginBottom: Spacing.lg,
}, },
logoText: {
fontSize: FontSizes.lg,
fontWeight: '700',
color: AppColors.white,
},
title: { title: {
fontSize: FontSizes['2xl'], fontSize: FontSizes['2xl'],
fontWeight: '700', fontWeight: '700',

View File

@ -35,7 +35,7 @@ export default function TabLayout() {
options={{ options={{
title: 'Dashboard', title: 'Dashboard',
tabBarIcon: ({ color, size }) => ( tabBarIcon: ({ color, size }) => (
<Feather name="grid" size={22} color={color} /> <Feather name="home" size={22} color={color} />
), ),
}} }}
/> />

View File

@ -9,9 +9,9 @@ import { useBeneficiary } from '@/contexts/BeneficiaryContext';
import { AppColors, BorderRadius, FontSizes, Spacing } from '@/constants/theme'; import { AppColors, BorderRadius, FontSizes, Spacing } from '@/constants/theme';
import { FullScreenError } from '@/components/ui/ErrorMessage'; import { FullScreenError } from '@/components/ui/ErrorMessage';
// Start with login page, then redirect to dashboard after auth // Dashboard URL with patient ID
const LOGIN_URL = 'https://react.eluxnetworks.net/login'; const getDashboardUrl = (deploymentId: string) =>
const DASHBOARD_URL = 'https://react.eluxnetworks.net/dashboard'; `https://react.eluxnetworks.net/dashboard/${deploymentId}`;
export default function BeneficiaryDashboardScreen() { export default function BeneficiaryDashboardScreen() {
const { id } = useLocalSearchParams<{ id: string }>(); const { id } = useLocalSearchParams<{ id: string }>();
@ -24,7 +24,9 @@ export default function BeneficiaryDashboardScreen() {
const [userName, setUserName] = useState<string | null>(null); const [userName, setUserName] = useState<string | null>(null);
const [userId, setUserId] = useState<string | null>(null); const [userId, setUserId] = useState<string | null>(null);
const [isTokenLoaded, setIsTokenLoaded] = useState(false); const [isTokenLoaded, setIsTokenLoaded] = useState(false);
const [webViewUrl, setWebViewUrl] = useState(DASHBOARD_URL);
// Build dashboard URL with patient ID
const dashboardUrl = id ? getDashboardUrl(id) : 'https://react.eluxnetworks.net/dashboard';
const beneficiaryName = currentBeneficiary?.name || 'Dashboard'; const beneficiaryName = currentBeneficiary?.name || 'Dashboard';
@ -169,7 +171,7 @@ export default function BeneficiaryDashboardScreen() {
<View style={styles.webViewContainer}> <View style={styles.webViewContainer}>
<WebView <WebView
ref={webViewRef} ref={webViewRef}
source={{ uri: webViewUrl }} source={{ uri: dashboardUrl }}
style={styles.webView} style={styles.webView}
onLoadStart={() => setIsLoading(true)} onLoadStart={() => setIsLoading(true)}
onLoadEnd={() => setIsLoading(false)} onLoadEnd={() => setIsLoading(false)}

View File

@ -52,7 +52,9 @@ export default function ChatScreen() {
? `${beneficiaryContext} ${trimmedInput}` ? `${beneficiaryContext} ${trimmedInput}`
: trimmedInput; : trimmedInput;
const response = await api.sendMessage(questionWithContext); // Pass deployment_id from selected beneficiary (fallback to '21' if not selected)
const deploymentId = currentBeneficiary?.id?.toString() || '21';
const response = await api.sendMessage(questionWithContext, deploymentId);
if (response.ok && response.data?.response) { if (response.ok && response.data?.response) {
const assistantMessage: Message = { const assistantMessage: Message = {

View File

@ -1,124 +1,145 @@
import React, { useState, useRef, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import { View, Text, StyleSheet, ActivityIndicator, TouchableOpacity } from 'react-native'; import {
import { WebView } from 'react-native-webview'; View,
Text,
StyleSheet,
FlatList,
TouchableOpacity,
ActivityIndicator,
RefreshControl
} from 'react-native';
import { Ionicons } from '@expo/vector-icons'; import { Ionicons } from '@expo/vector-icons';
import { SafeAreaView } from 'react-native-safe-area-context'; import { SafeAreaView } from 'react-native-safe-area-context';
import * as SecureStore from 'expo-secure-store'; import { router } from 'expo-router';
import { useAuth } from '@/contexts/AuthContext'; import { useAuth } from '@/contexts/AuthContext';
import { AppColors, FontSizes, Spacing } from '@/constants/theme'; import { useBeneficiary } from '@/contexts/BeneficiaryContext';
import { api } from '@/services/api';
import { AppColors, BorderRadius, FontSizes, Spacing } from '@/constants/theme';
import type { Beneficiary } from '@/types';
const DASHBOARD_URL = 'https://react.eluxnetworks.net/dashboard'; // Patient card component
interface PatientCardProps {
patient: Beneficiary;
onPress: () => void;
}
function PatientCard({ patient, onPress }: PatientCardProps) {
const isOnline = patient.status === 'online';
const wellnessColor = patient.wellness_score && patient.wellness_score >= 70
? AppColors.success
: patient.wellness_score && patient.wellness_score >= 40
? '#F59E0B'
: AppColors.error;
return (
<TouchableOpacity style={styles.card} onPress={onPress} activeOpacity={0.7}>
<View style={styles.cardContent}>
{/* Avatar */}
<View style={[styles.avatar, isOnline && styles.avatarOnline]}>
<Text style={styles.avatarText}>
{patient.name.charAt(0).toUpperCase()}
</Text>
{isOnline && <View style={styles.onlineIndicator} />}
</View>
{/* Info */}
<View style={styles.info}>
<Text style={styles.name}>{patient.name}</Text>
{patient.last_location && (
<View style={styles.locationRow}>
<Ionicons name="location-outline" size={12} color={AppColors.textSecondary} />
<Text style={styles.locationText}>{patient.last_location}</Text>
</View>
)}
<View style={styles.statusRow}>
<View style={[styles.statusBadge, isOnline ? styles.statusOnline : styles.statusOffline]}>
<Text style={[styles.statusText, isOnline ? styles.statusTextOnline : styles.statusTextOffline]}>
{isOnline ? 'Active' : 'Inactive'}
</Text>
</View>
{patient.last_activity && (
<Text style={styles.lastActivity}>{patient.last_activity}</Text>
)}
</View>
</View>
{/* Wellness Score */}
{patient.wellness_score !== undefined && (
<View style={styles.wellnessContainer}>
<Text style={[styles.wellnessScore, { color: wellnessColor }]}>
{patient.wellness_score}%
</Text>
<Text style={styles.wellnessLabel}>Wellness</Text>
</View>
)}
{/* Arrow */}
<Ionicons name="chevron-forward" size={24} color={AppColors.textMuted} />
</View>
</TouchableOpacity>
);
}
export default function HomeScreen() { export default function HomeScreen() {
const { user } = useAuth(); const { user } = useAuth();
const webViewRef = useRef<WebView>(null); const { currentBeneficiary, setCurrentBeneficiary } = useBeneficiary();
const [isLoading, setIsLoading] = useState(true); const [isLoading, setIsLoading] = useState(true);
const [isRefreshing, setIsRefreshing] = useState(false);
const [patients, setPatients] = useState<Beneficiary[]>([]);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const [canGoBack, setCanGoBack] = useState(false);
const [authToken, setAuthToken] = useState<string | null>(null);
const [userName, setUserName] = useState<string | null>(null);
const [userId, setUserId] = useState<string | null>(null);
const [isTokenLoaded, setIsTokenLoaded] = useState(false);
// Load credentials from SecureStore // Load patients from API
useEffect(() => { useEffect(() => {
const loadCredentials = async () => { loadPatients();
try {
const token = await SecureStore.getItemAsync('accessToken');
const user = await SecureStore.getItemAsync('userName');
const uid = await SecureStore.getItemAsync('userId');
setAuthToken(token);
setUserName(user);
setUserId(uid);
console.log('Home: Loaded credentials for WebView:', { hasToken: !!token, user, uid });
} catch (err) {
console.error('Failed to load credentials:', err);
} finally {
setIsTokenLoaded(true);
}
};
loadCredentials();
}, []); }, []);
// JavaScript to inject auth token into localStorage const loadPatients = async () => {
// Web app expects auth2 as JSON: {username, token, user_id}
const injectedJavaScript = authToken
? `
(function() {
try {
var authData = {
username: '${userName || ''}',
token: '${authToken}',
user_id: ${userId || 'null'}
};
localStorage.setItem('auth2', JSON.stringify(authData));
console.log('Auth injected:', authData.username);
} catch(e) {
console.error('Failed to inject token:', e);
}
})();
true;
`
: '';
const handleRefresh = () => {
setError(null);
setIsLoading(true); setIsLoading(true);
webViewRef.current?.reload(); setError(null);
}; try {
const response = await api.getAllPatients();
const handleWebViewBack = () => { if (response.ok && response.data) {
if (canGoBack) { setPatients(response.data);
webViewRef.current?.goBack(); // Auto-select first beneficiary if none selected
if (!currentBeneficiary && response.data.length > 0) {
setCurrentBeneficiary(response.data[0]);
}
} else {
setError(response.error?.message || 'Failed to load patients');
}
} catch (err) {
console.error('Failed to load patients:', err);
setError('Failed to load patients');
} finally {
setIsLoading(false);
} }
}; };
const handleNavigationStateChange = (navState: any) => { const handleRefresh = async () => {
setCanGoBack(navState.canGoBack); setIsRefreshing(true);
await loadPatients();
setIsRefreshing(false);
}; };
const handleError = () => { const handlePatientPress = (patient: Beneficiary) => {
setError('Failed to load dashboard. Please check your internet connection.'); // Set current beneficiary in context
setIsLoading(false); setCurrentBeneficiary(patient);
// Navigate to patient dashboard with deployment_id
router.push(`/(tabs)/beneficiaries/${patient.id}/dashboard`);
}; };
// Wait for token to load if (isLoading) {
if (!isTokenLoaded) {
return ( return (
<SafeAreaView style={styles.container} edges={['top']}> <SafeAreaView style={styles.container} edges={['top']}>
<View style={styles.header}> <View style={styles.header}>
<View> <View>
<Text style={styles.greeting}>Hello, {user?.user_name || 'User'}</Text> <Text style={styles.greeting}>Hello, {user?.user_name || 'User'}</Text>
<Text style={styles.headerTitle}>Dashboard</Text> <Text style={styles.headerTitle}>My Beneficiaries</Text>
</View> </View>
</View> </View>
<View style={styles.loadingContainer}> <View style={styles.loadingContainer}>
<ActivityIndicator size="large" color={AppColors.primary} /> <ActivityIndicator size="large" color={AppColors.primary} />
<Text style={styles.loadingText}>Preparing dashboard...</Text> <Text style={styles.loadingText}>Loading patients...</Text>
</View>
</SafeAreaView>
);
}
if (error) {
return (
<SafeAreaView style={styles.container} edges={['top']}>
<View style={styles.header}>
<View>
<Text style={styles.greeting}>Hello, {user?.user_name || 'User'}</Text>
<Text style={styles.headerTitle}>Dashboard</Text>
</View>
<TouchableOpacity style={styles.refreshButton} onPress={handleRefresh}>
<Ionicons name="refresh" size={22} color={AppColors.primary} />
</TouchableOpacity>
</View>
<View style={styles.errorContainer}>
<Ionicons name="cloud-offline-outline" size={64} color={AppColors.textMuted} />
<Text style={styles.errorTitle}>Connection Error</Text>
<Text style={styles.errorText}>{error}</Text>
<TouchableOpacity style={styles.retryButton} onPress={handleRefresh}>
<Text style={styles.retryButtonText}>Try Again</Text>
</TouchableOpacity>
</View> </View>
</SafeAreaView> </SafeAreaView>
); );
@ -130,52 +151,42 @@ export default function HomeScreen() {
<View style={styles.header}> <View style={styles.header}>
<View> <View>
<Text style={styles.greeting}>Hello, {user?.user_name || 'User'}</Text> <Text style={styles.greeting}>Hello, {user?.user_name || 'User'}</Text>
<Text style={styles.headerTitle}>Dashboard</Text> <Text style={styles.headerTitle}>My Beneficiaries</Text>
</View>
<View style={styles.headerActions}>
{canGoBack && (
<TouchableOpacity style={styles.actionButton} onPress={handleWebViewBack}>
<Ionicons name="chevron-back" size={22} color={AppColors.primary} />
</TouchableOpacity>
)}
<TouchableOpacity style={styles.actionButton} onPress={handleRefresh}>
<Ionicons name="refresh" size={22} color={AppColors.primary} />
</TouchableOpacity>
</View> </View>
<TouchableOpacity style={styles.refreshButton} onPress={handleRefresh}>
<Ionicons name="refresh" size={22} color={AppColors.primary} />
</TouchableOpacity>
</View> </View>
{/* WebView Dashboard */} {/* Patient List */}
<View style={styles.webViewContainer}> {patients.length === 0 ? (
<WebView <View style={styles.emptyContainer}>
ref={webViewRef} <Ionicons name="people-outline" size={64} color={AppColors.textMuted} />
source={{ uri: DASHBOARD_URL }} <Text style={styles.emptyTitle}>No Patients</Text>
style={styles.webView} <Text style={styles.emptyText}>You don't have any patients assigned yet.</Text>
onLoadStart={() => setIsLoading(true)} </View>
onLoadEnd={() => setIsLoading(false)} ) : (
onError={handleError} <FlatList
onHttpError={handleError} data={patients}
onNavigationStateChange={handleNavigationStateChange} keyExtractor={(item) => item.id.toString()}
javaScriptEnabled={true} renderItem={({ item }) => (
domStorageEnabled={true} <PatientCard
startInLoadingState={true} patient={item}
scalesPageToFit={true} onPress={() => handlePatientPress(item)}
allowsBackForwardNavigationGestures={true} />
injectedJavaScriptBeforeContentLoaded={injectedJavaScript}
injectedJavaScript={injectedJavaScript}
renderLoading={() => (
<View style={styles.loadingContainer}>
<ActivityIndicator size="large" color={AppColors.primary} />
<Text style={styles.loadingText}>Loading dashboard...</Text>
</View>
)} )}
contentContainerStyle={styles.listContent}
showsVerticalScrollIndicator={false}
refreshControl={
<RefreshControl
refreshing={isRefreshing}
onRefresh={handleRefresh}
colors={[AppColors.primary]}
tintColor={AppColors.primary}
/>
}
/> />
)}
{isLoading && (
<View style={styles.loadingOverlay}>
<ActivityIndicator size="large" color={AppColors.primary} />
</View>
)}
</View>
</SafeAreaView> </SafeAreaView>
); );
} }
@ -204,76 +215,151 @@ const styles = StyleSheet.create({
fontWeight: '700', fontWeight: '700',
color: AppColors.textPrimary, color: AppColors.textPrimary,
}, },
headerActions: {
flexDirection: 'row',
alignItems: 'center',
},
actionButton: {
padding: Spacing.xs,
marginLeft: Spacing.xs,
},
refreshButton: { refreshButton: {
padding: Spacing.xs, padding: Spacing.xs,
}, },
webViewContainer: {
flex: 1,
},
webView: {
flex: 1,
},
loadingContainer: { loadingContainer: {
position: 'absolute', flex: 1,
top: 0,
left: 0,
right: 0,
bottom: 0,
justifyContent: 'center', justifyContent: 'center',
alignItems: 'center', alignItems: 'center',
backgroundColor: AppColors.background,
},
loadingOverlay: {
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: 'rgba(255,255,255,0.8)',
}, },
loadingText: { loadingText: {
marginTop: Spacing.md, marginTop: Spacing.md,
fontSize: FontSizes.base, fontSize: FontSizes.base,
color: AppColors.textSecondary, color: AppColors.textSecondary,
}, },
errorContainer: { emptyContainer: {
flex: 1, flex: 1,
justifyContent: 'center', justifyContent: 'center',
alignItems: 'center', alignItems: 'center',
padding: Spacing.xl, padding: Spacing.xl,
}, },
errorTitle: { emptyTitle: {
fontSize: FontSizes.lg, fontSize: FontSizes.lg,
fontWeight: '600', fontWeight: '600',
color: AppColors.textPrimary, color: AppColors.textPrimary,
marginTop: Spacing.md, marginTop: Spacing.md,
}, },
errorText: { emptyText: {
fontSize: FontSizes.base, fontSize: FontSizes.base,
color: AppColors.textSecondary, color: AppColors.textSecondary,
textAlign: 'center', textAlign: 'center',
marginTop: Spacing.xs, marginTop: Spacing.xs,
}, },
retryButton: { listContent: {
marginTop: Spacing.lg, padding: Spacing.lg,
paddingHorizontal: Spacing.xl, paddingBottom: Spacing.xxl,
paddingVertical: Spacing.md,
backgroundColor: AppColors.primary,
borderRadius: 8,
}, },
retryButtonText: { // Card styles
color: AppColors.white, card: {
fontSize: FontSizes.base, backgroundColor: AppColors.white,
borderRadius: BorderRadius.lg,
marginBottom: Spacing.md,
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.05,
shadowRadius: 4,
elevation: 2,
},
cardContent: {
flexDirection: 'row',
alignItems: 'center',
padding: Spacing.md,
},
avatar: {
width: 56,
height: 56,
borderRadius: BorderRadius.full,
backgroundColor: AppColors.primaryLight,
justifyContent: 'center',
alignItems: 'center',
position: 'relative',
},
avatarOnline: {
borderWidth: 2,
borderColor: AppColors.success,
},
avatarText: {
fontSize: FontSizes.xl,
fontWeight: '600', fontWeight: '600',
color: AppColors.white,
},
onlineIndicator: {
position: 'absolute',
bottom: 2,
right: 2,
width: 14,
height: 14,
borderRadius: 7,
backgroundColor: AppColors.success,
borderWidth: 2,
borderColor: AppColors.white,
},
info: {
flex: 1,
marginLeft: Spacing.md,
},
name: {
fontSize: FontSizes.lg,
fontWeight: '600',
color: AppColors.textPrimary,
},
relationship: {
fontSize: FontSizes.sm,
color: AppColors.textSecondary,
marginTop: 2,
},
statusRow: {
flexDirection: 'row',
alignItems: 'center',
marginTop: Spacing.xs,
},
statusBadge: {
paddingHorizontal: Spacing.sm,
paddingVertical: 2,
borderRadius: BorderRadius.sm,
},
statusOnline: {
backgroundColor: 'rgba(34, 197, 94, 0.1)',
},
statusOffline: {
backgroundColor: 'rgba(107, 114, 128, 0.1)',
},
statusText: {
fontSize: FontSizes.xs,
fontWeight: '500',
},
statusTextOnline: {
color: AppColors.success,
},
statusTextOffline: {
color: AppColors.textMuted,
},
lastActivity: {
fontSize: FontSizes.xs,
color: AppColors.textMuted,
marginLeft: Spacing.sm,
},
locationRow: {
flexDirection: 'row',
alignItems: 'center',
marginTop: 2,
},
locationText: {
fontSize: FontSizes.xs,
color: AppColors.textSecondary,
marginLeft: 4,
},
wellnessContainer: {
alignItems: 'center',
marginRight: Spacing.sm,
},
wellnessScore: {
fontSize: FontSizes.lg,
fontWeight: '700',
},
wellnessLabel: {
fontSize: FontSizes.xs,
color: AppColors.textMuted,
}, },
}); });

View File

@ -1,4 +1,4 @@
import React from 'react'; import React, { useState } from 'react';
import { import {
View, View,
Text, Text,
@ -6,6 +6,7 @@ import {
ScrollView, ScrollView,
TouchableOpacity, TouchableOpacity,
Alert, Alert,
Switch,
} from 'react-native'; } from 'react-native';
import { router } from 'expo-router'; import { router } from 'expo-router';
import { Ionicons } from '@expo/vector-icons'; import { Ionicons } from '@expo/vector-icons';
@ -21,6 +22,7 @@ interface MenuItemProps {
subtitle?: string; subtitle?: string;
onPress?: () => void; onPress?: () => void;
showChevron?: boolean; showChevron?: boolean;
rightElement?: React.ReactNode;
} }
function MenuItem({ function MenuItem({
@ -31,9 +33,10 @@ function MenuItem({
subtitle, subtitle,
onPress, onPress,
showChevron = true, showChevron = true,
rightElement,
}: MenuItemProps) { }: MenuItemProps) {
return ( return (
<TouchableOpacity style={styles.menuItem} onPress={onPress}> <TouchableOpacity style={styles.menuItem} onPress={onPress} disabled={!onPress}>
<View style={[styles.menuIconContainer, { backgroundColor: iconBgColor }]}> <View style={[styles.menuIconContainer, { backgroundColor: iconBgColor }]}>
<Ionicons name={icon} size={20} color={iconColor} /> <Ionicons name={icon} size={20} color={iconColor} />
</View> </View>
@ -41,7 +44,7 @@ function MenuItem({
<Text style={styles.menuTitle}>{title}</Text> <Text style={styles.menuTitle}>{title}</Text>
{subtitle && <Text style={styles.menuSubtitle}>{subtitle}</Text>} {subtitle && <Text style={styles.menuSubtitle}>{subtitle}</Text>}
</View> </View>
{showChevron && ( {rightElement ? rightElement : showChevron && onPress && (
<Ionicons name="chevron-forward" size={20} color={AppColors.textMuted} /> <Ionicons name="chevron-forward" size={20} color={AppColors.textMuted} />
)} )}
</TouchableOpacity> </TouchableOpacity>
@ -51,6 +54,12 @@ function MenuItem({
export default function ProfileScreen() { export default function ProfileScreen() {
const { user, logout } = useAuth(); const { user, logout } = useAuth();
// Settings states
const [pushNotifications, setPushNotifications] = useState(true);
const [emailNotifications, setEmailNotifications] = useState(false);
const [darkMode, setDarkMode] = useState(false);
const [biometricLogin, setBiometricLogin] = useState(false);
const handleLogout = () => { const handleLogout = () => {
Alert.alert( Alert.alert(
'Logout', 'Logout',
@ -70,6 +79,34 @@ export default function ProfileScreen() {
); );
}; };
// Navigation handlers - now using actual page navigation
const handleEditProfile = () => router.push('/profile/edit');
const handleNotifications = () => router.push('/profile/notifications');
const handlePrivacy = () => router.push('/profile/privacy');
const handleUpgrade = () => router.push('/profile/subscription');
const handlePayment = () => router.push('/profile/subscription');
const handleHelp = () => router.push('/profile/help');
const handleSupport = () => router.push('/profile/support');
const handleTerms = () => router.push('/profile/terms');
const handlePrivacyPolicy = () => router.push('/profile/privacy-policy');
const handleLanguage = () => router.push('/profile/language');
const handleAbout = () => router.push('/profile/about');
const handleDevInfo = () => {
Alert.alert(
'Developer Info',
`User ID: ${user?.user_id || 'N/A'}\n` +
`Username: ${user?.user_name || 'N/A'}\n` +
`Role: ${user?.max_role || 'N/A'}\n` +
`Privileges: ${user?.privileges || 'N/A'}\n\n` +
'Tap "Copy" to copy debug info.',
[
{ text: 'Close' },
{ text: 'Copy', onPress: () => Alert.alert('Copied', 'Debug info copied to clipboard') },
]
);
};
return ( return (
<SafeAreaView style={styles.container} edges={['top']}> <SafeAreaView style={styles.container} edges={['top']}>
<ScrollView showsVerticalScrollIndicator={false}> <ScrollView showsVerticalScrollIndicator={false}>
@ -88,15 +125,34 @@ export default function ProfileScreen() {
<View style={styles.userInfo}> <View style={styles.userInfo}>
<Text style={styles.userName}>{user?.user_name || 'User'}</Text> <Text style={styles.userName}>{user?.user_name || 'User'}</Text>
<Text style={styles.userRole}> <Text style={styles.userRole}>
Role: {user?.max_role === 2 ? 'Admin' : 'User'} {user?.max_role === 2 ? 'Administrator' : 'Caregiver'}
</Text> </Text>
<Text style={styles.userId}>ID: {user?.user_id || 'N/A'}</Text>
</View> </View>
<TouchableOpacity style={styles.editButton}> <TouchableOpacity style={styles.editButton} onPress={handleEditProfile}>
<Ionicons name="pencil" size={18} color={AppColors.primary} /> <Ionicons name="pencil" size={18} color={AppColors.primary} />
</TouchableOpacity> </TouchableOpacity>
</View> </View>
{/* Menu Sections */} {/* Quick Stats */}
<View style={styles.statsContainer}>
<View style={styles.statItem}>
<Text style={styles.statValue}>{user?.privileges?.split(',').length || 0}</Text>
<Text style={styles.statLabel}>Beneficiaries</Text>
</View>
<View style={styles.statDivider} />
<View style={styles.statItem}>
<Text style={styles.statValue}>24/7</Text>
<Text style={styles.statLabel}>Monitoring</Text>
</View>
<View style={styles.statDivider} />
<View style={styles.statItem}>
<Text style={styles.statValue}>Free</Text>
<Text style={styles.statLabel}>Plan</Text>
</View>
</View>
{/* Account Section */}
<View style={styles.section}> <View style={styles.section}>
<Text style={styles.sectionTitle}>Account</Text> <Text style={styles.sectionTitle}>Account</Text>
<View style={styles.menuCard}> <View style={styles.menuCard}>
@ -104,6 +160,7 @@ export default function ProfileScreen() {
icon="person-outline" icon="person-outline"
title="Edit Profile" title="Edit Profile"
subtitle="Update your personal information" subtitle="Update your personal information"
onPress={handleEditProfile}
/> />
<View style={styles.menuDivider} /> <View style={styles.menuDivider} />
<MenuItem <MenuItem
@ -112,6 +169,7 @@ export default function ProfileScreen() {
iconColor={AppColors.warning} iconColor={AppColors.warning}
title="Notifications" title="Notifications"
subtitle="Manage notification preferences" subtitle="Manage notification preferences"
onPress={handleNotifications}
/> />
<View style={styles.menuDivider} /> <View style={styles.menuDivider} />
<MenuItem <MenuItem
@ -120,10 +178,104 @@ export default function ProfileScreen() {
iconColor={AppColors.success} iconColor={AppColors.success}
title="Privacy & Security" title="Privacy & Security"
subtitle="Password, 2FA, data" subtitle="Password, 2FA, data"
onPress={handlePrivacy}
/> />
</View> </View>
</View> </View>
{/* App Settings */}
<View style={styles.section}>
<Text style={styles.sectionTitle}>App Settings</Text>
<View style={styles.menuCard}>
<MenuItem
icon="notifications"
iconBgColor="#DBEAFE"
iconColor={AppColors.primary}
title="Push Notifications"
subtitle="Receive alerts on your device"
showChevron={false}
rightElement={
<Switch
value={pushNotifications}
onValueChange={setPushNotifications}
trackColor={{ false: '#E5E7EB', true: AppColors.primaryLight }}
thumbColor={pushNotifications ? AppColors.primary : '#9CA3AF'}
/>
}
/>
<View style={styles.menuDivider} />
<MenuItem
icon="mail-outline"
iconBgColor="#FEE2E2"
iconColor="#EF4444"
title="Email Notifications"
subtitle="Daily summary reports"
showChevron={false}
rightElement={
<Switch
value={emailNotifications}
onValueChange={setEmailNotifications}
trackColor={{ false: '#E5E7EB', true: AppColors.primaryLight }}
thumbColor={emailNotifications ? AppColors.primary : '#9CA3AF'}
/>
}
/>
<View style={styles.menuDivider} />
<MenuItem
icon="moon-outline"
iconBgColor="#E0E7FF"
iconColor="#4F46E5"
title="Dark Mode"
subtitle="Coming soon"
showChevron={false}
rightElement={
<Switch
value={darkMode}
onValueChange={(value) => {
setDarkMode(value);
Alert.alert('Dark Mode', 'Dark mode will be available in a future update!');
setDarkMode(false);
}}
trackColor={{ false: '#E5E7EB', true: AppColors.primaryLight }}
thumbColor={darkMode ? AppColors.primary : '#9CA3AF'}
/>
}
/>
<View style={styles.menuDivider} />
<MenuItem
icon="finger-print"
iconBgColor="#D1FAE5"
iconColor={AppColors.success}
title="Biometric Login"
subtitle="Face ID / Touch ID"
showChevron={false}
rightElement={
<Switch
value={biometricLogin}
onValueChange={(value) => {
setBiometricLogin(value);
if (value) {
Alert.alert('Biometric Login', 'Biometric authentication enabled!');
}
}}
trackColor={{ false: '#E5E7EB', true: AppColors.primaryLight }}
thumbColor={biometricLogin ? AppColors.primary : '#9CA3AF'}
/>
}
/>
<View style={styles.menuDivider} />
<MenuItem
icon="language-outline"
iconBgColor="#FEF3C7"
iconColor="#F59E0B"
title="Language"
subtitle="English"
onPress={handleLanguage}
/>
</View>
</View>
{/* Subscription */}
<View style={styles.section}> <View style={styles.section}>
<Text style={styles.sectionTitle}>Subscription</Text> <Text style={styles.sectionTitle}>Subscription</Text>
<View style={styles.menuCard}> <View style={styles.menuCard}>
@ -133,16 +285,19 @@ export default function ProfileScreen() {
iconColor="#9333EA" iconColor="#9333EA"
title="WellNuo Pro" title="WellNuo Pro"
subtitle="Upgrade for premium features" subtitle="Upgrade for premium features"
onPress={handleUpgrade}
/> />
<View style={styles.menuDivider} /> <View style={styles.menuDivider} />
<MenuItem <MenuItem
icon="card-outline" icon="card-outline"
title="Payment Methods" title="Payment Methods"
subtitle="Manage your payment options" subtitle="Manage your payment options"
onPress={handlePayment}
/> />
</View> </View>
</View> </View>
{/* Support */}
<View style={styles.section}> <View style={styles.section}>
<Text style={styles.sectionTitle}>Support</Text> <Text style={styles.sectionTitle}>Support</Text>
<View style={styles.menuCard}> <View style={styles.menuCard}>
@ -150,22 +305,50 @@ export default function ProfileScreen() {
icon="help-circle-outline" icon="help-circle-outline"
title="Help Center" title="Help Center"
subtitle="FAQs and guides" subtitle="FAQs and guides"
onPress={handleHelp}
/> />
<View style={styles.menuDivider} /> <View style={styles.menuDivider} />
<MenuItem <MenuItem
icon="chatbubble-outline" icon="chatbubble-outline"
title="Contact Support" title="Contact Support"
subtitle="Get help from our team" subtitle="Get help from our team"
onPress={handleSupport}
/> />
<View style={styles.menuDivider} /> <View style={styles.menuDivider} />
<MenuItem <MenuItem
icon="document-text-outline" icon="document-text-outline"
title="Terms of Service" title="Terms of Service"
onPress={handleTerms}
/> />
<View style={styles.menuDivider} /> <View style={styles.menuDivider} />
<MenuItem <MenuItem
icon="shield-outline" icon="shield-outline"
title="Privacy Policy" title="Privacy Policy"
onPress={handlePrivacyPolicy}
/>
</View>
</View>
{/* About */}
<View style={styles.section}>
<Text style={styles.sectionTitle}>About</Text>
<View style={styles.menuCard}>
<MenuItem
icon="information-circle-outline"
iconBgColor="#DBEAFE"
iconColor={AppColors.primary}
title="About WellNuo"
subtitle="Version, licenses, credits"
onPress={handleAbout}
/>
<View style={styles.menuDivider} />
<MenuItem
icon="bug-outline"
iconBgColor="#FEE2E2"
iconColor="#EF4444"
title="Developer Info"
subtitle="Debug information"
onPress={handleDevInfo}
/> />
</View> </View>
</View> </View>
@ -179,7 +362,7 @@ export default function ProfileScreen() {
</View> </View>
{/* Version */} {/* Version */}
<Text style={styles.version}>WellNuo v1.0.0</Text> <Text style={styles.version}>WellNuo v1.0.0 (Expo SDK 54)</Text>
</ScrollView> </ScrollView>
</SafeAreaView> </SafeAreaView>
); );
@ -207,7 +390,6 @@ const styles = StyleSheet.create({
alignItems: 'center', alignItems: 'center',
backgroundColor: AppColors.background, backgroundColor: AppColors.background,
padding: Spacing.lg, padding: Spacing.lg,
marginBottom: Spacing.md,
}, },
avatarContainer: { avatarContainer: {
width: 64, width: 64,
@ -234,7 +416,12 @@ const styles = StyleSheet.create({
userRole: { userRole: {
fontSize: FontSizes.sm, fontSize: FontSizes.sm,
color: AppColors.textSecondary, color: AppColors.textSecondary,
marginTop: Spacing.xs, marginTop: 2,
},
userId: {
fontSize: FontSizes.xs,
color: AppColors.textMuted,
marginTop: 2,
}, },
editButton: { editButton: {
width: 40, width: 40,
@ -244,6 +431,34 @@ const styles = StyleSheet.create({
justifyContent: 'center', justifyContent: 'center',
alignItems: 'center', alignItems: 'center',
}, },
statsContainer: {
flexDirection: 'row',
backgroundColor: AppColors.background,
paddingVertical: Spacing.md,
paddingHorizontal: Spacing.lg,
borderTopWidth: 1,
borderTopColor: AppColors.border,
marginBottom: Spacing.md,
},
statItem: {
flex: 1,
alignItems: 'center',
},
statValue: {
fontSize: FontSizes.xl,
fontWeight: '700',
color: AppColors.primary,
},
statLabel: {
fontSize: FontSizes.xs,
color: AppColors.textMuted,
marginTop: 2,
},
statDivider: {
width: 1,
backgroundColor: AppColors.border,
marginVertical: Spacing.xs,
},
section: { section: {
marginBottom: Spacing.md, marginBottom: Spacing.md,
}, },

81
app/profile/_layout.tsx Normal file
View File

@ -0,0 +1,81 @@
import { Stack } from 'expo-router';
import { AppColors } from '@/constants/theme';
export default function ProfileLayout() {
return (
<Stack
screenOptions={{
headerStyle: {
backgroundColor: AppColors.background,
},
headerTintColor: AppColors.primary,
headerTitleStyle: {
fontWeight: '600',
},
headerBackTitleVisible: false,
headerShadowVisible: false,
}}
>
<Stack.Screen
name="edit"
options={{
title: 'Edit Profile',
}}
/>
<Stack.Screen
name="notifications"
options={{
title: 'Notifications',
}}
/>
<Stack.Screen
name="privacy"
options={{
title: 'Privacy & Security',
}}
/>
<Stack.Screen
name="help"
options={{
title: 'Help Center',
}}
/>
<Stack.Screen
name="support"
options={{
title: 'Contact Support',
}}
/>
<Stack.Screen
name="subscription"
options={{
title: 'WellNuo Pro',
}}
/>
<Stack.Screen
name="terms"
options={{
title: 'Terms of Service',
}}
/>
<Stack.Screen
name="privacy-policy"
options={{
title: 'Privacy Policy',
}}
/>
<Stack.Screen
name="about"
options={{
title: 'About WellNuo',
}}
/>
<Stack.Screen
name="language"
options={{
title: 'Language',
}}
/>
</Stack>
);
}

358
app/profile/about.tsx Normal file
View File

@ -0,0 +1,358 @@
import React from 'react';
import {
View,
Text,
StyleSheet,
ScrollView,
TouchableOpacity,
Linking,
Image,
} from 'react-native';
import { Ionicons } from '@expo/vector-icons';
import { SafeAreaView } from 'react-native-safe-area-context';
import { AppColors, BorderRadius, FontSizes, Spacing } from '@/constants/theme';
interface InfoRowProps {
label: string;
value: string;
}
function InfoRow({ label, value }: InfoRowProps) {
return (
<View style={styles.infoRow}>
<Text style={styles.infoLabel}>{label}</Text>
<Text style={styles.infoValue}>{value}</Text>
</View>
);
}
interface LinkRowProps {
icon: keyof typeof Ionicons.glyphMap;
title: string;
onPress: () => void;
}
function LinkRow({ icon, title, onPress }: LinkRowProps) {
return (
<TouchableOpacity style={styles.linkRow} onPress={onPress}>
<Ionicons name={icon} size={20} color={AppColors.primary} />
<Text style={styles.linkText}>{title}</Text>
<Ionicons name="open-outline" size={16} color={AppColors.textMuted} />
</TouchableOpacity>
);
}
export default function AboutScreen() {
const openURL = (url: string) => {
Linking.openURL(url).catch(() => {});
};
return (
<SafeAreaView style={styles.container} edges={['bottom']}>
<ScrollView showsVerticalScrollIndicator={false}>
{/* App Logo & Name */}
<View style={styles.heroSection}>
<View style={styles.logoContainer}>
<Ionicons name="heart" size={48} color={AppColors.white} />
</View>
<Text style={styles.appName}>WellNuo</Text>
<Text style={styles.appTagline}>Caring for Those Who Matter Most</Text>
</View>
{/* Version Info */}
<View style={styles.section}>
<Text style={styles.sectionTitle}>App Information</Text>
<View style={styles.card}>
<InfoRow label="Version" value="1.0.0" />
<View style={styles.infoDivider} />
<InfoRow label="Build" value="2024.12.001" />
<View style={styles.infoDivider} />
<InfoRow label="Platform" value="iOS / Android" />
<View style={styles.infoDivider} />
<InfoRow label="SDK" value="Expo SDK 54" />
<View style={styles.infoDivider} />
<InfoRow label="Last Updated" value="December 2024" />
</View>
</View>
{/* Description */}
<View style={styles.section}>
<Text style={styles.sectionTitle}>About</Text>
<View style={styles.card}>
<Text style={styles.description}>
WellNuo is a comprehensive elderly care monitoring application designed to help
families and caregivers stay connected with their loved ones. Using advanced
sensor technology and AI-powered analytics, WellNuo provides real-time insights
into daily activities, health patterns, and emergency situations.
</Text>
<Text style={styles.description}>
Our mission is to bring peace of mind to families while preserving the independence
and dignity of elderly individuals.
</Text>
</View>
</View>
{/* Features */}
<View style={styles.section}>
<Text style={styles.sectionTitle}>Key Features</Text>
<View style={styles.card}>
<View style={styles.featureItem}>
<View style={[styles.featureIcon, { backgroundColor: '#DBEAFE' }]}>
<Ionicons name="pulse" size={20} color="#3B82F6" />
</View>
<View style={styles.featureContent}>
<Text style={styles.featureTitle}>Real-time Monitoring</Text>
<Text style={styles.featureDescription}>24/7 activity and wellness tracking</Text>
</View>
</View>
<View style={styles.featureItem}>
<View style={[styles.featureIcon, { backgroundColor: '#FEE2E2' }]}>
<Ionicons name="warning" size={20} color="#EF4444" />
</View>
<View style={styles.featureContent}>
<Text style={styles.featureTitle}>Emergency Alerts</Text>
<Text style={styles.featureDescription}>Instant notifications for falls and emergencies</Text>
</View>
</View>
<View style={styles.featureItem}>
<View style={[styles.featureIcon, { backgroundColor: '#D1FAE5' }]}>
<Ionicons name="analytics" size={20} color="#10B981" />
</View>
<View style={styles.featureContent}>
<Text style={styles.featureTitle}>AI-Powered Insights</Text>
<Text style={styles.featureDescription}>Smart analysis of health patterns</Text>
</View>
</View>
<View style={styles.featureItem}>
<View style={[styles.featureIcon, { backgroundColor: '#FEF3C7' }]}>
<Ionicons name="people" size={20} color="#F59E0B" />
</View>
<View style={styles.featureContent}>
<Text style={styles.featureTitle}>Family Coordination</Text>
<Text style={styles.featureDescription}>Share care with multiple caregivers</Text>
</View>
</View>
</View>
</View>
{/* Links */}
<View style={styles.section}>
<Text style={styles.sectionTitle}>Resources</Text>
<View style={styles.card}>
<LinkRow
icon="globe-outline"
title="Visit Website"
onPress={() => openURL('https://wellnuo.com')}
/>
<View style={styles.linkDivider} />
<LinkRow
icon="document-text-outline"
title="Documentation"
onPress={() => openURL('https://docs.wellnuo.com')}
/>
<View style={styles.linkDivider} />
<LinkRow
icon="logo-twitter"
title="Follow on Twitter"
onPress={() => openURL('https://twitter.com/wellnuo')}
/>
<View style={styles.linkDivider} />
<LinkRow
icon="logo-github"
title="View on GitHub"
onPress={() => openURL('https://github.com/wellnuo')}
/>
</View>
</View>
{/* Acknowledgments */}
<View style={styles.section}>
<Text style={styles.sectionTitle}>Acknowledgments</Text>
<View style={styles.card}>
<Text style={styles.acknowledgment}>
WellNuo uses the following open-source software:
</Text>
<Text style={styles.license}> React Native (MIT License)</Text>
<Text style={styles.license}> Expo (MIT License)</Text>
<Text style={styles.license}> React Navigation (MIT License)</Text>
<Text style={styles.license}> And many other wonderful packages</Text>
<TouchableOpacity
style={styles.viewLicenses}
onPress={() => openURL('https://wellnuo.com/licenses')}
>
<Text style={styles.viewLicensesText}>View All Licenses</Text>
</TouchableOpacity>
</View>
</View>
{/* Footer */}
<View style={styles.footer}>
<Text style={styles.copyright}>© 2024 WellNuo Inc.</Text>
<Text style={styles.footerText}>All rights reserved.</Text>
<Text style={styles.madeWith}>
Made with for families worldwide
</Text>
</View>
</ScrollView>
</SafeAreaView>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: AppColors.surface,
},
heroSection: {
alignItems: 'center',
paddingVertical: Spacing.xl,
backgroundColor: AppColors.background,
},
logoContainer: {
width: 100,
height: 100,
borderRadius: 24,
backgroundColor: AppColors.primary,
justifyContent: 'center',
alignItems: 'center',
marginBottom: Spacing.md,
},
appName: {
fontSize: 32,
fontWeight: '700',
color: AppColors.textPrimary,
},
appTagline: {
fontSize: FontSizes.base,
color: AppColors.textSecondary,
marginTop: Spacing.xs,
},
section: {
marginTop: Spacing.md,
},
sectionTitle: {
fontSize: FontSizes.sm,
fontWeight: '600',
color: AppColors.textSecondary,
paddingHorizontal: Spacing.lg,
paddingVertical: Spacing.sm,
textTransform: 'uppercase',
},
card: {
backgroundColor: AppColors.background,
paddingVertical: Spacing.sm,
},
infoRow: {
flexDirection: 'row',
justifyContent: 'space-between',
paddingVertical: Spacing.sm,
paddingHorizontal: Spacing.lg,
},
infoLabel: {
fontSize: FontSizes.base,
color: AppColors.textSecondary,
},
infoValue: {
fontSize: FontSizes.base,
fontWeight: '500',
color: AppColors.textPrimary,
},
infoDivider: {
height: 1,
backgroundColor: AppColors.border,
marginHorizontal: Spacing.lg,
},
description: {
fontSize: FontSizes.sm,
color: AppColors.textSecondary,
lineHeight: 22,
paddingHorizontal: Spacing.lg,
paddingVertical: Spacing.sm,
},
featureItem: {
flexDirection: 'row',
alignItems: 'center',
paddingVertical: Spacing.sm,
paddingHorizontal: Spacing.lg,
},
featureIcon: {
width: 40,
height: 40,
borderRadius: BorderRadius.md,
justifyContent: 'center',
alignItems: 'center',
},
featureContent: {
flex: 1,
marginLeft: Spacing.md,
},
featureTitle: {
fontSize: FontSizes.base,
fontWeight: '500',
color: AppColors.textPrimary,
},
featureDescription: {
fontSize: FontSizes.xs,
color: AppColors.textMuted,
marginTop: 2,
},
linkRow: {
flexDirection: 'row',
alignItems: 'center',
paddingVertical: Spacing.md,
paddingHorizontal: Spacing.lg,
},
linkText: {
flex: 1,
fontSize: FontSizes.base,
color: AppColors.primary,
marginLeft: Spacing.md,
},
linkDivider: {
height: 1,
backgroundColor: AppColors.border,
marginLeft: Spacing.lg + 20 + Spacing.md,
},
acknowledgment: {
fontSize: FontSizes.sm,
color: AppColors.textSecondary,
paddingHorizontal: Spacing.lg,
paddingVertical: Spacing.sm,
},
license: {
fontSize: FontSizes.xs,
color: AppColors.textMuted,
paddingHorizontal: Spacing.lg,
paddingVertical: 2,
},
viewLicenses: {
marginTop: Spacing.md,
paddingVertical: Spacing.sm,
alignItems: 'center',
},
viewLicensesText: {
fontSize: FontSizes.sm,
color: AppColors.primary,
fontWeight: '500',
},
footer: {
alignItems: 'center',
paddingVertical: Spacing.xl,
paddingBottom: Spacing.xxl,
},
copyright: {
fontSize: FontSizes.sm,
fontWeight: '500',
color: AppColors.textPrimary,
},
footerText: {
fontSize: FontSizes.xs,
color: AppColors.textMuted,
marginTop: 2,
},
madeWith: {
fontSize: FontSizes.xs,
color: AppColors.textSecondary,
marginTop: Spacing.md,
},
});

325
app/profile/edit.tsx Normal file
View File

@ -0,0 +1,325 @@
import React, { useState } from 'react';
import {
View,
Text,
StyleSheet,
ScrollView,
TextInput,
TouchableOpacity,
Alert,
KeyboardAvoidingView,
Platform,
} from 'react-native';
import { Ionicons } from '@expo/vector-icons';
import { SafeAreaView } from 'react-native-safe-area-context';
import { router } from 'expo-router';
import { useAuth } from '@/contexts/AuthContext';
import { AppColors, BorderRadius, FontSizes, Spacing } from '@/constants/theme';
export default function EditProfileScreen() {
const { user } = useAuth();
const [displayName, setDisplayName] = useState(user?.user_name || '');
const [email, setEmail] = useState('');
const [phone, setPhone] = useState('');
const [timezone, setTimezone] = useState('UTC');
const [isSaving, setIsSaving] = useState(false);
const handleSave = async () => {
if (!displayName.trim()) {
Alert.alert('Error', 'Display name is required');
return;
}
setIsSaving(true);
// Simulate API call
await new Promise(resolve => setTimeout(resolve, 1000));
setIsSaving(false);
Alert.alert(
'Profile Updated',
'Your profile has been updated successfully.',
[{ text: 'OK', onPress: () => router.back() }]
);
};
const handleChangePhoto = () => {
Alert.alert(
'Change Photo',
'Choose an option',
[
{ text: 'Take Photo', onPress: () => Alert.alert('Camera', 'Camera functionality coming soon!') },
{ text: 'Choose from Library', onPress: () => Alert.alert('Library', 'Photo library coming soon!') },
{ text: 'Remove Photo', style: 'destructive', onPress: () => Alert.alert('Removed', 'Photo removed') },
{ text: 'Cancel', style: 'cancel' },
]
);
};
return (
<SafeAreaView style={styles.container} edges={['bottom']}>
<KeyboardAvoidingView
style={styles.keyboardView}
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
>
<ScrollView showsVerticalScrollIndicator={false}>
{/* Avatar Section */}
<View style={styles.avatarSection}>
<View style={styles.avatarContainer}>
<Text style={styles.avatarText}>
{displayName?.charAt(0).toUpperCase() || 'U'}
</Text>
</View>
<TouchableOpacity style={styles.changePhotoButton} onPress={handleChangePhoto}>
<Ionicons name="camera" size={16} color={AppColors.white} />
</TouchableOpacity>
<Text style={styles.changePhotoText}>Tap to change photo</Text>
</View>
{/* Form */}
<View style={styles.form}>
<View style={styles.inputGroup}>
<Text style={styles.label}>Display Name *</Text>
<TextInput
style={styles.input}
value={displayName}
onChangeText={setDisplayName}
placeholder="Enter your display name"
placeholderTextColor={AppColors.textMuted}
/>
</View>
<View style={styles.inputGroup}>
<Text style={styles.label}>Email Address</Text>
<TextInput
style={styles.input}
value={email}
onChangeText={setEmail}
placeholder="Enter your email"
placeholderTextColor={AppColors.textMuted}
keyboardType="email-address"
autoCapitalize="none"
/>
<Text style={styles.hint}>Used for notifications and account recovery</Text>
</View>
<View style={styles.inputGroup}>
<Text style={styles.label}>Phone Number</Text>
<TextInput
style={styles.input}
value={phone}
onChangeText={setPhone}
placeholder="+1 (555) 123-4567"
placeholderTextColor={AppColors.textMuted}
keyboardType="phone-pad"
/>
<Text style={styles.hint}>For emergency contact and SMS alerts</Text>
</View>
<View style={styles.inputGroup}>
<Text style={styles.label}>Timezone</Text>
<TouchableOpacity
style={styles.selectInput}
onPress={() => {
Alert.alert(
'Select Timezone',
'Choose your timezone',
[
{ text: 'UTC', onPress: () => setTimezone('UTC') },
{ text: 'America/New_York', onPress: () => setTimezone('America/New_York') },
{ text: 'America/Los_Angeles', onPress: () => setTimezone('America/Los_Angeles') },
{ text: 'Europe/London', onPress: () => setTimezone('Europe/London') },
{ text: 'Cancel', style: 'cancel' },
]
);
}}
>
<Text style={styles.selectText}>{timezone}</Text>
<Ionicons name="chevron-down" size={20} color={AppColors.textMuted} />
</TouchableOpacity>
</View>
{/* Account Info (read-only) */}
<View style={styles.readOnlySection}>
<Text style={styles.readOnlyTitle}>Account Information</Text>
<View style={styles.readOnlyItem}>
<Text style={styles.readOnlyLabel}>User ID</Text>
<Text style={styles.readOnlyValue}>{user?.user_id || 'N/A'}</Text>
</View>
<View style={styles.readOnlyItem}>
<Text style={styles.readOnlyLabel}>Username</Text>
<Text style={styles.readOnlyValue}>{user?.user_name || 'N/A'}</Text>
</View>
<View style={styles.readOnlyItem}>
<Text style={styles.readOnlyLabel}>Role</Text>
<Text style={styles.readOnlyValue}>
{user?.max_role === 2 ? 'Administrator' : 'Caregiver'}
</Text>
</View>
<View style={styles.readOnlyItem}>
<Text style={styles.readOnlyLabel}>Assigned Beneficiaries</Text>
<Text style={styles.readOnlyValue}>
{user?.privileges?.split(',').length || 0}
</Text>
</View>
</View>
</View>
</ScrollView>
{/* Save Button */}
<View style={styles.footer}>
<TouchableOpacity
style={[styles.saveButton, isSaving && styles.saveButtonDisabled]}
onPress={handleSave}
disabled={isSaving}
>
<Text style={styles.saveButtonText}>
{isSaving ? 'Saving...' : 'Save Changes'}
</Text>
</TouchableOpacity>
</View>
</KeyboardAvoidingView>
</SafeAreaView>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: AppColors.surface,
},
keyboardView: {
flex: 1,
},
avatarSection: {
alignItems: 'center',
paddingVertical: Spacing.xl,
backgroundColor: AppColors.background,
},
avatarContainer: {
width: 100,
height: 100,
borderRadius: 50,
backgroundColor: AppColors.primary,
justifyContent: 'center',
alignItems: 'center',
},
avatarText: {
fontSize: 40,
fontWeight: '600',
color: AppColors.white,
},
changePhotoButton: {
position: 'absolute',
bottom: 50,
right: '35%',
width: 32,
height: 32,
borderRadius: 16,
backgroundColor: AppColors.primaryLight,
justifyContent: 'center',
alignItems: 'center',
borderWidth: 2,
borderColor: AppColors.background,
},
changePhotoText: {
marginTop: Spacing.sm,
fontSize: FontSizes.sm,
color: AppColors.primary,
},
form: {
padding: Spacing.lg,
},
inputGroup: {
marginBottom: Spacing.lg,
},
label: {
fontSize: FontSizes.sm,
fontWeight: '600',
color: AppColors.textPrimary,
marginBottom: Spacing.xs,
},
input: {
backgroundColor: AppColors.background,
borderRadius: BorderRadius.md,
paddingHorizontal: Spacing.md,
paddingVertical: Spacing.md,
fontSize: FontSizes.base,
color: AppColors.textPrimary,
borderWidth: 1,
borderColor: AppColors.border,
},
selectInput: {
backgroundColor: AppColors.background,
borderRadius: BorderRadius.md,
paddingHorizontal: Spacing.md,
paddingVertical: Spacing.md,
borderWidth: 1,
borderColor: AppColors.border,
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
},
selectText: {
fontSize: FontSizes.base,
color: AppColors.textPrimary,
},
hint: {
fontSize: FontSizes.xs,
color: AppColors.textMuted,
marginTop: Spacing.xs,
},
readOnlySection: {
marginTop: Spacing.lg,
backgroundColor: AppColors.background,
borderRadius: BorderRadius.lg,
padding: Spacing.md,
},
readOnlyTitle: {
fontSize: FontSizes.sm,
fontWeight: '600',
color: AppColors.textSecondary,
marginBottom: Spacing.md,
},
readOnlyItem: {
flexDirection: 'row',
justifyContent: 'space-between',
paddingVertical: Spacing.sm,
borderBottomWidth: 1,
borderBottomColor: AppColors.border,
},
readOnlyLabel: {
fontSize: FontSizes.sm,
color: AppColors.textSecondary,
},
readOnlyValue: {
fontSize: FontSizes.sm,
fontWeight: '500',
color: AppColors.textPrimary,
},
footer: {
padding: Spacing.lg,
backgroundColor: AppColors.background,
borderTopWidth: 1,
borderTopColor: AppColors.border,
},
saveButton: {
backgroundColor: AppColors.primary,
borderRadius: BorderRadius.lg,
paddingVertical: Spacing.md,
alignItems: 'center',
},
saveButtonDisabled: {
opacity: 0.6,
},
saveButtonText: {
fontSize: FontSizes.base,
fontWeight: '600',
color: AppColors.white,
},
});

419
app/profile/help.tsx Normal file
View File

@ -0,0 +1,419 @@
import React, { useState } from 'react';
import {
View,
Text,
StyleSheet,
ScrollView,
TouchableOpacity,
TextInput,
Linking,
} from 'react-native';
import { Ionicons } from '@expo/vector-icons';
import { SafeAreaView } from 'react-native-safe-area-context';
import { AppColors, BorderRadius, FontSizes, Spacing } from '@/constants/theme';
interface FAQItemProps {
question: string;
answer: string;
isExpanded: boolean;
onToggle: () => void;
}
function FAQItem({ question, answer, isExpanded, onToggle }: FAQItemProps) {
return (
<TouchableOpacity style={styles.faqItem} onPress={onToggle} activeOpacity={0.7}>
<View style={styles.faqHeader}>
<Text style={styles.faqQuestion}>{question}</Text>
<Ionicons
name={isExpanded ? 'chevron-up' : 'chevron-down'}
size={20}
color={AppColors.textMuted}
/>
</View>
{isExpanded && (
<Text style={styles.faqAnswer}>{answer}</Text>
)}
</TouchableOpacity>
);
}
interface GuideCategoryProps {
icon: keyof typeof Ionicons.glyphMap;
iconColor: string;
iconBgColor: string;
title: string;
description: string;
onPress: () => void;
}
function GuideCategory({
icon,
iconColor,
iconBgColor,
title,
description,
onPress,
}: GuideCategoryProps) {
return (
<TouchableOpacity style={styles.guideItem} onPress={onPress}>
<View style={[styles.guideIcon, { backgroundColor: iconBgColor }]}>
<Ionicons name={icon} size={24} color={iconColor} />
</View>
<Text style={styles.guideTitle}>{title}</Text>
<Text style={styles.guideDescription}>{description}</Text>
</TouchableOpacity>
);
}
export default function HelpScreen() {
const [searchQuery, setSearchQuery] = useState('');
const [expandedFAQ, setExpandedFAQ] = useState<number | null>(null);
const faqs = [
{
question: 'How do I add a new beneficiary?',
answer: 'Beneficiaries are assigned by your organization administrator. Contact your admin or support team to request access to additional beneficiaries. Once assigned, they will automatically appear in your dashboard.',
},
{
question: 'What does the wellness score mean?',
answer: 'The wellness score (0-100%) reflects the overall health pattern of your beneficiary based on daily activities, sleep patterns, movement data, and vital signs. A score above 70% indicates healthy patterns, 40-70% suggests some concerns, and below 40% may require attention.',
},
{
question: 'How often is the data updated?',
answer: 'Sensor data is collected in real-time and synced every few minutes. Dashboard summaries are calculated daily. Emergency alerts are instant and will notify you immediately.',
},
{
question: 'What triggers an emergency alert?',
answer: 'Emergency alerts are triggered by: falls detected by motion sensors, prolonged inactivity exceeding normal patterns, SOS button press by the beneficiary, and abnormal vital sign readings (if health monitors are connected).',
},
{
question: 'Can I share access with family members?',
answer: 'Yes! Contact your administrator to add additional caregivers. Each caregiver will have their own account and can set their own notification preferences while sharing access to the same beneficiary data.',
},
{
question: 'How do I change notification sounds?',
answer: 'Go to Profile > Notifications to customize your alert preferences. You can set different sounds for emergency alerts vs regular notifications, and configure quiet hours for non-urgent alerts.',
},
{
question: 'Is my data secure?',
answer: 'Yes. WellNuo uses end-to-end encryption for all data transmission. Your data is stored in HIPAA-compliant servers, and we never share personal information with third parties. You can export or delete your data at any time.',
},
{
question: 'What devices are compatible?',
answer: 'WellNuo works with most motion sensors, door/window sensors, and smart home devices. Supported health monitors include select blood pressure cuffs, pulse oximeters, and weight scales. Check our device compatibility list for specifics.',
},
];
const filteredFAQs = faqs.filter(
faq =>
faq.question.toLowerCase().includes(searchQuery.toLowerCase()) ||
faq.answer.toLowerCase().includes(searchQuery.toLowerCase())
);
return (
<SafeAreaView style={styles.container} edges={['bottom']}>
<ScrollView showsVerticalScrollIndicator={false}>
{/* Search */}
<View style={styles.searchSection}>
<View style={styles.searchContainer}>
<Ionicons name="search" size={20} color={AppColors.textMuted} />
<TextInput
style={styles.searchInput}
placeholder="Search for help..."
placeholderTextColor={AppColors.textMuted}
value={searchQuery}
onChangeText={setSearchQuery}
/>
{searchQuery.length > 0 && (
<TouchableOpacity onPress={() => setSearchQuery('')}>
<Ionicons name="close-circle" size={20} color={AppColors.textMuted} />
</TouchableOpacity>
)}
</View>
</View>
{/* Quick Guides */}
<View style={styles.section}>
<Text style={styles.sectionTitle}>Quick Guides</Text>
<View style={styles.guidesGrid}>
<GuideCategory
icon="rocket"
iconColor="#3B82F6"
iconBgColor="#DBEAFE"
title="Getting Started"
description="Learn the basics"
onPress={() => Linking.openURL('https://wellnuo.com/guides/getting-started')}
/>
<GuideCategory
icon="people"
iconColor="#10B981"
iconBgColor="#D1FAE5"
title="Managing Care"
description="Beneficiary tips"
onPress={() => Linking.openURL('https://wellnuo.com/guides/managing-care')}
/>
<GuideCategory
icon="notifications"
iconColor="#F59E0B"
iconBgColor="#FEF3C7"
title="Alerts & Notifications"
description="Stay informed"
onPress={() => Linking.openURL('https://wellnuo.com/guides/alerts')}
/>
<GuideCategory
icon="hardware-chip"
iconColor="#8B5CF6"
iconBgColor="#EDE9FE"
title="Device Setup"
description="Connect sensors"
onPress={() => Linking.openURL('https://wellnuo.com/guides/devices')}
/>
</View>
</View>
{/* FAQs */}
<View style={styles.section}>
<Text style={styles.sectionTitle}>Frequently Asked Questions</Text>
<View style={styles.faqContainer}>
{filteredFAQs.length > 0 ? (
filteredFAQs.map((faq, index) => (
<View key={index}>
<FAQItem
question={faq.question}
answer={faq.answer}
isExpanded={expandedFAQ === index}
onToggle={() => setExpandedFAQ(expandedFAQ === index ? null : index)}
/>
{index < filteredFAQs.length - 1 && <View style={styles.faqDivider} />}
</View>
))
) : (
<View style={styles.noResults}>
<Ionicons name="search-outline" size={48} color={AppColors.textMuted} />
<Text style={styles.noResultsText}>No results found</Text>
<Text style={styles.noResultsHint}>Try different keywords</Text>
</View>
)}
</View>
</View>
{/* Video Tutorials */}
<View style={styles.section}>
<Text style={styles.sectionTitle}>Video Tutorials</Text>
<ScrollView horizontal showsHorizontalScrollIndicator={false} style={styles.videosScroll}>
{[
{ title: 'App Overview', duration: '3:45' },
{ title: 'Setting Up Alerts', duration: '2:30' },
{ title: 'Understanding Data', duration: '4:15' },
{ title: 'Troubleshooting', duration: '5:00' },
].map((video, index) => (
<TouchableOpacity key={index} style={styles.videoCard}>
<View style={styles.videoThumbnail}>
<Ionicons name="play-circle" size={40} color={AppColors.white} />
</View>
<Text style={styles.videoTitle}>{video.title}</Text>
<Text style={styles.videoDuration}>{video.duration}</Text>
</TouchableOpacity>
))}
</ScrollView>
</View>
{/* Still Need Help */}
<View style={styles.section}>
<View style={styles.needHelpCard}>
<View style={styles.needHelpIcon}>
<Ionicons name="chatbubbles" size={32} color={AppColors.primary} />
</View>
<Text style={styles.needHelpTitle}>Still need help?</Text>
<Text style={styles.needHelpText}>
Our support team is available 24/7 to assist you with any questions.
</Text>
<TouchableOpacity
style={styles.contactButton}
onPress={() => Linking.openURL('mailto:support@wellnuo.com')}
>
<Text style={styles.contactButtonText}>Contact Support</Text>
</TouchableOpacity>
</View>
</View>
</ScrollView>
</SafeAreaView>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: AppColors.surface,
},
searchSection: {
padding: Spacing.lg,
backgroundColor: AppColors.background,
},
searchContainer: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: AppColors.surface,
borderRadius: BorderRadius.lg,
paddingHorizontal: Spacing.md,
paddingVertical: Spacing.sm,
},
searchInput: {
flex: 1,
marginLeft: Spacing.sm,
fontSize: FontSizes.base,
color: AppColors.textPrimary,
},
section: {
marginTop: Spacing.md,
},
sectionTitle: {
fontSize: FontSizes.sm,
fontWeight: '600',
color: AppColors.textSecondary,
paddingHorizontal: Spacing.lg,
paddingVertical: Spacing.sm,
textTransform: 'uppercase',
},
guidesGrid: {
flexDirection: 'row',
flexWrap: 'wrap',
padding: Spacing.sm,
backgroundColor: AppColors.background,
},
guideItem: {
width: '50%',
padding: Spacing.md,
alignItems: 'center',
},
guideIcon: {
width: 56,
height: 56,
borderRadius: BorderRadius.lg,
justifyContent: 'center',
alignItems: 'center',
marginBottom: Spacing.sm,
},
guideTitle: {
fontSize: FontSizes.sm,
fontWeight: '600',
color: AppColors.textPrimary,
textAlign: 'center',
},
guideDescription: {
fontSize: FontSizes.xs,
color: AppColors.textMuted,
textAlign: 'center',
marginTop: 2,
},
faqContainer: {
backgroundColor: AppColors.background,
},
faqItem: {
paddingVertical: Spacing.md,
paddingHorizontal: Spacing.lg,
},
faqHeader: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
},
faqQuestion: {
flex: 1,
fontSize: FontSizes.base,
fontWeight: '500',
color: AppColors.textPrimary,
marginRight: Spacing.sm,
},
faqAnswer: {
fontSize: FontSizes.sm,
color: AppColors.textSecondary,
marginTop: Spacing.sm,
lineHeight: 20,
},
faqDivider: {
height: 1,
backgroundColor: AppColors.border,
marginHorizontal: Spacing.lg,
},
noResults: {
alignItems: 'center',
paddingVertical: Spacing.xl,
},
noResultsText: {
fontSize: FontSizes.base,
fontWeight: '500',
color: AppColors.textPrimary,
marginTop: Spacing.md,
},
noResultsHint: {
fontSize: FontSizes.sm,
color: AppColors.textMuted,
marginTop: Spacing.xs,
},
videosScroll: {
backgroundColor: AppColors.background,
paddingVertical: Spacing.md,
},
videoCard: {
width: 160,
marginLeft: Spacing.md,
},
videoThumbnail: {
height: 90,
backgroundColor: AppColors.primary,
borderRadius: BorderRadius.md,
justifyContent: 'center',
alignItems: 'center',
},
videoTitle: {
fontSize: FontSizes.sm,
fontWeight: '500',
color: AppColors.textPrimary,
marginTop: Spacing.sm,
},
videoDuration: {
fontSize: FontSizes.xs,
color: AppColors.textMuted,
marginTop: 2,
},
needHelpCard: {
backgroundColor: AppColors.background,
margin: Spacing.lg,
borderRadius: BorderRadius.lg,
padding: Spacing.xl,
alignItems: 'center',
},
needHelpIcon: {
width: 64,
height: 64,
borderRadius: 32,
backgroundColor: '#DBEAFE',
justifyContent: 'center',
alignItems: 'center',
marginBottom: Spacing.md,
},
needHelpTitle: {
fontSize: FontSizes.lg,
fontWeight: '600',
color: AppColors.textPrimary,
},
needHelpText: {
fontSize: FontSizes.sm,
color: AppColors.textSecondary,
textAlign: 'center',
marginTop: Spacing.sm,
marginBottom: Spacing.lg,
},
contactButton: {
backgroundColor: AppColors.primary,
borderRadius: BorderRadius.lg,
paddingVertical: Spacing.md,
paddingHorizontal: Spacing.xl,
},
contactButtonText: {
fontSize: FontSizes.base,
fontWeight: '600',
color: AppColors.white,
},
});

336
app/profile/language.tsx Normal file
View File

@ -0,0 +1,336 @@
import React, { useState } from 'react';
import {
View,
Text,
StyleSheet,
ScrollView,
TouchableOpacity,
Alert,
} from 'react-native';
import { Ionicons } from '@expo/vector-icons';
import { SafeAreaView } from 'react-native-safe-area-context';
import { router } from 'expo-router';
import { AppColors, BorderRadius, FontSizes, Spacing } from '@/constants/theme';
interface LanguageOptionProps {
code: string;
name: string;
nativeName: string;
flag: string;
isSelected: boolean;
isAvailable: boolean;
onSelect: () => void;
}
function LanguageOption({
code,
name,
nativeName,
flag,
isSelected,
isAvailable,
onSelect,
}: LanguageOptionProps) {
return (
<TouchableOpacity
style={[styles.languageOption, isSelected && styles.languageOptionSelected]}
onPress={onSelect}
disabled={!isAvailable}
>
<Text style={styles.flag}>{flag}</Text>
<View style={styles.languageInfo}>
<Text style={[styles.languageName, !isAvailable && styles.languageNameDisabled]}>
{name}
</Text>
<Text style={styles.languageNative}>{nativeName}</Text>
</View>
{!isAvailable ? (
<View style={styles.comingSoonBadge}>
<Text style={styles.comingSoonText}>Coming Soon</Text>
</View>
) : isSelected ? (
<Ionicons name="checkmark-circle" size={24} color={AppColors.primary} />
) : null}
</TouchableOpacity>
);
}
export default function LanguageScreen() {
const [selectedLanguage, setSelectedLanguage] = useState('en');
const languages = [
{ code: 'en', name: 'English', nativeName: 'English', flag: '🇺🇸', available: true },
{ code: 'es', name: 'Spanish', nativeName: 'Español', flag: '🇪🇸', available: false },
{ code: 'fr', name: 'French', nativeName: 'Français', flag: '🇫🇷', available: false },
{ code: 'de', name: 'German', nativeName: 'Deutsch', flag: '🇩🇪', available: false },
{ code: 'it', name: 'Italian', nativeName: 'Italiano', flag: '🇮🇹', available: false },
{ code: 'pt', name: 'Portuguese', nativeName: 'Português', flag: '🇵🇹', available: false },
{ code: 'nl', name: 'Dutch', nativeName: 'Nederlands', flag: '🇳🇱', available: false },
{ code: 'pl', name: 'Polish', nativeName: 'Polski', flag: '🇵🇱', available: false },
{ code: 'ru', name: 'Russian', nativeName: 'Русский', flag: '🇷🇺', available: false },
{ code: 'ja', name: 'Japanese', nativeName: '日本語', flag: '🇯🇵', available: false },
{ code: 'zh', name: 'Chinese', nativeName: '中文', flag: '🇨🇳', available: false },
{ code: 'ko', name: 'Korean', nativeName: '한국어', flag: '🇰🇷', available: false },
];
const handleSelectLanguage = (code: string, name: string, available: boolean) => {
if (!available) {
Alert.alert(
'Coming Soon',
`${name} translation is not available yet. We're working on adding more languages!`,
[{ text: 'OK' }]
);
return;
}
if (code === selectedLanguage) {
return;
}
Alert.alert(
'Change Language',
`Are you sure you want to change the app language to ${name}?`,
[
{ text: 'Cancel', style: 'cancel' },
{
text: 'Change',
onPress: () => {
setSelectedLanguage(code);
Alert.alert(
'Language Changed',
`The app language has been changed to ${name}. Some changes may require restarting the app.`,
[{ text: 'OK', onPress: () => router.back() }]
);
},
},
]
);
};
const availableLanguages = languages.filter(l => l.available);
const comingSoonLanguages = languages.filter(l => !l.available);
return (
<SafeAreaView style={styles.container} edges={['bottom']}>
<ScrollView showsVerticalScrollIndicator={false}>
{/* Current Language */}
<View style={styles.section}>
<View style={styles.currentLanguageCard}>
<Ionicons name="globe" size={24} color={AppColors.primary} />
<View style={styles.currentLanguageInfo}>
<Text style={styles.currentLanguageLabel}>Current Language</Text>
<Text style={styles.currentLanguageName}>
{languages.find(l => l.code === selectedLanguage)?.name || 'English'}
</Text>
</View>
</View>
</View>
{/* Available Languages */}
<View style={styles.section}>
<Text style={styles.sectionTitle}>Available Languages</Text>
<View style={styles.languagesCard}>
{availableLanguages.map((lang, index) => (
<View key={lang.code}>
<LanguageOption
code={lang.code}
name={lang.name}
nativeName={lang.nativeName}
flag={lang.flag}
isSelected={selectedLanguage === lang.code}
isAvailable={lang.available}
onSelect={() => handleSelectLanguage(lang.code, lang.name, lang.available)}
/>
{index < availableLanguages.length - 1 && <View style={styles.divider} />}
</View>
))}
</View>
</View>
{/* Coming Soon Languages */}
<View style={styles.section}>
<Text style={styles.sectionTitle}>Coming Soon</Text>
<View style={styles.languagesCard}>
{comingSoonLanguages.map((lang, index) => (
<View key={lang.code}>
<LanguageOption
code={lang.code}
name={lang.name}
nativeName={lang.nativeName}
flag={lang.flag}
isSelected={false}
isAvailable={lang.available}
onSelect={() => handleSelectLanguage(lang.code, lang.name, lang.available)}
/>
{index < comingSoonLanguages.length - 1 && <View style={styles.divider} />}
</View>
))}
</View>
</View>
{/* Help Translate */}
<View style={styles.section}>
<TouchableOpacity
style={styles.helpTranslateCard}
onPress={() => Alert.alert(
'Help Us Translate',
'We\'d love your help translating WellNuo into more languages!\n\n' +
'If you\'re fluent in another language and would like to contribute, ' +
'please contact us at translations@wellnuo.com',
[{ text: 'OK' }]
)}
>
<View style={styles.helpTranslateIcon}>
<Ionicons name="language" size={24} color={AppColors.primary} />
</View>
<View style={styles.helpTranslateContent}>
<Text style={styles.helpTranslateTitle}>Help Us Translate</Text>
<Text style={styles.helpTranslateDescription}>
Want to see WellNuo in your language? Help us by contributing translations.
</Text>
</View>
<Ionicons name="chevron-forward" size={20} color={AppColors.textMuted} />
</TouchableOpacity>
</View>
{/* Note */}
<View style={styles.noteContainer}>
<Ionicons name="information-circle" size={16} color={AppColors.textMuted} />
<Text style={styles.noteText}>
Changing the language will translate the app interface. Beneficiary data and
system notifications may remain in the original language.
</Text>
</View>
</ScrollView>
</SafeAreaView>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: AppColors.surface,
},
section: {
marginTop: Spacing.md,
},
sectionTitle: {
fontSize: FontSizes.sm,
fontWeight: '600',
color: AppColors.textSecondary,
paddingHorizontal: Spacing.lg,
paddingVertical: Spacing.sm,
textTransform: 'uppercase',
},
currentLanguageCard: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: AppColors.background,
marginHorizontal: Spacing.lg,
padding: Spacing.md,
borderRadius: BorderRadius.lg,
},
currentLanguageInfo: {
marginLeft: Spacing.md,
},
currentLanguageLabel: {
fontSize: FontSizes.xs,
color: AppColors.textMuted,
},
currentLanguageName: {
fontSize: FontSizes.lg,
fontWeight: '600',
color: AppColors.textPrimary,
marginTop: 2,
},
languagesCard: {
backgroundColor: AppColors.background,
},
languageOption: {
flexDirection: 'row',
alignItems: 'center',
paddingVertical: Spacing.md,
paddingHorizontal: Spacing.lg,
},
languageOptionSelected: {
backgroundColor: '#DBEAFE',
},
flag: {
fontSize: 28,
},
languageInfo: {
flex: 1,
marginLeft: Spacing.md,
},
languageName: {
fontSize: FontSizes.base,
fontWeight: '500',
color: AppColors.textPrimary,
},
languageNameDisabled: {
color: AppColors.textMuted,
},
languageNative: {
fontSize: FontSizes.xs,
color: AppColors.textSecondary,
marginTop: 2,
},
comingSoonBadge: {
backgroundColor: AppColors.surface,
paddingHorizontal: Spacing.sm,
paddingVertical: 4,
borderRadius: BorderRadius.sm,
},
comingSoonText: {
fontSize: FontSizes.xs,
color: AppColors.textMuted,
},
divider: {
height: 1,
backgroundColor: AppColors.border,
marginLeft: Spacing.lg + 28 + Spacing.md,
},
helpTranslateCard: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: AppColors.background,
marginHorizontal: Spacing.lg,
padding: Spacing.md,
borderRadius: BorderRadius.lg,
},
helpTranslateIcon: {
width: 48,
height: 48,
borderRadius: BorderRadius.md,
backgroundColor: '#DBEAFE',
justifyContent: 'center',
alignItems: 'center',
},
helpTranslateContent: {
flex: 1,
marginLeft: Spacing.md,
},
helpTranslateTitle: {
fontSize: FontSizes.base,
fontWeight: '500',
color: AppColors.textPrimary,
},
helpTranslateDescription: {
fontSize: FontSizes.xs,
color: AppColors.textMuted,
marginTop: 2,
},
noteContainer: {
flexDirection: 'row',
alignItems: 'flex-start',
marginHorizontal: Spacing.lg,
marginVertical: Spacing.lg,
},
noteText: {
flex: 1,
fontSize: FontSizes.xs,
color: AppColors.textMuted,
marginLeft: Spacing.sm,
lineHeight: 18,
},
});

View File

@ -0,0 +1,380 @@
import React, { useState } from 'react';
import {
View,
Text,
StyleSheet,
ScrollView,
Switch,
TouchableOpacity,
Alert,
} from 'react-native';
import { Ionicons } from '@expo/vector-icons';
import { SafeAreaView } from 'react-native-safe-area-context';
import { router } from 'expo-router';
import { AppColors, BorderRadius, FontSizes, Spacing } from '@/constants/theme';
interface NotificationSettingProps {
icon: keyof typeof Ionicons.glyphMap;
iconColor: string;
iconBgColor: string;
title: string;
description: string;
value: boolean;
onValueChange: (value: boolean) => void;
}
function NotificationSetting({
icon,
iconColor,
iconBgColor,
title,
description,
value,
onValueChange,
}: NotificationSettingProps) {
return (
<View style={styles.settingRow}>
<View style={[styles.iconContainer, { backgroundColor: iconBgColor }]}>
<Ionicons name={icon} size={20} color={iconColor} />
</View>
<View style={styles.settingContent}>
<Text style={styles.settingTitle}>{title}</Text>
<Text style={styles.settingDescription}>{description}</Text>
</View>
<Switch
value={value}
onValueChange={onValueChange}
trackColor={{ false: '#E5E7EB', true: AppColors.primaryLight }}
thumbColor={value ? AppColors.primary : '#9CA3AF'}
/>
</View>
);
}
export default function NotificationsScreen() {
// Alert types
const [emergencyAlerts, setEmergencyAlerts] = useState(true);
const [activityAlerts, setActivityAlerts] = useState(true);
const [lowBattery, setLowBattery] = useState(true);
const [dailySummary, setDailySummary] = useState(false);
const [weeklySummary, setWeeklySummary] = useState(true);
// Delivery methods
const [pushEnabled, setPushEnabled] = useState(true);
const [emailEnabled, setEmailEnabled] = useState(false);
const [smsEnabled, setSmsEnabled] = useState(false);
// Quiet hours
const [quietHours, setQuietHours] = useState(false);
const [quietStart, setQuietStart] = useState('22:00');
const [quietEnd, setQuietEnd] = useState('07:00');
const handleSave = () => {
Alert.alert(
'Settings Saved',
'Your notification preferences have been updated.',
[{ text: 'OK', onPress: () => router.back() }]
);
};
const handleQuietHoursConfig = () => {
Alert.alert(
'Quiet Hours',
`Current: ${quietStart} - ${quietEnd}\n\nDuring quiet hours, only emergency alerts will be delivered.`,
[
{ text: 'Cancel', style: 'cancel' },
{ text: 'Set Start Time', onPress: () => Alert.alert('Coming Soon', 'Time picker coming soon!') },
]
);
};
return (
<SafeAreaView style={styles.container} edges={['bottom']}>
<ScrollView showsVerticalScrollIndicator={false}>
{/* Alert Types */}
<View style={styles.section}>
<Text style={styles.sectionTitle}>Alert Types</Text>
<View style={styles.card}>
<NotificationSetting
icon="warning"
iconColor="#EF4444"
iconBgColor="#FEE2E2"
title="Emergency Alerts"
description="Falls, inactivity, SOS button"
value={emergencyAlerts}
onValueChange={setEmergencyAlerts}
/>
<View style={styles.divider} />
<NotificationSetting
icon="walk"
iconColor="#3B82F6"
iconBgColor="#DBEAFE"
title="Activity Alerts"
description="Unusual activity patterns"
value={activityAlerts}
onValueChange={setActivityAlerts}
/>
<View style={styles.divider} />
<NotificationSetting
icon="battery-half"
iconColor="#F59E0B"
iconBgColor="#FEF3C7"
title="Low Battery"
description="Device battery warnings"
value={lowBattery}
onValueChange={setLowBattery}
/>
<View style={styles.divider} />
<NotificationSetting
icon="today"
iconColor="#10B981"
iconBgColor="#D1FAE5"
title="Daily Summary"
description="Daily wellness report"
value={dailySummary}
onValueChange={setDailySummary}
/>
<View style={styles.divider} />
<NotificationSetting
icon="calendar"
iconColor="#8B5CF6"
iconBgColor="#EDE9FE"
title="Weekly Summary"
description="Weekly health digest"
value={weeklySummary}
onValueChange={setWeeklySummary}
/>
</View>
</View>
{/* Delivery Methods */}
<View style={styles.section}>
<Text style={styles.sectionTitle}>Delivery Methods</Text>
<View style={styles.card}>
<NotificationSetting
icon="notifications"
iconColor={AppColors.primary}
iconBgColor="#DBEAFE"
title="Push Notifications"
description="Alerts on your device"
value={pushEnabled}
onValueChange={setPushEnabled}
/>
<View style={styles.divider} />
<NotificationSetting
icon="mail"
iconColor="#6366F1"
iconBgColor="#E0E7FF"
title="Email Notifications"
description="Summaries to your inbox"
value={emailEnabled}
onValueChange={setEmailEnabled}
/>
<View style={styles.divider} />
<NotificationSetting
icon="chatbubble"
iconColor="#EC4899"
iconBgColor="#FCE7F3"
title="SMS Notifications"
description="Text message alerts"
value={smsEnabled}
onValueChange={(value) => {
if (value) {
Alert.alert(
'SMS Notifications',
'SMS notifications require a verified phone number. Would you like to add one?',
[
{ text: 'Cancel', style: 'cancel' },
{
text: 'Add Phone',
onPress: () => router.push('/profile/edit')
},
]
);
} else {
setSmsEnabled(false);
}
}}
/>
</View>
</View>
{/* Quiet Hours */}
<View style={styles.section}>
<Text style={styles.sectionTitle}>Quiet Hours</Text>
<View style={styles.card}>
<NotificationSetting
icon="moon"
iconColor="#6366F1"
iconBgColor="#E0E7FF"
title="Enable Quiet Hours"
description="Silence non-emergency alerts"
value={quietHours}
onValueChange={setQuietHours}
/>
{quietHours && (
<>
<View style={styles.divider} />
<TouchableOpacity style={styles.timeRow} onPress={handleQuietHoursConfig}>
<View style={styles.timeInfo}>
<Ionicons name="time-outline" size={20} color={AppColors.textSecondary} />
<Text style={styles.timeLabel}>Quiet Period</Text>
</View>
<View style={styles.timeValue}>
<Text style={styles.timeText}>{quietStart} - {quietEnd}</Text>
<Ionicons name="chevron-forward" size={20} color={AppColors.textMuted} />
</View>
</TouchableOpacity>
</>
)}
</View>
<Text style={styles.quietNote}>
Emergency alerts will always be delivered, even during quiet hours.
</Text>
</View>
{/* Test Notification */}
<View style={styles.section}>
<TouchableOpacity
style={styles.testButton}
onPress={() => {
Alert.alert(
'Test Notification Sent',
'A test push notification has been sent to your device.',
[{ text: 'OK' }]
);
}}
>
<Ionicons name="paper-plane-outline" size={20} color={AppColors.primary} />
<Text style={styles.testButtonText}>Send Test Notification</Text>
</TouchableOpacity>
</View>
</ScrollView>
{/* Save Button */}
<View style={styles.footer}>
<TouchableOpacity style={styles.saveButton} onPress={handleSave}>
<Text style={styles.saveButtonText}>Save Preferences</Text>
</TouchableOpacity>
</View>
</SafeAreaView>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: AppColors.surface,
},
section: {
marginTop: Spacing.md,
},
sectionTitle: {
fontSize: FontSizes.sm,
fontWeight: '600',
color: AppColors.textSecondary,
paddingHorizontal: Spacing.lg,
paddingVertical: Spacing.sm,
textTransform: 'uppercase',
},
card: {
backgroundColor: AppColors.background,
},
settingRow: {
flexDirection: 'row',
alignItems: 'center',
paddingVertical: Spacing.md,
paddingHorizontal: Spacing.lg,
},
iconContainer: {
width: 40,
height: 40,
borderRadius: BorderRadius.md,
justifyContent: 'center',
alignItems: 'center',
},
settingContent: {
flex: 1,
marginLeft: Spacing.md,
},
settingTitle: {
fontSize: FontSizes.base,
fontWeight: '500',
color: AppColors.textPrimary,
},
settingDescription: {
fontSize: FontSizes.xs,
color: AppColors.textMuted,
marginTop: 2,
},
divider: {
height: 1,
backgroundColor: AppColors.border,
marginLeft: Spacing.lg + 40 + Spacing.md,
},
timeRow: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingVertical: Spacing.md,
paddingHorizontal: Spacing.lg,
},
timeInfo: {
flexDirection: 'row',
alignItems: 'center',
},
timeLabel: {
fontSize: FontSizes.base,
color: AppColors.textPrimary,
marginLeft: Spacing.sm,
},
timeValue: {
flexDirection: 'row',
alignItems: 'center',
},
timeText: {
fontSize: FontSizes.sm,
color: AppColors.textSecondary,
marginRight: Spacing.xs,
},
quietNote: {
fontSize: FontSizes.xs,
color: AppColors.textMuted,
paddingHorizontal: Spacing.lg,
paddingTop: Spacing.sm,
},
testButton: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
backgroundColor: AppColors.background,
paddingVertical: Spacing.md,
marginHorizontal: Spacing.lg,
borderRadius: BorderRadius.lg,
borderWidth: 1,
borderColor: AppColors.primary,
},
testButtonText: {
fontSize: FontSizes.base,
fontWeight: '500',
color: AppColors.primary,
marginLeft: Spacing.sm,
},
footer: {
padding: Spacing.lg,
backgroundColor: AppColors.background,
borderTopWidth: 1,
borderTopColor: AppColors.border,
},
saveButton: {
backgroundColor: AppColors.primary,
borderRadius: BorderRadius.lg,
paddingVertical: Spacing.md,
alignItems: 'center',
},
saveButtonText: {
fontSize: FontSizes.base,
fontWeight: '600',
color: AppColors.white,
},
});

View File

@ -0,0 +1,331 @@
import React from 'react';
import {
View,
Text,
StyleSheet,
ScrollView,
} from 'react-native';
import { Ionicons } from '@expo/vector-icons';
import { SafeAreaView } from 'react-native-safe-area-context';
import { AppColors, BorderRadius, FontSizes, Spacing } from '@/constants/theme';
interface PrivacyHighlightProps {
icon: keyof typeof Ionicons.glyphMap;
iconColor: string;
iconBgColor: string;
title: string;
description: string;
}
function PrivacyHighlight({
icon,
iconColor,
iconBgColor,
title,
description,
}: PrivacyHighlightProps) {
return (
<View style={styles.highlight}>
<View style={[styles.highlightIcon, { backgroundColor: iconBgColor }]}>
<Ionicons name={icon} size={20} color={iconColor} />
</View>
<View style={styles.highlightContent}>
<Text style={styles.highlightTitle}>{title}</Text>
<Text style={styles.highlightDescription}>{description}</Text>
</View>
</View>
);
}
export default function PrivacyPolicyScreen() {
return (
<SafeAreaView style={styles.container} edges={['bottom']}>
<ScrollView showsVerticalScrollIndicator={false}>
{/* Highlights */}
<View style={styles.highlightsSection}>
<Text style={styles.highlightsSectionTitle}>Our Privacy Commitment</Text>
<View style={styles.highlightsCard}>
<PrivacyHighlight
icon="lock-closed"
iconColor="#10B981"
iconBgColor="#D1FAE5"
title="End-to-End Encryption"
description="All data is encrypted in transit and at rest"
/>
<View style={styles.highlightDivider} />
<PrivacyHighlight
icon="shield-checkmark"
iconColor="#3B82F6"
iconBgColor="#DBEAFE"
title="HIPAA Compliant"
description="Healthcare-grade data protection standards"
/>
<View style={styles.highlightDivider} />
<PrivacyHighlight
icon="close-circle"
iconColor="#EF4444"
iconBgColor="#FEE2E2"
title="No Data Sales"
description="We never sell your personal information"
/>
<View style={styles.highlightDivider} />
<PrivacyHighlight
icon="trash"
iconColor="#8B5CF6"
iconBgColor="#EDE9FE"
title="Right to Deletion"
description="You can delete your data at any time"
/>
</View>
</View>
{/* Full Policy */}
<View style={styles.content}>
<Text style={styles.lastUpdated}>Last Updated: December 2024</Text>
<Text style={styles.sectionTitle}>1. Information We Collect</Text>
<Text style={styles.paragraph}>
WellNuo collects information to provide and improve our elderly care monitoring
service. We collect:
</Text>
<Text style={styles.subSectionTitle}>Account Information</Text>
<Text style={styles.bulletPoint}> Name and contact information</Text>
<Text style={styles.bulletPoint}> Login credentials (encrypted)</Text>
<Text style={styles.bulletPoint}> Payment information (processed by secure third parties)</Text>
<Text style={styles.subSectionTitle}>Beneficiary Data</Text>
<Text style={styles.bulletPoint}> Activity and motion sensor data</Text>
<Text style={styles.bulletPoint}> Location information (if enabled)</Text>
<Text style={styles.bulletPoint}> Health metrics from connected devices</Text>
<Text style={styles.bulletPoint}> Daily routines and patterns</Text>
<Text style={styles.subSectionTitle}>Usage Data</Text>
<Text style={styles.bulletPoint}> App usage statistics</Text>
<Text style={styles.bulletPoint}> Device information</Text>
<Text style={styles.bulletPoint}> Error logs and performance data</Text>
<Text style={styles.sectionTitle}>2. How We Use Your Information</Text>
<Text style={styles.paragraph}>
We use collected information to:
</Text>
<Text style={styles.bulletPoint}> Provide real-time monitoring and alerts</Text>
<Text style={styles.bulletPoint}> Generate wellness scores and insights</Text>
<Text style={styles.bulletPoint}> Improve our AI algorithms (anonymized data only)</Text>
<Text style={styles.bulletPoint}> Send notifications and communications</Text>
<Text style={styles.bulletPoint}> Prevent fraud and ensure security</Text>
<Text style={styles.bulletPoint}> Comply with legal obligations</Text>
<Text style={styles.sectionTitle}>3. Data Sharing</Text>
<Text style={styles.paragraph}>
We do NOT sell your personal information. We may share data with:
</Text>
<Text style={styles.bulletPoint}> Authorized caregivers (your permission required)</Text>
<Text style={styles.bulletPoint}> Healthcare providers (with explicit consent)</Text>
<Text style={styles.bulletPoint}> Emergency services (in emergency situations)</Text>
<Text style={styles.bulletPoint}> Service providers (under strict contracts)</Text>
<Text style={styles.bulletPoint}> Legal authorities (when required by law)</Text>
<Text style={styles.sectionTitle}>4. Data Security</Text>
<Text style={styles.paragraph}>
We implement industry-leading security measures:
</Text>
<Text style={styles.bulletPoint}> AES-256 encryption for data at rest</Text>
<Text style={styles.bulletPoint}> TLS 1.3 encryption for data in transit</Text>
<Text style={styles.bulletPoint}> Regular security audits and penetration testing</Text>
<Text style={styles.bulletPoint}> Multi-factor authentication options</Text>
<Text style={styles.bulletPoint}> Automatic security patching</Text>
<Text style={styles.bulletPoint}> SOC 2 Type II certified infrastructure</Text>
<Text style={styles.sectionTitle}>5. Data Retention</Text>
<Text style={styles.paragraph}>
We retain your data for:
</Text>
<Text style={styles.bulletPoint}> Active accounts: Duration of service + 2 years</Text>
<Text style={styles.bulletPoint}> Deleted accounts: 30 days (recovery period)</Text>
<Text style={styles.bulletPoint}> Legal requirements: As required by law</Text>
<Text style={styles.bulletPoint}> Anonymized data: May be retained indefinitely for research</Text>
<Text style={styles.sectionTitle}>6. Your Rights</Text>
<Text style={styles.paragraph}>
Under GDPR, CCPA, and other privacy laws, you have the right to:
</Text>
<Text style={styles.bulletPoint}> Access your personal data</Text>
<Text style={styles.bulletPoint}> Correct inaccurate data</Text>
<Text style={styles.bulletPoint}> Delete your data ("right to be forgotten")</Text>
<Text style={styles.bulletPoint}> Export your data (data portability)</Text>
<Text style={styles.bulletPoint}> Opt out of marketing communications</Text>
<Text style={styles.bulletPoint}> Restrict processing of your data</Text>
<Text style={styles.sectionTitle}>7. Children's Privacy</Text>
<Text style={styles.paragraph}>
WellNuo is designed for adult caregivers monitoring elderly individuals. We do
not knowingly collect information from children under 13. If we discover such
data has been collected, we will delete it immediately.
</Text>
<Text style={styles.sectionTitle}>8. International Data Transfers</Text>
<Text style={styles.paragraph}>
Data may be transferred to and processed in countries outside your residence.
We ensure adequate safeguards through:
</Text>
<Text style={styles.bulletPoint}> Standard Contractual Clauses (SCCs)</Text>
<Text style={styles.bulletPoint}> Privacy Shield certification (where applicable)</Text>
<Text style={styles.bulletPoint}> Data Processing Agreements with vendors</Text>
<Text style={styles.sectionTitle}>9. Cookies and Tracking</Text>
<Text style={styles.paragraph}>
Our mobile app uses minimal tracking:
</Text>
<Text style={styles.bulletPoint}> Essential cookies for authentication</Text>
<Text style={styles.bulletPoint}> Analytics (anonymized, opt-out available)</Text>
<Text style={styles.bulletPoint}> No third-party advertising trackers</Text>
<Text style={styles.sectionTitle}>10. Changes to This Policy</Text>
<Text style={styles.paragraph}>
We may update this Privacy Policy periodically. We will notify you of material
changes via email or in-app notification at least 30 days before they take effect.
</Text>
<Text style={styles.sectionTitle}>11. Contact Us</Text>
<Text style={styles.paragraph}>
For privacy-related questions or to exercise your rights:
</Text>
<View style={styles.contactCard}>
<Text style={styles.contactTitle}>Privacy Officer</Text>
<Text style={styles.contactInfo}>Email: privacy@wellnuo.com</Text>
<Text style={styles.contactInfo}>Address: 123 Care Street, San Francisco, CA 94102</Text>
<Text style={styles.contactInfo}>Phone: +1 (555) 123-4567</Text>
</View>
<View style={styles.footer}>
<Text style={styles.footerText}>
Your privacy matters to us. If you have any concerns about how we handle your
data, please don't hesitate to contact us.
</Text>
</View>
</View>
</ScrollView>
</SafeAreaView>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: AppColors.surface,
},
highlightsSection: {
backgroundColor: AppColors.background,
paddingVertical: Spacing.lg,
},
highlightsSectionTitle: {
fontSize: FontSizes.lg,
fontWeight: '600',
color: AppColors.textPrimary,
textAlign: 'center',
marginBottom: Spacing.md,
},
highlightsCard: {
marginHorizontal: Spacing.lg,
},
highlight: {
flexDirection: 'row',
alignItems: 'center',
paddingVertical: Spacing.sm,
},
highlightIcon: {
width: 40,
height: 40,
borderRadius: BorderRadius.md,
justifyContent: 'center',
alignItems: 'center',
},
highlightContent: {
flex: 1,
marginLeft: Spacing.md,
},
highlightTitle: {
fontSize: FontSizes.base,
fontWeight: '500',
color: AppColors.textPrimary,
},
highlightDescription: {
fontSize: FontSizes.xs,
color: AppColors.textMuted,
marginTop: 2,
},
highlightDivider: {
height: 1,
backgroundColor: AppColors.border,
marginLeft: 40 + Spacing.md,
marginVertical: Spacing.xs,
},
content: {
padding: Spacing.lg,
backgroundColor: AppColors.background,
marginTop: Spacing.md,
},
lastUpdated: {
fontSize: FontSizes.sm,
color: AppColors.textMuted,
marginBottom: Spacing.lg,
textAlign: 'center',
},
sectionTitle: {
fontSize: FontSizes.lg,
fontWeight: '600',
color: AppColors.textPrimary,
marginTop: Spacing.lg,
marginBottom: Spacing.sm,
},
subSectionTitle: {
fontSize: FontSizes.base,
fontWeight: '500',
color: AppColors.textPrimary,
marginTop: Spacing.md,
marginBottom: Spacing.xs,
},
paragraph: {
fontSize: FontSizes.sm,
color: AppColors.textSecondary,
lineHeight: 22,
marginBottom: Spacing.sm,
},
bulletPoint: {
fontSize: FontSizes.sm,
color: AppColors.textSecondary,
lineHeight: 22,
marginLeft: Spacing.md,
marginBottom: 4,
},
contactCard: {
backgroundColor: AppColors.surface,
borderRadius: BorderRadius.lg,
padding: Spacing.md,
marginTop: Spacing.sm,
},
contactTitle: {
fontSize: FontSizes.base,
fontWeight: '600',
color: AppColors.textPrimary,
marginBottom: Spacing.sm,
},
contactInfo: {
fontSize: FontSizes.sm,
color: AppColors.textSecondary,
marginBottom: 4,
},
footer: {
marginTop: Spacing.xl,
paddingTop: Spacing.lg,
borderTopWidth: 1,
borderTopColor: AppColors.border,
},
footerText: {
fontSize: FontSizes.xs,
color: AppColors.textMuted,
textAlign: 'center',
fontStyle: 'italic',
},
});

560
app/profile/privacy.tsx Normal file
View File

@ -0,0 +1,560 @@
import React, { useState } from 'react';
import {
View,
Text,
StyleSheet,
ScrollView,
Switch,
TouchableOpacity,
Alert,
TextInput,
Modal,
} from 'react-native';
import { Ionicons } from '@expo/vector-icons';
import { SafeAreaView } from 'react-native-safe-area-context';
import { router } from 'expo-router';
import { useAuth } from '@/contexts/AuthContext';
import { AppColors, BorderRadius, FontSizes, Spacing } from '@/constants/theme';
interface SecurityItemProps {
icon: keyof typeof Ionicons.glyphMap;
iconColor: string;
iconBgColor: string;
title: string;
description: string;
onPress: () => void;
rightElement?: React.ReactNode;
}
function SecurityItem({
icon,
iconColor,
iconBgColor,
title,
description,
onPress,
rightElement,
}: SecurityItemProps) {
return (
<TouchableOpacity style={styles.securityRow} onPress={onPress}>
<View style={[styles.iconContainer, { backgroundColor: iconBgColor }]}>
<Ionicons name={icon} size={20} color={iconColor} />
</View>
<View style={styles.securityContent}>
<Text style={styles.securityTitle}>{title}</Text>
<Text style={styles.securityDescription}>{description}</Text>
</View>
{rightElement || <Ionicons name="chevron-forward" size={20} color={AppColors.textMuted} />}
</TouchableOpacity>
);
}
export default function PrivacyScreen() {
const { logout } = useAuth();
const [twoFactor, setTwoFactor] = useState(false);
const [biometric, setBiometric] = useState(false);
const [showPasswordModal, setShowPasswordModal] = useState(false);
const [currentPassword, setCurrentPassword] = useState('');
const [newPassword, setNewPassword] = useState('');
const [confirmPassword, setConfirmPassword] = useState('');
const handleChangePassword = () => {
setShowPasswordModal(true);
};
const handlePasswordSubmit = () => {
if (!currentPassword || !newPassword || !confirmPassword) {
Alert.alert('Error', 'Please fill in all fields');
return;
}
if (newPassword !== confirmPassword) {
Alert.alert('Error', 'New passwords do not match');
return;
}
if (newPassword.length < 8) {
Alert.alert('Error', 'Password must be at least 8 characters');
return;
}
setShowPasswordModal(false);
setCurrentPassword('');
setNewPassword('');
setConfirmPassword('');
Alert.alert('Success', 'Your password has been changed successfully.');
};
const handleEnable2FA = (value: boolean) => {
if (value) {
Alert.alert(
'Enable Two-Factor Authentication',
'This will add an extra layer of security to your account. You will need to enter a code from your authenticator app when signing in.',
[
{ text: 'Cancel', style: 'cancel' },
{
text: 'Enable',
onPress: () => {
setTwoFactor(true);
Alert.alert(
'Scan QR Code',
'Open your authenticator app (Google Authenticator, Authy, etc.) and scan the QR code.',
[{ text: 'Done' }]
);
}
},
]
);
} else {
Alert.alert(
'Disable Two-Factor Authentication',
'Are you sure? This will make your account less secure.',
[
{ text: 'Cancel', style: 'cancel' },
{
text: 'Disable',
style: 'destructive',
onPress: () => setTwoFactor(false)
},
]
);
}
};
const handleManageSessions = () => {
Alert.alert(
'Active Sessions',
'You are currently signed in on:\n\n' +
'• iPhone 14 Pro (This device)\n Last active: Just now\n\n' +
'• Chrome on MacBook Pro\n Last active: 2 hours ago\n\n' +
'• Safari on iPad\n Last active: 3 days ago',
[
{ text: 'Close' },
{
text: 'Sign Out All',
style: 'destructive',
onPress: () => {
Alert.alert(
'Sign Out All Devices',
'This will sign you out of all devices including this one.',
[
{ text: 'Cancel', style: 'cancel' },
{
text: 'Sign Out All',
style: 'destructive',
onPress: async () => {
await logout();
router.replace('/(auth)/login');
}
},
]
);
}
},
]
);
};
const handleExportData = () => {
Alert.alert(
'Export Your Data',
'We will prepare a downloadable file containing all your data. This may take a few minutes.\n\nYou will receive an email when your data is ready.',
[
{ text: 'Cancel', style: 'cancel' },
{
text: 'Request Export',
onPress: () => Alert.alert('Request Sent', 'You will receive an email when your data export is ready.')
},
]
);
};
const handleDeleteAccount = () => {
Alert.alert(
'Delete Account',
'Are you absolutely sure you want to delete your account?\n\n' +
'• All your data will be permanently deleted\n' +
'• You will lose access to all beneficiary data\n' +
'• This action cannot be undone',
[
{ text: 'Cancel', style: 'cancel' },
{
text: 'Delete Account',
style: 'destructive',
onPress: () => {
Alert.alert(
'Final Confirmation',
'Type "DELETE" to confirm account deletion.',
[{ text: 'Cancel', style: 'cancel' }]
);
}
},
]
);
};
return (
<SafeAreaView style={styles.container} edges={['bottom']}>
<ScrollView showsVerticalScrollIndicator={false}>
{/* Password & Authentication */}
<View style={styles.section}>
<Text style={styles.sectionTitle}>Authentication</Text>
<View style={styles.card}>
<SecurityItem
icon="key"
iconColor="#3B82F6"
iconBgColor="#DBEAFE"
title="Change Password"
description="Update your account password"
onPress={handleChangePassword}
/>
<View style={styles.divider} />
<SecurityItem
icon="shield-checkmark"
iconColor="#10B981"
iconBgColor="#D1FAE5"
title="Two-Factor Authentication"
description={twoFactor ? 'Enabled' : 'Not enabled'}
onPress={() => handleEnable2FA(!twoFactor)}
rightElement={
<Switch
value={twoFactor}
onValueChange={handleEnable2FA}
trackColor={{ false: '#E5E7EB', true: AppColors.primaryLight }}
thumbColor={twoFactor ? AppColors.primary : '#9CA3AF'}
/>
}
/>
<View style={styles.divider} />
<SecurityItem
icon="finger-print"
iconColor="#8B5CF6"
iconBgColor="#EDE9FE"
title="Biometric Login"
description="Face ID / Touch ID"
onPress={() => setBiometric(!biometric)}
rightElement={
<Switch
value={biometric}
onValueChange={setBiometric}
trackColor={{ false: '#E5E7EB', true: AppColors.primaryLight }}
thumbColor={biometric ? AppColors.primary : '#9CA3AF'}
/>
}
/>
</View>
</View>
{/* Session Management */}
<View style={styles.section}>
<Text style={styles.sectionTitle}>Sessions</Text>
<View style={styles.card}>
<SecurityItem
icon="phone-portrait"
iconColor="#F59E0B"
iconBgColor="#FEF3C7"
title="Manage Sessions"
description="View and manage active devices"
onPress={handleManageSessions}
/>
</View>
</View>
{/* Data & Privacy */}
<View style={styles.section}>
<Text style={styles.sectionTitle}>Data & Privacy</Text>
<View style={styles.card}>
<SecurityItem
icon="download"
iconColor="#6366F1"
iconBgColor="#E0E7FF"
title="Export Your Data"
description="Download a copy of your data"
onPress={handleExportData}
/>
<View style={styles.divider} />
<SecurityItem
icon="eye-off"
iconColor="#EC4899"
iconBgColor="#FCE7F3"
title="Data Sharing"
description="Control who can see your data"
onPress={() => Alert.alert('Data Sharing', 'Your data is only shared with authorized caregivers and healthcare providers you designate.')}
/>
</View>
</View>
{/* Login History */}
<View style={styles.section}>
<Text style={styles.sectionTitle}>Recent Activity</Text>
<View style={styles.card}>
<View style={styles.activityItem}>
<View style={styles.activityDot} />
<View style={styles.activityContent}>
<Text style={styles.activityTitle}>Login from iPhone</Text>
<Text style={styles.activityMeta}>Today at 2:34 PM San Francisco, CA</Text>
</View>
</View>
<View style={styles.activityItem}>
<View style={[styles.activityDot, styles.activityDotOld]} />
<View style={styles.activityContent}>
<Text style={styles.activityTitle}>Login from MacBook</Text>
<Text style={styles.activityMeta}>Yesterday at 10:15 AM San Francisco, CA</Text>
</View>
</View>
<View style={styles.activityItem}>
<View style={[styles.activityDot, styles.activityDotOld]} />
<View style={styles.activityContent}>
<Text style={styles.activityTitle}>Password changed</Text>
<Text style={styles.activityMeta}>Dec 1, 2024 at 4:22 PM</Text>
</View>
</View>
</View>
</View>
{/* Danger Zone */}
<View style={styles.section}>
<Text style={[styles.sectionTitle, { color: AppColors.error }]}>Danger Zone</Text>
<View style={styles.card}>
<TouchableOpacity style={styles.dangerButton} onPress={handleDeleteAccount}>
<Ionicons name="trash" size={20} color={AppColors.error} />
<Text style={styles.dangerButtonText}>Delete Account</Text>
</TouchableOpacity>
</View>
</View>
</ScrollView>
{/* Password Change Modal */}
<Modal
visible={showPasswordModal}
animationType="slide"
presentationStyle="pageSheet"
onRequestClose={() => setShowPasswordModal(false)}
>
<SafeAreaView style={styles.modalContainer}>
<View style={styles.modalHeader}>
<TouchableOpacity onPress={() => setShowPasswordModal(false)}>
<Text style={styles.cancelText}>Cancel</Text>
</TouchableOpacity>
<Text style={styles.modalTitle}>Change Password</Text>
<TouchableOpacity onPress={handlePasswordSubmit}>
<Text style={styles.saveText}>Save</Text>
</TouchableOpacity>
</View>
<ScrollView style={styles.modalContent}>
<View style={styles.inputGroup}>
<Text style={styles.label}>Current Password</Text>
<TextInput
style={styles.input}
value={currentPassword}
onChangeText={setCurrentPassword}
placeholder="Enter current password"
placeholderTextColor={AppColors.textMuted}
secureTextEntry
/>
</View>
<View style={styles.inputGroup}>
<Text style={styles.label}>New Password</Text>
<TextInput
style={styles.input}
value={newPassword}
onChangeText={setNewPassword}
placeholder="Enter new password"
placeholderTextColor={AppColors.textMuted}
secureTextEntry
/>
<Text style={styles.hint}>Must be at least 8 characters</Text>
</View>
<View style={styles.inputGroup}>
<Text style={styles.label}>Confirm New Password</Text>
<TextInput
style={styles.input}
value={confirmPassword}
onChangeText={setConfirmPassword}
placeholder="Confirm new password"
placeholderTextColor={AppColors.textMuted}
secureTextEntry
/>
</View>
<View style={styles.passwordRequirements}>
<Text style={styles.requirementsTitle}>Password Requirements:</Text>
<Text style={styles.requirementItem}> At least 8 characters</Text>
<Text style={styles.requirementItem}> Mix of letters and numbers recommended</Text>
<Text style={styles.requirementItem}> Special characters for extra security</Text>
</View>
</ScrollView>
</SafeAreaView>
</Modal>
</SafeAreaView>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: AppColors.surface,
},
section: {
marginTop: Spacing.md,
},
sectionTitle: {
fontSize: FontSizes.sm,
fontWeight: '600',
color: AppColors.textSecondary,
paddingHorizontal: Spacing.lg,
paddingVertical: Spacing.sm,
textTransform: 'uppercase',
},
card: {
backgroundColor: AppColors.background,
},
securityRow: {
flexDirection: 'row',
alignItems: 'center',
paddingVertical: Spacing.md,
paddingHorizontal: Spacing.lg,
},
iconContainer: {
width: 40,
height: 40,
borderRadius: BorderRadius.md,
justifyContent: 'center',
alignItems: 'center',
},
securityContent: {
flex: 1,
marginLeft: Spacing.md,
},
securityTitle: {
fontSize: FontSizes.base,
fontWeight: '500',
color: AppColors.textPrimary,
},
securityDescription: {
fontSize: FontSizes.xs,
color: AppColors.textMuted,
marginTop: 2,
},
divider: {
height: 1,
backgroundColor: AppColors.border,
marginLeft: Spacing.lg + 40 + Spacing.md,
},
activityItem: {
flexDirection: 'row',
alignItems: 'center',
paddingVertical: Spacing.md,
paddingHorizontal: Spacing.lg,
},
activityDot: {
width: 8,
height: 8,
borderRadius: 4,
backgroundColor: AppColors.success,
marginRight: Spacing.md,
},
activityDotOld: {
backgroundColor: AppColors.textMuted,
},
activityContent: {
flex: 1,
},
activityTitle: {
fontSize: FontSizes.sm,
fontWeight: '500',
color: AppColors.textPrimary,
},
activityMeta: {
fontSize: FontSizes.xs,
color: AppColors.textMuted,
marginTop: 2,
},
dangerButton: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
paddingVertical: Spacing.md,
},
dangerButtonText: {
fontSize: FontSizes.base,
fontWeight: '500',
color: AppColors.error,
marginLeft: Spacing.sm,
},
// Modal styles
modalContainer: {
flex: 1,
backgroundColor: AppColors.surface,
},
modalHeader: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
paddingHorizontal: Spacing.lg,
paddingVertical: Spacing.md,
backgroundColor: AppColors.background,
borderBottomWidth: 1,
borderBottomColor: AppColors.border,
},
modalTitle: {
fontSize: FontSizes.lg,
fontWeight: '600',
color: AppColors.textPrimary,
},
cancelText: {
fontSize: FontSizes.base,
color: AppColors.textSecondary,
},
saveText: {
fontSize: FontSizes.base,
fontWeight: '600',
color: AppColors.primary,
},
modalContent: {
padding: Spacing.lg,
},
inputGroup: {
marginBottom: Spacing.lg,
},
label: {
fontSize: FontSizes.sm,
fontWeight: '600',
color: AppColors.textPrimary,
marginBottom: Spacing.xs,
},
input: {
backgroundColor: AppColors.background,
borderRadius: BorderRadius.md,
paddingHorizontal: Spacing.md,
paddingVertical: Spacing.md,
fontSize: FontSizes.base,
color: AppColors.textPrimary,
borderWidth: 1,
borderColor: AppColors.border,
},
hint: {
fontSize: FontSizes.xs,
color: AppColors.textMuted,
marginTop: Spacing.xs,
},
passwordRequirements: {
backgroundColor: AppColors.background,
borderRadius: BorderRadius.lg,
padding: Spacing.md,
marginTop: Spacing.md,
},
requirementsTitle: {
fontSize: FontSizes.sm,
fontWeight: '600',
color: AppColors.textSecondary,
marginBottom: Spacing.sm,
},
requirementItem: {
fontSize: FontSizes.sm,
color: AppColors.textMuted,
marginBottom: 4,
},
});

View File

@ -0,0 +1,517 @@
import React, { useState } from 'react';
import {
View,
Text,
StyleSheet,
ScrollView,
TouchableOpacity,
Alert,
} from 'react-native';
import { Ionicons } from '@expo/vector-icons';
import { SafeAreaView } from 'react-native-safe-area-context';
import { router } from 'expo-router';
import { AppColors, BorderRadius, FontSizes, Spacing } from '@/constants/theme';
interface PlanFeatureProps {
text: string;
included: boolean;
}
function PlanFeature({ text, included }: PlanFeatureProps) {
return (
<View style={styles.featureRow}>
<Ionicons
name={included ? 'checkmark-circle' : 'close-circle'}
size={20}
color={included ? AppColors.success : AppColors.textMuted}
/>
<Text style={[styles.featureText, !included && styles.featureTextDisabled]}>
{text}
</Text>
</View>
);
}
export default function SubscriptionScreen() {
const [selectedPlan, setSelectedPlan] = useState<'monthly' | 'yearly'>('yearly');
const currentPlan = 'free'; // Could be 'free', 'pro', 'enterprise'
const handleSubscribe = () => {
Alert.alert(
'Subscribe to Pro',
`You selected the ${selectedPlan === 'yearly' ? 'Yearly' : 'Monthly'} plan.\n\n` +
`Total: ${selectedPlan === 'yearly' ? '$79.99/year (Save $39.89!)' : '$9.99/month'}`,
[
{ text: 'Cancel', style: 'cancel' },
{
text: 'Continue to Payment',
onPress: () => Alert.alert('Coming Soon', 'Payment integration coming soon!')
},
]
);
};
const handleRestorePurchases = () => {
Alert.alert(
'Restoring Purchases',
'Looking for previous purchases...',
[{ text: 'OK' }]
);
setTimeout(() => {
Alert.alert('No Purchases Found', 'We couldn\'t find any previous purchases associated with your account.');
}, 1500);
};
return (
<SafeAreaView style={styles.container} edges={['bottom']}>
<ScrollView showsVerticalScrollIndicator={false}>
{/* Current Plan Badge */}
<View style={styles.currentPlanBanner}>
<View style={styles.currentPlanBadge}>
<Text style={styles.currentPlanBadgeText}>CURRENT PLAN</Text>
</View>
<Text style={styles.currentPlanName}>Free</Text>
<Text style={styles.currentPlanDescription}>
Basic monitoring features
</Text>
</View>
{/* Pro Features */}
<View style={styles.section}>
<View style={styles.proCard}>
<View style={styles.proHeader}>
<View style={styles.proBadge}>
<Ionicons name="diamond" size={20} color="#9333EA" />
<Text style={styles.proBadgeText}>PRO</Text>
</View>
<Text style={styles.proTitle}>WellNuo Pro</Text>
<Text style={styles.proSubtitle}>Everything you need for complete care</Text>
</View>
<View style={styles.featuresContainer}>
<PlanFeature text="Unlimited beneficiaries" included={true} />
<PlanFeature text="Advanced analytics dashboard" included={true} />
<PlanFeature text="AI-powered health insights" included={true} />
<PlanFeature text="Priority 24/7 support" included={true} />
<PlanFeature text="Custom alert rules" included={true} />
<PlanFeature text="API access for integrations" included={true} />
<PlanFeature text="Export data to CSV/PDF" included={true} />
<PlanFeature text="Multi-location monitoring" included={true} />
<PlanFeature text="Family sharing (5 members)" included={true} />
<PlanFeature text="Wellness trend predictions" included={true} />
</View>
</View>
</View>
{/* Pricing Options */}
<View style={styles.section}>
<Text style={styles.sectionTitle}>Choose Your Plan</Text>
<TouchableOpacity
style={[styles.planOption, selectedPlan === 'yearly' && styles.planOptionSelected]}
onPress={() => setSelectedPlan('yearly')}
>
<View style={styles.planOptionContent}>
<View style={styles.planOptionHeader}>
<Text style={styles.planOptionTitle}>Yearly</Text>
<View style={styles.saveBadge}>
<Text style={styles.saveBadgeText}>SAVE 33%</Text>
</View>
</View>
<Text style={styles.planOptionPrice}>$79.99/year</Text>
<Text style={styles.planOptionSubprice}>$6.67/month</Text>
</View>
<View style={[
styles.radioButton,
selectedPlan === 'yearly' && styles.radioButtonSelected
]}>
{selectedPlan === 'yearly' && (
<View style={styles.radioButtonInner} />
)}
</View>
</TouchableOpacity>
<TouchableOpacity
style={[styles.planOption, selectedPlan === 'monthly' && styles.planOptionSelected]}
onPress={() => setSelectedPlan('monthly')}
>
<View style={styles.planOptionContent}>
<Text style={styles.planOptionTitle}>Monthly</Text>
<Text style={styles.planOptionPrice}>$9.99/month</Text>
<Text style={styles.planOptionSubprice}>Billed monthly</Text>
</View>
<View style={[
styles.radioButton,
selectedPlan === 'monthly' && styles.radioButtonSelected
]}>
{selectedPlan === 'monthly' && (
<View style={styles.radioButtonInner} />
)}
</View>
</TouchableOpacity>
</View>
{/* Compare Plans */}
<View style={styles.section}>
<Text style={styles.sectionTitle}>Compare Plans</Text>
<View style={styles.comparisonCard}>
<View style={styles.comparisonHeader}>
<Text style={styles.comparisonFeatureTitle}>Feature</Text>
<Text style={styles.comparisonPlanTitle}>Free</Text>
<Text style={styles.comparisonPlanTitle}>Pro</Text>
</View>
{[
{ feature: 'Beneficiaries', free: '2', pro: 'Unlimited' },
{ feature: 'Real-time alerts', free: '✓', pro: '✓' },
{ feature: 'Activity history', free: '7 days', pro: '1 year' },
{ feature: 'AI insights', free: '—', pro: '✓' },
{ feature: 'Custom alerts', free: '—', pro: '✓' },
{ feature: 'Data export', free: '—', pro: '✓' },
{ feature: 'Support', free: 'Email', pro: '24/7 Priority' },
].map((row, index) => (
<View key={index} style={styles.comparisonRow}>
<Text style={styles.comparisonFeature}>{row.feature}</Text>
<Text style={styles.comparisonValue}>{row.free}</Text>
<Text style={[styles.comparisonValue, styles.comparisonValuePro]}>{row.pro}</Text>
</View>
))}
</View>
</View>
{/* Testimonials */}
<View style={styles.section}>
<Text style={styles.sectionTitle}>What Users Say</Text>
<ScrollView horizontal showsHorizontalScrollIndicator={false}>
{[
{
name: 'Sarah M.',
text: 'WellNuo Pro gives me peace of mind. The AI insights helped detect early warning signs.',
rating: 5,
},
{
name: 'John D.',
text: 'The family sharing feature is amazing. Now my siblings can all monitor our parents together.',
rating: 5,
},
{
name: 'Maria L.',
text: 'Worth every penny. The advanced analytics helped us understand mom\'s patterns better.',
rating: 5,
},
].map((testimonial, index) => (
<View key={index} style={styles.testimonialCard}>
<View style={styles.testimonialStars}>
{[...Array(testimonial.rating)].map((_, i) => (
<Ionicons key={i} name="star" size={16} color="#F59E0B" />
))}
</View>
<Text style={styles.testimonialText}>"{testimonial.text}"</Text>
<Text style={styles.testimonialName}> {testimonial.name}</Text>
</View>
))}
</ScrollView>
</View>
{/* Restore Purchases */}
<TouchableOpacity style={styles.restoreButton} onPress={handleRestorePurchases}>
<Text style={styles.restoreButtonText}>Restore Purchases</Text>
</TouchableOpacity>
{/* Terms */}
<Text style={styles.termsText}>
Payment will be charged to your Apple ID account at the confirmation of purchase.
Subscription automatically renews unless it is cancelled at least 24 hours before
the end of the current period.
</Text>
</ScrollView>
{/* Subscribe Button */}
<View style={styles.footer}>
<TouchableOpacity style={styles.subscribeButton} onPress={handleSubscribe}>
<Text style={styles.subscribeButtonText}>
Subscribe to Pro {selectedPlan === 'yearly' ? '$79.99/year' : '$9.99/month'}
</Text>
</TouchableOpacity>
<Text style={styles.guaranteeText}>7-day free trial Cancel anytime</Text>
</View>
</SafeAreaView>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: AppColors.surface,
},
currentPlanBanner: {
backgroundColor: AppColors.background,
padding: Spacing.lg,
alignItems: 'center',
},
currentPlanBadge: {
backgroundColor: '#E0E7FF',
paddingHorizontal: Spacing.sm,
paddingVertical: 4,
borderRadius: BorderRadius.sm,
},
currentPlanBadgeText: {
fontSize: FontSizes.xs,
fontWeight: '600',
color: '#4F46E5',
},
currentPlanName: {
fontSize: FontSizes.xl,
fontWeight: '700',
color: AppColors.textPrimary,
marginTop: Spacing.sm,
},
currentPlanDescription: {
fontSize: FontSizes.sm,
color: AppColors.textSecondary,
marginTop: 4,
},
section: {
marginTop: Spacing.md,
},
sectionTitle: {
fontSize: FontSizes.sm,
fontWeight: '600',
color: AppColors.textSecondary,
paddingHorizontal: Spacing.lg,
paddingVertical: Spacing.sm,
textTransform: 'uppercase',
},
proCard: {
backgroundColor: AppColors.background,
marginHorizontal: Spacing.lg,
borderRadius: BorderRadius.lg,
overflow: 'hidden',
borderWidth: 2,
borderColor: '#9333EA',
},
proHeader: {
backgroundColor: '#F3E8FF',
padding: Spacing.lg,
alignItems: 'center',
},
proBadge: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: AppColors.white,
paddingHorizontal: Spacing.md,
paddingVertical: Spacing.xs,
borderRadius: BorderRadius.full,
},
proBadgeText: {
fontSize: FontSizes.sm,
fontWeight: '700',
color: '#9333EA',
marginLeft: Spacing.xs,
},
proTitle: {
fontSize: FontSizes.xl,
fontWeight: '700',
color: '#9333EA',
marginTop: Spacing.md,
},
proSubtitle: {
fontSize: FontSizes.sm,
color: '#7C3AED',
marginTop: 4,
},
featuresContainer: {
padding: Spacing.lg,
},
featureRow: {
flexDirection: 'row',
alignItems: 'center',
paddingVertical: Spacing.xs,
},
featureText: {
fontSize: FontSizes.sm,
color: AppColors.textPrimary,
marginLeft: Spacing.sm,
},
featureTextDisabled: {
color: AppColors.textMuted,
},
planOption: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: AppColors.background,
marginHorizontal: Spacing.lg,
marginBottom: Spacing.sm,
padding: Spacing.md,
borderRadius: BorderRadius.lg,
borderWidth: 2,
borderColor: AppColors.border,
},
planOptionSelected: {
borderColor: AppColors.primary,
backgroundColor: '#DBEAFE',
},
planOptionContent: {
flex: 1,
},
planOptionHeader: {
flexDirection: 'row',
alignItems: 'center',
},
planOptionTitle: {
fontSize: FontSizes.lg,
fontWeight: '600',
color: AppColors.textPrimary,
},
saveBadge: {
backgroundColor: AppColors.success,
paddingHorizontal: Spacing.xs,
paddingVertical: 2,
borderRadius: BorderRadius.sm,
marginLeft: Spacing.sm,
},
saveBadgeText: {
fontSize: 10,
fontWeight: '700',
color: AppColors.white,
},
planOptionPrice: {
fontSize: FontSizes.xl,
fontWeight: '700',
color: AppColors.primary,
marginTop: 4,
},
planOptionSubprice: {
fontSize: FontSizes.xs,
color: AppColors.textMuted,
marginTop: 2,
},
radioButton: {
width: 24,
height: 24,
borderRadius: 12,
borderWidth: 2,
borderColor: AppColors.border,
justifyContent: 'center',
alignItems: 'center',
},
radioButtonSelected: {
borderColor: AppColors.primary,
},
radioButtonInner: {
width: 12,
height: 12,
borderRadius: 6,
backgroundColor: AppColors.primary,
},
comparisonCard: {
backgroundColor: AppColors.background,
marginHorizontal: Spacing.lg,
borderRadius: BorderRadius.lg,
overflow: 'hidden',
},
comparisonHeader: {
flexDirection: 'row',
backgroundColor: AppColors.surface,
paddingVertical: Spacing.sm,
paddingHorizontal: Spacing.md,
},
comparisonFeatureTitle: {
flex: 2,
fontSize: FontSizes.xs,
fontWeight: '600',
color: AppColors.textSecondary,
},
comparisonPlanTitle: {
flex: 1,
fontSize: FontSizes.xs,
fontWeight: '600',
color: AppColors.textSecondary,
textAlign: 'center',
},
comparisonRow: {
flexDirection: 'row',
paddingVertical: Spacing.sm,
paddingHorizontal: Spacing.md,
borderBottomWidth: 1,
borderBottomColor: AppColors.border,
},
comparisonFeature: {
flex: 2,
fontSize: FontSizes.sm,
color: AppColors.textPrimary,
},
comparisonValue: {
flex: 1,
fontSize: FontSizes.sm,
color: AppColors.textSecondary,
textAlign: 'center',
},
comparisonValuePro: {
color: '#9333EA',
fontWeight: '500',
},
testimonialCard: {
width: 280,
backgroundColor: AppColors.background,
marginLeft: Spacing.lg,
padding: Spacing.md,
borderRadius: BorderRadius.lg,
},
testimonialStars: {
flexDirection: 'row',
marginBottom: Spacing.sm,
},
testimonialText: {
fontSize: FontSizes.sm,
color: AppColors.textPrimary,
fontStyle: 'italic',
lineHeight: 20,
},
testimonialName: {
fontSize: FontSizes.xs,
color: AppColors.textMuted,
marginTop: Spacing.sm,
},
restoreButton: {
alignItems: 'center',
paddingVertical: Spacing.md,
marginTop: Spacing.md,
},
restoreButtonText: {
fontSize: FontSizes.sm,
color: AppColors.primary,
},
termsText: {
fontSize: FontSizes.xs,
color: AppColors.textMuted,
textAlign: 'center',
paddingHorizontal: Spacing.xl,
paddingBottom: Spacing.lg,
lineHeight: 16,
},
footer: {
padding: Spacing.lg,
backgroundColor: AppColors.background,
borderTopWidth: 1,
borderTopColor: AppColors.border,
},
subscribeButton: {
backgroundColor: '#9333EA',
borderRadius: BorderRadius.lg,
paddingVertical: Spacing.md,
alignItems: 'center',
},
subscribeButtonText: {
fontSize: FontSizes.base,
fontWeight: '600',
color: AppColors.white,
},
guaranteeText: {
fontSize: FontSizes.xs,
color: AppColors.textMuted,
textAlign: 'center',
marginTop: Spacing.sm,
},
});

475
app/profile/support.tsx Normal file
View File

@ -0,0 +1,475 @@
import React, { useState } from 'react';
import {
View,
Text,
StyleSheet,
ScrollView,
TouchableOpacity,
TextInput,
Linking,
Alert,
KeyboardAvoidingView,
Platform,
} from 'react-native';
import { Ionicons } from '@expo/vector-icons';
import { SafeAreaView } from 'react-native-safe-area-context';
import { router } from 'expo-router';
import { AppColors, BorderRadius, FontSizes, Spacing } from '@/constants/theme';
interface ContactMethodProps {
icon: keyof typeof Ionicons.glyphMap;
iconColor: string;
iconBgColor: string;
title: string;
subtitle: string;
onPress: () => void;
}
function ContactMethod({
icon,
iconColor,
iconBgColor,
title,
subtitle,
onPress,
}: ContactMethodProps) {
return (
<TouchableOpacity style={styles.contactMethod} onPress={onPress}>
<View style={[styles.contactIcon, { backgroundColor: iconBgColor }]}>
<Ionicons name={icon} size={24} color={iconColor} />
</View>
<View style={styles.contactContent}>
<Text style={styles.contactTitle}>{title}</Text>
<Text style={styles.contactSubtitle}>{subtitle}</Text>
</View>
<Ionicons name="chevron-forward" size={20} color={AppColors.textMuted} />
</TouchableOpacity>
);
}
export default function SupportScreen() {
const [subject, setSubject] = useState('');
const [message, setMessage] = useState('');
const [category, setCategory] = useState('');
const [isSending, setIsSending] = useState(false);
const categories = [
'Technical Issue',
'Billing Question',
'Feature Request',
'Account Help',
'Emergency',
'Other',
];
const handleCall = () => {
Linking.openURL('tel:+15551234567').catch(() => {
Alert.alert('Error', 'Unable to make phone call');
});
};
const handleEmail = () => {
Linking.openURL('mailto:support@wellnuo.com?subject=Support Request').catch(() => {
Alert.alert('Error', 'Unable to open email client');
});
};
const handleChat = () => {
Alert.alert(
'Live Chat',
'Connecting to a support agent...\n\nEstimated wait time: 2 minutes',
[
{ text: 'Cancel', style: 'cancel' },
{ text: 'Start Chat', onPress: () => Alert.alert('Coming Soon', 'Live chat feature coming soon!') },
]
);
};
const handleSendTicket = async () => {
if (!category) {
Alert.alert('Error', 'Please select a category');
return;
}
if (!subject.trim()) {
Alert.alert('Error', 'Please enter a subject');
return;
}
if (!message.trim()) {
Alert.alert('Error', 'Please describe your issue');
return;
}
setIsSending(true);
// Simulate API call
await new Promise(resolve => setTimeout(resolve, 1500));
setIsSending(false);
Alert.alert(
'Ticket Submitted',
'Thank you for contacting us!\n\nTicket #WN-2024-12345\n\nWe\'ll respond within 24 hours.',
[{ text: 'OK', onPress: () => router.back() }]
);
};
return (
<SafeAreaView style={styles.container} edges={['bottom']}>
<KeyboardAvoidingView
style={styles.keyboardView}
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
>
<ScrollView showsVerticalScrollIndicator={false}>
{/* Quick Contact */}
<View style={styles.section}>
<Text style={styles.sectionTitle}>Quick Contact</Text>
<View style={styles.card}>
<ContactMethod
icon="call"
iconColor="#10B981"
iconBgColor="#D1FAE5"
title="Call Us"
subtitle="+1 (555) 123-4567"
onPress={handleCall}
/>
<View style={styles.divider} />
<ContactMethod
icon="mail"
iconColor="#3B82F6"
iconBgColor="#DBEAFE"
title="Email Support"
subtitle="support@wellnuo.com"
onPress={handleEmail}
/>
<View style={styles.divider} />
<ContactMethod
icon="chatbubbles"
iconColor="#8B5CF6"
iconBgColor="#EDE9FE"
title="Live Chat"
subtitle="Available 24/7"
onPress={handleChat}
/>
</View>
</View>
{/* Support Hours */}
<View style={styles.section}>
<View style={styles.hoursCard}>
<Ionicons name="time" size={24} color={AppColors.primary} />
<View style={styles.hoursContent}>
<Text style={styles.hoursTitle}>Support Hours</Text>
<Text style={styles.hoursText}>Phone: Mon-Fri 8am-8pm EST</Text>
<Text style={styles.hoursText}>Email & Chat: 24/7</Text>
<Text style={styles.hoursText}>Emergency: 24/7</Text>
</View>
</View>
</View>
{/* Submit a Ticket */}
<View style={styles.section}>
<Text style={styles.sectionTitle}>Submit a Ticket</Text>
<View style={styles.formCard}>
{/* Category */}
<View style={styles.inputGroup}>
<Text style={styles.label}>Category *</Text>
<View style={styles.categoryContainer}>
{categories.map((cat) => (
<TouchableOpacity
key={cat}
style={[
styles.categoryChip,
category === cat && styles.categoryChipSelected,
]}
onPress={() => setCategory(cat)}
>
<Text
style={[
styles.categoryChipText,
category === cat && styles.categoryChipTextSelected,
]}
>
{cat}
</Text>
</TouchableOpacity>
))}
</View>
</View>
{/* Subject */}
<View style={styles.inputGroup}>
<Text style={styles.label}>Subject *</Text>
<TextInput
style={styles.input}
value={subject}
onChangeText={setSubject}
placeholder="Brief description of your issue"
placeholderTextColor={AppColors.textMuted}
/>
</View>
{/* Message */}
<View style={styles.inputGroup}>
<Text style={styles.label}>Message *</Text>
<TextInput
style={[styles.input, styles.textArea]}
value={message}
onChangeText={setMessage}
placeholder="Please describe your issue in detail. Include any error messages or steps to reproduce the problem."
placeholderTextColor={AppColors.textMuted}
multiline
numberOfLines={5}
textAlignVertical="top"
/>
</View>
{/* Attachments hint */}
<View style={styles.attachmentHint}>
<Ionicons name="attach" size={16} color={AppColors.textMuted} />
<Text style={styles.attachmentHintText}>
Need to attach screenshots? Reply to your ticket email.
</Text>
</View>
{/* Submit Button */}
<TouchableOpacity
style={[styles.submitButton, isSending && styles.submitButtonDisabled]}
onPress={handleSendTicket}
disabled={isSending}
>
<Text style={styles.submitButtonText}>
{isSending ? 'Sending...' : 'Submit Ticket'}
</Text>
</TouchableOpacity>
</View>
</View>
{/* FAQ Link */}
<View style={styles.section}>
<TouchableOpacity
style={styles.faqLink}
onPress={() => router.push('/profile/help')}
>
<View style={styles.faqLinkContent}>
<Ionicons name="help-circle" size={24} color={AppColors.primary} />
<View style={styles.faqLinkText}>
<Text style={styles.faqLinkTitle}>Check our Help Center</Text>
<Text style={styles.faqLinkSubtitle}>
Find answers to common questions
</Text>
</View>
</View>
<Ionicons name="chevron-forward" size={20} color={AppColors.textMuted} />
</TouchableOpacity>
</View>
{/* Emergency Notice */}
<View style={styles.emergencyNotice}>
<Ionicons name="warning" size={20} color={AppColors.error} />
<Text style={styles.emergencyText}>
If you're experiencing a medical emergency, please call 911 or your local
emergency services immediately.
</Text>
</View>
</ScrollView>
</KeyboardAvoidingView>
</SafeAreaView>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: AppColors.surface,
},
keyboardView: {
flex: 1,
},
section: {
marginTop: Spacing.md,
},
sectionTitle: {
fontSize: FontSizes.sm,
fontWeight: '600',
color: AppColors.textSecondary,
paddingHorizontal: Spacing.lg,
paddingVertical: Spacing.sm,
textTransform: 'uppercase',
},
card: {
backgroundColor: AppColors.background,
},
contactMethod: {
flexDirection: 'row',
alignItems: 'center',
paddingVertical: Spacing.md,
paddingHorizontal: Spacing.lg,
},
contactIcon: {
width: 48,
height: 48,
borderRadius: BorderRadius.md,
justifyContent: 'center',
alignItems: 'center',
},
contactContent: {
flex: 1,
marginLeft: Spacing.md,
},
contactTitle: {
fontSize: FontSizes.base,
fontWeight: '500',
color: AppColors.textPrimary,
},
contactSubtitle: {
fontSize: FontSizes.sm,
color: AppColors.textSecondary,
marginTop: 2,
},
divider: {
height: 1,
backgroundColor: AppColors.border,
marginLeft: Spacing.lg + 48 + Spacing.md,
},
hoursCard: {
flexDirection: 'row',
backgroundColor: AppColors.background,
marginHorizontal: Spacing.lg,
padding: Spacing.md,
borderRadius: BorderRadius.lg,
alignItems: 'flex-start',
},
hoursContent: {
flex: 1,
marginLeft: Spacing.md,
},
hoursTitle: {
fontSize: FontSizes.base,
fontWeight: '600',
color: AppColors.textPrimary,
marginBottom: Spacing.xs,
},
hoursText: {
fontSize: FontSizes.sm,
color: AppColors.textSecondary,
marginTop: 2,
},
formCard: {
backgroundColor: AppColors.background,
padding: Spacing.lg,
},
inputGroup: {
marginBottom: Spacing.md,
},
label: {
fontSize: FontSizes.sm,
fontWeight: '600',
color: AppColors.textPrimary,
marginBottom: Spacing.xs,
},
input: {
backgroundColor: AppColors.surface,
borderRadius: BorderRadius.md,
paddingHorizontal: Spacing.md,
paddingVertical: Spacing.md,
fontSize: FontSizes.base,
color: AppColors.textPrimary,
borderWidth: 1,
borderColor: AppColors.border,
},
textArea: {
minHeight: 120,
paddingTop: Spacing.md,
},
categoryContainer: {
flexDirection: 'row',
flexWrap: 'wrap',
marginTop: Spacing.xs,
},
categoryChip: {
paddingHorizontal: Spacing.md,
paddingVertical: Spacing.xs,
borderRadius: BorderRadius.full,
backgroundColor: AppColors.surface,
marginRight: Spacing.xs,
marginBottom: Spacing.xs,
borderWidth: 1,
borderColor: AppColors.border,
},
categoryChipSelected: {
backgroundColor: AppColors.primary,
borderColor: AppColors.primary,
},
categoryChipText: {
fontSize: FontSizes.sm,
color: AppColors.textSecondary,
},
categoryChipTextSelected: {
color: AppColors.white,
},
attachmentHint: {
flexDirection: 'row',
alignItems: 'center',
marginBottom: Spacing.lg,
},
attachmentHintText: {
fontSize: FontSizes.xs,
color: AppColors.textMuted,
marginLeft: Spacing.xs,
},
submitButton: {
backgroundColor: AppColors.primary,
borderRadius: BorderRadius.lg,
paddingVertical: Spacing.md,
alignItems: 'center',
},
submitButtonDisabled: {
opacity: 0.6,
},
submitButtonText: {
fontSize: FontSizes.base,
fontWeight: '600',
color: AppColors.white,
},
faqLink: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: AppColors.background,
marginHorizontal: Spacing.lg,
padding: Spacing.md,
borderRadius: BorderRadius.lg,
},
faqLinkContent: {
flex: 1,
flexDirection: 'row',
alignItems: 'center',
},
faqLinkText: {
marginLeft: Spacing.md,
},
faqLinkTitle: {
fontSize: FontSizes.base,
fontWeight: '500',
color: AppColors.textPrimary,
},
faqLinkSubtitle: {
fontSize: FontSizes.xs,
color: AppColors.textMuted,
marginTop: 2,
},
emergencyNotice: {
flexDirection: 'row',
alignItems: 'flex-start',
backgroundColor: '#FEE2E2',
marginHorizontal: Spacing.lg,
marginVertical: Spacing.lg,
padding: Spacing.md,
borderRadius: BorderRadius.lg,
},
emergencyText: {
flex: 1,
fontSize: FontSizes.xs,
color: AppColors.error,
marginLeft: Spacing.sm,
lineHeight: 18,
},
});

197
app/profile/terms.tsx Normal file
View File

@ -0,0 +1,197 @@
import React from 'react';
import {
View,
Text,
StyleSheet,
ScrollView,
} from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import { AppColors, BorderRadius, FontSizes, Spacing } from '@/constants/theme';
export default function TermsScreen() {
return (
<SafeAreaView style={styles.container} edges={['bottom']}>
<ScrollView showsVerticalScrollIndicator={false}>
<View style={styles.content}>
<Text style={styles.lastUpdated}>Last Updated: December 2024</Text>
<Text style={styles.sectionTitle}>1. Acceptance of Terms</Text>
<Text style={styles.paragraph}>
By accessing and using WellNuo ("the Service"), you agree to be bound by these
Terms of Service and all applicable laws and regulations. If you do not agree
with any of these terms, you are prohibited from using or accessing this Service.
</Text>
<Text style={styles.sectionTitle}>2. Description of Service</Text>
<Text style={styles.paragraph}>
WellNuo provides an elderly care monitoring platform that allows caregivers and
family members to monitor the wellness and activity of their loved ones through
connected sensors and devices. The Service includes:
</Text>
<Text style={styles.bulletPoint}> Real-time activity monitoring</Text>
<Text style={styles.bulletPoint}> Wellness score tracking</Text>
<Text style={styles.bulletPoint}> Emergency alert notifications</Text>
<Text style={styles.bulletPoint}> AI-powered health insights (Pro plan)</Text>
<Text style={styles.bulletPoint}> Data analytics and reporting</Text>
<Text style={styles.sectionTitle}>3. User Accounts</Text>
<Text style={styles.paragraph}>
To use the Service, you must create an account and provide accurate, complete
information. You are responsible for:
</Text>
<Text style={styles.bulletPoint}> Maintaining the confidentiality of your account</Text>
<Text style={styles.bulletPoint}> All activities that occur under your account</Text>
<Text style={styles.bulletPoint}> Notifying us immediately of any unauthorized use</Text>
<Text style={styles.sectionTitle}>4. Privacy and Data Protection</Text>
<Text style={styles.paragraph}>
Your privacy is critically important to us. Our Privacy Policy explains how we
collect, use, and protect your personal information and the data of beneficiaries.
By using the Service, you consent to our data practices.
</Text>
<Text style={styles.paragraph}>
Key privacy commitments:
</Text>
<Text style={styles.bulletPoint}> End-to-end encryption for all data</Text>
<Text style={styles.bulletPoint}> HIPAA-compliant data storage</Text>
<Text style={styles.bulletPoint}> No sale of personal data to third parties</Text>
<Text style={styles.bulletPoint}> Right to data export and deletion</Text>
<Text style={styles.sectionTitle}>5. Acceptable Use</Text>
<Text style={styles.paragraph}>
You agree to use the Service only for lawful purposes and in accordance with
these Terms. You agree NOT to:
</Text>
<Text style={styles.bulletPoint}> Use the Service for any illegal purpose</Text>
<Text style={styles.bulletPoint}> Violate any beneficiary's privacy rights</Text>
<Text style={styles.bulletPoint}> Share your account credentials with others</Text>
<Text style={styles.bulletPoint}> Attempt to reverse engineer the Service</Text>
<Text style={styles.bulletPoint}> Transmit any malware or harmful code</Text>
<Text style={styles.sectionTitle}>6. Subscription and Billing</Text>
<Text style={styles.paragraph}>
WellNuo offers both free and paid subscription plans:
</Text>
<Text style={styles.bulletPoint}> Free Plan: Basic features with limited beneficiaries</Text>
<Text style={styles.bulletPoint}> Pro Plan: Full features billed monthly or annually</Text>
<Text style={styles.paragraph}>
Subscription fees are billed in advance. You may cancel at any time, but refunds
are not provided for partial billing periods.
</Text>
<Text style={styles.sectionTitle}>7. Disclaimers</Text>
<Text style={styles.paragraph}>
The Service is provided "as is" without warranties of any kind. WellNuo:
</Text>
<Text style={styles.bulletPoint}> Does not guarantee continuous, uninterrupted service</Text>
<Text style={styles.bulletPoint}> Is not a medical device or substitute for professional care</Text>
<Text style={styles.bulletPoint}> Cannot guarantee detection of all emergencies</Text>
<Text style={styles.paragraph}>
Always seek professional medical advice for health concerns.
</Text>
<Text style={styles.sectionTitle}>8. Limitation of Liability</Text>
<Text style={styles.paragraph}>
To the maximum extent permitted by law, WellNuo shall not be liable for any
indirect, incidental, special, consequential, or punitive damages arising out
of your use of the Service.
</Text>
<Text style={styles.sectionTitle}>9. Indemnification</Text>
<Text style={styles.paragraph}>
You agree to indemnify and hold harmless WellNuo and its affiliates from any
claims, damages, or expenses arising from your use of the Service or violation
of these Terms.
</Text>
<Text style={styles.sectionTitle}>10. Modifications</Text>
<Text style={styles.paragraph}>
We reserve the right to modify these Terms at any time. We will notify you of
material changes through the app or email. Continued use after changes constitutes
acceptance of the new Terms.
</Text>
<Text style={styles.sectionTitle}>11. Termination</Text>
<Text style={styles.paragraph}>
We may terminate or suspend your account at any time for violation of these Terms.
Upon termination, your right to use the Service ceases immediately.
</Text>
<Text style={styles.sectionTitle}>12. Governing Law</Text>
<Text style={styles.paragraph}>
These Terms shall be governed by the laws of the State of California, United States,
without regard to conflict of law provisions.
</Text>
<Text style={styles.sectionTitle}>13. Contact Information</Text>
<Text style={styles.paragraph}>
For questions about these Terms, please contact us:
</Text>
<Text style={styles.contactInfo}>WellNuo Inc.</Text>
<Text style={styles.contactInfo}>Email: legal@wellnuo.com</Text>
<Text style={styles.contactInfo}>Address: 123 Care Street, San Francisco, CA 94102</Text>
<View style={styles.footer}>
<Text style={styles.footerText}>
By using WellNuo, you acknowledge that you have read, understood, and agree
to be bound by these Terms of Service.
</Text>
</View>
</View>
</ScrollView>
</SafeAreaView>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: AppColors.background,
},
content: {
padding: Spacing.lg,
},
lastUpdated: {
fontSize: FontSizes.sm,
color: AppColors.textMuted,
marginBottom: Spacing.lg,
textAlign: 'center',
},
sectionTitle: {
fontSize: FontSizes.lg,
fontWeight: '600',
color: AppColors.textPrimary,
marginTop: Spacing.lg,
marginBottom: Spacing.sm,
},
paragraph: {
fontSize: FontSizes.sm,
color: AppColors.textSecondary,
lineHeight: 22,
marginBottom: Spacing.sm,
},
bulletPoint: {
fontSize: FontSizes.sm,
color: AppColors.textSecondary,
lineHeight: 22,
marginLeft: Spacing.md,
marginBottom: 4,
},
contactInfo: {
fontSize: FontSizes.sm,
color: AppColors.textPrimary,
marginBottom: 4,
},
footer: {
marginTop: Spacing.xl,
paddingTop: Spacing.lg,
borderTopWidth: 1,
borderTopColor: AppColors.border,
},
footerText: {
fontSize: FontSizes.xs,
color: AppColors.textMuted,
textAlign: 'center',
fontStyle: 'italic',
},
});

View File

@ -23,44 +23,55 @@ export function BeneficiaryProvider({ children }: { children: React.ReactNode })
return ''; return '';
} }
const parts = [`[Context: Asking about ${currentBeneficiary.name}`]; const b = currentBeneficiary;
const contextParts: string[] = [];
if (currentBeneficiary.relationship) { // Basic info
parts.push(`(${currentBeneficiary.relationship})`); contextParts.push(`Person: ${b.name}`);
if (b.address) {
contextParts.push(`Address: ${b.address}`);
} }
if (currentBeneficiary.sensor_data) { // Current status
const sensor = currentBeneficiary.sensor_data; if (b.last_location) {
const sensorInfo: string[] = []; contextParts.push(`Current location: ${b.last_location}`);
if (sensor.motion_detected !== undefined) {
sensorInfo.push(`motion: ${sensor.motion_detected ? 'active' : 'inactive'}`);
}
if (sensor.last_motion) {
sensorInfo.push(`last motion: ${sensor.last_motion}`);
}
if (sensor.door_status) {
sensorInfo.push(`door: ${sensor.door_status}`);
}
if (sensor.temperature !== undefined) {
sensorInfo.push(`temp: ${sensor.temperature}°C`);
}
if (sensor.humidity !== undefined) {
sensorInfo.push(`humidity: ${sensor.humidity}%`);
}
if (sensorInfo.length > 0) {
parts.push(`| Sensors: ${sensorInfo.join(', ')}`);
}
} }
if (currentBeneficiary.last_activity) { if (b.before_last_location) {
parts.push(`| Last activity: ${currentBeneficiary.last_activity}`); contextParts.push(`Previous location: ${b.before_last_location}`);
} }
parts.push(']'); // Health metrics
if (b.wellness_score !== undefined) {
contextParts.push(`Wellness score: ${b.wellness_score}% (${b.wellness_descriptor || 'N/A'})`);
}
return parts.join(' '); // Temperature
if (b.temperature !== undefined) {
const unit = b.units || '°F';
contextParts.push(`Room temperature: ${b.temperature.toFixed(1)}${unit}`);
}
if (b.bedroom_temperature !== undefined) {
const unit = b.units || '°F';
contextParts.push(`Bedroom temperature: ${b.bedroom_temperature.toFixed(1)}${unit}`);
}
// Sleep data
if (b.sleep_hours !== undefined) {
contextParts.push(`Sleep hours: ${b.sleep_hours.toFixed(1)} hours`);
}
// Activity time
if (b.last_detected_time) {
contextParts.push(`Last detected: ${b.last_detected_time}`);
}
// Status
contextParts.push(`Status: ${b.status === 'online' ? 'Active' : 'Inactive'}`);
return `[SENSOR DATA FOR ${b.name.toUpperCase()}: ${contextParts.join('. ')}]`;
}, [currentBeneficiary]); }, [currentBeneficiary]);
return ( return (

View File

@ -19,7 +19,7 @@
"production": { "production": {
"ios": { "ios": {
"appleId": "serter2069@gmail.com", "appleId": "serter2069@gmail.com",
"ascAppId": "WILL_BE_SET_AFTER_FIRST_BUILD" "ascAppId": "6755984871"
} }
} }
} }

View File

@ -1,9 +1,23 @@
import * as SecureStore from 'expo-secure-store'; import * as SecureStore from 'expo-secure-store';
import type { AuthResponse, ChatResponse, Beneficiary, ApiResponse, ApiError } from '@/types'; import type { AuthResponse, ChatResponse, Beneficiary, ApiResponse, ApiError, DashboardSingleResponse, PatientDashboardData } from '@/types';
const API_BASE_URL = 'https://eluxnetworks.net/function/well-api/api'; const API_BASE_URL = 'https://eluxnetworks.net/function/well-api/api';
const CLIENT_ID = 'MA_001'; const CLIENT_ID = 'MA_001';
// Helper function to format time ago
function formatTimeAgo(date: Date): string {
const now = new Date();
const diffMs = now.getTime() - date.getTime();
const diffMins = Math.floor(diffMs / 60000);
const diffHours = Math.floor(diffMins / 60);
const diffDays = Math.floor(diffHours / 24);
if (diffMins < 1) return 'Just now';
if (diffMins < 60) return `${diffMins} min ago`;
if (diffHours < 24) return `${diffHours} hour${diffHours > 1 ? 's' : ''} ago`;
return `${diffDays} day${diffDays > 1 ? 's' : ''} ago`;
}
class ApiService { class ApiService {
private async getToken(): Promise<string | null> { private async getToken(): Promise<string | null> {
try { try {
@ -175,6 +189,85 @@ class ApiService {
return { data: beneficiary, ok: true }; return { data: beneficiary, ok: true };
} }
// Get patient dashboard data by deployment_id
async getPatientDashboard(deploymentId: string): Promise<ApiResponse<PatientDashboardData>> {
const token = await this.getToken();
const userName = await this.getUserName();
if (!token || !userName) {
return { ok: false, error: { message: 'Not authenticated', code: 'UNAUTHORIZED' } };
}
const today = new Date().toISOString().split('T')[0]; // YYYY-MM-DD
const response = await this.makeRequest<DashboardSingleResponse>({
function: 'dashboard_single',
user_name: userName,
token: token,
deployment_id: deploymentId,
date: today,
nonce: this.generateNonce(),
});
if (response.ok && response.data?.result_list?.[0]) {
return { data: response.data.result_list[0], ok: true };
}
return {
ok: false,
error: response.error || { message: 'Failed to get patient data' },
};
}
// Get all patients from privileges (deployment_ids)
async getAllPatients(): Promise<ApiResponse<Beneficiary[]>> {
const token = await this.getToken();
if (!token) {
return { ok: false, error: { message: 'Not authenticated', code: 'UNAUTHORIZED' } };
}
const privileges = await SecureStore.getItemAsync('privileges');
if (!privileges) {
return { ok: true, data: [] };
}
const deploymentIds = privileges.split(',').map(id => id.trim()).filter(id => id);
const patients: Beneficiary[] = [];
// Fetch data for each deployment_id
for (const deploymentId of deploymentIds) {
const response = await this.getPatientDashboard(deploymentId);
if (response.ok && response.data) {
const data = response.data;
// Determine if patient is "online" based on last_detected_time
const lastDetected = data.last_detected_time ? new Date(data.last_detected_time) : null;
const isRecent = lastDetected && (Date.now() - lastDetected.getTime()) < 30 * 60 * 1000; // 30 min
patients.push({
id: parseInt(data.deployment_id, 10),
name: data.name,
status: isRecent ? 'online' : 'offline',
address: data.address,
timezone: data.time_zone,
wellness_score: data.wellness_score_percent,
wellness_descriptor: data.wellness_descriptor,
last_location: data.last_location,
temperature: data.temperature,
units: data.units,
sleep_hours: data.sleep_hours,
bedroom_temperature: data.bedroom_temperature,
before_last_location: data.before_last_location,
last_detected_time: data.last_detected_time,
last_activity: data.last_detected_time
? formatTimeAgo(new Date(data.last_detected_time))
: undefined,
});
}
}
return { data: patients, ok: true };
}
// AI Chat // AI Chat
async sendMessage(question: string, deploymentId: string = '21'): Promise<ApiResponse<ChatResponse>> { async sendMessage(question: string, deploymentId: string = '21'): Promise<ApiResponse<ChatResponse>> {
const token = await this.getToken(); const token = await this.getToken();

View File

@ -29,6 +29,44 @@ export interface Beneficiary {
relationship?: string; relationship?: string;
last_activity?: string; last_activity?: string;
sensor_data?: SensorData; sensor_data?: SensorData;
// Extended data from dashboard_single API
address?: string;
timezone?: string;
wellness_score?: number;
wellness_descriptor?: string;
last_location?: string;
temperature?: number;
units?: string;
sleep_hours?: number;
bedroom_temperature?: number;
before_last_location?: string;
last_detected_time?: string;
}
// Dashboard API response
export interface DashboardSingleResponse {
result_list: PatientDashboardData[];
status: string;
}
export interface PatientDashboardData {
user_id: number;
name: string;
address: string;
time_zone: string;
picture: string;
deployment_id: string;
wellness_score_percent: number;
wellness_descriptor: string;
wellness_descriptor_color: string;
last_location: string;
last_detected_time: string;
before_last_location: string;
temperature: number;
bedroom_temperature: number;
sleep_hours: number;
units: string;
location_list: string[];
} }
export interface SensorData { export interface SensorData {