diff --git a/mono/android_iap/Android IAP with C#.csproj b/mono/android_iap/Android IAP with C#.csproj new file mode 100644 index 00000000..55d46702 --- /dev/null +++ b/mono/android_iap/Android IAP with C#.csproj @@ -0,0 +1,70 @@ + + + + Debug + AnyCPU + {FA89D4C3-B45B-49F1-A975-BF059CA82D86} + Library + .mono\temp\bin\$(Configuration) + AndroidIAPwithC + Android IAP with C# + v4.7 + 1.0.0.0 + .mono\temp\obj + $(BaseIntermediateOutputPath)\$(Configuration) + Debug + Release + + + true + portable + false + $(GodotDefineConstants);GODOT;DEBUG; + prompt + 4 + false + + + portable + true + $(GodotDefineConstants);GODOT; + prompt + 4 + false + + + true + portable + false + $(GodotDefineConstants);GODOT;DEBUG;TOOLS; + prompt + 4 + false + + + + 1.0.0 + All + + + $(ProjectDir)\.mono\assemblies\$(ApiConfiguration)\GodotSharp.dll + False + + + $(ProjectDir)\.mono\assemblies\$(ApiConfiguration)\GodotSharpEditor.dll + False + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/mono/android_iap/Android IAP with C#.sln b/mono/android_iap/Android IAP with C#.sln new file mode 100644 index 00000000..432ad2ee --- /dev/null +++ b/mono/android_iap/Android IAP with C#.sln @@ -0,0 +1,19 @@ +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 2012 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Android IAP with C#", "Android IAP with C#.csproj", "{FA89D4C3-B45B-49F1-A975-BF059CA82D86}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + ExportDebug|Any CPU = ExportDebug|Any CPU + ExportRelease|Any CPU = ExportRelease|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {FA89D4C3-B45B-49F1-A975-BF059CA82D86}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FA89D4C3-B45B-49F1-A975-BF059CA82D86}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FA89D4C3-B45B-49F1-A975-BF059CA82D86}.ExportDebug|Any CPU.ActiveCfg = ExportDebug|Any CPU + {FA89D4C3-B45B-49F1-A975-BF059CA82D86}.ExportDebug|Any CPU.Build.0 = ExportDebug|Any CPU + {FA89D4C3-B45B-49F1-A975-BF059CA82D86}.ExportRelease|Any CPU.ActiveCfg = ExportRelease|Any CPU + {FA89D4C3-B45B-49F1-A975-BF059CA82D86}.ExportRelease|Any CPU.Build.0 = ExportRelease|Any CPU + EndGlobalSection +EndGlobal diff --git a/mono/android_iap/GodotGooglePlayBilling/BillingResult.cs b/mono/android_iap/GodotGooglePlayBilling/BillingResult.cs new file mode 100644 index 00000000..0fa3d785 --- /dev/null +++ b/mono/android_iap/GodotGooglePlayBilling/BillingResult.cs @@ -0,0 +1,67 @@ +using Godot; +using Godot.Collections; + +namespace Android_Iap.GodotGooglePlayBilling +{ + // https://developer.android.com/reference/com/android/billingclient/api/BillingClient.BillingResponseCode + public enum BillingResponseCode + { + // The request has reached the maximum timeout before Google Play responds. + ServiceTimeout = -3, + + // Requested feature is not supported by Play Store on the current device. + FeatureNotSupported = -2, + + // Play Store service is not connected now - potentially transient state. + ServiceDisconnected = -1, + + // Success + Ok = 0, + + // User pressed back or canceled a dialog + UserCanceled = 1, + + // Network connection is down + ServiceUnavailable = 2, + + // Billing API version is not supported for the type requested + BillingUnavailable = 3, + + // Requested product is not available for purchase + ItemUnavailable = 4, + + // Invalid arguments provided to the API. + DeveloperError = 5, + + // Fatal error during the API action + Error = 6, + + // Failure to purchase since item is already owned + ItemAlreadyOwned = 7, + + // Failure to consume since item is not owned + ItemNotOwned = 8, + } + + public class BillingResult + { + public BillingResult() { } + public BillingResult(Dictionary billingResult) + { + try + { + Status = (int)billingResult["status"]; + ResponseCode = (billingResult.Contains("response_code") ? (BillingResponseCode)billingResult["response_code"] : BillingResponseCode.Ok); + DebugMessage = (billingResult.Contains("debug_message") ? (string)billingResult["debug_message"] : null); + } + catch (System.Exception ex) + { + GD.Print("BillingResult: ", ex.ToString()); + } + } + + public int Status { get; set; } + public BillingResponseCode ResponseCode { get; set; } + public string DebugMessage { get; set; } + } +} diff --git a/mono/android_iap/GodotGooglePlayBilling/GooglePlayBilling.cs b/mono/android_iap/GodotGooglePlayBilling/GooglePlayBilling.cs new file mode 100644 index 00000000..9fd85303 --- /dev/null +++ b/mono/android_iap/GodotGooglePlayBilling/GooglePlayBilling.cs @@ -0,0 +1,165 @@ +using Godot.Collections; +using Godot; + +namespace Android_Iap.GodotGooglePlayBilling +{ + public enum PurchaseType + { + InApp, + Subs + } + + public class GooglePlayBilling : Node + { + [Signal] public delegate void Connected(); + [Signal] public delegate void Disconnected(); + [Signal] public delegate void ConnectError(int code, string message); + [Signal] public delegate void SkuDetailsQueryCompleted(Array skuDetails); + [Signal] public delegate void SkuDetailsQueryError(int code, string message); + [Signal] public delegate void PurchasesUpdated(Array purchases); + [Signal] public delegate void PurchaseError(int code, string message); + [Signal] public delegate void PurchaseAcknowledged(string purchaseToken); + [Signal] public delegate void PurchaseAcknowledgementError(int code, string message); + [Signal] public delegate void PurchaseConsumed(string purchaseToken); + [Signal] public delegate void PurchaseConsumption_error(int code, string message, string purchaseToken); + + [Export] public bool AutoReconnect { get; set; } + [Export] public bool AutoConnect { get; set; } + + private Object _payment; + + public override void _Ready() + { + if (Engine.HasSingleton("GodotGooglePlayBilling")) + { + GD.Print("GodotGooglePlayBilling HasSingleton"); + _payment = Engine.GetSingleton("GodotGooglePlayBilling"); + // These are all signals supported by the API + // You can drop some of these based on your needs + _payment.Connect("connected", this, nameof(OnGodotGooglePlayBilling_connected)); // No params + _payment.Connect("disconnected", this, nameof(OnGodotGooglePlayBilling_disconnected)); // No params + _payment.Connect("connect_error", this, nameof(OnGodotGooglePlayBilling_connect_error)); // Response ID (int), Debug message (string) + _payment.Connect("sku_details_query_completed", this, nameof(OnGodotGooglePlayBilling_sku_details_query_completed)); // SKUs (Array of Dictionary) + _payment.Connect("sku_details_query_error", this, nameof(OnGodotGooglePlayBilling_sku_details_query_error)); // Response ID (int), Debug message (string), Queried SKUs (string[]) + _payment.Connect("purchases_updated", this, nameof(OnGodotGooglePlayBilling_purchases_updated)); // Purchases (Array of Dictionary) + _payment.Connect("purchase_error", this, nameof(OnGodotGooglePlayBilling_purchase_error)); // Response ID (int), Debug message (string) + _payment.Connect("purchase_acknowledged", this, nameof(OnGodotGooglePlayBilling_purchase_acknowledged)); // Purchase token (string) + _payment.Connect("purchase_acknowledgement_error", this, nameof(OnGodotGooglePlayBilling_purchase_acknowledgement_error)); // Response ID (int), Debug message (string), Purchase token (string) + _payment.Connect("purchase_consumed", this, nameof(OnGodotGooglePlayBilling_purchase_consumed)); // Purchase token (string) + _payment.Connect("purchase_consumption_error", this, nameof(OnGodotGooglePlayBilling_purchase_consumption_error)); // Response ID (int), Debug message (string), Purchase token (string) + + if (AutoConnect) StartConnection(); + } + else + { + GD.Print("GPB: Android IAP support is not enabled. Make sure you have enabled 'Custom Build' and the GodotGooglePlayBilling plugin in your Android export settings! IAP will not work."); + } + } + + #region GooglePlayBilling Methods + + public void StartConnection() => _payment?.Call("startConnection"); + + public void EndConnection() => _payment?.Call("endConnection"); + + public BillingResult Purchase(string sku) + { + if (_payment == null) return null; + var result = (Dictionary)_payment.Call("purchase", sku); + return new BillingResult(result); + } + + public void QuerySkuDetails(string[] querySkuDetails, PurchaseType type) => _payment?.Call("querySkuDetails", querySkuDetails, $"{type}".ToLower()); + + public bool IsReady() => (_payment?.Call("isReady") as bool?) ?? false; + + public PurchasesResult QueryPurchases(PurchaseType purchaseType) + { + if (_payment == null) return null; + var result = (Dictionary)_payment.Call("queryPurchases", $"{purchaseType}".ToLower()); + return new PurchasesResult(result); + } + + public void AcknowledgePurchase(string purchaseToken) => _payment?.Call("acknowledgePurchase", purchaseToken); + + public void ConsumePurchase(string purchaseToken) => _payment?.Call("consumePurchase", purchaseToken); + + #endregion + + #region GodotGooglePlayBilling Signals + + private void OnGodotGooglePlayBilling_connected() + { + GD.Print("GodotGooglePlayBilling Connected"); + EmitSignal(nameof(Connected)); + } + + private async void OnGodotGooglePlayBilling_disconnected() + { + GD.Print("GodotGooglePlayBilling Disconnected"); + EmitSignal(nameof(Disconnected)); + + if (AutoReconnect) + { + await ToSignal(GetTree().CreateTimer(10), "timeout"); + StartConnection(); + } + } + + private void OnGodotGooglePlayBilling_connect_error(int code, string message) + { + GD.Print($"GodotGooglePlayBilling ConnectError {code}: {message}"); + EmitSignal(nameof(ConnectError), code, message); + } + + private void OnGodotGooglePlayBilling_sku_details_query_completed(Array skuDetails) + { + GD.Print($"GodotGooglePlayBilling SkuDetailsQueryCompleted {skuDetails}"); + EmitSignal(nameof(SkuDetailsQueryCompleted), skuDetails); + } + + private void OnGodotGooglePlayBilling_sku_details_query_error(int code, string message) + { + GD.Print($"SkuDetailsQueryError error {code}: {message}"); + EmitSignal(nameof(SkuDetailsQueryError), code, message); + } + + private void OnGodotGooglePlayBilling_purchases_updated(Array purchases) + { + GD.Print($"GodotGooglePlayBilling PurchasesUpdated {purchases}"); + EmitSignal(nameof(PurchasesUpdated), purchases); + } + + private void OnGodotGooglePlayBilling_purchase_error(int code, string message) + { + GD.Print($"GodotGooglePlayBilling PurchaseError {code}: {message}"); + EmitSignal(nameof(PurchaseError), code, message); + } + + private void OnGodotGooglePlayBilling_purchase_acknowledged(string purchaseToken) + { + GD.Print($"GodotGooglePlayBilling PurchaseAcknowledged {purchaseToken}"); + EmitSignal(nameof(PurchaseAcknowledged), purchaseToken); + } + + private void OnGodotGooglePlayBilling_purchase_acknowledgement_error(int code, string message) + { + GD.Print($"GodotGooglePlayBilling PurchaseAcknowledgementError error {code}: {message}"); + EmitSignal(nameof(PurchaseAcknowledgementError), code, message); + } + + private void OnGodotGooglePlayBilling_purchase_consumed(string purchaseToken) + { + GD.Print($"GodotGooglePlayBilling PurchaseConsumed successfully: {purchaseToken}"); + EmitSignal(nameof(PurchaseConsumed), purchaseToken); + } + + private void OnGodotGooglePlayBilling_purchase_consumption_error(int code, string message, string purchaseToken) + { + GD.Print($"GodotGooglePlayBilling PurchaseConsumption_error error {code}: {message}, purchase token: {purchaseToken}"); + EmitSignal(nameof(PurchaseConsumption_error), code, message, purchaseToken); + } + + #endregion + } +} diff --git a/mono/android_iap/GodotGooglePlayBilling/GooglePlayBillingUtils.cs b/mono/android_iap/GodotGooglePlayBilling/GooglePlayBillingUtils.cs new file mode 100644 index 00000000..0c9bbca3 --- /dev/null +++ b/mono/android_iap/GodotGooglePlayBilling/GooglePlayBillingUtils.cs @@ -0,0 +1,32 @@ +using Godot; +using Godot.Collections; + +namespace Android_Iap.GodotGooglePlayBilling +{ + public static class GooglePlayBillingUtils + { + public static Purchase[] ConvertPurchaseDictionaryArray(Array arrPurchases) + { + if (arrPurchases == null) return null; + var purchases = new Purchase[arrPurchases.Count]; + for (int i = 0; i < arrPurchases.Count; i++) + { + purchases[i] = new Purchase((Dictionary)arrPurchases[i]); + } + + return purchases; + } + + public static SkuDetails[] ConvertSkuDetailsDictionaryArray(Array arrSkuDetails) + { + if (arrSkuDetails == null) return null; + var skusDetails = new SkuDetails[arrSkuDetails.Count]; + for (int i = 0; i < arrSkuDetails.Count; i++) + { + skusDetails[i] = new SkuDetails((Dictionary)arrSkuDetails[i]); + } + + return skusDetails; + } + } +} diff --git a/mono/android_iap/GodotGooglePlayBilling/Purchase.cs b/mono/android_iap/GodotGooglePlayBilling/Purchase.cs new file mode 100644 index 00000000..1ca8ac69 --- /dev/null +++ b/mono/android_iap/GodotGooglePlayBilling/Purchase.cs @@ -0,0 +1,74 @@ +using System; +using Godot; +using Godot.Collections; + +namespace Android_Iap.GodotGooglePlayBilling +{ + // https://developer.android.com/reference/com/android/billingclient/api/Purchase.PurchaseState + public enum PurchaseState + { + UnspecifiedState = 0, + Purchased = 1, + Pending = 2 + } + + public class Purchase + { + public Purchase() { } + + public Purchase(Dictionary purchase) + { + foreach (var key in purchase.Keys) + { + try + { + switch (key) + { + case "order_id": + OrderId = (string)purchase[key]; + break; + case "package_name": + PackageName = (string)purchase[key]; + break; + case "purchase_state": + PurchaseState = (PurchaseState)purchase[key]; + break; + case "purchase_time": + PurchaseTime = Convert.ToInt64(purchase[key]); + break; + case "purchase_token": + PurchaseToken = (string)purchase[key]; + break; + case "signature": + Signature = (string)purchase[key]; + break; + case "sku": + Sku = (string)purchase[key]; + break; + case "is_acknowledged": + IsAcknowledged = (bool)purchase[key]; + break; + case "is_auto_renewing": + IsAutoRenewing = (bool)purchase[key]; + break; + } + } + catch (System.Exception ex) + { + GD.Print("Error: ", purchase[key], " -> ", ex.ToString()); + } + + } + } + + public string OrderId { get; set; } + public string PackageName { get; set; } + public PurchaseState PurchaseState { get; set; } + public long PurchaseTime { get; set; } + public string PurchaseToken { get; set; } + public string Signature { get; set; } + public string Sku { get; set; } + public bool IsAcknowledged { get; set; } + public bool IsAutoRenewing { get; set; } + } +} diff --git a/mono/android_iap/GodotGooglePlayBilling/PurchasesResult.cs b/mono/android_iap/GodotGooglePlayBilling/PurchasesResult.cs new file mode 100644 index 00000000..427606b9 --- /dev/null +++ b/mono/android_iap/GodotGooglePlayBilling/PurchasesResult.cs @@ -0,0 +1,24 @@ +using Godot; +using Godot.Collections; + +namespace Android_Iap.GodotGooglePlayBilling +{ + public class PurchasesResult : BillingResult + { + public PurchasesResult() { } + public PurchasesResult(Dictionary purchasesResult) + : base(purchasesResult) + { + try + { + Purchases = (purchasesResult.Contains("purchases") ? GooglePlayBillingUtils.ConvertPurchaseDictionaryArray((Array)purchasesResult["purchases"]) : null); + } + catch (System.Exception ex) + { + GD.Print("PurchasesResult: ", ex.ToString()); + } + } + + public Purchase[] Purchases { get; set; } + } +} diff --git a/mono/android_iap/GodotGooglePlayBilling/SkuDetails.cs b/mono/android_iap/GodotGooglePlayBilling/SkuDetails.cs new file mode 100644 index 00000000..444c70bb --- /dev/null +++ b/mono/android_iap/GodotGooglePlayBilling/SkuDetails.cs @@ -0,0 +1,101 @@ +using System; +using Godot; +using Godot.Collections; + +namespace Android_Iap.GodotGooglePlayBilling +{ + public class SkuDetails + { + public SkuDetails() { } + + public SkuDetails(Dictionary skuDetails) + { + foreach (var key in skuDetails.Keys) + { + try + { + switch (key) + { + case "sku": + Sku = (string)skuDetails[key]; + break; + case "title": + Title = (string)skuDetails[key]; + break; + case "description": + Description = (string)skuDetails[key]; + break; + case "price": + Price = (string)skuDetails[key]; + break; + case "price_currency_code": + PriceCurrencyCode = (string)skuDetails[key]; + break; + case "price_amount_micros": + PriceAmountMicros = Convert.ToInt64(skuDetails[key]); + break; + case "free_trial_period": + FreeTrialPeriod = (string)skuDetails[key]; + break; + case "icon_url": + IconUrl = (string)skuDetails[key]; + break; + case "introductory_price": + IntroductoryPrice = (string)skuDetails[key]; + break; + case "introductory_price_amount_micros": + IntroductoryPriceAmountMicros = Convert.ToInt64(skuDetails[key]); + break; + case "introductory_price_cycles": + IntroductoryPriceCycles = (int)skuDetails[key]; + break; + case "introductory_price_period": + IntroductoryPricePeriod = (string)skuDetails[key]; + break; + case "original_price": + OriginalPrice = (string)skuDetails[key]; + break; + case "original_price_amount_micros": + OriginalPriceAmountMicros = Convert.ToInt64(skuDetails[key]); + break; + case "subscription_period": + SubscriptionPeriod = (string)skuDetails[key]; + break; + case "type": + switch(skuDetails[key]) + { + case "inapp": + Type = PurchaseType.InApp; + break; + case "subs": + Type = PurchaseType.Subs; + break; + } + break; + } + } + catch (System.Exception ex) + { + GD.Print("Error: ", skuDetails[key], " -> ", ex.ToString()); + } + } + } + + public string Sku { get; set; } + public string Title { get; set; } + public string Description { get; set; } + public string Price { get; set; } + public string PriceCurrencyCode { get; set; } + public long PriceAmountMicros { get; set; } + public string FreeTrialPeriod { get; set; } + public string IconUrl { get; set; } + public string IntroductoryPrice { get; set; } + public long IntroductoryPriceAmountMicros { get; set; } + public int IntroductoryPriceCycles { get; set; } + public string IntroductoryPricePeriod { get; set; } + public string OriginalPrice { get; set; } + public long OriginalPriceAmountMicros { get; set; } + public string SubscriptionPeriod { get; set; } + public PurchaseType Type { get; set; } + } +} diff --git a/mono/android_iap/Main.cs b/mono/android_iap/Main.cs new file mode 100644 index 00000000..3482dc0a --- /dev/null +++ b/mono/android_iap/Main.cs @@ -0,0 +1,232 @@ +using Android_Iap.GodotGooglePlayBilling; +using Godot; +using CoreGeneric = System.Collections.Generic; +using System.Linq; +using System; + +namespace Android_Iap +{ + /* + test skus + android.test.purchased + android.test.canceled + android.test.refunded + android.test.item_unavailable + */ + public class Main : Node2D + { + private readonly string[] ArrInAppProductsSKUs = new string[] + { + "android.test.purchased", + "android.test.canceled", + "android.test.refunded", + "android.test.item_unavailable" + }; + + + private Button _buyPotionButton; + private Label _totalPotionsLabel; + + private Panel _panel; + private Label _processLabel; + private Label _thanksLabel; + + private ProgressBar _playerLife; + private StyleBoxFlat _playerLifeStyleBoxFlat; + + private GooglePlayBilling _googlePlayBilling; + private int _totalPotion = 5; + + CoreGeneric.Dictionary _purchases = new CoreGeneric.Dictionary(); + + public override void _Ready() + { + _googlePlayBilling = GetNode("GooglePlayBilling"); + + _buyPotionButton = GetNode