Soumettre une app IA sur l'App Store en 2026 : les vrais rejets que je me suis pris
J’ai shippé VoiceJournal sur l’App Store. Pas du premier coup. Voici les rejets concrets que j’ai eus de l’App Review, les fixes exacts, et les pièges process qui m’ont coûté du temps et que je n’aurais trouvés dans aucune doc officielle.
Si tu shippes une app iOS — surtout une app IA avec abonnements — ce qui suit te fera gagner plusieurs jours.
Avant même la première soumission
Trois trucs à activer dès le jour 1, pas plus tard :
1. Apple Small Business Program (le formulaire). Réduit la commission Apple de 30% à 15% sur tous les achats in-app si tu fais moins d’1M$/an (= toi). Gratuit, applicable le trimestre suivant. Sur un abo à 4,99 €, tu gardes 4,24 € au lieu de 3,49 €. Aucune raison d’attendre.
2. Conformité DSA (Digital Services Act) si tu vises l’UE. App Store Connect → Business → “Conformité à la législation sur les services numériques” → cocher “J’ai le statut de commerçant”. Sans ça, tes abonnements peuvent être bloqués en statut DEVELOPER_ACTION_NEEDED et ne seront pas disponibles, même en Sandbox.
3. Sandbox Tester dédié. Ne tente pas de tester un IAP avec ton compte Apple personnel. Apple bloque pour empêcher les devs de tricher. Crée un tester dans App Store Connect → Users & Access → onglet Sandbox (pas “People”), utilise un alias tonmail+sandbox@gmail.com, et connecte-toi sur l’iPhone via Réglages → App Store → tout en bas → Compte Sandbox.
Ce que ton app DOIT contenir avant de soumettre
Avant de raconter les rejets, voici les règles connues qu’il ne sert à rien de contourner. Apple les check sur quasiment chaque app, et c’est plus efficace de les implémenter d’entrée que de te les prendre en review.
1. Suppression de compte (Guideline 5.1.1)
Bouton “Supprimer mon compte” dans les paramètres. Doit effacer toutes les données : entries, profil, abonnement local et le compte d’auth lui-même.
Côté Supabase, le client ne peut pas supprimer dans auth.users. Il faut une Edge Function avec la service role key :
Deno.serve(async (req) => {
const { user } = await authenticate(req);
await supabase.from("entries").delete().eq("user_id", user.id);
await supabase.from("profiles").delete().eq("id", user.id);
await supabaseAdmin.auth.admin.deleteUser(user.id);
return new Response("ok");
});
Apple teste : ils créent un compte, suppriment, re-tentent le login, vérifient que c’est mort.
2. Écran de consentement IA (Guideline 5.1.2(i))
Obligatoire depuis 2025 si l’app envoie des données à des services IA tiers. Pas une mention enfouie dans la privacy — un écran dédié dans le flow, avant la première transmission.
Trois éléments à présent dessus :
- Nom explicite des services (“Whisper d’OpenAI reçoit ton audio”, “Claude d’Anthropic reçoit le texte transcrit”).
- Deux boutons : “J’accepte et continue” et “Non merci”.
- Lien cliquable vers la privacy policy.
3. Sign in with Apple (Guideline 4.8)
Obligatoire si tu proposes Google Sign-In, Facebook, ou n’importe quel login tiers. Pas optionnel.
Et pas un bouton custom — utiliser le composant officiel :
import { AppleAuthenticationButton } from "expo-apple-authentication";
<AppleAuthenticationButton
buttonType={AppleAuthenticationButtonType.SIGN_IN}
buttonStyle={AppleAuthenticationButtonStyle.BLACK}
cornerRadius={5}
style={{ width: 200, height: 44 }}
onPress={handleAppleSignIn}
/>
Un bouton “Sign in with Apple” générique fait main avec une SVG de pomme = rejet immédiat.
4. Paywall — toutes les infos à côté du bouton d’achat (Guideline 3.1.2(c))
À proximité visuelle directe du bouton :
- Titre de l’abonnement
- Durée (1 mois, 1 an)
- Prix facturé
- Texte trial explicite : “Essai gratuit de 7 jours. Ensuite 4,99 €/mois sera automatiquement débité.”
- Texte légal : “Se renouvelle automatiquement. Annulable dans Réglages > Abonnements au moins 24h avant la fin de la période.”
- Liens cliquables vers Terms of Use et Privacy Policy.
- Bouton “Restaurer les achats”. Obligatoire. Même si tu trouves ça moche.
5. Privacy Policy + Terms — hébergement, contenu, liens partout
C’est le sujet le plus sous-estimé. Apple veut trois choses, simultanément :
a) Une page web Privacy Policy + Terms hébergée. GitHub Pages est gratuit et suffisant. Le contenu de la Privacy Policy doit explicitement couvrir :
- Quelles données sont collectées et pourquoi
- Tous les services tiers nommés par leur nom (OpenAI Whisper, Anthropic Claude, Supabase, RevenueCat, etc.). C’est non négociable et c’est la première chose checkée.
- Où les données sont stockées (UE, US) et combien de temps
- Transfert international (si serveurs hors UE)
- Droits utilisateurs (accès, suppression, portabilité — RGPD)
- Cookies et tracking, même si “aucun”
- Vente de données : explicitement dire “non”
- Protection des mineurs (< 13 ans)
- Procédure de contact
b) Liens cliquables dans l’app. Depuis Settings et depuis le paywall. Toujours via WebBrowser.openBrowserAsync() plutôt que Linking.openURL() — ce dernier peut crasher sur certains devices.
import * as WebBrowser from "expo-web-browser";
<Pressable onPress={() => WebBrowser.openBrowserAsync("https://aivoicejournal.app/privacy")}>
<Text>Privacy Policy</Text>
</Pressable>
c) Liens dans la description App Store Connect. Apple veut voir ces deux lignes à la fin du texte de description, dans chaque langue soumise :
Terms of Use: https://aivoicejournal.app/terms
Privacy Policy: https://aivoicejournal.app/privacy
Manquer un seul des trois (page hébergée, liens in-app, liens dans la description ASC) = rejet 3.1.2(c).
6. Privacy Nutrition Labels dans App Store Connect
Section “Confidentialité de l’app”. Tu dois déclarer chaque type de donnée que ton app collecte, et préciser l’usage. Le reviewer compare ce que tu déclares avec ce qu’il observe en testant. Discordance = rejet.
Pour VoiceJournal, voilà ce qui est coché :
| Catégorie | Donnée | Usage |
|---|---|---|
| Contact Info | Email Address | App Functionality |
| Contact Info | Name | App Functionality |
| User Content | Audio Data | App Functionality |
| User Content | Health & Fitness (mood) | App Functionality |
| User Content | Journal & transcript | App Functionality |
| Identifiers | User ID | App Functionality |
| Purchases | — | App Functionality |
| Tracking | — | Non |
Règle simple : si tu envoies de l’audio à un serveur, tu coches “Audio”. Si tu envoies du texte, “Text”. Si tu calcules un score d’humeur, “Health & Fitness”. Si tu loues les données à des tiers, “Tracking” — sinon non.
Une nutrition label incomplète, c’est le rejet le plus stupide à se prendre. Vingt minutes de boulot, 24-48h de review perdues si tu oublies.
7. Disclaimer santé / bien-être
Si ton app produit un score, un conseil, ou une analyse liée à la santé mentale, à l’humeur, à la nutrition, au sommeil : afficher un disclaimer visible.
Ne constitue pas un avis médical ou psychologique.
VoiceJournal donne un Mind Score : disclaimer présent dans l’app et dans la description App Store.
8. NSMicrophoneUsageDescription en anglais
Le reviewer est anglophone par défaut. Si ton Info.plist dit “L’app a besoin du micro pour t’enregistrer”, il rejette pour metadata incomplète. Écris en anglais (ou bilingue), et sois spécifique :
<key>NSMicrophoneUsageDescription</key>
<string>VoiceJournal needs microphone access to record your voice journal.</string>
Pareil pour NSCameraUsageDescription, NSPhotoLibraryUsageDescription, etc.
Ce qui est INTERDIT et déclenche un rejet quasi automatique
- “Coming Soon” sur des features non implémentées. Cache le bouton, ne le grise pas.
- Mock data visible par l’utilisateur. Si tu n’as pas de données, montre un empty state explicite.
- Boutons morts qui ne font rien quand on les clique (voir rejet 2.1(a) plus bas).
console.logen prod. Apple ne regarde pas, mais les compétiteurs peuvent inspecter les logs en debug.- Claimer “end-to-end encryption” si c’est juste du HTTPS. C’est un terme légal, l’utiliser sans le faire vraiment est trompeur.
- API keys d’OpenAI/Anthropic/Stripe côté client. Pas un rejet direct mais une faille de sécu qui peut te coûter cher.
Le rejet UX qui m’a le plus surpris : 4.0 — UI cassée sur iPad
Mon app cible iPhone. Je l’ai testée sur iPhone. Apple, eux, testent toujours sur iPad même si tu mets supportsTablet: false.
Sur iPad, la tab bar fait ~83px + safe area inset différent. Mes boutons “I accept and continue” (consent IA) et “Analyze my entry” (record) étaient complètement masqués derrière la tab bar.
Fix qui a marché du premier coup :
- Tout écran avec un bouton en bas doit être scrollable (
ScrollViewou équivalent). - Ne pas utiliser
flex: 1 + justifyContent: space-betweensur ces écrans. - Ajouter
paddingBottom: 140pxminimum (j’avais 48px, c’était insuffisant). - Appliquer ce padding globalement dans le
ScreenContainerpour tous les écrans scrollables.
Écrans concernés : paywall, consent, login, record, settings — tout ce qui a un CTA bas.
Rejet 2.1(a) — “No action when tapping Analyze my entry”
Le reviewer a enregistré 1 seconde. Mon handleAnalyze vérifiait isValidDuration() et retournait silencieusement false sans alerte, sans toast, sans rien. Bouton “mort” → rejet automatique.
Règle générale qui en sort :
Apple teste avec le minimum d’effort. Si un bouton peut être cliqué et que rien ne se passe visiblement, c’est un rejet.
Fix :
- Toujours produire un retour visible : alerte, toast, animation, message d’erreur.
- “Recording too short. Speak for at least 15 seconds.” est mille fois mieux qu’un retour silencieux.
- J’ai mis 15 secondes en durée minimum réaliste (sinon l’IA n’a rien à analyser de toute façon).
- Bonus : noter dans les remarques review “Recording must be at least 15 seconds long” pour aider le reviewer.
Rejet 5.1.2(i) — Consentement IA explicite
Depuis 2025, Apple exige un écran de consentement dédié si ton app envoie des données à des services IA tiers. Pas juste une mention dans la privacy policy — un écran dans le flow avec :
- Nom explicite des services (“ElevenLabs reçoit ton audio”, “Google Gemini reçoit le texte”)
- Boutons “J’accepte” et “Non merci”
- Lien direct vers la privacy policy
S’affiche avant la première transmission de données vers un service IA. Pas après, pas pendant. Avant.
Rejet 3.1.2(c) — Le prix annuel mis en avant comme un prix mensuel
Mon paywall annuel affichait “2,91 €/mois” en gros et “34,99 €/an” en petit en dessous. Apple rejette : le montant facturé doit dominer visuellement (font size, color, position).
✅ ANNUEL
34,99 € ← gros, prix facturé
par an
soit 2,91 €/mois ← petit, gris
❌ ANNUEL
2,91 € ← gros, prix mensuel calculé
par mois
34,99 €/an ← petit
Les deux infos sont OK à afficher. C’est l’ordre visuel qui compte.
Rejet 3.1.2(c) — Free trial pricing pas clair
J’avais écrit “7 jours gratuits, puis 4,99 €/mois”. Rejeté.
Fix exact :
Essai gratuit de 7 jours. Ensuite 4,99 €/mois sera automatiquement débité.
Le mot “automatiquement” est obligatoire. Apple veut que l’utilisateur comprenne que c’est lui qui doit annuler s’il ne veut pas être débité.
Rejet 5.1.1 — Suppression de compte incomplète
Un simple bouton “Supprimer mon compte” qui efface les entries ne suffit pas. Apple veut que toutes les données disparaissent, y compris le compte d’auth lui-même.
Côté Supabase, le client ne peut pas supprimer dans auth.users. Solution : une Edge Function avec la service role key.
Deno.serve(async (req) => {
const { user } = await authenticate(req);
await supabase.from("entries").delete().eq("user_id", user.id);
await supabase.from("profiles").delete().eq("id", user.id);
await supabaseAdmin.auth.admin.deleteUser(user.id);
return new Response("ok");
});
Apple teste : ils créent un compte, cliquent supprimer, vérifient que le re-login échoue.
Le piège DSA : 175 pays “Non disponible” après approbation
Mon app est approuvée. Je vais voir les disponibilités → 174 “Non disponible” + 1 “Vente impossible”. Soit 175/175. Message :
Le statut de commerçant est requis pour distribuer du contenu dans l’App Store au sein de l’Union européenne.
Le DSA ne bloque que les 27 pays UE, mais ASC décoche tous les pays par défaut tant que ton trader status est en attente. Comportement surprenant et mal documenté.
Workaround pour ne pas attendre la validation DSA :
- Sidebar → Tarifs et disponibilité → “Modifier les pays ou régions”
- Cocher tous les pays sauf l’UE (US, UK, CA, AU, JP, Asie, Amérique Latine, etc.) → enregistrer
- App live sur ~170 pays en 30 min à 4 h
- Une fois DSA validé (1-7 jours ouvrés), recocher l’UE
Tu rates pas un marché, tu le décales de quelques jours.
Le piège ITMS-90186 — version pas bumpée
Tu pushes un nouveau build avec la même version dans app.json qu’une version déjà approuvée :
ITMS-90186: Invalid Pre-Release Train
The train version '1.0.0' is closed for new build submissions
Apple ferme le “train” d’une version dès qu’elle est approuvée. Plus aucun build ne peut y entrer. À chaque resoumission après approbation, tu bumps version :
// app.json
{
"expo": {
"version": "1.1.0"
}
}
Le build perdu consomme un crédit EAS. Le rebuild avec la bonne version en consomme un autre. Vérifie deux fois avant de lancer.
Le piège RevenueCat — Product IDs cassés en silence
Mes logs RevenueCat disaient :
Starting store products request for: ["rc_one_year", "rc_one_month"]
Mais dans App Store Connect, les Product IDs étaient rc_yearly_abo et rc_monthly_Abo. Sensible à la casse en plus.
Résultat : RevenueCat ne charge aucun produit, le paywall affiche un bouton désactivé, le reviewer ne peut pas acheter → rejet 2.1(b) “Purchase buttons were not shown”.
Règle dérivée importante : ne désactive jamais le bouton d’achat. Garde-le toujours cliquable, et au moment du clic, retry Purchases.getOfferings(). Affiche une erreur claire seulement si le retry échoue aussi.
Le piège App Groups si tu ajoutes un widget
Au moment où j’ai ajouté un widget iOS (expo-widgets), le build EAS a planté :
Provisioning profile "...ExpoWidgetsTarget AdHoc" doesn't support
the group.com.sowdev.voicejournal App Group.
Le widget a besoin d’un App Group pour partager des données avec l’app principale, mais cette capability n’est pas activée par défaut sur le nouveau bundle ID du target.
Fix le plus rapide : eas credentials → iOS → Build Credentials → profil concerné → target ExpoWidgetsTarget → delete provisioning profile. EAS le régénère au prochain build en incluant la capability.
Si la même erreur revient, va manuellement sur developer.apple.com → Identifiers, coche App Groups sur les 2 bundle IDs (app + widget target), régénère les profiles.
La règle générale qui résume tout
Apple ne déteste pas l’IA. Apple ne déteste pas les abonnements. Apple ne déteste pas les indie devs.
Apple déteste les apps qui ne marchent pas quand un reviewer fatigué teste à 17h : un bouton qui ne réagit pas, un texte qui dit “7 jours gratuits” sans expliquer ce qui suit, un paywall où le prix annuel ressemble à un prix mensuel, une UI cassée sur iPad qu’ils testeront même si tu as dit “iPhone only”.
Chaque rejet sur cette liste est, en réalité, une UX cassée qui mérite d’être corrigée même indépendamment de l’App Review. La review Apple, c’est juste le moment où tu reçois l’addition des UX que tu as laissées passer.