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

1. Giới thiệu

Để thêm giao dịch mua hàng trong ứng dụng vào một ứ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 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 hàng này bằng một phần phụ trợ Dart với Firebase. Ứng dụng được cung cấp, Dash Clicker, có 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 sau đây:

  1. Một lựa chọn mua có thể lặp lại cho 2.000 Dash cùng một lúc.
  2. Mua bản nâng cấp một lần để chuyển đổi Bảng điều khiển kiểu cũ thành Bảng điều khiển kiểu hiện đại.
  3. Gói thuê bao giúp 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 những sản phẩm này nhiều lần. Đây được gọi là sản phẩm tiêu hao vì được tiêu thụ trực tiếp và có thể tiêu thụ nhiều lần.

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

Lựa chọn mua hàng thứ ba và cũng là lựa chọn cuối cùng là gói thuê bao. Trong thời gian gói thuê bao còn hiệu lực, người dùng sẽ nhận được Dash nhanh hơn, nhưng khi ngừng trả phí 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 một ứ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 đó bằng Firestore. Firestore được dùng để giúp quy trình này trở nên dễ dàng hơn, nhưng trong ứng dụng phát hành công khai, bạn có thể 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 một ứng dụng để hỗ trợ các giao dịch mua và gói thuê bao có thể sử dụng.
  • 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à Play Store bằng 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ữ giao dịch đó trong Firestore.
  • Cách quản lý giao dịch mua trong ứng dụng.

Bạn cần có

  • Android Studio
  • Xcode (để 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 mã nhận dạng gói cho iOS cũng như 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 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 dùng lệnh sau:

gh repo clone flutter/codelabs flutter-codelabs

Mã mẫu được sao chép vào một 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 nên ở vào cuối mỗi bước được đặt tên. Mã khởi đầu nằm ở bước 0, vì vậy, hãy chuyển đến mã này như sau:

cd flutter-codelabs/in_app_purchases/step_00

Nếu bạn muốn bỏ qua hoặc xem một bước nào đó sẽ trông như thế nào, hãy xem trong thư mục có tên theo 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 khởi đầu

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

Các ứng dụng mà bạn sắp tạo cần giao tiếp với App Store và Play Store để biết những sản phẩm nào có sẵn và giá bao nhiêu. 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 trên iOS, mã này được gọi là mã nhận dạng gói và đối với Cửa hàng Play trên Android, mã 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 đảo ngược. Ví dụ: khi tạo một ứ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, giờ đây bạn sẽ đặt giá trị đó trong phần cài đặt dự án.

Trước tiên, hãy thiết lập mã nhận dạng gói cho iOS. Để làm như vậy, hãy mở tệp Runner.xcworkspace trong ứng dụng Xcode.

a9fbac80a31e28e0.png

Trong cấu trúc thư mục của Xcode, dự án Runner nằm ở trên cùng, còn các mục tiêu Flutter, RunnerProducts nằm bên dưới dự án Runner. Nhấp đúp vào Runner để 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 mã nhận dạng gói mà bạn vừa chọn trong trường Nhóm để thiết lập nhóm của bạn.

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. Để làm như vậy, hãy mở tệp build.gradle.kts trong android/app, rồi thay đổi applicationId (ở dòng 24 trong ảnh chụp màn hình bên dưới) thành mã ứng dụng, giống như mã 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 chúng giống nhau sẽ ít xảy ra 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.

e320a49ff2068ac2.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 của dự án:

$ cd app
$ flutter pub add in_app_purchase dev:in_app_purchase_platform_interface
Resolving dependencies... 
Downloading packages... 
  characters 1.4.0 (1.4.1 available)
  flutter_lints 5.0.0 (6.0.0 available)
+ in_app_purchase 3.2.3
+ in_app_purchase_android 0.4.0+3
+ in_app_purchase_platform_interface 1.4.0
+ in_app_purchase_storekit 0.4.4
+ json_annotation 4.9.0
  lints 5.1.1 (6.0.0 available)
  material_color_utilities 0.11.1 (0.13.0 available)
  meta 1.16.0 (1.17.0 available)
  provider 6.1.5 (6.1.5+1 available)
  test_api 0.7.6 (0.7.7 available)
Changed 5 dependencies!
7 packages have newer versions incompatible with dependency constraints.
Try `flutter pub outdated` for more information.

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ột mục trong dependenciesin_app_purchase_platform_interface trong dev_dependencies.

pubspec.yaml

dependencies:
  flutter:
    sdk: flutter
  cloud_firestore: ^6.0.0
  cupertino_icons: ^1.0.8
  firebase_auth: ^6.0.1
  firebase_core: ^4.0.0
  google_sign_in: ^7.1.1
  http: ^1.5.0
  intl: ^0.20.2
  provider: ^6.1.5
  logging: ^1.3.0
  in_app_purchase: ^3.2.3

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

4. Thiết lập App Store

Để thiết lập và kiểm thử 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 các sản phẩm có thể mua tại đó. Bạn không cần xuất bản bất cứ 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ý tham gia 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ó một thoả thuận còn hiệu lực cho các ứ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

Bạn sẽ thấy các thoả thuận tại đây cho ứng dụng miễn phí và ứng dụng 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 trả phí là mới. Hãy nhớ xem các điều khoản, 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 đúng mọi thứ, 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ể dùng thử giao dịch mua hàng trong ứng dụng nếu không có thoả thuận đang có hiệu lực.

4a100bbb8cafdbbf.jpeg

Đăng ký mã nhận dạng ứng dụng

Tạo một mã nhận dạng mới trong Apple Developer Portal. Truy cập vào developer.apple.com/account/resources/identifiers/list rồi nhấp vào biểu tượng "dấu cộng" bên cạnh tiêu đề Identifiers (Giá trị nhận dạng).

55d7e592d9a3fc7b.png

Chọn mã ứng dụng

13f125598b72ca77.png

Chọn ứng dụng

41ac4c13404e2526.png

Cung cấp 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 có cùng giá trị như đã đặt trước đó trong XCode.

9d2c940ad80deeef.png

Để biết thêm hướng dẫn về cách tạo mã nhận dạng ứ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 mã 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 của App Store Connect.

Để kiểm thử giao dịch mua hàng trong ứng dụng, bạn cần có một người dùng kiểm thử trong hộp cát. Người dùng kiểm thử này không được kết nối với iTunes mà 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 phần Hộp cát để tạo tài khoản hộp cát mới hoặc quản lý các Apple ID hộp cát hiện có.

2ba0f599bcac9b36.png

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 > Nhà phát triển > Tài khoản Apple trong hộp cát.

74a545210b282ad8.png eaa67752f2350f74.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 3 mặt hàng có thể mua:

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

a118161fac83815a.png

Chuyển đến mục Lượt mua hàng trong ứng dụng.

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

  1. Thiết lập dash_consumable_2k làm Consumable (Sản phẩm tiêu hao). Sử dụng dash_consumable_2k làm mã nhận dạng 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. 1f8527fc03902099.png Thiết lập tình trạng rảnh/bận. Sản phẩm phải có ở quốc gia của người dùng hộp cát. bd6b2ce2d9314e6e.png Thêm giá và đặt giá thành $1.99 hoặc giá trị tương đương bằng đơn vị tiền tệ khác. 926b03544ae044c4.png Thêm bản bản địa hoá cho giao dịch mua. Gọi lệnh mua Spring is in the air với 2000 dashes fly out làm nội dung mô tả. e26dd4f966dcfece.png Thêm ảnh chụp màn hình bài đánh giá. Nội dung không quan trọng trừ phi sản phẩm được gửi đi xem xét, nhưng nội dung là bắt buộc để sản phẩm ở trạng thái "Sẵn sàng gửi". Đây là trạng thái cần thiết khi ứng dụng tìm nạp sản phẩm từ App Store. 25171bfd6f3a033a.png
  2. Thiết lập dash_upgrade_3d làm Không tiêu hao. Sử dụng dash_upgrade_3d làm mã nhận dạng sản phẩm. Đặt tên tham chiếu thành dash upgrade 3d. Gọi lệnh mua 3D Dash với Brings your dash back to the future làm nội dung mô tả. Đặt giá thành $0.99. Định cấu hình tình trạng còn hàng và tải ảnh chụp màn hình bài đánh giá lên theo cách tương tự như đối với sản phẩm dash_consumable_2k. 83878759f32a7d4a.png
  3. Thiết lập dash_subscription_doubler làm Gói thuê bao tự động gia hạn. Quy trình đăng ký có chút khác biệt. Trước tiên, bạn phải tạo một nhóm 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 số các gói này cùng một lúc, nhưng có thể nâng cấp hoặc hạ cấp giữa các gói thuê bao này. Chỉ cần gọi cho nhóm này subscriptions. 393a44b09f3cd8bf.png Thêm nội dung bản địa hoá cho nhóm thuê bao. 595aa910776349bd.png Tiếp theo, bạn sẽ tạo gói thuê bao. Đặt Tên tham chiếu thành dash subscription doubler và Mã sản phẩm thành dash_subscription_doubler. 7bfff7bbe11c8eec.png Tiếp theo, hãy chọn thời hạn thuê bao là 1 tuần và các bản bản địa hoá. Đặt tên cho gói thuê bao này là Jet Engine kèm theo nội dung mô tả Doubles your clicks. Đặt giá thành $0.49. Định cấu hình tình trạng còn hàng và tải ảnh chụp màn hình bài đánh giá lên theo cách tương tự như đối với sản phẩm dash_consumable_2k. 44d18e02b926a334.png

Bây giờ, bạn sẽ thấy các sản phẩm trong danh sách:

17f242b5c1426b79.png d71da951f595054a.png

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

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

Tạo ứng dụng mới

Tạo một ứ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. Nêu rõ rằng ứng dụng của bạn là một 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. Hoàn tất nội dung khai báo theo Nguyên tắc nội dung và luật xuất khẩu của Hoa Kỳ.
  7. 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 việc cần làm trong mục 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ý đơn đăng ký

Để 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 một kho khoá

Nếu bạn đã có một kho khoá, hãy chuyển sang bước tiếp theo. Nếu chưa, 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 dùng lệnh sau:

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

Trên Windows, hãy 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ố mà bạn truyền đến tham số -keystore. Giữ

keystore

tệp riêng tư; đừng kiểm tra tệp đó trong 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á của bạn:

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 việc ký trong Gradle

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

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

import java.util.Properties
import java.io.FileInputStream

plugins {
    // omitted
}

val keystoreProperties = Properties()
val keystorePropertiesFile = rootProject.file("key.properties")
if (keystorePropertiesFile.exists()) {
    keystoreProperties.load(FileInputStream(keystorePropertiesFile))
}

android {
    // omitted
}

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

Cập nhật khối buildTypes thành:

   buildTypes {
        release {
            signingConfig = signingConfigs.getByName("release")
        }
    }

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

   signingConfigs {
        create("release") {
            keyAlias = keystoreProperties["keyAlias"] as String
            keyPassword = keystoreProperties["keyPassword"] as String
            storeFile = keystoreProperties["storeFile"]?.let { file(it) }
            storePassword = keystoreProperties["storePassword"] as String
        }
    }

    buildTypes {
        release {
            signingConfig = signingConfigs.getByName("release")
        }
    }

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

Lệnh này tạo một bản phát hành theo mặc định và bạn có thể tìm thấy đầu ra 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 Kiểm thử và phát hành > Kiểm thử > Kiểm thử khép kín rồi tạo một bản phát hành kiểm thử khép kín mới.

Tiếp theo, hãy tải gói ứng dụng app-release.aab do lệnh tạo tạo ra 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ử khép kín để kích hoạt bản phát hành kiểm thử khép kín.

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 ở 2 vị trí:

  1. Đến kênh kiểm thử cụ thể (Kiểm thử nội bộ)
  2. Là người kiểm thử được cấp 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 Kiểm thử và phát hành > Kiểm thử > Kiểm thử 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 có quyền truy cập vào tính năng kiểm thử giao dịch mua hàng trong ứng dụng.

Tiếp theo, hãy chọn hộp đánh dấu cho danh sách rồi nhấp vào Lưu thay đổi.

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

  1. Quay lại chế độ xem Tất cả ứng dụng của Google Play Console.
  2. Chuyển đến phần Cài đặt > Kiểm thử giấy phép.
  3. Thêm địa chỉ email của những người kiểm thử cần có khả năng kiểm thử giao dịch mua hàng trong ứng dụng.
  4. Đặt Phản hồi về 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 riêng biệt:

  • dash_consumable_2k: Một giao dịch mua hàng tiêu hao mà người dùng có thể mua nhiều lần, mỗi lần mua sẽ cấp cho người dùng 2.000 Dash (đơn vị tiền tệ trong ứng dụng).
  • dash_upgrade_3d: Một giao dịch mua "nâng cấp" không tiêu hao mà người dùng chỉ có thể mua một lần. Giao dịch này sẽ cung cấp cho người dùng một Dash có hình thức khác.
  • dash_subscription_doubler: Gói thuê bao cho phép người dùng nhận được gấp đôi số Dash 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 hao và sản phẩm không tiêu hao.

  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 hoàn toàn khớp với mã nhận dạng mà bạn dự đị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 là 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 trang 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 rằng mã sản phẩm hoàn toàn khớp với mã nhận dạng mà bạn dự định sử dụng.
  5. Nhấp vào Lưu

Giờ đây, bạn có thể thiết lập các 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 trong cơ sở dữ liệu.
  • Người dùng sẽ không thể đánh 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 ngược đồ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 đám mây và Firestore thông qua Firebase của 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 đầu đã bao gồm một dự án Firebase xử lý các giao dịch mua cơ bản để giúp bạn bắt đầu.

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

Việc bạn cần làm là tạo dự án Firebase của riêng mình, định cấu hình cả ứng dụng và phần phụ trợ cho Firebase, rồi 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 các 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 sử dụng mô-đun xác thực của Firebase với tính năng đăng nhập bằng Google.

  1. Trên trang tổng quan Firebase, hãy chuyển đến mục 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.

fe2e0933d6810888.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 cả lựa chọn này.

d02d641821c71e2c.png

Đặt 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 mà 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 chọn cả hai nền tảng để bật iOSAndroid.

? 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 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 của 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/ rồi tạo một 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ó các thuộc tính VariantConfig được đặt thành debug. Rất có thể kho khoá nằm trong thư mục chính 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.

Cuối cùng, hãy chạy lại lệnh flutterfire configure để cập nhật ứng dụng nhằm thêm cấu hình ký.

$ flutterfire configure
? You have an existing `firebase.json` file and possibly already configured your project for Firebase. Would you prefer to reuse the valus in your existing `firebase.json` file to configure your project? (y/n) › yes
✔ You have an existing `firebase.json` file and possibly already configured your project for Firebase. Would you prefer to reuse the values in your existing `firebase.json` file to configure your project? · yes

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

Mở ios/Runner.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/ rồi nhấp vào flutter, sau đó nhấp vào lựa chọn open iOS module in Xcode.

Để cho phép đăng nhập bằng Google trên iOS, hãy thêm lựa chọn cấu hình CFBundleURLTypes vào tệp plist bản dựng của bạn. (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, tệp là ios/Runner/Info.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 khoá-giá trị đó:

  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 tệp ios/Runner/Info.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>

Giờ đây, bạn đã hoàn tất quy trình 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 2 trang. Trang này cũng tạo 3 Provider cho DashCounter, DashUpgrades,DashPurchases. DashCounter theo dõi số lượng Dash 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 Dash. 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 một 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 thông tin cập nhật về giao dịch mua khi ứng dụng khởi động, vì vậy, hãy tắt tính năng tải trì hoãn 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ó cách nào đó để 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ể bị 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!;
  }
}

Cập nhật kiểm thử như sau:

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 DashPurchasesChangeNotifier. Hiện tại, bạn chỉ có thể thêm DashCounter vào Dash đã mua.

Thêm một thuộc tính đăng ký luồng, _subscription (thuộc loại StreamSubscription<List<PurchaseDetails>> _subscription;), IAPConnection.instance, và các mục 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. Theo mặc định, dự án này được thiết lập để không thể rỗng (NNBD), tức 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ộ định tí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 theo dõi luồng. Trong phương thức dispose(), hãy huỷ đăng ký luồng.

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

import '../main.dart';
import '../model/purchasable_product.dart';
import '../model/store_state.dart';
import 'dash_counter.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.
}

Giờ đây, ứ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 kiểm thử bằng "flutter test" để xác minh rằng 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, xuất hiện trong danh sách và được mua khi người dùng nhấn vào sản phẩm.

Adapt 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 cửa hàng không hoạt độ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ửa hàng có sẵn, hãy tải các giao dịch mua có sẵn. Với chế độ thiết lập trước đây của Google Play và App Store, bạn sẽ thấy storeKeyConsumable, storeKeySubscription,storeKeyUpgrade. Khi giao dịch mua dự kiến không có sẵn, hãy in thông tin này ra bảng điều khiển; bạn cũng có thể muốn 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ã nhận dạng không tìm thấy và sản phẩm có thể mua được tìm thấy. 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();                                       // Add this line
  }

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 cho thấy _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 dùng trong bước tiếp theo.

Tiện ích _PurchaseList cho thấy 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 chúng được định cấu hình đúng cách. Xin lưu ý rằng có thể mất một khoảng thời gian trước khi các giao dịch mua xuất hiện 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 một sản phẩm. Bạn chỉ cần tách riêng hàng tiêu dùng với hàng không tiêu dùng. Bản nâng cấp và sản phẩm đăng ký là các sản phẩm không tiêu hao.

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 truy xuất 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 trên trang mua và áp dụng giao dịch mua vào 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 bước theo dõi và xác minh giao dịch mua, hãy thiết lập một 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 rằng 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 đầu. Bạn nên xem xét những nội dung đã có trong mã khởi đầu trước khi bắt đầu để biết cách bạn sẽ cấu trúc 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 mà máy chủ sẽ chạy. Để 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 đường nào. Sau đó, bạn sẽ tạo một tuyến đường để xử lý quy trình xác minh giao dịch mua.

Một phần đã có trong mã khởi đầu 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 đầu 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 những 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 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 và chuyển đến mục 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 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 đăng nhập JSON xuống.

  1. Truy cập vào trang Google Play Android Developer API trong Google Cloud Console. 629f0bd8e6b50be8.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 thực hiện yêu cầu đó trước rồi quay lại trang này.
  2. Tiếp theo, hãy chuyển đến trang Tài khoản dịch vụ rồi nhấp vào + Tạo tài khoản dịch vụ. 8dc97e3b1262328a.png
  3. Nhập Tên tài khoản dịch vụ rồi nhấp vào Tạo và tiếp tục. 4fe8106af85ce75f.png
  4. Chọn vai trò Người đăng ký Pub/Sub rồi nhấp vào Xong. a5b6fa6ea8ee22d.png
  5. Sau khi tạo tài khoản, hãy chuyển đến phần Quản lý khoá. eb36da2c1ad6dd06.png
  6. Chọn Thêm khoá > Tạo khoá mới. e92db9557a28a479.png
  7. Tạo và tải khoá JSON xuống. 711d04f2f4176333.png
  8. Đổ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/.
  9. Tiếp theo, hãy chuyển đến trang Người dùng và quyền trong Play Console28fffbfc35b45f97.png
  10. Nhấp vào Mời người dùng mới rồi nhập địa chỉ email của tài khoản dịch vụ bạn đã tạo trước đó. Bạn có thể tìm thấy email này trong bảng trên trang Tài khoản dịch vụe3310cc077f397d.png
  11. Cấp quyền Xem dữ liệu tài chínhQuản lý đơn đặt hàng và gói thuê bao cho ứng dụng. a3b8cf2b660d1900.png
  12. Nhấp vào Mời người dùng.

Một việc nữa bạn cần làm là mở lib/constants.dart, và thay thế giá trị của androidPackageId bằng mã nhận dạng 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 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 bên, hãy chuyển đến phần Chung > Thông tin ứng dụng.
  4. Nhấp vào Quản lý trong tiêu đề Khoá bí mật dùng chung dành riêng cho ứng dụng. ad419782c5fbacb2.png
  5. Tạo một khoá bí mật mới rồi sao chép khoá đó. b5b72a357459b0e5.png
  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.

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

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

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

Tạm thờ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 đối với iOS và Android.

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

Mã thông báo này được ứng dụng gửi đến dịch vụ phụ trợ của bạn. Sau đó, dịch vụ này sẽ xác minh giao dịch mua với máy chủ của cửa hàng tương ứng bằng mã thông báo được 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 xem giao dịch mua đó có hợp lệ hay không.

Bằng cách để 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, chẳng hạn như tua lại đồng hồ hệ thống.

Thiết lập phía Flutter

Thiết lập phương thức xác thực

Vì bạn sẽ gửi giao dịch mua đến dịch vụ phụ trợ, nên bạn cần đảm bảo người dùng được xác thực khi mua hàng. Hầu hết logic xác thực đã được thêm cho bạn trong dự án khởi đầu, 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 'package:flutter/material.dart';
import 'package:provider/provider.dart';

import '../logic/dash_purchases.dart';
import '../logic/firebase_notifier.dart';                  // Add this import
import '../model/firebase_state.dart';                     // And this import
import '../model/purchasable_product.dart';
import '../model/store_state.dart';
import '../repo/iap_repo.dart';
import 'login_page.dart';                                  // And this one as well

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

  @override
  Widget build(BuildContext context) {                     // Update from here
    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();
    }                                                      // To here.

    // ...

Gọi điểm cuối xác minh 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 http post.

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 xác minh hay chưa.

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

lib/logic/dash_purchases.dart

import 'dart:async';
import 'dart:convert';                                     // Add this import

import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;                   // And this import
import 'package:in_app_purchase/in_app_purchase.dart';

import '../constants.dart';
import '../main.dart';
import '../model/purchasable_product.dart';
import '../model/store_state.dart';
import 'dash_counter.dart';
import 'firebase_notifier.dart';                           // And this one

class DashPurchases extends ChangeNotifier {
  DashCounter counter;
  FirebaseNotifier firebaseNotifier;                       // Add this line
  StoreState storeState = StoreState.loading;
  late StreamSubscription<List<PurchaseDetails>> _subscription;
  List<PurchasableProduct> products = [];

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

  final iapConnection = IAPConnection.instance;

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

Thêm firebaseNotifier khi 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 getter cho Người dùng trong FirebaseNotifier để bạn có thể truyền mã nhận dạng người dùng vào hàm xác minh giao dịch mua.

lib/logic/firebase_notifier.dart

  Future<FirebaseFirestore> get firestore async {
    var isInitialized = await _isInitialized.future;
    if (!isInitialized) {
      throw Exception('Firebase is not initialized');
    }
    return FirebaseFirestore.instance;
  }

  User? get user => FirebaseAuth.instance.currentUser;     // Add this line

  Future<void> load() async {
    // ...

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 liệu 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 một ứng dụng phát hành công khai, bạn có thể chỉ định thêm điều 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 á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(2000);
          case storeKeyUpgrade:
            _beautifiedDashUpgrade = true;
        }
      }
    }

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

Trong ứng dụng, mọi thứ hiện đã 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 phần phụ trợ để 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ác cách triển khai riêng biệt cho từng cửa hàng.

be50c207c5a2a519.png

Bắt đầu bằng cách thêm một 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 có 2 phương thức trừu tượng để xác minh 2 loại giao dịch mua: gói thuê bao và giao dịch mua 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 đều yêu cầu 3 tham số:

  • userId: Mã nhận dạng của người dùng đã đăng nhập, nhờ đó 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 giây lá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 người dùng dễ dàng sử dụng các trình xử lý giao dịch mua này, 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ó các cách 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ã nhận dạng sản phẩm (đôi khi còn được 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 mà 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, phương thức này trả về true cho các phương thức của trình 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 lấy một thực thể của IapRepository. Trình xử lý giao dịch mua sử dụng phiên bản này để lưu trữ thông tin về các 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 lại 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,
  }) async {
    return true;
  }

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

Tuyệt vời! Giờ đây, bạn có 2 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 rồi tạo một điểm cuối API bằng cách dùng shelf_route:

bin/server.dart

import 'dart:convert';

import 'package:firebase_backend_dart/helpers.dart';
import 'package:firebase_backend_dart/products.dart';
import 'package:shelf/shelf.dart';
import 'package:shelf_router/shelf_router.dart';

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

  final purchaseHandlers = await _createPurchaseHandlers();

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

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

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

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

  await serveHandler(router.call);
}

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

Mã này sẽ thực hiện những việc sau:

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

Sau khi tạo điểm cuối API, bạn cần định cấu hình 2 trình xử lý giao dịch mua. 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ả Android Publisher API và Firebase Firestore API. Sau đó, hãy tạo 2 trình xử lý giao dịch mua với các phần phụ thuộc khác nhau:

bin/server.dart

import 'dart:convert';
import 'dart:io'; // new

import 'package:firebase_backend_dart/app_store_purchase_handler.dart'; // new
import 'package:firebase_backend_dart/google_play_purchase_handler.dart'; // new
import 'package:firebase_backend_dart/helpers.dart';
import 'package:firebase_backend_dart/iap_repository.dart'; // new
import 'package:firebase_backend_dart/products.dart';
import 'package:firebase_backend_dart/purchase_handler.dart'; // new
import 'package:googleapis/androidpublisher/v3.dart' as ap; // new
import 'package:googleapis/firestore/v1.dart' as fs; // new
import 'package:googleapis_auth/auth_io.dart' as auth; // new
import 'package:shelf/shelf.dart';
import 'package:shelf_router/shelf_router.dart';

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

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

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

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 qua Google Play.

Google đã cung cấp các gói Dart để tương tác với những API mà bạn cần xác minh giao dịch mua. Bạn đã khởi tạo các chuỗi này trong tệp server.dart và hiện đang dùng chúng trong lớp GooglePlayPurchaseHandler.

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

lib/google_play_purchase_handler.dart

  /// Handle non-subscription purchases (one time purchases).
  ///
  /// Retrieves the purchase status from Google Play and updates
  /// the Firestore Database accordingly.
  @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 don't 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 don't 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ợ việc phân tích cú pháp mã đơn đặt hàng, cũng như 2 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, bạn có thể dùng một gói Dart bên thứ ba có tên là app_store_server_sdk để giúp quy trình này trở nên dễ dàng hơn.

Hãy 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ư 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 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 chúng để 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 {

    // See next step
  }

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

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ợ

Lúc 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

Cách được đề xuất để theo dõi giao dịch mua của người dùng là trong dịch vụ phụ trợ. Điều này là do phần phụ trợ của bạn có thể phản hồi các sự kiện từ cửa hàng và 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ị giả mạo 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 ở phần phụ trợ bằng phần phụ trợ Dart mà bạn đã tạo.

Xử lý các sự kiện trên cửa hàng ở phần phụ trợ

Các 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 được gia hạn. Bạn có thể xử lý các sự kiện này trong phần phụ trợ để luôn 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 cho cả Cửa hàng Google Play và Apple App Store.

Xử lý các sự kiện Google Play Billing

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

Vì đây là chức năng dành riêng cho Google Play, nên bạn sẽ đư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 nội dung nhập PubsubApi:

lib/google_play_purchase_handler.dart

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

Sau đó, hãy truyền PubsubApi đến 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 _pullMessageFromPubSub sau 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 _pullMessageFromPubSub

lib/google_play_purchase_handler.dart

  /// Process messages from Google Play
  /// Called every 10 seconds
  Future<void> _pullMessageFromPubSub() async {
    print('Polling Google Play messages');
    final request = pubsub.PullRequest(maxMessages: 1000);
    final topicName =
        'projects/$googleCloudProjectId/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/$googleCloudProjectId/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 trên Google Cloud sau 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 mới nhất về từng giao dịch mua (cả gói thuê bao và giao dịch mua không phải gói thuê bao), gọi handleSubscription hoặc handleNonSubscription hiện có nếu cần.

Bạn cần xác nhận từng thông báo 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 đăng nhập:

bin/server.dart

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

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

Sau đó, hãy tạo phiên bản PubsubApi:

bin/server.dart

  final pubsubApi = pubsub.PubsubApi(clientGooglePlay);

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

bin/server.dart

  return {
    'google_play': GooglePlayPurchaseHandler(
      androidPublisher,
      iapRepository,
      pubsubApi,                                           // Add this line
    ),
    '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 xuất bản bất kỳ sự kiện thanh toán nào. Đã đến lúc thiết lập tính năng này.

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

  1. Đặt giá trị của googleCloudProjectId trong constants.dart thành mã nhận dạng của Dự án Google Cloud.
  2. Truy cập vào trang Cloud Pub/Sub trên Google Cloud Console.
  3. Đảm bảo rằng bạn đang ở trong dự án Firebase của mình, rồi nhấp vào + Create Topic (Tạo chủ đề). d5ebf6897a0a8bf5.png
  4. Đặt tên cho chủ đề mới, giống với giá trị được đặt cho googlePlayPubsubBillingTopic trong constants.dart. Trong trường hợp này, hãy đặt tên là play_billing. Nếu bạn chọn một thông tin khác, hãy nhớ cập nhật constants.dart. Tạo chủ đề. 20d690fc543c4212.png
  5. Trong danh sách chủ đề xuất bản/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
  6. Trong thanh bên ở bên phải, hãy chọn Thêm chủ thể.
  7. 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
  8. Lưu các thay đổi về quyền.
  9. Sao chép Tên chủ đề của chủ đề mà bạn vừa tạo.
  10. 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.
  11. Di chuyển xuống rồi chuyển đến trang Kiếm tiền > Thiết lập tính năng kiếm tiền.
  12. Điền đầy đủ chủ đề rồi lưu nội dung thay đổi. 7e5e875dc6ce5d54.png

Tất cả cá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 của Cửa hàng ứng dụng

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

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 đưa máy chủ của mình lên 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 các sự kiện từ App Store và Server API 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 rồi thêm phần phụ thuộc AppStoreServerAPI:

lib/app_store_purchase_handler.dart

  final AppStoreServerAPI appStoreServerAPI;                 // Add this member

  AppStorePurchaseHandler(
    this.iapRepository,
    this.appStoreServerAPI,                                  // And this parameter
  );

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. Đồng hồ hẹn giờ này sẽ gọi phương thức _pullStatus sau mỗi 10 giây. Bạn có thể điều chỉnh khoảng thời gian của bộ hẹn giờ này cho phù hợp với 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

  /// Request the App Store for the latest subscription status.
  /// Updates all App Store subscriptions in the database.
  /// NOTE: This code only handles when a subscription expires as example.
  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 các gói thuê bao đang hoạt động từ Firestore bằng IapRepository.
  2. Đối với mỗi đơn đặt hàng, ứng dụng sẽ yêu cầu trạng thái thuê bao đối với App Store Server API.
  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 thuê bao trên Firestore. Nếu đã hết hạn, trạng thái này 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 App Store Server API:

bin/server.dart

import 'package:app_store_server_sdk/app_store_server_sdk.dart';  // Add this import
import 'package:firebase_backend_dart/constants.dart';            // And this one.


  // 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,                                     // Add this argument
    ),
  };

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 Integrations > Keys > In-App Purchase (Tích hợp > Khoá > 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á đó, chẳng hạn như "Khoá Codelab".
  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 là 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 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ì khó bảo mật máy khách, nhưng bạn cần có cách nào đó để lấy 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 về trạng thái đăng ký. Bằng cách lưu trữ các giao dịch mua trong Firestore, bạn có thể đồng bộ hoá dữ liệu với ứng dụng và tự động cập nhật dữ liệu.

Bạn đã thêm IAPRepo vào ứng dụng. Đây là kho lưu trữ Firestore chứa tất cả dữ liệu mua hàng của người dùng trong List<PastPurchase> purchases. Kho lưu trữ này cũng chứa hasActiveSubscription, (đúng) 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 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((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();
    });
  }

Mọi logic mua hàng đều nằm trong lớp DashPurchases và là nơi á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à chỉ định iapRepo trong hàm khởi tạo. Tiếp theo, hãy thêm trực tiếp một trình nghe vào hàm khởi tạo và xoá trình nghe trong phương thức dispose(). Lúc đầu, trình nghe chỉ có thể là một hàm trống. Vì IAPRepo là một ChangeNotifier 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

import '../repo/iap_repo.dart';                              // Add this import

class DashPurchases extends ChangeNotifier {
  DashCounter counter;
  FirebaseNotifier firebaseNotifier;
  StoreState storeState = StoreState.loading;
  late StreamSubscription<List<PurchaseDetails>> _subscription;
  List<PurchasableProduct> products = [];
  IAPRepo iapRepo;                                           // Add this line

  bool get beautifiedDash => _beautifiedDashUpgrade;
  bool _beautifiedDashUpgrade = false;
  final iapConnection = IAPConnection.instance;

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

  Future<void> loadPurchases() async {
    // Elided.
  }

  @override
  void dispose() {
    _subscription.cancel();
    iapRepo.removeListener(purchasesUpdate);                 // Add this line
    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ữ đã được tạo trong Provider.

lib/main.dart

        ChangeNotifierProvider<DashPurchases>(
          create: (context) => DashPurchases(
            context.read<DashCounter>(),
            context.read<FirebaseNotifier>(),
            context.read<IAPRepo>(),                         // Add this line
          ),
          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ố nhân thành 10 hoặc 1, vì vậy, bạn không phải kiểm tra xem đã áp dụng chiết khấu cho gói thuê bao hay chưa. Khi trạng thá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 biết trên trang mua hàng rằng sản phẩm đã được kích hoạt. Đặ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 thuê bao và nâng cấp luôn mới nhất trong dịch vụ phụ trợ và được đồng bộ hoá với ứng dụng. Ứng dụng sẽ hành động cho phù hợp và áp dụng các tính năng thuê bao và nâng cấp cho trò chơi Dash clicker của bạn.

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ã nguồn hoàn chỉnh cho lớp học lập trình này trong thư mục android_studio_folder.png complete.

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