Upgrade billing API from v3 to v5

powerreport
Laurent 3 years ago
parent a32b9afd06
commit 49296c265c
  1. 6
      app/build.gradle
  2. 72
      app/src/main/java/net/pokeranalytics/android/ui/fragment/SubscriptionFragment.kt
  3. 57
      app/src/main/java/net/pokeranalytics/android/util/billing/AppGuard.kt

@ -35,8 +35,8 @@ android {
applicationId "net.pokeranalytics.android" applicationId "net.pokeranalytics.android"
minSdkVersion 23 minSdkVersion 23
targetSdkVersion 32 targetSdkVersion 32
versionCode 138 versionCode 139
versionName "5.5.2" versionName "5.5.3"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
} }
@ -115,7 +115,7 @@ dependencies {
implementation 'com.google.android.libraries.places:places:2.3.0' implementation 'com.google.android.libraries.places:places:2.3.0'
// Billing / Subscriptions // Billing / Subscriptions
implementation 'com.android.billingclient:billing:3.0.1' implementation 'com.android.billingclient:billing:5.0.0'
// Import the BoM for the Firebase platform // Import the BoM for the Firebase platform
implementation platform('com.google.firebase:firebase-bom:26.1.0') implementation platform('com.google.firebase:firebase-bom:26.1.0')

@ -35,7 +35,7 @@ import java.lang.ref.WeakReference
import java.time.Period import java.time.Period
import java.time.format.DateTimeParseException import java.time.format.DateTimeParseException
class SubscriptionFragment : BaseFragment(), SkuDetailsResponseListener, PurchaseListener, ViewPager.OnPageChangeListener { class SubscriptionFragment : BaseFragment(), ProductDetailsResponseListener, PurchaseListener, ViewPager.OnPageChangeListener {
companion object { companion object {
val parallax: Float = 64f.px val parallax: Float = 64f.px
@ -50,7 +50,9 @@ class SubscriptionFragment : BaseFragment(), SkuDetailsResponseListener, Purchas
} }
private var pagerAdapter: ScreenSlidePagerAdapter? = null private var pagerAdapter: ScreenSlidePagerAdapter? = null
private var selectedProduct: SkuDetails? = null private var selectedProduct: ProductDetails? = null
private var selectedOfferDetails: ProductDetails.SubscriptionOfferDetails? = null
private var showSessionMessage = false private var showSessionMessage = false
private var _binding: FragmentSubscriptionBinding? = null private var _binding: FragmentSubscriptionBinding? = null
@ -164,8 +166,14 @@ class SubscriptionFragment : BaseFragment(), SkuDetailsResponseListener, Purchas
return@setOnClickListener return@setOnClickListener
} }
this.selectedProduct?.let { this.selectedProduct?.let { productDetails ->
AppGuard.initiatePurchase(this.requireActivity(), it)
this.selectedOfferDetails?.let { offerDetails ->
AppGuard.initiatePurchase(this.requireActivity(), productDetails, offerDetails.offerToken)
}?: run {
Toast.makeText(requireContext(), R.string.product_unavailable, Toast.LENGTH_LONG).show()
}
} ?: run { } ?: run {
Toast.makeText(requireContext(), R.string.product_unavailable, Toast.LENGTH_LONG).show() Toast.makeText(requireContext(), R.string.product_unavailable, Toast.LENGTH_LONG).show()
} }
@ -225,32 +233,71 @@ class SubscriptionFragment : BaseFragment(), SkuDetailsResponseListener, Purchas
} }
// SkuDetailsResponseListener // SkuDetailsResponseListener
override fun onSkuDetailsResponse(result: BillingResult, skuDetailsList: MutableList<SkuDetails>?) { // override fun onSkuDetailsResponse(result: BillingResult, skuDetailsList: MutableList<SkuDetails>?) {
// if (result.responseCode == BillingClient.BillingResponseCode.OK) {
// this.hideLoader()
// selectedProduct = skuDetailsList?.firstOrNull { it.sku == IAPProducts.PRO.identifier }
// updateUI()
// }
// }
// ProductDetailsResponseListener
override fun onProductDetailsResponse(result: BillingResult, productList: MutableList<ProductDetails>) {
if (result.responseCode == BillingClient.BillingResponseCode.OK) { if (result.responseCode == BillingClient.BillingResponseCode.OK) {
this.hideLoader() this.hideLoader()
selectedProduct = skuDetailsList?.firstOrNull { it.sku == IAPProducts.PRO.identifier } selectedProduct = productList.firstOrNull { it.productId == IAPProducts.PRO.identifier }
this.selectedOfferDetails = selectedProduct?.subscriptionOfferDetails?.firstOrNull()
Timber.d("OFFERS = ${this.selectedProduct?.subscriptionOfferDetails?.size ?: 0}")
updateUI() updateUI()
} }
} }
private fun updateUI() { private fun updateUI() {
this.selectedProduct?.let { this.selectedProduct?.let { productDetails ->
var price: String? = null
var freeTrialPeriod: String? = null
productDetails.subscriptionOfferDetails?.firstOrNull()?.let { details ->
details.pricingPhases.pricingPhaseList.forEach { pricingPhase ->
when (pricingPhase.priceAmountMicros) {
0L -> {
freeTrialPeriod = pricingPhase.billingPeriod
}
else -> {
price = pricingPhase.formattedPrice
}
}
}
}
price?.let {
val perYearString = requireContext().getString(R.string.year_subscription) val perYearString = requireContext().getString(R.string.year_subscription)
val formattedPrice = it.price + " / " + perYearString val formattedPrice = "$it / $perYearString"
binding.price.text = formattedPrice binding.price.text = formattedPrice
}
freeTrialPeriod?.let {
var freeTrialDays = 30 // initial, should be more, no less var freeTrialDays = 30 // initial, should be more, no less
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
try { try {
val p = Period.parse(it.freeTrialPeriod) val p = Period.parse(it)
freeTrialDays = p.days freeTrialDays = p.days
} catch (e: DateTimeParseException) { } catch (e: DateTimeParseException) {
CrashLogging.log("Error parsing period with value: ${it.freeTrialPeriod}") CrashLogging.log("Error parsing period with value: $it")
} }
} }
val formattedFreeTrial = val formattedFreeTrial =
"$freeTrialDays " + requireContext().getString(R.string.days) + " " + requireContext().getString(R.string.free_trial) "$freeTrialDays " + requireContext().getString(R.string.days) + " " + requireContext().getString(R.string.free_trial)
binding.freetrial.text = formattedFreeTrial binding.freetrial.text = formattedFreeTrial
}
} ?: run { } ?: run {
Toast.makeText(requireContext(), R.string.contact_support, Toast.LENGTH_LONG).show() Toast.makeText(requireContext(), R.string.contact_support, Toast.LENGTH_LONG).show()
} }
@ -261,7 +308,7 @@ class SubscriptionFragment : BaseFragment(), SkuDetailsResponseListener, Purchas
override fun purchaseDidSucceed(purchase: Purchase) { override fun purchaseDidSucceed(purchase: Purchase) {
// record purchase in preferences for troubleshooting / verification // record purchase in preferences for troubleshooting / verification
val purchaseInfos = listOf(purchase.sku, purchase.orderId, purchase.purchaseToken) val purchaseInfos = listOf(purchase.products.joinToString(" - "), purchase.orderId, purchase.purchaseToken)
Preferences.setString(Preferences.Keys.LATEST_PURCHASE, purchaseInfos.joinToString("/"), requireContext()) Preferences.setString(Preferences.Keys.LATEST_PURCHASE, purchaseInfos.joinToString("/"), requireContext())
this.activity?.finish() this.activity?.finish()
@ -286,8 +333,7 @@ class SubscriptionFragment : BaseFragment(), SkuDetailsResponseListener, Purchas
private fun updatePagerIndicators(position: Int) { private fun updatePagerIndicators(position: Int) {
binding.pageIndicator.children.forEachIndexed { index, view -> binding.pageIndicator.children.forEachIndexed { index, view ->
val drawable = view.background when (val drawable = view.background) {
when (drawable) {
is GradientDrawable -> { is GradientDrawable -> {
val color = if (position == index) R.color.white else R.color.quantum_grey val color = if (position == index) R.color.white else R.color.quantum_grey
drawable.setColor(requireContext().getColor(color)) drawable.setColor(requireContext().getColor(color))

@ -12,7 +12,6 @@ import timber.log.Timber
import java.io.IOException import java.io.IOException
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.* import java.util.*
import kotlin.collections.ArrayList
enum class IAPProducts(var identifier: String) { enum class IAPProducts(var identifier: String) {
PRO("unlimited") PRO("unlimited")
@ -58,14 +57,14 @@ object AppGuard : PurchasesUpdatedListener {
/** /**
* Returns whether the user has the pro subscription * Returns whether the user has the pro subscription
* Always true for debugging * Usually true for debugging
*/ */
val isProUser: Boolean val isProUser: Boolean
get() { get() {
if (this.endOfUse != null) return true if (this.endOfUse != null) return true
return if (BuildConfig.DEBUG) { return if (BuildConfig.DEBUG) {
true //false //true this._isProUser //true //false //true
} else { } else {
this._isProUser this._isProUser
} }
@ -166,12 +165,22 @@ object AppGuard : PurchasesUpdatedListener {
private fun updatePurchases() { private fun updatePurchases() {
this.resetPurchases() this.resetPurchases()
// Automatically checks for purchases (when switching devices for example) // Automatically checks for purchases (when switching devices for example)
val purchasesResult = billingClient.queryPurchases(BillingClient.SkuType.SUBS)
purchasesResult.purchasesList?.forEach { val params = QueryPurchasesParams.newBuilder()
.setProductType(BillingClient.ProductType.SUBS)
billingClient.queryPurchasesAsync(params.build()) { _, purchases ->
purchases.forEach {
this.handlePurchase(it) this.handlePurchase(it)
} }
} }
// purchasesResult.purchasesList?.forEach {
// this.handlePurchase(it)
// }
}
/** /**
* Reset all purchases * Reset all purchases
* This is done before restoring in order to ensure that subscriptions stops * This is done before restoring in order to ensure that subscriptions stops
@ -183,17 +192,22 @@ object AppGuard : PurchasesUpdatedListener {
/** /**
* Requests the product descriptions * Requests the product descriptions
*/ */
fun requestProducts(listener: SkuDetailsResponseListener): Boolean { fun requestProducts(listener: ProductDetailsResponseListener): Boolean {
if (this.billingClientAvailable) { if (this.billingClientAvailable) {
val skuList = ArrayList<String>() val productList =
skuList.add(IAPProducts.PRO.identifier) listOf(
val params = SkuDetailsParams.newBuilder() QueryProductDetailsParams.Product.newBuilder()
params.setSkusList(skuList).setType(BillingClient.SkuType.SUBS) .setProductId(IAPProducts.PRO.identifier)
.setProductType(BillingClient.ProductType.SUBS)
.build()
)
val params = QueryProductDetailsParams.newBuilder().setProductList(productList)
this.executeServiceRequest { this.executeServiceRequest {
billingClient.querySkuDetailsAsync(params.build(), listener) billingClient.queryProductDetailsAsync(params.build(), listener)
} }
return true return true
@ -202,12 +216,20 @@ object AppGuard : PurchasesUpdatedListener {
} }
/** /**
* Initiates purchase with the product [skuDetails] * Initiates purchase with the product [productDetails]
*/ */
fun initiatePurchase(activity: Activity, skuDetails: SkuDetails) { fun initiatePurchase(activity: Activity, productDetails: ProductDetails, offerToken: String) {
val productList =
mutableListOf(
BillingFlowParams.ProductDetailsParams.newBuilder()
.setProductDetails(productDetails)
.setOfferToken(offerToken)
.build()
)
val flowParams = BillingFlowParams.newBuilder() val flowParams = BillingFlowParams.newBuilder()
.setSkuDetails(skuDetails) .setProductDetailsParamsList(productList)
.build() .build()
this.executeServiceRequest { this.executeServiceRequest {
@ -268,7 +290,10 @@ object AppGuard : PurchasesUpdatedListener {
private fun handlePurchase(purchase: Purchase) { private fun handlePurchase(purchase: Purchase) {
if (this.verifyValidSignature(purchase.originalJson, purchase.signature)) { if (this.verifyValidSignature(purchase.originalJson, purchase.signature)) {
when (purchase.sku) {
purchase.products.forEach { product ->
when (product) {
IAPProducts.PRO.identifier -> { IAPProducts.PRO.identifier -> {
val date = Date(purchase.purchaseTime) val date = Date(purchase.purchaseTime)
@ -298,6 +323,8 @@ object AppGuard : PurchasesUpdatedListener {
else -> { else -> {
} }
} }
}
} else { } else {
// invalid purchase // invalid purchase
} }

Loading…
Cancel
Save