Merge pull request #507 from ricardoalcantara/mono_android_iap

Rewriting Mono Android IAP to match with GDScript
This commit is contained in:
Aaron Franke
2020-08-12 19:25:21 -05:00
committed by GitHub
20 changed files with 289 additions and 517 deletions

View File

@@ -1,19 +0,0 @@
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

@@ -3,11 +3,11 @@
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProjectGuid>{FA89D4C3-B45B-49F1-A975-BF059CA82D86}</ProjectGuid>
<ProjectGuid>{480953C3-B1FD-42F6-8A07-51A3F69024D7}</ProjectGuid>
<OutputType>Library</OutputType>
<OutputPath>.mono\temp\bin\$(Configuration)</OutputPath>
<RootNamespace>AndroidIAPwithC</RootNamespace>
<AssemblyName>Android IAP with C#</AssemblyName>
<RootNamespace>AndroidInAppPurchasesWithCSharp</RootNamespace>
<AssemblyName>Android in-app purchases with C#</AssemblyName>
<TargetFrameworkVersion>v4.7</TargetFrameworkVersion>
<GodotProjectGeneratorVersion>1.0.0.0</GodotProjectGeneratorVersion>
<BaseIntermediateOutputPath>.mono\temp\obj</BaseIntermediateOutputPath>
@@ -47,12 +47,12 @@
<PrivateAssets>All</PrivateAssets>
</PackageReference>
<Reference Include="GodotSharp">
<HintPath>$(ProjectDir)\.mono\assemblies\$(ApiConfiguration)\GodotSharp.dll</HintPath>
<Private>False</Private>
<HintPath>$(ProjectDir)/.mono/assemblies/$(ApiConfiguration)/GodotSharp.dll</HintPath>
</Reference>
<Reference Include="GodotSharpEditor" Condition=" '$(Configuration)' == 'Debug' ">
<HintPath>$(ProjectDir)\.mono\assemblies\$(ApiConfiguration)\GodotSharpEditor.dll</HintPath>
<Private>False</Private>
<HintPath>$(ProjectDir)/.mono/assemblies/$(ApiConfiguration)/GodotSharpEditor.dll</HintPath>
</Reference>
<Reference Include="System" />
</ItemGroup>

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 in-app purchases with C#", "Android in-app purchases with C#.csproj", "{480953C3-B1FD-42F6-8A07-51A3F69024D7}"
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
{480953C3-B1FD-42F6-8A07-51A3F69024D7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{480953C3-B1FD-42F6-8A07-51A3F69024D7}.Debug|Any CPU.Build.0 = Debug|Any CPU
{480953C3-B1FD-42F6-8A07-51A3F69024D7}.ExportDebug|Any CPU.ActiveCfg = ExportDebug|Any CPU
{480953C3-B1FD-42F6-8A07-51A3F69024D7}.ExportDebug|Any CPU.Build.0 = ExportDebug|Any CPU
{480953C3-B1FD-42F6-8A07-51A3F69024D7}.ExportRelease|Any CPU.ActiveCfg = ExportRelease|Any CPU
{480953C3-B1FD-42F6-8A07-51A3F69024D7}.ExportRelease|Any CPU.Build.0 = ExportRelease|Any CPU
EndGlobalSection
EndGlobal

View File

@@ -1,7 +1,7 @@
using Godot;
using Godot.Collections;
namespace Android_Iap.GodotGooglePlayBilling
namespace AndroidInAppPurchasesWithCSharp.GodotGooglePlayBilling
{
// https://developer.android.com/reference/com/android/billingclient/api/BillingClient.BillingResponseCode
public enum BillingResponseCode

View File

@@ -1,7 +1,7 @@
using Godot.Collections;
using Godot;
namespace Android_Iap.GodotGooglePlayBilling
namespace AndroidInAppPurchasesWithCSharp.GodotGooglePlayBilling
{
public enum PurchaseType
{
@@ -21,18 +21,20 @@ namespace Android_Iap.GodotGooglePlayBilling
[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);
[Signal] public delegate void PurchaseConsumptionError(int code, string message, string purchaseToken);
[Export] public bool AutoReconnect { get; set; }
[Export] public bool AutoConnect { get; set; }
public bool IsAvailable { get; private set; }
private Object _payment;
public override void _Ready()
{
if (Engine.HasSingleton("GodotGooglePlayBilling"))
{
GD.Print("GodotGooglePlayBilling HasSingleton");
IsAvailable = true;
_payment = Engine.GetSingleton("GodotGooglePlayBilling");
// These are all signals supported by the API
// You can drop some of these based on your needs
@@ -47,12 +49,10 @@ namespace Android_Iap.GodotGooglePlayBilling
_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.");
IsAvailable = false;
}
}
@@ -62,6 +62,14 @@ namespace Android_Iap.GodotGooglePlayBilling
public void EndConnection() => _payment?.Call("endConnection");
public void QuerySkuDetails(string[] querySkuDetails, PurchaseType type) => _payment?.Call("querySkuDetails", querySkuDetails, $"{type}".ToLower());
public bool IsReady() => (_payment?.Call("isReady") as bool?) ?? false;
public void AcknowledgePurchase(string purchaseToken) => _payment?.Call("acknowledgePurchase", purchaseToken);
public void ConsumePurchase(string purchaseToken) => _payment?.Call("consumePurchase", purchaseToken);
public BillingResult Purchase(string sku)
{
if (_payment == null) return null;
@@ -69,10 +77,6 @@ namespace Android_Iap.GodotGooglePlayBilling
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;
@@ -80,85 +84,31 @@ namespace Android_Iap.GodotGooglePlayBilling
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 void OnGodotGooglePlayBilling_connected() => EmitSignal(nameof(Connected));
private async void OnGodotGooglePlayBilling_disconnected()
{
GD.Print("GodotGooglePlayBilling Disconnected");
EmitSignal(nameof(Disconnected));
private void OnGodotGooglePlayBilling_disconnected() => EmitSignal(nameof(Disconnected));
if (AutoReconnect)
{
await ToSignal(GetTree().CreateTimer(10), "timeout");
StartConnection();
}
}
private void OnGodotGooglePlayBilling_connect_error(int code, string message) => EmitSignal(nameof(ConnectError), code, message);
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) => EmitSignal(nameof(SkuDetailsQueryCompleted), skuDetails);
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) => EmitSignal(nameof(SkuDetailsQueryError), code, message);
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) => EmitSignal(nameof(PurchasesUpdated), purchases);
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) => EmitSignal(nameof(PurchaseError), code, message);
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) => EmitSignal(nameof(PurchaseAcknowledged), purchaseToken);
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) => EmitSignal(nameof(PurchaseAcknowledgementError), code, message);
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) => EmitSignal(nameof(PurchaseConsumed), purchaseToken);
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);
}
private void OnGodotGooglePlayBilling_purchase_consumption_error(int code, string message, string purchaseToken) => EmitSignal(nameof(PurchaseConsumptionError), code, message, purchaseToken);
#endregion
}

View File

@@ -1,7 +1,7 @@
using Godot;
using Godot.Collections;
namespace Android_Iap.GodotGooglePlayBilling
namespace AndroidInAppPurchasesWithCSharp.GodotGooglePlayBilling
{
public static class GooglePlayBillingUtils
{

View File

@@ -2,7 +2,7 @@ using System;
using Godot;
using Godot.Collections;
namespace Android_Iap.GodotGooglePlayBilling
namespace AndroidInAppPurchasesWithCSharp.GodotGooglePlayBilling
{
// https://developer.android.com/reference/com/android/billingclient/api/Purchase.PurchaseState
public enum PurchaseState

View File

@@ -1,7 +1,7 @@
using Godot;
using Godot.Collections;
namespace Android_Iap.GodotGooglePlayBilling
namespace AndroidInAppPurchasesWithCSharp.GodotGooglePlayBilling
{
public class PurchasesResult : BillingResult
{

View File

@@ -2,7 +2,7 @@ using System;
using Godot;
using Godot.Collections;
namespace Android_Iap.GodotGooglePlayBilling
namespace AndroidInAppPurchasesWithCSharp.GodotGooglePlayBilling
{
public class SkuDetails
{

View File

@@ -1,170 +1,82 @@
using Android_Iap.GodotGooglePlayBilling;
using AndroidInAppPurchasesWithCSharp.GodotGooglePlayBilling;
using Godot;
using CoreGeneric = System.Collections.Generic;
using System.Linq;
using System;
namespace Android_Iap
namespace AndroidInAppPurchasesWithCSharp
{
/*
test skus
android.test.purchased
android.test.canceled
android.test.refunded
android.test.item_unavailable
*/
public class Main : Node2D
public class Main : Control
{
private readonly string[] ArrInAppProductsSKUs = new string[]
{
"android.test.purchased",
"android.test.canceled",
"android.test.refunded",
"android.test.item_unavailable"
};
const string TestItemSku = "my_in_app_purchase_sku";
private AcceptDialog _alertDialog;
private Label _label;
private Button _buyPotionButton;
private Label _totalPotionsLabel;
private GooglePlayBilling _payment;
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>();
private string _testItemPurchaseToken;
public override void _Ready()
{
_googlePlayBilling = GetNode<GooglePlayBilling>("GooglePlayBilling");
_payment = GetNode<GooglePlayBilling>("GooglePlayBilling");
_alertDialog = GetNode<AcceptDialog>("AlertDialog");
_label = GetNode<Label>("Label");
_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)
if (_payment.IsAvailable)
{
_playerLife.Value -= delta;
}
else if (_playerLife.Value > 0.2)
{
_playerLife.Value -= delta / 2;
}
else if (_playerLife.Value > 0.1)
{
_playerLife.Value -= delta / 4;
}
_label.Text += $"\n\n\nTest item SKU: {TestItemSku}";
_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("Purchase Requested");
// No params.
_payment.Connect(nameof(GooglePlayBilling.Connected), this, nameof(OnConnected));
// No params.
_payment.Connect(nameof(GooglePlayBilling.Disconnected), this, nameof(OnDisconnected));
// Response ID (int), Debug message (string).
_payment.Connect(nameof(GooglePlayBilling.ConnectError), this, nameof(OnConnectError));
// Purchases (Dictionary[]).
_payment.Connect(nameof(GooglePlayBilling.PurchasesUpdated), this, nameof(OnPurchasesUpdated));
// Response ID (int), Debug message (string).
_payment.Connect(nameof(GooglePlayBilling.PurchaseError), this, nameof(OnPurchaseError));
// SKUs (Dictionary[]).
_payment.Connect(nameof(GooglePlayBilling.SkuDetailsQueryCompleted), this, nameof(OnSkuDetailsQueryCompleted));
// Response ID (int), Debug message (string), Queried SKUs (string[]).
_payment.Connect(nameof(GooglePlayBilling.SkuDetailsQueryError), this, nameof(OnSkuDetailsQueryError));
// Purchase token (string).
_payment.Connect(nameof(GooglePlayBilling.PurchaseAcknowledged), this, nameof(OnPurchaseAcknowledged));
// Response ID (int), Debug message (string), Purchase token (string).
_payment.Connect(nameof(GooglePlayBilling.PurchaseAcknowledgementError), this, nameof(OnPurchaseAcknowledgementError));
// Purchase token (string).
_payment.Connect(nameof(GooglePlayBilling.PurchaseConsumed), this, nameof(OnPurchaseConsumed));
// Response ID (int), Debug message (string), Purchase token (string).
_payment.Connect(nameof(GooglePlayBilling.PurchaseConsumptionError), this, nameof(OnPurchaseConsumptionError));
_payment.StartConnection();
}
else
{
GD.Print($"Purchase Failed {result.ResponseCode} {result.DebugMessage}");
ShowAlert("Android IAP support is not enabled. Make sure you have enabled 'Custom Build' and installed and enabled the GodotGooglePlayBilling plugin in your Android export settings! This application will not work.");
}
}
private void OnButton1_pressed()
private void ShowAlert(string text)
{
var result = _googlePlayBilling.Purchase("android.test.canceled");
if (result != null && result.Status == (int)Error.Ok)
{
GD.Print("Purchase Requested");
}
else
{
GD.Print($"Purchase Failed {result.ResponseCode} {result.DebugMessage}");
}
}
private void OnButton2_pressed()
{
var result = _googlePlayBilling.Purchase("android.test.refunded");
if (result != null && result.Status == (int)Error.Ok)
{
GD.Print("Purchase Requested");
}
else
{
GD.Print($"Purchase Failed {result.ResponseCode} {result.DebugMessage}");
}
}
private void OnButton3_pressed()
{
var result = _googlePlayBilling.Purchase("android.test.item_unavailable");
if (result != null && result.Status == (int)Error.Ok)
{
GD.Print("Purchase Requested");
}
else
{
GD.Print($"Purchase Failed {result.ResponseCode} {result.DebugMessage}");
}
_alertDialog.DialogText = text;
_alertDialog.PopupCentered();
}
private void OnOkButton_pressed()
private void OnConnected()
{
_panel.Hide();
_processLabel.Hide();
_thanksLabel.Hide();
}
GD.Print("PurchaseManager connected");
private void OnGooglePlayBilling_Connected()
{
_googlePlayBilling.QuerySkuDetails(ArrInAppProductsSKUs, PurchaseType.InApp);
var purchasesResult = _googlePlayBilling.QueryPurchases(PurchaseType.InApp);
// We must acknowledge all puchases.
// See https://developer.android.com/google/play/billing/integrate#process for more information
var purchasesResult = _payment.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")
if (!purchase.IsAcknowledged)
{
_googlePlayBilling.AcknowledgePurchase(purchase.PurchaseToken);
GD.Print($"Purchase {purchase.Sku} has not been acknowledged. Acknowledging...");
_payment.AcknowledgePurchase(purchase.PurchaseToken);
}
}
}
@@ -174,59 +86,105 @@ namespace Android_Iap
}
}
private void OnGooglePlayBilling_SkuDetailsQueryCompleted(Godot.Collections.Array arrSkuDetails)
private async void OnDisconnected()
{
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;
}
}
ShowAlert("GodotGooglePlayBilling disconnected. Will try to reconnect in 10s...");
await ToSignal(GetTree().CreateTimer(10), "timeout");
_payment.StartConnection();
}
private void OnGooglePlayBilling_PurchasesUpdated(Godot.Collections.Array arrPurchases)
private void OnConnectError()
{
_panel.Show();
_processLabel.Show();
_thanksLabel.Hide();
ShowAlert("PurchaseManager connect error");
}
private void OnPurchasesUpdated(Godot.Collections.Array arrPurchases)
{
GD.Print($"Purchases updated: {JSON.Print(arrPurchases)}");
// See OnConnected
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")
if (!purchase.IsAcknowledged)
{
_googlePlayBilling.AcknowledgePurchase(purchase.PurchaseToken);
GD.Print($"Purchase {purchase.Sku} has not been acknowledged. Acknowledging...");
_payment.AcknowledgePurchase(purchase.PurchaseToken);
}
}
}
private void OnGooglePlayBilling_PurchaseAcknowledged(string purchaseToken)
{
_googlePlayBilling.ConsumePurchase(purchaseToken);
}
private void OnGooglePlayBilling_PurchaseConsumed(string purchaseToken)
{
if (_purchases[purchaseToken] == "android.test.purchased")
if (purchases.Length > 0)
{
_totalPotion += 5;
_totalPotionsLabel.Text = $"{_totalPotion} Potions";
_purchases.Remove(purchaseToken);
_processLabel.Hide();
_thanksLabel.Show();
_testItemPurchaseToken = purchases.Last().PurchaseToken;
}
GD.Print("OnGooglePlayBilling_PurchaseConsumed ", purchaseToken);
}
private void OnPurchaseError(int code, string message)
{
ShowAlert($"Purchase error {code}: {message}");
}
private void OnSkuDetailsQueryCompleted(Godot.Collections.Array arrSkuDetails)
{
ShowAlert(JSON.Print(arrSkuDetails));
var skuDetails = GooglePlayBillingUtils.ConvertSkuDetailsDictionaryArray(arrSkuDetails);
foreach (var skuDetail in skuDetails)
{
GD.Print($"Sku {skuDetail.Sku}");
}
}
private void OnSkuDetailsQueryError(int code, string message)
{
ShowAlert($"SKU details query error {code}: {message}");
}
private void OnPurchaseAcknowledged(string purchaseToken)
{
ShowAlert($"Purchase acknowledged: {purchaseToken}");
}
private void OnPurchaseAcknowledgementError(int code, string message)
{
ShowAlert($"Purchase acknowledgement error {code}: {message}");
}
private void OnPurchaseConsumed(string purchaseToken)
{
ShowAlert($"Purchase consumed successfully: {purchaseToken}");
}
private void OnPurchaseConsumptionError(int code, string message)
{
ShowAlert($"Purchase acknowledgement error {code}: {message}");
}
// GUI
private void OnQuerySkuDetailsButton_pressed()
{
_payment.QuerySkuDetails(new string[] { TestItemSku }, PurchaseType.InApp); // Use "subs" for subscriptions.
}
private void OnPurchaseButton_pressed()
{
var response = _payment.Purchase(TestItemSku);
if (response != null && response.Status != (int)Error.Ok)
{
ShowAlert($"Purchase error {response.ResponseCode} {response.DebugMessage}");
}
}
private void OnConsumeButton_pressed()
{
if (string.IsNullOrEmpty(_testItemPurchaseToken))
{
ShowAlert("You need to set 'test_item_purchase_token' first! (either by hand or in code)");
return;
}
_payment.ConsumePurchase(_testItemPurchaseToken);
}
}
}

View File

@@ -1,215 +0,0 @@
[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

@@ -3,7 +3,7 @@ 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: AssemblyTitle("Android in-app purchases with C#")]
[assembly: AssemblyDescription("")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("")]

View File

@@ -1,21 +1,11 @@
# 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).
These demos depend on features only available in mobile phones or tablets.
The Android IAP demo only runs on Android.
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)
Note: There is a GDScript version available [here](https://github.com/godotengine/godot-demo-projects/tree/master/mobile/AndroidInAppPurchasesWithCSharp).

View File

@@ -0,0 +1,87 @@
[gd_scene load_steps=3 format=2]
[ext_resource path="res://Main.cs" type="Script" id=1]
[ext_resource path="res://GodotGooglePlayBilling/GooglePlayBilling.cs" type="Script" id=2]
[node name="Main" type="Control"]
anchor_left = 0.5
anchor_top = 0.5
anchor_right = 0.5
anchor_bottom = 0.5
margin_left = -512.711
margin_top = -300.0
margin_right = 511.289
margin_bottom = 300.0
size_flags_horizontal = 2
size_flags_vertical = 2
script = ExtResource( 1 )
__meta__ = {
"_edit_use_anchors_": false
}
[node name="AlertDialog" type="AcceptDialog" parent="."]
anchor_left = 0.5
anchor_top = 0.5
anchor_right = 0.5
anchor_bottom = 0.5
margin_left = 64.0
margin_top = 64.0
margin_right = -64.0
margin_bottom = -64.0
grow_horizontal = 2
grow_vertical = 2
rect_min_size = Vector2( 400, 0 )
size_flags_vertical = 4
popup_exclusive = true
dialog_autowrap = true
[node name="Label" type="Label" parent="."]
margin_left = 300.0
margin_top = 40.0
margin_right = 996.0
margin_bottom = 156.0
size_flags_horizontal = 2
size_flags_vertical = 0
text = "To test in-app purchase on android device,
1. Make sure you have enabled \"Custom Build\" and the GodotPayment plugin in your Android export settings
2. Export APK and upload it as alpha or beta stage to Google Play Developer Console and publish it.
(It's not published to public, but you and other testers can access it.)
3. There should be an activate in-app item. Copy its SKU into the TEST_ITEM_SKU constant in iap_demo.gd
4. Changes you make in the Play Console may take some time before taking effect"
[node name="QuerySkuDetailsButton" type="Button" parent="."]
margin_left = 40.5697
margin_top = 39.9347
margin_right = 221.57
margin_bottom = 91.9347
text = "Query SKU details"
__meta__ = {
"_edit_use_anchors_": false
}
[node name="PurchaseButton" type="Button" parent="."]
margin_left = 40.5697
margin_top = 101.203
margin_right = 221.57
margin_bottom = 153.203
text = "Purchase"
__meta__ = {
"_edit_use_anchors_": false
}
[node name="ConsumeButton" type="Button" parent="."]
margin_left = 40.5697
margin_top = 162.142
margin_right = 221.57
margin_bottom = 214.142
text = "Consume"
__meta__ = {
"_edit_use_anchors_": false
}
[node name="GooglePlayBilling" type="Node" parent="."]
script = ExtResource( 2 )
[connection signal="pressed" from="QuerySkuDetailsButton" to="." method="OnQuerySkuDetailsButton_pressed"]
[connection signal="pressed" from="PurchaseButton" to="." method="OnPurchaseButton_pressed"]
[connection signal="pressed" from="ConsumeButton" to="." method="OnConsumeButton_pressed"]

View File

@@ -15,21 +15,24 @@ _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/name="Android in-app purchases with C#"
run/main_scene="res://main.tscn"
config/icon="res://icon.png"
[debug]
gdscript/warnings/return_value_discarded=false
[display]
window/size/width=512
window/size/height=300
window/stretch/mode="2d"
window/stretch/aspect="keep"
window/stretch/aspect="expand"
[gdnative]
singletons=[ ]
[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

@@ -1 +0,0 @@

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB