Perché gli acquisti in-app
Monetizzare un'app mobile passa spesso dagli acquisti in-app (IAP): sblocco di funzionalità premium, contenuti extra o abbonamenti ricorrenti. Flutter offre il pacchetto ufficiale in_app_purchase, che astrae le API di StoreKit (iOS) e Google Play Billing (Android) dietro un'interfaccia unificata.
In questa guida vediamo come configurare i prodotti, caricarli, avviare un acquisto e verificarlo lato client.
Tipi di prodotto
- Consumabili: acquistabili più volte (es. monete di gioco).
- Non consumabili: acquisto una tantum (es. rimozione pubblicità).
- Abbonamenti: rinnovo automatico ricorrente.
Configurazione
Aggiungi la dipendenza:
dependencies:
in_app_purchase: ^3.2.0
Su Android non servono modifiche particolari al AndroidManifest.xml per le versioni recenti del plugin. Su iOS assicurati di abilitare la capability In-App Purchase in Xcode.
I prodotti vanno prima creati nelle rispettive console:
- Google Play Console → sezione Prodotti in-app / Abbonamenti.
- App Store Connect → sezione In-App Purchases.
Usa gli stessi identificatori di prodotto (productId) su entrambe le piattaforme per semplificare il codice.
Verificare la disponibilità dello store
import 'package:in_app_purchase/in_app_purchase.dart';
final InAppPurchase _iap = InAppPurchase.instance;
Future<bool> isStoreAvailable() async {
return _iap.isAvailable();
}
Caricare i prodotti
const Set<String> _kIds = {'remove_ads', 'premium_monthly'};
Future<List<ProductDetails>> loadProducts() async {
final ProductDetailsResponse response =
await _iap.queryProductDetails(_kIds);
if (response.notFoundIDs.isNotEmpty) {
debugPrint('ID non trovati: ${response.notFoundIDs}');
}
return response.productDetails;
}
Ogni ProductDetails contiene id, title, description e price (già formattato nella valuta locale dello store).
Ascoltare gli aggiornamenti degli acquisti
Il flusso di acquisto è asincrono: dobbiamo iscriverci allo stream purchaseStream prima di avviare qualsiasi transazione, tipicamente all'avvio dell'app.
late final StreamSubscription<List<PurchaseDetails>> _subscription;
void initPurchaseListener() {
_subscription = _iap.purchaseStream.listen(
_onPurchaseUpdate,
onDone: () => _subscription.cancel(),
onError: (error) => debugPrint('Errore acquisto: $error'),
);
}
Future<void> _onPurchaseUpdate(List<PurchaseDetails> purchases) async {
for (final purchase in purchases) {
switch (purchase.status) {
case PurchaseStatus.pending:
_showPending();
break;
case PurchaseStatus.purchased:
case PurchaseStatus.restored:
await _verifyAndDeliver(purchase);
break;
case PurchaseStatus.error:
debugPrint('Errore: ${purchase.error}');
break;
case PurchaseStatus.canceled:
debugPrint('Acquisto annullato');
break;
}
// Fondamentale: completare la transazione
if (purchase.pendingCompletePurchase) {
await _iap.completePurchase(purchase);
}
}
}
⚠️ Chiamare
completePurchaseè obbligatorio: se non lo fai, lo store considererà la transazione non consegnata e la ripresenterà a ogni avvio.
Avviare un acquisto
Future<void> buy(ProductDetails product, {required bool isConsumable}) async {
final PurchaseParam param = PurchaseParam(productDetails: product);
if (isConsumable) {
await _iap.buyConsumable(purchaseParam: param);
} else {
await _iap.buyNonConsumable(purchaseParam: param);
}
}
Gli abbonamenti si acquistano con buyNonConsumable.
Verifica lato server
La validazione fatta solo sul client è vulnerabile. La verifica corretta prevede l'invio del purchase.verificationData.serverVerificationData a un backend che interroga le API di Google/Apple.
Future<void> _verifyAndDeliver(PurchaseDetails purchase) async {
final token = purchase.verificationData.serverVerificationData;
final valid = await backend.verify(purchase.productID, token);
if (valid) {
// Sblocca il contenuto e persisti lo stato
await _deliverProduct(purchase.productID);
}
}
Ripristinare gli acquisti
Apple richiede un pulsante "Ripristina acquisti" per i prodotti non consumabili e gli abbonamenti:
Future<void> restorePurchases() async {
await _iap.restorePurchases();
}
Gli acquisti ripristinati arriveranno sul purchaseStream con stato restored.
Best practice
- Verifica sempre lato server per prevenire frodi.
- Non fidarti del client per lo sblocco di contenuti di valore.
- Testa con account sandbox (App Store) e testers con licenza (Play Console).
- Ricordati di annullare la subscription allo stream nel
dispose. - Per gli abbonamenti usa librerie come RevenueCat se vuoi gestione multipiattaforma e webhook già pronti.
Conclusione
Il pacchetto in_app_purchase copre bene i casi standard di monetizzazione mantenendo un'unica API cross-platform. La parte più delicata resta la verifica lato server: non trascurarla se vendi contenuti di valore. Per progetti complessi con molti abbonamenti, valuta soluzioni gestite che riducono il boilerplate e centralizzano l'analisi dei ricavi.
