diff --git a/apps/mobile/app/(app)/unlock.tsx b/apps/mobile/app/(app)/unlock.tsx new file mode 100644 index 0000000..932a58b --- /dev/null +++ b/apps/mobile/app/(app)/unlock.tsx @@ -0,0 +1,194 @@ +import { useRouter } from 'expo-router'; +import { useState } from 'react'; +import { + ActivityIndicator, + Pressable, + StyleSheet, + Text, + View, +} from 'react-native'; + +import { PinInput } from '../../components/PinInput'; +import { useAuth } from '../../lib/authContext'; +import { loadOrUnlockUserKey, resetIdentity } from '../../lib/userIdentity'; +import { colors } from '../../theme/colors'; + +type Mode = 'pin' | 'recovery'; + +export default function UnlockScreen() { + const router = useRouter(); + const { userId, userKeyState, refreshUserKeyState } = useAuth(); + const [mode, setMode] = useState('pin'); + const [pin, setPin] = useState(''); + const [recovery, setRecovery] = useState(''); + const [submitting, setSubmitting] = useState(false); + const [error, setError] = useState(null); + + const locked = + userKeyState.status === 'needs-unlock' && userKeyState.lockedUntil !== null; + const hasRecovery = + userKeyState.status === 'needs-unlock' ? userKeyState.hasRecovery : false; + + async function attempt(value: string, isRecoveryCode: boolean) { + if (!userId) return; + setSubmitting(true); + setError(null); + try { + const out = await loadOrUnlockUserKey({ userId, pin: value, isRecoveryCode }); + if (out.kind === 'unlocked') { + await refreshUserKeyState(); + router.replace('/(app)/chats'); + return; + } + if (out.kind === 'locked') { + setError('Konto bis ' + new Date(out.lockedUntil).toLocaleString('de-DE') + ' gesperrt.'); + await refreshUserKeyState(); + return; + } + setError('Kein Identitäts-Datensatz auf dem Server.'); + } catch (err) { + setError(err instanceof Error ? err.message : 'Entsperren fehlgeschlagen'); + if (isRecoveryCode) setRecovery(''); + else setPin(''); + await refreshUserKeyState(); + } finally { + setSubmitting(false); + } + } + + async function handleReset() { + if (!userId) return; + setSubmitting(true); + setError(null); + try { + await resetIdentity({ userId, pin: '000000' }); + await refreshUserKeyState(); + router.replace('/(app)/setup'); + } catch (err) { + setError(err instanceof Error ? err.message : 'Reset fehlgeschlagen'); + } finally { + setSubmitting(false); + } + } + + return ( + + Netralax entsperren + + setMode('pin')} + > + PIN + + setMode('recovery')} + > + + Recovery-Code + + + + + {mode === 'pin' ? ( + <> + + attempt(pin, false)} + > + {submitting ? ( + + ) : ( + Entsperren + )} + + + ) : ( + <> + Recovery-Code eingeben (mit oder ohne Bindestriche): + + {recovery} + + attempt(recovery, true)} + > + {submitting ? ( + + ) : ( + Mit Recovery entsperren + )} + + + )} + + {error && {error}} + + {locked && ( + + Identität zurücksetzen (alte Chats verloren) + + )} + + ); +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + backgroundColor: colors.bg, + paddingTop: 64, + paddingHorizontal: 24, + gap: 16, + }, + title: { color: colors.text, fontSize: 22, fontWeight: '700' }, + subtitle: { color: colors.textMuted, fontSize: 13 }, + tabs: { flexDirection: 'row', gap: 8 }, + tab: { + flex: 1, + paddingVertical: 10, + alignItems: 'center', + borderBottomWidth: 2, + borderBottomColor: colors.border, + }, + tabActive: { borderBottomColor: colors.accent }, + tabText: { color: colors.textMuted, fontWeight: '600' }, + tabTextActive: { color: colors.text }, + recoveryDisplay: { + color: colors.text, + fontFamily: 'Courier', + fontSize: 18, + letterSpacing: 1.5, + backgroundColor: colors.surface, + padding: 16, + borderRadius: 10, + }, + primary: { + backgroundColor: colors.accent, + paddingVertical: 14, + borderRadius: 12, + alignItems: 'center', + }, + primaryText: { color: colors.text, fontWeight: '700' }, + disabled: { opacity: 0.5 }, + danger: { + paddingVertical: 12, + alignItems: 'center', + borderColor: colors.danger, + borderWidth: 1, + borderRadius: 12, + }, + dangerText: { color: colors.danger, fontWeight: '700' }, + error: { color: colors.danger, fontSize: 13 }, +});