chore: google auth screens

This commit is contained in:
2024-11-01 19:34:54 +01:00
parent 569f81d9b9
commit 530817812f
11 changed files with 101 additions and 490 deletions

View File

@@ -1,28 +0,0 @@
import { View } from 'react-native'
import { Button, Text } from 'react-native-paper'
import { useAuth } from '@/lib/providers/auth'
function Layout() {
const { signIn, user } = useAuth()
return (
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
<Button onPress={async () => await signIn()}>log in!</Button>
<Text>this is {JSON.stringify(user)}.</Text>
</View>
/* <Stack
screenOptions={{
animation: 'slide_from_bottom',
header: (props) => (
<StackHeader navProps={props} children={undefined} />
),
}}
>
<Stack.Screen name="login" options={{ title: 'Log in' }} />
<Stack.Screen name="signup" options={{ title: 'Sign up' }} />
</Stack> */
)
}
export default Layout

View File

@@ -1,107 +1,7 @@
import { Image } from 'expo-image'
import { router } from 'expo-router'
import { Formik } from 'formik'
import React from 'react'
import {
Button,
Surface,
TextInput,
HelperText,
Text,
} from 'react-native-paper'
import * as Yup from 'yup'
import { Text } from "react-native-paper";
import { styles } from '@/lib/ui'
const Login = () => (
<Surface style={{ ...styles.screen, alignItems: undefined }}>
<Image
alt="Logo"
source={require('@/assets/images/icon.png')}
style={{
height: 150,
width: 150,
borderRadius: 16,
marginBottom: 32,
marginHorizontal: 'auto',
}}
/>
<Text variant="headlineLarge" style={{ textAlign: 'center' }}>
Welcome to ERNP
</Text>
<Text variant="bodyLarge" style={{ textAlign: 'center' }}>
We're excited to have you back. Please log in to continue.
</Text>
<Formik
initialValues={{ username: '', password: '' }}
onSubmit={(values) => console.log(values)}
validationSchema={Yup.object().shape({
username: Yup.string()
.min(3, 'Too Short!')
.max(64, 'Too Long!')
.required('Please enter a username.'),
password: Yup.string()
.min(8, 'Too Short! must be at least 8 characters.')
.max(64, 'Too Long!')
.matches(
/^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.*[!@#$%^&*])/,
'Must 1 uppercase, 1 lowercase, 1 number and 1 special case character',
)
.required('Please enter a password'),
})}
>
{({ handleChange, handleBlur, handleSubmit, values, errors }) => (
<>
<Surface elevation={0}>
<TextInput
maxLength={64}
mode="outlined"
label="Username"
value={values.username}
error={!!errors.username}
onBlur={handleBlur('username')}
right={64 - values.username.length}
placeholder="Enter your username..."
onChangeText={handleChange('username')}
/>
<HelperText type="error" visible={!!errors.username}>
{errors.username}
</HelperText>
</Surface>
<Surface elevation={0}>
<TextInput
maxLength={64}
mode="outlined"
label="Password"
value={values.password}
error={!!errors.password}
onBlur={handleBlur('password')}
right={64 - values.password.length}
placeholder="Enter your password..."
onChangeText={handleChange('password')}
/>
<HelperText type="error" visible={!!errors.password}>
{errors.password}
</HelperText>
</Surface>
<Button mode="contained" onPress={() => handleSubmit()}>
Login
</Button>
</>
)}
</Formik>
<Button
mode="contained-tonal"
onPress={() => router.push('/(auth)/signup')}
>
New here?
</Button>
</Surface>
)
export default Login
export default function Login() {
return (
<Text>Login please</Text>
)
}

View File

@@ -1,176 +0,0 @@
import { Image } from 'expo-image'
import { router } from 'expo-router'
import { Formik } from 'formik'
import React from 'react'
import { ScrollView } from 'react-native'
import {
Button,
Surface,
TextInput,
HelperText,
Text,
} from 'react-native-paper'
import * as Yup from 'yup'
import { styles } from '@/lib/ui'
const SignUp = () => (
<ScrollView style={{ flex: 1 }}>
<Surface style={{ ...styles.screen, alignItems: undefined }}>
<Image
alt="Logo"
source={require('@/assets/images/icon.png')}
style={{
height: 150,
width: 150,
borderRadius: 16,
marginBottom: 32,
marginHorizontal: 'auto',
}}
/>
<Text variant="headlineLarge" style={{ textAlign: 'center' }}>
Join ERNP Today!
</Text>
<Text variant="bodyLarge" style={{ textAlign: 'center' }}>
We're thrilled to have you on board. Let's get you set up.
</Text>
<Formik
initialValues={{
username: '',
password: '',
email: '',
firstName: '',
lastName: '',
}}
onSubmit={(values) => console.log(values)}
validationSchema={Yup.object().shape({
username: Yup.string()
.min(3, 'Too Short!')
.max(64, 'Too Long!')
.required('Please enter a username.'),
password: Yup.string()
.min(8, 'Too Short! must be at least 8 characters.')
.max(64, 'Too Long!')
.matches(
/^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.*[!@#$%^&*])/,
'Must 1 uppercase, 1 lowercase, 1 number and 1 special case character',
)
.required('Please enter a password'),
email: Yup.string().email().required('Please enter an email.'),
firstName: Yup.string()
.min(3, 'Too Short!')
.max(64, 'Too Long!')
.required('Please enter a first name.'),
lastName: Yup.string()
.min(3, 'Too Short!')
.max(64, 'Too Long!')
.required('Please enter a last name.'),
})}
>
{({ handleChange, handleBlur, handleSubmit, values, errors }) => (
<>
<Surface elevation={0}>
<TextInput
maxLength={64}
mode="outlined"
label="Username"
value={values.username}
error={!!errors.username}
onBlur={handleBlur('username')}
right={64 - values.username.length}
placeholder="Enter your username..."
onChangeText={handleChange('username')}
/>
<HelperText type="error" visible={!!errors.username}>
{errors.username}
</HelperText>
</Surface>
<Surface elevation={0}>
<TextInput
maxLength={64}
mode="outlined"
label="Password"
value={values.password}
error={!!errors.password}
onBlur={handleBlur('password')}
right={64 - values.password.length}
placeholder="Enter your password..."
onChangeText={handleChange('password')}
/>
<HelperText type="error" visible={!!errors.password}>
{errors.password}
</HelperText>
</Surface>
<Surface elevation={0}>
<TextInput
maxLength={64}
mode="outlined"
label="Email"
value={values.email}
error={!!errors.email}
onBlur={handleBlur('email')}
right={64 - values.email.length}
placeholder="Enter your email..."
onChangeText={handleChange('email')}
/>
<HelperText type="error" visible={!!errors.email}>
{errors.email}
</HelperText>
</Surface>
<Surface elevation={0}>
<TextInput
maxLength={64}
mode="outlined"
label="First name"
value={values.firstName}
error={!!errors.firstName}
onBlur={handleBlur('firstName')}
right={64 - values.firstName.length}
placeholder="Enter your first name..."
onChangeText={handleChange('firstName')}
/>
<HelperText type="error" visible={!!errors.firstName}>
{errors.firstName}
</HelperText>
</Surface>
<Surface elevation={0}>
<TextInput
maxLength={64}
mode="outlined"
label="Last name"
value={values.lastName}
error={!!errors.lastName}
onBlur={handleBlur('lastName')}
right={64 - values.lastName.length}
placeholder="Enter your first name..."
onChangeText={handleChange('lastName')}
/>
<HelperText type="error" visible={!!errors.lastName}>
{errors.lastName}
</HelperText>
</Surface>
<Button mode="contained" onPress={() => handleSubmit()}>
Sign up
</Button>
</>
)}
</Formik>
<Button
mode="contained-tonal"
onPress={() => router.push('/(auth)/login')}
>
Already have an account?
</Button>
</Surface>
</ScrollView>
)
export default SignUp

View File

@@ -68,35 +68,6 @@ const TabLayout = () => {
),
}}
/>
<Tabs.Screen
name="profile"
options={{
title: 'Profile',
headerRight: () => (
<>
<Tooltip title="Search">
<Appbar.Action
icon="magnify"
onPress={() => router.push('/search')}
/>
</Tooltip>
<Tooltip title="Settings">
<Appbar.Action
icon="cog"
onPress={() => router.push('/(tabs)/settings')}
/>
</Tooltip>
</>
),
tabBarIcon: (props) => (
<MaterialCommunityIcons
{...props}
size={24}
name={props.focused ? 'account' : 'account-outline'}
/>
),
}}
/>
<Tabs.Screen
name="settings"
options={{

View File

@@ -1,33 +0,0 @@
import { router } from 'expo-router'
import React from 'react'
import { Button, Surface } from 'react-native-paper'
import { ScreenInfo, styles } from '@/lib/ui'
const Profile = () => (
<Surface style={styles.screen}>
<ScreenInfo title="Profile" path="app/(tabs)/profile.tsx" />
<Surface
elevation={0}
style={{
padding: 16,
gap: 16,
position: 'absolute',
left: 0,
right: 0,
bottom: 0,
}}
>
<Button mode="contained" onPress={() => router.push('/(auth)/login')}>
Login
</Button>
<Button mode="contained" onPress={() => router.push('/(auth)/signup')}>
Sign Up
</Button>
</Surface>
</Surface>
)
export default Profile

View File

@@ -13,11 +13,15 @@ import {
import { Color, Setting } from '@/lib/types'
import { Colors, LoadingIndicator, ScreenInfo, styles } from '@/lib/ui'
import { reloadAppAsync } from 'expo'
import { useAuth } from '@/lib/providers/auth'
import { Image } from 'expo-image'
const Settings = () => {
const colorScheme = useColorScheme()
const [loading, setLoading] = React.useState<boolean>(false)
const [message, setMessage] = React.useState({ visible: false, content: '' })
const { user, signIn, signOut } = useAuth()
const [settings, setSettings] = React.useState<Setting>({
color: 'default',
theme: 'auto',
@@ -204,42 +208,72 @@ const Settings = () => {
</Menu>
)}
/>
<Button
mode="contained"
style={{ margin: 16 }}
icon="content-save"
onPress={() =>
SecureStore.setItemAsync('settings', JSON.stringify(settings))
.then(async () => {
setMessage({
visible: true,
content: 'Restarting app...',
})
await reloadAppAsync()
})
.catch((res) =>
setMessage({
visible: true,
content: res.message,
}),
)
}
>
Save
</Button>
</List.Accordion>
</List.AccordionGroup>
</Surface>
)}
<Surface elevation={0} style={styles.screen}>
<ScreenInfo title="Settings" path="app/(tabs)/settings.tsx" />
{/* make a surface for the authentication */}
<Surface elevation={0}>
<List.Accordion
id="2"
title="Authentication"
left={(props) => <List.Icon {...props} icon={'account'} />}
>
{user ? (
<>
<List.Item
title={`Hey ${user.user.givenName}!`}
description="Click here to check more info"
left={(props) => <List.Icon {...props} icon="fingerprint" />}
/>
<List.Item
title="Sign Out"
description="Sign out of your account"
left={(props) => <List.Icon {...props} icon="logout" />}
onPress={async () => {
await signOut()
}}
/>
</>
) : (
<>
<List.Item
title="Login"
description="Log in to your account"
left={(props) => <List.Icon {...props} icon="login" />}
onPress={async () => {
await signIn()
}}
/>
</>
)}
</List.Accordion>
</Surface>
<Button
mode="contained"
style={{ margin: 16 }}
onPress={() =>
Platform.OS !== 'web'
? SecureStore.setItemAsync('settings', JSON.stringify(settings))
.then(() =>
setMessage({
visible: true,
content: 'Please restart the app to apply changes.',
}),
)
.catch((res) =>
setMessage({
visible: true,
content: res.message,
}),
)
: setMessage({
visible: true,
content: 'This feature is not available on the web.',
})
}
>
Save
</Button>
<Snackbar
visible={message.visible}
onDismiss={() => setMessage({ ...message, visible: false })}

View File

@@ -4,15 +4,15 @@ import {
JetBrainsMono_400Regular,
} from '@expo-google-fonts/jetbrains-mono'
import { NotoSans_400Regular } from '@expo-google-fonts/noto-sans'
import { SplashScreen, Stack } from 'expo-router'
import { Redirect, SplashScreen, Stack, useRootNavigationState, useRouter } from 'expo-router'
import * as SecureStore from 'expo-secure-store'
import React from 'react'
import { Platform, useColorScheme } from 'react-native'
import { PaperProvider } from 'react-native-paper'
import { AuthProvider, useAuth } from '@/lib/providers/auth'
import { Setting } from '@/lib/types'
import { StackHeader, Themes } from '@/lib/ui'
import { AuthProvider } from '@/lib/providers/auth'
export {
// Catch any errors thrown by the Layout component.
@@ -34,6 +34,9 @@ const RootLayout = () => {
...MaterialCommunityIcons.font,
})
// Get authentication state
const rootNavigationState = useRootNavigationState()
// Expo Router uses Error Boundaries to catch errors in the navigation tree.
React.useEffect(() => {
if (error) throw error
@@ -45,7 +48,8 @@ const RootLayout = () => {
}
}, [loaded])
if (!loaded) {
// Make sure we have the navigation state before showing content
if (!loaded || !rootNavigationState) {
return null
}
@@ -61,32 +65,28 @@ const RootLayoutNav = () => {
// Load settings from the device
React.useEffect(() => {
if (Platform.OS !== 'web') {
SecureStore.getItemAsync('settings').then((result) => {
if (result === null) {
SecureStore.setItemAsync('settings', JSON.stringify(settings)).then(
(res) => console.log(res),
)
}
SecureStore.getItemAsync('settings').then((result) => {
if (result === null) {
SecureStore.setItemAsync('settings', JSON.stringify(settings)).then(
(res) => console.log(res),
)
}
setSettings(JSON.parse(result ?? JSON.stringify(settings)))
})
} else {
setSettings({ ...settings, theme: colorScheme ?? 'light' })
}
setSettings(JSON.parse(result ?? JSON.stringify(settings)))
})
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
return (
<PaperProvider
theme={
Themes[
settings.theme === 'auto' ? (colorScheme ?? 'dark') : settings.theme
][settings.color]
}
>
<AuthProvider>
<AuthProvider>
<PaperProvider
theme={
Themes[
settings.theme === 'auto' ? (colorScheme ?? 'dark') : settings.theme
][settings.color]
}
>
<Stack
screenOptions={{
animation: 'slide_from_bottom',
@@ -95,7 +95,6 @@ const RootLayoutNav = () => {
),
}}
>
<Stack.Screen name="(auth)" options={{ headerShown: false }} />
<Stack.Screen name="(tabs)" options={{ headerShown: false }} />
<Stack.Screen name="drawer" options={{ headerShown: false }} />
<Stack.Screen name="search" options={{ title: 'Search' }} />
@@ -104,8 +103,8 @@ const RootLayoutNav = () => {
options={{ title: 'Modal', presentation: 'modal' }}
/>
</Stack>
</AuthProvider>
</PaperProvider>
</PaperProvider>
</AuthProvider>
)
}

View File

@@ -77,29 +77,6 @@ const DrawerLayout = () => {
),
}}
/>
<Drawer.Screen
name="profile"
options={{
drawerLabel: 'Profile',
title: 'Profile',
headerRight: () => (
<>
<Tooltip title="Search">
<Appbar.Action
icon="magnify"
onPress={() => router.push('/search')}
/>
</Tooltip>
<Tooltip title="Settings">
<Appbar.Action
icon="cog"
onPress={() => router.push('/(tabs)/settings')}
/>
</Tooltip>
</>
),
}}
/>
<Drawer.Screen
name="settings"
options={{

View File

@@ -1,33 +0,0 @@
import { router } from 'expo-router'
import React from 'react'
import { Button, Surface } from 'react-native-paper'
import { ScreenInfo, styles } from '@/lib/ui'
const Profile = () => (
<Surface style={styles.screen}>
<ScreenInfo title="Profile" path="app/(tabs)/profile.tsx" />
<Surface
elevation={0}
style={{
padding: 16,
gap: 16,
position: 'absolute',
left: 0,
right: 0,
bottom: 0,
}}
>
<Button mode="contained" onPress={() => router.push('/(auth)/login')}>
Login
</Button>
<Button mode="contained" onPress={() => router.push('/(auth)/signup')}>
Sign Up
</Button>
</Surface>
</Surface>
)
export default Profile

View File

@@ -2,7 +2,13 @@ import {
GoogleSignin,
type User,
} from '@react-native-google-signin/google-signin'
import { createContext, useContext, useEffect, useState, type ReactNode } from 'react'
import {
createContext,
useContext,
useEffect,
useState,
type ReactNode,
} from 'react'
interface AuthProviderProps {
children: ReactNode

View File

@@ -20,12 +20,6 @@ const DrawerContent = (props: DrawerContentProps) => (
active={props.navProps.state.index === 0}
onPress={() => router.push('/drawer')}
/>
<Drawer.Item
label="Profile"
icon="account"
active={props.navProps.state.index === 1}
onPress={() => router.push('/drawer/profile')}
/>
<Drawer.Item
label="Settings"
icon="cog"