Build a custom flow for resetting a user's password
The password reset 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.
This guide demonstrates how to use Clerk's API to build a custom flow for resetting a user's password. It covers the following scenarios:
Email address
'use client'
import React from 'react'
import { useSignIn } from '@clerk/nextjs'
import { useRouter } from 'next/navigation'
export default function ForgotPassword() {
const { signIn, errors, fetchStatus } = useSignIn()
const router = useRouter()
const [emailAddress, setEmailAddress] = React.useState('')
const [code, setCode] = React.useState('')
const [password, setPassword] = React.useState('')
const [codeSent, setCodeSent] = React.useState(false)
async function sendCode(e: React.FormEvent) {
e.preventDefault()
const { error: createError } = await signIn.create({
identifier: emailAddress,
})
if (createError) {
console.error(JSON.stringify(createError, null, 2))
return
}
const { error: sendCodeError } = await signIn.resetPasswordEmailCode.sendCode()
if (sendCodeError) {
console.error(JSON.stringify(sendCodeError, null, 2))
return
}
setCodeSent(true)
}
async function verifyCode(e: React.FormEvent) {
e.preventDefault()
const { error } = await signIn.resetPasswordEmailCode.verifyCode({
code,
})
if (error) {
console.error(JSON.stringify(error, null, 2))
return
}
}
async function submitNewPassword(e: React.FormEvent) {
e.preventDefault()
const { error } = await signIn.resetPasswordEmailCode.submitPassword({
password,
})
if (error) {
console.error(JSON.stringify(error, null, 2))
return
}
if (signIn.status === 'needs_second_factor') {
// See https://clerk.com/docs/guides/development/custom-flows/authentication/multi-factor-authentication
}
if (signIn.status === 'complete') {
const { error } = await signIn.finalize({
navigate: async ({ session, decorateUrl }) => {
if (session?.currentTask) {
// Handle session tasks
// See https://clerk.com/docs/guides/development/custom-flows/authentication/session-tasks
console.log(session.currentTask)
return
}
const url = decorateUrl('/')
if (url.startsWith('http')) {
window.location.href = url
} else {
router.push(url)
}
},
})
if (error) {
console.error(JSON.stringify(error, null, 2))
return
}
}
}
return (
<div>
<h1>Forgot Password?</h1>
{!codeSent && (
<form onSubmit={sendCode}>
<label htmlFor="emailAddress">Provide your email address</label>
<input
id="emailAddress"
type="email"
placeholder="e.g john@doe.com"
value={emailAddress}
onChange={(e) => setEmailAddress(e.target.value)}
/>
{errors.fields.identifier && <p>{errors.fields.identifier.message}</p>}
<button type="submit" disabled={fetchStatus === 'fetching'}>
Send password reset code
</button>
</form>
)}
{codeSent && signIn.status !== 'needs_new_password' && (
<form onSubmit={verifyCode}>
<label htmlFor="code">Enter the password reset code that was sent to your email</label>
<input id="code" type="text" value={code} onChange={(e) => setCode(e.target.value)} />
{errors.fields.code && <p>{errors.fields.code.message}</p>}
<button type="submit" disabled={fetchStatus === 'fetching'}>
Verify code
</button>
</form>
)}
{signIn.status === 'needs_new_password' && (
<form onSubmit={submitNewPassword}>
<label htmlFor="password">Enter your new password</label>
<input
id="password"
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
{errors.fields.password && <p>{errors.fields.password.message}</p>}
<button type="submit" disabled={fetchStatus === 'fetching'}>
Set new password
</button>
</form>
)}
{signIn.status === 'needs_second_factor' && (
<p>2FA is required, but this UI does not handle that.</p>
)}
{/* For your debugging purposes. You can just console.log errors, but we put them in the UI for convenience */}
{errors && <pre>{JSON.stringify(errors, null, 2)}</pre>}
</div>
)
}import { ThemedText } from '@/components/themed-text'
import { ThemedView } from '@/components/themed-view'
import { useSignIn } from '@clerk/expo'
import { type Href, Link, useRouter } from 'expo-router'
import React from 'react'
import { Pressable, StyleSheet, TextInput, View } from 'react-native'
export default function ForgotPassword() {
const { signIn, errors, fetchStatus } = useSignIn()
const router = useRouter()
const [emailAddress, setEmailAddress] = React.useState('')
const [code, setCode] = React.useState('')
const [password, setPassword] = React.useState('')
const [codeSent, setCodeSent] = React.useState(false)
async function sendCode() {
const { error: createError } = await signIn.create({
identifier: emailAddress,
})
if (createError) {
console.error(JSON.stringify(createError, null, 2))
return
}
const { error: sendCodeError } = await signIn.resetPasswordEmailCode.sendCode()
if (sendCodeError) {
console.error(JSON.stringify(sendCodeError, null, 2))
return
}
setCodeSent(true)
}
async function verifyCode() {
const { error } = await signIn.resetPasswordEmailCode.verifyCode({
code,
})
if (error) {
console.error(JSON.stringify(error, null, 2))
return
}
}
async function submitNewPassword() {
const { error } = await signIn.resetPasswordEmailCode.submitPassword({
password,
})
if (error) {
console.error(JSON.stringify(error, null, 2))
return
}
if (signIn.status === 'needs_second_factor') {
// See https://clerk.com/docs/guides/development/custom-flows/authentication/multi-factor-authentication
return
}
if (signIn.status === 'complete') {
const { error } = await signIn.finalize({
navigate: async ({ session, decorateUrl }) => {
if (session?.currentTask) {
// Handle session tasks
// See https://clerk.com/docs/guides/development/custom-flows/authentication/session-tasks
console.log(session.currentTask)
return
}
const url = decorateUrl('/')
if (url.startsWith('http')) {
window.location.href = url
} else {
router.push(url as Href)
}
},
})
if (error) {
console.error(JSON.stringify(error, null, 2))
return
}
}
}
if (signIn.status === 'needs_second_factor') {
return (
<ThemedView style={styles.container}>
<ThemedText type="title" style={styles.title}>
Two-Factor Authentication Required
</ThemedText>
<ThemedText style={styles.message}>
2FA is required, but this UI does not handle that yet.
</ThemedText>
</ThemedView>
)
}
if (signIn.status === 'needs_new_password') {
return (
<ThemedView style={styles.container}>
<ThemedText type="title" style={styles.title}>
Set New Password
</ThemedText>
<ThemedText style={styles.label}>Enter your new password</ThemedText>
<TextInput
style={styles.input}
value={password}
placeholder="Enter new password"
placeholderTextColor="#666666"
secureTextEntry={true}
onChangeText={(password) => setPassword(password)}
/>
{errors.fields.password && (
<ThemedText style={styles.error}>{errors.fields.password.message}</ThemedText>
)}
<Pressable
style={({ pressed }) => [
styles.button,
fetchStatus === 'fetching' && styles.buttonDisabled,
pressed && styles.buttonPressed,
]}
onPress={submitNewPassword}
disabled={fetchStatus === 'fetching'}
>
<ThemedText style={styles.buttonText}>Set new password</ThemedText>
</Pressable>
</ThemedView>
)
}
if (codeSent) {
return (
<ThemedView style={styles.container}>
<ThemedText type="title" style={styles.title}>
Verify Code
</ThemedText>
<ThemedText style={styles.label}>
Enter the password reset code sent to your email
</ThemedText>
<TextInput
style={styles.input}
value={code}
placeholder="Enter verification code"
placeholderTextColor="#666666"
onChangeText={(code) => setCode(code)}
keyboardType="numeric"
/>
{errors.fields.code && (
<ThemedText style={styles.error}>{errors.fields.code.message}</ThemedText>
)}
<Pressable
style={({ pressed }) => [
styles.button,
fetchStatus === 'fetching' && styles.buttonDisabled,
pressed && styles.buttonPressed,
]}
onPress={verifyCode}
disabled={fetchStatus === 'fetching'}
>
<ThemedText style={styles.buttonText}>Verify code</ThemedText>
</Pressable>
</ThemedView>
)
}
return (
<ThemedView style={styles.container}>
<ThemedText type="title" style={styles.title}>
Forgot Password?
</ThemedText>
<ThemedText style={styles.label}>Email address</ThemedText>
<TextInput
style={styles.input}
autoCapitalize="none"
value={emailAddress}
placeholder="Enter email"
placeholderTextColor="#666666"
onChangeText={(emailAddress) => setEmailAddress(emailAddress)}
keyboardType="email-address"
/>
{errors.fields.identifier && (
<ThemedText style={styles.error}>{errors.fields.identifier.message}</ThemedText>
)}
<Pressable
style={({ pressed }) => [
styles.button,
(!emailAddress || fetchStatus === 'fetching') && styles.buttonDisabled,
pressed && styles.buttonPressed,
]}
onPress={sendCode}
disabled={!emailAddress || fetchStatus === 'fetching'}
>
<ThemedText style={styles.buttonText}>Send password reset code</ThemedText>
</Pressable>
{/* For your debugging purposes. You can just console.log errors, but we put them in the UI for convenience */}
{errors && <ThemedText style={styles.debug}>{JSON.stringify(errors, null, 2)}</ThemedText>}
<View style={styles.linkContainer}>
<ThemedText>Remember your password? </ThemedText>
<Link href="/sign-in">
<ThemedText type="link">Sign in</ThemedText>
</Link>
</View>
</ThemedView>
)
}
const styles = StyleSheet.create({
container: {
flex: 1,
padding: 20,
gap: 12,
},
title: {
marginBottom: 8,
},
label: {
fontWeight: '600',
fontSize: 14,
},
message: {
fontSize: 14,
marginTop: 8,
},
input: {
borderWidth: 1,
borderColor: '#ccc',
borderRadius: 8,
padding: 12,
fontSize: 16,
backgroundColor: '#fff',
},
button: {
backgroundColor: '#0a7ea4',
paddingVertical: 12,
paddingHorizontal: 24,
borderRadius: 8,
alignItems: 'center',
marginTop: 8,
},
buttonPressed: {
opacity: 0.7,
},
buttonDisabled: {
opacity: 0.5,
},
buttonText: {
color: '#fff',
fontWeight: '600',
},
linkContainer: {
flexDirection: 'row',
gap: 4,
marginTop: 12,
alignItems: 'center',
},
error: {
color: '#d32f2f',
fontSize: 12,
marginTop: -8,
},
debug: {
fontSize: 10,
opacity: 0.5,
marginTop: 8,
},
}) import SwiftUI
import ClerkKit
struct ForgotPasswordView: View {
@Environment(Clerk.self) private var clerk
@State private var email = ""
@State private var code = ""
@State private var newPassword = ""
var signIn: SignIn? {
clerk.auth.currentSignIn
}
var body: some View {
switch signIn?.status {
case .needsFirstFactor:
// Verify the reset code
TextField("Enter your code", text: $code)
Button("Verify") {
Task { await verify(code: code) }
}
case .needsSecondFactor:
// See https://clerk.com/docs/guides/development/custom-flows/authentication/multi-factor-authentication
Text("2FA is required, but this UI does not handle that")
case .needsNewPassword:
// Set the new password after verification
SecureField("New password", text: $newPassword)
Button("Set new password") {
Task { await setNewPassword(password: newPassword) }
}
default:
if let user = clerk.user {
// Show the current user after a successful reset
Text("Signed in as: \(user.id)")
Button("Sign Out") {
Task { try await clerk.auth.signOut() }
}
} else {
// Start the reset flow by creating a sign-in attempt
TextField("Email", text: $email)
Button("Forgot password?") {
Task { await startResetPassword(email: email) }
}
}
}
}
private func startResetPassword(email: String) async {
do {
try await clerk.auth.signIn(email)
try await signIn?.sendResetPasswordEmailCode()
// Clerk updates currentSignIn and UI shows code field (needsFirstFactor)
} catch {
dump(error)
}
}
private func verify(code: String) async {
do {
try await signIn?.verifyCode(code)
// Clerk updates currentSignIn and UI shows new password field (needsNewPassword)
} catch {
dump(error)
}
}
private func setNewPassword(password: String) async {
do {
try await signIn?.resetPassword(
newPassword: password,
signOutOfOtherSessions: true
)
// If verification was completed, the created session is available on `clerk.session` and UI shows signed in
} catch {
dump(error)
}
}
} import android.util.Log
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.clerk.api.Clerk
import com.clerk.api.network.serialization.errorMessage
import com.clerk.api.network.serialization.flatMap
import com.clerk.api.network.serialization.onFailure
import com.clerk.api.network.serialization.onSuccess
import com.clerk.api.signin.SignIn
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.launch
class ForgotPasswordEmailViewModel : ViewModel() {
private val _uiState = MutableStateFlow<UiState>(UiState.Loading)
val uiState = _uiState.asStateFlow()
init {
combine(Clerk.isInitialized, Clerk.userFlow) { isInitialized, user ->
_uiState.value =
when {
!isInitialized -> UiState.Loading
user != null -> UiState.Complete
else -> UiState.SignedOut
}
}
.launchIn(viewModelScope)
}
fun createSignIn(email: String) {
viewModelScope.launch {
Clerk.auth
.signIn { this.email = email }
.flatMap { signIn ->
signIn.sendResetPasswordCode { this.email = email }
}
.onSuccess { updateStateFromStatus(it.status) }
.onFailure {
// See https://clerk.com/docs/guides/development/custom-flows/error-handling
Log.e(
ForgotPasswordEmailViewModel::class.simpleName,
it.errorMessage,
it.throwable,
)
}
}
}
fun verify(code: String) {
val signIn = Clerk.signIn ?: return
viewModelScope.launch {
signIn
.verifyCode(code)
.onSuccess { updateStateFromStatus(it.status) }
.onFailure {
// See https://clerk.com/docs/guides/development/custom-flows/error-handling
Log.e(
ForgotPasswordEmailViewModel::class.simpleName,
it.errorMessage,
it.throwable,
)
}
}
}
fun setNewPassword(password: String) {
val signIn = Clerk.signIn ?: return
viewModelScope.launch {
signIn
.resetPassword(
newPassword = password,
signOutOfOtherSessions = true,
)
.onSuccess { updated ->
updateStateFromStatus(updated.status)
if (updated.status == SignIn.Status.COMPLETE) {
updated.finalize()
}
}
.onFailure {
// See https://clerk.com/docs/guides/development/custom-flows/error-handling
Log.e(
ForgotPasswordEmailViewModel::class.simpleName,
it.errorMessage,
it.throwable,
)
}
}
}
fun updateStateFromStatus(status: SignIn.Status) {
val state =
when (status) {
SignIn.Status.COMPLETE -> UiState.Complete
SignIn.Status.NEEDS_FIRST_FACTOR -> UiState.NeedsFirstFactor
SignIn.Status.NEEDS_SECOND_FACTOR -> UiState.NeedsSecondFactor // See https://clerk.com/docs/guides/development/custom-flows/authentication/multi-factor-authentication
SignIn.Status.NEEDS_NEW_PASSWORD -> UiState.NeedsNewPassword
else -> {
UiState.SignedOut
}
}
_uiState.value = state
}
sealed interface UiState {
data object Loading : UiState
data object SignedOut : UiState
data object NeedsFirstFactor : UiState
data object NeedsSecondFactor : UiState
data object NeedsNewPassword : UiState
data object Complete : UiState
}
} 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) }
}
}'use client'
import React from 'react'
import { useSignIn } from '@clerk/nextjs'
import { useRouter } from 'next/navigation'
export default function ForgotPassword() {
const { signIn, errors, fetchStatus } = useSignIn()
const router = useRouter()
const [phoneNumber, setPhoneNumber] = React.useState('')
const [code, setCode] = React.useState('')
const [password, setPassword] = React.useState('')
const [codeSent, setCodeSent] = React.useState(false)
async function sendCode(e: React.FormEvent) {
e.preventDefault()
const { error: createError } = await signIn.create({
identifier: phoneNumber,
})
if (createError) {
console.error(JSON.stringify(createError, null, 2))
return
}
const { error: sendCodeError } = await signIn.resetPasswordPhoneCode.sendCode()
if (sendCodeError) {
console.error(JSON.stringify(sendCodeError, null, 2))
return
}
setCodeSent(true)
}
async function verifyCode(e: React.FormEvent) {
e.preventDefault()
const { error } = await signIn.resetPasswordPhoneCode.verifyCode({
code,
})
if (error) {
console.error(JSON.stringify(error, null, 2))
return
}
}
async function submitNewPassword(e: React.FormEvent) {
e.preventDefault()
const { error } = await signIn.resetPasswordPhoneCode.submitPassword({
password,
})
if (error) {
console.error(JSON.stringify(error, null, 2))
return
}
if (signIn.status === 'needs_second_factor') {
// See https://clerk.com/docs/guides/development/custom-flows/authentication/multi-factor-authentication
return
}
if (signIn.status === 'complete') {
const { error } = await signIn.finalize({
navigate: async ({ session, decorateUrl }) => {
if (session?.currentTask) {
// Handle session tasks
// See https://clerk.com/docs/guides/development/custom-flows/authentication/session-tasks
console.log(session.currentTask)
return
}
const url = decorateUrl('/')
if (url.startsWith('http')) {
window.location.href = url
} else {
router.push(url)
}
},
})
if (error) {
console.error(JSON.stringify(error, null, 2))
return
}
}
}
return (
<div>
<h1>Forgot Password?</h1>
{!codeSent && (
<form onSubmit={sendCode}>
<label htmlFor="phoneNumber">Provide your phone number</label>
<input
id="phoneNumber"
type="phoneNumber"
placeholder="e.g +1234567890"
value={phoneNumber}
onChange={(e) => setPhoneNumber(e.target.value)}
/>
{errors.fields.identifier && <p>{errors.fields.identifier.message}</p>}
<button type="submit" disabled={fetchStatus === 'fetching'}>
Send password reset code
</button>
</form>
)}
{codeSent && signIn.status !== 'needs_new_password' && (
<form onSubmit={verifyCode}>
<label htmlFor="code">
Enter the password reset code that was sent to your phone number
</label>
<input id="code" type="text" value={code} onChange={(e) => setCode(e.target.value)} />
{errors.fields.code && <p>{errors.fields.code.message}</p>}
<button type="submit" disabled={fetchStatus === 'fetching'}>
Verify code
</button>
</form>
)}
{signIn.status === 'needs_new_password' && (
<form onSubmit={submitNewPassword}>
<label htmlFor="password">Enter your new password</label>
<input
id="password"
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
{errors.fields.password && <p>{errors.fields.password.message}</p>}
<button type="submit" disabled={fetchStatus === 'fetching'}>
Set new password
</button>
</form>
)}
{signIn.status === 'needs_second_factor' && (
<p>2FA is required, but this UI does not handle that.</p>
)}
{/* For your debugging purposes. You can just console.log errors, but we put them in the UI for convenience */}
{errors && <pre>{JSON.stringify(errors, null, 2)}</pre>}
</div>
)
}import { ThemedText } from '@/components/themed-text'
import { ThemedView } from '@/components/themed-view'
import { useSignIn } from '@clerk/expo'
import { type Href, Link, useRouter } from 'expo-router'
import React from 'react'
import { Pressable, StyleSheet, TextInput, View } from 'react-native'
export default function ForgotPassword() {
const { signIn, errors, fetchStatus } = useSignIn()
const router = useRouter()
const [phoneNumber, setPhoneNumber] = React.useState('')
const [code, setCode] = React.useState('')
const [password, setPassword] = React.useState('')
const [codeSent, setCodeSent] = React.useState(false)
async function sendCode() {
const { error: createError } = await signIn.create({
identifier: phoneNumber,
})
if (createError) {
console.error(JSON.stringify(createError, null, 2))
return
}
const { error: sendCodeError } = await signIn.resetPasswordPhoneCode.sendCode()
if (sendCodeError) {
console.error(JSON.stringify(sendCodeError, null, 2))
return
}
setCodeSent(true)
}
async function verifyCode() {
const { error } = await signIn.resetPasswordPhoneCode.verifyCode({
code,
})
if (error) {
console.error(JSON.stringify(error, null, 2))
return
}
}
async function submitNewPassword() {
const { error } = await signIn.resetPasswordPhoneCode.submitPassword({
password,
})
if (error) {
console.error(JSON.stringify(error, null, 2))
return
}
if (signIn.status === 'needs_second_factor') {
// See https://clerk.com/docs/guides/development/custom-flows/authentication/multi-factor-authentication
return
}
if (signIn.status === 'complete') {
const { error } = await signIn.finalize({
navigate: async ({ session, decorateUrl }) => {
if (session?.currentTask) {
// Handle session tasks
// See https://clerk.com/docs/guides/development/custom-flows/authentication/session-tasks
console.log(session.currentTask)
return
}
const url = decorateUrl('/')
if (url.startsWith('http')) {
window.location.href = url
} else {
router.push(url as Href)
}
},
})
if (error) {
console.error(JSON.stringify(error, null, 2))
return
}
}
}
if (signIn.status === 'needs_second_factor') {
return (
<ThemedView style={styles.container}>
<ThemedText type="title" style={styles.title}>
Two-Factor Authentication Required
</ThemedText>
<ThemedText style={styles.message}>
2FA is required, but this UI does not handle that yet.
</ThemedText>
</ThemedView>
)
}
if (signIn.status === 'needs_new_password') {
return (
<ThemedView style={styles.container}>
<ThemedText type="title" style={styles.title}>
Set New Password
</ThemedText>
<ThemedText style={styles.label}>Enter your new password</ThemedText>
<TextInput
style={styles.input}
value={password}
placeholder="Enter new password"
placeholderTextColor="#666666"
secureTextEntry={true}
onChangeText={(password) => setPassword(password)}
/>
{errors.fields.password && (
<ThemedText style={styles.error}>{errors.fields.password.message}</ThemedText>
)}
<Pressable
style={({ pressed }) => [
styles.button,
fetchStatus === 'fetching' && styles.buttonDisabled,
pressed && styles.buttonPressed,
]}
onPress={submitNewPassword}
disabled={fetchStatus === 'fetching'}
>
<ThemedText style={styles.buttonText}>Set new password</ThemedText>
</Pressable>
</ThemedView>
)
}
if (codeSent) {
return (
<ThemedView style={styles.container}>
<ThemedText type="title" style={styles.title}>
Verify Code
</ThemedText>
<ThemedText style={styles.label}>
Enter the password reset code sent to your phone number
</ThemedText>
<TextInput
style={styles.input}
value={code}
placeholder="Enter verification code"
placeholderTextColor="#666666"
onChangeText={(code) => setCode(code)}
keyboardType="numeric"
/>
{errors.fields.code && (
<ThemedText style={styles.error}>{errors.fields.code.message}</ThemedText>
)}
<Pressable
style={({ pressed }) => [
styles.button,
fetchStatus === 'fetching' && styles.buttonDisabled,
pressed && styles.buttonPressed,
]}
onPress={verifyCode}
disabled={fetchStatus === 'fetching'}
>
<ThemedText style={styles.buttonText}>Verify code</ThemedText>
</Pressable>
</ThemedView>
)
}
return (
<ThemedView style={styles.container}>
<ThemedText type="title" style={styles.title}>
Forgot Password?
</ThemedText>
<ThemedText style={styles.label}>Phone number</ThemedText>
<TextInput
style={styles.input}
autoCapitalize="none"
value={phoneNumber}
placeholder="Enter phone number"
placeholderTextColor="#666666"
onChangeText={(phoneNumber) => setPhoneNumber(phoneNumber)}
keyboardType="phone-pad"
/>
{errors.fields.identifier && (
<ThemedText style={styles.error}>{errors.fields.identifier.message}</ThemedText>
)}
<Pressable
style={({ pressed }) => [
styles.button,
(!phoneNumber || fetchStatus === 'fetching') && styles.buttonDisabled,
pressed && styles.buttonPressed,
]}
onPress={sendCode}
disabled={!phoneNumber || fetchStatus === 'fetching'}
>
<ThemedText style={styles.buttonText}>Send password reset code</ThemedText>
</Pressable>
{/* For your debugging purposes. You can just console.log errors, but we put them in the UI for convenience */}
{errors && <ThemedText style={styles.debug}>{JSON.stringify(errors, null, 2)}</ThemedText>}
<View style={styles.linkContainer}>
<ThemedText>Remember your password? </ThemedText>
<Link href="/sign-in">
<ThemedText type="link">Sign in</ThemedText>
</Link>
</View>
</ThemedView>
)
}
const styles = StyleSheet.create({
container: {
flex: 1,
padding: 20,
gap: 12,
},
title: {
marginBottom: 8,
},
label: {
fontWeight: '600',
fontSize: 14,
},
message: {
fontSize: 14,
marginTop: 8,
},
input: {
borderWidth: 1,
borderColor: '#ccc',
borderRadius: 8,
padding: 12,
fontSize: 16,
backgroundColor: '#fff',
},
button: {
backgroundColor: '#0a7ea4',
paddingVertical: 12,
paddingHorizontal: 24,
borderRadius: 8,
alignItems: 'center',
marginTop: 8,
},
buttonPressed: {
opacity: 0.7,
},
buttonDisabled: {
opacity: 0.5,
},
buttonText: {
color: '#fff',
fontWeight: '600',
},
linkContainer: {
flexDirection: 'row',
gap: 4,
marginTop: 12,
alignItems: 'center',
},
error: {
color: '#d32f2f',
fontSize: 12,
marginTop: -8,
},
debug: {
fontSize: 10,
opacity: 0.5,
marginTop: 8,
},
}) import SwiftUI
import ClerkKit
struct ForgotPasswordView: View {
@Environment(Clerk.self) private var clerk
@State private var phoneNumber = ""
@State private var code = ""
@State private var newPassword = ""
var signIn: SignIn? {
clerk.auth.currentSignIn
}
var body: some View {
switch clerk.auth.currentSignIn?.status {
case .needsFirstFactor:
// Verify the reset code
TextField("Enter your code", text: $code)
Button("Verify") {
Task { await verify(code: code) }
}
case .needsSecondFactor:
// See https://clerk.com/docs/guides/development/custom-flows/authentication/multi-factor-authentication
Text("2FA is required, but this UI does not handle that")
case .needsNewPassword:
// Set the new password after verification
SecureField("New password", text: $newPassword)
Button("Set new password") {
Task { await setNewPassword(password: newPassword) }
}
default:
if let user = clerk.user {
// Show the current user after a successful reset
Text("Signed in as: \(user.id)")
Button("Sign Out") {
Task { try await clerk.auth.signOut() }
}
} else {
TextField("Phone number", text: $phoneNumber)
Button("Forgot password?") {
Task { await startResetPassword(phoneNumber: phoneNumber) }
}
}
}
}
private func startResetPassword(phoneNumber: String) async {
do {
try await clerk.auth.signIn(phoneNumber)
try await signIn?.sendResetPasswordPhoneCode()
// Clerk updates currentSignIn and UI shows code field (needsFirstFactor)
} catch {
dump(error)
}
}
private func verify(code: String) async {
do {
try await signIn?.verifyCode(code)
// Clerk updates currentSignIn and UI shows new password field (needsNewPassword)
} catch {
dump(error)
}
}
private func setNewPassword(password: String) async {
do {
try await signIn?.resetPassword(
newPassword: password,
signOutOfOtherSessions: true
)
// If verification was completed, the created session is available on `clerk.session` and UI shows signed in
} catch {
dump(error)
}
}
}import android.util.Log
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.clerk.api.Clerk
import com.clerk.api.network.serialization.errorMessage
import com.clerk.api.network.serialization.flatMap
import com.clerk.api.network.serialization.onFailure
import com.clerk.api.network.serialization.onSuccess
import com.clerk.api.signin.SignIn
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.launch
class ForgotPasswordPhoneViewModel : ViewModel() {
private val _uiState = MutableStateFlow<UiState>(UiState.Loading)
val uiState = _uiState.asStateFlow()
init {
combine(Clerk.isInitialized, Clerk.userFlow) { isInitialized, user ->
_uiState.value =
when {
!isInitialized -> UiState.Loading
user != null -> UiState.Complete
else -> UiState.SignedOut
}
}
.launchIn(viewModelScope)
}
fun createSignIn(phoneNumber: String) {
viewModelScope.launch {
Clerk.auth
.signIn { this.phone = phoneNumber }
.flatMap { signIn ->
signIn.sendResetPasswordCode { this.phone = phoneNumber }
}
.onSuccess { updateStateFromStatus(it.status) }
.onFailure {
// See https://clerk.com/docs/guides/development/custom-flows/error-handling
Log.e(
ForgotPasswordPhoneViewModel::class.simpleName,
it.errorMessage,
it.throwable,
)
}
}
}
fun verify(code: String) {
val signIn = Clerk.signIn ?: return
viewModelScope.launch {
signIn
.verifyCode(code)
.onSuccess { updateStateFromStatus(it.status) }
.onFailure {
// See https://clerk.com/docs/guides/development/custom-flows/error-handling
Log.e(
ForgotPasswordPhoneViewModel::class.simpleName,
it.errorMessage,
it.throwable,
)
}
}
}
fun setNewPassword(password: String) {
val signIn = Clerk.signIn ?: return
viewModelScope.launch {
signIn
.resetPassword(
newPassword = password,
signOutOfOtherSessions = true,
)
.onSuccess { updated ->
updateStateFromStatus(updated.status)
if (updated.status == SignIn.Status.COMPLETE) {
updated.finalize()
}
}
.onFailure {
// See https://clerk.com/docs/guides/development/custom-flows/error-handling
Log.e(
ForgotPasswordPhoneViewModel::class.simpleName,
it.errorMessage,
it.throwable,
)
}
}
}
fun updateStateFromStatus(status: SignIn.Status) {
val state =
when (status) {
SignIn.Status.COMPLETE -> UiState.Complete
SignIn.Status.NEEDS_FIRST_FACTOR -> UiState.NeedsFirstFactor
// See https://clerk.com/docs/guides/development/custom-flows/authentication/multi-factor-authentication
SignIn.Status.NEEDS_SECOND_FACTOR -> UiState.NeedsSecondFactor
SignIn.Status.NEEDS_NEW_PASSWORD -> UiState.NeedsNewPassword
else -> {
UiState.SignedOut
}
}
_uiState.value = state
}
sealed interface UiState {
data object Loading : UiState
data object SignedOut : UiState
data object NeedsFirstFactor : UiState
data object NeedsSecondFactor : UiState
data object NeedsNewPassword : UiState
data object Complete : UiState
}
} import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.viewModels
import androidx.compose.foundation.layout.Arrangement.Absolute.spacedBy
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.Button
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.Text
import androidx.compose.material3.TextField
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.input.PasswordVisualTransformation
import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.clerk.api.Clerk
class ForgotPasswordPhoneActivity : ComponentActivity() {
val viewModel: ForgotPasswordPhoneViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
val state by viewModel.uiState.collectAsStateWithLifecycle()
ForgotPasswordView(
state,
onVerify = viewModel::verify,
onSetNewPassword = viewModel::setNewPassword,
onCreateSignIn = viewModel::createSignIn,
)
}
}
}
@Composable
fun ForgotPasswordView(
state: ForgotPasswordPhoneViewModel.UiState,
onVerify: (String) -> Unit,
onSetNewPassword: (String) -> Unit,
onCreateSignIn: (String) -> Unit,
) {
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
when (state) {
ForgotPasswordPhoneViewModel.UiState.Complete -> {
Text("Active session: ${Clerk.activeSession?.id}")
}
ForgotPasswordPhoneViewModel.UiState.NeedsFirstFactor -> {
InputContent(placeholder = "Enter your code", buttonText = "Verify", onClick = onVerify)
}
ForgotPasswordPhoneViewModel.UiState.NeedsNewPassword -> {
InputContent(
placeholder = "Enter your new password",
buttonText = "Set new password",
onClick = onSetNewPassword,
visualTransformation = PasswordVisualTransformation(),
)
}
ForgotPasswordPhoneViewModel.UiState.NeedsSecondFactor -> {
Text("2FA is required but this UI does not handle that")
}
ForgotPasswordPhoneViewModel.UiState.SignedOut -> {
InputContent(
placeholder = "Enter your phone number",
buttonText = "Forgot password?",
onClick = onCreateSignIn,
)
}
ForgotPasswordPhoneViewModel.UiState.Loading -> CircularProgressIndicator()
}
}
}
@Composable
fun InputContent(
placeholder: String,
buttonText: String,
visualTransformation: VisualTransformation = VisualTransformation.None,
onClick: (String) -> Unit,
) {
var value by remember { mutableStateOf("") }
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = spacedBy(16.dp, Alignment.CenterVertically),
) {
TextField(
value = value,
onValueChange = { value = it },
visualTransformation = visualTransformation,
placeholder = { Text(placeholder) },
)
Button(onClick = { onClick(value) }) { Text(buttonText) }
}
}Handle compromised passwords
If you have enabled rejection of compromised passwords also on sign-in, then it is possible for the sign-in attempt to be rejected with the form_password_pwned error code.
In this case, you can prompt the user to reset their password using the exact same logic detailed in the previous section.
Feedback
Last updated on