From 40b21a4446a3e114926298fa970cbe7befaed3eb Mon Sep 17 00:00:00 2001 From: Tamojit Biswas Date: Mon, 20 Jul 2026 18:55:35 +0530 Subject: [PATCH] feat: implement wallet feature with transaction history and navigation integration --- app/api/customerDetailsApi.ts | 10 +- .../screens/accountScreen/accountScreen.tsx | 1 + app/features/screens/index.ts | 1 + app/features/screens/walletScreen/index.ts | 3 + app/features/screens/walletScreen/reducer.ts | 30 ++++ app/features/screens/walletScreen/thunk.ts | 14 ++ .../walletScreen/walletScreen.styles.ts | 128 +++++++++++++++ .../screens/walletScreen/walletScreen.tsx | 149 ++++++++++++++++++ app/interfaces/index.ts | 1 + app/interfaces/wallet.ts | 24 +++ app/navigation/appStack.tsx | 3 + app/store/rootReducer.ts | 7 +- 12 files changed, 369 insertions(+), 2 deletions(-) create mode 100644 app/features/screens/walletScreen/index.ts create mode 100644 app/features/screens/walletScreen/reducer.ts create mode 100644 app/features/screens/walletScreen/thunk.ts create mode 100644 app/features/screens/walletScreen/walletScreen.styles.ts create mode 100644 app/features/screens/walletScreen/walletScreen.tsx create mode 100644 app/interfaces/wallet.ts diff --git a/app/api/customerDetailsApi.ts b/app/api/customerDetailsApi.ts index ffcc7d0..d04a848 100644 --- a/app/api/customerDetailsApi.ts +++ b/app/api/customerDetailsApi.ts @@ -1,4 +1,8 @@ -import { CustomerResponse, WalletResponse } from '@interfaces'; +import { + CustomerResponse, + WalletResponse, + WalletTransaction, +} from '@interfaces'; import { apiClient } from '@services'; export const getCustomerDetails = async () => { @@ -8,3 +12,7 @@ export const getCustomerDetails = async () => { export const getWalletDetails = async (): Promise => { return await apiClient.get('/wallets/balance'); }; + +export const getWalletTransactions = async (): Promise => { + return await apiClient.get('/wallets/transactions'); +}; diff --git a/app/features/screens/accountScreen/accountScreen.tsx b/app/features/screens/accountScreen/accountScreen.tsx index 24b5dc6..a34557b 100644 --- a/app/features/screens/accountScreen/accountScreen.tsx +++ b/app/features/screens/accountScreen/accountScreen.tsx @@ -48,6 +48,7 @@ const MENU_SECTIONS: MenuSectionData[] = [ label: 'Wallet', subLabel: 'Cards, UPI & wallets', tint: '#FFF4E5', + onPress: navigation => navigation.navigate('WalletScreen'), }, { icon: '🔔', diff --git a/app/features/screens/index.ts b/app/features/screens/index.ts index a267f9e..fe8b560 100644 --- a/app/features/screens/index.ts +++ b/app/features/screens/index.ts @@ -21,3 +21,4 @@ export * from './helpSupportScreen'; export * from './accountScreen'; export * from './writeReviewScreen'; export * from './orderDetailsScreen'; +export * from './walletScreen'; diff --git a/app/features/screens/walletScreen/index.ts b/app/features/screens/walletScreen/index.ts new file mode 100644 index 0000000..0de1788 --- /dev/null +++ b/app/features/screens/walletScreen/index.ts @@ -0,0 +1,3 @@ +export * from './walletScreen'; +export * from './reducer'; +export * from './thunk'; diff --git a/app/features/screens/walletScreen/reducer.ts b/app/features/screens/walletScreen/reducer.ts new file mode 100644 index 0000000..426a82c --- /dev/null +++ b/app/features/screens/walletScreen/reducer.ts @@ -0,0 +1,30 @@ +import { WalletTransaction } from '@interfaces'; +import { createReducer } from '@reduxjs/toolkit'; +import { getAllWalletTransactions } from './thunk'; + +export interface walletState { + walletTransactions: WalletTransaction[]; + loading: boolean; + error: string | null; +} + +const initialState: walletState = { + walletTransactions: [], + loading: false, + error: null, +}; + +export const walletReducer = createReducer(initialState, builder => { + builder.addCase(getAllWalletTransactions.pending, state => { + state.loading = true; + state.error = null; + }); + builder.addCase(getAllWalletTransactions.fulfilled, (state, action) => { + state.loading = false; + state.walletTransactions = action.payload; + }); + builder.addCase(getAllWalletTransactions.rejected, (state, action) => { + state.loading = false; + state.error = action.payload as string | null; + }); +}); diff --git a/app/features/screens/walletScreen/thunk.ts b/app/features/screens/walletScreen/thunk.ts new file mode 100644 index 0000000..88930d9 --- /dev/null +++ b/app/features/screens/walletScreen/thunk.ts @@ -0,0 +1,14 @@ +import { getWalletTransactions } from '@api'; +import { createAsyncThunk } from '@reduxjs/toolkit'; + +export const getAllWalletTransactions = createAsyncThunk( + 'wallet/getAllWalletTransactions', + async (_, { rejectWithValue }) => { + try { + const response = await getWalletTransactions(); + return response; + } catch (error: any) { + return rejectWithValue(error.response.data); + } + }, +); diff --git a/app/features/screens/walletScreen/walletScreen.styles.ts b/app/features/screens/walletScreen/walletScreen.styles.ts new file mode 100644 index 0000000..83762c6 --- /dev/null +++ b/app/features/screens/walletScreen/walletScreen.styles.ts @@ -0,0 +1,128 @@ +import { StyleSheet } from 'react-native'; + +export const getStyles = (theme: any) => + StyleSheet.create({ + container: { + flex: 1, + backgroundColor: '#F6F7FB', + }, + + balanceCard: { + margin: 16, + borderRadius: 18, + padding: 20, + backgroundColor: '#2F80ED', + }, + + balanceLabel: { + color: '#E6F0FF', + fontSize: 15, + }, + + balance: { + fontSize: 34, + fontWeight: '700', + color: '#FFF', + marginTop: 8, + }, + + addMoneyButton: { + marginTop: 20, + backgroundColor: '#FFF', + height: 48, + borderRadius: 12, + justifyContent: 'center', + alignItems: 'center', + flexDirection: 'row', + }, + + addMoneyText: { + color: '#2F80ED', + fontWeight: '700', + fontSize: 16, + marginLeft: 6, + }, + + sectionTitle: { + fontSize: 18, + fontWeight: '700', + marginHorizontal: 16, + marginBottom: 10, + color: '#222', + }, + + listContainer: { + paddingHorizontal: 16, + paddingBottom: 20, + }, + + transactionCard: { + backgroundColor: '#FFF', + borderRadius: 14, + padding: 15, + flexDirection: 'row', + alignItems: 'center', + marginBottom: 12, + elevation: 2, + shadowOpacity: 0.08, + shadowRadius: 5, + shadowOffset: { width: 0, height: 2 }, + }, + + iconContainer: { + width: 48, + height: 48, + borderRadius: 24, + justifyContent: 'center', + alignItems: 'center', + }, + + transactionInfo: { + flex: 1, + marginLeft: 14, + }, + + transactionTitle: { + fontWeight: '700', + fontSize: 15, + color: '#222', + }, + + transactionRef: { + color: '#777', + fontSize: 12, + marginTop: 2, + }, + + transactionDate: { + color: '#999', + fontSize: 12, + marginTop: 4, + }, + + amountContainer: { + alignItems: 'flex-end', + }, + + amount: { + fontWeight: '700', + fontSize: 16, + }, + + balanceText: { + color: '#888', + fontSize: 12, + marginTop: 4, + }, + + emptyContainer: { + alignItems: 'center', + marginTop: 80, + }, + + emptyText: { + marginTop: 12, + color: '#888', + fontSize: 16, + }, + }); diff --git a/app/features/screens/walletScreen/walletScreen.tsx b/app/features/screens/walletScreen/walletScreen.tsx new file mode 100644 index 0000000..4d302f7 --- /dev/null +++ b/app/features/screens/walletScreen/walletScreen.tsx @@ -0,0 +1,149 @@ +import React, { useCallback, useEffect } from 'react'; +import { + View, + Text, + SafeAreaView, + FlatList, + TouchableOpacity, +} from 'react-native'; +// import Icon from 'react-native-vector-icons/MaterialCommunityIcons'; +import { Header } from '@components'; +import { useAppTheme } from '@theme'; +import { getStyles } from './walletScreen.styles'; +import { WalletTransaction } from '@interfaces'; +import { useAppDispatch, useAppSelector } from '@store'; +import { getAllWalletTransactions } from './thunk'; +import { useFocusEffect } from '@react-navigation/native'; +import { getWalletBalanceThunk } from '../accountScreen'; + +// const balance = 17496.05; + +// const transactions: WalletTransaction[] = [ +// { +// id: '1', +// walletId: '', +// transactionReference: 'TXN-1784543790637-2387', +// direction: 'OUT', +// amount: '1408.95', +// balanceBefore: '18905', +// balanceAfter: '17496.05', +// type: 'PAYMENT', +// referenceId: null, +// description: null, +// idempotencyKey: null, +// createdAt: '2026-07-20T10:36:30.639Z', +// }, +// { +// id: '2', +// walletId: '', +// transactionReference: 'TXN-1784199810512-8502', +// direction: 'OUT', +// amount: '1095', +// balanceBefore: '20000', +// balanceAfter: '18905', +// type: 'PAYMENT', +// referenceId: null, +// description: null, +// idempotencyKey: null, +// createdAt: '2026-07-16T11:03:30.515Z', +// }, +// ]; + +const WalletScreen = () => { + const { colors } = useAppTheme(); + const styles = getStyles(colors); + + const dispatch = useAppDispatch(); + const { walletTransactions } = useAppSelector(state => state.wallet); + const { wallet } = useAppSelector(state => state.account); + + useFocusEffect( + useCallback(() => { + dispatch(getAllWalletTransactions()); + dispatch(getWalletBalanceThunk()); + }, []), + ); + + const renderItem = ({ item }: { item: WalletTransaction }) => { + const isCredit = item.direction === 'IN'; + + return ( + + + {/* */} + + + + {item.type} + + {item.transactionReference} + + + {new Date(item.createdAt).toLocaleString()} + + + + + + {isCredit ? '+' : '-'}₹{item.amount} + + + Bal ₹{item.balanceAfter} + + + ); + }; + + return ( + +
+ + + Available Balance + + ₹{wallet?.balance.toFixed(2)} + + + {/* */} + Add Money + + + + Recent Transactions + + item.id} + renderItem={renderItem} + showsVerticalScrollIndicator={false} + contentContainerStyle={styles.listContainer} + ListEmptyComponent={ + + {/* */} + No Transactions Yet + + } + /> + + ); +}; + +export default WalletScreen; diff --git a/app/interfaces/index.ts b/app/interfaces/index.ts index 0df0dbe..f3ff92f 100644 --- a/app/interfaces/index.ts +++ b/app/interfaces/index.ts @@ -98,3 +98,4 @@ export * from './customerProfile'; export * from './order'; export * from './offers'; export * from './productRating'; +export * from './wallet'; diff --git a/app/interfaces/wallet.ts b/app/interfaces/wallet.ts new file mode 100644 index 0000000..7b4c1ec --- /dev/null +++ b/app/interfaces/wallet.ts @@ -0,0 +1,24 @@ +export interface WalletTransaction { + id: string; + walletId: string; + transactionReference: string; + direction: TransactionDirection; + amount: string; + balanceBefore: string; + balanceAfter: string; + type: TransactionType; + referenceId: string | null; + description: string | null; + idempotencyKey: string | null; + createdAt: string; +} + +export type TransactionDirection = 'IN' | 'OUT'; + +export type TransactionType = + | 'PAYMENT' + | 'REFUND' + | 'TOPUP' + | 'WITHDRAWAL' + | 'TRANSFER' + | 'ADJUSTMENT'; diff --git a/app/navigation/appStack.tsx b/app/navigation/appStack.tsx index 0f32658..8f09c70 100644 --- a/app/navigation/appStack.tsx +++ b/app/navigation/appStack.tsx @@ -16,6 +16,7 @@ import { WriteReviewScreen, OrderDetailsScreen, } from '@features/screens'; +import WalletScreen from '@features/screens/walletScreen/walletScreen'; export type AppStackParamList = { MainTabs: NavigatorScreenParams | undefined; @@ -31,6 +32,7 @@ export type AppStackParamList = { HelpSupportScreen: undefined; WriteReviewScreen: { productId: string } | undefined; OrderDetailsScreen: { orderId: string } | undefined; + WalletScreen: undefined; }; const Stack = createStackNavigator(); @@ -69,6 +71,7 @@ export const AppStack: React.FC = () => { + ); }; diff --git a/app/store/rootReducer.ts b/app/store/rootReducer.ts index 021ece0..763ce73 100644 --- a/app/store/rootReducer.ts +++ b/app/store/rootReducer.ts @@ -11,7 +11,11 @@ import providerDetailsReducer from '@features/screens/providerDetailsScreen/redu import paymentMethodsReducer from '@features/screens/checkoutPaymentScreen/reducer'; import customerProfileReducer from './commonreducers/customerProfile/reducer'; import { offerReducer } from './commonreducers/offer'; -import { accountReducer, orderDetailsReducer } from '@features/screens'; +import { + accountReducer, + orderDetailsReducer, + walletReducer, +} from '@features/screens'; import searchReducer from '@features/screens/searchScreen/reducer'; const rootReducer = combineReducers({ @@ -29,6 +33,7 @@ const rootReducer = combineReducers({ account: accountReducer, search: searchReducer, orderDetails: orderDetailsReducer, + wallet: walletReducer, }); export type RootState = ReturnType;