Notice
Recent Posts
Recent Comments
Link
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |
Tags
- Dropdown
- RxJava
- retrofit2
- UIPickerView
- RecyclerView
- MotionEvent
- viewpager2
- SWIFT
- Android
- InAppPurchase
- ViewModel
- HTML
- dispatchQueue
- DispatchTouchEvent
- progressbar
- IOS
- 스터디
- MemoList
- web
- Alamofire
- imagepicker
- javascript
- CSS
- SplashFragment
- ayncAfter
- CalendarView
- aab
- 개발
- 실행지연
- Kotlin
Archives
- Today
- Total
멜팅비의 개발 공부
[Android/Kotlin] 안드로이드 인앱 결제(구독상품) 구현 정리 본문
반응형
오늘은 안드로이드 InApp 결제를 구현하기 위해서 공부한 내용과 실습 내용을 정리하려고 한다.
이번에 새로 들어가는 프로젝트의 요구사항 중 하나로 구독형 상품 결제 기능이 포함되어 있어서 이에 대한 기술 검토를 위해
InApp 결제 구현을 접하게 되었는데, 생각보다 어렵지 않고 정리가 잘 된 블로그가 있어서 쉽게 기술 검토 할 수 있었다.
구글 플레이 콘솔에서 설정
1. 구글 플레이 콘솔에 앱 등록
2. 구독 상품 등록 및 내부 테스트 등록
1. BillingClient 라이브러리 추가
dependencies {
// BillingClient Library
implementation "com.android.billingclient:billing:4.0.0"
// Coroutine
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.4.3"
}
2. Manifest에 BILLING Permission 추가
<uses-permission android:name="com.android.vending.BILLING" />
3. BillingManager 모듈 구현
/**
* 결제 Callback 인터페이스
*/
interface BillingCallback {
fun onBillingConnected() // BillingClient 연결 성공 시 호출
fun onSuccess(purchase: Purchase) // 구매 성공 시 호출 Purchase : 구매정보
fun onFailure(responseCode: Int) // 구매 실패 시 호출 errorCode : BillingResponseCode
}
class BillingManager(private val activity: Activity, private val callback: BillingCallback) {
private val purchasesUpdatedListener = PurchasesUpdatedListener { billingResult, purchases ->
if (billingResult.responseCode == BillingClient.BillingResponseCode.OK && purchases != null) {
for (purchase in purchases) {
confirmPurchase(purchase)
}
} else {
callback.onFailure(billingResult.responseCode)
}
}
private val billingClient = BillingClient.newBuilder(activity)
.setListener(purchasesUpdatedListener)
.enablePendingPurchases()
.build()
init {
billingClient.startConnection(object : BillingClientStateListener {
override fun onBillingServiceDisconnected() {
Log.d("BiilingManager", "== BillingClient onBillingServiceDisconnected() called ==")
}
override fun onBillingSetupFinished(billingResult: BillingResult) {
if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
callback.onBillingConnected()
} else {
callback.onFailure(billingResult.responseCode)
}
}
})
}
/**
* 콘솔에 등록한 상품 리스트를 가져온다.
* @param sku 상품 ID String
* @param billingType String IN_APP or SUBS
* @param resultBlock 결과로 받을 상품정보들에 대한 처리
*/
fun getSkuDetails(
vararg sku: String,
billingType: String,
resultBlock: (List<SkuDetails>) -> Unit = {}
) {
val params = SkuDetailsParams.newBuilder()
.setSkusList(sku.asList())
.setType(billingType)
billingClient.querySkuDetailsAsync(params.build()) { _, list ->
CoroutineScope(Dispatchers.Main).launch {
resultBlock(list ?: emptyList())
}
}
}
/**
* 구매 시도
* @param skuDetail SkuDetails 구매 할 상품
*/
fun purchaseSku(skuDetail: SkuDetails) {
val flowParams = BillingFlowParams.newBuilder().apply {
setSkuDetails(skuDetail)
}.build()
val responseCode = billingClient.launchBillingFlow(activity, flowParams).responseCode
if (responseCode != BillingClient.BillingResponseCode.OK) {
callback.onFailure(responseCode)
}
}
/**
* 구독 여부 확인
* @param sku String 구매 확인 상품
* @param resultBlock 구매 확인 상품에 대한 처리 return Purchase
*/
fun checkSubscribed(sku: String, resultBlock: (Purchase?) -> Unit) {
billingClient.queryPurchasesAsync(sku) { _, purchases ->
CoroutineScope(Dispatchers.Main).launch {
for (purchase in purchases) {
if (purchase.isAcknowledged && purchase.purchaseState == Purchase.PurchaseState.PURCHASED) {
return@launch resultBlock(purchase)
}
}
return@launch resultBlock(null)
}
}
}
/**
* 구매 확인
* @param purchase
*/
fun confirmPurchase(purchase: Purchase) {
if (purchase.purchaseState == Purchase.PurchaseState.PURCHASED && !purchase.isAcknowledged) {
// 구매를 완료 했지만 확인이 되지 않은 경우 확인 처리
val ackPurchaseParams = AcknowledgePurchaseParams.newBuilder()
.setPurchaseToken(purchase.purchaseToken)
CoroutineScope(Dispatchers.IO).launch {
billingClient.acknowledgePurchase(ackPurchaseParams.build()) {
CoroutineScope(Dispatchers.Main).launch {
if (it.responseCode == BillingClient.BillingResponseCode.OK) {
callback.onSuccess(purchase)
} else {
callback.onFailure(it.responseCode)
}
}
}
}
}
}
/**
* 구매 확인이 안 된 경우 다시 확인 할 수 있도록
*/
fun onResume(type: String) {
if (billingClient.isReady) {
billingClient.queryPurchasesAsync(type) { _, purchases ->
for (purchase in purchases) {
if (!purchase.isAcknowledged && purchase.purchaseState == Purchase.PurchaseState.PURCHASED) {
confirmPurchase(purchase)
}
}
}
}
}
}
4. Activity에서 결제 프로세스 구현
- activity_main.xml
<?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"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<TextView
android:id="@+id/textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:layout_marginStart="20dp"
android:text="1개월 구독권 : "
android:textColor="@color/black"
android:textSize="20sp"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/tv_state"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="12dp"
android:layout_marginTop="20dp"
android:textSize="20sp"
android:textColor="@color/purple_700"
app:layout_constraintStart_toEndOf="@+id/textView"
app:layout_constraintTop_toTopOf="parent" />
<androidx.appcompat.widget.AppCompatButton
android:id="@+id/btn_purchase"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="20dp"
android:layout_marginEnd="20dp"
android:layout_marginBottom="20dp"
android:background="#237C26"
android:text="1개월 구독 결제하기"
android:textColor="@color/white"
android:textSize="18sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
- MainActivity.kt
class MainActivity : AppCompatActivity() {
private lateinit var manager: BillingManager
val subsItemID = "SUBS_ITEM"
private var mSkuDetails = listOf<SkuDetails>()
set(value) {
field = value
getSkuDetails()
}
private var currentSubscription: Purchase? = null
set(value) {
field = value
updateSubscriptionState()
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
manager = BillingManager(this, object : BillingCallback {
override fun onBillingConnected() {
manager.getSkuDetails(subsItemID, billingType = BillingClient.SkuType.SUBS) { list ->
mSkuDetails = list
}
manager.checkSubscribed(subsItemID) {
currentSubscription = it
}
}
override fun onSuccess(purchase: Purchase) {
currentSubscription = purchase
}
override fun onFailure(responseCode: Int) {
Toast.makeText(
applicationContext,
"구매 도중 오류가 발생하였습니다. (${responseCode})",
Toast.LENGTH_SHORT
).show()
}
})
btn_purchase.setOnClickListener {
mSkuDetails.find { it.sku == subsItemID }?.let { skuDetail ->
manager.purchaseSku(skuDetail)
} ?: also {
Toast.makeText(this, "구매 가능 한 상품이 없습니다.", Toast.LENGTH_LONG).show()
}
}
}
private fun getSkuDetails() {
var info = ""
for (skuDetail in mSkuDetails) {
info += "${skuDetail.title}, ${skuDetail.price} \n"
}
Toast.makeText(this, info, Toast.LENGTH_SHORT).show()
}
@SuppressLint("SetTextI18n")
private fun updateSubscriptionState() {
currentSubscription?.let {
tv_state.text = "구독중: ${it.skus} "
} ?: also {
tv_state.text = "구독권이 없습니다."
}
}
}
SampleApp 결과
반응형
'개발 공부 > [Android 개발]' 카테고리의 다른 글
[Android/Kotlin] RecyclerView Drag&Drop 구현하기2 - GridLayout (0) | 2021.10.07 |
---|---|
[Android/Kotlin] RecyclerView Drag&Drop, Swipe 구현하기 (0) | 2021.09.23 |
[Android/Kotlin] 유용한 Custom Calendar Library 소개 (0) | 2021.09.09 |
[Android/Kotlin] retrofit2 사용 시 Response가 XML인 경우 Convert 하는 방법 (0) | 2021.09.09 |
[Android] RxJava2 관련 스터디 (0) | 2021.08.10 |
Comments