very basics of auth state - not actually authenticating anything right now....
This commit is contained in:
@@ -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
21
app/(tabs)/profile.tsx
Normal 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
|
||||||
|
}
|
||||||
|
})
|
||||||
@@ -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
46
app/ctx.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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
15
app/signup.tsx
Normal 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
12
app/splash.tsx
Normal 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
65
app/useStorageState.tsx
Normal 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]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user