# Expo Google Sign-In Without a WebView

Google Sign-In in [Expo](https://clerk.com/glossary/expo.md) apps has traditionally meant browser redirects, custom URL schemes, and a fragile chain of callbacks. Clerk's native Google Sign-In changes that. On Android, it uses Credential Manager — no browser at all. On iOS, configuring the `EXPO_PUBLIC_CLERK_GOOGLE_IOS_URL_SCHEME` environment variable enables `ASAuthorization`, Apple's native credential picker, instead of the default system browser sheet. The user taps one button, picks their Google account from a system-level sheet, and they're signed in.

This guide walks through the complete setup: Google Cloud credentials, Clerk Dashboard configuration, and a working Expo app with native Google Sign-In, email+OTP [authentication](https://clerk.com/glossary/authentication.md), user profile management, and sign-out. Every code example targets `@clerk/expo` Core 3 and the current stable Expo SDK.

## What Is Native Google Sign-In and Why It Matters for Expo Apps

### Browser-Based OAuth: The Standard Approach and Its Problems in Expo

The standard [OAuth](https://clerk.com/glossary/oauth.md) flow in Expo uses `expo-auth-session` to open a system browser (ASWebAuthenticationSession on iOS, Chrome Custom Tabs on Android). The user authenticates in that browser and gets redirected back to the app via a deep link.

This works, but the failure modes are real:

- **Redirect handling breaks.** Different callback URIs for development, preview, and production. One mismatch and the user lands nowhere.
- **Android dismiss race conditions.** Developers have reported Android redirect reliability issues where the browser dismisses before the callback completes ([expo/expo#23781](https://github.com/expo/expo/issues/23781)).
- **SDK upgrades break auth.** Expo SDK 53 introduced regressions in Google login flows that affected existing `expo-auth-session` implementations ([expo/expo#38666](https://github.com/expo/expo/issues/38666)).
- **The `auth.expo.io` proxy is gone.** The Google provider that relied on it has been deprecated since SDK 49 ([expo/expo#21084](https://github.com/expo/expo/issues/21084)).

Google blocked OAuth from embedded WebViews on September 30, 2021, returning `disallowed_useragent` errors ([Google Developers Blog, Jun 2021](https://developers.googleblog.com/2021/06/upcoming-security-changes-to-googles-oauth-2.0-authorization-endpoint.html)). Google continued enforcing this policy through 2023: remaining apps using embedded WebViews saw warnings starting in February 2023, with final blocking on July 24, 2023 ([Google Support FAQ](https://support.google.com/faqs/answer/12284343)). The system browser approach (`expo-auth-session`) was never blocked, but it still opens a browser. Native sign-in avoids a browser entirely.

Three tiers of Google authentication exist in mobile apps:

1. **Embedded WebView** (blocked by Google since 2021)
2. **System browser** via ASWebAuthenticationSession/Chrome Custom Tabs (what `expo-auth-session` does)
3. **Native credential picker** via ASAuthorization/Credential Manager (what Clerk's native flow does)

This article covers tier 3: no browser at all.

### What Native Google Sign-In Actually Is

#### ASAuthorization on iOS

Apple's `ASAuthorization` framework presents a system-level credential picker, the same UI used for [passkeys](https://clerk.com/glossary/passkeys.md) and Sign in with Apple. When Clerk's `@clerk/expo` config plugin is configured with an iOS URL scheme (`EXPO_PUBLIC_CLERK_GOOGLE_IOS_URL_SCHEME`), `useSignInWithGoogle()` uses `ASAuthorization` to present the native Google account picker. No browser opens.

This configuration step is optional. Without it, iOS falls back to `ASWebAuthenticationSession`, which opens a system browser sheet. The difference is a single environment variable.

#### Credential Manager on Android

[Credential Manager](https://developer.android.com/identity/sign-in/credential-manager) is Google's Jetpack library (`androidx.credentials`) that surfaces a system bottom sheet with the user's Google accounts. No browser opens. An ID token is produced directly by the OS.

Credential Manager replaces 5 deprecated APIs: the legacy Google Sign-In SDK (`play-services-auth`), Smart Lock for Passwords, One Tap sign-in, the Sign in with Google button, and FIDO2 local credentials. SDK removal is scheduled for May 2026; API calls will fail as early as July 2028 ([Android Developers Blog, Sep 2024](https://android-developers.googleblog.com/2024/09/streamlining-android-authentication-credential-manager-replaces-legacy-apis.html)).

> **Platform behavior summary:**
>
> - Android: Credential Manager. No browser at all.
> - iOS with native config: `ASAuthorization`. System credential picker, no browser.
> - iOS without native config: `ASWebAuthenticationSession`. System browser sheet (fallback).
>
> Configure `EXPO_PUBLIC_CLERK_GOOGLE_IOS_URL_SCHEME` in your `app.config.ts` to get the native credential picker on iOS.

### Why Native Sign-In Is Better Than Browser-Based OAuth

**User experience.** No context switch. No redirect failures. No browser tab left open. The conversion numbers back this up:

- Pinterest saw a **126% sign-up increase on Android** after adopting Google One Tap ([Google Case Study](https://developers.google.com/identity/sign-in/case-studies/pinterest)).
- Reddit reported a **185% overall conversion increase** combining Sign in with Google and One Tap ([Google Case Study](https://developers.google.com/identity/sign-in/case-studies/reddit)).
- Zoho achieved **6x faster logins** after migrating to Credential Manager, with 31% month-over-month passkey adoption growth ([Android Developers Blog, May 2025](https://android-developers.googleblog.com/2025/05/zoho-achieves-faster-logins-passkey-credential-manager-integration.html)).

**Security.** The native flow runs in a sandboxed system process that the app can't intercept. No redirect URI to spoof. No [PKCE](https://clerk.com/glossary/code-exchange-pkce.md) complexity exposed to the developer. [RFC 8252](https://datatracker.ietf.org/doc/html/rfc8252) (IETF BCP 212) states that native apps "MUST NOT use embedded user-agents" for OAuth. The [OAuth 2.1 draft](https://datatracker.ietf.org/doc/draft-ietf-oauth-v2-1/) (March 2026) makes PKCE mandatory for all clients.

**Reliability.** No `auth.expo.io` proxy dependency. No dismiss race conditions on Android. No production-vs-development differences in redirect handling.

## Prerequisites and Requirements

> Native Google Sign-In requires a development build. It won't work in Expo Go. Use `npx expo run:ios`, `npx expo run:android`, or `eas build --profile development` instead.

### Tools and Accounts You Need

- Node.js 20.9.0+
- Expo CLI (`npx expo`)
- EAS CLI (`npm install -g eas-cli`) and an Expo account
- A [Clerk account](https://dashboard.clerk.com)
- A [Google Cloud Console account](https://console.cloud.google.com/)
- iOS: Xcode 16+, [Apple Developer account](https://developer.apple.com) (for device testing)
- Android: Android Studio, physical device or emulator with Google Play Services

### Environment Variable Checklist

Your `.env` file needs these values (collected during the setup steps below):

```bash
EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_...
EXPO_PUBLIC_CLERK_GOOGLE_WEB_CLIENT_ID=...
EXPO_PUBLIC_CLERK_GOOGLE_IOS_CLIENT_ID=...
EXPO_PUBLIC_CLERK_GOOGLE_IOS_URL_SCHEME=com.googleusercontent.apps...
EXPO_PUBLIC_CLERK_GOOGLE_ANDROID_CLIENT_ID=...
```

### Compatibility: Clerk Core 3, @clerk/expo, and Expo SDK

- All code uses Core 3 import paths: `@clerk/expo`, `@clerk/expo/google`, `@clerk/expo/native`
- Native Google Sign-In requires `@clerk/expo` 3.1+
- Native components (`AuthView`, `UserButton`, `UserProfileView`) require Expo SDK 53+
- React 18 or 19

> Native components (`AuthView`, `UserButton`, `UserProfileView`) are currently in beta. See the [Native Components Overview](https://clerk.com/docs/reference/expo/native-components/overview.md) for the latest status.

### Development Build vs. Expo Go

Native Google Sign-In requires a development build because Expo Go ships a fixed native layer that can't load custom TurboModules like `NativeClerkGoogleSignIn`.

| Feature                 | Expo Go | Dev Build |
| ----------------------- | :-----: | :-------: |
| Email/password sign-in  |   Yes   |    Yes    |
| Google Sign-In (native) |    No   |    Yes    |
| `<AuthView />`          |    No   |    Yes    |
| `<UserButton />`        |    No   |    Yes    |
| Apple Sign-In (native)  |    No   |    Yes    |

To create a development build:

```bash
npx expo install expo-dev-client
npx expo run:ios
```

Or use EAS Build:

```bash
eas build --profile development --platform ios
```

## How to Add Google Sign-In to an Expo App Without a Browser Redirect

Three main approaches exist. Here's how they compare:

| Approach                      | Browser Required | Native Google UX | [Session Management](https://clerk.com/glossary/session-management.md) | Operational Burden                                 |
| ----------------------------- | :--------------: | :--------------: | :--------------------------------------------------------------------: | -------------------------------------------------- |
| `expo-auth-session`           |        Yes       |        No        |                                 Manual                                 | Redirect URIs, browser callbacks, token exchange   |
| `@react-native-google-signin` |        No        |      Partial     |                                 Manual                                 | Native Google setup plus separate session handling |
| **Clerk**                     |        No        |        Yes       |                                   Yes                                  | Dashboard setup plus native app registration       |

> On iOS, Clerk's native path requires the URL scheme config. Without it, iOS falls back to a browser sheet. The "No" for Browser Required applies when native config is complete.

### Option 1: Manual OAuth with expo-auth-session

The browser-based approach. Opens a system browser, handles the OAuth redirect, and returns a token. You manage [session](https://clerk.com/glossary/session.md) creation, token storage, and refresh yourself. Every Expo SDK upgrade risks breaking the redirect chain.

### Option 2: @react-native-google-signin/google-signin (DIY)

A React Native library that wraps Google's native SDKs. Gives you the native Google UI, but you still own session management, user state, and sign-out logic. The Credential Manager integration is gated behind a [paid tier](https://react-native-google-signin.github.io/) ($89–249/year).

### Option 3: Clerk Native Google Sign-In (Recommended)

Native Google Sign-In is built into `@clerk/expo`. On Android it uses Credential Manager. On iOS, the native path uses `ASAuthorization` when configured. Clerk handles the token exchange, [session](https://clerk.com/glossary/session.md) creation, and signed-in state after the provider returns.

#### Why Clerk Is the Right Choice for Expo Authentication

- **Fewest moving parts.** Dashboard config, environment variables, one hook or component. That's it.
- **Pre-built native UI.** `<AuthView />` renders SwiftUI on iOS and Jetpack Compose on Android. Google, Apple, email, phone, passkeys, and [MFA](https://clerk.com/glossary/multi-factor-authentication-mfa.md) are handled automatically.
- **Uses Google's current recommended APIs.** Credential Manager (not the deprecated legacy SDK).
- **Built-in session management.** User profiles, sign-out, and token refresh come included.
- **Automatic transfer flow.** If someone signs in with Google but doesn't have an account, one is created. If they sign up but already have an account, they're signed in. No separate screens needed.
- **Minimal dependency surface.** `@clerk/expo` with its peer dependencies (`expo-secure-store`, `expo-auth-session`, `expo-web-browser`) plus `expo-crypto` for the hook approach. AuthView doesn't need `expo-crypto`.

Clerk's native flow still goes through Clerk's backend for token verification and session creation. For how this works under the hood, see [How Clerk Works](https://clerk.com/docs/guides/how-clerk-works/overview.md).

## Setting Up Clerk for Native Google Sign-In

### Step 1: Create a Clerk Application and Enable Native API

1. Go to the [**Clerk Dashboard**](https://dashboard.clerk.com) and create a new application (or select an existing one).
2. Navigate to the [**Native Applications**](https://dashboard.clerk.com/~/native-applications) page and confirm that **Native API** is enabled.
3. Copy your [**Publishable Key**](https://dashboard.clerk.com/~/api-keys) from the **API Keys** page.

Native components and native sign-in hooks depend on Native API being enabled. Skip this and every native call silently fails.

### Step 2: Enable Google and Register Native Applications

1. In the Clerk Dashboard, go to [**Social Connections**](https://dashboard.clerk.com/~/user-authentication/sso-connections) > **Google** > **Use custom credentials**.
2. You'll configure the Client IDs here after creating them in Google Cloud Console (next step).
3. On the [**Native Applications**](https://dashboard.clerk.com/~/native-applications) page:
   - **iOS:** Add your Team ID and Bundle ID (must match `ios.bundleIdentifier` in `app.config.ts`)
   - **Android:** Add your package name (must match `android.package` in `app.config.ts`) and SHA-256 certificate fingerprint

> Different builds use different signing identities. A development build, an EAS-managed build, and a Google Play App Signing key each have different certificate fingerprints. Register the SHA-256 for each in the Clerk Dashboard.

### Step 3: Create a Google Cloud Project and OAuth Credentials

#### OAuth Consent Screen

Before creating client IDs, Google Cloud asks you to configure an OAuth consent screen.

Common blockers at this step:

- The app is still in **testing mode** (only test users can authenticate)
- Your Google account isn't listed as a **test user**
- You haven't completed **production publishing** for broader access

Set the consent screen to "External" and add your own email as a test user. You can publish to production later.

#### iOS Client ID

1. Go to **Google Cloud Console** > **APIs & Services** > **Credentials** > **Create OAuth Client ID**
2. Application type: **iOS**
3. Bundle ID: must match your `app.config.ts` `ios.bundleIdentifier` exactly
4. Save and note:
   - The **iOS Client ID** (goes into `EXPO_PUBLIC_CLERK_GOOGLE_IOS_CLIENT_ID`)
   - The **reversed client ID** (goes into `EXPO_PUBLIC_CLERK_GOOGLE_IOS_URL_SCHEME` for the native callback path)

#### Android Client ID and SHA-1 Fingerprint

1. Application type: **Android**
2. Package name: must match your `app.config.ts` `android.package`
3. SHA-1 fingerprint: get it from your signing keystore

Three different SHA-1 values exist depending on how you build:

```bash
# Debug keystore (local development)
keytool -list -v -keystore ~/.android/debug.keystore -alias androiddebugkey -storepass android
```

```bash
# EAS managed keystore
eas credentials --platform android
```

For production, find the App Signing key SHA-1 in **Google Play Console** > **Release** > **Setup** > **App Integrity**.

4. Create a **Web Application** OAuth Client ID too. Clerk uses it server-side for token verification. Add the Authorized Redirect URI from the Clerk Dashboard to this web client.

> Google Cloud Console requires **SHA-1** for the Android OAuth Client ID. The Clerk Dashboard Native Applications page requires **SHA-256**. One `keytool -list -v` command outputs both values — copy each to the correct place.

#### Configuration Validation Checklist

Before your first build, confirm:

1. Bundle ID matches in Google Cloud Console, Clerk Native Applications, and `app.config.ts`
2. Android package name matches in Google Cloud Console, Clerk Native Applications, and `app.config.ts`
3. Web, iOS, and Android Client IDs are in the correct environment variables
4. SHA-1 is registered in Google Cloud Console for each Android signing identity
5. SHA-256 is registered in the Clerk Dashboard Native Applications page for each Android signing identity
6. iOS reversed client ID is used as `EXPO_PUBLIC_CLERK_GOOGLE_IOS_URL_SCHEME`
7. Native API is enabled on the Clerk Dashboard Native Applications page

## Building the Complete Expo App

### Project Initialization

```bash
npx create-expo-app expo-clerk-google-signin
cd expo-clerk-google-signin
```

### Installing Dependencies

**For the hook approach** (custom UI, used in the complete app below):

```bash
npx expo install @clerk/expo expo-secure-store expo-crypto expo-auth-session expo-web-browser expo-dev-client
```

**For the AuthView approach** (pre-built native UI):

```bash
npx expo install @clerk/expo expo-secure-store expo-auth-session expo-web-browser expo-dev-client
```

AuthView doesn't need `expo-crypto` or `useSignInWithGoogle`. It handles everything internally. The `expo-auth-session` and `expo-web-browser` packages are peer dependencies of `@clerk/expo` and are required even when using native components.

### Configuring app.config.ts

```ts
import { ExpoConfig } from 'expo/config'

const config: ExpoConfig = {
  name: 'expo-clerk-google-signin',
  slug: 'expo-clerk-google-signin',
  version: '1.0.0',
  scheme: 'expo-clerk-google-signin',
  ios: {
    bundleIdentifier: 'com.yourcompany.expoclerkgooglesignin',
    supportsTablet: true,
  },
  android: {
    package: 'com.yourcompany.expoclerkgooglesignin',
    adaptiveIcon: {
      foregroundImage: './assets/adaptive-icon.png',
      backgroundColor: '#ffffff',
    },
  },
  plugins: ['@clerk/expo'],
  extra: {
    EXPO_PUBLIC_CLERK_GOOGLE_IOS_URL_SCHEME: process.env.EXPO_PUBLIC_CLERK_GOOGLE_IOS_URL_SCHEME,
  },
}

export default config
```

The `@clerk/expo` config plugin auto-injects the clerk-ios (Swift) and clerk-android (Kotlin) native SDKs, sets the iOS deployment target to 17.0, and configures the iOS URL scheme for the native Google callback.

### Setting Up ClerkProvider in Your App Entry Point

```tsx
// app/_layout.tsx
import { ClerkProvider } from '@clerk/expo'
import { tokenCache } from '@clerk/expo/token-cache'
import { Slot } from 'expo-router'

const publishableKey = process.env.EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY!

if (!publishableKey) {
  throw new Error('Add EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY to your .env file')
}

export default function RootLayout() {
  return (
    <ClerkProvider publishableKey={publishableKey} tokenCache={tokenCache}>
      <Slot />
    </ClerkProvider>
  )
}
```

The `tokenCache` uses `expo-secure-store` under the hood, which encrypts session tokens before storing them on the device. Without it, Clerk stores the active session token in memory only and it won't persist across app restarts.

The `publishableKey` must be passed explicitly because environment variables aren't automatically inlined in React Native production builds the way they are on web.

## Using the Native AuthView Component for Google Sign-In

`<AuthView />` is the fastest way to add Google Sign-In. It renders SwiftUI on iOS and Jetpack Compose on Android, with every auth method enabled in your Clerk Dashboard available automatically. Zero auth code required.

> `<AuthView />` requires a development build. It won't render in Expo Go.

### When to Use AuthView vs. the Hook

|                                       | AuthView                      | `useSignInWithGoogle` Hook |
| ------------------------------------- | ----------------------------- | -------------------------- |
| Lines of code                         | \~10                          | \~50                       |
| Custom UI                             | No (SwiftUI/Compose rendered) | Full control               |
| Extra dependencies beyond @clerk/expo | None                          | `expo-crypto`              |
| Google + Apple + email                | Automatic                     | Build each manually        |
| MFA support                           | Automatic                     | Manual                     |
| Beta status                           | Yes                           | Not labeled beta           |
| Web support                           | No (use `@clerk/expo/web`)    | No (use `@clerk/expo/web`) |

### Basic AuthView Setup

```tsx
// app/(auth)/sign-in.tsx
import { AuthView } from '@clerk/expo/native'
import { useAuth } from '@clerk/expo'
import { useRouter } from 'expo-router'
import { useEffect } from 'react'
import { View, StyleSheet } from 'react-native'

export default function SignInScreen() {
  const { isSignedIn } = useAuth()
  const router = useRouter()

  useEffect(() => {
    if (isSignedIn) {
      router.replace('/(home)')
    }
  }, [isSignedIn])

  return (
    <View style={styles.container}>
      <AuthView mode="signInOrUp" />
    </View>
  )
}

const styles = StyleSheet.create({
  container: { flex: 1 },
})
```

`AuthView` fills its parent container. Style the parent `View` to control size and position.

### Customizing the AuthView Appearance

Three modes are available:

- `signIn`: sign-in flows only
- `signUp`: sign-up flows only
- `signInOrUp`: auto-determines based on whether an account exists (default)

The `isDismissible` prop adds a dismiss button (defaults to `false`). Don't use `isDismissible` with React Native `<Modal>` as they conflict.

Which [social login](https://clerk.com/glossary/social-login.md) providers appear is controlled entirely by your Clerk Dashboard configuration. Enable Google, Apple, or any other provider there, and AuthView picks it up automatically.

## Implementing Google Sign-In with the useSignInWithGoogle Hook

For full control over the UI, use the `useSignInWithGoogle` hook. This is the approach the complete app example uses.

### The Sign-In Screen Component

```tsx
// components/GoogleSignInButton.tsx
import { useSignInWithGoogle } from '@clerk/expo/google'
import { Alert, TouchableOpacity, Text, StyleSheet, Platform } from 'react-native'

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

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

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

      if (createdSessionId && setActive) {
        await setActive({ session: createdSessionId })
      }
    } catch (err: any) {
      // User cancelled: don't show an error toast
      if (err?.code === 'SIGN_IN_CANCELLED' || err?.code === '-5') return

      Alert.alert('Sign-in error', err?.message ?? 'Something went wrong')
    }
  }

  return (
    <TouchableOpacity style={styles.button} onPress={handlePress}>
      <Text style={styles.text}>Continue with Google</Text>
    </TouchableOpacity>
  )
}

const styles = StyleSheet.create({
  button: {
    backgroundColor: '#4285F4',
    paddingVertical: 14,
    borderRadius: 8,
    alignItems: 'center',
  },
  text: { color: '#fff', fontSize: 16, fontWeight: '600' },
})
```

Import `useSignInWithGoogle` from `@clerk/expo/google` (not from `@clerk/expo` directly).

### Handling Sign-In Success and Errors

`startGoogleAuthenticationFlow()` returns:

- `createdSessionId`: the session ID if authentication succeeded
- `setActive`: function to activate the session
- `signIn` / `signUp`: the underlying Clerk objects (rarely needed)

On success, call `setActive({ session: createdSessionId })`. Clerk's token cache persists the session so the user stays signed in across app restarts.

**Transfer flow:** if someone signs in with Google but doesn't have a Clerk account, one is created automatically. If they sign up but already have an account, Clerk signs them in. No separate sign-in/sign-up screens needed for the Google flow.

**[Account linking](https://clerk.com/glossary/account-linking.md):** if the user's Google email matches an existing Clerk account, accounts are linked automatically when both emails are verified.

### Triggering the Native Google Sign-In Flow

When the user taps the button:

- **Android:** A bottom sheet appears from Credential Manager showing the user's Google accounts. They tap one, and the flow completes. No browser opens.
- **iOS (with native config):** The `ASAuthorization` system credential picker appears. Same pattern: tap, done, no browser.
- **iOS (without native config):** Falls back to `ASWebAuthenticationSession`, which opens a system browser sheet.

The flow is managed entirely by the OS. Your app receives a session ID on success.

## Adding Email + OTP Authentication Alongside Google Sign-In

The complete app combines Google Sign-In with email [one-time passcode](https://clerk.com/glossary/one-time-passcodes-email-sms.md) authentication on the same screen, with a visual separator between them.

### Building the Combined Sign-Up Screen

```tsx
// app/(auth)/sign-up.tsx
import { useState } from 'react'
import { View, Text, TextInput, TouchableOpacity, Alert, StyleSheet } from 'react-native'
import { useSignUp } from '@clerk/expo'
import { Link } from 'expo-router'
import { GoogleSignInButton } from '../../components/GoogleSignInButton'

export default function SignUpScreen() {
  const { signUp } = useSignUp()
  const [email, setEmail] = useState('')
  const [pendingVerification, setPendingVerification] = useState(false)
  const [code, setCode] = useState('')

  const handleEmailSignUp = async () => {
    try {
      await signUp.create({ emailAddress: email })
      await signUp.verifications.sendEmailCode()
      setPendingVerification(true)
    } catch (err: any) {
      Alert.alert('Error', err?.message ?? 'Could not create account')
    }
  }

  const handleVerify = async () => {
    try {
      await signUp.verifications.verifyEmailCode({ code })

      if (signUp.status === 'complete') {
        await signUp.finalize()
      }
    } catch (err: any) {
      Alert.alert('Verification failed', err?.message ?? 'Invalid code')
    }
  }

  if (pendingVerification) {
    return (
      <View style={styles.container}>
        <Text style={styles.title}>Verify your email</Text>
        <Text style={styles.subtitle}>We sent a code to {email}</Text>
        <TextInput
          value={code}
          onChangeText={setCode}
          placeholder="Enter 6-digit code"
          keyboardType="number-pad"
          maxLength={6}
          style={styles.input}
        />
        <TouchableOpacity style={styles.primaryButton} onPress={handleVerify}>
          <Text style={styles.primaryButtonText}>Verify</Text>
        </TouchableOpacity>
      </View>
    )
  }

  return (
    <View style={styles.container}>
      <Text style={styles.title}>Create an account</Text>

      <GoogleSignInButton />

      <View style={styles.divider}>
        <View style={styles.dividerLine} />
        <Text style={styles.dividerText}>or</Text>
        <View style={styles.dividerLine} />
      </View>

      <TextInput
        value={email}
        onChangeText={setEmail}
        placeholder="Email address"
        autoCapitalize="none"
        keyboardType="email-address"
        style={styles.input}
      />

      <TouchableOpacity style={styles.primaryButton} onPress={handleEmailSignUp}>
        <Text style={styles.primaryButtonText}>Send code</Text>
      </TouchableOpacity>

      <Link href="/(auth)/sign-in" asChild>
        <TouchableOpacity style={styles.linkButton}>
          <Text style={styles.linkText}>Already have an account? Sign in</Text>
        </TouchableOpacity>
      </Link>
    </View>
  )
}

const styles = StyleSheet.create({
  container: { flex: 1, justifyContent: 'center', padding: 24 },
  title: { fontSize: 28, fontWeight: 'bold', marginBottom: 24 },
  subtitle: { fontSize: 14, color: '#666', marginBottom: 16 },
  input: {
    borderWidth: 1,
    borderColor: '#ddd',
    padding: 14,
    borderRadius: 8,
    fontSize: 16,
    marginBottom: 16,
  },
  primaryButton: {
    backgroundColor: '#000',
    paddingVertical: 14,
    borderRadius: 8,
    alignItems: 'center',
    marginBottom: 12,
  },
  primaryButtonText: { color: '#fff', fontSize: 16, fontWeight: '600' },
  divider: {
    flexDirection: 'row',
    alignItems: 'center',
    marginVertical: 20,
  },
  dividerLine: { flex: 1, height: 1, backgroundColor: '#ddd' },
  dividerText: { marginHorizontal: 12, color: '#999', fontSize: 14 },
  linkButton: { alignItems: 'center', marginTop: 8 },
  linkText: { color: '#666', fontSize: 14 },
})
```

### Building the Combined Sign-In Screen

```tsx
// app/(auth)/sign-in.tsx
import { useState } from 'react'
import { View, Text, TextInput, TouchableOpacity, Alert, StyleSheet } from 'react-native'
import { useSignIn } from '@clerk/expo'
import { Link } from 'expo-router'
import { GoogleSignInButton } from '../../components/GoogleSignInButton'

export default function SignInScreen() {
  const { signIn } = useSignIn()
  const [email, setEmail] = useState('')
  const [pendingVerification, setPendingVerification] = useState(false)
  const [code, setCode] = useState('')

  const handleEmailSignIn = async () => {
    try {
      await signIn.emailCode.sendCode({ emailAddress: email })
      setPendingVerification(true)
    } catch (err: any) {
      Alert.alert('Error', err?.message ?? 'Could not send code')
    }
  }

  const handleVerify = async () => {
    try {
      await signIn.emailCode.verifyCode({ code })

      if (signIn.status === 'complete') {
        await signIn.finalize()
      } else if (signIn.status === 'needs_second_factor') {
        // Handle MFA if enabled. See:
        // /docs/guides/development/custom-flows/authentication/email-sms-otp
        Alert.alert('MFA required', 'Complete second factor authentication')
      }
    } catch (err: any) {
      Alert.alert('Verification failed', err?.message ?? 'Invalid code')
    }
  }

  if (pendingVerification) {
    return (
      <View style={styles.container}>
        <Text style={styles.title}>Check your email</Text>
        <Text style={styles.subtitle}>We sent a code to {email}</Text>
        <TextInput
          value={code}
          onChangeText={setCode}
          placeholder="Enter 6-digit code"
          keyboardType="number-pad"
          maxLength={6}
          style={styles.input}
        />
        <TouchableOpacity style={styles.primaryButton} onPress={handleVerify}>
          <Text style={styles.primaryButtonText}>Verify</Text>
        </TouchableOpacity>
      </View>
    )
  }

  return (
    <View style={styles.container}>
      <Text style={styles.title}>Sign in</Text>

      <GoogleSignInButton />

      <View style={styles.divider}>
        <View style={styles.dividerLine} />
        <Text style={styles.dividerText}>or</Text>
        <View style={styles.dividerLine} />
      </View>

      <TextInput
        value={email}
        onChangeText={setEmail}
        placeholder="Email address"
        autoCapitalize="none"
        keyboardType="email-address"
        style={styles.input}
      />

      <TouchableOpacity style={styles.primaryButton} onPress={handleEmailSignIn}>
        <Text style={styles.primaryButtonText}>Send code</Text>
      </TouchableOpacity>

      <Link href="/(auth)/sign-up" asChild>
        <TouchableOpacity style={styles.linkButton}>
          <Text style={styles.linkText}>Don't have an account? Sign up</Text>
        </TouchableOpacity>
      </Link>
    </View>
  )
}

const styles = StyleSheet.create({
  container: { flex: 1, justifyContent: 'center', padding: 24 },
  title: { fontSize: 28, fontWeight: 'bold', marginBottom: 24 },
  subtitle: { fontSize: 14, color: '#666', marginBottom: 16 },
  input: {
    borderWidth: 1,
    borderColor: '#ddd',
    padding: 14,
    borderRadius: 8,
    fontSize: 16,
    marginBottom: 16,
  },
  primaryButton: {
    backgroundColor: '#000',
    paddingVertical: 14,
    borderRadius: 8,
    alignItems: 'center',
    marginBottom: 12,
  },
  primaryButtonText: { color: '#fff', fontSize: 16, fontWeight: '600' },
  divider: {
    flexDirection: 'row',
    alignItems: 'center',
    marginVertical: 20,
  },
  dividerLine: { flex: 1, height: 1, backgroundColor: '#ddd' },
  dividerText: { marginHorizontal: 12, color: '#999', fontSize: 14 },
  linkButton: { alignItems: 'center', marginTop: 8 },
  linkText: { color: '#666', fontSize: 14 },
})
```

> The Google Sign-In button handles both sign-in and sign-up via Clerk's transfer flow. A user tapping "Continue with Google" on either screen gets the right outcome automatically.

### Verifying the OTP Code

Both screens use inline verification. After `signIn.emailCode.verifyCode()` or `signUp.verifications.verifyEmailCode()` succeeds, call `finalize()` to activate the session. The `<Show>` components in the layouts detect the auth state change and redirect automatically.

For production, handle non-happy-path status values like `needs_second_factor` (MFA enabled) and `needs_client_trust`. See the [Email/SMS OTP Custom Flow](https://clerk.com/docs/guides/development/custom-flows/authentication/email-sms-otp.md) docs for the complete set of status codes.

## Managing Sessions, the User Profile, and Sign-Out

### Checking Authentication State

```tsx
// app/(auth)/_layout.tsx
import { Show } from '@clerk/expo'
import { Redirect, Slot } from 'expo-router'

export default function AuthLayout() {
  return (
    <Show when="signed-out" fallback={<Redirect href="/(home)" />}>
      <Slot />
    </Show>
  )
}
```

```tsx
// app/(home)/_layout.tsx
import { Show } from '@clerk/expo'
import { Redirect, Slot } from 'expo-router'

export default function HomeLayout() {
  return (
    <Show when="signed-in" fallback={<Redirect href="/(auth)/sign-in" />}>
      <Slot />
    </Show>
  )
}
```

The `<Show>` component from `@clerk/expo` replaces the older `<SignedIn>` / `<SignedOut>` components. Use `when="signed-in"` or `when="signed-out"` to conditionally render based on auth state.

### The Native UserButton and UserProfile Components

```tsx
// app/(home)/index.tsx
import { View, Text, StyleSheet } from 'react-native'
import { Show } from '@clerk/expo'
import { UserButton } from '@clerk/expo/native'
import { useUser } from '@clerk/expo'

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

  return (
    <Show when="signed-in">
      <View style={styles.container}>
        <View style={styles.header}>
          <Text style={styles.greeting}>Welcome, {user?.firstName ?? 'there'}</Text>
          <View style={styles.avatar}>
            <UserButton />
          </View>
        </View>
        <Text style={styles.email}>{user?.primaryEmailAddress?.emailAddress}</Text>
      </View>
    </Show>
  )
}

const styles = StyleSheet.create({
  container: { flex: 1, padding: 24, paddingTop: 80 },
  header: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center' },
  greeting: { fontSize: 24, fontWeight: 'bold' },
  avatar: { width: 44, height: 44, borderRadius: 22, overflow: 'hidden' },
  email: { fontSize: 14, color: '#666', marginTop: 8 },
})
```

`useAuth()` returns `isSignedIn`, `userId`, `sessionId`, and `getToken`. `useUser()` returns the full user object with `user.firstName`, `user.primaryEmailAddress`, `user.imageUrl`, and more.

`<UserButton />` from `@clerk/expo/native` renders the user's avatar. Tapping it opens a native profile modal powered by `<UserProfileView />`. Sign-out is handled automatically and synced with the JS SDK. The component takes no props; control size and shape through the parent `View`.

For more control, use the `useUserProfileModal()` hook:

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

const { presentUserProfile, isAvailable } = useUserProfileModal()

// Open the profile modal programmatically
if (isAvailable) {
  await presentUserProfile()
}
```

### Signing Out Correctly

```tsx
import { useClerk } from '@clerk/expo'
import { useRouter } from 'expo-router'

export function SignOutButton() {
  const { signOut } = useClerk()
  const router = useRouter()

  const handleSignOut = async () => {
    await signOut()
    router.replace('/(auth)/sign-in')
  }

  return (
    <TouchableOpacity onPress={handleSignOut}>
      <Text>Sign out</Text>
    </TouchableOpacity>
  )
}
```

`signOut()` clears the session and the token cache. If you're using `<UserButton />`, sign-out is built in and syncs automatically with the JS SDK.

## Error Handling Reference

### Android Error Code 10: SHA-1 Fingerprint Mismatch

The most common error. Surfaces as `DEVELOPER_ERROR with code 10`. The Google sign-in dialog appears but immediately fails.

**Root cause:** SHA-1 registered in Google Cloud Console doesn't match the keystore that signed the current build.

**Three different SHA-1 values to manage:**

1. **Debug keystore** (local `npx expo run:android`):

```bash
keytool -list -v -keystore ~/.android/debug.keystore -alias androiddebugkey -storepass android
```

2. **EAS managed keystore** (cloud builds):

```bash
eas credentials --platform android
```

3. **Google Play App Signing key** (production): found in Play Console \u003e **Release** \u003e **Setup** \u003e **App Integrity**.

Each needs its own Android OAuth Client ID in Google Cloud Console.

**Also check:** the `webClientId` environment variable must reference the **Web Application** type Client ID, not the Android one.

### iOS: "The operation could not be completed"

Usually a configuration mismatch:

- `EXPO_PUBLIC_CLERK_GOOGLE_IOS_CLIENT_ID` doesn't match the Google Cloud Console iOS Client ID
- `ios.bundleIdentifier` in `app.config.ts` doesn't match what's registered in Google Cloud Console
- `EXPO_PUBLIC_CLERK_GOOGLE_IOS_URL_SCHEME` isn't set (or isn't the reversed client ID format, e.g., `com.googleusercontent.apps.123456`)

### Expo Go Limitations with Native Sign-In

`useSignInWithGoogle()` and `<AuthView />` won't work in Expo Go. The TurboModule `NativeClerkGoogleSignIn` isn't available.

Use a development build (`npx expo run:ios`) or EAS Build (`eas build --profile development`). JS-only email flows via `useSignIn`/`useSignUp` work in Expo Go for testing other parts of the app.

### Deep Link and Bundle Identifier Issues

For Clerk's native Google flow, the main iOS pitfall is the Google callback URL scheme and native app identifiers, not a custom Expo redirect URI.

Common mistakes:

- Bundle ID or package name in `app.config.ts` doesn't match Google Cloud Console and Clerk Dashboard entries
- The iOS URL scheme doesn't match the reversed client ID
- Forgetting to register Native Applications in the Clerk Dashboard (Team ID + Bundle ID for iOS, package name + SHA-256 for Android)

## Platform-Specific Configuration

### iOS: Info.plist and URL Schemes

The `@clerk/expo` config plugin handles iOS configuration automatically:

- Injects the iOS URL scheme from `EXPO_PUBLIC_CLERK_GOOGLE_IOS_URL_SCHEME`
- Sets the deployment target to iOS 17.0
- Adds the clerk-ios SPM package
- Includes the Apple Privacy Manifest (required since May 1, 2024)

No manual `Info.plist` editing required.

### Android: Credential Manager

Key differences from Firebase/Supabase approaches:

- **No `google-services.json` required.** Clerk doesn't use Firebase for authentication.
- SHA-1 is required in Google Cloud Console for the Android OAuth Client ID. SHA-256 is required in the Clerk Dashboard's Native Applications page. Both come from `keytool -list -v`.
- Credential Manager requires Google Play Services. Your emulator must include the Google Play Store image.
- Supports Android 4.4+ for passwords, Android 9+ for passkeys.

## EAS Build Configuration for Native Google Sign-In

```json
{
  "cli": {
    "version": ">= 14.0.0"
  },
  "build": {
    "development": {
      "developmentClient": true,
      "distribution": "internal",
      "env": {
        "EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY": "pk_test_..."
      }
    },
    "preview": {
      "distribution": "internal"
    },
    "production": {
      "autoIncrement": true
    }
  }
}
```

### Development Builds

```bash
eas build --profile development --platform ios
```

Set `developmentClient: true` and `distribution: "internal"`. Environment variables can be set per profile or in the EAS Dashboard.

### Preview and Production Builds

For production, the most common Google Sign-In failure is SHA-1 mismatch:

- Google Play App Signing uses an **app signing key** that's different from the **upload key**
- Both need their own Android OAuth Client IDs in Google Cloud Console

> Production Expo apps still need a domain on the Clerk production instance, even if there's no traditional web frontend. See the [Expo Deployment Guide](https://clerk.com/docs/guides/development/deployment/expo.md) for details.

## Migrating from Browser-Based Google OAuth

### From expo-auth-session

**Remove:** `useAuthRequest`, Google provider imports, redirect URI config, `makeRedirectUri`, `promptAsync`.

**Keep:** `expo-auth-session` and `expo-web-browser` (peer dependencies of `@clerk/expo`).

**Add:** `@clerk/expo`, `expo-secure-store`, `expo-dev-client`, and `expo-crypto` (hook approach only).

**Replace:** `useAuthRequest` and the entire OAuth flow with `useSignInWithGoogle` or `<AuthView />`. The native flow is one function call: `startGoogleAuthenticationFlow()`. No `discovery` object, no `makeRedirectUri`, no `promptAsync`.

### From @react-native-google-signin/google-signin

**Remove:** `@react-native-google-signin/google-signin`, `GoogleSignin.configure()`, `GoogleSignin.signIn()`, manual token extraction, `GoogleSignin.hasPlayServices()`.

**Remove (if only used for Google auth):** `google-services.json`, `GoogleService-Info.plist`, Firebase config. If you use Firebase for other features, keep these files.

**Add:** `@clerk/expo`, configure Clerk Dashboard with your existing Google Cloud credentials.

**Benefits of switching:**

- No separate Google Sign-In library needed
- No `google-services.json` or `GoogleService-Info.plist` config files (unless Firebase is needed for other features)
- Session management, user profiles, and sign-out are built in
- Credential Manager support included (the standalone library [gates this behind a paid tier](https://react-native-google-signin.github.io/))

> **Import path change from Core 2 to Core 3:** The package renamed from `@clerk/clerk-expo` to `@clerk/expo`. Hooks import from `@clerk/expo/google`, native components from `@clerk/expo/native`.

## Clerk vs. Other Expo Authentication Solutions

| If you want...                            | Best fit                      | Why                                                                     |
| ----------------------------------------- | ----------------------------- | ----------------------------------------------------------------------- |
| Browser-based OAuth that works in Expo Go | `expo-auth-session`           | No native build required, but you keep redirect complexity              |
| Full DIY native Google Sign-In            | `@react-native-google-signin` | Native provider control, but you still own session management           |
| Native Google Sign-In plus managed auth   | **Clerk**                     | Native provider flow plus Clerk-managed sessions, users, and profile UI |

Clerk's advantage isn't just the native Google UI. It's that the token exchange, session creation, session refresh, and user management happen automatically. The other approaches give you a Google ID token and leave the rest to you.

## Key Takeaways

- **Native Google Sign-In in Expo doesn't need a browser.** Clerk uses Credential Manager on Android (always native) and `ASAuthorization` on iOS (when `EXPO_PUBLIC_CLERK_GOOGLE_IOS_URL_SCHEME` is configured). Without the iOS URL scheme, iOS falls back to a system browser sheet.
- **Two approaches: `<AuthView />` for zero-code auth, `useSignInWithGoogle` for custom UI.** Both use the same native flow under the hood.
- **Certificate fingerprint management is the hardest part.** Debug, EAS, and production builds each have different fingerprints. Register SHA-1 in Google Cloud Console and SHA-256 in the Clerk Dashboard for each.
- **Expo Go can't run native sign-in.** Use development builds from the start.
- **Clerk handles the full auth lifecycle.** Sign-in, sign-up, transfer flow, session management, user profiles, and sign-out are included.

Get started: [Expo Quickstart](https://clerk.com/docs/expo/getting-started/quickstart.md) | [Sign in with Google Guide](https://clerk.com/docs/expo/guides/configure/auth-strategies/sign-in-with-google.md) | [clerk-expo-quickstart examples](https://github.com/clerk/clerk-expo-quickstart)

## Frequently Asked Questions

## FAQ

### Does Clerk's native Google Sign-In work in Expo Go?

No. Native Google Sign-In requires a development build because Expo Go ships a fixed native layer that can't load custom TurboModules like `NativeClerkGoogleSignIn`. Use `npx expo run:ios`, `npx expo run:android`, or `eas build --profile development` instead. JS-only email/password flows via `useSignIn`/`useSignUp` do work in Expo Go.

### Does this really avoid a browser on iOS?

Yes, but only when configured. Set `EXPO_PUBLIC_CLERK_GOOGLE_IOS_URL_SCHEME` to the reversed iOS Client ID (e.g. `com.googleusercontent.apps.YOUR_ID`) in your `app.config.ts` `extra` field. This tells the `@clerk/expo` config plugin to use `ASAuthorization` (Apple's native credential picker) instead of `ASWebAuthenticationSession` (system browser sheet). Without that variable, iOS falls back to the browser. Android always uses the native Credential Manager.

### Do I need a google-services.json file for Clerk?

No. Unlike Firebase-based approaches, Clerk doesn't require `google-services.json` (Android) or `GoogleService-Info.plist` (iOS). You still need Android SHA-1 fingerprints for Google Cloud Console and SHA-256 fingerprints for the Clerk Dashboard, plus iOS/Android Client IDs, but those go into environment variables and the respective dashboards, not into config files.

### Can I use AuthView and useSignInWithGoogle together?

Not on the same screen. `<AuthView />` renders its own complete sign-in/sign-up UI natively. `useSignInWithGoogle()` is for building custom UI. Pick one approach per screen. You can use AuthView for quick onboarding and the hook for a custom login page in the same app.

### What happens if a user signs in with Google but doesn't have an account?

Clerk automatically creates an account via the transfer flow. If a user taps "Continue with Google" and no Clerk account exists for that email, one is created and a session is started. If an account already exists, they're signed in. No separate sign-up handling needed for the Google flow.

### How do I test native Google Sign-In on an emulator?

Android: Use an emulator image that includes the Google Play Store (look for "Google Play" in the AVD name). Add the emulator's debug keystore SHA-1 to Google Cloud Console. iOS: Use a Simulator with a development build via `npx expo run:ios`. Physical devices work best for testing the full native flow.

### Is AuthView production-ready?

`<AuthView />` and other native components (`<UserButton />`, `<UserProfileView />`) are currently in beta. They work well for many use cases but Clerk hasn't removed the beta label yet. Check the [Native Components Overview](https://clerk.com/docs/reference/expo/native-components/overview.md) for the latest status before shipping to production.

### What's the difference between useSSO and useSignInWithGoogle?

`useSSO()` is a general-purpose hook for browser-based [SSO](https://clerk.com/glossary/single-sign-on-sso.md) flows (Google, GitHub, Apple, etc. via a system browser). `useSignInWithGoogle()` uses the native Google credential picker with no browser. For a native Google experience, use `useSignInWithGoogle`. For other OAuth providers that don't have native hooks, use `useSSO`.

### What version of @clerk/expo do I need for native Google Sign-In?

Native Google Sign-In and native components require `@clerk/expo` 3.1 or later (Core 3). The package was renamed from `@clerk/clerk-expo` (Core 2) to `@clerk/expo` (Core 3). See the [Core 3 Upgrade Guide](https://clerk.com/docs/guides/development/upgrading/upgrade-guides/core-3.md) for migration details.

### Do I need a domain for my Clerk production instance if I only have an Expo app?

Yes. Clerk production instances require a domain even for mobile-only apps. This is used for backend API communication and session management. See the [Expo Deployment Guide](https://clerk.com/docs/guides/development/deployment/expo.md) for production configuration.
