Merge pull request #528 from medici/feat/TP-31863
TP-31863 | ID Card maker flow
This commit is contained in:
@@ -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');
|
||||
}
|
||||
|
||||
69
src/action/profileActions.ts
Normal file
69
src/action/profileActions.ts
Normal 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');
|
||||
});
|
||||
};
|
||||
23
src/assets/icons/IDCardIcon.tsx
Normal file
23
src/assets/icons/IDCardIcon.tsx
Normal 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;
|
||||
@@ -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';
|
||||
|
||||
@@ -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,
|
||||
|
||||
71
src/reducer/profileSlice.ts
Normal file
71
src/reducer/profileSlice.ts
Normal 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;
|
||||
74
src/screens/Profile/AgentIdCard.tsx
Normal file
74
src/screens/Profile/AgentIdCard.tsx
Normal 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,
|
||||
},
|
||||
});
|
||||
111
src/screens/Profile/IDCardImageCapture.tsx
Normal file
111
src/screens/Profile/IDCardImageCapture.tsx
Normal 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 customer’s 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;
|
||||
57
src/screens/Profile/ReviewIDImage.tsx
Normal file
57
src/screens/Profile/ReviewIDImage.tsx
Normal 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,
|
||||
},
|
||||
});
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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'],
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user