Guadagnare
Vendi prodotti in-app (IAP)
Acquisti su Google Play dal tuo sito web o dalla tua app ZIP
1📥 Scarica la guida e l'app dimostrativa funzionante#
Tutto quello che c'è in questa guida è disponibile anche come PDF da leggere offline, più un'app dimostrativa completa che puoi compilare ed eseguire subito.
Tocca qui sotto ed entrambi vengono salvati sul telefono in Download/AppMint/Guides:
- AppMint-InApp-Purchases-Guide.pdf - la guida completa, con i passaggi della Play Console e gli esempi per React / Next.js
- iap-demo-index.html - un piccolo gioco che vende tre cose: rimuovi pubblicità (una tantum), un pacchetto di livelli (una tantum) e un abbonamento stagionale mensile
Per provare la demo: comprimi in uno ZIP iap-demo-index.html da solo, caricalo nello stepper e segui la guida. È il modo più veloce per vedere un vero acquisto funzionare da capo a fondo prima di toccare la tua app.
Nell'app, il pulsante di questo passaggio li salva sul tuo telefono. Qui sono download diretti.
AppMint-InApp-Purchases-Guide.pdf iap-demo-index.html2Cos'è? (in parole semplici)#
La tua app può vendere cose (un upgrade Pro, livelli extra, un abbonamento) e Google incassa i soldi e te li versa ogni mese.
COSTRUIREMO INSIEME UN ESEMPIO, per tutta la durata di questa guida:
A game that sells two things
• 'Levels 11-20' ₹99 paid once, kept forever
• 'Remove ads' ₹49 every month
Alla fine aggiungerai quattro righe alla tua pagina. Questo è tutto il lavoro: non c'è codice di acquisto da scrivere e, anche se non l'hai mai fatto prima, ce la farai senza problemi.
NON serve saper programmare oltre al copia-incolla e NON serve un server, un database o un account di una società di pagamenti. Google Play si occupa delle parti difficili:
- Mostra la schermata di pagamento (carta / UPI / fatturazione dell'operatore)
- Converte il tuo prezzo nella valuta di ogni paese
- Ricorda chi ha acquistato cosa, anche dopo una reinstallazione
- Invia i soldi alla tua banca
AppMint prende lo 0%: la vendita va direttamente sul TUO account Google Play.
Funziona allo stesso modo per Website → APK, ZIP → APK e per le app create con l'AI.
3Cosa ti serve prima di iniziare#
Solo quattro cose:
- Il tuo sito web o la tua app HTML/ZIP (quello che trasformi in APK).
- Qualcosa da vendere: decidi cosa ottengono gli acquirenti (ad es. 'Versione Pro': nessun limite, contenuti extra).
- Un account sviluppatore Google Play: una commissione una tantum di 25 $ su play.google.com/console. Serve per pubblicare QUALSIASI app sul Play Store, non solo quelle a pagamento.
- Circa 1 ora in totale, distribuita sui passaggi qui sotto.
Da sapere subito: gli acquisti in-app funzionano solo quando la tua app è installata DAL Play Store. È una regola di Google per tutti, non un limite di AppMint.
4Passaggio 1 - Inventa il tuo ID prodotto#
Un ID prodotto è solo un nome breve che inventi per ciò che vendi. Scriverai questo stesso nome in due posti più avanti (la tua pagina e la Play Console), quindi mantienilo semplice e annotalo.
Regole: solo lettere minuscole, numeri e trattini bassi. Deve iniziare con una lettera o un numero.
Copiane una:
- pro_unlock - upgrade 'Versione Pro' (buona scelta predefinita: il codice qui sotto la usa già)
- remove_ads - upgrade senza pubblicità
- level_pack_2 - pacchetto di contenuti extra
- coins_100 - pacchetto di valuta di gioco
- premium_monthly - abbonamento mensile
Il prezzo NON fa parte dell'ID: lo imposterai nel Passaggio 5 e potrai cambiarlo in qualsiasi momento senza toccare il codice.
5Passaggio 2 - Etichetta il tuo pulsante (nessun codice da scrivere)#
NON scrivi alcun codice di acquisto. La tua app contiene già il motore degli acquisti: ti basta etichettare il tuo HTML perché sappia cosa vendere.
Copia il blocco qui sotto nella tua pagina e cambia soltanto:
(1) pro_unlock → il tuo ID prodotto, in data-iap-buy
(2) le parole sul pulsante e dentro la sezione a pagamento
Ecco fatto. Il pulsante mostra poi il prezzo reale di Google nella valuta dell'acquirente, apre la schermata di pagamento di Google al tocco, mostra la sezione a pagamento dopo il pagamento (e di nuovo a ogni avvio per chi ha già acquistato), la toglie se un abbonamento scade o un pagamento viene rimborsato, e mostra un messaggio chiaro se qualcosa va storto.
LE QUATTRO ETICHETTE:
- data-iap-buy="id" - lo rende il pulsante di acquisto (obbligatorio)
- data-iap-label="…{price}…" - testo del pulsante; {price} diventa il prezzo reale di Google
- data-iap-owned="…" - testo del pulsante dopo l'acquisto
- data-iap-show - una sezione che compare solo dopo l'acquisto (parti da 'hidden')
Disponibili anche: data-iap-hide (scompare dopo l'acquisto), data-iap-container (un contenitore che compare solo dentro l'app), data-iap-text (mettilo sullo <span> che contiene il testo se il pulsante ha anche delle icone).
VENDI PIÙ COSE DIVERSE? Aggiungi un pulsante per ciascuna. Sono indipendenti: un'app può rimuovere le pubblicità E vendere pacchetti di livelli E gestire un abbonamento, e acquistarne uno non influisce mai sugli altri. Collega una sezione a un prodotto con data-iap-show="level_pack_2".
Solo gli ID che scrivi in 'Rimuovi pubblicità (IAP) → ID prodotto' spengono le pubblicità. Tutto il resto che vendi sblocca ciò che la tua pagina decide di farne, e le pubblicità continuano a girare.
DUE PIANI PER LA STESSA COSA (mensile contro a vita)? Dai a entrambi i pulsanti lo stesso data-iap-group="adfree": così acquistarne uno nasconde l'altro e nessuno paga due volte. Senza gruppo non viene nascosto nulla, come serve ai prodotti indipendenti.
<!-- (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>6Le 4 cose che le persone vendono di solito#
Scegli quello che fa per te. L'unica differenza tra i due sta nel punto in cui crei il prodotto nella Play Console: il codice della tua pagina ha sempre la stessa forma.
- RIMUOVI PUBBLICITÀ, PAGATO UNA VOLTA (per sempre)
Crealo in: Prodotti in-app
Scrivi l'ID anche in: Rimuovi pubblicità (IAP) → ID prodotto
- RIMUOVI PUBBLICITÀ, MENSILE
Crealo in: Abbonamenti
Scrivi l'ID anche in: Rimuovi pubblicità (IAP) → ID prodotto
- SBLOCCA LIVELLI / CONTENUTI, PAGATO UNA VOLTA
Crealo in: Prodotti in-app
NON scriverlo nel campo Rimuovi pubblicità: le tue pubblicità continuerebbero a girare.
- SBLOCCA LIVELLI / CONTENUTI, MENSILE
Crealo in: Abbonamenti
NON scriverlo nel campo Rimuovi pubblicità: le tue pubblicità continuerebbero a girare.
Puoi vendere TUTTI E QUATTRO nella stessa app, e tutti i prodotti che vuoi. Acquistarne uno non influisce mai sugli altri.
<!-- 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>7Quali acquisti rimuovono le pubblicità?#
È l'unica cosa in cui le persone sbagliano, quindi vale 30 secondi.
Le tue pubblicità NON fanno parte della tua pagina web. Sono vere pubblicità Android posate sopra di essa, quindi nulla nella tua pagina può spegnerle. Solo UNA cosa può farlo:
wizard Step 3 → Remove Ads (IAP) → Product ID
Qualunque ID tu scriva in quel campo rimuove le pubblicità quando qualcuno lo acquista. Ogni altro ID che vendi sblocca semplicemente ciò che la tua pagina decide di farne, e le pubblicità continuano.
Vendi sia un piano senza pubblicità SIA pacchetti di livelli? Scrivi in quel campo solo l'ID del piano senza pubblicità. Nel nostro esempio:
Product ID box: remove_ads_monthly
NOT in the box: level_pack_2
Offri due piani senza pubblicità (mensile E a vita)? Mettili entrambi in quel campo, separati da una virgola: possedere uno qualsiasi dei due rimuove le pubblicità:
remove_ads_monthly,remove_ads_lifetime
Non vendi affatto la rimozione delle pubblicità? Lascia quell'interruttore del tutto spento e usa solo '💰 Vendi prodotti in-app'.
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)8Esempio completo - pagina HTML semplice#
Ecco la nostra app di esempio completa: vende 'Livelli 11-20' una volta sola e 'Rimuovi pubblicità' ogni mese. Copiala, cambia i due ID e le parole, e hai finito.
Leggi cosa fa ogni etichetta:
- data-iap-buy → cosa vende questo pulsante
- data-iap-label → il testo; {price} diventa il prezzo reale di Google
- data-iap-owned → il testo dopo che hanno pagato
- data-iap-show="level_pack_2" → questo riquadro compare solo dopo l'acquisto DI QUEL prodotto
- data-iap-hide → questo riquadro scompare quando viene acquistato qualsiasi prodotto
Nota che non c'è nessun <script> e nessun codice di acquisto. La tua app lo contiene già.
<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>9Esempio completo - React o Next.js#
IL MODO FACILE: le stesse identiche etichette funzionano in React e Next.js, perché sono solo markup. Niente useState, niente useEffect, niente 'use client'. I pulsanti che React aggiunge più tardi vengono rilevati automaticamente.
È tutto ciò che serve alla maggior parte delle app: usa il primo blocco qui sotto e fermati lì.
IL MODO CONTROLLATO (secondo blocco): solo se vuoi che sia React stesso a mostrare il prezzo e a cambiare schermata. In tal caso:
- In Next.js metti 'use client' alla riga 1 del file, altrimenti la build fallisce.
- Usa window.AppMintIAP solo dentro useEffect: nel corpo del componente viene eseguito sul server, dove window non esiste.
- onChange(fn) ti consegna lo stato subito e di nuovo a ogni cambiamento, e restituisce la funzione per annullare l'iscrizione che React si aspetta.
- In Next.js, compila come esportazione statica: output: 'export' in 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>;
}10Controllo rapido nel tuo browser#
Apri la tua pagina in un normale browser (Chrome sul PC o sul telefono).
Dovresti vedere il pulsante di acquisto lì fermo senza fare nulla, e la tua sezione a pagamento ancora nascosta. Entrambe le cose sono corrette: fuori dall'app non c'è nulla da acquistare, quindi niente può essere acquistato per sbaglio.
Dentro l'app è una pagina del tutto diversa: il pulsante mostra il prezzo reale, apre la schermata di pagamento di Google e si nasconde da solo quando l'acquirente possiede il prodotto. Il tuo sito continua a funzionare normalmente per i visitatori comuni in entrambi i casi.
Vuoi che l'offerta sparisca del tutto sul web? Racchiudila in un <div data-iap-container hidden>: quel contenitore viene mostrato solo dentro l'app.
11Passaggio 3 - Attiva gli acquisti nello STEPPER#
⚠️ NON SALTARE QUESTO PASSAGGIO. Se lo salti i pulsanti compaiono comunque, ma ogni tocco risponde 'Gli acquisti in-app non sono abilitati in questa app.' e non incassi nulla.
Lo STEPPER è la sequenza di schermate Passaggio 1 → Passaggio 2 → Passaggio 3 che usi per creare la tua app. Gli acquisti si trovano nel PASSAGGIO 3 DELLO STEPPER.
FALLO IN ORDINE:
- Apri lo stepper e vai al PASSAGGIO 3 DELLO STEPPER (la schermata con Pubblicità, Notifiche e Autorizzazioni).
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)12Passaggio 4 - Metti la tua app su Google Play#
Nella tua Play Console (il sito dove gli sviluppatori gestiscono le loro app):
- 'Crea app' → compila il nome e le domande di base.
- Nel menu a sinistra: 'Test' → 'Test interni' → 'Crea nuova release' → carica il tuo file AAB → 'Salva' e distribuisci.
- Sempre in Test interni → scheda 'Tester' → aggiungi il tuo Gmail → copia il link di 'adesione' per dopo.
I test interni sono un'area privata: nessuno può ancora vedere la tua app. Esiste perché TU possa provare gli acquisti prima di andare pubblico.
13Passaggio 5 - Crea il tuo prodotto e imposta il prezzo#
Ora dì a Google cosa stai vendendo. Il menu da usare dipende dal fatto che si paghi una volta sola o ogni mese.
PAGATO UNA VOLTA (livelli, Pro, monete, rimuovi pubblicità per sempre)
- Play Console → la tua app → 'Monetizza' → 'Prodotti' → 'Prodotti in-app'.
- 'Crea prodotto'.
- ID prodotto: scrivi ESATTAMENTE l'ID della tua pagina - level_pack_2. Stessa ortografia, stesse maiuscole. Google non ti permette mai di cambiarlo o riutilizzarlo.
- Nome: quello che vedono gli acquirenti, ad es. 'Livelli 11-20'. Descrizione: una riga.
- Imposta il prezzo nella tua valuta: Google lo converte per ogni paese.
- Premi 'Attiva'.
OGNI MESE (rimuovi pubblicità mensile, abbonamento a tutti i livelli)
- Play Console → 'Monetizza' → 'Prodotti' → 'Abbonamenti'.
- 'Crea abbonamento'. ID prodotto: remove_ads_monthly.
- Salva, poi aggiungi un PIANO BASE: tipo 'Rinnovo automatico', periodo di fatturazione 'Mensile', il tuo prezzo.
- Premi 'Attiva' sul piano base E sull'abbonamento stesso. Se salti uno dei due, la tua app non riesce a vederlo.
Ripeti per ogni ID che hai usato. Il codice della tua pagina non cambia: l'app chiede a Google di che tipo è ogni ID.
VUOI MENSILE *E* ANNUALE? Crea DUE abbonamenti separati (all_levels_monthly e all_levels_yearly) invece di due piani base dentro un solo abbonamento. Un abbonamento con più piani base offre sempre il primo, senza modo di scegliere.
Cambia qualsiasi prezzo più tardi nella Play Console: la tua app mostra da sola il nuovo prezzo, senza bisogno di una nuova build.
14Passaggio 6 - Provalo (non ti verrà addebitato nulla)#
- Play Console → menu a sinistra 'Configurazione' → 'Test di licenza' → aggiungi il tuo Gmail all'elenco dei tester.
- Sul telefono: apri il link di adesione ai test interni del Passaggio 4, accetta e installa la tua app DAL Play Store.
- Apri l'app. Il tuo pulsante di acquisto compare con il prezzo reale.
- Toccalo: si apre la schermata di pagamento di Google con la scritta 'Carta di prova, approva sempre'. Acquista. Compare il tuo contenuto pro. Non paghi nulla.
- Controllo bonus: disinstalla e reinstalla l'app: il contenuto pro si sblocca da solo all'avvio. È Google che ripristina l'acquisto.
⚠️ Se installi l'APK direttamente (sideload) invece che dal Play Store, il pulsante di acquisto NON comparirà. È normale: prova sempre tramite il link del Play Store.
15Passaggio 7 - Vai online e fatti pagare#
- Quando i test vanno bene: Play Console → 'Produzione' → crea una release con lo stesso AAB → distribuisci. (Le app nuove passano dalla revisione di Google, di solito qualche giorno.)
- Gli utenti acquistano dentro la tua app → Google incassa i soldi.
- Google paga il tuo conto bancario ogni mese (Play Console → 'Profilo dei pagamenti'). Google trattiene la sua commissione standard (15% per la maggior parte degli sviluppatori). AppMint non trattiene nulla.
Per aggiungere altri prodotti più tardi: aggiungi il nuovo ID nel codice della tua pagina, compila di nuovo una volta e crea l'ID nella Play Console. Le modifiche di prezzo non richiedono mai una nuova build.
16Se qualcosa non funziona#
La tua app ti dice cosa è andato storto invece di fallire in silenzio. Trova il messaggio che vedi:
'Gli acquisti in-app non sono abilitati in questa app.'
→ '💰 Vendi prodotti in-app' era DISATTIVATO nel PASSAGGIO 3 DELLO STEPPER quando hai creato la build. Attivalo e compila di nuovo.
'Questo articolo non è ancora disponibile su Google Play.'
→ Google non riconosce quell'ID. Controllalo lettera per lettera con la Play Console, verifica che il prodotto sia ATTIVO (per un abbonamento, sia il piano base SIA l'abbonamento devono essere attivati) e assicurati di aver installato l'app dal link dei tester di Play.
'Google Play non è riuscito ad avviare il pagamento.'
→ L'app non è stata installata dal Play Store, oppure è stata firmata con una chiave diversa da quella che hai caricato.
'Pagamento in sospeso: si sbloccherà automaticamente appena Google conferma.'
→ Non devi fare nulla. I metodi di pagamento lenti (buoni in contanti, alcuni UPI) richiedono qualche minuto; lo sblocco arriva da solo, anche dopo aver riavviato l'app.
NESSUN MESSAGGIO, non succede nulla al tocco
→ Quasi sempre un errore di battitura in data-iap-buy, oppure l'app non è installata da Play. Ogni altra causa mostra uno dei messaggi qui sopra.
IL PAGAMENTO HA FUNZIONATO MA LE PUBBLICITÀ SONO ANCORA LÌ
→ Quell'ID prodotto non è nel PASSAGGIO 3 DELLO STEPPER → 'Rimuovi pubblicità (IAP)' → 'ID prodotto Play Store'. Aggiungilo (separa più ID con virgole) e compila di nuovo.
IL PULSANTE DI ACQUISTO NON COMPARE MAI NELL'APP
→ '💰 Vendi prodotti in-app' era disattivato al momento della build. In un normale browser web i pulsanti sono nascosti APPOSTA: fuori dall'app non c'è nulla da acquistare.
NON È SUCCESSO NULLA DOPO AVER CHIUSO LA SCHERMATA DI PAGAMENTO DI GOOGLE
→ È un normale annullamento. Tocca di nuovo e completa il pagamento.
IL PREZZO SEMBRA VECCHIO
→ Play conserva i prezzi in cache su un dispositivo per alcune ore. Una nuova installazione mostra sempre il prezzo aggiornato.
17React, Next.js o JavaScript semplice#
REACT E NEXT.JS NON RICHIEDONO NULLA DI SPECIALE. Le etichette qui sopra sono solo markup, quindi la stessa riga funziona anche lì: niente useState, niente useEffect, niente 'use client':
<button data-iap-buy="pro_unlock"
data-iap-label="Unlock Pro — {price}">Unlock Pro</button>
I pulsanti che React monta più tardi vengono trovati automaticamente e rimontarli è sicuro. In Next.js, compila come esportazione statica (output: 'export').
Preferisci il codice alle etichette? Una riga fa lo stesso lavoro:
AppMintIAP.sell('pro_unlock', '#buy-btn');
Terzo argomento facoltativo: {label:'Sblocca Pro - {price}', owned:'✓ Sbloccato', show:'#thanks', hide:'.upsell'}. Chiamalo una volta per prodotto.
SOLO SE VUOI CHE SIA REACT A MOSTRARE IL PREZZO: altrimenti salta questo passaggio:
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; // usa sempre l'optional chaining
if (!iap.available) return null; // niente da acquistare fuori dall'app
if (pro?.owned) return <p>Grazie!</p>;
return <button onClick={() => window.AppMintIAP.buy('pro_unlock')}>
Unlock Pro {pro?.price}
</button>;
DUE ABITUDINI EVITANO CHE TUTTO SI ROMPA:
- Leggi il possesso PER PRODOTTO - iap.products?.<id>?.owned. Il iap.owned di primo livello significa 'qualcosa è posseduto' e cambia significato il giorno in cui vendi un secondo prodotto.
- Usa l'optional chaining su tutto il percorso, così un campo imprevisto non può mai generare un errore mentre React sta disegnando.
onChange ti consegna {available, adFree, owned, ownedIds, products} subito e a ogni cambiamento, e restituisce una funzione per annullare l'iscrizione: così chi torna ad acquistare è già sbloccato al primo rendering. In Next.js aggiungi 'use client' come riga 1 e compila con output: 'export'.
Inoltre: AppMintIAP.available, .isOwned(id), .owned(), .buy(id), .state(), .refresh(). Le etichette data-iap-buy funzionano anche in JSX, e i pulsanti che React monta più tardi vengono rilevati automaticamente.
Preferisci usare tu stesso il bridge diretto? Tutto quanto sopra si basa su questi:
- iap.isOwned(id) - vero/falso immediato, funziona offline
- iap.getOwnedProducts() - array JSON di tutti gli ID posseduti
- iap.getProducts(reqId, idsJson) - prezzi in tempo reale; il risultato arriva come evento
'appmint:products'{requestId, products:[{productId, title, price, currency, type, owned}]} - iap.purchase(id) - prodotti una tantum E abbonamenti (rilevati automaticamente)
- Eventi:
'appmint:purchase'{productId} (concedi SOLO qui: un tocco su qualcosa già posseduto lo fa scattare di nuovo),'appmint:owned-changed'{products} (il ripristino all'avvio è terminato, o un rimborso ha tolto qualcosa: rileggi i possessi),'appmint:purchase-failed'{reason: disabled | not_found | billing_error | pending}
Vendi solo un semplice upgrade 'rimuovi pubblicità'? Usa invece la coppia integrata, più semplice: abilita 'Rimuovi pubblicità (IAP)' nel Passaggio 3 della procedura guidata:
var iap = window.WebToApk;
if (iap && !iap.isPremium()) {
showRemoveAdsButton(function onTap() {
iap.startRemoveAdsPurchase();
});
}
window.addEventListener('appmint:premium', function (e) {
if (e.detail.premium) hideRemoveAdsButton();
});Rimuovi pubblicità (IAP)
Configurazione della fatturazione di Google Play
1Crea un prodotto gestito#
Nella dashboard della tua Google Play Console:
- Vai su 'Monetizzazione' > 'Prodotti in-app'
- Clicca su 'Crea prodotto'
- Inserisci un ID prodotto (ad es. 'remove_ads_premium')
- Imposta un Nome (ad es. 'Rimuovi tutte le pubblicità') e una Descrizione
- Imposta il prezzo e clicca su 'Salva'
- Clicca su 'Attiva' per renderlo disponibile ai test
Vendi la rimozione delle pubblicità come ABBONAMENTO? Crealo in 'Monetizzazione' > 'Abbonamenti' con un piano base a rinnovo automatico e attiva SIA il piano base SIA l'abbonamento. L'assenza di pubblicità dura finché l'abbonamento è attivo, e le pubblicità tornano automaticamente alla scadenza.
2Ottieni la tua chiave di licenza#
Google Play richiede una chiave pubblica RSA per verificare gli acquisti in modo sicuro.
Nella Play Console:
- Vai su 'Monetizzazione' > 'Configurazione della monetizzazione'
- In 'Licenze', copia la 'Chiave pubblica RSA codificata in Base64'
- ⚠️ IMPORTANTE: dovrai incollarla nel Generatore più avanti.
Example Key Format:
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA...3Configurazione nel generatore WebToApk#
Nell'app del generatore:
- Abilita l'interruttore 'Rimuovi pubblicità (IAP)' nel Passaggio 3
- Inserisci ESATTAMENTE lo stesso 'ID prodotto' che hai creato nel Passaggio 1
- Incolla la tua 'Chiave di licenza Base64' del Passaggio 2
- Genera il tuo APK/AAB
Offri più di un piano senza pubblicità (ad es. un abbonamento mensile E uno sblocco a vita una tantum)? Metti ogni ID prodotto in questo unico campo, separato da virgole: possedere QUALSIASI di essi rimuove le pubblicità. Il primo ID elencato è quello che acquista la voce di menu integrata 'Rimuovi pubblicità', quindi metti per primo il tuo piano predefinito.
Product ID: remove_ads_premium
Two plans:
remove_ads_monthly,remove_ads_lifetime4Configura i tester di licenza#
Per provare gli acquisti senza spendere soldi veri:
- Nella Play Console, vai su 'Configurazione' > 'Test di licenza'
- Aggiungi il tuo indirizzo Gmail all'elenco dei 'Tester di licenza'
- Assicurati che 'Risposta di licenza' sia impostata su 'RESPOND_NORMALLY'
5Prova nel canale interno o chiuso#
Gli acquisti in-app funzionano SOLO quando l'app è installata tramite il Play Store (canali di test):
- Carica il tuo AAB generato nel canale 'Test interni' o 'Test chiusi'
- Condividi il link di adesione per tester con i tuoi account di prova
- Scarica l'app dal Play Store
- Tocca il pulsante 'Rimuovi pubblicità' nella tua app per provare il flusso!
Note: Purchases will show a 'Test instrument, always approves' message for license testers.Ricette di codice
I passaggi qui sopra coprono le basi: le quattro etichette, una pagina semplice completa, la riga unica per React. Queste ricette coprono ciò che quelli non coprono: più piani che si sostituiscono a vicenda, abbonamenti che scadono, hook dei framework, il bridge diretto al completo e le tabelle di riferimento.
L'unica regola#
Un acquisto viene concesso solo quando Google lo conferma. Non quando si clicca il pulsante, non quando si apre la schermata, non quando l'utente torna alla tua pagina. Tutto quello che segue è un modo diverso di esprimere questa unica regola.
Il runtime lo impone già: AppMintIAP rilegge i possessi dal bridge all'avvio, a ogni acquisto, a ogni rimborso e ogni volta che l'app viene riaperta. Il possesso sopravvive a reinstallazioni, a un nuovo telefono e all'assenza di rete, perché vive nell'account Google dell'acquirente, non nella tua pagina.
Non sbloccare mai nel gestore del clic e non tenere mai il diritto solo in localStorage. Uno spazio di archiviazione del browser svuotato non deve costare a un cliente pagante ciò che ha acquistato, e un valore localStorage copiato non deve regalarlo gratis.
Ricetta 1 - una tabella dei prezzi con prezzi in tempo reale#
Tre piani in cui acquistarne uno qualsiasi sostituisce gli altri. group è ciò che li rende alternative: senza, sono prodotti indipendenti e acquistare il piano a vita lascerebbe lì il pulsante mensile.
<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 ID prodotto che Google non conosce (non ancora creato, non attivato, oppure l'app non è stata installata dal Play Store) resta semplicemente senza prezzo. Il suo pulsante resta toccabile di proposito: toccandolo viene segnalata la vera ragione invece di non fare nulla.
Ricetta 2 - React#
onChange scatta subito con lo stato attuale e di nuovo a ogni cambiamento, e restituisce la propria funzione per annullare l'iscrizione: è esattamente la forma che serve a 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>
);
}Il pulsante chiama buy(); non sblocca nulla. Lo sblocco avviene perché Google conferma, onChange scatta, owns('pro_unlock') diventa true e il componente viene ridisegnato. È la stessa regola, espressa in React.
Ricetta 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>Ricetta 4 - abbonamenti#
Un abbonamento si acquista esattamente come un prodotto una tantum; la differenza è che può terminare. type indica di che tipo è un prodotto e period fornisce l'intervallo di fatturazione come durata ISO-8601: P1M mensile, P1Y annuale, P1W settimanale.
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.Non devi fare polling e non ti serve una data di scadenza. Play ripristina i prodotti posseduti a ogni avvio, quindi un abbonamento scaduto semplicemente smette di comparire in ownedIds.
Attiva sia l'abbonamento sia il suo piano base nella Play Console. Un abbonamento con un piano base non attivo non restituisce alcun dettaglio e il pulsante segnalerà "non ancora disponibile su Google Play".
Ricetta 5 - il bridge diretto, senza helper#
Tutto quanto sopra si basa su sei metodi e cinque eventi. Se vuoi gestire interamente l'interfaccia, usali direttamente.
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 scatta di nuovo per qualcosa già posseduto, di proposito. Scrivi unlockPro() in modo che eseguirlo due volte sia innocuo e non debba mai tenere traccia di averlo già eseguito.
Ricetta 6 - degradare con onestà fuori dall'app#
La stessa pagina di solito deve aprirsi anche in un normale browser, durante lo sviluppo o perché la pubblichi anche sul web. Lì window.WebToApk non esiste e nessun acquisto può avvenire. Dillo chiaramente, invece di mostrare un pulsante che non fa nulla.
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;
}Riferimento#
Tutto ciò che la pagina può chiamare, ascoltare o marcare.
| Chiamata | Fa |
|---|---|
AppMintIAP.available | False in un browser, o quando gli acquisti in-app non erano attivati per questa build. Controllalo prima di mostrare qualsiasi cosa in vendita. |
AppMintIAP.sell(id, button, opts) | Collega un prodotto a un pulsante. opts: label, owned, group, show, hide, labelEl. Chiamalo una volta per prodotto. |
AppMintIAP.start(config) | Collega più prodotti insieme: {plans, show, hide, container, toast, hideOtherPlans}. Usalo quando vuoi la tua funzione toast, o per tenere visibili gli altri piani di un gruppo dopo che uno è stato acquistato (hideOtherPlans: false). |
AppMintIAP.onChange(fn) | Scatta subito con lo stato attuale, poi a ogni cambiamento. Restituisce una funzione per annullare l'iscrizione. |
AppMintIAP.state() | Lo stesso oggetto, una volta: {available, adFree, owned, ownedIds, products}. |
AppMintIAP.isOwned(id) | Vero/falso, subito, anche offline. |
AppMintIAP.owned() | Array con ogni ID prodotto posseduto. |
AppMintIAP.buy(id) | Apre la schermata di acquisto di Google. Da sola non concede nulla. |
AppMintIAP.refresh() | Riesamina i pulsanti data-iap-buy aggiunti dopo l'ultima scansione. Raramente serve: il runtime osserva il DOM. |
| Chiamata | Restituisce |
|---|---|
purchase(productId) | Nulla. Apre la schermata; l'esito arriva come evento. |
getProducts(requestId, idsJson) | Nulla. La risposta arriva come appmint:products con lo stesso requestId. |
getOwnedProducts() | Array JSON degli ID prodotto posseduti, come stringa. |
isOwned(productId) | Booleano: un prodotto una tantum posseduto, oppure un abbonamento attualmente attivo. |
isPremium() | Booleano: nello specifico il diritto "rimuovi pubblicità". |
startRemoveAdsPurchase() | Nulla. Apre la schermata per il PRIMO ID nel campo "Rimuovi pubblicità" della procedura guidata; possedere uno qualsiasi degli ID elencati lì rimuove le pubblicità. L'esito arriva come appmint:premium. |
| Evento | dettaglio | Quando |
|---|---|---|
appmint:purchase | {productId, owned} | Google ha confermato. L'unico punto in cui sbloccare. Scatta di nuovo per qualcosa già posseduto. |
appmint:purchase-failed | {productId, reason} | La schermata non si è mai aperta, il flusso è andato in errore o il pagamento è in sospeso. Mai scattato per un semplice annullamento. |
appmint:products | {requestId, products[]} | Risposta a una chiamata getProducts(). Gli ID che Play non conosce semplicemente non ci sono. |
appmint:premium | {premium} | Il diritto senza pubblicità è cambiato: acquisto, ripristino all'avvio, rimborso o scadenza. |
appmint:owned-changed | {products[]} | Il ripristino all'avvio è terminato, o un rimborso ha tolto qualcosa. Contiene ogni ID posseduto. |
| Attributo | Su | Fa |
|---|---|---|
data-iap-buy="id" | pulsante | Gli fa acquistare quel prodotto. |
data-iap-label="… {price}" | pulsante | Modello di etichetta. {price} diventa il prezzo localizzato in tempo reale di Google. |
data-iap-owned="…" | pulsante | Etichetta una volta che il prodotto è stato acquistato. |
data-iap-group="name" | pulsante | Indica i piani come alternative l'uno all'altro. |
data-iap-show="id|group" | qualsiasi cosa | Mostrato solo quando il prodotto è posseduto. |
data-iap-hide="id|group" | qualsiasi cosa | Nascosto quando il prodotto è posseduto. |
data-iap-text | dentro un pulsante | L'elemento il cui testo riceve l'etichetta, quando il pulsante contiene più del solo testo. |
data-iap-container | contenitore | Mostrato solo dentro l'app; nascosto in un browser, dove non c'è nulla da acquistare. Racchiudi tutta l'offerta al suo interno. |
data-iap-state="owned" | impostato dal runtime | Sul pulsante di un prodotto posseduto, così puoi dargli uno stile. |
| motivo | Cosa è successo davvero |
|---|---|
not_found | Google non conosce questo ID prodotto: non è stato creato, non è stato attivato, oppure l'app non è stata installata dal Play Store. |
billing_error | Play non è riuscito ad avviare il pagamento. Di solito è temporaneo; l'acquirente dovrebbe riprovare. |
pending | Un metodo di pagamento lento (contanti, bonifico). Si sblocca da solo quando Google conferma: non trattarlo come un errore. |
disabled | Gli acquisti in-app non erano attivati per questa build. |
unknown | Qualsiasi altra cosa. |
Prima di pubblicare#
- Ogni ID prodotto nel tuo codice esiste nella Play Console ed è attivato; per un abbonamento, anche il piano base.
- La build contiene gli stessi ID e la chiave di licenza RSA in Base64 della configurazione della monetizzazione.
- Il tuo indirizzo Gmail è in Configurazione → Test di licenza, con la risposta impostata su RESPOND_NORMALLY.
- Hai caricato l'AAB nei test interni o chiusi e l'hai installato dal link del Play Store: un APK installato in sideload non può mai acquistare nulla.
- Hai acquistato ogni prodotto come tester di licenza e hai visto "Strumento di prova, approva sempre".
- Hai disinstallato e reinstallato, e l'acquisto è tornato da solo.
- Hai spento la rete e il contenuto a pagamento è ancora sbloccato.
- Hai rimborsato un acquisto di prova nella Play Console e hai visto l'app bloccarsi di nuovo.
Errori che costano denaro#
- Sbloccare nel gestore del clic. Chiunque apra la schermata e poi esca ottiene il prodotto gratis.
- Tenere il diritto solo in
localStorage. Cancellare i dati del sito toglie qualcosa che un cliente ha pagato; copiare un valore lo regala. - Scrivere il prezzo fisso nel tuo HTML. Sarà sbagliato in ogni altro paese e obsoleto il giorno in cui lo cambi. Usa
{price}oppureproducts[id].price. - Vendere beni fisici o un servizio del mondo reale tramite la fatturazione di Play. Google richiede l'opposto: questi devono usare un normale sistema di pagamento. È per questo che esiste la guida a Stripe.
- Provare con un APK installato in sideload. Gli acquisti in-app funzionano solo quando l'app è stata installata dal Play Store. Nulla di ciò che scrivi può cambiarlo.
- Trattare
pendingcome un errore. L'acquirente ha pagato con un metodo lento; si sblocca da solo quando Google conferma.