Skip to content

Commit ce35b78

Browse files
authored
Merge pull request #11 from modio/fix-mobile-platform
Adding Mobile Platform
2 parents 1464563 + 2b0c851 commit ce35b78

7 files changed

+354
-0
lines changed

Platform/Mobile.meta

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

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

0 commit comments

Comments
 (0)