fix(iap): 인 앱 결제 라이브러리 버전 8.0.0 적용, 결제 보완사항 적용 — 즉시 소비, ITEM_ALREADY_OWNED 처리, obfuscatedAccountId 설정
- 구매 성공 직후 consume 처리하여 재구매 불가(ITEM_ALREADY_OWNED) 이슈 완화 - ITEM_ALREADY_OWNED 응답 시 미소비 구매 자동 정리 및 안내 메시지 - BillingFlowParams에 obfuscatedAccountId 설정으로 계정 연계 강화 - 서비스 연결 문제에 대한 사용자 메시지 보강
This commit is contained in:
@@ -174,7 +174,7 @@ dependencies {
|
||||
annotationProcessor 'com.github.bumptech.glide:compiler:5.0.5'
|
||||
|
||||
// google in-app-purchase
|
||||
implementation "com.android.billingclient:billing-ktx:6.2.0"
|
||||
implementation "com.android.billingclient:billing-ktx:8.0.0"
|
||||
|
||||
// ROOM
|
||||
ksp "androidx.room:room-compiler:2.7.0"
|
||||
|
||||
@@ -5,6 +5,7 @@ import android.graphics.Rect
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.util.Base64
|
||||
import android.view.View
|
||||
import android.widget.Toast
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
@@ -14,6 +15,7 @@ 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.PendingPurchasesParams
|
||||
import com.android.billingclient.api.ProductDetails
|
||||
import com.android.billingclient.api.Purchase
|
||||
import com.android.billingclient.api.PurchasesUpdatedListener
|
||||
@@ -26,6 +28,7 @@ import kr.co.vividnext.sodalive.databinding.FragmentCanChargeIapBinding
|
||||
import kr.co.vividnext.sodalive.extensions.dpToPx
|
||||
import kr.co.vividnext.sodalive.mypage.can.charge.CanChargeActivity
|
||||
import org.koin.android.ext.android.inject
|
||||
import java.security.MessageDigest
|
||||
|
||||
class CanChargeIapFragment : BaseFragment<FragmentCanChargeIapBinding>(
|
||||
FragmentCanChargeIapBinding::inflate
|
||||
@@ -120,29 +123,51 @@ class CanChargeIapFragment : BaseFragment<FragmentCanChargeIapBinding>(
|
||||
private fun setupBillingClient() {
|
||||
purchaseUpdateListener = PurchasesUpdatedListener { billingResult, purchases ->
|
||||
handler.post {
|
||||
if (
|
||||
billingResult.responseCode == BillingClient.BillingResponseCode.OK &&
|
||||
purchases != null &&
|
||||
selectedProductDetails != null
|
||||
) {
|
||||
for (purchase in purchases) {
|
||||
handlePurchase(purchase)
|
||||
when (billingResult.responseCode) {
|
||||
BillingClient.BillingResponseCode.OK -> {
|
||||
if (purchases != null && selectedProductDetails != null) {
|
||||
for (purchase in purchases) {
|
||||
handlePurchase(purchase)
|
||||
}
|
||||
} else {
|
||||
selectedProductDetails = null
|
||||
viewModel.showToast("구매 정보를 확인할 수 없습니다. 다시 시도해 주세요.")
|
||||
}
|
||||
}
|
||||
|
||||
BillingClient.BillingResponseCode.USER_CANCELED -> {
|
||||
selectedProductDetails = null
|
||||
viewModel.showToast("구매를 취소했습니다.")
|
||||
}
|
||||
|
||||
BillingClient.BillingResponseCode.ITEM_ALREADY_OWNED -> {
|
||||
selectedProductDetails = null
|
||||
queryAndConsumeUnconsumedPurchases()
|
||||
viewModel.showToast("이전에 완료되지 않은 구매를 정리했습니다. 다시 시도해 주세요.")
|
||||
}
|
||||
|
||||
BillingClient.BillingResponseCode.SERVICE_UNAVAILABLE,
|
||||
BillingClient.BillingResponseCode.SERVICE_DISCONNECTED -> {
|
||||
selectedProductDetails = null
|
||||
viewModel.showToast("결제 서비스 연결이 원활하지 않습니다. 네트워크 상태를 확인 후 다시 시도해 주세요.")
|
||||
}
|
||||
|
||||
else -> {
|
||||
selectedProductDetails = null
|
||||
viewModel.showToast("구매를 하지 못했습니다. 다시 시도해 주세요.")
|
||||
}
|
||||
} else if (
|
||||
billingResult.responseCode == BillingClient.BillingResponseCode.USER_CANCELED
|
||||
) {
|
||||
selectedProductDetails = null
|
||||
viewModel.showToast("구매를 취소했습니다.")
|
||||
} else {
|
||||
selectedProductDetails = null
|
||||
viewModel.showToast("구매를 하지 못했습니다.\n다시 시도해 주세요.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
billingClient = BillingClient.newBuilder(requireActivity())
|
||||
.enablePendingPurchases()
|
||||
.setListener(purchaseUpdateListener)
|
||||
.enablePendingPurchases(
|
||||
PendingPurchasesParams.newBuilder()
|
||||
.enableOneTimeProducts()
|
||||
.build()
|
||||
)
|
||||
.enableAutoServiceReconnection()
|
||||
.build()
|
||||
|
||||
loadingDialog.show(screenWidth)
|
||||
@@ -187,9 +212,9 @@ class CanChargeIapFragment : BaseFragment<FragmentCanChargeIapBinding>(
|
||||
)
|
||||
.build()
|
||||
|
||||
billingClient.queryProductDetailsAsync(params) { billingResult, productDetailsList ->
|
||||
billingClient.queryProductDetailsAsync(params) { billingResult, result ->
|
||||
if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
|
||||
handler.post { adapter.addItems(productDetailsList) }
|
||||
handler.post { adapter.addItems(result.productDetailsList) }
|
||||
} else {
|
||||
viewModel.showToast("인 앱 결제 이용이 불가능 합니다. 다시 시도해 주세요.")
|
||||
}
|
||||
@@ -239,28 +264,43 @@ class CanChargeIapFragment : BaseFragment<FragmentCanChargeIapBinding>(
|
||||
purchase.purchaseState == Purchase.PurchaseState.PURCHASED &&
|
||||
!purchase.isAcknowledged
|
||||
) {
|
||||
val currentProduct = selectedProductDetails ?: return
|
||||
// 서버 검증/지급 먼저 수행
|
||||
viewModel.chargeCan(
|
||||
title = selectedProductDetails!!.name,
|
||||
selectedProductDetails = selectedProductDetails!!,
|
||||
title = currentProduct.name,
|
||||
selectedProductDetails = currentProduct,
|
||||
purchase = purchase
|
||||
) { chargeCan ->
|
||||
handler.post {
|
||||
viewModel.showToast("캔이 충전되었습니다")
|
||||
SharedPreferenceManager.can += chargeCan
|
||||
|
||||
if (activity != null) {
|
||||
if (activity as? CanChargeActivity != null) {
|
||||
(activity as CanChargeActivity).successIapCharge()
|
||||
} else {
|
||||
requireActivity().finish()
|
||||
}
|
||||
// 지급 성공 → 즉시 소비 처리 후 UI 업데이트
|
||||
if (!purchase.isAcknowledged) {
|
||||
consumePurchase(purchase) {
|
||||
onPurchaseConsumed(chargeCan)
|
||||
}
|
||||
} else {
|
||||
onPurchaseConsumed(chargeCan)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun onPurchaseConsumed(chargeCan: Int) {
|
||||
handler.post {
|
||||
selectedProductDetails = null
|
||||
viewModel.showToast("캔이 충전되었습니다")
|
||||
SharedPreferenceManager.can += chargeCan
|
||||
|
||||
if (activity != null) {
|
||||
if (activity as? CanChargeActivity != null) {
|
||||
(activity as CanChargeActivity).successIapCharge()
|
||||
} else {
|
||||
requireActivity().finish()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun launchPurchaseFlow(productDetails: ProductDetails) {
|
||||
val obfuscatedAccountId = obfuscateAccountId(SharedPreferenceManager.userId)
|
||||
val billingFlowParams = BillingFlowParams.newBuilder()
|
||||
.setProductDetailsParamsList(
|
||||
listOf(
|
||||
@@ -268,8 +308,23 @@ class CanChargeIapFragment : BaseFragment<FragmentCanChargeIapBinding>(
|
||||
.setProductDetails(productDetails)
|
||||
.build()
|
||||
)
|
||||
).build()
|
||||
)
|
||||
.setObfuscatedAccountId(obfuscatedAccountId)
|
||||
.build()
|
||||
|
||||
billingClient.launchBillingFlow(requireActivity(), billingFlowParams)
|
||||
}
|
||||
|
||||
private fun obfuscateAccountId(userId: Long): String {
|
||||
return try {
|
||||
val md = MessageDigest.getInstance("SHA-256")
|
||||
val digest = md.digest("voiceon_user_$userId".toByteArray(Charsets.UTF_8))
|
||||
Base64.encodeToString(digest, Base64.NO_WRAP)
|
||||
} catch (e: Exception) {
|
||||
Base64.encodeToString(
|
||||
"voiceon_user_$userId".toByteArray(Charsets.UTF_8),
|
||||
Base64.NO_WRAP
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user