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.ProductDetailsResponseListener 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() .build() fun start() { log("BillingManager.start()") if (billingClient.isReady) { log("Billing client already ready") refresh() return } billingClient.startConnection(object : BillingClientStateListener { override fun onBillingSetupFinished(result: BillingResult) { 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() { log("Billing service disconnected - attempting reconnect") // Auto-reconnection handled by billing client, retry connection start() } }) } 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 } // Check if product details feature is supported val featureResult = billingClient.isFeatureSupported(BillingClient.FeatureType.PRODUCT_DETAILS) if (featureResult.responseCode != BillingClient.BillingResponseCode.OK) { logError("PRODUCT_DETAILS feature not supported: ${featureResult.responseCode}") return } val products = listOf( QueryProductDetailsParams.Product.newBuilder() .setProductId(BillingProducts.SUB_REMOVE_ADS_MONTHLY) .setProductType(BillingClient.ProductType.SUBS) .build(), QueryProductDetailsParams.Product.newBuilder() .setProductId(BillingProducts.INAPP_REMOVE_ADS_FOREVER) .setProductType(BillingClient.ProductType.INAPP) .build(), ) val params = QueryProductDetailsParams.newBuilder() .setProductList(products) .build() billingClient.queryProductDetailsAsync(params, ProductDetailsResponseListener { result, detailsList -> if (result.responseCode == BillingClient.BillingResponseCode.OK) { log("Product details loaded: ${detailsList.size} items") _productDetails.value = detailsList.associateBy { it.productId } } else { logError("queryProductDetails failed: ${result.responseCode} ${result.debugMessage}") } }) } 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}") } } } } }