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;DR — RevenueCat (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
- 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. - L’achat non livré. Le flux d’achat peut être interrompu (crash, réseau) APRÈS le
paiement : le listener
Purchases.addCustomerInfoUpdateListenerau démarrage rattrape les achats en attente — le déblocage ne dépend jamais du seul « retour de la fonction d’achat ». - 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 —
priceStringtoujours.
✏️ 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.