Skip to content

Commit 114d586

Browse files
committed
Merge branch 'add-voucher-error-message-droid-923'
2 parents 766cdae + e12c64a commit 114d586

File tree

10 files changed

+124
-14
lines changed

10 files changed

+124
-14
lines changed

android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/dialog/RedeemVoucherDialog.kt

+13
Original file line numberDiff line numberDiff line change
@@ -274,10 +274,23 @@ private fun EnterVoucherBody(
274274
)
275275
}
276276
}
277+
if (
278+
state.voucherState is VoucherDialogState.Error &&
279+
state.voucherState.error is RedeemVoucherError.EnteredAccountNumber
280+
) {
281+
Text(
282+
modifier = Modifier.padding(top = Dimens.smallPadding),
283+
text = stringResource(id = R.string.voucher_is_account_number),
284+
color = MaterialTheme.colorScheme.onPrimary,
285+
style = MaterialTheme.typography.bodySmall
286+
)
287+
}
277288
}
278289

279290
private fun RedeemVoucherError.message(): Int =
280291
when (this) {
292+
RedeemVoucherError.TooShortVoucher,
293+
RedeemVoucherError.EnteredAccountNumber,
281294
RedeemVoucherError.InvalidVoucher -> R.string.invalid_voucher
282295
RedeemVoucherError.VoucherAlreadyUsed -> R.string.voucher_already_used
283296
RedeemVoucherError.RpcError,

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

+18-3
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package net.mullvad.mullvadvpn.viewmodel
22

33
import androidx.lifecycle.ViewModel
44
import androidx.lifecycle.viewModelScope
5+
import arrow.core.raise.either
56
import kotlinx.coroutines.flow.MutableStateFlow
67
import kotlinx.coroutines.flow.SharingStarted
78
import kotlinx.coroutines.flow.combine
@@ -11,7 +12,9 @@ import kotlinx.coroutines.launch
1112
import net.mullvad.mullvadvpn.compose.state.VoucherDialogState
1213
import net.mullvad.mullvadvpn.compose.state.VoucherDialogUiState
1314
import net.mullvad.mullvadvpn.constant.VOUCHER_LENGTH
15+
import net.mullvad.mullvadvpn.lib.model.ParseVoucherCodeError
1416
import net.mullvad.mullvadvpn.lib.model.RedeemVoucherError
17+
import net.mullvad.mullvadvpn.lib.model.VoucherCode
1518
import net.mullvad.mullvadvpn.lib.shared.VoucherRepository
1619
import net.mullvad.mullvadvpn.util.VoucherRegexHelper
1720

@@ -26,11 +29,23 @@ class VoucherDialogViewModel(private val voucherRepository: VoucherRepository) :
2629
}
2730
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), VoucherDialogUiState.INITIAL)
2831

29-
fun onRedeem(voucherCode: String) {
32+
fun onRedeem(voucherInput: String) {
3033
vmState.update { VoucherDialogState.Verifying }
3134
viewModelScope.launch {
32-
voucherRepository
33-
.submitVoucher(voucherCode)
35+
either {
36+
val voucherCode =
37+
VoucherCode.fromString(voucherInput)
38+
.mapLeft {
39+
when (it) {
40+
is ParseVoucherCodeError.AllDigit ->
41+
RedeemVoucherError.EnteredAccountNumber
42+
is ParseVoucherCodeError.TooShort ->
43+
RedeemVoucherError.TooShortVoucher
44+
}
45+
}
46+
.bind()
47+
voucherRepository.submitVoucher(voucherCode).bind()
48+
}
3449
.fold(
3550
{ error -> setError(error) },
3651
{ success -> handleAddedTime(success.timeAdded) }

android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/VoucherDialogViewModelTest.kt

+13-8
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import net.mullvad.mullvadvpn.compose.state.VoucherDialogState
1616
import net.mullvad.mullvadvpn.lib.common.test.TestCoroutineRule
1717
import net.mullvad.mullvadvpn.lib.model.RedeemVoucherError
1818
import net.mullvad.mullvadvpn.lib.model.RedeemVoucherSuccess
19+
import net.mullvad.mullvadvpn.lib.model.VoucherCode
1920
import net.mullvad.mullvadvpn.lib.shared.VoucherRepository
2021
import org.joda.time.DateTime
2122
import org.junit.jupiter.api.AfterEach
@@ -45,19 +46,20 @@ class VoucherDialogViewModelTest {
4546
@Test
4647
fun `ensure onRedeem invokes submit on VoucherRedeemer with same voucher code`() = runTest {
4748
val voucher = DUMMY_INVALID_VOUCHER
49+
val parsedVoucher = VoucherCode.fromString(voucher).getOrNull()!!
4850

4951
// Arrange
5052
val timeAdded = 0L
5153
val newExpiry = DateTime()
52-
coEvery { mockVoucherRepository.submitVoucher(voucher) } returns
54+
coEvery { mockVoucherRepository.submitVoucher(parsedVoucher) } returns
5355
RedeemVoucherSuccess(timeAdded, newExpiry).right()
5456

5557
// Act
5658
assertIs<VoucherDialogState.Default>(viewModel.uiState.value.voucherState)
5759
viewModel.onRedeem(voucher)
5860

5961
// Assert
60-
coVerify(exactly = 1) { mockVoucherRepository.submitVoucher(voucher) }
62+
coVerify(exactly = 1) { mockVoucherRepository.submitVoucher(parsedVoucher) }
6163
}
6264

6365
@Test
@@ -66,8 +68,9 @@ class VoucherDialogViewModelTest {
6668

6769
// Arrange
6870
every { mockVoucherSubmission.timeAdded } returns 0
69-
coEvery { mockVoucherRepository.submitVoucher(voucher) } returns
70-
RedeemVoucherError.InvalidVoucher.left()
71+
coEvery {
72+
mockVoucherRepository.submitVoucher(VoucherCode.fromString(voucher).getOrNull()!!)
73+
} returns RedeemVoucherError.InvalidVoucher.left()
7174

7275
// Act, Assert
7376
viewModel.uiState.test {
@@ -84,8 +87,9 @@ class VoucherDialogViewModelTest {
8487

8588
// Arrange
8689
every { mockVoucherSubmission.timeAdded } returns 0
87-
coEvery { mockVoucherRepository.submitVoucher(voucher) } returns
88-
RedeemVoucherSuccess(0, DateTime()).right()
90+
coEvery {
91+
mockVoucherRepository.submitVoucher(VoucherCode.fromString(voucher).getOrNull()!!)
92+
} returns RedeemVoucherSuccess(0, DateTime()).right()
8993

9094
// Act, Assert
9195
viewModel.uiState.test {
@@ -102,8 +106,9 @@ class VoucherDialogViewModelTest {
102106

103107
// Arrange
104108
every { mockVoucherSubmission.timeAdded } returns 0
105-
coEvery { mockVoucherRepository.submitVoucher(voucher) } returns
106-
RedeemVoucherError.VoucherAlreadyUsed.left()
109+
coEvery {
110+
mockVoucherRepository.submitVoucher(VoucherCode.fromString(voucher).getOrNull()!!)
111+
} returns RedeemVoucherError.VoucherAlreadyUsed.left()
107112

108113
// Act, Assert
109114
viewModel.uiState.test {

android/lib/daemon-grpc/src/main/kotlin/net/mullvad/mullvadvpn/lib/daemon/grpc/ManagementService.kt

+5-2
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,7 @@ import net.mullvad.mullvadvpn.lib.model.UnknownApiAccessMethodError
116116
import net.mullvad.mullvadvpn.lib.model.UnknownCustomListError
117117
import net.mullvad.mullvadvpn.lib.model.UpdateApiAccessMethodError
118118
import net.mullvad.mullvadvpn.lib.model.UpdateCustomListError
119+
import net.mullvad.mullvadvpn.lib.model.VoucherCode
119120
import net.mullvad.mullvadvpn.lib.model.WebsiteAuthToken
120121
import net.mullvad.mullvadvpn.lib.model.WireguardConstraints as ModelWireguardConstraints
121122
import net.mullvad.mullvadvpn.lib.model.WireguardEndpointData as ModelWireguardEndpointData
@@ -575,8 +576,10 @@ class ManagementService(
575576
.mapLeft(SetWireguardConstraintsError::Unknown)
576577
.mapEmpty()
577578

578-
suspend fun submitVoucher(voucher: String): Either<RedeemVoucherError, RedeemVoucherSuccess> =
579-
Either.catch { grpc.submitVoucher(StringValue.of(voucher)).toDomain() }
579+
suspend fun submitVoucher(
580+
voucher: VoucherCode
581+
): Either<RedeemVoucherError, RedeemVoucherSuccess> =
582+
Either.catch { grpc.submitVoucher(StringValue.of(voucher.value)).toDomain() }
580583
.mapLeftStatus {
581584
when (it.status.code) {
582585
Status.Code.INVALID_ARGUMENT,

android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/RedeemVoucherError.kt

+4
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@ sealed class RedeemVoucherError {
55

66
data object VoucherAlreadyUsed : RedeemVoucherError()
77

8+
data object TooShortVoucher : RedeemVoucherError()
9+
10+
data object EnteredAccountNumber : RedeemVoucherError()
11+
812
data object RpcError : RedeemVoucherError()
913

1014
data class Unknown(val error: Throwable) : RedeemVoucherError()
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package net.mullvad.mullvadvpn.lib.model
2+
3+
import arrow.core.Either
4+
import arrow.core.raise.either
5+
import arrow.core.raise.ensure
6+
7+
@JvmInline
8+
value class VoucherCode private constructor(val value: String) {
9+
10+
companion object {
11+
// Parsing reference:
12+
// <services-repository>/services/docs/adr/0018-distinguish-voucher-codes-from-account-numbers.md
13+
fun fromString(value: String): Either<ParseVoucherCodeError, VoucherCode> = either {
14+
val trimmedValue = value.trim()
15+
ensure(trimmedValue.length >= MIN_VOUCHER_LENGTH) {
16+
ParseVoucherCodeError.TooShort(trimmedValue)
17+
}
18+
ensure(!value.all { it.isDigit() }) { ParseVoucherCodeError.AllDigit(trimmedValue) }
19+
VoucherCode(trimmedValue)
20+
}
21+
22+
const val MIN_VOUCHER_LENGTH = 16
23+
}
24+
}
25+
26+
sealed interface ParseVoucherCodeError {
27+
28+
data class AllDigit(val value: String) : ParseVoucherCodeError
29+
30+
data class TooShort(val value: String) : ParseVoucherCodeError
31+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package net.mullvad.mullvadvpn.lib.model
2+
3+
import kotlin.test.assertEquals
4+
import kotlin.test.assertTrue
5+
import org.junit.jupiter.api.Test
6+
7+
class VoucherCodeTest {
8+
@Test
9+
fun `parsing a too short voucher code should return TooShort`() {
10+
val input = "mycode"
11+
val result = VoucherCode.fromString(input)
12+
13+
assertTrue(result.isLeft())
14+
assertEquals(ParseVoucherCodeError.TooShort(input), result.leftOrNull())
15+
}
16+
17+
@Test
18+
fun `numbers only should not be allowed`() {
19+
val input = "1234123412341234"
20+
val result = VoucherCode.fromString(input)
21+
22+
assertTrue(result.isLeft())
23+
assertEquals(ParseVoucherCodeError.AllDigit(input), result.leftOrNull())
24+
}
25+
26+
@Test
27+
fun `number only input when too short should return TooShort`() {
28+
val input = "123412341234"
29+
val result = VoucherCode.fromString(input)
30+
31+
assertTrue(result.isLeft())
32+
assertEquals(ParseVoucherCodeError.TooShort(input), result.leftOrNull())
33+
}
34+
}

android/lib/resource/src/main/res/values/strings.xml

+1
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
<string name="redeem">Redeem</string>
3434
<string name="invalid_voucher">Voucher code is invalid.</string>
3535
<string name="voucher_already_used">Voucher code has already been used.</string>
36+
<string name="voucher_is_account_number">It looks like you’ve entered an account number instead of a voucher code. If you would like to change the active account, please log out first.</string>
3637
<string name="error_occurred">An error occurred.</string>
3738
<string name="settings">Settings</string>
3839
<string name="no_internet_connection">No internet connection</string>

android/lib/shared/src/main/kotlin/net/mullvad/mullvadvpn/lib/shared/VoucherRepository.kt

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
package net.mullvad.mullvadvpn.lib.shared
22

33
import net.mullvad.mullvadvpn.lib.daemon.grpc.ManagementService
4+
import net.mullvad.mullvadvpn.lib.model.VoucherCode
45

56
class VoucherRepository(
67
private val managementService: ManagementService,
78
private val accountRepository: AccountRepository
89
) {
9-
suspend fun submitVoucher(voucher: String) =
10+
suspend fun submitVoucher(voucher: VoucherCode) =
1011
managementService.submitVoucher(voucher).onRight {
1112
accountRepository.onVoucherRedeemed(it.newExpiryDate)
1213
}

gui/locales/messages.pot

+3
Original file line numberDiff line numberDiff line change
@@ -2318,6 +2318,9 @@ msgstr ""
23182318
msgid "Invalid or missing value \"%s\""
23192319
msgstr ""
23202320

2321+
msgid "It looks like you’ve entered an account number instead of a voucher code. If you would like to change the active account, please log out first."
2322+
msgstr ""
2323+
23212324
msgid "Lets you select apps that should access the Internet directly without going through the VPN tunnel."
23222325
msgstr ""
23232326

0 commit comments

Comments
 (0)