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

1. Giới thiệu

Lần cập nhật gần đây nhất: 11/07/2023

Để thêm các giao dịch mua hàng trong ứng dụng vào ứng dụng Flutter, bạn phải thiết lập đúng cách 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 dành cho 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 Dash Clicker được cung cấp có một trò chơi sử dụng linh vật Dash làm tiền tệ. Bạn sẽ thêm các lựa chọn mua sau đây:

  1. Lựa chọn mua lặp lại cho 2.000 Trang cùng một lúc.
  2. Giao dịch mua một lần nâng cấp để biến kiểu cũ của Dash thành Dash 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 đầu tiên mang đến cho người dùng lợi ích trực tiếp là 2000 Trang. Người dùng có thể mua trực tiếp các mặt hàng này và mua được nhiều lần. Đây được gọi là sản phẩm tiêu dùng vì sản phẩm được tiêu thụ trực tiếp và có thể tiêu thụ nhiều lần.

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

Lựa chọn mua hàng thứ ba và là giao dịch mua gần đây nhất là gói thuê bao. Người dùng sẽ sử dụng được Dashes nhanh hơn khi gói thuê bao đang hoạt động. Tuy nhiên, khi họ ngừng trả phí cho gói thuê bao này, 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 giao dịch mua được thực hiện và lưu trữ chúng bằng Firestore. Firestore được dùng để giúp quá trình này dễ dàng hơn. Tuy nhiên, trong ứng dụng chính thức, bạn có thể dùng bất cứ loại dịch vụ phụ trợ nào.

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

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

  • Bạn sẽ mở rộng một ứng dụng để hỗ trợ giao dịch mua hàng tiêu dùng và gói thuê bao.
  • Bạn cũng sẽ mở rộng ứ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 thiết lập App Store và Cửa hàng Play bằng các sản phẩm có thể mua được.
  • Cách liên hệ với cửa hàng để xác minh giao dịch mua và lưu trữ những 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 4.1 trở lên
  • Xcode 12 trở lên (để 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 rồi 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 bằng 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 sử dụng lệnh sau:

gh repo clone flutter/codelabs flutter-codelabs

Mã mẫu được sao chép vào thư mục flutter-codelabs chứa mã dành cho một tập hợp lớp học lập trình. Mã dành 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 các ảnh chụp nhanh về vị trí bạn nên đến ở cuối mỗi bước được đặt tên. Mã khởi đầu đang ở bước 0, vì vậy, bạn có thể dễ dàng xác định vị trí các tệp phù hợp bằng cách:

cd flutter-codelabs/in_app_purchases/step_00

Nếu bạn muốn chuyển đến bước tiếp theo hoặc muốn xem kết quả nào đó sau một bước, hãy tìm trong thư mục có tên tương ứng với 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 qua step_00 trong IDE yêu thích của bạn. Chúng tôi 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, hãy đảm bảo rằng bạn đã cài đặt các trình bổ trợ Dart và Flutter mới nhất.

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

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

942772eb9a73bfaa.pngS

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

812f919d965c649a.jpeg

Bạn hiện có thể đóng Xcode và quay lại Android Studio để hoàn tất quá trình định 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 của bạn (trên dòng 37 trong ảnh chụp màn hình bên dưới) thành ID ứng dụng, giống như mã nhận dạng gói iOS. 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 mã nhận dạng giống nhau sẽ ít gặp lỗi hơn và 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 hệt nhau.

5c4733ac560ae8c2.pngS

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 của bạn:

$ cd app
$ flutter pub add in_app_purchase

pubspec.yaml

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

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 tính năng mua hàng trong ứng dụng và thử nghiệm các tính năng này 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 được tại đó. Bạn không cần phải xuất bản bất cứ điều gì 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 để làm 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ó thoả thuận còn hiệu lực cho ứng dụng có tính phí trong App Store Connect. Truy cập https://appstoreconnect.apple.com/ và nhấp vào Thoả thuận, thuế và ngân hàng.

6e373780e5e24a6f.png.

Bạn sẽ thấy các thoả thuận ở đây cho các ứ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 đảm bảo rằng bạn xem, chấp nhận các điều khoản và nhập tất cả thông tin bắt buộc.

74c73197472c9aec.png.

Khi mọi thứ được thiết lập chính xác, trạng thái của các ứng dụng có tính phí sẽ là 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 còn hiệu lực.

4a100bbb8cafdbbf.jpeg

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

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

55d7e592d9a3fc7b.png.

Chọn mã ứng dụng

13f125598b72ca77.pngS

Chọn ứng dụng

41ac4c13404e2526.pngS

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

9d2c940ad80deeef.png.

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

Tạo ứng dụng mới

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

10509b17fbf031bd.pngS

5b7c0bb684ef52c7.png.

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

Để kiểm thử tính năng 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 thử nghiệm này không nên kết nối với iTunes mà chỉ được sử dụng để thử nghiệm mua hàng trong ứng dụng. Bạn không thể sử dụng địa chỉ email đã được dùng cho 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 mục Hộp cát để tạo một tài khoản hộp cát mới hoặc để quản lý các ID Apple trong hộp cát hiện có.

3ca2b26d4e391a4c.jpeg

Bây giờ bạn có thể thiết lập người dùng hộp cát trên iPhone của mình bằng cách đi tới Settings > Cửa hàng ứng dụng > Tài khoản Sandbox.

c7dadad2c1d448fa.jpeg 5363f87efcddaa4.jpeg

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

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

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

d156b2f5bac43ca8.png

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

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

  1. Thiết lập dash_consumable_2k dưới dạng một Consumable (Tiêu hao).

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

ec1701834fd8527.png

  1. Thiết lập dash_upgrade_3d làm sản phẩm Sản phẩm khô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 rồi thêm nội dung bản địa hoá của giao dịch mua. Gọi giao dịch mua 3D Dash với nội dung mô tả là Brings your dash back to the future.

6765d4b711764c30.pngS

  1. 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 Tên tham chiếu và Mã sản phẩm:

6d29e08dae26a0c4.pngS

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 này cùng 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. Chỉ cần gọi nhóm này là subscriptions.

5bd0da17a85ac076.pngS

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

bd1b1d82eeee4cb3.png

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

d0bf39680ef0aa2e.png

Bây giờ, bạn sẽ thấy 3 giao dịch mua trong danh sách giao dịch mua:

99d5c4b446e8fecf.png.

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

Tương tự như với App Store, bạn cũng cần có một 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ý một tài khoản.

Tạo ứng dụng mới

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

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

Sau khi tạo ứng dụng, hãy chuyển đến trang tổng quan rồi hoàn thành tất cả nhiệm vụ trong phần Thiết lập ứng dụng. Tại đây, bạn sẽ 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ử tính năng 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 được việc này, bạn cần ký bản phát hành bằng một khoá không phải là khoá gỡ lỗi.

Tạo một kho khoá

Nếu bạn đã có kho khoá, hãy chuyển sang bước tiếp theo. Nếu chưa có, hãy tạo một tài khoản bằng cách chạy dòng sau ở 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 của bạn. 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ữ nguyên

keystore

tệp riêng tư; đừng đánh dấu vào phần kiểm soát nguồn công khai!

Tham chiếu kho khoá của ứng dụng

Tạo một tệp có tên <your app dir>/android/key.properties chứa 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 quy trình đăng nhập 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 về kho khoá qua 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ã dưới đây 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
       }
   }

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

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

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

Sau khi ứng dụng của bạn được định cấu hình để 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 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 Bản phát hành > Kiểm thử > Thử nghiệm khép kín và tạo một bản phát hành thử nghiệm khép kín mới.

Đối với lớp học lập trình này, bạn sẽ sử dụng phương thức ký ứng dụng của Google. Vì vậy, hãy tiếp tục và 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.

ba98446d9c5c40e0.png

Tiếp theo, hãy tải gói ứng dụng app-release.aab được tạo bằng lệnh tạo 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ử trong Google Play Console ở hai vị trí:

  1. Cho kênh kiểm thử cụ thể (Kiểm thử nội bộ)
  2. Với tư cách người kiểm thử được cấp phép

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

a0d0394e85128f84.png

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

Tiếp theo, chọn hộp đánh dấu cho danh sách đó rồi nhấp vào Save changes (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 chính địa chỉ email của những người kiểm thử cần có quyền kiểm thử giao dịch mua hàng trong ứng dụng.
  4. Đặt tuỳ chọn Phản hồi giấy phép thành RESPOND_NORMALLY.
  5. Nhấp vào Lưu thay đổi.

a1a0f9d3e55ea8da.png

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

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

Giống như trong App Store, bạn phải xác định ba giao dịch mua khác nhau:

  • dash_consumable_2k: Một giao dịch mua sản phẩm tiêu dùng có thể mua nhiều lần, cấp cho người dùng 2000 Dash (đơn vị tiền tệ trong ứng dụng) cho mỗi giao dịch mua.
  • dash_upgrade_3d: "Bản nâng cấp" cho sản phẩm tiêu dùng giao dịch mua chỉ có thể mua một lần, mang lại cho người dùng một dấu gạch ngang khác về mặt hình thức để nhấp vào.
  • dash_subscription_doubler: Một gói thuê bao cấp cho người dùng số lượng Dấu gạch ngang nhiều gấp đôi trên mỗi lượt nhấp trong suốt thời gian đăng ký.

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

  1. Truy cập vào Google Play Console rồi chọn ứng dụng của bạn.
  2. Chuyển đến 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 rằng mã sản phẩm hoàn toàn khớp với mã mà bạn định sử dụng.
  5. Nhấp vào Lưu.
  6. Nhấp vào Kích hoạt.
  7. Lặp lại quy trình "nâng cấp" sản phẩm tiêu dùng mua hà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 Kiếm tiền > Sản phẩm > Gói thuê bao.
  3. Nhấp vào Tạo gói thuê bao32a6a9eefdb71dd0.pngS
  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 bạn định sử dụng.
  5. Nhấp vào Lưu

Giao dịch mua của bạn bây giờ đã được thiết lập 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 dữ liệu mua hàng.

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

  • Bạn có thể xác minh giao dịch một cách an toàn.
  • Bạn có thể thể hiện cảm xúc với các sự kiện thanh toán trong 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 cao cấp bằng cách tua lại đồng hồ hệ thống của họ.

Mặc dù có nhiều cách để thiết lập một 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 Google.

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

Các trình bổ trợ Firebase cũng đi kèm với ứng dụng khởi đầu.

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, cuối cùng là triển khai phần phụ trợ.

Tạo dự án Firebase

Chuyển đến 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 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 phải xác thực. Để làm được việc này, hãy tận dụng mô-đun xác thực của Firebase với tính năng đăng nhập bằng Google.

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

7babb48832fbef29.png.

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

e20553e0de5ac331.png

Đặt các quy tắc trên 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 cài đặt Firebase trên ứng dụng Flutter bằng FlutterFire CLI. Làm theo hướng dẫn như giải thích trong trang thiết lập.

Khi chạy định cấu hình flutterfire, 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 bật iOSAndroid bằng cách chọn hai nền tảng.

? 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 mục Tổng quan về dự án,chọn Cài đặt rồi chọn thẻ Chung.

Di chuyển xuống mục Ứ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 hàm băm SHA-1 của chứng chỉ gỡ lỗi.

Lấy hàm băm cho 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 một báo cáo ký.

cd android
./gradlew :app:signingReport

Bạn sẽ thấy một danh sách gồm nhiều khoá ký. Vì bạn đang tìm hàm băm cho chứng chỉ gỡ lỗi, nên hãy tìm chứng chỉ có thuộc tính VariantConfig được đặt thành debug. Có thể kho khoá sẽ 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 và đ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.

Trong Android Studio, hãy nhấp chuột phải vào thư mục ios/ rồi 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 lựa 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 sẽ là ios/Runner/Info-Debug.plistios/Runner/Info-Release.plist.

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

  1. Lấy giá trị cho REVERSED_CLIENT_ID qua tệp GoogleService-Info.plist mà không có phần tử <string>..</string> xung quanh.
  2. Thay thế giá trị trong cả hai tệp ios/Runner/Info-Debug.plistios/Runner/Info-Release.plist trong 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ờ thì bạn đã thiết lập xong 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 theo dõi thông tin cập nhật về giao dịch mua và các lỗi sau khi ứng dụng khởi động.

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

Trong main.dart,, hãy tìm tiện ích MyHomePageScaffold với BottomNavigationBar chứa hai trang. Trang này cũng tạo ba Provider cho DashCounter, DashUpgrades,DashPurchases. DashCounter theo dõi số lượng Dấu gạch ngang hiện tại và tự động tăng chú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 theo dõi thông tin cập nhật về giao dịch mua ngay khi khởi động ứng dụng. Vì vậy, hãy tắt tính năng tải từng phầ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,
),

Bạn cũng cần có một thực thể của InAppPurchaseConnection. Tuy nhiên, để đảm bảo ứng dụng có thể kiểm thử được, 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ể ghi đè trong quá trình 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 quy trình kiểm thử nếu muốn quy trình kiểm thử tiếp tục hoạt động. Hãy xem widget_test.dart trên GitHub để biết mã đầy đủ cho TestIAPConnection.

test/widget_test.dart

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

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 một DashCounter vào Trang tổng quan đã 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à dữ liệu nhập. Mã kết quả sẽ có dạng như sau:

lib/logic/dash_purchases.dart

import 'package:in_app_purchase/in_app_purchase.dart';

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

  DashPurchases(this.counter);
}

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 là không thể có giá trị rỗng (NNBD). Điều này có nghĩa là các thuộc tính không được khai báo là có thể có giá trị rỗng phải có một giá trị khác 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 purchaseUpdatedStream và bắt đầu nghe luồng. Trong phương thức dispose(), hãy huỷ gói thuê bao xem trực tuyến.

lib/logic/dash_purchases.dart

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

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

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

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

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

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

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

Giờ đây, ứng dụng sẽ nhận được thông tin cập nhật về giao dịch mua, do đó, trong phần tiếp theo, bạn sẽ mua hàng!

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

$ 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 có thể mua được. Những sản phẩm này được tải từ cửa hàng, xuất hiện trong danh sách và được mua khi nhấn vào sản phẩm.

Điều chỉnh sản phẩm có thể mua được

PurchasableProduct cho thấy một sản phẩm mô phỏng. Hãy cập nhật lớp này để hiển thị nội dung thực tế bằng cách thay thế lớp PurchasableProduct trong purchasable_product.dart bằng đoạn 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 giao dịch mua hiện có

Để giúp người dùng có thể 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ửa hàng khả dụng, hãy tải các giao dịch mua hiện có. Dựa trên cách thiết lập Firebase trước đó, bạn sẽ thấy storeKeyConsumable, storeKeySubscription,storeKeyUpgrade. Khi không có giao dịch mua dự kiến, hãy in thông tin này 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 mà hệ thống tìm thấy. Sử dụng productDetails trong phản hồi để cập nhật giao diện người dùng rồi đặ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);
    for (var element in response.notFoundIDs) {
      debugPrint('Purchase $element not found');
    }
    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;

Giới thiệu các sản phẩm có thể mua được

Hãy cân nhắc về 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 hiển thị các giao dịch mua trước đây của người dùng được sử dụng trong bước tiếp theo.

Tiện ích _PurchaseList sẽ hiện 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ác sản phẩm hiện có 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 để giao dịch mua được nhập vào bảng điều khiển tương ứng.

ca1a9f97c21e552d.png

Quay lại dash_purchases.dart rồi triển khai hàm để mua sản phẩm. Bạn chỉ cần tách sản phẩm tiêu dùng khỏi sản phẩm tiêu dùng. Sản phẩm nâng cấp và gói thuê bao là những sản phẩm không phải hàng 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);
        break;
      case storeKeySubscription:
      case storeKeyUpgrade:
        await iapConnection.buyNonConsumable(purchaseParam: purchaseParam);
        break;
      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 trên trang mua hàng và áp dụng giao dịch mua đó cho logic bộ đếm. Bạn cần gọi cho 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();
          break;
        case storeKeyConsumable:
          counter.addBoughtDashes(2000);
          break;
        case storeKeyUpgrade:
          _beautifiedDashUpgrade = true;
          break;
      }
    }

    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àng, hãy thiết lập phần phụ trợ Dart để hỗ trợ việc này.

Trong phần này, hãy dùng 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 những phần đó sẽ được đưa vào mã khởi đầu. Bạn nên xem lại nội dung đã có trong mã khởi đầu trước khi bắt đầu để nắm được cách sắp xếp cấu trúc của mọi thứ.

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

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

Một phần đã có trong mã khởi đầu là IapRepository trong lib/iap_repository.dart. Vì việc học cách tương tác với Firestore hay cơ sở dữ liệu nói chung không phù hợp với lớp học lập trình này, nên đoạ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ụ. Hãy tạo một tài khoản mở phần cài đặt dự án Firebase và chuyển đến phần Tài khoản dịch vụ, sau đó chọn Tạo khoá riêng tư mới.

27590fc77ae94ad4.pngS

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

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

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

Còn một điều 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 của mình.

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

Để truy cập App Store để xác minh giao dịch mua, bạn phải thiết lập 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 thanh điều hướng bên, hãy chuyển đến Giao dịch mua hàng trong ứng dụng > Quản lý.
  4. Ở trên cùng bên phải danh sách, hãy nhấp vào Khoá bí mật được chia sẻ dành riêng cho ứng dụng.
  5. Tạo một khoá bí mật mới rồi sao chép mã đó.
  6. Mở lib/constants.dart, rồi thay thế giá trị của appStoreSharedSecret bằng mã thông báo bí mật dùng chung mà bạn vừa tạo.

d8b8042470aaeff.png

b72f4565750e2f40.png

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

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 dùng để 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 sử 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 xác minh giao dịch mua trên iOS và Android cũng tương tự như vậy.

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

Ứng dụng sẽ gửi mã thông báo này đến dịch vụ phụ trợ của bạn, sau đó xác minh giao dịch mua hàng 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à phản hồ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ợ thực hiện xác thực với 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 giành quyền truy cập vào các tính năng nâng cao, chẳng hạn như tua lại đồng hồ hệ thống của họ.

Thiết lập nhóm 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 rằng 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 dự án khởi đầu, bạn chỉ cần đảm bảo rằng PurchasePage cho thấy 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 bản dựng 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({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    var firebaseNotifier = context.watch<FirebaseNotifier>();
    if (firebaseNotifier.state == FirebaseState.loading) {
      return _PurchasesLoading();
    } else if (firebaseNotifier.state == FirebaseState.notAvailable) {
      return _PurchasesNotAvailable();
    }

    if (!firebaseNotifier.loggedIn) {
      return const LoginPage();
    }
    // omitted

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 của bạn bằng cách sử dụng lệnh gọi sau http.

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

Trong các 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 với việc 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ể chuyể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

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

lib/logic/dash_purchases.dart

  Future<bool> _verifyPurchase(PurchaseDetails purchaseDetails) async {
    final url = Uri.parse('http://$serverIp:8080/verifypurchase');
    const headers = {
      'Content-type': 'application/json',
      'Accept': 'application/json',
    };
    final response = await http.post(
      url,
      body: jsonEncode({
        'source': purchaseDetails.verificationData.source,
        'productId': purchaseDetails.productID,
        'verificationData':
            purchaseDetails.verificationData.serverVerificationData,
        'userId': firebaseNotifier.user?.uid,
      }),
      headers: headers,
    );
    if (response.statusCode == 200) {
      print('Successfully verified purchase');
      return true;
    } else {
      print('failed request: ${response.statusCode} - ${response.body}');
      return false;
    }
  }

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 này 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, chẳng hạn như á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 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();
            break;
          case storeKeyConsumable:
            counter.addBoughtDashes(1000);
            break;
        }
      }
    }

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

Giờ đây, mọi thứ trong ứng dụng đã 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 chức năng đám mây để xác minh giao dịch mua trên phần phụ trợ.

Xây dựng trình xử lý giao dịch mua

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

be50c207c5a2a519.png

Bắt đầu bằng cách thêm tệp purchase_handler.dart vào thư mục lib/, trong đó bạn xác định một lớp PurchaseHandler trừu tượng với hai phương thức trừu tượng để xác minh hai loại giao dịch mua khác nhau: 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 các 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 các trình xử lý giao dịch mua này dễ sử dụng hơn, hãy thêm phương thức verifyPurchase() có thể dùng cho cả gói thuê bao và gói thuê bao không phải gó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,
        );
    }
  }

Bây giờ, 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 được, bao gồm cả mã 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à một 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ố phương pháp 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, hàm này sẽ trả về true cho các phương thức của trình xử lý; bạn sẽ tìm hiểu sau.

Như bạn có thể nhận thấy, hàm khởi tạo sẽ sử dụng một thực thể của IapRepository. Trình xử lý giao dịch mua hàng 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 hãy sử dụng AndroidPublisherApi được cung cấp.

Tiếp theo, hãy làm tương tự với 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 giúp mở rộng PurchaseHandler lần nữa:

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ó 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 và tạo một đ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);
}

({
  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 sẽ thực hiện những việc sau:

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

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. Việc này yêu cầu bạn phải tải khoá tài khoản dịch vụ mà bạn đã nhận được ở bước trước, đồng thời định cấu hình quyền truy cập vào nhiều dịch vụ, bao gồm cả Android Publisher API (API Nhà xuất bản Android) và API Firebase Firestore. Sau đó, hãy tạo 2 trình xử lý mua hàng với 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 API mà bạn cần để xác minh giao dịch mua. Bạn đã khởi tạo chúng trong tệp server.dart và giờ sử dụng chúng trong lớp GooglePlayPurchaseHandler.

Triển khai trình xử lý cho các giao dịch mua không theo loại đăng ký:

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ợ việc phân tích cú pháp mã đơn 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

/// If a subscription suffix is present (..#) extract the orderId.
String extractOrderId(String orderId) {
  final orderIdSplit = orderId.split('..');
  if (orderIdSplit.isNotEmpty) {
    orderId = orderIdSplit[0];
  }
  return orderId;
}

NonSubscriptionStatus _nonSubscriptionStatusFrom(int? state) {
  return switch (state) {
    0 => NonSubscriptionStatus.completed,
    2 => NonSubscriptionStatus.pending,
    _ => NonSubscriptionStatus.cancelled,
  };
}

SubscriptionStatus _subscriptionStatusFrom(int? state) {
  return switch (state) {
    // Payment pending
    0 => SubscriptionStatus.pending,
    // Payment received
    1 => SubscriptionStatus.active,
    // Free trial
    2 => SubscriptionStatus.active,
    // Pending deferred upgrade/downgrade
    3 => SubscriptionStatus.pending,
    // Expired or cancelled
    _ => SubscriptionStatus.expired,
  };
}

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 này.

Tiếp theo, hãy chuyển sang giao dịch mua trên App Store dành 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, một gói Dart của bên thứ ba có tên là app_store_server_sdk giúp quá 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ý để tạo điều kiện 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 gó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 chúng với nhau để 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) {
      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 đã đượ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

Cách được đề xuất để theo dõi dữ liệu người dùng của bạn các giao dịch mua nằm 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ý sự kiện cửa hàng trên phần phụ trợ bằng phần phụ trợ Dart mà bạn đã xây dựng.

Xử lý sự kiện cửa hàng trên 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ề bất kỳ sự kiện thanh toán nào xảy ra, chẳng hạn như khi gói thuê bao gia hạn. Bạn có thể xử lý những 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 của mình. Trong phần này, hãy thiết lập ứng dụng cho cả Cửa hàng Google Play và App Store của Apple.

Xử lý 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ủ đề Cloud pub/sub. Về cơ bản, đây là những hàng đợi thông báo mà bạn có thể đăng 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 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 lệnh nhập PubsubApi:

lib/google_play_purchase_handler.dart

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

Sau đó, 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 _pullMessageFromSubSub mỗi 10 giây. Bạn có thể điều chỉnh Thời lượng theo ý muốn của 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ẽ kết nối với Chủ đề Pub/Sub trên Google Cloud cứ 10 giây một lần 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 tin nhắn đến và lấy thông tin cập nhật về từng giao dịch mua, cả gói thuê bao và gói không đăng ký, 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 xác thực:

bin/server.dart

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

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

bin/server.dart

  final pubsubApi = pubsub.PubsubApi(clientGooglePlay);

Cuối cùng, hãy truyền hàm đó 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ã để xử lý 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 điều này.

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

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

Giờ đây, tất cả sự kiện thanh toán trong Google Play sẽ được xuất bản về chủ đề này.

Xử lý 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 xử lý các bản cập nhật trong giao dịch mua trên App Store. Một là triển khai webhook mà bạn cung cấp cho Apple, rồi sử dụng webhook này để giao tiếp với máy chủ của bạn. Cách thứ hai là 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à 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 cho máy chủ của mình kết nối với Internet để triển khai webhook.

Trong môi trường phát hành công khai, lý tưởng là bạn muốn có cả hai. Webhook để lấy thông tin về 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 của một 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ộ tính giờ sẽ gọi phương thức _pullStatus. Bộ tính giờ này sẽ gọi phương thức _pullStatus 10 giây một lần. Bạn có thể điều chỉnh thời lượng của đồng hồ 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 đó, tạo phương thức _pullStatus như sau:

lib/app_store_purchase_handler.dart

  Future<void> _pullStatus() async {
    print('Polling App Store');
    final purchases = await iapRepository.getPurchases();
    // filter for App Store subscriptions
    final appStoreSubscriptions = purchases.where((element) =>
        element.type == ProductType.subscription &&
        element.iapSource == IAPSource.appstore);
    for (final purchase in appStoreSubscriptions) {
      final status =
          await appStoreServerAPI.getAllSubscriptionStatuses(purchase.orderId);
      // Obtain all subscriptions for the order id.
      for (final subscription in status.data) {
        // Last transaction contains the subscription status.
        for (final transaction in subscription.lastTransactions) {
          final expirationDate = DateTime.fromMillisecondsSinceEpoch(
              transaction.transactionInfo.expiresDate ?? 0);
          // Check if subscription has expired.
          final isExpired = expirationDate.isBefore(DateTime.now());
          print('Expiration Date: $expirationDate - isExpired: $isExpired');
          // Update the subscription status with the new expiration date and status.
          await iapRepository.updatePurchase(SubscriptionPurchase(
            userId: null,
            productId: transaction.transactionInfo.productId,
            iapSource: IAPSource.appstore,
            orderId: transaction.originalTransactionId,
            purchaseDate: DateTime.fromMillisecondsSinceEpoch(
                transaction.transactionInfo.originalPurchaseDate),
            type: ProductType.subscription,
            expiryDate: expirationDate,
            status: isExpired
                ? SubscriptionStatus.expired
                : SubscriptionStatus.active,
          ));
        }
      }
    }
  }

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

  1. Lấy danh sách 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 đăng ký API máy chủ App Store.
  3. Nhận giao dịch gần đây nhất của 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 của gói thuê bao trên Firestore. Nếu gói thuê bao đã hết hạn, thông tin này sẽ được đánh dấu.

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 App Store Server API:

bin/server.dart

  // add from here
  final subscriptionKeyAppStore =
      File('assets/SubscriptionKey.p8').readAsStringSync();

  // Configure Apple Store API access
  var appStoreEnvironment = AppStoreEnvironment.sandbox(
    bundleId: bundleId,
    issuerId: appStoreIssuerId,
    keyId: appStoreKeyId,
    privateKey: subscriptionKeyAppStore,
  );

  // Stored token for Apple Store API access, if available
  final file = File('assets/appstore.token');
  String? appStoreToken;
  if (file.existsSync() && file.lengthSync() > 0) {
    appStoreToken = file.readAsStringSync();
  }

  final appStoreServerAPI = AppStoreServerAPI(
    AppStoreServerHttpClient(
      appStoreEnvironment,
      jwt: appStoreToken,
      jwtTokenUpdatedCallback: (token) {
        file.writeAsStringSync(token);
      },
    ),
  );
  // to here


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

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 Loại khoá > Mua hàng trong ứng dụng.
  3. Nhấn vào dấu "dấu cộng" để thêm báo cáo mới.
  4. Đặt tên cho quảng cáo, ví dụ: "Khoá lớp học lập trình".
  5. Tải tệp p8 chứa khoá này xuống.
  6. Sao chép tệp này vào thư mục thành phần, có tên là SubscriptionKey.p8.
  7. Sao chép mã khoá của khoá mới tạo rồi đặt thành hằng số appStoreKeyId trong tệp lib/constants.dart.
  8. Sao chép mã phát hành ngay ở đầu danh sách khoá rồi đặt thành hằng số appStoreIssuerId trong tệp lib/constants.dart.

9540ea9ada3da151.pngS

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 rất khó bảo mật, nhưng bạn cần có cách nào đó nhằm đưa thông tin về cho máy khách để ứng dụng có thể xử lý thông tin trạng thái gói thuê bao. Bằng cách lưu trữ các giao dịch mua hàng 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 đã thêm IAPRepo vào ứng dụng. Đây là kho lưu trữ Firestore chứa tất cả dữ liệu về giao dịch mua của người dùng trong List<PastPurchase> purchases. Kho lưu trữ cũng chứa hasActiveSubscription,, là đúng khi có một giao dịch mua bằng productId storeKeySubscription với trạng thái chưa hết hạn. Khi người dùng không đă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((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 trực tiếp thêm trình nghe vào hàm khởi tạo và xoá trình nghe trong phương thức dispose(). Ban đầu, trình nghe có thể chỉ là một hàm trống. Vì IAPRepoChangeNotifier và bạn gọi notifyListeners() mỗi khi các 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() {
    iapRepo.removeListener(purchasesUpdate);
    _subscription.cancel();
    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,, phương thức applyPaidMultiplierremovePaidMultiplier sẽ đặt hệ số lần lượt 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 của gói thuê bao thay đổi, bạn cũng cần cập nhật trạng thái của sản phẩm có thể mua để trang mua hàng có thể cho biết rằng sản phẩm đó đang hoạt động. Đặt thuộc tính _beautifiedDashUpgrade dựa trên việc có mua bản 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 là thông tin mới nhất trong dịch vụ phụ trợ và được đồng bộ hoá với ứng dụng. Ứng dụng này sẽ hoạt động theo đó và áp dụng gói thuê bao cũng như nâng cấp các tính năng cho trò chơi Dash 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 đoạn mã đã hoàn tất cho lớp học lập trình này trong thư mục android_studio_folder.pngcomplete.

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