> ## Documentation Index
> Fetch the complete documentation index at: https://docs.prelude.so/llms.txt
> Use this file to discover all available pages before exploring further.

# Change Password

> Implement logged-in change password with the Prelude mobile SDKs.

Let an authenticated user change their password from the account area. The SDK acquires the `prld:pwd:write` session scope via a short OTP challenge, then calls the change-password endpoint — the scope is consumed atomically on save.

See the [Logged-in Change Password guide](/session/documentation/change-password) for the backend configuration (direct step-up on `prld:pwd:write`). This page focuses on the mobile integration.

## Flow at a glance

1. Request the `prld:pwd:write` scope via `requestStepUp`.
2. Fire OTP delivery for the returned challenge via `sendStepUpOTP` — the user receives the code on the identifier the server picked (`verify_email` or `verify_sms`).
3. Submit the code via `submitStepUpOTP`.
4. Once `submitStepUpOTP` returns no further step, the SDK has refreshed the session with the granted scope. Call `client.changePassword`.

`changePassword` consumes the scope atomically on save — the SDK invalidates the cached access token and runs a best-effort refresh so the next mint drops the now-spent scope. A leaked token therefore can't change the password again without re-stepping up.

For the full step-up SDK surface, see [Step-Up](/session/documentation/frontend-sdks/mobile/step-up).

## Example

<Tabs>
  <Tab title="iOS">
    ```swift theme={null}
    import PreludeAuth

    func changePassword(
        client: PreludeAuthClient,
        enteredCode: String,
        newPassword: String
    ) async throws {
        // 1. Acquire the scope.
        let challenge = try await client.requestStepUp(scope: "prld:pwd:write")
        guard challenge.status != .blocked else {
            throw PreludeAuthError.forbidden("Change password is not allowed")
        }

        // 2. Fire OTP delivery for the step the server picked.
        if challenge.currentStep == "verify_email"
            || challenge.currentStep == "verify_sms" {
            try await client.sendStepUpOTP(challenge)
        }

        // 3. Submit the code the user entered. In a real UI this is
        //    a separate submit handler bound to the user's input.
        let next = try await client.submitStepUpOTP(challenge, code: enteredCode)
        if next != nil {
            // Multi-step flow — call sendStepUpOTP(next) for the next
            // delivery step, then submitStepUpOTP again, until next == nil.
        }

        // 4. The SDK refreshed the session for us; the access token
        //    now carries `prld:pwd:write`. Save the new password.
        try await client.changePassword(RedactedString(newPassword))
    }
    ```
  </Tab>

  <Tab title="Android">
    ```kotlin theme={null}
    import so.prelude.android.auth.*

    suspend fun changePassword(
        client: PreludeAuthClient,
        enteredCode: String,
        newPassword: String,
    ) {
        // 1. Acquire the scope.
        val challenge = client.requestStepUp("prld:pwd:write")
        if (challenge.status == PreludeStepUpStatus.BLOCKED) {
            throw PreludeAuthError.Forbidden("Change password is not allowed")
        }

        // 2. Fire OTP delivery for the step the server picked.
        if (challenge.currentStep in setOf("verify_email", "verify_sms")) {
            client.sendStepUpOTP(challenge)
        }

        // 3. Submit the code the user entered. In a real UI this is
        //    a separate submit handler bound to the user's input.
        val next = client.submitStepUpOTP(challenge, enteredCode)
        if (next != null) {
            // Multi-step flow — call sendStepUpOTP(next) for the next
            // delivery step, then submitStepUpOTP again, until next == null.
        }

        // 4. The SDK refreshed the session for us; the access token
        //    now carries `prld:pwd:write`. Save the new password.
        client.changePassword(RedactedString(newPassword))
    }
    ```
  </Tab>

  <Tab title="Flutter">
    ```dart theme={null}
    import 'package:prelude_flutter_auth_sdk/prelude_flutter_auth_sdk.dart';

    Future<void> changePassword(
      PreludeAuthClient client,
      String enteredCode,
      String newPassword,
    ) async {
      // 1. Acquire the scope.
      final challenge = await client.requestStepUp(scope: 'prld:pwd:write');
      if (challenge.status == StepUpStatus.blocked) {
        throw const ForbiddenException('Change password is not allowed');
      }

      // 2. Fire OTP delivery for the step the server picked.
      if (challenge.currentStep == 'verify_email' ||
          challenge.currentStep == 'verify_sms') {
        await client.sendStepUpOTP(challenge);
      }

      // 3. Submit the code the user entered. In a real UI this is
      //    a separate submit handler bound to the user's input.
      final next = await client.submitStepUpOTP(challenge, enteredCode);
      if (next != null) {
        // Multi-step flow — call sendStepUpOTP(next) for the next
        // delivery step, then submitStepUpOTP again, until next == null.
      }

      // 4. The SDK refreshed the session for us; the access token
      //    now carries `prld:pwd:write`. Save the new password.
      await client.changePassword(RedactedString(newPassword));
    }
    ```
  </Tab>

  <Tab title="React Native">
    ```ts theme={null}
    import {
      ForbiddenError,
      PreludeAuthClient,
      RedactedString,
    } from "@prelude.so/react-native-auth-sdk";

    async function changePassword(
      client: PreludeAuthClient,
      enteredCode: string,
      newPassword: string,
    ) {
      // 1. Acquire the scope.
      const challenge = await client.requestStepUp("prld:pwd:write");
      if (challenge.status === "block") {
        throw new ForbiddenError("Change password is not allowed");
      }

      // 2. Fire OTP delivery for the step the server picked.
      if (
        challenge.currentStep === "verify_email" ||
        challenge.currentStep === "verify_sms"
      ) {
        await client.sendStepUpOTP(challenge);
      }

      // 3. Submit the code the user entered. In a real UI this is
      //    a separate submit handler bound to the user's input.
      const next = await client.submitStepUpOTP(challenge, enteredCode);
      if (next !== null) {
        // Multi-step flow — call sendStepUpOTP(next) for the next
        // delivery step, then submitStepUpOTP again, until next === null.
      }

      // 4. The SDK refreshed the session for us; the access token
      //    now carries `prld:pwd:write`. Save the new password.
      await client.changePassword(new RedactedString(newPassword));
    }
    ```
  </Tab>
</Tabs>

<Accordion title="Try it" icon="flask">
  Builds on the project from [Introduction](/session/documentation/frontend-sdks/mobile/introduction) and a working password login ([Password](/session/documentation/frontend-sdks/mobile/password)). The user you log in with must have an `email_address` or `phone_number` identifier.

  **1. Configure direct step-up for `prld:pwd:write`**

  ```bash theme={null}
  curl -X POST https://api.prelude.dev/v2/session/apps/${APP_ID}/config/stepup \
    -H "Authorization: Bearer ${MANAGEMENT_API_KEY}" \
    -H "Content-Type: application/json" \
    -d '{
      "jwks_url": "",
      "step_keys": [],
      "allowed_scopes": [
        {
          "scope": "prld:pwd:write",
          "mode": "direct",
          "direct": {
            "identifier_types": ["email_address"],
            "status": "review",
            "granted_for": 300,
            "grant_mode": "single-use",
            "steps": [
              { "order": 1, "key": "verify_email", "expiration_duration": 600 }
            ]
          }
        },
        {
          "scope": "prld:pwd:write",
          "mode": "direct",
          "direct": {
            "identifier_types": ["phone_number"],
            "status": "review",
            "granted_for": 300,
            "grant_mode": "single-use",
            "steps": [
              { "order": 1, "key": "verify_sms", "expiration_duration": 600 }
            ]
          }
        }
      ]
    }'
  ```

  **2. Register the scope**

  ```bash theme={null}
  curl -X POST https://api.prelude.dev/v2/session/apps/${APP_ID}/config/scopes \
    -H "Authorization: Bearer ${MANAGEMENT_API_KEY}" \
    -H "Content-Type: application/json" \
    -d '{ "scope": "prld:pwd:write" }'
  ```

  **3. Replace your entry file**

  <Tabs>
    <Tab title="iOS">
      Replace `ContentView.swift`:

      ```swift ContentView.swift theme={null}
      import SwiftUI
      import PreludeAuth

      private let appID = "YOUR_APP_ID"

      enum CPView { case login, logged, stepUpOTP, newPassword, done }

      @MainActor
      final class ChangePasswordModel: ObservableObject {
          let client = try! PreludeAuthClient(
              endpoint: .custom("https://\(appID).session.prelude.dev")
          )

          @Published var view: CPView = .login
          @Published var error: String?
          var challenge: StepUpChallenge?

          func login(email: String, password: String) async {
              error = nil
              do {
                  _ = try await client.loginWithPassword(
                      LoginWithPasswordOptions(emailAddress: email, password: password)
                  )
                  view = .logged
              } catch { self.error = error.localizedDescription }
          }

          func startChangePassword() async {
              error = nil
              do {
                  let c = try await client.requestStepUp(scope: "prld:pwd:write")
                  if c.status == .blocked {
                      error = "Couldn't change your password — please try again later."
                      return
                  }
                  if c.currentStep == "verify_email" || c.currentStep == "verify_sms" {
                      try await client.sendStepUpOTP(c)
                  }
                  challenge = c
                  view = .stepUpOTP
              } catch { self.error = error.localizedDescription }
          }

          func submitOTP(_ code: String) async {
              guard let c = challenge else { return }
              error = nil
              do {
                  if let next = try await client.submitStepUpOTP(c, code: code) {
                      challenge = next  // unusual: more than one step
                  } else {
                      view = .newPassword  // scope granted
                  }
              } catch PreludeAuthError.invalidOTPCode {
                  error = "Wrong code."
              } catch { self.error = error.localizedDescription }
          }

          func savePassword(_ newPassword: String) async {
              error = nil
              do {
                  try await client.changePassword(RedactedString(newPassword))
                  view = .done
              } catch { self.error = error.localizedDescription }
          }
      }

      struct ContentView: View {
          @StateObject private var model = ChangePasswordModel()
          @State private var email = ""
          @State private var password = ""
          @State private var otp = ""
          @State private var newPassword = ""

          var body: some View {
              VStack(spacing: 12) {
                  Text("Change Password Demo").font(.title3)
                  if let error = model.error {
                      Text(error).foregroundStyle(.red).font(.caption)
                  }

                  switch model.view {
                  case .login:
                      TextField("user@example.com", text: $email)
                          .textInputAutocapitalization(.never)
                          .keyboardType(.emailAddress)
                      SecureField("Current password", text: $password)
                      Button("Log in") {
                          Task { await model.login(email: email, password: password) }
                      }.buttonStyle(.borderedProminent)
                  case .logged:
                      Text("Logged in.")
                      Button("Change password") {
                          Task { await model.startChangePassword() }
                      }.buttonStyle(.borderedProminent)
                  case .stepUpOTP:
                      Text("Enter the code we just sent you.")
                      TextField("OTP code", text: $otp).keyboardType(.numberPad)
                      Button("Verify") {
                          Task { await model.submitOTP(otp) }
                      }.buttonStyle(.borderedProminent)
                  case .newPassword:
                      Text("Choose a new password.")
                      SecureField("New password", text: $newPassword)
                      Button("Save") {
                          Task { await model.savePassword(newPassword) }
                      }.buttonStyle(.borderedProminent)
                  case .done:
                      Text("Password updated.").font(.headline)
                      Text("`prld:pwd:write` has been consumed.")
                          .font(.caption).foregroundStyle(.secondary)
                  }
              }
              .textFieldStyle(.roundedBorder)
              .padding()
          }
      }
      ```
    </Tab>

    <Tab title="Android">
      Replace `MainActivity.kt`:

      ```kotlin MainActivity.kt theme={null}
      package com.example.sessiontest

      import android.os.Bundle
      import androidx.activity.ComponentActivity
      import androidx.activity.compose.setContent
      import androidx.compose.foundation.layout.*
      import androidx.compose.material3.*
      import androidx.compose.runtime.*
      import androidx.compose.ui.Modifier
      import androidx.compose.ui.text.input.KeyboardOptions
      import androidx.compose.ui.text.input.KeyboardType
      import androidx.compose.ui.text.input.PasswordVisualTransformation
      import androidx.compose.ui.unit.dp
      import androidx.lifecycle.ViewModel
      import androidx.lifecycle.viewModelScope
      import androidx.lifecycle.viewmodel.compose.viewModel
      import kotlinx.coroutines.launch
      import so.prelude.android.auth.*
      import java.net.URL

      private const val APP_ID = "YOUR_APP_ID"

      enum class CpView { Login, Logged, StepUpOtp, NewPassword, Done }

      class ChangePasswordViewModel(ctx: android.content.Context) : ViewModel() {
          val client = PreludeAuthClient(
              context = ctx,
              baseUrl = URL("https://$APP_ID.session.prelude.dev"),
          )

          var view by mutableStateOf(CpView.Login); private set
          var error by mutableStateOf<String?>(null); private set
          private var challenge: PreludeStepUpChallenge? = null

          fun login(email: String, password: String) {
              error = null
              viewModelScope.launch {
                  try {
                      client.loginWithPassword(
                          LoginWithPasswordOptions(identifier = email, password = password),
                      )
                      view = CpView.Logged
                  } catch (e: PreludeAuthError) { error = e.message }
              }
          }

          fun startChangePassword() {
              error = null
              viewModelScope.launch {
                  try {
                      val c = client.requestStepUp("prld:pwd:write")
                      if (c.status == PreludeStepUpStatus.BLOCKED) {
                          error = "Couldn't change your password — please try again later."
                          return@launch
                      }
                      if (c.currentStep in setOf("verify_email", "verify_sms")) {
                          client.sendStepUpOTP(c)
                      }
                      challenge = c
                      view = CpView.StepUpOtp
                  } catch (e: PreludeAuthError) { error = e.message }
              }
          }

          fun submitOtp(code: String) {
              val c = challenge ?: return
              error = null
              viewModelScope.launch {
                  try {
                      val next = client.submitStepUpOTP(c, code)
                      if (next != null) {
                          challenge = next  // unusual: more than one step
                      } else {
                          view = CpView.NewPassword  // scope granted
                      }
                  } catch (e: PreludeAuthError.InvalidOTPCode) { error = "Wrong code." }
                  catch (e: PreludeAuthError) { error = e.message }
              }
          }

          fun savePassword(newPassword: String) {
              error = null
              viewModelScope.launch {
                  try {
                      client.changePassword(RedactedString(newPassword))
                      view = CpView.Done
                  } catch (e: PreludeAuthError) { error = e.message }
              }
          }
      }

      class MainActivity : ComponentActivity() {
          override fun onCreate(savedInstanceState: Bundle?) {
              super.onCreate(savedInstanceState)
              setContent {
                  MaterialTheme {
                      Surface {
                          val vm: ChangePasswordViewModel = viewModel(factory = viewModelFactory(applicationContext))
                          var email by remember { mutableStateOf("") }
                          var password by remember { mutableStateOf("") }
                          var otp by remember { mutableStateOf("") }
                          var newPassword by remember { mutableStateOf("") }

                          Column(Modifier.fillMaxSize().padding(24.dp)) {
                              Text("Change Password Demo", style = MaterialTheme.typography.titleMedium)
                              Spacer(Modifier.height(12.dp))
                              vm.error?.let {
                                  Text(it, color = MaterialTheme.colorScheme.error)
                                  Spacer(Modifier.height(8.dp))
                              }
                              when (vm.view) {
                                  CpView.Login -> {
                                      OutlinedTextField(email, { email = it },
                                          label = { Text("user@example.com") },
                                          keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Email),
                                          modifier = Modifier.fillMaxWidth())
                                      Spacer(Modifier.height(8.dp))
                                      OutlinedTextField(password, { password = it },
                                          label = { Text("Current password") },
                                          visualTransformation = PasswordVisualTransformation(),
                                          modifier = Modifier.fillMaxWidth())
                                      Spacer(Modifier.height(12.dp))
                                      Button(onClick = { vm.login(email, password) }) { Text("Log in") }
                                  }
                                  CpView.Logged -> {
                                      Text("Logged in.")
                                      Spacer(Modifier.height(8.dp))
                                      Button(onClick = { vm.startChangePassword() }) {
                                          Text("Change password")
                                      }
                                  }
                                  CpView.StepUpOtp -> {
                                      Text("Enter the code we just sent you.")
                                      OutlinedTextField(otp, { otp = it },
                                          label = { Text("OTP code") },
                                          keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
                                          modifier = Modifier.fillMaxWidth())
                                      Spacer(Modifier.height(8.dp))
                                      Button(onClick = { vm.submitOtp(otp) }) { Text("Verify") }
                                  }
                                  CpView.NewPassword -> {
                                      Text("Choose a new password.")
                                      OutlinedTextField(newPassword, { newPassword = it },
                                          label = { Text("New password") },
                                          visualTransformation = PasswordVisualTransformation(),
                                          modifier = Modifier.fillMaxWidth())
                                      Spacer(Modifier.height(8.dp))
                                      Button(onClick = { vm.savePassword(newPassword) }) { Text("Save") }
                                  }
                                  CpView.Done -> {
                                      Text("Password updated.", style = MaterialTheme.typography.titleSmall)
                                      Text("`prld:pwd:write` has been consumed.",
                                          style = MaterialTheme.typography.bodySmall)
                                  }
                              }
                          }
                      }
                  }
              }
          }
      }
      ```

      (`viewModelFactory` is the small helper defined in [Introduction](/session/documentation/frontend-sdks/mobile/introduction#helpers).)
    </Tab>

    <Tab title="Flutter">
      Replace `lib/main.dart`:

      ```dart lib/main.dart theme={null}
      import 'package:flutter/material.dart';
      import 'package:prelude_flutter_auth_sdk/prelude_flutter_auth_sdk.dart';

      const appID = 'YOUR_APP_ID';

      void main() => runApp(const MyApp());

      enum _Screen { login, logged, stepUpOtp, newPassword, done }

      class MyApp extends StatefulWidget {
        const MyApp({super.key});
        @override
        State<MyApp> createState() => _MyAppState();
      }

      class _MyAppState extends State<MyApp> {
        late final client = PreludeAuthClient(
          endpoint: Endpoint.custom('https://$appID.session.prelude.dev'),
        );

        final _email = TextEditingController();
        final _password = TextEditingController();
        final _otp = TextEditingController();
        final _newPassword = TextEditingController();

        _Screen _view = _Screen.login;
        StepUpChallenge? _challenge;
        String? _error;

        Future<void> _login() async {
          setState(() => _error = null);
          try {
            await client.loginWithPassword(
              LoginWithPasswordOptions(
                emailAddress: _email.text,
                password: _password.text,
              ),
            );
            setState(() => _view = _Screen.logged);
          } on PreludeAuthException catch (e) {
            setState(() => _error = e.message);
          }
        }

        Future<void> _startChangePassword() async {
          setState(() => _error = null);
          try {
            final c = await client.requestStepUp(scope: 'prld:pwd:write');
            if (c.status == StepUpStatus.blocked) {
              setState(() => _error = 'Couldn\'t change your password — please try again later.');
              return;
            }
            if (c.currentStep == 'verify_email' ||
                c.currentStep == 'verify_sms') {
              await client.sendStepUpOTP(c);
            }
            setState(() {
              _challenge = c;
              _view = _Screen.stepUpOtp;
            });
          } on PreludeAuthException catch (e) {
            setState(() => _error = e.message);
          }
        }

        Future<void> _submitOtp() async {
          final c = _challenge;
          if (c == null) return;
          setState(() => _error = null);
          try {
            final next = await client.submitStepUpOTP(c, _otp.text);
            if (next != null) {
              setState(() => _challenge = next); // unusual: more than one step
            } else {
              setState(() => _view = _Screen.newPassword); // scope granted
            }
          } on InvalidOTPCodeException {
            setState(() => _error = 'Wrong code.');
          } on PreludeAuthException catch (e) {
            setState(() => _error = e.message);
          }
        }

        Future<void> _savePassword() async {
          setState(() => _error = null);
          try {
            await client.changePassword(RedactedString(_newPassword.text));
            setState(() => _view = _Screen.done);
          } on PreludeAuthException catch (e) {
            setState(() => _error = e.message);
          }
        }

        @override
        void dispose() {
          _email.dispose();
          _password.dispose();
          _otp.dispose();
          _newPassword.dispose();
          client.dispose();
          super.dispose();
        }

        @override
        Widget build(BuildContext context) {
          Widget body;
          switch (_view) {
            case _Screen.login:
              body = Column(mainAxisSize: MainAxisSize.min, children: [
                TextField(
                  controller: _email,
                  keyboardType: TextInputType.emailAddress,
                  decoration: const InputDecoration(labelText: 'user@example.com'),
                ),
                const SizedBox(height: 8),
                TextField(
                  controller: _password,
                  obscureText: true,
                  decoration: const InputDecoration(labelText: 'Current password'),
                ),
                const SizedBox(height: 12),
                FilledButton(onPressed: _login, child: const Text('Log in')),
              ]);
            case _Screen.logged:
              body = Column(mainAxisSize: MainAxisSize.min, children: [
                const Text('Logged in.'),
                const SizedBox(height: 12),
                FilledButton(
                  onPressed: _startChangePassword,
                  child: const Text('Change password'),
                ),
              ]);
            case _Screen.stepUpOtp:
              body = Column(mainAxisSize: MainAxisSize.min, children: [
                const Text('Enter the code we just sent you.'),
                TextField(
                  controller: _otp,
                  keyboardType: TextInputType.number,
                  decoration: const InputDecoration(labelText: 'OTP code'),
                ),
                const SizedBox(height: 12),
                FilledButton(onPressed: _submitOtp, child: const Text('Verify')),
              ]);
            case _Screen.newPassword:
              body = Column(mainAxisSize: MainAxisSize.min, children: [
                const Text('Choose a new password.'),
                TextField(
                  controller: _newPassword,
                  obscureText: true,
                  decoration: const InputDecoration(labelText: 'New password'),
                ),
                const SizedBox(height: 12),
                FilledButton(onPressed: _savePassword, child: const Text('Save')),
              ]);
            case _Screen.done:
              body = Column(mainAxisSize: MainAxisSize.min, children: const [
                Text('Password updated.', style: TextStyle(fontSize: 18)),
                SizedBox(height: 8),
                Text('`prld:pwd:write` has been consumed.',
                    style: TextStyle(color: Colors.grey)),
              ]);
          }

          return MaterialApp(
            home: Scaffold(
              body: Padding(
                padding: const EdgeInsets.all(24),
                child: Center(
                  child: Column(mainAxisSize: MainAxisSize.min, children: [
                    const Text('Change Password Demo', style: TextStyle(fontSize: 18)),
                    const SizedBox(height: 12),
                    if (_error != null) ...[
                      Text(_error!, style: const TextStyle(color: Colors.red)),
                      const SizedBox(height: 8),
                    ],
                    body,
                  ]),
                ),
              ),
            ),
          );
        }
      }
      ```
    </Tab>

    <Tab title="React Native">
      Replace `app/index.tsx`:

      ```tsx app/index.tsx theme={null}
      import {
        Endpoint,
        InvalidOTPCodeError,
        PreludeAuthClient,
        PreludeAuthError,
        RedactedString,
        StepUpChallenge,
      } from "@prelude.so/react-native-auth-sdk";
      import { useEffect, useRef, useState } from "react";
      import { Button, Text, TextInput, View } from "react-native";

      const APP_ID = "YOUR_APP_ID";

      type Stage = "login" | "logged" | "stepUpOtp" | "newPassword" | "done";

      export default function Home() {
        const clientRef = useRef<PreludeAuthClient | null>(null);
        const [stage, setStage] = useState<Stage>("login");
        const [email, setEmail] = useState("");
        const [password, setPassword] = useState("");
        const [otp, setOtp] = useState("");
        const [newPassword, setNewPassword] = useState("");
        const challengeRef = useRef<StepUpChallenge | null>(null);
        const [error, setError] = useState<string | null>(null);

        useEffect(() => {
          const c = new PreludeAuthClient({
            endpoint: Endpoint.custom(`https://${APP_ID}.session.prelude.dev`),
          });
          clientRef.current = c;
          return () => { c.dispose().catch(() => {}); };
        }, []);

        async function run(fn: () => Promise<void>) {
          setError(null);
          try { await fn(); }
          catch (e) {
            if (e instanceof InvalidOTPCodeError) setError("Wrong code.");
            else if (e instanceof PreludeAuthError) setError(e.message);
            else setError(String(e));
          }
        }

        const login = () => run(async () => {
          await clientRef.current!.loginWithPassword({
            emailAddress: email,
            password: new RedactedString(password),
          });
          setStage("logged");
        });

        const startChangePassword = () => run(async () => {
          const c = await clientRef.current!.requestStepUp("prld:pwd:write");
          if (c.status === "block") {
            setError("Couldn't change your password — please try again later.");
            return;
          }
          if (c.currentStep === "verify_email" || c.currentStep === "verify_sms") {
            await clientRef.current!.sendStepUpOTP(c);
          }
          challengeRef.current = c;
          setStage("stepUpOtp");
        });

        const submitOtp = () => run(async () => {
          const c = challengeRef.current;
          if (!c) return;
          const next = await clientRef.current!.submitStepUpOTP(c, otp);
          if (next !== null) {
            if (next.currentStep === "verify_email" || next.currentStep === "verify_sms") {
              await clientRef.current!.sendStepUpOTP(next);
            }
            challengeRef.current = next;
          } else {
            setStage("newPassword");
          }
        });

        const savePassword = () => run(async () => {
          await clientRef.current!.changePassword(new RedactedString(newPassword));
          setStage("done");
        });

        return (
          <View style={{ padding: 24, gap: 8 }}>
            <Text style={{ fontSize: 18 }}>Change Password Demo</Text>
            {error && <Text style={{ color: "red" }}>{error}</Text>}
            {stage === "login" && (
              <>
                <TextInput
                  placeholder="user@example.com"
                  value={email}
                  onChangeText={setEmail}
                  autoCapitalize="none"
                  keyboardType="email-address"
                  style={{ borderWidth: 1, padding: 8 }}
                />
                <TextInput
                  placeholder="Current password"
                  value={password}
                  onChangeText={setPassword}
                  secureTextEntry
                  style={{ borderWidth: 1, padding: 8 }}
                />
                <Button title="Log in" onPress={login} />
              </>
            )}
            {stage === "logged" && (
              <Button title="Change password" onPress={startChangePassword} />
            )}
            {stage === "stepUpOtp" && (
              <>
                <Text>Enter the code we just sent you.</Text>
                <TextInput
                  placeholder="OTP code"
                  value={otp}
                  onChangeText={setOtp}
                  keyboardType="number-pad"
                  style={{ borderWidth: 1, padding: 8 }}
                />
                <Button title="Verify" onPress={submitOtp} />
              </>
            )}
            {stage === "newPassword" && (
              <>
                <Text>Choose a new password.</Text>
                <TextInput
                  placeholder="New password"
                  value={newPassword}
                  onChangeText={setNewPassword}
                  secureTextEntry
                  style={{ borderWidth: 1, padding: 8 }}
                />
                <Button title="Save" onPress={savePassword} />
              </>
            )}
            {stage === "done" && (
              <>
                <Text style={{ fontWeight: "600" }}>Password updated.</Text>
                <Text>`prld:pwd:write` has been consumed.</Text>
              </>
            )}
          </View>
        );
      }
      ```
    </Tab>
  </Tabs>

  Build and run, log in, then tap **Change password**. You'll receive an OTP on the identifier type configured above. After verification, enter a new password — the `prld:pwd:write` scope is consumed atomically on save and removed from the session.
</Accordion>
