Bán gói thuê bao trong ứng dụng thông qua Thư viện Play Billing 5

1. Giới thiệu

Hệ thống thanh toán của Google Play là dịch vụ cho phép bạn bán các sản phẩm và nội dung kỹ thuật số trong ứng dụng Android của mình. Đó là cách trực tiếp nhất để bạn bán sản phẩm trong ứng dụng để kiếm tiền từ ứng dụng của mình. Lớp học lập trình này cho bạn biết cách sử dụng Thư viện Google Play Billing để bán gói thuê bao trong dự án của mình theo cách tổng hợp chi tiết thực chất khi tích hợp giao dịch mua với phần còn lại của ứng dụng.

Tài liệu này cũng giới thiệu các khái niệm liên quan đến gói thuê bao, chẳng hạn như gói cơ bản, ưu đãi, thẻ và gói trả trước. Để tìm hiểu thêm về các gói thuê bao trên Google Play Billing, bạn có thể tham khảo Trung tâm trợ giúp của chúng tôi.

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

Trong lớp học lập trình này, bạn sẽ thêm thư viện Billing mới nhất (phiên bản 5.0.0) vào một ứng dụng hồ sơ đơn giản dựa trên gói thuê bao. Ứng dụng này đã được tạo sẵn cho bạn, nên bạn chỉ cần thêm phần thanh toán. Như trong hình 1, trong ứng dụng này, người dùng đăng ký bất kỳ gói cơ bản và/hoặc ưu đãi nào được cung cấp qua 2 sản phẩm thuê bao có thể tái tạo (cơ bản và cao cấp) hoặc gói trả trước không thể gia hạn. Chỉ có vậy thôi. Gói cơ bản tương ứng là gói thuê bao hằng tháng và hằng năm. Người dùng có thể nâng cấp, hạ cấp hoặc chuyển đổi gói thuê bao trả trước thành gói thuê bao có thể gia hạn.

d7dba51f800a6cc4.png 2220c15b849d2ead.pngs

Để tích hợp Thư viện Google Play Billing vào ứng dụng, bạn sẽ tạo như sau:

  • BillingClientWrapper – một trình bao bọc cho thư viện BillingClient. Tính năng này dự định đóng gói các hoạt động tương tác với BillingClient của Thư viện Play Billing nhưng bạn không bắt buộc phải thực hiện việc này trong quá trình tích hợp của riêng mình.
  • SubscriptionDataRepository – kho lưu trữ thanh toán cho ứng dụng của bạn, chứa danh sách kho hàng sản phẩm thuê bao của ứng dụng (ví dụ: nội dung được bán) và danh sách các biến ShareFlow giúp thu thập trạng thái của giao dịch mua và chi tiết sản phẩm
  • MainViewModel – ViewModel mà phần còn lại của ứng dụng giao tiếp với kho lưu trữ thanh toán. Việc này giúp bắt đầu quy trình thanh toán trong giao diện người dùng bằng nhiều phương thức mua.

Sau khi hoàn tất, cấu trúc của ứng dụng sẽ giống như hình dưới đây:

c83bc759f32b0a63.png

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

  • Cách tích hợp thư viện Play Billing
  • Cách tạo sản phẩm thuê bao, gói cơ bản, ưu đãi và thẻ qua Play Console
  • Cách truy xuất các gói cơ bản và ưu đãi hiện có trong ứng dụng
  • Cách bắt đầu quy trình thanh toán bằng các thông số thích hợp
  • Cách cung cấp sản phẩm thuê bao trả trước

Lớp học lập trình này tập trung vào Google Play Billing. Các khái niệm và khối mã không liên quan được che khuất và chỉ được cung cấp cho bạn để sao chép và dán.

Bạn cần có

  • Phiên bản Android Studio gần đây (>= Arctic Fox | 2020.3.1)
  • Thiết bị Android chạy Android 8.0 trở lên
  • Mã mẫu được cung cấp cho bạn trên GitHub (hướng dẫn trong các phần sau)
  • Vừa phải có kiến thức về phát triển Android trên Android Studio
  • Có kiến thức về cách phát hành ứng dụng lên Cửa hàng Google Play
  • Có kinh nghiệm trung bình khi viết mã Kotlin
  • Thư viện Google Play Billing phiên bản 5.0.0

2. Thiết lập

Lấy mã từ GitHub

Chúng tôi đã đặt mọi thứ bạn cần cho dự án này vào Git repo. Để bắt đầu, bạn cần lấy mã và mở mã đó trong môi trường nhà phát triển yêu thích của bạn. Đối với lớp học lập trình này, bạn nên sử dụng Android Studio.

Mã để bắt đầu được lưu trữ trong kho lưu trữ GitHub. Bạn có thể sao chép kho lưu trữ bằng lệnh sau đây:

git clone https://github.com/android/play-billing-samples.git
cd PlayBillingCodelab

3. Phần nền

Điểm xuất phát của chúng ta là gì?

Điểm bắt đầu của chúng ta là một ứng dụng hồ sơ người dùng cơ bản được thiết kế cho lớp học lập trình này. Mã này đã được đơn giản hoá để hiển thị các khái niệm mà chúng tôi muốn minh hoạ và chưa được thiết kế để sử dụng trong thực tế. Nếu bạn chọn sử dụng lại bất kỳ phần nào của mã này trong ứng dụng chính thức, hãy nhớ làm theo các phương pháp hay nhất và kiểm thử đầy đủ tất cả mã của bạn.

Nhập dự án vào Android Studio

PlayBillingCodelab là ứng dụng cơ bản không có cách triển khai Google Play Billing. Khởi động Android Studio rồi nhập lớp học lập trình thanh toán bằng cách chọn Open > billing/PlayBillingCodelab

Dự án có hai mô-đun:

  • start có ứng dụng skeleton nhưng thiếu các phần phụ thuộc bắt buộc và tất cả các phương thức bạn cần để triển khai.
  • finished có dự án đã hoàn thành và có thể đóng vai trò là hướng dẫn khi bạn gặp khó khăn.

Ứng dụng này bao gồm 8 tệp lớp: BillingClientWrapper, SubscriptionDataRepository, Composables, MainState, MainViewModel, MainViewModelFactoryMainActivity.

  • BillingClientWrapper là một trình bao bọc tách biệt các phương thức [BillingClient] của Google Play Billing để có một quy trình triển khai đơn giản và phát ra phản hồi cho kho dữ liệu để xử lý.
  • SubscriptionDataRepository được dùng để tóm tắt nguồn dữ liệu Google Play Billing (tức là thư viện Ứng dụng thanh toán) và chuyển đổi dữ liệu StateFlow được phát ra trong BillingClientWrapper thành Flow.
  • ButtonModel là lớp dữ liệu dùng để tạo các nút trong giao diện người dùng.
  • Composables trích xuất mọi phương thức có thể kết hợp của giao diện người dùng thành một lớp.
  • MainState là một lớp dữ liệu để quản lý trạng thái.
  • MainViewModel dùng để lưu giữ dữ liệu liên quan đến việc thanh toán và các trạng thái dùng trong giao diện người dùng. Tệp này kết hợp tất cả các luồng trong SubscriptionDataRepository thành một đối tượng trạng thái.
  • MainActivity là lớp hoạt động chính tải các Thành phần kết hợp cho giao diện người dùng.
  • Hằng số là đối tượng có các hằng số được nhiều lớp sử dụng.

Gradle

Bạn cần thêm một phần phụ thuộc Gradle để có thể thêm Google Play Billing vào ứng dụng của mình. Mở tệp build.gradle của mô-đun ứng dụng và thêm đoạn mã sau:

dependencies {
    val billing_version = "5.0.0"

    implementation("com.android.billingclient:billing:$billing_version")
}

Google Play Console

Để phục vụ mục đích của lớp học lập trình này, bạn cần tạo 2 sản phẩm thuê bao sau đây trong phần gói thuê bao của Google Play Console:

  • 1 gói thuê bao cơ bản có mã sản phẩm up_basic_sub

Sản phẩm này phải có 3 gói cơ bản (2 gói tự động gia hạn và 1 gói trả trước) có các thẻ được liên kết : 1 gói thuê bao cơ bản hằng tháng có thẻ monthlybasic, 1 gói thuê bao cơ bản hằng năm có thẻ yearlybasic và 1 gói thuê bao trả trước có thẻ prepaidbasic

Bạn có thể thêm ưu đãi vào gói cơ bản. Ưu đãi sẽ kế thừa thẻ của các gói cơ bản có liên quan.

  • 1 gói thuê bao cao cấp có mã sản phẩm up_premium_sub

Sản phẩm này phải có 3 gói cơ bản(2 gói tự động gia hạn và 1 gói trả trước) có các thẻ được liên kết: 1 gói thuê bao cơ bản hằng tháng có thẻ monthlypremium, 1 gói thuê bao cơ bản hằng năm có thẻ yearlypremium và 1 gói thuê bao trả trước có thẻ prepaidpremium

a9f6fd6e70e69fed.png

Bạn có thể thêm ưu đãi vào gói cơ bản. Ưu đãi sẽ kế thừa thẻ của các gói cơ bản có liên quan.

Để biết thêm thông tin chi tiết về cách tạo sản phẩm thuê bao, gói cơ bản, ưu đãi và thẻ, vui lòng truy cập vào Trung tâm trợ giúp của Google Play.

4. Thiết lập Ứng dụng thanh toán

Trong phần này, bạn sẽ học trong lớp BillingClientWrapper.

Khi hoàn tất, bạn sẽ có mọi thứ cần thiết để tạo thực thể cho Ứng dụng thanh toán cũng như tất cả các phương thức có liên quan.

  1. Khởi động BillingClient

Sau khi thêm một phần phụ thuộc vào Thư viện Google Play Billing, chúng ta cần khởi động một thực thể BillingClient.

BillingClientWrapper.kt

private val billingClient = BillingClient.newBuilder(context)
   .setListener(this)
   .enablePendingPurchases()
   .build()
  1. Thiết lập kết nối với Google Play

Sau khi bạn tạo BillingClient, chúng tôi cần thiết lập kết nối với Google Play.

Để kết nối với Google Play, chúng ta gọi startConnection(). Quá trình kết nối không đồng bộ và chúng ta cần triển khai BillingClientStateListener để nhận một lệnh gọi lại sau khi ứng dụng được thiết lập và sẵn sàng tạo thêm yêu cầu.

BillingClientWrapper.kt

fun startBillingConnection(billingConnectionState: MutableLiveData<Boolean>) {

   billingClient.startConnection(object : BillingClientStateListener {
       override fun onBillingSetupFinished(billingResult: BillingResult) {
           if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
               Log.d(TAG, "Billing response OK")
               // The BillingClient is ready. You can query purchases and product details here
               queryPurchases()
               queryProductDetails()
               billingConnectionState.postValue(true)
           } else {
               Log.e(TAG, billingResult.debugMessage)
           }
       }

       override fun onBillingServiceDisconnected() {
           Log.i(TAG, "Billing connection disconnected")
           startBillingConnection(billingConnectionState)
       }
   })
}
  1. Truy vấn Google Play Billing cho các giao dịch mua hiện tại

Sau khi thiết lập kết nối với Google Play, chúng ta có thể truy vấn các giao dịch mua mà người dùng đã thực hiện trước đây bằng cách gọi queryPurchasesAsync().

BillingClientWrapper.kt

fun queryPurchases() {
   if (!billingClient.isReady) {
       Log.e(TAG, "queryPurchases: BillingClient is not ready")
   }
   // Query for existing subscription products that have been purchased.
   billingClient.queryPurchasesAsync(
       QueryPurchasesParams.newBuilder().setProductType(BillingClient.ProductType.SUBS).build()
   ) { billingResult, purchaseList ->
       if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
           if (!purchaseList.isNullOrEmpty()) {
               _purchases.value = purchaseList
           } else {
               _purchases.value = emptyList()
           }

       } else {
           Log.e(TAG, billingResult.debugMessage)
       }
   }
}
  1. Giới thiệu các sản phẩm có sẵn để mua

Bây giờ, chúng ta có thể truy vấn các sản phẩm hiện có và cho người dùng thấy các sản phẩm đó. Để truy vấn Google Play về thông tin chi tiết của sản phẩm thuê bao, chúng tôi sẽ gọi queryProductDetailsAsync(). Truy vấn thông tin chi tiết về sản phẩm là một bước quan trọng trước khi hiển thị sản phẩm cho người dùng vì việc truy vấn này sẽ trả về thông tin sản phẩm đã bản địa hoá.

BillingClientWrapper.kt

fun queryProductDetails() {
   val params = QueryProductDetailsParams.newBuilder()
   val productList = mutableListOf<QueryProductDetailsParams.Product>()
   for (product in LIST_OF_PRODUCTS) {

       productList.add(
           QueryProductDetailsParams.Product.newBuilder()
               .setProductId(product)
               .setProductType(BillingClient.ProductType.SUBS)
               .build()
       )

       params.setProductList(productList).let { productDetailsParams ->
           Log.i(TAG, "queryProductDetailsAsync")
           billingClient.queryProductDetailsAsync(productDetailsParams.build(), this)
       }
   }
}
  1. Đặt trình nghe cho truy vấn ProductDetails

Lưu ý: Phương thức này tạo ra kết quả của truy vấn trong một Ánh xạ tới _productWithProductDetails.

Ngoài ra, xin lưu ý truy vấn dự kiến sẽ trả về ProductDetails. Nếu không, vấn đề rất có thể là do các sản phẩm được thiết lập trong Play Console chưa được kích hoạt hoặc bạn chưa phát hành bản dựng có phần phụ thuộc ứng dụng thanh toán trong bất kỳ kênh phát hành nào.

BillingClientWrapper.kt

override fun onProductDetailsResponse(
   billingResult: BillingResult,
   productDetailsList: MutableList<ProductDetails>
) {
   val responseCode = billingResult.responseCode
   val debugMessage = billingResult.debugMessage
   when (responseCode) {
       BillingClient.BillingResponseCode.OK -> {
           var newMap = emptyMap<String, ProductDetails>()
           if (productDetailsList.isNullOrEmpty()) {
               Log.e(
                   TAG,
                   "onProductDetailsResponse: " +
                           "Found null or empty ProductDetails. " +
                           "Check to see if the Products you requested are correctly " +
                           "published in the Google Play Console."
               )
           } else {
               newMap = productDetailsList.associateBy {
                   it.productId
               }
           }
           _productWithProductDetails.value = newMap
       }
       else -> {
           Log.i(TAG, "onProductDetailsResponse: $responseCode $debugMessage")
       }
   }
}
  1. Bắt đầu quy trình mua

launchBillingFlow là phương thức được gọi khi người dùng nhấp để mua một mặt hàng. Thao tác này sẽ nhắc Google Play bắt đầu quy trình mua bằng ProductDetails của sản phẩm.

BillingClientWrapper.kt

fun launchBillingFlow(activity: Activity, params: BillingFlowParams) {
   if (!billingClient.isReady) {
       Log.e(TAG, "launchBillingFlow: BillingClient is not ready")
   }
   billingClient.launchBillingFlow(activity, params)

}
  1. Thiết lập trình nghe cho kết quả của thao tác mua hàng

Khi người dùng thoát khỏi màn hình mua hàng trên Google Play (bằng cách nhấn vào nút "Mua" để hoàn tất giao dịch mua hoặc bằng cách nhấn vào nút Quay lại để huỷ giao dịch mua), lệnh gọi lại onPurchaseUpdated() sẽ gửi kết quả của quy trình mua trở lại ứng dụng của bạn. Dựa vào BillingResult.responseCode, bạn có thể xác định xem người dùng có mua sản phẩm thành công hay không. Nếu giá trị là responseCode == OK, điều đó có nghĩa là giao dịch mua đã hoàn tất thành công.

onPurchaseUpdated() trả về danh sách đối tượng Purchase bao gồm tất cả giao dịch mua mà người dùng đã thực hiện qua ứng dụng. Trong số nhiều trường khác, mỗi đối tượng Purchase đều chứa các thuộc tính product id (mã sản phẩm), purchaseToken và isAcknowledged. Khi sử dụng các trường này, đối với mỗi đối tượng Purchase, bạn có thể xác định đó là giao dịch mua mới cần được xử lý hay giao dịch mua hiện có không cần xử lý thêm.

Đối với giao dịch mua gói thuê bao, việc xử lý chỉ giống như việc xác nhận giao dịch mua mới.

BillingClientWrapper.kt

override fun onPurchasesUpdated(
   billingResult: BillingResult,
   purchases: List<Purchase>?
) {
   if (billingResult.responseCode == BillingClient.BillingResponseCode.OK
       && !purchases.isNullOrEmpty()
   ) {
       // Post new purchase List to _purchases
       _purchases.value = purchases

       // Then, handle the purchases
       for (purchase in purchases) {
           acknowledgePurchases(purchase)
       }
   } else if (billingResult.responseCode == BillingClient.BillingResponseCode.USER_CANCELED) {
       // Handle an error caused by a user cancelling the purchase flow.
       Log.e(TAG, "User has cancelled")
   } else {
       // Handle any other error codes.
   }
}
  1. Xử lý giao dịch mua (Xác minh và xác nhận giao dịch mua)

Sau khi người dùng hoàn tất giao dịch mua, ứng dụng cần xử lý giao dịch mua đó bằng cách xác nhận giao dịch mua.

Ngoài ra, giá trị của _isNewPurchaseAcknowledged được đặt thành true khi xác nhận được xử lý thành công.

BillingClientWrapper.kt

private fun acknowledgePurchases(purchase: Purchase?) {
   purchase?.let {
       if (!it.isAcknowledged) {
           val params = AcknowledgePurchaseParams.newBuilder()
               .setPurchaseToken(it.purchaseToken)
               .build()

           billingClient.acknowledgePurchase(
               params
           ) { billingResult ->
               if (billingResult.responseCode == BillingClient.BillingResponseCode.OK &&
                   it.purchaseState == Purchase.PurchaseState.PURCHASED
               ) {
                   _isNewPurchaseAcknowledged.value = true
               }
           }
       }
   }
}
  1. Chấm dứt kết nối thanh toán

Cuối cùng, khi một hoạt động bị huỷ bỏ, bạn muốn chấm dứt kết nối với Google Play, vì vậy endConnection() được gọi để thực hiện việc này.

BillingClientWrapper.kt

fun terminateBillingConnection() {
   Log.i(TAG, "Terminating connection")
   billingClient.endConnection()
}

5. SubscriptionDataRepository

Trong BillingClientWrapper, các câu trả lời của QueryPurchasesAsyncQueryProductDetails được đăng lần lượt lên MutableStateFlow _purchases_productWithProductDetails (hiển thị bên ngoài lớp học) với giao dịch mua và productWithProductDetails.

Trong SubscriptionDataRepository, các giao dịch mua được xử lý thành ba Quy trình dựa trên sản phẩm của giao dịch mua được trả lại: hasRenewableBasic, hasPrepaidBasic, hasRenewablePremiumhasPremiumPrepaid.

Ngoài ra, productWithProductDetails được xử lý thành các Luồng basicProductDetailspremiumProductDetails tương ứng.

6. MainViewModel

Phần khó đã hoàn thành. Bây giờ, bạn sẽ định nghĩa MainViewModel. Đây chỉ là một giao diện công khai cho các ứng dụng khách của bạn để họ không cần biết nội dung bên trong của BillingClientWrapperSubscriptionDataRepository.

Trước tiên, trong MainViewModel, chúng ta bắt đầu kết nối thanh toán khi khởi chạy viewModel.

MainViewModel.kt

init {
   billingClient.startBillingConnection(billingConnectionState = _billingConnectionState)
}

Sau đó, Flow từ kho lưu trữ lần lượt được kết hợp thành productsForSaleFlows (đối với các sản phẩm có sẵn) và userCurrentSubscriptionFlow (đối với gói thuê bao hiện tại và đang hoạt động của người dùng) như được xử lý trong lớp kho lưu trữ.

Danh sách các giao dịch mua hiện tại cũng được cung cấp cho giao diện người dùng bằng currentPurchasesFlow.

MainViewModel.kt

val productsForSaleFlows = combine(
   repo.basicProductDetails,
   repo.premiumProductDetails
) { basicProductDetails,
   premiumProductDetails
   ->
   MainState(
       basicProductDetails = basicProductDetails,
       premiumProductDetails = premiumProductDetails
   )
}

// The userCurrentSubscriptionFlow object combines all the possible subscription flows into one
// for emission.
private val userCurrentSubscriptionFlow = combine(
   repo.hasRenewableBasic,
   repo.hasPrepaidBasic,
   repo.hasRenewablePremium,
   repo.hasPrepaidPremium
) { hasRenewableBasic,
   hasPrepaidBasic,
   hasRenewablePremium,
   hasPrepaidPremium
   ->
   MainState(
       hasRenewableBasic = hasRenewableBasic,
       hasPrepaidBasic = hasPrepaidBasic,
       hasRenewablePremium = hasRenewablePremium,
       hasPrepaidPremium = hasPrepaidPremium
   )
}

// Current purchases.
val currentPurchasesFlow = repo.purchases

userCurrentSubscriptionFlow kết hợp được thu thập trong một khối init và giá trị được đăng lên đối tượng MutableLiveData có tên là _destinationScreen.

init {
   viewModelScope.launch {
       userCurrentSubscriptionFlow.collectLatest { collectedSubscriptions ->
           when {
               collectedSubscriptions.hasRenewableBasic == true &&
                       collectedSubscriptions.hasRenewablePremium == false -> {
                   _destinationScreen.postValue(DestinationScreen.BASIC_RENEWABLE_PROFILE)
               }
               collectedSubscriptions.hasRenewablePremium == true &&
                       collectedSubscriptions.hasRenewableBasic == false -> {
                   _destinationScreen.postValue(DestinationScreen.PREMIUM_RENEWABLE_PROFILE)
               }
               collectedSubscriptions.hasPrepaidBasic == true &&
                       collectedSubscriptions.hasPrepaidPremium == false -> {
                   _destinationScreen.postValue(DestinationScreen.BASIC_PREPAID_PROFILE_SCREEN)
               }
               collectedSubscriptions.hasPrepaidPremium == true &&
                       collectedSubscriptions.hasPrepaidBasic == false -> {
                   _destinationScreen.postValue(
                       DestinationScreen.PREMIUM_PREPAID_PROFILE_SCREEN
                   )
               }
               else -> {
                   _destinationScreen.postValue(DestinationScreen.SUBSCRIPTIONS_OPTIONS_SCREEN)
               }
           }
       }

   }
}

MainViewModel cũng thêm một số phương thức rất hữu ích:

  1. Truy xuất gói cơ bản và mã thông báo ưu đãi

Kể từ Thư viện Play Billing phiên bản 5.0.0, tất cả sản phẩm thuê bao đều có thể có nhiều gói cơ bản và ưu đãi, ngoại trừ những gói cơ bản trả trước không được có ưu đãi.

Phương thức này giúp truy xuất tất cả ưu đãi và gói cơ bản mà người dùng đủ điều kiện sử dụng bằng cách sử dụng khái niệm mới ra mắt về những thẻ dùng để nhóm các ưu đãi có liên quan.

Ví dụ: khi người dùng cố gắng mua một gói thuê bao cơ bản hằng tháng, tất cả các gói cơ bản và ưu đãi liên kết với sản phẩm thuê bao cơ bản hằng tháng đều được gắn thẻ bằng chuỗi monthlyBasic.

MainViewModel.kt

private fun retrieveEligibleOffers(
   offerDetails: MutableList<ProductDetails.SubscriptionOfferDetails>,
   tag: String
): List<ProductDetails.SubscriptionOfferDetails> {
   val eligibleOffers = emptyList<ProductDetails.SubscriptionOfferDetails>().toMutableList()
   offerDetails.forEach { offerDetail ->
       if (offerDetail.offerTags.contains(tag)) {
           eligibleOffers.add(offerDetail)
       }
   }

   return eligibleOffers
}
  1. Cách tính giá đề nghị có giá thấp nhất

Khi người dùng đủ điều kiện nhận nhiều ưu đãi, phương thức leastPricedOfferToken() sẽ được dùng để tính ưu đãi thấp nhất trong số những ưu đãi do retrieveEligibleOffers() trả về.

Phương thức này sẽ trả về mã thông báo mã mặt hàng của ưu đãi đã chọn.

Cách triển khai này chỉ trả về các ưu đãi có giá thấp nhất theo tập hợp pricingPhases và không tính đến mức trung bình.

Một cách khác có thể là dùng ưu đãi có giá trung bình thấp nhất.

MainViewModel.kt

private fun leastPricedOfferToken(
   offerDetails: List<ProductDetails.SubscriptionOfferDetails>
): String {
   var offerToken = String()
   var leastPricedOffer: ProductDetails.SubscriptionOfferDetails
   var lowestPrice = Int.MAX_VALUE

   if (!offerDetails.isNullOrEmpty()) {
       for (offer in offerDetails) {
           for (price in offer.pricingPhases.pricingPhaseList) {
               if (price.priceAmountMicros < lowestPrice) {
                   lowestPrice = price.priceAmountMicros.toInt()
                   leastPricedOffer = offer
                   offerToken = leastPricedOffer.offerToken
               }
           }
       }
   }
   return offerToken
}
  1. Trình tạo BillingFlowParams

Để bắt đầu quy trình mua đối với một sản phẩm cụ thể, Bạn cần đặt ProductDetails và mã thông báo của ưu đãi đã chọn để tạo BilingFlowParams.

Có hai phương pháp để giúp bạn làm việc này:

upDowngradeBillingFlowParamsBuilder() tạo các tham số để nâng cấp và hạ cấp.

MainViewModel.kt

private fun upDowngradeBillingFlowParamsBuilder(
   productDetails: ProductDetails,
   offerToken: String,
   oldToken: String
): BillingFlowParams {
   return BillingFlowParams.newBuilder().setProductDetailsParamsList(
       listOf(
           BillingFlowParams.ProductDetailsParams.newBuilder()
               .setProductDetails(productDetails)
               .setOfferToken(offerToken)
               .build()
       )
   ).setSubscriptionUpdateParams(
       BillingFlowParams.SubscriptionUpdateParams.newBuilder()
           .setOldPurchaseToken(oldToken)
           .setReplaceProrationMode(
               BillingFlowParams.ProrationMode.IMMEDIATE_AND_CHARGE_FULL_PRICE
           )
           .build()
   ).build()
}

billingFlowParamsBuilder() tạo thông số cho các giao dịch mua hàng thông thường.

MainViewModel.kt

private fun billingFlowParamsBuilder(
   productDetails: ProductDetails,
   offerToken: String
): BillingFlowParams.Builder {
   return BillingFlowParams.newBuilder().setProductDetailsParamsList(
       listOf(
           BillingFlowParams.ProductDetailsParams.newBuilder()
               .setProductDetails(productDetails)
               .setOfferToken(offerToken)
               .build()
       )
   )
}
  1. Phương thức mua

Phương thức mua sử dụng launchBillingFlow() của BillingClientWrapperBillingFlowParams để tiến hành giao dịch mua.

MainViewModel.kt

fun buy(
   productDetails: ProductDetails,
   currentPurchases: List<Purchase>?,
   activity: Activity,
   tag: String
) {
   val offers =
       productDetails.subscriptionOfferDetails?.let {
           retrieveEligibleOffers(
               offerDetails = it,
               tag = tag.lowercase()
           )
       }
   val offerToken = offers?.let { leastPricedOfferToken(it) }
   val oldPurchaseToken: String

   // Get current purchase. In this app, a user can only have one current purchase at
   // any given time.
   if (!currentPurchases.isNullOrEmpty() &&
       currentPurchases.size == MAX_CURRENT_PURCHASES_ALLOWED
   ) {
       // This either an upgrade, downgrade, or conversion purchase.
       val currentPurchase = currentPurchases.first()

       // Get the token from current purchase.
       oldPurchaseToken = currentPurchase.purchaseToken

       val billingParams = offerToken?.let {
           upDowngradeBillingFlowParamsBuilder(
               productDetails = productDetails,
               offerToken = it,
               oldToken = oldPurchaseToken
           )
       }

       if (billingParams != null) {
           billingClient.launchBillingFlow(
               activity,
               billingParams
           )
       }
   } else if (currentPurchases == null) {
       // This is a normal purchase.
       val billingParams = offerToken?.let {
           billingFlowParamsBuilder(
               productDetails = productDetails,
               offerToken = it
           )
       }

       if (billingParams != null) {
           billingClient.launchBillingFlow(
               activity,
               billingParams.build()
           )
       }
   } else if (!currentPurchases.isNullOrEmpty() &&
       currentPurchases.size > MAX_CURRENT_PURCHASES_ALLOWED
   ) {
       // The developer has allowed users  to have more than 1 purchase, so they need to
       /// implement a logic to find which one to use.
       Log.d(TAG, "User has more than 1 current purchase.")
   }
}

d726a27add092140.png

  1. Chấm dứt kết nối thanh toán

Cuối cùng, phương thức terminateBillingConnection của BillingClientWrapper được gọi trên onCleared() của ViewModel.

Thao tác này sẽ chấm dứt kết nối thanh toán hiện tại khi hoạt động liên quan bị huỷ.

7. Giao diện người dùng

Đã đến lúc sử dụng mọi tính năng bạn đã xây dựng trong giao diện người dùng. Để hỗ trợ việc đó, bạn sẽ làm việc với các lớp Thành phần kết hợp và lớp MainActivity.

Composables.kt

Lớp Thành phần kết hợp được cung cấp đầy đủ và xác định tất cả các hàm trong Compose dùng để kết xuất giao diện người dùng cũng như cơ chế điều hướng giữa các hàm đó.

Hàm Subscriptions cho thấy 2 nút: Basic SubscriptionPremium Subscription.

Basic SubscriptionPremium Subscription đều tải các phương thức Compose mới để hiển thị 3 gói cơ bản tương ứng: hằng tháng, hằng năm và trả trước.

Sau đó, có thể có 3 chức năng soạn hồ sơ cho một gói thuê bao cụ thể mà người dùng có thể có: cấu hình Basic có thể gia hạn, Premium có thể gia hạn và cấu hình trả trước cơ bản hoặc trả trước Premium.

  • Khi người dùng có gói thuê bao Cơ bản, hồ sơ cơ bản sẽ cho phép họ nâng cấp lên gói thuê bao cao cấp trả trước, hằng tháng hoặc hằng năm.
  • Ngược lại, khi có gói thuê bao cao cấp, người dùng có thể hạ cấp xuống gói thuê bao hằng tháng, hằng năm hoặc gói thuê bao cơ bản trả trước.
  • Khi có gói thuê bao trả trước, người dùng có thể nạp tiền vào gói thuê bao bằng nút nạp tiền hoặc chuyển đổi gói thuê bao trả trước thành gói cơ bản tự động gia hạn tương ứng.

Cuối cùng, có chức năng Đang tải màn hình được sử dụng khi kết nối được thực hiện với Google Play và khi hồ sơ người dùng đang hiển thị.

MainActivity.kt

  1. Khi MainActivity được tạo, viewModel sẽ được tạo thực thể và hàm Compose có tên là MainNavHost sẽ được tải.
  2. MainNavHost bắt đầu bằng một biến isBillingConnected được tạo từ Livedata billingConnectionSate của viewModel và được quan sát để thay đổi vì khi tạo thực thể vieModel, biến đó sẽ chuyển billingConnectionSate vào phương thức startBillingConnection của BillingClientWrapper.

isBillingConnected được đặt thành true khi kết nối được thiết lập và false khi không thiết lập kết nối.

Khi bạn đặt chính sách này thành false, hàm có khả năng kết hợp LoadingScreen() sẽ được tải. Khi đó, hàm Subscription hoặc các hàm cấu hình sẽ được tải.

val isBillingConnected by viewModel.billingConnectionState.observeAsState()
  1. Khi mối kết nối thanh toán được thiết lập:

Đã tạo thực thể cho navController của Compose

val navController = rememberNavController()

Sau đó, các luồng trong MainViewModel sẽ được thu thập.

val productsForSale by viewModel.productsForSaleFlows.collectAsState(
   initial = MainState()
)

val currentPurchases by viewModel.currentPurchasesFlow.collectAsState(
   initial = listOf()
)

Cuối cùng, biến LiveData destinationScreen của viewModel được quan sát.

Dựa trên trạng thái gói thuê bao hiện tại của người dùng, chức năng soạn thư tương ứng sẽ xuất hiện.

val screen by viewModel.destinationScreen.observeAsState()
when (screen) {
   // User has a Basic Prepaid subscription
   // the corresponding profile is loaded.
   MainViewModel.DestinationScreen.BASIC_PREPAID_PROFILE_SCREEN -> {
       UserProfile(
           buttonModels =
           listOf(
               ButtonModel(R.string.topup_message) {
                   productsForSale.basicProductDetails?.let {
                       viewModel.buy(
                           productDetails = it,
                           currentPurchases = null,
                           tag = PREPAID_BASIC_PLANS_TAG,
                           activity = activity
                       )
                   }
               },
               ButtonModel(R.string.convert_to_basic_monthly_message) {
                   productsForSale.basicProductDetails?.let {
                       viewModel.buy(
                           productDetails = it,
                           currentPurchases = currentPurchases,
                           tag = MONTHLY_BASIC_PLANS_TAG,
                           activity = activity
                       )
                   }
               },
               ButtonModel(R.string.convert_to_basic_yearly_message) {
                   productsForSale.basicProductDetails?.let {
                       viewModel.buy(
                           productDetails = it,
                           currentPurchases = currentPurchases,
                           tag = YEARLY_BASIC_PLANS_TAG,
                           activity = activity
                       )
                   }
               },
           ),
           tag = PREPAID_BASIC_PLANS_TAG,
           profileTextStringResource = null
       )
   }
   // User has a renewable basic subscription
   // the corresponding profile is loaded.
   MainViewModel.DestinationScreen.BASIC_RENEWABLE_PROFILE -> {
       UserProfile(
           buttonModels =
           listOf(
               ButtonModel(R.string.monthly_premium_upgrade_message) {
                   productsForSale.premiumProductDetails?.let {
                       viewModel.buy(
                           productDetails = it,
                           currentPurchases = currentPurchases,
                           tag = MONTHLY_PREMIUM_PLANS_TAG,
                           activity = activity
                       )
                   }
               },
               ButtonModel(R.string.yearly_premium_upgrade_message) {
                   productsForSale.premiumProductDetails?.let {
                       viewModel.buy(
                           productDetails = it,
                           currentPurchases = currentPurchases,
                           tag = YEARLY_PREMIUM_PLANS_TAG,
                           activity = activity
                       )
                   }
               },
               ButtonModel(R.string.prepaid_premium_upgrade_message) {
                   productsForSale.premiumProductDetails?.let {
                       viewModel.buy(
                           productDetails = it,
                           currentPurchases = currentPurchases,
                           tag = PREPAID_PREMIUM_PLANS_TAG,
                           activity = activity
                       )
                   }
               }
           ),
           tag = null,
           profileTextStringResource = R.string.basic_sub_message
       )
   }
   // User has a prepaid Premium subscription
   // the corresponding profile is loaded.
   MainViewModel.DestinationScreen.PREMIUM_PREPAID_PROFILE_SCREEN -> {
       UserProfile(
           buttonModels =
           listOf(
               ButtonModel(R.string.topup_message) {
                   productsForSale.premiumProductDetails?.let {
                       viewModel.buy(
                           productDetails = it,
                           currentPurchases = null,
                           tag = PREPAID_PREMIUM_PLANS_TAG,
                           activity = activity
                       )
                   }
               },
               ButtonModel(R.string.convert_to_premium_monthly_message) {
                   productsForSale.premiumProductDetails?.let {
                       viewModel.buy(
                           productDetails = it,
                           currentPurchases = currentPurchases,
                           tag = MONTHLY_PREMIUM_PLANS_TAG,
                           activity = activity
                       )
                   }
               },
               ButtonModel(R.string.convert_to_premium_yearly_message) {
                   productsForSale.premiumProductDetails?.let {
                       viewModel.buy(
                           productDetails = it,
                           currentPurchases = currentPurchases,
                           tag = YEARLY_PREMIUM_PLANS_TAG,
                           activity = activity
                       )
                   }
               },
           ),
           tag = PREPAID_PREMIUM_PLANS_TAG,
           profileTextStringResource = null
       )
   }
   // User has a renewable Premium subscription
   // the corresponding profile is loaded.
   MainViewModel.DestinationScreen.PREMIUM_RENEWABLE_PROFILE -> {
       UserProfile(
           listOf(
               ButtonModel(R.string.monthly_basic_downgrade_message) {
                   productsForSale.basicProductDetails?.let {
                       viewModel.buy(
                           productDetails = it,
                           currentPurchases = currentPurchases,
                           tag = MONTHLY_BASIC_PLANS_TAG,
                           activity = activity
                       )
                   }
               },
               ButtonModel(R.string.yearly_basic_downgrade_message) {
                   productsForSale.basicProductDetails?.let {
                       viewModel.buy(
                           productDetails = it,
                           currentPurchases = currentPurchases,
                           tag = YEARLY_BASIC_PLANS_TAG,
                           activity = activity
                       )
                   }
               },
               ButtonModel(R.string.prepaid_basic_downgrade_message) {
                   productsForSale.basicProductDetails?.let {
                       viewModel.buy(
                           productDetails = it,
                           currentPurchases = currentPurchases,
                           tag = PREPAID_BASIC_PLANS_TAG,
                           activity = activity
                       )
                   }
               }
           ),
           tag = null,
           profileTextStringResource = R.string.premium_sub_message
       )
   }
   // User has no current subscription - the subscription composable
   // is loaded.
   MainViewModel.DestinationScreen.SUBSCRIPTIONS_OPTIONS_SCREEN -> {
       SubscriptionNavigationComponent(
           productsForSale = productsForSale,
           navController = navController,
           viewModel = viewModel
       )
   }
}

8. Mã nguồn giải pháp

Bạn có thể tìm thấy đoạn mã giải pháp hoàn chỉnh trong mô-đun giải pháp.

9. Xin chúc mừng

Xin chúc mừng! Bạn đã tích hợp thành công các sản phẩm thuê bao Thư viện Google Play Billing 5.0.0 vào một ứng dụng rất đơn giản!

Để tìm hiểu về phiên bản được ghi chép của một ứng dụng tinh vi hơn có chế độ thiết lập máy chủ bảo mật, hãy xem mẫu chính thức.

Tài liệu đọc thêm