diff --git a/jest/setup.ts b/jest/setup.ts
index 4589474f054c..53f3c4829995 100644
--- a/jest/setup.ts
+++ b/jest/setup.ts
@@ -352,7 +352,6 @@ jest.mock('@shopify/react-native-skia', () => ({
// 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})),
diff --git a/src/CONST/index.ts b/src/CONST/index.ts
index b45da3cac2ae..905ef3ea0141 100644
--- a/src/CONST/index.ts
+++ b/src/CONST/index.ts
@@ -1462,8 +1462,6 @@ const CONST = {
ARCHIVE_POLICIES: 'archivePolicies',
MERGE_ATS: 'mergeATSConnections',
REPORT_MERGE: 'reportMerge',
- INSIGHTS_PAGE: 'insightsPage',
- INSIGHTS_COMPARE: 'insightsCompare',
INSIGHTS_COMPLIANCE: 'insightsCompliance',
NL_FILTERS: 'nlFilters',
PAYMENT_HISTORY: 'paymentHistory',
diff --git a/src/components/Navigation/AccountAvatarButton.tsx b/src/components/Navigation/AccountAvatarButton.tsx
index 01d66b3ac2a2..6ecfc2582be2 100644
--- a/src/components/Navigation/AccountAvatarButton.tsx
+++ b/src/components/Navigation/AccountAvatarButton.tsx
@@ -8,7 +8,6 @@ import useAccountTabIndicatorStatus from '@hooks/useAccountTabIndicatorStatus';
import useIsAnonymousUser from '@hooks/useIsAnonymousUser';
import useLocalize from '@hooks/useLocalize';
import useOnyx from '@hooks/useOnyx';
-import usePermissions from '@hooks/usePermissions';
import useResponsiveLayout from '@hooks/useResponsiveLayout';
import useThemeStyles from '@hooks/useThemeStyles';
@@ -30,10 +29,9 @@ import React from 'react';
* the Account tab is not part of the navigation tab bar.
*/
function AccountAvatarButton() {
- const {isBetaEnabled} = usePermissions();
const {shouldUseNarrowLayout} = useResponsiveLayout();
- if (!shouldUseNarrowLayout || !isBetaEnabled(CONST.BETAS.INSIGHTS_PAGE)) {
+ if (!shouldUseNarrowLayout) {
return null;
}
diff --git a/src/components/Navigation/NavigationTabBar/index.tsx b/src/components/Navigation/NavigationTabBar/index.tsx
index d0260ac3617b..85d2ac810cd8 100644
--- a/src/components/Navigation/NavigationTabBar/index.tsx
+++ b/src/components/Navigation/NavigationTabBar/index.tsx
@@ -8,7 +8,6 @@ import {PressableWithFeedback} from '@components/Pressable';
import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset';
import useLocalize from '@hooks/useLocalize';
import useOnyx from '@hooks/useOnyx';
-import usePermissions from '@hooks/usePermissions';
import useResponsiveLayout from '@hooks/useResponsiveLayout';
import useStyleUtils from '@hooks/useStyleUtils';
import useThemeStyles from '@hooks/useThemeStyles';
@@ -46,8 +45,6 @@ function NavigationTabBar({selectedTab, shouldShowFloatingButtons = true}: Navig
const styles = useThemeStyles();
const {translate} = useLocalize();
const [isDebugModeEnabled] = useOnyx(ONYXKEYS.IS_DEBUG_MODE_ENABLED);
- const {isBetaEnabled} = usePermissions();
- const isInsightsTabVisible = isBetaEnabled(CONST.BETAS.INSIGHTS_PAGE);
const expensifyIcons = useMemoizedLazyExpensifyIcons(['ExpensifyAppIcon', 'Home']);
const {shouldUseNarrowLayout} = useResponsiveLayout();
@@ -124,12 +121,10 @@ function NavigationTabBar({selectedTab, shouldShowFloatingButtons = true}: Navig
selectedTab={selectedTab}
isWideLayout
/>
- {isInsightsTabVisible && (
-
- )}
+
- {isInsightsTabVisible && (
-
- )}
+
- {!isInsightsTabVisible && (
-
- )}
{shouldShowFloatingButtons && (
diff --git a/src/components/Search/SearchRouter/useNavigationSuggestions.tsx b/src/components/Search/SearchRouter/useNavigationSuggestions.tsx
index 700c20b558a2..cabef0dd0c39 100644
--- a/src/components/Search/SearchRouter/useNavigationSuggestions.tsx
+++ b/src/components/Search/SearchRouter/useNavigationSuggestions.tsx
@@ -111,7 +111,6 @@ type BuildTopLevelNavigationItemsParams = {
account: string;
};
icons: TopLevelNavigationIcons;
- isInsightsPageBetaEnabled: boolean;
getSpendRoute: () => Route;
getDestinationText: (destination: string) => string;
};
@@ -216,7 +215,7 @@ type BuildAccountNavigationItemsParams = {
};
// Tab buttons own stateful navigation behavior and do not expose reusable descriptors, so Search Router keeps deterministic destination actions here.
-function buildTopLevelNavigationItems({labels, icons, isInsightsPageBetaEnabled, getSpendRoute, getDestinationText}: BuildTopLevelNavigationItemsParams): NavigationSuggestionSourceItem[] {
+function buildTopLevelNavigationItems({labels, icons, getSpendRoute, getDestinationText}: BuildTopLevelNavigationItemsParams): NavigationSuggestionSourceItem[] {
return [
{
text: getDestinationText(labels.home),
@@ -239,17 +238,13 @@ function buildTopLevelNavigationItems({labels, icons, isInsightsPageBetaEnabled,
keyForList: 'topLevelSpend',
matchTerms: [labels.spend],
},
- ...(isInsightsPageBetaEnabled
- ? [
- {
- text: getDestinationText(labels.insights),
- singleIcon: icons.PieChart,
- action: () => Navigation.navigate(ROUTES.INSIGHTS.getRoute(CONST.INSIGHTS.DASHBOARD.SPEND)),
- keyForList: 'topLevelInsights',
- matchTerms: [labels.insights],
- },
- ]
- : []),
+ {
+ text: getDestinationText(labels.insights),
+ singleIcon: icons.PieChart,
+ action: () => Navigation.navigate(ROUTES.INSIGHTS.getRoute(CONST.INSIGHTS.DASHBOARD.SPEND)),
+ keyForList: 'topLevelInsights',
+ matchTerms: [labels.insights],
+ },
{
text: getDestinationText(labels.workspaces),
singleIcon: icons.Building,
@@ -429,7 +424,6 @@ function useNavigationSuggestions(query: string, shouldWatchForApprovals = true)
account: translate('initialSettingsPage.account'),
},
icons,
- isInsightsPageBetaEnabled: isBetaEnabled(CONST.BETAS.INSIGHTS_PAGE),
getSpendRoute: () => getSearchTabRoute(navigationRef.getRootState(), lastSearchParams, lastExpensesSearchQuerySelector(searchFilters)),
getDestinationText: (destination) => getGoToText(translate, destination),
});
diff --git a/src/hooks/useIsSettingsDrawnOverTabs/index.native.ts b/src/hooks/useIsSettingsDrawnOverTabs/index.native.ts
index 18adf3c373f0..b36aa835f6be 100644
--- a/src/hooks/useIsSettingsDrawnOverTabs/index.native.ts
+++ b/src/hooks/useIsSettingsDrawnOverTabs/index.native.ts
@@ -1,19 +1,12 @@
-import usePermissions from '@hooks/usePermissions';
import useResponsiveLayout from '@hooks/useResponsiveLayout';
-import getTabWithoutBarItem from '@libs/Navigation/AppNavigator/Navigators/NativeTabNavigator/getTabWithoutBarItem';
-
-import CONST from '@src/CONST';
-import NAVIGATORS from '@src/NAVIGATORS';
-
/**
- * When the native tab bar has no Account item, the Account tab is drawn inside another tab and has no history of its
- * own, so the Android back button needs its own way back.
+ * On narrow layouts, Account is drawn inside another tab without its own history,
+ * so the Android back button needs its own way back.
*/
function useIsSettingsDrawnOverTabs() {
const {shouldUseNarrowLayout} = useResponsiveLayout();
- const {isBetaEnabled} = usePermissions();
- return shouldUseNarrowLayout && getTabWithoutBarItem(isBetaEnabled(CONST.BETAS.INSIGHTS_PAGE)) === NAVIGATORS.SETTINGS_SPLIT_NAVIGATOR;
+ return shouldUseNarrowLayout;
}
export default useIsSettingsDrawnOverTabs;
diff --git a/src/hooks/useSearchTypeMenuSections.ts b/src/hooks/useSearchTypeMenuSections.ts
index 8d1cb9d21b84..9550793213ab 100644
--- a/src/hooks/useSearchTypeMenuSections.ts
+++ b/src/hooks/useSearchTypeMenuSections.ts
@@ -132,10 +132,6 @@ const useSearchTypeMenuSectionsForNavigation = (isScreenFocused = true) => {
sections = omitTravelApprovalMenuItems(sections);
}
- if (!isBetaEnabled(CONST.BETAS.INSIGHTS_PAGE)) {
- return sections;
- }
-
return omitInsightsPageMenuItems(sections);
};
diff --git a/src/libs/Navigation/AppNavigator/Navigators/NativeTabNavigator/getTabWithoutBarItem.ts b/src/libs/Navigation/AppNavigator/Navigators/NativeTabNavigator/getTabWithoutBarItem.ts
deleted file mode 100644
index 502b8506ea4e..000000000000
--- a/src/libs/Navigation/AppNavigator/Navigators/NativeTabNavigator/getTabWithoutBarItem.ts
+++ /dev/null
@@ -1,12 +0,0 @@
-import NAVIGATORS from '@src/NAVIGATORS';
-import SCREENS from '@src/SCREENS';
-
-/**
- * The native bar shows at most five of the six tabs. With the Insights beta, Insights takes the Account tab's place
- * and Account moves to the top bar; without it, Insights has no item.
- */
-function getTabWithoutBarItem(isInsightsBetaEnabled: boolean) {
- return isInsightsBetaEnabled ? NAVIGATORS.SETTINGS_SPLIT_NAVIGATOR : SCREENS.INSIGHTS;
-}
-
-export default getTabWithoutBarItem;
diff --git a/src/libs/Navigation/AppNavigator/Navigators/NativeTabNavigator/tabIconRasterizer.ts b/src/libs/Navigation/AppNavigator/Navigators/NativeTabNavigator/tabIconRasterizer.ts
index 4a1f536ff1a6..8e573fee813d 100644
--- a/src/libs/Navigation/AppNavigator/Navigators/NativeTabNavigator/tabIconRasterizer.ts
+++ b/src/libs/Navigation/AppNavigator/Navigators/NativeTabNavigator/tabIconRasterizer.ts
@@ -2,29 +2,22 @@ import FontUtils from '@styles/utils/FontUtils';
import variables from '@styles/variables';
import type {NativeBottomTabIcon, NativeBottomTabNavigationOptions} from '@react-navigation/bottom-tabs/unstable';
-import type {SkCanvas, SkImage, SkParagraph, SkPath, SkTypefaceFontProvider} from '@shopify/react-native-skia';
+import type {SkCanvas, SkParagraph, SkPath} from '@shopify/react-native-skia';
-import {BlendMode, ClipOp, FillType, FilterMode, FontWeight, ImageFormat, MipmapMode, Skia, TextAlign} from '@shopify/react-native-skia';
+import {BlendMode, FillType, FontWeight, ImageFormat, Skia, TextAlign} from '@shopify/react-native-skia';
import {PixelRatio} from 'react-native';
import type {NativeTabGlyph, NativeTabName} from './NATIVE_TAB_GLYPHS';
-import type {TabAvatarImage} from './useTabAvatarImage';
import NATIVE_TAB_GLYPHS from './NATIVE_TAB_GLYPHS';
type TabIconLayout = {
/** Side of a glyph's box. */
glyphSize: number;
- /** Side of the account avatar's box. */
- avatarSize: number;
+ /** Side of the box that aligns a glyph with its label. */
+ iconBoxSize: number;
/** Whether the label is drawn under the glyph or beside it, as UIKit lays out a tab in landscape. */
direction: 'stacked' | 'inline';
- /** Radius of the emoji status badge, sized with the avatar so it stays clear of the status dot. */
- emojiStatusRadius: number;
- /** Size of the emoji drawn in that badge. */
- emojiStatusFontSize: number;
- /** How far that badge sits past the bottom-right corner of the avatar's box. The canvas grows to fit it. */
- emojiStatusOffset: number;
};
type TabIconLabel = {
@@ -38,21 +31,10 @@ type TabIconLabel = {
maxWidth: number;
};
-type TabIconEmojiStatus = {
- emoji: string;
- backgroundColor: string;
- /** The custom emoji font, given only for a private use area emoji, which the system fonts can't draw. */
- fontManager?: SkTypefaceFontProvider;
-};
-
type TabIconParams = {
- /** The glyph drawn unless an avatar is given. */
name: NativeTabName;
color: string;
- avatar?: TabAvatarImage;
dotColor?: string;
- /** The user's status emoji, badged on the account icon as the JS tab bar does. */
- emojiStatus?: TabIconEmojiStatus;
label?: TabIconLabel;
};
@@ -114,27 +96,6 @@ function drawGlyph(canvas: SkCanvas, name: NativeTabName, left: number, top: num
paint.dispose();
}
-function drawCircularImage(canvas: SkCanvas, image: SkImage, left: number, top: number, size: number) {
- const sourceSize = Math.min(image.width(), image.height());
- const circle = Skia.Path.Make();
- circle.addCircle(left + size / 2, top + size / 2, size / 2);
- const paint = Skia.Paint();
-
- canvas.save();
- canvas.clipPath(circle, ClipOp.Intersect, true);
- canvas.drawImageRectOptions(
- image,
- Skia.XYWHRect((image.width() - sourceSize) / 2, (image.height() - sourceSize) / 2, sourceSize, sourceSize),
- Skia.XYWHRect(left, top, size, size),
- FilterMode.Linear,
- MipmapMode.Linear,
- paint,
- );
- canvas.restore();
- paint.dispose();
- circle.dispose();
-}
-
/** A cutout rather than a border in the bar's color, because the iOS 26 bar is glass and has no single color. */
function drawStatusDot(canvas: SkCanvas, right: number, top: number, scale: number, color: string) {
const radius = variables.nativeTabIconDotRadius * scale;
@@ -150,34 +111,6 @@ function drawStatusDot(canvas: SkCanvas, right: number, top: number, scale: numb
paint.dispose();
}
-/** Drawn at the bottom-right of the badge box, opposite the status dot, and as far past it as the layout's offset. */
-function drawEmojiStatus(canvas: SkCanvas, layout: TabIconLayout, right: number, bottom: number, scale: number, {emoji, backgroundColor, fontManager}: TabIconEmojiStatus) {
- const radius = layout.emojiStatusRadius * scale;
- const offset = layout.emojiStatusOffset * scale;
- const centerX = right + offset - radius;
- const centerY = bottom + offset - radius;
- const paint = Skia.Paint();
- paint.setAntiAlias(true);
- paint.setBlendMode(BlendMode.Clear);
- canvas.drawCircle(centerX, centerY, radius + variables.nativeTabIconDotCutout * scale, paint);
- paint.setBlendMode(BlendMode.SrcOver);
- paint.setColor(Skia.Color(backgroundColor));
- canvas.drawCircle(centerX, centerY, radius, paint);
- paint.dispose();
-
- // A paragraph, so the emoji falls back to the system's color emoji font. A custom emoji font is tried first.
- const paragraph = Skia.ParagraphBuilder.Make({maxLines: 1, textAlign: TextAlign.Center}, fontManager)
- .pushStyle({
- fontFamilies: fontManager ? [FontUtils.fontFamily.single.CUSTOM_EMOJI_FONT.fontFamily] : undefined,
- fontSize: layout.emojiStatusFontSize * scale,
- })
- .addText(emoji)
- .build();
- paragraph.layout(radius * 2);
- paragraph.paint(canvas, centerX - radius, centerY - paragraph.getHeight() / 2);
- paragraph.dispose();
-}
-
type TabIconFrame = {
canvasWidth: number;
canvasHeight: number;
@@ -188,26 +121,16 @@ type TabIconFrame = {
};
/**
- * With a label, every tab gives its glyph or avatar a box as big as the avatar, so glyphs and the avatar share a centre
- * and every label starts at the same place: under that box, or beside it in landscape. The canvas also grows by the
- * overhang of anything drawn past the content's bottom-right corner, on both sides where the bar centres the content.
+ * With a label, every tab gives its glyph the same box, so every label starts at the same place:
+ * under that box, or beside it in landscape.
*/
-function getTabIconFrame(
- layout: TabIconLayout,
- contentSize: number,
- overhang: number,
- labelGap: number,
- labelWidth: number,
- labelHeight: number,
- hasLabel: boolean,
- scale: number,
-): TabIconFrame {
- const boxSize = hasLabel ? Math.max(layout.glyphSize, layout.avatarSize) * scale : contentSize;
+function getTabIconFrame(layout: TabIconLayout, contentSize: number, labelGap: number, labelWidth: number, labelHeight: number, hasLabel: boolean, scale: number): TabIconFrame {
+ const boxSize = hasLabel ? Math.max(layout.glyphSize, layout.iconBoxSize) * scale : contentSize;
if (layout.direction === 'inline') {
- const canvasHeight = Math.ceil(Math.max(boxSize + 2 * overhang, labelHeight));
+ const canvasHeight = Math.ceil(Math.max(boxSize, labelHeight));
return {
- canvasWidth: Math.ceil(boxSize + (hasLabel ? labelGap + labelWidth : overhang)),
+ canvasWidth: Math.ceil(boxSize + (hasLabel ? labelGap + labelWidth : 0)),
canvasHeight,
contentLeft: (boxSize - contentSize) / 2,
contentTop: (canvasHeight - contentSize) / 2,
@@ -216,13 +139,12 @@ function getTabIconFrame(
};
}
- const canvasWidth = Math.ceil(Math.max(contentSize + 2 * overhang, labelWidth));
+ const canvasWidth = Math.ceil(Math.max(contentSize, labelWidth));
const labelTop = (boxSize + layout.glyphSize * scale) / 2 + labelGap;
- // Under a label, the overhang reaches into the gap above it, so only an unlabelled icon grows in height.
- const contentTop = (boxSize - contentSize) / 2 + (hasLabel ? 0 : overhang);
+ const contentTop = (boxSize - contentSize) / 2;
return {
canvasWidth,
- canvasHeight: Math.ceil(hasLabel ? Math.max(labelTop + labelHeight, contentTop + contentSize + overhang) : boxSize + 2 * overhang),
+ canvasHeight: Math.ceil(hasLabel ? Math.max(labelTop + labelHeight, contentTop + contentSize) : boxSize),
contentLeft: (canvasWidth - contentSize) / 2,
contentTop,
labelLeft: (canvasWidth - labelWidth) / 2,
@@ -230,21 +152,19 @@ function getTabIconFrame(
};
}
-function drawTabIcon(layout: TabIconLayout, {name, color, avatar, dotColor, emojiStatus, label}: TabIconParams): NativeBottomTabIcon | undefined {
+function drawTabIcon(layout: TabIconLayout, {name, color, dotColor, label}: TabIconParams): NativeBottomTabIcon | undefined {
const scale = PixelRatio.get();
- const contentSize = (avatar ? layout.avatarSize : layout.glyphSize) * scale;
- // With an emoji, the dot and badge sit on an avatar-sized box even over the glyph, because the badge is sized to clear the dot on the avatar.
- const badgeBoxSize = avatar || emojiStatus ? layout.avatarSize * scale : contentSize;
- const overhang = emojiStatus ? layout.emojiStatusOffset * scale : 0;
+ const contentSize = layout.glyphSize * scale;
const paragraph = label ? makeLabelParagraph(label, scale) : undefined;
- const {
- canvasWidth,
- canvasHeight,
- contentLeft: badgeBoxLeft,
- contentTop: badgeBoxTop,
- labelLeft,
- labelTop,
- } = getTabIconFrame(layout, badgeBoxSize, overhang, (label?.gap ?? 0) * scale, paragraph?.getLongestLine() ?? 0, paragraph?.getHeight() ?? 0, !!label, scale);
+ const {canvasWidth, canvasHeight, contentLeft, contentTop, labelLeft, labelTop} = getTabIconFrame(
+ layout,
+ contentSize,
+ (label?.gap ?? 0) * scale,
+ paragraph?.getLongestLine() ?? 0,
+ paragraph?.getHeight() ?? 0,
+ !!label,
+ scale,
+ );
const surface = Skia.Surface.Make(canvasWidth, canvasHeight);
if (!surface) {
@@ -253,18 +173,9 @@ function drawTabIcon(layout: TabIconLayout, {name, color, avatar, dotColor, emoj
}
const canvas = surface.getCanvas();
- const contentLeft = badgeBoxLeft + (badgeBoxSize - contentSize) / 2;
- const contentTop = badgeBoxTop + (badgeBoxSize - contentSize) / 2;
- if (avatar) {
- drawCircularImage(canvas, avatar.image, contentLeft, contentTop, contentSize);
- } else {
- drawGlyph(canvas, name, contentLeft, contentTop, contentSize, color);
- }
+ drawGlyph(canvas, name, contentLeft, contentTop, contentSize, color);
if (dotColor) {
- drawStatusDot(canvas, badgeBoxLeft + badgeBoxSize, badgeBoxTop, scale, dotColor);
- }
- if (emojiStatus) {
- drawEmojiStatus(canvas, layout, badgeBoxLeft + badgeBoxSize, badgeBoxTop + badgeBoxSize, scale, emojiStatus);
+ drawStatusDot(canvas, contentLeft + contentSize, contentTop, scale, dotColor);
}
if (paragraph) {
paragraph.paint(canvas, labelLeft, labelTop);
@@ -289,11 +200,9 @@ function drawTabIcon(layout: TabIconLayout, {name, color, avatar, dotColor, emoj
* stays the same. A status, theme or language change redraws only the icons it reaches.
*/
function getTabIcon(layout: TabIconLayout, params: TabIconParams): NativeBottomTabIcon | undefined {
- const {name, color, avatar, dotColor, emojiStatus, label} = params;
+ const {name, color, dotColor, label} = params;
// The label's font is fixed per platform, so it is left out of the key.
- const key = [layout.direction, name, avatar?.uri ?? color, dotColor, emojiStatus?.emoji, emojiStatus?.backgroundColor, label?.text, label?.color, label?.isBold, label?.maxWidth].join(
- '|',
- );
+ const key = [layout.direction, name, color, dotColor, label?.text, label?.color, label?.isBold, label?.maxWidth].join('|');
if (iconCache.has(key)) {
return iconCache.get(key);
}
@@ -313,4 +222,4 @@ function getTabBarIcon(layout: TabIconLayout, getParams: (isSelected: boolean) =
}
export default getTabBarIcon;
-export type {TabIconLayout, TabIconEmojiStatus};
+export type {TabIconLayout};
diff --git a/src/libs/Navigation/AppNavigator/Navigators/NativeTabNavigator/useNativeTabBarOptions/index.android.ts b/src/libs/Navigation/AppNavigator/Navigators/NativeTabNavigator/useNativeTabBarOptions/index.android.ts
index 6c3ccbb09fa7..1a9f12a3bb2b 100644
--- a/src/libs/Navigation/AppNavigator/Navigators/NativeTabNavigator/useNativeTabBarOptions/index.android.ts
+++ b/src/libs/Navigation/AppNavigator/Navigators/NativeTabNavigator/useNativeTabBarOptions/index.android.ts
@@ -4,13 +4,9 @@ import useThemeStyles from '@hooks/useThemeStyles';
import type {NativeTabName} from '@libs/Navigation/AppNavigator/Navigators/NativeTabNavigator/NATIVE_TAB_GLYPHS';
import getTabBarIcon from '@libs/Navigation/AppNavigator/Navigators/NativeTabNavigator/tabIconRasterizer';
import type {TabIconLayout} from '@libs/Navigation/AppNavigator/Navigators/NativeTabNavigator/tabIconRasterizer';
-import useTabAvatarImage from '@libs/Navigation/AppNavigator/Navigators/NativeTabNavigator/useTabAvatarImage';
-import useTabEmojiStatus from '@libs/Navigation/AppNavigator/Navigators/NativeTabNavigator/useTabEmojiStatus';
import variables from '@styles/variables';
-import NAVIGATORS from '@src/NAVIGATORS';
-
import type {NativeBottomTabNavigationOptions} from '@react-navigation/bottom-tabs/unstable';
import type NativeTabBarOptionsParams from './types';
@@ -24,19 +20,13 @@ const getFloatingButtonsBottom = (bottomInset: number) => variables.androidNativ
*/
const TAB_ICON_LAYOUT: TabIconLayout = {
glyphSize: variables.iconBottomBar,
- avatarSize: variables.iconBottomBar,
+ iconBoxSize: variables.iconBottomBar,
direction: 'stacked',
- emojiStatusRadius: variables.nativeTabIconEmojiStatusRadius,
- emojiStatusFontSize: variables.nativeTabIconEmojiStatusFontSize,
- // Material scales an icon down to fit its slot, so a badge past the avatar would shrink the whole icon.
- emojiStatusOffset: 0,
};
-function useNativeTabBarOptions({shouldShowNativeTabBar, isAccountAvatarShown, dotColors, tabLabels}: NativeTabBarOptionsParams) {
+function useNativeTabBarOptions({shouldShowNativeTabBar, dotColors, tabLabels}: NativeTabBarOptionsParams) {
const theme = useTheme();
const styles = useThemeStyles();
- const avatar = useTabAvatarImage(isAccountAvatarShown);
- const emojiStatus = useTabEmojiStatus();
// The tint only reaches the labels, since the icons arrive already recolored (react-native-screens patch 004).
const screenOptions: NativeBottomTabNavigationOptions = {
@@ -51,13 +41,10 @@ function useNativeTabBarOptions({shouldShowNativeTabBar, isAccountAvatarShown, d
const getTabOptions = (name: NativeTabName): NativeBottomTabNavigationOptions => ({
tabBarLabel: tabLabels[name],
- // The active indicator pill marks the selected tab, so the avatar is the same image in both selection states.
tabBarIcon: getTabBarIcon(TAB_ICON_LAYOUT, (isSelected) => ({
name,
color: isSelected ? theme.iconMenu : theme.icon,
- avatar: name === NAVIGATORS.SETTINGS_SPLIT_NAVIGATOR ? avatar : undefined,
dotColor: dotColors[name],
- emojiStatus: name === NAVIGATORS.SETTINGS_SPLIT_NAVIGATOR ? emojiStatus : undefined,
})),
});
diff --git a/src/libs/Navigation/AppNavigator/Navigators/NativeTabNavigator/useNativeTabBarOptions/index.ios.ts b/src/libs/Navigation/AppNavigator/Navigators/NativeTabNavigator/useNativeTabBarOptions/index.ios.ts
index 8aa769641e9f..9ae16c652efe 100644
--- a/src/libs/Navigation/AppNavigator/Navigators/NativeTabNavigator/useNativeTabBarOptions/index.ios.ts
+++ b/src/libs/Navigation/AppNavigator/Navigators/NativeTabNavigator/useNativeTabBarOptions/index.ios.ts
@@ -5,14 +5,10 @@ import useWindowDimensions from '@hooks/useWindowDimensions';
import type {NativeTabName} from '@libs/Navigation/AppNavigator/Navigators/NativeTabNavigator/NATIVE_TAB_GLYPHS';
import getTabBarIcon from '@libs/Navigation/AppNavigator/Navigators/NativeTabNavigator/tabIconRasterizer';
import type {TabIconLayout} from '@libs/Navigation/AppNavigator/Navigators/NativeTabNavigator/tabIconRasterizer';
-import useTabAvatarImage from '@libs/Navigation/AppNavigator/Navigators/NativeTabNavigator/useTabAvatarImage';
-import useTabEmojiStatus from '@libs/Navigation/AppNavigator/Navigators/NativeTabNavigator/useTabEmojiStatus';
import {fontScale} from '@styles/typography';
import variables from '@styles/variables';
-import NAVIGATORS from '@src/NAVIGATORS';
-
import type {NativeBottomTabNavigationOptions} from '@react-navigation/bottom-tabs/unstable';
import type NativeTabBarOptionsParams from './types';
@@ -22,22 +18,15 @@ const getFloatingButtonsBottom = (bottomInset: number) => variables.iosNativeTab
const TAB_ICON_LAYOUT: TabIconLayout = {
glyphSize: variables.iconNativeTabBarIOS,
- // A circle reads smaller than a glyph of the same box, so the avatar is drawn a little larger than the glyphs.
- avatarSize: variables.avatarNativeTabBarIOS,
+ iconBoxSize: variables.nativeTabIconBoxIOS,
direction: 'stacked',
- emojiStatusRadius: variables.nativeTabIconEmojiStatusRadius,
- emojiStatusFontSize: variables.nativeTabIconEmojiStatusFontSize,
- emojiStatusOffset: variables.nativeTabIconEmojiStatusOffset,
};
/** A phone in landscape gives the bar too little height for a stacked tab, so UIKit lays its own tabs out this way. */
const LANDSCAPE_TAB_ICON_LAYOUT: TabIconLayout = {
glyphSize: variables.iconNativeTabBarIOSLandscape,
- avatarSize: variables.avatarNativeTabBarIOSLandscape,
+ iconBoxSize: variables.nativeTabIconBoxIOSLandscape,
direction: 'inline',
- emojiStatusRadius: variables.nativeTabIconEmojiStatusRadiusLandscape,
- emojiStatusFontSize: variables.nativeTabIconEmojiStatusFontSizeLandscape,
- emojiStatusOffset: variables.nativeTabIconEmojiStatusOffset,
};
/**
@@ -45,18 +34,15 @@ const LANDSCAPE_TAB_ICON_LAYOUT: TabIconLayout = {
* in the color of the selected tab, so each icon is drawn with its own colors, its status dot and its label. Both
* selection states are images, because RNScreens rejects a tab whose icon and selectedIcon differ in type.
*/
-function useNativeTabBarOptions({shouldShowNativeTabBar, isAccountAvatarShown, dotColors, tabLabels}: NativeTabBarOptionsParams) {
+function useNativeTabBarOptions({shouldShowNativeTabBar, dotColors, tabLabels}: NativeTabBarOptionsParams) {
const theme = useTheme();
- const avatar = useTabAvatarImage(isAccountAvatarShown);
- const emojiStatus = useTabEmojiStatus();
const {windowWidth} = useWindowDimensions();
const {isInLandscapeMode} = useResponsiveLayout();
const tabIconLayout = isInLandscapeMode ? LANDSCAPE_TAB_ICON_LAYOUT : TAB_ICON_LAYOUT;
- // One of the tabs never has a bar item, so the bar splits its width between the others.
- const barItemCount = Object.keys(tabLabels).length - 1;
+ const barItemCount = Object.keys(tabLabels).length;
const itemWidth = (windowWidth - 2 * variables.iosNativeTabBarHorizontalInset) / barItemCount - variables.iosNativeTabBarLabelInset;
// In landscape the label shares the item's width with the glyph's box beside it.
- const labelMaxWidth = isInLandscapeMode ? itemWidth - tabIconLayout.avatarSize - variables.nativeTabIconLabelGap : itemWidth;
+ const labelMaxWidth = isInLandscapeMode ? itemWidth - tabIconLayout.iconBoxSize - variables.nativeTabIconLabelGap : itemWidth;
const screenOptions: NativeBottomTabNavigationOptions = {
// The labels are drawn into the icons.
@@ -75,9 +61,7 @@ function useNativeTabBarOptions({shouldShowNativeTabBar, isAccountAvatarShown, d
tabBarIcon: getTabBarIcon(tabIconLayout, (isSelected) => ({
name,
color: isSelected ? theme.iconMenu : theme.icon,
- avatar: name === NAVIGATORS.SETTINGS_SPLIT_NAVIGATOR ? avatar : undefined,
dotColor: dotColors[name],
- emojiStatus: name === NAVIGATORS.SETTINGS_SPLIT_NAVIGATOR ? emojiStatus : undefined,
label: {
text: tabLabels[name],
color: isSelected ? theme.text : theme.textSupporting,
diff --git a/src/libs/Navigation/AppNavigator/Navigators/NativeTabNavigator/useNativeTabBarOptions/types.ts b/src/libs/Navigation/AppNavigator/Navigators/NativeTabNavigator/useNativeTabBarOptions/types.ts
index 4091ff2f6522..2728ae0e274a 100644
--- a/src/libs/Navigation/AppNavigator/Navigators/NativeTabNavigator/useNativeTabBarOptions/types.ts
+++ b/src/libs/Navigation/AppNavigator/Navigators/NativeTabNavigator/useNativeTabBarOptions/types.ts
@@ -1,7 +1,5 @@
type NativeTabBarOptionsParams = {
shouldShowNativeTabBar: boolean;
- /** Whether the account tab has an item in a narrow layout's bar, so its avatar is drawn. */
- isAccountAvatarShown: boolean;
dotColors: Record;
tabLabels: Record;
};
diff --git a/src/libs/Navigation/AppNavigator/Navigators/NativeTabNavigator/useNativeTabNavigator.ts b/src/libs/Navigation/AppNavigator/Navigators/NativeTabNavigator/useNativeTabNavigator.ts
index d9d779c1e842..191b35482cd5 100644
--- a/src/libs/Navigation/AppNavigator/Navigators/NativeTabNavigator/useNativeTabNavigator.ts
+++ b/src/libs/Navigation/AppNavigator/Navigators/NativeTabNavigator/useNativeTabNavigator.ts
@@ -1,22 +1,17 @@
import {useFullScreenBlockingViewState} from '@components/FullScreenBlockingViewContextProvider';
-import useAccountTabIndicatorStatus from '@hooks/useAccountTabIndicatorStatus';
import useInboxTabIndicatorStatus from '@hooks/useInboxTabIndicatorStatus';
import useLocalize from '@hooks/useLocalize';
import useOnyx from '@hooks/useOnyx';
-import usePermissions from '@hooks/usePermissions';
import useResponsiveLayout from '@hooks/useResponsiveLayout';
import useWorkspacesTabIndicatorStatus from '@hooks/useWorkspacesTabIndicatorStatus';
import {getPreservedNavigatorState, setPreservedNavigatorState} from '@libs/Navigation/AppNavigator/createSplitNavigator/usePreserveNavigatorState';
import isTabRouteAtRoot from '@libs/Navigation/helpers/isTabRouteAtRoot';
-import Navigation from '@libs/Navigation/Navigation';
import type {TabNavigatorParamList} from '@libs/Navigation/types';
-import CONST from '@src/CONST';
import NAVIGATORS from '@src/NAVIGATORS';
import ONYXKEYS from '@src/ONYXKEYS';
-import ROUTES from '@src/ROUTES';
import SCREENS from '@src/SCREENS';
import type {NavigationAction, NavigationState, PartialState, Router, TabNavigationState} from '@react-navigation/native';
@@ -26,7 +21,6 @@ import {isAnonymousSessionSelector} from '@selectors/Session';
import {useEffect} from 'react';
import getActiveTabRoute from './getActiveTabRoute';
-import getTabWithoutBarItem from './getTabWithoutBarItem';
import {isInboxTabAtChatList, isNativeTabSelectionEnabled} from './tabScreenListeners';
import useIsWorkspacesTabRestored from './useIsWorkspacesTabRestored';
@@ -45,14 +39,11 @@ function isRealizedNavigationState(state: NavigationState | PartialState TAB_ROOT_SCREENS_WITHOUT_GESTURE.has(findFocusedRoute(state)?.name ?? ''));
@@ -62,10 +53,7 @@ function useNativeTabNavigator() {
// mounts, so the nearest navigation listener context is still the parent stack's.
const tabState = useNavigationState((parentState) => parentState.routes.find((parentRoute) => parentRoute.key === route.key)?.state);
const activeTabRoute = getActiveTabRoute(tabState);
- // Insights without the beta is a full screen until Home replaces it, so the bar hides while it is focused. Account
- // without a bar item keeps the bar, which then shows no tab as selected.
- const isInsightsFocusedWithoutBarItem = !isInsightsTabVisible && activeTabRoute?.name === SCREENS.INSIGHTS;
- const shouldShowNativeTabBar = shouldUseNarrowLayout && isTabRouteAtRoot(activeTabRoute) && !isBlockingViewVisible && !isInsightsFocusedWithoutBarItem;
+ const shouldShowNativeTabBar = shouldUseNarrowLayout && isTabRouteAtRoot(activeTabRoute) && !isBlockingViewVisible;
const getTabRoute = (tabName: string) => (isRealizedNavigationState(tabState) ? tabState.routes.find((tabRoute) => tabRoute.name === tabName) : undefined);
const isInboxAtChatList = isInboxTabAtChatList(getTabRoute(NAVIGATORS.REPORTS_SPLIT_NAVIGATOR));
const isWorkspacesTabRestored = useIsWorkspacesTabRestored(getTabRoute(NAVIGATORS.WORKSPACE_NAVIGATOR));
@@ -73,7 +61,6 @@ function useNativeTabNavigator() {
const dotColors: Record = {
[NAVIGATORS.REPORTS_SPLIT_NAVIGATOR]: inboxDotColor,
[NAVIGATORS.WORKSPACE_NAVIGATOR]: workspacesIndicatorStatus ? workspacesIndicatorColor : undefined,
- [NAVIGATORS.SETTINGS_SPLIT_NAVIGATOR]: accountIndicatorStatus ? accountIndicatorColor : undefined,
};
const tabLabels: Record = {
[SCREENS.HOME]: translate('common.home'),
@@ -81,7 +68,6 @@ function useNativeTabNavigator() {
[NAVIGATORS.SEARCH_FULLSCREEN_NAVIGATOR]: translate('common.spend'),
[SCREENS.INSIGHTS]: translate('common.insights'),
[NAVIGATORS.WORKSPACE_NAVIGATOR]: translate('common.workspacesTabTitle'),
- [NAVIGATORS.SETTINGS_SPLIT_NAVIGATOR]: translate('initialSettingsPage.account'),
};
useEffect(() => {
@@ -98,17 +84,6 @@ function useNativeTabNavigator() {
setPreservedNavigatorState(route.key, tabState);
}, [tabState, route.key]);
- // Without the beta, Insights has no bar item and its page is not found, so a restored or deep-linked Insights tab
- // would be a full screen with no way out. Home replaces it, so going back does not return to it. Betas that are
- // still loading are not a missing beta.
- const isInsightsTabFocusedWithoutBeta = isBetaEnabledOrUnknown(CONST.BETAS.INSIGHTS_PAGE) === false && activeTabRoute?.name === SCREENS.INSIGHTS;
- useEffect(() => {
- if (!isInsightsTabFocusedWithoutBeta) {
- return;
- }
- Navigation.navigate(ROUTES.HOME, {forceReplace: true});
- }, [isInsightsTabFocusedWithoutBeta]);
-
// The slicing optimization in useCustomRootStackNavigatorState can unmount and later remount
// this TAB_NAVIGATOR. Without restoration it would default to index 0. We restore the saved
// state by overriding the bottom-tab router's getInitialState, the same pattern SplitRouter
@@ -122,11 +97,9 @@ function useNativeTabNavigator() {
},
});
- const isAccountAvatarShown = shouldUseNarrowLayout && tabWithoutBarItem !== NAVIGATORS.SETTINGS_SPLIT_NAVIGATOR;
-
const isTabSelectionEnabled = (routeName: string) => isNativeTabSelectionEnabled(routeName, {isAnonymousUser, isInboxAtChatList, isWorkspacesTabRestored});
- return {shouldShowNativeTabBar, isAccountAvatarShown, dotColors, tabLabels, tabWithoutBarItem, tabRouterOverride, isTabSelectionEnabled};
+ return {shouldShowNativeTabBar, dotColors, tabLabels, tabWithoutBarItem, tabRouterOverride, isTabSelectionEnabled};
}
export default useNativeTabNavigator;
diff --git a/src/libs/Navigation/AppNavigator/Navigators/NativeTabNavigator/useTabAvatarImage.ts b/src/libs/Navigation/AppNavigator/Navigators/NativeTabNavigator/useTabAvatarImage.ts
deleted file mode 100644
index b38d210f3cd0..000000000000
--- a/src/libs/Navigation/AppNavigator/Navigators/NativeTabNavigator/useTabAvatarImage.ts
+++ /dev/null
@@ -1,54 +0,0 @@
-import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails';
-
-import {getAvatarURL} from '@libs/UserAvatarUtils';
-
-import type {SkImage} from '@shopify/react-native-skia';
-
-import {Skia} from '@shopify/react-native-skia';
-import {useEffect, useState} from 'react';
-
-type TabAvatarImage = {uri: string; image: SkImage};
-
-/** Held outside any component, so a status or theme redraw and a remounted navigator reuse the decoded avatar. */
-let lastDecodedAvatar: TabAvatarImage | undefined;
-
-/**
- * The current user's avatar, decoded once per URI for the account tab icon while that icon is shown. Until it is
- * decoded, and for avatars that are not a remote image, the account glyph stands in.
- */
-function useTabAvatarImage(isShown: boolean): TabAvatarImage | undefined {
- const {avatar, accountID} = useCurrentUserPersonalDetails();
- const avatarSource = getAvatarURL({avatarSource: avatar, accountID});
- const uri = typeof avatarSource === 'string' ? avatarSource : undefined;
- const [decodedAvatar, setDecodedAvatar] = useState(lastDecodedAvatar);
- const decodedUri = decodedAvatar?.uri;
-
- useEffect(() => {
- if (!isShown || !uri || decodedUri === uri) {
- return;
- }
- let isActive = true;
- Skia.Data.fromURI(uri)
- .then((data) => {
- const image = Skia.Image.MakeImageFromEncoded(data);
- data.dispose();
- if (!image) {
- return;
- }
- lastDecodedAvatar = {uri, image};
- if (isActive) {
- setDecodedAvatar(lastDecodedAvatar);
- }
- })
- // A failed decode keeps the account glyph.
- .catch(() => undefined);
- return () => {
- isActive = false;
- };
- }, [isShown, uri, decodedUri]);
-
- return decodedAvatar?.uri === uri ? decodedAvatar : undefined;
-}
-
-export default useTabAvatarImage;
-export type {TabAvatarImage};
diff --git a/src/libs/Navigation/AppNavigator/Navigators/NativeTabNavigator/useTabEmojiStatus.ts b/src/libs/Navigation/AppNavigator/Navigators/NativeTabNavigator/useTabEmojiStatus.ts
deleted file mode 100644
index 08802e1a4495..000000000000
--- a/src/libs/Navigation/AppNavigator/Navigators/NativeTabNavigator/useTabEmojiStatus.ts
+++ /dev/null
@@ -1,70 +0,0 @@
-import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails';
-import useOnyx from '@hooks/useOnyx';
-import useTheme from '@hooks/useTheme';
-
-import {containsCustomEmoji} from '@libs/CustomEmojiUtils';
-
-import FontUtils from '@styles/utils/FontUtils';
-
-import ONYXKEYS from '@src/ONYXKEYS';
-
-import type {SkTypefaceFontProvider} from '@shopify/react-native-skia';
-
-import {delegateEmailSelector} from '@selectors/Account';
-import {Skia} from '@shopify/react-native-skia';
-import {useEffect, useState} from 'react';
-import {Image} from 'react-native';
-
-import type {TabIconEmojiStatus} from './tabIconRasterizer';
-
-// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- require() of a bundled asset is untyped. Metro resolves it to an asset ID.
-const CUSTOM_EMOJI_FONT = require('@assets/fonts/native/CustomEmojiNativeFont.ttf') as number;
-
-/** Held outside any component, so the font is read from disk once and a remounted navigator reuses it. */
-let loadedCustomEmojiFontManager: SkTypefaceFontProvider | undefined;
-
-/**
- * The current user's status emoji for the account tab icon, as the JS tab bar badges it: not while acting as a
- * delegate, because the delegate avatar takes its place there. Skia's system fonts have no glyph for our private use
- * area emojis, so those wait for the custom emoji font to load rather than draw a blank badge.
- */
-function useTabEmojiStatus(): TabIconEmojiStatus | undefined {
- const theme = useTheme();
- const [delegateEmail] = useOnyx(ONYXKEYS.ACCOUNT, {selector: delegateEmailSelector});
- const emojiCode = useCurrentUserPersonalDetails().status?.emojiCode;
- const isCustomEmoji = containsCustomEmoji(emojiCode);
- const [customEmojiFontManager, setCustomEmojiFontManager] = useState(loadedCustomEmojiFontManager);
-
- useEffect(() => {
- if (!isCustomEmoji || customEmojiFontManager) {
- return;
- }
- let isActive = true;
- Skia.Data.fromURI(Image.resolveAssetSource(CUSTOM_EMOJI_FONT).uri)
- .then((data) => {
- const typeface = Skia.Typeface.MakeFreeTypeFaceFromData(data);
- data.dispose();
- if (!typeface) {
- return;
- }
- const fontManager = Skia.TypefaceFontProvider.Make();
- fontManager.registerFont(typeface, FontUtils.fontFamily.single.CUSTOM_EMOJI_FONT.fontFamily);
- loadedCustomEmojiFontManager = fontManager;
- if (isActive) {
- setCustomEmojiFontManager(fontManager);
- }
- })
- // A failed load keeps the badge off.
- .catch(() => undefined);
- return () => {
- isActive = false;
- };
- }, [isCustomEmoji, customEmojiFontManager]);
-
- if (!emojiCode || delegateEmail || (isCustomEmoji && !customEmojiFontManager)) {
- return undefined;
- }
- return {emoji: emojiCode, backgroundColor: theme.border, fontManager: isCustomEmoji ? customEmojiFontManager : undefined};
-}
-
-export default useTabEmojiStatus;
diff --git a/src/libs/Navigation/AppNavigator/Navigators/TabNavigator.native.tsx b/src/libs/Navigation/AppNavigator/Navigators/TabNavigator.native.tsx
index d3c119494ccd..59934c4fe6e6 100644
--- a/src/libs/Navigation/AppNavigator/Navigators/TabNavigator.native.tsx
+++ b/src/libs/Navigation/AppNavigator/Navigators/TabNavigator.native.tsx
@@ -35,8 +35,8 @@ const HIDDEN_TAB_OPTIONS = {tabBarItemHidden: true};
const renderNativeTabLayout = (props: NativeTabLayoutProps) => ;
function TabNavigator() {
- const {shouldShowNativeTabBar, isAccountAvatarShown, dotColors, tabLabels, tabWithoutBarItem, tabRouterOverride, isTabSelectionEnabled} = useNativeTabNavigator();
- const {screenOptions, getTabOptions} = useNativeTabBarOptions({shouldShowNativeTabBar, isAccountAvatarShown, dotColors, tabLabels});
+ const {shouldShowNativeTabBar, dotColors, tabLabels, tabWithoutBarItem, tabRouterOverride, isTabSelectionEnabled} = useNativeTabNavigator();
+ const {screenOptions, getTabOptions} = useNativeTabBarOptions({shouldShowNativeTabBar, dotColors, tabLabels});
// A tab with no bar item draws no icon.
const getScreenOptions = ({route}: {route: {name: NativeTabName}}) =>
route.name === tabWithoutBarItem
diff --git a/src/libs/Permissions.ts b/src/libs/Permissions.ts
index b5830dc79fbc..12a4e8880929 100644
--- a/src/libs/Permissions.ts
+++ b/src/libs/Permissions.ts
@@ -8,8 +8,6 @@ import type {OnyxEntry} from 'react-native-onyx';
import type Environment from './Environment/getEnvironment/types';
-import getPlatform from './getPlatform';
-
/**
* Overrides are a test tool, so they never apply in production.
* Android staging and TestFlight are compiled as production, so only the resolved environment tells a real production
@@ -40,11 +38,6 @@ function isBetaEnabled(beta: Beta, betas: OnyxEntry, betaConfiguration:
}
}
- // Release desktop web independently while the native navigation fixes roll out.
- if ((beta === CONST.BETAS.INSIGHTS_PAGE || beta === CONST.BETAS.INSIGHTS_COMPARE) && getPlatform(true) === CONST.PLATFORM.WEB) {
- return true;
- }
-
const hasAllBetasEnabled = canUseAllBetas(betas);
const isFeatureEnabled = !!betas?.includes(beta);
diff --git a/src/libs/SearchUIUtils.ts b/src/libs/SearchUIUtils.ts
index 36c670af1996..bf956cbd8243 100644
--- a/src/libs/SearchUIUtils.ts
+++ b/src/libs/SearchUIUtils.ts
@@ -7800,7 +7800,6 @@ export {
createTypeMenuSections,
omitInsightsPageMenuItems,
omitTravelApprovalMenuItems,
- SPEND_INSIGHT_KEYS,
formatBadgeText,
getSectionBadgeText,
getItemBadgeText,
diff --git a/src/pages/Insights/InsightsDashboard.tsx b/src/pages/Insights/InsightsDashboard.tsx
index df6505c68d11..4a7ecb15c442 100644
--- a/src/pages/Insights/InsightsDashboard.tsx
+++ b/src/pages/Insights/InsightsDashboard.tsx
@@ -15,7 +15,6 @@ import useLocalize from '@hooks/useLocalize';
import useMultipleSnapshots from '@hooks/useMultipleSnapshots';
import useNetwork from '@hooks/useNetwork';
import useOnyx from '@hooks/useOnyx';
-import usePermissions from '@hooks/usePermissions';
import useResponsiveLayout from '@hooks/useResponsiveLayout';
import useScrollEventEmitter from '@hooks/useScrollEventEmitter';
import useTheme from '@hooks/useTheme';
@@ -200,13 +199,10 @@ function InsightsDashboard({dashboardID, accessibleDashboards}: {dashboardID: In
const {login} = useCurrentUserPersonalDetails();
const [policies, policiesMetadata] = useOnyx(ONYXKEYS.COLLECTION.POLICY);
const {filters, defaultFilters, isResolved, setFilters} = useInsightsFilters(dashboardID);
- const {isBetaEnabled} = usePermissions();
-
- const isCompareEnabled = isBetaEnabled(CONST.BETAS.INSIGHTS_COMPARE);
const {headlineChart: headlineSpec, supportingCharts: supportingSpecs} = INSIGHTS_DASHBOARD_SPECS[dashboardID];
const eligibleCharts = getVisibleCharts(supportingSpecs, policies, filters.policyIDs, login);
const eligibleGraphKeys = eligibleCharts.map(({graphKey}) => graphKey).join(',');
- const query = isResolved && !isLoadingOnyxValue(policiesMetadata) ? buildInsightsJsonQuery(dashboardID, filters, eligibleCharts, isCompareEnabled) : undefined;
+ const query = isResolved && !isLoadingOnyxValue(policiesMetadata) ? buildInsightsJsonQuery(dashboardID, filters, eligibleCharts) : undefined;
const hash = query?.hash;
// The bucket actually queried, which Running total resolves to a real day/month bucket. Toggling Running total alone leaves this unchanged, so it stays a client-side transform with no refetch.
const queryGroupBy = isResolved ? getQueryGroupBy(filters) : undefined;
@@ -228,7 +224,7 @@ function InsightsDashboard({dashboardID, accessibleDashboards}: {dashboardID: In
}
onRequestConditionsChanged();
// Leaves out jsonQuery so toggling Compare mode doesn't refetch, since every response covers it. The effective bucket and the eligible charts are listed because the hash leaves them out, so changing them triggers a refetch while a pure Running total toggle does not.
- }, [dashboardID, hash, queryGroupBy, eligibleGraphKeys, isCompareEnabled, isFocused, isOffline]);
+ }, [dashboardID, hash, queryGroupBy, eligibleGraphKeys, isFocused, isOffline]);
const [dashboard] = useOnyx(`${ONYXKEYS.COLLECTION.INSIGHTS}${dashboardID}_${hash}`);
// Until the filters and workspaces load, the charts have no query to read data for, so they show their loading state
diff --git a/src/pages/Insights/InsightsPage.tsx b/src/pages/Insights/InsightsPage.tsx
index 5ebb859edab7..2b8924420547 100644
--- a/src/pages/Insights/InsightsPage.tsx
+++ b/src/pages/Insights/InsightsPage.tsx
@@ -1,6 +1,5 @@
import useDocumentTitle from '@hooks/useDocumentTitle';
import useLocalize from '@hooks/useLocalize';
-import usePermissions from '@hooks/usePermissions';
import Navigation from '@libs/Navigation/Navigation';
import type {TabNavigatorParamList} from '@libs/Navigation/types';
@@ -25,13 +24,12 @@ type InsightsPageProps = BottomTabScreenProps(), accessibleDashboardsMetadata] = useAccessibleDashboards();
useDocumentTitle(translate('common.insights'));
const dashboardID = Object.values(CONST.INSIGHTS.DASHBOARD).find((id) => id === route.params?.dashboardID);
- if (!isBetaEnabled(CONST.BETAS.INSIGHTS_PAGE) || !dashboardID || (!isLoadingOnyxValue(accessibleDashboardsMetadata) && !accessibleDashboards.includes(dashboardID))) {
+ if (!dashboardID || (!isLoadingOnyxValue(accessibleDashboardsMetadata) && !accessibleDashboards.includes(dashboardID))) {
return Navigation.goBack(ROUTES.HOME)} />;
}
diff --git a/src/pages/Insights/controls/InsightsPageControls.tsx b/src/pages/Insights/controls/InsightsPageControls.tsx
index 0c9057a235b9..06d364d4b233 100644
--- a/src/pages/Insights/controls/InsightsPageControls.tsx
+++ b/src/pages/Insights/controls/InsightsPageControls.tsx
@@ -1,14 +1,11 @@
import ScrollView from '@components/ScrollView';
import useLayoutSpacing from '@hooks/useLayoutSpacing';
-import usePermissions from '@hooks/usePermissions';
import useResponsiveLayout from '@hooks/useResponsiveLayout';
import useThemeStyles from '@hooks/useThemeStyles';
import type {InsightsFilters} from '@pages/Insights/insightsFilters';
-import CONST from '@src/CONST';
-
import React from 'react';
import {View} from 'react-native';
@@ -31,7 +28,6 @@ function InsightsPageControls({filters, defaultFilters, onChange}: InsightsPageC
const styles = useThemeStyles();
const {pageGutter} = useLayoutSpacing();
const {shouldUseNarrowLayout} = useResponsiveLayout();
- const {isBetaEnabled} = usePermissions();
const controls = (
<>
@@ -39,12 +35,10 @@ function InsightsPageControls({filters, defaultFilters, onChange}: InsightsPageC
value={filters.date}
onChange={(date) => onChange({date})}
/>
- {isBetaEnabled(CONST.BETAS.INSIGHTS_COMPARE) && (
- onChange({compare})}
- />
- )}
+ onChange({compare})}
+ />
onChange({policyIDs})}
diff --git a/src/pages/Insights/insightsQueries.ts b/src/pages/Insights/insightsQueries.ts
index d1af361c53d9..a98e6069ca86 100644
--- a/src/pages/Insights/insightsQueries.ts
+++ b/src/pages/Insights/insightsQueries.ts
@@ -103,15 +103,11 @@ type InsightsGraphHashes = {
type InsightsGraphHashEntry = [InsightsGraphKey, InsightsGraphHashes];
/** Returns the chart's graph slot paired with the hashes its current period, previous period and average are stored under. */
-function buildSnapshotHashEntry({chart, queryJSON, previousPeriodQueryJSON, averageQueryJSON}: InsightsChartQuery, shouldIncludeComparisons: boolean): InsightsGraphHashEntry | undefined {
+function buildSnapshotHashEntry({chart, queryJSON, previousPeriodQueryJSON, averageQueryJSON}: InsightsChartQuery): InsightsGraphHashEntry | undefined {
const snapshotHash = queryJSON?.hash;
if (!snapshotHash) {
return undefined;
}
- if (!shouldIncludeComparisons) {
- return [chart.graphKey, {snapshotHash}];
- }
-
const previousPeriodSnapshotHash = previousPeriodQueryJSON?.hash;
const averageSnapshotHash = averageQueryJSON?.hash;
if (!previousPeriodSnapshotHash || !averageSnapshotHash) {
@@ -136,12 +132,7 @@ type InsightsQuery = {
};
/** Builds one request for the dashboard's headline chart and the given supporting charts, naming each graph's snapshots by its chart's query hashes, and returns those chart queries to read the snapshots back with. */
-function buildInsightsJsonQuery(
- dashboard: InsightsDashboardID,
- filters: InsightsFilters,
- supportingChartSpecs: InsightsChartSpec[],
- shouldIncludeComparisons: boolean,
-): InsightsQuery | undefined {
+function buildInsightsJsonQuery(dashboard: InsightsDashboardID, filters: InsightsFilters, supportingChartSpecs: InsightsChartSpec[]): InsightsQuery | undefined {
// The backend saves inputQuery as the dashboard's filters, so it keeps the Group by the user picked, not the one fitted to the date range
const inputQuery = buildInsightsQueryString(filters);
const queryJSON = buildDashboardQuery(buildInsightsQueryString({...filters, groupBy: getQueryGroupBy(filters), compare: undefined}), dashboard);
@@ -153,16 +144,12 @@ function buildInsightsJsonQuery(
const buildChartQuery = (chart: InsightsChartSpec): InsightsChartQuery => ({
chart,
queryJSON: buildDashboardQuery(applyInsightsFilters(chart, filters), dashboard),
- ...(shouldIncludeComparisons && {
- previousPeriodQueryJSON: buildDashboardQuery(applyInsightsFilters(chart, filters, CONST.SEARCH.COMPARE.PREVIOUS_PERIOD), dashboard),
- averageQueryJSON: buildDashboardQuery(applyInsightsFilters(chart, filters, CONST.SEARCH.COMPARE.AVERAGE), dashboard),
- }),
+ previousPeriodQueryJSON: buildDashboardQuery(applyInsightsFilters(chart, filters, CONST.SEARCH.COMPARE.PREVIOUS_PERIOD), dashboard),
+ averageQueryJSON: buildDashboardQuery(applyInsightsFilters(chart, filters, CONST.SEARCH.COMPARE.AVERAGE), dashboard),
});
const headlineChart = buildChartQuery(spec.headlineChart);
const supportingCharts = supportingChartSpecs.map(buildChartQuery);
- const graphEntries = [headlineChart, ...supportingCharts]
- .map((chartQuery) => buildSnapshotHashEntry(chartQuery, shouldIncludeComparisons))
- .filter((entry): entry is InsightsGraphHashEntry => !!entry);
+ const graphEntries = [headlineChart, ...supportingCharts].map(buildSnapshotHashEntry).filter((entry): entry is InsightsGraphHashEntry => !!entry);
const insightsHashes: InsightsDashboard['graphs'] = Object.fromEntries(graphEntries);
return {
@@ -173,7 +160,7 @@ function buildInsightsJsonQuery(
inputQuery,
searchKey: spec.searchKey,
insightsHashes,
- numberOfPeriods: shouldIncludeComparisons ? COMPARE_AVERAGE_PERIOD_COUNT : undefined,
+ numberOfPeriods: COMPARE_AVERAGE_PERIOD_COUNT,
}),
hash: queryJSON.hash,
snapshotHashes: graphEntries.flatMap(([, hashes]) => Object.values(hashes).filter((hash): hash is number => hash !== undefined)),
diff --git a/src/pages/Insights/useInsightsChartData.ts b/src/pages/Insights/useInsightsChartData.ts
index b81bd72e4fe2..1acaef81a8bc 100644
--- a/src/pages/Insights/useInsightsChartData.ts
+++ b/src/pages/Insights/useInsightsChartData.ts
@@ -6,7 +6,6 @@ import type {GroupedItem, SearchQueryJSON} from '@components/Search/types';
import useLocalize from '@hooks/useLocalize';
import useNetwork from '@hooks/useNetwork';
-import usePermissions from '@hooks/usePermissions';
import useTheme from '@hooks/useTheme';
import {INSIGHTS_CHART_STATE, resolveInsightsChartData} from '@libs/resolveInsightsChartData';
@@ -63,12 +62,11 @@ function useInsightsChartData(dashboardChart: InsightsDashboardChart, filters: I
const theme = useTheme();
const {translate} = useLocalize();
const {isOffline} = useNetwork();
- const {isBetaEnabled} = usePermissions();
const sortedData = useGroupedItems(snapshot, queryJSON);
const current = resolveInsightsChartData({snapshot, queryJSON, sortedData, isOffline});
- const comparedChart = isBetaEnabled(CONST.BETAS.INSIGHTS_COMPARE) ? getComparedChart(dashboardChart, filters, translate) : undefined;
+ const comparedChart = getComparedChart(dashboardChart, filters, translate);
// Keeps every compared group, so each plotted group finds its own and not only the top ones.
const comparedSortedData = useGroupedItems(comparedChart?.snapshot, queryJSON && {...queryJSON, limit: undefined});
diff --git a/src/pages/home/InsightsSection/InsightsSectionContent.tsx b/src/pages/home/InsightsSection/InsightsSectionContent.tsx
index 66c5ec6ce9cc..2219ae6d96c2 100644
--- a/src/pages/home/InsightsSection/InsightsSectionContent.tsx
+++ b/src/pages/home/InsightsSection/InsightsSectionContent.tsx
@@ -9,7 +9,6 @@ import WidgetHeaderMenu from '@components/WidgetHeaderMenu';
import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset';
import useLocalize from '@hooks/useLocalize';
import useOnyx from '@hooks/useOnyx';
-import usePermissions from '@hooks/usePermissions';
import {setNameValuePair} from '@libs/actions/User';
import {canUseTouchScreen} from '@libs/DeviceCapabilities';
@@ -33,8 +32,6 @@ function InsightsSectionContent() {
const chartPaddingStyle = useInsightChartPaddingStyle();
const {translate} = useLocalize();
const icons = useMemoizedLazyExpensifyIcons(['Expand']);
- const {isBetaEnabled} = usePermissions();
- const isInsightsPageEnabled = isBetaEnabled(CONST.BETAS.INSIGHTS_PAGE);
const {configs: insightConfigs, isResolved: isConfigResolved, shouldShowRunningTotal} = useHomeInsightConfigs();
const [selectedKey, selectedKeyMetadata] = useOnyx(ONYXKEYS.NVP_HOME_SELECTED_INSIGHT);
@@ -54,12 +51,9 @@ function InsightsSectionContent() {
return null;
}
- const navigateToInsights = () =>
- Navigation.navigate(
- isInsightsPageEnabled ? ROUTES.INSIGHTS.getRoute(CONST.INSIGHTS.DASHBOARD.SPEND) : ROUTES.SEARCH_ROOT.getRoute({query: config.searchQuery, searchKey: config.key}),
- );
+ const navigateToInsights = () => Navigation.navigate(ROUTES.INSIGHTS.getRoute(CONST.INSIGHTS.DASHBOARD.SPEND));
- const showRunningTotal = isInsightsPageEnabled && shouldShowRunningTotal && config.key === CONST.INSIGHTS.GRAPH.SPEND_OVER_TIME && view === CONST.SEARCH.VIEW.LINE;
+ const showRunningTotal = shouldShowRunningTotal && config.key === CONST.INSIGHTS.GRAPH.SPEND_OVER_TIME && view === CONST.SEARCH.VIEW.LINE;
const widget = (
)}
);
- if (!isInsightsPageEnabled || canUseTouchScreen()) {
+ if (canUseTouchScreen()) {
return widget;
}
diff --git a/src/pages/home/InsightsSection/index.tsx b/src/pages/home/InsightsSection/index.tsx
index 1e3660a9243e..c5ee8150da74 100644
--- a/src/pages/home/InsightsSection/index.tsx
+++ b/src/pages/home/InsightsSection/index.tsx
@@ -1,11 +1,7 @@
-import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails';
import useOnyx from '@hooks/useOnyx';
-import usePermissions from '@hooks/usePermissions';
import {isGroupPolicy} from '@libs/PolicyUtils';
-import {isPolicyEligibleForSpendOverTime} from '@libs/SearchUIUtils';
-import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import React from 'react';
@@ -13,11 +9,8 @@ import React from 'react';
import InsightsSectionContent from './InsightsSectionContent';
function InsightsSection() {
- const {login} = useCurrentUserPersonalDetails();
- const {isBetaEnabled} = usePermissions();
- const isInsightsPageEnabled = isBetaEnabled(CONST.BETAS.INSIGHTS_PAGE);
const [shouldShowInsights] = useOnyx(ONYXKEYS.COLLECTION.POLICY, {
- selector: (policies) => Object.values(policies ?? {}).some((policy) => !!policy && (isInsightsPageEnabled ? isGroupPolicy(policy) : isPolicyEligibleForSpendOverTime(policy, login))),
+ selector: (policies) => Object.values(policies ?? {}).some((policy) => !!policy && isGroupPolicy(policy)),
});
if (!shouldShowInsights) {
diff --git a/src/pages/home/InsightsSection/useHomeInsightConfigs.ts b/src/pages/home/InsightsSection/useHomeInsightConfigs.ts
index 8855c15d0920..7455c7b3d471 100644
--- a/src/pages/home/InsightsSection/useHomeInsightConfigs.ts
+++ b/src/pages/home/InsightsSection/useHomeInsightConfigs.ts
@@ -1,10 +1,7 @@
-import useCardFeedsForDisplay from '@hooks/useCardFeedsForDisplay';
import useOnyx from '@hooks/useOnyx';
-import usePermissions from '@hooks/usePermissions';
import {buildSearchQueryJSON} from '@libs/SearchQueryUtils';
import type {SearchTypeMenuItem} from '@libs/SearchUIUtils';
-import {getSuggestedSearches, getSuggestedSearchesVisibility, SPEND_INSIGHT_KEYS} from '@libs/SearchUIUtils';
import type {InsightsChartSpec} from '@pages/Insights/dashboardSpecs';
import INSIGHTS_DASHBOARD_SPECS, {getVisibleCharts} from '@pages/Insights/dashboardSpecs';
@@ -18,9 +15,6 @@ import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import type {InsightsGraphKey} from '@src/types/onyx';
-import {defaultExpensifyCardSelector} from '@selectors/Card';
-import {isTrackIntentUserSelector} from '@selectors/Onboarding';
-
// Home only shows Spend charts, which also exist as Search menu items
type HomeInsightGraphKey = Exclude;
@@ -58,23 +52,13 @@ function buildInsightConfigFromChart(chart: HomeInsightChartSpec, filters: Insig
/**
* Builds the configs for the Home insights the current user should see, in display order.
- * With the Insights page beta, the charts and their visibility match the Insights Spend dashboard, otherwise the Spend menu.
+ * The charts and their visibility match the Insights Spend dashboard.
*/
function useHomeInsightConfigs(): {configs: SearchTypeMenuItem[]; isResolved: boolean; shouldShowRunningTotal: boolean} {
const [session] = useOnyx(ONYXKEYS.SESSION);
const [policies] = useOnyx(ONYXKEYS.COLLECTION.POLICY);
- const [defaultExpensifyCard] = useOnyx(ONYXKEYS.DERIVED.NON_PERSONAL_AND_WORKSPACE_CARD_LIST, {selector: defaultExpensifyCardSelector});
- const [isTrackIntentUser] = useOnyx(ONYXKEYS.NVP_INTRO_SELECTED, {selector: isTrackIntentUserSelector});
- const {defaultCardFeed, cardFeedsByPolicy} = useCardFeedsForDisplay();
- const {isBetaEnabled} = usePermissions();
const {filters, isResolved: areFiltersResolved} = useInsightsFilters(CONST.INSIGHTS.DASHBOARD.SPEND);
- if (!isBetaEnabled(CONST.BETAS.INSIGHTS_PAGE)) {
- const {visibility, shouldShowExpensifyCard} = getSuggestedSearchesVisibility(session?.email, cardFeedsByPolicy, policies, defaultExpensifyCard, false, !!isTrackIntentUser);
- const suggestedSearches = getSuggestedSearches(session?.accountID, (defaultCardFeed ?? defaultExpensifyCard)?.id, shouldShowExpensifyCard);
- return {configs: SPEND_INSIGHT_KEYS.filter((key) => visibility[key]).map((key) => suggestedSearches[key]), isResolved: true, shouldShowRunningTotal: false};
- }
-
const {headlineChart, supportingCharts} = INSIGHTS_DASHBOARD_SPECS[CONST.INSIGHTS.DASHBOARD.SPEND];
const configs = getVisibleCharts([headlineChart, ...supportingCharts], policies, filters.policyIDs, session?.email)
.filter(isHomeInsightChart)
diff --git a/src/styles/variables.ts b/src/styles/variables.ts
index dc2fa3e61aae..00d0679b1f4b 100644
--- a/src/styles/variables.ts
+++ b/src/styles/variables.ts
@@ -116,26 +116,16 @@ export default {
floatingTabBarGlyphBottomInset: 1,
/** Glyph size in the iOS native tab bar. */
iconNativeTabBarIOS: 22,
- /** Account avatar size in the iOS native tab bar. */
- avatarNativeTabBarIOS: 26,
+ /** Icon box size that preserves glyph and label spacing in the iOS native tab bar. */
+ nativeTabIconBoxIOS: 26,
/** Glyph size in the iOS native tab bar in landscape, where UIKit lays the label beside a smaller icon. */
iconNativeTabBarIOSLandscape: 18,
- /** Account avatar size in the iOS native tab bar in landscape. */
- avatarNativeTabBarIOSLandscape: 21,
+ /** Icon box size in the iOS native tab bar in landscape. */
+ nativeTabIconBoxIOSLandscape: 21,
/** Radius of the status dot drawn into a native tab bar icon. */
nativeTabIconDotRadius: 4,
/** Width of the ring cut out of a native tab bar icon around its status dot, as the JS tab bar's dot border does. */
nativeTabIconDotCutout: 2,
- /** Radius of the emoji status badge drawn into the account icon of a native tab bar. */
- nativeTabIconEmojiStatusRadius: 6,
- /** Size of the emoji drawn in that badge. */
- nativeTabIconEmojiStatusFontSize: 8,
- /** Radius of that badge on the smaller landscape avatar of the iOS native tab bar, where a full-size badge would run into the status dot. */
- nativeTabIconEmojiStatusRadiusLandscape: 5,
- /** Size of the emoji drawn in that landscape badge. */
- nativeTabIconEmojiStatusFontSizeLandscape: 7,
- /** How far the emoji status badge of an iOS native tab bar icon sits past the bottom-right corner of the avatar, as the JS badge hangs off its avatar. */
- nativeTabIconEmojiStatusOffset: 2,
/** Gap between the glyph and the label drawn under it, or beside it in landscape, in an iOS native tab bar icon. */
nativeTabIconLabelGap: 6,
/** Space between the screen edge and the first tab of the iOS 26 floating tab bar. */
diff --git a/tests/unit/SearchRouterNavigationTest.ts b/tests/unit/SearchRouterNavigationTest.ts
index 06b991ecb778..32131d112b83 100644
--- a/tests/unit/SearchRouterNavigationTest.ts
+++ b/tests/unit/SearchRouterNavigationTest.ts
@@ -408,45 +408,11 @@ describe('top-level Search Router navigation source', () => {
Globe: mockIcon,
Gear: mockIcon,
},
- isInsightsPageBetaEnabled: false,
getSpendRoute: () => ROUTES.SEARCH_ROOT.getRoute({query: 'type:expense'}),
getDestinationText: (destination) => `Go to ${destination}`,
});
- expect(items.map((item) => item.text)).toEqual(['Go to Home', 'Go to Inbox', 'Go to Spend', 'Go to Workspaces', 'Go to Domains', 'Go to Account']);
- expect(items.map((item) => item.keyForList)).toEqual(['topLevelHome', 'topLevelInbox', 'topLevelSpend', 'topLevelWorkspaces', 'topLevelDomains', 'topLevelAccount']);
- });
-
- it('adds the Insights destination when the beta is enabled', () => {
- // Given a user with the Insights beta
- const isInsightsPageBetaEnabled = true;
-
- // When the top-level destinations are built
- const items = buildTopLevelNavigationItems({
- labels: {
- home: 'Home',
- inbox: 'Inbox',
- spend: 'Spend',
- insights: 'Insights',
- workspaces: 'Workspaces',
- domains: 'Domains',
- account: 'Account',
- },
- icons: {
- Home: mockIcon,
- Inbox: mockIcon,
- ReceiptMultiple: mockIcon,
- PieChart: mockIcon,
- Building: mockIcon,
- Globe: mockIcon,
- Gear: mockIcon,
- },
- isInsightsPageBetaEnabled,
- getSpendRoute: () => ROUTES.SEARCH_ROOT.getRoute({query: 'type:expense'}),
- getDestinationText: (destination) => `Go to ${destination}`,
- });
-
- // Then the Insights row follows Spend and navigates to the Spend dashboard
+ expect(items.map((item) => item.text)).toEqual(['Go to Home', 'Go to Inbox', 'Go to Spend', 'Go to Insights', 'Go to Workspaces', 'Go to Domains', 'Go to Account']);
expect(items.map((item) => item.keyForList)).toEqual([
'topLevelHome',
'topLevelInbox',
@@ -456,8 +422,6 @@ describe('top-level Search Router navigation source', () => {
'topLevelDomains',
'topLevelAccount',
]);
- items.at(3)?.action?.();
- expect(Navigation.navigate).toHaveBeenCalledWith(ROUTES.INSIGHTS.getRoute(CONST.INSIGHTS.DASHBOARD.SPEND));
});
it('navigates each top-level row to its intended route', () => {
@@ -482,7 +446,6 @@ describe('top-level Search Router navigation source', () => {
Globe: mockIcon,
Gear: mockIcon,
},
- isInsightsPageBetaEnabled: false,
getSpendRoute,
getDestinationText: (destination) => `Go to ${destination}`,
});
@@ -495,9 +458,10 @@ describe('top-level Search Router navigation source', () => {
expect(Navigation.navigate).toHaveBeenNthCalledWith(2, ROUTES.INBOX);
expect(Navigation.navigate).toHaveBeenNthCalledWith(3, spendRoute);
expect(getSpendRoute).toHaveBeenCalledTimes(1);
- expect(Navigation.navigate).toHaveBeenNthCalledWith(4, ROUTES.WORKSPACES_LIST.route);
- expect(Navigation.navigate).toHaveBeenNthCalledWith(5, ROUTES.DOMAINS_LIST.route);
- expect(Navigation.navigate).toHaveBeenNthCalledWith(6, ROUTES.SETTINGS);
+ expect(Navigation.navigate).toHaveBeenNthCalledWith(4, ROUTES.INSIGHTS.getRoute(CONST.INSIGHTS.DASHBOARD.SPEND));
+ expect(Navigation.navigate).toHaveBeenNthCalledWith(5, ROUTES.WORKSPACES_LIST.route);
+ expect(Navigation.navigate).toHaveBeenNthCalledWith(6, ROUTES.DOMAINS_LIST.route);
+ expect(Navigation.navigate).toHaveBeenNthCalledWith(7, ROUTES.SETTINGS);
});
});
diff --git a/tests/unit/insightsQueriesTest.ts b/tests/unit/insightsQueriesTest.ts
index dc0346990291..a6221236aeda 100644
--- a/tests/unit/insightsQueriesTest.ts
+++ b/tests/unit/insightsQueriesTest.ts
@@ -57,7 +57,7 @@ describe('insightsQueries', () => {
const filters: InsightsFilters = {...FILTERS, groupBy: CONST.SEARCH.GROUP_BY.MONTH};
// When its request is built
- const request = buildInsightsJsonQuery(CONST.INSIGHTS.DASHBOARD.SPEND, filters, SPEND_SPEC.supportingCharts, true);
+ const request = buildInsightsJsonQuery(CONST.INSIGHTS.DASHBOARD.SPEND, filters, SPEND_SPEC.supportingCharts);
// Then it is addressed to the spend dashboard and names the snapshot each chart's data belongs in
const payload = parsePayload(request);
@@ -75,11 +75,11 @@ describe('insightsQueries', () => {
it('requests Compliance by submission or approval dates without sharing Spend snapshots', () => {
// Given Spend and Compliance using the same year-to-date picker selections
- const spend = buildInsightsJsonQuery(CONST.INSIGHTS.DASHBOARD.SPEND, FILTERS, [], true);
+ const spend = buildInsightsJsonQuery(CONST.INSIGHTS.DASHBOARD.SPEND, FILTERS, []);
// When Compliance requests its current, previous, and average periods
- const request = buildInsightsJsonQuery(CONST.INSIGHTS.DASHBOARD.COMPLIANCE, FILTERS, [], true);
- const compared = buildInsightsJsonQuery(CONST.INSIGHTS.DASHBOARD.COMPLIANCE, {...FILTERS, compare: CONST.SEARCH.COMPARE.PREVIOUS_PERIOD}, [], true);
+ const request = buildInsightsJsonQuery(CONST.INSIGHTS.DASHBOARD.COMPLIANCE, FILTERS, []);
+ const compared = buildInsightsJsonQuery(CONST.INSIGHTS.DASHBOARD.COMPLIANCE, {...FILTERS, compare: CONST.SEARCH.COMPARE.PREVIOUS_PERIOD}, []);
// Then both action stages use the same bounded period and all snapshots belong to the Compliance graph
expect(parsePayload(request)).toMatchObject({
@@ -115,13 +115,13 @@ describe('insightsQueries', () => {
});
it('keeps each Compliance action range bounded and workspace filters outside the OR', () => {
- // Given a custom Compliance period scoped to one workspace, without comparisons
+ // Given a custom Compliance period scoped to one workspace
const filters: InsightsFilters = {...FILTERS, policyIDs: ['A1'], date: {from: '2026-01-01', to: '2026-03-31'}};
// When its request is built
- const request = buildInsightsJsonQuery(CONST.INSIGHTS.DASHBOARD.COMPLIANCE, filters, [], false);
+ const request = buildInsightsJsonQuery(CONST.INSIGHTS.DASHBOARD.COMPLIANCE, filters, []);
- // Then either stage must satisfy both bounds, with no expense-date constraint or comparison snapshots
+ // Then either stage must satisfy both bounds, with no expense-date constraint and all comparison snapshots
expect(parsePayload(request)).toMatchObject({
filters: {
operator: 'and',
@@ -144,10 +144,20 @@ describe('insightsQueries', () => {
},
},
},
- insightsHashes: {violationsOverTime: {snapshotHash: request?.headlineChart.queryJSON?.hash}},
+ insightsHashes: {
+ violationsOverTime: {
+ snapshotHash: request?.headlineChart.queryJSON?.hash,
+ previousPeriodSnapshotHash: request?.headlineChart.previousPeriodQueryJSON?.hash,
+ averageSnapshotHash: request?.headlineChart.averageQueryJSON?.hash,
+ },
+ },
+ numberOfPeriods: 1,
});
- expect(parsePayload(request)).not.toHaveProperty('numberOfPeriods');
- expect(request?.snapshotHashes).toEqual([request?.headlineChart.queryJSON?.hash]);
+ expect(request?.snapshotHashes).toEqual([
+ request?.headlineChart.queryJSON?.hash,
+ request?.headlineChart.previousPeriodQueryJSON?.hash,
+ request?.headlineChart.averageQueryJSON?.hash,
+ ]);
});
it('uses a single selected day for both Compliance action stages', () => {
@@ -155,7 +165,7 @@ describe('insightsQueries', () => {
const filters: InsightsFilters = {...FILTERS, date: {on: '2026-03-04'}};
// When its request is built
- const request = buildInsightsJsonQuery(CONST.INSIGHTS.DASHBOARD.COMPLIANCE, filters, [], false);
+ const request = buildInsightsJsonQuery(CONST.INSIGHTS.DASHBOARD.COMPLIANCE, filters, []);
// Then both stages match that day and the headline uses daily buckets
expect(parsePayload(request)).toMatchObject({
@@ -172,11 +182,11 @@ describe('insightsQueries', () => {
it('requests the same data whether or not the dashboard is compared, saving the Compare mode along', () => {
// Given the spend dashboard shown on its own
- const standalone = buildInsightsJsonQuery(CONST.INSIGHTS.DASHBOARD.SPEND, FILTERS, SPEND_SPEC.supportingCharts, true);
+ const standalone = buildInsightsJsonQuery(CONST.INSIGHTS.DASHBOARD.SPEND, FILTERS, SPEND_SPEC.supportingCharts);
// When it is compared against the previous period
const filters: InsightsFilters = {...FILTERS, compare: CONST.SEARCH.COMPARE.PREVIOUS_PERIOD};
- const compared = buildInsightsJsonQuery(CONST.INSIGHTS.DASHBOARD.SPEND, filters, SPEND_SPEC.supportingCharts, true);
+ const compared = buildInsightsJsonQuery(CONST.INSIGHTS.DASHBOARD.SPEND, filters, SPEND_SPEC.supportingCharts);
// Then every response carries all comparisons, so switching one never fetches again or stores the response apart
expect(compared?.hash).toBe(standalone?.hash);
@@ -187,29 +197,12 @@ describe('insightsQueries', () => {
expect(parsePayload(compared)?.inputQuery).toContain(`compare:${CONST.SEARCH.COMPARE.PREVIOUS_PERIOD}`);
});
- it('requests only the current period for users without the compare beta', () => {
- // Given the spend dashboard for a user who can't compare it
- const filters: InsightsFilters = {...FILTERS, groupBy: CONST.SEARCH.GROUP_BY.MONTH};
-
- // When its request is built without comparisons
- const request = buildInsightsJsonQuery(CONST.INSIGHTS.DASHBOARD.SPEND, filters, SPEND_SPEC.supportingCharts, false);
-
- // Then each chart names only its own snapshot, so the backend doesn't compute periods nobody sees
- const payload = parsePayload(request);
- const expectedHashes = Object.fromEntries(
- [SPEND_SPEC.headlineChart, ...SPEND_SPEC.supportingCharts].map((chart) => [chart.graphKey, {snapshotHash: buildSearchQueryJSON(applyInsightsFilters(chart, filters))?.hash}]),
- );
- expect(payload).toMatchObject({insightsHashes: expectedHashes});
- expect(payload).not.toHaveProperty('numberOfPeriods');
- expect(request?.snapshotHashes).toEqual(Object.values(expectedHashes).map(({snapshotHash}) => snapshotHash));
- });
-
it('requests only the supporting charts shown on the dashboard', () => {
// Given the spend dashboard with only its first supporting chart shown, as when no workspace in scope is eligible for the rest
const [shownChart, ...hiddenCharts] = SPEND_SPEC.supportingCharts;
// When its request is built
- const request = buildInsightsJsonQuery(CONST.INSIGHTS.DASHBOARD.SPEND, FILTERS, [shownChart], true);
+ const request = buildInsightsJsonQuery(CONST.INSIGHTS.DASHBOARD.SPEND, FILTERS, [shownChart]);
// Then the hidden charts are left out, so the backend doesn't compute graphs nobody sees
const hiddenChartHashes = hiddenCharts.map((chart) => buildSearchQueryJSON(applyInsightsFilters(chart, FILTERS))?.hash);
@@ -222,10 +215,10 @@ describe('insightsQueries', () => {
it('hashes every set of filters on its own', () => {
// Given the spend dashboard grouped by month
- const monthly = buildInsightsJsonQuery(CONST.INSIGHTS.DASHBOARD.SPEND, FILTERS, SPEND_SPEC.supportingCharts, true);
+ const monthly = buildInsightsJsonQuery(CONST.INSIGHTS.DASHBOARD.SPEND, FILTERS, SPEND_SPEC.supportingCharts);
// When the same dashboard is grouped by quarter instead
- const quarterly = buildInsightsJsonQuery(CONST.INSIGHTS.DASHBOARD.SPEND, {...FILTERS, groupBy: CONST.SEARCH.GROUP_BY.QUARTER}, SPEND_SPEC.supportingCharts, true);
+ const quarterly = buildInsightsJsonQuery(CONST.INSIGHTS.DASHBOARD.SPEND, {...FILTERS, groupBy: CONST.SEARCH.GROUP_BY.QUARTER}, SPEND_SPEC.supportingCharts);
// Then each request carries the hash of the query it asks for, so the two responses are stored apart
expect(monthly?.hash).toBe(parsePayload(monthly)?.hash);
@@ -237,7 +230,7 @@ describe('insightsQueries', () => {
const filters: InsightsFilters = {...FILTERS, policyIDs: ['A1'], groupBy: CONST.SEARCH.GROUP_BY.MONTH, groupCurrency: 'USD'};
// When its request is built
- const request = buildInsightsJsonQuery(CONST.INSIGHTS.DASHBOARD.SPEND, filters, SPEND_SPEC.supportingCharts, true);
+ const request = buildInsightsJsonQuery(CONST.INSIGHTS.DASHBOARD.SPEND, filters, SPEND_SPEC.supportingCharts);
// Then the query carries those filters and nothing else, such as the sorting of a table no chart renders
expect(parsePayload(request)?.inputQuery).toBe('groupBy:month groupCurrency:USD policyID:A1 date:year-to-date');
@@ -251,7 +244,7 @@ describe('insightsQueries', () => {
};
// When its request is built
- const request = buildInsightsJsonQuery(CONST.INSIGHTS.DASHBOARD.SPEND, filters, SPEND_SPEC.supportingCharts, true);
+ const request = buildInsightsJsonQuery(CONST.INSIGHTS.DASHBOARD.SPEND, filters, SPEND_SPEC.supportingCharts);
// Then the query is bounded by the range, both ends included, and the charts ask for snapshots of it
expect(parsePayload(request)?.inputQuery).toBe('groupBy:month groupCurrency:USD date>=2026-01-01 date<=2026-03-31');
@@ -263,7 +256,7 @@ describe('insightsQueries', () => {
const filters: InsightsFilters = {...FILTERS, date: {on: '2026-03-04'}};
// When its request is built
- const request = buildInsightsJsonQuery(CONST.INSIGHTS.DASHBOARD.SPEND, filters, SPEND_SPEC.supportingCharts, true);
+ const request = buildInsightsJsonQuery(CONST.INSIGHTS.DASHBOARD.SPEND, filters, SPEND_SPEC.supportingCharts);
// Then the query names that day the same way it names a preset, and its one point is grouped by day, the only grouping a single date offers
expect(parsePayload(request)?.inputQuery).toBe('groupBy:month groupCurrency:USD date:2026-03-04');
@@ -276,7 +269,7 @@ describe('insightsQueries', () => {
const filters: InsightsFilters = {...FILTERS, date: {from: '2026-03-01', to: '2026-03-31'}, groupBy: CONST.SEARCH.GROUP_BY.QUARTER};
// When its request is built
- const payload = parsePayload(buildInsightsJsonQuery(CONST.INSIGHTS.DASHBOARD.SPEND, filters, SPEND_SPEC.supportingCharts, true));
+ const payload = parsePayload(buildInsightsJsonQuery(CONST.INSIGHTS.DASHBOARD.SPEND, filters, SPEND_SPEC.supportingCharts));
// Then the backend saves quarter, so a longer range brings it back, while the charts are grouped by week
expect(payload?.inputQuery).toBe('groupBy:quarter groupCurrency:USD date>=2026-03-01 date<=2026-03-31');
@@ -285,11 +278,11 @@ describe('insightsQueries', () => {
it('leaves the workspaces out of the query until some are selected', () => {
// Given a dashboard requested with no workspace selected
- const allWorkspaces = buildInsightsJsonQuery(CONST.INSIGHTS.DASHBOARD.SPEND, FILTERS, SPEND_SPEC.supportingCharts, true);
+ const allWorkspaces = buildInsightsJsonQuery(CONST.INSIGHTS.DASHBOARD.SPEND, FILTERS, SPEND_SPEC.supportingCharts);
// When two workspaces are selected
const filters: InsightsFilters = {...FILTERS, policyIDs: ['A1', 'B2']};
- const twoWorkspaces = buildInsightsJsonQuery(CONST.INSIGHTS.DASHBOARD.SPEND, filters, SPEND_SPEC.supportingCharts, true);
+ const twoWorkspaces = buildInsightsJsonQuery(CONST.INSIGHTS.DASHBOARD.SPEND, filters, SPEND_SPEC.supportingCharts);
// Then only the second query names them, and its charts point at different snapshots
expect(parsePayload(allWorkspaces)?.inputQuery).not.toContain('policyID');
@@ -307,7 +300,7 @@ describe('insightsQueries', () => {
const queryString = buildInsightsQueryString(filters);
// Then it is the very query the request carries
- expect(queryString).toBe(parsePayload(buildInsightsJsonQuery(CONST.INSIGHTS.DASHBOARD.SPEND, filters, SPEND_SPEC.supportingCharts, true))?.inputQuery);
+ expect(queryString).toBe(parsePayload(buildInsightsJsonQuery(CONST.INSIGHTS.DASHBOARD.SPEND, filters, SPEND_SPEC.supportingCharts))?.inputQuery);
});
});
diff --git a/tests/unit/useNativeTabBarOptionsTest.ts b/tests/unit/useNativeTabBarOptionsTest.ts
index b39ceb629279..30f2bb647464 100644
--- a/tests/unit/useNativeTabBarOptionsTest.ts
+++ b/tests/unit/useNativeTabBarOptionsTest.ts
@@ -1,167 +1,66 @@
-import {act, renderHook} from '@testing-library/react-native';
+import {renderHook} from '@testing-library/react-native';
+
+import useTheme from '@hooks/useTheme';
import getTabBarIcon from '@libs/Navigation/AppNavigator/Navigators/NativeTabNavigator/tabIconRasterizer';
import useAndroidNativeTabBarOptions from '@libs/Navigation/AppNavigator/Navigators/NativeTabNavigator/useNativeTabBarOptions/index.android';
import useIOSNativeTabBarOptions from '@libs/Navigation/AppNavigator/Navigators/NativeTabNavigator/useNativeTabBarOptions/index.ios';
import type NativeTabBarOptionsParams from '@libs/Navigation/AppNavigator/Navigators/NativeTabNavigator/useNativeTabBarOptions/types';
-import variables from '@styles/variables';
-
-import CONST from '@src/CONST';
import NAVIGATORS from '@src/NAVIGATORS';
-import ONYXKEYS from '@src/ONYXKEYS';
-
-import Onyx from 'react-native-onyx';
-
-import waitForBatchedUpdatesWithAct from '../utils/waitForBatchedUpdatesWithAct';
-
-let mockEmojiCode: string | undefined;
-
-jest.mock('@hooks/useCurrentUserPersonalDetails', () => ({
- __esModule: true,
- default: () => ({accountID: 1, status: mockEmojiCode ? {emojiCode: mockEmojiCode} : undefined}),
-}));
-
-const mockFontManager = {registerFont: jest.fn()};
-
-// The global mock leaves out Skia's imperative API, which loads the custom emoji font.
-jest.mock('@shopify/react-native-skia', () => ({
- Skia: {
- Data: {fromURI: jest.fn(() => Promise.resolve({dispose: jest.fn()}))},
- Typeface: {MakeFreeTypeFaceFromData: jest.fn(() => ({}))},
- TypefaceFontProvider: {Make: jest.fn(() => mockFontManager)},
- },
-}));
+import SCREENS from '@src/SCREENS';
const mockGetTabBarIcon = jest.mocked(getTabBarIcon);
const PARAMS: NativeTabBarOptionsParams = {
shouldShowNativeTabBar: true,
- isAccountAvatarShown: true,
- dotColors: {},
+ dotColors: {[NAVIGATORS.REPORTS_SPLIT_NAVIGATOR]: '#ff0000'},
tabLabels: {
+ [SCREENS.HOME]: 'Home',
[NAVIGATORS.REPORTS_SPLIT_NAVIGATOR]: 'Inbox',
- [NAVIGATORS.SETTINGS_SPLIT_NAVIGATOR]: 'Account',
+ [NAVIGATORS.SEARCH_FULLSCREEN_NAVIGATOR]: 'Spend',
+ [SCREENS.INSIGHTS]: 'Insights',
+ [NAVIGATORS.WORKSPACE_NAVIGATOR]: 'Workspaces',
},
};
-/** The emoji status each selection state of a tab's icon is drawn with. */
-function getDrawnEmojiStatuses() {
- const getParams = mockGetTabBarIcon.mock.calls.at(-1)?.at(1);
- if (typeof getParams !== 'function') {
- return [];
- }
- return [getParams(false).emojiStatus, getParams(true).emojiStatus];
-}
-
-/** The emoji each selection state of a tab's icon is drawn with. */
-function getDrawnEmojis(): Array {
- return getDrawnEmojiStatuses().map((emojiStatus) => emojiStatus?.emoji);
-}
-
describe.each([
- ['Android', useAndroidNativeTabBarOptions, 0],
- ['iOS', useIOSNativeTabBarOptions, variables.nativeTabIconEmojiStatusOffset],
-])('%s useNativeTabBarOptions', (_platform, useNativeTabBarOptions, expectedEmojiStatusOffset) => {
- beforeAll(() => {
- Onyx.init({keys: ONYXKEYS});
- });
-
- beforeEach(async () => {
- mockEmojiCode = undefined;
+ ['Android', useAndroidNativeTabBarOptions, 'tabBarLabel'],
+ ['iOS', useIOSNativeTabBarOptions, 'tabBarAccessibilityLabel'],
+] as const)('%s useNativeTabBarOptions', (_platform, useNativeTabBarOptions, labelOption) => {
+ beforeEach(() => {
mockGetTabBarIcon.mockClear();
- await Onyx.clear();
- await waitForBatchedUpdatesWithAct();
- });
-
- it('draws the status emoji on the account tab in both selection states', () => {
- // Given a user with a status emoji
- mockEmojiCode = '🌴';
- const {result} = renderHook(() => useNativeTabBarOptions(PARAMS));
-
- // When the account tab's icon is drawn
- result.current.getTabOptions(NAVIGATORS.SETTINGS_SPLIT_NAVIGATOR);
-
- // Then the emoji is badged on it whether or not the tab is selected, as the JS tab bar does
- expect(getDrawnEmojis()).toEqual(['🌴', '🌴']);
- });
-
- it('leaves the status emoji off every other tab', () => {
- // Given a user with a status emoji
- mockEmojiCode = '🌴';
- const {result} = renderHook(() => useNativeTabBarOptions(PARAMS));
-
- // When another tab's icon is drawn
- result.current.getTabOptions(NAVIGATORS.REPORTS_SPLIT_NAVIGATOR);
-
- // Then it has no badge, because the status belongs to the account avatar
- expect(getDrawnEmojis()).toEqual([undefined, undefined]);
- });
-
- it('draws no badge once the status is cleared', () => {
- // Given a user whose status emoji was set and then cleared
- mockEmojiCode = '🌴';
- const {result, rerender} = renderHook(() => useNativeTabBarOptions(PARAMS));
- mockEmojiCode = undefined;
- rerender({});
-
- // When the account tab's icon is drawn again
- result.current.getTabOptions(NAVIGATORS.SETTINGS_SPLIT_NAVIGATOR);
-
- // Then the badge is gone, so the icon goes back to the plain avatar
- expect(getDrawnEmojis()).toEqual([undefined, undefined]);
- });
-
- it('draws no badge while acting as a delegate', async () => {
- // Given a user with a status emoji who is acting as a delegate
- mockEmojiCode = '🌴';
- await Onyx.merge(ONYXKEYS.ACCOUNT, {delegatedAccess: {delegate: 'delegate@example.com'}});
- const {result} = renderHook(() => useNativeTabBarOptions(PARAMS));
- await waitForBatchedUpdatesWithAct();
-
- // When the account tab's icon is drawn
- result.current.getTabOptions(NAVIGATORS.SETTINGS_SPLIT_NAVIGATOR);
-
- // Then it has no badge, because the JS tab bar shows the delegate avatar there instead of the status
- expect(getDrawnEmojis()).toEqual([undefined, undefined]);
- });
-
- it('draws a custom emoji with the custom emoji font once it loads', async () => {
- // Given a user whose status is a custom emoji, which the system fonts have no glyph for
- mockEmojiCode = CONST.CUSTOM_EMOJIS.GLOBAL_CREATE;
- const {result} = renderHook(() => useNativeTabBarOptions(PARAMS));
-
- // When the account tab's icon is drawn after the font has loaded
- await act(async () => {
- await Promise.resolve();
- });
- result.current.getTabOptions(NAVIGATORS.SETTINGS_SPLIT_NAVIGATOR);
-
- // Then the badge is drawn with the custom emoji font, so it does not show a blank box
- expect(getDrawnEmojiStatuses().map((emojiStatus) => emojiStatus?.fontManager)).toEqual([mockFontManager, mockFontManager]);
});
- it('draws a standard emoji with the system fonts', () => {
- // Given a user whose status is a standard emoji
- mockEmojiCode = '🌴';
- const {result} = renderHook(() => useNativeTabBarOptions(PARAMS));
-
- // When the account tab's icon is drawn
- result.current.getTabOptions(NAVIGATORS.SETTINGS_SPLIT_NAVIGATOR);
-
- // Then no custom font is given, so the system's color emoji font draws it
- expect(getDrawnEmojiStatuses().map((emojiStatus) => emojiStatus?.fontManager)).toEqual([undefined, undefined]);
+ it('draws glyphs and status dots in both selection states without account badges', () => {
+ // Given an Inbox tab with a status dot
+ const {result} = renderHook(() => ({options: useNativeTabBarOptions(PARAMS), theme: useTheme()}));
+
+ // When its icon is drawn
+ result.current.options.getTabOptions(NAVIGATORS.REPORTS_SPLIT_NAVIGATOR);
+ const getParams = mockGetTabBarIcon.mock.calls.at(-1)?.[1];
+
+ // Then each state keeps its glyph color and status dot, without avatar or emoji data
+ for (const isSelected of [false, true]) {
+ const params = getParams?.(isSelected);
+ expect(params).toMatchObject({
+ name: NAVIGATORS.REPORTS_SPLIT_NAVIGATOR,
+ color: isSelected ? result.current.theme.iconMenu : result.current.theme.icon,
+ dotColor: PARAMS.dotColors[NAVIGATORS.REPORTS_SPLIT_NAVIGATOR],
+ });
+ expect(params).not.toHaveProperty('avatar');
+ expect(params).not.toHaveProperty('emojiStatus');
+ }
});
- it('hangs the badge past the avatar only where the icon can grow', () => {
- // Given a user with a status emoji
- mockEmojiCode = '🌴';
+ it('keeps the tab label accessible', () => {
+ // Given the five visible native tabs
const {result} = renderHook(() => useNativeTabBarOptions(PARAMS));
- // When the account tab's icon is drawn
- result.current.getTabOptions(NAVIGATORS.SETTINGS_SPLIT_NAVIGATOR);
+ // When the Inbox tab options are built
+ const options = result.current.getTabOptions(NAVIGATORS.REPORTS_SPLIT_NAVIGATOR);
- // Then iOS offsets the badge past the avatar's corner as the JS badge does, while Android keeps it inside, since Material would shrink a larger icon to fit its slot
- expect(mockGetTabBarIcon.mock.calls.at(-1)?.at(0)).toEqual(expect.objectContaining({emojiStatusOffset: expectedEmojiStatusOffset}));
+ // Then the native label or accessibility label still names the tab
+ expect(options[labelOption]).toBe('Inbox');
});
});