Gagner de l'argent
Vendre des produits intégrés (IAP)
Achats Google Play depuis votre propre site web ou application ZIP
1📥 Obtenez le guide + l'application de démonstration fonctionnelle#
Tout ce guide existe aussi en PDF à lire hors ligne, ainsi qu'une application de démonstration complète que vous pouvez compiler et exécuter dès aujourd'hui.
Touchez ci-dessous et les deux sont enregistrés sur votre téléphone dans Téléchargements/AppMint/Guides :
- AppMint-InApp-Purchases-Guide.pdf - le guide complet, avec les étapes de la Play Console et les exemples React / Next.js
- iap-demo-index.html - un petit jeu qui vend trois choses : suppression des publicités (ponctuelle), un pack de niveaux (ponctuel) et un pass de saison mensuel
Pour essayer la démo : compressez iap-demo-index.html seul, importez-le dans l'assistant et suivez le guide. C'est le moyen le plus rapide de voir un vrai achat fonctionner de bout en bout avant de toucher à votre propre application.
Dans l'application, le bouton de cette étape les enregistre sur votre téléphone. Ici, ce sont des téléchargements directs.
AppMint-InApp-Purchases-Guide.pdf iap-demo-index.html2De quoi s'agit-il ? (en termes simples)#
Votre application peut vendre des choses (une mise à niveau Pro, des niveaux supplémentaires, un abonnement) et Google encaisse l'argent et vous le verse chaque mois.
NOUS ALLONS CONSTRUIRE ENSEMBLE UN EXEMPLE, tout au long de ce guide :
A game that sells two things
• 'Levels 11-20' ₹99 paid once, kept forever
• 'Remove ads' ₹49 every month
Vous finirez par ajouter quatre lignes à votre page. C'est tout le travail : il n'y a aucun code d'achat à écrire, et même si vous ne l'avez jamais fait, vous vous en sortirez très bien.
Vous n'avez PAS besoin de savoir programmer au-delà du copier-coller, ni d'un serveur, d'une base de données ou d'un compte chez un prestataire de paiement. Google Play s'occupe des parties difficiles :
- Affiche l'écran de paiement (carte / UPI / facturation opérateur)
- Convertit votre prix dans la devise de chaque pays
- Mémorise qui a acheté quoi, même après une réinstallation
- Envoie l'argent sur votre compte bancaire
AppMint prélève 0 % : la vente va directement sur VOTRE compte Google Play.
Fonctionne de la même façon pour Site web → APK, ZIP → APK et les applications créées par l'IA.
3Ce qu'il vous faut avant de commencer#
Seulement quatre choses :
- Votre site web ou application HTML/ZIP (ce que vous transformez en APK).
- Quelque chose à vendre : décidez de ce que reçoivent les acheteurs (par ex. « Version Pro » : sans limites, contenu supplémentaire).
- Un compte développeur Google Play : frais uniques de 25 $ sur play.google.com/console. Il est nécessaire pour publier N'IMPORTE QUELLE application sur le Play Store, pas seulement les payantes.
- Environ 1 heure au total, répartie sur les étapes ci-dessous.
Important à savoir d'emblée : les achats intégrés ne fonctionnent que lorsque votre application est installée DEPUIS le Play Store. C'est une règle de Google pour tout le monde, pas une limite d'AppMint.
4Étape 1 - Inventez votre ID de produit#
Un ID de produit est simplement un nom court que vous inventez pour ce que vous vendez. Vous saisirez ce même nom à deux endroits plus tard (votre page + la Play Console), alors gardez-le simple et notez-le.
Règles : uniquement des lettres minuscules, des chiffres et des tirets bas. Doit commencer par une lettre ou un chiffre.
Copiez l'un de ceux-ci :
- pro_unlock - mise à niveau « version Pro » (bon choix par défaut : le code ci-dessous l'utilise déjà)
- remove_ads - mise à niveau sans publicités
- level_pack_2 - pack de contenu supplémentaire
- coins_100 - pack de monnaie de jeu
- premium_monthly - abonnement mensuel
Le prix ne fait PAS partie de l'ID : vous le fixerez à l'étape 5 et pourrez le changer à tout moment sans toucher à votre code.
5Étape 2 - Étiquetez votre bouton (aucun code à écrire)#
Vous n'écrivez AUCUN code d'achat. Votre application contient déjà le moteur d'achat : vous étiquetez simplement votre propre HTML pour qu'il sache quoi vendre.
Copiez le bloc ci-dessous dans votre page et modifiez uniquement :
(1) pro_unlock → votre ID de produit, dans data-iap-buy
(2) les mots sur le bouton et dans la section payante
C'est tout. Le bouton affiche alors le vrai prix de Google dans la devise de l'acheteur, ouvre l'écran de paiement de Google au toucher, révèle la section payante après le paiement (et à chaque lancement pour ceux qui ont déjà acheté), la retire si un abonnement expire ou si un paiement est remboursé, et affiche un message clair en cas de problème.
LES QUATRE LIBELLÉS :
- data-iap-buy="id" - fait de ce bouton le bouton d'achat (obligatoire)
- data-iap-label="…{price}…" - texte du bouton ; {price} devient le vrai prix Google
- data-iap-owned="…" - texte du bouton après l'achat
- data-iap-show - une section qui n'apparaît qu'après l'achat (démarrez-la en « hidden »)
Également disponibles : data-iap-hide (disparaît après l'achat), data-iap-container (un conteneur qui n'apparaît que dans l'application), data-iap-text (à placer sur le <span> qui contient le texte si votre bouton comporte aussi des icônes).
VOUS VENDEZ PLUSIEURS CHOSES DIFFÉRENTES ? Ajoutez un bouton pour chacune. Elles sont indépendantes : une application peut supprimer les publicités ET vendre des packs de niveaux ET proposer un abonnement, et l'achat de l'un n'affecte jamais les autres. Associez une section à un produit avec data-iap-show="level_pack_2".
Seuls les ID que vous saisissez dans « Supprimer les publicités (IAP) → ID de produit » désactivent les publicités. Tout le reste de ce que vous vendez débloque ce que votre page en fait, et les publicités continuent.
DEUX FORMULES POUR LA MÊME CHOSE (mensuelle et à vie) ? Donnez aux deux boutons le même data-iap-group="adfree" : acheter l'une masque l'autre, et personne ne paie deux fois. Sans groupe, rien n'est masqué, ce qui convient à des produits sans lien.
<!-- (1) CHANGE pro_unlock to your product ID -->
<button data-iap-buy="pro_unlock"
data-iap-label="Unlock Pro — {price}"
data-iap-owned="✓ Unlocked">
Unlock Pro
</button>
<!-- (2) CHANGE the words. Appears only after buying. -->
<div data-iap-show hidden>
🎉 Pro content here — visible after buying.
</div>
<!-- Optional: disappears after buying -->
<div data-iap-hide>
Upgrade to remove the ads.
</div>
<!-- DIFFERENT THINGS: independent — buying one never affects the others -->
<button data-iap-buy="remove_ads" data-iap-label="Remove ads — {price}">Remove ads</button>
<button data-iap-buy="level_pack_2" data-iap-label="Levels 11-20 — {price}">Get levels</button>
<div data-iap-show="level_pack_2" hidden>Levels 11-20 unlocked!</div>
<!-- SAME THING, TWO PLANS: one group, so buying either hides the other -->
<button data-iap-buy="remove_ads_monthly" data-iap-group="adfree"
data-iap-label="Remove ads — {price}/month"
data-iap-owned="✓ Ad-free (monthly)">Remove ads monthly</button>
<button data-iap-buy="remove_ads_lifetime" data-iap-group="adfree"
data-iap-label="Remove ads forever — {price}"
data-iap-owned="✓ Ads removed forever">Remove ads forever</button>6Les 4 choses que les gens vendent le plus souvent#
Choisissez celle qui vous correspond. La seule différence entre elles est l'endroit où vous créez le produit dans la Play Console ; le code de votre page garde toujours la même forme.
- SUPPRIMER LES PUBLICITÉS, PAYÉ UNE FOIS (pour toujours)
Créez-le sous : Produits intégrés
Saisissez aussi l'identifiant dans : Supprimer les publicités (IAP) → ID de produit
- SUPPRIMER LES PUBLICITÉS, CHAQUE MOIS
Créez-le sous : Abonnements
Saisissez aussi l'identifiant dans : Supprimer les publicités (IAP) → ID de produit
- DÉBLOQUER DES NIVEAUX / DU CONTENU, PAYÉ UNE FOIS
Créez-le sous : Produits intégrés
Ne le saisissez PAS dans la case Supprimer les publicités : vos publicités continueraient de s'afficher.
- DÉBLOQUER DES NIVEAUX / DU CONTENU, CHAQUE MOIS
Créez-le sous : Abonnements
Ne le saisissez PAS dans la case Supprimer les publicités : vos publicités continueraient de s'afficher.
Vous pouvez vendre LES QUATRE dans la même application, et autant de produits que vous le souhaitez. L'achat de l'un n'affecte jamais les autres.
<!-- 1. remove ads, paid once -->
<button data-iap-buy="remove_ads_lifetime"
data-iap-label="Remove ads forever — {price}"
data-iap-owned="✓ Ads removed">Remove ads forever</button>
<!-- 2. remove ads, monthly -->
<button data-iap-buy="remove_ads_monthly"
data-iap-label="Remove ads — {price}/month"
data-iap-owned="✓ Ad-free">Remove ads monthly</button>
<!-- 3. levels, paid once -->
<button data-iap-buy="level_pack_2"
data-iap-label="Levels 11-20 — {price}"
data-iap-owned="✓ Unlocked">Get levels 11-20</button>
<!-- 4. all levels, monthly -->
<button data-iap-buy="all_levels_monthly"
data-iap-label="All levels — {price}/month"
data-iap-owned="✓ Subscribed">Subscribe</button>7Quels achats suppriment les publicités ?#
C'est la seule chose sur laquelle les gens se trompent, alors elle mérite 30 secondes.
Vos publicités ne font PAS partie de votre page web. Ce sont de vraies publicités Android placées par-dessus, donc rien dans votre page ne peut les désactiver. Une seule chose le peut :
wizard Step 3 → Remove Ads (IAP) → Product ID
Tout identifiant que vous saisissez dans cette case supprime les publicités lorsque quelqu'un l'achète. Tout autre identifiant que vous vendez débloque simplement ce que votre page en fait, et les publicités continuent.
Vous vendez à la fois un forfait sans publicités ET des packs de niveaux ? Saisissez uniquement l'identifiant du forfait sans publicités dans cette case. Dans notre exemple :
Product ID box: remove_ads_monthly
NOT in the box: level_pack_2
Vous proposez deux forfaits sans publicités (mensuel ET à vie) ? Mettez les deux dans cette même case, séparés par une virgule : posséder l'un ou l'autre supprime les publicités :
remove_ads_monthly,remove_ads_lifetime
Vous ne vendez pas du tout la suppression des publicités ? Laissez complètement cette option désactivée et utilisez simplement « 💰 Vendre des produits intégrés ».
Selling levels only:
Remove Ads (IAP) OFF
Sell In-App Products ON
Selling ad removal only:
Remove Ads (IAP) ON → Product ID: remove_ads_monthly
Sell In-App Products ON
Selling both, with two ad-free plans:
Remove Ads (IAP) ON → Product ID: remove_ads_monthly,remove_ads_lifetime
Sell In-App Products ON
(level_pack_2 stays out of the box)8Exemple complet - page HTML simple#
Voici notre application d'exemple complète : elle vend « Niveaux 11-20 » une fois, et « Supprimer les publicités » chaque mois. Copiez-la, changez les deux identifiants et les textes, et c'est terminé.
Lisez ce que fait chaque libellé :
- data-iap-buy → ce que vend ce bouton
- data-iap-label → le texte ; {price} devient le vrai prix Google
- data-iap-owned → le texte une fois le paiement effectué
- data-iap-show="level_pack_2" → ce bloc n'apparaît qu'après l'achat de CE produit
- data-iap-hide → ce bloc disparaît dès que quoi que ce soit est acheté
Remarquez qu'il n'y a ni <script> ni code d'achat. Votre application le contient déjà.
<h2>Get more levels</h2>
<button data-iap-buy="level_pack_2"
data-iap-label="Levels 11-20 — {price}"
data-iap-owned="✓ Levels 11-20 unlocked">
Levels 11-20
</button>
<div data-iap-show="level_pack_2" hidden>
🎉 Levels 11 to 20 are yours. Have fun!
</div>
<h2>Tired of ads?</h2>
<button data-iap-buy="remove_ads_monthly"
data-iap-label="Remove ads — {price}/month"
data-iap-owned="✓ No more ads">
Remove ads
</button>
<div data-iap-hide>
Ads help keep this game free.
</div>9Exemple complet - React ou Next.js#
LA MÉTHODE FACILE : les mêmes libellés fonctionnent dans React et Next.js, car ce n'est que du balisage. Pas de useState, pas de useEffect, pas de 'use client'. Les boutons que React ajoute plus tard sont pris en compte automatiquement.
C'est tout ce dont la plupart des applications ont besoin : utilisez le premier bloc ci-dessous et arrêtez-vous là.
LA MÉTHODE CONTRÔLÉE (second bloc) : uniquement si vous voulez que React lui-même affiche le prix et change d'écran. Alors :
- Avec Next.js, mettez 'use client' en ligne 1 du fichier, sinon la compilation échoue.
- Ne touchez à window.AppMintIAP que dans useEffect : dans le corps du composant, il s'exécute côté serveur, où window n'existe pas.
- onChange(fn) vous donne l'état tout de suite et à chaque changement, et renvoie la fonction de désabonnement que React attend.
- Avec Next.js, compilez en export statique : output: 'export' dans next.config.js.
// ── EASY WAY — just JSX, nothing else ──────────────────────────
export default function Shop() {
return (
<>
<button data-iap-buy="level_pack_2"
data-iap-label="Levels 11-20 — {price}"
data-iap-owned="✓ Unlocked">Levels 11-20</button>
<div data-iap-show="level_pack_2" hidden>🎉 Levels 11-20 are yours!</div>
</>
);
}
// ── CONTROLLED WAY — React draws everything ───────────────────
'use client'; // Next.js: must be line 1
import { useEffect, useState } from 'react';
export default function Shop() {
const [iap, setIap] = useState({ available: false, products: {} });
useEffect(() => {
if (typeof window === 'undefined' || !window.AppMintIAP) return;
window.AppMintIAP.start({
plans: [{ id: 'level_pack_2' }, { id: 'remove_ads_monthly' }]
});
return window.AppMintIAP.onChange(setIap);
}, []);
if (!iap.available) return null; // outside the app there is nothing to buy
const levels = iap.products?.level_pack_2; // always optional-chain
return levels?.owned
? <p>🎉 Levels 11-20 are yours!</p>
: <button onClick={() => window.AppMintIAP.buy('level_pack_2')}>
Levels 11-20 {levels?.price}
</button>;
}10Vérification rapide dans votre navigateur#
Ouvrez votre page dans un navigateur normal (Chrome sur votre PC ou votre téléphone).
Vous devriez voir le bouton d'achat là, sans rien faire, et votre section payante toujours masquée. Les deux sont corrects : en dehors de l'application, il n'y a rien à acheter, donc rien ne peut être acheté par accident.
Dans l'application, c'est une tout autre page : le bouton reçoit le vrai prix, ouvre l'écran de paiement de Google et se masque dès que l'acheteur le possède. Votre site web continue de fonctionner normalement pour les visiteurs ordinaires dans les deux cas.
Vous voulez que l'offre disparaisse complètement sur le web ? Placez-la dans un <div data-iap-container hidden> : ce conteneur n'est révélé que dans l'application.
11Étape 3 - Activez les achats dans l'ASSISTANT#
⚠️ NE SAUTEZ PAS CETTE ÉTAPE. Si vous la sautez, vos boutons apparaissent quand même, mais chaque toucher répond « Les achats intégrés ne sont pas activés dans cette application. » et vous n'encaissez rien.
L'ASSISTANT désigne les écrans Étape 1 → Étape 2 → Étape 3 que vous utilisez pour créer votre application. Les achats se trouvent à l'ÉTAPE 3 DE L'ASSISTANT.
FAITES-LE DANS L'ORDRE :
- Ouvrez l'assistant et allez à l'ÉTAPE 3 DE L'ASSISTANT (l'écran avec Publicités, Notifications et Autorisations).
STEPPER STEP 3 — what to set
Ads ......................... ON (needed to see the card below)
💰 Sell In-App Products ..... ON ← REQUIRED for selling anything
Selling ad removal too?
Remove Ads (IAP) ............ ON
Play Store Product ID ....... remove_ads_monthly
(two plans) .......... remove_ads_monthly,remove_ads_lifetime
License Key ................. paste from Play Console
Export ...................... AAB (not APK)12Étape 4 - Mettez votre application sur Google Play#
Dans votre Play Console (le site où les développeurs gèrent leurs applications) :
- « Créer une application » → renseignez le nom et les questions de base.
- Dans le menu de gauche : « Test » → « Test interne » → « Créer une version » → importez votre fichier AAB → « Enregistrer » et déployez-la.
- Toujours dans Test interne → onglet « Testeurs » → ajoutez votre propre adresse Gmail → copiez le lien « rejoindre » pour plus tard.
Le test interne est une zone privée : personne ne peut encore voir votre application. Il existe pour que VOUS puissiez essayer les achats avant la publication.
13Étape 5 - Créez votre produit et fixez le prix#
Dites maintenant à Google ce que vous vendez. Le menu à utiliser dépend de si les clients paient une fois ou chaque mois.
PAYÉ UNE FOIS (niveaux, Pro, pièces, suppression définitive des publicités)
- Play Console → votre application → « Monétiser » → « Produits » → « Produits intégrés ».
- « Créer le produit ».
- ID de produit : saisissez EXACTEMENT l'identifiant de votre page - level_pack_2. Même orthographe, même casse. Google ne vous laisse jamais le modifier ni le réutiliser.
- Nom : ce que voient les acheteurs, par ex. « Niveaux 11-20 ». Description : une ligne.
- Fixez votre prix dans votre propre devise : Google le convertit pour chaque pays.
- Appuyez sur « Activer ».
CHAQUE MOIS (suppression mensuelle des publicités, abonnement à tous les niveaux)
- Play Console → « Monétiser » → « Produits » → « Abonnements ».
- « Créer un abonnement ». ID de produit : remove_ads_monthly.
- Enregistrez, puis ajoutez un PLAN DE BASE : type « Renouvellement automatique », période de facturation « Mensuelle », votre prix.
- Appuyez sur « Activer » pour le plan de base ET pour l'abonnement lui-même. Si vous en oubliez un, votre application ne pourra pas le voir.
Répétez pour chaque identifiant utilisé. Le code de votre page ne change pas : l'application demande à Google de quel type est chaque identifiant.
VOUS VOULEZ DU MENSUEL *ET* DE L'ANNUEL ? Créez DEUX abonnements distincts (all_levels_monthly et all_levels_yearly) plutôt que deux plans de base dans un seul abonnement. Un abonnement avec plusieurs plans de base propose toujours le premier, sans moyen de choisir.
Modifiez un prix plus tard dans la Play Console : votre application affiche le nouveau prix toute seule, sans nouvelle compilation.
14Étape 6 - Testez (vous ne serez pas débité)#
- Play Console → menu de gauche « Configuration » → « Test de licence » → ajoutez votre Gmail à la liste des testeurs.
- Sur votre téléphone : ouvrez le lien d'accès au test interne de l'étape 4, acceptez, et installez votre application DEPUIS le Play Store.
- Ouvrez l'application. Votre bouton d'achat apparaît avec le vrai prix.
- Touchez-le : la fenêtre de paiement de Google s'ouvre et indique « Carte de test, toujours approuvée ». Achetez. Votre contenu pro apparaît. Vous ne payez rien.
- Vérification bonus : désinstallez et réinstallez l'application : le contenu pro se débloque tout seul au lancement. C'est Google qui restaure l'achat.
⚠️ Si vous installez l'APK directement (sideload) au lieu de passer par le Play Store, le bouton d'achat n'apparaîtra PAS. C'est normal : testez toujours via le lien du Play Store.
15Étape 7 - Mettez en ligne et soyez payé#
- Quand les tests sont concluants : Play Console → « Production » → créez une version avec le même AAB → déployez. (Les nouvelles applications passent par la validation de Google, généralement quelques jours.)
- Les utilisateurs achètent dans votre application → Google encaisse l'argent.
- Google verse l'argent sur votre compte bancaire chaque mois (Play Console → « Profil de paiements »). Google prélève ses frais de service habituels (15 % pour la plupart des développeurs). AppMint ne prélève rien.
Pour ajouter d'autres produits plus tard : ajoutez le nouvel ID dans le code de votre page, compilez une fois, et créez l'ID dans la Play Console. Les changements de prix n'exigent jamais de nouvelle compilation.
16Si quelque chose ne fonctionne pas#
Votre application vous dit ce qui n'a pas fonctionné au lieu d'échouer en silence. Trouvez le message que vous voyez :
« Les achats intégrés ne sont pas activés dans cette application. »
→ « 💰 Vendre des produits intégrés » était DÉSACTIVÉ à l'ÉTAPE 3 DE L'ASSISTANT lors de la compilation. Activez-le et compilez à nouveau.
« Cet article n'est pas encore disponible sur Google Play. »
→ Google ne reconnaît pas cet identifiant. Vérifiez-le lettre par lettre dans la Play Console, confirmez que le produit est ACTIF (pour un abonnement, le plan de base ET l'abonnement doivent tous deux être activés), et assurez-vous d'avoir installé l'application depuis le lien de testeur Play.
« Google Play n'a pas pu démarrer le paiement. »
→ L'application n'a pas été installée depuis le Play Store, ou a été signée avec une clé différente de celle que vous avez importée.
« Paiement en attente - il se débloquera automatiquement dès que Google confirmera. »
→ Rien à faire. Les modes de paiement lents (bons en espèces, certains UPI) prennent quelques minutes ; le déblocage arrive tout seul, même après un redémarrage de l'application.
AUCUN MESSAGE, rien ne se passe au toucher
→ Presque toujours une faute de frappe dans data-iap-buy, ou l'application n'est pas installée depuis Play. Toute autre cause affiche l'un des messages ci-dessus.
LE PAIEMENT A FONCTIONNÉ MAIS LES PUBLICITÉS SONT TOUJOURS LÀ
→ Cet identifiant de produit ne figure pas à l'ÉTAPE 3 DE L'ASSISTANT → « Supprimer les publicités (IAP) » → « ID de produit Play Store ». Ajoutez-le (séparez plusieurs par des virgules) et compilez à nouveau.
LE BOUTON D'ACHAT N'APPARAÎT JAMAIS DANS L'APPLICATION
→ « 💰 Vendre des produits intégrés » était désactivé lors de la compilation. Dans un navigateur web normal, les boutons sont masqués VOLONTAIREMENT : il n'y a rien à acheter en dehors de l'application.
RIEN NE S'EST PASSÉ APRÈS AVOIR FERMÉ LA FENÊTRE DE PAIEMENT DE GOOGLE
→ C'est une annulation normale. Touchez à nouveau et terminez le paiement.
LE PRIX SEMBLE ANCIEN
→ Play met les prix en cache sur un appareil pendant quelques heures. Une nouvelle installation affiche toujours le nouveau prix.
17React, Next.js ou JavaScript simple#
REACT ET NEXT.JS N'ONT BESOIN DE RIEN DE SPÉCIAL. Les libellés ci-dessus ne sont que du balisage, donc la même ligne fonctionne aussi là : pas de useState, pas de useEffect, pas de 'use client' :
<button data-iap-buy="pro_unlock"
data-iap-label="Unlock Pro — {price}">Unlock Pro</button>
Les boutons que React monte plus tard sont trouvés automatiquement, et le remontage est sans danger. Avec Next.js, compilez en export statique (output: 'export').
Vous préférez le code aux libellés ? Une seule ligne fait le même travail :
AppMintIAP.sell('pro_unlock', '#buy-btn');
Troisième argument facultatif : {label:'Débloquer Pro - {price}', owned:'✓ Débloqué', show:'#thanks', hide:'.upsell'}. Appelez-la une fois par produit.
UNIQUEMENT SI VOUS VOULEZ QUE REACT AFFICHE LUI-MÊME LE PRIX, sinon passez cette partie :
const [iap, setIap] = useState({ available: false, products: {} });
useEffect(() => {
if (typeof window === 'undefined' || !window.AppMintIAP) return;
window.AppMintIAP.start({ plans: [{ id: 'pro_unlock' }] });
return window.AppMintIAP.onChange(setIap);
}, []);
const pro = iap.products?.pro_unlock; // toujours en chaînage optionnel
if (!iap.available) return null; // rien à acheter en dehors de l'application
if (pro?.owned) return <p>Merci !</p>;
return <button onClick={() => window.AppMintIAP.buy('pro_unlock')}>
Unlock Pro {pro?.price}
</button>;
DEUX HABITUDES ÉVITENT QUE CELA NE SE CASSE :
- Lisez les droits PAR PRODUIT - iap.products?.<id>?.owned. Le iap.owned de premier niveau signifie « quelque chose est acquis » et change de sens le jour où vous vendez un second produit.
- Utilisez le chaînage optionnel sur tout le chemin, pour qu'un champ inattendu ne puisse jamais provoquer d'erreur pendant que React affiche.
onChange vous donne {available, adFree, owned, ownedIds, products} immédiatement et à chaque changement, et renvoie une fonction de désabonnement : un acheteur qui revient est donc déjà débloqué au premier affichage. Avec Next.js, ajoutez 'use client' en ligne 1 et compilez avec output: 'export'.
Aussi : AppMintIAP.available, .isOwned(id), .owned(), .buy(id), .state(), .refresh(). Les libellés data-iap-buy fonctionnent aussi en JSX, et les boutons que React monte plus tard sont pris en compte automatiquement.
Vous préférez piloter vous-même le pont brut ? Tout ce qui précède repose sur ceci :
- iap.isOwned(id) - vrai/faux instantané, fonctionne hors ligne
- iap.getOwnedProducts() - tableau JSON de tous les ID acquis
- iap.getProducts(reqId, idsJson) - prix en direct ; le résultat arrive via l'événement
'appmint:products'{requestId, products:[{productId, title, price, currency, type, owned}]} - iap.purchase(id) - produits ponctuels ET abonnements (détection automatique)
- Événements :
'appmint:purchase'{productId} (n'accordez QU'ICI : toucher un produit déjà acquis le redéclenche),'appmint:owned-changed'{products} (la restauration au lancement est terminée, ou un remboursement a retiré quelque chose : relisez les droits),'appmint:purchase-failed'{reason: disabled | not_found | billing_error | pending}
Vous ne vendez qu'une seule mise à niveau « supprimer les publicités » ? Utilisez plutôt le duo intégré plus simple : activez « Supprimer les publicités (IAP) » à l'étape 3 de l'assistant :
var iap = window.WebToApk;
if (iap && !iap.isPremium()) {
showRemoveAdsButton(function onTap() {
iap.startRemoveAdsPurchase();
});
}
window.addEventListener('appmint:premium', function (e) {
if (e.detail.premium) hideRemoveAdsButton();
});Supprimer les publicités (IAP)
Configuration de la facturation Google Play
1Créer un produit géré#
Dans le tableau de bord de votre Google Play Console :
- Allez dans « Monétisation » > « Produits intégrés »
- Cliquez sur « Créer le produit »
- Saisissez un ID de produit (par ex. « remove_ads_premium »)
- Définissez un nom (par ex. « Supprimer toutes les publicités ») et une description
- Fixez le prix et cliquez sur « Enregistrer »
- Cliquez sur « Activer » pour le rendre disponible aux tests
Vous vendez la suppression des publicités sous forme d'ABONNEMENT ? Créez-la sous « Monétisation » > « Abonnements » avec un plan de base à renouvellement automatique, et activez le plan de base ET l'abonnement. L'absence de publicités dure alors tant que l'abonnement est actif, et les publicités reviennent automatiquement à son expiration.
2Obtenez votre clé de licence#
Google Play exige une clé publique RSA pour vérifier les achats en toute sécurité.
Dans la Play Console :
- Allez dans « Monétisation » > « Configuration de la monétisation »
- Sous « Licences », copiez la « clé publique RSA encodée en Base64 »
- ⚠️ IMPORTANT : vous devrez coller ceci plus tard dans le générateur.
Example Key Format:
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA...3Configuration dans le générateur WebToApk#
Dans l'application du générateur :
- Activez l'option « Supprimer les publicités (IAP) » à l'étape 3
- Saisissez EXACTEMENT le même « ID de produit » que celui créé à l'étape 1
- Collez votre « clé de licence Base64 » de l'étape 2
- Générez votre APK/AAB
Vous proposez plus d'un forfait sans publicités (par ex. un abonnement mensuel ET un déblocage à vie) ? Mettez chaque ID de produit dans ce seul champ, séparés par des virgules : en posséder N'IMPORTE LEQUEL supprime les publicités. Le premier ID listé est celui qu'achète l'élément de menu intégré « Supprimer les publicités », alors placez votre forfait par défaut en premier.
Product ID: remove_ads_premium
Two plans:
remove_ads_monthly,remove_ads_lifetime4Configurer les testeurs de licence#
Pour tester les achats sans dépenser d'argent réel :
- Dans la Play Console, allez dans « Configuration » > « Test de licence »
- Ajoutez votre adresse Gmail à la liste des « Testeurs de licence »
- Vérifiez que « Réponse de licence » est définie sur « RESPOND_NORMALLY »
5Testez en piste interne ou fermée#
Les achats intégrés ne fonctionnent QUE lorsque l'application est installée via le Play Store (pistes de test) :
- Importez votre AAB généré dans la piste « Test interne » ou « Test fermé »
- Partagez le lien d'accès des testeurs avec vos comptes de test
- Téléchargez l'application depuis le Play Store
- Touchez le bouton « Supprimer les publicités » dans votre application pour tester le déroulement !
Note: Purchases will show a 'Test instrument, always approves' message for license testers.Recettes de code
Les étapes ci-dessus couvrent l'essentiel : les quatre libellés, une page simple complète, la ligne unique pour React. Ces recettes couvrent ce qu'elles ne traitent pas : plusieurs formules qui se remplacent, des abonnements qui expirent, des hooks de framework, le pont brut en détail et les tableaux de référence.
La règle unique#
Un achat est accordé uniquement lorsque Google le confirme. Pas au clic sur le bouton, pas à l'ouverture de la fenêtre, pas quand l'utilisateur revient sur votre page. Tout ce qui suit est une façon différente d'exprimer cette règle unique.
Le runtime l'applique déjà : AppMintIAP relit les droits depuis le pont au lancement, à chaque achat, à chaque remboursement et à chaque réouverture de l'application. Les droits survivent aux réinstallations, à un nouveau téléphone et au mode hors ligne, car ils sont liés au compte Google de l'acheteur, pas à votre page.
Ne débloquez jamais dans le gestionnaire de clic, et ne conservez jamais le droit d'accès uniquement dans localStorage. Un stockage de navigateur vidé ne doit pas faire perdre à un client payant ce qu'il a acheté, et une valeur localStorage copiée ne doit pas l'offrir gratuitement.
Recette 1 - un tableau de tarifs avec prix en direct#
Trois formules où l'achat de l'une remplace les autres. group est ce qui en fait des alternatives : sans lui, ce sont des produits sans lien et acheter la formule à vie laisserait le bouton mensuel affiché.
<div class="plans" data-iap-container>
<button data-iap-buy="pass_monthly" data-iap-group="pass"
data-iap-label="Monthly — {price}" data-iap-owned="Active">Monthly</button>
<button data-iap-buy="pass_yearly" data-iap-group="pass"
data-iap-label="Yearly — {price}" data-iap-owned="Active">Yearly</button>
<button data-iap-buy="pass_lifetime" data-iap-group="pass"
data-iap-label="Lifetime — {price}" data-iap-owned="Owned">Lifetime</button>
</div>
<p data-iap-show="pass">You are a member. Thank you.</p>
<p data-iap-hide="pass">Members get every level and no ads.</p>Un identifiant de produit que Google ne connaît pas (pas encore créé, pas activé, ou application non installée depuis le Play Store) reste simplement sans prix. Son bouton reste volontairement actif : le toucher indique alors la vraie raison au lieu de ne rien faire.
Recette 2 - React#
onChange se déclenche immédiatement avec l'état actuel puis à chaque changement, et renvoie sa propre fonction de désabonnement : c'est exactement la forme qu'attend un useEffect.
import { useEffect, useState } from 'react';
/** Purchase state for the whole app. Safe in a browser too: available is false there. */
export function useIap() {
const [iap, setIap] = useState({ available: false, ownedIds: [], products: {}, adFree: false });
useEffect(() => {
if (!window.AppMintIAP) return; // running in a browser, or IAP not enabled
return window.AppMintIAP.onChange(setIap); // returns the unsubscribe fn
}, []);
return {
...iap,
owns: (id) => iap.ownedIds.includes(id),
priceOf: (id) => iap.products[id]?.price ?? null,
buy: (id) => window.AppMintIAP?.buy(id)
};
}import { useIap } from './hooks/useIap';
export function ProGate({ children }) {
const { available, owns, priceOf, buy } = useIap();
if (owns('pro_unlock')) return children;
return (
<div className="upsell">
<h2>Pro</h2>
<p>Unlimited projects, no watermark.</p>
<button onClick={() => buy('pro_unlock')} disabled={!available}>
{available ? `Unlock Pro${priceOf('pro_unlock') ? ` — ${priceOf('pro_unlock')}` : ''}` : 'Not available here'}
</button>
</div>
);
}Le bouton appelle buy() ; il ne débloque rien. Le déblocage a lieu parce que Google confirme, que onChange se déclenche, que owns('pro_unlock') devient vrai et que le composant se réaffiche. C'est la même règle, exprimée en React.
Recette 3 - Vue#
<script setup>
import { ref, onMounted, onUnmounted, computed } from 'vue';
const iap = ref({ available: false, ownedIds: [], products: {} });
let stop;
onMounted(() => { stop = window.AppMintIAP?.onChange(s => (iap.value = s)); });
onUnmounted(() => stop && stop());
const isPro = computed(() => iap.value.ownedIds.includes('pro_unlock'));
const price = computed(() => iap.value.products.pro_unlock?.price ?? '');
</script>
<template>
<slot v-if="isPro" />
<button v-else :disabled="!iap.available" @click="AppMintIAP.buy('pro_unlock')">
Unlock Pro {{ price }}
</button>
</template>Recette 4 - abonnements#
Un abonnement s'achète exactement comme un produit ponctuel ; la différence est qu'il peut prendre fin. type indique de quel type est un produit et period donne l'intervalle de facturation sous forme de durée ISO-8601 : P1M mensuel, P1Y annuel, P1W hebdomadaire.
AppMintIAP.onChange(state => {
const pass = state.products.season_pass; // undefined until Google answers
const active = state.ownedIds.includes('season_pass');
document.body.classList.toggle('has-pass', active);
if (pass && !active) {
const every = { P1W: 'week', P1M: 'month', P1Y: 'year' }[pass.period] || 'period';
label.textContent = `Season pass — ${pass.price} / ${every}`;
}
});
// The pass lapsed (not renewed, or refunded): the same handler runs with it gone.
// Nothing to write — the class comes off and the paid content hides itself.Vous n'interrogez pas en boucle et vous n'avez pas besoin d'une date d'expiration. Play restaure les produits acquis à chaque lancement, donc un abonnement expiré cesse simplement d'apparaître dans ownedIds.
Activez à la fois l'abonnement et son plan de base dans la Play Console. Un abonnement dont le plan de base est inactif ne renvoie aucun détail, et le bouton indiquera « pas encore disponible sur Google Play ».
Recette 5 - le pont brut, sans assistant#
Tout ce qui précède repose sur six méthodes et cinq événements. Si vous voulez maîtriser entièrement l'interface, utilisez-les directement.
const bridge = window.WebToApk;
const enabled = !!(bridge && bridge.purchase); // false in a browser
// --- what does Google charge for these, in this buyer's currency? ---
const reqId = 'prices-' + Date.now();
window.addEventListener('appmint:products', function onProducts(e) {
if (e.detail.requestId !== reqId) return; // not our answer
window.removeEventListener('appmint:products', onProducts);
for (const p of e.detail.products) {
// {productId, type:'inapp'|'subs', title, description, price, currency, period?, owned}
render(p);
}
});
bridge.getProducts(reqId, JSON.stringify(['pro_unlock', 'season_pass']));
// --- what does this buyer already own? instant, and works offline ---
const owned = JSON.parse(bridge.getOwnedProducts() || '[]');
const isPro = bridge.isOwned('pro_unlock');
// --- buy ---
buyButton.onclick = () => bridge.purchase('pro_unlock');
// --- the ONLY place a purchase is ever granted ---
window.addEventListener('appmint:purchase', e => {
if (e.detail.productId === 'pro_unlock' && e.detail.owned) unlockPro();
});
// --- and the only place failures show up ---
window.addEventListener('appmint:purchase-failed', e => {
toast({
not_found: 'This item is not available on Google Play yet.',
billing_error: 'Google Play could not start the payment. Please try again.',
pending: 'Payment pending — this unlocks automatically once Google confirms.',
disabled: 'In-app purchases are not enabled in this app.'
}[e.detail.reason] || 'Purchase could not be completed. Please try again.');
});
// --- the launch-time restore finished, or a refund took something away ---
window.addEventListener('appmint:owned-changed', () => refreshEverything());appmint:purchase se déclenche à nouveau pour un produit déjà acquis, volontairement. Écrivez unlockPro() de façon à ce que l'exécuter deux fois soit sans conséquence et que vous n'ayez jamais à vérifier si vous l'avez déjà exécuté.
Recette 6 - dégradez honnêtement hors de l'application#
La même page doit généralement aussi s'ouvrir dans un navigateur normal : pendant le développement, ou parce que vous la publiez aussi sur le web. Là, window.WebToApk n'existe pas et aucun achat n'est possible. Dites-le, plutôt que d'afficher un bouton qui ne fait rien.
const inApp = !!(window.AppMintIAP && window.AppMintIAP.available);
if (!inApp) {
buyButton.disabled = true;
buyButton.textContent = 'Available in the Android app';
// and let people get it:
storeLink.hidden = false;
}Référence#
Tout ce que la page peut appeler, écouter ou balisage.
| Appel | Fait |
|---|---|
AppMintIAP.available | Faux dans un navigateur, ou lorsque les achats intégrés n'ont pas été activés pour cette compilation. Vérifiez-le avant d'afficher quoi que ce soit à la vente. |
AppMintIAP.sell(id, button, opts) | Relie un produit à un bouton. opts : label, owned, group, show, hide, labelEl. Appelez-la une fois par produit. |
AppMintIAP.start(config) | Relie plusieurs produits à la fois : {plans, show, hide, container, toast, hideOtherPlans}. Utilisez-la si vous voulez votre propre fonction de toast, ou pour garder visibles les autres formules d'un groupe après l'achat de l'une d'elles (hideOtherPlans: false). |
AppMintIAP.onChange(fn) | Se déclenche immédiatement avec l'état actuel, puis à chaque changement. Renvoie une fonction de désabonnement. |
AppMintIAP.state() | Le même objet, une seule fois : {available, adFree, owned, ownedIds, products}. |
AppMintIAP.isOwned(id) | Vrai/faux, instantanément, hors ligne. |
AppMintIAP.owned() | Tableau de tous les identifiants de produits acquis. |
AppMintIAP.buy(id) | Ouvre la fenêtre d'achat de Google. N'accorde rien par elle-même. |
AppMintIAP.refresh() | Réanalyse les boutons data-iap-buy ajoutés depuis le dernier passage. Rarement nécessaire : le runtime surveille le DOM. |
| Appel | Renvoie |
|---|---|
purchase(productId) | Rien. Ouvre la fenêtre ; le résultat arrive sous forme d'événement. |
getProducts(requestId, idsJson) | Rien. La réponse arrive sous forme de appmint:products contenant le même requestId. |
getOwnedProducts() | Tableau JSON des identifiants de produits acquis, sous forme de chaîne. |
isOwned(productId) | Booléen : un produit ponctuel acquis, ou un abonnement actuellement actif. |
isPremium() | Booléen : spécifiquement le droit « supprimer les publicités ». |
startRemoveAdsPurchase() | Rien. Ouvre la fenêtre pour le PREMIER identifiant du champ « Supprimer les publicités » de l'assistant ; posséder n'importe quel identifiant listé là supprime les publicités. Le résultat arrive sous forme de appmint:premium. |
| Événement | détail | Quand |
|---|---|---|
appmint:purchase | {productId, owned} | Google a confirmé. Le seul endroit où débloquer. Se redéclenche pour un produit déjà acquis. |
appmint:purchase-failed | {productId, reason} | La fenêtre ne s'est jamais ouverte, le déroulement a échoué, ou le paiement est en attente. Jamais déclenché pour une simple annulation. |
appmint:products | {requestId, products[]} | Réponse à un appel getProducts(). Les identifiants que Play ne connaît pas sont tout simplement absents. |
appmint:premium | {premium} | Le droit « sans publicités » a changé : achat, restauration au lancement, remboursement ou expiration. |
appmint:owned-changed | {products[]} | La restauration au lancement est terminée, ou un remboursement a retiré quelque chose. Contient tous les identifiants acquis. |
| Attribut | Activé | Fait |
|---|---|---|
data-iap-buy="id" | bouton | Lui fait acheter ce produit. |
data-iap-label="… {price}" | bouton | Modèle de libellé. {price} devient le prix local en direct de Google. |
data-iap-owned="…" | bouton | Libellé une fois le produit acquis. |
data-iap-group="name" | bouton | Marque des formules comme alternatives les unes aux autres. |
data-iap-show="id|group" | n'importe quoi | Affiché uniquement lorsque ce produit est acquis. |
data-iap-hide="id|group" | n'importe quoi | Masqué dès que ce produit est acquis. |
data-iap-text | dans un bouton | L'élément dont le texte reçoit le libellé, lorsque le bouton contient autre chose que du texte. |
data-iap-container | conteneur | Affiché uniquement dans l'application ; masqué dans un navigateur, où il n'y a rien à acheter. Placez toute l'offre dedans. |
data-iap-state="owned" | défini par le runtime | Sur le bouton d'un produit acquis, pour que vous puissiez le styliser. |
| raison | Ce qui s'est réellement passé |
|---|---|
not_found | Google ne connaît pas cet identifiant de produit : il n'a pas été créé, pas activé, ou l'application n'a pas été installée depuis le Play Store. |
billing_error | Play n'a pas pu démarrer le paiement. Généralement temporaire ; l'acheteur doit réessayer. |
pending | Un mode de paiement lent (espèces, virement). Le contenu se débloque tout seul quand Google confirme : ne le considérez pas comme un échec. |
disabled | Les achats intégrés n'ont pas été activés pour cette compilation. |
unknown | Autre chose. |
Avant de publier#
- Chaque identifiant de produit de votre code existe dans la Play Console et est activé ; pour un abonnement, le plan de base aussi.
- La compilation contient les mêmes identifiants, ainsi que la clé de licence RSA en Base64 issue de la configuration de la monétisation.
- Votre adresse Gmail figure dans Configuration → Test de licence, avec la réponse définie sur RESPOND_NORMALLY.
- Vous avez importé l'AAB en test interne ou fermé et l'avez installé depuis le lien du Play Store : un APK installé en sideload ne peut jamais rien acheter.
- Vous avez acheté chaque produit en tant que testeur de licence et vu « Instrument de test, toujours approuvé ».
- Vous avez désinstallé puis réinstallé, et l'achat est revenu tout seul.
- Vous avez coupé le réseau et le contenu payant est toujours débloqué.
- Vous avez remboursé un achat de test dans la Play Console et vu l'application se verrouiller à nouveau.
Erreurs qui coûtent de l'argent#
- Débloquer dans le gestionnaire de clic. Quiconque ouvre la fenêtre puis la ferme obtient le produit gratuitement.
- Conserver le droit d'accès uniquement dans
localStorage. Vider les données du site retire à un client ce qu'il a payé ; copier une valeur l'offre gratuitement. - Écrire le prix en dur dans votre HTML. Il sera faux dans tous les autres pays et périmé dès le jour où vous le changez. Utilisez
{price}ouproducts[id].price. - Vendre des biens physiques ou un service réel via la facturation Play. Google exige l'inverse : ceux-ci doivent passer par un processeur de paiement classique. C'est à cela que sert le guide Stripe.
- Tester sur un APK installé en sideload. Les achats intégrés ne fonctionnent que si l'application a été installée depuis le Play Store. Rien de ce que vous écrivez n'y change quoi que ce soit.
- Considérer
pendingcomme un échec. L'acheteur a payé avec un mode de paiement lent ; le contenu se débloque tout seul quand Google confirme.