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 5d3e229a8ba6..c4d2fab62eda 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 @@ -9,7 +9,7 @@ import io.mockk.mockk import io.mockk.verify import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.asSharedFlow -import net.mullvad.mullvadvpn.compose.state.AccountUiState +import net.mullvad.mullvadvpn.viewmodel.AccountUiState import net.mullvad.mullvadvpn.viewmodel.AccountViewModel import org.junit.Before import org.junit.Rule 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..e80a4250410e --- /dev/null +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/dialog/DeviceNameInfoDialog.kt @@ -0,0 +1,20 @@ +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 = + buildString { + appendLine(stringResource(id = R.string.device_name_info_first_paragraph)) + appendLine() + appendLine(stringResource(id = R.string.device_name_info_second_paragraph)) + appendLine() + appendLine(stringResource(id = R.string.device_name_info_third_paragraph)) + }, + 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 22dfc34269d1..f27621539316 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 @@ -1,9 +1,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 +12,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.Surface import androidx.compose.material3.Text @@ -19,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 com.google.accompanist.systemuicontroller.rememberSystemUiController @@ -35,12 +38,14 @@ 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.state.AccountUiState +import net.mullvad.mullvadvpn.compose.dialog.DeviceNameInfoDialog import net.mullvad.mullvadvpn.constant.IS_PLAY_BUILD import net.mullvad.mullvadvpn.lib.common.util.capitalizeFirstCharOfEachWord import net.mullvad.mullvadvpn.lib.common.util.openAccountPageInBrowser import net.mullvad.mullvadvpn.lib.theme.Dimens import net.mullvad.mullvadvpn.util.toExpiryDateString +import net.mullvad.mullvadvpn.viewmodel.AccountScreenDialogState +import net.mullvad.mullvadvpn.viewmodel.AccountUiState import net.mullvad.mullvadvpn.viewmodel.AccountViewModel @OptIn(ExperimentalMaterial3Api::class) @@ -52,7 +57,8 @@ private fun PreviewAccountScreen() { AccountUiState( deviceName = "Test Name", accountNumber = "1234123412341234", - accountExpiry = null + accountExpiry = null, + dialogState = AccountScreenDialogState.NoDialog ), viewActions = MutableSharedFlow().asSharedFlow(), enterTransitionEndAction = MutableSharedFlow() @@ -64,6 +70,8 @@ private fun PreviewAccountScreen() { fun AccountScreen( uiState: AccountUiState, viewActions: SharedFlow, + onDeviceNameInfoClick: () -> Unit = {}, + onDismissInfoClick: () -> Unit = {}, enterTransitionEndAction: SharedFlow, onRedeemVoucherClick: () -> Unit = {}, onManageAccountClick: () -> Unit = {}, @@ -79,6 +87,10 @@ fun AccountScreen( LaunchedEffect(Unit) { enterTransitionEndAction.collect { systemUiController.setStatusBarColor(backgroundColor) } } + if (uiState.dialogState == AccountScreenDialogState.DeviceNameInfoDialog) { + DeviceNameInfoDialog(onDismissInfoClick) + } + CollapsingToolbarScaffold( backgroundColor = MaterialTheme.colorScheme.background, modifier = Modifier.fillMaxSize(), @@ -127,10 +139,21 @@ fun AccountScreen( 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, @@ -142,9 +165,7 @@ fun AccountScreen( top = Dimens.smallPadding ) ) - - CopyableObfuscationView(content = uiState.accountNumber) - + CopyableObfuscationView(content = uiState.accountNumber ?: "") Text( style = MaterialTheme.typography.labelMedium, text = stringResource(id = R.string.paid_until), 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 deleted file mode 100644 index a9527955711f..000000000000 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/AccountUiState.kt +++ /dev/null @@ -1,9 +0,0 @@ -package net.mullvad.mullvadvpn.compose.state - -import org.joda.time.DateTime - -data class AccountUiState( - val deviceName: String, - val accountNumber: String, - val accountExpiry: DateTime? -) 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 39013f11a868..0a1035732c49 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 @@ -36,10 +36,11 @@ class AccountFragment : BaseFragment(), StatusBarPainter, NavigationBarPainter { enterTransitionEndAction = vm.enterTransitionEndAction, 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/AccountUiState.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/AccountUiState.kt new file mode 100644 index 000000000000..2b3d90540466 --- /dev/null +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/AccountUiState.kt @@ -0,0 +1,28 @@ +package net.mullvad.mullvadvpn.viewmodel + +import net.mullvad.mullvadvpn.model.AccountExpiry +import net.mullvad.mullvadvpn.model.DeviceState +import org.joda.time.DateTime + +data class AccountUiState( + val deviceName: String?, + val accountNumber: String?, + val accountExpiry: DateTime?, + val dialogState: AccountScreenDialogState = AccountScreenDialogState.NoDialog +) { + companion object { + fun default() = + AccountUiState( + deviceName = DeviceState.Unknown.deviceName(), + accountNumber = DeviceState.Unknown.token(), + accountExpiry = AccountExpiry.Missing.date(), + dialogState = AccountScreenDialogState.NoDialog + ) + } +} + +sealed class AccountScreenDialogState { + data object NoDialog : AccountScreenDialogState() + + data object DeviceNameInfoDialog : AccountScreenDialogState() +} 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 e359deed2e3f..097da240bd44 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,13 +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.stateIn +import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch -import net.mullvad.mullvadvpn.compose.state.AccountUiState import net.mullvad.mullvadvpn.repository.AccountRepository import net.mullvad.mullvadvpn.repository.DeviceRepository import net.mullvad.mullvadvpn.ui.serviceconnection.ServiceConnectionManager @@ -25,27 +26,24 @@ class AccountViewModel( private val _enterTransitionEndAction = MutableSharedFlow() val viewActions = _viewActions.asSharedFlow() + private val dialogState = + MutableStateFlow(AccountScreenDialogState.NoDialog) + private val vmState: StateFlow = - combine(deviceRepository.deviceState, accountRepository.accountExpiryState) { + combine(deviceRepository.deviceState, accountRepository.accountExpiryState, dialogState) { deviceState, - accountExpiry -> + accountExpiry, + dialogState -> AccountUiState( - deviceName = deviceState.deviceName() ?: "", - accountNumber = deviceState.token() ?: "", - accountExpiry = accountExpiry.date() + deviceName = deviceState.deviceName(), + accountNumber = deviceState.token(), + accountExpiry = accountExpiry.date(), + dialogState = dialogState ) } - .stateIn( - viewModelScope, - SharingStarted.WhileSubscribed(), - AccountUiState(deviceName = "", accountNumber = "", accountExpiry = null) - ) + .stateIn(viewModelScope, SharingStarted.WhileSubscribed(), AccountUiState.default()) val uiState = - vmState.stateIn( - viewModelScope, - SharingStarted.WhileSubscribed(), - AccountUiState(deviceName = "", accountNumber = "", accountExpiry = null) - ) + vmState.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), AccountUiState.default()) val enterTransitionEndAction = _enterTransitionEndAction.asSharedFlow() @@ -63,6 +61,18 @@ class AccountViewModel( accountRepository.logout() } + fun onDeviceNameInfoClick() { + dialogState.update { AccountScreenDialogState.DeviceNameInfoDialog } + } + + fun onDismissInfoClick() { + hideDialog() + } + + private fun hideDialog() { + dialogState.update { AccountScreenDialogState.NoDialog } + } + fun onTransitionAnimationEnd() { viewModelScope.launch { _enterTransitionEndAction.emit(Unit) } } 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 82048b5fd145..fc1fd5e99b04 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 @@ -73,7 +73,7 @@ class AccountViewModelTest { // Act, Assert viewModel.uiState.test { var result = awaitItem() - assertEquals("", result.deviceName) + assertEquals(null, result.deviceName) deviceState.value = DeviceState.LoggedIn(accountAndDevice = dummyAccountAndDevice) result = awaitItem() assertEquals(DUMMY_DEVICE_NAME, result.accountNumber)