向 Flutter 应用添加应用内购商品

1. 简介

如需向 Flutter 应用添加应用内购商品,您需要正确设置应用商店和 Play 商店、验证购买交易,以及授予必要的权限(例如订阅福利)。

在此 Codelab 中,您将向一个应用(已提供)添加三种类型的应用内购,并使用 Dart 后端与 Firebase 一起验证这些购买交易。所提供的应用 Dash Clicker 包含一款使用 Dash 吉祥物作为货币的游戏。您将添加以下购买选项:

  1. 一次性购买 2,000 个 Dash 的购买选项(可重复使用)。
  2. 一次性升级购买交易,可将旧版 Dash 板块改为新版 Dash 板块。
  3. 一种订阅,可将自动生成的点击次数翻倍。

第一个购买选项可直接为用户提供 2,000 个 Dash 币。这些内容可直接供用户使用,并且可以多次购买。这称为消耗型资源,因为它会被直接消耗,并且可以多次消耗。

第二种方法是将 Dash 升级为更美观的 Dash。此服务只需购买一次,即可永久使用。此类购买交易称为“非消耗型购买交易”,因为应用无法消耗此类购买交易,但其有效期为永久。

第三种也是最后一种购买选项是订阅。订阅有效期间,用户将更快获得 Dash 短语,但当用户停止付费订阅后,相关福利也会随之消失。

后端服务(也由我们提供)会作为 Dart 应用运行,验证购买交易是否已完成,并使用 Firestore 存储这些交易。Firestore 用于简化此流程,但在正式版应用中,您可以使用任何类型的后端服务。

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

构建内容

  • 您将扩展应用以支持消耗型购买交易和订阅。
  • 您还将扩展 Dart 后端应用,以验证和存储购买的商品。

学习内容

  • 如何在 App Store 和 Play 商店中配置可购买的商品。
  • 如何与商店通信以验证购买交易并将其存储在 Firestore 中。
  • 如何管理应用内的购买交易。

所需条件

  • Android Studio 4.1 或更高版本
  • Xcode 12 或更高版本(用于 iOS 开发)
  • Flutter SDK

2. 设置开发环境

如需开始此 Codelab,请下载代码,然后更改 iOS 的软件包标识符和 Android 的软件包名称。

下载代码

如需从命令行克隆 GitHub 代码库,请使用以下命令:

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

或者,如果您已安装 GitHub 的 CLI 工具,请使用以下命令:

gh repo clone flutter/codelabs flutter-codelabs

示例代码会克隆到包含一系列 Codelab 代码的 flutter-codelabs 目录中。此 Codelab 的代码位于 flutter-codelabs/in_app_purchases 中。

flutter-codelabs/in_app_purchases 下的目录结构包含一系列快照,这些快照显示了您在完成每个命名步骤后应达到的状态。起始代码位于第 0 步,因此只需执行以下操作即可找到匹配的文件:

cd flutter-codelabs/in_app_purchases/step_00

如果您想跳转到后面的步骤或查看某个步骤之后的内容,请查看以相应步骤命名的目录。最后一步的代码位于 complete 文件夹下。

设置入门级项目

在您喜欢的 IDE 中打开 step_00 中的起始项目。我们使用 Android Studio 截取了屏幕截图,但 Visual Studio Code 也是不错的选择。无论使用哪种编辑器,请确保安装了最新的 Dart 和 Flutter 插件。

您要开发的应用需要与 App Store 和 Play 商店通信,以了解哪些商品有售以及价格。每个应用都有一个唯一 ID。对于 iOS App Store,此 ID 称为软件包标识符;对于 Android Play 商店,此 ID 称为应用 ID。这些标识符通常使用反向域名标记法创建。例如,为 flutter.dev 创建应用内购买应用时,您需要使用 dev.flutter.inapppurchase。为您的应用构想一个标识符,您现在将在项目设置中进行设置。

首先,为 iOS 设置软件包标识符。

在 Android Studio 中打开项目后,右键点击 iOS 文件夹,点击 Flutter,然后在 Xcode 应用中打开该模块。

942772eb9a73bfaa.png

在 Xcode 的文件夹结构中,Runner 项目位于顶部,FlutterRunnerProducts 目标位于 Runner 项目下方。双击 Runner 以修改项目设置,然后点击 Signing & Capabilities(签名和功能)。在团队字段下输入您刚刚选择的软件包标识符,以设置您的团队。

812f919d965c649a.jpeg

现在,您可以关闭 Xcode 并返回 Android Studio,以完成 Android 的配置。为此,请打开 android/app, 下的 build.gradle 文件,然后将 applicationId(在下方屏幕截图的 37 行)更改为应用 ID,该 ID 与 iOS 软件包标识符相同。请注意,iOS 和 Android 商店的 ID 不必完全相同,但保持它们相同会降低出错几率,因此在此 Codelab 中,我们也将使用完全相同的标识符。

5c4733ac560ae8c2.png

3. 安装插件

在此 Codelab 的此部分中,您将安装 in_app_purchase 插件。

在 pubspec 中添加依赖项

in_app_purchase 添加到 pubspec 的依赖项中,以将 in_app_purchase 添加到 pubspec:

$ cd app
$ flutter pub add in_app_purchase dev:in_app_purchase_platform_interface

打开 pubspec.yaml,并确认您现在已将 in_app_purchase 列为 dependencies 下的条目,并将 in_app_purchase_platform_interface 列为 dev_dependencies 下的条目。

pubspec.yaml

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

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

点击 pub get 下载软件包,或在命令行中运行 flutter pub get

4. 设置 App Store

如需在 iOS 上设置应用内购买功能并对其进行测试,您需要在 App Store 中创建新应用,并在其中创建可购买的商品。您无需发布任何内容,也无需将应用发送给 Apple 进行审核。您需要拥有开发者账号才能执行此操作。如果您还没有 Apple 开发者账号,请注册 Apple 开发者计划

如需使用应用内购,您还需要在 App Store Connect 中拥有有效的付费应用协议。前往 https://appstoreconnect.apple.com/,然后点击协议、税务和银行

11db9fca823e7608.png

您会在此处看到免费应用和付费应用的协议。免费应用的状态应为“有效”,付费应用的状态为“新”。请务必查看条款、接受条款并输入所有必要信息。

74c73197472c9aec.png

所有设置正确无误后,付费应用的状态将变为“已启用”。这一点非常重要,因为如果未签署有效的协议,您将无法试用应用内购买功能。

4a100bbb8cafdbbf.jpeg

注册应用 ID

在 Apple 开发者门户中创建新的标识符。

55d7e592d9a3fc7b.png

选择应用 ID

13f125598b72ca77.png

选择应用

41ac4c13404e2526.png

提供一些说明,并将软件包 ID 设置为与之前在 XCode 中设置的软件包 ID 相同的值。

9d2c940ad80deeef.png

如需有关如何创建新应用 ID 的更多指导,请参阅开发者账号帮助

创建新应用

在 App Store Connect 中使用您的唯一软件包标识符创建新应用。

10509b17fbf031bd.png

5b7c0bb684ef52c7.png

如需有关如何创建新应用和管理协议的更多指导,请参阅 App Store Connect 帮助

如需测试应用内购买交易,您需要创建沙盒测试用户。此测试用户不应与 iTunes 相关联,仅用于测试应用内购买交易。您不能使用已用于 Apple 账号的电子邮件地址。在用户和访问权限中,前往沙盒下的测试人员,以创建新的沙盒账号或管理现有的沙盒 Apple ID。

3ca2b26d4e391a4c.jpeg

现在,您可以在 iPhone 上设置沙盒用户,具体方法是依次前往设置 > App Store > 沙盒账号

d99e0b89673867cd.jpeg e1621bcaeb33d3c5.jpeg

配置应用内购买

现在,您需要配置三个可购买的商品:

  • dash_consumable_2k:可多次购买的消耗型购买交易,每次购买可为用户提供 2, 000 个 Dash(应用内货币)。
  • dash_upgrade_3d:一次性购买的非消耗型“升级”购买交易,可为用户提供外观不同的 Dash 来点击。
  • dash_subscription_doubler:一种订阅,可在订阅期间让用户每次点击获得的 Dash 数量翻倍。

d156b2f5bac43ca8.png

依次选择应用内购 > 管理

使用指定的 ID 创建应用内商品:

  1. dash_consumable_2k 设置为消耗型资源

使用 dash_consumable_2k 作为商品 ID。参考名称仅在 App Store Connect 中使用,只需将其设为 dash consumable 2k,然后添加购买交易的本地化内容即可。使用 2000 dashes fly out 作为说明调用购买交易 Spring is in the air

ec1701834fd8527.png

  1. dash_upgrade_3d 设置为非消耗型

使用 dash_upgrade_3d 作为商品 ID。将参考名称设置为 dash upgrade 3d,然后添加购买交易的本地化内容。使用 Brings your dash back to the future 作为说明调用购买交易 3D Dash

6765d4b711764c30.png

  1. dash_subscription_doubler 设置为自动续订的订阅

订阅流程略有不同。首先,您必须设置参考名称和商品 ID:

6d29e08dae26a0c4.png

接下来,您必须创建一个订阅组。如果多项订阅属于同一组,用户只能同时订阅其中一种,但可以轻松在这些订阅之间升级或降级。只需将此组命名为 subscriptions 即可。

5bd0da17a85ac076.png

接下来,输入订阅时长和本地化信息。为此订阅命名为 Jet Engine,并添加说明 Doubles your clicks。点击保存

bd1b1d82eeee4cb3.png

点击保存按钮后,添加订阅价格。选择您想要的任意价格。

d0bf39680ef0aa2e.png

现在,您应该会在购买交易列表中看到这三笔购买交易:

99d5c4b446e8fecf.png

5. 设置 Play 商店

与 App Store 一样,您还需要拥有 Play 商店开发者账号。如果您还没有账号,请注册账号

创建新应用

在 Google Play 管理中心内创建新应用:

  1. 打开 Play 管理中心
  2. 依次选择所有应用 > 创建应用
  3. 选择默认语言,并为您的应用添加名称。输入您希望应用在 Google Play 上显示的名称。您日后可以更改此名称。
  4. 指明您的应用是游戏。您可以在以后更改此设置。
  5. 指明您的应用是免费应用还是付费应用。
  6. 添加电子邮件地址,以便 Play 商店用户可以针对此应用与您联系。
  7. 填写“内容准则”和“美国出口法律”声明。
  8. 选择创建应用

创建应用后,前往信息中心,然后完成设置应用部分中的所有任务。在这里,您可以提供有关应用的一些信息,例如内容分级和屏幕截图。13845badcf9bc1db.png

对应用进行签名

若要测试应用内购买功能,您需要至少将一个 build 上传到 Google Play。

为此,您需要使用调试密钥以外的其他内容对发布 build 进行签名。

创建密钥库

如果您已有密钥库,请跳至下一步。如果没有,请在命令行中运行以下命令来创建一个。

在 Mac/Linux 上,使用以下命令:

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

在 Windows 上,使用以下命令:

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

此命令会将 key.jks 文件存储在您的主目录中。如果您想将文件存储在其他位置,请更改传递给 -keystore 参数的参数。保留

keystore

将文件设为私有;请勿将其签入公共源代码控制系统!

从应用引用密钥库

创建一个名为 <your app dir>/android/key.properties 的文件,其中包含对密钥库的引用:

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

在 Gradle 中配置签名

通过修改 <your app dir>/android/app/build.gradle 文件为应用配置签名。

android 代码块前面添加属性文件中的密钥库信息:

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

   android {
         // omitted
   }

key.properties 文件加载到 keystoreProperties 对象中。

buildTypes 代码块之前添加以下代码:

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

使用签名配置信息配置模块的 build.gradle 文件中的 signingConfigs 块:

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

应用的发布 build 现在会自动签名。

如需详细了解如何为应用签名,请参阅 developer.android.com 上的为应用签名

上传您的首个 build

将应用配置为签名后,您应该能够通过运行以下命令构建应用:

flutter build appbundle

此命令默认会生成一个发布 build,输出可以在 <your app dir>/build/app/outputs/bundle/release/ 下找到

在 Google Play 管理中心的控制台中,依次前往发布 > 测试 > 封闭式测试,然后创建新的封闭式测试版本。

对于本 Codelab,您将继续使用 Google 为应用签名,因此请继续按 Play 应用签名下的继续以选择加入。

ba98446d9c5c40e0.png

接下来,上传 build 命令生成的 app-release.aab app bundle。

点击保存,然后点击检查发布版本

最后,点击开始发布到内部测试以启用内部测试版本。

设置测试用户

如需测试应用内购买功能,您必须在 Google Play 管理中心的两个位置添加测试人员的 Google 账号:

  1. 到特定测试轨道(内部测试)
  2. 作为许可测试人员

首先,将测试人员添加到内部测试轨道。返回到发布 > 测试 > 内部测试,然后点击测试人员标签页。

a0d0394e85128f84.png

点击创建电子邮件收件人列表,创建新的电子邮件收件人列表。为列表命名,然后添加需要有权测试应用内购买的 Google 账号的电子邮件地址。

接下来,选中相应列表的复选框,然后点击保存更改

然后,添加许可测试人员:

  1. 返回 Google Play 管理中心的所有应用视图。
  2. 依次前往设置 > 许可测试
  3. 添加需要能够测试应用内购买的测试人员的电子邮件地址。
  4. 许可响应设置为 RESPOND_NORMALLY
  5. 点击保存

a1a0f9d3e55ea8da.png

配置应用内购买

现在,您将配置可在应用中购买的商品。

与在 App Store 中一样,您必须定义三种不同的购买交易:

  • dash_consumable_2k:可多次购买的消耗型购买交易,每次购买可为用户提供 2, 000 Dash(应用内货币)。
  • dash_upgrade_3d:一次性购买的非消耗型“升级”购买交易,可为用户提供外观不同的 Dash 来点击。
  • dash_subscription_doubler:一种订阅,可在订阅期间让用户每次点击获得的 Dash 数量翻倍。

首先,添加消耗型商品和非消耗型商品。

  1. 前往 Google Play 管理中心,然后选择您的应用。
  2. 依次前往创收 > 商品 > 应用内商品
  3. 点击创建商品c8d66e32f57dee21.png
  4. 输入商品的所有必需信息。请确保商品 ID 与您打算使用的 ID 完全一致。
  5. 点击保存
  6. 点击启用
  7. 对非消耗型“升级”购买交易重复上述过程。

接下来,添加订阅:

  1. 前往 Google Play 管理中心,然后选择您的应用。
  2. 依次前往创收 > 商品 > 订阅
  3. 点击创建订阅32a6a9eefdb71dd0.png
  4. 输入订阅的所有必要信息。请确保商品 ID 与您打算使用的 ID 完全一致。
  5. 点击保存

现在,您应该已在 Play 管理中心内设置购买交易。

6. 设置 Firebase

在此 Codelab 中,您将使用后端服务来验证和跟踪用户的购买交易。

使用后端服务具有以下几点优势:

  • 您可以安全地验证交易。
  • 您可以对应用商店中的结算事件做出响应。
  • 您可以在数据库中跟踪购买交易。
  • 用户将无法通过快退系统时钟来欺骗您的应用提供付费功能。

虽然有许多方法可以设置后端服务,但您将使用 Google 自己的 Firebase 通过 Cloud Functions 和 Firestore 来实现这一点。

编写后端超出了此 Codelab 的范围,因此起始代码已包含一个用于处理基本购买交易的 Firebase 项目,以便您快速上手。

该入门应用还包含 Firebase 插件。

接下来,您只需创建自己的 Firebase 项目、为 Firebase 配置应用和后端,最后部署后端即可。

创建 Firebase 项目

前往 Firebase 控制台,然后创建一个新的 Firebase 项目。在此示例中,将项目命名为 Dash Clicker。

在后端应用中,您需要将购买交易与特定用户相关联,因此需要进行身份验证。为此,请将 Firebase 的身份验证模块与 Google 登录功能搭配使用。

  1. 在 Firebase 信息中心内,前往身份验证,并根据需要启用该服务。
  2. 前往登录方法标签页,然后启用 Google 登录提供方。

7babb48832fbef29.png

由于您还将使用 Firebase 的 Firestore 数据库,因此也要启用此选项。

e20553e0de5ac331.png

设置 Cloud Firestore 规则,如下所示:

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

设置 Firebase for Flutter

在 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>  

接下来,选择 iOSAndroid 以启用这两个平台。

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

当系统提示您是否要替换 firebase_options.dart 时,请选择“是”。

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

设置 Firebase for Android:后续步骤

在 Firebase 信息中心,前往项目概览,选择设置,然后选择常规标签页。

向下滚动到您的应用,然后选择 dashclicker (android) 应用。

b22d46a759c0c834.png

如需允许在调试模式下使用 Google 登录,您必须提供调试证书的 SHA-1 哈希指纹。

获取调试签名证书哈希

在 Flutter 应用项目的根目录中,更改目录为 android/ 文件夹,然后生成签名报告。

cd android
./gradlew :app:signingReport

您会看到一个包含大量签名密钥的列表。由于您要查找调试证书的哈希值,因此请查找将 VariantConfig 属性设置为 debug 的证书。密钥库可能位于主文件夹下的 .android/debug.keystore 中。

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

复制 SHA-1 哈希,然后填写应用提交模态对话框中的最后一个字段。

设置适用于 iOS 的 Firebase:后续步骤

使用 Xcode 打开 ios/Runnder.xcworkspace。或者使用您偏好的 IDE。

在 VSCode 中,右键点击 ios/ 文件夹,然后点击 open in xcode

在 Android Studio 中,右键点击 ios/ 文件夹,然后依次点击 flutteropen iOS module in Xcode 选项。

如需在 iOS 上允许 Google 登录,请将 CFBundleURLTypes 配置选项添加到 build plist 文件中。(如需了解详情,请参阅 google_sign_in 软件包文档。)在本例中,文件为 ios/Runner/Info-Debug.plistios/Runner/Info-Release.plist

键值对已添加,但其值必须替换:

  1. GoogleService-Info.plist 文件中获取 REVERSED_CLIENT_ID 的值,不带其周围的 <string>..</string> 元素。
  2. 替换 ios/Runner/Info-Debug.plistios/Runner/Info-Release.plist 文件中 CFBundleURLTypes 键下的值。
<key>CFBundleURLTypes</key>
<array>
    <dict>
        <key>CFBundleTypeRole</key>
        <string>Editor</string>
        <key>CFBundleURLSchemes</key>
        <array>
            <!-- TODO Replace this value: -->
            <!-- Copied from GoogleService-Info.plist key REVERSED_CLIENT_ID -->
            <string>com.googleusercontent.apps.REDACTED</string>
        </array>
    </dict>
</array>

现在,您已完成 Firebase 设置。

7. 监听购买交易更新

在此 Codelab 的此部分中,您将准备好应用以购买商品。此过程包括在应用启动后监听购买交易更新和错误。

监听购买交易更新

main.dart, 中,找到包含 ScaffoldBottomNavigationBar 包含两个页面的 widget MyHomePage。此页面还会为 DashCounterDashUpgrades,DashPurchases 创建三个 ProviderDashCounter 会跟踪短划线的当前计数并自动递增。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 添加到已购买的 Dash 中。

添加一个串流订阅属性 _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;
    }
  }

商店可用时,加载可用的购买交易。根据之前的 Firebase 设置,您应该会看到 storeKeyConsumablestoreKeySubscription,storeKeyUpgrade。如果预期的购买交易不可用,请将此信息输出到控制台;您可能还需要将此信息发送到后端服务。

await iapConnection.queryProductDetails(ids) 方法会同时返回未找到的 ID 和找到的可购买商品。使用响应中的 productDetails 更新界面,并将 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 微件会显示 _PurchasesLoading_PurchaseList,_PurchasesNotAvailable,,具体取决于 StoreState。该 widget 还会显示用户的过往购买交易,以便在下一步中使用。

_PurchaseList widget 会显示可购买的商品列表,并向 DashPurchases 对象发送购买请求。

lib/pages/purchase_page.dart

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

如果 Android 和 iOS 商店配置正确,您应该能够在其中看到可供购买的商品。请注意,在相应控制台中输入购买交易后,可能需要一些时间才能看到这些交易。

ca1a9f97c21e552d.png

返回 dash_purchases.dart,实现购买商品的函数。您只需将消耗型商品与非消耗型商品分开即可。升级和订阅商品是非消耗型商品。

lib/logic/dash_purchases.dart

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

在继续之前,请创建变量 _beautifiedDashUpgrade 并更新 beautifiedDash Getter 以引用它。

lib/logic/dash_purchases.dart

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

_onPurchaseUpdate 方法会接收购买交易更新,更新购买页面中显示的商品状态,并将购买交易应用于计数器逻辑。请务必在处理购买交易后调用 completePurchase,以便商店知道购买交易已正确处理。

lib/logic/dash_purchases.dart

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

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

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

9. 设置后端

在继续跟踪和验证购买交易之前,请设置 Dart 后端以支持执行这些操作。

在本部分中,将 dart-backend/ 文件夹作为根目录。

请确保已安装以下工具:

基础项目概览

由于此项目的某些部分被视为超出此 Codelab 的范围,因此已包含在起始代码中。在开始之前,最好先查看起始代码中已有的内容,以了解如何构建项目。

此后端代码可以在您的机器上本地运行,您无需部署即可使用。不过,您需要能够从开发设备(Android 或 iPhone)连接到将运行服务器的机器。为此,它们必须位于同一网络中,并且您需要知道机器的 IP 地址。

尝试使用以下命令运行服务器:

$ dart ./bin/server.dart

Serving at http://0.0.0.0:8080

Dart 后端使用 shelfshelf_router 来提供 API 端点。默认情况下,服务器不提供任何路由。稍后,您将创建一个用于处理购买交易验证流程的路由。

起始代码中已经包含的部分之一是 lib/iap_repository.dart 中的 IapRepository。由于学习如何与 Firestore 或一般数据库交互与本 Codelab 无关,因此起始代码包含用于在 Firestore 中创建或更新购买交易的函数,以及这些购买交易的所有类。

设置 Firebase 访问权限

如需访问 Firebase Firestore,您需要有服务账号访问密钥。如需生成私钥,请打开 Firebase 项目设置,前往服务账号部分,然后选择生成新的私钥

27590fc77ae94ad4.png

将下载的 JSON 文件复制到 assets/ 文件夹,然后将其重命名为 service-account-firebase.json

设置 Google Play 访问权限

如需访问 Play 商店以验证购买交易,您必须生成具有这些权限的服务账号,并下载其 JSON 凭据。

  1. 前往 Google Play 管理中心,然后从所有应用页面开始。
  2. 依次前往设置 > API 访问权限317fdfb54921f50e.png 如果 Google Play 管理中心要求您创建或关联现有项目,请先完成相应操作,然后返回此页面。
  3. 找到用于定义服务账号的部分,然后点击创建新的服务账号1e70d3f8d794bebb.png
  4. 点击随即弹出的对话框中的 Google Cloud Platform 链接。7c9536336dd9e9b4.png
  5. 选择您的项目。如果您没有看到该选项,请确保您已登录右上角 Account(账号)下拉列表中的正确 Google 账号。3fb3a25bad803063.png
  6. 选择项目后,点击顶部菜单栏中的 + 创建服务账号62fe4c3f8644acd8.png
  7. 为服务账号提供名称,您也可以提供说明,以便记住其用途,然后执行下一步。8a92d5d6a3dff48c.png
  8. 为服务账号分配 Editor 角色。6052b7753667ed1a.png
  9. 完成向导,返回开发者控制台中的 API 访问权限页面,然后点击刷新服务账号。您应该会在列表中看到新创建的账号。5895a7db8b4c7659.png
  10. 点击新服务账号对应的授予访问权限
  11. 在下一页中向下滚动到财务数据部分。同时选择查看财务数据、订单和用户取消订阅时对调查问卷的书面回复管理订单和订阅75b22d0201cf67e.png
  12. 点击邀请用户70ea0b1288c62a59.png
  13. 账号设置完毕后,您只需生成一些凭据即可。返回 Cloud 控制台,在服务账号列表中找到您的服务账号,点击三点状图标,然后选择管理密钥853ee186b0e9954e.png
  14. 创建新的 JSON 密钥并下载该密钥。2a33a55803f5299c.png cb4bf48ebac0364e.png
  15. 将下载的文件重命名为 service-account-google-play.json,,并将其移至 assets/ 目录。

我们还需要做的一件事是打开 lib/constants.dart,,并将 androidPackageId 的值替换为您为 Android 应用选择的软件包 ID。

设置 Apple App Store 访问权限

如需访问 App Store 以验证购买交易,您必须设置共享密钥:

  1. 打开 App Store Connect
  2. 前往我的应用,然后选择您的应用。
  3. 在边栏导航栏中,依次选择应用内购 > 管理
  4. 点击列表右上角的应用专用共享密钥
  5. 生成一个新 Secret,并将其复制下来。
  6. 打开 lib/constants.dart,,并将 appStoreSharedSecret 的值替换为您刚刚生成的共享密钥。

d8b8042470aaeff.png

b72f4565750e2f40.png

常量配置文件

在继续操作之前,请确保已在 lib/constants.dart 文件中配置以下常量:

  • androidPackageId:在 Android 上使用的软件包 ID,例如 com.example.dashclicker
  • appStoreSharedSecret:用于访问 App Store Connect 以执行购买交易验证的共享密钥。
  • bundleId:在 iOS 上使用的软件包 ID,例如 com.example.dashclicker

目前,您可以忽略其余常量。

10. 验证购买交易

验证购买交易的一般流程在 iOS 和 Android 设备上大同小异。

对于这两种商店,您的应用都会在用户进行购买交易时收到令牌。

此令牌由应用发送到您的后端服务,后者随后使用所提供的令牌与相应商店的服务器进行购买交易验证。

然后,后端服务可以选择存储购买交易,并回复应用购买交易是否有效。

通过让后端服务与商店(而非在用户设备上运行的应用)进行验证,您可以防止用户通过例如回放系统时钟等方式获得对付费功能的访问权限。

设置 Flutter 端

设置身份验证

由于您要将购买交易发送到后端服务,因此需要确保用户在购买时已通过身份验证。我们已在起始项目中为您添加了大部分身份验证逻辑,您只需确保当用户尚未登录时,PurchasePage 会显示登录按钮。将以下代码添加到 PurchasePage 的 build 方法开头:

lib/pages/purchase_page.dart

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

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

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

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

从应用调用验证端点

在应用中,创建一个 _verifyPurchase(PurchaseDetails purchaseDetails) 函数,用于使用 http 发布调用在 Dart 后端调用 /verifypurchase 端点。

发送所选商店(对于 Play 商店为 google_play,对于 App Store 为 app_store)、serverVerificationDataproductID。服务器会返回状态代码,指明购买交易是否已通过验证。

在应用常量中,将服务器 IP 配置为本地机器 IP 地址。

lib/logic/dash_purchases.dart

  FirebaseNotifier firebaseNotifier;

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

main.dart: 中创建 DashPurchases 时添加 firebaseNotifier

lib/main.dart

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

在 FirebaseNotifier 中为 User 添加一个 getter,以便将用户 ID 传递给 verifyPurchase 函数。

lib/logic/firebase_notifier.dart

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

将函数 _verifyPurchase 添加到 DashPurchases 类。此 async 函数会返回一个布尔值,指示购买交易是否已验证。

lib/logic/dash_purchases.dart

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

在应用购买交易之前,在 _handlePurchase 中调用 _verifyPurchase 函数。只有在购买交易通过验证后,您才能应用购买交易。在正式版应用中,您可以进一步指定此属性,例如在商店暂时无法访问时应用试用订阅。不过,在此示例中,请保持简单,仅在购买交易成功验证后应用购买交易。

lib/logic/dash_purchases.dart

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

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

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

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

现在,应用中一切都已准备就绪,可以验证购买交易了。

设置后端服务

接下来,设置 Cloud Functions 函数以在后端验证购买交易。

构建购买交易处理脚本

由于这两家商店的验证流程几乎完全相同,因此请为每家商店分别设置抽象 PurchaseHandler 类实现。

be50c207c5a2a519.png

首先,将 purchase_handler.dart 文件添加到 lib/ 文件夹中,在该文件夹中,您可以定义一个抽象 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,
  });
}

如您所见,每个方法都需要三个参数:

  • userId: 已登录用户的 ID,以便您将购买交易与用户相关联。
  • productData: 商品相关数据。您将在下一分钟定义此属性。
  • token: 商店向用户提供的令牌。

此外,为了简化这些购买交易处理脚本的使用,请添加一个可同时用于订阅和非订阅的 verifyPurchase() 方法:

lib/purchase_handler.dart

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

现在,您只需针对这两种情况调用 verifyPurchase,但仍然可以使用单独的实现!

ProductData 类包含有关不同可购买商品的基本信息,包括商品 ID(有时也称为 SKU)和 ProductType

lib/products.dart

class ProductData {
  final String productId;
  final ProductType type;

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

ProductType 可以是订阅,也可以是非订阅。

lib/products.dart

enum ProductType {
  subscription,
  nonSubscription,
}

最后,在同一文件中将商品列表定义为映射。

lib/products.dart

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

接下来,为 Google Play 商店和 Apple App Store 定义一些占位符实现。从 Google Play 开始:

创建 lib/google_play_purchase_handler.dart,并添加一个扩展您刚刚编写的 PurchaseHandler 的类:

lib/google_play_purchase_handler.dart

import 'dart:async';

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

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

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

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

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

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

目前,它会针对处理程序方法返回 true;您稍后会看到这些方法。

您可能已经注意到,构造函数采用 IapRepository 的实例。购买交易处理脚本稍后会使用此实例在 Firestore 中存储购买交易相关信息。如需与 Google Play 通信,您可以使用提供的 AndroidPublisherApi

接下来,对应用商店处理脚本执行相同的操作。创建 lib/app_store_purchase_handler.dart,然后再次添加一个扩展 PurchaseHandler 的类:

lib/app_store_purchase_handler.dart

import 'dart:async';

import 'package:app_store_server_sdk/app_store_server_sdk.dart';

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

class AppStorePurchaseHandler extends PurchaseHandler {
  final IapRepository iapRepository;

  AppStorePurchaseHandler(
    this.iapRepository,
  );

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

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

太棒了!现在,您有两个购买交易处理脚本。接下来,我们来创建购买验证 API 端点。

使用购买交易处理程序

打开 bin/server.dart,然后使用 shelf_route 创建 API 端点:

bin/server.dart

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

  final purchaseHandlers = await _createPurchaseHandlers();

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

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

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

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

  await serveHandler(router.call);
}

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

上述代码会执行以下操作:

  1. 定义一个将从您之前创建的应用调用的 POST 端点。
  2. 解码 JSON 载荷并提取以下信息:
  3. userId:当前登录的用户 ID
  4. source:使用的存储区(app_storegoogle_play)。
  5. productData:从您之前创建的 productDataMap 中获取。
  6. token:包含要发送到商店的验证数据。
  7. 调用 verifyPurchase 方法(针对 GooglePlayPurchaseHandlerAppStorePurchaseHandler,具体取决于来源)。
  8. 如果验证成功,该方法会向客户端返回 Response.ok
  9. 如果验证失败,该方法会向客户端返回 Response.internalServerError

创建 API 端点后,您需要配置两个购买交易处理脚本。为此,您需要加载在前一步中获取的服务账号密钥,并配置对不同服务(包括 Android Publisher API 和 Firebase Firestore API)的访问权限。然后,使用不同的依赖项创建两个购买交易处理脚本:

bin/server.dart

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

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

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

验证 Android 购买交易:实现购买交易处理脚本

接下来,继续实现 Google Play 购买交易处理脚本。

Google 已提供 Dart 软件包,用于与您验证购买交易所需的 API 进行交互。您已在 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,以及两个用于解析购买交易状态的方法。

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 配置为每 10 秒调用一次 _pullMessageFromSubSub 方法。您可以根据自己的偏好调整时长。

然后,创建 _pullMessageFromSubSub

lib/google_play_purchase_handler.dart

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

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

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

您刚刚添加的代码会每 10 秒与 Google Cloud 中的 Pub/Sub 主题通信一次,并请求获取新消息。然后,在 _processMessage 方法中处理每条消息。

此方法会解码传入的消息,并获取有关每笔购买交易(包括订阅和非订阅)的更新信息,并根据需要调用现有的 handleSubscriptionhandleNonSubscription

每条消息都需要使用 _askMessage 方法进行确认。

接下来,将所需的依赖项添加到 server.dart 文件中。将 PubsubApi.cloudPlatformScope 添加到凭据配置中:

bin/server.dart

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

然后,创建 PubsubApi 实例:

bin/server.dart

  final pubsubApi = pubsub.PubsubApi(clientGooglePlay);

最后,将其传递给 GooglePlayPurchaseHandler 构造函数:

bin/server.dart

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

Google Play 设置

您已编写代码来使用 Pub/Sub 主题中的结算事件,但尚未创建 Pub/Sub 主题,也没有发布任何结算事件。现在开始进行设置。

首先,创建一个 pub/sub 主题:

  1. 访问 Google Cloud 控制台中的 Cloud Pub/Sub 页面
  2. 确保您处于 Firebase 项目页面,然后点击 + 创建主题d5ebf6897a0a8bf5.png
  3. 为新主题指定一个名称,该名称与 constants.ts 中为 GOOGLE_PLAY_PUBSUB_BILLING_TOPIC 设置的值相同。在本例中,将其命名为 play_billing。如果您选择其他内容,请务必更新 constants.ts。创建主题。20d690fc543c4212.png
  4. 在 Pub/Sub 主题列表中,点击您刚刚创建的主题对应的三个垂直点,然后点击查看权限ea03308190609fb.png
  5. 在右侧边栏中,选择添加主账号
  6. 在这里,添加 google-play-developer-notifications@system.gserviceaccount.com,然后授予其 Pub/Sub 发布商的角色。55631ec0549215bc.png
  7. 保存权限更改。
  8. 复制您刚刚创建的主题的主题名称
  9. 再次打开 Play 管理中心,然后从所有应用列表中选择您的应用。
  10. 向下滚动,然后依次前往创收 > 创收设置
  11. 填写完整的主题,然后保存更改。7e5e875dc6ce5d54.png

现在,所有 Google Play 结算事件都将发布到该主题。

处理 App Store 结算事件

接下来,对 App Store 结算事件执行相同的操作。您可以通过两种有效的方式实现在 App Store 中处理购买交易更新。一种是实现您提供给 Apple 的 webhook,Apple 会使用该 webhook 与您的服务器通信。第二种方法(您将在此 Codelab 中找到该方法)是连接到 App Store Server API 并手动获取订阅信息。

本 Codelab 之所以重点介绍第二种解决方案,是因为您必须将服务器公开到互联网上才能实现网络钩子。

在生产环境中,理想情况下,您应该同时拥有这两者。用于从 App Store 获取事件的 Webhook,以及用于在您错过事件或需要仔细检查订阅状态时使用的 Server API。

首先,打开 lib/app_store_purchase_handler.dart 并添加 AppStoreServerAPI 依赖项:

lib/app_store_purchase_handler.dart

final AppStoreServerAPI appStoreServerAPI;

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

修改构造函数以添加一个将调用 _pullStatus 方法的计时器。此计时器将每 10 秒调用一次 _pullStatus 方法。您可以根据需要调整此计时器时长。

lib/app_store_purchase_handler.dart

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

然后,按如下方式创建 _pullStatus 方法:

lib/app_store_purchase_handler.dart

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

此方法的运作方式如下:

  1. 使用 IapRepository 从 Firestore 获取有效订阅的列表。
  2. 对于每个订单,它都会向 App Store Server API 请求订阅状态。
  3. 获取相应订阅购买交易的最后一笔交易。
  4. 检查失效日期。
  5. 更新 Firestore 中的订阅状态,如果订阅已过期,则会被标记为已过期。

最后,添加所有必要的代码以配置 App Store Server API 访问权限:

bin/server.dart

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

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

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

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


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

App Store 设置

接下来,设置 App Store:

  1. 登录 App Store Connect,然后选择用户和访问权限
  2. 依次选择密钥类型 > 应用内购买
  3. 点按“加号”图标添加新的付款方式。
  4. 为其命名,例如“Codelab 密钥”。
  5. 下载包含密钥的 p8 文件。
  6. 将其复制到 assets 文件夹,并命名为 SubscriptionKey.p8
  7. 从新创建的密钥中复制密钥 ID,并将其设置为 lib/constants.dart 文件中的 appStoreKeyId 常量。
  8. 复制密钥列表顶部的颁发者 ID,并将其设置为 lib/constants.dart 文件中的 appStoreIssuerId 常量。

9540ea9ada3da151.png

跟踪设备上的购买交易

在服务器端跟踪购买交易是最安全的方式,因为客户端很难保证安全,但您需要找到一些方法将信息传回客户端,以便应用根据订阅状态信息采取行动。通过在 Firestore 中存储购买交易,您可以轻松将数据同步到客户端并自动进行更新。

您已在应用中添加 IAPRepo,这是 Firestore 仓库,其中包含 List<PastPurchase> purchases 中的所有用户购买交易数据。该代码库还包含 hasActiveSubscription,,如果存在状态未过期的 productId storeKeySubscription 购买交易,则 hasActiveSubscription, 的值为 true。如果用户未登录,列表为空。

lib/repo/iap_repo.dart

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

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

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

      notifyListeners();
    });
  }

所有购买逻辑都位于 DashPurchases 类中,订阅应在此类中应用或移除。因此,请在类中将 iapRepo 添加为属性,并在构造函数中分配 iapRepo。接下来,直接在构造函数中添加监听器,并在 dispose() 方法中移除监听器。最初,监听器可以只是一个空函数。由于 IAPRepoChangeNotifier,并且您会在 Firestore 中的购买交易发生变化时调用 notifyListeners(),因此在购买的商品发生变化时,系统始终会调用 purchasesUpdate() 方法。

lib/logic/dash_purchases.dart

  IAPRepo iapRepo;

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

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

  void purchasesUpdate() {
    //TODO manage updates
  }

接下来,将 IAPRepo 提供给 main.dart. 中的构造函数。您可以使用 context.read 获取代码库,因为它已在 Provider 中创建。

lib/main.dart

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

接下来,编写 purchaseUpdate() 函数的代码。在 dash_counter.dart, 中,applyPaidMultiplierremovePaidMultiplier 方法会分别将乘数设置为 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。您可以在 android_studio_folder.pngcomplete 文件夹中找到此 Codelab 的完整代码。

如需了解详情,请尝试学习其他 Flutter Codelab