# Build a custom forgot password flow

> This guide is for users who want to build a custom flow. To use a _prebuilt_ UI, use the [Account Portal pages](https://clerk.com/docs/guides/account-portal/overview.md?sdk=expo) or [prebuilt components](https://clerk.com/docs/expo/reference/components/overview.md).

> This guide applies to the following Clerk SDKs:
>
> - `@clerk/react` v6 or higher
> - `@clerk/nextjs` v7 or higher
> - `@clerk/expo` v3 or higher
> - `@clerk/react-router` v3 or higher
> - `@clerk/tanstack-react-start` v0.26.0 or higher
>
> If you're using an older version of one of these SDKs, or are using the legacy API, refer to the [legacy API documentation](https://clerk.com/docs/guides/development/custom-flows/authentication/legacy/forgot-password.md?sdk=expo).

It's common for a sign-in flow to include a flow for users that forgot their password.

In Clerk, the forgot password flow requires that either **Email** or **Phone** is enabled, as the password reset code can only be delivered to one of these identifiers.

The forgot password flow works as follows:

1. Users can have an email address or phone number, or both. The user enters their email address or phone number and asks for a password reset code.
2. Clerk sends an email or SMS to the user, containing a code.
3. The user enters the code and a new password.
4. Clerk verifies the code, and if successful, updates the user's password and signs them in.
5. If `signOutOfOtherSessions` is `true`, the user is signed out of all other authenticated sessions.

This guide demonstrates how to build a custom user interface for a forgot password flow that you can add to your existing sign-in flow. This guide covers the following scenarios:

- [The user has an email address as an identifier](#email-address)
- [The user has a phone number as an identifier](#phone-number)

> To allow users to update their password **after they've already signed in**, see the [custom flow for updating a user's password](https://clerk.com/docs/guides/development/custom-flows/account-updates/update-password.md?sdk=expo).

## Email address

filename: app/components/forgot-password.tsx
```tsx
import { ThemedText } from '@/components/themed-text'
import { ThemedView } from '@/components/themed-view'
import { useSignIn } from '@clerk/expo'
import { type Href, Link, useRouter } from 'expo-router'
import React from 'react'
import { Pressable, StyleSheet, TextInput, View } from 'react-native'

export default function ForgotPassword() {
  const { signIn, errors, fetchStatus } = useSignIn()
  const router = useRouter()

  const [emailAddress, setEmailAddress] = React.useState('')
  const [code, setCode] = React.useState('')
  const [password, setPassword] = React.useState('')
  const [codeSent, setCodeSent] = React.useState(false)

  // Step 1: Send the password reset code to the user's email
  async function sendCode() {
    const { error: createError } = await signIn.create({
      identifier: emailAddress,
    })
    if (createError) {
      console.error(JSON.stringify(createError, null, 2))
      return
    }

    const { error: sendCodeError } = await signIn.resetPasswordEmailCode.sendCode()
    if (sendCodeError) {
      console.error(JSON.stringify(sendCodeError, null, 2))
      return
    }

    setCodeSent(true)
  }

  // Step 2: Verify the code provided by the user
  async function verifyCode() {
    const { error } = await signIn.resetPasswordEmailCode.verifyCode({
      code,
    })
    if (error) {
      console.error(JSON.stringify(error, null, 2))
      return
    }
  }

  // Step 3: Submit the new password
  async function submitNewPassword() {
    const { error } = await signIn.resetPasswordEmailCode.submitPassword({
      password,
      // Optional: sign the user out of all other authenticated sessions
      signOutOfOtherSessions: true,
    })
    if (error) {
      console.error(JSON.stringify(error, null, 2))
      return
    }

    if (signIn.status === 'complete') {
      const { error } = await signIn.finalize({
        navigate: async ({ session, decorateUrl }) => {
          // Handle session tasks
          // See https://clerk.com/docs/guides/development/custom-flows/authentication/session-tasks
          if (session?.currentTask) {
            console.log(session.currentTask)
            return
          }

          // If no session tasks, navigate the signed-in user to the home page
          const url = decorateUrl('/')
          if (url.startsWith('http')) {
            window.location.href = url
          } else {
            router.push(url as Href)
          }
        },
      })

      if (error) {
        console.error(JSON.stringify(error, null, 2))
        return
      }
    } else if (signIn.status === 'needs_second_factor') {
      // See https://clerk.com/docs/guides/development/custom-flows/authentication/multi-factor-authentication
    } else {
      // Check why the sign-in is not complete
      console.error('Sign-in attempt not complete:', signIn)
    }
  }

  // Step 4 UI: Handle 2FA, or other authentication requirements
  // depending on the settings you've enabled in the Clerk Dashboard.
  // This may require combining this flow with other custom flows.
  if (signIn.status === 'needs_second_factor') {
    return (
      <ThemedView style={styles.container}>
        <ThemedText type="title" style={styles.title}>
          Two-Factor Authentication Required
        </ThemedText>
        <ThemedText style={styles.message}>
          2FA is required, but this UI does not handle that yet.
        </ThemedText>
      </ThemedView>
    )
  }

  // Step 3 UI: Collect the new password from the user
  if (signIn.status === 'needs_new_password') {
    return (
      <ThemedView style={styles.container}>
        <ThemedText type="title" style={styles.title}>
          Set New Password
        </ThemedText>
        <ThemedText style={styles.label}>Enter your new password</ThemedText>
        <TextInput
          style={styles.input}
          value={password}
          placeholder="Enter new password"
          placeholderTextColor="#666666"
          secureTextEntry={true}
          onChangeText={(password) => setPassword(password)}
        />
        {errors.fields.password && (
          <ThemedText style={styles.error}>{errors.fields.password.message}</ThemedText>
        )}
        <Pressable
          style={({ pressed }) => [
            styles.button,
            fetchStatus === 'fetching' && styles.buttonDisabled,
            pressed && styles.buttonPressed,
          ]}
          onPress={submitNewPassword}
          disabled={fetchStatus === 'fetching'}
        >
          <ThemedText style={styles.buttonText}>Set new password</ThemedText>
        </Pressable>
      </ThemedView>
    )
  }

  // Step 2 UI: Collect the code provided by the user
  if (codeSent) {
    return (
      <ThemedView style={styles.container}>
        <ThemedText type="title" style={styles.title}>
          Verify Code
        </ThemedText>
        <ThemedText style={styles.label}>
          Enter the password reset code sent to your email
        </ThemedText>
        <TextInput
          style={styles.input}
          value={code}
          placeholder="Enter verification code"
          placeholderTextColor="#666666"
          onChangeText={(code) => setCode(code)}
          keyboardType="numeric"
        />
        {errors.fields.code && (
          <ThemedText style={styles.error}>{errors.fields.code.message}</ThemedText>
        )}
        <Pressable
          style={({ pressed }) => [
            styles.button,
            fetchStatus === 'fetching' && styles.buttonDisabled,
            pressed && styles.buttonPressed,
          ]}
          onPress={verifyCode}
          disabled={fetchStatus === 'fetching'}
        >
          <ThemedText style={styles.buttonText}>Verify code</ThemedText>
        </Pressable>
      </ThemedView>
    )
  }

  // Step 1 UI: Collect the user's email so you can send them a password reset code
  return (
    <ThemedView style={styles.container}>
      <ThemedText type="title" style={styles.title}>
        Forgot Password?
      </ThemedText>
      <ThemedText style={styles.label}>Email address</ThemedText>
      <TextInput
        style={styles.input}
        autoCapitalize="none"
        value={emailAddress}
        placeholder="Enter email"
        placeholderTextColor="#666666"
        onChangeText={(emailAddress) => setEmailAddress(emailAddress)}
        keyboardType="email-address"
      />
      {errors.fields.identifier && (
        <ThemedText style={styles.error}>{errors.fields.identifier.message}</ThemedText>
      )}
      <Pressable
        style={({ pressed }) => [
          styles.button,
          (!emailAddress || fetchStatus === 'fetching') && styles.buttonDisabled,
          pressed && styles.buttonPressed,
        ]}
        onPress={sendCode}
        disabled={!emailAddress || fetchStatus === 'fetching'}
      >
        <ThemedText style={styles.buttonText}>Send password reset code</ThemedText>
      </Pressable>
      {/* For your debugging purposes. You can just console.log errors,
      but we put them in the UI for convenience */}
      {errors && <ThemedText style={styles.debug}>{JSON.stringify(errors, null, 2)}</ThemedText>}

      <View style={styles.linkContainer}>
        <ThemedText>Remember your password? </ThemedText>
        <Link href="/sign-in">
          <ThemedText type="link">Sign in</ThemedText>
        </Link>
      </View>
    </ThemedView>
  )
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    padding: 20,
    gap: 12,
  },
  title: {
    marginBottom: 8,
  },
  label: {
    fontWeight: '600',
    fontSize: 14,
  },
  message: {
    fontSize: 14,
    marginTop: 8,
  },
  input: {
    borderWidth: 1,
    borderColor: '#ccc',
    borderRadius: 8,
    padding: 12,
    fontSize: 16,
    backgroundColor: '#fff',
  },
  button: {
    backgroundColor: '#0a7ea4',
    paddingVertical: 12,
    paddingHorizontal: 24,
    borderRadius: 8,
    alignItems: 'center',
    marginTop: 8,
  },
  buttonPressed: {
    opacity: 0.7,
  },
  buttonDisabled: {
    opacity: 0.5,
  },
  buttonText: {
    color: '#fff',
    fontWeight: '600',
  },
  linkContainer: {
    flexDirection: 'row',
    gap: 4,
    marginTop: 12,
    alignItems: 'center',
  },
  error: {
    color: '#d32f2f',
    fontSize: 12,
    marginTop: -8,
  },
  debug: {
    fontSize: 10,
    opacity: 0.5,
    marginTop: 8,
  },
})
```

## Phone number

filename: app/components/forgot-password.tsx
```tsx
import { ThemedText } from '@/components/themed-text'
import { ThemedView } from '@/components/themed-view'
import { useSignIn } from '@clerk/expo'
import { type Href, Link, useRouter } from 'expo-router'
import React from 'react'
import { Pressable, StyleSheet, TextInput, View } from 'react-native'

export default function ForgotPassword() {
  const { signIn, errors, fetchStatus } = useSignIn()
  const router = useRouter()

  const [phoneNumber, setPhoneNumber] = React.useState('')
  const [code, setCode] = React.useState('')
  const [password, setPassword] = React.useState('')
  const [codeSent, setCodeSent] = React.useState(false)

  // Step 1: Send the password reset code to the user's phone number
  async function sendCode() {
    const { error: createError } = await signIn.create({
      identifier: phoneNumber,
    })
    if (createError) {
      console.error(JSON.stringify(createError, null, 2))
      return
    }

    const { error: sendCodeError } = await signIn.resetPasswordPhoneCode.sendCode()
    if (sendCodeError) {
      console.error(JSON.stringify(sendCodeError, null, 2))
      return
    }

    setCodeSent(true)
  }

  // Step 2: Verify the code provided by the user
  async function verifyCode() {
    const { error } = await signIn.resetPasswordPhoneCode.verifyCode({
      code,
    })
    if (error) {
      console.error(JSON.stringify(error, null, 2))
      return
    }
  }

  // Step 3: Submit the new password provided by the user
  async function submitNewPassword() {
    const { error } = await signIn.resetPasswordPhoneCode.submitPassword({
      password,
      // Optional: sign the user out of all other authenticated sessions
      signOutOfOtherSessions: true,
    })
    if (error) {
      console.error(JSON.stringify(error, null, 2))
      return
    }

    if (signIn.status === 'complete') {
      const { error } = await signIn.finalize({
        navigate: async ({ session, decorateUrl }) => {
          // Handle session tasks
          // See https://clerk.com/docs/guides/development/custom-flows/authentication/session-tasks
          if (session?.currentTask) {
            console.log(session.currentTask)
            return
          }

          // If no session tasks, navigate the signed-in user to the home page
          const url = decorateUrl('/')
          if (url.startsWith('http')) {
            window.location.href = url
          } else {
            router.push(url as Href)
          }
        },
      })

      if (error) {
        console.error(JSON.stringify(error, null, 2))
        return
      }
    } else if (signIn.status === 'needs_second_factor') {
      // See https://clerk.com/docs/guides/development/custom-flows/authentication/multi-factor-authentication
    } else {
      // Check why the sign-in is not complete
      console.error('Sign-in attempt not complete:', signIn)
    }
  }

  // Step 4 UI: Handle 2FA, or other authentication requirements
  // depending on the settings you've enabled in the Clerk Dashboard.
  // This may require combining this flow with other custom flows.
  if (signIn.status === 'needs_second_factor') {
    return (
      <ThemedView style={styles.container}>
        <ThemedText type="title" style={styles.title}>
          Two-Factor Authentication Required
        </ThemedText>
        <ThemedText style={styles.message}>
          2FA is required, but this UI does not handle that yet.
        </ThemedText>
      </ThemedView>
    )
  }

  // Step 3 UI: Collect the new password from the user
  if (signIn.status === 'needs_new_password') {
    return (
      <ThemedView style={styles.container}>
        <ThemedText type="title" style={styles.title}>
          Set New Password
        </ThemedText>
        <ThemedText style={styles.label}>Enter your new password</ThemedText>
        <TextInput
          style={styles.input}
          value={password}
          placeholder="Enter new password"
          placeholderTextColor="#666666"
          secureTextEntry={true}
          onChangeText={(password) => setPassword(password)}
        />
        {errors.fields.password && (
          <ThemedText style={styles.error}>{errors.fields.password.message}</ThemedText>
        )}
        <Pressable
          style={({ pressed }) => [
            styles.button,
            fetchStatus === 'fetching' && styles.buttonDisabled,
            pressed && styles.buttonPressed,
          ]}
          onPress={submitNewPassword}
          disabled={fetchStatus === 'fetching'}
        >
          <ThemedText style={styles.buttonText}>Set new password</ThemedText>
        </Pressable>
      </ThemedView>
    )
  }

  // Step 2 UI: Collect the code provided by the user
  if (codeSent) {
    return (
      <ThemedView style={styles.container}>
        <ThemedText type="title" style={styles.title}>
          Verify Code
        </ThemedText>
        <ThemedText style={styles.label}>
          Enter the password reset code sent to your phone number
        </ThemedText>
        <TextInput
          style={styles.input}
          value={code}
          placeholder="Enter verification code"
          placeholderTextColor="#666666"
          onChangeText={(code) => setCode(code)}
          keyboardType="numeric"
        />
        {errors.fields.code && (
          <ThemedText style={styles.error}>{errors.fields.code.message}</ThemedText>
        )}
        <Pressable
          style={({ pressed }) => [
            styles.button,
            fetchStatus === 'fetching' && styles.buttonDisabled,
            pressed && styles.buttonPressed,
          ]}
          onPress={verifyCode}
          disabled={fetchStatus === 'fetching'}
        >
          <ThemedText style={styles.buttonText}>Verify code</ThemedText>
        </Pressable>
      </ThemedView>
    )
  }

  // Step 1 UI: Collect the user's phone number so you can send them a password reset code
  return (
    <ThemedView style={styles.container}>
      <ThemedText type="title" style={styles.title}>
        Forgot Password?
      </ThemedText>
      <ThemedText style={styles.label}>Phone number</ThemedText>
      <TextInput
        style={styles.input}
        autoCapitalize="none"
        value={phoneNumber}
        placeholder="Enter phone number"
        placeholderTextColor="#666666"
        onChangeText={(phoneNumber) => setPhoneNumber(phoneNumber)}
        keyboardType="phone-pad"
      />
      {errors.fields.identifier && (
        <ThemedText style={styles.error}>{errors.fields.identifier.message}</ThemedText>
      )}
      <Pressable
        style={({ pressed }) => [
          styles.button,
          (!phoneNumber || fetchStatus === 'fetching') && styles.buttonDisabled,
          pressed && styles.buttonPressed,
        ]}
        onPress={sendCode}
        disabled={!phoneNumber || fetchStatus === 'fetching'}
      >
        <ThemedText style={styles.buttonText}>Send password reset code</ThemedText>
      </Pressable>
      {/* For your debugging purposes. You can just console.log errors, but we put them in the UI for convenience */}
      {errors && <ThemedText style={styles.debug}>{JSON.stringify(errors, null, 2)}</ThemedText>}

      <View style={styles.linkContainer}>
        <ThemedText>Remember your password? </ThemedText>
        <Link href="/sign-in">
          <ThemedText type="link">Sign in</ThemedText>
        </Link>
      </View>
    </ThemedView>
  )
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    padding: 20,
    gap: 12,
  },
  title: {
    marginBottom: 8,
  },
  label: {
    fontWeight: '600',
    fontSize: 14,
  },
  message: {
    fontSize: 14,
    marginTop: 8,
  },
  input: {
    borderWidth: 1,
    borderColor: '#ccc',
    borderRadius: 8,
    padding: 12,
    fontSize: 16,
    backgroundColor: '#fff',
  },
  button: {
    backgroundColor: '#0a7ea4',
    paddingVertical: 12,
    paddingHorizontal: 24,
    borderRadius: 8,
    alignItems: 'center',
    marginTop: 8,
  },
  buttonPressed: {
    opacity: 0.7,
  },
  buttonDisabled: {
    opacity: 0.5,
  },
  buttonText: {
    color: '#fff',
    fontWeight: '600',
  },
  linkContainer: {
    flexDirection: 'row',
    gap: 4,
    marginTop: 12,
    alignItems: 'center',
  },
  error: {
    color: '#d32f2f',
    fontSize: 12,
    marginTop: -8,
  },
  debug: {
    fontSize: 10,
    opacity: 0.5,
    marginTop: 8,
  },
})
```

## Handle compromised passwords

If you have enabled [rejection of compromised passwords also on sign-in](https://clerk.com/docs/guides/secure/password-protection-and-rules.md?sdk=expo#reject-compromised-passwords), then it is possible for the sign-in attempt to be rejected with the `form_password_pwned` error code.

In this case, you can prompt the user to reset their password using the exact same logic detailed in the previous section.

---

## Sitemap

[Overview of all docs pages](https://clerk.com/docs/llms.txt)
