diff --git a/.github/workflows/codePush.yml b/.github/workflows/codePush.yml index 748c5f0c..d1fdea92 100644 --- a/.github/workflows/codePush.yml +++ b/.github/workflows/codePush.yml @@ -3,26 +3,29 @@ name: code-push-cli on: workflow_dispatch: inputs: - environment: - description: Choose build environment - required: true - type: choice - options: - - QA - - Prod - target_versions: - description: please enter target versions - required: true - type: string - default: "2.3.4" - description: - description: Enter please add change log - required: true - type: string - default: "login sso" + environment: + description: Choose build environment + required: true + type: choice + options: + - QA + - Prod + target_versions: + description: please enter target versions + required: true + type: string + default: '2.3.4' + description: + description: Enter please add change log + required: true + type: string + default: 'login sso' jobs: generate: - runs-on: [ default ] + runs-on: [default] + outputs: + package_version: ${{ steps.get_version.outputs.version }} + build_number: ${{ steps.get_package_info.outputs.buildNumber }} steps: - name: Checkout uses: actions/checkout@v2 @@ -33,10 +36,15 @@ jobs: uses: actions/setup-node@v3 with: node-version: 16.x + - name: Get version from package.json + id: get_version + run: | + echo "::set-output name=version::$(node -p "require('./package.json').version")" + echo "::set-output name=buildNumber::$(node -p "require('./package.json').buildNumber")" - name: Install yarn run: npm install --global yarn - name: Install appcenter cli - run: npm install -g appcenter-cli + run: npm install -g appcenter-cli - name: Install dependency run: yarn - name: AppCenter login @@ -47,3 +55,32 @@ jobs: - name: CodePush Prod if: ((github.event.inputs.environment == 'Prod' || inputs.environment == 'Prod')) run: yarn move:prod && appcenter codepush release-react -a nfa-navi.com/nfa-app -d Production -t "${{github.event.inputs.target_versions}}" --description "${{github.event.inputs.description}}" + + create_release_tag: + needs: generate + runs-on: ubuntu-latest + if: success() && (github.event.inputs.environment == 'Prod') # Only create tag for Prod releases, have put QA for testing + steps: + - name: Checkout code + uses: actions/checkout@v2 + + - name: Create release tag + run: | + TAG_NAME="${{ needs.generate.outputs.package_version }}" + RELEASE_NAME="$TAG_NAME (build ${{ needs.generate.outputs.build_number }})" + DESCRIPTION="${{ github.event.inputs.description }}" + + # Create a release tag using the GitHub API + curl \ + -X POST \ + -H "Authorization: token ${{ secrets.MY_REPO_PAT }}" \ + -H "Content-Type: application/json" \ + -d '{ + "tag_name": "'$TAG_NAME'", + "target_commitish": "main", + "name": "'$RELEASE_NAME'", + "body": "'$DESCRIPTION'", + "draft": false, + "prerelease": false + }' \ + https://api.github.com/repos/${{ github.repository }}/releases diff --git a/android/app/build.gradle b/android/app/build.gradle index e40edbc3..d7838baf 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -134,8 +134,8 @@ def reactNativeArchitectures() { return value ? value.split(",") : ["armeabi-v7a", "x86", "x86_64", "arm64-v8a"] } -def VERSION_CODE = 127 -def VERSION_NAME = "2.7.9" +def VERSION_CODE = 137 +def VERSION_NAME = "2.8.8" android { ndkVersion rootProject.ext.ndkVersion diff --git a/package.json b/package.json index b397175e..8c754163 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "AV_APP", - "version": "2.7.9", - "buildNumber": "127", + "version": "2.8.8", + "buildNumber": "137", "private": true, "scripts": { "android:dev": "yarn move:dev && react-native run-android", diff --git a/src/action/addressGeolocationAction.ts b/src/action/addressGeolocationAction.ts index 07c230a5..e6178fe8 100644 --- a/src/action/addressGeolocationAction.ts +++ b/src/action/addressGeolocationAction.ts @@ -20,6 +20,10 @@ export const getAddressesGeolocation = .post(url, payload) .then((response) => { if (response?.data) { + addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_GEOLOCATION_LOADED, { + loanAccountNumber: payload.loanAccountNumber, + customerReferenceId: payload.customerReferenceId, + }); dispatch(setAddressGeolocation(response.data)); return; } @@ -56,23 +60,26 @@ export const addAddress = }); }; -export const getUngroupedAddress = (loanAccountNumber: string, infoToGet: UnifiedCaseDetailsTypes[]) => { +export const getUngroupedAddress = ( + loanAccountNumber: string, + infoToGet: UnifiedCaseDetailsTypes[] +) => { const queryParams = { ...initialUrlParams }; - for (const key of infoToGet) { - queryParams[key] = true; - } - addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_UNIFIED_ENTITY_REQUESTED, { - lan: loanAccountNumber || '', - requestedEntities: JSON.stringify(infoToGet || []) || '' - }); - const url = getApiUrl(ApiKeys.CASE_UNIFIED_DETAILS_V4, { loanAccountNumber }, queryParams); - return axiosInstance + for (const key of infoToGet) { + queryParams[key] = true; + } + addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_UNIFIED_ENTITY_REQUESTED, { + lan: loanAccountNumber || '', + requestedEntities: JSON.stringify(infoToGet || []) || '', + }); + const url = getApiUrl(ApiKeys.CASE_UNIFIED_DETAILS_V4, { loanAccountNumber }, queryParams); + return axiosInstance .get(url) .then((response) => { if (response.status === API_STATUS_CODE.OK) { const ungroupedAddressesWithFeedbacks: IUngroupedAddressWithFeedbacks = { - ungroupedAddresses: response?.data?.ungroupedAddresses || [], - ungroupedAddressFeedbacks: response?.data?.addressFeedbacks || [] + ungroupedAddresses: response?.data?.ungroupedAddresses || [], + ungroupedAddressFeedbacks: response?.data?.addressFeedbacks || [], }; return ungroupedAddressesWithFeedbacks; } @@ -83,3 +90,19 @@ export const getUngroupedAddress = (loanAccountNumber: string, infoToGet: Unifie throw new Error(err); }); }; + +export const getSimilarGeolocationTimestamps = (clusterId: string) => { + const url = getApiUrl(ApiKeys.SIMILAR_GEOLOCATION_TIMESTAMPS, { clusterId }); + return axiosInstance + .get(url) + .then((response) => { + if (response.status === API_STATUS_CODE.OK) { + return response.data; + } + throw response; + }) + .catch((err) => { + logError(err); + throw new Error(err); + }); +}; diff --git a/src/action/authActions.ts b/src/action/authActions.ts index 579db69c..9f35b57f 100644 --- a/src/action/authActions.ts +++ b/src/action/authActions.ts @@ -198,9 +198,17 @@ export const handleGoogleLogout = async () => { } }; -export const handleLogout = () => async (dispatch: AppDispatch) => { +const firebaseSignout = async () => { try { await auth().signOut(); + } catch (error) { + logError(error as Error, 'Firebase signout error'); + } +} + +export const handleLogout = () => async (dispatch: AppDispatch) => { + try { + await firebaseSignout(); await handleGoogleLogout(); await clearAllAsyncStorage(); await clearStorageEngine(); diff --git a/src/action/dataActions.ts b/src/action/dataActions.ts index 11043e90..971c2ef3 100644 --- a/src/action/dataActions.ts +++ b/src/action/dataActions.ts @@ -30,7 +30,7 @@ let _signedApiCallBucket: { req: any; added_At: number; callback: GenericFunctio let _signedApiCallBucketTimer: number = 0; const SIGNED_API_BUCKET_SIZE = 10; const SIGNED_API_BUCKET_TIMEOUT = 5 * 1000; -const SUBMIT_FEEDBACK_API_VERSION = 3; +const SUBMIT_FEEDBACK_API_VERSION = 4; export const postPinnedList = (pinnedCases: IPinnedCasesPayload[], updatedCaseList: ICaseItem[], type: string) => diff --git a/src/assets/icons/AddressIcon.tsx b/src/assets/icons/AddressIcon.tsx new file mode 100644 index 00000000..b9e3716a --- /dev/null +++ b/src/assets/icons/AddressIcon.tsx @@ -0,0 +1,31 @@ +import * as React from "react" +import Svg, { Rect, Path } from "react-native-svg" + +function AddressIcon() { + return ( + + + + + + + ) +} + +export default AddressIcon diff --git a/src/assets/icons/CallbackNotificationIcon.tsx b/src/assets/icons/CallbackNotificationIcon.tsx new file mode 100644 index 00000000..ef5c1711 --- /dev/null +++ b/src/assets/icons/CallbackNotificationIcon.tsx @@ -0,0 +1,44 @@ +import * as React from 'react'; +import Svg, { Defs, ClipPath, Path, Image, Mask, G, Use } from 'react-native-svg'; + +function CallbackNotificationIcon() { + return ( + + + + + + + + + + + + + + + + + + + ); +} + +export default CallbackNotificationIcon; diff --git a/src/common/BlockerScreen.tsx b/src/common/BlockerScreen.tsx index 67bdc2f8..fb10f4d4 100644 --- a/src/common/BlockerScreen.tsx +++ b/src/common/BlockerScreen.tsx @@ -36,7 +36,6 @@ const BlockerScreen = (props: IBlockerScreen) => { ); const { isWifiOrCellularOn, appState } = useAppSelector((state) => state.metadata); - const [shouldUpdate, setShouldUpdate] = useState(); const [showActionBtnLoader, setShowActionBtnLoader] = useState(false); @@ -72,15 +71,14 @@ const BlockerScreen = (props: IBlockerScreen) => { setForceReinstallData(undefined); }, [JSON.stringify(forceUninstallData || {})]); - React.useEffect(() => { if (!appState) return; const buildToCompare = GLOBAL.BUILD_FLAVOUR; if (!buildToCompare) return; let flavorToUpdate: IAppState; - if(GLOBAL.BUILD_FLAVOUR.includes('fieldAgents')){ + if (GLOBAL.BUILD_FLAVOUR.includes('fieldAgents')) { flavorToUpdate = appState?.fieldAgent; - }else { + } else { flavorToUpdate = appState?.telecallingAgents; } @@ -110,6 +108,12 @@ const BlockerScreen = (props: IBlockerScreen) => { return () => appStateChange.remove(); }, []); + React.useEffect(() => { + if (!isWifiOrCellularOn) { + addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_INTERNET_BLOCKER_SCREEN_LOAD); + } + }, [isWifiOrCellularOn]); + const handleDownloadNewApp = () => { if (forceReinstallData?.reinstall_endpoint) { openApkDownloadLink(forceReinstallData?.reinstall_endpoint); @@ -122,9 +126,9 @@ const BlockerScreen = (props: IBlockerScreen) => { const handleAppUpdate = () => { let appUpdateUrl; - if(GLOBAL.BUILD_FLAVOUR.includes(BUILD_FOR_FIELD)){ + if (GLOBAL.BUILD_FLAVOUR.includes(BUILD_FOR_FIELD)) { appUpdateUrl = appState?.fieldAgent.currentProdAPK; - }else { + } else { appUpdateUrl = appState?.telecallingAgents.currentProdAPK; } if (appUpdateUrl) { @@ -143,7 +147,6 @@ const BlockerScreen = (props: IBlockerScreen) => { } }; - if (shouldUpdate) { const { heading, instructions } = BLOCKER_SCREEN_DATA.UNINSTALL_APP; return ( diff --git a/src/common/Constants.ts b/src/common/Constants.ts index cc503bcb..cdf55e9d 100644 --- a/src/common/Constants.ts +++ b/src/common/Constants.ts @@ -776,6 +776,49 @@ export const CLICKSTREAM_EVENT_NAMES = { name: 'FA_FEEDBACK_API_CALLED', description: 'FA_FEEDBACK_API_CALLED', }, + + //ADDRESSES AND GEOLOCATIONS + FA_GEOLOCATION_TAB_CLICKED: { + name: 'FA_GEOLOCATION_TAB_CLICKED', + description: 'FA_GEOLOCATION_TAB_CLICKED', + }, + FA_ADDRESS_TAB_CLICKED: { + name: 'FA_ADDRESS_TAB_CLICKED', + description: 'FA_ADDRESS_TAB_CLICKED', + }, + FA_GEOLOCATION_LOADED: { + name: 'FA_GEOLOCATION_LOADED', + description: 'FA_GEOLOCATION_LOADED', + }, + FA_GEOLOCATION_TIMESTAMP_CLICKED: { + name: 'FA_GEOLOCATION_TIMESTAMP_CLICKED', + description: 'FA_GEOLOCATION_TIMESTAMP_CLICKED', + }, + FA_GEOLOCATION_TIMESTAMP_LOADED: { + name: 'FA_GEOLOCATION_TIMESTAMP_LOADED', + description: 'FA_GEOLOCATION_TIMESTAMP', + }, + FA_GEOLOCATION_ADDITIONAL_GL_CLICKED: { + name: 'FA_GEOLOCATION_ADDITIONAL_GL_CLICKED', + description: 'FA_GEOLOCATION_ADDITIONAL_GL_CLICKED', + }, + FA_GEOLOCATION_ADDITIONAL_GL_LOADED: { + name: 'FA_GEOLOCATION_ADDITIONAL_GL_LOADED', + description: 'FA_GEOLOCATION_ADDITIONAL_GL_LOADED', + }, + FA_ADDRESS_OLD_FEEDBACK_CLICKED: { + name: 'FA_ADDRESS_OLD_FEEDBACK_CLICKED', + description: 'FA_ADDRESS_OLD_FEEDBACK_CLICKED', + }, + FA_ADDRESS_ADD_FEEDBACK_CLICKED: { + name: 'FA_ADDRESS_ADD_FEEDBACK_CLICKED', + description: 'FA_ADDRESS_ADD_FEEDBACK_CLICKED', + }, + FA_INTERNET_BLOCKER_SCREEN_LOAD: { + name: 'FA_INTERNET_BLOCKER_SCREEN_LOAD', + description: 'FA_INTERNET_BLOCKER_SCREEN_LOAD', + } + } as const; export enum MimeType { @@ -892,3 +935,12 @@ export const REQUEST_TO_UNBLOCK_FOR_IMPERSONATION = [ ]; export const NAVI_AGENCY_CODE = '1000'; + +export const HIT_SLOP = { + top: 8, + bottom: 8, + left: 8, + right: 8, +}; + +export const LITMUS_URL = 'https://longhorn.navi.com/litmus'; diff --git a/src/common/TrackingComponent.tsx b/src/common/TrackingComponent.tsx index 334da4ad..e6191ffb 100644 --- a/src/common/TrackingComponent.tsx +++ b/src/common/TrackingComponent.tsx @@ -1,4 +1,4 @@ -import { type ReactNode, useEffect, useRef, useCallback } from 'react'; +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'; @@ -11,13 +11,13 @@ import CosmosForegroundService, { } from '../services/foregroundServices/foreground.service'; import useIsOnline from '../hooks/useIsOnline'; import { - type IGeolocationPayload, + IGeolocationPayload, getSyncTime, + sendCurrentGeolocationAndBuffer, 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 { setIsTimeSynced } from '../reducer/foregroundServiceSlice'; import { logError } from '../components/utlis/errorUtils'; import { useAppDispatch, useAppSelector } from '../hooks'; import { dataSyncService } from '../services/dataSync.service'; @@ -33,7 +33,7 @@ import { } 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 { 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'; @@ -42,13 +42,19 @@ import { AgentActivity } from '../types/agentActivity'; import { getActivityTimeOnApp, getActivityTimeWindowMedium, - getActivityTimeWindowHigh, getEnableFirestoreResync, getFirestoreResyncIntervalInMinutes, + getActivityTimeWindowHigh, + getEnableFirestoreResync, } from './AgentActivityConfigurableConstants'; import { GlobalImageMap } from './CachedImage'; import { addClickstreamEvent } from '../services/clickstreamEventService'; import { CLICKSTREAM_EVENT_NAMES } from './Constants'; -import useResyncFirebase from "@hooks/useResyncFirebase"; -import AsyncStorage from "@react-native-async-storage/async-storage"; +import useResyncFirebase from '@hooks/useResyncFirebase'; +import { CaptureGeolocation } from '@components/form/services/geoLocation.service'; +import getLitmusExperimentResult, { + LitmusExperimentName, + LitmusExperimentNameMap, +} from '@services/litmusExperiments.service'; +import { setLitmusExperimentResult } from '@reducers/litmusExperimentSlice'; export enum FOREGROUND_TASKS { GEOLOCATION = 'GEOLOCATION', @@ -59,7 +65,7 @@ export enum FOREGROUND_TASKS { UPDATE_AGENT_ACTIVITY = 'UPDATE_AGENT_ACTIVITY', DELETE_CACHE = 'DELETE_CACHE', FETCH_DATA_FROM_FIREBASE = 'FETCH_DATA_FROM_FIREBASE', - FIREBASE_RESYNC = 'FIREBASE_RESYNC' + FIREBASE_RESYNC = 'FIREBASE_RESYNC', } interface ITrackingComponent { @@ -69,28 +75,66 @@ interface ITrackingComponent { 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 deviceId = useAppSelector((state) => state?.user?.deviceId); - const caseSyncLock = useAppSelector((state) => state?.user?.caseSyncLock); - const lastFirebaseResyncTimestamp = useAppSelector((state) => state?.metadata?.lastFirebaseResyncTimestamp); - const { + isTeamLead, + caseSyncLock, + isTrackingComponentV2Enabled, referenceId, pendingList = [], pinnedList = [], geolocations = [], + deviceId, + userId, } = useAppSelector((state) => ({ + isTeamLead: state.user.isTeamLead, + caseSyncLock: state?.user?.caseSyncLock, + isTrackingComponentV2Enabled: state.litmusExperiment?.isTrackingComponentV2Enabled, referenceId: state.user.user?.referenceId!, pendingList: state.allCases.pendingList, pinnedList: state.allCases.pinnedList, geolocations: state.foregroundService.deviceGeolocationsBuffer, + deviceId: state?.user?.deviceId, + userId: state?.user?.user?.referenceId, })); + 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), + deviceId: deviceId, + }; + dispatch(sendLocationAndActivenessToServer([geolocation])); + } catch (e: any) { + logError(e, 'Error during background location sending.'); + } + }; + + useEffect(() => { + if (!isOnline || isTrackingComponentV2Enabled) { + return; + } + if (geolocations.length > 0) { + dispatch(sendLocationAndActivenessToServer(geolocations, true)); + } + }, [geolocations, isOnline, isTrackingComponentV2Enabled]); + const handleTimeSync = async () => { try { if (!isOnline) { @@ -106,42 +150,6 @@ const TrackingComponent: React.FC = ({ children }) => { } }; - 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), - deviceId : deviceId, - }; - dispatch(setDeviceGeolocationsBuffer(geolocation)); - dispatch(sendLocationAndActivenessToServer([geolocation])); - } catch (e: any) { - logError(e, 'Error during background location sending.'); - } - }; - - useEffect(() => { - if (!isOnline) { - return; - } - if (geolocations.length > 0) { - dispatch(sendLocationAndActivenessToServer(geolocations, true)); - } - }, [geolocations, isOnline]); - - const resyncFirebase = useResyncFirebase(); const handleGetCaseSyncStatus = async () => { @@ -173,9 +181,9 @@ const TrackingComponent: React.FC = ({ children }) => { } if (visitPlanStatus) { dispatch( - setLockData({ - visitPlanStatus, - }) + setLockData({ + visitPlanStatus, + }) ); } dispatch(setCaseSyncLock(false)); @@ -199,18 +207,18 @@ const TrackingComponent: React.FC = ({ children }) => { const stateSetTime = dayJs(stateSetTimestamp); const diffBetweenCurrentTimeAndForegroundTime = - dayJs().diff(foregroundTime, 'seconds') < 0 ? 0 : dayJs().diff(foregroundTime, 'seconds'); + dayJs().diff(foregroundTime, 'seconds') < 0 ? 0 : dayJs().diff(foregroundTime, 'seconds'); const diffBetweenCurrentTimeAndSetStateTime = - dayJs().diff(stateSetTime, 'minutes') < 0 ? 0 : dayJs().diff(stateSetTime, 'minutes'); + 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; + diffBetweenCurrentTimeAndSetStateTime < ACTIVITY_TIME_WINDOW_HIGH; const isStateSetTimeWithinMediumRange = - diffBetweenCurrentTimeAndSetStateTime < ACTIVITY_TIME_WINDOW_MEDIUM; + diffBetweenCurrentTimeAndSetStateTime < ACTIVITY_TIME_WINDOW_MEDIUM; const isForegroundTimeAfterBackground = dayJs(foregroundTimestamp).isAfter(backgroundTimestamp); if (AppState.currentState === AppStates.ACTIVE) { @@ -238,42 +246,42 @@ const TrackingComponent: React.FC = ({ children }) => { 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); - }); + .then((files) => { + for (const file of files) { + const filePath = `${directoryPath}/${file}`; + if (!file.endsWith('jpg') || !file.endsWith('pdf')) { + continue; } - }) - .catch((error) => { - console.error('Error reading directory:', error); - }); + 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 + FIREBASE_FETCH_TIMESTAMP && + currentTimestamp - FIREBASE_FETCH_TIMESTAMP > 15 * MILLISECONDS_IN_A_MINUTE ) { fetchUpdatedRemoteConfig(); } @@ -288,7 +296,9 @@ const TrackingComponent: React.FC = ({ children }) => { }, { taskId: FOREGROUND_TASKS.GEOLOCATION, - task: handleSendGeolocation, + task: isTrackingComponentV2Enabled + ? () => dispatch(sendCurrentGeolocationAndBuffer(appState.current)) + : handleSendGeolocation, delay: 3 * MILLISECONDS_IN_A_MINUTE, // 3 minutes onLoop: true, }, @@ -343,8 +353,8 @@ const TrackingComponent: React.FC = ({ children }) => { const foregroundTime = dayJs(foregroundTimestamp); const backgroundTime = dayJs(backgroundTimestamp); const diffBetweenBackgroundAndForegroundTime = dayJs(backgroundTime).diff( - foregroundTime, - 'seconds' + foregroundTime, + 'seconds' ); const ACTIVITY_TIME_ON_APP = getActivityTimeOnApp(); @@ -354,6 +364,32 @@ const TrackingComponent: React.FC = ({ children }) => { } }; + const handleGeolocationTaskUpdate = async () => { + const trackingComponentExperimentName = LitmusExperimentName.COSMOS_TRACKING_COMPONENT_V2; + const trackingComponentExperiment = await getLitmusExperimentResult( + trackingComponentExperimentName, + { 'x-customer-id': userId } + ); + // If the tracking component experiment is changed, update to new geolocation task + if (trackingComponentExperiment !== isTrackingComponentV2Enabled) { + const updatedGeolocationTask = { + taskId: FOREGROUND_TASKS.GEOLOCATION, + task: trackingComponentExperiment + ? () => dispatch(sendCurrentGeolocationAndBuffer(appState.current)) + : handleSendGeolocation, + delay: 3 * MILLISECONDS_IN_A_MINUTE, + onLoop: true, + }; + CosmosForegroundService.updateTask(updatedGeolocationTask); + dispatch( + setLitmusExperimentResult({ + experimentName: trackingComponentExperimentName, + result: trackingComponentExperiment, + }) + ); + } + }; + const handleAppStateChange = async (nextAppState: AppStateStatus) => { // App comes to foreground from background const now = dayJs().toString(); @@ -361,9 +397,11 @@ const TrackingComponent: React.FC = ({ children }) => { await setItem(StorageKeys.APP_FOREGROUND_TIMESTAMP, now); addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.AV_APP_FOREGROUND, { now }); handleGetCaseSyncStatus(); + handleTimeSync(); dispatch(getConfigData()); CosmosForegroundService.start(tasks); resyncFirebase(); + handleGeolocationTaskUpdate(); } if (nextAppState === AppStates.BACKGROUND) { await setItem(StorageKeys.APP_BACKGROUND_TIMESTAMP, now); @@ -395,6 +433,7 @@ const TrackingComponent: React.FC = ({ children }) => { let appStateSubscription: NativeEventSubscription; appStateSubscription = AppState.addEventListener('change', handleAppStateChange); CosmosForegroundService.start(tasks); + handleGeolocationTaskUpdate(); return () => { appStateSubscription?.remove(); }; @@ -405,4 +444,4 @@ const TrackingComponent: React.FC = ({ children }) => { return <>{children}; }; -export default TrackingComponent; \ No newline at end of file +export default TrackingComponent; diff --git a/src/components/filters/FilterOptions.tsx b/src/components/filters/FilterOptions.tsx index fc0955e2..59af0c6a 100644 --- a/src/components/filters/FilterOptions.tsx +++ b/src/components/filters/FilterOptions.tsx @@ -1,10 +1,11 @@ -import { View } from 'react-native'; +import {StyleSheet, View} from 'react-native'; import React from 'react'; import { IFilters, TFilterOptions } from './Filters'; import RadioGroup from '../../../RN-UI-LIB/src/components/radio_button/RadioGroup'; import RNRadioButton from '../../../RN-UI-LIB/src/components/radio_button/RadioButton'; import { GenericStyles } from '../../../RN-UI-LIB/src/styles'; import Checkbox from '../../../RN-UI-LIB/src/components/chechbox/Checkbox'; +import Text from "@rn-ui-lib/components/Text"; interface IFilterOptions { filters: IFilters; @@ -96,7 +97,7 @@ const FilterOptions: React.FC = ({ orientation="vertical" > {filter.options.map((option) => ( - + ))} ); @@ -119,4 +120,11 @@ const FilterOptions: React.FC = ({ return null; }; +const styles = StyleSheet.create({ + radioOptionSubLabel: { + marginLeft: 35, + marginTop: -10 + } +}); + export default FilterOptions; diff --git a/src/components/filters/Filters.tsx b/src/components/filters/Filters.tsx index 892284ee..903c5370 100644 --- a/src/components/filters/Filters.tsx +++ b/src/components/filters/Filters.tsx @@ -11,6 +11,7 @@ import NavigationHeader from '../../../RN-UI-LIB/src/components/NavigationHeader interface FilterOption { value: string; label: string; + subLabels?: [string]; } interface FilterData { diff --git a/src/components/form/AnswerRender.tsx b/src/components/form/AnswerRender.tsx index 961dd07d..3ddbcf4e 100644 --- a/src/components/form/AnswerRender.tsx +++ b/src/components/form/AnswerRender.tsx @@ -11,6 +11,8 @@ import { getAddressString, getPhoneNumberString, memoize } from '../utlis/common import { getBase64ImageFromOfflineDb } from '../../services/casePayload.transformer'; import { formatAmount } from '../../../RN-UI-LIB/src/utlis/amount'; import GeolocationAddressAnswer from './components/GeolocationAddressAnswer'; +import dayjs from 'dayjs'; +import { BUSINESS_DATE_FORMAT, CUSTOM_ISO_DATE_FORMAT } from '@rn-ui-lib/utils/dates'; const RATING_COMPONENT = 'Rating'; const MAX_RATING = 5; @@ -131,7 +133,9 @@ const AnswerRender: React.FC = (props) => { case AnswerType.phoneNumber: return ; case AnswerType.date: - return ; + return ; + case AnswerType.time: + return ; default: return ; } diff --git a/src/components/form/components/AddressSelection.tsx b/src/components/form/components/AddressSelection.tsx index 11cdabfb..25723745 100644 --- a/src/components/form/components/AddressSelection.tsx +++ b/src/components/form/components/AddressSelection.tsx @@ -1,6 +1,6 @@ -import { View } from 'react-native'; +import { TouchableOpacity, View } from 'react-native'; import React from 'react'; -import { useAppSelector } from '../../../hooks'; +import { useAppDispatch, useAppSelector } from '../../../hooks'; import { CaseAllocationType } from '../../../screens/allCases/interface'; import { validateInput } from '../services/validation.service'; import { Control, Controller } from 'react-hook-form'; @@ -14,6 +14,13 @@ import { AnswerType } from '../interface'; import { Address, GeolocationSource, IGeolocation } from '../../../screens/caseDetails/interface'; import GeolocationAddress from './GeolocationAddress'; import { GenericStyles } from '../../../../RN-UI-LIB/src/styles'; +import { RootState } from '@store'; +import NoLocationsIcon from '@rn-ui-lib/icons/NoLocationIcon'; +import Text from '@rn-ui-lib/components/Text'; +import LineLoader from '@rn-ui-lib/components/suspense_loader/LineLoader'; +import { UnifiedCaseDetailsTypes, getCaseUnifiedData } from '@actions/caseApiActions'; +import LoadingIcon from '@rn-ui-lib/icons/LoadingIcon'; +import { COLORS } from '@rn-ui-lib/colors'; interface IAddressSelection { questionType: string; @@ -40,10 +47,25 @@ export const AddressSourceMap: Record = { const AddressSelection: React.FC = (props) => { const { caseId, questionId, error, widgetId, sectionId, control, questionType } = props; - const currentCase = useAppSelector((state) => state.allCases.caseDetails[caseId]); + const { currentCase, addressGeolocation, isLoading, loanAccountNumber } = useAppSelector( + (state) => { + const currentCase = state.allCases.caseDetails[caseId]; + const loanAccountNumber = currentCase?.loanAccountNumber; + const addresses = state.address?.[loanAccountNumber]; + return { + currentCase, + loanAccountNumber, + addressGeolocation: addresses?.addressesAndGeoLocations || {}, + isLoading: addresses?.isLoading || false, + }; + } + ); const caseType = currentCase?.caseType || CaseAllocationType.ADDRESS_VERIFICATION_CASE; const template = useAppSelector((state) => state.case.templateData[caseType]); const question = template.questions[questionId]; + + const dispatch = useAppDispatch(); + const handleChange = (change: string | null, onChange: (...event: any[]) => void) => { addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.AV_FORM_ELEMENT_CHANGED, { caseId, @@ -62,10 +84,71 @@ const AddressSelection: React.FC = (props) => { const isGeolocation = question?.tag === VisitTypeSelection.GEOLOCATION_SELECTION; - const addresses = (isGeolocation ? currentCase?.geolocations : currentCase?.addresses) || []; + const addresses = + (isGeolocation ? addressGeolocation?.geoLocations : currentCase?.addresses) || []; const controllerName = `widgetContext.${widgetId}.sectionContext.${sectionId}.questionContext.${questionId}`; + const reloadGeolocations = () => { + dispatch( + getCaseUnifiedData([loanAccountNumber], [UnifiedCaseDetailsTypes.ADDRESS_AND_GEOLOCATIONS]) + ); + }; + + if (isGeolocation) { + if (isLoading) { + return ( + + {[...Array(7).keys()].map((_, index) => ( + + ))} + + ); + } + if (!addresses?.length) { + return ( + + + + + No nearby geolocations found + + + + + + + Retry + + + ); + } + } + return ( = (props) => { } return ( = ({ isFeedbackView, handlePageRouting, }) => { - const { latitude, longitude, capturedTimestamp, tag, id, primarySource } = address; + const { + latitude, + longitude, + timestamp, + tag, + id, + primarySource, + pingCount = 0, + clusterLabels = [], + } = address; const { deviceGeolocationCoordinate, prefilledAddressScreenTemplate } = useAppSelector( (state) => ({ deviceGeolocationCoordinate: state.foregroundService?.deviceGeolocationCoordinate, @@ -50,6 +73,7 @@ const GeolocationAddress: React.FC = ({ }) ); const dispatch = useAppDispatch(); + const showSimilarGeolocationsCTA = pingCount > 1 && !isFeedbackView; const relativeDistanceBwLatLong = getDistanceFromLatLonInKm(deviceGeolocationCoordinate, { latitude, @@ -62,16 +86,24 @@ const GeolocationAddress: React.FC = ({ : ''; const isFeedbackPresent = lastFeedbackForGeolocation?.feedbackPresent; + const isDataSutramPrimarySource = primarySource === GeolocationSource.DATA_SUTRAM; - const { addressDate, addressTime } = useMemo(() => { - const timestamp = new Date(Number(capturedTimestamp)); - if (timestamp.toString() === 'Invalid Date') { - return { addressDate: '', addressTime: '', isFeedbackPresent }; + const { addressDate, addressTime, lastFeedbackTimestampDate } = useMemo(() => { + const date = new Date(Number(timestamp)); + const lastFeedbackTimestamp = new Date( + Number(lastFeedbackForGeolocation?.latestFeedbackTimestamp) + ); + if (date.toString() === 'Invalid Date') { + return { addressDate: '', addressTime: '', lastFeedbackTimestampDate: '' }; } - const addressDate = dateFormat(timestamp, 'DD MMM YYYY'); - const addressTime = dateFormat(timestamp, 'hh:mm A'); - return { addressDate, addressTime }; - }, [lastFeedbackForGeolocation, capturedTimestamp]); + const addressDate = dateFormat(date, COSMOS_STANDARD_DATE_FORMAT); + const addressTime = dateFormat(date, COSMOS_STANDARD_TIME_FORMAT); + const lastFeedbackTimestampDate = dateFormat( + lastFeedbackTimestamp, + COSMOS_STANDARD_DATE_FORMAT + ); + return { addressDate, addressTime, lastFeedbackTimestampDate }; + }, [lastFeedbackForGeolocation, timestamp]); const handleCloseRouting = () => handlePageRouting?.(CaseDetailStackEnum.ADDRESS_GEO); @@ -79,7 +111,11 @@ const GeolocationAddress: React.FC = ({ if (!caseId) { return; } - addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_GEOLOCATION_ADD_FEEDBACK_CLICKED); + addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_GEOLOCATION_ADD_FEEDBACK_CLICKED, { + loanAccountNumber, + caseReferenceId: caseId, + geolocationId: id, + }); if (prefilledAddressScreenTemplate) { const addressKey = '{{addressReferenceId}}'; const { visitedWidgets, widgetContext } = getCollectionFeedbackOnAddressPreDefinedJourney( @@ -98,27 +134,33 @@ const GeolocationAddress: React.FC = ({ }) ); if (visitedWidgets?.length) { - _map(visitedWidgets, (visited) => - navigateToScreen(getTemplateRoute(visited, CaseAllocationType.COLLECTION_CASE), { - caseId: caseId, - journey: TaskTitleUIMapping.COLLECTION_FEEDBACK, - handleCloseRouting, - }) - ); - return; + const lastVisitedWidget = visitedWidgets[visitedWidgets.length - 1]; + navigateToScreen(getTemplateRoute(lastVisitedWidget, CaseAllocationType.COLLECTION_CASE), { + caseId: caseId, + journey: TaskTitleUIMapping.COLLECTION_FEEDBACK, + handleCloseRouting, + }); } } }; const openGeolocation = () => { - addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_GEOLOCATION_OPEN_MAP_CLICKED); + addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_GEOLOCATION_OPEN_MAP_CLICKED, { + loanAccountNumber, + caseReferenceId: caseId, + geolocationId: id, + }); const geolocationUrl = getGoogleMapUrl(address?.latitude, address?.longitude); if (!geolocationUrl) return; return Linking.openURL(geolocationUrl); }; const openOldFeedbacks = () => { - addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_GEOLOCATION_OLD_FEEDBACK_CLICKED); + addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_GEOLOCATION_OLD_FEEDBACK_CLICKED, { + loanAccountNumber, + caseReferenceId: caseId, + geolocationId: id, + }); handlePageRouting?.(CaseDetailStackEnum.GEOLOCATION_OLD_FEEDBACKS, { geolocation: address, addressReferenceIds: [address?.id], @@ -127,105 +169,164 @@ const GeolocationAddress: React.FC = ({ }); }; + const handleSimilarLocationCTA = () => { + if (showSimilarGeolocationsCTA) { + addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_GEOLOCATION_TIMESTAMP_CLICKED, { + loanAccountNumber, + caseReferenceId: caseId, + geolocationId: id, + }); + navigateToScreen(CaseDetailStackEnum.SIMILAR_GEOLOCATIONS, { ...address, caseId }); + } + }; + + const Container: React.ElementType = !isFeedbackView ? TouchableOpacity : View; + return ( - - - - {tag} - - {roundoffRelativeDistanceBwLatLong ? ( - ({roundoffRelativeDistanceBwLatLong} km away) - ) : null} - - - {addressDate ? {addressDate} : null} - {addressTime ? ( + + + + + {tag}{' '} + {roundoffRelativeDistanceBwLatLong ? ( + ({roundoffRelativeDistanceBwLatLong} km) + ) : null} + - {addressTime} + {addressDate ? {addressDate} : null} + {addressTime ? ( + + {', '} + {addressTime} + + ) : null} + + {showSimilarGeolocationsCTA ? : null} + + + {!isFeedbackView ? ( + + {isFeedbackPresent ? ( + <> + + Last visit:{' '} + + {' '} + {lastFeedbackForGeolocation?.latestFeedbackStatus || ''}{' '} + + + {lastFeedbackTimestampDate ? ( + + Visited on:{' '} + + {' '} + {lastFeedbackTimestampDate || ''} + + + ) : null} + + ) : ( + + Unvisited location + + )} ) : null} - {(primarySource && primarySource === GeolocationSource.DATA_SUTRAM) ? ( - - - Skip Tracing + + {!isFeedbackView && pingCount && pingCount >= 1 ? ( + + + Similar locations:{' '} + + {pingCount - 1} + + + } + variant={TagVariant.primary} + style={styles.similarLocations} + /> + + {clusterLabels?.length ? ( + <> + + + {clusterLabels.map((label: string) => { + return label ? ( + + ) : null; + })} + + + ) : null} ) : null} - - {!isFeedbackView ? ( - isFeedbackPresent ? ( - - Last visit:{' '} - + {showOpenMap ? ( + + + Open map + + + ) : null} + {showAddFeedback ? ( + - {' '} - {lastFeedbackForGeolocation?.latestFeedbackStatus || ''}{' '} - - - ) : ( - - Last visit:{' '} - - Unvisited location - - - ) - ) : null} - - {showOpenMap ? ( - - - Open map - - - ) : null} - {showAddFeedback ? ( - - - Add feedback - - - ) : null} - {showOldFeedbacks && isFeedbackPresent ? ( - - - Old feedbacks - - - ) : ( - - )} - + + Add feedback + + + ) : null} + {showOldFeedbacks && isFeedbackPresent ? ( + + + Old feedbacks + + + ) : ( + + )} + + ); }; -export default GeolocationAddress; - const styles = StyleSheet.create({ circleSeparator: { width: 4, height: 4, borderRadius: 2, backgroundColor: COLORS.BACKGROUND.GRAY_B1, - marginHorizontal: 8, + marginHorizontal: 4, marginTop: 2, }, openMapBtn: { @@ -239,4 +340,26 @@ const styles = StyleSheet.create({ fontSize: 12, fontWeight: 'bold', }, + lastFeedbackTimestampDate: { + lineHeight: 18, + fontWeight: '700', + }, + similarLocations: { + marginVertical: 2, + marginRight: 2, + }, + verticalLine: { + height: 17, + width: 1, + backgroundColor: COLORS.BORDER.PRIMARY, + margin: 4, + }, + fb24: { + flexBasis: '24%', + }, + tagText: { + lineHeight: 18, + }, }); + +export default GeolocationAddress; diff --git a/src/components/form/components/GeolocationAddressAnswer.tsx b/src/components/form/components/GeolocationAddressAnswer.tsx index e4970886..cf45dbc4 100644 --- a/src/components/form/components/GeolocationAddressAnswer.tsx +++ b/src/components/form/components/GeolocationAddressAnswer.tsx @@ -4,6 +4,7 @@ import { useAppSelector } from '../../../hooks'; import { VisitTypeSelection } from './AddressSelection'; import GeolocationAddress from './GeolocationAddress'; import Text from '../../../../RN-UI-LIB/src/components/Text'; +import { RootState } from '@store'; interface IGeolocationAddressAnswer { caseId: string; @@ -17,6 +18,11 @@ const GeolocationAddressAnswer: React.FC = ({ addressId, }) => { const currentCase = useAppSelector((state) => state.allCases.caseDetails[caseId]); + const loanAccountNumber = currentCase?.loanAccountNumber; + const { addressGeolocation } = useAppSelector((state: RootState) => ({ + addressGeolocation: state.address?.[loanAccountNumber]?.addressesAndGeoLocations || {}, + isLoading: state.address?.[loanAccountNumber]?.isLoading || false, + })); const isGeolocation = metadata?.type === VisitTypeSelection.GEOLOCATION_SELECTION; const getAddressFromId = memoize((id: string) => { @@ -24,10 +30,11 @@ const GeolocationAddressAnswer: React.FC = ({ }); if (isGeolocation) { - if (currentCase?.geolocations) { - const index = currentCase?.geolocations?.findIndex((a) => a.id === addressId); + const geolocations = addressGeolocation?.geoLocations; + if (geolocations?.length) { + const index = geolocations?.findIndex((a) => a.id === addressId); if (index !== -1) { - const address = currentCase?.geolocations?.[index]; + const address = geolocations?.[index]; return ; } } diff --git a/src/components/form/components/TimeInput.tsx b/src/components/form/components/TimeInput.tsx index 03b991e1..2938a991 100644 --- a/src/components/form/components/TimeInput.tsx +++ b/src/components/form/components/TimeInput.tsx @@ -55,7 +55,7 @@ const TimeInput: React.FC = (props) => { const outputTime = convertTo24HourFormat(text); onChange({ answer: outputTime, - type: AnswerType.date, + type: AnswerType.time, }); }; diff --git a/src/components/form/index.tsx b/src/components/form/index.tsx index 3d3496ca..0ffba372 100644 --- a/src/components/form/index.tsx +++ b/src/components/form/index.tsx @@ -5,7 +5,6 @@ import Geolocation from 'react-native-geolocation-service'; import { SafeAreaView } from 'react-native-safe-area-context'; import Button from '../../../RN-UI-LIB/src/components/Button'; import Heading from '../../../RN-UI-LIB/src/components/Heading'; -import Text from '../../../RN-UI-LIB/src/components/Text'; import ArrowSolidIcon from '../../../RN-UI-LIB/src/Icons/ArrowSolidIcon'; import { GenericStyles } from '../../../RN-UI-LIB/src/styles'; import { COLORS } from '../../../RN-UI-LIB/src/styles/colors'; @@ -47,6 +46,7 @@ import NudgeSuspiciousFeedbackBottomSheet from './NudgeSuspiciousFeedbackBottomS import { API_STATUS_CODE } from '../utlis/apiHelper'; import NavigationHeader, { Icon } from '../../../RN-UI-LIB/src/components/NavigationHeader'; import { CaseDetailStackEnum } from '@screens/caseDetails/CaseDetailStack'; +import { useNavigation, useRoute } from '@react-navigation/native'; interface IWidget { route: { @@ -59,6 +59,10 @@ interface IWidget { }; } +enum NavigationActions { + GO_BACK = 'GO_BACK', +} + const Widget: React.FC = (props) => { const [isJourneyFirstScreen, setIsJourneyFirstScreen] = useState(true); const [showNudgeBottomSheet, setNudgeBottomSheet] = useState(false); @@ -67,30 +71,28 @@ const Widget: React.FC = (props) => { const { params } = props.route; const { caseId, journey, handleCloseRouting } = params; const caseKey = useRef(''); - const { - caseType, - templateData, - caseData, - dataToBeValidated, - docsToBeUploaded, - intermediateDocsToBeUploaded, - } = useAppSelector((state) => { - const caseType = - state.allCases.caseDetails[caseId]?.caseType || CaseAllocationType.ADDRESS_VERIFICATION_CASE; - return { - caseType, - templateData: state.case.templateData[caseType], - caseData: state.allCases.caseDetails[caseId], - dataToBeValidated: state.case.caseForm?.[caseId]?.[journey], - docsToBeUploaded: state.feedbackImages.docsToBeUploaded, - intermediateDocsToBeUploaded: state.feedbackImages.intermediateDocsToBeUploaded, - }; - }); + const disableFormInteractionUpdate = useRef(false); + const { caseType, templateData, caseData, dataToBeValidated, intermediateDocsToBeUploaded } = + useAppSelector((state) => { + const caseType = + state.allCases.caseDetails[caseId]?.caseType || + CaseAllocationType.ADDRESS_VERIFICATION_CASE; + return { + caseType, + templateData: state.case.templateData[caseType], + caseData: state.allCases.caseDetails[caseId], + dataToBeValidated: state.case.caseForm?.[caseId]?.[journey], + docsToBeUploaded: state.feedbackImages.docsToBeUploaded, + intermediateDocsToBeUploaded: state.feedbackImages.intermediateDocsToBeUploaded, + }; + }); const name = getWidgetNameFromRoute(props.route.name, caseType); const { sections, conditionActions: widgetConditionActions, isLeaf } = templateData.widget[name]; const sectionMap = templateData.sections; const [error, setError] = useState(); const dispatch = useAppDispatch(); + const navigation = useNavigation(); + const route = useRoute(); useEffect(() => { let isFirst = false; for (const journey of Object.values(templateData.journey)) { @@ -125,6 +127,49 @@ const Widget: React.FC = (props) => { return () => subscription.unsubscribe(); }, [watch]); + useEffect(() => { + const beforeRemoveListener = navigation.addListener('beforeRemove', (e) => { + // If leaf widget, do nothing + if (isLeaf && disableFormInteractionUpdate.current) { + return; + } + + // Delete interaction on going back the stack + if (e.data.action.type === NavigationActions.GO_BACK) { + dispatch( + deleteInteraction({ + caseId, + journeyId: journey, + widgetId: name, + }) + ); + return; + } + + const numberOfRoutes = navigation.getState().routes.length; + const lastScreen = navigation.getState().routes[numberOfRoutes - 1]; + + const isLastScreen = lastScreen.name === route.name; + + // Update form interaction when the last screen is about to be unmounted + if (isLastScreen) { + const submitHandler = handleSubmit((answer) => { + dispatch( + updateInteraction({ + caseId, + journeyId: journey, + widgetId: name, + answer, + }) + ); + }); + submitHandler(); + } + }); + + return beforeRemoveListener; + }, [navigation, route]); + const onSubmit = (data: any) => { addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.AV_FORM_NEXT_BUTTON_CLICKED, { caseId, @@ -221,6 +266,7 @@ const Widget: React.FC = (props) => { }; const handleSubmitJourney = async (data: any, coords: Geolocation.GeoCoordinates) => { + disableFormInteractionUpdate.current = true; dispatch( updateInteraction({ caseId, @@ -371,13 +417,6 @@ const Widget: React.FC = (props) => { journeyId: journey, widgetId: name, }); - dispatch( - deleteInteraction({ - caseId, - journeyId: journey, - widgetId: name, - }) - ); goBack(); }; @@ -398,9 +437,6 @@ const Widget: React.FC = (props) => { Add feedback for {caseData?.customerInfo?.customerName || caseData?.customerName} } - subTitle={ - isJourneyNameExists ? {templateData?.journey?.[journey].name} : null - } icon={Icon.close} onBack={handleCloseIconPress} /> @@ -444,17 +480,15 @@ const Widget: React.FC = (props) => { styles.borderTop, ]} > - {!isJourneyFirstScreen && ( -