196 lines
6.0 KiB
TypeScript
196 lines
6.0 KiB
TypeScript
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<Step>('pin');
|
|
const [pin, setPin] = useState('');
|
|
const [confirm, setConfirm] = useState('');
|
|
const [recoveryCode, setRecoveryCode] = useState<string | null>(null);
|
|
const [withRecovery, setWithRecovery] = useState(true);
|
|
const [submitting, setSubmitting] = useState(false);
|
|
const [error, setError] = useState<string | null>(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 (
|
|
<View style={styles.container}>
|
|
<Text style={styles.title}>PIN festlegen</Text>
|
|
<Text style={styles.subtitle}>
|
|
Mit dieser 6-stelligen PIN entsperrst du Netralax auf jedem Gerät.
|
|
</Text>
|
|
<PinInput value={pin} onChange={setPin} ariaLabel="Neue PIN" autoFocus />
|
|
{error && <Text style={styles.error}>{error}</Text>}
|
|
<Pressable
|
|
style={[styles.primary, pin.length !== 6 && styles.disabled]}
|
|
disabled={pin.length !== 6}
|
|
onPress={() => setStep('confirm')}
|
|
>
|
|
<Text style={styles.primaryText}>Weiter</Text>
|
|
</Pressable>
|
|
</View>
|
|
);
|
|
}
|
|
|
|
if (step === 'confirm') {
|
|
return (
|
|
<View style={styles.container}>
|
|
<Text style={styles.title}>PIN bestätigen</Text>
|
|
<Text style={styles.subtitle}>Bitte gib dieselbe PIN erneut ein.</Text>
|
|
<PinInput value={confirm} onChange={setConfirm} ariaLabel="PIN bestätigen" autoFocus />
|
|
{error && <Text style={styles.error}>{error}</Text>}
|
|
<Pressable
|
|
style={[styles.primary, confirm.length !== 6 && styles.disabled]}
|
|
disabled={confirm.length !== 6 || submitting}
|
|
onPress={() => {
|
|
if (pin !== confirm) {
|
|
setError('PIN stimmt nicht überein.');
|
|
setConfirm('');
|
|
return;
|
|
}
|
|
void runSetup(withRecovery);
|
|
}}
|
|
>
|
|
{submitting ? (
|
|
<ActivityIndicator color={colors.text} />
|
|
) : (
|
|
<Text style={styles.primaryText}>PIN speichern</Text>
|
|
)}
|
|
</Pressable>
|
|
<Pressable onPress={() => setWithRecovery((v) => !v)} style={styles.secondary}>
|
|
<Text style={styles.secondaryText}>
|
|
{withRecovery
|
|
? 'Recovery-Code überspringen (riskant)'
|
|
: 'Recovery-Code erzeugen (empfohlen)'}
|
|
</Text>
|
|
</Pressable>
|
|
</View>
|
|
);
|
|
}
|
|
|
|
if (step === 'recovery' && recoveryCode) {
|
|
return (
|
|
<ScrollView contentContainerStyle={styles.container}>
|
|
<Text style={styles.title}>Recovery-Code</Text>
|
|
<Text style={styles.subtitle}>
|
|
Notiere dir diesen Code an einem sicheren Ort. Du brauchst ihn, wenn du deine PIN
|
|
vergisst. Wir zeigen ihn dir nur EINMAL.
|
|
</Text>
|
|
<View style={styles.codeBox}>
|
|
<Text style={styles.codeText} selectable>
|
|
{recoveryCode}
|
|
</Text>
|
|
</View>
|
|
<Pressable
|
|
style={styles.primary}
|
|
onPress={async () => {
|
|
await refreshUserKeyState();
|
|
router.replace('/(app)/chats');
|
|
}}
|
|
>
|
|
<Text style={styles.primaryText}>Habe ich gespeichert</Text>
|
|
</Pressable>
|
|
<Pressable
|
|
style={styles.secondary}
|
|
onPress={() => {
|
|
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');
|
|
},
|
|
},
|
|
],
|
|
);
|
|
}}
|
|
>
|
|
<Text style={styles.secondaryText}>Überspringen</Text>
|
|
</Pressable>
|
|
</ScrollView>
|
|
);
|
|
}
|
|
|
|
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',
|
|
},
|
|
});
|