mirror of
https://github.com/sudoxnym/habitica-android.git
synced 2026-04-14 11:46:32 +00:00
fix issue with subscriptions being reapplied
# Conflicts: # version.properties
This commit is contained in:
parent
e37f25bc21
commit
7a1541ac84
2 changed files with 84 additions and 62 deletions
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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 sender’s username and profile
|
||||
-You can view, complete, assign, and add tasks to your Group Plan’s 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
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue