Features
Application mobile (Capacitor)
Le front du kit est une SPA Vite : npm run build en sort un bundle statique complet. Capacitor emballe ce bundle dans une coque native iOS et Android — sans dupliquer une ligne de React. Le même code sert le web et les deux binaires ; les rares adaptations natives vivent dans les fichiers existants, derrière un test d'exécution.
Ce dont tu as besoin
- Node 22 et un
npm run buildvert sur le kit. - iOS : macOS + Xcode, avec au moins un simulateur installé.
- Android : Android Studio, avec un émulateur ou un téléphone en mode développeur.
- Un agent de code dans le dépôt : Claude Code ou Codex. Le skill
apexest déjà embarqué (.claude/skills/apex/pour Claude Code,.agents/skills/apex/pour Codex) — rien à installer.
Le brief à copier-coller
Ouvre ton agent à la racine du kit et colle le bloc ci-dessous tel quel. La première ligne invoque le skill apex avec ses drapeaux ; le reste est le cahier des charges qu'il exécute.
à coller dans ton agent
/apex -a -x -t -v -s -b Emballer ce kit en application mobile iOS et
Android avec Capacitor 8, sans dupliquer une seule ligne de React.
## Contexte
Ce dépôt est un SaaS Vite + React + TypeScript + Supabase. `npm run build`
produit un bundle statique dans `dist/`. C'est ce bundle, et lui seul, qui
doit tourner dans l'application : aucune copie du front, aucun second jeu de
composants, aucune route réécrite « pour le mobile ».
## Ce que je veux
1. Capacitor 8 installé : `@capacitor/core`, `@capacitor/cli`,
`@capacitor/ios`, `@capacitor/android`, puis `@capacitor/splash-screen`,
`@capacitor/status-bar`, `@capacitor/app`, `@capacitor/preferences` et
`@capacitor/haptics`.
2. Un `capacitor.config.ts` en BUNDLE EMBARQUÉ : `webDir: "dist"`, et
surtout PAS de `server.url`. L'application doit s'ouvrir sans réseau.
3. Les deux plateformes générées (`npx cap add ios`, `npx cap add android`)
et un script `npm run mobile:sync` qui enchaîne `npm run build` puis
`npx cap sync`.
4. Une coque native minimale ÉCRITE DANS LE CODE EXISTANT, derrière un test
d'exécution native : retrait de l'écran de lancement au premier rendu utile,
style de barre d'état aligné sur le thème de l'app.
## Contraintes non négociables
Chacune est un bug déjà vécu en production, pas une préférence de style.
a. `index.html` doit devenir :
<meta name="viewport"
content="width=device-width, initial-scale=1.0, viewport-fit=cover">
Sans `viewport-fit=cover`, tous les `env(safe-area-inset-*)` valent 0 et
le contenu passe sous l'encoche et sous la barre d'état.
b. Android 12 et au-delà (API 31) : le système compose lui-même l'écran de
lancement et IGNORE l'attribut `android:background` de
`values/styles.xml`, qui est pourtant ce que pose le modèle Capacitor.
Créer `android/app/src/main/res/values-v31/styles.xml` avec
`android:windowSplashScreenBackground` et
`android:windowSplashScreenAnimatedIcon`. L'icône doit tenir dans 66 % du
côté (cercle de 160/240) : le système masque tout ce qui dépasse.
c. `launchAutoHide: true` avec un `launchShowDuration` de secours, MÊME si
l'app retire elle-même l'écran de lancement au premier rendu. À `false`,
un JS qui ne démarre pas laisse l'app figée sur le logo — indiscernable
d'un plantage pour l'utilisateur comme pour le testeur, et silencieux dans
les logs.
d. Ne JAMAIS activer `limitsNavigationsToAppBoundDomains`. Sans la clé
`WKAppBoundDomains` dans l'`Info.plist`, WebKit casse silencieusement les
appels à l'API Supabase et l'ouverture des liens externes.
e. iOS : `contentInset: "never"`. Les marges système restent gérées en CSS
par `env(safe-area-inset-*)`, exactement comme sur le web.
f. N'écris jamais un nom de classe Tailwind à l'intérieur d'un commentaire
d'un fichier scanné : le scanner lit le texte brut, commentaires compris,
et génère le CSS correspondant. Le symptôme se lit comme un bug de style
sans cause, et il revient à chaque fois qu'on documente la classe fautive.
g. Règle 3.1.1 de l'App Store : le binaire ne doit contenir ni tarif, ni lien
vers un paiement. Ajoute un script qui échoue si le bundle embarqué contient
la page de tarifs, et branche-le dans `mobile:sync`. Prouver l'absence vaut
mieux que la plaider devant un examinateur.
## Preuve attendue
- `npm run build` et `npx cap sync` verts, sorties collées.
- L'app lancée sur un émulateur Android : capture de l'écran de lancement
(fond de marque, pas de pastille noire flottant sur du gris) et capture d'un
écran de contenu qui ne passe ni sous la barre d'état ni sous l'encoche.
- Le script de la règle 3.1.1 exécuté, sortie collée.
- `git status` montrant qu'aucun fichier de `src/` n'a été dupliqué.Ce que déclenche la première ligne
apex est un workflow d'exécution en onze étapes : initialisation, analyse, plan, tâches, exécution, validation, tests, revue adverse, résolution, preuve, clôture. Les drapeaux en minuscule activent, en MAJUSCULE désactivent (-a / -A).
-a— autonome : il ne s'arrête pas pour demander confirmation à chaque étape.-x— revue adverse : sécurité, logique, clean code et audit de maintenabilité tournent en parallèle sur le code qu'il vient d'écrire, puis il corrige ce qu'ils trouvent.-t— tests : il les écrit et les fait tourner jusqu'au vert.-v— preuve : il lance l'app et prouve le parcours réel, capture à l'appui. C'est le drapeau qui interdit le « ça devrait marcher ».-s— archive : plan, revues et preuves atterrissent dans.claude/output/apex/.-b— branche : il refuse de travailler surmainet en crée une.
-x, -v et -m délèguent à des agents : code-reviewer, thermo-nuclear-code-quality-review, verifier et implementer. Ils sont livrés avec le kit dans .agents/agents/ et .claude/agents/. Si tu les supprimes, ces trois modes tombent au lancement. npm run skills:sync régénère le miroir, npm run skills:validate le vérifie.Variantes utiles : -e (économie, aucun sous-agent) sur une petite machine, -m (équipes d'agents en parallèle) sur un gros chantier, -r <identifiant> pour reprendre une tâche interrompue.
Les sept pièges, et pourquoi ils sont dans le brief
Aucun de ces points n'est théorique : chacun a coûté une session de débogage sur une app réelle. Ils sont dans le brief pour que ton agent les évite au lieu de les redécouvrir.
1. Les marges système valent zéro sans viewport-fit=cover
Le kit livre un index.html avec width=device-width, initial-scale=1.0 — et rien d'autre. Dans une WebView, env(safe-area-inset-top) vaut alors 0 : ton en-tête passe sous l'encoche et sous l'heure. C'est la toute première ligne à changer, et elle n'a aucun effet sur le rendu web.
2. Android 12 ignore l'écran de lancement de Capacitor
Depuis l'API 31, le système compose lui-même l'écran de lancement et ignore l'android:background posé par le modèle Capacitor. Non défini, le fond retombe sur le colorBackground du thème : on obtient une pastille sombre flottant sur du gris. Le correctif tient dans un fichier :
android/app/src/main/res/values-v31/styles.xml
<resources>
<style name="AppTheme.NoActionBarLaunch" parent="Theme.SplashScreen">
<item name="android:windowSplashScreenBackground">@color/colorPrimary</item>
<item name="android:windowSplashScreenAnimatedIcon">@mipmap/ic_launcher_foreground</item>
</style>
</resources>Et l'icône fournie doit être le premier plan seul, dessiné à environ 52 % du côté : le système masque tout ce qui sort d'un cercle de 66 %.
3. launchAutoHide à false transforme une erreur JS en plantage
Le réflexe est de laisser l'app retirer l'écran de lancement au premier rendu utile, donc de couper le retrait automatique. Sauf que l'écran natif est posé par-dessus la WebView : si le JS ne s'exécute pas, l'app reste sur le logo pour toujours, sans un log pour le dire. Garde un retrait de secours — le JS gagne quand il répond, le délai prend le relais sinon.
4. limitsNavigationsToAppBoundDomains casse l'API en silence
La clé a l'air d'un durcissement gratuit. En réalité elle restreint la WebView aux domaines déclarés dans l'Info.plist : sans cette déclaration, les appels à Supabase et les liens externes échouent sans erreur visible. Le confinement existe déjà une couche plus haut — Capacitor n'autorise la navigation que sur l'hôte de l'app.
5. Tailwind lit aussi les commentaires
Le scanner de Tailwind ne parse pas le code : il lit le texte brut des fichiers, commentaires inclus. Écrire un nom de classe dans un commentaire suffit à faire générer le CSS correspondant. Le symptôme ressemble à un bug de style sans cause — et il revient à chaque fois qu'on documente la classe fautive en la nommant.
6. localhost sur un téléphone désigne le téléphone
En développement, pointer l'app sur http://localhost:5173 la fait parler à elle-même. Il faut l'adresse LAN de ta machine (http://192.168.x.x:5173), et le HTTP en clair doit être activé pour ce cas seulement — jamais en dur dans la configuration, sinon l'app de production reste capable de parler en clair.
7. La règle 3.1.1 se prouve, elle ne se plaide pas
Apple refuse un binaire qui mène à un paiement hors de l'achat in-app, et le simple lien suffit à déclencher le refus. Avec un bundle embarqué tu peux faire mieux que promettre : un script qui échoue si le bundle contient la page de tarifs rend l'absence vérifiable. Prévois aussi la règle 4.2 — un binaire qui n'est qu'un navigateur vers un site se fait rejeter.
La configuration qui en sort
Pour référence, voilà à quoi ressemble le fichier une fois les sept contraintes appliquées. Ton agent doit produire l'équivalent, avec tes couleurs et ton identifiant d'application.
capacitor.config.ts
import type { CapacitorConfig } from "@capacitor/cli";
const config: CapacitorConfig = {
appId: "com.tonproduit.app",
appName: "Ton produit",
// Le bundle Vite, tel quel. Pas de `server.url` : l'app s'ouvre sans réseau
// et le code des tarifs est physiquement absent du binaire.
webDir: "dist",
backgroundColor: "#0a0a0b",
ios: { contentInset: "never", backgroundColor: "#0a0a0b" },
android: { backgroundColor: "#0a0a0b" },
plugins: {
SplashScreen: {
launchAutoHide: true,
launchShowDuration: 4000,
launchFadeOutDuration: 200,
backgroundColor: "#0a0a0b",
showSpinner: false,
androidScaleType: "CENTER_CROP",
},
StatusBar: { style: "DEFAULT", backgroundColor: "#0a0a0b" },
Keyboard: { resize: "native" },
},
};
export default config;Si tu charges l'app à distance
Le bundle embarqué est le bon choix par défaut. Mais si ton front rend côté serveur, tu n'as pas le choix : le binaire charge le site distant via server.url. Trois pièges apparaissent alors, tous constatés en vrai :
- Le chemin d'entrée va dans `appStartPath`, jamais dans `server.url`. Capacitor décide si une navigation reste « dans l'app » par un préfixe sur la chaîne complète de
server.url: avec une URL qui finit par/app, une redirection vers/loginne correspond plus au préfixe, la navigation est annulée et la page s'ouvre dans Safari. - `errorPath` doit désigner ta page hors-ligne, sinon l'échec de chargement laisse une WebView vide : la page de secours est embarquée mais morte.
- iOS exige que le fichier d'`appStartPath` existe sur le disque et fait un
exit(1)sinon — un contrôle hérité du mode embarqué, appliqué quand même en mode distant. Déplacer le chemin d'entrée impose de déplacer le dossier.
Comment tu sais que c'est fini
npm run mobile:syncpasse de bout en bout.- L'app démarre sur un émulateur Android et un simulateur iOS.
- L'écran de lancement affiche ton fond de marque, pas une pastille sur du gris.
- Aucun contenu ne passe sous la barre d'état ni sous l'encoche.
- Le script de la règle 3.1.1 est vert.
git statusne montre aucun fichier desrc/dupliqué.
-v du brief impose à ton agent de te fournir ces preuves, captures comprises, avant de déclarer le travail terminé. S'il conclut sans elles, la réponse est « recommence l'étape de vérification » — pas « merci ».