Java

Google Play InApp v5 구글플레이 인앱 결제 버전5

Jadumate 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 구현