Full sync with auth screens and discussion docs

Added:
- forgot-password.tsx, register.tsx auth screens
- Discussion_Points.md and Discussion_Points.txt

Updated:
- login, chat, index, beneficiary detail screens
- profile/help and profile/support
- API service
- Scheme files (Discussion, AppStore)

All assets/images are tracked and safe.

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Sergei 2025-12-12 17:04:46 -08:00
parent 074433f164
commit ccf1701a34
13 changed files with 799 additions and 89 deletions

66
Discussion_Points.md Normal file
View File

@ -0,0 +1,66 @@
# WellNuo Mobile App
## Discussion Points for EluxNetworks Team
---
## 1. Backend/API Development Access
**Request:** Can I get access to develop backend/API endpoints myself?
### Why?
| Problem | Impact |
|---------|--------|
| No fixed technical specification | Requirements will change frequently |
| Waiting for backend = blocked work | Significant development delays |
| Can't predict all API needs upfront | Discovery happens during implementation |
### Option A: Access Granted (Preferred)
**I need:**
- Repository access (or separate repo)
- Database documentation
- Architecture overview
**I will:**
- Implement endpoints myself
- Document all changes
### Option B: No Access
**EluxNetworks implements these APIs:**
| Category | Endpoints |
|----------|-----------|
| Auth | Registration, Password reset, Email verification |
| Billing | Subscription plans, Payments (Stripe) |
| Beneficiary | CRUD, Family invites |
| Orders | Product catalog, Order tracking |
| Notifications | Device registration, Push settings |
| Profile | User CRUD, Password change, Account deletion |
**Important:** This list is a rough estimate, not a final specification. Actual requirements will change during development. Expect multiple rounds of revisions and delays.
---
## 2. WebView Embed Mode
**Request:** Add `?embedded=true` parameter to hide UI elements
**Hide:**
- Header "Dashboard Details"
- Navigation arrows (< >)
- Logout button
**Reason:** Mobile app has its own navigation and auth.
---
## Summary
| Option | Result |
|--------|--------|
| **Backend access** | Fast development, flexible, minimal delays |
| **Wait for APIs** | Slow, multiple spec revisions, blocked work |
**Recommendation:** Backend access for fastest delivery.

67
Discussion_Points.txt Normal file
View File

@ -0,0 +1,67 @@
WELLNUO MOBILE APP
Discussion Points for EluxNetworks Team
────────────────────────────────────────────────────────────
1. BACKEND/API DEVELOPMENT ACCESS
Request: Can I get access to develop backend/API endpoints myself?
Why?
- No fixed technical specification - requirements will change frequently
- Waiting for backend = blocked frontend work = significant delays
- Can't predict all API needs upfront - discovery happens during implementation
Option A: Access Granted (Preferred)
I need:
- Repository access (or separate repo)
- Database documentation
- Architecture overview
I will:
- Implement endpoints myself
- Document all changes
Option B: No Access
EluxNetworks implements these APIs:
- Auth: Registration, Password reset, Email verification
- Billing: Subscription plans, Payments (Stripe)
- Beneficiary: CRUD, Family invites
- Orders: Product catalog, Order tracking
- Notifications: Device registration, Push settings
- Profile: User CRUD, Password change, Account deletion
Important: This list is a rough estimate, not a final specification.
Actual requirements will change during development.
Expect multiple rounds of revisions and delays.
────────────────────────────────────────────────────────────
2. WEBVIEW EMBED MODE
Request: Add ?embedded=true parameter to hide UI elements
Hide:
- Header "Dashboard Details"
- Navigation arrows (< >)
- Logout button
Reason: Mobile app has its own navigation and auth.
────────────────────────────────────────────────────────────
SUMMARY
Backend access = fast development, flexible, minimal delays
Wait for APIs = slow, multiple spec revisions, blocked work
Recommendation: Backend access for fastest delivery.

View File

@ -0,0 +1,159 @@
import React, { useState } from 'react';
import {
View,
Text,
StyleSheet,
KeyboardAvoidingView,
Platform,
ScrollView,
TouchableOpacity,
Alert,
} from 'react-native';
import { router } from 'expo-router';
import { Ionicons } from '@expo/vector-icons';
import { Button } from '@/components/ui/Button';
import { Input } from '@/components/ui/Input';
import { AppColors, BorderRadius, FontSizes, Spacing } from '@/constants/theme';
export default function ForgotPasswordScreen() {
const [email, setEmail] = useState('');
const [isLoading, setIsLoading] = useState(false);
const handleSubmit = () => {
if (!email.trim()) {
Alert.alert('Error', 'Please enter your email address');
return;
}
// Show "in development" message
Alert.alert(
'Coming Soon',
'Password recovery is currently under development. Please contact support for assistance.',
[{ text: 'OK' }]
);
};
return (
<KeyboardAvoidingView
style={styles.container}
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
>
<ScrollView
contentContainerStyle={styles.scrollContent}
keyboardShouldPersistTaps="handled"
showsVerticalScrollIndicator={false}
>
{/* Back Button */}
<TouchableOpacity style={styles.backButton} onPress={() => router.back()}>
<Ionicons name="arrow-back" size={24} color={AppColors.textPrimary} />
</TouchableOpacity>
{/* Header */}
<View style={styles.header}>
<View style={styles.iconContainer}>
<Ionicons name="lock-open-outline" size={48} color={AppColors.primary} />
</View>
<Text style={styles.title}>Forgot Password?</Text>
<Text style={styles.subtitle}>
Enter your email address and we'll send you instructions to reset your password.
</Text>
</View>
{/* Form */}
<View style={styles.form}>
<Input
label="Email Address"
placeholder="Enter your email"
leftIcon="mail-outline"
value={email}
onChangeText={setEmail}
keyboardType="email-address"
autoCapitalize="none"
autoCorrect={false}
editable={!isLoading}
/>
<Button
title="Send Reset Link"
onPress={handleSubmit}
loading={isLoading}
fullWidth
size="lg"
/>
</View>
{/* Footer */}
<View style={styles.footer}>
<Text style={styles.footerText}>Remember your password? </Text>
<TouchableOpacity onPress={() => router.back()}>
<Text style={styles.footerLink}>Sign In</Text>
</TouchableOpacity>
</View>
</ScrollView>
</KeyboardAvoidingView>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: AppColors.background,
},
scrollContent: {
flexGrow: 1,
paddingHorizontal: Spacing.lg,
paddingTop: Spacing.xl,
paddingBottom: Spacing.xl,
},
backButton: {
width: 40,
height: 40,
borderRadius: BorderRadius.full,
backgroundColor: AppColors.surface,
justifyContent: 'center',
alignItems: 'center',
marginBottom: Spacing.lg,
},
header: {
alignItems: 'center',
marginBottom: Spacing.xl,
},
iconContainer: {
width: 100,
height: 100,
borderRadius: BorderRadius.full,
backgroundColor: AppColors.primaryLight + '30',
justifyContent: 'center',
alignItems: 'center',
marginBottom: Spacing.lg,
},
title: {
fontSize: FontSizes['2xl'],
fontWeight: '700',
color: AppColors.textPrimary,
marginBottom: Spacing.sm,
},
subtitle: {
fontSize: FontSizes.base,
color: AppColors.textSecondary,
textAlign: 'center',
lineHeight: 22,
},
form: {
marginBottom: Spacing.xl,
},
footer: {
flexDirection: 'row',
justifyContent: 'center',
alignItems: 'center',
},
footerText: {
fontSize: FontSizes.base,
color: AppColors.textSecondary,
},
footerLink: {
fontSize: FontSizes.base,
color: AppColors.primary,
fontWeight: '600',
},
});

View File

@ -108,7 +108,7 @@ export default function LoginScreen() {
returnKeyType="done" returnKeyType="done"
/> />
<TouchableOpacity style={styles.forgotPassword}> <TouchableOpacity style={styles.forgotPassword} onPress={() => router.push('/(auth)/forgot-password')}>
<Text style={styles.forgotPasswordText}>Forgot Password?</Text> <Text style={styles.forgotPasswordText}>Forgot Password?</Text>
</TouchableOpacity> </TouchableOpacity>
@ -124,7 +124,7 @@ export default function LoginScreen() {
{/* Footer */} {/* Footer */}
<View style={styles.footer}> <View style={styles.footer}>
<Text style={styles.footerText}>Don't have an account? </Text> <Text style={styles.footerText}>Don't have an account? </Text>
<TouchableOpacity> <TouchableOpacity onPress={() => router.push('/(auth)/register')}>
<Text style={styles.footerLink}>Create Account</Text> <Text style={styles.footerLink}>Create Account</Text>
</TouchableOpacity> </TouchableOpacity>
</View> </View>

219
app/(auth)/register.tsx Normal file
View File

@ -0,0 +1,219 @@
import React, { useState } from 'react';
import {
View,
Text,
StyleSheet,
KeyboardAvoidingView,
Platform,
ScrollView,
TouchableOpacity,
Alert,
} from 'react-native';
import { router } from 'expo-router';
import { Ionicons } from '@expo/vector-icons';
import { Button } from '@/components/ui/Button';
import { Input } from '@/components/ui/Input';
import { AppColors, BorderRadius, FontSizes, Spacing } from '@/constants/theme';
export default function RegisterScreen() {
const [username, setUsername] = useState('');
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [confirmPassword, setConfirmPassword] = useState('');
const [name, setName] = useState('');
const [phone, setPhone] = useState('');
const [isLoading, setIsLoading] = useState(false);
const handleSubmit = () => {
// Basic validation
if (!username.trim()) {
Alert.alert('Error', 'Username is required');
return;
}
if (!email.trim()) {
Alert.alert('Error', 'Email is required');
return;
}
if (!password.trim()) {
Alert.alert('Error', 'Password is required');
return;
}
if (password !== confirmPassword) {
Alert.alert('Error', 'Passwords do not match');
return;
}
if (!name.trim()) {
Alert.alert('Error', 'Full name is required');
return;
}
// Show "in development" message
Alert.alert(
'Coming Soon',
'Account registration is currently under development. Please contact support to create an account.',
[{ text: 'OK' }]
);
};
return (
<KeyboardAvoidingView
style={styles.container}
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
>
<ScrollView
contentContainerStyle={styles.scrollContent}
keyboardShouldPersistTaps="handled"
showsVerticalScrollIndicator={false}
>
{/* Back Button */}
<TouchableOpacity style={styles.backButton} onPress={() => router.back()}>
<Ionicons name="arrow-back" size={24} color={AppColors.textPrimary} />
</TouchableOpacity>
{/* Header */}
<View style={styles.header}>
<Text style={styles.title}>Create Account</Text>
<Text style={styles.subtitle}>
Join WellNuo to start monitoring your loved ones
</Text>
</View>
{/* Form */}
<View style={styles.form}>
<Input
label="Username"
placeholder="Choose a username"
leftIcon="person-outline"
value={username}
onChangeText={setUsername}
autoCapitalize="none"
autoCorrect={false}
editable={!isLoading}
/>
<Input
label="Email Address"
placeholder="Enter your email"
leftIcon="mail-outline"
value={email}
onChangeText={setEmail}
keyboardType="email-address"
autoCapitalize="none"
autoCorrect={false}
editable={!isLoading}
/>
<Input
label="Full Name"
placeholder="Enter your full name"
leftIcon="person-circle-outline"
value={name}
onChangeText={setName}
autoCapitalize="words"
editable={!isLoading}
/>
<Input
label="Phone (optional)"
placeholder="Enter your phone number"
leftIcon="call-outline"
value={phone}
onChangeText={setPhone}
keyboardType="phone-pad"
editable={!isLoading}
/>
<Input
label="Password"
placeholder="Create a password"
leftIcon="lock-closed-outline"
secureTextEntry
value={password}
onChangeText={setPassword}
editable={!isLoading}
/>
<Input
label="Confirm Password"
placeholder="Confirm your password"
leftIcon="lock-closed-outline"
secureTextEntry
value={confirmPassword}
onChangeText={setConfirmPassword}
editable={!isLoading}
/>
<Button
title="Create Account"
onPress={handleSubmit}
loading={isLoading}
fullWidth
size="lg"
/>
</View>
{/* Footer */}
<View style={styles.footer}>
<Text style={styles.footerText}>Already have an account? </Text>
<TouchableOpacity onPress={() => router.back()}>
<Text style={styles.footerLink}>Sign In</Text>
</TouchableOpacity>
</View>
</ScrollView>
</KeyboardAvoidingView>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: AppColors.background,
},
scrollContent: {
flexGrow: 1,
paddingHorizontal: Spacing.lg,
paddingTop: Spacing.xl,
paddingBottom: Spacing.xl,
},
backButton: {
width: 40,
height: 40,
borderRadius: BorderRadius.full,
backgroundColor: AppColors.surface,
justifyContent: 'center',
alignItems: 'center',
marginBottom: Spacing.lg,
},
header: {
alignItems: 'center',
marginBottom: Spacing.xl,
},
title: {
fontSize: FontSizes['2xl'],
fontWeight: '700',
color: AppColors.textPrimary,
marginBottom: Spacing.sm,
},
subtitle: {
fontSize: FontSizes.base,
color: AppColors.textSecondary,
textAlign: 'center',
},
form: {
marginBottom: Spacing.xl,
},
footer: {
flexDirection: 'row',
justifyContent: 'center',
alignItems: 'center',
},
footerText: {
fontSize: FontSizes.base,
color: AppColors.textSecondary,
},
footerLink: {
fontSize: FontSizes.base,
color: AppColors.primary,
fontWeight: '600',
},
});

View File

@ -6,6 +6,12 @@ import {
ScrollView, ScrollView,
TouchableOpacity, TouchableOpacity,
RefreshControl, RefreshControl,
Image,
Modal,
TextInput,
Alert,
KeyboardAvoidingView,
Platform,
} from 'react-native'; } from 'react-native';
import { useLocalSearchParams, router } from 'expo-router'; import { useLocalSearchParams, router } from 'expo-router';
import { Ionicons } from '@expo/vector-icons'; import { Ionicons } from '@expo/vector-icons';
@ -26,6 +32,13 @@ export default function BeneficiaryDetailScreen() {
const [isRefreshing, setIsRefreshing] = useState(false); const [isRefreshing, setIsRefreshing] = useState(false);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
// Edit modal state
const [isEditModalVisible, setIsEditModalVisible] = useState(false);
const [editForm, setEditForm] = useState({
name: '',
address: '',
});
const loadBeneficiary = useCallback(async (showLoading = true) => { const loadBeneficiary = useCallback(async (showLoading = true) => {
if (!id) return; if (!id) return;
@ -66,6 +79,32 @@ export default function BeneficiaryDetailScreen() {
router.push('/(tabs)/chat'); router.push('/(tabs)/chat');
}; };
const handleEditPress = () => {
if (beneficiary) {
setEditForm({
name: beneficiary.name || '',
address: beneficiary.address || '',
});
setIsEditModalVisible(true);
}
};
const handleSaveEdit = () => {
Alert.alert(
'Demo Mode',
'This is a mockup feature. Saving beneficiary data requires backend API endpoints that are not yet implemented.\n\nContact your administrator to enable this feature.',
[{ text: 'OK', onPress: () => setIsEditModalVisible(false) }]
);
};
const showComingSoon = (featureName: string) => {
Alert.alert(
'Coming Soon',
`${featureName} is currently in development.\n\nThis feature will be available in a future update.`,
[{ text: 'OK' }]
);
};
if (isLoading) { if (isLoading) {
return <LoadingSpinner fullScreen message="Loading beneficiary data..." />; return <LoadingSpinner fullScreen message="Loading beneficiary data..." />;
} }
@ -90,8 +129,8 @@ export default function BeneficiaryDetailScreen() {
<Ionicons name="arrow-back" size={24} color={AppColors.textPrimary} /> <Ionicons name="arrow-back" size={24} color={AppColors.textPrimary} />
</TouchableOpacity> </TouchableOpacity>
<Text style={styles.headerTitle}>{beneficiary.name}</Text> <Text style={styles.headerTitle}>{beneficiary.name}</Text>
<TouchableOpacity style={styles.menuButton}> <TouchableOpacity style={styles.editButton} onPress={handleEditPress}>
<Ionicons name="ellipsis-vertical" size={24} color={AppColors.textPrimary} /> <Ionicons name="create-outline" size={24} color={AppColors.primary} />
</TouchableOpacity> </TouchableOpacity>
</View> </View>
@ -109,9 +148,13 @@ export default function BeneficiaryDetailScreen() {
{/* Beneficiary Info Card */} {/* Beneficiary Info Card */}
<View style={styles.infoCard}> <View style={styles.infoCard}>
<View style={styles.avatarContainer}> <View style={styles.avatarContainer}>
{beneficiary.avatar ? (
<Image source={{ uri: beneficiary.avatar }} style={styles.avatarImage} />
) : (
<Text style={styles.avatarText}> <Text style={styles.avatarText}>
{beneficiary.name.charAt(0).toUpperCase()} {beneficiary.name.charAt(0).toUpperCase()}
</Text> </Text>
)}
<View <View
style={[ style={[
styles.statusBadge, styles.statusBadge,
@ -231,21 +274,21 @@ export default function BeneficiaryDetailScreen() {
<Text style={styles.actionLabel}>Chat with Julia</Text> <Text style={styles.actionLabel}>Chat with Julia</Text>
</TouchableOpacity> </TouchableOpacity>
<TouchableOpacity style={styles.actionCard}> <TouchableOpacity style={styles.actionCard} onPress={() => showComingSoon('Set Reminder')}>
<View style={[styles.actionIcon, { backgroundColor: '#FEF3C7' }]}> <View style={[styles.actionIcon, { backgroundColor: '#FEF3C7' }]}>
<Ionicons name="notifications" size={24} color={AppColors.warning} /> <Ionicons name="notifications" size={24} color={AppColors.warning} />
</View> </View>
<Text style={styles.actionLabel}>Set Reminder</Text> <Text style={styles.actionLabel}>Set Reminder</Text>
</TouchableOpacity> </TouchableOpacity>
<TouchableOpacity style={styles.actionCard}> <TouchableOpacity style={styles.actionCard} onPress={() => showComingSoon('Video Call')}>
<View style={[styles.actionIcon, { backgroundColor: '#DBEAFE' }]}> <View style={[styles.actionIcon, { backgroundColor: '#DBEAFE' }]}>
<Ionicons name="call" size={24} color={AppColors.primary} /> <Ionicons name="call" size={24} color={AppColors.primary} />
</View> </View>
<Text style={styles.actionLabel}>Video Call</Text> <Text style={styles.actionLabel}>Video Call</Text>
</TouchableOpacity> </TouchableOpacity>
<TouchableOpacity style={styles.actionCard}> <TouchableOpacity style={styles.actionCard} onPress={() => showComingSoon('Activity Report')}>
<View style={[styles.actionIcon, { backgroundColor: '#F3E8FF' }]}> <View style={[styles.actionIcon, { backgroundColor: '#F3E8FF' }]}>
<Ionicons name="analytics" size={24} color="#9333EA" /> <Ionicons name="analytics" size={24} color="#9333EA" />
</View> </View>
@ -264,6 +307,76 @@ export default function BeneficiaryDetailScreen() {
/> />
</View> </View>
</ScrollView> </ScrollView>
{/* Edit Modal */}
<Modal
visible={isEditModalVisible}
animationType="slide"
transparent={true}
onRequestClose={() => setIsEditModalVisible(false)}
>
<KeyboardAvoidingView
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
style={styles.modalOverlay}
>
<View style={styles.modalContent}>
<View style={styles.modalHeader}>
<Text style={styles.modalTitle}>Edit Beneficiary</Text>
<TouchableOpacity
onPress={() => setIsEditModalVisible(false)}
style={styles.modalCloseButton}
>
<Ionicons name="close" size={24} color={AppColors.textPrimary} />
</TouchableOpacity>
</View>
<ScrollView style={styles.modalForm}>
<Text style={styles.inputLabel}>Name</Text>
<TextInput
style={styles.textInput}
value={editForm.name}
onChangeText={(text) => setEditForm({ ...editForm, name: text })}
placeholder="Enter name"
placeholderTextColor={AppColors.textMuted}
/>
<Text style={styles.inputLabel}>Address</Text>
<TextInput
style={[styles.textInput, styles.textAreaInput]}
value={editForm.address}
onChangeText={(text) => setEditForm({ ...editForm, address: text })}
placeholder="Enter address"
placeholderTextColor={AppColors.textMuted}
multiline
numberOfLines={3}
/>
<View style={styles.infoBox}>
<Ionicons name="information-circle-outline" size={20} color={AppColors.primary} />
<Text style={styles.infoBoxText}>
Other data (wellness score, temperature, sleep hours) is collected automatically from sensors.
</Text>
</View>
</ScrollView>
<View style={styles.modalFooter}>
<TouchableOpacity
style={styles.cancelButton}
onPress={() => setIsEditModalVisible(false)}
>
<Text style={styles.cancelButtonText}>Cancel</Text>
</TouchableOpacity>
<TouchableOpacity
style={styles.saveButton}
onPress={handleSaveEdit}
>
<Ionicons name="checkmark" size={20} color={AppColors.white} />
<Text style={styles.saveButtonText}>Save Changes</Text>
</TouchableOpacity>
</View>
</View>
</KeyboardAvoidingView>
</Modal>
</SafeAreaView> </SafeAreaView>
); );
} }
@ -291,7 +404,7 @@ const styles = StyleSheet.create({
fontWeight: '600', fontWeight: '600',
color: AppColors.textPrimary, color: AppColors.textPrimary,
}, },
menuButton: { editButton: {
padding: Spacing.xs, padding: Spacing.xs,
}, },
content: { content: {
@ -317,6 +430,11 @@ const styles = StyleSheet.create({
fontWeight: '600', fontWeight: '600',
color: AppColors.white, color: AppColors.white,
}, },
avatarImage: {
width: 100,
height: 100,
borderRadius: BorderRadius.full,
},
statusBadge: { statusBadge: {
position: 'absolute', position: 'absolute',
bottom: 0, bottom: 0,
@ -423,4 +541,104 @@ const styles = StyleSheet.create({
padding: Spacing.lg, padding: Spacing.lg,
paddingBottom: Spacing.xxl, paddingBottom: Spacing.xxl,
}, },
// Modal styles
modalOverlay: {
flex: 1,
backgroundColor: 'rgba(0, 0, 0, 0.5)',
justifyContent: 'flex-end',
},
modalContent: {
backgroundColor: AppColors.background,
borderTopLeftRadius: BorderRadius.xl,
borderTopRightRadius: BorderRadius.xl,
maxHeight: '80%',
},
modalHeader: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
padding: Spacing.lg,
borderBottomWidth: 1,
borderBottomColor: AppColors.border,
},
modalTitle: {
fontSize: FontSizes.xl,
fontWeight: '600',
color: AppColors.textPrimary,
},
modalCloseButton: {
padding: Spacing.xs,
},
modalForm: {
padding: Spacing.lg,
},
inputLabel: {
fontSize: FontSizes.sm,
fontWeight: '500',
color: AppColors.textSecondary,
marginBottom: Spacing.xs,
marginTop: Spacing.md,
},
textInput: {
backgroundColor: AppColors.surface,
borderRadius: BorderRadius.md,
padding: Spacing.md,
fontSize: FontSizes.base,
color: AppColors.textPrimary,
borderWidth: 1,
borderColor: AppColors.border,
},
textAreaInput: {
minHeight: 80,
textAlignVertical: 'top',
},
infoBox: {
flexDirection: 'row',
backgroundColor: '#EFF6FF',
borderRadius: BorderRadius.md,
padding: Spacing.md,
marginTop: Spacing.lg,
gap: Spacing.sm,
},
infoBoxText: {
flex: 1,
fontSize: FontSizes.sm,
color: AppColors.primary,
lineHeight: 20,
},
modalFooter: {
flexDirection: 'row',
padding: Spacing.lg,
gap: Spacing.md,
borderTopWidth: 1,
borderTopColor: AppColors.border,
},
cancelButton: {
flex: 1,
paddingVertical: Spacing.md,
borderRadius: BorderRadius.md,
backgroundColor: AppColors.surface,
alignItems: 'center',
justifyContent: 'center',
},
cancelButtonText: {
fontSize: FontSizes.base,
fontWeight: '500',
color: AppColors.textSecondary,
},
saveButton: {
flex: 2,
flexDirection: 'row',
paddingVertical: Spacing.md,
borderRadius: BorderRadius.md,
backgroundColor: AppColors.primary,
alignItems: 'center',
justifyContent: 'center',
gap: Spacing.xs,
},
saveButtonText: {
fontSize: FontSizes.base,
fontWeight: '600',
color: AppColors.white,
},
}); });

View File

@ -8,6 +8,7 @@ import {
TouchableOpacity, TouchableOpacity,
KeyboardAvoidingView, KeyboardAvoidingView,
Platform, Platform,
Alert,
} from 'react-native'; } from 'react-native';
import { Ionicons } from '@expo/vector-icons'; import { Ionicons } from '@expo/vector-icons';
import { SafeAreaView } from 'react-native-safe-area-context'; import { SafeAreaView } from 'react-native-safe-area-context';
@ -142,7 +143,10 @@ export default function ChatScreen() {
</Text> </Text>
</View> </View>
</View> </View>
<TouchableOpacity style={styles.headerButton}> <TouchableOpacity
style={styles.headerButton}
onPress={() => Alert.alert('Coming Soon', 'Chat settings will be available in a future update.')}
>
<Ionicons name="ellipsis-vertical" size={24} color={AppColors.textPrimary} /> <Ionicons name="ellipsis-vertical" size={24} color={AppColors.textPrimary} />
</TouchableOpacity> </TouchableOpacity>
</View> </View>

View File

@ -6,7 +6,8 @@ import {
FlatList, FlatList,
TouchableOpacity, TouchableOpacity,
ActivityIndicator, ActivityIndicator,
RefreshControl RefreshControl,
Image,
} from 'react-native'; } from 'react-native';
import { Ionicons } from '@expo/vector-icons'; import { Ionicons } from '@expo/vector-icons';
import { SafeAreaView } from 'react-native-safe-area-context'; import { SafeAreaView } from 'react-native-safe-area-context';
@ -24,7 +25,6 @@ interface PatientCardProps {
} }
function PatientCard({ patient, onPress }: PatientCardProps) { function PatientCard({ patient, onPress }: PatientCardProps) {
const isOnline = patient.status === 'online';
const wellnessColor = patient.wellness_score && patient.wellness_score >= 70 const wellnessColor = patient.wellness_score && patient.wellness_score >= 70
? AppColors.success ? AppColors.success
: patient.wellness_score && patient.wellness_score >= 40 : patient.wellness_score && patient.wellness_score >= 40
@ -35,11 +35,16 @@ function PatientCard({ patient, onPress }: PatientCardProps) {
<TouchableOpacity style={styles.card} onPress={onPress} activeOpacity={0.7}> <TouchableOpacity style={styles.card} onPress={onPress} activeOpacity={0.7}>
<View style={styles.cardContent}> <View style={styles.cardContent}>
{/* Avatar */} {/* Avatar */}
<View style={[styles.avatar, isOnline && styles.avatarOnline]}> <View style={styles.avatarWrapper}>
{patient.avatar ? (
<Image source={{ uri: patient.avatar }} style={styles.avatarImage} />
) : (
<View style={styles.avatar}>
<Text style={styles.avatarText}> <Text style={styles.avatarText}>
{patient.name.charAt(0).toUpperCase()} {patient.name.charAt(0).toUpperCase()}
</Text> </Text>
{isOnline && <View style={styles.onlineIndicator} />} </View>
)}
</View> </View>
{/* Info */} {/* Info */}
@ -51,17 +56,10 @@ function PatientCard({ patient, onPress }: PatientCardProps) {
<Text style={styles.locationText}>{patient.last_location}</Text> <Text style={styles.locationText}>{patient.last_location}</Text>
</View> </View>
)} )}
<View style={styles.statusRow}>
<View style={[styles.statusBadge, isOnline ? styles.statusOnline : styles.statusOffline]}>
<Text style={[styles.statusText, isOnline ? styles.statusTextOnline : styles.statusTextOffline]}>
{isOnline ? 'Active' : 'Inactive'}
</Text>
</View>
{patient.last_activity && ( {patient.last_activity && (
<Text style={styles.lastActivity}>{patient.last_activity}</Text> <Text style={styles.lastActivity}>{patient.last_activity}</Text>
)} )}
</View> </View>
</View>
{/* Wellness Score */} {/* Wellness Score */}
{patient.wellness_score !== undefined && ( {patient.wellness_score !== undefined && (
@ -266,35 +264,31 @@ const styles = StyleSheet.create({
alignItems: 'center', alignItems: 'center',
padding: Spacing.md, padding: Spacing.md,
}, },
avatarWrapper: {
width: 56,
height: 56,
borderRadius: 28,
position: 'relative',
overflow: 'hidden',
},
avatar: { avatar: {
width: 56, width: 56,
height: 56, height: 56,
borderRadius: BorderRadius.full, borderRadius: 28,
backgroundColor: AppColors.primaryLight, backgroundColor: AppColors.primaryLight,
justifyContent: 'center', justifyContent: 'center',
alignItems: 'center', alignItems: 'center',
position: 'relative',
}, },
avatarOnline: { avatarImage: {
borderWidth: 2, width: 56,
borderColor: AppColors.success, height: 56,
borderRadius: 28,
}, },
avatarText: { avatarText: {
fontSize: FontSizes.xl, fontSize: FontSizes.xl,
fontWeight: '600', fontWeight: '600',
color: AppColors.white, 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: { info: {
flex: 1, flex: 1,
marginLeft: Spacing.md, marginLeft: Spacing.md,
@ -309,32 +303,6 @@ const styles = StyleSheet.create({
color: AppColors.textSecondary, color: AppColors.textSecondary,
marginTop: 2, 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: { lastActivity: {
fontSize: FontSizes.xs, fontSize: FontSizes.xs,
color: AppColors.textMuted, color: AppColors.textMuted,

View File

@ -7,6 +7,7 @@ import {
TouchableOpacity, TouchableOpacity,
TextInput, TextInput,
Linking, Linking,
Alert,
} from 'react-native'; } from 'react-native';
import { Ionicons } from '@expo/vector-icons'; import { Ionicons } from '@expo/vector-icons';
import { SafeAreaView } from 'react-native-safe-area-context'; import { SafeAreaView } from 'react-native-safe-area-context';
@ -209,7 +210,11 @@ export default function HelpScreen() {
{ title: 'Understanding Data', duration: '4:15' }, { title: 'Understanding Data', duration: '4:15' },
{ title: 'Troubleshooting', duration: '5:00' }, { title: 'Troubleshooting', duration: '5:00' },
].map((video, index) => ( ].map((video, index) => (
<TouchableOpacity key={index} style={styles.videoCard}> <TouchableOpacity
key={index}
style={styles.videoCard}
onPress={() => Alert.alert('Coming Soon', `"${video.title}" video tutorial will be available in a future update.`)}
>
<View style={styles.videoThumbnail}> <View style={styles.videoThumbnail}>
<Ionicons name="play-circle" size={40} color={AppColors.white} /> <Ionicons name="play-circle" size={40} color={AppColors.white} />
</View> </View>

View File

@ -374,7 +374,10 @@ export default function SupportScreen() {
numberOfLines={4} numberOfLines={4}
textAlignVertical="top" textAlignVertical="top"
/> />
<TouchableOpacity style={styles.replyButton}> <TouchableOpacity
style={styles.replyButton}
onPress={() => Alert.alert('Coming Soon', 'Reply functionality will be available in a future update.')}
>
<Text style={styles.replyButtonText}>Send Reply</Text> <Text style={styles.replyButtonText}>Send Reply</Text>
</TouchableOpacity> </TouchableOpacity>
</View> </View>

View File

@ -4,14 +4,13 @@ import type { AuthResponse, ChatResponse, Beneficiary, ApiResponse, ApiError, Da
const API_BASE_URL = 'https://eluxnetworks.net/function/well-api/api'; const API_BASE_URL = 'https://eluxnetworks.net/function/well-api/api';
const CLIENT_ID = 'MA_001'; const CLIENT_ID = 'MA_001';
// Avatar images for elderly beneficiaries (stable Unsplash URLs) // Avatar images for elderly beneficiaries - grandmothers (бабушки)
const ELDERLY_AVATARS = [ const ELDERLY_AVATARS = [
'https://images.unsplash.com/photo-1559839734-2b71ea197ec2?w=200&h=200&fit=crop&crop=face', // elderly woman 1 'https://images.unsplash.com/photo-1566616213894-2d4e1baee5d8?w=200&h=200&fit=crop&crop=face', // grandmother with gray hair
'https://images.unsplash.com/photo-1581579438747-104c53d7fbc4?w=200&h=200&fit=crop&crop=face', // elderly man 1 'https://images.unsplash.com/photo-1544027993-37dbfe43562a?w=200&h=200&fit=crop&crop=face', // elderly woman smiling
'https://images.unsplash.com/photo-1566616213894-2d4e1baee5d8?w=200&h=200&fit=crop&crop=face', // elderly woman 2 'https://images.unsplash.com/photo-1491308056676-205b7c9a7dc1?w=200&h=200&fit=crop&crop=face', // senior woman portrait
'https://images.unsplash.com/photo-1552058544-f2b08422138a?w=200&h=200&fit=crop&crop=face', // elderly man 2 'https://images.unsplash.com/photo-1580489944761-15a19d654956?w=200&h=200&fit=crop&crop=face', // older woman glasses
'https://images.unsplash.com/photo-1544005313-94ddf0286df2?w=200&h=200&fit=crop&crop=face', // elderly woman 3 'https://images.unsplash.com/photo-1548142813-c348350df52b?w=200&h=200&fit=crop&crop=face', // grandmother portrait
'https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=200&h=200&fit=crop&crop=face', // elderly man 3
]; ];
// Get consistent avatar based on deployment_id // Get consistent avatar based on deployment_id
@ -283,9 +282,11 @@ class ApiService {
const lastDetected = data.last_detected_time ? new Date(data.last_detected_time) : null; 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 const isRecent = lastDetected && (Date.now() - lastDetected.getTime()) < 30 * 60 * 1000; // 30 min
const deploymentId = parseInt(data.deployment_id, 10);
patients.push({ patients.push({
id: parseInt(data.deployment_id, 10), id: deploymentId,
name: data.name, name: data.name,
avatar: getAvatarForBeneficiary(deploymentId),
status: isRecent ? 'online' : 'offline', status: isRecent ? 'online' : 'offline',
address: data.address, address: data.address,
timezone: data.time_zone, timezone: data.time_zone,

View File

@ -176,7 +176,7 @@
"api", "api",
"profile" "profile"
], ],
"description": "GET /api/profile\nPUT /api/profile\n{\n name: string\n email: string\n phone?: string\n avatar_url?: string\n language: string\n timezone: string\n}\n\nPOST /api/profile/change-password\nDELETE /api/profile (удаление аккаунта)", "description": "GET /api/profile\nPUT /api/profile\n{\n name: string\n email: string\n phone?: string\n avatar_url?: string\n language: string\n timezone: string\n}\n\nPOST /api/profile/change-password\nDELETE /api/profile (delete account)",
"x": 2023.0888671875, "x": 2023.0888671875,
"y": 365.1834411621094 "y": 365.1834411621094
} }
@ -193,12 +193,12 @@
{ {
"from": "q_api_access", "from": "q_api_access",
"to": "api_yes", "to": "api_yes",
"label": "Да" "label": "Yes"
}, },
{ {
"from": "q_api_access", "from": "q_api_access",
"to": "api_no", "to": "api_no",
"label": "Нет" "label": "No"
}, },
{ {
"from": "api_no", "from": "api_no",

View File

@ -1,7 +1,7 @@
{ {
"_meta": { "_meta": {
"name": "App Store Publication", "name": "App Store Publication",
"updatedAt": "2025-12-12T21:45:27.718Z" "updatedAt": "2025-12-13T00:48:45.179Z"
}, },
"elements": [ "elements": [
{ {
@ -181,8 +181,8 @@
"design" "design"
], ],
"description": "**Size:** 1024×1024 PNG\n\n**Requirements:**\n- No transparency (no alpha)\n- No rounded corners\n- Clear at all sizes\n\n**Style:**\nHeart/care, blue gradient", "description": "**Size:** 1024×1024 PNG\n\n**Requirements:**\n- No transparency (no alpha)\n- No rounded corners\n- Clear at all sizes\n\n**Style:**\nHeart/care, blue gradient",
"x": 1660, "x": 1592.44091796875,
"y": 550 "y": 511.067626953125
}, },
{ {
"id": "prep_iap", "id": "prep_iap",
@ -194,8 +194,8 @@
"appstore" "appstore"
], ],
"description": "**Monthly:**\nID: com.wellnuo.premium.monthly\nPrice: $4.99/month\nName: WellNuo Premium\nDesc: Unlock unlimited history, AI insights, and family connections.\n\n**Yearly:**\nID: com.wellnuo.premium.yearly\nPrice: $49.99/year\nName: WellNuo Premium (Annual)\nDesc: Save 17% with annual subscription.\n\n**Lifetime:**\nID: com.wellnuo.lifetime\nPrice: $149.99\nName: WellNuo Lifetime\nDesc: One-time purchase for lifetime access.\n\n**Trial:** 7 days", "description": "**Monthly:**\nID: com.wellnuo.premium.monthly\nPrice: $4.99/month\nName: WellNuo Premium\nDesc: Unlock unlimited history, AI insights, and family connections.\n\n**Yearly:**\nID: com.wellnuo.premium.yearly\nPrice: $49.99/year\nName: WellNuo Premium (Annual)\nDesc: Save 17% with annual subscription.\n\n**Lifetime:**\nID: com.wellnuo.lifetime\nPrice: $149.99\nName: WellNuo Lifetime\nDesc: One-time purchase for lifetime access.\n\n**Trial:** 7 days",
"x": 2040, "x": 1115.09814453125,
"y": 150 "y": 637.2400722503662
}, },
{ {
"id": "prep_age_rating", "id": "prep_age_rating",