From 48384f07c53e95b472dfc0c067fb527b56d86928 Mon Sep 17 00:00:00 2001 From: Sergei Date: Fri, 12 Dec 2025 16:26:13 -0800 Subject: [PATCH] Full project sync - app updates and profile screens MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .gitignore | 1 + app.json | 2 +- app/(auth)/login.tsx | 23 +- app/(tabs)/_layout.tsx | 2 +- app/(tabs)/beneficiaries/[id]/dashboard.tsx | 12 +- app/(tabs)/chat.tsx | 4 +- app/(tabs)/index.tsx | 434 +++++++++------ app/(tabs)/profile.tsx | 233 +++++++- app/profile/_layout.tsx | 81 +++ app/profile/about.tsx | 358 +++++++++++++ app/profile/edit.tsx | 325 ++++++++++++ app/profile/help.tsx | 419 +++++++++++++++ app/profile/language.tsx | 336 ++++++++++++ app/profile/notifications.tsx | 380 +++++++++++++ app/profile/privacy-policy.tsx | 331 ++++++++++++ app/profile/privacy.tsx | 560 ++++++++++++++++++++ app/profile/subscription.tsx | 517 ++++++++++++++++++ app/profile/support.tsx | 475 +++++++++++++++++ app/profile/terms.tsx | 197 +++++++ contexts/BeneficiaryContext.tsx | 71 +-- eas.json | 2 +- services/api.ts | 95 +++- types/index.ts | 38 ++ 23 files changed, 4658 insertions(+), 238 deletions(-) create mode 100644 app/profile/_layout.tsx create mode 100644 app/profile/about.tsx create mode 100644 app/profile/edit.tsx create mode 100644 app/profile/help.tsx create mode 100644 app/profile/language.tsx create mode 100644 app/profile/notifications.tsx create mode 100644 app/profile/privacy-policy.tsx create mode 100644 app/profile/privacy.tsx create mode 100644 app/profile/subscription.tsx create mode 100644 app/profile/support.tsx create mode 100644 app/profile/terms.tsx diff --git a/.gitignore b/.gitignore index 2a6edee..05890c8 100644 --- a/.gitignore +++ b/.gitignore @@ -42,3 +42,4 @@ app-example /ios /android .git-credentials +wellnuoSheme/.history/ diff --git a/app.json b/app.json index 832bbed..2ebd056 100644 --- a/app.json +++ b/app.json @@ -10,7 +10,7 @@ "newArchEnabled": true, "ios": { "supportsTablet": true, - "bundleIdentifier": "com.wellnuo.app", + "bundleIdentifier": "com.kosyakorel1.wellnuo", "infoPlist": { "ITSAppUsesNonExemptEncryption": false } diff --git a/app/(auth)/login.tsx b/app/(auth)/login.tsx index ed77ae5..e9e0e11 100644 --- a/app/(auth)/login.tsx +++ b/app/(auth)/login.tsx @@ -58,9 +58,11 @@ export default function LoginScreen() { > {/* Logo / Header */} - - WellNuo - + Welcome Back Sign in to continue monitoring your loved ones @@ -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', diff --git a/app/(tabs)/_layout.tsx b/app/(tabs)/_layout.tsx index 84b0b88..1a14824 100644 --- a/app/(tabs)/_layout.tsx +++ b/app/(tabs)/_layout.tsx @@ -35,7 +35,7 @@ export default function TabLayout() { options={{ title: 'Dashboard', tabBarIcon: ({ color, size }) => ( - + ), }} /> diff --git a/app/(tabs)/beneficiaries/[id]/dashboard.tsx b/app/(tabs)/beneficiaries/[id]/dashboard.tsx index 22facf7..5e1c283 100644 --- a/app/(tabs)/beneficiaries/[id]/dashboard.tsx +++ b/app/(tabs)/beneficiaries/[id]/dashboard.tsx @@ -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(null); const [userId, setUserId] = useState(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() { setIsLoading(true)} onLoadEnd={() => setIsLoading(false)} diff --git a/app/(tabs)/chat.tsx b/app/(tabs)/chat.tsx index 98033dc..411be98 100644 --- a/app/(tabs)/chat.tsx +++ b/app/(tabs)/chat.tsx @@ -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 = { diff --git a/app/(tabs)/index.tsx b/app/(tabs)/index.tsx index e1fe67d..f3a38e7 100644 --- a/app/(tabs)/index.tsx +++ b/app/(tabs)/index.tsx @@ -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 ( + + + {/* Avatar */} + + + {patient.name.charAt(0).toUpperCase()} + + {isOnline && } + + + {/* Info */} + + {patient.name} + {patient.last_location && ( + + + {patient.last_location} + + )} + + + + {isOnline ? 'Active' : 'Inactive'} + + + {patient.last_activity && ( + {patient.last_activity} + )} + + + + {/* Wellness Score */} + {patient.wellness_score !== undefined && ( + + + {patient.wellness_score}% + + Wellness + + )} + + {/* Arrow */} + + + + ); +} export default function HomeScreen() { const { user } = useAuth(); - const webViewRef = useRef(null); + const { currentBeneficiary, setCurrentBeneficiary } = useBeneficiary(); const [isLoading, setIsLoading] = useState(true); + const [isRefreshing, setIsRefreshing] = useState(false); + const [patients, setPatients] = useState([]); const [error, setError] = useState(null); - const [canGoBack, setCanGoBack] = useState(false); - const [authToken, setAuthToken] = useState(null); - const [userName, setUserName] = useState(null); - const [userId, setUserId] = useState(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 ( Hello, {user?.user_name || 'User'} - Dashboard + My Beneficiaries - Preparing dashboard... - - - ); - } - - if (error) { - return ( - - - - Hello, {user?.user_name || 'User'} - Dashboard - - - - - - - - Connection Error - {error} - - Try Again - + Loading patients... ); @@ -130,52 +151,42 @@ export default function HomeScreen() { Hello, {user?.user_name || 'User'} - Dashboard - - - {canGoBack && ( - - - - )} - - - + My Beneficiaries + + + - {/* WebView Dashboard */} - - 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={() => ( - - - Loading dashboard... - + {/* Patient List */} + {patients.length === 0 ? ( + + + No Patients + You don't have any patients assigned yet. + + ) : ( + item.id.toString()} + renderItem={({ item }) => ( + handlePatientPress(item)} + /> )} + contentContainerStyle={styles.listContent} + showsVerticalScrollIndicator={false} + refreshControl={ + + } /> - - {isLoading && ( - - - - )} - + )} ); } @@ -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, }, }); diff --git a/app/(tabs)/profile.tsx b/app/(tabs)/profile.tsx index 095a28a..2f9b61b 100644 --- a/app/(tabs)/profile.tsx +++ b/app/(tabs)/profile.tsx @@ -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 ( - + @@ -41,7 +44,7 @@ function MenuItem({ {title} {subtitle && {subtitle}} - {showChevron && ( + {rightElement ? rightElement : showChevron && onPress && ( )} @@ -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 ( @@ -88,15 +125,34 @@ export default function ProfileScreen() { {user?.user_name || 'User'} - Role: {user?.max_role === 2 ? 'Admin' : 'User'} + {user?.max_role === 2 ? 'Administrator' : 'Caregiver'} + ID: {user?.user_id || 'N/A'} - + - {/* Menu Sections */} + {/* Quick Stats */} + + + {user?.privileges?.split(',').length || 0} + Beneficiaries + + + + 24/7 + Monitoring + + + + Free + Plan + + + + {/* Account Section */} Account @@ -104,6 +160,7 @@ export default function ProfileScreen() { icon="person-outline" title="Edit Profile" subtitle="Update your personal information" + onPress={handleEditProfile} /> + {/* App Settings */} + + App Settings + + + } + /> + + + } + /> + + { + 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'} + /> + } + /> + + { + setBiometricLogin(value); + if (value) { + Alert.alert('Biometric Login', 'Biometric authentication enabled!'); + } + }} + trackColor={{ false: '#E5E7EB', true: AppColors.primaryLight }} + thumbColor={biometricLogin ? AppColors.primary : '#9CA3AF'} + /> + } + /> + + + + + + {/* Subscription */} Subscription @@ -133,16 +285,19 @@ export default function ProfileScreen() { iconColor="#9333EA" title="WellNuo Pro" subtitle="Upgrade for premium features" + onPress={handleUpgrade} /> + {/* Support */} Support @@ -150,22 +305,50 @@ export default function ProfileScreen() { icon="help-circle-outline" title="Help Center" subtitle="FAQs and guides" + onPress={handleHelp} /> + + + + {/* About */} + + About + + + + @@ -179,7 +362,7 @@ export default function ProfileScreen() { {/* Version */} - WellNuo v1.0.0 + WellNuo v1.0.0 (Expo SDK 54) ); @@ -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, }, diff --git a/app/profile/_layout.tsx b/app/profile/_layout.tsx new file mode 100644 index 0000000..1a82841 --- /dev/null +++ b/app/profile/_layout.tsx @@ -0,0 +1,81 @@ +import { Stack } from 'expo-router'; +import { AppColors } from '@/constants/theme'; + +export default function ProfileLayout() { + return ( + + + + + + + + + + + + + ); +} diff --git a/app/profile/about.tsx b/app/profile/about.tsx new file mode 100644 index 0000000..d68f6f2 --- /dev/null +++ b/app/profile/about.tsx @@ -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 ( + + {label} + {value} + + ); +} + +interface LinkRowProps { + icon: keyof typeof Ionicons.glyphMap; + title: string; + onPress: () => void; +} + +function LinkRow({ icon, title, onPress }: LinkRowProps) { + return ( + + + {title} + + + ); +} + +export default function AboutScreen() { + const openURL = (url: string) => { + Linking.openURL(url).catch(() => {}); + }; + + return ( + + + {/* App Logo & Name */} + + + + + WellNuo + Caring for Those Who Matter Most + + + {/* Version Info */} + + App Information + + + + + + + + + + + + + + {/* Description */} + + About + + + 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. + + + Our mission is to bring peace of mind to families while preserving the independence + and dignity of elderly individuals. + + + + + {/* Features */} + + Key Features + + + + + + + Real-time Monitoring + 24/7 activity and wellness tracking + + + + + + + + Emergency Alerts + Instant notifications for falls and emergencies + + + + + + + + AI-Powered Insights + Smart analysis of health patterns + + + + + + + + Family Coordination + Share care with multiple caregivers + + + + + + {/* Links */} + + Resources + + openURL('https://wellnuo.com')} + /> + + openURL('https://docs.wellnuo.com')} + /> + + openURL('https://twitter.com/wellnuo')} + /> + + openURL('https://github.com/wellnuo')} + /> + + + + {/* Acknowledgments */} + + Acknowledgments + + + WellNuo uses the following open-source software: + + • React Native (MIT License) + • Expo (MIT License) + • React Navigation (MIT License) + • And many other wonderful packages + openURL('https://wellnuo.com/licenses')} + > + View All Licenses + + + + + {/* Footer */} + + © 2024 WellNuo Inc. + All rights reserved. + + Made with ❤️ for families worldwide + + + + + ); +} + +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, + }, +}); diff --git a/app/profile/edit.tsx b/app/profile/edit.tsx new file mode 100644 index 0000000..c506506 --- /dev/null +++ b/app/profile/edit.tsx @@ -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 ( + + + + {/* Avatar Section */} + + + + {displayName?.charAt(0).toUpperCase() || 'U'} + + + + + + Tap to change photo + + + {/* Form */} + + + Display Name * + + + + + Email Address + + Used for notifications and account recovery + + + + Phone Number + + For emergency contact and SMS alerts + + + + Timezone + { + 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' }, + ] + ); + }} + > + {timezone} + + + + + {/* Account Info (read-only) */} + + Account Information + + + User ID + {user?.user_id || 'N/A'} + + + + Username + {user?.user_name || 'N/A'} + + + + Role + + {user?.max_role === 2 ? 'Administrator' : 'Caregiver'} + + + + + Assigned Beneficiaries + + {user?.privileges?.split(',').length || 0} + + + + + + + {/* Save Button */} + + + + {isSaving ? 'Saving...' : 'Save Changes'} + + + + + + ); +} + +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, + }, +}); diff --git a/app/profile/help.tsx b/app/profile/help.tsx new file mode 100644 index 0000000..6875713 --- /dev/null +++ b/app/profile/help.tsx @@ -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 ( + + + {question} + + + {isExpanded && ( + {answer} + )} + + ); +} + +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 ( + + + + + {title} + {description} + + ); +} + +export default function HelpScreen() { + const [searchQuery, setSearchQuery] = useState(''); + const [expandedFAQ, setExpandedFAQ] = useState(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 ( + + + {/* Search */} + + + + + {searchQuery.length > 0 && ( + setSearchQuery('')}> + + + )} + + + + {/* Quick Guides */} + + Quick Guides + + Linking.openURL('https://wellnuo.com/guides/getting-started')} + /> + Linking.openURL('https://wellnuo.com/guides/managing-care')} + /> + Linking.openURL('https://wellnuo.com/guides/alerts')} + /> + Linking.openURL('https://wellnuo.com/guides/devices')} + /> + + + + {/* FAQs */} + + Frequently Asked Questions + + {filteredFAQs.length > 0 ? ( + filteredFAQs.map((faq, index) => ( + + setExpandedFAQ(expandedFAQ === index ? null : index)} + /> + {index < filteredFAQs.length - 1 && } + + )) + ) : ( + + + No results found + Try different keywords + + )} + + + + {/* Video Tutorials */} + + Video Tutorials + + {[ + { 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) => ( + + + + + {video.title} + {video.duration} + + ))} + + + + {/* Still Need Help */} + + + + + + Still need help? + + Our support team is available 24/7 to assist you with any questions. + + Linking.openURL('mailto:support@wellnuo.com')} + > + Contact Support + + + + + + ); +} + +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, + }, +}); diff --git a/app/profile/language.tsx b/app/profile/language.tsx new file mode 100644 index 0000000..09451c8 --- /dev/null +++ b/app/profile/language.tsx @@ -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 ( + + {flag} + + + {name} + + {nativeName} + + {!isAvailable ? ( + + Coming Soon + + ) : isSelected ? ( + + ) : null} + + ); +} + +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 ( + + + {/* Current Language */} + + + + + Current Language + + {languages.find(l => l.code === selectedLanguage)?.name || 'English'} + + + + + + {/* Available Languages */} + + Available Languages + + {availableLanguages.map((lang, index) => ( + + handleSelectLanguage(lang.code, lang.name, lang.available)} + /> + {index < availableLanguages.length - 1 && } + + ))} + + + + {/* Coming Soon Languages */} + + Coming Soon + + {comingSoonLanguages.map((lang, index) => ( + + handleSelectLanguage(lang.code, lang.name, lang.available)} + /> + {index < comingSoonLanguages.length - 1 && } + + ))} + + + + {/* Help Translate */} + + 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' }] + )} + > + + + + + Help Us Translate + + Want to see WellNuo in your language? Help us by contributing translations. + + + + + + + {/* Note */} + + + + Changing the language will translate the app interface. Beneficiary data and + system notifications may remain in the original language. + + + + + ); +} + +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, + }, +}); diff --git a/app/profile/notifications.tsx b/app/profile/notifications.tsx new file mode 100644 index 0000000..2530a59 --- /dev/null +++ b/app/profile/notifications.tsx @@ -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 ( + + + + + + {title} + {description} + + + + ); +} + +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 ( + + + {/* Alert Types */} + + Alert Types + + + + + + + + + + + + + + {/* Delivery Methods */} + + Delivery Methods + + + + + + { + 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); + } + }} + /> + + + + {/* Quiet Hours */} + + Quiet Hours + + + {quietHours && ( + <> + + + + + Quiet Period + + + {quietStart} - {quietEnd} + + + + + )} + + + Emergency alerts will always be delivered, even during quiet hours. + + + + {/* Test Notification */} + + { + Alert.alert( + 'Test Notification Sent', + 'A test push notification has been sent to your device.', + [{ text: 'OK' }] + ); + }} + > + + Send Test Notification + + + + + {/* Save Button */} + + + Save Preferences + + + + ); +} + +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, + }, +}); diff --git a/app/profile/privacy-policy.tsx b/app/profile/privacy-policy.tsx new file mode 100644 index 0000000..e221d0c --- /dev/null +++ b/app/profile/privacy-policy.tsx @@ -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 ( + + + + + + {title} + {description} + + + ); +} + +export default function PrivacyPolicyScreen() { + return ( + + + {/* Highlights */} + + Our Privacy Commitment + + + + + + + + + + + + {/* Full Policy */} + + Last Updated: December 2024 + + 1. Information We Collect + + WellNuo collects information to provide and improve our elderly care monitoring + service. We collect: + + + Account Information + • Name and contact information + • Login credentials (encrypted) + • Payment information (processed by secure third parties) + + Beneficiary Data + • Activity and motion sensor data + • Location information (if enabled) + • Health metrics from connected devices + • Daily routines and patterns + + Usage Data + • App usage statistics + • Device information + • Error logs and performance data + + 2. How We Use Your Information + + We use collected information to: + + • Provide real-time monitoring and alerts + • Generate wellness scores and insights + • Improve our AI algorithms (anonymized data only) + • Send notifications and communications + • Prevent fraud and ensure security + • Comply with legal obligations + + 3. Data Sharing + + We do NOT sell your personal information. We may share data with: + + • Authorized caregivers (your permission required) + • Healthcare providers (with explicit consent) + • Emergency services (in emergency situations) + • Service providers (under strict contracts) + • Legal authorities (when required by law) + + 4. Data Security + + We implement industry-leading security measures: + + • AES-256 encryption for data at rest + • TLS 1.3 encryption for data in transit + • Regular security audits and penetration testing + • Multi-factor authentication options + • Automatic security patching + • SOC 2 Type II certified infrastructure + + 5. Data Retention + + We retain your data for: + + • Active accounts: Duration of service + 2 years + • Deleted accounts: 30 days (recovery period) + • Legal requirements: As required by law + • Anonymized data: May be retained indefinitely for research + + 6. Your Rights + + Under GDPR, CCPA, and other privacy laws, you have the right to: + + • Access your personal data + • Correct inaccurate data + • Delete your data ("right to be forgotten") + • Export your data (data portability) + • Opt out of marketing communications + • Restrict processing of your data + + 7. Children's Privacy + + 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. + + + 8. International Data Transfers + + Data may be transferred to and processed in countries outside your residence. + We ensure adequate safeguards through: + + • Standard Contractual Clauses (SCCs) + • Privacy Shield certification (where applicable) + • Data Processing Agreements with vendors + + 9. Cookies and Tracking + + Our mobile app uses minimal tracking: + + • Essential cookies for authentication + • Analytics (anonymized, opt-out available) + • No third-party advertising trackers + + 10. Changes to This Policy + + 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. + + + 11. Contact Us + + For privacy-related questions or to exercise your rights: + + + Privacy Officer + Email: privacy@wellnuo.com + Address: 123 Care Street, San Francisco, CA 94102 + Phone: +1 (555) 123-4567 + + + + + Your privacy matters to us. If you have any concerns about how we handle your + data, please don't hesitate to contact us. + + + + + + ); +} + +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', + }, +}); diff --git a/app/profile/privacy.tsx b/app/profile/privacy.tsx new file mode 100644 index 0000000..1af9127 --- /dev/null +++ b/app/profile/privacy.tsx @@ -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 ( + + + + + + {title} + {description} + + {rightElement || } + + ); +} + +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 ( + + + {/* Password & Authentication */} + + Authentication + + + + handleEnable2FA(!twoFactor)} + rightElement={ + + } + /> + + setBiometric(!biometric)} + rightElement={ + + } + /> + + + + {/* Session Management */} + + Sessions + + + + + + {/* Data & Privacy */} + + Data & Privacy + + + + Alert.alert('Data Sharing', 'Your data is only shared with authorized caregivers and healthcare providers you designate.')} + /> + + + + {/* Login History */} + + Recent Activity + + + + + Login from iPhone + Today at 2:34 PM • San Francisco, CA + + + + + + Login from MacBook + Yesterday at 10:15 AM • San Francisco, CA + + + + + + Password changed + Dec 1, 2024 at 4:22 PM + + + + + + {/* Danger Zone */} + + Danger Zone + + + + Delete Account + + + + + + {/* Password Change Modal */} + setShowPasswordModal(false)} + > + + + setShowPasswordModal(false)}> + Cancel + + Change Password + + Save + + + + + + Current Password + + + + + New Password + + Must be at least 8 characters + + + + Confirm New Password + + + + + Password Requirements: + • At least 8 characters + • Mix of letters and numbers recommended + • Special characters for extra security + + + + + + ); +} + +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, + }, +}); diff --git a/app/profile/subscription.tsx b/app/profile/subscription.tsx new file mode 100644 index 0000000..e6712e3 --- /dev/null +++ b/app/profile/subscription.tsx @@ -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 ( + + + + {text} + + + ); +} + +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 ( + + + {/* Current Plan Badge */} + + + CURRENT PLAN + + Free + + Basic monitoring features + + + + {/* Pro Features */} + + + + + + PRO + + WellNuo Pro + Everything you need for complete care + + + + + + + + + + + + + + + + + + {/* Pricing Options */} + + Choose Your Plan + + setSelectedPlan('yearly')} + > + + + Yearly + + SAVE 33% + + + $79.99/year + $6.67/month + + + {selectedPlan === 'yearly' && ( + + )} + + + + setSelectedPlan('monthly')} + > + + Monthly + $9.99/month + Billed monthly + + + {selectedPlan === 'monthly' && ( + + )} + + + + + {/* Compare Plans */} + + Compare Plans + + + Feature + Free + Pro + + + {[ + { 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) => ( + + {row.feature} + {row.free} + {row.pro} + + ))} + + + + {/* Testimonials */} + + What Users Say + + {[ + { + 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) => ( + + + {[...Array(testimonial.rating)].map((_, i) => ( + + ))} + + "{testimonial.text}" + — {testimonial.name} + + ))} + + + + {/* Restore Purchases */} + + Restore Purchases + + + {/* Terms */} + + 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. + + + + {/* Subscribe Button */} + + + + Subscribe to Pro — {selectedPlan === 'yearly' ? '$79.99/year' : '$9.99/month'} + + + 7-day free trial • Cancel anytime + + + ); +} + +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, + }, +}); diff --git a/app/profile/support.tsx b/app/profile/support.tsx new file mode 100644 index 0000000..515c5b8 --- /dev/null +++ b/app/profile/support.tsx @@ -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 ( + + + + + + {title} + {subtitle} + + + + ); +} + +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 ( + + + + {/* Quick Contact */} + + Quick Contact + + + + + + + + + + {/* Support Hours */} + + + + + Support Hours + Phone: Mon-Fri 8am-8pm EST + Email & Chat: 24/7 + Emergency: 24/7 + + + + + {/* Submit a Ticket */} + + Submit a Ticket + + {/* Category */} + + Category * + + {categories.map((cat) => ( + setCategory(cat)} + > + + {cat} + + + ))} + + + + {/* Subject */} + + Subject * + + + + {/* Message */} + + Message * + + + + {/* Attachments hint */} + + + + Need to attach screenshots? Reply to your ticket email. + + + + {/* Submit Button */} + + + {isSending ? 'Sending...' : 'Submit Ticket'} + + + + + + {/* FAQ Link */} + + router.push('/profile/help')} + > + + + + Check our Help Center + + Find answers to common questions + + + + + + + + {/* Emergency Notice */} + + + + If you're experiencing a medical emergency, please call 911 or your local + emergency services immediately. + + + + + + ); +} + +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, + }, +}); diff --git a/app/profile/terms.tsx b/app/profile/terms.tsx new file mode 100644 index 0000000..c8fb019 --- /dev/null +++ b/app/profile/terms.tsx @@ -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 ( + + + + Last Updated: December 2024 + + 1. Acceptance of Terms + + 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. + + + 2. Description of Service + + 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: + + • Real-time activity monitoring + • Wellness score tracking + • Emergency alert notifications + • AI-powered health insights (Pro plan) + • Data analytics and reporting + + 3. User Accounts + + To use the Service, you must create an account and provide accurate, complete + information. You are responsible for: + + • Maintaining the confidentiality of your account + • All activities that occur under your account + • Notifying us immediately of any unauthorized use + + 4. Privacy and Data Protection + + 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. + + + Key privacy commitments: + + • End-to-end encryption for all data + • HIPAA-compliant data storage + • No sale of personal data to third parties + • Right to data export and deletion + + 5. Acceptable Use + + You agree to use the Service only for lawful purposes and in accordance with + these Terms. You agree NOT to: + + • Use the Service for any illegal purpose + • Violate any beneficiary's privacy rights + • Share your account credentials with others + • Attempt to reverse engineer the Service + • Transmit any malware or harmful code + + 6. Subscription and Billing + + WellNuo offers both free and paid subscription plans: + + • Free Plan: Basic features with limited beneficiaries + • Pro Plan: Full features billed monthly or annually + + Subscription fees are billed in advance. You may cancel at any time, but refunds + are not provided for partial billing periods. + + + 7. Disclaimers + + The Service is provided "as is" without warranties of any kind. WellNuo: + + • Does not guarantee continuous, uninterrupted service + • Is not a medical device or substitute for professional care + • Cannot guarantee detection of all emergencies + + Always seek professional medical advice for health concerns. + + + 8. Limitation of Liability + + 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. + + + 9. Indemnification + + 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. + + + 10. Modifications + + 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. + + + 11. Termination + + 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. + + + 12. Governing Law + + These Terms shall be governed by the laws of the State of California, United States, + without regard to conflict of law provisions. + + + 13. Contact Information + + For questions about these Terms, please contact us: + + WellNuo Inc. + Email: legal@wellnuo.com + Address: 123 Care Street, San Francisco, CA 94102 + + + + By using WellNuo, you acknowledge that you have read, understood, and agree + to be bound by these Terms of Service. + + + + + + ); +} + +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', + }, +}); diff --git a/contexts/BeneficiaryContext.tsx b/contexts/BeneficiaryContext.tsx index f769228..1faf6f1 100644 --- a/contexts/BeneficiaryContext.tsx +++ b/contexts/BeneficiaryContext.tsx @@ -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 ( diff --git a/eas.json b/eas.json index 40a8970..297be62 100644 --- a/eas.json +++ b/eas.json @@ -19,7 +19,7 @@ "production": { "ios": { "appleId": "serter2069@gmail.com", - "ascAppId": "WILL_BE_SET_AFTER_FIRST_BUILD" + "ascAppId": "6755984871" } } } diff --git a/services/api.ts b/services/api.ts index ac61606..7e29070 100644 --- a/services/api.ts +++ b/services/api.ts @@ -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 { try { @@ -175,6 +189,85 @@ class ApiService { return { data: beneficiary, ok: true }; } + // Get patient dashboard data by deployment_id + async getPatientDashboard(deploymentId: string): Promise> { + 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({ + 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> { + 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> { const token = await this.getToken(); diff --git a/types/index.ts b/types/index.ts index 193d8b9..42d5239 100644 --- a/types/index.ts +++ b/types/index.ts @@ -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 {