WellNuo/app/(auth)/add-loved-one.tsx
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

365 lines
10 KiB
TypeScript

import React, { useState } from 'react';
import {
View,
Text,
StyleSheet,
KeyboardAvoidingView,
Platform,
ScrollView,
TouchableOpacity,
TextInput,
Image,
Alert,
} from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import { router, useLocalSearchParams } from 'expo-router';
import { Ionicons } from '@expo/vector-icons';
import * as ImagePicker from 'expo-image-picker';
import { Button } from '@/components/ui/Button';
import { ErrorMessage } from '@/components/ui/ErrorMessage';
import { AppColors, FontSizes, Spacing, BorderRadius, FontWeights } from '@/constants/theme';
import { api } from '@/services/api';
export default function AddLovedOneScreen() {
const params = useLocalSearchParams<{ email: string; partnerCode: string }>();
const partnerCode = params.partnerCode || '';
const [name, setName] = useState('');
const [address, setAddress] = useState('');
const [avatarUri, setAvatarUri] = useState<string | null>(null);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const handlePickAvatar = async () => {
const { status } = await ImagePicker.requestMediaLibraryPermissionsAsync();
if (status !== 'granted') {
Alert.alert('Permission needed', 'Please allow access to your photo library.');
return;
}
const result = await ImagePicker.launchImageLibraryAsync({
mediaTypes: ['images'],
allowsEditing: true,
aspect: [1, 1],
quality: 0.5,
});
if (!result.canceled && result.assets[0]) {
setAvatarUri(result.assets[0].uri);
}
};
const handleTakePhoto = async () => {
const { status } = await ImagePicker.requestCameraPermissionsAsync();
if (status !== 'granted') {
Alert.alert('Permission needed', 'Please allow access to your camera.');
return;
}
const result = await ImagePicker.launchCameraAsync({
allowsEditing: true,
aspect: [1, 1],
quality: 0.5,
});
if (!result.canceled && result.assets[0]) {
setAvatarUri(result.assets[0].uri);
}
};
const handleAvatarPress = () => {
Alert.alert(
'Add Photo',
'Choose how to add a photo',
[
{ text: 'Take Photo', onPress: handleTakePhoto },
{ text: 'Choose from Library', onPress: handlePickAvatar },
{ text: 'Cancel', style: 'cancel' },
]
);
};
const handleContinue = async () => {
setError(null);
const trimmedName = name.trim();
if (!trimmedName) {
setError('Please enter the name of your loved one');
return;
}
setIsLoading(true);
try {
// Navigate to the purchase/subscription screen with all data
router.replace({
pathname: '/(auth)/purchase',
params: {
lovedOneName: trimmedName,
lovedOneAddress: address.trim(),
lovedOneAvatar: avatarUri || '',
partnerCode,
},
});
} catch (err) {
setError(err instanceof Error ? err.message : 'Something went wrong');
} finally {
setIsLoading(false);
}
};
const handleSkip = async () => {
// Mark onboarding as completed so we don't redirect back here
await api.setOnboardingCompleted(true);
// Skip and go to main app without adding loved one
router.replace('/(tabs)');
};
const nameInitial = name.trim() ? name.trim().charAt(0).toUpperCase() : '+';
return (
<SafeAreaView style={styles.container} edges={['top', 'bottom']}>
<KeyboardAvoidingView
style={styles.keyboardView}
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
>
<ScrollView
contentContainerStyle={styles.scrollContent}
keyboardShouldPersistTaps="handled"
showsVerticalScrollIndicator={false}
>
{/* Header */}
<View style={styles.header}>
<Text style={styles.title}>Add a Loved One</Text>
<Text style={styles.subtitle}>
Tell us about the person you want to care for
</Text>
</View>
{/* Error Message */}
{error && (
<ErrorMessage
message={error}
onDismiss={() => setError(null)}
/>
)}
{/* Avatar Section */}
<TouchableOpacity style={styles.avatarSection} onPress={handleAvatarPress}>
<View style={styles.avatarContainer}>
{avatarUri ? (
<Image source={{ uri: avatarUri }} style={styles.avatarImage} />
) : (
<Text style={styles.avatarText}>{nameInitial}</Text>
)}
<View style={styles.avatarEditBadge}>
<Ionicons name="camera" size={18} color={AppColors.white} />
</View>
</View>
<Text style={styles.avatarHint}>Tap to add photo</Text>
</TouchableOpacity>
{/* Form */}
<View style={styles.form}>
<View style={styles.inputGroup}>
<Text style={styles.inputLabel}>Name *</Text>
<View style={styles.inputContainer}>
<Ionicons name="person-outline" size={20} color={AppColors.textMuted} />
<TextInput
style={styles.input}
value={name}
onChangeText={(text) => {
setName(text);
setError(null);
}}
placeholder="e.g., Grandma Julia"
placeholderTextColor={AppColors.textMuted}
autoCapitalize="words"
autoCorrect={false}
editable={!isLoading}
/>
</View>
</View>
<View style={styles.inputGroup}>
<Text style={styles.inputLabel}>Address (optional)</Text>
<View style={[styles.inputContainer, styles.addressInput]}>
<Ionicons name="location-outline" size={20} color={AppColors.textMuted} style={styles.addressIcon} />
<TextInput
style={[styles.input, styles.addressTextInput]}
value={address}
onChangeText={setAddress}
placeholder="123 Main St, City, State"
placeholderTextColor={AppColors.textMuted}
multiline
numberOfLines={2}
editable={!isLoading}
/>
</View>
</View>
</View>
{/* Continue Button */}
<View style={styles.buttonContainer}>
<Button
title="Continue"
onPress={handleContinue}
loading={isLoading}
fullWidth
size="lg"
/>
</View>
{/* Info */}
<View style={styles.infoContainer}>
<Text style={styles.infoText}>
You'll be able to add more loved ones later and invite family members to help care for them
</Text>
</View>
{/* Skip Button */}
<TouchableOpacity style={styles.skipButton} onPress={handleSkip}>
<Text style={styles.skipText}>Skip for now</Text>
</TouchableOpacity>
</ScrollView>
</KeyboardAvoidingView>
</SafeAreaView>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: AppColors.background,
},
keyboardView: {
flex: 1,
},
scrollContent: {
flexGrow: 1,
paddingHorizontal: Spacing.lg,
paddingTop: Spacing.xl,
paddingBottom: Spacing.lg,
},
header: {
alignItems: 'center',
marginBottom: Spacing.xl,
},
title: {
fontSize: FontSizes['2xl'],
fontWeight: FontWeights.bold,
color: AppColors.textPrimary,
marginBottom: Spacing.md,
textAlign: 'center',
},
subtitle: {
fontSize: FontSizes.base,
color: AppColors.textSecondary,
textAlign: 'center',
},
avatarSection: {
alignItems: 'center',
marginBottom: Spacing.xl,
},
avatarContainer: {
width: 120,
height: 120,
borderRadius: 60,
backgroundColor: AppColors.primaryLight,
justifyContent: 'center',
alignItems: 'center',
position: 'relative',
},
avatarImage: {
width: 120,
height: 120,
borderRadius: 60,
},
avatarText: {
fontSize: FontSizes['3xl'],
fontWeight: FontWeights.bold,
color: AppColors.primary,
},
avatarEditBadge: {
position: 'absolute',
bottom: 4,
right: 4,
width: 36,
height: 36,
borderRadius: 18,
backgroundColor: AppColors.primary,
justifyContent: 'center',
alignItems: 'center',
borderWidth: 3,
borderColor: AppColors.background,
},
avatarHint: {
fontSize: FontSizes.sm,
color: AppColors.textMuted,
marginTop: Spacing.sm,
},
form: {
marginBottom: Spacing.lg,
},
inputGroup: {
marginBottom: Spacing.lg,
},
inputLabel: {
fontSize: FontSizes.sm,
fontWeight: FontWeights.medium,
color: AppColors.textSecondary,
marginBottom: Spacing.sm,
},
inputContainer: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: AppColors.surface,
borderRadius: BorderRadius.lg,
paddingHorizontal: Spacing.md,
borderWidth: 1,
borderColor: AppColors.border,
},
input: {
flex: 1,
fontSize: FontSizes.base,
color: AppColors.textPrimary,
paddingVertical: Spacing.md,
marginLeft: Spacing.sm,
},
addressInput: {
alignItems: 'flex-start',
},
addressIcon: {
marginTop: Spacing.md,
},
addressTextInput: {
minHeight: 60,
textAlignVertical: 'top',
},
buttonContainer: {
marginTop: Spacing.md,
},
infoContainer: {
alignItems: 'center',
marginTop: Spacing.lg,
paddingHorizontal: Spacing.md,
},
infoText: {
fontSize: FontSizes.sm,
color: AppColors.textMuted,
textAlign: 'center',
lineHeight: 20,
},
skipButton: {
alignItems: 'center',
paddingVertical: Spacing.lg,
marginTop: Spacing.xl,
},
skipText: {
fontSize: FontSizes.base,
color: AppColors.textSecondary,
textDecorationLine: 'underline',
},
});