kotlinsubscriptionplay-billing-library

setOfferToken is throwing a nullPointerException


So I have been trying to migrate to Play Billing Library 6, and while I was able to successfully make my in-app purchases work, I’m unable to make my subscriptions work. Subscriptions specifically needs the setOfferToken parameter. My code below:

class AccessAllActivity: AppCompatActivity() {
    private lateinit var binding: ActivityAccessAllBinding
    private lateinit var billingClient: BillingClient
    private lateinit var productDetails: ProductDetails

    private val onBackPressedCallback: OnBackPressedCallback = object: OnBackPressedCallback(true) {
        override fun handleOnBackPressed() {
            backPress()
        }
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        onBackPressedDispatcher.addCallback(this, onBackPressedCallback)
        binding = ActivityAccessAllBinding.inflate(layoutInflater)
        setContentView(binding.root)

        billingClient = BillingClient.newBuilder(this)
            .setListener(purchasesUpdatedListener)
            .enablePendingPurchases()
            .build()
        billingClient.startConnection(object: BillingClientStateListener {
            override fun onBillingSetupFinished(billingResult: BillingResult) {
                if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
                    Log.d(TAG, "Billing response OK")
                    queryPurchases()
                    queryAvailableProducts()
                } else {
                    Log.e(TAG, billingResult.debugMessage)
                }
            }
            override fun onBillingServiceDisconnected() {
                Log.i(TAG, "Billing connection disconnected")
            }
        })

        binding.btnClose.setOnClickListener {
            backPress()
        }

        binding.btnSubscribeNow.setOnClickListener {
            startSub()
        }
    }

    fun queryPurchases() {
        if (!billingClient.isReady) {
            Log.e(TAG, "queryPurchases: BillingClient is not ready")
        }
        billingClient.queryPurchasesAsync(
            QueryPurchasesParams.newBuilder().setProductType(BillingClient.ProductType.SUBS).build()
        ) { billingResult, _ ->
            if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
                /*if (purchaseList.isNotEmpty()) {
                    //_purchases.value = purchaseList
                } else {
                    //_purchases.value = emptyList()
                }*/
            } else {
                Log.e(TAG, billingResult.debugMessage)
            }
        }
    }

    private fun queryAvailableProducts() {
        val subProductList =
            listOf(
                Product.newBuilder()
                    .setProductId("subscription")
                    .setProductType(BillingClient.ProductType.SUBS)
                    .build(),
            )
        val subParams = QueryProductDetailsParams.newBuilder().setProductList(subProductList).build()
        billingClient.queryProductDetailsAsync(subParams) { billingResult, productDetailsList ->
            if (billingResult.responseCode == BillingClient.BillingResponseCode.OK && productDetailsList.isNotEmpty()) {
                val appSubProduct = productDetailsList.find { p-> p.productId == "subscription" }
                if (appSubProduct != null) {
                    startSub()
                }
            }
        }
    }

    private fun startSub() {
        if (!productDetails.subscriptionOfferDetails.isNullOrEmpty()) {
            val productDetailsParamsList =
                listOf(
                    ProductDetailsParams.newBuilder()
                        .setProductDetails(productDetails)
                        .setOfferToken(productDetails.subscriptionOfferDetails!!.first().offerToken)
                        .build()
                )
            val billingFlowParams = BillingFlowParams.newBuilder()
                .setProductDetailsParamsList(productDetailsParamsList)
                .build()
            billingClient.launchBillingFlow(this, billingFlowParams)
        } else {
            println("Check productDetails, because is null")
        }
    }

    private val purchasesUpdatedListener = PurchasesUpdatedListener { billingResult, purchases ->
        if (billingResult.responseCode == BillingClient.BillingResponseCode.OK && purchases != null) {
            for (purchase in purchases) {
                handlePurchase(purchase)
            }
        } else if (billingResult.responseCode == BillingClient.BillingResponseCode.USER_CANCELED) {
            Toast.makeText(this, string.payment_cancelled, Toast.LENGTH_LONG).show()
        } else {
            Toast.makeText(this, string.payment_error, Toast.LENGTH_LONG).show()
            Log.i(TAG, "Billing service error ${billingResult.responseCode}")
        }
    }

    private fun handlePurchase(purchase: Purchase) {
        if (purchase.purchaseState == Purchase.PurchaseState.PURCHASED) {
            val consumeParams = ConsumeParams.newBuilder()
                .setPurchaseToken(purchase.purchaseToken)
                .build()
            billingClient.consumeAsync(consumeParams) { _, _ ->
                finish()
            }
        } else if (purchase.purchaseState == Purchase.PurchaseState.PENDING) {
            Toast.makeText(this, string.payment_error, Toast.LENGTH_LONG).show()
        }
    }

    override fun onDestroy() {
        super.onDestroy()
        billingClient.endConnection()
    }

    private fun backPress() {
        finish()
    }

    companion object {
        private const val TAG = "ActivityAccessAll"
    }
}

I tried the documentation and all answers here on StackOverflow, but I’m still unable to figure this out. Any help would be greatly appreciated.


Solution

  • Firtly call setUpBillingClient() after start activity/fragment and found your products. productDetails must be defined before click to button.

    binding.btnSubscribeNow.setOnClickListener { startSub() }
    
    
    private fun startSub() {
        if (productDetails != null && !productDetails?.subscriptionOfferDetails.isNullOrEmpty()) {
            val productDetailsParamsList =
                listOf(
                    BillingFlowParams.ProductDetailsParams.newBuilder()
                        .setProductDetails(productDetails!!)
                        .setOfferToken(productDetails!!.subscriptionOfferDetails!!.first().offerToken)
                        .build()
                )
            val billingFlowParams =
                BillingFlowParams.newBuilder()
                    .setProductDetailsParamsList(productDetailsParamsList)
                    .build()
    
            // Launch the billing flow
            billingClient?.launchBillingFlow(requireActivity(), billingFlowParams)
    
        } else { 
         println("Check productDetails, because is null")
       }
    }