본문 바로가기
Floney

[Floney] 정기 결제 구현

by 박매트 2024. 10. 19.

 

 

1. 권한 추가한 후 내부 테스트로 앱 등록 

  • 결제 권한을 가진 앱을 내부 테스트로 등록을 해줘야 정기 결제 상품을 등록할 수 있다.
  • 그리고 아래 2가지는 꼭 추가해줘야 한다. 까먹고 manifest 파일에 해당 권한을 추가 안했더니 결제에서 오류가 계속 발생했다. 

gradle에 추가

implementation 'com.android.billingclient:billing:7.1.1'

 

manifest에 permission 추가

<uses-permission android:name="com.android.vending.BILLING" />

 

 

2. 정기 결제 상품 등록

Google Play Console에 들어가서

홈 -> 앱 -> Play를 통한 수익 창출 -> 정기 결제 에 들어가서

제품 id랑 금액, 혜택 등 필요한 정보를 등록해둔다 !

그리고 활성화 를 시켜야 결제 테스트를 할 수 있다. 해둬야 한다!

 

 

3. 설정 -> 라이선스 테스트에 테스트할 Playstore 계정 추가

  • 테스터로 등록이 안되면 테스트 결제를 할 수가 없다.
  • 그리고 테스트 결제는 따로 결제수단은 추가안해도 된다.

 

4. 결제 처리를 하기 위한 코드 작성

 

https://developer.android.com/google/play/billing/integrate?hl=ko순서

 

앱에 Google Play 결제 라이브러리 통합  |  Google Play's billing system  |  Android Developers

Reminder: By Aug 31, 2024, all new apps and updates to existing apps must use Billing Library version 6 or newer. If you need more time to update your app, you will be able to request an extension until Nov 1, 2024. Learn more. 이 페이지는 Cloud Trans

developer.android.com

구글에서 정리해놓은 코드이다.

이 코드가 최신이니 보고 확인하는 거 추천..

예전 티스토리 글 보고 했다가 함수가 바뀐 것들이 있어서 정기 결제 정보를 불러오지 못했었다.

 

 

1) Activity 코드에서는 BillingManaer 객체를 만들어서 부르면 된다.

billingManager = BillingManager(this@SubscribeInformActivity)
                    billingManager.startConnection()

 

2. 해당 activity 내에서 billingmanar 가 실행되면서 결제 bottom_sheet이 올라온다.


아직 오류 처리 로직은 구현을 안했다. 추가할 예정. 

아래 코드는 테스트 bottom_sheet가 뜨고 purchaseToken까지 얻어온 시점이다.
이제 서버에 보내서 서버는 결제 검증을 하는 과정을 이제 추가로 해야한다.

 

설명해보자면, 순서는 이렇다.

  • 결제 서비스 연결 (startConnection)
    이 메서드는 Google Play의 결제 서비스에 연결을 시도하는 단계이다. 먼저 billingClient.startConnection()을 호출하여 두 가지 주요 이벤트를 처리한다:
    • onBillingServiceDisconnected()는 서비스가 끊어졌을 때의 처리를 담당하며, 네트워크 문제로 연결이 끊어졌을 때 재시도를 할 수 있다.
    • onBillingSetupFinished()는 서비스 설정이 완료되었을 때 호출되며, 성공하면 querySubscriptionDetails()를 호출하여 구독 상품 정보를 가져온다.
  • 구독 상품 정보 조회 (querySubscriptionDetails)
    서비스 연결이 성공한 후 querySubscriptionDetails()를 호출하여 Google Play에서 구독 상품 정보를 조회한다.
    • QueryProductDetailsParams 객체를 사용하여 구독 상품의 ID와 구독 타입을 설정한 뒤, queryProductDetailsAsync() 메서드로 비동기적으로 상품 정보를 가져온다.
    • 상품 정보가 성공적으로 조회되면, 첫 번째 상품 정보를 launchPurchaseFlow()로 전달하여 결제 플로우를 시작한다.
    • 조회 실패 시 오류 로그를 출력한다.
  • 구매 플로우 실행 (launchPurchaseFlow)
    구독 상품 정보 조회가 성공하면, launchPurchaseFlow() 메서드를 호출하여 결제 플로우를 실행한다.
    • BillingFlowParams 객체를 생성하고, 구독 상품의 subscriptionOfferDetails에서 offerToken을 설정한 후 launchBillingFlow() 메서드를 통해 실제 결제를 시작한다.
  • 구매 처리 (handlePurchase)
    사용자가 결제를 완료하면 결제 결과에 따라 handlePurchase()가 호출된다.
    • 이 메서드는 구매 상태가 Purchase.PurchaseState.PURCHASED인지 확인하고, 실제로 결제가 완료된 상태에서만 구매 토큰(purchaseToken)을 서버로 전송하여 결제 검증을 진행한다.
    • 만약 구매가 아직 확인되지 않았다면(purchase.isAcknowledged가 false), acknowledgePurchase()를 호출하여 구매 확인을 처리한다.
  • 구매 확인 (acknowledgePurchase)
    구매가 완료된 후 acknowledgePurchase() 메서드를 통해 Google Play에 구매가 정상적으로 처리되었음을 알린다.
    • 구매 확인을 통해 Google에 결제 완료 상태를 전달하며, 일정 기간 내에 구매 확인이 이루어지지 않으면 Google이 자동으로 환불 처리를 할 수 있다.
    • 구매 확인이 성공하면 성공 로그가 출력되고, 실패 시 오류 로그가 출력된다.

코드

class BillingManager(private val activity: Activity) {
    private lateinit var billingClient : BillingClient

    init {
        billingClient = BillingClient.newBuilder(activity)
            .enablePendingPurchases()
            .setListener { billingResult, purchases ->
                if (billingResult.responseCode == BillingClient.BillingResponseCode.OK && purchases != null) {
                    for (purchase in purchases) {
                        handlePurchase(purchase) // 구매 처리
                    }
                } else {
                    // 결제 실패 처리
                    Timber.e("Purchase failed: ${billingResult.debugMessage}")
                }
            }
            .build()
    }

    // 구매 정보 처리
    private fun handlePurchase(purchase: Purchase) {
        // 구매 토큰을 서버로 보내기 전에 구매 상태를 확인해야 함
        if (purchase.purchaseState == Purchase.PurchaseState.PURCHASED) {
            // 구매가 완료된 상태에서만 처리
            val purchaseToken = purchase.purchaseToken
            Timber.i("Purchase successful, token: $purchaseToken")

            // 토큰을 서버로 전송하여 검증
            sendTokenToServer(purchaseToken)

            // 구매가 성공했음을 사용자에게 알림
            if (!purchase.isAcknowledged) {
                acknowledgePurchase(purchase)
            }
        }
    }

    // 서버로 토큰 전송
    private fun sendTokenToServer(purchaseToken: String) {
        // 서버로 토큰 전송을 위한 네트워크 요청
        Timber.i("Sending token to server: $purchaseToken")
    }

    // 구매 확인 (Acknowledgement)
    private fun acknowledgePurchase(purchase: Purchase) {
        val acknowledgePurchaseParams = AcknowledgePurchaseParams.newBuilder()
            .setPurchaseToken(purchase.purchaseToken)
            .build()

        billingClient.acknowledgePurchase(acknowledgePurchaseParams) { billingResult ->
            if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
                Timber.i("Purchase acknowledged")
            } else {
                Timber.e("Failed to acknowledge purchase: ${billingResult.debugMessage}")
            }
        }
    }
    fun startConnection() {
        billingClient.startConnection(object : BillingClientStateListener {
            override fun onBillingServiceDisconnected() {
                // Google Play 서비스 연결이 끊어진 경우 처리
                Timber.e("checking 1")
            }

            override fun onBillingSetupFinished(billingResult: BillingResult) {
                if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
                    // 구독 상품 로드 또는 구매 가능 처리
                    Timber.e("checking 2")
                    querySubscriptionDetails()
                }
            }
        })
    }

    fun querySubscriptionDetails() {
        val queryProductDetailsParams = QueryProductDetailsParams.newBuilder()
            .setProductList(
                listOf(
                    QueryProductDetailsParams.Product.newBuilder()
                        .setProductId("프로덕션 아이디")
                        .setProductType(BillingClient.ProductType.SUBS)
                        .build()
                )
            ).build()

        billingClient.queryProductDetailsAsync(queryProductDetailsParams) { billingResult, productDetailsList ->
            if (billingResult.responseCode == BillingClient.BillingResponseCode.OK && !productDetailsList.isNullOrEmpty()) {
                // SKU 세부 사항 로드 완료
                val productDetails = productDetailsList[0] // 첫 번째 상품 정보 사용
                // 구매 플로우 실행
                Timber.e("checking 33 Error code: ${billingResult.responseCode}, message: ${productDetailsList}")
                launchPurchaseFlow(productDetails)

            } else {
                // 오류 처리
                Timber.e("checking 3")
                Timber.e("checking Error code: ${billingResult.responseCode}, message: ${productDetailsList}")
            }
        }
    }

    fun launchPurchaseFlow(productDetails: ProductDetails) {
        val billingFlowParams = BillingFlowParams.newBuilder()
            .setProductDetailsParamsList(
                listOf(
                    BillingFlowParams.ProductDetailsParams.newBuilder()
                        .setProductDetails(productDetails)
                        .setOfferToken(productDetails.subscriptionOfferDetails?.get(0)?.offerToken!!)
                        .build()
                )
            ).build()

        billingClient.launchBillingFlow(activity, billingFlowParams) // Activity로 구매 플로우 실행
    }
}