Merge pull request #498 from ricardoalcantara/mono_android_iap

Mono Android IAP Demo Project
This commit is contained in:
Aaron Franke
2020-07-27 14:02:09 -04:00
committed by GitHub
21 changed files with 1122 additions and 0 deletions

View File

@@ -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>

View File

@@ -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

View File

@@ -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; }
}
}

View File

@@ -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
}
}

View File

@@ -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;
}
}
}

View File

@@ -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; }
}
}

View File

@@ -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; }
}
}

View File

@@ -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
mono/android_iap/Main.cs Normal file
View File

@@ -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
mono/android_iap/Main.tscn Normal file
View File

@@ -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"]

View File

@@ -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("")]

View File

@@ -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)

View File

@@ -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 Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

View File

@@ -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

View File

@@ -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"

View File

@@ -0,0 +1 @@

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB