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'; export enum FOREGROUND_TASKS { GEOLOCATION = 'GEOLOCATION', TIME_SYNC = 'TIME_SYNC', DATA_SYNC = 'DATA_SYNC', FIRESTORE_FALLBACK = 'FIRESTORE_FALLBACK', UPDATE_AGENT_ACTIVENESS = 'UPDATE_AGENT_ACTIVENESS', } interface ITrackingComponent { children?: ReactNode; } let LAST_SYNC_STATUS = 'SKIP'; const ACTIVITY_TIME_ON_APP = 5; // 5 seconds const ACTIVITY_TIME_WINDOW = 10; // 10 minutes const TrackingComponent: React.FC = ({ children }) => { const isOnline = useIsOnline(); const dispatch = useAppDispatch(); const appState = useRef(AppState.currentState); 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 geolocation: IGeolocationPayload = { latitude: location.latitude, longitude: location.longitude, accuracy: location.accuracy, timestamp: Date.now(), isActiveOnApp: Boolean(isActiveOnApp), }; 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); 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 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.FIRESTORE_FALLBACK, task: handleGetCaseSyncStatus, delay: 5 * MILLISECONDS_IN_A_MINUTE, // 5 minutes onLoop: true, }, { taskId: FOREGROUND_TASKS.UPDATE_AGENT_ACTIVENESS, task: handleUpdateActiveness, delay: ACTIVITY_TIME_WINDOW * MILLISECONDS_IN_A_MINUTE, // 10 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 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); handleGetCaseSyncStatus(); dispatch(getConfigData()); CosmosForegroundService.start(tasks); } if (nextAppState === AppStates.BACKGROUND) { await setItem(StorageKeys.APP_BACKGROUND_TIMESTAMP, now); } appState.current = nextAppState; }; // Fetch cases on login initially and set data useEffect(() => { (async () => { if (!referenceId) { return; } await handleGetCaseSyncStatus(); dispatch(getConfigData()); if (LAST_SYNC_STATUS !== SyncStatus.FETCH_CASES) { const updatedDetails: ISyncedCases = await fetchCasesToSync(referenceId); if (updatedDetails?.cases?.length) { dispatch(syncCasesByFallback(updatedDetails)); } } })(); }, []); useEffect(() => { let appStateSubscription: NativeEventSubscription; CosmosForegroundService.isRunning().then((isFGSRunning) => { if (!isFGSRunning) { appStateSubscription = AppState.addEventListener('change', handleAppStateChange); CosmosForegroundService.start(tasks); } }); return () => { appStateSubscription?.remove(); }; }, []); useIsLocationEnabled(); return <>{children}; }; export default TrackingComponent;