Build a custom flow for updating a user's password
This guide demonstrates how to build a custom user interface that allows users to update their password once they're already signed in.
'use client'
import { useReverification, useUser } from '@clerk/nextjs'
import { useState } from 'react'
export default function UpdatePasswordPage() {
const { isLoaded, isSignedIn, user } = useUser()
const [completed, setCompleted] = useState(false)
const [error, setError] = useState<Error | null>(null)
// Sensitive actions require reverification
// See https://clerk.com/docs/guides/secure/reverification
const updatePassword = useReverification(
({ currentPassword, newPassword }: { currentPassword: string; newPassword: string }) =>
user?.updatePassword({
currentPassword,
newPassword,
signOutOfOtherSessions: true,
}),
)
// Handle loading state
if (!isLoaded) return <p>Loading...</p>
// Handle signed-out state
if (!isSignedIn) return <p>You must be signed in to access this page</p>
const handleSubmit = async (formData: FormData) => {
setError(null)
const currentPassword = formData.get('currentPassword') as string
const newPassword = formData.get('newPassword') as string
try {
await updatePassword({
currentPassword,
newPassword,
})
setCompleted(true)
} catch (error) {
// See https://clerk.com/docs/guides/development/custom-flows/error-handling
// for more info on error handling
console.error(error)
setError(error as Error)
}
}
if (completed) {
return <h1>Password updated!</h1>
}
return (
<>
<h1>Update password</h1>
<form action={handleSubmit}>
<input type="password" name="currentPassword" placeholder="Current password" />
<input type="password" name="newPassword" placeholder="New password" />
<button type="submit">Update</button>
</form>
{error && <p>{error.message}</p>}
</>
)
}import { ThemedText } from '@/components/themed-text'
import { ThemedView } from '@/components/themed-view'
import { useUser } from '@clerk/expo'
import * as React from 'react'
import { Pressable, StyleSheet, TextInput } from 'react-native'
export default function UpdatePasswordScreen() {
const { isLoaded, isSignedIn, user } = useUser()
const [currentPassword, setCurrentPassword] = React.useState('')
const [newPassword, setNewPassword] = React.useState('')
const [isSubmitting, setIsSubmitting] = React.useState(false)
const [completed, setCompleted] = React.useState(false)
const [error, setError] = React.useState<Error | null>(null)
// Handle loading state
if (!isLoaded) {
return (
<ThemedView style={styles.container}>
<ThemedText>Loading...</ThemedText>
</ThemedView>
)
}
// Handle signed-out state
if (!isSignedIn) {
return (
<ThemedView style={styles.container}>
<ThemedText>You must be signed in to access this page</ThemedText>
</ThemedView>
)
}
const handleSubmit = async () => {
setError(null)
setIsSubmitting(true)
try {
await user.updatePassword({
currentPassword,
newPassword,
signOutOfOtherSessions: true,
})
setCompleted(true)
} catch (err: unknown) {
console.error(err)
if (err instanceof Error) {
setError(err)
} else {
setError(new Error('Something went wrong'))
}
} finally {
setIsSubmitting(false)
}
}
if (completed) {
return (
<ThemedView style={styles.container}>
<ThemedText type="title" style={styles.title}>
Password updated!
</ThemedText>
</ThemedView>
)
}
return (
<ThemedView style={styles.container}>
<ThemedText type="title" style={styles.title}>
Update password
</ThemedText>
<ThemedText style={styles.label}>Current password</ThemedText>
<TextInput
style={styles.input}
value={currentPassword}
onChangeText={setCurrentPassword}
placeholder="Current password"
placeholderTextColor="#666666"
secureTextEntry
autoCapitalize="none"
editable={!isSubmitting}
/>
<ThemedText style={styles.label}>New password</ThemedText>
<TextInput
style={styles.input}
value={newPassword}
onChangeText={setNewPassword}
placeholder="New password"
placeholderTextColor="#666666"
secureTextEntry
autoCapitalize="none"
editable={!isSubmitting}
/>
<Pressable
style={({ pressed }) => [
styles.button,
(!currentPassword || !newPassword || isSubmitting) && styles.buttonDisabled,
pressed && styles.buttonPressed,
]}
onPress={() => void handleSubmit()}
disabled={!currentPassword || !newPassword || isSubmitting}
>
<ThemedText style={styles.buttonText}>Update</ThemedText>
</Pressable>
{error ? <ThemedText style={styles.error}>{error.message}</ThemedText> : null}
</ThemedView>
)
}
const styles = StyleSheet.create({
container: {
flex: 1,
padding: 20,
gap: 12,
},
title: {
marginBottom: 8,
},
label: {
fontWeight: '600',
fontSize: 14,
},
input: {
borderWidth: 1,
borderColor: '#ccc',
borderRadius: 8,
padding: 12,
fontSize: 16,
backgroundColor: '#fff',
},
button: {
backgroundColor: '#0a7ea4',
paddingVertical: 12,
paddingHorizontal: 24,
borderRadius: 8,
alignItems: 'center',
marginTop: 8,
},
buttonPressed: {
opacity: 0.7,
},
buttonDisabled: {
opacity: 0.5,
},
buttonText: {
color: '#fff',
fontWeight: '600',
},
error: {
color: '#d32f2f',
fontSize: 12,
marginTop: 4,
},
}) import SwiftUI
import ClerkKit
struct UpdatePasswordView: View {
@Environment(Clerk.self) private var clerk
@State private var currentPassword = ""
@State private var newPassword = ""
@State private var completed = false
@State private var error: Error?
var body: some View {
Group {
if completed {
Text("Password updated!")
} else if clerk.user != nil {
VStack(alignment: .leading, spacing: 16) {
Text("Update password")
.font(.title)
SecureField("Current password", text: $currentPassword)
SecureField("New password", text: $newPassword)
Button("Update") {
Task { await handleSubmit() }
}
.disabled(currentPassword.isEmpty || newPassword.isEmpty)
if let error {
Text(error.localizedDescription)
}
}
} else {
Text("You must be signed in to access this page")
}
}
}
private func handleSubmit() async {
error = nil
guard let user = clerk.user else { return }
do {
_ = try await user.updatePassword(
.init(
currentPassword: currentPassword,
newPassword: newPassword,
signOutOfOtherSessions: true
)
)
completed = true
} catch {
// See https://clerk.com/docs/guides/development/custom-flows/error-handling
// for more info on error handling
dump(error)
self.error = 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.onFailure
import com.clerk.api.network.serialization.onSuccess
import com.clerk.api.user.updatePassword
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
class UpdatePasswordViewModel : ViewModel() {
private val _uiState = MutableStateFlow<UiState>(UiState.Loading)
val uiState = _uiState.asStateFlow()
private val _errorMessage = MutableStateFlow<String?>(null)
val errorMessage = _errorMessage.asStateFlow()
private val _isSubmitting = MutableStateFlow(false)
val isSubmitting = _isSubmitting.asStateFlow()
init {
// Observe the signed-in user reactively with `Clerk.userFlow`
combine(Clerk.isInitialized, Clerk.userFlow) { isInitialized, user ->
_uiState.update { current ->
when {
!isInitialized -> UiState.Loading
user == null -> UiState.SignedOut
current is UiState.PasswordUpdated -> UiState.PasswordUpdated
else -> UiState.Form
}
}
}.launchIn(viewModelScope)
}
fun updatePassword(currentPassword: String, newPassword: String) {
// Or read the current user directly with `Clerk.activeUser`
val user = Clerk.activeUser ?: return
viewModelScope.launch {
_errorMessage.value = null
_isSubmitting.value = true
user
.updatePassword(
currentPassword = currentPassword,
newPassword = newPassword,
signOutOfOtherSessions = true,
)
.onSuccess {
_uiState.value = UiState.PasswordUpdated
}
.onFailure {
// See https://clerk.com/docs/guides/development/custom-flows/error-handling
// for more info on error handling
Log.e(
UpdatePasswordViewModel::class.simpleName,
it.errorMessage,
it.throwable,
)
_errorMessage.value = it.errorMessage
}
_isSubmitting.value = false
}
}
sealed interface UiState {
data object Loading : UiState
data object SignedOut : UiState
data object Form : UiState
data object PasswordUpdated : 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.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
class UpdatePasswordActivity : ComponentActivity() {
val viewModel: UpdatePasswordViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
val state by viewModel.uiState.collectAsStateWithLifecycle()
val errorMessage by viewModel.errorMessage.collectAsStateWithLifecycle()
val isSubmitting by viewModel.isSubmitting.collectAsStateWithLifecycle()
UpdatePasswordScreen(
state = state,
errorMessage = errorMessage,
isSubmitting = isSubmitting,
onSubmit = viewModel::updatePassword,
)
}
}
}
@Composable
fun UpdatePasswordScreen(
state: UpdatePasswordViewModel.UiState,
errorMessage: String?,
isSubmitting: Boolean,
onSubmit: (String, String) -> Unit,
) {
var currentPassword by remember { mutableStateOf("") }
var newPassword by remember { mutableStateOf("") }
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
when (state) {
UpdatePasswordViewModel.UiState.Loading -> CircularProgressIndicator()
UpdatePasswordViewModel.UiState.SignedOut -> {
Text("You must be signed in to access this page")
}
UpdatePasswordViewModel.UiState.PasswordUpdated -> {
Text("Password updated!")
}
UpdatePasswordViewModel.UiState.Form -> {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = spacedBy(16.dp, Alignment.CenterVertically),
) {
Text("Update password")
TextField(
value = currentPassword,
onValueChange = { currentPassword = it },
placeholder = { Text("Current password") },
visualTransformation = PasswordVisualTransformation(),
)
TextField(
value = newPassword,
onValueChange = { newPassword = it },
placeholder = { Text("New password") },
visualTransformation = PasswordVisualTransformation(),
)
Button(
onClick = { onSubmit(currentPassword, newPassword) },
enabled = currentPassword.isNotEmpty() && newPassword.isNotEmpty() && !isSubmitting,
) {
Text("Update")
}
errorMessage?.let { Text(it) }
}
}
}
}
}Feedback
Last updated on