4

I use Google in-app Billing Version 3 in my Android App, I'm testing the order module of the App based the following code.

In the case, a user's order has been Refunded by Google Play, but when I run mBilling.initBillingClient(), I find purchase.purchaseState == Purchase.PurchaseState.PURCHASED in private fun processPurchases is launched.

It seems that an order with Refunded status as PURCHASED successfully, how can I fix it?

FragmentBuy.kt

class FragmentBuy : Fragment() {
     ...
     override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
        binding = DataBindingUtil.inflate(
            inflater, R.layout.layout_buy, container, false
        )
   
        val mBilling=Billing.getInstance(requireActivity(), getString(R.string.skuRegisterApp)) 
        mBilling.initBillingClient()
        ...       
    }

}

Billing.kt

class Billing private constructor (private val mContext: Context, private val purchaseItem :String)
    :PurchasesUpdatedListener, BillingClientStateListener   {

    private lateinit var playStoreBillingClient: BillingClient
    private val mapSkuDetails = mutableMapOf<String,SkuDetails>()

    fun initBillingClient() {
        playStoreBillingClient = BillingClient
            .newBuilder(mContext)
            .enablePendingPurchases() 
            .setListener(this)
            .build()

        if (!playStoreBillingClient.isReady) {
            playStoreBillingClient.startConnection(this)  //It will launach override fun onBillingSetupFinished()
        }
    }

    override fun onBillingSetupFinished(billingResult: BillingResult) {
        when (billingResult.responseCode) {
            BillingClient.BillingResponseCode.OK -> {                   
                queryAndProcessPurchasesAsync()
            }
                ...
        }
    }

    private fun queryAndProcessPurchasesAsync() {
        val purchasesResult = HashSet<Purchase>()
        var result = playStoreBillingClient.queryPurchases(BillingClient.SkuType.INAPP)
        result?.purchasesList?.apply { purchasesResult.addAll(this) }

        processPurchases(purchasesResult)
    }


    private fun processPurchases(purchasesResult: Set<Purchase>): Job {
        val validPurchases = HashSet<Purchase>(purchasesResult.size)
        purchasesResult.forEach { purchase ->
            if (purchase.purchaseState == Purchase.PurchaseState.PURCHASED) {
                if (purchase.sku.equals(purchaseItem)) {
                    if (isSignatureValid(purchase)) {
                        validPurchases.add(purchase)
                        setIsRegisteredAppAsTrue(mContext)  //It will be launched even if the status of the order is refunded.                           
                    }
                }
            }
            ...

        }
        acknowledgeNonConsumablePurchasesAsync(validPurchases.toList())

    }


    private fun acknowledgeNonConsumablePurchasesAsync(nonConsumables: List<Purchase>) {
        ...
    }


    override fun onPurchasesUpdated(billingResult: BillingResult, purchases: MutableList<Purchase>?) {
        when (billingResult.responseCode) {
            BillingClient.BillingResponseCode.OK -> {
                purchases?.apply { processPurchases(this.toSet()) }
            }
                ...
        }
    }

    fun purchaseProduct(activity: Activity) {
        val skuDetails = mapSkuDetails[purchaseItem]
        skuDetails?.let{
            val purchaseParams = BillingFlowParams.newBuilder().setSkuDetails(skuDetails).build()
            playStoreBillingClient.launchBillingFlow(activity, purchaseParams)
        }
    }

    companion object {
        private const val LOG_TAG = "BillingRepository"

        private var INSTANCE: Billing? = null
        fun getInstance(mContext: Context, purchaseItem: String): Billing =
            INSTANCE ?: synchronized(this) {
                INSTANCE ?: Billing(mContext, purchaseItem).also { INSTANCE = it }
            }
    }


}
Ryan M
  • 18,333
  • 31
  • 67
  • 74
HelloCW
  • 843
  • 22
  • 125
  • 310
  • In App Purchases may not be refreshed from Google Play as long as its cache is kept. Try removing Google Play's cache and see if the purchase is not coming from `queryPurchases` method, because technically it should not return since it's not an active purchase anymore. – Furkan Yurdakul Aug 10 '20 at 08:56

1 Answers1

1

You could also check purchase.isAcknowledged() on the purchase so that you don't add it to validPurchases anyway, if it's caching issue.

Misca
  • 459
  • 2
  • 11
  • 31
  • Thanks! I have clean cache, the code is still consider `Refunded` order as `Purchase.PurchaseState.PURCHASED`, how can I fix it? – HelloCW Aug 12 '20 at 09:07
  • Even if that's the status, purchase tokens should be validated ( as of "Processing purchases" phase, it's well documented). you would be sending the purchase token your server, if it's not serverless integration, which would validate it and only then you consider it done and acknowledge the purchase. Check this: https://developer.android.com/google/play/billing/security#verify – Misca Aug 12 '20 at 11:56
  • 1
    Thanks! I havn't a server for validate purchase tokens. In my mind, the order has been marked as Refund in Google Play, why will the in-app API treat the order as PURCHASED? – HelloCW Aug 13 '20 at 13:22
  • That's just a state, you should not rely on it for purchase validation, it might never. – Misca Aug 13 '20 at 14:43
  • Check out this phase: https://developer.android.com/google/play/billing/integrate#process – Misca Aug 13 '20 at 14:44