-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
6 changed files
with
526 additions
and
101 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -10,6 +10,9 @@ import Error from '@/components/error'; | |
import Input from '@/components/input'; | ||
import { useAuthStore } from '@/hooks/use-auth'; | ||
import { loginByEmail } from '@/services/auth'; | ||
Check failure on line 12 in frontend/sac-mobile/app/(auth)/_components/login-form.tsx GitHub Actions / Lint
|
||
import { useSignIn } from '@clerk/clerk-expo'; | ||
import { useState } from 'react'; | ||
import Spinner from 'react-native-loading-spinner-overlay'; | ||
|
||
type LoginFormData = { | ||
email: string; | ||
|
@@ -29,28 +32,40 @@ const LoginForm = () => { | |
handleSubmit, | ||
formState: { errors } | ||
} = useForm<LoginFormData>(); | ||
const { login } = useAuthStore(); | ||
|
||
const onSubmit = async (data: LoginFormData) => { | ||
const { signIn, setActive, isLoaded } = useSignIn(); | ||
const [loading, setLoading] = useState(false); | ||
|
||
const onSignInPress = async (loginData: LoginFormData) => { | ||
if (!isLoaded) { | ||
return; | ||
} | ||
|
||
setLoading(true); | ||
|
||
try { | ||
loginSchema.parse(data); | ||
const { user, tokens } = await loginByEmail( | ||
data.email.toLowerCase(), | ||
data.password | ||
); | ||
login(tokens, user); | ||
router.push('/(app)/'); | ||
} catch (e: unknown) { | ||
if (e instanceof ZodError) { | ||
Alert.alert('Validation Error', e.errors[0].message); // use a better way to display errors | ||
const validData = loginSchema.parse(loginData); | ||
|
||
const completeSignIn = await signIn.create({ | ||
identifier: validData.email, | ||
password: validData.password | ||
}); | ||
|
||
await setActive({ session: completeSignIn.createdSessionId }); | ||
} catch (err: any) { | ||
if (err instanceof ZodError) { | ||
Alert.alert(err.errors[0].message); | ||
} else { | ||
console.error('An unexpected error occurred:', e); | ||
Alert.alert('An error occurred', err.message); | ||
} | ||
} finally { | ||
setLoading(false); | ||
} | ||
}; | ||
|
||
return ( | ||
<> | ||
{loading && <Spinner visible={loading} />} | ||
<View> | ||
<Controller | ||
control={control} | ||
|
@@ -61,7 +76,7 @@ const LoginForm = () => { | |
placeholder="[email protected]" | ||
onChangeText={onChange} | ||
value={value} | ||
onSubmitEditing={handleSubmit(onSubmit)} | ||
onSubmitEditing={handleSubmit(onSignInPress)} | ||
error={!!errors.email} | ||
/> | ||
)} | ||
|
@@ -81,7 +96,7 @@ const LoginForm = () => { | |
onChangeText={onChange} | ||
value={value} | ||
secureTextEntry={true} | ||
onSubmitEditing={handleSubmit(onSubmit)} | ||
onSubmitEditing={handleSubmit(onSignInPress)} | ||
error={!!errors.password} | ||
/> | ||
)} | ||
|
@@ -106,7 +121,7 @@ const LoginForm = () => { | |
<Button | ||
size="lg" | ||
variant="default" | ||
onPress={handleSubmit(onSubmit)} | ||
onPress={handleSubmit(onSignInPress)} | ||
> | ||
Log in | ||
</Button> | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,100 +1,81 @@ | ||
import { useEffect } from 'react'; | ||
import { Text, View } from 'react-native'; | ||
|
||
import { useFonts } from 'expo-font'; | ||
import { Stack, router } from 'expo-router'; | ||
import { getItemAsync } from 'expo-secure-store'; | ||
import { Slot, Stack, useRouter, useSegments } from 'expo-router'; | ||
import * as SplashScreen from 'expo-splash-screen'; | ||
|
||
import FontAwesome from '@expo/vector-icons/FontAwesome'; | ||
|
||
import { useAuthStore } from '@/hooks/use-auth'; | ||
import { User } from '@/types/user'; | ||
import { ClerkProvider, useAuth } from '@clerk/clerk-expo'; | ||
import * as SecureStore from 'expo-secure-store'; | ||
|
||
export { | ||
// Catch any errors thrown by the Layout component. | ||
ErrorBoundary | ||
} from 'expo-router'; | ||
|
||
export const unstable_settings = { | ||
// Ensure that reloading on `/modal` keeps a back button present. | ||
initialRouteName: '' | ||
}; | ||
const CLERK_PUBLISHABLE_KEY = process.env.EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY | ||
|
||
// Prevent the splash screen from auto-hiding before asset loading is complete. | ||
SplashScreen.preventAutoHideAsync(); | ||
|
||
export default function RootLayout() { | ||
const [loaded, error] = useFonts({ | ||
SpaceMono: require('../assets/fonts/SpaceMono-Regular.ttf'), | ||
...FontAwesome.font | ||
}); | ||
const tokenCache = { | ||
async getToken(key: string) { | ||
try { | ||
return SecureStore.getItemAsync(key); | ||
} catch (error) { | ||
console.error('[RootLayoutNav] Error retrieving token:', error); | ||
return null; | ||
} | ||
}, | ||
async saveToken(key: string, value: string) { | ||
try { | ||
return SecureStore.setItemAsync(key, value); | ||
} catch (error) { | ||
console.error('[RootLayoutNav] Error setting token:', error); | ||
} | ||
} | ||
}; | ||
|
||
// Expo Router uses Error Boundaries to catch errors in the navigation tree. | ||
useEffect(() => { | ||
if (error) throw error; | ||
}, [error]); | ||
const InitalLayout = () => { | ||
const { isLoaded, isSignedIn } = useAuth(); | ||
const router = useRouter(); | ||
const segments = useSegments(); | ||
|
||
useEffect(() => { | ||
if (loaded) { | ||
SplashScreen.hideAsync(); | ||
if (!isLoaded) return; | ||
|
||
const inApp = segments[0] === "(app)"; | ||
|
||
if (isSignedIn && !inApp) { | ||
router.push("/(app)/"); | ||
} else if (!isSignedIn) { | ||
router.push("/(auth)/login"); | ||
} | ||
}, [loaded]); | ||
|
||
if (!loaded) { | ||
return ( | ||
<View> | ||
<Text>Loading...</Text> | ||
</View> | ||
); | ||
} | ||
|
||
return <RootLayoutNav />; | ||
} | ||
console.log({ isSignedIn, inApp }); | ||
}, [isSignedIn]); | ||
|
||
function RootLayoutNav() { | ||
const { isLoggedIn, login } = useAuthStore(); | ||
|
||
useEffect(() => { | ||
const checkLoginStatus = async () => { | ||
try { | ||
const accessToken = await getItemAsync('accessToken'); | ||
const refreshToken = await getItemAsync('refreshToken'); | ||
const savedUser = await getItemAsync('user'); | ||
|
||
console.log('[root] accessToken:', accessToken); | ||
console.log('[root] refreshToken:', refreshToken); | ||
|
||
const user: User = savedUser ? JSON.parse(savedUser) : null; | ||
|
||
if (accessToken && refreshToken) { | ||
// Consider adding token validation (e.g., expiration check) | ||
login({ accessToken, refreshToken }, user); | ||
} | ||
} catch (error) { | ||
console.error( | ||
'[RootLayoutNav] Error retrieving tokens:', | ||
error | ||
); | ||
} | ||
}; | ||
|
||
checkLoginStatus(); | ||
}, [login]); | ||
return <Slot />; | ||
} | ||
|
||
useEffect(() => { | ||
if (isLoggedIn === null) { | ||
router.push('/(auth)/welcome'); | ||
return; | ||
} | ||
const RootLayout = () => { | ||
const [loaded, error] = useFonts({ | ||
SpaceMono: require('../assets/fonts/SpaceMono-Regular.ttf'), | ||
...FontAwesome.font | ||
}); | ||
|
||
useEffect(() => { if (error) throw error }, [error]); | ||
useEffect(() => { if (loaded) SplashScreen.hideAsync() }, [loaded]); | ||
|
||
router.push(isLoggedIn ? '/(app)/' : '/(auth)/welcome'); | ||
}, [isLoggedIn]); | ||
if (!loaded) return null; | ||
|
||
return ( | ||
<Stack> | ||
<Stack.Screen name="(app)" options={{ headerShown: false }} /> | ||
<Stack.Screen name="(auth)" options={{ headerShown: false }} /> | ||
</Stack> | ||
<ClerkProvider publishableKey={CLERK_PUBLISHABLE_KEY!} tokenCache={tokenCache}> | ||
<InitalLayout /> | ||
</ClerkProvider> | ||
); | ||
} | ||
|
||
export default RootLayout; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
import React from 'react'; | ||
import { View, ActivityIndicator, Text } from 'react-native'; | ||
|
||
import { VariantProps, cva } from 'class-variance-authority'; | ||
import { cn } from '@/lib/utils'; | ||
|
||
const spinnerVariants = { | ||
size: { | ||
default: 'w-6 h-6', | ||
large: 'w-8 h-8', | ||
small: 'w-4 h-4', | ||
}, | ||
color: { | ||
default: 'text-gray-500', | ||
primary: 'text-blue-500', | ||
secondary: 'text-red-500', | ||
}, | ||
}; | ||
|
||
const spinnerStyles = cva(['items-center', 'justify-center'], { | ||
variants: spinnerVariants, | ||
defaultVariants: { | ||
size: 'default', | ||
color: 'default', | ||
}, | ||
}); | ||
|
||
export interface SpinnerProps | ||
extends VariantProps<typeof spinnerStyles> { | ||
text?: string; | ||
} | ||
|
||
const Spinner = ({ size, color, text }: SpinnerProps) => { | ||
return ( | ||
<View className={cn(spinnerStyles({ size, color }))}> | ||
<ActivityIndicator /> | ||
{text && <Text>{text}</Text>} | ||
</View> | ||
); | ||
}; | ||
|
||
Spinner.displayName = 'Spinner'; | ||
|
||
export { Spinner, spinnerVariants }; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.