feat: implement wallet feature with transaction history and navigation integration

This commit is contained in:
Tamojit Biswas 2026-07-20 18:55:35 +05:30
parent bed28f2310
commit 40b21a4446
12 changed files with 369 additions and 2 deletions

View File

@ -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<WalletResponse> => {
return await apiClient.get<WalletResponse>('/wallets/balance');
};
export const getWalletTransactions = async (): Promise<WalletTransaction[]> => {
return await apiClient.get<WalletTransaction[]>('/wallets/transactions');
};

View File

@ -48,6 +48,7 @@ const MENU_SECTIONS: MenuSectionData[] = [
label: 'Wallet',
subLabel: 'Cards, UPI & wallets',
tint: '#FFF4E5',
onPress: navigation => navigation.navigate('WalletScreen'),
},
{
icon: '🔔',

View File

@ -21,3 +21,4 @@ export * from './helpSupportScreen';
export * from './accountScreen';
export * from './writeReviewScreen';
export * from './orderDetailsScreen';
export * from './walletScreen';

View File

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

View File

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

View File

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

View File

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

View File

@ -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 (
<View style={styles.transactionCard}>
<View
style={[
styles.iconContainer,
{
backgroundColor: isCredit ? '#DFF7E4' : '#FFE5E5',
},
]}
>
{/* <Icon
name={isCredit ? 'arrow-bottom-left' : 'arrow-top-right'}
size={22}
color={isCredit ? '#18A558' : '#F04438'}
/> */}
</View>
<View style={styles.transactionInfo}>
<Text style={styles.transactionTitle}>{item.type}</Text>
<Text style={styles.transactionRef}>{item.transactionReference}</Text>
<Text style={styles.transactionDate}>
{new Date(item.createdAt).toLocaleString()}
</Text>
</View>
<View style={styles.amountContainer}>
<Text
style={[
styles.amount,
{
color: isCredit ? '#18A558' : '#F04438',
},
]}
>
{isCredit ? '+' : '-'}{item.amount}
</Text>
<Text style={styles.balanceText}>Bal {item.balanceAfter}</Text>
</View>
</View>
);
};
return (
<SafeAreaView style={styles.container}>
<Header title="Wallet" />
<View style={styles.balanceCard}>
<Text style={styles.balanceLabel}>Available Balance</Text>
<Text style={styles.balance}>{wallet?.balance.toFixed(2)}</Text>
<TouchableOpacity style={styles.addMoneyButton}>
{/* <Icon name="plus" color="#FFF" size={20} /> */}
<Text style={styles.addMoneyText}>Add Money</Text>
</TouchableOpacity>
</View>
<Text style={styles.sectionTitle}>Recent Transactions</Text>
<FlatList
data={walletTransactions}
keyExtractor={item => item.id}
renderItem={renderItem}
showsVerticalScrollIndicator={false}
contentContainerStyle={styles.listContainer}
ListEmptyComponent={
<View style={styles.emptyContainer}>
{/* <Icon name="wallet-outline" size={70} color="#BDBDBD" /> */}
<Text style={styles.emptyText}>No Transactions Yet</Text>
</View>
}
/>
</SafeAreaView>
);
};
export default WalletScreen;

View File

@ -98,3 +98,4 @@ export * from './customerProfile';
export * from './order';
export * from './offers';
export * from './productRating';
export * from './wallet';

24
app/interfaces/wallet.ts Normal file
View File

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

View File

@ -16,6 +16,7 @@ import {
WriteReviewScreen,
OrderDetailsScreen,
} from '@features/screens';
import WalletScreen from '@features/screens/walletScreen/walletScreen';
export type AppStackParamList = {
MainTabs: NavigatorScreenParams<MainTabParamList> | undefined;
@ -31,6 +32,7 @@ export type AppStackParamList = {
HelpSupportScreen: undefined;
WriteReviewScreen: { productId: string } | undefined;
OrderDetailsScreen: { orderId: string } | undefined;
WalletScreen: undefined;
};
const Stack = createStackNavigator<AppStackParamList>();
@ -69,6 +71,7 @@ export const AppStack: React.FC = () => {
<Stack.Screen name="HelpSupportScreen" component={HelpSupportScreen} />
<Stack.Screen name="WriteReviewScreen" component={WriteReviewScreen} />
<Stack.Screen name="OrderDetailsScreen" component={OrderDetailsScreen} />
<Stack.Screen name="WalletScreen" component={WalletScreen} />
</Stack.Navigator>
);
};

View File

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