この Codelab について
1. はじめに
Flutter アプリにアプリ内購入を追加するには、アプリストアと Google Play ストアを正しく設定し、購入を確認して、定期購入の特典などの必要な権限を付与する必要があります。
この Codelab では、3 種類のアプリ内購入をアプリ(提供済み)に追加し、Firebase で Dart バックエンドを使用してこれらの購入を確認します。提供されているアプリ「Dash Clicker」には、Dash のマスコットを通貨として使用するゲームが含まれています。次の購入オプションを追加します。
- 2,000 個のダッシュを一度に購入できる繰り返し可能な購入オプション。
- 従来のスタイルのダッシュボードを最新のスタイルのダッシュボードにアップグレードするための 1 回限りの購入。
- 自動生成されるクリック数を 2 倍にするサブスクリプション。
最初の購入オプションでは、ユーザーに 2,000 ダッシュの直接的な特典が提供されます。ユーザーは直接購入でき、何度でも購入できます。これは直接消費され、複数回消費できるため、消耗品と呼ばれます。
2 つ目のオプションは、ダッシュをより美しいダッシュにアップグレードします。購入は 1 回のみで、有効期間はありません。このような購入は、アプリで消費されず、有効期限がないため「非消費アイテム」と呼ばれます。
3 つ目の購入オプションは定期購入です。定期購入が有効な間は、ユーザーはより早くダッシュを獲得できますが、定期購入の支払いを停止すると特典も失われます。
バックエンド サービス(こちらも提供されます)は Dart アプリとして実行され、購入が行われたことを検証し、Firestore を使用して購入を保存します。Firestore はプロセスを容易にするために使用されますが、本番環境のアプリでは、任意のタイプのバックエンド サービスを使用できます。
作成するアプリの概要
- アプリを拡張して、消耗型の購入と定期購入をサポートします。
- また、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/app
からスターター プロジェクトを開きます。スクリーンショットには 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 を設定します。そのためには、Xcode アプリで Runner.xcworkspace
ファイルを開きます。
Xcode のフォルダ構造では、ランナー プロジェクトが最上位にあり、Flutter、Runner、Products ターゲットがランナー プロジェクトの下に配置されています。[Runner] をダブルクリックしてプロジェクト設定を編集し、[Signing & Capabilities] をクリックします。選択したバンドル ID を [チーム] フィールドに入力して、チームを設定します。
Xcode を閉じて Android Studio に戻り、Android の設定を完了します。そのためには、android/app,
の build.gradle.kts
ファイルを開き、applicationId
(下のスクリーンショットの 24 行目)を、iOS バンドル識別子と同じアプリケーション ID に変更します。iOS ストアと Android ストアの ID は同じである必要はありませんが、同じ ID を使用するとエラーが発生しにくくなるため、この Codelab では同じ ID を使用します。
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.6.3
cupertino_icons: ^1.0.8
firebase_auth: ^5.4.2
firebase_core: ^3.11.0
google_sign_in: ^6.2.2
http: ^1.3.0
intl: ^0.20.2
provider: ^6.1.2
in_app_purchase: ^3.2.1
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^5.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](契約、税金、銀行)をクリックします。
無料アプリと有料アプリの規約が表示されます。無料アプリのステータスは有効、有料アプリのステータスは新規である必要があります。利用規約を確認し、同意して、必要な情報をすべて入力してください。
すべての設定が正しく行われると、有料アプリのステータスは有効になります。有効な同意がない場合は、アプリ内購入を試すことができないため、この点は非常に重要です。
アプリ ID を登録する
Apple デベロッパー ポータルで新しい ID を作成します。https://developer.apple.com/account/resources/identifiers/list にアクセスし、[識別子] の横にあるプラスアイコンをクリックします。
アプリ ID を選択する
アプリの選択
説明を入力し、バンドル ID を設定します。バンドル ID は、XCode で以前に設定した値と同じにします。
新しいアプリ ID を作成する方法について詳しくは、デベロッパー アカウントのヘルプをご覧ください。
新しいアプリを作成する
一意のバンドル ID を使用して、App Store Connect で新しいアプリを作成します。
新しいアプリを作成し、契約を管理する方法について詳しくは、App Store Connect ヘルプをご覧ください。
アプリ内購入をテストするには、サンドボックス テストユーザーが必要です。このテストユーザーは iTunes に接続しないでください。このユーザーは、アプリ内購入のテストにのみ使用します。Apple アカウントですでに使用されているメールアドレスは使用できません。[ユーザーとアクセス] の [サンドボックス] に移動して、新しいサンドボックス アカウントを作成するか、既存のサンドボックス Apple ID を管理します。
これで、iPhone でサンドボックス ユーザーを設定できます。[設定] > [デベロッパー] > [サンドボックスの Apple アカウント] に移動します。
アプリ内購入の設定
次に、購入可能な 3 つのアイテムを設定します。
dash_consumable_2k
: 何度でも購入できる消費型の購入で、購入ごとに 2, 000 ダッシュ(アプリ内通貨)が付与されます。dash_upgrade_3d
: 1 回しか購入できない消耗しない「アップグレード」の購入で、ユーザーに外観が異なるダッシュボタンが表示されます。dash_subscription_doubler
: サブスクリプション期間中、クリックあたり 2 倍のダッシュをユーザーに付与するサブスクリプション。
[アプリ内購入] に移動します。
指定された ID を使用してアプリ内購入を作成します。
dash_consumable_2k
を Consumable として設定します。プロダクト 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
を非消耗品として設定します。プロダクト 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
と呼びます。サブスクリプション グループのローカライズを追加します。
次に、サブスクリプションを作成します。参照名を
dash subscription doubler
、プロダクト ID をdash_subscription_doubler
に設定します。次に、サブスクリプションの期間を 1 週間に設定し、ローカライズを選択します。このサブスクリプションに
Jet Engine
という名前を付け、説明をDoubles your clicks
にします。価格を$0.49
に設定します。dash_consumable_2k
商品の場合と同じ方法で在庫状況を設定し、レビューのスクリーンショットをアップロードします。
リストに商品が表示されます。
5. Google Play ストアをセットアップする
アプリストアと同様に、Google Play ストアにもデベロッパー アカウントが必要です。まだお持ちでない場合は、アカウントを登録してください。
新しいアプリを作成する
Google Play Console で新しいアプリを作成します。
- Google Play Console を開きます。
- [すべてのアプリ] > [アプリを作成] を選択します。
- デフォルトの言語を選択し、アプリのタイトルを追加します。タイトルには、Google Play に表示するアプリの名前を入力します。この名前は後から変更できます。
- アプリがゲームであることを指定します。これは後で変更できます。
- アプリが無料か有料かを指定します。
- コンテンツ ガイドラインと米国輸出法の宣言に記入します。
- [アプリを作成] を選択します。
アプリが作成されたら、ダッシュボードに移動し、[アプリをセットアップする] セクションのすべてのタスクを完了します。ここでは、コンテンツ レーティングやスクリーンショットなど、アプリに関する情報を入力します。
アプリケーションに署名する
アプリ内購入をテストするには、少なくとも 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.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 のダッシュボードで、[テストとリリース] > [テスト] > [クローズド テスト] に移動し、新しいクローズド テスト リリースを作成します。
次に、build コマンドで生成された app-release.aab
App Bundle をアップロードします。
[保存]、[リリースのレビュー] の順にクリックします。
最後に、[クローズド テストへのロールアウトを開始] をクリックして、クローズド テスト リリースを有効にします。
テストユーザーを設定する
アプリ内購入をテストするには、Google Play Console の次の 2 か所にテスターの Google アカウントを追加する必要があります。
- 特定のテストトラック(内部テスト)
- ライセンス テスターとして
まず、テスターを内部テストトラックに追加します。[テストとリリース > テスト > 内部テスト] に戻り、[テスター] タブをクリックします。
[メーリング リストを作成] をクリックして、新しいメーリング リストを作成します。リストに名前を付け、アプリ内購入のテストにアクセスする必要がある Google アカウントのメールアドレスを追加します。
次に、リストのチェックボックスをオンにして、[変更を保存] をクリックします。
次に、ライセンス テスターを追加します。
- Google Play Console の [すべてのアプリ] ビューに戻ります。
- [設定] > [ライセンス テスト] に移動します。
- アプリ内購入をテストする必要があるテスターと同じメールアドレスを追加します。
- [License response] を
RESPOND_NORMALLY
に設定します。 - [変更を保存] をクリックします。
アプリ内購入の設定
次は、アプリ内で購入できるアイテムを構成します。
App Store と同様に、次の 3 種類の購入を定義する必要があります。
dash_consumable_2k
: 何度でも購入できる消費型の購入で、購入ごとに 2, 000 ダッシュ(アプリ内通貨)が付与されます。dash_upgrade_3d
: 1 回のみ購入できる消耗しない「アップグレード」の購入。外観が異なるダッシュが表示されます。dash_subscription_doubler
: サブスクリプション期間中、クリックあたり 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 と呼びます。
バックエンド アプリでは、購入を特定のユーザーに関連付けるため、認証が必要です。これには、Google ログインで Firebase の認証モジュールを活用します。
- Firebase ダッシュボードで [認証] に移動し、必要に応じて有効にします。
- [ログイン方法] タブに移動し、[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
と ios/Runner/Info.plist
です。
Key-Value ペアはすでに追加されていますが、値を置き換える必要があります。
GoogleService-Info.plist
ファイルからREVERSED_CLIENT_ID
の値を取得します。この値は、<string>..</string>
要素で囲まれていない必要があります。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
で、DashPurchases ChangeNotifier
のコードに移動します。現在、購入したダッシュに追加できるのは 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
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.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();
}
最後に、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 へのアクセスを設定する
Play ストアにアクセスして購入を確認するには、これらの権限を持つサービス アカウントを生成し、その JSON 認証情報をダウンロードする必要があります。
- Google Cloud コンソールで Google Play Android Developer API のページにアクセスします。
Google Play Console でプロジェクトの作成または既存のプロジェクトへのリンクを求められた場合は、まずその手順を完了してから、このページに戻ってください。
- 次に、[サービス アカウント] ページに移動し、[+ サービス アカウントを作成] をクリックします。
- サービス アカウント名を入力し、[作成して続行] をクリックします。
- [Pub/Sub サブスクライバー] ロールを選択し、[完了] をクリックします。
- アカウントを作成したら、[キーを管理] に移動します。
- [鍵を追加] > [新しい鍵を作成] を選択します。
- JSON キーを作成してダウンロードします。
- ダウンロードしたファイルの名前を
service-account-google-play.json,
に変更し、assets/
ディレクトリに移動します。 - 次に、Google Play Console の [ユーザーと権限] ページに移動します。
- [新しいユーザーを招待] をクリックし、先ほど作成したサービス アカウントのメールアドレスを入力します。メールアドレスは、[サービス アカウント] ページの表で確認できます。
- アプリに [売上データの表示] 権限と [注文と定期購入の管理] 権限を付与します。
- [ユーザーを招待] をクリックします。
最後に、lib/constants.dart,
を開いて androidPackageId
の値を、Android アプリに選択したパッケージ ID に置き換えます。
Apple App Store へのアクセスを設定する
アプリストアにアクセスして購入を確認するには、共有シークレットを設定する必要があります。
- App Store Connect を開きます。
- [マイアプリ] に移動し、アプリを選択します。
- サイドバー ナビゲーションで、[全般] > [アプリ情報] に移動します。
- [アプリ固有の共有シークレット] ヘッダーの [管理] をクリックします。
- 新しいシークレットを生成してコピーします。
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 '../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
)、serverVerificationData
、productID
を送信します。サーバーは、購入が検証されたかどうかを示すステータス コードを返します。
アプリ定数で、サーバーの 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);
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'; // new
import 'package:firebase_backend_dart/helpers.dart';
import 'package:firebase_backend_dart/products.dart'; // new
import 'package:shelf/shelf.dart'; // new
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
@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;
次に、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
は、_pullMessageFromPubSub
メソッドを 10 秒ごとに呼び出すように構成されています。時間はご希望に応じて調整できます。
次に、_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/$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 トピックを作成します。
constants.dart
のgoogleCloudProjectId
の値を Google Cloud プロジェクトの ID に設定します。- Google Cloud コンソールの Cloud Pub/Sub ページに移動します。
- Firebase プロジェクトに移動し、[+ トピックを作成] をクリックします。
- 新しいトピックに、
constants.dart
のgooglePlayPubsubBillingTopic
に設定した値と同じ名前を付けます。この例では、play_billing
という名前を付けます。他のものを選択した場合は、constants.dart
を必ず更新してください。トピックを作成します。 - Pub/Sub トピックのリストで、作成したトピックのその他アイコンをクリックし、[権限を表示] をクリックします。
- 右側のサイドバーで [プリンシパルを追加] を選択します。
- ここに
google-play-developer-notifications@system.gserviceaccount.com
を追加し、Pub/Sub パブリッシャーのロールを付与します。 - 権限の変更を保存します。
- 作成したトピックのトピック名をコピーします。
- Google Play Console をもう一度開き、[すべてのアプリ] リストからアプリを選択します。
- 下にスクロールして、[収益化] > [収益化のセットアップ] に移動します。
- トピック全体を入力し、変更を保存します。
すべての 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,
));
}
}
}
}
この方法は次のように機能します。
- IapRepository を使用して、アクティブな定期購入のリストを Firestore から取得します。
- 注文ごとに、App Store Server API に定期購入のステータスをリクエストします。
- その定期購入の最後の取引を取得します。
- 有効期限を確認します。
- 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 をセットアップします。
- App Store Connect にログインし、[ユーザーとアクセス] を選択します。
- [Integrations] > [Keys] > [In-App Purchase] に移動します。
- 新しいスポットを追加するには、プラスアイコンをタップします。
- 名前を付けます(例:「Codelab key」)。
- 鍵を含む p8 ファイルをダウンロードします。
- アセット フォルダに
SubscriptionKey.p8
という名前でコピーします。 - 新しく作成した鍵から鍵 ID をコピーし、
lib/constants.dart
ファイルのappStoreKeyId
定数に設定します。 - 鍵リストの一番上に Issuer ID をコピーし、
lib/constants.dart
ファイルでappStoreIssuerId
定数に設定します。
デバイスで購入を追跡する
購入の追跡を行う最も安全な方法は、サーバーサイドで行うことです。これは、クライアントを保護するのが難しいためです。ただし、アプリが定期購入のステータス情報に基づいて動作できるように、情報をクライアントに戻す方法が必要です。購入データを 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((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
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 の最終的なコードは、complete フォルダで確認できます。
詳細については、別の Flutter の Codelab をご覧ください。