Skip to content

Usage ​

AppleAuth is the headless entry point of expo-apple-sign-in. It runs the Sign in with Apple flow on iOS, Android, and web through one set of calls and hands your code a credential with the identity token and the raw nonce, so you can render any interface and send the token to any backend. This page walks through the whole lifecycle in order: configure, check availability, sign in, read the credential, check its state, listen for revocation, and sign out. If you want a ready-made interface instead, see the Apple button and the useAppleAuth hook.

Configure ​

iOS signs in with the App ID of your app and needs no configuration. Android and web use a Services ID as clientId and an HTTPS redirectUri registered for that Services ID in the Apple Developer portal. Call configure once at startup, before the first sign-in.

lib/apple-auth.ts
ts
import { AppleAuth } from 'expo-apple-sign-in'

AppleAuth.configure({
  clientId: 'com.example.app.web',
  redirectUri: 'https://app.example.com/auth/apple',
  scopes: ['name', 'email'],
})

configure merges the values you pass into the stored configuration and skips undefined values, so you can call it again later to change a single field. scopes defaults to ['name', 'email'] when you never set it. AppleAuth.getConfig() returns a copy of the stored configuration, and AppleAuth.isConfigured() returns true on iOS and, on Android and web, only when both clientId and redirectUri are set. The Apple Developer, Android, and Web setup pages explain where each value comes from.

Web redirect URI

On web, redirectUri must have the same origin as the page that starts the sign-in. A different origin makes the call fail with ERR_NOT_CONFIGURED before Apple opens. See Troubleshooting.

Check availability ​

AppleAuth.isAvailable() returns a Promise<boolean>. On web it is true when a browser document exists. On iOS and Android it is true when the native module is linked, and false when the module is missing, for example in Expo Go, or when the native call throws.

lib/apple-available.ts
ts
import { AppleAuth } from 'expo-apple-sign-in'

export async function canShowAppleSignIn(): Promise<boolean> {
  const available = await AppleAuth.isAvailable()

  return available && AppleAuth.isConfigured()
}

Sign in ​

AppleAuth.signIn(options?) opens the Apple sheet on iOS, an in-app WebView on Android, and the Apple JS popup on web. It resolves with an AppleCredential and rejects with an AppleAuthError otherwise.

lib/sign-in.ts
ts
import { AppleAuth, generateNonce } from 'expo-apple-sign-in'
import type { TAppleCredential } from 'expo-apple-sign-in'

export async function signInWithApple(): Promise<TAppleCredential> {
  const nonce = generateNonce()

  return AppleAuth.signIn({
    nonce,
    state: 'checkout-flow',
    scopes: ['name', 'email'],
  })
}

All three options are optional.

OptionTypeDefaultDescription
noncestringgeneratedRaw, unhashed nonce. The library hashes it with SHA-256 before Apple sees it and returns the same raw value on credential.nonce. Never hash it yourself.
statestringgeneratedOpaque value that Apple echoes back. A random 16-byte hex value is generated when you omit it.
scopesAppleScope[]configured scopesOverrides the scopes from configure for this call. 'name' and 'fullName' request the same data.

When you omit nonce, the library generates one with generateNonce() (32 bytes from a secure random source, as 64 hex characters) and still returns it on the credential. Pass your own nonce when your server issues it and wants to check it later. The Android flow compares the returned state with the one it sent and fails with ERR_INVALID_RESPONSE when they differ.

On iOS and Android only one sign-in runs at a time. A call made while another one is pending rejects with ERR_REQUEST_FAILED, and its message says that a Sign in with Apple request is already in progress. On web, call signIn from a click or press handler. Browsers block popups that no user gesture opened, and a blocked Apple popup rejects with ERR_REQUEST_FAILED and a message that asks for a click handler.

What the credential contains ​

FieldTypeMeaning
identityTokenstringJWT signed by Apple. Send it to your backend or auth provider.
authorizationCodestring | nullSingle-use code that a server can exchange for tokens.
noncestringThe raw nonce that was hashed for Apple. Pass it unchanged to Supabase and Firebase.
statestring | nullThe state value Apple echoed back.
user.idstring | nullStable Apple user identifier.
user.emailstring | nullEmail address, possibly a private relay address.
user.givenNamestring | nullFirst name.
user.familyNamestring | nullLast name.
user.middleNamestring | nullMiddle name. iOS only.
user.namePrefixstring | nullName prefix, such as a title. iOS only.
user.nameSuffixstring | nullName suffix. iOS only.
user.nicknamestring | nullNickname. iOS only.
realUserStatus'unknown' | 'likelyReal' | 'unsupported'Apple's signal that the account belongs to a real person. Only iOS reports it. Android and web return 'unknown'.

On iOS, middleName, namePrefix, nameSuffix, and nickname come from the full name Apple returns on the first authorization, and each one is null when Apple leaves that part empty. Android and web always return null for them, because Apple's web flow sends only the first and last name. The library reads user.id and user.email from the identity token when the platform did not return them, so both are usually filled. That decoding does not verify the signature. Your backend still has to verify the token, as described in Backend verification.

Name and email arrive once

Apple returns the name and the email only the first time a person authorizes your app. Later sign-ins return null for givenName, familyName, and the other name fields, and the library fills user.email from the identity token. The name never appears inside the identity token. Store it the moment you receive it. To receive it again while testing, remove the app from your Apple Account (on iPhone: Settings, your name, Sign in with Apple, select the app, then Delete; on other platforms: account.apple.com, Sign-In & Security, Sign in with Apple), then sign in again.

lib/store-profile.ts
ts
import { AppleAuth } from 'expo-apple-sign-in'

export async function signInAndStoreName(): Promise<void> {
  const credential = await AppleAuth.signIn()
  const { givenName, familyName } = credential.user

  if (givenName || familyName) {
    await fetch('https://api.example.com/profile', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ appleUserId: credential.user.id, givenName, familyName }),
    })
  }
}

request and signIn ​

signIn and request run the same flow. They differ only in how they report the outcome.

signIn is built on request. It resolves with the credential on success. It throws AppleAuthError with the code ERR_REQUEST_CANCELED when the person dismisses the sheet, and it throws an AppleAuthError that carries the original code and message when the flow fails. Choose it when you want the usual try/catch shape, and when you pass the call to an adapter.

request never rejects. It catches every error, including configuration errors, and resolves with a discriminated union: { type: 'success', data }, { type: 'cancelled' }, or { type: 'error', code, message, cause }. Choose it when a cancel is a normal outcome that you do not want to treat as an exception. cause holds the original error when the library wrapped one. The helpers isSuccessResponse, isCancelledResponse, and isErrorResponse narrow the union.

lib/request.ts
ts
import { AppleAuth, isCancelledResponse, isErrorResponse, isSuccessResponse } from 'expo-apple-sign-in'
import type { TAppleCredential } from 'expo-apple-sign-in'

export async function requestAppleCredential(): Promise<TAppleCredential | null> {
  const response = await AppleAuth.request()

  if (isSuccessResponse(response)) {
    return response.data
  }

  if (isCancelledResponse(response)) {
    return null
  }

  if (isErrorResponse(response)) {
    console.warn(`Apple sign-in failed with ${response.code}: ${response.message}`)
  }

  return null
}

Read the last credential ​

AppleAuth.getCurrentCredential() returns the credential from the most recent successful sign-in, or null. It lives in memory only, so it is null after an app restart and after signOut(). Persist what you need (your own session, the Apple user id) yourself.

Check the credential state ​

AppleAuth.getCredentialState(userId) returns one of 'authorized', 'revoked', 'notFound', 'transferred', or 'unknown'.

On iOS the call asks Apple about userId. Android and web have no such API, so the call always resolves 'unknown' there, and iOS does the same when the native module is missing. Run the check at iOS startup, with the Apple user id you stored, to find out whether the person revoked access.

The example treats 'revoked' and 'notFound' as a reason to sign the person out. Any other state, including 'unknown', keeps the session, so the same code is safe to run on Android and web.

lib/credential-state.ts
ts
import { AppleAuth } from 'expo-apple-sign-in'

export async function needsAppleSignInAgain(appleUserId: string): Promise<boolean> {
  const state = await AppleAuth.getCredentialState(appleUserId)

  return state === 'revoked' || state === 'notFound'
}

Listen for revocation ​

On iOS, Apple notifies a running app when the person stops using Sign in with Apple with it, for example from the Apple Account settings. AppleAuth.addRevokeListener(listener) calls listener when that notification arrives and returns a subscription with a remove() method. Call remove() when you no longer need the listener. On Android and web the listener never fires, and remove() does nothing.

components/AppleRevokeWatcher.tsx
tsx
import { useEffect } from 'react'
import { AppleAuth } from 'expo-apple-sign-in'

type TAppleRevokeWatcherProps = {
  onRevoked: () => void
}

export function AppleRevokeWatcher({ onRevoked }: TAppleRevokeWatcherProps) {
  useEffect(() => {
    const subscription = AppleAuth.addRevokeListener(() => {
      void AppleAuth.signOut()
      onRevoked()
    })

    return () => subscription.remove()
  }, [onRevoked])

  return null
}

Pass a stable onRevoked, for example one wrapped in useCallback, so the effect does not subscribe again on every render. The listener only runs while the app is running and subscribed, so keep the startup check with getCredentialState as well. The useAppleAuth hook subscribes on its own and clears its credential when the event fires.

Sign out ​

AppleAuth.signOut() clears the in-memory credential and nothing else. Apple has no sign-out API, so there is no Apple session to end. Sign out of your own session (Supabase, Clerk, Firebase, or your server) in the same place.

lib/sign-out.ts
ts
import { AppleAuth } from 'expo-apple-sign-in'

export async function signOutOfApple(): Promise<void> {
  await AppleAuth.signOut()
}

Next steps ​

Handle failures with the error codes, verify the token on your server with Backend verification, or pass the credential to a provider through an adapter: Supabase, Clerk, or Firebase. Every signature is listed in the API reference.

Released under the MIT License.