본문 바로가기

Effective */Effective Android

[Google Play 인앱 구현] 결제 라이브러리 통합 2

developer.android.com/google/play/billing/integrate

 

앱에 Google Play 결제 라이브러리 통합  |  Google Play 결제 시스템  |  Android Developers

중요: 앱이 현재 AIDL을 사용하여 Google Play 결제 시스템을 통합하는 경우 AIDL에서 Google Play 결제 라이브러리로 이전하는 데 필요한 단계에 관한 대략적인 개요는 AIDL에서 Google Play 결제 라이브러리

developer.android.com

BillingClient 인스턴스 초기화

  • BillingClient
    • Google Play 결제 라이브리와 나머지 앱간의 통신을 위한 기본 인터페이스
    • 일반적적인 결제 작업 편의 메서드(동기 메스드, 비동기 메서드)를 제공
    • 생성 : newBuilder() 사용
    • 구매 관련 업데이트 수신 : setListenerI()를 호출하여 PurchaseUpdatedListener에 대한 참조를 전달
private val purchasesUpdateListener =
   PurchasesUpdatedListener { billingResult, purchases ->
       // To be implemented in a later section.
   }

private var billingClient = BillingClient.newBuilder(activity)
   .setListener(purchasesUpdatedListener)
   .enablePendingPurchases()
   .build()

 

Google Play 연결 설정

  • startConnection() : Google Play에 연결. 연결 프로세스는 비동기적
  • 클라이언트 설정이 완료되고 추가로 요청할 준비가 되면 BillingClientStateListenr를 구현하여 콜백 수신
  • onBillingSetUpFinished() : BillingClient 준비됨, 구매 가능한 제품을 쿼리하여 사용자에게 표시할 준비 완료
  • onBillingServiceDisconnected() : Google Play와 연결이 끊어진 문제 처리(재시도 로직). 콜백 메서드를 재정의하고 추가 요청을 하기 전에 BillingClient가 startConnection()을 호출하여 Google Play에 다시 연결하도록 해야 함

Google Play 연결 

 

billingClient.startConnection(object : BillingClientStateListener {
    override fun onBillingSetupFinished(billingResult: BillingResult) {
        if (billingResult.responseCode ==  BillingResponseCode.OK) {
            // The BillingClient is ready. You can query purchases here.
        }
    }
    override fun onBillingServiceDisconnected() {
        // Try to restart the connection on the next request to
        // Google Play by calling the startConnection() method.
    }
})

 

구입 가능한 제품 표시

  • querySkuDetailAsync()
    • Google Play에 인앱 상품의 현지화 된 세부 정보 쿼리
      • SkuType : INAPP/SUBS
      • Google Play Console에서 생성된 제품 ID
      • 문자열 목록을 지정하는 SkuDetailsParams의 인스턴스
  • SkuDetailResponsListenr
    • 비동기 작업의 결과 처리
    • 쿼리가 완료되면 리스너에게 알리는 onSkuDetailResponse() 재정의 가능
fun querySkuDetails() {
    val skuList = ArrayList<String>()
    skuList.add("premium_upgrade")
    skuList.add("gas")
    val params = SkuDetailsParams.newBuilder()
    params.setSkusList(skuList).setType(SkuType.INAPP)
    withContext(Dispatchers.IO) {
        billingClient.querySkuDetailsAsync(params.build()) { billingResult, skuDetailsList ->
            // Process the result.
        }
    }
}
  • 정기결제
    • 모든 인앱 프로모션 또는 스플래시 화면에서 명확한 정보를 전달하는 것이 중요
      • 정기결제 가격, 결제 주기 빈도, 앱을 사용하려면 정기 결제가 필요한지 등 혜택 조건을 명확하게 전달
      • sku이름이 정기결제 성격을 명확하게 전달해야함
      • 혜택은 사용자가 모두 이해할 수 있도록 이용약관과 독일한 언어로 현지화 되어 있어야 함
      • 정기 결제 관리 또는 취소할 수 있는 방법이 앱에 분명하게 설명되어야 함

구매 흐름 시작

  • launchBillingFlow() : 구매 요청 시작
    • querySkuDetailsAsync() 호출하여 받은 SkuDetails 객체가 포함된 BillingFolowParams 객체  참조
    • BillingFlowParams 객체를 생성하려면 BillingFlowParams.Builder 클래스 사용
    • BillingClient.BillingClient.BillingResponseCode의 응답 코드 반환(OK : 성공적으로시작)
    • 성공하면 시스템에서 Google Play 구매 화면 표시
val flowParams = BillingFlowParams.newBuilder()
        .setSkuDetails(skuDetails)
        .build()
val responseCode = billingClient.launchBillingFlow(activity, flowParams).responseCode

 

  • 구매 결과
    • Google Play가 onPurchaseUpdated()를 호출하여 PurchaseUpdatedListener 인터페이스를 구현하는 리스너에 구매 작업 결과를 전성
    • 리스너는 클라이언트를 초기화할 때 seListener() 메소드를 사용하여 지정
    • 구매 성공 : 사용자가 구매한 인앱 상품의 사용자 및 제품 ID를 나타내는 고유 식별자인 구매 토큰 생성
override fun onPurchasesUpdated(billingResult: BillingResult, purchases: List<Purchase>?) {
   if (billingResult.responseCode == BillingResponseCode.OK && purchases != null) {
       for (purchase in purchases) {
           handlePurchase(purchase)
       }
   } else if (billingResult.responseCode == BillingResponseCode.USER_CANCELED) {
       // Handle an error caused by a user cancelling the purchase flow.
   } else {
       // Handle any other error codes.
   }
}

 

구매 처리

  • 대부분의 경우 앱은 PurchaseUpdatedListenr를 통해 구매 알림을 받으나, BillingClient.queryPurchases() 호출을 인식하는 경우가 있음
  • 구매인증
    •  구매상태 (PURCHASED, PENDING)
    • PurchaseUpdatedListener 또는 queryPurchases에서 수신한 구매의 경우 앱이 자격을 부여하기 전에 구매를 추가로 인증하여 정당성을 확인해야함. 
    • 구매인증을 했다면 사용자에게 자격을 부여할 준비가 된 것, 자격을 부여한 후 구매를 확인해야 함(구매와 관련된 자격을 부여했음을 Google Play에 알려줌
    • 2.0 이상의 Google 결제 라이브러리 사용 시 3일이내 구매 확인을 하지 않으면 사용자는 자동 환불(Google Play 구매 취소)
    • 구매 인증 후 사용자에게 콘텐츠 제공 및 전송 확인(선택 적으로 사용자가 다시 구입할 수 있도록 항목을 소비됨으로 표시)
  •  소비성인증
    • consumeAsync()를 호출하고 Google Play에서 다시 구매할 수 있게 하매 토큰을 포함
    • ConsumeResposneListener(서비 작업의 결과 처리) 인터페이스를 구현하는 객체를 전달해야 함
    • 작업 완료시 Goolge Play 결제 라이브러리가 호출하는 onConsumeResponse() 메소드를 재정의 할 수 있음
    • 소비 요청이 실패 할 수 있으므로 보안 백엔드 서버를 확인하여 각 구매 토큰이 사용하지 않았는지 확인해야 함
      • 앱이 동일한 구매에 대해 여러 번 자격을 부여하지 않음
      • 자격을 부여하기 전 앱이 Google Play에서 성공적인 소비응답을 받을 때까지 기다릴 수 있음
      • Google Play에서 성공적인 소비 응답을 보낼 때까지 사용자의 구매를 보류하도록 선택하는 경우 소비 요청 이후 구매 추척을 놓치지 않도록 주의해야함

 

fun handlePurchase(purchase: Purchase) {
    // Purchase retrieved from BillingClient#queryPurchases or your PurchasesUpdatedListener.
    val purchase : Purchase = ...;

    // Verify the purchase.
    // Ensure entitlement was not already granted for this purchaseToken.
    // Grant entitlement to the user.

    val consumeParams =
        ConsumeParams.newBuilder()
            .setPurchaseToken(purchase.getPurchaseToken())
            .build()

    billingClient.consumeAsync(consumeParams, { billingResult, outToken ->
        if (billingResult.responseCode == BillingResponseCode.OK) {
            // Handle the success of the consume operation.
        }
    })
}

 

  • 비소비성 및 정기 결제 인증
    • 최초 정기 결제 구매는 모두 확인해야 함(정기 결제 갱신은 확인하지 않아도 됨)
    • isAcknowlege()메서드를 통해 앱에서 이미 구매를 확인했는지 검토
    • 구매 확인이 되지 않았으면 BillingClient.acknowlegePurchase()로 비소비성 구매 확인
val client: BillingClient = ...
val acknowledgePurchaseResponseListener: AcknowledgePurchaseResponseListener = ...

fun handlePurchase() {
    if (purchase.purchaseState === PurchaseState.PURCHASED) {
        if (!purchase.isAcknowledged) {
            val acknowledgePurchaseParams = AcknowledgePurchaseParams.newBuilder()
                    .setPurchaseToken(purchase.purchaseToken)
            val ackPurchaseResult = withContext(Dispatchers.IO) {
               client.acknowledgePurchase(acknowledgePurchaseParams.build())
            }
        }
     }
}

 

구매 가져오기

  • PurchaseUpdatedListener를 사용하여 구매 업데이트를 수신 대기하는 것만으로는 앱이 모든 구매를 처리하도록 보장할 수 없음
    • 구매 중 네트워크 문제 : 사용자가 구매를 성공적으로 완료하고 Google Play에 확인을 받았지만 PurchaseUpdatedListener를 통해 구매 알림을 받기 전 네트워크 연결이 끊김
    • 여러기기 : 사용자는 한 기기에서 항목을 구입한 후 기기를 전환할 때
    • 앱 외부에서 이루어진 구매 처리 : 프로모션 사용과 같은 일부 구매
  • 위 이유 등으로 구매 항목을 인식하지 못하는 경우
    • onResume() 및 onCreate()에서 OnBillingClient.queryPurchases()를 호출하여 구매 처리