প্লে বিলিং লাইব্রেরি 5 এর সাথে অ্যাপে সদস্যতা বিক্রি করুন

1। পরিচিতি

Google Play এর বিলিং সিস্টেম হল একটি পরিষেবা যা আপনাকে আপনার Android অ্যাপে ডিজিটাল পণ্য এবং সামগ্রী বিক্রি করতে দেয়৷ আপনার অ্যাপকে নগদীকরণ করতে অ্যাপ-মধ্যস্থ পণ্য বিক্রি করার এটি আপনার জন্য সবচেয়ে সরাসরি উপায়। এই কোডল্যাবটি আপনাকে দেখায় কিভাবে আপনার প্রোজেক্টে সাবস্ক্রিপশন বিক্রি করতে Google Play বিলিং লাইব্রেরি ব্যবহার করতে হয় যাতে আপনার অ্যাপের বাকি অংশের সাথে কেনাকাটাগুলিকে একীভূত করার সময় সূক্ষ্ম বিশদ বিবরণ এনক্যাপসুলেট করে।

এটি বেস প্ল্যান, অফার, ট্যাগ এবং প্রিপেইড প্ল্যানের মতো সাবস্ক্রিপশন সম্পর্কিত ধারণাগুলিও প্রবর্তন করে। Google Play বিলিং-এ সদস্যতা সম্পর্কে আরও জানতে, আপনি আমাদের সহায়তা কেন্দ্র দেখতে পারেন।

আপনি কি নির্মাণ করবেন

এই কোডল্যাবে, আপনি একটি সাধারণ সদস্যতা ভিত্তিক প্রোফাইল অ্যাপে সর্বশেষ বিলিং লাইব্রেরি (সংস্করণ 5.0.0) যোগ করতে যাচ্ছেন। অ্যাপটি ইতিমধ্যেই আপনার জন্য তৈরি করা হয়েছে, তাই আপনি শুধু বিলিং অংশ যোগ করবেন। চিত্র 1-এ দেখানো হয়েছে, এই অ্যাপটিতে ব্যবহারকারী দুটি পুনর্নবীকরণযোগ্য সাবস্ক্রিপশন পণ্য (বেসিক এবং প্রিমিয়াম) অথবা একটি অ-নবায়নযোগ্য প্রিপেইডের মাধ্যমে প্রদত্ত যে কোনো বেস প্ল্যান এবং/বা অফারগুলির জন্য সাইন আপ করে। এখানেই শেষ। বেস প্ল্যানগুলি যথাক্রমে মাসিক এবং বার্ষিক সাবস্ক্রিপশন। ব্যবহারকারী একটি প্রিপেইড সাবস্ক্রিপশন আপগ্রেড, ডাউনগ্রেড বা পুনর্নবীকরণযোগ্য একটিতে রূপান্তর করতে পারে৷

d7dba51f800a6cc4.png2220c15b849d2ead.png

আপনার অ্যাপে Google Play বিলিং লাইব্রেরি অন্তর্ভুক্ত করতে, আপনি নিম্নলিখিতগুলি তৈরি করবেন:

  • BillingClientWrapper - BillingClient লাইব্রেরির জন্য একটি মোড়ক। এটি প্লে বিলিং লাইব্রেরির BillingClient-এর সাথে ইন্টারঅ্যাকশনগুলিকে এনক্যাপসুলেট করতে চায় কিন্তু আপনার নিজের ইন্টিগ্রেশনে এটির প্রয়োজন নেই৷
  • SubscriptionDataRepository - আপনার অ্যাপের জন্য একটি বিলিং রিপোজিটরি যাতে অ্যাপের সাবস্ক্রিপশন পণ্যের তালিকা (অর্থাৎ বিক্রির জন্য কী) এবং শেয়ারফ্লো ভেরিয়েবলের একটি তালিকা থাকে যা ক্রয়ের অবস্থা এবং পণ্যের বিবরণ সংগ্রহ করতে সহায়তা করে
  • MainViewModel - একটি ViewModel যার মাধ্যমে আপনার বাকি অ্যাপটি বিলিং রিপোজিটরির সাথে যোগাযোগ করে। এটি বিভিন্ন ক্রয় পদ্ধতি ব্যবহার করে UI-তে বিলিং প্রবাহ চালু করতে সহায়তা করে।

শেষ হয়ে গেলে, আপনার অ্যাপের আর্কিটেকচারটি নীচের চিত্রের মতো দেখতে হবে:

c83bc759f32b0a63.png

আপনি কি শিখবেন

  • প্লে বিলিং লাইব্রেরি কিভাবে সংহত করবেন
  • প্লে কনসোলের মাধ্যমে কীভাবে সাবস্ক্রিপশন পণ্য, বেস প্ল্যান, অফার এবং ট্যাগ তৈরি করবেন
  • অ্যাপ থেকে উপলব্ধ বেস প্ল্যান এবং অফারগুলি কীভাবে পুনরুদ্ধার করবেন
  • যথাযথ পরামিতি সহ বিলিং প্রবাহ কিভাবে চালু করবেন
  • কিভাবে প্রিপেইড সাবস্ক্রিপশন পণ্য অফার

এই কোডল্যাবটি গুগল প্লে বিলিং-এ ফোকাস করা হয়েছে। অ-প্রাসঙ্গিক ধারণা এবং কোড ব্লকগুলিকে চকচকে করা হয়েছে এবং আপনাকে কেবল অনুলিপি এবং পেস্ট করার জন্য সরবরাহ করা হয়েছে।

আপনি কি প্রয়োজন হবে

  • অ্যান্ড্রয়েড স্টুডিওর একটি সাম্প্রতিক সংস্করণ (>= আর্কটিক ফক্স | 2020.3.1)
  • Android 8.0 বা তার বেশি সংস্করণ সহ একটি Android ডিভাইস
  • নমুনা কোড, গিটহাবে আপনার জন্য দেওয়া হয়েছে (পরবর্তী বিভাগে নির্দেশাবলী)
  • অ্যান্ড্রয়েড স্টুডিওতে অ্যান্ড্রয়েড ডেভেলপমেন্টের পরিমিত জ্ঞান
  • গুগল প্লে স্টোরে কীভাবে একটি অ্যাপ প্রকাশ করতে হয় সে সম্পর্কে জ্ঞান
  • কোটলিন কোড লেখার মাঝারি অভিজ্ঞতা
  • Google Play বিলিং লাইব্রেরি সংস্করণ 5.0.0

2. সেট আপ করা হচ্ছে

Github থেকে কোড পান

এই প্রজেক্টের জন্য আপনার যা যা প্রয়োজন আমরা তা একটি গিট রেপোতে রেখেছি। শুরু করার জন্য, আপনাকে কোডটি ধরতে হবে এবং আপনার প্রিয় ডেভ পরিবেশে এটি খুলতে হবে। এই কোডল্যাবের জন্য, আমরা Android স্টুডিও ব্যবহার করার পরামর্শ দিই।

শুরু করার কোডটি একটি GitHub সংগ্রহস্থলে সংরক্ষণ করা হয়। আপনি নিম্নলিখিত কমান্ডের মাধ্যমে সংগ্রহস্থল ক্লোন করতে পারেন:

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

3. গ্রাউন্ডওয়ার্ক

আমাদের শুরু বিন্দু কি?

আমাদের সূচনা পয়েন্ট হল এই কোডল্যাবের জন্য ডিজাইন করা একটি মৌলিক ব্যবহারকারী প্রোফাইল অ্যাপ। আমরা যে ধারণাগুলিকে চিত্রিত করতে চাই তা দেখানোর জন্য কোডটি সরলীকৃত করা হয়েছে এবং এটি উত্পাদন ব্যবহারের জন্য ডিজাইন করা হয়নি। আপনি যদি একটি প্রোডাকশন অ্যাপে এই কোডের কোনো অংশ পুনঃব্যবহার করতে চান, তাহলে সর্বোত্তম অনুশীলন অনুসরণ করতে ভুলবেন না এবং আপনার সমস্ত কোড সম্পূর্ণভাবে পরীক্ষা করুন।

অ্যান্ড্রয়েড স্টুডিওতে প্রকল্পটি আমদানি করুন

PlayBillingCodelab হল বেস অ্যাপ যেটিতে Google Play বিলিং বাস্তবায়ন নেই। অ্যান্ড্রয়েড স্টুডিও শুরু করুন এবং Open > billing/PlayBillingCodelab বেছে নিয়ে বিলিং-কোডেল্যাব আমদানি করুন

প্রকল্পের দুটি মডিউল রয়েছে:

  • start- এর কঙ্কাল অ্যাপ রয়েছে তবে প্রয়োজনীয় নির্ভরতা এবং আপনার বাস্তবায়নের জন্য প্রয়োজনীয় সমস্ত পদ্ধতির অভাব রয়েছে।
  • finished-এর সম্পূর্ণ প্রকল্প আছে এবং আপনি আটকে গেলে গাইড হিসেবে কাজ করতে পারেন।

অ্যাপটিতে আটটি শ্রেণীর ফাইল রয়েছে: BillingClientWrapper , SubscriptionDataRepository , Composables , MainState , MainViewModel , MainViewModelFactory , এবং MainActivity

  • BillingClientWrapper হল একটি মোড়ক যা Google Play বিলিং এর [BillingClient] পদ্ধতিগুলিকে বিচ্ছিন্ন করে যা একটি সহজ বাস্তবায়নের জন্য প্রয়োজনীয় এবং প্রক্রিয়াকরণের জন্য ডেটা সংগ্রহস্থলে প্রতিক্রিয়া নির্গত করে৷
  • SubscriptionDataRepository ব্যবহার করা হয় Google Play বিলিং ডেটা সোর্স (যেমন বিলিং ক্লায়েন্ট লাইব্রেরি) বিমূর্ত করতে এবং BillingClientWrapper-এ নির্গত StateFlow ডেটাকে Flow-এ রূপান্তরিত করে।
  • ButtonModel হল একটি ডেটা ক্লাস যা UI এ বোতাম তৈরি করতে ব্যবহৃত হয়।
  • কম্পোজেবল UI এর সমস্ত কম্পোজেবল পদ্ধতিকে একটি ক্লাসে বের করে।
  • MainState হল রাষ্ট্র পরিচালনার জন্য একটি ডেটা ক্লাস।
  • MainViewModel ব্যবহার করা হয় UI-তে ব্যবহৃত বিলিং সম্পর্কিত ডেটা এবং স্টেট রাখতে। এটি SubscriptionDataRepository-এর সমস্ত প্রবাহকে একটি স্টেট অবজেক্টে একত্রিত করে।
  • মেইনঅ্যাক্টিভিটি হল প্রধান অ্যাক্টিভিটি ক্লাস যা ইউজার ইন্টারফেসের জন্য কম্পোজেবল লোড করে।
  • ধ্রুবক হল বস্তু যে ধ্রুবকগুলি একাধিক শ্রেণী দ্বারা ব্যবহৃত হয়।

গ্রেডল

আপনার অ্যাপে Google Play বিলিং যোগ করার জন্য আপনাকে একটি Gradle নির্ভরতা যোগ করতে হবে। অ্যাপ মডিউলের build.gradle ফাইলটি খুলুন এবং নিম্নলিখিত যোগ করুন:

dependencies {
    val billing_version = "5.0.0"

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

গুগল প্লে কনসোল

এই কোডল্যাবের উদ্দেশ্যে, আপনাকে Google Play Console-এর সদস্যতা বিভাগে নিম্নলিখিত দুটি সাবস্ক্রিপশন পণ্য অফার তৈরি করতে হবে:

  • পণ্য আইডি up_basic_sub সহ 1 মৌলিক সদস্যতা

পণ্যটির সংশ্লিষ্ট ট্যাগ সহ 3টি বেস প্ল্যান (2টি স্বয়ংক্রিয়-নবীকরণ এবং 1টি প্রিপেইড) থাকতে হবে: 1টি মাসিক বেসিক সাবস্ক্রিপশন monthlybasic ট্যাগ সহ, 1টি বার্ষিক বেসিক সাবস্ক্রিপশন ট্যাগ yearlybasic এবং 1টি প্রিপেইড সাবস্ক্রিপশন prepaidbasic ট্যাগ সহ

আপনি বেস প্ল্যানে অফার যোগ করতে পারেন। অফারগুলি তাদের সম্পর্কিত বেস প্ল্যানগুলি থেকে ট্যাগগুলিকে উত্তরাধিকার সূত্রে পাবে৷

  • পণ্য আইডি up_premium_sub সহ 1 প্রিমিয়াম সদস্যতা

পণ্যটির সংশ্লিষ্ট ট্যাগ সহ 3টি বেস প্ল্যান (2টি স্বয়ংক্রিয় পুনর্নবীকরণ এবং 1টি প্রিপেইড) থাকতে হবে: monthlypremium ট্যাগ সহ 1টি মাসিক বেসিক সাবস্ক্রিপশন, yearlypremium ট্যাগ সহ 1 বার্ষিক বেসিক সাবস্ক্রিপশন এবং prepaidpremium ট্যাগ সহ 1টি প্রিপেইড সাবস্ক্রিপশন

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 প্রয়োগ করতে হবে।

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. পণ্যের বিস্তারিত প্রশ্নের জন্য শ্রোতা সেট করুন

দ্রষ্টব্য: এই পদ্ধতিটি _productWithProductDetails এ ম্যাপে প্রশ্নের ফলাফল নির্গত করে।

এছাড়াও মনে রাখবেন যে প্রশ্নটি ProductDetails ফেরত দেবে বলে আশা করা হচ্ছে। যদি এটি না ঘটে, তাহলে সম্ভবত সমস্যাটি হতে পারে যে প্লে কনসোলে সেট আপ করা পণ্যগুলি সক্রিয় করা হয়নি বা আপনি যেকোনও রিলিজ ট্র্যাকে বিলিং ক্লায়েন্ট নির্ভরতার সাথে একটি বিল্ড প্রকাশ করেননি।

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. ক্রয় অপারেশনের ফলাফলের জন্য শ্রোতা সেট করুন

যখন ব্যবহারকারী Google Play ক্রয় স্ক্রীন থেকে প্রস্থান করেন (ক্রয়টি সম্পূর্ণ করতে 'কিনুন' বোতামে ট্যাপ করে, অথবা ক্রয় বাতিল করতে ব্যাক বোতামে ট্যাপ করে), onPurchaseUpdated() কলব্যাক ক্রয় প্রবাহের ফলাফল আপনার কাছে ফেরত পাঠায় অ্যাপ BillingResult.responseCode এর উপর ভিত্তি করে, আপনি তারপর নির্ধারণ করতে পারেন যে ব্যবহারকারী সফলভাবে পণ্যটি কিনেছেন কিনা। responseCode == OK থাকলে, এর মানে ক্রয়টি সফলভাবে সম্পন্ন হয়েছে।

onPurchaseUpdated() Purchase বস্তুর একটি তালিকা ফেরত দেয় যাতে ব্যবহারকারী অ্যাপের মাধ্যমে করা সমস্ত কেনাকাটা অন্তর্ভুক্ত করে। অন্যান্য অনেক ক্ষেত্রের মধ্যে, প্রতিটি ক্রয় বস্তুতে পণ্য আইডি, ক্রয় টোকেন এবং 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 এর মান সত্য হিসাবে সেট করা হয় যখন স্বীকৃতিটি সফলভাবে প্রক্রিয়া করা হয়।

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 সংজ্ঞায়িত করতে যাচ্ছেন, যা আপনার ক্লায়েন্টদের জন্য একটি সর্বজনীন ইন্টারফেস যাতে তাদের BillingClientWrapper এবং SubscriptionDataRepository এর অভ্যন্তরীণ বিষয়গুলি জানতে হবে না।

প্রথমে MainViewModel এ, ভিউমডেল শুরু হলে আমরা বিলিং সংযোগ শুরু করি।

MainViewModel.kt

init {
   billingClient.startBillingConnection(billingConnectionState = _billingConnectionState)
}

তারপর, রিপোজিটরি থেকে প্রবাহগুলি যথাক্রমে productsForSaleFlows (উপলব্ধ পণ্যগুলির জন্য) এবং userCurrentSubscriptionFlow (ব্যবহারকারীর বর্তমান এবং সক্রিয় সদস্যতার জন্য) রেপো ক্লাসে প্রক্রিয়াজাত করা হয়।

বর্তমান কেনাকাটার তালিকা UI-এর কাছে 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 ব্লকে সংগ্রহ করা হয় এবং মানটি _destinationScreen নামক একটি MutableLiveData অবজেক্টে পোস্ট করা হয়।

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. বেস প্ল্যান এবং অফার টোকেন পুনরুদ্ধার

প্লে বিলিং লাইব্রেরি সংস্করণ 5.0.0 দিয়ে শুরু করে, সমস্ত সাবস্ক্রিপশন পণ্যের একাধিক বেস প্ল্যান এবং অফার থাকতে পারে প্রিপেইড বেস প্ল্যান ব্যতীত যার অফার থাকতে পারে না।

এই পদ্ধতিটি সমস্ত অফার এবং বেস প্ল্যানগুলি পুনরুদ্ধার করতে সাহায্য করে যার জন্য একজন ব্যবহারকারী যোগ্য ট্যাগগুলির নতুন প্রবর্তিত ধারণা ব্যবহার করে যা গোষ্ঠী সম্পর্কিত অফারগুলিতে ব্যবহৃত হয়৷

উদাহরণস্বরূপ, যখন একজন ব্যবহারকারী একটি মাসিক মৌলিক সাবস্ক্রিপশন কেনার চেষ্টা করেন, তখন মাসিক মৌলিক সাবস্ক্রিপশন পণ্যের সাথে সম্পর্কিত সমস্ত বেস প্ল্যান এবং অফার 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. সর্বনিম্ন মূল্যের অফার হিসাব

যখন একজন ব্যবহারকারী একাধিক অফারের জন্য যোগ্য হন, retrieveEligibleOffers() দ্বারা প্রত্যাবর্তিত অফারগুলির মধ্যে সর্বনিম্ন অফারটি গণনা করার জন্য leastPricedOfferToken() পদ্ধতি ব্যবহার করা হয়।

পদ্ধতিটি নির্বাচিত অফারের অফার আইডি টোকেন প্রদান করে।

এই বাস্তবায়নটি কেবলমাত্র 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 নির্মাতা

একটি নির্দিষ্ট পণ্যের জন্য ক্রয় প্রবাহ চালু করতে, পণ্যটির 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. পদ্ধতি কিনুন

কেনার পদ্ধতিটি কেনাকাটা শুরু করতে BillingClientWrapper এর launchBillingFlow() এবং 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. বিলিং সংযোগ বন্ধ করুন

অবশেষে, BillingClientWrapper এর terminateBillingConnection পদ্ধতিটিকে একটি ViewModel-এর onCleared() এ ডাকা হয়।

এটি বর্তমান বিলিং সংযোগ বন্ধ করার জন্য যখন সংশ্লিষ্ট কার্যকলাপ ধ্বংস হয়.

7. UI

এখন, আপনি UI-তে তৈরি করা সমস্ত কিছু ব্যবহার করার সময় এসেছে৷ এটিতে সহায়তা করার জন্য আপনি কম্পোজেবল এবং মেইন অ্যাক্টিভিটি ক্লাসের সাথে কাজ করবেন।

Composables.kt

Composables ক্লাস সম্পূর্ণরূপে সরবরাহ করা হয় এবং UI রেন্ডার করতে ব্যবহৃত সমস্ত রচনা ফাংশন এবং তাদের মধ্যে নেভিগেশন প্রক্রিয়া সংজ্ঞায়িত করে।

Subscriptions ফাংশন দুটি বোতাম দেখায়: Basic Subscription এবং Premium Subscription

Basic Subscription এবং Premium Subscription প্রতিটি নতুন কম্পোজ পদ্ধতি লোড করে যা তিনটি স্বতন্ত্র বেস প্ল্যান দেখায়: মাসিক, বার্ষিক এবং প্রিপেইড।

তারপরে, ব্যবহারকারীর থাকতে পারে একটি নির্দিষ্ট সদস্যতার জন্য তিনটি সম্ভাব্য প্রোফাইল রচনা ফাংশন রয়েছে: একটি পুনর্নবীকরণযোগ্য বেসিক, একটি পুনর্নবীকরণযোগ্য প্রিমিয়াম এবং হয় একটি প্রিপেইড বেসিক বা প্রিপেইড প্রিমিয়াম প্রোফাইল৷

  • যখন একজন ব্যবহারকারীর বেসিক সাবস্ক্রিপশন থাকে, তখন বেসিক প্রোফাইল তাদের হয় মাসিক, বার্ষিক বা প্রিপেইড প্রিমিয়াম সাবস্ক্রিপশনে আপগ্রেড করতে দেয়।
  • বিপরীতভাবে, যখন একজন ব্যবহারকারীর প্রিমিয়াম সাবস্ক্রিপশন থাকে, তখন তারা হয় একটি মাসিক, বার্ষিক বা প্রিপেইড মৌলিক সাবস্ক্রিপশনে ডাউনগ্রেড করতে পারে।
  • যখন একজন ব্যবহারকারীর প্রিপেইড সাবস্ক্রিপশন থাকে, তখন তারা টপ-আপ বোতাম দিয়ে তাদের সাবস্ক্রিপশন টপ-আপ করতে পারে বা তাদের প্রিপেইড সাবস্ক্রিপশনকে একটি সংশ্লিষ্ট স্বয়ংক্রিয় পুনর্নবীকরণ বেস প্ল্যানে রূপান্তর করতে পারে।

অবশেষে, একটি লোডিং স্ক্রিন ফাংশন রয়েছে যা Google Play-এর সাথে সংযোগ তৈরি করার সময় এবং যখন একটি ব্যবহারকারীর প্রোফাইল রেন্ডার করা হয় তখন ব্যবহার করা হয়।

MainActivity.kt

  1. যখন MainActivity তৈরি করা হয়, ভিউমডেল তাৎক্ষণিকভাবে তৈরি হয় এবং MainNavHost নামক একটি রচনা ফাংশন লোড হয়।
  2. MainNavHost একটি ভেরিয়েবল দিয়ে শুরু হয় একটি ভেরিয়েবল isBillingConnected যা ভিউমডেলের billingConnectionSate লাইভডেটা থেকে তৈরি করা হয়েছে এবং পরিবর্তনের জন্য পর্যবেক্ষণ করা হয়েছে কারণ যখন vieModel ইনস্ট্যান্ট করা হয়, তখন এটি BillingClientWrapper এর startBillingConnection পদ্ধতিতে billingConnectionSate পাস করে।

isBillingConnected সংযোগ স্থাপন করা হলে সত্য এবং না হলে মিথ্যা সেট করা হয়।

মিথ্যা হলে, 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()
)

অবশেষে, viewModel এর destinationScreen লাইভডেটা পরিবর্তনশীল পরিলক্ষিত হয়।

ব্যবহারকারীর বর্তমান সাবস্ক্রিপশন অবস্থার উপর ভিত্তি করে, সংশ্লিষ্ট রচনা ফাংশন রেন্ডার করা হয়।

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 সাবস্ক্রিপশন পণ্যগুলিকে একটি খুব সাধারণ অ্যাপে সংহত করেছেন!

একটি সুরক্ষিত সার্ভার সেটআপ সহ আরও পরিশীলিত অ্যাপের একটি নথিভুক্ত সংস্করণের জন্য, অফিসিয়াল নমুনা দেখুন৷

আরও পড়া