feat: implement bank account management module including screens, API integration, and navigation configuration

This commit is contained in:
Tamojit Biswas 2026-07-21 15:41:03 +05:30
parent d9766b842c
commit 65f685d5cf
20 changed files with 1323 additions and 1 deletions

52
app/api/bankDetailsApi.ts Normal file
View 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');
};

View File

@ -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';

View File

@ -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,
},
});

View File

@ -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;

View File

@ -0,0 +1,3 @@
export * from './addBankDetailsScreen';
export * from './thunk';
export * from './reducer';

View 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;
}),
);

View 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);
}
});

View File

@ -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,
},
});

View 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;

View File

@ -0,0 +1 @@
export * from './bankDetailsScreen';

View File

@ -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';

View File

@ -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...`);
} }

View 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[];

View File

@ -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';

View File

@ -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>
); );
}; };

View File

@ -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 };
}; };

View File

@ -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>;

View File

@ -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 {

View File

@ -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);
};

View File

@ -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/*"],