From 89ca2ac227ecce2b03f3fa3fe25b72be5c264f65 Mon Sep 17 00:00:00 2001 From: saber safavi Date: Thu, 24 Aug 2023 10:51:29 +0200 Subject: [PATCH] Add info dialog for device name --- .../compose/screen/AccountScreenTest.kt | 8 ++-- .../compose/dialog/DeviceNameInfoDialog.kt | 19 ++++++++ .../compose/screen/AccountScreen.kt | 39 ++++++++++++--- .../compose/state/AccountUiState.kt | 20 ++++++-- .../mullvadvpn/ui/fragment/AccountFragment.kt | 9 ++-- .../mullvadvpn/viewmodel/AccountViewModel.kt | 47 ++++++++++++++----- .../viewmodel/AccountViewModelState.kt | 43 +++++++++++++++++ .../resource/src/main/res/values/strings.xml | 3 ++ 8 files changed, 156 insertions(+), 32 deletions(-) create mode 100644 android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/dialog/DeviceNameInfoDialog.kt create mode 100644 android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/AccountViewModelState.kt diff --git a/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/AccountScreenTest.kt b/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/AccountScreenTest.kt index e616f67449b1..d10c4d91c5fb 100644 --- a/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/AccountScreenTest.kt +++ b/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/AccountScreenTest.kt @@ -30,7 +30,7 @@ class AccountScreenTest { composeTestRule.setContent { AccountScreen( uiState = - AccountUiState( + AccountUiState.DefaultUiState( deviceName = DUMMY_DEVICE_NAME, accountNumber = DUMMY_ACCOUNT_NUMBER, accountExpiry = null @@ -54,7 +54,7 @@ class AccountScreenTest { composeTestRule.setContent { AccountScreen( uiState = - AccountUiState( + AccountUiState.DefaultUiState( deviceName = DUMMY_DEVICE_NAME, accountNumber = DUMMY_ACCOUNT_NUMBER, accountExpiry = null @@ -79,7 +79,7 @@ class AccountScreenTest { composeTestRule.setContent { AccountScreen( uiState = - AccountUiState( + AccountUiState.DefaultUiState( deviceName = DUMMY_DEVICE_NAME, accountNumber = DUMMY_ACCOUNT_NUMBER, accountExpiry = null @@ -104,7 +104,7 @@ class AccountScreenTest { composeTestRule.setContent { AccountScreen( uiState = - AccountUiState( + AccountUiState.DefaultUiState( deviceName = DUMMY_DEVICE_NAME, accountNumber = DUMMY_ACCOUNT_NUMBER, accountExpiry = null diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/dialog/DeviceNameInfoDialog.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/dialog/DeviceNameInfoDialog.kt new file mode 100644 index 000000000000..179d14116053 --- /dev/null +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/dialog/DeviceNameInfoDialog.kt @@ -0,0 +1,19 @@ +package net.mullvad.mullvadvpn.compose.dialog + +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.stringResource +import net.mullvad.mullvadvpn.R + +@Composable +fun DeviceNameInfoDialog(onDismiss: () -> Unit) { + InfoDialog( + message = stringResource(id = R.string.local_network_sharing_info), + additionalInfo = + buildString { + appendLine(stringResource(id = R.string.device_name_info_part1)) + appendLine(stringResource(id = R.string.device_name_info_part2)) + appendLine(stringResource(id = R.string.device_name_info_part3)) + }, + onDismiss = onDismiss + ) +} 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 c9f90b4d53c5..e213418bc93f 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 @@ -2,8 +2,10 @@ package net.mullvad.mullvadvpn.compose.screen import androidx.compose.animation.animateContentSize import androidx.compose.foundation.background +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding @@ -11,6 +13,7 @@ import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -18,6 +21,7 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import kotlinx.coroutines.flow.MutableSharedFlow @@ -34,6 +38,7 @@ import net.mullvad.mullvadvpn.compose.component.CopyableObfuscationView import net.mullvad.mullvadvpn.compose.component.InformationView import net.mullvad.mullvadvpn.compose.component.MissingPolicy import net.mullvad.mullvadvpn.compose.component.drawVerticalScrollbar +import net.mullvad.mullvadvpn.compose.dialog.DeviceNameInfoDialog import net.mullvad.mullvadvpn.compose.state.AccountUiState import net.mullvad.mullvadvpn.lib.common.constant.BuildTypes import net.mullvad.mullvadvpn.lib.common.util.capitalizeFirstCharOfEachWord @@ -48,7 +53,7 @@ import net.mullvad.mullvadvpn.viewmodel.AccountViewModel private fun PreviewAccountScreen() { AccountScreen( uiState = - AccountUiState( + AccountUiState.DefaultUiState( deviceName = "Test Name", accountNumber = "1234123412341234", accountExpiry = null @@ -62,6 +67,8 @@ private fun PreviewAccountScreen() { fun AccountScreen( uiState: AccountUiState, viewActions: SharedFlow, + onDeviceNameInfoClick: () -> Unit = {}, + onDismissInfoClick: () -> Unit = {}, onRedeemVoucherClick: () -> Unit = {}, onManageAccountClick: () -> Unit = {}, onLogoutClick: () -> Unit = {}, @@ -71,6 +78,15 @@ fun AccountScreen( val state = rememberCollapsingToolbarScaffoldState() val progress = state.toolbarState.progress + when (uiState) { + is AccountUiState.DeviceNameDialogUiState -> { + DeviceNameInfoDialog(onDismissInfoClick) + } + else -> { + // NOOP + } + } + CollapsingToolbarScaffold( backgroundColor = MaterialTheme.colorScheme.background, modifier = Modifier.fillMaxSize(), @@ -118,12 +134,21 @@ fun AccountScreen( text = stringResource(id = R.string.device_name), modifier = Modifier.padding(start = Dimens.sideMargin, end = Dimens.sideMargin) ) - - InformationView( - content = uiState.deviceName.capitalizeFirstCharOfEachWord(), - whenMissing = MissingPolicy.SHOW_SPINNER - ) - + Row { + InformationView( + content = uiState.deviceName.capitalizeFirstCharOfEachWord(), + whenMissing = MissingPolicy.SHOW_SPINNER + ) + Icon( + modifier = + Modifier.clickable { onDeviceNameInfoClick() } + .padding(start = Dimens.mediumPadding, end = Dimens.mediumPadding) + .align(Alignment.CenterVertically), + painter = painterResource(id = R.drawable.icon_info), + contentDescription = null, + tint = MaterialTheme.colorScheme.inverseSurface + ) + } Text( style = MaterialTheme.typography.labelMedium, text = stringResource(id = R.string.account_number), diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/AccountUiState.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/AccountUiState.kt index a9527955711f..4198e2119b59 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/AccountUiState.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/AccountUiState.kt @@ -2,8 +2,20 @@ package net.mullvad.mullvadvpn.compose.state import org.joda.time.DateTime -data class AccountUiState( - val deviceName: String, - val accountNumber: String, +sealed interface AccountUiState { + val deviceName: String + val accountNumber: String val accountExpiry: DateTime? -) + + data class DefaultUiState( + override val deviceName: String = "", + override val accountNumber: String = "", + override val accountExpiry: DateTime? = null + ) : AccountUiState + + data class DeviceNameDialogUiState( + override val deviceName: String, + override val accountNumber: String, + override val accountExpiry: DateTime? + ) : AccountUiState +} diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/fragment/AccountFragment.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/fragment/AccountFragment.kt index 52e131ed7985..75385f3ef841 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/fragment/AccountFragment.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/fragment/AccountFragment.kt @@ -35,10 +35,11 @@ class AccountFragment : BaseFragment(), StatusBarPainter, NavigationBarPainter { viewActions = vm.viewActions, onRedeemVoucherClick = { openRedeemVoucherFragment() }, onManageAccountClick = vm::onManageAccountClick, - onLogoutClick = vm::onLogoutClick - ) { - activity?.onBackPressed() - } + onLogoutClick = vm::onLogoutClick, + onDeviceNameInfoClick = vm::onDeviceNameInfoClick, + onDismissInfoClick = vm::onDismissInfoClick, + onBackClick = { activity?.onBackPressedDispatcher?.onBackPressed() } + ) } } } 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 30c1464a91e9..5e818ea9c243 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 @@ -3,11 +3,14 @@ package net.mullvad.mullvadvpn.viewmodel import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import net.mullvad.mullvadvpn.compose.state.AccountUiState import net.mullvad.mullvadvpn.repository.AccountRepository @@ -24,27 +27,33 @@ class AccountViewModel( private val _viewActions = MutableSharedFlow(extraBufferCapacity = 1) val viewActions = _viewActions.asSharedFlow() - private val vmState: StateFlow = - combine(deviceRepository.deviceState, accountRepository.accountExpiryState) { + private val dialogState = + MutableStateFlow(AccountScreenDialogState.NoDialog) + + private val vmState: StateFlow = + combine(deviceRepository.deviceState, accountRepository.accountExpiryState, dialogState) { deviceState, - accountExpiry -> - AccountUiState( - deviceName = deviceState.deviceName() ?: "", - accountNumber = deviceState.token() ?: "", - accountExpiry = accountExpiry.date() + accountExpiry, + dialogState -> + AccountViewModelState( + deviceState = deviceState, + accountExpiry = accountExpiry, + dialogState = dialogState ) } .stateIn( viewModelScope, SharingStarted.WhileSubscribed(), - AccountUiState(deviceName = "", accountNumber = "", accountExpiry = null) + AccountViewModelState.default() ) val uiState = - vmState.stateIn( - viewModelScope, - SharingStarted.WhileSubscribed(), - AccountUiState(deviceName = "", accountNumber = "", accountExpiry = null) - ) + vmState + .map(AccountViewModelState::toUiState) + .stateIn( + viewModelScope, + SharingStarted.WhileSubscribed(), + AccountUiState.DefaultUiState() + ) fun onManageAccountClick() { viewModelScope.launch { @@ -60,6 +69,18 @@ class AccountViewModel( accountRepository.logout() } + fun onDeviceNameInfoClick() { + dialogState.update { AccountScreenDialogState.DeviceNameInfoDialog } + } + + fun onDismissInfoClick() { + hideDialog() + } + + private fun hideDialog() { + dialogState.update { AccountScreenDialogState.NoDialog } + } + sealed class ViewAction { data class OpenAccountView(val token: String) : ViewAction() } diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/AccountViewModelState.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/AccountViewModelState.kt new file mode 100644 index 000000000000..d1b11837b023 --- /dev/null +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/AccountViewModelState.kt @@ -0,0 +1,43 @@ +package net.mullvad.mullvadvpn.viewmodel + +import net.mullvad.mullvadvpn.compose.state.AccountUiState +import net.mullvad.mullvadvpn.model.AccountExpiry +import net.mullvad.mullvadvpn.model.DeviceState + +data class AccountViewModelState( + val deviceState: DeviceState, + val accountExpiry: AccountExpiry, + val dialogState: AccountScreenDialogState +) { + fun toUiState(): AccountUiState { + return when (dialogState) { + is AccountScreenDialogState.NoDialog -> + AccountUiState.DefaultUiState( + deviceName = deviceState.deviceName() ?: "", + accountNumber = deviceState.token() ?: "", + accountExpiry = accountExpiry.date() + ) + is AccountScreenDialogState.DeviceNameInfoDialog -> + AccountUiState.DeviceNameDialogUiState( + deviceName = deviceState.deviceName() ?: "", + accountNumber = deviceState.token() ?: "", + accountExpiry = accountExpiry.date() + ) + } + } + + companion object { + fun default() = + AccountViewModelState( + deviceState = DeviceState.Unknown, + accountExpiry = AccountExpiry.Missing, + dialogState = AccountScreenDialogState.NoDialog + ) + } +} + +sealed class AccountScreenDialogState { + data object NoDialog : AccountScreenDialogState() + + data object DeviceNameInfoDialog : AccountScreenDialogState() +} diff --git a/android/lib/resource/src/main/res/values/strings.xml b/android/lib/resource/src/main/res/values/strings.xml index 05e516c362d3..1a81a0d5803c 100644 --- a/android/lib/resource/src/main/res/values/strings.xml +++ b/android/lib/resource/src/main/res/values/strings.xml @@ -217,4 +217,7 @@ Remove custom port Valid ranges: %s Enter port + This is the name assigned to the device. Each device logged in on a Mullvad account gets a unique name that helps you identify it when you manage your devices in the app or on the website. + You can have up to 5 devices logged in on one Mullvad account. + If you log out, the device and the device name is removed. When you log back in again, the device will get a new name.