In our game, we initially tried to handle unprocessed purchases by doing the following:
-
Try to Purchase product.
-
The app store interface will open for the user to insert his payment info.
-
After entering your info and before the app store interface closes itself (while it’s processing), close the app.
-
While opening the game again, on the first loading screen (a splash screen scene), the ProcessPurchase callback will send the receipt to be validated on our server and return PurchaseProcessingResult.Pending.
-
When the receipt is validated, the product will be added to a list of UnhandledProducts so we can track which products we need to give to the player when the game finishes loading.
-
When the game is loaded, we call IStoreController.ConfirmPendingPurchase using the first product from the UnhandledProducts while also giving the player that reward.
-
When the user reopens the app, steps 4-6 will happen again with the next pending reward. We need to reward only 1 item at a time for unrelated reasons.
All of this works as expected and the player can still purchase the same consumable item again, however, for some reason, if the player had “broken” a purchase before the version where IAP Restoration was implemented (went through steps 1-3 in earlier versions of the game), calling IStoreController.ConfirmPendingPurchase only prevents the ProcessPurchase callback from being called again for that product but does not actually consume the item.
If this player tries to purchase the same consumable again, the app store interface says that he already owns that item, and like I’ve said before, ProcessPurchase does NOT get called again.
Also, if this player tries to erase the game’s data and cache (to completely erase the game’s save) and opens the game again, the ProcessPurchase callback will be called again for all of the previously broken items. This does not happen when the purchase is properly consumed.
The game does not crash and no errors happen.
If anyone from the Unity team wishes to join the Test versions for Android just let me know.
I will paste here all 3 scripts involved in this process
IAPStoreHandler
using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Purchasing;
using Firebase.Functions;
using Firebase.Extensions;
using System.Threading.Tasks;
namespace com.dod.spacezoo.iap
{
public class IAPStoreHandler : IStoreListener
{
public static bool IsInitialized { get; private set; }
private static ProductCatalog Catalog { get; set; }
public IStoreController Controller { get; private set; }
private IExtensionProvider Extension { get; set; }
private IAPStoreButton PurchasingButton { get; set; }
private PurchaseEventArgs PurchasingEvent { get; set; }
private bool IsPurchasing { get; set; }
public static List<Product> UnhandledProducts;
private static IAPStoreHandler instance;
public static IAPStoreHandler Instance
{
get
{
if (instance == null)
{
instance = new IAPStoreHandler();
}
return instance;
}
}
public Product GetProduct(string productID)
{
if (Controller != null && Controller.products != null && !string.IsNullOrEmpty(productID))
{
return Controller.products.WithID(productID);
}
Debug.LogError("[IAPStoreHandler] attempted to get unknown product " + productID);
return null;
}
#region INITIALIZATION
[RuntimeInitializeOnLoadMethod]
static void Initialize()
{
instance = new IAPStoreHandler();
IsInitialized = false;
Catalog = ProductCatalog.LoadDefaultCatalog();
if (!Catalog.IsEmpty())
{
Debug.Log($"[IAPStoreHandler] Initializing UnityPurchasing via IAP Store Handler IAP. Version {StandardPurchasingModule.Instance().Version}");
StandardPurchasingModule module = StandardPurchasingModule.Instance();
module.useFakeStoreUIMode = FakeStoreUIMode.StandardUser;
ConfigurationBuilder builder = ConfigurationBuilder.Instance(module);
PopulateConfigurationBuilder(ref builder, Catalog);
UnityPurchasing.Initialize(Instance, builder);
}
else
{
Debug.LogError("[IAPStoreHandler] Catalog is empty");
}
}
/// <summary>
/// Populate a ConfigurationBuilder with products from a ProductCatalog
/// </summary>
/// <param name="builder">Will be populated with product identifiers and payouts</param>
/// <param name="catalog">Source of product identifiers and payouts</param>
public static void PopulateConfigurationBuilder(ref ConfigurationBuilder builder, ProductCatalog catalog)
{
foreach (var product in catalog.allValidProducts)
{
if (product.allStoreIDs.Count > 0)
{
var ids = new IDs();
foreach (var storeID in product.allStoreIDs)
{
ids.Add(storeID.id, storeID.store);
}
builder.AddProduct(product.id, product.type, ids);
}
else
{
builder.AddProduct(product.id, product.type);
}
}
}
public void OnInitialized(IStoreController controller, IExtensionProvider extensions)
{
Controller = controller;
Extension = extensions;
IsInitialized = true;
foreach (IAPStoreButton button in IAPStoreButton.All)
{
button.UpdateText();
}
}
public void OnInitializeFailed(InitializationFailureReason error)
{
Debug.LogError(string.Format("[IAPStoreHandler] Purchasing failed to initialize. Reason: {0}", error.ToString()));
}
#endregion INITIALIZATION
#region PURCHASING
public void StartPurchase(string productID, IAPStoreButton iapStoreButton)
{
if (Controller == null)
{
Debug.LogError("[IAPStoreHandler] Purchase failed because Purchasing was not initialized correctly");
iapStoreButton.OnPurchaseFailed(null, PurchaseFailureReason.PurchasingUnavailable);
return;
}
if (IsPurchasing)
{
Debug.LogWarning("[IAPStoreHandler] Trying to purchase a second product while the first one is still going on. Please wait the first one to finish.");
return;
}
IsPurchasing = true;
PurchasingButton = iapStoreButton;
Controller.InitiatePurchase(productID);
}
public PurchaseProcessingResult ProcessPurchase(PurchaseEventArgs purchaseEvent)
{
PurchasingEvent = purchaseEvent;
Debug.Log("[IAPStoreHandler] Processing: " + purchaseEvent.purchasedProduct.definition.id);
ValidateReceiptFirebase(purchaseEvent);
return (PurchasingButton != null && PurchasingButton.ConsumePurchase) ? PurchaseProcessingResult.Complete : PurchaseProcessingResult.Pending;
}
public void OnPurchaseFailed(Product product, PurchaseFailureReason failureReason)
{
PurchasingButton.OnPurchaseFailed(product, failureReason);
PurchasingButton = null;
IsPurchasing = false;
}
#region SERVER VALIDATION
private void ValidateReceiptFirebase(PurchaseEventArgs purchaseEvent)
{
UnifiedReceipt receipt = JsonUtility.FromJson<UnifiedReceipt>(purchaseEvent.purchasedProduct.receipt);
Debug.LogFormat("[IAPStoreHandler] ValidateReceiptFirebase with receipt: Store: {0} TransactionID: {1} Payload: {2}", receipt.Store, receipt.TransactionID, receipt.Payload);
var functions = FirebaseFunctions.DefaultInstance;
var function = functions.GetHttpsCallable("validateReceipt_v2");
if (Application.platform == RuntimePlatform.IPhonePlayer)//iOS
{
string iOSdata = receipt.Payload;
//Debug.Log($"[IAPButton] Calling validateReceipt cloud function with iOS payload data: {iOSdata}");
function.CallAsync(iOSdata).ContinueWithOnMainThread(ValidationReturn);
}
else if (Application.platform == RuntimePlatform.Android)//android
{
GooglePayload payload = JsonUtility.FromJson<GooglePayload>(receipt.Payload);
//Debug.LogFormat($"[IAPButton] Calling validateReceipt cloud function with GooglePayload: {payload}");
var androidData = new Dictionary<string, object>();
androidData["data"] = payload.json;
androidData["signature"] = payload.signature;
function.CallAsync(androidData).ContinueWithOnMainThread(ValidationReturn);
}
else//unity editor
{
/*Test payload
* { signature: 'ZOK7heRfr9SKVkXu6rKQL8YR2ovEvZyLFRy0iytmQz31RxEYYlAJEu1VMD27It+BDWQ+bFAB0qY91uRnf2EwKg3ysHpS0JnOxOL2O1QpvcvP0jxb3ECinQfWTtzxPD8wNMgnqyNV4VBvjmnLMd+qljlrOKVrweUEeIxmRLxiy072AZYuu+iCygyvIjOFzA0+d0PNPvTiwPGoNWmTmpshS08V6Xl8u8hJgg9QnhPOvcX1anYqEBt8v0b9hN2hO+PbRAh4SosYkRbZF898E2TG494DS8YDyrpL9H6NJwV5d8CrVl0Axo4J0pmoFjY1PmaSLm4tFlaSEEsD4ynaF/ntOg==',
data: '{"orderId":"GPA.3346-3485-9108-62537","packageName":"com.overtimestudios.idlespacezoo","productId":"com.overtimestudios.idlespacezoo.ticketspack1","purchaseTime":1601492100062,"purchaseState":0,"developerPayload":"{\\"developerPayload\\":\\"\\",\\"is_free_trial\\":false,\\"has_introductory_price_trial\\":false,\\"is_updated\\":false,\\"accountId\\":\\"\\"}","purchaseToken":"hfghakcolemhkphonoeaadjf.AO-J1OwcKabX1lrbN0Snjs4TY4GWSC0hbbHG2ItDXGog-stkJht7Se739fix7u-o-Zi2SMILSYIdl-LmwRYeiJb2Tjz9oS6KyS7SuhG5fQEJf9PA4fm66bFLo96gKi84HE4aJsse2ht9YkhtcxfWq_cJ_SO-xuzQPVWtNx777vaKF5L1l2QAuJw"}' }
* */
const string testGoogleJSON = "{\"orderId\":\"GPA.3328-9127-0883-68116\",\"packageName\":\"com.dod.starzoo\",\"productId\":\"com.dod.starzoo.ticketspack1\",\"purchaseTime\":1602285429516,\"purchaseState\":0,\"developerPayload\":\"{\\\"developerPayload\\\":\\\"\\\",\\\"is_free_trial\\\":false,\\\"has_introductory_price_trial\\\":false,\\\"is_updated\\\":false,\\\"accountId\\\":\\\"\\\"}\",\"purchaseToken\":\"kbpglgnljkgdaaaomafdaiki.AO-J1OyBCISEWNE_7aFv8bNgsEJ9-ReEHqwrQ4ywZ91kzin_cmG9R5YNrwF6FLTML6peFIsAnxCPrOSM3dSmzgx-Sjw-nFDhgPjs1gyr7LFxwjFDs-Vl9Uqz_Jr2xSPPJaJFhY_C3I6E\"}";
const string testSignature = "hEb4hf0JaJv9XNc7eKbxbY8KFT5OgW896rbjZE7x/3FHYjXk/Fevf2gXMd621qQYFSv5gNSPecFvFTmb/BdWcIROBXqI85cAyO/nA8Jge58SXIuIbaZA/C7jiatVyEfXt8uez5BYSozXTKztXZAGZJtoG2Qji5yM3gv60OHwWQ1g7qMSibyg0lKr+t1fWdxF5uaVuEHojEFLz/7KG3Gwhjcb7cmv772Mh5NtQ2TrCkZ5ECv5B+q7N2gy5I+PLw/DkP35vHCqqI0kbWqs6u5YWcIz1fzr/UAalZgOr6VJzjF4AwcMGcyDYNy1C5cALSkqdXcwbN3pt1K2e3fZdQqbPA==]";
const string testAppleEncodedString = "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";
bool testWithAndroidPayload = true;
bool causeErrorInValidation = false;
if (testWithAndroidPayload)
{
var editorAndroidData = new Dictionary<string, object>();
editorAndroidData["data"] = testGoogleJSON;
editorAndroidData["signature"] = ((causeErrorInValidation) ? "aaa" : string.Empty) + testSignature;
Debug.LogFormat("[IAPStoreHandler] Building data for Unity Editor Validation: data: {0}, signature: {1}", testGoogleJSON, testSignature);
function.CallAsync(editorAndroidData).ContinueWithOnMainThread(ValidationReturn);
}
else
{
var editoriOSData = testAppleEncodedString + (causeErrorInValidation ? "aaaa" : string.Empty);
Debug.LogFormat("[IAPStoreHandler] Building data for Unity Editor Validation with iOS Payload: {0}", editoriOSData);
function.CallAsync(editoriOSData).ContinueWithOnMainThread(ValidationReturn);
}
}
}
public void ValidationReturn(Task<HttpsCallableResult> task)
{
if (task.IsFaulted)
{
foreach (var inner in task.Exception.InnerExceptions)
{
if (inner is FunctionsException)
{
var exception = (FunctionsException)inner;
// Function error code, will be INTERNAL if the failure
// was not handled properly in the function call.
var code = exception.ErrorCode;
var message = exception.Message;
Debug.LogErrorFormat($"[IAPStoreHandler] ValidationReturn() Exception: error code: {code} message: {message}");
Firebase.Crashlytics.Crashlytics.LogException(exception);
ReceiptValidationFinished(false, null, message);
}
}
}
else
{
try{
/*
{
"isValid":true,
"info":[
{"orderId":"GPA.3328-9127-0883-68116",
"packageName":"com.dod.starzoo",
"productId":"com.dod.starzoo.ticketspack1",
"purchaseTime":1602285429516,
"purchaseState":0,
"developerPayload":
"{\"developerPayload\":\"\",\"is_free_trial\":false,\"has_introductory_price_trial\":false,\"is_updated\":false,\"accountId\":\"\"}",
"purchaseToken":"kbpglgnljkgdaaaomafdaiki.AO-J1OyBCISEWNE_7aFv8bNgsEJ9-ReEHqwrQ4ywZ91kzin_cmG9R5YNrwF6FLTML6peFIsAnxCPrOSM3dSmzgx-Sjw-nFDhgPjs1gyr7LFxwjFDs-Vl9Uqz_Jr2xSPPJaJFhY_C3I6E",
"status":0,
"service":"google",
"transactionId":"kbpglgnljkgdaaaomafdaiki.AO-J1OyBCISEWNE_7aFv8bNgsEJ9-ReEHqwrQ4ywZ91kzin_cmG9R5YNrwF6FLTML6peFIsAnxCPrOSM3dSmzgx-Sjw-nFDhgPjs1gyr7LFxwjFDs-Vl9Uqz_Jr2xSPPJaJFhY_C3I6E",
"purchaseDate":1602285429516,
"quantity":1}]} */
Debug.Log("[IAPStoreHandler] Result JSON string: " + (string)task.Result.Data);
ValidationResult validationResult = JsonUtility.FromJson<ValidationResult>((string)task.Result.Data);
//TODO Find better solution, don't pass PurchasingData[] as parameter
ReceiptValidationFinished(validationResult.isValid, validationResult.info, validationResult.info.ToString());
}
catch(Exception e){
Debug.LogError(e);
}
}
}
private void ReceiptValidationFinished(bool success, PurchasingData[] info, string message)
{
try{
Debug.LogFormat("[IAPStoreHandler] ReceiptValidationFinished: success: {0}, message: {1}", success, message);
if (PurchasingButton != null)
{
if (success)
{
Debug.Log($"[IAPStoreHandler] Calling OnPurchaseCompleted on {PurchasingButton.gameObject.name}");
PurchasingButton.OnPurchaseCompleted(PurchasingEvent.purchasedProduct);
}
else
{
Debug.Log($"[IAPStoreHandler] Calling OnPurchaseFailed on {PurchasingButton.gameObject.name}");
PurchasingButton.OnPurchaseFailed(PurchasingEvent.purchasedProduct, PurchaseFailureReason.SignatureInvalid);
Firebase.Analytics.FirebaseAnalytics.SetUserProperty("cheater", "true");
}
PurchasingEvent = null;
PurchasingButton = null;
IsPurchasing = false;
}
else
{
if(success){
if(UnhandledProducts == null)
UnhandledProducts = new List<Product>();
Product validatedProduct = Controller.products.WithID(info[0].productId);
Debug.Log("[IAPStoreHandler] Added Unhandled Product: " + validatedProduct.definition.id);
UnhandledProducts.Add(validatedProduct);
}
else{
Debug.Log("[IAPStoreHandler] Cheater");
}
}
}
catch(Exception e){
Debug.LogError(e);
}
}
#endregion SERVER VALIDATION
#endregion PURCHASING
}
public class GooglePayload
{
public string json; //A JSON encoded string provided by Google; INAPP_PURCHASE_DATA
public string signature; //A signature for the json parameter, as provided by Google; INAPP_DATA_SIGNATURE
public override string ToString()
{
return string.Format("[GooglePayload: json={0}, signature={1}]", json, signature);
}
}
[Serializable]
public class ValidationResult
{
public bool isValid;
public PurchasingData[] info;
}
[Serializable]
public class PurchasingData{
public string productId;
public string orderId;
public string packageName;
}
}
IAPListenerHandler
IAPStoreButton