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.

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:

Reset user's password with an email address

This example is written for Next.js App Router but it can be adapted for any React-based framework, such as React Router or Tanstack React Start.

app/forgot-password/page.tsx
'use client'

import React, { useEffect, useState } from 'react'
import { useAuth, useSignIn } from '@clerk/nextjs'
import { useRouter } from 'next/navigation'

export default function Page() {
  const { isSignedIn } = useAuth()
  const { isLoaded, signIn, setActive } = useSignIn()
  const router = useRouter()

  const [email, setEmail] = useState('')
  const [password, setPassword] = useState('')
  const [code, setCode] = useState('')
  const [successfulCreation, setSuccessfulCreation] = useState(false)
  const [secondFactor, setSecondFactor] = useState(false)
  const [error, setError] = useState('')

  // Redirect signed-in users to home
  useEffect(() => {
    if (isSignedIn) router.push('/')
  }, [isSignedIn, router])

  if (!isLoaded) return <p>Loading...</p>

  // Send the password reset code to the user's email
  async function create(e: React.FormEvent) {
    e.preventDefault()
    await signIn
      ?.create({
        strategy: 'reset_password_email_code',
        identifier: email,
      })
      .then((_) => {
        setSuccessfulCreation(true)
        setError('')
      })
      .catch((err) => {
        console.error('error', err.errors[0].longMessage)
        setError(err.errors[0].longMessage)
      })
  }

  // Reset the user's password.
  // Upon successful reset, the user will be
  // signed in and redirected to the home page
  async function reset(e: React.FormEvent) {
    e.preventDefault()
    await signIn
      ?.attemptFirstFactor({
        strategy: 'reset_password_email_code',
        code,
        password,
      })
      .then((result) => {
        // Check if 2FA is required
        if (result.status === 'needs_second_factor') {
          setSecondFactor(true)
          setError('')
        } else if (result.status === 'complete') {
          // Set the active session to
          // the newly created session (user is now signed in)
          setActive({
            session: result.createdSessionId,
            navigate: async ({ session }) => {
              if (session?.currentTask) {
                // Handle pending session tasks
                // See https://clerk.com/docs/guides/development/custom-flows/authentication/session-tasks
                console.log(session?.currentTask)
                return
              }

              router.push('/')
            },
          })
          setError('')
        } else {
          console.log(result)
        }
      })
      .catch((err) => {
        console.error('error', err.errors[0].longMessage)
        setError(err.errors[0].longMessage)
      })
  }

  return (
    <div>
      <h1>Forgot Password?</h1>
      <form onSubmit={!successfulCreation ? create : reset}>
        {!successfulCreation && (
          <>
            <label htmlFor="email">Provide your email address</label>
            <input
              type="email"
              placeholder="e.g john@doe.com"
              value={email}
              onChange={(e) => setEmail(e.target.value)}
            />

            <button>Send password reset code</button>
            {error && <p>{error}</p>}
          </>
        )}

        {successfulCreation && (
          <>
            <label htmlFor="password">Enter your new password</label>
            <input type="password" value={password} onChange={(e) => setPassword(e.target.value)} />

            <label htmlFor="code">Enter the password reset code that was sent to your email</label>
            <input type="text" value={code} onChange={(e) => setCode(e.target.value)} />

            <button>Reset</button>
            {error && <p>{error}</p>}
          </>
        )}

        {secondFactor && <p>2FA is required, but this UI does not handle that</p>}
      </form>
    </div>
  )
}
app/(account)/forgot-password.tsx
import { ThemedText } from '@/components/themed-text'
import { ThemedView } from '@/components/themed-view'
import { useSignIn, useUser } from '@clerk/clerk-expo'
import { Redirect, useRouter } from 'expo-router'
import React, { useState } from 'react'
import { Pressable, StyleSheet, TextInput, View } from 'react-native'

export default function Page() {
  const { isSignedIn } = useUser()
  const { isLoaded, signIn, setActive } = useSignIn()
  const router = useRouter()

  const [email, setEmail] = useState('')
  const [password, setPassword] = useState('')
  const [code, setCode] = useState('')
  const [successfulCreation, setSuccessfulCreation] = useState(false)
  const [secondFactor, setSecondFactor] = useState(false)
  const [error, setError] = useState('')

  // Redirect signed-in users to home
  if (isSignedIn) {
    return <Redirect href="/" />
  }

  if (!isLoaded) {
    return (
      <ThemedView style={styles.container}>
        <ThemedText>Loading...</ThemedText>
      </ThemedView>
    )
  }

  // Send the password reset code to the user's email
  async function create() {
    try {
      await signIn?.create({
        strategy: 'reset_password_email_code',
        identifier: email,
      })
      setSuccessfulCreation(true)
      setError('')
    } catch (err: any) {
      console.error('error', err.errors?.[0]?.longMessage)
      setError(err.errors?.[0]?.longMessage || 'An error occurred')
    }
  }

  // Reset the user's password.
  // Upon successful reset, the user will be
  // signed in and redirected to the home page
  async function reset() {
    try {
      const result = await signIn?.attemptFirstFactor({
        strategy: 'reset_password_email_code',
        code,
        password,
      })

      if (!result) return

      // Check if 2FA is required
      if (result.status === 'needs_second_factor') {
        setSecondFactor(true)
        setError('')
      } else if (result.status === 'complete') {
        // Set the active session to
        // the newly created session (user is now signed in)
        await setActive?.({
          session: result.createdSessionId,
          navigate: async ({ session }) => {
            if (session?.currentTask) {
              // Handle pending session tasks
              // See https://clerk.com/docs/guides/development/custom-flows/authentication/session-tasks
              console.log(session?.currentTask)
              return
            }

            router.replace('/')
          },
        })
        setError('')
      } else {
        console.log(result)
      }
    } catch (err: any) {
      console.error('error', err.errors?.[0]?.longMessage)
      setError(err.errors?.[0]?.longMessage || 'An error occurred')
    }
  }

  return (
    <ThemedView style={styles.container}>
      <ThemedText type="title" style={styles.title}>
        Forgot Password?
      </ThemedText>

      {!successfulCreation && (
        <>
          <ThemedText style={styles.label}>Provide your email address</ThemedText>
          <TextInput
            style={styles.input}
            placeholder="e.g john@doe.com"
            placeholderTextColor="#666666"
            value={email}
            onChangeText={setEmail}
            autoCapitalize="none"
            keyboardType="email-address"
          />

          <Pressable
            style={({ pressed }) => [
              styles.button,
              !email && styles.buttonDisabled,
              pressed && styles.buttonPressed,
            ]}
            onPress={create}
            disabled={!email}
          >
            <ThemedText style={styles.buttonText}>Send password reset code</ThemedText>
          </Pressable>

          {error && (
            <View style={styles.errorContainer}>
              <ThemedText style={styles.errorText}>{error}</ThemedText>
            </View>
          )}
        </>
      )}

      {successfulCreation && !secondFactor && (
        <>
          <ThemedText style={styles.description}>
            A password reset code has been sent to your email
          </ThemedText>

          <ThemedText style={styles.label}>Enter your new password</ThemedText>
          <TextInput
            style={styles.input}
            placeholder="Enter new password"
            placeholderTextColor="#666666"
            value={password}
            onChangeText={setPassword}
            secureTextEntry={true}
          />

          <ThemedText style={styles.label}>Enter the password reset code</ThemedText>
          <TextInput
            style={styles.input}
            placeholder="Enter code"
            placeholderTextColor="#666666"
            value={code}
            onChangeText={setCode}
            keyboardType="numeric"
          />

          <Pressable
            style={({ pressed }) => [
              styles.button,
              (!password || !code) && styles.buttonDisabled,
              pressed && styles.buttonPressed,
            ]}
            onPress={reset}
            disabled={!password || !code}
          >
            <ThemedText style={styles.buttonText}>Reset Password</ThemedText>
          </Pressable>

          {error && (
            <View style={styles.errorContainer}>
              <ThemedText style={styles.errorText}>{error}</ThemedText>
            </View>
          )}
        </>
      )}

      {secondFactor && (
        <View style={styles.warningContainer}>
          <ThemedText style={styles.warningText}>
            2FA is required, but this UI does not handle that yet
          </ThemedText>
        </View>
      )}
    </ThemedView>
  )
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    padding: 20,
    gap: 12,
  },
  title: {
    marginBottom: 8,
  },
  description: {
    fontSize: 14,
    marginBottom: 16,
    opacity: 0.8,
  },
  label: {
    fontWeight: '600',
    fontSize: 14,
  },
  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',
  },
  errorContainer: {
    padding: 12,
    backgroundColor: '#ffebee',
    borderRadius: 8,
    marginTop: 8,
  },
  errorText: {
    color: '#c62828',
    fontWeight: '500',
  },
  warningContainer: {
    padding: 12,
    backgroundColor: '#fff3e0',
    borderRadius: 8,
    marginTop: 8,
  },
  warningText: {
    color: '#e65100',
    fontWeight: '500',
  },
})
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 = ""
    @State private var isVerifying = false

    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:
        // Handle any additional verification requirements.
        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)")
        } else {
          // Start the reset flow by creating a sign-in attempt.
          TextField("Email", text: $email)
          Button("Forgot password?") {
            Task { await createSignIn(email: email) }
          }
        }
      }
    }
  }
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.onFailure
  import com.clerk.api.network.serialization.onSuccess
  import com.clerk.api.signin.SignIn
  import com.clerk.api.signin.attemptFirstFactor
  import com.clerk.api.signin.resetPassword
  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 {
        SignIn.create(SignIn.CreateParams.Strategy.ResetPasswordEmailCode(identifier = email))
          .onSuccess { updateStateFromStatus(it.status) }
          .onFailure {
            // See https://clerk.com/docs/guides/development/custom-flows/error-handling
            // for more info on error handling
            Log.e(
              ForgotPasswordEmailViewModel::class.simpleName,
              it.errorMessage,
              it.throwable,
            )
          }
      }
    }

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

    fun setNewPassword(password: String) {
      val inProgressSignIn = Clerk.signIn ?: return
      viewModelScope.launch {
        inProgressSignIn
          .resetPassword(SignIn.ResetPasswordParams(password))
          .onSuccess { updateStateFromStatus(it.status) }
          .onFailure {
            // See https://clerk.com/docs/guides/development/custom-flows/error-handling
            // for more info on 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
          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) }
    }
  }

Examples for this SDK aren't available yet. For now, try switching to a supported SDK, such as Next.js, and converting the code to fit your SDK.

This example is written for Next.js App Router but it can be adapted for any React-based framework, such as React Router or Tanstack React Start.

app/forgot-password/page.tsx
'use client'

import React, { useState, useEffect } from 'react'
import { useAuth, useSignIn } from '@clerk/nextjs'
import { useRouter } from 'next/navigation'

export default function Page() {
  const { isSignedIn } = useAuth()
  const { isLoaded, signIn, setActive } = useSignIn()
  const router = useRouter()

  const [phoneNumber, setPhoneNumber] = useState('')
  const [password, setPassword] = useState('')
  const [code, setCode] = useState('')
  const [successfulCreation, setSuccessfulCreation] = useState(false)
  const [secondFactor, setSecondFactor] = useState(false)
  const [error, setError] = useState('')

  // Redirect signed-in users to home
  useEffect(() => {
    if (isSignedIn) router.push('/')
  }, [isSignedIn, router])

  if (!isLoaded) return <p>Loading...</p>

  // Send the password reset code to the user's email
  async function create(e: React.FormEvent) {
    e.preventDefault()
    await signIn
      ?.create({
        strategy: 'reset_password_phone_code',
        identifier: phoneNumber,
      })
      .then((_) => {
        setSuccessfulCreation(true)
        setError('')
      })
      .catch((err) => {
        console.error('error', err.errors[0].longMessage)
        setError(err.errors[0].longMessage)
      })
  }

  // Reset the user's password.
  // Upon successful reset, the user will be
  // signed in and redirected to the home page
  async function reset(e: React.FormEvent) {
    e.preventDefault()
    await signIn
      ?.attemptFirstFactor({
        strategy: 'reset_password_phone_code',
        code,
        password,
      })
      .then((result) => {
        // Check if 2FA is required
        if (result.status === 'needs_second_factor') {
          setSecondFactor(true)
          setError('')
        } else if (result.status === 'complete') {
          // Set the active session to
          // the newly created session (user is now signed in)
          setActive({
            session: result.createdSessionId,
            navigate: async ({ session }) => {
              if (session?.currentTask) {
                // Handle pending session tasks
                // See https://clerk.com/docs/guides/development/custom-flows/authentication/session-tasks
                console.log(session?.currentTask)
                return
              }

              router.push('/')
            },
          })
          setError('')
        } else {
          console.log(result)
        }
      })
      .catch((err) => {
        console.error('error', err.errors[0].longMessage)
        setError(err.errors[0].longMessage)
      })
  }

  return (
    <div>
      <h1>Forgot Password?</h1>
      <form onSubmit={!successfulCreation ? create : reset}>
        {!successfulCreation && (
          <>
            <label htmlFor="phoneNumber">Provide your phone number</label>
            <input
              type="tel"
              placeholder="e.g +1234567890"
              value={phoneNumber}
              onChange={(e) => setPhoneNumber(e.target.value)}
            />

            <button>Send password reset code</button>
            {error && <p>{error}</p>}
          </>
        )}

        {successfulCreation && (
          <>
            <label htmlFor="password">Enter your new password</label>
            <input type="password" value={password} onChange={(e) => setPassword(e.target.value)} />

            <label htmlFor="code">
              Enter the password reset code that was sent to your phone number
            </label>
            <input type="text" value={code} onChange={(e) => setCode(e.target.value)} />

            <button>Reset</button>
            {error && <p>{error}</p>}
          </>
        )}

        {secondFactor && <p>2FA is required, but this UI does not handle that</p>}
      </form>
    </div>
  )
}
app/(account)/forgot-password.tsx
import { ThemedText } from '@/components/themed-text'
import { ThemedView } from '@/components/themed-view'
import { useSignIn, useUser } from '@clerk/clerk-expo'
import { Redirect, useRouter } from 'expo-router'
import React, { useState } from 'react'
import { Pressable, StyleSheet, TextInput, View } from 'react-native'

export default function ForgotPasswordPage() {
  const { isSignedIn } = useUser()
  const { isLoaded, signIn, setActive } = useSignIn()
  const router = useRouter()

  const [phoneNumber, setPhoneNumber] = useState('')
  const [password, setPassword] = useState('')
  const [code, setCode] = useState('')
  const [successfulCreation, setSuccessfulCreation] = useState(false)
  const [secondFactor, setSecondFactor] = useState(false)
  const [error, setError] = useState('')

  // Redirect signed-in users to home
  if (isSignedIn) {
    return <Redirect href="/" />
  }

  if (!isLoaded) {
    return (
      <ThemedView style={styles.container}>
        <ThemedText>Loading...</ThemedText>
      </ThemedView>
    )
  }

  // Send the password reset code to the user's phone
  async function create() {
    try {
      await signIn?.create({
        strategy: 'reset_password_phone_code',
        identifier: phoneNumber,
      })
      setSuccessfulCreation(true)
      setError('')
    } catch (err: any) {
      console.error('error', err.errors?.[0]?.longMessage)
      setError(err.errors?.[0]?.longMessage || 'An error occurred')
    }
  }

  // Reset the user's password.
  // Upon successful reset, the user will be
  // signed in and redirected to the home page
  async function reset() {
    try {
      const result = await signIn?.attemptFirstFactor({
        strategy: 'reset_password_phone_code',
        code,
        password,
      })

      if (!result) return

      // Check if 2FA is required
      if (result.status === 'needs_second_factor') {
        setSecondFactor(true)
        setError('')
      } else if (result.status === 'complete') {
        // Set the active session to
        // the newly created session (user is now signed in)
        await setActive?.({
          session: result.createdSessionId,
          navigate: async ({ session }) => {
            if (session?.currentTask) {
              // Handle pending session tasks
              // See https://clerk.com/docs/guides/development/custom-flows/authentication/session-tasks
              console.log(session?.currentTask)
              return
            }

            router.replace('/')
          },
        })
        setError('')
      } else {
        console.log(result)
      }
    } catch (err: any) {
      console.error('error', err.errors?.[0]?.longMessage)
      setError(err.errors?.[0]?.longMessage || 'An error occurred')
    }
  }

  return (
    <ThemedView style={styles.container}>
      <ThemedText type="title" style={styles.title}>
        Forgot Password?
      </ThemedText>

      {!successfulCreation && (
        <>
          <ThemedText style={styles.label}>Provide your phone number</ThemedText>
          <TextInput
            style={styles.input}
            placeholder="e.g +1234567890"
            placeholderTextColor="#666666"
            value={phoneNumber}
            onChangeText={setPhoneNumber}
            keyboardType="phone-pad"
          />

          <Pressable
            style={({ pressed }) => [
              styles.button,
              !phoneNumber && styles.buttonDisabled,
              pressed && styles.buttonPressed,
            ]}
            onPress={create}
            disabled={!phoneNumber}
          >
            <ThemedText style={styles.buttonText}>Send password reset code</ThemedText>
          </Pressable>

          {error && (
            <View style={styles.errorContainer}>
              <ThemedText style={styles.errorText}>{error}</ThemedText>
            </View>
          )}
        </>
      )}

      {successfulCreation && !secondFactor && (
        <>
          <ThemedText style={styles.description}>
            A password reset code has been sent to your phone
          </ThemedText>

          <ThemedText style={styles.label}>Enter your new password</ThemedText>
          <TextInput
            style={styles.input}
            placeholder="Enter new password"
            placeholderTextColor="#666666"
            value={password}
            onChangeText={setPassword}
            secureTextEntry={true}
          />

          <ThemedText style={styles.label}>Enter the password reset code</ThemedText>
          <TextInput
            style={styles.input}
            placeholder="Enter code"
            placeholderTextColor="#666666"
            value={code}
            onChangeText={setCode}
            keyboardType="numeric"
          />

          <Pressable
            style={({ pressed }) => [
              styles.button,
              (!password || !code) && styles.buttonDisabled,
              pressed && styles.buttonPressed,
            ]}
            onPress={reset}
            disabled={!password || !code}
          >
            <ThemedText style={styles.buttonText}>Reset Password</ThemedText>
          </Pressable>

          {error && (
            <View style={styles.errorContainer}>
              <ThemedText style={styles.errorText}>{error}</ThemedText>
            </View>
          )}
        </>
      )}

      {secondFactor && (
        <View style={styles.warningContainer}>
          <ThemedText style={styles.warningText}>
            2FA is required, but this UI does not handle that yet
          </ThemedText>
        </View>
      )}
    </ThemedView>
  )
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    padding: 20,
    gap: 12,
  },
  title: {
    marginBottom: 8,
  },
  description: {
    fontSize: 14,
    marginBottom: 16,
    opacity: 0.8,
  },
  label: {
    fontWeight: '600',
    fontSize: 14,
  },
  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',
  },
  errorContainer: {
    padding: 12,
    backgroundColor: '#ffebee',
    borderRadius: 8,
    marginTop: 8,
  },
  errorText: {
    color: '#c62828',
    fontWeight: '500',
  },
  warningContainer: {
    padding: 12,
    backgroundColor: '#fff3e0',
    borderRadius: 8,
    marginTop: 8,
  },
  warningText: {
    color: '#e65100',
    fontWeight: '500',
  },
})
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.onFailure
import com.clerk.api.network.serialization.onSuccess
import com.clerk.api.signin.SignIn
import com.clerk.api.signin.attemptFirstFactor
import com.clerk.api.signin.resetPassword
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 {
      SignIn.create(SignIn.CreateParams.Strategy.ResetPasswordPhoneCode(identifier = phoneNumber))
        .onSuccess { updateStateFromStatus(it.status) }
        .onFailure {
          // See https://clerk.com/docs/guides/development/custom-flows/error-handling
          // for more info on error handling
          Log.e(
            ForgotPasswordPhoneViewModel::class.simpleName,
            it.errorMessage,
            it.throwable,
          )
        }
    }
  }

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

  fun setNewPassword(password: String) {
    val inProgressSignIn = Clerk.signIn ?: return
    viewModelScope.launch {
      inProgressSignIn
        .resetPassword(SignIn.ResetPasswordParams(password))
        .onSuccess { updateStateFromStatus(it.status) }
        .onFailure {
          // See https://clerk.com/docs/guides/development/custom-flows/error-handling
          // for more info on 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
        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) }
      }
  }

Examples for this SDK aren't available yet. For now, try switching to a supported SDK, such as Next.js, and converting the code to fit your SDK.

Handle compromised passwords

If you have enabled the Reject compromised passwords setting, sign-up/sign-in and password update attempts will be rejected with an error if the password is compromised.

User is trying to set a compromised password

If the user is trying to set a password that is compromised, the attempt will receive an HTTP status of 422 (Unprocessable Entity) and the form_password_pwned error code.

{
  "errors": [
    {
      "shortMessage": "Password has been found in an online data breach. For account safety, please <action>.",
      "code": "form_password_pwned",
      "meta": {
        "name": "param"
      }
    }
  ]
}

In this case, the user just needs to be prompted to use a different password. For example, you can add text to the form with the error's message so that the user can try again.

User's password has been marked as compromised

Warning

If your instance is older than December 18, 2025, you will need to to the Reset password session task update.

If you have manually marked a user's password as compromised, and the user tries authenticating with it, the sign-up/sign-in attempt will receive an HTTP status of 422 (Unprocessable Entity) and the form_password_compromised error code.

{
  "errors": [
    {
      "long_message": "Your password may be compromised. To protect your account, please continue with an alternative sign-in method. You will be required to reset your password after signing in.",
      "code": "form_password_compromised",
      "meta": {
        "name": "param"
      }
    }
  ]
}

The user will not be able to authenticate with their compromised password, so you should:

  1. Update your sign-up or sign-in flow to prompt them to authenticate with another method, such as an email address (so they can use email or email link), or a phone number (so they can use an SMS ). Once they authenticate with another method, until they reset their password. If they do not have any other identification methods, e.g if they only have username and password, they will be authenticated but until they reset their password.
  2. Handle the reset-password session task so the user can reset their password and their session can be updated from pending to active.

Update your sign-up/sign-in flow

To update your sign-up/sign-in flow, you can check for the error code and then prompt the user to authenticate with another method. This example uses the email/password custom flow but adds code that prompts the user to authenticate with an email code when their password is compromised. You can use the same approach with other custom flows.

This example is written for Next.js App Router but it can be adapted for any React-based framework, such as React Router or Tanstack React Start.

app/sign-in/page.tsx
'use client'

import * as React from 'react'
import { useSignIn } from '@clerk/nextjs'
import { useRouter } from 'next/navigation'
import { ClerkAPIError, EmailCodeFactor, SignInFirstFactor } from '@clerk/types'
import { isClerkAPIResponseError } from '@clerk/nextjs/errors'

const SignInWithEmailCode = () => {
  const { isLoaded, signIn, setActive } = useSignIn()
  const router = useRouter()

  const [verifying, setVerifying] = React.useState(false)
  const [email, setEmail] = React.useState('')
  const [code, setCode] = React.useState('')

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

    if (!isLoaded && !signIn) return null

    try {
      // Start the sign-in process using the email code method
      const { supportedFirstFactors } = await signIn.create({
        identifier: email,
      })

      // Filter the returned array to find the 'email_code' entry
      const isEmailCodeFactor = (factor: SignInFirstFactor): factor is EmailCodeFactor => {
        return factor.strategy === 'email_code'
      }
      const emailCodeFactor = supportedFirstFactors?.find(isEmailCodeFactor)

      if (emailCodeFactor) {
        // Grab the emailAddressId
        const { emailAddressId } = emailCodeFactor

        // Send the OTP code to the user
        await signIn.prepareFirstFactor({
          strategy: 'email_code',
          emailAddressId,
        })

        // Set verifying to true to display second form
        // and capture the OTP code
        setVerifying(true)
      }
    } catch (err) {
      // See https://clerk.com/docs/guides/development/custom-flows/error-handling
      // for more info on error handling
      console.error('Error:', JSON.stringify(err, null, 2))
    }
  }

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

    if (!isLoaded && !signIn) return null

    try {
      // Use the code provided by the user and attempt verification
      const signInAttempt = await signIn.attemptFirstFactor({
        strategy: 'email_code',
        code,
      })

      // If verification was completed, set the session to active
      // and redirect the user
      if (signInAttempt.status === 'complete') {
        await setActive({
          session: signInAttempt.createdSessionId,
          navigate: async ({ session }) => {
            if (session?.currentTask) {
              // Handle pending session tasks
              // See https://clerk.com/docs/guides/development/custom-flows/authentication/session-tasks
              console.log(session?.currentTask)
              return
            }

            router.push('/')
          },
        })
      } else {
        // If the status is not complete, check why. User may need to
        // complete further steps.
        console.error(signInAttempt)
      }
    } catch (err) {
      // See https://clerk.com/docs/guides/development/custom-flows/error-handling
      // for more info on error handling
      console.error('Error:', JSON.stringify(err, null, 2))
    }
  }

  if (verifying) {
    return (
      <>
        <h1>Verify your email address</h1>
        <form onSubmit={handleVerification}>
          <label htmlFor="code">Enter your email verification code</label>
          <input value={code} id="code" name="code" onChange={(e) => setCode(e.target.value)} />
          <button type="submit">Verify</button>
        </form>
      </>
    )
  }

  return (
    <>
      <form onSubmit={handleSubmit}>
        <label htmlFor="email">Enter email address</label>
        <input
          value={email}
          id="email"
          name="email"
          type="email"
          onChange={(e) => setEmail(e.target.value)}
        />
        <button type="submit">Continue</button>
      </form>
    </>
  )
}

export default function SignInForm() {
  const { isLoaded, signIn, setActive } = useSignIn()
  const router = useRouter()

  const [email, setEmail] = React.useState('')
  const [password, setPassword] = React.useState('')
  const [errors, setErrors] = React.useState<ClerkAPIError[]>()

  // Handle the submission of the sign-in form
  const handleSignInWithPassword = async (e: React.FormEvent) => {
    e.preventDefault()

    // Clear any errors that may have occurred during previous form submission
    setErrors(undefined)

    if (!isLoaded) {
      return
    }

    // Start the sign-in process using the email and password provided
    try {
      const signInAttempt = await signIn.create({
        identifier: email,
        password,
      })

      // If sign-in process is complete, set the created session as active
      // and redirect the user
      if (signInAttempt.status === 'complete') {
        await setActive({
          session: signInAttempt.createdSessionId,
          navigate: async ({ session }) => {
            if (session?.currentTask) {
              // Handle pending session tasks
              // See https://clerk.com/docs/guides/development/custom-flows/authentication/session-tasks
              console.log(session?.currentTask)
              return
            }

            router.push('/')
          },
        })
      } else {
        // If the status is not complete, check why. User may need to
        // complete further steps.
        console.error(JSON.stringify(signInAttempt, null, 2))
      }
    } catch (err) {
      if (isClerkAPIResponseError(err)) setErrors(err.errors)
      console.error(JSON.stringify(err, null, 2))
    }
  }

  if (errors && errors[0].code === 'form_password_compromised') {
    return (
      <>
        <h1>Sign in</h1>

        <p>
          Your password appears to have been compromised or it&apos;s no longer trusted and cannot
          be used. Please use email code to continue.
        </p>

        <SignInWithEmailCode />
      </>
    )
  }

  // Display a form to capture the user's email and password
  return (
    <>
      <h1>Sign in</h1>

      <form onSubmit={(e) => handleSignInWithPassword(e)}>
        <div>
          <label htmlFor="email">Enter email address</label>
          <input
            onChange={(e) => setEmail(e.target.value)}
            id="email"
            name="email"
            type="email"
            value={email}
          />
        </div>
        <div>
          <label htmlFor="password">Enter password</label>
          <input
            onChange={(e) => setPassword(e.target.value)}
            id="password"
            name="password"
            type="password"
            value={password}
          />
        </div>
        <button type="submit">Sign in</button>
      </form>

      {errors && (
        <ul>
          {errors.map((el, index) => (
            <li key={index}>{el.longMessage}</li>
          ))}
        </ul>
      )}
    </>
  )
}
app/(auth)/sign-in.tsx
import { ThemedText } from '@/components/themed-text'
import { ThemedView } from '@/components/themed-view'
import { isClerkAPIResponseError, useSignIn } from '@clerk/clerk-expo'
import { ClerkAPIError, EmailCodeFactor, SignInFirstFactor } from '@clerk/types'
import { Link, useRouter } from 'expo-router'
import * as React from 'react'
import { Pressable, StyleSheet, TextInput, View } from 'react-native'

const SignInWithEmailCode = () => {
  const { isLoaded, signIn, setActive } = useSignIn()
  const router = useRouter()

  const [verifying, setVerifying] = React.useState(false)
  const [email, setEmail] = React.useState('')
  const [code, setCode] = React.useState('')

  async function handleSubmit() {
    if (!isLoaded && !signIn) return

    try {
      // Start the sign-in process using the email code method
      const { supportedFirstFactors } = await signIn.create({
        identifier: email,
      })

      // Filter the returned array to find the 'email_code' entry
      const isEmailCodeFactor = (factor: SignInFirstFactor): factor is EmailCodeFactor => {
        return factor.strategy === 'email_code'
      }
      const emailCodeFactor = supportedFirstFactors?.find(isEmailCodeFactor)

      if (emailCodeFactor) {
        // Grab the emailAddressId
        const { emailAddressId } = emailCodeFactor

        // Send the OTP code to the user
        await signIn.prepareFirstFactor({
          strategy: 'email_code',
          emailAddressId,
        })

        // Set verifying to true to display second form
        // and capture the OTP code
        setVerifying(true)
      }
    } catch (err) {
      // See https://clerk.com/docs/guides/development/custom-flows/error-handling
      // for more info on error handling
      console.error('Error:', JSON.stringify(err, null, 2))
    }
  }

  async function handleVerification() {
    if (!isLoaded && !signIn) return

    try {
      // Use the code provided by the user and attempt verification
      const signInAttempt = await signIn.attemptFirstFactor({
        strategy: 'email_code',
        code,
      })

      // If verification was completed, set the session to active
      // and redirect the user
      if (signInAttempt.status === 'complete') {
        await setActive({
          session: signInAttempt.createdSessionId,
          navigate: async ({ session }) => {
            if (session?.currentTask) {
              // Handle pending session tasks
              // See https://clerk.com/docs/guides/development/custom-flows/authentication/session-tasks
              console.log(session?.currentTask)
              return
            }

            router.replace('/')
          },
        })
      } else {
        // If the status is not complete, check why. User may need to
        // complete further steps.
        console.error(signInAttempt)
      }
    } catch (err) {
      // See https://clerk.com/docs/guides/development/custom-flows/error-handling
      // for more info on error handling
      console.error('Error:', JSON.stringify(err, null, 2))
    }
  }

  if (verifying) {
    return (
      <ThemedView style={styles.container}>
        <ThemedText type="title" style={styles.title}>
          Verify your email address
        </ThemedText>
        <ThemedText style={styles.label}>Enter your email verification code</ThemedText>
        <TextInput
          style={styles.input}
          value={code}
          placeholder="Enter code"
          placeholderTextColor="#666666"
          onChangeText={setCode}
          keyboardType="numeric"
        />
        <Pressable
          style={({ pressed }) => [styles.button, pressed && styles.buttonPressed]}
          onPress={handleVerification}
        >
          <ThemedText style={styles.buttonText}>Verify</ThemedText>
        </Pressable>
      </ThemedView>
    )
  }

  return (
    <ThemedView style={styles.container}>
      <ThemedText style={styles.label}>Enter email address</ThemedText>
      <TextInput
        style={styles.input}
        value={email}
        placeholder="Enter email"
        placeholderTextColor="#666666"
        onChangeText={setEmail}
        autoCapitalize="none"
        keyboardType="email-address"
      />
      <Pressable
        style={({ pressed }) => [
          styles.button,
          !email && styles.buttonDisabled,
          pressed && styles.buttonPressed,
        ]}
        onPress={handleSubmit}
        disabled={!email}
      >
        <ThemedText style={styles.buttonText}>Continue</ThemedText>
      </Pressable>
    </ThemedView>
  )
}

export default function SignInForm() {
  const { isLoaded, signIn, setActive } = useSignIn()
  const router = useRouter()

  const [email, setEmail] = React.useState('')
  const [password, setPassword] = React.useState('')
  const [errors, setErrors] = React.useState<ClerkAPIError[]>()

  // Handle the submission of the sign-in form
  const handleSignInWithPassword = async () => {
    // Clear any errors that may have occurred during previous form submission
    setErrors(undefined)

    if (!isLoaded) {
      return
    }

    // Start the sign-in process using the email and password provided
    try {
      const signInAttempt = await signIn.create({
        identifier: email,
        password,
      })

      // If sign-in process is complete, set the created session as active
      // and redirect the user
      if (signInAttempt.status === 'complete') {
        await setActive({
          session: signInAttempt.createdSessionId,
          navigate: async ({ session }) => {
            if (session?.currentTask) {
              // Handle pending session tasks
              // See https://clerk.com/docs/guides/development/custom-flows/authentication/session-tasks
              console.log(session?.currentTask)
              return
            }

            router.replace('/')
          },
        })
      } else {
        // If the status is not complete, check why. User may need to
        // complete further steps.
        console.error(JSON.stringify(signInAttempt, null, 2))
      }
    } catch (err) {
      if (isClerkAPIResponseError(err)) setErrors(err.errors)
      console.error(JSON.stringify(err, null, 2))
    }
  }

  if (errors && errors[0].code === 'form_password_compromised') {
    return (
      <ThemedView style={styles.container}>
        <ThemedText type="title" style={styles.title}>
          Sign in
        </ThemedText>

        <View style={styles.warningContainer}>
          <ThemedText style={styles.warningText}>
            Your password appears to have been compromised or it's no longer trusted and cannot be
            used. Please use email code to continue.
          </ThemedText>
        </View>

        <SignInWithEmailCode />
      </ThemedView>
    )
  }

  // Display a form to capture the user's email and password
  return (
    <ThemedView style={styles.container}>
      <ThemedText type="title" style={styles.title}>
        Sign in
      </ThemedText>

      <ThemedText style={styles.label}>Email address</ThemedText>
      <TextInput
        style={styles.input}
        value={email}
        placeholder="Enter email"
        placeholderTextColor="#666666"
        onChangeText={setEmail}
        autoCapitalize="none"
        keyboardType="email-address"
      />

      <ThemedText style={styles.label}>Password</ThemedText>
      <TextInput
        style={styles.input}
        value={password}
        placeholder="Enter password"
        placeholderTextColor="#666666"
        onChangeText={setPassword}
        secureTextEntry={true}
      />

      <Pressable
        style={({ pressed }) => [
          styles.button,
          (!email || !password) && styles.buttonDisabled,
          pressed && styles.buttonPressed,
        ]}
        onPress={handleSignInWithPassword}
        disabled={!email || !password}
      >
        <ThemedText style={styles.buttonText}>Sign in</ThemedText>
      </Pressable>

      {errors && (
        <View style={styles.errorContainer}>
          {errors.map((el, index) => (
            <ThemedText key={index} style={styles.errorText}>
              • {el.longMessage}
            </ThemedText>
          ))}
        </View>
      )}

      <View style={styles.linkContainer}>
        <ThemedText>Don't have an account? </ThemedText>
        <Link href="/sign-up">
          <ThemedText type="link">Sign up</ThemedText>
        </Link>
      </View>
    </ThemedView>
  )
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    padding: 20,
    gap: 12,
  },
  title: {
    marginBottom: 8,
  },
  label: {
    fontWeight: '600',
    fontSize: 14,
  },
  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',
  },
  errorContainer: {
    padding: 12,
    backgroundColor: '#ffebee',
    borderRadius: 8,
    marginTop: 8,
    gap: 4,
  },
  errorText: {
    color: '#c62828',
    fontWeight: '500',
    fontSize: 14,
  },
  warningContainer: {
    padding: 12,
    backgroundColor: '#fff3e0',
    borderRadius: 8,
    marginBottom: 16,
  },
  warningText: {
    color: '#e65100',
    fontWeight: '500',
    fontSize: 14,
  },
  linkContainer: {
    flexDirection: 'row',
    gap: 4,
    marginTop: 12,
    alignItems: 'center',
  },
})

Handle the reset-password session task

Once a user has authenticated, their password is still considered compromised. The reset-password session task will cause the user's session to be in a pending state until they reset their password.

  1. First, you need to tell your app where to redirect users when they have pending session tasks. The taskUrls option allows you to specify custom URL paths where users are redirected after sign-up or sign-in when specific session tasks need to be completed.

    Configure the taskUrls option on the <ClerkProvider> component.

    <ClerkProvider
      taskUrls={{
        'choose-organization': '/session-tasks/choose-organization',
        'reset-password': '/session-tasks/reset-password',
      }}
    >
      {children}
    </ClerkProvider>

    Configure the taskUrls option on the clerk()Astro Icon integration.

    astro.config.mjs
    import { defineConfig } from 'astro/config'
    import node from '@astrojs/node'
    import clerk from '@clerk/astro'
    
    export default defineConfig({
      integrations: [
        clerk({
          taskUrls: {
            'choose-organization': '/session-tasks/choose-organization',
            'reset-password': '/session-tasks/reset-password',
          },
        }),
      ],
      adapter: node({ mode: 'standalone' }),
      output: 'server',
    })

    Configure the taskUrls option on the clerk.load() method.

    main.ts
    import { Clerk } from '@clerk/clerk-js'
    
    const clerkPubKey = import.meta.env.VITE_CLERK_PUBLISHABLE_KEY
    
    const clerk = new Clerk(clerkPubKey)
    await clerk.load({
      taskUrls: {
        'choose-organization': '/session-tasks/choose-organization',
        'reset-password': '/session-tasks/reset-password',
      },
    })

    Configure the taskUrls option on the clerkPlugin()Vue.js Icon integration.

    src/main.ts
    import { createApp } from 'vue'
    import './styles.css'
    import App from './App.vue'
    import { clerkPlugin } from '@clerk/vue'
    
    const PUBLISHABLE_KEY = import.meta.env.VITE_CLERK_PUBLISHABLE_KEY
    
    if (!PUBLISHABLE_KEY) {
      throw new Error('Add your Clerk publishable key to the .env.local file')
    }
    
    const app = createApp(App)
    app.use(clerkPlugin, {
      publishableKey: PUBLISHABLE_KEY,
      taskUrls: {
        'choose-organization': '/session-tasks/choose-organization',
        'reset-password': '/session-tasks/reset-password',
      },
    })
    app.mount('#app')

    Configure the taskUrls option on the defineNuxtConfig()Nuxt.js Icon integration.

    nuxt.config.ts
    export default defineNuxtConfig({
      compatibilityDate: '2025-07-15',
      devtools: { enabled: true },
      modules: ['@clerk/nuxt'],
      clerk: {
        taskUrls: {
          'choose-organization': '/session-tasks/choose-organization',
          'reset-password': '/session-tasks/reset-password',
        },
      },
    })

    Configure the taskUrls option on the clerkPlugin()Fastify Icon integration.

    src/main.ts
    import Fastify from 'fastify'
    import { clerkPlugin } from '@clerk/fastify'
    
    const fastify = Fastify({ logger: true })
    
    fastify.register(clerkPlugin, {
      taskUrls: {
        'choose-organization': '/session-tasks/choose-organization',
        'reset-password': '/session-tasks/reset-password',
      },
    })
  2. Now, the user will be redirected to the URL you've set with the taskUrls option. This page is where you will add the forgot/reset password code, such as the reset user's password with an email address example from the previous section.

  3. What if your user exits the authentication or session task flow before completing their tasks and doesn't know how to get to the appropriate page to complete their session tasks? What if your user is navigating through your app as a pending user and can't figure out why they can't access certain content? If a user's authentication or session task flow is interrupted and they aren't able to complete the tasks, you can use the <RedirectToTasks /> component to redirect them to the appropriate task page so they can complete the tasks and move their session to an active (signed-in) state. This component will redirect users based on the URL's you've set with the taskUrls option.

    In the following example, the <RedirectToTasks /> component is used to protect a page. Users can't access this page until they complete their pending session tasks. You can also wrap your entire application in the <RedirectToTasks /> component, or place it in your application's layout file, so that users can't access any of your app until they complete their pending session tasks.

    In the following example, the <RedirectToTasks /> component is used in the app's layout file so that users can't access any of the app until they complete their pending session tasks. However, you can also use the <RedirectToTasks /> component to protect a single page or route group.

    app/layout.tsx
    import { RedirectToTasks } from '@clerk/nextjs'
    
    export default function Layout({ children }: { children: React.ReactNode }) {
      return (
        <>
          <RedirectToTasks />
          {children}
        </>
      )
    }
    pages/index.tsx
    import { RedirectToTasks } from '@clerk/clerk-react'
    
    export default function Page() {
      return <RedirectToTasks />
    }
    app/routes/home.tsx
    import { RedirectToTasks } from '@clerk/react-router'
    
    export default function Home() {
      return <RedirectToTasks />
    }

    Note

    This component relies on React Router for navigation. Ensure that you have integrated React Router into your Chrome Extension application before using it. Learn how to add React Router to your Chrome Extension.

    src/routes/home.tsx
    import { RedirectToTasks } from '@clerk/chrome-extension'
    
    export default function Home() {
      return <RedirectToTasks />
    }
    app/routes/index.tsx
    import { RedirectToTasks } from '@clerk/tanstack-react-start'
    import { createFileRoute } from '@tanstack/react-router'
    
    export const Route = createFileRoute('/')({
      component: Home,
    })
    
    function Home() {
      return <RedirectToTasks />
    }
    App.vue
    <script setup lang="ts">
    import { RedirectToTasks } from '@clerk/vue'
    </script>
    
    <template>
      <RedirectToTasks />
    </template>
    App.vue
    <script setup lang="ts">
    // Components are automatically imported
    </script>
    
    <template>
      <RedirectToTasks />
    </template>

    This component is not available for your SDK. Please choose a different SDK.

Feedback

What did you think of this content?

Last updated on

GitHubEdit on GitHub