Unity에서 구글 인앱 구현해보기

사전준비

에셋설치

다운로드 받기

유니티 코드

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using OnePF;
public class TestIAP : MonoBehaviour {
[SerializeField]
Text m_debugText;
[SerializeField]
string m_googleStoreKey;
[SerializeField]
Text m_consumableText;
[SerializeField]
Text m_managedText;
[SerializeField]
Text m_subscribeText;
const string SKU_CONSUMABLE = "com.demo.firebase.consumable";
const string SKU_MANAGED = "com.demo.firebase.managed";
const string SKU_SUBSCRIBE = "com.demo.firebase.subscribe";
bool _processingPayment = false;
private void Awake() {
OpenIABEventManager.billingSupportedEvent += OnBillingSupported;
OpenIABEventManager.billingNotSupportedEvent += OnBillingNotSupported;
OpenIABEventManager.queryInventorySucceededEvent += OnQueryInventorySucceeded;
OpenIABEventManager.queryInventoryFailedEvent += OnQueryInventoryFailed;
OpenIABEventManager.purchaseSucceededEvent += OnPurchaseSucceded;
OpenIABEventManager.purchaseFailedEvent += OnPurchaseFailed;
OpenIABEventManager.consumePurchaseSucceededEvent += OnConsumePurchaseSucceeded;
OpenIABEventManager.consumePurchaseFailedEvent += OnConsumePurchaseFailed;
OpenIABEventManager.transactionRestoredEvent += OnTransactionRestored;
OpenIABEventManager.restoreSucceededEvent += OnRestoreSucceeded;
OpenIABEventManager.restoreFailedEvent += OnRestoreFailed;
}
private void Start() {
// SKU's for iOS MUST be mapped. Mappings for other stores are optional
OpenIAB.mapSku(SKU_CONSUMABLE, OpenIAB_Android.STORE_GOOGLE, SKU_CONSUMABLE);
OpenIAB.mapSku(SKU_MANAGED, OpenIAB_Android.STORE_GOOGLE, SKU_MANAGED);
OpenIAB.mapSku(SKU_SUBSCRIBE, OpenIAB_Android.STORE_GOOGLE, SKU_SUBSCRIBE);
var options = new OnePF.Options();
options.prefferedStoreNames = new string[] { OpenIAB_Android.STORE_GOOGLE };
options.availableStoreNames = new string[] { OpenIAB_Android.STORE_GOOGLE };
options.storeKeys.Add(OpenIAB_Android.STORE_GOOGLE, m_googleStoreKey);
options.storeSearchStrategy = SearchStrategy.INSTALLER_THEN_BEST_FIT;
OpenIAB.init(options);
}
private void OnDestroy() {
OpenIABEventManager.billingSupportedEvent -= OnBillingSupported;
OpenIABEventManager.billingNotSupportedEvent -= OnBillingNotSupported;
OpenIABEventManager.queryInventorySucceededEvent -= OnQueryInventorySucceeded;
OpenIABEventManager.queryInventoryFailedEvent -= OnQueryInventoryFailed;
OpenIABEventManager.purchaseSucceededEvent -= OnPurchaseSucceded;
OpenIABEventManager.purchaseFailedEvent -= OnPurchaseFailed;
OpenIABEventManager.consumePurchaseSucceededEvent -= OnConsumePurchaseSucceeded;
OpenIABEventManager.consumePurchaseFailedEvent -= OnConsumePurchaseFailed;
OpenIABEventManager.transactionRestoredEvent -= OnTransactionRestored;
OpenIABEventManager.restoreSucceededEvent -= OnRestoreSucceeded;
OpenIABEventManager.restoreFailedEvent -= OnRestoreFailed;
}
// Verifies the developer payload of a purchase.
bool VerifyDeveloperPayload(string developerPayload) {
/*
* 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;
}
private void OnBillingSupported() {
Debug.Log("Billing is supported");
m_debugText.text = "Billing is supported";
OpenIAB.queryInventory(new string[] { SKU_CONSUMABLE, SKU_MANAGED, SKU_SUBSCRIBE });
}
private void OnBillingNotSupported(string error) {
Debug.Log("Billing not supported: " + error);
m_debugText.text = "Billing not supported: " + error;
}
private void OnQueryInventorySucceeded(Inventory inventory) {
Debug.Log("Query inventory succeeded: " + inventory);
m_debugText.text += "\nQuery inventory succeeded: " + inventory;
// Do we have the infinite ammo subscription?
Purchase infiniteAmmoPurchase = inventory.GetPurchase(SKU_SUBSCRIBE);
bool subscribedToInfiniteAmmo = (infiniteAmmoPurchase != null && VerifyDeveloperPayload(infiniteAmmoPurchase.DeveloperPayload));
Debug.Log("User " + (subscribedToInfiniteAmmo ? "HAS" : "DOES NOT HAVE") + " infinite ammo subscription.");
if (subscribedToInfiniteAmmo) {
}
// Check cowboy hat purchase
Purchase cowboyHatPurchase = inventory.GetPurchase(SKU_MANAGED);
bool isCowboyHat = (cowboyHatPurchase != null && VerifyDeveloperPayload(cowboyHatPurchase.DeveloperPayload));
Debug.Log("User " + (isCowboyHat ? "HAS" : "HAS NO") + " cowboy hat");
Purchase ammoPurchase = inventory.GetPurchase(SKU_CONSUMABLE);
if (ammoPurchase != null && VerifyDeveloperPayload(ammoPurchase.DeveloperPayload)) {
//Debug.Log("We have ammo. Consuming it.");
OpenIAB.consumeProduct(ammoPurchase);
}
m_consumableText.text = inventory.GetSkuDetails(SKU_CONSUMABLE).Price;
m_managedText.text = inventory.GetSkuDetails(SKU_MANAGED).Price;
m_subscribeText.text = inventory.GetSkuDetails(SKU_SUBSCRIBE).Price;
}
private void OnQueryInventoryFailed(string error) {
Debug.Log("Query inventory failed: " + error);
m_debugText.text += "\nQuery inventory failed: " + error;
}
private void OnPurchaseSucceded(Purchase purchase) {
Debug.Log("Purchase succeded: " + purchase.Sku + "; Payload: " + purchase.DeveloperPayload);
m_debugText.text += "\nPurchase succeded: " + purchase.Sku + "; Payload: " + purchase.DeveloperPayload;
if (!VerifyDeveloperPayload(purchase.DeveloperPayload)) {
return;
}
switch (purchase.Sku) {
case SKU_CONSUMABLE:
OpenIAB.consumeProduct(purchase);
return;
case SKU_MANAGED:
break;
case SKU_SUBSCRIBE:
break;
default:
Debug.LogWarning("Unknown SKU: " + purchase.Sku);
break;
}
_processingPayment = false;
}
private void OnPurchaseFailed(int errorCode, string error) {
Debug.Log("Purchase failed: " + error);
m_debugText.text += "\nPurchase failed: " + error;
_processingPayment = false;
}
private void OnConsumePurchaseSucceeded(Purchase purchase) {
Debug.Log("Consume purchase succeded: " + purchase.ToString());
m_debugText.text += "\nConsume purchase succeded: " + purchase.ToString();
_processingPayment = false;
}
private void OnConsumePurchaseFailed(string error) {
Debug.Log("Consume purchase failed: " + error);
m_debugText.text += "\nConsume purchase failed: " + error;
_processingPayment = false;
}
private void OnTransactionRestored(string sku) {
Debug.Log("Transaction restored: " + sku);
m_debugText.text += "\nTransaction restored: " + sku;
}
private void OnRestoreSucceeded() {
Debug.Log("Transactions restored successfully");
m_debugText.text += "\nTransactions restored successfully";
}
private void OnRestoreFailed(string error) {
Debug.Log("Transaction restore failed: " + error);
m_debugText.text += "\nTransaction restore failed: " + error;
}
public void OnClickPurchaseConsumable()
{
if (_processingPayment == true)
return;
OpenIAB.purchaseProduct(SKU_CONSUMABLE, "payload");
}
public void OnClickPurchaseManaged()
{
if (_processingPayment == true)
return;
OpenIAB.purchaseProduct(SKU_MANAGED, "payload");
}
public void OnClickPurchaseSubscribe()
{
if (_processingPayment == true)
return;
OpenIAB.purchaseProduct(SKU_SUBSCRIBE, "payload");
}
}


코드 간단 설명

Awake함수에서

 인앱 콜백 함수 등록


Start함수에서

 인앱 초기화

 상품 코드 등록


OnBillingSupported함수에서

 구글 인벤토리 쿼리 


OnQueryInventorySucceeded함수에서

 인벤토리에서 상품의 가격 정보 얻고

 Consumable 아이템이 존재하면, 소비시킴


OnClickPurchaseConsumable, OnClickPurchaseManaged, OnClickPurchaseSubscribe함수에서

 각 아이템 결제 시도


OnPurchaseSucceded함수에서

 결제 성공한 아이템들 적용

 특히, Consumable 아이템은 OpenIAB.consumeProduct함수를 호출

 호출하지 않으면, 재구매를 할 수 없다.


OnPurchaseFailed함수에서

 결제 실패처리

 구글 코드로 결제하면, 인앱 버그로 실패할텐데,

 인벤토리를 다시 쿼리해서 Consumable 아이템이 존재하면, OpenIAB.consumeProduct함수를 호출


테스트 방법

녹스에서 간단하게 테스트 함

1
2
3
#인앱 테스트시 인스톨 방법
C:\Program Files (x86)\Nox\bin>nox_adb.exe connect 127.0.0.1:62001
C:\Program Files (x86)\Nox\bin>nox_adb.exe install -i com.android.vending G:\project\playground\firebase\firebase2.apk

앱을 실행


Consumable 결제 요청


결제 수단은 코드 사용 선택


발급 받은 코드를 입력



결제 진행


아이템이 추가되었다고 팝업이 뜸


하지만 이 항목을 소유하고 있다고 오류 팝업이 뜸

유니티에서 제공하는 인앱 모듈도 마찬가지 결과가 나왔다.

결제는 완료된 상태라, 다시 Consumable을 구매하려고 하면, 

이미 소유했다고 구매할 수 없었다.

구글 인앱 버그인 것 같다.



앱 실행시, 인앱 초기화와 인벤토리 정보를 얻는 과정의 로그

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
09-08 11:32:24.437: I/Unity(29754): ********** Android OpenIAB plugin initialized **********
09-08 11:32:24.437: I/Unity(29754): 
09-08 11:32:24.437: I/Unity(29754): (Filename: ./artifacts/generated/common/runtime/DebugBindings.gen.cpp Line: 51)
09-08 11:32:24.467: D/OpenIAB-UnityPlugin(29754): createBroadcasts
09-08 11:32:24.467: D/OpenIAB-UnityPlugin(29754): Starting setup.
09-08 11:32:24.487: D/OpenIAB-UnityPlugin(29754): Setup finished.
09-08 11:32:24.487: D/OpenIAB-UnityPlugin(29754): Setup successful.
09-08 11:32:24.607: I/Unity(29754): Billing is supported
09-08 11:32:24.607: I/Unity(29754): 
09-08 11:32:24.607: I/Unity(29754): (Filename: ./artifacts/generated/common/runtime/DebugBindings.gen.cpp Line: 51)
09-08 11:32:24.607: D/dalvikvm(29754): GC_FOR_ALLOC freed 490K, 14% free 3556K/4108K, paused 2ms, total 2ms
09-08 11:32:24.637: D/OpenIAB-UnityPlugin(29754): Query inventory finished.
09-08 11:32:24.637: D/OpenIAB-UnityPlugin(29754): Query inventory was successful.
09-08 11:32:24.747: I/Unity(29754): Query inventory succeeded: {purchaseMap:{"com.demo.firebase.managed":{SKU:com.demo.firebase.managed;{"packageName":"com.demo.firebase2","productId":"com.demo.firebase.managed","purchaseTime":1503280530533,"purchaseState":0,"purchaseToken":"paanhlcdpglkdjoekkmcmije.AO-J1OwQsbRcHDnZdvaqVbgFV3Xb8r-bbVS9hO2E5qS0Yvd-5h2Mndu8vIVNsJGV9BHpXFNUNV8bO13MPXLQq8oIysVCqmBkBdAdXv_bpDL3hJtyBpUSt0jga1yNCNPb_09Pt24ljNY3"}},},skuMap:{"com.demo.firebase.managed":{[SkuDetails: type = inapp, SKU = com.demo.firebase.managed, title = com.demo.firebase.managed (demo firebase2), price = ₩3,300, description = com.demo.firebase.managed, priceValue=3300, currency=KRW]},"com.demo.firebase.consumable":{[SkuDetails: type = inapp, SKU = com.demo.firebase.consumable, title = com.demo.firebase.consumable (demo firebase2), price = ₩1,100, description = com.demo.firebase.consumable, priceValue=1100, currency=KRW]},"com.demo.firebase.subscribe":{[SkuDetails: type = subs, SKU = com.demo.firebase.subscribe, title = com.demo.firebase.subscribe
09-08 11:32:24.747: I/Unity(29754): User DOES NOT HAVE infinite ammo subscription.
09-08 11:32:24.747: I/Unity(29754): 
09-08 11:32:24.747: I/Unity(29754): (Filename: ./artifacts/generated/common/runtime/DebugBindings.gen.cpp Line: 51)
09-08 11:32:24.757: I/Unity(29754): User HAS cowboy hat
09-08 11:32:24.757: I/Unity(29754): 
09-08 11:32:24.757: I/Unity(29754): (Filename: ./artifacts/generated/common/runtime/DebugBindings.gen.cpp Line: 51)

결제 진행 중에 실패 처리가 되면

다시 인벤토리 쿼리를 하여, Consumable 아이템이 존재하면, 소비 시키는 과정의 로그

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
09-08 11:42:26.059: I/Unity(29896): Purchase failed: User canceled. (response: -1005:User cancelled)
09-08 11:42:26.059: I/Unity(29896): 
09-08 11:42:26.059: I/Unity(29896): (Filename: ./artifacts/generated/common/runtime/DebugBindings.gen.cpp Line: 51)
09-08 11:42:26.159: D/OpenIAB-UnityPlugin(29896): Query inventory finished.
09-08 11:42:26.159: D/OpenIAB-UnityPlugin(29896): Query inventory was successful.
09-08 11:42:26.169: I/Unity(29896): Query inventory succeeded: {purchaseMap:{"com.demo.firebase.managed":{SKU:com.demo.firebase.managed;{"packageName":"com.demo.firebase2","productId":"com.demo.firebase.managed","purchaseTime":1503280530533,"purchaseState":0,"purchaseToken":"paanhlcdpglkdjoekkmcmije.AO-J1OwQsbRcHDnZdvaqVbgFV3Xb8r-bbVS9hO2E5qS0Yvd-5h2Mndu8vIVNsJGV9BHpXFNUNV8bO13MPXLQq8oIysVCqmBkBdAdXv_bpDL3hJtyBpUSt0jga1yNCNPb_09Pt24ljNY3"}},"com.demo.firebase.consumable":{SKU:com.demo.firebase.consumable;{"packageName":"com.demo.firebase2","productId":"com.demo.firebase.consumable","purchaseTime":1504838516831,"purchaseState":0,"purchaseToken":"opdhejfogkfboknppkflboem.AO-J1OzTZtvLC16UcpXENtzUp6JfRi0wlZidqsQN8dgku-xUgIleZ2s12ITjJls9QeHd13YKfG7ycmQo8XrHnSE2mgCcp79peP_nNcBr7bUeBRo8J86Zg4E6_flL6fCcDSyO1Zg4zFtIdsKoq2xcY5AmzYB7QIlWyQ"}},},skuMap:{"com.demo.firebase.managed":{[SkuDetails: type = inapp, SKU = com.demo.firebase.managed, title = com.demo.firebase.managed (demo firebase2), price = ₩3,300, description = com.demo.firebase.managed, price
09-08 11:42:26.169: I/Unity(29896): User DOES NOT HAVE infinite ammo subscription.
09-08 11:42:26.169: I/Unity(29896): 
09-08 11:42:26.169: I/Unity(29896): (Filename: ./artifacts/generated/common/runtime/DebugBindings.gen.cpp Line: 51)
09-08 11:42:26.169: I/Unity(29896): User HAS cowboy hat
09-08 11:42:26.169: I/Unity(29896): 
09-08 11:42:26.169: I/Unity(29896): (Filename: ./artifacts/generated/common/runtime/DebugBindings.gen.cpp Line: 51)
09-08 11:42:26.939: D/OpenIAB-UnityPlugin(29896): Consumption finished. Purchase: PurchaseInfo(type:inapp): {"orderId":,"packageName":com.demo.firebase2,"productId":com.demo.firebase.consumable,"purchaseTime":1504838516831,"purchaseState":0,"developerPayload":,"token":opdhejfogkfboknppkflboem.AO-J1OzTZtvLC16UcpXENtzUp6JfRi0wlZidqsQN8dgku-xUgIleZ2s12ITjJls9QeHd13YKfG7ycmQo8XrHnSE2mgCcp79peP_nNcBr7bUeBRo8J86Zg4E6_flL6fCcDSyO1Zg4zFtIdsKoq2xcY5AmzYB7QIlWyQ}, result: IabResult: 0, Successful consume of sku com.demo.firebase.consumable (response: 0:OK)
09-08 11:42:26.939: D/OpenIAB-UnityPlugin(29896): Consumption successful. Provisioning.
09-08 11:42:26.939: I/Unity(29896): Consume purchase succeded: SKU:com.demo.firebase.consumable;{"packageName":"com.demo.firebase2","productId":"com.demo.firebase.consumable","purchaseTime":1504838516831,"purchaseState":0,"purchaseToken":"opdhejfogkfboknppkflboem.AO-J1OzTZtvLC16UcpXENtzUp6JfRi0wlZidqsQN8dgku-xUgIleZ2s12ITjJls9QeHd13YKfG7ycmQo8XrHnSE2mgCcp79peP_nNcBr7bUeBRo8J86Zg4E6_flL6fCcDSyO1Zg4zFtIdsKoq2xcY5AmzYB7QIlWyQ"}
09-08 11:42:26.939: I/Unity(29896): 
09-08 11:42:26.939: I/Unity(29896): (Filename: ./artifacts/generated/common/runtime/DebugBindings.gen.cpp Line: 51)