Build a custom forgot password flow
It's common for a sign-in flow to include a flow for users that forgot their password.
In Clerk, the forgot password flow requires that either Email or Phone is enabled, as the password reset code can only be delivered to one of these identifiers.
The forgot password flow works as follows:
- 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.
- Clerk sends an email or SMS to the user, containing a code.
- The user enters the code and a new password.
- Clerk verifies the code, and if successful, updates the user's password and signs them in.
- If
signOutOfOtherSessionsistrue, the user is signed out of all other authenticated sessions.
This guide demonstrates how to build a custom user interface for a forgot password flow that you can add to your existing sign-in flow. This guide covers the following scenarios:
'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)
// Step 1: Send the password reset code to the user's email
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)
}
// Step 2: Verify the code provided by the user
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
}
}
// Step 3: Submit the new password
async function submitNewPassword(e: React.FormEvent) {
e.preventDefault()
const { error } = await signIn.resetPasswordEmailCode.submitPassword({
password,
// Optional: sign the user out of all other authenticated sessions
signOutOfOtherSessions: true,
})
if (error) {
console.error(JSON.stringify(error, null, 2))
return
}
if (signIn.status === 'complete') {
const { error } = await signIn.finalize({
navigate: async ({ session, decorateUrl }) => {
// Handle session tasks
// See https://clerk.com/docs/guides/development/custom-flows/authentication/session-tasks
if (session?.currentTask) {
console.log(session.currentTask)
return
}
// If no session tasks, navigate the signed-in user to the home page
const url = decorateUrl('/')
if (url.startsWith('http')) {
window.location.href = url
} else {
router.push(url)
}
},
})
if (error) {
console.error(JSON.stringify(error, null, 2))
return
}
} else if (signIn.status === 'needs_second_factor') {
// See https://clerk.com/docs/guides/development/custom-flows/authentication/multi-factor-authentication
} else {
// Check why the sign-in is not complete
console.error('Sign-in attempt not complete:', signIn)
}
}
return (
<div>
<h1>Forgot Password?</h1>
{/* Step 1 UI: Collect the user's email so you can send them a password reset code */}
{!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>
)}
{/* Step 2 UI: Collect the code provided by the user */}
{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>
)}
{/* Step 3 UI: Collect the new password from the user */}
{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>
)}
{/* Step 4 UI: Handle 2FA, or other authentication requirements
depending on the settings you've enabled in the Clerk Dashboard.
This may require combining this flow with other custom flows. */}
{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>
)
}import { ThemedText } from '@/components/themed-text'
import { ThemedView } from '@/components/themed-view'
import { useSignIn } from '@clerk/expo'
import { type Href, Link, useRouter } from 'expo-router'
import React from 'react'
import { Pressable, StyleSheet, TextInput, View } from 'react-native'
export default function ForgotPassword() {
const { signIn, errors, fetchStatus } = useSignIn()
const router = useRouter()
const [emailAddress, setEmailAddress] = React.useState('')
const [code, setCode] = React.useState('')
const [password, setPassword] = React.useState('')
const [codeSent, setCodeSent] = React.useState(false)
// Step 1: Send the password reset code to the user's email
async function sendCode() {
const { error: createError } = await signIn.create({
identifier: emailAddress,
})
if (createError) {
console.error(JSON.stringify(createError, null, 2))
return
}
const { error: sendCodeError } = await signIn.resetPasswordEmailCode.sendCode()
if (sendCodeError) {
console.error(JSON.stringify(sendCodeError, null, 2))
return
}
setCodeSent(true)
}
// Step 2: Verify the code provided by the user
async function verifyCode() {
const { error } = await signIn.resetPasswordEmailCode.verifyCode({
code,
})
if (error) {
console.error(JSON.stringify(error, null, 2))
return
}
}
// Step 3: Submit the new password
async function submitNewPassword() {
const { error } = await signIn.resetPasswordEmailCode.submitPassword({
password,
// Optional: sign the user out of all other authenticated sessions
signOutOfOtherSessions: true,
})
if (error) {
console.error(JSON.stringify(error, null, 2))
return
}
if (signIn.status === 'complete') {
const { error } = await signIn.finalize({
navigate: async ({ session, decorateUrl }) => {
// Handle session tasks
// See https://clerk.com/docs/guides/development/custom-flows/authentication/session-tasks
if (session?.currentTask) {
console.log(session.currentTask)
return
}
// If no session tasks, navigate the signed-in user to the home page
const url = decorateUrl('/')
if (url.startsWith('http')) {
window.location.href = url
} else {
router.push(url as Href)
}
},
})
if (error) {
console.error(JSON.stringify(error, null, 2))
return
}
} else if (signIn.status === 'needs_second_factor') {
// See https://clerk.com/docs/guides/development/custom-flows/authentication/multi-factor-authentication
} else {
// Check why the sign-in is not complete
console.error('Sign-in attempt not complete:', signIn)
}
}
// Step 4 UI: Handle 2FA, or other authentication requirements
// depending on the settings you've enabled in the Clerk Dashboard.
// This may require combining this flow with other custom flows.
if (signIn.status === 'needs_second_factor') {
return (
<ThemedView style={styles.container}>
<ThemedText type="title" style={styles.title}>
Two-Factor Authentication Required
</ThemedText>
<ThemedText style={styles.message}>
2FA is required, but this UI does not handle that yet.
</ThemedText>
</ThemedView>
)
}
// Step 3 UI: Collect the new password from the user
if (signIn.status === 'needs_new_password') {
return (
<ThemedView style={styles.container}>
<ThemedText type="title" style={styles.title}>
Set New Password
</ThemedText>
<ThemedText style={styles.label}>Enter your new password</ThemedText>
<TextInput
style={styles.input}
value={password}
placeholder="Enter new password"
placeholderTextColor="#666666"
secureTextEntry={true}
onChangeText={(password) => setPassword(password)}
/>
{errors.fields.password && (
<ThemedText style={styles.error}>{errors.fields.password.message}</ThemedText>
)}
<Pressable
style={({ pressed }) => [
styles.button,
fetchStatus === 'fetching' && styles.buttonDisabled,
pressed && styles.buttonPressed,
]}
onPress={submitNewPassword}
disabled={fetchStatus === 'fetching'}
>
<ThemedText style={styles.buttonText}>Set new password</ThemedText>
</Pressable>
</ThemedView>
)
}
// Step 2 UI: Collect the code provided by the user
if (codeSent) {
return (
<ThemedView style={styles.container}>
<ThemedText type="title" style={styles.title}>
Verify Code
</ThemedText>
<ThemedText style={styles.label}>
Enter the password reset code sent to your email
</ThemedText>
<TextInput
style={styles.input}
value={code}
placeholder="Enter verification code"
placeholderTextColor="#666666"
onChangeText={(code) => setCode(code)}
keyboardType="numeric"
/>
{errors.fields.code && (
<ThemedText style={styles.error}>{errors.fields.code.message}</ThemedText>
)}
<Pressable
style={({ pressed }) => [
styles.button,
fetchStatus === 'fetching' && styles.buttonDisabled,
pressed && styles.buttonPressed,
]}
onPress={verifyCode}
disabled={fetchStatus === 'fetching'}
>
<ThemedText style={styles.buttonText}>Verify code</ThemedText>
</Pressable>
</ThemedView>
)
}
// Step 1 UI: Collect the user's email so you can send them a password reset code
return (
<ThemedView style={styles.container}>
<ThemedText type="title" style={styles.title}>
Forgot Password?
</ThemedText>
<ThemedText style={styles.label}>Email address</ThemedText>
<TextInput
style={styles.input}
autoCapitalize="none"
value={emailAddress}
placeholder="Enter email"
placeholderTextColor="#666666"
onChangeText={(emailAddress) => setEmailAddress(emailAddress)}
keyboardType="email-address"
/>
{errors.fields.identifier && (
<ThemedText style={styles.error}>{errors.fields.identifier.message}</ThemedText>
)}
<Pressable
style={({ pressed }) => [
styles.button,
(!emailAddress || fetchStatus === 'fetching') && styles.buttonDisabled,
pressed && styles.buttonPressed,
]}
onPress={sendCode}
disabled={!emailAddress || fetchStatus === 'fetching'}
>
<ThemedText style={styles.buttonText}>Send password reset code</ThemedText>
</Pressable>
{/* For your debugging purposes. You can just console.log errors,
but we put them in the UI for convenience */}
{errors && <ThemedText style={styles.debug}>{JSON.stringify(errors, null, 2)}</ThemedText>}
<View style={styles.linkContainer}>
<ThemedText>Remember your password? </ThemedText>
<Link href="/sign-in">
<ThemedText type="link">Sign in</ThemedText>
</Link>
</View>
</ThemedView>
)
}
const styles = StyleSheet.create({
container: {
flex: 1,
padding: 20,
gap: 12,
},
title: {
marginBottom: 8,
},
label: {
fontWeight: '600',
fontSize: 14,
},
message: {
fontSize: 14,
marginTop: 8,
},
input: {
borderWidth: 1,
borderColor: '#ccc',
borderRadius: 8,
padding: 12,
fontSize: 16,
backgroundColor: '#fff',
},
button: {
backgroundColor: '#0a7ea4',
paddingVertical: 12,
paddingHorizontal: 24,
borderRadius: 8,
alignItems: 'center',
marginTop: 8,
},
buttonPressed: {
opacity: 0.7,
},
buttonDisabled: {
opacity: 0.5,
},
buttonText: {
color: '#fff',
fontWeight: '600',
},
linkContainer: {
flexDirection: 'row',
gap: 4,
marginTop: 12,
alignItems: 'center',
},
error: {
color: '#d32f2f',
fontSize: 12,
marginTop: -8,
},
debug: {
fontSize: 10,
opacity: 0.5,
marginTop: 8,
},
}) 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:
// Collect the code provided by the user
TextField("Enter your code", text: $code)
Button("Verify") {
Task { await verify(code: code) }
}
// Handle 2FA, or other authentication requirements
// depending on the settings you've enabled in the Clerk Dashboard.
// This may require combining this flow with other custom flows.
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 you've verified the code sent to the user
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 they've successfully reset their password and signed in
Text("Signed in as: \(user.id)")
Button("Sign Out") {
Task { try await clerk.auth.signOut() }
}
} else {
// Collect the user's email so you can send them a password reset code
TextField("Email", text: $email)
Button("Forgot password?") {
Task { await startResetPassword(email: email) }
}
}
}
}
// Step 1: Send the password reset code to the user's 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)
}
}
// Step 2: Verify the code provided by the user
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)
}
}
// Step 3: Submit the new password provided by the user
private func setNewPassword(password: String) async {
do {
try await signIn?.resetPassword(
newPassword: password,
// Optional: sign the user out of all other authenticated sessions
signOutOfOtherSessions: true
)
// If verification was completed, the created session is available on `clerk.session` and UI shows signed in
} catch {
dump(error)
}
}
} 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)
}
// Step 1: Create a `SignIn` attempt and send a password reset code to the user's email
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,
)
}
}
}
// Step 2: Verify the code provided by the user
fun verify(code: String) {
val signIn = Clerk.auth.currentSignIn ?: 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,
)
}
}
}
// Step 3: Submit the new password provided by the user
fun setNewPassword(password: String) {
val signIn = Clerk.auth.currentSignIn ?: return
viewModelScope.launch {
signIn
.resetPassword(
newPassword = password,
// Optional: sign the user out of all other authenticated sessions
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,
)
}
}
}
// Update the UI state based on the `SignIn` status
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
}
} 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) {
// Step 1 UI: Collect the user's email so you can send them a password reset code
ForgotPasswordEmailViewModel.UiState.SignedOut -> {
InputContent(
placeholder = "Enter your email address",
buttonText = "Forgot password?",
onClick = onCreateSignIn,
)
}
// Step 2 UI: Collect the code provided by the user
ForgotPasswordEmailViewModel.UiState.NeedsFirstFactor -> {
InputContent(placeholder = "Enter your code", buttonText = "Verify", onClick = onVerify)
}
// Step 3 UI: Collect the new password from the user
ForgotPasswordEmailViewModel.UiState.NeedsNewPassword -> {
InputContent(
placeholder = "Enter your new password",
buttonText = "Set new password",
onClick = onSetNewPassword,
visualTransformation = PasswordVisualTransformation(),
)
}
// Step 4 UI (`SignIn.Status.NEEDS_SECOND_FACTOR`):
// Handle 2FA, or other authentication requirements
ForgotPasswordEmailViewModel.UiState.NeedsSecondFactor -> {
Text("2FA is required but this UI does not handle that")
}
// Step 4 UI (`SignIn.Status.COMPLETE`):
// Show the current user the active session ID
// after they've successfully reset their password and signed in
ForgotPasswordEmailViewModel.UiState.Complete -> {
Text("Active session: ${Clerk.activeSession?.id}")
}
// Show a loading state
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) }
}
}'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)
// Step 1: Send the password reset code to the user's phone number
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)
}
// Step 2: Verify the code provided by the user
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
}
}
// Step 3: Submit the new password provided by the user
async function submitNewPassword(e: React.FormEvent) {
e.preventDefault()
const { error } = await signIn.resetPasswordPhoneCode.submitPassword({
password,
// Optional: sign the user out of all other authenticated sessions
signOutOfOtherSessions: true,
})
if (error) {
console.error(JSON.stringify(error, null, 2))
return
}
if (signIn.status === 'complete') {
const { error } = await signIn.finalize({
navigate: async ({ session, decorateUrl }) => {
// Handle session tasks
// See https://clerk.com/docs/guides/development/custom-flows/authentication/session-tasks
if (session?.currentTask) {
console.log(session.currentTask)
return
}
// If no session tasks, navigate the signed-in user to the home page
const url = decorateUrl('/')
if (url.startsWith('http')) {
window.location.href = url
} else {
router.push(url)
}
},
})
if (error) {
console.error(JSON.stringify(error, null, 2))
return
}
} else if (signIn.status === 'needs_second_factor') {
// See https://clerk.com/docs/guides/development/custom-flows/authentication/multi-factor-authentication
} else {
// Check why the sign-in is not complete
console.error('Sign-in attempt not complete:', signIn)
}
}
return (
<div>
<h1>Forgot Password?</h1>
{/* Step 1 UI: Collect the user's phone number so you can send them a password reset code */}
{!codeSent && (
<form onSubmit={sendCode}>
<label htmlFor="phoneNumber">Provide your phone number</label>
<input
id="phoneNumber"
type="tel"
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>
)}
{/* Step 2 UI: Collect the code provided by the user */}
{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>
)}
{/* Step 3 UI: Collect the new password from the user */}
{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>
)}
{/* Step 4 UI: Handle 2FA, or other authentication requirements
depending on the settings you've enabled in the Clerk Dashboard.
This may require combining this flow with other custom flows. */}
{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>
)
}import { ThemedText } from '@/components/themed-text'
import { ThemedView } from '@/components/themed-view'
import { useSignIn } from '@clerk/expo'
import { type Href, Link, useRouter } from 'expo-router'
import React from 'react'
import { Pressable, StyleSheet, TextInput, View } from 'react-native'
export default function ForgotPassword() {
const { signIn, errors, fetchStatus } = useSignIn()
const router = useRouter()
const [phoneNumber, setPhoneNumber] = React.useState('')
const [code, setCode] = React.useState('')
const [password, setPassword] = React.useState('')
const [codeSent, setCodeSent] = React.useState(false)
// Step 1: Send the password reset code to the user's phone number
async function sendCode() {
const { error: createError } = await signIn.create({
identifier: phoneNumber,
})
if (createError) {
console.error(JSON.stringify(createError, null, 2))
return
}
const { error: sendCodeError } = await signIn.resetPasswordPhoneCode.sendCode()
if (sendCodeError) {
console.error(JSON.stringify(sendCodeError, null, 2))
return
}
setCodeSent(true)
}
// Step 2: Verify the code provided by the user
async function verifyCode() {
const { error } = await signIn.resetPasswordPhoneCode.verifyCode({
code,
})
if (error) {
console.error(JSON.stringify(error, null, 2))
return
}
}
// Step 3: Submit the new password provided by the user
async function submitNewPassword() {
const { error } = await signIn.resetPasswordPhoneCode.submitPassword({
password,
// Optional: sign the user out of all other authenticated sessions
signOutOfOtherSessions: true,
})
if (error) {
console.error(JSON.stringify(error, null, 2))
return
}
if (signIn.status === 'complete') {
const { error } = await signIn.finalize({
navigate: async ({ session, decorateUrl }) => {
// Handle session tasks
// See https://clerk.com/docs/guides/development/custom-flows/authentication/session-tasks
if (session?.currentTask) {
console.log(session.currentTask)
return
}
// If no session tasks, navigate the signed-in user to the home page
const url = decorateUrl('/')
if (url.startsWith('http')) {
window.location.href = url
} else {
router.push(url as Href)
}
},
})
if (error) {
console.error(JSON.stringify(error, null, 2))
return
}
} else if (signIn.status === 'needs_second_factor') {
// See https://clerk.com/docs/guides/development/custom-flows/authentication/multi-factor-authentication
} else {
// Check why the sign-in is not complete
console.error('Sign-in attempt not complete:', signIn)
}
}
// Step 4 UI: Handle 2FA, or other authentication requirements
// depending on the settings you've enabled in the Clerk Dashboard.
// This may require combining this flow with other custom flows.
if (signIn.status === 'needs_second_factor') {
return (
<ThemedView style={styles.container}>
<ThemedText type="title" style={styles.title}>
Two-Factor Authentication Required
</ThemedText>
<ThemedText style={styles.message}>
2FA is required, but this UI does not handle that yet.
</ThemedText>
</ThemedView>
)
}
// Step 3 UI: Collect the new password from the user
if (signIn.status === 'needs_new_password') {
return (
<ThemedView style={styles.container}>
<ThemedText type="title" style={styles.title}>
Set New Password
</ThemedText>
<ThemedText style={styles.label}>Enter your new password</ThemedText>
<TextInput
style={styles.input}
value={password}
placeholder="Enter new password"
placeholderTextColor="#666666"
secureTextEntry={true}
onChangeText={(password) => setPassword(password)}
/>
{errors.fields.password && (
<ThemedText style={styles.error}>{errors.fields.password.message}</ThemedText>
)}
<Pressable
style={({ pressed }) => [
styles.button,
fetchStatus === 'fetching' && styles.buttonDisabled,
pressed && styles.buttonPressed,
]}
onPress={submitNewPassword}
disabled={fetchStatus === 'fetching'}
>
<ThemedText style={styles.buttonText}>Set new password</ThemedText>
</Pressable>
</ThemedView>
)
}
// Step 2 UI: Collect the code provided by the user
if (codeSent) {
return (
<ThemedView style={styles.container}>
<ThemedText type="title" style={styles.title}>
Verify Code
</ThemedText>
<ThemedText style={styles.label}>
Enter the password reset code sent to your phone number
</ThemedText>
<TextInput
style={styles.input}
value={code}
placeholder="Enter verification code"
placeholderTextColor="#666666"
onChangeText={(code) => setCode(code)}
keyboardType="numeric"
/>
{errors.fields.code && (
<ThemedText style={styles.error}>{errors.fields.code.message}</ThemedText>
)}
<Pressable
style={({ pressed }) => [
styles.button,
fetchStatus === 'fetching' && styles.buttonDisabled,
pressed && styles.buttonPressed,
]}
onPress={verifyCode}
disabled={fetchStatus === 'fetching'}
>
<ThemedText style={styles.buttonText}>Verify code</ThemedText>
</Pressable>
</ThemedView>
)
}
// Step 1 UI: Collect the user's phone number so you can send them a password reset code
return (
<ThemedView style={styles.container}>
<ThemedText type="title" style={styles.title}>
Forgot Password?
</ThemedText>
<ThemedText style={styles.label}>Phone number</ThemedText>
<TextInput
style={styles.input}
autoCapitalize="none"
value={phoneNumber}
placeholder="Enter phone number"
placeholderTextColor="#666666"
onChangeText={(phoneNumber) => setPhoneNumber(phoneNumber)}
keyboardType="phone-pad"
/>
{errors.fields.identifier && (
<ThemedText style={styles.error}>{errors.fields.identifier.message}</ThemedText>
)}
<Pressable
style={({ pressed }) => [
styles.button,
(!phoneNumber || fetchStatus === 'fetching') && styles.buttonDisabled,
pressed && styles.buttonPressed,
]}
onPress={sendCode}
disabled={!phoneNumber || fetchStatus === 'fetching'}
>
<ThemedText style={styles.buttonText}>Send password reset code</ThemedText>
</Pressable>
{/* For your debugging purposes. You can just console.log errors, but we put them in the UI for convenience */}
{errors && <ThemedText style={styles.debug}>{JSON.stringify(errors, null, 2)}</ThemedText>}
<View style={styles.linkContainer}>
<ThemedText>Remember your password? </ThemedText>
<Link href="/sign-in">
<ThemedText type="link">Sign in</ThemedText>
</Link>
</View>
</ThemedView>
)
}
const styles = StyleSheet.create({
container: {
flex: 1,
padding: 20,
gap: 12,
},
title: {
marginBottom: 8,
},
label: {
fontWeight: '600',
fontSize: 14,
},
message: {
fontSize: 14,
marginTop: 8,
},
input: {
borderWidth: 1,
borderColor: '#ccc',
borderRadius: 8,
padding: 12,
fontSize: 16,
backgroundColor: '#fff',
},
button: {
backgroundColor: '#0a7ea4',
paddingVertical: 12,
paddingHorizontal: 24,
borderRadius: 8,
alignItems: 'center',
marginTop: 8,
},
buttonPressed: {
opacity: 0.7,
},
buttonDisabled: {
opacity: 0.5,
},
buttonText: {
color: '#fff',
fontWeight: '600',
},
linkContainer: {
flexDirection: 'row',
gap: 4,
marginTop: 12,
alignItems: 'center',
},
error: {
color: '#d32f2f',
fontSize: 12,
marginTop: -8,
},
debug: {
fontSize: 10,
opacity: 0.5,
marginTop: 8,
},
}) 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:
// Step 2 UI: Collect the code provided by the user
TextField("Enter your code", text: $code)
Button("Verify") {
Task { await verify(code: code) }
}
// Step 3 UI: Collect the new password from the user
case .needsNewPassword:
// Set the new password after verification
SecureField("New password", text: $newPassword)
Button("Set new password") {
Task { await setNewPassword(password: newPassword) }
}
// Step 4 UI (`SignIn.Status.NEEDS_SECOND_FACTOR`):
// Handle 2FA, or other authentication requirements
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")
default:
if let user = clerk.user {
// Step 4 UI (`SignIn.Status.COMPLETE`):
// Show the current user after they've successfully reset their password and signed in
Text("Signed in as: \(user.id)")
Button("Sign Out") {
Task { try await clerk.auth.signOut() }
}
} else {
// Step 1 UI: Collect the user's phone number so you can send them a password reset code
TextField("Phone number", text: $phoneNumber)
Button("Forgot password?") {
Task { await startResetPassword(phoneNumber: phoneNumber) }
}
}
}
}
// Step 1: Create a `SignIn` attempt and send a password reset code to the user's phone number
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)
}
}
// Step 2: Verify the code provided by the user
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)
}
}
// Step 3: Submit the new password provided by the user
private func setNewPassword(password: String) async {
do {
try await signIn?.resetPassword(
newPassword: password,
// Optional: sign the user out of all other authenticated sessions
signOutOfOtherSessions: true
)
// If verification was completed, the created session is available on `clerk.session` and UI shows signed in
} catch {
dump(error)
}
}
}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)
}
// Step 1: Create a `SignIn` attempt and send a password reset code to the user's phone number
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,
)
}
}
}
// Step 2: Verify the code provided by the user
fun verify(code: String) {
val signIn = Clerk.auth.currentSignIn ?: 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,
)
}
}
}
// Step 3: Submit the new password provided by the user
fun setNewPassword(password: String) {
val signIn = Clerk.auth.currentSignIn ?: return
viewModelScope.launch {
signIn
.resetPassword(
newPassword = password,
// Optional: sign the user out of all other authenticated sessions
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,
)
}
}
}
// Update the UI state based on the `SignIn` status
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
}
} 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) {
// Step 1 UI: Collect the user's phone number so you can send them a password reset code
ForgotPasswordPhoneViewModel.UiState.SignedOut -> {
InputContent(
placeholder = "Enter your phone number",
buttonText = "Forgot password?",
onClick = onCreateSignIn,
)
}
// Step 2 UI: Collect the code provided by the user
ForgotPasswordPhoneViewModel.UiState.NeedsFirstFactor -> {
InputContent(placeholder = "Enter your code", buttonText = "Verify", onClick = onVerify)
}
// Step 3 UI: Collect the new password from the user
ForgotPasswordPhoneViewModel.UiState.NeedsNewPassword -> {
InputContent(
placeholder = "Enter your new password",
buttonText = "Set new password",
onClick = onSetNewPassword,
visualTransformation = PasswordVisualTransformation(),
)
}
// Step 4 UI (`SignIn.Status.NEEDS_SECOND_FACTOR`):
// Handle 2FA, or other authentication requirements
ForgotPasswordPhoneViewModel.UiState.NeedsSecondFactor -> {
Text("2FA is required but this UI does not handle that")
}
// Step 4 UI (`SignIn.Status.COMPLETE`):
// Show the current user the active session ID
// after they've successfully reset their password and signed in
ForgotPasswordPhoneViewModel.UiState.Complete -> {
Text("Active session: ${Clerk.activeSession?.id}")
}
// Show a loading state
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
Last updated on