package com.freeexile.arpg import android.app.Activity import android.content.Context import android.util.Log import com.android.billingclient.api.AcknowledgePurchaseParams import com.android.billingclient.api.BillingClient import com.android.billingclient.api.BillingClientStateListener import com.android.billingclient.api.BillingFlowParams import com.android.billingclient.api.BillingResult import com.android.billingclient.api.PendingPurchasesParams import com.android.billingclient.api.ProductDetails import com.android.billingclient.api.Purchase import com.android.billingclient.api.PurchasesUpdatedListener import com.android.billingclient.api.QueryProductDetailsParams import com.android.billingclient.api.QueryPurchasesParams import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext /** * Google Play Billing Library 7.0+ Manager. * Orchestrates IAP lifecycle, product querying, secure checkout, and acknowledgement. */ class PlayBillingManager( private val context: Context, private val scope: CoroutineScope, private val listener: BillingEventListener? = null ) : PurchasesUpdatedListener { companion object { private const val TAG = "PlayBillingManager" } interface BillingEventListener { fun onPurchaseSuccess(purchase: Purchase) fun onPurchaseFailed(errorCode: Int, message: String) fun onProductDetailsReady(products: List) } private var billingClient: BillingClient = BillingClient.newBuilder(context) .setListener(this) .enablePendingPurchases( PendingPurchasesParams.newBuilder() .enableOneTimeProducts() .build() ) .build() private val cachedProductDetails = mutableMapOf() init { startBillingConnection() } fun startBillingConnection() { if (billingClient.isReady) return billingClient.startConnection(object : BillingClientStateListener { override fun onBillingSetupFinished(billingResult: BillingResult) { if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) { Log.d(TAG, "Play Billing Client connection established.") queryPurchases() } else { Log.e(TAG, "Billing setup failed with response code: ${billingResult.responseCode}") } } override fun onBillingServiceDisconnected() { Log.w(TAG, "Billing service disconnected. Will attempt reconnection on next operation.") } }) } fun queryProductDetails(productIds: List, onResult: ((List) -> Unit)? = null) { if (!billingClient.isReady) { startBillingConnection() return } val productList = productIds.map { id -> QueryProductDetailsParams.Product.newBuilder() .setProductId(id) .setProductType(BillingClient.ProductType.INAPP) .build() } val params = QueryProductDetailsParams.newBuilder() .setProductList(productList) .build() billingClient.queryProductDetailsAsync(params) { billingResult, productDetailsList -> if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) { for (details in productDetailsList) { cachedProductDetails[details.productId] = details } listener?.onProductDetailsReady(productDetailsList) onResult?.invoke(productDetailsList) } else { Log.e(TAG, "Query product details failed: ${billingResult.debugMessage}") onResult?.invoke(emptyList()) } } } fun launchBillingFlow(activity: Activity, productId: String): Boolean { val productDetails = cachedProductDetails[productId] if (productDetails == null) { Log.e(TAG, "ProductDetails not cached for sku: $productId. Initiating query...") queryProductDetails(listOf(productId)) { list -> val details = list.firstOrNull() if (details != null) { executeBillingFlow(activity, details) } else { listener?.onPurchaseFailed(BillingClient.BillingResponseCode.ITEM_UNAVAILABLE, "Product not found") } } return false } return executeBillingFlow(activity, productDetails) } private fun executeBillingFlow(activity: Activity, productDetails: ProductDetails): Boolean { val productDetailsParamsList = listOf( BillingFlowParams.ProductDetailsParams.newBuilder() .setProductDetails(productDetails) .build() ) val billingFlowParams = BillingFlowParams.newBuilder() .setProductDetailsParamsList(productDetailsParamsList) .build() val billingResult = billingClient.launchBillingFlow(activity, billingFlowParams) return billingResult.responseCode == BillingClient.BillingResponseCode.OK } override fun onPurchasesUpdated(billingResult: BillingResult, purchases: List?) { if (billingResult.responseCode == BillingClient.BillingResponseCode.OK && purchases != null) { for (purchase in purchases) { handlePurchase(purchase) } } else if (billingResult.responseCode == BillingClient.BillingResponseCode.USER_CANCELED) { listener?.onPurchaseFailed(billingResult.responseCode, "User cancelled purchase.") } else { listener?.onPurchaseFailed(billingResult.responseCode, billingResult.debugMessage) } } private fun handlePurchase(purchase: Purchase) { if (purchase.purchaseState == Purchase.PurchaseState.PURCHASED) { if (!purchase.isAcknowledged) { val acknowledgePurchaseParams = AcknowledgePurchaseParams.newBuilder() .setPurchaseToken(purchase.purchaseToken) .build() billingClient.acknowledgePurchase(acknowledgePurchaseParams) { billingResult -> if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) { Log.d(TAG, "Purchase acknowledged successfully: ${purchase.orderId}") scope.launch(Dispatchers.Main) { listener?.onPurchaseSuccess(purchase) } } else { Log.e(TAG, "Failed to acknowledge purchase: ${billingResult.debugMessage}") } } } else { scope.launch(Dispatchers.Main) { listener?.onPurchaseSuccess(purchase) } } } } fun queryPurchases() { val params = QueryPurchasesParams.newBuilder() .setProductType(BillingClient.ProductType.INAPP) .build() billingClient.queryPurchasesAsync(params) { billingResult, purchases -> if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) { for (purchase in purchases) { handlePurchase(purchase) } } } } fun destroy() { if (billingClient.isReady) { billingClient.endConnection() } } }