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>
This commit is contained in:
@@ -0,0 +1,54 @@
|
||||
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" },
|
||||
});
|
||||
Reference in New Issue
Block a user