diff --git a/src/components/withTabAnimation.js b/src/components/withTabAnimation.js
deleted file mode 100644
index 2af96f0215a3..000000000000
--- a/src/components/withTabAnimation.js
+++ /dev/null
@@ -1,72 +0,0 @@
-import {useTabAnimation} from '@react-navigation/material-top-tabs';
-import PropTypes from 'prop-types';
-import * as React from 'react';
-import getComponentDisplayName from '@libs/getComponentDisplayName';
-import refPropTypes from './refPropTypes';
-
-const propTypes = {
- /** The HOC takes an optional ref as a prop and passes it as a ref to the wrapped component.
- * That way, if a ref is passed to a component wrapped in the HOC, the ref is a reference to the wrapped component, not the HOC. */
- forwardedRef: refPropTypes,
-
- /* Whether we're in a tab navigator */
- isInTabNavigator: PropTypes.bool.isRequired,
-};
-
-const defaultProps = {
- forwardedRef: () => {},
-};
-
-export default function (WrappedComponent) {
- // The component with tab animation prop
- function WrappedComponentWithTabAnimation(props) {
- const animation = useTabAnimation();
-
- return (
-
- );
- }
-
- WrappedComponentWithTabAnimation.displayName = `withAnimation(${getComponentDisplayName(WrappedComponent)})`;
-
- // Return a component with tab animation prop if this component is in tab navigator, otherwise return itself
- function WithTabAnimation({forwardedRef, ...rest}) {
- if (rest.isInTabNavigator) {
- return (
-
- );
- }
- return (
-
- );
- }
-
- WithTabAnimation.propTypes = propTypes;
- WithTabAnimation.defaultProps = defaultProps;
- WithTabAnimation.displayName = `withTabAnimation(${getComponentDisplayName(WrappedComponent)})`;
-
- // eslint-disable-next-line rulesdir/no-negated-variables
- const WithTabAnimationWithRef = React.forwardRef((props, ref) => (
-
- ));
-
- WithTabAnimationWithRef.displayName = `withTabAnimationWithRef(${getComponentDisplayName(WrappedComponent)})`;
-
- return WithTabAnimationWithRef;
-}
diff --git a/src/hooks/useTabNavigatorFocus/index.js b/src/hooks/useTabNavigatorFocus/index.js
new file mode 100644
index 000000000000..f83ec5bd9270
--- /dev/null
+++ b/src/hooks/useTabNavigatorFocus/index.js
@@ -0,0 +1,79 @@
+import {useTabAnimation} from '@react-navigation/material-top-tabs';
+import {useIsFocused} from '@react-navigation/native';
+import {useEffect, useState} from 'react';
+import DomUtils from '@libs/DomUtils';
+
+/**
+ * Custom React hook to determine the focus status of a tab in a Material Top Tab Navigator.
+ * It evaluates whether the current tab is focused based on the tab's animation position and
+ * the screen's focus status within a React Navigation environment.
+ *
+ * This hook is designed for use with the Material Top Tabs provided by '@react-navigation/material-top-tabs'.
+ * It leverages the `useTabAnimation` hook from the same package to track the animated position of tabs
+ * and the `useIsFocused` hook from '@react-navigation/native' to ascertain if the current screen is in focus.
+ *
+ * Note: This hook contains a conditional invocation of another hook (`useTabAnimation`),
+ * which is typically an anti-pattern in React. This is done to account for scenarios where the hook
+ * might not be used within a Material Top Tabs Navigator context. Proper usage should ensure that
+ * this hook is only used where appropriate.
+ *
+ * @param {Object} params - The parameters object.
+ * @param {Number} params.tabIndex - The index of the tab for which focus status is being determined.
+ * @returns {Boolean} Returns `true` if the tab is both animation-focused and screen-focused, otherwise `false`.
+ *
+ * @example
+ * const isTabFocused = useTabNavigatorFocus({ tabIndex: 1 });
+ */
+function useTabNavigatorFocus({tabIndex}) {
+ let tabPositionAnimation = null;
+ try {
+ // Retrieve the animation value from the tab navigator, which ranges from 0 to the total number of pages displayed.
+ // Even a minimal scroll towards the camera page (e.g., a value of 0.001 at start) should activate the camera for immediate responsiveness.
+ // STOP!!!!!!! This is not a pattern to be followed! We are conditionally rendering this hook becase when used in the edit flow we'll never be inside a tab navigator.
+ // eslint-disable-next-line react-hooks/rules-of-hooks
+ tabPositionAnimation = useTabAnimation();
+ } catch (error) {
+ tabPositionAnimation = null;
+ }
+ const isPageFocused = useIsFocused();
+ // set to true if the hook is not used within the MaterialTopTabs context
+ // the hook will then return true if the screen is focused
+ const [isTabFocused, setIsTabFocused] = useState(!tabPositionAnimation);
+
+ useEffect(() => {
+ if (!tabPositionAnimation) {
+ return;
+ }
+ const index = Number(tabIndex);
+
+ const listenerId = tabPositionAnimation.addListener(({value}) => {
+ // Activate camera as soon the index is animating towards the `tabIndex`
+ DomUtils.requestAnimationFrame(() => {
+ setIsTabFocused(value > index - 1 && value < index + 1);
+ });
+ });
+
+ // We need to get the position animation value on component initialization to determine
+ // if the tab is focused or not. Since it's an Animated.Value the only synchronous way
+ // to retrieve the value is to use a private method.
+ // eslint-disable-next-line no-underscore-dangle
+ const initialTabPositionValue = tabPositionAnimation.__getValue();
+
+ if (typeof initialTabPositionValue === 'number') {
+ DomUtils.requestAnimationFrame(() => {
+ setIsTabFocused(initialTabPositionValue > index - 1 && initialTabPositionValue < index + 1);
+ });
+ }
+
+ return () => {
+ if (!tabPositionAnimation) {
+ return;
+ }
+ tabPositionAnimation.removeListener(listenerId);
+ };
+ }, [tabIndex, tabPositionAnimation]);
+
+ return isTabFocused && isPageFocused;
+}
+
+export default useTabNavigatorFocus;
diff --git a/src/libs/DomUtils/index.native.ts b/src/libs/DomUtils/index.native.ts
index 9a9758228776..0864f1a16ac0 100644
--- a/src/libs/DomUtils/index.native.ts
+++ b/src/libs/DomUtils/index.native.ts
@@ -2,6 +2,15 @@ import GetActiveElement from './types';
const getActiveElement: GetActiveElement = () => null;
+const requestAnimationFrame = (callback: () => void) => {
+ if (!callback) {
+ return;
+ }
+
+ callback();
+};
+
export default {
getActiveElement,
+ requestAnimationFrame,
};
diff --git a/src/libs/DomUtils/index.ts b/src/libs/DomUtils/index.ts
index 94dd54547454..6a2eed57fbe6 100644
--- a/src/libs/DomUtils/index.ts
+++ b/src/libs/DomUtils/index.ts
@@ -4,4 +4,5 @@ const getActiveElement: GetActiveElement = () => document.activeElement;
export default {
getActiveElement,
+ requestAnimationFrame: window.requestAnimationFrame.bind(window),
};
diff --git a/src/pages/iou/ReceiptSelector/NavigationAwareCamera/index.js b/src/pages/iou/ReceiptSelector/NavigationAwareCamera/index.js
index e4b24f8a0ad8..10b16da13b6e 100644
--- a/src/pages/iou/ReceiptSelector/NavigationAwareCamera/index.js
+++ b/src/pages/iou/ReceiptSelector/NavigationAwareCamera/index.js
@@ -1,17 +1,20 @@
-import {useIsFocused} from '@react-navigation/native';
import PropTypes from 'prop-types';
import React, {useEffect, useRef} from 'react';
import {View} from 'react-native';
import Webcam from 'react-webcam';
+import useTabNavigatorFocus from '@hooks/useTabNavigatorFocus';
const propTypes = {
- /* Flag to turn on/off the torch/flashlight - if available */
+ /** Flag to turn on/off the torch/flashlight - if available */
torchOn: PropTypes.bool,
- /* Callback function when media stream becomes available - user granted camera permissions and camera starts to work */
+ /** The index of the tab that contains this camera */
+ cameraTabIndex: PropTypes.number.isRequired,
+
+ /** Callback function when media stream becomes available - user granted camera permissions and camera starts to work */
onUserMedia: PropTypes.func,
- /* Callback function passing torch/flashlight capability as bool param of the browser */
+ /** Callback function passing torch/flashlight capability as bool param of the browser */
onTorchAvailability: PropTypes.func,
};
@@ -22,9 +25,11 @@ const defaultProps = {
};
// Wraps a camera that will only be active when the tab is focused or as soon as it starts to become focused.
-const NavigationAwareCamera = React.forwardRef(({torchOn, onTorchAvailability, ...props}, ref) => {
+const NavigationAwareCamera = React.forwardRef(({torchOn, onTorchAvailability, cameraTabIndex, ...props}, ref) => {
const trackRef = useRef(null);
- const isCameraActive = useIsFocused();
+ const shouldShowCamera = useTabNavigatorFocus({
+ tabIndex: cameraTabIndex,
+ });
const handleOnUserMedia = (stream) => {
if (props.onUserMedia) {
@@ -51,7 +56,7 @@ const NavigationAwareCamera = React.forwardRef(({torchOn, onTorchAvailability, .
});
}, [torchOn]);
- if (!isCameraActive) {
+ if (!shouldShowCamera) {
return null;
}
return (
diff --git a/src/pages/iou/ReceiptSelector/NavigationAwareCamera/index.native.js b/src/pages/iou/ReceiptSelector/NavigationAwareCamera/index.native.js
index eca8042a6965..65c17d3cb7ab 100644
--- a/src/pages/iou/ReceiptSelector/NavigationAwareCamera/index.native.js
+++ b/src/pages/iou/ReceiptSelector/NavigationAwareCamera/index.native.js
@@ -1,77 +1,16 @@
-import {useNavigation} from '@react-navigation/native';
import PropTypes from 'prop-types';
-import React, {useEffect, useState} from 'react';
+import React from 'react';
import {Camera} from 'react-native-vision-camera';
-import withTabAnimation from '@components/withTabAnimation';
-import CONST from '@src/CONST';
+import useTabNavigatorFocus from '@hooks/useTabNavigatorFocus';
const propTypes = {
/* The index of the tab that contains this camera */
cameraTabIndex: PropTypes.number.isRequired,
-
- /* Whether we're in a tab navigator */
- isInTabNavigator: PropTypes.bool.isRequired,
-
- /** Name of the selected receipt tab */
- selectedTab: PropTypes.string.isRequired,
-
- /** The tab animation from hook */
- tabAnimation: PropTypes.shape({
- addListener: PropTypes.func,
- removeListener: PropTypes.func,
- }),
-};
-
-const defaultProps = {
- tabAnimation: undefined,
};
// Wraps a camera that will only be active when the tab is focused or as soon as it starts to become focused.
-const NavigationAwareCamera = React.forwardRef(({cameraTabIndex, isInTabNavigator, selectedTab, tabAnimation, ...props}, ref) => {
- // Get navigation to get initial isFocused value (only needed once during init!)
- const navigation = useNavigation();
- const [isCameraActive, setIsCameraActive] = useState(() => navigation.isFocused());
-
- // Retrieve the animation value from the tab navigator, which ranges from 0 to the total number of pages displayed.
- // Even a minimal scroll towards the camera page (e.g., a value of 0.001 at start) should activate the camera for immediate responsiveness.
-
- useEffect(() => {
- if (!isInTabNavigator) {
- return;
- }
-
- const listenerId = tabAnimation.addListener(({value}) => {
- if (selectedTab !== CONST.TAB.SCAN) {
- return;
- }
- // Activate camera as soon the index is animating towards the `cameraTabIndex`
- setIsCameraActive(value > cameraTabIndex - 1 && value < cameraTabIndex + 1);
- });
-
- return () => {
- tabAnimation.removeListener(listenerId);
- };
- }, [cameraTabIndex, tabAnimation, isInTabNavigator, selectedTab]);
-
- // Note: The useEffect can be removed once VisionCamera V3 is used.
- // Its only needed for android, because there is a native cameraX android bug. With out this flow would break the camera:
- // 1. Open camera tab
- // 2. Take a picture
- // 3. Go back from the opened screen
- // 4. The camera is not working anymore
- useEffect(() => {
- const removeBlurListener = navigation.addListener('blur', () => {
- setIsCameraActive(false);
- });
- const removeFocusListener = navigation.addListener('focus', () => {
- setIsCameraActive(true);
- });
-
- return () => {
- removeBlurListener();
- removeFocusListener();
- };
- }, [navigation]);
+const NavigationAwareCamera = React.forwardRef(({cameraTabIndex, ...props}, ref) => {
+ const isCameraActive = useTabNavigatorFocus({tabIndex: cameraTabIndex});
return (
!state, false);
- const [isTorchAvailable, setIsTorchAvailable] = useState(true);
+ const [isTorchAvailable, setIsTorchAvailable] = useState(false);
const cameraRef = useRef(null);
const hideReciptModal = () => {
@@ -200,6 +196,7 @@ function ReceiptSelector({route, transactionID, iou, report}) {
torchOn={isFlashLightOn}
onTorchAvailability={setIsTorchAvailable}
forceScreenshotSourceSize
+ cameraTabIndex={pageIndex}
/>
diff --git a/src/pages/iou/ReceiptSelector/index.native.js b/src/pages/iou/ReceiptSelector/index.native.js
index 824c242cf02f..ef81109ffb90 100644
--- a/src/pages/iou/ReceiptSelector/index.native.js
+++ b/src/pages/iou/ReceiptSelector/index.native.js
@@ -50,23 +50,15 @@ const propTypes = {
/** The id of the transaction we're editing */
transactionID: PropTypes.string,
-
- /** Whether or not the receipt selector is in a tab navigator for tab animations */
- isInTabNavigator: PropTypes.bool,
-
- /** Name of the selected receipt tab */
- selectedTab: PropTypes.string,
};
const defaultProps = {
report: {},
iou: iouDefaultProps,
transactionID: '',
- isInTabNavigator: true,
- selectedTab: '',
};
-function ReceiptSelector({route, report, iou, transactionID, isInTabNavigator, selectedTab}) {
+function ReceiptSelector({route, report, iou, transactionID}) {
const devices = useCameraDevices('wide-angle-camera');
const device = devices.back;
@@ -218,8 +210,6 @@ function ReceiptSelector({route, report, iou, transactionID, isInTabNavigator, s
zoom={device.neutralZoom}
photo
cameraTabIndex={pageIndex}
- isInTabNavigator={isInTabNavigator}
- selectedTab={selectedTab}
/>
)}