TP-00000 | synced with master

Merge branch 'master' of github.com:navi-medici/address-verification-app into WhatsappFeedbackEnhancment
This commit is contained in:
Ashish Deo
2023-10-31 18:56:09 +05:30
51 changed files with 1488 additions and 164 deletions

View File

@@ -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
View File

@@ -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 () => {

View File

@@ -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

View File

@@ -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 {
}

View File

@@ -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',

View File

@@ -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",

View File

@@ -167,6 +167,7 @@ interface ISignedRequestItem {
documentReferenceId: string;
caseType: CaseAllocationType;
caseId: string;
unSignedUri?: string;
}
export type ISignedRequest = ISignedRequestItem[];

View File

@@ -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}</>;
};

View File

@@ -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 {

View File

@@ -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) {

View File

@@ -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);

View File

@@ -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, {

View File

@@ -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);

View File

@@ -0,0 +1,5 @@
import { NativeModules } from 'react-native';
const { ScreenshotBlocker } = NativeModules;
export default ScreenshotBlocker;

View File

@@ -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;

View File

@@ -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,

View File

@@ -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 =

View File

@@ -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 };

View 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;

View 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;

View File

@@ -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}

View File

@@ -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>

View File

@@ -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}>&nbsp;&nbsp;&#9679;&nbsp;&nbsp;</Text>
{!isNaN(relativeDistanceBwLatLong) ? relativeDistanceBwLatLong.toFixed(2) : '--'} km
away
{!isNaN(relativeDistanceBwLatLong)
? relativeDistanceFormatter(relativeDistanceBwLatLong)
: '--'}{' '}
km away
</>
) : null}
{showSource ? <AddressSource addressItem={addressItem} /> : null}

View File

@@ -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;

View File

@@ -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;

View File

@@ -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) {

View File

@@ -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

View File

@@ -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);

View 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;

View File

@@ -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;

View File

@@ -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 {

View File

@@ -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);
};

View File

@@ -108,7 +108,9 @@ const AuthRouter = () => {
</BlockerScreen>
</TrackingComponent>
) : (
<UnProtectedRouter />
<BlockerScreen>
<UnProtectedRouter />
</BlockerScreen>
);
};

View File

@@ -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}

View File

@@ -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]}>

View File

@@ -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>
);

View 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;

View 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;

View 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;

View 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,
},
});

View File

@@ -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;
}

View 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;

View 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',
});
});
};

View 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;

View 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;

View File

@@ -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;

View File

@@ -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;
}

View File

@@ -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,
});

View File

@@ -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[];
}

View File

@@ -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"],