TP-30455 | Foreclosure payments (#448)

* TP-30455 | Foreclosure payment

* TP-30455 | Foreclosure payments

* TP-30455 | fix

* TP-30455 | submodule update

* TP-30455 | yarn lock update

* TP-30455 | fixes

* TP-30455 | submodule update
This commit is contained in:
Aman Chaturvedi
2023-05-31 15:44:26 +05:30
committed by GitHub Enterprise
parent 1eefbe36db
commit 42987636fa
18 changed files with 1017 additions and 471 deletions

View File

@@ -1,6 +1,5 @@
import { ToastMessages } from '../screens/allCases/constants';
import { Dispatch } from '@reduxjs/toolkit';
import { appendLoanIdToValue, setLoading, setPaymentLink } from '../reducer/paymentSlice';
import axiosInstance, {
ApiKeys,
API_STATUS_CODE,
@@ -11,17 +10,19 @@ import { toast } from '../../RN-UI-LIB/src/components/toast';
import { CLICKSTREAM_EVENT_NAMES } from '../common/Constants';
import { addClickstreamEvent } from '../services/clickstreamEventService';
import { logError } from '../components/utlis/errorUtils';
import { DEFAULT_FORECLOSURE_BREAKUP } from '../screens/registerPayements/Foreclosure';
export enum PaymentType {
EMI = 'EMI',
CUSTOM = 'CUSTOM',
FORECLOSURE = 'FORECLOSURE',
}
export interface GeneratePaymentPayload {
alternateContactNumber: string;
customAmount: {
amount: number;
currency: string;
};
customAmountProvided: boolean;
type: PaymentType;
phoneNumber: string;
customAmount?: number;
loanAccountNumber: string;
notifyToAlternateContact: boolean;
customerReferenceId: string;
customerId: string;
}
export interface IGeneratePaymentLinkApiResponse {
paymentLink: string;
@@ -31,75 +32,76 @@ export interface IGeneratePaymentLinkApiResponse {
export interface ILoanIdValue
extends IGeneratePaymentLinkApiResponse,
Pick<GeneratePaymentPayload, 'alternateContactNumber' | 'customAmount'> {}
Pick<GeneratePaymentPayload, 'phoneNumber' | 'customAmount'> {}
export const generatePaymentLinkAction =
(payload: GeneratePaymentPayload) => (dispatch: Dispatch) => {
const url = getApiUrl(ApiKeys.GENERATE_PAYMENT_LINK);
const { loanAccountNumber } = payload;
export const generatePaymentLinkAction = (
payload: GeneratePaymentPayload,
successCb: (paymentLink: string) => void,
failureCb: () => void
) => {
const url = getApiUrl(ApiKeys.GENERATE_PAYMENT_LINK);
axiosInstance
.post(url, payload)
.then((response) => {
if (response?.status === API_STATUS_CODE.OK) {
const responseData = response.data;
const { paymentLink, retriesLeft } = responseData || {};
if (paymentLink) {
successCb(paymentLink);
toast({
type: 'success',
text1: `${ToastMessages.PAYMENT_LINK_SUCCESS} ${retriesLeft} tr${
retriesLeft > 1 ? 'ies' : 'y'
} remaining.`,
});
}
}
})
.catch((err) => {
logError(err);
failureCb();
if (isAxiosError(err)) {
const { type, loanAccountNumber, customAmount, phoneNumber } = payload;
const clickstreamPayload = {
amount: customAmount,
lan: loanAccountNumber,
phoneNumber,
type,
};
addClickstreamEvent(
CLICKSTREAM_EVENT_NAMES.FA_SEND_PAYMENT_LINK_FAILED,
clickstreamPayload
);
if (err?.response?.status === API_STATUS_CODE.TOO_MANY_REQUESTS) {
addClickstreamEvent(
CLICKSTREAM_EVENT_NAMES.FA_SEND_PAYMENT_LINK_FAILED_LIMIT_REACHED,
clickstreamPayload
);
toast({
type: 'error',
text1: ToastMessages.PAYMENT_LINK_RETRY,
});
} else {
toast({
type: 'error',
text1: ToastMessages.PAYMENT_LINK_ERROR,
});
}
}
});
};
dispatch(setLoading(true));
try {
axiosInstance
.post(url, payload)
.then((response) => {
if (response?.status === API_STATUS_CODE.OK) {
const responseData = response.data;
const { paymentLink, retriesLeft } = responseData || {};
if (paymentLink) {
dispatch(setPaymentLink(paymentLink));
const storePayload = {
...responseData,
customAmount: payload.customAmount,
alternateContactNumber: payload.alternateContactNumber,
};
dispatch(
appendLoanIdToValue({
[loanAccountNumber]: storePayload,
})
);
toast({
type: 'success',
text1: `${ToastMessages.PAYMENT_LINK_SUCCESS} ${retriesLeft} tr${
retriesLeft > 1 ? 'ies' : 'y'
} remaining.`,
});
}
}
})
.catch((err) => {
logError(err);
if (isAxiosError(err)) {
addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_SEND_PAYMENT_LINK_FAILED, {
amount: payload.customAmount,
lan: payload.loanAccountNumber,
phoneNumber: payload.alternateContactNumber,
});
if (err?.response?.status === 429) {
addClickstreamEvent(
CLICKSTREAM_EVENT_NAMES.FA_SEND_PAYMENT_LINK_FAILED_LIMIT_REACHED,
{
amount: payload.customAmount,
lan: payload.loanAccountNumber,
phoneNumber: payload.alternateContactNumber,
}
);
toast({
type: 'error',
text1: ToastMessages.PAYMENT_LINK_RETRY,
});
} else {
toast({
type: 'error',
text1: ToastMessages.PAYMENT_LINK_ERROR,
});
}
}
})
.finally(() => {
dispatch(setLoading(false));
});
} catch (err) {
dispatch(setLoading(false));
}
};
export const getForeclosureAmount = async (loanAccountNumber: string, preclosureDate: string) => {
try {
const url = getApiUrl(
ApiKeys.GET_FORECLOSURE_AMOUNT,
{ loanAccountNumber },
{ preclosureDate }
);
const response = await axiosInstance.get(url);
return response?.data;
} catch (err) {
logError(err as Error, 'Error fetching foreclosure amount');
return DEFAULT_FORECLOSURE_BREAKUP;
}
};

View File

@@ -1,51 +1,90 @@
import React, { useState } from 'react';
import { View, StyleSheet, Pressable } from 'react-native';
import Text from '../../../RN-UI-LIB/src/components/Text';
import React, { useRef, useState } from 'react';
import {
View,
StyleSheet,
Pressable,
StyleProp,
ViewStyle,
Animated,
Easing,
LayoutChangeEvent,
} from 'react-native';
import { COLORS } from '../../../RN-UI-LIB/src/styles/colors';
import ArrowSolidDownIcon from '../../../RN-UI-LIB/src/Icons/ArrowSolidDownIcon';
import DropdownIcon from '../../../RN-UI-LIB/src/Icons/DropdownIcon';
import { GenericStyles } from '../../../RN-UI-LIB/src/styles';
interface IAccordion {
title: string;
content: React.ReactNode;
title: React.ReactNode;
children: React.ReactNode;
containerStyle?: StyleProp<ViewStyle>;
defaultExpanded?: boolean;
}
const Accordion: React.FC<IAccordion> = ({ title, content }) => {
const [isExpanded, setIsExpanded] = useState(false);
const Accordion: React.FC<IAccordion> = ({ title, children, containerStyle, defaultExpanded }) => {
const [isExpanded, setIsExpanded] = useState(defaultExpanded);
const [bodySectionHeight, setBodySectionHeight] = useState(0);
const animatedController = useRef(new Animated.Value(isExpanded ? 1 : 0)).current;
const bodyHeight = animatedController.interpolate({
inputRange: [0, 1],
outputRange: [0, bodySectionHeight],
});
const arrowAngle = animatedController.interpolate({
inputRange: [0, 1],
outputRange: ['0rad', `${Math.PI}rad`],
});
const handlePress = () => {
const sharedAnimationConfig = {
duration: 300,
useNativeDriver: false,
};
if (isExpanded) {
Animated.timing(animatedController, {
...sharedAnimationConfig,
toValue: 0,
easing: Easing.bezier(0.4, 0.0, 0.2, 1),
}).start();
} else {
Animated.timing(animatedController, {
...sharedAnimationConfig,
toValue: 1,
easing: Easing.bezier(0.4, 0.0, 0.2, 1),
}).start();
}
setIsExpanded(!isExpanded);
};
const handleLayout = (event: LayoutChangeEvent) => {
setBodySectionHeight(event.nativeEvent.layout.height);
};
return (
<View style={styles.container}>
<View style={[styles.container, containerStyle]}>
<Pressable onPress={handlePress}>
<View style={[styles.titleContainer]}>
<Text dark bold>
{title}
</Text>
<View
style={[
{
transform: [{ rotate: isExpanded ? '180deg' : '0deg' }],
},
]}
>
<ArrowSolidDownIcon size={10} />
</View>
{title}
<Animated.View style={{ transform: [{ rotateZ: arrowAngle }] }}>
<DropdownIcon />
</Animated.View>
</View>
</Pressable>
{isExpanded ? <View style={styles.content}>{content}</View> : null}
<Animated.View style={[GenericStyles.overflowHidden, { height: bodyHeight }]}>
<View style={styles.content} onLayout={handleLayout}>
{children}
</View>
</Animated.View>
</View>
);
};
const styles = StyleSheet.create({
container: {
marginVertical: 10,
marginHorizontal: 16,
borderRadius: 4,
borderRadius: 8,
borderWidth: 1,
borderColor: COLORS.BORDER.GREY,
position: 'relative',
},
titleContainer: {
flexDirection: 'row',
@@ -56,7 +95,10 @@ const styles = StyleSheet.create({
},
content: {
paddingHorizontal: 12,
paddingVertical: 18,
paddingBottom: 16,
position: 'absolute',
bottom: 0,
width: '100%',
},
});

View File

@@ -1,53 +1,58 @@
import { View, StyleSheet, TouchableOpacity } from 'react-native';
import { View, StyleSheet, StyleProp, ViewStyle } from 'react-native';
import React, { useState } from 'react';
import { GenericStyles, getShadowStyle } from '../../../RN-UI-LIB/src/styles';
import { GenericStyles } from '../../../RN-UI-LIB/src/styles';
import Text from '../../../RN-UI-LIB/src/components/Text';
import { COLORS } from '../../../RN-UI-LIB/src/styles/colors';
import InfoIcon from '../../../RN-UI-LIB/src/Icons/InfoIcon';
import CloseIconSmall from '../../../RN-UI-LIB/src/Icons/CloseIconSmall';
import Button from '../../../RN-UI-LIB/src/components/Button';
interface IFloatingInfoText {
top?: number;
bottom?: number;
visible?: boolean;
message: string;
style?: StyleProp<ViewStyle>;
onClose?: () => void;
}
const FloatingInfoText: React.FC<IFloatingInfoText> = ({ message, top, bottom }) => {
const [showText, setShowText] = useState(true);
const toggleText = () => setShowText(false);
const FloatingInfoText: React.FC<IFloatingInfoText> = ({
visible = true,
message,
onClose,
style,
}) => {
const [showText, setShowText] = useState(visible);
const toggleText = () => {
setShowText(false);
onClose && typeof onClose === 'function' && onClose();
};
if (!showText) return null;
return (
<View style={[styles.container, GenericStyles.centerAlignedRow, { top, bottom }]}>
<View style={[styles.textContainer, getShadowStyle(2), GenericStyles.centerAlignedRow]}>
<InfoIcon color={COLORS.BASE.BLUE} />
<Text small style={styles.text}>
{message}
</Text>
<TouchableOpacity onPress={toggleText} style={styles.closeIcon}>
<CloseIconSmall />
</TouchableOpacity>
</View>
<View
style={[
styles.textContainer,
GenericStyles.row,
GenericStyles.spaceBetween,
GenericStyles.alignCenter,
style,
]}
>
<Text small light>
{message}
</Text>
<Button
variant="primaryText"
onPress={toggleText}
title="Understood"
textStyle={GenericStyles.fontSize12}
/>
</View>
);
};
const styles = StyleSheet.create({
container: {
position: 'absolute',
width: '100%',
},
textContainer: {
paddingHorizontal: 30,
paddingHorizontal: 12,
paddingVertical: 3,
borderRadius: 4,
backgroundColor: COLORS.BACKGROUND.BLUE,
},
text: {
color: COLORS.BASE.BLUE,
marginLeft: 10,
},
closeIcon: {
marginLeft: 10,
backgroundColor: COLORS.BACKGROUND.SILVER,
},
});

View File

@@ -120,7 +120,7 @@ const FiltersContainer: React.FC<FilterContainerProps> = (props) => {
<>
<View
style={[
styles.filterGroupHeader,
styles.silverBackground,
GenericStyles.row,
GenericStyles.alignCenter,
GenericStyles.spaceBetween,

View File

@@ -65,7 +65,7 @@ const styles = StyleSheet.create({
borderRadius: 20,
alignItems: 'center',
},
filterGroupHeader: {
silverBackground: {
backgroundColor: COLORS.BACKGROUND.SILVER,
},
filterOption: {

View File

@@ -46,6 +46,7 @@ export enum ApiKeys {
CASES_SYNC_STATUS = 'CASES_SYNC_STATUS',
CASES_SEND_ID = 'CASES_SEND_ID',
FETCH_CASES = 'FETCH_CASES',
GET_FORECLOSURE_AMOUNT = 'GET_FORECLOSURE_AMOUNT',
}
export const API_URLS: Record<ApiKeys, string> = {} as Record<ApiKeys, string>;
@@ -58,7 +59,7 @@ API_URLS[ApiKeys.LOGOUT] = '/auth/logout';
API_URLS[ApiKeys.FEEDBACK] = '/cases/feedback';
API_URLS[ApiKeys.FILTERS] = '/cases/filters';
API_URLS[ApiKeys.JANUS] = '/events/json';
API_URLS[ApiKeys.GENERATE_PAYMENT_LINK] = '/send-payment-link';
API_URLS[ApiKeys.GENERATE_PAYMENT_LINK] = '/payments/send-payment-link';
API_URLS[ApiKeys.ADDRESSES_GEOLOCATION] = '/addresses-geolocations';
API_URLS[ApiKeys.NEW_ADDRESS] = '/addresses';
API_URLS[ApiKeys.GET_SIGNED_URL] = '/cases/get-signed-urls';
@@ -78,6 +79,7 @@ API_URLS[ApiKeys.TELEPHONES] = '/telephones';
API_URLS[ApiKeys.CASES_SYNC_STATUS] = '/cases/agents/sync-status';
API_URLS[ApiKeys.CASES_SEND_ID] = '/cases/sync';
API_URLS[ApiKeys.FETCH_CASES] = '/cases/agents/{agentReferenceId}';
API_URLS[ApiKeys.GET_FORECLOSURE_AMOUNT] = '/{loanAccountNumber}/pre-closure-amount';
export const API_STATUS_CODE = {
OK: 200,
@@ -87,6 +89,7 @@ export const API_STATUS_CODE = {
FORBIDDEN: 403,
NOT_FOUND: 404,
INTERNAL_SERVER_ERROR: 500,
TOO_MANY_REQUESTS: 429,
};
const API_TIMEOUT_INTERVAL = 2e4; // 20s

View File

@@ -6,12 +6,12 @@ import {
IDocument,
IPhoneSources,
PhoneNumber,
PhoneNumberSource,
TDocumentObj,
} from '../../screens/caseDetails/interface';
import { getPrefixBase64Image, LocalStorageKeys, MimeType } from '../../common/Constants';
import NetInfo from '@react-native-community/netinfo';
import Clipboard from '@react-native-clipboard/clipboard';
import address from '../form/components/Address';
import { useWindowDimensions } from 'react-native';
import { GlobalDocumentMap } from '../../../App';
import { GenericType } from '../../common/GenericTypes';
@@ -169,6 +169,21 @@ export const getPhoneSourceString = (sources: IPhoneSources[]) => {
return '';
};
export const getPrimaryPhoneNumber = (phoneNumbers: PhoneNumber[] | null) => {
if (!phoneNumbers?.length) {
return '';
}
const index = phoneNumbers.findIndex((number) => {
const { sources } = number;
const isPrimaryNumber = sources.some((source) => source.type === PhoneNumberSource.PRIMARY);
return isPrimaryNumber;
});
if (index !== -1) {
return phoneNumbers[2]?.number;
}
return phoneNumbers[0]?.number;
};
export const getDynamicBottomSheetHeightPercentageFn = (headerOffset = 100, rowHeight = 50) => {
const SCREEN_HEIGHT = useWindowDimensions().height;
@@ -320,11 +335,10 @@ export const getMaxByPropFromList = (arr: GenericType, prop: string) => {
return arr.find((x: GenericType) => x[prop] == max);
};
export const getGoogleMapUrl = (latitude: string | number, longitude: string | number) => {
if (!latitude || !longitude) return;
return `https://www.google.com/maps/search/${latitude},+${longitude}`;
}
};
export const isValidAmountEntered = (value: number) => {
return typeof value === 'number' && !isNaN(value);

View File

@@ -1,20 +0,0 @@
import AsyncStorage from '@react-native-async-storage/async-storage';
import { LocalStorageKeys } from '../../common/Constants';
import { ILoanIdToValue } from '../../reducer/paymentSlice';
import { setAsyncStorageItem } from './commonFunctions';
export const getLoanIdToValueFromLocal = async () => {
const loanIdToValueStored = await AsyncStorage.getItem(LocalStorageKeys.LOAN_ID_TO_VALUE);
if (loanIdToValueStored) {
let parsedLoanIdToValue = JSON.parse(loanIdToValueStored) as ILoanIdToValue;
const currentDate = Date.now();
for (const [key, value] of Object.entries(parsedLoanIdToValue)) {
if (Number(value.expiresAt) < currentDate) {
delete parsedLoanIdToValue?.[key];
}
}
await setAsyncStorageItem(LocalStorageKeys.LOAN_ID_TO_VALUE, parsedLoanIdToValue);
return parsedLoanIdToValue;
}
return null;
};

View File

@@ -4,52 +4,23 @@ import { ILoanIdValue } from '../action/paymentActions';
export type ILoanIdToValue = Record<string, ILoanIdValue>;
interface IPaymentState {
paymentLink: string;
isLoading: boolean;
loanIdToValue: ILoanIdToValue;
showForeclosureInfoText: boolean;
}
const initialState: IPaymentState = {
paymentLink: '',
isLoading: false,
loanIdToValue: {},
showForeclosureInfoText: true,
};
const paymentSlice = createSlice({
name: 'payment',
initialState,
reducers: {
setPaymentLink: (state, action: PayloadAction<string>) => {
return {
...state,
paymentLink: action.payload,
};
},
setLoading: (state, action: PayloadAction<boolean>) => {
return {
...state,
isLoading: action.payload,
};
},
setLoanIdToValue: (state, action: PayloadAction<ILoanIdToValue>) => {
return {
...state,
loanIdToValue: action.payload,
};
},
appendLoanIdToValue: (state, action: PayloadAction<ILoanIdToValue>) => {
return {
...state,
loanIdToValue: {
...state.loanIdToValue,
...action.payload,
},
};
setShowForeclosureInfoText: (state, action) => {
state.showForeclosureInfoText = action.payload;
},
},
});
export const { setPaymentLink, setLoading, setLoanIdToValue, appendLoanIdToValue } =
paymentSlice.actions;
export const { setShowForeclosureInfoText } = paymentSlice.actions;
export default paymentSlice.reducer;

View File

@@ -1,11 +1,4 @@
import {
Pressable,
RefreshControl,
SafeAreaView,
ScrollView,
StyleSheet,
View,
} from 'react-native';
import { RefreshControl, SafeAreaView, ScrollView, StyleSheet, View } from 'react-native';
import React, { useEffect, useState } from 'react';
import Layout from '../layout/Layout';
import { GenericStyles } from '../../../RN-UI-LIB/src/styles';
@@ -25,11 +18,18 @@ import { getCaseUnifiedData, UnifiedCaseDetailsTypes } from '../../action/caseAp
import RepaymentsTab from './repayments/RepaymentsTab';
import { toast } from '../../../RN-UI-LIB/src/components/toast';
import { ToastMessages } from '../allCases/constants';
import CustomTabs from '../../../RN-UI-LIB/src/components/customTabs/CustomTabs';
enum EmiTab {
Schedule,
Repayments,
}
const TABS = [
{
key: 'schedule',
label: 'Schedule',
},
{
key: 'repayments',
label: 'Repayments',
},
];
const PAGE_TITLE = 'EMI schedule';
interface IEmiSchedule {
@@ -47,7 +47,7 @@ const EmiSchedule: React.FC<IEmiSchedule> = (props) => {
params: { loanAccountNumber, customerReferenceId, caseId },
},
} = props;
const [currentTab, setCurrentTab] = useState<EmiTab>(EmiTab.Schedule);
const [currentTab, setCurrentTab] = useState<string>(TABS[0].key);
const dispatch = useAppDispatch();
const isOnline = useIsOnline();
const { refreshing, onRefresh } = useRefresh(() => {
@@ -70,7 +70,7 @@ const EmiSchedule: React.FC<IEmiSchedule> = (props) => {
const backHandler = () => {
popToScreen(1);
};
const onTabChange = (tab: EmiTab) => {
const onTabChange = (tab: string) => {
setCurrentTab(tab);
};
@@ -127,53 +127,15 @@ const EmiSchedule: React.FC<IEmiSchedule> = (props) => {
<Text light>Start date</Text>
</View>
</View>
<View style={[styles.tabContainer]}>
<View style={[GenericStyles.row, GenericStyles.alignCenter]}>
<Pressable
onPress={() => onTabChange(EmiTab.Schedule)}
style={{
borderBottomWidth: 2,
borderBottomColor:
currentTab === EmiTab.Schedule ? COLORS.BASE.BLUE : 'transparent',
padding: 8,
}}
>
<Text
style={[
{
color: currentTab === EmiTab.Schedule ? COLORS.BASE.BLUE : COLORS.TEXT.GREY,
},
]}
>
Schedule
</Text>
</Pressable>
<Pressable
onPress={() => onTabChange(EmiTab.Repayments)}
style={{
borderBottomWidth: 2,
borderBottomColor:
currentTab === EmiTab.Repayments ? COLORS.BASE.BLUE : 'transparent',
padding: 8,
marginLeft: 24,
}}
>
<Text
style={[
{
color: currentTab === EmiTab.Repayments ? COLORS.BASE.BLUE : COLORS.TEXT.GREY,
},
]}
>
Repayments
</Text>
</Pressable>
</View>
</View>
<CustomTabs
tabs={TABS}
currentTab={currentTab}
onTabChange={onTabChange}
containerStyle={[GenericStyles.ml4, styles.pt31]}
/>
</ScrollView>
<View style={styles.tabItemContainer}>
{currentTab === EmiTab.Schedule ? (
{currentTab === TABS[0].key ? (
<EmiScheduleTab loanAccountNumber={loanAccountNumber} />
) : (
<RepaymentsTab loanAccountNumber={loanAccountNumber} />
@@ -214,6 +176,9 @@ const styles = StyleSheet.create({
paddingBottom: 100,
backgroundColor: 'white',
},
pt31: {
paddingTop: 31,
},
});
export default EmiSchedule;

View File

@@ -0,0 +1,295 @@
import React, { useEffect, useMemo, useState } from 'react';
import { Controller, useForm } from 'react-hook-form';
import { ScrollView, StyleSheet, TouchableOpacity, View } from 'react-native';
import Button from '../../../RN-UI-LIB/src/components/Button';
import Text from '../../../RN-UI-LIB/src/components/Text';
import Dropdown from '../../../RN-UI-LIB/src/components/dropdown/Dropdown';
import { GenericStyles } from '../../../RN-UI-LIB/src/styles';
import { COLORS } from '../../../RN-UI-LIB/src/styles/colors';
import {
PaymentType,
generatePaymentLinkAction,
getForeclosureAmount,
} from '../../action/paymentActions';
import { CLICKSTREAM_EVENT_NAMES } from '../../common/Constants';
import {
copyToClipboard,
getDynamicBottomSheetHeightPercentageFn,
getPhoneNumberString,
} from '../../components/utlis/commonFunctions';
import { useAppDispatch, useAppSelector } from '../../hooks';
import useIsOnline from '../../hooks/useIsOnline';
import { setShowForeclosureInfoText } from '../../reducer/paymentSlice';
import { addClickstreamEvent } from '../../services/clickstreamEventService';
import { PhoneNumber } from '../caseDetails/interface';
import DropdownItem from './DropdownItem';
import ModalWrapper from '../../../RN-UI-LIB/src/components/modalWrapper/ModalWrapper';
import QrCodeModal from './QrCodeModal';
import { toast } from '../../../RN-UI-LIB/src/components/toast';
import { ToastMessages } from '../allCases/constants';
import { BUSINESS_DATE_FORMAT_SHORT_YEAR, dateFormat } from '../../../RN-UI-LIB/src/utlis/dates';
import Chevron from '../../../RN-UI-LIB/src/Icons/Chevron';
import FloatingInfoText from '../../components/floatingInfoText';
import ForeclosureBottomSheet from './ForeclosureBottomSheet';
import ForeclosureBreakupAccordion, { IForeclosureBreakup } from './ForeclosureBreakupAccordion';
interface IForeclosure {
caseId: string;
numbers: PhoneNumber[];
primaryPhoneNumber: string;
}
interface IForeclosureForm {
selectedPhoneNumber: string;
}
const HEADER_HEIGHT = 100;
const ROW_HEIGHT = 40;
export const DEFAULT_FORECLOSURE_BREAKUP = {
totalAmount: 0,
principal: 0,
interest: 0,
otherFees: 0,
};
const Foreclosure: React.FC<IForeclosure> = ({ caseId, numbers, primaryPhoneNumber }) => {
const { caseDetail, showForeclosureInfoText } = useAppSelector((state) => ({
caseDetail: state.allCases.caseDetails[caseId],
showForeclosureInfoText: state.payment.showForeclosureInfoText,
}));
const dispatch = useAppDispatch();
const isOnline = useIsOnline();
const [generateClicked, setGenerateClicked] = useState(false);
const [showQrCodeModal, setShowQrCodeModal] = React.useState<boolean>(false);
const [showForeclosureBottomSheet, setShowForeclosureBottomSheet] = useState(false);
const [paymentLink, setPaymentLink] = useState('');
const [isLoading, setIsLoading] = useState(false);
const [foreclosureBreakup, setForeclosureBreakup] = useState<IForeclosureBreakup>(
DEFAULT_FORECLOSURE_BREAKUP
);
const { loanAccountNumber } = caseDetail;
useEffect(() => {
if (paymentLink && generateClicked) {
setShowQrCodeModal(true);
}
}, [paymentLink, generateClicked]);
useEffect(() => {
(async () => {
const foreclosureAmount: IForeclosureBreakup = await getForeclosureAmount(
loanAccountNumber!!,
dateFormat(new Date(), 'YYYY-MM-DD')
);
setForeclosureBreakup(foreclosureAmount);
})();
}, []);
const {
control,
getValues,
formState: { isValid },
setValue,
trigger,
} = useForm<IForeclosureForm>({
defaultValues: {
selectedPhoneNumber: primaryPhoneNumber,
},
mode: 'onChange',
});
const getBottomSheetHeight = getDynamicBottomSheetHeightPercentageFn(HEADER_HEIGHT, ROW_HEIGHT);
const ChildComponents = useMemo(
() =>
numbers?.map((phoneNumber) => {
const { number, createdAt, sourceText } = phoneNumber;
return (
<DropdownItem
createdAt={createdAt}
id={number}
label={getPhoneNumberString(phoneNumber)}
sourceText={sourceText}
/>
);
}),
[numbers]
);
const onSuccess = (link: string) => {
setIsLoading(false);
setPaymentLink(link);
};
const onError = () => {
setIsLoading(false);
};
const generatePaymentLink = () => {
addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_SEND_PAYMENT_LINK, {
lan: caseDetail.loanAccountNumber!!,
phoneNumber: getValues('selectedPhoneNumber'),
type: PaymentType.FORECLOSURE,
});
setGenerateClicked(false);
if (paymentLink) {
setShowQrCodeModal(true);
} else {
setIsLoading(true);
generatePaymentLinkAction(
{
type: PaymentType.FORECLOSURE,
phoneNumber: getValues('selectedPhoneNumber'),
loanAccountNumber: caseDetail.loanAccountNumber!!,
customerId: caseDetail.customerReferenceId,
},
onSuccess,
onError
);
setGenerateClicked(true);
}
};
const isCopyButtonDisabled = !isValid || !isOnline || !paymentLink || paymentLink === '';
const isPaymentButtonDisabled = !isValid || !isOnline;
const copyButtonClick = () => {
addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_COPY_LINK_CLICKED, {
lan: caseDetail.loanAccountNumber!!,
phoneNumber: getValues('selectedPhoneNumber'),
paymentLink,
});
if (paymentLink) {
copyToClipboard(paymentLink);
toast({
type: 'info',
text1: ToastMessages.SUCCESS_COPYING_PAYMENT_LINK,
});
}
};
const toggleForeclosureBottomSheet = () => {
setShowForeclosureBottomSheet(!showForeclosureBottomSheet);
};
const handleCloseInfoText = () => {
dispatch(setShowForeclosureInfoText(false));
};
return (
<View style={GenericStyles.fill}>
<ScrollView style={[GenericStyles.p16, GenericStyles.whiteBackground]}>
<Text style={[GenericStyles.mb8]}>Select number to share foreclosure link</Text>
<Controller
control={control}
render={({ field: { value } }) => (
<Dropdown
placeholder="Select phone number"
onValueChange={(number) => {
addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_COLLECT_MONEY_NUMBER_CHANGED, {
phoneNumber: number,
lan: caseDetail.loanAccountNumber!!,
});
setValue('selectedPhoneNumber', number);
setPaymentLink('');
trigger();
}}
bottomSheetHeight={getBottomSheetHeight(numbers?.length)}
header="Select phone number"
value={value}
>
{ChildComponents}
</Dropdown>
)}
name="selectedPhoneNumber"
rules={{ required: true }}
/>
<ForeclosureBreakupAccordion
title={`Foreclosure amount as of ${dateFormat(
new Date(),
BUSINESS_DATE_FORMAT_SHORT_YEAR
)}`}
foreclosureBreakup={foreclosureBreakup}
/>
<TouchableOpacity
style={[
GenericStyles.row,
GenericStyles.alignCenter,
GenericStyles.pv10,
GenericStyles.mt4,
]}
activeOpacity={0.7}
onPress={toggleForeclosureBottomSheet}
>
<Text style={styles.futureText} bold>
Check amount for future date
</Text>
<View style={styles.mt3}>
<Chevron fillColor={COLORS.TEXT.BLUE} />
</View>
</TouchableOpacity>
</ScrollView>
<FloatingInfoText
visible={showForeclosureInfoText}
message={'The amount is updated daily on the same link!'}
style={GenericStyles.brt8}
onClose={handleCloseInfoText}
/>
<View
style={[
GenericStyles.whiteBackground,
GenericStyles.row,
GenericStyles.alignCenter,
GenericStyles.spaceBetween,
GenericStyles.elevation10,
GenericStyles.p16,
]}
>
<Button
title="Generate and share link"
style={[GenericStyles.w100]}
disabled={isPaymentButtonDisabled}
showLoader={isLoading}
onPress={generatePaymentLink}
/>
</View>
<ModalWrapper
animationType="slide"
animated
onRequestClose={() => setShowQrCodeModal((prev) => !prev)}
visible={showQrCodeModal}
>
<QrCodeModal
closeQrCodeModal={() => setShowQrCodeModal(false)}
isCopyButtonDisabled={isCopyButtonDisabled}
copyButtonClick={copyButtonClick}
caseId={caseId}
paymentLink={paymentLink}
/>
</ModalWrapper>
<ForeclosureBottomSheet
showForeclosureBottomSheet={showForeclosureBottomSheet}
loanAccountNumber={loanAccountNumber!!}
toggleForeclosureBottomSheet={toggleForeclosureBottomSheet}
/>
</View>
);
};
export default Foreclosure;
const styles = StyleSheet.create({
amount: {
fontSize: 13,
color: COLORS.TEXT.GREEN,
marginRight: 14,
},
futureText: {
color: COLORS.TEXT.BLUE,
marginRight: 8,
},
mt3: {
marginTop: 3,
},
});

View File

@@ -0,0 +1,74 @@
import { TouchableOpacity, View } from 'react-native';
import React, { useState } from 'react';
import Text from '../../../RN-UI-LIB/src/components/Text';
import BottomSheet from '../../../RN-UI-LIB/src/components/bottom_sheet/BottomSheet';
import Heading from '../../../RN-UI-LIB/src/components/Heading';
import { row } from '../emiSchedule/constants';
import { GenericStyles } from '../../../RN-UI-LIB/src/styles';
import CloseIcon from '../../../RN-UI-LIB/src/Icons/CloseIcon';
import { COLORS } from '../../../RN-UI-LIB/src/styles/colors';
import DateTimePicker, {
IDateTimePickerMode,
} from '../../../RN-UI-LIB/src/components/dateTimePicker/DateTimePicker';
import ForeclosureBreakupAccordion, { IForeclosureBreakup } from './ForeclosureBreakupAccordion';
import { getForeclosureAmount } from '../../action/paymentActions';
import { DEFAULT_FORECLOSURE_BREAKUP } from './Foreclosure';
import { BUSINESS_DATE_FORMAT } from '../../../RN-UI-LIB/src/utlis/dates';
interface IForeclosureBottomSheet {
showForeclosureBottomSheet: boolean;
loanAccountNumber: string;
toggleForeclosureBottomSheet: () => void;
}
const ForeclosureBottomSheet: React.FC<IForeclosureBottomSheet> = ({
showForeclosureBottomSheet,
loanAccountNumber,
toggleForeclosureBottomSheet,
}) => {
const [foreclosureBreakup, setForeclosureBreakup] = useState<IForeclosureBreakup>(
DEFAULT_FORECLOSURE_BREAKUP
);
const handleDateChange = async (date: string) => {
setForeclosureBreakup(DEFAULT_FORECLOSURE_BREAKUP);
const foreclosureAmount: IForeclosureBreakup = await getForeclosureAmount(
loanAccountNumber!!,
date
);
setForeclosureBreakup(foreclosureAmount);
};
return (
<BottomSheet
HeaderNode={() => (
<View style={[...row, GenericStyles.p16]}>
<Heading dark type="h3">
Foreclosure amount by selected date
</Heading>
<TouchableOpacity activeOpacity={0.7} onPress={toggleForeclosureBottomSheet}>
<CloseIcon color={COLORS.TEXT.LIGHT} />
</TouchableOpacity>
</View>
)}
heightPercentage={60}
visible={showForeclosureBottomSheet}
setVisible={toggleForeclosureBottomSheet}
>
<View style={GenericStyles.ph16}>
<Text style={[GenericStyles.fontSize13]}>Select payment date</Text>
<DateTimePicker
inputFieldClickable={true}
disabled={true}
mode={IDateTimePickerMode.DATE}
handleConfirm={handleDateChange}
minimumDate={new Date()}
resultDateFormat="YYYY-MM-DD"
displayDateTimeFormat={BUSINESS_DATE_FORMAT}
/>
<ForeclosureBreakupAccordion foreclosureBreakup={foreclosureBreakup} defaultExpanded />
</View>
</BottomSheet>
);
};
export default ForeclosureBottomSheet;

View File

@@ -0,0 +1,99 @@
import { StyleSheet, View } from 'react-native';
import React, { useMemo } from 'react';
import { GenericStyles } from '../../../RN-UI-LIB/src/styles';
import Accordion from '../../components/accordion';
import Text from '../../../RN-UI-LIB/src/components/Text';
import { COLORS } from '../../../RN-UI-LIB/src/styles/colors';
import { formatAmount } from '../../../RN-UI-LIB/src/utlis/amount';
import LineLoader from '../../../RN-UI-LIB/src/components/suspense_loader/LineLoader';
import SuspenseLoader from '../../../RN-UI-LIB/src/components/suspense_loader/SuspenseLoader';
interface IForeclosureBreakupAccordion {
title?: string;
defaultExpanded?: boolean;
foreclosureBreakup: IForeclosureBreakup;
}
export interface IForeclosureBreakup {
totalAmount: number;
principal: number;
interest: number;
otherFees: number;
}
const ForeclosureBreakupAccordion: React.FC<IForeclosureBreakupAccordion> = ({
title,
foreclosureBreakup,
defaultExpanded,
}) => {
const { totalAmount, principal, interest, otherFees } = foreclosureBreakup;
const foreclosureBreakupMap = useMemo(
() => [
{
label: 'Principal',
value: principal,
},
{
label: 'Interest',
value: interest,
},
{
label: 'Fees',
value: otherFees,
},
],
[principal, interest, otherFees]
);
return (
<View style={GenericStyles.mt16}>
{title ? <Text style={[GenericStyles.mb8]}>{title}</Text> : null}
<Accordion
title={
<View style={[GenericStyles.fill, GenericStyles.row, GenericStyles.spaceBetween]}>
<Text light>Total foreclosure amount</Text>
<Text style={styles.amount} bold>
<SuspenseLoader
loading={totalAmount === 0}
fallBack={<LineLoader width={80} height={10} />}
>
{formatAmount(totalAmount)}
</SuspenseLoader>
</Text>
</View>
}
containerStyle={GenericStyles.silverBackground}
defaultExpanded={defaultExpanded}
>
{foreclosureBreakupMap.map(({ label, value }) => (
<View
style={[
GenericStyles.row,
GenericStyles.spaceBetween,
GenericStyles.alignCenter,
GenericStyles.borderTop,
GenericStyles.pv10,
]}
>
<Text light>{label}</Text>
<SuspenseLoader
loading={totalAmount === 0}
fallBack={<LineLoader width={60} height={10} />}
>
<Text dark>{formatAmount(value)}</Text>
</SuspenseLoader>
</View>
))}
</Accordion>
</View>
);
};
export default ForeclosureBreakupAccordion;
const styles = StyleSheet.create({
amount: {
fontSize: 13,
color: COLORS.TEXT.GREEN,
marginRight: 14,
},
});

View File

@@ -0,0 +1,263 @@
import React, { useEffect, useState } from 'react';
import { Controller, useForm } from 'react-hook-form';
import { ScrollView, StyleSheet, View } from 'react-native';
import Button from '../../../RN-UI-LIB/src/components/Button';
import Text from '../../../RN-UI-LIB/src/components/Text';
import TextInput, { TextInputMaskType } from '../../../RN-UI-LIB/src/components/TextInput';
import Dropdown from '../../../RN-UI-LIB/src/components/dropdown/Dropdown';
import { GenericStyles } from '../../../RN-UI-LIB/src/styles';
import { COLORS } from '../../../RN-UI-LIB/src/styles/colors';
import { formatAmount } from '../../../RN-UI-LIB/src/utlis/amount';
import { PaymentType, generatePaymentLinkAction } from '../../action/paymentActions';
import { CLICKSTREAM_EVENT_NAMES } from '../../common/Constants';
import {
copyToClipboard,
getDynamicBottomSheetHeightPercentageFn,
getPhoneNumberString,
isValidAmountEntered,
} from '../../components/utlis/commonFunctions';
import { useAppSelector } from '../../hooks';
import useIsOnline from '../../hooks/useIsOnline';
import { addClickstreamEvent } from '../../services/clickstreamEventService';
import { PhoneNumber } from '../caseDetails/interface';
import DropdownItem from './DropdownItem';
import ModalWrapper from '../../../RN-UI-LIB/src/components/modalWrapper/ModalWrapper';
import QrCodeModal from './QrCodeModal';
import { toast } from '../../../RN-UI-LIB/src/components/toast';
import { ToastMessages } from '../allCases/constants';
interface IOnlinePayment {
caseId: string;
amount: number;
numbers: PhoneNumber[];
pos: number;
primaryPhoneNumber: string;
}
interface IRegisterForm {
selectedPhoneNumber: string;
amount: string;
}
const HEADER_HEIGHT = 100;
const ROW_HEIGHT = 40;
const OnlinePayment: React.FC<IOnlinePayment> = ({
caseId,
amount,
numbers,
pos,
primaryPhoneNumber,
}) => {
const { caseDetail } = useAppSelector((state) => ({
caseDetail: state.allCases.caseDetails[caseId],
}));
const isOnline = useIsOnline();
const [generateClicked, setGenerateClicked] = useState(false);
const [showQrCodeModal, setShowQrCodeModal] = React.useState<boolean>(false);
const [paymentLink, setPaymentLink] = useState('');
const [isLoading, setIsLoading] = useState(false);
useEffect(() => {
if (paymentLink && generateClicked) {
setShowQrCodeModal(true);
}
}, [paymentLink, generateClicked]);
const maxAmount = pos + amount;
const {
control,
getValues,
formState: { isValid, errors },
setValue,
trigger,
} = useForm<IRegisterForm>({
defaultValues: {
amount: String(amount) ?? '',
selectedPhoneNumber: primaryPhoneNumber,
},
mode: 'onChange',
});
const getBottomSheetHeight = getDynamicBottomSheetHeightPercentageFn(HEADER_HEIGHT, ROW_HEIGHT);
const ChildComponents = numbers?.map((phoneNumber) => {
const { number, createdAt, sourceText } = phoneNumber;
return (
<DropdownItem
createdAt={createdAt}
id={number}
label={getPhoneNumberString(phoneNumber)}
sourceText={sourceText}
/>
);
});
let errorMessage = '';
switch (errors.amount?.type) {
case 'required':
errorMessage = 'This is a required field';
break;
case 'min':
case 'max':
errorMessage = `The entered amount should be between ${formatAmount(1)} and ${formatAmount(
maxAmount
)}`;
break;
}
const isPaymentButtonDisabled = !isValid || !isOnline;
const onSuccess = (link: string) => {
setPaymentLink(link);
setIsLoading(false);
};
const onError = () => {
setIsLoading(false);
};
const generatePaymentLink = () => {
setIsLoading(true);
const [amountValue, phoneNumberValue] = [
Number(getValues('amount')),
getValues('selectedPhoneNumber'),
];
const { loanAccountNumber } = caseDetail;
addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_SEND_PAYMENT_LINK, {
amount: amountValue,
lan: loanAccountNumber!!,
phoneNumber: phoneNumberValue,
});
setGenerateClicked(false);
if (paymentLink) {
setShowQrCodeModal(true);
} else {
generatePaymentLinkAction(
{
type: PaymentType.CUSTOM,
phoneNumber: phoneNumberValue,
customAmount: amountValue,
loanAccountNumber: loanAccountNumber!!,
customerId: caseDetail.customerReferenceId,
},
onSuccess,
onError
);
setGenerateClicked(true);
}
};
const isCopyButtonDisabled = !isValid || !isOnline || !paymentLink || paymentLink === '';
const copyButtonClick = () => {
addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_COPY_LINK_CLICKED, {
amount: Number(getValues('amount')),
lan: caseDetail.loanAccountNumber!!,
phoneNumber: getValues('selectedPhoneNumber'),
paymentLink,
});
if (paymentLink) {
copyToClipboard(paymentLink);
toast({
type: 'info',
text1: ToastMessages.SUCCESS_COPYING_PAYMENT_LINK,
});
}
};
return (
<View style={GenericStyles.fill}>
<ScrollView style={[GenericStyles.p16, GenericStyles.whiteBackground]}>
<Text light>Share a payment link on the selected number</Text>
<Text style={[GenericStyles.mb8, GenericStyles.mt16]}>Select number</Text>
<Controller
control={control}
render={({ field: { value } }) => (
<Dropdown
placeholder="Select phone number"
onValueChange={(number) => {
addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_COLLECT_MONEY_NUMBER_CHANGED, {
phoneNumber: number,
lan: caseDetail.loanAccountNumber!!,
});
setValue('selectedPhoneNumber', number);
setPaymentLink('');
trigger();
}}
bottomSheetHeight={getBottomSheetHeight(numbers?.length)}
header="Select phone number"
value={value}
>
{ChildComponents}
</Dropdown>
)}
name="selectedPhoneNumber"
rules={{ required: true }}
/>
<View style={GenericStyles.mt16}>
<Controller
control={control}
render={({ field: { onChange, onBlur, value } }) => (
<TextInput
titleStyle={{ color: COLORS.TEXT.BLACK }}
keyboardType="decimal-pad"
title="Enter amount"
onChangeText={(s) => {
setPaymentLink('');
onChange(s);
}}
maskType={TextInputMaskType.CURRENCY}
onBlur={onBlur}
placeholder="Enter amount"
value={value}
LeftComponent={<Text dark></Text>}
error={errors?.amount ? true : false}
errorMessage={errorMessage}
/>
)}
name="amount"
rules={{
required: true,
min: 1,
max: maxAmount,
validate: (value) => isValidAmountEntered(Number(value)),
}}
/>
</View>
</ScrollView>
<View
style={[
GenericStyles.whiteBackground,
GenericStyles.row,
GenericStyles.alignCenter,
GenericStyles.spaceBetween,
GenericStyles.elevation10,
GenericStyles.p16,
]}
>
<Button
title="Generate and share link"
style={[GenericStyles.w100]}
disabled={isPaymentButtonDisabled}
showLoader={isLoading}
onPress={generatePaymentLink}
/>
</View>
<ModalWrapper
animationType="slide"
animated
onRequestClose={() => setShowQrCodeModal((prev) => !prev)}
visible={showQrCodeModal}
>
<QrCodeModal
closeQrCodeModal={() => setShowQrCodeModal(false)}
isCopyButtonDisabled={isCopyButtonDisabled}
copyButtonClick={copyButtonClick}
caseId={caseId}
paymentLink={paymentLink}
/>
</ModalWrapper>
</View>
);
};
export default OnlinePayment;

View File

@@ -5,8 +5,6 @@ import Heading from '../../../RN-UI-LIB/src/components/Heading';
import Text from '../../../RN-UI-LIB/src/components/Text';
import { COLORS } from '../../../RN-UI-LIB/src/styles/colors';
import QRCode from 'react-native-qrcode-svg';
import { useAppSelector } from '../../hooks';
import { RootState } from '../../store/store';
import Button from '../../../RN-UI-LIB/src/components/Button';
import { useEffect } from 'react';
import { addClickstreamEvent } from '../../services/clickstreamEventService';
@@ -17,6 +15,7 @@ interface IQrCodeModalProps {
isCopyButtonDisabled: boolean;
copyButtonClick: () => void;
caseId: string;
paymentLink: string;
}
const QrCodeModal: React.FC<IQrCodeModalProps> = ({
@@ -24,8 +23,8 @@ const QrCodeModal: React.FC<IQrCodeModalProps> = ({
isCopyButtonDisabled,
copyButtonClick,
caseId,
paymentLink,
}) => {
const { paymentLink } = useAppSelector((state: RootState) => state.payment);
useEffect(() => {
addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_QR_SCREEN_LOADED, {
caseId: caseId,

View File

@@ -1,41 +1,19 @@
import { View } from 'react-native';
import React, { useEffect, useState } from 'react';
import React, { useEffect, useMemo, useState } from 'react';
import NavigationHeader from '../../../RN-UI-LIB/src/components/NavigationHeader';
import { goBack } from '../../components/utlis/navigationUtlis';
import Text from '../../../RN-UI-LIB/src/components/Text';
import Layout from '../layout/Layout';
import { GenericStyles } from '../../../RN-UI-LIB/src/styles';
import Dropdown from '../../../RN-UI-LIB/src/components/dropdown/Dropdown';
import TextInput, { TextInputMaskType } from '../../../RN-UI-LIB/src/components/TextInput';
import Button from '../../../RN-UI-LIB/src/components/Button';
import { COLORS } from '../../../RN-UI-LIB/src/styles/colors';
import { Controller, useForm } from 'react-hook-form';
import { toast } from '../../../RN-UI-LIB/src/components/toast';
import {
copyToClipboard,
getDynamicBottomSheetHeightPercentageFn,
getPhoneNumberString,
isValidAmountEntered,
} from '../../components/utlis/commonFunctions';
import useIsOnline from '../../hooks/useIsOnline';
import { RootState } from '../../store/store';
import { generatePaymentLinkAction } from '../../action/paymentActions';
import { useAppDispatch, useAppSelector } from '../../hooks';
import DropdownItem from './DropdownItem';
import { PhoneNumber } from '../caseDetails/interface';
import { setLoading, setPaymentLink } from '../../reducer/paymentSlice';
import { ToastMessages } from '../allCases/constants';
import { addClickstreamEvent } from '../../services/clickstreamEventService';
import { CLICKSTREAM_EVENT_NAMES } from '../../common/Constants';
import QrCodeModal from './QrCodeModal';
import ModalWrapper from '../../../RN-UI-LIB/src/components/modalWrapper/ModalWrapper';
import OfflineScreen from '../../common/OfflineScreen';
import { formatAmount } from '../../../RN-UI-LIB/src/utlis/amount';
interface IRegisterForm {
selectedPhoneNumber: string;
amount: string;
}
import OnlinePayment from './OnlinePayment';
import CustomTabs from '../../../RN-UI-LIB/src/components/customTabs/CustomTabs';
import { GenericStyles } from '../../../RN-UI-LIB/src/styles';
import { View } from 'react-native';
import Foreclosure from './Foreclosure';
import { getPrimaryPhoneNumber } from '../../components/utlis/commonFunctions';
import { PaymentType } from '../../action/paymentActions';
interface IRegisterPayments {
route: {
@@ -48,119 +26,35 @@ interface IRegisterPayments {
};
}
const HEADER_HEIGHT = 100;
const ROW_HEIGHT = 40;
const PAGE_TITLE = 'Collect Money';
const TABS = [
{
key: PaymentType.CUSTOM,
label: 'Online payment',
},
{
key: PaymentType.FORECLOSURE,
label: 'Foreclosure',
},
];
const RegisterPayments: React.FC<IRegisterPayments> = ({ route }) => {
let {
params: { caseId, numbers, amount, pos },
params: { caseId, amount, numbers, pos },
} = route;
const { isLoading, paymentLink } = useAppSelector((state: RootState) => state.payment);
const dispatch = useAppDispatch();
const isOnline = useIsOnline();
const caseDetail = useAppSelector((state) => state.allCases.caseDetails[caseId]);
const maxAmount = pos + amount;
const {
control,
getValues,
formState: { isValid, defaultValues, errors },
setValue,
trigger,
} = useForm<IRegisterForm>({
defaultValues: {
amount: String(amount) ?? '',
selectedPhoneNumber: '',
},
mode: 'onChange',
});
const [generateClicked, setGenerateClicked] = useState(false);
useEffect(() => {
if (paymentLink && generateClicked) {
setShowQrCodeModal(true);
}
}, [paymentLink, generateClicked]);
const ChildComponents = numbers?.map((phoneNumber) => {
const { number, createdAt, sourceText } = phoneNumber;
return (
<DropdownItem
createdAt={createdAt}
id={number}
label={getPhoneNumberString(phoneNumber)}
sourceText={sourceText}
/>
);
});
const [showQrCodeModal, setShowQrCodeModal] = React.useState<boolean>(false);
const copyButtonClick = () => {
addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_COPY_LINK_CLICKED, {
amount: Number(getValues('amount')),
lan: caseDetail.loanAccountNumber!!,
phoneNumber: getValues('selectedPhoneNumber'),
paymentLink,
});
if (paymentLink) {
copyToClipboard(paymentLink);
toast({
type: 'info',
text1: ToastMessages.SUCCESS_COPYING_PAYMENT_LINK,
});
}
};
const isPaymentButtonDisabled = !isValid || !isOnline;
const isCopyButtonDisabled = !isValid || !isOnline || !paymentLink || paymentLink === '';
let errorMessage = '';
switch (errors.amount?.type) {
case 'required':
errorMessage = 'This is a required field';
break;
case 'min':
case 'max':
errorMessage = `The entered amount should be between ${formatAmount(1)} and ${formatAmount(
maxAmount
)}`;
break;
}
const generatePaymentLink = () => {
addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_SEND_PAYMENT_LINK, {
amount: Number(getValues('amount')),
lan: caseDetail.loanAccountNumber!!,
phoneNumber: getValues('selectedPhoneNumber'),
});
setGenerateClicked(false);
if (paymentLink) {
setShowQrCodeModal(true);
} else {
dispatch(
generatePaymentLinkAction({
alternateContactNumber: getValues('selectedPhoneNumber'),
customAmount: {
currency: 'INR',
amount: Number(getValues('amount')),
},
customAmountProvided: Number(getValues('amount')) > -1,
loanAccountNumber: caseDetail.loanAccountNumber!!,
notifyToAlternateContact: true,
customerReferenceId: caseDetail.customerReferenceId,
})
);
setGenerateClicked(true);
}
};
const [currentTab, setCurrentTab] = useState<string>(PaymentType.CUSTOM);
useEffect(() => {
addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_COLLECT_MONEY_LOADED, { caseId: caseId });
return () => {
dispatch(setPaymentLink(''));
dispatch(setLoading(false));
};
}, []);
const getBottomSheetHeight = getDynamicBottomSheetHeightPercentageFn(HEADER_HEIGHT, ROW_HEIGHT);
const onTabChange = (tab: string) => {
setCurrentTab(tab);
};
const primaryPhoneNumber: string = useMemo(() => getPrimaryPhoneNumber(numbers), [numbers]);
if (!isOnline) {
return <OfflineScreen goBack={goBack} pageTitle={PAGE_TITLE} />;
@@ -168,86 +62,26 @@ const RegisterPayments: React.FC<IRegisterPayments> = ({ route }) => {
return (
<Layout>
<NavigationHeader title={PAGE_TITLE} onBack={goBack} />
<View style={[GenericStyles.fill, GenericStyles.p16, GenericStyles.whiteBackground]}>
<Text light>Share a payment link on the selected number</Text>
<Text style={[GenericStyles.mb8, GenericStyles.mt16]}>Select number</Text>
<Controller
control={control}
render={({ field: { onChange, onBlur, value } }) => (
<Dropdown
placeholder="Select phone number"
onValueChange={(number) => {
addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_COLLECT_MONEY_NUMBER_CHANGED, {
phoneNumber: number,
lan: caseDetail.loanAccountNumber!!,
});
setValue('selectedPhoneNumber', number);
dispatch(setPaymentLink(''));
trigger();
}}
bottomSheetHeight={getBottomSheetHeight(numbers?.length)}
header="Select phone number"
value={value}
>
{ChildComponents}
</Dropdown>
)}
name="selectedPhoneNumber"
rules={{ required: true }}
<View style={[GenericStyles.fill, GenericStyles.whiteBackground]}>
<NavigationHeader title={PAGE_TITLE} onBack={goBack} />
<CustomTabs
tabs={TABS}
currentTab={currentTab}
onTabChange={onTabChange}
containerStyle={[GenericStyles.ml16, GenericStyles.pt16]}
/>
<View style={GenericStyles.mt16}>
<Controller
control={control}
render={({ field: { onChange, onBlur, value } }) => (
<TextInput
titleStyle={{ color: COLORS.TEXT.BLACK }}
keyboardType="decimal-pad"
title="Enter amount"
onChangeText={(s) => {
dispatch(setPaymentLink(''));
onChange(s);
}}
maskType={TextInputMaskType.CURRENCY}
onBlur={onBlur}
placeholder="Enter amount"
value={value}
LeftComponent={<Text dark></Text>}
error={errors?.amount ? true : false}
errorMessage={errorMessage}
/>
)}
name="amount"
rules={{
required: true,
min: 1,
max: maxAmount,
validate: (value) => isValidAmountEntered(Number(value)),
}}
{currentTab === PaymentType.CUSTOM ? (
<OnlinePayment
caseId={caseId}
amount={amount}
numbers={numbers}
pos={pos}
primaryPhoneNumber={primaryPhoneNumber}
/>
</View>
<Button
title="Generate and share payment link"
style={[GenericStyles.mt32, GenericStyles.w100]}
disabled={isPaymentButtonDisabled}
showLoader={isLoading}
onPress={generatePaymentLink}
/>
) : (
<Foreclosure caseId={caseId} numbers={numbers} primaryPhoneNumber={primaryPhoneNumber} />
)}
</View>
<ModalWrapper
animationType="slide"
animated
onRequestClose={() => setShowQrCodeModal((prev) => !prev)}
visible={showQrCodeModal}
>
<QrCodeModal
closeQrCodeModal={() => setShowQrCodeModal(false)}
isCopyButtonDisabled={isCopyButtonDisabled}
copyButtonClick={copyButtonClick}
caseId={caseId}
/>
</ModalWrapper>
</Layout>
);
};

View File

@@ -7566,7 +7566,7 @@ react-native-call-log@2.1.2:
resolved "https://registry.yarnpkg.com/react-native-call-log/-/react-native-call-log-2.1.2.tgz#f80d2fcb45f72118eb8048d5bfdef191fd4a3df3"
integrity sha512-nWHmb+QMN/AbbZFEuUGiQePssPgjQr5dibNAURDlqO4S5wuLk1XzxxsLUAVHZnB0FdJoMlajD7tUAXtaoxYwUQ==
react-native-clarity@^0.0.3:
react-native-clarity@0.0.3:
version "0.0.3"
resolved "https://registry.yarnpkg.com/react-native-clarity/-/react-native-clarity-0.0.3.tgz#e9c83dd097c0cdaf0751c7f6683fc3b038134637"
integrity sha512-uXccmC9iKwxBpsy7jOLaEo87RBQax/WNCI5EDa/hldn1+8eNCwwhkUdJZeNgi1pZ/dyBmHZj0Be0jgWy4K4gkA==