summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorKalle Lindström <karl.lindstrom@mullvad.net>2026-04-20 09:40:28 +0200
committerKalle Lindström <karl.lindstrom@mullvad.net>2026-04-20 09:40:28 +0200
commit819de86a77c006a180d41707d14fa1be6ef87bb3 (patch)
tree75b679826731f37db7e14a7e9c12dba969e11d6c
parentbc64759830f77c0f14051e868e72223a007de07a (diff)
parentfbfdbb487348413da1b9a6828e3bab99fadff843 (diff)
downloadmullvadvpn-819de86a77c006a180d41707d14fa1be6ef87bb3.tar.xz
mullvadvpn-819de86a77c006a180d41707d14fa1be6ef87bb3.zip
Merge branch 'add-monospace-font-droid-2499'
-rw-r--r--android/lib/feature/account/impl/src/main/kotlin/net/mullvad/mullvadvpn/feature/account/impl/AccountNumberView.kt2
-rw-r--r--android/lib/feature/account/impl/src/main/kotlin/net/mullvad/mullvadvpn/feature/account/impl/InformationView.kt3
-rw-r--r--android/lib/feature/account/impl/src/main/kotlin/net/mullvad/mullvadvpn/feature/account/impl/Text.kt3
-rw-r--r--android/lib/feature/home/impl/src/main/kotlin/net/mullvad/mullvadvpn/feature/home/impl/welcome/WelcomeScreen.kt2
-rw-r--r--android/lib/feature/login/impl/src/main/kotlin/net/mullvad/mullvadvpn/feature/login/impl/LoginScreen.kt84
5 files changed, 76 insertions, 18 deletions
diff --git a/android/lib/feature/account/impl/src/main/kotlin/net/mullvad/mullvadvpn/feature/account/impl/AccountNumberView.kt b/android/lib/feature/account/impl/src/main/kotlin/net/mullvad/mullvadvpn/feature/account/impl/AccountNumberView.kt
index a2e4de851f..6438ea166c 100644
--- a/android/lib/feature/account/impl/src/main/kotlin/net/mullvad/mullvadvpn/feature/account/impl/AccountNumberView.kt
+++ b/android/lib/feature/account/impl/src/main/kotlin/net/mullvad/mullvadvpn/feature/account/impl/AccountNumberView.kt
@@ -2,6 +2,7 @@ package net.mullvad.mullvadvpn.feature.account.impl
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
+import androidx.compose.ui.text.font.FontFamily
import net.mullvad.mullvadvpn.lib.common.util.groupPasswordModeWithSpaces
import net.mullvad.mullvadvpn.lib.common.util.groupWithSpaces
@@ -16,6 +17,7 @@ fun AccountNumberView(
if (obfuscateWithPasswordDots) accountNumber.groupPasswordModeWithSpaces()
else accountNumber.groupWithSpaces(),
modifier = modifier,
+ fontFamily = FontFamily.Monospace,
whenMissing = MissingPolicy.SHOW_SPINNER,
)
}
diff --git a/android/lib/feature/account/impl/src/main/kotlin/net/mullvad/mullvadvpn/feature/account/impl/InformationView.kt b/android/lib/feature/account/impl/src/main/kotlin/net/mullvad/mullvadvpn/feature/account/impl/InformationView.kt
index 14cf201bb9..0d1831c0de 100644
--- a/android/lib/feature/account/impl/src/main/kotlin/net/mullvad/mullvadvpn/feature/account/impl/InformationView.kt
+++ b/android/lib/feature/account/impl/src/main/kotlin/net/mullvad/mullvadvpn/feature/account/impl/InformationView.kt
@@ -6,6 +6,7 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
+import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.tooling.preview.Preview
import net.mullvad.mullvadvpn.lib.ui.designsystem.MullvadCircularProgressIndicatorSmall
import net.mullvad.mullvadvpn.lib.ui.theme.Dimens
@@ -28,6 +29,7 @@ fun InformationView(
modifier: Modifier = Modifier,
whenMissing: MissingPolicy = MissingPolicy.SHOW_VIEW,
maxLines: Int = 1,
+ fontFamily: FontFamily? = null,
) {
return if (content.isNotEmpty()) {
AutoResizeText(
@@ -37,6 +39,7 @@ fun InformationView(
maxTextSize = MaterialTheme.typography.titleMedium.fontSize,
maxLines = maxLines,
modifier = modifier.padding(vertical = Dimens.smallPadding),
+ fontFamily = fontFamily,
)
} else {
when (whenMissing) {
diff --git a/android/lib/feature/account/impl/src/main/kotlin/net/mullvad/mullvadvpn/feature/account/impl/Text.kt b/android/lib/feature/account/impl/src/main/kotlin/net/mullvad/mullvadvpn/feature/account/impl/Text.kt
index 333bf95f94..c3c775d585 100644
--- a/android/lib/feature/account/impl/src/main/kotlin/net/mullvad/mullvadvpn/feature/account/impl/Text.kt
+++ b/android/lib/feature/account/impl/src/main/kotlin/net/mullvad/mullvadvpn/feature/account/impl/Text.kt
@@ -13,6 +13,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawWithContent
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.unit.TextUnit
import androidx.compose.ui.unit.sp
@@ -28,6 +29,7 @@ fun AutoResizeText(
style: TextStyle = LocalTextStyle.current,
maxLines: Int = Int.MAX_VALUE,
color: Color = MaterialTheme.colorScheme.onSurface,
+ fontFamily: FontFamily? = null,
) {
var adjustedFontSize by remember { mutableFloatStateOf(maxTextSize.value) }
var isReadyToDraw by remember { mutableStateOf(false) }
@@ -38,6 +40,7 @@ fun AutoResizeText(
style = style,
color = color,
fontSize = adjustedFontSize.sp,
+ fontFamily = fontFamily,
onTextLayout = {
if (it.didOverflowHeight && isReadyToDraw.not()) {
val nextFontSizeValue = adjustedFontSize - textSizeStep.value
diff --git a/android/lib/feature/home/impl/src/main/kotlin/net/mullvad/mullvadvpn/feature/home/impl/welcome/WelcomeScreen.kt b/android/lib/feature/home/impl/src/main/kotlin/net/mullvad/mullvadvpn/feature/home/impl/welcome/WelcomeScreen.kt
index 4aaedd3a97..d2bc1d9b1a 100644
--- a/android/lib/feature/home/impl/src/main/kotlin/net/mullvad/mullvadvpn/feature/home/impl/welcome/WelcomeScreen.kt
+++ b/android/lib/feature/home/impl/src/main/kotlin/net/mullvad/mullvadvpn/feature/home/impl/welcome/WelcomeScreen.kt
@@ -31,6 +31,7 @@ import androidx.compose.ui.platform.LocalResources
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
@@ -280,6 +281,7 @@ private fun AccountNumberRow(snackbarHostState: SnackbarHostState, state: Welcom
text = state.accountNumber?.value?.groupWithSpaces() ?: "",
modifier = Modifier.weight(1f),
style = MaterialTheme.typography.headlineSmall,
+ fontFamily = FontFamily.Monospace,
color = MaterialTheme.colorScheme.onSurface,
)
diff --git a/android/lib/feature/login/impl/src/main/kotlin/net/mullvad/mullvadvpn/feature/login/impl/LoginScreen.kt b/android/lib/feature/login/impl/src/main/kotlin/net/mullvad/mullvadvpn/feature/login/impl/LoginScreen.kt
index 000faa6f16..3662608e5e 100644
--- a/android/lib/feature/login/impl/src/main/kotlin/net/mullvad/mullvadvpn/feature/login/impl/LoginScreen.kt
+++ b/android/lib/feature/login/impl/src/main/kotlin/net/mullvad/mullvadvpn/feature/login/impl/LoginScreen.kt
@@ -46,6 +46,9 @@ import androidx.compose.ui.autofill.ContentType
import androidx.compose.ui.draw.clip
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusProperties
+import androidx.compose.ui.layout.AlignmentLine
+import androidx.compose.ui.layout.FirstBaseline
+import androidx.compose.ui.layout.layout
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.platform.LocalResources
@@ -56,8 +59,10 @@ import androidx.compose.ui.semantics.contentType
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.SpanStyle
+import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
+import androidx.compose.ui.text.rememberTextMeasurer
import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.text.style.TextDirection
import androidx.compose.ui.text.style.TextOverflow
@@ -157,6 +162,7 @@ fun Login(
message = resources.getString(R.string.error_occurred)
)
}
+
is ApiUnreachableInfoDialogResult.Success -> {
when (it.arg.action) {
LoginAction.LOGIN -> vm.login(state.accountNumberInput)
@@ -170,14 +176,19 @@ fun Login(
when (it) {
LoginUiSideEffect.NavigateToWelcome ->
navigator.navigate(WelcomeNavKey, clearBackStack = true)
+
is LoginUiSideEffect.NavigateToConnect ->
navigator.navigate(ConnectNavKey, clearBackStack = true)
+
is LoginUiSideEffect.TooManyDevices ->
navigator.navigate(DeviceListNavKey(it.accountNumber))
+
LoginUiSideEffect.NavigateToOutOfTime ->
navigator.navigate(OutOfTimeNavKey, clearBackStack = true)
+
LoginUiSideEffect.NavigateToCreateAccountConfirmation ->
navigator.navigate(CreateAccountConfirmationNavKey)
+
LoginUiSideEffect.GenericError ->
snackbarHostState.showSnackbarImmediately(
message = resources.getString(R.string.error_occurred)
@@ -366,30 +377,21 @@ private fun ColumnScope.LoginInput(
accountNumberVisualTransformation(showPassword, if (showLastChar) 1 else 0),
enabled = state.loginState is LoginState.Idle,
colors = mullvadWhiteTextFieldColors(),
- textStyle = MaterialTheme.typography.bodyLarge.copy(textDirection = TextDirection.Ltr),
+ textStyle =
+ MaterialTheme.typography.bodyLarge.copy(
+ textDirection = TextDirection.Ltr,
+ fontFamily = FontFamily.Monospace,
+ ),
isError = state.loginState.isError(),
)
AnimatedVisibility(
visible = state.lastUsedAccount != null && state.loginState is LoginState.Idle
) {
- val token = state.lastUsedAccount?.value.orEmpty()
- val accountTransformation =
- remember(showPassword) {
- accountNumberVisualTransformation(
- showPassword,
- showLastX = ACCOUNT_NUMBER_CHUNK_SIZE,
- )
- }
- val transformedText =
- remember(token, accountTransformation) {
- accountTransformation.filter(AnnotatedString(token)).text
- }
-
- // Since content is number we should always do Ltr
CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Ltr) {
AccountDropDownItem(
- accountNumber = transformedText.toString(),
+ accountNumber = state.lastUsedAccount?.value.orEmpty(),
+ showPassword = showPassword,
onClick = {
state.lastUsedAccount?.let {
onAccountNumberChange(it.value)
@@ -416,6 +418,7 @@ private fun LoginIcon(loginState: LoginState, modifier: Modifier = Modifier) {
} else {
// If view is Idle, we display empty box to keep the same size as other states
}
+
is LoginState.Loading -> MullvadCircularProgressIndicatorLarge()
LoginState.Success ->
Image(
@@ -436,8 +439,10 @@ private fun LoginState.title(): String =
is LoginUiStateError.LoginError -> R.string.login_fail_title
is LoginUiStateError.CreateAccountError ->
R.string.create_account_fail_title
+
null -> R.string.log_in
}
+
is LoginState.Loading -> R.string.logging_in_title
LoginState.Success -> R.string.logged_in_title
}
@@ -472,6 +477,7 @@ private fun LoginState.supportingText(
(loginUiStateError is LoginUiStateError.LoginError.ApiUnreachable ||
loginUiStateError is LoginUiStateError.CreateAccountError.ApiUnreachable)
-> apiUnreachableText(loginUiStateError, onShowApiUnreachableDialog)
+
is LoginState.Idle -> {
when (loginUiStateError) {
LoginUiStateError.LoginError.InvalidCredentials -> R.string.login_fail_description
@@ -479,16 +485,20 @@ private fun LoginState.supportingText(
LoginUiStateError.LoginError.NoInternetConnection,
LoginUiStateError.CreateAccountError.NoInternetConnection ->
R.string.no_internet_connection
+
LoginUiStateError.LoginError.ApiUnreachable,
LoginUiStateError.CreateAccountError.ApiUnreachable -> R.string.api_unreachable
+
LoginUiStateError.LoginError.TooManyAttempts,
LoginUiStateError.CreateAccountError.TooManyAttempts ->
R.string.login_error_too_many_attempts
+
is LoginUiStateError.LoginError.Unknown -> R.string.error_occurred
LoginUiStateError.CreateAccountError.Unknown -> R.string.failed_to_create_account
null -> null
}?.toAnnotatedString()
}
+
is LoginState.Loading.CreatingAccount -> R.string.creating_new_account.toAnnotatedString()
is LoginState.Loading.LoggingIn -> R.string.logging_in_description.toAnnotatedString()
LoginState.Success -> R.string.logged_in_description.toAnnotatedString()
@@ -516,11 +526,21 @@ private fun Int.toAnnotatedString(): AnnotatedString = AnnotatedString(stringRes
@Composable
private fun AccountDropDownItem(
modifier: Modifier = Modifier,
+ showPassword: Boolean,
accountNumber: String,
enabled: Boolean,
onClick: () -> Unit,
onDeleteClick: () -> Unit,
) {
+ val accountTransformation =
+ remember(showPassword) {
+ accountNumberVisualTransformation(showPassword, showLastX = ACCOUNT_NUMBER_CHUNK_SIZE)
+ }
+ val transformedText =
+ remember(accountNumber, accountTransformation) {
+ accountTransformation.filter(AnnotatedString(accountNumber)).text
+ }
+
Row(
modifier =
modifier
@@ -534,6 +554,19 @@ private fun AccountDropDownItem(
.height(IntrinsicSize.Min),
verticalAlignment = Alignment.CenterVertically,
) {
+
+ // Hack, our PASSWORD_UNICODE dot char changes the baseline height, so this workaround
+ // ensures we always place it at the same baseline
+ val textStyle = MaterialTheme.typography.bodyLarge.copy(fontFamily = FontFamily.Monospace)
+ // Measure the digit baseline once to use as a fixed reference
+ val textMeasurer = rememberTextMeasurer()
+ val digitBaseline =
+ remember(textStyle) {
+ textMeasurer
+ .measure(text = AnnotatedString("0"), style = textStyle, maxLines = 1)
+ .firstBaseline
+ }
+
Box(
modifier =
Modifier.clickable(enabled = enabled, onClick = onClick)
@@ -543,9 +576,24 @@ private fun AccountDropDownItem(
contentAlignment = Alignment.CenterStart,
) {
Text(
- text = accountNumber,
+ text = transformedText,
overflow = TextOverflow.Clip,
- style = MaterialTheme.typography.bodyLarge,
+ style = textStyle,
+ maxLines = 1,
+ // Place text according to baseline so text does not jump as user hide/show password
+ modifier =
+ Modifier.layout { measurable, constraints ->
+ val placeable = measurable.measure(constraints)
+ val actualBaseline = placeable[FirstBaseline]
+ // Shift the text so its baseline aligns with the digit baseline
+ val yOffset =
+ if (actualBaseline != AlignmentLine.Unspecified) {
+ digitBaseline.toInt() - actualBaseline
+ } else {
+ 0
+ }
+ layout(placeable.width, placeable.height) { placeable.place(0, yOffset) }
+ },
)
}
IconButton(