diff --git a/android/app/src/main/java/com/expensify/chat/navbar/NavBarManagerModule.kt b/android/app/src/main/java/com/expensify/chat/navbar/NavBarManagerModule.kt
index 5c566df606eb..dd02d8aba1bd 100644
--- a/android/app/src/main/java/com/expensify/chat/navbar/NavBarManagerModule.kt
+++ b/android/app/src/main/java/com/expensify/chat/navbar/NavBarManagerModule.kt
@@ -1,10 +1,12 @@
package com.expensify.chat.navbar
+import android.content.res.Resources
import androidx.core.view.WindowInsetsControllerCompat
+import com.expensify.chat.R
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.bridge.ReactContextBaseJavaModule
import com.facebook.react.bridge.ReactMethod
-import com.facebook.react.bridge.UiThreadUtil;
+import com.facebook.react.bridge.UiThreadUtil
class NavBarManagerModule(
private val mReactContext: ReactApplicationContext,
@@ -24,4 +26,18 @@ class NavBarManagerModule(
}
}
}
+
+ @ReactMethod
+ fun getType(): String {
+ val resources = mReactContext.resources
+ val resourceId = resources.getIdentifier("config_navBarInteractionMode", "integer", "android");
+ if (resourceId > 0) {
+ val navBarInteractionMode = resources.getInteger(resourceId)
+ when (navBarInteractionMode) {
+ 0, 1 -> return "soft-keys"
+ 2 -> return "gesture-bar"
+ }
+ }
+ return "soft-keys";
+ }
}
diff --git a/contributingGuides/FORMS.md b/contributingGuides/FORMS.md
index 469281d3e86b..be769faf568e 100644
--- a/contributingGuides/FORMS.md
+++ b/contributingGuides/FORMS.md
@@ -68,7 +68,7 @@ Browsers use the name prop to autofill information into the input. Here's a [ref
```jsx
```
@@ -118,7 +118,7 @@ Once a user has “touched” an input, i.e. blurred the input, we will also sta
All form fields will additionally be validated when the form is submitted. Although we are validating on blur this additional step is necessary to cover edge cases where forms are auto-filled or when a form is submitted by pressing enter (i.e. there will be only a ‘submit’ event and no ‘blur’ event to hook into).
-The Form component takes care of validation internally and the only requirement is that we pass a validate callback prop. The validate callback takes in the input values as argument and should return an object with shape `{[inputID]: errorMessage}`.
+The Form component takes care of validation internally and the only requirement is that we pass a validate callback prop. The validate callback takes in the input values as argument and should return an object with shape `{[inputID]: errorMessage}`.
Here's an example for a form that has two inputs, `routingNumber` and `accountNumber`:
@@ -332,10 +332,10 @@ An example of this can be seen in the [ACHContractStep](https://github.com/Expen
### Safe Area Padding
Any `FormProvider.tsx` that has a button at the bottom. If the `` is inside a ``, the bottom safe area inset is handled automatically (`includeSafeAreaPaddingBottom` needs to be set to `true`, but its the default).
-If you have custom requirements and can't use ``, you can use the `useStyledSafeAreaInsets()` hook:
+If you have custom requirements and can't use ``, you can use the `useSafeAreaPaddings()` hook:
```jsx
-const { paddingTop, paddingBottom, safeAreaPaddingBottomStyle } = useStyledSafeAreaInsets();
+const { paddingTop, paddingBottom, safeAreaPaddingBottomStyle } = useSafeAreaPaddings();
diff --git a/src/App.tsx b/src/App.tsx
index 8dd2631a6b7d..3e6fc9ac2a27 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -17,6 +17,7 @@ import InitialURLContextProvider from './components/InitialURLContextProvider';
import {InputBlurContextProvider} from './components/InputBlurContext';
import KeyboardProvider from './components/KeyboardProvider';
import {LocaleContextProvider} from './components/LocaleContextProvider';
+import NavigationBar from './components/NavigationBar';
import OnyxProvider from './components/OnyxProvider';
import PopoverContextProvider from './components/PopoverProvider';
import {ProductTrainingContextProvider} from './components/ProductTrainingContext';
@@ -118,6 +119,7 @@ function App({url, hybridAppSettings, timestamp}: AppProps) {
+
diff --git a/src/CONST.ts b/src/CONST.ts
index b73bc7c33846..a5542620fdf9 100755
--- a/src/CONST.ts
+++ b/src/CONST.ts
@@ -1533,6 +1533,20 @@ const CONST = {
LIGHT: 'light',
DARK: 'dark',
},
+ NAVIGATION_BAR_TYPE: {
+ // We consider there to be no navigation bar in one of these cases:
+ // 1. The device has physical navigation buttons
+ // 2. The device uses gesture navigation without a gesture bar.
+ // 3. The device uses hidden (auto-hiding) soft keys.
+ NONE: 'none',
+ SOFT_KEYS: 'soft-keys',
+ GESTURE_BAR: 'gesture-bar',
+ },
+ // Currently, in Android there is no native API to detect the type of navigation bar (soft keys vs. gesture).
+ // The navigation bar on (standard) Android devices is around 30-50dpi tall. (Samsung: 40dpi, Huawei: ~34dpi)
+ // To leave room to detect soft-key navigation bars on non-standard Android devices,
+ // we set this height threshold to 30dpi, since gesture bars will never be taller than that. (Samsung & Huawei: ~14-15dpi)
+ NAVIGATION_BAR_ANDROID_SOFT_KEYS_MINIMUM_HEIGHT_THRESHOLD: 30,
TRANSACTION: {
DEFAULT_MERCHANT: 'Expense',
UNKNOWN_MERCHANT: 'Unknown Merchant',
diff --git a/src/components/Attachments/AttachmentView/index.tsx b/src/components/Attachments/AttachmentView/index.tsx
index 2c07863ea2e8..93729a8e0da3 100644
--- a/src/components/Attachments/AttachmentView/index.tsx
+++ b/src/components/Attachments/AttachmentView/index.tsx
@@ -14,7 +14,7 @@ import Text from '@components/Text';
import {usePlaybackContext} from '@components/VideoPlayerContexts/PlaybackContext';
import useLocalize from '@hooks/useLocalize';
import useNetwork from '@hooks/useNetwork';
-import useStyledSafeAreaInsets from '@hooks/useStyledSafeAreaInsets';
+import useSafeAreaPaddings from '@hooks/useSafeAreaPaddings';
import useStyleUtils from '@hooks/useStyleUtils';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
@@ -116,7 +116,7 @@ function AttachmentView({
const {updateCurrentlyPlayingURL} = usePlaybackContext();
const theme = useTheme();
- const {safeAreaPaddingBottomStyle} = useStyledSafeAreaInsets();
+ const {safeAreaPaddingBottomStyle} = useSafeAreaPaddings();
const styles = useThemeStyles();
const StyleUtils = useStyleUtils();
const [loadComplete, setLoadComplete] = useState(false);
diff --git a/src/components/AutoCompleteSuggestions/index.tsx b/src/components/AutoCompleteSuggestions/index.tsx
index 1426e000af58..a459cc193b40 100644
--- a/src/components/AutoCompleteSuggestions/index.tsx
+++ b/src/components/AutoCompleteSuggestions/index.tsx
@@ -64,7 +64,7 @@ function AutoCompleteSuggestions({measureParentContainerAndReportCu
const StyleUtils = useStyleUtils();
const insets = useSafeAreaInsets();
const {keyboardHeight, isKeyboardAnimatingRef} = useKeyboardState();
- const {paddingBottom: bottomInset, paddingTop: topInset} = StyleUtils.getSafeAreaPadding(insets ?? undefined);
+ const {paddingBottom: bottomInset, paddingTop: topInset} = StyleUtils.getPlatformSafeAreaPadding(insets ?? undefined);
useEffect(() => {
const container = containerRef.current;
diff --git a/src/components/BlockingViews/BlockingView.tsx b/src/components/BlockingViews/BlockingView.tsx
index 3166b45f0064..6fc43cd9b236 100644
--- a/src/components/BlockingViews/BlockingView.tsx
+++ b/src/components/BlockingViews/BlockingView.tsx
@@ -11,6 +11,7 @@ import Lottie from '@components/Lottie';
import type DotLottieAnimation from '@components/LottieAnimations/types';
import Text from '@components/Text';
import TextLink from '@components/TextLink';
+import useBottomSafeSafeAreaPaddingStyle from '@hooks/useBottomSafeSafeAreaPaddingStyle';
import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
import Navigation from '@libs/Navigation/Navigation';
@@ -47,6 +48,9 @@ type BaseBlockingViewProps = {
/** Additional styles to apply to the container */
containerStyle?: StyleProp;
+
+ /** Whether to add bottom safe area padding to the view. */
+ addBottomSafeAreaPadding?: boolean;
};
type BlockingViewIconProps = {
@@ -94,7 +98,8 @@ function BlockingView({
animationWebStyle = {},
CustomSubtitle,
contentFitImage,
- containerStyle,
+ containerStyle: containerStyleProp,
+ addBottomSafeAreaPadding = false,
}: BlockingViewProps) {
const styles = useThemeStyles();
const {translate} = useLocalize();
@@ -132,6 +137,8 @@ function BlockingView({
);
}, [styles, subtitleText, shouldEmbedLinkWithSubtitle, CustomSubtitle]);
+ const containerStyle = useBottomSafeSafeAreaPaddingStyle({addBottomSafeAreaPadding, style: containerStyleProp});
+
return (
{!!animation && (
diff --git a/src/components/BlockingViews/FullPageNotFoundView.tsx b/src/components/BlockingViews/FullPageNotFoundView.tsx
index d751d8bc666b..b9afe868878c 100644
--- a/src/components/BlockingViews/FullPageNotFoundView.tsx
+++ b/src/components/BlockingViews/FullPageNotFoundView.tsx
@@ -50,6 +50,9 @@ type FullPageNotFoundViewProps = {
/** Whether we should display the button that opens new SearchRouter */
shouldDisplaySearchRouter?: boolean;
+
+ /** Whether to add bottom safe area padding to the view. */
+ addBottomSafeAreaPadding?: boolean;
};
// eslint-disable-next-line rulesdir/no-negated-variables
@@ -67,6 +70,7 @@ function FullPageNotFoundView({
shouldForceFullScreen = false,
subtitleStyle,
shouldDisplaySearchRouter,
+ addBottomSafeAreaPadding = true,
}: FullPageNotFoundViewProps) {
const styles = useThemeStyles();
const {translate} = useLocalize();
@@ -93,6 +97,7 @@ function FullPageNotFoundView({
shouldShowLink={shouldShowLink}
onLinkPress={onLinkPress}
subtitleStyle={subtitleStyle}
+ addBottomSafeAreaPadding={addBottomSafeAreaPadding}
/>
diff --git a/src/components/BlockingViews/FullPageOfflineBlockingView.tsx b/src/components/BlockingViews/FullPageOfflineBlockingView.tsx
index 787752dd4e72..dc5c210b3178 100644
--- a/src/components/BlockingViews/FullPageOfflineBlockingView.tsx
+++ b/src/components/BlockingViews/FullPageOfflineBlockingView.tsx
@@ -6,7 +6,12 @@ import useTheme from '@hooks/useTheme';
import type ChildrenProps from '@src/types/utils/ChildrenProps';
import BlockingView from './BlockingView';
-function FullPageOfflineBlockingView({children}: ChildrenProps) {
+type FullPageOfflineBlockingViewProps = ChildrenProps & {
+ /** Whether to add bottom safe area padding to the view. */
+ addBottomSafeAreaPadding?: boolean;
+};
+
+function FullPageOfflineBlockingView({children, addBottomSafeAreaPadding = true}: FullPageOfflineBlockingViewProps) {
const {translate} = useLocalize();
const {isOffline} = useNetwork();
@@ -19,6 +24,7 @@ function FullPageOfflineBlockingView({children}: ChildrenProps) {
iconColor={theme.offline}
title={translate('common.youAppearToBeOffline')}
subtitle={translate('common.thisFeatureRequiresInternet')}
+ addBottomSafeAreaPadding={addBottomSafeAreaPadding}
/>
);
}
diff --git a/src/components/Button/index.tsx b/src/components/Button/index.tsx
index 5baba39107ac..fe3e28dcf50c 100644
--- a/src/components/Button/index.tsx
+++ b/src/components/Button/index.tsx
@@ -2,7 +2,7 @@ import {useIsFocused} from '@react-navigation/native';
import type {ForwardedRef} from 'react';
import React, {useCallback, useMemo, useState} from 'react';
import type {GestureResponderEvent, LayoutChangeEvent, StyleProp, TextStyle, ViewStyle} from 'react-native';
-import {ActivityIndicator, View} from 'react-native';
+import {ActivityIndicator, StyleSheet, View} from 'react-native';
import Icon from '@components/Icon';
import * as Expensicons from '@components/Icon/Expensicons';
import PressableWithFeedback from '@components/Pressable/PressableWithFeedback';
@@ -152,6 +152,12 @@ type ButtonProps = Partial & {
/** The text displays under the first line */
secondLineText?: string;
+
+ /**
+ * Whether the button should have a background layer in the color of theme.appBG.
+ * This is needed for buttons that allow content to display under them.
+ */
+ shouldBlendOpacity?: boolean;
};
type KeyboardShortcutComponentProps = Pick;
@@ -255,6 +261,7 @@ function Button(
isPressOnEnterActive,
isNested = false,
secondLineText = '',
+ shouldBlendOpacity = false,
...rest
}: ButtonProps,
ref: ForwardedRef,
@@ -356,6 +363,57 @@ function Button(
return textComponent;
};
+ const buttonStyles = useMemo>(
+ () => [
+ styles.button,
+ StyleUtils.getButtonStyleWithIcon(styles, small, medium, large, !!icon, !!(text?.length > 0), shouldShowRightIcon),
+ success ? styles.buttonSuccess : undefined,
+ danger ? styles.buttonDanger : undefined,
+ isDisabled ? styles.buttonOpacityDisabled : undefined,
+ isDisabled && !danger && !success ? styles.buttonDisabled : undefined,
+ shouldRemoveRightBorderRadius ? styles.noRightBorderRadius : undefined,
+ shouldRemoveLeftBorderRadius ? styles.noLeftBorderRadius : undefined,
+ text && shouldShowRightIcon ? styles.alignItemsStretch : undefined,
+ innerStyles,
+ link && styles.bgTransparent,
+ ],
+ [
+ StyleUtils,
+ danger,
+ icon,
+ innerStyles,
+ isDisabled,
+ large,
+ link,
+ medium,
+ shouldRemoveLeftBorderRadius,
+ shouldRemoveRightBorderRadius,
+ shouldShowRightIcon,
+ small,
+ styles,
+ success,
+ text,
+ ],
+ );
+
+ const buttonContainerStyles = useMemo>(
+ () => [buttonStyles, shouldBlendOpacity && styles.buttonBlendContainer],
+ [buttonStyles, shouldBlendOpacity, styles.buttonBlendContainer],
+ );
+
+ const buttonBlendForegroundStyle = useMemo>(() => {
+ if (!shouldBlendOpacity) {
+ return undefined;
+ }
+
+ const {backgroundColor, opacity} = StyleSheet.flatten(buttonStyles);
+
+ return {
+ backgroundColor,
+ opacity,
+ };
+ }, [buttonStyles, shouldBlendOpacity]);
+
return (
<>
{pressOnEnter && (
@@ -402,6 +460,7 @@ function Button(
onPressIn={onPressIn}
onPressOut={onPressOut}
onMouseDown={onMouseDown}
+ shouldBlendOpacity={shouldBlendOpacity}
disabled={isLoading || isDisabled}
wrapperStyle={[
isDisabled ? {...styles.cursorDisabled, ...styles.noSelect} : {},
@@ -410,19 +469,7 @@ function Button(
shouldRemoveLeftBorderRadius ? styles.noLeftBorderRadius : undefined,
style,
]}
- style={[
- styles.button,
- StyleUtils.getButtonStyleWithIcon(styles, small, medium, large, !!icon, !!(text?.length > 0), shouldShowRightIcon),
- success ? styles.buttonSuccess : undefined,
- danger ? styles.buttonDanger : undefined,
- isDisabled ? styles.buttonOpacityDisabled : undefined,
- isDisabled && !danger && !success ? styles.buttonDisabled : undefined,
- shouldRemoveRightBorderRadius ? styles.noRightBorderRadius : undefined,
- shouldRemoveLeftBorderRadius ? styles.noLeftBorderRadius : undefined,
- text && shouldShowRightIcon ? styles.alignItemsStretch : undefined,
- innerStyles,
- link && styles.bgTransparent,
- ]}
+ style={buttonContainerStyles}
isNested={isNested}
hoverStyle={[
shouldUseDefaultHover && !isDisabled ? styles.buttonDefaultHovered : undefined,
@@ -439,6 +486,7 @@ function Button(
onHoverIn={() => setIsHovered(true)}
onHoverOut={() => setIsHovered(false)}
>
+ {shouldBlendOpacity && }
{renderContent()}
{isLoading && (
> & {
/** Label for the search text input */
searchInputLabel: string;
diff --git a/src/components/EmptyStateComponent/index.tsx b/src/components/EmptyStateComponent/index.tsx
index 058b2e564f89..d3a715ad8748 100644
--- a/src/components/EmptyStateComponent/index.tsx
+++ b/src/components/EmptyStateComponent/index.tsx
@@ -29,6 +29,7 @@ function EmptyStateComponent({
lottieWebViewStyles,
showsVerticalScrollIndicator,
minModalHeight = 400,
+ addBottomSafeAreaPadding = false,
}: EmptyStateComponentProps) {
const styles = useThemeStyles();
const [videoAspectRatio, setVideoAspectRatio] = useState(VIDEO_ASPECT_RATIO);
@@ -88,6 +89,7 @@ function EmptyStateComponent({
showsVerticalScrollIndicator={showsVerticalScrollIndicator}
contentContainerStyle={[{minHeight: minModalHeight}, styles.flexGrow1, styles.flexShrink0, containerStyles]}
style={styles.flex1}
+ addBottomSafeAreaPadding={addBottomSafeAreaPadding}
>
= {
lottieWebViewStyles?: React.CSSProperties | undefined;
minModalHeight?: number;
showsVerticalScrollIndicator?: boolean;
+
+ /** Whether to add bottom safe area padding to the view. */
+ addBottomSafeAreaPadding?: boolean;
};
type MediaType = SharedProps & {
diff --git a/src/components/FixedFooter.tsx b/src/components/FixedFooter.tsx
index 90adaec7a27e..fe9179fa88b4 100644
--- a/src/components/FixedFooter.tsx
+++ b/src/components/FixedFooter.tsx
@@ -1,7 +1,8 @@
import type {ReactNode} from 'react';
-import React from 'react';
+import React, {useMemo} from 'react';
import type {StyleProp, ViewStyle} from 'react-native';
import {View} from 'react-native';
+import useSafeAreaPaddings from '@hooks/useSafeAreaPaddings';
import useThemeStyles from '@hooks/useThemeStyles';
type FixedFooterProps = {
@@ -10,16 +11,41 @@ type FixedFooterProps = {
/** Styles to be assigned to Container */
style?: StyleProp;
+
+ /** Whether to add bottom safe area padding to the content. */
+ addBottomSafeAreaPadding?: boolean;
+
+ /** Whether to stick the footer to the bottom of the screen. */
+ shouldStickToBottom?: boolean;
};
-function FixedFooter({style, children}: FixedFooterProps) {
+function FixedFooter({style, children, addBottomSafeAreaPadding = false, shouldStickToBottom = false}: FixedFooterProps) {
const styles = useThemeStyles();
+ const {paddingBottom} = useSafeAreaPaddings(true);
+
+ const footerStyle = useMemo>(() => {
+ const totalPaddingBottom = styles.pb5.paddingBottom + paddingBottom;
+
+ // If the footer should stick to the bottom, we use absolute positioning instead of flex.
+ // In this case, we need to use style.bottom instead of style.paddingBottom.
+ if (shouldStickToBottom) {
+ return {position: 'absolute', left: 0, right: 0, bottom: addBottomSafeAreaPadding ? totalPaddingBottom : styles.pb5.paddingBottom};
+ }
+
+ // If the footer should not stick to the bottom, we use flex and add the safe area padding in styles.paddingBottom.
+ if (addBottomSafeAreaPadding) {
+ return {paddingBottom: totalPaddingBottom};
+ }
+
+ // Otherwise, we just use the default bottom padding.
+ return styles.pb5;
+ }, [addBottomSafeAreaPadding, paddingBottom, shouldStickToBottom, styles.pb5]);
if (!children) {
return null;
}
- return {children};
+ return {children};
}
FixedFooter.displayName = 'FixedFooter';
diff --git a/src/components/Form/FormProvider.tsx b/src/components/Form/FormProvider.tsx
index bf3746b61776..6c8d3f989e16 100644
--- a/src/components/Form/FormProvider.tsx
+++ b/src/components/Form/FormProvider.tsx
@@ -75,6 +75,12 @@ type FormProviderProps = FormProps(null);
const formContentRef = useRef(null);
@@ -103,13 +110,78 @@ function FormWrapper({
focusInput?.focus?.();
}, [errors, formState?.errorFields, inputRefs]);
+ const {paddingBottom} = useSafeAreaPaddings(true);
+ const SubmitButton = useMemo(
+ () =>
+ isSubmitButtonVisible && (
+
+ ),
+ [
+ disablePressOnEnter,
+ enabledWhenOffline,
+ errorMessage,
+ errors,
+ footerContent,
+ formState?.errorFields,
+ formState?.isLoading,
+ isLoading,
+ isSubmitActionDangerous,
+ isSubmitButtonVisible,
+ isSubmitDisabled,
+ onFixTheErrorsLinkPressed,
+ onSubmit,
+ paddingBottom,
+ shouldHideFixErrorsAlert,
+ shouldSubmitButtonStickToBottom,
+ style,
+ styles.flex1,
+ styles.mh0,
+ styles.mt5,
+ styles.pb5.paddingBottom,
+ submitButtonStyles,
+ submitButtonText,
+ submitFlexEnabled,
+ ],
+ );
+
const scrollViewContent = useCallback(
() => (
{
if (!shouldScrollToEnd) {
return;
@@ -122,77 +194,50 @@ function FormWrapper({
}}
>
{children}
- {isSubmitButtonVisible && (
-
- )}
+ {!shouldSubmitButtonStickToBottom && SubmitButton}
),
- [
- formID,
- style,
- safeAreaInsetPaddingBottom,
- styles.pb5.paddingBottom,
- styles.mh0,
- styles.mt5,
- styles.flex1,
- children,
- isSubmitButtonVisible,
- submitButtonText,
- isSubmitDisabled,
- errors,
- formState?.errorFields,
- formState?.isLoading,
- shouldHideFixErrorsAlert,
- errorMessage,
- isLoading,
- onSubmit,
- footerContent,
- onFixTheErrorsLinkPressed,
- submitFlexEnabled,
- submitButtonStyles,
- enabledWhenOffline,
- isSubmitActionDangerous,
- disablePressOnEnter,
- shouldScrollToEnd,
- ],
+ [formID, style, styles.pb5, children, shouldSubmitButtonStickToBottom, SubmitButton, shouldScrollToEnd],
);
if (!shouldUseScrollView) {
+ if (shouldSubmitButtonStickToBottom) {
+ return (
+ <>
+ {scrollViewContent()}
+ {SubmitButton}
+ >
+ );
+ }
+
return scrollViewContent();
}
- return scrollContextEnabled ? (
-
- {scrollViewContent()}
-
- ) : (
-
- {scrollViewContent()}
-
+ return (
+
+ {scrollContextEnabled ? (
+
+ {scrollViewContent()}
+
+ ) : (
+
+ {scrollViewContent()}
+
+ )}
+ {shouldSubmitButtonStickToBottom && SubmitButton}
+
);
}
diff --git a/src/components/FormAlertWithSubmitButton.tsx b/src/components/FormAlertWithSubmitButton.tsx
index fabb5e54cb60..3eb0b0abd508 100644
--- a/src/components/FormAlertWithSubmitButton.tsx
+++ b/src/components/FormAlertWithSubmitButton.tsx
@@ -62,6 +62,12 @@ type FormAlertWithSubmitButtonProps = {
/** The priority to assign the enter key event listener to buttons. 0 is the highest priority. */
enterKeyEventListenerPriority?: number;
+
+ /**
+ * Whether the button should have a background layer in the color of theme.appBG.
+ * This is needed for buttons that allow content to display under them.
+ */
+ shouldBlendOpacity?: boolean;
};
function FormAlertWithSubmitButton({
@@ -83,6 +89,7 @@ function FormAlertWithSubmitButton({
useSmallerSubmitButtonSize = false,
errorMessageStyle,
enterKeyEventListenerPriority = 0,
+ shouldBlendOpacity = false,
}: FormAlertWithSubmitButtonProps) {
const styles = useThemeStyles();
const style = [!footerContent ? {} : styles.mb3, buttonStyles];
@@ -107,6 +114,7 @@ function FormAlertWithSubmitButton({
{isOffline && !enabledWhenOffline ? (
diff --git a/src/components/Modal/types.ts b/src/components/Modal/types.ts
index 0ed3c91c7a3c..ddc1e2bbd0e3 100644
--- a/src/components/Modal/types.ts
+++ b/src/components/Modal/types.ts
@@ -110,6 +110,13 @@ type BaseModalProps = Partial &
/** Whether to prevent the focus trap from scrolling the element into view. */
shouldPreventScrollOnFocus?: boolean;
+
+ /**
+ * Temporary flag to disable safe area bottom spacing in modals and to allow edge-to-edge content.
+ * Modals should not always apply bottom safe area padding, instead it should be applied to the scrollable/bottom-docked content directly.
+ * This flag can be removed, once all components/screens have switched to edge-to-edge safe area handling.
+ */
+ enableEdgeToEdgeBottomSafeAreaPadding?: boolean;
};
export default BaseModalProps;
diff --git a/src/components/Navigation/TopLevelBottomTabBar/index.tsx b/src/components/Navigation/TopLevelBottomTabBar/index.tsx
index ed0e65e23dbb..ecc713293093 100644
--- a/src/components/Navigation/TopLevelBottomTabBar/index.tsx
+++ b/src/components/Navigation/TopLevelBottomTabBar/index.tsx
@@ -4,7 +4,7 @@ import {InteractionManager, View} from 'react-native';
import {FullScreenBlockingViewContext} from '@components/FullScreenBlockingViewContextProvider';
import BottomTabBar from '@components/Navigation/BottomTabBar';
import useResponsiveLayout from '@hooks/useResponsiveLayout';
-import useStyledSafeAreaInsets from '@hooks/useStyledSafeAreaInsets';
+import useSafeAreaPaddings from '@hooks/useSafeAreaPaddings';
import useThemeStyles from '@hooks/useThemeStyles';
import type {PlatformStackNavigationState} from '@libs/Navigation/PlatformStackNavigation/types';
import getIsBottomTabVisibleDirectly from './getIsBottomTabVisibleDirectly';
@@ -26,7 +26,7 @@ type TopLevelBottomTabBarProps = {
function TopLevelBottomTabBar({state}: TopLevelBottomTabBarProps) {
const styles = useThemeStyles();
const {shouldUseNarrowLayout} = useResponsiveLayout();
- const {paddingBottom} = useStyledSafeAreaInsets();
+ const {paddingBottom} = useSafeAreaPaddings();
const [isAfterClosingTransition, setIsAfterClosingTransition] = useState(false);
const cancelAfterInteractions = useRef | undefined>();
const {isBlockingViewVisible} = useContext(FullScreenBlockingViewContext);
diff --git a/src/components/NavigationBar/index.android.tsx b/src/components/NavigationBar/index.android.tsx
new file mode 100644
index 000000000000..a7e8fe9ccd07
--- /dev/null
+++ b/src/components/NavigationBar/index.android.tsx
@@ -0,0 +1,22 @@
+import {useMemo} from 'react';
+import {View} from 'react-native';
+import useSafeAreaPaddings from '@hooks/useSafeAreaPaddings';
+import useStyleUtils from '@hooks/useStyleUtils';
+import useTheme from '@hooks/useTheme';
+import CONST from '@src/CONST';
+
+/** NavigationBar renders a semi-translucent background behind the three-button navigation bar on Android. */
+function NavigationBar() {
+ const theme = useTheme();
+ const StyleUtils = useStyleUtils();
+ const {insets} = useSafeAreaPaddings();
+
+ const navigationBarType = useMemo(() => StyleUtils.getNavigationBarType(insets), [StyleUtils, insets]);
+
+ const isSoftKeyNavigation = navigationBarType === CONST.NAVIGATION_BAR_TYPE.SOFT_KEYS;
+
+ return isSoftKeyNavigation ? : null;
+}
+NavigationBar.displayName = 'NavigationBar';
+
+export default NavigationBar;
diff --git a/src/components/NavigationBar/index.tsx b/src/components/NavigationBar/index.tsx
new file mode 100644
index 000000000000..d50404780bcd
--- /dev/null
+++ b/src/components/NavigationBar/index.tsx
@@ -0,0 +1,6 @@
+function NavigationBar() {
+ return null;
+}
+NavigationBar.displayName = 'NavigationBar';
+
+export default NavigationBar;
diff --git a/src/components/OfflineIndicator.tsx b/src/components/OfflineIndicator.tsx
index 337e5dd850f7..5b6a28156dc2 100644
--- a/src/components/OfflineIndicator.tsx
+++ b/src/components/OfflineIndicator.tsx
@@ -1,6 +1,7 @@
-import React, {useMemo} from 'react';
+import React from 'react';
import type {StyleProp, ViewStyle} from 'react-native';
import {View} from 'react-native';
+import useBottomSafeSafeAreaPaddingStyle from '@hooks/useBottomSafeSafeAreaPaddingStyle';
import useLocalize from '@hooks/useLocalize';
import useNetwork from '@hooks/useNetwork';
import useResponsiveLayout from '@hooks/useResponsiveLayout';
@@ -17,22 +18,22 @@ type OfflineIndicatorProps = {
/** Optional styles for the container */
style?: StyleProp;
+
+ /** Whether to add bottom safe area padding to the view. */
+ addBottomSafeAreaPadding?: boolean;
};
-function OfflineIndicator({style, containerStyles}: OfflineIndicatorProps) {
+function OfflineIndicator({style, containerStyles, addBottomSafeAreaPadding = false}: OfflineIndicatorProps) {
const theme = useTheme();
const styles = useThemeStyles();
const {translate} = useLocalize();
const {isOffline} = useNetwork();
const {shouldUseNarrowLayout} = useResponsiveLayout();
- const computedStyles = useMemo((): StyleProp => {
- if (containerStyles) {
- return containerStyles;
- }
-
- return shouldUseNarrowLayout ? styles.offlineIndicatorMobile : styles.offlineIndicator;
- }, [containerStyles, shouldUseNarrowLayout, styles.offlineIndicatorMobile, styles.offlineIndicator]);
+ const computedStyles = useBottomSafeSafeAreaPaddingStyle({
+ addBottomSafeAreaPadding,
+ style: containerStyles ?? (shouldUseNarrowLayout ? styles.offlineIndicatorMobile : styles.offlineIndicator),
+ });
if (!isOffline) {
return null;
diff --git a/src/components/PopoverWithoutOverlay/index.tsx b/src/components/PopoverWithoutOverlay/index.tsx
index 7d58ad6d22be..5dfaddcab2df 100644
--- a/src/components/PopoverWithoutOverlay/index.tsx
+++ b/src/components/PopoverWithoutOverlay/index.tsx
@@ -7,8 +7,8 @@ import useSafeAreaInsets from '@hooks/useSafeAreaInsets';
import useStyleUtils from '@hooks/useStyleUtils';
import useThemeStyles from '@hooks/useThemeStyles';
import useWindowDimensions from '@hooks/useWindowDimensions';
+import {onModalDidClose, setCloseModal, willAlertModalBecomeVisible} from '@libs/actions/Modal';
import variables from '@styles/variables';
-import * as Modal from '@userActions/Modal';
import viewRef from '@src/types/utils/viewRef';
import type PopoverWithoutOverlayProps from './types';
@@ -54,13 +54,13 @@ function PopoverWithoutOverlay(
close: onClose,
anchorRef,
});
- removeOnClose = Modal.setCloseModal(onClose);
+ removeOnClose = setCloseModal(onClose);
} else {
onModalHide();
close(anchorRef);
- Modal.onModalDidClose();
+ onModalDidClose();
}
- Modal.willAlertModalBecomeVisible(isVisible, true);
+ willAlertModalBecomeVisible(isVisible, true);
return () => {
if (!removeOnClose) {
@@ -77,7 +77,7 @@ function PopoverWithoutOverlay(
paddingBottom: safeAreaPaddingBottom,
paddingLeft: safeAreaPaddingLeft,
paddingRight: safeAreaPaddingRight,
- } = useMemo(() => StyleUtils.getSafeAreaPadding(insets), [StyleUtils, insets]);
+ } = useMemo(() => StyleUtils.getPlatformSafeAreaPadding(insets), [StyleUtils, insets]);
const modalPaddingStyles = useMemo(
() =>
diff --git a/src/components/Pressable/PressableWithFeedback.tsx b/src/components/Pressable/PressableWithFeedback.tsx
index 10e6ac7bbca6..9eeb0980d027 100644
--- a/src/components/Pressable/PressableWithFeedback.tsx
+++ b/src/components/Pressable/PressableWithFeedback.tsx
@@ -37,6 +37,12 @@ type PressableWithFeedbackProps = PressableProps & {
/** The color of the underlay that will show through when the Pressable is active. */
underlayColor?: Color;
+
+ /**
+ * Whether the button should have a background layer in the color of theme.appBG.
+ * This is needed for buttons that allow content to display under them.
+ */
+ shouldBlendOpacity?: boolean;
};
function PressableWithFeedback(
@@ -47,6 +53,7 @@ function PressableWithFeedback(
pressDimmingValue = variables.pressDimValue,
hoverDimmingValue = variables.hoverDimValue,
dimAnimationDuration,
+ shouldBlendOpacity,
...rest
}: PressableWithFeedbackProps,
ref: PressableRef,
@@ -56,7 +63,7 @@ function PressableWithFeedback(
return (
please use `useStyledSafeAreaInsets` instead.
*/
function SafeAreaConsumer({children}: SafeAreaConsumerProps) {
const StyleUtils = useStyleUtils();
@@ -16,7 +15,7 @@ function SafeAreaConsumer({children}: SafeAreaConsumerProps) {
{(safeAreaInsets) => {
const insets = StyleUtils.getSafeAreaInsets(safeAreaInsets);
- const {paddingTop, paddingBottom} = StyleUtils.getSafeAreaPadding(insets);
+ const {paddingTop, paddingBottom} = StyleUtils.getPlatformSafeAreaPadding(insets);
return children({
paddingTop,
diff --git a/src/components/ScreenWrapper.tsx b/src/components/ScreenWrapper.tsx
index 021b0e240aea..6ea4ca3951ae 100644
--- a/src/components/ScreenWrapper.tsx
+++ b/src/components/ScreenWrapper.tsx
@@ -7,10 +7,11 @@ import {Keyboard, PanResponder, View} from 'react-native';
import {useOnyx} from 'react-native-onyx';
import {PickerAvoidingView} from 'react-native-picker-select';
import type {EdgeInsets} from 'react-native-safe-area-context';
+import useBottomSafeSafeAreaPaddingStyle from '@hooks/useBottomSafeSafeAreaPaddingStyle';
import useEnvironment from '@hooks/useEnvironment';
import useInitialDimensions from '@hooks/useInitialWindowDimensions';
import useResponsiveLayout from '@hooks/useResponsiveLayout';
-import useStyledSafeAreaInsets from '@hooks/useStyledSafeAreaInsets';
+import useSafeAreaPaddings from '@hooks/useSafeAreaPaddings';
import useTackInputFocus from '@hooks/useTackInputFocus';
import useThemeStyles from '@hooks/useThemeStyles';
import useWindowDimensions from '@hooks/useWindowDimensions';
@@ -114,6 +115,19 @@ type ScreenWrapperProps = {
/** Overrides the focus trap default settings */
focusTrapSettings?: FocusTrapForScreenProps['focusTrapSettings'];
+
+ /**
+ * Temporary flag to disable safe area bottom spacing in the ScreenWrapper and to allow edge-to-edge content
+ * The ScreenWrapper should not always apply bottom safe area padding, instead it should be applied to the scrollable/bottom-docked content directly.
+ * This flag can be removed, once all components/screens have switched to edge-to-edge safe area handling.
+ */
+ enableEdgeToEdgeBottomSafeAreaPadding?: boolean;
+
+ /**
+ * Whether the KeyboardAvoidingView should compensate for the bottom safe area padding.
+ * The KeyboardAvoidingView will use a negative keyboardVerticalOffset.
+ */
+ shouldKeyboardOffsetBottomSafeAreaPadding?: boolean;
};
type ScreenWrapperStatusContextType = {
@@ -130,7 +144,7 @@ function ScreenWrapper(
shouldEnableMinHeight = false,
includePaddingTop = true,
keyboardAvoidingViewBehavior = 'padding',
- includeSafeAreaPaddingBottom = true,
+ includeSafeAreaPaddingBottom: includeSafeAreaPaddingBottomProp = true,
shouldEnableKeyboardAvoidingView = true,
shouldEnablePickerAvoiding = true,
headerGapStyles,
@@ -147,6 +161,8 @@ function ScreenWrapper(
shouldUseCachedViewportHeight = false,
focusTrapSettings,
bottomContent,
+ enableEdgeToEdgeBottomSafeAreaPadding = false,
+ shouldKeyboardOffsetBottomSafeAreaPadding = enableEdgeToEdgeBottomSafeAreaPadding,
}: ScreenWrapperProps,
ref: ForwardedRef,
) {
@@ -168,6 +184,8 @@ function ScreenWrapper(
const [isSingleNewDotEntry] = useOnyx(ONYXKEYS.IS_SINGLE_NEW_DOT_ENTRY);
+ const includeSafeAreaPaddingBottom = enableEdgeToEdgeBottomSafeAreaPadding ? false : includeSafeAreaPaddingBottomProp;
+
// We need to use isSmallScreenWidth instead of shouldUseNarrowLayout for a case where we want to show the offline indicator only on small screens
// eslint-disable-next-line rulesdir/prefer-shouldUseNarrowLayout-instead-of-isSmallScreenWidth
const {isSmallScreenWidth, shouldUseNarrowLayout} = useResponsiveLayout();
@@ -269,25 +287,41 @@ function ScreenWrapper(
// eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps
}, []);
- const {insets, paddingTop, paddingBottom, safeAreaPaddingBottomStyle, unmodifiedPaddings} = useStyledSafeAreaInsets();
- const paddingTopStyle: StyleProp = {};
- const paddingBottomStyle: StyleProp = {};
+ const {insets, paddingTop, paddingBottom, safeAreaPaddingBottomStyle, unmodifiedPaddings} = useSafeAreaPaddings(enableEdgeToEdgeBottomSafeAreaPadding);
const isSafeAreaTopPaddingApplied = includePaddingTop;
- if (includePaddingTop) {
- paddingTopStyle.paddingTop = paddingTop;
- }
- if (includePaddingTop && ignoreInsetsConsumption) {
- paddingTopStyle.paddingTop = unmodifiedPaddings.top;
- }
-
- // We always need the safe area padding bottom if we're showing the offline indicator since it is bottom-docked.
- if (includeSafeAreaPaddingBottom) {
- paddingBottomStyle.paddingBottom = paddingBottom;
- }
- if (includeSafeAreaPaddingBottom && ignoreInsetsConsumption) {
- paddingBottomStyle.paddingBottom = unmodifiedPaddings.bottom;
- }
+ const paddingTopStyle: StyleProp = useMemo(() => {
+ if (includePaddingTop && ignoreInsetsConsumption) {
+ return {paddingTop: unmodifiedPaddings.top};
+ }
+ if (includePaddingTop) {
+ return {paddingTop};
+ }
+ return {};
+ }, [ignoreInsetsConsumption, includePaddingTop, paddingTop, unmodifiedPaddings.top]);
+
+ const showBottomContent = enableEdgeToEdgeBottomSafeAreaPadding ? !!bottomContent : true;
+ const edgeToEdgeBottomContentStyle = useBottomSafeSafeAreaPaddingStyle({addBottomSafeAreaPadding: true});
+ const legacyBottomContentStyle: StyleProp = useMemo(() => {
+ const shouldUseUnmodifiedPaddings = includeSafeAreaPaddingBottom && ignoreInsetsConsumption;
+ if (shouldUseUnmodifiedPaddings) {
+ return {
+ paddingBottom: unmodifiedPaddings.bottom,
+ };
+ }
+
+ return {
+ // We always need the safe area padding bottom if we're showing the offline indicator since it is bottom-docked.
+ paddingBottom: includeSafeAreaPaddingBottom ? paddingBottom : undefined,
+ };
+ }, [ignoreInsetsConsumption, includeSafeAreaPaddingBottom, paddingBottom, unmodifiedPaddings.bottom]);
+ const bottomContentStyle = useMemo(
+ () => (enableEdgeToEdgeBottomSafeAreaPadding ? edgeToEdgeBottomContentStyle : legacyBottomContentStyle),
+ [enableEdgeToEdgeBottomSafeAreaPadding, edgeToEdgeBottomContentStyle, legacyBottomContentStyle],
+ );
+
+ const addMobileOfflineIndicatorBottomSafeAreaPadding = enableEdgeToEdgeBottomSafeAreaPadding ? !bottomContent : !includeSafeAreaPaddingBottom;
+ const addWidescreenOfflineIndicatorBottomSafeAreaPadding = enableEdgeToEdgeBottomSafeAreaPadding ? !bottomContent : true;
const isAvoidingViewportScroll = useTackInputFocus(isFocused && shouldEnableMaxHeight && shouldAvoidScrollOnVirtualViewport && isMobileWebKit());
const contextValue = useMemo(
@@ -314,6 +348,7 @@ function ScreenWrapper(
style={[styles.w100, styles.h100, !isBlurred ? {maxHeight} : undefined, isAvoidingViewportScroll ? [styles.overflowAuto, styles.overscrollBehaviorContain] : {}]}
behavior={keyboardAvoidingViewBehavior}
enabled={shouldEnableKeyboardAvoidingView}
+ shouldOffsetBottomSafeAreaPadding={shouldKeyboardOffsetBottomSafeAreaPadding}
>
{/* Since import state is tightly coupled to the offline state, it is safe to display it when showing offline indicator */}
@@ -351,6 +383,7 @@ function ScreenWrapper(
{/* Since import state is tightly coupled to the offline state, it is safe to display it when showing offline indicator */}
@@ -360,7 +393,7 @@ function ScreenWrapper(
- {bottomContent}
+ {showBottomContent && {bottomContent}}
);
diff --git a/src/components/ScrollView.tsx b/src/components/ScrollView.tsx
index a61c592015ee..8a2a4519e0d4 100644
--- a/src/components/ScrollView.tsx
+++ b/src/components/ScrollView.tsx
@@ -2,9 +2,20 @@ import React from 'react';
import type {ForwardedRef} from 'react';
// eslint-disable-next-line no-restricted-imports
import {ScrollView as RNScrollView} from 'react-native';
-import type {ScrollViewProps} from 'react-native';
+import type {ScrollViewProps as RNScrollViewProps} from 'react-native';
+import useBottomSafeSafeAreaPaddingStyle from '@hooks/useBottomSafeSafeAreaPaddingStyle';
+
+type ScrollViewProps = RNScrollViewProps & {
+ /** Whether to add bottom safe area padding to the content. */
+ addBottomSafeAreaPadding?: boolean;
+};
+
+function ScrollView(
+ {children, scrollIndicatorInsets, contentContainerStyle: contentContainerStyleProp, addBottomSafeAreaPadding = false, ...props}: ScrollViewProps,
+ ref: ForwardedRef,
+) {
+ const contentContainerStyle = useBottomSafeSafeAreaPaddingStyle({addBottomSafeAreaPadding, style: contentContainerStyleProp});
-function ScrollView({children, scrollIndicatorInsets, ...props}: ScrollViewProps, ref: ForwardedRef) {
return (
mergeCardListWithWorkspaceFeeds(workspaceCardFeeds ?? CONST.EMPTY_OBJECT, userCardList), [userCardList, workspaceCardFeeds]);
- const {unmodifiedPaddings} = useStyledSafeAreaInsets();
+ const {unmodifiedPaddings} = useSafeAreaPaddings();
const shouldGroupByReports = groupBy === CONST.SEARCH.GROUP_BY.REPORTS;
const cardFeedNamesWithType = useMemo(() => {
return getCardFeedNamesWithType({workspaceCardFeeds, userCardList, translate});
diff --git a/src/components/SectionList/BaseSectionList.tsx b/src/components/SectionList/BaseSectionList.tsx
new file mode 100644
index 000000000000..dc40211030b4
--- /dev/null
+++ b/src/components/SectionList/BaseSectionList.tsx
@@ -0,0 +1,24 @@
+import React, {forwardRef} from 'react';
+import useBottomSafeSafeAreaPaddingStyle from '@hooks/useBottomSafeSafeAreaPaddingStyle';
+import AnimatedSectionList from './AnimatedSectionList';
+import type {SectionListProps, SectionListRef} from './types';
+
+function BaseSectionList(
+ {addBottomSafeAreaPadding = false, contentContainerStyle: contentContainerStyleProp, ...restProps}: SectionListProps,
+ ref: SectionListRef,
+) {
+ const contentContainerStyle = useBottomSafeSafeAreaPaddingStyle({addBottomSafeAreaPadding, style: contentContainerStyleProp});
+
+ return (
+
+ );
+}
+
+BaseSectionList.displayName = 'BaseSectionList';
+
+export default forwardRef(BaseSectionList);
diff --git a/src/components/SectionList/index.android.tsx b/src/components/SectionList/index.android.tsx
index 157e546b3ea9..d9877fd5afca 100644
--- a/src/components/SectionList/index.android.tsx
+++ b/src/components/SectionList/index.android.tsx
@@ -1,10 +1,10 @@
import React, {forwardRef} from 'react';
-import AnimatedSectionList from './AnimatedSectionList';
+import BaseSectionList from './BaseSectionList';
import type {SectionListProps, SectionListRef} from './types';
function SectionListWithRef(props: SectionListProps, ref: SectionListRef) {
return (
- (props: SectionListProps, ref: SectionListRef) {
return (
- = RNSectionListProps & {
+ /** Whether to add bottom safe area padding to the content. */
+ addBottomSafeAreaPadding?: boolean;
+};
-type SectionListProps = SectionListPropsRN;
type SectionListRef = ForwardedRef>;
export type {SectionListProps, SectionListRef};
diff --git a/src/components/SelectionList/BaseSelectionList.tsx b/src/components/SelectionList/BaseSelectionList.tsx
index 67cf5e5a3841..c1e3818a12e2 100644
--- a/src/components/SelectionList/BaseSelectionList.tsx
+++ b/src/components/SelectionList/BaseSelectionList.tsx
@@ -16,13 +16,14 @@ import Text from '@components/Text';
import TextInput from '@components/TextInput';
import useActiveElementRole from '@hooks/useActiveElementRole';
import useArrowKeyFocusManager from '@hooks/useArrowKeyFocusManager';
+import useBottomSafeSafeAreaPaddingStyle from '@hooks/useBottomSafeSafeAreaPaddingStyle';
import useKeyboardShortcut from '@hooks/useKeyboardShortcut';
import useKeyboardState from '@hooks/useKeyboardState';
import useLocalize from '@hooks/useLocalize';
import usePrevious from '@hooks/usePrevious';
+import useSafeAreaPaddings from '@hooks/useSafeAreaPaddings';
import useScrollEnabled from '@hooks/useScrollEnabled';
import useSingleExecution from '@hooks/useSingleExecution';
-import useStyledSafeAreaInsets from '@hooks/useStyledSafeAreaInsets';
import useThemeStyles from '@hooks/useThemeStyles';
import getSectionsWithIndexOffset from '@libs/getSectionsWithIndexOffset';
import {addKeyDownPressListener, removeKeyDownPressListener} from '@libs/KeyboardShortcut/KeyDownPressListener';
@@ -33,7 +34,7 @@ import {isEmptyObject} from '@src/types/utils/EmptyObject';
import arraysEqual from '@src/utils/arraysEqual';
import BaseSelectionListItemRenderer from './BaseSelectionListItemRenderer';
import FocusAwareCellRendererComponent from './FocusAwareCellRendererComponent';
-import type {BaseSelectionListProps, ButtonOrCheckBoxRoles, FlattenedSectionsReturn, ListItem, SectionListDataType, SectionWithIndexOffset, SelectionListHandle} from './types';
+import type {ButtonOrCheckBoxRoles, FlattenedSectionsReturn, ListItem, SectionListDataType, SectionWithIndexOffset, SelectionListHandle, SelectionListProps} from './types';
const getDefaultItemHeight = () => variables.optionRowHeight;
@@ -115,7 +116,7 @@ function BaseSelectionList(
listItemWrapperStyle,
shouldIgnoreFocus = false,
scrollEventThrottle,
- contentContainerStyle,
+ contentContainerStyle: contentContainerStyleProp,
shouldHighlightSelectedItem = false,
shouldKeepFocusedItemAtTopOfViewableArea = false,
shouldDebounceScrolling = false,
@@ -127,7 +128,8 @@ function BaseSelectionList(
listItemTitleContainerStyles,
isScreenFocused = false,
shouldSubscribeToArrowKeyEvents = true,
- }: BaseSelectionListProps,
+ addBottomSafeAreaPadding = false,
+ }: SelectionListProps,
ref: ForwardedRef,
) {
const styles = useThemeStyles();
@@ -823,11 +825,24 @@ function BaseSelectionList(
);
- const {safeAreaPaddingBottomStyle} = useStyledSafeAreaInsets();
+ const {safeAreaPaddingBottomStyle} = useSafeAreaPaddings();
+ const paddingBottomStyle = useMemo(
+ () => (!isKeyboardShown || !!footerContent) && includeSafeAreaPaddingBottom && safeAreaPaddingBottomStyle,
+ [footerContent, includeSafeAreaPaddingBottom, isKeyboardShown, safeAreaPaddingBottomStyle],
+ );
+
+ // If the default confirm button is visible and it is bottom-sticky,
+ // we need to add additional padding bottom to the content container.
+ const contentContainerStyle = useBottomSafeSafeAreaPaddingStyle({
+ addBottomSafeAreaPadding: false, // Bottom safe area padding is already applied in the SectionList
+ style: contentContainerStyleProp,
+ });
+
+ const shouldHideContentBottomSafeAreaPadding = showConfirmButton || !!footerContent;
// TODO: test _every_ component that uses SelectionList
return (
-
+
{shouldShowTextInput && !shouldShowTextInputAfterHeader && renderInput()}
{/* If we are loading new options we will avoid showing any header message. This is mostly because one of the header messages says there are no options. */}
{/* This is misleading because we might be in the process of loading fresh options from the server. */}
@@ -884,6 +899,7 @@ function BaseSelectionList(
onEndReached={onEndReached}
onEndReachedThreshold={onEndReachedThreshold}
scrollEventThrottle={scrollEventThrottle}
+ addBottomSafeAreaPadding={!shouldHideContentBottomSafeAreaPadding && addBottomSafeAreaPadding}
contentContainerStyle={contentContainerStyle}
CellRendererComponent={shouldPreventActiveCellVirtualization ? FocusAwareCellRendererComponent : undefined}
/>
@@ -891,7 +907,10 @@ function BaseSelectionList(
>
)}
{showConfirmButton && (
-
+
)}
- {!!footerContent && {footerContent}}
+ {!!footerContent && (
+
+ {footerContent}
+
+ )}
);
}
diff --git a/src/components/SelectionList/BaseSelectionListItemRenderer.tsx b/src/components/SelectionList/BaseSelectionListItemRenderer.tsx
index 987a72e025c1..9f3623f75f43 100644
--- a/src/components/SelectionList/BaseSelectionListItemRenderer.tsx
+++ b/src/components/SelectionList/BaseSelectionListItemRenderer.tsx
@@ -4,10 +4,10 @@ import type useArrowKeyFocusManager from '@hooks/useArrowKeyFocusManager';
import type useSingleExecution from '@hooks/useSingleExecution';
import {isMobileChrome} from '@libs/Browser';
import {isReportListItemType} from '@libs/SearchUIUtils';
-import type {BaseListItemProps, BaseSelectionListProps, ExtendedTargetedEvent, ListItem} from './types';
+import type {BaseListItemProps, ExtendedTargetedEvent, ListItem, SelectionListProps} from './types';
type BaseSelectionListItemRendererProps = Omit, 'onSelectRow'> &
- Pick, 'ListItem' | 'shouldHighlightSelectedItem' | 'shouldIgnoreFocus' | 'shouldSingleExecuteRowSelect'> & {
+ Pick, 'ListItem' | 'shouldHighlightSelectedItem' | 'shouldIgnoreFocus' | 'shouldSingleExecuteRowSelect'> & {
index: number;
selectRow: (item: TItem, indexToFocus?: number) => void;
setFocusedIndex: ReturnType[1];
diff --git a/src/components/SelectionList/index.native.tsx b/src/components/SelectionList/index.native.tsx
index baccdf7c6024..4207f65c639c 100644
--- a/src/components/SelectionList/index.native.tsx
+++ b/src/components/SelectionList/index.native.tsx
@@ -2,9 +2,9 @@ import React, {forwardRef} from 'react';
import type {ForwardedRef} from 'react';
import {Keyboard} from 'react-native';
import BaseSelectionList from './BaseSelectionList';
-import type {BaseSelectionListProps, ListItem, SelectionListHandle} from './types';
+import type {ListItem, SelectionListHandle, SelectionListProps} from './types';
-function SelectionList(props: BaseSelectionListProps, ref: ForwardedRef) {
+function SelectionList(props: SelectionListProps, ref: ForwardedRef) {
return (
({onScroll, ...props}: BaseSelectionListProps, ref: ForwardedRef) {
+function SelectionList({onScroll, ...props}: SelectionListProps, ref: ForwardedRef) {
const [isScreenTouched, setIsScreenTouched] = useState(false);
const touchStart = () => setIsScreenTouched(true);
const touchEnd = () => setIsScreenTouched(false);
useEffect(() => {
- if (!DeviceCapabilities.canUseTouchScreen()) {
+ if (!canUseTouchScreen()) {
return;
}
@@ -73,7 +73,7 @@ function SelectionList({onScroll, ...props}: BaseSelecti
onScroll={onScroll ?? defaultOnScroll}
// Ignore the focus if it's caused by a touch event on mobile chrome.
// For example, a long press will trigger a focus event on mobile chrome.
- shouldIgnoreFocus={Browser.isMobileChrome() && isScreenTouched}
+ shouldIgnoreFocus={isMobileChrome() && isScreenTouched}
shouldDebounceScrolling={shouldDebounceScrolling}
/>
);
diff --git a/src/components/SelectionList/types.ts b/src/components/SelectionList/types.ts
index 3eb63ae97242..c409c6a61d38 100644
--- a/src/components/SelectionList/types.ts
+++ b/src/components/SelectionList/types.ts
@@ -394,7 +394,7 @@ type SkeletonViewProps = {
shouldAnimate: boolean;
};
-type BaseSelectionListProps = Partial & {
+type SelectionListProps = Partial & {
/** Sections for the section list */
sections: Array> | typeof CONST.EMPTY_ARRAY;
@@ -668,6 +668,9 @@ type BaseSelectionListProps = Partial & {
/** Whether the screen is focused or not. (useIsFocused state does not work in tab screens, e.g. SearchPageBottomTab) */
isScreenFocused?: boolean;
+
+ /** Whether to add bottom safe area padding to the content. */
+ addBottomSafeAreaPadding?: boolean;
} & TRightHandSideComponent;
type SelectionListHandle = {
@@ -704,7 +707,7 @@ type SectionListDataType = ExtendedSectionListData = BaseSelectionListProps & {
+type SelectionListWithModalProps = SelectionListProps & {
turnOnSelectionModeOnLongPress?: boolean;
onTurnOnSelectionMode?: (item: TItem | null) => void;
isSelected?: (item: TItem) => boolean;
diff --git a/src/components/SidePane/Help/index.android.tsx b/src/components/SidePane/Help/index.android.tsx
index e907643f6697..8be21c35a535 100644
--- a/src/components/SidePane/Help/index.android.tsx
+++ b/src/components/SidePane/Help/index.android.tsx
@@ -3,7 +3,7 @@ import React, {useCallback} from 'react';
// eslint-disable-next-line no-restricted-imports
import {Animated, BackHandler} from 'react-native';
import useResponsiveLayout from '@hooks/useResponsiveLayout';
-import useStyledSafeAreaInsets from '@hooks/useStyledSafeAreaInsets';
+import useSafeAreaPaddings from '@hooks/useSafeAreaPaddings';
import useThemeStyles from '@hooks/useThemeStyles';
import HelpContent from './HelpContent';
import type HelpProps from './types';
@@ -11,7 +11,7 @@ import type HelpProps from './types';
function Help({sidePaneTranslateX, closeSidePane}: HelpProps) {
const styles = useThemeStyles();
const {isExtraLargeScreenWidth, shouldUseNarrowLayout} = useResponsiveLayout();
- const {paddingTop, paddingBottom} = useStyledSafeAreaInsets();
+ const {paddingTop, paddingBottom} = useSafeAreaPaddings();
// SidePane isn't a native screen, this handles the back button press on Android
useFocusEffect(
diff --git a/src/components/SidePane/Help/index.ios.tsx b/src/components/SidePane/Help/index.ios.tsx
index 7b9e5b65849f..171b98e2c14d 100644
--- a/src/components/SidePane/Help/index.ios.tsx
+++ b/src/components/SidePane/Help/index.ios.tsx
@@ -3,7 +3,7 @@ import React from 'react';
import {Animated, Dimensions} from 'react-native';
import {Gesture, GestureDetector} from 'react-native-gesture-handler';
import useResponsiveLayout from '@hooks/useResponsiveLayout';
-import useStyledSafeAreaInsets from '@hooks/useStyledSafeAreaInsets';
+import useSafeAreaPaddings from '@hooks/useSafeAreaPaddings';
import useThemeStyles from '@hooks/useThemeStyles';
import CONST from '@src/CONST';
import HelpContent from './HelpContent';
@@ -14,7 +14,7 @@ const SCREEN_WIDTH = Dimensions.get('window').width;
function Help({sidePaneTranslateX, closeSidePane}: HelpProps) {
const styles = useThemeStyles();
const {isExtraLargeScreenWidth, shouldUseNarrowLayout} = useResponsiveLayout();
- const {paddingTop, paddingBottom} = useStyledSafeAreaInsets();
+ const {paddingTop, paddingBottom} = useSafeAreaPaddings();
// SidePane isn't a native screen, this simulates the 'close swipe gesture' on iOS
const panGesture = Gesture.Pan()
diff --git a/src/components/SidePane/Help/index.tsx b/src/components/SidePane/Help/index.tsx
index 08567e2f1fab..597bff5391ad 100644
--- a/src/components/SidePane/Help/index.tsx
+++ b/src/components/SidePane/Help/index.tsx
@@ -3,7 +3,7 @@ import React from 'react';
import {Animated} from 'react-native';
import useKeyboardShortcut from '@hooks/useKeyboardShortcut';
import useResponsiveLayout from '@hooks/useResponsiveLayout';
-import useStyledSafeAreaInsets from '@hooks/useStyledSafeAreaInsets';
+import useSafeAreaPaddings from '@hooks/useSafeAreaPaddings';
import useThemeStyles from '@hooks/useThemeStyles';
import CONST from '@src/CONST';
import HelpContent from './HelpContent';
@@ -12,7 +12,7 @@ import type HelpProps from './types';
function Help({sidePaneTranslateX, closeSidePane}: HelpProps) {
const styles = useThemeStyles();
const {isExtraLargeScreenWidth, shouldUseNarrowLayout} = useResponsiveLayout();
- const {paddingTop, paddingBottom} = useStyledSafeAreaInsets();
+ const {paddingTop, paddingBottom} = useSafeAreaPaddings();
useKeyboardShortcut(CONST.KEYBOARD_SHORTCUTS.ESCAPE, () => closeSidePane(), {isActive: !isExtraLargeScreenWidth});
diff --git a/src/components/SidePane/index.tsx b/src/components/SidePane/index.tsx
index 4a2f7adba8d3..72965d08f395 100644
--- a/src/components/SidePane/index.tsx
+++ b/src/components/SidePane/index.tsx
@@ -1,6 +1,7 @@
import React from 'react';
import {View} from 'react-native';
import useRootNavigationState from '@hooks/useRootNavigationState';
+import useSafeAreaPaddings from '@hooks/useSafeAreaPaddings';
import useSidePane from '@hooks/useSidePane';
import NAVIGATORS from '@src/NAVIGATORS';
import Help from './Help';
diff --git a/src/hooks/useBottomSafeSafeAreaPaddingStyle.ts b/src/hooks/useBottomSafeSafeAreaPaddingStyle.ts
new file mode 100644
index 000000000000..de56f88d8ec0
--- /dev/null
+++ b/src/hooks/useBottomSafeSafeAreaPaddingStyle.ts
@@ -0,0 +1,63 @@
+import {useMemo} from 'react';
+import {StyleSheet} from 'react-native';
+import type {StyleProp, ViewStyle} from 'react-native';
+import useSafeAreaPaddings from './useSafeAreaPaddings';
+
+/** The parameters for the useBottomSafeSafeAreaPaddingStyle hook. */
+type UseBottomSafeAreaPaddingStyleParams = {
+ /** Whether to add bottom safe area padding to the content. */
+ addBottomSafeAreaPadding?: boolean;
+
+ /** The style to adapt and add bottom safe area padding to. */
+ style?: StyleProp;
+
+ /** The additional padding to add to the bottom of the content. */
+ additionalPaddingBottom?: number;
+};
+
+/**
+ * useBottomSafeSafeAreaPaddingStyle is a hook that creates or adapts a given style and adds bottom safe area padding.
+ * It is useful for creating new styles or updating existing style props (e.g. contentContainerStyle).
+ * @param params - The parameters for the hook.
+ * @returns The style with bottom safe area padding applied.
+ */
+function useBottomSafeSafeAreaPaddingStyle(params?: UseBottomSafeAreaPaddingStyleParams) {
+ const {paddingBottom: safeAreaPaddingBottom} = useSafeAreaPaddings(true);
+
+ const {addBottomSafeAreaPadding, style, additionalPaddingBottom} = params ?? {};
+
+ return useMemo>(() => {
+ let totalPaddingBottom: number | string = additionalPaddingBottom ?? 0;
+
+ // Add the safe area padding to the total padding if the flag is enabled
+ if (addBottomSafeAreaPadding) {
+ totalPaddingBottom += safeAreaPaddingBottom;
+ }
+
+ // If there is no bottom safe area or additional padding, return the style as is
+ if (totalPaddingBottom === 0) {
+ return style;
+ }
+
+ // If a style is provided, flatten the style and add the padding to it
+ if (style) {
+ const contentContainerStyleFlattened = StyleSheet.flatten(style);
+ const stylePaddingBottom = contentContainerStyleFlattened?.paddingBottom;
+
+ if (typeof stylePaddingBottom === 'number') {
+ totalPaddingBottom += stylePaddingBottom;
+ } else if (typeof stylePaddingBottom === 'string') {
+ totalPaddingBottom = `calc(${totalPaddingBottom}px + ${stylePaddingBottom})`;
+ } else if (stylePaddingBottom !== undefined) {
+ return style;
+ }
+
+ return [style, {paddingBottom: totalPaddingBottom}];
+ }
+
+ // If no style is provided, return the padding as an object
+ return {paddingBottom: totalPaddingBottom};
+ }, [addBottomSafeAreaPadding, style, additionalPaddingBottom, safeAreaPaddingBottom]);
+}
+
+export default useBottomSafeSafeAreaPaddingStyle;
diff --git a/src/hooks/useSafeAreaInsets.ts b/src/hooks/useSafeAreaInsets.ts
index f954a6c99e94..33a76e3c0053 100644
--- a/src/hooks/useSafeAreaInsets.ts
+++ b/src/hooks/useSafeAreaInsets.ts
@@ -5,8 +5,8 @@ import useStyleUtils from './useStyleUtils';
/**
* Note: if you're looking for a hook to implement safe area padding in your screen, please either:
- * - use a component and set `includeSafeAreaPaddingBottom` to `true`. Or
- * - use the `useStyledSafeAreaInsets` hook.
+ * - add the `addBottomSafeAreaPadding` prop to generic components like ScrollView, SelectionList or FormProvider.
+ * - use the `useSafeAreaPaddings` hook.
*
* This hook is only meant for internal use cases where you need to access the raw safe area insets.
*/
diff --git a/src/hooks/useStyledSafeAreaInsets.ts b/src/hooks/useSafeAreaPaddings.ts
similarity index 71%
rename from src/hooks/useStyledSafeAreaInsets.ts
rename to src/hooks/useSafeAreaPaddings.ts
index b5a7bd2413c4..9087cd52a338 100644
--- a/src/hooks/useStyledSafeAreaInsets.ts
+++ b/src/hooks/useSafeAreaPaddings.ts
@@ -4,8 +4,7 @@ import useSafeAreaInsets from './useSafeAreaInsets';
import useStyleUtils from './useStyleUtils';
/**
- * Custom hook to get the styled safe area insets. The top and bottom padding values are adjusted
- * so that they will only ever be applied once per .
+ * Custom hook to get safe area padding values and styles.
*
* This hook utilizes the `SafeAreaInsetsContext` to retrieve the current safe area insets
* and applies styling adjustments using the `useStyleUtils` hook.
@@ -26,28 +25,41 @@ import useStyleUtils from './useStyleUtils';
* }
*
* function MyComponent() {
- * const { paddingTop, paddingBottom, safeAreaPaddingBottomStyle } = useStyledSafeAreaInsets();
+ * const { paddingTop, paddingBottom, safeAreaPaddingBottomStyle } = useSafeAreaPaddings();
*
* // Use these values to style your component accordingly
* }
*/
-function useStyledSafeAreaInsets() {
+function useSafeAreaPaddings(enableEdgeToEdgeBottomSafeAreaPadding = false) {
const StyleUtils = useStyleUtils();
const insets = useSafeAreaInsets();
- const {paddingTop, paddingBottom} = StyleUtils.getSafeAreaPadding(insets);
+ const {paddingTop, paddingBottom} = useMemo(() => StyleUtils.getPlatformSafeAreaPadding(insets), [StyleUtils, insets]);
const screenWrapperStatusContext = useContext(ScreenWrapperStatusContext);
const isSafeAreaTopPaddingApplied = screenWrapperStatusContext?.isSafeAreaTopPaddingApplied ?? false;
const isSafeAreaBottomPaddingApplied = screenWrapperStatusContext?.isSafeAreaBottomPaddingApplied ?? false;
+ const adaptedPaddingBottom = isSafeAreaBottomPaddingApplied ? 0 : paddingBottom;
+ const safeAreaPaddingBottomStyle = useMemo(
+ () => ({paddingBottom: enableEdgeToEdgeBottomSafeAreaPadding ? paddingBottom : adaptedPaddingBottom}),
+ [adaptedPaddingBottom, enableEdgeToEdgeBottomSafeAreaPadding, paddingBottom],
+ );
+
+ if (enableEdgeToEdgeBottomSafeAreaPadding) {
+ return {
+ paddingTop,
+ paddingBottom,
+ unmodifiedPaddings: {},
+ insets,
+ safeAreaPaddingBottomStyle,
+ };
+ }
+
const adaptedInsets = {
...insets,
top: isSafeAreaTopPaddingApplied ? 0 : insets?.top,
bottom: isSafeAreaBottomPaddingApplied ? 0 : insets?.bottom,
};
- const adaptedPaddingBottom = isSafeAreaBottomPaddingApplied ? 0 : paddingBottom;
-
- const safeAreaPaddingBottomStyle = useMemo(() => ({paddingBottom: adaptedPaddingBottom}), [adaptedPaddingBottom]);
return {
paddingTop: isSafeAreaTopPaddingApplied ? 0 : paddingTop,
@@ -61,4 +73,4 @@ function useStyledSafeAreaInsets() {
};
}
-export default useStyledSafeAreaInsets;
+export default useSafeAreaPaddings;
diff --git a/src/libs/NavBarManager/index.android.ts b/src/libs/NavBarManager/index.android.ts
index 81a4626bfb08..1bcfd24507c0 100644
--- a/src/libs/NavBarManager/index.android.ts
+++ b/src/libs/NavBarManager/index.android.ts
@@ -1,11 +1,13 @@
import {NativeModules} from 'react-native';
-import type StartupTimer from './types';
-import type {NavBarButtonStyle} from './types';
+import type NavBarManager from './types';
-const navBarManager: StartupTimer = {
- setButtonStyle: (style: NavBarButtonStyle) => {
+const navBarManager: NavBarManager = {
+ setButtonStyle: (style) => {
NativeModules.RNNavBarManager.setButtonStyle(style);
},
+ getType: () => {
+ return NativeModules.RNNavBarManager.getType();
+ },
};
export default navBarManager;
diff --git a/src/libs/NavBarManager/index.ts b/src/libs/NavBarManager/index.ts
index 79c9ef85fdcd..a7378895a2fe 100644
--- a/src/libs/NavBarManager/index.ts
+++ b/src/libs/NavBarManager/index.ts
@@ -1,7 +1,10 @@
+import getPlatform from '@libs/getPlatform';
+import CONST from '@src/CONST';
import type NavBarManager from './types';
const navBarManager: NavBarManager = {
setButtonStyle: () => {},
+ getType: () => (getPlatform() === CONST.PLATFORM.IOS ? CONST.NAVIGATION_BAR_TYPE.GESTURE_BAR : CONST.NAVIGATION_BAR_TYPE.NONE),
};
export default navBarManager;
diff --git a/src/libs/NavBarManager/types.ts b/src/libs/NavBarManager/types.ts
index 443db391da9d..f2003be8110c 100644
--- a/src/libs/NavBarManager/types.ts
+++ b/src/libs/NavBarManager/types.ts
@@ -1,8 +1,14 @@
+import type {ValueOf} from 'type-fest';
+import type CONST from '@src/CONST';
+
type NavBarButtonStyle = 'light' | 'dark';
+type NavigationBarType = ValueOf;
+
type NavBarManager = {
setButtonStyle: (style: NavBarButtonStyle) => void;
+ getType: () => NavigationBarType;
};
export default NavBarManager;
-export type {NavBarButtonStyle};
+export type {NavBarButtonStyle, NavigationBarType};
diff --git a/src/libs/actions/PaymentMethods.ts b/src/libs/actions/PaymentMethods.ts
index 6c3d17b8236b..d70c0af6294e 100644
--- a/src/libs/actions/PaymentMethods.ts
+++ b/src/libs/actions/PaymentMethods.ts
@@ -398,7 +398,7 @@ function resetWalletTransferData() {
});
}
-function saveWalletTransferAccountTypeAndID(selectedAccountType: string, selectedAccountID: string) {
+function saveWalletTransferAccountTypeAndID(selectedAccountType: string | undefined, selectedAccountID: string | undefined) {
Onyx.merge(ONYXKEYS.WALLET_TRANSFER, {selectedAccountType, selectedAccountID});
}
diff --git a/src/libs/actions/Task.ts b/src/libs/actions/Task.ts
index 0c3fc2b251a2..d3282dc3a923 100644
--- a/src/libs/actions/Task.ts
+++ b/src/libs/actions/Task.ts
@@ -127,6 +127,7 @@ function createTaskAndNavigate(
if (!parentReportID) {
return;
}
+
const optimisticTaskReport = ReportUtils.buildOptimisticTaskReport(currentUserAccountID, parentReportID, assigneeAccountID, title, description, policyID);
const assigneeChatReportID = assigneeChatReport?.reportID;
@@ -931,7 +932,11 @@ function startOutCreateTaskQuickAction(reportID: string, targetAccountID: number
/**
* Get the assignee data
*/
-function getAssignee(assigneeAccountID: number, personalDetails: OnyxEntry): Assignee {
+function getAssignee(assigneeAccountID: number | undefined, personalDetails: OnyxEntry): Assignee | undefined {
+ if (!assigneeAccountID) {
+ return;
+ }
+
const details = personalDetails?.[assigneeAccountID];
if (!details) {
diff --git a/src/pages/ErrorPage/UpdateRequiredView.tsx b/src/pages/ErrorPage/UpdateRequiredView.tsx
index c2d447da63e7..ac7a7237b0c8 100644
--- a/src/pages/ErrorPage/UpdateRequiredView.tsx
+++ b/src/pages/ErrorPage/UpdateRequiredView.tsx
@@ -26,7 +26,7 @@ function UpdateRequiredView() {
const isStandaloneNewAppProduction = isProduction && !CONFIG.IS_HYBRID_APP;
return (
-
+
diff --git a/src/pages/NewChatPage.tsx b/src/pages/NewChatPage.tsx
index f49b50da3c6a..6c14fcc0d237 100755
--- a/src/pages/NewChatPage.tsx
+++ b/src/pages/NewChatPage.tsx
@@ -22,8 +22,8 @@ import useLocalize from '@hooks/useLocalize';
import useNetwork from '@hooks/useNetwork';
import useResponsiveLayout from '@hooks/useResponsiveLayout';
import useSafeAreaInsets from '@hooks/useSafeAreaInsets';
+import useSafeAreaPaddings from '@hooks/useSafeAreaPaddings';
import useScreenWrapperTranstionStatus from '@hooks/useScreenWrapperTransitionStatus';
-import useStyledSafeAreaInsets from '@hooks/useStyledSafeAreaInsets';
import useThemeStyles from '@hooks/useThemeStyles';
import {navigateToAndOpenReport, searchInServer, setGroupDraft} from '@libs/actions/Report';
import {canUseTouchScreen} from '@libs/DeviceCapabilities';
@@ -148,7 +148,7 @@ function NewChatPage() {
const styles = useThemeStyles();
const personalData = useCurrentUserPersonalDetails();
const {top} = useSafeAreaInsets();
- const {insets, safeAreaPaddingBottomStyle} = useStyledSafeAreaInsets();
+ const {insets, safeAreaPaddingBottomStyle} = useSafeAreaPaddings();
const [isSearchingForReports] = useOnyx(ONYXKEYS.IS_SEARCHING_FOR_REPORTS, {initWithStoredValues: false});
const selectionListRef = useRef(null);
diff --git a/src/pages/settings/Profile/ProfilePage.tsx b/src/pages/settings/Profile/ProfilePage.tsx
index 06093b88e306..b305c483a586 100755
--- a/src/pages/settings/Profile/ProfilePage.tsx
+++ b/src/pages/settings/Profile/ProfilePage.tsx
@@ -17,8 +17,8 @@ import Section from '@components/Section';
import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails';
import useLocalize from '@hooks/useLocalize';
import useResponsiveLayout from '@hooks/useResponsiveLayout';
+import useSafeAreaPaddings from '@hooks/useSafeAreaPaddings';
import useScrollEnabled from '@hooks/useScrollEnabled';
-import useStyledSafeAreaInsets from '@hooks/useStyledSafeAreaInsets';
import useStyleUtils from '@hooks/useStyleUtils';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
@@ -39,7 +39,7 @@ function ProfilePage() {
const StyleUtils = useStyleUtils();
const {translate} = useLocalize();
const {shouldUseNarrowLayout} = useResponsiveLayout();
- const {safeAreaPaddingBottomStyle} = useStyledSafeAreaInsets();
+ const {safeAreaPaddingBottomStyle} = useSafeAreaPaddings();
const scrollEnabled = useScrollEnabled();
const [loginList] = useOnyx(ONYXKEYS.LOGIN_LIST);
const [privatePersonalDetails] = useOnyx(ONYXKEYS.PRIVATE_PERSONAL_DETAILS);
diff --git a/src/pages/settings/Wallet/ReportCardLostPage.tsx b/src/pages/settings/Wallet/ReportCardLostPage.tsx
index 52e5024209d4..64cb1c0422a8 100644
--- a/src/pages/settings/Wallet/ReportCardLostPage.tsx
+++ b/src/pages/settings/Wallet/ReportCardLostPage.tsx
@@ -11,18 +11,18 @@ import ValidateCodeActionModal from '@components/ValidateCodeActionModal';
import useBeforeRemove from '@hooks/useBeforeRemove';
import useLocalize from '@hooks/useLocalize';
import usePrevious from '@hooks/usePrevious';
-import useStyledSafeAreaInsets from '@hooks/useStyledSafeAreaInsets';
+import useSafeAreaPaddings from '@hooks/useSafeAreaPaddings';
import useThemeStyles from '@hooks/useThemeStyles';
+import {setErrors} from '@libs/actions/FormActions';
import {requestValidateCodeAction} from '@libs/actions/User';
-import * as ErrorUtils from '@libs/ErrorUtils';
+import {getLatestErrorMessageField} from '@libs/ErrorUtils';
import Navigation from '@libs/Navigation/Navigation';
import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types';
import type {SettingsNavigatorParamList} from '@libs/Navigation/types';
-import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils';
+import {getFormattedAddress} from '@libs/PersonalDetailsUtils';
import NotFoundPage from '@pages/ErrorPage/NotFoundPage';
+import {clearCardListErrors, requestReplacementExpensifyCard} from '@userActions/Card';
import type {ReplacementReason} from '@userActions/Card';
-import * as CardActions from '@userActions/Card';
-import * as FormActions from '@userActions/FormActions';
import type {TranslationPaths} from '@src/languages/types';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
@@ -74,14 +74,14 @@ function ReportCardLostPage({
const [shouldShowReasonError, setShouldShowReasonError] = useState(false);
const physicalCard = cardList?.[cardID];
- const validateError = ErrorUtils.getLatestErrorMessageField(physicalCard);
+ const validateError = getLatestErrorMessageField(physicalCard);
const [isValidateCodeActionModalVisible, setIsValidateCodeActionModalVisible] = useState(false);
const prevIsLoading = usePrevious(formData?.isLoading);
- const {paddingBottom} = useStyledSafeAreaInsets();
+ const {paddingBottom} = useSafeAreaPaddings();
- const formattedAddress = PersonalDetailsUtils.getFormattedAddress(privatePersonalDetails ?? {});
+ const formattedAddress = getFormattedAddress(privatePersonalDetails ?? {});
const primaryLogin = account?.primaryLogin ?? '';
useBeforeRemove(() => setIsValidateCodeActionModalVisible(false));
@@ -99,7 +99,7 @@ function ReportCardLostPage({
return;
}
- FormActions.setErrors(ONYXKEYS.FORMS.REPORT_PHYSICAL_CARD_FORM, physicalCard?.errors ?? {});
+ setErrors(ONYXKEYS.FORMS.REPORT_PHYSICAL_CARD_FORM, physicalCard?.errors ?? {});
}, [formData?.isLoading, physicalCard?.errors]);
const handleValidateCodeEntered = useCallback(
@@ -107,7 +107,7 @@ function ReportCardLostPage({
if (!physicalCard) {
return;
}
- CardActions.requestReplacementExpensifyCard(physicalCard.cardID, reason?.key as ReplacementReason, validateCode);
+ requestReplacementExpensifyCard(physicalCard.cardID, reason?.key as ReplacementReason, validateCode);
},
[physicalCard, reason?.key],
);
@@ -198,7 +198,7 @@ function ReportCardLostPage({
sendValidateCode={sendValidateCode}
validateError={validateError}
clearError={() => {
- CardActions.clearCardListErrors(physicalCard.cardID);
+ clearCardListErrors(physicalCard.cardID);
}}
onClose={() => setIsValidateCodeActionModalVisible(false)}
isVisible={isValidateCodeActionModalVisible}
diff --git a/src/pages/settings/Wallet/TransferBalancePage.tsx b/src/pages/settings/Wallet/TransferBalancePage.tsx
index 3d1a21d9cec1..9c573cacb40e 100644
--- a/src/pages/settings/Wallet/TransferBalancePage.tsx
+++ b/src/pages/settings/Wallet/TransferBalancePage.tsx
@@ -1,7 +1,6 @@
import React, {useEffect} from 'react';
import {View} from 'react-native';
-import type {OnyxEntry} from 'react-native-onyx';
-import {withOnyx} from 'react-native-onyx';
+import {useOnyx} from 'react-native-onyx';
import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView';
import ConfirmationPage from '@components/ConfirmationPage';
import CurrentWalletBalance from '@components/CurrentWalletBalance';
@@ -14,45 +13,39 @@ import ScrollView from '@components/ScrollView';
import Text from '@components/Text';
import useLocalize from '@hooks/useLocalize';
import useNetwork from '@hooks/useNetwork';
-import useStyledSafeAreaInsets from '@hooks/useStyledSafeAreaInsets';
+import useSafeAreaPaddings from '@hooks/useSafeAreaPaddings';
import useThemeStyles from '@hooks/useThemeStyles';
-import * as CurrencyUtils from '@libs/CurrencyUtils';
-import * as ErrorUtils from '@libs/ErrorUtils';
+import {
+ dismissSuccessfulTransferBalancePage,
+ resetWalletTransferData,
+ saveWalletTransferAccountTypeAndID,
+ saveWalletTransferMethodType,
+ transferWalletBalance,
+} from '@libs/actions/PaymentMethods';
+import {convertToDisplayString} from '@libs/CurrencyUtils';
+import {getLatestErrorMessage} from '@libs/ErrorUtils';
import Navigation from '@libs/Navigation/Navigation';
-import * as PaymentUtils from '@libs/PaymentUtils';
+import {calculateWalletTransferBalanceFee, formatPaymentMethods, hasExpensifyPaymentMethod} from '@libs/PaymentUtils';
import variables from '@styles/variables';
-import * as PaymentMethods from '@userActions/PaymentMethods';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
-import type {BankAccountList, FundList, UserWallet, WalletTransfer} from '@src/types/onyx';
import type PaymentMethod from '@src/types/onyx/PaymentMethod';
import type {FilterMethodPaymentType} from '@src/types/onyx/WalletTransfer';
import {isEmptyObject} from '@src/types/utils/EmptyObject';
-type TransferBalancePageOnyxProps = {
- /** User's wallet information */
- userWallet: OnyxEntry;
-
- /** List of bank accounts */
- bankAccountList: OnyxEntry;
-
- /** List of user's card objects */
- fundList: OnyxEntry;
-
- /** Wallet balance transfer props */
- walletTransfer: OnyxEntry;
-};
-
-type TransferBalancePageProps = TransferBalancePageOnyxProps;
-
const TRANSFER_TIER_NAMES: string[] = [CONST.WALLET.TIER_NAME.GOLD, CONST.WALLET.TIER_NAME.PLATINUM];
-function TransferBalancePage({bankAccountList, fundList, userWallet, walletTransfer}: TransferBalancePageProps) {
+function TransferBalancePage() {
const styles = useThemeStyles();
const {numberFormat, translate} = useLocalize();
const {isOffline} = useNetwork();
- const {paddingBottom} = useStyledSafeAreaInsets();
+ const {paddingBottom} = useSafeAreaPaddings();
+
+ const [userWallet] = useOnyx(ONYXKEYS.USER_WALLET);
+ const [walletTransfer] = useOnyx(ONYXKEYS.WALLET_TRANSFER);
+ const [bankAccountList] = useOnyx(ONYXKEYS.BANK_ACCOUNT_LIST);
+ const [fundList] = useOnyx(ONYXKEYS.FUND_LIST);
const paymentCardList = fundList ?? {};
const paymentTypes = [
@@ -61,7 +54,7 @@ function TransferBalancePage({bankAccountList, fundList, userWallet, walletTrans
title: translate('transferAmountPage.instant'),
description: translate('transferAmountPage.instantSummary', {
rate: numberFormat(CONST.WALLET.TRANSFER_METHOD_TYPE_FEE.INSTANT.RATE),
- minAmount: CurrencyUtils.convertToDisplayString(CONST.WALLET.TRANSFER_METHOD_TYPE_FEE.INSTANT.MINIMUM_FEE),
+ minAmount: convertToDisplayString(CONST.WALLET.TRANSFER_METHOD_TYPE_FEE.INSTANT.MINIMUM_FEE),
}),
icon: Expensicons.Bolt,
type: CONST.PAYMENT_METHODS.DEBIT_CARD,
@@ -79,7 +72,7 @@ function TransferBalancePage({bankAccountList, fundList, userWallet, walletTrans
* Get the selected/default payment method account for wallet transfer
*/
function getSelectedPaymentMethodAccount(): PaymentMethod | undefined {
- const paymentMethods = PaymentUtils.formatPaymentMethods(bankAccountList ?? {}, paymentCardList, styles);
+ const paymentMethods = formatPaymentMethods(bankAccountList ?? {}, paymentCardList, styles);
const defaultAccount = paymentMethods.find((method) => method.isDefault);
const selectedAccount = paymentMethods.find(
@@ -89,15 +82,15 @@ function TransferBalancePage({bankAccountList, fundList, userWallet, walletTrans
}
function navigateToChooseTransferAccount(filterPaymentMethodType: FilterMethodPaymentType) {
- PaymentMethods.saveWalletTransferMethodType(filterPaymentMethodType);
+ saveWalletTransferMethodType(filterPaymentMethodType);
// If we only have a single option for the given paymentMethodType do not force the user to make a selection
- const combinedPaymentMethods = PaymentUtils.formatPaymentMethods(bankAccountList ?? {}, paymentCardList, styles);
+ const combinedPaymentMethods = formatPaymentMethods(bankAccountList ?? {}, paymentCardList, styles);
const filteredMethods = combinedPaymentMethods.filter((paymentMethod) => paymentMethod.accountType === filterPaymentMethodType);
if (filteredMethods.length === 1) {
const account = filteredMethods.at(0);
- PaymentMethods.saveWalletTransferAccountTypeAndID(filterPaymentMethodType ?? '', account?.methodID?.toString() ?? '-1');
+ saveWalletTransferAccountTypeAndID(filterPaymentMethodType, account?.methodID?.toString());
return;
}
@@ -106,14 +99,14 @@ function TransferBalancePage({bankAccountList, fundList, userWallet, walletTrans
useEffect(() => {
// Reset to the default account when the page is opened
- PaymentMethods.resetWalletTransferData();
+ resetWalletTransferData();
const selectedAccount = getSelectedPaymentMethodAccount();
if (!selectedAccount) {
return;
}
- PaymentMethods.saveWalletTransferAccountTypeAndID(selectedAccount?.accountType ?? '', selectedAccount?.methodID?.toString() ?? '-1');
+ saveWalletTransferAccountTypeAndID(selectedAccount?.accountType, selectedAccount?.methodID?.toString());
// eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps -- we only want this effect to run on initial render
}, []);
@@ -122,7 +115,7 @@ function TransferBalancePage({bankAccountList, fundList, userWallet, walletTrans
);
@@ -143,13 +136,13 @@ function TransferBalancePage({bankAccountList, fundList, userWallet, walletTrans
const selectedPaymentType =
selectedAccount && selectedAccount.accountType === CONST.PAYMENT_METHODS.PERSONAL_BANK_ACCOUNT ? CONST.WALLET.TRANSFER_METHOD_TYPE.ACH : CONST.WALLET.TRANSFER_METHOD_TYPE.INSTANT;
- const calculatedFee = PaymentUtils.calculateWalletTransferBalanceFee(userWallet?.currentBalance ?? 0, selectedPaymentType);
+ const calculatedFee = calculateWalletTransferBalanceFee(userWallet?.currentBalance ?? 0, selectedPaymentType);
const transferAmount = userWallet?.currentBalance ?? 0 - calculatedFee;
const isTransferable = transferAmount > 0;
const isButtonDisabled = !isTransferable || !selectedAccount;
- const errorMessage = ErrorUtils.getLatestErrorMessage(walletTransfer);
+ const errorMessage = getLatestErrorMessage(walletTransfer);
- const shouldShowTransferView = PaymentUtils.hasExpensifyPaymentMethod(paymentCardList, bankAccountList ?? {}) && TRANSFER_TIER_NAMES.includes(userWallet?.tierName ?? '');
+ const shouldShowTransferView = hasExpensifyPaymentMethod(paymentCardList, bankAccountList ?? {}) && TRANSFER_TIER_NAMES.includes(userWallet?.tierName ?? '');
return (
@@ -206,16 +199,16 @@ function TransferBalancePage({bankAccountList, fundList, userWallet, walletTrans
)}
{translate('transferAmountPage.fee')}
- {CurrencyUtils.convertToDisplayString(calculatedFee)}
+ {convertToDisplayString(calculatedFee)}
selectedAccount && PaymentMethods.transferWalletBalance(selectedAccount)}
+ onSubmit={() => selectedAccount && transferWalletBalance(selectedAccount)}
isDisabled={isButtonDisabled || isOffline}
message={errorMessage}
isAlertVisible={!isEmptyObject(errorMessage)}
@@ -229,17 +222,4 @@ function TransferBalancePage({bankAccountList, fundList, userWallet, walletTrans
TransferBalancePage.displayName = 'TransferBalancePage';
-export default withOnyx({
- userWallet: {
- key: ONYXKEYS.USER_WALLET,
- },
- walletTransfer: {
- key: ONYXKEYS.WALLET_TRANSFER,
- },
- bankAccountList: {
- key: ONYXKEYS.BANK_ACCOUNT_LIST,
- },
- fundList: {
- key: ONYXKEYS.FUND_LIST,
- },
-})(TransferBalancePage);
+export default TransferBalancePage;
diff --git a/src/pages/signin/SignInPage.tsx b/src/pages/signin/SignInPage.tsx
index 221ca93701be..44fd8776b43f 100644
--- a/src/pages/signin/SignInPage.tsx
+++ b/src/pages/signin/SignInPage.tsx
@@ -302,7 +302,7 @@ function SignInPage({shouldEnableMaxHeight = true}: SignInPageInnerProps, ref: F
isEmptyObject(parentReport) || isAllowedToComment(parentReport), [parentReport]);
- const {paddingBottom} = useStyledSafeAreaInsets();
+ const {paddingBottom} = useSafeAreaPaddings();
const backTo = route.params?.backTo;
const confirmButtonRef = useRef(null);
diff --git a/src/pages/workspace/WorkspaceMoreFeaturesPage.tsx b/src/pages/workspace/WorkspaceMoreFeaturesPage.tsx
index 6cf2a73d3ece..e156aac0c465 100644
--- a/src/pages/workspace/WorkspaceMoreFeaturesPage.tsx
+++ b/src/pages/workspace/WorkspaceMoreFeaturesPage.tsx
@@ -11,7 +11,7 @@ import Text from '@components/Text';
import useLocalize from '@hooks/useLocalize';
import useNetwork from '@hooks/useNetwork';
import useResponsiveLayout from '@hooks/useResponsiveLayout';
-import useStyledSafeAreaInsets from '@hooks/useStyledSafeAreaInsets';
+import useSafeAreaPaddings from '@hooks/useSafeAreaPaddings';
import useStyleUtils from '@hooks/useStyleUtils';
import useThemeStyles from '@hooks/useThemeStyles';
import {getCompanyFeeds} from '@libs/CardUtils';
@@ -75,7 +75,7 @@ function WorkspaceMoreFeaturesPage({policy, route}: WorkspaceMoreFeaturesPagePro
const styles = useThemeStyles();
const stylesutils = useStyleUtils();
const {shouldUseNarrowLayout} = useResponsiveLayout();
- const {safeAreaPaddingBottomStyle} = useStyledSafeAreaInsets();
+ const {safeAreaPaddingBottomStyle} = useSafeAreaPaddings();
const {translate} = useLocalize();
const hasAccountingConnection = !isEmptyObject(policy?.connections);
const isAccountingEnabled = !!policy?.areConnectionsEnabled || !isEmptyObject(policy?.connections);
diff --git a/src/stories/SelectionList.stories.tsx b/src/stories/SelectionList.stories.tsx
index b3dc4c5ae2d2..e993d23dc0ba 100644
--- a/src/stories/SelectionList.stories.tsx
+++ b/src/stories/SelectionList.stories.tsx
@@ -3,7 +3,7 @@ import React, {useMemo, useState} from 'react';
import Badge from '@components/Badge';
import SelectionList from '@components/SelectionList';
import RadioListItem from '@components/SelectionList/RadioListItem';
-import type {BaseSelectionListProps, ListItem} from '@components/SelectionList/types';
+import type {ListItem, SelectionListProps} from '@components/SelectionList/types';
import withNavigationFallback from '@components/withNavigationFallback';
// eslint-disable-next-line no-restricted-imports
import {defaultStyles} from '@styles/index';
@@ -71,7 +71,7 @@ const SECTIONS = [
},
];
-function Default(props: BaseSelectionListProps) {
+function Default(props: SelectionListProps) {
const [selectedIndex, setSelectedIndex] = useState(1);
const sections = props.sections.map((section) => {
@@ -110,7 +110,7 @@ Default.args = {
initiallyFocusedOptionKey: 'option-2',
};
-function WithTextInput(props: BaseSelectionListProps) {
+function WithTextInput(props: SelectionListProps) {
const [searchText, setSearchText] = useState('');
const [selectedIndex, setSelectedIndex] = useState(1);
@@ -162,7 +162,7 @@ WithTextInput.args = {
onChangeText: () => {},
};
-function WithHeaderMessage(props: BaseSelectionListProps) {
+function WithHeaderMessage(props: SelectionListProps) {
return (
) {
+function WithAlternateText(props: SelectionListProps) {
const [selectedIndex, setSelectedIndex] = useState(1);
const sections = props.sections.map((section) => {
@@ -218,7 +218,7 @@ WithAlternateText.args = {
...Default.args,
};
-function MultipleSelection(props: BaseSelectionListProps) {
+function MultipleSelection(props: SelectionListProps) {
const [selectedIds, setSelectedIds] = useState(['option-1', 'option-2']);
const memo = useMemo(() => {
@@ -288,7 +288,7 @@ MultipleSelection.args = {
onSelectAll: () => {},
};
-function WithSectionHeader(props: BaseSelectionListProps) {
+function WithSectionHeader(props: SelectionListProps) {
const [selectedIds, setSelectedIds] = useState(['option-1', 'option-2']);
const memo = useMemo(() => {
@@ -356,7 +356,7 @@ WithSectionHeader.args = {
...MultipleSelection.args,
};
-function WithConfirmButton(props: BaseSelectionListProps) {
+function WithConfirmButton(props: SelectionListProps) {
const [selectedIds, setSelectedIds] = useState(['option-1', 'option-2']);
const memo = useMemo(() => {
diff --git a/src/styles/index.ts b/src/styles/index.ts
index c95b126d1278..6650743c5659 100644
--- a/src/styles/index.ts
+++ b/src/styles/index.ts
@@ -795,6 +795,13 @@ const styles = (theme: ThemeColors) =>
color: theme.textLight,
},
+ buttonBlendContainer: {
+ backgroundColor: theme.appBG,
+ opacity: 1,
+ position: 'relative',
+ overflow: 'hidden',
+ },
+
hoveredComponentBG: {
backgroundColor: theme.hoverComponentBG,
},
diff --git a/src/styles/theme/themes/dark.ts b/src/styles/theme/themes/dark.ts
index 4d5894812195..725966d487a8 100644
--- a/src/styles/theme/themes/dark.ts
+++ b/src/styles/theme/themes/dark.ts
@@ -154,6 +154,7 @@ const darkTheme = {
statusBarStyle: CONST.STATUS_BAR_STYLE.LIGHT_CONTENT,
navigationBarButtonsStyle: CONST.NAVIGATION_BAR_BUTTONS_STYLE.LIGHT,
+ navigationBarBackgroundColor: `${colors.productDark100}CD`, // CD is 80% opacity (80% of 0xFF)
colorScheme: CONST.COLOR_SCHEME.DARK,
} satisfies ThemeColors;
diff --git a/src/styles/theme/themes/light.ts b/src/styles/theme/themes/light.ts
index 1a187e85b0a2..add3bf183a42 100644
--- a/src/styles/theme/themes/light.ts
+++ b/src/styles/theme/themes/light.ts
@@ -154,6 +154,7 @@ const lightTheme = {
statusBarStyle: CONST.STATUS_BAR_STYLE.DARK_CONTENT,
navigationBarButtonsStyle: CONST.NAVIGATION_BAR_BUTTONS_STYLE.DARK,
+ navigationBarBackgroundColor: `${colors.productLight100}CD`, // CD is 80% opacity (80% of 0xFF)
colorScheme: CONST.COLOR_SCHEME.LIGHT,
} satisfies ThemeColors;
diff --git a/src/styles/theme/types.ts b/src/styles/theme/types.ts
index 9dfb26e611d9..51de2a662f4a 100644
--- a/src/styles/theme/types.ts
+++ b/src/styles/theme/types.ts
@@ -109,6 +109,7 @@ type ThemeColors = {
// e.g. the StatusBar displays either "light-content" or "dark-content" based on the theme
statusBarStyle: StatusBarStyle;
navigationBarButtonsStyle: NavBarButtonStyle;
+ navigationBarBackgroundColor: Color;
colorScheme: ColorScheme;
};
diff --git a/src/styles/utils/getNavigationBarType/index.android.ts b/src/styles/utils/getNavigationBarType/index.android.ts
new file mode 100644
index 000000000000..71a081010ddd
--- /dev/null
+++ b/src/styles/utils/getNavigationBarType/index.android.ts
@@ -0,0 +1,17 @@
+import NavBarManager from '@libs/NavBarManager';
+import CONST from '@src/CONST';
+import type GetNavigationBarType from './types';
+
+const getNavigationBarType: GetNavigationBarType = (insets) => {
+ const bottomInset = insets?.bottom ?? 0;
+
+ // If the bottom safe area inset is 0, we consider the device to have no navigation bar (or it being hidden by default).
+ // This could be mean either hidden soft keys, gesture navigation without a gesture bar or physical buttons.
+ if (bottomInset === 0) {
+ return CONST.NAVIGATION_BAR_TYPE.NONE;
+ }
+
+ return NavBarManager.getType();
+};
+
+export default getNavigationBarType;
diff --git a/src/styles/utils/getNavigationBarType/index.ios.ts b/src/styles/utils/getNavigationBarType/index.ios.ts
new file mode 100644
index 000000000000..1931ab0f85bb
--- /dev/null
+++ b/src/styles/utils/getNavigationBarType/index.ios.ts
@@ -0,0 +1,16 @@
+import CONST from '@src/CONST';
+import type GetNavigationBarType from './types';
+
+const getNavigationBarType: GetNavigationBarType = (insets) => {
+ const bottomInset = insets?.bottom ?? 0;
+
+ // If there is no bottom safe area inset, the device uses a physical navigation button.
+ if (bottomInset === 0) {
+ return CONST.NAVIGATION_BAR_TYPE.NONE;
+ }
+
+ // On iOS, if there is a bottom safe area inset, it means the device uses a gesture bar.
+ return CONST.NAVIGATION_BAR_TYPE.GESTURE_BAR;
+};
+
+export default getNavigationBarType;
diff --git a/src/styles/utils/getNavigationBarType/index.ts b/src/styles/utils/getNavigationBarType/index.ts
new file mode 100644
index 000000000000..32ea2065e938
--- /dev/null
+++ b/src/styles/utils/getNavigationBarType/index.ts
@@ -0,0 +1,9 @@
+import CONST from '@src/CONST';
+import type GetNavigationBarType from './types';
+
+const getNavigationBarType: GetNavigationBarType = () => {
+ // On web, there is no navigation bar.
+ return CONST.NAVIGATION_BAR_TYPE.NONE;
+};
+
+export default getNavigationBarType;
diff --git a/src/styles/utils/getNavigationBarType/types.ts b/src/styles/utils/getNavigationBarType/types.ts
new file mode 100644
index 000000000000..f0b30f4a2a72
--- /dev/null
+++ b/src/styles/utils/getNavigationBarType/types.ts
@@ -0,0 +1,6 @@
+import type {EdgeInsets} from 'react-native-safe-area-context';
+import type {NavigationBarType} from '@libs/NavBarManager/types';
+
+type GetNavigationBarType = (insets?: EdgeInsets) => NavigationBarType;
+
+export default GetNavigationBarType;
diff --git a/src/styles/utils/index.ts b/src/styles/utils/index.ts
index 39d5e5ca959a..b6ee3085d981 100644
--- a/src/styles/utils/index.ts
+++ b/src/styles/utils/index.ts
@@ -24,6 +24,7 @@ import createReportActionContextMenuStyleUtils from './generators/ReportActionCo
import createTooltipStyleUtils from './generators/TooltipStyleUtils';
import getContextMenuItemStyles from './getContextMenuItemStyles';
import getHighResolutionInfoWrapperStyle from './getHighResolutionInfoWrapperStyle';
+import getNavigationBarType from './getNavigationBarType/index';
import getNavigationModalCardStyle from './getNavigationModalCardStyles';
import getSafeAreaInsets from './getSafeAreaInsets';
import getSignInBgStyles from './getSignInBgStyles';
@@ -331,10 +332,11 @@ type SafeAreaPadding = {
};
/**
- * Takes safe area insets and returns padding to use for a View
+ * Takes safe area insets and returns platform specific padding to use for a View
*/
-function getSafeAreaPadding(insets?: EdgeInsets, insetsPercentageProp?: number): SafeAreaPadding {
+function getPlatformSafeAreaPadding(insets?: EdgeInsets, insetsPercentageProp?: number): SafeAreaPadding {
const platform = getPlatform();
+
let insetsPercentage = insetsPercentageProp;
if (insetsPercentage == null) {
switch (platform) {
@@ -1231,7 +1233,7 @@ const staticStyleUtils = {
getPaymentMethodMenuWidth,
getSafeAreaInsets,
getSafeAreaMargins,
- getSafeAreaPadding,
+ getPlatformSafeAreaPadding,
getSignInWordmarkWidthStyle,
getTextColorStyle,
getTransparentColor,
@@ -1257,6 +1259,7 @@ const staticStyleUtils = {
getBorderRadiusStyle,
getHighResolutionInfoWrapperStyle,
getItemBackgroundColorStyle,
+ getNavigationBarType,
};
const createStyleUtils = (theme: ThemeColors, styles: ThemeStyles) => ({
diff --git a/src/types/modules/react-native.d.ts b/src/types/modules/react-native.d.ts
index b5abe4f587a3..0b09824509b5 100644
--- a/src/types/modules/react-native.d.ts
+++ b/src/types/modules/react-native.d.ts
@@ -2,6 +2,7 @@
import type {TargetedEvent} from 'react-native';
import type {BootSplashModule} from '@libs/BootSplash/types';
import type {EnvironmentCheckerModule} from '@libs/Environment/betaChecker/types';
+import type {NavBarButtonStyle, NavigationBarType} from '@libs/NavBarManager/types';
import type {ShortcutManagerModule} from '@libs/ShortcutManager';
import type StartupTimer from '@libs/StartupTimer/types';
@@ -10,7 +11,8 @@ type RNTextInputResetModule = {
};
type RNNavBarManagerModule = {
- setButtonStyle: (style: 'light' | 'dark') => void;
+ setButtonStyle: (style: NavBarButtonStyle) => void;
+ getType(): NavigationBarType;
};
declare module 'react-native' {
diff --git a/tests/unit/BaseSelectionListTest.tsx b/tests/unit/BaseSelectionListTest.tsx
index 6589eec7db80..ba74a5fb6e08 100644
--- a/tests/unit/BaseSelectionListTest.tsx
+++ b/tests/unit/BaseSelectionListTest.tsx
@@ -3,12 +3,12 @@ import {fireEvent, render, screen} from '@testing-library/react-native';
import {SectionList} from 'react-native';
import BaseSelectionList from '@components/SelectionList/BaseSelectionList';
import RadioListItem from '@components/SelectionList/RadioListItem';
-import type {BaseSelectionListProps, ListItem} from '@components/SelectionList/types';
+import type {ListItem, SelectionListProps} from '@components/SelectionList/types';
import type Navigation from '@libs/Navigation/Navigation';
import CONST from '@src/CONST';
type BaseSelectionListSections = {
- sections: BaseSelectionListProps['sections'];
+ sections: SelectionListProps['sections'];
canSelectMultiple?: boolean;
};