Skip to content

Commit

Permalink
Merge branch 'add-exponential-back-off-to-payment-verification-droid-…
Browse files Browse the repository at this point in the history
…534'
  • Loading branch information
Pururun committed Dec 6, 2023
2 parents bc20099 + 9967b12 commit 74340e9
Show file tree
Hide file tree
Showing 7 changed files with 73 additions and 12 deletions.
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -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?>
Expand Down Expand Up @@ -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()
}
}
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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()
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package net.mullvad.mullvadvpn.util

import kotlin.math.pow

fun Long.pow(exponent: Int): Long = toDouble().pow(exponent).toLong()
Original file line number Diff line number Diff line change
Expand Up @@ -98,12 +98,13 @@ class AccountViewModel(
fun onClosePurchaseResultDialog(success: Boolean) {
// We are closing the dialog without any action, this can happen either if an error occurred
// during the purchase or the purchase ended successfully.
// In those cases we want to update the both the payment availability and the account
// expiry.
// If the payment was successful we want to update the account expiry. If not successful we
// should check payment availability and verify any purchases to handle potential errors.
if (success) {
updateAccountExpiry()
} else {
fetchPaymentAvailability()
verifyPurchases() // Attempt to verify again
}
viewModelScope.launch {
paymentUseCase.resetPurchaseResult() // So that we do not show the dialog again.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -128,13 +128,14 @@ class OutOfTimeViewModel(
fun onClosePurchaseResultDialog(success: Boolean) {
// We are closing the dialog without any action, this can happen either if an error occurred
// during the purchase or the purchase ended successfully.
// In those cases we want to update the both the payment availability and the account
// expiry.
// If the payment was successful we want to update the account expiry. If not successful we
// should check payment availability and verify any purchases to handle potential errors.
if (success) {
updateAccountExpiry()
_uiSideEffect.tryEmit(UiSideEffect.OpenConnectScreen)
} else {
fetchPaymentAvailability()
verifyPurchases() // Attempt to verify again
}
viewModelScope.launch {
paymentUseCase.resetPurchaseResult() // So that we do not show the dialog again.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -131,13 +131,14 @@ class WelcomeViewModel(
fun onClosePurchaseResultDialog(success: Boolean) {
// We are closing the dialog without any action, this can happen either if an error occurred
// during the purchase or the purchase ended successfully.
// In those cases we want to update the both the payment availability and the account
// expiry.
// If the payment was successful we want to update the account expiry. If not successful we
// should check payment availability and verify any purchases to handle potential errors.
if (success) {
updateAccountExpiry()
_uiSideEffect.tryEmit(UiSideEffect.OpenConnectScreen)
} else {
fetchPaymentAvailability()
verifyPurchases() // Attempt to verify again
}
viewModelScope.launch {
paymentUseCase.resetPurchaseResult() // So that we do not show the dialog again.
Expand Down

0 comments on commit 74340e9

Please sign in to comment.