מכירת מינויים באפליקציה באמצעות ספריית החיובים ב-Play 5

1. מבוא

מערכת החיוב של Google Play היא שירות שמאפשר לכם למכור מוצרים דיגיטליים ותוכן דיגיטלי באפליקציה ל-Android. זו הדרך הישירה ביותר למכירת מוצרים מתוך האפליקציה כדי לייצר הכנסות מהאפליקציה. ה-Codelab הזה מראה איך להשתמש בספריית החיובים של Google Play כדי למכור מינויים בפרויקט באופן שכולל את הפרטים הפרטניים כשמשלבים רכישות עם שאר האפליקציה.

הוא כולל גם מושגים שקשורים למינויים, כמו מינויים בסיסיים, מבצעים, תגים ומינויים בתשלום מראש. מידע נוסף על מינויים בחיוב ב-Google Play זמין במרכז העזרה.

מה תפַתחו

ב-Codelab הזה, אתם מתכוונים להוסיף את ספריית החיובים העדכנית (גרסה 5.0.0) לאפליקציית פרופיל פשוטה שמבוססת על מינויים. האפליקציה כבר מותאמת לך, לכן צריך רק להוסיף את החלק של החיוב. כפי שמוצג בתרשים 1, באפליקציה הזו המשתמש נרשם לכל אחת מהמינויים הבסיסיים ו/או מהמבצעים שמוצעים דרך שני מוצרים עם מינוי מתחדש (בסיסי ופרימיום) או למינוי בתשלום מראש שלא ניתן לחידוש. זה הכול. המינויים הבסיסיים הם מינויים חודשיים ושנתיים בהתאמה. המשתמש יכול לשדרג, לשדרג לאחור או להמיר מינוי בתשלום מראש למינוי מתחדש.

d7dba51f800a6cc4.png 2220c15b849d2ead.png

כדי לשלב את ספריית החיובים של Google Play באפליקציה, צריך ליצור את הפריטים הבאים:

  • BillingClientWrapper – wrapper לספריית BillingClient. המזהה הזה כולל את האינטראקציות עם ה-BillingClient של ספריית החיובים ב-Play, אבל לא חובה לעשות זאת בשילוב שלכם.
  • SubscriptionDataRepository – מאגר חיובים לאפליקציה שמכיל רשימה של מלאי המוצרים של האפליקציה במינוי (כלומר מה זמין למכירה) ורשימת משתנים של ShareFlow שעוזרים לאסוף את מצב הרכישות ואת פרטי המוצרים
  • MainViewModel – ViewModel, שבאמצעותה שאר האפליקציה מתקשרת עם מאגר החיוב. הם עוזרים להתחיל את תהליך החיוב בממשק המשתמש באמצעות שיטות רכישה שונות.

בסיום, הארכיטקטורה של האפליקציה אמורה להיראות בערך כך:

c83bc759f32b0a63.png

מה תלמדו

  • איך לשלב את ספריית החיובים ב-Play
  • איך יוצרים מוצרים מסוג מינוי, מינויים בסיסיים, מבצעים ותגים דרך Play Console
  • איך מאחזרים מינויים בסיסיים ומבצעים זמינים מהאפליקציה
  • איך מתחילים את תהליך החיוב עם הפרמטרים המתאימים
  • איך להציע מוצרים עם מינוי בתשלום מראש

ה-Codelab הזה מתמקד בחיוב ב-Google Play. מונחים לא רלוונטיים ובלוקים של קוד עם הפרדה מודגשת, כך שאפשר פשוט להעתיק ולהדביק אותם.

מה צריך להכין

  • גרסה עדכנית של Android Studio (>= Arctic Fox | 2020.3.1)
  • מכשיר Android עם Android מגרסה 8.0 ואילך
  • הקוד לדוגמה שניתן לך ב-GitHub (הוראות מפורטות בהמשך)
  • ידע מסוים על פיתוח Android ב-Android Studio
  • ידע איך לפרסם אפליקציה בחנות Google Play
  • ניסיון ברמה בינונית בכתיבת קוד Kotlin
  • גרסה 5.0.0 של ספריית החיובים ב-Google Play

2. בתהליך ההגדרה

קבלת הקוד מ-GitHub

העתקנו את כל מה שצריך לפרויקט הזה למאגר של Git. כדי להתחיל, צריך להשיג את הקוד ולפתוח אותו בסביבת הפיתוח המועדפת עליכם. כדי להשתמש ב-Codelab הזה, מומלץ להשתמש ב-Android Studio.

הקוד כדי להתחיל מאוחסן במאגר ב-GitHub. אפשר לשכפל את המאגר באמצעות הפקודה הבאה:

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

3. הבסיס

מהי נקודת ההתחלה שלנו?

נקודת ההתחלה היא אפליקציה בסיסית של פרופיל משתמש שמיועדת ל-Codelab הזה. הקוד נעשה בצורה פשוטה יותר כדי להציג את המושגים שאנחנו רוצים להדגים, והוא לא תוכנן לשימוש בסביבת ייצור. אם תבחרו לעשות שימוש חוזר בחלק כלשהו מהקוד הזה באפליקציית ייצור, חשוב לפעול לפי השיטות המומלצות ולבדוק את כל הקוד באופן מלא.

ייבוא הפרויקט ל-Android Studio

PlayBillingCodelab היא אפליקציית הבסיס שלא כוללת הטמעת נתוני חיוב ב-Google Play. כדי להפעיל את Android Studio ולייבא את Billing-codelab, בוחרים באפשרות Open > billing/PlayBillingCodelab

לפרויקט יש שני מודולים:

  • ל-start יש את אפליקציית השלד, אבל אין בה את יחסי התלות הנדרשים ואת כל השיטות שצריך להטמיע.
  • Finished כולל את הפרויקט שהושלם ויכול לשמש כמדריך כאשר נתקעים.

האפליקציה מכילה שמונה קבצים של כיתות: BillingClientWrapper, SubscriptionDataRepository, Composables, MainState, MainViewModel, MainViewModelFactory ו-MainActivity.

  • BillingClientWrapper הוא wrapper שבידוד את השיטות [BillingClient] של החיוב ב-Google Play שנדרשות להטמעה פשוטה, ופולט תשובות למאגר הנתונים לצורך עיבוד.
  • SubscriptionDataRepository משמש להפשטה של מקור הנתונים לחיוב ב-Google Play (כלומר, ספריית הלקוח לחיוב) ולהמרה של נתוני StateFlow שנפלטו מ-BillingClientWrapper ל-Fflows.
  • ButtonModel הוא סיווג נתונים שמשמש לבניית לחצנים בממשק המשתמש.
  • כשמשתמשים בComposables, המערכת מחלצת את כל השיטות הקומפוזביליות של ממשק המשתמש לקטגוריה אחת.
  • MainState הוא סיווג נתונים לניהול מדינה.
  • MainViewModel משמש לשמירת נתונים שקשורים לחיוב והמצבים שבהם נעשה שימוש בממשק המשתמש. הוא משלבת את כל הזרימה ב-SubscriptionDataRepository לאובייקט מצב אחד.
  • MainActivity הוא סיווג הפעילות הראשי שטוען את התכנים הקומפוזביליים לממשק המשתמש.
  • קבועים הוא האובייקט שמכיל את הקבועים שמשמשים מספר מחלקות.

Gradle

כדי להוסיף לאפליקציה חיוב ב-Google Play, צריך להוסיף תלות ב-Gradle. פותחים את הקובץ build.gradle של מודול האפליקציה, ומוסיפים את הפרטים הבאים:

dependencies {
    val billing_version = "5.0.0"

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

המסוף של Google Play

למטרות ה-Codelab הזה, צריך ליצור את שני המוצרים הבאים למינויים בקטע 'מינויים' ב-Google Play Console:

  • מינוי בסיסי אחד עם מזהה המוצר up_basic_sub

המוצר צריך לכלול 3 מינויים בסיסיים (2 מינויים מתחדשים אוטומטית ומינוי אחד בתשלום מראש) עם תגים משויכים : מינוי בסיסי אחד בחודש עם התג monthlybasic, מינוי בסיסי אחד לשנה עם התג yearlybasic ומינוי אחד בתשלום מראש עם התג prepaidbasic

אפשר להוסיף מבצעים למינויים הבסיסיים. מבצעים יקבלו בירושה את התגים ממינויים בסיסיים המשויכים אליהם.

  • מינוי פרימיום אחד עם מזהה המוצר up_premium_sub

המוצר צריך לכלול 3 מינויים בסיסיים(2 שמתחדשים אוטומטית ומינוי אחד בתשלום מראש) עם תגים משויכים: מינוי בסיסי אחד בחודש עם התג monthlypremium, מינוי בסיסי אחד לשנה עם התג yearlypremium ומינוי אחד בתשלום מראש עם התג prepaidpremium

a9f6fd6e70e69fed.png

אפשר להוסיף מבצעים למינויים הבסיסיים. מבצעים יקבלו בירושה את התגים ממינויים בסיסיים שמשויכים אליהם.

במרכז העזרה של Google Play מפורט מידע מפורט יותר על יצירת מוצרים מסוג מינוי, מינויים בסיסיים, מבצעים ותגים.

4. הלקוח לחיוב שהגדיר

בקטע הזה, עליך לעבוד בכיתה BillingClientWrapper.

בסוף התהליך, יהיה לכם כל מה שנחוץ כדי ליצור מודל של לקוח החיוב, וגם את כל השיטות הקשורות.

  1. אתחול של לקוח חיוב

אחרי שהוספנו תלות בספריית החיובים ב-Google Play, עלינו לאתחל מכונה של BillingClient.

BillingClientWrapper.kt

private val billingClient = BillingClient.newBuilder(context)
   .setListener(this)
   .enablePendingPurchases()
   .build()
  1. יצירת חיבור ל-Google Play

אחרי שיצרתם חשבון BillingClient, עלינו ליצור חיבור ל-Google Play.

כדי להתחבר ל-Google Play, אנחנו קוראים לפונקציה startConnection(). תהליך החיבור הוא אסינכרוני, ועלינו להטמיע BillingClientStateListener כדי לקבל קריאה חוזרת (callback) כשהגדרת הלקוח תסתיים והוא יהיה מוכן לשליחת בקשות נוספות.

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. שאילתות על רכישות קיימות ב-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)
       }
   }
}
  1. הצגת מוצרים שזמינים לקנייה

עכשיו אנחנו יכולים לשלוח שאילתות לגבי מוצרים זמינים ולהציג אותם למשתמשים. כדי לשלוח שאילתה ל-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)
       }
   }
}
  1. הגדרת ה-Listener לשאילתת ProductDetails

הערה: השיטה הזו פולטת את תוצאת השאילתה במפה אל _productWithProductDetails.

חשוב גם לשים לב שהשאילתה צפויה להחזיר ProductDetails. אם זה לא יקרה, סביר להניח שהבעיה היא שהמוצרים שהוגדרו ב-Play Console לא הופעלו, או שלא פרסמת גרסת build שתלויה בלקוח לחיוב באחד ממסלולי ההפצה.

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. הפעלת תהליך הרכישה

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)

}
  1. הגדרת ה-listener לתוצאה של פעולת הרכישה

כשהמשתמש יוצא ממסך הרכישה ב-Google Play (על ידי הקשה על הלחצן 'קנייה' כדי להשלים את הרכישה, או הקשה על הלחצן 'הקודם' כדי לבטל את הרכישה), הקריאה החוזרת (callback) של onPurchaseUpdated() שולחת לאפליקציה שלכם את התוצאה של תהליך הרכישה. על סמך BillingResult.responseCode, אפשר לקבוע אם המשתמש רכש את המוצר. אם הערך שלו הוא responseCode == OK, סימן שהרכישה הושלמה בהצלחה.

onPurchaseUpdated() מחזיר רשימה של Purchase אובייקטים שכוללת את כל הרכישות שהמשתמש ביצע דרך האפליקציה. מבין השדות הרבים האחרים, כל אובייקט רכישה מכיל את המאפיינים של מזהה המוצר, purchaseToken ו-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.
   }
}
  1. עיבוד רכישות (אימות ואישור רכישות)

אחרי שמשתמש משלים רכישה, האפליקציה צריכה לאשר את הרכישה כדי לעבד את הרכישה.

בנוסף, הערך של _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
               }
           }
       }
   }
}
  1. סיום הקישור לחיוב

לבסוף, כאשר פעילות מושמדת, ברצונך לסיים את החיבור ל-Google Play, לכן נשלחה קריאה ל-endConnection() לעשות זאת.

BillingClientWrapper.kt

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

5. SubscriptionDataRepository

בBillingClientWrapper, התשובות של QueryPurchasesAsync ושל QueryProductDetails מתפרסמות בהתאמה ל-MutableStateFlow _purchases ול-_productWithProductDetails שנחשפות מחוץ לכיתה עם רכישות ו-productWithProductDetails.

ב-SubscriptionDataRepository, רכישות מעובדות לשלושה תהליכי עבודה על סמך המוצר מהרכישה שהוחזרה: hasRenewableBasic, hasPrepaidBasic, hasRenewablePremium ו-hasPremiumPrepaid.

בנוסף, productWithProductDetails מעובד בתהליכים תואמים של basicProductDetails ו-premiumProductDetails.

6. ה-MainViewModel

החלק הקשה הושלם. עכשיו אתם עומדים להגדיר את MainViewModel, שהוא רק ממשק ציבורי עבור הלקוחות שלכם, כך שהם לא צריכים להכיר את BillingClientWrapper ו-SubscriptionDataRepository.

בשלב הראשון ב-MainViewModel, מתחילים את תהליך הקישור לחיוב לאחר אתחול של viewModel.

MainViewModel.kt

init {
   billingClient.startBillingConnection(billingConnectionState = _billingConnectionState)
}

לאחר מכן, נתוני התהליכים מהמאגר משולבים בהתאמה אל productsForSaleFlows (למוצרים זמינים) ו-userCurrentSubscriptionFlow (למינוי הנוכחי והפעיל של המשתמש), כפי שיעובד במחלקה של המאגר.

גם רשימת הרכישות הנוכחיות זמינה לממשק המשתמש עם 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 יש גם כמה שיטות שימושיות מאוד:

  1. אחזור של תוכניות Base Plan ואסימוני מבצע

החל מגרסה 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
}
  1. חישוב המבצע עם המחיר הנמוך ביותר

כשמשתמש עומד בדרישות לקבלת כמה מבצעים, השיטה 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
}
  1. BillingFlowParams builders

כדי להפעיל את תהליך הרכישה של מוצר מסוים, המוצר s 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()
       )
   )
}
  1. שיטת הקנייה

שיטת הקנייה משתמשת ב-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.")
   }
}

d726a27add092140.png

  1. מסיימים את תהליך הקישור לחיוב

לבסוף, השיטה terminateBillingConnection של BillingClientWrapper נקראת onCleared() של ViewModel.

הפעולה הזו תגרום לסיום החיבור הנוכחי לחיוב כשהפעילות המשויכת מושמדת.

7. ממשק המשתמש

עכשיו הגיע הזמן להשתמש בכל מה שבניתם בממשק המשתמש. כדי לעזור בכך, כדאי לעבוד עם הכיתות קומפוזביליות ו-MainActivity.

Composables.kt

המחלקה של הרכיבים הקומפוזביליים סופקה במלואה ומגדירה את כל פונקציות הכתיבה שמשמשות לעיבוד ממשק המשתמש ואת מנגנון הניווט ביניהן.

בפונקציה Subscriptions מוצגים שני לחצנים: Basic Subscription ו-Premium Subscription.

Basic Subscription ו-Premium Subscription טוענים כל אחד משיטות הכתיבה החדשות, שבהן מוצגות שלוש המינויים הבסיסיים המתאימים: חודשית, שנתית ובתשלום מראש.

לאחר מכן, יש שלוש פעולות אפשריות של כתיבת פרופיל לכל מינוי ספציפי שעשוי להיות למשתמש: מינוי Basic (בסיסי) מתחדש, מינוי Premium מתחדש ופרופיל בסיסי בתשלום מראש או פרימיום בתשלום מראש.

  • כשיש למשתמש מינוי בסיסי, הוא מאפשר לו לשדרג למינוי חודשי, שנתי או מינוי Premium בתשלום מראש.
  • לעומת זאת, כשמשתמש יש מינוי Premium, הוא יכול לשדרג לאחור למינוי חודשי, שנתי או מינוי בסיסי בתשלום מראש.
  • כשיש למשתמש מינוי בתשלום מראש, הוא יכול לחדש את המינוי באמצעות לחצן הוספת הכסף, או להמיר את המינוי בתשלום מראש למינוי בסיסי תואם שמתחדש אוטומטית.

לבסוף, יש פונקציית מסך בטעינה שמשמשת כשמתבצע חיבור ל-Google Play וכשמתבצע עיבוד של פרופיל משתמש.

MainActivity.kt

  1. כאשר נוצר MainActivity, נוצר רכיב ViewModel והטעינה של פונקציית הרכבה שנקראת MainNavHost מתבצעת.
  2. MainNavHost מתחיל במשתנה isBillingConnected שנוצר מה-Livedata billingConnectionSate של viewModel, וזוהה בו שינויים כי כשה-vieModel נוצר, הוא מעביר את billingConnectionSate לשיטת startBillingConnection של BillingClientWrapper.

הערך של isBillingConnected מוגדר כ-True כשהחיבור נוצר ו-False אם לא.

אם הערך הוא False, פונקציית הכתיבה LoadingScreen() נטענת וכשהפונקציות Subscription או הפרופיל נטענות.

val isBillingConnected by viewModel.billingConnectionState.observeAsState()
  1. כשנוצר חיבור לחיוב:

נוצר מופע של navController הכתיבה

val navController = rememberNavController()

לאחר מכן נאספים הזרימה ב-MainViewModel.

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

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

לבסוף, הוא נבדק במשתנה LiveData destinationScreen של 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 בגרסה 5.0.0 באפליקציה פשוטה מאוד!

לקבלת גרסה מתועדת של אפליקציה מתוחכמת יותר עם הגדרת שרת מאובטחת, ראו הדוגמה הרשמית.

קריאה נוספת