【Flutter】Androidの課金APIを操作する前に利用可能判定を行う【in_app_purchase】

エキサイト株式会社の@mthiroshiです。

Flutterのアプリ内課金の実装には、 in_app_purchase パッケージを使います。 Androidで実装する際に、少し躓いた問題があったのでご紹介します。

動作環境は、下記のpubspec.ymlの内容です。

dependencies:
  in_app_purchase: ^3.1.10
  in_app_purchase_android: ^0.3.0+13

購入商品の問い合わせに失敗するケース

アプリ起動時に課金状態を確認するため、購入商品の問い合わせ処理を実装していました。

下記にサンプルコードを示します。

/// アプリケーション用の課金操作クラス
class BillingAndroid {
  BillingAndroid() {

    InAppPurchaseAndroidPlatform.registerPlatform();

    subscription = inAppPurchase.purchaseStream.listen(
      // 購入イベントリスナー
      _listenToPurchaseUpdated,
      onDone: () {
        subscription.cancel();
      },
      onError: (_) {
      // エラー処理
      },
    );
  }

  final InAppPurchase inAppPurchase = InAppPurchase.instance;

  final BillingClientManager clientManager = BillingClientManager();

  late StreamSubscription<List<PurchaseDetails>> subscription;

  /// 購入商品の問い合わせ
  @override
  Future<void> query() async {
    final purchases = await clientManager.runWithClient((client) async {
      return await client.queryPurchases(ProductType.subs);
    });

    if (purchases.purchasesList.isEmpty) {
      // 購入商品がない場合の処理
      return;
    }

    for (final element in purchases.purchasesList) {
      // アプリケーションのAPIでレシート検証を行う
    }
  }

処理の流れは、下記になります。

  • InAppPurchaseクラスの初期化
  • Androidの課金クライアントを管理するBillingClientManagerqueryPurchasesを実行して、購入商品データ(レシート)を取得
  • 購入商品データの検証

BillingAndroidクラスのコンストラクタで、InAppPurchaseの初期化と購入イベントのリスナー設定を行っています。 そして、アプリ起動時に query() を呼び出して、購入商品の問い合わせを行います。

上記の処理では、商品を購入した状態にも関わらず、稀に空のレスポンスを返す挙動が起きていました。

課金APIの利用可能状態を確認する

InAppPurchase クラスには、 課金APIの利用可能状態を取得する isAvailable() があります。

pub.dev

isAvailable()queryPurchases() の挙動を確認してみたところ、 isAvailable() の返り値がfalseのときに、queryPurchases() が空のレスポンスを返していました。

つまり、queryPurchases() が空を返していた理由は、課金APIが利用できない状態だったからでした。

そこで、queryPurchases() の実行前に isAvailable() によって課金APIの利用可能判定を行うことで、この問題を回避しました。

下記に、利用可能判定を入れたコードを示します。

  static const maxRetryQuery = 5;

  int retryQueryCount = 0;

  @override
  Future<void> query() async {
    if (!await inAppPurchase.isAvailable()) {
      // 利用不可の場合はリトライ
      if (retryQueryCount < maxRetryQuery) {
        await Future<void>.delayed(const Duration(seconds: 1));
        await query();
        retryQueryCount++;
        return;
      }
      retryQueryCount = 0;
      return;
    }
    retryQueryCount = 0;

    final purchases = await clientManager.runWithClient((client) async {
      return await client.queryPurchases(ProductType.subs);
    });

    if (purchases.purchasesList.isEmpty) {
      // 購入情報がない場合の処理
      return;
    }

    for (final element in purchases.purchasesList) {
      // APIでレシート検証を行う
    }
  }

isAvailable() がfalseを返す際には、遅延処理を入れてリトライしています。 これにより、課金APIが利用可能な状態で操作できまして、購入商品を正しく問い合せることができました。

公式のサンプルコード

公式のサンプルコードを確認してみると、ストアの初期化処理で isAvailable() から利用可能判定を行っていました。

  Future<void> initStoreInfo() async {
    final bool isAvailable = await _inAppPurchase.isAvailable();
    if (!isAvailable) {
      setState(() {
        _isAvailable = isAvailable;
        _products = <ProductDetails>[];
        _purchases = <PurchaseDetails>[];
        _notFoundIds = <String>[];
        _consumables = <String>[];
        _purchasePending = false;
        _loading = false;
      });
      return;
    }

〜省略〜

github.com

今回対応した処理は、公式のサンプルコードから見落としていたようでした。

まとめ

FlutterのAndroidの課金実装において、課金APIを操作する前に利用可能判定を行うことについて説明しました。 課金APIの接続タイミングによっては想定した挙動にならないケースがあるので、 isAvailable() で確認してから行うことを推奨します。

参考になれば幸いです。