TP-22455 | Notifications | Aman C (#207)

* TP-22449 | Case Details Additional Information | Aman C

* TP-22961 | collectionCaseStatus used to move case to completed

* TP-22455 | Rever collectionCaseStatus

* TP-22455 | Notification changes

* TP-22455 | notification changes

* TP-22455 | Notification changes

* TP-22455 | Notification clickstream events added

* TP-22455 | Notification click stream events added

* TP-22455 | Removed unused file, updated submodule
This commit is contained in:
Aman Chaturvedi
2023-04-05 15:46:55 +05:30
committed by GitHub Enterprise
parent 12d3a9e8b4
commit 6367b1dd01
21 changed files with 1040 additions and 22 deletions

View File

@@ -35,6 +35,10 @@ import FeedbackDetailContainer from './src/screens/caseDetails/feedback/Feedback
import EmiSchedule from './src/screens/emiSchedule';
import { NetworkStatusService } from "./src/services/network-monitoring.service";
import AddNewNumber from './src/screens/addNewNumber';
import useFCM from './src/hooks/useFCM';
import Notifications from './src/screens/notifications';
import { getNotifications, notificationAction } from './src/action/notificationActions';
import useIsOnline from './src/hooks/useIsOnline';
const ANIMATION_DURATION = 300;
@@ -54,6 +58,9 @@ const ProtectedRouter = () => {
(state: RootState) => state.user,
);
const { newVisitedCases, caseDetails } = useAppSelector(state => state.allCases);
const { data = [], notificationsWithActions } = useAppSelector(state => state.notifications);
const {isLoggedIn, deviceId, sessionDetails} = user;
const isOnline = useIsOnline();
useEffect(() => {
if(newVisitedCases?.length) {
@@ -73,6 +80,20 @@ const ProtectedRouter = () => {
}
}, [newVisitedCases])
useEffect(() => {
if (data?.length) {
return;
}
dispatch(getNotifications());
}, []);
useEffect(() => {
if(isLoggedIn && isOnline && notificationsWithActions.length) {
dispatch(notificationAction(notificationsWithActions));
}
}, [isLoggedIn, isOnline, notificationsWithActions])
const avTemplate = useSelector(
(state: RootState) => state.case.templateData[CaseAllocationType.ADDRESS_VERIFICATION_CASE],
);
@@ -81,14 +102,15 @@ const ProtectedRouter = () => {
(state: RootState) => state.case.templateData[CaseAllocationType.COLLECTION_CASE],
);
const {isLoggedIn, deviceId, sessionDetails} = user;
interactionsHandler()
const dispatch = useAppDispatch();
NetworkStatusService.listenForOnline(dispatch);
// Firestore listener hook
useFirestoreUpdates();
useFirestoreUpdates();
// Firebase cloud messaging
useFCM();
if (!deviceId) {
getUniqueId().then(id => dispatch(setDeviceId(id)));
@@ -275,6 +297,16 @@ const ProtectedRouter = () => {
}}
listeners={getScreenFocusListenerObj}
/>
<Stack.Screen
name="Notifications"
component={Notifications}
options={{
header: () => null,
animation: 'slide_from_right',
animationDuration: ANIMATION_DURATION,
}}
listeners={getScreenFocusListenerObj}
/>
</>
) : (
<>

View File

@@ -30,6 +30,7 @@
"@react-native-firebase/crashlytics": "16.5.0",
"@react-native-firebase/database": "16.4.6",
"@react-native-firebase/firestore": "16.5.0",
"@react-native-firebase/messaging": "17.4.0",
"@react-navigation/bottom-tabs": "6.5.5",
"@react-navigation/native": "6.1.4",
"@react-navigation/native-stack": "6.9.4",

View File

@@ -19,6 +19,7 @@ import { AppDispatch } from '../store/store';
import { setGlobalUserData } from '../constants/Global';
import { resetCasesData } from '../reducer/allCasesSlice';
import {toast} from "../../RN-UI-LIB/src/components/toast";
import AsyncStorage from '@react-native-async-storage/async-storage';
import { clearAllAsyncStorage } from '../components/utlis/commonFunctions';
import { logError } from '../components/utlis/errorUtils';
@@ -63,14 +64,15 @@ export const generateOTP =
export const verifyOTP =
({ otp, otpToken }: VerifyOTPPayload) =>
(dispatch: AppDispatch) => {
async(dispatch: AppDispatch) => {
const url = getApiUrl(ApiKeys.VERIFY_OTP);
dispatch(setFormLoading(true));
const fcmToken = await AsyncStorage.getItem('fcmtoken');
axiosInstance
.post(
url,
{ otp, otpToken },
{ otp, otpToken, fcmToken },
{ headers: { donotHandleError: true } },
)
.then((response: AxiosResponse<IUser>) => {

View File

@@ -0,0 +1,90 @@
import axiosInstance, {
ApiKeys,
getApiUrl,
} from '../components/utlis/apiHelper';
import {
INotificationAction,
addActionToNotifications,
appendMoreNotifications,
setNotifications,
setNotificationsLoading,
} from '../reducer/notificationsSlice';
import { INotification } from '../screens/notifications/NotificationItem';
import { AppDispatch } from '../store/store';
interface INotificationPayload {
pageNo: number;
pageSize: number;
}
export const hasNotCompletedAction = (notification: INotification) => {
if (!notification.actions) {
return true;
}
return !Object.keys(notification.actions)?.length;
};
export const getNotifications =
(payload: INotificationPayload = { pageNo: 1, pageSize: 10 }) =>
(dispatch: AppDispatch) => {
dispatch(setNotificationsLoading(true));
let currentDate = new Date();
const yesterday = new Date(currentDate);
yesterday.setDate(yesterday.getDate() - 1);
yesterday.setHours(0, 0, 0, 0);
const startTime = yesterday.getTime();
const url = getApiUrl(
ApiKeys.NOTIFICATIONS,
{},
{ ...payload, startTime },
);
return axiosInstance
.get(url)
.then(response => {
const notifications =
response?.data?.notificationResponse?.notifications;
const { totalElements, totalPages, totalUnreadElements } =
response?.data;
if (notifications) {
const { pageNo } = payload;
if (pageNo === 1) {
dispatch(
setNotifications({
notifications,
totalElements,
totalPages,
pageNo: payload.pageNo,
totalUnreadElements,
}),
);
return;
}
dispatch(
appendMoreNotifications({
notifications,
totalElements,
totalPages,
pageNo: payload.pageNo,
totalUnreadElements,
}),
);
}
})
.finally(() => dispatch(setNotificationsLoading(false)));
};
export const notificationAction =
(payload: INotificationAction[]) => (dispatch: AppDispatch) => {
const url = getApiUrl(ApiKeys.NOTIFICATION_ACTION);
return axiosInstance.post(url, payload).then(() => {
dispatch(addActionToNotifications(payload));
});
};
export const notificationDelivered =
(payload: { ids: string[] }) => (dispatch: AppDispatch) => {
const url = getApiUrl(ApiKeys.NOTIFICATION_DELIVERED);
return axiosInstance.post(url, payload).then(res => {
console.log('res:', res);
});
};

View File

@@ -0,0 +1,57 @@
import * as React from "react";
import Svg, { Path, Rect } from "react-native-svg";
const NoNotificationIcon = () => (
<Svg
width={119}
height={87}
viewBox="0 0 119 87"
fill="none"
>
<Path
d="M68.1074 50.1827L64.8694 56.3035L78.1006 63.3031L81.3387 57.1824L68.1074 50.1827Z"
fill="#E8E8E8"
stroke="#12183E"
strokeWidth={0.552645}
strokeLinecap="round"
strokeLinejoin="round"
/>
<Path
d="M76.8312 55.0932L73.8384 60.7505L77.7999 62.8462L80.7928 57.1889L76.8312 55.0932Z"
fill="#E8E8E8"
/>
<Path
d="M62.9902 52.4499L62.9902 52.4498C70.7555 37.7713 65.154 19.5794 50.4829 11.8142L50.4828 11.8141C35.8041 4.04883 17.6124 9.65796 9.84711 24.329L9.84708 24.329C2.08183 39.0076 7.68336 57.1995 22.362 64.9648L22.3621 64.9648C37.0406 72.7225 55.2249 67.121 62.9902 52.4499ZM4.47926 21.4875C13.8123 3.84431 35.681 -2.88686 53.3243 6.4463C70.9675 15.7794 77.6987 37.6481 68.3655 55.2914C59.0324 72.9346 37.1638 79.6657 19.5204 70.3326C1.87727 60.9995 -4.8539 39.1308 4.47926 21.4875Z"
fill="#545454"
stroke="#12183E"
strokeWidth={0.552645}
/>
<Path
d="M64.6841 51.3527C56.9902 65.8889 38.973 71.439 24.4293 63.7526C9.88557 56.0587 4.3355 38.034 12.0294 23.4903C19.7233 8.95408 37.7479 3.39655 52.2917 11.0904C66.8279 18.7843 72.378 36.809 64.6841 51.3527Z"
fill="white"
fillOpacity={0.5}
/>
<Path
d="M55.3923 5.235C37.6141 -4.16948 15.5782 2.6131 6.17373 20.3912C-3.23076 38.1694 3.55182 60.2053 21.33 69.6098C39.1081 79.0143 61.144 72.2317 70.5485 54.4536C79.953 36.6754 73.1704 14.6395 55.3923 5.235ZM64.6847 51.3536C56.9908 65.8898 38.9736 71.4399 24.4299 63.7535C9.88622 56.0596 4.33615 38.0349 12.0301 23.4912C19.724 8.95497 37.7486 3.39743 52.2923 11.0913C66.8286 18.7852 72.3786 36.8099 64.6847 51.3536Z"
fill="#F7F7F7"
stroke="#12183E"
strokeWidth={0.552645}
strokeLinecap="round"
strokeLinejoin="round"
/>
<Rect
x={73.7046}
y={67.2915}
width={17.4453}
height={41.4829}
rx={7.73703}
transform="rotate(-61.6342 73.7046 67.2915)"
fill="#025ECB"
stroke="#12183E"
strokeWidth={0.552645}
strokeLinecap="round"
strokeLinejoin="round"
/>
</Svg>
);
export default NoNotificationIcon;

View File

@@ -48,6 +48,9 @@ export const ClickstreamAPIToMonitor = {
[API_URLS[ApiKeys.CASE_UNIFIED_DETAILS]]: 'AV_CASE_UNIFIED_DETAILS_API',
[API_URLS[ApiKeys.EMI_SCHEDULES]]: 'AV_EMI_SCHEDULES_API',
[API_URLS[ApiKeys.PAST_FEEDBACK]]: 'AV_PAST_FEEDBACK_API',
[API_URLS[ApiKeys.NOTIFICATIONS]]: 'AV_NOTIFICATIONS_FETCH_API',
[API_URLS[ApiKeys.NOTIFICATION_ACTION]]: 'AV_NOTIFICATIONS_ACTION_API',
[API_URLS[ApiKeys.NOTIFICATION_DELIVERED]]: 'AV_NOTIFICATIONS_DELIVERED_API',
};
export const CLICKSTREAM_EVENT_NAMES = {
@@ -176,7 +179,17 @@ export const CLICKSTREAM_EVENT_NAMES = {
FA_VIEW_PHOTO_CLICKED: {name: 'FA_VIEW_PHOTO_CLICKED', description: 'FA_VIEW_PHOTO_CLICKED'},
FA_UNIFIED_ENTITY_REQUESTED: {name: 'FA_UNIFIED_ENTITY_REQUESTED', description: 'FA_UNIFIED_ENTITY_REQUESTED'},
FA_UNIFIED_ENTITY_REQUEST_SUCCESS: {name: 'FA_UNIFIED_ENTITY_REQUEST_SUCCESS', description: 'FA_UNIFIED_ENTITY_REQUEST_SUCCESS'},
FA_UNIFIED_ENTITY_REQUEST_FAILED: {name: 'FA_UNIFIED_ENTITY_REQUEST_FAILED', description: 'FA_UNIFIED_ENTITY_REQUEST_FAILED'}
FA_UNIFIED_ENTITY_REQUEST_FAILED: {name: 'FA_UNIFIED_ENTITY_REQUEST_FAILED', description: 'FA_UNIFIED_ENTITY_REQUEST_FAILED'},
// Notifications
FA_NOTIFICATION_ICON_CLICK: {name: 'FA_NOTIFICATION_ICON_CLICK', description: 'FA_NOTIFICATION_ICON_CLICK'},
FA_NOTIFICATION_ITEM_CLICK: {name: 'FA_NOTIFICATION_ITEM_CLICK', description: 'FA_NOTIFICATION_ITEM_CLICK'},
AV_NOTIFICATIONS_FETCH_API_SUCCESS : {name: 'FA_NOTIFICATIONS_FETCH_API_SUCCESS', description: 'FA_NOTIFICATIONS_FETCH_API_SUCCESS'},
AV_NOTIFICATIONS_FETCH_API_FAILED : {name: 'FA_NOTIFICATIONS_FETCH_API_FAILED', description: 'FA_NOTIFICATIONS_FETCH_API_FAILED'},
AV_NOTIFICATION_ACTION_API_SUCCESS : {name: 'FA_NOTIFICATION_ACTION_API_SUCCESS', description: 'FA_NOTIFICATION_ACTION_API_SUCCESS'},
AV_NOTIFICATION_ACTION_API_FAILED : {name: 'FA_NOTIFICATION_ACTION_API_FAILED', description: 'FA_NOTIFICATION_ACTION_API_FAILED'},
AV_NOTIFICATION_DELIVERED_API_SUCCESS : {name: 'FA_NOTIFICATION_DELIVERED_API_SUCCESS', description: 'FA_NOTIFICATION_DELIVERED_API_SUCCESS'},
AV_NOTIFICATION_DELIVERED_API_FAILED : {name: 'FA_NOTIFICATION_DELIVERED_API_FAILED', description: 'FA_NOTIFICATION_DELIVERED_API_FAILED'},
} as const;
export enum MimeType {
@@ -192,8 +205,8 @@ export const getPrefixBase64Image = (contentType: MimeType) => {
export const PrefixJpegBase64Image = getPrefixBase64Image(MimeType["image/jpeg"]);
export const HEADER_HEIGHT_MAX = 112 + 50;
export const HEADER_HEIGHT_MIN = 70 + 50;
export const HEADER_HEIGHT_MAX = 134 + 50;
export const HEADER_HEIGHT_MIN = 80 + 50;
export const HEADER_SCROLL_DISTANCE = (HEADER_HEIGHT_MAX - HEADER_HEIGHT_MIN) * 2;
export const LocalStorageKeys = {

View File

@@ -0,0 +1,59 @@
import { Pressable, StyleSheet, View } from 'react-native';
import React from 'react';
import { pushToScreen } from '../utlis/navigationUtlis';
import { GenericStyles } from '../../../RN-UI-LIB/src/styles';
import BellIcon from '../../../RN-UI-LIB/src/Icons/BellIcon';
import { COLORS } from '../../../RN-UI-LIB/src/styles/colors';
import { useAppSelector } from '../../hooks';
import Text from '../../../RN-UI-LIB/src/components/Text';
import { addClickstreamEvent } from '../../services/clickstreamEventService';
import { CLICKSTREAM_EVENT_NAMES } from '../../common/Constants';
const NotificationMenu = () => {
const { totalUnreadElements } = useAppSelector(
state => state.notifications,
);
const handleNotificationPress = () => {
addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_NOTIFICATION_ICON_CLICK)
pushToScreen('Notifications')
};
return (
<Pressable style={GenericStyles.ph8} onPress={handleNotificationPress}>
<BellIcon />
<View style={[styles.notificationBadge, GenericStyles.alignCenter]}>
{totalUnreadElements ? (
<Text
bold
small
style={[
styles.notificationNumber,
{ width: totalUnreadElements > 9 ? 24 : 16 },
]}>
{totalUnreadElements > 9 ? '9+' : totalUnreadElements}
</Text>
) : null}
</View>
</Pressable>
);
};
const styles = StyleSheet.create({
notificationBadge: {
backgroundColor: COLORS.TEXT.RED,
position: 'absolute',
height: 16,
marginLeft: 18,
marginTop: -4,
borderRadius: 8,
zIndex: 100
},
notificationNumber: {
color: COLORS.TEXT.WHITE,
lineHeight: 16,
paddingHorizontal: 5,
},
});
export default NotificationMenu;

View File

@@ -28,6 +28,9 @@ export enum ApiKeys {
CASE_UNIFIED_DETAILS,
EMI_SCHEDULES,
PAST_FEEDBACK,
NOTIFICATIONS,
NOTIFICATION_ACTION,
NOTIFICATION_DELIVERED,
SEND_LOCATION,
}
@@ -48,6 +51,9 @@ API_URLS[ApiKeys.GET_SIGNED_URL] = '/cases/get-signed-urls';
API_URLS[ApiKeys.CASE_UNIFIED_DETAILS] = '/collection-cases/unified-details/{loanAccountNumber}';
API_URLS[ApiKeys.EMI_SCHEDULES] = '/collection-cases/emi-schedules';
API_URLS[ApiKeys.PAST_FEEDBACK] = '/feedback';
API_URLS[ApiKeys.NOTIFICATIONS] = '/notification/fetch';
API_URLS[ApiKeys.NOTIFICATION_ACTION] = '/notification/action';
API_URLS[ApiKeys.NOTIFICATION_DELIVERED] = '/notification/delivered';
API_URLS[ApiKeys.SEND_LOCATION] = '/geolocations/agents';
export const API_STATUS_CODE = {

63
src/hooks/useFCM.ts Normal file
View File

@@ -0,0 +1,63 @@
import messaging, {
FirebaseMessagingTypes,
} from '@react-native-firebase/messaging';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { useEffect } from 'react';
import auth from '@react-native-firebase/auth';
import { useAppDispatch } from '.';
import { prependNewNotifications } from '../reducer/notificationsSlice';
import { notificationDelivered } from '../action/notificationActions';
import { logError } from '../components/utlis/errorUtils';
// This can be used to get Notification permission.
const requestUserPermission = async () => {
const authStatus = await messaging().requestPermission();
const enabled =
authStatus === messaging.AuthorizationStatus.AUTHORIZED ||
authStatus === messaging.AuthorizationStatus.PROVISIONAL;
if (enabled) {
getFCMToken();
}
};
const getFCMToken = async () => {
let fcmtoken = await AsyncStorage.getItem('fcmtoken');
if (!fcmtoken) {
try {
let fcmtoken = await messaging().getToken();
if (fcmtoken) {
await AsyncStorage.setItem('fcmtoken', fcmtoken);
}
} catch (error: any) {
logError(error, 'unable to generate FCM token')
}
}
};
const notificationListener = (
handleNotificationMessage: (
remoteMessage: FirebaseMessagingTypes.RemoteMessage,
) => Promise<void>,
) => {
messaging().setBackgroundMessageHandler(handleNotificationMessage);
messaging().onMessage(handleNotificationMessage);
};
const useFCM = () => {
const dispatch = useAppDispatch();
const handleNotificationMessage = async (
remoteMessage: FirebaseMessagingTypes.RemoteMessage,
) => {
const { data } = remoteMessage;
if (data?.payload) {
const notification = JSON.parse(data.payload);
dispatch(notificationDelivered({ids: [notification.id]}))
dispatch(prependNewNotifications({ notification }));
}
};
useEffect(() => {
getFCMToken();
notificationListener(handleNotificationMessage);
}, []);
};
export default useFCM;

View File

@@ -0,0 +1,124 @@
import { createSlice } from '@reduxjs/toolkit';
import { INotification } from '../screens/notifications/NotificationItem';
import { WidgetStatus } from '../screens/notifications/constants';
export interface INotificationAction {
id: string;
action: {
widgetStatus: WidgetStatus;
timestamp: number;
actionBy: string;
};
}
interface INotificationsState {
data: INotification[];
pageNo: number;
pageSize: number;
totalElements: number;
totalPages: number;
newNotificationCount: number;
totalUnreadElements: number;
notificationsWithActions: INotificationAction[];
isLoading: boolean;
}
const initialState: INotificationsState = {
data: [],
pageNo: 1,
pageSize: 10,
totalElements: 0,
totalPages: 0,
newNotificationCount: 0,
totalUnreadElements: 0,
notificationsWithActions: [],
isLoading: false,
};
const NotificationsSlice = createSlice({
name: 'notificationsSlice',
initialState,
reducers: {
setNotifications: (state, action) => {
const {
notifications,
totalElements,
totalPages,
pageNo,
totalUnreadElements,
} = action.payload;
state.data = notifications;
state.totalElements = totalElements;
state.totalPages = totalPages;
state.newNotificationCount = 0;
state.totalUnreadElements = totalUnreadElements;
state.pageNo = pageNo;
state.isLoading = false;
},
prependNewNotifications: (state, action) => {
const { notification } = action.payload;
const { id } = notification;
const isNotificationAlreadyPresent =
state.data.findIndex(notification => notification.id === id) !==
-1;
state.isLoading = false;
if (isNotificationAlreadyPresent) {
return;
}
state.data = [notification, ...state.data];
state.newNotificationCount++;
state.totalUnreadElements++;
},
appendMoreNotifications: (state, action) => {
const {
notifications,
totalElements,
pageNo,
totalPages,
totalUnreadElements,
} = action.payload;
state.data = [...state.data, ...notifications];
state.totalElements = totalElements;
state.totalPages = totalPages;
state.pageNo = pageNo;
state.totalUnreadElements = totalUnreadElements;
state.isLoading = false;
},
addNotificationToQueue: (state, action) => {
state.notificationsWithActions = [
...state.notificationsWithActions,
action.payload,
];
},
addActionToNotifications: (state, action) => {
const actionPayload = action.payload;
actionPayload.forEach((action: INotificationAction) => {
const { id } = action;
const index = state.data.findIndex(
notification => notification.id === id,
);
if (index !== -1) {
state.data[index].actions = [
{
widgetStatus: WidgetStatus.CLICKED,
},
];
}
});
state.notificationsWithActions = [];
},
setNotificationsLoading: (state, action) => {
state.isLoading = action.payload;
},
},
});
export const {
setNotifications,
prependNewNotifications,
appendMoreNotifications,
setNotificationsLoading,
addNotificationToQueue,
addActionToNotifications,
} = NotificationsSlice.actions;
export default NotificationsSlice.reducer;

View File

@@ -4,6 +4,7 @@ import Heading from '../../../RN-UI-LIB/src/components/Heading';
import { COLORS } from '../../../RN-UI-LIB/src/styles/colors';
import { GenericStyles } from '../../../RN-UI-LIB/src/styles';
import Filters from './Filters';
import NotificationMenu from '../../components/notificationMenu';
interface ICaseListHeader {
filterCount: number;
@@ -32,12 +33,17 @@ const CaseListHeader: React.FC<ICaseListHeader> = ({
styles.filterContainer,
{ height: showFilters ? headerHeight : 'auto' },
]}>
<View>
<Heading
type="h3"
style={[styles.headerLabel, GenericStyles.p16]}>
<View
style={[
GenericStyles.row,
GenericStyles.spaceBetween,
GenericStyles.ph16,
GenericStyles.pv24,
]}>
<Heading type="h3" style={[styles.headerLabel]}>
{headerLabel}
</Heading>
<NotificationMenu />
</View>
{showFilters && (
<Filters

View File

@@ -20,7 +20,7 @@ interface IFilters {
const Filters: React.FC<IFilters> = ({searchQuery, filterCount, handleSearchChange, toggleFilterModal , isVisitPlan}) => {
return (
<View>
<View style={[GenericStyles.ph16, GenericStyles.centerAlignedRow , styles.searchContainer]}>
<View style={[GenericStyles.ph16, GenericStyles.pb16, GenericStyles.centerAlignedRow]}>
<TextInput
style={styles.textInput}
LeftComponent={<SearchIcon />}
@@ -100,9 +100,6 @@ const styles = StyleSheet.create({
chips: {
paddingHorizontal: 18,
backgroundColor: COLORS.BACKGROUND.GREY_LIGHT,
},
searchContainer: {
paddingBottom: 10
}
});

View File

@@ -2,13 +2,18 @@ import React from 'react';
import NavigationHeader from '../../../RN-UI-LIB/src/components/NavigationHeader';
import {goBack} from '../../components/utlis/navigationUtlis';
import {CaseDetail} from './interface';
import { View } from 'react-native';
import NotificationMenu from '../../components/notificationMenu';
const CaseDetailHeader: React.FC<{ caseDetail: CaseDetail }> = props => {
return (
<NavigationHeader
title={''}
onBack={goBack}
/>
<View style={{position: 'relative'}}>
<NavigationHeader
title={''}
onBack={goBack}
rightActionable={<NotificationMenu />}
/>
</View>
);
};

View File

@@ -0,0 +1,27 @@
import { Pressable, StyleSheet, View } from 'react-native';
import { GenericStyles } from '../../../RN-UI-LIB/src/styles';
import DoubleCheckIcon from '../../../RN-UI-LIB/src/Icons/DoubleCheckIcon';
import Text from '../../../RN-UI-LIB/src/components/Text';
import { COLORS } from '../../../RN-UI-LIB/src/styles/colors';
const MarkAllRead = () => {
const handleMarkAllRead = () => {};
return (
<Pressable onPress={handleMarkAllRead}>
<View style={GenericStyles.centerAlignedRow}>
<DoubleCheckIcon />
<Text style={[GenericStyles.ml4, styles.label]}>
Mark all read
</Text>
</View>
</Pressable>
);
};
const styles = StyleSheet.create({
label: {
color: COLORS.TEXT.WHITE,
},
});
export default MarkAllRead;

View File

@@ -0,0 +1,182 @@
import { Pressable, StyleSheet, View } from 'react-native';
import React from 'react';
import Text from '../../../RN-UI-LIB/src/components/Text';
import { GenericStyles } from '../../../RN-UI-LIB/src/styles';
import { COLORS } from '../../../RN-UI-LIB/src/styles/colors';
import {
NotificationIconsMap,
NotificationTypes,
WidgetStatus,
} from './constants';
import Heading from '../../../RN-UI-LIB/src/components/Heading';
import { getTimeDifference } from '../../../RN-UI-LIB/src/utlis/common';
import { useAppDispatch, useAppSelector } from '../../hooks';
import { CaseDetail } from '../caseDetails/interface';
import NotificationTemplate from './NotificationTemplate';
import { CaseAllocationType } from '../allCases/interface';
import { pushToScreen } from '../../components/utlis/navigationUtlis';
import {
hasNotCompletedAction,
notificationAction,
} from '../../action/notificationActions';
import useIsOnline from '../../hooks/useIsOnline';
import { addNotificationToQueue } from '../../reducer/notificationsSlice';
import { addClickstreamEvent } from '../../services/clickstreamEventService';
import { CLICKSTREAM_EVENT_NAMES } from '../../common/Constants';
export interface INotification {
id: string;
clickable: boolean;
collectionCaseId: string;
displayed: boolean;
params: {
amount: number;
customerName: string;
paymentMode: string;
paymentTimestamp: string;
revisitDate: string;
revisitTime: string;
variation: string;
promisedDate: string;
promisedAmount: number;
};
template: {
id: number;
templateName: keyof typeof NotificationTypes;
};
actions: Record<string, string>[];
createdAt: number;
updatedAt: number;
scheduledAt: number;
eventTimestamp: number;
headerLabel: string;
}
interface INotificationProps {
data: INotification;
label?: string;
}
const NotificationItem: React.FC<INotificationProps> = ({ data }) => {
const {
id,
clickable,
template,
scheduledAt,
collectionCaseId,
params,
headerLabel,
} = data;
if (headerLabel) {
return (
<Text
dark
style={[GenericStyles.ph24, GenericStyles.pv12, styles.darkBg]}
bold>
{headerLabel}
</Text>
);
}
const templateName = template?.templateName || '';
const caseDetailsMap: Record<string, CaseDetail> = useAppSelector(
state => state.allCases.caseDetails,
);
const { notificationsWithActions } = useAppSelector(
state => state.notifications,
);
const { phoneNumber } = useAppSelector(state => state.user.user!!);
const dispatch = useAppDispatch();
const isOnline = useIsOnline();
const customerName = params?.customerName || '';
const handleNotificationAction = () => {
const payload = {
id: id,
action: {
widgetStatus: WidgetStatus.CLICKED,
timestamp: Date.now(),
actionBy: phoneNumber,
},
};
if (isOnline) {
dispatch(
notificationAction([...notificationsWithActions, payload]),
);
} else {
dispatch(addNotificationToQueue(payload));
}
};
const handleNotificationPress = () => {
addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_NOTIFICATION_ITEM_CLICK, {collectionCaseId, notificationId: id, templateName})
const isActionNotCompleted = hasNotCompletedAction(data);
if (isActionNotCompleted) {
handleNotificationAction();
}
if (!collectionCaseId || !clickable) {
return;
}
const caseDetails = caseDetailsMap?.[collectionCaseId];
if (!caseDetails) {
return;
}
const { caseType } = caseDetails;
if (caseType === CaseAllocationType.COLLECTION_CASE) {
pushToScreen('collectionCaseDetail', {
caseId: collectionCaseId,
});
} else {
pushToScreen('caseDetail', { caseId: collectionCaseId });
}
};
const notificationIcon = NotificationIconsMap[templateName];
const notificationRead = !hasNotCompletedAction(data);
return (
<Pressable
style={[
GenericStyles.p16,
styles.container,
GenericStyles.row,
{
backgroundColor: !notificationRead
? COLORS.BACKGROUND.PRIMARY
: COLORS.BACKGROUND.GREY_LIGHT_2,
},
]}
onPress={handleNotificationPress}>
{notificationIcon}
<View style={[GenericStyles.pl16, GenericStyles.flex80]}>
<Heading type="h5" dark>
{customerName}
</Heading>
<NotificationTemplate data={data} />
<Text small>{getTimeDifference(scheduledAt)}</Text>
</View>
{!notificationRead ? <View style={styles.dot}></View> : null}
</Pressable>
);
};
const styles = StyleSheet.create({
container: {
borderBottomWidth: 1,
borderColor: COLORS.BORDER.GREY,
},
dot: {
width: 8,
height: 8,
borderRadius: 4,
backgroundColor: COLORS.BACKGROUND.DARK_BLUE,
marginLeft: 'auto',
},
darkBg: {
backgroundColor: COLORS.BACKGROUND.BLUE_LIGHT_2,
},
});
export default NotificationItem;

View File

@@ -0,0 +1,121 @@
import { StyleSheet } from 'react-native';
import React from 'react';
import Text from '../../../RN-UI-LIB/src/components/Text';
import { INotification } from './NotificationItem';
import { NotificationTypes } from './constants';
import { formatAmount } from '../../../RN-UI-LIB/src/utlis/amount';
interface INotificationTemplateProps {
data: INotification;
}
const NotificationTemplate: React.FC<INotificationTemplateProps> = ({
data,
}) => {
const { template, params } = data;
const templateName = template?.templateName || '';
const {
amount,
paymentTimestamp,
customerName,
revisitDate,
revisitTime,
variation,
promisedDate,
promisedAmount,
} = params || {};
switch (templateName) {
case NotificationTypes.NEW_ADDRESS_ADDED_NOTIFICATION_TEMPLATE:
return (
<Text>
New Address<Text light> added for {customerName}</Text>
</Text>
);
case NotificationTypes.PAYMENT_FAILED_TEMPLATE:
return (
<Text>
<Text light>Faced an error while making a payment of</Text>{' '}
{formatAmount(amount)} <Text light>on</Text>{' '}
{paymentTimestamp}
</Text>
);
case NotificationTypes.PAYMENT_MADE_TEMPLATE:
return (
<Text>
<Text light>Made a payment of</Text> {formatAmount(amount)}{' '}
<Text light>on </Text>
{paymentTimestamp}
</Text>
);
case NotificationTypes.PROMISED_TO_PAY_SCHEDULED_NOTIFICATION_TEMPLATE:
return (
<Text>
<Text light>Promise to pay</Text>{' '}
{formatAmount(promisedAmount)} <Text light>on</Text>{' '}
{promisedDate}
</Text>
);
case NotificationTypes.LONGHORN_ALERTS_NEW_PHONE_NUMBER_ADDED:
case NotificationTypes.NEW_TELEPHONE_ADDED_NOTIFICATION_TEMPLATE:
return (
<Text>
New Number<Text light> added for {customerName}</Text>
</Text>
);
case NotificationTypes.REVISIT_SCHEDULED_NOTIFICATION_TEMPLATE:
return (
<Text>
Revisit case <Text light>of {customerName} on</Text>{' '}
{revisitDate} <Text light>at</Text> {revisitTime}
</Text>
);
case NotificationTypes.LONGHORN_ALERTS_NEW_ENQUIRY:
return (
<Text>
New loan Enquiry{' '}
<Text light>was made by {customerName}</Text>
</Text>
);
case NotificationTypes.LONGHORN_ALERTS_NEW_TRADE_ACCOUNT:
return (
<Text>
New loan <Text light>is taken by {customerName}</Text>
</Text>
);
case NotificationTypes.LONGHORN_ALERTS_CHANGE_IN_UTILIZATION:
return (
<Text>
{variation} in POS{' '}
<Text light>across loans for {customerName}</Text>
</Text>
);
case NotificationTypes.LONGHORN_ALERTS_CHANGE_IN_DELIQUENCY_ACCOUNTS:
return (
<Text>
{variation} in DPD{' '}
<Text light>across loan accounts for {customerName}</Text>
</Text>
);
case NotificationTypes.LONGHORN_ALERTS_CHANGE_IN_SCORE:
return (
<Text>
{variation} in CIBIL score{' '}
<Text light>for {customerName}</Text>
</Text>
);
case NotificationTypes.LONGHORN_ALERTS_MULTIPLE_TRIGGER_ALERTS:
return (
<Text>
Multiple CIBIL alerts{' '}
<Text light> received for {customerName}</Text>
</Text>
);
default:
return <Text>New notification</Text>;
}
};
const styles = StyleSheet.create({});
export default NotificationTemplate;

View File

@@ -0,0 +1,44 @@
import NewAddressIcon from '../../../RN-UI-LIB/src/Icons/NewAddressIcon';
import NewTelephoneIcon from '../../../RN-UI-LIB/src/Icons/NewTelephoneIcon';
import NotificationIcon from '../../../RN-UI-LIB/src/Icons/NotificationIcon';
import PaymentFailedIcon from '../../../RN-UI-LIB/src/Icons/PaymentFailedIcon';
import PaymentSuccessIcon from '../../../RN-UI-LIB/src/Icons/PaymentSuccessIcon';
import PromiseToPayIcon from '../../../RN-UI-LIB/src/Icons/PromiseToPayIcon';
export const NotificationIconsMap = {
NEW_ADDRESS_ADDED_NOTIFICATION_TEMPLATE: <NewAddressIcon />,
PAYMENT_FAILED_TEMPLATE: <PaymentFailedIcon />,
PAYMENT_MADE_TEMPLATE: <PaymentSuccessIcon />,
PROMISED_TO_PAY_SCHEDULED_NOTIFICATION_TEMPLATE: <PromiseToPayIcon />,
NEW_TELEPHONE_ADDED_NOTIFICATION_TEMPLATE: <NewTelephoneIcon />,
LONGHORN_ALERTS_CHANGE_IN_DELIQUENCY: <NotificationIcon />,
LONGHORN_ALERTS_MULTIPLE_TRIGGER_ALERTS: <NotificationIcon />,
LONGHORN_ALERTS_CHANGE_IN_SCORE: <NotificationIcon />,
LONGHORN_ALERTS_NEW_PHONE_NUMBER_ADDED: <NotificationIcon />,
LONGHORN_ALERTS_CHANGE_IN_DELIQUENCY_ACCOUNTS: <NotificationIcon />,
LONGHORN_ALERTS_CHANGE_IN_UTILIZATION: <NotificationIcon />,
LONGHORN_ALERTS_NEW_TRADE_ACCOUNT: <NotificationIcon />,
LONGHORN_ALERTS_NEW_ENQUIRY: <NotificationIcon />,
REVISIT_SCHEDULED_NOTIFICATION_TEMPLATE: <NotificationIcon />,
};
export enum NotificationTypes {
PAYMENT_MADE_TEMPLATE = 'PAYMENT_MADE_TEMPLATE',
PROMISED_TO_PAY_SCHEDULED_NOTIFICATION_TEMPLATE = 'PROMISED_TO_PAY_SCHEDULED_NOTIFICATION_TEMPLATE',
PAYMENT_FAILED_TEMPLATE = 'PAYMENT_FAILED_TEMPLATE',
NEW_ADDRESS_ADDED_NOTIFICATION_TEMPLATE = 'NEW_ADDRESS_ADDED_NOTIFICATION_TEMPLATE',
NEW_TELEPHONE_ADDED_NOTIFICATION_TEMPLATE = 'NEW_TELEPHONE_ADDED_NOTIFICATION_TEMPLATE',
LONGHORN_ALERTS_CHANGE_IN_DELIQUENCY = 'LONGHORN_ALERTS_CHANGE_IN_DELIQUENCY',
LONGHORN_ALERTS_MULTIPLE_TRIGGER_ALERTS = 'LONGHORN_ALERTS_MULTIPLE_TRIGGER_ALERTS',
LONGHORN_ALERTS_CHANGE_IN_SCORE = 'LONGHORN_ALERTS_CHANGE_IN_SCORE',
LONGHORN_ALERTS_NEW_PHONE_NUMBER_ADDED = 'LONGHORN_ALERTS_NEW_PHONE_NUMBER_ADDED',
LONGHORN_ALERTS_CHANGE_IN_DELIQUENCY_ACCOUNTS = 'LONGHORN_ALERTS_CHANGE_IN_DELIQUENCY_ACCOUNTS',
LONGHORN_ALERTS_CHANGE_IN_UTILIZATION = 'LONGHORN_ALERTS_CHANGE_IN_UTILIZATION',
LONGHORN_ALERTS_NEW_TRADE_ACCOUNT = 'LONGHORN_ALERTS_NEW_TRADE_ACCOUNT',
LONGHORN_ALERTS_NEW_ENQUIRY = 'LONGHORN_ALERTS_NEW_ENQUIRY',
REVISIT_SCHEDULED_NOTIFICATION_TEMPLATE = 'REVISIT_SCHEDULED_NOTIFICATION_TEMPLATE',
}
export enum WidgetStatus {
CLICKED = 'CLICKED',
}

View File

@@ -0,0 +1,182 @@
import {
ActivityIndicator,
ListRenderItemInfo,
RefreshControl,
StyleSheet,
View,
VirtualizedList,
} from 'react-native';
import React, { useEffect, useMemo } from 'react';
import NavigationHeader, {
Icon,
} from '../../../RN-UI-LIB/src/components/NavigationHeader';
import { goBack } from '../../components/utlis/navigationUtlis';
import NotificationItem, { INotification } from './NotificationItem';
import { useAppDispatch, useAppSelector } from '../../hooks';
import { GenericStyles } from '../../../RN-UI-LIB/src/styles';
import { getNotifications } from '../../action/notificationActions';
import { COLORS } from '../../../RN-UI-LIB/src/styles/colors';
import useRefresh from '../../hooks/useRefresh';
import Text from '../../../RN-UI-LIB/src/components/Text';
import NoNotificationIcon from '../../assets/icons/NoNotificationIcon';
export const getItem = (item: Array<any>, index: number) => item[index];
const Notifications = () => {
const {
data = [],
pageNo,
totalElements,
pageSize,
newNotificationCount,
isLoading,
} = useAppSelector(state => state.notifications);
const dispatch = useAppDispatch();
const handlePullToRefresh = () => {
dispatch(getNotifications());
};
const totalNotifications = totalElements + newNotificationCount;
const { refreshing, onRefresh } = useRefresh(handlePullToRefresh);
const handleBack = () => {
goBack();
};
const handleLoadMoreNotifications = () => {
if (isLoading || data.length >= totalNotifications) {
return;
}
dispatch(getNotifications({ pageNo: pageNo + 1, pageSize }));
};
useEffect(() => {
if (data?.length) {
return;
}
dispatch(getNotifications());
}, []);
const notificationsList: INotification[] = useMemo(() => {
if (!data?.length) {
return [];
}
let notifications: INotification[] = [];
const currentDate = new Date();
currentDate.setHours(0, 0, 0, 0);
const todayStartTime = currentDate.getTime();
const yesterday = new Date(currentDate);
yesterday.setDate(yesterday.getDate() - 1);
yesterday.setHours(0, 0, 0, 0);
const yesterdayStartTime = yesterday.getTime();
const todaysNotification: INotification[] = [];
const yesterdaysNotification: INotification[] = [];
const earlierNotifications: INotification[] = [];
data.forEach(notification => {
const { scheduledAt } = notification;
if (scheduledAt > todayStartTime) {
todaysNotification.push(notification);
} else if (
scheduledAt < todayStartTime &&
scheduledAt > yesterdayStartTime
) {
yesterdaysNotification.push(notification);
} else {
earlierNotifications.push(notification);
}
});
notifications = [...todaysNotification];
if (yesterdaysNotification.length) {
notifications = [
...notifications,
{ headerLabel: 'Yesterday', id: -1 },
...yesterdaysNotification,
];
}
if (earlierNotifications.length) {
notifications = [
...notifications,
{ headerLabel: 'Earlier', id: -2 },
...earlierNotifications,
];
}
return notifications;
}, [data]);
const handleMarkAllRead = () => {};
const renderListItem = (row: ListRenderItemInfo<INotification>) => (
<NotificationItem data={row.item} />
);
const memoizedListItem = useMemo(() => renderListItem, [notificationsList]);
return (
<View style={GenericStyles.fill}>
<NavigationHeader
onBack={handleBack}
title="Notifications"
icon={Icon.close}
// TODO: Mark all read will be picked when API is ready
//rightActionable={<MarkAllRead />}
/>
<VirtualizedList
data={notificationsList}
getItem={getItem}
contentContainerStyle={[
data?.length ? null : GenericStyles.fill,
]}
renderItem={memoizedListItem}
keyExtractor={item => item.id}
onEndReached={handleLoadMoreNotifications}
onEndReachedThreshold={0}
getItemCount={item => item.length}
refreshControl={
<RefreshControl
refreshing={refreshing}
onRefresh={onRefresh}
/>
}
ListFooterComponent={
data?.length && data.length < totalNotifications ? (
<View
style={[
GenericStyles.centerAlignedRow,
GenericStyles.whiteBackground,
styles.loadingContainer,
]}>
{isLoading ? (
<ActivityIndicator color={COLORS.BASE.BLUE} />
) : null}
</View>
) : null
}
ListEmptyComponent={
<View style={styles.centerAbsolute}>
<NoNotificationIcon />
<Text style={GenericStyles.mt16} light>
You dont have any pending notifications
</Text>
</View>
}
/>
</View>
);
};
const styles = StyleSheet.create({
loadingContainer: {
height: 60,
},
centerAbsolute: {
height: '100%',
justifyContent: 'center',
alignItems: 'center',
paddingHorizontal: 4,
},
});
export default Notifications;

View File

@@ -24,6 +24,7 @@ import addressSlice from '../reducer/addressSlice';
import emiScheduleSlice from '../reducer/emiScheduleSlice';
import repaymentsSlice from '../reducer/repaymentsSlice';
import feedbackHistorySlice from '../reducer/feedbackHistorySlice';
import notificationsSlice from '../reducer/notificationsSlice';
import MetadataSlice from "../reducer/metadataSlice";
const rootReducer = combineReducers({
@@ -39,6 +40,7 @@ const rootReducer = combineReducers({
repayments: repaymentsSlice,
feedbackHistory: feedbackHistorySlice,
address: addressSlice,
notifications: notificationsSlice,
metadata : MetadataSlice,
});

View File

@@ -1572,6 +1572,11 @@
resolved "https://registry.yarnpkg.com/@react-native-firebase/firestore/-/firestore-16.5.0.tgz#84775327eed3f73fa7e3c206f2a6b5ef492fe698"
integrity sha512-LBoe2pCKCFVFFKofOEu4YERT3CBIGgASK+COjt60p10yIXfxHVv89tW6d1W39iganPajSYYTLqIj4X//CYO+jA==
"@react-native-firebase/messaging@17.4.0":
version "17.4.0"
resolved "https://registry.yarnpkg.com/@react-native-firebase/messaging/-/messaging-17.4.0.tgz#9e1df987183d0ca367d0922a14b14b7a53a140cf"
integrity sha512-RSiBBfyJ3K9G6TQfZc09XaGpxB9xlP5m9DYkqjbNIqnnTiahF90770lTAS65L1Ha78vCwVO2swIlk32XbcMcMQ==
"@react-native/assets@1.0.0":
version "1.0.0"
resolved "https://registry.yarnpkg.com/@react-native/assets/-/assets-1.0.0.tgz#c6f9bf63d274bafc8e970628de24986b30a55c8e"