1. معرفی
سیستم صورتحساب Google Play سرویسی است که به شما امکان میدهد محصولات و محتوای دیجیتال را در برنامه Android خود بفروشید. این مستقیم ترین راه برای فروش محصولات درون برنامه ای برای کسب درآمد از برنامه شما است. این لبه کد به شما نشان میدهد که چگونه از کتابخانه صورتحساب Google Play برای فروش اشتراکها در پروژهتان استفاده کنید، بهگونهای که هنگام ادغام خریدها با بقیه برنامهتان، جزئیات دقیق را در بر میگیرد.
همچنین مفاهیم مرتبط با اشتراک مانند طرح های پایه، پیشنهادات، برچسب ها و طرح های پیش پرداخت را معرفی می کند. برای کسب اطلاعات بیشتر درباره اشتراکها در صورتحساب Google Play، میتوانید مرکز راهنمای ما را بررسی کنید.
چیزی که خواهی ساخت
در این کد لبه، میخواهید آخرین کتابخانه صورتحساب (نسخه 5.0.0) را به یک برنامه نمایه مبتنی بر اشتراک ساده اضافه کنید. این برنامه قبلاً برای شما ساخته شده است، بنابراین فقط بخش صورتحساب را اضافه میکنید. همانطور که در شکل 1 نشان داده شده است، در این برنامه کاربر برای هر یک از طرح های پایه و/یا پیشنهادات ارائه شده از طریق دو محصول اشتراک تجدیدپذیر (پایه و حق بیمه) یا برای یک پیش پرداخت غیر قابل تمدید ثبت نام می کند. همین. طرح های پایه به ترتیب اشتراک ماهانه و سالانه هستند. کاربر میتواند اشتراک پیشپرداختشده را ارتقا، کاهش یا تبدیل به یک اشتراک تجدیدپذیر کند.
برای گنجاندن کتابخانه صورتحساب Google Play در برنامه خود، موارد زیر را ایجاد میکنید:
-
BillingClientWrapper
- یک بسته بندی برای کتابخانه BillingClient. این برنامه قصد دارد تعاملات را با BillingClient کتابخانه صورتحساب Play محصور کند، اما در یکپارچهسازی خودتان لازم نیست. -
SubscriptionDataRepository
- یک مخزن صورتحساب برای برنامه شما که حاوی لیستی از موجودی محصولات اشتراک برنامه (یعنی آنچه برای فروش است) و لیستی از متغیرهای ShareFlow که به جمع آوری وضعیت خریدها و جزئیات محصول کمک می کند. -
MainViewModel
- یک ViewModel که از طریق آن بقیه برنامه شما با مخزن صورتحساب ارتباط برقرار می کند. به راه اندازی جریان صورتحساب در UI با استفاده از روش های مختلف خرید کمک می کند.
پس از اتمام، معماری برنامه شما باید شبیه شکل زیر باشد:
چیزی که یاد خواهید گرفت
- نحوه ادغام کتابخانه صورتحساب Play
- نحوه ایجاد محصولات اشتراک، طرح های پایه، پیشنهادات و برچسب ها از طریق کنسول Play
- نحوه بازیابی طرح ها و پیشنهادات پایه موجود از برنامه
- نحوه راه اندازی جریان صورتحساب با پارامترهای مناسب
- نحوه ارائه محصولات اشتراک پیش پرداخت
این کد لبه روی صورتحساب Google Play متمرکز است. مفاهیم غیر مرتبط و بلوکهای کد محو شدهاند و برای شما ارائه میشوند تا به سادگی کپی و جایگذاری کنید.
آنچه شما نیاز دارید
- نسخه اخیر Android Studio (>= Arctic Fox | 2020.3.1)
- دستگاه Android با Android 8.0 یا بالاتر
- کد نمونه که در GitHub برای شما ارائه شده است (دستورالعمل در بخش های بعدی)
- دانش متوسط از توسعه اندروید در اندروید استودیو
- آشنایی با نحوه انتشار یک برنامه در فروشگاه Google Play
- تجربه متوسط در نوشتن کد کاتلین
- کتابخانه صورتحساب Google Play نسخه 5.0.0
2. راه اندازی
کد را از Github دریافت کنید
ما هر آنچه برای این پروژه نیاز دارید را در یک مخزن Git قرار داده ایم. برای شروع، باید کد را بگیرید و آن را در محیط برنامه نویس مورد علاقه خود باز کنید. برای این کد لبه، استفاده از Android Studio را توصیه می کنیم.
کد شروع در یک مخزن GitHub ذخیره می شود. از طریق دستور زیر می توانید مخزن را کلون کنید:
git clone https://github.com/android/play-billing-samples.git cd PlayBillingCodelab
3. مقدمات
نقطه شروع ما چیست؟
نقطه شروع ما یک برنامه نمایه کاربر اصلی است که برای این کد لبه طراحی شده است. کد برای نشان دادن مفاهیمی که می خواهیم نشان دهیم ساده شده است و برای استفاده در تولید طراحی نشده است. اگر تصمیم به استفاده مجدد از هر بخشی از این کد در یک برنامه تولیدی دارید، مطمئن شوید که بهترین شیوه ها را دنبال کرده و تمام کدهای خود را به طور کامل آزمایش کنید.
پروژه را به اندروید استودیو وارد کنید
PlayBillingCodelab برنامه پایهای است که شامل اجرای صورتحساب Google Play نیست. Android Studio را راه اندازی کنید و با انتخاب Open > billing/PlayBillingCodelab
billing-codelab را وارد کنید
این پروژه دارای دو ماژول است:
- start دارای برنامه اسکلت است اما فاقد وابستگی های مورد نیاز و تمام روش هایی است که شما باید پیاده سازی کنید.
- تمام شده دارای پروژه تکمیل شده است و می تواند به عنوان یک راهنما در زمانی که شما گیر کرده اید خدمت کند.
این برنامه از هشت فایل کلاس تشکیل شده است: BillingClientWrapper
، SubscriptionDataRepository
، Composables
، MainState
، MainViewModel
، MainViewModelFactory
، و MainActivity
.
- BillingClientWrapper پوششی است که روشهای صورتحساب Google Play [BillingClient] را که برای پیادهسازی ساده لازم است جدا میکند و پاسخهایی را برای پردازش به مخزن داده ارسال میکند.
- SubscriptionDataRepository برای انتزاع منبع داده Google Play Billing (به عنوان مثال کتابخانه Billing Client) استفاده می شود و داده StateFlow منتشر شده در BillingClientWrapper را به جریان تبدیل می کند.
- ButtonModel یک کلاس داده است که برای ساخت دکمه ها در UI استفاده می شود.
- Composables تمام متدهای کامپوزیشن UI را در یک کلاس استخراج می کند.
- MainState یک کلاس داده برای مدیریت حالت است.
- MainViewModel برای نگهداری دادههای مربوط به صورتحساب و وضعیتهای مورد استفاده در رابط کاربری استفاده میشود. تمام جریانهای موجود در SubscriptionDataRepository را در یک شیء حالت ترکیب میکند.
- MainActivity کلاس فعالیت اصلی است که Composables را برای رابط کاربری بارگیری می کند.
- ثابت ها جسمی است که دارای ثابت های مورد استفاده توسط چندین کلاس است.
گریدل
برای اضافه کردن صورتحساب Google Play به برنامه خود، باید یک وابستگی Gradle اضافه کنید. فایل build.gradle ماژول برنامه را باز کنید و موارد زیر را اضافه کنید:
dependencies { val billing_version = "5.0.0" implementation("com.android.billingclient:billing:$billing_version") }
کنسول Google Play
برای اهداف این کد لبه، باید دو پیشنهاد محصول اشتراکی زیر را در بخش اشتراکهای کنسول Google Play ایجاد کنید:
- 1 اشتراک اولیه با شناسه محصول
up_basic_sub
محصول باید دارای 3 طرح پایه (2 طرح تمدید خودکار و 1 پیش پرداخت) با برچسب های مرتبط باشد: 1 اشتراک پایه ماهانه با برچسب monthlybasic
، 1 اشتراک پایه سالانه با برچسب yearlybasic
و 1 اشتراک پیش پرداخت با برچسب prepaidbasic
می توانید پیشنهادات را به طرح های پایه اضافه کنید. پیشنهادها برچسب ها را از طرح های پایه مرتبط خود به ارث می برند.
- 1 اشتراک ممتاز با شناسه محصول
up_premium_sub
محصول باید دارای 3 طرح پایه (2 طرح تمدید خودکار و 1 پیش پرداخت) با برچسب های مرتبط باشد: 1 اشتراک پایه ماهانه با برچسب monthlypremium
، 1 اشتراک پایه سالانه با برچسب yearlypremium
و 1 اشتراک پیش پرداخت با برچسب prepaidpremium
می توانید پیشنهادات را به طرح های پایه اضافه کنید. پیشنهادها برچسب ها را از طرح های پایه مرتبط خود به ارث می برند.
برای اطلاعات بیشتر درباره نحوه ایجاد محصولات اشتراک، طرحهای پایه، پیشنهادات و برچسبها، لطفاً به مرکز راهنمای Google Play مراجعه کنید.
4. مشتری صورتحساب راه اندازی شد
برای این بخش، شما در کلاس BillingClientWrapper
کار خواهید کرد.
در پایان، شما همه چیز مورد نیاز برای نمونه سازی مشتری صورتحساب و همه روش های مرتبط را خواهید داشت.
- یک BillingClient را راه اندازی کنید
هنگامی که یک وابستگی به کتابخانه صورتحساب Google Play اضافه کردیم، باید یک نمونه BillingClient
مقداردهی اولیه کنیم.
BillingClientWrapper.kt
private val billingClient = BillingClient.newBuilder(context)
.setListener(this)
.enablePendingPurchases()
.build()
- با Google Play ارتباط برقرار کنید
پس از ایجاد BillingClient، باید با Google Play ارتباط برقرار کنیم.
برای اتصال به Google Play، startConnection()
را فراخوانی می کنیم. فرآیند اتصال ناهمزمان است و ما باید یک BillingClientStateListener
را پیاده سازی کنیم تا پس از تکمیل راه اندازی کلاینت و آماده شدن برای درخواست های بیشتر، یک تماس پاسخ دریافت کنیم.
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)
}
})
}
- برای خریدهای موجود، صورتحساب Google Play را پرس و جو کنید
پس از برقراری ارتباط با Google Play، آماده هستیم تا خریدهایی را که کاربر قبلاً با فراخوانی 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)
}
}
}
- نمایش محصولات موجود برای خرید
اکنون می توانیم محصولات موجود را پرس و جو کرده و به کاربران نمایش دهیم. برای پرس و جو از Google Play برای جزئیات محصول اشتراک، queryProductDetailsAsync()
را فراخوانی می کنیم. پرس و جو برای جزئیات محصول یک مرحله مهم قبل از نمایش محصولات به کاربران است، زیرا اطلاعات بومی سازی شده محصول را برمی گرداند.
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)
}
}
}
- شنونده را برای عبارت ProductDetails تنظیم کنید
توجه: این روش نتیجه پرس و جو را در Map به _productWithProductDetails
ارسال می کند.
همچنین توجه داشته باشید که پرس و جو انتظار می رود ProductDetails
را برگرداند. اگر این اتفاق نیفتد، مشکل به احتمال زیاد این است که محصولات راهاندازیشده در کنسول Play فعال نشدهاند یا ساختی با وابستگی به مشتری صورتحساب در هیچ یک از آهنگهای انتشار منتشر نکردهاید.
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")
}
}
}
- جریان خرید را راه اندازی کنید
launchBillingFlow
روشی است که وقتی کاربر برای خرید یک مورد کلیک می کند، فراخوانی می شود. از Google Play میخواهد که جریان خرید را با ProductDetails
محصول آغاز کند.
BillingClientWrapper.kt
fun launchBillingFlow(activity: Activity, params: BillingFlowParams) {
if (!billingClient.isReady) {
Log.e(TAG, "launchBillingFlow: BillingClient is not ready")
}
billingClient.launchBillingFlow(activity, params)
}
- شنونده را برای نتیجه عملیات خرید تنظیم کنید
وقتی کاربر از صفحه خرید Google Play خارج میشود (یا با ضربه زدن روی دکمه «خرید» برای تکمیل خرید، یا با ضربه زدن روی دکمه برگشت برای لغو خرید)، پاسخ تماس onPurchaseUpdated()
نتیجه جریان خرید را به شما ارسال میکند. برنامه بر اساس BillingResult.responseCode
، سپس می توانید تعیین کنید که آیا کاربر محصول را با موفقیت خریداری کرده است یا خیر. اگر responseCode == OK
، به این معنی است که خرید با موفقیت انجام شده است.
onPurchaseUpdated()
لیستی از اشیاء Purchase
را ارسال می کند که شامل تمام خریدهایی است که کاربر از طریق برنامه انجام داده است. در میان بسیاری از فیلدهای دیگر، هر شی خرید شامل شناسه محصول، buyToken و ویژگی های isAcknowledged
است. با استفاده از این فیلدها، برای هر شی Purchase
میتوانید تعیین کنید که آیا خرید جدیدی است که باید پردازش شود یا خریدی موجود است که نیازی به پردازش بیشتر ندارد.
برای خرید اشتراک، پردازش شبیه به تایید خرید جدید است.
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.
}
}
- پردازش خریدها (تأیید و تأیید خریدها)
هنگامی که کاربر یک خرید را تکمیل کرد، برنامه باید آن خرید را با تأیید آن پردازش کند.
علاوه بر این، وقتی تأیید با موفقیت پردازش شد، مقدار _isNewPurchaseAcknowledged
روی true تنظیم می شود.
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
}
}
}
}
}
- اتصال صورتحساب را قطع کنید
در نهایت، زمانی که یک فعالیت از بین میرود، میخواهید اتصال به Google Play را قطع کنید، بنابراین endConnection()
برای انجام این کار فراخوانی میشود.
BillingClientWrapper.kt
fun terminateBillingConnection() {
Log.i(TAG, "Terminating connection")
billingClient.endConnection()
}
5. مخزن داده های اشتراک
در BillingClientWrapper
، پاسخهای QueryPurchasesAsync
و QueryProductDetails
به ترتیب برای MutableStateFlow
_purchases
و _productWithProductDetails
پست میشوند که خارج از کلاس با خریدها و productWithProductDetails
نمایش داده میشوند.
در SubscriptionDataRepository
، خریدها بر اساس محصول خرید برگشتی به سه جریان پردازش میشوند: hasRenewableBasic
، hasPrepaidBasic
، hasRenewablePremium
، و hasPremiumPrepaid
.
بهعلاوه، productWithProductDetails
به basicProductDetails
و premiumProductDetails
Flow پردازش میشود.
6. MainViewModel
قسمت سخت انجام شده است. اکنون میخواهید MainViewModel
را تعریف کنید، که فقط یک رابط عمومی برای مشتریان شماست تا آنها مجبور نباشند اطلاعات داخلی BillingClientWrapper
و SubscriptionDataRepository
را بدانند.
ابتدا در MainViewModel
، هنگامی که viewModel مقداردهی اولیه می شود، اتصال صورتحساب را شروع می کنیم.
MainViewModel.kt
init {
billingClient.startBillingConnection(billingConnectionState = _billingConnectionState)
}
سپس، جریانهای مخزن به ترتیب در productsForSaleFlows
(برای محصولات موجود) و userCurrentSubscriptionFlow
(برای اشتراک فعلی و فعال کاربر) که در کلاس repo پردازش میشوند، ترکیب میشوند.
لیست خریدهای فعلی نیز با 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
ترکیبی در یک بلوک init جمعآوری میشود و مقدار به یک شی MutableLiveData به نام _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
همچنین چند روش بسیار مفید را اضافه می کند:
- طرح های پایه و بازیابی توکن های پیشنهادی
با شروع نسخه 5.0.0 کتابخانه صورتحساب Play، همه محصولات اشتراک میتوانند چندین طرح و پیشنهاد پایه داشته باشند به جز طرحهای پایه پیشپرداختی که نمیتوانند پیشنهاد داشته باشند.
این روش به بازیابی همه پیشنهادات و طرحهای پایه که یک کاربر واجد شرایط آن است، با استفاده از مفهوم جدید معرفیشده برچسبها که برای گروهبندی پیشنهادات مرتبط استفاده میشوند، کمک میکند.
به عنوان مثال، هنگامی که کاربری سعی می کند یک اشتراک ماهانه اولیه بخرد، همه طرح ها و پیشنهادات پایه مرتبط با محصول اشتراک پایه ماهانه با رشته 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
}
- محاسبه پیشنهاد کمترین قیمت
هنگامی که یک کاربر واجد شرایط برای چندین پیشنهاد است، از متد leastPricedOfferToken()
برای محاسبه کمترین پیشنهاد در میان پیشنهادهایی که توسط retrieveEligibleOffers()
برگردانده شده است استفاده می شود.
این روش کد شناسه پیشنهاد پیشنهاد انتخابی را برمی گرداند.
این پیادهسازی به سادگی کمترین قیمت را از نظر مجموعه pricingPhases
برمیگرداند و میانگینها را در نظر نمیگیرد.
پیاده سازی دیگر می تواند به جای آن به دنبال پایین ترین قیمت متوسط باشد.
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
}
- سازندگان BillingFlowParams
برای راه اندازی جریان خرید برای یک محصول خاص، ProductDetails
محصول و توکن پیشنهاد انتخابی باید تنظیم و برای ساخت BilingFlowParams استفاده شود.
دو روش برای کمک به این امر وجود دارد:
upDowngradeBillingFlowParamsBuilder()
پارامترها را برای ارتقا و تنزل میسازد.
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()
پارامترها را برای خریدهای معمولی می سازد.
MainViewModel.kt
private fun billingFlowParamsBuilder(
productDetails: ProductDetails,
offerToken: String
): BillingFlowParams.Builder {
return BillingFlowParams.newBuilder().setProductDetailsParamsList(
listOf(
BillingFlowParams.ProductDetailsParams.newBuilder()
.setProductDetails(productDetails)
.setOfferToken(offerToken)
.build()
)
)
}
- روش خرید
روش خرید از launchBillingFlow()
BillingClientWrapper
و BillingFlowParams
برای راه اندازی خرید استفاده می کند.
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.")
}
}
- اتصال صورتحساب را قطع کنید
در نهایت، روش terminateBillingConnection
BillingClientWrapper
در onCleared()
ViewModel فراخوانی می شود.
این برای پایان دادن به اتصال فعلی صورتحساب زمانی است که فعالیت مرتبط از بین میرود.
7. UI
اکنون، زمان استفاده از همه چیزهایی است که در UI ساخته اید. برای کمک به آن، با کلاس های Composables و MainActivity کار خواهید کرد.
Composables.kt
کلاس Composables به طور کامل ارائه شده است و تمام توابع Compose مورد استفاده برای رندر UI و مکانیسم ناوبری بین آنها را تعریف می کند.
عملکرد Subscriptions
دو دکمه را نشان می دهد: Basic Subscription
و Premium Subscription
.
Basic Subscription
و Premium Subscription
هر کدام روش های جدید Compose را بارگیری می کنند که سه طرح پایه مربوطه را نشان می دهد: ماهانه، سالانه، و پیش پرداخت.
سپس، سه عملکرد ممکن برای نوشتن نمایه وجود دارد که هر کدام برای یک اشتراک خاص ممکن است یک کاربر داشته باشد: یک Basic قابل تجدید، یک Premium قابل تجدید، و یک نمایه Prepaid Basic یا Prepaid Premium.
- هنگامی که یک کاربر اشتراک پایه دارد، نمایه اصلی به او اجازه میدهد تا اشتراک حق بیمه ماهانه، سالانه یا پیشپرداخت را ارتقا دهد.
- برعکس، وقتی یک کاربر اشتراک ممتاز دارد، میتواند به اشتراک اولیه ماهانه، سالانه یا پیشپرداخت تنزل دهد.
- هنگامی که یک کاربر اشتراک پیش پرداخت دارد، می تواند اشتراک خود را با دکمه بالا بردن شارژ کند یا اشتراک پیش پرداخت خود را به یک طرح پایه تمدید خودکار مربوطه تبدیل کند.
در نهایت، یک عملکرد صفحه نمایش بارگیری وجود دارد که هنگام اتصال به Google Play و زمانی که نمایه کاربر در حال ارائه است استفاده می شود.
MainActivity.kt
- هنگامی که
MainActivity
ایجاد می شود، viewModel نمونه سازی می شود و یک تابع نوشتن به نامMainNavHost
بارگیری می شود. -
MainNavHost
با متغیرisBillingConnected
شروع می شود که از viewModel'sbillingConnectionSate
Livedata ایجاد شده و برای تغییرات مشاهده می شود زیرا وقتی vieModel نمونه سازی می شود،billingConnectionSate
به روش startBillingConnectionBillingClientWrapper
منتقل می کند.
isBillingConnected
در صورت برقراری ارتباط روی true و زمانی که اتصال برقرار نمی شود false تنظیم می شود.
در صورت false، تابع نوشتن LoadingScreen()
بارگیری می شود و در صورت true، توابع Subscription
یا نمایه بارگذاری می شوند.
val isBillingConnected by viewModel.billingConnectionState.observeAsState()
- هنگامی که یک اتصال صورتحساب برقرار می شود:
Compose navController
نمونه سازی شده است
val navController = rememberNavController()
سپس، جریان ها در MainViewModel
جمع آوری می شوند.
val productsForSale by viewModel.productsForSaleFlows.collectAsState(
initial = MainState()
)
val currentPurchases by viewModel.currentPurchasesFlow.collectAsState(
initial = listOf()
)
در نهایت، متغیر destinationScreen
LiveData viewModel مشاهده می شود.
بر اساس وضعیت اشتراک فعلی کاربر، تابع نوشتن مربوطه ارائه می شود.
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. کد راه حل
کد راه حل کامل را می توان در ماژول راه حل پیدا کرد.
9. تبریک می گویم
تبریک میگوییم، شما با موفقیت محصولات اشتراک Google Play Billing Library 5.0.0 را در یک برنامه بسیار ساده ادغام کردید!
برای یک نسخه مستند از یک برنامه پیچیده تر با راه اندازی سرور ایمن، نمونه رسمی را ببینید.