From feca1dcebd6ed3332453f95a4887722821ca656b Mon Sep 17 00:00:00 2001 From: Vivian Pham Date: Mon, 17 Aug 2026 00:41:21 -0700 Subject: [PATCH 1/3] Implement authentication for SignUp and SignIn screens --- .env | 0 app.json | 6 +- app/(auth)/sign-in.tsx | 30 ++++++- app/(auth)/sign-up.tsx | 186 ++++++++++++++++++++++++++++++++++++++++- app/(auth)/welcome.tsx | 2 +- app/_layout.tsx | 22 +++-- lib/auth.ts | 5 ++ package.json | 6 ++ 8 files changed, 243 insertions(+), 14 deletions(-) create mode 100644 .env create mode 100644 lib/auth.ts diff --git a/.env b/.env new file mode 100644 index 0000000..e69de29 diff --git a/app.json b/app.json index cbdb370..1703811 100644 --- a/app.json +++ b/app.json @@ -34,12 +34,14 @@ "image": "./assets/images/splash.png", "imageWidth": 200, "resizeMode": "contain", - "backgroundColor": "#ffffff", + "backgroundColor": "#2f80ed", "dark": { "backgroundColor": "#000000" } } - ] + ], + "@clerk/expo", + "expo-secure-store" ], "experiments": { "typedRoutes": true, diff --git a/app/(auth)/sign-in.tsx b/app/(auth)/sign-in.tsx index 89d929f..6ff1d4c 100644 --- a/app/(auth)/sign-in.tsx +++ b/app/(auth)/sign-in.tsx @@ -1,18 +1,42 @@ -import { Image, ScrollView, Text, View } from "react-native"; +import { Alert, Image, ScrollView, Text, View } from "react-native"; import { icons, images } from "@/constants"; import InputField from "@/components/InputField"; import { useState } from "react"; import CustomButton from "@/components/CustomButton"; -import { Link } from "expo-router"; +import { Link, router } from "expo-router"; import OAuth from "@/components/OAuth"; +import { useSignIn } from "@clerk/expo"; const SignIn = () => { + const { signIn } = useSignIn(); const [form, setForm] = useState({ email: "", password: "", }); - const onSignInPress = async () => {}; + const onSignInPress = async () => { + const { error } = await signIn.password({ + emailAddress: form.email, + password: form.password, + }); + + if (error) { + console.error(JSON.stringify(error), null, 2); + + Alert.alert("Error", error?.errors[0].longMessage); + } + + if (signIn.status === "complete") { + const { error: finalizeError } = await signIn.finalize(); + + if (finalizeError) { + console.error(JSON.stringify(finalizeError, null, 2)); + Alert.alert("Error", finalizeError?.errors[0].longMessage); + } + + router.replace("/(root)/(tabs)/home"); + } + }; return ( diff --git a/app/(auth)/sign-up.tsx b/app/(auth)/sign-up.tsx index 747be58..5b143c5 100644 --- a/app/(auth)/sign-up.tsx +++ b/app/(auth)/sign-up.tsx @@ -1,19 +1,132 @@ -import { Image, ScrollView, Text, View } from "react-native"; +import { Alert, Image, ScrollView, Text, View } from "react-native"; import { icons, images } from "@/constants"; import InputField from "@/components/InputField"; import { useState } from "react"; import CustomButton from "@/components/CustomButton"; -import { Link } from "expo-router"; +import { Link, router } from "expo-router"; import OAuth from "@/components/OAuth"; +import { useAuth, useSignUp } from "@clerk/expo"; +import { ReactNativeModal } from "react-native-modal"; const SignUp = () => { + const { signUp, errors } = useSignUp(); + const { isLoaded } = useAuth(); const [form, setForm] = useState({ name: "", email: "", password: "", }); + const [verification, setVerification] = useState({ + state: "default", + error: "", + code: "", + }); + const [showSuccessModal, setShowSuccessModal] = useState(false); + + const onSignUpPress = async () => { + if (!isLoaded) return; + + const { error } = await signUp.password({ + emailAddress: form.email, + password: form.password, + }); + + if (error) { + console.error(JSON.stringify(error, null, 2)); + + // setVerification((_prev) => ({ + // ..._prev, + // error: error?.errors[0]?.longMessage || "Unable to create account", + // state: "error", + // })); + + Alert.alert("Error", error.errors[0].longMessage); + + return; + } + + const { error: sendError } = await signUp.verifications.sendEmailCode(); + if (sendError) { + console.error(JSON.stringify(sendError, null, 2)); + setVerification((_prev) => ({ + ..._prev, + error: + sendError?.errors[0]?.longMessage || + "Unable to send verification code", + state: "error", + })); + + return; + } + + setVerification((_prev) => ({ + ..._prev, + state: "pending", + })); + }; + + const onVerifyPress = async () => { + if (!isLoaded) return; + + const { error } = await signUp.verifications.verifyEmailCode({ + code: verification.code, + }); + + if (error) { + console.error(JSON.stringify(error, null, 2)); + + setVerification((_prev) => ({ + ..._prev, + error: error?.errors[0]?.longMessage || "Unable to verify account", + state: "failed", + })); + + return; + } - const onSignUpPress = async () => {}; + if (signUp.status !== "complete") { + setVerification((_prev) => ({ + ..._prev, + error: "Verification failed", + state: "failed", + })); + + return; + } + + if (!signUp.createdUserId) { + setVerification((_prev) => ({ + ..._prev, + error: "Unable to retrieve user information", + state: "failed", + })); + + return; + } + + // TODO: Insert new user to database + + // Make new Clerk session active + const { error: finalizeError } = await signUp.finalize(); + + if (finalizeError) { + console.error(JSON.stringify(finalizeError, null, 2)); + + setVerification((_prev) => ({ + ..._prev, + error: + finalizeError?.errors[0]?.longMessage || "Unable to complete sign up", + state: "failed", + })); + + return; + } + + setVerification((_prev) => ({ + ..._prev, + state: "success", + })); + }; return ( @@ -64,6 +177,7 @@ const SignUp = () => { }) } /> + { {/* Verification modal */} + + { + if (verification.state === "success") + setShowSuccessModal(() => true); + }} + > + + + Verification + + + We have sent a verification code to {form.email} + + + setVerification((_prev) => ({ + ..._prev, + code, + })) + } + /> + + {verification.error && ( + + {verification.error} + + )} + + + + + + + + + + Verified + + + You have successfully verified your account. + + + { + router.replace("/(root)/(tabs)/home"); + setShowSuccessModal(() => false); + }} + className="mt-5" + /> + + ); diff --git a/app/(auth)/welcome.tsx b/app/(auth)/welcome.tsx index 29fb66c..4f1f11e 100644 --- a/app/(auth)/welcome.tsx +++ b/app/(auth)/welcome.tsx @@ -61,7 +61,7 @@ const Index = () => { ? router.replace("/(auth)/sign-up") : swiperRef.current?.scrollBy(1) } - className="w-10/12 mt-10 mb-5" + className="w-11/12 mt-10 mb-5" /> ); diff --git a/app/_layout.tsx b/app/_layout.tsx index e749cc1..3ac320a 100644 --- a/app/_layout.tsx +++ b/app/_layout.tsx @@ -4,6 +4,16 @@ import "@/global.css"; import { useEffect } from "react"; import * as SplashScreen from "expo-splash-screen"; +import { ClerkProvider } from "@clerk/expo"; +import { tokenCache } from "@clerk/expo/token-cache"; + +const publishableKey: string = + process.env.EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY || ""; + +if (!publishableKey) { + throw new Error("Add your Clerk Publishable Key to the .env file"); +} + // Prevent the splash screen from auto-hiding before asset loading is complete. SplashScreen.preventAutoHideAsync(); @@ -25,10 +35,12 @@ export default function RootLayout() { if (!loaded) return null; return ( - - - - - + + + + + + + ); } diff --git a/lib/auth.ts b/lib/auth.ts new file mode 100644 index 0000000..142c8ab --- /dev/null +++ b/lib/auth.ts @@ -0,0 +1,5 @@ +export interface TokenCache { + getToken: (key: string) => Promise; + saveToken: (key: string, value: string) => Promise; + clearToken?: (key: string) => void; +} diff --git a/package.json b/package.json index 3346084..5e72f52 100644 --- a/package.json +++ b/package.json @@ -11,17 +11,22 @@ "lint": "expo lint" }, "dependencies": { + "@clerk/expo": "^4.3.0", + "@expo/metro-runtime": "~6.1.2", "@expo/vector-icons": "^15.0.3", "@react-navigation/bottom-tabs": "^7.4.0", "@react-navigation/elements": "^2.6.3", "@react-navigation/native": "^7.1.8", "expo": "~54.0.35", + "expo-auth-session": "~7.0.11", "expo-constants": "~18.0.13", + "expo-crypto": "~15.0.9", "expo-font": "~14.0.12", "expo-haptics": "~15.0.8", "expo-image": "~3.0.11", "expo-linking": "~8.0.12", "expo-router": "~6.0.24", + "expo-secure-store": "~15.0.8", "expo-splash-screen": "~31.0.13", "expo-status-bar": "~3.0.9", "expo-symbols": "~1.0.8", @@ -32,6 +37,7 @@ "react-dom": "19.1.0", "react-native": "0.81.5", "react-native-gesture-handler": "~2.28.0", + "react-native-modal": "^14.0.0-rc.1", "react-native-reanimated": "~4.1.1", "react-native-safe-area-context": "~5.6.0", "react-native-screens": "~4.16.0", From e449d0f96eadd44d53888c00c08d5c109b2f112b Mon Sep 17 00:00:00 2001 From: Vivian Pham Date: Mon, 17 Aug 2026 00:42:28 -0700 Subject: [PATCH 2/3] fix: remove .env from tracking and add to .gitignore Co-authored-by: Junie --- .env | 0 .gitignore | 1 + 2 files changed, 1 insertion(+) delete mode 100644 .env diff --git a/.env b/.env deleted file mode 100644 index e69de29..0000000 diff --git a/.gitignore b/.gitignore index 232f982..7feb67e 100644 --- a/.gitignore +++ b/.gitignore @@ -31,6 +31,7 @@ yarn-error.* *.pem # local env files +.env .env*.local # typescript From 8beb2641dba26a30cd3a4666b913d13ca949d878 Mon Sep 17 00:00:00 2001 From: Vivian Pham Date: Mon, 17 Aug 2026 01:01:26 -0700 Subject: [PATCH 3/3] Implement suggested CodeRabbit fixes and check if authentication is loaded before checking for sign-in state --- app/(auth)/sign-in.tsx | 4 ++++ app/index.tsx | 9 +++++++++ 2 files changed, 13 insertions(+) diff --git a/app/(auth)/sign-in.tsx b/app/(auth)/sign-in.tsx index 6ff1d4c..ad56903 100644 --- a/app/(auth)/sign-in.tsx +++ b/app/(auth)/sign-in.tsx @@ -24,6 +24,8 @@ const SignIn = () => { console.error(JSON.stringify(error), null, 2); Alert.alert("Error", error?.errors[0].longMessage); + + return; } if (signIn.status === "complete") { @@ -32,6 +34,8 @@ const SignIn = () => { if (finalizeError) { console.error(JSON.stringify(finalizeError, null, 2)); Alert.alert("Error", finalizeError?.errors[0].longMessage); + + return; } router.replace("/(root)/(tabs)/home"); diff --git a/app/index.tsx b/app/index.tsx index 974fd89..905390f 100644 --- a/app/index.tsx +++ b/app/index.tsx @@ -1,5 +1,14 @@ import { Redirect } from "expo-router"; +import { useAuth } from "@clerk/expo"; export default function Index() { + const { isSignedIn, isLoaded } = useAuth(); + + if (!isLoaded) return null; + + if (isSignedIn) { + return ; + } + return ; }