Seize the day

POST : Android Dev Study

Billing library 적용

https://developer.android.com/google/play/billing/billing_library_overview?hl=en 를 참조해서 체크 리스트 앱의  인앱 결재를 다시 작성해보았다. 기존 IabHelper 관련코드의 제거는 https://developer.android.com/google/play/billing/migrate를 참고하면된다. 

package com.mdiwebma.tasks.billing

import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleObserver
import androidx.lifecycle.OnLifecycleEvent
import com.android.billingclient.api.AcknowledgePurchaseParams
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.Purchase
import com.android.billingclient.api.Purchase.PurchaseState
import com.android.billingclient.api.PurchasesUpdatedListener
import com.android.billingclient.api.SkuDetails
import com.android.billingclient.api.SkuDetailsParams
import com.mdiwebma.base.ActionListener
import com.mdiwebma.base.BaseActivity
import com.mdiwebma.base.task.EventBus
import com.mdiwebma.base.utils.ToastUtils
import com.mdiwebma.tasks.BuildConfig
import com.mdiwebma.tasks.R
import java.util.concurrent.TimeUnit

class BillingHelper(private val activity: BaseActivity) : PurchasesUpdatedListener,
    LifecycleObserver {

    companion object {
        private const val ITEM_TEST_PURCHASE = "android.test.purchased"
        private const val ITEM_REMOVE_ADS = "android.remove_ads"
    }

    private val billingClient: BillingClient = BillingClient.newBuilder(activity.applicationContext)
        .setListener(this)
        .enablePendingPurchases()
        .build()
    private val purchaseItem: String =
        if (BuildConfig.DEBUG) ITEM_TEST_PURCHASE else ITEM_REMOVE_ADS
    private var cachedSkuDetails: SkuDetails? = null

    init {
        activity.lifecycle.addObserver(this)
    }

    private fun runOnConnection(showErrorToast: Boolean = true, action: ActionListener) {
        if (billingClient.isReady) {
            action.invoke()
            return
        }

        billingClient.startConnection(object : BillingClientStateListener {
            override fun onBillingServiceDisconnected() {
            }

            override fun onBillingSetupFinished(billingResult: BillingResult) {
                if (billingResult.responseCode == BillingResponseCode.OK) {
                    action.invoke()
                } else if (showErrorToast) {
                    showErrorToast(billingResult)
                }
            }
        })
    }

    private fun showErrorToast(billingResult: BillingResult?) {
        val message =
            activity.getString(R.string.error_unknown) + " (code: ${billingResult?.responseCode} ${billingResult?.debugMessage})"
        ToastUtils.show(message)
    }

    // 인앱 구매 페이지를 호출한다.
    fun requestPurchase() {
        runOnConnection {
            if (activity.isFinishingDestroyed) return@runOnConnection

            queryPurchaseSkuDetails()
        }
    }

    private fun queryPurchaseSkuDetails() {
        if (cachedSkuDetails != null) {
            launchBillingFlow(cachedSkuDetails)
            return
        }

        val skuDetailsParams = SkuDetailsParams.newBuilder()
            .setSkusList(listOf(purchaseItem))
            .setType(SkuType.INAPP)
            .build()

        billingClient.querySkuDetailsAsync(skuDetailsParams) { billingResult, skuDetailsList ->
            if (activity.isFinishingDestroyed) return@querySkuDetailsAsync

            if (billingResult?.responseCode == BillingResponseCode.OK &&
                skuDetailsList?.isNotEmpty() == true
            ) {
                cachedSkuDetails = skuDetailsList[0]
                launchBillingFlow(cachedSkuDetails)
            } else {
                showErrorToast(billingResult)
            }
        }
    }

    private fun launchBillingFlow(skuDetails: SkuDetails?) {
        skuDetails ?: return
        val flowParams = BillingFlowParams.newBuilder()
            .setSkuDetails(skuDetails)
            .build()
        val billingResult = billingClient.launchBillingFlow(activity, flowParams)

        if (billingResult.responseCode != BillingResponseCode.OK) {
            if (billingResult.responseCode == BillingResponseCode.ITEM_ALREADY_OWNED) {
                checkRefundPurchaseItem(showDoneToast = true)
                return
            }
            showErrorToast(billingResult)
        }
    }

    override fun onPurchasesUpdated(
        billingResult: BillingResult?,
        purchaseList: MutableList<Purchase>?
    ) {
        if (billingResult?.responseCode != BillingResponseCode.OK) return
        purchaseList ?: return

        for (purchase in purchaseList) {
            if (purchase.sku == purchaseItem) {
                if (purchase.purchaseState == PurchaseState.PURCHASED) {
                    maybeAcknowledgePurchase(purchase)
                }
                setHasRemovedAdsItem(purchase.purchaseState == PurchaseState.PURCHASED)
            }
        }
    }

    private fun maybeAcknowledgePurchase(purchase: Purchase) {
        if (!purchase.isAcknowledged) {
            val acknowledgePurchaseParams = AcknowledgePurchaseParams.newBuilder()
                .setPurchaseToken(purchase.purchaseToken)
                .build()

            billingClient.acknowledgePurchase(acknowledgePurchaseParams) { billingResult ->
                if (billingResult.responseCode != BillingResponseCode.OK) {
                    // 환불 체크 타임을 뒤로 돌려서, 1시간이 지나면 다시 checkRefundPurchaseItem를 호출해서 다시 시도한다.
                    BillingSettings.checkRefundPurchaseTime.value =
                        System.currentTimeMillis() - TimeUnit.HOURS.toMillis(23)
                }
            }
        }
    }

    private fun setHasRemovedAdsItem(hasRemovedAdsItem: Boolean) {
        BillingSettings.hasRemovedAdsItem.value = hasRemovedAdsItem

        if (hasRemovedAdsItem) {
            EventBus.getDefault().post(PurchaseCompletedEvent())
        }
    }

    // 24시간 마다 한 번 호출하여 환불을 체크한다.
    fun checkRefundPurchaseItem(showDoneToast: Boolean = false) {
        runOnConnection(showErrorToast = false) {
            val purchaseResult = billingClient.queryPurchases(SkuType.INAPP)

            if (purchaseResult.billingResult.responseCode == BillingResponseCode.OK &&
                purchaseResult.purchasesList != null
            ) {
                var hasRemovedAdsItem = false
                for (purchase in purchaseResult.purchasesList) {
                    if (purchase.sku == purchaseItem &&
                        purchase.purchaseState == PurchaseState.PURCHASED
                    ) {
                        maybeAcknowledgePurchase(purchase)
                        hasRemovedAdsItem = true
                        break
                    }
                }
                setHasRemovedAdsItem(hasRemovedAdsItem)
            }

            if (showDoneToast) ToastUtils.show(R.string.done)
        }
    }

    @OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
    fun onDestroy() {
        if (billingClient.isReady) {
            billingClient.endConnection()
        }
    }
}

 

public 함수는 인앱구매 페이지를 호출하는 requestPurchase()와, 하루에 1번씩 호출해서 환불했는지를 체크하는 checkRefundPurchaseItem() 함수 두 개다. acknowledgePurchase(소비성 결재라면 consumeAsync)를 호출해서 구매를 완료시킨다. 아직 리얼에 적용전이라 다른 문제가 있을지도 모른다.

build.gradle

implementation 'com.android.billingclient:billing:2.2.0'

 

proguard.pro

-keep class com.android.vending.billing.**
top

posted at

2020. 4. 26. 22:57


CONTENTS

Seize the day
BLOG main image
김대정의 앱 개발 노트와 사는 이야기
RSS 2.0Tattertools
공지
아카이브
최근 글 최근 댓글
카테고리 태그 구름사이트 링크