Sergei 7cb07c09ce Major UI/UX updates: Voice, Subscription, Beneficiaries, Profile
- Voice tab: simplified interface, voice picker improvements
- Subscription: Stripe integration, purchase flow updates
- Beneficiaries: dashboard, sharing, improved management
- Profile: drawer, edit, help, privacy sections
- Theme: expanded constants, new colors
- New components: MockDashboard, ProfileDrawer, Toast
- Backend: Stripe routes additions
- Auth: activate, add-loved-one, purchase screens
2025-12-29 15:36:44 -08:00

337 lines
9.5 KiB
TypeScript

import React, { useState, useEffect } from 'react';
import {
View,
Text,
StyleSheet,
ScrollView,
TextInput,
TouchableOpacity,
Alert,
KeyboardAvoidingView,
Platform,
Image,
} from 'react-native';
import { Ionicons } from '@expo/vector-icons';
import { SafeAreaView } from 'react-native-safe-area-context';
import { router } from 'expo-router';
import * as SecureStore from 'expo-secure-store';
import * as ImagePicker from 'expo-image-picker';
import { useAuth } from '@/contexts/AuthContext';
import { PageHeader } from '@/components/PageHeader';
import {
AppColors,
BorderRadius,
FontSizes,
Spacing,
FontWeights,
Shadows,
} from '@/constants/theme';
export default function EditProfileScreen() {
const { user, updateUser } = useAuth();
const [displayName, setDisplayName] = useState(user?.user_name || '');
const [phone, setPhone] = useState('');
const [isSaving, setIsSaving] = useState(false);
const [avatarUri, setAvatarUri] = useState<string | null>(null);
// Load saved data from SecureStore
useEffect(() => {
const loadData = async () => {
try {
const [savedAvatar, savedPhone, savedName] = await Promise.all([
SecureStore.getItemAsync('userAvatar'),
SecureStore.getItemAsync('userPhone'),
SecureStore.getItemAsync('userName'),
]);
if (savedAvatar) setAvatarUri(savedAvatar);
if (savedPhone) setPhone(savedPhone);
if (savedName) setDisplayName(savedName);
} catch (err) {
console.error('Failed to load profile data:', err);
}
};
loadData();
}, []);
const handleSave = async () => {
if (!displayName.trim()) {
Alert.alert('Error', 'Display name is required');
return;
}
setIsSaving(true);
try {
// Save to SecureStore
await Promise.all([
SecureStore.setItemAsync('userName', displayName.trim()),
phone ? SecureStore.setItemAsync('userPhone', phone) : Promise.resolve(),
]);
// Update user in AuthContext
if (updateUser) {
updateUser({ user_name: displayName.trim() });
}
Alert.alert(
'Profile Updated',
'Your profile has been updated successfully.',
[{ text: 'OK', onPress: () => router.back() }]
);
} catch (error) {
console.error('Failed to save profile:', error);
Alert.alert('Error', 'Failed to save profile. Please try again.');
}
setIsSaving(false);
};
const handleChangePhoto = async () => {
const { status } = await ImagePicker.requestMediaLibraryPermissionsAsync();
if (status !== 'granted') {
Alert.alert('Permission needed', 'Please allow access to your photo library to change avatar.');
return;
}
const result = await ImagePicker.launchImageLibraryAsync({
mediaTypes: ['images'],
allowsEditing: true,
aspect: [1, 1],
quality: 0.5,
});
if (!result.canceled && result.assets[0]) {
const uri = result.assets[0].uri;
setAvatarUri(uri);
await SecureStore.setItemAsync('userAvatar', uri);
}
};
const userInitial = displayName?.charAt(0).toUpperCase() || 'U';
return (
<SafeAreaView style={styles.container} edges={['top', 'bottom']}>
<PageHeader title="Edit Profile" />
<KeyboardAvoidingView
style={styles.keyboardView}
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
>
<ScrollView showsVerticalScrollIndicator={false}>
{/* Avatar Section */}
<View style={styles.avatarSection}>
<TouchableOpacity style={styles.avatarWrapper} onPress={handleChangePhoto}>
{avatarUri ? (
<Image source={{ uri: avatarUri }} style={styles.avatarImage} />
) : (
<View style={styles.avatarContainer}>
<Text style={styles.avatarText}>{userInitial}</Text>
</View>
)}
<View style={styles.changePhotoButton}>
<Ionicons name="camera" size={16} color={AppColors.white} />
</View>
</TouchableOpacity>
<Text style={styles.changePhotoText}>Tap to change photo</Text>
</View>
{/* Form */}
<View style={styles.form}>
<View style={styles.inputGroup}>
<Text style={styles.label}>Display Name *</Text>
<View style={styles.inputContainer}>
<Ionicons name="person-outline" size={20} color={AppColors.textMuted} />
<TextInput
style={styles.input}
value={displayName}
onChangeText={setDisplayName}
placeholder="Enter your display name"
placeholderTextColor={AppColors.textMuted}
/>
</View>
</View>
<View style={styles.inputGroup}>
<Text style={styles.label}>Email Address</Text>
<View style={[styles.inputContainer, styles.disabledInput]}>
<Ionicons name="mail-outline" size={20} color={AppColors.textMuted} />
<Text style={styles.disabledText}>{user?.email || 'Not set'}</Text>
<View style={styles.lockBadge}>
<Ionicons name="lock-closed" size={12} color={AppColors.textMuted} />
</View>
</View>
<Text style={styles.hint}>Email cannot be changed</Text>
</View>
<View style={styles.inputGroup}>
<Text style={styles.label}>Phone Number</Text>
<View style={styles.inputContainer}>
<Ionicons name="call-outline" size={20} color={AppColors.textMuted} />
<TextInput
style={styles.input}
value={phone}
onChangeText={setPhone}
placeholder="+1 (555) 123-4567"
placeholderTextColor={AppColors.textMuted}
keyboardType="phone-pad"
/>
</View>
<Text style={styles.hint}>For emergency contact and SMS alerts</Text>
</View>
</View>
</ScrollView>
{/* Save Button */}
<View style={styles.footer}>
<TouchableOpacity
style={[styles.saveButton, isSaving && styles.saveButtonDisabled]}
onPress={handleSave}
disabled={isSaving}
>
{isSaving ? (
<Text style={styles.saveButtonText}>Saving...</Text>
) : (
<>
<Ionicons name="checkmark" size={20} color={AppColors.white} />
<Text style={styles.saveButtonText}>Save Changes</Text>
</>
)}
</TouchableOpacity>
</View>
</KeyboardAvoidingView>
</SafeAreaView>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: AppColors.background,
},
keyboardView: {
flex: 1,
},
avatarSection: {
alignItems: 'center',
paddingVertical: Spacing.xl,
backgroundColor: AppColors.surface,
marginBottom: Spacing.md,
},
avatarWrapper: {
position: 'relative',
},
avatarContainer: {
width: 100,
height: 100,
borderRadius: 50,
backgroundColor: AppColors.primary,
justifyContent: 'center',
alignItems: 'center',
},
avatarImage: {
width: 100,
height: 100,
borderRadius: 50,
},
avatarText: {
fontSize: 40,
fontWeight: FontWeights.bold,
color: AppColors.white,
},
changePhotoButton: {
position: 'absolute',
bottom: 0,
right: 0,
width: 32,
height: 32,
borderRadius: 16,
backgroundColor: AppColors.primary,
justifyContent: 'center',
alignItems: 'center',
borderWidth: 3,
borderColor: AppColors.surface,
},
changePhotoText: {
marginTop: Spacing.sm,
fontSize: FontSizes.sm,
color: AppColors.textMuted,
},
form: {
padding: Spacing.lg,
},
inputGroup: {
marginBottom: Spacing.lg,
},
label: {
fontSize: FontSizes.sm,
fontWeight: FontWeights.medium,
color: AppColors.textSecondary,
marginBottom: Spacing.xs,
marginLeft: Spacing.xs,
},
inputContainer: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: AppColors.surface,
borderRadius: BorderRadius.lg,
paddingHorizontal: Spacing.md,
paddingVertical: Spacing.md,
borderWidth: 1.5,
borderColor: AppColors.borderLight,
gap: Spacing.sm,
...Shadows.xs,
},
input: {
flex: 1,
fontSize: FontSizes.base,
color: AppColors.textPrimary,
},
disabledInput: {
backgroundColor: AppColors.surfaceSecondary,
borderColor: AppColors.border,
},
disabledText: {
flex: 1,
fontSize: FontSizes.base,
color: AppColors.textSecondary,
},
lockBadge: {
width: 24,
height: 24,
borderRadius: 12,
backgroundColor: AppColors.border,
justifyContent: 'center',
alignItems: 'center',
},
hint: {
fontSize: FontSizes.xs,
color: AppColors.textMuted,
marginTop: Spacing.xs,
marginLeft: Spacing.xs,
},
footer: {
padding: Spacing.lg,
backgroundColor: AppColors.surface,
borderTopWidth: 1,
borderTopColor: AppColors.border,
},
saveButton: {
flexDirection: 'row',
backgroundColor: AppColors.primary,
borderRadius: BorderRadius.lg,
paddingVertical: Spacing.md,
alignItems: 'center',
justifyContent: 'center',
gap: Spacing.xs,
...Shadows.primary,
},
saveButtonDisabled: {
opacity: 0.6,
},
saveButtonText: {
fontSize: FontSizes.base,
fontWeight: FontWeights.semibold,
color: AppColors.white,
},
});