From 41359816f1cb69f7f2c854d1736b5c51e985030a Mon Sep 17 00:00:00 2001 From: byGalax Date: Sat, 16 May 2026 17:08:47 +0200 Subject: [PATCH] feat(mobile): UserKey setup screen (PIN + optional recovery code) --- apps/mobile/app/(app)/setup.tsx | 195 ++++++++++++++++++++++++++++++++ 1 file changed, 195 insertions(+) create mode 100644 apps/mobile/app/(app)/setup.tsx diff --git a/apps/mobile/app/(app)/setup.tsx b/apps/mobile/app/(app)/setup.tsx new file mode 100644 index 0000000..3713a19 --- /dev/null +++ b/apps/mobile/app/(app)/setup.tsx @@ -0,0 +1,195 @@ +import { useRouter } from 'expo-router'; +import { useState } from 'react'; +import { + ActivityIndicator, + Alert, + Pressable, + ScrollView, + StyleSheet, + Text, + View, +} from 'react-native'; + +import { PinInput } from '../../components/PinInput'; +import { useAuth } from '../../lib/authContext'; +import { setupNewUserIdentity } from '../../lib/userIdentity'; +import { colors } from '../../theme/colors'; + +type Step = 'pin' | 'confirm' | 'recovery' | 'done'; + +export default function SetupScreen() { + const router = useRouter(); + const { userId, refreshUserKeyState } = useAuth(); + const [step, setStep] = useState('pin'); + const [pin, setPin] = useState(''); + const [confirm, setConfirm] = useState(''); + const [recoveryCode, setRecoveryCode] = useState(null); + const [withRecovery, setWithRecovery] = useState(true); + const [submitting, setSubmitting] = useState(false); + const [error, setError] = useState(null); + + async function runSetup(saveRecovery: boolean) { + if (!userId) return; + setSubmitting(true); + setError(null); + try { + const out = await setupNewUserIdentity({ userId, pin, withRecovery: saveRecovery }); + setRecoveryCode(out.recoveryCode); + if (out.recoveryCode) { + setStep('recovery'); + } else { + setStep('done'); + await refreshUserKeyState(); + router.replace('/(app)/chats'); + } + } catch (err) { + setError(err instanceof Error ? err.message : 'Setup fehlgeschlagen'); + } finally { + setSubmitting(false); + } + } + + if (step === 'pin') { + return ( + + PIN festlegen + + Mit dieser 6-stelligen PIN entsperrst du Netralax auf jedem Gerät. + + + {error && {error}} + setStep('confirm')} + > + Weiter + + + ); + } + + if (step === 'confirm') { + return ( + + PIN bestätigen + Bitte gib dieselbe PIN erneut ein. + + {error && {error}} + { + if (pin !== confirm) { + setError('PIN stimmt nicht überein.'); + setConfirm(''); + return; + } + void runSetup(withRecovery); + }} + > + {submitting ? ( + + ) : ( + PIN speichern + )} + + setWithRecovery((v) => !v)} style={styles.secondary}> + + {withRecovery + ? 'Recovery-Code überspringen (riskant)' + : 'Recovery-Code erzeugen (empfohlen)'} + + + + ); + } + + if (step === 'recovery' && recoveryCode) { + return ( + + Recovery-Code + + Notiere dir diesen Code an einem sicheren Ort. Du brauchst ihn, wenn du deine PIN + vergisst. Wir zeigen ihn dir nur EINMAL. + + + + {recoveryCode} + + + { + await refreshUserKeyState(); + router.replace('/(app)/chats'); + }} + > + Habe ich gespeichert + + { + Alert.alert( + 'Sicher?', + 'Ohne Recovery-Code verlierst du den Zugriff, wenn du die PIN vergisst.', + [ + { text: 'Abbrechen', style: 'cancel' }, + { + text: 'Weiter ohne', + style: 'destructive', + onPress: async () => { + await refreshUserKeyState(); + router.replace('/(app)/chats'); + }, + }, + ], + ); + }} + > + Überspringen + + + ); + } + + return null; +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + padding: 24, + paddingTop: 64, + backgroundColor: colors.bg, + gap: 16, + }, + title: { color: colors.text, fontSize: 24, fontWeight: '700' }, + subtitle: { color: colors.textMuted, fontSize: 14, lineHeight: 20 }, + error: { color: colors.danger, fontSize: 13 }, + primary: { + backgroundColor: colors.accent, + paddingVertical: 14, + borderRadius: 12, + alignItems: 'center', + marginTop: 12, + }, + primaryText: { color: colors.text, fontWeight: '700' }, + disabled: { opacity: 0.5 }, + secondary: { paddingVertical: 12, alignItems: 'center' }, + secondaryText: { color: colors.accent, fontWeight: '600' }, + codeBox: { + backgroundColor: colors.surface, + borderColor: colors.border, + borderWidth: 1, + padding: 16, + borderRadius: 12, + }, + codeText: { + color: colors.text, + fontSize: 20, + fontFamily: 'Courier', + letterSpacing: 1.5, + textAlign: 'center', + }, +});