Unity3D Integration Google In-app Billing V3

 작년에 cocos2d-x에 구글 인앱 빌링 버전 2 던전 샘플 연동던전 샘플을 최적화해 자바 jar 라이브러리로 만들어 봤었는데요, 이번에 V3 버전을 유니티3D에 작업하게 되면서 관련해서 정리해보겠습니다.

 현재는 구글 플레이 개발자 콘솔의 UI가 많이 바뀌긴 했지만 안드로이드 앱 등록이나 구글 인앱 빌링 아이템 추가등의 절차는 기존의 내용을 참고하시기 바랍니다.


1. 구글 인앱 빌링 라이브러리 설치 및 임포트
 먼저 인앱빌링 라이브러리 설치가 안되신 분들은 안드로이드 SDK 메니저를 실행해서 Extras의 Google Play Billing Library를 설치해줍니다. Rev가 4 인데 아직 V3라고 부르는 것 같네요.

 V2 연동과 비슷하게 인앱 빌링 V3의 샘플인 trivialdrive 샘플의 필요한 부분을 가져와야합니다. 안드로이드 sdk 폴더\sdk\extras\google\play_billing\samples\TrivialDrive 에 있는 샘플 프로젝트를 위와 같이 src/com/android 부분과 example/android/trivialdrivesample/util 만 가져옵니다. MainActivity.java는 기존에 작업 중인 안드로이드 플러그인 프로젝트의 MainActivity를 사용하므로 가져오지 않습니다.


 임포트 한 모습입니다. MainActivity가 들어있는 프로젝트 패키지명이 unityandroidfacebookjar인데 페이스북 플러그인 프로젝트에 같이 작업하다보니 그런것으로 신경 안쓰셔도 됩니다.


2. AndroidManifest.xml 수정

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

 V2에 비해 AndroidManifest.xml이 쉬워졌습니다. 위와같이 빌링 관련된 권한만 추가해주면 됩니다.


3. 인앱 빌링 초기화 및 인벤토리 요청

public class MainActivity extends UnityPlayerActivity {
...
// (arbitrary) request code for the purchase flow
// RC_REQUEST는 IabHelper용 콜백 결과를 구분하기 위한 상수
static final int RC_REQUEST = 10001;
private IabHelper mHelper;
...

public void InAppInit_U(String strPublicKey, boolean bDebug) {                                                
Log.d(LOG_TAG, "Creating IAB helper." + bDebug);                                                            
mHelper = new IabHelper(this, strPublicKey);                                                                
                                                                                                           
if(bDebug == true) {                                                                                        
mHelper.enableDebugLogging(true, "IAB");                                                                  
}                                                                                                            
                                                                                                           
mHelper.startSetup(new IabHelper.OnIabSetupFinishedListener() {                                              
                                                                                                           
@Override                                                                                                    
public void onIabSetupFinished(IabResult result) {                                                          
// TODO Auto-generated method stub                                                                        
boolean bInit = result.isSuccess();                                                                        
Log.d(LOG_TAG, "IAB Init " + bInit + result.getMessage());                                                
                                                                                                               
if(bInit == true) {                                                                                        
Log.d(LOG_TAG, "Querying inventory.");                                                                  
mHelper.queryInventoryAsync(mGotInventoryListener);                                                      
}                                                                                                          
                                                                                                               
UnityPlayer.UnitySendMessage("GoogleIABManager", "InAppInitResult_J", String.valueOf(bInit));              
}                                                                                                            
});                                                                                                            
}                                                                                                              

 유니티3D에서 호출하는 InAppInit_U로 IabHelper 를 생성합니다. 인자로 넘어가는 strPublicKey는 구글 개발자 콘솔에 등록한 앱의 서비스 및 API 부분에 있는 라이선스 키입니다.

 초기화 성공 후 onIabSetupFinished에서 구매 후 소비되지 않은 아이템들이 있는지 확인을 위해 queryInventoryAsync를 실행 후 바로 유니티3D에는 인앱 빌링 초기화 결과를 알려줍니다.

                                                                                                               
// Listener that's called when we finish querying the items and subscriptions we own                          
IabHelper.QueryInventoryFinishedListener mGotInventoryListener = new IabHelper.QueryInventoryFinishedListener() {
    public void onQueryInventoryFinished(IabResult result, Inventory inventory) {                              
        if (result.isFailure()) {                                                                              
            Log.d(LOG_TAG, "Failed to query inventory: " + result);                                            
            SendConsumeResult(null, result);                                                                  
            return;                                                                                            
        }                                                                                                      
                                                                                                               
        /*                                                                                                    
         * Check for items we own. Notice that for each purchase, we check                                    
         * the developer payload to see if it's correct! See                                                  
         * verifyDeveloperPayload().                                                                          
         */                                                                                                    
                                                                                                               
        List<String> inappList = inventory.getAllOwnedSkus(IabHelper.ITEM_TYPE_INAPP);                        
                                                                                                               
        for(String inappSku : inappList) {                                                                    
        Purchase purchase = inventory.getPurchase(inappSku);                                                
        Log.d(LOG_TAG, "Consumeing ... " + inappSku);                                                        
        mHelper.consumeAsync(purchase, mConsumeFinishedListener);                                            
        }                                                                                                      
                                                                                                               
        Log.d(LOG_TAG, "Query inventory was successful.");                                                    
    }                                                                                                          
};

 구매 목록 쿼리 요청 후 콜백된 onQueryInventoryFinished에서 결과에 따라 처리하면 되는데 일단 인벤토리에 있는 것중 아이템 타입이 인앱인 것 즉, 소모성 아이템인 것을 모두 가져와 consumeAsyne를 통해 구글에 소비 요청을 합니다. 이때 바로 위처럼하면


List<String> inappList = inventory.getAllOwnedSkus(IabHelper.ITEM_TYPE_INAPP);

 위 부분에서 에러가 발생하는데요, getAllOwnedSkus가 public가 아니라서 그렇습니다. Inventory.java 의 71라인에 있는 부분에 public를 추가해줍니다.


//InAppBilling
public void InAppInit(bool bDebug = false)
{
string strPublicKey = "1234567890!@#$%^&*()";
curActivity.Call("InAppInit_U", strPublicKey, bDebug);
}

private void InAppInitResult_J(string strResult)
{
SetLog("InApp Init " + strResult);
}

 유니티3D에서 호출하는 부분입니다.


4. 아이템 구매 및 소비

public void InAppBuyItem_U(final String strItemId) {
runOnUiThread(new Runnable() {

@Override
public void run() {
// TODO Auto-generated method stub

        /* TODO: for security, generate your payload here for verification. See the comments on
         *        verifyDeveloperPayload() for more info. Since this is a SAMPLE, we just use
         *        an empty string, but on a production app you should carefully generate this. */
        String payload = "";
     
        mHelper.launchPurchaseFlow(UnityPlayer.currentActivity
        , strItemId, RC_REQUEST, mPurchaseFinishedListener, payload);
     
        Log.d(LOG_TAG, "InAppBuyItem_U " + strItemId);
}
});
}

 아이템 구매는 V2와 마찬가지로 아이템 id 만 있으면 요청할 수 있습니다. RC_REQUEST는 위에서 추가했던 상수구요.


// Callback for when a purchase is finished
IabHelper.OnIabPurchaseFinishedListener mPurchaseFinishedListener = new IabHelper.OnIabPurchaseFinishedListener() {
    public void onIabPurchaseFinished(IabResult result, Purchase purchase) {
        Log.d(LOG_TAG, "Purchase finished: " + result + ", purchase: " + purchase);
     
        if(purchase != null) {
        if (!verifyDeveloperPayload(purchase)) {
                Log.d(LOG_TAG, "Error purchasing. Authenticity verification failed.");
            }
         
            mHelper.consumeAsync(purchase, mConsumeFinishedListener);
        }
        else {        
            UnityPlayer.UnitySendMessage("GoogleIABManager", "InAppBuyItemResult_J", String.valueOf(result.getResponse()));
        }
    }
};

 구매 요청 완료 후 오는 콜백 처리 부분입니다. V2에서는 구매 완료 후 소비를 바로 하지 않아도 되었는데 V3에서는 바로 소비를 하도록 강제해 중복 구매를 방지하고 있네요. 유니티3D에는 구매 실패에 대한 것만 일단 넘겨줍니다.


/** Verifies the developer payload of a purchase. */
boolean verifyDeveloperPayload(Purchase p) {
    String payload = p.getDeveloperPayload();
 
    /*
     * TODO: verify that the developer payload of the purchase is correct. It will be
     * the same one that you sent when initiating the purchase.
     *
     * WARNING: Locally generating a random string when starting a purchase and
     * verifying it here might seem like a good approach, but this will fail in the
     * case where the user purchases an item on one device and then uses your app on
     * a different device, because on the other device you will not have access to the
     * random string you originally generated.
     *
     * So a good developer payload has these characteristics:
     *
     * 1. If two different users purchase an item, the payload is different between them,
     *    so that one user's purchase can't be replayed to another user.
     *
     * 2. The payload must be such that you can verify it even when the app wasn't the
     *    one who initiated the purchase flow (so that items purchased by the user on
     *    one device work on other devices owned by the user).
     *
     * Using your own server to store and verify developer payloads across app
     * installations is recommended.
     */
 
    return true;
}

 인증 강화를 위한 부분인 듯한데 일단 저도 자세히 리서치를 하지 않아 샘플 그대로 true를 리턴하게 해줬습니다.


// Called when consumption is complete
IabHelper.OnConsumeFinishedListener mConsumeFinishedListener = new IabHelper.OnConsumeFinishedListener() {
    public void onConsumeFinished(Purchase purchase, IabResult result) {
        Log.d(LOG_TAG, "Consumption finished. Purchase: " + purchase + ", result: " + result);
        SendConsumeResult(purchase, result);
    }
};

protected void SendConsumeResult(Purchase purchase, IabResult result) {
JSONObject jsonObj = new JSONObject();
 
    try {
jsonObj.put("Result", result.getResponse());
if(purchase != null) {
      jsonObj.put("OrderId", purchase.getOrderId());
          jsonObj.put("Sku", purchase.getSku());
          jsonObj.put("purchaseData", purchase.getOriginalJson());
          jsonObj.put("signature", purchase.getSignature());
      }
} catch (JSONException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
 
UnityPlayer.UnitySendMessage("GoogleIABManager", "InAppConsumeResult_J", jsonObj.toString());
}

 소비 요청에 대한 콜백입니다. 유니티3D에 정보를 넘길 때 purchase에 있는 orderid와 상품id 값인 sku를 json에 추가합니다. 해당 데이터는 서버에서 영수증 검증을 위한 데이터인 purchaseData에 이미 포함된 것이지만 유니티에서 편히 사용하라고 따로 추가한 것입니다. signature도 서버에서 영수증 검증에 필요한 값이라 같이 넘겨줍니다.


public void InAppBuyItem(string strItemId)
{
curActivity.Call("InAppBuyItem_U", strItemId);
}

/*
 * // IAB Helper error codes
    public static final int IABHELPER_ERROR_BASE = -1000;
    public static final int IABHELPER_REMOTE_EXCEPTION = -1001;
    public static final int IABHELPER_BAD_RESPONSE = -1002;
    public static final int IABHELPER_VERIFICATION_FAILED = -1003;
    public static final int IABHELPER_SEND_INTENT_FAILED = -1004;
    public static final int IABHELPER_USER_CANCELLED = -1005;
    public static final int IABHELPER_UNKNOWN_PURCHASE_RESPONSE = -1006;
    public static final int IABHELPER_MISSING_TOKEN = -1007;
    public static final int IABHELPER_UNKNOWN_ERROR = -1008;
    public static final int IABHELPER_SUBSCRIPTIONS_NOT_AVAILABLE = -1009;
    public static final int IABHELPER_INVALID_CONSUMPTION = -1010;
 */
private void InAppBuyItemResult_J(string strResult)
{
int iResult = System.Convert.ToInt32(strResult);
switch(iResult)
{
case -1005:
SetLog("InAppBuyItem Cancel" );
break;
default:
SetLog("InAppBuyItem Failed");
break;
}
}

/*
 * // Billing response codes
    public static final int BILLING_RESPONSE_RESULT_OK = 0;
    public static final int BILLING_RESPONSE_RESULT_USER_CANCELED = 1;
    public static final int BILLING_RESPONSE_RESULT_BILLING_UNAVAILABLE = 3;
    public static final int BILLING_RESPONSE_RESULT_ITEM_UNAVAILABLE = 4;
    public static final int BILLING_RESPONSE_RESULT_DEVELOPER_ERROR = 5;
    public static final int BILLING_RESPONSE_RESULT_ERROR = 6;
    public static final int BILLING_RESPONSE_RESULT_ITEM_ALREADY_OWNED = 7;
    public static final int BILLING_RESPONSE_RESULT_ITEM_NOT_OWNED = 8;
 */
private void InAppConsumeResult_J(string strResult)
{
JsonData jData = JsonMapper.ToObject(strResult);

int iResult = System.Convert.ToInt32(jData["Result"].ToString());
switch(iResult)
{
case 0:
string strOrderId = jData["OrderId"].ToString();
string strSku = jData["Sku"].ToString();
  string strPurchaseData = jData["purchaseData"].ToString();
  string strSignature = jData["signature"].ToString();
SetLog("InAppConsume Success." + strOrderId + strSku);
break;
default:
SetLog("InAppConsume Failed");
break;
}
}

 유니티3D쪽 인앱 구매 처리 부분입니다. strPurchaseData와 strSignature 를 서버에 보내 영수증 확인을 처리하도록 하면 됩니다. 유니티3D에서 JSON을 사용하기 위해 LitJson을 사용했습니다.

// In App Billing
fXpos += 150;
fYpos = 50;
if (GUI.Button (new Rect(fXpos, fYpos, 100, 50), "InAppInit") == true)
{
bool bDebug = true;
FacebookManager.GetInstance().InAppInit(bDebug);
}
fYpos += 50;
if (GUI.Button (new Rect(fXpos, fYpos, 100, 50), "BuyItem") == true)
{
string strItemId = "testinappitem1";
FacebookManager.GetInstance().InAppBuyItem(strItemId);
}

 이건 GUI 처리 부분이구요.


5. 후처리

@Override
public void onDestroy() {
    super.onDestroy();
 
    // very important:
    Log.d(LOG_TAG, "Destroying helper.");
    if (mHelper != null) mHelper.dispose();
    mHelper = null;
}

public void onActivityResult(int requestCode, int resultCode, Intent data) {
      Log.d(LOG_TAG, "onActivityResult(" + requestCode + "," + resultCode + "," + data);
      if(requestCode == RC_REQUEST) {
          // Pass on the activity result to the helper for handling
          if (!mHelper.handleActivityResult(requestCode, resultCode, data)) {
              // not handled, so handle it ourselves (here's where you'd
              // perform any handling of activity results not related to in-app
              // billing...
              super.onActivityResult(requestCode, resultCode, data);
          }
          else {
              Log.d(LOG_TAG, "onActivityResult handled by IABUtil.");
          }
      }
}

 구매 요청후 액티비티가 다시 게임으로 돌아올 때인 onActivityResult에서 RC_REQUEST를 가지고 인앱 빌링 처리 후인지 구분합니다. onDestroy는 따로 설명 안드려도 되겠죠?


6. 빌드 이슈

 플러그인 프로젝트를 빌드 후 jar 파일로 익스포트시 NoClassDefFoundError을 처리하기 위해 위와같이 gen/com/android/vending/billing 을 체크해서 IInAppBillingService.java도 포함시켜줘야 합니다.


 이제 실행해서 초기화 후 구매를 시도하면 위와같은 화면이 뜹니다. V2때는 테스트 주문도 실제 구매가 되더니 테스트 주문이므로 청구되지 않습니다. 라는 문구가 있네요.

 실제로도 구글 플레이 콘솔에 가보면 위와같이 구매 시도 완료했던 것들이 모두 보류 중으로 되어있어서 카드 결제가 되지 않았습니다.

 이건 작년에 cocos2d-x에 V2 연동시 구매 테스트 했던 내역인데 모두 일일이 취소를 했었죠.

 V2에 비해 많이 간단해졌네요. 임포트한 소스 수정도 최소화 되었구요.

이 블로그의 인기 게시물

CMake Windows에 설치하기

Unity3D 안드로이드 Keystore 생성하기

Unity3D iOS Plugin 만들어 연동하기