🏆

Flutterコード実装編【Flutter×Revenuecat】サブスクリプションを構築する

2023/04/28に公開

私が先日リリースした筋トレ習慣化アプリ(Fitness Slave)で、アプリ内課金によるサブスクリプション機能を実装しました。

◆iOS
https://apps.apple.com/jp/app/fitness-slave/id1664027797

◆Android
https://play.google.com/store/apps/details?id=net.japanblog.fitness.slave

Revenuecatのシリーズ記事です。

  1. iOSの各種設定
  2. Androidの各種設定
  3. Revenuecatでの設定
  4. Flutterアプリ内でのコード実装

今回は「4.Flutterアプリ内でのコード実装」編です。

それぞれの記事はこちら

https://zenn.dev/yamamotosaishu/articles/6e6b5aaefa397b

https://zenn.dev/yamamotosaishu/articles/8d80174051e4ac

https://zenn.dev/yamamotosaishu/articles/b19319e83fe6fb

Flutterアプリ内でのコード実装

  1. purchases_flutterのインストール
  2. ios/podfile の変更
  3. Xcodeで「In-App Purchase」を有効にする
  4. ユーザーがサブスクアイテムを保有しているかの確認
  5. 購入処理
  6. 購入の復元

purchases_flutterのインストール

https://pub.dev/packages/purchases_flutter

まずは、purchases_flutterライブラリをインストールしましょう

dependencies:
  purchases_flutter: 最新バージョンを入れてください

purchases_flutterのインストール

iOSのDeployment Targetを11.0 以上にする必要があります。

Xcodeで「In-App Purchase」を有効にする

Flutterのプロジェクトを、Xcodeで開きます。(Android studioを使っている場合は、「ios」フォルダで右クリック ⇨ Flutter ⇨ open ios module in Xcode)

そして、画像下部の赤枠で囲った 「In App Purchase」が有効になっているかを確認します。

有効になっていなかったら、左上のcapabilityから「In App Purchase」を追加して有効にします。

ユーザーがサブスクアイテムを保有しているかの確認

実際のコードの部分です。
まず、購入の前にユーザーがサブスクアイテムを保有しているかの確認が必要です。そのためのコードは以下で実装しました。

final inAppPurchaseManager =
    ChangeNotifierProvider((ref) => InAppPurchaseManager());

class InAppPurchaseManager with ChangeNotifier {
  bool isSubscribed = false;
  late Offerings offerings;
  
  Future<void> initInAppPurchase() async {

    try {
      //consoleにdebug情報を出力する
      await Purchases.setDebugLogsEnabled(true);
      late PurchasesConfiguration configuration;

      if (Platform.isAndroid) {
        configuration = PurchasesConfiguration(Android用のRevenuecat APIキー);
      } else if (Platform.isIOS) {
        configuration = PurchasesConfiguration(ios用のRevenuecat APIキー);
      }
      await Purchases.configure(configuration);
      //offeringsを取ってくる
      offerings = await Purchases.getOfferings();
      //firebaseのidと、revenuecatのuserIdを一緒にしている場合、firebaseAuthのuidでログイン
      final result = await Purchases.logIn(auth.currentUser!.uid);

      await getPurchaserInfo(result.customerInfo);

      //今アクティブになっているアイテムは以下のように取得可能
      print("アクティブなアイテム ${result.customerInfo.entitlements.active.keys}");
    } catch (e) {
      print("initInAppPurchase error caught! ${e.toString()}");
    }

  }

  Future<void> getPurchaserInfo(
      CustomerInfo customerInfo) async {

    try {
      isSubscribed = await updatePurchases(customerInfo, monthly_subscription);//monthly_subscriptionは、適宜ご自身のentitlement名に変えてください

    } on PlatformException catch (e) {
      print(" getPurchaserInfo error ${e.toString()}");
    }
  }

  Future<bool> updatePurchases(
      CustomerInfo purchaserInfo, String entitlement) async {
    var isPurchased = false;
    final entitlements = purchaserInfo.entitlements.all;
    if (entitlements.isEmpty) {
      isPurchased = false;
    }
    if (!entitlements.containsKey(entitlement)) {
      ///そもそもentitlementが設定されて無い場合
      isPurchased = false;
    } else if (entitlements[entitlement]!.isActive) {
      ///設定されていて、activeになっている場合
      isPurchased = true;
    } else {
      isPurchased = false;
    }
    return isPurchased;
  }
 
  }

ポイントとして、私の場合は状態管理をriverpodを使っており「InAppPurchaseManager」に課金関係のロジックを記載しています。

  • initInAppPurchase()でrevenuecatの初期化関係処理
  • getPurchaserInfo() → updatePurchases() で、サブスクを購入しているかの確認をして、上部にある変数「isSubscribed」に結果を格納します。

購入処理

流れとしては、まず、上記のinitInAppPurchase()を呼び出した後、以下のmakePurchase()を呼び出して実際に課金処理を行います。

  Future<void> makePurchase(
       String offeringsName) async {
    try {
      Package? package;
        package = offerings.all[offeringsName]?.monthly;//offeringsは適宜ご自身の設定したofferingsの名前に変えてください
      if (package != null) {
        await Purchases.logIn(auth.currentUser!.uid);
        CustomerInfo customerInfo = await Purchases.purchasePackage(package);
        await getPurchaserInfo(customerInfo);
      }
    } on PlatformException catch (e) {
      print(" makePurchase error ${e.toString()}");
    }
  }

購入の復元

iosの場合は、購入の復元(以前の購入履歴を復元する)を実装することが必要になります。(Appleの審査で必要)
自分の場合は、以下のように実装しました。

  Future<void> restorePurchase(String entitlement) async {
    try {
      CustomerInfo customerInfo = await Purchases.restorePurchases();
      final isActive = await updatePurchases(customerInfo, entitlement);
      if (!isActive) {
        print("購入情報なし");
      } else {
        await getPurchaserInfo(customerInfo);
        print("${entitlement} 購入情報あり 復元する");
      }
    } on PlatformException catch (e) {
      print("purchase repo  restorePurchase error ${e.toString()}");
    }
  }

なお、以下のようにしていますが、購入情報がない場合には、その旨をUI側でエラーメッセージなどで表示するのが良いかと思います。

      if (!isActive) {
        print("購入情報なし");
      } 

コード全文は以下です。


final inAppPurchaseManager =
ChangeNotifierProvider((ref) => InAppPurchaseManager());

class InAppPurchaseManager with ChangeNotifier {
  bool isSubscribed = false;
  late Offerings offerings;
  FirebaseAuth auth = FirebaseAuth.instance;

  Future<void> initInAppPurchase() async {
    try {
      //consoleにdebug情報を出力する
      await Purchases.setDebugLogsEnabled(true);
      late PurchasesConfiguration configuration;

      if (Platform.isAndroid) {
        configuration = PurchasesConfiguration(Android用のRevenuecat APIキー);
      } else if (Platform.isIOS) {
        configuration = PurchasesConfiguration(ios用のRevenuecat APIキー);
      }
      await Purchases.configure(configuration);
      //offeringsを取ってくる
      offerings = await Purchases.getOfferings();
      final result = await Purchases.logIn(auth.currentUser!.uid);

      await getPurchaserInfo(result.customerInfo);

      //今アクティブになっているアイテムは以下のように取得可能
      print("アクティブなアイテム ${result.customerInfo.entitlements.active.keys}");
    } catch (e) {
      print("initInAppPurchase error caught! ${e.toString()}");
    }
  }

  Future<void> getPurchaserInfo(CustomerInfo customerInfo) async {
    try {
      isSubscribed = await updatePurchases(customerInfo,
          monthly_subscription); //monthly_subscriptionは、適宜ご自身のentitlement名に変えてください

    } on PlatformException catch (e) {
      print(
          "getPurchaserInfo error ${PurchasesErrorHelper.getErrorCode(e)
              .toString()}");
    }
  }

  Future<bool> updatePurchases(CustomerInfo purchaserInfo,
      String entitlement) async {
    var isPurchased = false;
    final entitlements = purchaserInfo.entitlements.all;
    if (entitlements.isEmpty) {
      isPurchased = false;
    }
    if (!entitlements.containsKey(entitlement)) {
      ///そもそもentitlementが設定されて無い場合
      isPurchased = false;
    } else if (entitlements[entitlement]!.isActive) {
      ///設定されていて、activeになっている場合
      isPurchased = true;
    } else {
      isPurchased = false;
    }
    return isPurchased;
  }

  Future<void> makePurchase(
      String offeringsName) async {
    try {
      Package? package;
      package = offerings.all[offeringsName]?.monthly;
      if (package != null) {
        await Purchases.logIn(auth.currentUser!.uid);
        CustomerInfo customerInfo = await Purchases.purchasePackage(package);
        await getPurchaserInfo(customerInfo);
      }
    } on PlatformException catch (e) {
      print("purchase repo makePurchase error ${e.toString()}");
    }
  }

  Future<void> restorePurchase(String entitlement) async {
    try {
      CustomerInfo customerInfo = await Purchases.restorePurchases();
      final isActive = await updatePurchases(customerInfo, entitlement);
      if (!isActive) {
        print("購入情報なし");
      } else {
        await getPurchaserInfo(customerInfo);
        print("${entitlement} 購入情報あり 復元する");
      }
    } on PlatformException catch (e) {
      print("purchase repo  restorePurchase error ${e.toString()}");
    }
  }

}

なお、実際に課金の動きを確認する際は、ios,Androidの実機で確認する必要があります。iosの場合は、sandboxアカウントにログインした状態で行いましょう。この記事が参考になるかと思います。
https://zenn.dev/flutteruniv_dev/articles/4a5d40bb1dd7b7

Discussion