fc2ブログ

記事一覧

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


ある日突然、Google からメールが飛んできました。内容はイメージ画像の通りですが、要約すると 2021年11月1日 までに Google Play Billing Library 3 に対応するようにとのことでしたので、早速対応するコードを Xamarin 用に実装してみました。


xamarin_android_googleplaybillingclient_v3_02.png



前提条件
・Windows10 Pro 64Bit 1903
・Visual Studio 2019 Community v16.8.1
・Xamarin 16.8.000.255 (NuGet Xamarin.Forms 4.6.0.1141)


参考URL
 https://developer.android.com/google/play/billing/integrate?hl=ja



1.環境設定

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



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

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

xamarin_android_googleplaybillingclient_v3_01.png



3.Androidの実装

Android プロジェクトに以下のソースコードを埋め込みます。
※まだ未テストです。取り急ぎ、Google の Java のサンプルコードを翻訳したばかりで、これからテストを行って、改良・修正していきます。

InBillingServiceV3.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>
/// InBillingServiceV3
/// Google Play Billing Library v3 対応
/// </summary>
public class InBillingServiceV3
{
private BillingClient _billingClient = null;
private BillingClientStateListener _stateListener = null;
private SkuDetailsResponseListener _skuDetailsListener = 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 void SetConnected(bool isConnected)
{
_connected = isConnected;
}

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)
{
_productId = productId;
_itemType = itemType;

Activity activity = Forms.MainActivity;

var listener = new PurchasesUpdatedListener(this, _itemType);
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();
}

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

/// <summary>
/// 購入可能なアイテムを表示する
/// </summary>
public void QuerySkuDetails()
{
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();
_skuDetailsListener = new SkuDetailsResponseListener();
_billingClient.QuerySkuDetails(param, _skuDetailsListener);
}

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

//var detail = new SkuDetails(_productId);
var detail = _skuDetailsListener.SkuDetailsList.FirstOrDefault();
return this.CanPurchase(detail);
}

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

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

// Retrieve a value for "skuDetails" by calling querySkuDetailsAsync().
BillingFlowParams.Builder builder = BillingFlowParams.NewBuilder()
.SetSkuDetails(detail);
if (InBillingServiceV3.AccountIdentifiers != null)
{
//不正行為が行われる前に Google が検出できるようにする
builder = builder.SetObfuscatedAccountId(InBillingServiceV3.AccountIdentifiers.ObfuscatedAccountId)
.SetObfuscatedProfileId(InBillingServiceV3.AccountIdentifiers.ObfuscatedProfileId);
System.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;
}

/// <summary>
/// SKU商品を消費する
/// </summary>
/// <param name="purchaseToken"></param>
public void Consume(string purchaseToken)
{
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);
}

/// <summary>
/// 定期購読を消費する
/// </summary>
/// <param name="purchaseToken"></param>
public void AcknowledgePurchase(string purchaseToken)
{
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);
}

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

// https://developer.android.com/reference/com/android/billingclient/api/BillingClient
// queryPurchases(String skuType)
return _billingClient.QueryPurchases(itemType);
}

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

_purchaseHistoryListener = new PurchaseHistoryResponseListener();
_billingClient.QueryPurchaseHistory(_itemType, _purchaseHistoryListener);
}

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

/// <summary>
/// Google Play との接続を切断する
/// </summary>
public void Disconnect()
{
if (this.IsConnected &&
_billingClient != null)
{
_billingClient.EndConnection();
_billingClient.Dispose();
_billingClient = null; //インスタンスの初期化時の判断で必要です。
}
}
}

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

public BillingClientStateListener()
{
}

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

public void OnBillingSetupFinished(Android.BillingClient.Api.BillingResult result)
{
System.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.QuerySkuDetails(); //購入商品リストの確認
_inBillingService.QueryPurchaseHistory(); //購入履歴の確認
}
}
}
public void OnBillingServiceDisconnected()
{
// Try to restart the connection on the next request to
// Google Play by calling the startConnection() method.
System.Console.WriteLine("OnBillingServiceDisconnected");
if (_inBillingService != null)
{
_inBillingService.SetConnected(false);
}
}
}

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

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

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

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

public void OnPurchasesUpdated(Android.BillingClient.Api.BillingResult result, IList<Android.BillingClient.Api.Purchase> list)
{
System.Console.WriteLine("OnPurchasesUpdated ResponseCode : " + result.ResponseCode.ToString());
if (result.ResponseCode == BillingResponseCode.Ok &&
list != null)
{
list.ForEach(r =>
{
System.Console.WriteLine("OnPurchasesUpdated Purchase : " + r.OriginalJson);
InBillingServiceV3.AccountIdentifiers = r.AccountIdentifiers; //不正防止
this.HandlePurchase(r);
});
} else if (result.ResponseCode == BillingResponseCode.UserCancelled) {
// Handle an error caused by a user cancelling the purchase flow.
System.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.
System.Console.WriteLine("OnPurchasesUpdated Fail");
}
}

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

System.Console.WriteLine("HandlePurchase Consume Success.");
}
}

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

public void OnConsumeResponse(BillingResult result, string purchaseToken)
{
System.Console.WriteLine("OnConsumeResponse ResponseCode : " + result.ResponseCode.ToString());
if (result.ResponseCode == BillingResponseCode.Ok)
{
// Handle the success of the consume operation.
System.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)
{
System.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)
{
System.Console.WriteLine("OnPurchaseHistoryResponse ResponseCode : " + result.ResponseCode.ToString());
if (result.ResponseCode == BillingResponseCode.Ok &&
list != null)
{
list.ForEach(r =>
{
System.Console.WriteLine("OnPurchaseHistoryResponse SkuDetails : " + r.OriginalJson);
});
this.PurchaseHistoryList = list;
}
}
}
}

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


以上で、正常にビルドが通る状態になります。しかしながら、動作は確認ができていませんので、2021年11月までに Google Play Console でアプリを公開できるようにテストを行っていこうと考えております。
※まだ非公開テストを行っており、正式リリースはしておりません。

※2021/04/08追記
動作確認とテストが完了したソースコードに変更しました。

※2021/04/21追記
本番環境にリリースが完了したソースコードに変更しました。
Google Play Billing Library V2 との互換性も確認しました。

※2022/10/24追記
Google Play Billing Library V4 に更新しても当記事に記載のソースの変更点はありませんでした。



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

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