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,56 @@
|
||||
// Client de l'API CHLOVA (mobile). Le backend est distant : l'URL de base est
|
||||
// fournie par EXPO_PUBLIC_API_BASE (ex. https://chlova.example.com).
|
||||
|
||||
const BASE = process.env.EXPO_PUBLIC_API_BASE ?? "";
|
||||
|
||||
export interface Asset {
|
||||
id: string;
|
||||
type: string;
|
||||
version: string;
|
||||
riskTier: string;
|
||||
status: string;
|
||||
createdAt: number;
|
||||
expiresAt: number | null;
|
||||
execCount: number;
|
||||
commitLink: string | null;
|
||||
docLink: string | null;
|
||||
}
|
||||
|
||||
export class ApiError extends Error {
|
||||
constructor(
|
||||
public status: number,
|
||||
message: string,
|
||||
) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
|
||||
async function req<T>(path: string, token: string | null, init?: RequestInit): Promise<T> {
|
||||
const headers: Record<string, string> = { "content-type": "application/json" };
|
||||
if (token) headers.authorization = `Bearer ${token}`;
|
||||
const res = await fetch(`${BASE}/api${path}`, {
|
||||
...init,
|
||||
headers: { ...headers, ...(init?.headers as Record<string, string>) },
|
||||
});
|
||||
if (!res.ok) {
|
||||
const body = (await res.json().catch(() => ({}))) as { error?: string };
|
||||
throw new ApiError(res.status, body.error ?? `HTTP ${res.status}`);
|
||||
}
|
||||
return (await res.json()) as T;
|
||||
}
|
||||
|
||||
export const api = {
|
||||
login: (user: string, password: string, totp: string) =>
|
||||
req<{ token: string }>("/auth/login", null, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ user, password, totp }),
|
||||
}),
|
||||
chat: (token: string, message: string) =>
|
||||
req<{ reply: string }>("/chat", token, { method: "POST", body: JSON.stringify({ message }) }),
|
||||
review: (token: string) => req<{ assets: Asset[] }>("/review", token),
|
||||
approve: (token: string, id: string) =>
|
||||
req<{ asset: Asset }>(`/review/${encodeURIComponent(id)}/approve`, token, { method: "POST" }),
|
||||
refuse: (token: string, id: string) =>
|
||||
req<{ asset: Asset }>(`/review/${encodeURIComponent(id)}/refuse`, token, { method: "POST" }),
|
||||
state: (token: string) => req<{ phase: string; tools: number }>("/state", token),
|
||||
};
|
||||
@@ -0,0 +1,48 @@
|
||||
import { createContext, useContext, useEffect, useState, type ReactNode } from "react";
|
||||
import * as SecureStore from "expo-secure-store";
|
||||
import { api } from "./api";
|
||||
|
||||
/**
|
||||
* Auth mobile : JWT stocké de façon sécurisée (Keychain/Keystore via
|
||||
* expo-secure-store). Owner unique, login fort (mdp + TOTP) côté backend.
|
||||
*/
|
||||
interface AuthState {
|
||||
token: string | null;
|
||||
ready: boolean;
|
||||
login: (user: string, password: string, totp: string) => Promise<void>;
|
||||
logout: () => Promise<void>;
|
||||
}
|
||||
|
||||
const KEY = "chlova.token";
|
||||
const Ctx = createContext<AuthState | null>(null);
|
||||
|
||||
export function AuthProvider({ children }: { children: ReactNode }) {
|
||||
const [token, setToken] = useState<string | null>(null);
|
||||
const [ready, setReady] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
void SecureStore.getItemAsync(KEY).then((t) => {
|
||||
setToken(t);
|
||||
setReady(true);
|
||||
});
|
||||
}, []);
|
||||
|
||||
const login = async (user: string, password: string, totp: string): Promise<void> => {
|
||||
const { token: t } = await api.login(user, password, totp);
|
||||
await SecureStore.setItemAsync(KEY, t);
|
||||
setToken(t);
|
||||
};
|
||||
|
||||
const logout = async (): Promise<void> => {
|
||||
await SecureStore.deleteItemAsync(KEY);
|
||||
setToken(null);
|
||||
};
|
||||
|
||||
return <Ctx.Provider value={{ token, ready, login, logout }}>{children}</Ctx.Provider>;
|
||||
}
|
||||
|
||||
export function useAuth(): AuthState {
|
||||
const ctx = useContext(Ctx);
|
||||
if (!ctx) throw new Error("useAuth hors AuthProvider");
|
||||
return ctx;
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
// Tokens CHLOVA (dark HUD "Jarvis-red") — alignés sur docs/ui-design.md.
|
||||
export const C = {
|
||||
bg: "#020617",
|
||||
surface: "#0f172a",
|
||||
surface2: "#1e293b",
|
||||
border: "#334155",
|
||||
fg: "#f8fafc",
|
||||
muted: "#94a3b8",
|
||||
accent: "#ff3b3b",
|
||||
danger: "#b91c1c",
|
||||
success: "#22c55e",
|
||||
warning: "#f59e0b",
|
||||
};
|
||||
Reference in New Issue
Block a user