> ## 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.

# Password

> Implement password-based authentication with the Prelude mobile SDKs.

## Log in a user

Submit the user's email and password. On success, the SDK caches the access token and attaches it to subsequent protected requests.

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

    do {
        let user = try await client.loginWithPassword(
            LoginWithPasswordOptions(
                emailAddress: "user@example.com",
                password: "SecureP@ssw0rd!"
            )
        )
        // User is now authenticated.
    } catch PreludeAuthError.unauthorized {
        // Invalid credentials
    } catch PreludeAuthError.invalidPassword {
        // Password does not meet the policy
    } catch PreludeAuthError.badRequest {
        // Invalid email format
    } catch PreludeAuthError.rateLimited {
        // Too many login attempts
    }
    ```

    The password is wrapped in `RedactedString` internally — `LoginWithPasswordOptions` is safe to `print` or `dump`, and the SDK never persists the plaintext.
  </Tab>

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

    try {
        val user = client.loginWithPassword(
            LoginWithPasswordOptions(
                identifier = "user@example.com",
                password = "SecureP@ssw0rd!",
            ),
        )
        // User is now authenticated.
    } catch (e: PreludeAuthError.Unauthorized) {
        // Invalid credentials
    } catch (e: PreludeAuthError.InvalidPassword) {
        // Password does not meet the policy
    } catch (e: PreludeAuthError.BadRequest) {
        // Invalid email format
    } catch (e: PreludeAuthError.RateLimited) {
        // Too many login attempts
    }
    ```

    The password is wrapped in `RedactedString` internally — `LoginWithPasswordOptions.toString()` renders the value as `<redacted>`, and the SDK never persists the plaintext. (JVM strings can't be wiped from memory, so this guards against accidental leaks rather than guaranteeing the password is never observable.)

    `loginWithPassword` is a `suspend` function — call it from a coroutine, e.g. `viewModelScope.launch { ... }`.
  </Tab>

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

    try {
      final user = await client.loginWithPassword(
        LoginWithPasswordOptions(
          emailAddress: 'user@example.com',
          password: 'SecureP@ssw0rd!',
        ),
      );
      // User is now authenticated.
    } on UnauthorizedException {
      // Invalid credentials
    } on InvalidPasswordException {
      // Password does not meet the policy
    } on BadRequestException {
      // Invalid email format
    } on RateLimitedException {
      // Too many login attempts
    }
    ```

    The password is wrapped in `RedactedString` internally — `LoginWithPasswordOptions.toString()` renders the value as `<redacted>`, and the SDK never persists the plaintext. (Dart strings can't be wiped from memory, so this guards against accidental leaks rather than guaranteeing the password is never observable.)

    If the password is already wrapped further up the call stack, use the named `LoginWithPasswordOptions.redacted` constructor:

    ```dart theme={null}
    LoginWithPasswordOptions.redacted(
      emailAddress: 'user@example.com',
      password: heldRedactedPassword,
    );
    ```
  </Tab>

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

    try {
      const user = await client.loginWithPassword({
        emailAddress: "user@example.com",
        password: new RedactedString("SecureP@ssw0rd!"),
      });
      // User is now authenticated.
    } catch (e) {
      if (e instanceof UnauthorizedError) {
        // Invalid credentials
      } else if (e instanceof InvalidPasswordError) {
        // Password does not meet the policy
      } else if (e instanceof BadRequestError) {
        // Invalid email format
      } else if (e instanceof RateLimitedError) {
        // Too many login attempts
      } else {
        throw e;
      }
    }
    ```

    Wrap the password in `RedactedString` — `toString()` and `toJSON()` render the value as `<redacted>`, so `console.log` and Hermes inspectors can't accidentally leak it. The SDK unwraps the secret only at the bridge boundary; it's never persisted in JS.
  </Tab>
</Tabs>

<Accordion title="Try it" icon="flask">
  <Tabs>
    <Tab title="iOS">
      Replace `ContentView.swift` with:

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

      private let appID = "YOUR_APP_ID"

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

          @Published var user: PreludeUser?
          @Published var error: String?

          func login(email: String, password: String) async {
              error = nil
              do {
                  user = try await client.loginWithPassword(
                      LoginWithPasswordOptions(emailAddress: email, password: password)
                  )
              } catch PreludeAuthError.unauthorized {
                  error = "Invalid email or password."
              } catch PreludeAuthError.invalidPassword {
                  error = "Password doesn't meet the requirements."
              } catch PreludeAuthError.badRequest {
                  error = "Invalid email format."
              } catch PreludeAuthError.rateLimited {
                  error = "Too many attempts. Please try again later."
              } catch {
                  self.error = "Something went wrong. Please try again."
              }
          }
      }

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

          var body: some View {
              VStack(spacing: 12) {
                  if let user = model.user {
                      Text("Logged in").font(.headline)
                      Text(user.profile.userID ?? "—").font(.caption.monospaced())
                  } else {
                      TextField("Email", text: $email)
                          .keyboardType(.emailAddress)
                          .textInputAutocapitalization(.never)
                      SecureField("Password", text: $password)
                      if let error = model.error {
                          Text(error).foregroundStyle(.red).font(.caption)
                      }
                      Button("Log In") {
                          Task { await model.login(email: email, password: password) }
                      }
                      .buttonStyle(.borderedProminent)
                  }
              }
              .textFieldStyle(.roundedBorder)
              .padding()
          }
      }
      ```
    </Tab>

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

      ```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.PasswordVisualTransformation
      import androidx.compose.ui.unit.dp
      import androidx.compose.ui.unit.sp
      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"

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

          var user by mutableStateOf<PreludeUser?>(null)
              private set
          var error by mutableStateOf<String?>(null)
              private set

          fun login(email: String, password: String) {
              error = null
              viewModelScope.launch {
                  try {
                      user = client.loginWithPassword(
                          LoginWithPasswordOptions(identifier = email, password = password),
                      )
                  } catch (e: PreludeAuthError.Unauthorized) {
                      error = "Invalid email or password."
                  } catch (e: PreludeAuthError.InvalidPassword) {
                      error = "Password doesn't meet the requirements."
                  } catch (e: PreludeAuthError.BadRequest) {
                      error = "Invalid email format."
                  } catch (e: PreludeAuthError.RateLimited) {
                      error = "Too many attempts. Please try again later."
                  } catch (e: PreludeAuthError) {
                      error = "Something went wrong. Please try again."
                  }
              }
          }
      }

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

                          Column(Modifier.fillMaxSize().padding(24.dp)) {
                              if (vm.user != null) {
                                  Text("Logged in", style = MaterialTheme.typography.headlineSmall)
                                  Text(vm.user?.profile?.userId ?: "—", fontSize = 12.sp)
                              } else {
                                  OutlinedTextField(
                                      value = email, onValueChange = { email = it },
                                      label = { Text("Email") },
                                      modifier = Modifier.fillMaxWidth(),
                                  )
                                  Spacer(Modifier.height(8.dp))
                                  OutlinedTextField(
                                      value = password, onValueChange = { password = it },
                                      label = { Text("Password") },
                                      visualTransformation = PasswordVisualTransformation(),
                                      modifier = Modifier.fillMaxWidth(),
                                  )
                                  vm.error?.let {
                                      Spacer(Modifier.height(8.dp))
                                      Text(it, color = MaterialTheme.colorScheme.error)
                                  }
                                  Spacer(Modifier.height(16.dp))
                                  Button(onClick = { vm.login(email, password) }) {
                                      Text("Log In")
                                  }
                              }
                          }
                      }
                  }
              }
          }
      }
      ```

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

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

      ```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());

      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();
        PreludeUser? _user;
        String? _error;

        Future<void> _login() async {
          setState(() => _error = null);
          try {
            final user = await client.loginWithPassword(
              LoginWithPasswordOptions(
                emailAddress: _email.text,
                password: _password.text,
              ),
            );
            setState(() => _user = user);
          } on UnauthorizedException {
            setState(() => _error = 'Invalid email or password.');
          } on InvalidPasswordException {
            setState(() => _error = 'Password doesn\'t meet the requirements.');
          } on BadRequestException {
            setState(() => _error = 'Invalid email format.');
          } on RateLimitedException {
            setState(() => _error = 'Too many attempts. Please try again later.');
          } on PreludeAuthException {
            setState(() => _error = 'Something went wrong. Please try again.');
          }
        }

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

        @override
        Widget build(BuildContext context) {
          return MaterialApp(
            home: Scaffold(
              body: Padding(
                padding: const EdgeInsets.all(24),
                child: Center(
                  child: _user != null
                      ? Column(mainAxisSize: MainAxisSize.min, children: [
                          const Text('Logged in', style: TextStyle(fontSize: 20)),
                          const SizedBox(height: 8),
                          Text(_user!.profile.userID ?? '—',
                              style: const TextStyle(fontFamily: 'monospace')),
                        ])
                      : Column(mainAxisSize: MainAxisSize.min, children: [
                          TextField(
                            controller: _email,
                            decoration: const InputDecoration(labelText: 'Email'),
                            keyboardType: TextInputType.emailAddress,
                          ),
                          const SizedBox(height: 8),
                          TextField(
                            controller: _password,
                            obscureText: true,
                            decoration: const InputDecoration(labelText: 'Password'),
                          ),
                          if (_error != null) ...[
                            const SizedBox(height: 8),
                            Text(_error!, style: const TextStyle(color: Colors.red)),
                          ],
                          const SizedBox(height: 16),
                          FilledButton(onPressed: _login, child: const Text('Log In')),
                        ]),
                ),
              ),
            ),
          );
        }
      }
      ```
    </Tab>

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

      ```tsx app/index.tsx theme={null}
      import {
        BadRequestError,
        Endpoint,
        InvalidPasswordError,
        PreludeAuthClient,
        PreludeUser,
        RateLimitedError,
        RedactedString,
        UnauthorizedError,
      } 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";

      export default function Home() {
        const clientRef = useRef<PreludeAuthClient | null>(null);
        const [email, setEmail] = useState("");
        const [password, setPassword] = useState("");
        const [user, setUser] = useState<PreludeUser | 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 logIn() {
          setError(null);
          const client = clientRef.current;
          if (!client) return;
          try {
            const u = await client.loginWithPassword({
              emailAddress: email,
              password: new RedactedString(password),
            });
            setUser(u);
          } catch (e) {
            if (e instanceof UnauthorizedError) setError("Invalid email or password.");
            else if (e instanceof InvalidPasswordError) setError("Password doesn't meet the requirements.");
            else if (e instanceof BadRequestError) setError("Invalid email format.");
            else if (e instanceof RateLimitedError) setError("Too many attempts. Please try again later.");
            else setError("Something went wrong. Please try again.");
          }
        }

        if (user) {
          return (
            <View style={{ flex: 1, alignItems: "center", justifyContent: "center" }}>
              <Text style={{ fontWeight: "600" }}>Logged in</Text>
              <Text>{user.profile.userID ?? "—"}</Text>
            </View>
          );
        }

        return (
          <View style={{ padding: 24, gap: 8 }}>
            <TextInput
              placeholder="Email"
              value={email}
              onChangeText={setEmail}
              autoCapitalize="none"
              keyboardType="email-address"
              style={{ borderWidth: 1, padding: 8 }}
            />
            <TextInput
              placeholder="Password"
              value={password}
              onChangeText={setPassword}
              secureTextEntry
              style={{ borderWidth: 1, padding: 8 }}
            />
            {error && <Text style={{ color: "red" }}>{error}</Text>}
            <Button title="Log In" onPress={logIn} />
          </View>
        );
      }
      ```
    </Tab>
  </Tabs>
</Accordion>

## Validate a password (optional)

Your application can require passwords to meet certain rules — for example, a minimum length and a mix of uppercase, lowercase, number, or symbol characters. These rules are configured per application and enforced on both the client and the server.

Before submitting a sign-up form, validate the password against the configured rules. The SDK fetches the rules from the server and checks the password locally:

<Tabs>
  <Tab title="iOS">
    ```swift theme={null}
    let results = try await client.validatePassword("SecureP@ssw0rd!")

    if !results.valid {
        for result in results.results where !result.valid {
            print("\(result.criterion): expected \(result.expected), got \(result.actual)")
        }
    }
    ```

    If you already have the rules in hand — for example, fetched once and cached — you can validate without an extra round-trip:

    ```swift theme={null}
    let compliancy = try await client.passwordCompliancy()
    let results = PreludeAuthClient.validate(
        password: "SecureP@ssw0rd!",
        against: compliancy
    )
    ```

    Character classification uses Unicode `generalCategory` and counts code points, so passwords containing emoji or combining sequences classify consistently.
  </Tab>

  <Tab title="Android">
    ```kotlin theme={null}
    val results = client.validatePassword("SecureP@ssw0rd!")

    if (!results.valid) {
        results.results
            .filter { !it.valid }
            .forEach { r ->
                println("${r.criterion.wireValue}: expected ${r.expected}, got ${r.actual}")
            }
    }
    ```

    For live-typing UIs (where you classify on every keystroke), fetch the rules once and reuse them — `PreludePasswordCompliancy.validate` is a pure function with no I/O:

    ```kotlin theme={null}
    val policy = client.getPasswordCompliancy()
    val results = policy.validate("SecureP@ssw0rd!")
    ```

    Character classification uses the JVM's Unicode `Character.getType` and counts code points, so non-ASCII letters and combining sequences classify consistently with the server.
  </Tab>

  <Tab title="Flutter">
    ```dart theme={null}
    final results = await client.validatePassword('SecureP@ssw0rd!');

    if (!results.valid) {
      for (final r in results.results.where((r) => !r.valid)) {
        print('${r.criterion}: expected ${r.expected}, got ${r.actual}');
      }
    }
    ```

    For live-typing UIs (where you classify on every keystroke), fetch the rules once and reuse them — `PreludeAuthClient.validate` is a pure static function with no I/O:

    ```dart theme={null}
    final compliancy = await client.passwordCompliancy();
    final results = PreludeAuthClient.validate(
      password: 'SecureP@ssw0rd!',
      against: compliancy,
    );
    ```

    Length is counted in Unicode code points, not Dart's UTF-16 `length`, so emoji and astral-plane characters classify consistently with the server.
  </Tab>

  <Tab title="React Native">
    ```ts theme={null}
    const results = await client.validatePassword("SecureP@ssw0rd!");

    if (!results.valid) {
      for (const r of results.results.filter((r) => !r.valid)) {
        console.log(`${r.criterion}: expected ${r.expected}, got ${r.actual}`);
      }
    }
    ```

    For live-typing UIs, fetch the policy once and reuse it — `PreludeAuthClient.validate` is a pure static function with no I/O:

    ```ts theme={null}
    const compliancy = await client.passwordCompliancy();
    const results = PreludeAuthClient.validate("SecureP@ssw0rd!", compliancy);
    ```
  </Tab>
</Tabs>

<Accordion title="Try it" icon="flask">
  <Tabs>
    <Tab title="iOS">
      Replace `ContentView.swift` with:

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

      private let appID = "YOUR_APP_ID"

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

          @Published var results: PreludePasswordCompliancyResults?

          func validate(_ password: String) async {
              guard !password.isEmpty else { results = nil; return }
              results = try? await client.validatePassword(password)
          }
      }

      struct ContentView: View {
          @StateObject private var model = ValidatorModel()
          @State private var password = ""

          var body: some View {
              VStack(alignment: .leading, spacing: 12) {
                  Text("Password Validator").font(.title3)
                  SecureField("Type a password…", text: $password)
                      .textFieldStyle(.roundedBorder)
                      .onChange(of: password) { _, new in
                          Task { await model.validate(new) }
                      }
                  if let results = model.results {
                      ForEach(results.results, id: \.criterion) { r in
                          HStack {
                              Image(systemName: r.valid ? "checkmark.circle.fill" : "xmark.circle.fill")
                                  .foregroundStyle(r.valid ? .green : .red)
                              Text("\(r.criterion.rawValue): \(r.actual)/\(r.expected)")
                                  .font(.callout.monospaced())
                          }
                      }
                      Text(results.valid ? "Password is valid" : "Password does not meet requirements")
                          .font(.headline)
                          .foregroundStyle(results.valid ? .green : .red)
                  }
              }
              .padding()
          }
      }
      ```
    </Tab>

    <Tab title="Android">
      Replace `MainActivity.kt` with a Compose validator that re-runs on every keystroke against a single cached policy:

      ```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.material.icons.Icons
      import androidx.compose.material.icons.filled.Cancel
      import androidx.compose.material.icons.filled.CheckCircle
      import androidx.compose.material3.*
      import androidx.compose.runtime.*
      import androidx.compose.ui.Modifier
      import androidx.compose.ui.graphics.Color
      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"

      class ValidatorViewModel(ctx: android.content.Context) : ViewModel() {
          val client = PreludeAuthClient(
              context = ctx,
              baseUrl = URL("https://$APP_ID.session.prelude.dev"),
          )
          var policy by mutableStateOf<PreludePasswordCompliancy?>(null)
              private set
          var results by mutableStateOf<PreludePasswordCompliancyResults?>(null)
              private set

          init {
              viewModelScope.launch {
                  policy = runCatching { client.getPasswordCompliancy() }.getOrNull()
              }
          }

          fun onChange(password: String) {
              results = if (password.isEmpty()) null else policy?.validate(password)
          }
      }

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

                          Column(Modifier.fillMaxSize().padding(24.dp)) {
                              Text("Password Validator", style = MaterialTheme.typography.titleMedium)
                              Spacer(Modifier.height(8.dp))
                              OutlinedTextField(
                                  value = password,
                                  onValueChange = { password = it; vm.onChange(it) },
                                  label = { Text("Type a password…") },
                                  visualTransformation = PasswordVisualTransformation(),
                                  modifier = Modifier.fillMaxWidth(),
                              )
                              Spacer(Modifier.height(12.dp))
                              vm.results?.let { r ->
                                  r.results.forEach { result ->
                                      Row {
                                          Icon(
                                              if (result.valid) Icons.Filled.CheckCircle else Icons.Filled.Cancel,
                                              contentDescription = null,
                                              tint = if (result.valid) Color(0xFF2E7D32) else MaterialTheme.colorScheme.error,
                                          )
                                          Spacer(Modifier.width(8.dp))
                                          Text("${result.criterion}: ${result.actual}/${result.expected}")
                                      }
                                  }
                                  Spacer(Modifier.height(8.dp))
                                  Text(
                                      if (r.valid) "Password is valid"
                                      else "Password does not meet requirements",
                                      style = MaterialTheme.typography.titleSmall,
                                      color = if (r.valid) Color(0xFF2E7D32) else MaterialTheme.colorScheme.error,
                                  )
                              }
                          }
                      }
                  }
              }
          }
      }
      ```

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

    <Tab title="Flutter">
      Replace `lib/main.dart` with a live validator that re-runs against a single cached policy:

      ```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());

      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'),
        );

        PreludePasswordCompliancy? _policy;
        PreludePasswordCompliancyResults? _results;
        final _password = TextEditingController();

        @override
        void initState() {
          super.initState();
          client.passwordCompliancy().then((p) => setState(() => _policy = p));
        }

        void _onChanged(String value) {
          final policy = _policy;
          if (policy == null) return;
          setState(() {
            _results = value.isEmpty
                ? null
                : PreludeAuthClient.validate(password: value, against: policy);
          });
        }

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

        @override
        Widget build(BuildContext context) {
          final results = _results;
          return MaterialApp(
            home: Scaffold(
              body: Padding(
                padding: const EdgeInsets.all(24),
                child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
                  const Text('Password Validator',
                      style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600)),
                  const SizedBox(height: 8),
                  TextField(
                    controller: _password,
                    obscureText: true,
                    onChanged: _onChanged,
                    decoration: const InputDecoration(labelText: 'Type a password…'),
                  ),
                  const SizedBox(height: 16),
                  if (results != null) ...[
                    for (final r in results.results)
                      Row(children: [
                        Icon(
                          r.valid ? Icons.check_circle : Icons.cancel,
                          color: r.valid ? Colors.green : Colors.red,
                          size: 18,
                        ),
                        const SizedBox(width: 8),
                        Text('${r.criterion.name}: ${r.actual}/${r.expected}'),
                      ]),
                    const SizedBox(height: 8),
                    Text(
                      results.valid
                          ? 'Password is valid'
                          : 'Password does not meet requirements',
                      style: TextStyle(
                        fontWeight: FontWeight.w600,
                        color: results.valid ? Colors.green : Colors.red,
                      ),
                    ),
                  ],
                ]),
              ),
            ),
          );
        }
      }
      ```
    </Tab>

    <Tab title="React Native">
      Replace `app/index.tsx` with a live validator backed by a cached compliancy policy:

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

      const APP_ID = "YOUR_APP_ID";

      export default function Home() {
        const clientRef = useRef<PreludeAuthClient | null>(null);
        const [policy, setPolicy] = useState<PreludePasswordCompliancy | null>(null);
        const [results, setResults] = useState<PreludePasswordCompliancyResults | null>(null);
        const [password, setPassword] = useState("");

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

        function onChange(value: string) {
          setPassword(value);
          if (!policy || value.length === 0) {
            setResults(null);
            return;
          }
          setResults(PreludeAuthClient.validate(value, policy));
        }

        return (
          <View style={{ padding: 24, gap: 8 }}>
            <Text style={{ fontSize: 18, fontWeight: "600" }}>Password Validator</Text>
            <TextInput
              placeholder="Type a password…"
              value={password}
              onChangeText={onChange}
              secureTextEntry
              style={{ borderWidth: 1, padding: 8 }}
            />
            {results?.results.map((r) => (
              <Text key={r.criterion} style={{ color: r.valid ? "green" : "red" }}>
                {r.criterion}: {r.actual}/{r.expected}
              </Text>
            ))}
            {results && (
              <Text style={{ fontWeight: "600", color: results.valid ? "green" : "red" }}>
                {results.valid ? "Password is valid" : "Password does not meet requirements"}
              </Text>
            )}
          </View>
        );
      }
      ```
    </Tab>
  </Tabs>
</Accordion>
