Upgrade billing API from v3 to v5

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

@ -35,8 +35,8 @@ android {
applicationId "net.pokeranalytics.android"
minSdkVersion 23
targetSdkVersion 32
versionCode 138
versionName "5.5.2"
versionCode 139
versionName "5.5.3"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
@ -115,7 +115,7 @@ dependencies {
implementation 'com.google.android.libraries.places:places:2.3.0'
// 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
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.format.DateTimeParseException
class SubscriptionFragment : BaseFragment(), SkuDetailsResponseListener, PurchaseListener, ViewPager.OnPageChangeListener {
class SubscriptionFragment : BaseFragment(), ProductDetailsResponseListener, PurchaseListener, ViewPager.OnPageChangeListener {
companion object {
val parallax: Float = 64f.px
@ -50,7 +50,9 @@ class SubscriptionFragment : BaseFragment(), SkuDetailsResponseListener, Purchas
}
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 _binding: FragmentSubscriptionBinding? = null
@ -164,8 +166,14 @@ class SubscriptionFragment : BaseFragment(), SkuDetailsResponseListener, Purchas
return@setOnClickListener
}
this.selectedProduct?.let {
AppGuard.initiatePurchase(this.requireActivity(), it)
this.selectedProduct?.let { productDetails ->
this.selectedOfferDetails?.let { offerDetails ->
AppGuard.initiatePurchase(this.requireActivity(), productDetails, offerDetails.offerToken)
}?: run {
Toast.makeText(requireContext(), R.string.product_unavailable, Toast.LENGTH_LONG).show()
}
} ?: run {
Toast.makeText(requireContext(), R.string.product_unavailable, Toast.LENGTH_LONG).show()
}
@ -225,32 +233,71 @@ class SubscriptionFragment : BaseFragment(), SkuDetailsResponseListener, Purchas
}
// 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) {
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()
}
}
private fun updateUI() {
this.selectedProduct?.let {
val perYearString = requireContext().getString(R.string.year_subscription)
val formattedPrice = it.price + " / " + perYearString
binding.price.text = formattedPrice
var freeTrialDays = 30 // initial, should be more, no less
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
try {
val p = Period.parse(it.freeTrialPeriod)
freeTrialDays = p.days
} catch (e: DateTimeParseException) {
CrashLogging.log("Error parsing period with value: ${it.freeTrialPeriod}")
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 formattedPrice = "$it / $perYearString"
binding.price.text = formattedPrice
}
freeTrialPeriod?.let {
var freeTrialDays = 30 // initial, should be more, no less
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
try {
val p = Period.parse(it)
freeTrialDays = p.days
} catch (e: DateTimeParseException) {
CrashLogging.log("Error parsing period with value: $it")
}
}
val formattedFreeTrial =
"$freeTrialDays " + requireContext().getString(R.string.days) + " " + requireContext().getString(R.string.free_trial)
binding.freetrial.text = formattedFreeTrial
}
val formattedFreeTrial =
"$freeTrialDays " + requireContext().getString(R.string.days) + " " + requireContext().getString(R.string.free_trial)
binding.freetrial.text = formattedFreeTrial
} ?: run {
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) {
// 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())
this.activity?.finish()
@ -286,8 +333,7 @@ class SubscriptionFragment : BaseFragment(), SkuDetailsResponseListener, Purchas
private fun updatePagerIndicators(position: Int) {
binding.pageIndicator.children.forEachIndexed { index, view ->
val drawable = view.background
when (drawable) {
when (val drawable = view.background) {
is GradientDrawable -> {
val color = if (position == index) R.color.white else R.color.quantum_grey
drawable.setColor(requireContext().getColor(color))

@ -12,7 +12,6 @@ import timber.log.Timber
import java.io.IOException
import java.text.SimpleDateFormat
import java.util.*
import kotlin.collections.ArrayList
enum class IAPProducts(var identifier: String) {
PRO("unlimited")
@ -58,14 +57,14 @@ object AppGuard : PurchasesUpdatedListener {
/**
* Returns whether the user has the pro subscription
* Always true for debugging
* Usually true for debugging
*/
val isProUser: Boolean
get() {
if (this.endOfUse != null) return true
return if (BuildConfig.DEBUG) {
true //false //true
this._isProUser //true //false //true
} else {
this._isProUser
}
@ -166,10 +165,20 @@ object AppGuard : PurchasesUpdatedListener {
private fun updatePurchases() {
this.resetPurchases()
// Automatically checks for purchases (when switching devices for example)
val purchasesResult = billingClient.queryPurchases(BillingClient.SkuType.SUBS)
purchasesResult.purchasesList?.forEach {
this.handlePurchase(it)
val params = QueryPurchasesParams.newBuilder()
.setProductType(BillingClient.ProductType.SUBS)
billingClient.queryPurchasesAsync(params.build()) { _, purchases ->
purchases.forEach {
this.handlePurchase(it)
}
}
// purchasesResult.purchasesList?.forEach {
// this.handlePurchase(it)
// }
}
/**
@ -183,17 +192,22 @@ object AppGuard : PurchasesUpdatedListener {
/**
* Requests the product descriptions
*/
fun requestProducts(listener: SkuDetailsResponseListener): Boolean {
fun requestProducts(listener: ProductDetailsResponseListener): Boolean {
if (this.billingClientAvailable) {
val skuList = ArrayList<String>()
skuList.add(IAPProducts.PRO.identifier)
val params = SkuDetailsParams.newBuilder()
params.setSkusList(skuList).setType(BillingClient.SkuType.SUBS)
val productList =
listOf(
QueryProductDetailsParams.Product.newBuilder()
.setProductId(IAPProducts.PRO.identifier)
.setProductType(BillingClient.ProductType.SUBS)
.build()
)
val params = QueryProductDetailsParams.newBuilder().setProductList(productList)
this.executeServiceRequest {
billingClient.querySkuDetailsAsync(params.build(), listener)
billingClient.queryProductDetailsAsync(params.build(), listener)
}
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()
.setSkuDetails(skuDetails)
.setProductDetailsParamsList(productList)
.build()
this.executeServiceRequest {
@ -268,36 +290,41 @@ object AppGuard : PurchasesUpdatedListener {
private fun handlePurchase(purchase: Purchase) {
if (this.verifyValidSignature(purchase.originalJson, purchase.signature)) {
when (purchase.sku) {
IAPProducts.PRO.identifier -> {
val date = Date(purchase.purchaseTime)
Timber.d("*** Auto renewing = ${purchase.isAutoRenewing}")
Timber.d("*** purchaseTime = $date")
purchase.products.forEach { product ->
if (purchase.purchaseState == Purchase.PurchaseState.PURCHASED) {
when (product) {
IAPProducts.PRO.identifier -> {
if (!purchase.isAcknowledged) {
val params = AcknowledgePurchaseParams.newBuilder().setPurchaseToken(purchase.purchaseToken).build()
this.billingClient.acknowledgePurchase(params) { result ->
Timber.d("Acknowledge result: ${result.responseCode}")
val date = Date(purchase.purchaseTime)
Timber.d("*** Auto renewing = ${purchase.isAutoRenewing}")
Timber.d("*** purchaseTime = $date")
if (purchase.purchaseState == Purchase.PurchaseState.PURCHASED) {
if (!purchase.isAcknowledged) {
val params = AcknowledgePurchaseParams.newBuilder().setPurchaseToken(purchase.purchaseToken).build()
this.billingClient.acknowledgePurchase(params) { result ->
Timber.d("Acknowledge result: ${result.responseCode}")
}
}
}
this._isProUser = true
this._isProUser = true
this.purchaseListeners.forEach { listener ->
Handler(Looper.getMainLooper()).post {
listener.purchaseDidSucceed(purchase)
this.purchaseListeners.forEach { listener ->
Handler(Looper.getMainLooper()).post {
listener.purchaseDidSucceed(purchase)
}
}
}
}
}
else -> {
else -> {
}
}
}
} else {
// invalid purchase
}

Loading…
Cancel
Save