Flutter アプリ内課金とペイウォールの概要

0:02:00

RevenueCatのFlutter SDKコードラボへようこそ!

目標: このコードラボでは、RevenueCat Flutter SDKを新しいFlutterアプリケーションに統合し、商品Offeringを取得し、事前構築されたネイティブペイウォールUIを表示する方法を学びます。

構築するもの:

読み込みインジケーターを表示し、RevenueCatから設定した「Offering」を取得し、PaywallViewを表示するシンプルな1画面アプリ。

前提条件

開始する前に、以下を設定しておく必要があります:

  1. Flutter SDK: Flutter開発環境が準備されていること。
  2. RevenueCatアカウント: revenuecat.comで無料アカウント。
  3. App Store / Play Storeの設定:
App Store ConnectまたはGoogle Play Consoleで作成されたアプリ内商品(サブスクリプションまたは1回限りの購入)。Google Playの設定について詳しく知りたい場合は、コードラボ1:RevenueCat Google Play連携を確認してください。 RevenueCatダッシュボード内でこの商品がEntitlementOfferingにリンクされていること。これは最も重要なステップです。「Current」のOfferingがない場合、ペイウォールは表示されません。
  1. 物理デバイスまたは設定されたエミュレーター: アプリ内課金のテスト用。

このコードラボを終える頃には、Flutterアプリにアプリ内課金を正常に実装し、RevenueCatのFlutter SDKを使用して動的なペイウォールを表示できるようになります。

overview

RevenueCat SDKのインポート

0:05:00

まず最初に、アプリ内課金を実装する前に、RevenueCat SDKを既存または新規プロジェクトにインポートする必要があります。開始するには、pubspec.yamlファイルに以下の依存関係を追加します:

GitHubで最新リリースバージョンを確認できます。

gradle
dependencies:
  purchases_flutter: 9.2.3
  purchases_ui_flutter: 9.2.3

下の画像のように依存関係を追加したら、「Pub get」ボタンをクリックすると、必要な依存関係が自動的にダウンロードされます。

dependencies

次に、RevenueCat SDKを初期化しましょう。これはmain.dartファイルで行います。

  1. lib/main.dartを開き、その内容全体を以下のコードで置き換えます。
  2. _androidApiKey_iosApiKey定数にAPIキーを貼り付けます。
dart
// lib/main.dart

import 'package:flutter/material.dart';
import 'package:purchases_flutter/purchases_flutter.dart';
import 'dart:io' show Platform;

// --- ここにREVENUECATキーを貼り付けてください ---
const String _androidApiKey = "goog_YOUR_KEY_HERE";
const String _iosApiKey = "appl_YOUR_KEY_HERE";

void main() async {
  // 非同期プラグイン呼び出しの前にFlutterウィジェットが初期化されていることを確認
  WidgetsFlutterBinding.ensureInitialized();

  // アプリを実行する前にRevenueCat SDKを初期化
  await _initializeRevenueCat();

  runApp(const MyApp());
}

Future<void> _initializeRevenueCat() async {
  // 開発用にデバッグログを有効化
  await Purchases.setLogLevel(LogLevel.debug);

  PurchasesConfiguration? configuration;
  if (Platform.isAndroid) {
    configuration = PurchasesConfiguration(_androidApiKey);
  } else if (Platform.isIOS) {
    configuration = PurchasesConfiguration(_iosApiKey);
  } else {
    print("このプラットフォーム用にRevenueCatは設定されていません。");
    return;
  }

  try {
    await Purchases.configure(configuration);
    print("RevenueCatの設定に成功しました!");
  } catch (e) {
    print("RevenueCat設定エラー: $e");
  }
}

// ... 残りのコードは次のステップで追加します ...

async main関数を作成しました。これにより、_initializeRevenueCat関数をawaitでき、UIが構築される前にRevenueCat SDKが設定され準備が整っていることが保証されます。これは、最初から利用可能である必要があるプラグインを初期化する正しい方法です。

素晴らしい!これで実装の50%が完了しました。

Entitlementの検証

0:03:00

次に、ユーザーのEntitlementの検証に移りましょう。

前述の通り、Entitlementはユーザーが購入後にロック解除するアクセスレベルや機能を表します。これは、広告バナーを表示するかどうか、またはプレミアムアクセスを付与するかどうかを決定するのに役立ちます。

以下のコードスニペットを使用して、ユーザーがアクティブなEntitlementを持っているかどうかを簡単に確認できます:

dart
const ENTITLEMENT_IDENTIFIER = ".."; // RevenueCatダッシュボードから特定のEntitlement識別子を取得
final customerInfo = await Purchases.getCustomerInfo();
final isEntitled = customerInfo.entitlements.active[ENTITLEMENT_IDENTIFIER]?.isActive;

ユーザーが特定のEntitlementを持っているかどうかを確認したら、アプリのビジネスモデルに基づいてどのように進めるかを決定できます。

例えば、アプリが広告サポートの場合、AdMobバナーを表示または非表示にすることを選択できます。または、ペイウォールや購入ダイアログを表示して、ユーザーが高度な機能やコンテンツをロック解除できるようにすることもできます。

そのロジックを実装する方法の例を以下に示します:

dart
if (isEntitled == true) {
// ユーザーがこのEntitlementへのアクセスを付与されている場合、バナーを表示する必要はありません
} else {
// ここでバナーUIを表示するか、ペイウォールを表示
..
}

アプリ内課金の実装

0:04:00

次に、広告なし体験を提供するためのアプリ内課金を実装しましょう。開始するには、まずRevenueCatダッシュボードから関連する商品情報を取得する必要があります。この商品データは、ユーザーに購入オプションを提示するために使用されます。

以下の例に示すように、Purchases.sharedInstance.awaitGetProducts()を呼び出すことで利用可能な商品を取得できます:

dart
// RevenueCatサーバーから商品情報を取得
final List<StoreProduct> products = await Purchases.getProducts(['paywall_tester.subs']);

// アプリ内課金を進行
final purchaseResult = await Purchases.purchaseStoreProduct(products.first);

paywall_tester.subs:weeklypaywall_tester.subs:monthlypaywall_tester.subs:yearlyなど、複数の商品バリエーションを提供している場合、productIdsフィールドの値としてベース商品識別子paywall_tester.subsを使用することで商品の取得を簡素化できます。これにより、RevenueCatはすべての関連商品バリエーションをリストとして取得し、ペイウォールUIで動的に提示できます。

商品データを取得したら、Purchases.sharedInstance.awaitPurchase(product)を呼び出すことでアプリ内課金フローを開始できます。これにより自動的にGoogle Play購入ダイアログがトリガーされ、ユーザーはアプリ内でトランザクションを完了できます。

このように、わずか数行のコードで完全に機能するアプリ内課金フローを統合できました—レシート、ストアAPI、または購入検証を手動で処理する複雑さに対処する必要はありません。

完全なコード例は以下のようになります:

dart
import 'package:flutter/services.dart';
import 'package:purchases_flutter/purchases_flutter.dart';

/// 特定の商品をIDで取得し、購入フローを開始します。
///
/// この関数は、商品が見つからない、ユーザーが購入をキャンセルした、
/// またはその他のストアエラーなどの潜在的なエラーを処理します。
Future<void> purchaseProduct() async {
  // 1. 取得したい商品識別子を定義
  const String productId = 'paywall_tester.subs';

  try {
    // 2. RevenueCatからStoreProduct(s)を取得
    print('商品を取得中...');
    final List<StoreProduct> products = await Purchases.getProducts([productId]);

    // 3. 商品リストが空でないか確認
    if (products.isEmpty) {
      print('エラー:商品が見つかりません。IDとRevenueCatの設定を確認してください。');
      // オプションで、ユーザーにエラーメッセージを表示
      return;
    }

    final StoreProduct productToPurchase = products.first;
    print('商品が見つかりました:${productToPurchase.title}。購入を開始します...');

    // 4. 購入フローを開始
    final PurchaseResult purchaseResult = await Purchases.purchaseStoreProduct(productToPurchase);

    // 5. Entitlementを確認して購入が成功したか確認
    // "your_premium_entitlement"をRevenueCatからの実際のEntitlement識別子に置き換えてください
    if (purchaseResult.customerInfo.entitlements.all["your_premium_entitlement"]?.isActive ?? false) {
      print('購入成功!ユーザーはプレミアムアクセスを持っています。');
      // プレミアムコンテンツへのアクセスを付与
    } else {
      print('購入完了しましたが、Entitlementがアクティブではありません。');
    }
  } on PlatformException catch (e) {
    // 6. 潜在的なエラーを処理
    final PurchasesErrorCode error = PurchasesErrorHelper.getErrorCode(e);
    if (error == PurchasesErrorCode.purchaseCancelledError) {
      print('ユーザーによって購入がキャンセルされました。');
    } else {
      print('購入がエラーで失敗しました:${e.message}');
    }
  } catch (e) {
    print('予期しないエラーが発生しました:$e');
  }
}

ペイウォールの実装

0:07:00

次は、dartでペイウォールを実装する時間です。

ステップ1:アプリシェルの作成

次に、基本的なFlutterアプリの構造を追加しましょう。これにはMyAppウィジェットと、最終的にペイウォールを保持するステートフルなMyHomePageウィジェットが含まれます。

lib/main.dartファイルに以下のコードを追加します:

dart
// lib/main.dart(続き)

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'RevenueCatペイウォールデモ',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
        useMaterial3: true,
      ),
      home: const MyHomePage(title: 'RevenueCatペイウォールデモ'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key, required this.title});
  final String title;

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

// ... Stateクラスは次のステップで構築されます ...

ステップ2:Offeringの取得と状態の管理

これがコアロジックです。_MyHomePageStateで、RevenueCatからOfferingを取得し、読み込み状態とエラー状態を管理します。

コメント// ... Stateクラスは構築されます...を以下の完全な_MyHomePageStateクラスで置き換えます:

dart
// lib/main.dart(続き)

class _MyHomePageState extends State<MyHomePage> {
  // 状態変数
  Offering? _offering;
  bool _isLoading = true;
  String? _errorMessage;

  @override
  void initState() {
    super.initState();
    // ページが読み込まれたらすぐにOfferingを取得
    _loadOfferings();
  }

  Future<void> _loadOfferings() async {
    // setStateを呼び出す前にウィジェットがまだマウントされているか確認する必要があります
    if (!mounted) return;

    setState(() {
      _isLoading = true;
      _errorMessage = null; // 前のエラーをクリア
    });

    try {
      // RevenueCatから現在のOfferingを取得
      final offerings = await Purchases.getOfferings();

      // 非同期呼び出し後にウィジェットがまだマウントされているか確認
      if (!mounted) return;

      setState(() {
        _offering = offerings.current;
        _isLoading = false;
        if (_offering == null) {
          _errorMessage = "現在のOfferingが見つかりません。RevenueCatダッシュボードを確認してください。";
        }
      });
    } catch (e) {
      if (!mounted) return;
      setState(() {
        _isLoading = false;
        _errorMessage = "Offeringの読み込みに失敗しました:${e.toString()}";
      });
    }
  }

  // buildメソッドは最終ステップで追加されます...
  @override
  Widget build(BuildContext context) {
    // 今はプレースホルダー
    return Scaffold(
      appBar: AppBar(title: Text(widget.title)),
      body: const Center(child: Text("もうすぐ完成!")),
    );
  }
}
何をしたか?
  1. 3つの状態変数を作成しました:スピナーを表示するための_isLoading、エラーを表示するための_errorMessage、ペイウォール用のデータを保持するための_offering
  2. initStateで、_loadOfferings()を呼び出してすぐにデータ取得プロセスを開始しました。
  3. _loadOfferingsメソッドはPurchases.getOfferings()呼び出しをtry...catchブロックでラップしています。ローダーを表示/非表示にし、取得したOfferingまたはエラーメッセージを保存するように状態を更新します。

ステップ3:PaywallViewの表示

最後に、buildメソッドを更新して、状態変数に基づいて正しいUIを条件付きで表示します。Offeringが正常に読み込まれたら、PaywallViewを表示します。

_MyHomePageStatebuildメソッドをこの完全なバージョンで置き換えます:

dart
// lib/main.dart(_MyHomePageState内)

@override
Widget build(BuildContext context) {
  Widget content;

  // Offeringを取得中はローダーを表示
  if (_isLoading) {
    content = const Column(
      mainAxisAlignment: MainAxisAlignment.center,
      children: [
        CircularProgressIndicator(),
        SizedBox(height: 20),
        Text("ペイウォールを読み込み中..."),
      ],
    );
  }
  // 何か問題が発生した場合はエラーメッセージを表示
  else if (_errorMessage != null) {
    content = Padding(
      padding: const EdgeInsets.all(16.0),
      child: Text(
        _errorMessage!,
        textAlign: TextAlign.center,
        style: const TextStyle(color: Colors.red, fontSize: 16),
      ),
    );
  }
  // Offeringが読み込まれたらPaywallViewを表示
  else if (_offering != null) {
    content = PaywallView(
      offering: _offering!,
      onDismiss: () => Navigator.pop(context),
      onPurchaseCompleted: (customerInfo) {
        print("Entitlementの購入完了:${customerInfo.entitlements.active.keys.first}");
        // ここで閉じたりナビゲートしたりできます
      },
      onPurchaseError: (error) {
        print("購入エラー:${error.message}");
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(content: Text("購入に失敗しました:${error.message}")),
        );
      },
      onRestoreCompleted: (customerInfo) {
        print("復元完了。アクティブなEntitlement:${customerInfo.entitlements.active.length}");
        if (customerInfo.entitlements.active.isNotEmpty) {
           Navigator.pop(context);
        }
      },
    );
  }
  // ありそうにないケースのフォールバック
  else {
    content = const Text("利用可能なOfferingがありません。");
  }

  return Scaffold(
    appBar: AppBar(
      backgroundColor: Theme.of(context).colorScheme.inversePrimary,
      title: Text(widget.title),
    ),
    body: Center(
      child: content,
    ),
  );
}

完了です!

デバイスまたはエミュレーターでアプリケーションを実行してください。

sh
flutter run
以下が表示されるはずです:
  1. スピナー付きの「ペイウォールを読み込み中...」メッセージ。
  2. その後、下の画像のような完全に機能するネイティブペイウォールUI。
result

トラブルシューティング

ペイウォールが表示されない場合は、デバッグコンソールを確認し、これらの一般的な問題を確認してください: "現在のOfferingが見つかりません":これは最も一般的なエラーです。RevenueCatダッシュボードに「current」のOfferingがないことを意味します。Offeringsに移動し、Offeringを選択して、「current」としてマークされていることを確認してください。 不正なAPIキー:テストしているプラットフォームの正しいパブリックAPIキーをコピーしたことを再確認してください。 商品が設定されていない:ストアコンソールのアプリ内商品が正しく設定され、RevenueCatのEntitlementとOfferingにリンクされていることを確認してください。 サンドボックス/テストユーザーの問題:サンドボックステスターアカウント(iOS)でログインしているか、メールがライセンステスター(Android)として追加されていることを確認してください。

設定完了!🥳 これで、ユーザーが必要なEntitlementを持っていない場合に、ペイウォールエディターで設定したのとまったく同じデザインでペイウォールを表示できるようになります。

コードラボ:RevenueCat Google Play連携(ペイウォールの作成)で既に見たように、ペイウォールシステムはサーバードリブンUIに基づいて構築されています。これは、アプリの更新をプッシュしたり、レビュープロセスを経ることなく、ダッシュボードから直接ペイウォールのコンテンツとデザインを動的に更新できることを意味します。

まとめ

このコードラボでは、RevenueCatのFlutter SDKを統合し、アプリ内課金を実装し、Flutterでペイウォールを構築する方法を学びました。さあ、アプリをリリースしてもっとお金を稼ぎましょう!💰

以下のリソースでRevenueCat SDKの使用についてさらに学ぶこともできます: