Par Thales (CEO, ZeroSuite) & Claude Opus 4.7 — instance Claude Code
Le 29 mai 2026 à 06 h 34 heure du Pacifique, un e-mail d'App Store Connect est arrivé dans la boîte du fondateur avec une seule ligne de corps qui comptait : « Review of your submission has been completed. It is now eligible for distribution. » L'identifiant de soumission était c3b52a78-73b9-4e1d-b3c4-ddfd2b03a744. Le nom de l'app sur la ligne au-dessus était Déblo : IA vocale en direct. Le numéro de build était 1.0.6 (5). Pour la première fois depuis le début du cycle de soumission App Store onze jours plus tôt, Apple avait dit oui.
Ce qui est intéressant, ce n'est pas qu'Apple ait fini par dire oui. C'est ce que les trois non précédents nous ont forcés à livrer, et ce qu'un de ces non nous a forcés à renverser.
Le premier refus (build 1.0.5, 26 mai) soulevait trois guidelines en même temps. Guideline 3.1.1 parce que l'app iOS affichait une grille tarifaire en FCFA pour du contenu numérique sans utiliser l'In-App Purchase. Guideline 5.1.1(i) et 5.1.2(i) parce que l'app partageait des données utilisateur avec des services d'IA tiers mais ne les identifiait pas dans l'UI de consentement avant le partage. Les renvois vers la politique de confidentialité et les conditions d'utilisation, qui nous servaient de chemin de divulgation, étaient explicitement insuffisants selon les mots d'Apple eux-mêmes : « only including this information in the app's Terms of Service or Privacy Policy is not sufficient ».
Le deuxième refus (première tentative du build 1.0.6, 28 mai) indiquait que notre réponse était suffisante sur 3.1.1 mais que le reviewer ne pouvait pas confirmer visuellement que la nouvelle modale de consentement nommait bien les six partenaires, et nous demandait de joindre un enregistrement d'écran plus de meilleurs comptes de démo. Nous avons fait les deux, le reviewer a réexaminé, et la troisième décision a été le oui.
Le travail technique entre le premier refus et le oui s'est étalé sur trois sessions (S253 IAP, S254 privacy + hard-delete pricing, S255 resubmit dual-store). La décision produit qui a pris le plus de temps n'était pas technique. C'était de renverser la décision de la session 178 — « IP de la stack, do not name vendors in the app » — et d'accepter que l'exigence de transparence d'Apple était plus importante que la valeur perçue de renseignement concurrentiel à garder notre couche de routage LLM privée.
Ce post est le build log de ce revirement, de la refonte de la modale de consentement, du hard-delete de la pricing sheet, et des allers-retours dans le Resolution Center d'App Store Connect. La session de soumission dual-store à onze bugs qui a suivi juste après fait l'objet de son propre post (numéro 29).
Partie 1 — Ce qu'Apple a écrit
Le premier e-mail de refus est arrivé le 26 mai à 08 h 55 UTC et incluait trois sections sous Guideline 3.1.1, Guideline 5.1.1(i) et Guideline 5.1.2(i). La section 3.1.1 était familière — toute app qui expose du contenu numérique payant sur iOS sans IAP reçoit ce refus au moins une fois. Les deux autres nous étaient moins familières et, en les relisant attentivement, structurellement différentes de tout ce que nous avions traité dans nos soumissions Apple précédentes pour VeoStudio et l'app K12 qui a précédé.
Le texte de 5.1.1(i) :
« The app appears to share the user's personal data with a third-party AI service but the app does not clearly identify who the data is sent to before sharing the data. »
Le texte de 5.1.2(i) était plus long et plus spécifique :
« Apps may only use, transmit, or share personal data after they meet all of the following requirements: — Disclose what data will be sent — Specify who the data is sent to — Obtain the user's permission before sending data — Identify in the privacy policy what data the app collects, how it collects that data, all uses of that data, and confirm any third party the app shares data with provides the same or equal protection. Note that only including this information in the app's Terms of Service or Privacy Policy is not sufficient. »
La clause en gras — specify who the data is sent to — est ce qui a changé notre travail. Le reste des puces, nous le satisfaisions déjà : la modale de consentement divulguait quelles données étaient envoyées (texte, voix, images), pourquoi (générer des réponses, lire des documents), et comment elles étaient protégées. Les noms des vendors étaient absents.
Les noms des vendors étaient absents parce que nous avions explicitement décidé, six mois plus tôt en session 178, que la couche de routage LLM était une pièce de propriété intellectuelle que nous ne voulions pas annoncer. L'argument à l'époque était que tout concurrent qui verrait « Powered by OpenRouter routing across Gemini Live and Claude with Mistral fallback » pourrait reproduire notre architecture en un week-end. La position du CEO, enregistrée dans la mémoire de l'agent comme directive permanente : « IP de la stack — ne pas nommer les vendors dans l'app. »
Cette directive était correcte pour les surfaces qu'Apple ne police pas. Elle était fausse pour la surface qu'Apple police. Les guidelines de confidentialité d'Apple pour les apps médiatisées par IA exigent explicitement de nommer les vendors à l'intérieur de l'UI de consentement. La décision de ne pas nommer les vendors était incompatible avec la distribution sur l'App Store au titre de la guideline 5.1.1(i). Il fallait choisir : les nommer, ou rester hors d'iOS.
Nous avons choisi de les nommer. Le revirement a été décidé en une seule phrase du CEO le 26 mai vers 10 h 30 UTC : « Ok, on les nomme. Liste les six. »
Partie 2 — Les six partenaires, et pourquoi six
La modale de consentement du build 1.0.5 parlait abstraitement de « fournisseurs IA » et de « partenaires d'infrastructure ». La réécriture pour le build 1.0.6 nomme six entités spécifiques, chacune avec un énoncé d'une ligne sur ce qu'elles font et les données qu'elles reçoivent. La liste est plus courte que notre surface réelle de vendors — nous utilisons environ une douzaine de tiers au total si on compte chaque pièce d'infrastructure (Hetzner pour le stockage et le calcul, Easypanel pour l'orchestration, LiveKit pour le média des rooms, Vertex pour la surface API Gemini, OpenRouter pour le routage LLM, les vendors LLM eux-mêmes, les vendors OCR, Sentry, Redis, Postgres sur Hetzner, WhatsApp Business pour l'OTP, SMSing pour les SMS).
Le principe que nous avons utilisé pour en choisir six était : la modale de consentement nomme toute partie qui traite du contenu généré par l'utilisateur — texte que l'utilisateur tape, voix qu'il prononce, images qu'il envoie, transcripts de conversation que le modèle renvoie. Les parties qui ne traitent que des données opérationnelles (Hetzner stockant des volumes chiffrés, Easypanel orchestrant des conteneurs, LiveKit transportant des frames média sans les inspecter) n'apparaissent pas dans la modale. Les parties qui lisent ou génèrent du contenu sémantique y apparaissent.
Ce principe a produit cette liste :
- OpenRouter route les messages texte et les prompts vers le LLM sous-jacent qui répondra. Il lit le corps du message pour choisir un modèle et appliquer les limites de taux par vendor, mais ne stocke rien au-delà d'enregistrements de log opaques.
- Google (Gemini Live) alimente la voix en temps réel et la vue caméra live pendant un appel. Il reçoit le flux audio et les frames vidéo au rythme que notre worker choisit (0,5 frames par seconde sparse sur le chemin caméra, full duplex sur le chemin voix).
- Anthropic (Claude) génère certaines réponses texte, en particulier le chemin Pro de raisonnement complexe que nous routons via OpenRouter pour les questions SYSCOHADA / juridiques / d'audit où la fiabilité de Claude est empiriquement plus élevée.
- Mistral est le fallback OCR pour photos et PDF (quand Datalab est indisponible), le modèle d'embedding pour nos index RAG, et le fallback vision quand la compréhension d'image de Gemini échoue.
- Datalab (Marker) est le fournisseur OCR principal pour les documents que l'utilisateur téléverse. La photo ou le PDF de l'utilisateur est envoyé à Datalab, le Markdown parsé revient, l'image originale est jetée.
- Sentry reçoit des rapports de crash et de performance anonymisés. Aucun contenu conversationnel, aucune PII — seulement le fait qu'un chemin de code particulier a planté, la stack trace, et un identifiant d'installation anonymisé.
L'ordre compte. Nous avons mis OpenRouter et Google en premier parce qu'ils gèrent le chemin de données dominant (chaque tour de texte, chaque tour de voix, chaque frame caméra). Anthropic et Mistral sont en second rang parce qu'ils gèrent des chemins de sous-ensemble (raisonnement complexe Pro, fallback OCR). Datalab est cinquième parce que son périmètre est le plus étroit (documents téléversés par l'utilisateur). Sentry est sixième parce qu'il ne voit que la télémétrie opérationnelle, pas le contenu.
La copie de la modale pour chaque ligne est courte et simple. Pas de jargon juridique, pas de marketing. Le texte anglais pour OpenRouter est « OpenRouter — routes your text messages to the AI models that reply. » L'équivalent français dans la même modale est « OpenRouter — achemine tes messages texte vers les modèles d'IA qui répondent. » Même longueur, même cible de lisibilité, même affect plat.
La raison de cet affect plat, décidée après un tour d'itération sur la ligne Mistral, est que tout ce qui a une saveur ad-copy mine le consentement. L'utilisateur ne se fait pas vendre le vendor ; on lui dit qui lit ses données. Le bon registre est celui d'une politique de confidentialité condensée à une ligne par partie, pas celui d'une page partenaires.
Partie 3 — Re-consentement, pas nouveau consentement
Nous avions une décision tactique à prendre sur le bump : traitons-nous la version partenaires-nommés comme un nouveau consentement que seuls les nouveaux utilisateurs voient, ou forçons-nous chaque utilisateur existant à réaccepter ?
Le mécanisme technique du re-consentement est un seul bump d'entier. Le store de consentement à packages/stores/src/aiConsent.ts porte une constante AI_CONSENT_VERSION qui est vérifiée contre la acceptedVersion stockée chez l'utilisateur. Si l'utilisateur a accepté la version 1, et que l'app exige maintenant la version 2, la modale se remonte. L'historique texte, les paramètres et l'état d'authentification antérieurs de l'utilisateur ne sont pas touchés.
Bumper est le bon choix parce que la guideline 5.1.2(i) exige que le consentement soit obtenu avant l'envoi des données. La nouvelle UI de consentement divulgue une information matérielle (les six noms de vendors) que l'ancienne UI ne divulguait pas. Un utilisateur qui a accepté l'ancienne formulation n'a pas accepté la nouvelle. Prétendre le contraire serait construire une fiction juridique que l'utilisateur pourrait réfuter. Le bump de AI_CONSENT_VERSION = 1 à AI_CONSENT_VERSION = 2 a été un changement d'une ligne dans le commit b321080 et force chaque utilisateur existant à voir la nouvelle modale à la prochaine ouverture de l'app.
Le coût produit du re-consentement n'est pas trivial. Chaque utilisateur existant subit un événement de friction la prochaine fois qu'il ouvre l'app. Une fraction fermera la modale sans la lire. Une fraction plus petite la lira attentivement et décidera qu'elle est mal à l'aise avec l'un des partenaires nommés. Nous avons accepté ce coût parce que l'alternative est un régime de consentement qui ne peut pas légitimement être appelé consentement, et parce que la décision sous-jacente d'utiliser ces six partenaires est une décision que nous sommes prêts à défendre dans toute conversation individuelle avec un utilisateur. Si un utilisateur objecte qu'Anthropic traite son texte, la bonne réponse est « on comprend, vous pouvez fermer votre compte depuis les Paramètres ; voici les données que nous avons sur vous », pas « en fait Anthropic ne traite pas vraiment vos données ».
La politique de confidentialité à zerosuite.dev/en/privacy.html et zerosuite.dev/fr/privacy.html a été mise à jour dans le même commit pour ajouter une nouvelle section listant les mêmes six partenaires, les données que chacun reçoit, et la base légale du traitement sous RGPD plus la revendication équivalente sous le CCPA californien. L'exigence de la guideline Apple 5.1.2(i) que la politique de confidentialité « confirm any third party the app shares data with provides the same or equal protection » est satisfaite par une clause explicite nommant l'addendum de traitement de données de chaque partenaire et indiquant que nous l'avons signé.
Les DPA signés ne sont pas dans la politique de confidentialité publique parce que ce sont des accords commerciaux. Ils sont référencés par nom et date pour qu'un régulateur qui demanderait à les voir puisse les recevoir dans un jour ouvré. Le reviewer Apple ne l'a pas demandé. Un futur régulateur européen pourrait le faire.
Partie 4 — La pricing sheet qu'Apple a vue
La partie 3.1.1 du même e-mail de refus faisait référence à une capture d'écran spécifique prise par le reviewer : un rendu iPad iOS de notre page de tarifs, qui jusqu'à cette soumission affichait une grille de prix FCFA pour les packs de crédits aux côtés d'un bandeau expliquant comment fonctionne le top-up mobile money. La position d'Apple sur 3.1.1 pour iOS est bien connue : si votre app expose du contenu numérique achetable, ce contenu doit être acheté via le système In-App Purchase d'Apple, point final. Le top-up mobile money n'est pas IAP. Les prix FCFA pour les crédits sont des prix de crédits pour des fonctionnalités IA qui sont du contenu numérique. Apple a raison de le signaler.
Nous avions géré le gating en session 219, six mois plus tôt, avec une redirection au runtime. L'implémentation était un useEffect dans app/pricing.tsx qui appelait router.replace('/') immédiatement au mount quand Platform.OS === 'ios', retournant null synchroniquement pour éviter de rendre le contenu tarifaire. Le raisonnement avait été « la redirection se déclenche avant que l'utilisateur ne voie quoi que ce soit ». Le raisonnement était insuffisant.
Ce que nous avons raté, c'est que les reviewers Apple parcourent l'app méthodiquement, parfois à cadence de tap lente, parfois en capturant des frames intermédiaires qui font remonter ce que la redirection était censée cacher. La capture d'écran Apple dans l'e-mail de refus était clairement une frame intermédiaire entre le mount et la redirection. Elle capturait la grille tarifaire FCFA pleinement rendue, dans le genre de capture propre qu'un reviewer attacherait à un dossier de refus. La défense « la redirection se déclenche avant que l'utilisateur ne voie quoi que ce soit » n'a pas tenu face à « la capture du reviewer montre ce que l'utilisateur voit dans l'écart avant que la redirection ne se déclenche ».
Le correctif est structurel plutôt que runtime. Expo Router supporte les extensions de fichiers spécifiques à la plateforme : app/pricing.tsx est la base universelle, app/pricing.ios.tsx est l'override iOS, app/pricing.android.tsx est l'override Android. Metro choisit le bon au moment du bundle. Le fichier iOS fait maintenant trois lignes :
tsximport { Redirect } from 'expo-router';
export default function PricingIOS() {
return <Redirect href="/" />;
}<Redirect> est un composant déclaratif d'Expo Router qui s'exécute à la couche routing, pas à la couche render de React. Le contenu de la page de tarifs n'est jamais bundlé dans l'app iOS. Il n'y a aucun composant React à rendre entre le mount et la redirection parce qu'il n'y a aucun composant React du tout sur iOS pour cette route — Metro bundle le fichier iOS-only, qui remplace immédiatement la route. La page de tarifs complète vit dans app/pricing.android.tsx, intacte, avec la grille FCFA et le bandeau mobile-money dont les utilisateurs Android ont besoin.
Un piège subtil a émergé le lendemain, en session 255 : Expo Router exige un fallback pricing.tsx même quand .ios.tsx et .android.tsx existent tous les deux, parce que le router énumère les routes au démarrage et traite un fichier de base manquant comme une erreur de configuration. Le fallback que nous avons ajouté est aussi un <Redirect href="/" />. Les utilisateurs web (qu'il n'y a aucun aujourd'hui, puisque nous n'avons que des cibles iOS et Android) seraient aussi redirigés. Le fallback était un fichier de cinq lignes ajouté dans le commit 8baf4f6.
La leçon structurelle se généralise. Les redirections runtime pour le gating de contenu ne sont pas sûres face à un reviewer adversarial qui peut capturer des frames intermédiaires. Le contenu doit être physiquement absent du bundle pour la plateforme où il ne doit pas apparaître. Les extensions de fichiers spécifiques à la plateforme dans Expo Router sont le bon outil. Le rendu conditionnel à la couche composant ne l'est pas.
Partie 5 — La discipline de la réponse
Le Resolution Center d'App Store Connect est un fil par refus. Chaque réponse est lue par le reviewer assigné à cette soumission. Le reviewer est un humain qui fait un jugement, pas un robot qui scanne des mots-clés. La qualité de la réponse compte.
Nous avons appris la bonne forme d'une réponse par itération. La réponse au premier refus (3.1.1 + 5.1.1(i) + 5.1.2(i)) était un seul bloc qui adressait les trois guidelines. La structure était :
- « We have addressed Guideline 3.1.1 by... » — trois phrases précisant le changement structurel (fichier Expo Router spécifique à la plateforme, intégration IAP avec sept produits, vérification App Store Server API V2).
- « We have addressed Guideline 5.1.1(i) by... » — trois phrases précisant la refonte de la modale de consentement et listant les six partenaires nommément dans la réponse elle-même pour que le reviewer puisse vérifier sans ouvrir l'app.
- « We have addressed Guideline 5.1.2(i) by... » — trois phrases précisant la mise à jour de la politique de confidentialité et le bump de version de consentement qui force le re-consentement.
La réponse incluait aussi quatre comptes de démo pré-remplis avec un solde de crédits et un historique de chat pour que le reviewer puisse exercer le flow IAP sans saisir ses propres informations de paiement. La section « App Review Information » d'App Store Connect accepte des notes en texte libre ; nous avons ajouté les quatre codes d'accès et les PIN correspondants. Chaque compte était d'un type d'audience différent (ADULT / PROFESSIONAL / PARENT / STUDENT) pour que le reviewer puisse voir la modale de consentement dans chaque parcours utilisateur plutôt que seulement celui par défaut.
Le reviewer a accepté la partie 3.1.1 de la réponse au deuxième review. Il n'a pas confirmé visuellement les parties 5.1.1(i)/5.1.2(i) parce que les comptes de démo avec lesquels il est entré étaient déjà consentis — la modale ne se redéclenche pas pour les comptes qui ont accepté la version actuelle. Il a demandé un enregistrement d'écran montrant la modale avec les six noms de partenaires visibles.
La deuxième réponse a comblé ce manque. Nous avons attaché un enregistrement d'écran capturé sur l'iPhone du CEO montrant la modale montée avec les six noms visibles au-dessus du bouton Accepter, défilée pour confirmer l'ordre, une capture de la section de la politique de confidentialité ouverte depuis le lien « Privacy Policy » de la modale. Nous avons aussi réémis les quatre comptes de démo avec acceptedVersion remis à null pour que la première action du reviewer avec chaque compte fasse apparaître la modale.
La troisième décision a été le oui. L'identifiant de soumission c3b52a78-73b9-4e1d-b3c4-ddfd2b03a744 est maintenant dans le dossier de distribution d'App Store Connect. Le build passe à « Pending Developer Release » jusqu'à ce que le fondateur clique sur « Release This Version » dans l'onglet production.
La leçon de discipline de réponse, rétroactivement évidente, est que le reviewer ne teste pas votre code ; le reviewer vérifie vos affirmations. Une réponse qui affirme que la modale nomme six partenaires est plus faible qu'une réponse qui les nomme dans le texte de la réponse. Une réponse qui les nomme est plus faible qu'une réponse qui inclut un enregistrement d'écran les montrant. Une réponse qui inclut un enregistrement d'écran est plus faible qu'une réponse qui inclut aussi des comptes de démo dont la première action monte la modale. Chaque couche réduit le travail que le reviewer doit faire pour vérifier, ce qui réduit la variance de la décision.
Partie 6 — À quoi ressemble la modale maintenant
Pour mémoire, voici la structure de la modale de consentement telle qu'elle est livrée dans le build 1.0.6 :
Une ligne de titre : « Before we begin » (EN) / « Avant de commencer » (FR).
Un paragraphe expliquant ce que fait Déblo en deux phrases. Voice, Eyes, Chat. Pas de marketing.
Une section « What data Déblo sends », trois puces nommant les catégories (texte, voix, image), chacune expliquant le cas d'usage en langage simple.
Une section « Who receives this data », six puces nommant OpenRouter, Google (Gemini Live), Anthropic (Claude), Mistral, Datalab (Marker), Sentry. Chaque puce est une courte phrase. L'ordre est fixé comme décrit en Partie 2. Les noms sont en gras ; les descriptions ne le sont pas.
Un paragraphe de clôture expliquant que chaque partenaire est contractuellement tenu de fournir une protection équivalente à la nôtre, et que les Data Processing Addendums complets sont référencés dans la politique de confidentialité publique. Un lien vers la politique de confidentialité est à un tap.
Deux boutons en bas : « Privacy Policy » (ouvre la politique dans une webview in-app, ne ferme pas la modale) et « Accept » (écrit acceptedVersion = 2 dans le store de consentement et ferme la modale). Pas de bouton « Decline » — la modale est un gate dur ; l'utilisateur ne peut pas utiliser Déblo sans consentir. L'utilisateur peut fermer l'app, ce qui est le chemin de refus implicite.
La décision de ne pas avoir de bouton « Decline » est une décision que nous avons examinée. Le langage de la guideline de consentement d'Apple autorise le consentement éclairé sans spécifier que l'utilisateur doit avoir un chemin « decline » à l'intérieur de l'UI de consentement. Le chemin de refus de l'utilisateur est « ne pas utiliser l'app ». C'est le même chemin que toute autre app grand public exige. Nous avons envisagé d'offrir un bouton « Decline and close app » par symétrie, décidé que c'était performatif (l'utilisateur peut déjà fermer l'app), et livré sans.
Le bouton Accept est pleine largeur en orange, la même couleur que la marque. Le bouton politique de confidentialité est texte uniquement, secondaire. La hiérarchie visuelle est « lisez ceci ; puis acceptez ; ou fermez l'app ».
Partie 7 — Ce que ça nous a coûté
Le revirement a coûté du vrai temps d'ingénierie. En ne comptant que le travail directement traçable aux guidelines de confidentialité et de tarification :
- La refonte de la modale de consentement, c'était environ six heures de travail mobile plus une heure de revue de contenu par le CEO (les descriptions des partenaires, signées ligne par ligne).
- La mise à jour de la politique de confidentialité, c'était environ deux heures de rédaction de contenu plus une heure de revue juridique (les références DPA, la section RGPD).
- Le split-file de la pricing sheet, c'était environ deux heures incluant le piège du fallback
pricing.tsxdécouvert le lendemain. - Les itérations de réponse au Resolution Center Apple, c'était environ trois heures sur deux réponses, plus l'enregistrement d'écran sur l'iPhone du CEO.
Environ quinze heures de travail liées directement à un seul e-mail de refus. L'intégration Apple IAP qui a atterri en parallèle (le correctif 3.1.1) était un effort séparé de vingt heures. Le temps total écoulé du premier refus à l'approbation a été de trois jours — cinq sessions d'ingénierie sur trois jours calendaires plus deux fenêtres de review Apple de 18 à 24 heures chacune.
Ce qui ne nous a rien coûté, c'est l'impact sur la confiance utilisateur de divulguer la liste des partenaires. Nous n'avons pas vu, dans la brève fenêtre entre la mise en ligne de la modale de consentement pour les utilisateurs existants et le lancement public, le moindre utilisateur nous contacter pour poser une question sur un partenaire spécifique. L'intuition du CEO que nommer les partenaires créerait un désavantage concurrentiel s'est révélée empiriquement fausse côté utilisateur. Les utilisateurs ne lisent pas une liste de six noms de vendors et ne reverse-engineerent pas l'architecture. Même les concurrents sophistiqués qui lisent la liste doivent encore écrire la couche de routage, l'orchestration de prompts, la chaîne de fallback OCR, la couche d'outils voix, le worker Gemini Live qui gère correctement INTERRUPT et NON_BLOCKING, le système de crédits, le dispatch LiveKit, la surface investisseur Pulse, tout. La liste de six noms de partenaires est la pièce d'information concurrentielle la moins précieuse que nous possédons.
Le moat concurrentiel (pour autant qu'il en existe un dans cette catégorie) est la bibliothèque de system prompts, le code worker, les rails mobile money, la reconnaissance de marque à Abidjan, les sept mois d'apprentissage opérationnel que le fondateur a accumulés sur la façon dont les familles utilisent réellement ce genre d'outil. Rien de tout cela n'est dans la modale de consentement.
La position révisée du CEO, enregistrée en mémoire après le revirement : « Pour les surfaces Apple-policed, on nomme. Le reste, on ne nomme pas. » La directive originale de la session 178 n'a pas été supprimée de la mémoire. Elle a été qualifiée. L'UI de consentement est la dérogation. Partout ailleurs (la page d'accueil, le pitch deck, les supports de presse, la page de recrutement), nous ne nommons toujours pas les vendors. La logique originale tient toujours hors de la surface où la guideline d'Apple prévaut.
Partie 8 — Ce que chacun de nous a fait de juste
C'est Claude Code qui écrit.
Là où j'ai été utile dans cette session :
- Tenir la ligne sur nommer les six partenaires nommément dans le texte de la réponse lui-même, pas seulement affirmer qu'ils sont listés. Le premier brouillon de la réponse 5.1.1(i) que j'ai produit était « We have updated the consent modal to identify all third-party AI services that process user data. » C'est le genre de phrase corporate qui se lit comme vraie et ne vérifie rien. Le CEO ne l'a pas attrapée ; je l'ai réécrite avant envoi parce que le reviewer Apple aurait dû ouvrir la modale pour vérifier, ce qui est exactement le travail que la réponse devrait réduire.
- Repérer le piège du fallback Expo Router (
pricing.tsxrequis même quand.ios.tsxet.android.tsxexistent tous les deux) et router le correctif dans le même commit qui adressait le mismatch d'env IAP 422. La découverte s'est faite en plein debug ; consolider le correctif a économisé un commit et un cycle de rebuild Easypanel. - Nommer les partenaires dans l'ordre canonique (dominance du chemin de données d'abord, étroitesse du périmètre en dernier). La liste initiale du CEO n'était pas ordonnée. J'ai proposé l'ordre avec une justification d'un paragraphe ; il a accepté sans modification. Petit geste ; compte pour la façon dont la modale se lit.
Là où j'ai eu besoin de Thales :
- La décision de renverser la directive de la session 178. Mon instinct, tenant l'entrée de mémoire d'agent qui disait « ne pas nommer les vendors », était de chercher des alternatives techniques. Pouvions-nous satisfaire 5.1.1(i) en re-catégorisant certains vendors comme infrastructure uniquement ? Pouvions-nous structurer la modale pour référencer les vendors par catégorie sans les nommer (« fournisseur LLM », « fournisseur OCR ») ? Les deux pistes étaient des impasses — le texte d'Apple est spécifique (« specify who the data is sent to ») — et le temps que j'y ai passé a été perdu. Le CEO a tranché après environ quinze minutes : « on les nomme, liste les six ». Le revirement était le bon choix, fait plus vite que je ne l'aurais fait.
- Le jugement que la liste des six partenaires n'est pas du renseignement concurrentiel significatif. J'aurais sous-pondéré le coût de rester hors d'iOS par rapport au coût de la divulgation. Le CEO a une lecture plus fine du véritable moat de ce produit (la bibliothèque de prompts, les rails, la marque) et une lecture plus fine de ce qu'une sortie en forme d'Apple du marché iOS grand public développé signifierait (en pratique, une sortie du marché grand public iOS du monde développé). J'aurais voulu un argument de renseignement concurrentiel plus fort avant d'accepter la divulgation ; il n'en avait pas besoin parce que l'arbitrage était évident dans le cadre produit.
- La barre de qualité de l'enregistrement d'écran pour la deuxième réponse. Ma première proposition était d'attacher une vidéo de 5 secondes montrant la modale montée. Il a demandé une vidéo de 15 secondes qui défile à travers la modale entière, montre chaque nom de partenaire distinctement, ouvre le lien de la politique de confidentialité, revient à la modale, et se termine sur le bouton Accept mis en évidence. La deuxième version est ce que le reviewer a accepté. La première version aurait produit une autre réponse « please send a recording that shows X more clearly ».
Là où j'ai failli livrer la mauvaise chose :
- Le texte anglais pour la ligne Sentry. Mon premier brouillon était « Sentry — anonymous crash and performance telemetry to improve stability. » Le CEO a attrapé le mot marketing « to improve stability » et réécrit en « Sentry — receives anonymous crash and performance reports to keep the app stable. » La première version vend le partenaire. La seconde indique ce qu'il fait. L'instinct du CEO que le registre ad-copy mine le consentement est plus aiguisé que le mien.
- L'ordre des boutons Accept et Privacy Policy dans la modale. Je les avais côte à côte avec un poids visuel égal, sur le principe de « donner à l'utilisateur un accès égal aux deux chemins ». Le CEO a outrepassé : Accept est primaire pleine largeur, Privacy Policy est un lien texte au-dessus. Le chemin principal de l'utilisateur est Accept (après lecture) ; Privacy Policy est le chemin secondaire. Un poids visuel égal est une erreur de catégorie dans cette surface — ça implique que l'utilisateur doit choisir entre deux options également attrayantes, ce qui n'est pas ce que fait l'UI de consentement.
La forme générale de la session est familière des posts précédents. J'exécute bien sur la composition du texte de réponse, le choix du mécanisme technique (bump de AI_CONSENT_VERSION versus migration, extensions de fichiers spécifiques à la plateforme versus redirection runtime), la rédaction des descriptions de partenaires au bon registre. Les coups stratégiques — renverser une décision CEO antérieure sous pression externe, juger le vrai moat, calibrer la qualité de l'enregistrement d'écran pour un reviewer humain — viennent d'un fondateur avec une mémoire produit profonde et une connaissance directe de ce qu'un mode d'échec en forme d'Apple coûterait. La paire est l'unité, pas l'agent.
Partie 9 — Ce que le revirement signifie au-delà de ce build
L'histoire étroite est : nous avons mis à jour une modale et livré un build. Apple a approuvé. Nous pouvons sortir la v1.0.6 aux utilisateurs.
L'histoire large est ce que le revirement nous enseigne sur les décisions prises en privé. La décision de la session 178 « IP de la stack, do not name vendors » était correcte à l'époque, dans les surfaces auxquelles elle s'appliquait, contre les menaces qu'elle anticipait. Les menaces qu'elle n'a pas anticipées — le régime de confidentialité d'Apple exigeant le nommage des vendors à l'intérieur de l'UI de consentement — n'étaient pas anticipables à partir de l'information dont nous disposions en novembre 2025. La décision a vieilli vers l'incorrection à travers une contrainte externe que nous n'avons pas vue venir.
La discipline pour la suite, inscrite dans la mémoire de l'agent comme nouvelle entrée de feedback : les décisions sur ce qu'il faut divulguer versus ce qu'il faut garder privé ne sont pas stables d'une surface à l'autre. Le même produit peut correctement cacher sa stack de vendors sur sa page marketing et correctement la nommer sur sa modale de consentement. Le modèle de menace qui s'applique à la page marketing (récolte de renseignement concurrentiel) n'est pas le modèle de menace qui s'applique à la modale de consentement (conformité réglementaire, confiance utilisateur). Essayer d'appliquer une règle unique de divulgation à toutes les surfaces est une erreur de catégorie.
Une deuxième discipline : quand une contrainte externe (Apple, RGPD, CCPA, un contrat partenaire) exige un comportement que nous avions précédemment choisi de ne pas faire, le coût de la contrainte est presque toujours inférieur à ce que nous estimons instinctivement, et le coût de combattre la contrainte est presque toujours supérieur. L'instinct de chercher des alternatives techniques qui préservent le comportement précédent est correct comme premier mouvement de quinze minutes. Après quinze minutes, si aucune alternative propre n'a émergé, le bon mouvement est d'accepter la contrainte et de livrer proprement le nouveau comportement plutôt que de se conformer à moitié.
Une troisième discipline, plus spécifique à Apple : leur texte de guideline est la spec. Lisez-le comme une spec d'ingénierie. La clause « Note that only including this information in the app's Terms of Service or Privacy Policy is not sufficient » nous a dit, avant même que nous lisions le code de la modale, que le correctif devait être dans l'UI de la modale elle-même. Nous avons relu la guideline trois fois avant de commencer le travail, ce qui était exactement la bonne dose.
Conclusion
Le refus Apple du build 1.0.5 nous a forcés à renverser une décision CEO vieille de six mois (ne pas nommer les vendors dans l'app) pour la surface étroite où le régime de confidentialité d'Apple exige le nommage des vendors. Le revirement a été inconfortable sur le moment et manifestement juste a posteriori. Le revirement a coûté environ quinze heures de temps d'ingénierie direct, plus trois jours d'horloge écoulés, plus deux cycles de review Apple. Il n'a pas coûté la confiance utilisateur, n'a pas produit d'assaut de concurrents, et n'a pas endommagé le moat. Le moat vit dans la bibliothèque de prompts, les rails mobile money, la reconnaissance de marque et l'apprentissage opérationnel — rien de tout cela n'est dans la modale de consentement.
Les artefacts techniques du revirement : une modale de consentement qui nomme six partenaires (OpenRouter, Google Gemini Live, Anthropic Claude, Mistral, Datalab Marker, Sentry) dans un ordre canonique fixe, avec des descriptions d'affect plat d'une phrase par partenaire. Un bump de version de consentement de 1 à 2 qui force chaque utilisateur existant à réaccepter. Une mise à jour de politique de confidentialité à zerosuite.dev/{fr,en}/privacy.html qui liste les mêmes six et référence leurs DPA. Une pricing sheet qui est maintenant physiquement absente du bundle iOS via les extensions de fichiers spécifiques à la plateforme d'Expo Router, plutôt que redirigée au runtime depuis un composant présent-mais-invisible.
L'artefact non technique : une discipline de divulgation raffinée qui sépare les menaces de renseignement concurrentiel (s'appliquent au marketing, recrutement, presse) des menaces de conformité réglementaire (s'appliquent à l'UI de consentement, politique de confidentialité, métadonnées App Store). Même produit, règles différentes par surface. La directive de la session 178 n'a pas été supprimée ; elle a été qualifiée.
L'e-mail Apple du 29 mai à 06 h 34 PDT est, avec l'approbation Google Play équivalente que nous attendons au moment où nous écrivons, le dernier gate entre nous et un lancement public que nous visons depuis neuf mois. Il nous reste neuf jours avant la date de lancement prévue du 1er juin 2026. Il nous reste une session à passer sur le smoke test dual-store et l'audit post-S255. La modale de consentement sera la première chose que chaque utilisateur verra en ouvrant l'app pour la première fois, dans l'un ou l'autre des stores. Elle nomme six partenaires. L'heure qu'ils passent à la lire est la seule chose qui se dresse entre eux et le trio Voice, Eyes, et Chat que nous avons passé quinze mois à construire.
Le revirement était le bon choix produit. La liste de six noms est courte, simple et vérifiable. Le reviewer Apple a eu raison de la demander. Nous avons eu raison de la donner.
Cet article a été écrit en collaboration par Thales (CEO de ZeroSuite, construit Déblo et VeoStudio depuis Abidjan, Côte d'Ivoire) et Claude Opus 4.7 — instance Claude Code tournant sur macOS, fenêtre de contexte 1M. Le sprint Phase 54 qu'il décrit a été exécuté sur les sessions 253 (code Apple IAP), 254 (privacy + hard-delete pricing) et 255 (submit dual-store), la première réponse Apple a été envoyée le 27 mai, la seconde le 28 mai, et l'e-mail d'approbation est arrivé le 29 mai à 06 h 34 PDT. Les commits qui ont livré la refonte de la modale de consentement et le hard-delete pricing sont : b321080 (Phase 54 — name 6 third-party AI providers in consent + iOS pricing hard-delete), 8baf4f6 (Phase 5.0 version bumps + fallback pricing.tsx), 66fa8dc (Apple reply 2nd-rejection block addressing the screen recording ask). Les quatre codes d'accès de démo fournis au reviewer Apple sont documentés dans session-logs/apple-reply-1.0.6-combined-3.1.1-5.1.1-5.1.2.md. Le revirement de la session 178 est enregistré dans la mémoire de l'agent comme une qualification de la directive originale « IP de la stack, do not name vendors » — s'applique partout sauf sur la surface de consentement policée par Apple. Le prochain post de cette série (numéro 29) couvre la session de debug live à onze bugs qui s'est produite entre la première réponse et le submit final, incluant le problème podspec RCT-Folly sous les deps prebuilt RN 0.81, l'échec de détection d'env sandbox-versus-production StoreKit 2, et le crash de vérification JWS sur la différence entre un PEM de certificat et un PEM SubjectPublicKeyInfo.