인 앱 결제 로직 수정

versionName 1.8.16, versionCode 41
This commit is contained in:
klaus 2024-03-23 05:37:25 +09:00
parent 6a558ad25c
commit daed389264
6 changed files with 96 additions and 131 deletions

View File

@ -40,8 +40,8 @@ android {
applicationId "kr.co.vividnext.sodalive" applicationId "kr.co.vividnext.sodalive"
minSdk 23 minSdk 23
targetSdk 33 targetSdk 33
versionCode 32 versionCode 41
versionName "1.8.7" versionName "1.8.16"
} }
buildTypes { buildTypes {

View File

@ -3,7 +3,6 @@ package kr.co.vividnext.sodalive.mypage.can
import io.reactivex.rxjava3.core.Single import io.reactivex.rxjava3.core.Single
import kr.co.vividnext.sodalive.common.ApiResponse import kr.co.vividnext.sodalive.common.ApiResponse
import kr.co.vividnext.sodalive.mypage.can.charge.iap.GoogleChargeRequest import kr.co.vividnext.sodalive.mypage.can.charge.iap.GoogleChargeRequest
import kr.co.vividnext.sodalive.mypage.can.charge.iap.GoogleVerifyRequest
import kr.co.vividnext.sodalive.mypage.can.charge.pg.CanResponse import kr.co.vividnext.sodalive.mypage.can.charge.pg.CanResponse
import kr.co.vividnext.sodalive.mypage.can.charge.pg.ChargeRequest import kr.co.vividnext.sodalive.mypage.can.charge.pg.ChargeRequest
import kr.co.vividnext.sodalive.mypage.can.charge.pg.ChargeResponse import kr.co.vividnext.sodalive.mypage.can.charge.pg.ChargeResponse
@ -23,12 +22,6 @@ interface CanApi {
fun googleChargeCan( fun googleChargeCan(
@Body request: GoogleChargeRequest, @Body request: GoogleChargeRequest,
@Header("Authorization") authHeader: String @Header("Authorization") authHeader: String
): Single<ApiResponse<ChargeResponse>>
@POST("/charge/google/verify")
fun googleChargeVerify(
@Body request: GoogleVerifyRequest,
@Header("Authorization") authHeader: String
): Single<ApiResponse<Any>> ): Single<ApiResponse<Any>>
@POST("/charge") @POST("/charge")

View File

@ -1,7 +1,6 @@
package kr.co.vividnext.sodalive.mypage.can package kr.co.vividnext.sodalive.mypage.can
import kr.co.vividnext.sodalive.mypage.can.charge.iap.GoogleChargeRequest import kr.co.vividnext.sodalive.mypage.can.charge.iap.GoogleChargeRequest
import kr.co.vividnext.sodalive.mypage.can.charge.iap.GoogleVerifyRequest
import kr.co.vividnext.sodalive.mypage.can.charge.pg.ChargeRequest import kr.co.vividnext.sodalive.mypage.can.charge.pg.ChargeRequest
import kr.co.vividnext.sodalive.mypage.can.charge.pg.VerifyRequest import kr.co.vividnext.sodalive.mypage.can.charge.pg.VerifyRequest
import kr.co.vividnext.sodalive.mypage.can.coupon.UseCanCouponRequest import kr.co.vividnext.sodalive.mypage.can.coupon.UseCanCouponRequest
@ -13,11 +12,6 @@ class CanRepository(private val api: CanApi) {
token: String token: String
) = api.googleChargeCan(request, authHeader = token) ) = api.googleChargeCan(request, authHeader = token)
fun googleChargeVerify(
request: GoogleVerifyRequest,
token: String
) = api.googleChargeVerify(request, authHeader = token)
fun chargeCan( fun chargeCan(
chargeRequest: ChargeRequest, chargeRequest: ChargeRequest,
token: String token: String

View File

@ -14,6 +14,7 @@ import com.android.billingclient.api.BillingClientStateListener
import com.android.billingclient.api.BillingFlowParams import com.android.billingclient.api.BillingFlowParams
import com.android.billingclient.api.BillingResult import com.android.billingclient.api.BillingResult
import com.android.billingclient.api.ProductDetails import com.android.billingclient.api.ProductDetails
import com.android.billingclient.api.Purchase
import com.android.billingclient.api.PurchasesUpdatedListener import com.android.billingclient.api.PurchasesUpdatedListener
import com.android.billingclient.api.QueryProductDetailsParams import com.android.billingclient.api.QueryProductDetailsParams
import kr.co.vividnext.sodalive.base.BaseFragment import kr.co.vividnext.sodalive.base.BaseFragment
@ -34,31 +35,48 @@ class CanChargeIapFragment : BaseFragment<FragmentCanChargeIapBinding>(
private lateinit var billingClient: BillingClient private lateinit var billingClient: BillingClient
private val handler = Handler(Looper.getMainLooper()) private val handler = Handler(Looper.getMainLooper())
private var selectedProductDetails: ProductDetails? = null
private var chargeId: Long = 0
private var chargeCan: Int = 0
private val purchaseUpdateListener = PurchasesUpdatedListener { billingResult, purchases -> private val purchaseUpdateListener = PurchasesUpdatedListener { billingResult, purchases ->
if (billingResult.responseCode == BillingClient.BillingResponseCode.OK && purchases != null) { if (
for (purchase in purchases) { billingResult.responseCode == BillingClient.BillingResponseCode.OK &&
viewModel.verifyPayment( purchases != null &&
productId = purchase.products[0], selectedProductDetails != null
purchaseToken = purchase.purchaseToken,
chargeId = chargeId
) { ) {
Toast.makeText(requireContext(), "캔이 충전되었습니다", Toast.LENGTH_LONG).show() for (purchase in purchases) {
SharedPreferenceManager.can += chargeCan handlePurchase(purchase)
(requireActivity() as CanChargeActivity).successIapCharge()
}
} }
} else if (billingResult.responseCode == BillingClient.BillingResponseCode.USER_CANCELED) { } else if (billingResult.responseCode == BillingClient.BillingResponseCode.USER_CANCELED) {
chargeId = 0 selectedProductDetails = null
chargeCan = 0 handler.post { showToast("구매를 취소했습니다.") }
showToast("구매를 취소했습니다.")
} else { } else {
chargeId = 0 selectedProductDetails = null
chargeCan = 0 handler.post { showToast("구매를 하지 못했습니다.\n다시 시도해 주세요.") }
showToast("구매를 하지 못했습니다.\n다시 시도해 주세요.") }
}
private fun handlePurchase(purchase: Purchase) {
if (
purchase.purchaseState == Purchase.PurchaseState.PURCHASED &&
!purchase.isAcknowledged
) {
viewModel.chargeCan(
title = selectedProductDetails!!.name,
selectedProductDetails = selectedProductDetails!!,
purchase = purchase
) { chargeCan ->
handler.post {
showToast("캔이 충전되었습니다")
SharedPreferenceManager.can += chargeCan
val activity = requireActivity() as? CanChargeActivity
if (activity != null) {
activity.successIapCharge()
} else {
requireActivity().finish()
}
}
}
} }
} }
@ -80,25 +98,18 @@ class CanChargeIapFragment : BaseFragment<FragmentCanChargeIapBinding>(
loadingDialog.dismiss() loadingDialog.dismiss()
} }
} }
viewModel.toastLiveData.observe(viewLifecycleOwner) {
Toast.makeText(requireActivity(), it, Toast.LENGTH_LONG).show()
}
} }
private fun setupRecyclerView() { private fun setupRecyclerView() {
val recyclerView = binding.rvChargeCan val recyclerView = binding.rvChargeCan
adapter = CanChargeIapAdapter { productDetails -> adapter = CanChargeIapAdapter { productDetails ->
chargeCan = productDetails.description.toInt() selectedProductDetails = productDetails
viewModel.chargeCan(
title = productDetails.name,
chargeCan = chargeCan,
price = (
productDetails.oneTimePurchaseOfferDetails?.priceAmountMicros ?: 0L
).toDouble() / 1000000,
currencyCode = productDetails.oneTimePurchaseOfferDetails?.priceCurrencyCode
?: "KRW"
) { chargeId ->
this.chargeId = chargeId
launchPurchaseFlow(productDetails) launchPurchaseFlow(productDetails)
} }
}
recyclerView.layoutManager = LinearLayoutManager( recyclerView.layoutManager = LinearLayoutManager(
requireContext(), requireContext(),
@ -141,7 +152,7 @@ class CanChargeIapFragment : BaseFragment<FragmentCanChargeIapBinding>(
} }
private fun setupBillingClient() { private fun setupBillingClient() {
billingClient = BillingClient.newBuilder(requireContext()) billingClient = BillingClient.newBuilder(requireActivity())
.enablePendingPurchases() .enablePendingPurchases()
.setListener(purchaseUpdateListener) .setListener(purchaseUpdateListener)
.build() .build()
@ -149,6 +160,7 @@ class CanChargeIapFragment : BaseFragment<FragmentCanChargeIapBinding>(
loadingDialog.show(screenWidth) loadingDialog.show(screenWidth)
billingClient.startConnection(object : BillingClientStateListener { billingClient.startConnection(object : BillingClientStateListener {
override fun onBillingServiceDisconnected() { override fun onBillingServiceDisconnected() {
handler.post { showToast("인 앱 결제 이용이 불가능 합니다. 다시 시도해 주세요.") }
loadingDialog.dismiss() loadingDialog.dismiss()
} }
@ -156,7 +168,7 @@ class CanChargeIapFragment : BaseFragment<FragmentCanChargeIapBinding>(
if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) { if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
queryAvailableCans() queryAvailableCans()
} else { } else {
showToast("인 앱 결제 이용이 불가능 합니다. 다시 시도해 주세요.") handler.post { showToast("인 앱 결제 이용이 불가능 합니다. 다시 시도해 주세요.") }
loadingDialog.dismiss() loadingDialog.dismiss()
} }
} }
@ -165,7 +177,7 @@ class CanChargeIapFragment : BaseFragment<FragmentCanChargeIapBinding>(
@SuppressLint("NotifyDataSetChanged") @SuppressLint("NotifyDataSetChanged")
private fun queryAvailableCans() { private fun queryAvailableCans() {
val skuList = listOf( val productList = listOf(
"${requireContext().packageName}.can_35", "${requireContext().packageName}.can_35",
"${requireContext().packageName}.can_55", "${requireContext().packageName}.can_55",
"${requireContext().packageName}.can_105", "${requireContext().packageName}.can_105",
@ -177,10 +189,10 @@ class CanChargeIapFragment : BaseFragment<FragmentCanChargeIapBinding>(
val params = QueryProductDetailsParams.newBuilder() val params = QueryProductDetailsParams.newBuilder()
.setProductList( .setProductList(
skuList.map { productList.map {
QueryProductDetailsParams.Product.newBuilder() QueryProductDetailsParams.Product.newBuilder()
.setProductId(it) .setProductId(it)
.setProductType(BillingClient.ProductType.INAPP) // Use SUBS for subscriptions .setProductType(BillingClient.ProductType.INAPP)
.build() .build()
} }
) )

View File

@ -2,6 +2,8 @@ package kr.co.vividnext.sodalive.mypage.can.charge.iap
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import com.android.billingclient.api.ProductDetails
import com.android.billingclient.api.Purchase
import com.orhanobut.logger.Logger import com.orhanobut.logger.Logger
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.schedulers.Schedulers import io.reactivex.rxjava3.schedulers.Schedulers
@ -20,61 +22,26 @@ class CanChargeIapViewModel(private val repository: CanRepository) : BaseViewMod
fun chargeCan( fun chargeCan(
title: String, title: String,
chargeCan: Int, selectedProductDetails: ProductDetails,
price: Double, purchase: Purchase,
currencyCode: String, onSuccess: (Int) -> Unit
onSuccess: (Long) -> Unit
) { ) {
val productId = purchase.products.firstOrNull()
if (productId != null) {
_isLoading.value = true _isLoading.value = true
compositeDisposable.add( compositeDisposable.add(
repository.googleChargeCan( repository.googleChargeCan(
request = GoogleChargeRequest( request = GoogleChargeRequest(
title = title, title = title,
chargeCan = chargeCan, chargeCan = selectedProductDetails.description.toInt(),
price = price, price = (
currencyCode = currencyCode selectedProductDetails.oneTimePurchaseOfferDetails?.priceAmountMicros
), ?: 0L).toDouble() / 1000000,
token = "Bearer ${SharedPreferenceManager.token}" currencyCode = selectedProductDetails.oneTimePurchaseOfferDetails?.priceCurrencyCode
) ?: "KRW",
.subscribeOn(Schedulers.io()) productId = purchase.products[0],
.observeOn(AndroidSchedulers.mainThread()) purchaseToken = purchase.purchaseToken
.subscribe(
{
_isLoading.value = false
if (it.success && it.data != null) {
onSuccess(it.data.chargeId)
} else {
if (it.message != null) {
_toastLiveData.value = it.message
} else {
_toastLiveData.value = "알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
}
}
},
{
_isLoading.value = false
it.message?.let { message -> Logger.e(message) }
_toastLiveData.value = "알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
}
)
)
}
fun verifyPayment(
productId: String,
purchaseToken: String,
chargeId: Long,
onSuccess: () -> Unit
) {
_isLoading.value = true
compositeDisposable.add(
repository.googleChargeVerify(
request = GoogleVerifyRequest(
productId = productId,
purchaseToken = purchaseToken,
chargeId = chargeId
), ),
token = "Bearer ${SharedPreferenceManager.token}" token = "Bearer ${SharedPreferenceManager.token}"
) )
@ -84,7 +51,7 @@ class CanChargeIapViewModel(private val repository: CanRepository) : BaseViewMod
{ {
_isLoading.value = false _isLoading.value = false
if (it.success) { if (it.success) {
onSuccess() onSuccess(selectedProductDetails.description.toInt())
} else { } else {
if (it.message != null) { if (it.message != null) {
_toastLiveData.value = it.message _toastLiveData.value = it.message
@ -100,5 +67,8 @@ class CanChargeIapViewModel(private val repository: CanRepository) : BaseViewMod
} }
) )
) )
} else {
_toastLiveData.value = "구매를 하지 못했습니다.\n고객센터로 문의해 주시기 바랍니다."
}
} }
} }

View File

@ -8,11 +8,7 @@ data class GoogleChargeRequest(
@SerializedName("chargeCan") val chargeCan: Int, @SerializedName("chargeCan") val chargeCan: Int,
@SerializedName("price") val price: Double, @SerializedName("price") val price: Double,
@SerializedName("currencyCode") val currencyCode: String, @SerializedName("currencyCode") val currencyCode: String,
@SerializedName("paymentGateway") val paymentGateway: PaymentGateway = PaymentGateway.GOOGLE_IAP
)
data class GoogleVerifyRequest(
@SerializedName("productId") val productId: String, @SerializedName("productId") val productId: String,
@SerializedName("purchaseToken") val purchaseToken: String, @SerializedName("purchaseToken") val purchaseToken: String,
@SerializedName("chargeId") val chargeId: Long @SerializedName("paymentGateway") val paymentGateway: PaymentGateway = PaymentGateway.GOOGLE_IAP
) )