diff --git a/bounty-7/DESIGN.md b/bounty-7/DESIGN.md
new file mode 100644
index 00000000..6f6f7455
--- /dev/null
+++ b/bounty-7/DESIGN.md
@@ -0,0 +1,123 @@
+# Design: Note Locking with Biometrics / PIN
+
+## Architecture Overview
+
+The note-locking system uses a **centralized MobX store** (`LockStore`) that holds all lock-related state, with a **custom React hook** (`useNoteLock`) bridging the store to components. The auth layer supports **biometric** (Face ID / Touch ID) and **PIN** authentication, with configurable fallback priority.
+
+```
+┌─────────────────────────────────────────────────┐
+│ Components │
+│ LockToggle LockScreen LockedNoteCard │
+│ BiometricAuth PINSetup │
+└──────────────────────┬──────────────────────────┘
+ │ useNoteLock()
+┌──────────────────────▼──────────────────────────┐
+│ useNoteLock hook │
+│ - AppState listeners (foreground/background) │
+│ - Biometric availability check │
+│ - Auth orchestration (biometric → PIN fallback) │
+└──────────────────────┬──────────────────────────┘
+ │ reads / writes
+┌──────────────────────▼──────────────────────────┐
+│ LockStore (MobX) │
+│ - Per-note LockState map │
+│ - Global lock / unlock │
+│ - PIN hashing & verification │
+│ - Auto-lock timeout tracking │
+│ - Biometric status │
+│ - Serialization for persistence │
+└─────────────────────────────────────────────────┘
+```
+
+## Component Tree
+
+```
+App
+├── NoteList
+│ ├── LockedNoteCard ← shows locked preview
+│ │ └── LockToggle ← lock/unlock button
+│ └── LockScreen ← full-screen auth gate (modal)
+│ ├── BiometricAuth ← Face ID / Touch ID prompt
+│ └── PIN entry ← inline 4-digit keypad
+└── Settings
+ ├── BiometricAuth ← toggle / status
+ └── PINSetup ← create or change PIN
+```
+
+## Data Flow
+
+### Locking a Note
+1. User taps `LockToggle` → `useNoteLock.toggle()` → `lockStore.lockNote(id)`
+2. `LockState` → `'locked'`, `note.isLocked = true`
+3. `LockedNoteCard` re-renders (via MobX observer) showing locked UI
+4. `LockScreen` modal is presented when user tries to view the locked note
+
+### Unlocking a Note
+1. User taps locked note or unlock button → `useNoteLock.unlock()`
+2. Hook checks `authMethod` priority (biometric → PIN fallback)
+3. `BiometricAuth` component triggers `LocalAuthentication.authenticateAsync()`
+4. If biometrics succeed → `lockStore.unlockNote(id)` → `LockState` → `'unlocked'`
+5. If biometrics fail / fallback → PIN entry is shown
+6. PIN is verified against stored hash → success unlocks, failure increments counter
+7. On 5 consecutive failures → 60-second lockout
+
+### Auto-Lock (Timeout + Restart)
+- `lastUnlockTimestamp` is updated on every unlock
+- `AppState` listener in `useNoteLock` detects foreground/background
+- On foreground: `checkAutoLock()` computes elapsed time; if > `timeoutMs` (default 5 min), `lockAll()` is called
+- On restart: config option `requireAuthOnRestart` ensures locked state is restored from persisted store
+
+## Auth Method Priority
+
+| `authMethod` | Behavior |
+|----------------------|-------------------------------------------------------|
+| `biometric` | Biometric only; no PIN fallback |
+| `pin` | PIN only; no biometric fallback |
+| `biometric_or_pin` | Biometric first; fallback to PIN on cancel/failure |
+| `none` | No authentication (dev/debug) |
+
+## PIN Security
+
+- PINs are stored as **non-reversible hashes** (`hashPIN` function — simple FNV-1a-like hash; replace with `expo-crypto` SHA-256 in production)
+- Max **5 attempts** before **60-second lockout**
+- PIN creation requires confirmation (enter → re-enter)
+- PIN change requires old PIN verification
+
+## State Machine (Per-Note)
+
+```
+ ┌──────────┐
+ │ locked │ ◄──── lockAll(), timeout, restart
+ └────┬─────┘
+ │ tap unlock
+ ▼
+ ┌──────────┐ auth success ┌──────────┐
+ │ unlocking│ ──────────────────► │ unlocked │
+ └──────────┘ └────┬─────┘
+ │ tap lock
+ ▼
+ ┌──────────┐
+ │ locked │
+ └──────────┘
+```
+
+## Persistence
+
+`lockStore.toJSON()` / `lockStore.fromJSON()` serializes:
+- `authMethod`
+- `_pinHash`
+- `lockedNoteIds`
+- `isGloballyLocked`
+- `config`
+
+This data should be stored in **expo-secure-store** (keychain) for encrypted persistence across app restarts.
+
+## Dependencies
+
+| Package | Purpose |
+|------------------------------|----------------------------------|
+| `mobx` / `mobx-react-lite` | State management |
+| `expo-local-authentication` | Face ID / Touch ID API |
+| `@expo/vector-icons` | Lock icons, biometric icons |
+| `expo-secure-store` | Encrypted persistence (optional) |
+| `expo-crypto` | Secure PIN hashing (optional) |
diff --git a/bounty-7/README.md b/bounty-7/README.md
new file mode 100644
index 00000000..61f19bf0
--- /dev/null
+++ b/bounty-7/README.md
@@ -0,0 +1,147 @@
+# Warpspeed Bounty #7 — Note Locking with Biometrics / PIN
+
+**Bounty: $660** — Lock notes with Face ID, Touch ID, or PIN.
+
+## Features
+
+- 🔒 **Lock / unlock individual notes** via lock toggle button
+- 👤 **Face ID / Touch ID** authentication using `expo-local-authentication`
+- 🔢 **4-digit PIN** with confirmation, change flow, and brute-force lockout
+- 🔀 **Smart fallback** — biometrics first, PIN as backup
+- 🕐 **Auto-lock after timeout** (configurable, default 5 min)
+- 🔄 **Re-auth on app restart** — locks persist across sessions
+- 🚫 **Content hidden until auth** — locked cards show blurred preview
+- 🎨 **Clear locked-state UI** — lock icons, red accents, locked badge
+
+## Project Structure
+
+```
+src/
+├── types.ts # All TypeScript types & constants
+├── state/
+│ └── lockStore.ts # MobX store — all lock state & logic
+├── hooks/
+│ └── useNoteLock.ts # Custom hook — bridges store → components
+└── components/
+ ├── LockToggle.tsx # Lock/unlock icon button
+ ├── LockScreen.tsx # Full-screen auth gate (modal)
+ ├── LockedNoteCard.tsx # Card showing locked state in list
+ ├── BiometricAuth.tsx # Face ID / Touch ID handler
+ └── PINSetup.tsx # PIN creation / change flow
+```
+
+## Getting Started
+
+```bash
+npm install mobx mobx-react-lite expo-local-authentication @expo/vector-icons
+```
+
+Wrap your app root with MobX provider (optional — the global `lockStore` singleton can be imported directly):
+
+```tsx
+import { lockStore } from './state/lockStore';
+```
+
+## Quick Usage
+
+### Lock/Unlock a Note
+
+```tsx
+import { LockToggle } from './components/LockToggle';
+
+
+```
+
+### Show Lock Screen for Locked Notes
+
+```tsx
+import { LockScreen } from './components/LockScreen';
+
+ setIsLocked(false)}
+ onDismiss={() => navigation.goBack()}
+/>
+```
+
+### Display Locked Note in List
+
+```tsx
+import { LockedNoteCard } from './components/LockedNoteCard';
+
+ setShowLockScreen(true)}
+/>
+```
+
+### Set Up / Change PIN
+
+```tsx
+import { PINSetup } from './components/PINSetup';
+
+ {}}
+ onCancel={() => {}}
+/>
+```
+
+### Biometric Auth Component
+
+```tsx
+import { BiometricAuth } from './components/BiometricAuth';
+
+
+```
+
+### Using the Hook
+
+```tsx
+import { useNoteLock } from './hooks/useNoteLock';
+
+function NoteDetail({ id }: { id: string }) {
+ const {
+ isLocked,
+ lockState,
+ lock,
+ unlock,
+ toggle,
+ hasBiometrics,
+ hasPIN,
+ authenticateWithBiometrics,
+ authenticateWithPIN,
+ } = useNoteLock({ noteId: id });
+
+ return (
+
+ Status: {lockState}
+
+
+ );
+}
+```
+
+## Configuration
+
+Adjust global defaults via `lockStore.updateConfig()`:
+
+```ts
+lockStore.updateConfig({
+ timeoutMs: 300000, // 5 min auto-lock
+ maxPinAttempts: 5, // lockout after 5 failures
+ lockoutDurationMs: 60000, // 60 sec lockout
+ authMethod: 'biometric_or_pin',
+});
+```
+
+## Important Notes
+
+- **PINs are hashed** using a non-cryptographic hash by default. Replace `hashPIN()` in `lockStore.ts` with `expo-crypto` SHA-256 for production use.
+- **Biometric state** is checked once on mount. Call `checkBiometrics()` from the hook to re-check.
+- **Persistence** is not automatic. Serialize `lockStore.toJSON()` to `expo-secure-store` and restore on launch via `lockStore.fromJSON()`.
+- All components are wrapped with `observer()` from `mobx-react-lite` for reactive updates.
diff --git a/bounty-7/components/BiometricAuth.tsx b/bounty-7/components/BiometricAuth.tsx
new file mode 100644
index 00000000..6addb540
--- /dev/null
+++ b/bounty-7/components/BiometricAuth.tsx
@@ -0,0 +1,190 @@
+import React, { useEffect, useState } from 'react';
+import {
+ View,
+ Text,
+ TouchableOpacity,
+ StyleSheet,
+ ActivityIndicator,
+} from 'react-native';
+import { Ionicons } from '@expo/vector-icons';
+import * as LocalAuthentication from 'expo-local-authentication';
+import { lockStore } from '../state/lockStore';
+import { BiometricStatus, BiometricType } from '../types';
+
+interface BiometricAuthProps {
+ onAuthenticated: () => void;
+ onFallback?: () => void;
+ promptMessage?: string;
+}
+
+export const BiometricAuth: React.FC = ({
+ onAuthenticated,
+ onFallback,
+ promptMessage = 'Authenticate to unlock note',
+}) => {
+ const [status, setStatus] = useState({ isAvailable: false });
+ const [isLoading, setIsLoading] = useState(true);
+ const [isAuthenticating, setIsAuthenticating] = useState(false);
+ const [error, setError] = useState(null);
+
+ useEffect(() => {
+ initBiometrics();
+ }, []);
+
+ const initBiometrics = async () => {
+ setIsLoading(true);
+ try {
+ const [compatible, enrolled, types] = await Promise.all([
+ LocalAuthentication.hasHardwareAsync(),
+ LocalAuthentication.isEnrolledAsync(),
+ LocalAuthentication.supportedAuthenticationTypesAsync(),
+ ]);
+
+ const isAvailable = compatible && enrolled;
+ let biometryType: BiometricType = null;
+
+ if (isAvailable) {
+ if (types.includes(LocalAuthentication.AuthenticationType.FACIAL_RECOGNITION)) {
+ biometryType = 'FaceID';
+ } else if (
+ types.includes(LocalAuthentication.AuthenticationType.FINGERPRINT) ||
+ types.includes(LocalAuthentication.AuthenticationType.IRIS)
+ ) {
+ biometryType = 'TouchID';
+ }
+ }
+
+ const newStatus: BiometricStatus = { isAvailable, biometryType };
+ setStatus(newStatus);
+ lockStore.setBiometricStatus(newStatus);
+
+ if (isAvailable) {
+ handleAuthenticate();
+ }
+ } catch {
+ setStatus({ isAvailable: false });
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ const handleAuthenticate = async () => {
+ setIsAuthenticating(true);
+ setError(null);
+
+ try {
+ const result = await LocalAuthentication.authenticateAsync({
+ promptMessage,
+ fallbackLabel: onFallback ? 'Use PIN' : undefined,
+ cancelLabel: 'Cancel',
+ disableDeviceFallback: !onFallback,
+ });
+
+ if (result.success) {
+ onAuthenticated();
+ } else if (result.error === 'user_cancel' && onFallback) {
+ onFallback();
+ } else {
+ setError(result.error === 'user_fallback' ? null : 'Authentication failed');
+ if (result.error === 'user_fallback' && onFallback) {
+ onFallback();
+ }
+ }
+ } catch {
+ setError('Biometric authentication error');
+ } finally {
+ setIsAuthenticating(false);
+ }
+ };
+
+ if (isLoading) {
+ return (
+
+
+ Checking biometrics...
+
+ );
+ }
+
+ if (!status.isAvailable) {
+ return null;
+ }
+
+ const iconName = status.biometryType === 'FaceID' ? 'ios-faceid' : 'finger-print';
+ const displayName = status.biometryType ?? 'Biometrics';
+
+ return (
+
+
+ {isAuthenticating ? (
+
+ ) : (
+ <>
+
+
+ {displayName === 'FaceID' ? 'Use Face ID' : 'Use Touch ID'}
+
+ >
+ )}
+
+
+ {error && {error}}
+
+ {onFallback && status.isAvailable && (
+
+ Enter PIN Instead
+
+ )}
+
+ );
+};
+
+const styles = StyleSheet.create({
+ container: {
+ alignItems: 'center',
+ width: '100%',
+ marginVertical: 8,
+ },
+ statusText: {
+ marginTop: 8,
+ fontSize: 14,
+ color: '#999',
+ },
+ authButton: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ justifyContent: 'center',
+ backgroundColor: '#3498db',
+ paddingVertical: 14,
+ paddingHorizontal: 24,
+ borderRadius: 12,
+ width: '100%',
+ gap: 10,
+ },
+ authButtonText: {
+ color: '#fff',
+ fontSize: 16,
+ fontWeight: '600',
+ },
+ error: {
+ color: '#e74c3c',
+ fontSize: 13,
+ marginTop: 8,
+ },
+ fallbackButton: {
+ marginTop: 12,
+ paddingVertical: 8,
+ paddingHorizontal: 16,
+ },
+ fallbackText: {
+ color: '#3498db',
+ fontSize: 14,
+ fontWeight: '500',
+ },
+});
diff --git a/bounty-7/components/LockScreen.tsx b/bounty-7/components/LockScreen.tsx
new file mode 100644
index 00000000..b73a4b2b
--- /dev/null
+++ b/bounty-7/components/LockScreen.tsx
@@ -0,0 +1,416 @@
+import React, { useState, useCallback, useEffect } from 'react';
+import {
+ View,
+ Text,
+ StyleSheet,
+ TouchableOpacity,
+ Modal,
+ ActivityIndicator,
+ SafeAreaView,
+} from 'react-native';
+import { Ionicons } from '@expo/vector-icons';
+import { observer } from 'mobx-react-lite';
+import { useNoteLock } from '../hooks/useNoteLock';
+import { BiometricAuth } from './BiometricAuth';
+import { lockStore } from '../state/lockStore';
+
+interface LockScreenProps {
+ noteId: string;
+ visible: boolean;
+ onUnlocked: () => void;
+ onDismiss?: () => void;
+ title?: string;
+}
+
+export const LockScreen: React.FC = observer(
+ ({ noteId, visible, onUnlocked, onDismiss, title }) => {
+ const { unlock, authenticateWithPIN } = useNoteLock({ noteId });
+ const [mode, setMode] = useState<'biometric' | 'pin'>('biometric');
+ const [isAuthenticating, setIsAuthenticating] = useState(false);
+ const [error, setError] = useState(null);
+ const [pin, setPin] = useState('');
+ const PIN_LENGTH = 4;
+
+ useEffect(() => {
+ if (visible) {
+ setMode(
+ lockStore.hasBiometrics && lockStore.authMethod !== 'pin'
+ ? 'biometric'
+ : 'pin',
+ );
+ setError(null);
+ setPin('');
+ }
+ }, [visible]);
+
+ const handleBiometricSuccess = useCallback(() => {
+ lockStore.unlockNote(noteId);
+ lockStore.unlockGlobally();
+ onUnlocked();
+ }, [noteId, onUnlocked]);
+
+ const handleBiometricFallback = useCallback(() => {
+ setMode('pin');
+ }, []);
+
+ const handlePINSubmit = useCallback(
+ async (enteredPIN: string) => {
+ setIsAuthenticating(true);
+ setError(null);
+
+ try {
+ const result = authenticateWithPIN(enteredPIN);
+ if (result.success) {
+ lockStore.unlockNote(noteId);
+ lockStore.unlockGlobally();
+ onUnlocked();
+ } else {
+ setError(
+ result.error ??
+ 'Incorrect PIN' +
+ (result.attemptsRemaining != null
+ ? ` (${result.attemptsRemaining} attempts left)`
+ : ''),
+ );
+ }
+ } catch {
+ setError('An unexpected error occurred');
+ } finally {
+ setIsAuthenticating(false);
+ }
+ },
+ [noteId, onUnlocked, authenticateWithPIN],
+ );
+
+ const handleDigit = useCallback(
+ (digit: string) => {
+ if (pin.length >= PIN_LENGTH) return;
+ const next = pin + digit;
+ setPin(next);
+ if (next.length === PIN_LENGTH) {
+ handlePINSubmit(next);
+ setPin('');
+ }
+ },
+ [pin, handlePINSubmit],
+ );
+
+ const handleDelete = useCallback(() => {
+ setPin((prev) => prev.slice(0, -1));
+ setError(null);
+ }, []);
+
+ const showBiometrics =
+ mode === 'biometric' && lockStore.hasBiometrics;
+ const showPIN = mode === 'pin' && lockStore.hasPIN;
+ const showSetupMessage = !lockStore.hasBiometrics && !lockStore.hasPIN;
+
+ return (
+
+
+
+
+
+
+
+
+
+
+ {title ?? 'Note Locked'}
+
+
+ Authenticate to view this note
+
+
+ {error && (
+
+
+ {error}
+
+ )}
+
+ {isAuthenticating && mode === 'pin' && (
+
+ )}
+
+ {showBiometrics && (
+
+ )}
+
+ {showPIN && (
+
+ {showBiometrics && (
+
+
+
+ Use{' '}
+ {lockStore.biometricStatus.biometryType === 'FaceID'
+ ? 'Face ID'
+ : 'Touch ID'}
+
+
+ )}
+
+ Enter PIN
+
+
+ {Array.from({ length: PIN_LENGTH }).map((_, i) => (
+ i && styles.dotFilled,
+ ]}
+ />
+ ))}
+
+
+
+ {[
+ ['1', '2', '3'],
+ ['4', '5', '6'],
+ ['7', '8', '9'],
+ ['', '0', 'DEL'],
+ ]
+ .flat()
+ .map((key, index) => (
+ {
+ if (key === 'DEL') handleDelete();
+ else if (key) handleDigit(key);
+ }}
+ disabled={!key}
+ activeOpacity={0.6}
+ >
+ {key === 'DEL' ? (
+
+ ) : (
+
+ {key}
+
+ )}
+
+ ))}
+
+
+ )}
+
+ {showSetupMessage && (
+
+
+
+ No authentication method configured.{'\n'}
+ Enable biometrics or set a PIN in settings.
+
+
+ )}
+
+ {onDismiss && (
+
+ Cancel
+
+ )}
+
+
+
+ );
+ },
+);
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ backgroundColor: '#fff',
+ },
+ content: {
+ flex: 1,
+ alignItems: 'center',
+ justifyContent: 'center',
+ paddingHorizontal: 32,
+ },
+ iconWrapper: {
+ marginBottom: 24,
+ },
+ iconCircle: {
+ width: 80,
+ height: 80,
+ borderRadius: 40,
+ backgroundColor: '#e74c3c',
+ justifyContent: 'center',
+ alignItems: 'center',
+ },
+ title: {
+ fontSize: 24,
+ fontWeight: '700',
+ color: '#1a1a1a',
+ marginBottom: 8,
+ },
+ subtitle: {
+ fontSize: 15,
+ color: '#888',
+ marginBottom: 32,
+ textAlign: 'center',
+ },
+ errorContainer: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ backgroundColor: '#fde8e8',
+ paddingVertical: 10,
+ paddingHorizontal: 16,
+ borderRadius: 10,
+ marginBottom: 16,
+ gap: 8,
+ maxWidth: '100%',
+ },
+ errorText: {
+ color: '#e74c3c',
+ fontSize: 14,
+ flexShrink: 1,
+ },
+ loader: {
+ marginVertical: 20,
+ },
+ pinSection: {
+ alignItems: 'center',
+ width: '100%',
+ },
+ switchRow: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ gap: 6,
+ marginBottom: 20,
+ paddingVertical: 8,
+ paddingHorizontal: 12,
+ },
+ switchText: {
+ color: '#3498db',
+ fontSize: 14,
+ fontWeight: '500',
+ },
+ pinPrompt: {
+ fontSize: 16,
+ color: '#666',
+ marginBottom: 16,
+ },
+ dotsRow: {
+ flexDirection: 'row',
+ gap: 16,
+ marginBottom: 28,
+ },
+ dot: {
+ width: 18,
+ height: 18,
+ borderRadius: 9,
+ borderWidth: 2,
+ borderColor: '#ccc',
+ backgroundColor: 'transparent',
+ },
+ dotFilled: {
+ backgroundColor: '#1a1a1a',
+ borderColor: '#1a1a1a',
+ },
+ keypad: {
+ flexDirection: 'row',
+ flexWrap: 'wrap',
+ justifyContent: 'center',
+ width: 264,
+ gap: 12,
+ },
+ key: {
+ width: 72,
+ height: 72,
+ borderRadius: 36,
+ justifyContent: 'center',
+ alignItems: 'center',
+ backgroundColor: '#f5f5f5',
+ },
+ keyEmpty: {
+ backgroundColor: 'transparent',
+ },
+ keyText: {
+ fontSize: 26,
+ fontWeight: '500',
+ color: '#1a1a1a',
+ },
+ hidden: {
+ color: 'transparent',
+ },
+ setupMessage: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ backgroundColor: '#f8f8f8',
+ paddingVertical: 14,
+ paddingHorizontal: 20,
+ borderRadius: 12,
+ gap: 10,
+ maxWidth: '100%',
+ },
+ setupText: {
+ color: '#999',
+ fontSize: 13,
+ lineHeight: 18,
+ flexShrink: 1,
+ },
+ cancelButton: {
+ marginTop: 32,
+ paddingVertical: 12,
+ paddingHorizontal: 32,
+ },
+ cancelText: {
+ color: '#999',
+ fontSize: 16,
+ fontWeight: '500',
+ },
+});
diff --git a/bounty-7/components/LockToggle.tsx b/bounty-7/components/LockToggle.tsx
new file mode 100644
index 00000000..b70327c6
--- /dev/null
+++ b/bounty-7/components/LockToggle.tsx
@@ -0,0 +1,73 @@
+import React from 'react';
+import {
+ TouchableOpacity,
+ StyleSheet,
+ ActivityIndicator,
+ ViewStyle,
+} from 'react-native';
+import { Ionicons } from '@expo/vector-icons';
+import { observer } from 'mobx-react-lite';
+import { useNoteLock } from '../hooks/useNoteLock';
+import { LockState } from '../types';
+
+interface LockToggleProps {
+ noteId: string;
+ size?: number;
+ color?: string;
+ lockedColor?: string;
+ style?: ViewStyle;
+ onToggle?: (nowLocked: boolean) => void;
+}
+
+export const LockToggle: React.FC = observer(
+ ({
+ noteId,
+ size = 22,
+ color = '#666',
+ lockedColor = '#e74c3c',
+ style,
+ onToggle,
+ }) => {
+ const { isLocked, lockState, toggle } = useNoteLock({ noteId });
+
+ const isLoading =
+ lockState === 'locking' || lockState === 'unlocking';
+
+ const handlePress = async () => {
+ if (isLoading) return;
+ const wasLocked = isLocked;
+ await toggle();
+ onToggle?.(!wasLocked);
+ };
+
+ const iconName = isLocked ? 'lock-closed' : 'lock-open-outline';
+ const iconColor = isLocked ? lockedColor : color;
+
+ return (
+
+ {isLoading ? (
+
+ ) : (
+
+ )}
+
+ );
+ },
+);
+
+const styles = StyleSheet.create({
+ container: {
+ padding: 8,
+ justifyContent: 'center',
+ alignItems: 'center',
+ minWidth: 40,
+ minHeight: 40,
+ },
+});
diff --git a/bounty-7/components/LockedNoteCard.tsx b/bounty-7/components/LockedNoteCard.tsx
new file mode 100644
index 00000000..bdee60b5
--- /dev/null
+++ b/bounty-7/components/LockedNoteCard.tsx
@@ -0,0 +1,139 @@
+import React from 'react';
+import {
+ View,
+ Text,
+ StyleSheet,
+ TouchableOpacity,
+ ViewStyle,
+} from 'react-native';
+import { Ionicons } from '@expo/vector-icons';
+import { SecureNote } from '../types';
+
+interface LockedNoteCardProps {
+ note: SecureNote;
+ onPress: () => void;
+ style?: ViewStyle;
+}
+
+export const LockedNoteCard: React.FC = ({
+ note,
+ onPress,
+ style,
+}) => {
+ return (
+
+
+
+
+
+
+
+ {note.title}
+
+ Locked
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {note.lockMethod === 'biometric'
+ ? 'Locked with biometrics'
+ : note.lockMethod === 'pin'
+ ? 'Locked with PIN'
+ : 'Tap to unlock with Face ID / PIN'}
+
+
+
+ );
+};
+
+const styles = StyleSheet.create({
+ card: {
+ backgroundColor: '#fff',
+ borderRadius: 14,
+ padding: 16,
+ marginVertical: 6,
+ marginHorizontal: 16,
+ borderLeftWidth: 3,
+ borderLeftColor: '#e74c3c',
+ shadowColor: '#000',
+ shadowOffset: { width: 0, height: 2 },
+ shadowOpacity: 0.08,
+ shadowRadius: 8,
+ elevation: 3,
+ },
+ header: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ marginBottom: 12,
+ },
+ lockBadge: {
+ width: 36,
+ height: 36,
+ borderRadius: 18,
+ backgroundColor: '#fde8e8',
+ justifyContent: 'center',
+ alignItems: 'center',
+ marginRight: 12,
+ },
+ headerText: {
+ flex: 1,
+ },
+ title: {
+ fontSize: 16,
+ fontWeight: '600',
+ color: '#1a1a1a',
+ },
+ lockedLabel: {
+ fontSize: 11,
+ color: '#e74c3c',
+ fontWeight: '600',
+ marginTop: 2,
+ textTransform: 'uppercase',
+ letterSpacing: 0.5,
+ },
+ blurredContent: {
+ gap: 6,
+ marginBottom: 12,
+ paddingLeft: 2,
+ },
+ blurLine: {
+ height: 10,
+ backgroundColor: '#e8e8e8',
+ borderRadius: 5,
+ width: '100%',
+ },
+ blurLineShort: {
+ height: 10,
+ backgroundColor: '#e8e8e8',
+ borderRadius: 5,
+ width: '65%',
+ },
+ footer: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ gap: 6,
+ paddingTop: 8,
+ borderTopWidth: StyleSheet.hairlineWidth,
+ borderTopColor: '#eee',
+ },
+ footerText: {
+ fontSize: 12,
+ color: '#999',
+ flex: 1,
+ },
+});
diff --git a/bounty-7/components/PINSetup.tsx b/bounty-7/components/PINSetup.tsx
new file mode 100644
index 00000000..18f3339e
--- /dev/null
+++ b/bounty-7/components/PINSetup.tsx
@@ -0,0 +1,286 @@
+import React, { useState, useCallback } from 'react';
+import {
+ View,
+ Text,
+ StyleSheet,
+ TouchableOpacity,
+ SafeAreaView,
+} from 'react-native';
+import { Ionicons } from '@expo/vector-icons';
+import { lockStore } from '../state/lockStore';
+
+type SetupStep = 'enter_old' | 'enter_new' | 'confirm_new';
+
+interface PINSetupProps {
+ onComplete: () => void;
+ onCancel?: () => void;
+ mode?: 'create' | 'change';
+}
+
+export const PINSetup: React.FC = ({
+ onComplete,
+ onCancel,
+ mode = 'create',
+}) => {
+ const [step, setStep] = useState(mode === 'change' ? 'enter_old' : 'enter_new');
+ const [oldPIN, setOldPIN] = useState('');
+ const [newPIN, setNewPIN] = useState('');
+ const [error, setError] = useState(null);
+
+ const handleOldPINSubmit = useCallback(
+ (pin: string) => {
+ if (lockStore.verifyPIN(pin)) {
+ setOldPIN(pin);
+ setStep('enter_new');
+ setError(null);
+ } else {
+ setError('Incorrect PIN. Try again.');
+ }
+ },
+ [],
+ );
+
+ const handleNewPINSubmit = useCallback(
+ (pin: string) => {
+ setNewPIN(pin);
+ setStep('confirm_new');
+ setError(null);
+ },
+ [],
+ );
+
+ const handleConfirmSubmit = useCallback(
+ (pin: string) => {
+ if (pin === newPIN) {
+ lockStore.setPIN(pin);
+ lockStore.setAuthMethod(
+ lockStore.hasBiometrics ? 'biometric_or_pin' : 'pin',
+ );
+ onComplete();
+ } else {
+ setError('PINs do not match. Try again.');
+ }
+ },
+ [newPIN, onComplete],
+ );
+
+ let prompt: string;
+ let onSubmit: (pin: string) => void;
+
+ switch (step) {
+ case 'enter_old':
+ prompt = 'Enter Current PIN';
+ onSubmit = handleOldPINSubmit;
+ break;
+ case 'enter_new':
+ prompt = mode === 'change' ? 'Enter New PIN' : 'Create a PIN';
+ onSubmit = handleNewPINSubmit;
+ break;
+ case 'confirm_new':
+ prompt = 'Confirm PIN';
+ onSubmit = handleConfirmSubmit;
+ break;
+ }
+
+ return (
+
+
+
+
+
+
+
+
+ {mode === 'change' ? 'Change PIN' : 'Set Up PIN'}
+
+
+ {error && (
+
+
+ {error}
+
+ )}
+
+
+
+
+ );
+};
+
+interface PINKeypadProps {
+ prompt: string;
+ onSubmit: (pin: string) => void;
+}
+
+export const PINKeypad: React.FC = ({ prompt, onSubmit }) => {
+ const [pin, setPin] = useState('');
+ const PIN_LENGTH = 4;
+
+ const handleDigit = useCallback(
+ (digit: string) => {
+ if (pin.length >= PIN_LENGTH) return;
+ const next = pin + digit;
+ setPin(next);
+ if (next.length === PIN_LENGTH) {
+ setTimeout(() => {
+ onSubmit(next);
+ setPin('');
+ }, 150);
+ }
+ },
+ [pin, onSubmit],
+ );
+
+ const handleDelete = useCallback(() => {
+ setPin((prev) => prev.slice(0, -1));
+ }, []);
+
+ const keys = [
+ ['1', '2', '3'],
+ ['4', '5', '6'],
+ ['7', '8', '9'],
+ ['', '0', 'DEL'],
+ ];
+
+ return (
+
+ {prompt}
+
+
+ {Array.from({ length: PIN_LENGTH }).map((_, i) => (
+ i && styles.dotFilled]} />
+ ))}
+
+
+
+ {keys.flat().map((key, index) => (
+ {
+ if (key === 'DEL') handleDelete();
+ else if (key) handleDigit(key);
+ }}
+ disabled={!key && key !== 'DEL' && !key}
+ activeOpacity={0.6}
+ >
+ {key === 'DEL' ? (
+
+ ) : (
+
+ {key}
+
+ )}
+
+ ))}
+
+
+ );
+};
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ backgroundColor: '#fff',
+ },
+ content: {
+ flex: 1,
+ alignItems: 'center',
+ justifyContent: 'center',
+ paddingHorizontal: 32,
+ },
+ topCancel: {
+ position: 'absolute',
+ top: 16,
+ right: 16,
+ padding: 8,
+ zIndex: 1,
+ },
+ icon: {
+ marginBottom: 16,
+ },
+ title: {
+ fontSize: 22,
+ fontWeight: '700',
+ color: '#1a1a1a',
+ marginBottom: 24,
+ },
+ errorContainer: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ backgroundColor: '#fde8e8',
+ paddingVertical: 8,
+ paddingHorizontal: 16,
+ borderRadius: 8,
+ marginBottom: 16,
+ gap: 8,
+ },
+ errorText: {
+ color: '#e74c3c',
+ fontSize: 14,
+ },
+ keypadContainer: {
+ alignItems: 'center',
+ width: '100%',
+ },
+ prompt: {
+ fontSize: 16,
+ color: '#666',
+ marginBottom: 16,
+ },
+ dotsRow: {
+ flexDirection: 'row',
+ gap: 16,
+ marginBottom: 32,
+ },
+ dot: {
+ width: 18,
+ height: 18,
+ borderRadius: 9,
+ borderWidth: 2,
+ borderColor: '#ccc',
+ backgroundColor: 'transparent',
+ },
+ dotFilled: {
+ backgroundColor: '#1a1a1a',
+ borderColor: '#1a1a1a',
+ },
+ keypad: {
+ flexDirection: 'row',
+ flexWrap: 'wrap',
+ justifyContent: 'center',
+ width: 264,
+ gap: 12,
+ },
+ key: {
+ width: 72,
+ height: 72,
+ borderRadius: 36,
+ justifyContent: 'center',
+ alignItems: 'center',
+ backgroundColor: '#f5f5f5',
+ },
+ keyEmpty: {
+ backgroundColor: 'transparent',
+ },
+ keyDelete: {
+ backgroundColor: '#f0f0f0',
+ },
+ keyText: {
+ fontSize: 26,
+ fontWeight: '500',
+ color: '#1a1a1a',
+ },
+ keyTextEmpty: {
+ color: 'transparent',
+ },
+});
diff --git a/bounty-7/hooks/useNoteLock.ts b/bounty-7/hooks/useNoteLock.ts
new file mode 100644
index 00000000..f195bd54
--- /dev/null
+++ b/bounty-7/hooks/useNoteLock.ts
@@ -0,0 +1,188 @@
+import { useCallback, useEffect, useRef } from 'react';
+import { AppState, AppStateStatus } from 'react-native';
+import * as LocalAuthentication from 'expo-local-authentication';
+import { lockStore } from '../state/lockStore';
+import { LockState, AuthMethod, AuthResult } from '../types';
+
+interface UseNoteLockOptions {
+ noteId: string;
+}
+
+interface UseNoteLockReturn {
+ isLocked: boolean;
+ lockState: LockState;
+ isGloballyLocked: boolean;
+ hasBiometrics: boolean;
+ hasPIN: boolean;
+ lock: () => void;
+ unlock: () => Promise;
+ toggle: () => Promise;
+ authenticate: (method?: AuthMethod) => Promise;
+ authenticateWithBiometrics: () => Promise;
+ authenticateWithPIN: (pin: string) => AuthResult;
+ checkBiometrics: () => Promise;
+ resetLockState: () => void;
+}
+
+export function useNoteLock({ noteId }: UseNoteLockOptions): UseNoteLockReturn {
+ const appStateRef = useRef(AppState.currentState);
+
+ useEffect(() => {
+ const subscription = AppState.addEventListener('change', (nextState) => {
+ if (
+ appStateRef.current.match(/inactive|background/) &&
+ nextState === 'active'
+ ) {
+ lockStore.onAppForeground();
+ }
+ if (nextState.match(/inactive|background/)) {
+ lockStore.onAppBackground();
+ }
+ appStateRef.current = nextState;
+ });
+
+ return () => subscription.remove();
+ }, []);
+
+ const checkBiometrics = useCallback(async () => {
+ const [compatible, enrolled, types] = await Promise.all([
+ LocalAuthentication.hasHardwareAsync(),
+ LocalAuthentication.isEnrolledAsync(),
+ LocalAuthentication.supportedAuthenticationTypesAsync(),
+ ]);
+
+ const isAvailable = compatible && enrolled;
+ let biometryType: 'TouchID' | 'FaceID' | null = null;
+
+ if (isAvailable) {
+ if (types.includes(LocalAuthentication.AuthenticationType.FACIAL_RECOGNITION)) {
+ biometryType = 'FaceID';
+ } else if (
+ types.includes(LocalAuthentication.AuthenticationType.FINGERPRINT) ||
+ types.includes(LocalAuthentication.AuthenticationType.IRIS)
+ ) {
+ biometryType = 'TouchID';
+ }
+ }
+
+ lockStore.setBiometricStatus({ isAvailable, biometryType });
+ }, []);
+
+ useEffect(() => {
+ checkBiometrics();
+ }, [checkBiometrics]);
+
+ const authenticateWithBiometrics = useCallback(async (): Promise => {
+ if (!lockStore.hasBiometrics) {
+ return { success: false, error: 'Biometrics not available', method: 'biometric' };
+ }
+
+ try {
+ const result = await LocalAuthentication.authenticateAsync({
+ promptMessage: 'Unlock Note',
+ fallbackLabel: 'Use PIN',
+ cancelLabel: 'Cancel',
+ disableDeviceFallback: false,
+ });
+
+ if (result.success) {
+ return { success: true, method: 'biometric' };
+ }
+
+ if (result.error === 'user_cancel') {
+ return { success: false, error: 'Cancelled', method: 'biometric' };
+ }
+
+ return { success: false, error: result.error ?? 'Authentication failed', method: 'biometric' };
+ } catch {
+ return { success: false, error: 'Biometric authentication error', method: 'biometric' };
+ }
+ }, []);
+
+ const authenticateWithPIN = useCallback((pin: string): AuthResult => {
+ return lockStore.authenticateWithPin(pin);
+ }, []);
+
+ const authenticate = useCallback(
+ async (method?: AuthMethod): Promise => {
+ const authMethod = method ?? lockStore.authMethod;
+
+ if (authMethod === 'biometric' || authMethod === 'biometric_or_pin') {
+ if (lockStore.hasBiometrics) {
+ const result = await authenticateWithBiometrics();
+ if (result.success || authMethod === 'biometric') {
+ return result;
+ }
+ if (authMethod === 'biometric_or_pin' && !result.success) {
+ return { success: false, method: 'pin', error: 'PIN required' };
+ }
+ } else if (authMethod === 'biometric') {
+ return { success: false, error: 'Biometrics not available', method: 'biometric' };
+ }
+ }
+
+ if (authMethod === 'pin' || authMethod === 'biometric_or_pin') {
+ if (!lockStore.hasPIN) {
+ return { success: false, error: 'No PIN configured', method: 'pin' };
+ }
+ return { success: false, method: 'pin', error: 'PIN required' };
+ }
+
+ return { success: false, error: 'No authentication method configured', method: 'none' };
+ },
+ [authenticateWithBiometrics],
+ );
+
+ const unlock = useCallback(async (): Promise => {
+ if (!lockStore.isNoteLocked(noteId)) return true;
+
+ lockStore.setNoteLockState(noteId, 'unlocking');
+
+ const result = await authenticate();
+ if (result.success) {
+ lockStore.unlockNote(noteId);
+ lockStore.unlockGlobally();
+ return true;
+ }
+
+ lockStore.setNoteLockState(noteId, 'locked');
+
+ if (result.error === 'PIN required') {
+ return false;
+ }
+
+ return false;
+ }, [noteId, authenticate]);
+
+ const lock = useCallback(() => {
+ lockStore.lockNote(noteId);
+ }, [noteId]);
+
+ const toggle = useCallback(async () => {
+ if (lockStore.isNoteLocked(noteId)) {
+ await unlock();
+ } else {
+ lock();
+ }
+ }, [noteId, unlock, lock]);
+
+ const resetLockState = useCallback(() => {
+ lockStore.setNoteLockState(noteId, 'locked');
+ }, [noteId]);
+
+ return {
+ isLocked: lockStore.isNoteLocked(noteId),
+ lockState: lockStore.getNoteLockState(noteId),
+ isGloballyLocked: lockStore.isGloballyLocked,
+ hasBiometrics: lockStore.hasBiometrics,
+ hasPIN: lockStore.hasPIN,
+ lock,
+ unlock,
+ toggle,
+ authenticate,
+ authenticateWithBiometrics,
+ authenticateWithPIN,
+ checkBiometrics,
+ resetLockState,
+ };
+}
diff --git a/bounty-7/state/lockStore.ts b/bounty-7/state/lockStore.ts
new file mode 100644
index 00000000..89a46214
--- /dev/null
+++ b/bounty-7/state/lockStore.ts
@@ -0,0 +1,231 @@
+import { makeAutoObservable } from 'mobx';
+import {
+ LockState,
+ AuthMethod,
+ BiometricStatus,
+ LockConfig,
+ AuthResult,
+ DEFAULT_LOCK_CONFIG,
+} from '../types';
+
+export class LockStore {
+ private _noteLockStates: Map = new Map();
+ private _lockedNoteIdSet: Set = new Set();
+ private _pinHash: string | null = null;
+
+ authMethod: AuthMethod = DEFAULT_LOCK_CONFIG.authMethod;
+ biometricStatus: BiometricStatus = { isAvailable: false, biometryType: null };
+
+ isGloballyLocked = true;
+ lastUnlockTimestamp = 0;
+
+ pinAttempts = 0;
+ isLockedOut = false;
+ lockoutUntil = 0;
+
+ config: LockConfig = { ...DEFAULT_LOCK_CONFIG };
+
+ constructor() {
+ makeAutoObservable(this);
+ }
+
+ get hasBiometrics(): boolean {
+ return this.biometricStatus.isAvailable;
+ }
+
+ get hasPIN(): boolean {
+ return this._pinHash !== null;
+ }
+
+ get noteLockStates(): Record {
+ const obj: Record = {};
+ this._noteLockStates.forEach((v, k) => { obj[k] = v; });
+ return obj;
+ }
+
+ get lockedNoteIds(): string[] {
+ return Array.from(this._lockedNoteIdSet);
+ }
+
+ getNoteLockState(noteId: string): LockState {
+ return this._noteLockStates.get(noteId) ?? 'locked';
+ }
+
+ setNoteLockState(noteId: string, state: LockState) {
+ this._noteLockStates.set(noteId, state);
+ if (state === 'locked' || state === 'locking') {
+ this._lockedNoteIdSet.add(noteId);
+ } else if (state === 'unlocked') {
+ this._lockedNoteIdSet.delete(noteId);
+ }
+ }
+
+ isNoteLocked(noteId: string): boolean {
+ if (!this._noteLockStates.has(noteId)) return true;
+ const state = this.getNoteLockState(noteId);
+ return state === 'locked' || state === 'locking';
+ }
+
+ lockNote(noteId: string) {
+ this.setNoteLockState(noteId, 'locked');
+ }
+
+ unlockNote(noteId: string) {
+ this.setNoteLockState(noteId, 'unlocked');
+ this.lastUnlockTimestamp = Date.now();
+ }
+
+ toggleNoteLock(noteId: string) {
+ if (this.isNoteLocked(noteId)) {
+ this.unlockNote(noteId);
+ } else {
+ this.lockNote(noteId);
+ }
+ }
+
+ lockAll() {
+ this._noteLockStates.forEach((_, noteId) => {
+ this.setNoteLockState(noteId, 'locked');
+ });
+ this.isGloballyLocked = true;
+ }
+
+ unlockGlobally() {
+ this.isGloballyLocked = false;
+ this.lastUnlockTimestamp = Date.now();
+ }
+
+ checkAutoLock(): boolean {
+ if (!this.isGloballyLocked) {
+ const elapsed = Date.now() - this.lastUnlockTimestamp;
+ if (elapsed >= this.config.timeoutMs) {
+ this.lockAll();
+ return true;
+ }
+ }
+ return this.isGloballyLocked;
+ }
+
+ authenticateWithPin(pin: string): AuthResult {
+ if (this.isLockedOut) {
+ if (Date.now() < this.lockoutUntil) {
+ const remaining = Math.ceil((this.lockoutUntil - Date.now()) / 1000);
+ return {
+ success: false,
+ error: `Too many attempts. Try again in ${remaining}s`,
+ method: 'pin',
+ attemptsRemaining: 0,
+ };
+ }
+ this.isLockedOut = false;
+ this.pinAttempts = 0;
+ }
+
+ if (!this._pinHash) {
+ return { success: false, error: 'No PIN configured', method: 'pin' };
+ }
+
+ if (this._pinHash !== hashPIN(pin)) {
+ this.pinAttempts++;
+ if (this.pinAttempts >= this.config.maxPinAttempts) {
+ this.isLockedOut = true;
+ this.lockoutUntil = Date.now() + this.config.lockoutDurationMs;
+ return {
+ success: false,
+ error: `Locked out for ${this.config.lockoutDurationMs / 1000}s`,
+ method: 'pin',
+ attemptsRemaining: 0,
+ };
+ }
+ return {
+ success: false,
+ error: 'Incorrect PIN',
+ method: 'pin',
+ attemptsRemaining: this.config.maxPinAttempts - this.pinAttempts,
+ };
+ }
+
+ this.pinAttempts = 0;
+ return { success: true, method: 'pin' };
+ }
+
+ setPIN(pin: string) {
+ this._pinHash = hashPIN(pin);
+ this.pinAttempts = 0;
+ this.isLockedOut = false;
+ }
+
+ verifyPIN(pin: string): boolean {
+ return this._pinHash === hashPIN(pin);
+ }
+
+ removePIN() {
+ this._pinHash = null;
+ if (this.authMethod === 'pin') {
+ this.authMethod = 'none';
+ }
+ }
+
+ changePIN(oldPIN: string, newPIN: string): boolean {
+ if (!this.verifyPIN(oldPIN)) return false;
+ this.setPIN(newPIN);
+ return true;
+ }
+
+ setBiometricStatus(status: BiometricStatus) {
+ this.biometricStatus = status;
+ }
+
+ setAuthMethod(method: AuthMethod) {
+ this.authMethod = method;
+ }
+
+ updateConfig(partial: Partial) {
+ Object.assign(this.config, partial);
+ }
+
+ onAppForeground() {
+ this.checkAutoLock();
+ }
+
+ onAppBackground() {
+ if (this.config.requireAuthOnRestart) {
+ // On next foreground checkAutoLock will enforce timeout
+ }
+ }
+
+ toJSON() {
+ return {
+ authMethod: this.authMethod,
+ _pinHash: this._pinHash,
+ lockedNoteIds: Array.from(this._lockedNoteIdSet),
+ isGloballyLocked: this.isGloballyLocked,
+ config: this.config,
+ };
+ }
+
+ fromJSON(data: ReturnType) {
+ this.authMethod = data.authMethod;
+ this._pinHash = data._pinHash;
+ this._lockedNoteIdSet = new Set(data.lockedNoteIds);
+ this.isGloballyLocked = data.isGloballyLocked;
+ this.config = data.config;
+ for (const noteId of data.lockedNoteIds) {
+ this._noteLockStates.set(noteId, 'locked');
+ }
+ }
+}
+
+function hashPIN(pin: string): string {
+ let hash = 0;
+ for (let i = 0; i < pin.length; i++) {
+ const char = pin.charCodeAt(i);
+ hash = ((hash << 5) - hash) + char;
+ hash |= 0;
+ }
+ return `pin_${Math.abs(hash).toString(36)}`;
+}
+
+const lockStore = new LockStore();
+export { lockStore };
+export default lockStore;
diff --git a/bounty-7/types.ts b/bounty-7/types.ts
new file mode 100644
index 00000000..0b0e1cf1
--- /dev/null
+++ b/bounty-7/types.ts
@@ -0,0 +1,52 @@
+export type LockState = 'locked' | 'unlocked' | 'locking' | 'unlocking';
+
+export type AuthMethod = 'biometric' | 'pin' | 'biometric_or_pin' | 'none';
+
+export interface SecureNote {
+ id: string;
+ title: string;
+ content: string;
+ isLocked: boolean;
+ lockedAt?: number;
+ lockMethod?: AuthMethod;
+}
+
+export interface BiometricStatus {
+ isAvailable: boolean;
+ biometryType?: BiometricType;
+ error?: string;
+}
+
+export type BiometricType = 'TouchID' | 'FaceID' | 'Fingerprint' | 'Iris' | null;
+
+export interface LockConfig {
+ authMethod: AuthMethod;
+ timeoutMs: number;
+ requireAuthOnRestart: boolean;
+ maxPinAttempts: number;
+ lockoutDurationMs: number;
+ allowBiometricFallback: boolean;
+}
+
+export interface AuthResult {
+ success: boolean;
+ error?: string;
+ method: AuthMethod;
+ attemptsRemaining?: number;
+}
+
+export interface LockEvent {
+ noteId: string;
+ type: 'locked' | 'unlocked' | 'auth_failed' | 'auth_success';
+ timestamp: number;
+ method?: AuthMethod;
+}
+
+export const DEFAULT_LOCK_CONFIG: LockConfig = {
+ authMethod: 'biometric_or_pin',
+ timeoutMs: 5 * 60 * 1000,
+ requireAuthOnRestart: true,
+ maxPinAttempts: 5,
+ lockoutDurationMs: 60 * 1000,
+ allowBiometricFallback: true,
+};