Back to Guide

Thursday, July 3, 2025

Complete Clerk Authentication with expo Setup Guide

cover

Complete Clerk Authentication with expo Setup Guide

This guide will walk you through setting up Clerk authentication in your React Native Expo app from scratch.

šŸ“¦ Step 1: Install Required Packages

First, install all the necessary packages:

# Create a new expo app with desishub cli
desishub new my-expo-app

# Core Clerk package
pnpm install @clerk/clerk-expo

# Required dependencies
pnpm install expo-crypto expo-secure-store expo-web-browser expo-auth-session

# Form handling
pnpm install react-hook-form @hookform/resolvers zod

šŸ”§ Step 2: Configure Clerk

2.1 Create Clerk Account and App

  • Go to clerk.com and create an account
  • Create a new application
  • Copy your publishable key from the dashboard

2.2 Set up Environment Variables

  • Create a .env file in your project root:
EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY = pk_test_your_key_here;

2.3 Configure OAuth Providers (Optional)

In your Clerk dashboard:

  • Go to "User & Authentication" → "Social Connections"
  • Enable Google, GitHub, Apple, or Facebook
  • Follow the setup instructions for each provider

Add Clerk Provider to your Root Layout

import "@/global.css";
import { useColorScheme } from "@/hooks/useColorScheme";
import {
  DarkTheme,
  DefaultTheme,
  ThemeProvider,
} from "@react-navigation/native";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { useFonts } from "expo-font";
import { Stack } from "expo-router";
import { StatusBar } from "expo-status-bar";
import { GestureHandlerRootView } from "react-native-gesture-handler";
import { ClerkProvider } from "@clerk/clerk-expo";
import { createNotifications } from "react-native-notificated";
import { tokenCache } from "@clerk/clerk-expo/token-cache";
import "react-native-reanimated";
export default function RootLayout() {
  const colorScheme = useColorScheme();
  const [loaded] = useFonts({
    SpaceMono: require("../assets/fonts/SpaceMono-Regular.ttf"),
  });

  const { NotificationsProvider, useNotifications, ...events } =
    createNotifications();

  if (!loaded) {
    // Async font loading only occurs in development.
    return null;
  }

  const queryClient = new QueryClient();
  return (
    <ClerkProvider tokenCache={tokenCache}>
      <QueryClientProvider client={queryClient}>
        <GestureHandlerRootView>
          <NotificationsProvider>
            <ThemeProvider
              value={colorScheme === "dark" ? DarkTheme : DefaultTheme}
            >
              <Stack>
                <Stack.Screen name="(tabs)" options={{ headerShown: false }} />
                <Stack.Screen
                  name="(front-pages)"
                  options={{ headerShown: false }}
                />
                <Stack.Screen
                  name="(dashboard-pages)"
                  options={{ headerShown: false }}
                />
                <Stack.Screen name="(auth)" options={{ headerShown: false }} />
                <Stack.Screen name="+not-found" />
              </Stack>
              <StatusBar style="auto" />
            </ThemeProvider>
          </NotificationsProvider>
        </GestureHandlerRootView>

        {/* <Toast /> */}
      </QueryClientProvider>
    </ClerkProvider>
  );
}

Layout page

  • First, protect your sign-up and sign-in pages.

  • Create an (auth) route group. This will group your sign-up and sign-in pages. In the (auth) group, create a _layout.tsx file with the following code. The useAuth() hook is used to access the user's authentication state. If the user is already signed in, they will be redirected to the home page.

import { Redirect, Stack } from "expo-router";
import { useAuth } from "@clerk/clerk-expo";

export default function AuthRoutesLayout() {
  const { isSignedIn } = useAuth();

  if (isSignedIn) {
    return <Redirect href={"/"} />;
  }

  return <Stack />;
}

šŸŽØ Step 3: Create UI Components

  • Create a components/ui folder and add these files:

components/ui/Input.tsx

import { Ionicons } from "@expo/vector-icons";
import React, { forwardRef } from "react";
import {
  Text,
  TextInput,
  TextInputProps,
  TouchableOpacity,
  View,
} from "react-native";

interface InputProps extends TextInputProps {
  label?: string;
  error?: string;
  leftIcon?: keyof typeof Ionicons.glyphMap;
  rightIcon?: keyof typeof Ionicons.glyphMap;
  onRightIconPress?: () => void;
  containerClassName?: string;
}

export default forwardRef<TextInput, InputProps>(function Input(
  {
    label,
    error,
    leftIcon,
    rightIcon,
    onRightIconPress,
    containerClassName = "",
    ...props
  },
  ref
) {
  return (
    <View className={`mb-4 ${containerClassName}`}>
      {label && (
        <Text className="text-gray-800 text-base font-medium mb-2">
          {label}
        </Text>
      )}
      <View className="relative">
        <View
          className={`flex-row items-center bg-white border-2 rounded-xl px-4 py-4 ${
            error ? "border-red-400" : "border-gray-200"
          }`}
        >
          {leftIcon && (
            <Ionicons
              name={leftIcon}
              size={20}
              color={error ? "#f87171" : "#9ca3af"}
              style={{ marginRight: 12 }}
            />
          )}
          <TextInput
            ref={ref}
            className="flex-1 text-gray-900 text-base"
            placeholderTextColor="#9ca3af"
            {...props}
          />
          {rightIcon && (
            <TouchableOpacity onPress={onRightIconPress} className="ml-3">
              <Ionicons
                name={rightIcon}
                size={20}
                color={error ? "#f87171" : "#9ca3af"}
              />
            </TouchableOpacity>
          )}
        </View>
      </View>
      {error && <Text className="text-red-500 text-sm mt-2 ml-1">{error}</Text>}
    </View>
  );
});

components/ui/Button.tsx

import React from "react";
import {
  ActivityIndicator,
  Text,
  TouchableOpacity,
  TouchableOpacityProps,
} from "react-native";

interface ButtonProps extends TouchableOpacityProps {
  title: string;
  loading?: boolean;
  variant?: "primary" | "secondary" | "outline";
  size?: "small" | "medium" | "large";
}

export default function Button({
  title,
  loading = false,
  variant = "primary",
  size = "large",
  disabled,
  ...props
}: ButtonProps) {
  const getButtonStyles = () => {
    const baseStyles = "rounded-xl items-center justify-center";

    // Size styles
    const sizeStyles = {
      small: "px-4 py-2",
      medium: "px-6 py-3",
      large: "px-6 py-4",
    };

    // Variant styles
    const variantStyles = {
      primary: disabled || loading ? "bg-gray-300" : "bg-black",
      secondary:
        disabled || loading
          ? "bg-gray-100 border border-gray-200"
          : "bg-gray-100 border border-gray-200",
      outline:
        disabled || loading
          ? "bg-white border-2 border-gray-200"
          : "bg-white border-2 border-black",
    };

    return `${baseStyles} ${sizeStyles[size]} ${variantStyles[variant]}`;
  };

  const getTextStyles = () => {
    const baseStyles = "font-semibold";

    // Size styles
    const sizeStyles = {
      small: "text-sm",
      medium: "text-base",
      large: "text-base",
    };

    // Variant styles
    const variantStyles = {
      primary: disabled || loading ? "text-gray-500" : "text-white",
      secondary: disabled || loading ? "text-gray-400" : "text-gray-700",
      outline: disabled || loading ? "text-gray-400" : "text-black",
    };

    return `${baseStyles} ${sizeStyles[size]} ${variantStyles[variant]}`;
  };

  return (
    <TouchableOpacity
      className={getButtonStyles()}
      disabled={disabled || loading}
      {...props}
    >
      {loading ? (
        <ActivityIndicator
          size="small"
          color={variant === "primary" ? "#ffffff" : "#666666"}
        />
      ) : (
        <Text className={getTextStyles()}>{title}</Text>
      )}
    </TouchableOpacity>
  );
}

components/ui/SocialButton.tsx

import { Ionicons } from "@expo/vector-icons";
import React from "react";
import { ActivityIndicator, Text, TouchableOpacity, View } from "react-native";

interface SocialButtonProps {
  provider: "google" | "apple" | "facebook" | "github";
  onPress: () => void;
  loading?: boolean;
  disabled?: boolean;
}

export default function SocialButton({
  provider,
  onPress,
  loading = false,
  disabled = false,
}: SocialButtonProps) {
  const getProviderConfig = () => {
    switch (provider) {
      case "google":
        return {
          icon: "logo-google" as const,
          iconColor: "#4285F4",
          text: "Continue with Google",
          bgColor: "bg-white",
          borderColor: "border-gray-200",
          textColor: "text-gray-700",
        };
      case "apple":
        return {
          icon: "logo-apple" as const,
          iconColor: "#000000",
          text: "Sign in with Apple",
          bgColor: "bg-white",
          borderColor: "border-gray-200",
          textColor: "text-gray-700",
        };
      case "facebook":
        return {
          icon: "logo-facebook" as const,
          iconColor: "#ffffff",
          text: "Continue with Facebook",
          bgColor: "bg-blue-600",
          borderColor: "border-blue-600",
          textColor: "text-white",
        };
      case "github":
        return {
          icon: "logo-github" as const,
          iconColor: "#333333",
          text: "Continue with GitHub",
          bgColor: "bg-white",
          borderColor: "border-gray-200",
          textColor: "text-gray-700",
        };
      default:
        return {
          icon: "link-outline" as const,
          iconColor: "#666666",
          text: "Continue",
          bgColor: "bg-white",
          borderColor: "border-gray-200",
          textColor: "text-gray-700",
        };
    }
  };

  const config = getProviderConfig();
  const isDisabled = disabled || loading;

  return (
    <TouchableOpacity
      onPress={onPress}
      disabled={isDisabled}
      className={`
        w-full flex-row items-center justify-center
        ${config.bgColor} border-2 ${config.borderColor}
        rounded-xl py-4 px-6 mb-3
        ${isDisabled ? "opacity-50" : ""}
      `}
    >
      {loading ? (
        <ActivityIndicator
          size="small"
          color={provider === "facebook" ? "#ffffff" : "#666666"}
        />
      ) : (
        <View className="flex-row items-center">
          <Ionicons name={config.icon} size={20} color={config.iconColor} />
          <Text className={`${config.textColor} font-medium text-base ml-3`}>
            {config.text}
          </Text>
        </View>
      )}
    </TouchableOpacity>
  );
}

šŸ“± Step 4: Create Authentication Screens

screens/SignInScreen.tsx

import { Ionicons } from "@expo/vector-icons";
import { useSignIn, useSSO } from "@clerk/clerk-expo";
import * as AuthSession from "expo-auth-session";
import * as WebBrowser from "expo-web-browser";
import { useRouter } from "expo-router";
import React, { useCallback, useEffect, useState } from "react";
import { Controller, useForm } from "react-hook-form";
import {
  SafeAreaView,
  ScrollView,
  Text,
  TouchableOpacity,
  View,
} from "react-native";
import Button from "@/components/ui/Button";
import Input from "@/components/ui/Input";
import SocialButton from "@/components/ui/SocialButton";

// Browser optimization hook
export const useWarmUpBrowser = () => {
  useEffect(() => {
    void WebBrowser.warmUpAsync();
    return () => {
      void WebBrowser.coolDownAsync();
    };
  }, []);
};

WebBrowser.maybeCompleteAuthSession();

interface SignInFormData {
  email: string;
  password: string;
}

interface SignInScreenProps {
  onSignUp?: () => void;
}

export default function SignInScreen({ onSignUp }: SignInScreenProps) {
  useWarmUpBrowser();
  const { signIn, setActive, isLoaded } = useSignIn();
  const { startSSOFlow } = useSSO();
  const router = useRouter();

  const [showPassword, setShowPassword] = useState(false);
  const [loading, setLoading] = useState(false);
  const [socialLoading, setSocialLoading] = useState(false);

  const {
    control,
    handleSubmit,
    formState: { errors },
    setError,
  } = useForm<SignInFormData>({
    defaultValues: {
      email: "",
      password: "",
    },
  });

  const onSubmit = async (data: SignInFormData) => {
    if (!isLoaded) return;

    setLoading(true);
    try {
      const signInAttempt = await signIn.create({
        identifier: data.email,
        password: data.password,
      });

      if (signInAttempt.status === "complete") {
        await setActive({ session: signInAttempt.createdSessionId });
        router.replace("/(tabs)");
      }
    } catch (err: any) {
      console.error("Sign in error:", JSON.stringify(err, null, 2));
      setError("root", {
        message: err.errors?.[0]?.message || "Invalid email or password",
      });
    } finally {
      setLoading(false);
    }
  };

  const handleSocialSignIn = useCallback(
    async (strategy: "oauth_google" | "oauth_github" | "oauth_apple") => {
      setSocialLoading(true);
      try {
        const { createdSessionId, setActive } = await startSSOFlow({
          strategy,
          redirectUrl: AuthSession.makeRedirectUri(),
        });

        if (createdSessionId) {
          setActive!({ session: createdSessionId });
          router.replace("/(tabs)");
        }
      } catch (err: any) {
        console.error("Social sign in error:", JSON.stringify(err, null, 2));
        setError("root", {
          message: "Social sign in failed. Please try again.",
        });
      } finally {
        setSocialLoading(false);
      }
    },
    []
  );

  return (
    <SafeAreaView className="flex-1 bg-gray-50">
      <ScrollView className="px-6" showsVerticalScrollIndicator={false}>
        {/* Logo */}
        <View className="items-center pt-16 pb-8">
          <View className="w-16 h-16 bg-black rounded-2xl items-center justify-center mb-6">
            <Ionicons name="home" size={24} color="white" />
          </View>
          <Text className="text-2xl font-bold text-gray-900 text-center mb-2">
            Reales - Your Key to{"\n"}Seamless Real Estate
          </Text>
          <Text className="text-gray-600 text-center">
            Sign in or register and we'll get started.
          </Text>
        </View>

        {/* Form */}
        <View className="mb-6">
          <Controller
            control={control}
            name="email"
            rules={{
              required: "Email is required",
              pattern: {
                value: /^\S+@\S+$/i,
                message: "Invalid email address",
              },
            }}
            render={({ field: { onChange, onBlur, value } }) => (
              <Input
                label="Email"
                placeholder="flowforge@gmail.com"
                keyboardType="email-address"
                autoCapitalize="none"
                value={value}
                onChangeText={onChange}
                onBlur={onBlur}
                error={errors.email?.message}
              />
            )}
          />

          <Controller
            control={control}
            name="password"
            rules={{ required: "Password is required" }}
            render={({ field: { onChange, onBlur, value } }) => (
              <Input
                label="Password"
                placeholder="••••••••••••"
                secureTextEntry={!showPassword}
                rightIcon={showPassword ? "eye-off-outline" : "eye-outline"}
                onRightIconPress={() => setShowPassword(!showPassword)}
                value={value}
                onChangeText={onChange}
                onBlur={onBlur}
                error={errors.password?.message}
              />
            )}
          />

          <TouchableOpacity className="mb-6">
            <Text className="text-blue-600 text-right font-medium">
              Forgot password?
            </Text>
          </TouchableOpacity>

          {errors.root && (
            <Text className="text-red-500 text-sm mb-4 text-center">
              {errors.root.message}
            </Text>
          )}

          <Button
            title="Sign In"
            onPress={handleSubmit(onSubmit)}
            loading={loading}
          />
        </View>

        {/* Divider */}
        <View className="flex-row items-center mb-6">
          <View className="flex-1 h-px bg-gray-300" />
          <Text className="px-4 text-gray-500">Or</Text>
          <View className="flex-1 h-px bg-gray-300" />
        </View>

        {/* Social Buttons */}
        <View className="mb-8">
          <SocialButton
            provider="apple"
            onPress={() => handleSocialSignIn("oauth_apple")}
            loading={socialLoading}
          />
          <SocialButton
            provider="google"
            onPress={() => handleSocialSignIn("oauth_google")}
            loading={socialLoading}
          />
          <SocialButton
            provider="github"
            onPress={() => handleSocialSignIn("oauth_github")}
            loading={socialLoading}
          />
        </View>

        {/* Sign Up Link */}
        <View className="flex-row justify-center mb-8">
          <Text className="text-gray-600">Not a member? </Text>
          <TouchableOpacity onPress={onSignUp}>
            <Text className="text-blue-600 font-medium">Register</Text>
          </TouchableOpacity>
        </View>
      </ScrollView>
    </SafeAreaView>
  );
}

screens/SignUpScreen.tsx

import { Ionicons } from "@expo/vector-icons";
import { useSignUp, useSSO } from "@clerk/clerk-expo";
import * as AuthSession from "expo-auth-session";
import * as WebBrowser from "expo-web-browser";
import { useRouter } from "expo-router";
import React, { useCallback, useEffect, useState } from "react";
import { Controller, useForm } from "react-hook-form";
import {
  SafeAreaView,
  ScrollView,
  Text,
  TouchableOpacity,
  View,
} from "react-native";
import Button from "@/components/ui/Button";
import Input from "@/components/ui/Input";
import SocialButton from "@/components/ui/SocialButton";

// Browser optimization hook
export const useWarmUpBrowser = () => {
  useEffect(() => {
    void WebBrowser.warmUpAsync();
    return () => {
      void WebBrowser.coolDownAsync();
    };
  }, []);
};

WebBrowser.maybeCompleteAuthSession();

interface SignUpFormData {
  firstName: string;
  lastName: string;
  email: string;
  password: string;
}

interface SignUpScreenProps {
  onSignIn?: () => void;
}

export default function SignUpScreen({ onSignIn }: SignUpScreenProps) {
  useWarmUpBrowser();
  const { isLoaded, signUp, setActive } = useSignUp();
  const { startSSOFlow } = useSSO();
  const router = useRouter();

  const [loading, setLoading] = useState(false);
  const [socialLoading, setSocialLoading] = useState(false);
  const [showPassword, setShowPassword] = useState(false);
  const [pendingVerification, setPendingVerification] = useState(false);
  const [code, setCode] = useState("");

  const {
    control,
    handleSubmit,
    formState: { errors },
    setError,
  } = useForm<SignUpFormData>({
    defaultValues: {
      firstName: "",
      lastName: "",
      email: "",
      password: "",
    },
  });

  const onSubmit = async (data: SignUpFormData) => {
    if (!isLoaded) return;

    setLoading(true);
    try {
      await signUp.create({
        firstName: data.firstName,
        lastName: data.lastName,
        emailAddress: data.email,
        password: data.password,
      });

      await signUp.prepareEmailAddressVerification({ strategy: "email_code" });
      setPendingVerification(true);
    } catch (err: any) {
      console.error("Sign up error:", JSON.stringify(err, null, 2));
      setError("root", {
        message: err.errors?.[0]?.message || "Something went wrong",
      });
    } finally {
      setLoading(false);
    }
  };

  const onVerifyPress = async () => {
    if (!isLoaded) return;

    setLoading(true);
    try {
      const signUpAttempt = await signUp.attemptEmailAddressVerification({
        code,
      });

      if (signUpAttempt.status === "complete") {
        await setActive({ session: signUpAttempt.createdSessionId });
        router.replace("/(tabs)");
      }
    } catch (err: any) {
      console.error("Verification error:", JSON.stringify(err, null, 2));
      setError("root", {
        message: err.errors?.[0]?.message || "Invalid verification code",
      });
    } finally {
      setLoading(false);
    }
  };

  const handleSocialSignUp = useCallback(
    async (strategy: "oauth_google" | "oauth_github" | "oauth_apple") => {
      setSocialLoading(true);
      try {
        const { createdSessionId, setActive } = await startSSOFlow({
          strategy,
          redirectUrl: AuthSession.makeRedirectUri(),
        });

        if (createdSessionId) {
          setActive!({ session: createdSessionId });
          router.replace("/(tabs)");
        }
      } catch (err: any) {
        console.error("Social sign up error:", JSON.stringify(err, null, 2));
        setError("root", {
          message: "Social sign up failed. Please try again.",
        });
      } finally {
        setSocialLoading(false);
      }
    },
    []
  );

  if (pendingVerification) {
    return (
      <SafeAreaView className="flex-1 bg-gray-50">
        <ScrollView className="px-6" showsVerticalScrollIndicator={false}>
          <View className="items-center pt-16 pb-8">
            <View className="w-24 h-24 bg-blue-100 rounded-full items-center justify-center mb-6">
              <Ionicons name="mail-outline" size={40} color="#3b82f6" />
            </View>
            <Text className="text-2xl font-bold text-gray-900 mb-2">
              Check Your Email
            </Text>
            <Text className="text-gray-600 text-center mb-8">
              We've sent a verification code to your email address.
            </Text>
          </View>

          <Input
            label="Verification Code"
            placeholder="Enter 6-digit code"
            value={code}
            onChangeText={setCode}
            keyboardType="number-pad"
            maxLength={6}
          />

          {errors.root && (
            <Text className="text-red-500 text-sm mb-4 text-center">
              {errors.root.message}
            </Text>
          )}

          <Button
            title="Verify Email"
            onPress={onVerifyPress}
            loading={loading}
          />

          <TouchableOpacity
            onPress={() => setPendingVerification(false)}
            className="mt-4"
          >
            <Text className="text-blue-600 text-center font-medium">
              Back to Sign Up
            </Text>
          </TouchableOpacity>
        </ScrollView>
      </SafeAreaView>
    );
  }

  return (
    <SafeAreaView className="flex-1 bg-gray-50">
      <ScrollView className="px-6" showsVerticalScrollIndicator={false}>
        {/* Logo */}
        <View className="items-center pt-16 pb-8">
          <View className="w-16 h-16 bg-black rounded-2xl items-center justify-center mb-6">
            <Ionicons name="home" size={24} color="white" />
          </View>
          <Text className="text-2xl font-bold text-gray-900 text-center mb-2">
            Create Your Account
          </Text>
          <Text className="text-gray-600 text-center">
            Join us and start your real estate journey.
          </Text>
        </View>

        {/* Social Buttons */}
        <View className="mb-6">
          <SocialButton
            provider="apple"
            onPress={() => handleSocialSignUp("oauth_apple")}
            loading={socialLoading}
          />
          <SocialButton
            provider="google"
            onPress={() => handleSocialSignUp("oauth_google")}
            loading={socialLoading}
          />
          <SocialButton
            provider="github"
            onPress={() => handleSocialSignUp("oauth_github")}
            loading={socialLoading}
          />
        </View>

        {/* Divider */}
        <View className="flex-row items-center mb-6">
          <View className="flex-1 h-px bg-gray-300" />
          <Text className="px-4 text-gray-500">Or register with email</Text>
          <View className="flex-1 h-px bg-gray-300" />
        </View>

        {/* Form */}
        <View className="mb-6">
          <View className="flex-row space-x-3 mb-4">
            <View className="flex-1">
              <Controller
                control={control}
                name="firstName"
                rules={{ required: "First name is required" }}
                render={({ field: { onChange, onBlur, value } }) => (
                  <Input
                    label="First Name"
                    placeholder="John"
                    value={value}
                    onChangeText={onChange}
                    onBlur={onBlur}
                    error={errors.firstName?.message}
                  />
                )}
              />
            </View>
            <View className="flex-1">
              <Controller
                control={control}
                name="lastName"
                rules={{ required: "Last name is required" }}
                render={({ field: { onChange, onBlur, value } }) => (
                  <Input
                    label="Last Name"
                    placeholder="Doe"
                    value={value}
                    onChangeText={onChange}
                    onBlur={onBlur}
                    error={errors.lastName?.message}
                  />
                )}
              />
            </View>
          </View>

          <Controller
            control={control}
            name="email"
            rules={{
              required: "Email is required",
              pattern: {
                value: /^\S+@\S+$/i,
                message: "Invalid email address",
              },
            }}
            render={({ field: { onChange, onBlur, value } }) => (
              <Input
                label="Email"
                placeholder="john@example.com"
                keyboardType="email-address"
                autoCapitalize="none"
                value={value}
                onChangeText={onChange}
                onBlur={onBlur}
                error={errors.email?.message}
              />
            )}
          />

          <Controller
            control={control}
            name="password"
            rules={{
              required: "Password is required",
              minLength: {
                value: 8,
                message: "Password must be at least 8 characters",
              },
            }}
            render={({ field: { onChange, onBlur, value } }) => (
              <Input
                label="Password"
                placeholder="••••••••••••"
                secureTextEntry={!showPassword}
                rightIcon={showPassword ? "eye-off-outline" : "eye-outline"}
                onRightIconPress={() => setShowPassword(!showPassword)}
                value={value}
                onChangeText={onChange}
                onBlur={onBlur}
                error={errors.password?.message}
              />
            )}
          />

          {errors.root && (
            <Text className="text-red-500 text-sm mb-4 text-center">
              {errors.root.message}
            </Text>
          )}

          <Button
            title="Create Account"
            onPress={handleSubmit(onSubmit)}
            loading={loading}
          />
        </View>

        {/* Sign In Link */}
        <View className="flex-row justify-center mb-8">
          <Text className="text-gray-600">Already have an account? </Text>
          <TouchableOpacity onPress={onSignIn}>
            <Text className="text-blue-600 font-medium">Sign In</Text>
          </TouchableOpacity>
        </View>
      </ScrollView>
    </SafeAreaView>
  );
}

screens/ProfileScreen.tsx

import { Ionicons } from "@expo/vector-icons";
import { useAuth, useUser } from "@clerk/clerk-expo";
import { useRouter } from "expo-router";
import React, { useState } from "react";
import { Controller, useForm } from "react-hook-form";
import {
  Alert,
  Image,
  SafeAreaView,
  ScrollView,
  Text,
  TouchableOpacity,
  View,
} from "react-native";
import Button from "@/components/ui/Button";
import Input from "@/components/ui/Input";

interface ProfileFormData {
  firstName: string;
  lastName: string;
}

export default function ProfileScreen() {
  const { signOut } = useAuth();
  const { user, isLoaded } = useUser();
  const router = useRouter();

  const [editMode, setEditMode] = useState(false);
  const [loading, setLoading] = useState(false);

  const {
    control,
    handleSubmit,
    formState: { errors },
    reset,
  } = useForm<ProfileFormData>({
    defaultValues: {
      firstName: user?.firstName || "",
      lastName: user?.lastName || "",
    },
  });

  const handleSignOut = () => {
    Alert.alert("Sign Out", "Are you sure you want to sign out?", [
      {
        text: "Cancel",
        style: "cancel",
      },
      {
        text: "Sign Out",
        style: "destructive",
        onPress: async () => {
          try {
            await signOut();
            router.replace("/sign-in");
          } catch (error) {
            console.error("Sign out error:", error);
          }
        },
      },
    ]);
  };

  const onSubmit = async (data: ProfileFormData) => {
    if (!user) return;

    setLoading(true);
    try {
      await user.update({
        firstName: data.firstName,
        lastName: data.lastName,
      });
      setEditMode(false);
      Alert.alert("Success", "Profile updated successfully!");
    } catch (error: any) {
      console.error("Update profile error:", error);
      Alert.alert("Error", "Failed to update profile. Please try again.");
    } finally {
      setLoading(false);
    }
  };

  const getInitials = () => {
    if (!user) return "";
    const first = user.firstName?.[0] || "";
    const last = user.lastName?.[0] || "";
    return `${first}${last}`.toUpperCase();
  };

  if (!isLoaded) {
    return (
      <SafeAreaView className="flex-1 bg-gray-50 items-center justify-center">
        <Text className="text-gray-600">Loading...</Text>
      </SafeAreaView>
    );
  }

  return (
    <SafeAreaView className="flex-1 bg-gray-50">
      <ScrollView className="px-6" showsVerticalScrollIndicator={false}>
        {/* Header */}
        <View className="items-center pt-16 pb-8">
          {user?.imageUrl ? (
            <Image
              source={{ uri: user.imageUrl }}
              className="w-24 h-24 rounded-full border-4 border-white shadow-lg"
            />
          ) : (
            <View className="w-24 h-24 rounded-full bg-black items-center justify-center border-4 border-white shadow-lg">
              <Text className="text-white text-2xl font-bold">
                {getInitials()}
              </Text>
            </View>
          )}
          <Text className="text-2xl font-bold text-gray-900 mt-4">
            {user?.fullName || "User"}
          </Text>
          <Text className="text-gray-600 text-base">
            {user?.primaryEmailAddress?.emailAddress}
          </Text>
        </View>

        {/* Profile Form */}
        <View className="bg-white rounded-2xl p-6 mb-6 shadow-sm">
          <View className="flex-row items-center justify-between mb-6">
            <Text className="text-xl font-bold text-gray-900">
              Profile Information
            </Text>
            <TouchableOpacity
              onPress={() => {
                if (editMode) {
                  setEditMode(false);
                  reset({
                    firstName: user?.firstName || "",
                    lastName: user?.lastName || "",
                  });
                } else {
                  setEditMode(true);
                }
              }}
              className="p-2"
            >
              <Ionicons
                name={editMode ? "close" : "pencil"}
                size={20}
                color="#374151"
              />
            </TouchableOpacity>
          </View>

          {editMode ? (
            <View>
              <View className="flex-row space-x-3 mb-4">
                <View className="flex-1">
                  <Controller
                    control={control}
                    name="firstName"
                    rules={{ required: "First name is required" }}
                    render={({ field: { onChange, onBlur, value } }) => (
                      <Input
                        label="First Name"
                        placeholder="Enter first name"
                        value={value}
                        onChangeText={onChange}
                        onBlur={onBlur}
                        error={errors.firstName?.message}
                      />
                    )}
                  />
                </View>
                <View className="flex-1">
                  <Controller
                    control={control}
                    name="lastName"
                    rules={{ required: "Last name is required" }}
                    render={({ field: { onChange, onBlur, value } }) => (
                      <Input
                        label="Last Name"
                        placeholder="Enter last name"
                        value={value}
                        onChangeText={onChange}
                        onBlur={onBlur}
                        error={errors.lastName?.message}
                      />
                    )}
                  />
                </View>
              </View>

              <Button
                title="Save Changes"
                onPress={handleSubmit(onSubmit)}
                loading={loading}
              />
            </View>
          ) : (
            <View className="space-y-4">
              <View>
                <Text className="text-gray-600 text-sm mb-1">Full Name</Text>
                <Text className="text-gray-900 text-base font-medium">
                  {user?.fullName || "Not provided"}
                </Text>
              </View>
              <View>
                <Text className="text-gray-600 text-sm mb-1">Email</Text>
                <Text className="text-gray-900 text-base font-medium">
                  {user?.primaryEmailAddress?.emailAddress || "Not provided"}
                </Text>
              </View>
              <View>
                <Text className="text-gray-600 text-sm mb-1">Member Since</Text>
                <Text className="text-gray-900 text-base font-medium">
                  {user?.createdAt
                    ? new Date(user.createdAt).toLocaleDateString()
                    : "N/A"}
                </Text>
              </View>
            </View>
          )}
        </View>

        {/* Connected Accounts */}
        {user?.externalAccounts && user.externalAccounts.length > 0 && (
          <View className="bg-white rounded-2xl p-6 mb-6 shadow-sm">
            <Text className="text-xl font-bold text-gray-900 mb-4">
              Connected Accounts
            </Text>
            <View className="space-y-3">
              {user.externalAccounts.map((account, index) => {
                const providerString = account.provider?.toString() || "";
                const isGoogle = providerString.includes("google");
                const isGithub = providerString.includes("github");
                const isApple = providerString.includes("apple");

                return (
                  <View
                    key={index}
                    className="flex-row items-center justify-between py-3 border-b border-gray-100 last:border-b-0"
                  >
                    <View className="flex-row items-center">
                      <Ionicons
                        name={
                          isGoogle
                            ? "logo-google"
                            : isGithub
                            ? "logo-github"
                            : isApple
                            ? "logo-apple"
                            : "link-outline"
                        }
                        size={20}
                        color={
                          isGoogle
                            ? "#4285F4"
                            : isGithub
                            ? "#333"
                            : isApple
                            ? "#000"
                            : "#6b7280"
                        }
                      />
                      <View className="ml-3">
                        <Text className="text-gray-900 font-medium">
                          {isGoogle
                            ? "Google"
                            : isGithub
                            ? "GitHub"
                            : isApple
                            ? "Apple"
                            : providerString}
                        </Text>
                        <Text className="text-gray-500 text-sm">
                          {account.emailAddress || "Connected"}
                        </Text>
                      </View>
                    </View>
                    <View className="w-3 h-3 bg-green-500 rounded-full" />
                  </View>
                );
              })}
            </View>
          </View>
        )}

        {/* Settings */}
        <View className="bg-white rounded-2xl p-6 mb-6 shadow-sm">
          <Text className="text-xl font-bold text-gray-900 mb-4">Settings</Text>
          <View className="space-y-4">
            <TouchableOpacity className="flex-row items-center justify-between py-3">
              <View className="flex-row items-center">
                <Ionicons
                  name="notifications-outline"
                  size={20}
                  color="#6b7280"
                />
                <Text className="text-gray-900 font-medium ml-3">
                  Notifications
                </Text>
              </View>
              <Ionicons name="chevron-forward" size={16} color="#6b7280" />
            </TouchableOpacity>

            <TouchableOpacity className="flex-row items-center justify-between py-3">
              <View className="flex-row items-center">
                <Ionicons name="shield-outline" size={20} color="#6b7280" />
                <Text className="text-gray-900 font-medium ml-3">
                  Privacy & Security
                </Text>
              </View>
              <Ionicons name="chevron-forward" size={16} color="#6b7280" />
            </TouchableOpacity>

            <TouchableOpacity className="flex-row items-center justify-between py-3">
              <View className="flex-row items-center">
                <Ionicons
                  name="help-circle-outline"
                  size={20}
                  color="#6b7280"
                />
                <Text className="text-gray-900 font-medium ml-3">
                  Help & Support
                </Text>
              </View>
              <Ionicons name="chevron-forward" size={16} color="#6b7280" />
            </TouchableOpacity>
          </View>
        </View>

        {/* Sign Out */}
        <View className="bg-white rounded-2xl p-6 mb-8 shadow-sm">
          <Button title="Sign Out" variant="outline" onPress={handleSignOut} />
        </View>
      </ScrollView>
    </SafeAreaView>
  );
}

šŸš€ Step 5: Create App Pages

app/sign-in.tsx

import { useRouter } from "expo-router";
import SignInScreen from "@/screens/SignInScreen";

export default function SignInPage() {
  const router = useRouter();

  return <SignInScreen onSignUp={() => router.push("/sign-up")} />;
}

app/sign-up.tsx

import { useRouter } from "expo-router";
import SignUpScreen from "@/screens/SignUpScreen";

export default function SignUpPage() {
  const router = useRouter();

  return <SignUpScreen onSignIn={() => router.push("/sign-in")} />;
}

app/(tabs)/profile.tsx (Profile Tab)

import ProfileScreen from "@/screens/ProfileScreen";

export default function ProfileTab() {
  return <ProfileScreen />;
}

šŸŽ‰ Congratulations! You now have a fully functional authentication system with:

āœ… Beautiful, modern UI matching the design āœ… Email/password authentication āœ… Social authentication (Google, GitHub, Apple) āœ… Email verification āœ… Profile management āœ… Secure token storage āœ… Form validation with React Hook Form āœ… TypeScript support

Your app is ready for production! šŸš€

Subscribe to My Latest Guides

All the latest Guides and tutorials, straight from the team.

Subscribe to be the first to receive the new Guide when it comes out

•Get All Guides at Once