Skip to content

Commit

Permalink
Merge branch 'add-schedulers-from-arrow-instead-of-using-our-own-retr…
Browse files Browse the repository at this point in the history
…y-droid-1014'
  • Loading branch information
Pururun committed Jun 19, 2024
2 parents 88c5d62 + 8934d4b commit 121286d
Show file tree
Hide file tree
Showing 16 changed files with 88 additions and 98 deletions.
1 change: 1 addition & 0 deletions android/app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -329,6 +329,7 @@ dependencies {
implementation(Dependencies.AndroidX.lifecycleViewmodelKtx)
implementation(Dependencies.AndroidX.lifecycleRuntimeCompose)
implementation(Dependencies.Arrow.core)
implementation(Dependencies.Arrow.resilience)
implementation(Dependencies.Compose.constrainLayout)
implementation(Dependencies.Compose.foundation)
implementation(Dependencies.Compose.material3)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package net.mullvad.mullvadvpn.constant

import kotlin.time.Duration.Companion.seconds

const val VERIFICATION_MAX_ATTEMPTS = 4
const val VERIFICATION_INITIAL_BACK_OFF_MILLISECONDS = 3000L
const val VERIFICATION_BACK_OFF_FACTOR = 3L
val VERIFICATION_INITIAL_BACK_OFF_DURATION = 3.seconds
const val VERIFICATION_BACK_OFF_FACTOR = 3.toDouble()
Original file line number Diff line number Diff line change
@@ -1,20 +1,24 @@
package net.mullvad.mullvadvpn.usecase

import android.app.Activity
import arrow.core.Either
import arrow.core.right
import arrow.resilience.Schedule
import arrow.resilience.retryEither
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.transform
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_INITIAL_BACK_OFF_DURATION
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.VerificationError
import net.mullvad.mullvadvpn.lib.payment.model.VerificationResult
import net.mullvad.mullvadvpn.util.retryWithExponentialBackOff

interface PaymentUseCase {
val paymentAvailability: Flow<PaymentAvailability?>
Expand All @@ -26,7 +30,7 @@ interface PaymentUseCase {

suspend fun resetPurchaseResult()

suspend fun verifyPurchases(onSuccessfulVerification: () -> Unit = {})
suspend fun verifyPurchases(): Either<VerificationError, VerificationResult>
}

class PlayPaymentUseCase(private val paymentRepository: PaymentRepository) : PaymentUseCase {
Expand Down Expand Up @@ -60,24 +64,19 @@ class PlayPaymentUseCase(private val paymentRepository: PaymentRepository) : Pay
}

@Suppress("ensure every public functions method is named 'invoke' with operator modifier")
override suspend fun verifyPurchases(onSuccessfulVerification: () -> Unit) {
paymentRepository
.verifyPurchases()
.retryWithExponentialBackOff(
maxAttempts = VERIFICATION_MAX_ATTEMPTS,
initialBackOffDelay = VERIFICATION_INITIAL_BACK_OFF_MILLISECONDS,
backOffDelayFactor = VERIFICATION_BACK_OFF_FACTOR
) {
it is VerificationResult.Error
}
.collect {
override suspend fun verifyPurchases() =
Schedule.exponential<VerificationError>(
VERIFICATION_INITIAL_BACK_OFF_DURATION,
VERIFICATION_BACK_OFF_FACTOR
)
.and(Schedule.recurs(VERIFICATION_MAX_ATTEMPTS.toLong()))
.retryEither { paymentRepository.verifyPurchases() }
.onRight {
if (it == VerificationResult.Success) {
// Update the payment availability after a successful verification.
queryPaymentAvailability()
onSuccessfulVerification()
}
}
}

private fun PurchaseResult?.shouldDelayLoading() =
this is PurchaseResult.FetchingProducts || this is PurchaseResult.VerificationStarted
Expand Down Expand Up @@ -107,7 +106,5 @@ class EmptyPaymentUseCase : PaymentUseCase {
}

@Suppress("ensure every public functions method is named 'invoke' with operator modifier")
override suspend fun verifyPurchases(onSuccessfulVerification: () -> Unit) {
// No op
}
override suspend fun verifyPurchases() = VerificationResult.NothingToVerify.right()
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,7 @@
package net.mullvad.mullvadvpn.util

import kotlinx.coroutines.Deferred
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.retryWhen

inline fun <T1, T2, T3, T4, T5, T6, R> combine(
flow: Flow<T1>,
Expand Down Expand Up @@ -90,34 +86,3 @@ fun <T> Deferred<T>.getOrDefault(default: T) =
} catch (e: IllegalStateException) {
default
}

@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,8 @@
package net.mullvad.mullvadvpn.util

import arrow.core.Either
import net.mullvad.mullvadvpn.lib.payment.model.VerificationError
import net.mullvad.mullvadvpn.lib.payment.model.VerificationResult

fun Either<VerificationError, VerificationResult>.isSuccess() =
getOrNull() == VerificationResult.Success
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import net.mullvad.mullvadvpn.lib.payment.model.ProductId
import net.mullvad.mullvadvpn.lib.shared.AccountRepository
import net.mullvad.mullvadvpn.lib.shared.DeviceRepository
import net.mullvad.mullvadvpn.usecase.PaymentUseCase
import net.mullvad.mullvadvpn.util.isSuccess
import net.mullvad.mullvadvpn.util.toPaymentState
import org.joda.time.DateTime

Expand Down Expand Up @@ -90,8 +91,9 @@ class AccountViewModel(

private fun verifyPurchases() {
viewModelScope.launch {
paymentUseCase.verifyPurchases()
updateAccountExpiry()
if (paymentUseCase.verifyPurchases().isSuccess()) {
updateAccountExpiry()
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import net.mullvad.mullvadvpn.usecase.PaymentUseCase
import net.mullvad.mullvadvpn.usecase.SelectedLocationTitleUseCase
import net.mullvad.mullvadvpn.util.combine
import net.mullvad.mullvadvpn.util.daysFromNow
import net.mullvad.mullvadvpn.util.isSuccess
import net.mullvad.mullvadvpn.util.toInAddress
import net.mullvad.mullvadvpn.util.toOutAddress

Expand Down Expand Up @@ -114,8 +115,8 @@ class ConnectViewModel(

init {
viewModelScope.launch {
paymentUseCase.verifyPurchases {
viewModelScope.launch { accountRepository.getAccountData() }
if (paymentUseCase.verifyPurchases().isSuccess()) {
accountRepository.getAccountData()
}
}
viewModelScope.launch { deviceRepository.updateDevice() }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import net.mullvad.mullvadvpn.lib.shared.ConnectionProxy
import net.mullvad.mullvadvpn.lib.shared.DeviceRepository
import net.mullvad.mullvadvpn.usecase.OutOfTimeUseCase
import net.mullvad.mullvadvpn.usecase.PaymentUseCase
import net.mullvad.mullvadvpn.util.isSuccess
import net.mullvad.mullvadvpn.util.toPaymentState

class OutOfTimeViewModel(
Expand Down Expand Up @@ -76,8 +77,9 @@ class OutOfTimeViewModel(

private fun verifyPurchases() {
viewModelScope.launch {
paymentUseCase.verifyPurchases()
updateAccountExpiry()
if (paymentUseCase.verifyPurchases().isSuccess()) {
updateAccountExpiry()
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import net.mullvad.mullvadvpn.lib.shared.AccountRepository
import net.mullvad.mullvadvpn.lib.shared.ConnectionProxy
import net.mullvad.mullvadvpn.lib.shared.DeviceRepository
import net.mullvad.mullvadvpn.usecase.PaymentUseCase
import net.mullvad.mullvadvpn.util.isSuccess
import net.mullvad.mullvadvpn.util.toPaymentState

class WelcomeViewModel(
Expand Down Expand Up @@ -79,8 +80,9 @@ class WelcomeViewModel(

private fun verifyPurchases() {
viewModelScope.launch {
paymentUseCase.verifyPurchases()
updateAccountExpiry()
if (paymentUseCase.verifyPurchases().isSuccess()) {
updateAccountExpiry()
}
}
}

Expand Down
1 change: 1 addition & 0 deletions android/buildSrc/src/main/kotlin/Dependencies.kt
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ object Dependencies {
const val core = "io.arrow-kt:arrow-core:${Versions.Arrow.base}"
const val optics = "io.arrow-kt:arrow-optics:${Versions.Arrow.base}"
const val opticsKsp = "io.arrow-kt:arrow-optics-ksp-plugin:${Versions.Arrow.base}"
const val resilience = "io.arrow-kt:arrow-resilience:${Versions.Arrow.base}"
}

object Compose {
Expand Down
16 changes: 16 additions & 0 deletions android/gradle/verification-metadata.xml
Original file line number Diff line number Diff line change
Expand Up @@ -3266,6 +3266,22 @@
<sha256 value="1458ec6f2cff3f22df3f57ff712e93d15c2540ee242243e167ed6d9f106bfcf8" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="io.arrow-kt" name="arrow-resilience" version="1.2.3">
<artifact name="arrow-resilience-1.2.3.module">
<sha256 value="4ee897e34ac212dba6d1c8fcbb06192d25a3f4ba12b2893f37ca06bd928071ae" origin="Generated by Gradle"/>
</artifact>
<artifact name="arrow-resilience-metadata-1.2.3.jar">
<sha256 value="289d20131b1194d36d74dbe48dac5a274ab0e7f76383565547048f549e9ddf1f" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="io.arrow-kt" name="arrow-resilience-jvm" version="1.2.3">
<artifact name="arrow-resilience-jvm-1.2.3.jar">
<sha256 value="002904da95b1ea17b9d5119920e8d76dbf43ce3cb5a12bb4a161b1978be48bab" origin="Generated by Gradle"/>
</artifact>
<artifact name="arrow-resilience-jvm-1.2.3.module">
<sha256 value="9559b8d788d46f9e8817e7f74bb6b72fdea8996adea6c6f7ac7887bd67856d77" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="io.github.davidburstrom.contester" name="contester-breakpoint" version="0.2.0">
<artifact name="contester-breakpoint-0.2.0.jar">
<sha256 value="672cbebb5d45a72b35dd81fd6127e187451bb6fb7fba35315bbdf2f57cfce835" origin="Generated by Gradle"/>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package net.mullvad.mullvadvpn.lib.billing

import android.app.Activity
import arrow.core.Either
import arrow.core.raise.either
import arrow.core.raise.ensure
import com.android.billingclient.api.BillingClient.BillingResponseCode
import com.android.billingclient.api.Purchase
import kotlinx.coroutines.flow.Flow
Expand All @@ -22,6 +25,7 @@ import net.mullvad.mullvadvpn.lib.payment.ProductIds
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.VerificationError
import net.mullvad.mullvadvpn.lib.payment.model.VerificationResult

class BillingPaymentRepository(
Expand Down Expand Up @@ -129,28 +133,19 @@ class BillingPaymentRepository(
}
}

override fun verifyPurchases(): Flow<VerificationResult> = flow {
emit(VerificationResult.FetchingUnfinishedPurchases)
override suspend fun verifyPurchases(): Either<VerificationError, VerificationResult> = either {
val purchasesResult = billingRepository.queryPurchases()
when (purchasesResult.responseCode()) {
BillingResponseCode.OK -> {
val purchases = purchasesResult.nonPendingPurchases()
if (purchases.isNotEmpty()) {
emit(VerificationResult.VerificationStarted)
emit(
verifyPurchase(purchases.first())
.fold(
{ VerificationResult.Error.VerificationError(null) },
{ VerificationResult.Success }
)
)
} else {
emit(VerificationResult.NothingToVerify)
}
}
else ->
emit(VerificationResult.Error.BillingError(purchasesResult.toBillingException()))
ensure(purchasesResult.responseCode() == BillingResponseCode.OK) {
VerificationError.BillingError(purchasesResult.toBillingException())
}
val purchases = purchasesResult.nonPendingPurchases()
if (purchases.isEmpty()) {
return@either VerificationResult.NothingToVerify
}
verifyPurchase(purchases.first())
.mapLeft { VerificationError.PlayVerificationError }
.map { VerificationResult.Success }
.bind()
}

private suspend fun initialisePurchase() = playPurchaseRepository.initializePlayPurchase()
Expand Down
1 change: 1 addition & 0 deletions android/lib/payment/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ android {
}

dependencies {
implementation(Dependencies.Arrow.core)
implementation(Dependencies.Kotlin.stdlib)
implementation(Dependencies.KotlinX.coroutinesAndroid)
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
package net.mullvad.mullvadvpn.lib.payment

import android.app.Activity
import arrow.core.Either
import kotlinx.coroutines.flow.Flow
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.VerificationError
import net.mullvad.mullvadvpn.lib.payment.model.VerificationResult

interface PaymentRepository {
Expand All @@ -14,7 +16,7 @@ interface PaymentRepository {
activityProvider: () -> Activity
): Flow<PurchaseResult>

fun verifyPurchases(): Flow<VerificationResult>
suspend fun verifyPurchases(): Either<VerificationError, VerificationResult>

fun queryPaymentAvailability(): Flow<PaymentAvailability>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package net.mullvad.mullvadvpn.lib.payment.model

sealed interface VerificationError {
data class BillingError(val exception: Throwable) : VerificationError

data object PlayVerificationError : VerificationError
}
Original file line number Diff line number Diff line change
@@ -1,19 +1,7 @@
package net.mullvad.mullvadvpn.lib.payment.model

sealed interface VerificationResult {
data object FetchingUnfinishedPurchases : VerificationResult

data object VerificationStarted : VerificationResult

// No verification was needed as there is no purchases to verify
interface VerificationResult {
data object NothingToVerify : VerificationResult

data object Success : VerificationResult

// Generic error, add more cases as needed
sealed interface Error : VerificationResult {
data class BillingError(val exception: Throwable?) : Error

data class VerificationError(val exception: Throwable?) : Error
}
}

0 comments on commit 121286d

Please sign in to comment.