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
|
||||
/android
|
||||
.git-credentials
|
||||
wellnuoSheme/.history/
|
||||
|
||||
2
app.json
2
app.json
@ -10,7 +10,7 @@
|
||||
"newArchEnabled": true,
|
||||
"ios": {
|
||||
"supportsTablet": true,
|
||||
"bundleIdentifier": "com.wellnuo.app",
|
||||
"bundleIdentifier": "com.kosyakorel1.wellnuo",
|
||||
"infoPlist": {
|
||||
"ITSAppUsesNonExemptEncryption": false
|
||||
}
|
||||
|
||||
@ -58,9 +58,11 @@ export default function LoginScreen() {
|
||||
>
|
||||
{/* Logo / Header */}
|
||||
<View style={styles.header}>
|
||||
<View style={styles.logoContainer}>
|
||||
<Text style={styles.logoText}>WellNuo</Text>
|
||||
</View>
|
||||
<Image
|
||||
source={require('@/assets/images/icon.png')}
|
||||
style={styles.logo}
|
||||
resizeMode="contain"
|
||||
/>
|
||||
<Text style={styles.title}>Welcome Back</Text>
|
||||
<Text style={styles.subtitle}>Sign in to continue monitoring your loved ones</Text>
|
||||
</View>
|
||||
@ -149,20 +151,11 @@ const styles = StyleSheet.create({
|
||||
alignItems: 'center',
|
||||
marginBottom: Spacing.xl,
|
||||
},
|
||||
logoContainer: {
|
||||
width: 80,
|
||||
height: 80,
|
||||
borderRadius: BorderRadius.xl,
|
||||
backgroundColor: AppColors.primary,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
logo: {
|
||||
width: 180,
|
||||
height: 100,
|
||||
marginBottom: Spacing.lg,
|
||||
},
|
||||
logoText: {
|
||||
fontSize: FontSizes.lg,
|
||||
fontWeight: '700',
|
||||
color: AppColors.white,
|
||||
},
|
||||
title: {
|
||||
fontSize: FontSizes['2xl'],
|
||||
fontWeight: '700',
|
||||
|
||||
@ -35,7 +35,7 @@ export default function TabLayout() {
|
||||
options={{
|
||||
title: 'Dashboard',
|
||||
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 { FullScreenError } from '@/components/ui/ErrorMessage';
|
||||
|
||||
// Start with login page, then redirect to dashboard after auth
|
||||
const LOGIN_URL = 'https://react.eluxnetworks.net/login';
|
||||
const DASHBOARD_URL = 'https://react.eluxnetworks.net/dashboard';
|
||||
// Dashboard URL with patient ID
|
||||
const getDashboardUrl = (deploymentId: string) =>
|
||||
`https://react.eluxnetworks.net/dashboard/${deploymentId}`;
|
||||
|
||||
export default function BeneficiaryDashboardScreen() {
|
||||
const { id } = useLocalSearchParams<{ id: string }>();
|
||||
@ -24,7 +24,9 @@ export default function BeneficiaryDashboardScreen() {
|
||||
const [userName, setUserName] = useState<string | null>(null);
|
||||
const [userId, setUserId] = useState<string | null>(null);
|
||||
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';
|
||||
|
||||
@ -169,7 +171,7 @@ export default function BeneficiaryDashboardScreen() {
|
||||
<View style={styles.webViewContainer}>
|
||||
<WebView
|
||||
ref={webViewRef}
|
||||
source={{ uri: webViewUrl }}
|
||||
source={{ uri: dashboardUrl }}
|
||||
style={styles.webView}
|
||||
onLoadStart={() => setIsLoading(true)}
|
||||
onLoadEnd={() => setIsLoading(false)}
|
||||
|
||||
@ -52,7 +52,9 @@ export default function ChatScreen() {
|
||||
? `${beneficiaryContext} ${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) {
|
||||
const assistantMessage: Message = {
|
||||
|
||||
@ -1,124 +1,145 @@
|
||||
import React, { useState, useRef, useEffect } from 'react';
|
||||
import { View, Text, StyleSheet, ActivityIndicator, TouchableOpacity } from 'react-native';
|
||||
import { WebView } from 'react-native-webview';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
View,
|
||||
Text,
|
||||
StyleSheet,
|
||||
FlatList,
|
||||
TouchableOpacity,
|
||||
ActivityIndicator,
|
||||
RefreshControl
|
||||
} from 'react-native';
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
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 { 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() {
|
||||
const { user } = useAuth();
|
||||
const webViewRef = useRef<WebView>(null);
|
||||
const { currentBeneficiary, setCurrentBeneficiary } = useBeneficiary();
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [isRefreshing, setIsRefreshing] = useState(false);
|
||||
const [patients, setPatients] = useState<Beneficiary[]>([]);
|
||||
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(() => {
|
||||
const loadCredentials = async () => {
|
||||
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();
|
||||
loadPatients();
|
||||
}, []);
|
||||
|
||||
// JavaScript to inject auth token into localStorage
|
||||
// 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);
|
||||
const loadPatients = async () => {
|
||||
setIsLoading(true);
|
||||
webViewRef.current?.reload();
|
||||
};
|
||||
|
||||
const handleWebViewBack = () => {
|
||||
if (canGoBack) {
|
||||
webViewRef.current?.goBack();
|
||||
setError(null);
|
||||
try {
|
||||
const response = await api.getAllPatients();
|
||||
if (response.ok && response.data) {
|
||||
setPatients(response.data);
|
||||
// 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) => {
|
||||
setCanGoBack(navState.canGoBack);
|
||||
const handleRefresh = async () => {
|
||||
setIsRefreshing(true);
|
||||
await loadPatients();
|
||||
setIsRefreshing(false);
|
||||
};
|
||||
|
||||
const handleError = () => {
|
||||
setError('Failed to load dashboard. Please check your internet connection.');
|
||||
setIsLoading(false);
|
||||
const handlePatientPress = (patient: Beneficiary) => {
|
||||
// Set current beneficiary in context
|
||||
setCurrentBeneficiary(patient);
|
||||
// Navigate to patient dashboard with deployment_id
|
||||
router.push(`/(tabs)/beneficiaries/${patient.id}/dashboard`);
|
||||
};
|
||||
|
||||
// Wait for token to load
|
||||
if (!isTokenLoaded) {
|
||||
if (isLoading) {
|
||||
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>
|
||||
<Text style={styles.headerTitle}>My Beneficiaries</Text>
|
||||
</View>
|
||||
</View>
|
||||
<View style={styles.loadingContainer}>
|
||||
<ActivityIndicator size="large" color={AppColors.primary} />
|
||||
<Text style={styles.loadingText}>Preparing dashboard...</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>
|
||||
<Text style={styles.loadingText}>Loading patients...</Text>
|
||||
</View>
|
||||
</SafeAreaView>
|
||||
);
|
||||
@ -130,52 +151,42 @@ export default function HomeScreen() {
|
||||
<View style={styles.header}>
|
||||
<View>
|
||||
<Text style={styles.greeting}>Hello, {user?.user_name || 'User'}</Text>
|
||||
<Text style={styles.headerTitle}>Dashboard</Text>
|
||||
</View>
|
||||
<View style={styles.headerActions}>
|
||||
{canGoBack && (
|
||||
<TouchableOpacity style={styles.actionButton} onPress={handleWebViewBack}>
|
||||
<Ionicons name="chevron-back" size={22} color={AppColors.primary} />
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
<TouchableOpacity style={styles.actionButton} onPress={handleRefresh}>
|
||||
<Ionicons name="refresh" size={22} color={AppColors.primary} />
|
||||
</TouchableOpacity>
|
||||
<Text style={styles.headerTitle}>My Beneficiaries</Text>
|
||||
</View>
|
||||
<TouchableOpacity style={styles.refreshButton} onPress={handleRefresh}>
|
||||
<Ionicons name="refresh" size={22} color={AppColors.primary} />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
{/* WebView Dashboard */}
|
||||
<View style={styles.webViewContainer}>
|
||||
<WebView
|
||||
ref={webViewRef}
|
||||
source={{ uri: DASHBOARD_URL }}
|
||||
style={styles.webView}
|
||||
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>
|
||||
{/* Patient List */}
|
||||
{patients.length === 0 ? (
|
||||
<View style={styles.emptyContainer}>
|
||||
<Ionicons name="people-outline" size={64} color={AppColors.textMuted} />
|
||||
<Text style={styles.emptyTitle}>No Patients</Text>
|
||||
<Text style={styles.emptyText}>You don't have any patients assigned yet.</Text>
|
||||
</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>
|
||||
);
|
||||
}
|
||||
@ -204,76 +215,151 @@ const styles = StyleSheet.create({
|
||||
fontWeight: '700',
|
||||
color: AppColors.textPrimary,
|
||||
},
|
||||
headerActions: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
},
|
||||
actionButton: {
|
||||
padding: Spacing.xs,
|
||||
marginLeft: Spacing.xs,
|
||||
},
|
||||
refreshButton: {
|
||||
padding: Spacing.xs,
|
||||
},
|
||||
webViewContainer: {
|
||||
flex: 1,
|
||||
},
|
||||
webView: {
|
||||
flex: 1,
|
||||
},
|
||||
loadingContainer: {
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
flex: 1,
|
||||
justifyContent: '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: {
|
||||
marginTop: Spacing.md,
|
||||
fontSize: FontSizes.base,
|
||||
color: AppColors.textSecondary,
|
||||
},
|
||||
errorContainer: {
|
||||
emptyContainer: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
padding: Spacing.xl,
|
||||
},
|
||||
errorTitle: {
|
||||
emptyTitle: {
|
||||
fontSize: FontSizes.lg,
|
||||
fontWeight: '600',
|
||||
color: AppColors.textPrimary,
|
||||
marginTop: Spacing.md,
|
||||
},
|
||||
errorText: {
|
||||
emptyText: {
|
||||
fontSize: FontSizes.base,
|
||||
color: AppColors.textSecondary,
|
||||
textAlign: 'center',
|
||||
marginTop: Spacing.xs,
|
||||
},
|
||||
retryButton: {
|
||||
marginTop: Spacing.lg,
|
||||
paddingHorizontal: Spacing.xl,
|
||||
paddingVertical: Spacing.md,
|
||||
backgroundColor: AppColors.primary,
|
||||
borderRadius: 8,
|
||||
listContent: {
|
||||
padding: Spacing.lg,
|
||||
paddingBottom: Spacing.xxl,
|
||||
},
|
||||
retryButtonText: {
|
||||
color: AppColors.white,
|
||||
fontSize: FontSizes.base,
|
||||
// Card styles
|
||||
card: {
|
||||
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',
|
||||
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 {
|
||||
View,
|
||||
Text,
|
||||
@ -6,6 +6,7 @@ import {
|
||||
ScrollView,
|
||||
TouchableOpacity,
|
||||
Alert,
|
||||
Switch,
|
||||
} from 'react-native';
|
||||
import { router } from 'expo-router';
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
@ -21,6 +22,7 @@ interface MenuItemProps {
|
||||
subtitle?: string;
|
||||
onPress?: () => void;
|
||||
showChevron?: boolean;
|
||||
rightElement?: React.ReactNode;
|
||||
}
|
||||
|
||||
function MenuItem({
|
||||
@ -31,9 +33,10 @@ function MenuItem({
|
||||
subtitle,
|
||||
onPress,
|
||||
showChevron = true,
|
||||
rightElement,
|
||||
}: MenuItemProps) {
|
||||
return (
|
||||
<TouchableOpacity style={styles.menuItem} onPress={onPress}>
|
||||
<TouchableOpacity style={styles.menuItem} onPress={onPress} disabled={!onPress}>
|
||||
<View style={[styles.menuIconContainer, { backgroundColor: iconBgColor }]}>
|
||||
<Ionicons name={icon} size={20} color={iconColor} />
|
||||
</View>
|
||||
@ -41,7 +44,7 @@ function MenuItem({
|
||||
<Text style={styles.menuTitle}>{title}</Text>
|
||||
{subtitle && <Text style={styles.menuSubtitle}>{subtitle}</Text>}
|
||||
</View>
|
||||
{showChevron && (
|
||||
{rightElement ? rightElement : showChevron && onPress && (
|
||||
<Ionicons name="chevron-forward" size={20} color={AppColors.textMuted} />
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
@ -51,6 +54,12 @@ function MenuItem({
|
||||
export default function ProfileScreen() {
|
||||
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 = () => {
|
||||
Alert.alert(
|
||||
'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 (
|
||||
<SafeAreaView style={styles.container} edges={['top']}>
|
||||
<ScrollView showsVerticalScrollIndicator={false}>
|
||||
@ -88,15 +125,34 @@ export default function ProfileScreen() {
|
||||
<View style={styles.userInfo}>
|
||||
<Text style={styles.userName}>{user?.user_name || 'User'}</Text>
|
||||
<Text style={styles.userRole}>
|
||||
Role: {user?.max_role === 2 ? 'Admin' : 'User'}
|
||||
{user?.max_role === 2 ? 'Administrator' : 'Caregiver'}
|
||||
</Text>
|
||||
<Text style={styles.userId}>ID: {user?.user_id || 'N/A'}</Text>
|
||||
</View>
|
||||
<TouchableOpacity style={styles.editButton}>
|
||||
<TouchableOpacity style={styles.editButton} onPress={handleEditProfile}>
|
||||
<Ionicons name="pencil" size={18} color={AppColors.primary} />
|
||||
</TouchableOpacity>
|
||||
</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}>
|
||||
<Text style={styles.sectionTitle}>Account</Text>
|
||||
<View style={styles.menuCard}>
|
||||
@ -104,6 +160,7 @@ export default function ProfileScreen() {
|
||||
icon="person-outline"
|
||||
title="Edit Profile"
|
||||
subtitle="Update your personal information"
|
||||
onPress={handleEditProfile}
|
||||
/>
|
||||
<View style={styles.menuDivider} />
|
||||
<MenuItem
|
||||
@ -112,6 +169,7 @@ export default function ProfileScreen() {
|
||||
iconColor={AppColors.warning}
|
||||
title="Notifications"
|
||||
subtitle="Manage notification preferences"
|
||||
onPress={handleNotifications}
|
||||
/>
|
||||
<View style={styles.menuDivider} />
|
||||
<MenuItem
|
||||
@ -120,10 +178,104 @@ export default function ProfileScreen() {
|
||||
iconColor={AppColors.success}
|
||||
title="Privacy & Security"
|
||||
subtitle="Password, 2FA, data"
|
||||
onPress={handlePrivacy}
|
||||
/>
|
||||
</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}>
|
||||
<Text style={styles.sectionTitle}>Subscription</Text>
|
||||
<View style={styles.menuCard}>
|
||||
@ -133,16 +285,19 @@ export default function ProfileScreen() {
|
||||
iconColor="#9333EA"
|
||||
title="WellNuo Pro"
|
||||
subtitle="Upgrade for premium features"
|
||||
onPress={handleUpgrade}
|
||||
/>
|
||||
<View style={styles.menuDivider} />
|
||||
<MenuItem
|
||||
icon="card-outline"
|
||||
title="Payment Methods"
|
||||
subtitle="Manage your payment options"
|
||||
onPress={handlePayment}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* Support */}
|
||||
<View style={styles.section}>
|
||||
<Text style={styles.sectionTitle}>Support</Text>
|
||||
<View style={styles.menuCard}>
|
||||
@ -150,22 +305,50 @@ export default function ProfileScreen() {
|
||||
icon="help-circle-outline"
|
||||
title="Help Center"
|
||||
subtitle="FAQs and guides"
|
||||
onPress={handleHelp}
|
||||
/>
|
||||
<View style={styles.menuDivider} />
|
||||
<MenuItem
|
||||
icon="chatbubble-outline"
|
||||
title="Contact Support"
|
||||
subtitle="Get help from our team"
|
||||
onPress={handleSupport}
|
||||
/>
|
||||
<View style={styles.menuDivider} />
|
||||
<MenuItem
|
||||
icon="document-text-outline"
|
||||
title="Terms of Service"
|
||||
onPress={handleTerms}
|
||||
/>
|
||||
<View style={styles.menuDivider} />
|
||||
<MenuItem
|
||||
icon="shield-outline"
|
||||
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>
|
||||
@ -179,7 +362,7 @@ export default function ProfileScreen() {
|
||||
</View>
|
||||
|
||||
{/* Version */}
|
||||
<Text style={styles.version}>WellNuo v1.0.0</Text>
|
||||
<Text style={styles.version}>WellNuo v1.0.0 (Expo SDK 54)</Text>
|
||||
</ScrollView>
|
||||
</SafeAreaView>
|
||||
);
|
||||
@ -207,7 +390,6 @@ const styles = StyleSheet.create({
|
||||
alignItems: 'center',
|
||||
backgroundColor: AppColors.background,
|
||||
padding: Spacing.lg,
|
||||
marginBottom: Spacing.md,
|
||||
},
|
||||
avatarContainer: {
|
||||
width: 64,
|
||||
@ -234,7 +416,12 @@ const styles = StyleSheet.create({
|
||||
userRole: {
|
||||
fontSize: FontSizes.sm,
|
||||
color: AppColors.textSecondary,
|
||||
marginTop: Spacing.xs,
|
||||
marginTop: 2,
|
||||
},
|
||||
userId: {
|
||||
fontSize: FontSizes.xs,
|
||||
color: AppColors.textMuted,
|
||||
marginTop: 2,
|
||||
},
|
||||
editButton: {
|
||||
width: 40,
|
||||
@ -244,6 +431,34 @@ const styles = StyleSheet.create({
|
||||
justifyContent: '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: {
|
||||
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 '';
|
||||
}
|
||||
|
||||
const parts = [`[Context: Asking about ${currentBeneficiary.name}`];
|
||||
const b = currentBeneficiary;
|
||||
const contextParts: string[] = [];
|
||||
|
||||
if (currentBeneficiary.relationship) {
|
||||
parts.push(`(${currentBeneficiary.relationship})`);
|
||||
// Basic info
|
||||
contextParts.push(`Person: ${b.name}`);
|
||||
|
||||
if (b.address) {
|
||||
contextParts.push(`Address: ${b.address}`);
|
||||
}
|
||||
|
||||
if (currentBeneficiary.sensor_data) {
|
||||
const sensor = currentBeneficiary.sensor_data;
|
||||
const sensorInfo: string[] = [];
|
||||
|
||||
if (sensor.motion_detected !== undefined) {
|
||||
sensorInfo.push(`motion: ${sensor.motion_detected ? 'active' : 'inactive'}`);
|
||||
}
|
||||
if (sensor.last_motion) {
|
||||
sensorInfo.push(`last motion: ${sensor.last_motion}`);
|
||||
}
|
||||
if (sensor.door_status) {
|
||||
sensorInfo.push(`door: ${sensor.door_status}`);
|
||||
}
|
||||
if (sensor.temperature !== undefined) {
|
||||
sensorInfo.push(`temp: ${sensor.temperature}°C`);
|
||||
}
|
||||
if (sensor.humidity !== undefined) {
|
||||
sensorInfo.push(`humidity: ${sensor.humidity}%`);
|
||||
}
|
||||
|
||||
if (sensorInfo.length > 0) {
|
||||
parts.push(`| Sensors: ${sensorInfo.join(', ')}`);
|
||||
}
|
||||
// Current status
|
||||
if (b.last_location) {
|
||||
contextParts.push(`Current location: ${b.last_location}`);
|
||||
}
|
||||
|
||||
if (currentBeneficiary.last_activity) {
|
||||
parts.push(`| Last activity: ${currentBeneficiary.last_activity}`);
|
||||
if (b.before_last_location) {
|
||||
contextParts.push(`Previous location: ${b.before_last_location}`);
|
||||
}
|
||||
|
||||
parts.push(']');
|
||||
// Health metrics
|
||||
if (b.wellness_score !== undefined) {
|
||||
contextParts.push(`Wellness score: ${b.wellness_score}% (${b.wellness_descriptor || 'N/A'})`);
|
||||
}
|
||||
|
||||
return parts.join(' ');
|
||||
// Temperature
|
||||
if (b.temperature !== undefined) {
|
||||
const unit = b.units || '°F';
|
||||
contextParts.push(`Room temperature: ${b.temperature.toFixed(1)}${unit}`);
|
||||
}
|
||||
|
||||
if (b.bedroom_temperature !== undefined) {
|
||||
const unit = b.units || '°F';
|
||||
contextParts.push(`Bedroom temperature: ${b.bedroom_temperature.toFixed(1)}${unit}`);
|
||||
}
|
||||
|
||||
// Sleep data
|
||||
if (b.sleep_hours !== undefined) {
|
||||
contextParts.push(`Sleep hours: ${b.sleep_hours.toFixed(1)} hours`);
|
||||
}
|
||||
|
||||
// Activity time
|
||||
if (b.last_detected_time) {
|
||||
contextParts.push(`Last detected: ${b.last_detected_time}`);
|
||||
}
|
||||
|
||||
// Status
|
||||
contextParts.push(`Status: ${b.status === 'online' ? 'Active' : 'Inactive'}`);
|
||||
|
||||
return `[SENSOR DATA FOR ${b.name.toUpperCase()}: ${contextParts.join('. ')}]`;
|
||||
}, [currentBeneficiary]);
|
||||
|
||||
return (
|
||||
|
||||
2
eas.json
2
eas.json
@ -19,7 +19,7 @@
|
||||
"production": {
|
||||
"ios": {
|
||||
"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 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 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 {
|
||||
private async getToken(): Promise<string | null> {
|
||||
try {
|
||||
@ -175,6 +189,85 @@ class ApiService {
|
||||
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
|
||||
async sendMessage(question: string, deploymentId: string = '21'): Promise<ApiResponse<ChatResponse>> {
|
||||
const token = await this.getToken();
|
||||
|
||||
@ -29,6 +29,44 @@ export interface Beneficiary {
|
||||
relationship?: string;
|
||||
last_activity?: string;
|
||||
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 {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user