diff --git a/src/action/authActions.ts b/src/action/authActions.ts index 1fc63425..932c0c29 100644 --- a/src/action/authActions.ts +++ b/src/action/authActions.ts @@ -27,7 +27,7 @@ import { clearAllAsyncStorage } from '../components/utlis/commonFunctions'; import { logError } from '../components/utlis/errorUtils'; import auth from '@react-native-firebase/auth'; import foregroundService from '../services/foregroundServices/foreground.service'; -import { loggedOutCurrentUser } from '../hooks/useFirestoreUpdates'; +import { loggedOutCurrentUser } from '@hooks/useFirestoreUpdates'; import { GenericFunctionArgs, GenericType } from '../common/GenericTypes'; import { GoogleSignin } from '@react-native-google-signin/google-signin'; import { resetConfig } from '../reducer/configSlice'; @@ -38,6 +38,7 @@ import { resetPerformanceData } from '@reducers/agentPerformanceSlice'; import { clearStorageEngine } from '../PersistStorageEngine'; import { resetNearbyCasesData } from '@reducers/nearbyCasesSlice'; import { resetActiveCallData } from '@reducers/activeCallSlice'; +import { firestoreService } from '@services/firestoreService'; export interface GenerateOTPPayload { phoneNumber: string; @@ -198,6 +199,7 @@ export const handleGoogleLogout = async () => { const firebaseSignout = async () => { try { + await firestoreService.unsubscribeAll(); await auth().signOut(); } catch (error) { logError(error as Error, 'Firebase signout error'); diff --git a/src/action/firebaseFallbackActions.ts b/src/action/firebaseFallbackActions.ts deleted file mode 100644 index 49d44747..00000000 --- a/src/action/firebaseFallbackActions.ts +++ /dev/null @@ -1,74 +0,0 @@ -import { AxiosResponse } from 'axios'; -import axiosInstance, { ApiKeys, getApiUrl } from '../components/utlis/apiHelper'; -import { logError } from '../components/utlis/errorUtils'; -import { VisitPlanStatus } from '../reducer/userSlice'; -import { FilterResponse } from '../screens/allCases/interface'; -import { CaseDetail } from '../screens/caseDetails/interface'; - -export enum SyncStatus { - SEND_CASES = 'SEND_CASES', - FETCH_CASES = 'FETCH_CASES', - SKIP = 'SKIP', -} - -interface IFilterSync { - filterComponentList: FilterResponse[]; -} - -export interface ISyncedCases { - cases: CaseDetail[]; - filters: IFilterSync; - deletedCaseIds: string[]; - payloadCreatedAt: number; -} - -interface ICases { - caseId: string; - caseViewCreatedAt?: number; -} - -export interface ISyncCaseIdPayload { - agentId: string; - cases: ICases[]; -} - -interface ICasesSyncStatus { - syncStatus: SyncStatus; - visitPlanStatus: VisitPlanStatus; -} - -export const getCasesSyncStatus = async (userReferenceId: string) => { - try { - const url = getApiUrl(ApiKeys.CASES_SYNC_STATUS, {}, { userReferenceId }); - const response: AxiosResponse = await axiosInstance.get(url, { - headers: { donotHandleError: true }, - }); - return response?.data; - } catch (err) { - logError(err as Error, 'Error getting sync status'); - } -}; - -export const sendSyncCaseIds = async (payload: ISyncCaseIdPayload) => { - try { - const url = getApiUrl(ApiKeys.CASES_SEND_ID); - const response = await axiosInstance.post(url, payload, { - headers: { donotHandleError: true }, - }); - return response?.data; - } catch (err) { - logError(err as Error, 'Error sending case ids sync'); - } -}; - -export const fetchCasesToSync = async (agentReferenceId: string) => { - //disabling this function since its conflicting with the new sync logic - return null; - try { - const url = getApiUrl(ApiKeys.FETCH_CASES, { agentReferenceId }); - const response = await axiosInstance.get(url, { headers: { donotHandleError: true } }); - return response?.data; - } catch (err) { - logError(err as Error, 'Error fetching cases to be synced'); - } -}; diff --git a/src/common/AgentActivityConfigurableConstants.ts b/src/common/AgentActivityConfigurableConstants.ts index c2d1daf5..582bf6cf 100644 --- a/src/common/AgentActivityConfigurableConstants.ts +++ b/src/common/AgentActivityConfigurableConstants.ts @@ -1,7 +1,6 @@ let ACTIVITY_TIME_ON_APP: number = 5; //5 seconds let ACTIVITY_TIME_WINDOW_HIGH: number = 10; //10 minutes let ACTIVITY_TIME_WINDOW_MEDIUM: number = 30; //30 minutes -let ENABLE_FIRESTORE_RESYNC = false; let FIRESTORE_RESYNC_INTERVAL_IN_MINUTES = 15; let DATA_SYNC_JOB_INTERVAL_IN_MINUTES = 10; // 10 minutes let IMAGE_UPLOAD_JOB_INTERVAL_IN_MINUTES = 10; // 10 minutes @@ -14,7 +13,6 @@ export const getActivityTimeOnApp = () => ACTIVITY_TIME_ON_APP; export const getActivityTimeWindowHigh = () => ACTIVITY_TIME_WINDOW_HIGH; export const getActivityTimeWindowMedium = () => ACTIVITY_TIME_WINDOW_MEDIUM; -export const getEnableFirestoreResync = () => ENABLE_FIRESTORE_RESYNC; export const getFirestoreResyncIntervalInMinutes = () => FIRESTORE_RESYNC_INTERVAL_IN_MINUTES; export const getDataSyncJobIntervalInMinutes = () => DATA_SYNC_JOB_INTERVAL_IN_MINUTES; export const getImageUploadJobIntervalInMinutes = () => IMAGE_UPLOAD_JOB_INTERVAL_IN_MINUTES; @@ -37,10 +35,6 @@ export const setActivityTimeWindowMedium = (activityTimeWindowMedium: number) => ACTIVITY_TIME_WINDOW_MEDIUM = activityTimeWindowMedium; }; -export const setEnableFirestoreResync = (enableFirestoreResync: boolean) => { - ENABLE_FIRESTORE_RESYNC = enableFirestoreResync; -}; - export const setFirestoreResyncIntervalInMinutes = (firestoreResyncIntervalInMinutes: number) => { FIRESTORE_RESYNC_INTERVAL_IN_MINUTES = firestoreResyncIntervalInMinutes; }; diff --git a/src/common/TrackingComponent.tsx b/src/common/TrackingComponent.tsx index be41283c..ca5ae264 100644 --- a/src/common/TrackingComponent.tsx +++ b/src/common/TrackingComponent.tsx @@ -17,18 +17,7 @@ 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 { MILLISECONDS_IN_A_MINUTE} from '../../RN-UI-LIB/src/utlis/common'; import { getConfigData } from '../action/configActions'; import { AppStates } from '../types/appStates'; import { StorageKeys } from '../types/storageKeys'; @@ -37,7 +26,6 @@ import { getActivityTimeOnApp, getActivityTimeWindowMedium, getActivityTimeWindowHigh, - getEnableFirestoreResync, getDataSyncJobIntervalInMinutes, getImageUploadJobIntervalInMinutes, getVideoUploadJobIntervalInMinutes, @@ -47,7 +35,7 @@ import { } from './AgentActivityConfigurableConstants'; import { GlobalImageMap } from './CachedImage'; import { addClickstreamEvent } from '../services/clickstreamEventService'; -import { CLICKSTREAM_EVENT_NAMES, LocalStorageKeys } from './Constants'; +import { CLICKSTREAM_EVENT_NAMES } from './Constants'; import useResyncFirebase from '@hooks/useResyncFirebase'; import { imageSyncService, sendImagesToServer } from '@services/imageSyncService'; import { sendAudiosToServer } from '@services/audioSyncService'; @@ -82,7 +70,6 @@ interface ITrackingComponent { children?: ReactNode; } -let LAST_SYNC_STATUS = 'SKIP'; const ACTIVITY_TIME_WINDOW = 10; // 10 minutes const TrackingComponent: React.FC = ({ children }) => { @@ -90,11 +77,7 @@ const TrackingComponent: React.FC = ({ children }) => { const dispatch = useAppDispatch(); const appState = useRef(AppState.currentState); - const isTeamLead = useAppSelector((state) => state.user.isTeamLead); - const caseSyncLock = useAppSelector((state) => state?.user?.caseSyncLock); const referenceId = useAppSelector((state) => state.user.user?.referenceId!); - const pendingList = useAppSelector((state) => state.allCases.pendingList); - const pinnedList = useAppSelector((state) => state.allCases.pinnedList); const handleTimeSync = async () => { try { @@ -113,46 +96,6 @@ const TrackingComponent: React.FC = ({ children }) => { 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); @@ -163,7 +106,6 @@ const TrackingComponent: React.FC = ({ children }) => { } const foregroundTime = dayJs(foregroundTimestamp); - const backgroundTime = dayJs(backgroundTimestamp); const stateSetTime = dayJs(stateSetTimestamp); const diffBetweenCurrentTimeAndForegroundTime = @@ -323,15 +265,6 @@ const TrackingComponent: React.FC = ({ children }) => { }, ]; - 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; @@ -371,7 +304,6 @@ const TrackingComponent: React.FC = ({ children }) => { 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); @@ -385,22 +317,11 @@ const TrackingComponent: React.FC = ({ children }) => { 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)); - } - } - })(); + if (!referenceId) { + return; + } + dispatch(getConfigData()); }, []); useEffect(() => { diff --git a/src/hooks/useFirestoreUpdates.ts b/src/hooks/useFirestoreUpdates.ts index 7f2539d9..d2fd2a41 100644 --- a/src/hooks/useFirestoreUpdates.ts +++ b/src/hooks/useFirestoreUpdates.ts @@ -1,24 +1,22 @@ import { useEffect, useRef } from 'react'; -import firestore, { type FirebaseFirestoreTypes } from '@react-native-firebase/firestore'; +import { type FirebaseFirestoreTypes } from '@react-native-firebase/firestore'; import auth from '@react-native-firebase/auth'; import { setFeedbackFilterTemplate } from '@reducers/feedbackFiltersSlice'; import { InteractionManager } from 'react-native'; import { useAppDispatch, useAppSelector } from '.'; -import { setLoading, updateCaseDetailsFirestore } from '../reducer/allCasesSlice'; +import { setLoading } from '../reducer/allCasesSlice'; import { type CaseDetail } from '../screens/caseDetails/interface'; -import { CLICKSTREAM_EVENT_NAMES, FirestoreUpdateTypes } from '../common/Constants'; import { toast } from '../../RN-UI-LIB/src/components/toast'; import { updateCollectionTemplateData } from '../reducer/caseReducer'; import { type ILockData, MY_CASE_ITEM, setLockData, VisitPlanStatus } from '../reducer/userSlice'; import { setFilters } from '../reducer/filtersSlice'; import { ToastMessages } from '../screens/allCases/constants'; import { setCurrentProdAPK } from '../reducer/metadataSlice'; -import { logError } from '../components/utlis/errorUtils'; -import { type GenericFunctionArgs } from '../common/GenericTypes'; -import { addClickstreamEvent } from '@services/clickstreamEventService'; import { setCsaFilters } from '@reducers/cosmosSupportSlice'; import { setActiveCallData, setCallingFeedbackNudgeBottomSheet } from '@reducers/activeCallSlice'; import { FEEDBACK_NUDGE_STATUS } from '@screens/caseDetails/CallingFlow/interfaces'; +import { updateCases } from '@screens/caseDetails/utils/caseDetailsUtils'; +import { firestoreService } from '@services/firestoreService'; export interface CaseUpdates { updateType: string; @@ -31,13 +29,12 @@ export const loggedOutCurrentUser = async () => { } }; +const { subscribeToDoc, subscribeToCollection, unsubscribeAll } = firestoreService; + const isUserSignedIn = () => !!auth().currentUser; const useFirestoreUpdates = () => { const isTeamLead = useAppSelector((state) => state.user.isTeamLead); - const caseDetails = useAppSelector((state) => state.allCases.caseDetails); - const casesList = useAppSelector((state) => state.allCases.casesList); - const user = useAppSelector((state) => state.user.user); const isLoggedIn = useAppSelector((state) => state.user.isLoggedIn); const sessionDetails = useAppSelector((state) => state.user.sessionDetails); @@ -50,82 +47,17 @@ const useFirestoreUpdates = () => { useEffect(() => { lockRef.current = lock; }, [lock]); - - let casesUnsubscribe: GenericFunctionArgs; - let collectionTemplateUnsubscribe: GenericFunctionArgs; - let filterUnsubscribe: GenericFunctionArgs; - let lockUnsubscribe: GenericFunctionArgs; - let feedbackFiltersUnsubscribe: GenericFunctionArgs; - let appUpdateUnsubscribe: GenericFunctionArgs; - let csaFiltersUnsubscribe: GenericFunctionArgs; - let activeCallDetailsUnsubscribe: GenericFunctionArgs; const dispatch = useAppDispatch(); - const showCaseUpdationToast = (newlyAddedCases: number, deletedCases: number) => { - let toastConfig: any = null; - const addedCasesText = newlyAddedCases - ? `${newlyAddedCases} new case${newlyAddedCases > 1 ? 's' : ''} allocated` - : ''; - const deletedCasesText = deletedCases - ? `${deletedCases} case${deletedCases > 1 ? 's' : ''} de-allocated` - : ''; - if (newlyAddedCases && deletedCases) { - toastConfig = { - type: 'info', - text1: `${addedCasesText} & ${deletedCasesText}`, - }; - } else if (newlyAddedCases) { - toastConfig = { type: 'success', text1: addedCasesText }; - } else if (deletedCases) { - toastConfig = { type: 'error', text1: deletedCasesText }; - } - if (toastConfig) { - toast(toastConfig); - } - }; - const handleCasesUpdate = async (querySnapshot: FirebaseFirestoreTypes.QuerySnapshot) => { - let newlyAddedCases = 0; - let deletedCases = 0; - const caseUpdates: CaseUpdates[] = []; - querySnapshot - .docChanges() - .forEach((documentSnapshot: FirebaseFirestoreTypes.DocumentChange) => { - InteractionManager.runAfterInteractions(() => { - const updateType = documentSnapshot.type; - const updatedCaseDetail = documentSnapshot.doc.data() as CaseDetail; - if (updateType === FirestoreUpdateTypes.ADDED) { - if (!caseDetails[updatedCaseDetail.id]) { - newlyAddedCases++; - caseUpdates.push({ updateType, updatedCaseDetail }); - } - } else { - if (updateType === FirestoreUpdateTypes.REMOVED) { - deletedCases++; - } - caseUpdates.push({ updateType, updatedCaseDetail }); - } - }); - }); - const isInitialLoad = casesList.length === 0; - InteractionManager.runAfterInteractions(() => { - requestAnimationFrame(() => { - InteractionManager.runAfterInteractions(() => { - dispatch( - updateCaseDetailsFirestore({ - caseUpdates, - isInitialLoad, - isVisitPlanLocked: lockRef?.current?.visitPlanStatus === VisitPlanStatus.LOCKED, - selectedAgent, - }) - ); - }); - }); - }); - !isInitialLoad && showCaseUpdationToast(newlyAddedCases, deletedCases); - addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_SNAPSHOT_LISTENER, { - snapshot_path: 'handleCasesUpdate', - }); + const isVisitPlanLocked = lockRef?.current?.visitPlanStatus === VisitPlanStatus.LOCKED; + dispatch( + updateCases({ + selectedAgent, + isVisitPlanLocked, + querySnapshot, + }) + ); }; const handleCollectionTemplateUpdate = ( @@ -166,17 +98,12 @@ const useFirestoreUpdates = () => { snapshot: FirebaseFirestoreTypes.DocumentSnapshot ) => { const activeCallDetails = snapshot.data(); - if(activeCallDetails?.feedbackNudgeStatus === FEEDBACK_NUDGE_STATUS.PENDING) { + if (activeCallDetails?.feedbackNudgeStatus === FEEDBACK_NUDGE_STATUS.PENDING) { dispatch(setCallingFeedbackNudgeBottomSheet(true)); } dispatch(setActiveCallData(activeCallDetails)); }; - const handleError = (err: any, collectionPath?: string) => { - const errMsg = `Error while fetching fireStore snapshot: referenceId: ${user?.referenceId} collectionPath: ${collectionPath}`; - logError(err as Error, errMsg); - }; - const handleAppUpdate = ( snapshot: FirebaseFirestoreTypes.DocumentSnapshot ) => { @@ -184,107 +111,51 @@ const useFirestoreUpdates = () => { dispatch(setCurrentProdAPK(configData)); }; - const subscribeToAppUpdate = () => { - const collectionPath = 'app-state/app-update'; - return subscribeToDoc(handleAppUpdate, collectionPath); - }; - const signInUserToFirebase = () => { if (!sessionDetails) { + dispatch(setLoading(false)); return; } - auth() - .signInWithCustomToken(sessionDetails?.firebaseToken) - .then((userCredential) => { - addFirestoreListeners(); - }) - .catch((error) => { + firestoreService.signInUserToFirebase( + sessionDetails.firebaseToken, + subscribeToFirestore, + () => { dispatch(setLoading(false)); - logError(error as Error, 'Error in signInUserToFirebase'); toast({ type: 'error', text1: ToastMessages.FIRESTORE_SIGNIN_FAILED, }); - }); - }; - - const subscribeToDoc = (successCb: GenericFunctionArgs, collectionPath: string) => - firestore() - .doc(collectionPath) - .onSnapshot( - async (data) => { - successCb(data); - }, - (err) => { - handleError(err, collectionPath); - } - ); - - const subscribeToCases = () => { - let refId = user?.referenceId; - if (isTeamLead && selectedAgent?.referenceId !== MY_CASE_ITEM.referenceId) { - refId = selectedAgent?.referenceId; - } - const collectionPath = `allocations/${refId}/cases`; - return firestore() - .collection(collectionPath) - .orderBy('totalOverdueAmount', 'asc') // It is descending order only, but acting weirdly. Need to check. - .onSnapshot(handleCasesUpdate, (err) => { - handleError(err, collectionPath); - }); - }; - - const subscribeToCollectionTemplate = () => { - const collectionPath = `template/${isExternalAgent ? 'external' : 'inhouse'}_template`; - return subscribeToDoc(handleCollectionTemplateUpdate, collectionPath); - }; - - const subscribeToFilters = () => { - let refId = user?.referenceId; - if (isTeamLead && selectedAgent?.referenceId !== MY_CASE_ITEM.referenceId) { - refId = selectedAgent?.referenceId; - } - const collectionPath = `filters/${refId}`; - - return subscribeToDoc(handleFilterUpdate, collectionPath); - }; - - const subscribeToFeedbackFilters = () => { - const feedbackFiltersPath = `feedback-filters/v2`; - return subscribeToDoc(handleFeedbackFilters, feedbackFiltersPath); - }; - - const subscribeToLocks = () => { - const lockPath = `locks/${user?.referenceId}`; - return subscribeToDoc(handleLockUpdate, lockPath); - }; - - const subscribeToCsaFilters = () => { - const collectionPath = 'global-filters/v1'; - return subscribeToDoc(handleCsaFilters, collectionPath); - }; - - const subscribeToActiveCallDetailsUnsubscribe = () => { - const refId = user?.referenceId; - const collectionPath = `allocations/${refId}/agentActivity/callDetails`; - return subscribeToDoc(handleActiveCallDetails, collectionPath); + } + ); }; const subscribeToFirestore = () => { - addFirestoreListeners(); + let refId = user?.referenceId; + if (isTeamLead && selectedAgent?.referenceId !== MY_CASE_ITEM.referenceId) { + refId = selectedAgent?.referenceId; + } + InteractionManager.runAfterInteractions(() => { + subscribeToCollection( + `allocations/${refId}/cases`, + (ref) => ref.orderBy('totalOverdueAmount', 'asc'), + handleCasesUpdate + ); + subscribeToDoc(`filters/${refId}`, handleFilterUpdate); + subscribeToDoc( + `template/${isExternalAgent ? 'external' : 'inhouse'}_template`, + handleCollectionTemplateUpdate + ); + subscribeToDoc(`locks/${user?.referenceId}`, handleLockUpdate); + subscribeToDoc(`feedback-filters/v2`, handleFeedbackFilters); + subscribeToDoc('global-filters/v1', handleCsaFilters); + subscribeToDoc( + `allocations/${user?.referenceId}/agentActivity/callDetails`, + handleActiveCallDetails + ); + subscribeToDoc('app-state/app-update', handleAppUpdate); + }); }; - function addFirestoreListeners() { - casesUnsubscribe = subscribeToCases(); - filterUnsubscribe = subscribeToFilters(); - collectionTemplateUnsubscribe = subscribeToCollectionTemplate(); - lockUnsubscribe = subscribeToLocks(); - feedbackFiltersUnsubscribe = subscribeToFeedbackFilters(); - csaFiltersUnsubscribe = subscribeToCsaFilters(); - activeCallDetailsUnsubscribe = subscribeToActiveCallDetailsUnsubscribe(); - appUpdateUnsubscribe = subscribeToAppUpdate(); - } - useEffect(() => { if (!user?.referenceId) { return; @@ -294,48 +165,12 @@ const useFirestoreUpdates = () => { return; } if (isUserSignedIn()) { - InteractionManager.runAfterInteractions(() => { - subscribeToFirestore(); - }); + subscribeToFirestore(); } else { dispatch(setLoading(true)); signInUserToFirebase(); } - return () => { - casesUnsubscribe && casesUnsubscribe(); - filterUnsubscribe && filterUnsubscribe(); - collectionTemplateUnsubscribe && collectionTemplateUnsubscribe(); - lockUnsubscribe && lockUnsubscribe(); - feedbackFiltersUnsubscribe && feedbackFiltersUnsubscribe(); - csaFiltersUnsubscribe && csaFiltersUnsubscribe(); - activeCallDetailsUnsubscribe && activeCallDetailsUnsubscribe(); - appUpdateUnsubscribe && appUpdateUnsubscribe(); - }; - }, [isLoggedIn, user?.referenceId, isExternalAgent]); - - useEffect(() => { - if (!isTeamLead) { - return; - } - if (!selectedAgent?.referenceId) { - return; - } - if (!isLoggedIn || !sessionDetails?.firebaseToken) { - loggedOutCurrentUser(); - return; - } - // unsubscribe from previous agent's cases - casesUnsubscribe && casesUnsubscribe(); - filterUnsubscribe && filterUnsubscribe(); - dispatch(setLoading(true)); - // subscribe to new agent's cases - subscribeToCases(); - subscribeToFilters(); - return () => { - casesUnsubscribe && casesUnsubscribe(); - filterUnsubscribe && filterUnsubscribe(); - }; - }, [selectedAgent, isTeamLead]); + }, [isLoggedIn, user?.referenceId, isExternalAgent, selectedAgent]); }; export default useFirestoreUpdates; diff --git a/src/hooks/useResyncFirebase.ts b/src/hooks/useResyncFirebase.ts index 8b71e999..236eec79 100644 --- a/src/hooks/useResyncFirebase.ts +++ b/src/hooks/useResyncFirebase.ts @@ -1,20 +1,19 @@ import firestore from '@react-native-firebase/firestore'; -import { useAppDispatch, useAppSelector } from '@hooks'; -import store, { type RootState } from '@store'; -import { updateCaseDetailsFirestore } from '@reducers/allCasesSlice'; +import { useAppDispatch } from '@hooks'; +import store from '@store'; import { CLICKSTREAM_EVENT_NAMES, FirestoreUpdateTypes, SyncedSource } from '@common/Constants'; import axiosInstance, { ApiKeys, getApiUrl } from '@utils/apiHelper'; import { getSyncCaseIds } from '@utils/firebaseFallbackUtils'; import { logError } from '@utils/errorUtils'; import { addClickstreamEvent } from '@services/clickstreamEventService'; -import { GenericObject } from '@common/GenericTypes'; import { setLastFirebaseResyncTimestamp } from '@reducers/metadataSlice'; import dayJs from 'dayjs'; -import { - getEnableFirestoreResync, - getFirestoreResyncIntervalInMinutes, -} from '@common/AgentActivityConfigurableConstants'; import AsyncStorage from '@react-native-async-storage/async-storage'; +import { updateCases } from '@screens/caseDetails/utils/caseDetailsUtils'; +import { CaseAllocationType } from '@screens/allCases/interface'; +import { CaseDetail } from '@screens/caseDetails/interface'; +import { CaseUpdates } from './useFirestoreUpdates'; +import { getFirestoreResyncIntervalInMinutes } from '@common/AgentActivityConfigurableConstants'; const selectedAgentReferenceIDForMyCases = 'MY_CASES'; @@ -25,9 +24,9 @@ type CasesToFetchPayload = { updatedCaseIds: string[]; }; }; + const useResyncFirebase = () => { const dispatch = useAppDispatch(); - const refId = store?.getState()?.user?.user?.referenceId || ''; const selectedAgent = store?.getState()?.user?.selectedAgent; const selectedAgentRefId = store?.getState()?.user?.selectedAgent?.referenceId || ''; @@ -46,36 +45,126 @@ const useResyncFirebase = () => { }); }; - const updateCaseInRedux = ( - updateType: FirestoreUpdateTypes, - caseDetails: GenericObject, - selectedAgent: GenericObject + const fetchFirestoreCases = async ( + caseIds: string[], + casesPath: string, + updateType: string = FirestoreUpdateTypes.ADDED ) => { + const caseDocs = await firestore() + .collection(casesPath) + .where('caseReferenceId', 'in', caseIds) + .get(); + + const firebaseAllocatedCases: CaseUpdates[] = []; + + caseDocs?.forEach((doc) => { + const firebaseCase = doc.data() as CaseDetail; + if (!firebaseCase?.caseReferenceId) { + return; + } + firebaseAllocatedCases.push({ + updateType, + updatedCaseDetail: firebaseCase, + }); + void addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_FIREBASE_RESYNC, { + [updateType === FirestoreUpdateTypes.ADDED ? 'added' : 'update']: + firebaseCase.caseReferenceId, + syncedSource: SyncedSource.FIREBASE, + }); + }); + + // If firebase case is not found, fetch from API + const firebaseCaseIdsSet = new Set( + firebaseAllocatedCases.map((firebaseCase) => firebaseCase.updatedCaseDetail.caseReferenceId) + ); + + const caseIdsToFetch = caseIds.filter((caseId) => !firebaseCaseIdsSet.has(caseId)); + + for (const caseId of caseIdsToFetch) { + try { + const res = await _getCaseDetailsFromApi(caseId); + const caseDetails = res?.data; + firebaseAllocatedCases.push({ + updateType, + updatedCaseDetail: caseDetails, + }); + void addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_FIREBASE_RESYNC, { + updated: caseId, + syncedSource: SyncedSource.API, + }); + } catch (err) { + logError(err as Error, 'Error fetching cases from firestore'); + } + } + + return firebaseAllocatedCases; + }; + + const addCasesToRedux = async (allocatedCases: string[], casesPath: string) => { + if (!allocatedCases.length) { + return; + } + const firebaseAllocatedCases = await fetchFirestoreCases(allocatedCases, casesPath); dispatch( - updateCaseDetailsFirestore({ - caseUpdates: [ - { - updateType: updateType, - updatedCaseDetail: caseDetails, - }, - ], - isInitialLoad: false, - isVisitPlanLocked: false, + updateCases({ selectedAgent, + caseUpdateList: firebaseAllocatedCases, + }) + ); + }; + + const removeCasesFromRedux = (unallocatedCases: string[]) => { + if (!unallocatedCases.length) { + return; + } + const unallocatedCasesUpdates: CaseUpdates[] = []; + unallocatedCases.forEach((caseId: string) => { + if (!caseId) { + return null; + } + unallocatedCasesUpdates.push({ + updateType: FirestoreUpdateTypes.REMOVED, + updatedCaseDetail: { + caseType: CaseAllocationType.COLLECTION_CASE, + caseReferenceId: caseId, + id: '', + pinRank: null, + caseViewCreatedAt: 0, + } as CaseDetail, + }); + void addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_FIREBASE_RESYNC, { deleted: caseId }); + }); + dispatch( + updateCases({ + selectedAgent, + caseUpdateList: unallocatedCasesUpdates, + }) + ); + }; + + const modifyCasesInRedux = async (updatedCases: string[], casesPath: string) => { + if (!updatedCases.length) { + return; + } + const firebaseUpdatedCases = await fetchFirestoreCases( + updatedCases, + casesPath, + FirestoreUpdateTypes.MODIFIED + ); + dispatch( + updateCases({ + selectedAgent, + caseUpdateList: firebaseUpdatedCases, }) ); }; return async (): Promise => { - console.log('firebase resync called'); const now = dayJs().toString(); const FIRST_DATE = new Date(1970, 1, 1); const lastFirebaseResyncTimestamp = (await AsyncStorage.getItem('lastFirebaseResyncTimestamp')) || dayJs(FIRST_DATE).toString(); const minutesSinceLastResync = dayJs(now).diff(dayJs(lastFirebaseResyncTimestamp), 'minutes'); - if (!getEnableFirestoreResync()) { - return; - } if (minutesSinceLastResync < getFirestoreResyncIntervalInMinutes()) { return; } @@ -93,117 +182,16 @@ const useResyncFirebase = () => { } ); - const allocatedCases = casesToFetch?.data?.allocatedCaseIds; - const unallocatedCases = casesToFetch?.data?.deallocatedCaseIds; - const updatedCases = casesToFetch?.data?.updatedCaseIds; + const allocatedCases = casesToFetch?.data?.allocatedCaseIds || []; + const unallocatedCases = casesToFetch?.data?.deallocatedCaseIds || []; + const updatedCases = casesToFetch?.data?.updatedCaseIds || []; - allocatedCases.forEach((caseId: string) => { - if (!caseId) { - return null; - } - firestore() - .collection(casesPath) - .doc(caseId.toString()) - .get({ source: 'server' }) - .then((res) => { - const firebaseCase = res?.data() || {}; - if (!firebaseCase?.caseReferenceId) { - throw new Error('could not find case in firebase'); - } - dispatch( - updateCaseDetailsFirestore({ - caseUpdates: [ - { - updateType: FirestoreUpdateTypes.ADDED, - updatedCaseDetail: firebaseCase, - }, - ], - isInitialLoad: false, - isVisitPlanLocked: false, - selectedAgent, - }) - ); - void addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_FIREBASE_RESYNC, { - added: caseId, - syncedSource: SyncedSource.FIREBASE, - }); - }) - .catch((err) => { - logError(err as Error, 'Error fetching cases from firestore'); - void _getCaseDetailsFromApi(caseId).then((res: { data: GenericObject }) => { - const caseDetails = res?.data; - updateCaseInRedux(FirestoreUpdateTypes.ADDED, caseDetails, selectedAgent); - void addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_FIREBASE_RESYNC, { - updated: caseId, - syncedSource: SyncedSource.API, - }); - }); - }); - }); - - unallocatedCases.forEach((caseId: string) => { - if (!caseId) { - return null; - } - dispatch( - updateCaseDetailsFirestore({ - caseUpdates: [ - { - updateType: FirestoreUpdateTypes.REMOVED, - updatedCaseDetail: { - caseType: '', - caseReferenceId: caseId, - id: '', - pinRank: '', - caseViewCreatedAt: '', - }, - }, - ], - isInitialLoad: false, - isVisitPlanLocked: false, - selectedAgent, - }) - ); - void addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_FIREBASE_RESYNC, { deleted: caseId }); - }); - - updatedCases.forEach((caseId: string) => { - if (!caseId) { - return null; - } - firestore() - .collection(casesPath) - .doc(caseId.toString()) - .get({ source: 'server' }) - .then((res) => { - const firebaseCase = res?.data() || {}; - - if (!firebaseCase?.caseReferenceId) { - throw new Error('could not find case in firebase'); - } - updateCaseInRedux(FirestoreUpdateTypes.MODIFIED, firebaseCase, selectedAgent); - void addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_FIREBASE_RESYNC, { - updated: caseId, - syncedSource: SyncedSource.FIREBASE, - }); - }) - .catch((err) => { - void _getCaseDetailsFromApi(caseId).then((res: { data: GenericObject }) => { - const caseDetails = res?.data || {}; - updateCaseInRedux(FirestoreUpdateTypes.MODIFIED, caseDetails, selectedAgent); - void addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_FIREBASE_RESYNC, { - updated: caseId, - syncedSource: SyncedSource.API, - }); - }); - logError(err as Error, 'Error fetching cases from firestore'); - console.log('cases err:', err); - }); - }); + addCasesToRedux(allocatedCases, casesPath); + removeCasesFromRedux(unallocatedCases); + modifyCasesInRedux(updatedCases, casesPath); dispatch(setLastFirebaseResyncTimestamp(dayJs().toString())); await AsyncStorage.setItem('lastFirebaseResyncTimestamp', dayJs().toString()); - await Promise.resolve(); }; }; diff --git a/src/reducer/allCasesSlice.ts b/src/reducer/allCasesSlice.ts index dd4e68f7..5675949f 100644 --- a/src/reducer/allCasesSlice.ts +++ b/src/reducer/allCasesSlice.ts @@ -1,49 +1,32 @@ import { createSlice } from '@reduxjs/toolkit'; -import { toast } from '../../RN-UI-LIB/src/components/toast'; import { _map } from '../../RN-UI-LIB/src/utlis/common'; -import { - findDocumentByDocumentType, - getLoanAccountNumber, -} from '../components/utlis/commonFunctions'; -import { CLICKSTREAM_EVENT_NAMES, FirestoreUpdateTypes } from '../common/Constants'; -import { getCurrentScreen, navigateToScreen } from '../components/utlis/navigationUtlis'; -import { type CaseUpdates } from '../hooks/useFirestoreUpdates'; +import { CLICKSTREAM_EVENT_NAMES } from '../common/Constants'; +import { navigateToScreen } from '../components/utlis/navigationUtlis'; import { COMPLETED_STATUSES } from '../screens/allCases/constants'; - -import { CaseAllocationType, type ICaseItem, type IReportee } from '../screens/allCases/interface'; -import { - type CaseDetail, - DOCUMENT_TYPE, - type IGeolocation, -} from '../screens/caseDetails/interface'; +import { CaseAllocationType, type ICaseItem } from '../screens/allCases/interface'; +import { type CaseDetail } from '../screens/caseDetails/interface'; import { addClickstreamEvent } from '../services/clickstreamEventService'; import { getVisitedWidgetsNodeList } from '../components/form/services/forms.service'; import { CollectionCaseWidgetId, CommonCaseWidgetId } from '../types/template.types'; import { type IAvatarUri } from '../action/caseListAction'; -import { MY_CASE_ITEM } from './userSlice'; export type ICasesMap = Record; interface IAllCasesSlice { casesList: ICaseItem[]; - casesListMap: ICasesMap; intermediateTodoList: ICaseItem[]; intermediateTodoListMap: ICasesMap; selectedTodoListMap: ICasesMap; selectedTodoListCount: number; - initialPinnedRankCount: number; pinnedRankCount: number; loading: boolean; newlyPinnedCases: number; - completedCases: number; caseDetails: Record; searchQuery: string; visitPlansUpdating: boolean; pendingList: ICaseItem[]; completedList: ICaseItem[]; pinnedList: ICaseItem[]; - newVisitedCases: string[]; - geolocations?: IGeolocation[]; selectedCaseId: string; allCasesViewSearchQuery: string; visitPlanSearchQuery: string; @@ -51,45 +34,24 @@ interface IAllCasesSlice { const initialState: IAllCasesSlice = { casesList: [], - casesListMap: {}, intermediateTodoList: [], intermediateTodoListMap: {}, selectedTodoListCount: 0, selectedTodoListMap: {}, - initialPinnedRankCount: 0, pinnedRankCount: 0, loading: false, newlyPinnedCases: 0, - completedCases: 0, caseDetails: {}, searchQuery: '', visitPlansUpdating: false, pendingList: [], completedList: [], pinnedList: [], - newVisitedCases: [], selectedCaseId: '', allCasesViewSearchQuery: '', visitPlanSearchQuery: '', }; -const getCaseListComponents = (casesList: ICaseItem[], caseDetails: Record) => { - const pendingList: ICaseItem[] = []; - const completedList: ICaseItem[] = []; - const pinnedList: ICaseItem[] = []; - casesList.forEach((item) => { - const { caseReferenceId, pinRank } = item; - const { caseStatus } = caseDetails[caseReferenceId] || {}; - const isCaseCompleted = COMPLETED_STATUSES.includes(caseStatus); - isCaseCompleted - ? completedList.push(item) - : pinRank - ? pinnedList.push(item) - : pendingList.push(item); - }); - return { pendingList, completedList, pinnedList }; -}; - export const getUpdatedCollectionCaseDetail = ({ caseData, answer, @@ -101,8 +63,6 @@ export const getUpdatedCollectionCaseDetail = ({ updatedValue.isSynced = false; updatedValue.isApiCalled = false; updatedValue.taskStatus = 'completed'; - // @deprecating - const { visitedWidgets } = answer; const allWidget = answer.widgetContext; const widgetContext = {}; @@ -127,16 +87,6 @@ export const getUpdatedCollectionCaseDetail = ({ return updatedValue; }; -const getCaseListItem = ( - caseReferenceId: string, - pinRank?: number | null, - caseViewCreatedAt?: number -) => ({ - caseReferenceId, - pinRank: pinRank || null, - caseViewCreatedAt, -}); - const allCasesSlice = createSlice({ name: 'cases', initialState, @@ -144,166 +94,21 @@ const allCasesSlice = createSlice({ setLoading: (state, action) => { state.loading = action.payload; }, - updateCaseDetailsFirestore: (state, action) => { - const { caseUpdates, isInitialLoad, isVisitPlanLocked, selectedAgent } = action.payload as { - caseUpdates: CaseUpdates[]; - isInitialLoad: boolean; - isVisitPlanLocked: boolean; - selectedAgent: IReportee; - }; - const newVisitCaseLoanIds: string[] = []; - const newVisitCollectionCases: string[] = []; - const removedVisitedCasesLoanIds: string[] = []; - caseUpdates.forEach(({ updateType, updatedCaseDetail }) => { - const { caseType, caseReferenceId, id, pinRank, caseViewCreatedAt } = updatedCaseDetail; - - const caseId = caseReferenceId || id; - switch (updateType) { - case FirestoreUpdateTypes.MODIFIED: { - const index = state.casesList?.findIndex( - (caseItem) => caseItem.caseReferenceId?.toString() === caseId?.toString() - ); - if (index !== -1) { - if (pinRank && !state.casesList[index].pinRank) { - // this is a new visit case - newVisitCaseLoanIds.push( - state.caseDetails[caseId]?.loanAccountNumber ?? - state.caseDetails[caseId]?.loanDetails?.loanAccountNumber ?? - 0 - ); - if (caseType === CaseAllocationType.COLLECTION_CASE) { - newVisitCollectionCases.push(caseId); - } - } - if (!pinRank && state.casesList[index].pinRank) { - // this is a removed visit case - removedVisitedCasesLoanIds.push( - state.caseDetails[caseId]?.loanAccountNumber ?? - state.caseDetails[caseId]?.loanDetails?.loanAccountNumber ?? - 0 - ); - } - state.casesList[index] = { - ...state.casesList[index], - caseReferenceId: caseId, - pinRank: pinRank || null, - caseViewCreatedAt: caseViewCreatedAt, - }; - } - let currentTask = null; - if (caseType !== CaseAllocationType.COLLECTION_CASE) { - const { tasks, currentTask: updatedCurrentTask } = updatedCaseDetail; - currentTask = tasks?.find((task) => task.taskType === (updatedCurrentTask as string)); - } - state.caseDetails[caseId] = { - ...updatedCaseDetail, - currentTask, - isSynced: true, - imageUri: state.caseDetails[caseId]?.imageUri, - }; - break; - } - case FirestoreUpdateTypes.ADDED: { - if (state.caseDetails[caseId]) { - return; - } - if (pinRank && caseType === CaseAllocationType.COLLECTION_CASE) { - newVisitCollectionCases.push(caseId); - } - const caseListItem = getCaseListItem(caseId, pinRank, caseViewCreatedAt); - state.casesList.unshift(caseListItem); - let currentTask = null; - if (caseType !== CaseAllocationType.COLLECTION_CASE) { - const { tasks, currentTask: updatedCurrentTask } = updatedCaseDetail; - currentTask = tasks?.find( - (task) => task?.taskType === (updatedCurrentTask as string) - ); - } - const imageUri = - findDocumentByDocumentType( - updatedCaseDetail.documents, - DOCUMENT_TYPE.OPTIMIZED_SELFIE - )?.uri || ''; - state.caseDetails[caseId] = { - ...updatedCaseDetail, - currentTask, - isSynced: true, - isNewlyAdded: !isInitialLoad, - imageUri, - }; - break; - } - case FirestoreUpdateTypes.REMOVED: { - const index = state.casesList.findIndex( - (caseItem) => caseItem.caseReferenceId?.toString() === caseId?.toString() - ); - const currentScreen = getCurrentScreen(); - // Redirect to home screen if the case deletes which the agent is seeing - if (currentScreen?.name === 'caseDetail') { - const { caseId: id } = currentScreen.params; - if (id === caseId) { - navigateToScreen('Home'); - } - } - if (index !== -1) { - state.casesList.splice(index, 1); - } - delete state.caseDetails[caseId]; - break; - } - default: - break; - } - }); - const { pendingList, completedList, pinnedList } = getCaseListComponents( - state.casesList, - state.caseDetails - ); + updateCaseDetailsFromFirestore: (state, action) => { + const { + updatedCasesList, + updatedCaseDetails, + updatedLoading, + pendingList, + completedList, + pinnedList, + } = action.payload; + state.casesList = updatedCasesList; + state.caseDetails = updatedCaseDetails; + state.loading = updatedLoading; state.pendingList = pendingList; state.completedList = completedList; state.pinnedList = pinnedList; - state.newVisitedCases = newVisitCollectionCases; - if (state.loading) { - if (selectedAgent && selectedAgent.referenceId !== MY_CASE_ITEM.referenceId) { - addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_AGENT_CASE_LOAD_SUCCESS, { - selectedAgent: selectedAgent.referenceId, - }); - } - state.loading = false; - } - - if (newVisitCaseLoanIds?.length > 0) { - addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_VISIT_PLAN_UPDATED, { - newPinCases: [...newVisitCaseLoanIds], - currentPinCases: pinnedList.map((item) => - getLoanAccountNumber(state.caseDetails[item?.caseReferenceId]) - ), - }); - if (!isVisitPlanLocked) { - toast({ - type: 'info', - text1: `${newVisitCaseLoanIds.length} case${ - newVisitCaseLoanIds.length > 1 ? 's' : '' - } added to the visit plan`, - }); - } - } - if (removedVisitedCasesLoanIds.length > 0) { - addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_VISIT_PLAN_UPDATED, { - newUnpinCases: [...removedVisitedCasesLoanIds], - currentPinCases: pinnedList.map((item) => - getLoanAccountNumber(state.caseDetails[item?.caseReferenceId]) - ), - }); - if (!isVisitPlanLocked) { - toast({ - type: 'info', - text1: `${removedVisitedCasesLoanIds.length} case${ - removedVisitedCasesLoanIds.length > 1 ? 's' : '' - } removed from the visit plan`, - }); - } - } }, updateCaseDetail: (state, action) => { const { caseKey, updatedCaseDetail } = action.payload; @@ -348,7 +153,7 @@ const allCasesSlice = createSlice({ resetTodoList: (state) => { state.intermediateTodoListMap = {}; state.newlyPinnedCases = 0; - state.pinnedRankCount = state.initialPinnedRankCount; + state.pinnedRankCount = 0; }, setSelectedTodoListMap: (state, action: { payload: ICaseItem }) => { const caseId = action.payload.caseReferenceId; @@ -405,10 +210,6 @@ const allCasesSlice = createSlice({ state.caseDetails[id].isSynced = false; } }, - updateCaseDetailBeforeApiCall: (state, action) => { - const { caseId } = action.payload; - state.caseDetails[caseId].isApiCalled = false; - }, toggleNewlyAddedCase: (state, action) => { if (state.caseDetails[action.payload]) { state.caseDetails[action.payload].isNewlyAdded = false; @@ -418,44 +219,6 @@ const allCasesSlice = createSlice({ setVisitPlansUpdating: (state, action) => { state.visitPlansUpdating = action.payload; }, - resetNewVisitedCases: (state) => { - state.newVisitedCases = []; - }, - syncCasesByFallback: (state, action) => { - const { cases = [], deletedCaseIds = [], payloadCreatedAt } = action.payload; - cases.forEach((caseItem: CaseDetail | null) => { - if (!caseItem) { - return; - } - const { caseViewCreatedAt, caseReferenceId, isSynced, pinRank } = caseItem; - const isCaseAlreadyPresent = state.caseDetails[caseReferenceId]; - if ( - !isCaseAlreadyPresent || - (isSynced && caseViewCreatedAt && caseViewCreatedAt < payloadCreatedAt) - ) { - const caseListItem = getCaseListItem(caseReferenceId, pinRank, caseViewCreatedAt); - state.casesList.unshift(caseListItem); - const imageUri = isCaseAlreadyPresent - ? state.caseDetails[caseReferenceId]?.imageUri - : findDocumentByDocumentType(caseItem.documents, DOCUMENT_TYPE.OPTIMIZED_SELFIE)?.uri || - ''; - state.caseDetails[caseReferenceId] = { ...caseItem, isSynced: true, imageUri }; - } - }); - const { pendingList, completedList, pinnedList } = getCaseListComponents( - state.casesList, - state.caseDetails - ); - state.pendingList = pendingList; - state.completedList = completedList; - state.pinnedList = pinnedList; - deletedCaseIds.forEach((caseItem: CaseDetail) => { - const { caseViewCreatedAt, caseReferenceId } = caseItem; - if (caseViewCreatedAt && caseViewCreatedAt < payloadCreatedAt) { - delete state.caseDetails[caseReferenceId]; - } - }); - }, setCasesImageUri: (state, action) => { const imageUris: IAvatarUri[] = action.payload; imageUris.forEach(({ caseId, imageUri }) => { @@ -486,17 +249,14 @@ export const { resetSelectedTodoList, updateCaseDetail, updateSingleCase, - updateCaseDetailsFirestore, toggleNewlyAddedCase, resetCasesData, - updateCaseDetailBeforeApiCall, setVisitPlansUpdating, - resetNewVisitedCases, - syncCasesByFallback, setCasesImageUri, setSelectedCaseId, setAllCasesViewSearchQuery, setVisitPlanSearchQuery, + updateCaseDetailsFromFirestore, } = allCasesSlice.actions; export default allCasesSlice.reducer; diff --git a/src/screens/allCases/AgentListItem.tsx b/src/screens/allCases/AgentListItem.tsx index 19bc42a9..dce897d5 100644 --- a/src/screens/allCases/AgentListItem.tsx +++ b/src/screens/allCases/AgentListItem.tsx @@ -7,7 +7,7 @@ import { COLORS } from '../../../RN-UI-LIB/src/styles/colors'; import { useAppDispatch, useAppSelector } from '../../hooks'; import { setShowAgentSelectionBottomSheet } from '../../reducer/reporteesSlice'; import { IReportee } from './interface'; -import { resetCasesData } from '../../reducer/allCasesSlice'; +import { resetCasesData, setLoading } from '@reducers/allCasesSlice'; import fuzzySort from '../../../RN-UI-LIB/src/utlis/fuzzySort'; import fuzzysort from 'fuzzysort'; import { MY_CASE_ITEM, setSelectedAgent } from '../../reducer/userSlice'; @@ -15,6 +15,7 @@ import { resetFilters } from '../../reducer/filtersSlice'; import { setGlobalUserData } from '../../constants/Global'; import { addClickstreamEvent } from '../../services/clickstreamEventService'; import { CLICKSTREAM_EVENT_NAMES } from '../../common/Constants'; +import { firestoreService } from '@services/firestoreService'; interface IAgentListItem { agent: IReportee; @@ -22,8 +23,17 @@ interface IAgentListItem { searchQuery: string; } +const unsubscribeFromPreviousDoc = (agentId?: string) => { + if (!agentId) { + return; + } + firestoreService.unsubscribeFromPath(`allocations/${agentId}/cases`); + firestoreService.unsubscribeFromPath(`filters/${agentId}`); +}; + const AgentListItem: React.FC = ({ agent, leftAdornment, searchQuery }) => { const selectedAgent = useAppSelector((state) => state.user.selectedAgent) || MY_CASE_ITEM; + const userReferenceId = useAppSelector((state) => state.user.user?.referenceId); const dispatch = useAppDispatch(); const handleAgentSelection = () => { @@ -31,10 +41,16 @@ const AgentListItem: React.FC = ({ agent, leftAdornment, searchQ addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_AGENT_SELECT_BUTTON_CLICKED, { selectedAgentId, }); + if (selectedAgentId !== MY_CASE_ITEM.referenceId) { + unsubscribeFromPreviousDoc(selectedAgent.referenceId); + } else { + unsubscribeFromPreviousDoc(userReferenceId); + } dispatch(setSelectedAgent(agent)); setGlobalUserData({ selectedAgentId }); dispatch(resetFilters()); dispatch(resetCasesData()); + dispatch(setLoading(true)); dispatch(setShowAgentSelectionBottomSheet(false)); }; diff --git a/src/screens/allCases/index.tsx b/src/screens/allCases/index.tsx index 6959e0eb..47036bd4 100644 --- a/src/screens/allCases/index.tsx +++ b/src/screens/allCases/index.tsx @@ -125,7 +125,6 @@ const AllCasesMain = () => { initCrashlytics(userState); } dispatch(setVisitPlansUpdating(false)); - dispatch(setLoading(false)); dispatch(resetTodoList()); dispatch(resetSelectedTodoList()); }, []); diff --git a/src/screens/caseDetails/utils/caseDetailsUtils.tsx b/src/screens/caseDetails/utils/caseDetailsUtils.tsx new file mode 100644 index 00000000..4982af9b --- /dev/null +++ b/src/screens/caseDetails/utils/caseDetailsUtils.tsx @@ -0,0 +1,253 @@ +import { FirebaseFirestoreTypes } from '@react-native-firebase/firestore'; +import { ICaseItem, IReportee } from '@screens/allCases/interface'; +import { InteractionManager } from 'react-native'; +import { CaseDetail, DOCUMENT_TYPE } from '../interface'; +import { CLICKSTREAM_EVENT_NAMES, FirestoreUpdateTypes } from '@common/Constants'; +import store, { AppDispatch } from '@store'; +import { + findDocumentByDocumentType, + getLoanAccountNumber, +} from '@components/utlis/commonFunctions'; +import { getCurrentScreen, navigateToScreen } from '@components/utlis/navigationUtlis'; +import { COMPLETED_STATUSES } from '@screens/allCases/constants'; +import { MY_CASE_ITEM } from '@reducers/userSlice'; +import { addClickstreamEvent } from '@services/clickstreamEventService'; +import { toast } from '@rn-ui-lib/components/toast'; +import { updateCaseDetailsFromFirestore } from '@reducers/allCasesSlice'; +import { _map } from '@rn-ui-lib/utils/common'; + +interface UpdateCasesProps { + selectedAgent: IReportee | null; + isVisitPlanLocked?: boolean; + querySnapshot?: FirebaseFirestoreTypes.QuerySnapshot; + caseUpdateList?: Array<{ updateType: string; updatedCaseDetail: CaseDetail }>; +} + +export const updateCases = (props: UpdateCasesProps) => (dispatch: AppDispatch) => { + const { + selectedAgent, + isVisitPlanLocked = false, + querySnapshot, + caseUpdateList, + } = props; + const caseDetails = store?.getState()?.allCases?.caseDetails || {}; + const casesList = store?.getState()?.allCases?.casesList || []; + const loading = store?.getState()?.allCases?.loading || false; + const isInitialLoad = !casesList.length; + let newlyAddedCases = 0; + let deletedCases = 0; + let updatedCaseDetails = { ...caseDetails }; + const newVisitCaseLoanIds: string[] = []; + const removedVisitedCasesLoanIds: string[] = []; + const newVisitCollectionCases: string[] = []; + let updatedLoading = loading; + + const getCaseListComponents = (caseDetails: Record) => { + const pendingList: ICaseItem[] = []; + const completedList: ICaseItem[] = []; + const pinnedList: ICaseItem[] = []; + const updatedCasesList: ICaseItem[] = []; + _map(caseDetails, (caseReferenceId: string) => { + if (!caseDetails?.[caseReferenceId]) { + return; + } + const { caseStatus, pinRank, caseViewCreatedAt } = caseDetails?.[caseReferenceId]; + const isCaseCompleted = COMPLETED_STATUSES.includes(caseStatus); + const caseItem = { caseReferenceId, pinRank, caseViewCreatedAt } as ICaseItem; + updatedCasesList.push(caseItem); + isCaseCompleted + ? completedList.push(caseItem) + : pinRank + ? pinnedList.push(caseItem) + : pendingList.push(caseItem); + }); + return { pendingList, completedList, pinnedList, updatedCasesList }; + }; + + const handleCaseAddition = (newCaseDetail: CaseDetail) => { + const { caseReferenceId, id, pinRank } = newCaseDetail || {}; + const caseId = caseReferenceId || id; + if (caseDetails?.[caseId]) { + return; + } + newlyAddedCases++; + const newVisitCollectionCases: string[] = []; + if (pinRank) { + newVisitCollectionCases.push(caseId); + } + const imageUri = + findDocumentByDocumentType(newCaseDetail.documents, DOCUMENT_TYPE.OPTIMIZED_SELFIE)?.uri || + ''; + const newCaseDetails = { + ...newCaseDetail, + isSynced: true, + isNewlyAdded: !isInitialLoad, + imageUri, + }; + updatedCaseDetails = { ...updatedCaseDetails, [caseId]: newCaseDetails }; + }; + + const handleCaseModification = (newCaseDetail: CaseDetail) => { + const { caseReferenceId, id, pinRank, caseViewCreatedAt } = newCaseDetail || {}; + const caseId = caseReferenceId || id; + if (!updatedCaseDetails?.[caseId]) { + return; + } + const caseDetail = updatedCaseDetails[caseId]; + if (pinRank && !caseDetail?.pinRank) { + // this is a new visit case + newVisitCaseLoanIds.push(caseDetails[caseId]?.loanAccountNumber); + newVisitCollectionCases.push(caseId); + } + if (!pinRank && caseDetail?.pinRank) { + // this is a removed visit case + removedVisitedCasesLoanIds.push(caseDetails[caseId]?.loanAccountNumber); + } + const newCaseDetails = { + ...newCaseDetail, + isSynced: true, + imageUri: caseDetail?.imageUri, + }; + updatedCaseDetails = { ...updatedCaseDetails, [caseId]: newCaseDetails }; + }; + + const handleCaseRemoval = (newCaseDetail: CaseDetail) => { + const { caseReferenceId, id } = newCaseDetail || {}; + const caseId = caseReferenceId || id; + if (!updatedCaseDetails?.[caseId]) { + return; + } + const currentScreen = getCurrentScreen(); + // Redirect to home screen if the case deletes which the agent is seeing + if (currentScreen?.name === 'caseDetail') { + const { caseId: id } = currentScreen.params; + if (id === caseId) { + navigateToScreen('Home'); + } + } + delete updatedCaseDetails[caseId]; + deletedCases++; + }; + + const updateCaseDetails = (newCaseDetail: CaseDetail, updateType: string) => { + switch (updateType) { + case FirestoreUpdateTypes.ADDED: + handleCaseAddition(newCaseDetail); + break; + case FirestoreUpdateTypes.MODIFIED: + handleCaseModification(newCaseDetail); + break; + case FirestoreUpdateTypes.REMOVED: + handleCaseRemoval(newCaseDetail); + break; + default: + } + }; + + const showCaseUpdationToast = (newlyAddedCases: number, deletedCases: number) => { + let toastConfig: any = null; + const addedCasesText = newlyAddedCases + ? `${newlyAddedCases} new case${newlyAddedCases > 1 ? 's' : ''} allocated` + : ''; + const deletedCasesText = deletedCases + ? `${deletedCases} case${deletedCases > 1 ? 's' : ''} de-allocated` + : ''; + if (newlyAddedCases && deletedCases) { + toastConfig = { + type: 'info', + text1: `${addedCasesText} & ${deletedCasesText}`, + }; + } else if (newlyAddedCases) { + toastConfig = { type: 'success', text1: addedCasesText }; + } else if (deletedCases) { + toastConfig = { type: 'error', text1: deletedCasesText }; + } + if (toastConfig) { + toast(toastConfig); + } + }; + + const processCases = async () => { + if (caseUpdateList?.length) { + // If we are providing cases list directly + caseUpdateList.forEach((caseUpdate) => { + const { updateType, updatedCaseDetail } = caseUpdate; + updateCaseDetails(updatedCaseDetail, updateType); + }); + } else { + // If we are providing querySnapshot from firestore subscription + await querySnapshot + ?.docChanges() + ?.forEach((documentSnapshot: FirebaseFirestoreTypes.DocumentChange) => { + InteractionManager.runAfterInteractions(() => { + const updateType = documentSnapshot.type; + const updatedCaseDetail = documentSnapshot.doc.data() as CaseDetail; + updateCaseDetails(updatedCaseDetail, updateType); + }); + }); + } + const { pendingList, completedList, pinnedList, updatedCasesList } = + getCaseListComponents(updatedCaseDetails); + if (updatedLoading) { + if (selectedAgent && selectedAgent.referenceId !== MY_CASE_ITEM.referenceId) { + addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_AGENT_CASE_LOAD_SUCCESS, { + selectedAgent: selectedAgent.referenceId, + }); + } + updatedLoading = false; + } + + if (newVisitCaseLoanIds?.length > 0) { + addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_VISIT_PLAN_UPDATED, { + newPinCases: [...newVisitCaseLoanIds], + currentPinCases: pinnedList.map((item) => + getLoanAccountNumber(updatedCaseDetails[item?.caseReferenceId]) + ), + }); + if (!isVisitPlanLocked) { + toast({ + type: 'info', + text1: `${newVisitCaseLoanIds.length} case${ + newVisitCaseLoanIds.length > 1 ? 's' : '' + } added to the visit plan`, + }); + } + } + if (removedVisitedCasesLoanIds.length > 0) { + addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_VISIT_PLAN_UPDATED, { + newUnpinCases: [...removedVisitedCasesLoanIds], + currentPinCases: pinnedList.map((item) => + getLoanAccountNumber(updatedCaseDetails[item?.caseReferenceId]) + ), + }); + if (!isVisitPlanLocked) { + toast({ + type: 'info', + text1: `${removedVisitedCasesLoanIds.length} case${ + removedVisitedCasesLoanIds.length > 1 ? 's' : '' + } removed from the visit plan`, + }); + } + } + + dispatch( + updateCaseDetailsFromFirestore({ + updatedCasesList, + updatedCaseDetails, + updatedLoading, + pendingList, + completedList, + pinnedList, + }) + ); + }; + requestAnimationFrame(() => { + InteractionManager.runAfterInteractions(async () => { + await processCases(); + !isInitialLoad && showCaseUpdationToast(newlyAddedCases, deletedCases); + addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_SNAPSHOT_LISTENER, { + snapshot_path: 'handleCasesUpdate', + }); + }); + }); +}; diff --git a/src/services/firebaseFetchAndUpdate.service.ts b/src/services/firebaseFetchAndUpdate.service.ts index 32e010d2..8033f5de 100644 --- a/src/services/firebaseFetchAndUpdate.service.ts +++ b/src/services/firebaseFetchAndUpdate.service.ts @@ -3,7 +3,6 @@ import { setActivityTimeOnApp, setActivityTimeWindowHigh, setActivityTimeWindowMedium, - setEnableFirestoreResync, setFirestoreResyncIntervalInMinutes, setAudioUploadJobIntervalInMinutes, setCalendarAndAccountsUploadJobIntervalInMinutes, @@ -40,9 +39,6 @@ async function fetchUpdatedRemoteConfig() { .getValue('ACTIVITY_TIME_WINDOW_MEDIUM') .asNumber(); const BLACKLISTED_APPS = remoteConfig().getValue('BLACKLISTED_APPS').asString(); - const ENABLE_FIRESTORE_RESYNC = remoteConfig() - .getValue('ENABLE_FIRESTORE_RESYNC') - .asBoolean(); const FIRESTORE_RESYNC_INTERVAL_IN_MINUTES = remoteConfig() .getValue('FIRESTORE_RESYNC_INTERVAL_IN_MINUTES') .asNumber(); @@ -68,7 +64,6 @@ async function fetchUpdatedRemoteConfig() { setActivityTimeWindowHigh(ACTIVITY_TIME_WINDOW_HIGH); setActivityTimeWindowMedium(ACTIVITY_TIME_WINDOW_MEDIUM); setBlacklistedAppsList(BLACKLISTED_APPS); - setEnableFirestoreResync(ENABLE_FIRESTORE_RESYNC); setFirestoreResyncIntervalInMinutes(FIRESTORE_RESYNC_INTERVAL_IN_MINUTES); if(DATA_SYNC_JOB_INTERVAL_IN_MINUTES) setDataSyncJobIntervalInMinutes(DATA_SYNC_JOB_INTERVAL_IN_MINUTES); diff --git a/src/services/firestoreService.js b/src/services/firestoreService.js new file mode 100644 index 00000000..919aa889 --- /dev/null +++ b/src/services/firestoreService.js @@ -0,0 +1,69 @@ +import firestore from '@react-native-firebase/firestore'; +import auth from '@react-native-firebase/auth'; +import { logError } from '../components/utlis/errorUtils'; + +const subscriptions = new Map(); + +const subscribeToDoc = (collectionPath, successCb) => { + if (subscriptions.has(collectionPath)) { + return; + } + + const unsubscribe = firestore() + .doc(collectionPath) + .onSnapshot(successCb, (error) => { + logError(error, `Error in firetore subscription: ${collectionPath}`); + }); + + subscriptions.set(collectionPath, unsubscribe); +}; + +const subscribeToCollection = (collectionPath, queryFn, successCb) => { + if (subscriptions.has(collectionPath)) { + return; + } + + let ref = firestore().collection(collectionPath); + if (queryFn) { + ref = queryFn(ref); + } + + const unsubscribe = ref.onSnapshot(successCb, (error) => { + logError(error, `Error in firestore subscription: ${collectionPath}`); + }); + + subscriptions.set(collectionPath, unsubscribe); +}; + +const unsubscribeFromPath = (collectionPath) => { + const unsubscribe = subscriptions.get(collectionPath); + if (unsubscribe) { + unsubscribe(); + subscriptions.delete(collectionPath); + } +}; + +const unsubscribeAll = () => { + subscriptions.forEach((unsubscribe, path) => { + unsubscribe(); + }); + subscriptions.clear(); +}; + +const signInUserToFirebase = async (firebaseToken, onSuccess, onError) => { + try { + await auth().signInWithCustomToken(firebaseToken); + onSuccess && onSuccess(); + } catch (error) { + onError && onError(error); + logError(error, 'Error in signInUserToFirebase'); + } +}; + +export const firestoreService = { + subscribeToDoc, + subscribeToCollection, + unsubscribeFromPath, + unsubscribeAll, + signInUserToFirebase, +};