Skip to Content

Chapitre 17.3 — Achats in-app & RevenueCat

Où on en est : le modèle choisi (souvent : le Pro unlock). Reste l’implémentation — et Play Billing brut est un marécage (validation de reçus, états d’achat, edge cases). RevenueCat est la couche qui l’assèche.

⏱️ TL;DRRevenueCat (react-native-purchases) enveloppe Play Billing : les produits se configurent dans SON dashboard (« entitlements » — les droits), le code tient en trois appels (getOfferings, purchasePackage, customerInfo.entitlements), la validation des reçus et la restauration sont gérées. Gratuit jusqu’à 2 500 $/mois de revenus. Le paywall : un écran honnête qui liste ce que débloque le Pro. Et les pièges qui font les avis 1★ : la restauration cassée et l’achat non livré.

🎯 Objectifs

  • Comprendre la chaîne complète d’un IAP (Console → RevenueCat → l’app).
  • Implémenter le Pro unlock : produit, paywall, déblocage, restauration.
  • Tester les achats (licence testeurs, cartes de test — sans dépenser).
  • Éviter les trois bugs qui coûtent la confiance (et les avis).

Pourquoi une couche au-dessus de Play Billing

Play Billing brut impose : gérer les états d’achat (pending, acknowledged…), VALIDER les reçus (idéalement côté serveur — que nous n’avons pas !), la restauration multi-appareils, les edge cases (achat interrompu, remboursement). RevenueCat fait tout ça — validation serveur comprise (LEURS serveurs — l’exception assumée à notre « zéro backend » : c’est le backend de quelqu’un d’autre, gratuit sous 2 500 $/mois) :

La chaîne de configuration (une fois) : le produit dans la Play Console (Monétiser → Produits → drill_pro, 4,99 €) → le projet RevenueCat (lié par la clé de service Play — la même mécanique que eas submit, ch. 13.2) → l’entitlement pro (le DROIT, découplé du produit : demain un abonnement pourra donner le même droit) → l’offering (ce que le paywall affiche).

Le code — trois appels

npx expo install react-native-purchases # module natif → dev build (le réflexe)
// lib/purchases.ts import Purchases from 'react-native-purchases' export async function initPurchases() { Purchases.configure({ apiKey: 'goog_XXXX' }) // clé PUBLIQUE RevenueCat (pas un secret) } export async function getProOffering() { const offerings = await Purchases.getOfferings() return offerings.current?.availablePackages[0] ?? null } export async function buyPro(): Promise<boolean> { const pkg = await getProOffering() if (!pkg) return false const { customerInfo } = await Purchases.purchasePackage(pkg) return customerInfo.entitlements.active['pro'] !== undefined } export async function hasPro(): Promise<boolean> { const info = await Purchases.getCustomerInfo() return info.entitlements.active['pro'] !== undefined } export async function restorePurchases(): Promise<boolean> { const info = await Purchases.restorePurchases() return info.entitlements.active['pro'] !== undefined }

L’app interroge l’entitlement, jamais le produit : hasPro() alimente un store Zustand (isPro) consulté par les features verrouillées. La vérité vient de RevenueCat (cache local géré par le SDK — ça marche offline après le premier check).

Le paywall honnête

L’écran qui convertit SANS manipuler — les règles éprouvées :

  • La liste concrète de ce que débloque le Pro (nos features, pas des adjectifs) ;
  • Le prix localisé vient du package (pkg.product.priceString — jamais en dur : les devises et promos vivent dans la Console) ;
  • « Restaurer mes achats » visible (obligatoire moralement ET par politique) ;
  • Une fermeture évidente (le paywall-prison est un motif d’avis vengeurs) ;
  • Le moment : après la VALEUR démontrée (l’utilisateur bute sur la limite gratuite), pas au premier lancement.
// app/paywall.tsx — présenté en modal (ch. 5.4), l'essentiel <Screen> <AppText variant="title">Drill Pro</AppText> {['Thèmes illimités', 'Statistiques avancées', 'Révision espacée', 'Soutenir le dev ❤️'] .map(f => <Row key={f}><AppText>✓ {f}</AppText></Row>)} <Button label={`Débloquer — ${pkg?.product.priceString ?? '…'}`} onPress={onBuy} loading={buying} /> <Button label="Restaurer mes achats" variant="ghost" onPress={onRestore} /> </Screen>

Tester sans dépenser

La licence de test : Play Console → Paramètres → Licence de test → ajouter vos comptes Google → les achats passent SANS débit (cartes de test), remboursements instantanés testables. Le circuit complet se teste sur la piste interne (ch. 13.2) — l’IAP ne fonctionne PAS hors d’une app connue de la Console (même en dev build : il faut au moins un upload sur une piste). C’est LE point de friction du développement IAP : prévoir l’aller-retour.

Les trois bugs qui coûtent la confiance

  1. La restauration cassée. Réinstallation/nouveau téléphone → le Pro doit revenir (bouton Restaurer + hasPro() au démarrage). Un utilisateur qui REPAIE ou perd son dû = l’avis 1★ le plus mérité du monde.
  2. L’achat non livré. Le flux d’achat peut être interrompu (crash, réseau) APRÈS le paiement : le listener Purchases.addCustomerInfoUpdateListener au démarrage rattrape les achats en attente — le déblocage ne dépend jamais du seul « retour de la fonction d’achat ».
  3. L’annulation traitée comme une erreur. L’utilisateur qui ferme la feuille de paiement (PURCHASE_CANCELLED) n’est pas une exception à logger en rouge ni un écran d’erreur — c’est un « non merci » : on ferme, poliment.

⚠️ Piège — Le Pro unlock local (un booléen isPro en AsyncStorage) se contourne en 5 minutes (backup/restore, root). RevenueCat valide côté serveur — c’est le niveau de sécurité PROPORTIONNÉ pour un unlock à 5 € (l’utilisateur qui pirate ne l’aurait jamais acheté). Ne sur-ingéniériez pas au-delà : le DRM parfait n’existe pas, et votre temps vaut plus que les 0,1 % de fraude.

💡 Pour un dev Next.js — RevenueCat est le « Stripe du mobile » dans votre modèle mental : le SDK qui enveloppe la complexité du processeur de paiement, le dashboard, les webhooks (oui, il en a — le jour où un backend voudra savoir qui est Pro). Différence notable : les prix vivent dans la Play Console (localisation par Google), pas dans votre code — priceString toujours.

✏️ Exercices

1. Implémentez le flux Pro complet sur une app P19 (le Todo, par exemple) : entitlement, store isPro, paywall au bon moment (à la création de la 20e tâche), restauration.

✅ Solution

Config Console+RevenueCat → lib/purchases.ts du chapitre → isPro dans le store settings (hydraté par hasPro() au démarrage + le listener) → la garde : `if (!isPro && tasks.length

= 20) router.push(‘/paywall’)`. Le paywall liste ce que débloque Pro (tâches illimitées, thèmes, export). Test : licence de test + piste interne. Le moment du paywall (à la LIMITE, pas avant) est ce qui le rend légitime : l’utilisateur a déjà reçu de la valeur.

2. Écrivez le test manuel de la restauration (le scénario complet en 6 étapes) — il rejoint la checklist de release.

✅ Solution

(1) Acheter le Pro (licence test) → vérifier le déblocage. (2) adb shell pm clear (simule la réinstallation). (3) Rouvrir : l’app est-elle revenue en gratuit ? (normal — cache vidé). (4) hasPro() au démarrage a-t-il restauré silencieusement ? (RevenueCat reconnaît le compte Google). (5) Sinon : bouton « Restaurer » → le Pro revient. (6) Vérifier sur un SECOND appareil (même compte). Les étapes 4-6 sont celles que les devs oublient — et que les utilisateurs vivent.

3. Le grand débat : abonnement 1,99 €/mois vs unlock 4,99 € pour l’app Dépenses (P19, avec la sync hypothétique). Tranchez avec les chiffres (LTV, churn 8 %/mois typique) et UNE phrase de position.

✅ Solution

Abonnement : LTV ≈ 1,99/0,08 ≈ 25 € théoriques, MAIS churn réel des utilitaires souvent

15 %/mois → LTV ~13 €, et l’obligation morale du service récurrent (la sync = un backend à faire tourner, des coûts). Unlock : 4,99 € une fois, zéro promesse continue, zéro infra. Position défendable : « unlock tant que la sync n’existe pas ; l’abonnement le JOUR où la sync tourne — et il la finance ». Le modèle suit la structure de coûts, jamais l’inverse.

🧠 Quiz

1. Que gère RevenueCat que Play Billing brut vous laisserait ?

Réponse

La validation des reçus (côté serveur — le leur), les états d’achat et edge cases, la restauration multi-appareils, le dashboard revenus — gratuit sous 2 500 $/mois.

2. Entitlement vs produit : pourquoi le découplage ?

Réponse

L’app interroge le DROIT (« pro ») ; les produits qui l’accordent (unlock, abonnement, promo) peuvent changer côté dashboard sans toucher au code.

3. D’où vient le prix affiché sur le paywall ?

Réponse

De pkg.product.priceString — localisé par la Play Console (devises, promos). Jamais en dur dans le code.

4. Citez les trois bugs de confiance et leur parade.

Réponse

Restauration cassée (bouton + hasPro au démarrage), achat non livré (le listener customerInfo rattrape), annulation traitée en erreur (c’est un « non merci » — fermer poliment).

5. Comment teste-t-on des achats sans payer ?

Réponse

La licence de test de la Play Console (comptes exemptés de débit) + l’app sur une piste (interne suffit) — l’IAP ne fonctionne pas hors d’une app connue de la Console.


👉 Chapitre suivant : 17.4 — Le business d’une app solo — ASO, analytics, pricing : la boucle complète.