Merge pull request #528 from medici/feat/TP-31863

TP-31863 | ID Card maker flow
This commit is contained in:
Aman Chaturvedi
2023-07-02 21:40:44 +05:30
committed by GitHub Enterprise
17 changed files with 590 additions and 30 deletions

View File

@@ -26,6 +26,7 @@ import { loggedOutCurrentUser } from '../hooks/useFirestoreUpdates';
import { GenericFunctionArgs, GenericType } from '../common/GenericTypes';
import { GoogleSignin } from '@react-native-google-signin/google-signin';
import { resetConfig } from '../reducer/configSlice';
import { resetProfileData } from '../reducer/profileSlice';
export interface GenerateOTPPayload {
phoneNumber: string;
@@ -198,6 +199,7 @@ export const handleLogout = () => async (dispatch: AppDispatch) => {
);
dispatch(resetCasesData());
dispatch(resetConfig());
dispatch(resetProfileData());
} catch (err) {
logError(err as Error, 'Logout clear session details error');
}

View File

@@ -0,0 +1,69 @@
import { toast } from '../../RN-UI-LIB/src/components/toast';
import axiosInstance, { ApiKeys, getApiUrl } from '../components/utlis/apiHelper';
import { logError } from '../components/utlis/errorUtils';
import {
ImageApprovalStatus,
setApprovalStatus,
setIsUploadingImage,
setOriginalImageDetails,
} from '../reducer/profileSlice';
import { ToastMessages } from '../screens/allCases/constants';
import { AppDispatch } from '../store/store';
export const uploadImageId = (agentId: string, uri: string) => (dispatch: AppDispatch) => {
const url = getApiUrl(ApiKeys.UPLOAD_IMAGE_ID);
const formData = new FormData();
dispatch(setIsUploadingImage(true));
formData.append('file', {
uri: uri,
type: 'image/jpeg',
name: `${agentId}_selfie.jpg`,
} as any);
axiosInstance
.post(url, formData, {
headers: {
'Content-Type': 'multipart/form-data',
},
})
.then(() => {
toast({ type: 'info', text1: ToastMessages.IMAGE_UPLOAD_SUCCESS });
dispatch(setApprovalStatus(ImageApprovalStatus.PENDING));
})
.finally(() => {
dispatch(setIsUploadingImage(false));
});
};
export const getSelfieDocument = () => (dispatch: AppDispatch) => {
const url = getApiUrl(ApiKeys.GET_DOCUMENTS, {}, { type: 'SELFIE' });
axiosInstance
.get(url)
.then((res) => {
if (res.data?.documents?.[0]) {
const { documents, agencyName, agencyCode } = res.data;
const { originalDocumentUri, optimizedDocumentUri, updatedAt, approvalStatus } =
documents?.[0] || {};
dispatch(
setOriginalImageDetails({
originalImageUri: originalDocumentUri || '',
validationDate: updatedAt || '',
approvalStatus: approvalStatus || ImageApprovalStatus.REJECTED,
agencyName: agencyName || '',
agencyCode: agencyCode || '',
optimizedImageUri: optimizedDocumentUri || '',
})
);
} else {
dispatch(
setOriginalImageDetails({
originalImageUri: '',
validationDate: '',
approvalStatus: ImageApprovalStatus.REJECTED,
})
);
}
})
.catch((err) => {
logError(err as Error, 'Error while fetching selfie document');
});
};

View File

@@ -0,0 +1,23 @@
import * as React from 'react';
import Svg, { Path } from 'react-native-svg';
import { IconProps } from '../../../RN-UI-LIB/src/Icons/types';
import { COLORS } from '../../../RN-UI-LIB/src/styles/colors';
const IDCardIcon: React.FC<IconProps> = ({ size = 20, fillColor = COLORS.TEXT.TEAL }) => (
<Svg width={size} height={size} viewBox={`0 0 ${size} ${size}`} fill="none">
<Path
d="M10 7.91797C10.4945 7.91797 10.9778 8.06459 11.3889 8.33929C11.8 8.614 12.1205 9.00444 12.3097 9.46126C12.4989 9.91808 12.5484 10.4207 12.452 10.9057C12.3555 11.3906 12.1174 11.8361 11.7678 12.1857C11.4181 12.5354 10.9727 12.7735 10.4877 12.8699C10.0028 12.9664 9.50011 12.9169 9.04329 12.7277C8.58648 12.5384 8.19603 12.218 7.92133 11.8069C7.64662 11.3958 7.5 10.9124 7.5 10.418C7.5 9.75493 7.76339 9.11904 8.23223 8.6502C8.70107 8.18136 9.33696 7.91797 10 7.91797ZM14.1083 16.4763C14.0732 16.5354 14.0232 16.5843 13.9632 16.6179C13.9032 16.6516 13.8354 16.6688 13.7667 16.668H6.26667C6.1979 16.6688 6.13013 16.6516 6.07015 16.6179C6.01017 16.5843 5.96011 16.5354 5.925 16.4763C5.89052 16.4183 5.87232 16.3521 5.87232 16.2846C5.87232 16.2172 5.89052 16.151 5.925 16.093C6.34978 15.3799 6.95252 14.7893 7.67417 14.3792C8.39582 13.9691 9.21163 13.7535 10.0417 13.7535C10.8717 13.7535 11.6875 13.9691 12.4092 14.3792C13.1308 14.7893 13.7336 15.3799 14.1583 16.093C14.1856 16.1554 14.1953 16.224 14.1865 16.2916C14.1777 16.3591 14.1507 16.423 14.1083 16.4763Z"
fill={fillColor}
/>
<Path
d="M16.25 2.5H14.1667C14.0562 2.5 13.9502 2.5439 13.872 2.62204C13.7939 2.70018 13.75 2.80616 13.75 2.91667V3.75C13.75 3.86051 13.7939 3.96649 13.872 4.04463C13.9502 4.12277 14.0562 4.16667 14.1667 4.16667H15.8333C15.9438 4.16667 16.0498 4.21057 16.128 4.28871C16.2061 4.36685 16.25 4.47283 16.25 4.58333V17.9167C16.2479 18.0265 16.2033 18.1313 16.1256 18.2089C16.0479 18.2866 15.9432 18.3312 15.8333 18.3333H4.16667C4.05616 18.3333 3.95018 18.2894 3.87204 18.2113C3.7939 18.1332 3.75 18.0272 3.75 17.9167V4.58333C3.75 4.47283 3.7939 4.36685 3.87204 4.28871C3.95018 4.21057 4.05616 4.16667 4.16667 4.16667H5.83333C5.94384 4.16667 6.04982 4.12277 6.12796 4.04463C6.2061 3.96649 6.25 3.86051 6.25 3.75V2.91667C6.25 2.80616 6.2061 2.70018 6.12796 2.62204C6.04982 2.5439 5.94384 2.5 5.83333 2.5H3.75C3.30797 2.5 2.88405 2.67559 2.57149 2.98816C2.25893 3.30072 2.08333 3.72464 2.08333 4.16667V18.3333C2.08333 18.7754 2.25893 19.1993 2.57149 19.5118C2.88405 19.8244 3.30797 20 3.75 20H16.25C16.692 20 17.116 19.8244 17.4285 19.5118C17.7411 19.1993 17.9167 18.7754 17.9167 18.3333V4.16667C17.9167 3.72464 17.7411 3.30072 17.4285 2.98816C17.116 2.67559 16.692 2.5 16.25 2.5Z"
fill={fillColor}
/>
<Path
d="M7.5 5C7.5 5.33152 7.6317 5.64946 7.86612 5.88388C8.10054 6.1183 8.41848 6.25 8.75 6.25H11.25C11.5815 6.25 11.8995 6.1183 12.1339 5.88388C12.3683 5.64946 12.5 5.33152 12.5 5V2.5C12.5 1.83696 12.2366 1.20107 11.7678 0.732233C11.2989 0.263392 10.663 0 10 0C9.33696 0 8.70107 0.263392 8.23223 0.732233C7.76339 1.20107 7.5 1.83696 7.5 2.5V5ZM9.16667 2.08333C9.16667 1.91852 9.21554 1.7574 9.30711 1.62036C9.39868 1.48332 9.52883 1.37651 9.6811 1.31343C9.83337 1.25036 10.0009 1.23386 10.1626 1.26601C10.3242 1.29817 10.4727 1.37753 10.5893 1.49408C10.7058 1.61062 10.7852 1.75911 10.8173 1.92076C10.8495 2.08241 10.833 2.24996 10.7699 2.40224C10.7068 2.55451 10.6 2.68466 10.463 2.77622C10.3259 2.86779 10.1648 2.91667 10 2.91667C9.77899 2.91667 9.56702 2.82887 9.41074 2.67259C9.25446 2.51631 9.16667 2.30435 9.16667 2.08333Z"
fill={fillColor}
/>
</Svg>
);
export default IDCardIcon;

View File

@@ -256,6 +256,22 @@ export const CLICKSTREAM_EVENT_NAMES = {
name: 'FA_PROFILE_PAGE_LOGOUT_API_FAILED',
description: 'Logout API failed',
},
FA_ID_CARD_CLICKED: {
name: 'FA_ID_CARD_CLICKED',
description: 'FA_ID_CARD_CLICKED',
},
FA_ID_CARD_LANDED: {
name: 'FA_ID_CARD_LANDED',
description: 'FA_ID_CARD_LANDED',
},
FA_CREATE_ID_CLICKED: {
name: 'FA_CREATE_ID_CLICKED',
description: 'FA_CREATE_ID_CLICKED',
},
FA_CREATE_ID_PROCEED_CLICKED: {
name: 'FA_CREATE_ID_PROCEED_CLICKED',
description: 'FA_CREATE_ID_PROCEED_CLICKED',
},
//FORMS
AV_FORM_LOADED: { name: 'FA_FORM_LOADED', description: 'Form loaded' },
@@ -601,3 +617,5 @@ export const REQUEST_TO_UNBLOCK_FOR_IMPERSONATION = [
getApiUrl(ApiKeys.GET_SIGNED_URL),
getApiUrl(ApiKeys.LOGOUT),
];
export const NAVI_AGENCY_CODE = '1000';

View File

@@ -52,6 +52,8 @@ export enum ApiKeys {
UPLOAD_FEEDBACK_IMAGES = 'UPLOAD_FEEDBACK_IMAGES',
ORIGINAL_IMAGES = 'ORIGINAL_IMAGES',
GLOBAL_CONFIG = 'GLOBAL_CONFIG',
UPLOAD_IMAGE_ID = 'UPLOAD_IMAGE_ID',
GET_DOCUMENTS = 'GET_DOCUMENTS',
}
export const API_URLS: Record<ApiKeys, string> = {} as Record<ApiKeys, string>;
@@ -89,6 +91,8 @@ API_URLS[ApiKeys.FETCH_CASES] = '/cases/agents/{agentReferenceId}';
API_URLS[ApiKeys.GET_FORECLOSURE_AMOUNT] = '/{loanAccountNumber}/pre-closure-amount';
API_URLS[ApiKeys.UPLOAD_FEEDBACK_IMAGES] = '/feedback/persist-original-images';
API_URLS[ApiKeys.GLOBAL_CONFIG] = '/global-config';
API_URLS[ApiKeys.UPLOAD_IMAGE_ID] = '/user/documents/selfie';
API_URLS[ApiKeys.GET_DOCUMENTS] = '/user/documents';
export const API_STATUS_CODE = {
OK: 200,

View File

@@ -0,0 +1,71 @@
import { createSlice } from '@reduxjs/toolkit';
export enum ImageApprovalStatus {
NOT_INITIATED = 'NOT_INITIATED',
APPROVED = 'APPROVED',
PENDING = 'PENDING',
REJECTED = 'REJECTED',
}
const initialState = {
isUploadingImage: false,
imageUri: '',
originalImageUri: '',
validationDate: '',
showReviewImageModal: false,
approvalStatus: ImageApprovalStatus.NOT_INITIATED,
agencyName: '',
agencyCode: '',
optimizedImageUri: '',
};
export const profileSlice = createSlice({
name: 'profile',
initialState,
reducers: {
setImageUri: (state, action) => {
state.imageUri = action.payload;
},
setOriginalImageDetails: (state, action) => {
const {
originalImageUri,
optimizedImageUri,
validationDate,
approvalStatus,
agencyName,
agencyCode,
} = action.payload;
state.originalImageUri = originalImageUri;
state.validationDate = validationDate;
state.approvalStatus = approvalStatus;
state.agencyName = agencyName;
state.agencyCode = agencyCode;
state.optimizedImageUri = optimizedImageUri;
},
setIsUploadingImage: (state, action) => {
state.isUploadingImage = action.payload;
if (!action.payload) {
state.showReviewImageModal = false;
state.imageUri = '';
}
},
setShowReviewImageModal: (state, action) => {
state.showReviewImageModal = action.payload;
},
setApprovalStatus: (state, action) => {
state.approvalStatus = action.payload;
},
resetProfileData: () => initialState,
},
});
export const {
setImageUri,
setIsUploadingImage,
setShowReviewImageModal,
setOriginalImageDetails,
setApprovalStatus,
resetProfileData,
} = profileSlice.actions;
export default profileSlice.reducer;

View File

@@ -0,0 +1,74 @@
import { StyleSheet, View } from 'react-native';
import React from 'react';
import NaviLogoWithTextIcon from '../../../RN-UI-LIB/src/Icons/NaviLogoWithTextIcon';
import Avatar from '../../../RN-UI-LIB/src/components/Avatar';
import { GenericStyles, SCREEN_WIDTH } from '../../../RN-UI-LIB/src/styles';
import Text from '../../../RN-UI-LIB/src/components/Text';
import Heading from '../../../RN-UI-LIB/src/components/Heading';
import { COLORS } from '../../../RN-UI-LIB/src/styles/colors';
import { useAppSelector } from '../../hooks';
import { BUSINESS_DATE_FORMAT, dateFormat } from '../../../RN-UI-LIB/src/utlis/dates';
import { NAVI_AGENCY_CODE } from '../../common/Constants';
const AgentIdCard = () => {
const {
originalImageUri,
optimizedImageUri,
agentName,
agentPhone,
validationDate,
agencyName,
agencyCode,
} = useAppSelector((state) => ({
originalImageUri: state.profile.originalImageUri,
agentName: state.user.user?.name!!,
agentPhone: state.user.user?.phoneNumber,
validationDate: state.profile.validationDate,
agencyName: state.profile.agencyName,
agencyCode: state.profile.agencyCode,
optimizedImageUri: state.profile.optimizedImageUri,
}));
return (
<View style={[GenericStyles.p24, GenericStyles.alignCenter, { width: SCREEN_WIDTH - 32 }]}>
<NaviLogoWithTextIcon />
{agencyName && agencyCode !== NAVI_AGENCY_CODE ? (
<>
<Text small bold style={styles.greyColor}>
in association with
</Text>
<Text dark bold>
{agencyName}
</Text>
</>
) : null}
<View style={GenericStyles.mv16}>
<Avatar
loading={false}
size={160}
name={agentName}
dataURI={originalImageUri}
fallbackDataUri={optimizedImageUri}
/>
</View>
<Text small bold style={styles.greyColor}>
Valid as of {dateFormat(new Date(validationDate), BUSINESS_DATE_FORMAT)}
</Text>
<Heading type="h4" dark bold style={GenericStyles.mv4}>
{agentName}
</Heading>
<Text style={GenericStyles.mb4}>Collection agent</Text>
<Text light bold>
{agentPhone}
</Text>
</View>
);
};
export default AgentIdCard;
const styles = StyleSheet.create({
greyColor: {
color: COLORS.TEXT.GREY,
},
});

View File

@@ -0,0 +1,111 @@
import { Modal, StyleSheet, TouchableOpacity, View } from 'react-native';
import React, { useEffect } from 'react';
import Text from '../../../RN-UI-LIB/src/components/Text';
import { PERMISSIONS, RESULTS, request } from 'react-native-permissions';
import { CameraOptions, launchCamera } from 'react-native-image-picker';
import IDCardIcon from '../../assets/icons/IDCardIcon';
import { GenericStyles } from '../../../RN-UI-LIB/src/styles';
import ArrowRightOutlineIcon from '../../../RN-UI-LIB/src/Icons/ArrowRightOutlineIcon';
import { COLORS } from '../../../RN-UI-LIB/src/styles/colors';
import ReviewIDImage from './ReviewIDImage';
import { useAppDispatch, useAppSelector } from '../../hooks';
import { setImageUri, setShowReviewImageModal } from '../../reducer/profileSlice';
const IDCardImageCapture = () => {
const { imageUri, showReviewImageModal } = useAppSelector((state) => state.profile);
const dispatch = useAppDispatch();
useEffect(() => {
return () => {
dispatch(setImageUri(''));
};
}, []);
const handleImageCapture = () => {
request(PERMISSIONS.ANDROID.CAMERA).then(async (result) => {
if (result === RESULTS.GRANTED) {
const config: CameraOptions = {
mediaType: 'photo',
cameraType: 'front',
};
config.includeExtra = true;
launchCamera(config, (response) => {
if (response?.didCancel) {
return;
}
if (response?.assets?.length) {
const { uri } = response.assets[0];
if (uri) {
dispatch(setImageUri(uri));
if (!imageUri) {
toggleReviewImageModal();
}
}
}
});
}
});
};
const toggleReviewImageModal = () => {
dispatch(setShowReviewImageModal(!showReviewImageModal));
};
const handleCloseModal = () => {
toggleReviewImageModal();
dispatch(setImageUri(''));
};
return (
<View>
<TouchableOpacity
activeOpacity={0.7}
style={[
GenericStyles.mb24,
GenericStyles.row,
GenericStyles.br4,
styles.elevation2,
GenericStyles.overflowHidden,
GenericStyles.whiteBackground,
]}
onPress={handleImageCapture}
>
<View style={styles.iconContainer}>
<IDCardIcon />
</View>
<View style={[GenericStyles.p12, GenericStyles.fill, GenericStyles.row]}>
<Text style={GenericStyles.fill}>
Capture your selfie to create an ID card for customers reference.
</Text>
<View style={styles.chevron}>
<ArrowRightOutlineIcon fillColor={COLORS.TEXT.LIGHT} />
</View>
</View>
</TouchableOpacity>
<Modal visible={showReviewImageModal} onRequestClose={toggleReviewImageModal}>
<ReviewIDImage
closeModal={handleCloseModal}
imageUri={imageUri}
retakeImage={handleImageCapture}
/>
</Modal>
</View>
);
};
const styles = StyleSheet.create({
iconContainer: {
backgroundColor: COLORS.BORDER.TEAL,
paddingHorizontal: 12,
justifyContent: 'center',
},
chevron: {
marginLeft: 10,
marginTop: 6,
},
elevation2: {
elevation: 2,
},
});
export default IDCardImageCapture;

View File

@@ -0,0 +1,57 @@
import { StyleSheet, View } from 'react-native';
import React from 'react';
import NavigationHeader from '../../../RN-UI-LIB/src/components/NavigationHeader';
import { GenericStyles } from '../../../RN-UI-LIB/src/styles';
import { COLORS } from '../../../RN-UI-LIB/src/styles/colors';
import Button from '../../../RN-UI-LIB/src/components/Button';
import RNFastImage from '../../../RN-UI-LIB/src/components/FastImage';
import { useAppDispatch, useAppSelector } from '../../hooks';
import { uploadImageId } from '../../action/profileActions';
interface IReviewIDImage {
imageUri: string;
retakeImage: () => void;
closeModal: () => void;
}
const ReviewIDImage: React.FC<IReviewIDImage> = ({ imageUri, retakeImage, closeModal }) => {
const dispatch = useAppDispatch();
const { agentId, isUploadingImage } = useAppSelector((state) => ({
agentId: state.user.user?.referenceId!!,
isUploadingImage: state.profile.isUploadingImage,
}));
const handleImageUpload = () => {
dispatch(uploadImageId(agentId, imageUri));
};
return (
<View style={[GenericStyles.fill, styles.container]}>
<NavigationHeader title="Review selfie" onBack={closeModal} />
<View style={[GenericStyles.fill, GenericStyles.pv24]}>
<RNFastImage source={{ uri: imageUri }} style={{ height: '100%' }} resizeMode={'cover'} />
</View>
<View style={[GenericStyles.p16, GenericStyles.whiteBackground, GenericStyles.row]}>
<Button
title="Retake"
variant="secondary"
style={GenericStyles.fill}
onPress={retakeImage}
/>
<Button
title="Proceed"
showLoader={isUploadingImage}
style={[GenericStyles.ml16, GenericStyles.fill]}
onPress={handleImageUpload}
/>
</View>
</View>
);
};
export default ReviewIDImage;
const styles = StyleSheet.create({
container: {
backgroundColor: COLORS.BACKGROUND.BLACK,
},
});

View File

@@ -1,10 +1,11 @@
import React, { useState } from 'react';
import React, { useEffect, useState } from 'react';
import {
Alert,
Linking,
Pressable,
ScrollView,
StyleSheet,
TouchableHighlight,
TouchableOpacity,
View,
} from 'react-native';
@@ -31,22 +32,45 @@ import { CaseDetail } from '../caseDetails/interface';
import CaseItem from '../allCases/CaseItem';
import { IUserRole } from '../../reducer/userSlice';
import QuestionMarkIcon from '../../assets/icons/QuestionMarkIcon';
import IDCardImageCapture from './IDCardImageCapture';
import AgentIdCard from './AgentIdCard';
import TranslucentModal from '../../../RN-UI-LIB/src/components/TranslucentModal/TranslucentModal';
import ArrowSolidIcon from '../../../RN-UI-LIB/src/Icons/ArrowSolidIcon';
import { getSelfieDocument } from '../../action/profileActions';
import { ImageApprovalStatus } from '../../reducer/profileSlice';
const Profile: React.FC = () => {
const [buttonPressedCount, setButtonPressedCount] = useState(0);
const [showIdCard, setShowIdCard] = useState(false);
const dispatch = useAppDispatch();
const { phoneNumber, name, roles } = useAppSelector((state: RootState) => state.user.user!!);
const {
originalImageUri,
phoneNumber,
approvalStatus,
name,
roles,
completedList: completeCasesList,
caseDetails,
supportLink,
pendingCases,
} = useAppSelector((state: RootState) => ({
originalImageUri: state.profile.originalImageUri,
imageUri: state.profile.imageUri,
approvalStatus: state.profile.approvalStatus,
phoneNumber: state.user.user?.phoneNumber,
name: state.user.user?.name!!,
roles: state.user.user?.roles!!,
completedList: state.allCases.completedList,
caseDetails: state.allCases.caseDetails,
supportLink: state.config.data?.supportLink,
isUploadingImage: state.profile.isUploadingImage,
pendingCases: state.allCases.pendingList,
}));
useEffect(() => {
dispatch(getSelfieDocument());
}, []);
useFocusEffect(
React.useCallback(() => {
addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.AV_PROFILE_PAGE_LOADED);
@@ -97,26 +121,53 @@ const Profile: React.FC = () => {
const helpButtonClickHandler = () => Linking.openURL(supportLink);
const toggleIdCard = () => setShowIdCard((prev) => !prev);
const hideUploadImageBtn =
approvalStatus === ImageApprovalStatus.PENDING ||
approvalStatus === ImageApprovalStatus.APPROVED;
const handleViewIdCard = () => {
toggleIdCard();
addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_ID_CARD_CLICKED);
};
return (
<View style={GenericStyles.fill}>
<View style={[GenericStyles.fill]}>
<NavigationHeader
title={name}
subTitle={phoneNumber}
showAvatarIcon
bottomActionable={
originalImageUri && pendingCases?.length ? (
<TouchableHighlight
onPress={handleViewIdCard}
underlayColor={COLORS.HIGHLIGHTER.LIGHT_BUTTON}
style={styles.bottomActionable}
>
<View style={[GenericStyles.row, GenericStyles.alignStart]}>
<Text small style={styles.whiteText}>
View ID card
</Text>
<View style={{ transform: [{ rotate: '180deg' }] }}>
<ArrowSolidIcon fillColor={COLORS.TEXT.WHITE} />
</View>
</View>
</TouchableHighlight>
) : null
}
rightActionable={
supportLink ? (
<View style={[GenericStyles.row, GenericStyles.fill, GenericStyles.alignStart]}>
<TouchableOpacity
activeOpacity={0.7}
onPress={helpButtonClickHandler}
style={[GenericStyles.row, GenericStyles.centerAlignedRow]}
>
<QuestionMarkIcon />
<Text style={[GenericStyles.whiteText, styles.helpText, GenericStyles.fw500]}>
Help
</Text>
</TouchableOpacity>
</View>
<TouchableOpacity
activeOpacity={0.7}
onPress={helpButtonClickHandler}
style={[GenericStyles.row, GenericStyles.p16]}
>
<QuestionMarkIcon style={GenericStyles.mt4} />
<Text style={[GenericStyles.whiteText, styles.helpText, GenericStyles.fw500]}>
Help
</Text>
</TouchableOpacity>
) : (
false
)
@@ -127,10 +178,10 @@ const Profile: React.FC = () => {
style={[
GenericStyles.ph16,
GenericStyles.pt16,
GenericStyles.whiteBackground,
numberOfCompletedCases === 2 ? { paddingBottom: 6 } : {},
]}
>
{hideUploadImageBtn ? null : <IDCardImageCapture />}
<View
style={[
GenericStyles.row,
@@ -146,7 +197,13 @@ const Profile: React.FC = () => {
{numberOfCompletedCases
? completeCasesList.slice(0, 2).map((caseItem) => {
const caseDetailItem = caseDetails[caseItem.caseReferenceId] as CaseDetail;
return <CaseItem caseDetailObj={caseDetailItem} isCompleted={true} />;
return (
<CaseItem
key={caseItem.caseReferenceId}
caseDetailObj={caseDetailItem}
isCompleted={true}
/>
);
})
: null}
{numberOfCompletedCases > 2 ? (
@@ -189,6 +246,15 @@ const Profile: React.FC = () => {
</Text>
</Pressable>
</ScrollView>
{pendingCases?.length ? (
<TranslucentModal
visible={showIdCard}
onRequestClose={() => setShowIdCard(false)}
flipAnimation
>
<AgentIdCard />
</TranslucentModal>
) : null}
</View>
);
};
@@ -214,4 +280,12 @@ const styles = StyleSheet.create({
display: 'flex',
flexDirection: 'row',
},
bottomActionable: {
padding: 2,
borderRadius: 4,
alignSelf: 'flex-start',
},
whiteText: {
color: COLORS.TEXT.WHITE,
},
});

View File

@@ -70,6 +70,7 @@ export const ToastMessages = {
CASES_SELECTION_DISABLED: 'Case addition is disabled during the generation of visit plan',
CASES_DELETION_DISABLED: 'Case deletion is disabled during the generation of visit plan',
GEOLOCATION_COORDINATES_INCORRECT: 'Geolocation not found',
IMAGE_UPLOAD_SUCCESS: 'Your ID card has been sent for approval',
};
export enum BOTTOM_TAB_ROUTES {

View File

@@ -21,6 +21,7 @@ import {
import { addClickstreamEvent } from '../../services/clickstreamEventService';
import { CLICKSTREAM_EVENT_NAMES } from '../../common/Constants';
import { BOTTOM_TAB_ROUTES } from './constants';
import { getSelfieDocument } from '../../action/profileActions';
const AllCasesMain = () => {
const { pendingList, pinnedList, loading } = useAppSelector((state) => state.allCases);
@@ -53,6 +54,9 @@ const AllCasesMain = () => {
currentTab: getCurrentScreen()?.name,
nextTab: e?.data?.routeName,
});
if (e?.data?.routeName === BOTTOM_TAB_ROUTES.Profile) {
dispatch(getSelfieDocument());
}
};
useEffect(() => {

View File

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

View File

@@ -108,6 +108,14 @@ const NotificationItem: React.FC<INotificationProps> = ({ data }) => {
navigateToScreen(BOTTOM_TAB_ROUTES.VisitPlan);
return;
}
if (
templateName === NotificationTypes.AGENT_ID_APPROVED_TEMPLATE ||
templateName === NotificationTypes.AGENT_ID_REJECTED_TEMPLATE
) {
navigateToScreen(BOTTOM_TAB_ROUTES.VisitPlan);
navigateToScreen(BOTTOM_TAB_ROUTES.Profile);
return;
}
if (!collectionCaseId || !clickable) {
return;
}
@@ -144,11 +152,11 @@ const NotificationItem: React.FC<INotificationProps> = ({ data }) => {
>
{notificationIcon}
<View style={[GenericStyles.pl16, GenericStyles.flex80]}>
<Heading type="h5" dark>
{templateName === NotificationTypes.VISIT_PLAN_UPDATE_NOTIFICATION_TEMPLATE
? 'New visit plan created!'
: customerName}
</Heading>
{customerName ? (
<Heading type="h5" dark>
{customerName}
</Heading>
) : null}
<NotificationTemplate data={data} />
<Text small>{getTimeDifference(scheduledAt)}</Text>
</View>

View File

@@ -1,9 +1,10 @@
import { StyleSheet } from 'react-native';
import { View } 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';
import Heading from '../../../RN-UI-LIB/src/components/Heading';
interface INotificationTemplateProps {
data: INotification;
@@ -167,15 +168,40 @@ const NotificationTemplate: React.FC<INotificationTemplateProps> = ({ data }) =>
);
case NotificationTypes.VISIT_PLAN_UPDATE_NOTIFICATION_TEMPLATE:
return (
<Text light>
for <Text dark>{date}</Text> with <Text dark>{caseCount}</Text> cases
</Text>
<View>
<Heading type="h5" dark>
New visit plan created!
</Heading>
<Text light>
for <Text dark>{date}</Text> with <Text dark>{caseCount}</Text> cases
</Text>
</View>
);
case NotificationTypes.AGENT_ID_APPROVED_TEMPLATE:
return (
<View>
<Heading type="h5" dark>
ID card approved!
</Heading>
<Text light>
Your ID card has been approved. Head over to profile to checkout your ID card
</Text>
</View>
);
case NotificationTypes.AGENT_ID_REJECTED_TEMPLATE:
return (
<View>
<Heading type="h5" dark>
ID card rejected!
</Heading>
<Text light>
Your ID card has been rejected. Please upload your selfie again for the ID card
</Text>
</View>
);
default:
return <Text>New notification</Text>;
}
};
const styles = StyleSheet.create({});
export default NotificationTemplate;

View File

@@ -1,3 +1,5 @@
import IDCardApproveIcon from '../../../RN-UI-LIB/src/Icons/IDCardApproveIcon';
import IDCardRejectIcon from '../../../RN-UI-LIB/src/Icons/IDCardRejectIcon';
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';
@@ -26,6 +28,8 @@ export enum NotificationTypes {
ENACH_PAYMENT_MADE_TEMPLATE = 'ENACH_PAYMENT_MADE_TEMPLATE',
ENACH_PAYMENT_FAILED_TEMPLATE = 'ENACH_PAYMENT_FAILED_TEMPLATE',
VISIT_PLAN_UPDATE_NOTIFICATION_TEMPLATE = 'VISIT_PLAN_UPDATE_NOTIFICATION_TEMPLATE',
AGENT_ID_APPROVED_TEMPLATE = 'AGENT_ID_APPROVED_TEMPLATE',
AGENT_ID_REJECTED_TEMPLATE = 'AGENT_ID_REJECTED_TEMPLATE',
}
export const NotificationIconsMap = {
@@ -48,6 +52,8 @@ export const NotificationIconsMap = {
[NotificationTypes.LONGHORN_ALERTS_NEW_ENQUIRY]: <NotificationIcon />,
[NotificationTypes.REVISIT_SCHEDULED_NOTIFICATION_TEMPLATE]: <NotificationIcon />,
[NotificationTypes.VISIT_PLAN_UPDATE_NOTIFICATION_TEMPLATE]: <NotificationVisitPlanIcon />,
[NotificationTypes.AGENT_ID_APPROVED_TEMPLATE]: <IDCardApproveIcon />,
[NotificationTypes.AGENT_ID_REJECTED_TEMPLATE]: <IDCardRejectIcon />,
};
export enum WidgetStatus {

View File

@@ -29,6 +29,7 @@ import MetadataSlice from '../reducer/metadataSlice';
import foregroundServiceSlice from '../reducer/foregroundServiceSlice';
import feedbackImagesSlice from '../reducer/feedbackImagesSlice';
import configSlice from '../reducer/configSlice';
import profileSlice from '../reducer/profileSlice';
const rootReducer = combineReducers({
case: caseReducer,
@@ -48,6 +49,7 @@ const rootReducer = combineReducers({
foregroundService: foregroundServiceSlice,
feedbackImages: feedbackImagesSlice,
config: configSlice,
profile: profileSlice,
});
const persistConfig = {
@@ -67,6 +69,7 @@ const persistConfig = {
'address',
'feedbackImages',
'config',
'profile',
],
blackList: ['case', 'filters'],
};