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 all 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
72 changes: 0 additions & 72 deletions src/components/withTabAnimation.js

This file was deleted.

79 changes: 79 additions & 0 deletions src/hooks/useTabNavigatorFocus/index.js
Original file line number Diff line number Diff line change
@@ -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;
9 changes: 9 additions & 0 deletions src/libs/DomUtils/index.native.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,15 @@ import GetActiveElement from './types';

const getActiveElement: GetActiveElement = () => null;

const requestAnimationFrame = (callback: () => void) => {
if (!callback) {
return;
}

callback();
};

export default {
getActiveElement,
requestAnimationFrame,
};
1 change: 1 addition & 0 deletions src/libs/DomUtils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@ const getActiveElement: GetActiveElement = () => document.activeElement;

export default {
getActiveElement,
requestAnimationFrame: window.requestAnimationFrame.bind(window),
};
19 changes: 12 additions & 7 deletions src/pages/iou/ReceiptSelector/NavigationAwareCamera/index.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
Original file line number Diff line number Diff line change
@@ -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 (
<Camera
Expand All @@ -84,7 +23,6 @@ const NavigationAwareCamera = React.forwardRef(({cameraTabIndex, isInTabNavigato
});

NavigationAwareCamera.propTypes = propTypes;
NavigationAwareCamera.defaultProps = defaultProps;
NavigationAwareCamera.displayName = 'NavigationAwareCamera';

export default withTabAnimation(NavigationAwareCamera);
export default NavigationAwareCamera;
9 changes: 3 additions & 6 deletions src/pages/iou/ReceiptSelector/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,21 +53,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 @@ -81,7 +77,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 @@ -200,6 +196,7 @@ function ReceiptSelector({route, transactionID, iou, report}) {
torchOn={isFlashLightOn}
onTorchAvailability={setIsTorchAvailable}
forceScreenshotSourceSize
cameraTabIndex={pageIndex}
/>
</View>

Expand Down
12 changes: 1 addition & 11 deletions src/pages/iou/ReceiptSelector/index.native.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -218,8 +210,6 @@ function ReceiptSelector({route, report, iou, transactionID, isInTabNavigator, s
zoom={device.neutralZoom}
photo
cameraTabIndex={pageIndex}
isInTabNavigator={isInTabNavigator}
selectedTab={selectedTab}
/>
)}
<View style={[styles.flexRow, styles.justifyContentAround, styles.alignItemsCenter, styles.pv3]}>
Expand Down
Loading