fixing sing up flow now using an email code to verify.. on sign up we create user in backend that has reference to clerkID for filtering posts later

This commit is contained in:
Will Baumbach
2025-08-06 23:18:26 -05:00
parent 1ce2d1583f
commit 482bc915a5
7 changed files with 180 additions and 108 deletions

View File

@@ -1,44 +1,23 @@
import mongoose from 'mongoose'
import mongoose from 'mongoose'; import validator from 'validator'
import validator from 'validator';
const userSchema = new mongoose.Schema({ const userSchema = new mongoose.Schema({
name: { name: {
type: String, type: String,
required: [true, 'Please tell us your name!'] required: [true, 'Please tell us your name!']
}, },
email: { email: {
type: String, type: String,
required: [true, 'Please provide your email'], required: [true, 'Please provide your email'],
unique: true, unique: true,
lowercase: true, lowercase: true,
validate: [validator.isEmail, 'Please provide a valid email'] validate: [validator.isEmail, 'Please provide a valid email']
}, },
role: { clerkUserID: {
type: String, type: String
enum: ['user', 'admin'], }
default: 'user' })
}
});
const User = mongoose.model('User', userSchema); const User = mongoose.model('User', userSchema)
export default User; export default User
// password: {
// type: String,
// required: [true, 'Please provide a password'],
// minlength: 8,
// select: false
// },
// passwordConfirm: {
// type: String,
// required: [true, 'Please confirm your password'],
// validate: {
// // This only works on CREATE and SAVE!!!
// validator: function(el) {
// return el === this.password;
// },
// message: 'Passwords are not the same!'
// }
// }

View File

@@ -1,21 +1,26 @@
import { useSignUp } from '@clerk/clerk-expo' import { isClerkAPIResponseError, useSignUp } from '@clerk/clerk-expo'
import { ClerkAPIError } from '@clerk/types'
import { router } from 'expo-router' import { router } from 'expo-router'
import React from 'react' import { useState } from 'react'
import { StyleSheet, Text, TextInput, TouchableOpacity, View } from 'react-native' import { StyleSheet, Text, TextInput, TouchableOpacity, View } from 'react-native'
export default function SignUpScreen() { export default function SignUpScreen() {
const { signUp, setActive, isLoaded } = useSignUp() const { signUp, setActive, isLoaded } = useSignUp()
const [fname, setFname] = React.useState('') const [pendingVerification, setPendingVerification] = useState(false)
const [lname, setLname] = React.useState('') const [code, setCode] = useState('')
const [email, setUsername] = React.useState('') const [errors, setErrors] = useState<ClerkAPIError[]>([])
const [password, setPassword] = React.useState('') const [fname, setFname] = useState('')
const [lname, setLname] = useState('')
const [email, setUsername] = useState('')
const [password, setPassword] = useState('')
async function handleSignUp() { async function onSignUpPress() {
if (!isLoaded) return if (!isLoaded) return
setErrors([])
try { try {
// Start Auth // Start Auth
const signUpAttempt = await signUp.create({ await signUp.create({
firstName: fname, firstName: fname,
lastName: lname, lastName: lname,
emailAddress: email, emailAddress: email,
@@ -23,58 +28,137 @@ export default function SignUpScreen() {
}) })
// Set confirmation // Set confirmation
await signUp.prepareVerification({ await signUp.prepareEmailAddressVerification()
strategy: 'email_link', setPendingVerification(true)
redirectUrl: '/'
})
if (signUpAttempt.status === 'complete') {
setActive({ session: signUpAttempt.createdSessionId })
router.replace('/')
} else {
console.log(signUpAttempt)
}
} catch (e) { } catch (e) {
if (isClerkAPIResponseError(e)) setErrors(e.errors)
console.log(JSON.stringify(e)) console.log(JSON.stringify(e))
} }
} }
return ( async function onVerifyPress() {
<View style={styles.container}> if (!isLoaded) return
<Text style={styles.text}>Create an account</Text> setErrors([])
<View style={{ width: '80%', marginTop: 20 }}>
<Text style={styles.textlabel}>First name</Text> try {
<TextInput style={styles.input} value={fname} placeholder='Enter first name' onChangeText={setFname} /> // Use the code the user provided to attempt verification
<Text style={styles.textlabel}>Last name</Text> const signUpAttempt = await signUp.attemptEmailAddressVerification({
<TextInput style={styles.input} value={lname} placeholder='Enter last name' onChangeText={setLname} /> code
<Text style={styles.textlabel}>Email</Text> })
<TextInput style={styles.input} value={email} placeholder='Enter email' onChangeText={setUsername} />
<Text style={styles.textlabel}>Password</Text> // If verification was completed, set the session to active
// and redirect the user
if (signUpAttempt.status === 'complete') {
await createUser(signUpAttempt.createdUserId)
await setActive({ session: signUpAttempt.createdSessionId })
} else {
// If the status is not complete, check why. User may need to
// complete further steps.
console.error(JSON.stringify(signUpAttempt, null, 2))
}
} catch (e: any) {
// See https://clerk.com/docs/custom-flows/error-handling
// for more info on error handling
console.error(JSON.stringify(e, null, 2))
setErrors(e.errors)
}
}
async function createUser(clerkUserID: string | null) {
fetch('http://localhost:3000/api/v1/users', {
method: 'POST',
headers: {
Accept: 'application/json, text/plain, */*',
'Content-Type': 'application/json'
},
body: JSON.stringify({
name: `${fname} ${lname}`,
email,
clerkUserID
})
})
}
if (pendingVerification) {
return (
<View style={styles.container}>
<Text style={styles.textCenter}>Enter the verification code we sent to {email}</Text>
<TextInput <TextInput
style={styles.input} style={styles.input}
value={password} value={code}
placeholder='Enter password' placeholder='Enter your verification code'
secureTextEntry onChangeText={(code) => setCode(code)}
onChangeText={setPassword}
/> />
<TouchableOpacity <TouchableOpacity onPress={onVerifyPress} style={styles.button}>
style={styles.button} <Text>Verify</Text>
onPress={() => {
handleSignUp()
}}
>
<Text style={{ color: '#fff', fontWeight: 'bold' }}>Sign Up</Text>
</TouchableOpacity> </TouchableOpacity>
<Text style={styles.textCenter}>Already have an account?</Text> {errors.map((error) => (
<TouchableOpacity <Text key={error.longMessage} style={{ color: 'red' }}>
style={styles.signIn} {error.longMessage}
onPress={() => { </Text>
router.navigate('/sign-in') ))}
}}
>
<Text style={{ color: '#fff' }}>Sign In</Text>
</TouchableOpacity>{' '}
</View> </View>
)
}
return (
<View style={styles.container}>
<>
<Text style={styles.text}>Create an account</Text>
<View style={{ width: '80%', marginTop: 20 }}>
<Text style={styles.textlabel}>First name</Text>
<TextInput
style={styles.input}
value={fname}
placeholder='Enter first name'
onChangeText={setFname}
/>
<Text style={styles.textlabel}>Last name</Text>
<TextInput
style={styles.input}
value={lname}
placeholder='Enter last name'
onChangeText={setLname}
/>
<Text style={styles.textlabel}>Email</Text>
<TextInput
style={styles.input}
value={email}
placeholder='Enter email'
onChangeText={setUsername}
/>
<Text style={styles.textlabel}>Password</Text>
<TextInput
style={styles.input}
value={password}
placeholder='Enter password'
secureTextEntry
onChangeText={setPassword}
/>
<TouchableOpacity
style={styles.button}
onPress={() => {
onSignUpPress()
}}
>
<Text style={{ color: '#fff', fontWeight: 'bold' }}>Sign Up</Text>
</TouchableOpacity>
<Text style={styles.textCenter}>Already have an account?</Text>
<TouchableOpacity
style={styles.signIn}
onPress={() => {
router.navigate('/sign-in')
}}
>
<Text style={{ color: '#fff' }}>Sign In</Text>
</TouchableOpacity>{' '}
</View>
{errors.map((error) => (
<Text key={error.longMessage} style={{ color: 'red' }}>
{error.longMessage}
</Text>
))}
</>
</View> </View>
) )
} }
@@ -92,7 +176,7 @@ const styles = StyleSheet.create({
textCenter: { textCenter: {
color: '#ffffff', color: '#ffffff',
alignSelf: 'center', alignSelf: 'center',
marginBottom: 6 marginBottom: 12
}, },
textlabel: { textlabel: {
color: '#fff', color: '#fff',

View File

@@ -1,8 +1,11 @@
import { useUser } from '@clerk/clerk-react'
import Ionicons from '@expo/vector-icons/Ionicons' import Ionicons from '@expo/vector-icons/Ionicons'
import { Tabs } from 'expo-router' import { Tabs } from 'expo-router'
import React from 'react' import React from 'react'
export default function TabLayout() { export default function TabLayout() {
const { user } = useUser()
return ( return (
<Tabs <Tabs
screenOptions={{ screenOptions={{
@@ -34,7 +37,8 @@ export default function TabLayout() {
headerShown: false, headerShown: false,
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} />
) ),
tabBarItemStyle: { display: user?.publicMetadata.role === 'admin' ? 'flex' : 'none' }
}} }}
/> />
<Tabs.Screen <Tabs.Screen

View File

@@ -71,7 +71,6 @@ const styles = StyleSheet.create({
}, },
profileInfoCard: { profileInfoCard: {
backgroundColor: '#373d44ff', backgroundColor: '#373d44ff',
flex: 0.25,
marginTop: 12, marginTop: 12,
borderColor: '#626e7aff', borderColor: '#626e7aff',
borderStyle: 'solid', borderStyle: 'solid',
@@ -79,25 +78,24 @@ const styles = StyleSheet.create({
borderRadius: 12 borderRadius: 12
}, },
profilePic: { profilePic: {
flex: 0.4,
marginTop: 12, marginTop: 12,
display: 'flex', display: 'flex',
alignItems: 'center' alignItems: 'center'
}, },
image: { image: {
height: 75, height: 75,
width: 75 width: 75,
borderRadius: 37.5
}, },
nameContainer: { flex: 0.3, display: 'flex', alignItems: 'center' }, nameContainer: { display: 'flex', alignItems: 'center' },
text: { text: {
color: 'white', color: 'white',
fontSize: 16, fontSize: 16,
marginTop: 6 marginTop: 6
}, },
buttonLayout: { buttonLayout: {
flex: 0.2,
alignItems: 'center', alignItems: 'center',
marginTop: 12 marginVertical: 12
}, },
userPostsCard: { userPostsCard: {
flex: 1 flex: 1

View File

@@ -56,10 +56,16 @@ export const PostComponent: React.FC<PostComponentProps> = ({ post, fetchData })
<Text style={{ color: '#fff' }}>{post.notes}</Text> <Text style={{ color: '#fff' }}>{post.notes}</Text>
{user?.publicMetadata.role !== 'admin' && ( {user?.publicMetadata.role !== 'admin' && (
<View style={{ flexDirection: 'row', justifyContent: 'space-between', marginTop: 10 }}> <View style={{ flexDirection: 'row', justifyContent: 'space-between', marginTop: 10 }}>
{post.status === StatusEnum.Created && <Text style={styles.created}>Created</Text>} {post.status === StatusEnum.Created && (
{post.status === StatusEnum.Pending && <Text style={styles.pending}>Pending</Text>} <Text style={[styles.created, styles.statusTag]}>Created</Text>
{post.status === StatusEnum.Denied && <Text style={styles.denied}>Denied</Text>} )}
{post.status === StatusEnum.Accepted && <Text style={styles.accepted}>Accepted</Text>} {post.status === StatusEnum.Pending && (
<Text style={[styles.pending, styles.statusTag]}>Pending</Text>
)}
{post.status === StatusEnum.Denied && <Text style={[styles.denied, styles.statusTag]}>Denied</Text>}
{post.status === StatusEnum.Approved && (
<Text style={[styles.approved, styles.statusTag]}>Approved</Text>
)}
</View> </View>
)} )}
{user?.publicMetadata.role === 'admin' && ( {user?.publicMetadata.role === 'admin' && (
@@ -111,8 +117,9 @@ const styles = StyleSheet.create({
padding: 10, padding: 10,
width: '100%' width: '100%'
}, },
created: { color: 'white' }, statusTag: { paddingVertical: 3, paddingHorizontal: 10, borderRadius: 6 },
pending: { color: 'yellow' }, created: { backgroundColor: '#0d6efd', color: '#ffffff' },
denied: { color: 'red' }, pending: { backgroundColor: '#ffc107', color: '#000000' },
accepted: { color: 'green' } denied: { backgroundColor: '#dc3545', color: '#ffffff' },
approved: { backgroundColor: '#198754', color: '#ffffff' }
}) })

View File

@@ -26,7 +26,7 @@ export const SignOutButton = () => {
const styles = StyleSheet.create({ const styles = StyleSheet.create({
button: { button: {
width: '40%', width: '40%',
height: '100%', height: 35,
backgroundColor: 'rgba(192, 196, 199, 1)', backgroundColor: 'rgba(192, 196, 199, 1)',
borderRadius: 5, borderRadius: 5,
alignItems: 'center', alignItems: 'center',

View File

@@ -10,5 +10,5 @@ export enum StatusEnum {
Created = 'created', Created = 'created',
Pending = 'pending', Pending = 'pending',
Denied = 'denied', Denied = 'denied',
Accepted = 'accepted' Approved = 'approved'
} }