parent
79cb4b995a
commit
6e3a4e1125
|
@ -40,8 +40,8 @@ android {
|
||||||
applicationId "kr.co.vividnext.sodalive"
|
applicationId "kr.co.vividnext.sodalive"
|
||||||
minSdk 23
|
minSdk 23
|
||||||
targetSdk 33
|
targetSdk 33
|
||||||
versionCode 27
|
versionCode 28
|
||||||
versionName "1.8.2"
|
versionName "1.8.3"
|
||||||
}
|
}
|
||||||
|
|
||||||
buildTypes {
|
buildTypes {
|
||||||
|
@ -149,4 +149,7 @@ dependencies {
|
||||||
annotationProcessor 'com.github.bumptech.glide:compiler:4.12.0'
|
annotationProcessor 'com.github.bumptech.glide:compiler:4.12.0'
|
||||||
|
|
||||||
implementation "com.michalsvec:single-row-calednar:1.0.0"
|
implementation "com.michalsvec:single-row-calednar:1.0.0"
|
||||||
|
|
||||||
|
// google in-app-purchase
|
||||||
|
implementation "com.android.billingclient:billing-ktx:6.2.0"
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:tools="http://schemas.android.com/tools">
|
xmlns:tools="http://schemas.android.com/tools">
|
||||||
|
|
||||||
|
<uses-permission android:name="com.android.vending.BILLING" />
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
|
|
|
@ -74,6 +74,7 @@ import kr.co.vividnext.sodalive.mypage.auth.AuthApi
|
||||||
import kr.co.vividnext.sodalive.mypage.auth.AuthRepository
|
import kr.co.vividnext.sodalive.mypage.auth.AuthRepository
|
||||||
import kr.co.vividnext.sodalive.mypage.can.CanApi
|
import kr.co.vividnext.sodalive.mypage.can.CanApi
|
||||||
import kr.co.vividnext.sodalive.mypage.can.CanRepository
|
import kr.co.vividnext.sodalive.mypage.can.CanRepository
|
||||||
|
import kr.co.vividnext.sodalive.mypage.can.charge.iap.CanChargeIapViewModel
|
||||||
import kr.co.vividnext.sodalive.mypage.can.charge.pg.CanChargePgViewModel
|
import kr.co.vividnext.sodalive.mypage.can.charge.pg.CanChargePgViewModel
|
||||||
import kr.co.vividnext.sodalive.mypage.can.coupon.CanCouponViewModel
|
import kr.co.vividnext.sodalive.mypage.can.coupon.CanCouponViewModel
|
||||||
import kr.co.vividnext.sodalive.mypage.can.payment.CanPaymentViewModel
|
import kr.co.vividnext.sodalive.mypage.can.payment.CanPaymentViewModel
|
||||||
|
@ -231,6 +232,7 @@ class AppDI(private val context: Context, isDebugMode: Boolean) {
|
||||||
viewModel { CreatorCommunityWriteViewModel(get()) }
|
viewModel { CreatorCommunityWriteViewModel(get()) }
|
||||||
viewModel { CreatorCommunityModifyViewModel(get()) }
|
viewModel { CreatorCommunityModifyViewModel(get()) }
|
||||||
viewModel { CanCouponViewModel(get()) }
|
viewModel { CanCouponViewModel(get()) }
|
||||||
|
viewModel { CanChargeIapViewModel(get()) }
|
||||||
}
|
}
|
||||||
|
|
||||||
private val repositoryModule = module {
|
private val repositoryModule = module {
|
||||||
|
|
|
@ -2,6 +2,8 @@ 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.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
|
||||||
|
@ -17,6 +19,18 @@ import retrofit2.http.POST
|
||||||
import retrofit2.http.Query
|
import retrofit2.http.Query
|
||||||
|
|
||||||
interface CanApi {
|
interface CanApi {
|
||||||
|
@POST("/charge/google")
|
||||||
|
fun googleChargeCan(
|
||||||
|
@Body request: GoogleChargeRequest,
|
||||||
|
@Header("Authorization") authHeader: String
|
||||||
|
): Single<ApiResponse<ChargeResponse>>
|
||||||
|
|
||||||
|
@POST("/charge/google/verify")
|
||||||
|
fun googleChargeVerify(
|
||||||
|
@Body request: GoogleVerifyRequest,
|
||||||
|
@Header("Authorization") authHeader: String
|
||||||
|
): Single<ApiResponse<Any>>
|
||||||
|
|
||||||
@POST("/charge")
|
@POST("/charge")
|
||||||
fun chargeCan(
|
fun chargeCan(
|
||||||
@Body chargeRequest: ChargeRequest,
|
@Body chargeRequest: ChargeRequest,
|
||||||
|
|
|
@ -1,11 +1,23 @@
|
||||||
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.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
|
||||||
import java.util.TimeZone
|
import java.util.TimeZone
|
||||||
|
|
||||||
class CanRepository(private val api: CanApi) {
|
class CanRepository(private val api: CanApi) {
|
||||||
|
fun googleChargeCan(
|
||||||
|
request: GoogleChargeRequest,
|
||||||
|
token: String
|
||||||
|
) = 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
|
||||||
|
|
|
@ -12,9 +12,11 @@ import kr.co.vividnext.sodalive.base.BaseActivity
|
||||||
import kr.co.vividnext.sodalive.common.Constants
|
import kr.co.vividnext.sodalive.common.Constants
|
||||||
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
|
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
|
||||||
import kr.co.vividnext.sodalive.databinding.ActivityCanChargeBinding
|
import kr.co.vividnext.sodalive.databinding.ActivityCanChargeBinding
|
||||||
|
import kr.co.vividnext.sodalive.mypage.can.charge.iap.CanChargeIapFragment
|
||||||
import kr.co.vividnext.sodalive.mypage.can.charge.pg.CanChargePgFragment
|
import kr.co.vividnext.sodalive.mypage.can.charge.pg.CanChargePgFragment
|
||||||
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.payment.CanPaymentActivity
|
import kr.co.vividnext.sodalive.mypage.can.payment.CanPaymentActivity
|
||||||
|
import kr.co.vividnext.sodalive.mypage.can.status.CanStatusActivity
|
||||||
|
|
||||||
class CanChargeActivity : BaseActivity<ActivityCanChargeBinding>(
|
class CanChargeActivity : BaseActivity<ActivityCanChargeBinding>(
|
||||||
ActivityCanChargeBinding::inflate
|
ActivityCanChargeBinding::inflate
|
||||||
|
@ -39,7 +41,7 @@ class CanChargeActivity : BaseActivity<ActivityCanChargeBinding>(
|
||||||
false
|
false
|
||||||
)
|
)
|
||||||
|
|
||||||
changeFragment(PG_TAG_KEY)
|
changeFragment(IAP_TAG_KEY)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun setupView() {
|
override fun setupView() {
|
||||||
|
@ -56,7 +58,7 @@ class CanChargeActivity : BaseActivity<ActivityCanChargeBinding>(
|
||||||
if (SharedPreferenceManager.isAuth) {
|
if (SharedPreferenceManager.isAuth) {
|
||||||
val tabs = binding.tabs
|
val tabs = binding.tabs
|
||||||
|
|
||||||
tabs.visibility = View.GONE
|
tabs.visibility = View.VISIBLE
|
||||||
tabs.addTab(tabs.newTab().setText("인 앱 결제").setTag(IAP_TAG_KEY))
|
tabs.addTab(tabs.newTab().setText("인 앱 결제").setTag(IAP_TAG_KEY))
|
||||||
tabs.addTab(tabs.newTab().setText("PG").setTag(PG_TAG_KEY))
|
tabs.addTab(tabs.newTab().setText("PG").setTag(PG_TAG_KEY))
|
||||||
|
|
||||||
|
@ -91,7 +93,7 @@ class CanChargeActivity : BaseActivity<ActivityCanChargeBinding>(
|
||||||
fragment = if (tag == PG_TAG_KEY) {
|
fragment = if (tag == PG_TAG_KEY) {
|
||||||
CanChargePgFragment()
|
CanChargePgFragment()
|
||||||
} else {
|
} else {
|
||||||
CanChargePgFragment()
|
CanChargeIapFragment()
|
||||||
}
|
}
|
||||||
|
|
||||||
fragmentTransaction.add(R.id.fl_container, fragment, tag)
|
fragmentTransaction.add(R.id.fl_container, fragment, tag)
|
||||||
|
@ -111,6 +113,16 @@ class CanChargeActivity : BaseActivity<ActivityCanChargeBinding>(
|
||||||
activityResultLauncher.launch(intent)
|
activityResultLauncher.launch(intent)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun successIapCharge() {
|
||||||
|
if (gotoPrevPage) {
|
||||||
|
setResult(RESULT_OK)
|
||||||
|
finish()
|
||||||
|
} else {
|
||||||
|
val intent = Intent(applicationContext, CanStatusActivity::class.java)
|
||||||
|
startActivity(intent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val IAP_TAG_KEY = "iap"
|
const val IAP_TAG_KEY = "iap"
|
||||||
const val PG_TAG_KEY = "pg"
|
const val PG_TAG_KEY = "pg"
|
||||||
|
|
|
@ -0,0 +1,57 @@
|
||||||
|
package kr.co.vividnext.sodalive.mypage.can.charge.iap
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.content.Context
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.core.content.res.ResourcesCompat
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import com.android.billingclient.api.ProductDetails
|
||||||
|
import kr.co.vividnext.sodalive.R
|
||||||
|
import kr.co.vividnext.sodalive.databinding.ItemCanChargeBinding
|
||||||
|
import kr.co.vividnext.sodalive.extensions.fontSpan
|
||||||
|
|
||||||
|
class CanChargeIapAdapter(
|
||||||
|
private val onClick: (ProductDetails) -> Unit
|
||||||
|
) : RecyclerView.Adapter<CanChargeIapAdapter.ViewHolder>() {
|
||||||
|
|
||||||
|
val items = mutableListOf<ProductDetails>()
|
||||||
|
|
||||||
|
inner class ViewHolder(
|
||||||
|
private val context: Context,
|
||||||
|
private val binding: ItemCanChargeBinding
|
||||||
|
) : RecyclerView.ViewHolder(binding.root) {
|
||||||
|
fun bind(item: ProductDetails) {
|
||||||
|
binding.tvPrice.text = item.oneTimePurchaseOfferDetails?.formattedPrice
|
||||||
|
|
||||||
|
val typeface = ResourcesCompat.getFont(context, R.font.gmarket_sans_medium)
|
||||||
|
binding.tvTitle.text = item.name.fontSpan(
|
||||||
|
typeface,
|
||||||
|
"캔"
|
||||||
|
)
|
||||||
|
|
||||||
|
binding.root.setOnClickListener { onClick(item) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ViewHolder(
|
||||||
|
parent.context,
|
||||||
|
ItemCanChargeBinding.inflate(
|
||||||
|
LayoutInflater.from(parent.context),
|
||||||
|
parent,
|
||||||
|
false
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||||
|
holder.bind(items[position])
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getItemCount() = items.count()
|
||||||
|
|
||||||
|
@SuppressLint("NotifyDataSetChanged")
|
||||||
|
fun addItems(items: List<ProductDetails>) {
|
||||||
|
this.items.addAll(items.sortedBy { it.description.toInt() })
|
||||||
|
notifyDataSetChanged()
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,221 @@
|
||||||
|
package kr.co.vividnext.sodalive.mypage.can.charge.iap
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.graphics.Rect
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.os.Handler
|
||||||
|
import android.os.Looper
|
||||||
|
import android.view.View
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import com.android.billingclient.api.BillingClient
|
||||||
|
import com.android.billingclient.api.BillingClientStateListener
|
||||||
|
import com.android.billingclient.api.BillingFlowParams
|
||||||
|
import com.android.billingclient.api.BillingResult
|
||||||
|
import com.android.billingclient.api.ProductDetails
|
||||||
|
import com.android.billingclient.api.PurchasesUpdatedListener
|
||||||
|
import com.android.billingclient.api.QueryProductDetailsParams
|
||||||
|
import kr.co.vividnext.sodalive.base.BaseFragment
|
||||||
|
import kr.co.vividnext.sodalive.common.LoadingDialog
|
||||||
|
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
|
||||||
|
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
|
||||||
|
|
||||||
|
class CanChargeIapFragment : BaseFragment<FragmentCanChargeIapBinding>(
|
||||||
|
FragmentCanChargeIapBinding::inflate
|
||||||
|
) {
|
||||||
|
private val viewModel: CanChargeIapViewModel by inject()
|
||||||
|
|
||||||
|
private lateinit var adapter: CanChargeIapAdapter
|
||||||
|
private lateinit var loadingDialog: LoadingDialog
|
||||||
|
private lateinit var billingClient: BillingClient
|
||||||
|
|
||||||
|
private val handler = Handler(Looper.getMainLooper())
|
||||||
|
|
||||||
|
private var chargeId: Long = 0
|
||||||
|
private var chargeCan: Int = 0
|
||||||
|
|
||||||
|
private val purchaseUpdateListener = PurchasesUpdatedListener { billingResult, purchases ->
|
||||||
|
if (billingResult.responseCode == BillingClient.BillingResponseCode.OK && purchases != null) {
|
||||||
|
for (purchase in purchases) {
|
||||||
|
viewModel.verifyPayment(
|
||||||
|
productId = purchase.products[0],
|
||||||
|
purchaseToken = purchase.purchaseToken,
|
||||||
|
chargeId = chargeId
|
||||||
|
) {
|
||||||
|
SharedPreferenceManager.can += chargeCan
|
||||||
|
(requireActivity() as CanChargeActivity).successIapCharge()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (billingResult.responseCode == BillingClient.BillingResponseCode.USER_CANCELED) {
|
||||||
|
chargeId = 0
|
||||||
|
chargeCan = 0
|
||||||
|
showToast("구매를 취소했습니다.")
|
||||||
|
} else {
|
||||||
|
chargeId = 0
|
||||||
|
chargeCan = 0
|
||||||
|
showToast("구매를 하지 못했습니다.\n다시 시도해 주세요.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
|
||||||
|
loadingDialog = LoadingDialog(requireActivity(), layoutInflater)
|
||||||
|
|
||||||
|
bindData()
|
||||||
|
setupRecyclerView()
|
||||||
|
setupBillingClient()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun bindData() {
|
||||||
|
viewModel.isLoading.observe(viewLifecycleOwner) {
|
||||||
|
if (it) {
|
||||||
|
loadingDialog.show(screenWidth)
|
||||||
|
} else {
|
||||||
|
loadingDialog.dismiss()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setupRecyclerView() {
|
||||||
|
val recyclerView = binding.rvChargeCan
|
||||||
|
adapter = CanChargeIapAdapter { productDetails ->
|
||||||
|
chargeCan = productDetails.description.toInt()
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
recyclerView.layoutManager = LinearLayoutManager(
|
||||||
|
requireContext(),
|
||||||
|
LinearLayoutManager.VERTICAL,
|
||||||
|
false
|
||||||
|
)
|
||||||
|
|
||||||
|
recyclerView.addItemDecoration(object : RecyclerView.ItemDecoration() {
|
||||||
|
override fun getItemOffsets(
|
||||||
|
outRect: Rect,
|
||||||
|
view: View,
|
||||||
|
parent: RecyclerView,
|
||||||
|
state: RecyclerView.State
|
||||||
|
) {
|
||||||
|
super.getItemOffsets(outRect, view, parent, state)
|
||||||
|
|
||||||
|
outRect.left = 13.3f.dpToPx().toInt()
|
||||||
|
outRect.right = 13.3f.dpToPx().toInt()
|
||||||
|
|
||||||
|
when (parent.getChildAdapterPosition(view)) {
|
||||||
|
0 -> {
|
||||||
|
outRect.top = 13.3f.dpToPx().toInt()
|
||||||
|
outRect.bottom = 6.7f.dpToPx().toInt()
|
||||||
|
}
|
||||||
|
|
||||||
|
adapter.itemCount - 1 -> {
|
||||||
|
outRect.top = 6.7f.dpToPx().toInt()
|
||||||
|
outRect.bottom = 26.7f.dpToPx().toInt()
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> {
|
||||||
|
outRect.top = 6.7f.dpToPx().toInt()
|
||||||
|
outRect.bottom = 6.7f.dpToPx().toInt()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
recyclerView.adapter = adapter
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setupBillingClient() {
|
||||||
|
billingClient = BillingClient.newBuilder(requireContext())
|
||||||
|
.enablePendingPurchases()
|
||||||
|
.setListener(purchaseUpdateListener)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
loadingDialog.show(screenWidth)
|
||||||
|
billingClient.startConnection(object : BillingClientStateListener {
|
||||||
|
override fun onBillingServiceDisconnected() {
|
||||||
|
loadingDialog.dismiss()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBillingSetupFinished(billingResult: BillingResult) {
|
||||||
|
if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
|
||||||
|
queryAvailableCans()
|
||||||
|
} else {
|
||||||
|
showToast("인 앱 결제 이용이 불가능 합니다. 다시 시도해 주세요.")
|
||||||
|
loadingDialog.dismiss()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("NotifyDataSetChanged")
|
||||||
|
private fun queryAvailableCans() {
|
||||||
|
val skuList = listOf(
|
||||||
|
"${requireContext().packageName}.can_35",
|
||||||
|
"${requireContext().packageName}.can_55",
|
||||||
|
"${requireContext().packageName}.can_105",
|
||||||
|
"${requireContext().packageName}.can_350",
|
||||||
|
"${requireContext().packageName}.can_550",
|
||||||
|
"${requireContext().packageName}.can_1170"
|
||||||
|
)
|
||||||
|
|
||||||
|
val params = QueryProductDetailsParams.newBuilder()
|
||||||
|
.setProductList(
|
||||||
|
skuList.map {
|
||||||
|
QueryProductDetailsParams.Product.newBuilder()
|
||||||
|
.setProductId(it)
|
||||||
|
.setProductType(BillingClient.ProductType.INAPP) // Use SUBS for subscriptions
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
billingClient.queryProductDetailsAsync(params) { billingResult, productDetailsList ->
|
||||||
|
if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
|
||||||
|
handler.post {
|
||||||
|
adapter.addItems(productDetailsList)
|
||||||
|
productDetailsList.forEach {
|
||||||
|
Toast.makeText(
|
||||||
|
requireContext(),
|
||||||
|
"${it.name}, ${it.description}, ${it.productId}, ${it.productType}, ${
|
||||||
|
it.oneTimePurchaseOfferDetails?.priceAmountMicros
|
||||||
|
}, ${it.oneTimePurchaseOfferDetails?.priceCurrencyCode}",
|
||||||
|
Toast.LENGTH_LONG
|
||||||
|
).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
handler.post { showToast("인 앱 결제 이용이 불가능 합니다. 다시 시도해 주세요.") }
|
||||||
|
}
|
||||||
|
|
||||||
|
handler.post { loadingDialog.dismiss() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun launchPurchaseFlow(productDetails: ProductDetails) {
|
||||||
|
val billingFlowParams = BillingFlowParams.newBuilder()
|
||||||
|
.setProductDetailsParamsList(
|
||||||
|
listOf(
|
||||||
|
BillingFlowParams.ProductDetailsParams.newBuilder()
|
||||||
|
.setProductDetails(productDetails)
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
).build()
|
||||||
|
|
||||||
|
billingClient.launchBillingFlow(requireActivity(), billingFlowParams)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,104 @@
|
||||||
|
package kr.co.vividnext.sodalive.mypage.can.charge.iap
|
||||||
|
|
||||||
|
import androidx.lifecycle.LiveData
|
||||||
|
import androidx.lifecycle.MutableLiveData
|
||||||
|
import com.orhanobut.logger.Logger
|
||||||
|
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||||
|
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||||
|
import kr.co.vividnext.sodalive.base.BaseViewModel
|
||||||
|
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
|
||||||
|
import kr.co.vividnext.sodalive.mypage.can.CanRepository
|
||||||
|
|
||||||
|
class CanChargeIapViewModel(private val repository: CanRepository) : BaseViewModel() {
|
||||||
|
private val _toastLiveData = MutableLiveData<String?>()
|
||||||
|
val toastLiveData: LiveData<String?>
|
||||||
|
get() = _toastLiveData
|
||||||
|
|
||||||
|
private var _isLoading = MutableLiveData(false)
|
||||||
|
val isLoading: LiveData<Boolean>
|
||||||
|
get() = _isLoading
|
||||||
|
|
||||||
|
fun chargeCan(
|
||||||
|
title: String,
|
||||||
|
chargeCan: Int,
|
||||||
|
price: Double,
|
||||||
|
currencyCode: String,
|
||||||
|
onSuccess: (Long) -> Unit
|
||||||
|
) {
|
||||||
|
_isLoading.value = true
|
||||||
|
|
||||||
|
compositeDisposable.add(
|
||||||
|
repository.googleChargeCan(
|
||||||
|
request = GoogleChargeRequest(
|
||||||
|
title = title,
|
||||||
|
chargeCan = chargeCan,
|
||||||
|
price = price,
|
||||||
|
currencyCode = currencyCode
|
||||||
|
),
|
||||||
|
token = "Bearer ${SharedPreferenceManager.token}"
|
||||||
|
)
|
||||||
|
.subscribeOn(Schedulers.io())
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.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}"
|
||||||
|
)
|
||||||
|
.subscribeOn(Schedulers.io())
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.subscribe(
|
||||||
|
{
|
||||||
|
_isLoading.value = false
|
||||||
|
if (it.success) {
|
||||||
|
onSuccess()
|
||||||
|
} else {
|
||||||
|
if (it.message != null) {
|
||||||
|
_toastLiveData.value = it.message
|
||||||
|
} else {
|
||||||
|
_toastLiveData.value = "알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
_isLoading.value = false
|
||||||
|
it.message?.let { message -> Logger.e(message) }
|
||||||
|
_toastLiveData.value = "알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,18 @@
|
||||||
|
package kr.co.vividnext.sodalive.mypage.can.charge.iap
|
||||||
|
|
||||||
|
import com.google.gson.annotations.SerializedName
|
||||||
|
import kr.co.vividnext.sodalive.mypage.can.payment.PaymentGateway
|
||||||
|
|
||||||
|
data class GoogleChargeRequest(
|
||||||
|
@SerializedName("title") val title: String,
|
||||||
|
@SerializedName("chargeCan") val chargeCan: Int,
|
||||||
|
@SerializedName("price") val price: Double,
|
||||||
|
@SerializedName("currencyCode") val currencyCode: String,
|
||||||
|
@SerializedName("paymentGateway") val paymentGateway: PaymentGateway = PaymentGateway.GOOGLE_IAP
|
||||||
|
)
|
||||||
|
|
||||||
|
data class GoogleVerifyRequest(
|
||||||
|
@SerializedName("productId") val productId: String,
|
||||||
|
@SerializedName("purchaseToken") val purchaseToken: String,
|
||||||
|
@SerializedName("chargeId") val chargeId: Long
|
||||||
|
)
|
|
@ -81,16 +81,16 @@ class CanChargePgFragment : BaseFragment<FragmentCanChargePgBinding>(
|
||||||
|
|
||||||
@SuppressLint("NotifyDataSetChanged")
|
@SuppressLint("NotifyDataSetChanged")
|
||||||
private fun bindData() {
|
private fun bindData() {
|
||||||
viewModel.canChargeLiveData.observe(this) {
|
viewModel.canChargeLiveData.observe(viewLifecycleOwner) {
|
||||||
adapter.items.addAll(it)
|
adapter.items.addAll(it)
|
||||||
adapter.notifyDataSetChanged()
|
adapter.notifyDataSetChanged()
|
||||||
}
|
}
|
||||||
|
|
||||||
viewModel.toastLiveData.observe(this) {
|
viewModel.toastLiveData.observe(viewLifecycleOwner) {
|
||||||
it?.let { Toast.makeText(requireActivity(), it, Toast.LENGTH_LONG).show() }
|
it?.let { Toast.makeText(requireActivity(), it, Toast.LENGTH_LONG).show() }
|
||||||
}
|
}
|
||||||
|
|
||||||
viewModel.isLoading.observe(this) {
|
viewModel.isLoading.observe(viewLifecycleOwner) {
|
||||||
if (it) {
|
if (it) {
|
||||||
loadingDialog.show(screenWidth)
|
loadingDialog.show(screenWidth)
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -140,8 +140,8 @@ class CanPaymentActivity : BaseActivity<ActivityCanPaymentBinding>(
|
||||||
R.font.gmarket_sans_bold
|
R.font.gmarket_sans_bold
|
||||||
)
|
)
|
||||||
|
|
||||||
view.setTextColor(ContextCompat.getColor(applicationContext, R.color.color_9970ff))
|
view.setTextColor(ContextCompat.getColor(applicationContext, R.color.color_3bb9f1))
|
||||||
view.setBackgroundResource(R.drawable.bg_round_corner_10_4d9970ff_9970ff)
|
view.setBackgroundResource(R.drawable.bg_round_corner_10_13181b_3bb9f1)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun requestCharge() {
|
private fun requestCharge() {
|
||||||
|
|
|
@ -1,8 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
|
||||||
<solid android:color="@color/color_4d9970ff" />
|
|
||||||
<corners android:radius="10dp" />
|
|
||||||
<stroke
|
|
||||||
android:width="1dp"
|
|
||||||
android:color="@color/color_9970ff" />
|
|
||||||
</shape>
|
|
|
@ -1,8 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
|
||||||
<solid android:color="@color/color_4d9970ff" />
|
|
||||||
<corners android:radius="6.7dp" />
|
|
||||||
<stroke
|
|
||||||
android:width="1.3dp"
|
|
||||||
android:color="@color/color_9970ff" />
|
|
||||||
</shape>
|
|
|
@ -0,0 +1,16 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
|
android:id="@+id/rv_charge_can"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -21,15 +21,6 @@
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
tools:text="5000 캔 + 1000 캔" />
|
tools:text="5000 캔 + 1000 캔" />
|
||||||
|
|
||||||
<LinearLayout
|
|
||||||
android:id="@+id/ll_price"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:orientation="horizontal"
|
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:layout_constraintTop_toTopOf="parent">
|
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/tv_price"
|
android:id="@+id/tv_price"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
|
@ -37,15 +28,8 @@
|
||||||
android:fontFamily="@font/gmarket_sans_bold"
|
android:fontFamily="@font/gmarket_sans_bold"
|
||||||
android:textColor="@color/color_eeeeee"
|
android:textColor="@color/color_eeeeee"
|
||||||
android:textSize="15.3sp"
|
android:textSize="15.3sp"
|
||||||
tools:text="3,300" />
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
<TextView
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
android:layout_width="wrap_content"
|
tools:text="₩3,300" />
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:fontFamily="@font/gmarket_sans_medium"
|
|
||||||
android:text=" 원"
|
|
||||||
android:textColor="@color/color_eeeeee"
|
|
||||||
android:textSize="15.3sp" />
|
|
||||||
|
|
||||||
</LinearLayout>
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
Loading…
Reference in New Issue