Merge pull request #414 from medici/uat/12-may-2023

16 May Release
This commit is contained in:
Aman Chaturvedi
2023-05-18 16:37:50 +05:30
committed by GitHub Enterprise
54 changed files with 1086 additions and 693 deletions

View File

@@ -119,11 +119,7 @@ const App = () => {
}
/>
</NavigationContainer>
{
<KeyboardAvoidingView behavior="position">
<ToastContainer config={toastConfigs} position="bottom" />
</KeyboardAvoidingView>
}
<ToastContainer config={toastConfigs} position="top" topOffset={18} />
</PersistGate>
</Provider>
);

View File

@@ -53,6 +53,7 @@
<activity
android:name=".MainActivity"
android:label="@string/app_name"
android:screenOrientation="portrait"
android:configChanges="keyboard|keyboardHidden|orientation|screenLayout|screenSize|smallestScreenSize|uiMode"
android:launchMode="singleTask"
android:windowSoftInputMode="stateUnspecified|adjustPan"

View File

@@ -0,0 +1,72 @@
package com.avapp;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.location.LocationManager;
import androidx.annotation.Nullable;
import com.facebook.react.bridge.ActivityEventListener;
import com.facebook.react.bridge.Promise;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
import org.json.JSONArray;
import java.util.List;
public class DeviceUtilsModule extends ReactContextBaseJavaModule implements ActivityEventListener {
private ReactApplicationContext RNContext;
public DeviceUtilsModule(@Nullable ReactApplicationContext reactContext){
super(reactContext);
RNContext = reactContext;
}
@Override
public String getName() {
return "DeviceUtilsModule";
}
@Override
public void onActivityResult(Activity activity, int i, int i1, @Nullable Intent intent) {
}
@Override
public void onNewIntent(Intent intent) {
}
@ReactMethod
public void isLocationEnabled (Promise promise) {
try {
LocationManager locationManager = (LocationManager) RNContext.getApplicationContext().getSystemService(Context.LOCATION_SERVICE);
Boolean isEnabled = locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER);
promise.resolve(isEnabled);
} catch (Exception err) {
promise.reject(err);
}
}
@ReactMethod
public void getAllInstalledApp (Promise promise) {
try {
PackageManager packageManager = RNContext.getPackageManager();
List<ApplicationInfo> packages = packageManager.getInstalledApplications(PackageManager.GET_META_DATA);
JSONArray jsonArray = new JSONArray();
for (ApplicationInfo applicationInfo : packages) {
jsonArray.put(applicationInfo.packageName);
}
promise.resolve( jsonArray.toString());
}catch (Exception err){
promise.reject( err);
}
}
}

View File

@@ -0,0 +1,28 @@
package com.avapp;
import com.facebook.react.ReactPackage;
import com.facebook.react.bridge.NativeModule;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.uimanager.ViewManager;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class DeviceUtilsModulePackage implements ReactPackage {
@Override
public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
return Collections.emptyList();
}
@Override
public List<NativeModule> createNativeModules(
ReactApplicationContext reactContext) {
List<NativeModule> modules = new ArrayList<>();
modules.add(new DeviceUtilsModule(reactContext));
return modules;
}
}

View File

@@ -31,6 +31,7 @@ public class MainApplication extends Application implements ReactApplication {
List<ReactPackage> packages = new PackageList(this).getPackages();
// Packages that cannot be autolinked yet can be added manually here, for example:
// packages.add(new MyReactNativePackage());
packages.add(new DeviceUtilsModulePackage());
return packages;
}

View File

@@ -28,6 +28,7 @@ import { GoogleSignin } from '@react-native-google-signin/google-signin';
export interface GenerateOTPPayload {
phoneNumber: string;
otpToken?: string;
}
export interface VerifyOTPPayload {
@@ -44,12 +45,16 @@ export interface ImpersonationPayload {
}
export const generateOTP =
({ phoneNumber }: GenerateOTPPayload, isResendOTP?: boolean) =>
({ phoneNumber, otpToken }: GenerateOTPPayload, isResendOTP?: boolean) =>
(dispatch: Dispatch) => {
const url = getApiUrl(ApiKeys.GENERATE_OTP);
dispatch(setFormLoading(true));
axiosInstance
.post(url, { phoneNumber: Number(phoneNumber) }, { headers: { donotHandleError: true } })
.post(
url,
{ phoneNumber: Number(phoneNumber), otpToken },
{ headers: { donotHandleError: true } }
)
.then((response) => {
if (response.status === 200) {
if (response?.data?.data?.otpToken) {

View File

@@ -1,18 +1,16 @@
import { ToastMessages } from '../screens/allCases/constants';
import { ILoanIdToValue } from './../reducer/paymentSlice';
import { setAsyncStorageItem } from './../components/utlis/commonFunctions';
import { Dispatch } from '@reduxjs/toolkit';
import { appendLoanIdToValue, setLoading, setPaymentLink } from '../reducer/paymentSlice';
import axiosInstance, {
ApiKeys,
API_STATUS_CODE,
getApiUrl,
getErrorMessage,
isAxiosError,
} from '../components/utlis/apiHelper';
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';
export interface GeneratePaymentPayload {
alternateContactNumber: string;
@@ -36,82 +34,72 @@ export interface ILoanIdValue
Pick<GeneratePaymentPayload, 'alternateContactNumber' | 'customAmount'> {}
export const generatePaymentLinkAction =
(payload: GeneratePaymentPayload, loanIdToValue: ILoanIdToValue) => (dispatch: Dispatch) => {
(payload: GeneratePaymentPayload) => (dispatch: Dispatch) => {
const url = getApiUrl(ApiKeys.GENERATE_PAYMENT_LINK);
dispatch(setLoading(true));
const currentTime = Date.now();
const { loanAccountNumber } = payload;
const value = loanIdToValue?.[loanAccountNumber];
// if (
// value &&
// Number(value?.expiresAt) > currentTime &&
// value?.alternateContactNumber === payload.alternateContactNumber &&
// value?.customAmount.amount === payload.customAmount.amount
// ) {
// dispatch(setPaymentLink(value.paymentLink));
// dispatch(setLoading(false));
// return;
// }
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,
})
);
// await setAsyncStorageItem(
// LocalStorageKeys.LOAN_ID_TO_VALUE,
// {
// ...loanIdToValue,
// [loanAccountNumber]: storePayload,
// },
// );
toast({
type: 'success',
text1: `${ToastMessages.PAYMENT_LINK_SUCCESS} ${retriesLeft} tr${
retriesLeft > 1 ? 'ies' : 'y'
} remaining.`,
});
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) => {
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, {
})
.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,
});
toast({
type: 'error',
text1: ToastMessages.PAYMENT_LINK_RETRY,
});
} else {
toast({
type: 'error',
text1: ToastMessages.PAYMENT_LINK_ERROR,
});
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));
});
})
.finally(() => {
dispatch(setLoading(false));
});
} catch (err) {
dispatch(setLoading(false));
}
};

View File

@@ -0,0 +1,78 @@
import * as React from 'react';
import Svg, { Path, Rect, Circle } from 'react-native-svg';
const EmptyScheduledListIcon = () => (
<Svg width={181} height={103} viewBox="0 0 181 103" fill="none">
<Path
d="M34.6897 102.46H20.9695L33.0756 10.7637L34.6897 102.46Z"
fill="#545454"
stroke="#12183E"
strokeWidth={0.565625}
/>
<Path
d="M142.747 102.447H34.5993L33.0133 12.1618C32.9979 11.2874 33.7025 10.5703 34.5771 10.5703H139.596C140.449 10.5703 141.145 11.2539 141.16 12.1068L142.747 102.447Z"
fill="white"
stroke="#12183E"
strokeWidth={0.565625}
strokeLinecap="round"
strokeLinejoin="round"
/>
<Path
d="M141.416 26.5777H33.238L33.0092 11.3375C32.9961 10.4646 33.7001 9.75 34.573 9.75H139.59C140.443 9.75 141.139 10.434 141.154 11.2872L141.416 26.5777Z"
fill="#F7F7F7"
stroke="#12183E"
strokeWidth={0.565625}
strokeLinecap="round"
strokeLinejoin="round"
/>
<Rect
x={57.7374}
y={0.59375}
width={4.78646}
height={19.1458}
rx={0.790366}
fill="white"
stroke="#12183E"
strokeWidth={0.565625}
strokeLinecap="round"
strokeLinejoin="round"
/>
<Rect
x={117.417}
y={0.59375}
width={4.78646}
height={19.1458}
rx={0.790366}
fill="white"
stroke="#12183E"
strokeWidth={0.565625}
strokeLinecap="round"
strokeLinejoin="round"
/>
<Path
d="M112.648 65.1756C112.648 52.6463 102.488 42.4863 89.9586 42.4863C77.4293 42.4863 67.2693 52.6463 67.2693 65.1756C67.2693 77.705 77.4293 87.8649 89.9586 87.8649C96.2233 87.8649 101.896 85.3339 106.006 81.2233"
stroke="#12183E"
strokeWidth={0.565625}
strokeLinecap="round"
strokeLinejoin="round"
/>
<Circle cx={90.0276} cy={65.2908} r={20.1834} fill="#EEEEEE" />
<Path
d="M99.4657 73.941L90.3287 64.8039V51.3379"
stroke="#12183E"
strokeWidth={0.565625}
strokeLinecap="round"
strokeLinejoin="round"
/>
<Path
d="M106.017 60.4157L112.698 65.1755L117.458 58.4941"
stroke="#12183E"
strokeWidth={0.565625}
strokeLinecap="round"
strokeLinejoin="round"
/>
<Path d="M151.632 49.9648H181" stroke="#12183E" strokeWidth={0.565625} strokeLinejoin="round" />
<Path d="M0 73.9297H16.4687" stroke="#12183E" strokeWidth={0.565625} strokeLinejoin="round" />
</Svg>
);
export default EmptyScheduledListIcon;

View File

@@ -1,4 +1,4 @@
import React, { ReactNode, useCallback, useEffect, useState } from 'react';
import React, { ReactNode, useCallback, useState } from 'react';
import { Linking } from 'react-native';
import { useSelector } from 'react-redux';
import { RootState } from '../store/store';
@@ -7,17 +7,14 @@ import { getAppVersion } from '../components/utlis/commonFunctions';
import BlockerInstructions from './BlockerInstructions';
import { BLOCKER_SCREEN_DATA } from './Constants';
import { useAppDispatch, useAppSelector } from '../hooks';
import Geolocation from 'react-native-geolocation-service';
import { setIsDeviceLocationEnabled } from '../reducer/foregroundServiceSlice';
import { logError } from '../components/utlis/errorUtils';
import { MILLISECONDS_IN_A_SECOND } from '../../RN-UI-LIB/src/utlis/common';
import { toast } from '../../RN-UI-LIB/src/components/toast';
import { locationEnabled } from '../components/utlis/DeviceUtils';
interface IBlockerScreen {
children?: ReactNode;
}
const RETRY_GEOLOCATION_TIME = 10 * MILLISECONDS_IN_A_SECOND; // 10 seconds
const RETRY_GEOLOCATION_STEPS =
'Unable to retrieve location. Kindly follow the given steps and try again.';
@@ -65,36 +62,15 @@ const BlockerScreen = (props: IBlockerScreen) => {
await Linking.openSettings();
}, []);
useEffect(() => {
let timeoutId = -1;
if (showActionBtnLoader) {
timeoutId = setTimeout(() => {
setShowActionBtnLoader(false);
toast({ type: 'info', text1: RETRY_GEOLOCATION_STEPS });
}, RETRY_GEOLOCATION_TIME);
}
return () => {
if (timeoutId !== -1) {
clearTimeout(timeoutId);
}
};
}, [showActionBtnLoader]);
const handleLocationAccess = async () => {
setShowActionBtnLoader(true);
Geolocation.getCurrentPosition(
() => {
setShowActionBtnLoader(false);
if (!isDeviceLocationEnabled) {
dispatch(setIsDeviceLocationEnabled(true));
}
},
(error) => {
setShowActionBtnLoader(false);
logError(error as any, 'Unable to get location on retry button');
},
{ enableHighAccuracy: true }
);
const isLocationEnabled = await locationEnabled();
if (isLocationEnabled) {
dispatch(setIsDeviceLocationEnabled(isLocationEnabled));
} else {
setShowActionBtnLoader(false);
toast({ type: 'info', text1: RETRY_GEOLOCATION_STEPS });
}
};
if (forceReinstallData?.reinstall_endpoint) {

View File

@@ -382,6 +382,14 @@ export const CLICKSTREAM_EVENT_NAMES = {
name: 'FA_VIEW_PAST_FEEDBACK_PREV_PAGE_CLICKED',
description: 'FA_VIEW_PAST_FEEDBACK_PREV_PAGE_CLICKED',
},
FA_VIEW_PAST_FEEDBACK_FIRST_PAGE_CLICKED: {
name: 'FA_VIEW_PAST_FEEDBACK_FIRST_PAGE_CLICKED',
description: 'FA_VIEW_PAST_FEEDBACK_FIRST_PAGE_CLICKED',
},
FA_VIEW_PAST_FEEDBACK_LAST_PAGE_CLICKED: {
name: 'FA_VIEW_PAST_FEEDBACK_LAST_PAGE_CLICKED',
description: 'FA_VIEW_PAST_FEEDBACK_LAST_PAGE_CLICKED',
},
FA_VIEW_PAST_FEEDBACK_NAVIGATION_PAGE_FAILED: {
name: 'FA_VIEW_PAST_FEEDBACK_PREV_PAGE_FAILED',
description: 'FA_VIEW_PAST_FEEDBACK_PREV_PAGE_FAILED',

View File

@@ -10,7 +10,7 @@ import NoWifiIcon from '../assets/icons/NoWifiIcon';
import { GenericFunctionArgs } from './GenericTypes';
interface IOfflineScreen {
handleRetryEvent: GenericFunctionArgs;
handleRetryEvent?: GenericFunctionArgs;
goBack: GenericFunctionArgs;
pageTitle: string;
}

View File

@@ -31,6 +31,7 @@ const TrackingComponent: React.FC<ITrackingComponent> = ({ children }) => {
const dispatch = useAppDispatch();
const appState = useRef(AppState.currentState);
const bgTrackingTimeoutId = useRef<number>();
const user = useAppSelector((state) => state.user);
const handleTimeSync = async () => {
try {
@@ -46,8 +47,8 @@ const TrackingComponent: React.FC<ITrackingComponent> = ({ children }) => {
const handleSendGeolocation = async () => {
try {
const location = await CaptureGeolocation.fetchLocation(Date.now() + '', 0);
if (location) {
const location = await CaptureGeolocation.fetchLocation(Date.now() + '', 0, appState.current);
if (location && user.isLoggedIn) {
await sendLocationToServer(location);
}
} catch (e: any) {
@@ -112,14 +113,6 @@ const TrackingComponent: React.FC<ITrackingComponent> = ({ children }) => {
useIsLocationEnabled();
useEffect(() => {
return () => {
if (UnstoppableService.isRunning()) {
UnstoppableService.stopAll();
}
};
}, []);
return <>{children}</>;
};

View File

@@ -98,7 +98,7 @@ const OTPInput: React.FC<OTPInputProps> = ({
value={otp[i]}
maxLength={1}
keyboardType="number-pad"
onChangeText={handleOnChangeText}
// onChangeText={handleOnChangeText}
onKeyPress={({ nativeEvent }) => handleKeyPress(i, nativeEvent.key)}
containerStyle={styles.inputContainer}
style={styles.input}

View File

@@ -0,0 +1,53 @@
import { StyleSheet, View } from 'react-native';
import React from 'react';
import { GenericStyles } from '../../../RN-UI-LIB/src/styles';
import WebView from 'react-native-webview';
import NavigationHeader from '../../../RN-UI-LIB/src/components/NavigationHeader';
interface IExpandableImage {
imageSrc?: string;
close?: () => void;
title?: string;
}
const imageHtml = (imageSrc?: string) => `
<html>
<head>
<title>Image Viewer</title>
<style>
body{
height: 100vh;
padding: 0;
margin: 0;
background: #000000;
display: flex;
justify-content: center;
align-items: center;
}
</style>
</head>
<body>
<div class="page pinch-zoom-parent">
<div class="pinch-zoom">
<img style="width: calc(100vw - 32px); max-height: calc(100vh - 32px)" src=${imageSrc} />
</div>
</div>
</body>
</html>
`;
const ExpandableImage: React.FC<IExpandableImage> = ({ imageSrc, title, close }) => {
return (
<View style={GenericStyles.fill}>
<NavigationHeader onBack={close} title={title} />
<WebView
scalesPageToFit={true}
bounces={false}
scrollEnabled={false}
source={{ html: imageHtml(imageSrc) }}
/>
</View>
);
};
export default ExpandableImage;

View File

@@ -81,7 +81,6 @@ const Widget: React.FC<IWidget> = (props) => {
}, [templateData, name]);
const dataToBeValidated = useAppSelector((state) => state.case.caseForm?.[caseId]?.[journey]);
const {
control,
handleSubmit,
@@ -180,6 +179,7 @@ const Widget: React.FC<IWidget> = (props) => {
caseData,
answer: data,
coords,
templateId: templateData.templateId,
});
if (isOnline) {
setIsSubmitting(true);
@@ -259,7 +259,7 @@ const Widget: React.FC<IWidget> = (props) => {
);
toast({
type: 'info',
text1: "Feedback will be submitted automatically once you're back online",
text1: ToastMessages.FEEDBACK_SUBMITTED_OFFLINE,
});
navigateToScreen('caseDetail', {
journey: journey,

View File

@@ -1,8 +1,7 @@
import { PermissionsAndroid } from 'react-native';
import { AppStateStatus, PermissionsAndroid } from 'react-native';
import Geolocation from 'react-native-geolocation-service';
import { toast } from '../../../../RN-UI-LIB/src/components/toast';
import { logError } from '../../utlis/errorUtils';
import { PositionError } from '../../../hooks/useIsLocationEnabled';
const FIVE_MIN = 5 * 60 * 1000;
export const requestLocationPermission = async () => {
try {
@@ -47,7 +46,8 @@ export class CaptureGeolocation {
}
static async fetchLocation(
resourceId: string,
cacheTTL: number = FIVE_MIN
cacheTTL: number = FIVE_MIN,
appState: AppStateStatus = 'active'
): Promise<Geolocation.GeoCoordinates | undefined> {
return new Promise(async (resolve, reject) => {
let cachedLocation = CaptureGeolocation.capturedLocation?.[resourceId];
@@ -60,7 +60,7 @@ export class CaptureGeolocation {
}
CaptureGeolocation.setCapturing(resourceId, true);
const isLocationOn = await requestLocationPermission();
if (!isLocationOn) {
if (!isLocationOn && appState === 'active') {
CaptureGeolocation.setCapturing(resourceId, false);
toast({
type: 'error',
@@ -81,10 +81,6 @@ export class CaptureGeolocation {
logError(error as any, 'Unable to get location');
CaptureGeolocation.setCapturing(resourceId, false);
reject(error);
const { PERMISSION_DENIED, POSITION_UNAVAILABLE } = PositionError;
if (error.code === PERMISSION_DENIED || error.code === POSITION_UNAVAILABLE) {
return;
}
toast({
type: 'error',
text1: 'Error getting geolocation' + JSON.stringify(error || {}),

View File

@@ -1,39 +1,50 @@
import { Pressable, StyleSheet, View } from 'react-native';
import React from 'react';
import { getCurrentScreen, pushToScreen } from '../utlis/navigationUtlis';
import { GenericStyles } from '../../../RN-UI-LIB/src/styles';
import React, { useState } from 'react';
import { StyleSheet, TouchableHighlight, View } from 'react-native';
import BellIcon from '../../../RN-UI-LIB/src/Icons/BellIcon';
import { COLORS } from '../../../RN-UI-LIB/src/styles/colors';
import { useAppSelector } from '../../hooks';
import Text from '../../../RN-UI-LIB/src/components/Text';
import { addClickstreamEvent } from '../../services/clickstreamEventService';
import { GenericStyles } from '../../../RN-UI-LIB/src/styles';
import { COLORS } from '../../../RN-UI-LIB/src/styles/colors';
import { CLICKSTREAM_EVENT_NAMES } from '../../common/Constants';
import { useAppSelector } from '../../hooks';
import { addClickstreamEvent } from '../../services/clickstreamEventService';
import { getCurrentScreen, pushToScreen } from '../utlis/navigationUtlis';
const NotificationMenu = () => {
const { totalUnreadElements } = useAppSelector((state) => state.notifications);
const [isEnabled, setIsEnabled] = useState(false);
const handleNotificationPress = () => {
if (isEnabled) return;
setIsEnabled(true);
addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_NOTIFICATION_ICON_CLICK, {
screenName: getCurrentScreen()?.name,
});
pushToScreen('Notifications');
setTimeout(() => setIsEnabled(false), 400);
};
return (
<Pressable style={[GenericStyles.ph8, GenericStyles.pv10]} onPress={handleNotificationPress}>
<BellIcon />
<View style={[styles.notificationBadge, GenericStyles.alignCenter]}>
{totalUnreadElements ? (
<Text
bold
small
style={[styles.notificationNumber, { width: totalUnreadElements > 9 ? 24 : 16 }]}
>
{totalUnreadElements > 9 ? '9+' : totalUnreadElements}
</Text>
) : null}
<TouchableHighlight
underlayColor={COLORS.HIGHLIGHTER.DARK_BUTTON}
activeOpacity={1}
style={GenericStyles.iconContainerButton}
onPress={handleNotificationPress}
>
<View style={[GenericStyles.ph8, GenericStyles.pv10]}>
<BellIcon />
<View style={[styles.notificationBadge, GenericStyles.alignCenter]}>
{totalUnreadElements ? (
<Text
bold
small
style={[styles.notificationNumber, { width: totalUnreadElements > 9 ? 24 : 16 }]}
>
{totalUnreadElements > 9 ? '9+' : totalUnreadElements}
</Text>
) : null}
</View>
</View>
</Pressable>
</TouchableHighlight>
);
};
@@ -43,7 +54,7 @@ const styles = StyleSheet.create({
position: 'absolute',
height: 16,
marginLeft: 18,
marginTop: -4,
marginTop: 4,
borderRadius: 8,
zIndex: 100,
},

View File

@@ -0,0 +1,10 @@
import { NativeModules } from 'react-native';
const { DeviceUtilsModule } = NativeModules; // this is the same name we returned in getName function.
// returns true if enabled, and false if disabled.
export const locationEnabled = (): Promise<boolean> => DeviceUtilsModule.isLocationEnabled();
// returns array of all the installed packages.
export const getAllInstalledApp = (): Promise<Array<string>> =>
DeviceUtilsModule.getAllInstalledApp();

View File

@@ -46,7 +46,7 @@ export enum ApiKeys {
}
export const API_URLS: Record<ApiKeys, string> = {} as Record<ApiKeys, string>;
API_URLS[ApiKeys.GENERATE_OTP] = '/auth/otp/generate';
API_URLS[ApiKeys.GENERATE_OTP] = '/auth/request-otp';
API_URLS[ApiKeys.VERIFY_OTP] = '/auth/otp/verify';
API_URLS[ApiKeys.ALL_CASES] = '/cases/all-cases';
API_URLS[ApiKeys.CASE_DETAIL] = '/cases/get-cases';
@@ -65,7 +65,7 @@ API_URLS[ApiKeys.NOTIFICATIONS] = '/notification/fetch';
API_URLS[ApiKeys.NOTIFICATION_ACTION] = '/notification/action';
API_URLS[ApiKeys.NOTIFICATION_DELIVERED] = '/notification/delivered';
API_URLS[ApiKeys.SEND_LOCATION] = '/geolocations/agents';
API_URLS[ApiKeys.VERIFY_GOOGLE_SIGN_IN] = '/auth/session/internal/exchange';
API_URLS[ApiKeys.VERIFY_GOOGLE_SIGN_IN] = '/auth/session/internal/exchange/v2';
API_URLS[ApiKeys.SYNC_TIME] = '/sync/server-timestamp';
API_URLS[ApiKeys.IS_DATA_SYNC_REQUIRED] = '/sync-data/is-sync-required';
API_URLS[ApiKeys.GET_PRE_SIGNED_URL_DATA_SYNC] = '/sync-data/get-pre-signed-url';

View File

@@ -27,7 +27,7 @@ export const sendLocationToServer = async (location: GeoCoordinates) => {
export const getSyncTime = async () => {
try {
const url = getApiUrl(ApiKeys.SYNC_TIME);
const response = await axiosInstance.get(url);
const response = await axiosInstance.get(url, { headers: { donotHandleError: true } });
return response?.data?.currentTimestamp;
} catch (error) {
console.log(error);

View File

@@ -180,7 +180,10 @@ const useFirestoreUpdates = () => {
const subscribeToCases = () => {
const collectionPath = `allocations/${user?.referenceId}/cases`;
return subscribeToCollection(handleCasesUpdate, collectionPath);
return firestore()
.collection(collectionPath)
.orderBy('totalOverdueAmount', 'asc') // It is descending order only, but acting weirdly. Need to check.
.onSnapshot(handleCasesUpdate, (err) => handleError(err, collectionPath));
};
const subscribeToAvTemplate = () => {

View File

@@ -1,58 +1,33 @@
import { useEffect, useState } from 'react';
import Geolocation from 'react-native-geolocation-service';
import { useAppDispatch, useAppSelector } from '.';
import { setIsDeviceLocationEnabled } from '../reducer/foregroundServiceSlice';
import usePolling from './usePolling';
import { locationEnabled } from '../components/utlis/DeviceUtils';
import { logError } from '../components/utlis/errorUtils';
import { MILLISECONDS_IN_A_SECOND } from '../../RN-UI-LIB/src/utlis/common';
enum LocationState {
INITIATING = 'INITIATING',
ENABLED = 'ENABLED',
DISABLED = 'DISABLED',
}
export enum PositionError {
PERMISSION_DENIED = 1,
POSITION_UNAVAILABLE = 2,
}
const CHECK_DEVICE_LOCATION_INTERVAL = 2 * MILLISECONDS_IN_A_SECOND;
const useIsLocationEnabled = () => {
const [geolocationPosition, setGeolocationPosition] = useState<LocationState>(
LocationState.INITIATING
);
const { isDeviceLocationEnabled } = useAppSelector((state) => state.foregroundService);
const dispatch = useAppDispatch();
useEffect(() => {
let watchId: number | null = null;
watchId = Geolocation.watchPosition(
(pos) => {
setGeolocationPosition(LocationState.ENABLED);
},
(err) => {
// When device has no location service access
const { PERMISSION_DENIED, POSITION_UNAVAILABLE } = PositionError;
if (err.code === PERMISSION_DENIED || err.code === POSITION_UNAVAILABLE) {
setGeolocationPosition(LocationState.DISABLED);
}
},
{ enableHighAccuracy: true, distanceFilter: 0, showLocationDialog: false }
);
return () => {
if (watchId) {
Geolocation.clearWatch(watchId);
const checkLocationEnabled = async () => {
try {
const isLocationEnabled = await locationEnabled();
if (!isDeviceLocationEnabled && isLocationEnabled) {
dispatch(setIsDeviceLocationEnabled(isLocationEnabled));
return;
}
};
}, []);
if (isDeviceLocationEnabled && !isLocationEnabled) {
dispatch(setIsDeviceLocationEnabled(isLocationEnabled));
return;
}
} catch (err) {
logError(err as Error, 'Error while using location module');
}
};
useEffect(() => {
if (geolocationPosition === LocationState.INITIATING) {
return;
}
if (geolocationPosition === LocationState.ENABLED && !isDeviceLocationEnabled) {
dispatch(setIsDeviceLocationEnabled(true));
} else if (geolocationPosition === LocationState.DISABLED && isDeviceLocationEnabled) {
dispatch(setIsDeviceLocationEnabled(false));
}
}, [geolocationPosition, isDeviceLocationEnabled]);
usePolling(checkLocationEnabled, CHECK_DEVICE_LOCATION_INTERVAL);
return isDeviceLocationEnabled;
};

38
src/hooks/usePolling.ts Normal file
View File

@@ -0,0 +1,38 @@
import { useEffect, useRef } from 'react';
type PollingCallback = () => void;
const usePolling = (callback: PollingCallback, interval: number): (() => void) => {
const savedCallback = useRef<PollingCallback>(callback);
const intervalIdRef = useRef<number | null>(null);
useEffect(() => {
savedCallback.current = callback;
}, [callback]);
useEffect(() => {
const poll = () => {
savedCallback.current();
};
intervalIdRef.current = setInterval(poll, interval);
return () => {
if (intervalIdRef.current) {
clearInterval(intervalIdRef.current);
intervalIdRef.current = null;
}
};
}, [interval]);
const stop = () => {
if (intervalIdRef.current) {
clearInterval(intervalIdRef.current);
intervalIdRef.current = null;
}
};
return stop;
};
export default usePolling;

View File

@@ -81,7 +81,7 @@ const getCaseListComponents = (casesList: ICaseItem[], caseDetails: Record<strin
return { pendingList, completedList, pinnedList };
};
export const getUpdatedCollectionCaseDetail = ({ caseData, answer, coords }: any) => {
export const getUpdatedCollectionCaseDetail = ({ caseData, answer, coords, templateId }: any) => {
const updatedValue = { ...caseData };
updatedValue.isSynced = false;
updatedValue.isApiCalled = false;
@@ -99,6 +99,7 @@ export const getUpdatedCollectionCaseDetail = ({ caseData, answer, coords }: any
};
updatedValue.answer = newAnswer;
updatedValue.coords = coords;
updatedValue.templateId = templateId;
return updatedValue;
};
@@ -194,6 +195,7 @@ export const getUpdatedAVCaseDetail = ({
updatedValue.caseVerdict = caseVerdict.EXHAUSTED;
}
updatedValue.coords = coords;
updatedValue.templateId = templateId;
return updatedValue;
};

View File

@@ -84,8 +84,21 @@ const Profile: React.FC = () => {
<View style={GenericStyles.fill}>
<NavigationHeader title={name} subTitle={phoneNumber} showAvatarIcon />
<ScrollView>
<View style={[GenericStyles.p16, GenericStyles.mt8]}>
<View style={[GenericStyles.row, GenericStyles.alignCenter]}>
<View
style={[
GenericStyles.ph16,
GenericStyles.pt16,
GenericStyles.whiteBackground,
numberOfCompletedCases === 2 ? { paddingBottom: 6 } : {},
]}
>
<View
style={[
GenericStyles.row,
GenericStyles.alignCenter,
numberOfCompletedCases ? { paddingBottom: 12 } : GenericStyles.pb16,
]}
>
<View style={[GenericStyles.ml4, GenericStyles.mr8]}>
<GroupIcon />
</View>
@@ -104,40 +117,39 @@ const Profile: React.FC = () => {
style={[
GenericStyles.w100,
GenericStyles.br8,
GenericStyles.mt8,
GenericStyles.mb8,
GenericStyles.mt6,
GenericStyles.mb12,
GenericStyles.whiteBackground,
]}
onPress={handleViewAllCases}
/>
) : null}
</View>
<View style={styles.logoutContainer}>
<View style={[styles.logoutContainer, GenericStyles.whiteBackground]}>
<TouchableOpacity
onPress={handleLogout}
style={[GenericStyles.row, GenericStyles.alignCenter]}
style={[GenericStyles.row, GenericStyles.centerAligned, GenericStyles.fill]}
>
<LogoutIcon />
<Text style={styles.logoutText}>Logout</Text>
</TouchableOpacity>
</View>
<Pressable
style={[
GenericStyles.row,
GenericStyles.centerAligned,
GenericStyles.fill,
GenericStyles.w100,
GenericStyles.mt8,
]}
onPress={appVersionClickHandler}
>
<Text bold dark style={styles.version}>
App Version: {getAppVersion()} Gradle Version: {VersionNumber.appVersion} Gradle Build
No: {VersionNumber.buildVersion}
</Text>
</Pressable>
</ScrollView>
<Pressable
style={[
GenericStyles.row,
GenericStyles.centerAligned,
GenericStyles.fill,
GenericStyles.w100,
styles.appDetailsText,
]}
onPress={appVersionClickHandler}
>
<Text bold dark style={styles.version}>
App Version: {getAppVersion()}
Gradle Version: {VersionNumber.appVersion}
Gradle Build No: {VersionNumber.buildVersion}
</Text>
</Pressable>
</View>
);
};
@@ -146,9 +158,9 @@ export default Profile;
const styles = StyleSheet.create({
logoutContainer: {
backgroundColor: COLORS.BACKGROUND.PRIMARY,
paddingVertical: 16,
paddingHorizontal: 34,
paddingHorizontal: 20,
marginTop: 16,
},
logoutText: {
marginLeft: 6,
@@ -156,12 +168,8 @@ const styles = StyleSheet.create({
version: {
fontSize: 10,
color: COLORS.TEXT.GREY,
paddingLeft: 10,
paddingBottom: 10,
display: 'flex',
flexDirection: 'row',
},
appDetailsText: {
position: 'absolute',
bottom: 20,
},
});

View File

@@ -56,22 +56,32 @@ const AddressContainer: React.FC<IAddressContainer> = ({ addressList }) => {
}
}}
customExpandUi={{
whenCollapsed: <Text style={addressAccordionToggleStyle}>View Details</Text>,
whenExpanded: <Text style={addressAccordionToggleStyle}>Hide Details</Text>,
whenCollapsed: address?.similarAddresses.length ? (
<Text style={addressAccordionToggleStyle}>View Details</Text>
) : (
<></>
),
whenExpanded: address?.similarAddresses.length ? (
<Text style={addressAccordionToggleStyle}>Hide Details</Text>
) : (
<></>
),
}}
>
<View>
<Text style={[styles.textContainer, styles.accordionDetailHeading]}>
Similar Addresses
</Text>
{address?.similarAddresses?.map((similarAddress: any) => (
<AddressItem
key={similarAddress?.addressDTO?.externalReferenceId}
addressItem={similarAddress?.addressDTO}
containerStyle={styles.card}
/>
))}
</View>
{address?.similarAddresses.length ? (
<View>
<Text style={[styles.textContainer, styles.accordionDetailHeading]}>
Similar Addresses
</Text>
{address?.similarAddresses?.map((similarAddress: any) => (
<AddressItem
key={similarAddress?.addressDTO?.externalReferenceId}
addressItem={similarAddress?.addressDTO}
containerStyle={styles.card}
/>
))}
</View>
) : null}
</Accordion>
))}
</View>

View File

@@ -139,6 +139,7 @@ const NewAddressContainer: React.FC<INewAddressContainer> = ({ route: routeParam
onBlur={onBlur}
placeholder="Enter here"
value={value}
required
/>
)}
name="lineOne"
@@ -156,6 +157,7 @@ const NewAddressContainer: React.FC<INewAddressContainer> = ({ route: routeParam
onBlur={onBlur}
placeholder="Enter here"
value={value}
required
/>
)}
name="lineTwo"
@@ -175,6 +177,7 @@ const NewAddressContainer: React.FC<INewAddressContainer> = ({ route: routeParam
onBlur={onBlur}
placeholder="Enter here"
value={value}
required
/>
)}
name="pinCode"
@@ -192,6 +195,7 @@ const NewAddressContainer: React.FC<INewAddressContainer> = ({ route: routeParam
onBlur={onBlur}
placeholder="Enter here"
value={value}
required
/>
)}
name="city"

View File

@@ -263,8 +263,16 @@ const styles = StyleSheet.create({
},
selectBtn: {
position: 'absolute',
top: 12,
right: 12,
paddingTop: 12,
right: 0,
paddingRight: 12,
width: 80,
height: 80,
display: 'flex',
justifyContent: 'flex-start',
alignItems: 'flex-end',
fill: 1,
zIndex: 100,
},
});

View File

@@ -267,13 +267,11 @@ export enum InteractionStatuses {
export interface IOutstandingEmiDetail {
referenceId: string;
emiAmount: number;
otherFees: number;
emiPenaltyCharges: number;
}
export interface IOutstandingAmountBreakup {
emiAmount: number;
otherFees: number;
emiPenaltyCharges: number;
totalOverdueAmount: number;
}

View File

@@ -14,6 +14,7 @@ import TrackingComponent from '../../common/TrackingComponent';
import useFCM from '../../hooks/useFCM';
import { NetworkStatusService } from '../../services/network-monitoring.service';
import BlockerScreen from '../../common/BlockerScreen';
import UnstoppableService from '../../services/foregroundServices/foreground.service';
const AuthRouter = () => {
const dispatch = useAppDispatch();
@@ -46,6 +47,14 @@ const AuthRouter = () => {
Linking.addEventListener('url', (event) => handleUrl(event.url));
}, []);
useEffect(() => {
if (!isLoggedIn) {
if (UnstoppableService.isRunning()) {
UnstoppableService.stopAll();
}
}
}, [isLoggedIn]);
// Firebase cloud messaging listener
useFCM();

View File

@@ -2,35 +2,35 @@ import { createNativeStackNavigator } from '@react-navigation/native-stack';
import React, { useEffect } from 'react';
import { useSelector } from 'react-redux';
import { _map } from '../../../RN-UI-LIB/src/utlis/common';
import { UnifiedCaseDetailsTypes, getCaseUnifiedData } from '../../action/caseApiActions';
import { getNotifications, notificationAction } from '../../action/notificationActions';
import { SCREEN_ANIMATION_DURATION } from '../../common/Constants';
import Widget from '../../components/form';
import { getScreenFocusListenerObj } from '../../components/utlis/commonFunctions';
import { getTemplateRoute } from '../../components/utlis/navigationUtlis';
import { useAppDispatch, useAppSelector } from '../../hooks';
import useFirestoreUpdates from '../../hooks/useFirestoreUpdates';
import useIsOnline from '../../hooks/useIsOnline';
import { resetNewVisitedCases } from '../../reducer/allCasesSlice';
import { RootState } from '../../store/store';
import Profile from '../Profile';
import AddNewNumber from '../addNewNumber';
import AddressGeolocation from '../addressGeolocation';
import NewAddressContainer from '../addressGeolocation/NewAddressContainer';
import AllCasesMain from '../allCases';
import CompletedCase from '../allCases/CompletedCase';
import { CaseAllocationType } from '../allCases/interface';
import CaseDetails from '../caseDetails/CaseDetails';
import CollectionCaseDetails from '../caseDetails/CollectionCaseDetail';
import interactionsHandler from '../caseDetails/interactionsHandler';
import Profile from '../Profile';
import RegisterPayments from '../registerPayements/RegisterPayments';
import TodoList from '../todoList/TodoList';
import { RootState } from '../../store/store';
import { CaseAllocationType } from '../allCases/interface';
import { getTemplateRoute } from '../../components/utlis/navigationUtlis';
import { resetNewVisitedCases } from '../../reducer/allCasesSlice';
import { getCaseUnifiedData, UnifiedCaseDetailsTypes } from '../../action/caseApiActions';
import FeedbackDetailContainer from '../caseDetails/feedback/FeedbackDetailContainer';
import EmiSchedule from '../emiSchedule';
import AddNewNumber from '../addNewNumber';
import Notifications from '../notifications';
import { getNotifications, notificationAction } from '../../action/notificationActions';
import useIsOnline from '../../hooks/useIsOnline';
import CustomerProfile from '../caseDetails/CustomerProfile';
import VKYCFullScreen from '../caseDetails/VKYCFullScreen';
import { SCREEN_ANIMATION_DURATION } from '../../common/Constants';
import { getScreenFocusListenerObj } from '../../components/utlis/commonFunctions';
import FeedbackDetailContainer from '../caseDetails/feedback/FeedbackDetailContainer';
import interactionsHandler from '../caseDetails/interactionsHandler';
import EmiSchedule from '../emiSchedule';
import ImpersonatedUser from '../impersonatedUser';
import Notifications from '../notifications';
import RegisterPayments from '../registerPayements/RegisterPayments';
import TodoList from '../todoList/TodoList';
const Stack = createNativeStackNavigator();

View File

@@ -23,7 +23,7 @@ const CollectionCaseData: React.FC<ICollectionCaseData> = ({ caseData }) => {
</Text>
)}
<View style={[GenericStyles.row, GenericStyles.mv8]}>
<Text style={[GenericStyles.chip, GenericStyles.whiteBackground, GenericStyles.mr8]} small>
<Text style={[GenericStyles.chip, GenericStyles.whiteBackground]} small>
Current DPD {currentDpd}
</Text>
{loanAccountNumber && <LANChip loanAccountNumber={loanAccountNumber} />}

View File

@@ -1,4 +1,4 @@
import React, { useEffect, useRef } from 'react';
import React, { useEffect, useRef, useState } from 'react';
import {
Animated,
Pressable,
@@ -34,13 +34,12 @@ import { _map } from '../../../RN-UI-LIB/src/utlis/common';
import FeedbackListContainer from './feedback/FeedbackListContainer';
import { RootState } from '../../store/store';
import { IFeedback, IUnSyncedFeedbackItem } from '../../types/feedback.types';
import { setFeedbackHistory } from '../../reducer/feedbackHistorySlice';
import OutstandingAmountBreakupBottomSheet from './OutstandingAmountBreakupBottomSheet';
import { getCaseUnifiedData, UnifiedCaseDetailsTypes } from '../../action/caseApiActions';
import useIsOnline from '../../hooks/useIsOnline';
import { CLICKSTREAM_EVENT_NAMES } from '../../common/Constants';
import { addClickstreamEvent } from '../../services/clickstreamEventService';
import { getLoanAccountNumber } from '../../components/utlis/commonFunctions';
import EmiBreakupBottomSheet from '../emiSchedule/EmiBreakupBottomSheet';
interface ICaseDetails {
route: {
@@ -56,15 +55,12 @@ const getOutstandingAmountBreakUp = (
) => {
const outstandingAmountBreakup: IOutstandingAmountBreakup = {
emiAmount: 0,
otherFees: 0,
emiPenaltyCharges: 0,
totalOverdueAmount: totalOverdueAmount,
};
_map(outstandingEmiDetails, (record) => {
outstandingAmountBreakup.emiAmount =
outstandingAmountBreakup.emiAmount + (record.emiAmount ? Number(record.emiAmount) : 0);
outstandingAmountBreakup.otherFees =
outstandingAmountBreakup.otherFees + (record.otherFees ? Number(record.otherFees) : 0);
outstandingAmountBreakup.emiPenaltyCharges =
outstandingAmountBreakup.emiPenaltyCharges +
(record.emiPenaltyCharges ? Number(record.emiPenaltyCharges) : 0);
@@ -143,6 +139,11 @@ const CollectionCaseDetails: React.FC<ICaseDetails> = (props) => {
const [showOutstandingAmountBottomSheet, setShowOutstandingAmountBottomSheet] =
React.useState(false);
const [showPhoneNumberSheet, setShowPhoneNumberSheet] = React.useState(false);
const {
totalOverdueAmount: outstandingTotalOverDueAmount,
emiAmount,
emiPenaltyCharges,
} = getOutstandingAmountBreakUp(caseDetail.outstandingEmiDetails, caseDetail.totalOverdueAmount);
const handleCustomerCall = () => {
addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.AV_CASE_DETAILS_CALL_CUSTOMER_CLICKED, {
@@ -279,68 +280,98 @@ const CollectionCaseDetails: React.FC<ICaseDetails> = (props) => {
styles.secondSection,
getShadowStyle(2),
GenericStyles.mt16,
GenericStyles.pv24,
GenericStyles.ph16,
]}
>
<View style={[GenericStyles.row, GenericStyles.alignCenter, GenericStyles.ph8]}>
<View>
<View style={[GenericStyles.row, GenericStyles.pb4, GenericStyles.alignCenter]}>
<Text style={[GenericStyles.redText, GenericStyles.fontSize16]}>
{formatAmount(totalOverdueAmount)}
</Text>
<Text
style={[GenericStyles.pl4, GenericStyles.redText, GenericStyles.fontSize12]}
<View style={[GenericStyles.row, GenericStyles.alignCenter, GenericStyles.relative]}>
<TouchableOpacity
style={[
GenericStyles.pv24,
{
flexBasis: '50%',
},
GenericStyles.centerAligned,
]}
activeOpacity={0.7}
onPress={() => {
addClickstreamEvent(
CLICKSTREAM_EVENT_NAMES.FA_TOTAL_OUTSTANDING_BREAKUP_CLICKED,
{ lan: loanAccountNumber }
);
setShowOutstandingAmountBottomSheet(true);
}}
>
<View style={GenericStyles.fill}>
<View
style={[
GenericStyles.row,
GenericStyles.pb4,
GenericStyles.alignCenter,
GenericStyles.justifyStart,
]}
>
Total due
</Text>
</View>
{caseDetail.outstandingEmiDetails &&
caseDetail.outstandingEmiDetails.length > 0 && (
<View style={[GenericStyles.row, GenericStyles.alignCenter]}>
<Pressable
onPress={() => {
addClickstreamEvent(
CLICKSTREAM_EVENT_NAMES.FA_TOTAL_OUTSTANDING_BREAKUP_CLICKED,
{ lan: loanAccountNumber }
);
setShowOutstandingAmountBottomSheet(true);
}}
style={[GenericStyles.row, GenericStyles.alignCenter]}
>
<Text style={[styles.emiCardCtas]}>View total overdue amt</Text>
<Text style={[GenericStyles.redText, GenericStyles.fontSize16]}>
{formatAmount(totalOverdueAmount)}
</Text>
<Text
style={[GenericStyles.pl4, GenericStyles.redText, GenericStyles.fontSize12]}
>
Total due
</Text>
</View>
{caseDetail.outstandingEmiDetails &&
caseDetail.outstandingEmiDetails.length > 0 && (
<View style={[GenericStyles.row, GenericStyles.alignCenter]}>
<Text style={[styles.emiCardCtas]}>View breakup</Text>
<Chevron fillColor={COLORS.TEXT.BLUE} />
</Pressable>
</View>
)}
</View>
<View style={styles.verticalLine} />
<View>
<View style={[GenericStyles.row, GenericStyles.pb4, GenericStyles.alignCenter]}>
<Text style={[styles.yellowText, GenericStyles.fontSize16]}>
{caseDetail?.outstandingEmiDetails?.length}
</Text>
<Text style={[GenericStyles.pl4, styles.yellowText, GenericStyles.fontSize12]}>
Overdue EMI's
</Text>
</View>
)}
</View>
<View style={[GenericStyles.row, GenericStyles.alignCenter]}>
<Pressable
onPress={() => {
addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_VIEW_EMI_SCHEDULE_CLICKED, {
lan: loanAccountNumber,
});
navigateToScreen(PageRouteEnum.EMI_SCHEDULE, { loanAccountNumber, caseId });
}}
style={[GenericStyles.row, GenericStyles.alignCenter]}
>
</TouchableOpacity>
<View style={[styles.verticalLine]} />
<TouchableOpacity
style={[
GenericStyles.pv24,
{
flexBasis: '50%',
},
GenericStyles.centerAligned,
]}
activeOpacity={0.7}
onPress={() => {
addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_VIEW_EMI_SCHEDULE_CLICKED, {
lan: loanAccountNumber,
});
navigateToScreen(PageRouteEnum.EMI_SCHEDULE, { loanAccountNumber, caseId });
}}
>
<View style={GenericStyles.fill}>
<View style={[GenericStyles.row, GenericStyles.pb4, GenericStyles.alignCenter]}>
<Text style={[styles.yellowText, GenericStyles.fontSize16]}>
{caseDetail?.outstandingEmiDetails?.length}
</Text>
<Text
style={[GenericStyles.pl4, styles.yellowText, GenericStyles.fontSize12]}
>
Overdue EMI's
</Text>
</View>
<View style={[GenericStyles.row, GenericStyles.alignCenter]}>
<Text style={[styles.emiCardCtas]}>EMI schedule</Text>
<Chevron fillColor={COLORS.TEXT.BLUE} />
</Pressable>
</View>
</View>
</View>
</TouchableOpacity>
</View>
<View style={styles.horizontalLine} />
</View>
<View
style={[
GenericStyles.whiteBackground,
styles.secondSection,
getShadowStyle(2),
GenericStyles.mt16,
]}
>
<Button
onPress={collectMoneyCta}
title="Collect money"
@@ -365,7 +396,7 @@ const CollectionCaseDetails: React.FC<ICaseDetails> = (props) => {
}
rightIcon={
<View style={styles.rightButtonIcon}>
<Chevron fillColor={COLORS.BACKGROUND.LIGHT} />
<Chevron fillColor={COLORS.TEXT.BLUE} />
</View>
}
/>
@@ -443,13 +474,12 @@ const CollectionCaseDetails: React.FC<ICaseDetails> = (props) => {
setShowPhoneNumberBottomSheet={setShowPhoneNumberSheet}
handleAddNewNumberCta={handleAddNewNumberCta}
/>
<OutstandingAmountBreakupBottomSheet
outstandingEmiDetails={getOutstandingAmountBreakUp(
caseDetail.outstandingEmiDetails,
caseDetail.totalOverdueAmount
)}
setShowOutstandingAmountBreakupBottomSheet={setShowOutstandingAmountBottomSheet}
showOutstandingAmountBreakupBottomSheet={showOutstandingAmountBottomSheet}
<EmiBreakupBottomSheet
openBottomSheet={showOutstandingAmountBottomSheet}
setOpenBottomSheet={setShowOutstandingAmountBottomSheet}
totalUnpaidEmiPenaltyCharges={emiPenaltyCharges}
totalUnpaidEmiAmount={emiAmount}
totalOverDueAmount={outstandingTotalOverDueAmount}
/>
</SafeAreaView>
</Layout>
@@ -472,9 +502,11 @@ export const styles = StyleSheet.create({
position: 'absolute',
bottom: 0,
padding: 12,
marginBottom: 8,
justifyContent: 'space-around',
alignItems: 'center',
backgroundColor: COLORS.BACKGROUND.PRIMARY,
borderTopWidth: 1,
borderTopColor: COLORS.BORDER.PRIMARY,
},
chevronContainer: {
marginLeft: 14,
@@ -511,8 +543,9 @@ export const styles = StyleSheet.create({
verticalLine: {
height: 32,
width: 1,
marginHorizontal: 24,
backgroundColor: COLORS.BORDER.PRIMARY,
position: 'absolute',
left: '50%',
},
leftButtonIcon: {
backgroundColor: COLORS.TEXT.GREEN,
@@ -524,21 +557,25 @@ export const styles = StyleSheet.create({
alignContent: 'center',
justifyContent: 'center',
position: 'relative',
marginLeft: 16,
},
rightButtonIcon: {
flex: 1,
flexDirection: 'row',
justifyContent: 'flex-end',
paddingRight: 6,
paddingRight: 22,
},
collectCtaStyle1: {
flex: 1,
width: '100%',
height: 68,
alignItems: 'center',
justifyContent: 'center',
},
collectCtaStyle2: {
flex: 1,
marginTop: 36,
width: '100%',
borderRadius: 0,
},
collectLeftStyle: {
color: COLORS.TEXT.WHITE,

View File

@@ -154,10 +154,6 @@ const CustomerProfile: React.FC<ICustomerProfile> = (props) => {
<Text style={[styles.errorText]} bold>
Error loading image
</Text>
) : errorModalImage ? (
<Text style={[styles.errorText]} bold>
Error loading image
</Text>
) : null}
</View>
</View>

View File

@@ -28,19 +28,25 @@ const LANChip: React.FC<ILANChip> = ({ loanAccountNumber }) => {
});
}
};
const isClipped = loanAccountNumber?.length >= 13;
const clippedStyle = isClipped ? GenericStyles.fill : {};
return (
<TouchableHighlight style={[GenericStyles.ml8]} onPress={copyLAN}>
<TouchableHighlight style={[GenericStyles.ml8, clippedStyle]} onPress={copyLAN}>
<View
style={[
GenericStyles.chip,
GenericStyles.whiteBackground,
GenericStyles.row,
GenericStyles.alignCenter,
GenericStyles.centerAligned,
]}
>
<Text small>LAN {loanAccountNumber}</Text>
<View style={[GenericStyles.ml4]}>
<View style={clippedStyle}>
<Text numberOfLines={1} ellipsizeMode={isClipped ? 'tail' : undefined} small>
LAN {loanAccountNumber}
</Text>
</View>
<View style={isClipped ? {} : GenericStyles.ml4}>
<CopyIcon />
</View>
</View>

View File

@@ -1,115 +0,0 @@
import * as React from 'react';
import { IOutstandingAmountBreakup } from '../allCases/interface';
import { StyleSheet, TouchableOpacity, View } from 'react-native';
import { GenericStyles } from '../../../RN-UI-LIB/src/styles';
import Heading from '../../../RN-UI-LIB/src/components/Heading';
import CloseIcon from '../../../RN-UI-LIB/src/Icons/CloseIcon';
import { COLORS } from '../../../RN-UI-LIB/src/styles/colors';
import BottomSheet from '../../../RN-UI-LIB/src/components/bottom_sheet/BottomSheet';
import Text from '../../../RN-UI-LIB/src/components/Text';
import { formatAmount } from '../../../RN-UI-LIB/src/utlis/amount';
interface IOutstandingAmountBreakupProps {
outstandingEmiDetails: IOutstandingAmountBreakup;
setShowOutstandingAmountBreakupBottomSheet: React.Dispatch<React.SetStateAction<boolean>>;
showOutstandingAmountBreakupBottomSheet: boolean;
}
const OutstandingAmountBreakupBottomSheet: React.FC<IOutstandingAmountBreakupProps> = ({
outstandingEmiDetails,
setShowOutstandingAmountBreakupBottomSheet,
showOutstandingAmountBreakupBottomSheet,
}) => {
return (
<BottomSheet
heightPercentage={40}
visible={showOutstandingAmountBreakupBottomSheet}
HeaderNode={() => (
<View
style={[
GenericStyles.row,
GenericStyles.alignCenter,
GenericStyles.justifyContentSpaceBetween,
GenericStyles.p16,
]}
>
<Heading dark type="h4">
Total outstanding amount breakup
</Heading>
<TouchableOpacity
activeOpacity={0.7}
onPress={() => setShowOutstandingAmountBreakupBottomSheet((prev) => !prev)}
>
<CloseIcon color={COLORS.TEXT.LIGHT} />
</TouchableOpacity>
</View>
)}
setVisible={() => setShowOutstandingAmountBreakupBottomSheet((prev) => !prev)}
>
<View style={[styles.container]}>
<View
style={[
GenericStyles.row,
GenericStyles.fill,
GenericStyles.spaceBetween,
GenericStyles.pb16,
]}
>
<Text light>{`EMI Amount`}</Text>
<Text dark>{formatAmount(outstandingEmiDetails.emiAmount)}</Text>
</View>
<View
style={[
GenericStyles.row,
GenericStyles.fill,
GenericStyles.spaceBetween,
GenericStyles.pb16,
]}
>
<Text light>{`EMI Penalty Charges`}</Text>
<Text dark>{formatAmount(outstandingEmiDetails.emiPenaltyCharges)}</Text>
</View>
<View
style={[
GenericStyles.row,
GenericStyles.fill,
GenericStyles.spaceBetween,
GenericStyles.pb16,
]}
>
<Text light>{`Other Fees`}</Text>
<Text dark>{formatAmount(outstandingEmiDetails.otherFees)}</Text>
</View>
<View style={[styles.horizontalLine]} />
<View
style={[
GenericStyles.row,
GenericStyles.fill,
GenericStyles.spaceBetween,
GenericStyles.pt16,
]}
>
<Text dark>Total Overdue Amount</Text>
<Text style={[GenericStyles.redText]}>
{formatAmount(outstandingEmiDetails.totalOverdueAmount)}
</Text>
</View>
</View>
</BottomSheet>
);
};
const styles = StyleSheet.create({
container: {
paddingHorizontal: 16,
paddingBottom: 30,
flex: 1,
},
horizontalLine: {
backgroundColor: COLORS.TEXT.LIGHT,
width: '100%',
height: 0.5,
},
});
export default OutstandingAmountBreakupBottomSheet;

View File

@@ -166,6 +166,7 @@ const UserDetailsSection: React.FC<IUserDetailsSection> = (props) => {
const styles = StyleSheet.create({
infoContainer: {
marginLeft: 16,
flex: 1,
},
imageStyle: {
height: 500,

View File

@@ -1,17 +1,14 @@
import React, { useEffect } from 'react';
import { ImageBackground, StyleSheet, View } from 'react-native';
import { StyleSheet, View } from 'react-native';
import Text from '../../../../RN-UI-LIB/src/components/Text';
import { GenericStyles } from '../../../../RN-UI-LIB/src/styles';
import { COLORS } from '../../../../RN-UI-LIB/src/styles/colors';
import {
BUSINESS_DATE_FORMAT,
BUSINESS_TIME_FORMAT,
dateFormat,
} from '../../../../RN-UI-LIB/src/utlis/dates';
import { BUSINESS_DATE_FORMAT, dateFormat } from '../../../../RN-UI-LIB/src/utlis/dates';
import { sanitizeString } from '../../../components/utlis/commonFunctions';
import { AnswerType, IAnswerView, OPTION_TAG } from '../../../types/feedback.types';
import { addClickstreamEvent } from '../../../services/clickstreamEventService';
import { CLICKSTREAM_EVENT_NAMES } from '../../../common/Constants';
import FeedbackDetailImageItem from './FeedbackDetailImageItem';
const getAnswerText = (answer: IAnswerView) => {
switch (answer?.answerType) {
@@ -34,6 +31,9 @@ interface IFeedbackDetailAnswerContainer {
loanAccountNumber: string;
activeFeedbackReferenceId: string;
}
export const getQuestionText = (answerItem: IAnswerView) => {
return (answerItem.questionKey ? answerItem.questionName : answerItem.questionText) || '--';
};
const FeedbackDetailAnswerContainer: React.FC<IFeedbackDetailAnswerContainer> = ({
answerList,
@@ -44,10 +44,6 @@ const FeedbackDetailAnswerContainer: React.FC<IFeedbackDetailAnswerContainer> =
(answer) => answer.questionTag === OPTION_TAG.IMAGE_UPLOAD
);
const getQuestionText = (answerItem: IAnswerView) => {
return (answerItem.questionKey ? answerItem.questionName : answerItem.questionText) || '--';
};
useEffect(() => {
addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_CASE_DETAILS_INDIVIDUAL_FEEDBACK_CLICKED, {
lan: loanAccountNumber,
@@ -58,16 +54,7 @@ const FeedbackDetailAnswerContainer: React.FC<IFeedbackDetailAnswerContainer> =
return (
<View style={[styles.container]}>
{imageAnswerList.map((image) => (
<View style={[GenericStyles.columnDirection, GenericStyles.mv8]}>
<Text style={[styles.textContainer, styles.questionText]}>{getQuestionText(image)}</Text>
<ImageBackground
style={[styles.image, GenericStyles.mt8]}
imageStyle={styles.br8}
source={{
uri: image.inputText,
}}
/>
</View>
<FeedbackDetailImageItem image={image} />
))}
{answerList
.filter((answer) => answer.questionTag !== OPTION_TAG.IMAGE_UPLOAD)
@@ -86,10 +73,6 @@ const FeedbackDetailAnswerContainer: React.FC<IFeedbackDetailAnswerContainer> =
};
const styles = StyleSheet.create({
container: {
padding: 16,
backgroundColor: COLORS.TEXT.GREY_LIGHT,
},
textContainer: {
fontSize: 12,
lineHeight: 18,
@@ -99,17 +82,13 @@ const styles = StyleSheet.create({
questionText: {
color: COLORS.TEXT.LIGHT,
},
container: {
padding: 16,
backgroundColor: COLORS.TEXT.GREY_LIGHT,
},
answerText: {
color: COLORS.TEXT.DARK,
},
br8: {
borderRadius: 8,
},
image: {
width: '100%',
height: 350,
resizeMode: 'contain',
},
});
export default FeedbackDetailAnswerContainer;

View File

@@ -1,10 +1,9 @@
import React, { useEffect, useState } from 'react';
import { Dimensions, RefreshControl, ScrollView, StyleSheet, View } from 'react-native';
import { RefreshControl, ScrollView, StyleSheet, View } from 'react-native';
import Accordion from '../../../../RN-UI-LIB/src/components/accordian/Accordian';
import NavigationHeader from '../../../../RN-UI-LIB/src/components/NavigationHeader';
import Pagination from '../../../../RN-UI-LIB/src/components/Pagination';
import Text from '../../../../RN-UI-LIB/src/components/Text';
import { GenericStyles, SCREEN_HEIGHT, getShadowStyle } from '../../../../RN-UI-LIB/src/styles';
import { GenericStyles, getShadowStyle } from '../../../../RN-UI-LIB/src/styles';
import { COLORS } from '../../../../RN-UI-LIB/src/styles/colors';
import { getPastFeedbacks } from '../../../action/feedbackActions';
import { GenericType } from '../../../common/GenericTypes';
@@ -18,6 +17,10 @@ import FeedbackDetailAnswerContainer from './FeedbackDetailAnswerContainer';
import FeedbackDetailItem from './FeedbackDetailItem';
import { addClickstreamEvent } from '../../../services/clickstreamEventService';
import { CLICKSTREAM_EVENT_NAMES } from '../../../common/Constants';
import Pagination from '../../../../RN-UI-LIB/src/components/pagination/Pagination';
import Layout from '../../layout/Layout';
import { toast } from '../../../../RN-UI-LIB/src/components/toast';
import { ToastMessages } from './../../../screens/allCases/constants';
const PAGE_TITLE = 'All feedbacks';
const SCROLL_LAYOUT_OFFSET = 10;
@@ -103,70 +106,101 @@ const FeedbackDetailContainer: React.FC<IFeedbackDetailContainer> = ({ route: ro
});
}, [ref, dataSourceCord]);
const handlePageChange = (page: number) => {
if (!isOnline) {
toast({ type: 'info', text1: ToastMessages.OFFLINE_MESSAGE });
return;
}
setCurrentPage(page);
};
const handlePageChangeClickstream = (
page: number,
eventName: { name: string; description: string }
) => {
addClickstreamEvent(eventName, {
currentPageNumber: page,
lan: loanAccountNumber,
});
};
return (
<View style={[GenericStyles.fill, GenericStyles.whiteBackground]}>
<NavigationHeader title={PAGE_TITLE} onBack={goBack} />
<ScrollView
refreshControl={<RefreshControl refreshing={loading} />}
style={[GenericStyles.ph16, GenericStyles.mt16]}
ref={(x) => setRef(x)}
>
{feedbackList.map((feedback: IFeedback, index) => (
<View
key={index}
onLayout={(event) => {
const layout = event.nativeEvent.layout;
if (!dataSourceCord && feedback.referenceId === activeFeedbackReferenceId) {
setDataSourceCord(layout.y + SCROLL_LAYOUT_OFFSET);
}
}}
>
<Accordion
accordionStyle={[GenericStyles.pv12, GenericStyles.br8, getShadowStyle(4)]}
isActive={feedback.referenceId === activeFeedbackReferenceId}
accordionHeader={
<FeedbackDetailItem
key={feedback.referenceId}
feedbackItem={feedback}
isExpanded={isExpanded}
/>
}
customExpandUi={{
whenCollapsed: <Text style={styles.accordionExpandBtn}>View more</Text>,
whenExpanded: <Text style={styles.accordionExpandBtn}>View less</Text>,
}}
onExpanded={(value) => {
setIsExpanded(value);
<Layout>
<View style={[GenericStyles.fill, GenericStyles.whiteBackground]}>
<NavigationHeader title={PAGE_TITLE} onBack={goBack} />
<ScrollView
refreshControl={<RefreshControl refreshing={loading} />}
style={[GenericStyles.ph16, GenericStyles.mt16]}
ref={(x) => setRef(x)}
>
{feedbackList.map((feedback: IFeedback, index) => (
<View
key={index}
onLayout={(event) => {
const layout = event.nativeEvent.layout;
if (!dataSourceCord && feedback.referenceId === activeFeedbackReferenceId) {
setDataSourceCord(layout.y + SCROLL_LAYOUT_OFFSET);
}
}}
>
<FeedbackDetailAnswerContainer
answerList={feedback.answerViews}
activeFeedbackReferenceId={feedback.referenceId}
loanAccountNumber={loanAccountNumber}
/>
</Accordion>
</View>
))}
</ScrollView>
<Pagination
onNext={() => {
addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_VIEW_PAST_FEEDBACK_NEXT_PAGE_CLICKED, {
currentPageNumber: currentPage,
lan: loanAccountNumber,
});
setCurrentPage((page) => page + 1);
}}
onPrev={() => {
addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_VIEW_PAST_FEEDBACK_PREV_PAGE_CLICKED, {
currentPageNumber: currentPage,
lan: loanAccountNumber,
});
setCurrentPage((page) => page - 1);
}}
currentPage={currentPage}
totalPages={feedbackTotalPages}
/>
</View>
<Accordion
accordionStyle={[GenericStyles.pv12, GenericStyles.br8, getShadowStyle(4)]}
isActive={feedback.referenceId === activeFeedbackReferenceId}
accordionHeader={
<FeedbackDetailItem
key={feedback.referenceId}
feedbackItem={feedback}
isExpanded={isExpanded}
/>
}
customExpandUi={{
whenCollapsed: <Text style={styles.accordionExpandBtn}>View more</Text>,
whenExpanded: <Text style={styles.accordionExpandBtn}>View less</Text>,
}}
onExpanded={(value) => {
setIsExpanded(value);
}}
>
<FeedbackDetailAnswerContainer
answerList={feedback.answerViews}
activeFeedbackReferenceId={feedback.referenceId}
loanAccountNumber={loanAccountNumber}
/>
</Accordion>
</View>
))}
</ScrollView>
<Pagination
onPageChange={handlePageChange}
currentPage={currentPage}
totalPages={feedbackTotalPages}
onNextPage={(page) =>
handlePageChangeClickstream(
page,
CLICKSTREAM_EVENT_NAMES.FA_VIEW_PAST_FEEDBACK_NEXT_PAGE_CLICKED
)
}
onPrevPage={(page) =>
handlePageChangeClickstream(
page,
CLICKSTREAM_EVENT_NAMES.FA_VIEW_PAST_FEEDBACK_PREV_PAGE_CLICKED
)
}
onFirstPage={(page) =>
handlePageChangeClickstream(
page,
CLICKSTREAM_EVENT_NAMES.FA_VIEW_PAST_FEEDBACK_FIRST_PAGE_CLICKED
)
}
onLastPage={(page) =>
handlePageChangeClickstream(
page,
CLICKSTREAM_EVENT_NAMES.FA_VIEW_PAST_FEEDBACK_LAST_PAGE_CLICKED
)
}
/>
</View>
</Layout>
);
};

View File

@@ -0,0 +1,83 @@
import { Image, Modal, StyleSheet, TouchableOpacity, View } from 'react-native';
import React, { useState } from 'react';
import Text from '../../../../RN-UI-LIB/src/components/Text';
import { GenericStyles } from '../../../../RN-UI-LIB/src/styles';
import IconButton from '../../../../RN-UI-LIB/src/components/IconButton';
import ExpandIcon from '../../../../RN-UI-LIB/src/Icons/ExpandIcon';
import { IAnswerView } from '../../../types/feedback.types';
import { COLORS } from '../../../../RN-UI-LIB/src/styles/colors';
import { getQuestionText } from './FeedbackDetailAnswerContainer';
import ExpandableImage from '../../../components/expandableImage/ExpandableImage';
interface IFeedbackDetailImageItem {
image: IAnswerView;
}
const IMAGE_HEIGHT = 131;
const IMAGE_WIDTH = 105;
const FeedbackDetailImageItem: React.FC<IFeedbackDetailImageItem> = ({ image }) => {
const [isImageExpanded, setIsImageExpanded] = useState(false);
const handleExpandImage = () => {
setIsImageExpanded(true);
};
const handleExpandedImageClose = () => {
setIsImageExpanded(false);
};
const questionText = getQuestionText(image);
return (
<View style={[GenericStyles.mv8]}>
<Text style={[styles.textContainer, styles.questionText]}>{questionText}</Text>
<TouchableOpacity activeOpacity={1} onPress={handleExpandImage}>
<Image
style={[styles.image, GenericStyles.mt8, GenericStyles.br8]}
source={{
uri: image.inputText,
}}
/>
<IconButton
icon={<ExpandIcon />}
round
size={28}
style={styles.expandIcon}
onPress={handleExpandImage}
/>
</TouchableOpacity>
<Modal visible={isImageExpanded} transparent animationType="fade">
<ExpandableImage
imageSrc={image.inputText}
title={questionText}
close={handleExpandedImageClose}
/>
</Modal>
</View>
);
};
const styles = StyleSheet.create({
textContainer: {
fontSize: 12,
lineHeight: 18,
fontWeight: '400',
width: '100%',
},
questionText: {
color: COLORS.TEXT.LIGHT,
},
image: {
height: IMAGE_HEIGHT,
width: IMAGE_WIDTH,
},
expandIcon: {
position: 'absolute',
bottom: 8,
left: IMAGE_WIDTH - 36,
backgroundColor: COLORS.BACKGROUND.PRIMARY,
},
});
export default FeedbackDetailImageItem;

View File

@@ -74,7 +74,7 @@ const PhoneNumberSelectionBottomSheet: React.FC<PhoneNumberSelectionBottomSheetP
);
})}
</ScrollView>
<ToastContainer config={toastConfigs} position="bottom" />
<ToastContainer config={toastConfigs} position="top" />
</BottomSheet>
);
};

View File

@@ -10,23 +10,36 @@ import Text from '../../../RN-UI-LIB/src/components/Text';
import { IEmiItem } from './EmiScheduleItem';
import { formatAmount } from '../../../RN-UI-LIB/src/utlis/amount';
import { getNumberWithRankSuffix } from '../../../RN-UI-LIB/src/utlis/common';
import { getDynamicBottomSheetHeightPercentageFn } from '../../components/utlis/commonFunctions';
interface IEmiBreakupBottomSheet {
openBottomSheet: boolean;
setOpenBottomSheet: React.Dispatch<React.SetStateAction<boolean>>;
emiItem: IEmiItem;
listNumber: number;
listNumber?: number;
totalUnpaidEmiAmount: number;
totalUnpaidEmiPenaltyCharges: number;
totalOverDueAmount: number;
}
const EmiBreakupBottomSheet: React.FC<IEmiBreakupBottomSheet> = (props) => {
const { openBottomSheet, setOpenBottomSheet, emiItem, listNumber } = props;
const {
openBottomSheet,
setOpenBottomSheet,
listNumber,
totalUnpaidEmiPenaltyCharges,
totalOverDueAmount,
totalUnpaidEmiAmount,
} = props;
const height = getDynamicBottomSheetHeightPercentageFn();
return (
<BottomSheet
heightPercentage={40}
heightPercentage={height(3)}
visible={openBottomSheet}
HeaderNode={() => (
<View style={[...row, GenericStyles.p16]}>
<Heading dark type="h4">
{getNumberWithRankSuffix(listNumber)} EMI breakup
{listNumber
? `${getNumberWithRankSuffix(listNumber)} EMI breakup`
: 'Total due breakup'}
</Heading>
<TouchableOpacity activeOpacity={0.7} onPress={() => setOpenBottomSheet((prev) => !prev)}>
<CloseIcon color={COLORS.TEXT.LIGHT} />
@@ -37,15 +50,13 @@ const EmiBreakupBottomSheet: React.FC<IEmiBreakupBottomSheet> = (props) => {
>
<View style={[GenericStyles.p16]}>
<View style={[...row, GenericStyles.pb16]}>
<Text light>EMI</Text>
<Text dark>{formatAmount(emiItem?.totalUnpaidEmiAmount ?? 0)}</Text>
<Text light>EMI amount</Text>
<Text dark>{formatAmount(totalUnpaidEmiAmount)}</Text>
</View>
<View style={[...row, GenericStyles.pb16]}>
<Text light>EMI Penalty charges</Text>
<Text light>EMI penalty charges</Text>
<Text dark bold>
{formatAmount(
emiItem?.totalUnpaidEmiPenaltyCharges ?? emiItem?.totalUnpaidOtherFees ?? 0
)}
{formatAmount(totalUnpaidEmiPenaltyCharges)}
</Text>
</View>
<View style={styles.horizontalLine} />
@@ -53,9 +64,7 @@ const EmiBreakupBottomSheet: React.FC<IEmiBreakupBottomSheet> = (props) => {
<Text dark bold>
Total Overdue Amount
</Text>
<Text style={{ color: COLORS.TEXT.RED }}>
{formatAmount(emiItem?.totalOverDueAmount ?? 0)}
</Text>
<Text style={{ color: COLORS.TEXT.RED }}>{formatAmount(totalOverDueAmount)}</Text>
</View>
</View>
</BottomSheet>

View File

@@ -30,13 +30,19 @@ interface IEmiScheduleItem {
listNumber: number;
emiItem: IEmiItem;
selectedTab: EmiSelectedTab;
isLast: boolean;
}
const EmiScheduleItem: React.FC<IEmiScheduleItem> = ({ listNumber, emiItem, selectedTab }) => {
const EmiScheduleItem: React.FC<IEmiScheduleItem> = ({
listNumber,
emiItem,
selectedTab,
isLast,
}) => {
const [openBottomSheet, setOpenBottomSheet] = React.useState(false);
const { totalOverDueAmount, totalUnpaidEmiAmount, totalUnpaidEmiPenaltyCharges } = emiItem;
return (
<View style={styles.container}>
<View style={[styles.container, isLast ? GenericStyles.mb24 : {}]}>
<View style={styles.leftContainer}>
<Text dark style={[GenericStyles.pl12, GenericStyles.fontSize12]}>
{getNumberWithRankSuffix(listNumber)}
@@ -105,7 +111,9 @@ const EmiScheduleItem: React.FC<IEmiScheduleItem> = ({ listNumber, emiItem, sele
</View>
</View>
<EmiBreakupBottomSheet
emiItem={emiItem}
totalOverDueAmount={totalOverDueAmount ?? 0}
totalUnpaidEmiAmount={totalUnpaidEmiAmount ?? 0}
totalUnpaidEmiPenaltyCharges={totalUnpaidEmiPenaltyCharges ?? 0}
openBottomSheet={openBottomSheet}
setOpenBottomSheet={setOpenBottomSheet}
listNumber={listNumber}

View File

@@ -8,13 +8,25 @@ import EmptyPaidListIcon from '../../assets/icons/EmptyPaidListIcon';
import EmptyUnpaidListIcon from '../../assets/icons/EmptyUnpaidListIcon';
import { useAppSelector } from '../../hooks';
import { getFilteredData } from './utils';
import { _map } from '../../../RN-UI-LIB/src/utlis/common';
import LineLoader from '../../../RN-UI-LIB/src/components/suspense_loader/LineLoader';
import SuspenseLoader from '../../../RN-UI-LIB/src/components/suspense_loader/SuspenseLoader';
import EmptyScheduledListIcon from '../../assets/icons/EmptyScheduledListIcon';
export enum EmiSelectedTab {
UNPAID = 'UNPAID',
PAID = 'PAID',
SCHEDULED = 'SCHEDULED',
ALL = 'ALL',
}
const emiTypes = {
[EmiSelectedTab.UNPAID]: 'Unpaid',
[EmiSelectedTab.PAID]: 'Paid',
[EmiSelectedTab.SCHEDULED]: 'Scheduled',
[EmiSelectedTab.ALL]: 'All',
};
const pressableChipStyle: StyleProp<ViewStyle> = {
marginTop: 0,
};
@@ -25,7 +37,8 @@ interface IEmiScheduleTab {
const EmiScheduleTab: React.FC<IEmiScheduleTab> = (props) => {
const { loanAccountNumber } = props;
const [selectedTab, setSelectedTab] = useState<EmiSelectedTab>(EmiSelectedTab.UNPAID);
const emiData = useAppSelector((state) => state.emiSchedule?.[loanAccountNumber]?.data);
const emiEcheduleData = useAppSelector((state) => state.emiSchedule?.[loanAccountNumber]) || {};
const { data: emiData, isLoading } = emiEcheduleData;
const filteredData =
selectedTab !== EmiSelectedTab.ALL ? getFilteredData(emiData, selectedTab) : emiData;
@@ -36,60 +49,74 @@ const EmiScheduleTab: React.FC<IEmiScheduleTab> = (props) => {
}}
>
<View style={[GenericStyles.row, GenericStyles.alignCenter, GenericStyles.mb8]}>
<PressableChip
onSelectionChange={() => setSelectedTab(EmiSelectedTab.UNPAID)}
checked={selectedTab === EmiSelectedTab.UNPAID}
label="Unpaid"
key={1}
containerStyles={pressableChipStyle}
/>
<PressableChip
onSelectionChange={() => setSelectedTab(EmiSelectedTab.PAID)}
checked={selectedTab === EmiSelectedTab.PAID}
label="Paid"
key={2}
containerStyles={pressableChipStyle}
/>
<PressableChip
onSelectionChange={() => setSelectedTab(EmiSelectedTab.ALL)}
checked={selectedTab === EmiSelectedTab.ALL}
label="All"
key={3}
containerStyles={pressableChipStyle}
/>
{/* @ts-ignore */}
{_map(emiTypes, (tab: EmiSelectedTab) => (
<PressableChip
onSelectionChange={() => setSelectedTab(tab)}
checked={selectedTab === tab}
label={emiTypes[tab]}
key={tab}
containerStyles={pressableChipStyle}
disabled={isLoading}
/>
))}
</View>
{filteredData?.length > 0 ? (
<FlatList
data={filteredData}
renderItem={({ item, index }) => (
<EmiScheduleItem
emiItem={item}
listNumber={item?.rank ?? filteredData.length - index}
key={item.referenceId}
selectedTab={selectedTab}
/>
)}
/>
) : (
<View style={[GenericStyles.alignCenter, GenericStyles.mt16]}>
{selectedTab === EmiSelectedTab.PAID ? (
<>
<EmptyPaidListIcon />
<Text style={[GenericStyles.mt16]} light>
Customer has not paid any EMIs yet
</Text>
</>
) : null}
{selectedTab === EmiSelectedTab.UNPAID ? (
<>
<EmptyUnpaidListIcon />
<Text style={[GenericStyles.mt16]} light>
Customer has paid all due EMIs
</Text>
</>
) : null}
</View>
)}
<SuspenseLoader
loading={isLoading}
fallBack={
<>
{[...Array(5).keys()].map(() => (
<LineLoader
width={'100%'}
height={60}
style={[GenericStyles.br6, { marginBottom: 20 }]}
/>
))}
</>
}
>
{filteredData?.length > 0 ? (
<FlatList
data={filteredData}
renderItem={({ item, index }) => (
<EmiScheduleItem
emiItem={item}
isLast={index === filteredData.length - 1}
listNumber={item?.rank ?? filteredData.length - index}
key={item.referenceId}
selectedTab={selectedTab}
/>
)}
/>
) : (
<View style={[GenericStyles.alignCenter, GenericStyles.mt16]}>
{selectedTab === EmiSelectedTab.PAID ? (
<>
<EmptyPaidListIcon />
<Text style={[GenericStyles.mt16]} light>
Customer has not paid any EMIs yet
</Text>
</>
) : null}
{selectedTab === EmiSelectedTab.UNPAID ? (
<>
<EmptyUnpaidListIcon />
<Text style={[GenericStyles.mt16]} light>
Customer has paid all due EMIs
</Text>
</>
) : null}
{selectedTab === EmiSelectedTab.SCHEDULED ? (
<>
<EmptyScheduledListIcon />
<Text style={[GenericStyles.mt16]} light>
No scheduled EMIs
</Text>
</>
) : null}
</View>
)}
</SuspenseLoader>
</View>
);
};

View File

@@ -3,48 +3,58 @@ import { StyleSheet, View } from 'react-native';
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 RepaymentsUnpaidIcon from '../../../assets/icons/RepaymentsUnpaidIcon';
import RepaymentsPaidIcon from '../../../assets/icons/RepaymentsPaidIcon';
import Tag from '../../../../RN-UI-LIB/src/components/Tag';
import { IRepaymentsRecord, REPAYMENT_STATUS } from '../../../types/repayments.types';
import { row } from '../constants';
import { BUSINESS_DATE_FORMAT, dateFormat } from '../../../../RN-UI-LIB/src/utlis/dates';
import {
IRepaymentsRecord,
REPAYMENT_STATUS,
RepaymentStatusTagging,
} from '../../../types/repayments.types';
import {
BUSINESS_DATE_FORMAT,
BUSINESS_TIME_FORMAT,
dateFormat,
} from '../../../../RN-UI-LIB/src/utlis/dates';
import InfoIcon from '../../../../RN-UI-LIB/src/Icons/InfoIcon';
interface RepaymentsItemProps {
repaymentRecord: IRepaymentsRecord;
}
const StatusIcon: React.FC<{ status: REPAYMENT_STATUS }> = ({ status }) => {
switch (status) {
case REPAYMENT_STATUS.FAILED:
return <RepaymentsUnpaidIcon />;
case REPAYMENT_STATUS.SUCCESS:
return <RepaymentsPaidIcon />;
default:
return <></>;
}
};
const RepaymentsItem: React.FC<RepaymentsItemProps> = ({ repaymentRecord }) => {
const repaymentTag = RepaymentStatusTagging[repaymentRecord.status];
return (
<View style={styles.container}>
<View style={styles.leftContainer}>
<StatusIcon status={repaymentRecord.status} />
{repaymentRecord.status === REPAYMENT_STATUS.SUCCESS ? (
<RepaymentsPaidIcon />
) : repaymentTag ? (
<InfoIcon color={repaymentTag.iconColor} />
) : null}
</View>
<View style={[GenericStyles.fill]}>
<View style={[GenericStyles.row, GenericStyles.fill]}>
<View style={[GenericStyles.row, GenericStyles.fill]}>
<Text style={[styles.repaymentsText]}> {repaymentRecord?.amount}</Text>
{repaymentRecord.status === REPAYMENT_STATUS.FAILED && (
<Tag text="Failed" variant="error" />
)}
{repaymentTag ? <Tag text={repaymentTag.label} variant={repaymentTag.variant} /> : null}
</View>
<Text>{dateFormat(new Date(repaymentRecord.valueDate ?? ''), BUSINESS_DATE_FORMAT)}</Text>
<Text>
{dateFormat(
new Date(repaymentRecord.orderCompletionTimestamp ?? ''),
BUSINESS_DATE_FORMAT
)}
</Text>
</View>
<View style={[GenericStyles.row]}>
<View style={[GenericStyles.row, GenericStyles.justifyContentSpaceBetween]}>
<Text style={[GenericStyles.pt7]} light>
{repaymentRecord.repaymentMode}
</Text>
<Text style={[GenericStyles.pt7, styles.timeStamp]}>
{dateFormat(
new Date(repaymentRecord.orderCompletionTimestamp ?? ''),
BUSINESS_TIME_FORMAT
)}
</Text>
</View>
</View>
</View>
@@ -66,6 +76,9 @@ const styles = StyleSheet.create({
flexBasis: '8%',
},
repaymentsText: { marginRight: 5, lineHeight: 19 },
timeStamp: {
color: '#C9C9C9',
},
});
export default RepaymentsItem;

View File

@@ -20,13 +20,15 @@ const OtpText: React.FC<IOtpText> = ({ resetOtp }) => {
const [countDownTimeLeft, setCountDownTimeLeft] = useState<number>(RESEND_OTP_TIME);
const dispatch = useAppDispatch();
const { phoneNumber, verifyOTPError } = useSelector((state: RootState) => state.loginInfo);
const { phoneNumber, verifyOTPError, otpToken } = useSelector(
(state: RootState) => state.loginInfo
);
const handleResendOTP = () => {
setCountDownComplete(false);
setCountDownTimeLeft(RESEND_OTP_TIME);
dispatch(resetVerifyOTPError());
dispatch(generateOTP({ phoneNumber }, true));
dispatch(generateOTP({ phoneNumber, otpToken }, true));
resetOtp();
};

View File

@@ -39,6 +39,8 @@ export enum GoogleSignInError {
GoogleSignin.configure({
webClientId: GOOGLE_SSO_CLIENT_ID,
forceCodeForRefreshToken: true,
offlineAccess: true,
});
function Login() {
@@ -49,9 +51,9 @@ function Login() {
formState: { isValid },
} = useForm<ILoginForm>();
const dispatch = useAppDispatch();
const { phoneNumber, OTPError, isLoading, deviceId } = useSelector((state: RootState) => ({
deviceId: state.user.deviceId,
const { phoneNumber, OTPError, isLoading, otpToken } = useSelector((state: RootState) => ({
phoneNumber: state.loginInfo.phoneNumber,
otpToken: state.loginInfo.otpToken,
OTPError: state.loginInfo.OTPError,
isLoading: state.loginInfo.isLoading,
}));
@@ -68,20 +70,24 @@ function Login() {
}, []);
const handleGenerateOTP = (data: GenerateOTPPayload) => {
let payload = {
...data,
otpToken: data.phoneNumber === phoneNumber ? otpToken : undefined,
};
addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.AV_LOGIN_SCREEN_SEND_OTP_CLICKED);
dispatch(generateOTP(data));
dispatch(generateOTP(payload));
};
const onPressGoogle = async () => {
try {
await GoogleSignin.hasPlayServices();
const userInfo: GoogleSigninUser = await GoogleSignin.signIn();
if (userInfo?.idToken) {
if (userInfo?.serverAuthCode) {
toast({
text1: ToastMessages.FETCHING_USER_DATA,
type: 'success',
});
await dispatch(verifyGoogleSignIn(userInfo.idToken));
await dispatch(verifyGoogleSignIn(userInfo.serverAuthCode));
return;
}
throw userInfo;

View File

@@ -64,13 +64,7 @@ const Notifications = () => {
addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_NOTIFICATIONS_LOADED, {
count: data?.length,
});
if (data?.length) {
return;
}
if (!isOnline) {
return;
}
dispatch(getNotifications());
onRefresh();
}, []);
const notificationsList: INotification[] = useMemo(() => {

View File

@@ -23,7 +23,7 @@ const DropdownItem: React.FC<IDropdownItem> = ({
sourceText,
}) => {
const pressHandler = () => {
handleSelection && handleSelection(label);
handleSelection && handleSelection(id);
close && close();
};
return (

View File

@@ -14,6 +14,7 @@ import { toast } from '../../../RN-UI-LIB/src/components/toast';
import {
copyToClipboard,
getDynamicBottomSheetHeightPercentageFn,
getPhoneNumberString,
} from '../../components/utlis/commonFunctions';
import useIsOnline from '../../hooks/useIsOnline';
import { RootState } from '../../store/store';
@@ -21,12 +22,13 @@ import { generatePaymentLinkAction } from '../../action/paymentActions';
import { useAppDispatch, useAppSelector } from '../../hooks';
import DropdownItem from './DropdownItem';
import { PhoneNumber } from '../caseDetails/interface';
import { setPaymentLink } from '../../reducer/paymentSlice';
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';
interface IRegisterForm {
selectedPhoneNumber: string;
@@ -46,14 +48,12 @@ interface IRegisterPayments {
const HEADER_HEIGHT = 100;
const ROW_HEIGHT = 40;
const PAGE_TITLE = 'Collect Money';
const RegisterPayments: React.FC<IRegisterPayments> = ({ route }) => {
let {
params: { caseId, numbers, amount, pos },
} = route;
const { isLoading, paymentLink, loanIdToValue } = useAppSelector(
(state: RootState) => state.payment
);
const { isLoading, paymentLink } = useAppSelector((state: RootState) => state.payment);
const dispatch = useAppDispatch();
const isOnline = useIsOnline();
const caseDetail = useAppSelector((state) => state.allCases.caseDetails[caseId]);
@@ -79,9 +79,15 @@ const RegisterPayments: React.FC<IRegisterPayments> = ({ route }) => {
}
}, [paymentLink, generateClicked]);
const ChildComponents = numbers?.map(({ number, createdAt, sourceText }) => {
const ChildComponents = numbers?.map((phoneNumber) => {
const { number, createdAt, sourceText } = phoneNumber;
return (
<DropdownItem createdAt={createdAt} id={number} label={number} sourceText={sourceText} />
<DropdownItem
createdAt={createdAt}
id={number}
label={getPhoneNumberString(phoneNumber)}
sourceText={sourceText}
/>
);
});
@@ -128,20 +134,17 @@ const RegisterPayments: React.FC<IRegisterPayments> = ({ route }) => {
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,
generatePaymentLinkAction({
alternateContactNumber: getValues('selectedPhoneNumber'),
customAmount: {
currency: 'INR',
amount: Number(getValues('amount')),
},
loanIdToValue
)
customAmountProvided: Number(getValues('amount')) > -1,
loanAccountNumber: caseDetail.loanAccountNumber!!,
notifyToAlternateContact: true,
customerReferenceId: caseDetail.customerReferenceId,
})
);
setGenerateClicked(true);
}
@@ -151,14 +154,19 @@ const RegisterPayments: React.FC<IRegisterPayments> = ({ route }) => {
addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_COLLECT_MONEY_LOADED, { caseId: caseId });
return () => {
dispatch(setPaymentLink(''));
dispatch(setLoading(false));
};
}, []);
const getBottomSheetHeight = getDynamicBottomSheetHeightPercentageFn(HEADER_HEIGHT, ROW_HEIGHT);
if (!isOnline) {
return <OfflineScreen goBack={goBack} pageTitle={PAGE_TITLE} />;
}
return (
<Layout>
<NavigationHeader title={'Collect Money'} onBack={goBack} />
<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>

View File

@@ -90,6 +90,7 @@ export interface IGetTransformedCaseItem extends CaseDetail {
answer: any;
caseId: string;
coords: Geolocation.GeoCoordinates;
templateId: string;
}
export const getTransformedCollectionCaseItem = async (caseItem: IGetTransformedCaseItem) => {
let cloneCaseItem = { ...caseItem };
@@ -100,7 +101,7 @@ export const getTransformedCollectionCaseItem = async (caseItem: IGetTransformed
location: cloneCaseItem?.coords,
offlineCaseKey: cloneCaseItem?.offlineCaseKey,
};
return { caseType: caseItem.caseType, data };
return { caseType: caseItem.caseType, data, templateId: caseItem.templateId };
};
export const getTransformedAvCase = async (

View File

@@ -120,6 +120,8 @@ export const dataSyncService = async () => {
}
}
if (!syncUploadStatusPayload.length) return;
const uploadCompletedApiPayload: IUploadCompletedApiPayload = {
deviceId: GLOBAL.DEVICE_ID,
syncUploadStatus: syncUploadStatusPayload,

View File

@@ -1,9 +1,39 @@
import { COLORS } from '../../RN-UI-LIB/src/styles/colors';
export enum REPAYMENT_STATUS {
SUCCESS = 'SUCCESS',
FAILED = 'FAILED',
FAILURE = 'FAILURE',
SCHEDULED = 'SCHEDULED',
UNKNOWN = 'UNKNOWN',
FAILED = 'FAILED',
}
interface IRepaymentStatusTagging {
[key: string]: {
label: string;
iconColor: string;
variant: string;
};
}
export const RepaymentStatusTagging: IRepaymentStatusTagging = {
[REPAYMENT_STATUS.FAILURE]: {
label: 'Failed',
iconColor: COLORS.TEXT.RED,
variant: 'error',
},
[REPAYMENT_STATUS.FAILED]: {
label: 'Failed',
iconColor: COLORS.TEXT.RED,
variant: 'error',
},
[REPAYMENT_STATUS.UNKNOWN]: {
label: 'Unknown',
iconColor: COLORS.TEXT.LIGHT,
variant: 'gray',
},
};
export interface IRepaymentsRecord {
loanReferenceId: string;
customerReferenceId: string;
@@ -15,4 +45,5 @@ export interface IRepaymentsRecord {
amount: number;
failed: boolean;
success: boolean;
orderCompletionTimestamp: string;
}