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 038eb60663db..a25380b64900 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 @@ -6,6 +6,7 @@ import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box 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.fillMaxWidth @@ -14,10 +15,16 @@ import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext @@ -25,6 +32,7 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.sp import kotlinx.coroutines.flow.MutableSharedFlow @@ -34,6 +42,7 @@ import net.mullvad.mullvadvpn.R import net.mullvad.mullvadvpn.compose.button.ActionButton import net.mullvad.mullvadvpn.compose.component.ScaffoldWithTopBar import net.mullvad.mullvadvpn.compose.component.drawVerticalScrollbar +import net.mullvad.mullvadvpn.compose.dialog.InfoDialog import net.mullvad.mullvadvpn.compose.state.WelcomeUiState import net.mullvad.mullvadvpn.lib.common.util.SdkUtils import net.mullvad.mullvadvpn.lib.common.util.groupWithSpaces @@ -41,6 +50,7 @@ import net.mullvad.mullvadvpn.lib.common.util.openAccountPageInBrowser import net.mullvad.mullvadvpn.lib.theme.AlphaTopBar import net.mullvad.mullvadvpn.lib.theme.AppTheme import net.mullvad.mullvadvpn.lib.theme.Dimens +import net.mullvad.mullvadvpn.lib.theme.MullvadWhite import net.mullvad.mullvadvpn.ui.extension.copyToClipboard import net.mullvad.mullvadvpn.viewmodel.WelcomeViewModel @@ -50,7 +60,7 @@ private fun PreviewWelcomeScreen() { AppTheme { WelcomeScreen( showSitePayment = true, - uiState = WelcomeUiState(accountNumber = "4444555566667777"), + uiState = WelcomeUiState(accountNumber = "4444555566667777", deviceName = "Happy Mole"), viewActions = MutableSharedFlow().asSharedFlow(), onSitePaymentClick = {}, onRedeemVoucherClick = {}, @@ -163,6 +173,54 @@ fun WelcomeScreen( style = MaterialTheme.typography.headlineSmall, color = MaterialTheme.colorScheme.onPrimary ) + Row( + modifier = Modifier.padding(horizontal = Dimens.sideMargin), + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + modifier = Modifier.weight(1f, fill = false), + text = + buildString { + append(stringResource(id = R.string.device_name)) + append(": ") + append(uiState.deviceName) + }, + style = MaterialTheme.typography.bodySmall, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + + var showDeviceNameDialog by remember { mutableStateOf(false) } + IconButton( + modifier = Modifier.align(Alignment.CenterVertically), + onClick = { showDeviceNameDialog = true } + ) { + Icon( + painter = painterResource(id = R.drawable.icon_info), + contentDescription = null, + tint = MullvadWhite + ) + } + if (showDeviceNameDialog) { + 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 = { showDeviceNameDialog = false } + ) + } + } Text( text = buildString { diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/WelcomeUiState.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/WelcomeUiState.kt index b8a12ce4ae53..c6959f23e016 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/WelcomeUiState.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/WelcomeUiState.kt @@ -4,5 +4,6 @@ import net.mullvad.mullvadvpn.model.TunnelState data class WelcomeUiState( val tunnelState: TunnelState = TunnelState.Disconnected, - val accountNumber: String? = null + val accountNumber: String? = null, + val deviceName: String? = null ) 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 eaba6ad78417..483d4fe4dc1d 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 @@ -57,7 +57,11 @@ class WelcomeViewModel( it.addDebounceForUnknownState(UNKNOWN_STATE_DEBOUNCE_DELAY_MILLISECONDS) } ) { tunnelState, deviceState -> - WelcomeUiState(tunnelState = tunnelState, accountNumber = deviceState.token()) + WelcomeUiState( + tunnelState = tunnelState, + accountNumber = deviceState.token(), + deviceName = deviceState.deviceName() + ) } } .stateIn(viewModelScope, SharingStarted.WhileSubscribed(), WelcomeUiState()) 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 88f6f0c9cb51..f02cebf0190d 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 @@ -16,6 +16,7 @@ import net.mullvad.mullvadvpn.compose.state.WelcomeUiState import net.mullvad.mullvadvpn.lib.common.test.TestCoroutineRule import net.mullvad.mullvadvpn.model.AccountAndDevice import net.mullvad.mullvadvpn.model.AccountExpiry +import net.mullvad.mullvadvpn.model.Device import net.mullvad.mullvadvpn.model.DeviceState import net.mullvad.mullvadvpn.model.TunnelState import net.mullvad.mullvadvpn.repository.AccountRepository @@ -124,6 +125,8 @@ class WelcomeViewModelTest { runTest(testCoroutineRule.testDispatcher) { // Arrange val expectedAccountNumber = "4444555566667777" + val device: Device = mockk() + every { device.name } returns "" // Act, Assert viewModel.uiState.test { @@ -135,7 +138,7 @@ class WelcomeViewModelTest { accountAndDevice = AccountAndDevice( account_token = expectedAccountNumber, - device = mockk() + device = device ) ) val result = awaitItem() diff --git a/android/lib/resource/src/main/res/values/strings.xml b/android/lib/resource/src/main/res/values/strings.xml index ac979ed2106c..6b1486fe5623 100644 --- a/android/lib/resource/src/main/res/values/strings.xml +++ b/android/lib/resource/src/main/res/values/strings.xml @@ -51,6 +51,9 @@ Privacy policy Account number Device name + 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. Mullvad account number Copied Mullvad account number to clipboard Paid until