diff --git a/App.tsx b/App.tsx index 3960551f..4f03242e 100644 --- a/App.tsx +++ b/App.tsx @@ -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(); }; diff --git a/RN-UI-LIB b/RN-UI-LIB index 4c7f4f68..8876fa8a 160000 --- a/RN-UI-LIB +++ b/RN-UI-LIB @@ -1 +1 @@ -Subproject commit 4c7f4f6880d96bffa856e04d7b2b4d383e42c5cf +Subproject commit 8876fa8a05de03421e39f598a1365cf0ddfdb8ee diff --git a/android/app/build.gradle b/android/app/build.gradle index 047ab2e5..649d58b3 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -131,8 +131,8 @@ def reactNativeArchitectures() { return value ? value.split(",") : ["armeabi-v7a", "x86", "x86_64", "arm64-v8a"] } -def VERSION_CODE = 83 -def VERSION_NAME = "2.3.10" +def VERSION_CODE = 88 +def VERSION_NAME = "2.4.4" android { ndkVersion rootProject.ext.ndkVersion diff --git a/android/app/google-services.json b/android/app/google-services.json index 9d2a90ba..1df006c6 100644 --- a/android/app/google-services.json +++ b/android/app/google-services.json @@ -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" - } \ No newline at end of file + } + ], + "configuration_version": "1" +} diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 4815e009..dbaef9b9 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -30,7 +30,12 @@ -> -21 -> lollipop 28 -> + + + + + + - + + + + + diff --git a/android/app/src/main/java/com/avapp/DeviceUtilsModule.java b/android/app/src/main/java/com/avapp/DeviceUtilsModule.java index 56c69465..879a05fe 100644 --- a/android/app/src/main/java/com/avapp/DeviceUtilsModule.java +++ b/android/app/src/main/java/com/avapp/DeviceUtilsModule.java @@ -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 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; + } + + + + } diff --git a/android/app/src/main/java/com/avapp/MainActivity.java b/android/app/src/main/java/com/avapp/MainActivity.java index 9ac0a557..d688d4d8 100644 --- a/android/app/src/main/java/com/avapp/MainActivity.java +++ b/android/app/src/main/java/com/avapp/MainActivity.java @@ -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 diff --git a/android/app/src/main/java/com/avapp/ScreenshotBlockerModule.java b/android/app/src/main/java/com/avapp/ScreenshotBlockerModule.java index 37cd1b86..36739f1a 100644 --- a/android/app/src/main/java/com/avapp/ScreenshotBlockerModule.java +++ b/android/app/src/main/java/com/avapp/ScreenshotBlockerModule.java @@ -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 + ); + } } } diff --git a/android/app/src/main/java/com/avapp/ScreenshotBlockerModulePackage.java b/android/app/src/main/java/com/avapp/ScreenshotBlockerModulePackage.java index f16bdd35..b62a1e78 100644 --- a/android/app/src/main/java/com/avapp/ScreenshotBlockerModulePackage.java +++ b/android/app/src/main/java/com/avapp/ScreenshotBlockerModulePackage.java @@ -1,6 +1,5 @@ package com.avapp; - import com.facebook.react.ReactPackage; import com.facebook.react.bridge.NativeModule; import com.facebook.react.bridge.ReactApplicationContext; diff --git a/babel.config.js b/babel.config.js index 5bff64bf..ae7561b4 100644 --- a/babel.config.js +++ b/babel.config.js @@ -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', }, }, ], diff --git a/package.json b/package.json index dd0fcbad..1bc39ce4 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/assets/icons/ChevronDown.tsx b/src/assets/icons/ChevronDown.tsx new file mode 100644 index 00000000..ce42c6ba --- /dev/null +++ b/src/assets/icons/ChevronDown.tsx @@ -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 ( + + + + ); +}; +export default ChevronDown; diff --git a/src/assets/icons/ChevronUp.tsx b/src/assets/icons/ChevronUp.tsx new file mode 100644 index 00000000..e4e5ba08 --- /dev/null +++ b/src/assets/icons/ChevronUp.tsx @@ -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 ( + + + + ); +}; +export default ChevronUp; diff --git a/src/assets/icons/WhatsAppFeedbackShareIcon.tsx b/src/assets/icons/WhatsAppFeedbackShareIcon.tsx new file mode 100644 index 00000000..10067e6f --- /dev/null +++ b/src/assets/icons/WhatsAppFeedbackShareIcon.tsx @@ -0,0 +1,11 @@ +import * as React from 'react'; +import Svg, { Path, SvgProps } from 'react-native-svg'; +const SvgComponent = (props: SvgProps) => ( + + + +); +export default SvgComponent; diff --git a/src/common/AgentActivityConfigurableConstants.ts b/src/common/AgentActivityConfigurableConstants.ts new file mode 100644 index 00000000..015db2b6 --- /dev/null +++ b/src/common/AgentActivityConfigurableConstants.ts @@ -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; +}; diff --git a/src/common/Constants.ts b/src/common/Constants.ts index 3896a70e..f9577e94 100644 --- a/src/common/Constants.ts +++ b/src/common/Constants.ts @@ -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: { diff --git a/src/common/TrackingComponent.tsx b/src/common/TrackingComponent.tsx index 8517cd73..73fe23df 100644 --- a/src/common/TrackingComponent.tsx +++ b/src/common/TrackingComponent.tsx @@ -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 = ({ children }) => { @@ -94,12 +103,16 @@ const TrackingComponent: React.FC = ({ 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 = ({ 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 = ({ 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 = ({ 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 = ({ 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 = ({ 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(); }; diff --git a/src/components/utlis/DeviceUtils.ts b/src/components/utlis/DeviceUtils.ts index 5443d7d7..926d85a3 100644 --- a/src/components/utlis/DeviceUtils.ts +++ b/src/components/utlis/DeviceUtils.ts @@ -7,3 +7,10 @@ export const locationEnabled = (): Promise => DeviceUtilsModule.isLocat // returns array of all the installed packages. export const getAllInstalledApp = (): Promise => DeviceUtilsModule.getAllInstalledApp(); + +// sends feedback data to whatsapp. +export const sendFeedbackToWhatsapp = ( + message: string, + imageUrl: string, + mimeType: string +): Promise => DeviceUtilsModule.sendFeedbackToWhatsapp(message, imageUrl, mimeType); diff --git a/src/components/utlis/ScreenshotBlocker.tsx b/src/components/utlis/ScreenshotBlocker.tsx new file mode 100644 index 00000000..d0269405 --- /dev/null +++ b/src/components/utlis/ScreenshotBlocker.tsx @@ -0,0 +1,5 @@ +import { NativeModules } from 'react-native'; + +const { ScreenshotBlocker } = NativeModules; + +export default ScreenshotBlocker; diff --git a/src/components/utlis/commonFunctions.ts b/src/components/utlis/commonFunctions.ts index 7cfd9a0d..7cb55682 100644 --- a/src/components/utlis/commonFunctions.ts +++ b/src/components/utlis/commonFunctions.ts @@ -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; +} diff --git a/src/hooks/capturingApi.ts b/src/hooks/capturingApi.ts index df42af7a..4c32cc1b 100644 --- a/src/hooks/capturingApi.ts +++ b/src/hooks/capturingApi.ts @@ -13,6 +13,7 @@ export interface IGeolocationPayload { accuracy: number; timestamp: number; isActiveOnApp: boolean; + userActivityOnApp: string; } export const sendLocationAndActivenessToServer = diff --git a/src/hooks/useScreenshotTracking.ts b/src/hooks/useScreenshotTracking.ts index 13db1aca..4a8a34fa 100644 --- a/src/hooks/useScreenshotTracking.ts +++ b/src/hooks/useScreenshotTracking.ts @@ -13,7 +13,6 @@ const useScreenshotTracking = () => { })); const screenshotEventEmitter = useMemo(() => new NativeEventEmitter(ScreenshotBlocker), []); - useEffect(() => { ScreenshotBlocker.startScreenshotTracking(); diff --git a/src/screens/addressGeolocation/SimilarAddressItem.tsx b/src/screens/addressGeolocation/SimilarAddressItem.tsx index ecbd8399..a9873961 100644 --- a/src/screens/addressGeolocation/SimilarAddressItem.tsx +++ b/src/screens/addressGeolocation/SimilarAddressItem.tsx @@ -155,11 +155,7 @@ function SimilarAddressItem({ ) : null} - + {sanitizeString(addressItem?.addressText)} { + return ; +}; + const UngroupedAddressContainer: React.FC = ({ route: routeParams }) => { const { params: { loanAccountNumber, caseId, customerReferenceId, fetchUngroupedAddress }, @@ -87,6 +88,14 @@ const UngroupedAddressContainer: React.FC = ({ 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 ( @@ -107,23 +116,76 @@ const UngroupedAddressContainer: React.FC = ({ route: routePa > {ungroupedAddressList?.length ? ( - {ungroupedAddressList.map((ungroupedAddressItem: IAddress) => ( + {ungroupedAddressList.map((ungroupedAddressItem: IAddress, index: number) => ( - { - handleOpenOldFeedbacks(ungroupedAddressItem); - }} - handleCloseRouting={() => { - navigateToScreen(PageRouteEnum.ADDITIONAL_ADDRESSES, commonParams); - }} - showSource - /> + {!ungroupedAddressItem?.similarAddresses ? ( + { + handleOpenOldFeedbacks(ungroupedAddressItem); + }} + handleCloseRouting={() => { + navigateToScreen(PageRouteEnum.ADDITIONAL_ADDRESSES, commonParams); + }} + showSource + /> + ) : ( + { + handleOpenOldFeedbacks(ungroupedAddressItem); + }} + handleCloseRouting={() => { + navigateToScreen(PageRouteEnum.ADDITIONAL_ADDRESSES, commonParams); + }} + showSource + /> + } + onExpanded={(isExpanded: boolean) => { + handleAccordionExpand(isExpanded, ungroupedAddressItem?.id); + }} + > + {ungroupedAddressItem?.similarAddresses.length ? ( + + + + Similar addresses + + {ungroupedAddressItem.similarAddresses.map((similarAddress: IAddress) => ( + + + + ))} + + ) : null} + + )} ))} @@ -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; diff --git a/src/screens/addressGeolocation/index.tsx b/src/screens/addressGeolocation/index.tsx index 3f9c0061..239246f5 100644 --- a/src/screens/addressGeolocation/index.tsx +++ b/src/screens/addressGeolocation/index.tsx @@ -87,10 +87,15 @@ const AddressGeolocation: React.FC = ({ 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(() => { diff --git a/src/screens/allCases/constants.ts b/src/screens/allCases/constants.ts index 7df28452..ec1d1c1d 100644 --- a/src/screens/allCases/constants.ts +++ b/src/screens/allCases/constants.ts @@ -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 { diff --git a/src/screens/allCases/index.tsx b/src/screens/allCases/index.tsx index 0fc99dff..2ec22de5 100644 --- a/src/screens/allCases/index.tsx +++ b/src/screens/allCases/index.tsx @@ -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( diff --git a/src/screens/caseDetails/Chip.tsx b/src/screens/caseDetails/Chip.tsx new file mode 100644 index 00000000..7202f19a --- /dev/null +++ b/src/screens/caseDetails/Chip.tsx @@ -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; + subText?: string; + numberOfLines?: number; + clipCount?: number; + showCopyBtn?: boolean; + clickstreamEvent?: () => void; + successMessage?: string; + errorMessage?: string; + textStyle?: StyleProp; +} + +const Chip: React.FC = (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 ( + + + + {subText && subText} + {text} + + + {showCopyBtn && ( + + + + )} + + ); + }; + + return showCopyBtn ? ( + + {renderChip()} + + ) : ( + {renderChip()} + ); +}; + +export default Chip; diff --git a/src/screens/caseDetails/CollectionCaseData.tsx b/src/screens/caseDetails/CollectionCaseData.tsx index 7ac3ce3f..2764f14e 100644 --- a/src/screens/caseDetails/CollectionCaseData.tsx +++ b/src/screens/caseDetails/CollectionCaseData.tsx @@ -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 = ({ caseData }) => { employmentDetail, } = caseData; - const showEmploymentDetails = false; - return ( {fatherName && ( - S/O {fatherName} + Parent name: {toTileCase(fatherName)} )} Current DPD {currentDpd} - {loanAccountNumber && } + {loanAccountNumber && ( + + addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_COPY_LAN_CLICKED, { + lan: loanAccountNumber, + }) + } + successMessage={ToastMessages.SUCCESS_COPYING_LAN} + errorMessage={ToastMessages.ERROR_COPYING_LAN} + clipCount={14} + /> + )} + + + DPD bucket {dpdBucket} @@ -46,29 +66,15 @@ const CollectionCaseData: React.FC = ({ caseData }) => { POS {formatAmount(pos)} - - {showEmploymentDetails ? ( - - {employmentDetail?.employmentType && ( - - {employmentDetail.employmentType} - - )} - {employmentDetail?.employmentType && employmentDetail?.employerName && ( + {collectionTag ? ( + <> - )} - {employmentDetail?.employerName && ( - - {employmentDetail.employerName} + + {collectionTag} - )} - - ) : null} - {collectionTag ? ( - - {collectionTag} - - ) : null} + + ) : null} + ); }; diff --git a/src/screens/caseDetails/CollectionCaseDetail.tsx b/src/screens/caseDetails/CollectionCaseDetail.tsx index e658383e..6afa8c5b 100644 --- a/src/screens/caseDetails/CollectionCaseDetail.tsx +++ b/src/screens/caseDetails/CollectionCaseDetail.tsx @@ -217,7 +217,7 @@ const CollectionCaseDetails: React.FC = (props) => { const commonParams = { loanAccountNumber: caseDetail.loanAccountNumber, customerReferenceId: caseDetail.customerReferenceId, - caseId, + caseId: caseId, }; navigateToScreen(route, { ...params, ...commonParams }); }; @@ -469,6 +469,7 @@ const CollectionCaseDetails: React.FC = (props) => { diff --git a/src/screens/caseDetails/EmploymentDetails.tsx b/src/screens/caseDetails/EmploymentDetails.tsx new file mode 100644 index 00000000..0dfbe5a2 --- /dev/null +++ b/src/screens/caseDetails/EmploymentDetails.tsx @@ -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 ( + + 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 ( + + ); + } + + return null; +}; + +export default EmploymentDetails; + +const styles = StyleSheet.create({ + lineHeight: { lineHeight: 20, paddingVertical: 1.6 }, +}); diff --git a/src/screens/caseDetails/feedback/FeedbackDetailContainer.tsx b/src/screens/caseDetails/feedback/FeedbackDetailContainer.tsx index 3a19a0db..78186917 100644 --- a/src/screens/caseDetails/feedback/FeedbackDetailContainer.tsx +++ b/src/screens/caseDetails/feedback/FeedbackDetailContainer.tsx @@ -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 = ({ 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 = ({ route: ro }} > = ({ route: ro key={feedback.referenceId} feedbackItem={feedback} isExpanded={isExpanded} + caseId={caseId} /> } customExpandUi={{ - whenCollapsed: View more, - whenExpanded: View less, + whenCollapsed: , + whenExpanded: , }} 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; diff --git a/src/screens/caseDetails/feedback/FeedbackDetailItem.tsx b/src/screens/caseDetails/feedback/FeedbackDetailItem.tsx index bcb5e69c..3ca7bfea 100644 --- a/src/screens/caseDetails/feedback/FeedbackDetailItem.tsx +++ b/src/screens/caseDetails/feedback/FeedbackDetailItem.tsx @@ -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 = { - FIELD_VISIT: , - INHOUSE_FIELD_VISIT: , - SELF_CALL: , - CALL_BRIDGE: , + FIELD_VISIT: , + INHOUSE_FIELD_VISIT: , + SELF_CALL: , + CALL_BRIDGE: , }; 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 ( - + {feedbackTypeIcon[feedbackItem.type] ? ( {feedbackTypeIcon[feedbackItem.type]} @@ -70,13 +201,17 @@ const FeedbackDetailItem = ({ feedbackItem, isExpanded }: IFeedbackDetailItem) = {sanitizeString(dateFormat(new Date(feedbackItem.createdAt), BUSINESS_DATE_FORMAT))} -   ●   + {sanitizeString(dateFormat(new Date(feedbackItem.createdAt), BUSINESS_TIME_FORMAT))} - {feedbackItem.metadata?.interactionLatitude && FIELD_FEEDBACKS.includes(feedbackItem.type) ? ( - - openGeolocation( - feedbackItem.metadata?.interactionLatitude, - feedbackItem.metadata?.interactionLongitude - ) - } - style={[GenericStyles.row, GenericStyles.pv12]} - > - Open map - + <> + + + openGeolocation( + feedbackItem.metadata?.interactionLatitude, + feedbackItem.metadata?.interactionLongitude + ) + } + style={[GenericStyles.row, styles.BtnPadding]} + > + Open map + + sendToWhatsapp(feedbackItem, caseDetails, agentId)} + style={[GenericStyles.row, styles.BtnPadding]} + > + } + textStyle={{ color: COLORS.BASE.BLUE }} + /> + + + ) : null} ); @@ -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, }, }); diff --git a/src/screens/caseDetails/feedback/FeedbackListContainer.tsx b/src/screens/caseDetails/feedback/FeedbackListContainer.tsx index 721f983b..31565c4a 100644 --- a/src/screens/caseDetails/feedback/FeedbackListContainer.tsx +++ b/src/screens/caseDetails/feedback/FeedbackListContainer.tsx @@ -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 = ({ const FeedbackListContainer: React.FC = ({ loanAccountNumber, feedbackList, + caseId, }) => { const [retryBtnCount, setRetryBtnCount] = useState(0); @@ -97,6 +99,7 @@ const FeedbackListContainer: React.FC = ({ feedbackItem={feedbackItem} loanAccountNumber={loanAccountNumber} showHorizontalLine={++idx !== feedbackList.length} + caseId={caseId} /> ))} diff --git a/src/screens/caseDetails/feedback/FeedbackListItem.tsx b/src/screens/caseDetails/feedback/FeedbackListItem.tsx index 3b7c06a7..f7fc51c5 100644 --- a/src/screens/caseDetails/feedback/FeedbackListItem.tsx +++ b/src/screens/caseDetails/feedback/FeedbackListItem.tsx @@ -15,11 +15,13 @@ interface IFeedbackListItem { feedbackItem: IFeedback | IUnSyncedFeedbackItem; showHorizontalLine?: boolean; loanAccountNumber: string; + caseId: string; } const FeedbackListItem: React.FC = ({ feedbackItem, loanAccountNumber, + caseId, showHorizontalLine = true, }) => { const handleRouting = (route: PageRouteEnum, params: object | undefined = undefined) => { @@ -27,6 +29,7 @@ const FeedbackListItem: React.FC = ({ const commonParams = { loanAccountNumber, activeFeedbackReferenceId: (feedbackItem as IFeedback).referenceId, + caseId: caseId, }; navigateToScreen(route, { ...params, ...commonParams }); }; diff --git a/src/services/firebaseFetchAndUpdate.service.ts b/src/services/firebaseFetchAndUpdate.service.ts new file mode 100644 index 00000000..1816fb35 --- /dev/null +++ b/src/services/firebaseFetchAndUpdate.service.ts @@ -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; diff --git a/src/types/addressGeolocation.types.ts b/src/types/addressGeolocation.types.ts index 98d18a41..03959e8e 100644 --- a/src/types/addressGeolocation.types.ts +++ b/src/types/addressGeolocation.types.ts @@ -39,6 +39,7 @@ export interface IAddress { groupId: string; primarySource?: PrimarySourcesType; secondarySource?: string; + similarAddresses?: IAddress[]; } export interface IGroupedAddressesItem { diff --git a/src/types/agentActivity.ts b/src/types/agentActivity.ts new file mode 100644 index 00000000..0184175b --- /dev/null +++ b/src/types/agentActivity.ts @@ -0,0 +1,5 @@ +export enum AgentActivity { + HIGH = 'HIGH', + MEDIUM = 'MEDIUM', + LOW = 'LOW', +} diff --git a/src/types/storageKeys.ts b/src/types/storageKeys.ts index c18bd31d..bb480c6a 100644 --- a/src/types/storageKeys.ts +++ b/src/types/storageKeys.ts @@ -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', } diff --git a/tsconfig.json b/tsconfig.json index bc394ebd..d9248cf8 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -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/*"], + } }, } diff --git a/yarn.lock b/yarn.lock index 5fe23084..27be2e18 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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"