Files
chlova/mobile/app/login.tsx
T
Kantin-Petit faa1e82301 feat: app mobile React Native / Expo (v0.31.0)
Package mobile/ (Expo SDK 56, expo-router) réutilisant l'API backend :
Login (mdp+TOTP), Chat (+ TTS expo-speech), Review (approuver/refuser).
JWT en expo-secure-store, thème dark HUD, EXPO_PUBLIC_API_BASE. typecheck
vert. STT mobile reporté (lib native), TTS OK. Versions gérées par Expo.

Palier de risque : reversible (client mobile, API commune).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 07:39:38 +02:00

55 lines
2.4 KiB
TypeScript

import { useState } from "react";
import { View, Text, TextInput, Pressable, StyleSheet } from "react-native";
import { useRouter } from "expo-router";
import { useAuth } from "@/auth";
import { ApiError } from "@/api";
import { C } from "@/theme";
export default function Login() {
const { login } = useAuth();
const router = useRouter();
const [user, setUser] = useState("");
const [password, setPassword] = useState("");
const [totp, setTotp] = useState("");
const [busy, setBusy] = useState(false);
const [error, setError] = useState<string | null>(null);
const submit = async (): Promise<void> => {
setBusy(true);
setError(null);
try {
await login(user, password, totp);
router.replace("/(tabs)/chat");
} catch (err) {
setError(err instanceof ApiError && err.status === 429 ? "Trop de tentatives." : "Identifiants invalides.");
} finally {
setBusy(false);
}
};
return (
<View style={s.wrap}>
<Text style={s.title}>CHLOVA</Text>
<Text style={s.sub}>Accès propriétaire authentification forte.</Text>
<TextInput style={s.input} placeholder="Utilisateur" placeholderTextColor={C.muted} autoCapitalize="none" value={user} onChangeText={setUser} />
<TextInput style={s.input} placeholder="Mot de passe" placeholderTextColor={C.muted} secureTextEntry value={password} onChangeText={setPassword} />
<TextInput style={s.input} placeholder="Code 2FA (TOTP)" placeholderTextColor={C.muted} keyboardType="number-pad" value={totp} onChangeText={setTotp} />
{error && <Text style={s.error}>{error}</Text>}
<Pressable style={[s.btn, busy && s.btnDisabled]} onPress={submit} disabled={busy}>
<Text style={s.btnText}>{busy ? "Connexion…" : "Se connecter"}</Text>
</Pressable>
</View>
);
}
const s = StyleSheet.create({
wrap: { flex: 1, justifyContent: "center", padding: 24, gap: 12, backgroundColor: C.bg },
title: { color: C.accent, fontSize: 32, fontWeight: "700", letterSpacing: 2 },
sub: { color: C.muted, marginBottom: 8 },
input: { backgroundColor: C.surface2, borderColor: C.border, borderWidth: 1, borderRadius: 8, color: C.fg, padding: 12 },
error: { color: C.danger },
btn: { backgroundColor: C.accent, borderRadius: 8, padding: 14, alignItems: "center", marginTop: 4 },
btnDisabled: { opacity: 0.5 },
btnText: { color: C.bg, fontWeight: "600" },
});