Skip to content

Commit ada341f

Browse files
authored
Merge pull request #13 from modio/hotfix/mobile-compilation
Added directives for mobile entitlements
2 parents f07692c + 2c7b4ef commit ada341f

9 files changed

+368
-4
lines changed

Platform/Mobile.meta

+8
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
+114
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
#if UNITY_ANDROID || UNITY_IOS
2+
#if MODIO_IN_APP_PURCHASING
3+
using System;
4+
using System.Collections.Generic;
5+
using System.Linq;
6+
using ModIO;
7+
using Newtonsoft.Json;
8+
using Newtonsoft.Json.Linq;
9+
using UnityEngine;
10+
using UnityEngine.Purchasing;
11+
using Logger = ModIO.Implementation.Logger;
12+
13+
namespace Plugins.mod.io.Platform.Mobile
14+
{
15+
public static class MobilePurchaseHelper
16+
{
17+
const string PayloadJsonKey = "json";
18+
const string PayloadKey = "payload";
19+
20+
// All unconsumed receipts
21+
static readonly Dictionary<string, PurchaseData> Purchases = new Dictionary<string, PurchaseData>();
22+
public static int PurchasesCount => Purchases.Count;
23+
24+
public static PurchaseData GetNextPurchase()
25+
{
26+
if (Purchases.Count == 0)
27+
return null;
28+
29+
var firstPurchase = Purchases.First();
30+
Purchases.Remove(firstPurchase.Key);
31+
return firstPurchase.Value;
32+
}
33+
34+
public static void CompleteValidation(Entitlement[] entitlements)
35+
{
36+
foreach (var entitlement in entitlements)
37+
{
38+
if (entitlement.entitlementConsumed && Purchases.TryGetValue(entitlement.transactionId, out PurchaseData data))
39+
data.CompleteValidation?.Invoke();
40+
}
41+
}
42+
43+
public static void QueuePurchase(Product product, Action completeValidation)
44+
{
45+
//Is purchase already queued?
46+
if (Purchases.TryGetValue(product.transactionID, out _))
47+
{
48+
Logger.Log(LogLevel.Verbose, $"Product {product.definition.id} was not added because it is already in the queue.");
49+
return;
50+
}
51+
52+
JObject receiptObject;
53+
try
54+
{
55+
receiptObject = JObject.Parse(product.receipt);
56+
}
57+
catch (Exception e)
58+
{
59+
Logger.Log(LogLevel.Error, $"Receipt parse failed: {e.Message}");
60+
return;
61+
}
62+
63+
bool keyFound = receiptObject.TryGetValue(PayloadKey, out JToken jsonToken);
64+
if(!keyFound)
65+
{
66+
Logger.Log(LogLevel.Error, $"Unable to get key from receipt object: {PayloadKey}");
67+
return;
68+
}
69+
70+
PurchaseData purchaseData = new PurchaseData
71+
{
72+
Payload = jsonToken.ToString(),
73+
Product = product,
74+
CompleteValidation = completeValidation,
75+
};
76+
77+
JObject payloadObject;
78+
try
79+
{
80+
payloadObject = JObject.Parse(purchaseData.Payload);
81+
}
82+
catch (Exception e)
83+
{
84+
Logger.Log(LogLevel.Error, $"Payload parse failed: {e.Message}");
85+
return;
86+
}
87+
88+
keyFound = payloadObject.TryGetValue(PayloadJsonKey, out jsonToken);
89+
if(!keyFound)
90+
{
91+
Logger.Log(LogLevel.Error, $"Unable to get key from receipt object: {PayloadJsonKey}");
92+
return;
93+
}
94+
95+
if (purchaseData.Payload != null && Application.platform == RuntimePlatform.Android)
96+
{
97+
purchaseData.PayloadJson = jsonToken.ToString();
98+
}
99+
100+
Purchases.Add(purchaseData.Product.transactionID, purchaseData);
101+
102+
//Call sync entitlements after every purchase
103+
ModIOUnity.SyncEntitlements((syncResult =>
104+
{
105+
if (syncResult.result.Succeeded())
106+
{
107+
Logger.Log(LogLevel.Verbose, $"Synced {syncResult.value.Length} purchase(s).");
108+
}
109+
}));
110+
}
111+
}
112+
}
113+
#endif
114+
#endif

Platform/Mobile/MobilePurchaseHelper.cs.meta

+11
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
+188
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
#if (UNITY_IOS || UNITY_ANDROID)
2+
#if MODIO_IN_APP_PURCHASING
3+
4+
using System;
5+
using System.Collections.Generic;
6+
using ModIO;
7+
using Plugins.mod.io.Platform.Mobile;
8+
using Unity.Services.Core;
9+
using Unity.Services.Core.Environments;
10+
using UnityEngine;
11+
using UnityEngine.Purchasing;
12+
using UnityEngine.Purchasing.Extension;
13+
using Logger = ModIO.Implementation.Logger;
14+
using LogLevel = ModIO.LogLevel;
15+
16+
namespace External
17+
{
18+
/// <summary>
19+
/// An Example of how to use the Unity Purchasing module for android and ios devices.
20+
/// </summary>
21+
public class MobilePurchasingExample : MonoBehaviour, IDetailedStoreListener
22+
{
23+
static IStoreController storeController;
24+
static IExtensionProvider storeExtensionProvider;
25+
List<ProductCatalogItem> catalogProducts;
26+
27+
public async void Awake()
28+
{
29+
InitializationOptions options = new InitializationOptions();
30+
31+
// Set environment based on build type
32+
#if UNITY_EDITOR || DEVELOPMENT_BUILD
33+
options.SetEnvironmentName("test");
34+
#else
35+
options.SetEnvironmentName("production");
36+
#endif
37+
Logger.Log(LogLevel.Verbose, "Initializing Unity Services");
38+
await UnityServices.InitializeAsync(options);
39+
}
40+
41+
public void Start()
42+
{
43+
ResourceRequest operation = Resources.LoadAsync<TextAsset>("IAPProductCatalog");
44+
operation.completed += HandleIAPCatalogLoaded;
45+
Logger.Log(LogLevel.Verbose, "Initialized Unity Services");
46+
}
47+
48+
bool IsInitialized()
49+
{
50+
return storeController != null && storeExtensionProvider != null;
51+
}
52+
53+
//Converts items from the IAP catalog for use by the Unity Purchasing module
54+
void HandleIAPCatalogLoaded(AsyncOperation operation)
55+
{
56+
ResourceRequest request = operation as ResourceRequest;
57+
Logger.Log(LogLevel.Verbose, $"Loaded Asset: {request?.asset}");
58+
59+
var catalog = JsonUtility.FromJson<ProductCatalog>((request?.asset as TextAsset)?.text);
60+
if (catalog.allProducts is List<ProductCatalogItem> productList)
61+
{
62+
catalogProducts = productList;
63+
}
64+
else
65+
{
66+
Logger.Log(LogLevel.Error, "Catalog data is corrupted!");
67+
return;
68+
}
69+
70+
Logger.Log(LogLevel.Verbose, $"Loaded Catalog with {catalog.allProducts.Count} items.");
71+
ConfigurationBuilder builder;
72+
#if UNITY_IOS
73+
builder = ConfigurationBuilder.Instance(StandardPurchasingModule.Instance(AppStore.AppleAppStore));
74+
#elif UNITY_ANDROID
75+
builder = ConfigurationBuilder.Instance(StandardPurchasingModule.Instance(AppStore.GooglePlay));
76+
#else
77+
builder = ConfigurationBuilder.Instance(StandardPurchasingModule.Instance());
78+
#endif
79+
80+
foreach (var p in catalog.allProducts)
81+
{
82+
Logger.Log(LogLevel.Verbose, $"Added product {p.id} to builder.");
83+
builder.AddProduct(p.id, p.type);
84+
}
85+
86+
Logger.Log(LogLevel.Verbose, $"UnityPurchasing Initialize called");
87+
UnityPurchasing.Initialize(this, builder);
88+
}
89+
90+
91+
// Method called when store initialization fails
92+
93+
public void OnInitialized(IStoreController controller, IExtensionProvider extensions)
94+
{
95+
storeController = controller;
96+
storeExtensionProvider = extensions;
97+
Logger.Log(LogLevel.Verbose, $"OnInitialized IAP Success");
98+
}
99+
100+
// Overloaded method called when store initialization fails with message
101+
public void OnInitializeFailed(InitializationFailureReason error)
102+
{
103+
Logger.Log(LogLevel.Error, $"OnInitialize IAP Failed - {error}");
104+
}
105+
106+
public void OnInitializeFailed(InitializationFailureReason error, string message)
107+
{
108+
Logger.Log(LogLevel.Error, $"OnInitialize IAP Failed - {error} : {message}");
109+
}
110+
111+
public void GetWalletBalance()
112+
{
113+
if (!IsInitialized())
114+
{
115+
Debug.LogWarning("IAPManager is not initialized.");
116+
return;
117+
}
118+
119+
//We must create a wallet before a user can sync
120+
ModIOUnity.GetUserWalletBalance(balanceResult =>
121+
{
122+
if (balanceResult.result.Succeeded())
123+
{
124+
Logger.Log(LogLevel.Verbose, $"WalletBalance is {balanceResult.value.balance}");
125+
}
126+
});
127+
}
128+
129+
//attached to a button to initiate a token purchase
130+
public void Buy1000Tokens()
131+
{
132+
if (!IsInitialized())
133+
{
134+
Debug.LogWarning("IAPManager is not initialized.");
135+
return;
136+
}
137+
138+
if(catalogProducts.Count == 0)
139+
{
140+
Logger.Log(LogLevel.Verbose, "No products found in catalog!");
141+
return;
142+
}
143+
144+
string id = catalogProducts[0].id;
145+
Product product = storeController.products.WithID(id);
146+
if (product != null && product.availableToPurchase)
147+
storeController.InitiatePurchase(product);
148+
}
149+
150+
// Method called when InitiatePurchase is completed without an error
151+
public PurchaseProcessingResult ProcessPurchase(PurchaseEventArgs args)
152+
{
153+
Logger.Log(LogLevel.Verbose, "ProcessPurchase Called");
154+
if(catalogProducts.Count == 0)
155+
{
156+
Logger.Log(LogLevel.Verbose, "No products found in catalog!");
157+
return PurchaseProcessingResult.Pending;
158+
}
159+
160+
if (String.Equals(args.purchasedProduct.definition.id, catalogProducts[0].id, StringComparison.Ordinal))
161+
{
162+
//Save the receipt for processing. This also calls sync entitlements.
163+
MobilePurchaseHelper.QueuePurchase(args.purchasedProduct, ()=>storeController.ConfirmPendingPurchase(args.purchasedProduct));
164+
Logger.Log(LogLevel.Verbose, string.Format("ProcessPurchase: PASS. Product: '{0}'", args.purchasedProduct.definition.id));
165+
}
166+
else
167+
{
168+
Logger.Log(LogLevel.Verbose, string.Format("ProcessPurchase: FAIL. Unrecognized product: '{0}'", args.purchasedProduct.definition.id));
169+
}
170+
171+
return PurchaseProcessingResult.Pending;
172+
}
173+
174+
// Method called when purchase fails
175+
public void OnPurchaseFailed(Product product, PurchaseFailureReason failureReason)
176+
{
177+
Logger.Log(LogLevel.Error, $"Purchase Failed - {product.definition.id} : {failureReason}");
178+
}
179+
180+
// Overloaded method called when purchase fails with description
181+
public void OnPurchaseFailed(Product product, PurchaseFailureDescription failureDescription)
182+
{
183+
Logger.Log(LogLevel.Error, $"Purchase Failed - {product.definition.id} : {failureDescription.message}");
184+
}
185+
}
186+
}
187+
#endif
188+
#endif

Platform/Mobile/MobilePurchasingExample.cs.meta

+11
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Platform/Mobile/PurchaseData.cs

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
#if UNITY_IOS || UNITY_ANDROID
2+
#if MODIO_IN_APP_PURCHASING
3+
4+
using System;
5+
using UnityEngine.Purchasing;
6+
7+
namespace Plugins.mod.io.Platform.Mobile
8+
{
9+
[Serializable]
10+
public class PurchaseData
11+
{
12+
public string Payload { get; set; }
13+
public string PayloadJson { get; set; } = null;
14+
public Product Product { get; set; }
15+
public Action CompleteValidation { get; set; }
16+
}
17+
}
18+
#endif
19+
#endif

Platform/Mobile/PurchaseData.cs.meta

+11
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Runtime/ModIO.Implementation/Classes/ModCollectionManager.cs

-4
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,6 @@
66
using ModIO.Implementation.API.Requests;
77
using Runtime.Enums;
88

9-
#if UNITY_IOS || UNITY_ANDROID
10-
using Plugins.mod.io.Platform.Mobile;
11-
#endif
12-
139
#if UNITY_GAMECORE
1410
using Unity.GameCore;
1511
#endif

0 commit comments

Comments
 (0)