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:
parent
68b89d2565
commit
48384f07c5
1
.gitignore
vendored
1
.gitignore
vendored
@ -42,3 +42,4 @@ app-example
|
|||||||
/ios
|
/ios
|
||||||
/android
|
/android
|
||||||
.git-credentials
|
.git-credentials
|
||||||
|
wellnuoSheme/.history/
|
||||||
|
|||||||
2
app.json
2
app.json
@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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',
|
||||||
|
|||||||
@ -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} />
|
||||||
),
|
),
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -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)}
|
||||||
|
|||||||
@ -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 = {
|
||||||
|
|||||||
@ -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>
|
||||||
<View style={styles.headerActions}>
|
<TouchableOpacity style={styles.refreshButton} onPress={handleRefresh}>
|
||||||
{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} />
|
<Ionicons name="refresh" size={22} color={AppColors.primary} />
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</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)}
|
|
||||||
onLoadEnd={() => setIsLoading(false)}
|
|
||||||
onError={handleError}
|
|
||||||
onHttpError={handleError}
|
|
||||||
onNavigationStateChange={handleNavigationStateChange}
|
|
||||||
javaScriptEnabled={true}
|
|
||||||
domStorageEnabled={true}
|
|
||||||
startInLoadingState={true}
|
|
||||||
scalesPageToFit={true}
|
|
||||||
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>
|
</View>
|
||||||
)}
|
) : (
|
||||||
|
<FlatList
|
||||||
|
data={patients}
|
||||||
|
keyExtractor={(item) => item.id.toString()}
|
||||||
|
renderItem={({ item }) => (
|
||||||
|
<PatientCard
|
||||||
|
patient={item}
|
||||||
|
onPress={() => handlePatientPress(item)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
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,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@ -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
81
app/profile/_layout.tsx
Normal 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
358
app/profile/about.tsx
Normal 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
325
app/profile/edit.tsx
Normal 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
419
app/profile/help.tsx
Normal 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
336
app/profile/language.tsx
Normal 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,
|
||||||
|
},
|
||||||
|
});
|
||||||
380
app/profile/notifications.tsx
Normal file
380
app/profile/notifications.tsx
Normal 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,
|
||||||
|
},
|
||||||
|
});
|
||||||
331
app/profile/privacy-policy.tsx
Normal file
331
app/profile/privacy-policy.tsx
Normal 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
560
app/profile/privacy.tsx
Normal 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,
|
||||||
|
},
|
||||||
|
});
|
||||||
517
app/profile/subscription.tsx
Normal file
517
app/profile/subscription.tsx
Normal 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
475
app/profile/support.tsx
Normal 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
197
app/profile/terms.tsx
Normal 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',
|
||||||
|
},
|
||||||
|
});
|
||||||
@ -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) {
|
if (b.before_last_location) {
|
||||||
parts.push(`| Sensors: ${sensorInfo.join(', ')}`);
|
contextParts.push(`Previous location: ${b.before_last_location}`);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (currentBeneficiary.last_activity) {
|
// Health metrics
|
||||||
parts.push(`| Last activity: ${currentBeneficiary.last_activity}`);
|
if (b.wellness_score !== undefined) {
|
||||||
|
contextParts.push(`Wellness score: ${b.wellness_score}% (${b.wellness_descriptor || 'N/A'})`);
|
||||||
}
|
}
|
||||||
|
|
||||||
parts.push(']');
|
// Temperature
|
||||||
|
if (b.temperature !== undefined) {
|
||||||
|
const unit = b.units || '°F';
|
||||||
|
contextParts.push(`Room temperature: ${b.temperature.toFixed(1)}${unit}`);
|
||||||
|
}
|
||||||
|
|
||||||
return parts.join(' ');
|
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 (
|
||||||
|
|||||||
2
eas.json
2
eas.json
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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();
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user