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

1. 简介

上次更新日期:2023 年 7 月 11 日

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

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

  1. 一次购买 2000 个 Dash 可重复购买。
  2. 只需完成一次升级,即可将旧版 Dash 变成现代 Dash。
  3. 使自动生成的点击次数翻倍的订阅。

第一个购买选项会直接为用户提供 2,000 个短划线。此类应用直接面向用户出售,可供多次购买。这称为消耗型商品,因为它是直接消耗的,可以多次消耗。

第二个选项将 Dash 升级为更漂亮的 Dash。此产品只需购买一次,且永久有效。此类购买称为“非消耗型”,因为它无法被应用消耗,但永久有效。

第三个也是最后一个购买选项是订阅。当订阅处于活动状态时,用户将可以更快地获得 Dash,但是当他停止为订阅付费时,其福利也会消失。

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

300123416ebc8dc1 7145d0fffe6ea741 646317a79be08214

构建内容

  • 您将扩展应用以支持消耗型购买和订阅。
  • 您还将扩展一个 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

示例代码会克隆到 flutter-codelabs 目录中,其中包含一系列 Codelab 的代码。此 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,这称为软件包标识符;在 Android Play 商店中,这称为应用 ID。这些标识符通常使用倒序域名表示法生成。例如,在为 flutter.dev 构建应用内购买应用时,我们可以使用 dev.flutter.inapppurchase。想一个应用的标识符,现在您需要在项目设置中进行设置。

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

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

942772eb9a73bfaa

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

812f919d965c649a.jpeg

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

5c4733ac560ae8c2

3. 安装插件

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

在 pubspec 中添加依赖项

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

$ cd app
$ flutter pub add in_app_purchase

pubspec.yaml

dependencies:
  ..
  cloud_firestore: ^4.0.3
  firebase_auth: ^4.2.2
  firebase_core: ^2.5.0
  google_sign_in: ^6.0.1
  http: ^0.13.4
  in_app_purchase: ^3.0.1
  intl: ^0.18.0
  provider: ^6.0.2
  ..

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

4. 设置 App Store

若要设置应用内购商品并在 iOS 设备上进行测试,您需要在 App Store 中创建新的应用,然后在那里创建可购买的商品。您无需发布任何内容或将应用发送给 Apple 审核。您需要有开发者账号才能执行此操作。如果没有,请加入 Apple Developer 计划

若要使用应用内购商品,您还需要在 App Store Connect 中针对付费应用达成有效协议。转至 https://appstoreconnect.apple.com/,然后点击 Agreements, Tax, and Banking

6e373780e5e24a6f

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

74c73197472c9aec

如果一切设置正确,付费应用的状态将显示为“有效”。这一点非常重要,因为您必须先签署有效的协议,然后才能尝试进行应用内购买交易。

4a100bbb8cafdbbf.jpeg

注册应用 ID

在 Apple Developer 门户中创建新的标识符。

55d7e592d9a3fc7b

选择应用 ID

13f125598b72ca77

选择应用

41ac4c13404e2526

提供一些说明并设置软件包 ID,使其将软件包 ID 与之前在 XCode 中设置的值保持一致。

9d2c940ad80deeef.png

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

创建新应用

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

10509b17fbf031bd

5b7c0bb684ef52c7

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

要测试应用内购买,您需要拥有沙盒测试用户。此测试用户仅用于测试应用内购买,不应连接到 iTunes。您不能使用已经用于 Apple 账号的电子邮件地址。在用户和访问权限中,转到沙盒下的测试人员,创建新的沙盒账号或管理现有的沙盒 Apple ID。

3ca2b26d4e391a4c.jpeg

现在您可以在 iPhone 上设置沙盒用户,只需依次转到设置 >应用商店 >沙盒账号。

c7dadad2c1d448fa.jpeg 5363f87efcddaa4.jpeg

配置应用内购买

现在,您将配置三种可购买的物品:

  • dash_consumable_2k:一种可多次购买的消耗型商品,用户每次购买可获赠 2, 000 个 Dash(应用内货币)。
  • dash_upgrade_3d:非消耗型“升级”购买一次,为用户提供外观不同的 Dash 按钮,供用户点击。
  • dash_subscription_doubler:订阅后,用户在订阅期内,每次点击的破折号数量将增加一倍。

d156b2f5bac43ca8.png

访问应用内购买 >管理

使用指定 ID 创建应用内购买交易:

  1. dash_consumable_2k 设置为消耗型商品

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

ec1701834fd8527.png

  1. dash_upgrade_3d 设置为非消耗型商品

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

6765d4b711764c30

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

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

6d29e08dae26a0c4.png

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

5bd0da17a85ac076

接下来,输入订阅时长和本地化内容。将此订阅命名为 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

对应用签名

为了能够测试应用内购商品,您至少需要上传到 Google Play 的一个 build。

为此,您需要使用调试密钥以外的方式为发布 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 应用签名下的 Continue 以选择加入该计划。

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:订阅后,用户在订阅期内,每次点击的破折号数量将增加一倍。

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

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

接下来,添加订阅:

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

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

6. 设置 Firebase

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

使用后端服务有几个好处:

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

虽然可以通过多种方式设置后端服务,但您需要使用 Cloud Functions 和 Firestore 以及 Google 自己的 Firebase 来实现这一点。

编写后端不在本 Codelab 的讨论范围内,因此起始代码已包含一个可处理基本购买交易的 Firebase 项目,帮助您顺利上手。

起始应用中也包含 Firebase 插件。

接下来,您要做的就是创建自己的 Firebase 项目,为 Firebase 配置应用和后端,最后部署后端。

创建 Firebase 项目

前往 Firebase 控制台,然后创建一个新的 Firebase 项目。在本示例中,请调用项目 Dash Clicker。

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

  1. 在 Firebase 信息中心内,转到 Authentication(身份验证)并根据需要启用它。
  2. 转到登录方法标签页,然后启用 Google 登录提供方。

7babb48832fbef29

您还将使用 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>  

接下来,通过选择 iOS 和 Android 这两个平台来启用这两个平台。iOSiOS

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

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

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

设置 Firebase for Android:后续步骤

在 Firebase 信息中心内,转到 Project Overview(项目概览),选择 Settings(设置),然后选择 General(常规)标签页。

向下滚动到您的应用,然后选择 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 哈希,并填写应用提交模态对话框中的最后一个字段。

设置 Firebase for iOS:后续步骤

使用 Xcode 打开 ios/Runnder.xcworkspace。或者通过您选择的 IDE。

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

在 Android Studio 中,右键点击 ios/ 文件夹,然后点击 flutter,接着点击 open 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,
),

您还需要一个 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!;
  }
}

如果您想让测试继续运行,必须稍微更新测试。在 GitHub 上查看 widget_test.dart,以获取 TestIAPConnection 的完整代码。

test/widget_test.dart

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

lib/logic/dash_purchases.dart 中,找到 DashPurchases ChangeNotifier 的代码。目前,您只能向购买的 Dash 添加 DashCounter

添加数据流订阅属性 _subscription(类型为 StreamSubscription<List<PurchaseDetails>> _subscription;)、IAPConnection.instance, 和导入项。生成的代码应如下所示:

lib/logic/dash_purchases.dart

import 'package:in_app_purchase/in_app_purchase.dart';

class DashPurchases extends ChangeNotifier {
  late StreamSubscription<List<PurchaseDetails>> _subscription;
  final iapConnection = IAPConnection.instance;

  DashPurchases(this.counter);
}

late 关键字会添加到 _subscription 中,因为 _subscription 已在构造函数中初始化。此项目默认设置为不可为 null (NNBD),这意味着未声明可为 null 的属性必须具有非 null 值。late 限定符可让您延迟定义此值。

在构造函数中,获取 purchaseUpdatedStream 并开始监听音频流。在 dispose() 方法中,取消音频流订阅。

lib/logic/dash_purchases.dart

class DashPurchases extends ChangeNotifier {
  DashCounter counter;
  late StreamSubscription<List<PurchaseDetails>> _subscription;
  final iapConnection = IAPConnection.instance;

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

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

  Future<void> buy(PurchasableProduct product) async {
    // omitted
  }

  void _onPurchaseUpdate(List<PurchaseDetails> purchaseDetailsList) {
    // Handle purchases here
  }

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

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

现在,应用会接收购买更新,因此,在下一部分中,您将完成购买!

在继续操作之前,请使用“flutter test"”运行测试,以验证所有设置均正确无误。

$ flutter test

00:01 +1: All tests passed!                                                                                   

8. 购物

在此 Codelab 的这一部分中,您将使用真实的可购商品替换当前现有的模拟商品。这些商品从商店中加载并显示在列表中,点按商品时可购买。

Adapt 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);
    for (var element in response.notFoundIDs) {
      debugPrint('Purchase $element not found');
    }
    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 widget 根据 StoreState 显示 _PurchasesLoading_PurchaseList,_PurchasesNotAvailable,。该微件还会显示用户过去的购买交易,下一步中将会用到。

_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);
        break;
      case storeKeySubscription:
      case storeKeyUpgrade:
        await iapConnection.buyNonConsumable(purchaseParam: purchaseParam);
        break;
      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();
          break;
        case storeKeyConsumable:
          counter.addBoughtDashes(2000);
          break;
        case storeKeyUpgrade:
          _beautifiedDashUpgrade = true;
          break;
      }
    }

    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

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

设置 Google Play 访问权限

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

  1. 前往 Google Play 管理中心,从所有应用页面开始。
  2. 转到设置 >API 访问权限317fdfb54921f50e如果 Google Play 管理中心要求您创建项目或关联到现有项目,请先创建项目或关联到现有项目,然后再返回此页面。
  3. 找到定义服务账号的部分,然后点击创建新的服务账号1e70d3f8d794bebb
  4. 点击弹出对话框中的 Google Cloud Platform 链接。7c9536336dd9e9b4
  5. 选择您的项目。如果您没有看到此选项,请在右上角的账号下拉列表下确认您登录的是正确的 Google 账号。3fb3a25bad803063
  6. 选择项目后,点击顶部菜单栏中的 + 创建服务账号62fe4c3f8644acd8
  7. 为服务账号提供一个名称,并视需要添加说明以便您记住它的用途,然后继续执行下一步。8a92d5d6a3dff48c
  8. 为服务账号分配 Editor 角色。6052b7753667ed1a
  9. 完成向导的操作后,返回开发者控制台中的 API 访问权限页面,然后点击刷新服务账号。您应该可以在列表中看到新创建的账号。5895a7db8b4c7659
  10. 点击新服务账号对应的授予访问权限
  11. 向下滚动下一页,找到财务数据区块。同时选中查看财务数据、订单和用户取消订阅时对调查问卷的书面回复管理订单和订阅75b22d0201cf67e
  12. 点击邀请用户70ea0b1288c62a59
  13. 账号设置完毕后,您只需生成一些凭据即可。返回 Cloud 控制台,在服务账号列表中找到您的服务账号,点击三个垂直点,然后选择管理密钥853ee186b0e9954e
  14. 创建新的 JSON 密钥并下载。2a33a55803f5299c 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. 在边栏导航栏中,转至 In-App Purchases >管理
  4. 点击列表右上角的应用专用共享密钥
  5. 生成新密钥并复制。
  6. 打开 lib/constants.dart, 并将 appStoreSharedSecret 的值替换为您刚刚生成的共享密钥。

d8b8042470aaeff.png

b72f4565750e2f40.png

常量配置文件

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

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

您可以暂时忽略其余常量。

10. 验证购买交易

验证购买交易的一般流程与 iOS 和 Android 类似。

对于这两家商店,您的应用都会在购买时收到一个令牌。

应用将此令牌发送到您的后端服务,然后后端服务再使用提供的令牌向相应商店的服务器验证购买交易。

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

通过让后端服务对商店(而不是用户设备上运行的应用)进行验证,您可以阻止用户使用其系统时钟倒退等高级功能。

设置 Flutter 端

设置身份验证

当您将购买交易发送到后端服务时,您需要确保用户在购物时通过身份验证。入门级项目中已经为您添加了大部分身份验证逻辑,您只需确保 PurchasePage 在用户尚未登录时显示登录按钮即可。将以下代码添加到 PurchasePage 的构建方法的开头:

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({Key? key}) : super(key: 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 post 调用来调用 Dart 后端上的 /verifypurchase 端点。

发送选定的商店(google_play 用于 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 中为用户添加一个 getter,以便将用户 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) {
      print('Successfully verified purchase');
      return true;
    } else {
      print('failed request: ${response.statusCode} - ${response.body}');
      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();
            break;
          case storeKeyConsumable:
            counter.addBoughtDashes(1000);
            break;
        }
      }
    }

    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);
}

({
  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. 针对 GooglePlayPurchaseHandlerAppStorePurchaseHandler 调用 verifyPurchase 方法,具体取决于调用源。
  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

/// 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;
}

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,
  };
}

现在,您的 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) {
      print('Successfully verified purchase');
      final receipts = response.latestReceiptInfo ?? [];
      for (final receipt in receipts) {
        final product = productDataMap[receipt.productId];
        if (product == null) {
          print('Error: Unknown product: ${receipt.productId}');
          continue;
        }
        switch (product.type) {
          case ProductType.nonSubscription:
            await iapRepository.createOrUpdatePurchase(NonSubscriptionPurchase(
              userId: userId,
              productId: receipt.productId ?? '',
              iapSource: IAPSource.appstore,
              orderId: receipt.originalTransactionId ?? '',
              purchaseDate: DateTime.fromMillisecondsSinceEpoch(
                  int.parse(receipt.originalPurchaseDateMs ?? '0')),
              type: product.type,
              status: NonSubscriptionStatus.completed,
            ));
            break;
          case ProductType.subscription:
            await iapRepository.createOrUpdatePurchase(SubscriptionPurchase(
              userId: userId,
              productId: receipt.productId ?? '',
              iapSource: IAPSource.appstore,
              orderId: receipt.originalTransactionId ?? '',
              purchaseDate: DateTime.fromMillisecondsSinceEpoch(
                  int.parse(receipt.originalPurchaseDateMs ?? '0')),
              type: product.type,
              expiryDate: DateTime.fromMillisecondsSinceEpoch(
                  int.parse(receipt.expiresDateMs ?? '0')),
              status: SubscriptionStatus.active,
            ));
            break;
        }
      }
      return true;
    } else {
      print('Error: Status: ${response.status}');
      return false;
    }
  }

现在,您的 App Store 购买交易应该已得到验证,并存储在数据库中!

运行后端

此时,您可以运行 dart bin/server.dart/verifypurchase 端点提供服务。

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

11. 跟踪购买交易

推荐用于跟踪用户“购买”操作位于后端服务中这是因为您的后端可以响应来自存储区的事件,因此不易因缓存而遇到过时的信息,而且不易被篡改。

首先,使用您构建的 Dart 后端设置在后端处理商店事件。

在后端处理存储事件

商店能够将发生的任何结算事件(例如订阅续订时)告知您的后端。您可以在后端处理这些事件,以使数据库中的购买交易保持最新状态。在本部分中,您需要为 Google Play 商店和 Apple App Store 都进行此项设置。

处理 Google Play 结算事件

Google Play 通过 Cloud Pub/Sub 主题提供结算事件。从本质上讲,这些队列是用于发布消息和使用消息的消息队列。

由于这是 Google Play 特有的功能,因此请将此功能添加到 GooglePlayPurchaseHandler 中。

首先打开 lib/google_play_purchase_handler.dart,然后添加 PubsubApi 导入:

lib/google_play_purchase_handler.dart

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

然后,将 PubsubApi 传递给 GooglePlayPurchaseHandler,并修改类构造函数以创建 Timer,如下所示:

lib/google_play_purchase_handler.dart

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

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

Timer 配置为每十秒调用一次 _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,
    );
  }

您刚刚添加的代码每十秒与 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
  4. 在 Pub/Sub 主题列表中,点击您刚刚创建的主题对应的三个垂直点,然后点击查看权限ea03308190609fb.png
  5. 在右侧边栏中,选择添加主账号
  6. 在此处添加 google-play-developer-notifications@system.gserviceaccount.com,然后授予其 Pub/Sub Publisher 角色。55631ec0549215bc
  7. 保存对权限所做的更改。
  8. 复制刚刚创建的主题的主题名称
  9. 再次打开 Play 管理中心,然后从所有应用列表中选择您的应用。
  10. 向下滚动并转到获利 >创收设置
  11. 填写完整的主题,然后保存更改。7e5e875dc6ce5d54

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

处理 App Store 结算事件

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

此 Codelab 重点介绍第二个解决方案,因为您必须向互联网公开自己的服务器才能实现 webhook。

在生产环境中,最好同时提供这两种功能。用于从 App Store 获取事件的 Webhook 和服务器 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. 将其复制到资源文件夹,并将其命名为 SubscriptionKey.p8
  7. 从新创建的密钥中复制密钥 ID,并在 lib/constants.dart 文件中将其设置为 appStoreKeyId 常量。
  8. 复制密钥列表顶部的发卡机构 ID,并在 lib/constants.dart 文件中将其设置为 appStoreIssuerId 常量。

9540ea9ada3da151

跟踪设备上的购买交易

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

您已在应用中添加了 IAPRepo,后者是一个 Firestore 代码库,其中包含 List<PastPurchase> purchases 中的所有用户购买数据。代码库中还包含 hasActiveSubscription,,当有购买交易的 productId storeKeySubscription 处于未到期状态时,该值为 true。如果用户未登录,此列表为空。

lib/repo/iap_repo.dart

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

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

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

      notifyListeners();
    });
  }

所有购买逻辑均位于 DashPurchases 类中,并在其中应用或移除订阅。因此,将 iapRepo 添加为类中的属性,并在构造函数中分配 iapRepo。接下来,直接在构造函数中添加监听器,并在 dispose() 方法中移除监听器。最初,监听器可以只是一个空函数。由于 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() {
    iapRepo.removeListener(purchasesUpdate);
    _subscription.cancel();
    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 文件夹

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