import { type ReactNode, useEffect, useRef, useCallback } from 'react'; import { type NativeEventSubscription, AppState, type AppStateStatus } from 'react-native'; import dayJs from 'dayjs'; import { setItem, getItem } from '../components/utlis/storageHelper'; import CosmosForegroundService, { type IForegroundTask, } from '../services/foregroundServices/foreground.service'; import useIsOnline from '../hooks/useIsOnline'; import { IGeolocationPayload, getSyncTime, sendLocationAndActivenessToServer, } from '../hooks/capturingApi'; import { isTimeDifferenceWithinRange } from '../components/utlis/commonFunctions'; import { setDeviceGeolocationsBuffer, setIsTimeSynced } from '../reducer/foregroundServiceSlice'; import { CaptureGeolocation } from '../components/form/services/geoLocation.service'; import { logError } from '../components/utlis/errorUtils'; import { useAppDispatch, useAppSelector } from '../hooks'; import { dataSyncService } from '../services/dataSync.service'; import { DATA_SYNC_TIME_INTERVAL, IS_DATA_SYNC_REQUIRED } from '../constants/config'; import useIsLocationEnabled from '../hooks/useIsLocationEnabled'; import { type ISyncCaseIdPayload, type ISyncedCases, SyncStatus, fetchCasesToSync, getCasesSyncStatus, sendSyncCaseIds, } from '../action/firebaseFallbackActions'; import { getSyncCaseIds } from '../components/utlis/firebaseFallbackUtils'; import { syncCasesByFallback } from '../reducer/allCasesSlice'; import { MILLISECONDS_IN_A_MINUTE } from '../../RN-UI-LIB/src/utlis/common'; import { setLockData } from '../reducer/userSlice'; import { getConfigData } from '../action/configActions'; import { AppStates } from '../types/appStates'; import { StorageKeys } from '../types/storageKeys'; import { AgentActivity } from '../types/agentActivity'; import { getActivityTimeOnApp, getActivityTimeWindowMedium, getActivityTimeWindowHigh, } from './AgentActivityConfigurableConstants'; import RNFS from 'react-native-fs'; import { GlobalImageMap } from './CachedImage'; import { get } from 'react-hook-form'; import { addClickstreamEvent } from '../services/clickstreamEventService'; import { CLICKSTREAM_EVENT_NAMES } from './Constants'; export enum FOREGROUND_TASKS { GEOLOCATION = 'GEOLOCATION', TIME_SYNC = 'TIME_SYNC', DATA_SYNC = 'DATA_SYNC', FIRESTORE_FALLBACK = 'FIRESTORE_FALLBACK', UPDATE_AGENT_ACTIVENESS = 'UPDATE_AGENT_ACTIVENESS', UPDATE_AGENT_ACTIVITY = 'UPDATE_AGENT_ACTIVITY', DELETE_CACHE = 'DELETE_CACHE', } interface ITrackingComponent { children?: ReactNode; } let LAST_SYNC_STATUS = 'SKIP'; const ACTIVITY_TIME_WINDOW = 10; // 10 minutes const TrackingComponent: React.FC = ({ children }) => { const isOnline = useIsOnline(); const dispatch = useAppDispatch(); const appState = useRef(AppState.currentState); const isTeamLead = useAppSelector((state) => state.user.isTeamLead); const { referenceId, pendingList = [], pinnedList = [], geolocations = [], } = useAppSelector((state) => ({ referenceId: state.user.user?.referenceId!, pendingList: state.allCases.pendingList, pinnedList: state.allCases.pinnedList, geolocations: state.foregroundService.deviceGeolocationsBuffer, })); const handleTimeSync = async () => { try { if (!isOnline) { return; } const timestamp = await getSyncTime(); if (timestamp) { const isTimeDifferenceLess = isTimeDifferenceWithinRange(timestamp, 5); dispatch(setIsTimeSynced(isTimeDifferenceLess)); } } catch (e: any) { logError(e, 'Error during fetching timestamp from server.'); } }; const handleSendGeolocation = async () => { try { const location = await CaptureGeolocation.fetchLocation(Date.now() + '', 0, appState.current); if (!location) { return; } const isActiveOnApp: string | boolean = (await getItem(StorageKeys.IS_USER_ACTIVE)) || false; const userActivityonApp: string = (await getItem(StorageKeys.USER_ACTIVITY_ON_APP)) || AgentActivity.LOW; const geolocation: IGeolocationPayload = { latitude: location.latitude, longitude: location.longitude, accuracy: location.accuracy, timestamp: Date.now(), isActiveOnApp: Boolean(isActiveOnApp), userActivityOnApp: String(userActivityonApp), }; dispatch(setDeviceGeolocationsBuffer(geolocation)); dispatch(sendLocationAndActivenessToServer([geolocation])); } catch (e: any) { logError(e, 'Error during background location sending.'); } }; useEffect(() => { if (!isOnline) { return; } if (geolocations.length) { dispatch(sendLocationAndActivenessToServer(geolocations, true)); } }, [geolocations, isOnline]); const handleGetCaseSyncStatus = async () => { try { if (!isOnline) { return; } const { syncStatus, visitPlanStatus } = (await getCasesSyncStatus(referenceId)) ?? {}; if (syncStatus) { // Keep track of the last status received LAST_SYNC_STATUS = syncStatus; } if (syncStatus === SyncStatus.SEND_CASES) { const cases = getSyncCaseIds([...pendingList, ...pinnedList]); const payload: ISyncCaseIdPayload = { agentId: referenceId, cases, }; sendSyncCaseIds(payload); } else if (syncStatus === SyncStatus.FETCH_CASES) { const updatedDetails: ISyncedCases = await fetchCasesToSync(referenceId); if (updatedDetails?.cases?.length) { dispatch(syncCasesByFallback(updatedDetails)); } } if (visitPlanStatus) { dispatch( setLockData({ visitPlanStatus, }) ); } } catch (e) { logError(e as Error, 'Error during fetching case sync status'); } }; const handleUpdateActiveness = async () => { if (AppState.currentState === AppStates.ACTIVE) { await setItem(StorageKeys.IS_USER_ACTIVE, 'true'); return; } const foregroundTimestamp = await getItem(StorageKeys.APP_FOREGROUND_TIMESTAMP); const backgroundTimestamp = await getItem(StorageKeys.APP_BACKGROUND_TIMESTAMP); const foregroundTime = dayJs(foregroundTimestamp); const backgroundTime = dayJs(backgroundTimestamp); const diffBetweenBackgroundAndForegroundTime = dayJs(backgroundTime).diff( foregroundTime, 'seconds' ); const diffBetweenCurrentTimeAndForegroundTime = dayJs().diff(foregroundTime, 'minutes') < 0 ? 0 : dayJs().diff(foregroundTime, 'minutes'); const isForegroundTimeWithInRange = diffBetweenCurrentTimeAndForegroundTime <= ACTIVITY_TIME_WINDOW; const isForegroundTimeAfterBackground = dayJs(foregroundTimestamp).isAfter(backgroundTimestamp); const ACTIVITY_TIME_ON_APP = getActivityTimeOnApp(); if (isForegroundTimeWithInRange) { if ( isForegroundTimeAfterBackground || diffBetweenBackgroundAndForegroundTime >= ACTIVITY_TIME_ON_APP ) { await setItem(StorageKeys.IS_USER_ACTIVE, 'true'); return; } await setItem(StorageKeys.IS_USER_ACTIVE, 'false'); } await setItem(StorageKeys.IS_USER_ACTIVE, 'false'); return; }; const handleUpdateActivity = async () => { const foregroundTimestamp = await getItem(StorageKeys.APP_FOREGROUND_TIMESTAMP); const backgroundTimestamp = await getItem(StorageKeys.APP_BACKGROUND_TIMESTAMP); const stateSetTimestamp = await getItem(StorageKeys.STATE_SET_TIMESTAMP); if (foregroundTimestamp == null) { console.log('fts set after installation'); await setItem(StorageKeys.APP_FOREGROUND_TIMESTAMP, dayJs().toString()); } const foregroundTime = dayJs(foregroundTimestamp); const backgroundTime = dayJs(backgroundTimestamp); const stateSetTime = dayJs(stateSetTimestamp); const diffBetweenCurrentTimeAndForegroundTime = dayJs().diff(foregroundTime, 'seconds') < 0 ? 0 : dayJs().diff(foregroundTime, 'seconds'); const diffBetweenCurrentTimeAndSetStateTime = dayJs().diff(stateSetTime, 'minutes') < 0 ? 0 : dayJs().diff(stateSetTime, 'minutes'); const ACTIVITY_TIME_ON_APP = getActivityTimeOnApp(); const ACTIVITY_TIME_WINDOW_HIGH = getActivityTimeWindowHigh(); const ACTIVITY_TIME_WINDOW_MEDIUM = getActivityTimeWindowMedium(); const isStateSetTimeWithinHighRange = diffBetweenCurrentTimeAndSetStateTime < ACTIVITY_TIME_WINDOW_HIGH; const isStateSetTimeWithinMediumRange = diffBetweenCurrentTimeAndSetStateTime < ACTIVITY_TIME_WINDOW_MEDIUM; const isForegroundTimeAfterBackground = dayJs(foregroundTimestamp).isAfter(backgroundTimestamp); if (AppState.currentState === AppStates.ACTIVE) { if (diffBetweenCurrentTimeAndForegroundTime >= ACTIVITY_TIME_ON_APP) { await setItem(StorageKeys.USER_ACTIVITY_ON_APP, AgentActivity.HIGH); return; } return; } if (isForegroundTimeAfterBackground) { if (diffBetweenCurrentTimeAndForegroundTime >= ACTIVITY_TIME_ON_APP) { await setItem(StorageKeys.USER_ACTIVITY_ON_APP, AgentActivity.HIGH); return; } return; } else if (isStateSetTimeWithinHighRange) { return; } else if (isStateSetTimeWithinMediumRange) { await setItem(StorageKeys.USER_ACTIVITY_ON_APP, AgentActivity.MEDIUM); return; } else { await setItem(StorageKeys.USER_ACTIVITY_ON_APP, AgentActivity.LOW); return; } }; const deleteCache = () => { const directoryPath = RNFS.CachesDirectoryPath; const currentDate = new Date().getTime(); RNFS.readdir(directoryPath) .then((files) => { for (const file of files) { const filePath = `${directoryPath}/${file}`; if (!file.endsWith('jpg') || !file.endsWith('pdf')) { continue; } RNFS.stat(filePath) .then((fileStat) => { // Calculate the age of the file in milliseconds const fileAgeMs = currentDate - new Date(fileStat.mtime).getTime(); // Check if the file is older than 30 days (30 days = 30 * 24 * 60 * 60 * 1000 milliseconds) if (fileAgeMs > 30 * 24 * 60 * 60 * 1000) { delete GlobalImageMap[filePath]; return RNFS.unlink(filePath); // Delete the file } }) .then(() => { console.log(`Deleted old file: ${file}`); }) .catch((error) => { console.error(`Error deleting file: ${file}`, error); }); } }) .catch((error) => { console.error('Error reading directory:', error); }); }; const tasks: IForegroundTask[] = [ { taskId: FOREGROUND_TASKS.TIME_SYNC, task: handleTimeSync, delay: 5 * MILLISECONDS_IN_A_MINUTE, // 5 minutes, onLoop: true, }, { taskId: FOREGROUND_TASKS.GEOLOCATION, task: handleSendGeolocation, delay: 3 * MILLISECONDS_IN_A_MINUTE, // 3 minutes onLoop: true, }, { taskId: FOREGROUND_TASKS.UPDATE_AGENT_ACTIVENESS, task: handleUpdateActiveness, delay: ACTIVITY_TIME_WINDOW * MILLISECONDS_IN_A_MINUTE, // 10 minutes onLoop: true, }, { taskId: FOREGROUND_TASKS.UPDATE_AGENT_ACTIVITY, task: handleUpdateActivity, delay: ACTIVITY_TIME_WINDOW * MILLISECONDS_IN_A_MINUTE, // 10 minutes onLoop: true, }, { taskId: FOREGROUND_TASKS.DELETE_CACHE, task: deleteCache, delay: DATA_SYNC_TIME_INTERVAL, onLoop: true, }, ]; if (!isTeamLead) { tasks.push({ taskId: FOREGROUND_TASKS.FIRESTORE_FALLBACK, task: handleGetCaseSyncStatus, delay: 5 * MILLISECONDS_IN_A_MINUTE, // 5 minutes onLoop: true, }); } const handleDataSync = () => { if (!isOnline) { return; } dataSyncService(); }; if (IS_DATA_SYNC_REQUIRED) { tasks.push({ taskId: FOREGROUND_TASKS.DATA_SYNC, task: handleDataSync, delay: DATA_SYNC_TIME_INTERVAL, onLoop: true, }); } const userActivityUpdateOnBackground = async () => { const foregroundTimestamp = await getItem(StorageKeys.APP_FOREGROUND_TIMESTAMP); const backgroundTimestamp = await getItem(StorageKeys.APP_BACKGROUND_TIMESTAMP); const foregroundTime = dayJs(foregroundTimestamp); const backgroundTime = dayJs(backgroundTimestamp); const diffBetweenBackgroundAndForegroundTime = dayJs(backgroundTime).diff( foregroundTime, 'seconds' ); const ACTIVITY_TIME_ON_APP = getActivityTimeOnApp(); if (diffBetweenBackgroundAndForegroundTime >= ACTIVITY_TIME_ON_APP) { await setItem(StorageKeys.USER_ACTIVITY_ON_APP, AgentActivity.HIGH); await setItem(StorageKeys.STATE_SET_TIMESTAMP, dayJs().toString()); return; } return; }; const handleAppStateChange = async (nextAppState: AppStateStatus) => { // App comes to foreground from background const now = dayJs().toString(); if (nextAppState === AppStates.ACTIVE) { await setItem(StorageKeys.APP_FOREGROUND_TIMESTAMP, now); addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.AV_APP_FOREGROUND, { now }); handleGetCaseSyncStatus(); dispatch(getConfigData()); CosmosForegroundService.start(tasks); } if (nextAppState === AppStates.BACKGROUND) { await setItem(StorageKeys.APP_BACKGROUND_TIMESTAMP, now); userActivityUpdateOnBackground(); addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.AV_APP_BACKGROUND, { now }); } appState.current = nextAppState; }; // Fetch cases on login initially and set data useEffect(() => { (async () => { if (!referenceId) { return; } await handleGetCaseSyncStatus(); dispatch(getConfigData()); if (!isTeamLead && LAST_SYNC_STATUS !== SyncStatus.FETCH_CASES) { const updatedDetails: ISyncedCases = await fetchCasesToSync(referenceId); if (updatedDetails?.cases?.length) { dispatch(syncCasesByFallback(updatedDetails)); } } })(); }, []); useEffect(() => { let appStateSubscription: NativeEventSubscription; appStateSubscription = AppState.addEventListener('change', handleAppStateChange); CosmosForegroundService.start(tasks); return () => { appStateSubscription?.remove(); }; }, []); useIsLocationEnabled(); return <>{children}; }; export default TrackingComponent;