Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
b4ef3a0
feat(Authentication): added a new folders that contain token save, ge…
diego31anaya Oct 2, 2026
01c7374
feat(Authentication): Added error session state and session action fo…
diego31anaya Oct 3, 2026
a4f4eff
feat(Authentication): Added signIn, signOut, restoreSession logic wit…
diego31anaya Oct 3, 2026
6a4ffc1
feat(Authentication): Added sign out to profile.tsx with InlineError …
diego31anaya Oct 6, 2026
36a10a4
feat(Authentication): Added sign in to sign-in.tsx with InlineError a…
diego31anaya Oct 6, 2026
5308639
feat(Authentication): Added expiredSessoin to SessionProvider.tsx. Fu…
diego31anaya Oct 7, 2026
f46bedf
Added error state status state with sign out + retry in root layo
diego31anaya Oct 8, 2026
2a9079c
feat(Authentication): Added test
diego31anaya Oct 11, 2026
4646390
test(Authentication): updated SessionProvider test with QueryProvider
diego31anaya Oct 11, 2026
0aad842
fix(Authentication): added AppError + ConnectionBanner to sign-in.tsx
diego31anaya Oct 11, 2026
aaee854
fix(Authentication): Added AppError and ConnectionBanner to profile.tsx
diego31anaya Oct 11, 2026
ec0c469
fix(Authentication): Added error screen for root layout
diego31anaya Oct 11, 2026
b261dbb
fix(Authentication): Added fakeAuthgateway for dev only testing
diego31anaya Oct 11, 2026
53e4012
test(Authentication): Added authentication testing and modified exist…
diego31anaya Oct 11, 2026
e6c9c1b
feat(Authentication): AuthGateway connected to apiClient, JWT decodin…
diego31anaya Oct 11, 2026
a1207de
fix(Authentication): deleted secureStore and studentToken
diego31anaya Oct 11, 2026
9b9e399
fix(Authentication): Fixed SessionUser to work with backedn and forma…
diego31anaya Oct 11, 2026
ef3f039
fix(Authentication): Added fakeApiClient to AuthGateway.ts and tempor…
diego31anaya Oct 11, 2026
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
51 changes: 49 additions & 2 deletions app/(app)/(tabs)/profile.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,12 @@ import {
spacing,
} from '@/design/tokens';
import { useSession } from '@/providers/SessionProvider';
import { useState } from 'react';
import { InlineError } from '@/components/feedback/InlineError';
import { ConnectionBanner } from '@/components/feedback/ConnectionBanner';
import { Spinner } from '@/components/ui/Spinner';
// Temporary AppError import until API is merged
import { AppError } from '@/services/storage/TokenStore';

type Badge = {
id: string;
Expand Down Expand Up @@ -139,6 +145,36 @@ export default function ProfileRoute() {
session.status === 'authenticated' ? session.user.role : 'student';
const initial = displayName.charAt(0).toUpperCase();
const earnedCount = BADGES.filter((b) => b.earned).length;
const [signingOut, setSigningOut] = useState(false);
const [error, setError] = useState('');
const [connectionError, setConnectionError] = useState('');

const signOutPressed = async () => {
setSigningOut(true);
setError('');
setConnectionError('');

try {
await signOut();
} catch (error) {
setSigningOut(false);
if (error instanceof AppError) {
switch (error.code) {
case 'NETWORK_ERROR':
setConnectionError(error.message);
break;
case 'REQUEST_TIMEOUT':
setConnectionError(error.message);
break;
default:
setError(error.message);
break;
}
} else {
setError('Sign out failed. Please try again.');
}
}
};

return (
<SafeAreaView style={s.safe}>
Expand Down Expand Up @@ -258,15 +294,26 @@ export default function ProfileRoute() {
<Pressable
accessibilityLabel="Sign out"
accessibilityRole="button"
onPress={signOut}
onPress={signOutPressed}
disabled={signingOut}
style={({ pressed }) => [
s.signOutBtn,
pressed && s.signOutBtnPressed,
]}
>
<Text style={s.signOutText}>Sign out</Text>
{signingOut ? (
<Spinner label="Signing out" />
) : (
<Text style={s.signOutText}>Sign out</Text>
)}
</Pressable>

{error && <InlineError message={error} />}

{connectionError && (
<ConnectionBanner message={connectionError} status="offline" />
)}

<View style={{ height: spacing.xl }} />
</ScrollView>
</SafeAreaView>
Expand Down
126 changes: 97 additions & 29 deletions app/(public)/sign-in.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,17 +26,29 @@ import {
radii,
spacing,
} from '@/design/tokens';

import { useSession } from '@/providers/SessionProvider';
import { Spinner } from '@/components/ui/Spinner';
import { InlineError } from '@/components/feedback/InlineError';
import { ConnectionBanner } from '@/components/feedback/ConnectionBanner';
import { isNonEmptyString } from '@/utils/isNonEmptyString';
// Temporary AppError import until API is merged
import { AppError } from '@/services/storage/TokenStore';

const chessPiecePattern = require('@/assets/images/chess-piece-pattern.png');

export default function SignInRoute() {
const { mockSignInAs } = useSession();
const { mockSignInAs, signIn, session } = useSession();
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
const [usernameFocused, setUsernameFocused] = useState(false);
const [passwordFocused, setPasswordFocused] = useState(false);
const [showPassword, setShowPassword] = useState(false);
const [signingIn, setSigningIn] = useState(false);
const [error, setError] = useState(
session.status === 'expired' ? session.expiredMessage : '',
);
const [connectionError, setConnectionError] = useState('');

const [btnScale] = useState(() => new Animated.Value(1));
const pressIn = () =>
Expand All @@ -54,6 +66,41 @@ export default function SignInRoute() {
bounciness: 4,
}).start();

const signInPressed = async () => {
setError('');
setConnectionError('');
setSigningIn(true);

if (!isNonEmptyString(username) || !isNonEmptyString(password)) {
setSigningIn(false);

setError('Please enter your username and password');

return;
}

try {
await signIn(username, password);
} catch (error) {
setSigningIn(false);
if (error instanceof AppError) {
switch (error.code) {
case 'NETWORK_ERROR':
setConnectionError(error.message);
break;
case 'REQUEST_TIMEOUT':
setConnectionError(error.message);
break;
default:
setError(error.message);
break;
}
} else {
setError('Sign in failed. Please try again.');
}
}
};

return (
<SafeAreaView style={s.safe}>
{/* ── Patterned Background ──────────────── */}
Expand Down Expand Up @@ -94,6 +141,7 @@ export default function SignInRoute() {
<View style={[s.field, usernameFocused && s.fieldFocused]}>
<Text style={s.fieldIcon}>👤</Text>
<TextInput
testID="username-input"
autoCapitalize="none"
autoCorrect={false}
onBlur={() => setUsernameFocused(false)}
Expand All @@ -110,6 +158,7 @@ export default function SignInRoute() {
<View style={[s.field, passwordFocused && s.fieldFocused]}>
<Text style={s.fieldIcon}>🔒</Text>
<TextInput
testID="password-input"
autoCapitalize="none"
autoCorrect={false}
onBlur={() => setPasswordFocused(false)}
Expand Down Expand Up @@ -142,12 +191,23 @@ export default function SignInRoute() {
accessibilityRole="button"
onPressIn={pressIn}
onPressOut={pressOut}
onPress={signInPressed}
disabled={signingIn}
style={s.enterBtn}
>
<Text style={s.enterBtnText}>Sign In →</Text>
{signingIn ? (
<Spinner label="Signing In" />
) : (
<Text style={s.enterBtnText}>Sign In →</Text>
)}
</Pressable>
</Animated.View>

{error && <InlineError message={error} />}

{connectionError && (
<ConnectionBanner message={connectionError} status="offline" />
)}
<View style={s.signupRow}>
<Text style={s.signupHint}>New here? </Text>
<Pressable hitSlop={8}>
Expand All @@ -157,34 +217,42 @@ export default function SignInRoute() {
</View>

{/* ── Dev Quick-Login ───────────────────── */}
<View style={s.dev}>
<View style={s.devDivider}>
<View style={s.devLine} />
<Text style={s.devLabel}>DEV ONLY</Text>
<View style={s.devLine} />
</View>
<View style={s.devRow}>
{(['student', 'mentor'] as const).map((role) => (
<Pressable
key={role}
accessibilityLabel={`Continue as ${role}`}
accessibilityRole="button"
onPress={() => mockSignInAs(role)}
style={({ pressed }) => [
s.devBtn,
pressed && s.devBtnPressed,
]}
>
<Text style={s.devBtnIcon}>
{role === 'student' ? '🎒' : '🎓'}
</Text>
<Text style={s.devBtnText}>
{role === 'student' ? 'Student' : 'Mentor'}
</Text>
</Pressable>
))}
{__DEV__ && (
<View style={s.dev}>
<View style={s.devDivider}>
<View style={s.devLine} />
<Text style={s.devLabel}>DEV ONLY</Text>
<View style={s.devLine} />
</View>
<View style={s.devRow}>
{(['student', 'mentor'] as const).map((role) => (
<Pressable
key={role}
accessibilityLabel={`Continue as ${role}`}
accessibilityRole="button"
onPress={() => {
void mockSignInAs(role).catch(() => {
setError(
'Unable to start a test session. Please try again.',
);
});
}}
style={({ pressed }) => [
s.devBtn,
pressed && s.devBtnPressed,
]}
>
<Text style={s.devBtnIcon}>
{role === 'student' ? '🎒' : '🎓'}
</Text>
<Text style={s.devBtnText}>
{role === 'student' ? 'Student' : 'Mentor'}
</Text>
</Pressable>
))}
</View>
</View>
</View>
)}
</ScrollView>
</KeyboardAvoidingView>
</SafeAreaView>
Expand Down
28 changes: 26 additions & 2 deletions app/_layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import { QueryProvider } from '@/providers/QueryProvider';

import '@/config/env';

import ErrorScreen from '@/features/auth/components/error-screen';

export default function RootLayout() {
return (
<QueryProvider>
Expand All @@ -20,7 +22,7 @@ export default function RootLayout() {
}

function RootNavigator() {
const { session } = useSession();
const { session, restoreSession, signOut } = useSession();

if (session.status === 'bootstrapping') {
return (
Expand All @@ -30,9 +32,31 @@ function RootNavigator() {
);
}

// If an error occured on launch while checking if a user has a session.
if (session.status === 'error') {
// Get error message from session
const message = session.errorMessage;
return (
<Screen
background="patterned"
centered
testID="session-error-screen"
contentStyle={{ gap: 16 }}
>
<ErrorScreen
message={message}
restoreSession={restoreSession}
signOut={signOut}
/>
</Screen>
);
}

return (
<Stack screenOptions={{ headerShown: false }}>
<Stack.Protected guard={session.status === 'anonymous'}>
<Stack.Protected
guard={session.status === 'anonymous' || session.status === 'expired'}
>
<Stack.Screen name="(public)" />
</Stack.Protected>

Expand Down
20 changes: 20 additions & 0 deletions src/features/auth/components/error-screen.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { Button } from '@/components/ui/Button';
import { InlineError } from '@/components/feedback/InlineError';

export default function ErrorScreen({
message,
restoreSession,
signOut,
}: {
message: string;
restoreSession: () => Promise<void>;
signOut: () => Promise<void>;
}) {
return (
<>
<InlineError message={message} />
<Button onPress={restoreSession}>Retry</Button>
<Button onPress={signOut}>Sign Out</Button>
</>
);
}
Loading
Loading