āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā ā š shadcn/directory/clerk/clerk-docs/guides/development/custom-flows/account-updates/forgot-password ā āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā
The password reset flow works as follows:
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:
{/* TODO: Add vanilla JS example. */}
<Tabs items={["Next.js", "iOS", "Android"]}> <Tab> ```tsx {{ filename: 'app/forgot-password.tsx', collapsible: true }} 'use client' import React, { useEffect, useState } from 'react' import { useAuth, useSignIn } from '@clerk/nextjs' import type { NextPage } from 'next' import { useRouter } from 'next/navigation'
const ForgotPasswordPage: NextPage = () => {
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('')
const router = useRouter()
const clerk = useClerk()
const { isSignedIn } = useAuth()
const { isLoaded, signIn, setActive } = useSignIn()
useEffect(() => {
if (isSignedIn) {
router.push('/')
}
}, [isSignedIn, router])
if (!isLoaded) {
return null
}
// 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) {
// Check for tasks and navigate to custom UI to help users resolve them
// See https://clerk.com/docs/guides/development/custom-flows/overview#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>
)
}
export default ForgotPasswordPage
```
</Tab>
<Tab>
```swift {{ filename: 'ForgotPasswordView.swift', collapsible: true }}
import SwiftUI
import Clerk
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.client?.signIn?.status {
case .needsFirstFactor:
TextField("Enter your code", text: $code)
Button("Verify") {
Task { await verify(code: code) }
}
case .needsSecondFactor:
Text("2FA is required, but this UI does not handle that")
case .needsNewPassword:
SecureField("New password", text: $newPassword)
Button("Set new password") {
Task { await setNewPassword(password: newPassword) }
}
default:
if let session = clerk.session {
Text("Active Session: \(session.id)")
} else {
TextField("Email", text: $email)
Button("Forgot password?") {
Task { await createSignIn(email: email) }
}
}
}
}
}
```
</Tab>
<Tab>
```kotlin {{ filename: 'ForgotPasswordEmailViewModel.kt', collapsible: true }}
import android.util.Log
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.clerk.api.Clerk
import com.clerk.api.network.serialization.longErrorMessageOrNull
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.longErrorMessageOrNull,
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.longErrorMessageOrNull,
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.longErrorMessageOrNull,
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
}
}
```
```kotlin {{ filename: 'ForgotPasswordEmailActivity.kt', collapsible: true }}
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.session?.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) }
}
}
```
</Tab>
</Tabs>
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.
{/* TODO: Add iOS and vanilla JS example. */}
<Tabs items={["Next.js", "Android"]}> <Tab> ```tsx {{ filename: 'app/forgot-password.tsx', collapsible: true }} 'use client' import React, { useState, useEffect } from 'react' import { useClerk, useAuth, useSignIn } from '@clerk/nextjs' import type { NextPage } from 'next' import { useRouter } from 'next/navigation'
const ForgotPasswordPage: NextPage = () => {
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('')
const router = useRouter()
const { isSignedIn } = useAuth()
const clerk = useClerk()
const { isLoaded, signIn, setActive } = useSignIn()
useEffect(() => {
if (isSignedIn) {
router.push('/')
}
}, [isSignedIn, router])
if (!isLoaded) {
return null
}
// 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) {
// Check for tasks and navigate to custom UI to help users resolve them
// See https://clerk.com/docs/guides/development/custom-flows/overview#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>
)
}
export default ForgotPasswordPage
```
</Tab>
<Tab>
```kotlin {{ filename: 'ForgotPasswordPhoneViewModel.kt', collapsible: true }}
import android.util.Log
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.clerk.api.Clerk
import com.clerk.api.network.serialization.longErrorMessageOrNull
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.longErrorMessageOrNull,
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.longErrorMessageOrNull,
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.longErrorMessageOrNull,
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
}
}
```
```kotlin {{ filename: 'ForgotPasswordPhoneActivity.kt', collapsible: true }}
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.session?.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) }
}
}
```
</Tab>
</Tabs>ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā