반응형

2019년 이번해는 정기구독 서비스들이 대세를 이뤘다. 이에 맞춰 정기구독기능을 추가해달라는 요청이 들어와 붙혀보고 정리하는 시간을 가질까한다.

 

 

앱스토어 준비

<그림 1>

-이미 플레이스토어에 앱이 올라와있다고 가정하고 진행하겠다.

- 앱정보 -> 인앱상품 -> 구독 으로 가면 해당 페이지가 뜬다.

- 구독 만들기를 눌러 새로운 구독 아이템을 만들어보자.

 

<그림 2>

 다른부분은 일반적으로 작성하면된다. 그중 중요하게 봐야는 부분은 '제품 ID'인데 추후에 앱에서 연동할때 쓰이는 key 값이기 때문에 잘 기억해 둬야한다.(중요)

<그림 3>

 -<그림2>을 보면 가격 추가부분이 있다.

이부분을 누르면<그림3>이 나오고 가격을 책정하면 자동으로 10프로 부가세 붙어 가격이 책정된다.

- 나머지 부분은 너무 쉽기때문에 생략하고 작성완료하면 <그림1>과 같이 아이템이 생성된다.

 


앱과연동

gradle,manifest에 추가

dependencies {
    //billing
    implementation 'com.android.billingclient:billing:2.0.3'
    implementation 'com.google.code.gson:gson:2.8.6'

}

-gradle dependencies에 추가해 라이브러리를 불러온다.

 

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">
	....
    <uses-permission android:name="com.android.vending.BILLING" />
    
        <application
        android:icon="@drawable/ic_launcher"
        android:label="@string/app_name">
    	.....
        </application>

</manifest>

- use permission에 com.android.vending.BILLNG을추가해준다.

 

핸들러

    public Handler billingHandler = new Handler(){
        @Override
        public void handleMessage(Message msg) {
        super.handleMessage(msg);
        switch (msg.what){
            //스토어와 연결
            case Common.HANDLER_BILLING_INIT://1
                initBilling();
                break;
                // 연결된 상태에서 구독 상품 가저오기
            case Common.HANDLER_BILLING_GET_CONTENTS://2
                getContentsList();
                break;
                //가저온 구독 상품을 화면에 보여주기
            case Common.HANDLER_BILLING_SHOW_PURCHASE:
                showBilling(userSkuDetails);
                break;
                //스토어 앱캐시로 체크(첫번째 체크 방법)
            case Common.HANDLER_BILLING_CHECK_PERCHASE:
                checkPurchaseAppCache();
                break;
                //스토어와 통신한뒤 체크(두번째 체크 방법)
            case Common.HANDLER_BILLING_HISTORY_PURCHASE:
                checkPurchaseHttpConection();
                break;
                //통신 실패시 처리
            case Common.HANDLER_BILLING_CONNECTION_FAIL:
                unstableServer();
                break;

        }
        }
    };

- 핸들러로 정확하게 동작을 제어해준다.

 

 

 

결제콜백 만들어주기

  //purchase callback
    private PurchasesUpdatedListener purchasesUpdatedListener = (BillingResult billingResult, @Nullable List<Purchase> purchases)->{
        //성공
        if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK && purchases != null){
            for (Purchase purchaseItem: purchases){
                handlePurchase(purchaseItem);//구매 정보
                confirmPerchase(purchaseItem);//구매 확정

            }
        }

        //취소
        else if (BillingClient.BillingResponseCode.USER_CANCELED == billingResult.getResponseCode()){
         
        }

        //에러
        else {
          
        }
    };

-결제 한뒤 결과를 알려주는 listener이다.

-성공 : 성공한뒤에는 구매정보는 서버에 저장시켜놓고, 구매확정을 해야 최종 결제 승인이된다(안할경우 자동 취소됨)

-취소  

-에러 

 

 

구매 정보

    private void handlePurchase(Purchase purchase) {
        String purchase_json= purchase.getOriginalJson();
		// 원하는 데이터를 가공해 서버에 저장시킨다
    }

- json으로 구매 데이터가 들어오는데 이부분을 서버측에 저장시킨다.

 

 

구매 확정

    //구매확정
    public void confirmPerchase(Purchase purchase) {
        //PURCHASED
        if (purchase.getPurchaseState() == Purchase.PurchaseState.PURCHASED) {
            if (!purchase.isAcknowledged()) {
                AcknowledgePurchaseParams acknowledgePurchaseParams =
                        AcknowledgePurchaseParams.newBuilder()
                                .setPurchaseToken(purchase.getPurchaseToken())
                                .build();
                billingClient.acknowledgePurchase(acknowledgePurchaseParams, (BillingResult billingResult) ->{
                  	// 구매확정콜백

                });
            }
        }
        //PENDING
        else if (purchase.getPurchaseState() == Purchase.PurchaseState.PENDING) {
			//구매 유예

        }
        else {
            //구매확정 취소됨(기타 다양한 사유...)

        }
    }

 

 

스토어와 연결

 private BillingClient billingClient;

 public void initBilling(){
        billingClient = BillingClient.newBuilder(mContext)
                .enablePendingPurchases()
                .setListener(purchasesUpdatedListener)
                .build();
        billingClient.startConnection(new BillingClientStateListener() {
            @Override
            public void onBillingSetupFinished(BillingResult billingResult) {
                //연결 성공
                if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) {
                    billingHandler.sendEmptyMessage(Common.HANDLER_BILLING_GET_CONTENTS);//상품리스트 가저오기
                }
                //연결 실패
                else{
                    billingHandler.sendEmptyMessage(Common.HANDLER_BILLING_CONNECTION_FAIL);
                }
            }
			//연결 끊김
            @Override
            public void onBillingServiceDisconnected() {
			
            }
        });
 }

- 위에서 만들엇던 listner를 setListner부분에 붙혀준다.

-연결성공 : 연결이 성공되면 그커넥션을 가지고 상품 리스트를 가저와야한다.

-연결실패 : 하면서 연결이 실패된적은 없엇던것같다.

-연결끊김 

 

상품가저오기

    public void getContentsList(){
        List<String> sku_contents_list = new ArrayList<String>();
        sku_contents_list.add(itemName);
        SkuDetailsParams.Builder params = SkuDetailsParams.newBuilder();
        params.setSkusList(sku_contents_list).setType(BillingClient.SkuType.SUBS);//결제타입 지정
        SkuDetailsResponseListener listener = new SkuDetailsResponseListener() {
            @Override
            public void onSkuDetailsResponse(BillingResult billingResult, List<SkuDetails> skuDetailsList) {
                //연결을 못함.
                if (billingResult.getResponseCode() != BillingClient.BillingResponseCode.OK){
                    return;
                }

                //상품정보를 가저오지 못함 -
                if (skuDetailsList == null){
                    return;
                }

                //상품사이즈 체크
                L.i("## response data size : " + skuDetailsList.size());

                //상품가저오기 : 정기결제상품 하나라서 한개만 처리함.
                try {
                    for (SkuDetails skuDetails : skuDetailsList) {
                        String title = skuDetails.getTitle();
                        String sku = skuDetails.getSku();
                        String price = skuDetails.getPrice();
                        userSkuDetails = skuDetails;

                        if (itemName.equals(sku)){
                            showBilling(skuDetails);
                        }
                    }
                }
                catch (Exception e){
                    L.e("## 리스트 가저오기 오류" + e.toString());
                }

            }

        };
        billingClient.querySkuDetailsAsync(params.build() , listener);
    }

- 결제타입 지정 : BillingClient.SkuType.SUBS, BillingClient.SkuType.INAPP 두 가지중 우리는 정기구독을 만들것이기 때문에 BillingClient.SkuType.SUBS를 사용한다.

-itemName은 <그림2>에서 언급한 key값을 넣어주면된다.

- 상품가저온 뒤엔 skuDetails를 가지고 상품을 보여줘야한다. ->showBilling(skuDetails)

 

결제화면 보여주기

    //결제화면 보여주기
    private void showBilling(SkuDetails skuDetails) {
        // Retrieve a value for "skuDetails" by calling querySkuDetailsAsync().
        BillingFlowParams flowParams = BillingFlowParams.newBuilder()
                .setSkuDetails(skuDetails)
                .build();
        billingClient.launchBillingFlow(mActivity,flowParams);
    }

- google 라이브러리내 내장된 결제 화면이 나온다.

- purchaseUpdateListner에 결제 결과가 나온다.

- 작동시키면 아마 화면은 뜨지만 결제 화면은 안뜰것이다. 뒤쪽에 설명하겠다.

 

결제상태 체크

 public void checkPurchase() {
        billingClient = BillingClient.newBuilder(mContext)
                .enablePendingPurchases()
                .setListener(purchasesUpdatedListener)
                .build();
        billingClient.startConnection(new BillingClientStateListener() {
            @Override
            public void onBillingSetupFinished(BillingResult billingResult) {
                //connection success
                if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) {


                    //check query
                    Purchase.PurchasesResult purchasesResult = billingClient.queryPurchases(BillingClient.SkuType.SUBS);

                    int list = purchasesResult.getPurchasesList().size();
                    //앱스토어에서 정기결제 구독한 사람
                    if (list>=0){
                        for (Purchase purchase : purchasesResult.getPurchasesList()){
                           
                            Gson gson = new Gson();
                            BillingStateVO bsVO = gson.fromJson(purchase.getOriginalJson(), BillingStateVO.class);
                            boolean isPerchase = false;
                            isPerchase =  pakageName.equals(bsVO.getPackageName())&&itemName.equals(bsVO.getProductId());
                            //해당앱 정기 구독한 사람
                            if (isPerchase){
                                //정기구독 유지중인 사람
                                if (bsVO.isAutoRenewing()){

                                }
                                //정기구독 취소한사람
                                else {
                                   
                                }
                            }
                            //해당앱 정기구독 구입한적이 없는 사람
                            else {
                               
                            }

                        }
                    }
                    //앱스토어에서 정기결제 한번도 한적 없는 사람
                    else {
                    }

                }
                //connection fail
                else{
                
                }
            }

            @Override
            public void onBillingServiceDisconnected() {
// 
            }
        });


    }

- 필요할때 상태를 체크해 서버에 업데이트를 한다.

 

결제상태 체크 2번째

콜백

    public interface CheckPurchaseCallback{
        void onSubscribe(BillingStateVO bsVO);
        void onSubscribePending();
        void onSubscribeEND(BillingStateVO bsVO);
        void onNotPurchased(String msg);
        void onUnstableServer();
    }

구현

 public static void checkPurchaseAppCache(
     Context context 
     ,String itemID 
     ,CheckPurchaseCallback callback) 
     {
        BillingClient  cBillingClient = BillingClient.newBuilder(context)
                .enablePendingPurchases()
                .setListener((BillingResult billingResult, @Nullable List<Purchase> purchases)->{

                })
                .build();
        cBillingClient.startConnection(new BillingClientStateListener() {
            @Override
            public void onBillingSetupFinished(BillingResult billingResult) {
                //connection success
                if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) {
                    //check query
                    Purchase.PurchasesResult purchasesResult = cBillingClient.queryPurchases(BillingClient.SkuType.SUBS);
                    List<Purchase> list = purchasesResult.getPurchasesList();

                    //앱스토어에서 정기결제 구독한 사람
                    if (list.size() > 0 ){
                    	//제품이 하나라서 한개만 처리
                        Purchase purchase = list.get(0);
                        Gson gson = new Gson();
                        BillingStateVO bsVO = gson.fromJson(purchase.getOriginalJson(), BillingStateVO.class);
                        boolean isPerchase = false;
                        isPerchase =  itemID.equals(bsVO.getPackageName())&&itemID.equals(bsVO.getProductId());
                        // 정기 구독한 사람
                        if (isPerchase){

							//시간비교 
                            long diff = DateUtils.currentTime() - bsVO.getPurchaseTime();
                            int diffDays = (int)(diff/(24*60*60*1000));

                            switch (bsVO.getPurchaseState()){
                                //구독중 ...
                                case LifeInAppConfig.PURCHASE_STATE_PURCHASED:
                                    callback.onSubscribe(bsVO);
                                    break;

                                // 구독이 끝나도 30일간 사용 가능
                                case LifeInAppConfig.PURCHASE_STATE_CANCELED:
                                    //30일 지나감
                                    if (diffDays>30){
                                        callback.onSubscribeEND(bsVO);
                                    } else {
                                        callback.onSubscribe(bsVO);
                                    }
                                    break;

                                // 결제 수단문제로 구매 보류가 이뤄졌을때 ...
                                case LifeInAppConfig.PURCHASE_STATE_PENDING:
                                    callback.onSubscribePending();
                                    break;
                            }
                        }
                        // 정기구독 구입한적이 없는 사람
                        else {
                            callback.onNotPurchased("auto Renewing NOT YET");
                        }
                    }
                    //앱스토어에서 정기결제 한번도 한적 없는 사람
                    else {
                        callback.onNotPurchased(" auto renew nothing ");
                    }

                }
                //connection fail
                else{
                    callback.onUnstableServer();
                }
            }

            @Override
            public void onBillingServiceDisconnected() {

            }
        });
    }

purchase state : 중요한 녀석이다. 결제상태를 확인할 수 있다.

0 :  결제상태

1  :  취소상태

2 :  결제 보류 상태

 

 

 

사용

 private void checkInAppbilling(){
        LifeInAppBilling.checkPurchaseAppCache(
                this
                ,AppConfig.BILLING_ITEM
                ,new CheckPurchaseCallback() {
            @Override
            public void onSubscribe(BillingStateVO bsVO) {
                bsVO.printVO();

					
                if (!bsVO.isAutoRenewing()){
                    //정책에 따라 자동갱신안한경우 자동갱신하도록 유도
                }
            }
            @Override
            public void onSubscribePending() {
               //보류
            }
            @Override
            public void onSubscribeEND(BillingStateVO bsVO) {
                bsVO.printVO();
            }

            @Override
            public void onNotPurchased(String msg) {
               
            }

            @Override
            public void onUnstableServer() {
        
            }
        });
    }

 

 

- interface callback을 수정해 원하는 콜백으로 구현하면 됩니다.

BillingStateVO

// 상태조회시 넘어오는 값들.
public class BillingStateVO {

    private String orderId;
    private String packageName;
    private String productId;
    private long purchaseTime;//long
    private String purchaseState;//int
    private String purchaseToken;
    private boolean autoRenewing;//boolean
    private boolean acknowledged;//boolean

    public String getOrderId() {
        return orderId;
    }

    public void setOrderId(String orderId) {
        this.orderId = orderId;
    }

    public String getPackageName() {
        return packageName;
    }

    public void setPackageName(String pakageName) {
        this.packageName = pakageName;
    }

    public String getProductId() {
        return productId;
    }

    public void setProductId(String productId) {
        this.productId = productId;
    }

    public long getPurchaseTime() {
        return purchaseTime;
    }

    public void setPurchaseTime(long purchaseTime) {
        this.purchaseTime = purchaseTime;
    }

    public String getPurchaseState() {
        return purchaseState;
    }

    public void setPurchaseState(String purchaseState) {
        this.purchaseState = purchaseState;
    }

    public String getPurchaseToken() {
        return purchaseToken;
    }

    public void setPurchaseToken(String purchaseToken) {
        this.purchaseToken = purchaseToken;
    }

    public boolean isAutoRenewing() {
        return autoRenewing;
    }

    public void setAutoRenewing(boolean autoRenewing) {
        this.autoRenewing = autoRenewing;
    }

    public boolean isAcknowledged() {
        return acknowledged;
    }

    public void setAcknowledged(boolean acknowledged) {
        this.acknowledged = acknowledged;
    }


    public void printVO(){
        L.i("\n## orderId : "+orderId +
                "\n pakageName : "+packageName +
                "\n productId : "+productId +
                "\n purchaseTime : "+purchaseTime +
                "\n purchaseState : "+purchaseState +
                "\n purchaseToken : "+purchaseToken +
                "\n autoRenewing : "+autoRenewing +
                "\n acknowledged : "+acknowledged +"\n"
                );
    }
}

 

 

알파테스트

- 앱버전 -> 관리

 

-새 목록 만들기

- 테스트 참여 URL 복사한뒤 구글아이디가 로그인된 브라우저(휴대폰)에서 해당 url로 이동

- 테스트 참여 시작 클릭!(

-몇분뒤 테스터로 등록되고 플레이 스토어 들어가보면 [ 앱이름 (베타,알파) ]로 이름이 바뀌어있다,  테스트 앱 등록 완료

- 결제 테스트 진행!

 

 

 

 

테스트

여기다 이메일 추가하면 15분 정도뒤 테스터로 등록되 실제 결제 하듯이 결제할수있다.

 

 

일회성 제품 및 정기 결제의 구매 흐름은 비슷하지만, 정기 결제에는 정기 결제 갱신의 성공 또는 거부와 같은 추가 시나리오가 있습니다. 두 가지 상황 모두에서 애플리케이션을 테스트하는 데 도움이 되도록 '테스트 도구, 항상 승인' 및 '테스트 도구, 항상 거부' 결제 수단을 사용할 수 있습니다. 성공적인 정기 결제 시나리오 이상의 시나리오를 테스트하려면 이러한 결제 수단을 사용하세요.

테스트 정기 결제 갱신

테스트 정기 결제는 테스트를 돕기 위해 일반적인 경우보다 더 빨리 갱신됩니다. 다음 표에는 기간이 다양한 정기 결제의 테스트 갱신 시간이 나와 있습니다.

참고: 이러한 시간은 대략적인 것으로, 이벤트의 정확한 시간에는 약간의 변동이 있을 수 있습니다. 이러한 변동을 보완하려면 각 정기 결제 만료일 이후 API를 호출하여 현재 상태를 확인하세요.

프로덕션 정기 결제 기간테스트 구독 갱신

1주 5분
1개월 5분
3개월 10분
6개월 15분
1년 30분

참고: 테스트 정기 결제는 최대 6회 갱신됩니다.

반응형

+ Recent posts