Skip to content

Commit 121286d

Browse files
committed
Merge branch 'add-schedulers-from-arrow-instead-of-using-our-own-retry-droid-1014'
2 parents 88c5d62 + 8934d4b commit 121286d

File tree

16 files changed

+88
-98
lines changed

16 files changed

+88
-98
lines changed

android/app/build.gradle.kts

+1
Original file line numberDiff line numberDiff line change
@@ -329,6 +329,7 @@ dependencies {
329329
implementation(Dependencies.AndroidX.lifecycleViewmodelKtx)
330330
implementation(Dependencies.AndroidX.lifecycleRuntimeCompose)
331331
implementation(Dependencies.Arrow.core)
332+
implementation(Dependencies.Arrow.resilience)
332333
implementation(Dependencies.Compose.constrainLayout)
333334
implementation(Dependencies.Compose.foundation)
334335
implementation(Dependencies.Compose.material3)
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package net.mullvad.mullvadvpn.constant
22

3+
import kotlin.time.Duration.Companion.seconds
4+
35
const val VERIFICATION_MAX_ATTEMPTS = 4
4-
const val VERIFICATION_INITIAL_BACK_OFF_MILLISECONDS = 3000L
5-
const val VERIFICATION_BACK_OFF_FACTOR = 3L
6+
val VERIFICATION_INITIAL_BACK_OFF_DURATION = 3.seconds
7+
const val VERIFICATION_BACK_OFF_FACTOR = 3.toDouble()

android/app/src/main/kotlin/net/mullvad/mullvadvpn/usecase/PaymentUseCase.kt

+16-19
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,24 @@
11
package net.mullvad.mullvadvpn.usecase
22

33
import android.app.Activity
4+
import arrow.core.Either
5+
import arrow.core.right
6+
import arrow.resilience.Schedule
7+
import arrow.resilience.retryEither
48
import kotlinx.coroutines.delay
59
import kotlinx.coroutines.flow.Flow
610
import kotlinx.coroutines.flow.MutableStateFlow
711
import kotlinx.coroutines.flow.asStateFlow
812
import kotlinx.coroutines.flow.transform
913
import net.mullvad.mullvadvpn.constant.VERIFICATION_BACK_OFF_FACTOR
10-
import net.mullvad.mullvadvpn.constant.VERIFICATION_INITIAL_BACK_OFF_MILLISECONDS
14+
import net.mullvad.mullvadvpn.constant.VERIFICATION_INITIAL_BACK_OFF_DURATION
1115
import net.mullvad.mullvadvpn.constant.VERIFICATION_MAX_ATTEMPTS
1216
import net.mullvad.mullvadvpn.lib.payment.PaymentRepository
1317
import net.mullvad.mullvadvpn.lib.payment.model.PaymentAvailability
1418
import net.mullvad.mullvadvpn.lib.payment.model.ProductId
1519
import net.mullvad.mullvadvpn.lib.payment.model.PurchaseResult
20+
import net.mullvad.mullvadvpn.lib.payment.model.VerificationError
1621
import net.mullvad.mullvadvpn.lib.payment.model.VerificationResult
17-
import net.mullvad.mullvadvpn.util.retryWithExponentialBackOff
1822

1923
interface PaymentUseCase {
2024
val paymentAvailability: Flow<PaymentAvailability?>
@@ -26,7 +30,7 @@ interface PaymentUseCase {
2630

2731
suspend fun resetPurchaseResult()
2832

29-
suspend fun verifyPurchases(onSuccessfulVerification: () -> Unit = {})
33+
suspend fun verifyPurchases(): Either<VerificationError, VerificationResult>
3034
}
3135

3236
class PlayPaymentUseCase(private val paymentRepository: PaymentRepository) : PaymentUseCase {
@@ -60,24 +64,19 @@ class PlayPaymentUseCase(private val paymentRepository: PaymentRepository) : Pay
6064
}
6165

6266
@Suppress("ensure every public functions method is named 'invoke' with operator modifier")
63-
override suspend fun verifyPurchases(onSuccessfulVerification: () -> Unit) {
64-
paymentRepository
65-
.verifyPurchases()
66-
.retryWithExponentialBackOff(
67-
maxAttempts = VERIFICATION_MAX_ATTEMPTS,
68-
initialBackOffDelay = VERIFICATION_INITIAL_BACK_OFF_MILLISECONDS,
69-
backOffDelayFactor = VERIFICATION_BACK_OFF_FACTOR
70-
) {
71-
it is VerificationResult.Error
72-
}
73-
.collect {
67+
override suspend fun verifyPurchases() =
68+
Schedule.exponential<VerificationError>(
69+
VERIFICATION_INITIAL_BACK_OFF_DURATION,
70+
VERIFICATION_BACK_OFF_FACTOR
71+
)
72+
.and(Schedule.recurs(VERIFICATION_MAX_ATTEMPTS.toLong()))
73+
.retryEither { paymentRepository.verifyPurchases() }
74+
.onRight {
7475
if (it == VerificationResult.Success) {
7576
// Update the payment availability after a successful verification.
7677
queryPaymentAvailability()
77-
onSuccessfulVerification()
7878
}
7979
}
80-
}
8180

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

109108
@Suppress("ensure every public functions method is named 'invoke' with operator modifier")
110-
override suspend fun verifyPurchases(onSuccessfulVerification: () -> Unit) {
111-
// No op
112-
}
109+
override suspend fun verifyPurchases() = VerificationResult.NothingToVerify.right()
113110
}

android/app/src/main/kotlin/net/mullvad/mullvadvpn/util/FlowUtils.kt

-35
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,7 @@
33
package net.mullvad.mullvadvpn.util
44

55
import kotlinx.coroutines.Deferred
6-
import kotlinx.coroutines.delay
76
import kotlinx.coroutines.flow.Flow
8-
import kotlinx.coroutines.flow.catch
9-
import kotlinx.coroutines.flow.map
10-
import kotlinx.coroutines.flow.retryWhen
117

128
inline fun <T1, T2, T3, T4, T5, T6, R> combine(
139
flow: Flow<T1>,
@@ -90,34 +86,3 @@ fun <T> Deferred<T>.getOrDefault(default: T) =
9086
} catch (e: IllegalStateException) {
9187
default
9288
}
93-
94-
@Suppress("UNCHECKED_CAST")
95-
suspend inline fun <T> Flow<T>.retryWithExponentialBackOff(
96-
maxAttempts: Int,
97-
initialBackOffDelay: Long,
98-
backOffDelayFactor: Long,
99-
crossinline predicate: (T) -> Boolean,
100-
): Flow<T> =
101-
map {
102-
if (predicate(it)) {
103-
throw ExceptionWrapper(it as Any)
104-
}
105-
it
106-
}
107-
.retryWhen { _, attempt ->
108-
if (attempt >= maxAttempts) {
109-
return@retryWhen false
110-
}
111-
val backOffDelay = initialBackOffDelay * backOffDelayFactor.pow(attempt.toInt())
112-
delay(backOffDelay)
113-
true
114-
}
115-
.catch {
116-
if (it is ExceptionWrapper) {
117-
this.emit(it.item as T)
118-
} else {
119-
throw it
120-
}
121-
}
122-
123-
class ExceptionWrapper(val item: Any) : Throwable()
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package net.mullvad.mullvadvpn.util
2+
3+
import arrow.core.Either
4+
import net.mullvad.mullvadvpn.lib.payment.model.VerificationError
5+
import net.mullvad.mullvadvpn.lib.payment.model.VerificationResult
6+
7+
fun Either<VerificationError, VerificationResult>.isSuccess() =
8+
getOrNull() == VerificationResult.Success

android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/AccountViewModel.kt

+4-2
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import net.mullvad.mullvadvpn.lib.payment.model.ProductId
2424
import net.mullvad.mullvadvpn.lib.shared.AccountRepository
2525
import net.mullvad.mullvadvpn.lib.shared.DeviceRepository
2626
import net.mullvad.mullvadvpn.usecase.PaymentUseCase
27+
import net.mullvad.mullvadvpn.util.isSuccess
2728
import net.mullvad.mullvadvpn.util.toPaymentState
2829
import org.joda.time.DateTime
2930

@@ -90,8 +91,9 @@ class AccountViewModel(
9091

9192
private fun verifyPurchases() {
9293
viewModelScope.launch {
93-
paymentUseCase.verifyPurchases()
94-
updateAccountExpiry()
94+
if (paymentUseCase.verifyPurchases().isSuccess()) {
95+
updateAccountExpiry()
96+
}
9597
}
9698
}
9799

android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/ConnectViewModel.kt

+3-2
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import net.mullvad.mullvadvpn.usecase.PaymentUseCase
3232
import net.mullvad.mullvadvpn.usecase.SelectedLocationTitleUseCase
3333
import net.mullvad.mullvadvpn.util.combine
3434
import net.mullvad.mullvadvpn.util.daysFromNow
35+
import net.mullvad.mullvadvpn.util.isSuccess
3536
import net.mullvad.mullvadvpn.util.toInAddress
3637
import net.mullvad.mullvadvpn.util.toOutAddress
3738

@@ -114,8 +115,8 @@ class ConnectViewModel(
114115

115116
init {
116117
viewModelScope.launch {
117-
paymentUseCase.verifyPurchases {
118-
viewModelScope.launch { accountRepository.getAccountData() }
118+
if (paymentUseCase.verifyPurchases().isSuccess()) {
119+
accountRepository.getAccountData()
119120
}
120121
}
121122
viewModelScope.launch { deviceRepository.updateDevice() }

android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/OutOfTimeViewModel.kt

+4-2
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import net.mullvad.mullvadvpn.lib.shared.ConnectionProxy
2020
import net.mullvad.mullvadvpn.lib.shared.DeviceRepository
2121
import net.mullvad.mullvadvpn.usecase.OutOfTimeUseCase
2222
import net.mullvad.mullvadvpn.usecase.PaymentUseCase
23+
import net.mullvad.mullvadvpn.util.isSuccess
2324
import net.mullvad.mullvadvpn.util.toPaymentState
2425

2526
class OutOfTimeViewModel(
@@ -76,8 +77,9 @@ class OutOfTimeViewModel(
7677

7778
private fun verifyPurchases() {
7879
viewModelScope.launch {
79-
paymentUseCase.verifyPurchases()
80-
updateAccountExpiry()
80+
if (paymentUseCase.verifyPurchases().isSuccess()) {
81+
updateAccountExpiry()
82+
}
8183
}
8284
}
8385

android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/WelcomeViewModel.kt

+4-2
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import net.mullvad.mullvadvpn.lib.shared.AccountRepository
2121
import net.mullvad.mullvadvpn.lib.shared.ConnectionProxy
2222
import net.mullvad.mullvadvpn.lib.shared.DeviceRepository
2323
import net.mullvad.mullvadvpn.usecase.PaymentUseCase
24+
import net.mullvad.mullvadvpn.util.isSuccess
2425
import net.mullvad.mullvadvpn.util.toPaymentState
2526

2627
class WelcomeViewModel(
@@ -79,8 +80,9 @@ class WelcomeViewModel(
7980

8081
private fun verifyPurchases() {
8182
viewModelScope.launch {
82-
paymentUseCase.verifyPurchases()
83-
updateAccountExpiry()
83+
if (paymentUseCase.verifyPurchases().isSuccess()) {
84+
updateAccountExpiry()
85+
}
8486
}
8587
}
8688

android/buildSrc/src/main/kotlin/Dependencies.kt

+1
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ object Dependencies {
4646
const val core = "io.arrow-kt:arrow-core:${Versions.Arrow.base}"
4747
const val optics = "io.arrow-kt:arrow-optics:${Versions.Arrow.base}"
4848
const val opticsKsp = "io.arrow-kt:arrow-optics-ksp-plugin:${Versions.Arrow.base}"
49+
const val resilience = "io.arrow-kt:arrow-resilience:${Versions.Arrow.base}"
4950
}
5051

5152
object Compose {

android/gradle/verification-metadata.xml

+16
Original file line numberDiff line numberDiff line change
@@ -3266,6 +3266,22 @@
32663266
<sha256 value="1458ec6f2cff3f22df3f57ff712e93d15c2540ee242243e167ed6d9f106bfcf8" origin="Generated by Gradle"/>
32673267
</artifact>
32683268
</component>
3269+
<component group="io.arrow-kt" name="arrow-resilience" version="1.2.3">
3270+
<artifact name="arrow-resilience-1.2.3.module">
3271+
<sha256 value="4ee897e34ac212dba6d1c8fcbb06192d25a3f4ba12b2893f37ca06bd928071ae" origin="Generated by Gradle"/>
3272+
</artifact>
3273+
<artifact name="arrow-resilience-metadata-1.2.3.jar">
3274+
<sha256 value="289d20131b1194d36d74dbe48dac5a274ab0e7f76383565547048f549e9ddf1f" origin="Generated by Gradle"/>
3275+
</artifact>
3276+
</component>
3277+
<component group="io.arrow-kt" name="arrow-resilience-jvm" version="1.2.3">
3278+
<artifact name="arrow-resilience-jvm-1.2.3.jar">
3279+
<sha256 value="002904da95b1ea17b9d5119920e8d76dbf43ce3cb5a12bb4a161b1978be48bab" origin="Generated by Gradle"/>
3280+
</artifact>
3281+
<artifact name="arrow-resilience-jvm-1.2.3.module">
3282+
<sha256 value="9559b8d788d46f9e8817e7f74bb6b72fdea8996adea6c6f7ac7887bd67856d77" origin="Generated by Gradle"/>
3283+
</artifact>
3284+
</component>
32693285
<component group="io.github.davidburstrom.contester" name="contester-breakpoint" version="0.2.0">
32703286
<artifact name="contester-breakpoint-0.2.0.jar">
32713287
<sha256 value="672cbebb5d45a72b35dd81fd6127e187451bb6fb7fba35315bbdf2f57cfce835" origin="Generated by Gradle"/>

android/lib/billing/src/main/kotlin/net/mullvad/mullvadvpn/lib/billing/BillingPaymentRepository.kt

+15-20
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
package net.mullvad.mullvadvpn.lib.billing
22

33
import android.app.Activity
4+
import arrow.core.Either
5+
import arrow.core.raise.either
6+
import arrow.core.raise.ensure
47
import com.android.billingclient.api.BillingClient.BillingResponseCode
58
import com.android.billingclient.api.Purchase
69
import kotlinx.coroutines.flow.Flow
@@ -22,6 +25,7 @@ import net.mullvad.mullvadvpn.lib.payment.ProductIds
2225
import net.mullvad.mullvadvpn.lib.payment.model.PaymentAvailability
2326
import net.mullvad.mullvadvpn.lib.payment.model.ProductId
2427
import net.mullvad.mullvadvpn.lib.payment.model.PurchaseResult
28+
import net.mullvad.mullvadvpn.lib.payment.model.VerificationError
2529
import net.mullvad.mullvadvpn.lib.payment.model.VerificationResult
2630

2731
class BillingPaymentRepository(
@@ -129,28 +133,19 @@ class BillingPaymentRepository(
129133
}
130134
}
131135

132-
override fun verifyPurchases(): Flow<VerificationResult> = flow {
133-
emit(VerificationResult.FetchingUnfinishedPurchases)
136+
override suspend fun verifyPurchases(): Either<VerificationError, VerificationResult> = either {
134137
val purchasesResult = billingRepository.queryPurchases()
135-
when (purchasesResult.responseCode()) {
136-
BillingResponseCode.OK -> {
137-
val purchases = purchasesResult.nonPendingPurchases()
138-
if (purchases.isNotEmpty()) {
139-
emit(VerificationResult.VerificationStarted)
140-
emit(
141-
verifyPurchase(purchases.first())
142-
.fold(
143-
{ VerificationResult.Error.VerificationError(null) },
144-
{ VerificationResult.Success }
145-
)
146-
)
147-
} else {
148-
emit(VerificationResult.NothingToVerify)
149-
}
150-
}
151-
else ->
152-
emit(VerificationResult.Error.BillingError(purchasesResult.toBillingException()))
138+
ensure(purchasesResult.responseCode() == BillingResponseCode.OK) {
139+
VerificationError.BillingError(purchasesResult.toBillingException())
140+
}
141+
val purchases = purchasesResult.nonPendingPurchases()
142+
if (purchases.isEmpty()) {
143+
return@either VerificationResult.NothingToVerify
153144
}
145+
verifyPurchase(purchases.first())
146+
.mapLeft { VerificationError.PlayVerificationError }
147+
.map { VerificationResult.Success }
148+
.bind()
154149
}
155150

156151
private suspend fun initialisePurchase() = playPurchaseRepository.initializePlayPurchase()

android/lib/payment/build.gradle.kts

+1
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ android {
3939
}
4040

4141
dependencies {
42+
implementation(Dependencies.Arrow.core)
4243
implementation(Dependencies.Kotlin.stdlib)
4344
implementation(Dependencies.KotlinX.coroutinesAndroid)
4445
}

android/lib/payment/src/main/kotlin/net/mullvad/mullvadvpn/lib/payment/PaymentRepository.kt

+3-1
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
package net.mullvad.mullvadvpn.lib.payment
22

33
import android.app.Activity
4+
import arrow.core.Either
45
import kotlinx.coroutines.flow.Flow
56
import net.mullvad.mullvadvpn.lib.payment.model.PaymentAvailability
67
import net.mullvad.mullvadvpn.lib.payment.model.ProductId
78
import net.mullvad.mullvadvpn.lib.payment.model.PurchaseResult
9+
import net.mullvad.mullvadvpn.lib.payment.model.VerificationError
810
import net.mullvad.mullvadvpn.lib.payment.model.VerificationResult
911

1012
interface PaymentRepository {
@@ -14,7 +16,7 @@ interface PaymentRepository {
1416
activityProvider: () -> Activity
1517
): Flow<PurchaseResult>
1618

17-
fun verifyPurchases(): Flow<VerificationResult>
19+
suspend fun verifyPurchases(): Either<VerificationError, VerificationResult>
1820

1921
fun queryPaymentAvailability(): Flow<PaymentAvailability>
2022
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package net.mullvad.mullvadvpn.lib.payment.model
2+
3+
sealed interface VerificationError {
4+
data class BillingError(val exception: Throwable) : VerificationError
5+
6+
data object PlayVerificationError : VerificationError
7+
}
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,7 @@
11
package net.mullvad.mullvadvpn.lib.payment.model
22

3-
sealed interface VerificationResult {
4-
data object FetchingUnfinishedPurchases : VerificationResult
5-
6-
data object VerificationStarted : VerificationResult
7-
8-
// No verification was needed as there is no purchases to verify
3+
interface VerificationResult {
94
data object NothingToVerify : VerificationResult
105

116
data object Success : VerificationResult
12-
13-
// Generic error, add more cases as needed
14-
sealed interface Error : VerificationResult {
15-
data class BillingError(val exception: Throwable?) : Error
16-
17-
data class VerificationError(val exception: Throwable?) : Error
18-
}
197
}

0 commit comments

Comments
 (0)