diff options
| author | Kalle Lindström <karl.lindstrom@mullvad.net> | 2024-10-09 13:08:23 +0200 |
|---|---|---|
| committer | David Göransson <david.goransson@mullvad.net> | 2024-10-15 14:38:37 +0200 |
| commit | 57aa8880dcd2d739d852fee1fa60bdd49dec9f80 (patch) | |
| tree | 698af4176e8685c2ca8d2517fb16488df391ae32 /android/app | |
| parent | 9e2e53486a66fd3ad83d5775b050041a29131652 (diff) | |
| download | mullvadvpn-57aa8880dcd2d739d852fee1fa60bdd49dec9f80.tar.xz mullvadvpn-57aa8880dcd2d739d852fee1fa60bdd49dec9f80.zip | |
Show create account confirm when user has account
Diffstat (limited to 'android/app')
4 files changed, 119 insertions, 23 deletions
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/dialog/CreateAccountConfirmationDialog.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/dialog/CreateAccountConfirmationDialog.kt new file mode 100644 index 0000000000..b670f7d79e --- /dev/null +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/dialog/CreateAccountConfirmationDialog.kt @@ -0,0 +1,54 @@ +package net.mullvad.mullvadvpn.compose.dialog + +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import com.ramcosta.composedestinations.annotation.Destination +import com.ramcosta.composedestinations.annotation.RootGraph +import com.ramcosta.composedestinations.result.EmptyResultBackNavigator +import com.ramcosta.composedestinations.result.ResultBackNavigator +import com.ramcosta.composedestinations.spec.DestinationStyle +import net.mullvad.mullvadvpn.R +import net.mullvad.mullvadvpn.compose.dialog.info.InfoConfirmationDialog +import net.mullvad.mullvadvpn.compose.dialog.info.InfoConfirmationDialogTitleType +import net.mullvad.mullvadvpn.lib.theme.AppTheme +import net.mullvad.mullvadvpn.lib.theme.Dimens + +@Preview +@Composable +private fun PreviewCreateAccountConfirmationDialog() { + AppTheme { CreateAccountConfirmation(EmptyResultBackNavigator()) } +} + +@Composable +@Destination<RootGraph>(style = DestinationStyle.Dialog::class) +fun CreateAccountConfirmation(navigator: ResultBackNavigator<Boolean>) { + InfoConfirmationDialog( + navigator = navigator, + titleType = InfoConfirmationDialogTitleType.IconOnly, + confirmButtonTitle = stringResource(R.string.create_new_account), + cancelButtonTitle = stringResource(R.string.cancel), + ) { + Text( + text = stringResource(id = R.string.create_new_account_warning_paragraph1), + color = MaterialTheme.colorScheme.onSurface, + style = MaterialTheme.typography.bodySmall, + modifier = Modifier.fillMaxWidth(), + ) + + Spacer(modifier = Modifier.height(Dimens.verticalSpace)) + + Text( + text = stringResource(id = R.string.create_new_account_warning_paragraph2), + color = MaterialTheme.colorScheme.onSurface, + style = MaterialTheme.typography.bodySmall, + modifier = Modifier.fillMaxWidth(), + ) + } +} 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 7a3d338022..a0829b79d5 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 @@ -56,11 +56,13 @@ import com.ramcosta.composedestinations.annotation.Destination import com.ramcosta.composedestinations.annotation.RootGraph import com.ramcosta.composedestinations.generated.NavGraphs import com.ramcosta.composedestinations.generated.destinations.ConnectDestination +import com.ramcosta.composedestinations.generated.destinations.CreateAccountConfirmationDestination import com.ramcosta.composedestinations.generated.destinations.DeviceListDestination import com.ramcosta.composedestinations.generated.destinations.OutOfTimeDestination import com.ramcosta.composedestinations.generated.destinations.SettingsDestination import com.ramcosta.composedestinations.generated.destinations.WelcomeDestination import com.ramcosta.composedestinations.navigation.DestinationsNavigator +import com.ramcosta.composedestinations.result.ResultRecipient import net.mullvad.mullvadvpn.R import net.mullvad.mullvadvpn.compose.button.PrimaryButton import net.mullvad.mullvadvpn.compose.button.VariantButton @@ -78,6 +80,7 @@ import net.mullvad.mullvadvpn.compose.test.LOGIN_TITLE_TEST_TAG 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.OnNavResultValue import net.mullvad.mullvadvpn.compose.util.accountNumberVisualTransformation import net.mullvad.mullvadvpn.compose.util.showSnackbarImmediately import net.mullvad.mullvadvpn.lib.theme.AppTheme @@ -86,7 +89,7 @@ import net.mullvad.mullvadvpn.viewmodel.LoginUiSideEffect import net.mullvad.mullvadvpn.viewmodel.LoginViewModel import org.koin.androidx.compose.koinViewModel -@Preview("Default|Loading.LogginIn|Loading.CreatingAccount|LoginError|Success") +@Preview("Default|Loading.LoggingIn|Loading.CreatingAccount|LoginError|Success") @Composable private fun PreviewLoginScreen( @PreviewParameter(LoginUiStatePreviewParameterProvider::class) state: LoginUiState @@ -103,6 +106,8 @@ fun Login( navigator: DestinationsNavigator, accountNumber: String? = null, vm: LoginViewModel = koinViewModel(), + createAccountConfirmationDialogResult: + ResultRecipient<CreateAccountConfirmationDestination, Boolean>, ) { val state by vm.uiState.collectAsStateWithLifecycle() @@ -114,6 +119,12 @@ fun Login( } } + createAccountConfirmationDialogResult.OnNavResultValue { createAccount -> + if (createAccount) { + vm.onCreateAccountConfirmed() + } + } + val context = LocalContext.current val snackbarHostState = remember { SnackbarHostState() } CollectSideEffectWithLifecycle(vm.uiSideEffect) { @@ -137,6 +148,8 @@ fun Login( launchSingleTop = true popUpTo(NavGraphs.root) { inclusive = true } } + LoginUiSideEffect.NavigateToCreateAccountConfirmation -> + navigator.navigate(CreateAccountConfirmationDestination) LoginUiSideEffect.GenericError -> snackbarHostState.showSnackbarImmediately( message = context.getString(R.string.error_occurred) @@ -147,7 +160,7 @@ fun Login( state = state, snackbarHostState = snackbarHostState, onLoginClick = vm::login, - onCreateAccountClick = vm::createAccount, + onCreateAccountClick = vm::onCreateAccountClick, onDeleteHistoryClick = vm::clearAccountHistory, onAccountNumberChange = vm::onAccountNumberChange, onSettingsClick = dropUnlessResumed { navigator.navigate(SettingsDestination) }, 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 3481a8d1e7..25a8a47da3 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 @@ -41,6 +41,8 @@ sealed interface LoginUiSideEffect { data object NavigateToOutOfTime : LoginUiSideEffect + data object NavigateToCreateAccountConfirmation : LoginUiSideEffect + data class TooManyDevices(val accountNumber: AccountNumber) : LoginUiSideEffect data object GenericError : LoginUiSideEffect @@ -58,10 +60,8 @@ class LoginViewModel( private val _uiSideEffect = Channel<LoginUiSideEffect>() val uiSideEffect = _uiSideEffect.receiveAsFlow() - private val _mutableAccountHistory: MutableStateFlow<AccountNumber?> = MutableStateFlow(null) - private val _uiState = - combine(_loginInput, _mutableAccountHistory, _loginState) { + combine(_loginInput, accountRepository.accountHistory, _loginState) { loginInput, historyAccountNumber, loginState -> @@ -70,27 +70,31 @@ class LoginViewModel( val uiState: StateFlow<LoginUiState> = _uiState - .onStart { - viewModelScope.launch { - _mutableAccountHistory.update { accountRepository.fetchAccountHistory() } - } - } + .onStart { viewModelScope.launch { accountRepository.fetchAccountHistory() } } .stateIn(viewModelScope, SharingStarted.WhileSubscribed(), LoginUiState.INITIAL) fun clearAccountHistory() = viewModelScope.launch { - accountRepository - .clearAccountHistory() - .fold( - { _uiSideEffect.send(LoginUiSideEffect.GenericError) }, - { - _mutableAccountHistory.update { null } - _mutableAccountHistory.update { accountRepository.fetchAccountHistory() } - }, - ) + accountRepository.clearAccountHistory().onLeft { + _uiSideEffect.send(LoginUiSideEffect.GenericError) + } } - fun createAccount() { + fun onCreateAccountClick() { + if (hasPreviouslyCreatedAccount()) { + viewModelScope.launch { + _uiSideEffect.send(LoginUiSideEffect.NavigateToCreateAccountConfirmation) + } + } else { + createAccount() + } + } + + fun onCreateAccountConfirmed() { + createAccount() + } + + private fun createAccount() { _loginState.value = Loading.CreatingAccount viewModelScope.launch(dispatcher) { accountRepository @@ -127,7 +131,7 @@ class LoginViewModel( } } - private suspend fun onSuccessfulLogin() { + private fun onSuccessfulLogin() { newDeviceRepository.newDeviceCreated() viewModelScope.launch(dispatcher) { @@ -169,6 +173,8 @@ class LoginViewModel( return internetAvailableUseCase() } + private fun hasPreviouslyCreatedAccount(): Boolean = uiState.value.lastUsedAccount != null + companion object { private const val SHOW_SUCCESSFUL_LOGIN_MILLIS = 1000L } 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 ba636197f4..1a80597066 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 @@ -43,13 +43,16 @@ class LoginViewModelTest { @MockK private lateinit var mockedAccountRepository: AccountRepository private lateinit var loginViewModel: LoginViewModel + private lateinit var accountHistoryFlow: MutableStateFlow<AccountNumber?> @BeforeEach fun setup() { Dispatchers.setMain(UnconfinedTestDispatcher()) MockKAnnotations.init(this, relaxUnitFun = true) + accountHistoryFlow = MutableStateFlow(null) every { connectivityUseCase() } returns true coEvery { mockedAccountRepository.fetchAccountHistory() } returns null + coEvery { mockedAccountRepository.accountHistory } returns accountHistoryFlow loginViewModel = LoginViewModel( @@ -96,13 +99,33 @@ class LoginViewModelTest { // Act, Assert uiStates.skipDefaultItem() - loginViewModel.createAccount() + loginViewModel.onCreateAccountConfirmed() assertEquals(Loading.CreatingAccount, uiStates.awaitItem().loginState) assertEquals(LoginUiSideEffect.NavigateToWelcome, sideEffects.awaitItem()) } } @Test + fun `when creating a new account the confirmation dialog should be shown when an account exists in the history`() = + runTest { + turbineScope { + // Arrange + val uiStates = loginViewModel.uiState.testIn(backgroundScope) + val sideEffects = loginViewModel.uiSideEffect.testIn(backgroundScope) + + // Act, Assert + uiStates.skipDefaultItem() + accountHistoryFlow.value = DUMMY_ACCOUNT_NUMBER + loginViewModel.onCreateAccountClick() + assertEquals(Idle(null), uiStates.awaitItem().loginState) + assertEquals( + LoginUiSideEffect.NavigateToCreateAccountConfirmation, + sideEffects.awaitItem(), + ) + } + } + + @Test fun `given valid account when logging in then navigate to connect view`() = runTest { turbineScope { // Arrange @@ -197,7 +220,7 @@ class LoginViewModelTest { fun `on new accountHistory emission uiState should include lastUsedAccount matching accountHistory`() = runTest { // Arrange - coEvery { mockedAccountRepository.fetchAccountHistory() } returns DUMMY_ACCOUNT_NUMBER + accountHistoryFlow.value = DUMMY_ACCOUNT_NUMBER // Act, Assert loginViewModel.uiState.test { |
