Skip to main content
Docs

Build a custom flow for updating a user's password

Warning

This guide is for users who want to build a . To use a prebuilt UI, use the Account Portal pages or prebuilt components.

Warning

This guide is for users who want to build a . To use a prebuilt UI, use the Account Portal pages or prebuilt views.

This guide demonstrates how to build a custom user interface that allows users to update their password once they're already signed in.

Tip

To allow users to reset their password before they've signed in, see the custom forgot password flow.

Tip

Examples for this SDK aren't available yet. For now, try adapting the available example to fit your SDK.

app/account/update-password/page.tsx
'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>}
    </>
  )
}
app/(account)/update-password.tsx
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,
  },
})
UpdatePasswordView.swift
  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
      }
    }
  }
UpdatePasswordViewModel.kt
  import android.util.Log
  import androidx.lifecycle.ViewModel
  import androidx.lifecycle.viewModelScope
  import com.clerk.api.Clerk
  import com.clerk.api.network.serialization.errorMessage
  import com.clerk.api.network.serialization.onFailure
  import com.clerk.api.network.serialization.onSuccess
  import com.clerk.api.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
    }
  }
UpdatePasswordActivity.kt
  import android.os.Bundle
  import androidx.activity.ComponentActivity
  import androidx.activity.compose.setContent
  import androidx.activity.viewModels
  import androidx.compose.foundation.layout.Arrangement.Absolute.spacedBy
  import androidx.compose.foundation.layout.Box
  import androidx.compose.foundation.layout.Column
  import androidx.compose.foundation.layout.fillMaxSize
  import androidx.compose.material3.Button
  import androidx.compose.material3.CircularProgressIndicator
  import androidx.compose.material3.Text
  import androidx.compose.material3.TextField
  import androidx.compose.runtime.Composable
  import androidx.compose.runtime.getValue
  import androidx.compose.runtime.mutableStateOf
  import androidx.compose.runtime.remember
  import androidx.compose.runtime.setValue
  import androidx.compose.ui.Alignment
  import androidx.compose.ui.Modifier
  import androidx.compose.ui.text.input.PasswordVisualTransformation
  import androidx.compose.ui.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

What did you think of this content?

Last updated on