Report: State of in-app subscriptions in the US 2023 Get a report

Mua trong ứng dụng Android, phần 1: cấu hình và thêm vào dự án

Vlad Guriev

Updated: March 20, 2023

Content

62fdf1dbf601140886432256 jp android tutorial 1 configuration 1 6

Mua trong ứng dụng (in-app purchase), đặc biệt là gói đăng ký (subscription), là phương thức kiếm tiền từ ứng dụng phổ biến nhất. Nhìn chung, một mặt, gói đăng ký cho phép nhà phát triển đầu tư vào phát triển nội dung và sản phẩm, mặt khác, gói đăng ký giúp người dùng có được ứng dụng với chất lượng cao hơn. Mua trong ứng dụng sẽ chịu 30% phí hoa hồng, nhưng nếu người dùng đã đăng ký một năm trở lên hoặc ứng dụng có doanh thu hàng năm dưới 1 triệu USD, phí hoa hồng phải chịu là 15%.

Trong bài viết này, chúng tôi sẽ giải thích cách:  

  • Tạo một sản phẩm trong Google Play Console;
  • Cấu hình gói đăng ký: cách chỉ định thời gian, giá cả và dùng thử; 
  • Thêm danh sách sản phẩm trong một ứng dụng. 

Tạo gói đăng ký

Trước khi chúng ta bắt đầu, đảm bảo rằng bạn:

  1. tài khoản nhà phát triển trên Google Play.
  2. Đã ký tất cả thoả thuận và sẵn sàng bắt đầu làm việc.  

Bây giờ, hãy bắt tay vào việc và tạo sản phẩm đầu tiên của chúng ta.  

Đổi sang tài khoản nhà phát triển của bạn và chọn ứng dụng.

60fef7a69f036b3bfc996258 jsaoezesquwst5gknnib6kc3xqckd8yldedvgpfkaagrqbg9xksbckpwo g7ypda7kelwc zzkmwkt2powjgjwok3yphfxvmaquf6fhiqrqtyz06nuu5yfdyta502n1saakxczfz 6

Sau đó, trong menu bên trái, tìm phần Sản phẩm, chọn Subscriptions  và nhấp vào Create a Subscription

60fef7a7b3b5c7c37e286978 qy go34alibtdmnfdkaii3wg1fcjv5bexstyoefeazq 2oagsnx37gndqykcx6rcwcyu7ahpfzug gusku2bg0sxtq nkmglkw5uuvlabnnlnmp7j2ljfc9 nkzhyhbhegp1nl8r 6

Sau đó, chúng ta sẽ thấy bộ cấu hình gói đăng ký. Sau đây là một số điểm quan trọng.

60fef7a7d069f50dac0202a5 edgfvm0dhtqew hin9r1uqwvyzuzna1nw4auk hsl8kvgoc2uev8ulj5yhinnvdwv6xau1c1kzy2vd0fmpc8ezyurr3vtl 1qbj19ewbfwqspewed3d echiz1gjqfrfpeiwm0yb 6
  1. Tạo ID sẽ được sử dụng trong ứng dụng. Nên thêm kỳ đăng ký hoặc thông tin hữu dụng khác vào ID, nhờ đó, bạn có thể tạo ra sản phẩm theo một phong cách và làm cho việc phân tích thống kê bán hàng trở nên dễ dàng hơn.
  2. Tên gói đăng ký mà người dùng sẽ nhìn thấy trong cửa hàng.  
  3. Mô tả gói đăng ký. Người dùng cũng sẽ thấy thông tin này.  
60fef7a755430242d074b04c 5mix72nt8rn7nbjrkxd7l7pfit6yuebw9f8x0bke47mx1pbvvagpf4uar w dlkpkkfl9q7udzodqctd451avnwnvqccvtmz0wwj90f8md8mbmrl464wo ljps2wriusg8ax8p1c 6

Cuộn xuống và chọn kỳ đăng ký. Trong trường hợp của chúng ta – khoảng thời gian này là một tuần. Đặt mức giá.  

60fef7a8d11d44258c894e7d 658mke hovj itrvd6mamegcdp6wvefdt0bdamvvssnwg0cca hwzr7wcgvm9sxf7f6a5iipl2xuaxm9hwznynmah1ad4n7ttsl4uxjpt29aimx2swzisoplqaxcj5avpepiocum 6

Thông thường, bạn đặt mức giá bằng đơn vị tiền tệ của tài khoản cơ bản và hệ thống sẽ tự động đổi giá. Nhưng bạn cũng có thể chỉnh sửa giá cho một quốc gia cụ thể theo cách thủ công.  

Vui lòng chú ý rằng Google hiển thị thuế cho mọi quốc gia. Điều này thật tuyệt vời, App Store Connect không có chức năng này.  

60fef7a7ebfe90328d0a6a28 3k q7t5g7luv4axvw25thwlj3aibgsnnkahdtfeleaba31hqwgvo1yin2qayyh1r97tp4p nyvavypbvrt2 9caldi51tr15xhj4tamhq6kuluec5gikmuya1irmfzibqu79mfj1 6

Cuộn xuống và chọn (nếu cần): 

  1. Thời gian dùng thử miễn phí.
  2. Giá ưu đãi mặt hàng mới, là một loại ưu đãi cho kỳ thanh toán đầu tiên.  
  3. Thời gian gia hạn. Nếu người dùng gặp vấn đề với thanh toán, bạn vẫn có thể cấp cho họ quyền truy cập cao cấp cho một số ngày.
  4. Một cơ hội để đăng ký lại từ Play Store, không phải từ ứng dụng, sau khi hủy. 

So sánh quy trình mua trong Play Console và App Store Connect

Tuy kiếm tiền bằng gói đăng ký trên iOS hiệu quả hơn nhưng bảng quản trị của Play Console lại tiện lợi hơn, được sắp xếp và địa phương hóa tốt hơn và hoạt động nhanh hơn.

Quy trình tạo sản phẩm được thiết kế đơn giản nhất có thể. Chúng tôi chia sẻ về cách tạo sản phẩm trên iOS tại đây.

Lấy danh sách sản phẩm trong một ứng dụng

Sau khi đã tạo sản phẩm, hãy tiếp tục tạo cấu trúc chấp nhận và xử lý giao dịch mua. Nhìn chung, quy trình như sau:   

  1. Thêm Billing Library.
  2. Phát triển một lớp để tương tác với sản phẩm trên Google Play.
  3. Triển khai tất cả phương thức để xử lý mua.  
  4. Thêm xác nhận máy chủ của đơn hàng.  
  5. Thu thập phân tích.

Trong phần này, hãy cùng xem xét kỹ hơn hai điểm đầu tiên.  

Thêm Billing Library vào một dự án:

implementation "com.android.billingclient:billing:4.0.0"

Tại thời điểm viết bài, phiên bản mới nhất là 4.0.0. Bạn có thể thay bằng bất kỳ phiên bản nào khác ở bất kỳ thời điểm nào.  

Hãy tạo một lớp wrapper về logic tương tác với Google Play và khởi chạy BillingClient từ Billing Library trong lớp đó. Hãy gọi lớp này là BillingClientWrapper.

Lớp này sẽ triển khai giao diện PurchasesUpdatedListener. Bây giờ, chúng ta sẽ ghi đè một phương thức cho nó – onPurchasesUpdated(billingResult: BillingResult, purchaseList: MutableList<Purchase>?) – phương thức này cần thiết ngay sau khi giao dịch mua được thực hiện nhưng chúng ta sẽ mô tả quy trình triển khai trong bài viết tiếp theo. 

import android.content.Context import com.android.billingclient.api.*  class BillingClientWrapper(context: Context) : PurchasesUpdatedListener {     private val billingClient = BillingClient        .newBuilder(context)        .enablePendingPurchases()        .setListener(this)        .build()     override fun onPurchasesUpdated(billingResult: BillingResult, purchaseList: MutableList<Purchase>?) {        // here come callbacks about new purchases    } }

Google khuyến nghị không nên có nhiều hơn một kết nối đang hoạt động giữa BillingClient và Google Play để tránh thực thi nhiều lần hàm callback về giao dịch mua được thực hiện. Theo đó, bạn chỉ nên có một BillingClient duy nhất trong một lớp singleton. Lớp trong ví dụ không phải là một singleton nhưng chúng ta có thể dùng dependency injection (ví dụ: với sự trợ giúp của Dagger hoặc Koin) theo cách này, cho phép chỉ một trường hợp được tồn tại ở một thời điểm đơn nhất. 

Để đưa ra bất kỳ yêu cầu nào với Billing Library, BillingClient phải có một kết nối đang hoạt động với Google Play tại thời điểm khi yêu cầu được đưa ra, nhưng kết nối đôi khi cũng có thể bị ngắt. Để tiện dụng, hãy viết một wrapper cho phép chúng ta tạo bất kỳ yêu cầu nào nhưng chỉ khi kết nối đang hoạt động.   

Để lấy sản phẩm, chúng ta cần ID mà chúng ta đã cài đặt trong thị trường. Nhưng như vậy vẫn chưa đủ cho một yêu cầu, chúng ta cũng cần loại sản phẩm (gói đăng ký hoặc mua một lần) đó là lý do tại sao chúng ta có thể lấy danh sách sản phẩm tổng hợp bằng cách “kết hợp” kết quả của hai yêu cầu.

Yêu cầu đối với các sản phẩm không đồng bộ nên chúng ta cần một hàm callback để cung cấp danh sách sản phẩm hoặc trả về mô hình lỗi. Khi xảy ra lỗi, Billing Library trả về một trong các mã BillingResponseCodes cũng như debugMessage. Hãy tạo giao diện callback và mô hình cho lỗi:  

interface OnQueryProductsListener {   fun onSuccess(products: List < SkuDetails > )   fun onFailure(error: Error) }  class Error(val responseCode: Int, val debugMessage: String)

Đây là mã cho phương thức riêng tư để lấy dữ liệu về một loại sản phẩm cụ thể và phương thức công khai sẽ “kết hợp” kết quả của hai yêu cầu và cung cấp cho người dùng danh sách sản phẩm cuối cùng hoặc hiển thị thông báo lỗi.

fun queryProducts(listener: OnQueryProductsListener) {    val skusList = listOf("premium_sub_month", "premium_sub_year", "some_inapp")     queryProductsForType(        skusList,        BillingClient.SkuType.SUBS    ) { billingResult, skuDetailsList ->        if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) {            val products = skuDetailsList ?: mutableListOf()            queryProductsForType(                skusList,                BillingClient.SkuType.INAPP            ) { billingResult, skuDetailsList ->                if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) {                    products.addAll(skuDetailsList ?: listOf())                    listener.onSuccess(products)                } else {                    listener.onFailure(                        Error(billingResult.responseCode, billingResult.debugMessage)                    )                }            }        } else {            listener.onFailure(                Error(billingResult.responseCode, billingResult.debugMessage)            )        }    } }  private fun queryProductsForType(    skusList: List<String>,    @BillingClient.SkuType type: String,    listener: SkuDetailsResponseListener ) {    onConnected {        billingClient.querySkuDetailsAsync(            SkuDetailsParams.newBuilder().setSkusList(skusList).setType(type).build(),            listener        )    } }

Subscribe to Adapty newsletter

Get fresh paywall ideas, subscription insights, and mobile app news every month!

Nhờ đó, chúng ta có thông tin giá trị về sản phẩm (SkuDetails) chứa tên, giá, loại sản phẩm đã địa phương hóa cũng như kỳ thanh toán và thông tin về giá ưu đãi mặt hàng mới và thời gian dùng thử (nếu có cho người dùng này) của gói đăng ký. Lớp cuối cùng có dạng như sau:   

import android.content.Context import com.android.billingclient.api.* class BillingClientWrapper(context: Context) : PurchasesUpdatedListener {    interface OnQueryProductsListener {        fun onSuccess(products: List<SkuDetails>)        fun onFailure(error: Error)    }    class Error(val responseCode: Int, val debugMessage: String)    private val billingClient = BillingClient        .newBuilder(context)        .enablePendingPurchases()        .setListener(this)        .build()    fun queryProducts(listener: OnQueryProductsListener) {        val skusList = listOf("premium_sub_month", "premium_sub_year", "some_inapp")        queryProductsForType(            skusList,            BillingClient.SkuType.SUBS        ) { billingResult, skuDetailsList ->            if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) {                val products = skuDetailsList ?: mutableListOf()                queryProductsForType(                    skusList,                    BillingClient.SkuType.INAPP                ) { billingResult, skuDetailsList ->                    if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) {                        products.addAll(skuDetailsList ?: listOf())                        listener.onSuccess(products)                    } else {                        listener.onFailure(                            Error(billingResult.responseCode, billingResult.debugMessage)                        )                    }                }            } else {                listener.onFailure(                    Error(billingResult.responseCode, billingResult.debugMessage)                )            }        }    }    private fun queryProductsForType(        skusList: List<String>,        @BillingClient.SkuType type: String,        listener: SkuDetailsResponseListener    ) {        onConnected {            billingClient.querySkuDetailsAsync(                SkuDetailsParams.newBuilder().setSkusList(skusList).setType(type).build(),                listener            )        }    }    private fun onConnected(block: () -> Unit) {        billingClient.startConnection(object : BillingClientStateListener {            override fun onBillingSetupFinished(billingResult: BillingResult) {                block()            }            override fun onBillingServiceDisconnected() {                // Try to restart the connection on the next request to                // Google Play by calling the startConnection() method.            }        })    }    override fun onPurchasesUpdated(billingResult: BillingResult, purchaseList: MutableList<Purchase>?) {        // here come callbacks about new purchases    } }

Bài viết cho hôm nay đã hết. Trong bài viết tiếp theo, chúng tôi sẽ cho bạn biết về cách triển khai mua hàng, thử nghiệm và xử lý lỗi.