Adding subscriptions to your Android app. Part 3. Checking if user is subscribed

theplebdev

Tristan Elliott

Posted on March 24, 2023

Adding subscriptions to your Android app. Part 3. Checking if user is subscribed

Table of contents

  1. Resources I used
  2. Before we start
  3. Start coding
  4. What is the next tutorial about?

My app on the Google Playstore

GitHub code

Resources I used

Before we start

  • Before you can read any further, you must have done the following 2 things

1) Properly setup your google developer accound : The instructions can be found HERE

2) Add the Google billing library : The library can be found HERE. You need to add this library to your gradle.build file, then publish your app to either production, internal testing or closed testing.

Determine if the user is subscribed or not

  • So assuming you have done all the previously mentioned things, we can now worry about how to determine if the user is subscribed or not.

  • The first thing we are going to do is to create a class called BillingClientWrapper and this class is where we will have all the code the interacts directly with the Google Play Billing Library

Creating state inside the BillingClientWrapper

  • Now we need to create a MutableState object that will hold our subscribed information. We then need to map it to a State object and expose it to the rest of our code:
class BillingClientWrapper(
    context: Context
): PurchasesUpdatedListener{

    // Current Purchases
    private val _purchases =
        MutableStateFlow<List<Purchase>>(listOf())
    val purchases = _purchases.asStateFlow()

}

Enter fullscreen mode Exit fullscreen mode
  • This mapping from MutableState to State and then exposing as a public variable called purchases is a good coding practice. It allows us to define clear boundaries between our code.

for the moment we can ignore the PurchasesUpdatedListener interface. But don't worry we will talk about it shortly

Initialize a BillingClient

  • Everything starts with the BillingClient. We initialize the BillingClient like so:
// Initialize the BillingClient.
    private val billingClient = BillingClient.newBuilder(context)
        .setListener(this)
        .enablePendingPurchases()
        .build()
Enter fullscreen mode Exit fullscreen mode
  • BillingClient is the main interface for communication between the Google Play Billing Library and the rest of your app. BillingClient provides convenience methods, both synchronous and asynchronous, for many common billing operations. It's strongly recommended that we have one active BillingClient connection open at one time to avoid multiple PurchasesUpdatedListener callbacks for a single event

In .setListener(this) the this is referring to the PurchasesUpdatedListener interface. Since it is an interface, we have to implement its method, which is the onPurchasesUpdated method:

 override fun onPurchasesUpdated(
        billingResult: BillingResult, //contains the response code from the In-App billing API
        purchases: List<Purchase>? // a list of objects representing in-app purchases
    ) {

        if (billingResult.responseCode == BillingClient.BillingResponseCode.OK
            && !purchases.isNullOrEmpty()
        ) {
            // Post new purchase List to _purchases
            _purchases.value = purchases

            // Then, handle the purchases
        for (purchase in purchases) {
         acknowledgePurchases(purchase)// dont need to worry about
            }
        } else if (billingResult.responseCode == BillingClient.BillingResponseCode.USER_CANCELED) {
            // Handle an error caused by a user cancelling the purchase flow.

            Timber.tag("BILLINGR").e("User has cancelled")
        } else {
            // Handle any other error codes.
        }
    }

Enter fullscreen mode Exit fullscreen mode
  • The code above is responsible for updating our _purchases variable from earlier and will run each time our user makes a purchase. I also want to point out the the section of the code I marked acknowledgePurchases(purchase)// dont need to worry about. At this moment we don't need to worry about this code. Because it is used for detecting the different states of the Purchase objects(I will talk about in a future tutorial). But if you still want to checkout the code yourself, HERE it is.

Connect to Google play

  • After we have created a BillingClient, you need to establish a connection to Google Play. To connect to Google Play, call startConnection(). The connection process is asynchronous, and we must implement a BillingClientStateListener to receive a callback once the setup of the client is complete and it’s ready to make further requests:
   /*******CALLED TO INITIALIZE EVERYTHING******/
    // Establish a connection to Google Play.
    fun startBillingConnection(billingConnectionState: MutableLiveData<Boolean>) {

        billingClient.startConnection(object : BillingClientStateListener {
            override fun onBillingSetupFinished(billingResult: BillingResult) {
                if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
                    Timber.tag("BILLINGR").d("Billing response OK")
        // The BillingClient is ready. You can query purchases and product details here
       queryPurchases()// we talk about this in the next paragraph
                } else {


           Timber.tag("BILLINGR").e(billingResult.debugMessage)
                }
            }

            override fun onBillingServiceDisconnected() {

                Timber.tag("BILLINGR").d("Billing connection disconnected")
                startBillingConnection(billingConnectionState)
            }
        })
    }

Enter fullscreen mode Exit fullscreen mode
  • Inside the code block above you can see we create a anonymous class with billingClient.startConnection(object : BillingClientStateListener. Which means we are creating a instance of the BillingClientStateListener interface. Meaning we have to override it's two methods.

1) onBillingSetupFinished() : called when the connection to Google play is done and we can query for the user's purchases.

2) onBillingServiceDisconnected() : called when the connection to Google play billing service is lost. when this happens we just try to reconnect with a recursive call to startBillingConnection(billingConnectionState)

The queryPurchases() method

  • Now we can talk about queryPurchases():
fun queryPurchases() {
        if (!billingClient.isReady) {

            Timber.tag("BILLINGR").e("queryPurchases: BillingClient is not ready")
        }

        // QUERY FOR EXISTING SUBSCRIPTION PRODUCTS THAT HAVE BEEN PURCHASED
        billingClient.queryPurchasesAsync(
            QueryPurchasesParams.newBuilder().setProductType(BillingClient.ProductType.SUBS).build()
        ) { billingResult, purchaseList ->
            if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
                if (!purchaseList.isNullOrEmpty()) {
                    _purchases.value = purchaseList
                } else {
                    _purchases.value = emptyList()
                }

            } else {
                Timber.tag("BILLINGR").e(billingResult.debugMessage)
            }
        }
    }

Enter fullscreen mode Exit fullscreen mode
  • If you think this code seems similar to the BillingClientStateListener you're right. But as the documentation states, there are cases(such as buying a subscription outside of your app) where your app will be made aware of purchases by calling BillingClient.queryPurchasesAsync().

  • Next we are using QueryPurchasesParams.newBuilder().setProductType(BillingClient.ProductType.SUBS).build() to query the Google library for any subscriptions that the user has made.

Repository Layer

  • Now we can create a repository layer that will be used as a filter and only return the subscriptions needed by our app.
class SubscriptionDataRepository(billingClientWrapper: BillingClientWrapper) {

    // Set to true when a returned purchase is an auto-renewing basic subscription.
    //hasRenewablePremium IS HOW WE WILL DETERMINE IF THERE IS A SUBSCRIPTION OR NOT
    val hasRenewablePremium: Flow<Boolean> = billingClientWrapper.purchases.map { value: List<Purchase> ->
        value.any { purchase: Purchase ->  purchase.products.contains(PREMIUM_SUB) && purchase.isAutoRenewing}
    }
   companion object {
        // List of subscription product offerings
        private const val PREMIUM_SUB = "your_subscription_id"
    }
}
Enter fullscreen mode Exit fullscreen mode
  • The important things from the code above are the isAutoRenewing and contains(PREMIUM_SUB). isAutoRenewing is a flag stored on a Purchase object to determine if the subscription is active or not. contains(PREMIUM_SUB) is used as a filter and you need to put your own subscription_id for you subscription. A subscription id can be found inside of your google play console under Monetization -> Subscriptions.

ViewModel layer

  • Now we are in the ViewModel there are 6 things that we need to do:

1) Initialize the BillingClientWrapper

2) Initialize the Repository

3) Create state to hold the repository data

4) Create a method to query the repository

5) Start the connection

6) Using DisposableEffect to call queryPurchasesAsync()

1) Initialize the BillingClientWrapper

  • We know all of the code to talk to the Google play Billing library lives inside of the BillingClientWrapper. So the first thing inside of our ViewModel is to initialize it:
class BillingViewModel(application: Application): AndroidViewModel(application){

    var billingClient: BillingClientWrapper = BillingClientWrapper(application)

}

Enter fullscreen mode Exit fullscreen mode
  • Notice how we use the AndroidViewModel(application) instead of the traditional ViewModel(). We do this to allow our BillingViewModel access to the application context, which we use to initialize the BillingClientWrapper.

2) Initialize the Repository

  • The next step is to use the initialized billingClient and pass it to the repository:
private var repo: SubscriptionDataRepository =
        SubscriptionDataRepository(billingClientWrapper = billingClient)

Enter fullscreen mode Exit fullscreen mode

3) Create state to hold the repository data

  • We now need a object to hold the state we get from our repository and expose it to our view:
data class BillingUiState(
    val subscribed:Boolean = false
)

class BillingViewModel(application: Application): AndroidViewModel(application){

    private val _uiState = mutableStateOf(BillingUiState())
    val state:State<BillingUiState> = _uiState


    private var billingClient: BillingClientWrapper = BillingClientWrapper(application)

    private var repo: SubscriptionDataRepository =
        SubscriptionDataRepository(billingClientWrapper = billingClient)

}

Enter fullscreen mode Exit fullscreen mode

4) Create a method to query our Repository

 fun refreshPurchases(){

        viewModelScope.launch {
            repo.hasRenewablePremium.collect { collectedSubscriptions ->
                _uiState.value = _uiState.value.copy(
                    subscribed = collectedSubscriptions
                )
            }
        }

    }

Enter fullscreen mode Exit fullscreen mode
  • As you can see from the code above, we are using the viewModelScope to collect{} from the Flow stored inside of our repository layer repo.hasRenewablePremium.

5) Start the connection

  • Now that we have all of our methods set up we need to initialize everything inside of the BillingViewModel's init{} blocks:
init {
        billingClient.startBillingConnection(billingConnectionState = _billingConnectionState)
    }
init{

  refreshPurchases()

}

Enter fullscreen mode Exit fullscreen mode
  • Technically when the our BillingViewModel is initialized, our code will be able to recognize if the user has bought any subscriptions or not. But there are many more situations we are not accounting for, such as the ones described in the documentation, HERE. As the documentation states, to be able to handle those situations we need to call the BillingClient.queryPurchasesAsync() in our onResume() method.

6) Using DisposableEffect to call queryPurchasesAsync()

  • We can accomplish calling BillingClient.queryPurchasesAsync() in our onResume() by using Side-effects. More specifically we are going to use DisposableEffect which allows us to create, add and remove a lifecycle observe to the onResume() method. We can actually do so inside of a Compose fuctions:
@Composable 
fun AddObserver(viewModel:BillingViewModel){
val lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current
DisposableEffect(lifecycleOwner) {
        // Create an observer that triggers our remembered callbacks
        // for sending analytics events
        val observer = LifecycleEventObserver { _, event ->
            if (event == Lifecycle.Event.ON_RESUME) {

                Timber.tag("LIFEEVENTCYCLE").d("WE ARE RESUMING")
                viewModel.refreshPurchases()
            }
        }

        // Add the observer to the lifecycle
        lifecycleOwner.lifecycle.addObserver(observer)

        // When the effect leaves the Composition, remove the observer
        onDispose {
            lifecycleOwner.lifecycle.removeObserver(observer)
        }
    }


}

Enter fullscreen mode Exit fullscreen mode
  • In order to make this code work we first need to update the refreshPurchases() method inside of the BillingViewModel:
     fun refreshPurchases(){

        viewModelScope.launch {
            billingClient.queryPurchases()
            repo.hasRenewablePremium.collect { collectedSubscriptions ->
                //val value = collectedSubscriptions.hasRenewablePremium
                _uiState.value = _uiState.value.copy(
                    subscribed = collectedSubscriptions
                )
            }
        }

    }

Enter fullscreen mode Exit fullscreen mode
  • Notice that the code above now calls the billingClient.queryPurchases(), which will update the _purchases variable inside of the BillingClientWrapper, which will update the hasRenewablePremium variable inside of the repository, which will then be collected in the BilingViewModel, which can then be shown to the use as the variable state.subscribed

What is the next tutorial

  • In the next tutorial we will implement the code to allow the user to buy our subscriptions

Conclusion

  • Thank you for taking the time out of your day to read this blog post of mine. If you have any questions or concerns please comment below or reach out to me on Twitter.
💖 💪 🙅 🚩
theplebdev
Tristan Elliott

Posted on March 24, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related