summaryrefslogtreecommitdiffhomepage
path: root/android
diff options
context:
space:
mode:
authorAlbin <albin@mullvad.net>2024-08-23 10:27:49 +0200
committerAlbin <albin@mullvad.net>2024-08-23 10:27:49 +0200
commit97ee66d64d9e5ef9265eae3fe74464d1fc807dd4 (patch)
tree8506abc903055521c1110e0ec8cb428e1431c6ba /android
parent67486d316b94262cb2e478765f4234b5d12afcba (diff)
parent4c7a6fa7941519ea637345cf6521edd067a6aa3a (diff)
downloadmullvadvpn-97ee66d64d9e5ef9265eae3fe74464d1fc807dd4.tar.xz
mullvadvpn-97ee66d64d9e5ef9265eae3fe74464d1fc807dd4.zip
Merge branch 'ensure-all-potential-grpc-errors-are-wrapped-droid-1170'
Diffstat (limited to 'android')
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/AccountScreen.kt4
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/LoginScreen.kt24
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/OutOfTimeScreen.kt13
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/WelcomeScreen.kt11
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/AccountViewModel.kt10
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/ConnectViewModel.kt18
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/LoginViewModel.kt14
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/OutOfTimeViewModel.kt7
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/WelcomeViewModel.kt6
-rw-r--r--android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/AccountViewModelTest.kt4
-rw-r--r--android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/ConnectViewModelTest.kt6
-rw-r--r--android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/DeviceRevokedViewModelTest.kt11
-rw-r--r--android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/LoginViewModelTest.kt3
-rw-r--r--android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/OutOfTimeViewModelTest.kt3
-rw-r--r--android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/WelcomeViewModelTest.kt3
-rw-r--r--android/lib/daemon-grpc/src/main/kotlin/net/mullvad/mullvadvpn/lib/daemon/grpc/ManagementService.kt104
-rw-r--r--android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/ClearAccountHistoryError.kt5
-rw-r--r--android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/LogoutAccountError.kt5
-rw-r--r--android/lib/shared/src/main/kotlin/net/mullvad/mullvadvpn/lib/shared/AccountRepository.kt6
-rw-r--r--android/test/arch/src/test/kotlin/net/mullvad/mullvadvpn/test/arch/ManagementServiceTest.kt31
20 files changed, 235 insertions, 53 deletions
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/AccountScreen.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/AccountScreen.kt
index 6969e56518..bd64cc3561 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/AccountScreen.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/AccountScreen.kt
@@ -57,6 +57,7 @@ import net.mullvad.mullvadvpn.compose.transitions.SlideInFromBottomTransition
import net.mullvad.mullvadvpn.compose.util.LaunchedEffectCollect
import net.mullvad.mullvadvpn.compose.util.SecureScreenWhileInView
import net.mullvad.mullvadvpn.compose.util.createCopyToClipboardHandle
+import net.mullvad.mullvadvpn.compose.util.showSnackbarImmediately
import net.mullvad.mullvadvpn.lib.model.AccountNumber
import net.mullvad.mullvadvpn.lib.payment.model.PaymentProduct
import net.mullvad.mullvadvpn.lib.payment.model.PaymentStatus
@@ -164,6 +165,7 @@ fun AccountScreen(
val snackbarHostState = remember { SnackbarHostState() }
val copyTextString = stringResource(id = R.string.copied_mullvad_account_number)
+ val errorString = stringResource(id = R.string.error_occurred)
val copyToClipboard = createCopyToClipboardHandle(snackbarHostState = snackbarHostState)
val openAccountPage = LocalUriHandler.current.createOpenAccountPageHook()
LaunchedEffectCollect(uiSideEffect) { sideEffect ->
@@ -173,6 +175,8 @@ fun AccountScreen(
openAccountPage(sideEffect.token)
is AccountViewModel.UiSideEffect.CopyAccountNumber ->
launch { copyToClipboard(sideEffect.accountNumber, copyTextString) }
+ AccountViewModel.UiSideEffect.GenericError ->
+ snackbarHostState.showSnackbarImmediately(message = errorString)
}
}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/LoginScreen.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/LoginScreen.kt
index 8a31520aad..1758a31432 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/LoginScreen.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/LoginScreen.kt
@@ -24,6 +24,7 @@ import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Text
import androidx.compose.material3.TextField
import androidx.compose.runtime.Composable
@@ -37,6 +38,7 @@ import androidx.compose.ui.draw.clip
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusProperties
import androidx.compose.ui.layout.ContentScale
+import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
@@ -73,6 +75,7 @@ import net.mullvad.mullvadvpn.compose.textfield.mullvadWhiteTextFieldColors
import net.mullvad.mullvadvpn.compose.transitions.LoginTransition
import net.mullvad.mullvadvpn.compose.util.CollectSideEffectWithLifecycle
import net.mullvad.mullvadvpn.compose.util.accountNumberVisualTransformation
+import net.mullvad.mullvadvpn.compose.util.showSnackbarImmediately
import net.mullvad.mullvadvpn.lib.theme.AppTheme
import net.mullvad.mullvadvpn.lib.theme.Dimens
import net.mullvad.mullvadvpn.viewmodel.LoginUiSideEffect
@@ -126,6 +129,8 @@ fun Login(
}
}
+ val context = LocalContext.current
+ val snackbarHostState = remember { SnackbarHostState() }
CollectSideEffectWithLifecycle(vm.uiSideEffect) {
when (it) {
LoginUiSideEffect.NavigateToWelcome ->
@@ -147,21 +152,27 @@ fun Login(
launchSingleTop = true
popUpTo(NavGraphs.root) { inclusive = true }
}
+ LoginUiSideEffect.GenericError ->
+ snackbarHostState.showSnackbarImmediately(
+ message = context.getString(R.string.error_occurred),
+ )
}
}
LoginScreen(
- state,
- vm::login,
- vm::createAccount,
- vm::clearAccountHistory,
- vm::onAccountNumberChange,
- dropUnlessResumed { navigator.navigate(SettingsDestination) }
+ state = state,
+ snackbarHostState = snackbarHostState,
+ onLoginClick = vm::login,
+ onCreateAccountClick = vm::createAccount,
+ onDeleteHistoryClick = vm::clearAccountHistory,
+ onAccountNumberChange = vm::onAccountNumberChange,
+ onSettingsClick = dropUnlessResumed { navigator.navigate(SettingsDestination) }
)
}
@Composable
private fun LoginScreen(
state: LoginUiState,
+ snackbarHostState: SnackbarHostState = SnackbarHostState(),
onLoginClick: (String) -> Unit = {},
onCreateAccountClick: () -> Unit = {},
onDeleteHistoryClick: () -> Unit = {},
@@ -169,6 +180,7 @@ private fun LoginScreen(
onSettingsClick: () -> Unit = {},
) {
ScaffoldWithTopBar(
+ snackbarHostState = snackbarHostState,
topBarColor = MaterialTheme.colorScheme.primary,
iconTintColor = MaterialTheme.colorScheme.onPrimary,
onSettingsClicked = onSettingsClick,
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/OutOfTimeScreen.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/OutOfTimeScreen.kt
index eea7a20e47..7b5b4bff55 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/OutOfTimeScreen.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/OutOfTimeScreen.kt
@@ -11,11 +11,14 @@ import androidx.compose.foundation.layout.size
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
+import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.painterResource
@@ -49,6 +52,7 @@ import net.mullvad.mullvadvpn.compose.state.OutOfTimeUiState
import net.mullvad.mullvadvpn.compose.test.OUT_OF_TIME_SCREEN_TITLE_TEST_TAG
import net.mullvad.mullvadvpn.compose.transitions.HomeTransition
import net.mullvad.mullvadvpn.compose.util.CollectSideEffectWithLifecycle
+import net.mullvad.mullvadvpn.compose.util.showSnackbarImmediately
import net.mullvad.mullvadvpn.lib.model.ErrorState
import net.mullvad.mullvadvpn.lib.model.ErrorStateCause
import net.mullvad.mullvadvpn.lib.model.TunnelState
@@ -135,6 +139,8 @@ fun OutOfTime(
}
}
+ val snackbarHostState = remember { SnackbarHostState() }
+ val context = LocalContext.current
val openAccountPage = LocalUriHandler.current.createOpenAccountPageHook()
CollectSideEffectWithLifecycle(vm.uiSideEffect, Lifecycle.State.RESUMED) { uiSideEffect ->
when (uiSideEffect) {
@@ -145,11 +151,16 @@ fun OutOfTime(
launchSingleTop = true
popUpTo(NavGraphs.root) { inclusive = true }
}
+ OutOfTimeViewModel.UiSideEffect.GenericError ->
+ snackbarHostState.showSnackbarImmediately(
+ message = context.getString(R.string.error_occurred)
+ )
}
}
OutOfTimeScreen(
state = state,
+ snackbarHostState = snackbarHostState,
onSitePaymentClick = vm::onSitePaymentClick,
onRedeemVoucherClick = dropUnlessResumed { navigator.navigate(RedeemVoucherDestination) },
onSettingsClick = dropUnlessResumed { navigator.navigate(SettingsDestination) },
@@ -165,6 +176,7 @@ fun OutOfTime(
@Composable
fun OutOfTimeScreen(
state: OutOfTimeUiState,
+ snackbarHostState: SnackbarHostState = SnackbarHostState(),
onDisconnectClick: () -> Unit = {},
onSitePaymentClick: () -> Unit = {},
onRedeemVoucherClick: () -> Unit = {},
@@ -176,6 +188,7 @@ fun OutOfTimeScreen(
val scrollState = rememberScrollState()
ScaffoldWithTopBarAndDeviceName(
+ snackbarHostState = snackbarHostState,
topBarColor =
if (state.tunnelState.isSecured()) {
MaterialTheme.colorScheme.tertiary
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/WelcomeScreen.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/WelcomeScreen.kt
index bb376c09f9..e53ec044cc 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/WelcomeScreen.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/WelcomeScreen.kt
@@ -21,6 +21,7 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
@@ -57,6 +58,7 @@ import net.mullvad.mullvadvpn.compose.state.WelcomeUiState
import net.mullvad.mullvadvpn.compose.transitions.HomeTransition
import net.mullvad.mullvadvpn.compose.util.CollectSideEffectWithLifecycle
import net.mullvad.mullvadvpn.compose.util.createCopyToClipboardHandle
+import net.mullvad.mullvadvpn.compose.util.showSnackbarImmediately
import net.mullvad.mullvadvpn.lib.common.util.groupWithSpaces
import net.mullvad.mullvadvpn.lib.model.AccountNumber
import net.mullvad.mullvadvpn.lib.payment.model.PaymentProduct
@@ -131,6 +133,8 @@ fun Welcome(
}
}
+ val snackbarHostState = remember { SnackbarHostState() }
+ val context = LocalContext.current
val openAccountPage = LocalUriHandler.current.createOpenAccountPageHook()
CollectSideEffectWithLifecycle(sideEffect = vm.uiSideEffect, Lifecycle.State.RESUMED) {
uiSideEffect ->
@@ -141,11 +145,16 @@ fun Welcome(
launchSingleTop = true
popUpTo(NavGraphs.root) { inclusive = true }
}
+ WelcomeViewModel.UiSideEffect.GenericError ->
+ snackbarHostState.showSnackbarImmediately(
+ message = context.getString(R.string.error_occurred)
+ )
}
}
WelcomeScreen(
state = state,
+ snackbarHostState = snackbarHostState,
onSitePaymentClick = dropUnlessResumed { vm.onSitePaymentClick() },
onRedeemVoucherClick = dropUnlessResumed { navigator.navigate(RedeemVoucherDestination) },
onSettingsClick = dropUnlessResumed { navigator.navigate(SettingsDestination) },
@@ -163,6 +172,7 @@ fun Welcome(
@Composable
fun WelcomeScreen(
state: WelcomeUiState,
+ snackbarHostState: SnackbarHostState = SnackbarHostState(),
onSitePaymentClick: () -> Unit,
onRedeemVoucherClick: () -> Unit,
onSettingsClick: () -> Unit,
@@ -173,7 +183,6 @@ fun WelcomeScreen(
navigateToVerificationPendingDialog: () -> Unit
) {
val scrollState = rememberScrollState()
- val snackbarHostState = remember { SnackbarHostState() }
ScaffoldWithTopBar(
topBarColor = MaterialTheme.colorScheme.primary,
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/AccountViewModel.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/AccountViewModel.kt
index b830d00c60..f8f66563e8 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/AccountViewModel.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/AccountViewModel.kt
@@ -75,8 +75,12 @@ class AccountViewModel(
fun onLogoutClick() {
viewModelScope.launch {
- accountRepository.logout()
- _uiSideEffect.send(UiSideEffect.NavigateToLogin)
+ accountRepository
+ .logout()
+ .fold(
+ { _uiSideEffect.send(UiSideEffect.GenericError) },
+ { _uiSideEffect.send(UiSideEffect.NavigateToLogin) }
+ )
}
}
@@ -127,6 +131,8 @@ class AccountViewModel(
UiSideEffect()
data class CopyAccountNumber(val accountNumber: String) : UiSideEffect()
+
+ data object GenericError : UiSideEffect()
}
}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/ConnectViewModel.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/ConnectViewModel.kt
index d1f4b3713b..325ee42b43 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/ConnectViewModel.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/ConnectViewModel.kt
@@ -123,11 +123,19 @@ class ConnectViewModel(
}
fun onDisconnectClick() {
- viewModelScope.launch { connectionProxy.disconnect() }
+ viewModelScope.launch {
+ connectionProxy.disconnect().onLeft {
+ _uiSideEffect.send(UiSideEffect.ConnectError.Generic)
+ }
+ }
}
fun onReconnectClick() {
- viewModelScope.launch { connectionProxy.reconnect() }
+ viewModelScope.launch {
+ connectionProxy.reconnect().onLeft {
+ _uiSideEffect.send(UiSideEffect.ConnectError.Generic)
+ }
+ }
}
fun onConnectClick() {
@@ -156,7 +164,11 @@ class ConnectViewModel(
}
fun onCancelClick() {
- viewModelScope.launch { connectionProxy.disconnect() }
+ viewModelScope.launch {
+ connectionProxy.disconnect().onLeft {
+ _uiSideEffect.send(UiSideEffect.ConnectError.Generic)
+ }
+ }
}
fun onManageAccountClick() {
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/LoginViewModel.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/LoginViewModel.kt
index 30bea42cfe..3c02c92917 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/LoginViewModel.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/LoginViewModel.kt
@@ -42,6 +42,8 @@ sealed interface LoginUiSideEffect {
data object NavigateToOutOfTime : LoginUiSideEffect
data class TooManyDevices(val accountNumber: AccountNumber) : LoginUiSideEffect
+
+ data object GenericError : LoginUiSideEffect
}
class LoginViewModel(
@@ -78,9 +80,15 @@ class LoginViewModel(
fun clearAccountHistory() =
viewModelScope.launch {
- accountRepository.clearAccountHistory()
- _mutableAccountHistory.update { null }
- _mutableAccountHistory.update { accountRepository.fetchAccountHistory() }
+ accountRepository
+ .clearAccountHistory()
+ .fold(
+ { _uiSideEffect.send(LoginUiSideEffect.GenericError) },
+ {
+ _mutableAccountHistory.update { null }
+ _mutableAccountHistory.update { accountRepository.fetchAccountHistory() }
+ }
+ )
}
fun createAccount() {
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/OutOfTimeViewModel.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/OutOfTimeViewModel.kt
index b2738a56ae..dfa4e6e3cc 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/OutOfTimeViewModel.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/OutOfTimeViewModel.kt
@@ -22,6 +22,7 @@ import net.mullvad.mullvadvpn.usecase.OutOfTimeUseCase
import net.mullvad.mullvadvpn.usecase.PaymentUseCase
import net.mullvad.mullvadvpn.util.isSuccess
import net.mullvad.mullvadvpn.util.toPaymentState
+import net.mullvad.mullvadvpn.viewmodel.WelcomeViewModel.UiSideEffect
class OutOfTimeViewModel(
private val accountRepository: AccountRepository,
@@ -71,7 +72,9 @@ class OutOfTimeViewModel(
}
fun onDisconnectClick() {
- viewModelScope.launch { connectionProxy.disconnect() }
+ viewModelScope.launch {
+ connectionProxy.disconnect().onLeft { _uiSideEffect.send(UiSideEffect.GenericError) }
+ }
}
private fun verifyPurchases() {
@@ -118,5 +121,7 @@ class OutOfTimeViewModel(
data class OpenAccountView(val token: WebsiteAuthToken?) : UiSideEffect
data object OpenConnectScreen : UiSideEffect
+
+ data object GenericError : UiSideEffect
}
}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/WelcomeViewModel.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/WelcomeViewModel.kt
index 525c6ca54e..e532045ae2 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/WelcomeViewModel.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/WelcomeViewModel.kt
@@ -78,7 +78,9 @@ class WelcomeViewModel(
}
fun onDisconnectClick() {
- viewModelScope.launch { connectionProxy.disconnect() }
+ viewModelScope.launch {
+ connectionProxy.disconnect().onLeft { _uiSideEffect.send(UiSideEffect.GenericError) }
+ }
}
private fun verifyPurchases() {
@@ -118,6 +120,8 @@ class WelcomeViewModel(
data class OpenAccountView(val token: WebsiteAuthToken?) : UiSideEffect
data object OpenConnectScreen : UiSideEffect
+
+ data object GenericError : UiSideEffect
}
companion object {
diff --git a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/AccountViewModelTest.kt b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/AccountViewModelTest.kt
index 706d8031e7..d239e01331 100644
--- a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/AccountViewModelTest.kt
+++ b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/AccountViewModelTest.kt
@@ -2,6 +2,7 @@ package net.mullvad.mullvadvpn.viewmodel
import android.app.Activity
import app.cash.turbine.test
+import arrow.core.right
import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.every
@@ -93,6 +94,9 @@ class AccountViewModelTest {
@Test
fun `onLogoutClick should invoke logout on AccountRepository`() {
+ // Arrange
+ coEvery { mockAccountRepository.logout() } returns Unit.right()
+
// Act
viewModel.onLogoutClick()
diff --git a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/ConnectViewModelTest.kt b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/ConnectViewModelTest.kt
index e6803d4e08..50a16d1432 100644
--- a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/ConnectViewModelTest.kt
+++ b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/ConnectViewModelTest.kt
@@ -234,7 +234,7 @@ class ConnectViewModelTest {
@Test
fun `onDisconnectClick should invoke disconnect on ConnectionProxy`() = runTest {
// Arrange
- coEvery { mockConnectionProxy.disconnect() } returns true
+ coEvery { mockConnectionProxy.disconnect() } returns true.right()
// Act
viewModel.onDisconnectClick()
@@ -246,7 +246,7 @@ class ConnectViewModelTest {
@Test
fun `onReconnectClick should invoke reconnect on ConnectionProxy`() = runTest {
// Arrange
- coEvery { mockConnectionProxy.reconnect() } returns true
+ coEvery { mockConnectionProxy.reconnect() } returns true.right()
// Act
viewModel.onReconnectClick()
@@ -270,7 +270,7 @@ class ConnectViewModelTest {
@Test
fun `onCancelClick should invoke disconnect on ConnectionProxy`() = runTest {
// Arrange
- coEvery { mockConnectionProxy.disconnect() } returns true
+ coEvery { mockConnectionProxy.disconnect() } returns true.right()
// Act
viewModel.onCancelClick()
diff --git a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/DeviceRevokedViewModelTest.kt b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/DeviceRevokedViewModelTest.kt
index b63f59b302..51ea31540c 100644
--- a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/DeviceRevokedViewModelTest.kt
+++ b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/DeviceRevokedViewModelTest.kt
@@ -1,14 +1,13 @@
package net.mullvad.mullvadvpn.viewmodel
import app.cash.turbine.test
+import arrow.core.right
import io.mockk.MockKAnnotations
-import io.mockk.Runs
import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.coVerifyOrder
import io.mockk.every
import io.mockk.impl.annotations.MockK
-import io.mockk.just
import io.mockk.mockk
import io.mockk.unmockkAll
import kotlinx.coroutines.flow.MutableSharedFlow
@@ -70,8 +69,8 @@ class DeviceRevokedViewModelTest {
@Test
fun `onGoToLoginClicked should invoke logout on AccountRepository`() {
// Arrange
- coEvery { mockConnectionProxy.disconnect() } returns true
- coEvery { mockedAccountRepository.logout() } just Runs
+ coEvery { mockConnectionProxy.disconnect() } returns true.right()
+ coEvery { mockedAccountRepository.logout() } returns Unit.right()
// Act
viewModel.onGoToLoginClicked()
@@ -83,8 +82,8 @@ class DeviceRevokedViewModelTest {
@Test
fun `onGoToLoginClicked should invoke disconnect before logout when connected`() {
// Arrange
- coEvery { mockConnectionProxy.disconnect() } returns true
- coEvery { mockedAccountRepository.logout() } just Runs
+ coEvery { mockConnectionProxy.disconnect() } returns true.right()
+ coEvery { mockedAccountRepository.logout() } returns Unit.right()
// Act
viewModel.onGoToLoginClicked()
diff --git a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/LoginViewModelTest.kt b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/LoginViewModelTest.kt
index 33eb3bdc3c..a9905e9506 100644
--- a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/LoginViewModelTest.kt
+++ b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/LoginViewModelTest.kt
@@ -208,6 +208,9 @@ class LoginViewModelTest {
@Test
fun `clearAccountHistory should invoke clearAccountHistory on AccountRepository`() = runTest {
+ // Arrange
+ coEvery { mockedAccountRepository.clearAccountHistory() } returns Unit.right()
+
// Act, Assert
loginViewModel.clearAccountHistory()
coVerify { mockedAccountRepository.clearAccountHistory() }
diff --git a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/OutOfTimeViewModelTest.kt b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/OutOfTimeViewModelTest.kt
index bd26effe82..886cb58fda 100644
--- a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/OutOfTimeViewModelTest.kt
+++ b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/OutOfTimeViewModelTest.kt
@@ -2,6 +2,7 @@ package net.mullvad.mullvadvpn.viewmodel
import androidx.lifecycle.viewModelScope
import app.cash.turbine.test
+import arrow.core.right
import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.every
@@ -146,7 +147,7 @@ class OutOfTimeViewModelTest {
@Test
fun `onDisconnectClick should invoke disconnect on ConnectionProxy`() = runTest {
// Arrange
- coEvery { mockConnectionProxy.disconnect() } returns true
+ coEvery { mockConnectionProxy.disconnect() } returns true.right()
// Act
viewModel.onDisconnectClick()
diff --git a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/WelcomeViewModelTest.kt b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/WelcomeViewModelTest.kt
index 1c7a7d0e3b..9ef1455bb7 100644
--- a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/WelcomeViewModelTest.kt
+++ b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/WelcomeViewModelTest.kt
@@ -2,6 +2,7 @@ package net.mullvad.mullvadvpn.viewmodel
import androidx.lifecycle.viewModelScope
import app.cash.turbine.test
+import arrow.core.right
import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.every
@@ -217,7 +218,7 @@ class WelcomeViewModelTest {
@Test
fun `when on disconnect click is called should call connection proxy disconnect`() = runTest {
// Arrange
- coEvery { mockConnectionProxy.disconnect() } returns true
+ coEvery { mockConnectionProxy.disconnect() } returns true.right()
// Act
viewModel.onDisconnectClick()
diff --git a/android/lib/daemon-grpc/src/main/kotlin/net/mullvad/mullvadvpn/lib/daemon/grpc/ManagementService.kt b/android/lib/daemon-grpc/src/main/kotlin/net/mullvad/mullvadvpn/lib/daemon/grpc/ManagementService.kt
index 36460ae1fa..c10f3b58e6 100644
--- a/android/lib/daemon-grpc/src/main/kotlin/net/mullvad/mullvadvpn/lib/daemon/grpc/ManagementService.kt
+++ b/android/lib/daemon-grpc/src/main/kotlin/net/mullvad/mullvadvpn/lib/daemon/grpc/ManagementService.kt
@@ -53,6 +53,7 @@ import net.mullvad.mullvadvpn.lib.model.ApiAccessMethodId
import net.mullvad.mullvadvpn.lib.model.ApiAccessMethodSetting
import net.mullvad.mullvadvpn.lib.model.AppId
import net.mullvad.mullvadvpn.lib.model.AppVersionInfo as ModelAppVersionInfo
+import net.mullvad.mullvadvpn.lib.model.ClearAccountHistoryError
import net.mullvad.mullvadvpn.lib.model.ClearAllOverridesError
import net.mullvad.mullvadvpn.lib.model.ConnectError
import net.mullvad.mullvadvpn.lib.model.Constraint
@@ -77,6 +78,7 @@ import net.mullvad.mullvadvpn.lib.model.GetDeviceListError
import net.mullvad.mullvadvpn.lib.model.GetDeviceStateError
import net.mullvad.mullvadvpn.lib.model.GetVersionInfoError
import net.mullvad.mullvadvpn.lib.model.LoginAccountError
+import net.mullvad.mullvadvpn.lib.model.LogoutAccountError
import net.mullvad.mullvadvpn.lib.model.NameAlreadyExists
import net.mullvad.mullvadvpn.lib.model.NewAccessMethodSetting
import net.mullvad.mullvadvpn.lib.model.ObfuscationSettings
@@ -245,16 +247,19 @@ class ManagementService(
suspend fun getDevice(): Either<GetDeviceStateError, ModelDeviceState> =
Either.catch { grpc.getDevice(Empty.getDefaultInstance()) }
.map { it.toDomain() }
+ .onLeft { Logger.e("Get device error") }
.mapLeft { GetDeviceStateError.Unknown(it) }
suspend fun updateDevice(): Either<DeviceUpdateError, Unit> =
Either.catch { grpc.updateDevice(Empty.getDefaultInstance()) }
.mapEmpty()
+ .onLeft { Logger.e("Update device error") }
.mapLeft { DeviceUpdateError(it) }
suspend fun getDeviceList(token: AccountNumber): Either<GetDeviceListError, List<Device>> =
Either.catch { grpc.listDevices(StringValue.of(token.value)) }
.map { it.devicesList.map(ManagementInterface.Device::toDomain) }
+ .onLeft { Logger.e("Get device list error") }
.mapLeft { GetDeviceListError.Unknown(it) }
suspend fun removeDevice(
@@ -270,15 +275,23 @@ class ManagementService(
)
}
.mapEmpty()
+ .onLeft { Logger.e("Remove device error") }
.mapLeft { DeleteDeviceError.Unknown(it) }
suspend fun connect(): Either<ConnectError, Boolean> =
Either.catch { grpc.connectTunnel(Empty.getDefaultInstance()).value }
+ .onLeft { Logger.e("Connect error") }
.mapLeft(ConnectError::Unknown)
- suspend fun disconnect(): Boolean = grpc.disconnectTunnel(Empty.getDefaultInstance()).value
+ suspend fun disconnect(): Either<ConnectError, Boolean> =
+ Either.catch { grpc.disconnectTunnel(Empty.getDefaultInstance()).value }
+ .onLeft { Logger.e("Disconnect error") }
+ .mapLeft(ConnectError::Unknown)
- suspend fun reconnect(): Boolean = grpc.reconnectTunnel(Empty.getDefaultInstance()).value
+ suspend fun reconnect(): Either<ConnectError, Boolean> =
+ Either.catch { grpc.reconnectTunnel(Empty.getDefaultInstance()).value }
+ .onLeft { Logger.e("Reconnect error") }
+ .mapLeft(ConnectError::Unknown)
private suspend fun getTunnelState(): ModelTunnelState =
grpc.getTunnelState(Empty.getDefaultInstance()).toDomain()
@@ -296,11 +309,17 @@ class ManagementService(
// will get 404 until the api have been published, thus we need to ignore error downstream.
private suspend fun getVersionInfo(): Either<GetVersionInfoError, ModelAppVersionInfo> =
Either.catch { grpc.getVersionInfo(Empty.getDefaultInstance()).toDomain() }
+ .onLeft { Logger.e("Get version info error") }
.mapLeft { GetVersionInfoError.Unknown(it) }
- suspend fun logoutAccount() {
- grpc.logoutAccount(Empty.getDefaultInstance())
- }
+ private suspend fun getCurrentApiAccessMethod(): ApiAccessMethodSetting =
+ grpc.getCurrentApiAccessMethod(Empty.getDefaultInstance()).toDomain()
+
+ suspend fun logoutAccount(): Either<LogoutAccountError, Unit> =
+ Either.catch { grpc.logoutAccount(Empty.getDefaultInstance()) }
+ .onLeft { Logger.e("Logout account error") }
+ .mapLeft(LogoutAccountError::Unknown)
+ .mapEmpty()
suspend fun loginAccount(accountNumber: AccountNumber): Either<LoginAccountError, Unit> =
Either.catch { grpc.loginAccount(StringValue.of(accountNumber.value)) }
@@ -310,14 +329,19 @@ class ManagementService(
Status.Code.RESOURCE_EXHAUSTED ->
LoginAccountError.MaxDevicesReached(accountNumber)
Status.Code.UNAVAILABLE -> LoginAccountError.RpcError
- else -> LoginAccountError.Unknown(it)
+ else -> {
+ Logger.e("Unknown login account error")
+ LoginAccountError.Unknown(it)
+ }
}
}
.mapEmpty()
- suspend fun clearAccountHistory() {
- grpc.clearAccountHistory(Empty.getDefaultInstance())
- }
+ suspend fun clearAccountHistory(): Either<ClearAccountHistoryError, Unit> =
+ Either.catch { grpc.clearAccountHistory(Empty.getDefaultInstance()) }
+ .onLeft { Logger.e("Clear account history error") }
+ .mapLeft(ClearAccountHistoryError::Unknown)
+ .mapEmpty()
suspend fun getAccountHistory(): Either<GetAccountHistoryError, AccountNumber?> =
Either.catch {
@@ -328,6 +352,7 @@ class ManagementService(
null
}
}
+ .onLeft { Logger.e("Get account history error") }
.mapLeft(GetAccountHistoryError::Unknown)
private suspend fun getInitialServiceState() {
@@ -347,6 +372,7 @@ class ManagementService(
accountNumber: AccountNumber
): Either<GetAccountDataError, AccountData> =
Either.catch { grpc.getAccountData(StringValue.of(accountNumber.value)).toDomain() }
+ .onLeft { Logger.e("Get account data error") }
.mapLeft(GetAccountDataError::Unknown)
suspend fun createAccount(): Either<CreateAccountError, AccountNumber> =
@@ -354,10 +380,12 @@ class ManagementService(
val accountNumberStringValue = grpc.createNewAccount(Empty.getDefaultInstance())
AccountNumber(accountNumberStringValue.value)
}
+ .onLeft { Logger.e("Create account error") }
.mapLeft(CreateAccountError::Unknown)
suspend fun setDnsOptions(dnsOptions: ModelDnsOptions): Either<SetDnsOptionsError, Unit> =
Either.catch { grpc.setDnsOptions(dnsOptions.fromDomain()) }
+ .onLeft { Logger.e("Set dns options error") }
.mapLeft(SetDnsOptionsError::Unknown)
.mapEmpty()
@@ -367,6 +395,7 @@ class ManagementService(
val updated = DnsOptions.state.set(currentDnsOptions, dnsState)
grpc.setDnsOptions(updated.fromDomain())
}
+ .onLeft { Logger.e("Set dns state error") }
.mapLeft(SetDnsOptionsError::Unknown)
.mapEmpty()
@@ -380,6 +409,7 @@ class ManagementService(
grpc.setDnsOptions(updatedDnsOptions.fromDomain())
}
+ .onLeft { Logger.e("Set custom dns error") }
.mapLeft(SetDnsOptionsError::Unknown)
.mapEmpty()
@@ -391,6 +421,7 @@ class ManagementService(
grpc.setDnsOptions(updatedDnsOptions.fromDomain())
updatedDnsOptions.customOptions.addresses.lastIndex
}
+ .onLeft { Logger.e("Add custom dns error") }
.mapLeft(SetDnsOptionsError::Unknown)
suspend fun deleteCustomDns(index: Int): Either<SetDnsOptionsError, Unit> =
@@ -404,16 +435,19 @@ class ManagementService(
}
grpc.setDnsOptions(updatedDnsOptions.fromDomain())
}
+ .onLeft { Logger.e("Delete custom dns error") }
.mapLeft(SetDnsOptionsError::Unknown)
.mapEmpty()
suspend fun setWireguardMtu(value: Int): Either<SetWireguardMtuError, Unit> =
Either.catch { grpc.setWireguardMtu(UInt32Value.of(value)) }
+ .onLeft { Logger.e("Set wireguard mtu error") }
.mapLeft(SetWireguardMtuError::Unknown)
.mapEmpty()
suspend fun resetWireguardMtu(): Either<SetWireguardMtuError, Unit> =
Either.catch { grpc.setWireguardMtu(UInt32Value.newBuilder().clearValue().build()) }
+ .onLeft { Logger.e("Reset wireguard mtu error") }
.mapLeft(SetWireguardMtuError::Unknown)
.mapEmpty()
@@ -421,14 +455,10 @@ class ManagementService(
value: ModelQuantumResistantState
): Either<SetWireguardQuantumResistantError, Unit> =
Either.catch { grpc.setQuantumResistantTunnel(value.toDomain()) }
+ .onLeft { Logger.e("Set wireguard quantum resistant error") }
.mapLeft(SetWireguardQuantumResistantError::Unknown)
.mapEmpty()
- // Todo needs to be more advanced
- suspend fun setRelaySettings(value: RelaySettings) {
- grpc.setRelaySettings(value.fromDomain())
- }
-
suspend fun setObfuscation(
value: SelectedObfuscation
): Either<SetObfuscationOptionsError, Unit> =
@@ -441,6 +471,7 @@ class ManagementService(
}
grpc.setObfuscationSettings(updatedObfuscationSettings.fromDomain())
}
+ .onLeft { Logger.e("Set obfuscation error") }
.mapLeft(SetObfuscationOptionsError::Unknown)
.mapEmpty()
@@ -454,16 +485,19 @@ class ManagementService(
}
grpc.setObfuscationSettings(updatedSettings.fromDomain())
}
+ .onLeft { Logger.e("Set obfuscation port error") }
.mapLeft(SetObfuscationOptionsError::Unknown)
.mapEmpty()
suspend fun setAutoConnect(isEnabled: Boolean): Either<SetAutoConnectError, Unit> =
Either.catch { grpc.setAutoConnect(BoolValue.of(isEnabled)) }
+ .onLeft { Logger.e("Set auto connect error") }
.mapLeft(SetAutoConnectError::Unknown)
.mapEmpty()
suspend fun setAllowLan(allow: Boolean): Either<SetAllowLanError, Unit> =
Either.catch { grpc.setAllowLan(BoolValue.of(allow)) }
+ .onLeft { Logger.e("Set allow lan error") }
.mapLeft(SetAllowLanError::Unknown)
.mapEmpty()
@@ -477,6 +511,7 @@ class ManagementService(
)
grpc.setRelaySettings(updatedRelaySettings.fromDomain())
}
+ .onLeft { Logger.e("Set relay location error") }
.mapLeft(SetRelayLocationError::Unknown)
.mapEmpty()
@@ -488,7 +523,10 @@ class ManagementService(
.mapLeftStatus {
when (it.status.code) {
Status.Code.ALREADY_EXISTS -> CustomListAlreadyExists
- else -> UnknownCustomListError(it)
+ else -> {
+ Logger.e("Unknown create custom list error")
+ UnknownCustomListError(it)
+ }
}
}
@@ -497,18 +535,23 @@ class ManagementService(
.mapLeftStatus {
when (it.status.code) {
Status.Code.ALREADY_EXISTS -> NameAlreadyExists(customList.name)
- else -> UnknownCustomListError(it)
+ else -> {
+ Logger.e("Unknown update custom list error")
+ UnknownCustomListError(it)
+ }
}
}
.mapEmpty()
suspend fun deleteCustomList(id: CustomListId): Either<DeleteCustomListError, Unit> =
Either.catch { grpc.deleteCustomList(StringValue.of(id.value)) }
+ .onLeft { Logger.e("Delete custom list error") }
.mapLeft(::UnknownCustomListError)
.mapEmpty()
suspend fun clearAllRelayOverrides(): Either<ClearAllOverridesError, Unit> =
Either.catch { grpc.clearAllRelayOverrides(Empty.getDefaultInstance()) }
+ .onLeft { Logger.e("Clear all relay overrides error") }
.mapLeft(ClearAllOverridesError::Unknown)
.mapEmpty()
@@ -518,7 +561,10 @@ class ManagementService(
when (it.status.code) {
// Currently we only get invalid argument errors from daemon via gRPC
Status.Code.INVALID_ARGUMENT -> SettingsPatchError.ParsePatch
- else -> SettingsPatchError.ApplyPatch
+ else -> {
+ Logger.e("Unknown apply settings patch error")
+ SettingsPatchError.ApplyPatch
+ }
}
}
.mapEmpty()
@@ -532,6 +578,7 @@ class ManagementService(
RelaySettings.relayConstraints.wireguardConstraints.set(relaySettings, value)
grpc.setRelaySettings(updated.fromDomain())
}
+ .onLeft { Logger.e("Set wireguard constraints error") }
.mapLeft(SetWireguardConstraintsError::Unknown)
.mapEmpty()
@@ -550,6 +597,7 @@ class ManagementService(
}
grpc.setRelaySettings(updated.fromDomain())
}
+ .onLeft { Logger.e("Set ownership and providers error") }
.mapLeft(SetWireguardConstraintsError::Unknown)
.mapEmpty()
@@ -561,6 +609,7 @@ class ManagementService(
val updated = RelaySettings.relayConstraints.ownership.set(relaySettings, ownership)
grpc.setRelaySettings(updated.fromDomain())
}
+ .onLeft { Logger.e("Set ownership error") }
.mapLeft(SetWireguardConstraintsError::Unknown)
.mapEmpty()
@@ -573,6 +622,7 @@ class ManagementService(
RelaySettings.relayConstraints.providers.set(relaySettings, providersConstraint)
grpc.setRelaySettings(updated.fromDomain())
}
+ .onLeft { Logger.e("Set providers error") }
.mapLeft(SetWireguardConstraintsError::Unknown)
.mapEmpty()
@@ -587,26 +637,33 @@ class ManagementService(
Status.Code.ALREADY_EXISTS,
Status.Code.RESOURCE_EXHAUSTED -> RedeemVoucherError.VoucherAlreadyUsed
Status.Code.UNAVAILABLE -> RedeemVoucherError.RpcError
- else -> RedeemVoucherError.Unknown(it)
+ else -> {
+ Logger.e("Unknown submit voucher error")
+ RedeemVoucherError.Unknown(it)
+ }
}
}
suspend fun initializePlayPurchase(): Either<PlayPurchaseInitError, PlayPurchasePaymentToken> =
Either.catch { grpc.initPlayPurchase(Empty.getDefaultInstance()).toDomain() }
+ .onLeft { Logger.e("Initialize play purchase error") }
.mapLeft { PlayPurchaseInitError.OtherError }
suspend fun verifyPlayPurchase(purchase: PlayPurchase): Either<PlayPurchaseVerifyError, Unit> =
Either.catch { grpc.verifyPlayPurchase(purchase.fromDomain()) }
+ .onLeft { Logger.e("Verify play purchase error") }
.mapLeft { PlayPurchaseVerifyError.OtherError }
.mapEmpty()
suspend fun addSplitTunnelingApp(app: AppId): Either<AddSplitTunnelingAppError, Unit> =
Either.catch { grpc.addSplitTunnelApp(StringValue.of(app.value)) }
+ .onLeft { Logger.e("Add split tunneling app error") }
.mapLeft(AddSplitTunnelingAppError::Unknown)
.mapEmpty()
suspend fun removeSplitTunnelingApp(app: AppId): Either<RemoveSplitTunnelingAppError, Unit> =
Either.catch { grpc.removeSplitTunnelApp(StringValue.of(app.value)) }
+ .onLeft { Logger.e("Remove split tunneling app error") }
.mapLeft(RemoveSplitTunnelingAppError::Unknown)
.mapEmpty()
@@ -614,17 +671,20 @@ class ManagementService(
enabled: Boolean
): Either<RemoveSplitTunnelingAppError, Unit> =
Either.catch { grpc.setSplitTunnelState(BoolValue.of(enabled)) }
+ .onLeft { Logger.e("Set split tunneling state error") }
.mapLeft(RemoveSplitTunnelingAppError::Unknown)
.mapEmpty()
suspend fun getWebsiteAuthToken(): Either<Throwable, WebsiteAuthToken> =
Either.catch { grpc.getWwwAuthToken(Empty.getDefaultInstance()) }
+ .onLeft { Logger.e("Get website auth token error") }
.map { WebsiteAuthToken.fromString(it.value) }
suspend fun addApiAccessMethod(
newAccessMethodSetting: NewAccessMethodSetting
): Either<AddApiAccessMethodError, ApiAccessMethodId> =
Either.catch { grpc.addApiAccessMethod(newAccessMethodSetting.fromDomain()) }
+ .onLeft { Logger.e("Add api access method error") }
.mapLeft(AddApiAccessMethodError::Unknown)
.map { ApiAccessMethodId.fromString(it.value) }
@@ -632,6 +692,7 @@ class ManagementService(
apiAccessMethodId: ApiAccessMethodId
): Either<RemoveApiAccessMethodError, Unit> =
Either.catch { grpc.removeApiAccessMethod(apiAccessMethodId.fromDomain()) }
+ .onLeft { Logger.e("Remove api access method error") }
.mapLeft(RemoveApiAccessMethodError::Unknown)
.mapEmpty()
@@ -639,6 +700,7 @@ class ManagementService(
apiAccessMethodId: ApiAccessMethodId
): Either<SetApiAccessMethodError, Unit> =
Either.catch { grpc.setApiAccessMethod(apiAccessMethodId.fromDomain()) }
+ .onLeft { Logger.e("Set api access method error") }
.mapLeft(SetApiAccessMethodError::Unknown)
.mapEmpty()
@@ -646,16 +708,15 @@ class ManagementService(
apiAccessMethodSetting: ApiAccessMethodSetting
): Either<UpdateApiAccessMethodError, Unit> =
Either.catch { grpc.updateApiAccessMethod(apiAccessMethodSetting.fromDomain()) }
+ .onLeft { Logger.e("Update api access method error") }
.mapLeft(::UnknownApiAccessMethodError)
.mapEmpty()
- private suspend fun getCurrentApiAccessMethod(): ApiAccessMethodSetting =
- grpc.getCurrentApiAccessMethod(Empty.getDefaultInstance()).toDomain()
-
suspend fun testCustomApiAccessMethod(
customProxy: ApiAccessMethod.CustomProxy
): Either<TestApiAccessMethodError, Unit> =
Either.catch { grpc.testCustomApiAccessMethod(customProxy.fromDomain()) }
+ .onLeft { Logger.e("Test custom api access method error") }
.mapLeftStatus { TestApiAccessMethodError.Grpc }
.map { result ->
either { ensure(result.value) { TestApiAccessMethodError.CouldNotAccess } }
@@ -665,6 +726,7 @@ class ManagementService(
apiAccessMethodId: ApiAccessMethodId
): Either<TestApiAccessMethodError, Unit> =
Either.catch { grpc.testApiAccessMethodById(apiAccessMethodId.fromDomain()) }
+ .onLeft { Logger.e("Test api access method error") }
.mapLeftStatus { TestApiAccessMethodError.Grpc }
.map { result ->
either { ensure(result.value) { TestApiAccessMethodError.CouldNotAccess } }
diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/ClearAccountHistoryError.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/ClearAccountHistoryError.kt
new file mode 100644
index 0000000000..bd5cff4002
--- /dev/null
+++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/ClearAccountHistoryError.kt
@@ -0,0 +1,5 @@
+package net.mullvad.mullvadvpn.lib.model
+
+interface ClearAccountHistoryError {
+ data class Unknown(val t: Throwable) : ClearAccountHistoryError
+}
diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/LogoutAccountError.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/LogoutAccountError.kt
new file mode 100644
index 0000000000..da92b9216c
--- /dev/null
+++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/LogoutAccountError.kt
@@ -0,0 +1,5 @@
+package net.mullvad.mullvadvpn.lib.model
+
+interface LogoutAccountError {
+ data class Unknown(val t: Throwable) : LogoutAccountError
+}
diff --git a/android/lib/shared/src/main/kotlin/net/mullvad/mullvadvpn/lib/shared/AccountRepository.kt b/android/lib/shared/src/main/kotlin/net/mullvad/mullvadvpn/lib/shared/AccountRepository.kt
index a71dbe8efb..82f70e8140 100644
--- a/android/lib/shared/src/main/kotlin/net/mullvad/mullvadvpn/lib/shared/AccountRepository.kt
+++ b/android/lib/shared/src/main/kotlin/net/mullvad/mullvadvpn/lib/shared/AccountRepository.kt
@@ -54,10 +54,8 @@ class AccountRepository(
suspend fun login(accountNumber: AccountNumber): Either<LoginAccountError, Unit> =
managementService.loginAccount(accountNumber)
- suspend fun logout() {
- managementService.logoutAccount()
- _isNewAccount.update { false }
- }
+ suspend fun logout() =
+ managementService.logoutAccount().onRight { _isNewAccount.update { false } }
suspend fun fetchAccountHistory(): AccountNumber? =
managementService.getAccountHistory().getOrNull()
diff --git a/android/test/arch/src/test/kotlin/net/mullvad/mullvadvpn/test/arch/ManagementServiceTest.kt b/android/test/arch/src/test/kotlin/net/mullvad/mullvadvpn/test/arch/ManagementServiceTest.kt
new file mode 100644
index 0000000000..a649a3ac5d
--- /dev/null
+++ b/android/test/arch/src/test/kotlin/net/mullvad/mullvadvpn/test/arch/ManagementServiceTest.kt
@@ -0,0 +1,31 @@
+package net.mullvad.mullvadvpn.test.arch
+
+import com.lemonappdev.konsist.api.Konsist
+import com.lemonappdev.konsist.api.ext.list.modifierprovider.withPublicOrDefaultModifier
+import com.lemonappdev.konsist.api.verify.assertTrue
+import org.junit.jupiter.api.Test
+
+class ManagementServiceTest {
+
+ @Test
+ fun `ensure all public functions are returning Either`() {
+ managementServiceClass()
+ .functions()
+ .withPublicOrDefaultModifier()
+ .filter { excludedFunctions().contains(it.name).not() }
+ .assertTrue { it.returnType?.name?.startsWith(EITHER_CLASS_NAME) == true }
+ }
+
+ private fun managementServiceClass() =
+ Konsist.scopeFromProject().classes().first { it.name == MANAGEMENT_SERVICE_CLASS_NAME }
+
+ private fun excludedFunctions() = setOf(START, STOP, ENTER_IDLE)
+
+ companion object {
+ private const val MANAGEMENT_SERVICE_CLASS_NAME = "ManagementService"
+ private const val START = "start"
+ private const val STOP = "stop"
+ private const val ENTER_IDLE = "enterIdle"
+ private const val EITHER_CLASS_NAME = "Either"
+ }
+}