ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Google Play InApp v5 구글플레이 인앱 결제 버전5
    Java 2022. 10. 25. 16:40
    728x90
    반응형

    22년 11월 1일부터, 구글플레이 모든 앱의 인앱결제 라이브러리 버전3을 사용이 완전 종료된다. 따라서, v4나 v5로 마이그레이션을 필수적으로 해야 하는데,  

     

    망할 구글 샘플 소스 https://developer.android.com/google/play/billing/integrate?hl=ko#java 는  ImmutableList 라는 클래스 덕택에 컴파일 되지 않는다. 

     

    삽질끝에 ArrayList로 변경하여 구현하는데 성공

     

    1. build.graddle(:app)에 라이브러리 추가

    def billingVersion = "5.0.0"
    implementation "com.android.billingclient:billing:$billingVersion"
    

     

    2. graddle 변경후에는 반드시 "Sync Project with Graddle Files"를 해준다.

     

    3.  빌링 모듈 클래스를 만들자. PurchaseUpdateListener 와 ConsumeResponseListener 를 구현해야 한다.

    public class BillingModule implements PurchasesUpdatedListener, ConsumeResponseListener {
    

     

    4. 이 모듈에서 발생하는 이벤트를 받을 콜백 인터페이스를 선언해주자

    public interface BillingModuleCallback {
          void onBillingModulesIsReady(List<ProductDetails> detailsList);
          void onSuccess(Purchase purchase);
          void onFailure(int errorCode);
          void onInit(int result);
       }

    5. 생성자 선언. 이 모듈의 엄마(?)가 될 액티비티를 전달해주어야 한다.

     

    BillingClient billingClient = null;
    Activity callActivity = null;
    BillingModuleCallback callBack = null;
    
    public BillingModule(Activity activity, BillingModuleCallback callback)
    {
       billingClient = BillingClient.newBuilder(activity)
           .setListener(this)
           .enablePendingPurchases()
           .build();
       
       billingClient.startConnection(new BillingClientStateListener() {
           @Override
           public void onBillingSetupFinished(BillingResult billingResult) {
               if (billingResult.getResponseCode() ==  BillingResponseCode.OK) {
                   // The BillingClient is ready. You can query purchases here.
                  callBack.onInit(0);    
               }
           }
           @Override
           public void onBillingServiceDisconnected() {
               // Try to restart the connection on the next request to
               // Google Play by calling the startConnection() method.
           }
       });
       
       callActivity = activity;
       callBack = callback;
    }
    
    public void dispose()
    {
       billingClient.endConnection();
    }
    

     

    빌링 셋업이 성공적으로 완료되면 엄마의 onInit();를 호출하게 만들어줬다.   부모는 onInit() 를 받으면, SKU 리스트를 달라고 요청한다. 

    ...
    BillingModule bm = null;
    ...
    
    @Override
    public void onCreate (Bundle savedInstanceState) 
    {
    ...
    	bm = new BillingModule(this, this);
    ...
    }
    
    
    final String SKU_STAR10  = "star10";
    final String SKU_STAR20  = "star20";
    final String SKU_STAR100 = "star100";
    
    @Override
    public void onInit(int result) {
       // TODO Auto-generated method stub
       if(result == BillingClient.BillingResponseCode.OK){
          ArrayList<String> listSku = new ArrayList <String> ();
    
          listSku.add(SKU_STAR10);
          listSku.add(SKU_STAR20);
          listSku.add(SKU_STAR100);
    
          bm.asyncGetReadyToBuy(listSku);
    
       } else {
          Log.d("iap5", "onInit error : " + result);
       }
    }

    MainActivity의 onInit 구현

     

    6.  getReadyToBuy 의 구현 

    
    public void asyncGetReadyToBuy(ArrayList<String> listSku)
    {
       ArrayList <QueryProductDetailsParams.Product> listProduct = new ArrayList<>();
    
       for(String sku : listSku) {
          Log.d("iap5", "asyncGetReadyToBuy sku : " + sku);
          listProduct.add(QueryProductDetailsParams.Product.newBuilder().setProductId(sku).setProductType(BillingClient.ProductType.INAPP).build());
       }
    
       QueryProductDetailsParams params = QueryProductDetailsParams.newBuilder()
          .setProductList(listProduct)
          .build();
    
       billingClient.queryProductDetailsAsync(
          params,
          new ProductDetailsResponseListener() {
             public void onProductDetailsResponse(BillingResult billingResult, List<ProductDetails> productDetailsList) {
                // Process the result
                Log.d("iap5", "onProductDetailsResponse : " + productDetailsList.size());
                callBack.onBillingModulesIsReady(productDetailsList);
             }
          }
       );
    }


    지정된 SKU들의 Detail을 달라고 요청하고,  요청이 도착하면 엄마를 콜백한다. 

    ArrayMap<String, ProductDetails> skus = new ArrayMap<String, ProductDetails> ();
    
    @Override
    public void onBillingModulesIsReady(List<ProductDetails> listSku) {
       // TODO Auto-generated method stub
       if(listSku == null) {
          Log.d("iap5", "onBillingModulesIsReady sku is null");
       } else {
          for(ProductDetails sku : listSku){
             Log.d("iap5", "onBillingModulesIsReady sku : " + sku.toString());
             skus.put(sku.getProductId(),  sku);
          }
       }
    }

    MainActivity의 onBillingModilesIsReady 구현 :  준비가된 SKU들을 어레이맵에 담아둔다.

    7. 여기까지 결제준비는 완료되었다! 이제 사용자가 구매를 눌렀을때 처리를 시작해보자.  사용자가 어떤 아이템 구매버튼을 누르면, 이 아이템의 ProductDetail (예전 버전에서는 SKU Detail)을 알아내서 구글플레이 서버로 전송해야 한다. 

    public boolean buyStar(String productId)
    {
       if(skus.isEmpty()){
          showToast("Not available (NO SKUS)");
          return false;
       }
    
       ProductDetails sku = skus.get(productId);
       if(sku == null){
          showToast(String.format("Not available (NO SKUS : %s)", productId));
          return false;
       }
       
       bm.asyncLaunchFlow(sku);
       return true;
    }

     

    8. 빌링 플로우 시작

    public int asyncLaunchFlow(ProductDetails productDetails){
       ArrayList<BillingFlowParams.ProductDetailsParams> productDetailsParamsList = new ArrayList<>();
    
       productDetailsParamsList.add(BillingFlowParams.ProductDetailsParams.newBuilder()
          .setProductDetails(productDetails)
          .build()
       );
    
       BillingFlowParams billingFlowParams = BillingFlowParams.newBuilder()
          .setProductDetailsParamsList(productDetailsParamsList)
          .build();
    
       Log.d("iap5", "asyncLaunchFlow : " + productDetails.getProductId());
    
       return billingClient.launchBillingFlow(callActivity, billingFlowParams).getResponseCode();
    }

     

    9.  구매를 성공(혹은 실패)하면 onPurchaseUpdated 가 호출된다. 

    
    @Override
    public void onPurchasesUpdated(BillingResult billingResult, List<Purchase> purchases) {
       // TODO Auto-generated method stub
       Log.d("iap5", "onPurchasesUpdated : " + billingResult.getResponseCode());
    
       if (billingResult.getResponseCode() == BillingResponseCode.OK && purchases != null) {
          for (Purchase purchase : purchases) {
             handlePurchase(purchase);
          }
       } else if (billingResult.getResponseCode() == BillingResponseCode.USER_CANCELED) {
          // Handle an error caused by a user cancelling the purchase flow.
       } else {
          // Handle any other error codes.
       }
    }

     

    구매성공하면, 구매가 아니라 구매들(!)이 리스트로 넘어온다. 이제 이것들을 잘 처리하면 된다.

    10. 구매 물건에 대한 처리 = Consume

    private Purchase curPurchase = null;
    
    void handlePurchase(Purchase purchase) {
       curPurchase = purchase;
       ConsumeParams consumeParams =  ConsumeParams.newBuilder()
           .setPurchaseToken(purchase.getPurchaseToken())
            .build();
    
       Log.d("iap5", "handlePurchase : " + purchase.getProducts().get(0));
    
        billingClient.consumeAsync(consumeParams, this);
    }

     

    11. Consume 에 대한 처리 : 최종적으로 부모의 onSuccess()가 호출된다. 

    
    @Override
    public void onConsumeResponse(BillingResult billingResult, String token) {
       // TODO Auto-generated method stub
         if (billingResult.getResponseCode() == BillingResponseCode.OK) {
                // Handle the success of the consume operation.
           Log.d("iap5", "onConsumeResponse : " + billingResult.getResponseCode());
                callBack.onSuccess(curPurchase);
            }
    }


    12. 결제 프로세스가 중단되는경우 (결제는 완료했는데, Consume 이 되지 않은 경우) 에는 다시 확인하고 처리해야한다.

    public void resume(String ptype){
       Log.d("iap5", "resume : " + ptype);
    
       if (billingClient.isReady()) {
          billingClient.queryPurchasesAsync(
             QueryPurchasesParams.newBuilder().setProductType(ptype).build(), new PurchasesResponseListener() {
                public void onQueryPurchasesResponse(BillingResult billingResult, List<Purchase> purchases) {
                   Log.d("iap5", "onResume.onQueryPurchasesResponse : " + billingResult.getResponseCode());
                   if (billingResult.getResponseCode() == BillingResponseCode.OK && purchases != null) {
                      for (Purchase purchase : purchases) {
                         handlePurchase(purchase);
                      }
                   }
                }
             }
          );
       }

     

    @Override
    protected void onResume() {
       // TODO Auto-generated method stub
       super.onResume();
       
       bm.resume(BillingClient.ProductType.INAPP);
    }

    MainActivity의 onResume 구현

Designed by Tistory.