Flutter アプリにアプリ内購入を追加する

Flutter アプリにアプリ内購入を追加する

この Codelab について

subject最終更新: 1月 23, 2025
account_circle作成者: Rene Floor, Bodhi Mulders, Jop Middelkamp, Miguel Beltran

1. はじめに

Flutter アプリにアプリ内購入を追加するには、アプリストアと Google Play ストアを正しく設定し、購入を確認して、定期購入の特典などの必要な権限を付与する必要があります。

この Codelab では、3 種類のアプリ内購入をアプリ(提供済み)に追加し、Firebase で Dart バックエンドを使用してこれらの購入を確認します。提供されているアプリ「Dash Clicker」には、Dash のマスコットを通貨として使用するゲームが含まれています。次の購入オプションを追加します。

  1. 2,000 個のダッシュを一度に購入できる繰り返し可能な購入オプション。
  2. 従来のスタイルのダッシュボードを最新のスタイルのダッシュボードにアップグレードする 1 回限りの購入。
  3. 自動生成されるクリック数を 2 倍にするサブスクリプション。

最初の購入オプションでは、ユーザーに 2,000 ダッシュの直接的な特典が提供されます。ユーザーは直接購入でき、何度でも購入できます。これは直接消費され、複数回消費できるため、消耗品と呼ばれます。

2 つ目のオプションは、ダッシュをより美しいダッシュにアップグレードします。購入は 1 回のみで、永続的に使用できます。このような購入は、アプリで消費されず、有効期限がないため「非消費アイテム」と呼ばれます。

3 つ目の購入オプションは定期購入です。定期購入が有効な間は、ユーザーはより早くダッシュを獲得できますが、定期購入のお支払いを停止すると特典も失われます。

バックエンド サービス(こちらも提供されます)は Dart アプリとして実行され、購入が行われたことを検証し、Firestore を使用して購入を保存します。Firestore はプロセスを容易にするために使用されますが、本番環境のアプリでは、任意のタイプのバックエンド サービスを使用できます。

300123416ebc8dc1.png 7145d0fffe6ea741.png 646317a79be08214.png

作成するアプリの概要

  • アプリを拡張して、消耗型の購入と定期購入をサポートします。
  • また、Dart バックエンド アプリを拡張して、購入したアイテムを確認して保存します。

学習内容

  • 購入可能な商品を使用して App Store と Play ストアを設定する方法。
  • ストアと通信して購入を確認して Firestore に保存する方法。
  • アプリ内購入を管理する方法。

必要なもの

  • Android Studio 4.1 以降
  • Xcode 12 以降(iOS 開発用)
  • Flutter SDK

2. 開発環境を設定する

この Codelab を開始するには、コードをダウンロードして、iOS のバンドル ID と Android のパッケージ名を変更します。

コードをダウンロードする

コマンドラインから GitHub リポジトリのクローンを作成するには、次のコマンドを使用します。

git clone https://github.com/flutter/codelabs.git flutter-codelabs

GitHub の CLI ツールがインストールされている場合は、次のコマンドを使用します。

gh repo clone flutter/codelabs flutter-codelabs

サンプルコードは、Codelab のコレクションのコードを含む flutter-codelabs ディレクトリにクローンされます。この Codelab のコードは flutter-codelabs/in_app_purchases にあります。

flutter-codelabs/in_app_purchases のディレクトリ構造には、名前付きの各ステップの終了時に到達する必要がある状態のスナップショットが一連で含まれています。スターター コードはステップ 0 にあるため、一致するファイルを簡単に見つけることができます。

cd flutter-codelabs/in_app_purchases/step_00

先に進む場合や、ステップ後の状態を確認したい場合は、該当するステップの名前が付けられたディレクトリを参照してください。最後のステップのコードは complete フォルダにあります。

スターター プロジェクトをセットアップする

任意の IDE で step_00 からスターター プロジェクトを開きます。スクリーンショットには Android Studio を使用しましたが、Visual Studio Code もおすすめです。どちらのエディタでも、最新の Dart プラグインと Flutter プラグインがインストールされていることを確認します。

作成するアプリは、App Store や Google Play ストアと通信して、購入可能な商品とその価格を把握する必要があります。すべてのアプリは一意の ID で識別されます。iOS App Store ではバンドル ID、Android Play ストアではアプリケーション ID と呼ばれます。これらの識別子は通常、逆ドメイン名の表記を使用して作成されます。たとえば、flutter.dev のアプリ内購入アプリを作成する場合は、dev.flutter.inapppurchase を使用します。アプリの ID を決めて、プロジェクト設定で設定します。

まず、iOS のバンドル ID を設定します。

Android Studio でプロジェクトを開き、iOS フォルダを右クリックして [Flutter] をクリックし、Xcode アプリでモジュールを開きます。

942772eb9a73bfaa.png

Xcode のフォルダ構造では、ランナー プロジェクトが最上位にあり、FlutterRunnerProducts のターゲットがランナー プロジェクトの下に配置されています。[Runner] をダブルクリックしてプロジェクト設定を編集し、[Signing & Capabilities] をクリックします。選択したバンドル ID を [チーム] フィールドに入力して、チームを設定します。

812f919d965c649a.jpeg

Xcode を閉じて Android Studio に戻り、Android の設定を完了します。これを行うには、android/app,build.gradle ファイルを開き、applicationId(下のスクリーンショットの 37 行目)を、iOS バンドル識別子と同じアプリケーション ID に変更します。iOS ストアと Android ストアの ID は同じである必要はありませんが、同じ ID を使用するとエラーが発生しにくくなります。そのため、この Codelab でも同じ ID を使用します。

5c4733ac560ae8c2.png

3. プラグインをインストールする

この Codelab のパートでは、in_app_purchase プラグインをインストールします。

pubspec に依存関係を追加する

pubspec の依存関係に in_app_purchase を追加して、pubspec に in_app_purchase を追加します。

$ cd app
$ flutter pub add in_app_purchase dev:in_app_purchase_platform_interface

pubspec.yaml を開き、dependencies の下に in_app_purchase がエントリとして、dev_dependencies の下に in_app_purchase_platform_interface がエントリとして追加されていることを確認します。

pubspec.yaml

dependencies:
  flutter:
    sdk: flutter
  cloud_firestore: ^5.5.1
  cupertino_icons: ^1.0.8
  firebase_auth: ^5.3.4
  firebase_core: ^3.8.1
  google_sign_in: ^6.2.2
  http: ^1.2.2
  intl: ^0.20.1
  provider: ^6.1.2
  in_app_purchase: ^3.2.0

dev_dependencies:
  flutter_test:
    sdk: flutter
  flutter_lints: ^4.0.0
  in_app_purchase_platform_interface: ^1.4.0

[pub get] をクリックしてパッケージをダウンロードするか、コマンドラインで flutter pub get を実行します。

4. App Store を設定する

アプリ内購入を設定して iOS でテストするには、App Store で新しいアプリを作成し、そこで購入可能な商品を作成する必要があります。公開したり、審査のためにアプリを Apple に送信したりする必要はありません。これを行うには、デベロッパー アカウントが必要です。まだお持ちでない場合は、Apple デベロッパー プログラムに登録してください。

アプリ内購入を使用するには、App Store Connect で有料アプリに関する有効な契約も必要です。https://appstoreconnect.apple.com/ にアクセスし、[Agreements, Tax, and Banking](契約、税金、銀行)をクリックします。

11db9fca823e7608.png

無料アプリと有料アプリの規約が表示されます。無料アプリのステータスは有効、有料アプリのステータスは新規である必要があります。利用規約を確認し、同意して、必要な情報をすべて入力してください。

74c73197472c9aec.png

すべての設定が正しく行われると、有料アプリのステータスは有効になります。有効な同意がない場合は、アプリ内購入を試すことができないため、この点は非常に重要です。

4a100bbb8cafdbbf.jpeg

アプリ ID を登録する

Apple デベロッパー ポータルで新しい ID を作成します。

55d7e592d9a3fc7b.png

アプリ ID を選択する

13f125598b72ca77.png

アプリの選択

41ac4c13404e2526.png

説明を入力し、バンドル ID を設定します。バンドル ID は、XCode で以前に設定した値と同じにします。

9d2c940ad80deeef.png

新しいアプリ ID を作成する方法について詳しくは、デベロッパー アカウントのヘルプをご覧ください。

新しいアプリを作成する

一意のバンドル ID を使用して、App Store Connect で新しいアプリを作成します。

10509b17fbf031bd.png

5b7c0bb684ef52c7.png

新しいアプリを作成し、契約を管理する方法について詳しくは、App Store Connect ヘルプをご覧ください。

アプリ内購入をテストするには、サンドボックス テストユーザーが必要です。このテストユーザーは iTunes に接続しないでください。このユーザーは、アプリ内購入のテストにのみ使用します。Apple アカウントですでに使用されているメールアドレスは使用できません。[ユーザーとアクセス] の [サンドボックス] にある [テスター] に移動して、新しいサンドボックス アカウントを作成するか、既存のサンドボックス Apple ID を管理します。

3ca2b26d4e391a4c.jpeg

これで、iPhone でサンドボックス ユーザーを設定できます。[設定 > App Store > サンドボックス アカウント] に移動します。

d99e0b89673867cd.jpeg e1621bcaeb33d3c5.jpeg

アプリ内購入の設定

次に、購入可能な 3 つのアイテムを設定します。

  • dash_consumable_2k: 何度でも購入できる消費型の購入で、購入ごとに 2, 000 ダッシュ(アプリ内通貨)が付与されます。
  • dash_upgrade_3d: 1 回しか購入できない消耗しない「アップグレード」の購入で、見た目が異なるダッシュボタンがユーザーに提供されます。
  • dash_subscription_doubler: サブスクリプション期間中、クリックあたり 2 倍のダッシュをユーザーに付与するサブスクリプション。

d156b2f5bac43ca8.png

[アプリ内購入] > [管理] に移動します。

指定された ID を使用してアプリ内購入を作成します。

  1. dash_consumable_2kConsumable として設定します。

プロダクト ID として dash_consumable_2k を使用します。参照名は App Store Connect でのみ使用されます。dash consumable 2k に設定し、購入のローカライズを追加します。説明として 2000 dashes fly out を指定して、購入 Spring is in the air を呼び出します。

ec1701834fd8527.png

  1. dash_upgrade_3d非消耗品として設定します。

プロダクト ID として dash_upgrade_3d を使用します。参照名を dash upgrade 3d に設定し、購入のローカライズを追加します。説明として Brings your dash back to the future を指定して、購入 3D Dash を呼び出します。

6765d4b711764c30.png

  1. dash_subscription_doubler定期購入の自動更新として設定します。

定期購入のフローは少し異なります。まず、参照名とプロダクト ID を設定する必要があります。

6d29e08dae26a0c4.png

次に、サブスクリプション グループを作成する必要があります。複数のサブスクリプションが同じグループに属している場合、ユーザーは一度に 1 つのサブスクリプションのみを定期購入できますが、これらのサブスクリプション間で簡単にアップグレードまたはダウングレードできます。このグループを subscriptions と呼びます。

5bd0da17a85ac076.png

次に、定期購入の期間とローカライズを入力します。このサブスクリプションに Jet Engine という名前を付け、説明を Doubles your clicks にします。[保存] をクリックします。

bd1b1d82eeee4cb3.png

[保存] ボタンをクリックしたら、定期購入の料金を追加します。任意の価格を選択します。

d0bf39680ef0aa2e.png

購入リストに 3 件の購入が表示されます。

99d5c4b446e8fecf.png

5. Google Play ストアをセットアップする

アプリストアと同様に、Google Play ストアにもデベロッパー アカウントが必要です。まだお持ちでない場合は、アカウントを登録してください。

新しいアプリを作成する

Google Play Console で新しいアプリを作成します。

  1. Google Play Console を開きます。
  2. [すべてのアプリ] > [アプリを作成] を選択します。
  3. デフォルトの言語を選択し、アプリのタイトルを追加します。タイトルには、Google Play に表示するアプリの名前を入力します。この名前は後から変更できます。
  4. アプリがゲームであることを指定します。これは後で変更できます。
  5. アプリが無料か有料かを指定します。
  6. Play ストアのユーザーがこのアプリの連絡先として使用できるメールアドレスを追加します。
  7. コンテンツ ガイドラインと米国輸出法の宣言に記入します。
  8. [アプリを作成] を選択します。

アプリが作成されたら、ダッシュボードに移動し、[アプリをセットアップする] セクションのすべてのタスクを完了します。ここでは、コンテンツ レーティングやスクリーンショットなど、アプリに関する情報を入力します。13845badcf9bc1db.png

アプリケーションに署名する

アプリ内購入をテストするには、少なくとも 1 つのビルドを Google Play にアップロードする必要があります。

そのためには、リリースビルドにデバッグ鍵以外のもので署名する必要があります。

キーストアを作成する

既存のキーストアがある場合は、次のステップに進みます。ない場合は、コマンドラインで次のコマンドを実行して作成します。

Mac または Linux の場合は、次のコマンドを使用します。

keytool -genkey -v -keystore ~/key.jks -keyalg RSA -keysize 2048 -validity 10000 -alias key

Windows では、次のコマンドを使用します。

keytool -genkey -v -keystore c:\Users\USER_NAME\key.jks -storetype JKS -keyalg RSA -keysize 2048 -validity 10000 -alias key

このコマンドは、key.jks ファイルをホーム ディレクトリに保存します。ファイルを別の場所に保存する場合は、-keystore パラメータに渡す引数を変更します。

keystore

ファイルを非公開にします。パブリック ソース管理にチェックインしないでください。

アプリからキーストアを参照する

キーストアへの参照を含む <your app dir>/android/key.properties という名前のファイルを作成します。

storePassword=<password from previous step>
keyPassword
=<password from previous step>
keyAlias
=key
storeFile
=<location of the key store file, such as /Users/<user name>/key.jks>

Gradle で署名を設定する

<your app dir>/android/app/build.gradle ファイルを編集して、アプリの署名を構成します。

プロパティ ファイルのキーストア情報を android ブロックの前に追加します。

   def keystoreProperties = new Properties()
   def keystorePropertiesFile = rootProject.file('key.properties')
   if (keystorePropertiesFile.exists()) {
       keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
   }

   android {
         // omitted
   }

key.properties ファイルを keystoreProperties オブジェクトに読み込みます。

buildTypes ブロックの前に次のコードを追加します。

   buildTypes {
       release
{
           
// TODO: Add your own signing config for the release build.
           
// Signing with the debug keys for now,
           
// so `flutter run --release` works.
           signingConfig signingConfigs
.debug
       
}
   
}

モジュールの build.gradle ファイルの signingConfigs ブロックに、署名構成情報を設定します。

   signingConfigs {
       release
{
           keyAlias keystoreProperties
['keyAlias']
           keyPassword keystoreProperties
['keyPassword']
           storeFile keystoreProperties
['storeFile'] ? file(keystoreProperties['storeFile']) : null
           storePassword keystoreProperties
['storePassword']
       
}
   
}
   buildTypes
{
       release
{
           signingConfig signingConfigs
.release
       
}
   
}

アプリのリリースビルドが自動的に署名されるようになりました。

アプリへの署名について詳しくは、developer.android.comアプリへの署名をご覧ください。

最初のビルドをアップロードする

アプリが署名用に構成されたら、次のコマンドを実行してアプリケーションをビルドできます。

flutter build appbundle

このコマンドはデフォルトでリリースビルドを生成します。出力は <your app dir>/build/app/outputs/bundle/release/ にあります。

Google Play Console のダッシュボードで、[リリース] > [テスト] > [クローズド テスト] に移動し、新しいクローズド テスト リリースを作成します。

この Codelab では、Google によるアプリ署名のみを使用するため、[Play アプリ署名] で [続行] をクリックしてオプトインします。

ba98446d9c5c40e0.png

次に、build コマンドで生成された app-release.aab App Bundle をアップロードします。

[保存]、[リリースのレビュー] の順にクリックします。

最後に、[内部テストとしての公開を開始] をクリックして、内部テスト リリースを有効にします。

テストユーザーを設定する

アプリ内購入をテストするには、Google Play Console の次の 2 か所にテスターの Google アカウントを追加する必要があります。

  1. 特定のテストトラック(内部テスト)
  2. ライセンス テスターとして

まず、テスターを内部テストトラックに追加します。[リリース] > [テスト] > [内部テスト] に戻り、[テスター] タブをクリックします。

a0d0394e85128f84.png

[メーリング リストを作成] をクリックして、新しいメーリング リストを作成します。リストに名前を付け、アプリ内購入のテストにアクセスする必要がある Google アカウントのメールアドレスを追加します。

次に、リストのチェックボックスをオンにして、[変更を保存] をクリックします。

次に、ライセンス テスターを追加します。

  1. Google Play Console の [すべてのアプリ] ビューに戻ります。
  2. [設定] > [ライセンス テスト] に移動します。
  3. アプリ内購入をテストする必要があるテスターと同じメールアドレスを追加します。
  4. [License response] を RESPOND_NORMALLY に設定します。
  5. [変更を保存] をクリックします。

a1a0f9d3e55ea8da.png

アプリ内購入の設定

次は、アプリ内で購入できるアイテムを構成します。

App Store と同様に、3 種類の購入を定義する必要があります。

  • dash_consumable_2k: 何度でも購入できる消費型の購入で、購入ごとに 2, 000 ダッシュ(アプリ内通貨)が付与されます。
  • dash_upgrade_3d: 1 回のみ購入できる消耗しない「アップグレード」の購入。外観が異なるダッシュが表示されます。
  • dash_subscription_doubler: サブスクリプション期間中、クリックあたり 2 倍のダッシュをユーザーに付与するサブスクリプション。

まず、消耗品と非消耗品を追加します。

  1. Google Play Console に移動し、アプリケーションを選択します。
  2. [収益化 > 商品 > アプリ内アイテム] に移動します。
  3. [アイテムを作成] をクリックします。c8d66e32f57dee21.png
  4. 商品に関する必要な情報をすべて入力します。商品 ID が、使用する ID と完全に一致していることを確認します。
  5. [保存] をクリックします。
  6. [有効にする] をクリックします。
  7. 消耗しない「アップグレード」の購入についても、上記の手順を繰り返します。

次に、サブスクリプションを追加します。

  1. Google Play Console に移動し、アプリケーションを選択します。
  2. [収益化] > [商品] > [サブスクリプション] に移動します。
  3. [サブスクリプションを作成] をクリックします。32a6a9eefdb71dd0.png
  4. 定期購入に必要な情報をすべて入力します。商品 ID が、使用する ID と完全に一致していることを確認します。
  5. [保存] をクリックします。

これで、購入が Google Play Console で設定されます。

6. Firebase を設定する

この Codelab では、バックエンド サービスを使用してユーザーの購入を確認して追跡します。

バックエンド サービスを使用するには、次のようなメリットがあります。

  • 取引を安全に検証できます。
  • アプリストアから課金イベントに応答できます。
  • 購入履歴はデータベースで管理できます。
  • ユーザーがシステム時計を巻き戻して、アプリにプレミアム機能を提供させることができなくなります。

バックエンド サービスを設定する方法はいくつかありますが、ここでは Google 独自の Firebase を使用して、Cloud Functions と Firestore を使用して設定します。

バックエンドの作成は、この Codelab の範囲外と見なされます。そのため、スターター コードには、基本的な購入を処理する Firebase プロジェクトがすでに含まれています。

スターターアプリには Firebase プラグインも含まれています。

残す作業は、独自の Firebase プロジェクトを作成し、Firebase 用にアプリとバックエンドの両方を構成し、最後にバックエンドをデプロイすることです。

Firebase プロジェクトを作成する

Firebase コンソールに移動して、新しい Firebase プロジェクトを作成します。この例では、プロジェクトを Dash Clicker と呼びます。

バックエンド アプリでは、購入を特定のユーザーに関連付けるため、認証が必要です。これには、Google ログインで Firebase の認証モジュールを利用します。

  1. Firebase ダッシュボードで [認証] に移動し、必要に応じて有効にします。
  2. [ログイン方法] タブに移動し、[Google] ログイン プロバイダを有効にします。

7babb48832fbef29.png

Firebase の Firestore データベースも使用するため、こちらも有効にします。

e20553e0de5ac331.png

Cloud Firestore のルールは次のように設定します。

rules_version = '2';
service cloud
.firestore {
  match
/databases/{database}/documents {
    match
/purchases/{purchaseId} {
      allow read
: if request.auth != null && request.auth.uid == resource.data.userId
   
}
 
}
}

Flutter 向け Firebase を設定する

Flutter アプリに Firebase をインストールするおすすめの方法は、FlutterFire CLI を使用することです。設定ページの手順に沿って操作します。

flutterfire configure を実行するときに、前の手順で作成したプロジェクトを選択します。

$ flutterfire configure

i
Found 5 Firebase projects.                                                                                                  
? Select a Firebase project to configure your Flutter application with                                                      
in-app-purchases-1234 (in-app-purchases-1234)                                                                        
  other-flutter-codelab-1 (other-flutter-codelab-1)                                                                          
  other-flutter-codelab-2 (other-flutter-codelab-2)                                                                      
  other-flutter-codelab-3 (other-flutter-codelab-3)                                                                          
  other-flutter-codelab-4 (other-flutter-codelab-4)                                                                                                                                                              
  <create a new project>  

次に、2 つのプラットフォームを選択して、iOSAndroid を有効にします。

? Which platforms should your configuration support (use arrow keys & space to select)?                                      
android                                                                                                                    
ios                                                                                                                        
  macos                                                                                                                      
  web                                                                                                                          

firebase_options.dart のオーバーライドについてプロンプトが表示されたら、[はい] を選択します。

? Generated FirebaseOptions file lib/firebase_options.dart already exists, do you want to override it? (y/n)  yes                                                                                                                         

Android 用 Firebase を設定する: 次のステップ

Firebase ダッシュボードで [プロジェクトの概要] に移動し、[設定] を選択して [全般] タブを選択します。

[アプリ] まで下にスクロールし、[dashclicker(Android)] アプリを選択します。

b22d46a759c0c834.png

デバッグモードで Google ログインを許可するには、デバッグ証明書の SHA-1 ハッシュフィンガープリントを指定する必要があります。

デバッグ用の署名証明書のハッシュを取得する

Flutter アプリ プロジェクトのルートで、ディレクトリを android/ フォルダに変更し、署名レポートを生成します。

cd android
./gradlew :app:signingReport

署名鍵の長いリストが表示されます。デバッグ証明書のハッシュを探しているため、Variant プロパティと Config プロパティが debug に設定されている証明書を探します。キーストアは、通常はホームフォルダの .android/debug.keystore にあります。

> Task :app:signingReport
Variant: debug
Config: debug
Store: /<USER_HOME_FOLDER>/.android/debug.keystore
Alias: AndroidDebugKey
MD5
: XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX
SHA1
: XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX
SHA
-256: XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX
Valid until: Tuesday, January 19, 2038

SHA-1 ハッシュをコピーし、アプリ送信モーダル ダイアログの最後のフィールドに入力します。

iOS 向け Firebase を設定する: その他の手順

Xcode を使用して ios/Runnder.xcworkspace を開きます。または、任意の IDE を使用します。

VSCode で、ios/ フォルダを右クリックし、open in xcode をクリックします。

Android Studio で ios/ フォルダを右クリックし、flutter をクリックして open iOS module in Xcode オプションを選択します。

iOS で Google ログインを許可するには、ビルド plist ファイルに CFBundleURLTypes 構成オプションを追加します。(詳細については、google_sign_in パッケージのドキュメントをご覧ください)。この場合、ファイルは ios/Runner/Info-Debug.plistios/Runner/Info-Release.plist です。

Key-Value ペアはすでに追加されていますが、値を置き換える必要があります。

  1. GoogleService-Info.plist ファイルから REVERSED_CLIENT_ID の値を取得します。この値は <string>..</string> 要素で囲まれません。
  2. ios/Runner/Info-Debug.plist ファイルと ios/Runner/Info-Release.plist ファイルの両方の CFBundleURLTypes キーの値を置き換えます。
<key>CFBundleURLTypes</key>
<array>
   
<dict>
       
<key>CFBundleTypeRole</key>
       
<string>Editor</string>
       
<key>CFBundleURLSchemes</key>
       
<array>
           
<!-- TODO Replace this value: -->
           
<!-- Copied from GoogleService-Info.plist key REVERSED_CLIENT_ID -->
           
<string>com.googleusercontent.apps.REDACTED</string>
       
</array>
   
</dict>
</array>

これで Firebase のセットアップは完了です。

7. 購入に関する最新情報を確認する

この Codelab のパートでは、商品を購入するためのアプリを準備します。このプロセスには、アプリの起動後に購入の更新とエラーをリッスンすることが含まれます。

購入に関する最新情報を確認する

main.dart, で、2 つのページを含む BottomNavigationBar を持つ Scaffold を持つウィジェット MyHomePage を見つけます。このページでは、DashCounterDashUpgrades,DashPurchases の 3 つの Provider も作成します。DashCounter はダッシュの現在の数を追跡し、自動的にインクリメントします。DashUpgrades は、Dash で購入できるアップグレードを管理します。この Codelab では、DashPurchases を中心に説明します。

デフォルトでは、プロバイダのオブジェクトは、そのオブジェクトが初めてリクエストされたときに定義されます。このオブジェクトは、アプリの起動時に購入の更新を直接リッスンするため、lazy: false を使用してこのオブジェクトの遅延読み込みを無効にします。

lib/main.dart

ChangeNotifierProvider<DashPurchases>(
  create
: (context) => DashPurchases(
    context
.read<DashCounter>(),
 
),
  lazy
: false,                                             // Add this line
),

また、InAppPurchaseConnection のインスタンスも必要です。ただし、アプリをテスト可能にするには、接続をモックする方法が必要です。これを行うには、テストでオーバーライドできるインスタンス メソッドを作成し、main.dart に追加します。

lib/main.dart

// Gives the option to override in tests.
class IAPConnection {
  static InAppPurchase? _instance;
  static set instance(InAppPurchase value) {
    _instance = value;
  }

  static InAppPurchase get instance {
    _instance ??= InAppPurchase.instance;
    return _instance!;
  }
}

テストを継続して使用するには、テストを少し更新する必要があります。

test/widget_test.dart

import 'package:dashclicker/main.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:in_app_purchase/in_app_purchase.dart';     // Add this import
import 'package:in_app_purchase_platform_interface/src/in_app_purchase_platform_addition.dart'; // And this import

void main() {
 
testWidgets('App starts', (tester) async {
   
IAPConnection.instance = TestIAPConnection();          // Add this line
   
await tester.pumpWidget(const MyApp());
   
expect(find.text('Tim Sneath'), findsOneWidget);
 
});
}

class TestIAPConnection implements InAppPurchase {         // Add from here
 
@override
 
Future<bool> buyConsumable(
     
{required PurchaseParam purchaseParam, bool autoConsume = true}) {
   
return Future.value(false);
 
}

 
@override
 
Future<bool> buyNonConsumable({required PurchaseParam purchaseParam}) {
   
return Future.value(false);
 
}

 
@override
 
Future<void> completePurchase(PurchaseDetails purchase) {
   
return Future.value();
 
}

 
@override
 
Future<bool> isAvailable() {
   
return Future.value(false);
 
}

 
@override
 
Future<ProductDetailsResponse> queryProductDetails(Set<String> identifiers) {
   
return Future.value(ProductDetailsResponse(
     
productDetails: [],
     
notFoundIDs: [],
   
));
 
}

 
@override
 
T getPlatformAddition<T extends InAppPurchasePlatformAddition?>() {
   
// TODO: implement getPlatformAddition
   
throw UnimplementedError();
 
}

 
@override
 
Stream<List<PurchaseDetails>> get purchaseStream =>
     
Stream.value(<PurchaseDetails>[]);

 
@override
 
Future<void> restorePurchases({String? applicationUserName}) {
   
// TODO: implement restorePurchases
   
throw UnimplementedError();
 
}

 
@override
 
Future<String> countryCode() {
   
// TODO: implement countryCode
   
throw UnimplementedError();
 
}
}                                                          // To here.

lib/logic/dash_purchases.dart で、DashPurchases ChangeNotifier のコードに移動します。現在、購入したダッシュに追加できるのは DashCounter のみです。

ストリーム サブスクリプション プロパティ _subscriptionStreamSubscription<List<PurchaseDetails>> _subscription; 型)、IAPConnection.instance,、インポートを追加します。変更後のコードは次のようになります。

lib/logic/dash_purchases.dart

import 'dart:async';

import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:in_app_purchase/in_app_purchase.dart';     // Add this import

import '../main.dart';                                     // And this import
import '../model/purchasable_product.dart';
import '../model/store_state.dart';
import 'dash_counter.dart';

class DashPurchases extends ChangeNotifier {
 
DashCounter counter;
 
StoreState storeState = StoreState.available;
 
late StreamSubscription<List<PurchaseDetails>> _subscription;  // Add this line
 
List<PurchasableProduct> products = [
   
PurchasableProduct(
     
'Spring is in the air',
     
'Many dashes flying out from their nests',
     
'\$0.99',
   
),
   
PurchasableProduct(
     
'Jet engine',
     
'Doubles you clicks per second for a day',
     
'\$1.99',
   
),
 
];

 
bool get beautifiedDash => false;

 
final iapConnection = IAPConnection.instance;            // And this line

 
DashPurchases(this.counter);

 
Future<void> buy(PurchasableProduct product) async {
   
product.status = ProductStatus.pending;
   
notifyListeners();
   
await Future<void>.delayed(const Duration(seconds: 5));
   
product.status = ProductStatus.purchased;
   
notifyListeners();
   
await Future<void>.delayed(const Duration(seconds: 5));
   
product.status = ProductStatus.purchasable;
   
notifyListeners();
 
}
}

_subscription はコンストラクタで初期化されるため、late キーワードが _subscription に追加されています。このプロジェクトは、デフォルトで null 不可(NNBD)になるように設定されています。つまり、null 可能と宣言されていないプロパティには null 以外の値が必要です。late 修飾子を使用すると、この値の定義を遅らせることができます。

コンストラクタで purchaseUpdated ストリームを取得し、ストリームのリッスンを開始します。dispose() メソッドで、ストリーム サブスクリプションをキャンセルします。

lib/logic/dash_purchases.dart

class DashPurchases extends ChangeNotifier {
  DashCounter counter;
  StoreState storeState = StoreState.notAvailable;         // Modify this line
  late StreamSubscription<List<PurchaseDetails>> _subscription;
  List<PurchasableProduct> products = [
    PurchasableProduct(
      'Spring is in the air',
      'Many dashes flying out from their nests',
      '\$0.99',
    ),
    PurchasableProduct(
      'Jet engine',
      'Doubles you clicks per second for a day',
      '\$1.99',
    ),
  ];

  bool get beautifiedDash => false;

  final iapConnection = IAPConnection.instance;

  DashPurchases(this.counter) {                            // Add from here
    final purchaseUpdated = iapConnection.purchaseStream;
    _subscription = purchaseUpdated.listen(
      _onPurchaseUpdate,
      onDone: _updateStreamOnDone,
      onError: _updateStreamOnError,
    );
  }

  @override
  void dispose() {
    _subscription.cancel();
    super.dispose();
  }                                                        // To here.

  Future<void> buy(PurchasableProduct product) async {
    product.status = ProductStatus.pending;
    notifyListeners();
    await Future<void>.delayed(const Duration(seconds: 5));
    product.status = ProductStatus.purchased;
    notifyListeners();
    await Future<void>.delayed(const Duration(seconds: 5));
    product.status = ProductStatus.purchasable;
    notifyListeners();
  }
                                                           // Add from here
  void _onPurchaseUpdate(List<PurchaseDetails> purchaseDetailsList) {
    // Handle purchases here
  }

  void _updateStreamOnDone() {
    _subscription.cancel();
  }

  void _updateStreamOnError(dynamic error) {
    //Handle error here
  }                                                        // To here.
}

これで、アプリは購入の更新を受け取るようになりました。次のセクションでは、購入を行います。

先に進む前に、flutter test" を使用してテストを実行し、すべてが正しく設定されていることを確認します。

$ flutter test

00:01 +1: All tests passed!                                                                                  

8. 購入を行う

Codelab のこのパートでは、現在存在するモック商品を、実際に購入できる商品に置き換えます。これらの商品はストアから読み込まれ、リストに表示されます。商品をタップすると購入できます。

PurchasableProduct を適応する

PurchasableProduct は、商品のモックを示します。purchasable_product.dartPurchasableProduct クラスを次のコードに置き換えて、実際のコンテンツを表示するように更新します。

lib/model/purchasable_product.dart

import 'package:in_app_purchase/in_app_purchase.dart';

enum ProductStatus {
 
purchasable,
 
purchased,
 
pending,
}

class PurchasableProduct {
 
String get id => productDetails.id;
 
String get title => productDetails.title;
 
String get description => productDetails.description;
 
String get price => productDetails.price;
 
ProductStatus status;
 
ProductDetails productDetails;

 
PurchasableProduct(this.productDetails) : status = ProductStatus.purchasable;
}

dash_purchases.dart, でダミーの購入を削除し、空のリスト List<PurchasableProduct> products = []; に置き換えます。

購入可能なコンテンツを読み込む

ユーザーが購入できるようにするには、ストアから購入を読み込みます。まず、ストアが利用可能かどうかを確認します。ストアが利用できない場合は、storeStatenotAvailable に設定すると、ユーザーにエラー メッセージが表示されます。

lib/logic/dash_purchases.dart

  Future<void> loadPurchases() async {
    final available = await iapConnection.isAvailable();
    if (!available) {
      storeState = StoreState.notAvailable;
      notifyListeners();
      return;
    }
  }

ストアが利用可能になったら、購入可能な商品を読み込みます。以前の Firebase 設定では、storeKeyConsumablestoreKeySubscription,storeKeyUpgrade が表示されます。想定される購入が利用できない場合は、この情報をコンソールに出力します。この情報をバックエンド サービスに送信することもできます。

await iapConnection.queryProductDetails(ids) メソッドは、見つからなかった ID と、見つかった購入可能な商品の両方を返します。レスポンスの productDetails を使用して UI を更新し、StoreStateavailable に設定します。

lib/logic/dash_purchases.dart

import '../constants.dart';

 
Future<void> loadPurchases() async {
   
final available = await iapConnection.isAvailable();
   
if (!available) {
     
storeState = StoreState.notAvailable;
     
notifyListeners();
     
return;
   
}
   
const ids = <String>{
     
storeKeyConsumable,
     
storeKeySubscription,
     
storeKeyUpgrade,
   
};
   
final response = await iapConnection.queryProductDetails(ids);
   
products = response.productDetails.map((e) => PurchasableProduct(e)).toList();
   
storeState = StoreState.available;
   
notifyListeners();
 
}

コンストラクタで loadPurchases() 関数を呼び出します。

lib/logic/dash_purchases.dart

  DashPurchases(this.counter) {
    final purchaseUpdated = iapConnection.purchaseStream;
    _subscription = purchaseUpdated.listen(
      _onPurchaseUpdate,
      onDone: _updateStreamOnDone,
      onError: _updateStreamOnError,
    );
    loadPurchases();
  }

最後に、storeState フィールドの値を StoreState.available から StoreState.loading: に変更します。

lib/logic/dash_purchases.dart

StoreState storeState = StoreState.loading;

購入可能な商品を表示する

purchase_page.dart ファイルを考えてみましょう。PurchasePage ウィジェットには、StoreState に応じて _PurchasesLoading_PurchaseList,、または _PurchasesNotAvailable, が表示されます。ウィジェットには、ユーザーの過去の購入履歴も表示され、次のステップで使用されます。

_PurchaseList ウィジェットは、購入可能な商品のリストを表示し、DashPurchases オブジェクトに購入リクエストを送信します。

lib/pages/purchase_page.dart

class _PurchaseList extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    var purchases = context.watch<DashPurchases>();
    var products = purchases.products;
    return Column(
      children: products
          .map((product) => _PurchaseWidget(
              product: product,
              onPressed: () {
                purchases.buy(product);
              }))
          .toList(),
    );
  }
}

正しく設定されていれば、Android ストアと iOS ストアで利用可能な商品が表示されます。なお、それぞれのコンソールに入力した後、購入できるようになるまでに時間がかかることがあります。

ca1a9f97c21e552d.png

dash_purchases.dart に戻り、商品を購入する関数を実装します。分類する必要があるのは、消耗品と非消耗品のみです。アップグレードと定期購入商品は消耗品ではありません。

lib/logic/dash_purchases.dart

  Future<void> buy(PurchasableProduct product) async {
    final purchaseParam = PurchaseParam(productDetails: product.productDetails);
    switch (product.id) {
      case storeKeyConsumable:
        await iapConnection.buyConsumable(purchaseParam: purchaseParam);
      case storeKeySubscription:
      case storeKeyUpgrade:
        await iapConnection.buyNonConsumable(purchaseParam: purchaseParam);
      default:
        throw ArgumentError.value(
            product.productDetails, '${product.id} is not a known product');
    }
  }

続行する前に、変数 _beautifiedDashUpgrade を作成し、それを参照するように beautifiedDash ゲッターを更新します。

lib/logic/dash_purchases.dart

  bool get beautifiedDash => _beautifiedDashUpgrade;
 
bool _beautifiedDashUpgrade = false;

_onPurchaseUpdate メソッドは購入の更新を受け取り、購入ページに表示される商品のステータスを更新し、購入をカウンタ ロジックに適用します。購入を処理した後、completePurchase を呼び出して、購入が正しく処理されたことをストアに通知することが重要です。

lib/logic/dash_purchases.dart

  Future<void> _onPurchaseUpdate(
      List<PurchaseDetails> purchaseDetailsList) async {
    for (var purchaseDetails in purchaseDetailsList) {
      await _handlePurchase(purchaseDetails);
    }
    notifyListeners();
  }

  Future<void> _handlePurchase(PurchaseDetails purchaseDetails) async {
    if (purchaseDetails.status == PurchaseStatus.purchased) {
      switch (purchaseDetails.productID) {
        case storeKeySubscription:
          counter.applyPaidMultiplier();
        case storeKeyConsumable:
          counter.addBoughtDashes(2000);
        case storeKeyUpgrade:
          _beautifiedDashUpgrade = true;
      }
    }

    if (purchaseDetails.pendingCompletePurchase) {
      await iapConnection.completePurchase(purchaseDetails);
    }
  }

9. バックエンドを設定する

購入の追跡と検証に進む前に、それらをサポートする Dart バックエンドを設定します。

このセクションでは、dart-backend/ フォルダをルートとして使用します。

次のツールがインストールされていることを確認してください。

ベース プロジェクトの概要

このプロジェクトの一部は、この Codelab の範囲外と見なされるため、スターター コードに含まれています。始める前に、スターター コードにすでに含まれているものを確認して、どのように構成するかを把握しておくことをおすすめします。

このバックエンド コードはローカルのマシンで実行できます。使用するためにデプロイする必要はありません。ただし、開発デバイス(Android または iPhone)から、サーバーを実行するマシンに接続できる必要があります。そのためには、マシンが同じネットワークに接続されていること、およびマシンの IP アドレスを把握していることが必要です。

次のコマンドを使用してサーバーを実行してみてください。

$ dart ./bin/server.dart

Serving at http://0.0.0.0:8080

Dart バックエンドは、shelfshelf_router を使用して API エンドポイントを提供します。デフォルトでは、サーバーはルートを提供しません。後で、購入確認プロセスを処理するルートを作成します。

スターター コードにすでに含まれている部分の 1 つが、lib/iap_repository.dartIapRepository です。Firestore や一般的なデータベースの操作方法を学ぶことは、この Codelab の関連事項ではないため、スターター コードには、Firestore で購入を作成または更新するための関数と、それらの購入のすべてのクラスが含まれています。

Firebase のアクセス権を設定する

Firebase Firestore にアクセスするには、サービス アカウントのアクセスキーが必要です。Firebase プロジェクトの設定を開き、[サービス アカウント] セクションに移動して、[新しい秘密鍵の生成] を選択します。

27590fc77ae94ad4.png

ダウンロードした JSON ファイルを assets/ フォルダにコピーし、名前を service-account-firebase.json に変更します。

Google Play へのアクセスを設定する

Play ストアにアクセスして購入を確認するには、これらの権限を持つサービス アカウントを生成し、その JSON 認証情報をダウンロードする必要があります。

  1. Google Play Console に移動し、[すべてのアプリ] ページから始めます。
  2. [設定 > API アクセス] に移動します。317fdfb54921f50e.png Google Play Console でプロジェクトの作成または既存のプロジェクトへのリンクを求められた場合は、まずその手順を完了してから、このページに戻ってください。
  3. サービス アカウントを定義できるセクションを見つけて、[サービス アカウントを新規作成] をクリックします。1e70d3f8d794bebb.png
  4. ポップアップ ダイアログで [Google Cloud Platform] リンクをクリックします。7c9536336dd9e9b4.png
  5. プロジェクトを選択します。表示されない場合は、右上の [アカウント] プルダウン リストで、正しい Google アカウントにログインしていることを確認してください。3fb3a25bad803063.png
  6. プロジェクトを選択したら、上部のメニューバーで [+ サービス アカウントを作成] をクリックします。62fe4c3f8644acd8.png
  7. サービス アカウントの名前を入力し、必要に応じて説明を入力して、用途を覚えておきます。次のステップに進みます。8a92d5d6a3dff48c.png
  8. サービス アカウントに編集者ロールを割り当てます。6052b7753667ed1a.png
  9. ウィザードを完了し、デベロッパー コンソールの [API アクセス] ページに戻り、[サービス アカウントを更新] をクリックします。新しく作成したアカウントがリストに表示されます。5895a7db8b4c7659.png
  10. 新しいサービス アカウントの [アクセスを許可] をクリックします。
  11. 次のページを下にスクロールして、[財務データ] ブロックまで移動します。[売上データ、注文、解約アンケートの回答の閲覧] と [注文と定期購入の管理] の両方を選択します。75b22d0201cf67e.png
  12. [ユーザーを招待] をクリックします。70ea0b1288c62a59.png
  13. アカウントが設定されたので、認証情報を生成するだけです。Cloud コンソールに移動し、サービス アカウントのリストでサービス アカウントを見つけて、縦 3 点アイコンをクリックし、[鍵を管理] を選択します。853ee186b0e9954e.png
  14. 新しい JSON キーを作成してダウンロードします。2a33a55803f5299c.png cb4bf48ebac0364e.png
  15. ダウンロードしたファイルの名前を service-account-google-play.json, に変更し、assets/ ディレクトリに移動します。

最後に、lib/constants.dart, を開き、androidPackageId の値を Android アプリに選択したパッケージ ID に置き換えます。

Apple App Store へのアクセスを設定する

アプリストアにアクセスして購入を確認するには、共有シークレットを設定する必要があります。

  1. App Store Connect を開きます。
  2. [マイアプリ] に移動し、アプリを選択します。
  3. サイドバー ナビゲーションで、[アプリ内購入] > [管理] に移動します。
  4. リストの右上にある [アプリ固有の共有シークレット] をクリックします。
  5. 新しいシークレットを生成してコピーします。
  6. lib/constants.dart, を開き、appStoreSharedSecret の値を先ほど生成した共有シークレットに置き換えます。

d8b8042470aaeff.png

b72f4565750e2f40.png

定数の構成ファイル

続行する前に、lib/constants.dart ファイルで次の定数が構成されていることを確認してください。

  • androidPackageId: Android で使用されるパッケージ ID(例: com.example.dashclicker
  • appStoreSharedSecret: App Store Connect にアクセスして購入確認を行うための共有シークレット。
  • bundleId: iOS で使用されるバンドル ID(例: com.example.dashclicker

残りの定数は当面は無視してかまいません。

10. 購入を確認する

購入の確認の一般的な流れは、iOS と Android で同様です。

どちらのストアでも、購入が行われるとアプリにトークンが届きます。

このトークンはアプリからバックエンド サービスに送信され、バックエンド サービスは提供されたトークンを使用して、それぞれのストアのサーバーで購入を確認します。

バックエンド サービスは、購入を保存し、購入が有効かどうかをアプリに返信できます。

ユーザーのデバイスで実行されているアプリではなく、バックエンド サービスがストアとの検証を行うようにすることで、システム時計を巻き戻すなどして、ユーザーがプレミアム機能にアクセスできないようにできます。

Flutter 側をセットアップする

認証の設定

購入をバックエンド サービスに送信するため、購入中にユーザーが認証されていることを確認する必要があります。認証ロジックのほとんどは、スターター プロジェクトにすでに追加されています。ユーザーがまだログインしていないときに PurchasePage にログインボタンが表示されるようにするだけです。PurchasePage の build メソッドの先頭に次のコードを追加します。

lib/pages/purchase_page.dart

import '../logic/firebase_notifier.dart';
import '../model/firebase_state.dart';
import 'login_page.dart';

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

 
@override
 
Widget build(BuildContext context) {
   
var firebaseNotifier = context.watch<FirebaseNotifier>();
   
if (firebaseNotifier.state == FirebaseState.loading) {
     
return _PurchasesLoading();
   
} else if (firebaseNotifier.state == FirebaseState.notAvailable) {
     
return _PurchasesNotAvailable();
   
}

   
if (!firebaseNotifier.loggedIn) {
     
return const LoginPage();
   
}
   
// omitted

アプリから確認エンドポイントを呼び出す

アプリで、http ポスト呼び出しを使用して Dart バックエンドの /verifypurchase エンドポイントを呼び出す _verifyPurchase(PurchaseDetails purchaseDetails) 関数を作成します。

選択したストア(Google Play ストアの場合は google_play、App Store の場合は app_store)、serverVerificationDataproductID を送信します。サーバーは、購入が検証されたかどうかを示すステータス コードを返します。

アプリ定数で、サーバーの IP をローカルマシンの IP アドレスに構成します。

lib/logic/dash_purchases.dart

  FirebaseNotifier firebaseNotifier;

 
DashPurchases(this.counter, this.firebaseNotifier) {
   
// omitted
 
}

main.dart:DashPurchases を作成して firebaseNotifier を追加

lib/main.dart

        ChangeNotifierProvider<DashPurchases>(
          create
: (context) => DashPurchases(
            context
.read<DashCounter>(),
            context
.read<FirebaseNotifier>(),
         
),
          lazy
: false,
       
),

FirebaseNotifier にユーザーのゲッターを追加して、ユーザー ID を購入確認関数に渡せるようにします。

lib/logic/firebase_notifier.dart

  User? get user => FirebaseAuth.instance.currentUser;

_verifyPurchase 関数を DashPurchases クラスに追加します。この async 関数は、購入が検証されたかどうかを示すブール値を返します。

lib/logic/dash_purchases.dart

  Future<bool> _verifyPurchase(PurchaseDetails purchaseDetails) async {
    final url = Uri.parse('http://$serverIp:8080/verifypurchase');
    const headers = {
      'Content-type': 'application/json',
      'Accept': 'application/json',
    };
    final response = await http.post(
      url,
      body: jsonEncode({
        'source': purchaseDetails.verificationData.source,
        'productId': purchaseDetails.productID,
        'verificationData':
            purchaseDetails.verificationData.serverVerificationData,
        'userId': firebaseNotifier.user?.uid,
      }),
      headers: headers,
    );
    if (response.statusCode == 200) {
      return true;
    } else {
      return false;
    }
  }

購入を適用する直前に、_handlePurchase_verifyPurchase 関数を呼び出します。購入は、確認が取れた場合にのみ適用してください。製品版アプリでは、ストアが一時的に利用できない場合に試用版サブスクリプションを適用するなど、さらに指定できます。ただし、この例ではシンプルにするため、購入が正常に確認された場合にのみ購入を適用します。

lib/logic/dash_purchases.dart

  Future<void> _onPurchaseUpdate(
      List<PurchaseDetails> purchaseDetailsList) async {
    for (var purchaseDetails in purchaseDetailsList) {
      await _handlePurchase(purchaseDetails);
    }
    notifyListeners();
  }

  Future<void> _handlePurchase(PurchaseDetails purchaseDetails) async {
    if (purchaseDetails.status == PurchaseStatus.purchased) {
      // Send to server
      var validPurchase = await _verifyPurchase(purchaseDetails);

      if (validPurchase) {
        // Apply changes locally
        switch (purchaseDetails.productID) {
          case storeKeySubscription:
            counter.applyPaidMultiplier();
          case storeKeyConsumable:
            counter.addBoughtDashes(1000);
        }
      }
    }

    if (purchaseDetails.pendingCompletePurchase) {
      await iapConnection.completePurchase(purchaseDetails);
    }
  }

これで、アプリで購入の検証を行う準備が整いました。

バックエンド サービスを設定する

次に、バックエンドで購入を確認する Cloud Functions 関数を設定します。

購入ハンドラを作成する

両方の店舗の確認フローはほぼ同じであるため、抽象的な PurchaseHandler クラスを設定し、店舗ごとに個別の実装を用意します。

be50c207c5a2a519.png

まず、lib/ フォルダに purchase_handler.dart ファイルを追加します。ここで、サブスクリプションと非サブスクリプションの 2 種類の購入を確認する 2 つの抽象メソッドを持つ抽象 PurchaseHandler クラスを定義します。

lib/purchase_handler.dart

import 'products.dart';

/// Generic purchase handler,
/// must be implemented for Google Play and Apple Store
abstract class PurchaseHandler {

 
/// Verify if non-subscription purchase (aka consumable) is valid
 
/// and update the database
 
Future<bool> handleNonSubscription({
   
required String userId,
   
required ProductData productData,
   
required String token,
 
});

 
/// Verify if subscription purchase (aka non-consumable) is valid
 
/// and update the database
 
Future<bool> handleSubscription({
   
required String userId,
   
required ProductData productData,
   
required String token,
 
});
}

ご覧のとおり、各メソッドには次の 3 つのパラメータが必要です。

  • userId: ログイン中のユーザーの ID。購入をユーザーに関連付けることができます。
  • productData: 商品に関するデータ。この定義については、後ほど説明します。
  • token: ストアからユーザーに提供されたトークン。

また、これらの購入ハンドラを使いやすくするために、サブスクリプションとサブスクリプション以外の両方で使用できる verifyPurchase() メソッドを追加します。

lib/purchase_handler.dart

  /// Verify if purchase is valid and update the database
  Future<bool> verifyPurchase({
    required String userId,
    required ProductData productData,
    required String token,
  }) async {
    switch (productData.type) {
      case ProductType.subscription:
        return handleSubscription(
          userId: userId,
          productData: productData,
          token: token,
        );
      case ProductType.nonSubscription:
        return handleNonSubscription(
          userId: userId,
          productData: productData,
          token: token,
        );
    }
  }

これで、両方のケースで verifyPurchase を呼び出すだけで、別々の実装を維持できます。

ProductData クラスには、購入可能なさまざまな商品に関する基本情報が含まれています。これには、商品 ID(SKU とも呼ばれる)と ProductType が含まれます。

lib/products.dart

class ProductData {
  final String productId;
  final ProductType type;

  const ProductData(this.productId, this.type);
}

ProductType は、定期購入または定期購入以外のいずれかです。

lib/products.dart

enum ProductType {
  subscription,
  nonSubscription,
}

最後に、商品のリストが同じファイル内のマップとして定義されます。

lib/products.dart

const productDataMap = {
  'dash_consumable_2k': ProductData(
    'dash_consumable_2k',
    ProductType.nonSubscription,
  ),
  'dash_upgrade_3d': ProductData(
    'dash_upgrade_3d',
    ProductType.nonSubscription,
  ),
  'dash_subscription_doubler': ProductData(
    'dash_subscription_doubler',
    ProductType.subscription,
  ),
};

次に、Google Play ストアと Apple App Store のプレースホルダ実装を定義します。Google Play から始めましょう。

lib/google_play_purchase_handler.dart を作成し、作成した PurchaseHandler を拡張するクラスを追加します。

lib/google_play_purchase_handler.dart

import 'dart:async';

import 'package:googleapis/androidpublisher/v3.dart' as ap;

import 'constants.dart';
import 'iap_repository.dart';
import 'products.dart';
import 'purchase_handler.dart';

class GooglePlayPurchaseHandler extends PurchaseHandler {
 
final ap.AndroidPublisherApi androidPublisher;
 
final IapRepository iapRepository;

 
GooglePlayPurchaseHandler(
   
this.androidPublisher,
   
this.iapRepository,
 
);

 
@override
 
Future<bool> handleNonSubscription({
   
required String? userId,
   
required ProductData productData,
   
required String token,
 
}) async {
   
return true;
 
}

 
@override
 
Future<bool> handleSubscription({
   
required String? userId,
   
required ProductData productData,
   
required String token,
 
}) async {
   
return true;
 
}
}

現時点では、ハンドラ メソッドに対して true を返します。ハンドラ メソッドについては後で説明します。

ご覧のとおり、コンストラクタは IapRepository のインスタンスを受け取ります。購入ハンドラは、このインスタンスを使用して、後で Firestore に購入に関する情報を保存します。Google Play と通信するには、提供された AndroidPublisherApi を使用します。

次に、アプリストア ハンドラでも同じ手順を行います。lib/app_store_purchase_handler.dart を作成し、PurchaseHandler を拡張するクラスをもう一度追加します。

lib/app_store_purchase_handler.dart

import 'dart:async';

import 'package:app_store_server_sdk/app_store_server_sdk.dart';

import 'constants.dart';
import 'iap_repository.dart';
import 'products.dart';
import 'purchase_handler.dart';

class AppStorePurchaseHandler extends PurchaseHandler {
 
final IapRepository iapRepository;

 
AppStorePurchaseHandler(
   
this.iapRepository,
 
);

 
@override
 
Future<bool> handleNonSubscription({
   
required String userId,
   
required ProductData productData,
   
required String token,
 
}) {
   
return true;
 
}

 
@override
 
Future<bool> handleSubscription({
   
required String userId,
   
required ProductData productData,
   
required String token,
 
}) {
   
return true;
 
}
}

これで、これで、2 つの購入ハンドラが作成されました。次に、購入確認 API エンドポイントを作成します。

購入ハンドラを使用する

bin/server.dart を開き、shelf_route を使用して API エンドポイントを作成します。

bin/server.dart

Future<void> main() async {
  final router = Router();

  final purchaseHandlers = await _createPurchaseHandlers();

  router.post('/verifypurchase', (Request request) async {
    final dynamic payload = json.decode(await request.readAsString());

    final (:userId, :source, :productData, :token) = getPurchaseData(payload);

    final result = await purchaseHandlers[source]!.verifyPurchase(
      userId: userId,
      productData: productData,
      token: token,
    );

    if (result) {
      return Response.ok('all good!');
    } else {
      return Response.internalServerError();
    }
  });

  await serveHandler(router.call);
}

({
  String userId,
  String source,
  ProductData productData,
  String token,
}) getPurchaseData(dynamic payload) {
  if (payload
      case {
        'userId': String userId,
        'source': String source,
        'productId': String productId,
        'verificationData': String token,
      }) {
    return (
      userId: userId,
      source: source,
      productData: productDataMap[productId]!,
      token: token,
    );
  } else {
    throw const FormatException('Unexpected JSON');
  }
}

上記のコードは次の処理を行います。

  1. 前に作成したアプリから呼び出される POST エンドポイントを定義します。
  2. JSON ペイロードをデコードし、次の情報を抽出します。
  3. userId: 現在ログインしているユーザー ID
  4. source: 使用するストア(app_store または google_play)。
  5. productData: 前に作成した productDataMap から取得します。
  6. token: ストアに送信する検証データが含まれます。
  7. ソースに応じて、GooglePlayPurchaseHandler または AppStorePurchaseHandlerverifyPurchase メソッドを呼び出します。
  8. 検証が成功すると、このメソッドは Response.ok をクライアントに返します。
  9. 検証に失敗した場合、メソッドはクライアントに Response.internalServerError を返します。

API エンドポイントを作成したら、2 つの購入ハンドラを構成する必要があります。これを行うには、前の手順で取得したサービス アカウント キーを読み込み、Android Publisher API や Firebase Firestore API など、さまざまなサービスへのアクセスを構成する必要があります。次に、異なる依存関係を持つ 2 つの購入ハンドラを作成します。

bin/server.dart

Future<Map<String, PurchaseHandler>> _createPurchaseHandlers() async {
 
// Configure Android Publisher API access
 
final serviceAccountGooglePlay =
     
File('assets/service-account-google-play.json').readAsStringSync();
 
final clientCredentialsGooglePlay =
      auth
.ServiceAccountCredentials.fromJson(serviceAccountGooglePlay);
 
final clientGooglePlay =
      await auth
.clientViaServiceAccount(clientCredentialsGooglePlay, [
    ap
.AndroidPublisherApi.androidpublisherScope,
 
]);
 
final androidPublisher = ap.AndroidPublisherApi(clientGooglePlay);

 
// Configure Firestore API access
 
final serviceAccountFirebase =
     
File('assets/service-account-firebase.json').readAsStringSync();
 
final clientCredentialsFirebase =
      auth
.ServiceAccountCredentials.fromJson(serviceAccountFirebase);
 
final clientFirebase =
      await auth
.clientViaServiceAccount(clientCredentialsFirebase, [
    fs
.FirestoreApi.cloudPlatformScope,
 
]);
 
final firestoreApi = fs.FirestoreApi(clientFirebase);
 
final dynamic json = jsonDecode(serviceAccountFirebase);
 
final projectId = json['project_id'] as String;
 
final iapRepository = IapRepository(firestoreApi, projectId);

 
return {
   
'google_play': GooglePlayPurchaseHandler(
      androidPublisher
,
      iapRepository
,
   
),
   
'app_store': AppStorePurchaseHandler(
      iapRepository
,
   
),
 
};
}

Android での購入を確認する: 購入ハンドラを実装する

次に、Google Play 購入ハンドラの実装を続けます。

Google は、購入の確認に必要な API を操作するための Dart パッケージをすでに提供しています。これらは server.dart ファイルで初期化され、GooglePlayPurchaseHandler クラスで使用されます。

定期購入以外の購入のハンドラを実装します。

lib/google_play_purchase_handler.dart

  @override
  Future<bool> handleNonSubscription({
    required String? userId,
    required ProductData productData,
    required String token,
  }) async {
    print(
      'GooglePlayPurchaseHandler.handleNonSubscription'
      '($userId, ${productData.productId}, ${token.substring(0, 5)}...)',
    );

    try {
      // Verify purchase with Google
      final response = await androidPublisher.purchases.products.get(
        androidPackageId,
        productData.productId,
        token,
      );

      print('Purchases response: ${response.toJson()}');

      // Make sure an order id exists
      if (response.orderId == null) {
        print('Could not handle purchase without order id');
        return false;
      }
      final orderId = response.orderId!;

      final purchaseData = NonSubscriptionPurchase(
        purchaseDate: DateTime.fromMillisecondsSinceEpoch(
          int.parse(response.purchaseTimeMillis ?? '0'),
        ),
        orderId: orderId,
        productId: productData.productId,
        status: _nonSubscriptionStatusFrom(response.purchaseState),
        userId: userId,
        iapSource: IAPSource.googleplay,
      );

      // Update the database
      if (userId != null) {
        // If we know the userId,
        // update the existing purchase or create it if it does not exist.
        await iapRepository.createOrUpdatePurchase(purchaseData);
      } else {
        // If we do not know the user id, a previous entry must already
        // exist, and thus we'll only update it.
        await iapRepository.updatePurchase(purchaseData);
      }
      return true;
    } on ap.DetailedApiRequestError catch (e) {
      print(
        'Error on handle NonSubscription: $e\n'
        'JSON: ${e.jsonResponse}',
      );
    } catch (e) {
      print('Error on handle NonSubscription: $e\n');
    }
    return false;
  }

サブスクリプション購入ハンドラも同様の方法で更新できます。

lib/google_play_purchase_handler.dart

  /// Handle subscription purchases.
  ///
  /// Retrieves the purchase status from Google Play and updates
  /// the Firestore Database accordingly.
  @override
  Future<bool> handleSubscription({
    required String? userId,
    required ProductData productData,
    required String token,
  }) async {
    print(
      'GooglePlayPurchaseHandler.handleSubscription'
      '($userId, ${productData.productId}, ${token.substring(0, 5)}...)',
    );

    try {
      // Verify purchase with Google
      final response = await androidPublisher.purchases.subscriptions.get(
        androidPackageId,
        productData.productId,
        token,
      );

      print('Subscription response: ${response.toJson()}');

      // Make sure an order id exists
      if (response.orderId == null) {
        print('Could not handle purchase without order id');
        return false;
      }
      final orderId = extractOrderId(response.orderId!);

      final purchaseData = SubscriptionPurchase(
        purchaseDate: DateTime.fromMillisecondsSinceEpoch(
          int.parse(response.startTimeMillis ?? '0'),
        ),
        orderId: orderId,
        productId: productData.productId,
        status: _subscriptionStatusFrom(response.paymentState),
        userId: userId,
        iapSource: IAPSource.googleplay,
        expiryDate: DateTime.fromMillisecondsSinceEpoch(
          int.parse(response.expiryTimeMillis ?? '0'),
        ),
      );

      // Update the database
      if (userId != null) {
        // If we know the userId,
        // update the existing purchase or create it if it does not exist.
        await iapRepository.createOrUpdatePurchase(purchaseData);
      } else {
        // If we do not know the user id, a previous entry must already
        // exist, and thus we'll only update it.
        await iapRepository.updatePurchase(purchaseData);
      }
      return true;
    } on ap.DetailedApiRequestError catch (e) {
      print(
        'Error on handle Subscription: $e\n'
        'JSON: ${e.jsonResponse}',
      );
    } catch (e) {
      print('Error on handle Subscription: $e\n');
    }
    return false;
  }
}

注文 ID の解析を容易にする次のメソッドと、購入ステータスを解析する 2 つのメソッドを追加します。

lib/google_play_purchase_handler.dart

NonSubscriptionStatus _nonSubscriptionStatusFrom(int? state) {
  return switch (state) {
    0 => NonSubscriptionStatus.completed,
    2 => NonSubscriptionStatus.pending,
    _ => NonSubscriptionStatus.cancelled,
 
};
}

SubscriptionStatus _subscriptionStatusFrom(int? state) {
  return switch (state) {
    // Payment pending
    0 => SubscriptionStatus.pending,
    // Payment received
    1 => SubscriptionStatus.active,
    // Free trial
    2 => SubscriptionStatus.active,
    // Pending deferred upgrade/downgrade
    3 => SubscriptionStatus.pending,
    // Expired or cancelled
    _ => SubscriptionStatus.expired,
 
};
}

/// If a subscription suffix is present (..#) extract the orderId.
String extractOrderId(String orderId) {
 
final orderIdSplit = orderId.split('..');
 
if (orderIdSplit.isNotEmpty) {
    orderId
= orderIdSplit[0];
 
}
 
return orderId;
}

Google Play での購入が検証され、データベースに保存されます。

次に、iOS の App Store での購入に移ります。

iOS での購入を確認する: 購入ハンドラを実装する

App Store での購入の確認には、app_store_server_sdk というサードパーティ製の Dart パッケージがあり、このパッケージを使用するとプロセスを簡単に行うことができます。

まず、ITunesApi インスタンスを作成します。サンドボックス構成を使用し、ロギングを有効にしてエラーのデバッグを容易にします。

lib/app_store_purchase_handler.dart

  final _iTunesAPI = ITunesApi(
    ITunesHttpClient(
      ITunesEnvironment.sandbox(),
      loggingEnabled: true,
    ),
  );

Google Play API とは異なり、App Store では定期購入と定期購入以外の両方で同じ API エンドポイントが使用されます。つまり、両方のハンドラに同じロジックを使用できます。これらを統合して、同じ実装を呼び出すようにします。

lib/app_store_purchase_handler.dart

  @override
  Future<bool> handleNonSubscription({
    required String userId,
    required ProductData productData,
    required String token,
  }) {
    return handleValidation(userId: userId, token: token);
  }

  @override
  Future<bool> handleSubscription({
    required String userId,
    required ProductData productData,
    required String token,
  }) {
    return handleValidation(userId: userId, token: token);
  }

  /// Handle purchase validation.
  Future<bool> handleValidation({
    required String userId,
    required String token,
  }) async {
   //..
  }

次に、handleValidation を実装します。

lib/app_store_purchase_handler.dart

  /// Handle purchase validation.
  Future<bool> handleValidation({
    required String userId,
    required String token,
  }) async {
    print('AppStorePurchaseHandler.handleValidation');
    final response = await _iTunesAPI.verifyReceipt(
      password: appStoreSharedSecret,
      receiptData: token,
    );
    print('response: $response');
    if (response.status == 0) {
      final receipts = response.latestReceiptInfo ?? [];
      for (final receipt in receipts) {
        final product = productDataMap[receipt.productId];
        if (product == null) {
          print('Error: Unknown product: ${receipt.productId}');
          continue;
        }
        switch (product.type) {
          case ProductType.nonSubscription:
            await iapRepository.createOrUpdatePurchase(NonSubscriptionPurchase(
              userId: userId,
              productId: receipt.productId ?? '',
              iapSource: IAPSource.appstore,
              orderId: receipt.originalTransactionId ?? '',
              purchaseDate: DateTime.fromMillisecondsSinceEpoch(
                  int.parse(receipt.originalPurchaseDateMs ?? '0')),
              type: product.type,
              status: NonSubscriptionStatus.completed,
            ));
            break;
          case ProductType.subscription:
            await iapRepository.createOrUpdatePurchase(SubscriptionPurchase(
              userId: userId,
              productId: receipt.productId ?? '',
              iapSource: IAPSource.appstore,
              orderId: receipt.originalTransactionId ?? '',
              purchaseDate: DateTime.fromMillisecondsSinceEpoch(
                  int.parse(receipt.originalPurchaseDateMs ?? '0')),
              type: product.type,
              expiryDate: DateTime.fromMillisecondsSinceEpoch(
                  int.parse(receipt.expiresDateMs ?? '0')),
              status: SubscriptionStatus.active,
            ));
            break;
        }
      }
      return true;
    } else {
      print('Error: Status: ${response.status}');
      return false;
    }
  }

これで、App Store での購入が検証され、データベースに保存されます。

バックエンドを実行する

この時点で、dart bin/server.dart を実行して /verifypurchase エンドポイントを提供できます。

$ dart bin/server.dart 
Serving at http://0.0.0.0:8080

11. 購入の管理

ユーザーの購入をトラッキングするには、バックエンド サービスを使用することをおすすめします。これは、バックエンドがストアのイベントに応答できるため、キャッシュに起因する古い情報に遭遇する可能性が低く、改ざんされにくいためです。

まず、構築した Dart バックエンドを使用して、バックエンドでのストアイベントの処理を設定します。

バックエンドで店舗イベントを処理する

ストアは、定期購入の更新時など、発生した課金イベントをバックエンドに通知できます。これらのイベントをバックエンドで処理して、データベース内の購入を最新の状態に保つことができます。このセクションでは、Google Play ストアと Apple App Store の両方で設定を行います。

Google Play の課金イベントを処理する

Google Play は、Cloud Pub/Sub トピックと呼ばれるものを通じて課金イベントを提供します。これらは基本的に、メッセージのパブリッシュと使用が可能なメッセージ キューです。

これは Google Play に固有の機能であるため、この機能を GooglePlayPurchaseHandler に含めます。

まず、lib/google_play_purchase_handler.dart を開き、PubsubApi のインポートを追加します。

lib/google_play_purchase_handler.dart

import 'package:googleapis/pubsub/v1.dart' as pubsub;

次に、PubsubApiGooglePlayPurchaseHandler に渡し、クラスのコンストラクタを変更して Timer を作成します。

lib/google_play_purchase_handler.dart

class GooglePlayPurchaseHandler extends PurchaseHandler {
  final ap.AndroidPublisherApi androidPublisher;
  final IapRepository iapRepository;
  final pubsub.PubsubApi pubsubApi; // new

  GooglePlayPurchaseHandler(
    this.androidPublisher,
    this.iapRepository,
    this.pubsubApi, // new
  ) {
    // Poll messages from Pub/Sub every 10 seconds
    Timer.periodic(Duration(seconds: 10), (_) {
      _pullMessageFromPubSub();
    });
  }

Timer は、_pullMessageFromSubSub メソッドを 10 秒ごとに呼び出すように構成されています。時間はご希望に応じて調整できます。

次に、_pullMessageFromSubSub を作成します。

lib/google_play_purchase_handler.dart

  /// Process messages from Google Play
  /// Called every 10 seconds
  Future<void> _pullMessageFromPubSub() async {
    print('Polling Google Play messages');
    final request = pubsub.PullRequest(
      maxMessages: 1000,
    );
    final topicName =
        'projects/$googlePlayProjectName/subscriptions/$googlePlayPubsubBillingTopic-sub';
    final pullResponse = await pubsubApi.projects.subscriptions.pull(
      request,
      topicName,
    );
    final messages = pullResponse.receivedMessages ?? [];
    for (final message in messages) {
      final data64 = message.message?.data;
      if (data64 != null) {
        await _processMessage(data64, message.ackId);
      }
    }
  }

  Future<void> _processMessage(String data64, String? ackId) async {
    final dataRaw = utf8.decode(base64Decode(data64));
    print('Received data: $dataRaw');
    final dynamic data = jsonDecode(dataRaw);
    if (data['testNotification'] != null) {
      print('Skip test messages');
      if (ackId != null) {
        await _ackMessage(ackId);
      }
      return;
    }
    final dynamic subscriptionNotification = data['subscriptionNotification'];
    final dynamic oneTimeProductNotification =
        data['oneTimeProductNotification'];
    if (subscriptionNotification != null) {
      print('Processing Subscription');
      final subscriptionId =
          subscriptionNotification['subscriptionId'] as String;
      final purchaseToken = subscriptionNotification['purchaseToken'] as String;
      final productData = productDataMap[subscriptionId]!;
      final result = await handleSubscription(
        userId: null,
        productData: productData,
        token: purchaseToken,
      );
      if (result && ackId != null) {
        await _ackMessage(ackId);
      }
    } else if (oneTimeProductNotification != null) {
      print('Processing NonSubscription');
      final sku = oneTimeProductNotification['sku'] as String;
      final purchaseToken =
          oneTimeProductNotification['purchaseToken'] as String;
      final productData = productDataMap[sku]!;
      final result = await handleNonSubscription(
        userId: null,
        productData: productData,
        token: purchaseToken,
      );
      if (result && ackId != null) {
        await _ackMessage(ackId);
      }
    } else {
      print('invalid data');
    }
  }

  /// ACK Messages from Pub/Sub
  Future<void> _ackMessage(String id) async {
    print('ACK Message');
    final request = pubsub.AcknowledgeRequest(
      ackIds: [id],
    );
    final subscriptionName =
        'projects/$googlePlayProjectName/subscriptions/$googlePlayPubsubBillingTopic-sub';
    await pubsubApi.projects.subscriptions.acknowledge(
      request,
      subscriptionName,
    );
  }

追加したコードは、Google Cloud の Pub/Sub トピックと 10 秒ごとに通信し、新しいメッセージをリクエストします。次に、_processMessage メソッドで各メッセージを処理します。

このメソッドは、受信したメッセージをデコードし、各購入(定期購入と定期購入以外の両方)に関する最新情報を取得します。必要に応じて、既存の handleSubscription または handleNonSubscription を呼び出します。

各メッセージは _askMessage メソッドで確認応答する必要があります。

次に、必要な依存関係を server.dart ファイルに追加します。PubsubApi.cloudPlatformScope を認証情報の構成に追加します。

bin/server.dart

 final clientGooglePlay =
      await auth
.clientViaServiceAccount(clientCredentialsGooglePlay, [
    ap
.AndroidPublisherApi.androidpublisherScope,
    pubsub
.PubsubApi.cloudPlatformScope, // new
 
]);

次に、PubsubApi インスタンスを作成します。

bin/server.dart

  final pubsubApi = pubsub.PubsubApi(clientGooglePlay);

最後に、それを GooglePlayPurchaseHandler コンストラクタに渡します。

bin/server.dart

  return {
   
'google_play': GooglePlayPurchaseHandler(
      androidPublisher
,
      iapRepository
,
      pubsubApi
, // new
   
),
   
'app_store': AppStorePurchaseHandler(
      iapRepository
,
   
),
 
};

Google Play のセットアップ

Pub/Sub トピックから課金イベントを使用するコードは作成しましたが、Pub/Sub トピックを作成しておらず、課金イベントもパブリッシュしていません。設定を開始しましょう。

まず、Pub/Sub トピックを作成します。

  1. Google Cloud コンソールの Cloud Pub/Sub ページに移動します。
  2. Firebase プロジェクトに移動し、[+ トピックを作成] をクリックします。d5ebf6897a0a8bf5.png
  3. 新しいトピックに名前を付けます。この名前は、constants.tsGOOGLE_PLAY_PUBSUB_BILLING_TOPIC に設定した値と同じにします。この例では、play_billing という名前を付けます。他のものを選択した場合は、constants.ts を必ず更新してください。トピックを作成します。20d690fc543c4212.png
  4. Pub/Sub トピックのリストで、作成したトピックのその他アイコンをクリックし、[権限を表示] をクリックします。ea03308190609fb.png
  5. 右側のサイドバーで [プリンシパルを追加] を選択します。
  6. ここに google-play-developer-notifications@system.gserviceaccount.com を追加し、Pub/Sub パブリッシャーのロールを付与します。55631ec0549215bc.png
  7. 権限の変更を保存します。
  8. 作成したトピックのトピック名をコピーします。
  9. Google Play Console をもう一度開き、[すべてのアプリ] リストからアプリを選択します。
  10. 下にスクロールして、[収益化] > [収益化のセットアップ] に移動します。
  11. トピック全体を入力し、変更を保存します。7e5e875dc6ce5d54.png

すべての Google Play 請求イベントがこのトピックに公開されるようになりました。

App Store の課金イベントを処理する

次に、App Store の請求イベントについても同じ手順を行います。App Store での購入の更新処理を実装する方法は 2 つあります。1 つは、Apple に提供する Webhook を実装し、Apple がサーバーとの通信に使用する方法です。2 つ目の方法(この Codelab で説明する方法)は、App Store Server API に接続して定期購入情報を手動で取得する方法です。

この Codelab では 2 番目のソリューションに焦点を当てています。これは、Webhook を実装するにはサーバーをインターネットに公開する必要があるためです。

本番環境では、両方を使用することをおすすめします。App Store からイベントを取得するための Webhook と、イベントを逃したときや定期購入のステータスを再確認する必要がある場合に使用する Server API。

まず、lib/app_store_purchase_handler.dart を開いて、AppStoreServerAPI の依存関係を追加します。

lib/app_store_purchase_handler.dart

final AppStoreServerAPI appStoreServerAPI;

AppStorePurchaseHandler(
 
this.iapRepository,
 
this.appStoreServerAPI, // new
)

コンストラクタを変更して、_pullStatus メソッドを呼び出すタイマーを追加します。このタイマーは、10 秒ごとに _pullStatus メソッドを呼び出します。このタイマーの長さは必要に応じて調整できます。

lib/app_store_purchase_handler.dart

  AppStorePurchaseHandler(
    this.iapRepository,
    this.appStoreServerAPI,
  ) {
    // Poll Subscription status every 10 seconds.
    Timer.periodic(Duration(seconds: 10), (_) {
      _pullStatus();
    });
  }

次に、_pullStatus メソッドを次のように作成します。

lib/app_store_purchase_handler.dart

  Future<void> _pullStatus() async {
    print('Polling App Store');
    final purchases = await iapRepository.getPurchases();
    // filter for App Store subscriptions
    final appStoreSubscriptions = purchases.where((element) =>
        element.type == ProductType.subscription &&
        element.iapSource == IAPSource.appstore);
    for (final purchase in appStoreSubscriptions) {
      final status =
          await appStoreServerAPI.getAllSubscriptionStatuses(purchase.orderId);
      // Obtain all subscriptions for the order id.
      for (final subscription in status.data) {
        // Last transaction contains the subscription status.
        for (final transaction in subscription.lastTransactions) {
          final expirationDate = DateTime.fromMillisecondsSinceEpoch(
              transaction.transactionInfo.expiresDate ?? 0);
          // Check if subscription has expired.
          final isExpired = expirationDate.isBefore(DateTime.now());
          print('Expiration Date: $expirationDate - isExpired: $isExpired');
          // Update the subscription status with the new expiration date and status.
          await iapRepository.updatePurchase(SubscriptionPurchase(
            userId: null,
            productId: transaction.transactionInfo.productId,
            iapSource: IAPSource.appstore,
            orderId: transaction.originalTransactionId,
            purchaseDate: DateTime.fromMillisecondsSinceEpoch(
                transaction.transactionInfo.originalPurchaseDate),
            type: ProductType.subscription,
            expiryDate: expirationDate,
            status: isExpired
                ? SubscriptionStatus.expired
                : SubscriptionStatus.active,
          ));
        }
      }
    }
  }

この方法は次のように機能します。

  1. IapRepository を使用して、アクティブな定期購入のリストを Firestore から取得します。
  2. 注文ごとに、App Store Server API に定期購入のステータスをリクエストします。
  3. その定期購入の最後の取引を取得します。
  4. 有効期限を確認します。
  5. Firestore でサブスクリプションのステータスを更新します。期限切れの場合は、期限切れとしてマークされます。

最後に、App Store Server API アクセスを構成するために必要なコードをすべて追加します。

bin/server.dart

  // add from here
  final subscriptionKeyAppStore =
      File('assets/SubscriptionKey.p8').readAsStringSync();

  // Configure Apple Store API access
  var appStoreEnvironment = AppStoreEnvironment.sandbox(
    bundleId: bundleId,
    issuerId: appStoreIssuerId,
    keyId: appStoreKeyId,
    privateKey: subscriptionKeyAppStore,
  );

  // Stored token for Apple Store API access, if available
  final file = File('assets/appstore.token');
  String? appStoreToken;
  if (file.existsSync() && file.lengthSync() > 0) {
    appStoreToken = file.readAsStringSync();
  }

  final appStoreServerAPI = AppStoreServerAPI(
    AppStoreServerHttpClient(
      appStoreEnvironment,
      jwt: appStoreToken,
      jwtTokenUpdatedCallback: (token) {
        file.writeAsStringSync(token);
      },
    ),
  );
  // to here


  return {
    'google_play': GooglePlayPurchaseHandler(
      androidPublisher,
      iapRepository,
      pubsubApi,
    ),
    'app_store': AppStorePurchaseHandler(
      iapRepository,
      appStoreServerAPI, // new
    ),
  };

App Store のセットアップ

次に、App Store をセットアップします。

  1. App Store Connect にログインし、[ユーザーとアクセス] を選択します。
  2. [Key Type] > [In-App Purchase] に移動します。
  3. プラスアイコンをタップして、新しいタグを追加します。
  4. 名前を付けます(「Codelab キー」など)。
  5. 鍵を含む p8 ファイルをダウンロードします。
  6. アセット フォルダに SubscriptionKey.p8 という名前でコピーします。
  7. 新しく作成した鍵から鍵 ID をコピーし、lib/constants.dart ファイルの appStoreKeyId 定数に設定します。
  8. 鍵リストの一番上に Issuer ID をコピーし、lib/constants.dart ファイルで appStoreIssuerId 定数に設定します。

9540ea9ada3da151.png

デバイスで購入を追跡する

購入の追跡を行う最も安全な方法はサーバーサイドです。これは、クライアントを保護するのが難しいためです。ただし、アプリが定期購入のステータス情報に基づいて動作できるように、情報をクライアントに戻す方法が必要です。購入データを Firestore に保存すると、データをクライアントと簡単に同期し、自動的に更新できます。

アプリにはすでに IAPRepo が含まれています。これは、ユーザーのすべての購入データが List<PastPurchase> purchases に格納されている Firestore リポジトリです。リポジトリには hasActiveSubscription, も含まれます。これは、ステータスが期限切れでない productId storeKeySubscription を含む購入がある場合に true になります。ユーザーがログインしていない場合、リストは空になります。

lib/repo/iap_repo.dart

  void updatePurchases() {
    _purchaseSubscription?.cancel();
    var user = _user;
    if (user == null) {
      purchases = [];
      hasActiveSubscription = false;
      hasUpgrade = false;
      return;
    }
    var purchaseStream = _firestore
        .collection('purchases')
        .where('userId', isEqualTo: user.uid)
        .snapshots();
    _purchaseSubscription = purchaseStream.listen((snapshot) {
      purchases = snapshot.docs.map((DocumentSnapshot document) {
        var data = document.data();
        return PastPurchase.fromJson(data);
      }).toList();

      hasActiveSubscription = purchases.any((element) =>
          element.productId == storeKeySubscription &&
          element.status != Status.expired);

      hasUpgrade = purchases.any(
        (element) => element.productId == storeKeyUpgrade,
      );

      notifyListeners();
    });
  }

購入ロジックはすべて DashPurchases クラスにあり、定期購入の適用または削除を行う場所です。そのため、iapRepo をクラスのプロパティとして追加し、コンストラクタで iapRepo を代入します。次に、コンストラクタにリスナーを直接追加し、dispose() メソッドでリスナーを削除します。最初は、リスナーを空の関数にすることもできます。IAPRepoChangeNotifier であり、Firestore での購入が変更されるたびに notifyListeners() を呼び出すため、購入した商品が変更されると常に purchasesUpdate() メソッドが呼び出されます。

lib/logic/dash_purchases.dart

  IAPRepo iapRepo;

  DashPurchases(this.counter, this.firebaseNotifier, this.iapRepo) {
    final purchaseUpdated = iapConnection.purchaseStream;
    _subscription = purchaseUpdated.listen(
      _onPurchaseUpdate,
      onDone: _updateStreamOnDone,
      onError: _updateStreamOnError,
    );
    iapRepo.addListener(purchasesUpdate);
    loadPurchases();
  }

  @override
  void dispose() {
    _subscription.cancel();
    iapRepo.removeListener(purchasesUpdate);
    super.dispose();
  }

  void purchasesUpdate() {
    //TODO manage updates
  }

次に、main.dart. のコンストラクタに IAPRepo を指定します。リポジトリは Provider ですでに作成されているため、context.read を使用して取得できます。

lib/main.dart

        ChangeNotifierProvider<DashPurchases>(
          create
: (context) => DashPurchases(
            context
.read<DashCounter>(),
            context
.read<FirebaseNotifier>(),
            context
.read<IAPRepo>(),
         
),
          lazy
: false,
       
),

次に、purchaseUpdate() 関数のコードを記述します。dash_counter.dart, では、applyPaidMultiplier メソッドと removePaidMultiplier メソッドで乗数をそれぞれ 10 または 1 に設定しているため、定期購入がすでに適用されているかどうかを確認する必要はありません。サブスクリプションのステータスを変更するときは、購入可能な商品のステータスも更新して、すでに有効であることを購入ページに表示できるようにします。アップグレードが購入されたかどうかに基づいて _beautifiedDashUpgrade プロパティを設定します。

lib/logic/dash_purchases.dart

  void purchasesUpdate() {
    var subscriptions = <PurchasableProduct>[];
    var upgrades = <PurchasableProduct>[];
    // Get a list of purchasable products for the subscription and upgrade.
    // This should be 1 per type.
    if (products.isNotEmpty) {
      subscriptions = products
          .where((element) => element.productDetails.id == storeKeySubscription)
          .toList();
      upgrades = products
          .where((element) => element.productDetails.id == storeKeyUpgrade)
          .toList();
    }

    // Set the subscription in the counter logic and show/hide purchased on the
    // purchases page.
    if (iapRepo.hasActiveSubscription) {
      counter.applyPaidMultiplier();
      for (var element in subscriptions) {
        _updateStatus(element, ProductStatus.purchased);
      }
    } else {
      counter.removePaidMultiplier();
      for (var element in subscriptions) {
        _updateStatus(element, ProductStatus.purchasable);
      }
    }

    // Set the Dash beautifier and show/hide purchased on
    // the purchases page.
    if (iapRepo.hasUpgrade != _beautifiedDashUpgrade) {
      _beautifiedDashUpgrade = iapRepo.hasUpgrade;
      for (var element in upgrades) {
        _updateStatus(
            element,
            _beautifiedDashUpgrade
                ? ProductStatus.purchased
                : ProductStatus.purchasable);
      }
      notifyListeners();
    }
  }

  void _updateStatus(PurchasableProduct product, ProductStatus status) {
    if (product.status != ProductStatus.purchased) {
      product.status = ProductStatus.purchased;
      notifyListeners();
    }
  }

これで、バックエンド サービスで定期購入とアップグレードのステータスが常に最新の状態になり、アプリと同期されるようになりました。アプリはそれに応じて動作し、定期購入とアップグレードの機能を Dash クリックゲームに適用します。

12. 完了

お疲れさまでした。Codelab を完了しました。この Codelab の最終的なコードは、android_studio_folder.pngcomplete フォルダで確認できます。

詳細については、別の Flutter の Codelab をご覧ください。