- 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
375 lines
11 KiB
TypeScript
375 lines
11 KiB
TypeScript
import React, { useState } from 'react';
|
|
import {
|
|
View,
|
|
Text,
|
|
StyleSheet,
|
|
TextInput,
|
|
TouchableOpacity,
|
|
Alert,
|
|
ActivityIndicator,
|
|
ScrollView,
|
|
KeyboardAvoidingView,
|
|
Platform,
|
|
} from 'react-native';
|
|
import { Ionicons } from '@expo/vector-icons';
|
|
import { SafeAreaView } from 'react-native-safe-area-context';
|
|
import { router, useLocalSearchParams } from 'expo-router';
|
|
import { useBeneficiary } from '@/contexts/BeneficiaryContext';
|
|
import {
|
|
AppColors,
|
|
BorderRadius,
|
|
FontSizes,
|
|
FontWeights,
|
|
Spacing,
|
|
Shadows,
|
|
} from '@/constants/theme';
|
|
|
|
type Role = 'caretaker' | 'guardian';
|
|
|
|
export default function ShareAccessScreen() {
|
|
const { id } = useLocalSearchParams<{ id: string }>();
|
|
const { currentBeneficiary } = useBeneficiary();
|
|
|
|
const [email, setEmail] = useState('');
|
|
const [label, setLabel] = useState('');
|
|
const [role, setRole] = useState<Role>('caretaker');
|
|
const [isLoading, setIsLoading] = useState(false);
|
|
|
|
const beneficiaryName = currentBeneficiary?.name || 'this person';
|
|
|
|
const validateEmail = (email: string): boolean => {
|
|
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
return emailRegex.test(email);
|
|
};
|
|
|
|
const handleSendInvite = async () => {
|
|
const trimmedEmail = email.trim().toLowerCase();
|
|
const trimmedLabel = label.trim();
|
|
|
|
if (!trimmedEmail) {
|
|
Alert.alert('Email Required', 'Please enter an email address.');
|
|
return;
|
|
}
|
|
|
|
if (!validateEmail(trimmedEmail)) {
|
|
Alert.alert('Invalid Email', 'Please enter a valid email address.');
|
|
return;
|
|
}
|
|
|
|
setIsLoading(true);
|
|
|
|
try {
|
|
// TODO: Send invitation via API
|
|
// await api.sendInvitation({
|
|
// beneficiaryId: id,
|
|
// email: trimmedEmail,
|
|
// label: trimmedLabel,
|
|
// role: role,
|
|
// });
|
|
|
|
// For now, show success message
|
|
Alert.alert(
|
|
'Invitation Sent',
|
|
`An invitation has been sent to ${trimmedEmail} to ${role === 'guardian' ? 'manage' : 'view'} ${beneficiaryName}.`,
|
|
[{ text: 'OK', onPress: () => router.back() }]
|
|
);
|
|
} catch (error) {
|
|
console.error('Failed to send invitation:', error);
|
|
Alert.alert('Error', 'Failed to send invitation. Please try again.');
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<SafeAreaView style={styles.container} edges={['top', 'bottom']}>
|
|
{/* Header */}
|
|
<View style={styles.header}>
|
|
<TouchableOpacity style={styles.backButton} onPress={() => router.back()}>
|
|
<Ionicons name="arrow-back" size={24} color={AppColors.textPrimary} />
|
|
</TouchableOpacity>
|
|
<Text style={styles.headerTitle}>Share Access</Text>
|
|
<View style={styles.placeholder} />
|
|
</View>
|
|
|
|
<KeyboardAvoidingView
|
|
style={styles.content}
|
|
behavior={Platform.OS === 'ios' ? 'padding' : undefined}
|
|
>
|
|
<ScrollView
|
|
contentContainerStyle={styles.scrollContent}
|
|
showsVerticalScrollIndicator={false}
|
|
>
|
|
{/* Info Banner */}
|
|
<View style={styles.infoBanner}>
|
|
<Ionicons name="people" size={24} color={AppColors.primary} />
|
|
<Text style={styles.infoBannerText}>
|
|
Invite family members or caregivers to help monitor {beneficiaryName}
|
|
</Text>
|
|
</View>
|
|
|
|
{/* Email Input */}
|
|
<View style={styles.inputGroup}>
|
|
<Text style={styles.inputLabel}>Email Address</Text>
|
|
<View style={styles.inputContainer}>
|
|
<Ionicons name="mail-outline" size={20} color={AppColors.textMuted} />
|
|
<TextInput
|
|
style={styles.input}
|
|
placeholder="Enter email address"
|
|
placeholderTextColor={AppColors.textMuted}
|
|
value={email}
|
|
onChangeText={setEmail}
|
|
autoCapitalize="none"
|
|
autoCorrect={false}
|
|
keyboardType="email-address"
|
|
editable={!isLoading}
|
|
/>
|
|
</View>
|
|
</View>
|
|
|
|
{/* Label Input */}
|
|
<View style={styles.inputGroup}>
|
|
<Text style={styles.inputLabel}>Label (optional)</Text>
|
|
<View style={styles.inputContainer}>
|
|
<Ionicons name="pricetag-outline" size={20} color={AppColors.textMuted} />
|
|
<TextInput
|
|
style={styles.input}
|
|
placeholder="e.g., Sister, Nurse, Neighbor"
|
|
placeholderTextColor={AppColors.textMuted}
|
|
value={label}
|
|
onChangeText={setLabel}
|
|
autoCapitalize="words"
|
|
editable={!isLoading}
|
|
/>
|
|
</View>
|
|
</View>
|
|
|
|
{/* Role Selection */}
|
|
<View style={styles.inputGroup}>
|
|
<Text style={styles.inputLabel}>Access Level</Text>
|
|
|
|
<TouchableOpacity
|
|
style={[styles.roleOption, role === 'caretaker' && styles.roleOptionSelected]}
|
|
onPress={() => setRole('caretaker')}
|
|
disabled={isLoading}
|
|
>
|
|
<View style={styles.roleHeader}>
|
|
<View style={[styles.roleIcon, role === 'caretaker' && styles.roleIconSelected]}>
|
|
<Ionicons
|
|
name="eye-outline"
|
|
size={20}
|
|
color={role === 'caretaker' ? AppColors.white : AppColors.primary}
|
|
/>
|
|
</View>
|
|
<View style={styles.roleInfo}>
|
|
<Text style={[styles.roleTitle, role === 'caretaker' && styles.roleTitleSelected]}>
|
|
Caretaker
|
|
</Text>
|
|
<Text style={styles.roleDescription}>
|
|
Can view activity and chat with Julia
|
|
</Text>
|
|
</View>
|
|
{role === 'caretaker' && (
|
|
<Ionicons name="checkmark-circle" size={24} color={AppColors.primary} />
|
|
)}
|
|
</View>
|
|
</TouchableOpacity>
|
|
|
|
<TouchableOpacity
|
|
style={[styles.roleOption, role === 'guardian' && styles.roleOptionSelected]}
|
|
onPress={() => setRole('guardian')}
|
|
disabled={isLoading}
|
|
>
|
|
<View style={styles.roleHeader}>
|
|
<View style={[styles.roleIcon, role === 'guardian' && styles.roleIconSelected]}>
|
|
<Ionicons
|
|
name="shield-outline"
|
|
size={20}
|
|
color={role === 'guardian' ? AppColors.white : AppColors.accent}
|
|
/>
|
|
</View>
|
|
<View style={styles.roleInfo}>
|
|
<Text style={[styles.roleTitle, role === 'guardian' && styles.roleTitleSelected]}>
|
|
Guardian
|
|
</Text>
|
|
<Text style={styles.roleDescription}>
|
|
Full access: edit info, manage subscription, invite others
|
|
</Text>
|
|
</View>
|
|
{role === 'guardian' && (
|
|
<Ionicons name="checkmark-circle" size={24} color={AppColors.accent} />
|
|
)}
|
|
</View>
|
|
</TouchableOpacity>
|
|
</View>
|
|
|
|
{/* Send Button */}
|
|
<TouchableOpacity
|
|
style={[styles.sendButton, isLoading && styles.sendButtonDisabled]}
|
|
onPress={handleSendInvite}
|
|
disabled={isLoading}
|
|
>
|
|
{isLoading ? (
|
|
<ActivityIndicator color={AppColors.white} />
|
|
) : (
|
|
<>
|
|
<Ionicons name="send" size={20} color={AppColors.white} />
|
|
<Text style={styles.sendButtonText}>Send Invitation</Text>
|
|
</>
|
|
)}
|
|
</TouchableOpacity>
|
|
|
|
{/* Help Text */}
|
|
<Text style={styles.helpText}>
|
|
The person will receive an email with instructions to access {beneficiaryName}'s information.
|
|
</Text>
|
|
</ScrollView>
|
|
</KeyboardAvoidingView>
|
|
</SafeAreaView>
|
|
);
|
|
}
|
|
|
|
const styles = StyleSheet.create({
|
|
container: {
|
|
flex: 1,
|
|
backgroundColor: AppColors.background,
|
|
},
|
|
header: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
justifyContent: 'space-between',
|
|
paddingHorizontal: Spacing.md,
|
|
paddingVertical: Spacing.sm,
|
|
borderBottomWidth: 1,
|
|
borderBottomColor: AppColors.border,
|
|
},
|
|
backButton: {
|
|
padding: Spacing.xs,
|
|
},
|
|
headerTitle: {
|
|
fontSize: FontSizes.lg,
|
|
fontWeight: FontWeights.semibold,
|
|
color: AppColors.textPrimary,
|
|
},
|
|
placeholder: {
|
|
width: 32,
|
|
},
|
|
content: {
|
|
flex: 1,
|
|
},
|
|
scrollContent: {
|
|
padding: Spacing.lg,
|
|
paddingBottom: Spacing.xxl,
|
|
},
|
|
infoBanner: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
backgroundColor: AppColors.primaryLighter,
|
|
padding: Spacing.md,
|
|
borderRadius: BorderRadius.lg,
|
|
marginBottom: Spacing.xl,
|
|
gap: Spacing.md,
|
|
},
|
|
infoBannerText: {
|
|
flex: 1,
|
|
fontSize: FontSizes.sm,
|
|
color: AppColors.textPrimary,
|
|
lineHeight: 20,
|
|
},
|
|
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,
|
|
paddingVertical: Spacing.md,
|
|
marginLeft: Spacing.sm,
|
|
fontSize: FontSizes.base,
|
|
color: AppColors.textPrimary,
|
|
},
|
|
roleOption: {
|
|
backgroundColor: AppColors.surface,
|
|
borderRadius: BorderRadius.lg,
|
|
padding: Spacing.md,
|
|
marginBottom: Spacing.sm,
|
|
borderWidth: 2,
|
|
borderColor: AppColors.border,
|
|
},
|
|
roleOptionSelected: {
|
|
borderColor: AppColors.primary,
|
|
backgroundColor: AppColors.primaryLighter,
|
|
},
|
|
roleHeader: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
},
|
|
roleIcon: {
|
|
width: 40,
|
|
height: 40,
|
|
borderRadius: BorderRadius.md,
|
|
backgroundColor: AppColors.surfaceSecondary,
|
|
justifyContent: 'center',
|
|
alignItems: 'center',
|
|
marginRight: Spacing.md,
|
|
},
|
|
roleIconSelected: {
|
|
backgroundColor: AppColors.primary,
|
|
},
|
|
roleInfo: {
|
|
flex: 1,
|
|
},
|
|
roleTitle: {
|
|
fontSize: FontSizes.base,
|
|
fontWeight: FontWeights.semibold,
|
|
color: AppColors.textPrimary,
|
|
},
|
|
roleTitleSelected: {
|
|
color: AppColors.primary,
|
|
},
|
|
roleDescription: {
|
|
fontSize: FontSizes.sm,
|
|
color: AppColors.textSecondary,
|
|
marginTop: 2,
|
|
},
|
|
sendButton: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
backgroundColor: AppColors.primary,
|
|
paddingVertical: Spacing.md,
|
|
borderRadius: BorderRadius.lg,
|
|
marginTop: Spacing.lg,
|
|
gap: Spacing.sm,
|
|
...Shadows.primary,
|
|
},
|
|
sendButtonDisabled: {
|
|
opacity: 0.7,
|
|
},
|
|
sendButtonText: {
|
|
fontSize: FontSizes.base,
|
|
fontWeight: FontWeights.semibold,
|
|
color: AppColors.white,
|
|
},
|
|
helpText: {
|
|
fontSize: FontSizes.sm,
|
|
color: AppColors.textMuted,
|
|
textAlign: 'center',
|
|
marginTop: Spacing.lg,
|
|
lineHeight: 20,
|
|
},
|
|
});
|