From de14328cfbd537e878eb845cbbafbc235157e5ed Mon Sep 17 00:00:00 2001 From: Patrick Honkonen Date: Tue, 30 Dec 2025 15:12:41 -0500 Subject: [PATCH 1/2] Force LTR text direction for passwords and TOTP codes Adds TextStyle extension to force left-to-right text direction with locale-aware alignment for sensitive alphanumeric content. This ensures passwords and TOTP codes read correctly in RTL locales while maintaining proper alignment. - Add TextStyle.withForcedLtr() extension in TypographyExtensions.kt - Refactor BitwardenPasswordField to use extension - Refactor VerificationCodeItem (app) to use extension - Refactor VaultVerificationCodeItem (authenticator) to use extension --- .../verificationcode/VerificationCodeItem.kt | 3 +- .../listitem/VaultVerificationCodeItem.kt | 3 +- .../field/BitwardenPasswordField.kt | 3 +- .../ui/platform/util/TypographyExtensions.kt | 45 +++++++++++++++++++ 4 files changed, 51 insertions(+), 3 deletions(-) create mode 100644 ui/src/main/kotlin/com/bitwarden/ui/platform/util/TypographyExtensions.kt diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/verificationcode/VerificationCodeItem.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/verificationcode/VerificationCodeItem.kt index ba82c0c0cfd..366f2714233 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/verificationcode/VerificationCodeItem.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/verificationcode/VerificationCodeItem.kt @@ -24,6 +24,7 @@ import com.bitwarden.ui.platform.components.model.CardStyle import com.bitwarden.ui.platform.resource.BitwardenDrawable import com.bitwarden.ui.platform.resource.BitwardenString import com.bitwarden.ui.platform.theme.BitwardenTheme +import com.bitwarden.ui.platform.util.withForcedLtr /** * The verification code item displayed to the user. @@ -107,7 +108,7 @@ fun VaultVerificationCodeItem( if (!hideAuthCode) { Text( text = authCode.chunked(3).joinToString(" "), - style = BitwardenTheme.typography.sensitiveInfoSmall, + style = BitwardenTheme.typography.sensitiveInfoSmall.withForcedLtr(), color = BitwardenTheme.colorScheme.text.primary, ) diff --git a/authenticator/src/main/kotlin/com/bitwarden/authenticator/ui/platform/components/listitem/VaultVerificationCodeItem.kt b/authenticator/src/main/kotlin/com/bitwarden/authenticator/ui/platform/components/listitem/VaultVerificationCodeItem.kt index 4bd755ae2c7..69b3423d306 100644 --- a/authenticator/src/main/kotlin/com/bitwarden/authenticator/ui/platform/components/listitem/VaultVerificationCodeItem.kt +++ b/authenticator/src/main/kotlin/com/bitwarden/authenticator/ui/platform/components/listitem/VaultVerificationCodeItem.kt @@ -29,6 +29,7 @@ import com.bitwarden.ui.platform.components.model.CardStyle import com.bitwarden.ui.platform.resource.BitwardenDrawable import com.bitwarden.ui.platform.resource.BitwardenString import com.bitwarden.ui.platform.theme.BitwardenTheme +import com.bitwarden.ui.platform.util.withForcedLtr /** * The verification code item displayed to the user. @@ -155,7 +156,7 @@ fun VaultVerificationCodeItem( Text( modifier = Modifier.testTag(tag = "AuthCode"), text = authCode.chunked(size = 3).joinToString(separator = " "), - style = BitwardenTheme.typography.sensitiveInfoSmall, + style = BitwardenTheme.typography.sensitiveInfoSmall.withForcedLtr(), color = BitwardenTheme.colorScheme.text.primary, ) diff --git a/ui/src/main/kotlin/com/bitwarden/ui/platform/components/field/BitwardenPasswordField.kt b/ui/src/main/kotlin/com/bitwarden/ui/platform/components/field/BitwardenPasswordField.kt index 18842041baf..1d34b5a6add 100644 --- a/ui/src/main/kotlin/com/bitwarden/ui/platform/components/field/BitwardenPasswordField.kt +++ b/ui/src/main/kotlin/com/bitwarden/ui/platform/components/field/BitwardenPasswordField.kt @@ -60,6 +60,7 @@ import com.bitwarden.ui.platform.components.util.nonLetterColorVisualTransformat import com.bitwarden.ui.platform.resource.BitwardenDrawable import com.bitwarden.ui.platform.resource.BitwardenString import com.bitwarden.ui.platform.theme.BitwardenTheme +import com.bitwarden.ui.platform.util.withForcedLtr /** * Represents a Bitwarden-styled password field that hoists show/hide password state to the caller. @@ -155,7 +156,7 @@ fun BitwardenPasswordField( var focused by remember { mutableStateOf(value = false) } TextField( colors = bitwardenTextFieldColors(), - textStyle = BitwardenTheme.typography.sensitiveInfoSmall, + textStyle = BitwardenTheme.typography.sensitiveInfoSmall.withForcedLtr(), label = label?.let { { Row(verticalAlignment = Alignment.CenterVertically) { diff --git a/ui/src/main/kotlin/com/bitwarden/ui/platform/util/TypographyExtensions.kt b/ui/src/main/kotlin/com/bitwarden/ui/platform/util/TypographyExtensions.kt new file mode 100644 index 00000000000..413a171909a --- /dev/null +++ b/ui/src/main/kotlin/com/bitwarden/ui/platform/util/TypographyExtensions.kt @@ -0,0 +1,45 @@ +package com.bitwarden.ui.platform.util + +import androidx.compose.runtime.Composable +import androidx.compose.ui.platform.LocalLayoutDirection +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextDirection +import androidx.compose.ui.unit.LayoutDirection +import com.bitwarden.annotation.OmitFromCoverage + +/** + * Returns a [TextStyle] that forces left-to-right text direction while maintaining + * locale-aware alignment. + * + * This extension is designed for sensitive alphanumeric content (passwords, TOTP codes) + * that must always read left-to-right regardless of system locale, but should align + * according to the layout direction (right-aligned in RTL locales, left-aligned in LTR). + * + * **Implementation:** + * - Sets `textDirection = TextDirection.Ltr` to force LTR reading order + * - Sets `textAlign` conditionally: + * - `TextAlign.End` in RTL layouts (aligns to right side) + * - `TextAlign.Start` in LTR layouts (aligns to left side) + * + * **Use cases:** + * - Password fields that should read "Pass123!" not "!321ssaP" in RTL locales + * - TOTP verification codes that should read "123 456" not "654 321" + * - Any alphanumeric content requiring LTR reading with locale-aware positioning + * + * @return A merged [TextStyle] with forced LTR direction and locale-aware alignment. + */ +@OmitFromCoverage +@Composable +fun TextStyle.withForcedLtr(): TextStyle { + val layoutDirection = LocalLayoutDirection.current + return merge( + TextStyle( + textDirection = TextDirection.Ltr, + textAlign = when (layoutDirection) { + LayoutDirection.Rtl -> TextAlign.End + LayoutDirection.Ltr -> TextAlign.Start + }, + ), + ) +} From a0463b52f0a705b3abf1ea936d09faa600fc45a9 Mon Sep 17 00:00:00 2001 From: Patrick Honkonen Date: Mon, 5 Jan 2026 13:57:24 -0500 Subject: [PATCH 2/2] Address review comments - Rename extension to be more descriptive. - Use `Left` and `Right` text alignments instead of `Start` and `End`. --- .../feature/verificationcode/VerificationCodeItem.kt | 4 ++-- .../components/listitem/VaultVerificationCodeItem.kt | 4 ++-- .../platform/components/field/BitwardenPasswordField.kt | 4 ++-- .../bitwarden/ui/platform/util/TypographyExtensions.kt | 9 +++++---- 4 files changed, 11 insertions(+), 10 deletions(-) diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/verificationcode/VerificationCodeItem.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/verificationcode/VerificationCodeItem.kt index 366f2714233..375a04df746 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/verificationcode/VerificationCodeItem.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/verificationcode/VerificationCodeItem.kt @@ -24,7 +24,7 @@ import com.bitwarden.ui.platform.components.model.CardStyle import com.bitwarden.ui.platform.resource.BitwardenDrawable import com.bitwarden.ui.platform.resource.BitwardenString import com.bitwarden.ui.platform.theme.BitwardenTheme -import com.bitwarden.ui.platform.util.withForcedLtr +import com.bitwarden.ui.platform.util.withForcedLtrDirection /** * The verification code item displayed to the user. @@ -108,7 +108,7 @@ fun VaultVerificationCodeItem( if (!hideAuthCode) { Text( text = authCode.chunked(3).joinToString(" "), - style = BitwardenTheme.typography.sensitiveInfoSmall.withForcedLtr(), + style = BitwardenTheme.typography.sensitiveInfoSmall.withForcedLtrDirection(), color = BitwardenTheme.colorScheme.text.primary, ) diff --git a/authenticator/src/main/kotlin/com/bitwarden/authenticator/ui/platform/components/listitem/VaultVerificationCodeItem.kt b/authenticator/src/main/kotlin/com/bitwarden/authenticator/ui/platform/components/listitem/VaultVerificationCodeItem.kt index 69b3423d306..7c0f408986f 100644 --- a/authenticator/src/main/kotlin/com/bitwarden/authenticator/ui/platform/components/listitem/VaultVerificationCodeItem.kt +++ b/authenticator/src/main/kotlin/com/bitwarden/authenticator/ui/platform/components/listitem/VaultVerificationCodeItem.kt @@ -29,7 +29,7 @@ import com.bitwarden.ui.platform.components.model.CardStyle import com.bitwarden.ui.platform.resource.BitwardenDrawable import com.bitwarden.ui.platform.resource.BitwardenString import com.bitwarden.ui.platform.theme.BitwardenTheme -import com.bitwarden.ui.platform.util.withForcedLtr +import com.bitwarden.ui.platform.util.withForcedLtrDirection /** * The verification code item displayed to the user. @@ -156,7 +156,7 @@ fun VaultVerificationCodeItem( Text( modifier = Modifier.testTag(tag = "AuthCode"), text = authCode.chunked(size = 3).joinToString(separator = " "), - style = BitwardenTheme.typography.sensitiveInfoSmall.withForcedLtr(), + style = BitwardenTheme.typography.sensitiveInfoSmall.withForcedLtrDirection(), color = BitwardenTheme.colorScheme.text.primary, ) diff --git a/ui/src/main/kotlin/com/bitwarden/ui/platform/components/field/BitwardenPasswordField.kt b/ui/src/main/kotlin/com/bitwarden/ui/platform/components/field/BitwardenPasswordField.kt index 1d34b5a6add..9898a33b21e 100644 --- a/ui/src/main/kotlin/com/bitwarden/ui/platform/components/field/BitwardenPasswordField.kt +++ b/ui/src/main/kotlin/com/bitwarden/ui/platform/components/field/BitwardenPasswordField.kt @@ -60,7 +60,7 @@ import com.bitwarden.ui.platform.components.util.nonLetterColorVisualTransformat import com.bitwarden.ui.platform.resource.BitwardenDrawable import com.bitwarden.ui.platform.resource.BitwardenString import com.bitwarden.ui.platform.theme.BitwardenTheme -import com.bitwarden.ui.platform.util.withForcedLtr +import com.bitwarden.ui.platform.util.withForcedLtrDirection /** * Represents a Bitwarden-styled password field that hoists show/hide password state to the caller. @@ -156,7 +156,7 @@ fun BitwardenPasswordField( var focused by remember { mutableStateOf(value = false) } TextField( colors = bitwardenTextFieldColors(), - textStyle = BitwardenTheme.typography.sensitiveInfoSmall.withForcedLtr(), + textStyle = BitwardenTheme.typography.sensitiveInfoSmall.withForcedLtrDirection(), label = label?.let { { Row(verticalAlignment = Alignment.CenterVertically) { diff --git a/ui/src/main/kotlin/com/bitwarden/ui/platform/util/TypographyExtensions.kt b/ui/src/main/kotlin/com/bitwarden/ui/platform/util/TypographyExtensions.kt index 413a171909a..212809b1be4 100644 --- a/ui/src/main/kotlin/com/bitwarden/ui/platform/util/TypographyExtensions.kt +++ b/ui/src/main/kotlin/com/bitwarden/ui/platform/util/TypographyExtensions.kt @@ -31,14 +31,15 @@ import com.bitwarden.annotation.OmitFromCoverage */ @OmitFromCoverage @Composable -fun TextStyle.withForcedLtr(): TextStyle { - val layoutDirection = LocalLayoutDirection.current +fun TextStyle.withForcedLtrDirection( + layoutDirection: LayoutDirection = LocalLayoutDirection.current, +): TextStyle { return merge( TextStyle( textDirection = TextDirection.Ltr, textAlign = when (layoutDirection) { - LayoutDirection.Rtl -> TextAlign.End - LayoutDirection.Ltr -> TextAlign.Start + LayoutDirection.Rtl -> TextAlign.Right + LayoutDirection.Ltr -> TextAlign.Left }, ), )