import { type ReactNode, useEffect, useRef } from 'react'; import { type NativeEventSubscription, AppState, type AppStateStatus } from 'react-native'; import dayJs from 'dayjs'; import RNFS from 'react-native-fs'; import fetchUpdatedRemoteConfig, { FIREBASE_FETCH_TIMESTAMP, } from '@services/firebaseFetchAndUpdate.service'; import { setItem, getItem } from '../components/utlis/storageHelper'; import CosmosForegroundService, { type IForegroundTask, } from '../services/foregroundServices/foreground.service'; import useIsOnline from '../hooks/useIsOnline'; import { getSyncTime, sendCurrentGeolocationAndBuffer } from '../hooks/capturingApi'; import { isTimeDifferenceWithinRange, setAsyncStorageItem } from '../components/utlis/commonFunctions'; import { setIsTimeSynced } from '../reducer/foregroundServiceSlice'; 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, noop } from '../../RN-UI-LIB/src/utlis/common'; import { setCaseSyncLock, 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, getEnableFirestoreResync, } from './AgentActivityConfigurableConstants'; import { GlobalImageMap } from './CachedImage'; import { addClickstreamEvent } from '../services/clickstreamEventService'; import { CLICKSTREAM_EVENT_NAMES, LocalStorageKeys } from './Constants'; import useResyncFirebase from '@hooks/useResyncFirebase'; import { imageSyncService, prepareImagesForUpload, sendImagesToServer } from '@services/imageSyncService'; import { getImages } from '@components/utlis/ImageUtlis'; import getLitmusExperimentResult, { LitmusExperimentName, LitmusExperimentNameMap } from '@services/litmusExperiments.service'; import { GLOBAL } from '@constants/Global'; import { sendAudiosToServer } from '@services/audioSyncService'; import { sendVideosToServer } from '@services/videoSyncService'; 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', FETCH_DATA_FROM_FIREBASE = 'FETCH_DATA_FROM_FIREBASE', FIREBASE_RESYNC = 'FIREBASE_RESYNC', IMAGE_SYNC_JOB = 'IMAGE_SYNC_JOB', IMAGE_UPLOAD_JOB = 'IMAGE_UPLOAD_JOB', VIDEO_UPLOAD_JOB = 'VIDEO_UPLOAD_JOB', AUDIO_UPLOAD_JOB = 'AUDIO_UPLOAD_JOB', } 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, caseSyncLock, referenceId, pendingList = [], pinnedList = [], } = useAppSelector((state) => ({ isTeamLead: state.user.isTeamLead, caseSyncLock: state?.user?.caseSyncLock, referenceId: state.user.user?.referenceId!, pendingList: state.allCases.pendingList, pinnedList: state.allCases.pinnedList, })); 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 resyncFirebase = useResyncFirebase(); const handleGetCaseSyncStatus = async () => { if (caseSyncLock || getEnableFirestoreResync()) { return; } dispatch(setCaseSyncLock(true)); 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, }) ); } dispatch(setCaseSyncLock(false)); } catch (e) { logError(e as Error, 'Error during fetching case sync status'); } }; 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) { 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); } } else if (isStateSetTimeWithinHighRange) { } else if (isStateSetTimeWithinMediumRange) { await setItem(StorageKeys.USER_ACTIVITY_ON_APP, AgentActivity.MEDIUM); } else { await setItem(StorageKeys.USER_ACTIVITY_ON_APP, AgentActivity.LOW); } }; 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(async (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]; await 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 handleFetchUpdatedDataFromFirebase = async () => { const currentTimestamp: number = Date.now(); if ( FIREBASE_FETCH_TIMESTAMP && currentTimestamp - FIREBASE_FETCH_TIMESTAMP > 15 * MILLISECONDS_IN_A_MINUTE ) { fetchUpdatedRemoteConfig(); } }; 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: () => dispatch(sendCurrentGeolocationAndBuffer(appState.current)), delay: 3 * MILLISECONDS_IN_A_MINUTE, // 3 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, }, { taskId: FOREGROUND_TASKS.FETCH_DATA_FROM_FIREBASE, task: handleFetchUpdatedDataFromFirebase, delay: 60 * MILLISECONDS_IN_A_MINUTE, // 60 minutes onLoop: true, }, { taskId: FOREGROUND_TASKS.IMAGE_SYNC_JOB, task: imageSyncService, delay: 0.5 * MILLISECONDS_IN_A_MINUTE, // 30 minutes onLoop: true, }, { taskId: FOREGROUND_TASKS.IMAGE_UPLOAD_JOB, task: sendImagesToServer, delay: 0.5 * MILLISECONDS_IN_A_MINUTE, // 30 minutes onLoop: true, }, { taskId: FOREGROUND_TASKS.VIDEO_UPLOAD_JOB, task: sendVideosToServer, delay: 0.5 * MILLISECONDS_IN_A_MINUTE, // 30 minutes onLoop: true, }, { taskId: FOREGROUND_TASKS.AUDIO_UPLOAD_JOB, task: sendAudiosToServer, delay: 0.5 * MILLISECONDS_IN_A_MINUTE, // 30 minutes 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()); } }; 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(); handleTimeSync(); dispatch(getConfigData()); CosmosForegroundService.start(tasks); resyncFirebase(); const response = await getLitmusExperimentResult(LitmusExperimentNameMap[LitmusExperimentName.COSMOS_IMAGE_SYNC], { 'x-customer-id': GLOBAL.AGENT_ID }); setAsyncStorageItem(LocalStorageKeys.IS_IMAGE_SYNC_ALLOWED, response); } 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()); const isFirestoreResyncEnabled = getEnableFirestoreResync(); if (!isTeamLead && LAST_SYNC_STATUS !== SyncStatus.FETCH_CASES && !isFirestoreResyncEnabled) { 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;