diff --git a/app/build.gradle b/app/build.gradle index 9d382bae..ae07479a 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -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') diff --git a/app/src/main/java/net/pokeranalytics/android/ui/fragment/SubscriptionFragment.kt b/app/src/main/java/net/pokeranalytics/android/ui/fragment/SubscriptionFragment.kt index e11b4680..29ae2d9b 100644 --- a/app/src/main/java/net/pokeranalytics/android/ui/fragment/SubscriptionFragment.kt +++ b/app/src/main/java/net/pokeranalytics/android/ui/fragment/SubscriptionFragment.kt @@ -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?) { +// override fun onSkuDetailsResponse(result: BillingResult, skuDetailsList: MutableList?) { +// 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) { 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)) diff --git a/app/src/main/java/net/pokeranalytics/android/util/billing/AppGuard.kt b/app/src/main/java/net/pokeranalytics/android/util/billing/AppGuard.kt index 512e4065..6b331807 100644 --- a/app/src/main/java/net/pokeranalytics/android/util/billing/AppGuard.kt +++ b/app/src/main/java/net/pokeranalytics/android/util/billing/AppGuard.kt @@ -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() - 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 }