mobileapp / src /screens /onboarding /ProfileSetupScreen.tsx
Antaram Dev Bot
feat: complete ANTARAM.ORG ride-sharing app frontend
5c876be
import React, { useState, useEffect, useMemo } from 'react';
import { View, StyleSheet } from 'react-native';
import {
Text,
TextInput,
Button,
Avatar,
Surface,
SegmentedButtons,
useTheme,
Appbar,
} from 'react-native-paper';
import { useNavigation } from '@react-navigation/native';
import type { NativeStackNavigationProp } from '@react-navigation/native-stack';
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
import { useAuthStore } from '../../store/useAuthStore';
import { useCurrentUser } from '../../hooks/useCurrentUser';
import type { User, UserRole } from '../../types/user';
// ─── Types ─────────────────────────────────────────────────────────────────────
type RootStackParamList = {
ProfileSetup: undefined;
Main: undefined;
};
type NavigationProp = NativeStackNavigationProp<RootStackParamList, 'ProfileSetup'>;
// ─── ProfileSetupScreen Component ──────────────────────────────────────────────
export default function ProfileSetupScreen() {
const theme = useTheme();
const navigation = useNavigation<NavigationProp>();
const { user, setAuth, setOnboarded, updateUser } = useAuthStore();
const { updateProfile } = useCurrentUser();
const [displayName, setDisplayName] = useState(user?.displayName ?? '');
const [phone, setPhone] = useState(user?.phone ?? '');
const [selectedRole, setSelectedRole] = useState<UserRole>('rider');
const [saving, setSaving] = useState(false);
// Pre-fill display name from auth store when it changes
useEffect(() => {
if (user?.displayName) {
setDisplayName(user.displayName);
}
}, [user?.displayName]);
// Get initials from display name for avatar
const initials = useMemo(() => {
if (!displayName.trim()) return '?';
return displayName
.trim()
.split(' ')
.map((word) => word[0])
.join('')
.toUpperCase()
.slice(0, 2);
}, [displayName]);
// ── Handle Submit ─────────────────────────────────────────────────────────────
const handleSubmit = async () => {
if (!displayName.trim()) return;
setSaving(true);
try {
// Update profile in SQLite via hook
await updateProfile({
fullName: displayName.trim(),
phone: phone.trim() || null,
});
// Update auth store with profile data
updateUser({
displayName: displayName.trim(),
phone: phone.trim() || null,
currentRole: selectedRole,
});
// Mark onboarding as complete
setOnboarded(true);
// Navigate to main app
navigation.replace('Main');
} catch (error) {
console.error('[ProfileSetupScreen] Failed to save profile:', error);
} finally {
setSaving(false);
}
};
// ── Render ────────────────────────────────────────────────────────────────────
return (
<Surface style={styles.container}>
{/* Appbar */}
<Appbar.Header
style={{ backgroundColor: theme.colors.surface }}
elevation={0}
>
<Appbar.BackAction
onPress={() => navigation.goBack()}
color={theme.colors.onSurface}
/>
<Appbar.Content
title="Complete Your Profile"
titleStyle={{ color: theme.colors.onSurface, fontWeight: '600' }}
/>
</Appbar.Header>
<View style={styles.content}>
{/* Avatar */}
<View style={styles.avatarContainer}>
<Avatar.Text
size={96}
label={initials}
style={[
styles.avatar,
{ backgroundColor: theme.colors.primaryContainer },
]}
labelStyle={[
styles.avatarLabel,
{ color: theme.colors.onPrimaryContainer },
]}
/>
<Text
variant="bodySmall"
style={[styles.tapToChange, { color: theme.colors.primary }]}
>
Tap to change
</Text>
</View>
{/* Display Name */}
<TextInput
mode="outlined"
label="Display Name"
value={displayName}
onChangeText={setDisplayName}
placeholder="Enter your name"
style={styles.input}
outlineColor={theme.colors.outline}
activeOutlineColor={theme.colors.primary}
/>
{/* Phone */}
<TextInput
mode="outlined"
label="Phone Number"
value={phone}
onChangeText={(text) => {
// Allow only digits after +91 prefix
const cleaned = text.replace(/[^0-9+]/g, '');
if (!cleaned.startsWith('+91')) {
setPhone(cleaned ? `+91${cleaned.replace('+91', '')}` : '');
} else {
setPhone(cleaned);
}
}}
placeholder="+91"
keyboardType="phone-pad"
maxLength={15}
style={styles.input}
outlineColor={theme.colors.outline}
activeOutlineColor={theme.colors.primary}
left={<TextInput.Affix text="+91" textStyle={{ color: theme.colors.onSurfaceVariant }} />}
/>
{/* Role Selection */}
<Text
variant="titleSmall"
style={[styles.roleLabel, { color: theme.colors.onSurface }]}
>
How will you use Antaram?
</Text>
<SegmentedButtons
value={selectedRole}
onValueChange={(value) => setSelectedRole(value as UserRole)}
buttons={[
{
value: 'rider',
label: 'πŸš— Rider',
icon: () => (
<MaterialCommunityIcons
name="steering"
size={18}
color={
selectedRole === 'rider'
? theme.colors.onPrimary
: theme.colors.onSurface
}
/>
),
},
{
value: 'passenger',
label: 'πŸ§‘ Passenger',
icon: () => (
<MaterialCommunityIcons
name="account-walk"
size={18}
color={
selectedRole === 'passenger'
? theme.colors.onPrimary
: theme.colors.onSurface
}
/>
),
},
]}
style={styles.segmentedButtons}
/>
{/* Submit */}
<Button
mode="contained"
onPress={handleSubmit}
loading={saving}
disabled={!displayName.trim() || saving}
style={styles.submitButton}
buttonColor={theme.colors.primary}
textColor={theme.colors.onPrimary}
contentStyle={styles.submitButtonContent}
>
Let's Go!
</Button>
</View>
</Surface>
);
}
// ─── Styles ────────────────────────────────────────────────────────────────────
const styles = StyleSheet.create({
container: {
flex: 1,
},
content: {
flex: 1,
paddingHorizontal: 24,
paddingTop: 16,
paddingBottom: 32,
},
avatarContainer: {
alignItems: 'center',
marginBottom: 32,
},
avatar: {
elevation: 2,
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1,
shadowRadius: 4,
},
avatarLabel: {
fontWeight: '600',
},
tapToChange: {
marginTop: 8,
},
input: {
marginBottom: 16,
},
roleLabel: {
marginBottom: 12,
fontWeight: '600',
},
segmentedButtons: {
marginBottom: 32,
},
submitButton: {
marginTop: 'auto',
},
submitButtonContent: {
paddingVertical: 6,
},
});