Thông tin về lớp học lập trình này
1. Giới thiệu
Để thêm giao dịch mua hàng trong ứng dụng vào ứng dụng Flutter, bạn cần thiết lập chính xác Cửa hàng ứng dụng và Cửa hàng Play, xác minh giao dịch mua và cấp các quyền cần thiết, chẳng hạn như các đặc quyền của gói thuê bao.
Trong lớp học lập trình này, bạn sẽ thêm 3 loại giao dịch mua hàng trong ứng dụng vào một ứng dụng (được cung cấp cho bạn) và xác minh các giao dịch mua này bằng cách sử dụng phần phụ trợ Dart với Firebase. Ứng dụng được cung cấp, Dash Clicker, chứa một trò chơi sử dụng linh vật Dash làm đơn vị tiền tệ. Bạn sẽ thêm các lựa chọn mua hàng sau:
- Lựa chọn mua lặp lại 2.000 Dash cùng một lúc.
- Giao dịch mua nâng cấp một lần để chuyển Dash kiểu cũ thành Dash kiểu hiện đại.
- Gói thuê bao tăng gấp đôi số lượt nhấp được tạo tự động.
Lựa chọn mua hàng đầu tiên mang lại cho người dùng lợi ích trực tiếp là 2.000 Dash. Người dùng có thể mua trực tiếp các mặt hàng này và có thể mua nhiều lần. Đây được gọi là tài nguyên tiêu dùng vì được tiêu thụ trực tiếp và có thể được tiêu thụ nhiều lần.
Tuỳ chọn thứ hai sẽ nâng cấp Dash lên một Dash đẹp hơn. Bạn chỉ cần mua một lần và có thể sử dụng mãi mãi. Giao dịch mua như vậy được gọi là giao dịch mua không phải hàng tiêu dùng vì ứng dụng không thể sử dụng giao dịch mua đó nhưng giao dịch mua đó có hiệu lực vĩnh viễn.
Lựa chọn mua thứ ba và cuối cùng là gói thuê bao. Khi gói thuê bao đang hoạt động, người dùng sẽ nhận được Dash nhanh hơn, nhưng khi họ ngừng thanh toán cho gói thuê bao, các lợi ích cũng sẽ biến mất.
Dịch vụ phụ trợ (cũng được cung cấp cho bạn) chạy dưới dạng ứng dụng Dart, xác minh rằng các giao dịch mua đã được thực hiện và lưu trữ các giao dịch mua đó bằng Firestore. Firestore được dùng để giúp quá trình này dễ dàng hơn, nhưng trong ứng dụng chính thức, bạn có thể sử dụng bất kỳ loại dịch vụ phụ trợ nào.
Sản phẩm bạn sẽ tạo ra
- Bạn sẽ mở rộng ứng dụng để hỗ trợ các giao dịch mua hàng tiêu dùng và gói thuê bao.
- Bạn cũng sẽ mở rộng một ứng dụng phụ trợ Dart để xác minh và lưu trữ các mặt hàng đã mua.
Kiến thức bạn sẽ học được
- Cách định cấu hình App Store và Cửa hàng Play cho các sản phẩm có thể mua.
- Cách giao tiếp với các cửa hàng để xác minh giao dịch mua và lưu trữ các giao dịch mua đó trong Firestore.
- Cách quản lý giao dịch mua trong ứng dụng.
Bạn cần có
- Android Studio 4.1 trở lên
- Xcode 12 trở lên (dành cho hoạt động phát triển iOS)
- SDK Flutter
2. Thiết lập môi trường phát triển
Để bắt đầu lớp học lập trình này, hãy tải mã xuống và thay đổi giá trị nhận dạng gói cho iOS và tên gói cho Android.
Tải mã xuống
Để sao chép kho lưu trữ GitHub từ dòng lệnh, hãy sử dụng lệnh sau:
git clone https://github.com/flutter/codelabs.git flutter-codelabs
Hoặc nếu bạn đã cài đặt công cụ cli của GitHub, hãy sử dụng lệnh sau:
gh repo clone flutter/codelabs flutter-codelabs
Mã mẫu được nhân bản vào thư mục flutter-codelabs
chứa mã cho một tập hợp các lớp học lập trình. Mã cho lớp học lập trình này nằm trong flutter-codelabs/in_app_purchases
.
Cấu trúc thư mục trong flutter-codelabs/in_app_purchases
chứa một loạt ảnh chụp nhanh về vị trí bạn sẽ ở cuối mỗi bước được đặt tên. Mã khởi động nằm ở bước 0, vì vậy, bạn có thể dễ dàng tìm thấy các tệp trùng khớp bằng cách:
cd flutter-codelabs/in_app_purchases/step_00
Nếu bạn muốn chuyển tiếp hoặc xem nội dung sau một bước, hãy tìm trong thư mục được đặt theo tên của bước mà bạn quan tâm. Mã của bước cuối cùng nằm trong thư mục complete
.
Thiết lập dự án ban đầu
Mở dự án khởi động từ step_00
trong IDE mà bạn yêu thích. Chúng tôi đã sử dụng Android Studio để chụp ảnh màn hình, nhưng Visual Studio Code cũng là một lựa chọn tuyệt vời. Với bất kỳ trình chỉnh sửa nào, hãy đảm bảo bạn đã cài đặt các trình bổ trợ Dart và Flutter mới nhất.
Ứng dụng mà bạn sắp tạo cần giao tiếp với App Store và Cửa hàng Play để biết những sản phẩm nào có sẵn và giá của sản phẩm. Mỗi ứng dụng được xác định bằng một mã nhận dạng duy nhất. Đối với App Store của iOS, giá trị này được gọi là mã nhận dạng gói và đối với Cửa hàng Play của Android, giá trị này là mã ứng dụng. Các giá trị nhận dạng này thường được tạo bằng ký hiệu tên miền ngược. Ví dụ: khi tạo ứng dụng mua hàng trong ứng dụng cho flutter.dev, bạn sẽ sử dụng dev.flutter.inapppurchase
. Hãy nghĩ đến một giá trị nhận dạng cho ứng dụng của bạn, bạn sẽ thiết lập giá trị đó trong phần cài đặt dự án.
Trước tiên, hãy thiết lập giá trị nhận dạng gói cho iOS.
Khi dự án đang mở trong Android Studio, hãy nhấp chuột phải vào thư mục iOS, nhấp vào Flutter rồi mở mô-đun trong ứng dụng Xcode.
Trong cấu trúc thư mục của Xcode, Dự án Runner (Dự án trình chạy) nằm ở trên cùng và các mục tiêu Flutter (Flutter), Runner (Trình chạy) và Products (Sản phẩm) nằm bên dưới dự án Runner. Nhấp đúp vào Runner (Trình chạy) để chỉnh sửa chế độ cài đặt dự án, rồi nhấp vào Signing & Capabilities (Ký và chức năng). Nhập giá trị nhận dạng gói mà bạn vừa chọn trong trường Nhóm để đặt nhóm.
Giờ đây, bạn có thể đóng Xcode và quay lại Android Studio để hoàn tất cấu hình cho Android. Để thực hiện việc này, hãy mở tệp build.gradle
trong android/app,
và thay đổi applicationId
(trên dòng 37 trong ảnh chụp màn hình bên dưới) thành mã ứng dụng, giống với giá trị nhận dạng gói iOS. Xin lưu ý rằng mã nhận dạng cho cửa hàng iOS và Android không nhất thiết phải giống nhau, tuy nhiên việc giữ cho các mã nhận dạng này giống nhau sẽ ít gặp lỗi hơn. Do đó, trong lớp học lập trình này, chúng ta cũng sẽ sử dụng các giá trị nhận dạng giống nhau.
3. Cài đặt trình bổ trợ
Trong phần này của lớp học lập trình, bạn sẽ cài đặt trình bổ trợ in_app_purchase.
Thêm phần phụ thuộc trong pubspec
Thêm in_app_purchase
vào pubspec bằng cách thêm in_app_purchase
vào các phần phụ thuộc trong pubspec:
$ cd app $ flutter pub add in_app_purchase dev:in_app_purchase_platform_interface
Mở pubspec.yaml
và xác nhận rằng bạn hiện đã có in_app_purchase
được liệt kê dưới dạng mục trong dependencies
và in_app_purchase_platform_interface
trong dev_dependencies
.
pubspec.yaml
dependencies:
flutter:
sdk: flutter
cloud_firestore: ^5.5.1
cupertino_icons: ^1.0.8
firebase_auth: ^5.3.4
firebase_core: ^3.8.1
google_sign_in: ^6.2.2
http: ^1.2.2
intl: ^0.20.1
provider: ^6.1.2
in_app_purchase: ^3.2.0
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^4.0.0
in_app_purchase_platform_interface: ^1.4.0
Nhấp vào pub get để tải gói xuống hoặc chạy flutter pub get
trong dòng lệnh.
4. Thiết lập App Store
Để thiết lập và thử nghiệm giao dịch mua hàng trong ứng dụng trên iOS, bạn cần tạo một ứng dụng mới trong App Store và tạo sản phẩm có thể mua trong ứng dụng đó. Bạn không cần phải xuất bản nội dung nào hoặc gửi ứng dụng cho Apple để xem xét. Bạn cần có tài khoản nhà phát triển để thực hiện việc này. Nếu bạn chưa có tài khoản, hãy đăng ký chương trình dành cho nhà phát triển của Apple.
Thoả thuận về ứng dụng có tính phí
Để sử dụng tính năng mua hàng trong ứng dụng, bạn cũng cần có thoả thuận còn hiệu lực cho ứng dụng có tính phí trong App Store Connect. Truy cập vào https://appstoreconnect.apple.com/ rồi nhấp vào Thoả thuận, thuế và ngân hàng.
Tại đây, bạn sẽ thấy các thoả thuận dành cho ứng dụng miễn phí và có tính phí. Trạng thái của ứng dụng miễn phí phải là đang hoạt động và trạng thái của ứng dụng có tính phí phải là mới. Hãy nhớ xem, chấp nhận các điều khoản và nhập tất cả thông tin bắt buộc.
Khi bạn thiết lập mọi thứ đúng cách, trạng thái của ứng dụng có tính phí sẽ là đang hoạt động. Điều này rất quan trọng vì bạn sẽ không thể thử mua hàng trong ứng dụng nếu không có thoả thuận đang hoạt động.
Đăng ký mã ứng dụng
Tạo một giá trị nhận dạng mới trong cổng thông tin dành cho nhà phát triển của Apple.
Chọn Mã ứng dụng
Chọn ứng dụng
Cung cấp một số nội dung mô tả và đặt mã nhận dạng gói sao cho khớp với mã nhận dạng gói đã đặt trước đó trong XCode.
Để biết thêm hướng dẫn về cách tạo mã ứng dụng mới, hãy xem phần Trợ giúp về tài khoản nhà phát triển .
Tạo ứng dụng mới
Tạo một ứng dụng mới trong App Store Connect bằng giá trị nhận dạng gói duy nhất của bạn.
Để biết thêm hướng dẫn về cách tạo ứng dụng mới và quản lý thoả thuận, hãy xem phần Trợ giúp về App Store Connect.
Để kiểm thử giao dịch mua hàng trong ứng dụng, bạn cần có người dùng thử nghiệm trong hộp cát. Người dùng thử nghiệm này không được kết nối với iTunes – người dùng này chỉ được dùng để kiểm thử giao dịch mua hàng trong ứng dụng. Bạn không thể sử dụng địa chỉ email đã được dùng cho một tài khoản Apple. Trong phần Người dùng và quyền truy cập, hãy chuyển đến mục Người kiểm thử trong phần Sandbox để tạo tài khoản hộp cát mới hoặc quản lý mã Apple ID hộp cát hiện có.
Giờ đây, bạn có thể thiết lập người dùng hộp cát trên iPhone bằng cách chuyển đến phần Cài đặt > App Store > Sandbox-account (Cài đặt > App Store > Tài khoản hộp cát).
Định cấu hình giao dịch mua hàng trong ứng dụng
Bây giờ, bạn sẽ định cấu hình 3 mặt hàng có thể mua:
dash_consumable_2k
: Giao dịch mua hàng tiêu dùng có thể mua nhiều lần, cấp cho người dùng 2.000 Dash (đơn vị tiền tệ trong ứng dụng) cho mỗi giao dịch mua.dash_upgrade_3d
: Giao dịch mua "nâng cấp" không phải hàng tiêu dùng, chỉ có thể mua một lần và cung cấp cho người dùng một Dash khác về mặt thẩm mỹ để nhấp vào.dash_subscription_doubler
: Gói thuê bao cấp cho người dùng số lượng Dash gấp đôi cho mỗi lượt nhấp trong thời gian thuê bao.
Chuyển đến phần Giao dịch mua hàng trong ứng dụng > Quản lý.
Tạo giao dịch mua hàng trong ứng dụng bằng mã nhận dạng được chỉ định:
- Thiết lập
dash_consumable_2k
làm Mặt hàng tiêu dùng.
Sử dụng dash_consumable_2k
làm Mã sản phẩm. Tên tham chiếu chỉ được dùng trong App Store Connect, bạn chỉ cần đặt tên này thành dash consumable 2k
và thêm bản địa hoá cho giao dịch mua. Gọi giao dịch mua Spring is in the air
với nội dung mô tả là 2000 dashes fly out
.
- Thiết lập
dash_upgrade_3d
làm Không phải hàng tiêu dùng.
Sử dụng dash_upgrade_3d
làm Mã sản phẩm. Đặt tên tham chiếu thành dash upgrade 3d
và thêm nội dung bản địa hoá cho giao dịch mua. Gọi giao dịch mua 3D Dash
với nội dung mô tả là Brings your dash back to the future
.
- Thiết lập
dash_subscription_doubler
làm Gói thuê bao tự động gia hạn.
Quy trình cho gói thuê bao có đôi chút khác biệt. Trước tiên, bạn phải đặt Tên tham chiếu và Mã sản phẩm:
Tiếp theo, bạn phải tạo một nhóm gói thuê bao. Khi nhiều gói thuê bao thuộc cùng một nhóm, người dùng chỉ có thể đăng ký một trong các gói thuê bao đó cùng một lúc, nhưng có thể dễ dàng nâng cấp hoặc hạ cấp giữa các gói thuê bao này. Bạn chỉ cần gọi nhóm này là subscriptions
.
Tiếp theo, hãy nhập thời hạn của gói thuê bao và các bản địa hoá. Đặt tên cho gói thuê bao này là Jet Engine
với nội dung mô tả là Doubles your clicks
. Nhấp vào Lưu.
Sau khi nhấp vào nút Lưu, hãy thêm giá gói thuê bao. Chọn bất kỳ mức giá nào bạn muốn.
Lúc này, bạn sẽ thấy 3 giao dịch mua trong danh sách giao dịch mua:
5. Thiết lập Cửa hàng Play
Cũng giống như App Store, bạn cũng cần có tài khoản nhà phát triển cho Cửa hàng Play. Nếu bạn chưa có tài khoản, hãy đăng ký tài khoản.
Tạo ứng dụng mới
Tạo ứng dụng mới trong Google Play Console:
- Mở Play Console.
- Chọn Tất cả ứng dụng > Tạo ứng dụng.
- 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.
- Chỉ định rằng ứng dụng của bạn là trò chơi. Bạn có thể thay đổi những thông tin này về sau.
- Chỉ định rõ ứng dụng của bạn là ứng dụng miễn phí hay có tính phí.
- Thêm một địa chỉ email mà người dùng ứng dụng trên Cửa hàng Play có thể sử dụng để liên hệ với bạn về ứng dụng này.
- Hoàn tất thông tin khai báo theo Nguyên tắc nội dung và luật xuất khẩu của Hoa Kỳ.
- Chọn Tạo ứng dụng.
Sau khi tạo ứng dụng, hãy chuyển đến trang tổng quan và hoàn tất tất cả các nhiệm vụ trong phần Thiết lập ứng dụng. Tại đây, bạn cung cấp một số thông tin về ứng dụng của mình, chẳng hạn như mức phân loại nội dung và ảnh chụp màn hình.
Ký ứng dụng
Để có thể kiểm thử giao dịch mua hàng trong ứng dụng, bạn cần tải ít nhất một bản dựng lên Google Play.
Để làm việc này, bạn cần ký bản phát hành bằng một khoá khác ngoài khoá gỡ lỗi.
Tạo kho khoá
Nếu bạn đã có kho khoá, hãy chuyển sang bước tiếp theo. Nếu không, hãy tạo một tệp bằng cách chạy lệnh sau trên dòng lệnh.
Trên Mac/Linux, hãy sử dụng lệnh sau:
keytool -genkey -v -keystore ~/key.jks -keyalg RSA -keysize 2048 -validity 10000 -alias key
Trên Windows, hãy sử dụng lệnh sau:
keytool -genkey -v -keystore c:\Users\USER_NAME\key.jks -storetype JKS -keyalg RSA -keysize 2048 -validity 10000 -alias key
Lệnh này lưu trữ tệp key.jks
trong thư mục gốc. Nếu bạn muốn lưu trữ tệp ở nơi khác, hãy thay đổi đối số bạn truyền vào tham số -keystore
. Giữ lại
keystore
tệp riêng tư; đừng kiểm tra tệp đó vào hệ thống kiểm soát nguồn công khai!
Tham chiếu kho khoá từ ứng dụng
Tạo một tệp có tên là <your app dir>/android/key.properties
chứa thông tin tham chiếu đến kho khoá:
storePassword=<password from previous step>
keyPassword=<password from previous step>
keyAlias=key
storeFile=<location of the key store file, such as /Users/<user name>/key.jks>
Định cấu hình tính năng ký trong Gradle
Định cấu hình tính năng ký cho ứng dụng bằng cách chỉnh sửa tệp <your app dir>/android/app/build.gradle
.
Thêm thông tin kho khoá từ tệp thuộc tính trước khối android
:
def keystoreProperties = new Properties()
def keystorePropertiesFile = rootProject.file('key.properties')
if (keystorePropertiesFile.exists()) {
keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
}
android {
// omitted
}
Tải tệp key.properties
vào đối tượng keystoreProperties
.
Thêm mã sau vào trước khối buildTypes
:
buildTypes {
release {
// TODO: Add your own signing config for the release build.
// Signing with the debug keys for now,
// so `flutter run --release` works.
signingConfig signingConfigs.debug
}
}
Định cấu hình khối signingConfigs
trong tệp build.gradle
của mô-đun bằng thông tin cấu hình ký:
signingConfigs {
release {
keyAlias keystoreProperties['keyAlias']
keyPassword keystoreProperties['keyPassword']
storeFile keystoreProperties['storeFile'] ? file(keystoreProperties['storeFile']) : null
storePassword keystoreProperties['storePassword']
}
}
buildTypes {
release {
signingConfig signingConfigs.release
}
}
Giờ đây, các bản phát hành của ứng dụng sẽ được ký tự động.
Để biết thêm thông tin về việc ký ứng dụng, hãy xem bài viết Ký ứng dụng trên developer.android.com.
Tải bản dựng đầu tiên lên
Sau khi định cấu hình ứng dụng để ký, bạn có thể tạo ứng dụng bằng cách chạy:
flutter build appbundle
Theo mặc định, lệnh này sẽ tạo một bản phát hành và bạn có thể tìm thấy kết quả tại <your app dir>/build/app/outputs/bundle/release/
Trên trang tổng quan trong Google Play Console, hãy chuyển đến phần Bản phát hành > Thử nghiệm > Thử nghiệm khép kín rồi tạo một bản phát hành thử nghiệm khép kín mới.
Trong lớp học lập trình này, bạn sẽ sử dụng tính năng ký ứng dụng của Google. Vì vậy, hãy nhấn vào Tiếp tục trong phần Tính năng ký ứng dụng của Play để chọn sử dụng tính năng này.
Tiếp theo, hãy tải gói ứng dụng app-release.aab
được tạo bằng lệnh bản dựng lên.
Nhấp vào Lưu, rồi nhấp vào Xem lại bản phát hành.
Cuối cùng, hãy nhấp vào Bắt đầu phát hành cho Kiểm thử nội bộ để kích hoạt bản phát hành kiểm thử nội bộ.
Thiết lập người dùng thử nghiệm
Để có thể kiểm thử giao dịch mua hàng trong ứng dụng, bạn phải thêm tài khoản Google của người kiểm thử vào Google Play Console ở hai vị trí:
- Đến kênh kiểm thử cụ thể (Kiểm thử nội bộ)
- Là người kiểm thử giấy phép
Trước tiên, hãy bắt đầu bằng cách thêm người kiểm thử vào kênh kiểm thử nội bộ. Quay lại phần Bản phát hành > Thử nghiệm > Thử nghiệm nội bộ rồi nhấp vào thẻ Người kiểm thử.
Tạo danh sách email mới bằng cách nhấp vào Tạo danh sách email. Đặt tên cho danh sách và thêm địa chỉ email của những Tài khoản Google cần quyền truy cập để thử nghiệm giao dịch mua hàng trong ứng dụng.
Tiếp theo, hãy đánh dấu vào hộp kiểm của danh sách rồi nhấp vào Lưu thay đổi.
Sau đó, hãy thêm người kiểm thử giấy phép:
- Quay lại chế độ xem Tất cả ứng dụng trên Google Play Console.
- Chuyển đến phần Cài đặt > Kiểm thử giấy phép.
- Thêm cùng một địa chỉ email của những người thử nghiệm cần kiểm thử giao dịch mua hàng trong ứng dụng.
- Đặt License response (Phản hồi giấy phép) thành
RESPOND_NORMALLY
. - Nhấp vào Lưu thay đổi.
Định cấu hình giao dịch mua hàng trong ứng dụng
Bây giờ, bạn sẽ định cấu hình các mặt hàng có thể mua trong ứng dụng.
Cũng giống như trong App Store, bạn phải xác định 3 giao dịch mua:
dash_consumable_2k
: Giao dịch mua hàng tiêu dùng có thể mua nhiều lần, cấp cho người dùng 2.000 Dash (đơn vị tiền tệ trong ứng dụng) cho mỗi giao dịch mua.dash_upgrade_3d
: Giao dịch mua "nâng cấp" không phải hàng tiêu dùng, chỉ có thể mua một lần, giúp người dùng có một Dash khác về mặt thẩm mỹ để nhấp vào.dash_subscription_doubler
: Gói thuê bao cấp cho người dùng số lượng Dash gấp đôi cho mỗi lượt nhấp trong thời gian thuê bao.
Trước tiên, hãy thêm sản phẩm tiêu dùng và sản phẩm không tiêu dùng.
- Truy cập vào Google Play Console rồi chọn ứng dụng của bạn.
- Chuyển đến phần Kiếm tiền > Sản phẩm > Sản phẩm trong ứng dụng.
- Nhấp vào Tạo sản phẩm
- Nhập tất cả thông tin bắt buộc cho sản phẩm của bạn. Đảm bảo mã sản phẩm khớp chính xác với mã bạn định sử dụng.
- Nhấp vào Lưu.
- Nhấp vào Kích hoạt.
- Lặp lại quy trình này cho giao dịch mua "nâng cấp" không phải hàng tiêu dùng.
Tiếp theo, hãy thêm gói thuê bao:
- Truy cập vào Google Play Console rồi chọn ứng dụng của bạn.
- Chuyển đến phần Kiếm tiền > Sản phẩm > Gói thuê bao.
- Nhấp vào Tạo gói thuê bao
- Nhập tất cả thông tin bắt buộc cho gói thuê bao của bạn. Đảm bảo mã sản phẩm khớp chính xác với mã bạn định sử dụng.
- Nhấp vào Lưu
Giờ đây, bạn đã thiết lập xong giao dịch mua trong Play Console.
6. Thiết lập Firebase
Trong lớp học lập trình này, bạn sẽ sử dụng một dịch vụ phụ trợ để xác minh và theo dõi giao dịch mua của người dùng.
Việc sử dụng dịch vụ phụ trợ mang lại một số lợi ích:
- Bạn có thể xác minh giao dịch một cách an toàn.
- Bạn có thể phản hồi các sự kiện thanh toán từ cửa hàng ứng dụng.
- Bạn có thể theo dõi các giao dịch mua hàng trong cơ sở dữ liệu.
- Người dùng sẽ không thể lừa ứng dụng của bạn cung cấp các tính năng nâng cao bằng cách tua lại đồng hồ hệ thống.
Mặc dù có nhiều cách để thiết lập dịch vụ phụ trợ, nhưng bạn sẽ thực hiện việc này bằng cách sử dụng các hàm trên đám mây và Firestore, thông qua Firebase của chính Google.
Việc viết phần phụ trợ được coi là nằm ngoài phạm vi của lớp học lập trình này, vì vậy, mã khởi động đã bao gồm một dự án Firebase xử lý các giao dịch mua cơ bản để bạn bắt đầu.
Trình bổ trợ Firebase cũng được đưa vào ứng dụng khởi động.
Việc còn lại là tạo dự án Firebase của riêng bạn, định cấu hình cả ứng dụng và phần phụ trợ cho Firebase, sau đó triển khai phần phụ trợ.
Tạo dự án Firebase
Truy cập vào bảng điều khiển của Firebase rồi tạo một dự án Firebase mới. Trong ví dụ này, hãy gọi dự án là Dash Clicker.
Trong ứng dụng phụ trợ, bạn liên kết giao dịch mua với một người dùng cụ thể, do đó, bạn cần xác thực. Để làm việc này, hãy tận dụng mô-đun xác thực của Firebase bằng tính năng đăng nhập bằng Google.
- Trên trang tổng quan Firebase, hãy chuyển đến phần Xác thực rồi bật tính năng này nếu cần.
- 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.
Vì bạn cũng sẽ sử dụng cơ sở dữ liệu Firestore của Firebase, nên hãy bật tuỳ chọn này.
Thiết lập các quy tắc Cloud Firestore như sau:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /purchases/{purchaseId} {
allow read: if request.auth != null && request.auth.uid == resource.data.userId
}
}
}
Thiết lập Firebase cho Flutter
Bạn nên sử dụng FlutterFire CLI để cài đặt Firebase trên ứng dụng Flutter. Làm theo hướng dẫn như được giải thích trong trang thiết lập.
Khi chạy flutterfire configure, hãy chọn dự án bạn vừa tạo ở bước trước.
$ flutterfire configure
i Found 5 Firebase projects.
? Select a Firebase project to configure your Flutter application with ›
❯ in-app-purchases-1234 (in-app-purchases-1234)
other-flutter-codelab-1 (other-flutter-codelab-1)
other-flutter-codelab-2 (other-flutter-codelab-2)
other-flutter-codelab-3 (other-flutter-codelab-3)
other-flutter-codelab-4 (other-flutter-codelab-4)
<create a new project>
Tiếp theo, hãy bật iOS và Android bằng cách chọn hai nền tảng này.
? Which platforms should your configuration support (use arrow keys & space to select)? ›
✔ android
✔ ios
macos
web
Khi được nhắc về việc ghi đè firebase_options.dart, hãy chọn yes (có).
? Generated FirebaseOptions file lib/firebase_options.dart already exists, do you want to override it? (y/n) › yes
Thiết lập Firebase cho Android: Các bước tiếp theo
Trên trang tổng quan của Firebase, hãy chuyển đến phần Tổng quan về dự án,chọn Cài đặt rồi chọn thẻ Chung.
Di chuyển xuống phần Ứng dụng của bạn rồi chọn ứng dụng dashclicker (android).
Để cho phép đăng nhập bằng Google ở chế độ gỡ lỗi, bạn phải cung cấp dấu vân tay băm SHA-1 của chứng chỉ gỡ lỗi.
Lấy hàm băm chứng chỉ ký gỡ lỗi
Trong thư mục gốc của dự án ứng dụng Flutter, hãy thay đổi thư mục thành thư mục android/
, sau đó tạo báo cáo ký.
cd android ./gradlew :app:signingReport
Bạn sẽ thấy một danh sách lớn các khoá ký. Vì bạn đang tìm hàm băm cho chứng chỉ gỡ lỗi, hãy tìm chứng chỉ có thuộc tính Variant
và Config
được đặt thành debug
. Kho khoá có thể nằm trong thư mục gốc của bạn trong .android/debug.keystore
.
> Task :app:signingReport
Variant: debug
Config: debug
Store: /<USER_HOME_FOLDER>/.android/debug.keystore
Alias: AndroidDebugKey
MD5: XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX
SHA1: XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX
SHA-256: XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX
Valid until: Tuesday, January 19, 2038
Sao chép hàm băm SHA-1 rồi điền vào trường cuối cùng trong hộp thoại phương thức gửi ứng dụng.
Thiết lập Firebase cho iOS: Các bước tiếp theo
Mở ios/Runnder.xcworkspace
bằng Xcode
. Hoặc bằng IDE mà bạn chọn.
Trên VSCode, hãy nhấp chuột phải vào thư mục ios/
rồi nhấp vào open in xcode
.
Trên Android Studio, hãy nhấp chuột phải vào thư mục ios/
, sau đó nhấp vào flutter
, rồi nhấp vào tuỳ chọn open iOS module in Xcode
.
Để cho phép đăng nhập bằng Google trên iOS, hãy thêm tuỳ chọn cấu hình CFBundleURLTypes
vào các tệp plist
của bản dựng. (Hãy xem tài liệu về gói google_sign_in
để biết thêm thông tin.) Trong trường hợp này, các tệp là ios/Runner/Info-Debug.plist
và ios/Runner/Info-Release.plist
.
Cặp khoá-giá trị đã được thêm, nhưng bạn phải thay thế các giá trị của cặp này:
- Lấy giá trị cho
REVERSED_CLIENT_ID
từ tệpGoogleService-Info.plist
, mà không có phần tử<string>..</string>
bao quanh. - Thay thế giá trị trong cả tệp
ios/Runner/Info-Debug.plist
vàios/Runner/Info-Release.plist
theo khoáCFBundleURLTypes
.
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>CFBundleURLSchemes</key>
<array>
<!-- TODO Replace this value: -->
<!-- Copied from GoogleService-Info.plist key REVERSED_CLIENT_ID -->
<string>com.googleusercontent.apps.REDACTED</string>
</array>
</dict>
</array>
Bạn đã hoàn tất việc thiết lập Firebase.
7. Nghe thông tin cập nhật về giao dịch mua
Trong phần này của lớp học lập trình, bạn sẽ chuẩn bị ứng dụng để mua sản phẩm. Quá trình này bao gồm việc theo dõi thông tin cập nhật về giao dịch mua và lỗi sau khi ứng dụng khởi động.
Nghe thông tin cập nhật về giao dịch mua
Trong main.dart,
, hãy tìm tiện ích MyHomePage
có Scaffold
với BottomNavigationBar
chứa hai trang. Trang này cũng tạo ba Provider
cho DashCounter
, DashUpgrades,
và DashPurchases
. DashCounter
theo dõi số lượng Dấu gạch ngang hiện tại và tự động tăng số lượng đó. DashUpgrades
quản lý các bản nâng cấp mà bạn có thể mua bằng Dashes. Lớp học lập trình này tập trung vào DashPurchases
.
Theo mặc định, đối tượng của nhà cung cấp được xác định khi đối tượng đó được yêu cầu lần đầu tiên. Đối tượng này trực tiếp theo dõi các bản cập nhật giao dịch mua khi ứng dụng khởi động, vì vậy, hãy tắt tính năng tải lười trên đối tượng này bằng lazy: false
:
lib/main.dart
ChangeNotifierProvider<DashPurchases>(
create: (context) => DashPurchases(
context.read<DashCounter>(),
),
lazy: false, // Add this line
),
Bạn cũng cần một thực thể của InAppPurchaseConnection
. Tuy nhiên, để có thể kiểm thử ứng dụng, bạn cần có một số cách để mô phỏng kết nối. Để thực hiện việc này, hãy tạo một phương thức thực thể có thể được ghi đè trong kiểm thử và thêm phương thức đó vào main.dart
.
lib/main.dart
// Gives the option to override in tests.
class IAPConnection {
static InAppPurchase? _instance;
static set instance(InAppPurchase value) {
_instance = value;
}
static InAppPurchase get instance {
_instance ??= InAppPurchase.instance;
return _instance!;
}
}
Bạn phải cập nhật một chút mã kiểm thử nếu muốn mã kiểm thử tiếp tục hoạt động.
test/widget_test.dart
import 'package:dashclicker/main.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:in_app_purchase/in_app_purchase.dart'; // Add this import
import 'package:in_app_purchase_platform_interface/src/in_app_purchase_platform_addition.dart'; // And this import
void main() {
testWidgets('App starts', (tester) async {
IAPConnection.instance = TestIAPConnection(); // Add this line
await tester.pumpWidget(const MyApp());
expect(find.text('Tim Sneath'), findsOneWidget);
});
}
class TestIAPConnection implements InAppPurchase { // Add from here
@override
Future<bool> buyConsumable(
{required PurchaseParam purchaseParam, bool autoConsume = true}) {
return Future.value(false);
}
@override
Future<bool> buyNonConsumable({required PurchaseParam purchaseParam}) {
return Future.value(false);
}
@override
Future<void> completePurchase(PurchaseDetails purchase) {
return Future.value();
}
@override
Future<bool> isAvailable() {
return Future.value(false);
}
@override
Future<ProductDetailsResponse> queryProductDetails(Set<String> identifiers) {
return Future.value(ProductDetailsResponse(
productDetails: [],
notFoundIDs: [],
));
}
@override
T getPlatformAddition<T extends InAppPurchasePlatformAddition?>() {
// TODO: implement getPlatformAddition
throw UnimplementedError();
}
@override
Stream<List<PurchaseDetails>> get purchaseStream =>
Stream.value(<PurchaseDetails>[]);
@override
Future<void> restorePurchases({String? applicationUserName}) {
// TODO: implement restorePurchases
throw UnimplementedError();
}
@override
Future<String> countryCode() {
// TODO: implement countryCode
throw UnimplementedError();
}
} // To here.
Trong lib/logic/dash_purchases.dart
, hãy chuyển đến mã cho DashPurchases ChangeNotifier
. Hiện tại, bạn chỉ có thể thêm DashCounter
vào các Dash đã mua.
Thêm thuộc tính đăng ký luồng, _subscription
(loại StreamSubscription<List<PurchaseDetails>> _subscription;
), IAPConnection.instance,
và các tệp nhập. Mã kết quả sẽ có dạng như sau:
lib/logic/dash_purchases.dart
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:in_app_purchase/in_app_purchase.dart'; // Add this import
import '../main.dart'; // And this import
import '../model/purchasable_product.dart';
import '../model/store_state.dart';
import 'dash_counter.dart';
class DashPurchases extends ChangeNotifier {
DashCounter counter;
StoreState storeState = StoreState.available;
late StreamSubscription<List<PurchaseDetails>> _subscription; // Add this line
List<PurchasableProduct> products = [
PurchasableProduct(
'Spring is in the air',
'Many dashes flying out from their nests',
'\$0.99',
),
PurchasableProduct(
'Jet engine',
'Doubles you clicks per second for a day',
'\$1.99',
),
];
bool get beautifiedDash => false;
final iapConnection = IAPConnection.instance; // And this line
DashPurchases(this.counter);
Future<void> buy(PurchasableProduct product) async {
product.status = ProductStatus.pending;
notifyListeners();
await Future<void>.delayed(const Duration(seconds: 5));
product.status = ProductStatus.purchased;
notifyListeners();
await Future<void>.delayed(const Duration(seconds: 5));
product.status = ProductStatus.purchasable;
notifyListeners();
}
}
Từ khoá late
được thêm vào _subscription
vì _subscription
được khởi tạo trong hàm khởi tạo. Dự án này được thiết lập để không có giá trị rỗng theo mặc định (NNBD), nghĩa là các thuộc tính không được khai báo là có thể rỗng phải có giá trị không rỗng. Bộ hạn định late
cho phép bạn trì hoãn việc xác định giá trị này.
Trong hàm khởi tạo, hãy lấy luồng purchaseUpdated
và bắt đầu nghe luồng đó. Trong phương thức dispose()
, hãy huỷ gói thuê bao luồng.
lib/logic/dash_purchases.dart
class DashPurchases extends ChangeNotifier {
DashCounter counter;
StoreState storeState = StoreState.notAvailable; // Modify this line
late StreamSubscription<List<PurchaseDetails>> _subscription;
List<PurchasableProduct> products = [
PurchasableProduct(
'Spring is in the air',
'Many dashes flying out from their nests',
'\$0.99',
),
PurchasableProduct(
'Jet engine',
'Doubles you clicks per second for a day',
'\$1.99',
),
];
bool get beautifiedDash => false;
final iapConnection = IAPConnection.instance;
DashPurchases(this.counter) { // Add from here
final purchaseUpdated = iapConnection.purchaseStream;
_subscription = purchaseUpdated.listen(
_onPurchaseUpdate,
onDone: _updateStreamOnDone,
onError: _updateStreamOnError,
);
}
@override
void dispose() {
_subscription.cancel();
super.dispose();
} // To here.
Future<void> buy(PurchasableProduct product) async {
product.status = ProductStatus.pending;
notifyListeners();
await Future<void>.delayed(const Duration(seconds: 5));
product.status = ProductStatus.purchased;
notifyListeners();
await Future<void>.delayed(const Duration(seconds: 5));
product.status = ProductStatus.purchasable;
notifyListeners();
}
// Add from here
void _onPurchaseUpdate(List<PurchaseDetails> purchaseDetailsList) {
// Handle purchases here
}
void _updateStreamOnDone() {
_subscription.cancel();
}
void _updateStreamOnError(dynamic error) {
//Handle error here
} // To here.
}
Bây giờ, ứng dụng sẽ nhận được thông tin cập nhật về giao dịch mua. Vì vậy, trong phần tiếp theo, bạn sẽ thực hiện một giao dịch mua!
Trước khi tiếp tục, hãy chạy các chương trình kiểm thử bằng "flutter test"
để xác minh mọi thứ đã được thiết lập chính xác.
$ flutter test
00:01 +1: All tests passed!
8. Mua hàng
Trong phần này của lớp học lập trình, bạn sẽ thay thế các sản phẩm mô phỏng hiện có bằng các sản phẩm thực tế có thể mua. Những sản phẩm này được tải từ các cửa hàng, hiển thị trong danh sách và được mua khi người dùng nhấn vào sản phẩm.
Điều chỉnh PurchasableProduct
PurchasableProduct
hiển thị một sản phẩm mô phỏng. Cập nhật để hiển thị nội dung thực tế bằng cách thay thế lớp PurchasableProduct
trong purchasable_product.dart
bằng mã sau:
lib/model/purchasable_product.dart
import 'package:in_app_purchase/in_app_purchase.dart';
enum ProductStatus {
purchasable,
purchased,
pending,
}
class PurchasableProduct {
String get id => productDetails.id;
String get title => productDetails.title;
String get description => productDetails.description;
String get price => productDetails.price;
ProductStatus status;
ProductDetails productDetails;
PurchasableProduct(this.productDetails) : status = ProductStatus.purchasable;
}
Trong dash_purchases.dart,
, hãy xoá các giao dịch mua giả và thay thế bằng một danh sách trống, List<PurchasableProduct> products = [];
Tải các giao dịch mua hiện có
Để cho phép người dùng mua hàng, hãy tải các giao dịch mua từ cửa hàng. Trước tiên, hãy kiểm tra xem cửa hàng có hoạt động hay không. Khi không có cửa hàng, việc đặt storeState
thành notAvailable
sẽ hiển thị thông báo lỗi cho người dùng.
lib/logic/dash_purchases.dart
Future<void> loadPurchases() async {
final available = await iapConnection.isAvailable();
if (!available) {
storeState = StoreState.notAvailable;
notifyListeners();
return;
}
}
Khi có cửa hàng, hãy tải các giao dịch mua hiện có. Với chế độ thiết lập Firebase trước đó, bạn sẽ thấy storeKeyConsumable
, storeKeySubscription,
và storeKeyUpgrade
. Khi không có giao dịch mua dự kiến, hãy in thông tin này vào bảng điều khiển; bạn cũng có thể gửi thông tin này đến dịch vụ phụ trợ.
Phương thức await iapConnection.queryProductDetails(ids)
trả về cả mã không tìm thấy và sản phẩm có thể mua được. Sử dụng productDetails
từ phản hồi để cập nhật giao diện người dùng và đặt StoreState
thành available
.
lib/logic/dash_purchases.dart
import '../constants.dart';
Future<void> loadPurchases() async {
final available = await iapConnection.isAvailable();
if (!available) {
storeState = StoreState.notAvailable;
notifyListeners();
return;
}
const ids = <String>{
storeKeyConsumable,
storeKeySubscription,
storeKeyUpgrade,
};
final response = await iapConnection.queryProductDetails(ids);
products = response.productDetails.map((e) => PurchasableProduct(e)).toList();
storeState = StoreState.available;
notifyListeners();
}
Gọi hàm loadPurchases()
trong hàm khởi tạo:
lib/logic/dash_purchases.dart
DashPurchases(this.counter) {
final purchaseUpdated = iapConnection.purchaseStream;
_subscription = purchaseUpdated.listen(
_onPurchaseUpdate,
onDone: _updateStreamOnDone,
onError: _updateStreamOnError,
);
loadPurchases();
}
Cuối cùng, hãy thay đổi giá trị của trường storeState
từ StoreState.available
thành StoreState.loading:
lib/logic/dash_purchases.dart
StoreState storeState = StoreState.loading;
Hiển thị các sản phẩm có thể mua
Hãy xem xét tệp purchase_page.dart
. Tiện ích PurchasePage
hiển thị _PurchasesLoading
, _PurchaseList,
hoặc _PurchasesNotAvailable,
tuỳ thuộc vào StoreState
. Tiện ích này cũng cho thấy các giao dịch mua trước đây của người dùng được sử dụng ở bước tiếp theo.
Tiện ích _PurchaseList
hiển thị danh sách các sản phẩm có thể mua và gửi yêu cầu mua đến đối tượng DashPurchases
.
lib/pages/purchase_page.dart
class _PurchaseList extends StatelessWidget {
@override
Widget build(BuildContext context) {
var purchases = context.watch<DashPurchases>();
var products = purchases.products;
return Column(
children: products
.map((product) => _PurchaseWidget(
product: product,
onPressed: () {
purchases.buy(product);
}))
.toList(),
);
}
}
Bạn sẽ thấy các sản phẩm có sẵn trên cửa hàng Android và iOS nếu các sản phẩm đó được thiết lập đúng cách. Xin lưu ý rằng có thể mất chút thời gian thì giao dịch mua mới có hiệu lực khi được nhập vào bảng điều khiển tương ứng.
Quay lại dash_purchases.dart
và triển khai hàm để mua sản phẩm. Bạn chỉ cần tách biệt hàng tiêu dùng với hàng không phải hàng tiêu dùng. Bản nâng cấp và sản phẩm thuê bao không phải là sản phẩm tiêu dùng.
lib/logic/dash_purchases.dart
Future<void> buy(PurchasableProduct product) async {
final purchaseParam = PurchaseParam(productDetails: product.productDetails);
switch (product.id) {
case storeKeyConsumable:
await iapConnection.buyConsumable(purchaseParam: purchaseParam);
case storeKeySubscription:
case storeKeyUpgrade:
await iapConnection.buyNonConsumable(purchaseParam: purchaseParam);
default:
throw ArgumentError.value(
product.productDetails, '${product.id} is not a known product');
}
}
Trước khi tiếp tục, hãy tạo biến _beautifiedDashUpgrade
và cập nhật phương thức getter beautifiedDash
để tham chiếu đến biến đó.
lib/logic/dash_purchases.dart
bool get beautifiedDash => _beautifiedDashUpgrade;
bool _beautifiedDashUpgrade = false;
Phương thức _onPurchaseUpdate
nhận thông tin cập nhật về giao dịch mua, cập nhật trạng thái của sản phẩm xuất hiện trong trang mua hàng và áp dụng giao dịch mua cho logic bộ đếm. Bạn cần gọi completePurchase
sau khi xử lý giao dịch mua để cửa hàng biết rằng giao dịch mua đã được xử lý đúng cách.
lib/logic/dash_purchases.dart
Future<void> _onPurchaseUpdate(
List<PurchaseDetails> purchaseDetailsList) async {
for (var purchaseDetails in purchaseDetailsList) {
await _handlePurchase(purchaseDetails);
}
notifyListeners();
}
Future<void> _handlePurchase(PurchaseDetails purchaseDetails) async {
if (purchaseDetails.status == PurchaseStatus.purchased) {
switch (purchaseDetails.productID) {
case storeKeySubscription:
counter.applyPaidMultiplier();
case storeKeyConsumable:
counter.addBoughtDashes(2000);
case storeKeyUpgrade:
_beautifiedDashUpgrade = true;
}
}
if (purchaseDetails.pendingCompletePurchase) {
await iapConnection.completePurchase(purchaseDetails);
}
}
9. Thiết lập phần phụ trợ
Trước khi chuyển sang theo dõi và xác minh giao dịch mua, hãy thiết lập phần phụ trợ Dart để hỗ trợ việc này.
Trong phần này, hãy làm việc từ thư mục dart-backend/
làm thư mục gốc.
Đảm bảo bạn đã cài đặt các công cụ sau:
Tổng quan về dự án cơ sở
Vì một số phần của dự án này được coi là nằm ngoài phạm vi của lớp học lập trình này, nên chúng được đưa vào mã khởi động. Bạn nên xem lại những gì đã có trong mã khởi động trước khi bắt đầu để biết cách bạn sẽ sắp xếp mọi thứ.
Mã phụ trợ này có thể chạy cục bộ trên máy của bạn, bạn không cần triển khai mã này để sử dụng. Tuy nhiên, bạn cần có thể kết nối từ thiết bị phát triển (Android hoặc iPhone) với máy sẽ chạy máy chủ. Để làm được việc đó, các thiết bị phải nằm trong cùng một mạng và bạn cần biết địa chỉ IP của máy.
Hãy thử chạy máy chủ bằng lệnh sau:
$ dart ./bin/server.dart
Serving at http://0.0.0.0:8080
Phần phụ trợ Dart sử dụng shelf
và shelf_router
để phân phát các điểm cuối API. Theo mặc định, máy chủ không cung cấp bất kỳ tuyến nào. Sau đó, bạn sẽ tạo một tuyến để xử lý quy trình xác minh giao dịch mua.
Một phần đã có trong mã khởi động là IapRepository
trong lib/iap_repository.dart
. Vì việc tìm hiểu cách tương tác với Firestore hoặc cơ sở dữ liệu nói chung không được coi là phù hợp với lớp học lập trình này, nên mã khởi động chứa các hàm để bạn tạo hoặc cập nhật giao dịch mua trong Firestore, cũng như tất cả các lớp cho các giao dịch mua đó.
Thiết lập quyền truy cập vào Firebase
Để truy cập vào Firebase Firestore, bạn cần có khoá truy cập vào tài khoản dịch vụ. Tạo một khoá bằng cách mở phần cài đặt dự án Firebase rồi chuyển đến phần Tài khoản dịch vụ, sau đó chọn Tạo khoá riêng tư mới.
Sao chép tệp JSON đã tải xuống vào thư mục assets/
rồi đổi tên tệp đó thành service-account-firebase.json
.
Thiết lập quyền truy cập vào Google Play
Để truy cập vào Cửa hàng Play nhằm xác minh giao dịch mua, bạn phải tạo một tài khoản dịch vụ có các quyền này và tải thông tin xác thực JSON xuống cho tài khoản đó.
- Truy cập vào Google Play Console rồi bắt đầu từ trang Tất cả ứng dụng.
- Chuyển đến phần Thiết lập > Quyền truy cập API.
Trong trường hợp Google Play Console yêu cầu bạn tạo hoặc liên kết với một dự án hiện có, hãy làm việc đó trước rồi quay lại trang này.
- Tìm phần mà bạn có thể xác định tài khoản dịch vụ rồi nhấp vào Tạo tài khoản dịch vụ mới.
- Nhấp vào đường liên kết Google Cloud Platform trong hộp thoại bật lên.
- Chọn dự án của bạn. Nếu bạn không thấy, hãy đảm bảo rằng bạn đã đăng nhập vào đúng Tài khoản Google trong danh sách thả xuống Tài khoản ở trên cùng bên phải.
- Sau khi chọn dự án, hãy nhấp vào + Tạo tài khoản dịch vụ trong thanh trình đơn trên cùng.
- Đặt tên cho tài khoản dịch vụ, cung cấp nội dung mô tả (không bắt buộc) để bạn nhớ được mục đích của tài khoản đó rồi chuyển sang bước tiếp theo.
- Chỉ định vai trò Người chỉnh sửa cho tài khoản dịch vụ.
- Hoàn tất trình hướng dẫn, quay lại trang Quyền truy cập API trong bảng điều khiển dành cho nhà phát triển rồi nhấp vào Làm mới tài khoản dịch vụ. Bạn sẽ thấy tài khoản mới tạo của mình trong danh sách.
- Nhấp vào Cấp quyền truy cập cho tài khoản dịch vụ mới.
- Cuộn xuống trang tiếp theo, đến khối Dữ liệu tài chính. Chọn cả Xem dữ liệu tài chính, đơn đặt hàng và phản hồi trong bản khảo sát về quyết định huỷ gói thuê bao và Quản lý đơn đặt hàng và gói thuê bao.
- Nhấp vào Mời người dùng.
- Giờ đây, khi tài khoản đã được thiết lập, bạn chỉ cần tạo một số thông tin xác thực. Quay lại bảng điều khiển Google Cloud, hãy tìm tài khoản dịch vụ của bạn trong danh sách tài khoản dịch vụ, nhấp vào biểu tượng ba dấu chấm dọc rồi chọn Quản lý khoá.
- Tạo khoá JSON mới và tải khoá đó xuống.
- Đổ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ụcassets/
.
Một việc nữa chúng ta cần làm là mở lib/constants.dart,
và thay thế giá trị của androidPackageId
bằng mã gói mà bạn đã chọn cho ứng dụng Android.
Thiết lập quyền truy cập vào Apple App Store
Để truy cập vào App Store nhằm xác minh giao dịch mua, bạn phải thiết lập một khoá bí mật dùng chung:
- Mở App Store Connect.
- Chuyển đến phần Ứng dụng của tôi rồi chọn ứng dụng của bạn.
- Trong bảng điều hướng thanh bên, hãy chuyển đến Giao dịch mua hàng trong ứng dụng > Quản lý.
- Ở trên cùng bên phải danh sách, hãy nhấp vào Khoá bí mật dùng chung dành riêng cho ứng dụng.
- Tạo một khoá bí mật mới rồi sao chép khoá đó.
- Mở
lib/constants.dart,
và thay thế giá trị củaappStoreSharedSecret
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 các hằng số sau được định cấu hình trong tệp lib/constants.dart
:
androidPackageId
: Mã gói được dùng trên Android. Ví dụ:com.example.dashclicker
appStoreSharedSecret
: Khoá bí mật dùng chung để truy cập vào App Store Connect nhằm xác minh giao dịch mua.bundleId
: Mã nhận dạng gói được dùng trên iOS. Ví dụ:com.example.dashclicker
Hiện tại, bạn có thể bỏ qua các hằng số còn lại.
10. Xác minh giao dịch mua
Quy trình chung để xác minh giao dịch mua tương tự nhau trên iOS và Android.
Đối với cả hai cửa hàng, ứng dụng của bạn sẽ nhận được mã thông báo khi có giao dịch mua.
Ứng dụng sẽ gửi mã thông báo này đến dịch vụ phụ trợ của bạn. Sau đó, dịch vụ này sẽ xác minh giao dịch mua bằng máy chủ của cửa hàng tương ứng bằng mã thông báo đã cung cấp.
Sau đó, dịch vụ phụ trợ có thể chọn lưu trữ giao dịch mua và trả lời ứng dụng về việc giao dịch mua có hợp lệ hay không.
Bằng cách yêu cầu dịch vụ phụ trợ xác thực với các cửa hàng thay vì ứng dụng chạy trên thiết bị của người dùng, bạn có thể ngăn người dùng truy cập vào các tính năng cao cấp bằng cách, ví dụ: tua lại đồng hồ hệ thống của họ.
Thiết lập phía Flutter
Thiết lập tính năng xác thực
Khi gửi giao dịch mua đến dịch vụ phụ trợ, bạn cần đảm bảo người dùng được xác thực trong khi mua hàng. Hầu hết logic xác thực đã được thêm vào cho bạn trong dự án khởi động, bạn chỉ cần đảm bảo PurchasePage
hiển thị nút đăng nhập khi người dùng chưa đăng nhập. Thêm mã sau vào đầu phương thức tạo của PurchasePage
:
lib/pages/purchase_page.dart
import '../logic/firebase_notifier.dart';
import '../model/firebase_state.dart';
import 'login_page.dart';
class PurchasePage extends StatelessWidget {
const PurchasePage({super.key});
@override
Widget build(BuildContext context) {
var firebaseNotifier = context.watch<FirebaseNotifier>();
if (firebaseNotifier.state == FirebaseState.loading) {
return _PurchasesLoading();
} else if (firebaseNotifier.state == FirebaseState.notAvailable) {
return _PurchasesNotAvailable();
}
if (!firebaseNotifier.loggedIn) {
return const LoginPage();
}
// omitted
Điểm cuối xác minh cuộc gọi từ ứng dụng
Trong ứng dụng, hãy tạo hàm _verifyPurchase(PurchaseDetails purchaseDetails)
gọi điểm cuối /verifypurchase
trên phần phụ trợ Dart bằng lệnh gọi bài đăng http.
Gửi cửa hàng đã chọn (google_play
cho Cửa hàng Play hoặc app_store
cho App Store), serverVerificationData
và productID
. Máy chủ trả về mã trạng thái cho biết giao dịch mua có được xác minh hay không.
Trong hằng số ứng dụng, hãy định cấu hình IP máy chủ thành địa chỉ IP của máy cục bộ.
lib/logic/dash_purchases.dart
FirebaseNotifier firebaseNotifier;
DashPurchases(this.counter, this.firebaseNotifier) {
// omitted
}
Thêm firebaseNotifier
bằng cách tạo DashPurchases
trong main.dart:
lib/main.dart
ChangeNotifierProvider<DashPurchases>(
create: (context) => DashPurchases(
context.read<DashCounter>(),
context.read<FirebaseNotifier>(),
),
lazy: false,
),
Thêm một phương thức getter cho Người dùng trong FirebaseNotifier để bạn có thể truyền mã nhận dạng người dùng đến hàm xác minh giao dịch mua.
lib/logic/firebase_notifier.dart
User? get user => FirebaseAuth.instance.currentUser;
Thêm hàm _verifyPurchase
vào lớp DashPurchases
. Hàm async
này trả về một giá trị boolean cho biết giao dịch mua có được xác thực hay không.
lib/logic/dash_purchases.dart
Future<bool> _verifyPurchase(PurchaseDetails purchaseDetails) async {
final url = Uri.parse('http://$serverIp:8080/verifypurchase');
const headers = {
'Content-type': 'application/json',
'Accept': 'application/json',
};
final response = await http.post(
url,
body: jsonEncode({
'source': purchaseDetails.verificationData.source,
'productId': purchaseDetails.productID,
'verificationData':
purchaseDetails.verificationData.serverVerificationData,
'userId': firebaseNotifier.user?.uid,
}),
headers: headers,
);
if (response.statusCode == 200) {
return true;
} else {
return false;
}
}
Gọi hàm _verifyPurchase
trong _handlePurchase
ngay trước khi bạn áp dụng giao dịch mua. Bạn chỉ nên áp dụng giao dịch mua khi giao dịch đó được xác minh. Trong ứng dụng chính thức, bạn có thể chỉ định thêm thông tin này, ví dụ: áp dụng gói thuê bao dùng thử khi cửa hàng tạm thời không hoạt động. Tuy nhiên, đối với ví dụ này, hãy đơn giản hoá và chỉ áp dụng giao dịch mua khi giao dịch mua được xác minh thành công.
lib/logic/dash_purchases.dart
Future<void> _onPurchaseUpdate(
List<PurchaseDetails> purchaseDetailsList) async {
for (var purchaseDetails in purchaseDetailsList) {
await _handlePurchase(purchaseDetails);
}
notifyListeners();
}
Future<void> _handlePurchase(PurchaseDetails purchaseDetails) async {
if (purchaseDetails.status == PurchaseStatus.purchased) {
// Send to server
var validPurchase = await _verifyPurchase(purchaseDetails);
if (validPurchase) {
// Apply changes locally
switch (purchaseDetails.productID) {
case storeKeySubscription:
counter.applyPaidMultiplier();
case storeKeyConsumable:
counter.addBoughtDashes(1000);
}
}
}
if (purchaseDetails.pendingCompletePurchase) {
await iapConnection.completePurchase(purchaseDetails);
}
}
Trong ứng dụng, mọi thứ đã sẵn sàng để xác thực giao dịch mua.
Thiết lập dịch vụ phụ trợ
Tiếp theo, hãy thiết lập hàm trên đám mây để xác minh giao dịch mua ở phần phụ trợ.
Tạo trình xử lý giao dịch mua
Vì quy trình xác minh cho cả hai cửa hàng gần như giống hệt nhau, hãy thiết lập một lớp PurchaseHandler
trừu tượng với cách triển khai riêng cho từng cửa hàng.
Bắt đầu bằng cách thêm tệp purchase_handler.dart
vào thư mục lib/
, trong đó bạn xác định một lớp PurchaseHandler
trừu tượng với hai phương thức trừu tượng để xác minh hai loại giao dịch mua: gói thuê bao và không phải gói thuê bao.
lib/purchase_handler.dart
import 'products.dart';
/// Generic purchase handler,
/// must be implemented for Google Play and Apple Store
abstract class PurchaseHandler {
/// Verify if non-subscription purchase (aka consumable) is valid
/// and update the database
Future<bool> handleNonSubscription({
required String userId,
required ProductData productData,
required String token,
});
/// Verify if subscription purchase (aka non-consumable) is valid
/// and update the database
Future<bool> handleSubscription({
required String userId,
required ProductData productData,
required String token,
});
}
Như bạn có thể thấy, mỗi phương thức yêu cầu 3 tham số:
userId:
Mã nhận dạng của người dùng đã đăng nhập để bạn có thể liên kết giao dịch mua với người dùng đó.productData:
Dữ liệu về sản phẩm. Bạn sẽ xác định điều này trong một phút.token:
Mã thông báo do cửa hàng cung cấp cho người dùng.
Ngoài ra, để giúp các trình xử lý giao dịch mua này dễ sử dụng hơn, hãy thêm một phương thức verifyPurchase()
có thể dùng cho cả gói thuê bao và gói không phải thuê bao:
lib/purchase_handler.dart
/// Verify if purchase is valid and update the database
Future<bool> verifyPurchase({
required String userId,
required ProductData productData,
required String token,
}) async {
switch (productData.type) {
case ProductType.subscription:
return handleSubscription(
userId: userId,
productData: productData,
token: token,
);
case ProductType.nonSubscription:
return handleNonSubscription(
userId: userId,
productData: productData,
token: token,
);
}
}
Giờ đây, bạn chỉ cần gọi verifyPurchase
cho cả hai trường hợp, nhưng vẫn có thể triển khai riêng biệt!
Lớp ProductData
chứa thông tin cơ bản về các sản phẩm có thể mua, bao gồm mã sản phẩm (đôi khi còn gọi là SKU) và ProductType
.
lib/products.dart
class ProductData {
final String productId;
final ProductType type;
const ProductData(this.productId, this.type);
}
ProductType
có thể là gói thuê bao hoặc không phải gói thuê bao.
lib/products.dart
enum ProductType {
subscription,
nonSubscription,
}
Cuối cùng, danh sách sản phẩm được xác định là một bản đồ trong cùng một tệp.
lib/products.dart
const productDataMap = {
'dash_consumable_2k': ProductData(
'dash_consumable_2k',
ProductType.nonSubscription,
),
'dash_upgrade_3d': ProductData(
'dash_upgrade_3d',
ProductType.nonSubscription,
),
'dash_subscription_doubler': ProductData(
'dash_subscription_doubler',
ProductType.subscription,
),
};
Tiếp theo, hãy xác định một số cách triển khai phần giữ chỗ cho Cửa hàng Google Play và Apple App Store. Bắt đầu với Google Play:
Tạo lib/google_play_purchase_handler.dart
và thêm một lớp mở rộng PurchaseHandler
bạn vừa viết:
lib/google_play_purchase_handler.dart
import 'dart:async';
import 'package:googleapis/androidpublisher/v3.dart' as ap;
import 'constants.dart';
import 'iap_repository.dart';
import 'products.dart';
import 'purchase_handler.dart';
class GooglePlayPurchaseHandler extends PurchaseHandler {
final ap.AndroidPublisherApi androidPublisher;
final IapRepository iapRepository;
GooglePlayPurchaseHandler(
this.androidPublisher,
this.iapRepository,
);
@override
Future<bool> handleNonSubscription({
required String? userId,
required ProductData productData,
required String token,
}) async {
return true;
}
@override
Future<bool> handleSubscription({
required String? userId,
required ProductData productData,
required String token,
}) async {
return true;
}
}
Hiện tại, hàm này trả về true
cho các phương thức xử lý; bạn sẽ tìm hiểu về các phương thức này sau.
Như bạn có thể nhận thấy, hàm khởi tạo sẽ lấy một thực thể của IapRepository
. Trình xử lý giao dịch mua sẽ sử dụng thực thể này để lưu trữ thông tin về giao dịch mua trong Firestore sau này. Để giao tiếp với Google Play, bạn sử dụng AndroidPublisherApi
được cung cấp.
Tiếp theo, hãy làm tương tự cho trình xử lý cửa hàng ứng dụng. Tạo lib/app_store_purchase_handler.dart
và thêm một lớp mở rộng PurchaseHandler
:
lib/app_store_purchase_handler.dart
import 'dart:async';
import 'package:app_store_server_sdk/app_store_server_sdk.dart';
import 'constants.dart';
import 'iap_repository.dart';
import 'products.dart';
import 'purchase_handler.dart';
class AppStorePurchaseHandler extends PurchaseHandler {
final IapRepository iapRepository;
AppStorePurchaseHandler(
this.iapRepository,
);
@override
Future<bool> handleNonSubscription({
required String userId,
required ProductData productData,
required String token,
}) {
return true;
}
@override
Future<bool> handleSubscription({
required String userId,
required ProductData productData,
required String token,
}) {
return true;
}
}
Tuyệt vời! Bây giờ, bạn có hai trình xử lý giao dịch mua. Tiếp theo, hãy tạo điểm cuối API xác minh giao dịch mua.
Sử dụng trình xử lý giao dịch mua
Mở bin/server.dart
và tạo điểm cuối API bằng shelf_route
:
bin/server.dart
Future<void> main() async {
final router = Router();
final purchaseHandlers = await _createPurchaseHandlers();
router.post('/verifypurchase', (Request request) async {
final dynamic payload = json.decode(await request.readAsString());
final (:userId, :source, :productData, :token) = getPurchaseData(payload);
final result = await purchaseHandlers[source]!.verifyPurchase(
userId: userId,
productData: productData,
token: token,
);
if (result) {
return Response.ok('all good!');
} else {
return Response.internalServerError();
}
});
await serveHandler(router.call);
}
({
String userId,
String source,
ProductData productData,
String token,
}) getPurchaseData(dynamic payload) {
if (payload
case {
'userId': String userId,
'source': String source,
'productId': String productId,
'verificationData': String token,
}) {
return (
userId: userId,
source: source,
productData: productDataMap[productId]!,
token: token,
);
} else {
throw const FormatException('Unexpected JSON');
}
}
Mã trên thực hiện những việc sau:
- Xác định điểm cuối POST sẽ được gọi từ ứng dụng bạn đã tạo trước đó.
- Giải mã tải trọng JSON và trích xuất các thông tin sau:
userId
: Mã nhận dạng người dùng hiện đã đăng nhậpsource
: Kho lưu trữ được sử dụng,app_store
hoặcgoogle_play
.productData
: Nhận được từproductDataMap
mà bạn đã tạo trước đó.token
: Chứa dữ liệu xác minh để gửi đến các cửa hàng.- Gọi đến phương thức
verifyPurchase
, choGooglePlayPurchaseHandler
hoặcAppStorePurchaseHandler
, tuỳ thuộc vào nguồn. - Nếu xác minh thành công, phương thức này sẽ trả về
Response.ok
cho ứng dụng. - Nếu không xác minh được, phương thức này sẽ trả về
Response.internalServerError
cho ứng dụng.
Sau khi tạo điểm cuối API, bạn cần định cấu hình hai trình xử lý giao dịch mua. Để làm việc này, bạn cần tải các khoá tài khoản dịch vụ mà bạn đã nhận được ở bước trước và định cấu hình quyền truy cập vào các dịch vụ khác nhau, bao gồm cả API Nhà xuất bản Android và API Firebase Firestore. Sau đó, hãy tạo hai trình xử lý giao dịch mua bằng các phần phụ thuộc khác nhau:
bin/server.dart
Future<Map<String, PurchaseHandler>> _createPurchaseHandlers() async {
// Configure Android Publisher API access
final serviceAccountGooglePlay =
File('assets/service-account-google-play.json').readAsStringSync();
final clientCredentialsGooglePlay =
auth.ServiceAccountCredentials.fromJson(serviceAccountGooglePlay);
final clientGooglePlay =
await auth.clientViaServiceAccount(clientCredentialsGooglePlay, [
ap.AndroidPublisherApi.androidpublisherScope,
]);
final androidPublisher = ap.AndroidPublisherApi(clientGooglePlay);
// Configure Firestore API access
final serviceAccountFirebase =
File('assets/service-account-firebase.json').readAsStringSync();
final clientCredentialsFirebase =
auth.ServiceAccountCredentials.fromJson(serviceAccountFirebase);
final clientFirebase =
await auth.clientViaServiceAccount(clientCredentialsFirebase, [
fs.FirestoreApi.cloudPlatformScope,
]);
final firestoreApi = fs.FirestoreApi(clientFirebase);
final dynamic json = jsonDecode(serviceAccountFirebase);
final projectId = json['project_id'] as String;
final iapRepository = IapRepository(firestoreApi, projectId);
return {
'google_play': GooglePlayPurchaseHandler(
androidPublisher,
iapRepository,
),
'app_store': AppStorePurchaseHandler(
iapRepository,
),
};
}
Xác minh giao dịch mua trên Android: Triển khai trình xử lý giao dịch mua
Tiếp theo, hãy tiếp tục triển khai trình xử lý giao dịch mua trên Google Play.
Google đã cung cấp các gói Dart để tương tác với các API mà bạn cần để xác minh giao dịch mua. Bạn đã khởi tạo các biến này trong tệp server.dart
và hiện sử dụng các biến này trong lớp GooglePlayPurchaseHandler
.
Triển khai trình xử lý cho các giao dịch mua không phải loại gói thuê bao:
lib/google_play_purchase_handler.dart
@override
Future<bool> handleNonSubscription({
required String? userId,
required ProductData productData,
required String token,
}) async {
print(
'GooglePlayPurchaseHandler.handleNonSubscription'
'($userId, ${productData.productId}, ${token.substring(0, 5)}...)',
);
try {
// Verify purchase with Google
final response = await androidPublisher.purchases.products.get(
androidPackageId,
productData.productId,
token,
);
print('Purchases response: ${response.toJson()}');
// Make sure an order id exists
if (response.orderId == null) {
print('Could not handle purchase without order id');
return false;
}
final orderId = response.orderId!;
final purchaseData = NonSubscriptionPurchase(
purchaseDate: DateTime.fromMillisecondsSinceEpoch(
int.parse(response.purchaseTimeMillis ?? '0'),
),
orderId: orderId,
productId: productData.productId,
status: _nonSubscriptionStatusFrom(response.purchaseState),
userId: userId,
iapSource: IAPSource.googleplay,
);
// Update the database
if (userId != null) {
// If we know the userId,
// update the existing purchase or create it if it does not exist.
await iapRepository.createOrUpdatePurchase(purchaseData);
} else {
// If we do not know the user id, a previous entry must already
// exist, and thus we'll only update it.
await iapRepository.updatePurchase(purchaseData);
}
return true;
} on ap.DetailedApiRequestError catch (e) {
print(
'Error on handle NonSubscription: $e\n'
'JSON: ${e.jsonResponse}',
);
} catch (e) {
print('Error on handle NonSubscription: $e\n');
}
return false;
}
Bạn có thể cập nhật trình xử lý giao dịch mua gói thuê bao theo cách tương tự:
lib/google_play_purchase_handler.dart
/// Handle subscription purchases.
///
/// Retrieves the purchase status from Google Play and updates
/// the Firestore Database accordingly.
@override
Future<bool> handleSubscription({
required String? userId,
required ProductData productData,
required String token,
}) async {
print(
'GooglePlayPurchaseHandler.handleSubscription'
'($userId, ${productData.productId}, ${token.substring(0, 5)}...)',
);
try {
// Verify purchase with Google
final response = await androidPublisher.purchases.subscriptions.get(
androidPackageId,
productData.productId,
token,
);
print('Subscription response: ${response.toJson()}');
// Make sure an order id exists
if (response.orderId == null) {
print('Could not handle purchase without order id');
return false;
}
final orderId = extractOrderId(response.orderId!);
final purchaseData = SubscriptionPurchase(
purchaseDate: DateTime.fromMillisecondsSinceEpoch(
int.parse(response.startTimeMillis ?? '0'),
),
orderId: orderId,
productId: productData.productId,
status: _subscriptionStatusFrom(response.paymentState),
userId: userId,
iapSource: IAPSource.googleplay,
expiryDate: DateTime.fromMillisecondsSinceEpoch(
int.parse(response.expiryTimeMillis ?? '0'),
),
);
// Update the database
if (userId != null) {
// If we know the userId,
// update the existing purchase or create it if it does not exist.
await iapRepository.createOrUpdatePurchase(purchaseData);
} else {
// If we do not know the user id, a previous entry must already
// exist, and thus we'll only update it.
await iapRepository.updatePurchase(purchaseData);
}
return true;
} on ap.DetailedApiRequestError catch (e) {
print(
'Error on handle Subscription: $e\n'
'JSON: ${e.jsonResponse}',
);
} catch (e) {
print('Error on handle Subscription: $e\n');
}
return false;
}
}
Thêm phương thức sau để hỗ trợ phân tích cú pháp mã đơn đặt hàng, cũng như hai phương thức để phân tích cú pháp trạng thái mua hàng.
lib/google_play_purchase_handler.dart
NonSubscriptionStatus _nonSubscriptionStatusFrom(int? state) {
return switch (state) {
0 => NonSubscriptionStatus.completed,
2 => NonSubscriptionStatus.pending,
_ => NonSubscriptionStatus.cancelled,
};
}
SubscriptionStatus _subscriptionStatusFrom(int? state) {
return switch (state) {
// Payment pending
0 => SubscriptionStatus.pending,
// Payment received
1 => SubscriptionStatus.active,
// Free trial
2 => SubscriptionStatus.active,
// Pending deferred upgrade/downgrade
3 => SubscriptionStatus.pending,
// Expired or cancelled
_ => SubscriptionStatus.expired,
};
}
/// If a subscription suffix is present (..#) extract the orderId.
String extractOrderId(String orderId) {
final orderIdSplit = orderId.split('..');
if (orderIdSplit.isNotEmpty) {
orderId = orderIdSplit[0];
}
return orderId;
}
Giờ đây, các giao dịch mua của bạn trên Google Play sẽ được xác minh và lưu trữ trong cơ sở dữ liệu.
Tiếp theo, hãy chuyển sang giao dịch mua qua App Store cho iOS.
Xác minh giao dịch mua trên iOS: Triển khai trình xử lý giao dịch mua
Để xác minh giao dịch mua bằng App Store, có một gói Dart bên thứ ba tên là app_store_server_sdk
giúp quá trình này trở nên dễ dàng hơn.
Bắt đầu bằng cách tạo thực thể ITunesApi
. Sử dụng cấu hình hộp cát, cũng như bật tính năng ghi nhật ký để hỗ trợ gỡ lỗi.
lib/app_store_purchase_handler.dart
final _iTunesAPI = ITunesApi(
ITunesHttpClient(
ITunesEnvironment.sandbox(),
loggingEnabled: true,
),
);
Giờ đây, không giống như các API của Google Play, App Store sử dụng cùng một điểm cuối API cho cả gói thuê bao và gói không phải thuê bao. Điều này có nghĩa là bạn có thể sử dụng cùng một logic cho cả hai trình xử lý. Hợp nhất các tệp này với nhau để chúng gọi cùng một phương thức triển khai:
lib/app_store_purchase_handler.dart
@override
Future<bool> handleNonSubscription({
required String userId,
required ProductData productData,
required String token,
}) {
return handleValidation(userId: userId, token: token);
}
@override
Future<bool> handleSubscription({
required String userId,
required ProductData productData,
required String token,
}) {
return handleValidation(userId: userId, token: token);
}
/// Handle purchase validation.
Future<bool> handleValidation({
required String userId,
required String token,
}) async {
//..
}
Bây giờ, hãy triển khai handleValidation
:
lib/app_store_purchase_handler.dart
/// Handle purchase validation.
Future<bool> handleValidation({
required String userId,
required String token,
}) async {
print('AppStorePurchaseHandler.handleValidation');
final response = await _iTunesAPI.verifyReceipt(
password: appStoreSharedSecret,
receiptData: token,
);
print('response: $response');
if (response.status == 0) {
final receipts = response.latestReceiptInfo ?? [];
for (final receipt in receipts) {
final product = productDataMap[receipt.productId];
if (product == null) {
print('Error: Unknown product: ${receipt.productId}');
continue;
}
switch (product.type) {
case ProductType.nonSubscription:
await iapRepository.createOrUpdatePurchase(NonSubscriptionPurchase(
userId: userId,
productId: receipt.productId ?? '',
iapSource: IAPSource.appstore,
orderId: receipt.originalTransactionId ?? '',
purchaseDate: DateTime.fromMillisecondsSinceEpoch(
int.parse(receipt.originalPurchaseDateMs ?? '0')),
type: product.type,
status: NonSubscriptionStatus.completed,
));
break;
case ProductType.subscription:
await iapRepository.createOrUpdatePurchase(SubscriptionPurchase(
userId: userId,
productId: receipt.productId ?? '',
iapSource: IAPSource.appstore,
orderId: receipt.originalTransactionId ?? '',
purchaseDate: DateTime.fromMillisecondsSinceEpoch(
int.parse(receipt.originalPurchaseDateMs ?? '0')),
type: product.type,
expiryDate: DateTime.fromMillisecondsSinceEpoch(
int.parse(receipt.expiresDateMs ?? '0')),
status: SubscriptionStatus.active,
));
break;
}
}
return true;
} else {
print('Error: Status: ${response.status}');
return false;
}
}
Giờ đây, các giao dịch mua của bạn trên App Store sẽ được xác minh và lưu trữ trong cơ sở dữ liệu!
Chạy phần phụ trợ
Tại thời điểm này, bạn có thể chạy dart bin/server.dart
để phân phát điểm cuối /verifypurchase
.
$ dart bin/server.dart
Serving at http://0.0.0.0:8080
11. Theo dõi các giao dịch mua
Bạn nên theo dõi giao dịch mua của người dùng trong dịch vụ phụ trợ. Lý do là phần phụ trợ của bạn có thể phản hồi các sự kiện từ cửa hàng, do đó ít gặp phải thông tin lỗi thời do lưu vào bộ nhớ đệm, cũng như ít bị can thiệp hơn.
Trước tiên, hãy thiết lập quy trình xử lý các sự kiện cửa hàng trên phần phụ trợ bằng phần phụ trợ Dart mà bạn đang xây dựng.
Xử lý các sự kiện tại cửa hàng thực tế ở phần phụ trợ
Cửa hàng có thể thông báo cho phần phụ trợ của bạn về mọi sự kiện thanh toán xảy ra, chẳng hạn như khi gói thuê bao gia hạn. Bạn có thể xử lý các sự kiện này trong phần phụ trợ để cập nhật các giao dịch mua trong cơ sở dữ liệu. Trong phần này, hãy thiết lập thông tin này cho cả Cửa hàng Google Play và Apple App Store.
Xử lý các sự kiện thanh toán trên Google Play
Google Play cung cấp các sự kiện thanh toán thông qua chủ đề pub/sub trên đám mây. Về cơ bản, đây là các hàng đợi thông báo mà bạn có thể phát hành thông báo cũng như sử dụng thông báo.
Vì đây là chức năng dành riêng cho Google Play, nên bạn hãy đưa chức năng này vào GooglePlayPurchaseHandler
.
Bắt đầu bằng cách mở lib/google_play_purchase_handler.dart
và thêm lệnh nhập PubsubApi:
lib/google_play_purchase_handler.dart
import 'package:googleapis/pubsub/v1.dart' as pubsub;
Sau đó, truyền PubsubApi
vào GooglePlayPurchaseHandler
và sửa đổi hàm khởi tạo lớp để tạo Timer
như sau:
lib/google_play_purchase_handler.dart
class GooglePlayPurchaseHandler extends PurchaseHandler {
final ap.AndroidPublisherApi androidPublisher;
final IapRepository iapRepository;
final pubsub.PubsubApi pubsubApi; // new
GooglePlayPurchaseHandler(
this.androidPublisher,
this.iapRepository,
this.pubsubApi, // new
) {
// Poll messages from Pub/Sub every 10 seconds
Timer.periodic(Duration(seconds: 10), (_) {
_pullMessageFromPubSub();
});
}
Timer
được định cấu hình để gọi phương thức _pullMessageFromSubSub
mỗi 10 giây. Bạn có thể điều chỉnh Thời lượng theo ý mình.
Sau đó, hãy tạo _pullMessageFromSubSub
lib/google_play_purchase_handler.dart
/// Process messages from Google Play
/// Called every 10 seconds
Future<void> _pullMessageFromPubSub() async {
print('Polling Google Play messages');
final request = pubsub.PullRequest(
maxMessages: 1000,
);
final topicName =
'projects/$googlePlayProjectName/subscriptions/$googlePlayPubsubBillingTopic-sub';
final pullResponse = await pubsubApi.projects.subscriptions.pull(
request,
topicName,
);
final messages = pullResponse.receivedMessages ?? [];
for (final message in messages) {
final data64 = message.message?.data;
if (data64 != null) {
await _processMessage(data64, message.ackId);
}
}
}
Future<void> _processMessage(String data64, String? ackId) async {
final dataRaw = utf8.decode(base64Decode(data64));
print('Received data: $dataRaw');
final dynamic data = jsonDecode(dataRaw);
if (data['testNotification'] != null) {
print('Skip test messages');
if (ackId != null) {
await _ackMessage(ackId);
}
return;
}
final dynamic subscriptionNotification = data['subscriptionNotification'];
final dynamic oneTimeProductNotification =
data['oneTimeProductNotification'];
if (subscriptionNotification != null) {
print('Processing Subscription');
final subscriptionId =
subscriptionNotification['subscriptionId'] as String;
final purchaseToken = subscriptionNotification['purchaseToken'] as String;
final productData = productDataMap[subscriptionId]!;
final result = await handleSubscription(
userId: null,
productData: productData,
token: purchaseToken,
);
if (result && ackId != null) {
await _ackMessage(ackId);
}
} else if (oneTimeProductNotification != null) {
print('Processing NonSubscription');
final sku = oneTimeProductNotification['sku'] as String;
final purchaseToken =
oneTimeProductNotification['purchaseToken'] as String;
final productData = productDataMap[sku]!;
final result = await handleNonSubscription(
userId: null,
productData: productData,
token: purchaseToken,
);
if (result && ackId != null) {
await _ackMessage(ackId);
}
} else {
print('invalid data');
}
}
/// ACK Messages from Pub/Sub
Future<void> _ackMessage(String id) async {
print('ACK Message');
final request = pubsub.AcknowledgeRequest(
ackIds: [id],
);
final subscriptionName =
'projects/$googlePlayProjectName/subscriptions/$googlePlayPubsubBillingTopic-sub';
await pubsubApi.projects.subscriptions.acknowledge(
request,
subscriptionName,
);
}
Mã bạn vừa thêm sẽ giao tiếp với Chủ đề Pub/Sub từ Google Cloud mỗi 10 giây và yêu cầu tin nhắn mới. Sau đó, xử lý từng thông báo trong phương thức _processMessage
.
Phương thức này giải mã các thông báo đến và lấy thông tin cập nhật về từng giao dịch mua, cả gói thuê bao và không phải gói thuê bao, gọi handleSubscription
hoặc handleNonSubscription
hiện có nếu cần.
Mỗi thông báo cần được xác nhận bằng phương thức _askMessage
.
Tiếp theo, hãy thêm các phần phụ thuộc bắt buộc vào tệp server.dart
. Thêm PubsubApi.cloudPlatformScope vào cấu hình thông tin xác thực:
bin/server.dart
final clientGooglePlay =
await auth.clientViaServiceAccount(clientCredentialsGooglePlay, [
ap.AndroidPublisherApi.androidpublisherScope,
pubsub.PubsubApi.cloudPlatformScope, // new
]);
Sau đó, hãy tạo thực thể PubsubApi:
bin/server.dart
final pubsubApi = pubsub.PubsubApi(clientGooglePlay);
Cuối cùng, hãy truyền đối tượng này vào hàm khởi tạo GooglePlayPurchaseHandler
:
bin/server.dart
return {
'google_play': GooglePlayPurchaseHandler(
androidPublisher,
iapRepository,
pubsubApi, // new
),
'app_store': AppStorePurchaseHandler(
iapRepository,
),
};
Thiết lập Google Play
Bạn đã viết mã để sử dụng các sự kiện thanh toán từ chủ đề pub/sub, nhưng bạn chưa tạo chủ đề pub/sub và cũng chưa phát hành bất kỳ sự kiện thanh toán nào. Đã đến lúc thiết lập.
Trước tiên, hãy tạo một chủ đề pub/sub:
- Truy cập vào trang Cloud Pub/Sub trên Google Cloud Console.
- Đảm bảo bạn đang ở dự án Firebase rồi nhấp vào + Create Topic (Tạo chủ đề).
- Đặt tên cho chủ đề mới, giống với giá trị được đặt cho
GOOGLE_PLAY_PUBSUB_BILLING_TOPIC
trongconstants.ts
. Trong trường hợp này, hãy đặt tên làplay_billing
. Nếu bạn chọn một giá trị khác, hãy nhớ cập nhậtconstants.ts
. Tạo chủ đề. - Trong danh sách chủ đề phát hành/thuê bao, hãy nhấp vào biểu tượng ba dấu chấm dọc cho chủ đề bạn vừa tạo rồi nhấp vào Xem quyền.
- Trong thanh bên ở bên phải, hãy chọn Thêm người đại diện.
- 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. - Lưu các thay đổi về quyền.
- Sao chép Tên chủ đề của chủ đề bạn vừa tạo.
- 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.
- Di chuyển xuống rồi chuyển đến phần Kiếm tiền > Thiết lập tính năng kiếm tiền.
- Điền đầy đủ chủ đề rồi lưu nội dung thay đổi.
Tất cả sự kiện thanh toán trên Google Play hiện sẽ được xuất bản trên chủ đề này.
Xử lý các sự kiện thanh toán trên App Store
Tiếp theo, hãy làm tương tự cho các sự kiện thanh toán trên App Store. Có hai cách hiệu quả để triển khai việc xử lý nội dung cập nhật trong giao dịch mua hàng trên App Store. Một cách là triển khai webhook mà bạn cung cấp cho Apple và họ sử dụng để giao tiếp với máy chủ của bạn. Cách thứ hai (cũng là cách bạn sẽ tìm thấy trong lớp học lập trình này) là kết nối với API Máy chủ Cửa hàng ứng dụng và lấy thông tin gói thuê bao theo cách thủ công.
Lý do lớp học lập trình này tập trung vào giải pháp thứ hai là vì bạn sẽ phải hiển thị máy chủ của mình với Internet để triển khai webhook.
Trong môi trường phát hành công khai, tốt nhất là bạn nên có cả hai. Webhook để lấy sự kiện từ App Store và API máy chủ trong trường hợp bạn bỏ lỡ một sự kiện hoặc cần kiểm tra kỹ trạng thái gói thuê bao.
Bắt đầu bằng cách mở lib/app_store_purchase_handler.dart
và thêm phần phụ thuộc AppStoreServerAPI:
lib/app_store_purchase_handler.dart
final AppStoreServerAPI appStoreServerAPI;
AppStorePurchaseHandler(
this.iapRepository,
this.appStoreServerAPI, // new
)
Sửa đổi hàm khởi tạo để thêm một bộ hẹn giờ sẽ gọi đến phương thức _pullStatus
. Bộ hẹn giờ này sẽ gọi phương thức _pullStatus
mỗi 10 giây. Bạn có thể điều chỉnh thời lượng của bộ hẹn giờ này theo nhu cầu của mình.
lib/app_store_purchase_handler.dart
AppStorePurchaseHandler(
this.iapRepository,
this.appStoreServerAPI,
) {
// Poll Subscription status every 10 seconds.
Timer.periodic(Duration(seconds: 10), (_) {
_pullStatus();
});
}
Sau đó, hãy tạo phương thức _pullStatus như sau:
lib/app_store_purchase_handler.dart
Future<void> _pullStatus() async {
print('Polling App Store');
final purchases = await iapRepository.getPurchases();
// filter for App Store subscriptions
final appStoreSubscriptions = purchases.where((element) =>
element.type == ProductType.subscription &&
element.iapSource == IAPSource.appstore);
for (final purchase in appStoreSubscriptions) {
final status =
await appStoreServerAPI.getAllSubscriptionStatuses(purchase.orderId);
// Obtain all subscriptions for the order id.
for (final subscription in status.data) {
// Last transaction contains the subscription status.
for (final transaction in subscription.lastTransactions) {
final expirationDate = DateTime.fromMillisecondsSinceEpoch(
transaction.transactionInfo.expiresDate ?? 0);
// Check if subscription has expired.
final isExpired = expirationDate.isBefore(DateTime.now());
print('Expiration Date: $expirationDate - isExpired: $isExpired');
// Update the subscription status with the new expiration date and status.
await iapRepository.updatePurchase(SubscriptionPurchase(
userId: null,
productId: transaction.transactionInfo.productId,
iapSource: IAPSource.appstore,
orderId: transaction.originalTransactionId,
purchaseDate: DateTime.fromMillisecondsSinceEpoch(
transaction.transactionInfo.originalPurchaseDate),
type: ProductType.subscription,
expiryDate: expirationDate,
status: isExpired
? SubscriptionStatus.expired
: SubscriptionStatus.active,
));
}
}
}
}
Phương thức này hoạt động như sau:
- Lấy danh sách gói thuê bao đang hoạt động từ Firestore bằng IapRepository.
- Đối với mỗi đơn đặt hàng, API này sẽ yêu cầu trạng thái gói thuê bao cho API Máy chủ Cửa hàng ứng dụng.
- Lấy giao dịch gần đây nhất cho giao dịch mua gói thuê bao đó.
- Kiểm tra ngày hết hạn.
- Cập nhật trạng thái gói thuê bao trên Firestore. Nếu gói thuê bao đã hết hạn, thì gói thuê bao đó sẽ được đánh dấu là hết hạn.
Cuối cùng, hãy thêm tất cả mã cần thiết để định cấu hình quyền truy cập vào API máy chủ Cửa hàng ứng dụng:
bin/server.dart
// add from here
final subscriptionKeyAppStore =
File('assets/SubscriptionKey.p8').readAsStringSync();
// Configure Apple Store API access
var appStoreEnvironment = AppStoreEnvironment.sandbox(
bundleId: bundleId,
issuerId: appStoreIssuerId,
keyId: appStoreKeyId,
privateKey: subscriptionKeyAppStore,
);
// Stored token for Apple Store API access, if available
final file = File('assets/appstore.token');
String? appStoreToken;
if (file.existsSync() && file.lengthSync() > 0) {
appStoreToken = file.readAsStringSync();
}
final appStoreServerAPI = AppStoreServerAPI(
AppStoreServerHttpClient(
appStoreEnvironment,
jwt: appStoreToken,
jwtTokenUpdatedCallback: (token) {
file.writeAsStringSync(token);
},
),
);
// to here
return {
'google_play': GooglePlayPurchaseHandler(
androidPublisher,
iapRepository,
pubsubApi,
),
'app_store': AppStorePurchaseHandler(
iapRepository,
appStoreServerAPI, // new
),
};
Thiết lập App Store
Tiếp theo, hãy thiết lập App Store:
- Đăng nhập vào App Store Connect rồi chọn Người dùng và quyền truy cập.
- Chuyển đến Key Type (Loại khoá) > In-App Purchase (Giao dịch mua hàng trong ứng dụng).
- Nhấn vào biểu tượng "dấu cộng" để thêm một thẻ mới.
- Đặt tên cho khoá, ví dụ: "khoá lớp học lập trình".
- Tải tệp p8 chứa khoá xuống.
- Sao chép tệp này vào thư mục tài sản, với tên
SubscriptionKey.p8
. - Sao chép mã khoá từ khoá mới tạo rồi đặt mã khoá đó thành hằng số
appStoreKeyId
trong tệplib/constants.dart
. - Sao chép mã nhận dạng của tổ chức phát hành ngay ở đầu danh sách khoá và đặt mã nhận dạng này thành hằng số
appStoreIssuerId
trong tệplib/constants.dart
.
Theo dõi giao dịch mua trên thiết bị
Cách an toàn nhất để theo dõi giao dịch mua là ở phía máy chủ vì máy khách khó bảo mật. Tuy nhiên, bạn cần có một số cách để đưa thông tin trở lại máy khách để ứng dụng có thể hành động dựa trên thông tin trạng thái gói thuê bao. Bằng cách lưu trữ các giao dịch mua trong Firestore, bạn có thể dễ dàng đồng bộ hoá dữ liệu với ứng dụng và tự động cập nhật dữ liệu đó.
Bạn đã đưa IAPRepo vào ứng dụng. Đây là kho lưu trữ Firestore chứa tất cả dữ liệu giao dịch mua của người dùng trong List<PastPurchase> purchases
. Kho lưu trữ cũng chứa hasActiveSubscription,
là true khi có giao dịch mua bằng productId storeKeySubscription
có trạng thái chưa hết hạn. Khi người dùng chưa đăng nhập, danh sách này sẽ trống.
lib/repo/iap_repo.dart
void updatePurchases() {
_purchaseSubscription?.cancel();
var user = _user;
if (user == null) {
purchases = [];
hasActiveSubscription = false;
hasUpgrade = false;
return;
}
var purchaseStream = _firestore
.collection('purchases')
.where('userId', isEqualTo: user.uid)
.snapshots();
_purchaseSubscription = purchaseStream.listen((snapshot) {
purchases = snapshot.docs.map((DocumentSnapshot document) {
var data = document.data();
return PastPurchase.fromJson(data);
}).toList();
hasActiveSubscription = purchases.any((element) =>
element.productId == storeKeySubscription &&
element.status != Status.expired);
hasUpgrade = purchases.any(
(element) => element.productId == storeKeyUpgrade,
);
notifyListeners();
});
}
Tất cả logic mua hàng đều nằm trong lớp DashPurchases
và là nơi bạn nên áp dụng hoặc xoá gói thuê bao. Vì vậy, hãy thêm iapRepo
làm thuộc tính trong lớp và gán iapRepo
trong hàm khởi tạo. Tiếp theo, hãy thêm trực tiếp trình nghe trong hàm khởi tạo và xoá trình nghe trong phương thức dispose()
. Ban đầu, trình nghe có thể chỉ là một hàm trống. Vì IAPRepo
là 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
IAPRepo iapRepo;
DashPurchases(this.counter, this.firebaseNotifier, this.iapRepo) {
final purchaseUpdated = iapConnection.purchaseStream;
_subscription = purchaseUpdated.listen(
_onPurchaseUpdate,
onDone: _updateStreamOnDone,
onError: _updateStreamOnError,
);
iapRepo.addListener(purchasesUpdate);
loadPurchases();
}
@override
void dispose() {
_subscription.cancel();
iapRepo.removeListener(purchasesUpdate);
super.dispose();
}
void purchasesUpdate() {
//TODO manage updates
}
Tiếp theo, hãy cung cấp IAPRepo
cho hàm khởi tạo trong main.dart.
. Bạn có thể lấy kho lưu trữ bằng cách sử dụng context.read
vì kho lưu trữ này đã được tạo trong Provider
.
lib/main.dart
ChangeNotifierProvider<DashPurchases>(
create: (context) => DashPurchases(
context.read<DashCounter>(),
context.read<FirebaseNotifier>(),
context.read<IAPRepo>(),
),
lazy: false,
),
Tiếp theo, hãy viết mã cho hàm purchaseUpdate()
. Trong dash_counter.dart,
, các phương thức applyPaidMultiplier
và removePaidMultiplier
lần lượt đặt hệ số là 10 hoặc 1, vì vậy, bạn không cần phải kiểm tra xem gói thuê bao đã được áp dụng hay chưa. Khi trạng thái gói thuê bao thay đổi, bạn cũng cập nhật trạng thái của sản phẩm có thể mua để có thể cho thấy trên trang mua hàng rằng sản phẩm đó đã hoạt động. Đặt thuộc tính _beautifiedDashUpgrade
dựa trên việc người dùng có mua gói nâng cấp hay không.
lib/logic/dash_purchases.dart
void purchasesUpdate() {
var subscriptions = <PurchasableProduct>[];
var upgrades = <PurchasableProduct>[];
// Get a list of purchasable products for the subscription and upgrade.
// This should be 1 per type.
if (products.isNotEmpty) {
subscriptions = products
.where((element) => element.productDetails.id == storeKeySubscription)
.toList();
upgrades = products
.where((element) => element.productDetails.id == storeKeyUpgrade)
.toList();
}
// Set the subscription in the counter logic and show/hide purchased on the
// purchases page.
if (iapRepo.hasActiveSubscription) {
counter.applyPaidMultiplier();
for (var element in subscriptions) {
_updateStatus(element, ProductStatus.purchased);
}
} else {
counter.removePaidMultiplier();
for (var element in subscriptions) {
_updateStatus(element, ProductStatus.purchasable);
}
}
// Set the Dash beautifier and show/hide purchased on
// the purchases page.
if (iapRepo.hasUpgrade != _beautifiedDashUpgrade) {
_beautifiedDashUpgrade = iapRepo.hasUpgrade;
for (var element in upgrades) {
_updateStatus(
element,
_beautifiedDashUpgrade
? ProductStatus.purchased
: ProductStatus.purchasable);
}
notifyListeners();
}
}
void _updateStatus(PurchasableProduct product, ProductStatus status) {
if (product.status != ProductStatus.purchased) {
product.status = ProductStatus.purchased;
notifyListeners();
}
}
Giờ đây, bạn đã đảm bảo rằng trạng thái gói thuê bao và nâng cấp luôn mới nhất trong dịch vụ phụ trợ và đồng bộ hoá với ứng dụng. Ứng dụng sẽ hành động tương ứng và áp dụng các tính năng gói thuê bao và nâng cấp cho trò chơi nhấp chuột Dash.
12. Đã xong!
Xin chúc mừng!!! Bạn đã hoàn tất lớp học lập trình. Bạn có thể tìm thấy mã hoàn chỉnh cho lớp học lập trình này trong thư mục complete.
Để tìm hiểu thêm, hãy thử tham gia các lớp học lập trình khác về Flutter.