1. はじめに
Flutter アプリにアプリ内購入を追加するには、アプリと Play ストアを正しく設定し、購入を確認して、定期購入特典などの必要な権限を付与する必要があります。
この Codelab では、アプリ(提供)に 3 種類のアプリ内購入を追加し、Firebase を使用した Dart バックエンドでこれらの購入を確認します。提供されたアプリ「Dash Clicker」には、Dash マスコットを通貨として使用するゲームが含まれています。次の購入オプションを追加します。
- 2,000 個の Dash を一度に購入できる購入オプション(繰り返し購入可能)。
- 古いスタイルの Dash をモダンなスタイルの Dash にするための 1 回限りのアップグレード購入。
- 自動生成されたクリック数を 2 倍にするサブスクリプション。
最初の購入オプションでは、ユーザーに 2,000 個のダッシュが直接付与されます。ユーザーが直接利用でき、何度でも購入できます。これは直接使用され、複数回使用できるため、消耗品と呼ばれます。
2 つ目のオプションでは、Dash がより美しい Dash にアップグレードされます。購入は 1 回のみで、永続的に利用できます。このような購入は、アプリで消費することはできないが、有効期限がないため、非消費アイテムと呼ばれます。
3 つ目の最後の購入オプションはサブスクリプションです。定期購入が有効な間は、ユーザーはダッシュをより早く獲得できますが、定期購入の支払いを停止すると、特典もなくなります。
バックエンド サービス(こちらも提供されます)は Dart アプリとして実行され、購入が行われたことを確認し、Firestore を使用して保存します。Firestore はプロセスを簡素化するために使用されますが、本番環境アプリでは任意のタイプのバックエンド サービスを使用できます。
作成するアプリの概要
- アプリを拡張して、消耗品購入と定期購入をサポートします。
- また、購入したアイテムを確認して保存するように Dart バックエンド アプリを拡張します。
学習内容
- 購入可能なアイテムで App Store と Google Play ストアを設定する方法。
- ストアと通信して購入を検証し、Firestore に保存する方法。
- アプリ内購入を管理する方法。
必要なもの
- Android Studio
- Xcode(iOS 開発用)
- Flutter SDK
2. 開発環境を設定する
この Codelab を始めるには、コードをダウンロードし、iOS のバンドル識別子と 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/app
からスターター プロジェクトを開きます。スクリーンショットには Android Studio を使用しましたが、Visual Studio Code も優れた選択肢です。どちらのエディタでも、最新の Dart プラグインと Flutter プラグインがインストールされていることを確認してください。
作成するアプリは、App Store と Play ストアと通信して、どの商品がどの価格で利用可能かを確認する必要があります。すべてのアプリは一意の ID で識別されます。iOS App Store ではバンドル ID、Android Play ストアではアプリケーション ID と呼ばれます。これらの識別子は通常、逆ドメイン名表記を使用して作成されます。たとえば、flutter.dev 用のアプリ内購入アプリを作成する場合は、dev.flutter.inapppurchase
を使用します。アプリの識別子を考えます。この識別子をプロジェクト設定で設定します。
まず、iOS のバンドル ID を設定します。そのためには、Xcode アプリで Runner.xcworkspace
ファイルを開きます。
Xcode のフォルダ構造では、Runner プロジェクトが最上位にあり、Flutter、Runner、Products の各ターゲットは Runner プロジェクトの下にあります。[Runner] をダブルクリックしてプロジェクト設定を編集し、[Signing & Capabilities] をクリックします。[Team] フィールドに、選択したばかりのバンドル ID を入力して、チームを設定します。
これで Xcode を閉じて Android Studio に戻り、Android の設定を完了できます。そのためには、android/app,
の build.gradle.kts
ファイルを開き、applicationId
(下のスクリーンショットの 24 行目)を iOS バンドル識別子と同じアプリケーション ID に変更します。iOS ストアと Android ストアの ID は同じである必要はありませんが、同じにしておくとエラーが発生しにくいため、この Codelab では同じ識別子を使用します。
3. プラグインをインストールする
この Codelab のこのパートでは、in_app_purchase プラグインをインストールします。
pubspec に依存関係を追加する
プロジェクトの依存関係に in_app_purchase
を追加して、pubspec に in_app_purchase
を追加します。
$ cd app $ flutter pub add in_app_purchase dev:in_app_purchase_platform_interface Resolving dependencies... Downloading packages... characters 1.4.0 (1.4.1 available) flutter_lints 5.0.0 (6.0.0 available) + in_app_purchase 3.2.3 + in_app_purchase_android 0.4.0+3 + in_app_purchase_platform_interface 1.4.0 + in_app_purchase_storekit 0.4.4 + json_annotation 4.9.0 lints 5.1.1 (6.0.0 available) material_color_utilities 0.11.1 (0.13.0 available) meta 1.16.0 (1.17.0 available) provider 6.1.5 (6.1.5+1 available) test_api 0.7.6 (0.7.7 available) Changed 5 dependencies! 7 packages have newer versions incompatible with dependency constraints. Try `flutter pub outdated` for more information.
pubspec.yaml
を開き、dependencies
のエントリとして in_app_purchase
が、dev_dependencies
のエントリとして in_app_purchase_platform_interface
がリストされていることを確認します。
pubspec.yaml
dependencies:
flutter:
sdk: flutter
cloud_firestore: ^6.0.0
cupertino_icons: ^1.0.8
firebase_auth: ^6.0.1
firebase_core: ^4.0.0
google_sign_in: ^7.1.1
http: ^1.5.0
intl: ^0.20.2
provider: ^6.1.5
logging: ^1.3.0
in_app_purchase: ^3.2.3
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^5.0.0
in_app_purchase_platform_interface: ^1.4.0
4. App Store を設定する
iOS でアプリ内購入を設定してテストするには、App Store で新しいアプリを作成し、購入可能なアイテムを作成する必要があります。公開したり、審査のために Apple にアプリを送信したりする必要はありません。これを行うには、デベロッパー アカウントが必要です。お持ちでない場合は、Apple デベロッパー プログラムに登録してください。
有料アプリ契約
アプリ内購入を利用するには、App Store Connect で有料アプリの有効な契約も必要です。https://appstoreconnect.apple.com/ にアクセスし、[契約 / 税金 / 銀行口座情報] をクリックします。
ここでは、無料アプリと有料アプリの契約を確認できます。無料アプリのステータスは「有効」、有料アプリのステータスは「新規」である必要があります。利用規約を確認し、同意して、必要な情報をすべて入力してください。
すべてが正しく設定されると、有料アプリのステータスが有効になります。有効な契約がないとアプリ内購入を試すことができないため、これは非常に重要です。
アプリ ID を登録する
Apple Developer Portal で新しい識別子を作成します。developer.apple.com/account/resources/identifiers/list にアクセスし、[Identifiers] ヘッダーの横にあるプラスアイコンをクリックします。
アプリ ID を選択する
アプリの選択
説明を入力し、バンドル ID を Xcode で以前に設定した値と同じ値に設定します。
新しいアプリ ID の作成方法について詳しくは、デベロッパー アカウントのヘルプをご覧ください。
新しいアプリを作成する
一意のバンドル識別子を使用して、App Store Connect で新しいアプリを作成します。
新しいアプリの作成方法や契約の管理方法について詳しくは、App Store Connect ヘルプをご覧ください。
アプリ内購入をテストするには、サンドボックス テストユーザーが必要です。このテストユーザーは iTunes に接続しないでください。アプリ内購入のテストでのみ使用します。Apple アカウントですでに使用されているメールアドレスは使用できません。[ユーザーとアクセス] で [サンドボックス] に移動し、新しいサンドボックス アカウントを作成するか、既存のサンドボックス Apple ID を管理します。
[設定] > [デベロッパー] > [Sandbox Apple アカウント] に移動して、iPhone でサンドボックス ユーザーを設定できるようになりました。
アプリ内購入を設定する
次に、3 つの購入可能なアイテムを設定します。
dash_consumable_2k
: 何度も購入できる消費型アイテム。購入ごとに 2, 000 ダッシュ(アプリ内通貨)がユーザーに付与されます。dash_upgrade_3d
: 1 回のみ購入可能な「アップグレード」購入。ユーザーは、クリックできる外観の異なるダッシュを入手できます。dash_subscription_doubler
: 定期購入期間中、1 回のクリックで獲得できるダッシュの数が 2 倍になる定期購入。
[アプリ内購入] に移動します。
指定した ID でアプリ内購入を作成します。
dash_consumable_2k
を消費アイテムとして設定します。商品 ID としてdash_consumable_2k
を使用します。参照名は App Store Connect でのみ使用されるため、dash consumable 2k
に設定します。空き情報を設定します。商品はサンドボックス ユーザーの国で利用可能である必要があります。
価格を追加し、価格を
$1.99
または他の通貨での同等の価格に設定します。購入のローカライズを追加します。説明として
2000 dashes fly out
を使用して購入Spring is in the air
を呼び出します。クチコミのスクリーンショットを追加します。商品が審査に送信される場合を除き、コンテンツは重要ではありませんが、アプリが App Store から商品を取得する際に必要な「送信準備完了」状態にするには、コンテンツが必要です。
dash_upgrade_3d
を Non-consumable として設定します。商品 ID としてdash_upgrade_3d
を使用します。参照名をdash upgrade 3d
に設定します。説明としてBrings your dash back to the future
を使用して購入3D Dash
を呼び出します。価格を$0.99
に設定します。dash_consumable_2k
プロダクトと同じ方法で、在庫状況を設定し、レビューのスクリーンショットをアップロードします。dash_subscription_doubler
を自動更新の定期購入として設定します。定期購入のフローは少し異なります。まず、サブスクリプション グループを作成する必要があります。複数の定期購入が同じグループに属している場合、ユーザーは同時に 1 つの定期購入のみに登録できますが、これらの定期購入間でアップグレードまたはダウングレードできます。このグループをsubscriptions
と呼びます。また、サブスクリプション グループのローカライズを追加します。
次に、サブスクリプションを作成します。[Reference Name] を
dash subscription doubler
に、[Product ID] をdash_subscription_doubler
に設定します。次に、定期購入期間(1 週間)とローカライズを選択します。このサブスクリプションに名前
Jet Engine
と説明Doubles your clicks
を付けます。価格を$0.49
に設定します。dash_consumable_2k
プロダクトと同じ方法で、在庫状況を設定し、レビューのスクリーンショットをアップロードします。
リストに商品が表示されます。
5. Google Play ストアを設定する
App Store と同様に、Play ストアでもデベロッパー アカウントが必要です。まだお持ちでない場合は、アカウントを登録します。
新しいアプリを作成する
Google Play Console で新しいアプリを作成します。
- Play Console を開きます。
- [すべてのアプリ] > [アプリを作成] を選択します。
- デフォルトの言語を選択し、アプリのタイトルを追加します。タイトルには、Google Play に表示するアプリの名前を入力します。この名前は後から変更できます。
- アプリがゲームであることを指定します。これは後で変更できます。
- アプリが無料か有料かを指定します。
- 「コンテンツ ガイドライン」と「米国輸出法」の宣言に記入します。
- [アプリを作成] を選択します。
アプリを作成したら、ダッシュボードに移動し、[アプリを設定する] セクションのすべてのタスクを完了します。ここでは、コンテンツ レーティングやスクリーンショットなど、アプリに関する情報を入力します。
アプリケーションに署名する
アプリ内購入をテストするには、Google Play にアップロードされたビルドが 1 つ以上必要です。
そのためには、リリースビルドがデバッグ鍵以外の鍵で署名されている必要があります。
キーストアを作成する
既存のキーストアがある場合は、次のステップに進みます。存在しない場合は、コマンドラインで次のコマンドを実行して作成します。
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
パラメータに渡す引数を変更します。Keep the
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.kts
ファイルを編集して、アプリの署名を構成します。
android
ブロックの前に、プロパティ ファイルからキーストア情報を追加します。
import java.util.Properties
import java.io.FileInputStream
plugins {
// omitted
}
val keystoreProperties = Properties()
val keystorePropertiesFile = rootProject.file("key.properties")
if (keystorePropertiesFile.exists()) {
keystoreProperties.load(FileInputStream(keystorePropertiesFile))
}
android {
// omitted
}
key.properties
ファイルを keystoreProperties
オブジェクトに読み込みます。
buildTypes
ブロックを次のように更新します。
buildTypes {
release {
signingConfig = signingConfigs.getByName("release")
}
}
モジュールの build.gradle.kts
ファイルの signingConfigs
ブロックに署名構成情報を設定します。
signingConfigs {
create("release") {
keyAlias = keystoreProperties["keyAlias"] as String
keyPassword = keystoreProperties["keyPassword"] as String
storeFile = keystoreProperties["storeFile"]?.let { file(it) }
storePassword = keystoreProperties["storePassword"] as String
}
}
buildTypes {
release {
signingConfig = signingConfigs.getByName("release")
}
}
アプリのリリースビルドが自動的に署名されるようになりました。
アプリへの署名について詳しくは、developer.android.com のアプリへの署名をご覧ください。
最初のビルドをアップロードする
署名用にアプリを構成したら、次のコマンドを実行してアプリをビルドできるようになります。
flutter build appbundle
このコマンドはデフォルトでリリースビルドを生成し、出力は <your app dir>/build/app/outputs/bundle/release/
にあります。
Google Play Console のダッシュボードで、[テストとリリース] > [テスト] > [クローズド テスト] に移動し、新しいクローズド テスト リリースを作成します。
次に、ビルドコマンドで生成された app-release.aab
App Bundle をアップロードします。
[保存]、[リリースのレビュー] の順にクリックします。
最後に、[クローズド テストへのロールアウトを開始] をクリックして、クローズド テスト リリースを有効にします。
テストユーザーを設定する
アプリ内購入をテストするには、テスターの Google アカウントを Google Play Console の次の 2 か所に追加する必要があります。
- 特定のテストトラック(内部テスト)
- ライセンス テスターとして
まず、テスターを内部テストトラックに追加します。[テストとリリース > テスト > 内部テスト] に戻り、[テスター] タブをクリックします。
[メーリング リストを作成] をクリックして、新しいメーリング リストを作成します。リストに名前を付け、アプリ内購入のテストへのアクセス権が必要な Google アカウントのメールアドレスを追加します。
次に、リストのチェックボックスをオンにして、[変更を保存] をクリックします。
次に、ライセンス テスターを追加します。
- Google Play Console の [すべてのアプリ] ビューに戻ります。
- [設定] > [ライセンス テスト] に移動します。
- アプリ内購入のテストを行う必要があるテスターのメールアドレスを同じように追加します。
- [ライセンス レスポンス] を
RESPOND_NORMALLY
に設定します。 - [変更を保存] をクリックします。
アプリ内購入を設定する
次に、アプリ内で購入可能なアイテムを設定します。
App Store と同様に、3 つの異なる購入を定義する必要があります。
dash_consumable_2k
: 何度も購入できる消費型アイテム。購入ごとに 2, 000 ダッシュ(アプリ内通貨)がユーザーに付与されます。dash_upgrade_3d
: 1 回のみ購入可能な「アップグレード」の消費型アイテム。ユーザーがクリックできる外観の異なる Dash が提供されます。dash_subscription_doubler
: 定期購入期間中、1 回のクリックで獲得できるダッシュの数が 2 倍になる定期購入。
まず、消耗品と非消耗品を追加します。
- Google Play Console に移動し、アプリを選択します。
- [収益化] > [商品] > [アプリ内アイテム] に移動します。
- [アイテムを作成]
をクリックします。
- 商品に関する必要な情報をすべて入力します。商品 ID が、使用する ID と完全に一致していることを確認します。
- [保存] をクリックします。
- [有効にする] をクリックします。
- 消費されない「アップグレード」購入についても同じ手順を繰り返します。
次に、サブスクリプションを追加します。
- Google Play Console に移動し、アプリを選択します。
- [収益化] > [商品] > [定期購入] に移動します。
- [サブスクリプションを作成] をクリックします。
- 定期購入に必要な情報をすべて入力します。商品 ID が、使用する ID と完全に一致していることを確認します。
- [保存] をクリックします。
これで、購入が Google Play Console で設定されます。
6. Firebase を設定する
この Codelab では、バックエンド サービスを使用してユーザーの購入を検証し、追跡します。
バックエンド サービスを使用すると、次のようなメリットがあります。
- 取引を安全に確認できます。
- アプリストアから課金イベントに反応できます。
- 購入はデータベースで追跡できます。
- ユーザーがシステム時計を巻き戻してアプリをだまし、プレミアム機能を利用することはできません。
バックエンド サービスの設定方法は数多くありますが、ここでは Google 独自の Firebase を使用して、Cloud Functions と Firestore を使用して設定します。
バックエンドの作成はこの Codelab の範囲外と見なされるため、スターター コードには、基本的な購入を処理する Firebase プロジェクトがすでに含まれています。
Firebase プラグインもスターター アプリに含まれています。
残りの作業は、独自の Firebase プロジェクトを作成し、アプリとバックエンドの両方を Firebase 用に構成して、最後にバックエンドをデプロイすることです。
Firebase プロジェクトを作成する
Firebase コンソールに移動して、新しい Firebase プロジェクトを作成します。この例では、プロジェクトを Dash Clicker と呼びます。
バックエンド アプリでは、購入を特定のユーザーに関連付けるため、認証が必要です。これには、Firebase の認証モジュールと Google ログインを使用します。
- Firebase ダッシュボードで [認証] に移動し、必要に応じて有効にします。
- [Sign-in method] タブに移動し、[Google] ログイン プロバイダを有効にします。
Firebase の Firestore データベースも使用するため、これも有効にします。
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 つのプラットフォームを選択して、iOS と Android を有効にします。
? 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)] アプリを選択します。
デバッグモードで 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 ハッシュをコピーし、アプリ送信モーダル ダイアログの最後のフィールドに入力します。
最後に、flutterfire configure
コマンドを再度実行して、署名構成を含めるようにアプリを更新します。
$ flutterfire configure ? You have an existing `firebase.json` file and possibly already configured your project for Firebase. Would you prefer to reuse the valus in your existing `firebase.json` file to configure your project? (y/n) › yes ✔ You have an existing `firebase.json` file and possibly already configured your project for Firebase. Would you prefer to reuse the values in your existing `firebase.json` file to configure your project? · yes
iOS 用の Firebase の設定: その他の手順
Xcode
で ios/Runner.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.plist
です。
Key-Value ペアはすでに存在しますが、値を置き換える必要があります。
<string>..</string>
要素で囲まれていないGoogleService-Info.plist
ファイルからREVERSED_CLIENT_ID
の値を取得します。ios/Runner/Info.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
を見つけます。このページでは、DashCounter
、DashUpgrades,
、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
で、DashPurchasesChangeNotifier
のコードに移動します。現時点では、購入した Dash に追加できるのは DashCounter
のみです。
ストリーム サブスクリプション プロパティ _subscription
(StreamSubscription<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
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:in_app_purchase/in_app_purchase.dart';
import '../main.dart';
import '../model/purchasable_product.dart';
import '../model/store_state.dart';
import 'dash_counter.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
にはモック商品が表示されます。purchasable_product.dart
の PurchasableProduct
クラスを次のコードに置き換えて、実際のコンテンツを表示するように更新します。
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 = [];
に置き換えます。
利用可能な購入を読み込む
ユーザーが購入できるようにするには、ストアから購入を読み込みます。まず、ストアが利用可能かどうかを確認します。ストアが利用できない場合、storeState
を notAvailable
に設定すると、ユーザーにエラー メッセージが表示されます。
lib/logic/dash_purchases.dart
Future<void> loadPurchases() async {
final available = await iapConnection.isAvailable();
if (!available) {
storeState = StoreState.notAvailable;
notifyListeners();
return;
}
}
ストアが利用可能になったら、利用可能な購入を読み込みます。前述の Google Play と App Store の設定では、storeKeyConsumable
、storeKeySubscription,
、storeKeyUpgrade
が表示されます。購入予定の商品が利用できない場合は、この情報をコンソールに出力します。この情報をバックエンド サービスに送信することもできます。
await iapConnection.queryProductDetails(ids)
メソッドは、見つからなかった ID と見つかった購入可能な商品の両方を返します。レスポンスの productDetails
を使用して UI を更新し、StoreState
を available
に設定します。
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(); // Add this line
}
最後に、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 ストアで利用可能な商品を確認できます。各コンソールに入力した購入内容が反映されるまでには、時間がかかることがあります。
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/
フォルダをルートとして作業します。
次のツールがインストールされていることを確認します。
- Dart
- Firebase CLI
ベース プロジェクトの概要
このプロジェクトの一部は、この Codelab の範囲外と見なされるため、スターター コードに含まれています。始める前に、スターター コードに何が含まれているかを確認して、構造を把握しておくことをおすすめします。
このバックエンド コードはマシンでローカルに実行できます。デプロイする必要はありません。ただし、開発デバイス(Android または iPhone)からサーバーが実行されるマシンに接続できる必要があります。そのためには、同じネットワークに接続している必要があり、マシンの IP アドレスを知っておく必要があります。
次のコマンドを使用してサーバーを実行してみてください。
$ dart ./bin/server.dart Serving at http://0.0.0.0:8080
Dart バックエンドは、shelf
と shelf_router
を使用して API エンドポイントを提供します。デフォルトでは、サーバーはルートを提供しません。後で、購入確認プロセスを処理するルートを作成します。
スターター コードにすでに含まれている部分の 1 つは、lib/iap_repository.dart
の IapRepository
です。Firestore やデータベース全般の操作方法を学ぶことは、この Codelab の趣旨に沿っていないため、スターター コードには、Firestore で購入を作成または更新するための関数と、それらの購入に関するすべてのクラスが含まれています。
Firebase へのアクセス権を設定する
Firebase Firestore にアクセスするには、サービス アカウント アクセスキーが必要です。Firebase プロジェクトの設定を開いて [サービス アカウント] セクションに移動し、[新しい秘密鍵の生成] を選択して生成します。
ダウンロードした JSON ファイルを assets/
フォルダにコピーし、ファイル名を service-account-firebase.json
に変更します。
Google Play へのアクセスを設定する
購入の確認のために Google Play ストアにアクセスするには、これらの権限を持つサービス アカウントを生成し、その JSON 認証情報をダウンロードする必要があります。
- Google Cloud コンソールで Google Play Android Developer API のページにアクセスします。
Google Play Console でプロジェクトの作成または既存のプロジェクトへのリンクを求められた場合は、まずその操作を行ってから、このページに戻ってください。
- 次に、サービス アカウント ページに移動し、[+ サービス アカウントを作成] をクリックします。
- [サービス アカウント名] を入力し、[作成して続行] をクリックします。
- [Pub/Sub サブスクライバー] ロールを選択し、[完了] をクリックします。
- アカウントを作成したら、[キーの管理] に移動します。
- [鍵を追加 > 新しい鍵を作成] を選択します。
- JSON キーを作成してダウンロードします。
- ダウンロードしたファイルの名前を
service-account-google-play.json,
に変更し、assets/
ディレクトリに移動します。 - 次に、Google Play Console の [ユーザーと権限] ページに移動します。
- [新しいユーザーを招待] をクリックし、先ほど作成したサービス アカウントのメールアドレスを入力します。メールアドレスは、[サービス アカウント] ページのテーブルで確認できます。
- アプリケーションに [売上データの表示] 権限と [注文と定期購入の管理] 権限を付与します。
- [ユーザーを招待] をクリックします。
もう 1 つ必要な作業は、lib/constants.dart,
を開き、androidPackageId
の値を Android アプリ用に選択したパッケージ ID に置き換えることです。
Apple App Store へのアクセスを設定する
購入の検証のために App Store にアクセスするには、共有シークレットを設定する必要があります。
- App Store Connect を開きます。
- [マイアプリ] に移動し、アプリを選択します。
- サイドバーのナビゲーションで、[General] > [App information] に移動します。
- [アプリ固有の共有シークレット] ヘッダーの下にある [管理] をクリックします。
- 新しいシークレットを生成してコピーします。
lib/constants.dart,
を開き、appStoreSharedSecret
の値を生成した共有シークレットに置き換えます。
定数の構成ファイル
続行する前に、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 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../logic/dash_purchases.dart';
import '../logic/firebase_notifier.dart'; // Add this import
import '../model/firebase_state.dart'; // And this import
import '../model/purchasable_product.dart';
import '../model/store_state.dart';
import '../repo/iap_repo.dart';
import 'login_page.dart'; // And this one as well
class PurchasePage extends StatelessWidget {
const PurchasePage({super.key});
@override
Widget build(BuildContext context) { // Update from here
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();
} // To here.
// ...
アプリから通話確認エンドポイントを呼び出す
アプリで、http post 呼び出しを使用して Dart バックエンドの /verifypurchase
エンドポイントを呼び出す _verifyPurchase(PurchaseDetails purchaseDetails)
関数を作成します。
選択したストア(Play ストアの場合は google_play
、App Store の場合は app_store
)、serverVerificationData
、productID
を送信します。サーバーは、購入が検証されたかどうかを示すステータス コードを返します。
アプリの定数で、サーバー IP をローカルマシンの IP アドレスに構成します。
lib/logic/dash_purchases.dart
import 'dart:async';
import 'dart:convert'; // Add this import
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http; // And this import
import 'package:in_app_purchase/in_app_purchase.dart';
import '../constants.dart';
import '../main.dart';
import '../model/purchasable_product.dart';
import '../model/store_state.dart';
import 'dash_counter.dart';
import 'firebase_notifier.dart'; // And this one
class DashPurchases extends ChangeNotifier {
DashCounter counter;
FirebaseNotifier firebaseNotifier; // Add this line
StoreState storeState = StoreState.loading;
late StreamSubscription<List<PurchaseDetails>> _subscription;
List<PurchasableProduct> products = [];
bool get beautifiedDash => _beautifiedDashUpgrade;
bool _beautifiedDashUpgrade = false;
final iapConnection = IAPConnection.instance;
DashPurchases(this.counter, this.firebaseNotifier) { // Update this line
final purchaseUpdated = iapConnection.purchaseStream;
_subscription = purchaseUpdated.listen(
_onPurchaseUpdate,
onDone: _updateStreamOnDone,
onError: _updateStreamOnError,
);
loadPurchases();
}
main.dart:
で DashPurchases
の作成とともに firebaseNotifier
を追加します。
lib/main.dart
ChangeNotifierProvider<DashPurchases>(
create: (context) => DashPurchases(
context.read<DashCounter>(),
context.read<FirebaseNotifier>(),
),
lazy: false,
),
FirebaseNotifier に User のゲッターを追加して、ユーザー ID を購入確認関数に渡せるようにします。
lib/logic/firebase_notifier.dart
Future<FirebaseFirestore> get firestore async {
var isInitialized = await _isInitialized.future;
if (!isInitialized) {
throw Exception('Firebase is not initialized');
}
return FirebaseFirestore.instance;
}
User? get user => FirebaseAuth.instance.currentUser; // Add this line
Future<void> load() async {
// ...
関数 _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(2000);
case storeKeyUpgrade:
_beautifiedDashUpgrade = true;
}
}
}
if (purchaseDetails.pendingCompletePurchase) {
await iapConnection.completePurchase(purchaseDetails);
}
}
アプリで、購入を検証する準備が整いました。
バックエンド サービスを設定する
次に、バックエンドで購入を確認するためのバックエンドを設定します。
購入ハンドラをビルドする
両方のストアの確認フローはほぼ同じであるため、ストアごとに個別の実装で抽象 PurchaseHandler
クラスを設定します。
まず、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,
}) async {
return true;
}
@override
Future<bool> handleSubscription({
required String userId,
required ProductData productData,
required String token,
}) async {
return true;
}
}
これで、これで、購入ハンドラが 2 つになりました。次に、購入確認 API エンドポイントを作成します。
購入ハンドラを使用する
bin/server.dart
を開き、shelf_route
を使用して API エンドポイントを作成します。
bin/server.dart
import 'dart:convert';
import 'package:firebase_backend_dart/helpers.dart';
import 'package:firebase_backend_dart/products.dart';
import 'package:shelf/shelf.dart';
import 'package:shelf_router/shelf_router.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');
}
}
このコードは次の処理を行っています。
- 前に作成したアプリから呼び出される POST エンドポイントを定義します。
- JSON ペイロードをデコードし、次の情報を抽出します。
userId
: ログインしたユーザーの IDsource
: 使用されたストア(app_store
またはgoogle_play
)。productData
: 以前に作成したproductDataMap
から取得します。token
: ストアに送信する検証データが含まれます。
- ソースに応じて、
GooglePlayPurchaseHandler
またはAppStorePurchaseHandler
のいずれかのverifyPurchase
メソッドを呼び出します。 - 検証が成功すると、メソッドはクライアントに
Response.ok
を返します。 - 検証に失敗した場合、メソッドはクライアントに
Response.internalServerError
を返します。
API エンドポイントを作成したら、2 つの購入ハンドラを構成する必要があります。これには、前の手順で取得したサービス アカウント キーを読み込み、Android Publisher API や Firebase Firestore API などのさまざまなサービスへのアクセスを設定する必要があります。次に、異なる依存関係を持つ 2 つの購入ハンドラを作成します。
bin/server.dart
import 'dart:convert';
import 'dart:io'; // new
import 'package:firebase_backend_dart/app_store_purchase_handler.dart'; // new
import 'package:firebase_backend_dart/google_play_purchase_handler.dart'; // new
import 'package:firebase_backend_dart/helpers.dart';
import 'package:firebase_backend_dart/iap_repository.dart'; // new
import 'package:firebase_backend_dart/products.dart';
import 'package:firebase_backend_dart/purchase_handler.dart'; // new
import 'package:googleapis/androidpublisher/v3.dart' as ap; // new
import 'package:googleapis/firestore/v1.dart' as fs; // new
import 'package:googleapis_auth/auth_io.dart' as auth; // new
import 'package:shelf/shelf.dart';
import 'package:shelf_router/shelf_router.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
/// Handle non-subscription purchases (one time purchases).
///
/// Retrieves the purchase status from Google Play and updates
/// the Firestore Database accordingly.
@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 don't 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 don't 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 {
// See next step
}
次に、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) {
print('Successfully verified purchase');
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;
次に、PubsubApi
を GooglePlayPurchaseHandler
に渡し、クラス コンストラクタを変更して、次のように 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
は 10 秒ごとに _pullMessageFromPubSub
メソッドを呼び出すように構成されています。[期間] はご希望に応じて調整できます。
次に、_pullMessageFromPubSub
を作成します。
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/$googleCloudProjectId/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/$googleCloudProjectId/subscriptions/$googlePlayPubsubBillingTopic-sub';
await pubsubApi.projects.subscriptions.acknowledge(
request,
subscriptionName,
);
}
追加したコードは、10 秒ごとに Google Cloud の Pub/Sub トピックと通信して、新しいメッセージをリクエストします。次に、_processMessage
メソッドで各メッセージを処理します。
このメソッドは、受信メッセージをデコードし、定期購入と定期購入以外の両方について、各購入に関する更新情報を取得します。必要に応じて、既存の handleSubscription
または handleNonSubscription
を呼び出します。
各メッセージは _askMessage
メソッドで確認応答する必要があります。
次に、必要な依存関係を server.dart
ファイルに追加します。PubsubApi.cloudPlatformScope を認証情報の構成に追加します。
bin/server.dart
import 'package:googleapis/pubsub/v1.dart' as pubsub; // Add this import
final clientGooglePlay = await auth
.clientViaServiceAccount(clientCredentialsGooglePlay, [
ap.AndroidPublisherApi.androidpublisherScope,
pubsub.PubsubApi.cloudPlatformScope, // Add this line
]);
次に、PubsubApi インスタンスを作成します。
bin/server.dart
final pubsubApi = pubsub.PubsubApi(clientGooglePlay);
最後に、これを GooglePlayPurchaseHandler
コンストラクタに渡します。
bin/server.dart
return {
'google_play': GooglePlayPurchaseHandler(
androidPublisher,
iapRepository,
pubsubApi, // Add this line
),
'app_store': AppStorePurchaseHandler(
iapRepository,
),
};
Google Play のセットアップ
pub/sub トピックから課金イベントを使用するコードは記述しましたが、pub/sub トピックは作成しておらず、課金イベントも公開していません。セットアップしましょう。
まず、Pub/Sub トピックを作成します。
constants.dart
のgoogleCloudProjectId
の値を Google Cloud プロジェクトの ID に設定します。- Google Cloud コンソールの Cloud Pub/Sub ページにアクセスします。
- Firebase プロジェクトに移動し、[+ トピックを作成] をクリックします。
- 新しいトピックに、
constants.dart
のgooglePlayPubsubBillingTopic
に設定された値と同じ名前を付けます。この場合は、play_billing
という名前を付けます。別のものを選択した場合は、必ずconstants.dart
を更新してください。トピックを作成します。 - Pub/Sub トピックのリストで、作成したトピックのその他アイコン(縦に並んだ 3 つの点)をクリックし、[権限を表示] をクリックします。
- 右側のサイドバーで、[プリンシパルを追加] を選択します。
- ここで、
google-play-developer-notifications@system.gserviceaccount.com
を追加して、Pub/Sub パブリッシャーのロールを付与します。 - 権限の変更を保存します。
- 作成したトピックのトピック名をコピーします。
- Google Play Console を再度開き、[すべてのアプリ] リストからアプリを選択します。
- 下にスクロールして、[収益化] > [収益化のセットアップ] に移動します。
- トピック全体を入力し、変更を保存します。
これで、すべての Google Play の課金イベントがトピックに公開されるようになります。
アプリストアの課金イベントを処理する
次に、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; // Add this member
AppStorePurchaseHandler(
this.iapRepository,
this.appStoreServerAPI, // And this parameter
);
_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
/// Request the App Store for the latest subscription status.
/// Updates all App Store subscriptions in the database.
/// NOTE: This code only handles when a subscription expires as example.
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,
),
);
}
}
}
}
このメソッドは次のように動作します。
- IapRepository を使用して、Firestore から有効な定期購入のリストを取得します。
- 注文ごとに、App Store Server API に定期購入ステータスをリクエストします。
- 定期購入の購入の最後の取引を取得します。
- 有効期限を確認します。
- Firestore でサブスクリプションのステータスを更新します。期限切れの場合は、その旨がマークされます。
最後に、App Store Server API へのアクセスを構成するために必要なコードをすべて追加します。
bin/server.dart
import 'package:app_store_server_sdk/app_store_server_sdk.dart'; // Add this import
import 'package:firebase_backend_dart/constants.dart'; // And this one.
// 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, // Add this argument
),
};
App Store の設定
次に、App Store を設定します。
- [App Store Connect] にログインし、[Users and Access] を選択します。
- [Integrations] > [Keys] > [In-App Purchase] に移動します。
- プラスアイコンをタップして新しいものを追加します。
- 「Codelab key」などの名前を付けます。
- 鍵を含む p8 ファイルをダウンロードします。
- アセット フォルダに
SubscriptionKey.p8
という名前でコピーします。 - 新しく作成した鍵から鍵 ID をコピーし、
lib/constants.dart
ファイルのappStoreKeyId
定数に設定します。 - 鍵リストの最上部にある発行者 ID をコピーし、
lib/constants.dart
ファイルのappStoreIssuerId
定数に設定します。
デバイスでの購入を追跡する
購入を追跡する最も安全な方法はサーバーサイドで行うことです。クライアントのセキュリティを確保することは難しいですが、アプリが定期購入ステータス情報に基づいて動作できるように、情報をクライアントに返す方法が必要です。購入情報を Firestore に保存することで、データをクライアントと同期し、自動的に最新の状態に保つことができます。
アプリにはすでに IAPRepo が含まれています。これは、List<PastPurchase> purchases
のすべてのユーザーの購入データを含む Firestore リポジトリです。リポジトリには、期限切れでないステータスの productId storeKeySubscription
を含む購入がある場合に true になる hasActiveSubscription,
も含まれています。ユーザーがログインしていない場合、リストは空になります。
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((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()
メソッドでリスナーを削除します。最初は、リスナーは空の関数でかまいません。IAPRepo
は ChangeNotifier
であり、Firestore の購入が変更されるたびに notifyListeners()
を呼び出すため、購入した商品が変更されると常に purchasesUpdate()
メソッドが呼び出されます。
lib/logic/dash_purchases.dart
import '../repo/iap_repo.dart'; // Add this import
class DashPurchases extends ChangeNotifier {
DashCounter counter;
FirebaseNotifier firebaseNotifier;
StoreState storeState = StoreState.loading;
late StreamSubscription<List<PurchaseDetails>> _subscription;
List<PurchasableProduct> products = [];
IAPRepo iapRepo; // Add this line
bool get beautifiedDash => _beautifiedDashUpgrade;
bool _beautifiedDashUpgrade = false;
final iapConnection = IAPConnection.instance;
// Add this.iapRepo as a parameter
DashPurchases(this.counter, this.firebaseNotifier, this.iapRepo) {
final purchaseUpdated = iapConnection.purchaseStream;
_subscription = purchaseUpdated.listen(
_onPurchaseUpdate,
onDone: _updateStreamOnDone,
onError: _updateStreamOnError,
);
iapRepo.addListener(purchasesUpdate);
loadPurchases();
}
Future<void> loadPurchases() async {
// Elided.
}
@override
void dispose() {
_subscription.cancel();
iapRepo.removeListener(purchasesUpdate); // Add this line
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>(), // Add this line
),
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 の最終的なコードは、 complete フォルダで確認できます。
詳細については、別の Flutter Codelab をご覧ください。