fc2ブログ

記事一覧

Google Play Billing Library V5 の対応方法 | Xamarin.Forms


毎年更新が必須になってきました、Google Play Billing Library v5 への対応方法を調査・実装してみました。毎年10月頃になるとGoogle から以下のようなメールが飛んできます。内容は2022年のメールの内容ですが、今年も 2023年11月1日 までに Google Play Billing Library 5 に対応することが求められると思いますので、早速対応するコードを Xamarin 用に実装してみました。



GoogleBillingClientV4.png


前提条件
・Windows10 Pro 64Bit 1903
・Visual Studio 2022 Community v17.3.6
・Xamarin 17.3.0.308 (NuGet Xamarin.Forms 4.6.0.1141)
・以前の記事「Google Play Billing Library V3 に対応する方法


参考URL
 https://codelabs.developers.google.com/play-billing-codelab?hl=ja#0



1.環境設定

前提となりますパーミッションの設定やインストール、Google Play Console の設定方法などは、過去の記事「Xamarin.InAppBillingでAndroid用の課金アプリを作る (決済機能)」をご参考ください。



2.Xamarin.Android.Google.BillingClient の更新

NuGet パッケージマネージャを開き、Xamarin.Android.Google.BillingClient を v5.2.0 へ更新します。

GoogleBillingClientV5_02.png



3.Androidの実装

Google Play Billing Library V3 から V4 には変更なしで更新できましたが、V4 から V5 へはソースコードを30ステップほど書き換える必要があります。主には Sku という表記が Product という表記のメソッド名に変更されており、引数なども変更になっています。
機能概要V3/V4V5
購入可能なアイテム取得 QuerySkuDetails(SkuDetailsParams, ISkuDetailsResponseListener)
QueryProductDetails(QueryProductDetailsParams, IProductDetailsResponseListener)
購入履歴取得 QueryPurchaseHistory(文字列、IPurchaseHistoryListener) QueryPurchaseHistory(QueryPurchaseHistoryParams, IPurchaseHistoryListener);

その他、V4 と V5 の詳細な違いについてはGoogleのホームページに記載がありますので、そちらでご確認ください。

完成した Android 用のソースコードは以下の通りです。

InBillingServiceV5.cs
using Android.App;
using System;
using System.Collections.Generic;
using System.Linq;
using Android.BillingClient.Api;
using Forms = AppName.Droid.Models.Forms;
using Xamarin.Forms.Internals;

namespace AppName.Droid.Services
{
/// <summary>
/// InBillingServiceV5
/// Google Play Billing Library v5 対応
/// https://developer.android.com/google/play/billing/integrate?hl=ja
/// </summary>
public class InBillingServiceV5
{
private BillingClient _billingClient = null;
private BillingClientStateListener _stateListener = null;
private ProductDetailsResponseListener _productDetailsListener = null;
private PurchaseHistoryResponseListener _purchaseHistoryListener = null;
private bool _connected = false;
private string _productId = String.Empty;
private string _itemType = String.Empty;

/// <summary>
/// アカウント情報(アプリ起動中に永続的に記憶させたいためstaticに設定)
/// </summary>
public static AccountIdentifiers AccountIdentifiers { get; set; }

/// <summary>
/// 購入結果をハンドリングするイベント
/// </summary>
public event OnPurchaseProductDelegate OnPurchaseProduct;
public event OnUserCanceledDelegate OnUserCanceled;
public event OnPurchaseProductErrorDelegate OnPurchaseProductError;
public event OnConnectedDelegate OnConnected;

public void SetConnected(bool isConnected)
{
_connected = isConnected;
if (this.OnConnected != null &&
isConnected)
{
this.OnConnected();
}
}

public bool IsConnected
{
get
{
if (_billingClient == null)
{
return false;
}
return _billingClient.IsReady && _connected;
}
}

/// <summary>
/// BillingClient を初期化する
/// </summary>
/// <param name="productId">Google Play ConsoleのアイテムID</param>
/// <param name="itemType">BillingClient.SkuType.Inapp or BillingClient.SkuType.Subs</param>
public void Initialize(string productId, string itemType)
{
try
{
_productId = productId;
_itemType = itemType;

Activity activity = Forms.MainActivity;

var listener = new PurchasesUpdatedListener(this, _itemType);
//listener.OnPurchaseProduct += () =>
//{
// if (this.OnPurchaseProduct != null)
// {
// this.OnPurchaseProduct();
// }
//};
listener.OnUserCanceled += () =>
{
if (this.OnUserCanceled != null)
{
this.OnUserCanceled();
}
};
listener.OnPurchaseProductError += (responseCode, sku) =>
{
if (this.OnPurchaseProductError != null)
{
this.OnPurchaseProductError(responseCode, sku);
}
};

//Google Play Billing Library
_billingClient = BillingClient.NewBuilder(activity)
.EnablePendingPurchases()
.SetListener(listener)
.Build();
}
catch (Exception ex)
{
Console.WriteLine(ex.Tostring());
}
}

/// <summary>
/// Google Play との接続を確立する
/// </summary>
public void StartConnection()
{
try
{
if (_billingClient == null)
{
if (!String.IsNullOrEmpty(_productId) &&
!String.IsNullOrEmpty(_itemType))
{
//一度でも接続した場合、再初期化する
this.Initialize(_productId, _itemType);
}
else
{
return;
}
}
_stateListener = new BillingClientStateListener(this);
_billingClient.StartConnection(_stateListener);
}
catch (Exception ex)
{
Console.WriteLine(ex.Tostring());
}
}

/// <summary>
/// 購入可能なアイテムを表示する
/// </summary>
public void QueryProductDetails()
{
try
{
if (!this.IsConnected)
{
return;
}

// var skuList = new List<string>();
// //skuList.Add("premium_upgrade");
// //skuList.Add("gas");
// skuList.Add(_productId);
// SkuDetailsParams param = SkuDetailsParams.NewBuilder()
// .SetSkusList(skuList)
// .SetType(_itemType)
// .Build();
var list = new List<QueryProductDetailsParams.Product>();
list.Add(QueryProductDetailsParams.Product.NewBuilder()
.SetProductId(_productId)
.SetProductType(_itemType)
.Build()
);
var prms = QueryProductDetailsParams.NewBuilder()
.SetProductList(list)
.Build();

_productDetailsListener = new ProductDetailsResponseListener();
//_billingClient.QueryProductDetails(param, _skuDetailsListener);
_billingClient.QueryProductDetails(prms, _productDetailsListener);
}
catch (Exception ex)
{
Console.WriteLine(ex.Tostring());
}
}

/// <summary>
/// 購入フローを起動する
/// </summary>
public bool CanPurchase()
{
try
{
if (!this.IsConnected)
{
return false;
}

//var detail = new SkuDetails(_productId);
var detail = _productDetailsListener.ProductDetailsList.FirstOrDefault();
return this.CanPurchase(detail);
}
catch (Exception ex)
{
Console.WriteLine(ex.Tostring());
}
return false;
}

/// <summary>
/// 購入フローを起動する
/// </summary>
public bool CanPurchase(ProductDetails productDetails)
{
try
{
if (!this.IsConnected)
{
return false;
}

// An activity reference from which the billing flow will be launched.
Activity activity = Forms.MainActivity;

// Retrieve a value for "productDetails" by calling queryProductDetailsAsync()
// Get the offerToken of the selected offer
var offerToken = productDetails.GetSubscriptionOfferDetails().FirstOrDefault().OfferToken;

var list = new List<ProductDetailsParams>();
var prms = ProductDetailsParams.NewBuilder()
.SetProductDetails(productDetails)
.SetOfferToken(offerToken)
.Build();
list.Add(prms);

BillingFlowParams.Builder builder = BillingFlowParams.NewBuilder()
.SetProductDetailsParamsList(list);
if (InBillingServiceV5.AccountIdentifiers != null)
{
//不正行為が行われる前に Google が検出できるようにする
builder = builder.SetObfuscatedAccountId(InBillingServiceV5.AccountIdentifiers.ObfuscatedAccountId)
.SetObfuscatedProfileId(InBillingServiceV5.AccountIdentifiers.ObfuscatedProfileId);
Console.WriteLine(" ** CanPurchase Set AccountIdentifiers.");
}
BillingFlowParams billingFlowParams = builder.Build();
BillingResult result = _billingClient.LaunchBillingFlow(activity, billingFlowParams);
BillingResponseCode responseCode = result.ResponseCode;

// Handle the result.
return responseCode == BillingResponseCode.Ok;
}
catch (Exception ex)
{
Console.WriteLine(ex.Tostring());
}
return false;
}

/// <summary>
/// SKU商品を消費する
/// </summary>
/// <param name="purchaseToken"></param>
public void Consume(string purchaseToken)
{
try
{
if (!this.IsConnected)
{
return;
}

ConsumeParams consumeParams =
ConsumeParams.NewBuilder()
.SetPurchaseToken(purchaseToken)
.Build();

ConsumeResponseListener listener = new ConsumeResponseListener();
listener.OnPurchaseProduct += () =>
{
if (this.OnPurchaseProduct != null)
{
this.OnPurchaseProduct();
}
};

_billingClient.Consume(consumeParams, listener);
}
catch (Exception ex)
{
Console.WriteLine(ex.Tostring());
}
}

/// <summary>
/// 定期購読を消費する
/// </summary>
/// <param name="purchaseToken"></param>
public void AcknowledgePurchase(string purchaseToken)
{
try
{
if (!this.IsConnected)
{
return;
}

AcknowledgePurchaseParams acknowledgeParams =
AcknowledgePurchaseParams.NewBuilder()
.SetPurchaseToken(purchaseToken)
.Build();

AcknowledgePurchaseResponseListener listener = new AcknowledgePurchaseResponseListener();
listener.OnPurchaseProduct += () =>
{
if (this.OnPurchaseProduct != null)
{
this.OnPurchaseProduct();
}
};

_billingClient.AcknowledgePurchase(acknowledgeParams, listener);
}
catch (Exception ex)
{
Console.WriteLine(ex.Tostring());
}
}

/// <summary>
/// 購入結果の確認クエリを投げる
/// </summary>
public Task<QueryPurchasesResult> GetQueryPurchases(string itemType)
{
try
{
if (!this.IsConnected)
{
return null;
}

// https://developer.android.com/google/play/billing/migrate-gpblv5
var prms = QueryPurchasesParams.NewBuilder()
.SetProductType(itemType)
.Build();
return _billingClient.QueryPurchasesAsync(prms);
}
catch (Exception ex)
{
Console.WriteLine(ex.Tostring());
}
return null;
}

/// <summary>
/// 購入履歴の確認クエリを投げる
/// </summary>
public void QueryPurchaseHistory()
{
try
{
if (!this.IsConnected)
{
return;
}

_purchaseHistoryListener = new PurchaseHistoryResponseListener();
var prms = QueryPurchaseHistoryParams.NewBuilder()
.SetProductType(_itemType)
.Build();
_billingClient.QueryPurchaseHistory(prms, _purchaseHistoryListener);
}
catch (Exception ex)
{
Console.WriteLine(ex.Tostring());
}
}

/// <summary>
/// 購入履歴を取得する
/// </summary>
/// <returns>購入履歴のリスト</returns>
public IList<Android.BillingClient.Api.PurchaseHistoryRecord> GetPurchaseHistoryRecords()
{
try
{
if (!this.IsConnected)
{
return null;
}

return _purchaseHistoryListener.PurchaseHistoryList;
}
catch (Exception ex)
{
Console.WriteLine(ex.Tostring());
}
return null;

}
// public async Task<IList<Android.BillingClient.Api.PurchaseHistoryRecord>> GetPurchaseHistoryRecordsAsync()
// {
// if (!this.IsConnected)
// {
// return null;
// }
//
// var ret = await _billingClient.QueryPurchaseHistoryAsync(_itemType);
// return ret.PurchaseHistoryRecords;
//
// }


/// <summary>
/// Google Play との接続を切断する
/// </summary>
public void Disconnect()
{
try
{
if (this.IsConnected &&
_billingClient != null)
{
_billingClient.EndConnection();
_billingClient.Dispose();
_billingClient = null;
}
}
catch (Exception ex)
{
Console.WriteLine(ex.Tostring());
}
}


}

/// <summary>
/// Google Play との接続状態を管理する
/// </summary>
public class BillingClientStateListener : Java.Lang.Object, IBillingClientStateListener
{
private InBillingServiceV5 _inBillingService = null;

public BillingClientStateListener()
{
}

public BillingClientStateListener(InBillingServiceV5 instance)
{
_inBillingService = instance;
}

public void OnBillingSetupFinished(Android.BillingClient.Api.BillingResult result)
{
Console.WriteLine("OnBillingSetupFinished ResponseCode : " + result.ResponseCode.ToString());
if (result.ResponseCode == BillingResponseCode.Ok)
{
// The BillingClient is ready. You can query purchases here.
if (_inBillingService != null)
{
_inBillingService.SetConnected(true);
_inBillingService.QueryProductDetails(); //購入商品リストの確認
_inBillingService.QueryPurchaseHistory(); //購入履歴の確認
}
}
}
public void OnBillingServiceDisconnected()
{
// Try to restart the connection on the next request to
// Google Play by calling the startConnection() method.
Console.WriteLine("OnBillingServiceDisconnected");
if (_inBillingService != null)
{
_inBillingService.SetConnected(false);
}
}
}

/// <summary>
/// 購入可能なアイテムの結果を取得する
/// </summary>
public class ProductDetailsResponseListener : Java.Lang.Object, IProductDetailsResponseListener
{
public IList<ProductDetails> ProductDetailsList = null;

public void OnProductDetailsResponse(BillingResult result, IList<ProductDetails> list)
{
Console.WriteLine("OnProductDetailsResponse ResponseCode : " + result.ResponseCode.ToString());
if (result.ResponseCode == BillingResponseCode.Ok)
{
list.ForEach(r =>
{
Console.WriteLine("OnProductDetailsResponse ProductDetails : " + r);
});
this.ProductDetailsList = list;
}
}
}

/// <summary>
/// 購入結果の確認
/// </summary>
public class PurchasesUpdatedListener : Java.Lang.Object, IPurchasesUpdatedListener
{
private InBillingServiceV5 _instance= null;
private string _itemType = String.Empty;
public event OnUserCanceledDelegate OnUserCanceled;
public event OnPurchaseProductErrorDelegate OnPurchaseProductError;

public PurchasesUpdatedListener(InBillingServiceV5 instance, string itemType)
{
_instance = instance;
_itemType = itemType;
}

public void OnPurchasesUpdated(Android.BillingClient.Api.BillingResult result, IList<Android.BillingClient.Api.Purchase> list)
{
Console.WriteLine("OnPurchasesUpdated ResponseCode : " + result.ResponseCode.ToString());
if (result.ResponseCode == BillingResponseCode.Ok &&
list != null)
{
list.ForEach(r =>
{
//Console.WriteLine("OnPurchasesUpdated Purchase : " + r.OriginalJson);
InBillingServiceV5.AccountIdentifiers = r.AccountIdentifiers; //不正防止
this.HandlePurchase(r);
});
} else if (result.ResponseCode == BillingResponseCode.UserCancelled) {
// Handle an error caused by a user cancelling the purchase flow.
Console.WriteLine("OnPurchasesUpdated UserCancelled");
if (this.OnUserCanceled != null)
{
this.OnUserCanceled();
}
} else {
if (this.OnPurchaseProductError != null)
{
this.OnPurchaseProductError((int)result.ResponseCode, "");
}
// Handle any other error codes.
Console.WriteLine("OnPurchasesUpdated Fail");
}
}

/// <summary>
/// 購入を処理する
/// </summary>
/// <param name="purchase"></param>
public void HandlePurchase(Purchase purchase)
{
try
{
if (purchase == null)
{
Console.WriteLine("HandlePurchase purchase is null.");
return;
}
if (_instance == null)
{
Console.WriteLine("HandlePurchase _instance is null.");
return;
}

// Purchase retrieved from BillingClient#queryPurchases or your PurchasesUpdatedListener.
//Purchase purchase = ...;

// Verify the purchase.
// Ensure entitlement was not already granted for this purchaseToken.
// Grant entitlement to the user.

if (_itemType == BillingClient.SkuType.Subs)
{
_instance.AcknowledgePurchase(purchase.PurchaseToken);
}
else
{
_instance.Consume(purchase.PurchaseToken);
}

Console.WriteLine("HandlePurchase Consume Success.");
}
catch (Exception ex)
{
Console.WriteLine(ex.Tostring());
}
}
}

/// <summary>
/// 購入の処理結果を取得する
/// </summary>
public class ConsumeResponseListener : Java.Lang.Object, IConsumeResponseListener
{
public event OnPurchaseProductDelegate OnPurchaseProduct;
public event OnPurchaseProductErrorDelegate OnPurchaseProductError;

public void OnConsumeResponse(BillingResult result, string purchaseToken)
{
Console.WriteLine("OnConsumeResponse ResponseCode : " + result.ResponseCode.ToString());
if (result.ResponseCode == BillingResponseCode.Ok)
{
// Handle the success of the consume operation.
//Console.WriteLine("OnConsumeResponse purchaseToken : " + purchaseToken);
if (this.OnPurchaseProduct != null)
{
this.OnPurchaseProduct();
}
}
else
{
if (this.OnPurchaseProductError != null)
{
this.OnPurchaseProductError((int)result.ResponseCode, "");
}
}
}
}

/// <summary>
/// 定期購読の処理結果を取得する
/// </summary>
public class AcknowledgePurchaseResponseListener : Java.Lang.Object, IAcknowledgePurchaseResponseListener
{
public event OnPurchaseProductDelegate OnPurchaseProduct;
public event OnPurchaseProductErrorDelegate OnPurchaseProductError;

public void OnAcknowledgePurchaseResponse(BillingResult result)
{
Console.WriteLine("OnAcknowledgePurchaseResponse ResponseCode : " + result.ResponseCode.ToString());
if (result.ResponseCode == BillingResponseCode.Ok)
{
// Handle the success of the consume operation.
if (this.OnPurchaseProduct != null)
{
this.OnPurchaseProduct();
}
}
else
{
if (this.OnPurchaseProductError != null)
{
this.OnPurchaseProductError((int)result.ResponseCode, "");
}
}
}
}

/// <summary>
/// 購入履歴を取得する
/// </summary>
public class PurchaseHistoryResponseListener : Java.Lang.Object, IPurchaseHistoryResponseListener
{
public IList<PurchaseHistoryRecord> PurchaseHistoryList = null;

public void OnPurchaseHistoryResponse(BillingResult result, IList<PurchaseHistoryRecord> list)
{
Console.WriteLine("OnPurchaseHistoryResponse ResponseCode : " + result.ResponseCode.ToString());
if (result.ResponseCode == BillingResponseCode.Ok &&
list != null)
{
//list.ForEach(r =>
//{
// Console.WriteLine("OnPurchaseHistoryResponse PurchaseHistoryRecord : " + r.OriginalJson);
//});
this.PurchaseHistoryList = list;
}
}
}
}

※ 次の記事「MainActivityのインスタンスを取得する方法」でも紹介しておりますが、Forms.MainActivity は MainActivity.cs で取得した Activity のインスタンスをシングルトンで保持しているプロパティです。Forms.Context が廃止されたための対応を行っております。


以上で、正常にGoogle Play で決済を行うことができます。
※動作確認とテストが完了したソースコードとなっています。



4.デバッグ

デバッグ方法は、過去の記事「Xamarin.InAppBillingでAndroid用の課金アプリを作る (決済機能)」をご参考ください。





最後までお読みいただきありがとうございます。
当ブログの内容をまとめた Xamarin逆引きメニュー は以下のURLからご覧になれます。
https://itblog.dynaspo.com/blog-entry-81.html


関連記事

コメント

コメントの投稿

※名前とタイトルが入力されていないコメントでは他のコメントとの区別ができません。

 入力されていないコメントには返信しませんのであらかじめご了承くださいませ。

※ニックネームでも良いので必ずご入力ください。

    

※必ずご入力ください。

    
    

※必ずご入力ください。

※技術的な質問には環境やエラーについて正確かつ詳細にお教えください。

・正確なエラーの内容

・Windowsのバージョン番号

・Visual Studioのバージョン

・機器の型番

・アプリやソフトのバージョン

    

カテゴリ別記事一覧

広告

プロフィール

石河 純


著者名 :石河 純
自己紹介:素人上がりのIT技術者。趣味は卓球・車・ボウリング

IT関連の知識はざっくりとこんな感じです。
【OS関連】
WindowsServer: 2012/2008R2/2003/2000/NT4
Windows: 10/8/7/XP/2000/me/NT4/98
Linux: CentOS RedHatLinux9
Mac: macOS Catalina 10.15 / Mojave 10.14 / High Sierra 10.13 / Sierra 10.12 / OSX Lion 10.7.5 / OSX Snow Leopard 10.6.8
【言語】
VB.net ASP.NET C#.net Java VBA
Xamarin.Forms
【データベース】
Oracle 10g/9i
SQLServer 2016/2008R2/2005/2000
SQLAnywhere 16/11/8
【BI/レポートツール】
Cognos ReportNet (IBM)
Microsoft PowerBI
ActiveReport (GrapeCity)
CrystalReport
【OCX関連】
GrapeCity InputMan SPREAD MultiRow GridView
【ネットワーク関連】
CCNP シスコ技術者認定
Cisco Catalyst シリーズ
Yamaha RTXシリーズ
FireWall関連
【WEB関連】
SEO SEM CSS jQuery IIS6/7 apache2

休みの日は卓球をやっています。
現在、卓球用品通販ショップは休業中です。