very basics of auth state - not actually authenticating anything right now....

This commit is contained in:
Will Baumbach
2025-08-05 12:04:02 -05:00
parent 813cb44637
commit 409f15f233
8 changed files with 224 additions and 23 deletions

View File

@@ -3,6 +3,9 @@ import { Tabs } from 'expo-router'
import React from 'react' import React from 'react'
export default function TabLayout() { export default function TabLayout() {
const isAdmin = localStorage.getItem('session') === 'admin'
console.log(isAdmin)
return ( return (
<Tabs <Tabs
screenOptions={{ screenOptions={{
@@ -32,15 +35,16 @@ export default function TabLayout() {
options={{ options={{
title: 'Posts', title: 'Posts',
headerShown: false, headerShown: false,
tabBarItemStyle: { display: isAdmin ? 'flex' : 'none' },
tabBarIcon: ({ color, focused }) => ( tabBarIcon: ({ color, focused }) => (
<Ionicons name={focused ? 'rocket-sharp' : 'rocket-outline'} size={24} color={color} /> <Ionicons name={focused ? 'rocket-sharp' : 'rocket-outline'} size={24} color={color} />
) )
}} }}
/> />
<Tabs.Screen <Tabs.Screen
name='login' name='profile'
options={{ options={{
title: 'Login', title: 'Profile',
headerShown: false, headerShown: false,
tabBarIcon: ({ color, focused }) => ( tabBarIcon: ({ color, focused }) => (
<Ionicons <Ionicons

21
app/(tabs)/profile.tsx Normal file
View File

@@ -0,0 +1,21 @@
import React from 'react'
import { StyleSheet, TouchableOpacity, View } from 'react-native'
import { useSession } from '../ctx'
export default function PostsScreen() {
const { logout } = useSession()
return (
<View style={styles.wrapper}>
<TouchableOpacity onPress={() => logout()}>Logout</TouchableOpacity>
</View>
)
}
const styles = StyleSheet.create({
wrapper: {
flex: 1,
backgroundColor: '#25292e',
padding: 5
}
})

View File

@@ -1,9 +1,29 @@
import { Stack } from 'expo-router' import { Stack } from 'expo-router'
import { SessionProvider, useSession } from './ctx'
import { SplashScreenController } from './splash'
export default function RootLayout() { export default function Root() {
// Set up the auth context and render our layout inside of it.
return (
<SessionProvider>
<SplashScreenController />
<RootNavigator />
</SessionProvider>
)
}
// Separate this into a new component so it can access the SessionProvider context later
function RootNavigator() {
const { session } = useSession()
return ( return (
<Stack> <Stack>
<Stack.Screen name='(tabs)' options={{ headerShown: false }}></Stack.Screen> <Stack.Protected guard={!!session}>
<Stack.Screen options={{ headerShown: false }} name='(tabs)' />
</Stack.Protected>
<Stack.Protected guard={!session}>
<Stack.Screen options={{ headerShown: false }} name='login' />
</Stack.Protected>
</Stack> </Stack>
) )
} }

46
app/ctx.tsx Normal file
View File

@@ -0,0 +1,46 @@
import { createContext, use, type PropsWithChildren } from 'react'
import { useStorageState } from './useStorageState'
const AuthContext = createContext<{
login: () => void
logout: () => void
session?: string | null
isLoading: boolean
}>({
login: () => null,
logout: () => null,
session: null,
isLoading: false
})
// This hook can be used to access the user info.
export function useSession() {
const value = use(AuthContext)
if (!value) {
throw new Error('useSession must be wrapped in a <SessionProvider />')
}
return value
}
export function SessionProvider({ children }: PropsWithChildren) {
const [[isLoading, session], setSession] = useStorageState('session')
return (
<AuthContext
value={{
login: () => {
// Perform sign-in logic here
setSession('admin')
},
logout: () => {
setSession(null)
},
session,
isLoading
}}
>
{children}
</AuthContext>
)
}

View File

@@ -1,44 +1,54 @@
import React from 'react'; import { router } from 'expo-router'
import { StyleSheet, Text, TextInput, TouchableOpacity, View } from 'react-native'; import React from 'react'
import { StyleSheet, Text, TextInput, TouchableOpacity, View } from 'react-native'
import { useSession } from './ctx'
export default function LoginScreen() { export default function LoginScreen() {
const [username, setUsername] = React.useState('')
const [username, setUsername] = React.useState(''); const [password, setPassword] = React.useState('')
const [password, setPassword] = React.useState(''); const { login } = useSession()
function login(username:string, password:string) {
console.log(username, password);
}
return ( return (
<View style={styles.container}> <View style={styles.container}>
<Text style={styles.text}>Login</Text> <Text style={styles.text}>Login</Text>
<View style={{ width: '80%', marginTop: 20 }}> <View style={{ width: '80%', marginTop: 20 }}>
<Text style={styles.text}>Username</Text> <Text style={styles.textlabel}>Username</Text>
<TextInput <TextInput
style={styles.input} style={styles.input}
value={username} value={username}
placeholder="Enter username" placeholder='Enter username'
onChangeText={setUsername} onChangeText={setUsername}
/> />
<Text style={styles.text}>Password</Text> <Text style={styles.textlabel}>Password</Text>
<TextInput <TextInput
style={styles.input} style={styles.input}
value={password} value={password}
placeholder="Enter password" placeholder='Enter password'
secureTextEntry secureTextEntry
onChangeText={setPassword} onChangeText={setPassword}
/> />
<TouchableOpacity <TouchableOpacity
style={styles.button} style={styles.button}
onPress={() => login(username, password)} onPress={() => {
login()
if (localStorage.getItem('session') === 'success') {
router.replace('/')
}
}}
> >
<Text style={{ color: '#fff', fontWeight: 'bold' }}>Login</Text> <Text style={{ color: '#fff', fontWeight: 'bold' }}>Login</Text>
</TouchableOpacity> </TouchableOpacity>
<TouchableOpacity
style={styles.signup}
onPress={() => {
router.navigate('/signup')
}}
>
<Text style={{ color: '#fff' }}>Sign Up</Text>
</TouchableOpacity>
</View> </View>
</View> </View>
); )
} }
const styles = StyleSheet.create({ const styles = StyleSheet.create({
@@ -46,21 +56,29 @@ const styles = StyleSheet.create({
flex: 1, flex: 1,
backgroundColor: '#25292e', backgroundColor: '#25292e',
justifyContent: 'center', justifyContent: 'center',
alignItems: 'center', alignItems: 'center'
}, },
text: { text: {
color: '#fff'
},
textlabel: {
color: '#fff', color: '#fff',
marginBottom: 3
}, },
input: { input: {
backgroundColor: '#fff', backgroundColor: '#fff',
borderRadius: 5, borderRadius: 5,
padding: 10, padding: 10,
marginBottom: 15, marginBottom: 15
}, },
button: { button: {
backgroundColor: '#1e90ff', backgroundColor: '#1e90ff',
padding: 12, padding: 12,
borderRadius: 5, borderRadius: 5,
alignItems: 'center', alignItems: 'center',
marginBottom: 12
},
signup: {
alignItems: 'center'
} }
}); })

15
app/signup.tsx Normal file
View File

@@ -0,0 +1,15 @@
import React from 'react'
import { StyleSheet, View } from 'react-native'
export default function SignupScreen() {
return <View style={styles.container}></View>
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#25292e',
justifyContent: 'center',
alignItems: 'center'
}
})

12
app/splash.tsx Normal file
View File

@@ -0,0 +1,12 @@
import { SplashScreen } from 'expo-router'
import { useSession } from './ctx'
export function SplashScreenController() {
const { isLoading } = useSession()
if (!isLoading) {
SplashScreen.hideAsync()
}
return null
}

65
app/useStorageState.tsx Normal file
View File

@@ -0,0 +1,65 @@
import * as SecureStore from 'expo-secure-store'
import { useCallback, useEffect, useReducer } from 'react'
import { Platform } from 'react-native'
type UseStateHook<T> = [[boolean, T | null], (value: T | null) => void]
function useAsyncState<T>(initialValue: [boolean, T | null] = [true, null]): UseStateHook<T> {
return useReducer(
(state: [boolean, T | null], action: T | null = null): [boolean, T | null] => [false, action],
initialValue
) as UseStateHook<T>
}
export async function setStorageItemAsync(key: string, value: string | null) {
if (Platform.OS === 'web') {
try {
if (value === null) {
localStorage.removeItem(key)
} else {
localStorage.setItem(key, value)
}
} catch (e) {
console.error('Local storage is unavailable:', e)
}
} else {
if (value == null) {
await SecureStore.deleteItemAsync(key)
} else {
await SecureStore.setItemAsync(key, value)
}
}
}
export function useStorageState(key: string): UseStateHook<string> {
// Public
const [state, setState] = useAsyncState<string>()
// Get
useEffect(() => {
if (Platform.OS === 'web') {
try {
if (typeof localStorage !== 'undefined') {
setState(localStorage.getItem(key))
}
} catch (e) {
console.error('Local storage is unavailable:', e)
}
} else {
SecureStore.getItemAsync(key).then((value) => {
setState(value)
})
}
}, [key, setState])
// Set
const setValue = useCallback(
(value: string | null) => {
setState(value)
setStorageItemAsync(key, value)
},
[key, setState]
)
return [state, setValue]
}