diff --git a/.github/workflows/newBuild.yml b/.github/workflows/newBuild.yml index ddc37bbe..b1d8b8e3 100644 --- a/.github/workflows/newBuild.yml +++ b/.github/workflows/newBuild.yml @@ -42,6 +42,12 @@ jobs: with: token: ${{ secrets.MY_REPO_PAT }} submodules: recursive + - name: update codepush key QA + if: (github.event.inputs.environment == 'QA' || inputs.environment == 'QA') + run: sed -i "s/pastethekeyhere/${{ secrets.CODEPUSH_QA_KEY }}/" android/app/src/main/res/values/strings.xml && cat android/app/src/main/res/values/strings.xml + - name: update codepush key PROD + if: (github.event.inputs.environment == 'Prod' || inputs.environment == 'Prod') + run: sed -i "s/pastethekeyhere/${{ secrets.CODEPUSH_PROD_KEY }}/" android/app/src/main/res/values/strings.xml && cat android/app/src/main/res/values/strings.xml - name: Generate keystore if: (github.event.inputs.type == 'release' || inputs.type == 'release') run: echo "${{ secrets.KEY_STORE }}" > keystore.asc && gpg -d --passphrase "${{ secrets.PASSPHARASE }}" --batch keystore.asc > android/app/my-upload-key.keystore diff --git a/App.tsx b/App.tsx index 5539ee6b..29db2845 100644 --- a/App.tsx +++ b/App.tsx @@ -1,4 +1,4 @@ -import React, { useState } from 'react'; +import React, { useEffect, useState } from 'react'; import { AppState, LogBox, @@ -15,7 +15,7 @@ import * as Sentry from '@sentry/react-native'; import codePush from 'react-native-code-push'; import AsyncStorage from '@react-native-async-storage/async-storage'; import CodePush from 'react-native-code-push'; -import store, { persistor } from './src/store/store'; +import store, { persistor, RootState } from './src/store/store'; import { navigationRef } from './src/components/utlis/navigationUtlis'; import FullScreenLoader from './RN-UI-LIB/src/components/FullScreenLoader'; @@ -42,8 +42,9 @@ import { StorageKeys } from './src/types/storageKeys'; import dayJs from 'dayjs'; import { GlobalImageMap, hydrateGlobalImageMap } from './src/common/CachedImage'; import analytics from '@react-native-firebase/analytics'; -import handleUpdatedConfigureValuesFromFirebase from './src/services/firebaseFetchAndUpdate.service'; +import fetchUpdatedRemoteConfig from './src/services/firebaseFetchAndUpdate.service'; import { addClickstreamEvent } from './src/services/clickstreamEventService'; +import ScreenshotBlocker from './src/components/utlis/ScreenshotBlocker'; initSentry(); @@ -124,7 +125,12 @@ function App() { active: true, }); + useEffect(() => { + ScreenshotBlocker.unblockScreenshots(); + }, []); + React.useEffect(() => { + fetchUpdatedRemoteConfig(); askForPermissions(); const appStateChange = AppState.addEventListener('change', async (change) => { handleAppStateChange(change); @@ -139,7 +145,6 @@ function App() { setIsGlobalDocumentMapLoaded(true); })(); checkCodePushAndSync(); - handleUpdatedConfigureValuesFromFirebase(); setForegroundTimeStampAndClickstream(); return () => { diff --git a/RN-UI-LIB b/RN-UI-LIB index 9f4a3ae2..64db024f 160000 --- a/RN-UI-LIB +++ b/RN-UI-LIB @@ -1 +1 @@ -Subproject commit 9f4a3ae2675e913bbbbe3326d49c52ab987b3339 +Subproject commit 64db024f2d7de7ccdec44a2c18f3e6df1ffce8e0 diff --git a/android/app/build.gradle b/android/app/build.gradle index 0b4a2be9..ab90a5e0 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -131,8 +131,8 @@ def reactNativeArchitectures() { return value ? value.split(",") : ["armeabi-v7a", "x86", "x86_64", "arm64-v8a"] } -def VERSION_CODE = 93 -def VERSION_NAME = "2.4.9" +def VERSION_CODE = 96 +def VERSION_NAME = "2.5.1" android { ndkVersion rootProject.ext.ndkVersion diff --git a/android/app/src/main/java/com/avapp/DeviceUtilsModule.java b/android/app/src/main/java/com/avapp/DeviceUtilsModule.java index 478a84f3..c91bb2da 100644 --- a/android/app/src/main/java/com/avapp/DeviceUtilsModule.java +++ b/android/app/src/main/java/com/avapp/DeviceUtilsModule.java @@ -100,6 +100,20 @@ public class DeviceUtilsModule extends ReactContextBaseJavaModule { } } + public String getAppIcon(String packageName) { + try { + Context context = RNContext.getApplicationContext(); + PackageManager pm = context.getPackageManager(); + ApplicationInfo appInfo = pm.getApplicationInfo(packageName, PackageManager.GET_META_DATA); + + Uri imageUri = Uri.parse("android.resource://" + appInfo.packageName + "/drawable/" + appInfo.icon); + return imageUri.toString(); + + } catch (Exception e) { + return null; + } + } + @ReactMethod public void getAllInstalledApp(Promise promise) { try { @@ -108,13 +122,26 @@ public class DeviceUtilsModule extends ReactContextBaseJavaModule { List packages = packageManager.getInstalledApplications(PackageManager.GET_META_DATA); JSONArray jsonArray = new JSONArray(); for (PackageInfo packageInfo : installedPackages) { + + final PackageManager pm = RNContext.getApplicationContext().getPackageManager(); + ApplicationInfo appsInstalled; + try { + appsInstalled = pm.getApplicationInfo( packageInfo.packageName, 0); + } catch (final PackageManager.NameNotFoundException e) { + appsInstalled = null; + } + final String applicationName = (String) (appsInstalled != null ? pm.getApplicationLabel(appsInstalled) : "(unknown)"); + JSONObject mainObject = new JSONObject(); - JSONObject appObject = new JSONObject(); - appObject.put("appName", packageInfo.applicationInfo.processName); - appObject.put("firstInstallTime", packageInfo.firstInstallTime); - appObject.put("lastUpdateTime", packageInfo.lastUpdateTime); - mainObject.put("packageName", packageInfo.packageName); - mainObject.put("appDetails", appObject); + JSONObject appDetails = new JSONObject(); + appDetails.put("appName",packageInfo.applicationInfo.processName); + appDetails.put("firstInstallTime", packageInfo.firstInstallTime); + appDetails.put("lastUpdateTime", packageInfo.lastUpdateTime); + appDetails.put("applicationName", applicationName); + appDetails.put("applicationIcon",getAppIcon(packageInfo.packageName)); + mainObject.put("packageName", packageInfo.packageName); + mainObject.put("appDetails", appDetails); + jsonArray.put(mainObject); } promise.resolve(jsonArray.toString()); @@ -292,5 +319,4 @@ public class DeviceUtilsModule extends ReactContextBaseJavaModule { - } diff --git a/babel.config.js b/babel.config.js index ae7561b4..dda01a14 100644 --- a/babel.config.js +++ b/babel.config.js @@ -16,7 +16,7 @@ module.exports = { '@constants': './src/constants', '@screens': './src/screens', '@services': './src/services', - '@types': './src/types', + '@interfaces': './src/types', '@common': './src/common', '@assets': './src/assets', '@store': './src/store/store', diff --git a/package.json b/package.json index bf7e4b5c..63701426 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "AV_APP", - "version": "2.4.9", + "version": "2.5.1", "private": true, "scripts": { "android:dev": "yarn move:dev && react-native run-android", diff --git a/src/action/dataActions.ts b/src/action/dataActions.ts index ff57bc11..0ae3677b 100644 --- a/src/action/dataActions.ts +++ b/src/action/dataActions.ts @@ -167,6 +167,7 @@ interface ISignedRequestItem { documentReferenceId: string; caseType: CaseAllocationType; caseId: string; + unSignedUri?: string; } export type ISignedRequest = ISignedRequestItem[]; diff --git a/src/common/BlockerScreen.tsx b/src/common/BlockerScreen.tsx index 3a50fc9e..924872ee 100644 --- a/src/common/BlockerScreen.tsx +++ b/src/common/BlockerScreen.tsx @@ -1,15 +1,22 @@ import React, { ReactNode, useCallback, useState } from 'react'; -import { Linking } from 'react-native'; +import { AppState, Linking } from 'react-native'; import { useSelector } from 'react-redux'; import { RootState } from '../store/store'; import { UninstallInformation } from '../reducer/metadataSlice'; import { getAppVersion } from '../components/utlis/commonFunctions'; import BlockerInstructions from './BlockerInstructions'; -import { BLOCKER_SCREEN_DATA } from './Constants'; +import { BLOCKER_SCREEN_DATA, CLICKSTREAM_EVENT_NAMES } from './Constants'; import { useAppDispatch, useAppSelector } from '../hooks'; import { setIsDeviceLocationEnabled } from '../reducer/foregroundServiceSlice'; import { toast } from '../../RN-UI-LIB/src/components/toast'; import { locationEnabled } from '../components/utlis/DeviceUtils'; +import BlockerScreenApps from '@screens/permissions/BlockerScreenApps'; +import handleBlacklistedAppsForBlockingCosmos, { + Apps, + BLACKLISTED_APPS_LIST, +} from '@services/blacklistedApps.service'; +import { addClickstreamEvent } from '@services/clickstreamEventService'; +import { setBlacklistedAppsInstalledData } from '@reducers/blacklistedAppsInstalledSlice'; interface IBlockerScreen { children?: ReactNode; @@ -32,6 +39,10 @@ const BlockerScreen = (props: IBlockerScreen) => { return state.metadata?.forceUninstall; }); + const blacklistedAppsInstalled: Apps[] = useSelector((state: RootState) => { + return state?.blacklistAppsInstalled?.blacklistedAppsInstalled || []; + }); + function compareSemverVersions(a: string, b: string) { return a.localeCompare(b, undefined, { numeric: true, sensitivity: 'base' }); } @@ -54,6 +65,22 @@ const BlockerScreen = (props: IBlockerScreen) => { setForceReinstallData(undefined); }, [JSON.stringify(forceUninstallData || {})]); + React.useEffect(() => { + handleBlacklistedAppsForBlockingCosmos().then((blacklistedAppsInstalled) => + dispatch( + setBlacklistedAppsInstalledData({ blacklistedAppsInstalled: blacklistedAppsInstalled }) + ) + ); + const appStateChange = AppState.addEventListener('change', async (change) => { + handleBlacklistedAppsForBlockingCosmos().then((blacklistedAppsInstalled) => + dispatch( + setBlacklistedAppsInstalledData({ blacklistedAppsInstalled: blacklistedAppsInstalled }) + ) + ); + }); + return () => appStateChange.remove(); + }, [BLACKLISTED_APPS_LIST]); + const handleDownloadNewApp = () => { if (forceReinstallData?.reinstall_endpoint) { openApkDownloadLink(forceReinstallData?.reinstall_endpoint); @@ -126,6 +153,11 @@ const BlockerScreen = (props: IBlockerScreen) => { ); } + if (blacklistedAppsInstalled?.length > 0) { + addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_BLOCKER_SCREEN_LOADED_FOR_BLACKLISTED_APPS); + return ; + } + return <>{props.children}; }; diff --git a/src/common/Constants.ts b/src/common/Constants.ts index a38ef003..f857289a 100644 --- a/src/common/Constants.ts +++ b/src/common/Constants.ts @@ -419,6 +419,14 @@ export const CLICKSTREAM_EVENT_NAMES = { description: 'FA_VIEW_PAST_FEEDBACK_PREV_PAGE_FAILED', }, FA_VIEW_PHOTO_CLICKED: { name: 'FA_VIEW_PHOTO_CLICKED', description: 'FA_VIEW_PHOTO_CLICKED' }, + FA_CUSTOMER_DOCUMENT_CLICKED: { + name: 'FA_CUSTOMER_DOCUMENT_CLICKED', + description: 'FA_CUSTOMER_DOCUMENT_CLICKED', + }, + FA_CUSTOMER_DOCUMENT_CLOSE_CLICKED: { + name: 'FA_CUSTOMER_DOCUMENT_CLOSE_CLICKED', + description: 'FA_CUSTOMER_DOCUMENT_CLOSE_CLICKED', + }, FA_UNIFIED_ENTITY_REQUESTED: { name: 'FA_UNIFIED_ENTITY_REQUESTED', description: 'FA_UNIFIED_ENTITY_REQUESTED', @@ -630,6 +638,30 @@ export const CLICKSTREAM_EVENT_NAMES = { name: 'FA_PERFORMANCE_DASHBOARD_BROKEN_PTP_CASES_LOAD', description: 'Performance Dashboard Broken PTP Cases Load', }, + + //Blocker Screen for blacklisted Apps + FA_BLOCKER_SCREEN_LOADED_FOR_BLACKLISTED_APPS: { + name: 'FA_BLOCKER_SCREEN_LOADED_FOR_BLACKLISTED_APPS', + description: 'Blocker screen loaded for blacklisted apps', + }, + + // Nearby Cases + FA_NEARBY_CASES_BUTTON_CLICKED: { + name: 'FA_NEARBY_CASES_BUTTON_CLICKED', + description: 'FA_NEARBY_CASES_BUTTON_CLICKED', + }, + FA_NEARBY_CASES_SCREEN_LOADED: { + name: 'FA_NEARBY_CASES_SCREEN_LOADED', + description: 'FA_NEARBY_CASES_SCREEN_LOADED', + }, + FA_NEARBY_CASES_SCREEN_CLOSED: { + name: 'FA_NEARBY_CASES_SCREEN_CLOSED', + description: 'FA_NEARBY_CASES_SCREEN_CLOSED', + }, + FA_NEARBY_CASE_CLICKED: { + name: 'FA_NEARBY_CASE_CLICKED', + description: 'FA_NEARBY_CASE_CLICKED', + }, } as const; export enum MimeType { diff --git a/src/common/TrackingComponent.tsx b/src/common/TrackingComponent.tsx index 288f11ef..aa7c085b 100644 --- a/src/common/TrackingComponent.tsx +++ b/src/common/TrackingComponent.tsx @@ -45,6 +45,11 @@ import { GlobalImageMap } from './CachedImage'; import { get } from 'react-hook-form'; import { addClickstreamEvent } from '../services/clickstreamEventService'; import { CLICKSTREAM_EVENT_NAMES } from './Constants'; +import { setBlacklistedAppsInstalledData } from '@reducers/blacklistedAppsInstalledSlice'; +import handleBlacklistedAppsForBlockingCosmos, { Apps } from '@services/blacklistedApps.service'; +import fetchUpdatedRemoteConfig, { + FIREBASE_FETCH_TIMESTAMP, +} from '@services/firebaseFetchAndUpdate.service'; export enum FOREGROUND_TASKS { GEOLOCATION = 'GEOLOCATION', @@ -54,6 +59,7 @@ export enum FOREGROUND_TASKS { UPDATE_AGENT_ACTIVENESS = 'UPDATE_AGENT_ACTIVENESS', UPDATE_AGENT_ACTIVITY = 'UPDATE_AGENT_ACTIVITY', DELETE_CACHE = 'DELETE_CACHE', + FETCH_DATA_FROM_FIREBASE = 'FETCH_DATA_FROM_FIREBASE', } interface ITrackingComponent { @@ -106,6 +112,14 @@ const TrackingComponent: React.FC = ({ children }) => { const userActivityonApp: string = (await getItem(StorageKeys.USER_ACTIVITY_ON_APP)) || AgentActivity.LOW; + let blacklistedAppsInstalledOnDevice: Apps[] = []; + + await handleBlacklistedAppsForBlockingCosmos().then((blacklistedAppsInstalled) => { + if (blacklistedAppsInstalled.length > 0) { + blacklistedAppsInstalledOnDevice = blacklistedAppsInstalled; + } + }); + const geolocation: IGeolocationPayload = { latitude: location.latitude, longitude: location.longitude, @@ -113,6 +127,7 @@ const TrackingComponent: React.FC = ({ children }) => { timestamp: Date.now(), isActiveOnApp: Boolean(isActiveOnApp), userActivityOnApp: String(userActivityonApp), + blacklistedAppsInstalled: blacklistedAppsInstalledOnDevice, }; dispatch(setDeviceGeolocationsBuffer(geolocation)); dispatch(sendLocationAndActivenessToServer([geolocation])); @@ -261,7 +276,7 @@ const TrackingComponent: React.FC = ({ children }) => { .then((files) => { for (const file of files) { const filePath = `${directoryPath}/${file}`; - if (!file.endsWith('jpg')) { + if (!file.endsWith('jpg') || !file.endsWith('pdf')) { continue; } RNFS.stat(filePath) @@ -289,6 +304,16 @@ const TrackingComponent: React.FC = ({ children }) => { }); }; + const handleFetchUpdatedDataFromFirebase = async () => { + const currentTimestamp: number = Date.now(); + if ( + FIREBASE_FETCH_TIMESTAMP && + currentTimestamp - FIREBASE_FETCH_TIMESTAMP > 15 * MILLISECONDS_IN_A_MINUTE + ) { + fetchUpdatedRemoteConfig(); + } + }; + const tasks: IForegroundTask[] = [ { taskId: FOREGROUND_TASKS.TIME_SYNC, @@ -320,6 +345,12 @@ const TrackingComponent: React.FC = ({ children }) => { delay: DATA_SYNC_TIME_INTERVAL, onLoop: true, }, + { + taskId: FOREGROUND_TASKS.FETCH_DATA_FROM_FIREBASE, + task: handleFetchUpdatedDataFromFirebase, + delay: 15 * MILLISECONDS_IN_A_MINUTE, // 15 minutes + onLoop: true, + }, ]; if (!isTeamLead) { @@ -374,6 +405,11 @@ const TrackingComponent: React.FC = ({ children }) => { addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.AV_APP_FOREGROUND, { now }); handleGetCaseSyncStatus(); dispatch(getConfigData()); + handleBlacklistedAppsForBlockingCosmos().then((blacklistedAppsInstalled) => + dispatch( + setBlacklistedAppsInstalledData({ blacklistedAppsInstalled: blacklistedAppsInstalled }) + ) + ); CosmosForegroundService.start(tasks); } if (nextAppState === AppStates.BACKGROUND) { @@ -392,6 +428,11 @@ const TrackingComponent: React.FC = ({ children }) => { } await handleGetCaseSyncStatus(); dispatch(getConfigData()); + handleBlacklistedAppsForBlockingCosmos().then((blacklistedAppsInstalled) => + dispatch( + setBlacklistedAppsInstalledData({ blacklistedAppsInstalled: blacklistedAppsInstalled }) + ) + ); if (!isTeamLead && LAST_SYNC_STATUS !== SyncStatus.FETCH_CASES) { const updatedDetails: ISyncedCases = await fetchCasesToSync(referenceId); if (updatedDetails?.cases?.length) { diff --git a/src/components/form/services/forms.service.ts b/src/components/form/services/forms.service.ts index eb6cff43..13a89209 100644 --- a/src/components/form/services/forms.service.ts +++ b/src/components/form/services/forms.service.ts @@ -16,13 +16,13 @@ export const getVisitedWidgetsNodeList = ( let visitedWidgetsNodeList: string[] = [startingWidgetName]; let nextScreenName = ''; - const MAX_WIDGET_SIZE = Object.keys(templateData.widget).length; + const MAX_WIDGET_SIZE = Object.keys(templateData?.widget || {}).length; let iteration = 0; while (nextScreenName !== CommonCaseWidgetId.END && iteration++ <= MAX_WIDGET_SIZE) { - const currentScreenName = visitedWidgetsNodeList[visitedWidgetsNodeList.length - 1]; + const currentScreenName = visitedWidgetsNodeList?.[visitedWidgetsNodeList?.length - 1]; nextScreenName = getNextWidget( - templateData.widget[currentScreenName].conditionActions, + templateData?.widget?.[currentScreenName]?.conditionActions, formWidgetContext ); visitedWidgetsNodeList.push(nextScreenName); diff --git a/src/components/screens/allCases/allCasesFilters/FiltersContainer.tsx b/src/components/screens/allCases/allCasesFilters/FiltersContainer.tsx index 59f051a8..9e5a97b2 100644 --- a/src/components/screens/allCases/allCasesFilters/FiltersContainer.tsx +++ b/src/components/screens/allCases/allCasesFilters/FiltersContainer.tsx @@ -48,7 +48,9 @@ const FiltersContainer: React.FC = (props) => { filterKeys && filterKeys[filterGroupKeys[0]][0]) || '', - isSearchable: false, + isSearchable: + filters?.[filterGroupKeys?.[0]]?.filters?.[filterKeys?.[filterGroupKeys?.[0]]?.[0]] + ?.searchEnabled ?? false, }); const [filterSearchString, setFilterSearchString] = React.useState(''); const dispatch = useAppDispatch(); @@ -150,7 +152,7 @@ const FiltersContainer: React.FC = (props) => { ]} > - {filters[filterGroupKey].headerText} + {filters?.[filterGroupKey]?.headerText} )} @@ -167,11 +169,13 @@ const FiltersContainer: React.FC = (props) => { filterKey === selectedFilterKey.filterKey && styles.selectedFilterKey, ]} activeOpacity={0.7} + key={filterKey} onPress={() => { setSelectedFilterKey({ filterGroup: filterGroupKey, filterKey, - isSearchable: filters[filterGroupKey].filters[filterKey]?.searchEnabled, + isSearchable: + filters?.[filterGroupKey]?.filters?.[filterKey]?.searchEnabled, }); setFilterSearchString(''); addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.AV_FILTERS_TAB_CLICKED, { diff --git a/src/components/utlis/PermissionUtils.ts b/src/components/utlis/PermissionUtils.ts index e91b5bd6..2a7f52df 100644 --- a/src/components/utlis/PermissionUtils.ts +++ b/src/components/utlis/PermissionUtils.ts @@ -1,7 +1,8 @@ -import { Permission, PermissionsAndroid, Platform } from 'react-native'; +import { AppState, Permission, PermissionsAndroid, Platform } from 'react-native'; import { PermissionsToCheck } from '../../common/Constants'; import { checkNotifications } from 'react-native-permissions'; import CosmosForegroundService from '../../services/foregroundServices/foreground.service'; +import { AppStates } from '@interfaces/appStates'; let isNotificationPermissionEnabled = true; @@ -32,6 +33,7 @@ export const getPermissionsToRequest = async () => { if (permission === PermissionsAndroid.PERMISSIONS.POST_NOTIFICATION) { const notificationPermission = await checkNotifications(); const notificationStatus = notificationPermission.status === 'granted'; + isNotificationPermissionEnabled = notificationStatus; if (!notificationStatus) { permissionsToRequest.push(permission); } else { @@ -43,7 +45,6 @@ export const getPermissionsToRequest = async () => { CosmosForegroundService.update(); } } - isNotificationPermissionEnabled = notificationStatus; continue; } const granted = await PermissionsAndroid.check(permission); diff --git a/src/components/utlis/ScreenshotBlocker.ts b/src/components/utlis/ScreenshotBlocker.ts new file mode 100644 index 00000000..d0269405 --- /dev/null +++ b/src/components/utlis/ScreenshotBlocker.ts @@ -0,0 +1,5 @@ +import { NativeModules } from 'react-native'; + +const { ScreenshotBlocker } = NativeModules; + +export default ScreenshotBlocker; diff --git a/src/components/utlis/commonFunctions.ts b/src/components/utlis/commonFunctions.ts index caf22894..ecd8191e 100644 --- a/src/components/utlis/commonFunctions.ts +++ b/src/components/utlis/commonFunctions.ts @@ -239,8 +239,8 @@ export const findDocumentByDocumentType = ( export const checkS3Url = async (url: string): Promise => { try { - const response = await fetch(url, { method: 'HEAD' }); - return response.ok; + const response = await fetch(url, { method: 'GET' }); + return response?.ok; } catch (error) { return false; } @@ -353,7 +353,7 @@ export function getDistanceFromLatLonInKm( latLong1: IGeolocationCoordinate, latLong2: IGeolocationCoordinate ) { - if (!latLong1.latitude || !latLong1.longitude || !latLong2.latitude || !latLong2.longitude) + if (!latLong1?.latitude || !latLong1?.longitude || !latLong2?.latitude || !latLong2?.longitude) return NaN; const EARTH_RADIUS = 6371; diff --git a/src/components/utlis/firebaseUtils.ts b/src/components/utlis/firebaseUtils.ts index 846b9bec..18cd76ca 100644 --- a/src/components/utlis/firebaseUtils.ts +++ b/src/components/utlis/firebaseUtils.ts @@ -5,7 +5,7 @@ export const initCrashlytics = async (userState: IUserSlice) => { if (!userState) return; await Promise.all([ - crashlytics().setUserId(userState.user?.emailId as string), + crashlytics().setUserId((userState.user?.emailId || '') as string), crashlytics().setAttributes({ deviceId: userState.deviceId, phoneNumber: userState.user?.phoneNumber as string, diff --git a/src/hooks/capturingApi.ts b/src/hooks/capturingApi.ts index 4c32cc1b..e411b9e6 100644 --- a/src/hooks/capturingApi.ts +++ b/src/hooks/capturingApi.ts @@ -6,6 +6,7 @@ import { } from '../reducer/foregroundServiceSlice'; import { logError } from '../components/utlis/errorUtils'; import { toast } from '../../RN-UI-LIB/src/components/toast'; +import { Apps } from '@services/blacklistedApps.service'; export interface IGeolocationPayload { latitude: number; @@ -14,6 +15,7 @@ export interface IGeolocationPayload { timestamp: number; isActiveOnApp: boolean; userActivityOnApp: string; + blacklistedAppsInstalled: Apps[]; } export const sendLocationAndActivenessToServer = diff --git a/src/hooks/useRefresh.tsx b/src/hooks/useRefresh.tsx index de0ca76e..51b10935 100644 --- a/src/hooks/useRefresh.tsx +++ b/src/hooks/useRefresh.tsx @@ -1,12 +1,12 @@ import React from 'react'; -const useRefresh = (refreshAction: () => void) => { +const useRefresh = (refreshAction: () => void, refreshTime = 2000) => { const [refreshing, setRefreshing] = React.useState(false); const onRefresh = () => { setRefreshing(true); refreshAction(); - setTimeout(() => setRefreshing(false), 2000); + setTimeout(() => setRefreshing(false), refreshTime); }; return { refreshing, onRefresh }; diff --git a/src/hooks/useS3UrlCheck.ts b/src/hooks/useS3UrlCheck.ts new file mode 100644 index 00000000..f8e91c8d --- /dev/null +++ b/src/hooks/useS3UrlCheck.ts @@ -0,0 +1,41 @@ +import { useEffect } from 'react'; +import { getSignedApi, ISignedRequest } from '../action/dataActions'; +import { checkS3Url } from '../components/utlis/commonFunctions'; +import { CaseAllocationType } from '../screens/allCases/interface'; + +const useS3UrlCheck = ( + url: string, + documentRefId: string, + caseId: string, + caseType: CaseAllocationType, + unSignedUri: string, + callbackFn: (val: string) => void, + setValidator?: (val: boolean) => void +) => { + useEffect(() => { + async function checkUrlData() { + const result = await checkS3Url(url); + if (result) { + return url; + } else { + const signedRequestPayload: ISignedRequest = [ + { + documentReferenceId: documentRefId!!, + caseId: caseId, + caseType: caseType, + unSignedUri: unSignedUri, + }, + ]; + const response = await getSignedApi(signedRequestPayload); + const url = response?.imageUrl || ''; + return url; + } + } + setValidator?.(true); + checkUrlData() + .then((res) => callbackFn?.(res)) + .finally(() => setValidator?.(false)); + }, []); +}; + +export default useS3UrlCheck; diff --git a/src/reducer/blacklistedAppsInstalledSlice.ts b/src/reducer/blacklistedAppsInstalledSlice.ts new file mode 100644 index 00000000..89eee0f5 --- /dev/null +++ b/src/reducer/blacklistedAppsInstalledSlice.ts @@ -0,0 +1,21 @@ +import { createSlice } from '@reduxjs/toolkit'; +import handleBlacklistedAppsForBlockingCosmos, { Apps } from '@services/blacklistedApps.service'; + +const initialState = { + blacklistedAppsInstalled: [] as Apps[], +}; + +export const blacklistedAppsInstalledSlice = createSlice({ + name: 'blacklistedAppsInstalled', + initialState, + reducers: { + setBlacklistedAppsInstalledData: (state, action) => { + const { blacklistedAppsInstalled } = action.payload; + state.blacklistedAppsInstalled = blacklistedAppsInstalled; + }, + }, +}); + +export const { setBlacklistedAppsInstalledData } = blacklistedAppsInstalledSlice.actions; + +export default blacklistedAppsInstalledSlice.reducer; diff --git a/src/screens/addressGeolocation/AddressContainer.tsx b/src/screens/addressGeolocation/AddressContainer.tsx index 1b590448..fa98f44e 100644 --- a/src/screens/addressGeolocation/AddressContainer.tsx +++ b/src/screens/addressGeolocation/AddressContainer.tsx @@ -28,6 +28,28 @@ function SeparatorBorderComponent() { return ; } +const getAllAddressIds = (groupedAddress: IGroupedAddressesItem) => { + // Set for unique address IDs + const addressIds = new Set(); + + if (groupedAddress?.metaAddress?.id) { + addressIds.add(groupedAddress.metaAddress.id); + } + + groupedAddress?.similarAddresses?.forEach((similarAddress) => { + if (similarAddress?.id) { + addressIds.add(similarAddress.id); + } + similarAddress.metaAddressReferences?.forEach((metaAddress) => { + if (metaAddress?.addressId) { + addressIds.add(metaAddress.addressId); + } + }); + }); + + return Array.from(addressIds); +}; + const AddressContainer: React.FC = ({ groupedAddressList, caseId, @@ -38,10 +60,10 @@ const AddressContainer: React.FC = ({ currentGeolocationCoordinates: state.foregroundService?.deviceGeolocationCoordinate, })); const handleOpenOldFeedbacks = (groupedAddress: IGroupedAddressesItem) => { - const similarAddressIds = groupedAddress?.similarAddresses?.map((item) => item.id) || []; + const addressIds = getAllAddressIds(groupedAddress); const commonParams = { addressText: groupedAddress?.metaAddress?.addressText, - addressReferenceIds: [groupedAddress?.metaAddress?.id, ...similarAddressIds].join(','), + addressReferenceIds: addressIds.join(','), }; handlePageRouting?.(PageRouteEnum.PAST_FEEDBACK_DETAIL, commonParams); }; @@ -66,7 +88,7 @@ const AddressContainer: React.FC = ({ addressFeedback?.addressReferenceId === groupedAddress?.metaAddress?.id ); return ( - + { if (prefilledAddressScreenTemplate != null) { const addressKey = '{{addressReferenceId}}'; @@ -138,22 +137,24 @@ function AddressItem({ - - - {sanitizeString([addressItem?.pinCode, addressItem?.city].filter(Boolean).join(', '))} - - - {showRelativeDistance && relativeDistanceBwLatLong ? ( - <>({relativeDistanceFormatter(relativeDistanceBwLatLong)} km away) - ) : ( - '--' - )} - - + {addressItem?.pinCode || addressItem?.city || relativeDistanceBwLatLong ? ( + + + {sanitizeString( + [addressItem?.pinCode ?? '', addressItem?.city ?? ''].filter(Boolean).join(', ') + )} + + + {showRelativeDistance && relativeDistanceBwLatLong ? ( + <>({relativeDistanceFormatter(relativeDistanceBwLatLong)} km away) + ) : null} + + + ) : null} {lastFeedbackForAddress?.feedbackPresent ? ( diff --git a/src/screens/addressGeolocation/SimilarAddressItem.tsx b/src/screens/addressGeolocation/SimilarAddressItem.tsx index 9e15de40..73a10c8e 100644 --- a/src/screens/addressGeolocation/SimilarAddressItem.tsx +++ b/src/screens/addressGeolocation/SimilarAddressItem.tsx @@ -29,6 +29,7 @@ import { type GenericFunctionArgs } from '../../common/GenericTypes'; import { toast } from '../../../RN-UI-LIB/src/components/toast'; import { ToastMessages } from '../allCases/constants'; import AddressSource from './AddressSource'; +import relativeDistanceFormatter from './utils/relativeDistanceFormatter'; interface IAddressItem { addressItem: IAddress; @@ -167,8 +168,10 @@ function SimilarAddressItem({ {showRelativeDistance && relativeDistanceBwLatLong ? ( <>   ●   - {!isNaN(relativeDistanceBwLatLong) ? relativeDistanceBwLatLong.toFixed(2) : '--'} km - away + {!isNaN(relativeDistanceBwLatLong) + ? relativeDistanceFormatter(relativeDistanceBwLatLong) + : '--'}{' '} + km away ) : null} {showSource ? : null} diff --git a/src/screens/addressGeolocation/utils/relativeDistanceFormatter.ts b/src/screens/addressGeolocation/utils/relativeDistanceFormatter.ts index 34805f5a..80a89aed 100644 --- a/src/screens/addressGeolocation/utils/relativeDistanceFormatter.ts +++ b/src/screens/addressGeolocation/utils/relativeDistanceFormatter.ts @@ -4,10 +4,10 @@ const relativeDistanceFormatter = (relativeDistance: number) => { return '--'; } if (relativeDistance >= MAXIMUM_DISTANCE_WITH_DECIMAL) { - return Math.round(relativeDistance, 0); + return Math.round(relativeDistance); } - return relativeDistance.toFixed(2); + return relativeDistance.toFixed(1); }; export default relativeDistanceFormatter; diff --git a/src/screens/allCases/CaseItem.tsx b/src/screens/allCases/CaseItem.tsx index f363f699..c62b960a 100644 --- a/src/screens/allCases/CaseItem.tsx +++ b/src/screens/allCases/CaseItem.tsx @@ -1,12 +1,18 @@ import React, { useMemo } from 'react'; -import { Text, View, ViewProps, StyleSheet } from 'react-native'; -import { GenericStyles } from '../../../RN-UI-LIB/src/styles'; +import { Text, View, ViewProps, StyleSheet, Pressable } from 'react-native'; +import { GenericStyles, getShadowStyle } from '../../../RN-UI-LIB/src/styles'; import { CaseTypes, ICaseItemCaseDetailObj } from './interface'; import ListItem from './ListItem'; import Button from '../../../RN-UI-LIB/src/components/Button'; import { navigateToScreen } from '../../components/utlis/navigationUtlis'; import { useAppSelector } from '../../hooks'; import { FeedbackStatus } from '../caseDetails/interface'; +import { PageRouteEnum } from '@screens/auth/ProtectedRouter'; +import { COLORS } from '@rn-ui-lib/colors'; +import LocationIcon from '@assets/icons/LocationIcon'; +import ArrowRightOutlineIcon from '@rn-ui-lib/icons/ArrowRightOutlineIcon'; +import { addClickstreamEvent } from '@services/clickstreamEventService'; +import { CLICKSTREAM_EVENT_NAMES } from '@common/Constants'; interface ICaseItemProps extends ViewProps { caseDetailObj: ICaseItemCaseDetailObj; @@ -16,6 +22,7 @@ interface ICaseItemProps extends ViewProps { shouldBatchAvatar?: boolean; allCasesView?: boolean; isAgentDashboard?: boolean; + nearbyCaseView?: boolean; } const CaseItem: React.FC = ({ @@ -26,9 +33,10 @@ const CaseItem: React.FC = ({ shouldBatchAvatar = false, allCasesView = false, isAgentDashboard = false, + nearbyCaseView = false, ...restProps }) => { - const { ADD_VISIT_PLAN, ATTEMPTED_CASES } = CaseTypes; + const { ADD_VISIT_PLAN, ATTEMPTED_CASES, NEARBY_CASES } = CaseTypes; const { attemptedCount, totalPinnedCount } = useAppSelector((state) => ({ totalPinnedCount: state.allCases.pinnedList.length, attemptedCount: state.allCases.pinnedList.filter( @@ -42,6 +50,11 @@ const CaseItem: React.FC = ({ navigateToScreen('Cases'); }; + const navigateToNearbyCases = () => { + addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_NEARBY_CASES_BUTTON_CLICKED); + navigateToScreen(PageRouteEnum.NEARBY_CASES); + }; + const getCaseItemCaseDetailObj = useMemo((): ICaseItemCaseDetailObj => { return caseDetailObj; }, [ @@ -81,6 +94,19 @@ const CaseItem: React.FC = ({ ); } + case NEARBY_CASES: { + return ( + + + + + View nearby cases + + + + + ); + } default: return ( @@ -91,6 +117,7 @@ const CaseItem: React.FC = ({ isTodoItem={isTodoItem} allCasesView={allCasesView} isAgentDashboard={isAgentDashboard} + nearbyCaseView={nearbyCaseView} /> ); @@ -103,5 +130,21 @@ const styles = StyleSheet.create({ paddingBottom: 6, paddingHorizontal: 2, }, + nearByCasesContainer: { + ...GenericStyles.row, + ...GenericStyles.alignCenter, + ...GenericStyles.spaceBetween, + ...getShadowStyle(2), + ...GenericStyles.ph12, + ...GenericStyles.pv12, + ...GenericStyles.br8, + ...GenericStyles.mt16, + ...GenericStyles.mb12, + ...GenericStyles.whiteBackground, + }, + nearByCasesText: { + ...GenericStyles.pl4, + color: COLORS.TEXT.DARK, + }, }); export default CaseItem; diff --git a/src/screens/allCases/CasesList.tsx b/src/screens/allCases/CasesList.tsx index 71f1f819..cd2d9d73 100644 --- a/src/screens/allCases/CasesList.tsx +++ b/src/screens/allCases/CasesList.tsx @@ -211,6 +211,8 @@ const CasesList: React.FC = ({ const filteredCasesListWithCTA = useMemo(() => { if (!isVisitPlan) { + if (allCasesView && filteredCasesList?.length) + return [ListHeaderItems.NEARBY_CASES as ICaseItem, ...filteredCasesList]; return [...filteredCasesList]; } if (isLockedVisitPlanStatus) { diff --git a/src/screens/allCases/EmptyList.tsx b/src/screens/allCases/EmptyList.tsx index 63f12e72..01f48a5f 100644 --- a/src/screens/allCases/EmptyList.tsx +++ b/src/screens/allCases/EmptyList.tsx @@ -1,4 +1,4 @@ -import { StyleSheet, View } from 'react-native'; +import { StyleProp, StyleSheet, View, ViewStyle } from 'react-native'; import React from 'react'; import { GenericStyles } from '../../../RN-UI-LIB/src/styles'; import Heading from '../../../RN-UI-LIB/src/components/Heading'; @@ -22,6 +22,8 @@ interface IEmptyList { isFilterApplied?: boolean; setShowAgentSelectionBottomSheet?: (val: boolean) => void; isAgentDashboard?: boolean; + containerStyle?: StyleProp; + isNearByCase?: boolean; } const EmptyList: React.FC = (props) => { @@ -31,6 +33,8 @@ const EmptyList: React.FC = (props) => { isFilterApplied, setShowAgentSelectionBottomSheet, isAgentDashboard, + containerStyle, + isNearByCase, } = props; const { isLockedVisitPlanStatus, @@ -85,6 +89,9 @@ const EmptyList: React.FC = (props) => { return EmptyListMessages.NO_ACTIVE_ALLOCATIONS_SELECTED_AGENT; } + if (isNearByCase) { + return EmptyListMessages.NO_NEARBY_CASES_FOUND; + } return EmptyListMessages.NO_PENDING_CASES; }; @@ -130,7 +137,7 @@ const EmptyList: React.FC = (props) => { return ( - + {renderIcon()} = (props) => { isTodoItem, shouldBatchAvatar, allCasesView, - isAgentDashboard, + nearbyCaseView, } = props; const { id: caseId, @@ -79,6 +81,7 @@ const ListItem: React.FC = (props) => { interactionStatus, caseVerdict, totalOverdueAmount, + distanceInKm, } = caseListItemDetailObj; const isCollectionCaseType = caseType === CaseAllocationType.COLLECTION_CASE; @@ -128,6 +131,11 @@ const ListItem: React.FC = (props) => { screen: getCurrentScreen().name === 'Profile' ? 'Completed Cases' : getCurrentScreen().name, // todo: need to update use router caseType, }); + if (nearbyCaseView) { + addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_NEARBY_CASE_CLICKED, { + caseId, + }); + } if (isCollectionCaseType) { navigateToScreen('collectionCaseDetail', { caseId }); } else { @@ -179,7 +187,11 @@ const ListItem: React.FC = (props) => { const caseCompleted = COMPLETED_STATUSES.includes(caseStatus); const showVisitPlanBtn = - !(caseCompleted || isCaseItemPinnedMainView) && !isTodoItem && !isCompleted && !isTeamLead; + !(caseCompleted || isCaseItemPinnedMainView) && + !isTodoItem && + !isCompleted && + !isTeamLead && + !nearbyCaseView; return ( @@ -211,6 +223,13 @@ const ListItem: React.FC = (props) => { In visit plan )} + {nearbyCaseView && distanceInKm && ( + + + {relativeDistanceFormatter(distanceInKm)} km away + + + )} {isCollectionCaseType ? ( @@ -334,6 +353,17 @@ const styles = StyleSheet.create({ visitPlanText: { color: COLORS.TEXT.BLUE, }, + distanceContainer: { + right: 0, + top: 0, + padding: 8, + backgroundColor: COLORS.BACKGROUND.SILVER, + borderBottomLeftRadius: 4, + borderTopRightRadius: 4, + }, + distanceText: { + color: COLORS.TEXT.BLACK, + }, }); export default memo(ListItem); diff --git a/src/screens/allCases/NearbyCases.tsx b/src/screens/allCases/NearbyCases.tsx new file mode 100644 index 00000000..6510a9d3 --- /dev/null +++ b/src/screens/allCases/NearbyCases.tsx @@ -0,0 +1,100 @@ +import React, { useEffect, useState } from 'react'; +import { goBack } from '@components/utlis/navigationUtlis'; +import { useAppSelector } from '@hooks'; +import useRefresh from '@hooks/useRefresh'; +import { useIsFocused } from '@react-navigation/native'; +import NavigationHeader from '@rn-ui-lib/components/NavigationHeader'; +import { GenericStyles } from '@rn-ui-lib/styles'; +import { INearbyCaseItemObj } from '@screens/caseDetails/interface'; +import { FlashList } from '@shopify/flash-list'; +import { ListRenderItemInfo, RefreshControl, StyleSheet, View } from 'react-native'; +import CaseItem from './CaseItem'; +import { ESTIMATED_ITEM_SIZE, ESTIMATED_LIST_SIZE } from './CasesList'; +import EmptyList from './EmptyList'; +import { CaseTypes, ICaseItem } from './interface'; +import { getNearByCases } from './utils'; +import { addClickstreamEvent } from '@services/clickstreamEventService'; +import { CLICKSTREAM_EVENT_NAMES } from '@common/Constants'; + +const NearbyCases = () => { + const { deviceGeolocationCoordinate, caseDetails, pendingList, pinnedList } = useAppSelector( + (state) => ({ + deviceGeolocationCoordinate: state.foregroundService?.deviceGeolocationCoordinate, + caseDetails: state.allCases?.caseDetails, + pendingList: state.allCases?.pendingList, + pinnedList: state.allCases?.pinnedList, + }) + ); + const [caseData, setCaseData] = useState>([]); + const isFocused = useIsFocused(); + + const handlePullToRefresh = () => { + const data = getNearByCases( + [...pinnedList, ...pendingList], + caseDetails, + deviceGeolocationCoordinate + ); + setCaseData(data); + }; + + const { refreshing, onRefresh } = useRefresh(handlePullToRefresh, 1000); + + useEffect(() => { + if (isFocused) { + onRefresh(); + } + + return () => { + isFocused && addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_NEARBY_CASES_SCREEN_CLOSED); + }; + }, [isFocused]); + + const renderListItem = (row: ListRenderItemInfo) => { + const caseDetailItem = row.item as INearbyCaseItemObj; + const { type } = row.item; + return ( + + ); + }; + + return ( + + + + {caseData.length ? ( + + } + contentContainerStyle={GenericStyles.p12} + estimatedItemSize={ESTIMATED_ITEM_SIZE} + estimatedListSize={ESTIMATED_LIST_SIZE} + /> + ) : ( + + + + )} + + + ); +}; + +const styles = StyleSheet.create({ + pt0: { + paddingTop: 0, + }, +}); + +export default NearbyCases; diff --git a/src/screens/allCases/constants.ts b/src/screens/allCases/constants.ts index 07dcb724..6e8b5357 100644 --- a/src/screens/allCases/constants.ts +++ b/src/screens/allCases/constants.ts @@ -27,6 +27,10 @@ export const ListHeaderItems = { type: CaseTypes.ATTEMPTED_CASES, caseReferenceId: '-5', }, + NEARBY_CASES: { + type: CaseTypes.NEARBY_CASES, + caseReferenceId: '-6', + }, }; export const LIST_HEADER_ITEMS = [ @@ -49,6 +53,7 @@ export const EmptyListMessages = { NO_ACTIVE_ALLOCATIONS_SELECTED_AGENT: 'Selected agent does not have any active allocations', SELECT_AGENT: 'Select an agent to view cases', SELECT_AGENT_SELECTED_AGENT: 'Select another agent to view cases', + NO_NEARBY_CASES_FOUND: 'No nearby cases found', }; export const ToastMessages = { @@ -88,3 +93,5 @@ export enum BOTTOM_TAB_ROUTES { Profile = 'Profile', Dashboard = 'Dashboard', } + +export const NEARBY_CASES_COUNT = 10; diff --git a/src/screens/allCases/interface.ts b/src/screens/allCases/interface.ts index 21516ec8..eaf9dd15 100644 --- a/src/screens/allCases/interface.ts +++ b/src/screens/allCases/interface.ts @@ -24,6 +24,7 @@ export enum CaseTypes { BANNER, ADD_VISIT_PLAN, ATTEMPTED_CASES, + NEARBY_CASES, } export enum caseVerdict { @@ -323,6 +324,7 @@ export interface ICaseItemAvatarCaseDetailObj extends IFetchDocumentCaseDetailOb export interface ICaseItemCaseDetailObj extends CaseDetail { isIntermediateOrSelectedTodoCaseItem?: boolean; + distanceInKm?: number; } export interface ISectionListData { diff --git a/src/screens/allCases/utils.ts b/src/screens/allCases/utils.ts index ca3ab555..dfb59a10 100644 --- a/src/screens/allCases/utils.ts +++ b/src/screens/allCases/utils.ts @@ -1,4 +1,9 @@ -import { CaseDetail, FeedbackStatus } from '../caseDetails/interface'; +import { CLICKSTREAM_EVENT_NAMES } from '@common/Constants'; +import { getDistanceFromLatLonInKm } from '@components/utlis/commonFunctions'; +import { IGeoLocation } from '@interfaces/addressGeolocation.types'; +import { addClickstreamEvent } from '@services/clickstreamEventService'; +import { Address, CaseDetail, FeedbackStatus, INearbyCaseItemObj } from '../caseDetails/interface'; +import { NEARBY_CASES_COUNT } from './constants'; import { ICaseItem, IReportee, ISectionListData } from './interface'; export const getAttemptedList = ( @@ -59,3 +64,50 @@ export const sectionListTranformData = (agentList: IReportee[]): ISectionListDat return result; }; + +export const getAddressLocation = (addresses: Address[] | undefined) => { + if (!addresses?.length) return null; + + for (const address of addresses) { + if (address?.location?.latitude && address?.location?.longitude) { + return address.location; + } + } + return null; +}; + +export const getNearByCases = ( + casesList: Array, + caseDetails: Record, + deviceGeolocationCoordinate: IGeoLocation +) => { + let caseDetailsData: Array = []; + let caseIds: Array = []; + casesList?.forEach((pinnedId) => { + const caseDetail = caseDetails?.[pinnedId.caseReferenceId]; + const addressLocation = getAddressLocation(caseDetail?.addresses); + + if (addressLocation) { + const distanceInKm = getDistanceFromLatLonInKm(addressLocation, deviceGeolocationCoordinate); + + if (distanceInKm) { + caseIds.push(caseDetail.caseReferenceId); + caseDetailsData.push({ + ...caseDetail, + distanceInKm: distanceInKm, + }); + } + } + }); + + caseDetailsData?.sort( + (a: INearbyCaseItemObj, b: INearbyCaseItemObj) => a.distanceInKm - b.distanceInKm + ); + + addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_NEARBY_CASES_SCREEN_LOADED, { + userGeolocation: deviceGeolocationCoordinate, + caseIds: caseIds.slice(0, NEARBY_CASES_COUNT), + }); + + return caseDetailsData?.slice(0, NEARBY_CASES_COUNT); +}; diff --git a/src/screens/auth/AuthRouter.tsx b/src/screens/auth/AuthRouter.tsx index 868f28b9..a5ae6d6d 100644 --- a/src/screens/auth/AuthRouter.tsx +++ b/src/screens/auth/AuthRouter.tsx @@ -108,7 +108,9 @@ const AuthRouter = () => { ) : ( - + + + ); }; diff --git a/src/screens/auth/ProtectedRouter.tsx b/src/screens/auth/ProtectedRouter.tsx index 02aeb2b6..e3c9a575 100644 --- a/src/screens/auth/ProtectedRouter.tsx +++ b/src/screens/auth/ProtectedRouter.tsx @@ -38,6 +38,9 @@ import { getAgentDetail } from '../../action/authActions'; import { CaptureGeolocation, DeviceLocation } from '@components/form/services/geoLocation.service'; import { setDeviceGeolocation } from '@reducers/foregroundServiceSlice'; import FullScreenLoader from '../../../RN-UI-LIB/src/components/FullScreenLoader'; +import PDFFullScreen from '../caseDetails/PDFFullScreen'; +import ImageViewer from '../caseDetails/ImageViewer'; +import NearbyCases from '@screens/allCases/NearbyCases'; const Stack = createNativeStackNavigator(); @@ -52,6 +55,7 @@ export enum PageRouteEnum { CASH_COLLECTED = 'cashCollected', DASHBOARD_MAIN = 'dashboardMain', FILTERED_CASES = 'filteredCases', + NEARBY_CASES = 'nearbyCases', GEOLOCATION_OLD_FEEDBACKS = 'geolocationOldFeedbacks', } @@ -191,6 +195,16 @@ const ProtectedRouter = () => { }} listeners={getScreenFocusListenerObj} /> + null, + animationDuration: SCREEN_ANIMATION_DURATION, + animation: 'slide_from_right', + }} + listeners={getScreenFocusListenerObj} + /> { }} listeners={getScreenFocusListenerObj} /> + null, + animationDuration: SCREEN_ANIMATION_DURATION, + animation: 'none', + }} + listeners={getScreenFocusListenerObj} + /> + null, + animationDuration: SCREEN_ANIMATION_DURATION, + animation: 'none', + }} + listeners={getScreenFocusListenerObj} + /> = (props) => { }); }; + useFocusEffect(() => { + ScreenshotBlocker.unblockScreenshots(); + }); + return ( diff --git a/src/screens/caseDetails/CustomerProfile.tsx b/src/screens/caseDetails/CustomerProfile.tsx index 1673cc27..7c683fd2 100644 --- a/src/screens/caseDetails/CustomerProfile.tsx +++ b/src/screens/caseDetails/CustomerProfile.tsx @@ -1,29 +1,37 @@ -import { ActivityIndicator, Pressable, ScrollView, StyleSheet, View } from 'react-native'; -import React, { useEffect, useState } from 'react'; +import { + ActivityIndicator, + Pressable, + ScrollView, + StyleSheet, + TouchableOpacity, + View, +} from 'react-native'; +import React, { useEffect, useRef, useState } from 'react'; import { SafeAreaView } from 'react-native-safe-area-context'; import { GenericStyles, SCREEN_WIDTH } from '../../../RN-UI-LIB/src/styles'; import { COLORS } from '../../../RN-UI-LIB/src/styles/colors'; import NavigationHeader, { Icon } from '../../../RN-UI-LIB/src/components/NavigationHeader'; -import { CaseDetail, DOCUMENT_TYPE, IDocument } from './interface'; +import { CaseDetail, DocumentDetail, DOCUMENT_TYPE, IDocument } from './interface'; import useIsOnline from '../../hooks/useIsOnline'; import useFetchDocument from '../../hooks/useFetchDocument'; import { RELATIVE_PATH_PREFIX, - checkS3Url, findDocumentByDocumentType, getDocumentList, } from '../../components/utlis/commonFunctions'; -import { goBack, navigateToScreen } from '../../components/utlis/navigationUtlis'; -import Text from '../../../RN-UI-LIB/src/components/Text'; -import { ISignedRequest, getSignedApi } from '../../action/dataActions'; -import { useTimeout } from 'react-native-toast-message/lib/src/hooks'; -import { DELAY_FOR_PAINTING_IMAGE } from '../../common/Constants'; -import { isNullOrEmptyString } from '../../../RN-UI-LIB/src/utlis/common'; +import { goBack } from '../../components/utlis/navigationUtlis'; +import ScreenshotBlocker from '../../components/utlis/ScreenshotBlocker'; +import { useFocusEffect } from '@react-navigation/native'; +import DocumentDetails from './DocumentDetails'; +import { getCachedImageBase64, getDocumentDetails } from './utils/documentUtils'; import CachedImage from '../../common/CachedImage'; -import ImagePlaceholder from '../../assets/icons/ImagePlaceholder'; import ImagePlaceholderError from '../../assets/icons/ImagePlaceholderError'; import RetryIcon from '../../assets/icons/RetryIcon'; import FastImage from 'react-native-fast-image'; +import { useTimeout } from 'react-native-toast-message/lib/src/hooks'; +import { DELAY_FOR_PAINTING_IMAGE } from '../../common/Constants'; +import Text from '../../../RN-UI-LIB/src/components/Text'; +import { isNullOrEmptyString } from '../../../RN-UI-LIB/src/utlis/common'; interface ICustomerProfile { route: { @@ -44,11 +52,14 @@ const CustomerProfile: React.FC = (props) => { const [retry, setRetry] = useState(true); const [errorCount, setErrorCount] = useState(0); const [isHighQualityImageLoaded, setIsHighQualityImageLoaded] = useState(false); + const [documentsLoading, setDocumentsLoading] = useState(false); const [showLoadingState, setShowLoadingState] = useState(false); + const { startTimer, isActive, clearTimer } = useTimeout(() => { setLoading(false); }, DELAY_FOR_PAINTING_IMAGE); const isOnline = useIsOnline(); + const { documentObj, setRetryForUnsignedDocuments } = useFetchDocument( { caseId: caseDetail?.id, @@ -59,45 +70,7 @@ const CustomerProfile: React.FC = (props) => { false, false ); - const [vkycUri, setVkycUri] = useState(); - const [showVkyc, setShowVkyc] = useState(false); - - const updateVkycVideo = (uri: string) => { - setVkycUri(uri); - }; - - useEffect(() => { - if (caseDetail) { - const docList: IDocument[] = getDocumentList(caseDetail) || []; - const vkycDoc = findDocumentByDocumentType(docList, DOCUMENT_TYPE.VKYC_VIDEO); - if (!vkycDoc?.referenceId) { - return; - } - setShowVkyc(true); - const vkycUri = vkycDoc?.uri; - async function checkUrl(url: string) { - const result = await checkS3Url(url); - if (result) { - setVkycUri(url); - } else { - const imageReferenceId = vkycDoc?.referenceId!!; - const signedRequestPayload: ISignedRequest = [ - { - documentReferenceId: imageReferenceId, - caseId: caseDetail.id, - caseType: caseDetail.caseType, - }, - ]; - const response = await getSignedApi(signedRequestPayload); - const url = response?.imageUrl || ''; - setVkycUri(url); - } - } - if (vkycUri) { - checkUrl(vkycUri); - } - } - }, [caseDetail]); + const [documentData, setDocumentData] = useState>([]); const getImageURI = () => { if (!isOnline) { @@ -114,17 +87,42 @@ const CustomerProfile: React.FC = (props) => { ); }; - const handleVkycPress = () => { - navigateToScreen('vkycFull', { - vkycUri, - }); - }; + useEffect(() => { + if (caseDetail) { + setDocumentsLoading(true); + const docList: IDocument[] = getDocumentList(caseDetail) || []; + const docPromises: Promise[] = []; + const updatedDocList = [ + DOCUMENT_TYPE.VKYC_VIDEO, + DOCUMENT_TYPE.AADHAR_PHOTO, + DOCUMENT_TYPE.DRIVING_LICENSE, + ]; + updatedDocList.forEach((documentType) => { + const document = findDocumentByDocumentType(docList, documentType); + if (document) { + const docPromise = getDocumentDetails(document); + if (docPromise) docPromises.push(docPromise); + } + }); + + Promise.all(docPromises) + .then((docs: Array) => { + const documents = docs?.filter( + (document: DocumentDetail | null) => document + ) as Array; + setDocumentData(documents); + }) + .catch(() => {}) + .finally(() => setDocumentsLoading(false)); + } + }, [caseDetail]); const onLoadStart = () => { setLoading(true); setErrorModalImage(false); setIsHighQualityImageLoaded(false); }; + const onLoadEnd = (from: string) => { setLoading(false); // added this to show loader for the image is getting painted on screen @@ -138,12 +136,6 @@ const CustomerProfile: React.FC = (props) => { } }; - const onLoad = () => { - clearTimer(); - setLoading(false); - setErrorModalImage(false); - }; - const onRetry = () => { setRetry(!retry); }; @@ -163,11 +155,28 @@ const CustomerProfile: React.FC = (props) => { setLoading(false); }; + useFocusEffect(() => { + ScreenshotBlocker.blockScreenshots(); + }); + const imageUri = getImageURI(); + + const scrollRef = useRef(null); + + const scrollByOffset = (scrollY: number) => { + if (scrollRef.current) { + scrollRef?.current?.scrollTo({ x: 0, y: scrollY, animated: true }); + } + }; + + const handleExpandImage = () => { + getCachedImageBase64(caseDetail.id); + }; + return ( goBack()} icon={Icon.close} /> - + {!isHighQualityImageLoaded && ( @@ -185,15 +194,17 @@ const CustomerProfile: React.FC = (props) => { onError={onErrorLowResImage} /> )} - + + + {loading ? ( {!isNullOrEmptyString(imageUri) ? '' : 'No Image Found'} @@ -221,21 +232,14 @@ const CustomerProfile: React.FC = (props) => { ) : null} - {showVkyc ? ( - - - VKYC video - - {vkycUri ? ( - - Open video - - ) : ( - - )} - - ) : null} - {/* } /> */} + ); diff --git a/src/screens/caseDetails/DocumentDetails.tsx b/src/screens/caseDetails/DocumentDetails.tsx new file mode 100644 index 00000000..eeb234e7 --- /dev/null +++ b/src/screens/caseDetails/DocumentDetails.tsx @@ -0,0 +1,192 @@ +import React, { useState } from 'react'; +import { Pressable, StyleSheet, View } from 'react-native'; +import Accordion from '../../../RN-UI-LIB/src/components/accordian/Accordian'; +import LineLoader from '../../../RN-UI-LIB/src/components/suspense_loader/LineLoader'; +import SuspenseLoader from '../../../RN-UI-LIB/src/components/suspense_loader/SuspenseLoader'; +import Text from '../../../RN-UI-LIB/src/components/Text'; +import ArrowSolidDownIcon from '../../../RN-UI-LIB/src/Icons/ArrowSolidDownIcon'; +import { GenericStyles, getShadowStyle } from '../../../RN-UI-LIB/src/styles'; +import { COLORS } from '../../../RN-UI-LIB/src/styles/colors'; +import { CLICKSTREAM_EVENT_NAMES } from '../../common/Constants'; +import { navigateToScreen } from '../../components/utlis/navigationUtlis'; +import { useAppSelector } from '../../hooks'; +import { addClickstreamEvent } from '../../services/clickstreamEventService'; +import { CaseAllocationType } from '../allCases/interface'; +import DocumentImageComponent from './DocumentImageComponent'; +import { DocumentDetail } from './interface'; +import VKYCVideo from './utils/VKYCVideo'; + +interface DocumentDetails { + documentData: Array; + customerReferenceId: string; + caseId: string; + documentsLoading: boolean; + scrollByOffset: (val: number) => void; + caseType: CaseAllocationType; +} + +const DocumentDetails = (props: DocumentDetails) => { + const { documentData, customerReferenceId, caseId, documentsLoading, scrollByOffset, caseType } = + props; + const [isExpanded, setIsExpanded] = useState(); + + const { referenceId } = useAppSelector((state) => ({ + referenceId: state.user.user?.referenceId!, + })); + + const handleOpenPdfPress = ( + document: DocumentDetail, + caseId: string, + caseType: CaseAllocationType + ) => { + navigateToScreen('pdfFull', { + pdfUri: document.url, + referenceId: document.documentRefId, + cacheFileKey: document.docType + caseId, + unSignedUri: document.unSignedUri, + caseId, + caseType, + screenName: 'Aadhar PDF viewer', + }); + }; + + return ( + + + {[...Array(3).keys()].map(() => ( + + ))} + + } + > + {documentData.map((document: DocumentDetail) => + document.docContentType !== 'pdf' ? ( + + {document?.icon} + {document?.title} + + } + customExpandUi={{ + whenCollapsed: ( + + + + ), + whenExpanded: ( + + + + ), + }} + scrollByOffset={scrollByOffset} + onExpanded={(value) => { + if (isExpanded !== undefined) { + addClickstreamEvent( + value + ? CLICKSTREAM_EVENT_NAMES.FA_CUSTOMER_DOCUMENT_CLICKED + : CLICKSTREAM_EVENT_NAMES.FA_CUSTOMER_DOCUMENT_CLOSE_CLICKED, + { + agentId: referenceId, + customerId: customerReferenceId, + documentIdentifier: document.docType, + documentType: document.docContentType, + } + ); + } + setIsExpanded(value); + }} + > + + {document.docContentType === 'video' ? ( + + ) : ( + + )} + + + ) : ( + + + {document?.icon} + {document?.title} + + handleOpenPdfPress(document, caseId, caseType)} + style={[GenericStyles.ph16, GenericStyles.pt12]} + > + Open PDF + + + ) + )} + + + ); +}; + +const styles = StyleSheet.create({ + openPdfTxt: { + color: COLORS.TEXT.BLUE, + }, + accordionExpandBtn: { + fontSize: 13, + marginTop: 8, + fontWeight: '500', + lineHeight: 20, + color: COLORS.TEXT.BLUE, + paddingRight: 16, + }, + accordionContentWrapper: { + backgroundColor: '#F7F7F7', + borderBottomLeftRadius: 8, + borderBottomRightRadius: 8, + height: 250, + padding: 16, + }, + rotateBtn: { transform: [{ rotateX: '180deg' }], marginTop: 0 }, + headerWrapper: { + ...GenericStyles.alignCenter, + ...GenericStyles.row, + ...GenericStyles.ph16, + ...GenericStyles.mb12, + }, + accordionStyles: { + ...GenericStyles.pt12, + ...GenericStyles.br8, + ...getShadowStyle(4), + paddingHorizontal: 0, + marginBottom: 16, + }, + pt0: { paddingTop: 0 }, +}); + +export default DocumentDetails; diff --git a/src/screens/caseDetails/DocumentImageComponent.tsx b/src/screens/caseDetails/DocumentImageComponent.tsx new file mode 100644 index 00000000..a7ce206a --- /dev/null +++ b/src/screens/caseDetails/DocumentImageComponent.tsx @@ -0,0 +1,113 @@ +import React, { useState } from 'react'; +import { StyleSheet, TouchableOpacity } from 'react-native'; +import Text from '../../../RN-UI-LIB/src/components/Text'; +import { GenericStyles } from '../../../RN-UI-LIB/src/styles'; +import { COLORS } from '../../../RN-UI-LIB/src/styles/colors'; +import { isNullOrEmptyString } from '../../../RN-UI-LIB/src/utlis/common'; +import CachedImage from '../../common/CachedImage'; +import useS3UrlCheck from '../../hooks/useS3UrlCheck'; +import { CaseAllocationType } from '../allCases/interface'; +import { DocumentDetail, DOCUMENT_TYPE } from './interface'; +import { getCachedImageBase64 } from './utils/documentUtils'; + +interface DocumentImageComponentProps { + document: DocumentDetail; + caseId: string; + caseType: CaseAllocationType; +} + +const DocumentImageComponent = (props: DocumentImageComponentProps) => { + const { document, caseId, caseType } = props; + const { url, docType, unSignedUri, documentRefId } = document; + const [imageUrl, setImageUrl] = useState(url); + const [isValidating, setIsValidating] = useState(false); + const [errorModalImage, setErrorModalImage] = useState({ + [DOCUMENT_TYPE.AADHAR_PHOTO]: false, + [DOCUMENT_TYPE.DRIVING_LICENSE]: false, + }); + + const [loading, setLoading] = useState({ + [DOCUMENT_TYPE.AADHAR_PHOTO]: false, + [DOCUMENT_TYPE.DRIVING_LICENSE]: false, + }); + + const handleExpandImage = () => { + getCachedImageBase64(docType + caseId); + }; + + const onLoadStart = (docType: DOCUMENT_TYPE) => { + setLoading({ + ...loading, + [docType]: true, + }); + setErrorModalImage({ + ...errorModalImage, + [docType]: false, + }); + }; + + const onLoadEnd = (docType: DOCUMENT_TYPE) => { + // added this to show loader for the image is getting painted on screen + setLoading({ + ...loading, + [docType]: false, + }); + }; + + useS3UrlCheck(url, documentRefId, caseId, caseType, unSignedUri, setImageUrl, setIsValidating); + + return ( + <> + + { + setErrorModalImage({ + ...errorModalImage, + [docType]: true, + }); + setLoading({ + ...loading, + [docType]: false, + }); + }} + onLoadStart={() => onLoadStart(docType)} + onLoadEnd={() => onLoadEnd(docType)} + /> + + {loading[docType] && ( + + {!isNullOrEmptyString(imageUrl) ? 'Loading Image...' : 'No Image Found'} + + )} + {errorModalImage[docType] && ( + + Error loading image{isValidating && '. Retrying !!'} + + )} + + ); +}; + +const styles = StyleSheet.create({ + imageStyle: { + height: '100%', + }, + errorText: { + ...GenericStyles.absolute, + top: '50%', + textAlign: 'center', + width: '100%', + color: COLORS.TEXT.RED, + }, + loadingText: { + ...GenericStyles.absolute, + top: '50%', + textAlign: 'center', + width: '100%', + }, +}); + +export default DocumentImageComponent; diff --git a/src/screens/caseDetails/ImageViewer.tsx b/src/screens/caseDetails/ImageViewer.tsx new file mode 100644 index 00000000..18d5b2c8 --- /dev/null +++ b/src/screens/caseDetails/ImageViewer.tsx @@ -0,0 +1,35 @@ +import React from 'react'; +import { View } from 'react-native'; +import { GenericStyles } from '../../../RN-UI-LIB/src/styles'; +import { goBack } from '../../components/utlis/navigationUtlis'; +import ExpandableImage from '../../components/expandableImage/ExpandableImage'; + +interface ImageViewer { + route: { + params: { + imageUrl: string; + headerTitle: string; + }; + }; +} + +const ImageViewer: React.FC = (props) => { + const { + route: { + params: { imageUrl, headerTitle }, + }, + } = props; + + return ( + + + + ); +}; + +export default ImageViewer; diff --git a/src/screens/caseDetails/PDFFullScreen.tsx b/src/screens/caseDetails/PDFFullScreen.tsx new file mode 100644 index 00000000..10c82116 --- /dev/null +++ b/src/screens/caseDetails/PDFFullScreen.tsx @@ -0,0 +1,129 @@ +import React, { useEffect, useState } from 'react'; +import { ActivityIndicator, SafeAreaView, StyleSheet, View } from 'react-native'; +import RNFetchBlob from 'react-native-blob-util'; +import RNFS from 'react-native-fs'; +import NavigationHeader, { Icon } from '../../../RN-UI-LIB/src/components/NavigationHeader'; +import { GenericStyles, SCREEN_HEIGHT, SCREEN_WIDTH } from '../../../RN-UI-LIB/src/styles'; +import { COLORS } from '../../../RN-UI-LIB/src/styles/colors'; +import { goBack } from '../../components/utlis/navigationUtlis'; +import Text from '../../../RN-UI-LIB/src/components/Text'; +import PdfRendererView from 'react-native-pdf-renderer'; +import { CaseAllocationType } from '../allCases/interface'; +import useS3UrlCheck from '../../hooks/useS3UrlCheck'; + +interface IPdfFullScreen { + route: { + params: { + pdfUri: string; + referenceId: string; + cacheFileKey: string; + screenName: string; + caseId: string; + caseType: CaseAllocationType; + unSignedUri: string; + }; + }; +} + +const PDFFullScreen: React.FC = (props) => { + const { + route: { + params: { pdfUri, referenceId, cacheFileKey, screenName, caseId, caseType, unSignedUri }, + }, + } = props; + + const [isLoading, setIsLoading] = useState(false); + const [pdfFilePath, setPdfFilePath] = useState(''); + const [error, setError] = useState(false); + const [pdfUrl, setPdfUrl] = useState(pdfUri); + const [isValidating, setIsValidating] = useState(false); + + useS3UrlCheck(pdfUri, referenceId, caseId, caseType, unSignedUri, setPdfUrl, setIsValidating); + + useEffect(() => { + const cacheDirectory = RNFS.CachesDirectoryPath; + const sanitizedHighQualityUri = cacheFileKey; + const cacheFilePath = `${cacheDirectory}/${sanitizedHighQualityUri}.pdf`; + const fetchPdf = async () => { + setIsLoading(true); + setError(false); + try { + await RNFS.mkdir(cacheDirectory); + const exists = await RNFS.exists(cacheFilePath); + + if (exists) { + setPdfFilePath(cacheFilePath); + setIsLoading(false); + setError(false); + return; + } + const highQualityResponse = await RNFetchBlob.fetch('GET', pdfUrl); + + if (highQualityResponse.respInfo.status !== 200) { + setIsLoading(false); + setError(true); + } else if (highQualityResponse.respInfo.status === 200) { + const highQualityImageBase64 = await highQualityResponse.base64(); + RNFS.writeFile(cacheFilePath, highQualityImageBase64, 'base64'); + setPdfFilePath(cacheFilePath); + setIsLoading(false); + setError(false); + } + } catch (error) { + setIsLoading(false); + setError(true); + } + }; + + fetchPdf(); + }, [pdfUrl, cacheFileKey]); + + return ( + + + {pdfFilePath && ( + + + + )} + + {isLoading && ( + + )} + {error && ( + Error loading PDF{isValidating && '. Retrying !!'} + )} + + ); +}; + +export default PDFFullScreen; + +const styles = StyleSheet.create({ + container: { + flex: 1, + }, + pdf: { + flex: 1, + width: SCREEN_WIDTH, + height: SCREEN_HEIGHT, + backgroundColor: COLORS.BACKGROUND.GREY_LIGHT_2, + }, + errorText: { + ...GenericStyles.absolute, + top: '50%', + textAlign: 'center', + width: '100%', + color: COLORS.TEXT.RED, + }, +}); diff --git a/src/screens/caseDetails/interface.ts b/src/screens/caseDetails/interface.ts index 33050698..9236728c 100644 --- a/src/screens/caseDetails/interface.ts +++ b/src/screens/caseDetails/interface.ts @@ -1,3 +1,4 @@ +import { IGeolocationCoordinate } from '@interfaces/addressGeolocation.types'; import { CaseAllocationType, CaseStatuses, @@ -80,6 +81,7 @@ export interface Address { permanent: boolean; zipCode?: any; addressQualityStatus?: any; + location: IGeolocationCoordinate; } export interface Metadata { '@class': string; @@ -139,6 +141,10 @@ export enum DOCUMENT_TYPE { SELFIE = 'SELFIE', OPTIMIZED_SELFIE = 'OPTIMIZED_SELFIE', VKYC_VIDEO = 'VKYC_VIDEO', + PAN = 'PAN', + AADHAR = 'AADHAR', + AADHAR_PHOTO = 'aadhar_photo', + DRIVING_LICENSE = 'DRIVING_LICENSE', } export type TDocumentObj = { @@ -149,6 +155,18 @@ export interface IDocument { referenceId: string; uri: string; type: DOCUMENT_TYPE; + docContentType: string; + unSignedUri: string; +} + +export interface DocumentDetail { + icon: React.ReactNode; + title: string; + docType: DOCUMENT_TYPE; + docContentType: string; + url: string; + unSignedUri: string; + documentRefId: string; } export enum FeedbackStatus { @@ -331,3 +349,7 @@ export enum PhoneNumberSource { CRIF = 'CRIF', CIBIL_CU = 'CIBIL_CU', } + +export interface INearbyCaseItemObj extends CaseDetail { + distanceInKm: number; +} diff --git a/src/screens/caseDetails/utils/VKYCVideo.tsx b/src/screens/caseDetails/utils/VKYCVideo.tsx new file mode 100644 index 00000000..aa77deb8 --- /dev/null +++ b/src/screens/caseDetails/utils/VKYCVideo.tsx @@ -0,0 +1,22 @@ +import React, { useState } from 'react'; +import WebView from 'react-native-webview'; +import useS3UrlCheck from '../../../hooks/useS3UrlCheck'; +import { vkycHtml } from '../vkycTemplate'; + +const VKYCVideo = ({ document, caseId, caseType }: any) => { + const { url, unSignedUri, documentRefId } = document; + const [videoUrl, setVideoUrl] = useState(url); + + useS3UrlCheck(url, documentRefId, caseId, caseType, unSignedUri, setVideoUrl); + + return ( + + ); +}; + +export default VKYCVideo; diff --git a/src/screens/caseDetails/utils/documentUtils.tsx b/src/screens/caseDetails/utils/documentUtils.tsx new file mode 100644 index 00000000..9eaa9ff0 --- /dev/null +++ b/src/screens/caseDetails/utils/documentUtils.tsx @@ -0,0 +1,81 @@ +import ImageIcon from '../../../../RN-UI-LIB/src/Icons/ImageIcon'; +import PdfIcon from '../../../../RN-UI-LIB/src/Icons/PdfIcon'; +import VideoIcon from '../../../../RN-UI-LIB/src/Icons/VideoIcon'; +import { CaseAllocationType } from '../../allCases/interface'; +import { DOCUMENT_TYPE, IDocument } from '../interface'; +import RNFS from 'react-native-fs'; +import { navigateToScreen } from '../../../components/utlis/navigationUtlis'; + +const getDocumentType = (docContentType: string) => { + if (!docContentType) return 'image'; + if (docContentType.includes('image')) return 'image'; + if (docContentType.includes('pdf')) return 'pdf'; + + return null; +}; + +export const getDocumentDetails = async (document: IDocument) => { + if (!document.referenceId) { + return null; + } + if (document?.uri) { + try { + const imageUrl = document.uri; + if (!imageUrl) return null; + const docType = getDocumentType(document?.docContentType); + if (!docType) return null; + switch (document.type) { + case DOCUMENT_TYPE.VKYC_VIDEO: + return { + icon: , + title: 'VKYC video', + docType: DOCUMENT_TYPE.VKYC_VIDEO, + docContentType: 'video', + url: imageUrl, + unSignedUri: document.unSignedUri, + documentRefId: document.referenceId, + }; + + case DOCUMENT_TYPE.AADHAR_PHOTO: + return { + icon: , + title: 'Aadhar photo', + docType: DOCUMENT_TYPE.AADHAR_PHOTO, + docContentType: docType, + url: imageUrl, + unSignedUri: document.unSignedUri, + documentRefId: document.referenceId, + }; + + case DOCUMENT_TYPE.DRIVING_LICENSE: + return { + icon: , + title: 'Driving license', + docType: DOCUMENT_TYPE.DRIVING_LICENSE, + docContentType: docType, + url: imageUrl, + unSignedUri: document.unSignedUri, + documentRefId: document.referenceId, + }; + + default: + break; + } + } catch (error) { + return null; + } + } + return null; +}; + +export const getCachedImageBase64 = (cacheKey: string) => { + const cacheDirectory = RNFS.CachesDirectoryPath; + const cacheFilePath = `${cacheDirectory}/${cacheKey}.jpg`; + + RNFS.readFile(cacheFilePath, 'base64').then((data) => { + navigateToScreen('imageFull', { + imageUrl: `data:image/png;base64,${data}`, + headerTitle: 'Image Viewer', + }); + }); +}; diff --git a/src/screens/permissions/BlockerScreenApps.tsx b/src/screens/permissions/BlockerScreenApps.tsx new file mode 100644 index 00000000..045833d3 --- /dev/null +++ b/src/screens/permissions/BlockerScreenApps.tsx @@ -0,0 +1,107 @@ +import * as React from 'react'; +import { View, StyleSheet, ScrollView, Image } from 'react-native'; +import { GenericStyles } from '../../../RN-UI-LIB/src/styles'; +import { COLORS } from '@rn-ui-lib/colors'; +import PermissionImage from '@assets/images/PermissionImage'; +import { Apps } from '@services/blacklistedApps.service'; +import Text from '@rn-ui-lib/components/Text'; + +const BlockerScreenApps: React.FC<{ blacklistedAppsInstalled: Apps[] }> = ({ + blacklistedAppsInstalled, +}) => { + return ( + + + + + + + + Some installed apps are banned by Navi + + + + + + Uninstall these apps for uninterrupted usage + + + + + + {blacklistedAppsInstalled.map((app: Apps, index: number) => ( + + + + {app.applicationName} + + {index < blacklistedAppsInstalled.length - 1 && ( + + )} + + ))} + + + + ); +}; + +const styles = StyleSheet.create({ + container: { + flexDirection: 'column', + flex: 1, + backgroundColor: COLORS.BACKGROUND.PRIMARY, + }, + + imageContainer: { + marginLeft: 97, + marginTop: 40, + marginBottom: 37, + }, + + textDark: { + color: COLORS.TEXT.DARK, + fontWeight: '500', + }, + + textLight: { + color: COLORS.TEXT.LIGHT, + fontWeight: '400', + marginBottom: 14, + }, + + appsContainer: { + paddingHorizontal: 16, + paddingTop: 4, + borderRadius: 4, + marginBottom: 16, + backgroundColor: COLORS.BACKGROUND.SILVER_LIGHT, + }, + + appsListItem: { + flexDirection: 'row', + alignItems: 'center', + paddingHorizontal: 8, + paddingVertical: 12, + }, + + appNameText: { + marginLeft: 8, + fontSize: 12, + }, + + appIcon: { + width: 35, + height: 35, + backgroundColor: COLORS.BACKGROUND.PRIMARY, + borderRadius: 20, + }, + + horizontalLine: { + backgroundColor: COLORS.BACKGROUND.SILVER_LIGHT_2, + width: '100%', + height: 1, + }, +}); + +export default BlockerScreenApps; diff --git a/src/services/blacklistedApps.service.ts b/src/services/blacklistedApps.service.ts new file mode 100644 index 00000000..f3f87855 --- /dev/null +++ b/src/services/blacklistedApps.service.ts @@ -0,0 +1,68 @@ +import { GenericType } from '@common/GenericTypes'; +import { getAllInstalledApp } from '@components/utlis/DeviceUtils'; +import { logError } from '@components/utlis/errorUtils'; + +export type Apps = { + packageName: string; + applicationName: string; + applicationIcon: string; +}; + +type deviceApps = { + packageName: string; + appDetails: deviceAppDetails; +}; + +type deviceAppDetails = { + applicationName: string; + applicationIcon: string; +}; + +export let BLACKLISTED_APPS_LIST: string[] = []; +let installedBlacklistedApps: Apps[] = []; + +export const getBlacklistedAppsList = () => BLACKLISTED_APPS_LIST; + +export const setBlacklistedAppsList = (blacklistedAppsString: string) => { + BLACKLISTED_APPS_LIST = blacklistedAppsString.split(','); +}; + +function getBlacklistAppsPresent(installedApps: Apps[], blacklistedApps: string[]) { + installedBlacklistedApps = []; + const blacklistedAppsSet = new Set(blacklistedApps); + for (const app of installedApps) { + if (blacklistedAppsSet.has(app.packageName)) { + installedBlacklistedApps.push({ + packageName: app.packageName, + applicationName: app.applicationName, + applicationIcon: app.applicationIcon, + }); + } + } +} + +const handleBlacklistedAppsForBlockingCosmos = async () => { + const blacklistedApps = getBlacklistedAppsList(); + return getAllInstalledApp() + .then((apps) => { + try { + const appsArray = JSON.parse(apps); + const installedApps = appsArray.map((app: deviceApps) => ({ + packageName: app.packageName, + applicationName: app.appDetails.applicationName, + applicationIcon: app.appDetails.applicationIcon, + })); + getBlacklistAppsPresent(installedApps, blacklistedApps); + return installedBlacklistedApps; + } catch (error: GenericType) { + logError(error); + return []; + } + }) + .catch((error) => { + logError(error); + return []; + }); +}; + +export default handleBlacklistedAppsForBlockingCosmos; diff --git a/src/services/firebaseFetchAndUpdate.service.ts b/src/services/firebaseFetchAndUpdate.service.ts index 1816fb35..eddb8706 100644 --- a/src/services/firebaseFetchAndUpdate.service.ts +++ b/src/services/firebaseFetchAndUpdate.service.ts @@ -4,9 +4,11 @@ import { setActivityTimeWindowHigh, setActivityTimeWindowMedium, } from '../common/AgentActivityConfigurableConstants'; +import { setBlacklistedAppsList } from './blacklistedApps.service'; const FIREBASE_FETCH_TIME = 15 * 60; -async function handleUpdatedConfigureValuesFromFirebase() { +export let FIREBASE_FETCH_TIMESTAMP: number; +async function fetchUpdatedRemoteConfig() { await remoteConfig().fetch(FIREBASE_FETCH_TIME); //15 minutes remoteConfig() .activate() @@ -28,10 +30,13 @@ async function handleUpdatedConfigureValuesFromFirebase() { const ACTIVITY_TIME_WINDOW_MEDIUM = remoteConfig() .getValue('ACTIVITY_TIME_WINDOW_MEDIUM') .asNumber(); + const BLACKLISTED_APPS = remoteConfig().getValue('BLACKLISTED_APPS').asString(); setActivityTimeOnApp(ACTIVITY_TIME_ON_APP); setActivityTimeWindowHigh(ACTIVITY_TIME_WINDOW_HIGH); setActivityTimeWindowMedium(ACTIVITY_TIME_WINDOW_MEDIUM); + setBlacklistedAppsList(BLACKLISTED_APPS); + FIREBASE_FETCH_TIMESTAMP = Date.now(); }); } -export default handleUpdatedConfigureValuesFromFirebase; +export default fetchUpdatedRemoteConfig; diff --git a/src/services/foregroundServices/foreground.service.ts b/src/services/foregroundServices/foreground.service.ts index f40965ba..575ad1bc 100644 --- a/src/services/foregroundServices/foreground.service.ts +++ b/src/services/foregroundServices/foreground.service.ts @@ -1,6 +1,8 @@ import ForegroundService from '@supersami/rn-foreground-service'; import { logError } from '../../components/utlis/errorUtils'; import { GLOBAL } from '../../constants/Global'; +import { AppState } from 'react-native'; +import { AppStates } from '@interfaces/appStates'; export interface IForegroundTask { task: () => void; @@ -34,6 +36,9 @@ class CosmosForegroundService { private constructor() {} static async start(tasks?: IForegroundTask[]) { + if (AppState.currentState !== AppStates.ACTIVE) { + return; + } if (GLOBAL.IS_IMPERSONATED) { return; } @@ -60,6 +65,9 @@ class CosmosForegroundService { } static async update() { + if (AppState.currentState !== AppStates.ACTIVE) { + return; + } if (GLOBAL.IS_IMPERSONATED) { return; } diff --git a/src/store/store.ts b/src/store/store.ts index 43ee523f..44cca84a 100644 --- a/src/store/store.ts +++ b/src/store/store.ts @@ -31,6 +31,7 @@ import feedbackImagesSlice from '../reducer/feedbackImagesSlice'; import configSlice from '../reducer/configSlice'; import profileSlice from '../reducer/profileSlice'; import reporteesSlice from '../reducer/reporteesSlice'; +import blacklistedAppsInstalledSlice from '@reducers/blacklistedAppsInstalledSlice'; import feedbackFiltersSlice from '@reducers/feedbackFiltersSlice'; import agentPerformanceSlice from '../reducer/agentPerformanceSlice'; @@ -54,6 +55,7 @@ const rootReducer = combineReducers({ config: configSlice, profile: profileSlice, reportees: reporteesSlice, + blacklistAppsInstalled: blacklistedAppsInstalledSlice, feedbackFilters: feedbackFiltersSlice, agentPerformance: agentPerformanceSlice, }); diff --git a/src/types/addressGeolocation.types.ts b/src/types/addressGeolocation.types.ts index 7ab0584e..04fe80eb 100644 --- a/src/types/addressGeolocation.types.ts +++ b/src/types/addressGeolocation.types.ts @@ -28,6 +28,13 @@ export enum PrimarySourcesType { ACCOUNT_AGGREGATOR = 'ACCOUNT_AGGREGATOR', } +interface IMetaAddress { + addressId: string; + promptVersionId: string; + runId: string; + taggedAt: string; +} + export interface IAddress { id: string; pinCode: string; @@ -41,6 +48,7 @@ export interface IAddress { groupId: string; primarySource?: PrimarySourcesType; secondarySource?: string; + metaAddressReferences: IMetaAddress[]; similarAddresses?: IAddress[]; } diff --git a/tsconfig.json b/tsconfig.json index d9248cf8..9a4b2625 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -23,7 +23,7 @@ "@constants/*": ["src/constants/*"], "@screens/*": ["src/screens/*"], "@services/*": ["src/services/*"], - "@types/*": ["src/types/*"], + "@interfaces/*": ["src/types/*"], "@common/*": ["src/common/*"], "@assets/*": ["src/assets/*"], "@store": ["src/store/store"],