Parcourir la source

Merge pull request #498 from ricardoalcantara/mono_android_iap

Mono Android IAP Demo Project
Aaron Franke il y a 5 ans
Parent
commit
ddffbd7599

+ 70 - 0
mono/android_iap/Android IAP with C#.csproj

@@ -0,0 +1,70 @@
+<?xml version="1.0" encoding="utf-8"?>
+<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+  <PropertyGroup>
+    <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
+    <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
+    <ProjectGuid>{FA89D4C3-B45B-49F1-A975-BF059CA82D86}</ProjectGuid>
+    <OutputType>Library</OutputType>
+    <OutputPath>.mono\temp\bin\$(Configuration)</OutputPath>
+    <RootNamespace>AndroidIAPwithC</RootNamespace>
+    <AssemblyName>Android IAP with C#</AssemblyName>
+    <TargetFrameworkVersion>v4.7</TargetFrameworkVersion>
+    <GodotProjectGeneratorVersion>1.0.0.0</GodotProjectGeneratorVersion>
+    <BaseIntermediateOutputPath>.mono\temp\obj</BaseIntermediateOutputPath>
+    <IntermediateOutputPath>$(BaseIntermediateOutputPath)\$(Configuration)</IntermediateOutputPath>
+    <ApiConfiguration Condition=" '$(Configuration)' != 'ExportRelease' ">Debug</ApiConfiguration>
+    <ApiConfiguration Condition=" '$(Configuration)' == 'ExportRelease' ">Release</ApiConfiguration>
+  </PropertyGroup>
+  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'ExportDebug|AnyCPU' ">
+    <DebugSymbols>true</DebugSymbols>
+    <DebugType>portable</DebugType>
+    <Optimize>false</Optimize>
+    <DefineConstants>$(GodotDefineConstants);GODOT;DEBUG;</DefineConstants>
+    <ErrorReport>prompt</ErrorReport>
+    <WarningLevel>4</WarningLevel>
+    <ConsolePause>false</ConsolePause>
+  </PropertyGroup>
+  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'ExportRelease|AnyCPU' ">
+    <DebugType>portable</DebugType>
+    <Optimize>true</Optimize>
+    <DefineConstants>$(GodotDefineConstants);GODOT;</DefineConstants>
+    <ErrorReport>prompt</ErrorReport>
+    <WarningLevel>4</WarningLevel>
+    <ConsolePause>false</ConsolePause>
+  </PropertyGroup>
+  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
+    <DebugSymbols>true</DebugSymbols>
+    <DebugType>portable</DebugType>
+    <Optimize>false</Optimize>
+    <DefineConstants>$(GodotDefineConstants);GODOT;DEBUG;TOOLS;</DefineConstants>
+    <ErrorReport>prompt</ErrorReport>
+    <WarningLevel>4</WarningLevel>
+    <ConsolePause>false</ConsolePause>
+  </PropertyGroup>
+  <ItemGroup>
+    <PackageReference Include="Microsoft.NETFramework.ReferenceAssemblies">
+      <Version>1.0.0</Version>
+      <PrivateAssets>All</PrivateAssets>
+    </PackageReference>
+    <Reference Include="GodotSharp">
+      <HintPath>$(ProjectDir)\.mono\assemblies\$(ApiConfiguration)\GodotSharp.dll</HintPath>
+      <Private>False</Private>
+    </Reference>
+    <Reference Include="GodotSharpEditor" Condition=" '$(Configuration)' == 'Debug' ">
+      <HintPath>$(ProjectDir)\.mono\assemblies\$(ApiConfiguration)\GodotSharpEditor.dll</HintPath>
+      <Private>False</Private>
+    </Reference>
+    <Reference Include="System" />
+  </ItemGroup>
+  <ItemGroup>
+    <Compile Include="GodotGooglePlayBilling\BillingResult.cs" />
+    <Compile Include="GodotGooglePlayBilling\GooglePlayBilling.cs" />
+    <Compile Include="GodotGooglePlayBilling\GooglePlayBillingUtils.cs" />
+    <Compile Include="GodotGooglePlayBilling\Purchase.cs" />
+    <Compile Include="GodotGooglePlayBilling\PurchasesResult.cs" />
+    <Compile Include="GodotGooglePlayBilling\SkuDetails.cs" />
+    <Compile Include="Main.cs" />
+    <Compile Include="Properties\AssemblyInfo.cs" />
+  </ItemGroup>
+  <Import Project="$(MSBuildBinPath)\Microsoft.CSharp.targets" />
+</Project>

+ 19 - 0
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

+ 67 - 0
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; }
+    }
+}

+ 165 - 0
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
+    }
+}

+ 32 - 0
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;
+        }
+    }
+}

+ 74 - 0
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; }
+    }
+}

+ 24 - 0
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; }
+    }
+}

+ 101 - 0
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; }
+    }
+}

+ 232 - 0
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<string, string> _purchases = new CoreGeneric.Dictionary<string, string>();
+
+        public override void _Ready()
+        {
+            _googlePlayBilling = GetNode<GooglePlayBilling>("GooglePlayBilling");
+
+            _buyPotionButton = GetNode<Button>("VBoxContainer2/BuyPotionButton");
+            _totalPotionsLabel = GetNode<Label>("VBoxContainer/Label");
+
+            _panel = GetNode<Panel>("Panel");
+            _processLabel = GetNode<Label>("Panel/ProcessLabel");
+            _thanksLabel = GetNode<Label>("Panel/ThanksLabel");
+
+            _playerLife = GetNode<ProgressBar>("Sprite/ProgressBar");
+
+            _playerLifeStyleBoxFlat = _playerLife.Get("custom_styles/fg") as StyleBoxFlat;
+            _playerLifeStyleBoxFlat.BgColor = Colors.Red.LinearInterpolate(Colors.Green, 1);
+
+            _playerLife.Value = 1;
+
+            _panel.Hide();
+            _processLabel.Hide();
+            _thanksLabel.Hide();
+            _buyPotionButton.Hide();
+            _totalPotionsLabel.Text = $"{_totalPotion} Potions";
+        }
+
+        public override void _Process(float delta)
+        {
+            if (_playerLife.Value > 0.5)
+            {
+                _playerLife.Value -= delta;
+            }
+            else if (_playerLife.Value > 0.2)
+            {
+                _playerLife.Value -= delta / 2;
+            }
+            else if (_playerLife.Value > 0.1)
+            {
+                _playerLife.Value -= delta / 4;
+            }
+
+            _playerLifeStyleBoxFlat.BgColor = Colors.Red.LinearInterpolate(Colors.Green, Convert.ToSingle(_playerLife.Value));
+        }
+
+        private void OnUsePotionButton_pressed()
+        {
+            if (_totalPotion > 0)
+            {
+                _totalPotion -= 1;
+                _totalPotionsLabel.Text = $"{_totalPotion} Potions";
+                _playerLifeStyleBoxFlat.BgColor = Colors.Red.LinearInterpolate(Colors.Green, Convert.ToSingle(_playerLife.Value));
+
+                _playerLife.Value += 20;
+            }
+        }
+
+        private void OnBuyPotionButton_pressed()
+        {
+            var result = _googlePlayBilling.Purchase("android.test.purchased");
+            if (result != null && result.Status == (int)Error.Ok)
+            {
+                GD.Print("Bought");
+            }
+            else
+            {
+                GD.Print("Failed");
+            }
+        }
+
+        private void OnButton1_pressed()
+        {
+            var result = _googlePlayBilling.Purchase("android.test.canceled");
+            if (result != null && result.Status == (int)Error.Ok)
+            {
+                GD.Print("Bought");
+            }
+            else
+            {
+                GD.Print("Failed");
+            }
+        }
+        private void OnButton2_pressed()
+        {
+            var result = _googlePlayBilling.Purchase("android.test.refunded");
+            if (result != null && result.Status == (int)Error.Ok)
+            {
+                GD.Print("Bought");
+            }
+            else
+            {
+                GD.Print("Failed");
+            }
+        }
+        private void OnButton3_pressed()
+        {
+            var result = _googlePlayBilling.Purchase("android.test.item_unavailable");
+            if (result != null && result.Status == (int)Error.Ok)
+            {
+                GD.Print("Bought");
+            }
+            else
+            {
+                GD.Print("Failed");
+            }
+        }
+
+        private void OnOkButton_pressed()
+        {
+            _panel.Hide();
+            _processLabel.Hide();
+            _thanksLabel.Hide();
+        }
+
+        private void OnGooglePlayBilling_Connected()
+        {
+            _googlePlayBilling.QuerySkuDetails(ArrInAppProductsSKUs, PurchaseType.InApp);
+
+            var purchasesResult = _googlePlayBilling.QueryPurchases(PurchaseType.InApp);
+            if (purchasesResult.Status == (int)Error.Ok)
+            {
+                foreach (var purchase in purchasesResult.Purchases)
+                {
+                    _purchases.Add(purchase.PurchaseToken, purchase.Sku);
+                    // We only expect this SKU
+                    if (purchase.Sku == "android.test.purchased")
+                    {
+                        _googlePlayBilling.AcknowledgePurchase(purchase.PurchaseToken);
+                    }
+                }
+            }
+            else
+            {
+                GD.Print($"Purchase query failed: {purchasesResult.ResponseCode} - {purchasesResult.DebugMessage}");
+            }
+        }
+
+        private void OnGooglePlayBilling_SkuDetailsQueryCompleted(Godot.Collections.Array arrSkuDetails)
+        {
+            var skuDetails = GooglePlayBillingUtils.ConvertSkuDetailsDictionaryArray(arrSkuDetails);
+            foreach (var sku in skuDetails)
+            {
+                switch (sku.Sku)
+                {
+                    // our fake potion
+                    case "android.test.purchased":
+                        _buyPotionButton.Text = $"Buy {sku.Price}";
+                        _buyPotionButton.Show();
+                        break;
+                }
+            }
+        }
+
+        private void OnGooglePlayBilling_PurchasesUpdated(Godot.Collections.Array arrPurchases)
+        {
+            _panel.Show();
+            _processLabel.Show();
+            _thanksLabel.Hide();
+
+            var purchases = GooglePlayBillingUtils.ConvertPurchaseDictionaryArray(arrPurchases);
+
+            foreach (var purchase in purchases)
+            {
+                _purchases.Add(purchase.PurchaseToken, purchase.Sku);
+                // We only expect this SKU
+                if (purchase.Sku == "android.test.purchased")
+                {
+                    _googlePlayBilling.AcknowledgePurchase(purchase.PurchaseToken);
+                }
+            }
+        }
+
+        private void OnGooglePlayBilling_PurchaseAcknowledged(string purchaseToken)
+        {
+            _googlePlayBilling.ConsumePurchase(purchaseToken);
+        }
+
+        private void OnGooglePlayBilling_PurchaseConsumed(string purchaseToken)
+        {
+            if (_purchases[purchaseToken] == "android.test.purchased")
+            {
+                _totalPotion += 5;
+                _totalPotionsLabel.Text = $"{_totalPotion} Potions";
+                _purchases.Remove(purchaseToken);
+
+                _processLabel.Hide();
+                _thanksLabel.Show();
+
+            }
+            GD.Print("OnGooglePlayBilling_PurchaseConsumed ", purchaseToken);
+        }
+    }
+}

+ 215 - 0
mono/android_iap/Main.tscn

@@ -0,0 +1,215 @@
+[gd_scene load_steps=6 format=2]
+
+[ext_resource path="res://icon.png" type="Texture" id=1]
+[ext_resource path="res://Main.cs" type="Script" id=2]
+[ext_resource path="res://GodotGooglePlayBilling/GooglePlayBilling.cs" type="Script" id=3]
+
+[sub_resource type="StyleBoxFlat" id=1]
+bg_color = Color( 1, 0, 0, 1 )
+
+[sub_resource type="StyleBoxFlat" id=2]
+bg_color = Color( 0, 0, 0, 1 )
+
+[node name="Main" type="Node2D"]
+script = ExtResource( 2 )
+
+[node name="GooglePlayBilling" type="Node" parent="."]
+script = ExtResource( 3 )
+AutoConnect = true
+
+[node name="Sprite" type="Sprite" parent="."]
+position = Vector2( 239.092, 225.037 )
+scale = Vector2( 2, 2 )
+texture = ExtResource( 1 )
+
+[node name="ProgressBar" type="ProgressBar" parent="Sprite"]
+margin_left = -57.9025
+margin_top = -47.7944
+margin_right = 57.0975
+margin_bottom = -33.7944
+custom_styles/fg = SubResource( 1 )
+custom_styles/bg = SubResource( 2 )
+max_value = 1.0
+value = 0.5
+percent_visible = false
+__meta__ = {
+"_edit_use_anchors_": false
+}
+
+[node name="VBoxContainer" type="VBoxContainer" parent="."]
+margin_left = 65.0538
+margin_top = 30.4056
+margin_right = 185.054
+margin_bottom = 88.4056
+__meta__ = {
+"_edit_use_anchors_": false
+}
+
+[node name="Label" type="Label" parent="VBoxContainer"]
+margin_right = 120.0
+margin_bottom = 14.0
+text = "1 Potion"
+align = 1
+__meta__ = {
+"_edit_lock_": true,
+"_edit_use_anchors_": false
+}
+
+[node name="UsePotionButton" type="Button" parent="VBoxContainer"]
+margin_top = 18.0
+margin_right = 120.0
+margin_bottom = 58.0
+rect_min_size = Vector2( 120, 40 )
+text = "Use Potion"
+__meta__ = {
+"_edit_lock_": true,
+"_edit_use_anchors_": false
+}
+
+[node name="VBoxContainer2" type="VBoxContainer" parent="."]
+margin_left = 340.826
+margin_top = 29.6986
+margin_right = 460.826
+margin_bottom = 87.6986
+__meta__ = {
+"_edit_use_anchors_": false
+}
+
+[node name="Label" type="Label" parent="VBoxContainer2"]
+margin_right = 121.0
+margin_bottom = 14.0
+text = "Get 5 Potions now!"
+align = 1
+__meta__ = {
+"_edit_lock_": true,
+"_edit_use_anchors_": false
+}
+
+[node name="BuyPotionButton" type="Button" parent="VBoxContainer2"]
+margin_top = 18.0
+margin_right = 121.0
+margin_bottom = 58.0
+rect_min_size = Vector2( 120, 40 )
+text = "Buy"
+__meta__ = {
+"_edit_lock_": true,
+"_edit_use_anchors_": false
+}
+
+[node name="VBoxContainer3" type="VBoxContainer" parent="."]
+margin_left = 373.353
+margin_top = 143.543
+margin_right = 494.353
+margin_bottom = 289.543
+__meta__ = {
+"_edit_use_anchors_": false
+}
+
+[node name="Label" type="Label" parent="VBoxContainer3"]
+margin_right = 121.0
+margin_bottom = 14.0
+text = "Other Items"
+align = 1
+__meta__ = {
+"_edit_lock_": true,
+"_edit_use_anchors_": false
+}
+
+[node name="Button1" type="Button" parent="VBoxContainer3"]
+margin_top = 18.0
+margin_right = 121.0
+margin_bottom = 58.0
+rect_min_size = Vector2( 120, 40 )
+text = "Canceled Item"
+__meta__ = {
+"_edit_lock_": true,
+"_edit_use_anchors_": false
+}
+
+[node name="Button2" type="Button" parent="VBoxContainer3"]
+margin_top = 62.0
+margin_right = 121.0
+margin_bottom = 102.0
+rect_min_size = Vector2( 120, 40 )
+text = "Refunded Item"
+__meta__ = {
+"_edit_lock_": true,
+"_edit_use_anchors_": false
+}
+
+[node name="Button3" type="Button" parent="VBoxContainer3"]
+margin_top = 106.0
+margin_right = 121.0
+margin_bottom = 146.0
+rect_min_size = Vector2( 120, 40 )
+text = "Unavailable Item"
+__meta__ = {
+"_edit_lock_": true,
+"_edit_use_anchors_": false
+}
+
+[node name="Panel" type="Panel" parent="."]
+visible = false
+self_modulate = Color( 1, 1, 1, 0.666667 )
+margin_left = 114.414
+margin_top = 38.6777
+margin_right = 379.414
+margin_bottom = 235.678
+__meta__ = {
+"_edit_use_anchors_": false
+}
+
+[node name="ProcessLabel" type="Label" parent="Panel"]
+anchor_left = 0.5
+anchor_top = 0.5
+anchor_right = 0.5
+anchor_bottom = 0.5
+margin_left = -88.0
+margin_top = -8.5
+margin_right = 88.0
+margin_bottom = 8.5
+text = "Processing..."
+align = 1
+__meta__ = {
+"_edit_use_anchors_": false
+}
+
+[node name="ThanksLabel" type="Label" parent="Panel"]
+anchor_left = 0.5
+anchor_top = 0.5
+anchor_right = 0.5
+anchor_bottom = 0.5
+margin_left = -88.0
+margin_top = -8.5
+margin_right = 88.0
+margin_bottom = 8.5
+text = "Thanks for your purchase"
+align = 1
+__meta__ = {
+"_edit_use_anchors_": false
+}
+
+[node name="OkButton" type="Button" parent="Panel"]
+anchor_left = 0.5
+anchor_top = 1.0
+anchor_right = 0.5
+anchor_bottom = 1.0
+margin_left = -48.0
+margin_top = -52.0919
+margin_right = 48.0
+margin_bottom = -19.0919
+text = "Ok"
+__meta__ = {
+"_edit_use_anchors_": false
+}
+[connection signal="Connected" from="GooglePlayBilling" to="." method="OnGooglePlayBilling_Connected"]
+[connection signal="PurchaseAcknowledged" from="GooglePlayBilling" to="." method="OnGooglePlayBilling_PurchaseAcknowledged"]
+[connection signal="PurchaseConsumed" from="GooglePlayBilling" to="." method="OnGooglePlayBilling_PurchaseConsumed"]
+[connection signal="PurchasesUpdated" from="GooglePlayBilling" to="." method="OnGooglePlayBilling_PurchasesUpdated"]
+[connection signal="SkuDetailsQueryCompleted" from="GooglePlayBilling" to="." method="OnGooglePlayBilling_SkuDetailsQueryCompleted"]
+[connection signal="pressed" from="VBoxContainer/UsePotionButton" to="." method="OnUsePotionButton_pressed"]
+[connection signal="pressed" from="VBoxContainer2/BuyPotionButton" to="." method="OnBuyPotionButton_pressed"]
+[connection signal="pressed" from="VBoxContainer3/Button1" to="." method="OnButton1_pressed"]
+[connection signal="pressed" from="VBoxContainer3/Button2" to="." method="OnButton2_pressed"]
+[connection signal="pressed" from="VBoxContainer3/Button3" to="." method="OnButton3_pressed"]
+[connection signal="pressed" from="Panel/OkButton" to="." method="OnOkButton_pressed"]

+ 25 - 0
mono/android_iap/Properties/AssemblyInfo.cs

@@ -0,0 +1,25 @@
+using System.Reflection;
+
+// Information about this assembly is defined by the following attributes.
+// Change them to the values specific to your project.
+
+[assembly: AssemblyTitle("Android IAP with C#")]
+[assembly: AssemblyDescription("")]
+[assembly: AssemblyConfiguration("")]
+[assembly: AssemblyCompany("")]
+[assembly: AssemblyProduct("")]
+[assembly: AssemblyCopyright("")]
+[assembly: AssemblyTrademark("")]
+[assembly: AssemblyCulture("")]
+
+// The assembly version has the format "{Major}.{Minor}.{Build}.{Revision}".
+// The form "{Major}.{Minor}.*" will automatically update the build and revision,
+// and "{Major}.{Minor}.{Build}.*" will update just the revision.
+
+[assembly: AssemblyVersion("1.0.*")]
+
+// The following attributes are used to specify the signing key for the assembly,
+// if desired. See the Mono documentation for more information about signing.
+
+//[assembly: AssemblyDelaySign(false)]
+//[assembly: AssemblyKeyFile("")]

+ 21 - 0
mono/android_iap/README.md

@@ -0,0 +1,21 @@
+# Android IAP with C#
+
+A simple Android IAP game. This demo shows how to buy a product and consume it for game development in Godot, including
+[Android in-app purchases](https://docs.godotengine.org/en/latest/tutorials/platform/android_in_app_purchases.html).
+
+Language: [C#](https://docs.godotengine.org/en/latest/getting_started/scripting/c_sharp/index.html)
+
+Renderer: GLES 2
+
+Note: There is a GDScript version available, but it's a bit simpler [here](https://github.com/godotengine/godot-demo-projects/tree/master/mobile/android_iap).
+
+## How does it work?
+
+You have to use potions to heal the fake Player and you have only 5 potions left, you can just buy more to keep healing the Player. The purchase is fake and won't charge anything, it uses the fake ids provided by Google.
+
+## Screenshots
+
+![Screenshot](./screenshots/1.jpg)
+![Screenshot](./screenshots/2.jpg)
+![Screenshot](./screenshots/3.jpg)
+![Screenshot](./screenshots/4.jpg)

+ 7 - 0
mono/android_iap/default_env.tres

@@ -0,0 +1,7 @@
+[gd_resource type="Environment" load_steps=2 format=2]
+
+[sub_resource type="ProceduralSky" id=1]
+
+[resource]
+background_mode = 2
+background_sky = SubResource( 1 )

BIN
mono/android_iap/icon.png


+ 34 - 0
mono/android_iap/icon.png.import

@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="StreamTexture"
+path="res://.import/icon.png-487276ed1e3a0c39cad0279d744ee560.stex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://icon.png"
+dest_files=[ "res://.import/icon.png-487276ed1e3a0c39cad0279d744ee560.stex" ]
+
+[params]
+
+compress/mode=0
+compress/lossy_quality=0.7
+compress/hdr_mode=0
+compress/bptc_ldr=0
+compress/normal_map=0
+flags/repeat=0
+flags/filter=true
+flags/mipmaps=false
+flags/anisotropic=false
+flags/srgb=2
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/HDR_as_SRGB=false
+process/invert_color=false
+stream=false
+size_limit=0
+detect_3d=true
+svg/scale=1.0

+ 35 - 0
mono/android_iap/project.godot

@@ -0,0 +1,35 @@
+; Engine configuration file.
+; It's best edited using the editor UI and not directly,
+; since the parameters that go here are not all obvious.
+;
+; Format:
+;   [section] ; section goes between []
+;   param=value ; assign values to parameters
+
+config_version=4
+
+_global_script_classes=[  ]
+_global_script_class_icons={
+
+}
+
+[application]
+
+config/name="Android IAP with C#"
+config/description="A simple Android IAP game. This demo shows how to buy a product and consume it."
+run/main_scene="res://Main.tscn"
+config/icon="res://icon.png"
+
+[display]
+
+window/size/width=512
+window/size/height=300
+window/stretch/mode="2d"
+window/stretch/aspect="keep"
+
+[rendering]
+
+quality/driver/driver_name="GLES2"
+vram_compression/import_etc=true
+vram_compression/import_etc2=false
+environment/default_environment="res://default_env.tres"

+ 1 - 0
mono/android_iap/screenshots/.gdignore

@@ -0,0 +1 @@
+

BIN
mono/android_iap/screenshots/1.jpg


BIN
mono/android_iap/screenshots/2.jpg


BIN
mono/android_iap/screenshots/3.jpg


BIN
mono/android_iap/screenshots/4.jpg