Skip to main content
Docs

Build a custom flow for resetting a user's password

Warning

This guide is for users who want to build a . To use a prebuilt UI, use the Account Portal pages or prebuilt components.

Important

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.

The password reset 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.

This guide demonstrates how to use Clerk's API to build a custom flow for resetting a user's password. It covers the following scenarios:

Email address

Tip

Examples for this SDK aren't available yet. For now, try adapting the available example to fit your SDK.

app/components/forgot-password.tsx
'use client'
import React from 'react'
import { useSignIn } from '@clerk/nextjs'
import { useRouter } from 'next/navigation'

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)

  async function sendCode(e: React.FormEvent) {
    e.preventDefault()

    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)
  }

  async function verifyCode(e: React.FormEvent) {
    e.preventDefault()

    const { error } = await signIn.resetPasswordEmailCode.verifyCode({
      code,
    })
    if (error) {
      console.error(JSON.stringify(error, null, 2))
      return
    }
  }

  async function submitNewPassword(e: React.FormEvent) {
    e.preventDefault()

    const { error } = await signIn.resetPasswordEmailCode.submitPassword({
      password,
    })
    if (error) {
      console.error(JSON.stringify(error, null, 2))
      return
    }

    if (signIn.status === 'needs_second_factor') {
      // See https://clerk.com/docs/guides/development/custom-flows/authentication/multi-factor-authentication
    }

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

          const url = decorateUrl('/')
          if (url.startsWith('http')) {
            window.location.href = url
          } else {
            router.push(url)
          }
        },
      })

      if (error) {
        console.error(JSON.stringify(error, null, 2))
        return
      }
    }
  }

  return (
    <div>
      <h1>Forgot Password?</h1>

      {!codeSent && (
        <form onSubmit={sendCode}>
          <label htmlFor="emailAddress">Provide your email address</label>
          <input
            id="emailAddress"
            type="email"
            placeholder="e.g john@doe.com"
            value={emailAddress}
            onChange={(e) => setEmailAddress(e.target.value)}
          />
          {errors.fields.identifier && <p>{errors.fields.identifier.message}</p>}
          <button type="submit" disabled={fetchStatus === 'fetching'}>
            Send password reset code
          </button>
        </form>
      )}

      {codeSent && signIn.status !== 'needs_new_password' && (
        <form onSubmit={verifyCode}>
          <label htmlFor="code">Enter the password reset code that was sent to your email</label>
          <input id="code" type="text" value={code} onChange={(e) => setCode(e.target.value)} />
          {errors.fields.code && <p>{errors.fields.code.message}</p>}
          <button type="submit" disabled={fetchStatus === 'fetching'}>
            Verify code
          </button>
        </form>
      )}

      {signIn.status === 'needs_new_password' && (
        <form onSubmit={submitNewPassword}>
          <label htmlFor="password">Enter your new password</label>
          <input
            id="password"
            type="password"
            value={password}
            onChange={(e) => setPassword(e.target.value)}
          />
          {errors.fields.password && <p>{errors.fields.password.message}</p>}
          <button type="submit" disabled={fetchStatus === 'fetching'}>
            Set new password
          </button>
        </form>
      )}

      {signIn.status === 'needs_second_factor' && (
        <p>2FA is required, but this UI does not handle that.</p>
      )}

      {/* For your debugging purposes. You can just console.log errors, but we put them in the UI for convenience */}
      {errors && <pre>{JSON.stringify(errors, null, 2)}</pre>}
    </div>
  )
}
app/components/forgot-password.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)

  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)
  }

  async function verifyCode() {
    const { error } = await signIn.resetPasswordEmailCode.verifyCode({
      code,
    })
    if (error) {
      console.error(JSON.stringify(error, null, 2))
      return
    }
  }

  async function submitNewPassword() {
    const { error } = await signIn.resetPasswordEmailCode.submitPassword({
      password,
    })
    if (error) {
      console.error(JSON.stringify(error, null, 2))
      return
    }

    if (signIn.status === 'needs_second_factor') {
      // See https://clerk.com/docs/guides/development/custom-flows/authentication/multi-factor-authentication
      return
    }

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

          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
      }
    }
  }

  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>
    )
  }

  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>
    )
  }

  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>
    )
  }

  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,
  },
})
ForgotPasswordView.swift
  import SwiftUI
  import ClerkKit

  struct ForgotPasswordView: View {
    @Environment(Clerk.self) private var clerk
    @State private var email = ""
    @State private var code = ""
    @State private var newPassword = ""

    var signIn: SignIn? {
      clerk.auth.currentSignIn
    }

    var body: some View {
      switch signIn?.status {
      case .needsFirstFactor:
        // Verify the reset code
        TextField("Enter your code", text: $code)
        Button("Verify") {
          Task { await verify(code: code) }
        }

      case .needsSecondFactor:
        // See https://clerk.com/docs/guides/development/custom-flows/authentication/multi-factor-authentication
        Text("2FA is required, but this UI does not handle that")

      case .needsNewPassword:
        // Set the new password after verification
        SecureField("New password", text: $newPassword)
        Button("Set new password") {
          Task { await setNewPassword(password: newPassword) }
        }

      default:
        if let user = clerk.user {
          // Show the current user after a successful reset
          Text("Signed in as: \(user.id)")
          Button("Sign Out") {
            Task { try await clerk.auth.signOut() }
          }
        } else {
          // Start the reset flow by creating a sign-in attempt
          TextField("Email", text: $email)
          Button("Forgot password?") {
            Task { await startResetPassword(email: email) }
          }
        }
      }
    }

    private func startResetPassword(email: String) async {
      do {
        try await clerk.auth.signIn(email)
        try await signIn?.sendResetPasswordEmailCode()
        // Clerk updates currentSignIn and UI shows code field (needsFirstFactor)
      } catch {
        dump(error)
      }
    }

    private func verify(code: String) async {

      do {
        try await signIn?.verifyCode(code)
        // Clerk updates currentSignIn and UI shows new password field (needsNewPassword)
      } catch {
        dump(error)
      }
    }

    private func setNewPassword(password: String) async {
      do {
        try await signIn?.resetPassword(
          newPassword: password,
          signOutOfOtherSessions: true
        )

        // If verification was completed, the created session is available on `clerk.session` and UI shows signed in
      } catch {
        dump(error)
      }
    }
  }
ForgotPasswordEmailViewModel.kt
  import android.util.Log
  import androidx.lifecycle.ViewModel
  import androidx.lifecycle.viewModelScope
  import com.clerk.api.Clerk
  import com.clerk.api.network.serialization.errorMessage
  import com.clerk.api.network.serialization.flatMap
  import com.clerk.api.network.serialization.onFailure
  import com.clerk.api.network.serialization.onSuccess
  import com.clerk.api.signin.SignIn
  import kotlinx.coroutines.flow.MutableStateFlow
  import kotlinx.coroutines.flow.asStateFlow
  import kotlinx.coroutines.flow.combine
  import kotlinx.coroutines.flow.launchIn
  import kotlinx.coroutines.launch

  class ForgotPasswordEmailViewModel : ViewModel() {
    private val _uiState = MutableStateFlow<UiState>(UiState.Loading)
    val uiState = _uiState.asStateFlow()

    init {
      combine(Clerk.isInitialized, Clerk.userFlow) { isInitialized, user ->
          _uiState.value =
            when {
              !isInitialized -> UiState.Loading
              user != null -> UiState.Complete
              else -> UiState.SignedOut
            }
        }
        .launchIn(viewModelScope)
    }

    fun createSignIn(email: String) {
      viewModelScope.launch {
        Clerk.auth
          .signIn { this.email = email }
          .flatMap { signIn ->
            signIn.sendResetPasswordCode { this.email = email }
          }
          .onSuccess { updateStateFromStatus(it.status) }
          .onFailure {
          // See https://clerk.com/docs/guides/development/custom-flows/error-handling
          Log.e(
            ForgotPasswordEmailViewModel::class.simpleName,
            it.errorMessage,
            it.throwable,
          )
        }
      }
    }

    fun verify(code: String) {
      val signIn = Clerk.signIn ?: return
      viewModelScope.launch {
        signIn
          .verifyCode(code)
          .onSuccess { updateStateFromStatus(it.status) }
          .onFailure {
          // See https://clerk.com/docs/guides/development/custom-flows/error-handling
          Log.e(
            ForgotPasswordEmailViewModel::class.simpleName,
            it.errorMessage,
            it.throwable,
          )
        }
      }
    }

    fun setNewPassword(password: String) {
      val signIn = Clerk.signIn ?: return
      viewModelScope.launch {
        signIn
          .resetPassword(
            newPassword = password,
            signOutOfOtherSessions = true,
          )
          .onSuccess { updated ->
            updateStateFromStatus(updated.status)
            if (updated.status == SignIn.Status.COMPLETE) {
              updated.finalize()
            }
          }
          .onFailure {
          // See https://clerk.com/docs/guides/development/custom-flows/error-handling
          Log.e(
            ForgotPasswordEmailViewModel::class.simpleName,
            it.errorMessage,
            it.throwable,
          )
        }
      }
    }

    fun updateStateFromStatus(status: SignIn.Status) {
      val state =
        when (status) {
          SignIn.Status.COMPLETE -> UiState.Complete
          SignIn.Status.NEEDS_FIRST_FACTOR -> UiState.NeedsFirstFactor
          SignIn.Status.NEEDS_SECOND_FACTOR -> UiState.NeedsSecondFactor // See https://clerk.com/docs/guides/development/custom-flows/authentication/multi-factor-authentication
          SignIn.Status.NEEDS_NEW_PASSWORD -> UiState.NeedsNewPassword
          else -> {
            UiState.SignedOut
          }
        }

      _uiState.value = state
    }

    sealed interface UiState {
      data object Loading : UiState
      data object SignedOut : UiState
      data object NeedsFirstFactor : UiState
      data object NeedsSecondFactor : UiState
      data object NeedsNewPassword : UiState
      data object Complete : UiState
    }
  }
ForgotPasswordEmailActivity.kt
  import android.os.Bundle
  import androidx.activity.ComponentActivity
  import androidx.activity.compose.setContent
  import androidx.activity.viewModels
  import androidx.compose.foundation.layout.Arrangement.Absolute.spacedBy
  import androidx.compose.foundation.layout.Box
  import androidx.compose.foundation.layout.Column
  import androidx.compose.foundation.layout.fillMaxSize
  import androidx.compose.material3.Button
  import androidx.compose.material3.CircularProgressIndicator
  import androidx.compose.material3.Text
  import androidx.compose.material3.TextField
  import androidx.compose.runtime.Composable
  import androidx.compose.runtime.getValue
  import androidx.compose.runtime.mutableStateOf
  import androidx.compose.runtime.remember
  import androidx.compose.runtime.setValue
  import androidx.compose.ui.Alignment
  import androidx.compose.ui.Modifier
  import androidx.compose.ui.text.input.PasswordVisualTransformation
  import androidx.compose.ui.text.input.VisualTransformation
  import androidx.compose.ui.unit.dp
  import androidx.lifecycle.compose.collectAsStateWithLifecycle
  import com.clerk.api.Clerk

  class ForgotPasswordEmailActivity : ComponentActivity() {
    val viewModel: ForgotPasswordEmailViewModel by viewModels()

    override fun onCreate(savedInstanceState: Bundle?) {
      super.onCreate(savedInstanceState)
      setContent {
        val state by viewModel.uiState.collectAsStateWithLifecycle()
        ForgotPasswordView(
          state,
          onVerify = viewModel::verify,
          onSetNewPassword = viewModel::setNewPassword,
          onCreateSignIn = viewModel::createSignIn,
        )
      }
    }
  }

  @Composable
  fun ForgotPasswordView(
    state: ForgotPasswordEmailViewModel.UiState,
    onVerify: (String) -> Unit,
    onSetNewPassword: (String) -> Unit,
    onCreateSignIn: (String) -> Unit,
  ) {

    Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
      when (state) {
        ForgotPasswordEmailViewModel.UiState.Complete -> {
          Text("Active session: ${Clerk.activeSession?.id}")
        }

        ForgotPasswordEmailViewModel.UiState.NeedsFirstFactor -> {
          InputContent(placeholder = "Enter your code", buttonText = "Verify", onClick = onVerify)
        }
        ForgotPasswordEmailViewModel.UiState.NeedsNewPassword -> {
          InputContent(
            placeholder = "Enter your new password",
            buttonText = "Set new password",
            onClick = onSetNewPassword,
            visualTransformation = PasswordVisualTransformation(),
          )
        }
        ForgotPasswordEmailViewModel.UiState.NeedsSecondFactor -> {
          Text("2FA is required but this UI does not handle that")
        }
        ForgotPasswordEmailViewModel.UiState.SignedOut -> {
          InputContent(
            placeholder = "Enter your email address",
            buttonText = "Forgot password?",
            onClick = onCreateSignIn,
          )
        }

        ForgotPasswordEmailViewModel.UiState.Loading -> CircularProgressIndicator()
      }
    }
  }

  @Composable
  fun InputContent(
    placeholder: String,
    buttonText: String,
    visualTransformation: VisualTransformation = VisualTransformation.None,
    onClick: (String) -> Unit,
  ) {
    var value by remember { mutableStateOf("") }
    Column(
      horizontalAlignment = Alignment.CenterHorizontally,
      verticalArrangement = spacedBy(16.dp, Alignment.CenterVertically),
    ) {
      TextField(
        value = value,
        onValueChange = { value = it },
        visualTransformation = visualTransformation,
        placeholder = { Text(placeholder) },
      )
      Button(onClick = { onClick(value) }) { Text(buttonText) }
    }
  }

Tip

Examples for this SDK aren't available yet. For now, try adapting the available example to fit your SDK.

app/components/forgot-password.tsx
'use client'
import React from 'react'
import { useSignIn } from '@clerk/nextjs'
import { useRouter } from 'next/navigation'

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)

  async function sendCode(e: React.FormEvent) {
    e.preventDefault()

    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)
  }

  async function verifyCode(e: React.FormEvent) {
    e.preventDefault()

    const { error } = await signIn.resetPasswordPhoneCode.verifyCode({
      code,
    })
    if (error) {
      console.error(JSON.stringify(error, null, 2))
      return
    }
  }

  async function submitNewPassword(e: React.FormEvent) {
    e.preventDefault()

    const { error } = await signIn.resetPasswordPhoneCode.submitPassword({
      password,
    })
    if (error) {
      console.error(JSON.stringify(error, null, 2))
      return
    }

    if (signIn.status === 'needs_second_factor') {
      // See https://clerk.com/docs/guides/development/custom-flows/authentication/multi-factor-authentication
      return
    }

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

          const url = decorateUrl('/')
          if (url.startsWith('http')) {
            window.location.href = url
          } else {
            router.push(url)
          }
        },
      })

      if (error) {
        console.error(JSON.stringify(error, null, 2))
        return
      }
    }
  }

  return (
    <div>
      <h1>Forgot Password?</h1>

      {!codeSent && (
        <form onSubmit={sendCode}>
          <label htmlFor="phoneNumber">Provide your phone number</label>
          <input
            id="phoneNumber"
            type="phoneNumber"
            placeholder="e.g +1234567890"
            value={phoneNumber}
            onChange={(e) => setPhoneNumber(e.target.value)}
          />
          {errors.fields.identifier && <p>{errors.fields.identifier.message}</p>}
          <button type="submit" disabled={fetchStatus === 'fetching'}>
            Send password reset code
          </button>
        </form>
      )}

      {codeSent && signIn.status !== 'needs_new_password' && (
        <form onSubmit={verifyCode}>
          <label htmlFor="code">
            Enter the password reset code that was sent to your phone number
          </label>
          <input id="code" type="text" value={code} onChange={(e) => setCode(e.target.value)} />
          {errors.fields.code && <p>{errors.fields.code.message}</p>}
          <button type="submit" disabled={fetchStatus === 'fetching'}>
            Verify code
          </button>
        </form>
      )}

      {signIn.status === 'needs_new_password' && (
        <form onSubmit={submitNewPassword}>
          <label htmlFor="password">Enter your new password</label>
          <input
            id="password"
            type="password"
            value={password}
            onChange={(e) => setPassword(e.target.value)}
          />
          {errors.fields.password && <p>{errors.fields.password.message}</p>}
          <button type="submit" disabled={fetchStatus === 'fetching'}>
            Set new password
          </button>
        </form>
      )}

      {signIn.status === 'needs_second_factor' && (
        <p>2FA is required, but this UI does not handle that.</p>
      )}

      {/* For your debugging purposes. You can just console.log errors, but we put them in the UI for convenience */}
      {errors && <pre>{JSON.stringify(errors, null, 2)}</pre>}
    </div>
  )
}
app/components/forgot-password.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)

  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)
  }

  async function verifyCode() {
    const { error } = await signIn.resetPasswordPhoneCode.verifyCode({
      code,
    })
    if (error) {
      console.error(JSON.stringify(error, null, 2))
      return
    }
  }

  async function submitNewPassword() {
    const { error } = await signIn.resetPasswordPhoneCode.submitPassword({
      password,
    })
    if (error) {
      console.error(JSON.stringify(error, null, 2))
      return
    }

    if (signIn.status === 'needs_second_factor') {
      // See https://clerk.com/docs/guides/development/custom-flows/authentication/multi-factor-authentication
      return
    }

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

          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
      }
    }
  }

  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>
    )
  }

  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>
    )
  }

  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>
    )
  }

  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,
  },
})
ForgotPasswordView.swift
  import SwiftUI
  import ClerkKit

  struct ForgotPasswordView: View {
    @Environment(Clerk.self) private var clerk
    @State private var phoneNumber = ""
    @State private var code = ""
    @State private var newPassword = ""

    var signIn: SignIn? {
      clerk.auth.currentSignIn
    }

    var body: some View {
      switch clerk.auth.currentSignIn?.status {
      case .needsFirstFactor:
        // Verify the reset code
        TextField("Enter your code", text: $code)
        Button("Verify") {
          Task { await verify(code: code) }
        }

      case .needsSecondFactor:
        // See https://clerk.com/docs/guides/development/custom-flows/authentication/multi-factor-authentication
        Text("2FA is required, but this UI does not handle that")

      case .needsNewPassword:
        // Set the new password after verification
        SecureField("New password", text: $newPassword)
        Button("Set new password") {
          Task { await setNewPassword(password: newPassword) }
        }

      default:
        if let user = clerk.user {
          // Show the current user after a successful reset
          Text("Signed in as: \(user.id)")
          Button("Sign Out") {
            Task { try await clerk.auth.signOut() }
          }
        } else {
          TextField("Phone number", text: $phoneNumber)
          Button("Forgot password?") {
            Task { await startResetPassword(phoneNumber: phoneNumber) }
          }
        }
      }
    }

    private func startResetPassword(phoneNumber: String) async {
      do {
        try await clerk.auth.signIn(phoneNumber)
        try await signIn?.sendResetPasswordPhoneCode()
        // Clerk updates currentSignIn and UI shows code field (needsFirstFactor)
      } catch {
        dump(error)
      }
    }

    private func verify(code: String) async {
      do {
         try await signIn?.verifyCode(code)
        // Clerk updates currentSignIn and UI shows new password field (needsNewPassword)
      } catch {
        dump(error)
      }
    }

    private func setNewPassword(password: String) async {
      do {
        try await signIn?.resetPassword(
          newPassword: password,
          signOutOfOtherSessions: true
        )

        // If verification was completed, the created session is available on `clerk.session` and UI shows signed in
      } catch {
        dump(error)
      }
    }
  }
ForgotPasswordPhoneViewModel.kt
import android.util.Log
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.clerk.api.Clerk
import com.clerk.api.network.serialization.errorMessage
import com.clerk.api.network.serialization.flatMap
import com.clerk.api.network.serialization.onFailure
import com.clerk.api.network.serialization.onSuccess
import com.clerk.api.signin.SignIn
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.launch

class ForgotPasswordPhoneViewModel : ViewModel() {
  private val _uiState = MutableStateFlow<UiState>(UiState.Loading)
  val uiState = _uiState.asStateFlow()

  init {
    combine(Clerk.isInitialized, Clerk.userFlow) { isInitialized, user ->
        _uiState.value =
          when {
            !isInitialized -> UiState.Loading
            user != null -> UiState.Complete
            else -> UiState.SignedOut
          }
      }
      .launchIn(viewModelScope)
  }

  fun createSignIn(phoneNumber: String) {
    viewModelScope.launch {
      Clerk.auth
        .signIn { this.phone = phoneNumber }
        .flatMap { signIn ->
          signIn.sendResetPasswordCode { this.phone = phoneNumber }
        }
        .onSuccess { updateStateFromStatus(it.status) }
        .onFailure {
        // See https://clerk.com/docs/guides/development/custom-flows/error-handling
        Log.e(
          ForgotPasswordPhoneViewModel::class.simpleName,
          it.errorMessage,
          it.throwable,
        )
      }
    }
  }

  fun verify(code: String) {
    val signIn = Clerk.signIn ?: return
    viewModelScope.launch {
      signIn
        .verifyCode(code)
        .onSuccess { updateStateFromStatus(it.status) }
        .onFailure {
        // See https://clerk.com/docs/guides/development/custom-flows/error-handling
        Log.e(
          ForgotPasswordPhoneViewModel::class.simpleName,
          it.errorMessage,
          it.throwable,
        )
      }
    }
  }

  fun setNewPassword(password: String) {
    val signIn = Clerk.signIn ?: return
    viewModelScope.launch {
      signIn
        .resetPassword(
          newPassword = password,
          signOutOfOtherSessions = true,
        )
        .onSuccess { updated ->
          updateStateFromStatus(updated.status)
          if (updated.status == SignIn.Status.COMPLETE) {
            updated.finalize()
          }
        }
        .onFailure {
        // See https://clerk.com/docs/guides/development/custom-flows/error-handling
        Log.e(
          ForgotPasswordPhoneViewModel::class.simpleName,
          it.errorMessage,
          it.throwable,
        )
      }
    }
  }

  fun updateStateFromStatus(status: SignIn.Status) {
    val state =
      when (status) {
        SignIn.Status.COMPLETE -> UiState.Complete
        SignIn.Status.NEEDS_FIRST_FACTOR -> UiState.NeedsFirstFactor
        // See https://clerk.com/docs/guides/development/custom-flows/authentication/multi-factor-authentication
        SignIn.Status.NEEDS_SECOND_FACTOR -> UiState.NeedsSecondFactor
        SignIn.Status.NEEDS_NEW_PASSWORD -> UiState.NeedsNewPassword
        else -> {
          UiState.SignedOut
        }
      }

    _uiState.value = state
  }

  sealed interface UiState {
    data object Loading : UiState
    data object SignedOut : UiState
    data object NeedsFirstFactor : UiState
    data object NeedsSecondFactor : UiState
    data object NeedsNewPassword : UiState
    data object Complete : UiState
  }
}
ForgotPasswordPhoneActivity.kt
  import android.os.Bundle
  import androidx.activity.ComponentActivity
  import androidx.activity.compose.setContent
  import androidx.activity.viewModels
  import androidx.compose.foundation.layout.Arrangement.Absolute.spacedBy
  import androidx.compose.foundation.layout.Box
  import androidx.compose.foundation.layout.Column
  import androidx.compose.foundation.layout.fillMaxSize
  import androidx.compose.material3.Button
  import androidx.compose.material3.CircularProgressIndicator
  import androidx.compose.material3.Text
  import androidx.compose.material3.TextField
  import androidx.compose.runtime.Composable
  import androidx.compose.runtime.getValue
  import androidx.compose.runtime.mutableStateOf
  import androidx.compose.runtime.remember
  import androidx.compose.runtime.setValue
  import androidx.compose.ui.Alignment
  import androidx.compose.ui.Modifier
  import androidx.compose.ui.text.input.PasswordVisualTransformation
  import androidx.compose.ui.text.input.VisualTransformation
  import androidx.compose.ui.unit.dp
  import androidx.lifecycle.compose.collectAsStateWithLifecycle
  import com.clerk.api.Clerk

  class ForgotPasswordPhoneActivity : ComponentActivity() {
      val viewModel: ForgotPasswordPhoneViewModel by viewModels()

      override fun onCreate(savedInstanceState: Bundle?) {
          super.onCreate(savedInstanceState)
          setContent {
              val state by viewModel.uiState.collectAsStateWithLifecycle()
              ForgotPasswordView(
                  state,
                  onVerify = viewModel::verify,
                  onSetNewPassword = viewModel::setNewPassword,
                  onCreateSignIn = viewModel::createSignIn,
              )
          }
      }
  }

  @Composable
  fun ForgotPasswordView(
      state: ForgotPasswordPhoneViewModel.UiState,
      onVerify: (String) -> Unit,
      onSetNewPassword: (String) -> Unit,
      onCreateSignIn: (String) -> Unit,
  ) {

      Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
          when (state) {
              ForgotPasswordPhoneViewModel.UiState.Complete -> {
                  Text("Active session: ${Clerk.activeSession?.id}")
              }
              ForgotPasswordPhoneViewModel.UiState.NeedsFirstFactor -> {
                  InputContent(placeholder = "Enter your code", buttonText = "Verify", onClick = onVerify)
              }
              ForgotPasswordPhoneViewModel.UiState.NeedsNewPassword -> {
                  InputContent(
                      placeholder = "Enter your new password",
                      buttonText = "Set new password",
                      onClick = onSetNewPassword,
                      visualTransformation = PasswordVisualTransformation(),
                  )
              }
              ForgotPasswordPhoneViewModel.UiState.NeedsSecondFactor -> {
                  Text("2FA is required but this UI does not handle that")
              }
              ForgotPasswordPhoneViewModel.UiState.SignedOut -> {
                  InputContent(
                      placeholder = "Enter your phone number",
                      buttonText = "Forgot password?",
                      onClick = onCreateSignIn,
                  )
              }

              ForgotPasswordPhoneViewModel.UiState.Loading -> CircularProgressIndicator()
          }
      }
  }

  @Composable
  fun InputContent(
      placeholder: String,
      buttonText: String,
      visualTransformation: VisualTransformation = VisualTransformation.None,
      onClick: (String) -> Unit,
  ) {
      var value by remember { mutableStateOf("") }
      Column(
          horizontalAlignment = Alignment.CenterHorizontally,
          verticalArrangement = spacedBy(16.dp, Alignment.CenterVertically),
      ) {
          TextField(
              value = value,
              onValueChange = { value = it },
              visualTransformation = visualTransformation,
              placeholder = { Text(placeholder) },
          )
          Button(onClick = { onClick(value) }) { Text(buttonText) }
      }
  }

Handle compromised passwords

If you have enabled rejection of compromised passwords also on sign-in, 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.

Feedback

What did you think of this content?

Last updated on