Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
76 commits
Select commit Hold shift + click to select a range
d7c11c8
feat: liquid glass tab bar owned by the tab navigator
sumo-slonik Sep 15, 2026
e4b2f69
Merge branch 'main' into feat/liquid-glass-tab-bar
sumo-slonik Sep 15, 2026
2c15f8a
feat: native bottom tabs on iOS and Android, floating capsule on mWeb
sumo-slonik Sep 15, 2026
39d637e
fix: floating tab bar inset on the two remaining tab roots
sumo-slonik Sep 15, 2026
08e28cd
chore: bump react-navigation for the native tabs contract
sumo-slonik Sep 15, 2026
f330dda
fix: reveal the tab bar when the pop starts, not when it commits
sumo-slonik Sep 15, 2026
6e868e3
fix: tint native tab bar icons and mount tab screens eagerly
sumo-slonik Sep 16, 2026
8760e96
fix: keep each native tab's badge color with that tab on iOS 26
sumo-slonik Sep 16, 2026
5eab7d4
revert: drop the iOS 26 tab bar color patch, measurements show it has…
sumo-slonik Sep 16, 2026
4c2aa31
fix: draw tab status dots into the icons and fade the floating button…
sumo-slonik Sep 16, 2026
0c8fac9
fix: let UIKit animate hiding the native tab bar instead of blinking it
sumo-slonik Sep 16, 2026
9d243e9
fix: match the floating button fade to the system tab bar animation
sumo-slonik Sep 16, 2026
fc40c0f
Merge branch 'main' into feat/liquid-glass-tab-bar
sumo-slonik Sep 22, 2026
022c63a
fix: drop the beta gate the merge left in the inner insights component
sumo-slonik Sep 22, 2026
b3dd3d5
fix: draw the Android tab bar like the iOS one
sumo-slonik Sep 22, 2026
e09d3fd
fix: restore tab content after an opaque push
sumo-slonik Sep 22, 2026
67db700
fix: show skeleton rows while Search loads
sumo-slonik Sep 22, 2026
a3b1482
feat: floating tab bar on Android, painted labels on iOS
sumo-slonik Sep 23, 2026
8ce0b8f
Merge branch 'main' into feat/liquid-glass-tab-bar
sumo-slonik Sep 23, 2026
713268a
fix: remove dead tab bar footer slot, android patches, style duplicates
sumo-slonik Sep 23, 2026
d2f8b24
Merge remote-tracking branch 'upstream/main' into feat/liquid-glass-t…
sumo-slonik Sep 23, 2026
0415ee9
fix: fold tab bar surface style into the container, correct stale com…
sumo-slonik Sep 23, 2026
877e3e7
feat: scale down floating tab bar icons and blur what the bar covers
sumo-slonik Sep 23, 2026
69a666e
feat: blur the focused tab scene under the Android tab bar
sumo-slonik Sep 24, 2026
7adbdd0
feat: native Material tab bar on Android, flat floating bar on mobile…
sumo-slonik Sep 30, 2026
fcb90fe
Merge upstream/main into feat/liquid-glass-tab-bar
sumo-slonik Sep 30, 2026
4f891cc
refactor: share native tab navigator code, lay out the mWeb bar via b…
sumo-slonik Sep 30, 2026
aab4316
fix: start tab navigation spans from native tab bar presses
sumo-slonik Sep 30, 2026
2782a22
fix: larger iOS native tab icons, softer Android tab pill
sumo-slonik Sep 30, 2026
44c3fb4
feat: Insights tab on the native bar with the beta, Account avatar on…
sumo-slonik Sep 30, 2026
c81bbef
clearing up pr
sumo-slonik Sep 30, 2026
8e215cb
clearing up pr
sumo-slonik Sep 30, 2026
8d5dfff
fix: restore Search skeleton rows dropped in the PR cleanup
sumo-slonik Sep 30, 2026
355459f
Merge remote-tracking branch 'upstream/main' into feat/liquid-glass-t…
sumo-slonik Oct 1, 2026
1ac76e6
fix: tint Android tab icons in release builds, even pill inset on mWeb
sumo-slonik Oct 1, 2026
4f32e80
fix: keep the avatar on the selected Android Account tab
sumo-slonik Oct 1, 2026
c2c6539
fix: run iOS tab root content under the glass tab bar, inset list end…
sumo-slonik Oct 1, 2026
0becb2a
fix: keep Android tab root content above the Material tab bar
sumo-slonik Oct 1, 2026
f4b79ce
feat: Android tab bar on appBG with a soft top shadow instead of a to…
sumo-slonik Oct 2, 2026
75f34ea
feat: float the mWeb tab bar over tab root content, inset list ends p…
sumo-slonik Oct 2, 2026
db655b4
fix: draw iOS tab glyphs as Skia vector paths so resized icons stay s…
sumo-slonik Oct 5, 2026
124600e
fix: bold the selected Android tab label and tighten label letter spa…
sumo-slonik Oct 5, 2026
92918cf
perf: freeze background native tabs, which have no freezeOnBlur
sumo-slonik Oct 5, 2026
a30432a
perf: give tab bars the Inbox status alone, so LHN updates do not re-…
sumo-slonik Oct 5, 2026
46fe6f7
perf: draw native tab icons synchronously from vector glyphs on both …
sumo-slonik Oct 5, 2026
d8ab0f4
refactor: one tab root scroll props hook, platform tab bar insets wit…
sumo-slonik Oct 5, 2026
5f852c0
refactor: one rule for the tab the native bar has no item for
sumo-slonik Oct 5, 2026
c46d048
chore: merge the two Android tab label patches for react-native-scree…
sumo-slonik Oct 5, 2026
901bff3
refactor: fade the Android tab bar shadow and the floating buttons in…
sumo-slonik Oct 5, 2026
29c2652
revert: move the Search skeleton rows change to its own branch, fix/s…
sumo-slonik Oct 5, 2026
4d63259
chore: relative import for the tab without a bar item rule
sumo-slonik Oct 5, 2026
4a3e8d1
perf: no icon draws for the tab without a bar item, gesture effect re…
sumo-slonik Oct 5, 2026
35304d7
refactor: one tab root scroll props call in the Insights dashboard
sumo-slonik Oct 5, 2026
743cc0e
chore: drop the unused src hunk from the bottom-tabs indicator color …
sumo-slonik Oct 5, 2026
4ba41ea
docs: note when the tab bar hide and indicator color patches can go
sumo-slonik Oct 5, 2026
4d060b2
fix: name the iOS native tabs for VoiceOver, note in patch docs which…
sumo-slonik Oct 5, 2026
43a71c0
revert: stop freezing background native tabs, the native bar shows a …
sumo-slonik Oct 5, 2026
aa861e2
fix: show Search skeleton rows on the first visit to a background mou…
sumo-slonik Oct 5, 2026
f873587
fix: derive the natively selected tab from the history, so a beta cha…
sumo-slonik Oct 5, 2026
2a9acc3
fix: replace instead of push when leaving Insights without the beta o…
sumo-slonik Oct 5, 2026
3855aca
fix: native tab taps run the JS tab button rules, Inbox opens at its …
sumo-slonik Oct 5, 2026
433cc00
fix: still background Spend skeleton, no tab bar animation before it …
sumo-slonik Oct 5, 2026
0eb4f28
chore: shared tab root scroll props type, constraint-only comments, p…
sumo-slonik Oct 5, 2026
ac4fd10
fix: Android selected tab no longer sits higher than the others, tab …
sumo-slonik Oct 6, 2026
9aefaea
fix: navigating to the split sidebar already on top is a handled no-o…
sumo-slonik Oct 6, 2026
cd5a62b
fix: Android tab dots drawn into the icon keep their own color, nativ…
sumo-slonik Oct 6, 2026
0f50568
Merge remote-tracking branch 'upstream/main' into feat/liquid-glass-t…
sumo-slonik Oct 6, 2026
71ffd6e
chore: no double safe area under the mWeb Inbox list, one owner for t…
sumo-slonik Oct 6, 2026
9b01141
fix: native Workspaces and first Spend taps follow the JS tab buttons…
sumo-slonik Oct 6, 2026
61a832a
fix: mock native tab icons under the Skia jest mock, platform hook ty…
sumo-slonik Oct 6, 2026
412eefe
refactor: one listener for the native Spend and Workspaces taps, plat…
sumo-slonik Oct 6, 2026
4c9148e
fix: native bar switches to Inbox, Spend and Workspaces itself when t…
sumo-slonik Oct 6, 2026
434ec5a
fix: iOS tab labels fall back for non-Latin scripts and truncate to t…
sumo-slonik Oct 7, 2026
1b99159
refactor: review fixes, narrow overlay split out, tab options from sc…
sumo-slonik Oct 7, 2026
b627741
chore: bump bottom-tabs to 7.18.16 and native to 7.3.16, the newest w…
sumo-slonik Oct 7, 2026
c1aabf4
fix: iOS offline indicator sits above the native tab bar on every tab…
sumo-slonik Oct 7, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions jest/setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -355,6 +355,10 @@ jest.mock('@shopify/react-native-skia', () => ({
listFontFamilies: jest.fn(() => []),
}));

// The native tab bar icons are drawn with Skia's imperative API, which the mock above leaves out, so the tabs get no icons.
jest.mock('@libs/Navigation/AppNavigator/Navigators/NativeTabNavigator/tabIconRasterizer', () => jest.fn(() => undefined));
jest.mock('@libs/Navigation/AppNavigator/Navigators/NativeTabNavigator/useTabAvatarImage', () => jest.fn(() => undefined));

jest.mock('@sbaiahmed1/react-native-biometrics', () => ({
isSensorAvailable: jest.fn(() => Promise.resolve({available: false})),
createKeys: jest.fn(() => Promise.resolve({publicKey: ''})),
Expand Down
40 changes: 25 additions & 15 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -121,10 +121,10 @@
"@react-native-firebase/app": "^22.2.1",
"@react-native-google-signin/google-signin": "^10.0.1",
"@react-native-picker/picker": "2.11.3",
"@react-navigation/bottom-tabs": "7.15.5",
"@react-navigation/bottom-tabs": "7.18.16",
"@react-navigation/core": "7.21.12",
"@react-navigation/material-top-tabs": "7.4.19",
"@react-navigation/native": "7.1.33",
"@react-navigation/native": "7.3.16",
"@react-navigation/native-stack": "7.14.5",
"@react-navigation/stack": "7.8.5",
"@react-ng/bounds-observer": "^0.2.1",
Expand Down Expand Up @@ -418,6 +418,7 @@
"node-abi": "^3.78.0",
"pdfjs-dist": "^5.5.207",
"lightningcss": "1.30.1",
"@react-navigation/elements": "2.9.38",
"@fullstory/react-native": {
"expo": "57.0.8"
},
Expand Down
24 changes: 24 additions & 0 deletions patches/react-native-screens/details.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# `react-native-screens` patches

### [react-native-screens+4.28.0+001+animate-hiding-the-native-tab-bar.patch](react-native-screens+4.28.0+001+animate-hiding-the-native-tab-bar.patch)

- Reason: The App hides the native tab bar on screens that are not a tab root, which React Navigation forwards to RNScreens as `tabBarHidden` (it derives the flag from `tabBarStyle.display === 'none'`). RNScreens calls UIKit's `setTabBarHidden:animated:` with `animated:NO`, so the bar blinks in and out instead of travelling with the screen that hid it. The patch animates the change once the bar is in a window, which lets UIKit run its own show/hide animation without animating the bar out on the first render. The pre-iOS 18 branch is left alone: it assigns `tabBar.hidden` directly and has no animated counterpart. **This patch can be removed on the bump to react-native-screens 4.29.0**, the first release with the upstream fix linked below.
- Upstream PR/issue: fixed upstream by [software-mansion/react-native-screens#4632](https://github.com/software-mansion/react-native-screens/pull/4632), which animates by default behind a new `ios.tabBarHiddenAnimationEnabled` prop, and [#4673](https://github.com/software-mansion/react-native-screens/pull/4673), which skips the animation on the first render. Both are merged after the 4.28.0 release and labeled for the v4 backport.
- E/App issue: [#101169](https://github.com/Expensify/App/issues/101169)
- PR introducing patch: [#101339](https://github.com/Expensify/App/pull/101339)

### [react-native-screens+4.28.0+002+android-tab-label-typography.patch](react-native-screens+4.28.0+002+android-tab-label-typography.patch)

- Reason: Two changes to the Android tab labels, both in `TabsAppearanceApplicator`, which React Navigation has no option for.
- Bold selected label. The Android tab bar takes one font weight for every label, while the design marks the selected tab with a bold label, the same way the JS side bar does. Material's `BottomNavigationView` draws each item with two labels, a small one shown while unselected and a large one shown while selected. `updateFontStyles` sets the large label's typeface with `Typeface.BOLD`, the way Material applies its own bold, so Android picks the bold face of the same font family. Material's `setItemTextAppearanceActiveBoldEnabled` is not used for this: it re-applies Material's 12sp text size to the large label alone and recomputes the selected item's offset from the difference between the two labels' sizes, so every selected tab would sit higher than the others.
- Letter spacing. `updateFontStyles` replaces the typeface and size of Material's tab labels but keeps the rest of Material 3's `LabelMedium` text appearance, which tracks letters 0.5sp apart. With Expensify Neue the labels read too spread out next to the rest of the app, so the patch sets the letter spacing of both labels to 0.
- Upstream PR/issue: not reported, because both changes are the App's typography (a bold selected label and its letter spacing) rather than a library defect, and RNScreens has no option for either.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we could add such options in an upstream PR? If not we'll have to support this patch forever

- E/App issue: [#101169](https://github.com/Expensify/App/issues/101169)
- PR introducing patch: [#101339](https://github.com/Expensify/App/pull/101339)

### [react-native-screens+4.28.0+003+no-android-tab-icon-tint.patch](react-native-screens+4.28.0+003+no-android-tab-icon-tint.patch)

- Reason: The Android account tab shows the user's avatar. `TabsAppearanceApplicator` assigns `bottomNavigationView.itemIconTintList` unconditionally, and a `ColorStateList` tint is `SRC_IN`, so it flattens the avatar to a solid silhouette in the tint color. React Navigation's `tinted: false` only reaches iOS, since `getPlatformIcon` in `@react-navigation/bottom-tabs` always hands Android a plain `imageSource`. The patch drops the icon tint list so Android draws every icon bitmap as supplied. The App recolors the glyphs off-screen in Skia for both selection states, so they keep the design's colors. Label colors are untouched and keep coming from `tabBarItemTitleFontColor`.
- Upstream PR/issue: not reported, because dropping the tint for every icon fits only a bar whose icons all arrive pre-colored, as the App's do. The disabled and focused icon colors go with it, which the App does not use.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here, can't we add an option in react-native-screens?

- E/App issue: [#101169](https://github.com/Expensify/App/issues/101169)
- PR introducing patch: [#101339](https://github.com/Expensify/App/pull/101339)
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
diff --git a/node_modules/react-native-screens/ios/tabs/host/RNSTabsHostComponentView.mm b/node_modules/react-native-screens/ios/tabs/host/RNSTabsHostComponentView.mm
--- a/node_modules/react-native-screens/ios/tabs/host/RNSTabsHostComponentView.mm
+++ b/node_modules/react-native-screens/ios/tabs/host/RNSTabsHostComponentView.mm
@@ -244,7 +244,9 @@
_tabBarHidden = newComponentProps.tabBarHidden;
#if RNS_IPHONE_OS_VERSION_AVAILABLE(18_0)
if (@available(iOS 18.0, *)) {
- [_controller setTabBarHidden:_tabBarHidden animated:NO];
+ // Animated so the bar slides out with the screen that hid it instead of blinking away, except before the bar
+ // is on screen, where the first render would animate it out.
+ [_controller setTabBarHidden:_tabBarHidden animated:[[_controller tabBar] window] != nil];
} else
#endif // RNS_IPHONE_OS_VERSION_AVAILABLE(18_0)
{
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
diff --git a/node_modules/react-native-screens/android/src/main/java/com/swmansion/rnscreens/gamma/tabs/appearance/TabsAppearanceApplicator.kt b/node_modules/react-native-screens/android/src/main/java/com/swmansion/rnscreens/gamma/tabs/appearance/TabsAppearanceApplicator.kt
index 10cd91b..6f059bd 100644
--- a/node_modules/react-native-screens/android/src/main/java/com/swmansion/rnscreens/gamma/tabs/appearance/TabsAppearanceApplicator.kt
+++ b/node_modules/react-native-screens/android/src/main/java/com/swmansion/rnscreens/gamma/tabs/appearance/TabsAppearanceApplicator.kt
@@ -3,6 +3,7 @@ package com.swmansion.rnscreens.gamma.tabs.appearance
import android.annotation.SuppressLint
import android.content.Context
import android.content.res.ColorStateList
+import android.graphics.Typeface
import android.graphics.drawable.StateListDrawable
import android.util.TypedValue
import android.view.MenuItem
@@ -161,13 +162,18 @@ internal class TabsAppearanceApplicator(
tabBarAppearance?.tabBarItemTitleLargeLabelFontSize?.takeIf { it > 0 }?.let { PixelUtil.toPixelFromSP(it) }
?: context.resources.getDimension(R.dimen.design_bottom_navigation_text_size)

+ // Both labels drop the 0.5sp letter tracking of Material's text appearance, which reads wider than the app's labels.
// Inactive
smallLabel.setTextSize(TypedValue.COMPLEX_UNIT_PX, smallFontSize)
smallLabel.typeface = fontFamily
+ smallLabel.letterSpacing = 0f

// Active
+ // Bold is set here rather than with `setItemTextAppearanceActiveBoldEnabled`, which re-applies Material's text size
+ // to this label alone, so Material lifts the selected item by the size difference between the two labels.
largeLabel.setTextSize(TypedValue.COMPLEX_UNIT_PX, largeFontSize)
- largeLabel.typeface = fontFamily
+ largeLabel.setTypeface(fontFamily, Typeface.BOLD)
+ largeLabel.letterSpacing = 0f
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
diff --git a/node_modules/react-native-screens/android/src/main/java/com/swmansion/rnscreens/gamma/tabs/appearance/TabsAppearanceApplicator.kt b/node_modules/react-native-screens/android/src/main/java/com/swmansion/rnscreens/gamma/tabs/appearance/TabsAppearanceApplicator.kt
index 9238416..d7d2024 100644
--- a/node_modules/react-native-screens/android/src/main/java/com/swmansion/rnscreens/gamma/tabs/appearance/TabsAppearanceApplicator.kt
+++ b/node_modules/react-native-screens/android/src/main/java/com/swmansion/rnscreens/gamma/tabs/appearance/TabsAppearanceApplicator.kt
@@ -64,24 +64,10 @@ internal class TabsAppearanceApplicator(
bottomNavigationView.itemTextColor = ColorStateList(states, fontColors)

// Icon color
- val iconDisabledColor =
- tabBarAppearance?.disabled?.tabBarItemIconColor
- ?: resolveColorAttr(context, R.attr.colorOnSurfaceVariant)
-
- val iconFocusedColor =
- tabBarAppearance?.focused?.tabBarItemIconColor
- ?: resolveColorAttr(context, R.attr.colorOnSurfaceVariant)
-
- val iconSelectedColor =
- tabBarAppearance?.selected?.tabBarItemIconColor
- ?: resolveColorAttr(context, R.attr.colorOnSecondaryContainer)
-
- val iconNormalColor =
- tabBarAppearance?.normal?.tabBarItemIconColor
- ?: resolveColorAttr(context, R.attr.colorOnSurfaceVariant)
-
- val iconColors = intArrayOf(iconDisabledColor, iconSelectedColor, iconFocusedColor, iconNormalColor)
- bottomNavigationView.itemIconTintList = ColorStateList(states, iconColors)
+ // Every tab icon arrives as a bitmap that already carries its own colors: the recolored glyph and the user's
+ // avatar on the account tab. A tint list paints over all of it with a single color, so no tint is applied and
+ // the bitmap is drawn as supplied, which is how `tinted: false` already behaves on iOS.
+ bottomNavigationView.itemIconTintList = null

// LabelVisibilityMode
// From docs: can be one of LABEL_VISIBILITY_AUTO, LABEL_VISIBILITY_SELECTED, LABEL_VISIBILITY_LABELED, or LABEL_VISIBILITY_UNLABELED
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
diff --git a/node_modules/@react-navigation/bottom-tabs/lib/module/unstable/NativeBottomTabView.native.js b/node_modules/@react-navigation/bottom-tabs/lib/module/unstable/NativeBottomTabView.native.js
index 1183c51..303635c 100644
--- a/node_modules/@react-navigation/bottom-tabs/lib/module/unstable/NativeBottomTabView.native.js
+++ b/node_modules/@react-navigation/bottom-tabs/lib/module/unstable/NativeBottomTabView.native.js
@@ -160,7 +160,7 @@ export function NativeBottomTabView({
ios: PlatformColor('label'),
default: colors.text
});
- const activeIndicatorColor = currentOptions?.tabBarActiveIndicatorColor ?? typeof activeTintColor === 'string' ? Color(activeTintColor)?.alpha(0.1).string() : undefined;
+ const activeIndicatorColor = currentOptions?.tabBarActiveIndicatorColor ?? (typeof activeTintColor === 'string' ? Color(activeTintColor)?.alpha(0.1).string() : undefined);
const onTransitionStart = ({
closing,
route
Loading
Loading