Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions frontends/react-native-ios/app.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
"newArchEnabled": true,
"splash": {
"image": "./assets/splash-icon.png",
"resizeMode": "contain",
"resizeMode": "cover",
"backgroundColor": "#0a0a0a"
},
"ios": {
Expand All @@ -29,7 +29,7 @@
{
"backgroundColor": "#0a0a0a",
"image": "./assets/splash-icon.png",
"imageWidth": 200
"resizeMode": "cover"
}
]
],
Expand Down
17 changes: 8 additions & 9 deletions frontends/react-native-ios/app/(app)/(tabs)/home.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,19 @@
* home.tsx
*/

import { useCurrentUser } from '@/api/hooks'
import { DottedBackground } from '@/shared/components'
import type React from 'react'
import { SafeAreaView } from 'react-native-safe-area-context'
import { Stack, Text, YStack } from 'tamagui'
import { useCurrentUser } from '@/api/hooks'
import { colors } from '@/theme/tokens'

export default function HomeScreen(): React.ReactElement {
const { data: user } = useCurrentUser()

return (
<SafeAreaView
style={{ flex: 1, backgroundColor: colors.bgDefault.val }}
edges={['top']}
>
<YStack flex={1} padding="$6">
<DottedBackground>
<SafeAreaView style={{ flex: 1 }} edges={['top']}>
<YStack flex={1} padding="$6">
<Stack marginBottom="$6">
<Text
fontSize={26}
Expand All @@ -43,7 +41,8 @@ export default function HomeScreen(): React.ReactElement {
This is a template home screen. Customize it for your app.
</Text>
</Stack>
</YStack>
</SafeAreaView>
</YStack>
</SafeAreaView>
</DottedBackground>
)
}
44 changes: 26 additions & 18 deletions frontends/react-native-ios/app/(app)/(tabs)/settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,17 @@
* settings.tsx
*/

import { ChevronRight, LogOut, Shield, User } from 'lucide-react-native'
import type React from 'react'
import { SafeAreaView } from 'react-native-safe-area-context'
import { Stack, Switch, Text, YStack } from 'tamagui'
import { useCurrentUser, useLogout } from '@/api/hooks'
import { useBiometricsEnabled, useUIStore } from '@/core/lib'
import { DottedBackground } from '@/shared/components'
import { getBiometricLabel, useBiometrics } from '@/shared/hooks'
import { haptics } from '@/shared/utils'
import { colors } from '@/theme/tokens'
import { router } from 'expo-router'
import { ChevronRight, LogOut, Shield, User } from 'lucide-react-native'
import type React from 'react'
import { SafeAreaView } from 'react-native-safe-area-context'
import { Stack, Switch, Text, YStack } from 'tamagui'

function SettingsRow({
icon,
Expand Down Expand Up @@ -67,11 +69,9 @@ export default function SettingsScreen(): React.ReactElement {
const canUseBiometrics = isAvailable && isEnrolled

return (
<SafeAreaView
style={{ flex: 1, backgroundColor: colors.bgDefault.val }}
edges={['top']}
>
<YStack flex={1} padding="$6">
<DottedBackground>
<SafeAreaView style={{ flex: 1 }} edges={['top']}>
<YStack flex={1} padding="$6">
<Stack marginBottom="$6">
<Text
fontSize={26}
Expand All @@ -97,23 +97,30 @@ export default function SettingsScreen(): React.ReactElement {
<SettingsRow
icon={<User size={18} color={colors.textLight.val} />}
label="Profile"
onPress={() => {}}
onPress={() => router.push('/(app)/profile')}
/>

{canUseBiometrics && (
<SettingsRow
icon={<Shield size={18} color={colors.textLight.val} />}
label={getBiometricLabel(biometryType)}
label={`${getBiometricLabel(biometryType)} Lock`}
rightElement={
<Switch
size="$3"
size="$4"
checked={biometricsEnabled}
onCheckedChange={handleBiometricsToggle}
backgroundColor={
biometricsEnabled ? '$accent' : '$bgSurface200'
}
backgroundColor={biometricsEnabled ? colors.accent.val : colors.bgSurface200.val}
borderWidth={1}
borderColor={biometricsEnabled ? colors.accent.val : colors.borderDefault.val}
width={52}
height={28}
>
<Switch.Thumb backgroundColor="$white" />
<Switch.Thumb
backgroundColor={colors.white.val}
animation="quick"
width={24}
height={24}
/>
</Switch>
}
/>
Expand All @@ -133,7 +140,8 @@ export default function SettingsScreen(): React.ReactElement {
onPress={handleLogout}
/>
</Stack>
</YStack>
</SafeAreaView>
</YStack>
</SafeAreaView>
</DottedBackground>
)
}
224 changes: 224 additions & 0 deletions frontends/react-native-ios/app/(app)/profile.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,224 @@
/**
* @AngelaMos | 2026
* profile.tsx
*/

import { useChangePassword, useCurrentUser } from '@/api/hooks'
import { useUpdateProfile } from '@/api/hooks/useUsers'
import { Button, DottedBackground, Input, PasswordInput } from '@/shared/components'
import { haptics } from '@/shared/utils'
import { router } from 'expo-router'
import { ArrowLeft } from 'lucide-react-native'
import type React from 'react'
import { useEffect, useState } from 'react'
import { Alert, KeyboardAvoidingView, Platform, ScrollView } from 'react-native'
import { SafeAreaView } from 'react-native-safe-area-context'
import { Stack, Text, YStack } from 'tamagui'
import { colors } from '@/theme/tokens'

export default function ProfileScreen(): React.ReactElement {
const { data: user } = useCurrentUser()
const updateProfile = useUpdateProfile()
const changePassword = useChangePassword()

const [fullName, setFullName] = useState('')
const [currentPassword, setCurrentPassword] = useState('')
const [newPassword, setNewPassword] = useState('')
const [confirmPassword, setConfirmPassword] = useState('')

useEffect(() => {
if (user) {
setFullName(user.full_name ?? '')
}
}, [user])

const handleSaveProfile = (): void => {
if (!fullName.trim()) {
haptics.error()
Alert.alert('Error', 'Please enter your name')
return
}

updateProfile.mutate(
{ full_name: fullName.trim() },
{
onSuccess: () => {
haptics.success()
Alert.alert('Success', 'Profile updated')
},
onError: (err) => {
haptics.error()
Alert.alert('Error', err.message)
},
}
)
}

const handleChangePassword = (): void => {
if (!currentPassword || !newPassword || !confirmPassword) {
haptics.error()
Alert.alert('Error', 'Please fill in all password fields')
return
}

if (newPassword !== confirmPassword) {
haptics.error()
Alert.alert('Error', 'New passwords do not match')
return
}

if (newPassword.length < 8) {
haptics.error()
Alert.alert('Error', 'Password must be at least 8 characters')
return
}

changePassword.mutate(
{ current_password: currentPassword, new_password: newPassword },
{
onSuccess: () => {
haptics.success()
Alert.alert('Success', 'Password changed')
setCurrentPassword('')
setNewPassword('')
setConfirmPassword('')
},
onError: (err) => {
haptics.error()
Alert.alert('Error', err.message)
},
}
)
}

return (
<DottedBackground>
<SafeAreaView style={{ flex: 1 }} edges={['top']}>
<KeyboardAvoidingView
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
style={{ flex: 1 }}
>
<ScrollView
contentContainerStyle={{ padding: 24 }}
keyboardShouldPersistTaps="handled"
>
<Stack
flexDirection="row"
alignItems="center"
gap="$2"
marginBottom="$6"
pressStyle={{ opacity: 0.7 }}
onPress={() => router.back()}
>
<ArrowLeft size={18} color={colors.textLight.val} />
<Text fontSize={14} color="$textLight">
Back
</Text>
</Stack>

<Text
fontSize={22}
fontWeight="600"
color="$textDefault"
marginBottom="$6"
>
Profile
</Text>

<YStack gap="$6">
<Stack
backgroundColor="$bgSurface100"
borderWidth={1}
borderColor="$borderDefault"
borderRadius="$3"
padding="$4"
>
<Text
fontSize={14}
fontWeight="500"
color="$textDefault"
marginBottom="$4"
>
Account Info
</Text>

<YStack gap="$3">
<Input
label="Email"
value={user?.email ?? ''}
editable={false}
autoCapitalize="none"
keyboardType="email-address"
/>

<Input
label="Full Name"
value={fullName}
onChangeText={setFullName}
placeholder="Enter your name"
/>

<Stack marginTop="$2">
<Button
onPress={handleSaveProfile}
loading={updateProfile.isPending}
>
{updateProfile.isPending ? 'Saving...' : 'Save Changes'}
</Button>
</Stack>
</YStack>
</Stack>

<Stack
backgroundColor="$bgSurface100"
borderWidth={1}
borderColor="$borderDefault"
borderRadius="$3"
padding="$4"
>
<Text
fontSize={14}
fontWeight="500"
color="$textDefault"
marginBottom="$4"
>
Change Password
</Text>

<YStack gap="$3">
<PasswordInput
label="Current Password"
value={currentPassword}
onChangeText={setCurrentPassword}
/>

<PasswordInput
label="New Password"
value={newPassword}
onChangeText={setNewPassword}
/>

<PasswordInput
label="Confirm New Password"
value={confirmPassword}
onChangeText={setConfirmPassword}
/>

<Stack marginTop="$2">
<Button
variant="secondary"
onPress={handleChangePassword}
loading={changePassword.isPending}
>
{changePassword.isPending ? 'Updating...' : 'Update Password'}
</Button>
</Stack>
</YStack>
</Stack>
</YStack>
</ScrollView>
</KeyboardAvoidingView>
</SafeAreaView>
</DottedBackground>
)
}
Loading
Loading