diff --git a/App.tsx b/App.tsx index 54bcd648..5539ee6b 100644 --- a/App.tsx +++ b/App.tsx @@ -18,12 +18,12 @@ import CodePush from 'react-native-code-push'; import store, { persistor } from './src/store/store'; import { navigationRef } from './src/components/utlis/navigationUtlis'; -import FullScreenLoaderWrapper from './src/common/FullScreenLoaderWrapper'; +import FullScreenLoader from './RN-UI-LIB/src/components/FullScreenLoader'; 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,9 +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 FullScreenLoader from './RN-UI-LIB/src/components/FullScreenLoader'; +import handleUpdatedConfigureValuesFromFirebase from './src/services/firebaseFetchAndUpdate.service'; +import { addClickstreamEvent } from './src/services/clickstreamEventService'; initSentry(); @@ -104,6 +108,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({ @@ -129,6 +139,9 @@ function App() { setIsGlobalDocumentMapLoaded(true); })(); checkCodePushAndSync(); + handleUpdatedConfigureValuesFromFirebase(); + setForegroundTimeStampAndClickstream(); + return () => { appStateChange.remove(); }; diff --git a/android/app/build.gradle b/android/app/build.gradle index c4be2701..eb7bd643 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -131,16 +131,10 @@ def reactNativeArchitectures() { return value ? value.split(",") : ["armeabi-v7a", "x86", "x86_64", "arm64-v8a"] } -def VERSION_CODE = 85 -def VERSION_NAME = "2.4.1" +def VERSION_CODE = 86 +def VERSION_NAME = "2.4.2" android { - packagingOptions { - pickFirst 'lib/x86/libc++_shared.so' - pickFirst 'lib/x86_64/libc++_shared.so' - pickFirst 'lib/armeabi-v7a/libc++_shared.so' - pickFirst 'lib/arm64-v8a/libc++_shared.so' - } ndkVersion rootProject.ext.ndkVersion compileSdkVersion rootProject.ext.compileSdkVersion diff --git a/android/app/src/main/java/com/avapp/MainApplication.java b/android/app/src/main/java/com/avapp/MainApplication.java index b9343755..77f64a53 100644 --- a/android/app/src/main/java/com/avapp/MainApplication.java +++ b/android/app/src/main/java/com/avapp/MainApplication.java @@ -47,6 +47,7 @@ public class MainApplication extends Application implements ReactApplication { List packages = new PackageList(this).getPackages(); // Packages that cannot be autolinked yet can be added manually here, for example: packages.add(new DeviceUtilsModulePackage()); + packages.add(new ScreenshotBlockerModulePackage()); return packages; } diff --git a/android/app/src/main/java/com/avapp/ScreenshotBlockerModule.java b/android/app/src/main/java/com/avapp/ScreenshotBlockerModule.java new file mode 100644 index 00000000..37cd1b86 --- /dev/null +++ b/android/app/src/main/java/com/avapp/ScreenshotBlockerModule.java @@ -0,0 +1,101 @@ +package com.avapp; + +import android.app.Activity; +import android.view.WindowManager; +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.bridge.ReactContextBaseJavaModule; +import com.facebook.react.bridge.ReactMethod; +import com.facebook.react.modules.core.DeviceEventManagerModule; +import com.facebook.react.bridge.WritableMap; +import com.facebook.react.bridge.Arguments; + +import android.os.Handler; +import android.os.Looper; +import android.content.ContentResolver; +import android.database.ContentObserver; +import android.net.Uri; +import android.provider.MediaStore; + +public class ScreenshotBlockerModule extends ReactContextBaseJavaModule { + private ReactApplicationContext reactContext; + private ContentObserver contentObserver; + private boolean isTracking = false; + + public ScreenshotBlockerModule(ReactApplicationContext reactContext) { + super(reactContext); + this.reactContext = reactContext; + } + + @Override + public String getName() { + return "ScreenshotBlocker"; + } + + @ReactMethod + public void blockScreenshots() { + new Handler(Looper.getMainLooper()).post(new Runnable() { + Activity activity = getCurrentActivity(); + + @Override + public void run() { + if (activity != null) { + activity.getWindow().setFlags( + WindowManager.LayoutParams.FLAG_SECURE, + WindowManager.LayoutParams.FLAG_SECURE + ); + } + } + }); + } + + @ReactMethod + public void unblockScreenshots() { + new Handler(Looper.getMainLooper()).post(new Runnable() { + Activity activity = getCurrentActivity(); + + @Override + public void run() { + if (activity != null) { + activity.getWindow().clearFlags( + WindowManager.LayoutParams.FLAG_SECURE + ); + } + } + }); + } + + @ReactMethod + public void startScreenshotTracking() { + if (!isTracking) { + isTracking = true; + ContentResolver contentResolver = getCurrentActivity().getContentResolver(); + contentObserver = new ContentObserver(new Handler(Looper.getMainLooper())) { + long lastEventTimestamp = 0; + + @Override + public void onChange(boolean selfChange, Uri uri) { + long currentTimeMillis = System.currentTimeMillis(); + + if (currentTimeMillis - lastEventTimestamp > 1000) { + sendScreenshotEvent(); + lastEventTimestamp = currentTimeMillis; + } + } + }; + + contentResolver.registerContentObserver( + MediaStore.Images.Media.EXTERNAL_CONTENT_URI, + true, + contentObserver + ); + } + } + + private void sendScreenshotEvent() { + WritableMap params = Arguments.createMap(); + params.putBoolean("isScreenshotDetected", true); + reactContext + .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class) + .emit("screenshotTaken", params); + } +} \ No newline at end of file diff --git a/android/app/src/main/java/com/avapp/ScreenshotBlockerModulePackage.java b/android/app/src/main/java/com/avapp/ScreenshotBlockerModulePackage.java new file mode 100644 index 00000000..b62a1e78 --- /dev/null +++ b/android/app/src/main/java/com/avapp/ScreenshotBlockerModulePackage.java @@ -0,0 +1,27 @@ +package com.avapp; + +import com.facebook.react.ReactPackage; +import com.facebook.react.bridge.NativeModule; +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.uimanager.ViewManager; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public class ScreenshotBlockerModulePackage implements ReactPackage { + @Override + public List createViewManagers(ReactApplicationContext reactContext) { + return Collections.emptyList(); + } + + @Override + public List createNativeModules( + ReactApplicationContext reactContext) { + List modules = new ArrayList<>(); + + modules.add(new ScreenshotBlockerModule(reactContext)); + + return modules; + } +} \ No newline at end of file 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 7e39aedb..d270d32e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "AV_APP", - "version": "2.4.1", + "version": "2.4.2", "private": true, "scripts": { "android:dev": "yarn move:dev && react-native run-android", @@ -11,7 +11,7 @@ "android-field:prod": "yarn move:prod && react-native run-android --variant=fieldAgentsQADebug", "release-field:dev": "yarn move:dev && react-native run-android --variant=fieldAgentsQARelease && cd android && ./gradlew assemblefieldAgentsQARelease", "release-field:qa": "yarn move:qa && react-native run-android --variant=fieldAgentsQARelease && cd android && ./gradlew assemblefieldAgentsQARelease", - "release-field:prod": "yarn move:dev && react-native run-android --variant=fieldAgentsProdRelease && cd android && ./gradlew assemblefieldAgentsProdRelease", + "release-field:prod": "yarn move:prod && react-native run-android --variant=fieldAgentsProdRelease && cd android && ./gradlew assemblefieldAgentsProdRelease", "android-calling:dev": "yarn move:dev && react-native run-android --variant=callingAgentsQADebug", "android-calling:qa": "yarn move:qa && react-native run-android --variant=callingAgentsQADebug", "android-calling:prod": "yarn move:prod && react-native run-android --variant=callingAgentsQADebug", @@ -49,6 +49,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", @@ -80,6 +81,7 @@ "react-native-gzip": "1.0.0", "react-native-image-picker": "4.10.2", "react-native-pager-view": "6.1.2", + "react-native-pdf-renderer": "1.1.1", "react-native-permissions": "3.6.1", "react-native-qrcode-svg": "^6.2.0", "react-native-safe-area-context": "4.4.1", 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 8cf8ca88..d3d1d46a 100644 --- a/src/common/Constants.ts +++ b/src/common/Constants.ts @@ -95,6 +95,10 @@ export const CLICKSTREAM_EVENT_NAMES = { name: 'FA_LOGIN_SCREEN_SEND_OTP_API_FAILED', description: 'Send OTP API failed', }, + FA_SCREENSHOT_TAKEN: { + name: 'FA_SCREENSHOT_TAKEN', + description: 'Screenshot atempt detected', + }, // OTP screen AV_OTP_SCREEN_LOAD: { name: 'FA_OTP_SCREEN_LOAD', description: 'OTP screen loaded' }, diff --git a/src/common/TrackingComponent.tsx b/src/common/TrackingComponent.tsx index 5eabd878..9d8e448f 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/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/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 new file mode 100644 index 00000000..4a8a34fa --- /dev/null +++ b/src/hooks/useScreenshotTracking.ts @@ -0,0 +1,39 @@ +import { useEffect, useMemo } from 'react'; +import { NativeEventEmitter } from 'react-native'; +import { useAppSelector } from '.'; +import { CLICKSTREAM_EVENT_NAMES } from '../common/Constants'; +import { getCurrentScreen } from '../components/utlis/navigationUtlis'; +import ScreenshotBlocker from '../components/utlis/ScreenshotBlocker'; +import { addClickstreamEvent } from '../services/clickstreamEventService'; + +const useScreenshotTracking = () => { + const { user, allCases } = useAppSelector((state) => ({ + user: state.user, + allCases: state.allCases, + })); + + const screenshotEventEmitter = useMemo(() => new NativeEventEmitter(ScreenshotBlocker), []); + useEffect(() => { + ScreenshotBlocker.startScreenshotTracking(); + + const screenshotTakenSubscription = screenshotEventEmitter.addListener( + 'screenshotTaken', + (event) => { + if (event?.isScreenshotDetected) { + addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_SCREENSHOT_TAKEN, { + userId: user?.user?.referenceId ?? '', + caseId: allCases.selectedCaseId ?? '', + page: getCurrentScreen()?.name, + }); + return; + } + } + ); + + return () => { + if (screenshotTakenSubscription) screenshotTakenSubscription.remove(); + }; + }, [allCases?.selectedCaseId, user?.user?.referenceId]); +}; + +export default useScreenshotTracking; diff --git a/src/reducer/allCasesSlice.ts b/src/reducer/allCasesSlice.ts index bf1ffab6..fae5f5f4 100644 --- a/src/reducer/allCasesSlice.ts +++ b/src/reducer/allCasesSlice.ts @@ -44,6 +44,7 @@ interface IAllCasesSlice { completedList: ICaseItem[]; pinnedList: ICaseItem[]; newVisitedCases: string[]; + selectedCaseId: string; } const initialState: IAllCasesSlice = { @@ -67,6 +68,7 @@ const initialState: IAllCasesSlice = { completedList: [], pinnedList: [], newVisitedCases: [], + selectedCaseId: '', }; const getCaseListComponents = (casesList: ICaseItem[], caseDetails: Record) => { @@ -563,6 +565,9 @@ const allCasesSlice = createSlice({ } }); }, + setSelectedCaseId: (state, action) => { + state.selectedCaseId = action.payload; + }, }, }); @@ -584,6 +589,7 @@ export const { resetNewVisitedCases, syncCasesByFallback, setCasesImageUri, + setSelectedCaseId, } = allCasesSlice.actions; export default allCasesSlice.reducer; diff --git a/src/screens/allCases/index.tsx b/src/screens/allCases/index.tsx index eecb965d..3a04550d 100644 --- a/src/screens/allCases/index.tsx +++ b/src/screens/allCases/index.tsx @@ -1,28 +1,28 @@ -import React, { useEffect, useMemo, useRef, useState } from 'react'; -import { useAppDispatch, useAppSelector } from '../../hooks'; +import React, { useEffect, useMemo } from 'react'; +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 FullScreenLoaderWrapper from '../../common/FullScreenLoaderWrapper'; -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 ErrorBoundary from '../../common/ErrorBoundary'; +import { getSelfieDocument } from '@actions/profileActions'; +import FullScreenLoaderWrapper from '@common/FullScreenLoaderWrapper'; const AllCasesMain = () => { const { pendingList, pinnedList, completedList, loading } = useAppSelector( diff --git a/src/screens/auth/AuthRouter.tsx b/src/screens/auth/AuthRouter.tsx index c9af2d70..868f28b9 100644 --- a/src/screens/auth/AuthRouter.tsx +++ b/src/screens/auth/AuthRouter.tsx @@ -23,6 +23,7 @@ import { } from '../../components/utlis/DeviceUtils'; import { getAppVersion, getPhoneNumberString } from '../../components/utlis/commonFunctions'; import AnswerRender from '../../components/form/AnswerRender'; +import useScreenshotTracking from '../../hooks/useScreenshotTracking'; const AuthRouter = () => { const dispatch = useAppDispatch(); @@ -98,6 +99,7 @@ const AuthRouter = () => { // Firebase cloud messaging listener useFCM(); + useScreenshotTracking(); return isLoggedIn ? ( diff --git a/src/screens/caseDetails/CollectionCaseDetail.tsx b/src/screens/caseDetails/CollectionCaseDetail.tsx index fbeb55c8..3131fcf1 100644 --- a/src/screens/caseDetails/CollectionCaseDetail.tsx +++ b/src/screens/caseDetails/CollectionCaseDetail.tsx @@ -42,6 +42,7 @@ import { getLoanAccountNumber } from '../../components/utlis/commonFunctions'; import EmiBreakupBottomSheet from '../emiSchedule/EmiBreakupBottomSheet'; import { CollectionCaseWidgetId } from '../../types/template.types'; import { useIsFocused } from '@react-navigation/native'; +import { setSelectedCaseId } from '../../reducer/allCasesSlice'; interface ICaseDetails { route: { @@ -78,6 +79,14 @@ const CollectionCaseDetails: React.FC = (props) => { }, } = props; + useEffect(() => { + if (caseId) dispatch(setSelectedCaseId(caseId)); + + return () => { + dispatch(setSelectedCaseId('')); + }; + }, [caseId]); + const dispatch = useAppDispatch(); const isFocused = useIsFocused(); const isOnline = useIsOnline(); 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/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 2f13bddd..56f6ead8 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" @@ -7775,6 +7780,11 @@ react-native-pager-view@6.1.2: resolved "https://registry.yarnpkg.com/react-native-pager-view/-/react-native-pager-view-6.1.2.tgz#3522079b9a9d6634ca5e8d153bc0b4d660254552" integrity sha512-qs2KSFc+7N7B+UZ6SG2sTvCkppagm5fVyRclv1KFKc7lDtrhXLzN59tXJw575LDP/dRJoXsNwqUAhZJdws6ABQ== +react-native-pdf-renderer@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/react-native-pdf-renderer/-/react-native-pdf-renderer-1.1.1.tgz#73a5428c034a7c76bc5fe3435e584cd6493bdfb2" + integrity sha512-XNtSwtMKvH90YcJxZfUu0HRE/DjiXxXIaSa0PCAPPROFOXB5bQPTgZ5n70w98tzjQ5smKMJnMYDFJQV5/pA01w== + react-native-permissions@3.6.1: version "3.6.1" resolved "https://registry.yarnpkg.com/react-native-permissions/-/react-native-permissions-3.6.1.tgz#73adcc1cef8cd57a9ef167b4507405f4ff5749c4"