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) |