Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Chrome mWeb - scan button provides no feedback #30094

Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 81 additions & 0 deletions src/hooks/useTabNavigatorFocus/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import {useTabAnimation} from '@react-navigation/material-top-tabs';
import {useIsFocused} from '@react-navigation/native';
import {useEffect, useState} from 'react';

/**
* 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.
*
* Note: This hook is almost identical to native implementation, except for the `selectedTab` parameter
* and the fact that it uses requestAnimationFrame to mitigate issues when updating the isTabFocused state.
*
* @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`.
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
* @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`.
* @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`.

Copy link
Contributor

Choose a reason for hiding this comment

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

Would you mind applying this change, then I think it should be all ready to merge.

*
* @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`
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') {
requestAnimationFrame(() => {
setIsTabFocused(initialTabPositionValue > index - 1 && initialTabPositionValue < index + 1);
});
}

return () => {
if (!tabPositionAnimation) {
return;
}
tabPositionAnimation.removeListener(listenerId);
};
}, [tabIndex, tabPositionAnimation]);

return isTabFocused && isPageFocused;
}

export default useTabNavigatorFocus;
75 changes: 75 additions & 0 deletions src/hooks/useTabNavigatorFocus/index.native.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import {useTabAnimation} from '@react-navigation/material-top-tabs';
import {useIsFocused} from '@react-navigation/native';
import {useEffect, useState} from 'react';
import CONST from '@src/CONST';

/**
* Custom React hook to determine the focus status of a specific tab in a Material Top Tab Navigator, with additional
* conditions based on a selected tab state. It evaluates whether the specified tab is focused by combining the tab's
* animation position and the screen's focus status within a React Navigation environment.
*
* The hook is primarily intended for use with Material Top Tabs provided by '@react-navigation/material-top-tabs'.
* It utilizes the `useTabAnimation` hook from this package to track the animated position of the tabs and the
* `useIsFocused` hook from '@react-navigation/native' to determine if the current screen is focused. Additionally,
* it uses a `selectedTab` parameter to apply custom logic based on the currently selected tab.
*
* Note: This hook employs a conditional invocation of the `useTabAnimation` hook, which is generally against React's
* rules of hooks. This pattern is used here to handle scenarios where the hook might not be employed within a
* Material Top Tabs Navigator context. Ensure this hook is only used in appropriate scenarios to avoid potential issues.
*
* @param {Object} params - The parameters object.
* @param {number} params.tabIndex - The index of the tab for which focus status is being determined.
* @param {string} params.selectedTab - The tab identifier passed by <TopTab.Screen /> to the component. Used only on native platform
*
* @returns {boolean} Returns `true` if the specified tab is both animation-focused and screen-focused, otherwise `false`.
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
* @param {number} params.tabIndex - The index of the tab for which focus status is being determined.
* @param {string} params.selectedTab - The tab identifier passed by <TopTab.Screen /> to the component. Used only on native platform
*
* @returns {boolean} Returns `true` if the specified tab is both animation-focused and screen-focused, otherwise `false`.
* @param {Number} params.tabIndex - The index of the tab for which focus status is being determined.
* @param {String} params.selectedTab - The tab identifier passed by <TopTab.Screen /> to the component. Used only on native platform
*
* @returns {Boolean} Returns `true` if the specified tab is both animation-focused and screen-focused, otherwise `false`.

*
* @example
* const isTabFocused = useTabNavigatorFocus({ tabIndex: 1, selectedTab: 'home' });
*/
function useTabNavigatorFocus({tabIndex, selectedTab}) {
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);

// 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

useEffect(() => {
if (!tabPositionAnimation) {
return;
}

const listenerId = tabPositionAnimation.addListener(({value}) => {
if (selectedTab !== CONST.TAB.SCAN) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Is this the only difference between this file and index.js? If so, I think it would be better to find a way to only extract this specific thing into a platfor-specific module. That way most of this code is not duplicated.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

not only, also requestanimation frame is meant for web while it can't be used on native.

Copy link
Contributor

Choose a reason for hiding this comment

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

Ah, I see. Can you please comment at the top of both of these files to explain the differences? Neither of those was very obvious to me.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

sure :)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Added descriptions

return;
}
// Activate camera as soon the index is animating towards the `cameraTabIndex`
setIsTabFocused(value > tabIndex - 1 && value < tabIndex + 1);
});

return () => {
if (!tabPositionAnimation) {
return;
}
tabPositionAnimation.removeListener(listenerId);
};
}, [tabIndex, tabPositionAnimation, selectedTab]);

return isTabFocused && isPageFocused;
}

export default useTabNavigatorFocus;
19 changes: 12 additions & 7 deletions src/pages/iou/ReceiptSelector/NavigationAwareCamera.js
Original file line number Diff line number Diff line change
@@ -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,
};

Expand All @@ -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) {
Expand All @@ -51,7 +56,7 @@ const NavigationAwareCamera = React.forwardRef(({torchOn, onTorchAvailability, .
});
}, [torchOn]);

if (!isCameraActive) {
if (!shouldShowCamera) {
return null;
}
return (
Expand Down
60 changes: 4 additions & 56 deletions src/pages/iou/ReceiptSelector/NavigationAwareCamera.native.js
Original file line number Diff line number Diff line change
@@ -1,71 +1,19 @@
import {useTabAnimation} from '@react-navigation/material-top-tabs';
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 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,
};

// 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, ...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.

// 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
const tabPositionAnimation = isInTabNavigator ? useTabAnimation() : null;

useEffect(() => {
if (!isInTabNavigator) {
return;
}

const listenerId = tabPositionAnimation.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 () => {
tabPositionAnimation.removeListener(listenerId);
};
}, [cameraTabIndex, tabPositionAnimation, 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, selectedTab, ...props}, ref) => {
const isCameraActive = useTabNavigatorFocus({tabIndex: cameraTabIndex, selectedTab});

return (
<Camera
Expand Down
9 changes: 3 additions & 6 deletions src/pages/iou/ReceiptSelector/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,21 +54,17 @@ 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 */
// eslint-disable-next-line react/no-unused-prop-types
isInTabNavigator: PropTypes.bool,
};

const defaultProps = {
report: {},
iou: iouDefaultProps,
transactionID: '',
isInTabNavigator: true,
};

function ReceiptSelector({route, transactionID, iou, report}) {
const iouType = lodashGet(route, 'params.iouType', '');
const pageIndex = lodashGet(route, 'params.pageIndex', 1);

// Grouping related states
const [isAttachmentInvalid, setIsAttachmentInvalid] = useState(false);
Expand All @@ -82,7 +78,7 @@ function ReceiptSelector({route, transactionID, iou, report}) {

const [cameraPermissionState, setCameraPermissionState] = useState('prompt');
const [isFlashLightOn, toggleFlashlight] = useReducer((state) => !state, false);
const [isTorchAvailable, setIsTorchAvailable] = useState(true);
const [isTorchAvailable, setIsTorchAvailable] = useState(false);
const cameraRef = useRef(null);

const hideReciptModal = () => {
Expand Down Expand Up @@ -201,6 +197,7 @@ function ReceiptSelector({route, transactionID, iou, report}) {
torchOn={isFlashLightOn}
onTorchAvailability={setIsTorchAvailable}
forceScreenshotSourceSize
cameraTabIndex={pageIndex}
/>
</View>

Expand Down
7 changes: 1 addition & 6 deletions src/pages/iou/ReceiptSelector/index.native.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,9 +51,6 @@ 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,
};
Expand All @@ -62,11 +59,10 @@ const defaultProps = {
report: {},
iou: iouDefaultProps,
transactionID: '',
isInTabNavigator: true,
selectedTab: '',
};

function ReceiptSelector({route, report, iou, transactionID, isInTabNavigator, selectedTab}) {
function ReceiptSelector({route, report, iou, transactionID, selectedTab}) {
const devices = useCameraDevices('wide-angle-camera');
const device = devices.back;

Expand Down Expand Up @@ -198,7 +194,6 @@ function ReceiptSelector({route, report, iou, transactionID, isInTabNavigator, s
zoom={device.neutralZoom}
photo
cameraTabIndex={pageIndex}
isInTabNavigator={isInTabNavigator}
selectedTab={selectedTab}
/>
)}
Expand Down
Loading