import React, { useState, useCallback, useEffect } from 'react'; import { Pressable, StyleSheet, Text, View } from 'react-native'; import { SafeAreaView } from 'react-native-safe-area-context'; import { Ionicons } from '@expo/vector-icons'; import * as SecureStore from 'expo-secure-store'; import { useTheme } from '../theme'; import { useT } from '../i18n'; import { authenticate, RealAuthProvider, hashPin, verifyPin } from '../services/security'; /** SecureStore 中存储 PIN 哈希的 key。 */ const PIN_HASH_KEY = 'app_pin_hash'; interface LockScreenProps { onUnlock: () => void; } export function LockScreen({ onUnlock }: LockScreenProps) { const { theme } = useTheme(); const t = useT(); const [error, setError] = useState(null); const [retrying, setRetrying] = useState(false); const [showPinInput, setShowPinInput] = useState(false); const [pin, setPin] = useState(''); const [hasPin, setHasPin] = useState(true); // 是否已设置过 PIN const handleAuth = useCallback(async () => { setRetrying(true); setError(null); const provider = new RealAuthProvider(); const result = await authenticate(provider); if (result.success) { onUnlock(); } else { setError(t('lockScreen.authFail')); setShowPinInput(true); } setRetrying(false); }, [onUnlock, t]); useEffect(() => { // 检查是否已设置 PIN SecureStore.getItemAsync(PIN_HASH_KEY).then(hash => { setHasPin(hash !== null); }); // 尝试生物识别 handleAuth(); }, []); const handleKeyPress = async (num: string) => { setError(null); if (pin.length < 4) { const nextPin = pin + num; setPin(nextPin); if (nextPin.length === 4) { // 验证 PIN const storedHash = await SecureStore.getItemAsync(PIN_HASH_KEY); if (storedHash) { const valid = await verifyPin(nextPin, storedHash); if (valid) { onUnlock(); } else { setError(t('lockScreen.pinFail')); setTimeout(() => setPin(''), 500); } } else { // 未设置 PIN,直接放行(不应发生——首次设置在 preferences 页) onUnlock(); } } } }; const handleBackspace = () => { setPin(prev => prev.slice(0, -1)); }; const handleClear = () => { setPin(''); }; const renderKey = (val: string) => ( handleKeyPress(val)} accessibilityRole="button" accessibilityLabel={val} style={({ pressed }) => [ styles.keyBtn, { backgroundColor: theme.colors.bgTertiary, borderColor: theme.colors.border, opacity: pressed ? 0.6 : 1, }, ]} > {val} ); return ( {!showPinInput ? ( {t('lockScreen.locked')} {t('lockScreen.bioPrompt')} {error && ( {error} )} [ styles.unlockBtn, { backgroundColor: theme.colors.accent, opacity: pressed || retrying ? 0.6 : 1 }, ]} > {retrying ? t('lockScreen.authenticating') : t('lockScreen.clickUnlock')} {hasPin && ( setShowPinInput(true)} accessibilityRole="button" accessibilityLabel={t('lockScreen.usePin')} style={({ pressed }) => [ styles.unlockBtn, { backgroundColor: theme.colors.bgTertiary, borderWidth: 1, borderColor: theme.colors.border, opacity: pressed ? 0.6 : 1 }, ]} > {t('lockScreen.usePin')} )} ) : ( {t('lockScreen.enterPin')} {/* Dots */} {[0, 1, 2, 3].map(i => ( i ? theme.colors.accent : theme.colors.border, }, ]} /> ))} {error && ( {error} )} {/* Keypad Grid */} {renderKey('1')} {renderKey('2')} {renderKey('3')} {renderKey('4')} {renderKey('5')} {renderKey('6')} {renderKey('7')} {renderKey('8')} {renderKey('9')} {t('lockScreen.clear')} {renderKey('0')} { setShowPinInput(false); setError(null); setPin(''); }} style={styles.switchBackBtn} accessibilityRole="button" accessibilityLabel={t('lockScreen.useBio')} > {t('lockScreen.useBio')} )} ); } /** 导出 PIN 设置/验证辅助函数供 settings 页面使用。 */ export async function setPin(pin: string): Promise { const hash = await hashPin(pin); await SecureStore.setItemAsync(PIN_HASH_KEY, hash); } export async function clearPin(): Promise { await SecureStore.deleteItemAsync(PIN_HASH_KEY); } export async function hasPinSet(): Promise { const hash = await SecureStore.getItemAsync(PIN_HASH_KEY); return hash !== null; } const styles = StyleSheet.create({ page: { flex: 1 }, content: { flex: 1, alignItems: 'center', justifyContent: 'center', padding: 24 }, centerSection: { alignItems: 'center', justifyContent: 'center' }, iconWrap: { width: 96, height: 96, borderRadius: 48, alignItems: 'center', justifyContent: 'center' }, actionRow: { flexDirection: 'row', gap: 12, marginTop: 32 }, unlockBtn: { paddingVertical: 14, paddingHorizontal: 28, borderRadius: 12, minWidth: 120, alignItems: 'center' }, pinSection: { alignItems: 'center', width: '100%' }, dotsRow: { flexDirection: 'row', gap: 20, marginBottom: 24 }, dot: { width: 16, height: 16, borderRadius: 8 }, grid: { width: '80%', gap: 12 }, gridRow: { flexDirection: 'row', justifyContent: 'space-between', gap: 12 }, keyBtn: { flex: 1, height: 60, borderRadius: 30, borderWidth: 1, alignItems: 'center', justifyContent: 'center' }, keyText: { fontSize: 24, fontWeight: '700' }, utilityBtn: { flex: 1, height: 60, alignItems: 'center', justifyContent: 'center' }, switchBackBtn: { marginTop: 32, padding: 8 }, });