Skip to content

Commit

Permalink
Merge pull request #28411 from lukemorawski/new-web-scan-flow
Browse files Browse the repository at this point in the history
Make Mobile Web and Native App Scanning Consistent
  • Loading branch information
AndrewGable authored Oct 11, 2023
2 parents c313a62 + 98519f0 commit 9e6af2b
Show file tree
Hide file tree
Showing 8 changed files with 394 additions and 102 deletions.
1 change: 1 addition & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,7 @@
"react-pdf": "^6.2.2",
"react-plaid-link": "3.3.2",
"react-web-config": "^1.0.0",
"react-webcam": "^7.1.1",
"react-window": "^1.8.9",
"save": "^2.4.0",
"semver": "^7.5.2",
Expand Down
52 changes: 51 additions & 1 deletion src/libs/fileDownload/FileUtils.js
Original file line number Diff line number Diff line change
Expand Up @@ -170,4 +170,54 @@ const readFileAsync = (path, fileName) =>
});
});

export {showGeneralErrorAlert, showSuccessAlert, showPermissionErrorAlert, splitExtensionFromFileName, getAttachmentName, getFileType, cleanFileName, appendTimeToFileName, readFileAsync};
/**
* Converts a base64 encoded image string to a File instance.
* Adds a `uri` property to the File instance for accessing the blob as a URI.
*
* @param {string} base64 - The base64 encoded image string.
* @param {string} filename - Desired filename for the File instance.
* @returns {File} The File instance created from the base64 string with an additional `uri` property.
*
* @example
* const base64Image = "data:image/png;base64,..."; // your base64 encoded image
* const imageFile = base64ToFile(base64Image, "example.png");
* console.log(imageFile.uri); // Blob URI
*/
function base64ToFile(base64, filename) {
// Decode the base64 string
const byteString = atob(base64.split(',')[1]);

// Get the mime type from the base64 string
const mimeString = base64.split(',')[0].split(':')[1].split(';')[0];

// Convert byte string to Uint8Array
const arrayBuffer = new ArrayBuffer(byteString.length);
const uint8Array = new Uint8Array(arrayBuffer);
for (let i = 0; i < byteString.length; i++) {
uint8Array[i] = byteString.charCodeAt(i);
}

// Create a blob from the Uint8Array
const blob = new Blob([uint8Array], {type: mimeString});

// Create a File instance from the Blob
const file = new File([blob], filename, {type: mimeString, lastModified: Date.now()});

// Add a uri property to the File instance for accessing the blob as a URI
file.uri = URL.createObjectURL(blob);

return file;
}

export {
showGeneralErrorAlert,
showSuccessAlert,
showPermissionErrorAlert,
splitExtensionFromFileName,
getAttachmentName,
getFileType,
cleanFileName,
appendTimeToFileName,
readFileAsync,
base64ToFile,
};
101 changes: 58 additions & 43 deletions src/pages/iou/ReceiptSelector/NavigationAwareCamera.js
Original file line number Diff line number Diff line change
@@ -1,60 +1,75 @@
import React, {useEffect, useState} from 'react';
import {Camera} from 'react-native-vision-camera';
import {useNavigation} from '@react-navigation/native';
import React, {useEffect, useRef} from 'react';
import Webcam from 'react-webcam';
import {useIsFocused} from '@react-navigation/native';
import PropTypes from 'prop-types';
import refPropTypes from '../../../components/refPropTypes';
import {View} from 'react-native';

const propTypes = {
/* The index of the tab that contains this camera */
cameraTabIndex: PropTypes.number.isRequired,
/* Flag to turn on/off the torch/flashlight - if available */
torchOn: PropTypes.bool,

/* Forwarded ref */
forwardedRef: refPropTypes.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 */
onTorchAvailability: PropTypes.func,
};

const defaultProps = {
onUserMedia: undefined,
onTorchAvailability: undefined,
torchOn: false,
};

// Wraps a camera that will only be active when the tab is focused or as soon as it starts to become focused.
function NavigationAwareCamera({cameraTabIndex, forwardedRef, ...props}) {
// Get navigation to get initial isFocused value (only needed once during init!)
const navigation = useNavigation();
const [isCameraActive, setIsCameraActive] = useState(navigation.isFocused());

// 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
function NavigationAwareCamera({torchOn, onTorchAvailability, ...props}, ref) {
const trackRef = useRef(null);
const isCameraActive = useIsFocused();

const handleOnUserMedia = (stream) => {
if (props.onUserMedia) {
props.onUserMedia(stream);
}

const [track] = stream.getVideoTracks();
const capabilities = track.getCapabilities();
if (capabilities.torch) {
trackRef.current = track;
}
if (onTorchAvailability) {
onTorchAvailability(!!capabilities.torch);
}
};

useEffect(() => {
const removeBlurListener = navigation.addListener('blur', () => {
setIsCameraActive(false);
});
const removeFocusListener = navigation.addListener('focus', () => {
setIsCameraActive(true);
});
if (!trackRef.current) {
return;
}

return () => {
removeBlurListener();
removeFocusListener();
};
}, [navigation]);
trackRef.current.applyConstraints({
advanced: [{torch: torchOn}],
});
}, [torchOn]);

if (!isCameraActive) {
return null;
}
return (
<Camera
ref={forwardedRef}
// eslint-disable-next-line react/jsx-props-no-spreading
{...props}
isActive={isCameraActive}
/>
<View>
<Webcam
audio={false}
screenshotFormat="image/png"
// eslint-disable-next-line react/jsx-props-no-spreading
{...props}
ref={ref}
onUserMedia={handleOnUserMedia}
/>
</View>
);
}

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

export default React.forwardRef((props, ref) => (
<NavigationAwareCamera
// eslint-disable-next-line react/jsx-props-no-spreading
{...props}
forwardedRef={ref}
/>
));
export default React.forwardRef(NavigationAwareCamera);
76 changes: 76 additions & 0 deletions src/pages/iou/ReceiptSelector/NavigationAwareCamera.native.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import React, {useEffect, useState} from 'react';
import {Camera} from 'react-native-vision-camera';
import {useTabAnimation} from '@react-navigation/material-top-tabs';
import {useNavigation} from '@react-navigation/native';
import PropTypes from 'prop-types';
import refPropTypes from '../../../components/refPropTypes';

const propTypes = {
/* The index of the tab that contains this camera */
cameraTabIndex: PropTypes.number.isRequired,

/* Forwarded ref */
forwardedRef: refPropTypes.isRequired,
};

// Wraps a camera that will only be active when the tab is focused or as soon as it starts to become focused.
function NavigationAwareCamera({cameraTabIndex, forwardedRef, ...props}) {
// 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.
const tabPositionAnimation = useTabAnimation();

useEffect(() => {
const listenerId = tabPositionAnimation.addListener(({value}) => {
// Activate camera as soon the index is animating towards the `cameraTabIndex`
setIsCameraActive(value > cameraTabIndex - 1 && value < cameraTabIndex + 1);
});

return () => {
tabPositionAnimation.removeListener(listenerId);
};
}, [cameraTabIndex, tabPositionAnimation]);

// 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]);

return (
<Camera
ref={forwardedRef}
// eslint-disable-next-line react/jsx-props-no-spreading
{...props}
isActive={isCameraActive}
/>
);
}

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

export default React.forwardRef((props, ref) => (
<NavigationAwareCamera
// eslint-disable-next-line react/jsx-props-no-spreading
{...props}
forwardedRef={ref}
/>
));
Loading

0 comments on commit 9e6af2b

Please sign in to comment.