summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorJonatan Rhodin <jonatan.rhodin@mullvad.net>2023-11-27 15:13:54 +0100
committerJonatan Rhodin <jonatan.rhodin@mullvad.net>2023-12-06 23:35:00 +0100
commitfdfd71bbd099645bd1cd99bb9b754ea6cc48800a (patch)
tree34f86ea484188da428c13cfa081ecac37b3228ba
parentbc20099e5e8a68aecbb53afdca4132b2972045dc (diff)
downloadmullvadvpn-fdfd71bbd099645bd1cd99bb9b754ea6cc48800a.tar.xz
mullvadvpn-fdfd71bbd099645bd1cd99bb9b754ea6cc48800a.zip
Add exponential back off for api verification of purchases
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/constant/PaymentConstant.kt5
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/usecase/PaymentUseCase.kt25
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/util/FlowUtils.kt35
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/util/LongExtensions.kt5
4 files changed, 64 insertions, 6 deletions
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/constant/PaymentConstant.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/constant/PaymentConstant.kt
new file mode 100644
index 0000000000..1cf1c32b2c
--- /dev/null
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/constant/PaymentConstant.kt
@@ -0,0 +1,5 @@
+package net.mullvad.mullvadvpn.constant
+
+const val VERIFICATION_MAX_ATTEMPTS = 4
+const val VERIFICATION_INITIAL_BACK_OFF_MILLISECONDS = 3000L
+const val VERIFICATION_BACK_OFF_FACTOR = 3L
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/usecase/PaymentUseCase.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/usecase/PaymentUseCase.kt
index bda53bcaf2..5a50e80401 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/usecase/PaymentUseCase.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/usecase/PaymentUseCase.kt
@@ -4,11 +4,15 @@ import android.app.Activity
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
+import net.mullvad.mullvadvpn.constant.VERIFICATION_BACK_OFF_FACTOR
+import net.mullvad.mullvadvpn.constant.VERIFICATION_INITIAL_BACK_OFF_MILLISECONDS
+import net.mullvad.mullvadvpn.constant.VERIFICATION_MAX_ATTEMPTS
import net.mullvad.mullvadvpn.lib.payment.PaymentRepository
import net.mullvad.mullvadvpn.lib.payment.model.PaymentAvailability
import net.mullvad.mullvadvpn.lib.payment.model.ProductId
import net.mullvad.mullvadvpn.lib.payment.model.PurchaseResult
import net.mullvad.mullvadvpn.lib.payment.model.VerificationResult
+import net.mullvad.mullvadvpn.util.retryWithExponentialBackOff
interface PaymentUseCase {
val paymentAvailability: Flow<PaymentAvailability?>
@@ -43,13 +47,22 @@ class PlayPaymentUseCase(private val paymentRepository: PaymentRepository) : Pay
}
override suspend fun verifyPurchases(onSuccessfulVerification: () -> Unit) {
- paymentRepository.verifyPurchases().collect {
- if (it == VerificationResult.Success) {
- // Update the payment availability after a successful verification.
- queryPaymentAvailability()
- onSuccessfulVerification()
+ paymentRepository
+ .verifyPurchases()
+ .retryWithExponentialBackOff(
+ maxAttempts = VERIFICATION_MAX_ATTEMPTS,
+ initialBackOffDelay = VERIFICATION_INITIAL_BACK_OFF_MILLISECONDS,
+ backOffDelayFactor = VERIFICATION_BACK_OFF_FACTOR
+ ) {
+ it is VerificationResult.Error
+ }
+ .collect {
+ if (it == VerificationResult.Success) {
+ // Update the payment availability after a successful verification.
+ queryPaymentAvailability()
+ onSuccessfulVerification()
+ }
}
- }
}
}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/util/FlowUtils.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/util/FlowUtils.kt
index a754b5a6c2..9be4b13b59 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/util/FlowUtils.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/util/FlowUtils.kt
@@ -5,9 +5,13 @@ import kotlin.coroutines.EmptyCoroutineContext
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.callbackFlow
+import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.flatMapLatest
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.retryWhen
import kotlinx.coroutines.flow.take
import kotlinx.coroutines.withTimeoutOrNull
import net.mullvad.mullvadvpn.lib.common.util.safeOffer
@@ -129,3 +133,34 @@ inline fun <T1, T2, T3, T4, T5, T6, T7, T8, R> combine(
suspend inline fun <T> Deferred<T>.awaitWithTimeoutOrNull(timeout: Long) =
withTimeoutOrNull(timeout) { await() }
+
+@Suppress("UNCHECKED_CAST")
+suspend inline fun <T> Flow<T>.retryWithExponentialBackOff(
+ maxAttempts: Int,
+ initialBackOffDelay: Long,
+ backOffDelayFactor: Long,
+ crossinline predicate: (T) -> Boolean,
+): Flow<T> =
+ map {
+ if (predicate(it)) {
+ throw ExceptionWrapper(it as Any)
+ }
+ it
+ }
+ .retryWhen { _, attempt ->
+ if (attempt >= maxAttempts) {
+ return@retryWhen false
+ }
+ val backOffDelay = initialBackOffDelay * backOffDelayFactor.pow(attempt.toInt())
+ delay(backOffDelay)
+ true
+ }
+ .catch {
+ if (it is ExceptionWrapper) {
+ this.emit(it.item as T)
+ } else {
+ throw it
+ }
+ }
+
+class ExceptionWrapper(val item: Any) : Throwable()
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/util/LongExtensions.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/util/LongExtensions.kt
new file mode 100644
index 0000000000..850776ca76
--- /dev/null
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/util/LongExtensions.kt
@@ -0,0 +1,5 @@
+package net.mullvad.mullvadvpn.util
+
+import kotlin.math.pow
+
+fun Long.pow(exponent: Int): Long = toDouble().pow(exponent).toLong()