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