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.**