feat: implement wallet feature with transaction history and navigation integration
This commit is contained in:
parent
bed28f2310
commit
40b21a4446
@ -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');
|
||||
};
|
||||
|
||||
@ -48,6 +48,7 @@ const MENU_SECTIONS: MenuSectionData[] = [
|
||||
label: 'Wallet',
|
||||
subLabel: 'Cards, UPI & wallets',
|
||||
tint: '#FFF4E5',
|
||||
onPress: navigation => navigation.navigate('WalletScreen'),
|
||||
},
|
||||
{
|
||||
icon: '🔔',
|
||||
|
||||
@ -21,3 +21,4 @@ export * from './helpSupportScreen';
|
||||
export * from './accountScreen';
|
||||
export * from './writeReviewScreen';
|
||||
export * from './orderDetailsScreen';
|
||||
export * from './walletScreen';
|
||||
|
||||
3
app/features/screens/walletScreen/index.ts
Normal file
3
app/features/screens/walletScreen/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export * from './walletScreen';
|
||||
export * from './reducer';
|
||||
export * from './thunk';
|
||||
30
app/features/screens/walletScreen/reducer.ts
Normal file
30
app/features/screens/walletScreen/reducer.ts
Normal 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;
|
||||
});
|
||||
});
|
||||
14
app/features/screens/walletScreen/thunk.ts
Normal file
14
app/features/screens/walletScreen/thunk.ts
Normal 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);
|
||||
}
|
||||
},
|
||||
);
|
||||
128
app/features/screens/walletScreen/walletScreen.styles.ts
Normal file
128
app/features/screens/walletScreen/walletScreen.styles.ts
Normal 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,
|
||||
},
|
||||
});
|
||||
149
app/features/screens/walletScreen/walletScreen.tsx
Normal file
149
app/features/screens/walletScreen/walletScreen.tsx
Normal 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;
|
||||
@ -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
24
app/interfaces/wallet.ts
Normal 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';
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@ -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>;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user