# How to Set Up Clerk Authentication with Expo Router: Protected Routes, MFA, and Social Login - Part 2

> Part 2 of 2. Start with [How to Set Up Clerk Authentication with Expo Router: Protected Routes, MFA, and Social Login](https://clerk.com/articles/how-to-set-up-clerk-authentication-with-expo-router-protected-routes-mfa-and-soc.md).

_This is Part 2 of our guide on setting up Clerk authentication with Expo Router. In [Part 1](https://clerk.com/articles/how-to-set-up-clerk-authentication-with-expo-router-protected-routes-mfa-and-soc.md), you scaffolded the Expo project, configured `ClerkProvider`, built the core sign-in and sign-up flows, and set up protected routes. In this part, you will add advanced features to your application, including user profiles, multi-factor authentication (MFA), social logins, and customization._

## User profile and user button

### Displaying the UserButton component

The native `UserButton` component from `@clerk/expo/native` provides a prebuilt avatar that opens the user profile modal on tap. It requires a development build.

```tsx
import { UserButton } from '@clerk/expo/native'
import { View } from 'react-native'

function UserButtonExample() {
  return (
    <View style={{ width: 40, height: 40, borderRadius: 20, overflow: 'hidden' }}>
      <UserButton />
    </View>
  )
}
```

The native `UserButton` accepts no props. Sizing is controlled entirely by the parent container's `width`, `height`, `borderRadius`, and `overflow` styles. Tapping the button opens a native `UserProfileView` modal automatically.

For Expo Go compatibility, build a custom user button using `useUser()`:

```tsx
import { useUser } from '@clerk/expo'
import { Image, TouchableOpacity } from 'react-native'

function CustomUserButton({ onPress }: { onPress: () => void }) {
  const { user } = useUser()

  return (
    <TouchableOpacity onPress={onPress}>
      <Image source={{ uri: user?.imageUrl }} style={{ width: 40, height: 40, borderRadius: 20 }} />
    </TouchableOpacity>
  )
}
```

### Building a user profile page

The profile page uses `useUser()` to display user information:

```tsx
import { useUser } from '@clerk/expo'
import { Image, Text, View } from 'react-native'

export default function ProfileScreen() {
  const { user } = useUser()

  if (!user) return null

  return (
    <View style={{ flex: 1, padding: 24 }}>
      <View style={{ alignItems: 'center', marginBottom: 24 }}>
        <Image
          source={{ uri: user.imageUrl }}
          style={{ width: 80, height: 80, borderRadius: 40, marginBottom: 12 }}
        />
        <Text style={{ fontSize: 22, fontWeight: 'bold' }}>
          {user.firstName} {user.lastName}
        </Text>
        <Text style={{ color: '#666', marginTop: 4 }}>
          {user.primaryEmailAddress?.emailAddress}
        </Text>
      </View>

      <View style={{ borderTopWidth: 1, borderTopColor: '#eee', paddingTop: 16 }}>
        <Text style={{ fontWeight: '600', marginBottom: 8 }}>Account details</Text>
        <Text style={{ color: '#666', marginBottom: 4 }}>User ID: {user.id}</Text>
        <Text style={{ color: '#666', marginBottom: 4 }}>
          Created: {user.createdAt?.toLocaleDateString()}
        </Text>
        <Text style={{ color: '#666' }}>
          Last sign-in: {user.lastSignInAt?.toLocaleDateString()}
        </Text>
      </View>
    </View>
  )
}
```

For a native profile experience, use the `UserProfileView` component from `@clerk/expo/native` (requires a development build). This renders a native SwiftUI/Jetpack Compose profile management interface with built-in account settings, connected accounts, and security options.

### Customizing user profile fields

Clerk provides three metadata fields on the user object for storing custom data:

- `user.publicMetadata`: Readable from both frontend and backend; writable from backend only. Use for roles, feature flags, or other non-sensitive data that should be visible to the client.
- `user.unsafeMetadata`: Readable and writable from the frontend. Use for user preferences or non-sensitive settings.
- `user.privateMetadata`: Readable from backend only. Not accessible in client-side code.

To update user information programmatically:

```tsx
import { useUser } from '@clerk/expo'

function UpdateProfile() {
  const { user } = useUser()

  const updateName = async () => {
    await user?.update({
      firstName: 'Jane',
      lastName: 'Doe',
    })
  }

  const updatePreferences = async () => {
    await user?.update({
      unsafeMetadata: {
        theme: 'dark',
        language: 'en',
      },
    })
  }
}
```

## Adding multi-factor authentication

### Understanding MFA strategies in Clerk

Clerk supports three MFA strategies in Expo:

1. **SMS verification codes**: A one-time code sent via SMS to the user's registered phone number
2. **[Authenticator apps](https://clerk.com/glossary.md#authenticator-apps-totp) (TOTP)**: Time-based one-time passwords generated by apps like Google Authenticator, Authy, or 1Password
3. **[Backup codes](https://clerk.com/glossary.md#backup-codes)**: Single-use recovery codes generated when MFA is first enrolled

MFA must be enabled in the Clerk Dashboard under [**User & authentication → Multi-factor**](https://dashboard.clerk.com/~/user-authentication/multi-factor). The "Require multi-factor authentication" toggle forces MFA enrollment for all users. Backup codes require at least one other MFA strategy to be enabled first. MFA is available on the [Pro plan ($20/month billed annually as of 2026)](https://clerk.com/pricing).

### Handling MFA during sign-in

After calling `signIn.password()`, check `signIn.status`:

- `'complete'`: First factor succeeded, no MFA required. Call `signIn.finalize()`.
- `'needs_second_factor'`: MFA is required. Present a verification form and use the `signIn.mfa.*` methods.

The `signIn.supportedSecondFactors` property lists the available MFA methods after the first factor is verified. This tells you which verification options to present to the user.

### Building the MFA verification screen

This component handles all three MFA strategies and can be integrated into the sign-in flow:

```tsx
import { useSignIn } from '@clerk/expo'
import { useState } from 'react'
import { Text, TextInput, TouchableOpacity, View } from 'react-native'

type MfaStrategy = 'totp' | 'phone_code' | 'backup_code'

export function MfaVerification() {
  const { signIn, errors, fetchStatus } = useSignIn()
  const [code, setCode] = useState('')
  const [strategy, setStrategy] = useState<MfaStrategy>('totp')

  const strategies: { key: MfaStrategy; label: string }[] = [
    { key: 'totp', label: 'Authenticator app' },
    { key: 'phone_code', label: 'SMS code' },
    { key: 'backup_code', label: 'Backup code' },
  ]

  const onSendSmsCode = async () => {
    await signIn.mfa.sendPhoneCode()
  }

  const onVerify = async () => {
    if (strategy === 'totp') {
      await signIn.mfa.verifyTOTP({ code })
    } else if (strategy === 'phone_code') {
      await signIn.mfa.verifyPhoneCode({ code })
    } else if (strategy === 'backup_code') {
      await signIn.mfa.verifyBackupCode({ code })
    }

    if (signIn.status === 'complete') {
      await signIn.finalize()
    }
  }

  return (
    <View style={{ flex: 1, padding: 24, justifyContent: 'center' }}>
      <Text style={{ fontSize: 24, fontWeight: 'bold', marginBottom: 8 }}>
        Two-factor authentication
      </Text>
      <Text style={{ color: '#666', marginBottom: 24 }}>
        Choose a verification method and enter your code.
      </Text>

      <View style={{ flexDirection: 'row', marginBottom: 16, gap: 8 }}>
        {strategies.map(({ key, label }) => (
          <TouchableOpacity
            key={key}
            onPress={() => {
              setStrategy(key)
              setCode('')
              if (key === 'phone_code') onSendSmsCode()
            }}
            style={{
              flex: 1,
              padding: 8,
              borderRadius: 8,
              borderWidth: 1,
              borderColor: strategy === key ? '#6C47FF' : '#ccc',
              backgroundColor: strategy === key ? '#F0ECFF' : 'white',
              alignItems: 'center',
            }}
          >
            <Text
              style={{
                fontSize: 12,
                color: strategy === key ? '#6C47FF' : '#666',
                fontWeight: strategy === key ? '600' : '400',
              }}
            >
              {label}
            </Text>
          </TouchableOpacity>
        ))}
      </View>

      <TextInput
        value={code}
        onChangeText={setCode}
        placeholder={strategy === 'backup_code' ? 'Enter backup code' : 'Enter verification code'}
        keyboardType={strategy === 'backup_code' ? 'default' : 'number-pad'}
        style={{
          borderWidth: 1,
          borderColor: '#ccc',
          borderRadius: 8,
          padding: 12,
          marginBottom: 16,
        }}
      />

      {errors?.fields?.code && (
        <Text style={{ color: 'red', marginBottom: 8 }}>{errors.fields.code[0]?.message}</Text>
      )}

      <TouchableOpacity
        onPress={onVerify}
        disabled={fetchStatus === 'fetching'}
        style={{
          backgroundColor: '#6C47FF',
          padding: 14,
          borderRadius: 8,
          alignItems: 'center',
        }}
      >
        <Text style={{ color: 'white', fontWeight: '600' }}>Verify</Text>
      </TouchableOpacity>
    </View>
  )
}
```

For TOTP verification, no "send" step is needed because the code is generated by the authenticator app. For SMS, call `signIn.mfa.sendPhoneCode()` first to trigger the SMS delivery, then verify with `signIn.mfa.verifyPhoneCode({ code })`. Backup codes are single-use and validated with `signIn.mfa.verifyBackupCode({ code })`.

> For MFA enrollment (setting up TOTP for the first time from within your app), see the [Clerk TOTP management guide](https://clerk.com/docs/guides/development/custom-flows/account-updates/manage-totp-based-mfa.md). The enrollment flow involves `user.createTOTP()` to generate a QR code URI, `user.verifyTOTP({ code })` to confirm setup, and `user.createBackupCode()` to generate recovery codes.

## Adding social login and OAuth

### Native Google sign-in

Native Google sign-in uses `useSignInWithGoogle` from `@clerk/expo/google`. On Android, it uses Credential Manager (no browser popup). On iOS, it uses ASAuthorization for a native experience.

Setup requirements:

1. Create three OAuth 2.0 credentials in [Google Cloud Console](https://console.cloud.google.com/apis/credentials): iOS Client ID, Android Client ID, and Web Client ID (the Web Client ID is required even for native mobile flows)
2. Register SHA-1 fingerprints with Google Cloud Console for creating OAuth client IDs. Register SHA-256 fingerprints in the Clerk Dashboard under [**Native applications**](https://dashboard.clerk.com/~/native-applications) for Android App Links verification (used for passkeys and deep linking). Both come from the same keystore via `keytool -list -v` but serve different purposes
3. Add Client IDs to your `.env` and configure `app.json`
4. Enable Google in the Clerk Dashboard under [**SSO connections → Social**](https://dashboard.clerk.com/~/user-authentication/sso-connections/social)

```tsx
import { useSignInWithGoogle } from '@clerk/expo/google'
import { useRouter } from 'expo-router'
import { Platform, Text, TouchableOpacity } from 'react-native'

export function GoogleSignInButton() {
  const { startGoogleAuthenticationFlow } = useSignInWithGoogle()
  const router = useRouter()

  const onGoogleSignIn = async () => {
    try {
      const { createdSessionId, setActive } = await startGoogleAuthenticationFlow()

      if (createdSessionId && setActive) {
        await setActive({ session: createdSessionId })
        router.replace('/')
      }
    } catch (err: unknown) {
      const error = err as { code?: string | number }
      if (error.code === 'SIGN_IN_CANCELLED' || error.code === -5) {
        return
      }
      console.error('Google sign-in error:', err)
    }
  }

  if (Platform.OS === 'web') return null

  return (
    <TouchableOpacity
      onPress={onGoogleSignIn}
      style={{
        flexDirection: 'row',
        alignItems: 'center',
        justifyContent: 'center',
        backgroundColor: '#fff',
        borderWidth: 1,
        borderColor: '#ddd',
        borderRadius: 8,
        padding: 14,
      }}
    >
      <Text style={{ fontWeight: '600' }}>Continue with Google</Text>
    </TouchableOpacity>
  )
}
```

> The native Google/Apple sign-in hooks still use the older setActive() pattern rather than signIn.finalize(). This is the current documented behavior as of @clerk/expo v3.1.

The `SIGN_IN_CANCELLED` error (or code `-5` on Android) occurs when the user dismisses the sign-in prompt. Handle this silently without showing an error message. Native Google sign-in requires a development build and does not work in Expo Go.

### Native Apple sign-in

Native Apple sign-in uses `useSignInWithApple` from `@clerk/expo/apple`. This is iOS only.

> [Apple App Store Guideline 4.8](https://developer.apple.com/app-store/review/guidelines/#sign-in-with-apple) requires that any app offering third-party social login must also offer Sign in with Apple on iOS.

Setup requirements:

1. Register your native app in the Clerk Dashboard under [**Native applications**](https://dashboard.clerk.com/~/native-applications) with your Team ID (App ID Prefix) and Bundle ID
2. Enable Apple in the Clerk Dashboard under [**SSO connections → Social**](https://dashboard.clerk.com/~/user-authentication/sso-connections/social)
3. Install `expo-apple-authentication` and `expo-crypto`
4. Add `expo-apple-authentication` to the plugins in `app.json`

```tsx
import { useSignInWithApple } from '@clerk/expo/apple'
import { useRouter } from 'expo-router'
import { Platform, Text, TouchableOpacity } from 'react-native'

export function AppleSignInButton() {
  const { startAppleAuthenticationFlow } = useSignInWithApple()
  const router = useRouter()

  const onAppleSignIn = async () => {
    try {
      const { createdSessionId, setActive } = await startAppleAuthenticationFlow()

      if (createdSessionId && setActive) {
        await setActive({ session: createdSessionId })
        router.replace('/')
      }
    } catch (err: unknown) {
      const error = err as { code?: string }
      if (error.code === 'ERR_REQUEST_CANCELED') {
        return
      }
      console.error('Apple sign-in error:', err)
    }
  }

  if (Platform.OS !== 'ios') return null

  return (
    <TouchableOpacity
      onPress={onAppleSignIn}
      style={{
        flexDirection: 'row',
        alignItems: 'center',
        justifyContent: 'center',
        backgroundColor: '#000',
        borderRadius: 8,
        padding: 14,
      }}
    >
      <Text style={{ color: '#fff', fontWeight: '600' }}>Continue with Apple</Text>
    </TouchableOpacity>
  )
}
```

Simulator support for Apple sign-in is limited. Test on a physical device for reliable behavior. The `ERR_REQUEST_CANCELED` error indicates the user dismissed the Apple sign-in prompt.

### Browser-based SSO with useSSO

For providers without native SDKs (GitHub, Discord, and others), use the `useSSO` hook. This opens the system browser for authentication using Chrome Custom Tabs on Android and SFSafariViewController on iOS.

> `useSSO()` replaces the deprecated `useOAuth()` hook from Core 2. If migrating from older code, update all `useOAuth` calls to `useSSO`.

```tsx
import { useSSO } from '@clerk/expo'
import { useRouter } from 'expo-router'
import { Text, TouchableOpacity } from 'react-native'
import * as WebBrowser from 'expo-web-browser'

WebBrowser.maybeCompleteAuthSession()

export function GitHubSignInButton() {
  const { startSSOFlow } = useSSO()
  const router = useRouter()

  const onGitHubSignIn = async () => {
    try {
      const { createdSessionId, setActive } = await startSSOFlow({
        strategy: 'oauth_github',
      })

      if (createdSessionId && setActive) {
        await setActive({ session: createdSessionId })
        router.replace('/')
      }
    } catch (err) {
      console.error('GitHub sign-in error:', err)
    }
  }

  return (
    <TouchableOpacity
      onPress={onGitHubSignIn}
      style={{
        flexDirection: 'row',
        alignItems: 'center',
        justifyContent: 'center',
        backgroundColor: '#24292e',
        borderRadius: 8,
        padding: 14,
      }}
    >
      <Text style={{ color: '#fff', fontWeight: '600' }}>Continue with GitHub</Text>
    </TouchableOpacity>
  )
}
```

> Browser-based OAuth on Android is less reliable than the native Google flow. Around 30% of Android attempts can return a `DISMISS` result instead of completing, because `expo-auth-session` can misread the redirect back into the app as a user cancellation. This affects Chrome Custom Tabs on Android but not iOS, and the issue remains open ([expo/expo#23781](https://github.com/expo/expo/issues/23781)). For providers that offer a native hook — such as Google — prefer the native flow in production.

The `strategy` parameter accepts any of the [30+ OAuth providers](https://clerk.com/docs/authentication/social-connections/oauth.md) supported by Clerk (e.g., `'oauth_discord'`, `'oauth_slack'`, `'oauth_linkedin'`). Browser-based SSO requires `expo-auth-session` and `expo-web-browser`. Unlike native sign-in hooks and components, browser-based SSO works in Expo Go; the development build requirement applies only to native flows.

## Customization options

### Styling Clerk components with the appearance prop

Clerk provides [six prebuilt themes](https://clerk.com/docs/expo/guides/customizing-clerk/appearance-prop/themes.md) and 24 customization variables. Import themes from `@clerk/ui/themes` and pass them via the `appearance` prop on `ClerkProvider`:

```tsx
import { ClerkProvider } from '@clerk/expo'
import { tokenCache } from '@clerk/expo/token-cache'
import { dark } from '@clerk/ui/themes'
import { Slot } from 'expo-router'

const publishableKey = process.env.EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY!

export default function RootLayout() {
  return (
    <ClerkProvider
      publishableKey={publishableKey}
      tokenCache={tokenCache}
      appearance={{
        theme: dark,
        variables: {
          colorPrimary: '#6C47FF',
          borderRadius: '0.5rem',
          fontFamily: 'Inter',
        },
      }}
    >
      <Slot />
    </ClerkProvider>
  )
}
```

Available prebuilt themes: `default`, `simple`, `shadcn`, `dark`, `shadesOfPurple`, `neobrutalism`. Themes can be stacked by passing an array: `theme: [dark, neobrutalism]`.

Key customization variables include `colorPrimary`, `colorBackground`, `colorDanger`, `fontFamily`, `fontSize`, `borderRadius`, and `spacing`. The full variable list is available in the [appearance variables reference](https://clerk.com/docs/expo/guides/customizing-clerk/appearance-prop/variables.md).

### Localization

Clerk supports [52+ locales](https://clerk.com/docs/customization/localization.md) through the `@clerk/localizations` package:

```tsx
import { frFR } from '@clerk/localizations'
;<ClerkProvider localization={frFR} />
```

Localization is an experimental feature. It updates text in Clerk components but does not affect the hosted Account Portal. Custom string overrides are supported for fine-grained control.

### Native components vs. custom UI trade-offs

| Aspect           |       JS Custom UI       |    JS + Native Sign-In   |   Native Components  |
| ---------------- | :----------------------: | :----------------------: | :------------------: |
| Expo Go          |            Yes           |            No            |          No          |
| Customization    |           Full           |   Full + native buttons  | Limited (ClerkTheme) |
| Code required    |           Most           |         Moderate         |   Least (\~5 lines)  |
| OAuth experience |     Browser redirect     |    Native (no browser)   |  Native (automatic)  |
| Passkeys         | Via @clerk/expo-passkeys | Via @clerk/expo-passkeys |       Built-in       |
| Status           |            Yes           |            Yes           |         Beta         |

For the least code possible, the native `AuthView` component handles the entire sign-in/sign-up flow:

```tsx
import { useAuth } from '@clerk/expo'
import { AuthView } from '@clerk/expo/native'
import { Slot } from 'expo-router'

export default function AuthOrApp() {
  const { isSignedIn, isLoaded } = useAuth({ treatPendingAsSignedOut: false })

  if (!isLoaded) return null
  if (!isSignedIn) return <AuthView mode="signInOrUp" />
  return <Slot />
}
```

Start with the JS custom UI approach to understand the underlying API, then consider native components for production if styling flexibility is not a priority. Native components render using SwiftUI on iOS and Jetpack Compose on Android, providing a platform-native look and feel.

> AuthView passkey support requires domain association setup: iOS Associated Domains with a `webcredentials:` entry, and Android App Links with SHA-256 fingerprints. Without domain association, passkeys silently fail on physical devices. For full setup details, see the [Clerk Expo passkeys reference](https://clerk.com/docs/reference/expo/passkeys.md).

## Best practices

### Security considerations

- Always use `expo-secure-store` for token caching. Never use `AsyncStorage`, which stores data in plaintext
- Use separate Clerk instances with `pk_test_` keys for development and `pk_live_` keys for production
- Register native apps in the Clerk Dashboard under [**Native applications**](https://dashboard.clerk.com/~/native-applications) with the correct bundle identifiers and SHA fingerprints
- Do not use the deprecated `auth.expo.io` proxy ([CVE-2023-28131](https://www.cve.org/CVERecord?id=CVE-2023-28131))
- Use HTTPS for all API communication
- Remember that `<Show>` only visually hides content. Protect sensitive data with server-side validation
- Verify Android Auto Backup exclusion rules if using custom backup configuration

### Performance optimization

- Use `<ClerkLoaded>` and `<ClerkLoading>` strategically around Clerk-dependent components rather than wrapping the entire app
- Let Clerk handle token refresh automatically (60-second lifetime, refreshed every 50 seconds). Do not implement manual token management
- Consider the experimental `__experimental_resourceCache` from `@clerk/expo/resource-cache` for [offline support](https://clerk.com/docs/guides/development/offline-support.md)
- Use specific hooks (`useUser()`, `useAuth()`) instead of `useClerk()` to minimize unnecessary re-renders
- Call `SplashScreen.preventAutoHideAsync()` from `expo-splash-screen` to prevent a flash of the wrong content during auth initialization

### Code organization

- Separate auth and protected routes into distinct route groups (`(auth)` and `(home)`)
- Keep auth configuration (ClerkProvider) in the root layout only
- Place non-route files (components, hooks, utilities) outside the `app/` directory. Expo Router treats every file in `app/` as a route
- Use environment-specific configuration with `eas.json` profiles for development, staging, and production builds
- The `signIn` and `signUp` Future objects from `useSignIn()` and `useSignUp()` have unstable identity (they create a new reference on each flow state change). Prefer event-handler patterns over `useEffect`. When `useEffect` is necessary, include them in the dependency array and guard execution with a `useRef(false)` flag to prevent re-runs. See the [OAuth custom flow example](https://clerk.com/docs/guides/development/custom-flows/authentication/oauth-connections.md) for this pattern

### Testing strategies

- Use `pk_test_` keys for all development and testing
- Test on physical devices for [biometric authentication](https://clerk.com/glossary.md#biometric-authentication), native OAuth flows, and passkeys
- Verify all auth state transitions: sign-in, sign-out, token refresh, MFA verification, and session expiry
- Test deep linking to protected routes while unauthenticated to confirm redirect behavior
- Test passkeys on physical devices only (iOS 16+, Android 9+). Passkeys do not work on Android emulators or in Expo Go

### User experience

- Show loading states during auth initialization and all authentication transitions
- Provide clear error messages for failed authentication attempts using the structured `errors.fields` object from Core 3 hooks
- Consider `useLocalCredentials()` for biometric login after initial password authentication (Beta, password-based sign-in only)
- Handle expired sessions and network failures gracefully. Import `isClerkRuntimeError` from `@clerk/expo` and use `isClerkRuntimeError(err) && err.code === 'network_error'` to detect network errors specifically

## Comparison: Clerk vs. other Expo authentication solutions

| Feature                          |                   Clerk                   |                 Firebase Auth                |     Supabase Auth     |                Auth0                |
| -------------------------------- | :---------------------------------------: | :------------------------------------------: | :-------------------: | :---------------------------------: |
| Works in Expo Go                 |                  JS: Yes                  |                  JS SDK: Yes                 |        Limited        |                  No                 |
| Prebuilt RN UI                   |              AuthView (Beta)              |                      No                      |           No          |            Browser-based            |
| MFA                              |       TOTP, SMS, backup codes (Pro)       | Typically requires Identity Platform upgrade | TOTP free; phone paid | Pro MFA (Essentials+); none on free |
| Social login providers           |                    30+                    |                     \~10                     |          19+          |              Unlimited              |
| Token storage                    |        expo-secure-store (1 import)       |                    Manual                    |      AsyncStorage     |          credentialsManager         |
| Native OAuth                     |            Google + Apple hooks           |             Via native SDKs only             |     Browser-based     |            Browser-based            |
| Passkeys (RN)                    |             Native + JS hooks             |                      No                      |           No          |                  No                 |
| Free tier (as of 2026)           |                  50K MRU                  |              Usage-based limits              |  50K MAU (auto-pause) |               25K MAU               |
| Paid starting price (as of 2026) | $20/month billed annually (50K MRU incl.) |          Per-MAU (Identity Platform)         |       $25/month       |         $35/month (500 MAU)         |

Clerk is the only provider in this comparison that offers prebuilt native UI components for React Native, native passkey support in Expo, and dedicated OAuth hooks for Google and Apple sign-in. Its token management requires a single import (`@clerk/expo/token-cache`), compared to manual secure storage setup with other providers. Competitor pricing and feature details change frequently; check each provider's current pricing page for the latest information: [Firebase Pricing](https://firebase.google.com/pricing), [Supabase Pricing](https://supabase.com/pricing), [Auth0 Pricing](https://auth0.com/pricing).

## Conclusion

You have successfully expanded your React Native Expo application with advanced authentication features. You integrated user profiles, implemented robust multi-factor authentication with TOTP and SMS, and added seamless native social login experiences using Google and Apple. With these additions, your app provides a secure and modern login experience.

## FAQ

**Can I use multi-factor authentication (MFA) with Clerk in Expo?**
Yes, Clerk supports MFA in Expo applications. You can use the `signIn.mfa.*` methods to handle secondary verification steps, such as TOTP codes or SMS passcodes, directly within your React Native application.

**How does native social login work in Expo compared to the browser?**
Native social logins use the device's built-in authentication systems (like Apple's native sign-in sheet). Clerk's `useSignInWithApple` and `useSignInWithGoogle` hooks manage these native flows securely, offering a smoother user experience than browser-based redirects.

**Can I customize the built-in Clerk components?**
Yes, components like `AuthView` and `UserButton` accept an `appearance` prop. This allows you to customize colors, fonts, and layout to match your application's design system without having to build the components from scratch.

## In this series

1. [How to Set Up Clerk Authentication with Expo Router: Protected Routes, MFA, and Social Login](https://clerk.com/articles/how-to-set-up-clerk-authentication-with-expo-router-protected-routes-mfa-and-soc.md)
2. **How to Set Up Clerk Authentication with Expo Router: Protected Routes, MFA, and Social Login - Part 2** (you are here)
