This commit is contained in:
yashmantri
2023-10-04 19:37:54 +05:30
41 changed files with 1049 additions and 190 deletions

16
App.tsx
View File

@@ -23,7 +23,7 @@ import { toastConfigs, ToastContainer } from './RN-UI-LIB/src/components/toast';
import { APM_APP_NAME, APM_BASE_URL, ENV } from './src/constants/config';
import { COLORS } from './RN-UI-LIB/src/styles/colors';
import { LocalStorageKeys } from './src/common/Constants';
import { CLICKSTREAM_EVENT_NAMES, LocalStorageKeys } from './src/common/Constants';
import Permissions from './src/screens/permissions/Permissions';
import { setJsErrorHandler } from './src/services/exception-handler.service';
import SuspenseLoader from './RN-UI-LIB/src/components/suspense_loader/SuspenseLoader';
@@ -37,8 +37,13 @@ import {
} from './src/components/utlis/PermissionUtils';
import usePolling from './src/hooks/usePolling';
import { MILLISECONDS_IN_A_SECOND } from './RN-UI-LIB/src/utlis/common';
import { setItem } from './src/components/utlis/storageHelper';
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 { addClickstreamEvent } from './src/services/clickstreamEventService';
import ScreenshotBlocker from './src/components/utlis/ScreenshotBlocker';
initSentry();
@@ -104,6 +109,12 @@ function App() {
return route?.name || '';
};
async function setForegroundTimeStampAndClickstream() {
const now = dayJs().toString();
await setItem(StorageKeys.APP_FOREGROUND_TIMESTAMP, now);
addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.AV_APP_FOREGROUND, { now });
}
usePolling(askForPermissions, PERMISSION_CHECK_POLL_INTERVAL);
initApm({
@@ -133,6 +144,9 @@ function App() {
setIsGlobalDocumentMapLoaded(true);
})();
checkCodePushAndSync();
handleUpdatedConfigureValuesFromFirebase();
setForegroundTimeStampAndClickstream();
return () => {
appStateChange.remove();
};

View File

@@ -131,8 +131,8 @@ def reactNativeArchitectures() {
return value ? value.split(",") : ["armeabi-v7a", "x86", "x86_64", "arm64-v8a"]
}
def VERSION_CODE = 83
def VERSION_NAME = "2.3.10"
def VERSION_CODE = 88
def VERSION_NAME = "2.4.4"
android {
ndkVersion rootProject.ext.ndkVersion

View File

@@ -1,39 +1,40 @@
{
"project_info": {
"project_number": "60755663443",
"project_id": "address-verification-app",
"storage_bucket": "address-verification-app.appspot.com"
},
"client": [
{
"client_info": {
"mobilesdk_app_id": "1:60755663443:android:988149d3da3c00d38584a6",
"android_client_info": {
"package_name": "com.avapp"
}
},
"oauth_client": [
{
"client_id": "60755663443-40k0fbrbbqv4ci4hrjlbrphab5fj387b.apps.googleusercontent.com",
"client_type": 3
}
],
"api_key": [
{
"current_key": "AIzaSyA70_d2M2ke-Mu0OHGZ6iZilBbD6A-_z0c"
}
],
"services": {
"appinvite_service": {
"other_platform_oauth_client": [
{
"client_id": "60755663443-40k0fbrbbqv4ci4hrjlbrphab5fj387b.apps.googleusercontent.com",
"client_type": 3
}
]
}
"project_info": {
"project_number": "60755663443",
"project_id": "address-verification-app",
"storage_bucket": "address-verification-app.appspot.com",
"firebase_url": "https://address-verification-app-default-rtdb.firebaseio.com"
},
"client": [
{
"client_info": {
"mobilesdk_app_id": "1:60755663443:android:4a948ee9d0b4e3098584a6",
"android_client_info": {
"package_name": "com.avapp"
}
},
"oauth_client": [
{
"client_id": "60755663443-40k0fbrbbqv4ci4hrjlbrphab5fj387b.apps.googleusercontent.com",
"client_type": 3
}
],
"api_key": [
{
"current_key": "AIzaSyA70_d2M2ke-Mu0OHGZ6iZilBbD6A-_z0c"
}
],
"services": {
"appinvite_service": {
"other_platform_oauth_client": [
{
"client_id": "60755663443-40k0fbrbbqv4ci4hrjlbrphab5fj387b.apps.googleusercontent.com",
"client_type": 3
}
]
}
}
],
"configuration_version": "1"
}
}
],
"configuration_version": "1"
}

View File

@@ -30,7 +30,12 @@
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/> ->
<uses-permission android:name="android.permission.WAKE_LOCK" />
21 -> lollipop 28 ->
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" />
<queries>
<package android:name="com.whatsapp" />
</queries>
<uses-permission android:name="android.permission.USE_EXACT_ALARM"/>
<application
android:name=".MainApplication"
android:label="@string/app_name"
@@ -55,7 +60,17 @@
<service android:name="com.supersami.foregroundservice.ForegroundService" android:foregroundServiceType="location"></service>
<service android:name="com.supersami.foregroundservice.ForegroundServiceTask" android:foregroundServiceType="location"></service>
<activity
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.provider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/provider_paths" />
</provider>
<activity
android:name=".MainActivity"
android:label="@string/app_name"
android:screenOrientation="portrait"
@@ -74,5 +89,6 @@
<data android:scheme="cosmosapp" android:host="cosmos" />
</intent-filter>
</activity>
</application>
</manifest>

View File

@@ -1,32 +1,57 @@
package com.avapp;
import static android.app.Activity.RESULT_CANCELED;
import static android.app.Activity.RESULT_OK;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.location.LocationManager;
import androidx.annotation.Nullable;
import androidx.core.content.FileProvider;
import com.facebook.react.bridge.ActivityEventListener;
import com.facebook.react.bridge.BaseActivityEventListener;
import com.facebook.react.bridge.Promise;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
import android.content.pm.PackageInfo;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.os.Environment;
import android.os.Handler;
import android.util.Base64;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.util.List;
public class DeviceUtilsModule extends ReactContextBaseJavaModule implements ActivityEventListener {
import android.net.Uri;
import android.util.Log;
import android.widget.Toast;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
public class DeviceUtilsModule extends ReactContextBaseJavaModule {
private ReactApplicationContext RNContext;
private File imageFile;
private int WHATSAPP_SHARE_REQUEST_CODE = 12345;
public DeviceUtilsModule(@Nullable ReactApplicationContext reactContext){
super(reactContext);
RNContext = reactContext;
reactContext.addActivityEventListener(mActivityEventListener);
}
@Override
@@ -34,15 +59,22 @@ public class DeviceUtilsModule extends ReactContextBaseJavaModule implements Act
return "DeviceUtilsModule";
}
@Override
public void onActivityResult(Activity activity, int i, int i1, @Nullable Intent intent) {
private final ActivityEventListener mActivityEventListener = new BaseActivityEventListener() {
@Override
public void onActivityResult(Activity activity, int i, int i1, @Nullable Intent intent) {
if(i1 != RESULT_CANCELED) {
if (i == WHATSAPP_SHARE_REQUEST_CODE) {
new File(Uri.fromFile(imageFile).getPath()).delete();
}
}
}
}
@Override
public void onNewIntent(Intent intent) {
@Override
public void onNewIntent(Intent intent) {
}
};
}
@ReactMethod
public void isLocationEnabled (Promise promise) {
@@ -77,4 +109,74 @@ public class DeviceUtilsModule extends ReactContextBaseJavaModule implements Act
promise.reject( err);
}
}
private static File convertBase64ToFile(Context context,String base64Data) {
try {
byte[] decodedBytes = Base64.decode(base64Data, Base64.DEFAULT);
File outputDir = context.getCacheDir();
File file = File.createTempFile("temp_image", ".jpg", Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS));
FileOutputStream fos = new FileOutputStream(file);
fos.write(decodedBytes);
fos.flush();
fos.close();
return file;
} catch (IOException e) {
Log.e("ShareUtils", "Failed to convert Base64 to file: " + e.getMessage());
return null;
}
}
public boolean isWhatsAppInstalled() {
PackageManager packageManager = RNContext.getPackageManager();
List<PackageInfo> packages = packageManager.getInstalledPackages(PackageManager.GET_META_DATA);
for (PackageInfo packageInfo : packages) {
String packageName = packageInfo.packageName;
if(packageName.equals("com.whatsapp")){
return true;
}
}
return false;
}
@ReactMethod
public void sendFeedbackToWhatsapp(String message, String imageUrl, String mimeType, Promise promise) {
try{
if(!isWhatsAppInstalled()){
promise.reject("errorCode", "1");
return;
}
Intent sendIntent = new Intent();
sendIntent.setAction(Intent.ACTION_SEND);
sendIntent.putExtra(Intent.EXTRA_TEXT, message);
if(imageUrl.equals("")) {
sendIntent.setType("text/plain");
sendIntent.setPackage("com.whatsapp");
getCurrentActivity().startActivity(sendIntent);
} else {
sendIntent.setType(mimeType);
sendIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
imageFile = convertBase64ToFile(getReactApplicationContext(), imageUrl);
Uri fileUri = FileProvider.getUriForFile(getReactApplicationContext(), BuildConfig.APPLICATION_ID + ".provider", new File(
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS),
imageFile.getName()
)
);
sendIntent.putExtra(Intent.EXTRA_STREAM, fileUri);
sendIntent.setPackage("com.whatsapp");
getCurrentActivity().startActivityForResult(sendIntent, WHATSAPP_SHARE_REQUEST_CODE);
}
promise.resolve(true);
return;
} catch (Error e){
promise.reject("errorCode","2");
}
return;
}
}

View File

@@ -3,6 +3,8 @@ package com.avapp;
import com.facebook.react.ReactActivity;
import com.facebook.react.ReactActivityDelegate;
import com.facebook.react.ReactRootView;
import android.content.IntentFilter;
import android.os.Bundle;
public class MainActivity extends ReactActivity {
@@ -16,14 +18,10 @@ public class MainActivity extends ReactActivity {
return "AVAPP";
}
// https://github.com/software-mansion/react-native-screens#android
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(null);
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(null);
}
/**
* Returns the instance of the {@link ReactActivityDelegate}. There the RootView is created and

View File

@@ -68,26 +68,29 @@ public class ScreenshotBlockerModule extends ReactContextBaseJavaModule {
public void startScreenshotTracking() {
if (!isTracking) {
isTracking = true;
ContentResolver contentResolver = getCurrentActivity().getContentResolver();
contentObserver = new ContentObserver(new Handler(Looper.getMainLooper())) {
long lastEventTimestamp = 0;
Activity activity = getCurrentActivity();
if(activity != null) {
ContentResolver contentResolver = activity.getContentResolver();
contentObserver = new ContentObserver(new Handler(Looper.getMainLooper())) {
long lastEventTimestamp = 0;
@Override
public void onChange(boolean selfChange, Uri uri) {
long currentTimeMillis = System.currentTimeMillis();
@Override
public void onChange(boolean selfChange, Uri uri) {
long currentTimeMillis = System.currentTimeMillis();
if (currentTimeMillis - lastEventTimestamp > 1000) {
sendScreenshotEvent();
lastEventTimestamp = currentTimeMillis;
if (currentTimeMillis - lastEventTimestamp > 1000) {
sendScreenshotEvent();
lastEventTimestamp = currentTimeMillis;
}
}
}
};
};
contentResolver.registerContentObserver(
MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
true,
contentObserver
);
contentResolver.registerContentObserver(
MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
true,
contentObserver
);
}
}
}

View File

@@ -1,6 +1,5 @@
package com.avapp;
import com.facebook.react.ReactPackage;
import com.facebook.react.bridge.NativeModule;
import com.facebook.react.bridge.ReactApplicationContext;

View File

@@ -8,7 +8,24 @@ module.exports = {
cwd: 'babelrc',
extensions: ['.ts', '.tsx', '.js', '.ios.js', '.android.js'],
alias: {
'@cuteapp': './app',
'@root': './src',
'@components': './src/components',
'@hooks': './src/hooks',
'@actions': './src/action',
'@reducers': './src/reducer',
'@constants': './src/constants',
'@screens': './src/screens',
'@services': './src/services',
'@types': './src/types',
'@common': './src/common',
'@assets': './src/assets',
'@store': './src/store/store',
'@utils': './src/components/utlis',
'@rn-ui-lib/components': './RN-UI-LIB/src/components',
'@rn-ui-lib/icons': './RN-UI-LIB/src/Icons',
'@rn-ui-lib/styles': './RN-UI-LIB/src/styles/index',
'@rn-ui-lib/colors': './RN-UI-LIB/src/styles/colors',
'@rn-ui-lib/utils': './RN-UI-LIB/src/utlis',
},
},
],

View File

@@ -1,6 +1,6 @@
{
"name": "AV_APP",
"version": "2.4.0",
"version": "2.4.4",
"private": true,
"scripts": {
"android:dev": "yarn move:dev && react-native run-android",
@@ -37,6 +37,7 @@
"@react-native-firebase/database": "16.4.6",
"@react-native-firebase/firestore": "16.5.0",
"@react-native-firebase/messaging": "17.4.0",
"@react-native-firebase/remote-config": "16.4.6",
"@react-native-google-signin/google-signin": "9.0.2",
"@react-navigation/bottom-tabs": "6.5.5",
"@react-navigation/native": "6.1.4",
@@ -81,7 +82,8 @@
"react-native-webview": "12.0.2",
"react-redux": "8.0.5",
"redux": "4.2.0",
"redux-persist": "6.0.0"
"redux-persist": "6.0.0",
"rn-fetch-blob": "0.12.0"
},
"devDependencies": {
"@babel/core": "7.12.9",

View File

@@ -0,0 +1,14 @@
import * as React from 'react';
import Svg, { Mask, Path, G } from 'react-native-svg';
const ChevronDown = (props) => {
const { fillColor = '#969696', size = 24 } = props;
return (
<Svg width={size} height={size} viewBox="0 0 24 24" fill="none">
<Path
d="M11.9958 13.9585C11.907 13.9585 11.8238 13.9445 11.7461 13.9165C11.6684 13.8891 11.5963 13.842 11.5297 13.7755L8.44982 10.6985C8.32774 10.5765 8.26958 10.424 8.27535 10.2408C8.28068 10.0581 8.34439 9.90571 8.46647 9.78374C8.58856 9.66176 8.74394 9.60078 8.93261 9.60078C9.12129 9.60078 9.27667 9.66176 9.39875 9.78374L11.9958 12.3784L14.6095 9.7671C14.7316 9.64513 14.8843 9.58681 15.0677 9.59213C15.2506 9.5979 15.4031 9.66176 15.5252 9.78374C15.6473 9.90571 15.7083 10.0609 15.7083 10.2494C15.7083 10.4379 15.6473 10.5932 15.5252 10.7151L12.462 13.7755C12.3954 13.842 12.3232 13.8891 12.2455 13.9165C12.1678 13.9445 12.0846 13.9585 11.9958 13.9585Z"
fill={fillColor}
/>
</Svg>
);
};
export default ChevronDown;

View File

@@ -0,0 +1,14 @@
import * as React from 'react';
import Svg, { Mask, Path, G } from 'react-native-svg';
const ChevronUp = (props) => {
const { fillColor = '#969696', size = 24 } = props;
return (
<Svg width={size} height={size} viewBox="0 0 24 24" fill="none">
<Path
d="M8.466 13.767a.63.63 0 0 1-.183-.465.63.63 0 0 1 .183-.465l3.057-3.055a.587.587 0 0 1 .216-.141.742.742 0 0 1 .25-.041c.088 0 .171.013.249.04.077.029.15.076.216.142l3.073 3.072a.609.609 0 0 1 .183.448.637.637 0 0 1-.2.465.63.63 0 0 1-.465.183.63.63 0 0 1-.465-.183l-2.592-2.59-2.608 2.607a.61.61 0 0 1-.449.182.638.638 0 0 1-.465-.199Z"
fill={fillColor}
/>
</Svg>
);
};
export default ChevronUp;

View File

@@ -0,0 +1,11 @@
import * as React from 'react';
import Svg, { Path, SvgProps } from 'react-native-svg';
const SvgComponent = (props: SvgProps) => (
<Svg xmlns="http://www.w3.org/2000/svg" fill="none" {...props}>
<Path
fill="#25D366"
d="M8 1.6A6.4 6.4 0 0 0 1.6 8c0 1.2.337 2.32.912 3.28l-.855 3.12 3.187-.837c.932.53 2.007.837 3.156.837A6.4 6.4 0 0 0 8 1.6ZM5.81 5.014c.103 0 .21 0 .302.004.115.003.239.011.358.274.141.313.449 1.097.488 1.176.04.08.068.173.013.277-.052.107-.08.172-.156.266-.08.092-.167.206-.239.275-.08.08-.162.167-.07.325s.41.678.881 1.097c.606.54 1.117.707 1.275.786.16.08.251.067.343-.04.095-.103.397-.46.503-.619.104-.159.21-.131.354-.08.147.053.926.437 1.085.516.159.08.263.12.303.184.041.066.041.384-.09.754-.132.37-.78.727-1.07.752-.292.027-.565.132-1.903-.395-1.612-.635-2.63-2.287-2.709-2.393-.08-.104-.646-.86-.646-1.638 0-.781.41-1.164.553-1.323a.583.583 0 0 1 .424-.198Z"
/>
</Svg>
);
export default SvgComponent;

View File

@@ -0,0 +1,19 @@
let ACTIVITY_TIME_ON_APP: number = 5; //5 seconds
let ACTIVITY_TIME_WINDOW_HIGH: number = 10; //10 minutes
let ACTIVITY_TIME_WINDOW_MEDIUM: number = 30; //30 minutes
export const getActivityTimeOnApp = () => ACTIVITY_TIME_ON_APP;
export const getActivityTimeWindowHigh = () => ACTIVITY_TIME_WINDOW_HIGH;
export const getActivityTimeWindowMedium = () => ACTIVITY_TIME_WINDOW_MEDIUM;
export const setActivityTimeOnApp = (activityTimeOnApp: number) => {
ACTIVITY_TIME_ON_APP = activityTimeOnApp;
};
export const setActivityTimeWindowHigh = (activityTimeWindowHigh: number) => {
ACTIVITY_TIME_WINDOW_HIGH = activityTimeWindowHigh;
};
export const setActivityTimeWindowMedium = (activityTimeWindowMedium: number) => {
ACTIVITY_TIME_WINDOW_MEDIUM = activityTimeWindowMedium;
};

View File

@@ -338,6 +338,10 @@ export const CLICKSTREAM_EVENT_NAMES = {
description: 'FA_COLLECT_MONEY_NUMBER_CHANGED',
},
FA_COPY_LAN_CLICKED: { name: 'FA_COPY_LAN_CLICKED', description: 'FA_COPY_LAN_CLICKED' },
FA_COPY_EMPLOYER_NAME_CLICKED: {
name: 'FA_COPY_EMPLOYER_NAME_CLICKED',
description: 'FA_COPY_EMPLOYER_NAME_CLICKED',
},
FA_COPY_LINK_CLICKED: { name: 'FA_COPY_LINK_CLICKED', description: 'FA_COPY_LINK_CLICKED' },
FA_COPY_LINK_FAILED: { name: 'FA_COPY_LINK_FAILED', description: 'FA_COPY_LINK_FAILED' },
FA_PAST_FEEDBACKS_FEEDBACK_CLICKED: {
@@ -435,6 +439,15 @@ export const CLICKSTREAM_EVENT_NAMES = {
name: 'FA_UNIFIED_ENTITY_REQUEST_FAILED',
description: 'FA_UNIFIED_ENTITY_REQUEST_FAILED',
},
FA_SHARE_FEEDBACK_CLICKED: {
name: 'FA_SHARE_FEEDBACK_CLICKED',
description:
'When user clicks on share feedback on case details screen for any of the filled feedback',
},
FA_SHARE_SUCCESSFUL: {
name: 'FA_SHARE_SUCCESSFUL',
description: 'When user is redirected to WhatsApp after clicking on share feedback',
},
// Notifications
FA_NOTIFICATION_ICON_CLICK: {

View File

@@ -34,8 +34,17 @@ import { setLockData } from '../reducer/userSlice';
import { getConfigData } from '../action/configActions';
import { AppStates } from '../types/appStates';
import { StorageKeys } from '../types/storageKeys';
import { AgentActivity } from '../types/agentActivity';
import {
getActivityTimeOnApp,
getActivityTimeWindowMedium,
getActivityTimeWindowHigh,
} from './AgentActivityConfigurableConstants';
import RNFS from 'react-native-fs';
import { GlobalImageMap } from './CachedImage';
import { get } from 'react-hook-form';
import { addClickstreamEvent } from '../services/clickstreamEventService';
import { CLICKSTREAM_EVENT_NAMES } from './Constants';
export enum FOREGROUND_TASKS {
GEOLOCATION = 'GEOLOCATION',
@@ -43,6 +52,7 @@ export enum FOREGROUND_TASKS {
DATA_SYNC = 'DATA_SYNC',
FIRESTORE_FALLBACK = 'FIRESTORE_FALLBACK',
UPDATE_AGENT_ACTIVENESS = 'UPDATE_AGENT_ACTIVENESS',
UPDATE_AGENT_ACTIVITY = 'UPDATE_AGENT_ACTIVITY',
DELETE_CACHE = 'DELETE_CACHE',
}
@@ -51,7 +61,6 @@ interface ITrackingComponent {
}
let LAST_SYNC_STATUS = 'SKIP';
const ACTIVITY_TIME_ON_APP = 5; // 5 seconds
const ACTIVITY_TIME_WINDOW = 10; // 10 minutes
const TrackingComponent: React.FC<ITrackingComponent> = ({ children }) => {
@@ -94,12 +103,16 @@ const TrackingComponent: React.FC<ITrackingComponent> = ({ children }) => {
return;
}
const isActiveOnApp: string | boolean = (await getItem(StorageKeys.IS_USER_ACTIVE)) || false;
const userActivityonApp: string =
(await getItem(StorageKeys.USER_ACTIVITY_ON_APP)) || AgentActivity.LOW;
const geolocation: IGeolocationPayload = {
latitude: location.latitude,
longitude: location.longitude,
accuracy: location.accuracy,
timestamp: Date.now(),
isActiveOnApp: Boolean(isActiveOnApp),
userActivityOnApp: String(userActivityonApp),
};
dispatch(sendLocationAndActivenessToServer([geolocation]));
} catch (e: any) {
@@ -169,6 +182,7 @@ const TrackingComponent: React.FC<ITrackingComponent> = ({ children }) => {
const isForegroundTimeWithInRange =
diffBetweenCurrentTimeAndForegroundTime <= ACTIVITY_TIME_WINDOW;
const isForegroundTimeAfterBackground = dayJs(foregroundTimestamp).isAfter(backgroundTimestamp);
const ACTIVITY_TIME_ON_APP = getActivityTimeOnApp();
if (isForegroundTimeWithInRange) {
if (
@@ -184,6 +198,60 @@ const TrackingComponent: React.FC<ITrackingComponent> = ({ children }) => {
return;
};
const handleUpdateActivity = async () => {
const foregroundTimestamp = await getItem(StorageKeys.APP_FOREGROUND_TIMESTAMP);
const backgroundTimestamp = await getItem(StorageKeys.APP_BACKGROUND_TIMESTAMP);
const stateSetTimestamp = await getItem(StorageKeys.STATE_SET_TIMESTAMP);
if (foregroundTimestamp == null) {
console.log('fts set after installation');
await setItem(StorageKeys.APP_FOREGROUND_TIMESTAMP, dayJs().toString());
}
const foregroundTime = dayJs(foregroundTimestamp);
const backgroundTime = dayJs(backgroundTimestamp);
const stateSetTime = dayJs(stateSetTimestamp);
const diffBetweenCurrentTimeAndForegroundTime =
dayJs().diff(foregroundTime, 'seconds') < 0 ? 0 : dayJs().diff(foregroundTime, 'seconds');
const diffBetweenCurrentTimeAndSetStateTime =
dayJs().diff(stateSetTime, 'minutes') < 0 ? 0 : dayJs().diff(stateSetTime, 'minutes');
const ACTIVITY_TIME_ON_APP = getActivityTimeOnApp();
const ACTIVITY_TIME_WINDOW_HIGH = getActivityTimeWindowHigh();
const ACTIVITY_TIME_WINDOW_MEDIUM = getActivityTimeWindowMedium();
const isStateSetTimeWithinHighRange =
diffBetweenCurrentTimeAndSetStateTime < ACTIVITY_TIME_WINDOW_HIGH;
const isStateSetTimeWithinMediumRange =
diffBetweenCurrentTimeAndSetStateTime < ACTIVITY_TIME_WINDOW_MEDIUM;
const isForegroundTimeAfterBackground = dayJs(foregroundTimestamp).isAfter(backgroundTimestamp);
if (AppState.currentState === AppStates.ACTIVE) {
if (diffBetweenCurrentTimeAndForegroundTime >= ACTIVITY_TIME_ON_APP) {
await setItem(StorageKeys.USER_ACTIVITY_ON_APP, AgentActivity.HIGH);
return;
}
return;
}
if (isForegroundTimeAfterBackground) {
if (diffBetweenCurrentTimeAndForegroundTime >= ACTIVITY_TIME_ON_APP) {
await setItem(StorageKeys.USER_ACTIVITY_ON_APP, AgentActivity.HIGH);
return;
}
return;
} else if (isStateSetTimeWithinHighRange) {
return;
} else if (isStateSetTimeWithinMediumRange) {
await setItem(StorageKeys.USER_ACTIVITY_ON_APP, AgentActivity.MEDIUM);
return;
} else {
await setItem(StorageKeys.USER_ACTIVITY_ON_APP, AgentActivity.LOW);
return;
}
};
const deleteCache = () => {
const directoryPath = RNFS.CachesDirectoryPath;
const currentDate = new Date().getTime();
@@ -239,6 +307,12 @@ const TrackingComponent: React.FC<ITrackingComponent> = ({ children }) => {
delay: ACTIVITY_TIME_WINDOW * MILLISECONDS_IN_A_MINUTE, // 10 minutes
onLoop: true,
},
{
taskId: FOREGROUND_TASKS.UPDATE_AGENT_ACTIVITY,
task: handleUpdateActivity,
delay: ACTIVITY_TIME_WINDOW * MILLISECONDS_IN_A_MINUTE, // 10 minutes
onLoop: true,
},
{
taskId: FOREGROUND_TASKS.DELETE_CACHE,
task: deleteCache,
@@ -272,17 +346,39 @@ const TrackingComponent: React.FC<ITrackingComponent> = ({ children }) => {
});
}
const userActivityUpdateOnBackground = async () => {
const foregroundTimestamp = await getItem(StorageKeys.APP_FOREGROUND_TIMESTAMP);
const backgroundTimestamp = await getItem(StorageKeys.APP_BACKGROUND_TIMESTAMP);
const foregroundTime = dayJs(foregroundTimestamp);
const backgroundTime = dayJs(backgroundTimestamp);
const diffBetweenBackgroundAndForegroundTime = dayJs(backgroundTime).diff(
foregroundTime,
'seconds'
);
const ACTIVITY_TIME_ON_APP = getActivityTimeOnApp();
if (diffBetweenBackgroundAndForegroundTime >= ACTIVITY_TIME_ON_APP) {
await setItem(StorageKeys.USER_ACTIVITY_ON_APP, AgentActivity.HIGH);
await setItem(StorageKeys.STATE_SET_TIMESTAMP, dayJs().toString());
return;
}
return;
};
const handleAppStateChange = async (nextAppState: AppStateStatus) => {
// App comes to foreground from background
const now = dayJs().toString();
if (nextAppState === AppStates.ACTIVE) {
await setItem(StorageKeys.APP_FOREGROUND_TIMESTAMP, now);
addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.AV_APP_FOREGROUND, { now });
handleGetCaseSyncStatus();
dispatch(getConfigData());
CosmosForegroundService.start(tasks);
}
if (nextAppState === AppStates.BACKGROUND) {
await setItem(StorageKeys.APP_BACKGROUND_TIMESTAMP, now);
userActivityUpdateOnBackground();
addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.AV_APP_BACKGROUND, { now });
}
appState.current = nextAppState;
};
@@ -306,12 +402,8 @@ const TrackingComponent: React.FC<ITrackingComponent> = ({ children }) => {
useEffect(() => {
let appStateSubscription: NativeEventSubscription;
CosmosForegroundService.isRunning().then((isFGSRunning) => {
if (!isFGSRunning) {
appStateSubscription = AppState.addEventListener('change', handleAppStateChange);
CosmosForegroundService.start(tasks);
}
});
appStateSubscription = AppState.addEventListener('change', handleAppStateChange);
CosmosForegroundService.start(tasks);
return () => {
appStateSubscription?.remove();
};

View File

@@ -7,3 +7,10 @@ export const locationEnabled = (): Promise<boolean> => DeviceUtilsModule.isLocat
// returns array of all the installed packages.
export const getAllInstalledApp = (): Promise<string> => DeviceUtilsModule.getAllInstalledApp();
// sends feedback data to whatsapp.
export const sendFeedbackToWhatsapp = (
message: string,
imageUrl: string,
mimeType: string
): Promise<boolean> => DeviceUtilsModule.sendFeedbackToWhatsapp(message, imageUrl, mimeType);

View File

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

View File

@@ -368,3 +368,10 @@ export function getDistanceFromLatLonInKm(
const distance = 2 * Math.atan2(Math.sqrt(intermediateResult), Math.sqrt(1 - intermediateResult));
return EARTH_RADIUS * distance;
}
export function insertCommasinAmount(amount: number | undefined) {
const reversedAmount = amount?.toString().split('').reverse().join('');
const groups = reversedAmount?.match(/.{1,3}/g);
const result = groups?.join(',').split('').reverse().join('');
return result;
}

View File

@@ -13,6 +13,7 @@ export interface IGeolocationPayload {
accuracy: number;
timestamp: number;
isActiveOnApp: boolean;
userActivityOnApp: string;
}
export const sendLocationAndActivenessToServer =

View File

@@ -13,7 +13,6 @@ const useScreenshotTracking = () => {
}));
const screenshotEventEmitter = useMemo(() => new NativeEventEmitter(ScreenshotBlocker), []);
useEffect(() => {
ScreenshotBlocker.startScreenshotTracking();

View File

@@ -155,11 +155,7 @@ function SimilarAddressItem({
<Tag variant={TagVariant.yellow} text={contactabilityStatus} />
</View>
) : null}
<Text
numberOfLines={3}
ellipsizeMode="tail"
style={[styles.textContainer, styles.cardLightTitle, { color: COLORS.TEXT.BLACK }]}
>
<Text style={[styles.textContainer, styles.cardLightTitle, { color: COLORS.TEXT.BLACK }]}>
{sanitizeString(addressItem?.addressText)}
</Text>
<Text

View File

@@ -4,8 +4,7 @@ import NavigationHeader from '../../../RN-UI-LIB/src/components/NavigationHeader
import { goBack, navigateToScreen } from '../../components/utlis/navigationUtlis';
import useIsOnline from '../../hooks/useIsOnline';
import OfflineScreen from '../../common/OfflineScreen';
import { getUngroupedAddress } from '../../action/addressGeolocationAction';
import { type IAddress } from '../../types/addressGeolocation.types';
import { IGroupedAddressesItem, type IAddress } from '../../types/addressGeolocation.types';
import AddressItem from './AddressItem';
import { COLORS } from '../../../RN-UI-LIB/src/styles/colors';
import { PageRouteEnum } from '../auth/ProtectedRouter';
@@ -16,10 +15,8 @@ import SuspenseLoader from '../../../RN-UI-LIB/src/components/suspense_loader/Su
import LineLoader from '../../../RN-UI-LIB/src/components/suspense_loader/LineLoader';
import { CLICKSTREAM_EVENT_NAMES } from '../../common/Constants';
import { addClickstreamEvent } from '../../services/clickstreamEventService';
import filterFarAwayMetaAddresses from './utils/FilterFarAwayMetaAddresses';
import { useAppSelector } from '../../hooks';
import { CaseAllocationType } from '../allCases/interface';
import SimilarAddressItem from './SimilarAddressItem';
import Accordion from '../../../RN-UI-LIB/src/components/accordian/Accordian';
const PAGE_TITLE = 'Additional addresses';
@@ -34,6 +31,10 @@ interface IUngroupedAddress {
};
}
const SeparatorBorderComponent = () => {
return <View style={styles.borderLine} />;
};
const UngroupedAddressContainer: React.FC<IUngroupedAddress> = ({ route: routeParams }) => {
const {
params: { loanAccountNumber, caseId, customerReferenceId, fetchUngroupedAddress },
@@ -87,6 +88,14 @@ const UngroupedAddressContainer: React.FC<IUngroupedAddress> = ({ route: routePa
navigateToScreen(PageRouteEnum.PAST_FEEDBACK_DETAIL, { ...params, ...commonParams });
};
const handleAccordionExpand = (isExpanded: boolean, addressId: string) => {
if (isExpanded) {
addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_VIEW_MORE_ADDRESSES_CLICKED, {
addressId,
});
}
};
return (
<View style={GenericStyles.fill}>
<NavigationHeader title={PAGE_TITLE} onBack={goBack} />
@@ -107,23 +116,76 @@ const UngroupedAddressContainer: React.FC<IUngroupedAddress> = ({ route: routePa
>
{ungroupedAddressList?.length ? (
<View>
{ungroupedAddressList.map((ungroupedAddressItem: IAddress) => (
{ungroupedAddressList.map((ungroupedAddressItem: IAddress, index: number) => (
<View>
<AddressItem
caseId={caseId}
showRelativeDistance
containerStyle={styles.addressItemContainer}
key={ungroupedAddressItem?.id}
addressItem={ungroupedAddressItem}
showActionButtons
handleOldFeedbackRouting={() => {
handleOpenOldFeedbacks(ungroupedAddressItem);
}}
handleCloseRouting={() => {
navigateToScreen(PageRouteEnum.ADDITIONAL_ADDRESSES, commonParams);
}}
showSource
/>
{!ungroupedAddressItem?.similarAddresses ? (
<AddressItem
caseId={caseId}
showRelativeDistance
containerStyle={styles.addressItemContainer}
key={ungroupedAddressItem?.id}
addressItem={ungroupedAddressItem}
showActionButtons
handleOldFeedbackRouting={() => {
handleOpenOldFeedbacks(ungroupedAddressItem);
}}
handleCloseRouting={() => {
navigateToScreen(PageRouteEnum.ADDITIONAL_ADDRESSES, commonParams);
}}
showSource
/>
) : (
<Accordion
accordionStyle={{ paddingTop: 16 }}
isExpansionDisabled={!ungroupedAddressItem?.similarAddresses.length}
touchableOpacity={0.8}
touchableDelay={50}
accordionHeader={
<AddressItem
caseId={caseId}
showRelativeDistance
containerStyle={styles.addressItemContainerAccordian}
key={ungroupedAddressItem?.id}
addressItem={ungroupedAddressItem}
showActionButtons
handleOldFeedbackRouting={() => {
handleOpenOldFeedbacks(ungroupedAddressItem);
}}
handleCloseRouting={() => {
navigateToScreen(PageRouteEnum.ADDITIONAL_ADDRESSES, commonParams);
}}
showSource
/>
}
onExpanded={(isExpanded: boolean) => {
handleAccordionExpand(isExpanded, ungroupedAddressItem?.id);
}}
>
{ungroupedAddressItem?.similarAddresses.length ? (
<View>
<SeparatorBorderComponent />
<Text
style={[
styles.textContainer,
styles.accordionDetailHeading,
GenericStyles.pb8,
]}
>
Similar addresses
</Text>
{ungroupedAddressItem.similarAddresses.map((similarAddress: IAddress) => (
<View style={GenericStyles.fill} key={similarAddress?.id}>
<SimilarAddressItem
addressItem={similarAddress}
containerStyle={styles.card}
showSource
/>
</View>
))}
</View>
) : null}
</Accordion>
)}
</View>
))}
</View>
@@ -163,6 +225,28 @@ const styles = StyleSheet.create({
paddingVertical: 16,
marginBottom: 12,
},
addressItemContainerAccordian: {
paddingHorizontal: 0,
paddingTop: 0,
paddingBottom: 16,
marginBottom: 0,
},
borderLine: {
borderWidth: 0.5,
borderColor: COLORS.BORDER.PRIMARY,
marginTop: 0,
marginBottom: 16,
},
accordionDetailHeading: {
color: COLORS.TEXT.BLACK_24,
},
card: {
padding: 16,
marginBottom: 16,
borderRadius: 8,
backgroundColor: COLORS.BACKGROUND.SILVER,
fontSize: 14,
},
});
export default UngroupedAddressContainer;

View File

@@ -87,10 +87,15 @@ const AddressGeolocation: React.FC<IAddressGeolocation> = ({ route: routeParams
maximumDistance: MAXIMUM_ALLOWED_DISTANCE_FOR_GROUPED_ADDRESSES,
currentLocationCoordinates: currentGeolocationCoordinates,
});
const metaAddresses = farAwayAddresses.map((farAwayAddress) => farAwayAddress?.metaAddress);
setUngroupedAddress([...res, ...metaAddresses]);
return [...res, ...metaAddresses];
const metaAddresses = farAwayAddresses.map((farAwayAddress) => {
return {
...farAwayAddress?.metaAddress,
similarAddresses: farAwayAddress?.similarAddresses,
};
});
const ungroupedAddresses = [...metaAddresses, ...res];
setUngroupedAddress(ungroupedAddresses);
return ungroupedAddresses;
});
useEffect(() => {

View File

@@ -54,7 +54,9 @@ export const EmptyListMessages = {
export const ToastMessages = {
VISIT_PLAN_OFFLINE: 'Please connect to the internet to update the visit plan',
ERROR_COPYING_LAN: 'Error copying LAN!!',
ERROR_COPYING_EMPLOYER_NAME: 'Error copying employer name!!',
SUCCESS_COPYING_LAN: 'LAN Copied Successfully!!',
SUCCESS_COPYING_EMPLOYER_NAME: 'Employer Name Copied Successfully!!',
FEEDBACK_SUCCESSFUL: 'Feedback submitted successfully!',
FEEDBACK_FAILED: 'Feedback submission failed',
FIRESTORE_SIGNIN_FAILED: 'Error signing in to Firestore',
@@ -75,6 +77,9 @@ export const ToastMessages = {
CASES_DELETION_DISABLED: 'Case deletion is disabled during the generation of visit plan',
GEOLOCATION_COORDINATES_INCORRECT: 'Geolocation not found',
IMAGE_UPLOAD_SUCCESS: 'Your ID card has been sent for approval',
WHATSAPP_FEEDBACK_SHARE_SUCCESS: 'Feedback shared successfully via WhatsApp',
WHATSAPP_FEEDBACK_SHARE_FAILURE: 'Feedback sharing failed via WhatsApp',
WHATSAPP_NOT_INSTALLED: 'WhatsApp is not installed on your device',
};
export enum BOTTOM_TAB_ROUTES {

View File

@@ -1,27 +1,27 @@
import React, { useEffect, useMemo } from 'react';
import { useAppDispatch, useAppSelector } from '../../hooks';
import { useAppDispatch, useAppSelector } from '@hooks';
import CasesList from './CasesList';
import { RootState } from '../../store/store';
import { initCrashlytics } from '../../components/utlis/firebaseUtils';
import { RootState } from '@store';
import { initCrashlytics } from '@utils/firebaseUtils';
import Layout from '../layout/Layout';
import BottomNavigator, { ITabScreen } from '../../../RN-UI-LIB/src/components/bottomNavigator';
import CasesIcon from '../../../RN-UI-LIB/src/Icons/CasesIcon';
import BottomNavigator, { ITabScreen } from '@rn-ui-lib/components/bottomNavigator';
import CasesIcon from '@rn-ui-lib/icons/CasesIcon';
import Profile from '../Profile';
import ProfileIcon from '../../../RN-UI-LIB/src/Icons/ProfileIcon';
import VisitPlanIcon from '../../../RN-UI-LIB/src/Icons/VisitPlanIcon';
import ProfileIcon from '@rn-ui-lib/icons/ProfileIcon';
import VisitPlanIcon from '@rn-ui-lib/icons/VisitPlanIcon';
import CasesActionButtons from './CasesActionButtons';
import FullScreenLoader from '../../../RN-UI-LIB/src/components/FullScreenLoader';
import { getCurrentScreen } from '../../components/utlis/navigationUtlis';
import FullScreenLoader from '@rn-ui-lib/components/FullScreenLoader';
import { getCurrentScreen } from '@utils/navigationUtlis';
import {
resetSelectedTodoList,
resetTodoList,
setLoading,
setVisitPlansUpdating,
} from '../../reducer/allCasesSlice';
import { addClickstreamEvent } from '../../services/clickstreamEventService';
import { CLICKSTREAM_EVENT_NAMES } from '../../common/Constants';
} from '@reducers/allCasesSlice';
import { addClickstreamEvent } from '@services/clickstreamEventService';
import { CLICKSTREAM_EVENT_NAMES } from '@common/Constants';
import { BOTTOM_TAB_ROUTES } from './constants';
import { getSelfieDocument } from '../../action/profileActions';
import { getSelfieDocument } from '@actions/profileActions';
const AllCasesMain = () => {
const { pendingList, pinnedList, completedList, loading } = useAppSelector(

View File

@@ -0,0 +1,95 @@
import { StyleProp, TextStyle, TouchableHighlight, View, ViewStyle } from 'react-native';
import React from 'react';
import { GenericStyles } from '../../../RN-UI-LIB/src/styles';
import { toast } from '../../../RN-UI-LIB/src/components/toast';
import { copyToClipboard } from '../../components/utlis/commonFunctions';
import Text from '../../../RN-UI-LIB/src/components/Text';
import CopyIcon from '../../../RN-UI-LIB/src/Icons/CopyIcon';
interface IChip {
text: string;
containerStyle: StyleProp<ViewStyle>;
subText?: string;
numberOfLines?: number;
clipCount?: number;
showCopyBtn?: boolean;
clickstreamEvent?: () => void;
successMessage?: string;
errorMessage?: string;
textStyle?: StyleProp<TextStyle>;
}
const Chip: React.FC<IChip> = (props) => {
const {
text,
containerStyle,
subText,
numberOfLines = 1,
clipCount = 0,
showCopyBtn = false,
clickstreamEvent,
successMessage = 'Copied Successfully!!',
errorMessage = 'Error copying!!',
textStyle,
} = props;
const copyData = () => {
clickstreamEvent?.();
if (!showCopyBtn) return;
if (text) {
copyToClipboard(text);
toast({
text1: successMessage,
type: 'info',
});
} else {
toast({
text1: errorMessage,
type: 'error',
});
}
};
const isClipped = text?.length >= clipCount;
const clippedStyle = isClipped ? GenericStyles.fill : {};
const renderChip = () => {
return (
<View
style={[
GenericStyles.chip,
GenericStyles.whiteBackground,
GenericStyles.row,
GenericStyles.alignCenter,
]}
>
<View style={clippedStyle}>
<Text
numberOfLines={numberOfLines}
ellipsizeMode={isClipped ? 'tail' : undefined}
small
style={textStyle}
>
{subText && subText}
{text}
</Text>
</View>
{showCopyBtn && (
<View style={GenericStyles.ml4}>
<CopyIcon />
</View>
)}
</View>
);
};
return showCopyBtn ? (
<TouchableHighlight style={[containerStyle, clippedStyle]} onPress={copyData}>
{renderChip()}
</TouchableHighlight>
) : (
<View style={[containerStyle, clippedStyle]}>{renderChip()}</View>
);
};
export default Chip;

View File

@@ -5,8 +5,12 @@ import { GenericStyles } from '../../../RN-UI-LIB/src/styles';
import { COLORS } from '../../../RN-UI-LIB/src/styles/colors';
import Text from '../../../RN-UI-LIB/src/components/Text';
import { formatAmount } from '../../../RN-UI-LIB/src/utlis/amount';
import LANChip from './LANChip';
import Tag from '../../../RN-UI-LIB/src/components/Tag';
import Chip from './Chip';
import EmploymentDetails from './EmploymentDetails';
import { addClickstreamEvent } from '../../services/clickstreamEventService';
import { CLICKSTREAM_EVENT_NAMES } from '../../common/Constants';
import { ToastMessages } from '../allCases/constants';
import { toTileCase } from '../../components/utlis/commonFunctions';
interface ICollectionCaseData {
caseData: CaseDetail;
@@ -23,22 +27,38 @@ const CollectionCaseData: React.FC<ICollectionCaseData> = ({ caseData }) => {
employmentDetail,
} = caseData;
const showEmploymentDetails = false;
return (
<View>
{fatherName && (
<Text style={[styles.headerText]} small>
S/O {fatherName}
Parent name: {toTileCase(fatherName)}
</Text>
)}
<View style={[GenericStyles.row, GenericStyles.mv8]}>
<Text style={[GenericStyles.chip, GenericStyles.whiteBackground]} small>
Current DPD {currentDpd}
</Text>
{loanAccountNumber && <LANChip loanAccountNumber={loanAccountNumber} />}
{loanAccountNumber && (
<Chip
text={loanAccountNumber}
showCopyBtn
containerStyle={GenericStyles.ml8}
subText="LAN "
clickstreamEvent={() =>
addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_COPY_LAN_CLICKED, {
lan: loanAccountNumber,
})
}
successMessage={ToastMessages.SUCCESS_COPYING_LAN}
errorMessage={ToastMessages.ERROR_COPYING_LAN}
clipCount={14}
/>
)}
</View>
<View style={[GenericStyles.row, GenericStyles.alignCenter, GenericStyles.mt4]}>
<EmploymentDetails employmentDetail={employmentDetail} />
</View>
<View style={[GenericStyles.row, GenericStyles.alignCenter, GenericStyles.mt8]}>
<Text style={[styles.greyText]} small>
DPD bucket {dpdBucket}
</Text>
@@ -46,29 +66,15 @@ const CollectionCaseData: React.FC<ICollectionCaseData> = ({ caseData }) => {
<Text style={[styles.greyText]} small>
POS {formatAmount(pos)}
</Text>
</View>
{showEmploymentDetails ? (
<View style={[GenericStyles.row, GenericStyles.alignCenter, GenericStyles.mt4]}>
{employmentDetail?.employmentType && (
<Text style={[styles.greyText]} small>
{employmentDetail.employmentType}
</Text>
)}
{employmentDetail?.employmentType && employmentDetail?.employerName && (
{collectionTag ? (
<>
<View style={styles.lineStyle} />
)}
{employmentDetail?.employerName && (
<Text style={[styles.greyText]} small numberOfLines={1}>
{employmentDetail.employerName}
<Text style={[styles.greyText]} small>
{collectionTag}
</Text>
)}
</View>
) : null}
{collectionTag ? (
<Text style={[styles.greyText]} small>
{collectionTag}
</Text>
) : null}
</>
) : null}
</View>
</View>
);
};

View File

@@ -217,7 +217,7 @@ const CollectionCaseDetails: React.FC<ICaseDetails> = (props) => {
const commonParams = {
loanAccountNumber: caseDetail.loanAccountNumber,
customerReferenceId: caseDetail.customerReferenceId,
caseId,
caseId: caseId,
};
navigateToScreen(route, { ...params, ...commonParams });
};
@@ -469,6 +469,7 @@ const CollectionCaseDetails: React.FC<ICaseDetails> = (props) => {
<FeedbackListContainer
feedbackList={[...getUnSyncedFeedback(), ...feedbackList]?.splice(0, 5)}
loanAccountNumber={caseDetail?.loanAccountNumber as string}
caseId={caseId}
/>
</View>
</Animated.View>

View File

@@ -0,0 +1,47 @@
import React from 'react';
import { StyleSheet } from 'react-native';
import { GenericStyles } from '../../../RN-UI-LIB/src/styles';
import { CLICKSTREAM_EVENT_NAMES } from '../../common/Constants';
import { addClickstreamEvent } from '../../services/clickstreamEventService';
import { ToastMessages } from '../allCases/constants';
import Chip from './Chip';
const EmploymentDetails = ({ employmentDetail }: any) => {
if (employmentDetail?.employerName) {
return (
<Chip
text={employmentDetail.employerName}
numberOfLines={0}
showCopyBtn
containerStyle={GenericStyles.mr8}
clipCount={42}
clickstreamEvent={() =>
addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_COPY_EMPLOYER_NAME_CLICKED, {
employerName: employmentDetail.employerName,
})
}
successMessage={ToastMessages.SUCCESS_COPYING_EMPLOYER_NAME}
errorMessage={ToastMessages.ERROR_COPYING_EMPLOYER_NAME}
textStyle={styles.lineHeight}
/>
);
}
if (employmentDetail?.employmentType) {
return (
<Chip
text={employmentDetail.employmentType}
containerStyle={GenericStyles.mr8}
clipCount={20}
/>
);
}
return null;
};
export default EmploymentDetails;
const styles = StyleSheet.create({
lineHeight: { lineHeight: 20, paddingVertical: 1.6 },
});

View File

@@ -25,6 +25,8 @@ import { setFeedbackHistoryLoading } from '../../../reducer/feedbackHistorySlice
import SuspenseLoader from '../../../../RN-UI-LIB/src/components/suspense_loader/SuspenseLoader';
import LineLoader from '../../../../RN-UI-LIB/src/components/suspense_loader/LineLoader';
import NoPastFeedbackIcon from '../../../assets/icons/NoPastFeedbackIcon';
import ChevronDown from '../../../assets/icons/ChevronDown';
import ChevronUp from '../../../assets/icons/ChevronUp';
const FEEDBACK_PAGE_TITLE = 'All feedbacks';
const ADDRESS_FEEDBACK_PAGE_TITLE = 'Address feedback';
@@ -39,13 +41,20 @@ interface IFeedbackDetailContainer {
addressReferenceIds?: string[];
addressText?: string;
activeFeedbackReferenceId?: string;
caseId: string;
};
};
}
const FeedbackDetailContainer: React.FC<IFeedbackDetailContainer> = ({ route: routeParams }) => {
const {
params: { loanAccountNumber, activeFeedbackReferenceId, addressReferenceIds, addressText },
params: {
loanAccountNumber,
activeFeedbackReferenceId,
addressReferenceIds,
addressText,
caseId,
},
} = routeParams;
const isPastFeedbackOnAddress = addressText || addressReferenceIds?.length;
@@ -212,7 +221,12 @@ const FeedbackDetailContainer: React.FC<IFeedbackDetailContainer> = ({ route: ro
}}
>
<Accordion
accordionStyle={[GenericStyles.pv12, GenericStyles.br8, getShadowStyle(4)]}
accordionStyle={[
GenericStyles.br8,
GenericStyles.ph16,
styles.accordianPadding,
getShadowStyle(4),
]}
isActive={feedback.referenceId === activeFeedbackReferenceId}
touchableDelay={50}
touchableOpacity={0.8}
@@ -221,11 +235,12 @@ const FeedbackDetailContainer: React.FC<IFeedbackDetailContainer> = ({ route: ro
key={feedback.referenceId}
feedbackItem={feedback}
isExpanded={isExpanded}
caseId={caseId}
/>
}
customExpandUi={{
whenCollapsed: <Text style={styles.accordionExpandBtn}>View more</Text>,
whenExpanded: <Text style={styles.accordionExpandBtn}>View less</Text>,
whenCollapsed: <ChevronDown />,
whenExpanded: <ChevronUp />,
}}
onExpanded={(value) => {
setIsExpanded(value);
@@ -321,6 +336,10 @@ const styles = StyleSheet.create({
lineHeight: 20,
color: COLORS.TEXT.BLUE,
},
accordianPadding: {
paddingTop: 16,
paddingBottom: 8,
},
});
export default FeedbackDetailContainer;

View File

@@ -1,5 +1,5 @@
import React, { ReactNode } from 'react';
import { View, StyleSheet, TouchableOpacity, Linking } from 'react-native';
import React, { ReactNode, useEffect, useState } from 'react';
import { View, StyleSheet, TouchableOpacity, Linking, Platform, NativeModules } 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';
@@ -8,25 +8,42 @@ import {
BUSINESS_TIME_FORMAT,
dateFormat,
} from '../../../../RN-UI-LIB/src/utlis/dates';
import { getGoogleMapUrl, sanitizeString } from '../../../components/utlis/commonFunctions';
import { FIELD_FEEDBACKS, ICallingFeedback, IFeedback } from '../../../types/feedback.types';
import { Address as IAddress } from '../interface';
import {
getGoogleMapUrl,
insertCommasinAmount,
sanitizeString,
} from '../../../components/utlis/commonFunctions';
import {
FIELD_FEEDBACKS,
ICallingFeedback,
IFeedback,
OPTION_TAG,
} from '../../../types/feedback.types';
import { CaseDetail, Address as IAddress } from '../interface';
import MapIcon from '../../../../RN-UI-LIB/src/Icons/MapIcon';
import { FEEDBACK_TYPE } from '../../../types/feedback.types';
import CallIcon from '../../../../RN-UI-LIB/src/Icons/CallIcon';
import { addClickstreamEvent } from '../../../services/clickstreamEventService';
import { CLICKSTREAM_EVENT_NAMES } from '../../../common/Constants';
import IconLabel from '../../../common/IconLabel';
import WhatsAppFeedbackShareIcon from '../../../assets/icons/WhatsAppIcon';
import ReactNativeBlobUtil from 'react-native-blob-util';
import { useAppSelector } from '../../../hooks';
import { toast } from '../../../../RN-UI-LIB/src/components/toast';
import { ToastMessages } from '../../allCases/constants';
import { sendFeedbackToWhatsapp } from '../../../components/utlis/DeviceUtils';
interface IFeedbackDetailItem {
feedbackItem: IFeedback;
isExpanded: boolean;
caseId: string;
}
const feedbackTypeIcon: Record<FEEDBACK_TYPE, ReactNode> = {
FIELD_VISIT: <MapIcon />,
INHOUSE_FIELD_VISIT: <MapIcon />,
SELF_CALL: <CallIcon fillColor={COLORS.TEXT.BLUE} />,
CALL_BRIDGE: <CallIcon fillColor={COLORS.TEXT.BLUE} />,
FIELD_VISIT: <MapIcon fillColor={COLORS.TEXT.LIGHT} />,
INHOUSE_FIELD_VISIT: <MapIcon fillColor={COLORS.TEXT.LIGHT} />,
SELF_CALL: <CallIcon fillColor={COLORS.TEXT.LIGHT} />,
CALL_BRIDGE: <CallIcon fillColor={COLORS.TEXT.LIGHT} />,
};
const getAddress = (address?: IAddress) => {
@@ -47,9 +64,123 @@ const openGeolocation = (latitude: string, longitude: string) => {
return Linking.openURL(geolocationUrl);
};
const FeedbackDetailItem = ({ feedbackItem, isExpanded }: IFeedbackDetailItem) => {
function getLocationLink(latitude: string, longitude: string): string {
const link = 'https://www.google.com/maps/search/?api=1&query=' + latitude + ',' + longitude;
return link;
}
const sendToWhatsappNative = (
message: string,
imageUrl: string,
mimeType: string,
caseDetails: CaseDetail,
agentId: string
) => {
sendFeedbackToWhatsapp(message, imageUrl, mimeType)
.then((res: boolean) => {
addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_SHARE_SUCCESSFUL, {
caseId: caseDetails?.id,
agentId: agentId,
});
})
.catch((err: Error) => {
if (err.message === '1') {
toast({
text1: ToastMessages.WHATSAPP_NOT_INSTALLED,
type: 'error',
});
} else {
toast({
text1: ToastMessages.WHATSAPP_FEEDBACK_SHARE_FAILURE,
type: 'error',
});
}
});
};
const sendToWhatsapp = (feedbackItem: IFeedback, caseDetails: CaseDetail, agentId: string) => {
addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_SHARE_FEEDBACK_CLICKED, {
caseId: caseDetails?.id,
agentId: agentId,
});
var message = `*Visit Feedback* for ${sanitizeString(caseDetails?.customerName)}
_${sanitizeString(dateFormat(new Date(feedbackItem?.createdAt), 'DD MMM, YYYY | HH:mm a.'))}_\n
*LAN*: ${sanitizeString(caseDetails?.loanAccountNumber)}
*DPD Bucket*: ${sanitizeString(caseDetails?.dpdBucket)}
*EMI Amount*: ₹${insertCommasinAmount(caseDetails?.outstandingEmiDetails?.[0]?.emiAmount)}\n
*Disposition*: ${sanitizeString(feedbackItem?.interactionStatus)}`;
const ptpDate = feedbackItem?.answerViews.filter((answer) => answer.questionName === 'PTP Date');
if (ptpDate.length > 0) {
message +=
' for ' + sanitizeString(dateFormat(new Date(ptpDate[0].inputDate), 'DD MMM, YYYY')) + '\n\n';
} else {
message += '\n\n';
}
message += '*Remarks*: ';
const answerList = feedbackItem?.answerViews?.filter(
(answer) => answer.questionName === 'Comments'
);
if (answerList.length > 0) {
message += sanitizeString(answerList[0]?.inputText) + '\n\n';
} else {
message += 'N.A.\n\n';
}
{
feedbackItem?.metadata?.interactionLatitude &&
feedbackItem?.metadata?.interactionLongitude &&
FIELD_FEEDBACKS.includes(feedbackItem?.type)
? (message +=
'*Location of feedback*: ' +
sanitizeString(
getLocationLink(
feedbackItem?.metadata?.interactionLatitude,
feedbackItem?.metadata?.interactionLongitude
)
) +
'\n\n')
: null;
}
const imagesList = feedbackItem?.answerViews.filter(
(answer) => answer.questionTag === OPTION_TAG.IMAGE_UPLOAD
);
var imageUrl = '';
const mimeType = 'image/*';
if (imagesList.length > 0) {
var imageUri = '';
imageUri = imagesList[0]?.inputText ? imagesList[0].inputText : '';
let imagePath = '';
ReactNativeBlobUtil.config({
fileCache: true,
})
.fetch('GET', imageUri)
.then((resp: any) => {
if (resp.info().status !== 200) {
return '';
} else {
imagePath = resp.path();
return resp.readFile('base64');
}
})
.then((base64Data: any) => {
imageUrl = base64Data;
sendToWhatsappNative(message, imageUrl, mimeType, caseDetails, agentId);
ReactNativeBlobUtil.fs.unlink(imagePath);
});
} else {
sendToWhatsappNative(message, imageUrl, mimeType, caseDetails, agentId);
}
};
const FeedbackDetailItem = ({ feedbackItem, isExpanded, caseId }: IFeedbackDetailItem) => {
const caseDetails = useAppSelector((state) => state.allCases.caseDetails[caseId]);
const { agentId } = useAppSelector((state) => ({ agentId: state.user.user?.referenceId!! }));
return (
<View style={[GenericStyles.ph8]}>
<View style={[styles.addressItem]}>
<View style={[GenericStyles.row, GenericStyles.alignCenter]}>
{feedbackTypeIcon[feedbackItem.type] ? (
<View style={GenericStyles.mr8}>{feedbackTypeIcon[feedbackItem.type]}</View>
@@ -70,13 +201,17 @@ const FeedbackDetailItem = ({ feedbackItem, isExpanded }: IFeedbackDetailItem) =
<Text
numberOfLines={1}
ellipsizeMode="tail"
style={[styles.textContainer, styles.cardFooterText, GenericStyles.fontSize12]}
style={[
styles.textContainer,
styles.cardFooterText,
GenericStyles.fontSize12,
GenericStyles.mb12,
]}
>
{sanitizeString(dateFormat(new Date(feedbackItem.createdAt), BUSINESS_DATE_FORMAT))}
&nbsp;&nbsp;&#9679;&nbsp;&nbsp;
<Text style={styles.bullet}> </Text>
{sanitizeString(dateFormat(new Date(feedbackItem.createdAt), BUSINESS_TIME_FORMAT))}
</Text>
<View style={[GenericStyles.borderTop, GenericStyles.w100, GenericStyles.mv16]} />
<Text
numberOfLines={isExpanded ? 100 : 3}
ellipsizeMode="tail"
@@ -93,18 +228,33 @@ const FeedbackDetailItem = ({ feedbackItem, isExpanded }: IFeedbackDetailItem) =
</Text>
{feedbackItem.metadata?.interactionLatitude && FIELD_FEEDBACKS.includes(feedbackItem.type) ? (
<TouchableOpacity
activeOpacity={0.7}
onPress={() =>
openGeolocation(
feedbackItem.metadata?.interactionLatitude,
feedbackItem.metadata?.interactionLongitude
)
}
style={[GenericStyles.row, GenericStyles.pv12]}
>
<Text style={[styles.textContainer, styles.geolocationBtn]}>Open map</Text>
</TouchableOpacity>
<>
<View style={styles.container}>
<TouchableOpacity
activeOpacity={0.7}
onPress={() =>
openGeolocation(
feedbackItem.metadata?.interactionLatitude,
feedbackItem.metadata?.interactionLongitude
)
}
style={[GenericStyles.row, styles.BtnPadding]}
>
<Text style={[styles.textContainer, styles.geolocationBtn]}>Open map</Text>
</TouchableOpacity>
<TouchableOpacity
activeOpacity={0.7}
onPress={() => sendToWhatsapp(feedbackItem, caseDetails, agentId)}
style={[GenericStyles.row, styles.BtnPadding]}
>
<IconLabel
text="Share"
icon={<WhatsAppFeedbackShareIcon />}
textStyle={{ color: COLORS.BASE.BLUE }}
/>
</TouchableOpacity>
</View>
</>
) : null}
</View>
);
@@ -121,7 +271,7 @@ const styles = StyleSheet.create({
},
cardLightTitle: {
fontWeight: '400',
color: '#BCBCBC',
color: COLORS.TEXT.BLACK,
},
cardFooterText: {
fontWeight: '400',
@@ -129,6 +279,22 @@ const styles = StyleSheet.create({
},
geolocationBtn: {
color: COLORS.BASE.BLUE,
marginRight: 20,
},
container: {
flexDirection: 'row',
alignItems: 'center',
marginTop: 0,
},
bullet: {
color: COLORS.TEXT.GREY_1,
},
BtnPadding: {
paddingTop: 8,
paddingBottom: 8,
},
addressItem: {
paddingHorizontal: 0,
},
});

View File

@@ -18,6 +18,7 @@ import { getCaseUnifiedData, UnifiedCaseDetailsTypes } from '../../../action/cas
interface IFeedbackListContainer {
loanAccountNumber: string;
feedbackList: (IFeedback | IUnSyncedFeedbackItem)[];
caseId: string;
}
interface IOfflineFeedbackListContainer {
@@ -66,6 +67,7 @@ const OfflineFeedbackListContainer: React.FC<IOfflineFeedbackListContainer> = ({
const FeedbackListContainer: React.FC<IFeedbackListContainer> = ({
loanAccountNumber,
feedbackList,
caseId,
}) => {
const [retryBtnCount, setRetryBtnCount] = useState(0);
@@ -97,6 +99,7 @@ const FeedbackListContainer: React.FC<IFeedbackListContainer> = ({
feedbackItem={feedbackItem}
loanAccountNumber={loanAccountNumber}
showHorizontalLine={++idx !== feedbackList.length}
caseId={caseId}
/>
))}
</View>

View File

@@ -15,11 +15,13 @@ interface IFeedbackListItem {
feedbackItem: IFeedback | IUnSyncedFeedbackItem;
showHorizontalLine?: boolean;
loanAccountNumber: string;
caseId: string;
}
const FeedbackListItem: React.FC<IFeedbackListItem> = ({
feedbackItem,
loanAccountNumber,
caseId,
showHorizontalLine = true,
}) => {
const handleRouting = (route: PageRouteEnum, params: object | undefined = undefined) => {
@@ -27,6 +29,7 @@ const FeedbackListItem: React.FC<IFeedbackListItem> = ({
const commonParams = {
loanAccountNumber,
activeFeedbackReferenceId: (feedbackItem as IFeedback).referenceId,
caseId: caseId,
};
navigateToScreen(route, { ...params, ...commonParams });
};

View File

@@ -0,0 +1,37 @@
import remoteConfig from '@react-native-firebase/remote-config';
import {
setActivityTimeOnApp,
setActivityTimeWindowHigh,
setActivityTimeWindowMedium,
} from '../common/AgentActivityConfigurableConstants';
const FIREBASE_FETCH_TIME = 15 * 60;
async function handleUpdatedConfigureValuesFromFirebase() {
await remoteConfig().fetch(FIREBASE_FETCH_TIME); //15 minutes
remoteConfig()
.activate()
.then((fetchedRemotely) => {
if (fetchedRemotely) {
console.log('Configs were fetched.');
} else {
console.log('No configs were fetched.');
}
})
.catch((error) => {
console.error(error);
})
.finally(() => {
const ACTIVITY_TIME_ON_APP = remoteConfig().getValue('ACTIVITY_TIME_ON_APP').asNumber();
const ACTIVITY_TIME_WINDOW_HIGH = remoteConfig()
.getValue('ACTIVITY_TIME_WINDOW_HIGH')
.asNumber();
const ACTIVITY_TIME_WINDOW_MEDIUM = remoteConfig()
.getValue('ACTIVITY_TIME_WINDOW_MEDIUM')
.asNumber();
setActivityTimeOnApp(ACTIVITY_TIME_ON_APP);
setActivityTimeWindowHigh(ACTIVITY_TIME_WINDOW_HIGH);
setActivityTimeWindowMedium(ACTIVITY_TIME_WINDOW_MEDIUM);
});
}
export default handleUpdatedConfigureValuesFromFirebase;

View File

@@ -39,6 +39,7 @@ export interface IAddress {
groupId: string;
primarySource?: PrimarySourcesType;
secondarySource?: string;
similarAddresses?: IAddress[];
}
export interface IGroupedAddressesItem {

View File

@@ -0,0 +1,5 @@
export enum AgentActivity {
HIGH = 'HIGH',
MEDIUM = 'MEDIUM',
LOW = 'LOW',
}

View File

@@ -2,4 +2,6 @@ export enum StorageKeys {
APP_FOREGROUND_TIMESTAMP = 'appForegroundTimestamp',
APP_BACKGROUND_TIMESTAMP = 'appBackgroundTimestamp',
IS_USER_ACTIVE = 'isUserActive',
USER_ACTIVITY_ON_APP = 'userActivityOnApp',
STATE_SET_TIMESTAMP = 'stateSetTimestamp',
}

View File

@@ -5,9 +5,34 @@
/* Visit https://aka.ms/tsconfig.json to read more about this file */
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"lib": ["dom","es5","es2020"],
"lib": [
"dom",
"es5",
"es2020"
],
/* Completeness */
"skipLibCheck": true /* Skip type checking all .d.ts files. */
"skipLibCheck": true, /* Skip type checking all .d.ts files. */
"baseUrl": ".",
"paths": {
"@root/*": ["."],
"@components/*": ["src/components/*"],
"@hooks": ["src/hooks/index"],
"@hooks/*": ["src/hooks/*"],
"@actions/*": ["src/action/*"],
"@reducers/*": ["src/reducer/*"],
"@constants/*": ["src/constants/*"],
"@screens/*": ["src/screens/*"],
"@services/*": ["src/services/*"],
"@types/*": ["src/types/*"],
"@common/*": ["src/common/*"],
"@assets/*": ["src/assets/*"],
"@store": ["src/store/store"],
"@utils/*": ["src/components/utlis/*"],
"@rn-ui-lib/components/*": ["RN-UI-LIB/src/components/*"],
"@rn-ui-lib/icons/*": ["RN-UI-LIB/src/Icons/*"],
"@rn-ui-lib/styles": ["RN-UI-LIB/src/styles/index"],
"@rn-ui-lib/colors": ["RN-UI-LIB/src/styles/colors"],
"@rn-ui-lib/utils/*": ["RN-UI-LIB/src/utlis/*"],
}
},
}

View File

@@ -1648,6 +1648,11 @@
resolved "https://registry.yarnpkg.com/@react-native-firebase/messaging/-/messaging-17.4.0.tgz#9e1df987183d0ca367d0922a14b14b7a53a140cf"
integrity sha512-RSiBBfyJ3K9G6TQfZc09XaGpxB9xlP5m9DYkqjbNIqnnTiahF90770lTAS65L1Ha78vCwVO2swIlk32XbcMcMQ==
"@react-native-firebase/remote-config@16.4.6":
version "16.4.6"
resolved "https://registry.yarnpkg.com/@react-native-firebase/remote-config/-/remote-config-16.4.6.tgz#dec215f2448f555cdba893a31f5cdf419b47b33e"
integrity sha512-2KPUao9xby+gp+JQUmikx9N0zcCLb0+6GkgI8//sYJ6Z3EaI53kx5kJHJDgYqdjF/zFjv3rm+yhm5LAgARPMHA==
"@react-native-google-signin/google-signin@9.0.2":
version "9.0.2"
resolved "https://registry.yarnpkg.com/@react-native-google-signin/google-signin/-/google-signin-9.0.2.tgz#fd9d0cbb58591265c2ea9404b2d2ea7e514b9ea9"
@@ -4700,6 +4705,18 @@ glob@5.0.15:
once "^1.3.0"
path-is-absolute "^1.0.0"
glob@7.0.6:
version "7.0.6"
resolved "https://registry.yarnpkg.com/glob/-/glob-7.0.6.tgz#211bafaf49e525b8cd93260d14ab136152b3f57a"
integrity sha512-f8c0rE8JiCxpa52kWPAOa3ZaYEnzofDzCQLCn3Vdk0Z5OVLq3BsRFJI4S4ykpeVW6QMGBUkMeUpoEgWnMTnw5Q==
dependencies:
fs.realpath "^1.0.0"
inflight "^1.0.4"
inherits "2"
minimatch "^3.0.2"
once "^1.3.0"
path-is-absolute "^1.0.0"
glob@7.1.6:
version "7.1.6"
resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6"
@@ -8231,6 +8248,14 @@ rimraf@~2.6.2:
dependencies:
glob "^7.1.3"
rn-fetch-blob@0.12.0:
version "0.12.0"
resolved "https://registry.yarnpkg.com/rn-fetch-blob/-/rn-fetch-blob-0.12.0.tgz#ec610d2f9b3f1065556b58ab9c106eeb256f3cba"
integrity sha512-+QnR7AsJ14zqpVVUbzbtAjq0iI8c9tCg49tIoKO2ezjzRunN7YL6zFSFSWZm6d+mE/l9r+OeDM3jmb2tBb2WbA==
dependencies:
base-64 "0.1.0"
glob "7.0.6"
route-recognizer@^0.3.3:
version "0.3.4"
resolved "https://registry.yarnpkg.com/route-recognizer/-/route-recognizer-0.3.4.tgz#39ab1ffbce1c59e6d2bdca416f0932611e4f3ca3"