TP-00000 | synced with master
Merge branch 'master' of github.com:navi-medici/address-verification-app into WhatsappFeedbackEnhancment
This commit is contained in:
6
.github/workflows/newBuild.yml
vendored
6
.github/workflows/newBuild.yml
vendored
@@ -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
|
||||
|
||||
13
App.tsx
13
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 () => {
|
||||
|
||||
Submodule RN-UI-LIB updated: 9f4a3ae267...64db024f2d
@@ -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
|
||||
|
||||
@@ -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<ApplicationInfo> 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 {
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -167,6 +167,7 @@ interface ISignedRequestItem {
|
||||
documentReferenceId: string;
|
||||
caseType: CaseAllocationType;
|
||||
caseId: string;
|
||||
unSignedUri?: string;
|
||||
}
|
||||
export type ISignedRequest = ISignedRequestItem[];
|
||||
|
||||
|
||||
@@ -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 <BlockerScreenApps blacklistedAppsInstalled={blacklistedAppsInstalled} />;
|
||||
}
|
||||
|
||||
return <>{props.children}</>;
|
||||
};
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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<ITrackingComponent> = ({ 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<ITrackingComponent> = ({ 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<ITrackingComponent> = ({ 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<ITrackingComponent> = ({ 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<ITrackingComponent> = ({ 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<ITrackingComponent> = ({ 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<ITrackingComponent> = ({ 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) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -48,7 +48,9 @@ const FiltersContainer: React.FC<FilterContainerProps> = (props) => {
|
||||
filterKeys &&
|
||||
filterKeys[filterGroupKeys[0]][0]) ||
|
||||
'',
|
||||
isSearchable: false,
|
||||
isSearchable:
|
||||
filters?.[filterGroupKeys?.[0]]?.filters?.[filterKeys?.[filterGroupKeys?.[0]]?.[0]]
|
||||
?.searchEnabled ?? false,
|
||||
});
|
||||
const [filterSearchString, setFilterSearchString] = React.useState<string>('');
|
||||
const dispatch = useAppDispatch();
|
||||
@@ -150,7 +152,7 @@ const FiltersContainer: React.FC<FilterContainerProps> = (props) => {
|
||||
]}
|
||||
>
|
||||
<Heading type="h5" bold dark>
|
||||
{filters[filterGroupKey].headerText}
|
||||
{filters?.[filterGroupKey]?.headerText}
|
||||
</Heading>
|
||||
</View>
|
||||
)}
|
||||
@@ -167,11 +169,13 @@ const FiltersContainer: React.FC<FilterContainerProps> = (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, {
|
||||
|
||||
@@ -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);
|
||||
|
||||
5
src/components/utlis/ScreenshotBlocker.ts
Normal file
5
src/components/utlis/ScreenshotBlocker.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { NativeModules } from 'react-native';
|
||||
|
||||
const { ScreenshotBlocker } = NativeModules;
|
||||
|
||||
export default ScreenshotBlocker;
|
||||
@@ -239,8 +239,8 @@ export const findDocumentByDocumentType = (
|
||||
|
||||
export const checkS3Url = async (url: string): Promise<boolean> => {
|
||||
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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -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 };
|
||||
|
||||
41
src/hooks/useS3UrlCheck.ts
Normal file
41
src/hooks/useS3UrlCheck.ts
Normal file
@@ -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;
|
||||
21
src/reducer/blacklistedAppsInstalledSlice.ts
Normal file
21
src/reducer/blacklistedAppsInstalledSlice.ts
Normal file
@@ -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;
|
||||
@@ -28,6 +28,28 @@ function SeparatorBorderComponent() {
|
||||
return <View style={[styles.borderLine, GenericStyles.mv16]} />;
|
||||
}
|
||||
|
||||
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<IAddressContainer> = ({
|
||||
groupedAddressList,
|
||||
caseId,
|
||||
@@ -38,10 +60,10 @@ const AddressContainer: React.FC<IAddressContainer> = ({
|
||||
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<IAddressContainer> = ({
|
||||
addressFeedback?.addressReferenceId === groupedAddress?.metaAddress?.id
|
||||
);
|
||||
return (
|
||||
<View>
|
||||
<View key={groupedAddress?.metaAddress?.id}>
|
||||
<Accordion
|
||||
accordionStyle={[GenericStyles.pv24, GenericStyles.ph16]}
|
||||
isExpansionDisabled={!groupedAddress?.similarAddresses.length}
|
||||
|
||||
@@ -73,16 +73,15 @@ function AddressItem({
|
||||
|
||||
let relativeDistanceBwLatLong = 0;
|
||||
|
||||
if (isGroupedAddress) {
|
||||
const addressGeolocationCoordinated: IGeolocationCoordinate = {
|
||||
latitude: addressItem.latitude,
|
||||
longitude: addressItem.longitude,
|
||||
};
|
||||
relativeDistanceBwLatLong = getDistanceFromLatLonInKm(
|
||||
currentGeolocationCoordinates,
|
||||
addressGeolocationCoordinated
|
||||
);
|
||||
}
|
||||
const addressGeolocationCoordinated: IGeolocationCoordinate = {
|
||||
latitude: addressItem.latitude,
|
||||
longitude: addressItem.longitude,
|
||||
};
|
||||
relativeDistanceBwLatLong = getDistanceFromLatLonInKm(
|
||||
currentGeolocationCoordinates,
|
||||
addressGeolocationCoordinated
|
||||
);
|
||||
|
||||
const handleAddFeedback = () => {
|
||||
if (prefilledAddressScreenTemplate != null) {
|
||||
const addressKey = '{{addressReferenceId}}';
|
||||
@@ -138,22 +137,24 @@ function AddressItem({
|
||||
<View
|
||||
style={[styles.container, GenericStyles.columnDirection, containerStyle, , { flex: 1 }]}
|
||||
>
|
||||
<View style={[styles.container, GenericStyles.row, { alignItems: 'center' }]}>
|
||||
<Text
|
||||
numberOfLines={1}
|
||||
ellipsizeMode="tail"
|
||||
style={[styles.textContainer, styles.cardBoldTitle, { fontWeight: 'bold' }]}
|
||||
>
|
||||
{sanitizeString([addressItem?.pinCode, addressItem?.city].filter(Boolean).join(', '))}
|
||||
</Text>
|
||||
<Text numberOfLines={1} ellipsizeMode="tail" style={[GenericStyles.ml4]}>
|
||||
{showRelativeDistance && relativeDistanceBwLatLong ? (
|
||||
<>({relativeDistanceFormatter(relativeDistanceBwLatLong)} km away)</>
|
||||
) : (
|
||||
'--'
|
||||
)}
|
||||
</Text>
|
||||
</View>
|
||||
{addressItem?.pinCode || addressItem?.city || relativeDistanceBwLatLong ? (
|
||||
<View style={[styles.container, GenericStyles.row, { alignItems: 'center' }]}>
|
||||
<Text
|
||||
numberOfLines={1}
|
||||
ellipsizeMode="tail"
|
||||
style={[styles.textContainer, styles.cardBoldTitle, { fontWeight: 'bold' }]}
|
||||
>
|
||||
{sanitizeString(
|
||||
[addressItem?.pinCode ?? '', addressItem?.city ?? ''].filter(Boolean).join(', ')
|
||||
)}
|
||||
</Text>
|
||||
<Text numberOfLines={1} ellipsizeMode="tail" style={[GenericStyles.ml4]}>
|
||||
{showRelativeDistance && relativeDistanceBwLatLong ? (
|
||||
<>({relativeDistanceFormatter(relativeDistanceBwLatLong)} km away)</>
|
||||
) : null}
|
||||
</Text>
|
||||
</View>
|
||||
) : null}
|
||||
{lastFeedbackForAddress?.feedbackPresent ? (
|
||||
<View style={[styles.container, { marginVertical: 8 }]}>
|
||||
<View>
|
||||
|
||||
@@ -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 ? (
|
||||
<>
|
||||
<Text style={GenericStyles.tiny}> ● </Text>
|
||||
{!isNaN(relativeDistanceBwLatLong) ? relativeDistanceBwLatLong.toFixed(2) : '--'} km
|
||||
away
|
||||
{!isNaN(relativeDistanceBwLatLong)
|
||||
? relativeDistanceFormatter(relativeDistanceBwLatLong)
|
||||
: '--'}{' '}
|
||||
km away
|
||||
</>
|
||||
) : null}
|
||||
{showSource ? <AddressSource addressItem={addressItem} /> : null}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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<ICaseItemProps> = ({
|
||||
@@ -26,9 +33,10 @@ const CaseItem: React.FC<ICaseItemProps> = ({
|
||||
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<ICaseItemProps> = ({
|
||||
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<ICaseItemProps> = ({
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
case NEARBY_CASES: {
|
||||
return (
|
||||
<Pressable onPress={navigateToNearbyCases}>
|
||||
<View style={styles.nearByCasesContainer}>
|
||||
<View style={[GenericStyles.row, GenericStyles.alignCenter]}>
|
||||
<LocationIcon />
|
||||
<Text style={styles.nearByCasesText}>View nearby cases</Text>
|
||||
</View>
|
||||
<ArrowRightOutlineIcon fillColor={COLORS.BACKGROUND.LIGHT} />
|
||||
</View>
|
||||
</Pressable>
|
||||
);
|
||||
}
|
||||
default:
|
||||
return (
|
||||
<View {...restProps}>
|
||||
@@ -91,6 +117,7 @@ const CaseItem: React.FC<ICaseItemProps> = ({
|
||||
isTodoItem={isTodoItem}
|
||||
allCasesView={allCasesView}
|
||||
isAgentDashboard={isAgentDashboard}
|
||||
nearbyCaseView={nearbyCaseView}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
@@ -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;
|
||||
|
||||
@@ -211,6 +211,8 @@ const CasesList: React.FC<ICasesList> = ({
|
||||
|
||||
const filteredCasesListWithCTA = useMemo(() => {
|
||||
if (!isVisitPlan) {
|
||||
if (allCasesView && filteredCasesList?.length)
|
||||
return [ListHeaderItems.NEARBY_CASES as ICaseItem, ...filteredCasesList];
|
||||
return [...filteredCasesList];
|
||||
}
|
||||
if (isLockedVisitPlanStatus) {
|
||||
|
||||
@@ -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<ViewStyle>;
|
||||
isNearByCase?: boolean;
|
||||
}
|
||||
|
||||
const EmptyList: React.FC<IEmptyList> = (props) => {
|
||||
@@ -31,6 +33,8 @@ const EmptyList: React.FC<IEmptyList> = (props) => {
|
||||
isFilterApplied,
|
||||
setShowAgentSelectionBottomSheet,
|
||||
isAgentDashboard,
|
||||
containerStyle,
|
||||
isNearByCase,
|
||||
} = props;
|
||||
const {
|
||||
isLockedVisitPlanStatus,
|
||||
@@ -85,6 +89,9 @@ const EmptyList: React.FC<IEmptyList> = (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<IEmptyList> = (props) => {
|
||||
|
||||
return (
|
||||
<View>
|
||||
<View style={[GenericStyles.w100, styles.centerAbsolute]}>
|
||||
<View style={[GenericStyles.w100, styles.centerAbsolute, containerStyle]}>
|
||||
{renderIcon()}
|
||||
<View style={[GenericStyles.mt12, styles.text]}>
|
||||
<Heading
|
||||
|
||||
@@ -33,6 +33,7 @@ import { toast } from '../../../RN-UI-LIB/src/components/toast';
|
||||
import { COMPLETED_STATUSES, ToastMessages } from './constants';
|
||||
import { VisitPlanStatus } from '../../reducer/userSlice';
|
||||
import { PaymentStatus } from '../caseDetails/interface';
|
||||
import relativeDistanceFormatter from '@screens/addressGeolocation/utils/relativeDistanceFormatter';
|
||||
|
||||
interface IListItem {
|
||||
caseListItemDetailObj: ICaseItemCaseDetailObj;
|
||||
@@ -41,6 +42,7 @@ interface IListItem {
|
||||
shouldBatchAvatar?: boolean;
|
||||
allCasesView?: boolean;
|
||||
isAgentDashboard?: boolean;
|
||||
nearbyCaseView?: boolean;
|
||||
}
|
||||
|
||||
const paymentStatusMapping: Record<
|
||||
@@ -63,7 +65,7 @@ const ListItem: React.FC<IListItem> = (props) => {
|
||||
isTodoItem,
|
||||
shouldBatchAvatar,
|
||||
allCasesView,
|
||||
isAgentDashboard,
|
||||
nearbyCaseView,
|
||||
} = props;
|
||||
const {
|
||||
id: caseId,
|
||||
@@ -79,6 +81,7 @@ const ListItem: React.FC<IListItem> = (props) => {
|
||||
interactionStatus,
|
||||
caseVerdict,
|
||||
totalOverdueAmount,
|
||||
distanceInKm,
|
||||
} = caseListItemDetailObj;
|
||||
|
||||
const isCollectionCaseType = caseType === CaseAllocationType.COLLECTION_CASE;
|
||||
@@ -128,6 +131,11 @@ const ListItem: React.FC<IListItem> = (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<IListItem> = (props) => {
|
||||
const caseCompleted = COMPLETED_STATUSES.includes(caseStatus);
|
||||
|
||||
const showVisitPlanBtn =
|
||||
!(caseCompleted || isCaseItemPinnedMainView) && !isTodoItem && !isCompleted && !isTeamLead;
|
||||
!(caseCompleted || isCaseItemPinnedMainView) &&
|
||||
!isTodoItem &&
|
||||
!isCompleted &&
|
||||
!isTeamLead &&
|
||||
!nearbyCaseView;
|
||||
|
||||
return (
|
||||
<Pressable onPress={handleCaseClick}>
|
||||
@@ -211,6 +223,13 @@ const ListItem: React.FC<IListItem> = (props) => {
|
||||
<Text style={[GenericStyles.fontSize12, styles.visitPlanText]}>In visit plan</Text>
|
||||
</View>
|
||||
)}
|
||||
{nearbyCaseView && distanceInKm && (
|
||||
<View style={[GenericStyles.absolute, styles.distanceContainer]}>
|
||||
<Text style={[GenericStyles.fontSize12, styles.distanceText]}>
|
||||
{relativeDistanceFormatter(distanceInKm)} km away
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
<View style={[styles.caseItemInfo]}>
|
||||
<View style={styles.tag}>
|
||||
{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);
|
||||
|
||||
100
src/screens/allCases/NearbyCases.tsx
Normal file
100
src/screens/allCases/NearbyCases.tsx
Normal file
@@ -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<Array<INearbyCaseItemObj>>([]);
|
||||
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<ICaseItem>) => {
|
||||
const caseDetailItem = row.item as INearbyCaseItemObj;
|
||||
const { type } = row.item;
|
||||
return (
|
||||
<CaseItem
|
||||
key={caseDetailItem?.distanceInKm}
|
||||
renderingType={type}
|
||||
caseDetailObj={caseDetailItem}
|
||||
shouldBatchAvatar={true}
|
||||
testID={`case-${type === CaseTypes.TODO ? 'todo' : ''}-${row.index}`}
|
||||
nearbyCaseView
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<View style={[GenericStyles.fill, GenericStyles.relative]}>
|
||||
<NavigationHeader title="Nearby Cases" onBack={goBack} />
|
||||
<View style={GenericStyles.fill}>
|
||||
{caseData.length ? (
|
||||
<FlashList
|
||||
data={caseData}
|
||||
keyboardShouldPersistTaps={'handled'}
|
||||
scrollEventThrottle={16}
|
||||
renderItem={renderListItem}
|
||||
refreshControl={
|
||||
<RefreshControl refreshing={refreshing} onRefresh={handlePullToRefresh} />
|
||||
}
|
||||
contentContainerStyle={GenericStyles.p12}
|
||||
estimatedItemSize={ESTIMATED_ITEM_SIZE}
|
||||
estimatedListSize={ESTIMATED_LIST_SIZE}
|
||||
/>
|
||||
) : (
|
||||
<View style={GenericStyles.ph12}>
|
||||
<EmptyList containerStyle={styles.pt0} isNearByCase />
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
pt0: {
|
||||
paddingTop: 0,
|
||||
},
|
||||
});
|
||||
|
||||
export default NearbyCases;
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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<ICaseItem>,
|
||||
caseDetails: Record<string, CaseDetail>,
|
||||
deviceGeolocationCoordinate: IGeoLocation
|
||||
) => {
|
||||
let caseDetailsData: Array<INearbyCaseItemObj> = [];
|
||||
let caseIds: Array<string> = [];
|
||||
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);
|
||||
};
|
||||
|
||||
@@ -108,7 +108,9 @@ const AuthRouter = () => {
|
||||
</BlockerScreen>
|
||||
</TrackingComponent>
|
||||
) : (
|
||||
<UnProtectedRouter />
|
||||
<BlockerScreen>
|
||||
<UnProtectedRouter />
|
||||
</BlockerScreen>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name={PageRouteEnum.NEARBY_CASES}
|
||||
component={NearbyCases}
|
||||
options={{
|
||||
header: () => null,
|
||||
animationDuration: SCREEN_ANIMATION_DURATION,
|
||||
animation: 'slide_from_right',
|
||||
}}
|
||||
listeners={getScreenFocusListenerObj}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name={'vkycFull'}
|
||||
component={VKYCFullScreen}
|
||||
@@ -201,6 +215,26 @@ const ProtectedRouter = () => {
|
||||
}}
|
||||
listeners={getScreenFocusListenerObj}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name={'pdfFull'}
|
||||
component={PDFFullScreen}
|
||||
options={{
|
||||
header: () => null,
|
||||
animationDuration: SCREEN_ANIMATION_DURATION,
|
||||
animation: 'none',
|
||||
}}
|
||||
listeners={getScreenFocusListenerObj}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name={'imageFull'}
|
||||
component={ImageViewer}
|
||||
options={{
|
||||
header: () => null,
|
||||
animationDuration: SCREEN_ANIMATION_DURATION,
|
||||
animation: 'none',
|
||||
}}
|
||||
listeners={getScreenFocusListenerObj}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name={PageRouteEnum.PAYMENTS}
|
||||
component={RegisterPayments}
|
||||
|
||||
@@ -41,6 +41,8 @@ import { addClickstreamEvent } from '../../services/clickstreamEventService';
|
||||
import { getLoanAccountNumber } from '../../components/utlis/commonFunctions';
|
||||
import EmiBreakupBottomSheet from '../emiSchedule/EmiBreakupBottomSheet';
|
||||
import { CollectionCaseWidgetId } from '../../types/template.types';
|
||||
import { useFocusEffect } from '@react-navigation/native';
|
||||
import ScreenshotBlocker from '../../components/utlis/ScreenshotBlocker';
|
||||
import { useIsFocused } from '@react-navigation/native';
|
||||
import { setSelectedCaseId } from '../../reducer/allCasesSlice';
|
||||
|
||||
@@ -232,6 +234,10 @@ const CollectionCaseDetails: React.FC<ICaseDetails> = (props) => {
|
||||
});
|
||||
};
|
||||
|
||||
useFocusEffect(() => {
|
||||
ScreenshotBlocker.unblockScreenshots();
|
||||
});
|
||||
|
||||
return (
|
||||
<Layout>
|
||||
<SafeAreaView style={[GenericStyles.fill, GenericStyles.whiteBackground]}>
|
||||
|
||||
@@ -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<ICustomerProfile> = (props) => {
|
||||
const [retry, setRetry] = useState(true);
|
||||
const [errorCount, setErrorCount] = useState<number>(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<ICustomerProfile> = (props) => {
|
||||
false,
|
||||
false
|
||||
);
|
||||
const [vkycUri, setVkycUri] = useState<string>();
|
||||
const [showVkyc, setShowVkyc] = useState<boolean>(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<Array<DocumentDetail>>([]);
|
||||
|
||||
const getImageURI = () => {
|
||||
if (!isOnline) {
|
||||
@@ -114,17 +87,42 @@ const CustomerProfile: React.FC<ICustomerProfile> = (props) => {
|
||||
);
|
||||
};
|
||||
|
||||
const handleVkycPress = () => {
|
||||
navigateToScreen('vkycFull', {
|
||||
vkycUri,
|
||||
});
|
||||
};
|
||||
useEffect(() => {
|
||||
if (caseDetail) {
|
||||
setDocumentsLoading(true);
|
||||
const docList: IDocument[] = getDocumentList(caseDetail) || [];
|
||||
const docPromises: Promise<DocumentDetail | null>[] = [];
|
||||
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<DocumentDetail | null>) => {
|
||||
const documents = docs?.filter(
|
||||
(document: DocumentDetail | null) => document
|
||||
) as Array<DocumentDetail>;
|
||||
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<ICustomerProfile> = (props) => {
|
||||
}
|
||||
};
|
||||
|
||||
const onLoad = () => {
|
||||
clearTimer();
|
||||
setLoading(false);
|
||||
setErrorModalImage(false);
|
||||
};
|
||||
|
||||
const onRetry = () => {
|
||||
setRetry(!retry);
|
||||
};
|
||||
@@ -163,11 +155,28 @@ const CustomerProfile: React.FC<ICustomerProfile> = (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 (
|
||||
<SafeAreaView style={[GenericStyles.fill, styles.container, GenericStyles.whiteBackground]}>
|
||||
<NavigationHeader title="Customer profile" onBack={() => goBack()} icon={Icon.close} />
|
||||
<ScrollView>
|
||||
<ScrollView ref={scrollRef}>
|
||||
<View style={GenericStyles.centerAlignedRow}>
|
||||
<View style={styles.imageContainer}>
|
||||
{!isHighQualityImageLoaded && (
|
||||
@@ -185,15 +194,17 @@ const CustomerProfile: React.FC<ICustomerProfile> = (props) => {
|
||||
onError={onErrorLowResImage}
|
||||
/>
|
||||
)}
|
||||
<CachedImage
|
||||
highQualityUri={imageUri}
|
||||
cacheFileKey={caseDetail.id}
|
||||
style={styles.imageStyle}
|
||||
onError={onErrorHighResImage}
|
||||
onLoadStart={onLoadStart}
|
||||
onLoadEnd={onLoadEnd}
|
||||
retry={retry}
|
||||
/>
|
||||
<TouchableOpacity activeOpacity={1} onPress={handleExpandImage}>
|
||||
<CachedImage
|
||||
highQualityUri={imageUri}
|
||||
cacheFileKey={caseDetail.id}
|
||||
style={styles.imageStyle}
|
||||
onError={onErrorHighResImage}
|
||||
onLoadStart={onLoadStart}
|
||||
onLoadEnd={onLoadEnd}
|
||||
retry={retry}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
{loading ? (
|
||||
<Text style={[GenericStyles.mt6, GenericStyles.whiteText, styles.loadingState]} bold>
|
||||
{!isNullOrEmptyString(imageUri) ? '' : 'No Image Found'}
|
||||
@@ -221,21 +232,14 @@ const CustomerProfile: React.FC<ICustomerProfile> = (props) => {
|
||||
) : null}
|
||||
</View>
|
||||
</View>
|
||||
{showVkyc ? (
|
||||
<View style={styles.vkyc}>
|
||||
<Text bold dark>
|
||||
VKYC video
|
||||
</Text>
|
||||
{vkycUri ? (
|
||||
<Pressable onPress={handleVkycPress}>
|
||||
<Text style={styles.openVideoTxt}>Open video</Text>
|
||||
</Pressable>
|
||||
) : (
|
||||
<ActivityIndicator color={COLORS.BASE.BLUE} />
|
||||
)}
|
||||
</View>
|
||||
) : null}
|
||||
{/* <Accordion title="VKYC Video" content={<VKYCVideo />} /> */}
|
||||
<DocumentDetails
|
||||
documentData={documentData}
|
||||
customerReferenceId={caseDetail.customerReferenceId}
|
||||
caseId={caseDetail.id}
|
||||
caseType={caseDetail.caseType}
|
||||
documentsLoading={documentsLoading}
|
||||
scrollByOffset={scrollByOffset}
|
||||
/>
|
||||
</ScrollView>
|
||||
</SafeAreaView>
|
||||
);
|
||||
|
||||
192
src/screens/caseDetails/DocumentDetails.tsx
Normal file
192
src/screens/caseDetails/DocumentDetails.tsx
Normal file
@@ -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<DocumentDetail>;
|
||||
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<boolean>();
|
||||
|
||||
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 (
|
||||
<View style={GenericStyles.ph16}>
|
||||
<SuspenseLoader
|
||||
loading={documentsLoading}
|
||||
fallBack={
|
||||
<>
|
||||
{[...Array(3).keys()].map(() => (
|
||||
<LineLoader
|
||||
width="100%"
|
||||
height={50}
|
||||
style={[GenericStyles.br6, GenericStyles.mb20]}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
}
|
||||
>
|
||||
{documentData.map((document: DocumentDetail) =>
|
||||
document.docContentType !== 'pdf' ? (
|
||||
<Accordion
|
||||
key={document.documentRefId}
|
||||
accordionStyle={styles.accordionStyles}
|
||||
isActive={isExpanded}
|
||||
touchableDelay={50}
|
||||
touchableOpacity={0.8}
|
||||
accordionHeader={
|
||||
<View style={styles.headerWrapper}>
|
||||
{document?.icon}
|
||||
<Text style={GenericStyles.pl6}>{document?.title}</Text>
|
||||
</View>
|
||||
}
|
||||
customExpandUi={{
|
||||
whenCollapsed: (
|
||||
<View style={styles.accordionExpandBtn}>
|
||||
<ArrowSolidDownIcon />
|
||||
</View>
|
||||
),
|
||||
whenExpanded: (
|
||||
<View style={[styles.accordionExpandBtn, styles.rotateBtn]}>
|
||||
<ArrowSolidDownIcon />
|
||||
</View>
|
||||
),
|
||||
}}
|
||||
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);
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={[
|
||||
styles.accordionContentWrapper,
|
||||
document.docContentType === 'video' && { height: 320 },
|
||||
]}
|
||||
>
|
||||
{document.docContentType === 'video' ? (
|
||||
<VKYCVideo document={document} caseId={caseId} caseType={caseType} />
|
||||
) : (
|
||||
<DocumentImageComponent document={document} caseId={caseId} caseType={caseType} />
|
||||
)}
|
||||
</View>
|
||||
</Accordion>
|
||||
) : (
|
||||
<View
|
||||
style={[
|
||||
styles.accordionStyles,
|
||||
GenericStyles.row,
|
||||
GenericStyles.justifyContentSpaceBetween,
|
||||
GenericStyles.mb12,
|
||||
GenericStyles.whiteBackground,
|
||||
styles.pt0,
|
||||
]}
|
||||
key={document.documentRefId}
|
||||
>
|
||||
<View style={[styles.headerWrapper, GenericStyles.ph16, GenericStyles.pt12]}>
|
||||
{document?.icon}
|
||||
<Text style={GenericStyles.pl6}>{document?.title}</Text>
|
||||
</View>
|
||||
<Pressable
|
||||
onPress={() => handleOpenPdfPress(document, caseId, caseType)}
|
||||
style={[GenericStyles.ph16, GenericStyles.pt12]}
|
||||
>
|
||||
<Text style={styles.openPdfTxt}>Open PDF</Text>
|
||||
</Pressable>
|
||||
</View>
|
||||
)
|
||||
)}
|
||||
</SuspenseLoader>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
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;
|
||||
113
src/screens/caseDetails/DocumentImageComponent.tsx
Normal file
113
src/screens/caseDetails/DocumentImageComponent.tsx
Normal file
@@ -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 (
|
||||
<>
|
||||
<TouchableOpacity activeOpacity={1} onPress={handleExpandImage}>
|
||||
<CachedImage
|
||||
highQualityUri={imageUrl}
|
||||
cacheFileKey={docType + caseId}
|
||||
style={styles.imageStyle}
|
||||
onError={() => {
|
||||
setErrorModalImage({
|
||||
...errorModalImage,
|
||||
[docType]: true,
|
||||
});
|
||||
setLoading({
|
||||
...loading,
|
||||
[docType]: false,
|
||||
});
|
||||
}}
|
||||
onLoadStart={() => onLoadStart(docType)}
|
||||
onLoadEnd={() => onLoadEnd(docType)}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
{loading[docType] && (
|
||||
<Text style={[styles.loadingText]} bold>
|
||||
{!isNullOrEmptyString(imageUrl) ? 'Loading Image...' : 'No Image Found'}
|
||||
</Text>
|
||||
)}
|
||||
{errorModalImage[docType] && (
|
||||
<Text style={[styles.errorText]} bold>
|
||||
Error loading image{isValidating && '. Retrying !!'}
|
||||
</Text>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
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;
|
||||
35
src/screens/caseDetails/ImageViewer.tsx
Normal file
35
src/screens/caseDetails/ImageViewer.tsx
Normal file
@@ -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<ImageViewer> = (props) => {
|
||||
const {
|
||||
route: {
|
||||
params: { imageUrl, headerTitle },
|
||||
},
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<View style={GenericStyles.fill}>
|
||||
<ExpandableImage
|
||||
imageSrc={imageUrl}
|
||||
fallbackImage={imageUrl}
|
||||
title={headerTitle}
|
||||
close={goBack}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default ImageViewer;
|
||||
129
src/screens/caseDetails/PDFFullScreen.tsx
Normal file
129
src/screens/caseDetails/PDFFullScreen.tsx
Normal file
@@ -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<IPdfFullScreen> = (props) => {
|
||||
const {
|
||||
route: {
|
||||
params: { pdfUri, referenceId, cacheFileKey, screenName, caseId, caseType, unSignedUri },
|
||||
},
|
||||
} = props;
|
||||
|
||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||
const [pdfFilePath, setPdfFilePath] = useState<string>('');
|
||||
const [error, setError] = useState<boolean>(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 (
|
||||
<View style={GenericStyles.fill}>
|
||||
<NavigationHeader onBack={goBack} title={screenName} icon={Icon.close} />
|
||||
{pdfFilePath && (
|
||||
<SafeAreaView style={styles.container}>
|
||||
<PdfRendererView
|
||||
style={styles.pdf}
|
||||
source={`file:/${pdfFilePath}`}
|
||||
distanceBetweenPages={16}
|
||||
maxZoom={5}
|
||||
/>
|
||||
</SafeAreaView>
|
||||
)}
|
||||
|
||||
{isLoading && (
|
||||
<ActivityIndicator
|
||||
size={'large'}
|
||||
style={styles.errorText}
|
||||
animating={isLoading}
|
||||
color={COLORS.BASE.BLUE}
|
||||
/>
|
||||
)}
|
||||
{error && (
|
||||
<Text style={styles.errorText}>Error loading PDF{isValidating && '. Retrying !!'}</Text>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
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,
|
||||
},
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
22
src/screens/caseDetails/utils/VKYCVideo.tsx
Normal file
22
src/screens/caseDetails/utils/VKYCVideo.tsx
Normal file
@@ -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 (
|
||||
<WebView
|
||||
source={{ html: vkycHtml(videoUrl) }}
|
||||
style={{ flex: 1 }}
|
||||
allowsFullscreenVideo={true}
|
||||
mediaPlaybackRequiresUserAction={false}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default VKYCVideo;
|
||||
81
src/screens/caseDetails/utils/documentUtils.tsx
Normal file
81
src/screens/caseDetails/utils/documentUtils.tsx
Normal file
@@ -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: <VideoIcon />,
|
||||
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: <ImageIcon />,
|
||||
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: <ImageIcon />,
|
||||
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',
|
||||
});
|
||||
});
|
||||
};
|
||||
107
src/screens/permissions/BlockerScreenApps.tsx
Normal file
107
src/screens/permissions/BlockerScreenApps.tsx
Normal file
@@ -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 (
|
||||
<View style={[GenericStyles.ph16, GenericStyles.pb4, styles.container]}>
|
||||
<View style={[styles.imageContainer]}>
|
||||
<PermissionImage />
|
||||
</View>
|
||||
|
||||
<View>
|
||||
<Text style={[GenericStyles.fontSize14, GenericStyles.mb4, styles.textDark]}>
|
||||
Some installed apps are banned by Navi
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<View>
|
||||
<Text style={[GenericStyles.fontSize12, styles.textLight]}>
|
||||
Uninstall these apps for uninterrupted usage
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<ScrollView>
|
||||
<View style={[styles.appsContainer]}>
|
||||
{blacklistedAppsInstalled.map((app: Apps, index: number) => (
|
||||
<View key={index}>
|
||||
<View style={styles.appsListItem}>
|
||||
<Image source={{ uri: app.applicationIcon }} style={styles.appIcon} />
|
||||
<Text style={[styles.textDark, styles.appNameText]}>{app.applicationName}</Text>
|
||||
</View>
|
||||
{index < blacklistedAppsInstalled.length - 1 && (
|
||||
<View style={[styles.horizontalLine]} />
|
||||
)}
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
</ScrollView>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
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;
|
||||
68
src/services/blacklistedApps.service.ts
Normal file
68
src/services/blacklistedApps.service.ts
Normal file
@@ -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;
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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[];
|
||||
}
|
||||
|
||||
|
||||
@@ -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"],
|
||||
|
||||
Reference in New Issue
Block a user