PaymentRepository
unknown
kotlin
2 years ago
6.6 kB
2
Indexable
Never
package *.*.*.donate import android.app.Activity import android.content.Context import com.android.billingclient.api.BillingClient import com.android.billingclient.api.BillingClient.BillingResponseCode import com.android.billingclient.api.BillingClient.SkuType import com.android.billingclient.api.BillingClientStateListener import com.android.billingclient.api.BillingFlowParams import com.android.billingclient.api.BillingResult import com.android.billingclient.api.ConsumeParams import com.android.billingclient.api.Purchase import com.android.billingclient.api.Purchase.PurchaseState import com.android.billingclient.api.PurchasesResponseListener import com.android.billingclient.api.SkuDetails import com.android.billingclient.api.SkuDetailsParams import com.android.billingclient.api.consumePurchase import com.android.billingclient.api.querySkuDetails import com.naveensingh.android.screenrecorder.donate.payment.PurchaseResult import kotlinx.coroutines.channels.Channel import kotlin.coroutines.resume import kotlin.coroutines.resumeWithException import kotlin.coroutines.suspendCoroutine /** * Handles core billing & licensing logic. * * @author Naveen Singh (@Naveen3Singh) */ interface PaymentRepository { /** * Starts a connect to Google Play [BillingClient] and returns true * if the connection was successful. */ suspend fun startBillingConnection(): Boolean /** * Returns the list of [SkuDetails] given the [SkuDetailsParams]. */ suspend fun queryProducts(params: SkuDetailsParams): List<SkuDetails> /** * Returns a list of all [Purchase]s by the current user. */ suspend fun queryPurchases(skuType: String = SkuType.INAPP): List<Purchase> /** * Starts a Google Play purchase flow given the [SkuDetails]. */ suspend fun startPurchaseFlow(activity: Activity, skuDetails: SkuDetails): PurchaseResult /** * Consume a purchased item given the [purchaseToken]. */ suspend fun consumePurchase(purchaseToken: String): String /** * Consumes all previous purchases. Returns true if all purchases were consumed. * Returns false if there were no purchases to consume. */ suspend fun consumePreviousPurchases(purchases: List<Purchase>? = null): Boolean } /** @author Naveen Singh (@Naveen3Singh) */ class RealPaymentRepository(context: Context) : PaymentRepository { private val purchaseChannel: Channel<PurchaseResult> = Channel(Channel.UNLIMITED) private var connected = false private val billingClient by lazy { BillingClient.newBuilder(context) .setListener { billingResult, purchases -> purchaseChannel.trySend( element = PurchaseResult( responseCode = billingResult.responseCode, purchases = purchases.orEmpty() ) ) } .enablePendingPurchases() .build() } override suspend fun startBillingConnection(): Boolean { return if (connected) connected else suspendCoroutine { continuation -> billingClient.startConnection(object : BillingClientStateListener { override fun onBillingSetupFinished(billingResult: BillingResult) { if (billingResult.responseCode.isSuccess) { connected = true continuation.resume(true) } else { connected = false continuation.resume(false) } } override fun onBillingServiceDisconnected() { connected = false } }) } } override suspend fun queryProducts(params: SkuDetailsParams): List<SkuDetails> { val skuDetailsResult = billingClient.querySkuDetails(params) val billingResult = skuDetailsResult.billingResult if (billingResult.responseCode.isSuccess) { return skuDetailsResult.skuDetailsList.orEmpty() } else { throw Exception("${billingResult.responseCode}, ${billingResult.debugMessage}") } } override suspend fun queryPurchases(skuType: String): List<Purchase> = suspendCoroutine { continuation -> // setup async listener val purchaseResponseListener = PurchasesResponseListener { billingResult, purchases -> val responseCode = billingResult.responseCode if (billingResult.responseCode.isSuccess) { continuation.resume(value = purchases) } else if (responseCode != BillingResponseCode.USER_CANCELED) { continuation.resumeWithException( exception = Throwable("${billingResult.debugMessage}, ${billingResult.responseCode}") ) } } // fetch purchases billingClient.queryPurchasesAsync(skuType, purchaseResponseListener) } override suspend fun startPurchaseFlow( activity: Activity, skuDetails: SkuDetails, ): PurchaseResult { val params = BillingFlowParams.newBuilder() .setSkuDetails(skuDetails) .build() billingClient.launchBillingFlow(activity, params) return purchaseChannel.receive() } override suspend fun consumePurchase(purchaseToken: String): String { val consumeParams = ConsumeParams.newBuilder() .setPurchaseToken(purchaseToken) .build() val consumeResult = billingClient.consumePurchase(consumeParams) val billingResult = consumeResult.billingResult if (billingResult.responseCode.isSuccess) { return consumeResult.purchaseToken.orEmpty() } else { throw Exception( "Error consuming purchase. Response code: ${billingResult.responseCode}, ${billingResult.debugMessage}" ) } } override suspend fun consumePreviousPurchases(purchases: List<Purchase>?): Boolean { (purchases ?: queryPurchases()) .filter { !it.isAcknowledged } .filter { it.purchaseState == PurchaseState.PURCHASED } .apply { val consume = isNotEmpty() if (consume) { forEach { consumePurchase(it.purchaseToken) } } return consume } } }