From 0272c759e218ce48a45d63291051b4850718d203 Mon Sep 17 00:00:00 2001 From: Ravindra Singh Date: Mon, 12 Aug 2024 12:48:40 +0530 Subject: [PATCH 01/13] navigate to scan screen after granting camera permission from setting --- src/ONYXKEYS.ts | 4 ++ .../AttachmentPicker/index.native.tsx | 4 +- src/components/AttachmentPicker/types.ts | 3 ++ src/libs/actions/App.ts | 5 +++ src/libs/fileDownload/FileUtils.ts | 9 +++- .../SidebarScreen/BaseSidebarScreen.tsx | 43 +++++++++++++++++-- .../step/IOURequestStepScan/index.native.tsx | 5 ++- 7 files changed, 65 insertions(+), 8 deletions(-) diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index 7102d6396381..fc51e578fa24 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -400,6 +400,9 @@ const ONYXKEYS = { /** Stores the information about currently edited advanced approval workflow */ APPROVAL_WORKFLOW: 'approvalWorkflow', + /** screen to open after reloading app after changing app permission from settings */ + LAST_SCREEN: 'last_screen', + /** Collection Keys */ COLLECTION: { DOWNLOAD: 'download_', @@ -879,6 +882,7 @@ type OnyxValuesMapping = { [ONYXKEYS.NVP_WORKSPACE_TOOLTIP]: OnyxTypes.WorkspaceTooltip; [ONYXKEYS.NVP_PRIVATE_CANCELLATION_DETAILS]: OnyxTypes.CancellationDetails[]; [ONYXKEYS.APPROVAL_WORKFLOW]: OnyxTypes.ApprovalWorkflow; + [ONYXKEYS.LAST_SCREEN]: string; }; type OnyxValues = OnyxValuesMapping & OnyxCollectionValuesMapping & OnyxFormValuesMapping & OnyxFormDraftValuesMapping; diff --git a/src/components/AttachmentPicker/index.native.tsx b/src/components/AttachmentPicker/index.native.tsx index edcdabed9101..ae26832b7879 100644 --- a/src/components/AttachmentPicker/index.native.tsx +++ b/src/components/AttachmentPicker/index.native.tsx @@ -112,7 +112,7 @@ const getDataForUpload = (fileData: FileResponse): Promise => { * a callback. This is the ios/android implementation * opening a modal with attachment options */ -function AttachmentPicker({type = CONST.ATTACHMENT_PICKER_TYPE.FILE, children, shouldHideCameraOption = false}: AttachmentPickerProps) { +function AttachmentPicker({type = CONST.ATTACHMENT_PICKER_TYPE.FILE, children, shouldHideCameraOption = false, lastScreen = undefined}: AttachmentPickerProps) { const styles = useThemeStyles(); const [isVisible, setIsVisible] = useState(false); @@ -150,7 +150,7 @@ function AttachmentPicker({type = CONST.ATTACHMENT_PICKER_TYPE.FILE, children, s if (response.errorCode) { switch (response.errorCode) { case 'permission': - FileUtils.showCameraPermissionsAlert(); + FileUtils.showCameraPermissionsAlert(lastScreen); return resolve(); default: showGeneralAlert(); diff --git a/src/components/AttachmentPicker/types.ts b/src/components/AttachmentPicker/types.ts index 445d79bce07a..dc2e38b901e7 100644 --- a/src/components/AttachmentPicker/types.ts +++ b/src/components/AttachmentPicker/types.ts @@ -40,6 +40,9 @@ type AttachmentPickerProps = { /** The types of files that can be selected with this picker. */ type?: ValueOf; + + /** Last screen to save for navigating after granting camera permission from settings in ios */ + lastScreen?: string | undefined; }; export default AttachmentPickerProps; diff --git a/src/libs/actions/App.ts b/src/libs/actions/App.ts index 2a12b679972a..e0c8e3a94459 100644 --- a/src/libs/actions/App.ts +++ b/src/libs/actions/App.ts @@ -495,6 +495,10 @@ function updateLastVisitedPath(path: string) { Onyx.merge(ONYXKEYS.LAST_VISITED_PATH, path); } +function updateLastScreen(screen: string) { + Onyx.set(ONYXKEYS.LAST_SCREEN, screen); +} + export { setLocale, setLocaleAndNavigate, @@ -512,5 +516,6 @@ export { savePolicyDraftByNewWorkspace, createWorkspaceWithPolicyDraftAndNavigateToIt, updateLastVisitedPath, + updateLastScreen, KEYS_TO_PRESERVE, }; diff --git a/src/libs/fileDownload/FileUtils.ts b/src/libs/fileDownload/FileUtils.ts index c8520f3c66cd..7a20af7a80dd 100644 --- a/src/libs/fileDownload/FileUtils.ts +++ b/src/libs/fileDownload/FileUtils.ts @@ -2,7 +2,9 @@ import {Str} from 'expensify-common'; import {Alert, Linking, Platform} from 'react-native'; import ImageSize from 'react-native-image-size'; import type {FileObject} from '@components/AttachmentModal'; +import {updateLastScreen} from '@libs/actions/App'; import DateUtils from '@libs/DateUtils'; +import getPlatform from '@libs/getPlatform'; import * as Localize from '@libs/Localize'; import Log from '@libs/Log'; import CONST from '@src/CONST'; @@ -63,7 +65,7 @@ function showPermissionErrorAlert() { /** * Inform the users when they need to grant camera access and guide them to settings */ -function showCameraPermissionsAlert() { +function showCameraPermissionsAlert(screenName: string | undefined) { Alert.alert( Localize.translateLocal('attachmentPicker.cameraPermissionRequired'), Localize.translateLocal('attachmentPicker.expensifyDoesntHaveAccessToCamera'), @@ -76,6 +78,11 @@ function showCameraPermissionsAlert() { text: Localize.translateLocal('common.settings'), onPress: () => { Linking.openSettings(); + // In case of ios, app reload when we enable camera permission from settings + // we are saving last screen so we can navigate to it after app reload + if (getPlatform() === CONST.PLATFORM.IOS && screenName) { + updateLastScreen(screenName); + } }, }, ], diff --git a/src/pages/home/sidebar/SidebarScreen/BaseSidebarScreen.tsx b/src/pages/home/sidebar/SidebarScreen/BaseSidebarScreen.tsx index 597318ab2b26..ca12518e741e 100644 --- a/src/pages/home/sidebar/SidebarScreen/BaseSidebarScreen.tsx +++ b/src/pages/home/sidebar/SidebarScreen/BaseSidebarScreen.tsx @@ -1,19 +1,25 @@ import React, {useEffect} from 'react'; import {View} from 'react-native'; -import {useOnyx} from 'react-native-onyx'; +import {useOnyx, withOnyx} from 'react-native-onyx'; +import type {OnyxEntry} from 'react-native-onyx'; import ScreenWrapper from '@components/ScreenWrapper'; import useActiveWorkspaceFromNavigationState from '@hooks/useActiveWorkspaceFromNavigationState'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; +import {updateLastScreen} from '@libs/actions/App'; import {updateLastAccessedWorkspace} from '@libs/actions/Policy/Policy'; import * as Browser from '@libs/Browser'; +import interceptAnonymousUser from '@libs/interceptAnonymousUser'; import TopBar from '@libs/Navigation/AppNavigator/createCustomBottomTabNavigator/TopBar'; import Navigation from '@libs/Navigation/Navigation'; import Performance from '@libs/Performance'; +import * as ReportUtils from '@libs/ReportUtils'; import SidebarLinksData from '@pages/home/sidebar/SidebarLinksData'; +import * as IOU from '@userActions/IOU'; import Timing from '@userActions/Timing'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; +import SCREENS from '@src/SCREENS'; /** * Function called when a pinned chat is selected. @@ -23,7 +29,14 @@ const startTimer = () => { Performance.markStart(CONST.TIMING.SWITCH_REPORT); }; -function BaseSidebarScreen() { +type BaseSidebarScreenOnyxProps = { + /** last visited screen */ + lastScreen: OnyxEntry; +}; + +type BaseSidebarScreenProps = BaseSidebarScreenOnyxProps; + +function BaseSidebarScreen({lastScreen}: BaseSidebarScreenProps) { const styles = useThemeStyles(); const activeWorkspaceID = useActiveWorkspaceFromNavigationState(); const {translate} = useLocalize(); @@ -43,6 +56,26 @@ function BaseSidebarScreen() { updateLastAccessedWorkspace(undefined); }, [activeWorkspace, activeWorkspaceID]); + /** + * Navigate to scan receipt screen after it enabling camera permission from setting + * This will only works for ios application because we are saving last screen only for ios + */ + useEffect(() => { + if (lastScreen !== SCREENS.RIGHT_MODAL.MONEY_REQUEST) { + return; + } + + interceptAnonymousUser(() => { + updateLastScreen(''); + IOU.startMoneyRequest( + CONST.IOU.TYPE.SUBMIT, + // When starting to create an expense from the global FAB, there is not an existing report yet. A random optimistic reportID is generated and used + // for all of the routes in the creation flow. + ReportUtils.generateReportID(), + ); + }); + }, []); + return ( ({ + lastScreen: { + key: ONYXKEYS.LAST_SCREEN, + }, +})(BaseSidebarScreen); diff --git a/src/pages/iou/request/step/IOURequestStepScan/index.native.tsx b/src/pages/iou/request/step/IOURequestStepScan/index.native.tsx index 2630b82a7ba4..a708a46e876b 100644 --- a/src/pages/iou/request/step/IOURequestStepScan/index.native.tsx +++ b/src/pages/iou/request/step/IOURequestStepScan/index.native.tsx @@ -38,6 +38,7 @@ import * as IOU from '@userActions/IOU'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; +import SCREENS from '@src/SCREENS'; import type {Receipt} from '@src/types/onyx/Transaction'; import CameraPermission from './CameraPermission'; import NavigationAwareCamera from './NavigationAwareCamera/Camera'; @@ -96,7 +97,7 @@ function IOURequestStepScan({ setCameraPermissionStatus(status); if (status === RESULTS.BLOCKED) { - FileUtils.showCameraPermissionsAlert(); + FileUtils.showCameraPermissionsAlert(SCREENS.RIGHT_MODAL.MONEY_REQUEST); } }) .catch(() => { @@ -540,7 +541,7 @@ function IOURequestStepScan({ )} - + {({openPicker}) => ( Date: Mon, 12 Aug 2024 15:39:17 +0530 Subject: [PATCH 02/13] fix navigation for track and submit expense --- src/ONYXKEYS.ts | 3 ++- src/components/AttachmentPicker/index.native.tsx | 2 +- src/components/AttachmentPicker/types.ts | 3 ++- src/libs/actions/App.ts | 3 ++- src/libs/fileDownload/FileUtils.ts | 3 ++- .../home/sidebar/SidebarScreen/BaseSidebarScreen.tsx | 9 ++++----- .../iou/request/step/IOURequestStepScan/index.native.tsx | 5 ++--- 7 files changed, 15 insertions(+), 13 deletions(-) diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index fc51e578fa24..582a36c62944 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -1,4 +1,5 @@ import type {ValueOf} from 'type-fest'; +import type {IOUType} from './CONST'; import type CONST from './CONST'; import type * as FormTypes from './types/form'; import type * as OnyxTypes from './types/onyx'; @@ -882,7 +883,7 @@ type OnyxValuesMapping = { [ONYXKEYS.NVP_WORKSPACE_TOOLTIP]: OnyxTypes.WorkspaceTooltip; [ONYXKEYS.NVP_PRIVATE_CANCELLATION_DETAILS]: OnyxTypes.CancellationDetails[]; [ONYXKEYS.APPROVAL_WORKFLOW]: OnyxTypes.ApprovalWorkflow; - [ONYXKEYS.LAST_SCREEN]: string; + [ONYXKEYS.LAST_SCREEN]: IOUType | ''; }; type OnyxValues = OnyxValuesMapping & OnyxCollectionValuesMapping & OnyxFormValuesMapping & OnyxFormDraftValuesMapping; diff --git a/src/components/AttachmentPicker/index.native.tsx b/src/components/AttachmentPicker/index.native.tsx index ae26832b7879..de83c16a8f4c 100644 --- a/src/components/AttachmentPicker/index.native.tsx +++ b/src/components/AttachmentPicker/index.native.tsx @@ -112,7 +112,7 @@ const getDataForUpload = (fileData: FileResponse): Promise => { * a callback. This is the ios/android implementation * opening a modal with attachment options */ -function AttachmentPicker({type = CONST.ATTACHMENT_PICKER_TYPE.FILE, children, shouldHideCameraOption = false, lastScreen = undefined}: AttachmentPickerProps) { +function AttachmentPicker({type = CONST.ATTACHMENT_PICKER_TYPE.FILE, children, shouldHideCameraOption = false, lastScreen = ''}: AttachmentPickerProps) { const styles = useThemeStyles(); const [isVisible, setIsVisible] = useState(false); diff --git a/src/components/AttachmentPicker/types.ts b/src/components/AttachmentPicker/types.ts index dc2e38b901e7..0d18f9b44940 100644 --- a/src/components/AttachmentPicker/types.ts +++ b/src/components/AttachmentPicker/types.ts @@ -2,6 +2,7 @@ import type {ReactNode} from 'react'; import type {ValueOf} from 'type-fest'; import type {FileObject} from '@components/AttachmentModal'; import type CONST from '@src/CONST'; +import type {IOUType} from '@src/CONST'; type PickerOptions = { /** A callback that will be called with the selected attachment. */ @@ -42,7 +43,7 @@ type AttachmentPickerProps = { type?: ValueOf; /** Last screen to save for navigating after granting camera permission from settings in ios */ - lastScreen?: string | undefined; + lastScreen?: IOUType | ''; }; export default AttachmentPickerProps; diff --git a/src/libs/actions/App.ts b/src/libs/actions/App.ts index e0c8e3a94459..0c8e89d865ca 100644 --- a/src/libs/actions/App.ts +++ b/src/libs/actions/App.ts @@ -17,6 +17,7 @@ import Navigation from '@libs/Navigation/Navigation'; import Performance from '@libs/Performance'; import * as ReportActionsUtils from '@libs/ReportActionsUtils'; import * as SessionUtils from '@libs/SessionUtils'; +import type {IOUType} from '@src/CONST'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {OnyxKey} from '@src/ONYXKEYS'; @@ -495,7 +496,7 @@ function updateLastVisitedPath(path: string) { Onyx.merge(ONYXKEYS.LAST_VISITED_PATH, path); } -function updateLastScreen(screen: string) { +function updateLastScreen(screen: IOUType | '') { Onyx.set(ONYXKEYS.LAST_SCREEN, screen); } diff --git a/src/libs/fileDownload/FileUtils.ts b/src/libs/fileDownload/FileUtils.ts index 7a20af7a80dd..5ad8a8255e51 100644 --- a/src/libs/fileDownload/FileUtils.ts +++ b/src/libs/fileDownload/FileUtils.ts @@ -7,6 +7,7 @@ import DateUtils from '@libs/DateUtils'; import getPlatform from '@libs/getPlatform'; import * as Localize from '@libs/Localize'; import Log from '@libs/Log'; +import type {IOUType} from '@src/CONST'; import CONST from '@src/CONST'; import getImageManipulator from './getImageManipulator'; import getImageResolution from './getImageResolution'; @@ -65,7 +66,7 @@ function showPermissionErrorAlert() { /** * Inform the users when they need to grant camera access and guide them to settings */ -function showCameraPermissionsAlert(screenName: string | undefined) { +function showCameraPermissionsAlert(screenName: IOUType | '') { Alert.alert( Localize.translateLocal('attachmentPicker.cameraPermissionRequired'), Localize.translateLocal('attachmentPicker.expensifyDoesntHaveAccessToCamera'), diff --git a/src/pages/home/sidebar/SidebarScreen/BaseSidebarScreen.tsx b/src/pages/home/sidebar/SidebarScreen/BaseSidebarScreen.tsx index ca12518e741e..8958441a2475 100644 --- a/src/pages/home/sidebar/SidebarScreen/BaseSidebarScreen.tsx +++ b/src/pages/home/sidebar/SidebarScreen/BaseSidebarScreen.tsx @@ -17,9 +17,9 @@ import * as ReportUtils from '@libs/ReportUtils'; import SidebarLinksData from '@pages/home/sidebar/SidebarLinksData'; import * as IOU from '@userActions/IOU'; import Timing from '@userActions/Timing'; +import type {IOUType} from '@src/CONST'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import SCREENS from '@src/SCREENS'; /** * Function called when a pinned chat is selected. @@ -31,7 +31,7 @@ const startTimer = () => { type BaseSidebarScreenOnyxProps = { /** last visited screen */ - lastScreen: OnyxEntry; + lastScreen: OnyxEntry; }; type BaseSidebarScreenProps = BaseSidebarScreenOnyxProps; @@ -61,14 +61,13 @@ function BaseSidebarScreen({lastScreen}: BaseSidebarScreenProps) { * This will only works for ios application because we are saving last screen only for ios */ useEffect(() => { - if (lastScreen !== SCREENS.RIGHT_MODAL.MONEY_REQUEST) { + if (!lastScreen) { return; } - interceptAnonymousUser(() => { updateLastScreen(''); IOU.startMoneyRequest( - CONST.IOU.TYPE.SUBMIT, + lastScreen, // When starting to create an expense from the global FAB, there is not an existing report yet. A random optimistic reportID is generated and used // for all of the routes in the creation flow. ReportUtils.generateReportID(), diff --git a/src/pages/iou/request/step/IOURequestStepScan/index.native.tsx b/src/pages/iou/request/step/IOURequestStepScan/index.native.tsx index a708a46e876b..9cf93bf7eab0 100644 --- a/src/pages/iou/request/step/IOURequestStepScan/index.native.tsx +++ b/src/pages/iou/request/step/IOURequestStepScan/index.native.tsx @@ -38,7 +38,6 @@ import * as IOU from '@userActions/IOU'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; -import SCREENS from '@src/SCREENS'; import type {Receipt} from '@src/types/onyx/Transaction'; import CameraPermission from './CameraPermission'; import NavigationAwareCamera from './NavigationAwareCamera/Camera'; @@ -97,7 +96,7 @@ function IOURequestStepScan({ setCameraPermissionStatus(status); if (status === RESULTS.BLOCKED) { - FileUtils.showCameraPermissionsAlert(SCREENS.RIGHT_MODAL.MONEY_REQUEST); + FileUtils.showCameraPermissionsAlert(iouType); } }) .catch(() => { @@ -541,7 +540,7 @@ function IOURequestStepScan({ )} - + {({openPicker}) => ( Date: Mon, 12 Aug 2024 16:56:10 +0530 Subject: [PATCH 03/13] fix lint issues --- src/components/AttachmentPicker/index.native.tsx | 2 +- src/pages/home/sidebar/SidebarScreen/BaseSidebarScreen.tsx | 2 ++ src/pages/iou/request/step/IOURequestStepScan/index.native.tsx | 2 +- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/components/AttachmentPicker/index.native.tsx b/src/components/AttachmentPicker/index.native.tsx index de83c16a8f4c..54127e2d06be 100644 --- a/src/components/AttachmentPicker/index.native.tsx +++ b/src/components/AttachmentPicker/index.native.tsx @@ -196,7 +196,7 @@ function AttachmentPicker({type = CONST.ATTACHMENT_PICKER_TYPE.FILE, children, s } }); }), - [showGeneralAlert, type], + [showGeneralAlert, type, lastScreen], ); /** * Launch the DocumentPicker. Results are in the same format as ImagePicker diff --git a/src/pages/home/sidebar/SidebarScreen/BaseSidebarScreen.tsx b/src/pages/home/sidebar/SidebarScreen/BaseSidebarScreen.tsx index 8958441a2475..4e682a7d7dd3 100644 --- a/src/pages/home/sidebar/SidebarScreen/BaseSidebarScreen.tsx +++ b/src/pages/home/sidebar/SidebarScreen/BaseSidebarScreen.tsx @@ -73,6 +73,8 @@ function BaseSidebarScreen({lastScreen}: BaseSidebarScreenProps) { ReportUtils.generateReportID(), ); }); + // disabling this rule, as we want this to run only on the first render + // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps }, []); return ( diff --git a/src/pages/iou/request/step/IOURequestStepScan/index.native.tsx b/src/pages/iou/request/step/IOURequestStepScan/index.native.tsx index 4c987e9aa62a..0c806edf6591 100644 --- a/src/pages/iou/request/step/IOURequestStepScan/index.native.tsx +++ b/src/pages/iou/request/step/IOURequestStepScan/index.native.tsx @@ -463,7 +463,7 @@ function IOURequestStepScan({ showCameraAlert(); Log.warn('Error taking photo', error); }); - }, [cameraPermissionStatus, didCapturePhoto, flash, hasFlash, user?.isMutedAllSounds, translate, transactionID, action, navigateToConfirmationStep, updateScanAndNavigate]); + }, [cameraPermissionStatus, didCapturePhoto, flash, hasFlash, user?.isMutedAllSounds, askForPermissions, translate, transactionID, action, navigateToConfirmationStep, updateScanAndNavigate]); // Wait for camera permission status to render if (cameraPermissionStatus == null) { From 222c7b3d9e26b31fc6d1741fd4efc1d718421fc0 Mon Sep 17 00:00:00 2001 From: Ravindra Singh Date: Mon, 12 Aug 2024 17:52:04 +0530 Subject: [PATCH 04/13] fix lint issue --- .../step/IOURequestStepScan/index.native.tsx | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/src/pages/iou/request/step/IOURequestStepScan/index.native.tsx b/src/pages/iou/request/step/IOURequestStepScan/index.native.tsx index 0c806edf6591..e77905acdbb4 100644 --- a/src/pages/iou/request/step/IOURequestStepScan/index.native.tsx +++ b/src/pages/iou/request/step/IOURequestStepScan/index.native.tsx @@ -90,7 +90,7 @@ function IOURequestStepScan({ const {translate} = useLocalize(); - const askForPermissions = () => { + const askForPermissions = useCallback(() => { // There's no way we can check for the BLOCKED status without requesting the permission first // https://github.com/zoontek/react-native-permissions/blob/a836e114ce3a180b2b23916292c79841a267d828/README.md?plain=1#L670 CameraPermission.requestCameraPermission?.() @@ -104,7 +104,7 @@ function IOURequestStepScan({ .catch(() => { setCameraPermissionStatus(RESULTS.UNAVAILABLE); }); - }; + }, [iouType]); const focusIndicatorOpacity = useSharedValue(0); const focusIndicatorScale = useSharedValue(2); @@ -463,7 +463,19 @@ function IOURequestStepScan({ showCameraAlert(); Log.warn('Error taking photo', error); }); - }, [cameraPermissionStatus, didCapturePhoto, flash, hasFlash, user?.isMutedAllSounds, askForPermissions, translate, transactionID, action, navigateToConfirmationStep, updateScanAndNavigate]); + }, [ + cameraPermissionStatus, + didCapturePhoto, + flash, + hasFlash, + user?.isMutedAllSounds, + askForPermissions, + translate, + transactionID, + action, + navigateToConfirmationStep, + updateScanAndNavigate, + ]); // Wait for camera permission status to render if (cameraPermissionStatus == null) { From ef85f11e05a32e65251b4ee2cfc4338acde0eb2b Mon Sep 17 00:00:00 2001 From: Ravindra Singh Date: Wed, 21 Aug 2024 10:44:48 +0530 Subject: [PATCH 05/13] create ios file for fileUtils --- src/libs/fileDownload/FileUtils.ios.ts | 349 +++++++++++++++++++++++++ src/libs/fileDownload/FileUtils.ts | 8 +- 2 files changed, 350 insertions(+), 7 deletions(-) create mode 100644 src/libs/fileDownload/FileUtils.ios.ts diff --git a/src/libs/fileDownload/FileUtils.ios.ts b/src/libs/fileDownload/FileUtils.ios.ts new file mode 100644 index 000000000000..9e42b0166b9d --- /dev/null +++ b/src/libs/fileDownload/FileUtils.ios.ts @@ -0,0 +1,349 @@ +import {Str} from 'expensify-common'; +import {Alert, Linking, Platform} from 'react-native'; +import ImageSize from 'react-native-image-size'; +import type {FileObject} from '@components/AttachmentModal'; +import {updateLastScreen} from '@libs/actions/App'; +import DateUtils from '@libs/DateUtils'; +import * as Localize from '@libs/Localize'; +import Log from '@libs/Log'; +import type {IOUType} from '@src/CONST'; +import CONST from '@src/CONST'; +import getImageManipulator from './getImageManipulator'; +import getImageResolution from './getImageResolution'; +import type {ReadFileAsync, SplitExtensionFromFileName} from './types'; + +/** + * Show alert on successful attachment download + * @param successMessage + */ +function showSuccessAlert(successMessage?: string) { + Alert.alert( + Localize.translateLocal('fileDownload.success.title'), + // successMessage can be an empty string and we want to default to `Localize.translateLocal('fileDownload.success.message')` + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + successMessage || Localize.translateLocal('fileDownload.success.message'), + [ + { + text: Localize.translateLocal('common.ok'), + style: 'cancel', + }, + ], + {cancelable: false}, + ); +} + +/** + * Show alert on attachment download error + */ +function showGeneralErrorAlert() { + Alert.alert(Localize.translateLocal('fileDownload.generalError.title'), Localize.translateLocal('fileDownload.generalError.message'), [ + { + text: Localize.translateLocal('common.cancel'), + style: 'cancel', + }, + ]); +} + +/** + * Show alert on attachment download permissions error + */ +function showPermissionErrorAlert() { + Alert.alert(Localize.translateLocal('fileDownload.permissionError.title'), Localize.translateLocal('fileDownload.permissionError.message'), [ + { + text: Localize.translateLocal('common.cancel'), + style: 'cancel', + }, + { + text: Localize.translateLocal('common.settings'), + onPress: () => { + Linking.openSettings(); + }, + }, + ]); +} + +/** + * Inform the users when they need to grant camera access and guide them to settings + */ +function showCameraPermissionsAlert(screenName: IOUType | '') { + Alert.alert( + Localize.translateLocal('attachmentPicker.cameraPermissionRequired'), + Localize.translateLocal('attachmentPicker.expensifyDoesntHaveAccessToCamera'), + [ + { + text: Localize.translateLocal('common.cancel'), + style: 'cancel', + }, + { + text: Localize.translateLocal('common.settings'), + onPress: () => { + Linking.openSettings(); + // In case of ios, app reload when we enable camera permission from settings + // we are saving last screen so we can navigate to it after app reload + if (screenName) { + updateLastScreen(screenName); + } + }, + }, + ], + {cancelable: false}, + ); +} + +/** + * Extracts a filename from a given URL and sanitizes it for file system usage. + * + * This function takes a URL as input and performs the following operations: + * 1. Extracts the last segment of the URL. + * 2. Decodes the extracted segment from URL encoding to a plain string for better readability. + * 3. Replaces any characters in the decoded string that are illegal in file names + * with underscores. + */ +function getFileName(url: string): string { + const fileName = url.split('/').pop()?.split('?')[0].split('#')[0] ?? ''; + + if (!fileName) { + Log.warn('[FileUtils] Could not get attachment name', {url}); + } + + return decodeURIComponent(fileName).replace(CONST.REGEX.ILLEGAL_FILENAME_CHARACTERS, '_'); +} + +function isImage(fileName: string): boolean { + return CONST.FILE_TYPE_REGEX.IMAGE.test(fileName); +} + +function isVideo(fileName: string): boolean { + return CONST.FILE_TYPE_REGEX.VIDEO.test(fileName); +} + +/** + * Returns file type based on the uri + */ +function getFileType(fileUrl: string): string | undefined { + if (!fileUrl) { + return; + } + + const fileName = getFileName(fileUrl); + + if (!fileName) { + return; + } + + if (isImage(fileName)) { + return CONST.ATTACHMENT_FILE_TYPE.IMAGE; + } + if (isVideo(fileName)) { + return CONST.ATTACHMENT_FILE_TYPE.VIDEO; + } + return CONST.ATTACHMENT_FILE_TYPE.FILE; +} + +/** + * Returns the filename split into fileName and fileExtension + */ +const splitExtensionFromFileName: SplitExtensionFromFileName = (fullFileName) => { + const fileName = fullFileName.trim(); + const splitFileName = fileName.split('.'); + const fileExtension = splitFileName.length > 1 ? splitFileName.pop() : ''; + return {fileName: splitFileName.join('.'), fileExtension: fileExtension ?? ''}; +}; + +/** + * Returns the filename replacing special characters with underscore + */ +function cleanFileName(fileName: string): string { + return fileName.replace(/[^a-zA-Z0-9\-._]/g, '_'); +} + +function appendTimeToFileName(fileName: string): string { + const file = splitExtensionFromFileName(fileName); + let newFileName = `${file.fileName}-${DateUtils.getDBTime()}`; + // Replace illegal characters before trying to download the attachment. + newFileName = newFileName.replace(CONST.REGEX.ILLEGAL_FILENAME_CHARACTERS, '_'); + if (file.fileExtension) { + newFileName += `.${file.fileExtension}`; + } + return newFileName; +} + +/** + * Reads a locally uploaded file + * @param path - the blob url of the locally uploaded file + * @param fileName - name of the file to read + */ +const readFileAsync: ReadFileAsync = (path, fileName, onSuccess, onFailure = () => {}, fileType = '') => + new Promise((resolve) => { + if (!path) { + resolve(); + onFailure('[FileUtils] Path not specified'); + return; + } + fetch(path) + .then((res) => { + // For some reason, fetch is "Unable to read uploaded file" + // on Android even though the blob is returned, so we'll ignore + // in that case + if (!res.ok && Platform.OS !== 'android') { + throw Error(res.statusText); + } + res.blob() + .then((blob) => { + // On Android devices, fetching blob for a file with name containing spaces fails to retrieve the type of file. + // In this case, let us fallback on fileType provided by the caller of this function. + const file = new File([blob], cleanFileName(fileName), {type: blob.type || fileType}); + file.source = path; + // For some reason, the File object on iOS does not have a uri property + // so images aren't uploaded correctly to the backend + file.uri = path; + onSuccess(file); + resolve(file); + }) + .catch((e) => { + console.debug('[FileUtils] Could not read uploaded file', e); + onFailure(e); + resolve(); + }); + }) + .catch((e) => { + console.debug('[FileUtils] Could not read uploaded file', e); + onFailure(e); + resolve(); + }); + }); + +/** + * 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 base64 - The base64 encoded image string. + * @param filename - Desired filename for the File instance. + * @returns 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: string, filename: string): File { + // 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; +} + +function validateImageForCorruption(file: FileObject): Promise<{width: number; height: number} | void> { + if (!Str.isImage(file.name ?? '') || !file.uri) { + return Promise.resolve(); + } + return new Promise((resolve, reject) => { + ImageSize.getSize(file.uri ?? '') + .then(() => resolve()) + .catch(() => reject(new Error('Error reading file: The file is corrupted'))); + }); +} + +/** Verify file format based on the magic bytes of the file - some formats might be identified by multiple signatures */ +function verifyFileFormat({fileUri, formatSignatures}: {fileUri: string; formatSignatures: readonly string[]}) { + return fetch(fileUri) + .then((file) => file.arrayBuffer()) + .then((arrayBuffer) => { + const uintArray = new Uint8Array(arrayBuffer, 4, 12); + + const hexString = Array.from(uintArray) + .map((b) => b.toString(16).padStart(2, '0')) + .join(''); + + return hexString; + }) + .then((hexSignature) => { + return formatSignatures.some((signature) => hexSignature.startsWith(signature)); + }); +} + +function isLocalFile(receiptUri?: string | number): boolean { + if (!receiptUri) { + return false; + } + return typeof receiptUri === 'number' || receiptUri?.startsWith('blob:') || receiptUri?.startsWith('file:') || receiptUri?.startsWith('/'); +} + +function getFileResolution(targetFile: FileObject | undefined): Promise<{width: number; height: number} | null> { + if (!targetFile) { + return Promise.resolve(null); + } + + // If the file already has width and height, return them directly + if ('width' in targetFile && 'height' in targetFile) { + return Promise.resolve({width: targetFile.width ?? 0, height: targetFile.height ?? 0}); + } + + // Otherwise, attempt to get the image resolution + return getImageResolution(targetFile) + .then(({width, height}) => ({width, height})) + .catch((error: Error) => { + Log.hmmm('Failed to get image resolution:', error); + return null; + }); +} + +function isHighResolutionImage(resolution: {width: number; height: number} | null): boolean { + return resolution !== null && (resolution.width > CONST.IMAGE_HIGH_RESOLUTION_THRESHOLD || resolution.height > CONST.IMAGE_HIGH_RESOLUTION_THRESHOLD); +} + +const getImageDimensionsAfterResize = (file: FileObject) => + ImageSize.getSize(file.uri ?? '').then(({width, height}) => { + const scaleFactor = CONST.MAX_IMAGE_DIMENSION / (width < height ? height : width); + const newWidth = Math.max(1, width * scaleFactor); + const newHeight = Math.max(1, height * scaleFactor); + + return {width: newWidth, height: newHeight}; + }); + +const resizeImageIfNeeded = (file: FileObject) => { + if (!file || !Str.isImage(file.name ?? '') || (file?.size ?? 0) <= CONST.API_ATTACHMENT_VALIDATIONS.MAX_SIZE) { + return Promise.resolve(file); + } + return getImageDimensionsAfterResize(file).then(({width, height}) => getImageManipulator({fileUri: file.uri ?? '', width, height, fileName: file.name ?? '', type: file.type})); +}; +export { + showGeneralErrorAlert, + showSuccessAlert, + showPermissionErrorAlert, + showCameraPermissionsAlert, + splitExtensionFromFileName, + getFileName, + getFileType, + cleanFileName, + appendTimeToFileName, + readFileAsync, + base64ToFile, + isLocalFile, + validateImageForCorruption, + isImage, + getFileResolution, + isHighResolutionImage, + verifyFileFormat, + getImageDimensionsAfterResize, + resizeImageIfNeeded, +}; diff --git a/src/libs/fileDownload/FileUtils.ts b/src/libs/fileDownload/FileUtils.ts index 5ad8a8255e51..d0485716f660 100644 --- a/src/libs/fileDownload/FileUtils.ts +++ b/src/libs/fileDownload/FileUtils.ts @@ -2,9 +2,7 @@ import {Str} from 'expensify-common'; import {Alert, Linking, Platform} from 'react-native'; import ImageSize from 'react-native-image-size'; import type {FileObject} from '@components/AttachmentModal'; -import {updateLastScreen} from '@libs/actions/App'; import DateUtils from '@libs/DateUtils'; -import getPlatform from '@libs/getPlatform'; import * as Localize from '@libs/Localize'; import Log from '@libs/Log'; import type {IOUType} from '@src/CONST'; @@ -66,6 +64,7 @@ function showPermissionErrorAlert() { /** * Inform the users when they need to grant camera access and guide them to settings */ +// eslint-disable-next-line @typescript-eslint/no-unused-vars function showCameraPermissionsAlert(screenName: IOUType | '') { Alert.alert( Localize.translateLocal('attachmentPicker.cameraPermissionRequired'), @@ -79,11 +78,6 @@ function showCameraPermissionsAlert(screenName: IOUType | '') { text: Localize.translateLocal('common.settings'), onPress: () => { Linking.openSettings(); - // In case of ios, app reload when we enable camera permission from settings - // we are saving last screen so we can navigate to it after app reload - if (getPlatform() === CONST.PLATFORM.IOS && screenName) { - updateLastScreen(screenName); - } }, }, ], From d2e9ba4b78edb5c1c8b589ed69ccae61fc1e061b Mon Sep 17 00:00:00 2001 From: Ravindra Singh Date: Thu, 22 Aug 2024 11:44:51 +0530 Subject: [PATCH 06/13] make navigation after app reload generic --- src/ONYXKEYS.ts | 4 +- .../AttachmentPicker/index.native.tsx | 2 +- src/components/AttachmentPicker/types.ts | 4 +- src/libs/actions/App.ts | 4 +- src/libs/fileDownload/FileUtils.ios.ts | 8 ++-- src/libs/fileDownload/FileUtils.ts | 4 +- .../SidebarScreen/BaseSidebarScreen.tsx | 39 +++++++++++++------ .../step/IOURequestStepScan/index.native.tsx | 5 ++- src/types/onyx/OnyxCommon.ts | 10 ++++- 9 files changed, 52 insertions(+), 28 deletions(-) diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index fcf6ff05be15..e122de3b1ed9 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -1,9 +1,9 @@ import type {ValueOf} from 'type-fest'; -import type {IOUType} from './CONST'; import type CONST from './CONST'; import type * as FormTypes from './types/form'; import type * as OnyxTypes from './types/onyx'; import type Onboarding from './types/onyx/Onboarding'; +import type {LastScreen} from './types/onyx/OnyxCommon'; import type AssertTypesEqual from './types/utils/AssertTypesEqual'; import type DeepValueOf from './types/utils/DeepValueOf'; @@ -895,7 +895,7 @@ type OnyxValuesMapping = { [ONYXKEYS.NVP_WORKSPACE_TOOLTIP]: OnyxTypes.WorkspaceTooltip; [ONYXKEYS.NVP_PRIVATE_CANCELLATION_DETAILS]: OnyxTypes.CancellationDetails[]; [ONYXKEYS.APPROVAL_WORKFLOW]: OnyxTypes.ApprovalWorkflowOnyx; - [ONYXKEYS.LAST_SCREEN]: IOUType | ''; + [ONYXKEYS.LAST_SCREEN]: LastScreen; }; type OnyxValues = OnyxValuesMapping & OnyxCollectionValuesMapping & OnyxFormValuesMapping & OnyxFormDraftValuesMapping; diff --git a/src/components/AttachmentPicker/index.native.tsx b/src/components/AttachmentPicker/index.native.tsx index 54127e2d06be..1fbec0becbc8 100644 --- a/src/components/AttachmentPicker/index.native.tsx +++ b/src/components/AttachmentPicker/index.native.tsx @@ -112,7 +112,7 @@ const getDataForUpload = (fileData: FileResponse): Promise => { * a callback. This is the ios/android implementation * opening a modal with attachment options */ -function AttachmentPicker({type = CONST.ATTACHMENT_PICKER_TYPE.FILE, children, shouldHideCameraOption = false, lastScreen = ''}: AttachmentPickerProps) { +function AttachmentPicker({type = CONST.ATTACHMENT_PICKER_TYPE.FILE, children, shouldHideCameraOption = false, lastScreen}: AttachmentPickerProps) { const styles = useThemeStyles(); const [isVisible, setIsVisible] = useState(false); diff --git a/src/components/AttachmentPicker/types.ts b/src/components/AttachmentPicker/types.ts index 481152e5154d..6bc8bd258b69 100644 --- a/src/components/AttachmentPicker/types.ts +++ b/src/components/AttachmentPicker/types.ts @@ -2,7 +2,7 @@ import type {ReactNode} from 'react'; import type {ValueOf} from 'type-fest'; import type {FileObject} from '@components/AttachmentModal'; import type CONST from '@src/CONST'; -import type {IOUType} from '@src/CONST'; +import type {LastScreen} from '@src/types/onyx/OnyxCommon'; type PickerOptions = { /** A callback that will be called with the selected attachment. */ @@ -43,7 +43,7 @@ type AttachmentPickerProps = { type?: ValueOf; /** Last screen to save for navigating after granting camera permission from settings in ios */ - lastScreen?: IOUType | ''; + lastScreen?: LastScreen; acceptedFileTypes?: Array>; }; diff --git a/src/libs/actions/App.ts b/src/libs/actions/App.ts index 77e9546e45b0..f5a85f505310 100644 --- a/src/libs/actions/App.ts +++ b/src/libs/actions/App.ts @@ -17,13 +17,13 @@ import Navigation from '@libs/Navigation/Navigation'; import Performance from '@libs/Performance'; import * as ReportActionsUtils from '@libs/ReportActionsUtils'; import * as SessionUtils from '@libs/SessionUtils'; -import type {IOUType} from '@src/CONST'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {OnyxKey} from '@src/ONYXKEYS'; import type {Route} from '@src/ROUTES'; import ROUTES from '@src/ROUTES'; import type * as OnyxTypes from '@src/types/onyx'; +import type {LastScreen} from '@src/types/onyx/OnyxCommon'; import type {OnyxData} from '@src/types/onyx/Request'; import * as Policy from './Policy/Policy'; import * as Session from './Session'; @@ -497,7 +497,7 @@ function updateLastVisitedPath(path: string) { Onyx.merge(ONYXKEYS.LAST_VISITED_PATH, path); } -function updateLastScreen(screen: IOUType | '') { +function updateLastScreen(screen: LastScreen) { Onyx.set(ONYXKEYS.LAST_SCREEN, screen); } diff --git a/src/libs/fileDownload/FileUtils.ios.ts b/src/libs/fileDownload/FileUtils.ios.ts index 9e42b0166b9d..6d0ce109ea1a 100644 --- a/src/libs/fileDownload/FileUtils.ios.ts +++ b/src/libs/fileDownload/FileUtils.ios.ts @@ -6,8 +6,8 @@ import {updateLastScreen} from '@libs/actions/App'; import DateUtils from '@libs/DateUtils'; import * as Localize from '@libs/Localize'; import Log from '@libs/Log'; -import type {IOUType} from '@src/CONST'; import CONST from '@src/CONST'; +import type {LastScreen} from '@src/types/onyx/OnyxCommon'; import getImageManipulator from './getImageManipulator'; import getImageResolution from './getImageResolution'; import type {ReadFileAsync, SplitExtensionFromFileName} from './types'; @@ -65,7 +65,7 @@ function showPermissionErrorAlert() { /** * Inform the users when they need to grant camera access and guide them to settings */ -function showCameraPermissionsAlert(screenName: IOUType | '') { +function showCameraPermissionsAlert(lastScreen: LastScreen) { Alert.alert( Localize.translateLocal('attachmentPicker.cameraPermissionRequired'), Localize.translateLocal('attachmentPicker.expensifyDoesntHaveAccessToCamera'), @@ -80,8 +80,8 @@ function showCameraPermissionsAlert(screenName: IOUType | '') { Linking.openSettings(); // In case of ios, app reload when we enable camera permission from settings // we are saving last screen so we can navigate to it after app reload - if (screenName) { - updateLastScreen(screenName); + if (lastScreen) { + updateLastScreen(lastScreen); } }, }, diff --git a/src/libs/fileDownload/FileUtils.ts b/src/libs/fileDownload/FileUtils.ts index d0485716f660..95dfa5d050c4 100644 --- a/src/libs/fileDownload/FileUtils.ts +++ b/src/libs/fileDownload/FileUtils.ts @@ -5,8 +5,8 @@ import type {FileObject} from '@components/AttachmentModal'; import DateUtils from '@libs/DateUtils'; import * as Localize from '@libs/Localize'; import Log from '@libs/Log'; -import type {IOUType} from '@src/CONST'; import CONST from '@src/CONST'; +import type {LastScreen} from '@src/types/onyx/OnyxCommon'; import getImageManipulator from './getImageManipulator'; import getImageResolution from './getImageResolution'; import type {ReadFileAsync, SplitExtensionFromFileName} from './types'; @@ -65,7 +65,7 @@ function showPermissionErrorAlert() { * Inform the users when they need to grant camera access and guide them to settings */ // eslint-disable-next-line @typescript-eslint/no-unused-vars -function showCameraPermissionsAlert(screenName: IOUType | '') { +function showCameraPermissionsAlert(screenName: LastScreen | undefined) { Alert.alert( Localize.translateLocal('attachmentPicker.cameraPermissionRequired'), Localize.translateLocal('attachmentPicker.expensifyDoesntHaveAccessToCamera'), diff --git a/src/pages/home/sidebar/SidebarScreen/BaseSidebarScreen.tsx b/src/pages/home/sidebar/SidebarScreen/BaseSidebarScreen.tsx index cfbd7127ead5..78c241ba0189 100644 --- a/src/pages/home/sidebar/SidebarScreen/BaseSidebarScreen.tsx +++ b/src/pages/home/sidebar/SidebarScreen/BaseSidebarScreen.tsx @@ -17,9 +17,10 @@ import * as ReportUtils from '@libs/ReportUtils'; import SidebarLinksData from '@pages/home/sidebar/SidebarLinksData'; import * as IOU from '@userActions/IOU'; import Timing from '@userActions/Timing'; -import type {IOUType} from '@src/CONST'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; +import SCREENS from '@src/SCREENS'; +import type {LastScreen} from '@src/types/onyx/OnyxCommon'; /** * Function called when a pinned chat is selected. @@ -31,7 +32,7 @@ const startTimer = () => { type BaseSidebarScreenOnyxProps = { /** last visited screen */ - lastScreen: OnyxEntry; + lastScreen: OnyxEntry; }; type BaseSidebarScreenProps = BaseSidebarScreenOnyxProps; @@ -61,18 +62,31 @@ function BaseSidebarScreen({lastScreen}: BaseSidebarScreenProps) { * This will only works for ios application because we are saving last screen only for ios */ useEffect(() => { - if (!lastScreen) { + if (!lastScreen || lastScreen?.screenName === '') { return; } - interceptAnonymousUser(() => { - updateLastScreen(''); - IOU.startMoneyRequest( - lastScreen, - // When starting to create an expense from the global FAB, there is not an existing report yet. A random optimistic reportID is generated and used - // for all of the routes in the creation flow. - ReportUtils.generateReportID(), - ); - }); + + updateLastScreen({screenName: '', iouType: undefined}); + + switch (lastScreen.screenName) { + case SCREENS.RIGHT_MODAL.MONEY_REQUEST: + interceptAnonymousUser(() => { + if (!lastScreen.iouType) { + return; + } + + IOU.startMoneyRequest( + lastScreen.iouType, + // When starting to create an expense from the global FAB, there is not an existing report yet. A random optimistic reportID is generated and used + // for all of the routes in the creation flow. + ReportUtils.generateReportID(), + ); + }); + break; + + default: + break; + } // disabling this rule, as we want this to run only on the first render // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps }, []); @@ -108,5 +122,6 @@ BaseSidebarScreen.displayName = 'BaseSidebarScreen'; export default withOnyx({ lastScreen: { key: ONYXKEYS.LAST_SCREEN, + selector: (lastScreen) => lastScreen, }, })(BaseSidebarScreen); diff --git a/src/pages/iou/request/step/IOURequestStepScan/index.native.tsx b/src/pages/iou/request/step/IOURequestStepScan/index.native.tsx index e77905acdbb4..b34c51e09ebf 100644 --- a/src/pages/iou/request/step/IOURequestStepScan/index.native.tsx +++ b/src/pages/iou/request/step/IOURequestStepScan/index.native.tsx @@ -39,6 +39,7 @@ import * as IOU from '@userActions/IOU'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; +import SCREENS from '@src/SCREENS'; import type {Receipt} from '@src/types/onyx/Transaction'; import CameraPermission from './CameraPermission'; import NavigationAwareCamera from './NavigationAwareCamera/Camera'; @@ -98,7 +99,7 @@ function IOURequestStepScan({ setCameraPermissionStatus(status); if (status === RESULTS.BLOCKED) { - FileUtils.showCameraPermissionsAlert(iouType); + FileUtils.showCameraPermissionsAlert({screenName: SCREENS.RIGHT_MODAL.MONEY_REQUEST, iouType}); } }) .catch(() => { @@ -561,7 +562,7 @@ function IOURequestStepScan({ )} - + {({openPicker}) => ( | undefined; +}; + +export type {Icon, PendingAction, PendingFields, ErrorFields, Errors, AvatarType, OnyxValueWithOfflineFeedback, LastScreen}; From 0b54f776594a1ab693478b552beee8863dc2a51a Mon Sep 17 00:00:00 2001 From: Ravindra Singh Date: Thu, 22 Aug 2024 17:44:10 +0530 Subject: [PATCH 07/13] use activeRoute for saving last screen --- src/ONYXKEYS.ts | 3 +- .../AttachmentPicker/index.native.tsx | 6 +- src/libs/actions/App.ts | 3 +- src/libs/fileDownload/FileUtils.ios.ts | 349 ------------------ src/libs/fileDownload/FileUtils.ts | 8 +- src/libs/saveLastScreen/index.ios.ts | 6 + src/libs/saveLastScreen/index.ts | 3 + .../SidebarScreen/BaseSidebarScreen.tsx | 33 +- .../step/IOURequestStepScan/index.native.tsx | 4 +- src/types/onyx/OnyxCommon.ts | 10 +- 10 files changed, 26 insertions(+), 399 deletions(-) delete mode 100644 src/libs/fileDownload/FileUtils.ios.ts create mode 100644 src/libs/saveLastScreen/index.ios.ts create mode 100644 src/libs/saveLastScreen/index.ts diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index e122de3b1ed9..c3797704f914 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -3,7 +3,6 @@ import type CONST from './CONST'; import type * as FormTypes from './types/form'; import type * as OnyxTypes from './types/onyx'; import type Onboarding from './types/onyx/Onboarding'; -import type {LastScreen} from './types/onyx/OnyxCommon'; import type AssertTypesEqual from './types/utils/AssertTypesEqual'; import type DeepValueOf from './types/utils/DeepValueOf'; @@ -895,7 +894,7 @@ type OnyxValuesMapping = { [ONYXKEYS.NVP_WORKSPACE_TOOLTIP]: OnyxTypes.WorkspaceTooltip; [ONYXKEYS.NVP_PRIVATE_CANCELLATION_DETAILS]: OnyxTypes.CancellationDetails[]; [ONYXKEYS.APPROVAL_WORKFLOW]: OnyxTypes.ApprovalWorkflowOnyx; - [ONYXKEYS.LAST_SCREEN]: LastScreen; + [ONYXKEYS.LAST_SCREEN]: string; }; type OnyxValues = OnyxValuesMapping & OnyxCollectionValuesMapping & OnyxFormValuesMapping & OnyxFormDraftValuesMapping; diff --git a/src/components/AttachmentPicker/index.native.tsx b/src/components/AttachmentPicker/index.native.tsx index 1fbec0becbc8..edcdabed9101 100644 --- a/src/components/AttachmentPicker/index.native.tsx +++ b/src/components/AttachmentPicker/index.native.tsx @@ -112,7 +112,7 @@ const getDataForUpload = (fileData: FileResponse): Promise => { * a callback. This is the ios/android implementation * opening a modal with attachment options */ -function AttachmentPicker({type = CONST.ATTACHMENT_PICKER_TYPE.FILE, children, shouldHideCameraOption = false, lastScreen}: AttachmentPickerProps) { +function AttachmentPicker({type = CONST.ATTACHMENT_PICKER_TYPE.FILE, children, shouldHideCameraOption = false}: AttachmentPickerProps) { const styles = useThemeStyles(); const [isVisible, setIsVisible] = useState(false); @@ -150,7 +150,7 @@ function AttachmentPicker({type = CONST.ATTACHMENT_PICKER_TYPE.FILE, children, s if (response.errorCode) { switch (response.errorCode) { case 'permission': - FileUtils.showCameraPermissionsAlert(lastScreen); + FileUtils.showCameraPermissionsAlert(); return resolve(); default: showGeneralAlert(); @@ -196,7 +196,7 @@ function AttachmentPicker({type = CONST.ATTACHMENT_PICKER_TYPE.FILE, children, s } }); }), - [showGeneralAlert, type, lastScreen], + [showGeneralAlert, type], ); /** * Launch the DocumentPicker. Results are in the same format as ImagePicker diff --git a/src/libs/actions/App.ts b/src/libs/actions/App.ts index f5a85f505310..8fc7e2d4dfbe 100644 --- a/src/libs/actions/App.ts +++ b/src/libs/actions/App.ts @@ -23,7 +23,6 @@ import type {OnyxKey} from '@src/ONYXKEYS'; import type {Route} from '@src/ROUTES'; import ROUTES from '@src/ROUTES'; import type * as OnyxTypes from '@src/types/onyx'; -import type {LastScreen} from '@src/types/onyx/OnyxCommon'; import type {OnyxData} from '@src/types/onyx/Request'; import * as Policy from './Policy/Policy'; import * as Session from './Session'; @@ -497,7 +496,7 @@ function updateLastVisitedPath(path: string) { Onyx.merge(ONYXKEYS.LAST_VISITED_PATH, path); } -function updateLastScreen(screen: LastScreen) { +function updateLastScreen(screen: string) { Onyx.set(ONYXKEYS.LAST_SCREEN, screen); } diff --git a/src/libs/fileDownload/FileUtils.ios.ts b/src/libs/fileDownload/FileUtils.ios.ts deleted file mode 100644 index 6d0ce109ea1a..000000000000 --- a/src/libs/fileDownload/FileUtils.ios.ts +++ /dev/null @@ -1,349 +0,0 @@ -import {Str} from 'expensify-common'; -import {Alert, Linking, Platform} from 'react-native'; -import ImageSize from 'react-native-image-size'; -import type {FileObject} from '@components/AttachmentModal'; -import {updateLastScreen} from '@libs/actions/App'; -import DateUtils from '@libs/DateUtils'; -import * as Localize from '@libs/Localize'; -import Log from '@libs/Log'; -import CONST from '@src/CONST'; -import type {LastScreen} from '@src/types/onyx/OnyxCommon'; -import getImageManipulator from './getImageManipulator'; -import getImageResolution from './getImageResolution'; -import type {ReadFileAsync, SplitExtensionFromFileName} from './types'; - -/** - * Show alert on successful attachment download - * @param successMessage - */ -function showSuccessAlert(successMessage?: string) { - Alert.alert( - Localize.translateLocal('fileDownload.success.title'), - // successMessage can be an empty string and we want to default to `Localize.translateLocal('fileDownload.success.message')` - // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - successMessage || Localize.translateLocal('fileDownload.success.message'), - [ - { - text: Localize.translateLocal('common.ok'), - style: 'cancel', - }, - ], - {cancelable: false}, - ); -} - -/** - * Show alert on attachment download error - */ -function showGeneralErrorAlert() { - Alert.alert(Localize.translateLocal('fileDownload.generalError.title'), Localize.translateLocal('fileDownload.generalError.message'), [ - { - text: Localize.translateLocal('common.cancel'), - style: 'cancel', - }, - ]); -} - -/** - * Show alert on attachment download permissions error - */ -function showPermissionErrorAlert() { - Alert.alert(Localize.translateLocal('fileDownload.permissionError.title'), Localize.translateLocal('fileDownload.permissionError.message'), [ - { - text: Localize.translateLocal('common.cancel'), - style: 'cancel', - }, - { - text: Localize.translateLocal('common.settings'), - onPress: () => { - Linking.openSettings(); - }, - }, - ]); -} - -/** - * Inform the users when they need to grant camera access and guide them to settings - */ -function showCameraPermissionsAlert(lastScreen: LastScreen) { - Alert.alert( - Localize.translateLocal('attachmentPicker.cameraPermissionRequired'), - Localize.translateLocal('attachmentPicker.expensifyDoesntHaveAccessToCamera'), - [ - { - text: Localize.translateLocal('common.cancel'), - style: 'cancel', - }, - { - text: Localize.translateLocal('common.settings'), - onPress: () => { - Linking.openSettings(); - // In case of ios, app reload when we enable camera permission from settings - // we are saving last screen so we can navigate to it after app reload - if (lastScreen) { - updateLastScreen(lastScreen); - } - }, - }, - ], - {cancelable: false}, - ); -} - -/** - * Extracts a filename from a given URL and sanitizes it for file system usage. - * - * This function takes a URL as input and performs the following operations: - * 1. Extracts the last segment of the URL. - * 2. Decodes the extracted segment from URL encoding to a plain string for better readability. - * 3. Replaces any characters in the decoded string that are illegal in file names - * with underscores. - */ -function getFileName(url: string): string { - const fileName = url.split('/').pop()?.split('?')[0].split('#')[0] ?? ''; - - if (!fileName) { - Log.warn('[FileUtils] Could not get attachment name', {url}); - } - - return decodeURIComponent(fileName).replace(CONST.REGEX.ILLEGAL_FILENAME_CHARACTERS, '_'); -} - -function isImage(fileName: string): boolean { - return CONST.FILE_TYPE_REGEX.IMAGE.test(fileName); -} - -function isVideo(fileName: string): boolean { - return CONST.FILE_TYPE_REGEX.VIDEO.test(fileName); -} - -/** - * Returns file type based on the uri - */ -function getFileType(fileUrl: string): string | undefined { - if (!fileUrl) { - return; - } - - const fileName = getFileName(fileUrl); - - if (!fileName) { - return; - } - - if (isImage(fileName)) { - return CONST.ATTACHMENT_FILE_TYPE.IMAGE; - } - if (isVideo(fileName)) { - return CONST.ATTACHMENT_FILE_TYPE.VIDEO; - } - return CONST.ATTACHMENT_FILE_TYPE.FILE; -} - -/** - * Returns the filename split into fileName and fileExtension - */ -const splitExtensionFromFileName: SplitExtensionFromFileName = (fullFileName) => { - const fileName = fullFileName.trim(); - const splitFileName = fileName.split('.'); - const fileExtension = splitFileName.length > 1 ? splitFileName.pop() : ''; - return {fileName: splitFileName.join('.'), fileExtension: fileExtension ?? ''}; -}; - -/** - * Returns the filename replacing special characters with underscore - */ -function cleanFileName(fileName: string): string { - return fileName.replace(/[^a-zA-Z0-9\-._]/g, '_'); -} - -function appendTimeToFileName(fileName: string): string { - const file = splitExtensionFromFileName(fileName); - let newFileName = `${file.fileName}-${DateUtils.getDBTime()}`; - // Replace illegal characters before trying to download the attachment. - newFileName = newFileName.replace(CONST.REGEX.ILLEGAL_FILENAME_CHARACTERS, '_'); - if (file.fileExtension) { - newFileName += `.${file.fileExtension}`; - } - return newFileName; -} - -/** - * Reads a locally uploaded file - * @param path - the blob url of the locally uploaded file - * @param fileName - name of the file to read - */ -const readFileAsync: ReadFileAsync = (path, fileName, onSuccess, onFailure = () => {}, fileType = '') => - new Promise((resolve) => { - if (!path) { - resolve(); - onFailure('[FileUtils] Path not specified'); - return; - } - fetch(path) - .then((res) => { - // For some reason, fetch is "Unable to read uploaded file" - // on Android even though the blob is returned, so we'll ignore - // in that case - if (!res.ok && Platform.OS !== 'android') { - throw Error(res.statusText); - } - res.blob() - .then((blob) => { - // On Android devices, fetching blob for a file with name containing spaces fails to retrieve the type of file. - // In this case, let us fallback on fileType provided by the caller of this function. - const file = new File([blob], cleanFileName(fileName), {type: blob.type || fileType}); - file.source = path; - // For some reason, the File object on iOS does not have a uri property - // so images aren't uploaded correctly to the backend - file.uri = path; - onSuccess(file); - resolve(file); - }) - .catch((e) => { - console.debug('[FileUtils] Could not read uploaded file', e); - onFailure(e); - resolve(); - }); - }) - .catch((e) => { - console.debug('[FileUtils] Could not read uploaded file', e); - onFailure(e); - resolve(); - }); - }); - -/** - * 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 base64 - The base64 encoded image string. - * @param filename - Desired filename for the File instance. - * @returns 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: string, filename: string): File { - // 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; -} - -function validateImageForCorruption(file: FileObject): Promise<{width: number; height: number} | void> { - if (!Str.isImage(file.name ?? '') || !file.uri) { - return Promise.resolve(); - } - return new Promise((resolve, reject) => { - ImageSize.getSize(file.uri ?? '') - .then(() => resolve()) - .catch(() => reject(new Error('Error reading file: The file is corrupted'))); - }); -} - -/** Verify file format based on the magic bytes of the file - some formats might be identified by multiple signatures */ -function verifyFileFormat({fileUri, formatSignatures}: {fileUri: string; formatSignatures: readonly string[]}) { - return fetch(fileUri) - .then((file) => file.arrayBuffer()) - .then((arrayBuffer) => { - const uintArray = new Uint8Array(arrayBuffer, 4, 12); - - const hexString = Array.from(uintArray) - .map((b) => b.toString(16).padStart(2, '0')) - .join(''); - - return hexString; - }) - .then((hexSignature) => { - return formatSignatures.some((signature) => hexSignature.startsWith(signature)); - }); -} - -function isLocalFile(receiptUri?: string | number): boolean { - if (!receiptUri) { - return false; - } - return typeof receiptUri === 'number' || receiptUri?.startsWith('blob:') || receiptUri?.startsWith('file:') || receiptUri?.startsWith('/'); -} - -function getFileResolution(targetFile: FileObject | undefined): Promise<{width: number; height: number} | null> { - if (!targetFile) { - return Promise.resolve(null); - } - - // If the file already has width and height, return them directly - if ('width' in targetFile && 'height' in targetFile) { - return Promise.resolve({width: targetFile.width ?? 0, height: targetFile.height ?? 0}); - } - - // Otherwise, attempt to get the image resolution - return getImageResolution(targetFile) - .then(({width, height}) => ({width, height})) - .catch((error: Error) => { - Log.hmmm('Failed to get image resolution:', error); - return null; - }); -} - -function isHighResolutionImage(resolution: {width: number; height: number} | null): boolean { - return resolution !== null && (resolution.width > CONST.IMAGE_HIGH_RESOLUTION_THRESHOLD || resolution.height > CONST.IMAGE_HIGH_RESOLUTION_THRESHOLD); -} - -const getImageDimensionsAfterResize = (file: FileObject) => - ImageSize.getSize(file.uri ?? '').then(({width, height}) => { - const scaleFactor = CONST.MAX_IMAGE_DIMENSION / (width < height ? height : width); - const newWidth = Math.max(1, width * scaleFactor); - const newHeight = Math.max(1, height * scaleFactor); - - return {width: newWidth, height: newHeight}; - }); - -const resizeImageIfNeeded = (file: FileObject) => { - if (!file || !Str.isImage(file.name ?? '') || (file?.size ?? 0) <= CONST.API_ATTACHMENT_VALIDATIONS.MAX_SIZE) { - return Promise.resolve(file); - } - return getImageDimensionsAfterResize(file).then(({width, height}) => getImageManipulator({fileUri: file.uri ?? '', width, height, fileName: file.name ?? '', type: file.type})); -}; -export { - showGeneralErrorAlert, - showSuccessAlert, - showPermissionErrorAlert, - showCameraPermissionsAlert, - splitExtensionFromFileName, - getFileName, - getFileType, - cleanFileName, - appendTimeToFileName, - readFileAsync, - base64ToFile, - isLocalFile, - validateImageForCorruption, - isImage, - getFileResolution, - isHighResolutionImage, - verifyFileFormat, - getImageDimensionsAfterResize, - resizeImageIfNeeded, -}; diff --git a/src/libs/fileDownload/FileUtils.ts b/src/libs/fileDownload/FileUtils.ts index 95dfa5d050c4..ab2bbd41ded2 100644 --- a/src/libs/fileDownload/FileUtils.ts +++ b/src/libs/fileDownload/FileUtils.ts @@ -5,8 +5,8 @@ import type {FileObject} from '@components/AttachmentModal'; import DateUtils from '@libs/DateUtils'; import * as Localize from '@libs/Localize'; import Log from '@libs/Log'; +import saveLastScreen from '@libs/saveLastScreen'; import CONST from '@src/CONST'; -import type {LastScreen} from '@src/types/onyx/OnyxCommon'; import getImageManipulator from './getImageManipulator'; import getImageResolution from './getImageResolution'; import type {ReadFileAsync, SplitExtensionFromFileName} from './types'; @@ -64,8 +64,7 @@ function showPermissionErrorAlert() { /** * Inform the users when they need to grant camera access and guide them to settings */ -// eslint-disable-next-line @typescript-eslint/no-unused-vars -function showCameraPermissionsAlert(screenName: LastScreen | undefined) { +function showCameraPermissionsAlert() { Alert.alert( Localize.translateLocal('attachmentPicker.cameraPermissionRequired'), Localize.translateLocal('attachmentPicker.expensifyDoesntHaveAccessToCamera'), @@ -78,6 +77,9 @@ function showCameraPermissionsAlert(screenName: LastScreen | undefined) { text: Localize.translateLocal('common.settings'), onPress: () => { Linking.openSettings(); + // In case of ios, app reload when we enable camera permission from settings + // we are saving last screen so we can navigate to it after app reload + saveLastScreen(); }, }, ], diff --git a/src/libs/saveLastScreen/index.ios.ts b/src/libs/saveLastScreen/index.ios.ts new file mode 100644 index 000000000000..d2f243019e7d --- /dev/null +++ b/src/libs/saveLastScreen/index.ios.ts @@ -0,0 +1,6 @@ +import {updateLastScreen} from '@libs/actions/App'; +import Navigation from '@libs/Navigation/Navigation'; + +export default function saveLastScreen() { + updateLastScreen(Navigation.getActiveRoute()); +} diff --git a/src/libs/saveLastScreen/index.ts b/src/libs/saveLastScreen/index.ts new file mode 100644 index 000000000000..33ce3e489298 --- /dev/null +++ b/src/libs/saveLastScreen/index.ts @@ -0,0 +1,3 @@ +/** we are only saving last screen in case of ios */ +// eslint-disable-next-line @typescript-eslint/no-unused-vars +export default function saveLastScreen() {} diff --git a/src/pages/home/sidebar/SidebarScreen/BaseSidebarScreen.tsx b/src/pages/home/sidebar/SidebarScreen/BaseSidebarScreen.tsx index 78c241ba0189..344c6dbcfc57 100644 --- a/src/pages/home/sidebar/SidebarScreen/BaseSidebarScreen.tsx +++ b/src/pages/home/sidebar/SidebarScreen/BaseSidebarScreen.tsx @@ -9,18 +9,13 @@ import useThemeStyles from '@hooks/useThemeStyles'; import {updateLastScreen} from '@libs/actions/App'; import {updateLastAccessedWorkspace} from '@libs/actions/Policy/Policy'; import * as Browser from '@libs/Browser'; -import interceptAnonymousUser from '@libs/interceptAnonymousUser'; import TopBar from '@libs/Navigation/AppNavigator/createCustomBottomTabNavigator/TopBar'; import Navigation from '@libs/Navigation/Navigation'; import Performance from '@libs/Performance'; -import * as ReportUtils from '@libs/ReportUtils'; import SidebarLinksData from '@pages/home/sidebar/SidebarLinksData'; -import * as IOU from '@userActions/IOU'; import Timing from '@userActions/Timing'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import SCREENS from '@src/SCREENS'; -import type {LastScreen} from '@src/types/onyx/OnyxCommon'; /** * Function called when a pinned chat is selected. @@ -32,7 +27,7 @@ const startTimer = () => { type BaseSidebarScreenOnyxProps = { /** last visited screen */ - lastScreen: OnyxEntry; + lastScreen: OnyxEntry; }; type BaseSidebarScreenProps = BaseSidebarScreenOnyxProps; @@ -62,31 +57,12 @@ function BaseSidebarScreen({lastScreen}: BaseSidebarScreenProps) { * This will only works for ios application because we are saving last screen only for ios */ useEffect(() => { - if (!lastScreen || lastScreen?.screenName === '') { + if (!lastScreen) { return; } - updateLastScreen({screenName: '', iouType: undefined}); - - switch (lastScreen.screenName) { - case SCREENS.RIGHT_MODAL.MONEY_REQUEST: - interceptAnonymousUser(() => { - if (!lastScreen.iouType) { - return; - } - - IOU.startMoneyRequest( - lastScreen.iouType, - // When starting to create an expense from the global FAB, there is not an existing report yet. A random optimistic reportID is generated and used - // for all of the routes in the creation flow. - ReportUtils.generateReportID(), - ); - }); - break; - - default: - break; - } + updateLastScreen(''); + Navigation.navigate(lastScreen); // disabling this rule, as we want this to run only on the first render // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps }, []); @@ -122,6 +98,5 @@ BaseSidebarScreen.displayName = 'BaseSidebarScreen'; export default withOnyx({ lastScreen: { key: ONYXKEYS.LAST_SCREEN, - selector: (lastScreen) => lastScreen, }, })(BaseSidebarScreen); diff --git a/src/pages/iou/request/step/IOURequestStepScan/index.native.tsx b/src/pages/iou/request/step/IOURequestStepScan/index.native.tsx index b34c51e09ebf..f049977ef657 100644 --- a/src/pages/iou/request/step/IOURequestStepScan/index.native.tsx +++ b/src/pages/iou/request/step/IOURequestStepScan/index.native.tsx @@ -99,13 +99,13 @@ function IOURequestStepScan({ setCameraPermissionStatus(status); if (status === RESULTS.BLOCKED) { - FileUtils.showCameraPermissionsAlert({screenName: SCREENS.RIGHT_MODAL.MONEY_REQUEST, iouType}); + FileUtils.showCameraPermissionsAlert(); } }) .catch(() => { setCameraPermissionStatus(RESULTS.UNAVAILABLE); }); - }, [iouType]); + }, []); const focusIndicatorOpacity = useSharedValue(0); const focusIndicatorScale = useSharedValue(2); diff --git a/src/types/onyx/OnyxCommon.ts b/src/types/onyx/OnyxCommon.ts index 550aa771a6ea..4f97df07ea26 100644 --- a/src/types/onyx/OnyxCommon.ts +++ b/src/types/onyx/OnyxCommon.ts @@ -54,12 +54,4 @@ type Icon = { fill?: string; }; -/** Object store last screen data for navigating back after app reload due to permission change */ -type LastScreen = { - /** Name of the main screen */ - screenName: string; - /** iouType for launching request expense screen */ - iouType: ValueOf | undefined; -}; - -export type {Icon, PendingAction, PendingFields, ErrorFields, Errors, AvatarType, OnyxValueWithOfflineFeedback, LastScreen}; +export type {Icon, PendingAction, PendingFields, ErrorFields, Errors, AvatarType, OnyxValueWithOfflineFeedback}; From 20654ab2c14d70bb5be542f257a8709aaea0a341 Mon Sep 17 00:00:00 2001 From: Ravindra Singh Date: Thu, 22 Aug 2024 17:54:52 +0530 Subject: [PATCH 08/13] fix type issues --- src/components/AttachmentPicker/types.ts | 3 --- src/pages/home/sidebar/SidebarScreen/BaseSidebarScreen.tsx | 4 +++- .../iou/request/step/IOURequestStepScan/index.native.tsx | 3 +-- 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/src/components/AttachmentPicker/types.ts b/src/components/AttachmentPicker/types.ts index 6bc8bd258b69..057ec72de27e 100644 --- a/src/components/AttachmentPicker/types.ts +++ b/src/components/AttachmentPicker/types.ts @@ -2,7 +2,6 @@ import type {ReactNode} from 'react'; import type {ValueOf} from 'type-fest'; import type {FileObject} from '@components/AttachmentModal'; import type CONST from '@src/CONST'; -import type {LastScreen} from '@src/types/onyx/OnyxCommon'; type PickerOptions = { /** A callback that will be called with the selected attachment. */ @@ -42,8 +41,6 @@ type AttachmentPickerProps = { /** The types of files that can be selected with this picker. */ type?: ValueOf; - /** Last screen to save for navigating after granting camera permission from settings in ios */ - lastScreen?: LastScreen; acceptedFileTypes?: Array>; }; diff --git a/src/pages/home/sidebar/SidebarScreen/BaseSidebarScreen.tsx b/src/pages/home/sidebar/SidebarScreen/BaseSidebarScreen.tsx index 344c6dbcfc57..70abccff73d6 100644 --- a/src/pages/home/sidebar/SidebarScreen/BaseSidebarScreen.tsx +++ b/src/pages/home/sidebar/SidebarScreen/BaseSidebarScreen.tsx @@ -16,6 +16,7 @@ import SidebarLinksData from '@pages/home/sidebar/SidebarLinksData'; import Timing from '@userActions/Timing'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; +import type {Route} from '@src/ROUTES'; /** * Function called when a pinned chat is selected. @@ -62,7 +63,8 @@ function BaseSidebarScreen({lastScreen}: BaseSidebarScreenProps) { } updateLastScreen(''); - Navigation.navigate(lastScreen); + const route = lastScreen as Route; + Navigation.navigate(route); // disabling this rule, as we want this to run only on the first render // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps }, []); diff --git a/src/pages/iou/request/step/IOURequestStepScan/index.native.tsx b/src/pages/iou/request/step/IOURequestStepScan/index.native.tsx index f049977ef657..bd425373efd3 100644 --- a/src/pages/iou/request/step/IOURequestStepScan/index.native.tsx +++ b/src/pages/iou/request/step/IOURequestStepScan/index.native.tsx @@ -39,7 +39,6 @@ import * as IOU from '@userActions/IOU'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; -import SCREENS from '@src/SCREENS'; import type {Receipt} from '@src/types/onyx/Transaction'; import CameraPermission from './CameraPermission'; import NavigationAwareCamera from './NavigationAwareCamera/Camera'; @@ -562,7 +561,7 @@ function IOURequestStepScan({ )} - + {({openPicker}) => ( Date: Fri, 23 Aug 2024 14:05:29 +0530 Subject: [PATCH 09/13] remove unnecessary useCallback --- .../step/IOURequestStepScan/index.native.tsx | 18 +++--------------- 1 file changed, 3 insertions(+), 15 deletions(-) diff --git a/src/pages/iou/request/step/IOURequestStepScan/index.native.tsx b/src/pages/iou/request/step/IOURequestStepScan/index.native.tsx index bd425373efd3..528b3ab1e88f 100644 --- a/src/pages/iou/request/step/IOURequestStepScan/index.native.tsx +++ b/src/pages/iou/request/step/IOURequestStepScan/index.native.tsx @@ -90,7 +90,7 @@ function IOURequestStepScan({ const {translate} = useLocalize(); - const askForPermissions = useCallback(() => { + const askForPermissions = () => { // There's no way we can check for the BLOCKED status without requesting the permission first // https://github.com/zoontek/react-native-permissions/blob/a836e114ce3a180b2b23916292c79841a267d828/README.md?plain=1#L670 CameraPermission.requestCameraPermission?.() @@ -104,7 +104,7 @@ function IOURequestStepScan({ .catch(() => { setCameraPermissionStatus(RESULTS.UNAVAILABLE); }); - }, []); + }; const focusIndicatorOpacity = useSharedValue(0); const focusIndicatorScale = useSharedValue(2); @@ -463,19 +463,7 @@ function IOURequestStepScan({ showCameraAlert(); Log.warn('Error taking photo', error); }); - }, [ - cameraPermissionStatus, - didCapturePhoto, - flash, - hasFlash, - user?.isMutedAllSounds, - askForPermissions, - translate, - transactionID, - action, - navigateToConfirmationStep, - updateScanAndNavigate, - ]); + }, [cameraPermissionStatus, didCapturePhoto, flash, hasFlash, user?.isMutedAllSounds, translate, transactionID, action, navigateToConfirmationStep, updateScanAndNavigate]); // Wait for camera permission status to render if (cameraPermissionStatus == null) { From 46dfb03c6b46802dc85917dc62e20431fe8bcded Mon Sep 17 00:00:00 2001 From: Ravindra Singh Date: Mon, 26 Aug 2024 12:12:35 +0530 Subject: [PATCH 10/13] refactor variable names and comments --- src/ONYXKEYS.ts | 6 ++--- src/libs/actions/App.ts | 6 ++--- src/libs/fileDownload/FileUtils.ts | 8 +++---- src/libs/saveLastRoute/index.ios.ts | 6 +++++ src/libs/saveLastRoute/index.ts | 3 +++ src/libs/saveLastScreen/index.ios.ts | 6 ----- src/libs/saveLastScreen/index.ts | 3 --- .../SidebarScreen/BaseSidebarScreen.tsx | 24 +++++++++---------- 8 files changed, 31 insertions(+), 31 deletions(-) create mode 100644 src/libs/saveLastRoute/index.ios.ts create mode 100644 src/libs/saveLastRoute/index.ts delete mode 100644 src/libs/saveLastScreen/index.ios.ts delete mode 100644 src/libs/saveLastScreen/index.ts diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index c3797704f914..9c5deda2feb3 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -400,8 +400,8 @@ const ONYXKEYS = { /** Stores the information about currently edited advanced approval workflow */ APPROVAL_WORKFLOW: 'approvalWorkflow', - /** screen to open after reloading app after changing app permission from settings */ - LAST_SCREEN: 'last_screen', + /** Stores the route to open after changing app permission from settings */ + LAST_ROUTE: 'lastRoute', /** Collection Keys */ COLLECTION: { @@ -894,7 +894,7 @@ type OnyxValuesMapping = { [ONYXKEYS.NVP_WORKSPACE_TOOLTIP]: OnyxTypes.WorkspaceTooltip; [ONYXKEYS.NVP_PRIVATE_CANCELLATION_DETAILS]: OnyxTypes.CancellationDetails[]; [ONYXKEYS.APPROVAL_WORKFLOW]: OnyxTypes.ApprovalWorkflowOnyx; - [ONYXKEYS.LAST_SCREEN]: string; + [ONYXKEYS.LAST_ROUTE]: string; }; type OnyxValues = OnyxValuesMapping & OnyxCollectionValuesMapping & OnyxFormValuesMapping & OnyxFormDraftValuesMapping; diff --git a/src/libs/actions/App.ts b/src/libs/actions/App.ts index 8fc7e2d4dfbe..1d196ab4f72c 100644 --- a/src/libs/actions/App.ts +++ b/src/libs/actions/App.ts @@ -496,8 +496,8 @@ function updateLastVisitedPath(path: string) { Onyx.merge(ONYXKEYS.LAST_VISITED_PATH, path); } -function updateLastScreen(screen: string) { - Onyx.set(ONYXKEYS.LAST_SCREEN, screen); +function updateLastRoute(screen: string) { + Onyx.set(ONYXKEYS.LAST_ROUTE, screen); } export { @@ -517,6 +517,6 @@ export { savePolicyDraftByNewWorkspace, createWorkspaceWithPolicyDraftAndNavigateToIt, updateLastVisitedPath, - updateLastScreen, + updateLastRoute, KEYS_TO_PRESERVE, }; diff --git a/src/libs/fileDownload/FileUtils.ts b/src/libs/fileDownload/FileUtils.ts index ab2bbd41ded2..05f29390ca14 100644 --- a/src/libs/fileDownload/FileUtils.ts +++ b/src/libs/fileDownload/FileUtils.ts @@ -5,7 +5,7 @@ import type {FileObject} from '@components/AttachmentModal'; import DateUtils from '@libs/DateUtils'; import * as Localize from '@libs/Localize'; import Log from '@libs/Log'; -import saveLastScreen from '@libs/saveLastScreen'; +import saveLastRoute from '@libs/saveLastRoute'; import CONST from '@src/CONST'; import getImageManipulator from './getImageManipulator'; import getImageResolution from './getImageResolution'; @@ -77,9 +77,9 @@ function showCameraPermissionsAlert() { text: Localize.translateLocal('common.settings'), onPress: () => { Linking.openSettings(); - // In case of ios, app reload when we enable camera permission from settings - // we are saving last screen so we can navigate to it after app reload - saveLastScreen(); + // In the case of ios, the App reloads when we update camera permission from settings + // we are saving last route so we can navigate to it after app reload + saveLastRoute(); }, }, ], diff --git a/src/libs/saveLastRoute/index.ios.ts b/src/libs/saveLastRoute/index.ios.ts new file mode 100644 index 000000000000..12107937800b --- /dev/null +++ b/src/libs/saveLastRoute/index.ios.ts @@ -0,0 +1,6 @@ +import {updateLastRoute} from '@libs/actions/App'; +import Navigation from '@libs/Navigation/Navigation'; + +export default function saveLastRoute() { + updateLastRoute(Navigation.getActiveRoute()); +} diff --git a/src/libs/saveLastRoute/index.ts b/src/libs/saveLastRoute/index.ts new file mode 100644 index 000000000000..4b532cf6ee75 --- /dev/null +++ b/src/libs/saveLastRoute/index.ts @@ -0,0 +1,3 @@ +const saveLastRoute = () => {}; + +export default saveLastRoute; diff --git a/src/libs/saveLastScreen/index.ios.ts b/src/libs/saveLastScreen/index.ios.ts deleted file mode 100644 index d2f243019e7d..000000000000 --- a/src/libs/saveLastScreen/index.ios.ts +++ /dev/null @@ -1,6 +0,0 @@ -import {updateLastScreen} from '@libs/actions/App'; -import Navigation from '@libs/Navigation/Navigation'; - -export default function saveLastScreen() { - updateLastScreen(Navigation.getActiveRoute()); -} diff --git a/src/libs/saveLastScreen/index.ts b/src/libs/saveLastScreen/index.ts deleted file mode 100644 index 33ce3e489298..000000000000 --- a/src/libs/saveLastScreen/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -/** we are only saving last screen in case of ios */ -// eslint-disable-next-line @typescript-eslint/no-unused-vars -export default function saveLastScreen() {} diff --git a/src/pages/home/sidebar/SidebarScreen/BaseSidebarScreen.tsx b/src/pages/home/sidebar/SidebarScreen/BaseSidebarScreen.tsx index 70abccff73d6..70a86681e533 100644 --- a/src/pages/home/sidebar/SidebarScreen/BaseSidebarScreen.tsx +++ b/src/pages/home/sidebar/SidebarScreen/BaseSidebarScreen.tsx @@ -6,7 +6,7 @@ import ScreenWrapper from '@components/ScreenWrapper'; import useActiveWorkspaceFromNavigationState from '@hooks/useActiveWorkspaceFromNavigationState'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; -import {updateLastScreen} from '@libs/actions/App'; +import {updateLastRoute} from '@libs/actions/App'; import {updateLastAccessedWorkspace} from '@libs/actions/Policy/Policy'; import * as Browser from '@libs/Browser'; import TopBar from '@libs/Navigation/AppNavigator/createCustomBottomTabNavigator/TopBar'; @@ -27,13 +27,13 @@ const startTimer = () => { }; type BaseSidebarScreenOnyxProps = { - /** last visited screen */ - lastScreen: OnyxEntry; + /** last visited route */ + lastRoute: OnyxEntry; }; type BaseSidebarScreenProps = BaseSidebarScreenOnyxProps; -function BaseSidebarScreen({lastScreen}: BaseSidebarScreenProps) { +function BaseSidebarScreen({lastRoute}: BaseSidebarScreenProps) { const styles = useThemeStyles(); const activeWorkspaceID = useActiveWorkspaceFromNavigationState(); const {translate} = useLocalize(); @@ -54,18 +54,18 @@ function BaseSidebarScreen({lastScreen}: BaseSidebarScreenProps) { }, [activeWorkspace, activeWorkspaceID]); /** - * Navigate to scan receipt screen after it enabling camera permission from setting - * This will only works for ios application because we are saving last screen only for ios + * Navigate to the last route after enabling camera permissions from the settings. + * This functionality is specific to the iOS application, as we are storing the last route only for iOS. */ useEffect(() => { - if (!lastScreen) { + if (!lastRoute) { return; } - updateLastScreen(''); - const route = lastScreen as Route; + updateLastRoute(''); + const route = lastRoute as Route; Navigation.navigate(route); - // disabling this rule, as we want this to run only on the first render + // Disabling this rule because we only want it to run on the first render. // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps }, []); @@ -98,7 +98,7 @@ function BaseSidebarScreen({lastScreen}: BaseSidebarScreenProps) { BaseSidebarScreen.displayName = 'BaseSidebarScreen'; export default withOnyx({ - lastScreen: { - key: ONYXKEYS.LAST_SCREEN, + lastRoute: { + key: ONYXKEYS.LAST_ROUTE, }, })(BaseSidebarScreen); From e623380a770d519488b46ee5115e12f08901daf8 Mon Sep 17 00:00:00 2001 From: Ravindra Singh Date: Mon, 26 Aug 2024 12:32:15 +0530 Subject: [PATCH 11/13] moved last route navigation logic to expensify.js --- src/Expensify.tsx | 25 +++++++++++++ .../SidebarScreen/BaseSidebarScreen.tsx | 36 ++----------------- 2 files changed, 28 insertions(+), 33 deletions(-) diff --git a/src/Expensify.tsx b/src/Expensify.tsx index 620243440384..3100bf59ca09 100644 --- a/src/Expensify.tsx +++ b/src/Expensify.tsx @@ -16,6 +16,7 @@ import UpdateAppModal from './components/UpdateAppModal'; import * as CONFIG from './CONFIG'; import CONST from './CONST'; import useLocalize from './hooks/useLocalize'; +import {updateLastRoute} from './libs/actions/App'; import * as EmojiPickerAction from './libs/actions/EmojiPickerAction'; import * as Report from './libs/actions/Report'; import * as User from './libs/actions/User'; @@ -75,6 +76,9 @@ type ExpensifyOnyxProps = { /** Last visited path in the app */ lastVisitedPath: OnyxEntry; + + /** last visited route */ + lastRoute: OnyxEntry; }; type ExpensifyProps = ExpensifyOnyxProps; @@ -94,6 +98,7 @@ function Expensify({ updateRequired = false, focusModeNotification = false, lastVisitedPath, + lastRoute, }: ExpensifyProps) { const appStateChangeListener = useRef(null); const [isNavigationReady, setIsNavigationReady] = useState(false); @@ -189,6 +194,7 @@ function Expensify({ focusModeNotification, isAuthenticated, lastVisitedPath, + lastRoute, }; Log.alert('[BootSplash] splash screen is still visible', {propsToLog}, false); } @@ -236,6 +242,22 @@ function Expensify({ Audio.setAudioModeAsync({playsInSilentModeIOS: true}); }, []); + /** + * Navigate to the last route after enabling camera permissions from the settings. + * This functionality is specific to the iOS application, as we are storing the last route only for iOS. + */ + useEffect(() => { + if (!isNavigationReady || !lastRoute) { + return; + } + + updateLastRoute(''); + const route = lastRoute as Route; + Navigation.navigate(route); + // Disabling this rule because we only want it to run on the first render. + // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps + }, [isNavigationReady]); + useEffect(() => { if (!isAuthenticated) { return; @@ -335,6 +357,9 @@ export default withOnyx({ lastVisitedPath: { key: ONYXKEYS.LAST_VISITED_PATH, }, + lastRoute: { + key: ONYXKEYS.LAST_ROUTE, + }, })(Expensify); export {SplashScreenHiddenContext}; diff --git a/src/pages/home/sidebar/SidebarScreen/BaseSidebarScreen.tsx b/src/pages/home/sidebar/SidebarScreen/BaseSidebarScreen.tsx index 70a86681e533..edc8dfb3cb3a 100644 --- a/src/pages/home/sidebar/SidebarScreen/BaseSidebarScreen.tsx +++ b/src/pages/home/sidebar/SidebarScreen/BaseSidebarScreen.tsx @@ -1,12 +1,10 @@ import React, {useEffect} from 'react'; import {View} from 'react-native'; -import {useOnyx, withOnyx} from 'react-native-onyx'; -import type {OnyxEntry} from 'react-native-onyx'; +import {useOnyx} from 'react-native-onyx'; import ScreenWrapper from '@components/ScreenWrapper'; import useActiveWorkspaceFromNavigationState from '@hooks/useActiveWorkspaceFromNavigationState'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; -import {updateLastRoute} from '@libs/actions/App'; import {updateLastAccessedWorkspace} from '@libs/actions/Policy/Policy'; import * as Browser from '@libs/Browser'; import TopBar from '@libs/Navigation/AppNavigator/createCustomBottomTabNavigator/TopBar'; @@ -16,7 +14,6 @@ import SidebarLinksData from '@pages/home/sidebar/SidebarLinksData'; import Timing from '@userActions/Timing'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import type {Route} from '@src/ROUTES'; /** * Function called when a pinned chat is selected. @@ -26,14 +23,7 @@ const startTimer = () => { Performance.markStart(CONST.TIMING.SWITCH_REPORT); }; -type BaseSidebarScreenOnyxProps = { - /** last visited route */ - lastRoute: OnyxEntry; -}; - -type BaseSidebarScreenProps = BaseSidebarScreenOnyxProps; - -function BaseSidebarScreen({lastRoute}: BaseSidebarScreenProps) { +function BaseSidebarScreen() { const styles = useThemeStyles(); const activeWorkspaceID = useActiveWorkspaceFromNavigationState(); const {translate} = useLocalize(); @@ -53,22 +43,6 @@ function BaseSidebarScreen({lastRoute}: BaseSidebarScreenProps) { updateLastAccessedWorkspace(undefined); }, [activeWorkspace, activeWorkspaceID]); - /** - * Navigate to the last route after enabling camera permissions from the settings. - * This functionality is specific to the iOS application, as we are storing the last route only for iOS. - */ - useEffect(() => { - if (!lastRoute) { - return; - } - - updateLastRoute(''); - const route = lastRoute as Route; - Navigation.navigate(route); - // Disabling this rule because we only want it to run on the first render. - // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps - }, []); - return ( ({ - lastRoute: { - key: ONYXKEYS.LAST_ROUTE, - }, -})(BaseSidebarScreen); +export default BaseSidebarScreen; From 3edf7ea91bbdba37885e40588c69aad8f3c5893c Mon Sep 17 00:00:00 2001 From: Ravindra Singh Date: Mon, 26 Aug 2024 14:20:16 +0530 Subject: [PATCH 12/13] use useOnyx for lastRoute and code cleanup --- src/Expensify.tsx | 17 ++--------------- 1 file changed, 2 insertions(+), 15 deletions(-) diff --git a/src/Expensify.tsx b/src/Expensify.tsx index 3100bf59ca09..9d391eeb8b7f 100644 --- a/src/Expensify.tsx +++ b/src/Expensify.tsx @@ -76,9 +76,6 @@ type ExpensifyOnyxProps = { /** Last visited path in the app */ lastVisitedPath: OnyxEntry; - - /** last visited route */ - lastRoute: OnyxEntry; }; type ExpensifyProps = ExpensifyOnyxProps; @@ -98,7 +95,6 @@ function Expensify({ updateRequired = false, focusModeNotification = false, lastVisitedPath, - lastRoute, }: ExpensifyProps) { const appStateChangeListener = useRef(null); const [isNavigationReady, setIsNavigationReady] = useState(false); @@ -108,6 +104,7 @@ function Expensify({ const {translate} = useLocalize(); const [account] = useOnyx(ONYXKEYS.ACCOUNT); const [session] = useOnyx(ONYXKEYS.SESSION); + const [lastRoute] = useOnyx(ONYXKEYS.LAST_ROUTE); const [shouldShowRequire2FAModal, setShouldShowRequire2FAModal] = useState(false); useEffect(() => { @@ -194,7 +191,6 @@ function Expensify({ focusModeNotification, isAuthenticated, lastVisitedPath, - lastRoute, }; Log.alert('[BootSplash] splash screen is still visible', {propsToLog}, false); } @@ -242,18 +238,12 @@ function Expensify({ Audio.setAudioModeAsync({playsInSilentModeIOS: true}); }, []); - /** - * Navigate to the last route after enabling camera permissions from the settings. - * This functionality is specific to the iOS application, as we are storing the last route only for iOS. - */ useEffect(() => { if (!isNavigationReady || !lastRoute) { return; } - updateLastRoute(''); - const route = lastRoute as Route; - Navigation.navigate(route); + Navigation.navigate(lastRoute as Route); // Disabling this rule because we only want it to run on the first render. // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps }, [isNavigationReady]); @@ -357,9 +347,6 @@ export default withOnyx({ lastVisitedPath: { key: ONYXKEYS.LAST_VISITED_PATH, }, - lastRoute: { - key: ONYXKEYS.LAST_ROUTE, - }, })(Expensify); export {SplashScreenHiddenContext}; From 9031f4431b9928ccbf668c40b3c3f927c28cc24d Mon Sep 17 00:00:00 2001 From: Ravindra Singh Date: Tue, 27 Aug 2024 09:38:30 +0530 Subject: [PATCH 13/13] replace useEffect with useLayoutEffect --- src/Expensify.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Expensify.tsx b/src/Expensify.tsx index 9d391eeb8b7f..11a638670a32 100644 --- a/src/Expensify.tsx +++ b/src/Expensify.tsx @@ -238,7 +238,7 @@ function Expensify({ Audio.setAudioModeAsync({playsInSilentModeIOS: true}); }, []); - useEffect(() => { + useLayoutEffect(() => { if (!isNavigationReady || !lastRoute) { return; }