본문 바로가기
Android/안드로이드 스터디(Kotlin)

Android, Kotlin] 구글 인앱결제 적용하기(1회성 소모결제)

by 김마리님 2022. 5. 14.

https://itstudy-mary.tistory.com/400

 

Android Studio, Java] 구글 인앱결제 적용하기(1회성 소모결제)

Kotiln 구현은 이쪽으로 << https://itstudy-mary.tistory.com/399?category=961265 Android, Kotlin] 구글 인앱결제 적용하기(1회성 소모결제) (자바도 쓰긴 할건데 언제가 될진) 6/1일부터 구글에서 결제 우회하..

itstudy-mary.tistory.com

자바는 이쪽 <<

 

 

6/1일부터 구글에서 결제 우회하면 앱을 내릴거라는 경고가 왔다고, 급히 인앱결제를 추가해달라는 요청이 왔다.

근데 여기서 주의해야할 점이 있다. 왜냐면 최신 billing libaray는 5.0.0인데 한국어 docs에는 4.1.0 버전으로 아직 업데이트가 안 되어있기 때문이다 ^-^ ...

 

그런고로 5.0.0 기준으로 해당 포스팅을 작성한다.

그럼 ver.4.0 과 ver.5.0의 기준이 무엇이냐...... 많이 다르다;

기존의 결제 가능 객체를 부르고 저장할때 썼던 sku -  부분이 죄다 product - 로 변경되면서 매서드 이름이 죄다 변경되었으며, 사용하는 flow의 빌드 요구 객체 자료형도 많이 달라졌다. 천천히 봅시다.

 

(해당 포스팅은 안드로이드와 코틀린을 어느정도 해본 사람을 대상으로 합니다.)

(해당 포스팅에서의 DlogUtil은 제가 애용하는 커스텀 logcat용 오브젝트입니다. logd와 유사합니다.)

 

먼저 종속성을 다음과 같이 추가해줍시다.

    //구글 결제서비스
    implementation("com.android.billingclient:billing-ktx:5.0.0")

    //코루틴
    implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.0'

    //Immutable Collection
    // https://mvnrepository.com/artifact/org.jetbrains.kotlinx/kotlinx-collections-immutable
    runtimeOnly 'org.jetbrains.kotlinx:kotlinx-collections-immutable:0.3.5'

    // https://mvnrepository.com/artifact/com.google.guava/guava
    implementation 'com.google.guava:guava:31.1-android'

 

 

전역변수를 다음과 같이 미리 지정한다. 테스트 코드라 급하게 짰기 때문에 리팩터링은 아직 안 했다... 더 깔끔하게 짤 수 있다면 끝까지 읽고 변수를 좀 더 깔끔하게 지정할 수도 있을 것이다.

    private lateinit var billingCilent: BillingClient
    private var skuDetailsList: List<SkuDetails> = mutableListOf()
    private var productDetailsList: List<ProductDetails> = mutableListOf()
    private lateinit var consumeListenser : ConsumeResponseListener

 

그리고 상속을 추가한다. 이후 결제완료 콜백 리스너를 오버라이딩 하기 위해서이다.

class MainActivity : AppCompatActivity(), PurchasesUpdatedListene

 

 

먼저 통신을 지원하는 인터페이스인 billingClient를 초기화하고 구글 결제 서버와 연결한다.

이 작업은 view가 create되는 시점에 해도 무방하다.

연결을 성공했따면 querySkuDetail이라는, 결제가능 목록 리스트를 호출할 것이다.

    private fun initView() {
        textViewOneTimePayment = findViewById(R.id.textViewOneTimePayment)
    }

    /**
     * Billing Client 초기화 ->
     * BliiingClient : 결제 라이브러리 통신 인터페이스
     */
    private fun initBillingClient() {
        billingCilent = BillingClient.newBuilder(this)
            .setListener(this)
            .enablePendingPurchases()
            .build()

        billingCilent.startConnection(object : BillingClientStateListener {
            override fun onBillingServiceDisconnected() {
                //연결이 종료될 시 재시도 요망
                DlogUtil.d(TAG, "연결 실패")
            }

            override fun onBillingSetupFinished(billingResult: BillingResult) {
                if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
                    // 연결 성공
                    DlogUtil.d(TAG, "연결 성공")
                    //Suspend 함수는 반드시 코루틴 내부에서 실행
                    CoroutineScope(Dispatchers.Main).launch {
                        querySkuDetails()
                    }

                }
            }

        })

    }

 

코루틴을 쓴 이유는 서버에서 온 연결 성공을 받고, 그 결과값이 와야 querySkuDetail을 사용하기 위해서이다.

그럼 구매 가능 목록을 호출해보자.

    /**
     * 구매 가능 목록 호출
     * 필요하다면 API 구분 필요
     * */
    suspend fun querySkuDetails() {
        DlogUtil.d(TAG, "querySkuDetails")

        //5.0 마이그레이션
        val tempParam = QueryProductDetailsParams.newBuilder()
            .setProductList(
                ImmutableList.of(
                    QueryProductDetailsParams.Product.newBuilder()
                        .setProductId("test2")
                        .setProductType(BillingClient.ProductType.INAPP)
                        .build()
                )
            ).build()


        billingCilent.queryProductDetailsAsync(tempParam) { billingResult, mutableList ->
            productDetailsList = mutableList
        }

    }

 

ImmutableList는 구글에서 제공하는 불변 컬렉션이다. 라이브러리가 따로 존재하며, 종속성을 추가해야한다.

productId에서 인앱결제시에 등록했던 id를 set한다.

productType는 인앱결제 목록을 불러올건지, 구독목록을 불러올건지에 대한 타입이다. 여기서 구현하는건 1회성 소모아이템이므로 INAPP을 선택한다.

이후에 빌드된 param으로 queryProductDetailsAsync를 호출한다. billingResult = 0 으로 정상적으로 호출됐다면 mutableList에 구매가능 목록이 호출된다.

(이 작업이 뷰 생성과 비동기로 이루어지므로, 아마 progressbar을 넣거나 해서 목록이 호출되기 전까지는 클릭을 지양하도록 하는 것이 좋을 것 같다.)

 


이제 버튼을 눌렀을때의 결제 시도 프로세스를 확인하자.

            var flowProductDetailParams = BillingFlowParams.ProductDetailsParams.newBuilder()
                .setProductDetails(productDetailsList[0])
                .build()

            var flowParams = BillingFlowParams.newBuilder()
                .setProductDetailsParamsList(listOf(flowProductDetailParams))
                .build()

            val responseCode = billingCilent.launchBillingFlow(this, flowParams).responseCode
            DlogUtil.d(TAG, responseCode)
            DlogUtil.d(TAG, BillingClient.BillingResponseCode.OK)
        }

flowDetailParams를 통해 먼저 BillingFlowParams.ProductDetailParams 객체를 만들어야 한다.. 왜냐면..

BillingFlowParams는 setProductDetailsParamsList 만 제공하고, 여기에서 요구하는 객체는 List<BillingFlowParams.ProductDetailParams>이기 때문이다.. 따라서 우리는 결제목록을 불러왔다면, 거기서 결제하고 싶은 상품을 BillingFlowParams.ProductDetailParams로 우선적으로 만들고, 그것을 listOf를 통해 리스트화 해서 넣어야한다. 이후에 billingCilent.launchBillingFlow(this, flowParams)를 통해 구글 결제창을 띄운다.

 

    override fun onPurchasesUpdated(
        billingResult: BillingResult,
        purchases: MutableList<Purchase>?
    ) {
        DlogUtil.d(TAG, "???? ${billingResult.responseCode}")
        if (billingResult.responseCode == BillingClient.BillingResponseCode.OK && purchases != null) {
            for (purchase in purchases) {
                DlogUtil.d(TAG,"구매 성공?")
  
                // 거래 성공 코드
                // ?
                val consumeParams = ConsumeParams.newBuilder()
                    .setPurchaseToken(purchase.purchaseToken)
                    .build()

                billingCilent.consumeAsync(consumeParams, consumeListenser)

            }
        } else if (billingResult.responseCode == BillingClient.BillingResponseCode.USER_CANCELED) {
            // 유저 취소 errorcode
        }
    }

 

(주의 : 이 업데이트 매서드는 결제창이 내려간 후에 실행됩니다. 즉, 구매가 거부되었어도 일단은 responseCode를 0을 뱉더라고요)

창이 정상적인 방식으로 내려간다면 해당 매서드의 구매성공? 이라고 되어있는 해당 부분으로 이동하여 코드를 진행합니다. 여기서 구매에 대한 핸들링을 진행할 수 있겠습니다.

 

다만, 해당 제품이 1회 소모성 제품이기 때문에 일반적인 앱의 서비스 결제를 계속해서 할 수 있도록 하기 위해, 구글 서버에서는 이 제품이 소비되었다는 것을 인지시킬 필요가 있습니다. 그것이 billingCilent.consumeAsync(consumeParams, consumeListenser)

결제된 제품의 토큰을 통해 소비를 확인하고, 리스너를 통해 소비의 확인을 핸들링합니다.

따라서 이것을 통해 제품이 정상적으로 결제되었나를 구분하는 지표가 되기도 합니다.

 

이 말이 이해가 안되실까봐 예시를 적어두었습니다.

(예를 들어, 앱의 황금 10개를 결제하고 난 후에 소비매서드를 돌리지 않으면 이것은 소비매서드를 거치기 전까지는 유저가 소비하지 않고 보유하는 형태가 되며, 다음 황금 10개를 결제할 수 없게 됩니다. 그러므로 결제 완료 시점에서 미리 소비매서드를 돌려 황금 10개를 결제한 후에도 소비하지 않고 추가로 황금 10개를 결제할 수 있도록 해야합니다.)

 

이제 소비 결과에 따른 리스너를 핸들링합니다.

        consumeListenser = ConsumeResponseListener { billingResult, purchaseToken ->
            DlogUtil.d(TAG, "billingResult.responseCode : ${billingResult.responseCode}")
            if(billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
               DlogUtil.d(TAG, "소모 성공")
           } else {
               DlogUtil.d(TAG, "소모 실패")
           }
        }

 

만일 결제상태가 PENDING으로 정상적으로 완료되지 않았다면 billingResult를 5를 뱉으며 소모실패가 이루어집니다. 아마 구글 테스트 중 느린결제가 있을텐데, 느린 결제여도 정상적으로 결제가 완료됐다면 override fun onPurchasesUpdated가 한번 더 호출되어서 소비매서드로 이동하므로, 결제가 완료되었을때의 최종 로직은 여기서 처리한다고 보셔도 무방합니다.

 

 

전체 코드를 올려드릴테니 한번 참조하셔서 인앱결제 붙여봅시다 :)

https://github.com/littlemary1379/googlepayinapptest/blob/master/app/src/main/java/com/mary/onandoff/MainActivity.kt#L59

 

GitHub - littlemary1379/googlepayinapptest

Contribute to littlemary1379/googlepayinapptest development by creating an account on GitHub.

github.com

 

반응형