본문 바로가기
android

인앱 결제 쉽게 따라 하기...

by Billcoreatech Billcoreatech 2021. 5. 15.
반응형

인앱 결제를 하기 위해서 오늘도 구글링을 하시는 분들께...  기본적은 헤맴을 줄여보기 위해서 정리를 해 둡니다.

 

 

인앱 결제를 하려면 일단, 할 일은 앱을 하나 만들어서 구글 플레이에 등록을 하는 것이다. 등록하는 가이드는 구글링을 통해 많이 나와 있으니 생략한다.

 

여기서 등록한다고 해서 꼭 출시상태를 만들 필요는 없다. 알파테스트(비공개 테스트) 단계까지만 등록해도 된다. 

그리고 해야할 것은 인앱 상품을 만들어서 등록하는 것이다. 

 

이처럼 등록을 하고 나면 금방이 승인이 나지 않는 다.  등록하고 다음날 확인해 보는 게 마음 편한 방법이다.

승인이 확인되면,  manifest 부터 설정을 해 봐야 한다. 

    <uses-permission android:name="com.android.vending.BILLING"/>
    <uses-permission android:name="android.permission.INTERNET"/>

권한은 꼭 등록이 되어야 한다. internet 사용 권한도 등록해 두어야 한다. 

그다음은 build.gradle의 내용 중에서 다음은 선언 있어야 한다. 

dependencies {


    implementation 'com.android.billingclient:billing:3.0.3'
    implementation 'com.google.code.gson:gson:2.8.6'

}

이런저런 다른 것들도 필요하면 넣어야 한다. 저 예시는 빌링을 위한 필수 항목이다.  2021.8.2. 부터는 모든 앱이 Ver 3.0을 사용해야 한다고 권고하고 있다.  빌링은 아래 예시 처럼 전체 소스를 확인해야 쉽게 이해가 쉽게 될 것 같다.

반응형

아래 예시 처럼 하나 만들어 두고 사용하는 것도 좋은 방법이 될 것 같다. 이 예제에서 수정할 부분은 

billcode는 구글스토어에서 등록해 놓은 인앱 상품의 상품 ID가 되며,  아래 예시에는 firebase의 realtime database와 연동을 통해서 구매한 사용자의 목록을 관리하기 위한 billingUserBean 항목이 있으나, 이건 개발자의 선택에 따라 필요 없을 수 도 있다.  그런 부분을 제거해 버리면 그냥 그대로 사용할 수 있다.

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

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

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 com.billcoreatech.dream314.R;
import com.billcoreatech.dream314.util.StringUtil;
import com.google.gson.JsonObject;

import org.json.JSONException;
import org.json.JSONObject;

import java.util.ArrayList;
import java.util.List;

public class BillingManager<mConsumResListnere> implements PurchasesUpdatedListener {
    String TAG = "BillingManager" ;
    BillingClient mBillingClient ;
    Activity mActivity ;
    public List<SkuDetails> mSkuDetails ;
    public enum connectStatusTypes { waiting, connected, fail, disconnected }
    public connectStatusTypes connectStatus = connectStatusTypes.waiting ;
    private ConsumeResponseListener mConsumResListnere ;
    String billCode = "210420_onetime_pay" ; // 인앱결제 상품ID는 그때 그때 달라요

    public BillingManager (Activity _activity) {
        mActivity = _activity ;

        mBillingClient = BillingClient.newBuilder(mActivity)
                .setListener(this)
                .enablePendingPurchases()
                .build() ;
                
        mBillingClient.startConnection(new BillingClientStateListener() {
            @Override
            public void onBillingSetupFinished(@NonNull BillingResult billingResult) {
                Log.i(TAG, "respCode=" + billingResult.getResponseCode() ) ;
                if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) {
                    connectStatus = connectStatusTypes.connected ;
                    Log.i(TAG, "connected...") ;

                    Log.i(TAG, "resp=" + mBillingClient.queryPurchases(billCode).getBillingResult()
                             + "=" + mBillingClient.queryPurchases(billCode).getResponseCode());

                    getSkuDetailList() ;

                } else {
                    connectStatus = connectStatusTypes.fail ;
                    Log.i(TAG, "connected... fail ") ;
                }
            }

            @Override
            public void onBillingServiceDisconnected() {
                connectStatus = connectStatusTypes.disconnected ;
                Log.i(TAG, "disconnected ") ;
            }
        });

        mConsumResListnere = new ConsumeResponseListener() {
            @Override
            public void onConsumeResponse(@NonNull BillingResult billingResult, @NonNull String purchaseToken) {
                if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) {
                    Log.i(TAG, "사용끝 + " + purchaseToken) ;
                    return ;
                } else {
                    Log.i(TAG, "소모에 실패 " + billingResult.getResponseCode() + " 대상 상품 " + purchaseToken) ;
                    return ;
                }
            }
        };
    }

    public int purchase(SkuDetails skuDetails) {
        BillingFlowParams flowParams = BillingFlowParams.newBuilder()
                .setSkuDetails(skuDetails)
                .build();
        return mBillingClient.launchBillingFlow(mActivity, flowParams).getResponseCode();
    }


    public void getSkuDetailList() {
        List<String> skuIdList = new ArrayList<>() ;
        skuIdList.add(billCode);

        SkuDetailsParams.Builder params = SkuDetailsParams.newBuilder();
        params.setSkusList(skuIdList).setType(BillingClient.SkuType.INAPP); // INAPP 가 인앱결제라는 구분임.
        mBillingClient.querySkuDetailsAsync(params.build(), new SkuDetailsResponseListener() {
            @Override
            public void onSkuDetailsResponse(@NonNull BillingResult billingResult, @Nullable List<SkuDetails> skuDetailsList) {
                if (billingResult.getResponseCode() != BillingClient.BillingResponseCode.OK) {
                    Log.i(TAG, "detail respCode=" + billingResult.getResponseCode()) ;
                    return ;
                }
                if (skuDetailsList == null) {
                    Toast.makeText(mActivity, mActivity.getString(R.string.msgNotInfo), Toast.LENGTH_LONG).show();
                    return ;
                }
                Log.i(TAG, "listCount=" + skuDetailsList.size());
                for(SkuDetails skuDetails : skuDetailsList) {
                    Log.i(TAG, "\n" + skuDetails.getSku()
                            + "\n" + skuDetails.getTitle()
                            + "\n" + skuDetails.getPrice()
                            + "\n" + skuDetails.getDescription()
                            + "\n" + skuDetails.getFreeTrialPeriod()
                            + "\n" + skuDetails.getIconUrl()
                            + "\n" + skuDetails.getIntroductoryPrice()
                            + "\n" + skuDetails.getIntroductoryPriceAmountMicros()
                            + "\n" + skuDetails.getOriginalPrice()
                            + "\n" + skuDetails.getPriceCurrencyCode()) ;
                }
                mSkuDetails = skuDetailsList ;

            }
        });
    }

    /**
     * @param billingResult
     * @param purchases
     */
    @Override
    public void onPurchasesUpdated(@NonNull BillingResult billingResult, @Nullable List<Purchase> purchases) {

        userinfoDB = FirebaseDatabase.getInstance().getReference("UserInfoDB");
        if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK && purchases != null) {
            Log.i(TAG, "구매 성공>>>" + billingResult.getDebugMessage());
            JSONObject object = null ;
            String pID = "" ;
            String pDate = "" ;
            for(Purchase purchase : purchases) {
                Log.i(TAG, "성공값=" + purchase.getPurchaseToken()) ;
                Log.i(TAG, "성공값=" + purchase.getOriginalJson());
                try {
                    object = new JSONObject(purchase.getOriginalJson());
                    pID = object.getString("purchaseToken");
                    pDate = StringUtil.getDate(object.getLong("purchaseTime")); // 날자를 구하기 위해서

                } catch (JSONException e) {
                    e.printStackTrace();
                }
                Log.i(TAG, "token=" + pID + "" + pDate) ;
                ConsumeParams params = ConsumeParams.newBuilder()
                        .setPurchaseToken(pID)
                        .build() ;
                mBillingClient.consumeAsync(params, mConsumResListnere);
            }
        } else if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.USER_CANCELED) {
            Log.i(TAG, "결제 취소");
        } else {
            Log.i(TAG, "오류 코드=" + billingResult.getResponseCode()) ;
        }
    }
}

 

이제 위에서 작성한 billingManager 을 호출하는 MainActivity의 예시를 보자

onCreate 등에서 미리 billingManger 에 대한 선언을 해 두고,  화면에서 버튼을 클릭했거나, 결제 메뉴를 선택했을 떄 아래 부분만 추가해 주면 결제 흐름은 끝이다. 

BillingManager billingManager = new BillingManager(getActivity());

...

               if(billingManager.connectStatus == BillingManager.connectStatusTypes.connected) {
                    Log.i(TAG, "connected ..") ;
                    SkuDetails skuDetails = (SkuDetails) billingManager.mSkuDetails.get(0);
                    int iResp = billingManager.purchase(skuDetails) ;
                    Log.i(TAG, "iResp=" + iResp) ;
                }

물론 이 예시는 1회성 인앱 결제의 흐름을 따라가는 예시 이므로 정기 결제 흐름에 대한 부분은 추가로 더 확인을 해야 할 필요가 있다.  (정기결제 흐름을 만들다가 포기(?)한 이유는 정기결제 후에 결제 소모 구현을 해야 하는데, 잘 되지 않아서... ㅠㅠ;;)

 

이 처럼해 정리해 두면 차후에 라도 다른 거 인앱 결제를 구현할 때 복사해서 쓸 기본 소스는 준비하는 것이 될 것 같다.

 

끝.

반응형

댓글0