fix issue with subscriptions being reapplied

# Conflicts:
#	version.properties
This commit is contained in:
Phillip Thelen 2023-03-27 14:30:52 +02:00
parent e37f25bc21
commit 7a1541ac84
2 changed files with 84 additions and 62 deletions

View file

@ -5,6 +5,7 @@ import android.content.Context
import android.content.SharedPreferences
import androidx.core.content.edit
import androidx.core.os.bundleOf
import androidx.lifecycle.asFlow
import com.android.billingclient.api.AcknowledgePurchaseParams
import com.android.billingclient.api.BillingClient
import com.android.billingclient.api.BillingClientStateListener
@ -41,6 +42,8 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.take
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.json.JSONObject
@ -51,37 +54,47 @@ import kotlin.time.DurationUnit
import kotlin.time.toDuration
class PurchaseHandler(
private val context: Context,
private val analyticsManager: AnalyticsManager,
private val apiClient: ApiClient,
private val userViewModel: MainUserViewModel
private val context : Context,
private val analyticsManager : AnalyticsManager,
private val apiClient : ApiClient,
private val userViewModel : MainUserViewModel
) : PurchasesUpdatedListener, PurchasesResponseListener {
private val billingClient =
BillingClient.newBuilder(context).setListener(this).enablePendingPurchases().build()
override fun onPurchasesUpdated(result: BillingResult, purchases: MutableList<Purchase>?) {
override fun onPurchasesUpdated(result : BillingResult, purchases : MutableList<Purchase>?) {
purchases?.let { processPurchases(result, it) }
}
override fun onQueryPurchasesResponse(result: BillingResult, purchases: MutableList<Purchase>) {
override fun onQueryPurchasesResponse(
result : BillingResult,
purchases : MutableList<Purchase>
) {
processPurchases(result, purchases)
}
private fun processPurchases(result: BillingResult, purchases: List<Purchase>) {
private fun processPurchases(result : BillingResult, purchases : List<Purchase>) {
when (result.responseCode) {
BillingClient.BillingResponseCode.OK -> {
val mostRecentSub = findMostRecentSubscription(purchases)
val plan = userViewModel.user.value?.purchased?.plan
for (purchase in purchases) {
if (plan?.isActive == true && PurchaseTypes.allSubscriptionTypes.contains(
purchase.products.firstOrNull()
)
) {
if (((plan.dateTerminated != null) == purchase.isAutoRenewing) || mostRecentSub?.orderId != purchase.orderId) {
return
MainScope().launchCatching {
userViewModel.user.asFlow()
.filterNotNull().take(1).collect {
val plan = it.purchased!!.plan
for (purchase in purchases) {
if (plan?.isActive == true && PurchaseTypes.allSubscriptionTypes.contains(
purchase.products.firstOrNull()
)
) {
if (((plan.dateTerminated != null) == purchase.isAutoRenewing)
|| mostRecentSub?.orderId != purchase.orderId
|| purchase.purchaseToken == plan.customerId) {
continue
}
}
handle(purchase)
}
}
}
handle(purchase)
}
}
@ -111,12 +124,12 @@ class PurchaseHandler(
startListening()
}
private var billingClientState: BillingClientState = BillingClientState.UNINITIALIZED
private var billingClientState : BillingClientState = BillingClientState.UNINITIALIZED
private enum class BillingClientState {
UNINITIALIZED, READY, UNAVAILABLE, DISCONNECTED, CONNECTING;
val canMaybePurchase: Boolean
val canMaybePurchase : Boolean
get() {
return this == UNINITIALIZED || this == READY || this == CONNECTING
}
@ -132,7 +145,7 @@ class PurchaseHandler(
}
billingClientState = BillingClientState.CONNECTING
billingClient.startConnection(object : BillingClientStateListener {
override fun onBillingSetupFinished(billingResult: BillingResult) {
override fun onBillingSetupFinished(billingResult : BillingResult) {
if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
billingClientState = BillingClientState.READY
MainScope().launchCatching {
@ -193,18 +206,18 @@ class PurchaseHandler(
suspend fun getAllGiftSubscriptionProducts() =
getSKUs(BillingClient.ProductType.INAPP, PurchaseTypes.allSubscriptionNoRenewTypes)
suspend fun getInAppPurchaseSKU(identifier: String) =
suspend fun getInAppPurchaseSKU(identifier : String) =
getSKU(BillingClient.ProductType.INAPP, identifier)
private suspend fun getSKUs(type: String, identifiers: List<String>) =
private suspend fun getSKUs(type : String, identifiers : List<String>) =
loadInventory(type, identifiers) ?: emptyList()
private suspend fun getSKU(type: String, identifier: String): ProductDetails? {
private suspend fun getSKU(type : String, identifier : String) : ProductDetails? {
val inventory = loadInventory(type, listOf(identifier))
return inventory?.firstOrNull()
}
private suspend fun loadInventory(type: String, skus: List<String>): List<ProductDetails>? {
private suspend fun loadInventory(type : String, skus : List<String>) : List<ProductDetails>? {
retryUntil {
if (billingClientState == BillingClientState.DISCONNECTED) {
startListening()
@ -223,11 +236,11 @@ class PurchaseHandler(
}
fun purchase(
activity: Activity,
skuDetails: ProductDetails,
recipient: String? = null,
recipientUsername: String? = null,
isSaleGemPurchase: Boolean = false
activity : Activity,
skuDetails : ProductDetails,
recipient : String? = null,
recipientUsername : String? = null,
isSaleGemPurchase : Boolean = false
) {
this.isSaleGemPurchase = isSaleGemPurchase
recipient?.let {
@ -245,7 +258,7 @@ class PurchaseHandler(
billingClient.launchBillingFlow(activity, flowParams)
}
private suspend fun consume(purchase: Purchase, retries: Int = 4) {
private suspend fun consume(purchase : Purchase, retries : Int = 4) {
retryUntil { billingClientState.canMaybePurchase && billingClient.isReady }
val params = ConsumeParams.newBuilder().setPurchaseToken(purchase.purchaseToken).build()
val result = billingClient.consumePurchase(params)
@ -257,10 +270,12 @@ class PurchaseHandler(
}
}
private fun handle(purchase: Purchase) {
if (purchase.purchaseState != Purchase.PurchaseState.PURCHASED) {
private var processedPurchases = mutableSetOf<String>()
private fun handle(purchase : Purchase) {
if (purchase.purchaseState != Purchase.PurchaseState.PURCHASED || processedPurchases.contains(purchase.orderId)) {
return
}
processedPurchases.add(purchase.orderId)
val sku = purchase.products.firstOrNull()
when {
sku == PurchaseTypes.JubilantGrphatrice -> {
@ -274,7 +289,7 @@ class PurchaseHandler(
consume(purchase)
}
displayGryphatriceConfirmationDialog(purchase, gift?.third)
} catch (throwable: Throwable) {
} catch (throwable : Throwable) {
handleError(throwable, purchase)
}
}
@ -291,7 +306,7 @@ class PurchaseHandler(
consume(purchase)
}
displayConfirmationDialog(purchase, gift?.third)
} catch (throwable: Throwable) {
} catch (throwable : Throwable) {
handleError(throwable, purchase)
}
}
@ -308,7 +323,7 @@ class PurchaseHandler(
consume(purchase)
}
displayConfirmationDialog(purchase, gift?.third)
} catch (throwable: Throwable) {
} catch (throwable : Throwable) {
handleError(throwable, purchase)
}
}
@ -325,7 +340,7 @@ class PurchaseHandler(
acknowledgePurchase(purchase)
}
displayConfirmationDialog(purchase)
} catch (throwable: Throwable) {
} catch (throwable : Throwable) {
handleError(throwable, purchase)
}
}
@ -333,7 +348,7 @@ class PurchaseHandler(
}
}
private suspend fun acknowledgePurchase(purchase: Purchase, retries: Int = 4) {
private suspend fun acknowledgePurchase(purchase : Purchase, retries : Int = 4) {
val params =
AcknowledgePurchaseParams.newBuilder().setPurchaseToken(purchase.purchaseToken).build()
val response = billingClient.acknowledgePurchase(params)
@ -343,13 +358,13 @@ class PurchaseHandler(
}
}
private fun processedPurchase(purchase: Purchase) {
private fun processedPurchase(purchase : Purchase) {
MainScope().launch(ExceptionHandler.coroutine()) {
userViewModel.userRepository.retrieveUser(false, true)
}
}
private fun buildValidationRequest(purchase: Purchase): PurchaseValidationRequest {
private fun buildValidationRequest(purchase : Purchase) : PurchaseValidationRequest {
val validationRequest = PurchaseValidationRequest()
validationRequest.sku = purchase.products.firstOrNull()
validationRequest.transaction = Transaction()
@ -367,7 +382,7 @@ class PurchaseHandler(
return validationRequest
}
private fun handleError(throwable: Throwable, purchase: Purchase) {
private fun handleError(throwable : Throwable, purchase : Purchase) {
(throwable as? HttpException)?.let { error ->
if (error.code() == 401) {
val res = apiClient.getErrorResponse(throwable)
@ -381,27 +396,28 @@ class PurchaseHandler(
}
}
}
processedPurchases.remove(purchase.orderId)
FirebaseCrashlytics.getInstance().recordException(throwable)
}
suspend fun checkForSubscription(): Purchase? {
suspend fun checkForSubscription() : Purchase? {
val result = withContext(Dispatchers.IO) {
val params =
QueryPurchasesParams.newBuilder().setProductType(BillingClient.ProductType.SUBS)
.build()
billingClient.queryPurchasesAsync(params)
}
val fallback: Purchase? = null
val fallback : Purchase? = null
if (result.billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
return findMostRecentSubscription(result.purchasesList)
}
return fallback
}
private fun findMostRecentSubscription(purchasesList: List<Purchase>): Purchase? {
private fun findMostRecentSubscription(purchasesList : List<Purchase>) : Purchase? {
val purchases =
purchasesList.filter { it.isAcknowledged }.sortedByDescending { it.purchaseTime }
var fallback: Purchase? = null
var fallback : Purchase? = null
// If there is a subscription that is still active, prioritise that. Otherwise return the most recent one.
for (purchase in purchases) {
if (purchase.isAutoRenewing) {
@ -413,12 +429,15 @@ class PurchaseHandler(
return fallback
}
suspend fun cancelSubscription(): User? {
private var alreadyTriedCancellation = false
suspend fun cancelSubscription() : User? {
if (alreadyTriedCancellation) return null
alreadyTriedCancellation = true
apiClient.cancelSubscription()
return userViewModel.userRepository.retrieveUser(false, true)
}
private fun durationString(sku: String): String {
private fun durationString(sku : String) : String {
return when (sku) {
PurchaseTypes.Subscription1MonthNoRenew, PurchaseTypes.Subscription1Month -> "1"
PurchaseTypes.Subscription3MonthNoRenew, PurchaseTypes.Subscription3Month -> "3"
@ -430,7 +449,7 @@ class PurchaseHandler(
private var isSaleGemPurchase = false
private fun gemAmountString(sku: String): String {
private fun gemAmountString(sku : String) : String {
if (isSaleGemPurchase) {
isSaleGemPurchase = false
return when (sku) {
@ -453,7 +472,7 @@ class PurchaseHandler(
private val displayedConfirmations = mutableListOf<String>()
private fun displayConfirmationDialog(purchase: Purchase, giftedTo: String? = null) {
private fun displayConfirmationDialog(purchase : Purchase, giftedTo : String? = null) {
if (displayedConfirmations.contains(purchase.orderId)) {
return
}
@ -509,7 +528,10 @@ class PurchaseHandler(
}
}
private fun displayGryphatriceConfirmationDialog(purchase: Purchase, giftedTo: String? = null) {
private fun displayGryphatriceConfirmationDialog(
purchase : Purchase,
giftedTo : String? = null
) {
MainScope().launch(ExceptionHandler.coroutine()) {
val application = (context as? HabiticaBaseApplication)
?: (context.applicationContext as? HabiticaBaseApplication) ?: return@launch
@ -536,15 +558,15 @@ class PurchaseHandler(
companion object {
private const val PENDING_GIFTS_KEY = "PENDING_GIFTS_DATED"
private var pendingGifts: MutableMap<String, Triple<Date, String, String>> = HashMap()
private var preferences: SharedPreferences? = null
private var pendingGifts : MutableMap<String, Triple<Date, String, String>> = HashMap()
private var preferences : SharedPreferences? = null
fun addGift(sku: String, userID: String, username: String) {
fun addGift(sku : String, userID : String, username : String) {
pendingGifts[sku] = Triple(Date(), userID, username)
savePendingGifts()
}
private fun removeGift(sku: String?): Triple<Date, String, String>? {
private fun removeGift(sku : String?) : Triple<Date, String, String>? {
val gift = pendingGifts.remove(sku)
savePendingGifts()
return gift
@ -561,11 +583,11 @@ class PurchaseHandler(
}
suspend fun retryUntil(
times: Int = Int.MAX_VALUE,
initialDelay: Long = 100, // 0.1 second
maxDelay: Long = 1000, // 1 second
factor: Double = 2.0,
block: suspend () -> Boolean
times : Int = Int.MAX_VALUE,
initialDelay : Long = 100, // 0.1 second
maxDelay : Long = 1000, // 1 second
factor : Double = 2.0,
block : suspend () -> Boolean
) {
var currentDelay = initialDelay
repeat(times - 1) {

View file

@ -1,9 +1,9 @@
New in 4.1.8:
-Fixed various for: item names, shared task creation, and duplicate items
-You can view, complete, assign, and add tasks to your Group Plan's shared task board!
New in 4.1.9:
-Various subscription status optimizations
-Party invites will show the senders username and profile
-You can view, complete, assign, and add tasks to your Group Plans shared task board
-Tap your name on a task screen to switch to different task boards
-More intuitive system notification settings
-New Avatar Customization interface
-Audio will be controlled by media volume now
-Task details are now tinted based on task health