package com.craigvg.lichun_android.managers import android.app.Activity import android.content.Context import com.android.billingclient.api.* import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import javax.inject.Inject import javax.inject.Singleton /** * Product IDs for in-app purchases */ object ProductIds { const val DIAMOND_PACK_SMALL = "diamond1" const val DIAMOND_PACK_LARGE = "diamond2" } /** * Represents a purchasable product */ data class PurchasableProduct( val productId: String, val title: String, val description: String, val price: String, val diamondReward: Int ) /** * Purchase result states */ sealed class PurchaseResult { object Success : PurchaseResult() object Pending : PurchaseResult() data class Error(val message: String) : PurchaseResult() object Cancelled : PurchaseResult() } /** * Billing Manager for Google Play Billing * Implements real BillingClient with consumable diamond packs. */ @Singleton class BillingManager( private val context: Context ) : PurchasesUpdatedListener { private val _isReady = MutableStateFlow(false) val isReady: StateFlow = _isReady.asStateFlow() private val _products = MutableStateFlow>(emptyList()) val products: StateFlow> = _products.asStateFlow() private val _purchaseResult = MutableStateFlow(null) val purchaseResult: StateFlow = _purchaseResult.asStateFlow() private val _isPurchasing = MutableStateFlow(false) val isPurchasing: StateFlow = _isPurchasing.asStateFlow() private var billingClient: BillingClient? = null private var cachedProductDetails = mutableMapOf() // Callback for notifying server after consume var onPurchaseConsumed: ((productId: String, purchaseToken: String) -> Unit)? = null /** * Initialize billing client and start connection */ fun initialize() { billingClient = BillingClient.newBuilder(context) .setListener(this) .enablePendingPurchases( PendingPurchasesParams.newBuilder() .enableOneTimeProducts() .build() ) .build() billingClient?.startConnection(object : BillingClientStateListener { override fun onBillingSetupFinished(billingResult: BillingResult) { if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) { _isReady.value = true queryProducts() } } override fun onBillingServiceDisconnected() { _isReady.value = false } }) } /** * Query available products from Play Store */ fun queryProducts() { val client = billingClient ?: return val productList = listOf( QueryProductDetailsParams.Product.newBuilder() .setProductId(ProductIds.DIAMOND_PACK_SMALL) .setProductType(BillingClient.ProductType.INAPP) .build(), QueryProductDetailsParams.Product.newBuilder() .setProductId(ProductIds.DIAMOND_PACK_LARGE) .setProductType(BillingClient.ProductType.INAPP) .build() ) val params = QueryProductDetailsParams.newBuilder() .setProductList(productList) .build() client.queryProductDetailsAsync(params) { billingResult, queryProductDetailsResult -> if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) { cachedProductDetails.clear() val purchasableProducts = queryProductDetailsResult.productDetailsList.mapNotNull { details -> cachedProductDetails[details.productId] = details val offerDetails = details.oneTimePurchaseOfferDetails ?: return@mapNotNull null val diamondReward = when (details.productId) { ProductIds.DIAMOND_PACK_SMALL -> 100 ProductIds.DIAMOND_PACK_LARGE -> 550 else -> 0 } PurchasableProduct( productId = details.productId, title = details.title, description = details.description, price = offerDetails.formattedPrice, diamondReward = diamondReward ) } _products.value = purchasableProducts } } } /** * Launch purchase flow for a product */ fun launchPurchase(activity: Activity, productId: String) { val productDetails = cachedProductDetails[productId] if (productDetails == null) { _purchaseResult.value = PurchaseResult.Error("Product not found. Try again later.") return } _isPurchasing.value = true val productDetailsParamsList = listOf( BillingFlowParams.ProductDetailsParams.newBuilder() .setProductDetails(productDetails) .build() ) val billingFlowParams = BillingFlowParams.newBuilder() .setProductDetailsParamsList(productDetailsParamsList) .build() billingClient?.launchBillingFlow(activity, billingFlowParams) } /** * Called by Play Billing when purchase state changes */ override fun onPurchasesUpdated(billingResult: BillingResult, purchases: List?) { _isPurchasing.value = false when (billingResult.responseCode) { BillingClient.BillingResponseCode.OK -> { purchases?.forEach { purchase -> if (purchase.purchaseState == Purchase.PurchaseState.PURCHASED) { consumePurchase(purchase) } else if (purchase.purchaseState == Purchase.PurchaseState.PENDING) { _purchaseResult.value = PurchaseResult.Pending } } } BillingClient.BillingResponseCode.USER_CANCELED -> { _purchaseResult.value = PurchaseResult.Cancelled } else -> { _purchaseResult.value = PurchaseResult.Error( "Purchase failed (code ${billingResult.responseCode}): ${billingResult.debugMessage}" ) } } } /** * Consume a purchase so it can be bought again */ private fun consumePurchase(purchase: Purchase) { val consumeParams = ConsumeParams.newBuilder() .setPurchaseToken(purchase.purchaseToken) .build() billingClient?.consumeAsync(consumeParams) { billingResult, _ -> if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) { val productId = purchase.products.firstOrNull() ?: "" onPurchaseConsumed?.invoke(productId, purchase.purchaseToken) _purchaseResult.value = PurchaseResult.Success } else { _purchaseResult.value = PurchaseResult.Error( "Failed to complete purchase. Please contact support." ) } } } /** * Restore previous purchases — consume any unconsumed INAPP purchases */ fun restorePurchases() { val client = billingClient ?: return val params = QueryPurchasesParams.newBuilder() .setProductType(BillingClient.ProductType.INAPP) .build() client.queryPurchasesAsync(params) { billingResult, purchases -> if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) { purchases.forEach { purchase -> if (purchase.purchaseState == Purchase.PurchaseState.PURCHASED) { consumePurchase(purchase) } } } } } /** * Clear purchase result state */ fun clearPurchaseResult() { _purchaseResult.value = null } /** * End billing connection */ fun endConnection() { billingClient?.endConnection() billingClient = null _isReady.value = false } }