feat: implement bank account management module including screens, API integration, and navigation configuration
This commit is contained in:
parent
d9766b842c
commit
65f685d5cf
52
app/api/bankDetailsApi.ts
Normal file
52
app/api/bankDetailsApi.ts
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
import { AddBankDetailsPayload, GetBankAccountsResponse } from '@interfaces';
|
||||||
|
import { apiClient } from '@services';
|
||||||
|
|
||||||
|
export const addAccountApi = async (
|
||||||
|
payload: AddBankDetailsPayload | FormData,
|
||||||
|
): Promise<GetBankAccountsResponse> => {
|
||||||
|
let body: any = payload;
|
||||||
|
|
||||||
|
if (
|
||||||
|
!(payload instanceof FormData) &&
|
||||||
|
typeof payload === 'object' &&
|
||||||
|
payload !== null
|
||||||
|
) {
|
||||||
|
const { passbookImage, ...rest } = payload as AddBankDetailsPayload;
|
||||||
|
|
||||||
|
if (passbookImage) {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('accountHolderName', rest.accountHolderName);
|
||||||
|
formData.append('bankName', rest.bankName);
|
||||||
|
formData.append('accountNumber', rest.accountNumber);
|
||||||
|
formData.append('ifscCode', rest.ifscCode);
|
||||||
|
if (rest.branchName) {
|
||||||
|
formData.append('branchName', rest.branchName);
|
||||||
|
}
|
||||||
|
if (rest.isDefault !== undefined) {
|
||||||
|
formData.append('isDefault', String(rest.isDefault));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof passbookImage === 'object' && (passbookImage as any).uri) {
|
||||||
|
formData.append('passbookImage', {
|
||||||
|
uri: (passbookImage as any).uri,
|
||||||
|
name: (passbookImage as any).name || 'passbook.jpg',
|
||||||
|
type: (passbookImage as any).type || 'image/jpeg',
|
||||||
|
} as any);
|
||||||
|
} else if (typeof passbookImage === 'string') {
|
||||||
|
formData.append('passbookImage', {
|
||||||
|
uri: passbookImage,
|
||||||
|
name: 'passbook.jpg',
|
||||||
|
type: 'image/jpeg',
|
||||||
|
} as any);
|
||||||
|
}
|
||||||
|
body = formData;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return await apiClient.post<GetBankAccountsResponse>('/bank-accounts', body);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const fetchBankAccountsApi =
|
||||||
|
async (): Promise<GetBankAccountsResponse> => {
|
||||||
|
return await apiClient.get<GetBankAccountsResponse>('/bank-accounts');
|
||||||
|
};
|
||||||
@ -4,3 +4,4 @@ export * from './kycApi';
|
|||||||
export * from './dashboardApi';
|
export * from './dashboardApi';
|
||||||
export * from './deliveryApi';
|
export * from './deliveryApi';
|
||||||
export * from './accountApi';
|
export * from './accountApi';
|
||||||
|
export * from './bankDetailsApi';
|
||||||
|
|||||||
@ -0,0 +1,221 @@
|
|||||||
|
import { StyleSheet } from 'react-native';
|
||||||
|
import { typography, spacing, borderRadius, shadow } from '@theme';
|
||||||
|
|
||||||
|
export const getStyles = (colors: any) =>
|
||||||
|
StyleSheet.create({
|
||||||
|
container: {
|
||||||
|
flex: 1,
|
||||||
|
backgroundColor: colors.background,
|
||||||
|
},
|
||||||
|
scrollContainer: {
|
||||||
|
paddingHorizontal: spacing.lg,
|
||||||
|
paddingTop: spacing.md,
|
||||||
|
paddingBottom: spacing.xxl + 40,
|
||||||
|
},
|
||||||
|
infoBanner: {
|
||||||
|
backgroundColor: colors.primaryLight,
|
||||||
|
borderRadius: borderRadius.lg,
|
||||||
|
padding: spacing.lg,
|
||||||
|
marginBottom: spacing.lg,
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: colors.border,
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
},
|
||||||
|
infoIconContainer: {
|
||||||
|
width: 44,
|
||||||
|
height: 44,
|
||||||
|
borderRadius: borderRadius.full,
|
||||||
|
backgroundColor: colors.primary,
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
marginRight: spacing.md,
|
||||||
|
},
|
||||||
|
infoTextContainer: {
|
||||||
|
flex: 1,
|
||||||
|
},
|
||||||
|
infoTitle: {
|
||||||
|
fontSize: typography.fontSize.md,
|
||||||
|
fontWeight: typography.fontWeight.bold,
|
||||||
|
color: colors.primaryDark || colors.primary,
|
||||||
|
marginBottom: 2,
|
||||||
|
},
|
||||||
|
infoSub: {
|
||||||
|
fontSize: typography.fontSize.xs,
|
||||||
|
color: colors.textSecondary,
|
||||||
|
lineHeight: 18,
|
||||||
|
},
|
||||||
|
formCard: {
|
||||||
|
backgroundColor: colors.cardBg,
|
||||||
|
borderRadius: borderRadius.lg,
|
||||||
|
padding: spacing.lg,
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: colors.border,
|
||||||
|
...shadow.sm,
|
||||||
|
},
|
||||||
|
fieldContainer: {
|
||||||
|
marginBottom: spacing.lg,
|
||||||
|
},
|
||||||
|
labelRow: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
marginBottom: spacing.xs + 2,
|
||||||
|
},
|
||||||
|
label: {
|
||||||
|
fontSize: typography.fontSize.sm,
|
||||||
|
fontWeight: typography.fontWeight.semibold,
|
||||||
|
color: colors.text,
|
||||||
|
},
|
||||||
|
requiredStar: {
|
||||||
|
color: colors.error,
|
||||||
|
marginLeft: 4,
|
||||||
|
fontSize: typography.fontSize.sm,
|
||||||
|
fontWeight: typography.fontWeight.bold,
|
||||||
|
},
|
||||||
|
input: {
|
||||||
|
backgroundColor: colors.inputBg,
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: colors.border,
|
||||||
|
borderRadius: borderRadius.md,
|
||||||
|
paddingHorizontal: spacing.md,
|
||||||
|
paddingVertical: spacing.md - 2,
|
||||||
|
fontSize: typography.fontSize.md,
|
||||||
|
color: colors.text,
|
||||||
|
},
|
||||||
|
inputError: {
|
||||||
|
borderColor: colors.error,
|
||||||
|
},
|
||||||
|
errorText: {
|
||||||
|
fontSize: typography.fontSize.xs,
|
||||||
|
color: colors.error,
|
||||||
|
marginTop: 4,
|
||||||
|
},
|
||||||
|
toggleRow: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
backgroundColor: colors.inputBg,
|
||||||
|
padding: spacing.md,
|
||||||
|
borderRadius: borderRadius.md,
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: colors.border,
|
||||||
|
marginBottom: spacing.lg,
|
||||||
|
},
|
||||||
|
toggleTextContainer: {
|
||||||
|
flex: 1,
|
||||||
|
marginRight: spacing.md,
|
||||||
|
},
|
||||||
|
toggleTitle: {
|
||||||
|
fontSize: typography.fontSize.sm,
|
||||||
|
fontWeight: typography.fontWeight.bold,
|
||||||
|
color: colors.text,
|
||||||
|
marginBottom: 2,
|
||||||
|
},
|
||||||
|
toggleSub: {
|
||||||
|
fontSize: typography.fontSize.xs,
|
||||||
|
color: colors.textSecondary,
|
||||||
|
},
|
||||||
|
uploadBox: {
|
||||||
|
backgroundColor: colors.inputBg,
|
||||||
|
borderWidth: 1.5,
|
||||||
|
borderStyle: 'dashed',
|
||||||
|
borderColor: colors.border,
|
||||||
|
borderRadius: borderRadius.md,
|
||||||
|
padding: spacing.lg,
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
},
|
||||||
|
uploadBoxActive: {
|
||||||
|
borderColor: colors.primary,
|
||||||
|
borderStyle: 'solid',
|
||||||
|
backgroundColor: colors.primaryLight,
|
||||||
|
},
|
||||||
|
uploadIconCircle: {
|
||||||
|
width: 42,
|
||||||
|
height: 42,
|
||||||
|
borderRadius: borderRadius.full,
|
||||||
|
backgroundColor: colors.cardBg,
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
marginBottom: spacing.xs,
|
||||||
|
},
|
||||||
|
uploadTitle: {
|
||||||
|
fontSize: typography.fontSize.sm,
|
||||||
|
fontWeight: typography.fontWeight.bold,
|
||||||
|
color: colors.primary,
|
||||||
|
marginBottom: 2,
|
||||||
|
},
|
||||||
|
uploadSub: {
|
||||||
|
fontSize: typography.fontSize.xs,
|
||||||
|
color: colors.textSecondary,
|
||||||
|
},
|
||||||
|
filePreviewRow: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
width: '100%',
|
||||||
|
},
|
||||||
|
fileInfo: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
flex: 1,
|
||||||
|
marginRight: spacing.sm,
|
||||||
|
},
|
||||||
|
fileThumbnail: {
|
||||||
|
width: 40,
|
||||||
|
height: 40,
|
||||||
|
borderRadius: borderRadius.sm,
|
||||||
|
marginRight: spacing.sm,
|
||||||
|
},
|
||||||
|
fileName: {
|
||||||
|
fontSize: typography.fontSize.sm,
|
||||||
|
fontWeight: typography.fontWeight.semibold,
|
||||||
|
color: colors.text,
|
||||||
|
},
|
||||||
|
fileSize: {
|
||||||
|
fontSize: typography.fontSize.xs,
|
||||||
|
color: colors.textSecondary,
|
||||||
|
marginTop: 2,
|
||||||
|
},
|
||||||
|
removeBtn: {
|
||||||
|
paddingHorizontal: spacing.sm,
|
||||||
|
paddingVertical: spacing.xs,
|
||||||
|
borderRadius: borderRadius.sm,
|
||||||
|
backgroundColor: colors.errorLight,
|
||||||
|
},
|
||||||
|
removeBtnText: {
|
||||||
|
fontSize: typography.fontSize.xs,
|
||||||
|
color: colors.error,
|
||||||
|
fontWeight: typography.fontWeight.bold,
|
||||||
|
},
|
||||||
|
errorBanner: {
|
||||||
|
backgroundColor: colors.errorLight,
|
||||||
|
borderRadius: borderRadius.md,
|
||||||
|
padding: spacing.md,
|
||||||
|
marginBottom: spacing.lg,
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: colors.error,
|
||||||
|
},
|
||||||
|
errorBannerText: {
|
||||||
|
fontSize: typography.fontSize.sm,
|
||||||
|
color: colors.error,
|
||||||
|
fontWeight: typography.fontWeight.medium,
|
||||||
|
},
|
||||||
|
submitBtn: {
|
||||||
|
backgroundColor: colors.primary,
|
||||||
|
borderRadius: borderRadius.md,
|
||||||
|
paddingVertical: spacing.md + 2,
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
marginTop: spacing.sm,
|
||||||
|
...shadow.md,
|
||||||
|
},
|
||||||
|
submitBtnDisabled: {
|
||||||
|
opacity: 0.6,
|
||||||
|
},
|
||||||
|
submitBtnText: {
|
||||||
|
fontSize: typography.fontSize.md,
|
||||||
|
fontWeight: typography.fontWeight.bold,
|
||||||
|
color: colors.white,
|
||||||
|
},
|
||||||
|
});
|
||||||
@ -0,0 +1,373 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import {
|
||||||
|
View,
|
||||||
|
Text,
|
||||||
|
ScrollView,
|
||||||
|
TextInput,
|
||||||
|
TouchableOpacity,
|
||||||
|
Switch,
|
||||||
|
ActivityIndicator,
|
||||||
|
Alert,
|
||||||
|
Image,
|
||||||
|
} from 'react-native';
|
||||||
|
import { useAppTheme } from '@theme';
|
||||||
|
import { ScreenHeader } from '@components';
|
||||||
|
import { ClipboardIcon, WalletIcon } from '@icons';
|
||||||
|
import { useAppDispatch, useAppSelector } from '@store';
|
||||||
|
import { useNavigation } from '@react-navigation/native';
|
||||||
|
import { pick, types, errorCodes } from '@react-native-documents/picker';
|
||||||
|
import { AddBankDetailsPayload } from '@interfaces';
|
||||||
|
import { addBankAccount } from './thunk';
|
||||||
|
import { resetAddBankState } from './reducer';
|
||||||
|
import { getStyles } from './addBankDetailsScreen.styles';
|
||||||
|
|
||||||
|
export const AddBankDetailsScreen: React.FC = () => {
|
||||||
|
const { colors } = useAppTheme();
|
||||||
|
const styles = getStyles(colors);
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const navigation = useNavigation();
|
||||||
|
|
||||||
|
const { isLoading, error, success } = useAppSelector(
|
||||||
|
state => state.addBankDetails,
|
||||||
|
);
|
||||||
|
|
||||||
|
const [accountHolderName, setAccountHolderName] = useState('');
|
||||||
|
const [bankName, setBankName] = useState('');
|
||||||
|
const [accountNumber, setAccountNumber] = useState('');
|
||||||
|
const [ifscCode, setIfscCode] = useState('');
|
||||||
|
const [branchName, setBranchName] = useState('');
|
||||||
|
const [isDefault, setIsDefault] = useState(true);
|
||||||
|
const [passbookImage, setPassbookImage] = useState<{
|
||||||
|
uri: string;
|
||||||
|
name: string;
|
||||||
|
type: string;
|
||||||
|
} | null>(null);
|
||||||
|
|
||||||
|
const [errors, setErrors] = useState<Record<string, string>>({});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
dispatch(resetAddBankState());
|
||||||
|
};
|
||||||
|
}, [dispatch]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (success) {
|
||||||
|
Alert.alert(
|
||||||
|
'Bank Account Registered',
|
||||||
|
'Your bank account has been added successfully.',
|
||||||
|
[
|
||||||
|
{
|
||||||
|
text: 'OK',
|
||||||
|
onPress: () => {
|
||||||
|
dispatch(resetAddBankState());
|
||||||
|
navigation.goBack();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}, [success, dispatch, navigation]);
|
||||||
|
|
||||||
|
const handlePickPassbook = async () => {
|
||||||
|
try {
|
||||||
|
const [result] = await pick({
|
||||||
|
type: [types.images, types.pdf],
|
||||||
|
allowMultiSelection: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result) {
|
||||||
|
setPassbookImage({
|
||||||
|
uri: result.uri,
|
||||||
|
name: result.name ?? `passbook_${Date.now()}.jpg`,
|
||||||
|
type: result.type ?? 'image/jpeg',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (err: any) {
|
||||||
|
if (err?.code === errorCodes.OPERATION_CANCELED) return;
|
||||||
|
Alert.alert('File Picker Error', err?.message ?? 'Failed to pick file.');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRemovePassbook = () => {
|
||||||
|
setPassbookImage(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = () => {
|
||||||
|
const newErrors: Record<string, string> = {};
|
||||||
|
|
||||||
|
if (!accountHolderName.trim()) {
|
||||||
|
newErrors.accountHolderName = 'Account holder name is required';
|
||||||
|
}
|
||||||
|
if (!bankName.trim()) {
|
||||||
|
newErrors.bankName = 'Bank name is required';
|
||||||
|
}
|
||||||
|
if (!accountNumber.trim()) {
|
||||||
|
newErrors.accountNumber = 'Account number is required';
|
||||||
|
} else if (accountNumber.trim().length < 8) {
|
||||||
|
newErrors.accountNumber = 'Enter a valid account number (min 8 digits)';
|
||||||
|
}
|
||||||
|
if (!ifscCode.trim()) {
|
||||||
|
newErrors.ifscCode = 'IFSC code is required';
|
||||||
|
} else if (ifscCode.trim().length < 4) {
|
||||||
|
newErrors.ifscCode = 'Enter a valid IFSC code';
|
||||||
|
}
|
||||||
|
|
||||||
|
setErrors(newErrors);
|
||||||
|
|
||||||
|
if (Object.keys(newErrors).length > 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload: AddBankDetailsPayload = {
|
||||||
|
accountHolderName: accountHolderName.trim(),
|
||||||
|
bankName: bankName.trim(),
|
||||||
|
accountNumber: accountNumber.trim(),
|
||||||
|
ifscCode: ifscCode.trim().toUpperCase(),
|
||||||
|
branchName: branchName.trim() || undefined,
|
||||||
|
isDefault,
|
||||||
|
passbookImage: passbookImage ? (passbookImage as any) : undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
dispatch(addBankAccount(payload));
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={styles.container}>
|
||||||
|
<ScreenHeader
|
||||||
|
title="Add Bank Account"
|
||||||
|
showBack={true}
|
||||||
|
onBack={() => navigation.goBack()}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ScrollView
|
||||||
|
contentContainerStyle={styles.scrollContainer}
|
||||||
|
showsVerticalScrollIndicator={false}
|
||||||
|
>
|
||||||
|
{/* Info Header Banner */}
|
||||||
|
<View style={styles.infoBanner}>
|
||||||
|
<View style={styles.infoIconContainer}>
|
||||||
|
<WalletIcon size={22} color={colors.white} />
|
||||||
|
</View>
|
||||||
|
<View style={styles.infoTextContainer}>
|
||||||
|
<Text style={styles.infoTitle}>Payout Account Details</Text>
|
||||||
|
<Text style={styles.infoSub}>
|
||||||
|
Enter your official bank details to receive weekly delivery earnings and automated payouts.
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Error Banner */}
|
||||||
|
{error && (
|
||||||
|
<View style={styles.errorBanner}>
|
||||||
|
<Text style={styles.errorBannerText}>{error}</Text>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Form Fields Card */}
|
||||||
|
<View style={styles.formCard}>
|
||||||
|
{/* Account Holder Name */}
|
||||||
|
<View style={styles.fieldContainer}>
|
||||||
|
<View style={styles.labelRow}>
|
||||||
|
<Text style={styles.label}>Account Holder Name</Text>
|
||||||
|
<Text style={styles.requiredStar}>*</Text>
|
||||||
|
</View>
|
||||||
|
<TextInput
|
||||||
|
style={[
|
||||||
|
styles.input,
|
||||||
|
errors.accountHolderName ? styles.inputError : null,
|
||||||
|
]}
|
||||||
|
placeholder="Name as per bank account (e.g. John Doe)"
|
||||||
|
placeholderTextColor={colors.placeholder}
|
||||||
|
value={accountHolderName}
|
||||||
|
onChangeText={text => {
|
||||||
|
setAccountHolderName(text);
|
||||||
|
if (errors.accountHolderName) {
|
||||||
|
setErrors(prev => ({ ...prev, accountHolderName: '' }));
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
autoCapitalize="words"
|
||||||
|
/>
|
||||||
|
{errors.accountHolderName && (
|
||||||
|
<Text style={styles.errorText}>{errors.accountHolderName}</Text>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Bank Name */}
|
||||||
|
<View style={styles.fieldContainer}>
|
||||||
|
<View style={styles.labelRow}>
|
||||||
|
<Text style={styles.label}>Bank Name</Text>
|
||||||
|
<Text style={styles.requiredStar}>*</Text>
|
||||||
|
</View>
|
||||||
|
<TextInput
|
||||||
|
style={[styles.input, errors.bankName ? styles.inputError : null]}
|
||||||
|
placeholder="e.g. HDFC Bank / ICICI Bank"
|
||||||
|
placeholderTextColor={colors.placeholder}
|
||||||
|
value={bankName}
|
||||||
|
onChangeText={text => {
|
||||||
|
setBankName(text);
|
||||||
|
if (errors.bankName) {
|
||||||
|
setErrors(prev => ({ ...prev, bankName: '' }));
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
autoCapitalize="words"
|
||||||
|
/>
|
||||||
|
{errors.bankName && (
|
||||||
|
<Text style={styles.errorText}>{errors.bankName}</Text>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Account Number */}
|
||||||
|
<View style={styles.fieldContainer}>
|
||||||
|
<View style={styles.labelRow}>
|
||||||
|
<Text style={styles.label}>Account Number</Text>
|
||||||
|
<Text style={styles.requiredStar}>*</Text>
|
||||||
|
</View>
|
||||||
|
<TextInput
|
||||||
|
style={[
|
||||||
|
styles.input,
|
||||||
|
errors.accountNumber ? styles.inputError : null,
|
||||||
|
]}
|
||||||
|
placeholder="e.g. 50100432845612"
|
||||||
|
placeholderTextColor={colors.placeholder}
|
||||||
|
value={accountNumber}
|
||||||
|
onChangeText={text => {
|
||||||
|
setAccountNumber(text);
|
||||||
|
if (errors.accountNumber) {
|
||||||
|
setErrors(prev => ({ ...prev, accountNumber: '' }));
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
keyboardType="number-pad"
|
||||||
|
/>
|
||||||
|
{errors.accountNumber && (
|
||||||
|
<Text style={styles.errorText}>{errors.accountNumber}</Text>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* IFSC Code */}
|
||||||
|
<View style={styles.fieldContainer}>
|
||||||
|
<View style={styles.labelRow}>
|
||||||
|
<Text style={styles.label}>IFSC Code</Text>
|
||||||
|
<Text style={styles.requiredStar}>*</Text>
|
||||||
|
</View>
|
||||||
|
<TextInput
|
||||||
|
style={[styles.input, errors.ifscCode ? styles.inputError : null]}
|
||||||
|
placeholder="e.g. HDFC0000012"
|
||||||
|
placeholderTextColor={colors.placeholder}
|
||||||
|
value={ifscCode}
|
||||||
|
onChangeText={text => {
|
||||||
|
setIfscCode(text.toUpperCase());
|
||||||
|
if (errors.ifscCode) {
|
||||||
|
setErrors(prev => ({ ...prev, ifscCode: '' }));
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
autoCapitalize="characters"
|
||||||
|
/>
|
||||||
|
{errors.ifscCode && (
|
||||||
|
<Text style={styles.errorText}>{errors.ifscCode}</Text>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Branch Name (Optional) */}
|
||||||
|
<View style={styles.fieldContainer}>
|
||||||
|
<View style={styles.labelRow}>
|
||||||
|
<Text style={styles.label}>Branch Name (Optional)</Text>
|
||||||
|
</View>
|
||||||
|
<TextInput
|
||||||
|
style={styles.input}
|
||||||
|
placeholder="e.g. Sector 62 Noida"
|
||||||
|
placeholderTextColor={colors.placeholder}
|
||||||
|
value={branchName}
|
||||||
|
onChangeText={setBranchName}
|
||||||
|
autoCapitalize="sentences"
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Default Account Toggle */}
|
||||||
|
<View style={styles.toggleRow}>
|
||||||
|
<View style={styles.toggleTextContainer}>
|
||||||
|
<Text style={styles.toggleTitle}>Default Payout Account</Text>
|
||||||
|
<Text style={styles.toggleSub}>
|
||||||
|
Mark as default bank account for payouts
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
<Switch
|
||||||
|
value={isDefault}
|
||||||
|
onValueChange={setIsDefault}
|
||||||
|
trackColor={{ false: colors.border, true: colors.primary }}
|
||||||
|
thumbColor={colors.white}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Passbook / Cancelled Cheque Image (Optional) */}
|
||||||
|
<View style={styles.fieldContainer}>
|
||||||
|
<View style={styles.labelRow}>
|
||||||
|
<Text style={styles.label}>
|
||||||
|
Passbook / Cancelled Cheque (Optional)
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<TouchableOpacity
|
||||||
|
style={[
|
||||||
|
styles.uploadBox,
|
||||||
|
passbookImage ? styles.uploadBoxActive : null,
|
||||||
|
]}
|
||||||
|
onPress={handlePickPassbook}
|
||||||
|
activeOpacity={0.8}
|
||||||
|
>
|
||||||
|
{passbookImage ? (
|
||||||
|
<View style={styles.filePreviewRow}>
|
||||||
|
<View style={styles.fileInfo}>
|
||||||
|
{passbookImage.type.startsWith('image/') && (
|
||||||
|
<Image
|
||||||
|
source={{ uri: passbookImage.uri }}
|
||||||
|
style={styles.fileThumbnail}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<View style={{ flex: 1 }}>
|
||||||
|
<Text style={styles.fileName} numberOfLines={1}>
|
||||||
|
{passbookImage.name}
|
||||||
|
</Text>
|
||||||
|
<Text style={styles.fileSize}>Attached document</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
<TouchableOpacity
|
||||||
|
style={styles.removeBtn}
|
||||||
|
onPress={handleRemovePassbook}
|
||||||
|
activeOpacity={0.7}
|
||||||
|
>
|
||||||
|
<Text style={styles.removeBtnText}>Remove</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<View style={styles.uploadIconCircle}>
|
||||||
|
<ClipboardIcon size={20} color={colors.primary} />
|
||||||
|
</View>
|
||||||
|
<Text style={styles.uploadTitle}>Choose Passbook / Cheque File</Text>
|
||||||
|
<Text style={styles.uploadSub}>Supports JPG, PNG, or PDF format</Text>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Submit Button */}
|
||||||
|
<TouchableOpacity
|
||||||
|
style={[styles.submitBtn, isLoading ? styles.submitBtnDisabled : null]}
|
||||||
|
onPress={handleSubmit}
|
||||||
|
disabled={isLoading}
|
||||||
|
activeOpacity={0.8}
|
||||||
|
>
|
||||||
|
{isLoading ? (
|
||||||
|
<ActivityIndicator color={colors.white} size="small" />
|
||||||
|
) : (
|
||||||
|
<Text style={styles.submitBtnText}>Save Bank Account</Text>
|
||||||
|
)}
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
</ScrollView>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AddBankDetailsScreen;
|
||||||
3
app/features/screens/addBankDetailsScreen/index.ts
Normal file
3
app/features/screens/addBankDetailsScreen/index.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export * from './addBankDetailsScreen';
|
||||||
|
export * from './thunk';
|
||||||
|
export * from './reducer';
|
||||||
54
app/features/screens/addBankDetailsScreen/reducer.ts
Normal file
54
app/features/screens/addBankDetailsScreen/reducer.ts
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
import { createAction, createReducer } from '@reduxjs/toolkit';
|
||||||
|
import { GetBankAccountsResponse } from '@interfaces';
|
||||||
|
import { addBankAccount, fetchBankAccounts } from './thunk';
|
||||||
|
|
||||||
|
export interface AddBankDetailsState {
|
||||||
|
isLoading: boolean;
|
||||||
|
error: string | null;
|
||||||
|
success: boolean;
|
||||||
|
bankAccounts: GetBankAccountsResponse | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const initialState: AddBankDetailsState = {
|
||||||
|
isLoading: false,
|
||||||
|
error: null,
|
||||||
|
success: false,
|
||||||
|
bankAccounts: null,
|
||||||
|
};
|
||||||
|
export const resetAddBankState = createAction('addBankDetails/reset');
|
||||||
|
|
||||||
|
export const addBankDetailsReducer = createReducer(initialState, builder =>
|
||||||
|
builder
|
||||||
|
.addCase(addBankAccount.pending, state => {
|
||||||
|
state.isLoading = true;
|
||||||
|
state.error = null;
|
||||||
|
state.success = false;
|
||||||
|
})
|
||||||
|
.addCase(addBankAccount.fulfilled, (state, action) => {
|
||||||
|
state.isLoading = false;
|
||||||
|
state.success = true;
|
||||||
|
state.bankAccounts = action.payload;
|
||||||
|
})
|
||||||
|
.addCase(addBankAccount.rejected, (state, action) => {
|
||||||
|
state.isLoading = false;
|
||||||
|
state.error = action.payload as string;
|
||||||
|
state.success = false;
|
||||||
|
})
|
||||||
|
.addCase(fetchBankAccounts.pending, state => {
|
||||||
|
state.isLoading = true;
|
||||||
|
state.error = null;
|
||||||
|
})
|
||||||
|
.addCase(fetchBankAccounts.fulfilled, (state, action) => {
|
||||||
|
state.isLoading = false;
|
||||||
|
state.bankAccounts = action.payload;
|
||||||
|
})
|
||||||
|
.addCase(fetchBankAccounts.rejected, (state, action) => {
|
||||||
|
state.isLoading = false;
|
||||||
|
state.error = action.payload as string;
|
||||||
|
})
|
||||||
|
.addCase(resetAddBankState, state => {
|
||||||
|
state.isLoading = false;
|
||||||
|
state.error = null;
|
||||||
|
state.success = false;
|
||||||
|
}),
|
||||||
|
);
|
||||||
37
app/features/screens/addBankDetailsScreen/thunk.ts
Normal file
37
app/features/screens/addBankDetailsScreen/thunk.ts
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
import { createAsyncThunk } from '@reduxjs/toolkit';
|
||||||
|
import { addAccountApi, fetchBankAccountsApi } from '@api';
|
||||||
|
import { AddBankDetailsPayload, GetBankAccountsResponse } from '@interfaces';
|
||||||
|
|
||||||
|
export const addBankAccount = createAsyncThunk<
|
||||||
|
GetBankAccountsResponse,
|
||||||
|
AddBankDetailsPayload | FormData,
|
||||||
|
{ rejectValue: string }
|
||||||
|
>('addBankDetails/addBankAccount', async (payload, { rejectWithValue }) => {
|
||||||
|
try {
|
||||||
|
const response = await addAccountApi(payload);
|
||||||
|
return response;
|
||||||
|
} catch (error: any) {
|
||||||
|
const message =
|
||||||
|
error?.response?.data?.message ||
|
||||||
|
error?.message ||
|
||||||
|
'Failed to add bank account';
|
||||||
|
return rejectWithValue(message);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export const fetchBankAccounts = createAsyncThunk<
|
||||||
|
GetBankAccountsResponse,
|
||||||
|
void,
|
||||||
|
{ rejectValue: string }
|
||||||
|
>('addBankDetails/fetchBankAccounts', async (_, { rejectWithValue }) => {
|
||||||
|
try {
|
||||||
|
const response = await fetchBankAccountsApi();
|
||||||
|
return response;
|
||||||
|
} catch (error: any) {
|
||||||
|
const message =
|
||||||
|
error?.response?.data?.message ||
|
||||||
|
error?.message ||
|
||||||
|
'Failed to fetch bank accounts';
|
||||||
|
return rejectWithValue(message);
|
||||||
|
}
|
||||||
|
});
|
||||||
@ -0,0 +1,274 @@
|
|||||||
|
import { StyleSheet } from 'react-native';
|
||||||
|
import { typography, spacing, borderRadius, shadow } from '@theme';
|
||||||
|
|
||||||
|
export const getStyles = (colors: any) =>
|
||||||
|
StyleSheet.create({
|
||||||
|
container: {
|
||||||
|
flex: 1,
|
||||||
|
backgroundColor: colors.background,
|
||||||
|
},
|
||||||
|
scrollContainer: {
|
||||||
|
paddingHorizontal: spacing.lg,
|
||||||
|
paddingTop: spacing.md,
|
||||||
|
paddingBottom: spacing.xxl + 40,
|
||||||
|
},
|
||||||
|
// ── Empty State ────────────────────────────────────────────────────
|
||||||
|
emptyContainer: {
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
paddingVertical: spacing.huge + 24,
|
||||||
|
},
|
||||||
|
emptyIconCircle: {
|
||||||
|
width: 80,
|
||||||
|
height: 80,
|
||||||
|
borderRadius: borderRadius.full,
|
||||||
|
backgroundColor: colors.primaryLight,
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
marginBottom: spacing.lg,
|
||||||
|
},
|
||||||
|
emptyTitle: {
|
||||||
|
fontSize: typography.fontSize.lg,
|
||||||
|
fontWeight: typography.fontWeight.bold,
|
||||||
|
color: colors.text,
|
||||||
|
marginBottom: spacing.xs,
|
||||||
|
},
|
||||||
|
emptySubtitle: {
|
||||||
|
fontSize: typography.fontSize.sm,
|
||||||
|
color: colors.textSecondary,
|
||||||
|
textAlign: 'center',
|
||||||
|
lineHeight: 20,
|
||||||
|
paddingHorizontal: spacing.xl,
|
||||||
|
},
|
||||||
|
// ── Summary Banner ────────────────────────────────────────────────
|
||||||
|
summaryBanner: {
|
||||||
|
backgroundColor: colors.primaryLight,
|
||||||
|
borderRadius: borderRadius.lg,
|
||||||
|
padding: spacing.lg,
|
||||||
|
marginBottom: spacing.lg,
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: colors.border,
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
},
|
||||||
|
summaryIconCircle: {
|
||||||
|
width: 44,
|
||||||
|
height: 44,
|
||||||
|
borderRadius: borderRadius.full,
|
||||||
|
backgroundColor: colors.primary,
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
marginRight: spacing.md,
|
||||||
|
},
|
||||||
|
summaryTextContainer: {
|
||||||
|
flex: 1,
|
||||||
|
},
|
||||||
|
summaryTitle: {
|
||||||
|
fontSize: typography.fontSize.md,
|
||||||
|
fontWeight: typography.fontWeight.bold,
|
||||||
|
color: colors.primaryDark || colors.primary,
|
||||||
|
marginBottom: 2,
|
||||||
|
},
|
||||||
|
summarySubtitle: {
|
||||||
|
fontSize: typography.fontSize.xs,
|
||||||
|
color: colors.textSecondary,
|
||||||
|
},
|
||||||
|
// ── Section Header ─────────────────────────────────────────────────
|
||||||
|
sectionHeader: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
marginBottom: spacing.md,
|
||||||
|
},
|
||||||
|
sectionTitle: {
|
||||||
|
fontSize: typography.fontSize.md,
|
||||||
|
fontWeight: typography.fontWeight.bold,
|
||||||
|
color: colors.text,
|
||||||
|
},
|
||||||
|
countBadge: {
|
||||||
|
backgroundColor: colors.primaryLight,
|
||||||
|
paddingHorizontal: spacing.sm,
|
||||||
|
paddingVertical: 3,
|
||||||
|
borderRadius: borderRadius.full,
|
||||||
|
},
|
||||||
|
countBadgeText: {
|
||||||
|
fontSize: typography.fontSize.xs - 1,
|
||||||
|
fontWeight: typography.fontWeight.bold,
|
||||||
|
color: colors.primary,
|
||||||
|
},
|
||||||
|
// ── Account Card ───────────────────────────────────────────────────
|
||||||
|
accountCard: {
|
||||||
|
backgroundColor: colors.cardBg,
|
||||||
|
borderRadius: borderRadius.lg,
|
||||||
|
marginBottom: spacing.md,
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: colors.border,
|
||||||
|
overflow: 'hidden',
|
||||||
|
...shadow.sm,
|
||||||
|
},
|
||||||
|
defaultCard: {
|
||||||
|
borderColor: colors.primary,
|
||||||
|
borderWidth: 1.5,
|
||||||
|
},
|
||||||
|
cardTopStrip: {
|
||||||
|
height: 4,
|
||||||
|
backgroundColor: colors.primary,
|
||||||
|
width: '100%',
|
||||||
|
},
|
||||||
|
cardBody: {
|
||||||
|
padding: spacing.lg,
|
||||||
|
},
|
||||||
|
cardHeaderRow: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
marginBottom: spacing.md,
|
||||||
|
},
|
||||||
|
cardHeaderLeft: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
flex: 1,
|
||||||
|
marginRight: spacing.sm,
|
||||||
|
},
|
||||||
|
bankIconCircle: {
|
||||||
|
width: 46,
|
||||||
|
height: 46,
|
||||||
|
borderRadius: borderRadius.md,
|
||||||
|
backgroundColor: colors.primaryLight,
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
marginRight: spacing.md,
|
||||||
|
},
|
||||||
|
cardTitleGroup: {
|
||||||
|
flex: 1,
|
||||||
|
},
|
||||||
|
bankName: {
|
||||||
|
fontSize: typography.fontSize.md,
|
||||||
|
fontWeight: typography.fontWeight.bold,
|
||||||
|
color: colors.text,
|
||||||
|
marginBottom: 2,
|
||||||
|
},
|
||||||
|
holderName: {
|
||||||
|
fontSize: typography.fontSize.xs,
|
||||||
|
color: colors.textSecondary,
|
||||||
|
},
|
||||||
|
badgeRow: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: 6,
|
||||||
|
},
|
||||||
|
defaultBadge: {
|
||||||
|
backgroundColor: colors.primary,
|
||||||
|
paddingHorizontal: spacing.sm,
|
||||||
|
paddingVertical: 3,
|
||||||
|
borderRadius: borderRadius.full,
|
||||||
|
},
|
||||||
|
defaultBadgeText: {
|
||||||
|
fontSize: typography.fontSize.xs - 2,
|
||||||
|
fontWeight: typography.fontWeight.bold,
|
||||||
|
color: colors.white,
|
||||||
|
letterSpacing: 0.3,
|
||||||
|
},
|
||||||
|
verifiedBadge: {
|
||||||
|
backgroundColor: colors.successLight,
|
||||||
|
paddingHorizontal: spacing.sm,
|
||||||
|
paddingVertical: 3,
|
||||||
|
borderRadius: borderRadius.full,
|
||||||
|
},
|
||||||
|
unverifiedBadge: {
|
||||||
|
backgroundColor: colors.warningLight,
|
||||||
|
paddingHorizontal: spacing.sm,
|
||||||
|
paddingVertical: 3,
|
||||||
|
borderRadius: borderRadius.full,
|
||||||
|
},
|
||||||
|
verifiedBadgeText: {
|
||||||
|
fontSize: typography.fontSize.xs - 2,
|
||||||
|
fontWeight: typography.fontWeight.bold,
|
||||||
|
color: colors.success,
|
||||||
|
letterSpacing: 0.3,
|
||||||
|
},
|
||||||
|
unverifiedBadgeText: {
|
||||||
|
fontSize: typography.fontSize.xs - 2,
|
||||||
|
fontWeight: typography.fontWeight.bold,
|
||||||
|
color: colors.warning,
|
||||||
|
letterSpacing: 0.3,
|
||||||
|
},
|
||||||
|
divider: {
|
||||||
|
height: 1,
|
||||||
|
backgroundColor: colors.divider,
|
||||||
|
marginVertical: spacing.md,
|
||||||
|
},
|
||||||
|
// ── Card Fields ────────────────────────────────────────────────────
|
||||||
|
fieldsGrid: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
flexWrap: 'wrap',
|
||||||
|
},
|
||||||
|
fieldItem: {
|
||||||
|
width: '50%',
|
||||||
|
marginBottom: spacing.md,
|
||||||
|
},
|
||||||
|
fieldItemFull: {
|
||||||
|
width: '100%',
|
||||||
|
marginBottom: spacing.md,
|
||||||
|
},
|
||||||
|
fieldLabel: {
|
||||||
|
fontSize: typography.fontSize.xs - 1,
|
||||||
|
color: colors.textSecondary,
|
||||||
|
marginBottom: 3,
|
||||||
|
textTransform: 'uppercase',
|
||||||
|
letterSpacing: 0.5,
|
||||||
|
},
|
||||||
|
fieldValue: {
|
||||||
|
fontSize: typography.fontSize.sm,
|
||||||
|
fontWeight: typography.fontWeight.semibold,
|
||||||
|
color: colors.text,
|
||||||
|
},
|
||||||
|
fieldValueMasked: {
|
||||||
|
fontSize: typography.fontSize.sm,
|
||||||
|
fontWeight: typography.fontWeight.semibold,
|
||||||
|
color: colors.text,
|
||||||
|
letterSpacing: 2,
|
||||||
|
},
|
||||||
|
// ── Card Footer ────────────────────────────────────────────────────
|
||||||
|
cardFooter: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
paddingTop: spacing.sm,
|
||||||
|
},
|
||||||
|
addedAtText: {
|
||||||
|
fontSize: typography.fontSize.xs,
|
||||||
|
color: colors.textSecondary,
|
||||||
|
},
|
||||||
|
// ── Add Button ─────────────────────────────────────────────────────
|
||||||
|
addButton: {
|
||||||
|
backgroundColor: colors.primary,
|
||||||
|
borderRadius: borderRadius.md,
|
||||||
|
paddingVertical: spacing.md + 2,
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
flexDirection: 'row',
|
||||||
|
marginTop: spacing.sm,
|
||||||
|
...shadow.md,
|
||||||
|
},
|
||||||
|
addButtonText: {
|
||||||
|
fontSize: typography.fontSize.md,
|
||||||
|
fontWeight: typography.fontWeight.bold,
|
||||||
|
color: colors.white,
|
||||||
|
marginLeft: spacing.sm,
|
||||||
|
},
|
||||||
|
addButtonIcon: {
|
||||||
|
width: 22,
|
||||||
|
height: 22,
|
||||||
|
borderRadius: borderRadius.full,
|
||||||
|
backgroundColor: 'rgba(255,255,255,0.25)',
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
},
|
||||||
|
addButtonIconText: {
|
||||||
|
color: colors.white,
|
||||||
|
fontSize: 18,
|
||||||
|
lineHeight: 20,
|
||||||
|
fontWeight: typography.fontWeight.bold,
|
||||||
|
},
|
||||||
|
});
|
||||||
252
app/features/screens/bankDetailsScreen/bankDetailsScreen.tsx
Normal file
252
app/features/screens/bankDetailsScreen/bankDetailsScreen.tsx
Normal file
@ -0,0 +1,252 @@
|
|||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import {
|
||||||
|
View,
|
||||||
|
Text,
|
||||||
|
ScrollView,
|
||||||
|
TouchableOpacity,
|
||||||
|
RefreshControl,
|
||||||
|
} from 'react-native';
|
||||||
|
import { useAppTheme } from '@theme';
|
||||||
|
import { ScreenHeader } from '@components';
|
||||||
|
import { WalletIcon, CheckCircleIcon, ClipboardIcon } from '@icons';
|
||||||
|
import { useNavigation } from '@react-navigation/native';
|
||||||
|
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
|
||||||
|
import { AppStackParamList } from '@navigation/navigationTypes';
|
||||||
|
import { RouteNames } from '@utils/constants';
|
||||||
|
import { BankAccount } from '@interfaces';
|
||||||
|
import { getStyles } from './bankDetailsScreen.styles';
|
||||||
|
import { formatDate, maskAccountNumber } from '@utils';
|
||||||
|
import { fetchBankAccounts } from '../addBankDetailsScreen';
|
||||||
|
import { useAppDispatch, useAppSelector } from '@store';
|
||||||
|
|
||||||
|
// ── Dummy data — replace with API data later ──────────────────────────────────
|
||||||
|
// const DUMMY_ACCOUNTS: BankAccount[] = [
|
||||||
|
// {
|
||||||
|
// id: '071e5d96-9e7f-4e57-a79c-593c2e2d0453',
|
||||||
|
// ownerType: 'MERCHANT',
|
||||||
|
// ownerId: 'b2b301a9-bc8f-4b22-b724-78279022dd85',
|
||||||
|
// accountHolderName: 'sddsd',
|
||||||
|
// bankName: 'sddsds',
|
||||||
|
// accountNumber: 'ssdsds',
|
||||||
|
// ifscCode: 'sdssd',
|
||||||
|
// branchName: 'sdssdsd',
|
||||||
|
// isDefault: true,
|
||||||
|
// isVerified: false,
|
||||||
|
// passbookImage:
|
||||||
|
// '/bank-accounts/image/b2b301a9-bc8f-4b22-b724-78279022dd85-1784618982576.png',
|
||||||
|
// createdAt: '2026-07-21T07:29:26.645Z',
|
||||||
|
// updatedAt: '2026-07-21T07:29:42.582Z',
|
||||||
|
// },
|
||||||
|
// ];
|
||||||
|
export const BankDetailsScreen: React.FC = () => {
|
||||||
|
const { colors } = useAppTheme();
|
||||||
|
const styles = getStyles(colors);
|
||||||
|
const navigation =
|
||||||
|
useNavigation<NativeStackNavigationProp<AppStackParamList>>();
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const { bankAccounts, isLoading, error } = useAppSelector(
|
||||||
|
state => state.addBankDetails,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Replace this state with Redux selector when wiring real API
|
||||||
|
// const [accounts] = useState<BankAccount[]>(bankAccounts);
|
||||||
|
const [refreshing, setRefreshing] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
dispatch(fetchBankAccounts());
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleRefresh = () => {
|
||||||
|
setRefreshing(true);
|
||||||
|
dispatch(fetchBankAccounts())
|
||||||
|
.unwrap()
|
||||||
|
.then(() => setRefreshing(false))
|
||||||
|
.catch(() => setRefreshing(false));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAddAccount = () => {
|
||||||
|
navigation.navigate(RouteNames.AddBankDetails);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={styles.container}>
|
||||||
|
<ScreenHeader
|
||||||
|
title="Bank Details"
|
||||||
|
showBack={true}
|
||||||
|
onBack={() => navigation.goBack()}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ScrollView
|
||||||
|
contentContainerStyle={styles.scrollContainer}
|
||||||
|
showsVerticalScrollIndicator={false}
|
||||||
|
refreshControl={
|
||||||
|
<RefreshControl
|
||||||
|
refreshing={refreshing}
|
||||||
|
onRefresh={handleRefresh}
|
||||||
|
tintColor={colors.primary}
|
||||||
|
colors={[colors.primary]}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{/* Summary Banner */}
|
||||||
|
<View style={styles.summaryBanner}>
|
||||||
|
<View style={styles.summaryIconCircle}>
|
||||||
|
<WalletIcon size={22} color={colors.white} />
|
||||||
|
</View>
|
||||||
|
<View style={styles.summaryTextContainer}>
|
||||||
|
<Text style={styles.summaryTitle}>Payout Accounts</Text>
|
||||||
|
<Text style={styles.summarySubtitle}>
|
||||||
|
Manage your bank accounts for receiving earnings
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Empty State */}
|
||||||
|
{bankAccounts?.length === 0 ? (
|
||||||
|
<View style={styles.emptyContainer}>
|
||||||
|
<View style={styles.emptyIconCircle}>
|
||||||
|
<WalletIcon size={36} color={colors.primary} />
|
||||||
|
</View>
|
||||||
|
<Text style={styles.emptyTitle}>No Bank Accounts</Text>
|
||||||
|
<Text style={styles.emptySubtitle}>
|
||||||
|
Add your bank account to start receiving your delivery earnings
|
||||||
|
directly.
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{/* Section Header */}
|
||||||
|
<View style={styles.sectionHeader}>
|
||||||
|
<Text style={styles.sectionTitle}>Your Accounts</Text>
|
||||||
|
<View style={styles.countBadge}>
|
||||||
|
<Text style={styles.countBadgeText}>
|
||||||
|
{bankAccounts?.length}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Account Cards */}
|
||||||
|
{bankAccounts?.map(account => (
|
||||||
|
<View
|
||||||
|
key={account.id}
|
||||||
|
style={[
|
||||||
|
styles.accountCard,
|
||||||
|
account.isDefault ? styles.defaultCard : null,
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
{/* Top color strip for default account */}
|
||||||
|
{account.isDefault && <View style={styles.cardTopStrip} />}
|
||||||
|
|
||||||
|
<View style={styles.cardBody}>
|
||||||
|
{/* Card Header Row */}
|
||||||
|
<View style={styles.cardHeaderRow}>
|
||||||
|
<View style={styles.cardHeaderLeft}>
|
||||||
|
<View style={styles.bankIconCircle}>
|
||||||
|
<ClipboardIcon size={22} color={colors.primary} />
|
||||||
|
</View>
|
||||||
|
<View style={styles.cardTitleGroup}>
|
||||||
|
<Text style={styles.bankName} numberOfLines={1}>
|
||||||
|
{account.bankName}
|
||||||
|
</Text>
|
||||||
|
<Text style={styles.holderName} numberOfLines={1}>
|
||||||
|
{account.accountHolderName}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Status Badges */}
|
||||||
|
<View style={styles.badgeRow}>
|
||||||
|
{account.isDefault && (
|
||||||
|
<View style={styles.defaultBadge}>
|
||||||
|
<Text style={styles.defaultBadgeText}>DEFAULT</Text>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
<View
|
||||||
|
style={
|
||||||
|
account.isVerified
|
||||||
|
? styles.verifiedBadge
|
||||||
|
: styles.unverifiedBadge
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Text
|
||||||
|
style={
|
||||||
|
account.isVerified
|
||||||
|
? styles.verifiedBadgeText
|
||||||
|
: styles.unverifiedBadgeText
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{account.isVerified ? 'VERIFIED' : 'PENDING'}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View style={styles.divider} />
|
||||||
|
|
||||||
|
{/* Fields Grid */}
|
||||||
|
<View style={styles.fieldsGrid}>
|
||||||
|
<View style={styles.fieldItem}>
|
||||||
|
<Text style={styles.fieldLabel}>Account Number</Text>
|
||||||
|
<Text style={styles.fieldValueMasked}>
|
||||||
|
{maskAccountNumber(account.accountNumber)}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View style={styles.fieldItem}>
|
||||||
|
<Text style={styles.fieldLabel}>IFSC Code</Text>
|
||||||
|
<Text style={styles.fieldValue}>
|
||||||
|
{account.ifscCode || '—'}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{account.branchName ? (
|
||||||
|
<View style={styles.fieldItem}>
|
||||||
|
<Text style={styles.fieldLabel}>Branch</Text>
|
||||||
|
<Text style={styles.fieldValue} numberOfLines={1}>
|
||||||
|
{account.branchName}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
<View style={styles.fieldItem}>
|
||||||
|
<Text style={styles.fieldLabel}>Owner Type</Text>
|
||||||
|
<Text style={styles.fieldValue}>
|
||||||
|
{account.ownerType || '—'}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View style={styles.divider} />
|
||||||
|
|
||||||
|
{/* Card Footer */}
|
||||||
|
<View style={styles.cardFooter}>
|
||||||
|
<Text style={styles.addedAtText}>
|
||||||
|
Added {formatDate(account.createdAt)}
|
||||||
|
</Text>
|
||||||
|
{account.isVerified && (
|
||||||
|
<CheckCircleIcon size={16} color={colors.success} />
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Add Bank Account Button */}
|
||||||
|
<TouchableOpacity
|
||||||
|
style={styles.addButton}
|
||||||
|
onPress={handleAddAccount}
|
||||||
|
activeOpacity={0.8}
|
||||||
|
>
|
||||||
|
<View style={styles.addButtonIcon}>
|
||||||
|
<Text style={styles.addButtonIconText}>+</Text>
|
||||||
|
</View>
|
||||||
|
<Text style={styles.addButtonText}>Add Bank Account</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</ScrollView>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default BankDetailsScreen;
|
||||||
1
app/features/screens/bankDetailsScreen/index.ts
Normal file
1
app/features/screens/bankDetailsScreen/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from './bankDetailsScreen';
|
||||||
@ -20,3 +20,5 @@ export * from './setLocationScreen';
|
|||||||
export * from './completeProfileScreen';
|
export * from './completeProfileScreen';
|
||||||
export * from './kycScreen';
|
export * from './kycScreen';
|
||||||
export * from './profileScreen';
|
export * from './profileScreen';
|
||||||
|
export * from './addBankDetailsScreen';
|
||||||
|
export * from './bankDetailsScreen';
|
||||||
|
|||||||
@ -39,6 +39,7 @@ export const ProfileScreen: React.FC = () => {
|
|||||||
id: 'bank',
|
id: 'bank',
|
||||||
title: 'Bank Details',
|
title: 'Bank Details',
|
||||||
icon: <ClipboardIcon size={20} color={colors.primary} />,
|
icon: <ClipboardIcon size={20} color={colors.primary} />,
|
||||||
|
route: RouteNames.BankDetails,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'emergency',
|
id: 'emergency',
|
||||||
@ -54,7 +55,7 @@ export const ProfileScreen: React.FC = () => {
|
|||||||
|
|
||||||
const handleMenuPress = (item: any) => {
|
const handleMenuPress = (item: any) => {
|
||||||
if (item.route) {
|
if (item.route) {
|
||||||
navigation.navigate(RouteNames.Documents);
|
navigation.navigate(item.route);
|
||||||
} else {
|
} else {
|
||||||
Alert.alert('Menu Option', `Viewing ${item.title} section...`);
|
Alert.alert('Menu Option', `Viewing ${item.title} section...`);
|
||||||
}
|
}
|
||||||
|
|||||||
27
app/interfaces/bankDetails.ts
Normal file
27
app/interfaces/bankDetails.ts
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
export interface AddBankDetailsPayload {
|
||||||
|
accountHolderName: string;
|
||||||
|
bankName: string;
|
||||||
|
accountNumber: string;
|
||||||
|
ifscCode: string;
|
||||||
|
branchName?: string;
|
||||||
|
isDefault?: boolean;
|
||||||
|
passbookImage?: string; // FormData file (React Native image)
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BankAccount {
|
||||||
|
id: string;
|
||||||
|
ownerType: string;
|
||||||
|
ownerId: string;
|
||||||
|
accountHolderName: string;
|
||||||
|
bankName: string;
|
||||||
|
accountNumber: string;
|
||||||
|
ifscCode: string;
|
||||||
|
branchName: string;
|
||||||
|
isDefault: boolean;
|
||||||
|
isVerified: boolean;
|
||||||
|
passbookImage: string;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type GetBankAccountsResponse = BankAccount[];
|
||||||
@ -5,3 +5,4 @@ export * from './dashboard';
|
|||||||
export * from './delivery';
|
export * from './delivery';
|
||||||
export * from './order';
|
export * from './order';
|
||||||
export * from './accountInfo';
|
export * from './accountInfo';
|
||||||
|
export * from './bankDetails';
|
||||||
|
|||||||
@ -13,6 +13,8 @@ import {
|
|||||||
DeliverOrderScreen,
|
DeliverOrderScreen,
|
||||||
DeliveryCompletedScreen,
|
DeliveryCompletedScreen,
|
||||||
DocumentsScreen,
|
DocumentsScreen,
|
||||||
|
AddBankDetailsScreen,
|
||||||
|
BankDetailsScreen,
|
||||||
} from '@features/screens';
|
} from '@features/screens';
|
||||||
import { RouteNames } from '@utils/constants';
|
import { RouteNames } from '@utils/constants';
|
||||||
|
|
||||||
@ -62,6 +64,14 @@ const AppStack: React.FC = () => {
|
|||||||
component={DeliveryCompletedScreen}
|
component={DeliveryCompletedScreen}
|
||||||
/>
|
/>
|
||||||
<Stack.Screen name={RouteNames.Documents} component={DocumentsScreen} />
|
<Stack.Screen name={RouteNames.Documents} component={DocumentsScreen} />
|
||||||
|
<Stack.Screen
|
||||||
|
name={RouteNames.BankDetails}
|
||||||
|
component={BankDetailsScreen}
|
||||||
|
/>
|
||||||
|
<Stack.Screen
|
||||||
|
name={RouteNames.AddBankDetails}
|
||||||
|
component={AddBankDetailsScreen}
|
||||||
|
/>
|
||||||
</Stack.Navigator>
|
</Stack.Navigator>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -36,6 +36,8 @@ export type AppStackParamList = {
|
|||||||
[RouteNames.DeliverOrder]: { jobId: string };
|
[RouteNames.DeliverOrder]: { jobId: string };
|
||||||
[RouteNames.DeliveryCompleted]: undefined;
|
[RouteNames.DeliveryCompleted]: undefined;
|
||||||
[RouteNames.Documents]: undefined;
|
[RouteNames.Documents]: undefined;
|
||||||
|
[RouteNames.AddBankDetails]: undefined;
|
||||||
|
[RouteNames.BankDetails]: undefined;
|
||||||
// [RouteNames.JobDetails]: { jobId: string };
|
// [RouteNames.JobDetails]: { jobId: string };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -8,6 +8,7 @@ import {
|
|||||||
completeProfileReducer,
|
completeProfileReducer,
|
||||||
kycReducer,
|
kycReducer,
|
||||||
profileReducer,
|
profileReducer,
|
||||||
|
addBankDetailsReducer,
|
||||||
} from '@features/screens';
|
} from '@features/screens';
|
||||||
import { onBoardingCompleteReducer } from '@features/screens/onboardingCompleteScreen';
|
import { onBoardingCompleteReducer } from '@features/screens/onboardingCompleteScreen';
|
||||||
import { dashBoardReducer } from '@features/screens/dashboardScreen';
|
import { dashBoardReducer } from '@features/screens/dashboardScreen';
|
||||||
@ -25,6 +26,7 @@ const rootReducer = combineReducers({
|
|||||||
dashboard: dashBoardReducer,
|
dashboard: dashBoardReducer,
|
||||||
deliveries: activeDeliveriesReducer,
|
deliveries: activeDeliveriesReducer,
|
||||||
accountInfo: profileReducer,
|
accountInfo: profileReducer,
|
||||||
|
addBankDetails: addBankDetailsReducer,
|
||||||
});
|
});
|
||||||
|
|
||||||
export type RootState = ReturnType<typeof rootReducer>;
|
export type RootState = ReturnType<typeof rootReducer>;
|
||||||
|
|||||||
@ -30,6 +30,8 @@ export enum RouteNames {
|
|||||||
DeliveryCompleted = 'DeliveryCompleted',
|
DeliveryCompleted = 'DeliveryCompleted',
|
||||||
Documents = 'Documents',
|
Documents = 'Documents',
|
||||||
Kyc = 'Kyc',
|
Kyc = 'Kyc',
|
||||||
|
AddBankDetails = 'AddBankDetails',
|
||||||
|
BankDetails = 'BankDetails',
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum JobStatus {
|
export enum JobStatus {
|
||||||
|
|||||||
@ -9,3 +9,9 @@ export const isPdf = (fileNameOrUrl?: string) => {
|
|||||||
const clean = fileNameOrUrl.toLowerCase().split('?')[0];
|
const clean = fileNameOrUrl.toLowerCase().split('?')[0];
|
||||||
return clean.endsWith('.pdf');
|
return clean.endsWith('.pdf');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const maskAccountNumber = (num: string): string => {
|
||||||
|
if (!num || num.length <= 4) return num;
|
||||||
|
const masked = '•'.repeat(num.length - 4);
|
||||||
|
return masked + num.slice(-4);
|
||||||
|
};
|
||||||
|
|||||||
@ -16,6 +16,7 @@
|
|||||||
"@services": ["./app/services"],
|
"@services": ["./app/services"],
|
||||||
"@services/*": ["./app/services/*"],
|
"@services/*": ["./app/services/*"],
|
||||||
"@hooks/*": ["./app/hooks/*"],
|
"@hooks/*": ["./app/hooks/*"],
|
||||||
|
"@utils": ["./app/utils"],
|
||||||
"@utils/*": ["./app/utils/*"],
|
"@utils/*": ["./app/utils/*"],
|
||||||
"@icons": ["./app/components/icons"],
|
"@icons": ["./app/components/icons"],
|
||||||
"@icons/*": ["./app/components/icons/*"],
|
"@icons/*": ["./app/components/icons/*"],
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user