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

Make Mobile Web and Native App Scanning Consistent #28411

Merged
merged 14 commits into from
Oct 11, 2023
4 changes: 2 additions & 2 deletions ios/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -783,7 +783,7 @@ PODS:
- React-Core
- RNReactNativeHapticFeedback (1.14.0):
- React-Core
- RNReanimated (3.5.4):
- RNReanimated (3.4.0):
Copy link
Contributor

Choose a reason for hiding this comment

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

A downgrade?

Copy link
Contributor

Choose a reason for hiding this comment

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

Copy link
Contributor

Choose a reason for hiding this comment

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

- DoubleConversion
- FBLazyVector
- glog
Expand Down Expand Up @@ -1298,7 +1298,7 @@ SPEC CHECKSUMS:
rnmapbox-maps: 6f638ec002aa6e906a6f766d69cd45f968d98e64
RNPermissions: dcdb7b99796bbeda6975a6e79ad519c41b251b1c
RNReactNativeHapticFeedback: 1e3efeca9628ff9876ee7cdd9edec1b336913f8c
RNReanimated: ab2e96c6d5591c3dfbb38a464f54c8d17fb34a87
RNReanimated: 020859659f64be2d30849a1fe88c821a7c3e0cbf
RNScreens: d037903436160a4b039d32606668350d2a808806
RNSVG: ed492aaf3af9ca01bc945f7a149d76d62e73ec82
SDWebImage: a7f831e1a65eb5e285e3fb046a23fcfbf08e696d
Expand Down
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.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -112,8 +112,8 @@
"react-collapse": "^5.1.0",
"react-content-loader": "^6.1.0",
"react-dom": "18.1.0",
"react-map-gl": "^7.1.3",
"react-error-boundary": "^4.0.11",
"react-map-gl": "^7.1.3",
"react-native": "0.72.4",
"react-native-blob-util": "^0.17.3",
"react-native-collapsible": "^1.6.0",
Expand Down Expand Up @@ -157,6 +157,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,
};
92 changes: 52 additions & 40 deletions src/pages/iou/ReceiptSelector/NavigationAwareCamera.js
Original file line number Diff line number Diff line change
@@ -1,60 +1,72 @@
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';

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}
<Webcam
audio={false}
screenshotFormat="image/png"
// eslint-disable-next-line react/jsx-props-no-spreading
{...props}
isActive={isCameraActive}
ref={ref}
onUserMedia={handleOnUserMedia}
/>
);
}

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);
77 changes: 77 additions & 0 deletions src/pages/iou/ReceiptSelector/NavigationAwareCamera.native.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
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());

// Get the animation value from the tab navigator. Its a value between 0 and the
Copy link
Contributor

Choose a reason for hiding this comment

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

Its -> It's

Copy link
Contributor

@cubuspl42 cubuspl42 Oct 5, 2023

Choose a reason for hiding this comment

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

@lukemorawski All threads have to be handled

Copy link
Contributor

Choose a reason for hiding this comment

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

// number of pages we render in the tab navigator. When we even just slightly start to scroll to the camera page,
// (value is e.g. 0.001 on animation start) we want to activate the camera, so its as fast as possible active.
Copy link
Contributor

Choose a reason for hiding this comment

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

its -> it's

Also, the sentence structure in the comment isn't proper English, the comment could be passed through GPT or another tool with a prompt like "Improve language correctness"

Copy link
Contributor

Choose a reason for hiding this comment

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

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
Loading