package solutions.tretter.mindmachine.billing 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.ProductDetails import com.android.billingclient.api.QueryProductDetailsResult import com.android.billingclient.api.PendingPurchasesParams import com.android.billingclient.api.Purchase import com.android.billingclient.api.PurchasesResponseListener import com.android.billingclient.api.QueryProductDetailsParams import com.android.billingclient.api.QueryPurchasesParams import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow class BillingManager( context: Context, private val onNoAdsEntitlementChanged: (Boolean) -> Unit, ) { private val appContext = context.applicationContext private val _hasNoAds = MutableStateFlow(false) val hasNoAds: StateFlow = _hasNoAds.asStateFlow() private val _productDetails = MutableStateFlow>(emptyMap()) val productDetails: StateFlow> = _productDetails.asStateFlow() private fun log(message: String) { Log.d("MindMachineBilling", message) } private fun logError(message: String) { Log.e("MindMachineBilling", message) } private val billingClient: BillingClient = BillingClient.newBuilder(appContext) .setListener { _, purchases -> if (purchases != null) { handlePurchases(purchases) } } .enablePendingPurchases( PendingPurchasesParams.newBuilder() .enableOneTimeProducts() .build() ) .build() @Volatile private var isConnecting = false fun start() { log("BillingManager.start() - isReady=${billingClient.isReady}, isConnecting=$isConnecting") if (billingClient.isReady) { log("Billing client already ready") refresh() return } if (isConnecting) { log("Billing client connection already in progress") return } isConnecting = true log("Calling startConnection()") billingClient.startConnection(object : BillingClientStateListener { override fun onBillingSetupFinished(result: BillingResult) { isConnecting = false log("Billing setup finished: ${result.responseCode} ${result.debugMessage}") if (result.responseCode == BillingClient.BillingResponseCode.OK) { refresh() } else { logError("Billing setup failed: ${result.responseCode} ${result.debugMessage}") } } override fun onBillingServiceDisconnected() { isConnecting = false log("Billing service disconnected") } }) } fun refresh() { log("refresh() called") queryProductDetails() queryActivePurchases() } fun launchPurchase(activity: Activity, productId: String) { log("launchPurchase() productId=$productId") val details = _productDetails.value[productId] if (details == null) { log("Product details not found for $productId") return } val productDetailsParams = when (details.productType) { BillingClient.ProductType.SUBS -> { val offerToken = details.subscriptionOfferDetails ?.firstOrNull() ?.offerToken if (offerToken == null) { log("No subscription offer token for $productId") return } BillingFlowParams.ProductDetailsParams.newBuilder() .setProductDetails(details) .setOfferToken(offerToken) .build() } BillingClient.ProductType.INAPP -> { BillingFlowParams.ProductDetailsParams.newBuilder() .setProductDetails(details) .build() } else -> { log("Unknown product type for $productId") return } } val params = BillingFlowParams.newBuilder() .setProductDetailsParamsList(listOf(productDetailsParams)) .build() val result = billingClient.launchBillingFlow(activity, params) if (result.responseCode == BillingClient.BillingResponseCode.OK) { log("launchBillingFlow succeeded") } else { logError("launchBillingFlow failed: ${result.responseCode} ${result.debugMessage}") } } private fun queryProductDetails() { log("queryProductDetails()") if (!billingClient.isReady) { log("Billing client not ready") return } val featureResult = billingClient.isFeatureSupported(BillingClient.FeatureType.PRODUCT_DETAILS) log("PRODUCT_DETAILS support: ${featureResult.responseCode} ${featureResult.debugMessage}") if (featureResult.responseCode != BillingClient.BillingResponseCode.OK) { logError("PRODUCT_DETAILS feature not supported") return } queryProductDetailsForType( productType = BillingClient.ProductType.SUBS, productIds = listOf(BillingProducts.SUB_REMOVE_ADS_MONTHLY) ) queryProductDetailsForType( productType = BillingClient.ProductType.INAPP, productIds = listOf(BillingProducts.INAPP_REMOVE_ADS_FOREVER) ) } private fun queryProductDetailsForType( productType: String, productIds: List ) { try { log("Querying $productType product details for $productIds") val products = productIds.map { productId -> QueryProductDetailsParams.Product.newBuilder() .setProductId(productId) .setProductType(productType) .build() } val params = QueryProductDetailsParams.newBuilder() .setProductList(products) .build() billingClient.queryProductDetailsAsync(params) { billingResult, queryResult -> log("queryProductDetailsAsync($productType) result: ${billingResult.responseCode} ${billingResult.debugMessage}") val detailsList = queryResult.productDetailsList log("Fetched $productType products: ${detailsList?.map { it.productId } ?: emptyList()}") if (queryResult.unfetchedProductList.isNotEmpty()) { logError("Unfetched $productType products: ${queryResult.unfetchedProductList}") } if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) { _productDetails.value = _productDetails.value + (detailsList?.associateBy { it.productId } ?: emptyMap()) log("Product details cache now has: ${_productDetails.value.keys}") } } } catch (t: Throwable) { Log.e("MindMachineBilling", "Exception querying $productType product details", t) } } private fun queryActivePurchases() { log("queryActivePurchases()") if (!billingClient.isReady) { log("Billing client not ready") return } val listener = PurchasesResponseListener { result, purchases -> if (result.responseCode == BillingClient.BillingResponseCode.OK) { log("Query purchases returned ${purchases.size} items") handlePurchases(purchases) } else { logError("Query purchases failed: ${result.responseCode} ${result.debugMessage}") } } billingClient.queryPurchasesAsync( QueryPurchasesParams.newBuilder().setProductType(BillingClient.ProductType.SUBS).build(), listener ) billingClient.queryPurchasesAsync( QueryPurchasesParams.newBuilder().setProductType(BillingClient.ProductType.INAPP).build(), listener ) } private fun handlePurchases(purchases: List) { log("handlePurchases count=${purchases.size}") val active = purchases.any { p -> p.purchaseState == Purchase.PurchaseState.PURCHASED && p.products.any { it == BillingProducts.SUB_REMOVE_ADS_MONTHLY || it == BillingProducts.INAPP_REMOVE_ADS_FOREVER } } log("Active entitlement: $active") if (_hasNoAds.value != active) { log("Entitlement changed: ${_hasNoAds.value} -> $active") _hasNoAds.value = active onNoAdsEntitlementChanged(active) } // Acknowledge where needed purchases.filter { it.purchaseState == Purchase.PurchaseState.PURCHASED && !it.isAcknowledged } .forEach { purchase -> log("Acknowledging purchase: ${purchase.purchaseToken}") val params = AcknowledgePurchaseParams.newBuilder() .setPurchaseToken(purchase.purchaseToken) .build() billingClient.acknowledgePurchase(params) { billingResult -> if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) { log("Purchase acknowledged successfully") } else { logError("Failed to acknowledge purchase: ${billingResult.responseCode} ${billingResult.debugMessage}") } } } } }