You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
[Due for payment 2026-09-02] Android - Onboarding - Missing "employer" in "Submit expenses to my employer" #98690
Version Number: 9.4.53-9 Reproducible in staging?: Yes Reproducible in production?: No If this was caught during regression testing, add the test name, ID and link from BrowserStack:https://github.com/Expensify/Mobile-Expensify/pull/14021 Email or phone of affected tester (no customers):sdsiodjnosdjiodsj@gmail.com Issue reported by: Applause Internal Team Bug source: Exploratory - Significant User Experience Deterioration Device used: Samsung Galaxy S25 FE / Android 16 App Component: Other
Action Performed:
Precondition:
Device is Android 16.
Set device font size to minimum.
Launch Expensify app.
Log in with a new Gmail account.
Tap Skip on work email step.
Tap Submit expenses to my employer.
Tap app back button.
Expected Result:
"Submit expenses to my employer" will be shown after navigating back.
Actual Result:
"Submit expenses to my" is shown after navigating back.
👋 Friendly reminder that deploy blockers are time-sensitive ⏱ issues! Check out the open `StagingDeployCash` deploy checklist to see the list of PRs included in this release, then work quickly to do one of the following:
Identify the pull request that introduced this issue and revert it.
Recommendation: NEEDS INVESTIGATION (revert #97590 first if an on-device bisect confirms it)
The bug is a staging-only, Android-app-only text-truncation regression on the Onboarding "purpose" screen. Two PRs in this deploy touch that exact area, but a line-level review of both did not isolate a definitive change to the title's line-wrapping, so an on-device bisect (Android 16, device font size = minimum) is needed before reverting.
Assigned: @grgia, @marcochavezf, @suneox (approving reviewers of the highest-confidence suspect #97590; the PR author is MelvinBot). cc @war-in for secondary suspect #97339. Labels: No changes. Kept DeployBlockerCash — this is a frontend/App regression, it is not reproducible in production, and it is the only blocker label on the issue. No DeployBlocker (Web) label is present.
📋 Detailed Analysis
Affected code path
The purpose options ("Submit expenses to my employer", etc.) are rendered by BaseOnboardingPurpose.tsx via MenuItemList, which passes numberOfLinesTitle: 0 per item. MenuItemList forwards that prop to the legacyMenuItem component (src/components/MenuItem/MenuItem.tsx), not the new compound/preset API. In the legacy component, numberOfLinesTitle: 0 resolves to:
So the intended behavior is that a long title wraps to a second line ("Submit expenses to my / employer"). The reported symptom ("Submit expenses to my" with "employer" cut off) is a single-line clamp — the title is losing its ability to wrap after the screen is remounted on back-navigation.
Evidence — why these two PRs
Only two PRs in this StagingDeployCash checklist (#98677) touch this area:
Directly rewrites src/pages/OnboardingPurpose/BaseOnboardingPurpose.tsx, swapping HeaderWithBackButton (with step counter / progress bar) for a new OnboardingHeader, and restructures the onboarding header layout across every onboarding screen.
Adds a new fixed-size header container style (onboardingHeaderContainer: height: 52, marginTop: 8) and removes the progress-bar styles in src/styles/index.ts.
The change is scoped to onboarding, which matches a regression that appears only on an onboarding screen. A layout/remount change on this exact screen is the most plausible trigger for text that renders correctly on first mount but clamps after back-navigation.
A 60+ file refactor of the MenuItem component that renders the truncating text. It introduces a new src/components/MenuItem/leaves/text/MenuItemTitle.tsx leaf that hardcodes numberOfLines={1} + styles.pre and ignores numberOfLinesTitle — behavior that would produce exactly this truncation.
However, onboarding does not use that leaf (it uses the legacy MenuItem path), and the legacy title-wrapping logic is unchanged by this PR (its only MenuItem.tsx edits are import reordering, extracting a useRemoveNonInteractiveClickHandler hook, and an opacityAnimationDuration value). If this PR were the cause, we would expect widespread menu-text truncation, not a single onboarding option — and no other such blocker exists. This asymmetry is why Remove onboarding progress bar #97590 (onboarding-scoped) ranks above [No QA] MenuItem decomposition - simplest usages #97339.
Verification
Affected file: src/pages/OnboardingPurpose/BaseOnboardingPurpose.tsx (renders the options) → src/components/MenuItemList.tsx → src/components/MenuItem/MenuItem.tsx (legacy title rendering).
Reproduces on staging only (v9.4.53-9), not production → cause is in this deploy.
Root cause
Not isolated to a single line with high confidence. The symptom (a title that should wrap losing its wrap and clamping to one line after remount, at minimum device font size on Android 16) points at a layout/measurement regression on the onboarding purpose screen rather than a static prop change. The header/layout restructure in #97590 is the most likely trigger. Recommended next step: bisect #97590 vs #97339 on a real Android 16 device with font size set to minimum; revert #97590 first if it reproduces, otherwise revert #97339.
No PR in the deploy checklist caused this bug. No PR from that checklist is in the build where the bug appeared.
The reported build predates the checklist
The report gives version 9.4.53-9. That build is commit e5e39887f7c, tag 9.4.53-9-staging, from 2026-08-14 17:19 UTC.
The checklist covers the range from production 9.4.53-10 to staging 9.4.54-1. All 87 PRs in the checklist are in that later range. None of them are in build 9.4.53-9.
Neither named suspect is in the build
Remove onboarding progress bar adds the file src/components/OnboardingHeader.tsx. That file does not exist at tag 9.4.53-9-staging. At that tag, BaseOnboardingPurpose.tsx still imports HeaderWithBackButton and still passes progressBarPercentage.
The attached video confirms this. The onboarding progress bar is visible in the recording. That PR removes this bar.
MenuItem decomposition renames src/components/MenuItem.tsx to src/components/MenuItem/MenuItem.tsx. At tag 9.4.53-9-staging, the file is still at the old path. It is also byte-identical to the production version.
The real cause
Production at the time of the test was 9.4.52-11. Between 9.4.52-11 and 9.4.53-9, every file on the title render path is byte-identical:
the onboarding.purpose.* strings in src/languages/en.ts
One relevant change remains. package.json moved from "react-native": "0.85.3" to "react-native": "0.86.0". This upgrade is upgrade react native to 0.86.0.
React Native 0.86 enables enableFontScaleChangesUpdatingLayout. This flag controls how Fabric measures text nodes again after a font scale change. We already found this class of bug once. Patch RN 0.86 to fix Fabric reusing nodes with a stale font scale adds patches/react-native/react-native+0.86.0+037+fix-stale-font-scale.patch. That patch is in build 9.4.53-9, so it does not cover this case.
The symptom is a stale measurement, not a style clamp
Frames from the attached video show two states. At 5 seconds, on first render, the full string fits on one line. At 12 seconds, after back navigation, the row height is the same. The cut falls on a word boundary and no ellipsis appears. About 180 px of space remains free to the right.
A numberOfLines clamp adds an ellipsis and cuts at the container edge. On Android, styles.pre and styles.preWrap are both {}. The screen passes numberOfLinesTitle: 0, which becomes numberOfLines={undefined}. No JavaScript style can clamp this title.
The conditions to reproduce the bug match the React Native 0.86 font scale path:
The font scale is not the default value.
The text is correct on first mount and wrong after back navigation. Fabric reuses nodes at that point.
The bug is Android only.
Recommendation
This bug is not in the 9.4.54 deploy. It reached production in 9.4.53-10 on 2026-08-14 at 19:14 UTC. Remove the DeployBlockerCash label and swap Hourly for Daily.
A device bisect of 9.4.52-11 against 9.4.53-0 confirms the React Native 0.86 cause.
cc @roryabraham (author of the existing font scale patch) and @jakubstec (the 0.86 upgrade).
The onboarding purpose screen's "Submit expenses to my employer" option truncates to "Submit expenses to my" after back navigation on Android devices with minimum font size set.
Root cause: numberOfLinesTitle: 0 in BaseOnboardingPurpose.tsx:112 resolves to numberOfLines={undefined} in MenuItem.tsx:1068, which should allow unlimited wrapping. However, on Android with non-default font scaling, the Text component's layout measurement becomes inaccurate after remount.
When numberOfLinesTitle: 0 is passed, the MenuItem renders:
The combinedTitleTextStyle applies styles.preWrap (since numberOfLinesTitle !== 1), but on native platforms, preWrap resolves to an empty object in src/styles/utils/whiteSpace/index.native.ts, so it has no actual effect.
The real problem: when Android's minimum font size accessibility setting is active, React Native's text layout algorithm measures the wrapped text incorrectly after a screen remount (triggered by back navigation). The first render calculates correctly, but the remount causes the trailing word to wrap onto a second line that the container then clips, because the container height was calculated for single-line text.
This is confirmed by the bug report's video evidence: the text is cut at a word boundary ("my" vs "employer"), with no ellipsis and around 180px of free space remaining to the right, indicating a wrapped second line being clipped, not a numberOfLines truncation.
What changes do you think we should make in order to solve the problem?
Change numberOfLinesTitle: 0 to numberOfLinesTitle: 2 in BaseOnboardingPurpose.tsx:112:
Why this fixes it: numberOfLines={2} gives React Native an explicit line count to measure against, preventing the measurement ambiguity that causes the wrapped text to be clipped on Android. The longest purpose string is "Submit expenses to my employer" at 32 characters, which fits comfortably in 2 lines even at minimum font size, while preventing the wrapped-line-clip bug.
Verified pattern: src/pages/workspace/accounting/intacct/DynamicSageIntacctPrerequisitesPage.tsx uses numberOfLinesTitle: 2 for similar menu items with wrapping text.
What alternative solutions did you explore? (Optional)
Setting numberOfLinesTitle: 1 would prevent wrapping entirely, but this would truncate longer purpose strings with an ellipsis on devices with large font sizes, creating the opposite accessibility problem.
Using undefined (by removing the prop entirely) is equivalent to numberOfLinesTitle: 0 and has the same bug.
A React Native core fix for the font-scale measurement issue would be ideal long-term, but numberOfLinesTitle: 2 is a safe immediate fix that works within the current RN behavior.
changed the title [-]Android - Onboarding - Missing "employer" in "Submit expenses to my employer"[/-][+][Due for payment 2026-09-02] Android - Onboarding - Missing "employer" in "Submit expenses to my employer"[/+]on Aug 26, 2026
The solution for this issue has been 🚀 deployed to production 🚀 in version 9.4.61-4 and is now subject to a 7-day regression period 📆. Here is the list of pull requests that resolve this issue:
If no regressions arise, payment will be issued on 2026-09-02. 🎊
The following checklist (instructions) will need to be completed before the issue can be closed. Please copy/paste the Contributor+ Checklist from here into a new comment on this GH and complete it. If you have the K2 extension, you can simply click: [this button]. If no checklist is needed for this issue, you can click: [no checklist button]
Please re-state the problem that we are trying to solve in this issue.
On Android 16 with minimum system font size, the onboarding purpose option "Submit expenses to my employer" renders correctly on first visit, but after tapping that option and pressing the in-app back button, the title is clipped to "Submit expenses to my". The missing word is cut on a word boundary with no ellipsis, the row height does not change, and free horizontal space remains in the row.
What is the root cause of that problem?
This is not a JavaScript copy, styling, or numberOfLines clamp bug. The onboarding screen already passes numberOfLinesTitle: 0, which is intended to allow the full title to wrap:
That prop flows into legacy MenuItem, which selects the wrapping style branch and passes numberOfLines={undefined} (no line clamp) to our shared Text component:
In RN 0.86, upstream facebook/react-native#56409 deleted the enableAndroidLinearText feature flag and unconditionally enabled Paint.LINEAR_TEXT_FLAG on Android for both Text (CustomStyleSpan) and TextInput (ReactEditText). In RN 0.85.3 (production), that flag defaulted to false, so production never set it.
LINEAR_TEXT_FLAG disables font hinting, which shifts glyph advance widths by a fraction of a pixel. On its own that is invisible, but Android TextLayoutManager rounds the measured text width up (ceil(Layout.getDesiredWidth(...))) and the available width down (floor(width)), and line breaking is a step function. A sub-pixel shift is enough to push a trailing word such as "employer" onto a second line inside a view whose height was already computed for one line. That second line is clipped, so the word disappears with no ellipsis and no change in row height — exactly what the attached video shows.
Why this reproduces only on Android native, only with non-default font scale, and often only on specific devices (Galaxy S25 FE, Galaxy A54): whether a given string lands on the wrong side of that boundary depends on fractional layout residues from screen width, display density, and font scale. The S25 FE overrides display density to 420 while its physical density is 450, so derived widths carry fractional values that make the ceil/floor mismatch more likely. Devices left at their physical density mostly do not reproduce it, which is why many retests came back clean.
Why it appears after back navigation: remounting the screen triggers a fresh native text layout pass where the sub-pixel rounding disagreement can move the trailing word onto the clipped second line. First mount can still measure as one line and look correct.
This is the same bug class as #98499. Patch 037 (fix-stale-font-scale, PR #98507) addresses a different C++/Yoga stale-fontSizeMultiplier mechanism; both #98499 and #98690 still reproduced on builds that already contained that patch (e.g. 9.4.53-3, 9.4.54-4).
What changes do you think we should make in order to solve the problem?
Add a new React Native patch, react-native+0.86.0+040+restore-non-linear-text.patch, that restores RN 0.85.3 Android text rendering by reverting the unconditional LINEAR_TEXT_FLAG rollout from upstream #56409. Both call sites must be reverted together so Text and TextInput rasterise glyphs identically.
1. CustomStyleSpan.kt (used by all Fabric Text nodes, including onboarding menu titles):
isLinearText = true
(remove line — do not set linear text)
2. ReactEditText.kt (used by TextInput, so inputs match Text):
paintFlags = if (enableSubpixelText) { paintFlags or Paint.LINEAR_TEXT_FLAG } else { paintFlags and Paint.LINEAR_TEXT_FLAG.inv() }
(remove block — do not set LINEAR_TEXT_FLAG)
Document the patch in patches/react-native/details.md as a deliberate deviation from upstream (not a backport), with a note to re-evaluate on the RN 0.87 upgrade.
Why this is the best fix
Root cause, not symptom: It removes the sub-pixel glyph-width shift that makes TextLayoutManager's asymmetric rounding clip a trailing word.
Covers all reproduction paths: Fixes every Android Fabric Text/TextInput string affected after remount — onboarding purpose, Expensify Card ("Your data is secure"), and any other screen hit by the same mechanism — not just this one menu item.
Matches existing conventions: RN 0.86 rendering regressions in Expensify are fixed via targeted patches/react-native/ files, same as patch 037.
Low blast radius: No onboarding JS, copy, or layout changes. Web and iOS are unaffected. Production behavior (0.85.3, no linear text) is restored on Android.
Verified fix path: PR fix: cut out text on android #99069 implements exactly this and reports the issue no longer reproduces under the QA preconditions (Android 16, non-default font scale, non-default display density, navigate forward then back).
Regression / edge cases checked mentally
Default font scale and physical display density: behavior matches 0.85.3 production, which never enabled linear text.
TextInput vs Text consistency: both call sites are reverted together, so mixed screens do not rasterise differently.
Patch 037 remains in place; it solves a separate stale-font-scale problem and is not removed by this change.
What alternative solutions did you explore? (Optional)
Extend patch 037 (fix-stale-font-scale): Rejected — that patch dirties Yoga nodes when fontSizeMultiplier changes. This bug still reproduced on 9.4.53-3 and 9.4.54-4 with patch 037 present, and the symptom (word clipped with no ellipsis, unchanged row height, free space to the right) matches native measure/draw rounding, not a stale multiplier.
Revert onboarding header / progress-bar PR Remove onboarding progress bar #97590: Ruled out — that PR is not in build 9.4.53-9 where QA first saw the bug (the recording still shows the old progress bar), and none of the JS files on the title render path changed between production and that staging build.
Set numberOfLinesTitle: 2 or other JS layout tweaks in BaseOnboardingPurpose: Rejected — the clip is not a JS numberOfLines ellipsis; native layout already computed a one-line height and is clipping an overflow line.
Force a React remount with a focus-based key on MenuItemList: Rejected — masks onboarding only, remounts on every refocus, and does not fix the underlying Android text-rasterisation change.
Key the app root on fontScale: Rejected — remounts the entire tree and is unnecessarily heavy for a native Paint-flag regression.
Activity
applause-bot commentedon Aug 14, 2026
While Applause doesn't embed attachments for this bug, here are the links for quick view:
https://platform.applause.com/services/links/v1/external/c806329eaa7ee9eacc8fe147bfe148b500aa36c695174acac99efc1f6085ddb0
melvin-bot commentedon Aug 14, 2026
Triggered auto assignment to @deetergp (
DeployBlockerCash), see https://stackoverflowteams.com/c/expensify/questions/9980/ for more details.melvin-bot commentedon Aug 14, 2026
💬 A slack conversation has been started in #expensify-open-source
github-actions commentedon Aug 14, 2026
👋 Friendly reminder that deploy blockers are time-sensitive ⏱ issues! Check out the open `StagingDeployCash` deploy checklist to see the list of PRs included in this release, then work quickly to do one of the following:
MelvinBot commentedon Aug 14, 2026
🔍 Investigation Summary
Classification: Frontend bug (App / React Native onboarding screen)
Causing PR: #97590 - "Remove onboarding progress bar" (Medium confidence) — secondary suspect #97339 - "[No QA] MenuItem decomposition - simplest usages" by @war-in (Medium confidence)
Related Issues: none
Recommendation: NEEDS INVESTIGATION (revert #97590 first if an on-device bisect confirms it)
The bug is a staging-only, Android-app-only text-truncation regression on the Onboarding "purpose" screen. Two PRs in this deploy touch that exact area, but a line-level review of both did not isolate a definitive change to the title's line-wrapping, so an on-device bisect (Android 16, device font size = minimum) is needed before reverting.
Assigned: @grgia, @marcochavezf, @suneox (approving reviewers of the highest-confidence suspect #97590; the PR author is MelvinBot). cc @war-in for secondary suspect #97339.
Labels: No changes. Kept
DeployBlockerCash— this is a frontend/App regression, it is not reproducible in production, and it is the only blocker label on the issue. NoDeployBlocker(Web) label is present.📋 Detailed Analysis
Affected code path
The purpose options ("Submit expenses to my employer", etc.) are rendered by
BaseOnboardingPurpose.tsxviaMenuItemList, which passesnumberOfLinesTitle: 0per item.MenuItemListforwards that prop to the legacyMenuItemcomponent (src/components/MenuItem/MenuItem.tsx), not the new compound/preset API. In the legacy component,numberOfLinesTitle: 0resolves to:numberOfLinesTitle !== 1 ? styles.preWrap : styles.pre→preWrap(wrapping allowed), andnumberOfLines={numberOfLinesTitle || undefined}→undefined(unlimited lines).So the intended behavior is that a long title wraps to a second line ("Submit expenses to my / employer"). The reported symptom ("Submit expenses to my" with "employer" cut off) is a single-line clamp — the title is losing its ability to wrap after the screen is remounted on back-navigation.
Evidence — why these two PRs
Only two PRs in this StagingDeployCash checklist (#98677) touch this area:
#97590 "Remove onboarding progress bar" (primary suspect, Medium):
src/pages/OnboardingPurpose/BaseOnboardingPurpose.tsx, swappingHeaderWithBackButton(with step counter / progress bar) for a newOnboardingHeader, and restructures the onboarding header layout across every onboarding screen.onboardingHeaderContainer:height: 52,marginTop: 8) and removes the progress-bar styles insrc/styles/index.ts.#97339 "MenuItem decomposition" (secondary suspect, Medium):
MenuItemcomponent that renders the truncating text. It introduces a newsrc/components/MenuItem/leaves/text/MenuItemTitle.tsxleaf that hardcodesnumberOfLines={1}+styles.preand ignoresnumberOfLinesTitle— behavior that would produce exactly this truncation.MenuItempath), and the legacy title-wrapping logic is unchanged by this PR (its onlyMenuItem.tsxedits are import reordering, extracting auseRemoveNonInteractiveClickHandlerhook, and anopacityAnimationDurationvalue). If this PR were the cause, we would expect widespread menu-text truncation, not a single onboarding option — and no other such blocker exists. This asymmetry is why Remove onboarding progress bar #97590 (onboarding-scoped) ranks above [No QA] MenuItem decomposition - simplest usages #97339.Verification
src/pages/OnboardingPurpose/BaseOnboardingPurpose.tsx(renders the options) →src/components/MenuItemList.tsx→src/components/MenuItem/MenuItem.tsx(legacy title rendering).BaseOnboardingPurpose.tsx,OnboardingHeader.tsx, andsrc/styles/index.ts.MenuItem.tsxand the newMenuItem/leaves/text/MenuItemTitle.tsx, but does not change the legacynumberOfLinesTitlehandling used by onboarding.Root cause
Not isolated to a single line with high confidence. The symptom (a title that should wrap losing its wrap and clamping to one line after remount, at minimum device font size on Android 16) points at a layout/measurement regression on the onboarding purpose screen rather than a static prop change. The header/layout restructure in #97590 is the most likely trigger. Recommended next step: bisect #97590 vs #97339 on a real Android 16 device with font size set to minimum; revert #97590 first if it reproduces, otherwise revert #97339.
deetergp commentedon Aug 14, 2026
Root cause analysis
No PR in the deploy checklist caused this bug. No PR from that checklist is in the build where the bug appeared.
The reported build predates the checklist
The report gives version
9.4.53-9. That build is commite5e39887f7c, tag9.4.53-9-staging, from 2026-08-14 17:19 UTC.The checklist covers the range from production
9.4.53-10to staging9.4.54-1. All 87 PRs in the checklist are in that later range. None of them are in build9.4.53-9.Neither named suspect is in the build
Remove onboarding progress bar adds the file
src/components/OnboardingHeader.tsx. That file does not exist at tag9.4.53-9-staging. At that tag,BaseOnboardingPurpose.tsxstill importsHeaderWithBackButtonand still passesprogressBarPercentage.The attached video confirms this. The onboarding progress bar is visible in the recording. That PR removes this bar.
MenuItem decomposition renames
src/components/MenuItem.tsxtosrc/components/MenuItem/MenuItem.tsx. At tag9.4.53-9-staging, the file is still at the old path. It is also byte-identical to the production version.The real cause
Production at the time of the test was
9.4.52-11. Between9.4.52-11and9.4.53-9, every file on the title render path is byte-identical:src/components/MenuItem.tsxsrc/components/MenuItemList.tsxsrc/pages/OnboardingPurpose/BaseOnboardingPurpose.tsxsrc/styles/utils/index.tssrc/components/Text.tsxonboarding.purpose.*strings insrc/languages/en.tsOne relevant change remains.
package.jsonmoved from"react-native": "0.85.3"to"react-native": "0.86.0". This upgrade is upgrade react native to 0.86.0.React Native 0.86 enables
enableFontScaleChangesUpdatingLayout. This flag controls how Fabric measures text nodes again after a font scale change. We already found this class of bug once. Patch RN 0.86 to fix Fabric reusing nodes with a stale font scale addspatches/react-native/react-native+0.86.0+037+fix-stale-font-scale.patch. That patch is in build9.4.53-9, so it does not cover this case.The symptom is a stale measurement, not a style clamp
Frames from the attached video show two states. At 5 seconds, on first render, the full string fits on one line. At 12 seconds, after back navigation, the row height is the same. The cut falls on a word boundary and no ellipsis appears. About 180 px of space remains free to the right.
A
numberOfLinesclamp adds an ellipsis and cuts at the container edge. On Android,styles.preandstyles.preWrapare both{}. The screen passesnumberOfLinesTitle: 0, which becomesnumberOfLines={undefined}. No JavaScript style can clamp this title.The conditions to reproduce the bug match the React Native 0.86 font scale path:
Recommendation
This bug is not in the 9.4.54 deploy. It reached production in
9.4.53-10on 2026-08-14 at 19:14 UTC. Remove theDeployBlockerCashlabel and swapHourlyforDaily.A device bisect of
9.4.52-11against9.4.53-0confirms the React Native 0.86 cause.cc @roryabraham (author of the existing font scale patch) and @jakubstec (the 0.86 upgrade).
24 remaining items
jakubstec commentedon Aug 24, 2026
we've found a proper fix for that #99069, there is just one question regarding the implementation, so I think there is no need for external help.
amar-sheikh commentedon Aug 25, 2026
Proposal
What is the root cause of that problem?
The onboarding purpose screen's "Submit expenses to my employer" option truncates to "Submit expenses to my" after back navigation on Android devices with minimum font size set.
Root cause:
numberOfLinesTitle: 0inBaseOnboardingPurpose.tsx:112resolves tonumberOfLines={undefined}inMenuItem.tsx:1068, which should allow unlimited wrapping. However, on Android with non-default font scaling, the Text component's layout measurement becomes inaccurate after remount.When
numberOfLinesTitle: 0is passed, the MenuItem renders:The
combinedTitleTextStyleappliesstyles.preWrap(sincenumberOfLinesTitle !== 1), but on native platforms,preWrapresolves to an empty object insrc/styles/utils/whiteSpace/index.native.ts, so it has no actual effect.The real problem: when Android's minimum font size accessibility setting is active, React Native's text layout algorithm measures the wrapped text incorrectly after a screen remount (triggered by back navigation). The first render calculates correctly, but the remount causes the trailing word to wrap onto a second line that the container then clips, because the container height was calculated for single-line text.
This is confirmed by the bug report's video evidence: the text is cut at a word boundary ("my" vs "employer"), with no ellipsis and around 180px of free space remaining to the right, indicating a wrapped second line being clipped, not a
numberOfLinestruncation.What changes do you think we should make in order to solve the problem?
Change
numberOfLinesTitle: 0tonumberOfLinesTitle: 2inBaseOnboardingPurpose.tsx:112:Why this fixes it:
numberOfLines={2}gives React Native an explicit line count to measure against, preventing the measurement ambiguity that causes the wrapped text to be clipped on Android. The longest purpose string is "Submit expenses to my employer" at 32 characters, which fits comfortably in 2 lines even at minimum font size, while preventing the wrapped-line-clip bug.Verified pattern:
src/pages/workspace/accounting/intacct/DynamicSageIntacctPrerequisitesPage.tsxusesnumberOfLinesTitle: 2for similar menu items with wrapping text.What alternative solutions did you explore? (Optional)
Setting
numberOfLinesTitle: 1would prevent wrapping entirely, but this would truncate longer purpose strings with an ellipsis on devices with large font sizes, creating the opposite accessibility problem.Using
undefined(by removing the prop entirely) is equivalent tonumberOfLinesTitle: 0and has the same bug.A React Native core fix for the font-scale measurement issue would be ideal long-term, but
numberOfLinesTitle: 2is a safe immediate fix that works within the current RN behavior.m-natarajan commentedon Aug 25, 2026
Not repro, Samsung Galaxy S25 FE / Android 16 (Ad-hoc build)
Screen_Recording_20260825_122700_Expensify.Adhoc.mp4
jakubstec commentedon Aug 25, 2026
#99069 - PR with a fix has been merged!
[-]Android - Onboarding - Missing "employer" in "Submit expenses to my employer"[/-][+][Due for payment 2026-09-02] Android - Onboarding - Missing "employer" in "Submit expenses to my employer"[/+]melvin-bot commentedon Aug 26, 2026
@suneox
The solution for this issue has been 🚀 deployed to production 🚀 in version 9.4.61-4 and is now subject to a 7-day regression period 📆. Here is the list of pull requests that resolve this issue:
If no regressions arise, payment will be issued on 2026-09-02. 🎊
The following checklist (instructions) will need to be completed before the issue can be closed. Please copy/paste the Contributor+ Checklist from here into a new comment on this GH and complete it. If you have the K2 extension, you can simply click: [this button]. If no checklist is needed for this issue, you can click: [no checklist button]
mvtglobally commentedon Aug 28, 2026
Issue not reproducible during KI retests. (First week)
melvin-bot commentedon Aug 28, 2026
@suneox Whoops! This issue is 2 days overdue. Let's get this updated quick!
jakubstec commentedon Aug 31, 2026
I think we can close it then 😄
deetergp commentedon Aug 31, 2026
Agreed @jakubstec! Closing.
neerajbachani commentedon Oct 6, 2026
Proposal
Please re-state the problem that we are trying to solve in this issue.
On Android 16 with minimum system font size, the onboarding purpose option "Submit expenses to my employer" renders correctly on first visit, but after tapping that option and pressing the in-app back button, the title is clipped to "Submit expenses to my". The missing word is cut on a word boundary with no ellipsis, the row height does not change, and free horizontal space remains in the row.
What is the root cause of that problem?
This is not a JavaScript copy, styling, or
numberOfLinesclamp bug. The onboarding screen already passesnumberOfLinesTitle: 0, which is intended to allow the full title to wrap:App/src/pages/OnboardingPurpose/BaseOnboardingPurpose.tsx
Lines 95 to 107 in 6a84277
const menuItems: MenuItemProps[] = onboardingChoices.map((choice) => {
const translationKey =
onboarding.purpose.${choice}as const;return {
key: translationKey,
title: translate(translationKey),
icon: menuIcons[choice],
displayInDefaultIconColor: true,
iconWidth: variables.menuIconSize,
iconHeight: variables.menuIconSize,
iconStyles: [styles.mh3],
wrapperStyle: [styles.purposeMenuItem],
numberOfLinesTitle: 0,
App/src/pages/OnboardingPurpose/BaseOnboardingPurpose.tsx
Lines 95 to 107 in 6a84277
That prop flows into legacy
MenuItem, which selects the wrapping style branch and passesnumberOfLines={undefined}(no line clamp) to our sharedTextcomponent:App/src/components/MenuItem/MenuItem.tsx
Lines 677 to 684 in 6a84277
const combinedTitleTextStyle = StyleUtils.combineStyles(
[
styles.flexShrink1,
styles.popoverMenuText,
iconLeftPadding,
shouldShowBasicTitle ? {} : styles.textStrong,
numberOfLinesTitle !== 1 ? styles.preWrap : styles.pre,
https://github.com/Expensify/App/blob/6a84277ef2dc4e9f748e7e59ac607972ed2637ee/src/components/MenuItem/MenuItem.tsx#L677-L684
App/src/components/MenuItem/MenuItem.tsx
Lines 1048 to 1056 in 6a84277
{!shouldRenderAsHTML && !shouldParseTitle && !!title && (
<Text
style={combinedTitleTextStyle}
numberOfLines={numberOfLinesTitle || undefined}
dataSet={{[CONST.SELECTION_SCRAPER_HIDDEN_ELEMENT]: interactive && disabled}}
accessibilityRole={titleAccessibilityRole}
>
{renderTitleContent()}
)}
App/src/components/MenuItem/MenuItem.tsx
Lines 1048 to 1056 in 6a84277
On native Android,
styles.preWrap/styles.preare empty objects, so no JSwhiteSpacerule can explain the clip:App/src/styles/utils/whiteSpace/index.native.ts
Lines 3 to 7 in 6a84277
const whiteSpace: WhiteSpaceStyles = {
noWrap: {},
preWrap: {},
pre: {},
};
App/src/styles/utils/whiteSpace/index.native.ts
Lines 3 to 7 in 6a84277
The regression was introduced by upgrading to React Native 0.86.0 (PR #96372):
App/package.json
Lines 181 to 181 in 6a84277
"react-native": "0.86.0",
App/package.json
Line 181 in 6a84277
In RN 0.86, upstream facebook/react-native#56409 deleted the
enableAndroidLinearTextfeature flag and unconditionally enabledPaint.LINEAR_TEXT_FLAGon Android for bothText(CustomStyleSpan) andTextInput(ReactEditText). In RN 0.85.3 (production), that flag defaulted tofalse, so production never set it.LINEAR_TEXT_FLAGdisables font hinting, which shifts glyph advance widths by a fraction of a pixel. On its own that is invisible, but AndroidTextLayoutManagerrounds the measured text width up (ceil(Layout.getDesiredWidth(...))) and the available width down (floor(width)), and line breaking is a step function. A sub-pixel shift is enough to push a trailing word such as "employer" onto a second line inside a view whose height was already computed for one line. That second line is clipped, so the word disappears with no ellipsis and no change in row height — exactly what the attached video shows.Why this reproduces only on Android native, only with non-default font scale, and often only on specific devices (Galaxy S25 FE, Galaxy A54): whether a given string lands on the wrong side of that boundary depends on fractional layout residues from screen width, display density, and font scale. The S25 FE overrides display density to 420 while its physical density is 450, so derived widths carry fractional values that make the
ceil/floormismatch more likely. Devices left at their physical density mostly do not reproduce it, which is why many retests came back clean.Why it appears after back navigation: remounting the screen triggers a fresh native text layout pass where the sub-pixel rounding disagreement can move the trailing word onto the clipped second line. First mount can still measure as one line and look correct.
This is the same bug class as #98499. Patch
037(fix-stale-font-scale, PR #98507) addresses a different C++/Yoga stale-fontSizeMultipliermechanism; both #98499 and #98690 still reproduced on builds that already contained that patch (e.g. 9.4.53-3, 9.4.54-4).What changes do you think we should make in order to solve the problem?
Add a new React Native patch,
react-native+0.86.0+040+restore-non-linear-text.patch, that restores RN 0.85.3 Android text rendering by reverting the unconditionalLINEAR_TEXT_FLAGrollout from upstream #56409. Both call sites must be reverted together soTextandTextInputrasterise glyphs identically.1.
CustomStyleSpan.kt(used by all FabricTextnodes, including onboarding menu titles):isLinearText = true2.
ReactEditText.kt(used byTextInput, so inputs matchText):paintFlags = if (enableSubpixelText) { paintFlags or Paint.LINEAR_TEXT_FLAG } else { paintFlags and Paint.LINEAR_TEXT_FLAG.inv() }LINEAR_TEXT_FLAG)Document the patch in
patches/react-native/details.mdas a deliberate deviation from upstream (not a backport), with a note to re-evaluate on the RN 0.87 upgrade.Why this is the best fix
TextLayoutManager's asymmetric rounding clip a trailing word.Text/TextInputstring affected after remount — onboarding purpose, Expensify Card ("Your data is secure"), and any other screen hit by the same mechanism — not just this one menu item.patches/react-native/files, same as patch037.Regression / edge cases checked mentally
037remains in place; it solves a separate stale-font-scale problem and is not removed by this change.What alternative solutions did you explore? (Optional)
037(fix-stale-font-scale): Rejected — that patch dirties Yoga nodes whenfontSizeMultiplierchanges. This bug still reproduced on 9.4.53-3 and 9.4.54-4 with patch037present, and the symptom (word clipped with no ellipsis, unchanged row height, free space to the right) matches native measure/draw rounding, not a stale multiplier.9.4.53-9where QA first saw the bug (the recording still shows the old progress bar), and none of the JS files on the title render path changed between production and that staging build.numberOfLinesTitle: 2or other JS layout tweaks inBaseOnboardingPurpose: Rejected — the clip is not a JSnumberOfLinesellipsis; native layout already computed a one-line height and is clipping an overflow line.keyonMenuItemList: Rejected — masks onboarding only, remounts on every refocus, and does not fix the underlying Android text-rasterisation change.fontScale: Rejected — remounts the entire tree and is unnecessarily heavy for a native Paint-flag regression.