From de14328cfbd537e878eb845cbbafbc235157e5ed Mon Sep 17 00:00:00 2001 From: Patrick Honkonen Date: Tue, 30 Dec 2025 15:12:41 -0500 Subject: [PATCH] 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 ba82c0c0cf..366f271423 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 4bd755ae2c..69b3423d30 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 18842041ba..1d34b5a6ad 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 0000000000..413a171909 --- /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 + }, + ), + ) +}