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

# Idle Lock

> Lock the session after time in the background, and unlock it with an app passcode or biometrics with the Prelude mobile SDKs.

The idle lock protects a signed-in session while the app is not in use. After a set time in the background, the SDK locks the session. To continue, the user enters an app passcode or, if enabled, passes Face ID, Touch ID, or a fingerprint check. The unlock works offline: no network call and no new sign-in.

While the session is locked, the client is sealed:

* The profile and the access token are hidden.
* Every protected call and every refresh fails with a "session locked" error.
* On unlock, the SDK restores the session from the secure store, without a network call.

Apps that do not create an idle lock see no change.

Pick your platform once: tabs across this section stay in sync.

## Requirements

| Platform | SDK version | Notes |
| - | - | - |
| iOS | `PreludeAuth` 0.8.0 or later | Needs UIKit. Use 0.8.1 or later to set a passcode on the iOS Simulator. |
| Android | `so.prelude.android:auth-sdk` 0.8.0 or later | The client must be built with a `Context`. |
| React Native | `@prelude.so/react-native-auth-sdk` 0.8.1 or later | Use 0.8.1: with 0.8.0, setting a passcode fails on the iOS Simulator. |

> [!NOTE]
>
> The Flutter SDK does not have the idle lock yet.

## How it works

1. **Create the lock** with a threshold, the number of seconds the app can stay in the background before it locks, and start it.
2. **Set a passcode** after sign-in. The lock does nothing until a passcode is set, so there is always a way back in. Show a "set a passcode" screen while no passcode is set.
3. **Optionally enable biometrics** as a shortcut past the passcode.
4. **The app goes to the background.** When it comes back after more than the threshold, the SDK seals the client and reports the lock as locked. Show your lock screen.
5. **The user unlocks** with the passcode or with biometrics. The SDK unseals the client and the session continues.
6. **On sign-out**, disable the lock. This forgets the passcode and the biometric shortcut.

By default, the lock also engages when the app starts (`lockOnStart`), so closing and reopening the app is not a way past it.

Wrong passcodes are limited to `maxAttempts` (default 5). After the last one, unlocking with the passcode fails with a "locked out" error: sign the user out.

> [!NOTE]
>
> The passcode never leaves the device. On a device, the SDK protects it with the Keychain (iOS) or a key in the Android Keystore, and the biometric shortcut is an item that the system only releases after a successful biometric check.

## Platform setup

> [tab] iOS
>
> For biometrics, add `NSFaceIDUsageDescription` to your app's `Info.plist`. iOS shows this text the first time the app uses Face ID:
>
> ```xml theme={null}
> <key>NSFaceIDUsageDescription</key>
> <string>Unlock the app with Face ID.</string>
> ```

> [tab] Android
>
> Build the client with a `Context`: the lock keeps the passcode in the app's storage.
>
> Biometrics is optional. The SDK references `androidx.biometric` as `compileOnly`, so apps that skip biometrics pull no extra dependency. To use biometrics, add it to your module's `build.gradle.kts`:
>
> ```kotlin theme={null}
> dependencies {
>     implementation("androidx.biometric:biometric:1.1.0")
> }
> ```
>
> Without it, `isBiometricsAvailable` is `false`. Biometric unlock needs a strong (class 3) biometric enrolled on the device, and runs from a `FragmentActivity`.

> [tab] React Native
>
> For biometrics on iOS, add `NSFaceIDUsageDescription` to `expo.ios.infoPlist`:
>
> ```jsonc theme={null}
> "ios": {
>   "infoPlist": {
>     "NSFaceIDUsageDescription": "Unlock the app with Face ID."
>   }
> }
> ```
>
> On Android, the SDK already includes `androidx.biometric`. Nothing else is needed.

## Create and start the lock

Create one lock per client, start it once, and keep it for the life of the client.

> [tab] iOS
>
> ```swift theme={null}
> import PreludeAuth
>
> let idleLock = PreludeIdleLock(
>     client: client,
>     threshold: 60,                       // seconds in the background
>     unlockReason: "Unlock to continue"   // shown on the Face ID / Touch ID sheet
> )
> idleLock.start()
> ```
>
> `PreludeIdleLock` is an `ObservableObject`. Its published properties drive your screens:
>
> | Property | Meaning |
> | - | - |
> | `isLocked` | The session is sealed. Show the lock screen. |
> | `isEnabled` | A passcode is set. Show the passcode setup while `false`. |
> | `isBiometricsEnabled` | The biometric shortcut is on. |
> | `isBiometricsAvailable` | The device has enrolled biometrics. |
>
> Other parameters: `lockOnStart` (default `true`) and `maxAttempts` (default 5).

> [tab] Android
>
> ```kotlin theme={null}
> import so.prelude.android.auth.lock.PreludeIdleLock
>
> val idleLock = PreludeIdleLock(
>     client = client,
>     thresholdMs = 60_000L,               // time in the background
>     unlockTitle = "Unlock to continue",  // shown on the biometric prompt
> )
> idleLock.start()
> ```
>
> The state is exposed as `StateFlow`s:
>
> | Property | Meaning |
> | - | - |
> | `isLocked` | The session is sealed. Show the lock screen. |
> | `isEnabled` | A passcode is set. Show the passcode setup while `false`. |
> | `isBiometricsEnabled` | The biometric shortcut is on. |
> | `isBiometricsAvailable` | `androidx.biometric` is present and the device has a strong biometric enrolled. |
>
> Other parameters: `lockOnStart` (default `true`) and `maxAttempts` (default 5).

> [tab] React Native
>
> ```ts theme={null}
> const idleLock = client.idleLock({
>   thresholdSeconds: 60,
>   unlockReason: "Unlock to continue", // shown on the biometric prompt
> });
> await idleLock.start();
>
> const { isLocked, isEnabled, isBiometricsEnabled, isBiometricsAvailable } = idleLock.getState();
> const unsubscribe = idleLock.subscribe(() => render(idleLock.getState()));
> ```
>
> `client.idleLock()` returns the same lock on every call. Other options: `lockOnStart` (default `true`) and `maxAttempts` (default 5).
>
> With the React layer, pass the options to the provider instead. See [React hooks](#react-hooks).

## Set the passcode

Ask for a passcode right after sign-in, while `isEnabled` is `false`. The passcode needs at least 4 characters. Setting a new passcode replaces the old one and turns biometrics off, so the user enables it again.

> [tab] iOS
>
> ```swift theme={null}
> try idleLock.setPasscode(passcode)
> ```

> [tab] Android
>
> ```kotlin theme={null}
> idleLock.setPasscode(passcode)
> ```

> [tab] React Native
>
> ```ts theme={null}
> await idleLock.setPasscode(passcode);
> ```

## Enable biometrics

Offer biometrics when `isBiometricsAvailable` is `true`. It needs a passcode first.

> [tab] iOS
>
> ```swift theme={null}
> if idleLock.isBiometricsAvailable {
>     try idleLock.enableBiometrics()
> }
> // later: idleLock.disableBiometrics()
> ```

> [tab] Android
>
> ```kotlin theme={null}
> if (idleLock.isBiometricsAvailable) {
>     idleLock.enableBiometrics()
> }
> // later: idleLock.disableBiometrics()
> ```

> [tab] React Native
>
> ```ts theme={null}
> if (idleLock.getState().isBiometricsAvailable) {
>   await idleLock.enableBiometrics();
> }
> // later: await idleLock.disableBiometrics();
> ```

## Unlock

Show your lock screen while `isLocked` is `true`. The SDK does not show a biometric prompt on its own: call `unlockWithBiometrics` from a button, or when the lock screen appears.

> [tab] iOS
>
> ```swift theme={null}
> do {
>     try await idleLock.unlock(passcode: passcode)
> } catch PreludeAuthError.invalidPasscode(let message) {
>     // Wrong passcode; the message says how many attempts remain.
> } catch PreludeAuthError.lockedOut {
>     idleLock.disable()
>     try await client.logout()
> }
>
> // Biometrics (Face ID / Touch ID):
> try await idleLock.unlockWithBiometrics()
> ```

> [tab] Android
>
> ```kotlin theme={null}
> try {
>     idleLock.unlock(passcode)
> } catch (e: PreludeAuthError.InvalidPasscode) {
>     // Wrong passcode; the message says how many attempts remain.
> } catch (e: PreludeAuthError.LockedOut) {
>     idleLock.disable()
>     client.logout()
> }
>
> // Biometrics, from a FragmentActivity:
> idleLock.unlockWithBiometrics(activity)
> ```

> [tab] React Native
>
> ```ts theme={null}
> import { InvalidPasscodeError, LockedOutError } from "@prelude.so/react-native-auth-sdk";
>
> try {
>   await idleLock.unlock(passcode);
> } catch (e) {
>   if (e instanceof InvalidPasscodeError) {
>     // Wrong passcode; the message says how many attempts remain.
>   } else if (e instanceof LockedOutError) {
>     await idleLock.disable();
>     await client.logout();
>   }
> }
>
> // Biometrics:
> await idleLock.unlockWithBiometrics();
> ```

To lock the session at once, for example from a "Lock now" button, call `lock()`. It does nothing when no passcode is set.

## Sign out

On sign-out, call `disable()`. It forgets the passcode and the biometric shortcut and removes the lock, so the next user of the device starts without them. With the React layer, `signOut()` does this for you.

## React hooks

With `PreludeAuthProvider`, pass the lock options as the `idleLock` prop. The provider creates and starts the lock before it restores the session, so with `lockOnStart` a relaunch lands on the lock screen.

```tsx theme={null}
import {
  Locked,
  PreludeAuthProvider,
  SignedIn,
  SignedOut,
  useIdleLock,
} from "@prelude.so/react-native-auth-sdk/react";

export default function App() {
  return (
    <PreludeAuthProvider
      options={{ endpoint }}
      idleLock={{ thresholdSeconds: 60, unlockReason: "Unlock to continue" }}
    >
      <SignedOut><LoginScreen /></SignedOut>
      <Locked><LockScreen /></Locked>
      <SignedIn><Home /></SignedIn>
    </PreludeAuthProvider>
  );
}

function LockScreen() {
  const { unlock, unlockWithBiometrics, isBiometricsEnabled, pending, error } = useIdleLock();
  // A passcode field that calls unlock(passcode), and a biometrics button:
  // {isBiometricsEnabled && <Button title="Use biometrics" onPress={unlockWithBiometrics} />}
}
```

* While the session is locked, `useAuth().stage` is `"locked"` and `user` is `null`, so `SignedIn` unmounts and `Locked` renders.
* In `Home`, show a passcode setup while `useIdleLock().isEnabled` is `false`.
* Hook actions never throw: a wrong passcode lands in `useIdleLock().error`. On `LockedOutError` the provider signs the user out.
* Every path that ends the session (`signOut`, a rejected refresh, a launch with no session) forgets the passcode.

## Errors

| Error | Triggered by | Typical recovery |
| - | - | - |
| Session locked | A protected call or a refresh while the session is locked | Unlock first |
| Invalid passcode | A wrong passcode; attempts remain | Let the user try again. The message says how many attempts remain |
| Locked out | The last allowed wrong passcode | Disable the lock and sign the user out |
| Lock unavailable | Biometrics is not enabled or not available, the biometric check failed or was cancelled, or the device cannot store the lock | Offer the passcode |

Each SDK names these errors in its own convention, for example `PreludeAuthError.lockedOut` (iOS), `PreludeAuthError.LockedOut` (Android), and `LockedOutError` (React Native).

## Test on a simulator or emulator

* **iOS Simulator**: setting a passcode needs `PreludeAuth` 0.8.1 or later (React Native 0.8.1 or later). For biometrics, turn on Features > Face ID > Enrolled. The simulator does not show the Face ID sheet for the biometric unlock, so test the face check itself on a device.
* **Android emulator**: set a screen lock and enroll a fingerprint in Settings > Security, then touch the sensor from the emulator's extended controls (Fingerprint), or with `adb -e emu finger touch 1`.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.