Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 80 additions & 0 deletions clients/mobile/app/(auth)/sign-in.tsx
Original file line number Diff line number Diff line change
@@ -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<string>("");
const [password, setPassword] = useState<string>("");
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 (
<TouchableWithoutFeedback onPress={Keyboard.dismiss}>
<View className="flex-1 justify-center px-6 bg-white">
<Text className="text-2xl font-bold text-primary mb-2">
Welcome back
</Text>
<Text className="text-sm text-shadow-strong mb-8">
Sign in to your account
</Text>

<View className="gap-y-3 mb-4">
<TextInput
value={email}
onChangeText={setEmail}
placeholder="Email"
keyboardType="email-address"
autoCapitalize="none"
className="border border-stroke-subtle rounded-xl px-4 py-3 text-base"
/>
<TextInput
value={password}
onChangeText={setPassword}
placeholder="Password"
secureTextEntry
autoCapitalize="none"
className="border border-stroke-subtle rounded-xl px-4 py-3 text-base"
/>
</View>

{error && <Text className="text-danger text-sm mb-4">{error}</Text>}

<Pressable
onPress={onLogin}
className="bg-primary rounded-xl py-4 items-center mb-4 active:opacity-80"
>
<Text className="text-white font-semibold text-base">Sign in</Text>
</Pressable>

<Link
href="/sign-up"
className="text-center text-sm text-shadow-strong"
>
Do not have an account?{" "}
<Text className="text-primary font-medium">Sign up</Text>
</Link>
</View>
</TouchableWithoutFeedback>
);
}
96 changes: 96 additions & 0 deletions clients/mobile/app/(auth)/sign-up.tsx
Original file line number Diff line number Diff line change
@@ -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<string>("");
const [password, setPassword] = useState<string>("");
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 (
<TouchableWithoutFeedback onPress={Keyboard.dismiss}>
<View className="flex-1 justify-center px-6 bg-white">
<Text className="text-2xl font-bold text-primary mb-2">
Create account
</Text>
<Text className="text-sm text-shadow-strong mb-8">
Sign up to get started
</Text>

<View className="gap-y-3 mb-4">
<TextInput
value={firstName}
onChangeText={setFirstName}
placeholder="First name"
className="border border-stroke-subtle rounded-xl px-4 py-3 text-base"
/>
<TextInput
value={lastName}
onChangeText={setLastName}
placeholder="Last name"
className="border border-stroke-subtle rounded-xl px-4 py-3 text-base"
/>
<TextInput
value={email}
onChangeText={setEmail}
placeholder="Email"
keyboardType="email-address"
autoCapitalize="none"
className="border border-stroke-subtle rounded-xl px-4 py-3 text-base"
/>
<TextInput
value={password}
onChangeText={setPassword}
placeholder="Password"
secureTextEntry
autoCapitalize="none"
className="border border-stroke-subtle rounded-xl px-4 py-3 text-base"
/>
</View>

{error && <Text className="text-danger text-sm mb-4">{error}</Text>}

<Pressable
onPress={onSignUp}
className="bg-primary rounded-xl py-4 items-center mb-4 active:opacity-80"
>
<Text className="text-white font-semibold text-base">Sign Up</Text>
</Pressable>

<Link
href="/sign-in"
className="text-center text-sm text-shadow-strong"
>
Already have an account?{" "}
<Text className="text-primary font-medium">Sign in</Text>
</Link>
</View>
</TouchableWithoutFeedback>
);
}
76 changes: 76 additions & 0 deletions clients/mobile/app/(auth)/verify.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<TouchableWithoutFeedback onPress={Keyboard.dismiss}>
<View className="flex-1 justify-center px-6 bg-white">
<Text className="text-2xl font-bold text-primary mb-2">
Check your email
</Text>
<Text className="text-sm text-shadow-strong mb-8">
Verification code sent to {email}
</Text>

<TextInput
value={code}
onChangeText={setCode}
placeholder="Enter code"
keyboardType="number-pad"
autoComplete="one-time-code"
autoFocus
className="border border-stroke-subtle rounded-xl px-4 py-3 text-base mb-4"
/>

{error && <Text className="text-danger text-sm mb-4">{error}</Text>}

<Pressable
onPress={onVerify}
className="bg-primary rounded-xl py-4 items-center mb-3 active:opacity-80"
>
<Text className="text-white font-semibold text-base">Verify</Text>
</Pressable>

<Pressable
onPress={onResend}
className="py-3 items-center active:opacity-80"
>
<Text className="text-primary text-sm font-medium">Resend code</Text>
</Pressable>
</View>
</TouchableWithoutFeedback>
);
}
9 changes: 9 additions & 0 deletions clients/mobile/app/(tabs)/_layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,15 @@ export default function TabLayout() {
),
}}
/>
<Tabs.Screen
name="profile"
options={{
title: "Profile",
tabBarIcon: ({ color }) => (
<IconSymbol size={28} name="person.fill" color={color} />
),
}}
/>
</Tabs>
);
}
15 changes: 15 additions & 0 deletions clients/mobile/app/(tabs)/profile.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<View className="flex-1 justify-center px-6">
<LogoutButton onSignOut={onSignOut} />
</View>
);
}
19 changes: 19 additions & 0 deletions clients/mobile/app/home.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { Text, View, Pressable } from "react-native";
import { router } from "expo-router";

export default function Page() {
return (
<View className="flex-1 items-center p-6">
<View className="flex-1 justify-center max-w-4xl mx-auto">
<Text className="text-6xl font-bold">Hello</Text>
<Text className="text-4xl text-gray-700">World</Text>
<Pressable
onPress={() => router.push("/(tabs)/explore")}
className="bg-primary rounded-xl py-4 items-center mt-8 active:opacity-80"
>
<Text>Go to tabs</Text>
</Pressable>
</View>
</View>
);
}
27 changes: 17 additions & 10 deletions clients/mobile/app/index.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<View className="flex-1 items-center p-6">
<View className="flex-1 justify-center max-w-4xl mx-auto">
<Text className="text-6xl font-bold">Hello</Text>
<Text className="text-4xl text-gray-700">World</Text>
</View>
</View>
);
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;
}
20 changes: 20 additions & 0 deletions clients/mobile/components/Logout.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<Pressable
onPress={onPress}
className="border border-danger rounded-xl py-4 items-center active:opacity-80"
>
<Text className="text-danger font-semibold text-base">Sign Out</Text>
</Pressable>
);
}
3 changes: 0 additions & 3 deletions clients/mobile/components/ui/guest-card.tsx
Original file line number Diff line number Diff line change
@@ -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;
Expand Down
4 changes: 4 additions & 0 deletions clients/mobile/constants/clerk.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export enum ClerkStatus {
Complete = "complete",
MissingRequirements = "missing_requirements",
}
12 changes: 12 additions & 0 deletions clients/mobile/hooks/useClerkErrorHandler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
export const useClerkErrorHandler = (setError: (msg: string) => void) => {
const run = async (action: () => Promise<void>) => {
setError("");
try {
await action();
} catch (err: any) {
setError(err.errors?.[0]?.message);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks good but just wanna double check that this is standard error format for all errors returned by clerk

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, just double checked

}
};

return run;
};
Loading