import React, { useEffect, useRef, useState } from 'react'; import { ActivityIndicator, Animated, Pressable, StyleSheet, Text, View, Vibration, } from 'react-native'; import * as Haptics from 'expo-haptics'; import { authenticateBiometric, biometricAvailable, getSettings, verifyPin, } from '../lib/applock'; import { theme } from '../theme'; interface Props { onUnlock: () => void; onLogout?: () => void; } const KEYS: (string | null)[] = ['1', '2', '3', '4', '5', '6', '7', '8', '9', null, '0', '⌫']; export function AppLockScreen({ onUnlock, onLogout }: Props) { const [pin, setPin] = useState(''); const [bioAvailable, setBioAvailable] = useState(false); const [bioEnabled, setBioEnabled] = useState(false); const [verifying, setVerifying] = useState(false); const [error, setError] = useState(false); const shake = useRef(new Animated.Value(0)).current; useEffect(() => { (async () => { const [settings, available] = await Promise.all([getSettings(), biometricAvailable()]); setBioEnabled(settings.bioEnabled); setBioAvailable(available); if (settings.bioEnabled && available) { try { const ok = await authenticateBiometric(); if (ok) onUnlock(); } catch { // fall through to pin entry } } })(); }, [onUnlock]); async function tryUnlock(candidate: string) { setVerifying(true); const ok = await verifyPin(candidate); setVerifying(false); if (ok) { Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success).catch(() => {}); onUnlock(); } else { Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error).catch(() => {}); setError(true); setPin(''); Vibration.vibrate(50); Animated.sequence([ Animated.timing(shake, { toValue: 10, duration: 50, useNativeDriver: true }), Animated.timing(shake, { toValue: -10, duration: 50, useNativeDriver: true }), Animated.timing(shake, { toValue: 10, duration: 50, useNativeDriver: true }), Animated.timing(shake, { toValue: 0, duration: 50, useNativeDriver: true }), ]).start(); } } function press(key: string) { if (verifying) return; setError(false); if (key === '⌫') { setPin((p) => p.slice(0, -1)); Haptics.selectionAsync().catch(() => {}); return; } if (pin.length >= 8) return; const next = pin + key; setPin(next); Haptics.selectionAsync().catch(() => {}); if (next.length >= 4) { // Auto-verify when at least 4 digits, nicer UX. User can keep typing if PIN longer. tryUnlock(next); } } async function biometricRetry() { if (!bioEnabled || !bioAvailable) return; const ok = await authenticateBiometric(); if (ok) onUnlock(); } return ( g Goon locked {bioEnabled && bioAvailable ? 'Use fingerprint or PIN' : 'Enter PIN'} {[0, 1, 2, 3].map((i) => { const filled = i < pin.length; return ( ); })} {verifying ? : } {KEYS.map((k, i) => { if (k === null) { return bioEnabled && bioAvailable ? ( 👆 ) : ( ); } return ( [styles.key, pressed && styles.keyPressed]} onPress={() => press(k)} > {k} ); })} {onLogout ? ( Log out ) : null} ); } const KEY_SIZE = 76; const styles = StyleSheet.create({ root: { flex: 1, backgroundColor: theme.bg, paddingTop: 80, alignItems: 'center', paddingHorizontal: 24, }, header: { alignItems: 'center', marginBottom: 36 }, lockBadge: { width: 72, height: 72, borderRadius: 18, backgroundColor: theme.card, borderWidth: 1, borderColor: theme.borderFocus, alignItems: 'center', justifyContent: 'center', marginBottom: 16, }, lockGlyph: { color: theme.accentGlow, fontSize: 38, fontWeight: '900', lineHeight: 42 }, title: { color: theme.fg, fontSize: 22, fontWeight: '700' }, subtitle: { color: theme.muted, fontSize: 13, marginTop: 4 }, dots: { flexDirection: 'row', gap: 16, marginTop: 8 }, dot: { width: 14, height: 14, borderRadius: 7, borderWidth: 1.5, borderColor: theme.border, }, dotFilled: { backgroundColor: theme.accent, borderColor: theme.accentText }, dotError: { borderColor: theme.bad, backgroundColor: theme.bad }, pad: { marginTop: 20, flexDirection: 'row', flexWrap: 'wrap', width: KEY_SIZE * 3 + 32, justifyContent: 'space-between', rowGap: 14, }, key: { width: KEY_SIZE, height: KEY_SIZE, borderRadius: KEY_SIZE / 2, backgroundColor: theme.card, borderWidth: 1, borderColor: theme.border, alignItems: 'center', justifyContent: 'center', }, keyEmpty: { backgroundColor: 'transparent', borderColor: 'transparent' }, keyPressed: { backgroundColor: theme.bgElevated, borderColor: theme.borderFocus }, keyText: { color: theme.fg, fontSize: 28, fontWeight: '500' }, keyBack: { color: theme.muted, fontSize: 22 }, keyBio: { fontSize: 28 }, logout: { marginTop: 28, padding: 12 }, logoutText: { color: theme.muted, fontSize: 13 }, });