Nel primo semestre 2024 Miriade ha sviluppato una piattaforma web per l’analisi di dati in uno specifico ambito di applicazione. La prima versione di questa applicazione, disponibile per l’accesso via web, comprendeva la possibilità di acquistare pacchetti o abbonamenti, per sbloccare speciali funzionalità, attraverso la popolare piattaforma di pagamenti Stripe. Stripe offre, infatti, una serie di funzionalità molto interessanti sia per gli sviluppatori, sia sul versante della gestione delle fatture e delle ricevute verso gli utenti finali. Permette, inoltre, di integrarsi ai suoi servizi attraverso API Rest.
Nella seconda parte del 2024, quando Miriade ha iniziato a sviluppare la versione mobile della piattaforma, sperava di poter utilizzare Stripe, di fatto riusando gran parte del codice già scritto e implementando solamente una parte nuova di interfaccia grafica, per la pagina di acquisto dei pacchetti.
Questa speranza si è infranta durante la fase di approvazione delle app da parte di Google ed Apple, che hanno richiesto espressamente di utilizzare i framework nativi per gli acquisti in-app all’interno delle rispettive applicazioni mobile.
Come tutti ben sanno, gli acquisti in-app rappresentano una strategia essenziale per monetizzare le applicazioni mobile, tuttavia questa scelta ci ha forzato a dover integrare diverse piattaforme di pagamento per la gestione dei vari scenari (Web, Google Play, App Store).
In Miriade utilizziamo da anni Flutter, il popolare framework di sviluppo cross-platform di Google, sfruttando la sua versatilità per creare applicazioni robuste e performanti. Tra le altre, sono disponibili per questa piattaforma una serie di librerie per gestire gli acquisti digitali in modo efficiente.
In questo articolo, esploreremo le sfide legate all'implementazione degli acquisti in-app e forniremo una guida dettagliata basata sulle best practice e sulla nostra esperienza specifica.
Il Contesto
Molte applicazioni mobile utilizzano gli acquisti in-app per offrire contenuti premium, abbonamenti o funzioni esclusive. Tuttavia, implementare questa funzionalità in modo sicuro e conforme alle policy degli store (Google Play e App Store) richiede attenzione. Problemi comuni includono difficoltà nell’integrazione con i servizi di pagamento, la gestione delle transazioni fallite e il mantenimento degli stati di acquisto degli utenti.
Inoltre, nel nostro caso specifico, era richiesta l’integrazione con l’esistente logica di acquisto dei pacchetti via web, per non stravolgere il flusso esistente e non aumentare troppo l’effort di implementazione.
Le componenti interessate erano queste:
- l’integrazione esistente di Stripe, nella quale era già configurata una serie di pacchetti e prodotti da poter acquistare, con relative descrizioni e caratteristiche.
- Il nostro backend applicativo, scritto in Java/Spring, nel quale era già presente l’integrazione per
- recuperare i pacchetti disponibili di Stripe;
- effettuare validazione degli acquisti;
- associare gli acquisti effettuati a particolari ruoli utenti, a scadenza alla fine della validità dell’abbonamento, per permettere di accedere a funzionalità aggiuntive;
- gestire la logica per la cancellazione o la revoca degli acquisti.
- Il nostro frontend applicativo, scritto in Angular, nella quale era già presente tutta la parte di acquisti e verifica delle “licenze” attive. Questa parte non è strettamente coinvolta nell’attività ma si chiedeva di non stravolgerla.
- La nostra app mobile, sviluppata in Flutter, che già implementava la logica di verifica di permessi e licenze (quindi pacchetti acquistati), e di fatto implementava già tutta una sezione per la selezione delle licenze e dei pagamenti.
Di fatto quindi la modifica riguardava, nel nostro caso, solamente il pagamento effettivo dei pacchetti e la validazione di questi acquisti.
Il Problema
Le sfide principali nell’implementazione degli acquisti in-app - anche nel nostro caso - sono generalmente legate a questi aspetti.
- La gestione delle transazioni: era necessario assicurarsi che gli acquisti fossero elaborati correttamente.
- La persistenza dello stato degli acquisti: lo stato degli acquisti si doveva mantenere anche nel caso di disconnessione o reinstallazione dell’app.
- La verifica della ricevuta: questo accorgimento era diretto a prevenire frodi, garantendo che gli acquisti fossero effettivamente validi.
- Un’esperienza utente fluida: ci eravamo prefissati di creare un flusso di acquisto intuitivo e di garantire il minor numero di errori.
Gli Obiettivi
Il nostro obiettivo era, dunque, implementare un sistema di acquisti in-app il cui impatto sugli sviluppi già effettuati fosse minimo e che garantisse la sicurezza delle transazioni e le funzionalità richieste da Apple e Google, per permettere l’approvazione della soluzione.
Stavamo cercando quindi una soluzione “quick & dirty” e non un intervento esteso.
Per questo motivo il recupero dei prodotti e dei prezzi dai relativi store, così come la validazione delle licenze acquistate, non sono stati affrontati in quanto la relativa logica era già implementata attraverso altre soluzioni. La sola accortezza adottata in merito a ciò, è stata una configurazione accurata della parte di monetizzazione dell’app nei relativi store, per facilitare l’integrazione con la parte di soluzione esistente.
La Soluzione
Flutter fornisce il pacchetto in_app_purchase
per gestire gli acquisti digitali. Vediamo i passi principali per l’integrazione:
1. Configurazione del progetto
1.1. Aggiungere la dipendenza al file pubspec.yaml
in_app_purchase: ^3.1.4
1.2. Configurare le piattaforme
1.2.1. Android
1.2.1.1. Abilitare la fatturazione aggiungendo il permesso richiesto in AndroidManifest.xml
1.2.1.2. Configurare gli acquisti in Google Play Console:
1.2.1.2.1. Accedere alla console e selezionare l'app.
1.2.1.2.2. Andare alla sezione Monetizzazione > Prodotti in-app.
1.2.1.2.3. Creare prodotti specificando ID, prezzo e descrizione.
1.2.2. iOS
1.2.2.1 Configurare gli acquisti in App Store Connect:
1.2.2.1.1. Accedere ad App Store Connect e selezionare l'app.
1.2.2.1.2. Andare alla sezione Funzionalità > Acquisti in-app e creare un nuovo prodotto.
1.2.2.1.3. Specificare ID prodotto, tipo (consumabile/non consumabile/abbonamento), prezzo e descrizione.
1.2.2.2. Abilitare gli acquisti in-app in Xcode:
1.2.2.2.1. Aprire Info.plist e aggiungere la chiave In-App Purchase
.
1.2.2.2.. Attivare la capacità In-App Purchase nelle impostazioni del progetto.
2. Configurazione dei prodotti negli Store
Per poter implementare correttamente gli acquisti in-app, è necessario configurare i prodotti all'interno delle console di Google Play e App Store.
2.1. Google Play Console
2.1.1. Accedi alla Google Play Console.
2.1.2. Seleziona la tua app e vai alla sezione Monetizzazione > Prodotti in-app.
2.1.3. Clicca su Crea prodotto e scegli tra Consumabile o Non consumabile/Abbonamento.
2.1.4. Assegna un ID prodotto univoco (es. product_id_1).
2.1.5. Inserisci una descrizione, un prezzo e completa le informazioni richieste.
2.1.6. Pubblica il prodotto e attendi la verifica da parte di Google.
2.2. Apple App Store Connect
2.2.1. Accedi a App Store Connect.
2.2.2. Seleziona la tua app e vai alla scheda Funzionalità > Acquisti in-app.
2.2.3. Clicca su Aggiungi nuovo acquisto in-app.
2.2.4. Scegli il tipo di acquisto (Consumabile, Non consumabile, Abbonamento).
2.2.5. Definisci un ID prodotto, nome, descrizione e il prezzo.
2.2.6. Completa le informazioni richieste e invia il prodotto per approvazione.
Una volta configurati i prodotti nelle rispettive console, è possibile integrarli nel codice dell’app come illustrato nei passi successivi.
NB: Attenzione a due passaggi:
- In entrambi i casi il product_id, una volta configurato, non è né modificabile né riusabile (neppure in caso di cancellazione del prodotto), quindi se volete applicare delle logiche su questi ID, valutate attentamente come configurarli (ed eventualmente create prima degli id di prova, che poi cancellerete). In particolare Miriade ha scelto di configurare come product_id gli stessi ID già utilizzati su flutter, in modo da poter gestire in modo più efficiente l’associazione i prodotti / flussi già esistenti
- In entrambi i casi sarà necessario caricare sugli store una versione dell’app aggiornata, contenente l’abilitazione agli acquisti (vedi “configurazione del progetto”) per sbloccare la creazione completa dei prodotti. In entrambi gli store, prima di questo caricamento si potrà creare un solo prodotto di prova.
3. Inizializzazione del pacchetto
È necessario inizializzare la libreria, creando un’istanza dell’oggetto InAppPurchase
.
Questo oggetto dovrebbe essere un singleton all’interno dell’applicazione, per evitare problemi con il flusso degli acquisti, per cui dovrebbe essere posto all’interno di un classe di servizio o di un provider. Nel nostro caso stiamo utilizzando la libreria riverpod per la gestione dello stato dell’app, per cui può essere integrato all’interno di uno StateNotifier, ma potete utilizzarlo con qualsiasi altra libreria.
final InAppPurchase _iap = InAppPurchase.instance;
4. Effettuare un acquisto
Come già specificato in precedenza, la parte di interfaccia grafica di visualizzazione dei prodotti acquistabili (nel nostro caso abbonamenti), così come il recupero dei prodotti esistenti, era già stata implementata con logiche custom e via API Rest con il backend Java, per cui non riporteremo qui la logica specifica. La parte interessante è la gestione dell’acquisto vero e proprio, a partire dal productId.
void purchaseProduct(String productId) async {
_iap.isAvailable().then((available) async {
if (!available) {
logger.e('In-App Purchase is not available');
return;
}
Set<String> productIds = {productId};
final ProductDetailsResponse queryProductResult = await _iap.queryProductDetails(productIds);
if (queryProductResult.notFoundIDs.isNotEmpty) {
logger.e('Product not found');
// Product not found
return;
}
final PurchaseParam purchaseParam = PurchaseParam(productDetails: queryProductResult.productDetails.first);
bool result = await _iap.buyNonConsumable(purchaseParam: purchaseParam);
logger.i('Purchase Result: $result');
// Purchase verification is on separate step, consider implementing a "loading" placeholder while the purchase has been verified
}, onError: (error) {
logger.e('Error occurred while checking availability: $error');
});
}
La parte di verifica delle transazioni avviene di fatto creando uno stream in ascolto di eventi dallo store, che al momento dell’acquisto invia una serie di informazioni e di stati sulla transazione.
Nel nostro caso lo stream viene creato ed inizializzato in uno StateNotifier ad hoc per gli acquisti.
Future<void> initializePurchases() async {
if (!(await _purchaseService.isAvailable())) return;
///catch all purchase updates
_purchasesSubscription = InAppPurchase.instance.purchaseStream.listen(
(List<PurchaseDetails> purchaseDetailsList) {
handlePurchaseUpdates(purchaseDetailsList);
},
onDone: () {
_purchasesSubscription.cancel();
},
onError: (error) {},
);
}
void handlePurchaseUpdates(List<PurchaseDetails> purchaseDetailsList) async {
for (int index = 0; index < purchaseDetailsList.length; index++) {
var purchaseStatus = purchaseDetailsList[index].status;
switch (purchaseDetailsList[index].status) {
case PurchaseStatus.pending:
print(' purchase is in pending ');
continue;
case PurchaseStatus.error:
print(' purchase error ');
break;
case PurchaseStatus.canceled:
print(' purchase cancel ');
break;
case PurchaseStatus.purchased:
print(' purchased ');
break;
case PurchaseStatus.restored:
print(' purchase restore ');
break;
}
if (purchaseDetailsList[index].pendingCompletePurchase) {
await _purchaseService.completePurchase(purchaseDetailsList[index]).then((value) {
if (purchaseStatus == PurchaseStatus.purchased) {
//on purchase success you can call your logic and your API here
_api.validatePurchase(MobilePurchaseDto((b) {
b
..source = 1 //check if source is android or apple
..purchaseToken = purchaseDetailsList[index].verificationData.serverVerificationData
..productId = purchaseDetailsList[index].productID;
})).then((response) {
logger.i(response.data);
if (response.data != null) {
state = state.update(userSubs: [...state.userSubs, response.data!]);
}
}, onError: (error) {
logger.e(error);
});
.
}
});
}
}
}
Come si può vedere dal codice, al momento della notifica di un acquisto in pending, il codice rimane in attesa del completamento dell’acquisto, e successivamente effettua una chiamata alle API Rest del backend per validare i dati. Noi abbiamo utilizzato un banale MobilePurchaseDto per inviare dati al backend, ma potete scegliere i dati in qualsiasi formato voi preferiate.
Viene omessa al momento in questo articolo la parte di logica che si occupa di effettuare la chiamata Rest, poiché di nuovo lo sviluppatore può utilizzare la libreria che preferisce per questa funzione. Noi in Miriade utilizziamo Dio, che fornisce anche un builder per creare direttamente il client rest a partire dal file swagger generato dal backend.
5. Validare l’acquisto effettuato
Una volta ricevuta la conferma dell’acquisto dagli store è necessario validare la ricevuta di pagamento dello store, per evitare eventuali frodi da parte di utenti malevoli. Come indicato nel paragrafo precedente, i dati con la ricevuta vengono inviati al backend, che si occuperà della verifica.
Ricordiamo che il backend è implementato in Java con Spring, ma qualsiasi altro tipo di backend è utilizzabile, con la medesima logica.
In particolare sono da prevedere tre passaggi.
5.1. La ricezione dei dati con un controller rest
@RolesAllowed({ RoleKeys.User })
public ResponseEntity<SubscriptionDto> purchase(@RequestBody @Valid MobilePurchaseDto purchase) {
try {
SubscriptionDto resp = subscriptionService.createSubscriptionFromMobilePurchase(purchase);
if (resp != null)
return ResponseEntity.ok(resp);
return ResponseEntity.status(HttpStatus.NOT_FOUND).build();
} catch (NoSuchEntityException nse) {
return ResponseEntity.status(HttpStatus.NOT_FOUND).build();
} catch (UnAuthorizedException ue) {
_log.error(ue.getMessage());
return ResponseEntity.status(HttpStatus.FORBIDDEN).build();
} catch (Throwable e) {
_log.error(e.getMessage());
if (_log.isDebugEnabled())
_log.debug(e.getMessage(), e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
}
}
5.2. La validazione della ricevuta in base a quale sia lo store di provenienza
@Override
@Transactional
public SubscriptionDto createSubscriptionFromMobilePurchase(MobilePurchaseDto subscription) throws UnAuthorizedException, NoSuchEntityException {
switch (subscription.getSource()) {
case 1:
// Google Play
purchaseService.verifyGooglePlayPurchase(subscription.getPurchaseToken(), subscription.getProductId());
break;
case 2:
// Apple Store
purchaseService.verifyAppleStorePurchase(subscription.getPurchaseToken(), subscription.getProductId());
break;
default:
throw new NoSuchEntityException("Unknown purchase source: " + subscription.getSource());
}
return createSubscriptionFromMobilePurchase(subscription);
//logic here is only related to backend and database structure, anche the method saves data on database and return a generic SubscriptionDto, already used by the existing backend logic
}
5.3. La verifica effettiva della ricevuta
In questo caso stiamo utilizzando la libreria google-api-services-androidpublisher, versione v3-rev20250102-2.0.0 per la validazione lato Google. Qui si deve prestare attenzione, perché prima di poter effettuare questa chiamata è necessario configurare un service account google e dargli i permessi per accedere.
Ulteriori dettagli su questo si possono trovare nella documentazione ufficiale (ma, ad essere sinceri, le informazioni in questo caso sono un po’ sparpagliate), oppure direttamente dal sacro graal degli sviluppatori.
La verifica in senso stretto della ricevuta di pagamento è molto semplice
public void verifyGooglePlayPurchase(String purchaseToken, String productId) {
try {
final AndroidPublisher.Purchases.Subscriptionsv2.Get get =
androidPublisher.purchases().subscriptionsv2().get(GOOGLE_PLAY_PACKAGE_NAME, purchaseToken);
final SubscriptionPurchaseV2 purchase = get.execute();
System.out.println("Found google purchase item " + purchase.toPrettyString());
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
Una verifica simile può essere effettuata lato Apple utilizzando la libreria app-store-server-library versione 3.3.0.
I Risultati
Il risultato di questa implementazione ha permesso la gestione integrata degli acquisti in-app, secondo gli standard dettati dai due store (Google Play ed Apple Store), utilizzando però al contempo gran parte della logica di gestione dei prodotti e delle licenze attive già esistenti. In questo modo l’utente non si è quasi accorto del cambio di framework di pagamento utilizzato, beneficiando al tempo stesso di un’esperienza più simile al resto delle app degli store.
Conclusioni
L’implementazione degli acquisti in-app con Flutter richiede attenzione alla sicurezza e all’esperienza utente. Seguendo i passi descritti, è possibile realizzare un sistema robusto e scalabile. Se hai bisogno di supporto, il team di Miriade può aiutarti a ottimizzare la tua applicazione e integrare soluzioni di pagamento avanzate.