Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
119 changes: 119 additions & 0 deletions mobile/app/(auth)/login.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import { View, Text, Button, StyleSheet, TextInput, ActivityIndicator } from 'react-native';
import { Link } from 'expo-router';
import { Formik } from 'formik';
import * as Yup from 'yup';
import { useAuth } from '../../context/AuthContext';
import authApi from '../../services/authApi';
import { useState } from 'react';

const LoginSchema = Yup.object().shape({
email: Yup.string().email('Invalid email').required('Email is required'),
password: Yup.string().required('Password is required'),
});

export default function LoginScreen() {
const { login } = useAuth();
const [error, setError] = useState<string | null>(null);

const handleLogin = async (values: any) => {
setError(null);
try {
const response = await authApi.login({ email: values.email, password: values.password });
if (response.data && response.data.token) {
await login(response.data.token);
} else {
setError('Login failed: No token received.');
}
} catch (err: any) {
const errorMessage = err.response?.data?.message || 'Invalid credentials or server error.';
setError(errorMessage);
}
};

return (
<View style={styles.container}>
<Text style={styles.title}>Welcome Back!</Text>

<Formik
initialValues={{ email: '', password: '' }}
validationSchema={LoginSchema}
onSubmit={handleLogin}
>
{({ handleChange, handleBlur, handleSubmit, values, errors, touched, isSubmitting }) => (
<View>
<TextInput
style={styles.input}
placeholder="Email"
onChangeText={handleChange('email')}
onBlur={handleBlur('email')}
value={values.email}
keyboardType="email-address"
autoCapitalize="none"
/>
{errors.email && touched.email ? <Text style={styles.errorText}>{errors.email}</Text> : null}

<TextInput
style={styles.input}
placeholder="Password"
onChangeText={handleChange('password')}
onBlur={handleBlur('password')}
value={values.password}
secureTextEntry
/>
{errors.password && touched.password ? <Text style={styles.errorText}>{errors.password}</Text> : null}

{error && <Text style={styles.errorText}>{error}</Text>}

{isSubmitting ? (
<ActivityIndicator size="large" style={{ marginTop: 10 }} />
) : (
<Button onPress={() => handleSubmit()} title="Login" />
)}
</View>
)}
</Formik>

<View style={styles.linkContainer}>
<Text>Don't have an account? </Text>
<Link href="/signup" style={styles.link}>
Sign Up
</Link>
</View>
</View>
);
}

const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
padding: 20,
},
title: {
fontSize: 28,
fontWeight: 'bold',
textAlign: 'center',
marginBottom: 24,
},
input: {
height: 40,
borderColor: 'gray',
borderWidth: 1,
borderRadius: 5,
marginBottom: 10,
paddingHorizontal: 10,
},
errorText: {
color: 'red',
marginBottom: 10,
},
linkContainer: {
flexDirection: 'row',
justifyContent: 'center',
marginTop: 20,
},
link: {
color: 'blue',
fontWeight: 'bold',
},
});
24 changes: 23 additions & 1 deletion mobile/app/_layout.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,31 @@
import { Stack } from 'expo-router';
import { AuthProvider, useAuth } from '../context/AuthContext';
import { ActivityIndicator, View } from 'react-native';

const InitialLayout = () => {
const { isLoading, token } = useAuth();

if (isLoading) {
return (
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
<ActivityIndicator size="large" />
</View>
);
}

export default function RootLayout() {
return (
<Stack>
<Stack.Screen name="(tabs)" options={{ headerShown: false }} />
<Stack.Screen name="(auth)" options={{ headerShown: false }} />
</Stack>
);
};


export default function RootLayout() {
return (
<AuthProvider>
<InitialLayout />
</AuthProvider>
);
}
80 changes: 80 additions & 0 deletions mobile/context/AuthContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { createContext, useContext, useState, useEffect } from 'react';
import { useRouter, useSegments } from 'expo-router';
import authStorage from '../utils/authStorage';

interface AuthContextType {
token: string | null;
login: (newToken: string) => void;
logout: () => void;
isAuthenticated: boolean;
isLoading: boolean;
}

const AuthContext = createContext<AuthContextType | null>(null);

export function useAuth() {
const context = useContext(AuthContext);
if (!context) {
throw new Error('useAuth must be used within an AuthProvider');
}
return context;
}

export function AuthProvider({ children }: { children: React.ReactNode }) {
const [token, setToken] = useState<string | null>(null);
const [isLoading, setIsLoading] = useState(true);
const segments = useSegments();
const router = useRouter();

useEffect(() => {

const loadToken = async () => {
const storedToken = await authStorage.getToken();
if (storedToken) {
setToken(storedToken);
}
setIsLoading(false);
};

loadToken();
}, []);

useEffect(() => {
if (isLoading) return;

const inAuthGroup = segments[0] === '(auth)';

if (token && inAuthGroup) {


router.replace('/');
} else if (!token && !inAuthGroup) {


router.replace('/login');
}
}, [token, segments, isLoading]);


const login = async (newToken: string) => {
setToken(newToken);
await authStorage.storeToken(newToken);
router.replace('/');
};

const logout = async () => {
setToken(null);
await authStorage.removeToken();
router.replace('/login');
};

const value = {
token,
login,
logout,
isAuthenticated: !!token,
isLoading,
};

return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
}
4 changes: 3 additions & 1 deletion mobile/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
"expo-symbols": "~1.0.7",
"expo-system-ui": "~6.0.7",
"expo-web-browser": "~15.0.7",
"formik": "^2.4.6",
"react": "19.1.0",
"react-dom": "19.1.0",
"react-native": "0.81.4",
Expand All @@ -38,7 +39,8 @@
"react-native-safe-area-context": "~5.6.0",
"react-native-screens": "~4.16.0",
"react-native-web": "~0.21.0",
"react-native-worklets": "0.5.1"
"react-native-worklets": "0.5.1",
"yup": "^1.7.1"
},
"devDependencies": {
"@types/react": "~19.1.0",
Expand Down
34 changes: 34 additions & 0 deletions mobile/services/authApi.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import apiClient from './apiClient';
import { AxiosResponse } from 'axios';


interface AuthPayload {
email: string;
password: string;
}

interface SignUpPayload extends AuthPayload {
name: string;
}

interface AuthResponse {
message: string;
token: string;

user?: { id: string; name: string; email: string };
}


const signUp = (payload: SignUpPayload): Promise<AxiosResponse<AuthResponse>> => {
return apiClient.post('/auth/signup', payload);
};

const login = (payload: AuthPayload): Promise<AxiosResponse<AuthResponse>> => {
return apiClient.post('/auth/login', payload);
};


export default {
signUp,
login,
};
Loading
Loading