diff --git a/package.json b/package.json index 4a87b83591e5a..4be5ad715abd1 100644 --- a/package.json +++ b/package.json @@ -46,7 +46,7 @@ "test:debug": "TZ=utc NODE_OPTIONS='--inspect-brk --experimental-vm-modules' jest --runInBand", "perf-test": "NODE_OPTIONS=--experimental-vm-modules npx reassure", "typecheck": "NODE_OPTIONS=--max_old_space_size=8192 tsc", - "lint": "NODE_OPTIONS=--max_old_space_size=8192 eslint . --max-warnings=325 --cache --cache-location=node_modules/.cache/eslint", + "lint": "NODE_OPTIONS=--max_old_space_size=8192 eslint . --max-warnings=322 --cache --cache-location=node_modules/.cache/eslint", "lint-changed": "NODE_OPTIONS=--max_old_space_size=8192 ./scripts/lintChanged.sh", "lint-watch": "npx eslint-watch --watch --changed", "shellcheck": "./scripts/shellCheck.sh", diff --git a/src/hooks/useIOUUtils.ts b/src/hooks/useIOUUtils.ts new file mode 100644 index 0000000000000..12e7481cba2ff --- /dev/null +++ b/src/hooks/useIOUUtils.ts @@ -0,0 +1,19 @@ +import DateUtils from '@libs/DateUtils'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import useOnyx from './useOnyx'; + +function useIOUUtils() { + const [lastLocationPermissionPrompt] = useOnyx(ONYXKEYS.NVP_LAST_LOCATION_PERMISSION_PROMPT, {canBeMissing: true}); + function shouldStartLocationPermissionFlow() { + return ( + !lastLocationPermissionPrompt || + (DateUtils.isValidDateString(lastLocationPermissionPrompt ?? '') && + DateUtils.getDifferenceInDaysFromNow(new Date(lastLocationPermissionPrompt ?? '')) > CONST.IOU.LOCATION_PERMISSION_PROMPT_THRESHOLD_DAYS) + ); + } + + return {shouldStartLocationPermissionFlow}; +} + +export default useIOUUtils; diff --git a/src/libs/IOUUtils.ts b/src/libs/IOUUtils.ts index 0572cc3ed0bdc..3fdffbe33c24a 100644 --- a/src/libs/IOUUtils.ts +++ b/src/libs/IOUUtils.ts @@ -1,25 +1,16 @@ -import Onyx from 'react-native-onyx'; import type {ValueOf} from 'type-fest'; import type {IOUAction, IOUType} from '@src/CONST'; import CONST from '@src/CONST'; -import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import type {OnyxInputOrEntry, PersonalDetails, Report} from '@src/types/onyx'; import type {Attendee} from '@src/types/onyx/IOU'; import type {IOURequestType} from './actions/IOU'; import {getCurrencyUnit} from './CurrencyUtils'; -import DateUtils from './DateUtils'; import Navigation from './Navigation/Navigation'; import Performance from './Performance'; import {getReportTransactions} from './ReportUtils'; import {getCurrency, getTagArrayFromName} from './TransactionUtils'; -let lastLocationPermissionPrompt: string; -Onyx.connect({ - key: ONYXKEYS.NVP_LAST_LOCATION_PERMISSION_PROMPT, - callback: (val) => (lastLocationPermissionPrompt = val ?? ''), -}); - function navigateToStartMoneyRequestStep(requestType: IOURequestType, iouType: IOUType, transactionID: string, reportID: string, iouAction?: IOUAction): void { if (iouAction === CONST.IOU.ACTION.CATEGORIZE || iouAction === CONST.IOU.ACTION.SUBMIT || iouAction === CONST.IOU.ACTION.SHARE) { Navigation.goBack(); @@ -214,14 +205,6 @@ function formatCurrentUserToAttendee(currentUser?: PersonalDetails, reportID?: s return [initialAttendee]; } -function shouldStartLocationPermissionFlow() { - return ( - !lastLocationPermissionPrompt || - (DateUtils.isValidDateString(lastLocationPermissionPrompt ?? '') && - DateUtils.getDifferenceInDaysFromNow(new Date(lastLocationPermissionPrompt ?? '')) > CONST.IOU.LOCATION_PERMISSION_PROMPT_THRESHOLD_DAYS) - ); -} - export { calculateAmount, insertTagIntoTransactionTagsString, @@ -232,6 +215,5 @@ export { navigateToStartMoneyRequestStep, updateIOUOwnerAndTotal, formatCurrentUserToAttendee, - shouldStartLocationPermissionFlow, navigateToParticipantPage, }; diff --git a/src/pages/iou/request/step/IOURequestStepScan/index.native.tsx b/src/pages/iou/request/step/IOURequestStepScan/index.native.tsx index 30d77306866bd..3d4c9c7b1ff9b 100644 --- a/src/pages/iou/request/step/IOURequestStepScan/index.native.tsx +++ b/src/pages/iou/request/step/IOURequestStepScan/index.native.tsx @@ -24,6 +24,7 @@ import PressableWithFeedback from '@components/Pressable/PressableWithFeedback'; import Text from '@components/Text'; import withCurrentUserPersonalDetails from '@components/withCurrentUserPersonalDetails'; import useFilesValidation from '@hooks/useFilesValidation'; +import useIOUUtils from '@hooks/useIOUUtils'; import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; import usePolicy from '@hooks/usePolicy'; @@ -38,7 +39,7 @@ import getPlatform from '@libs/getPlatform'; import type Platform from '@libs/getPlatform/types'; import getReceiptsUploadFolderPath from '@libs/getReceiptsUploadFolderPath'; import HapticFeedback from '@libs/HapticFeedback'; -import {navigateToParticipantPage, shouldStartLocationPermissionFlow} from '@libs/IOUUtils'; +import {navigateToParticipantPage} from '@libs/IOUUtils'; import Log from '@libs/Log'; import Navigation from '@libs/Navigation/Navigation'; import {getManagerMcTestParticipant, getParticipantsOption, getReportOption} from '@libs/OptionsListUtils'; @@ -117,6 +118,7 @@ function IOURequestStepScan({ const [didCapturePhoto, setDidCapturePhoto] = useState(false); const [shouldShowMultiScanEducationalPopup, setShouldShowMultiScanEducationalPopup] = useState(false); const [cameraKey, setCameraKey] = useState(0); + const {shouldStartLocationPermissionFlow} = useIOUUtils(); const defaultTaxCode = getDefaultTaxCode(policy, initialTransaction); const transactionTaxCode = (initialTransaction?.taxCode ? initialTransaction?.taxCode : defaultTaxCode) ?? ''; @@ -602,7 +604,7 @@ function IOURequestStepScan({ } navigateToConfirmationStep(files, false); }, - [initialTransaction, iouType, navigateToConfirmationStep, shouldSkipConfirmation], + [shouldSkipConfirmation, navigateToConfirmationStep, initialTransaction, iouType, shouldStartLocationPermissionFlow], ); const capturePhoto = useCallback(() => { diff --git a/src/pages/iou/request/step/IOURequestStepScan/index.tsx b/src/pages/iou/request/step/IOURequestStepScan/index.tsx index e2768ad9d846a..578d3b7bf5d7d 100644 --- a/src/pages/iou/request/step/IOURequestStepScan/index.tsx +++ b/src/pages/iou/request/step/IOURequestStepScan/index.tsx @@ -26,6 +26,7 @@ import Text from '@components/Text'; import TextLink from '@components/TextLink'; import withCurrentUserPersonalDetails from '@components/withCurrentUserPersonalDetails'; import useFilesValidation from '@hooks/useFilesValidation'; +import useIOUUtils from '@hooks/useIOUUtils'; import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; import usePolicy from '@hooks/usePolicy'; @@ -38,7 +39,7 @@ import {dismissProductTraining} from '@libs/actions/Welcome'; import {isMobile, isMobileWebKit} from '@libs/Browser'; import {base64ToFile, isLocalFile as isLocalFileFileUtils} from '@libs/fileDownload/FileUtils'; import getCurrentPosition from '@libs/getCurrentPosition'; -import {navigateToParticipantPage, shouldStartLocationPermissionFlow} from '@libs/IOUUtils'; +import {navigateToParticipantPage} from '@libs/IOUUtils'; import Log from '@libs/Log'; import Navigation from '@libs/Navigation/Navigation'; import {getManagerMcTestParticipant, getParticipantsOption, getReportOption} from '@libs/OptionsListUtils'; @@ -119,6 +120,7 @@ function IOURequestStepScan({ const isEditing = action === CONST.IOU.ACTION.EDIT; const canUseMultiScan = !isEditing && iouType !== CONST.IOU.TYPE.SPLIT && !backTo && !backToReport; const isReplacingReceipt = (isEditing && hasReceipt(initialTransaction)) || (!!initialTransaction?.receipt && !!backTo); + const {shouldStartLocationPermissionFlow} = useIOUUtils(); const [optimisticTransactions] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_DRAFT, { selector: (items) => Object.values(items ?? {}), @@ -670,7 +672,7 @@ function IOURequestStepScan({ } navigateToConfirmationStep(files, false); }, - [initialTransaction, iouType, navigateToConfirmationStep, shouldSkipConfirmation], + [initialTransaction, iouType, shouldStartLocationPermissionFlow, navigateToConfirmationStep, shouldSkipConfirmation], ); const getScreenshot = useCallback(() => { diff --git a/tests/unit/useIOUUtilsTest.ts b/tests/unit/useIOUUtilsTest.ts new file mode 100644 index 0000000000000..fd02258e02dc3 --- /dev/null +++ b/tests/unit/useIOUUtilsTest.ts @@ -0,0 +1,116 @@ +import {renderHook} from '@testing-library/react-native'; +import Onyx from 'react-native-onyx'; +import CONST from '@src/CONST'; +import useIOUUtils from '@src/hooks/useIOUUtils'; +import ONYXKEYS from '@src/ONYXKEYS'; +import waitForBatchedUpdatesWithAct from '../utils/waitForBatchedUpdatesWithAct'; + +describe('useIOUUtils', () => { + beforeAll(() => { + Onyx.init({ + keys: ONYXKEYS, + }); + }); + + beforeEach(async () => { + await Onyx.clear(); + }); + + describe('shouldStartLocationPermissionFlow', () => { + const now = new Date(); + const daysAgo = (days: number) => { + const d = new Date(now); + d.setDate(d.getDate() - days); + return d.toISOString(); + }; + + it('returns true when lastLocationPermissionPrompt is undefined', async () => { + const {result} = renderHook(() => useIOUUtils()); + await waitForBatchedUpdatesWithAct(); + + expect(result.current.shouldStartLocationPermissionFlow()).toBe(true); + }); + + it('returns true when lastLocationPermissionPrompt is null', async () => { + Onyx.set(ONYXKEYS.NVP_LAST_LOCATION_PERMISSION_PROMPT, null); + await waitForBatchedUpdatesWithAct(); + + const {result} = renderHook(() => useIOUUtils()); + await waitForBatchedUpdatesWithAct(); + + expect(result.current.shouldStartLocationPermissionFlow()).toBe(true); + }); + + it('returns true when lastLocationPermissionPrompt is empty string', async () => { + Onyx.set(ONYXKEYS.NVP_LAST_LOCATION_PERMISSION_PROMPT, ''); + await waitForBatchedUpdatesWithAct(); + + const {result} = renderHook(() => useIOUUtils()); + await waitForBatchedUpdatesWithAct(); + + expect(result.current.shouldStartLocationPermissionFlow()).toBe(true); + }); + + it('returns false when lastLocationPermissionPrompt is a valid date string within threshold', async () => { + const recentDate = daysAgo(CONST.IOU.LOCATION_PERMISSION_PROMPT_THRESHOLD_DAYS - 1); + Onyx.set(ONYXKEYS.NVP_LAST_LOCATION_PERMISSION_PROMPT, recentDate); + await waitForBatchedUpdatesWithAct(); + + const {result} = renderHook(() => useIOUUtils()); + await waitForBatchedUpdatesWithAct(); + + expect(result.current.shouldStartLocationPermissionFlow()).toBe(false); + }); + + it('returns true when lastLocationPermissionPrompt is a valid date string outside threshold', async () => { + const oldDate = daysAgo(CONST.IOU.LOCATION_PERMISSION_PROMPT_THRESHOLD_DAYS + 1); + Onyx.set(ONYXKEYS.NVP_LAST_LOCATION_PERMISSION_PROMPT, oldDate); + await waitForBatchedUpdatesWithAct(); + + const {result} = renderHook(() => useIOUUtils()); + await waitForBatchedUpdatesWithAct(); + + expect(result.current.shouldStartLocationPermissionFlow()).toBe(true); + }); + + it('returns false when lastLocationPermissionPrompt is an invalid date string', async () => { + Onyx.set(ONYXKEYS.NVP_LAST_LOCATION_PERMISSION_PROMPT, 'not-a-date'); + await waitForBatchedUpdatesWithAct(); + + const {result} = renderHook(() => useIOUUtils()); + await waitForBatchedUpdatesWithAct(); + + expect(result.current.shouldStartLocationPermissionFlow()).toBe(false); + }); + + it('returns false when lastLocationPermissionPrompt is exactly at threshold', async () => { + const thresholdDate = daysAgo(CONST.IOU.LOCATION_PERMISSION_PROMPT_THRESHOLD_DAYS); + Onyx.set(ONYXKEYS.NVP_LAST_LOCATION_PERMISSION_PROMPT, thresholdDate); + await waitForBatchedUpdatesWithAct(); + + const {result} = renderHook(() => useIOUUtils()); + await waitForBatchedUpdatesWithAct(); + + expect(result.current.shouldStartLocationPermissionFlow()).toBe(false); + }); + + it('reacts to changes in lastLocationPermissionPrompt', async () => { + const {result} = renderHook(() => useIOUUtils()); + await waitForBatchedUpdatesWithAct(); + + expect(result.current.shouldStartLocationPermissionFlow()).toBe(true); + + const recentDate = daysAgo(CONST.IOU.LOCATION_PERMISSION_PROMPT_THRESHOLD_DAYS - 1); + Onyx.set(ONYXKEYS.NVP_LAST_LOCATION_PERMISSION_PROMPT, recentDate); + await waitForBatchedUpdatesWithAct(); + + expect(result.current.shouldStartLocationPermissionFlow()).toBe(false); + + const oldDate = daysAgo(CONST.IOU.LOCATION_PERMISSION_PROMPT_THRESHOLD_DAYS + 1); + Onyx.set(ONYXKEYS.NVP_LAST_LOCATION_PERMISSION_PROMPT, oldDate); + await waitForBatchedUpdatesWithAct(); + + expect(result.current.shouldStartLocationPermissionFlow()).toBe(true); + }); + }); +});