Skip to content

Conversation

@SaintPatrck
Copy link
Contributor

@SaintPatrck SaintPatrck commented Sep 3, 2025

🎟️ Tracking

PM-25423

📔 Objective

This commit introduces BidiTextManager to manage and ensure correct display of text containing both LTR and RTL characters. It includes:

  • BidiTextManager interface with a unicodeWrap method.
  • BidiTextManagerImpl as the default implementation, using androidx.core.text.BidiFormatter.
  • LocalBidiTextManager CompositionLocal for providing the manager in Compose UI.

📸 Screenshots

Before

image image

After

image image

⏰ Reminders before review

  • Contributor guidelines followed
  • All formatters and local linters executed and passed
  • Written new unit and / or integration tests where applicable
  • Protected functional changes with optionality (feature flags)
  • Used internationalization (i18n) for all UI strings
  • CI builds passed
  • Communicated to DevOps any deployment requirements
  • Updated any necessary documentation (Confluence, contributing docs) or informed the documentation team

🦮 Reviewer guidelines

  • 👍 (:+1:) or similar for great changes
  • 📝 (:memo:) or ℹ️ (:information_source:) for notes or general info
  • ❓ (:question:) for questions
  • 🤔 (:thinking:) or 💭 (:thought_balloon:) for more open inquiry that's not quite a confirmed issue and could potentially benefit from discussion
  • 🎨 (:art:) for suggestions / improvements
  • ❌ (:x:) or ⚠️ (:warning:) for more significant problems or concerns needing attention
  • 🌱 (:seedling:) or ♻️ (:recycle:) for future improvements or indications of technical debt
  • ⛏ (:pick:) for minor or nitpick changes

@github-actions
Copy link
Contributor

github-actions bot commented Sep 3, 2025

Logo
Checkmarx One – Scan Summary & Detailscd892bb2-5c69-4b29-9d36-57ee8ae43bac

Great job! No new security vulnerabilities introduced in this pull request

@codecov
Copy link

codecov bot commented Sep 3, 2025

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 84.78%. Comparing base (562b48d) to head (eff8906).

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #5825      +/-   ##
==========================================
+ Coverage   84.74%   84.78%   +0.03%     
==========================================
  Files         737      720      -17     
  Lines       55439    55051     -388     
  Branches     7655     7653       -2     
==========================================
- Hits        46983    46675     -308     
+ Misses       5775     5695      -80     
  Partials     2681     2681              

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@SaintPatrck
Copy link
Contributor Author

@codokie Can you confirm if the Verification Codes are being handled correctly in the screenshots? If the code is 123 456, should it appear as 456 123, or 123 456 when the app language is RTL?

@codokie
Copy link

codokie commented Sep 4, 2025

If the code is 123 456, should it appear as 456 123, or 123 456 when the app language is RTL?

It should appear as 123 456. Even though letters are written from right-to-left, Western Arabic numerals are interpreted from left-to-right.

I have seen a very few apps which do not take this into consideration and thus expect 456 123 or 654 321 as input.

Most localized services consider 123456 as the correct input because they don’t even use a space to divide the verification code. Preferring 456 123 would cause much confusion.

By the way, I’m not sure if #5012 handled verification codes correctly because I had used a dummy account w/o Premium for testing. So it’s good thing that you’ve brought this up.

@codokie
Copy link

codokie commented Sep 4, 2025

While trying to test this change on my end, I noticed that it did not have any effect unless BitwardenTextField and BitwardenPasswordField were modified to use the new BidiTextManager (it was also necessary initialize it in LocalManagerProvider).

Having done that, after a brief testing I can say that your minimalistic method fixes most of the issues described in #4753, with the added benefit that it does not cause alignment issues like in #5012.

However, I’ve noticed 3 issues that make me doubt if unicodeWrap should be the preferred approach:

  1. Saved password with the value of 1234* appears as *1234, which is wrong.
  2. The caret position is not updated correctly. This makes editing text fields uncomfortable.
  3. After pressing the backspace key in an empty input field, the first character is shifted upon entering the second character. Very unexpected.
Video demonstrating points 1 & 2 (click to expand)
1.and.2.webm

Notice that the caret is incorrectly aligned to the left.

The input should have appeared as test12@

Video demonstrating point no. 3 (click to expand)
3.webm

This happened while trying to write user

@SaintPatrck SaintPatrck force-pushed the PM-25423/bidi-text-manager branch from a4253c9 to eff8906 Compare October 23, 2025 02:11
This commit introduces `BidiTextManager` to manage and ensure correct display of text containing both LTR and RTL characters. It includes:
- `BidiTextManager` interface with a `unicodeWrap` method.
- `BidiTextManagerImpl` as the default implementation, using `androidx.core.text.BidiFormatter`.
- `LocalBidiTextManager` CompositionLocal for providing the manager in Compose UI.
…tting methods using native BidiFormatter

- Add forceLtr(), forceRtl(), formatVerificationCode(), formatPhoneNumber(), formatCardNumber() to interface
- Implement all methods using BidiFormatter.unicodeWrap() with appropriate TextDirectionHeuristicsCompat:
  - LTR for forceLtr()
  - RTL for forceRtl()
  - ANYRTL_LTR for unicodeWrap()
- Add basic unit tests for edge cases (empty string handling)
- Full bidi behavior should be tested via instrumentation tests with Android framework
- Create DSL builder function bidiTextManager() to instantiate BidiTextManagerImpl outside ui module
- Add BidiTextManager provider to PlatformUiManagerModule for Hilt dependency injection
- Follow established pattern from CXF module for internal class instantiation
BidiTextManager foundation ready:
- Interface with 6 methods (unicodeWrap, forceLtr, forceRtl, formatVerificationCode, formatPhoneNumber, formatCardNumber)
- Implementation using native BidiFormatter with TextDirectionHeuristicsCompat
- DSL builder for cross-module instantiation
- Hilt DI integration via PlatformUiManagerModule
- Unit tests passing

Ready to proceed with parallel implementation across 3 workstreams
@SaintPatrck SaintPatrck force-pushed the PM-25423/bidi-text-manager branch 2 times, most recently from 243603a to c316882 Compare October 27, 2025 17:21
SaintPatrck and others added 16 commits October 27, 2025 13:23
- Apply formatCardNumber() to card number display
- Apply forceLtr() to card security code field
… for RTL support

Generated with Claude Code

Co-Authored-By: Claude <[email protected]>
… for RTL support

Generated with Claude Code

Co-Authored-By: Claude <[email protected]>
…main support

- Added LocalBidiTextManager import and acquisition
- Applied unicodeWrap() to URI field display values
- Applied unicodeWrap() to URI list display
- Ensures proper bidirectional text rendering for domains in RTL contexts

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
…me for RTL support

- Added LocalBidiTextManager import and acquisition
- Applied forceLtr() to card expiration for MM/YY format consistency
- Applied unicodeWrap() to cardholder name for proper bidirectional rendering
- Ensures date formats remain LTR and names render correctly in RTL contexts

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
…icense, phone, address) for RTL support

- Applied forceLtr() to SSN field for directional consistency
- Applied unicodeWrap() to passport number for bidirectional rendering
- Applied unicodeWrap() to license number for bidirectional rendering
- Applied formatPhoneNumber() to phone field for locale-aware formatting
- Applied unicodeWrap() to address field for bidirectional rendering
- Ensures identity data renders correctly in RTL and LTR contexts

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
…ration

Resolve conflicting declarations from workstream merges in VaultItemCardContent.
Keep single bidiTextManager instance at composable scope.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
…ocal hierarchy

Add BidiTextManager to both app and authenticator LocalManagerProvider
CompositionLocalProvider blocks. Without this, accessing LocalBidiTextManager.current
causes runtime crashes with "CompositionLocal BidiTextManager not present" error.

Files modified:
- app/.../LocalManagerProvider.kt: Add bidiTextManager parameter and provide
- authenticator/.../LocalManagerProvider.kt: Add bidiTextManager parameter and provide

This resolves crashes when viewing TOTP verification codes and other bidi-enabled UI.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
…Content

Alphabetically sort BidiTextManager-related imports to follow Kotlin style
conventions. No functional changes.

Files modified:
- VaultItemCardContent.kt: Removed unused LocalBidiTextManager import
- VaultItemLoginContent.kt: Reordered LocalBidiTextManager and BidiTextManager imports

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
@SaintPatrck SaintPatrck force-pushed the PM-25423/bidi-text-manager branch from c316882 to 407f20b Compare October 27, 2025 17:23
This commit refactors the `BidiTextManager` to remove the logic for formatting credit card numbers into four-digit chunks. The responsibility for formatting is delegated to the UI components that display the card numbers.

The primary objective is to simplify `BidiTextManager`, making it solely responsible for ensuring correct text directionality (LTR/RTL) without handling presentation-layer formatting. This change improves the separation of concerns.

Specific changes:
- Removed the card number chunking logic from `BidiTextManagerImpl`. The `formatCardNumber` function now only enforces Left-To-Right (LTR) directionality.
- Updated `VaultItemCardContent` and `VaultAddEditCardItems` to remove calls to `bidiTextManager.formatCardNumber` and `bidiTextManager.forceLtr`. The values are now passed directly to the composables.
- Removed the `BidiTextManager` provider from the `PlatformUiManagerModule` as it's no longer needed as a singleton.
- Updated `LocalManagerProvider` in both the `app` and `authenticator` modules to instantiate `BidiTextManager` using the `bidiTextManager()` DSL function.
- Removed unnecessary usages of `LocalBidiTextManager` and `forceLtr` for PIN inputs in `BitwardenPinDialog` and `PinInputDialog`.
- Adjusted related unit and UI tests to reflect these changes.
@SaintPatrck SaintPatrck marked this pull request as ready for review October 27, 2025 18:41
@claude
Copy link
Contributor

claude bot commented Oct 27, 2025

Claude finished @SaintPatrck's task —— View job


Code Review: BidiTextManager for Bidirectional Text Handling

I've completed a comprehensive review of this PR. While the implementation demonstrates good architectural practices, there are critical issues identified by @codokie that need to be addressed before merging.


Summary of Changes

This PR introduces BidiTextManager to handle bidirectional text display for RTL languages (Arabic, Hebrew). The implementation:

  • Adds interface BidiTextManager with methods for wrapping text with Unicode directionality markers
  • Provides BidiTextManagerImpl using androidx.core.text.BidiFormatter
  • Integrates via LocalBidiTextManager CompositionLocal
  • Applies to vault items: login credentials, card details, identity fields, and verification codes
  • 15 files changed with +277/-18 lines

Critical Issues ⚠️

1. BLOCKER: TextField Integration Problems

@codokie identified three critical issues when the BidiTextManager is applied to editable text fields (though this PR currently only applies it to read-only display fields):

Issue Details from Testing
  1. Incorrect character ordering: Password 1234* displays as *1234 (wrong order)
  2. Broken caret positioning: Cursor position doesn't update correctly during editing
  3. Character shift on backspace: After backspace in empty field, first character shifts when entering second character

These issues suggest that unicodeWrap() is not suitable for editable text fields - only for read-only display text.

Current Status: This PR only applies BidiTextManager to read-only fields, so these issues don't immediately affect this implementation. However:

  • The interface design suggests it might be used for editable fields in the future
  • Documentation should explicitly warn against using this on editable TextField components
  • Consider adding a comment in BidiTextManager.kt:26 explaining this limitation

Recommendation: Add KDoc warnings and consider whether the interface should be redesigned to prevent misuse on editable fields.


Architecture & Design Issues

2. Missing Comprehensive Tests

Severity: High

BidiTextManagerTest.kt only validates empty string handling. Missing tests for:

  • unicodeWrap() behavior with mixed LTR/RTL text
  • forceLtr() and forceRtl() output verification
  • formatVerificationCode() chunking and directionality
  • formatPhoneNumber() and formatCardNumber() directionality enforcement

Location: ui/src/test/kotlin/com/bitwarden/ui/platform/manager/BidiTextManagerTest.kt:15-31

// Current test coverage is minimal:
@Test
fun `formatVerificationCode handles empty string`() {
    val result = manager.formatVerificationCode("")
    assertEquals("", result)
}

Recommendation: Add comprehensive unit tests or document why instrumentation tests are preferred for BidiFormatter behavior.


3. Inconsistent Application of BidiTextManager

Severity: Medium

The PR applies different methods inconsistently across similar data types:

Field Type Method Used Location
Password unicodeWrap() VaultItemLoginContent.kt:402
URI unicodeWrap() VaultItemLoginContent.kt:516
Card Number formatCardNumber() (forceLtr) VaultItemCardContent.kt:99
Security Code forceLtr() VaultItemCardContent.kt:179
SSN forceLtr() VaultItemIdentityContent.kt:143
Email unicodeWrap() VaultItemIdentityContent.kt:209
Phone formatPhoneNumber() (forceLtr) VaultItemIdentityContent.kt:231
Passport Number unicodeWrap() VaultItemIdentityContent.kt:165
License Number unicodeWrap() VaultItemIdentityContent.kt:187

Questions:

  1. Why does SSN use forceLtr() while passport/license numbers use unicodeWrap()? All are typically numeric/alphanumeric identifiers.
  2. Should email addresses always use unicodeWrap() or should they be forced LTR like phone numbers?
  3. Is there a documented decision matrix for which method to use?

Recommendation: Document the decision criteria for choosing between unicodeWrap(), forceLtr(), and forceRtl() in the interface KDoc or create a separate doc explaining the usage patterns.


4. Verification Code Handling Confirmed Correct

Severity: N/A (Positive Finding)

Per @codokie's confirmation, verification codes correctly display as 123 456 (not 456 123) in RTL mode. Western Arabic numerals are interpreted left-to-right even in RTL contexts. The implementation at VaultItemLoginContent.kt:458-461 and VerificationCodeItem.kt:112 is correct.


Code Quality Issues

5. @OmitFromCoverage Overuse

Severity: Low

Multiple files are marked @OmitFromCoverage without clear justification:

  • BidiTextManagerImpl.kt:11 - Implementation could be unit tested
  • LocalBidiTextManagerProvider.kt:1 - CompositionLocal provider
  • BidiTextManagerBuilder.kt:1 - DSL builder could be tested

Recommendation: Only use @OmitFromCoverage for truly untestable code (e.g., Android framework dependencies). Consider testing BidiTextManagerImpl with instrumented tests or mock BidiFormatter.


6. Card Number Formatting Removed

Severity: Low (Intentional Change)

Commit 12625daab removed card number formatting logic from formatCardNumber(). The method now only applies forceLtr() without grouping digits.

Before:

override fun formatCardNumber(number: String): String {
    val digits = number.filter { it.isDigit() }
    val chunks = digits.chunked(4)
    return forceLtr(chunks.joinToString(" "))
}

After (current):

override fun formatCardNumber(number: String): String {
    if (number.isEmpty()) return ""
    return forceLtr(number)
}

Question: Was this intentional? Does the app handle card number formatting elsewhere?

Recommendation: Confirm this change is intentional and update KDoc if the method no longer provides formatting.


Good Practices Observed 👍

  • Clean architecture: Manager pattern in :ui module aligns with project structure
  • Dependency injection: Proper Hilt integration via DSL builder
  • CompositionLocal usage: Appropriate for cross-cutting UI concerns
  • Interface segregation: Clean interface with focused methods
  • Commit hygiene: 21 well-structured commits with clear messages
  • Security scan passed: No new vulnerabilities introduced
  • Test coverage maintained: 84.78% overall coverage (per Codecov)

Action Items for Author

Must Address (Blocking)

Should Address (Non-blocking but Important)

Nice to Have

  • Add instrumented tests showing actual RTL behavior on device/emulator
  • Consider adding usage examples to method KDocs
  • Document the verification code design decision (123 456 vs 456 123) in code comments

References


@SaintPatrck
Copy link
Contributor Author

Pivoting to a different approach.

@SaintPatrck SaintPatrck deleted the PM-25423/bidi-text-manager branch October 29, 2025 18:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants