PaymentRepository

mail@pastecode.io avatar
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
            }
    }
}