created transaction list show and create a custom card

This commit is contained in:
Jaanu423
2025-09-25 15:44:58 +05:30
parent 7f95ae73a0
commit fb55686724
10 changed files with 569 additions and 10 deletions
+154
View File
@@ -33,6 +33,7 @@
"react-native-paper": "^5.14.5",
"react-native-safe-area-context": "^5.6.1",
"react-native-screens": "^4.16.0",
"react-native-svg": "^15.13.0",
"react-native-walkthrough-tooltip": "^1.6.0",
"react-redux": "^9.2.0"
},
@@ -4787,10 +4788,17 @@
"devOptional": true,
"license": "MIT"
},
"node_modules/boolbase": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz",
"integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==",
"license": "ISC"
},
"node_modules/brace-expansion": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0"
@@ -5440,6 +5448,47 @@
"node": ">= 8"
}
},
"node_modules/css-select": {
"version": "5.2.2",
"resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz",
"integrity": "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==",
"license": "BSD-2-Clause",
"dependencies": {
"boolbase": "^1.0.0",
"css-what": "^6.1.0",
"domhandler": "^5.0.2",
"domutils": "^3.0.1",
"nth-check": "^2.0.1"
},
"funding": {
"url": "https://github.com/sponsors/fb55"
}
},
"node_modules/css-tree": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.1.3.tgz",
"integrity": "sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q==",
"license": "MIT",
"dependencies": {
"mdn-data": "2.0.14",
"source-map": "^0.6.1"
},
"engines": {
"node": ">=8.0.0"
}
},
"node_modules/css-what": {
"version": "6.2.2",
"resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz",
"integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==",
"license": "BSD-2-Clause",
"engines": {
"node": ">= 6"
},
"funding": {
"url": "https://github.com/sponsors/fb55"
}
},
"node_modules/csstype": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
@@ -5697,6 +5746,61 @@
"node": ">=6.0.0"
}
},
"node_modules/dom-serializer": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz",
"integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==",
"license": "MIT",
"dependencies": {
"domelementtype": "^2.3.0",
"domhandler": "^5.0.2",
"entities": "^4.2.0"
},
"funding": {
"url": "https://github.com/cheeriojs/dom-serializer?sponsor=1"
}
},
"node_modules/domelementtype": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz",
"integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/fb55"
}
],
"license": "BSD-2-Clause"
},
"node_modules/domhandler": {
"version": "5.0.3",
"resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz",
"integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==",
"license": "BSD-2-Clause",
"dependencies": {
"domelementtype": "^2.3.0"
},
"engines": {
"node": ">= 4"
},
"funding": {
"url": "https://github.com/fb55/domhandler?sponsor=1"
}
},
"node_modules/domutils": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz",
"integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==",
"license": "BSD-2-Clause",
"dependencies": {
"dom-serializer": "^2.0.0",
"domelementtype": "^2.3.0",
"domhandler": "^5.0.3"
},
"funding": {
"url": "https://github.com/fb55/domutils?sponsor=1"
}
},
"node_modules/dunder-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
@@ -5751,6 +5855,18 @@
"node": ">= 0.8"
}
},
"node_modules/entities": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
"integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
"license": "BSD-2-Clause",
"engines": {
"node": ">=0.12"
},
"funding": {
"url": "https://github.com/fb55/entities?sponsor=1"
}
},
"node_modules/env-paths": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz",
@@ -8078,6 +8194,7 @@
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
"devOptional": true,
"license": "ISC"
},
"node_modules/istanbul-lib-coverage": {
@@ -8834,6 +8951,7 @@
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz",
"integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==",
"devOptional": true,
"license": "MIT"
},
"node_modules/json-schema-traverse": {
@@ -9255,6 +9373,12 @@
"node": ">= 0.4"
}
},
"node_modules/mdn-data": {
"version": "2.0.14",
"resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.14.tgz",
"integrity": "sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==",
"license": "CC0-1.0"
},
"node_modules/media-typer": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
@@ -9753,6 +9877,7 @@
"version": "9.0.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
"integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
"dev": true,
"license": "ISC",
"dependencies": {
"brace-expansion": "^2.0.1"
@@ -12349,6 +12474,18 @@
"inBundle": true,
"license": "ISC"
},
"node_modules/nth-check": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz",
"integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==",
"license": "BSD-2-Clause",
"dependencies": {
"boolbase": "^1.0.0"
},
"funding": {
"url": "https://github.com/fb55/nth-check?sponsor=1"
}
},
"node_modules/nullthrows": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/nullthrows/-/nullthrows-1.1.1.tgz",
@@ -13429,6 +13566,21 @@
"react-native": "*"
}
},
"node_modules/react-native-svg": {
"version": "15.13.0",
"resolved": "https://registry.npmjs.org/react-native-svg/-/react-native-svg-15.13.0.tgz",
"integrity": "sha512-/YPK+PAAXg4T0x2d2vYPvqqAhOYid2bRKxUVT7STIyd1p2JxWmsGQkfZxXCkEFN7TwLfIyVlT5RimT91Pj/qXw==",
"license": "MIT",
"dependencies": {
"css-select": "^5.1.0",
"css-tree": "^1.1.3",
"warn-once": "0.1.1"
},
"peerDependencies": {
"react": "*",
"react-native": "*"
}
},
"node_modules/react-native-swipe-gestures": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/react-native-swipe-gestures/-/react-native-swipe-gestures-1.0.5.tgz",
@@ -14761,6 +14913,7 @@
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
"integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==",
"dev": true,
"license": "MIT"
},
"node_modules/throat": {
@@ -15219,6 +15372,7 @@
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
"devOptional": true,
"license": "ISC",
"dependencies": {
"isexe": "^2.0.0"
+1
View File
@@ -35,6 +35,7 @@
"react-native-paper": "^5.14.5",
"react-native-safe-area-context": "^5.6.1",
"react-native-screens": "^4.16.0",
"react-native-svg": "^15.13.0",
"react-native-walkthrough-tooltip": "^1.6.0",
"react-redux": "^9.2.0"
},
+188
View File
@@ -0,0 +1,188 @@
import React, { useRef } from "react";
import {
View,
Text,
StyleSheet,
TouchableWithoutFeedback,
Animated,
} from "react-native";
import LinearGradient from "react-native-linear-gradient";
import { Ionicons, IoniconsIconName } from "@react-native-vector-icons/ionicons";
type CardProps = {
title: string;
subtitle?: string;
iconName?: IoniconsIconName;
onPress?: () => void;
backOne?: string;
backTwo?: string
};
const AnimatedCard: React.FC<CardProps> = ({
title,
subtitle,
iconName = "ellipse",
onPress,
backOne = '#4facfe',
backTwo = '#00f2fe'
}) => {
const scale = useRef(new Animated.Value(1)).current;
const handlePressIn = () => {
Animated.spring(scale, {
toValue: 0.96,
useNativeDriver: true,
friction: 4,
}).start();
};
const handlePressOut = () => {
Animated.spring(scale, {
toValue: 1,
useNativeDriver: true,
friction: 4,
}).start(() => onPress && onPress());
};
return (
<TouchableWithoutFeedback
onPressIn={handlePressIn}
onPressOut={handlePressOut}
>
<Animated.View style={[styles.card, { transform: [{ scale }] }]}>
<LinearGradient
colors={[backOne, backTwo]}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 1 }}
style={styles.gradient}
>
{/* Decorative Graphics */}
<View style={styles.graphicCircleLarge} />
<View style={styles.graphicCircleMedium} />
<View style={styles.graphicCircleSmall} />
<View style={styles.graphicTriangleTop} />
<View style={styles.graphicTriangleBottom} />
{/* Content */}
<View style={styles.content}>
{iconName && (
<Ionicons
name={iconName}
size={32}
color="#fff"
style={styles.icon}
/>
)}
<View style={styles.textContainer}>
<Text style={styles.title}>{title}</Text>
{subtitle && <Text style={styles.subtitle}>{subtitle}</Text>}
</View>
</View>
</LinearGradient>
</Animated.View>
</TouchableWithoutFeedback>
);
};
const styles = StyleSheet.create({
card: {
// marginHorizontal: 16,
marginVertical: 10,
borderRadius: 20,
overflow: "hidden",
shadowColor: "#000",
shadowOffset: { width: 0, height: 4 },
shadowOpacity: 0.25,
shadowRadius: 8,
elevation: 8,
},
gradient: {
minHeight: 120,
position: "relative",
},
// Graphics
graphicCircleLarge: {
position: "absolute",
top: -20,
right: -20,
width: 80,
height: 80,
borderRadius: 40,
backgroundColor: "rgba(124, 80, 80, 0.42)",
},
graphicCircleMedium: {
position: "absolute",
bottom: 30,
left: -15,
width: 60,
height: 60,
borderRadius: 30,
backgroundColor: "rgba(122, 41, 41, 0.39)",
},
graphicCircleSmall: {
position: "absolute",
bottom: -10,
right: 40,
width: 40,
height: 40,
borderRadius: 20,
backgroundColor: "rgba(207, 110, 80, 0.25)",
},
graphicTriangleTop: {
position: "absolute",
top: 0,
left: 0,
width: 0,
height: 0,
borderStyle: "solid",
borderRightWidth: 60,
borderTopWidth: 60,
borderRightColor: "transparent",
borderTopColor: "rgba(143, 62, 62, 0.31)",
},
graphicTriangleBottom: {
position: "absolute",
bottom: 0,
right: 0,
width: 0,
height: 0,
borderStyle: "solid",
borderLeftWidth: 40,
borderBottomWidth: 40,
borderLeftColor: "transparent",
borderBottomColor: "rgba(172, 51, 51, 0.27)",
},
content: {
flexDirection: "row", // stack vertically
alignItems: "center", // center horizontally
justifyContent: "center", // center vertically
padding: 20,
position: "relative",
zIndex: 1,
alignSelf: "center",
flex:1
}
,
icon: {
marginRight: 12,
},
textContainer: {
// alignItems: "center", // centers title and subtitle
},
title: {
fontSize: 18,
fontWeight: "700",
color: "#fff",
marginBottom: 4,
textTransform:'uppercase'
// textAlign: "center",
},
subtitle: {
fontSize: 14,
color: "rgba(255, 255, 255, 0.8)",
fontWeight: "500",
textAlign: "center",
},
});
export default AnimatedCard;
+3
View File
@@ -15,6 +15,7 @@ import Transaction from "../screens/profile/Transaction";
import GroupDetails from "../screens/home/user-home/GroupDetails";
import EditGroupScreen from "../screens/management/EditGroupScreen";
import EditBankDetails from "../screens/profile/EditBankDetails";
import TransactionsList from "../screens/transaction/TransactionList";
export type RootStackParamList = {
Splash: undefined;
@@ -32,6 +33,7 @@ export type RootStackParamList = {
YourContacts: undefined;
Transaction: undefined;
EditBankDetails: { currentBankName: string | null; currentAccountNumber: string | null; currentBranchCode: string | null };
TransactionList:undefined
};
const Stack = createNativeStackNavigator<RootStackParamList>();
@@ -66,6 +68,7 @@ export default function AppNavigator() {
<Stack.Screen name="GroupDetails" component={GroupDetails} options={{ headerShown: false }} />
<Stack.Screen name="EditGroup" component={EditGroupScreen} options={{ headerShown: false }} />
<Stack.Screen name="EditBankDetails" component={EditBankDetails} options={{ headerShown: false }} />
<Stack.Screen name="TransactionList" component={TransactionsList} options={{ headerShown: false }} />
</Stack.Navigator>
</NavigationContainer>
);
+1 -2
View File
@@ -67,7 +67,7 @@ const Home = () => {
console.log("👤 Profile Response:", result);
if (result.status === "success") {
setApiUser(result.data);
dispatch(
dispatch(
setUserData({
id: result.data.id,
userId: result.data.userId,
@@ -106,7 +106,6 @@ const Home = () => {
<View style={styles.header}>
<Text style={styles.greeting}>Hello, {user.firstName || apiUser?.firstName} {user.lastName || apiUser?.lastName}👋</Text>
<TouchableOpacity onPress={() => navigation.navigate("Profile" as never)}>
<Image
source={
user.imageBlob
+12 -2
View File
@@ -15,7 +15,7 @@ import colors from "../../../components/colors";
import { Ionicons } from "@react-native-vector-icons/ionicons";
import Header from "../../../components/common/Header";
import { apiCall } from "../../../utils/ApiHelper";
import { CONTRIBUTE, GROUP_ALL_MEMBER } from "../../../utils/ApiUrls";
import { BASE_URL, CONTRIBUTE, GROUP_ALL_MEMBER } from "../../../utils/ApiUrls";
import ContributionScreen from "../../ContributionScreen";
// Define type for a member
@@ -108,10 +108,20 @@ const GroupDetails = () => {
const confirmPayment = async () => {
try {
const res = await apiCall(`${BASE_URL}payments/contribute/${group?.id}?amount=${group?.collected}`, 'POST') as any
console.log("payment response : ", res)
if (res.status) {
ToastAndroid.show('Payment done!', ToastAndroid.SHORT)
}
if (res.status === 400) {
ToastAndroid.show(res?.data?.message || 'Payment done!', ToastAndroid.SHORT)
}
} catch (error) {
console.log("payment error : ", error)
}
}
const renderMember = ({ item, index }: { item: any; index: number }) => (
<View
+9 -4
View File
@@ -7,6 +7,8 @@ import {
TouchableOpacity,
Alert,
RefreshControl,
ScrollView,
Button,
} from "react-native";
import { useNavigation } from "@react-navigation/native";
import type { NativeStackNavigationProp } from "@react-navigation/native-stack";
@@ -15,6 +17,7 @@ import colors from "../../../components/colors";
import { Ionicons } from "@react-native-vector-icons/ionicons";
import { GET_JOINED_GROUP_URL } from "../../../utils/ApiUrls";
import * as Keychain from "react-native-keychain";
import AnimatedCard from "../../../components/common/AnimatedCard";
type NavigationProp = NativeStackNavigationProp<
RootStackParamList,
@@ -47,13 +50,12 @@ const UserHome = () => {
0
);
const onRefresh = async () => {
setRefreshing(true)
await handleGroup()
setRefreshing(false)
}
const handleGroup = async () => {
try {
setLoading(true)
@@ -120,7 +122,7 @@ const UserHome = () => {
};
return (
<View>
<>
{/* Stats */}
<View style={[styles.statsRow, { marginVertical: 24 }]}>
<View style={styles.statBox}>
@@ -132,6 +134,9 @@ const UserHome = () => {
<Text style={styles.statLabel}>Cycle Contribution</Text>
</View>
</View>
{/* <Button title="Transaction List" onPress={()=>navigation.navigate('TransactionList')}/> */}
<AnimatedCard
onPress={() => navigation.navigate("TransactionList")} title={"transaction list"} iconName="calculator-outline" subtitle="View your transaction history" />
{/* Groups List */}
<Text style={styles.sectionTitle}>Your Groups</Text>
@@ -143,7 +148,7 @@ const UserHome = () => {
onRefresh={onRefresh}
refreshing={refreshing}
/>
</View>
</>
);
};
+193
View File
@@ -0,0 +1,193 @@
import React, { useCallback, useEffect, useMemo, useState } from "react";
import {
View,
Text,
StyleSheet,
FlatList,
RefreshControl,
TouchableOpacity,
SafeAreaView,
} from "react-native";
import { apiCall } from "../../utils/ApiHelper";
import { TRANSACTION } from "../../utils/ApiUrls";
import colors from "../../components/colors";
import AppView from "../../components/AppView";
import Header from "../../components/common/Header";
type Transaction = {
id: string;
fundGroupId: string;
fundGroupName: string;
amount: number;
status: string;
payerMobile?: string;
transactionDate: number[];
};
type ApiResponse = {
message: string;
status: string;
statusCode: number;
data: Transaction[];
};
// Helper: convert the array-style date into a JS Date
const parseTransactionDate = (arr: number[] | undefined): Date | null => {
if (!arr || arr.length < 6) return null;
const [year, month, day, hour = 0, minute = 0, second = 0, sub = 0] = arr;
let milli = 0;
if (sub > 999999999) {
milli = Math.floor(sub / 1e6);
} else if (sub > 1000) {
milli = Math.floor(sub / 1e6);
} else {
milli = sub;
}
return new Date(year, month - 1, day, hour, minute, second, milli);
};
const formatINR = (amount: number) => {
try {
return new Intl.NumberFormat("en-IN", { style: "currency", currency: "INR" }).format(amount);
} catch (e) {
return `₹${amount.toFixed(2)}`;
}
};
const StatusBadge: React.FC<{ status?: string }> = ({ status }) => {
const text = status ?? "UNKNOWN";
const color =
text === "SUCCESS" ? styles.statusSuccess : text === "PENDING" ? styles.statusPending : styles.statusFailed;
return (
<View style={[styles.statusBadge, color]}>
<Text style={styles.statusText}>{text}</Text>
</View>
);
};
const TransactionItem: React.FC<{ item: Transaction }> = ({ item }) => {
const date = useMemo(() => parseTransactionDate(item.transactionDate), [item.transactionDate]);
const dateStr = date ? date.toLocaleString("en-IN") : "-";
return (
<TouchableOpacity activeOpacity={0.8} style={styles.card}>
<View style={styles.cardLeft}>
<Text style={styles.fundName}>{item.fundGroupName}</Text>
<Text style={styles.payer}>Payer: {item.payerMobile ?? "-"}</Text>
<Text style={styles.date}>{dateStr}</Text>
</View>
<View style={styles.cardRight}>
<Text style={styles.amount}>{formatINR(item.amount)}</Text>
<StatusBadge status={item.status} />
</View>
</TouchableOpacity>
);
};
const TransactionsList: React.FC = () => {
const [refreshing, setRefreshing] = useState(false);
const [transactions, setTransactions] = useState<Transaction[]>([]);
const [message, setMessage] = useState("");
const handleTransactionList = async () => {
try {
const res = (await apiCall(TRANSACTION, "GET")) as ApiResponse;
console.log("transaction list response : ", res);
if (res.statusCode === 200) {
setTransactions(res.data);
setMessage(res.message);
}
} catch (error) {
console.error("Error fetching transactions:", error);
}
};
useEffect(() => {
handleTransactionList();
}, []);
const handleRefresh = useCallback(async () => {
try {
setRefreshing(true);
await handleTransactionList();
} catch (e) {
console.warn("Refresh failed", e);
} finally {
setRefreshing(false);
}
}, []);
const renderEmpty = () => (
<View style={styles.emptyContainer}>
<Text style={styles.emptyTitle}>No transactions</Text>
<Text style={styles.emptySubtitle}>Transactions will appear here when available.</Text>
</View>
);
return (
<AppView>
<Header title="transactions" titleColor={colors.Home} arrowColor={colors.Home}/>
<Text style={styles.headerTitle}>{message}</Text>
<FlatList
data={transactions}
keyExtractor={(item) => item.id}
renderItem={({ item }) => <TransactionItem item={item} />}
ListEmptyComponent={renderEmpty}
contentContainerStyle={transactions.length === 0 ? { flex: 1 } : undefined}
refreshControl={<RefreshControl refreshing={refreshing} onRefresh={handleRefresh} />}
/>
</AppView>
);
};
export default TransactionsList;
const styles = StyleSheet.create({
container: { flex: 1, backgroundColor: "#f7f7fb" },
headerTitle: { fontSize: 18, fontWeight: "500", color: colors.primary },
headerSubtitle: { marginTop: 4, color: "#6b7280" },
card: {
flexDirection: "row",
justifyContent: "space-between",
padding: 16,
// marginHorizontal: 12,
marginVertical: 8,
backgroundColor: "#fff",
borderRadius: 12,
shadowColor: "#000",
shadowOpacity: 0.03,
shadowRadius: 8,
elevation: 2,
},
cardLeft: { flex: 1 },
fundName: { fontSize: 16, fontWeight: "600" },
payer: { marginTop: 6, color: "#6b7280" },
date: { marginTop: 6, color: "#9ca3af" },
cardRight: { alignItems: "flex-end", justifyContent: "space-between" },
amount: { fontSize: 16, fontWeight: "700" },
statusBadge: {
marginTop: 8,
paddingHorizontal: 8,
paddingVertical: 4,
borderRadius: 999,
},
statusText: { fontSize: 12, fontWeight: "600", color: "#fff" },
statusSuccess: { backgroundColor: "#10b981" },
statusPending: { backgroundColor: "#f59e0b" },
statusFailed: { backgroundColor: "#ef4444" },
emptyContainer: { alignItems: "center", justifyContent: "center", },
emptyTitle: { fontSize: 18, fontWeight: "600", color: "#374151" },
emptySubtitle: { marginTop: 8, color: "#6b7280" },
});
+7 -2
View File
@@ -1,4 +1,5 @@
import axios, { AxiosRequestConfig } from "axios";
import { Alert, ToastAndroid } from "react-native";
import * as Keychain from "react-native-keychain";
/**
@@ -12,7 +13,8 @@ export async function apiCall<T>(
url: string,
method: "GET" | "POST" | "PUT" | "DELETE" = "GET",
data?: any,
config?: AxiosRequestConfig
config?: AxiosRequestConfig,
responseErrorMessage?:string
): Promise<T | null > {
try {
// Get saved credentials from Keychain
@@ -41,12 +43,15 @@ export async function apiCall<T>(
`Status: ${error.response.status}`,
`Data: ${JSON.stringify(error.response.data)}`
);
if(error.response.status === 400){
ToastAndroid.show(error.response.data.message || responseErrorMessage, ToastAndroid.SHORT)
}
} else if (error.request) {
// Request was made but no response received
console.error("API Error: No response received", error.request);
} else {
// Something else went wrong while setting up the request
console.error("API Error:", error.message);
console.error("API Error:", error);
}
return null; // return null so the caller can handle it
}
+1
View File
@@ -19,3 +19,4 @@ export const USER_REQUEST = `${BASE_URL}fund/my-requests`
export const GROUP_ALL_MEMBER = `${BASE_URL}fund/group/all/member`
export const UPDATE_BANK_URL = `${BASE_URL}user/bank/update`;
export const CONTRIBUTE = `${BASE_URL}payments/groups/`;
export const TRANSACTION = `${BASE_URL}payments/my-transactions`;