Thêm tính năng mua hàng trong ứng dụng vào ứng dụng Flutter

Thêm tính năng mua hàng trong ứng dụng vào ứng dụng Flutter

Thông tin về lớp học lập trình này

subjectLần cập nhật gần đây nhất: thg 1 23, 2025
account_circleTác giả: Rene Floor, Bodhi Mulders, Jop Middelkamp, Miguel Beltran

1. Giới thiệu

Để thêm giao dịch mua hàng trong ứng dụng vào ứng dụng Flutter, bạn cần thiết lập chính xác Cửa hàng ứng dụng và Cửa hàng Play, xác minh giao dịch mua và cấp các quyền cần thiết, chẳng hạn như các đặc quyền của gói thuê bao.

Trong lớp học lập trình này, bạn sẽ thêm 3 loại giao dịch mua hàng trong ứng dụng vào một ứng dụng (được cung cấp cho bạn) và xác minh các giao dịch mua này bằng cách sử dụng phần phụ trợ Dart với Firebase. Ứng dụng được cung cấp, Dash Clicker, chứa một trò chơi sử dụng linh vật Dash làm đơn vị tiền tệ. Bạn sẽ thêm các lựa chọn mua hàng sau:

  1. Lựa chọn mua lặp lại 2.000 Dash cùng một lúc.
  2. Giao dịch mua nâng cấp một lần để chuyển Dash kiểu cũ thành Dash kiểu hiện đại.
  3. Gói thuê bao tăng gấp đôi số lượt nhấp được tạo tự động.

Lựa chọn mua hàng đầu tiên mang lại cho người dùng lợi ích trực tiếp là 2.000 Dash. Người dùng có thể mua trực tiếp các mặt hàng này và có thể mua nhiều lần. Đây được gọi là tài nguyên tiêu dùng vì được tiêu thụ trực tiếp và có thể được tiêu thụ nhiều lần.

Tuỳ chọn thứ hai sẽ nâng cấp Dash lên một Dash đẹp hơn. Bạn chỉ cần mua một lần và có thể sử dụng mãi mãi. Giao dịch mua như vậy được gọi là giao dịch mua không phải hàng tiêu dùng vì ứng dụng không thể sử dụng giao dịch mua đó nhưng giao dịch mua đó có hiệu lực vĩnh viễn.

Lựa chọn mua thứ ba và cuối cùng là gói thuê bao. Khi gói thuê bao đang hoạt động, người dùng sẽ nhận được Dash nhanh hơn, nhưng khi họ ngừng thanh toán cho gói thuê bao, các lợi ích cũng sẽ biến mất.

Dịch vụ phụ trợ (cũng được cung cấp cho bạn) chạy dưới dạng ứng dụng Dart, xác minh rằng các giao dịch mua đã được thực hiện và lưu trữ các giao dịch mua đó bằng Firestore. Firestore được dùng để giúp quá trình này dễ dàng hơn, nhưng trong ứng dụng chính thức, bạn có thể sử dụng bất kỳ loại dịch vụ phụ trợ nào.

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

Sản phẩm bạn sẽ tạo ra

  • Bạn sẽ mở rộng ứng dụng để hỗ trợ các giao dịch mua hàng tiêu dùng và gói thuê bao.
  • Bạn cũng sẽ mở rộng một ứng dụng phụ trợ Dart để xác minh và lưu trữ các mặt hàng đã mua.

Kiến thức bạn sẽ học được

  • Cách định cấu hình App Store và Cửa hàng Play cho các sản phẩm có thể mua.
  • Cách giao tiếp với các cửa hàng để xác minh giao dịch mua và lưu trữ các giao dịch mua đó trong Firestore.
  • Cách quản lý giao dịch mua trong ứng dụng.

Bạn cần có

  • Android Studio 4.1 trở lên
  • Xcode 12 trở lên (dành cho hoạt động phát triển iOS)
  • SDK Flutter

2. Thiết lập môi trường phát triển

Để bắt đầu lớp học lập trình này, hãy tải mã xuống và thay đổi giá trị nhận dạng gói cho iOS và tên gói cho Android.

Tải mã xuống

Để sao chép kho lưu trữ GitHub từ dòng lệnh, hãy sử dụng lệnh sau:

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

Hoặc nếu bạn đã cài đặt công cụ cli của GitHub, hãy sử dụng lệnh sau:

gh repo clone flutter/codelabs flutter-codelabs

Mã mẫu được nhân bản vào thư mục flutter-codelabs chứa mã cho một tập hợp các lớp học lập trình. Mã cho lớp học lập trình này nằm trong flutter-codelabs/in_app_purchases.

Cấu trúc thư mục trong flutter-codelabs/in_app_purchases chứa một loạt ảnh chụp nhanh về vị trí bạn sẽ ở cuối mỗi bước được đặt tên. Mã khởi động nằm ở bước 0, vì vậy, bạn có thể dễ dàng tìm thấy các tệp trùng khớp bằng cách:

cd flutter-codelabs/in_app_purchases/step_00

Nếu bạn muốn chuyển tiếp hoặc xem nội dung sau một bước, hãy tìm trong thư mục được đặt theo tên của bước mà bạn quan tâm. Mã của bước cuối cùng nằm trong thư mục complete.

Thiết lập dự án ban đầu

Mở dự án khởi động từ step_00 trong IDE mà bạn yêu thích. Chúng tôi đã sử dụng Android Studio để chụp ảnh màn hình, nhưng Visual Studio Code cũng là một lựa chọn tuyệt vời. Với bất kỳ trình chỉnh sửa nào, hãy đảm bảo bạn đã cài đặt các trình bổ trợ Dart và Flutter mới nhất.

Ứng dụng mà bạn sắp tạo cần giao tiếp với App Store và Cửa hàng Play để biết những sản phẩm nào có sẵn và giá của sản phẩm. Mỗi ứng dụng được xác định bằng một mã nhận dạng duy nhất. Đối với App Store của iOS, giá trị này được gọi là mã nhận dạng gói và đối với Cửa hàng Play của Android, giá trị này là mã ứng dụng. Các giá trị nhận dạng này thường được tạo bằng ký hiệu tên miền ngược. Ví dụ: khi tạo ứng dụng mua hàng trong ứng dụng cho flutter.dev, bạn sẽ sử dụng dev.flutter.inapppurchase. Hãy nghĩ đến một giá trị nhận dạng cho ứng dụng của bạn, bạn sẽ thiết lập giá trị đó trong phần cài đặt dự án.

Trước tiên, hãy thiết lập giá trị nhận dạng gói cho iOS.

Khi dự án đang mở trong Android Studio, hãy nhấp chuột phải vào thư mục iOS, nhấp vào Flutter rồi mở mô-đun trong ứng dụng Xcode.

942772eb9a73bfaa.png

Trong cấu trúc thư mục của Xcode, Dự án Runner (Dự án trình chạy) nằm ở trên cùng và các mục tiêu Flutter (Flutter), Runner (Trình chạy) và Products (Sản phẩm) nằm bên dưới dự án Runner. Nhấp đúp vào Runner (Trình chạy) để chỉnh sửa chế độ cài đặt dự án, rồi nhấp vào Signing & Capabilities (Ký và chức năng). Nhập giá trị nhận dạng gói mà bạn vừa chọn trong trường Nhóm để đặt nhóm.

812f919d965c649a.jpeg

Giờ đây, bạn có thể đóng Xcode và quay lại Android Studio để hoàn tất cấu hình cho Android. Để thực hiện việc này, hãy mở tệp build.gradle trong android/app, và thay đổi applicationId (trên dòng 37 trong ảnh chụp màn hình bên dưới) thành mã ứng dụng, giống với giá trị nhận dạng gói iOS. Xin lưu ý rằng mã nhận dạng cho cửa hàng iOS và Android không nhất thiết phải giống nhau, tuy nhiên việc giữ cho các mã nhận dạng này giống nhau sẽ ít gặp lỗi hơn. Do đó, trong lớp học lập trình này, chúng ta cũng sẽ sử dụng các giá trị nhận dạng giống nhau.

5c4733ac560ae8c2.png

3. Cài đặt trình bổ trợ

Trong phần này của lớp học lập trình, bạn sẽ cài đặt trình bổ trợ in_app_purchase.

Thêm phần phụ thuộc trong pubspec

Thêm in_app_purchase vào pubspec bằng cách thêm in_app_purchase vào các phần phụ thuộc trong pubspec:

$ cd app
$ flutter pub add in_app_purchase dev:in_app_purchase_platform_interface

Mở pubspec.yaml và xác nhận rằng bạn hiện đã có in_app_purchase được liệt kê dưới dạng mục trong dependenciesin_app_purchase_platform_interface trong 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

Nhấp vào pub get để tải gói xuống hoặc chạy flutter pub get trong dòng lệnh.

4. Thiết lập App Store

Để thiết lập và thử nghiệm giao dịch mua hàng trong ứng dụng trên iOS, bạn cần tạo một ứng dụng mới trong App Store và tạo sản phẩm có thể mua trong ứng dụng đó. Bạn không cần phải xuất bản nội dung nào hoặc gửi ứng dụng cho Apple để xem xét. Bạn cần có tài khoản nhà phát triển để thực hiện việc này. Nếu bạn chưa có tài khoản, hãy đăng ký chương trình dành cho nhà phát triển của Apple.

Để sử dụng tính năng mua hàng trong ứng dụng, bạn cũng cần có thoả thuận còn hiệu lực cho ứng dụng có tính phí trong App Store Connect. Truy cập vào https://appstoreconnect.apple.com/ rồi nhấp vào Thoả thuận, thuế và ngân hàng.

11db9fca823e7608.png

Tại đây, bạn sẽ thấy các thoả thuận dành cho ứng dụng miễn phí và có tính phí. Trạng thái của ứng dụng miễn phí phải là đang hoạt động và trạng thái của ứng dụng có tính phí phải là mới. Hãy nhớ xem, chấp nhận các điều khoản và nhập tất cả thông tin bắt buộc.

74c73197472c9aec.png

Khi bạn thiết lập mọi thứ đúng cách, trạng thái của ứng dụng có tính phí sẽ là đang hoạt động. Điều này rất quan trọng vì bạn sẽ không thể thử mua hàng trong ứng dụng nếu không có thoả thuận đang hoạt động.

4a100bbb8cafdbbf.jpeg

Đăng ký mã ứng dụng

Tạo một giá trị nhận dạng mới trong cổng thông tin dành cho nhà phát triển của Apple.

55d7e592d9a3fc7b.png

Chọn Mã ứng dụng

13f125598b72ca77.png

Chọn ứng dụng

41ac4c13404e2526.png

Cung cấp một số nội dung mô tả và đặt mã nhận dạng gói sao cho khớp với mã nhận dạng gói đã đặt trước đó trong XCode.

9d2c940ad80deeef.png

Để biết thêm hướng dẫn về cách tạo mã ứng dụng mới, hãy xem phần Trợ giúp về tài khoản nhà phát triển .

Tạo ứng dụng mới

Tạo một ứng dụng mới trong App Store Connect bằng giá trị nhận dạng gói duy nhất của bạn.

10509b17fbf031bd.png

5b7c0bb684ef52c7.png

Để biết thêm hướng dẫn về cách tạo ứng dụng mới và quản lý thoả thuận, hãy xem phần Trợ giúp về App Store Connect.

Để kiểm thử giao dịch mua hàng trong ứng dụng, bạn cần có người dùng thử nghiệm trong hộp cát. Người dùng thử nghiệm này không được kết nối với iTunes – người dùng này chỉ được dùng để kiểm thử giao dịch mua hàng trong ứng dụng. Bạn không thể sử dụng địa chỉ email đã được dùng cho một tài khoản Apple. Trong phần Người dùng và quyền truy cập, hãy chuyển đến mục Người kiểm thử trong phần Sandbox để tạo tài khoản hộp cát mới hoặc quản lý mã Apple ID hộp cát hiện có.

3ca2b26d4e391a4c.jpeg

Giờ đây, bạn có thể thiết lập người dùng hộp cát trên iPhone bằng cách chuyển đến phần Cài đặt > App Store > Sandbox-account (Cài đặt > App Store > Tài khoản hộp cát).

d99e0b89673867cd.jpeg e1621bcaeb33d3c5.jpeg

Định cấu hình giao dịch mua hàng trong ứng dụng

Bây giờ, bạn sẽ định cấu hình 3 mặt hàng có thể mua:

  • dash_consumable_2k: Giao dịch mua hàng tiêu dùng có thể mua nhiều lần, cấp cho người dùng 2.000 Dash (đơn vị tiền tệ trong ứng dụng) cho mỗi giao dịch mua.
  • dash_upgrade_3d: Giao dịch mua "nâng cấp" không phải hàng tiêu dùng, chỉ có thể mua một lần và cung cấp cho người dùng một Dash khác về mặt thẩm mỹ để nhấp vào.
  • dash_subscription_doubler: Gói thuê bao cấp cho người dùng số lượng Dash gấp đôi cho mỗi lượt nhấp trong thời gian thuê bao.

d156b2f5bac43ca8.png

Chuyển đến phần Giao dịch mua hàng trong ứng dụng > Quản lý.

Tạo giao dịch mua hàng trong ứng dụng bằng mã nhận dạng được chỉ định:

  1. Thiết lập dash_consumable_2k làm Mặt hàng tiêu dùng.

Sử dụng dash_consumable_2k làm Mã sản phẩm. Tên tham chiếu chỉ được dùng trong App Store Connect, bạn chỉ cần đặt tên này thành dash consumable 2k và thêm bản địa hoá cho giao dịch mua. Gọi giao dịch mua Spring is in the air với nội dung mô tả là 2000 dashes fly out.

ec1701834fd8527.png

  1. Thiết lập dash_upgrade_3d làm Không phải hàng tiêu dùng.

Sử dụng dash_upgrade_3d làm Mã sản phẩm. Đặt tên tham chiếu thành dash upgrade 3d và thêm nội dung bản địa hoá cho giao dịch mua. Gọi giao dịch mua 3D Dash với nội dung mô tả là Brings your dash back to the future.

6765d4b711764c30.png

  1. Thiết lập dash_subscription_doubler làm Gói thuê bao tự động gia hạn.

Quy trình cho gói thuê bao có đôi chút khác biệt. Trước tiên, bạn phải đặt Tên tham chiếu và Mã sản phẩm:

6d29e08dae26a0c4.png

Tiếp theo, bạn phải tạo một nhóm gói thuê bao. Khi nhiều gói thuê bao thuộc cùng một nhóm, người dùng chỉ có thể đăng ký một trong các gói thuê bao đó cùng một lúc, nhưng có thể dễ dàng nâng cấp hoặc hạ cấp giữa các gói thuê bao này. Bạn chỉ cần gọi nhóm này là subscriptions.

5bd0da17a85ac076.png

Tiếp theo, hãy nhập thời hạn của gói thuê bao và các bản địa hoá. Đặt tên cho gói thuê bao này là Jet Engine với nội dung mô tả là Doubles your clicks. Nhấp vào Lưu.

bd1b1d82eeee4cb3.png

Sau khi nhấp vào nút Lưu, hãy thêm giá gói thuê bao. Chọn bất kỳ mức giá nào bạn muốn.

d0bf39680ef0aa2e.png

Lúc này, bạn sẽ thấy 3 giao dịch mua trong danh sách giao dịch mua:

99d5c4b446e8fecf.png

5. Thiết lập Cửa hàng Play

Cũng giống như App Store, bạn cũng cần có tài khoản nhà phát triển cho Cửa hàng Play. Nếu bạn chưa có tài khoản, hãy đăng ký tài khoản.

Tạo ứng dụng mới

Tạo ứng dụng mới trong Google Play Console:

  1. Mở Play Console.
  2. Chọn Tất cả ứng dụng > Tạo ứng dụng.
  3. Chọn ngôn ngữ mặc định và thêm tiêu đề cho ứng dụng. Nhập tên của ứng dụng mà bạn muốn xuất hiện trên Google Play. Bạn có thể đổi tên chỉ số vào lúc khác.
  4. Chỉ định rằng ứng dụng của bạn là trò chơi. Bạn có thể thay đổi những thông tin này về sau.
  5. Chỉ định rõ ứng dụng của bạn là ứng dụng miễn phí hay có tính phí.
  6. Thêm một địa chỉ email mà người dùng ứng dụng trên Cửa hàng Play có thể sử dụng để liên hệ với bạn về ứng dụng này.
  7. Hoàn tất thông tin khai báo theo Nguyên tắc nội dung và luật xuất khẩu của Hoa Kỳ.
  8. Chọn Tạo ứng dụng.

Sau khi tạo ứng dụng, hãy chuyển đến trang tổng quan và hoàn tất tất cả các nhiệm vụ trong phần Thiết lập ứng dụng. Tại đây, bạn cung cấp một số thông tin về ứng dụng của mình, chẳng hạn như mức phân loại nội dung và ảnh chụp màn hình. 13845badcf9bc1db.png

Ký ứng dụng

Để có thể kiểm thử giao dịch mua hàng trong ứng dụng, bạn cần tải ít nhất một bản dựng lên Google Play.

Để làm việc này, bạn cần ký bản phát hành bằng một khoá khác ngoài khoá gỡ lỗi.

Tạo kho khoá

Nếu bạn đã có kho khoá, hãy chuyển sang bước tiếp theo. Nếu không, hãy tạo một tệp bằng cách chạy lệnh sau trên dòng lệnh.

Trên Mac/Linux, hãy sử dụng lệnh sau:

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

Trên Windows, hãy sử dụng lệnh sau:

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

Lệnh này lưu trữ tệp key.jks trong thư mục gốc. Nếu bạn muốn lưu trữ tệp ở nơi khác, hãy thay đổi đối số bạn truyền vào tham số -keystore. Giữ lại

keystore

tệp riêng tư; đừng kiểm tra tệp đó vào hệ thống kiểm soát nguồn công khai!

Tham chiếu kho khoá từ ứng dụng

Tạo một tệp có tên là <your app dir>/android/key.properties chứa thông tin tham chiếu đến kho khoá:

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>

Định cấu hình tính năng ký trong Gradle

Định cấu hình tính năng ký cho ứng dụng bằng cách chỉnh sửa tệp <your app dir>/android/app/build.gradle.

Thêm thông tin kho khoá từ tệp thuộc tính trước khối android:

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

   android {
         // omitted
   }

Tải tệp key.properties vào đối tượng keystoreProperties.

Thêm mã sau vào trước khối 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
       
}
   
}

Định cấu hình khối signingConfigs trong tệp build.gradle của mô-đun bằng thông tin cấu hình ký:

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

Giờ đây, các bản phát hành của ứng dụng sẽ được ký tự động.

Để biết thêm thông tin về việc ký ứng dụng, hãy xem bài viết Ký ứng dụng trên developer.android.com.

Tải bản dựng đầu tiên lên

Sau khi định cấu hình ứng dụng để ký, bạn có thể tạo ứng dụng bằng cách chạy:

flutter build appbundle

Theo mặc định, lệnh này sẽ tạo một bản phát hành và bạn có thể tìm thấy kết quả tại <your app dir>/build/app/outputs/bundle/release/

Trên trang tổng quan trong Google Play Console, hãy chuyển đến phần Bản phát hành > Thử nghiệm > Thử nghiệm khép kín rồi tạo một bản phát hành thử nghiệm khép kín mới.

Trong lớp học lập trình này, bạn sẽ sử dụng tính năng ký ứng dụng của Google. Vì vậy, hãy nhấn vào Tiếp tục trong phần Tính năng ký ứng dụng của Play để chọn sử dụng tính năng này.

ba98446d9c5c40e0.png

Tiếp theo, hãy tải gói ứng dụng app-release.aab được tạo bằng lệnh bản dựng lên.

Nhấp vào Lưu, rồi nhấp vào Xem lại bản phát hành.

Cuối cùng, hãy nhấp vào Bắt đầu phát hành cho Kiểm thử nội bộ để kích hoạt bản phát hành kiểm thử nội bộ.

Thiết lập người dùng thử nghiệm

Để có thể kiểm thử giao dịch mua hàng trong ứng dụng, bạn phải thêm tài khoản Google của người kiểm thử vào Google Play Console ở hai vị trí:

  1. Đến kênh kiểm thử cụ thể (Kiểm thử nội bộ)
  2. Là người kiểm thử giấy phép

Trước tiên, hãy bắt đầu bằng cách thêm người kiểm thử vào kênh kiểm thử nội bộ. Quay lại phần Bản phát hành > Thử nghiệm > Thử nghiệm nội bộ rồi nhấp vào thẻ Người kiểm thử.

a0d0394e85128f84.png

Tạo danh sách email mới bằng cách nhấp vào Tạo danh sách email. Đặt tên cho danh sách và thêm địa chỉ email của những Tài khoản Google cần quyền truy cập để thử nghiệm giao dịch mua hàng trong ứng dụng.

Tiếp theo, hãy đánh dấu vào hộp kiểm của danh sách rồi nhấp vào Lưu thay đổi.

Sau đó, hãy thêm người kiểm thử giấy phép:

  1. Quay lại chế độ xem Tất cả ứng dụng trên Google Play Console.
  2. Chuyển đến phần Cài đặt > Kiểm thử giấy phép.
  3. Thêm cùng một địa chỉ email của những người thử nghiệm cần kiểm thử giao dịch mua hàng trong ứng dụng.
  4. Đặt License response (Phản hồi giấy phép) thành RESPOND_NORMALLY.
  5. Nhấp vào Lưu thay đổi.

a1a0f9d3e55ea8da.png

Định cấu hình giao dịch mua hàng trong ứng dụng

Bây giờ, bạn sẽ định cấu hình các mặt hàng có thể mua trong ứng dụng.

Cũng giống như trong App Store, bạn phải xác định 3 giao dịch mua:

  • dash_consumable_2k: Giao dịch mua hàng tiêu dùng có thể mua nhiều lần, cấp cho người dùng 2.000 Dash (đơn vị tiền tệ trong ứng dụng) cho mỗi giao dịch mua.
  • dash_upgrade_3d: Giao dịch mua "nâng cấp" không phải hàng tiêu dùng, chỉ có thể mua một lần, giúp người dùng có một Dash khác về mặt thẩm mỹ để nhấp vào.
  • dash_subscription_doubler: Gói thuê bao cấp cho người dùng số lượng Dash gấp đôi cho mỗi lượt nhấp trong thời gian thuê bao.

Trước tiên, hãy thêm sản phẩm tiêu dùng và sản phẩm không tiêu dùng.

  1. Truy cập vào Google Play Console rồi chọn ứng dụng của bạn.
  2. Chuyển đến phần Kiếm tiền > Sản phẩm > Sản phẩm trong ứng dụng.
  3. Nhấp vào Tạo sản phẩmc8d66e32f57dee21.png
  4. Nhập tất cả thông tin bắt buộc cho sản phẩm của bạn. Đảm bảo mã sản phẩm khớp chính xác với mã bạn định sử dụng.
  5. Nhấp vào Lưu.
  6. Nhấp vào Kích hoạt.
  7. Lặp lại quy trình này cho giao dịch mua "nâng cấp" không phải hàng tiêu dùng.

Tiếp theo, hãy thêm gói thuê bao:

  1. Truy cập vào Google Play Console rồi chọn ứng dụng của bạn.
  2. Chuyển đến phần Kiếm tiền > Sản phẩm > Gói thuê bao.
  3. Nhấp vào Tạo gói thuê bao32a6a9eefdb71dd0.png
  4. Nhập tất cả thông tin bắt buộc cho gói thuê bao của bạn. Đảm bảo mã sản phẩm khớp chính xác với mã bạn định sử dụng.
  5. Nhấp vào Lưu

Giờ đây, bạn đã thiết lập xong giao dịch mua trong Play Console.

6. Thiết lập Firebase

Trong lớp học lập trình này, bạn sẽ sử dụng một dịch vụ phụ trợ để xác minh và theo dõi giao dịch mua của người dùng.

Việc sử dụng dịch vụ phụ trợ mang lại một số lợi ích:

  • Bạn có thể xác minh giao dịch một cách an toàn.
  • Bạn có thể phản hồi các sự kiện thanh toán từ cửa hàng ứng dụng.
  • Bạn có thể theo dõi các giao dịch mua hàng trong cơ sở dữ liệu.
  • Người dùng sẽ không thể lừa ứng dụng của bạn cung cấp các tính năng nâng cao bằng cách tua lại đồng hồ hệ thống.

Mặc dù có nhiều cách để thiết lập dịch vụ phụ trợ, nhưng bạn sẽ thực hiện việc này bằng cách sử dụng các hàm trên đám mây và Firestore, thông qua Firebase của chính Google.

Việc viết phần phụ trợ được coi là nằm ngoài phạm vi của lớp học lập trình này, vì vậy, mã khởi động đã bao gồm một dự án Firebase xử lý các giao dịch mua cơ bản để bạn bắt đầu.

Trình bổ trợ Firebase cũng được đưa vào ứng dụng khởi động.

Việc còn lại là tạo dự án Firebase của riêng bạn, định cấu hình cả ứng dụng và phần phụ trợ cho Firebase, sau đó triển khai phần phụ trợ.

Tạo dự án Firebase

Truy cập vào bảng điều khiển của Firebase rồi tạo một dự án Firebase mới. Trong ví dụ này, hãy gọi dự án là Dash Clicker.

Trong ứng dụng phụ trợ, bạn liên kết giao dịch mua với một người dùng cụ thể, do đó, bạn cần xác thực. Để làm việc này, hãy tận dụng mô-đun xác thực của Firebase bằng tính năng đăng nhập bằng Google.

  1. Trên trang tổng quan Firebase, hãy chuyển đến phần Xác thực rồi bật tính năng này nếu cần.
  2. Chuyển đến thẻ Phương thức đăng nhập rồi bật nhà cung cấp dịch vụ đăng nhập Google.

7babb48832fbef29.png

Vì bạn cũng sẽ sử dụng cơ sở dữ liệu Firestore của Firebase, nên hãy bật tuỳ chọn này.

e20553e0de5ac331.png

Thiết lập các quy tắc Cloud Firestore như sau:

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

Thiết lập Firebase cho Flutter

Bạn nên sử dụng FlutterFire CLI để cài đặt Firebase trên ứng dụng Flutter. Làm theo hướng dẫn như được giải thích trong trang thiết lập.

Khi chạy flutterfire configure, hãy chọn dự án bạn vừa tạo ở bước trước.

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

Tiếp theo, hãy bật iOSAndroid bằng cách chọn hai nền tảng này.

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

Khi được nhắc về việc ghi đè firebase_options.dart, hãy chọn yes (có).

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

Thiết lập Firebase cho Android: Các bước tiếp theo

Trên trang tổng quan của Firebase, hãy chuyển đến phần Tổng quan về dự án,chọn Cài đặt rồi chọn thẻ Chung.

Di chuyển xuống phần Ứng dụng của bạn rồi chọn ứng dụng dashclicker (android).

b22d46a759c0c834.png

Để cho phép đăng nhập bằng Google ở chế độ gỡ lỗi, bạn phải cung cấp dấu vân tay băm SHA-1 của chứng chỉ gỡ lỗi.

Lấy hàm băm chứng chỉ ký gỡ lỗi

Trong thư mục gốc của dự án ứng dụng Flutter, hãy thay đổi thư mục thành thư mục android/, sau đó tạo báo cáo ký.

cd android
./gradlew :app:signingReport

Bạn sẽ thấy một danh sách lớn các khoá ký. Vì bạn đang tìm hàm băm cho chứng chỉ gỡ lỗi, hãy tìm chứng chỉ có thuộc tính VariantConfig được đặt thành debug. Kho khoá có thể nằm trong thư mục gốc của bạn trong .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

Sao chép hàm băm SHA-1 rồi điền vào trường cuối cùng trong hộp thoại phương thức gửi ứng dụng.

Thiết lập Firebase cho iOS: Các bước tiếp theo

Mở ios/Runnder.xcworkspace bằng Xcode. Hoặc bằng IDE mà bạn chọn.

Trên VSCode, hãy nhấp chuột phải vào thư mục ios/ rồi nhấp vào open in xcode.

Trên Android Studio, hãy nhấp chuột phải vào thư mục ios/, sau đó nhấp vào flutter, rồi nhấp vào tuỳ chọn open iOS module in Xcode.

Để cho phép đăng nhập bằng Google trên iOS, hãy thêm tuỳ chọn cấu hình CFBundleURLTypes vào các tệp plist của bản dựng. (Hãy xem tài liệu về gói google_sign_in để biết thêm thông tin.) Trong trường hợp này, các tệp là ios/Runner/Info-Debug.plistios/Runner/Info-Release.plist.

Cặp khoá-giá trị đã được thêm, nhưng bạn phải thay thế các giá trị của cặp này:

  1. Lấy giá trị cho REVERSED_CLIENT_ID từ tệp GoogleService-Info.plist, mà không có phần tử <string>..</string> bao quanh.
  2. Thay thế giá trị trong cả tệp ios/Runner/Info-Debug.plistios/Runner/Info-Release.plist theo khoá 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>

Bạn đã hoàn tất việc thiết lập Firebase.

7. Nghe thông tin cập nhật về giao dịch mua

Trong phần này của lớp học lập trình, bạn sẽ chuẩn bị ứng dụng để mua sản phẩm. Quá trình này bao gồm việc theo dõi thông tin cập nhật về giao dịch mua và lỗi sau khi ứng dụng khởi động.

Nghe thông tin cập nhật về giao dịch mua

Trong main.dart,, hãy tìm tiện ích MyHomePageScaffold với BottomNavigationBar chứa hai trang. Trang này cũng tạo ba Provider cho DashCounter, DashUpgrades,DashPurchases. DashCounter theo dõi số lượng Dấu gạch ngang hiện tại và tự động tăng số lượng đó. DashUpgrades quản lý các bản nâng cấp mà bạn có thể mua bằng Dashes. Lớp học lập trình này tập trung vào DashPurchases.

Theo mặc định, đối tượng của nhà cung cấp được xác định khi đối tượng đó được yêu cầu lần đầu tiên. Đối tượng này trực tiếp theo dõi các bản cập nhật giao dịch mua khi ứng dụng khởi động, vì vậy, hãy tắt tính năng tải lười trên đối tượng này bằng lazy: false:

lib/main.dart

ChangeNotifierProvider<DashPurchases>(
  create
: (context) => DashPurchases(
    context
.read<DashCounter>(),
 
),
  lazy
: false,                                             // Add this line
),

Bạn cũng cần một thực thể của InAppPurchaseConnection. Tuy nhiên, để có thể kiểm thử ứng dụng, bạn cần có một số cách để mô phỏng kết nối. Để thực hiện việc này, hãy tạo một phương thức thực thể có thể được ghi đè trong kiểm thử và thêm phương thức đó vào 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!;
  }
}

Bạn phải cập nhật một chút mã kiểm thử nếu muốn mã kiểm thử tiếp tục hoạt động.

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.

Trong lib/logic/dash_purchases.dart, hãy chuyển đến mã cho DashPurchases ChangeNotifier. Hiện tại, bạn chỉ có thể thêm DashCounter vào các Dash đã mua.

Thêm thuộc tính đăng ký luồng, _subscription (loại StreamSubscription<List<PurchaseDetails>> _subscription;), IAPConnection.instance, và các tệp nhập. Mã kết quả sẽ có dạng như sau:

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

Từ khoá late được thêm vào _subscription_subscription được khởi tạo trong hàm khởi tạo. Dự án này được thiết lập để không có giá trị rỗng theo mặc định (NNBD), nghĩa là các thuộc tính không được khai báo là có thể rỗng phải có giá trị không rỗng. Bộ hạn định late cho phép bạn trì hoãn việc xác định giá trị này.

Trong hàm khởi tạo, hãy lấy luồng purchaseUpdated và bắt đầu nghe luồng đó. Trong phương thức dispose(), hãy huỷ gói thuê bao luồng.

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.
}

Bây giờ, ứng dụng sẽ nhận được thông tin cập nhật về giao dịch mua. Vì vậy, trong phần tiếp theo, bạn sẽ thực hiện một giao dịch mua!

Trước khi tiếp tục, hãy chạy các chương trình kiểm thử bằng "flutter test" để xác minh mọi thứ đã được thiết lập chính xác.

$ flutter test

00:01 +1: All tests passed!                                                                                  

8. Mua hàng

Trong phần này của lớp học lập trình, bạn sẽ thay thế các sản phẩm mô phỏng hiện có bằng các sản phẩm thực tế có thể mua. Những sản phẩm này được tải từ các cửa hàng, hiển thị trong danh sách và được mua khi người dùng nhấn vào sản phẩm.

Điều chỉnh PurchasableProduct

PurchasableProduct hiển thị một sản phẩm mô phỏng. Cập nhật để hiển thị nội dung thực tế bằng cách thay thế lớp PurchasableProduct trong purchasable_product.dart bằng mã sau:

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

Trong dash_purchases.dart,, hãy xoá các giao dịch mua giả và thay thế bằng một danh sách trống, List<PurchasableProduct> products = [];

Tải các giao dịch mua hiện có

Để cho phép người dùng mua hàng, hãy tải các giao dịch mua từ cửa hàng. Trước tiên, hãy kiểm tra xem cửa hàng có hoạt động hay không. Khi không có cửa hàng, việc đặt storeState thành notAvailable sẽ hiển thị thông báo lỗi cho người dùng.

lib/logic/dash_purchases.dart

  Future<void> loadPurchases() async {
    final available = await iapConnection.isAvailable();
    if (!available) {
      storeState = StoreState.notAvailable;
      notifyListeners();
      return;
    }
  }

Khi có cửa hàng, hãy tải các giao dịch mua hiện có. Với chế độ thiết lập Firebase trước đó, bạn sẽ thấy storeKeyConsumable, storeKeySubscription,storeKeyUpgrade. Khi không có giao dịch mua dự kiến, hãy in thông tin này vào bảng điều khiển; bạn cũng có thể gửi thông tin này đến dịch vụ phụ trợ.

Phương thức await iapConnection.queryProductDetails(ids) trả về cả mã không tìm thấy và sản phẩm có thể mua được. Sử dụng productDetails từ phản hồi để cập nhật giao diện người dùng và đặt StoreState thành 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();
 
}

Gọi hàm loadPurchases() trong hàm khởi tạo:

lib/logic/dash_purchases.dart

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

Cuối cùng, hãy thay đổi giá trị của trường storeState từ StoreState.available thành StoreState.loading:

lib/logic/dash_purchases.dart

StoreState storeState = StoreState.loading;

Hiển thị các sản phẩm có thể mua

Hãy xem xét tệp purchase_page.dart. Tiện ích PurchasePage hiển thị _PurchasesLoading, _PurchaseList, hoặc _PurchasesNotAvailable, tuỳ thuộc vào StoreState. Tiện ích này cũng cho thấy các giao dịch mua trước đây của người dùng được sử dụng ở bước tiếp theo.

Tiện ích _PurchaseList hiển thị danh sách các sản phẩm có thể mua và gửi yêu cầu mua đến đối tượng 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(),
    );
  }
}

Bạn sẽ thấy các sản phẩm có sẵn trên cửa hàng Android và iOS nếu các sản phẩm đó được thiết lập đúng cách. Xin lưu ý rằng có thể mất chút thời gian thì giao dịch mua mới có hiệu lực khi được nhập vào bảng điều khiển tương ứng.

ca1a9f97c21e552d.png

Quay lại dash_purchases.dart và triển khai hàm để mua sản phẩm. Bạn chỉ cần tách biệt hàng tiêu dùng với hàng không phải hàng tiêu dùng. Bản nâng cấp và sản phẩm thuê bao không phải là sản phẩm tiêu dùng.

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

Trước khi tiếp tục, hãy tạo biến _beautifiedDashUpgrade và cập nhật phương thức getter beautifiedDash để tham chiếu đến biến đó.

lib/logic/dash_purchases.dart

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

Phương thức _onPurchaseUpdate nhận thông tin cập nhật về giao dịch mua, cập nhật trạng thái của sản phẩm xuất hiện trong trang mua hàng và áp dụng giao dịch mua cho logic bộ đếm. Bạn cần gọi completePurchase sau khi xử lý giao dịch mua để cửa hàng biết rằng giao dịch mua đã được xử lý đúng cách.

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. Thiết lập phần phụ trợ

Trước khi chuyển sang theo dõi và xác minh giao dịch mua, hãy thiết lập phần phụ trợ Dart để hỗ trợ việc này.

Trong phần này, hãy làm việc từ thư mục dart-backend/ làm thư mục gốc.

Đảm bảo bạn đã cài đặt các công cụ sau:

Tổng quan về dự án cơ sở

Vì một số phần của dự án này được coi là nằm ngoài phạm vi của lớp học lập trình này, nên chúng được đưa vào mã khởi động. Bạn nên xem lại những gì đã có trong mã khởi động trước khi bắt đầu để biết cách bạn sẽ sắp xếp mọi thứ.

Mã phụ trợ này có thể chạy cục bộ trên máy của bạn, bạn không cần triển khai mã này để sử dụng. Tuy nhiên, bạn cần có thể kết nối từ thiết bị phát triển (Android hoặc iPhone) với máy sẽ chạy máy chủ. Để làm được việc đó, các thiết bị phải nằm trong cùng một mạng và bạn cần biết địa chỉ IP của máy.

Hãy thử chạy máy chủ bằng lệnh sau:

$ dart ./bin/server.dart

Serving at http://0.0.0.0:8080

Phần phụ trợ Dart sử dụng shelfshelf_router để phân phát các điểm cuối API. Theo mặc định, máy chủ không cung cấp bất kỳ tuyến nào. Sau đó, bạn sẽ tạo một tuyến để xử lý quy trình xác minh giao dịch mua.

Một phần đã có trong mã khởi động là IapRepository trong lib/iap_repository.dart. Vì việc tìm hiểu cách tương tác với Firestore hoặc cơ sở dữ liệu nói chung không được coi là phù hợp với lớp học lập trình này, nên mã khởi động chứa các hàm để bạn tạo hoặc cập nhật giao dịch mua trong Firestore, cũng như tất cả các lớp cho các giao dịch mua đó.

Thiết lập quyền truy cập vào Firebase

Để truy cập vào Firebase Firestore, bạn cần có khoá truy cập vào tài khoản dịch vụ. Tạo một khoá bằng cách mở phần cài đặt dự án Firebase rồi chuyển đến phần Tài khoản dịch vụ, sau đó chọn Tạo khoá riêng tư mới.

27590fc77ae94ad4.png

Sao chép tệp JSON đã tải xuống vào thư mục assets/ rồi đổi tên tệp đó thành service-account-firebase.json.

Thiết lập quyền truy cập vào Google Play

Để truy cập vào Cửa hàng Play nhằm xác minh giao dịch mua, bạn phải tạo một tài khoản dịch vụ có các quyền này và tải thông tin xác thực JSON xuống cho tài khoản đó.

  1. Truy cập vào Google Play Console rồi bắt đầu từ trang Tất cả ứng dụng.
  2. Chuyển đến phần Thiết lập > Quyền truy cập API. 317fdfb54921f50e.png Trong trường hợp Google Play Console yêu cầu bạn tạo hoặc liên kết với một dự án hiện có, hãy làm việc đó trước rồi quay lại trang này.
  3. Tìm phần mà bạn có thể xác định tài khoản dịch vụ rồi nhấp vào Tạo tài khoản dịch vụ mới.1e70d3f8d794bebb.png
  4. Nhấp vào đường liên kết Google Cloud Platform trong hộp thoại bật lên. 7c9536336dd9e9b4.png
  5. Chọn dự án của bạn. Nếu bạn không thấy, hãy đảm bảo rằng bạn đã đăng nhập vào đúng Tài khoản Google trong danh sách thả xuống Tài khoản ở trên cùng bên phải. 3fb3a25bad803063.png
  6. Sau khi chọn dự án, hãy nhấp vào + Tạo tài khoản dịch vụ trong thanh trình đơn trên cùng. 62fe4c3f8644acd8.png
  7. Đặt tên cho tài khoản dịch vụ, cung cấp nội dung mô tả (không bắt buộc) để bạn nhớ được mục đích của tài khoản đó rồi chuyển sang bước tiếp theo. 8a92d5d6a3dff48c.png
  8. Chỉ định vai trò Người chỉnh sửa cho tài khoản dịch vụ. 6052b7753667ed1a.png
  9. Hoàn tất trình hướng dẫn, quay lại trang Quyền truy cập API trong bảng điều khiển dành cho nhà phát triển rồi nhấp vào Làm mới tài khoản dịch vụ. Bạn sẽ thấy tài khoản mới tạo của mình trong danh sách. 5895a7db8b4c7659.png
  10. Nhấp vào Cấp quyền truy cập cho tài khoản dịch vụ mới.
  11. Cuộn xuống trang tiếp theo, đến khối Dữ liệu tài chính. Chọn cả Xem dữ liệu tài chính, đơn đặt hàng và phản hồi trong bản khảo sát về quyết định huỷ gói thuê baoQuản lý đơn đặt hàng và gói thuê bao. 75b22d0201cf67e.png
  12. Nhấp vào Mời người dùng. 70ea0b1288c62a59.png
  13. Giờ đây, khi tài khoản đã được thiết lập, bạn chỉ cần tạo một số thông tin xác thực. Quay lại bảng điều khiển Google Cloud, hãy tìm tài khoản dịch vụ của bạn trong danh sách tài khoản dịch vụ, nhấp vào biểu tượng ba dấu chấm dọc rồi chọn Quản lý khoá. 853ee186b0e9954e.png
  14. Tạo khoá JSON mới và tải khoá đó xuống. 2a33a55803f5299c.png cb4bf48ebac0364e.png
  15. Đổi tên tệp đã tải xuống thành service-account-google-play.json, rồi di chuyển tệp đó vào thư mục assets/.

Một việc nữa chúng ta cần làm là mở lib/constants.dart, và thay thế giá trị của androidPackageId bằng mã gói mà bạn đã chọn cho ứng dụng Android.

Thiết lập quyền truy cập vào Apple App Store

Để truy cập vào App Store nhằm xác minh giao dịch mua, bạn phải thiết lập một khoá bí mật dùng chung:

  1. Mở App Store Connect.
  2. Chuyển đến phần Ứng dụng của tôi rồi chọn ứng dụng của bạn.
  3. Trong bảng điều hướng thanh bên, hãy chuyển đến Giao dịch mua hàng trong ứng dụng > Quản lý.
  4. Ở trên cùng bên phải danh sách, hãy nhấp vào Khoá bí mật dùng chung dành riêng cho ứng dụng.
  5. Tạo một khoá bí mật mới rồi sao chép khoá đó.
  6. Mở lib/constants.dart, và thay thế giá trị của appStoreSharedSecret bằng khoá bí mật dùng chung mà bạn vừa tạo.

d8b8042470aaeff.png

b72f4565750e2f40.png

Tệp cấu hình hằng số

Trước khi tiếp tục, hãy đảm bảo rằng các hằng số sau được định cấu hình trong tệp lib/constants.dart:

  • androidPackageId: Mã gói được dùng trên Android. Ví dụ: com.example.dashclicker
  • appStoreSharedSecret: Khoá bí mật dùng chung để truy cập vào App Store Connect nhằm xác minh giao dịch mua.
  • bundleId: Mã nhận dạng gói được dùng trên iOS. Ví dụ: com.example.dashclicker

Hiện tại, bạn có thể bỏ qua các hằng số còn lại.

10. Xác minh giao dịch mua

Quy trình chung để xác minh giao dịch mua tương tự nhau trên iOS và Android.

Đối với cả hai cửa hàng, ứng dụng của bạn sẽ nhận được mã thông báo khi có giao dịch mua.

Ứng dụng sẽ gửi mã thông báo này đến dịch vụ phụ trợ của bạn. Sau đó, dịch vụ này sẽ xác minh giao dịch mua bằng máy chủ của cửa hàng tương ứng bằng mã thông báo đã cung cấp.

Sau đó, dịch vụ phụ trợ có thể chọn lưu trữ giao dịch mua và trả lời ứng dụng về việc giao dịch mua có hợp lệ hay không.

Bằng cách yêu cầu dịch vụ phụ trợ xác thực với các cửa hàng thay vì ứng dụng chạy trên thiết bị của người dùng, bạn có thể ngăn người dùng truy cập vào các tính năng cao cấp bằng cách, ví dụ: tua lại đồng hồ hệ thống của họ.

Thiết lập phía Flutter

Thiết lập tính năng xác thực

Khi gửi giao dịch mua đến dịch vụ phụ trợ, bạn cần đảm bảo người dùng được xác thực trong khi mua hàng. Hầu hết logic xác thực đã được thêm vào cho bạn trong dự án khởi động, bạn chỉ cần đảm bảo PurchasePage hiển thị nút đăng nhập khi người dùng chưa đăng nhập. Thêm mã sau vào đầu phương thức tạo của 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

Điểm cuối xác minh cuộc gọi từ ứng dụng

Trong ứng dụng, hãy tạo hàm _verifyPurchase(PurchaseDetails purchaseDetails) gọi điểm cuối /verifypurchase trên phần phụ trợ Dart bằng lệnh gọi bài đăng http.

Gửi cửa hàng đã chọn (google_play cho Cửa hàng Play hoặc app_store cho App Store), serverVerificationDataproductID. Máy chủ trả về mã trạng thái cho biết giao dịch mua có được xác minh hay không.

Trong hằng số ứng dụng, hãy định cấu hình IP máy chủ thành địa chỉ IP của máy cục bộ.

lib/logic/dash_purchases.dart

  FirebaseNotifier firebaseNotifier;

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

Thêm firebaseNotifier bằng cách tạo DashPurchases trong main.dart:

lib/main.dart

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

Thêm một phương thức getter cho Người dùng trong FirebaseNotifier để bạn có thể truyền mã nhận dạng người dùng đến hàm xác minh giao dịch mua.

lib/logic/firebase_notifier.dart

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

Thêm hàm _verifyPurchase vào lớp DashPurchases. Hàm async này trả về một giá trị boolean cho biết giao dịch mua có được xác thực hay không.

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

Gọi hàm _verifyPurchase trong _handlePurchase ngay trước khi bạn áp dụng giao dịch mua. Bạn chỉ nên áp dụng giao dịch mua khi giao dịch đó được xác minh. Trong ứng dụng chính thức, bạn có thể chỉ định thêm thông tin này, ví dụ: áp dụng gói thuê bao dùng thử khi cửa hàng tạm thời không hoạt động. Tuy nhiên, đối với ví dụ này, hãy đơn giản hoá và chỉ áp dụng giao dịch mua khi giao dịch mua được xác minh thành công.

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

Trong ứng dụng, mọi thứ đã sẵn sàng để xác thực giao dịch mua.

Thiết lập dịch vụ phụ trợ

Tiếp theo, hãy thiết lập hàm trên đám mây để xác minh giao dịch mua ở phần phụ trợ.

Tạo trình xử lý giao dịch mua

Vì quy trình xác minh cho cả hai cửa hàng gần như giống hệt nhau, hãy thiết lập một lớp PurchaseHandler trừu tượng với cách triển khai riêng cho từng cửa hàng.

be50c207c5a2a519.png

Bắt đầu bằng cách thêm tệp purchase_handler.dart vào thư mục lib/, trong đó bạn xác định một lớp PurchaseHandler trừu tượng với hai phương thức trừu tượng để xác minh hai loại giao dịch mua: gói thuê bao và không phải gói thuê bao.

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

Như bạn có thể thấy, mỗi phương thức yêu cầu 3 tham số:

  • userId: Mã nhận dạng của người dùng đã đăng nhập để bạn có thể liên kết giao dịch mua với người dùng đó.
  • productData: Dữ liệu về sản phẩm. Bạn sẽ xác định điều này trong một phút.
  • token: Mã thông báo do cửa hàng cung cấp cho người dùng.

Ngoài ra, để giúp các trình xử lý giao dịch mua này dễ sử dụng hơn, hãy thêm một phương thức verifyPurchase() có thể dùng cho cả gói thuê bao và gói không phải thuê bao:

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

Giờ đây, bạn chỉ cần gọi verifyPurchase cho cả hai trường hợp, nhưng vẫn có thể triển khai riêng biệt!

Lớp ProductData chứa thông tin cơ bản về các sản phẩm có thể mua, bao gồm mã sản phẩm (đôi khi còn gọi là SKU) và ProductType.

lib/products.dart

class ProductData {
  final String productId;
  final ProductType type;

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

ProductType có thể là gói thuê bao hoặc không phải gói thuê bao.

lib/products.dart

enum ProductType {
  subscription,
  nonSubscription,
}

Cuối cùng, danh sách sản phẩm được xác định là một bản đồ trong cùng một tệp.

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

Tiếp theo, hãy xác định một số cách triển khai phần giữ chỗ cho Cửa hàng Google Play và Apple App Store. Bắt đầu với Google Play:

Tạo lib/google_play_purchase_handler.dart và thêm một lớp mở rộng PurchaseHandler bạn vừa viết:

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

Hiện tại, hàm này trả về true cho các phương thức xử lý; bạn sẽ tìm hiểu về các phương thức này sau.

Như bạn có thể nhận thấy, hàm khởi tạo sẽ lấy một thực thể của IapRepository. Trình xử lý giao dịch mua sẽ sử dụng thực thể này để lưu trữ thông tin về giao dịch mua trong Firestore sau này. Để giao tiếp với Google Play, bạn sử dụng AndroidPublisherApi được cung cấp.

Tiếp theo, hãy làm tương tự cho trình xử lý cửa hàng ứng dụng. Tạo lib/app_store_purchase_handler.dart và thêm một lớp mở rộng 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;
 
}
}

Tuyệt vời! Bây giờ, bạn có hai trình xử lý giao dịch mua. Tiếp theo, hãy tạo điểm cuối API xác minh giao dịch mua.

Sử dụng trình xử lý giao dịch mua

Mở bin/server.dart và tạo điểm cuối API bằng shelf_route:

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

Mã trên thực hiện những việc sau:

  1. Xác định điểm cuối POST sẽ được gọi từ ứng dụng bạn đã tạo trước đó.
  2. Giải mã tải trọng JSON và trích xuất các thông tin sau:
  3. userId: Mã nhận dạng người dùng hiện đã đăng nhập
  4. source: Kho lưu trữ được sử dụng, app_store hoặc google_play.
  5. productData: Nhận được từ productDataMap mà bạn đã tạo trước đó.
  6. token: Chứa dữ liệu xác minh để gửi đến các cửa hàng.
  7. Gọi đến phương thức verifyPurchase, cho GooglePlayPurchaseHandler hoặc AppStorePurchaseHandler, tuỳ thuộc vào nguồn.
  8. Nếu xác minh thành công, phương thức này sẽ trả về Response.ok cho ứng dụng.
  9. Nếu không xác minh được, phương thức này sẽ trả về Response.internalServerError cho ứng dụng.

Sau khi tạo điểm cuối API, bạn cần định cấu hình hai trình xử lý giao dịch mua. Để làm việc này, bạn cần tải các khoá tài khoản dịch vụ mà bạn đã nhận được ở bước trước và định cấu hình quyền truy cập vào các dịch vụ khác nhau, bao gồm cả API Nhà xuất bản Android và API Firebase Firestore. Sau đó, hãy tạo hai trình xử lý giao dịch mua bằng các phần phụ thuộc khác nhau:

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

Xác minh giao dịch mua trên Android: Triển khai trình xử lý giao dịch mua

Tiếp theo, hãy tiếp tục triển khai trình xử lý giao dịch mua trên Google Play.

Google đã cung cấp các gói Dart để tương tác với các API mà bạn cần để xác minh giao dịch mua. Bạn đã khởi tạo các biến này trong tệp server.dart và hiện sử dụng các biến này trong lớp GooglePlayPurchaseHandler.

Triển khai trình xử lý cho các giao dịch mua không phải loại gói thuê bao:

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

Bạn có thể cập nhật trình xử lý giao dịch mua gói thuê bao theo cách tương tự:

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

Thêm phương thức sau để hỗ trợ phân tích cú pháp mã đơn đặt hàng, cũng như hai phương thức để phân tích cú pháp trạng thái mua hàng.

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

Giờ đây, các giao dịch mua của bạn trên Google Play sẽ được xác minh và lưu trữ trong cơ sở dữ liệu.

Tiếp theo, hãy chuyển sang giao dịch mua qua App Store cho iOS.

Xác minh giao dịch mua trên iOS: Triển khai trình xử lý giao dịch mua

Để xác minh giao dịch mua bằng App Store, có một gói Dart bên thứ ba tên là app_store_server_sdk giúp quá trình này trở nên dễ dàng hơn.

Bắt đầu bằng cách tạo thực thể ITunesApi. Sử dụng cấu hình hộp cát, cũng như bật tính năng ghi nhật ký để hỗ trợ gỡ lỗi.

lib/app_store_purchase_handler.dart

  final _iTunesAPI = ITunesApi(
    ITunesHttpClient(
      ITunesEnvironment.sandbox(),
      loggingEnabled: true,
    ),
  );

Giờ đây, không giống như các API của Google Play, App Store sử dụng cùng một điểm cuối API cho cả gói thuê bao và gói không phải thuê bao. Điều này có nghĩa là bạn có thể sử dụng cùng một logic cho cả hai trình xử lý. Hợp nhất các tệp này với nhau để chúng gọi cùng một phương thức triển khai:

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 {
   //..
  }

Bây giờ, hãy triển khai 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;
    }
  }

Giờ đây, các giao dịch mua của bạn trên App Store sẽ được xác minh và lưu trữ trong cơ sở dữ liệu!

Chạy phần phụ trợ

Tại thời điểm này, bạn có thể chạy dart bin/server.dart để phân phát điểm cuối /verifypurchase.

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

11. Theo dõi các giao dịch mua

Bạn nên theo dõi giao dịch mua của người dùng trong dịch vụ phụ trợ. Lý do là phần phụ trợ của bạn có thể phản hồi các sự kiện từ cửa hàng, do đó ít gặp phải thông tin lỗi thời do lưu vào bộ nhớ đệm, cũng như ít bị can thiệp hơn.

Trước tiên, hãy thiết lập quy trình xử lý các sự kiện cửa hàng trên phần phụ trợ bằng phần phụ trợ Dart mà bạn đang xây dựng.

Xử lý các sự kiện tại cửa hàng thực tế ở phần phụ trợ

Cửa hàng có thể thông báo cho phần phụ trợ của bạn về mọi sự kiện thanh toán xảy ra, chẳng hạn như khi gói thuê bao gia hạn. Bạn có thể xử lý các sự kiện này trong phần phụ trợ để cập nhật các giao dịch mua trong cơ sở dữ liệu. Trong phần này, hãy thiết lập thông tin này cho cả Cửa hàng Google Play và Apple App Store.

Xử lý các sự kiện thanh toán trên Google Play

Google Play cung cấp các sự kiện thanh toán thông qua chủ đề pub/sub trên đám mây. Về cơ bản, đây là các hàng đợi thông báo mà bạn có thể phát hành thông báo cũng như sử dụng thông báo.

Vì đây là chức năng dành riêng cho Google Play, nên bạn hãy đưa chức năng này vào GooglePlayPurchaseHandler.

Bắt đầu bằng cách mở lib/google_play_purchase_handler.dart và thêm lệnh nhập PubsubApi:

lib/google_play_purchase_handler.dart

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

Sau đó, truyền PubsubApi vào GooglePlayPurchaseHandler và sửa đổi hàm khởi tạo lớp để tạo Timer như sau:

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 được định cấu hình để gọi phương thức _pullMessageFromSubSub mỗi 10 giây. Bạn có thể điều chỉnh Thời lượng theo ý mình.

Sau đó, hãy tạo _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,
    );
  }

Mã bạn vừa thêm sẽ giao tiếp với Chủ đề Pub/Sub từ Google Cloud mỗi 10 giây và yêu cầu tin nhắn mới. Sau đó, xử lý từng thông báo trong phương thức _processMessage.

Phương thức này giải mã các thông báo đến và lấy thông tin cập nhật về từng giao dịch mua, cả gói thuê bao và không phải gói thuê bao, gọi handleSubscription hoặc handleNonSubscription hiện có nếu cần.

Mỗi thông báo cần được xác nhận bằng phương thức _askMessage.

Tiếp theo, hãy thêm các phần phụ thuộc bắt buộc vào tệp server.dart. Thêm PubsubApi.cloudPlatformScope vào cấu hình thông tin xác thực:

bin/server.dart

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

Sau đó, hãy tạo thực thể PubsubApi:

bin/server.dart

  final pubsubApi = pubsub.PubsubApi(clientGooglePlay);

Cuối cùng, hãy truyền đối tượng này vào hàm khởi tạo GooglePlayPurchaseHandler:

bin/server.dart

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

Thiết lập Google Play

Bạn đã viết mã để sử dụng các sự kiện thanh toán từ chủ đề pub/sub, nhưng bạn chưa tạo chủ đề pub/sub và cũng chưa phát hành bất kỳ sự kiện thanh toán nào. Đã đến lúc thiết lập.

Trước tiên, hãy tạo một chủ đề pub/sub:

  1. Truy cập vào trang Cloud Pub/Sub trên Google Cloud Console.
  2. Đảm bảo bạn đang ở dự án Firebase rồi nhấp vào + Create Topic (Tạo chủ đề). d5ebf6897a0a8bf5.png
  3. Đặt tên cho chủ đề mới, giống với giá trị được đặt cho GOOGLE_PLAY_PUBSUB_BILLING_TOPIC trong constants.ts. Trong trường hợp này, hãy đặt tên là play_billing. Nếu bạn chọn một giá trị khác, hãy nhớ cập nhật constants.ts. Tạo chủ đề. 20d690fc543c4212.png
  4. Trong danh sách chủ đề phát hành/thuê bao, hãy nhấp vào biểu tượng ba dấu chấm dọc cho chủ đề bạn vừa tạo rồi nhấp vào Xem quyền. ea03308190609fb.png
  5. Trong thanh bên ở bên phải, hãy chọn Thêm người đại diện.
  6. Tại đây, hãy thêm google-play-developer-notifications@system.gserviceaccount.com và cấp cho tài khoản này vai trò Nhà xuất bản Pub/Sub. 55631ec0549215bc.png
  7. Lưu các thay đổi về quyền.
  8. Sao chép Tên chủ đề của chủ đề bạn vừa tạo.
  9. Mở lại Play Console rồi chọn ứng dụng của bạn trong danh sách Tất cả ứng dụng.
  10. Di chuyển xuống rồi chuyển đến phần Kiếm tiền > Thiết lập tính năng kiếm tiền.
  11. Điền đầy đủ chủ đề rồi lưu nội dung thay đổi. 7e5e875dc6ce5d54.png

Tất cả sự kiện thanh toán trên Google Play hiện sẽ được xuất bản trên chủ đề này.

Xử lý các sự kiện thanh toán trên App Store

Tiếp theo, hãy làm tương tự cho các sự kiện thanh toán trên App Store. Có hai cách hiệu quả để triển khai việc xử lý nội dung cập nhật trong giao dịch mua hàng trên App Store. Một cách là triển khai webhook mà bạn cung cấp cho Apple và họ sử dụng để giao tiếp với máy chủ của bạn. Cách thứ hai (cũng là cách bạn sẽ tìm thấy trong lớp học lập trình này) là kết nối với API Máy chủ Cửa hàng ứng dụng và lấy thông tin gói thuê bao theo cách thủ công.

Lý do lớp học lập trình này tập trung vào giải pháp thứ hai là vì bạn sẽ phải hiển thị máy chủ của mình với Internet để triển khai webhook.

Trong môi trường phát hành công khai, tốt nhất là bạn nên có cả hai. Webhook để lấy sự kiện từ App Store và API máy chủ trong trường hợp bạn bỏ lỡ một sự kiện hoặc cần kiểm tra kỹ trạng thái gói thuê bao.

Bắt đầu bằng cách mở lib/app_store_purchase_handler.dart và thêm phần phụ thuộc AppStoreServerAPI:

lib/app_store_purchase_handler.dart

final AppStoreServerAPI appStoreServerAPI;

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

Sửa đổi hàm khởi tạo để thêm một bộ hẹn giờ sẽ gọi đến phương thức _pullStatus. Bộ hẹn giờ này sẽ gọi phương thức _pullStatus mỗi 10 giây. Bạn có thể điều chỉnh thời lượng của bộ hẹn giờ này theo nhu cầu của mình.

lib/app_store_purchase_handler.dart

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

Sau đó, hãy tạo phương thức _pullStatus như sau:

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

Phương thức này hoạt động như sau:

  1. Lấy danh sách gói thuê bao đang hoạt động từ Firestore bằng IapRepository.
  2. Đối với mỗi đơn đặt hàng, API này sẽ yêu cầu trạng thái gói thuê bao cho API Máy chủ Cửa hàng ứng dụng.
  3. Lấy giao dịch gần đây nhất cho giao dịch mua gói thuê bao đó.
  4. Kiểm tra ngày hết hạn.
  5. Cập nhật trạng thái gói thuê bao trên Firestore. Nếu gói thuê bao đã hết hạn, thì gói thuê bao đó sẽ được đánh dấu là hết hạn.

Cuối cùng, hãy thêm tất cả mã cần thiết để định cấu hình quyền truy cập vào API máy chủ Cửa hàng ứng dụng:

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

Thiết lập App Store

Tiếp theo, hãy thiết lập App Store:

  1. Đăng nhập vào App Store Connect rồi chọn Người dùng và quyền truy cập.
  2. Chuyển đến Key Type (Loại khoá) > In-App Purchase (Giao dịch mua hàng trong ứng dụng).
  3. Nhấn vào biểu tượng "dấu cộng" để thêm một thẻ mới.
  4. Đặt tên cho khoá, ví dụ: "khoá lớp học lập trình".
  5. Tải tệp p8 chứa khoá xuống.
  6. Sao chép tệp này vào thư mục tài sản, với tên SubscriptionKey.p8.
  7. Sao chép mã khoá từ khoá mới tạo rồi đặt mã khoá đó thành hằng số appStoreKeyId trong tệp lib/constants.dart.
  8. Sao chép mã nhận dạng của tổ chức phát hành ngay ở đầu danh sách khoá và đặt mã nhận dạng này thành hằng số appStoreIssuerId trong tệp lib/constants.dart.

9540ea9ada3da151.png

Theo dõi giao dịch mua trên thiết bị

Cách an toàn nhất để theo dõi giao dịch mua là ở phía máy chủ vì máy khách khó bảo mật. Tuy nhiên, bạn cần có một số cách để đưa thông tin trở lại máy khách để ứng dụng có thể hành động dựa trên thông tin trạng thái gói thuê bao. Bằng cách lưu trữ các giao dịch mua trong Firestore, bạn có thể dễ dàng đồng bộ hoá dữ liệu với ứng dụng và tự động cập nhật dữ liệu đó.

Bạn đã đưa IAPRepo vào ứng dụng. Đây là kho lưu trữ Firestore chứa tất cả dữ liệu giao dịch mua của người dùng trong List<PastPurchase> purchases. Kho lưu trữ cũng chứa hasActiveSubscription, là true khi có giao dịch mua bằng productId storeKeySubscription có trạng thái chưa hết hạn. Khi người dùng chưa đăng nhập, danh sách này sẽ trống.

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

Tất cả logic mua hàng đều nằm trong lớp DashPurchases và là nơi bạn nên áp dụng hoặc xoá gói thuê bao. Vì vậy, hãy thêm iapRepo làm thuộc tính trong lớp và gán iapRepo trong hàm khởi tạo. Tiếp theo, hãy thêm trực tiếp trình nghe trong hàm khởi tạo và xoá trình nghe trong phương thức dispose(). Ban đầu, trình nghe có thể chỉ là một hàm trống. Vì IAPRepoChangeNotifier và bạn gọi notifyListeners() mỗi khi giao dịch mua trong Firestore thay đổi, nên phương thức purchasesUpdate() luôn được gọi khi sản phẩm đã mua thay đổi.

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
  }

Tiếp theo, hãy cung cấp IAPRepo cho hàm khởi tạo trong main.dart.. Bạn có thể lấy kho lưu trữ bằng cách sử dụng context.read vì kho lưu trữ này đã được tạo trong Provider.

lib/main.dart

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

Tiếp theo, hãy viết mã cho hàm purchaseUpdate(). Trong dash_counter.dart,, các phương thức applyPaidMultiplierremovePaidMultiplier lần lượt đặt hệ số là 10 hoặc 1, vì vậy, bạn không cần phải kiểm tra xem gói thuê bao đã được áp dụng hay chưa. Khi trạng thái gói thuê bao thay đổi, bạn cũng cập nhật trạng thái của sản phẩm có thể mua để có thể cho thấy trên trang mua hàng rằng sản phẩm đó đã hoạt động. Đặt thuộc tính _beautifiedDashUpgrade dựa trên việc người dùng có mua gói nâng cấp hay không.

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

Giờ đây, bạn đã đảm bảo rằng trạng thái gói thuê bao và nâng cấp luôn mới nhất trong dịch vụ phụ trợ và đồng bộ hoá với ứng dụng. Ứng dụng sẽ hành động tương ứng và áp dụng các tính năng gói thuê bao và nâng cấp cho trò chơi nhấp chuột Dash.

12. Đã xong!

Xin chúc mừng!!! Bạn đã hoàn tất lớp học lập trình. Bạn có thể tìm thấy mã hoàn chỉnh cho lớp học lập trình này trong thư mục android_studio_folder.pngcomplete.

Để tìm hiểu thêm, hãy thử tham gia các lớp học lập trình khác về Flutter.