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:
committed by
GitHub Enterprise
parent
12d3a9e8b4
commit
6367b1dd01
@@ -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}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
|
||||
Submodule RN-UI-LIB updated: 70084651bb...106cc696c1
@@ -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",
|
||||
|
||||
@@ -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>) => {
|
||||
|
||||
90
src/action/notificationActions.ts
Normal file
90
src/action/notificationActions.ts
Normal 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);
|
||||
});
|
||||
};
|
||||
57
src/assets/icons/NoNotificationIcon.tsx
Normal file
57
src/assets/icons/NoNotificationIcon.tsx
Normal 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;
|
||||
@@ -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 = {
|
||||
|
||||
59
src/components/notificationMenu/index.tsx
Normal file
59
src/components/notificationMenu/index.tsx
Normal 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;
|
||||
@@ -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
63
src/hooks/useFCM.ts
Normal 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;
|
||||
124
src/reducer/notificationsSlice.ts
Normal file
124
src/reducer/notificationsSlice.ts
Normal 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;
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
27
src/screens/notifications/MarkAllRead.tsx
Normal file
27
src/screens/notifications/MarkAllRead.tsx
Normal 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;
|
||||
182
src/screens/notifications/NotificationItem.tsx
Normal file
182
src/screens/notifications/NotificationItem.tsx
Normal 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;
|
||||
121
src/screens/notifications/NotificationTemplate.tsx
Normal file
121
src/screens/notifications/NotificationTemplate.tsx
Normal 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;
|
||||
44
src/screens/notifications/constants.tsx
Normal file
44
src/screens/notifications/constants.tsx
Normal 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',
|
||||
}
|
||||
182
src/screens/notifications/index.tsx
Normal file
182
src/screens/notifications/index.tsx
Normal 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 don’t 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;
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user