diff --git a/app/api/bankDetailsApi.ts b/app/api/bankDetailsApi.ts new file mode 100644 index 0000000..6ed5354 --- /dev/null +++ b/app/api/bankDetailsApi.ts @@ -0,0 +1,52 @@ +import { AddBankDetailsPayload, GetBankAccountsResponse } from '@interfaces'; +import { apiClient } from '@services'; + +export const addAccountApi = async ( + payload: AddBankDetailsPayload | FormData, +): Promise => { + 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('/bank-accounts', body); +}; + +export const fetchBankAccountsApi = + async (): Promise => { + return await apiClient.get('/bank-accounts'); + }; diff --git a/app/api/index.ts b/app/api/index.ts index cb16e59..d9c31d3 100644 --- a/app/api/index.ts +++ b/app/api/index.ts @@ -4,3 +4,4 @@ export * from './kycApi'; export * from './dashboardApi'; export * from './deliveryApi'; export * from './accountApi'; +export * from './bankDetailsApi'; diff --git a/app/features/screens/addBankDetailsScreen/addBankDetailsScreen.styles.ts b/app/features/screens/addBankDetailsScreen/addBankDetailsScreen.styles.ts new file mode 100644 index 0000000..477a4b3 --- /dev/null +++ b/app/features/screens/addBankDetailsScreen/addBankDetailsScreen.styles.ts @@ -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, + }, + }); diff --git a/app/features/screens/addBankDetailsScreen/addBankDetailsScreen.tsx b/app/features/screens/addBankDetailsScreen/addBankDetailsScreen.tsx new file mode 100644 index 0000000..6ace1be --- /dev/null +++ b/app/features/screens/addBankDetailsScreen/addBankDetailsScreen.tsx @@ -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>({}); + + 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 = {}; + + 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 ( + + navigation.goBack()} + /> + + + {/* Info Header Banner */} + + + + + + Payout Account Details + + Enter your official bank details to receive weekly delivery earnings and automated payouts. + + + + + {/* Error Banner */} + {error && ( + + {error} + + )} + + {/* Form Fields Card */} + + {/* Account Holder Name */} + + + Account Holder Name + * + + { + setAccountHolderName(text); + if (errors.accountHolderName) { + setErrors(prev => ({ ...prev, accountHolderName: '' })); + } + }} + autoCapitalize="words" + /> + {errors.accountHolderName && ( + {errors.accountHolderName} + )} + + + {/* Bank Name */} + + + Bank Name + * + + { + setBankName(text); + if (errors.bankName) { + setErrors(prev => ({ ...prev, bankName: '' })); + } + }} + autoCapitalize="words" + /> + {errors.bankName && ( + {errors.bankName} + )} + + + {/* Account Number */} + + + Account Number + * + + { + setAccountNumber(text); + if (errors.accountNumber) { + setErrors(prev => ({ ...prev, accountNumber: '' })); + } + }} + keyboardType="number-pad" + /> + {errors.accountNumber && ( + {errors.accountNumber} + )} + + + {/* IFSC Code */} + + + IFSC Code + * + + { + setIfscCode(text.toUpperCase()); + if (errors.ifscCode) { + setErrors(prev => ({ ...prev, ifscCode: '' })); + } + }} + autoCapitalize="characters" + /> + {errors.ifscCode && ( + {errors.ifscCode} + )} + + + {/* Branch Name (Optional) */} + + + Branch Name (Optional) + + + + + {/* Default Account Toggle */} + + + Default Payout Account + + Mark as default bank account for payouts + + + + + + {/* Passbook / Cancelled Cheque Image (Optional) */} + + + + Passbook / Cancelled Cheque (Optional) + + + + + {passbookImage ? ( + + + {passbookImage.type.startsWith('image/') && ( + + )} + + + {passbookImage.name} + + Attached document + + + + Remove + + + ) : ( + <> + + + + Choose Passbook / Cheque File + Supports JPG, PNG, or PDF format + + )} + + + + {/* Submit Button */} + + {isLoading ? ( + + ) : ( + Save Bank Account + )} + + + + + ); +}; + +export default AddBankDetailsScreen; diff --git a/app/features/screens/addBankDetailsScreen/index.ts b/app/features/screens/addBankDetailsScreen/index.ts new file mode 100644 index 0000000..d25275f --- /dev/null +++ b/app/features/screens/addBankDetailsScreen/index.ts @@ -0,0 +1,3 @@ +export * from './addBankDetailsScreen'; +export * from './thunk'; +export * from './reducer'; diff --git a/app/features/screens/addBankDetailsScreen/reducer.ts b/app/features/screens/addBankDetailsScreen/reducer.ts new file mode 100644 index 0000000..8e2842f --- /dev/null +++ b/app/features/screens/addBankDetailsScreen/reducer.ts @@ -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; + }), +); diff --git a/app/features/screens/addBankDetailsScreen/thunk.ts b/app/features/screens/addBankDetailsScreen/thunk.ts new file mode 100644 index 0000000..2abaec1 --- /dev/null +++ b/app/features/screens/addBankDetailsScreen/thunk.ts @@ -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); + } +}); diff --git a/app/features/screens/bankDetailsScreen/bankDetailsScreen.styles.ts b/app/features/screens/bankDetailsScreen/bankDetailsScreen.styles.ts new file mode 100644 index 0000000..539e7cc --- /dev/null +++ b/app/features/screens/bankDetailsScreen/bankDetailsScreen.styles.ts @@ -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, + }, + }); diff --git a/app/features/screens/bankDetailsScreen/bankDetailsScreen.tsx b/app/features/screens/bankDetailsScreen/bankDetailsScreen.tsx new file mode 100644 index 0000000..89ed361 --- /dev/null +++ b/app/features/screens/bankDetailsScreen/bankDetailsScreen.tsx @@ -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>(); + const dispatch = useAppDispatch(); + const { bankAccounts, isLoading, error } = useAppSelector( + state => state.addBankDetails, + ); + + // Replace this state with Redux selector when wiring real API + // const [accounts] = useState(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 ( + + navigation.goBack()} + /> + + + } + > + {/* Summary Banner */} + + + + + + Payout Accounts + + Manage your bank accounts for receiving earnings + + + + + {/* Empty State */} + {bankAccounts?.length === 0 ? ( + + + + + No Bank Accounts + + Add your bank account to start receiving your delivery earnings + directly. + + + ) : ( + <> + {/* Section Header */} + + Your Accounts + + + {bankAccounts?.length} + + + + + {/* Account Cards */} + {bankAccounts?.map(account => ( + + {/* Top color strip for default account */} + {account.isDefault && } + + + {/* Card Header Row */} + + + + + + + + {account.bankName} + + + {account.accountHolderName} + + + + + {/* Status Badges */} + + {account.isDefault && ( + + DEFAULT + + )} + + + {account.isVerified ? 'VERIFIED' : 'PENDING'} + + + + + + + + {/* Fields Grid */} + + + Account Number + + {maskAccountNumber(account.accountNumber)} + + + + + IFSC Code + + {account.ifscCode || '—'} + + + + {account.branchName ? ( + + Branch + + {account.branchName} + + + ) : null} + + + Owner Type + + {account.ownerType || '—'} + + + + + + + {/* Card Footer */} + + + Added {formatDate(account.createdAt)} + + {account.isVerified && ( + + )} + + + + ))} + + )} + + {/* Add Bank Account Button */} + + + + + + Add Bank Account + + + + ); +}; + +export default BankDetailsScreen; diff --git a/app/features/screens/bankDetailsScreen/index.ts b/app/features/screens/bankDetailsScreen/index.ts new file mode 100644 index 0000000..fe28fd3 --- /dev/null +++ b/app/features/screens/bankDetailsScreen/index.ts @@ -0,0 +1 @@ +export * from './bankDetailsScreen'; diff --git a/app/features/screens/index.ts b/app/features/screens/index.ts index 85b874d..b82fb94 100644 --- a/app/features/screens/index.ts +++ b/app/features/screens/index.ts @@ -20,3 +20,5 @@ export * from './setLocationScreen'; export * from './completeProfileScreen'; export * from './kycScreen'; export * from './profileScreen'; +export * from './addBankDetailsScreen'; +export * from './bankDetailsScreen'; diff --git a/app/features/screens/profileScreen/profileScreen.tsx b/app/features/screens/profileScreen/profileScreen.tsx index 7b742bf..e25fbf9 100644 --- a/app/features/screens/profileScreen/profileScreen.tsx +++ b/app/features/screens/profileScreen/profileScreen.tsx @@ -39,6 +39,7 @@ export const ProfileScreen: React.FC = () => { id: 'bank', title: 'Bank Details', icon: , + route: RouteNames.BankDetails, }, { id: 'emergency', @@ -54,7 +55,7 @@ export const ProfileScreen: React.FC = () => { const handleMenuPress = (item: any) => { if (item.route) { - navigation.navigate(RouteNames.Documents); + navigation.navigate(item.route); } else { Alert.alert('Menu Option', `Viewing ${item.title} section...`); } diff --git a/app/interfaces/bankDetails.ts b/app/interfaces/bankDetails.ts new file mode 100644 index 0000000..0828db1 --- /dev/null +++ b/app/interfaces/bankDetails.ts @@ -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[]; diff --git a/app/interfaces/index.ts b/app/interfaces/index.ts index a2bd8fe..214aa68 100644 --- a/app/interfaces/index.ts +++ b/app/interfaces/index.ts @@ -5,3 +5,4 @@ export * from './dashboard'; export * from './delivery'; export * from './order'; export * from './accountInfo'; +export * from './bankDetails'; diff --git a/app/navigation/appStack.tsx b/app/navigation/appStack.tsx index 3b81b17..c997e2e 100644 --- a/app/navigation/appStack.tsx +++ b/app/navigation/appStack.tsx @@ -13,6 +13,8 @@ import { DeliverOrderScreen, DeliveryCompletedScreen, DocumentsScreen, + AddBankDetailsScreen, + BankDetailsScreen, } from '@features/screens'; import { RouteNames } from '@utils/constants'; @@ -62,6 +64,14 @@ const AppStack: React.FC = () => { component={DeliveryCompletedScreen} /> + + ); }; diff --git a/app/navigation/navigationTypes.ts b/app/navigation/navigationTypes.ts index 09022f6..66f9f77 100644 --- a/app/navigation/navigationTypes.ts +++ b/app/navigation/navigationTypes.ts @@ -36,6 +36,8 @@ export type AppStackParamList = { [RouteNames.DeliverOrder]: { jobId: string }; [RouteNames.DeliveryCompleted]: undefined; [RouteNames.Documents]: undefined; + [RouteNames.AddBankDetails]: undefined; + [RouteNames.BankDetails]: undefined; // [RouteNames.JobDetails]: { jobId: string }; }; diff --git a/app/store/rootReducer.ts b/app/store/rootReducer.ts index e9cb52b..17ed65e 100644 --- a/app/store/rootReducer.ts +++ b/app/store/rootReducer.ts @@ -8,6 +8,7 @@ import { completeProfileReducer, kycReducer, profileReducer, + addBankDetailsReducer, } from '@features/screens'; import { onBoardingCompleteReducer } from '@features/screens/onboardingCompleteScreen'; import { dashBoardReducer } from '@features/screens/dashboardScreen'; @@ -25,6 +26,7 @@ const rootReducer = combineReducers({ dashboard: dashBoardReducer, deliveries: activeDeliveriesReducer, accountInfo: profileReducer, + addBankDetails: addBankDetailsReducer, }); export type RootState = ReturnType; diff --git a/app/utils/constants.ts b/app/utils/constants.ts index fd6e50d..b496a5c 100644 --- a/app/utils/constants.ts +++ b/app/utils/constants.ts @@ -30,6 +30,8 @@ export enum RouteNames { DeliveryCompleted = 'DeliveryCompleted', Documents = 'Documents', Kyc = 'Kyc', + AddBankDetails = 'AddBankDetails', + BankDetails = 'BankDetails', } export enum JobStatus { diff --git a/app/utils/helper.ts b/app/utils/helper.ts index 48e7216..80c025d 100644 --- a/app/utils/helper.ts +++ b/app/utils/helper.ts @@ -9,3 +9,9 @@ export const isPdf = (fileNameOrUrl?: string) => { const clean = fileNameOrUrl.toLowerCase().split('?')[0]; 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); +}; diff --git a/tsconfig.json b/tsconfig.json index 405df1a..49a66cf 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -16,6 +16,7 @@ "@services": ["./app/services"], "@services/*": ["./app/services/*"], "@hooks/*": ["./app/hooks/*"], + "@utils": ["./app/utils"], "@utils/*": ["./app/utils/*"], "@icons": ["./app/components/icons"], "@icons/*": ["./app/components/icons/*"],