Skip to content

Commit

Permalink
Feat : 인앱 결제 라이브러리 연동
Browse files Browse the repository at this point in the history
  • Loading branch information
DongChyeon committed Jul 28, 2024
1 parent 2141ccd commit 59d205d
Show file tree
Hide file tree
Showing 7 changed files with 222 additions and 10 deletions.
15 changes: 8 additions & 7 deletions app/src/main/java/com/dev/briefing/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,24 +3,25 @@ package com.dev.briefing
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import dagger.hilt.android.AndroidEntryPoint
import store.newsbriefing.app.core.common.util.InAppUtil
import store.newsbriefing.app.core.designsystem.theme.BriefingTheme

@AndroidEntryPoint
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
InAppUtil.initBillingClient(this)

setContent {
BriefingTheme {
BriefingApp()
}
}
}

override fun onResume() {
super.onResume()
InAppUtil.onResume()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ object Config {
targetSdkVersion = 34,
compileSdkVersion = 34,
applicationId = "com.dev.briefing",
versionCode = 1,
versionCode = 9,
versionName = "2.0.0",
nameSpace = "com.dev.briefing"
)
Expand Down
1 change: 1 addition & 0 deletions core/common/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,5 @@ android {

dependencies {
implementation(libs.kotlinx.coroutines.android)
api(libs.billing.client)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
package store.newsbriefing.app.core.common.util

import android.app.Activity
import android.util.Log
import com.android.billingclient.api.AcknowledgePurchaseParams
import com.android.billingclient.api.AcknowledgePurchaseResponseListener
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.Purchase
import com.android.billingclient.api.PurchasesUpdatedListener
import com.android.billingclient.api.QueryProductDetailsParams
import com.android.billingclient.api.QueryPurchasesParams

object InAppUtil : PurchasesUpdatedListener {

private const val TAG = "GooglePayUtil"

private lateinit var billingClient: BillingClient
private var productDetailsList: List<ProductDetails> = mutableListOf()
private lateinit var acknowledgePurchaseResponseListener: AcknowledgePurchaseResponseListener

private val productIdList = listOf("premium")

/**
* Billing Client 초기화
* Google Play 연결
*/
fun initBillingClient(activity: Activity) {
// Billing Client 초기화
billingClient = BillingClient.newBuilder(activity)
.setListener(this)
.enablePendingPurchases()
.build()

// Billing Client 와 Google Play 연결
billingClient.startConnection(object : BillingClientStateListener {
override fun onBillingServiceDisconnected() {
// 연결이 종료될 시 재시도 요망
Log.d(TAG, "연결 실패")
}

override fun onBillingSetupFinished(billingResult: BillingResult) {
if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
// 연결 성공
Log.d(TAG, "연결 성공")
Log.d(TAG, "billingClient.connectionState : ${billingClient.connectionState}")
queryProductDetails(productIdList)
}
}
})

acknowledgePurchaseResponseListener = AcknowledgePurchaseResponseListener { billingResult ->
Log.d(TAG, "billingResult.responseCode : ${billingResult.responseCode}")
if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
Log.d(TAG, "비소비성 제품 구매 성공")
} else {
Log.d(TAG, "비소비성 제품 구매 실패")
}
}
}

/**
* 구매한 상품 조회
* */
fun onResume() {
if (!billingClient.isReady) return

val params = QueryPurchasesParams.newBuilder()
.setProductType(BillingClient.ProductType.SUBS)

billingClient.queryPurchasesAsync(params.build()) { _, purchases ->
Log.d(TAG, "$purchases")

for (purchase in purchases) {
handlePurchase(purchase)
}
}
}

private fun handlePurchase(purchase: Purchase) {
if (purchase.purchaseState == Purchase.PurchaseState.PURCHASED && !purchase.isAcknowledged) {
// 구독이 활성화된 상태이고 아직 인식되지 않은 경우
acknowledgePurchase(purchase)
}
}

private fun acknowledgePurchase(purchase: Purchase) {
val acknowledgePurchaseParams = AcknowledgePurchaseParams.newBuilder()
.setPurchaseToken(purchase.purchaseToken)
.build()

billingClient.acknowledgePurchase(acknowledgePurchaseParams) { billingResult ->
if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
Log.d(TAG, "Purchase acknowledged successfully")
} else {
Log.e(TAG, "Error acknowledging purchase: ${billingResult.debugMessage}")
}
}
}

/**
* 상품 목록 조회
* */
fun queryProductDetails(productIds: List<String>) {
Log.d(TAG, "queryProductDetails")

val productList = ArrayList<QueryProductDetailsParams.Product>()
for (id in productIds) {
productList.add(
QueryProductDetailsParams.Product.newBuilder()
.setProductId(id)
.setProductType(BillingClient.ProductType.SUBS)
.build()
)
}

val queryProductDetailsParams =
QueryProductDetailsParams.newBuilder()
.setProductList(productList)
.build()

billingClient.queryProductDetailsAsync(queryProductDetailsParams) { billingResult, products ->
if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
productDetailsList = products
Log.d(TAG, "$billingResult")
Log.d(TAG, "$productDetailsList")
} else {
Log.e(TAG, "Error querying product details: ${billingResult.debugMessage}")
}
}
}

/**
* 상품 결제 요청
* */
fun getPay(activity: Activity, id: String) {
Log.d(TAG, "getPay : $id")

val list: MutableList<BillingFlowParams.ProductDetailsParams> = mutableListOf()

for (productDetails in productDetailsList) {
if (productDetails.productId == id) {
val subscriptionOfferDetails = productDetails.subscriptionOfferDetails
if (subscriptionOfferDetails != null && subscriptionOfferDetails.isNotEmpty()) {
val offerToken = subscriptionOfferDetails[0].offerToken

val flowProductDetailParams = BillingFlowParams.ProductDetailsParams.newBuilder()
.setProductDetails(productDetails)
.setOfferToken(offerToken)
.build()

list.add(flowProductDetailParams)
}
}
}

val flowParams = BillingFlowParams.newBuilder()
.setProductDetailsParamsList(list)
.build()

val responseCode = billingClient.launchBillingFlow(activity, flowParams).responseCode
Log.d(TAG, responseCode.toString())
Log.d(TAG, BillingClient.BillingResponseCode.OK.toString())
}

/**
* 결제 결과 수신
* */
override fun onPurchasesUpdated(
billingResult: BillingResult,
purchases: MutableList<Purchase>?
) {
if (billingResult.responseCode == BillingClient.BillingResponseCode.OK && purchases != null) {
for (purchase in purchases) {
Log.d(TAG, "onPurchasesUpdated : 구매 성공")
handlePurchase(purchase)
}
} else if (billingResult.responseCode == BillingClient.BillingResponseCode.USER_CANCELED) {
Log.d(TAG, "유저 취소")
} else {
Log.e(TAG, "Error: ${billingResult.debugMessage}")
}
}
}
7 changes: 6 additions & 1 deletion feature/auth/proguard-rules.pro
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,9 @@

# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
#-renamesourcefileattribute SourceFile

-if class androidx.credentials.CredentialManager
-keep class androidx.credentials.playservices.** {
*;
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
package store.newsbriefing.app.feature.setting

import android.app.Activity
import android.content.Context
import android.content.ContextWrapper
import android.content.Intent
import android.net.Uri
import androidx.compose.foundation.background
Expand Down Expand Up @@ -33,6 +36,7 @@ import androidx.compose.ui.unit.sp
import androidx.core.content.ContextCompat.startActivity
import androidx.hilt.navigation.compose.hiltViewModel
import kotlinx.coroutines.flow.collectLatest
import store.newsbriefing.app.core.common.util.InAppUtil
import store.newsbriefing.app.core.designsystem.theme.BriefingTheme
import store.newsbriefing.app.core.designsystem.theme.Pretendard

Expand Down Expand Up @@ -75,6 +79,7 @@ internal fun SettingScreen(
appVersion: String
) {
val context = LocalContext.current
val activity = context.findActivity()

Column(
modifier = Modifier
Expand All @@ -88,7 +93,7 @@ internal fun SettingScreen(

SettingTitle(stringResource(id = R.string.subscription_service))
SettingItem(stringResource(id = R.string.briefing_premium)) {

InAppUtil.getPay(activity, "premium")
}

SettingTitle(stringResource(id = R.string.app_information))
Expand Down Expand Up @@ -274,4 +279,15 @@ private fun AppVersionItem(
)
)
}
}

private fun Context.findActivity(): Activity {
var context = this
while (context is ContextWrapper) {
if (context is Activity) {
return context
}
context = context.baseContext
}
throw IllegalStateException("Activity not found")
}
2 changes: 2 additions & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ googleid = {module ="com.google.android.libraries.identity.googleid:googleid", v
androidx-credentials = { module = "androidx.credentials:credentials", version.ref = "credentialsPlayServicesAuth" }
androidx-credentials-play-services-auth = { module = "androidx.credentials:credentials-play-services-auth", version.ref = "credentialsPlayServicesAuth" }

billing-client = { module = "com.android.billingclient:billing-ktx", version = "7.0.0" }

core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "core-ktx" }
junit = { group = "junit", name = "junit", version.ref = "junit" }
androidx-test-ext-junit = { group = "androidx.test.ext", name = "junit", version.ref = "androidx-test-ext-junit" }
Expand Down

0 comments on commit 59d205d

Please sign in to comment.