BlogRight arrowTutorialRight ArrowAcquisti in-app per Android, parte 1: configurazione e aggiunta al progetto
BlogRight arrowTutorialRight ArrowAcquisti in-app per Android, parte 1: configurazione e aggiunta al progetto

Acquisti in-app per Android, parte 1: configurazione e aggiunta al progetto

Acquisti in-app per Android, parte 1: configurazione e aggiunta al progetto
Listen to the episode
Acquisti in-app per Android, parte 1: configurazione e aggiunta al progetto

Gli acquisti in-app (in-app purchases), in particolare gli abbonamenti (subscriptions), sono i metodi più diffusi per guadagnare con un'app. Da un lato, l'abbonamento consente allo sviluppatore di investire nello sviluppo dei contenuti e del prodotto, dall'altro aiuta gli utenti a ottenere un'app, nel complesso, di qualità superiore. Gli acquisti in-app sono soggetti a una commissione del 30%, ma se un utente è abbonato da più di un anno o un'app  guadagna meno di $1М all’anno, la commissione si riduce al 15%.

In questo articolo spiegheremo come:  

  • Creare un prodotto nella Console Google Play;
  • Configurare gli abbonamenti: come specificare durata, prezzo, periodi di prova; 
  • Aggiungere un elenco di prodotti in un'app. 
⭐️ Download our guide on in-app techniques which will make in-app purchases in your app perfect

Creazione degli abbonamenti

Prima di iniziare, accertati di:

  1. Possedere un account sviluppatore per Google Play.
  2. Aver firmato tutti gli accordi ed essere pronto a iniziare a lavorare.  

Ora, mettiamoci al lavoro e creiamo il nostro primo prodotto.  

Passa al tuo account sviluppatore e scegli l’app.

Quindi, nel menu a sinistra, cerca la sezione Products, seleziona Subscriptions e fai clic su Create a Subscription

Ora vedremo il configuratore di abbonamenti. Ecco alcuni punti importanti.

  1. Creazione dell’ID che verrà usata nell’app. È una buona idea aggiungere all’ID un periodo di abbonamento o altre informazioni utili, in modo da poter creare prodotti in un unico stile e facilitare l'analisi delle statistiche di vendita.
  2. Il nome dell'abbonamento che l'utente vedrà nel negozio.  
  3. Descrizione dell’abbonamento. L’utente vedrà anche questa informazione.  

Scorriamo verso il basso e scegliamo il periodo di abbonamento. Nel nostro caso, sarà di una settimana. Impostiamo il prezzo.  

Di solito, si imposta il prezzo nella valuta dell’account di base e il sistema lo converte automaticamente. Ma è anche possibile modificare il prezzo per un paese specifico in modalità manuale.  

Nota che Google mostra la tassa per ciascun paese. È fantastico, App Store Connect non lo fa.  

Scorriamo verso il basso e scegliamo (se necessario): 

  1. Periodo di prova gratuito (Free trial).
  2. Prezzo introduttivo, cioè un'offerta per i primi periodi di pagamento.  
  3. Periodo di tolleranza (Grace period). Se un utente ha problemi di pagamento, puoi comunque fornirgli l'accesso premium per un certo numero di giorni.
  4. L'opportunità di iscriversi nuovamente dal Play Store, non dall'app, in seguito all’annullamento (cancellation). 

Confronto tra il processo di acquisto nella Play Console e in App Store Connect

Benché gli abbonamenti siano monetizzati in modo più efficace su iOS, il pannello di amministrazione della Play Console è più comodo, è organizzato e localizzato meglio ed è più rapido.

Il processo di creazione del prodotto è reso quanto più semplice possibile. Qui spieghiamo come creare prodotti in iOS.

Come ottenere un elenco di prodotti in un'app

Una volta creati i prodotti, lavoriamo sull'architettura per accettare ed elaborare gli acquisti. In generale, il processo è il seguente:   

  1. Aggiungere una Libreria Fatturazione.
  2. Sviluppare una classe per l'interazione con i prodotti di Google Play.
  3. Implementare tutti i metodi per elaborare gli acquisti.  
  4. Aggiungere la convalida di un acquisto (purchase validation) da parte del server.  
  5. Raccogliere le analisi.

In questa parte, esaminiamo più da vicino i primi due punti.  

Aggiunta della Libreria Fatturazione a un progetto:

implementation "com.android.billingclient:billing:4.0.0"

Al momento della stesura di questo articolo, l'ultima versione è la 4.0.0. È possibile sostituirla con qualsiasi altra versione in qualsiasi momento.  

Creiamo una classe wrapper che copra la logica di interazione con Google Play e inizializziamo BillingClient dalla Libreria Fatturazione. Chiamiamo questa classe BillingClientWrapper.

Questa classe implementerà l'interfaccia PurchasesUpdatedListener. Ora sovrascriviamo un metodo per farlo: onPurchasesUpdated(billingResult: BillingResult, purchaseList: MutableList<Purchase>?) , è necessario subito dopo aver effettuato un acquisto, ma descriveremo il processo di implementazione nel prossimo articolo. 

import android.content.Context
import com.android.billingclient.api.*

class BillingClientWrapper(context: Context) : PurchasesUpdatedListener {

   private val billingClient = BillingClient
       .newBuilder(context)
       .enablePendingPurchases()
       .setListener(this)
       .build()

   override fun onPurchasesUpdated(billingResult: BillingResult, purchaseList: MutableList?) {
       // here come callbacks about new purchases
   }
}

Google raccomanda di evitare di avere più di una connessione attiva tra BillingClient e Google Play, per evitare che un callback relativo a un acquisto effettuato venga eseguito più volte. Pertanto, si dovrebbe avere un unico BillingClient in una classe singleton. La classe dell'esempio non è un singleton, ma possiamo usare dependency injection (per esempio, con l’aiuto di Dagger o Koin) in questo modo, consentendo l'esistenza di una sola istanza in un dato momento. 

Per effettuare qualsiasi richiesta con la Libreria Fatturazione, BillingClient deve avere una connessione attiva con Google Play nel momento in cui viene effettuata la richiesta, ma, a un certo punto, la connessione può essere persa. Per comodità, scriviamo un wrapper che ci permetta di fare richieste solo quando la connessione è attiva.  

Per ottenere i prodotti, abbiamo bisogno dei loro ID impostati sul mercato. Ma non basta una richiesta, serve anche il tipo di prodotto (abbonamenti o acquisti una tantum), per questo possiamo ottenere un elenco generale di prodotti "combinando" i risultati di due richieste.   

Start for free

Convenient in-app purchases infrastructure.

Adapty SDK has it all:
— server-side purchase validation,
— all side cases covered,
— simple implementation.

Start for free

La richiesta di prodotti è asincrona, quindi abbiamo bisogno di un callback che ci fornisca un elenco di prodotti o restituisca un modello di errore. Quando si verifica un errore, la Libreria Fatturazione restituisce un BillingResponseCodes, e un debugMessage. Creiamo un'interfaccia di callback e un modello per un errore:  

interface OnQueryProductsListener {
  fun onSuccess(products: List < SkuDetails > )
  fun onFailure(error: Error)
}

class Error(val responseCode: Int, val debugMessage: String)

Ecco il codice di un metodo privato per ottenere dati su un tipo specifico di prodotti e di un metodo pubblico che "combinerà" i risultati di due richieste e fornirà all'utente l'elenco finale dei prodotti o mostrerà un messaggio di errore.  

fun queryProducts(listener: OnQueryProductsListener) {
   val skusList = listOf("premium_sub_month", "premium_sub_year", "some_inapp")

   queryProductsForType(
       skusList,
       BillingClient.SkuType.SUBS
   ) { billingResult, skuDetailsList ->
       if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
           val products = skuDetailsList ?: mutableListOf()
           queryProductsForType(
               skusList,
               BillingClient.SkuType.INAPP
           ) { billingResult, skuDetailsList ->
               if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
                   products.addAll(skuDetailsList ?: listOf())
                   listener.onSuccess(products)
               } else {
                   listener.onFailure(
                       Error(billingResult.responseCode, billingResult.debugMessage)
                   )
               }
           }
       } else {
           listener.onFailure(
               Error(billingResult.responseCode, billingResult.debugMessage)
           )
       }
   }
}

private fun queryProductsForType(
   skusList: List,
   @BillingClient.SkuType type: String,
   listener: SkuDetailsResponseListener
) {
   onConnected {
       billingClient.querySkuDetailsAsync(
           SkuDetailsParams.newBuilder().setSkusList(skusList).setType(type).build(),
           listener
       )
   }
}

Abbiamo così ottenuto informazioni preziose sui prodotti (SkuDetails) che ci indicano i nomi, i prezzi e il tipo di prodotto localizzati, così come il periodo di fatturazione e le informazioni sul prezzo di lancio e sul periodo di prova (se disponibile per questo utente) per gli abbonamenti. La classe definitiva verrà visualizzata in questo modo:   

import android.content.Context
import com.android.billingclient.api.*
class BillingClientWrapper(context: Context) : PurchasesUpdatedListener {
   interface OnQueryProductsListener {
       fun onSuccess(products: List<SkuDetails>)
       fun onFailure(error: Error)
   }
   class Error(val responseCode: Int, val debugMessage: String)
   private val billingClient = BillingClient
       .newBuilder(context)
       .enablePendingPurchases()
       .setListener(this)
       .build()
   fun queryProducts(listener: OnQueryProductsListener) {
       val skusList = listOf("premium_sub_month", "premium_sub_year", "some_inapp")
       queryProductsForType(
           skusList,
           BillingClient.SkuType.SUBS
       ) { billingResult, skuDetailsList ->
           if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
               val products = skuDetailsList ?: mutableListOf()
               queryProductsForType(
                   skusList,
                   BillingClient.SkuType.INAPP
               ) { billingResult, skuDetailsList ->
                   if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
                       products.addAll(skuDetailsList ?: listOf())
                       listener.onSuccess(products)
                   } else {
                       listener.onFailure(
                           Error(billingResult.responseCode, billingResult.debugMessage)
                       )
                   }
               }
           } else {
               listener.onFailure(
                   Error(billingResult.responseCode, billingResult.debugMessage)
               )
           }
       }
   }
   private fun queryProductsForType(
       skusList: List<String>,
       @BillingClient.SkuType type: String,
       listener: SkuDetailsResponseListener
   ) {
       onConnected {
           billingClient.querySkuDetailsAsync(
               SkuDetailsParams.newBuilder().setSkusList(skusList).setType(type).build(),
               listener
           )
       }
   }
   private fun onConnected(block: () -> Unit) {
       billingClient.startConnection(object : BillingClientStateListener {
           override fun onBillingSetupFinished(billingResult: BillingResult) {
               block()
           }
           override fun onBillingServiceDisconnected() {
               // Try to restart the connection on the next request to
               // Google Play by calling the startConnection() method.
           }
       })
   }
   override fun onPurchasesUpdated(billingResult: BillingResult, purchaseList: MutableList?) {
       // here come callbacks about new purchases
   }
}

È tutto, per oggi. Nei prossimi articoli parleremo dell'implementazione degli acquisti (purchase implementation), dei test e della gestione degli errori.

Further reading

What's wrong with my in-app subscription on iOS?
What's wrong with my in-app subscription on iOS?
November 18, 2019
8 min read
How to choose money-making keywords for your app
How to choose money-making keywords for your app
March 31, 2022
10 min read
Adapty August Update: Server notification status, A/B testing CSV, and SDK 1.18-beta
Adapty August Update: Server notification status, A/B testing CSV, and SDK 1.18-beta
September 15, 2022
5 min read