반응형

결제 모듈 흐름

1. store 연결여부
2. 제품이 존재하는지 체크
3. 제품을 구매
4. 구매 후 구매 확정

 

큰 결제 흐름은 위와 같으며 이에 맞춰 interface를 커스텀 하면 됩니다.

 

예제

public interface BillingModuleCallback {
    //store 연결여부
    public void onStoreConnection(BillingModule billingModule ,int result);
}

activity에 사용할 interface 부분을 이런식으로 만들어서 쓴다.


0.기본셋팅

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.han.goodmorningpops">

    <uses-permission android:name="android.permission.INTERNET" />
    
    ....
    
</manifest>
dependencies {
    //billingClient v3
    implementation 'com.android.billingclient:billing:3.0.0'
}

 

optional

public class BillingModule {

    private String PRODUCT_TYPE = BillingClient.SkuType.INAPP;

    //결제 모듈을 사용할 액티비티
    private Activity activity;
    //스토어에 등록된 제품명
    private List<String> sku_contents_list;
    //결제모듈
    private BillingClient billingClient;

	..........
}
	......
	/**
     * late init
     */
    private BillingModule(){ }
    private static class BillingPlaceHolder{
        private static final  BillingModule INSTANCE = new BillingModule();
    }
    public static BillingModule getInstance(){
        return BillingPlaceHolder.INSTANCE;
    }

    /**
     * step1
     * create instance
     */
    public BillingModule whereToUse(@NonNull Activity activity){
        this.activity = activity;
        return this;
    }

    /**
     * step2
     * set product list
     */
    public BillingModule setContentsList(@NonNull List<String> sku_contents_list){
        this.sku_contents_list = sku_contents_list;
        return this;
    }
    public BillingModule setPurchaseType(@BillingClient.SkuType String purchaseType){
        this.PRODUCT_TYPE = purchaseType;
        return this;
    }
    .......

-     편하게 사용하기위해 별도 클래스를 빼서 사용함.

 

 

activit에서 사용

//initialize
List<String> sku_contents_list = new ArrayList<>();
sku_contents_list.add("com.my.showmethemonby.product1");
billingModule = BillingModule.getInstance()
	.whereToUse(this)
	.setContentsList(sku_contents_list)
	.setBillingModuleCallback(this);
    
//start
billingModule.start();

 


1. store 연결여부

private BillingClient billingClient;

billingClient = BillingClient.newBuilder(activity)
                              .setListener(purchaseUpdateListener)
                              .enablePendingPurchases()
                              .build();
  • purchaseUpdateListener'3. 제품을 구매' 에서 설명.
billingClient.startConnection(new BillingClientStateListener() {
            @Override
            public void onBillingSetupFinished(@NonNull BillingResult billingResult) {
                //ready to connection
                if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) {
                
                    getProduct();
                }
                // billing is not ready
                else {
					
                }
            }

            @Override
            public void onBillingServiceDisconnected() {
                // Try to restart the connection on the next request to
                // Google Play by calling the startConnection() method.
				// 
            }
});
  • onBillingSetupFinished로 스토어와 통신이 이뤄지는지 체크한다.

 

optional

    public BillingModule start(){

        //check validation
        if (activity == null) throw new IllegalArgumentException("activity must be 'Not Null'");
        if (sku_contents_list == null) throw new IllegalArgumentException("sku product list must be 'not Null'");
        if (sku_contents_list.size()==0) throw new IllegalArgumentException("sku product list size greater than '0'");

        //initialize billingClient
        if (billingClient == null){
            billingClient = BillingClient.newBuilder(activity)
                    .setListener(purchaseUpdateListener)
                    .enablePendingPurchases()
                    .build();
        }

        //request to store connection open
        getStoreBillingConnection();
        return this;
    }

- optional을 선택한 분은 이렇게 시작을해 start() 사용시 필요한 인자들을 validation 할 수 있다.

- throw로 본인이 원하는 validation custom


2. 제품이 존재하는지 체크

private List<String> sku_contents_list;//product name in your store
    private void getProduct(){
        if (sku_contents_list==null || sku_contents_list.size() == 0) return;

        SkuDetailsParams.Builder params = SkuDetailsParams.newBuilder();
        params.setSkusList(sku_contents_list)
              .setType(BillingClient.SkuType.INAPP);
        billingClient.querySkuDetailsAsync(params.build(),getProductCallback);
    }
  • sku_contents_list를 가지고 스토어에 이런 제품이 있는지 질의한다.
  • paramssetType이 있는데 INAPP은 일회성 결제 , SUBS는 정기 구독 결제를 의미한다.
   //get product callback
    private SkuDetailsResponseListener getProductCallback = new SkuDetailsResponseListener() {
        @Override
        public void onSkuDetailsResponse(@NonNull BillingResult billingResult, 
        		@Nullable List<SkuDetails> list) {
            //연결성공
            if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK){
                //상품 가저오기 실패
                if (list == null){
                    return;
                }
                //상품을 가저왔으나 제품이 없음
                if (list.size()==0){
                    return;
                }

                //success
                for (SkuDetails details:list){
                    showBilling(details);
                }
            }else {
                //fail to connect
            }
        }
    };

-   SkuDetails listsku_contents_list에서 질의한 제품만큼 리턴된다.

    SkuDetails은 VO객체라서 for문을 써서 처리해도 되고 ,

    가지고 있다가 원하는 타이밍에 활용해도된다.

 


3. 제품을 구매

    //결제화면 보여주기
    private void showBilling(SkuDetails skuDetails) {
        BillingFlowParams flowParams = BillingFlowParams.newBuilder()
                .setSkuDetails(skuDetails)
                .build();
        billingClient.launchBillingFlow(activity,flowParams);
        //go to PurchasesUpdatedListener
    }
    //결제 결과 처리
    private PurchasesUpdatedListener purchaseUpdateListener = new PurchasesUpdatedListener() {
        @Override
        public void onPurchasesUpdated(@NonNull BillingResult billingResult, @Nullable List<Purchase> list) {
            if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK && list != null) {

                //when you complete purchase , confirm product purchase
                for (Purchase purchase : list) {
                    handlePurchase(purchase);
                    //결제결과를 리턴
                }
            } else if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.USER_CANCELED) {
                // Handle an error caused by a user cancelling the purchase flow.
                // 사용자의 의지대로 취소했을 경우
            } else {
                // Handle any other error codes.
                // 알수 없는 에러
            }
        }
    };
  • 위에서 제품이 있는지 확인 했으니 가저온 SkuDetails 를 가지고 '저 결제 할게요' 하고 통신 하는 부분.
  • 결제 결과는 purchaseUpdateListener에 들어온다.
  • Purchase객체는 4번 내용에 이어지니 잘 가지고 있어야한다.

4. 구매 후 구매 확정

    private void handlePurchase(Purchase purchase){
        switch (PRODUCT_TYPE){
            case BillingClient.SkuType.INAPP:
                handlePurchaseINAPP(purchase);
                break;
            case BillingClient.SkuType.SUBS:
                handlePurchaseSUBS(purchase);
                break;
            default:
                throw new IllegalArgumentException("Undefined purchase type!");
        }
    }

-      위에서 설명한것처럼 두가지 결제 타입이 있고 결제 타입에 따라 처리하는 method가 다르다.

        둘중 하나 만 사용한다면 이부분은 없어도 되는 부분

 

 

일회성 결제 BillingClient.SkuType.INAPP

  //소비성 결제 상품 결제확정
    private void handlePurchaseINAPP(Purchase purchase) {
        ConsumeParams consumeParams =
                ConsumeParams.newBuilder()
                        .setPurchaseToken(purchase.getPurchaseToken())
                        .build();
        billingClient.consumeAsync(consumeParams, consumeResponseListener);
    }
    //소비성 결제 상품 결제확정 callback
    private ConsumeResponseListener consumeResponseListener = new ConsumeResponseListener() {
        @Override
        public void onConsumeResponse(BillingResult billingResult, String purchaseToken) {
            if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) {
                // Handle the success of the consume operation.

            } else {
                //fail connection
                
            }
        }
    };

-      3에서 언급한듯이 Purchase객체를 잘가지고 있다가 소비성 결제 상품결제 확정했다는 것을 구글에 알림

 

 

 

정기구독결제 BillingClient.SkuType.SUBS

  private void handlePurchaseSUBS(Purchase purchase) {
        if (purchase.getPurchaseState() == Purchase.PurchaseState.PURCHASED) {
            if (!purchase.isAcknowledged()) {
                AcknowledgePurchaseParams acknowledgePurchaseParams =
                        AcknowledgePurchaseParams.newBuilder()
                                .setPurchaseToken(purchase.getPurchaseToken())
                                .build();
                billingClient.acknowledgePurchase(acknowledgePurchaseParams, 
                acknowledgePurchaseResponseListener);
            }
        } else if (purchase.getPurchaseState() == Purchase.PurchaseState.PENDING){
            //거래 중지 등등 ... 결제관련 문제가 발생했을때
        } else {
            //그외 알 수 없는 에러들...
        }
    }
    //인앱결제 확정 callback
    private AcknowledgePurchaseResponseListener acknowledgePurchaseResponseListener = new AcknowledgePurchaseResponseListener() {
        @Override
        public void onAcknowledgePurchaseResponse(@NonNull BillingResult billingResult) {
            if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) {
                //success
            } else {
                //fail
            }
        }
    };

-      구독상품 같은 경우는 결제 유예라는 시스템을 두어 , 사용자가 결제문제를 해결할때까지 기다려준다.

 

 


참고용 

import android.app.Activity;
import android.widget.Toast;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;

import com.android.billingclient.api.AcknowledgePurchaseParams;
import com.android.billingclient.api.AcknowledgePurchaseResponseListener;
import com.android.billingclient.api.BillingClient;
import com.android.billingclient.api.BillingClientStateListener;
import com.android.billingclient.api.BillingFlowParams;
import com.android.billingclient.api.BillingResult;
import com.android.billingclient.api.ConsumeParams;
import com.android.billingclient.api.ConsumeResponseListener;
import com.android.billingclient.api.Purchase;
import com.android.billingclient.api.PurchasesUpdatedListener;
import com.android.billingclient.api.SkuDetails;
import com.android.billingclient.api.SkuDetailsParams;
import com.android.billingclient.api.SkuDetailsResponseListener;
import java.util.List;

public class BillingModule {

    private String PRODUCT_TYPE = BillingClient.SkuType.INAPP;

    /**
     *
     */
    private Activity activity;
    private List<String> sku_contents_list;//product name in your store

    private BillingClient billingClient;
    private PurchasesUpdatedListener purchaseUpdateListener;
    private BillingModuleCallback billingModuleCallback;

    /**
     * late init
     */
    private BillingModule(){ }
    private static class BillingPlaceHolder{
        private static final  BillingModule INSTANCE = new BillingModule();
    }
    public static BillingModule getInstance(){
        return BillingPlaceHolder.INSTANCE;
    }

    /**
     * step1
     * create instance
     */
    public BillingModule whereToUse(@NonNull Activity activity){
        this.activity = activity;
        return this;
    }

    /**
     * step2
     * set product list
     */
    public BillingModule setContentsList(@NonNull List<String> sku_contents_list){
        this.sku_contents_list = sku_contents_list;
        return this;
    }
    public BillingModule setPurchaseType(@BillingClient.SkuType String purchaseType){
        this.PRODUCT_TYPE = purchaseType;
        return this;
    }

    /**
     * optional
     * add listener
     */
    public BillingModule setBillingModuleCallback(BillingModuleCallback billingModuleCallback){
        this.billingModuleCallback = billingModuleCallback;
        return this;
    }
//    public BillingModule setPurchaseUpdateListener(PurchasesUpdatedListener purchaseUpdateListener){
//        this.purchaseUpdateListener = purchaseUpdateListener;
//        return this;
//    }
//    public BillingModule setPurchaseUpdateListener(){
//        this.purchaseUpdateListener = purchaseUpdateListener2;
//        return this;
//    }


    /**
     * ### FLOW1: open connection with store ###
     * open
     */
    public BillingModule start(){

        //check validation
        if (activity == null) throw new IllegalArgumentException("activity must be 'Not Null'");
        if (sku_contents_list == null) throw new IllegalArgumentException("sku product list must be 'not Null'");
        if (sku_contents_list.size()==0) throw new IllegalArgumentException("sku product list size greater than '0'");

        //initialize billingClient
        if (billingClient == null){
            billingClient = BillingClient.newBuilder(activity)
                    .setListener(purchaseUpdateListener2)
                    .enablePendingPurchases()
                    .build();
        }

        //request to store connection open
        getStoreBillingConnection();
        return this;
    }
    //request to store connection open
    private void getStoreBillingConnection(){
        billingClient.startConnection(new BillingClientStateListener() {
            @Override
            public void onBillingSetupFinished(@NonNull BillingResult billingResult) {
                //ready to connection
                if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) {
                    if (billingModuleCallback !=null){
                        billingModuleCallback.onStoreConnection(BillingModule.this,BillingModuleCallback.OpenStore.CONNECTION_OK);
                    }

                    getProduct();
                }
                // billing is not ready
                else {
                    if (billingModuleCallback !=null){
                        billingModuleCallback.onStoreConnection(BillingModule.this,BillingModuleCallback.OpenStore.CONNECTION_FAIL);
                    }
                }
            }

            @Override
            public void onBillingServiceDisconnected() {
                // Try to restart the connection on the next request to
                // Google Play by calling the startConnection() method.
                if (billingModuleCallback !=null) {
                    billingModuleCallback.onStoreConnection(BillingModule.this,BillingModuleCallback.OpenStore.CONNECTION_CANCEL);
                }
            }
        });
    }

    /**
     *  ### FLOW 2: Check if there is a product in store ###
     *
     *  BillingClient.SkuType.INAPP : 일회성 상품
     *  BillingClient.SkuType.SUBS : 구독상품
     *
     *  check product
     */
    private void getProduct(){
        if (sku_contents_list==null || sku_contents_list.size() == 0) return;

        SkuDetailsParams.Builder params = SkuDetailsParams.newBuilder();
        params.setSkusList(sku_contents_list).setType(BillingClient.SkuType.INAPP);
        billingClient.querySkuDetailsAsync(params.build(),getProductCallback);
    }
    //get product callback
    private SkuDetailsResponseListener getProductCallback = new SkuDetailsResponseListener() {
        @Override
        public void onSkuDetailsResponse(@NonNull BillingResult billingResult, @Nullable List<SkuDetails> list) {
            //연결성공
            if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK){
                //상품 가저오기 실패
                if (list == null){
                    if (billingModuleCallback !=null) {
                        billingModuleCallback.onCheckProduct(BillingModule.this,BillingModuleCallback.CheckProduct.PRODUCT_NULL);
                    }
                    return;
                }
                //can get
                if (list.size()==0){
                    if (billingModuleCallback !=null) {
                        billingModuleCallback.onCheckProduct(BillingModule.this,BillingModuleCallback.CheckProduct.PRODUCT_EMPTY);
                    }
                    return;
                }

                //success
                if (billingModuleCallback !=null) {
                    billingModuleCallback.onCheckProduct(BillingModule.this,BillingModuleCallback.CheckProduct.PRODUCT_GET);
                }
                for (SkuDetails details:list){
                    showBilling(details);
                }
            }else {
                //fail to connect
                if (billingModuleCallback !=null) {
                    billingModuleCallback.onCheckProduct(BillingModule.this,BillingModuleCallback.CheckProduct.CONNECTION_FAIL);
                }
            }
        }
    };


    /**
     *  ### FLOW3: show Billing module and Check purchase your Product ###
     * show purchase
     */
    //결제화면 보여주기
    private void showBilling(SkuDetails skuDetails) {
        BillingFlowParams flowParams = BillingFlowParams.newBuilder()
                .setSkuDetails(skuDetails)
                .build();
        billingClient.launchBillingFlow(activity,flowParams);
        //go to PurchasesUpdatedListener
    }
    //결제 결과 처리
    private PurchasesUpdatedListener purchaseUpdateListener2 = new PurchasesUpdatedListener() {
        @Override
        public void onPurchasesUpdated(@NonNull BillingResult billingResult, @Nullable List<Purchase> list) {
            if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK && list != null) {
                //when you complete purchase , confirm product purchase
                for (Purchase purchase : list) {
                    handlePurchase(purchase);
                }
            } else if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.USER_CANCELED) {
                // Handle an error caused by a user cancelling the purchase flow.
            } else {
                // Handle any other error codes.
            }
        }
    };

    /**
     * ### FLOW4: Confirm purchased your product ###
     *
     *
     * check purchase type
     * confirm purchased
     */
    private void handlePurchase(Purchase purchase){
        switch (PRODUCT_TYPE){
            case BillingClient.SkuType.INAPP:
                handlePurchaseINAPP(purchase);
                break;
            case BillingClient.SkuType.SUBS:
                handlePurchaseSUBS(purchase);
                break;
            default:
                throw new IllegalArgumentException("Undefined purchase type!");
        }
    }
    /**
     * 소비성 결제 BillingClient.SkuType.INAPP
     */
    //소비성 결제 상품 결제확정
    private void handlePurchaseINAPP(Purchase purchase) {
        ConsumeParams consumeParams =
                ConsumeParams.newBuilder()
                        .setPurchaseToken(purchase.getPurchaseToken())
                        .build();
        billingClient.consumeAsync(consumeParams, consumeResponseListener);
    }
    //소비성 결제 상품 결제확정 callback
    private ConsumeResponseListener consumeResponseListener = new ConsumeResponseListener() {
        @Override
        public void onConsumeResponse(BillingResult billingResult, String purchaseToken) {
            if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) {
                // Handle the success of the consume operation.
            } else {
                //fail connection
            }
        }
    };

    /**
     * 인앱결제 BillingClient.SkuType.SUBS
     * - 결제 확정
     *         int UNSPECIFIED_STATE = 0;
     *         int PURCHASED = 1;
     *         int PENDING = 2;
     */
    private void handlePurchaseSUBS(Purchase purchase) {
        if (purchase.getPurchaseState() == Purchase.PurchaseState.PURCHASED) {
            if (!purchase.isAcknowledged()) {
                AcknowledgePurchaseParams acknowledgePurchaseParams =
                        AcknowledgePurchaseParams.newBuilder()
                                .setPurchaseToken(purchase.getPurchaseToken())
                                .build();
                billingClient.acknowledgePurchase(acknowledgePurchaseParams, acknowledgePurchaseResponseListener);
            }
        } else if (purchase.getPurchaseState() == Purchase.PurchaseState.PENDING){
            //거래 중지 등등 ... 결제관련 문제가 발생했을때
        } else {
            //그외 알 수 없는 에러들...
        }
    }
    //인앱결제 확정 callback
    private AcknowledgePurchaseResponseListener acknowledgePurchaseResponseListener = new AcknowledgePurchaseResponseListener() {
        @Override
        public void onAcknowledgePurchaseResponse(@NonNull BillingResult billingResult) {
            if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) {
                //success
            } else {
                //fail
            }
        }
    };


    /**
     * etc tool
     */
    private void showToast(String msg){
        Toast.makeText(activity,msg,Toast.LENGTH_SHORT).show();
    }
}
반응형

+ Recent posts