程式碼研究室簡介
1. 簡介
如要在 Flutter 應用程式中新增應用程式內購,您必須正確設定應用程式和 Play 商店、驗證購買交易,並授予必要權限 (例如訂閱福利)。
在本程式碼研究室中,您將在應用程式 (由系統提供) 中新增三種類型的應用程式內購,並使用 Firebase 搭配 Dart 後端驗證這些購買交易。提供的應用程式 Dash Clicker 包含使用 Dash 吉祥物做為貨幣的遊戲。您將新增下列購買選項:
- 一次購買 2000 個 Dash 的循環購買選項。
- 一次性升級購買項目,可將舊版 Dash 升級為新版 Dash。
- 自動產生點擊次數加倍的訂閱方案。
第一個購買選項可讓使用者直接獲得 2000 個 Dashes,這些商品可直接供使用者購買,且可多次購買。這稱為可消耗資源,因為它會直接消耗,且可多次消耗。
第二個選項是將 Dash 升級為更美觀的 Dash。這項服務只需購買一次,就能永久使用。這類購買交易稱為非消耗性商品,因為應用程式無法消耗這類商品,但這類商品永久有效。
第三個也是最後一個購買選項是訂閱。在訂閱項目有效期間,使用者可以更快取得 Dashes,但當他停止付費訂閱時,福利也會隨之消失。
後端服務 (我們也會提供) 會以 Dart 應用程式執行,驗證購買交易,並使用 Firestore 儲存交易。使用 Firestore 可簡化這項程序,但在正式版應用程式中,您可以使用任何類型的後端服務。
建構項目
- 您將擴充應用程式,以支援消耗性購買交易和訂閱項目。
- 您也會擴充 Dart 後端應用程式,以驗證及儲存購買的項目。
課程內容
- 如何設定可購買產品的 App Store 和 Play 商店。
- 如何與商店聯繫,驗證交易並儲存在 Firestore 中。
- 如何管理應用程式中的購買交易。
軟硬體需求
- Android Studio 4.1 以上版本
- Xcode 12 以上版本 (適用於 iOS 開發)
- Flutter SDK
2. 設定開發環境
如要開始本程式碼研究室,請下載程式碼,然後變更 iOS 的軟體包 ID 和 Android 的套件名稱。
下載程式碼
如要從指令列複製 GitHub 存放區,請使用下列指令:
git clone https://github.com/flutter/codelabs.git flutter-codelabs
或者,如果您已安裝 GitHub 的 CLI 工具,請使用下列指令:
gh repo clone flutter/codelabs flutter-codelabs
程式碼範例會複製到 flutter-codelabs
目錄,該目錄包含一系列程式碼研究室的程式碼。本程式碼研究室的程式碼位於 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
。請想想應用程式的 ID,現在您將在專案設定中設定該 ID。
首先,請設定 iOS 的軟體包 ID。
在 Android Studio 中開啟專案後,請在 iOS 資料夾上按一下滑鼠右鍵,然後點選「Flutter」,並在 Xcode 應用程式中開啟模組。
在 Xcode 的資料夾結構中,Runner 專案位於頂端,而 Flutter、Runner 和 Products 目標則位於 Runner 專案下方。按兩下「Runner」即可編輯專案設定,然後按一下「Signing & Capabilities」。在「Team」欄位下方輸入剛剛選擇的套件 ID,即可設定團隊。
您現在可以關閉 Xcode,並返回 Android Studio 完成 Android 設定。如要這樣做,請開啟 android/app,
底下的 build.gradle
檔案,然後將 applicationId
(位於下方螢幕截圖的第 37 行) 變更為應用程式 ID,這與 iOS 軟體包 ID 相同。請注意,iOS 和 Android 商店的 ID 不必相同,但保持一致可降低發生錯誤的可能性,因此在本程式碼研究室中,我們也會使用相同的 ID。
3. 安裝外掛程式
在本程式碼研究室的這個部分,您將安裝 in_app_purchase 外掛程式。
在 pubspec 中新增依附元件
將 in_app_purchase
新增至 pubspec,方法是在 pubspec 的依附元件中加入 in_app_purchase
:
$ 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 開發人員計畫。
付費應用程式協議
如要使用應用程式內購項目,您也必須在 App Store Connect 中簽署有效的付費應用程式協議。前往 https://appstoreconnect.apple.com/,然後按一下「協議、稅務和銀行」。
您會在這裡看到免費和付費應用程式的協議。免費應用程式的狀態應為有效,付費應用程式的狀態則為新應用程式。請務必查看並接受條款,並輸入所有必要資訊。
一切設定正確後,付費應用程式的狀態就會啟用。這點非常重要,因為如果未啟用協議,您就無法試用應用程式內購功能。
註冊應用程式 ID
在 Apple 開發人員入口網站中建立新的 ID。
選擇應用程式 ID
選擇應用程式
提供一些說明,並將軟體包 ID 設為與先前在 XCode 中設定的軟體包 ID 相同的值。
如需進一步瞭解如何建立新的應用程式 ID,請參閱開發人員帳戶說明。
建立新應用程式
在 App Store Connect 中使用專屬軟體包 ID 建立新的應用程式。
如需有關建立新應用程式和管理協議的詳細指引,請參閱 App Store Connect 說明。
如要測試應用程式內購,您需要沙箱測試使用者。這個測試使用者不應連結至 iTunes,僅用於測試應用程式內購。您無法使用已用於 Apple 帳戶的電子郵件地址。在「使用者和存取權」中,前往「沙箱」下方的「測試人員」,建立新的沙箱帳戶或管理現有的沙箱 Apple ID。
您現在可以在 iPhone 上設定沙箱使用者,方法是依序前往「設定」>「App Store」>「沙箱帳戶」。
設定應用程式內購
接下來,您將設定三個可購買的商品:
dash_consumable_2k
:可多次購買的消耗性商品,每筆交易可為使用者提供 2000 個 Dashes (應用程式內貨幣)。dash_upgrade_3d
:非消耗性「升級」購買項目,只能購買一次,並為使用者提供外觀不同的 Dash 按鈕。dash_subscription_doubler
:訂閱方案,可讓使用者在訂閱期間獲得每點擊兩個 Dash。
依序前往「應用程式內購買項目」>「管理」。
使用指定的 ID 建立應用程式內購:
- 將
dash_consumable_2k
設為 Consumable。
請使用 dash_consumable_2k
做為產品 ID。參考名稱僅用於 App Store Connect,只要將其設為 dash consumable 2k
,然後新增購買交易的本地化版本即可。以 2000 dashes fly out
做為說明,呼叫購買交易 Spring is in the air
。
- 將
dash_upgrade_3d
設為非消耗性。
請使用 dash_upgrade_3d
做為產品 ID。將參考名稱設為 dash upgrade 3d
,然後新增購買交易的本地化版本。以 Brings your dash back to the future
做為說明,呼叫購買交易 3D Dash
。
- 將
dash_subscription_doubler
設為自動續訂訂閱項目。
訂閱的流程稍有不同。首先,您必須設定參考名稱和產品 ID:
接下來,您必須建立訂閱群組。如果多個訂閱項目屬於同一個群組,使用者只能同時訂閱其中一個,但可以輕鬆在這些訂閱項目之間升級或降級。只要將這個群組命名為 subscriptions
即可。
接著,輸入訂閱期限和本地化資訊。將此訂閱命名為 Jet Engine
,並加上說明 Doubles your clicks
。按一下「儲存」。
按一下「儲存」按鈕後,請新增訂閱價格。你可以選擇任何價格。
你現在應該會在購買清單中看到這三筆交易:
5. 設定 Play 商店
如同 App Store,您也需要 Play 商店的開發人員帳戶。如果還沒有帳戶,請註冊帳戶。
建立新應用程式
在 Google Play 管理中心建立新的應用程式:
- 開啟 Play 管理中心。
- 依序選取「所有應用程式」>「建立應用程式」。
- 選擇應用程式的預設語言並設定名稱。請輸入應用程式在 Google Play 顯示的名稱。您日後可以變更名稱。
- 指定應用程式為遊戲。日後可再變更這項設定。
- 指定您的應用程式是否收費。
- 新增電子郵件地址,讓 Play 商店使用者可就應用程式的相關事宜與您聯絡。
- 填妥內容指南和美國出口法律聲明表單。
- 選取「建立應用程式」。
建立應用程式後,請前往資訊主頁,並完成「設定應用程式」部分中的所有工作。請在此提供應用程式相關資訊,例如內容分級和螢幕截圖。
簽署應用程式
如要測試應用程式內購,您至少需要將一個版本上傳至 Google Play。
為此,您需要使用除偵錯金鑰以外的項目簽署發布版本。
建立 KeyStore
如果您已有金鑰庫,請略過下一個步驟。如果沒有,請在指令列中執行下列指令來建立一個。
在 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
檔案為私人檔案,請勿將其簽入公開來源控管系統!
從應用程式參照 KeyStore
建立名為 <your app dir>/android/key.properties
的檔案,其中包含對 KeyStore 的參照:
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
區塊前方,從屬性檔案新增 KeyStore 資訊:
def keystoreProperties = new Properties()
def keystorePropertiesFile = rootProject.file('key.properties')
if (keystorePropertiesFile.exists()) {
keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
}
android {
// omitted
}
將 key.properties
檔案載入 keystoreProperties
物件。
請在 buildTypes
區塊前方加入以下程式碼:
buildTypes {
release {
// TODO: Add your own signing config for the release build.
// Signing with the debug keys for now,
// so `flutter run --release` works.
signingConfig signingConfigs.debug
}
}
在模組的 build.gradle
檔案中,使用簽署設定資訊設定 signingConfigs
區塊:
signingConfigs {
release {
keyAlias keystoreProperties['keyAlias']
keyPassword keystoreProperties['keyPassword']
storeFile keystoreProperties['storeFile'] ? file(keystoreProperties['storeFile']) : null
storePassword keystoreProperties['storePassword']
}
}
buildTypes {
release {
signingConfig signingConfigs.release
}
}
應用程式的發布版本現在會自動簽署。
如要進一步瞭解如何簽署應用程式,請參閱 developer.android.com 上的「簽署應用程式」。
上傳第一個版本
應用程式設定完成後,您應該可以執行以下命令來建構應用程式:
flutter build appbundle
這個指令預設會產生發布版本,輸出內容可在 <your app dir>/build/app/outputs/bundle/release/
中找到
在 Google Play 管理中心的資訊主頁中,依序前往「版本」>「測試」>「封閉測試」,然後建立新的封閉測試版本。
在本程式碼研究室中,您將繼續使用 Google 簽署應用程式,因此請按一下「Play 應用程式簽署」下方的「繼續」,選擇加入。
接著,請上傳建構指令產生的 app-release.aab
應用程式套件。
依序點選「儲存」和「檢查版本」。
最後,請點選「Start rollout to Internal testing」,啟用內部測試版本。
設定測試使用者
如要測試應用程式內購,您必須在 Google Play 管理中心的兩個位置新增測試人員的 Google 帳戶:
- 將應用程式發布至特定測試群組 (內部測試)
- 身為授權測試人員
首先,請將測試人員新增至內部測試群組。返回「發布」>「測試」>「內部測試」,然後點選「測試人員」分頁。
按一下「建立電子郵件名單」,建立新的電子郵件名單。為清單命名,然後新增需要存取應用程式內購功能的 Google 帳戶電子郵件地址。
接著,勾選清單的核取方塊,然後按一下「儲存變更」。
接著,新增授權測試人員:
- 返回 Google Play 管理中心的「所有應用程式」檢視畫面。
- 依序前往「設定」>「授權測試」。
- 新增需要測試應用程式內購的測試人員,並使用相同的電子郵件地址。
- 將「License response」設為
RESPOND_NORMALLY
。 - 按一下 [儲存變更]。
設定應用程式內購
接下來,您將設定可在應用程式中購買的商品。
就像在 App Store 中一樣,您必須定義三種不同的購買項目:
dash_consumable_2k
:可多次購買的消耗性商品,每筆交易可為使用者提供 2000 個 Dashes (應用程式內貨幣)。dash_upgrade_3d
:只能購買一次的非消耗性「升級」購買項目,可讓使用者點選外觀不同的 Dash。dash_subscription_doubler
:訂閱方案,可讓使用者在訂閱期間獲得每點擊兩個 Dash。
首先,新增消耗性和非消耗性。
- 前往 Google Play 管理中心,然後選取應用程式。
- 依序前往「營利」>「產品」>「應用程式內商品」。
- 按一下「建立產品」
- 輸入產品的所有必要資訊。請確認產品 ID 與你要使用的 ID 完全一致。
- 按一下 [儲存]。
- 按一下「啟用」。
- 重複執行非消耗性「升級」購買程序。
接著,新增訂閱項目:
- 前往 Google Play 管理中心,然後選取應用程式。
- 依序前往「營利」>「產品」>「訂閱」。
- 按一下「建立訂閱項目」
- 輸入訂閱項目的所有必要資訊。請確認產品 ID 與你要使用的 ID 完全一致。
- 點選「儲存」。
您現在應該已在 Play 管理中心設定購買交易。
6. 設定 Firebase
在本程式碼研究室中,您將使用後端服務來驗證及追蹤使用者的購買交易。
使用後端服務有幾項優點:
- 您可以安全地驗證交易。
- 您可以回應應用程式商店的結帳事件。
- 您可以在資料庫中追蹤購買交易。
- 使用者無法透過將系統時鐘倒轉,欺騙應用程式提供付費功能。
雖然設定後端服務的方式有很多種,但您將使用 Google 自家的 Firebase 來使用 Cloud 函式和 Firestore。
編寫後端程式碼不在本程式碼研究室的範圍內,因此範例程式碼已包含可處理基本購買交易的 Firebase 專案,讓您快速上手。
啟動器應用程式也包含 Firebase 外掛程式。
接下來,您只需建立專屬的 Firebase 專案、為 Firebase 設定應用程式和後端,最後再部署後端即可。
建立 Firebase 專案
前往 Firebase 控制台,建立新的 Firebase 專案。在本範例中,請呼叫 Dash Clicker 專案。
在後端應用程式中,您將購買交易與特定使用者建立關聯,因此需要驗證。為此,請利用 Firebase 驗證模組搭配 Google 登入功能。
- 在 Firebase 資訊主頁中前往「驗證」,然後視需要啟用。
- 前往「登入方式」分頁,啟用 Google 登入提供者。
由於您也會使用 Firebase 的 Firestore 資料庫,因此也請啟用這項功能。
設定 Cloud Firestore 規則,如下所示:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /purchases/{purchaseId} {
allow read: if request.auth != null && request.auth.uid == resource.data.userId
}
}
}
為 Flutter 設定 Firebase
在 Flutter 應用程式上安裝 Firebase 的建議方式是使用 FlutterFire CLI。請按照設定頁面中的說明操作。
執行 flutterfire configure 時,請選取您在上一個步驟中建立的專案。
$ flutterfire configure
i Found 5 Firebase projects.
? Select a Firebase project to configure your Flutter application with ›
❯ in-app-purchases-1234 (in-app-purchases-1234)
other-flutter-codelab-1 (other-flutter-codelab-1)
other-flutter-codelab-2 (other-flutter-codelab-2)
other-flutter-codelab-3 (other-flutter-codelab-3)
other-flutter-codelab-4 (other-flutter-codelab-4)
<create a new project>
接著,選取「iOS」和「Android」,啟用這兩個平台。
? Which platforms should your configuration support (use arrow keys & space to select)? ›
✔ android
✔ ios
macos
web
當系統提示是否要覆寫 firebase_options.dart 時,請選取「是」。
? Generated FirebaseOptions file lib/firebase_options.dart already exists, do you want to override it? (y/n) › yes
設定 Android 版 Firebase:後續步驟
在 Firebase 資訊主頁中,前往「專案總覽」,然後依序選取「設定」和「一般」分頁。
向下捲動至「你的應用程式」,然後選取「dashclicker (android)」應用程式。
如要在偵錯模式下允許 Google 登入,您必須提供偵錯憑證的 SHA-1 雜湊指紋。
取得偵錯簽署憑證雜湊
在 Flutter 應用程式專案的根目錄中,將目錄變更為 android/
資料夾,然後產生簽署報表。
cd android ./gradlew :app:signingReport
畫面上會顯示大量簽署金鑰清單。由於您要尋找的是偵錯憑證的雜湊值,請尋找 Variant
和 Config
屬性設為 debug
的憑證。鍵值儲存庫可能位於 .android/debug.keystore
底下的家目錄資料夾。
> Task :app:signingReport
Variant: debug
Config: debug
Store: /<USER_HOME_FOLDER>/.android/debug.keystore
Alias: AndroidDebugKey
MD5: XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX
SHA1: XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX
SHA-256: XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX
Valid until: Tuesday, January 19, 2038
複製 SHA-1 雜湊,並在應用程式提交模式對話方塊中填入最後一個欄位。
設定 iOS 版 Firebase:後續步驟
使用 Xcode
開啟 ios/Runnder.xcworkspace
。或使用您偏好的 IDE。
在 VSCode 上按一下 ios/
資料夾的右鍵,然後點選 open in xcode
。
在 Android Studio 中,按一下 ios/
資料夾的滑鼠右鍵,然後依序點選 flutter
和 open iOS module in Xcode
選項。
如要在 iOS 上啟用 Google 登入功能,請在建構 plist
檔案中新增 CFBundleURLTypes
設定選項。(詳情請參閱 google_sign_in
套件說明文件)。在本例中,檔案為 ios/Runner/Info-Debug.plist
和 ios/Runner/Info-Release.plist
。
鍵/值組合已新增,但必須更換其值:
- 從
GoogleService-Info.plist
檔案取得REVERSED_CLIENT_ID
的值,但不包含其中的<string>..</string>
元素。 - 在
CFBundleURLTypes
鍵下,將ios/Runner/Info-Debug.plist
和ios/Runner/Info-Release.plist
檔案中的值都替換為新值。
<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. 收聽購買更新
在本程式碼研究室的這個部分,您將準備購買產品的應用程式。這個程序包括在應用程式啟動後,監聽購買交易更新和錯誤。
收聽購買更新資訊
在 main.dart,
中,找出含有 Scaffold
的小工具 MyHomePage
,其中 BottomNavigationBar
包含兩個頁面。這個頁面也會為 DashCounter
、DashUpgrades,
和 DashPurchases
建立三個 Provider
。DashCounter
會追蹤 Dash 目前的計數,並自動遞增。DashUpgrades
會管理您可透過 Dashes 購買的升級項目。本程式碼研究室著重於 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();
}
}
late
關鍵字會新增至 _subscription
,因為 _subscription
是在建構函式中初始化。這個專案預設為不可為空值 (NNBD),也就是說,未宣告為可為空值的屬性必須具有非空值。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. 購買商品
在本程式碼研究室的這個部分,您將以實際可購買的產品取代現有的模擬產品。系統會從商店載入這些產品,並在清單中顯示,只要輕觸產品即可購買。
調整 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 設定,您應該會看到 storeKeyConsumable
、storeKeySubscription,
和 storeKeyUpgrade
。如果無法完成預期的購買交易,請將這項資訊列印到主控台;您也可以將這項資訊傳送至後端服務。
await iapConnection.queryProductDetails(ids)
方法會傳回找不到的 ID,以及找到的可購買產品。使用回應中的 productDetails
更新 UI,並將 StoreState
設為 available
。
lib/logic/dash_purchases.dart
import '../constants.dart';
Future<void> loadPurchases() async {
final available = await iapConnection.isAvailable();
if (!available) {
storeState = StoreState.notAvailable;
notifyListeners();
return;
}
const ids = <String>{
storeKeyConsumable,
storeKeySubscription,
storeKeyUpgrade,
};
final response = await iapConnection.queryProductDetails(ids);
products = response.productDetails.map((e) => PurchasableProduct(e)).toList();
storeState = StoreState.available;
notifyListeners();
}
在建構函式中呼叫 loadPurchases()
函式:
lib/logic/dash_purchases.dart
DashPurchases(this.counter) {
final purchaseUpdated = iapConnection.purchaseStream;
_subscription = purchaseUpdated.listen(
_onPurchaseUpdate,
onDone: _updateStreamOnDone,
onError: _updateStreamOnError,
);
loadPurchases();
}
最後,將 storeState
欄位的值從 StoreState.available
變更為 StoreState.loading:
lib/logic/dash_purchases.dart
StoreState storeState = StoreState.loading;
顯示可購買的產品
以 purchase_page.dart
檔案為例。PurchasePage
小工具會根據 StoreState
顯示 _PurchasesLoading
、_PurchaseList,
或 _PurchasesNotAvailable,
。小工具也會顯示使用者在下一個步驟中使用的過去購買交易。
_PurchaseList
小工具會顯示可購買的產品清單,並將購買要求傳送至 DashPurchases
物件。
lib/pages/purchase_page.dart
class _PurchaseList extends StatelessWidget {
@override
Widget build(BuildContext context) {
var purchases = context.watch<DashPurchases>();
var products = purchases.products;
return Column(
children: products
.map((product) => _PurchaseWidget(
product: product,
onPressed: () {
purchases.buy(product);
}))
.toList(),
);
}
}
如果 Android 和 iOS 商店設定正確,你應該會在商店中看到可購買的產品。請注意,在各個主機上輸入購買交易後,可能需要一些時間才能使用。
返回 dash_purchases.dart
,並實作購買產品的函式。您只需要將消耗品與非消耗品分開即可。升級和訂閱產品均為非消耗品。
lib/logic/dash_purchases.dart
Future<void> buy(PurchasableProduct product) async {
final purchaseParam = PurchaseParam(productDetails: product.productDetails);
switch (product.id) {
case storeKeyConsumable:
await iapConnection.buyConsumable(purchaseParam: purchaseParam);
case storeKeySubscription:
case storeKeyUpgrade:
await iapConnection.buyNonConsumable(purchaseParam: purchaseParam);
default:
throw ArgumentError.value(
product.productDetails, '${product.id} is not a known product');
}
}
繼續之前,請建立變數 _beautifiedDashUpgrade
,並更新 beautifiedDash
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/
資料夾做為根目錄進行操作。
請確認您已安裝下列工具:
- Dart
- Firebase CLI
基礎專案總覽
由於本專案的部分內容超出本程式碼研究室的範圍,因此已納入範例程式碼。建議您在開始之前先查看範例程式碼中已有的內容,瞭解如何安排程式碼結構。
這個後端程式碼可以在本機上執行,您不必部署即可使用。不過,您必須能夠從開發裝置 (Android 或 iPhone) 連線至要執行伺服器的機器。為此,兩部裝置必須位於相同的網路中,你也必須知道機器的 IP 位址。
請嘗試使用下列指令執行伺服器:
$ dart ./bin/server.dart
Serving at http://0.0.0.0:8080
Dart 後端會使用 shelf
和 shelf_router
提供 API 端點。根據預設,伺服器不會提供任何路徑。稍後您將建立路由,用於處理購買驗證程序。
範例程式碼中已包含的部分,是 lib/iap_repository.dart
中的 IapRepository
。由於學習如何與 Firestore 或一般資料庫互動,與本程式碼研究室無關,因此範例程式碼包含可在 Firestore 中建立或更新購買交易的函式,以及這些購買交易的所有類別。
設定 Firebase 存取權
如要存取 Firebase Firestore,您需要服務帳戶存取金鑰。如要產生金鑰,請開啟 Firebase 專案設定,前往「服務帳戶」部分,然後選取「產生新的私密金鑰」。
將下載的 JSON 檔案複製到 assets/
資料夾,並將檔案重新命名為 service-account-firebase.json
。
設定 Google Play 存取權
如要存取 Play 商店來驗證購買交易,您必須建立具有這些權限的服務帳戶,並下載該服務帳戶的 JSON 憑證。
- 前往 Google Play 管理中心,然後前往「所有應用程式」頁面。
- 依序前往「設定」>「API 存取權」。
如果 Google Play 管理中心要求您建立或連結現有專案,請先完成這項操作,再返回本頁。
- 找到可定義服務帳戶的部分,然後按一下「建立新的服務帳戶」。
- 在彈出式對話方塊中,按一下「Google Cloud Platform」連結。
- 選取所需的專案,如果找不到,請確認已登入正確的 Google 帳戶,方法是前往右上方的「帳戶」下拉式選單。
- 選取專案後,按一下頂端選單列中的「+ 建立服務帳戶」。
- 提供服務帳戶名稱,並視需要提供說明,以便您記得該帳戶的用途,然後繼續下一個步驟。
- 將編輯者角色指派給服務帳戶。
- 完成精靈後,請返回開發人員控制台的「API 存取權」頁面,然後按一下「重新整理服務帳戶」。清單中應會顯示新建立的帳戶。
- 按一下新服務帳戶的「授予存取權」。
- 向下捲動至下一個頁面,然後點選「財務資料」區塊。請同時選取「查看財務資料、訂單和取消訂閱問卷回覆情形」和「管理訂單和訂閱項目」。
- 按一下「邀請使用者」。
- 帳戶設定完成後,您只需要產生一些憑證。回到雲端控制台,在服務帳戶清單中找出您的服務帳戶,按一下三個垂直點,然後選擇「管理金鑰」。
- 建立新的 JSON 金鑰並下載。
- 將下載的檔案重新命名為
service-account-google-play.json,
,並移至assets/
目錄。
我們還需要做的一件事,就是開啟 lib/constants.dart,
,並將 androidPackageId
的值替換為您為 Android 應用程式選擇的套件 ID。
設定 Apple App Store 存取權
如要存取 App Store 來驗證購買交易,您必須設定共用密鑰:
- 開啟 App Store Connect。
- 前往「我的應用程式」,然後選取所需應用程式。
- 在側欄導覽中,依序前往「應用程式內購」>「管理」。
- 按一下清單右上方的「應用程式專屬共用金鑰」。
- 產生新的密鑰並複製。
- 開啟
lib/constants.dart,
,然後將appStoreSharedSecret
的值替換為您剛才產生的共用密鑰。
常數設定檔
繼續操作之前,請確認 lib/constants.dart
檔案中已設定下列常數:
androidPackageId
:Android 上使用的套件 ID,例如com.example.dashclicker
appStoreSharedSecret
:存取 App Store Connect 執行購買驗證的共用密鑰。bundleId
:iOS 上使用的軟體包 ID,例如com.example.dashclicker
您可以暫時忽略其他常數。
10. 驗證購買交易
iOS 和 Android 的一般購買驗證流程相似。
無論是哪個商店,應用程式都會在使用者購買時收到憑證。
應用程式會將這個權杖傳送至後端服務,後者會使用提供的權杖,向相應商店的伺服器驗證購買交易。
後端服務可以選擇儲存購買交易,並回覆應用程式是否有效。
請讓後端服務執行驗證作業,而非在使用者的裝置上執行應用程式,這樣一來,您就能防止使用者透過倒轉系統時鐘等方式存取付費功能。
設定 Flutter 端
設定驗證方法
您會將購買交易傳送至後端服務,因此請務必確保使用者在購買時已完成驗證。在入門專案中,我們已為您新增大部分驗證邏輯,您只需確保 PurchasePage
在使用者尚未登入時顯示登入按鈕即可。將下列程式碼新增至 PurchasePage
的建構方法開頭:
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
)、serverVerificationData
和 productID
。伺服器會傳回狀態碼,指出是否已驗證購買交易。
在應用程式常數中,將伺服器 IP 設定為本機機器的 IP 位址。
lib/logic/dash_purchases.dart
FirebaseNotifier firebaseNotifier;
DashPurchases(this.counter, this.firebaseNotifier) {
// omitted
}
在 main.dart:
中建立 DashPurchases
時,新增 firebaseNotifier
lib/main.dart
ChangeNotifierProvider<DashPurchases>(
create: (context) => DashPurchases(
context.read<DashCounter>(),
context.read<FirebaseNotifier>(),
),
lazy: false,
),
在 FirebaseNotifier 中為使用者新增 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) {
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);
}
}
應用程式現在已準備就緒,可以驗證購買交易。
設定後端服務
接下來,請設定雲端函式,在後端驗證購買交易。
建構購買處理常式
由於兩個商店的驗證流程幾乎相同,請設定抽象 PurchaseHandler
類別,並為每個商店實作個別實作項目。
首先,請將 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');
}
}
上述程式碼會執行以下作業:
- 定義將從先前建立的應用程式呼叫的 POST 端點。
- 解碼 JSON 酬載並擷取下列資訊:
userId
:目前登入的使用者 IDsource
:使用的儲存庫,為app_store
或google_play
。productData
:從先前建立的productDataMap
取得。token
:包含要傳送至商店的驗證資料。- 呼叫
verifyPurchase
方法,視來源而定,可能是GooglePlayPurchaseHandler
或AppStorePurchaseHandler
。 - 如果驗證成功,這個方法會將
Response.ok
傳回給用戶端。 - 如果驗證失敗,方法會將
Response.internalServerError
傳回給用戶端。
建立 API 端點後,您需要設定兩個購買處理常式。您必須載入先前步驟中取得的服務帳戶金鑰,並設定不同服務的存取權,包括 Android 發布者 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
方法中處理每則訊息。
這個方法會解碼傳入的訊息,並取得每筆交易 (包括訂閱和非訂閱) 的最新資訊,視需要呼叫現有的 handleSubscription
或 handleNonSubscription
。
每則訊息都必須透過 _askMessage
方法確認。
接著,請在 server.dart
檔案中新增必要的依附元件。將 PubsubApi.cloudPlatformScope 新增至憑證設定:
bin/server.dart
final clientGooglePlay =
await auth.clientViaServiceAccount(clientCredentialsGooglePlay, [
ap.AndroidPublisherApi.androidpublisherScope,
pubsub.PubsubApi.cloudPlatformScope, // new
]);
接著,建立 PubsubApi 例項:
bin/server.dart
final pubsubApi = pubsub.PubsubApi(clientGooglePlay);
最後,將其傳遞至 GooglePlayPurchaseHandler
建構函式:
bin/server.dart
return {
'google_play': GooglePlayPurchaseHandler(
androidPublisher,
iapRepository,
pubsubApi, // new
),
'app_store': AppStorePurchaseHandler(
iapRepository,
),
};
Google Play 設定
您已編寫程式碼,可從 Pub/Sub 主題使用帳單事件,但您尚未建立 Pub/Sub 主題,也未發布任何帳單事件。是時候設定了。
首先建立 Pub/Sub 主題:
- 前往 Google Cloud Console 的 Cloud Pub/Sub 頁面。
- 確認您已登入 Firebase 專案,然後按一下「+ 建立主題」。
- 為新主題命名,名稱必須與
constants.ts
中GOOGLE_PLAY_PUBSUB_BILLING_TOPIC
的值相同。在本例中,請將其命名為play_billing
。如果您選擇其他選項,請務必更新constants.ts
。建立主題。 - 在 Pub/Sub 主題清單中,按一下剛建立的主題旁的三個垂直排列的圓點,然後按一下「View permissions」(查看權限)。
- 在右側的側欄中,選擇「新增負責人」。
- 在此新增
google-play-developer-notifications@system.gserviceaccount.com
,並授予 Pub/Sub 發布者的角色。 - 儲存權限變更。
- 複製剛建立的主題的「主題名稱」。
- 再次開啟 Play 管理中心,然後從「所有應用程式」清單中選擇應用程式。
- 向下捲動,然後依序前往「營利」>「營利設定」。
- 填入完整主題並儲存變更。
所有 Google Play 帳單事件現在都會發布至這個主題。
處理 App Store 結帳事件
接著,請對 App Store 結帳事件執行相同的操作。您可以透過兩種有效方式,處理應用程式商店購買交易的更新。一種是實作您提供給 Apple 的 webhook,讓 Apple 用來與您的伺服器通訊。第二種方法是連線至 App Store Server API,並手動取得訂閱資訊。您會在本程式碼研究室中找到這項方法。
本程式碼研究室著重於第二個解決方案,因為您必須將伺服器公開至網際網路,才能導入 webhook。
在實際工作環境中,您最好同時使用這兩種方法。Webhook 可用於從 App Store 取得事件,而伺服器 API 則可用於在錯過事件或需要再次確認訂閱狀態時使用。
首先開啟 lib/app_store_purchase_handler.dart
,然後新增 AppStoreServerAPI 依附元件:
lib/app_store_purchase_handler.dart
final AppStoreServerAPI appStoreServerAPI;
AppStorePurchaseHandler(
this.iapRepository,
this.appStoreServerAPI, // new
)
修改建構函式,新增會呼叫 _pullStatus
方法的計時器。這個計時器會每隔 10 秒呼叫 _pullStatus
方法。你可以視需求調整計時器時間長度。
lib/app_store_purchase_handler.dart
AppStorePurchaseHandler(
this.iapRepository,
this.appStoreServerAPI,
) {
// Poll Subscription status every 10 seconds.
Timer.periodic(Duration(seconds: 10), (_) {
_pullStatus();
});
}
接著,請建立 _pullStatus 方法,如下所示:
lib/app_store_purchase_handler.dart
Future<void> _pullStatus() async {
print('Polling App Store');
final purchases = await iapRepository.getPurchases();
// filter for App Store subscriptions
final appStoreSubscriptions = purchases.where((element) =>
element.type == ProductType.subscription &&
element.iapSource == IAPSource.appstore);
for (final purchase in appStoreSubscriptions) {
final status =
await appStoreServerAPI.getAllSubscriptionStatuses(purchase.orderId);
// Obtain all subscriptions for the order id.
for (final subscription in status.data) {
// Last transaction contains the subscription status.
for (final transaction in subscription.lastTransactions) {
final expirationDate = DateTime.fromMillisecondsSinceEpoch(
transaction.transactionInfo.expiresDate ?? 0);
// Check if subscription has expired.
final isExpired = expirationDate.isBefore(DateTime.now());
print('Expiration Date: $expirationDate - isExpired: $isExpired');
// Update the subscription status with the new expiration date and status.
await iapRepository.updatePurchase(SubscriptionPurchase(
userId: null,
productId: transaction.transactionInfo.productId,
iapSource: IAPSource.appstore,
orderId: transaction.originalTransactionId,
purchaseDate: DateTime.fromMillisecondsSinceEpoch(
transaction.transactionInfo.originalPurchaseDate),
type: ProductType.subscription,
expiryDate: expirationDate,
status: isExpired
? SubscriptionStatus.expired
: SubscriptionStatus.active,
));
}
}
}
}
這個方法的運作方式如下:
- 使用 IapRepository 從 Firestore 取得有效訂閱項目清單。
- 針對每筆訂單,它會向 App Store Server API 要求訂閱狀態。
- 取得該訂閱項目購買交易的最後一筆交易。
- 檢查到期日。
- 更新 Firestore 上的訂閱狀態,如果訂閱已過期,系統會標示為過期。
最後,請新增所有必要程式碼來設定 App Store Server API 存取權:
bin/server.dart
// add from here
final subscriptionKeyAppStore =
File('assets/SubscriptionKey.p8').readAsStringSync();
// Configure Apple Store API access
var appStoreEnvironment = AppStoreEnvironment.sandbox(
bundleId: bundleId,
issuerId: appStoreIssuerId,
keyId: appStoreKeyId,
privateKey: subscriptionKeyAppStore,
);
// Stored token for Apple Store API access, if available
final file = File('assets/appstore.token');
String? appStoreToken;
if (file.existsSync() && file.lengthSync() > 0) {
appStoreToken = file.readAsStringSync();
}
final appStoreServerAPI = AppStoreServerAPI(
AppStoreServerHttpClient(
appStoreEnvironment,
jwt: appStoreToken,
jwtTokenUpdatedCallback: (token) {
file.writeAsStringSync(token);
},
),
);
// to here
return {
'google_play': GooglePlayPurchaseHandler(
androidPublisher,
iapRepository,
pubsubApi,
),
'app_store': AppStorePurchaseHandler(
iapRepository,
appStoreServerAPI, // new
),
};
App Store 設定
接著,設定 App Store:
- 登入 App Store Connect,然後選取「使用者和存取權」。
- 依序前往「金鑰類型」>「應用程式內購買」。
- 輕觸「加號」圖示即可新增。
- 為其命名,例如「Codelab key」。
- 下載含有金鑰的 p8 檔案。
- 將其複製到 assets 資料夾,並命名為
SubscriptionKey.p8
。 - 從新建立的金鑰複製金鑰 ID,並將其設為
lib/constants.dart
檔案中的appStoreKeyId
常數。 - 複製金鑰清單頂端的發出者 ID,並將其設為
lib/constants.dart
檔案中的appStoreIssuerId
常數。
追蹤裝置上的購買交易
追蹤購買交易最安全的方式是在伺服器端,因為用戶端難以確保安全性,但您必須設法將資訊傳回用戶端,讓應用程式能根據訂閱狀態資訊採取行動。只要將購買交易儲存在 Firestore 中,即可輕鬆將資料同步處理至用戶端,並自動保持更新。
您已在應用程式中加入 IAPRepo,這是包含 List<PastPurchase> purchases
中所有使用者購買資料的 Firestore 存放區。存放區也包含 hasActiveSubscription,
,如果有 productId storeKeySubscription
的消費交易,且狀態未過期,則為 true。如果使用者未登入,清單會顯示空白。
lib/repo/iap_repo.dart
void updatePurchases() {
_purchaseSubscription?.cancel();
var user = _user;
if (user == null) {
purchases = [];
hasActiveSubscription = false;
hasUpgrade = false;
return;
}
var purchaseStream = _firestore
.collection('purchases')
.where('userId', isEqualTo: user.uid)
.snapshots();
_purchaseSubscription = purchaseStream.listen((snapshot) {
purchases = snapshot.docs.map((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() {
_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,
中,applyPaidMultiplier
和 removePaidMultiplier
方法分別將乘數設為 10 或 1,因此您不必檢查訂閱項目是否已套用。訂閱狀態變更時,請一併更新可購買產品的狀態,以便在購買頁面中顯示產品已啟用。根據是否購買升級項目,設定 _beautifiedDashUpgrade
屬性。
lib/logic/dash_purchases.dart
void purchasesUpdate() {
var subscriptions = <PurchasableProduct>[];
var upgrades = <PurchasableProduct>[];
// Get a list of purchasable products for the subscription and upgrade.
// This should be 1 per type.
if (products.isNotEmpty) {
subscriptions = products
.where((element) => element.productDetails.id == storeKeySubscription)
.toList();
upgrades = products
.where((element) => element.productDetails.id == storeKeyUpgrade)
.toList();
}
// Set the subscription in the counter logic and show/hide purchased on the
// purchases page.
if (iapRepo.hasActiveSubscription) {
counter.applyPaidMultiplier();
for (var element in subscriptions) {
_updateStatus(element, ProductStatus.purchased);
}
} else {
counter.removePaidMultiplier();
for (var element in subscriptions) {
_updateStatus(element, ProductStatus.purchasable);
}
}
// Set the Dash beautifier and show/hide purchased on
// the purchases page.
if (iapRepo.hasUpgrade != _beautifiedDashUpgrade) {
_beautifiedDashUpgrade = iapRepo.hasUpgrade;
for (var element in upgrades) {
_updateStatus(
element,
_beautifiedDashUpgrade
? ProductStatus.purchased
: ProductStatus.purchasable);
}
notifyListeners();
}
}
void _updateStatus(PurchasableProduct product, ProductStatus status) {
if (product.status != ProductStatus.purchased) {
product.status = ProductStatus.purchased;
notifyListeners();
}
}
您現在已確保後端服務中的訂閱和升級狀態一律保持最新狀態,並與應用程式保持同步。應用程式會依此採取行動,並將訂閱和升級功能套用至 Dash 點擊遊戲。