본문 바로가기

안드로이드/자바

[AOS] 인앱 결제 라이브러리 4 적용 및 AIDL 마이그레이션 ( + 광고 제거 기능 구현 )

반응형

※신규앱의 경우 8월부터, 업데이트의 경우 11월부터 Billing Library 버전 3 이상을 사용해야 업로드 가능합니다.

마이그레이션 하시는 분들중 뭘 지워야할지 헷갈리신다면 기존에 추가한 모든걸 삭제해도 무방합니다. 간혹 저처럼 어떤걸 추가 했는지 헷갈리시는 분들을 위해 간략하게 기술하겠지만 그래도 헷갈리신다면 제가 작성한 기존 포스팅을 참고해주세요. 

 

신규 적용 이신분은 스크롤을 조금만 내리셔서 '신규 라이브러리 적용'부터 참고해주세요 ! 

 

| 구버전 소스 제거 

aidl 소스 파일 제거 

기존에 사용중인 결제 관련 코드를 제거합니다. ( ex. labHelper..등등 ) 

저의 경우 아래 사진에 있는 파일들이 AIDL ( 구 버전) 관련 파일들인데 모두 삭제하겠습니다 ( aidl 폴더와 util 폴더) 

레거시 코드 파일

퍼미션 제거 

기존 결제 모듈에 사용된 퍼미션( com.android.vending.BILLING )을 제거 합니다

( billing 라이브러리 낮은 버전에선 추가하란 얘기가 있지만 최신 버전은 추가하지 않기 때문에 제거 합니다. ) 

 

| 신규 라이브러리 적용 

개인 앱에 적용한 사례로 무료 앱에 일정 금액을 지불하고 광고를 제거하는 방식으로 구현 됐습니다.

 

build.gradle ( app ) 

    // 결제
    implementation "com.android.billingclient:billing:4.0.0"

BillingClient 인스턴스를 생성

인앱 결제 모듈 인스턴스를 생성하고 연결하는 메소드로 결제가 필요한 화면을 진입했을때 한번만 호출합니다.

    private BillingClient payClient;
	.....
    private void setInapp() {

        billingClient = BillingClient.newBuilder(this).setListener(new PurchasesUpdatedListener() {
            @Override
            public void onPurchasesUpdated(@NonNull BillingResult billingResult, @Nullable List<Purchase> list) {
                // 결제 결과 처리

                if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) {
                    // 결제 완료
                    for (Purchase purchase : list) {
                        handlePurchase(purchase);
                    }
                } else if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.USER_CANCELED) {
                    // 유저가 취소 했을때

                } else if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.ITEM_ALREADY_OWNED) {
                    // 이미 구매 했을 경우
                    Common.showToast(CenterActivity.this, getString(R.string.already_buy_item));
                    alreadyBuyedItem();
                }
            }
        }).enablePendingPurchases().build();

        payClient.startConnection(new BillingClientStateListener() {
            @Override
            public void onBillingServiceDisconnected() {
                // 연결이 끊겼을때
            }

            @Override
            public void onBillingSetupFinished(@NonNull BillingResult billingResult) {
                // 연결 완료 했을때
            }
        });
    }

 

startConnection

생성된 클라이언트를 통해 구글 서버와 연결합니다. 상품 구매등을 위해선 반드시 연결이 완료 되어야 하며 연결이 끊겼을땐 재 연결 해야합니다. 

'onBillingServiceDisconnected' 가 호출됐을때 재 연결을 한다면 무한 호출이 될수 있으니, billingClient를 사용하거나 onResume시 한번씩 체크하는걸 권장합니다. 

 

onPurchasesUpdated

결제가 완료 됐을때 처리하는 리스너로 실제 사용자가 구매를 완료 했을때 호출됩니다.  호출된 경우 구매 확정 과정을 거쳐야 합니다. 

 

상품 정보 조회 

구매 확정에 앞서 구매하고자 하는 아이템 키를 가지고 구글 서버로부터 아이템 정보들을 내려 받습니다. ( 'itemKey' 는 스토어에 등록한 인앱 결제 아이템의 아이디 입니다. ) 

    private void getItemDetail(String itemKey) {

        // 상품 조회, 조회할 상품들의 아이디 값을 넘겨 준다
        SkuDetailsParams params = SkuDetailsParams.newBuilder()
                .setSkusList(Arrays.asList(itemKey))// 구매할 아이템
                .setType(BillingClient.SkuType.INAPP)// 구매 타입  인앱이냐 구독이냐, 현재는 인앱
                .build();

        billingClient.querySkuDetailsAsync(params, new SkuDetailsResponseListener() {
            @Override
            public void onSkuDetailsResponse(@NonNull BillingResult billingResult, @Nullable List<SkuDetails> list) {
                if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) {
                   
                    if (list.size() > 0)
                        buyItem(list.get(0));

                }
            }
        });
    }

저의 경우에는 '광고제거' 단일 상품을 운영중이기 때문에 '광고 제거' 버튼을 눌렀을때 위 메소드를 호출하고 파라미터론 '광고제거'의 키를 넘겨줍니다.  그리고 상품 정보를 받았을때 바로 구매 로직을 태웁니다.

하지만 여러 아이템을 운영하신다면 기존에 갖고 있는 키값을 리스트로 넘겨주고 리턴받은 리스트를 갖고 있다가 각 아이템 구매 버튼을 클릭했을때 그에 맞는 Skudetails 아이템을 구매로직에 넘겨줘야 합니다.

상품 구매

launchbillingFlow를 실행하면 사용자에게 실제 구매 팝업이 나타나고, 완료 하거나 실패했을 경우 'onPurchasesUpdated'가 호출됩니다.위에서 언급한것처럼 'onPurchasesUpdated' 호출된 후 구매 확정 로직을 거쳐야 합니다.

    private void buyItem(SkuDetails skuDetails) {
        // 실제 구매
        BillingFlowParams flowParams = BillingFlowParams.newBuilder()
                .setSkuDetails(skuDetails)
                .build();

        billingClient.launchBillingFlow(this, flowParams);
    }

 

구매 확정

구매 완료 후 3일 이내 확정하지 않으면 환불처리가 되기 때문에 '구매완료 > 구매확정' 과정을 꼭 거쳐야 합니다. 

그리고 확정을 하는 과정에서 앱이 꺼지거나 네트워크가 불안정해서 안될수도 있으니 앱 구동시나 onResume에 방어코드를 추가하는걸 권장합니다. 방어코드 예시는 아래 기재 했습니다.

    private void handlePurchase(Purchase purchase) {
        // 구매 확정, 구매 완료후 처리를 3일이내 하지 않으면 자동으로 환불된다.

        if (purchase.getPurchaseState() == Purchase.PurchaseState.PURCHASED &&
                !purchase.isAcknowledged()) {
            // 완료는 되었지만 확인처리 안됐을 경우
            AcknowledgePurchaseParams acknowledgePurchaseParams = AcknowledgePurchaseParams.newBuilder()
                    .setPurchaseToken(purchase.getPurchaseToken()).build();

            billingClient.acknowledgePurchase(acknowledgePurchaseParams, new AcknowledgePurchaseResponseListener() {
                @Override
                public void onAcknowledgePurchaseResponse(@NonNull BillingResult billingResult) {

                    if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) {
                        // 최종 구매 확정
                        alreadyBuyedItem();
                    }
                }
            });
        }
    }

방어코드

billingClient가 연결 됐을때 호출하는 메소드로 SKU_ITEM(광고제거 아이템)이 구매 했는지 여부를 체크하여 그에 맞는 처리를 해줍니다.

- 구매 했는지 

- 구매 했으면 확정 했는지 ( 안한 경우 광고는 비노출로 하고 확정 프로세스 태움 ) 

- 구매 안했으면 광고 노출 

   
    private void checkPurchase() {
        if (billingClient != null && billingClient.isReady()) {
            billingClient.queryPurchasesAsync(BillingClient.SkuType.INAPP, new PurchasesResponseListener() {
                @Override
                public void onQueryPurchasesResponse(@NonNull BillingResult billingResult, @NonNull List<Purchase> list) {

                    for (Purchase item : list) {
                        if (item.getSkus().size() > 0) {
                            if (TextUtils.equals(item.getSkus().get(0), SKU_ITEM)) {
                                // 광고 제거 아이템을 구매 했는지 여부

                                if(item.getPurchaseState() == Purchase.PurchaseState.PURCHASED){
                                    // 구매 완료한 경우

                                    alreadyBuyedItem();

                                    if(!item.isAcknowledged()){
                                        // 구매 확정만 안한 경우
                                        handlePurchase(item);
                                    }
                                }else{
                                    // 구매 안한 경우
                                    initAds();
                                }
                            }
                        }

                    }
                }
            });
        }
    }

위 코드는 onResume시나 billingClient가 구글 서버에 연결 됐을때 호출하는등 적절하게 넣어주시면 될것 같습니다.

소모 처리  

판매하는 아이템이 '광고 제거'와 같이 한번 구매하면 영구적으로 갖고 있을 경우 별다른 처리를 하지 않아도 됩니다. 

하지만 재 구매가 가능한 별도 재화나 아이템을 판매했다면 별도 소모 처리를 진행해야 합니다.  그리고 소모 처리가 완료 됐을때 재화나 아이템을 제공해야 합니다.

onConsumeResponse에서 response코드를 체크 후 재화를 제공하면 됩니다.

    private void completePurchaseInapp(Purchase purchase) {

        // 인앱 구매 완료후 소모할때
        ConsumeParams consumeParams = ConsumeParams.newBuilder()
                .setPurchaseToken(purchase.getPurchaseToken())
                .build();

        billingClient.consumeAsync(consumeParams, new ConsumeResponseListener() {
            @Override
            public void onConsumeResponse(@NonNull BillingResult billingResult, @NonNull String s) {
                if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) {
                    // 구매한 아이템 소모 완료 
                }
            }
        });
    }

EndConnection 

Activity나 Fragment가 Destroyed 될때 'EndConnection'을 호출해야 메모리 누수를 피할 수 있습니다.

    @Override
    protected void onDestroy() {
        if (billingClient != null) {
            billingClient.endConnection();
        }

        super.onDestroy();
    }

 

 

이해 안되는 부분이 있으시면 댓글로 남겨주세요 ! 

반응형