diff --git a/src/libs/Navigation/helpers/getAdaptedStateFromPath.ts b/src/libs/Navigation/helpers/getAdaptedStateFromPath.ts index c36bb94fe072..11a0e3168d35 100644 --- a/src/libs/Navigation/helpers/getAdaptedStateFromPath.ts +++ b/src/libs/Navigation/helpers/getAdaptedStateFromPath.ts @@ -8,6 +8,7 @@ import {config} from '@libs/Navigation/linkingConfig/config'; import {RHP_TO_DOMAIN, RHP_TO_SEARCH, RHP_TO_SETTINGS, RHP_TO_SIDEBAR, RHP_TO_WORKSPACE, RHP_TO_WORKSPACES_LIST} from '@libs/Navigation/linkingConfig/RELATIONS'; import type {NavigationPartialRoute, RootNavigatorParamList} from '@libs/Navigation/types'; import CONST from '@src/CONST'; +import type {IOUAction, IOUType} from '@src/CONST'; import {getSearchParamFromPath} from '@src/libs/Url'; import NAVIGATORS from '@src/NAVIGATORS'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -45,6 +46,28 @@ function isRouteWithReportID(route: NavigationPartialRoute): route is Route { + return ( + route.params !== undefined && + 'action' in route.params && + typeof route.params.action === 'string' && + 'iouType' in route.params && + typeof route.params.iouType === 'string' && + 'transactionID' in route.params && + typeof route.params.transactionID === 'string' && + 'reportID' in route.params && + typeof route.params.reportID === 'string' && + 'imageType' in route.params && + typeof route.params.imageType === 'string' + ); +} + /** * Get the appropriate screen name for RHP_TO_SEARCH lookup. * Split tabs (amount, percentage, date) are nested routes within SPLIT_EXPENSE/SPLIT_EXPENSE_SEARCH. @@ -180,7 +203,7 @@ function getDefaultFullScreenRoute(route?: NavigationPartialRoute) { if (route && isRouteWithReportID(route)) { const reportID = route.params.reportID; - if (!allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`]?.reportID) { + if (!isValidReportID(reportID)) { return {name: NAVIGATORS.REPORTS_SPLIT_NAVIGATOR}; } @@ -214,7 +237,7 @@ function getOnboardingAdaptedState(state: PartialState): Partia return getRoutesWithIndex(routes); } -function getAdaptedState(state: PartialState>): GetAdaptedStateReturnType { +function getAdaptedState(state: PartialState>, defaultFullScreenRouteSource?: NavigationPartialRoute): GetAdaptedStateReturnType { const fullScreenRoute = state.routes.find((route) => isFullScreenName(route.name)); const onboardingNavigator = state.routes.find((route) => route.name === NAVIGATORS.ONBOARDING_MODAL_NAVIGATOR); const isWorkspaceSplitNavigator = fullScreenRoute?.name === NAVIGATORS.WORKSPACE_SPLIT_NAVIGATOR; @@ -252,7 +275,12 @@ function getAdaptedState(state: PartialState { let normalizedPath = !path.startsWith('/') ? `/${path}` : path; + let defaultFullScreenRouteSource: NavigationPartialRoute | undefined; normalizedPath = getRedirectedPath(normalizedPath); normalizedPath = getMatchingNewRoute(normalizedPath) ?? normalizedPath; @@ -283,6 +312,20 @@ const getAdaptedStateFromPath: GetAdaptedStateFromPath = (path, options, shouldR normalizedPath = '/'; } + if (normalizedPath.includes('/receipt/')) { + const stateWithOdometerReceiptPreview = getStateFromPath(normalizedPath, options) as PartialState>; + const focusedRoute = stateWithOdometerReceiptPreview ? findFocusedRoute(stateWithOdometerReceiptPreview) : undefined; + const focusedRouteAsPartial = focusedRoute as NavigationPartialRoute | undefined; + + if (focusedRouteAsPartial?.name === SCREENS.MONEY_REQUEST.RECEIPT_PREVIEW && isRouteWithOdometerReceiptPreviewParams(focusedRouteAsPartial)) { + const {action, iouType, transactionID, reportID} = focusedRouteAsPartial.params; + if (isValidReportID(reportID)) { + defaultFullScreenRouteSource = focusedRouteAsPartial; + } + normalizedPath = `/${ROUTES.DISTANCE_REQUEST_CREATE_TAB_ODOMETER.getRoute(action, iouType, transactionID, reportID)}`; + } + } + const state = getStateFromPath(normalizedPath, options) as PartialState>; if (shouldReplacePathInNestedState) { replacePathInNestedState(state, normalizedPath); @@ -292,7 +335,7 @@ const getAdaptedStateFromPath: GetAdaptedStateFromPath = (path, options, shouldR throw new Error(`[getAdaptedStateFromPath] Unable to get state from path: ${path}`); } - return getAdaptedState(state); + return getAdaptedState(state, defaultFullScreenRouteSource); }; export default getAdaptedStateFromPath; diff --git a/src/pages/iou/request/step/IOURequestStepDistanceOdometer.tsx b/src/pages/iou/request/step/IOURequestStepDistanceOdometer.tsx index ae8969c4fece..21bef9ef46eb 100644 --- a/src/pages/iou/request/step/IOURequestStepDistanceOdometer.tsx +++ b/src/pages/iou/request/step/IOURequestStepDistanceOdometer.tsx @@ -24,6 +24,7 @@ import useThemeStyles from '@hooks/useThemeStyles'; import { createDistanceRequest, getMoneyRequestParticipantsFromReport, + removeMoneyRequestOdometerImage, setCustomUnitRateID, setMoneyRequestDistance, setMoneyRequestMerchant, @@ -251,24 +252,60 @@ function IOURequestStepDistanceOdometer({ const startImageSource = useMemo(() => getImageSource(odometerStartImage), [getImageSource, odometerStartImage]); const endImageSource = useMemo(() => getImageSource(odometerEndImage), [getImageSource, odometerEndImage]); + const hasStartOdometerImage = !!startImageSource; + const hasEndOdometerImage = !!endImageSource; useEffect(() => { return () => { + if (isEditingConfirmation) { + return; + } if (!startImageSource?.startsWith('blob:')) { return; } URL.revokeObjectURL(startImageSource); }; - }, [startImageSource]); + }, [startImageSource, isEditingConfirmation]); useEffect(() => { return () => { + if (isEditingConfirmation) { + return; + } if (!endImageSource?.startsWith('blob:')) { return; } URL.revokeObjectURL(endImageSource); }; - }, [endImageSource]); + }, [endImageSource, isEditingConfirmation]); + + const validateOdometerBlobSource = useCallback( + (imageSource: string | undefined, imageType: OdometerImageType) => { + if (!imageSource?.startsWith('blob:')) { + return; + } + + // Blob URLs are process-local and can become stale after refresh. + // If the blob cannot be fetched anymore, clear it from transaction state. + fetch(imageSource).catch(() => { + if (imageType === CONST.IOU.ODOMETER_IMAGE_TYPE.START) { + initialStartImageRef.current = undefined; + } else { + initialEndImageRef.current = undefined; + } + removeMoneyRequestOdometerImage(transactionID, imageType, isTransactionDraft); + }); + }, + [isTransactionDraft, transactionID], + ); + + useEffect(() => { + validateOdometerBlobSource(startImageSource, CONST.IOU.ODOMETER_IMAGE_TYPE.START); + }, [startImageSource, validateOdometerBlobSource]); + + useEffect(() => { + validateOdometerBlobSource(endImageSource, CONST.IOU.ODOMETER_IMAGE_TYPE.END); + }, [endImageSource, validateOdometerBlobSource]); const buttonText = (() => { if (shouldSkipConfirmation) { @@ -331,6 +368,17 @@ function IOURequestStepDistanceOdometer({ [reportID, transactionID, action, iouType], ); + const handleOdometerImagePress = useCallback( + (imageType: OdometerImageType, hasImage: boolean) => { + if (!hasImage) { + handleCaptureImage(imageType); + return; + } + handleViewOdometerImage(imageType); + }, + [handleCaptureImage, handleViewOdometerImage], + ); + // Navigate to confirmation page helper - following Manual tab pattern const navigateToConfirmationPage = () => { if (!transactionID || !reportID) { @@ -593,11 +641,7 @@ function IOURequestStepDistanceOdometer({ accessibilityRole="button" sentryLabel={CONST.SENTRY_LABEL.ODOMETER_EXPENSE.CAPTURE_IMAGE_START} onPress={() => { - if (odometerStartImage) { - handleViewOdometerImage(CONST.IOU.ODOMETER_IMAGE_TYPE.START); - } else { - handleCaptureImage(CONST.IOU.ODOMETER_IMAGE_TYPE.START); - } + handleOdometerImagePress(CONST.IOU.ODOMETER_IMAGE_TYPE.START, hasStartOdometerImage); }} style={[ StyleUtils.getWidthAndHeightStyle(variables.inputHeight, variables.inputHeight), @@ -639,11 +683,7 @@ function IOURequestStepDistanceOdometer({ accessibilityRole="button" sentryLabel={CONST.SENTRY_LABEL.ODOMETER_EXPENSE.CAPTURE_IMAGE_END} onPress={() => { - if (odometerEndImage) { - handleViewOdometerImage(CONST.IOU.ODOMETER_IMAGE_TYPE.END); - } else { - handleCaptureImage(CONST.IOU.ODOMETER_IMAGE_TYPE.END); - } + handleOdometerImagePress(CONST.IOU.ODOMETER_IMAGE_TYPE.END, hasEndOdometerImage); }} style={[ StyleUtils.getWidthAndHeightStyle(variables.inputHeight, variables.inputHeight), diff --git a/src/pages/iou/request/step/IOURequestStepOdometerImage/index.tsx b/src/pages/iou/request/step/IOURequestStepOdometerImage/index.tsx index 070af2d31188..c66aae40506f 100644 --- a/src/pages/iou/request/step/IOURequestStepOdometerImage/index.tsx +++ b/src/pages/iou/request/step/IOURequestStepOdometerImage/index.tsx @@ -23,7 +23,6 @@ import variables from '@styles/variables'; import {setMoneyRequestOdometerImage} from '@userActions/IOU'; import CONST from '@src/CONST'; import type {IOUAction, IOUType} from '@src/CONST'; -import ROUTES from '@src/ROUTES'; import type SCREENS from '@src/SCREENS'; import type {FileObject} from '@src/types/utils/Attachment'; @@ -31,7 +30,7 @@ type IOURequestStepOdometerImageProps = WithFullTransactionOrNotFoundProps${message}`; - const odometerRoute = ROUTES.DISTANCE_REQUEST_CREATE_TAB_ODOMETER.getRoute(action, iouType, transactionID, reportID); - const navigateBack = useCallback(() => { - Navigation.goBack(odometerRoute); - }, [odometerRoute]); + Navigation.goBack(); + }, []); const revokeDropBlobUrls = useCallback(() => { for (const url of dropBlobUrlsRef.current) {