diff --git a/clients/mobile/app/(auth)/sign-in.tsx b/clients/mobile/app/(auth)/sign-in.tsx new file mode 100644 index 00000000..26aebcb6 --- /dev/null +++ b/clients/mobile/app/(auth)/sign-in.tsx @@ -0,0 +1,80 @@ +import { ClerkStatus } from "@/constants/clerk"; +import { useClerkErrorHandler } from "@/hooks/useClerkErrorHandler"; +import { useSignIn } from "@clerk/clerk-expo"; +import { Link, router } from "expo-router"; +import { useState } from "react"; +import { + Pressable, + TextInput, + View, + Text, + Keyboard, + TouchableWithoutFeedback, +} from "react-native"; + +export default function Login() { + const { isLoaded, signIn, setActive } = useSignIn(); + const [email, setEmail] = useState(""); + const [password, setPassword] = useState(""); + const [error, setError] = useState(""); + const handleClerkAction = useClerkErrorHandler(setError); + + const onLogin = () => + handleClerkAction(async () => { + if (!isLoaded || !signIn || !setActive) return; + const result = await signIn.create({ identifier: email, password }); + if (result.status === ClerkStatus.Complete) { + await setActive({ session: result.createdSessionId }); + router.replace("/home"); + } + }); + + return ( + + + + Welcome back + + + Sign in to your account + + + + + + + + {error && {error}} + + + Sign in + + + + Do not have an account?{" "} + Sign up + + + + ); +} diff --git a/clients/mobile/app/(auth)/sign-up.tsx b/clients/mobile/app/(auth)/sign-up.tsx new file mode 100644 index 00000000..d5cbed27 --- /dev/null +++ b/clients/mobile/app/(auth)/sign-up.tsx @@ -0,0 +1,96 @@ +import { useClerkErrorHandler } from "@/hooks/useClerkErrorHandler"; +import { useSignUp } from "@clerk/clerk-expo"; +import { Link, router } from "expo-router"; +import { useState } from "react"; +import { + Pressable, + TextInput, + View, + Text, + Keyboard, + TouchableWithoutFeedback, +} from "react-native"; + +export default function SignUp() { + const { isLoaded, signUp } = useSignUp(); + const [email, setEmail] = useState(""); + const [password, setPassword] = useState(""); + const [firstName, setFirstName] = useState(""); + const [lastName, setLastName] = useState(""); + const [error, setError] = useState(""); + const handleClerkAction = useClerkErrorHandler(setError); + + const onSignUp = () => + handleClerkAction(async () => { + if (!isLoaded) return; + await signUp.create({ + emailAddress: email, + password, + firstName, + lastName, + }); + await signUp.prepareEmailAddressVerification({ strategy: "email_code" }); + router.push(`/verify?email=${email}`); + }); + + return ( + + + + Create account + + + Sign up to get started + + + + + + + + + + {error && {error}} + + + Sign Up + + + + Already have an account?{" "} + Sign in + + + + ); +} diff --git a/clients/mobile/app/(auth)/verify.tsx b/clients/mobile/app/(auth)/verify.tsx new file mode 100644 index 00000000..b95bfab1 --- /dev/null +++ b/clients/mobile/app/(auth)/verify.tsx @@ -0,0 +1,76 @@ +import { ClerkStatus } from "@/constants/clerk"; +import { useClerkErrorHandler } from "@/hooks/useClerkErrorHandler"; +import { useSignUp } from "@clerk/clerk-expo"; +import { router, useLocalSearchParams } from "expo-router"; +import { useState } from "react"; +import { + Pressable, + TextInput, + Text, + View, + Keyboard, + TouchableWithoutFeedback, +} from "react-native"; + +export default function VerifyEmail() { + const { isLoaded, signUp, setActive } = useSignUp(); + const { email } = useLocalSearchParams(); + const [code, setCode] = useState(""); + const [error, setError] = useState(""); + const handleClerkAction = useClerkErrorHandler(setError); + + const onVerify = () => + handleClerkAction(async () => { + if (!isLoaded) return; + const result = await signUp.attemptEmailAddressVerification({ code }); + if (result.status === ClerkStatus.Complete) { + await setActive({ session: result.createdSessionId }); + router.replace("/home"); + } + }); + + const onResend = () => + handleClerkAction(async () => { + if (!isLoaded) return; + await signUp.prepareEmailAddressVerification({ strategy: "email_code" }); + }); + + return ( + + + + Check your email + + + Verification code sent to {email} + + + + + {error && {error}} + + + Verify + + + + Resend code + + + + ); +} diff --git a/clients/mobile/app/(tabs)/_layout.tsx b/clients/mobile/app/(tabs)/_layout.tsx index 5aa33365..d2b2e545 100644 --- a/clients/mobile/app/(tabs)/_layout.tsx +++ b/clients/mobile/app/(tabs)/_layout.tsx @@ -53,6 +53,15 @@ export default function TabLayout() { ), }} /> + ( + + ), + }} + /> ); } diff --git a/clients/mobile/app/(tabs)/profile.tsx b/clients/mobile/app/(tabs)/profile.tsx new file mode 100644 index 00000000..298e0461 --- /dev/null +++ b/clients/mobile/app/(tabs)/profile.tsx @@ -0,0 +1,15 @@ +import LogoutButton from "@/components/Logout"; +import { router } from "expo-router"; +import { View } from "react-native"; + +export default function Profile() { + const onSignOut = () => { + router.replace("/sign-in"); + }; + + return ( + + + + ); +} diff --git a/clients/mobile/app/home.tsx b/clients/mobile/app/home.tsx new file mode 100644 index 00000000..a5760c58 --- /dev/null +++ b/clients/mobile/app/home.tsx @@ -0,0 +1,19 @@ +import { Text, View, Pressable } from "react-native"; +import { router } from "expo-router"; + +export default function Page() { + return ( + + + Hello + World + router.push("/(tabs)/explore")} + className="bg-primary rounded-xl py-4 items-center mt-8 active:opacity-80" + > + Go to tabs + + + + ); +} diff --git a/clients/mobile/app/index.tsx b/clients/mobile/app/index.tsx index 39a4f698..2c65c172 100644 --- a/clients/mobile/app/index.tsx +++ b/clients/mobile/app/index.tsx @@ -1,12 +1,19 @@ -import { StyleSheet, Text, View } from "react-native"; +import { useAuth } from "@clerk/clerk-expo"; +import { useRouter } from "expo-router"; +import { useEffect } from "react"; -export default function Page() { - return ( - - - Hello - World - - - ); +export default function Index() { + const { isLoaded, isSignedIn } = useAuth(); + const router = useRouter(); + + useEffect(() => { + if (!isLoaded) return; + if (isSignedIn) { + router.replace("/home"); + } else { + router.replace("/sign-in"); + } + }, [router, isLoaded, isSignedIn]); + + return null; } diff --git a/clients/mobile/components/Logout.tsx b/clients/mobile/components/Logout.tsx new file mode 100644 index 00000000..5523d88c --- /dev/null +++ b/clients/mobile/components/Logout.tsx @@ -0,0 +1,20 @@ +import { useClerk } from "@clerk/clerk-expo"; +import { Pressable, Text } from "react-native"; + +export default function LogoutButton({ onSignOut }: { onSignOut: () => void }) { + const { signOut } = useClerk(); + + const onPress = async () => { + await signOut(); + onSignOut(); + }; + + return ( + + Sign Out + + ); +} diff --git a/clients/mobile/components/ui/guest-card.tsx b/clients/mobile/components/ui/guest-card.tsx index e71cd269..6c98a9fe 100644 --- a/clients/mobile/components/ui/guest-card.tsx +++ b/clients/mobile/components/ui/guest-card.tsx @@ -1,7 +1,4 @@ -import React from "react"; import { Pressable, View, Text } from "react-native"; -import { User } from "lucide-react-native"; -import { cn } from "@shared/utils"; interface GuestCardProps { firstName: string; diff --git a/clients/mobile/constants/clerk.ts b/clients/mobile/constants/clerk.ts new file mode 100644 index 00000000..5f7ac842 --- /dev/null +++ b/clients/mobile/constants/clerk.ts @@ -0,0 +1,4 @@ +export enum ClerkStatus { + Complete = "complete", + MissingRequirements = "missing_requirements", +} diff --git a/clients/mobile/hooks/useClerkErrorHandler.ts b/clients/mobile/hooks/useClerkErrorHandler.ts new file mode 100644 index 00000000..afed762f --- /dev/null +++ b/clients/mobile/hooks/useClerkErrorHandler.ts @@ -0,0 +1,12 @@ +export const useClerkErrorHandler = (setError: (msg: string) => void) => { + const run = async (action: () => Promise) => { + setError(""); + try { + await action(); + } catch (err: any) { + setError(err.errors?.[0]?.message); + } + }; + + return run; +};