fix UI issues and implement Transaction Detail Screen
parent
b648a485be
commit
75a3eb87e1
|
|
@ -6,7 +6,7 @@ import { AppDispatch, RootState } from "@/store";
|
|||
import { initSocket } from "@/services/socket";
|
||||
import { useSnackbar } from "@/contexts/Snackbar";
|
||||
import NetInfo from "@react-native-community/netinfo";
|
||||
import { getUserDetails } from "@/store/userSlice";
|
||||
import { getPaymentSummary, getUserDetails } from "@/store/userSlice";
|
||||
import { useDispatch } from "react-redux";
|
||||
import { logout } from "@/store/authSlice";
|
||||
|
||||
|
|
@ -35,6 +35,7 @@ export default function TabLayout() {
|
|||
const fetchAndInit = async () => {
|
||||
try {
|
||||
const result = await dispatch(getUserDetails()).unwrap();
|
||||
await dispatch(getPaymentSummary());
|
||||
console.log("User details fetched", result);
|
||||
initSocket();
|
||||
} catch (error) {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
import {
|
||||
Animated,
|
||||
Easing,
|
||||
Pressable,
|
||||
ScrollView,
|
||||
StyleSheet,
|
||||
|
|
@ -26,8 +28,16 @@ import ProfileImage from "@/components/home/Profile";
|
|||
import { calculateBearing, calculateDistance } from "@/utils/Map";
|
||||
import { useDispatch } from "react-redux";
|
||||
import RefreshIcon from "@/assets/icons/refresh.svg";
|
||||
import { disconnectSocket, initSocket } from "@/services/socket";
|
||||
import { setTelemetryError, setTelemetryLoading } from "@/store/telemetrySlice";
|
||||
|
||||
import { payments } from "@/constants/config";
|
||||
import {
|
||||
clearUser,
|
||||
getPaymentSummary,
|
||||
getUserDetails,
|
||||
} from "@/store/userSlice";
|
||||
import api from "@/services/axiosClient";
|
||||
import { setDueAmount } from "@/store/paymentSlice";
|
||||
import { EmiResponse } from "./payments";
|
||||
|
||||
export default function HomeScreen() {
|
||||
const { t } = useTranslation();
|
||||
|
|
@ -35,7 +45,13 @@ export default function HomeScreen() {
|
|||
const [isSupportModalVisible, setIsSupportModalVisible] = useState(false);
|
||||
const { SoC, SoH, chargingState, lat, lon, loading, error, totalDistance } =
|
||||
useSelector((state: RootState) => state.telemetry);
|
||||
const { data } = useSelector((state: RootState) => state.user);
|
||||
|
||||
const [refreshing, setRefreshing] = useState(false);
|
||||
const spinValue = useState(new Animated.Value(0))[0];
|
||||
|
||||
const { data, paymentSummary } = useSelector(
|
||||
(state: RootState) => state.user
|
||||
);
|
||||
const [prevPosition, setPrevPosition] = useState<{
|
||||
lat: number;
|
||||
lon: number;
|
||||
|
|
@ -110,34 +126,36 @@ export default function HomeScreen() {
|
|||
),
|
||||
headerRight: () => (
|
||||
<View style={styles.rightContainer}>
|
||||
<Pressable onPress={() => handleManualRefresh()}>
|
||||
<RefreshIcon width={50} height={50} />
|
||||
<Pressable onPress={handleManualRefresh} disabled={refreshing}>
|
||||
<Animated.View style={{ transform: [{ rotate: spin }] }}>
|
||||
<RefreshIcon
|
||||
width={50}
|
||||
height={50}
|
||||
opacity={refreshing ? 0.5 : 1}
|
||||
/>
|
||||
</Animated.View>
|
||||
</Pressable>
|
||||
<Pressable
|
||||
style={styles.supportButton}
|
||||
onPress={() => {
|
||||
console.log("Support Pressed");
|
||||
setIsSupportModalVisible(true);
|
||||
}}
|
||||
onPress={() => setIsSupportModalVisible(true)}
|
||||
>
|
||||
<CustomerCareIcon width={50} height={50} />
|
||||
</Pressable>
|
||||
<Pressable
|
||||
<TouchableOpacity
|
||||
onPress={() => {
|
||||
router.push("/user/profile");
|
||||
}}
|
||||
>
|
||||
<ProfileImage
|
||||
username={data?.name || "Vec"}
|
||||
onClick={() => router.push("/user/profile")}
|
||||
textSize={20}
|
||||
boxSize={40}
|
||||
/>
|
||||
</Pressable>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
),
|
||||
});
|
||||
}, [navigation, data, model, chasisNumber]);
|
||||
}, [navigation, data, model, chasisNumber, refreshing]);
|
||||
|
||||
const openInGoogleMaps = () => {
|
||||
const url = `https://www.google.com/maps/search/?api=1&query=${lat},${lon}`;
|
||||
|
|
@ -146,27 +164,85 @@ export default function HomeScreen() {
|
|||
);
|
||||
};
|
||||
|
||||
const handleManualRefresh = async () => {
|
||||
dispatch(setTelemetryLoading());
|
||||
const startSpin = () => {
|
||||
spinValue.setValue(0);
|
||||
Animated.loop(
|
||||
Animated.timing(spinValue, {
|
||||
toValue: 1,
|
||||
duration: 1000,
|
||||
easing: Easing.linear,
|
||||
useNativeDriver: true,
|
||||
})
|
||||
).start();
|
||||
};
|
||||
|
||||
const stopSpin = () => {
|
||||
spinValue.stopAnimation();
|
||||
spinValue.setValue(0);
|
||||
};
|
||||
|
||||
const spin = spinValue.interpolate({
|
||||
inputRange: [0, 1],
|
||||
outputRange: ["0deg", "360deg"],
|
||||
});
|
||||
|
||||
const handleManualRefresh = async () => {
|
||||
try {
|
||||
disconnectSocket();
|
||||
await initSocket();
|
||||
} catch (err) {
|
||||
console.error("Refresh failed", err);
|
||||
dispatch(setTelemetryError("Refresh failed"));
|
||||
setRefreshing(true);
|
||||
startSpin();
|
||||
|
||||
await dispatch(clearUser());
|
||||
await dispatch(getUserDetails()).unwrap();
|
||||
await dispatch(getPaymentSummary()).unwrap();
|
||||
|
||||
console.log("Manual refresh complete");
|
||||
} catch (error) {
|
||||
console.error("Manual refresh failed", error);
|
||||
} finally {
|
||||
stopSpin();
|
||||
setRefreshing(false);
|
||||
}
|
||||
};
|
||||
|
||||
const {
|
||||
daysLeftToPayEmi,
|
||||
daysLeftToPayEmiText,
|
||||
regularServiceDueInDaysText,
|
||||
due_amount,
|
||||
} = paymentSummary || {};
|
||||
|
||||
useEffect(() => {
|
||||
if (paymentSummary?.due_amount != null) {
|
||||
dispatch(setDueAmount(paymentSummary.due_amount));
|
||||
}
|
||||
}, [paymentSummary, dispatch]);
|
||||
|
||||
return (
|
||||
<View style={{ flex: 1, backgroundColor: "#F3F5F8" }}>
|
||||
<StatusBar style="dark" />
|
||||
<ScrollView contentContainerStyle={styles.container} style={{ flex: 1 }}>
|
||||
{daysLeftToPayEmi && daysLeftToPayEmiText && daysLeftToPayEmi > 0 && (
|
||||
<ServiceReminderCard
|
||||
type={
|
||||
daysLeftToPayEmi >= payments.EMI_WARNING_DAYS_THRESHOLD
|
||||
? "warning"
|
||||
: "danger"
|
||||
}
|
||||
message={daysLeftToPayEmiText}
|
||||
subMessage="Pay now"
|
||||
redirectPath="/(tabs)/payments"
|
||||
/>
|
||||
)}
|
||||
|
||||
{regularServiceDueInDaysText && (
|
||||
<ServiceReminderCard
|
||||
type="info"
|
||||
message="Service will be due in 3 days"
|
||||
message={regularServiceDueInDaysText}
|
||||
subMessage="Service Reminder"
|
||||
redirectPath="/(tabs)/service"
|
||||
/>
|
||||
)}
|
||||
|
||||
<View style={styles.iconContainer}>
|
||||
<SemiCircleProgress progress={SoC} status={chargingState} />
|
||||
</View>
|
||||
|
|
@ -178,13 +254,15 @@ export default function HomeScreen() {
|
|||
unit="km"
|
||||
/>
|
||||
</View>
|
||||
{due_amount && (
|
||||
<PaymentDueCard
|
||||
label="Payment Due"
|
||||
amount="$2,000"
|
||||
amount={due_amount}
|
||||
onPress={() => {
|
||||
alert("le is number pe bhej de 8685846459");
|
||||
router.push("/payments/selectAmount");
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<View style={styles.map}>
|
||||
{loading ? (
|
||||
<View style={styles.errorContainer}>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,14 @@
|
|||
import { useLayoutEffect, useMemo, useState, useEffect } from "react";
|
||||
import {
|
||||
useLayoutEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
useEffect,
|
||||
useCallback,
|
||||
} from "react";
|
||||
import { useNavigation, useRouter } from "expo-router";
|
||||
import {
|
||||
Animated,
|
||||
Easing,
|
||||
NativeScrollEvent,
|
||||
NativeSyntheticEvent,
|
||||
Pressable,
|
||||
|
|
@ -21,6 +29,10 @@ import { BASE_URL } from "@/constants/config";
|
|||
import { useDispatch } from "react-redux";
|
||||
import { setDueAmount, setMyPlan } from "@/store/paymentSlice";
|
||||
import { ActivityIndicator } from "react-native-paper";
|
||||
import { useFocusEffect } from "@react-navigation/native";
|
||||
import { displayValue } from "@/utils/Common";
|
||||
import RefreshIcon from "@/assets/icons/refresh.svg";
|
||||
import CustomerSupport from "@/components/home/CustomerSupportModal";
|
||||
|
||||
export interface MyPlan {
|
||||
no_of_emi: number;
|
||||
|
|
@ -43,7 +55,7 @@ interface EmiDetails {
|
|||
myPlain: MyPlan;
|
||||
}
|
||||
|
||||
interface EmiResponse {
|
||||
export interface EmiResponse {
|
||||
success: boolean;
|
||||
data: EmiDetails[];
|
||||
}
|
||||
|
|
@ -71,33 +83,28 @@ interface PaymentHistoryResponse {
|
|||
};
|
||||
}
|
||||
|
||||
const formatPaymentDate = (dateString: string) => {
|
||||
try {
|
||||
return dateString.split(",")[0];
|
||||
} catch {
|
||||
return dateString;
|
||||
}
|
||||
};
|
||||
function formatPaymentDate(input: string): string {
|
||||
const date = new Date(input);
|
||||
const options: Intl.DateTimeFormatOptions = {
|
||||
day: "2-digit",
|
||||
month: "short",
|
||||
year: "numeric",
|
||||
};
|
||||
return date.toLocaleDateString("en-GB", options);
|
||||
}
|
||||
|
||||
// Format time for payment history
|
||||
const formatPaymentTime = (dateString: string) => {
|
||||
try {
|
||||
// Expected: "12 Aug 2025, 12:38:23 pm"
|
||||
const [datePart, timePart] = dateString.split(",");
|
||||
|
||||
// Ensure JS can parse (convert "12 Aug 2025 12:38:23 pm" to valid format)
|
||||
const parsedDate = new Date(`${datePart.trim()} ${timePart.trim()}`);
|
||||
|
||||
if (isNaN(parsedDate.getTime())) {
|
||||
return dateString;
|
||||
}
|
||||
|
||||
const dayName = parsedDate.toLocaleDateString("en-US", { weekday: "long" });
|
||||
return `${timePart.trim()} | ${dayName}`;
|
||||
} catch (err) {
|
||||
return dateString;
|
||||
}
|
||||
};
|
||||
function formatPaymentTime(input: string): string {
|
||||
if (!input) return "--";
|
||||
const date = new Date(input);
|
||||
const timeStr = date.toLocaleTimeString("en-US", {
|
||||
hour: "numeric",
|
||||
minute: "2-digit",
|
||||
hour12: true,
|
||||
});
|
||||
const dayStr = date.toLocaleDateString("en-US", { weekday: "long" });
|
||||
return `${timeStr} | ${dayStr}`;
|
||||
}
|
||||
|
||||
export default function PaymentsTabScreen() {
|
||||
const navigation = useNavigation();
|
||||
|
|
@ -114,6 +121,8 @@ export default function PaymentsTabScreen() {
|
|||
const [paymentHistory, setPaymentHistory] = useState<PaymentHistoryItem[]>(
|
||||
[]
|
||||
);
|
||||
const [refreshing, setRefreshing] = useState(false);
|
||||
const spinValue = useState(new Animated.Value(0))[0];
|
||||
const [isHistoryLoading, setIsHistoryLoading] = useState(false);
|
||||
const [showFullHistory, setShowFullHistory] = useState(false);
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
|
|
@ -132,11 +141,10 @@ export default function PaymentsTabScreen() {
|
|||
const model = vehicle?.model ?? "---";
|
||||
const chasisNumber = vehicle?.chasis_number ?? "---";
|
||||
|
||||
// Fetch EMI details
|
||||
useEffect(() => {
|
||||
const fetchEmiDetails = async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
setEmiDetails(null);
|
||||
|
||||
const response = await api.get(`/api/v1/emi-details`);
|
||||
const result: EmiResponse = response.data;
|
||||
|
|
@ -161,8 +169,51 @@ export default function PaymentsTabScreen() {
|
|||
}
|
||||
};
|
||||
|
||||
const handleRefresh = async () => {
|
||||
try {
|
||||
setShowFullHistory(false);
|
||||
setRefreshing(true);
|
||||
startSpin();
|
||||
|
||||
await Promise.all([fetchEmiDetails(), fetchPaymentHistory(1, false)]);
|
||||
console.log("Manual refresh complete");
|
||||
} catch (error) {
|
||||
console.error("Manual refresh failed", error);
|
||||
} finally {
|
||||
stopSpin();
|
||||
setRefreshing(false);
|
||||
}
|
||||
};
|
||||
|
||||
const startSpin = () => {
|
||||
spinValue.setValue(0);
|
||||
Animated.loop(
|
||||
Animated.timing(spinValue, {
|
||||
toValue: 1,
|
||||
duration: 1000,
|
||||
easing: Easing.linear,
|
||||
useNativeDriver: true,
|
||||
})
|
||||
).start();
|
||||
};
|
||||
|
||||
const stopSpin = () => {
|
||||
spinValue.stopAnimation();
|
||||
spinValue.setValue(0);
|
||||
};
|
||||
|
||||
const spin = spinValue.interpolate({
|
||||
inputRange: [0, 1],
|
||||
outputRange: ["0deg", "360deg"],
|
||||
});
|
||||
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
setShowFullHistory(false);
|
||||
fetchEmiDetails();
|
||||
}, []);
|
||||
fetchPaymentHistory(1, false);
|
||||
}, [])
|
||||
);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
navigation.setOptions({
|
||||
|
|
@ -177,6 +228,15 @@ export default function PaymentsTabScreen() {
|
|||
),
|
||||
headerRight: () => (
|
||||
<View style={styles.rightContainer}>
|
||||
<Pressable onPress={() => handleRefresh()} disabled={refreshing}>
|
||||
<Animated.View style={{ transform: [{ rotate: spin }] }}>
|
||||
<RefreshIcon
|
||||
height={50}
|
||||
width={50}
|
||||
style={{ opacity: refreshing ? 0.5 : 1 }}
|
||||
/>
|
||||
</Animated.View>
|
||||
</Pressable>
|
||||
<Pressable
|
||||
style={styles.supportButton}
|
||||
onPress={() => {
|
||||
|
|
@ -184,7 +244,7 @@ export default function PaymentsTabScreen() {
|
|||
setIsSupportModalVisible(true);
|
||||
}}
|
||||
>
|
||||
<CustomerCareIcon />
|
||||
<CustomerCareIcon height={50} width={50} />
|
||||
</Pressable>
|
||||
<Pressable
|
||||
onPress={() => {
|
||||
|
|
@ -201,7 +261,7 @@ export default function PaymentsTabScreen() {
|
|||
</View>
|
||||
),
|
||||
});
|
||||
}, [navigation, data, model, chasisNumber]);
|
||||
}, [navigation, data, model, chasisNumber, refreshing]);
|
||||
|
||||
// Format currency
|
||||
const formatCurrency = (amount: number) => {
|
||||
|
|
@ -307,11 +367,8 @@ export default function PaymentsTabScreen() {
|
|||
setIsEndReached(isAtBottom);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchPaymentHistory(1, false);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<ScrollView
|
||||
contentContainerStyle={{ paddingHorizontal: 16, paddingBottom: 125 }}
|
||||
showsVerticalScrollIndicator={false}
|
||||
|
|
@ -330,41 +387,43 @@ export default function PaymentsTabScreen() {
|
|||
<View style={styles.divider} />
|
||||
|
||||
{/* EMI Details Content */}
|
||||
{true && (
|
||||
<View style={styles.cardContent}>
|
||||
<View style={styles.detailRow}>
|
||||
<Text style={styles.detailLabel}>Amount Due</Text>
|
||||
<Text style={styles.detailValue}>
|
||||
{emiDetails?.due_amount
|
||||
? formatCurrency(emiDetails.due_amount)
|
||||
: "---"}
|
||||
{displayValue(emiDetails?.due_amount, formatCurrency)}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<View style={styles.detailRow}>
|
||||
<Text style={styles.detailLabel}>Amount Paid</Text>
|
||||
<Text style={styles.detailValue}>
|
||||
{emiDetails?.total_amount_paid_in_current_cycle &&
|
||||
formatCurrency(emiDetails.total_amount_paid_in_current_cycle)}
|
||||
{displayValue(
|
||||
emiDetails?.total_amount_paid_in_current_cycle,
|
||||
formatCurrency
|
||||
)}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<View style={styles.detailRow}>
|
||||
<Text style={styles.detailLabel}>Due Date</Text>
|
||||
<Text style={styles.detailValue}>
|
||||
{emiDetails?.due_date && emiDetails.due_date}
|
||||
{displayValue(emiDetails?.due_date)}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<View style={styles.detailRow}>
|
||||
<Text style={styles.detailLabel}>Payment Status</Text>
|
||||
{emiDetails?.status ? (
|
||||
<StatusBadge
|
||||
label={emiDetails?.status && emiDetails.status}
|
||||
type={emiDetails?.status && emiDetails.status.toLowerCase()}
|
||||
label={emiDetails.status}
|
||||
type={emiDetails.status.toLowerCase()}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
) : (
|
||||
<Text style={styles.detailValue}>--</Text>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View style={styles.divider} />
|
||||
|
||||
|
|
@ -451,6 +510,11 @@ export default function PaymentsTabScreen() {
|
|||
</View>
|
||||
</View>
|
||||
</ScrollView>
|
||||
<CustomerSupport
|
||||
visible={isSupportModalVisible}
|
||||
onClose={() => setIsSupportModalVisible(false)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -469,13 +533,7 @@ const StatusBadge = ({
|
|||
backgroundColor: "#FFF0E3",
|
||||
color: "#8E4400",
|
||||
};
|
||||
case "failed":
|
||||
return {
|
||||
backgroundColor: "#FDE8E7",
|
||||
color: "#D51C10",
|
||||
};
|
||||
case "completed":
|
||||
case "paid":
|
||||
return {
|
||||
backgroundColor: "#E8F5E8",
|
||||
color: "#2D7D32",
|
||||
|
|
|
|||
|
|
@ -138,6 +138,7 @@ const styles = StyleSheet.create({
|
|||
paddingHorizontal: 16,
|
||||
paddingBottom: 16,
|
||||
justifyContent: "space-between",
|
||||
marginTop: 8,
|
||||
},
|
||||
qrFrame: {
|
||||
backgroundColor: "#fcfcfc",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,372 @@
|
|||
import React, { useState, useEffect, useLayoutEffect } from "react";
|
||||
import {
|
||||
View,
|
||||
Text,
|
||||
StyleSheet,
|
||||
TouchableOpacity,
|
||||
ActivityIndicator,
|
||||
} from "react-native";
|
||||
import { useNavigation, useRoute } from "@react-navigation/native";
|
||||
import { useRouter } from "expo-router";
|
||||
import api from "@/services/axiosClient";
|
||||
import { BASE_URL } from "@/constants/config";
|
||||
import { useSnackbar } from "@/contexts/Snackbar";
|
||||
import { SafeAreaView } from "react-native-safe-area-context";
|
||||
|
||||
// Import your success/failure icons
|
||||
import SuccessIcon from "@/assets/icons/check_circle.svg";
|
||||
import FailureIcon from "@/assets/icons/cancel.svg";
|
||||
import PendingIcon from "@/assets/icons/pending.svg";
|
||||
|
||||
interface TransactionDetailData {
|
||||
id: number;
|
||||
amount: string;
|
||||
status: string;
|
||||
transaction_date: string | null;
|
||||
upi_handle: string;
|
||||
payment_mode: string[];
|
||||
paid_by_upi_handle: string | null;
|
||||
order_id: string;
|
||||
payment_reference_id: string | null;
|
||||
transaction_order_id: string;
|
||||
}
|
||||
|
||||
interface TransactionResponse {
|
||||
success: boolean;
|
||||
data: {
|
||||
payments: TransactionDetailData[];
|
||||
pagination: {
|
||||
total_records: number;
|
||||
page_number: number;
|
||||
page_size: number;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export default function TransactionDetailScreen() {
|
||||
const navigation = useNavigation();
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const { showSnackbar } = useSnackbar();
|
||||
|
||||
// Get payment ID from route params
|
||||
const { paymentId } = route.params as { paymentId: number };
|
||||
|
||||
const [transactionData, setTransactionData] =
|
||||
useState<TransactionDetailData | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
navigation.setOptions({
|
||||
headerStyle: {
|
||||
backgroundColor: "#F3F5F8",
|
||||
},
|
||||
headerTitle: "Transaction Detail",
|
||||
headerTitleStyle: {
|
||||
fontSize: 18,
|
||||
fontWeight: "600",
|
||||
color: "#111827",
|
||||
},
|
||||
headerLeft: () => (
|
||||
<TouchableOpacity
|
||||
onPress={() => router.back()}
|
||||
style={styles.backButton}
|
||||
>
|
||||
<Text style={styles.backText}>‹</Text>
|
||||
</TouchableOpacity>
|
||||
),
|
||||
});
|
||||
}, [navigation, router]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchTransactionDetail();
|
||||
}, [paymentId]);
|
||||
|
||||
const fetchTransactionDetail = async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const response = await api.get(
|
||||
`${BASE_URL}/api/v1/payment-history?id=${paymentId}`
|
||||
);
|
||||
const result: TransactionResponse = response.data;
|
||||
|
||||
if (result.success && result.data.payments.length > 0) {
|
||||
setTransactionData(result.data.payments[0]);
|
||||
} else {
|
||||
showSnackbar("Transaction details not found", "error");
|
||||
router.back();
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Error fetching transaction details:", err);
|
||||
const errorMessage =
|
||||
err instanceof Error ? err.message : "Something went wrong";
|
||||
showSnackbar(errorMessage, "error");
|
||||
router.back();
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const formatCurrency = (amount: string) => {
|
||||
return `₹${parseFloat(amount).toLocaleString()}`;
|
||||
};
|
||||
|
||||
const formatDateTime = (dateString: string | null) => {
|
||||
if (!dateString) return "--";
|
||||
const date = new Date(dateString);
|
||||
const dateStr = date.toLocaleDateString("en-IN", {
|
||||
day: "2-digit",
|
||||
month: "long",
|
||||
year: "numeric",
|
||||
});
|
||||
const timeStr = date.toLocaleTimeString("en-US", {
|
||||
hour: "numeric",
|
||||
minute: "2-digit",
|
||||
hour12: true,
|
||||
});
|
||||
const dayStr = date.toLocaleDateString("en-US", { weekday: "long" });
|
||||
return `${dateStr}, ${timeStr}, ${dayStr}`;
|
||||
};
|
||||
|
||||
const getStatusIcon = (status: string) => {
|
||||
if (status.toLowerCase() === "confirmed") {
|
||||
return <SuccessIcon width={80} height={80} />;
|
||||
} else if (status.toLowerCase() === "pending") {
|
||||
return <PendingIcon width={80} height={80} />;
|
||||
} else {
|
||||
return <FailureIcon width={80} height={80} />;
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusText = (status: string) => {
|
||||
switch (status.toLowerCase()) {
|
||||
case "success":
|
||||
case "completed":
|
||||
case "confirmed":
|
||||
return "Payment successful";
|
||||
case "failure":
|
||||
case "failed":
|
||||
return "Payment failed";
|
||||
case "pending":
|
||||
return "Payment pending";
|
||||
default:
|
||||
return `Payment ${status}`;
|
||||
}
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<SafeAreaView style={styles.container}>
|
||||
<View style={styles.loadingContainer}>
|
||||
<ActivityIndicator size="large" color="#00BE88" />
|
||||
</View>
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
|
||||
if (!transactionData) {
|
||||
return (
|
||||
<SafeAreaView style={styles.container}>
|
||||
<View style={styles.errorContainer}>
|
||||
<Text style={styles.errorText}>Transaction not found</Text>
|
||||
</View>
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<SafeAreaView style={styles.container}>
|
||||
<View style={styles.content}>
|
||||
{/* Status Icon */}
|
||||
<View style={styles.iconContainer}>
|
||||
{getStatusIcon(transactionData.status)}
|
||||
</View>
|
||||
|
||||
{/* Amount */}
|
||||
<Text style={styles.amount}>
|
||||
{formatCurrency(transactionData.amount)}
|
||||
</Text>
|
||||
|
||||
{/* Status Text */}
|
||||
<Text style={styles.statusText}>
|
||||
{getStatusText(transactionData.status)}
|
||||
</Text>
|
||||
|
||||
{/* Date Time */}
|
||||
<Text style={styles.dateTime}>
|
||||
{formatDateTime(transactionData.transaction_date)}
|
||||
</Text>
|
||||
|
||||
{/* Transaction Details Card */}
|
||||
<View style={styles.detailsCard}>
|
||||
<Text style={styles.detailsTitle}>Transaction Details</Text>
|
||||
|
||||
<DetailRow
|
||||
label="Payment mode"
|
||||
value={transactionData.payment_mode.join(", ") || "--"}
|
||||
/>
|
||||
|
||||
<DetailRow
|
||||
label="Paid to"
|
||||
value={transactionData.upi_handle || "--"}
|
||||
/>
|
||||
|
||||
<DetailRow
|
||||
label="Paid by"
|
||||
value={transactionData.paid_by_upi_handle || "--"}
|
||||
/>
|
||||
|
||||
<DetailRow
|
||||
label="Order ID"
|
||||
value={transactionData.order_id || "--"}
|
||||
/>
|
||||
|
||||
<DetailRow
|
||||
label="Transaction ID"
|
||||
value={transactionData.transaction_order_id || "--"}
|
||||
/>
|
||||
|
||||
<DetailRow
|
||||
label="RRN"
|
||||
value={transactionData.payment_reference_id || "--"}
|
||||
isLast={true}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
|
||||
const DetailRow = ({
|
||||
label,
|
||||
value,
|
||||
isLast = false,
|
||||
}: {
|
||||
label: string;
|
||||
value: string;
|
||||
isLast?: boolean;
|
||||
}) => (
|
||||
<View style={[styles.detailRow, isLast && styles.detailRowLast]}>
|
||||
<Text style={styles.detailLabel}>{label}</Text>
|
||||
<Text style={styles.detailValue}>{value}</Text>
|
||||
</View>
|
||||
);
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: "#F3F5F8",
|
||||
},
|
||||
loadingContainer: {
|
||||
flex: 1,
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
},
|
||||
errorContainer: {
|
||||
flex: 1,
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
},
|
||||
errorText: {
|
||||
fontSize: 16,
|
||||
color: "#6B7280",
|
||||
},
|
||||
content: {
|
||||
flex: 1,
|
||||
padding: 16,
|
||||
alignItems: "center",
|
||||
},
|
||||
backButton: {
|
||||
paddingLeft: 16,
|
||||
paddingRight: 8,
|
||||
paddingVertical: 8,
|
||||
},
|
||||
backText: {
|
||||
fontSize: 24,
|
||||
color: "#111827",
|
||||
fontWeight: "300",
|
||||
},
|
||||
iconContainer: {
|
||||
marginTop: 32,
|
||||
marginBottom: 16,
|
||||
},
|
||||
successIcon: {
|
||||
width: 60,
|
||||
height: 60,
|
||||
borderRadius: 30,
|
||||
backgroundColor: "#00BE88",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
},
|
||||
failureIcon: {
|
||||
width: 60,
|
||||
height: 60,
|
||||
borderRadius: 30,
|
||||
backgroundColor: "#EF4444",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
},
|
||||
checkmark: {
|
||||
color: "white",
|
||||
fontSize: 24,
|
||||
fontWeight: "bold",
|
||||
},
|
||||
xmark: {
|
||||
color: "white",
|
||||
fontSize: 20,
|
||||
fontWeight: "bold",
|
||||
},
|
||||
amount: {
|
||||
fontSize: 20,
|
||||
fontWeight: "600",
|
||||
color: "#252A34",
|
||||
marginBottom: 16,
|
||||
},
|
||||
statusText: {
|
||||
fontSize: 14,
|
||||
fontWeight: "400",
|
||||
color: "#252A34",
|
||||
marginBottom: 4,
|
||||
},
|
||||
dateTime: {
|
||||
fontSize: 14,
|
||||
color: "#252A34",
|
||||
textAlign: "center",
|
||||
marginBottom: 24,
|
||||
},
|
||||
detailsCard: {
|
||||
backgroundColor: "#FCFCFC",
|
||||
borderRadius: 8,
|
||||
padding: 16,
|
||||
width: "100%",
|
||||
},
|
||||
detailsTitle: {
|
||||
fontSize: 14,
|
||||
fontWeight: "600",
|
||||
color: "#252A34",
|
||||
marginBottom: 8,
|
||||
},
|
||||
detailRow: {
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "flex-start",
|
||||
paddingVertical: 8,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: "#E5E9F0",
|
||||
},
|
||||
detailRowLast: {
|
||||
borderBottomWidth: 0,
|
||||
},
|
||||
detailLabel: {
|
||||
fontSize: 14,
|
||||
color: "#252A34",
|
||||
flex: 1,
|
||||
},
|
||||
detailValue: {
|
||||
fontSize: 14,
|
||||
fontWeight: "600",
|
||||
color: "#252A34",
|
||||
flex: 1,
|
||||
textAlign: "right",
|
||||
},
|
||||
});
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
import Header from "@/components/common/Header";
|
||||
import ProgressCard from "@/components/common/ProgressCard";
|
||||
import { RootState } from "@/store/rootReducer";
|
||||
import { useRouter } from "expo-router";
|
||||
import React from "react";
|
||||
import { View, Text, StyleSheet, TouchableOpacity } from "react-native";
|
||||
import { useSelector } from "react-redux";
|
||||
|
|
@ -23,6 +24,8 @@ const MyPlanScreen: React.FC = () => {
|
|||
(state: RootState) => state.payments.due_amount
|
||||
);
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
// Helper function to format currency
|
||||
const formatCurrency = (amount?: number | null): string => {
|
||||
if (amount == null) return "---";
|
||||
|
|
@ -102,7 +105,7 @@ const MyPlanScreen: React.FC = () => {
|
|||
<View style={styles.halfCard}>
|
||||
<Text style={styles.label}>Monthly EMI</Text>
|
||||
<Text style={styles.value}>
|
||||
{formatCurrency(myPlan?.total_emi)}
|
||||
{formatCurrency(myPlan?.emi_amount)}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
|
|
@ -129,7 +132,10 @@ const MyPlanScreen: React.FC = () => {
|
|||
)}
|
||||
/>
|
||||
|
||||
<TouchableOpacity style={styles.payButton}>
|
||||
<TouchableOpacity
|
||||
style={styles.payButton}
|
||||
onPress={() => router.push("/payments/selectAmount")}
|
||||
>
|
||||
<Text style={styles.payButtonText}>Pay EMI</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
|
@ -141,12 +147,6 @@ const styles = StyleSheet.create({
|
|||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: "#F3F5F8",
|
||||
paddingTop: 16,
|
||||
},
|
||||
header: {
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 16,
|
||||
backgroundColor: "#F3F5F8",
|
||||
},
|
||||
headerTitle: {
|
||||
fontSize: 18,
|
||||
|
|
@ -261,7 +261,7 @@ const styles = StyleSheet.create({
|
|||
borderRadius: 10,
|
||||
},
|
||||
payButton: {
|
||||
backgroundColor: "#4CAF50",
|
||||
backgroundColor: "#008761",
|
||||
borderRadius: 8,
|
||||
paddingVertical: 16,
|
||||
alignItems: "center",
|
||||
|
|
|
|||
|
|
@ -108,7 +108,7 @@ const UpiPaymentScreen = () => {
|
|||
};
|
||||
|
||||
const getUpiUrl = (): string => {
|
||||
const upiString = paymentOrder.ppayment_link;
|
||||
const upiString = paymentOrder.payment_link;
|
||||
|
||||
const upiMatch = upiString.match(/upi_string=([^&]+)/);
|
||||
if (upiMatch) {
|
||||
|
|
@ -134,32 +134,12 @@ const UpiPaymentScreen = () => {
|
|||
if (canOpenUrl) {
|
||||
await Linking.openURL(upiUrl);
|
||||
} else {
|
||||
Alert.alert(
|
||||
"UPI App Required",
|
||||
"Please install a UPI-enabled app like PhonePe, Paytm, Google Pay, or BHIM to make payments.",
|
||||
[
|
||||
{
|
||||
text: "Share Payment Details",
|
||||
onPress: () => sharePaymentDetails(),
|
||||
},
|
||||
{ text: "Cancel", style: "cancel" },
|
||||
]
|
||||
);
|
||||
showSnackbar("UPI App Required.", "error");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error opening UPI app:", error);
|
||||
dispatch(updatePaymentStatus("failed"));
|
||||
Alert.alert(
|
||||
"Error",
|
||||
"Unable to open UPI app. Would you like to share payment details instead?",
|
||||
[
|
||||
{
|
||||
text: "Share Details",
|
||||
onPress: () => sharePaymentDetails(),
|
||||
},
|
||||
{ text: "Cancel", style: "cancel" },
|
||||
]
|
||||
);
|
||||
showSnackbar("Unable to open UPI App", "error");
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -179,7 +159,7 @@ const UpiPaymentScreen = () => {
|
|||
title: "UPI Payment Details",
|
||||
});
|
||||
} catch (error) {
|
||||
Alert.alert("Error", "Failed to share payment details");
|
||||
showSnackbar("Failed to share payment details", "error");
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -204,10 +184,7 @@ const UpiPaymentScreen = () => {
|
|||
try {
|
||||
const { status } = await MediaLibrary.requestPermissionsAsync();
|
||||
if (status !== "granted") {
|
||||
Alert.alert(
|
||||
"Permission Required",
|
||||
"Please grant permission to save images"
|
||||
);
|
||||
showSnackbar("Please grant permission to save images", "error");
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -221,12 +198,12 @@ const UpiPaymentScreen = () => {
|
|||
if (downloadResult.status === 200) {
|
||||
const asset = await MediaLibrary.createAssetAsync(downloadResult.uri);
|
||||
await MediaLibrary.createAlbumAsync("Payment QR Codes", asset, false);
|
||||
Alert.alert("Success", "QR code saved to gallery");
|
||||
showSnackbar("QR code saved to gallery", "success");
|
||||
} else {
|
||||
Alert.alert("Error", "Failed to download QR code");
|
||||
showSnackbar("Failed to Download QR code", "error");
|
||||
}
|
||||
} catch (error) {
|
||||
Alert.alert("Error", "Failed to download QR code");
|
||||
showSnackbar("Failed to Download QR code", "error");
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -140,6 +140,7 @@ const SelectAmountScreen = () => {
|
|||
initialValues={initialValues}
|
||||
validationSchema={validationSchema}
|
||||
onSubmit={handleSubmit}
|
||||
// Add validateOnChange to make validation more responsive
|
||||
validateOnChange={true}
|
||||
validateOnBlur={true}
|
||||
>
|
||||
|
|
@ -169,6 +170,7 @@ const SelectAmountScreen = () => {
|
|||
return values.customAmount ? parseFloat(values.customAmount) : 0;
|
||||
};
|
||||
|
||||
// Improved button state logic
|
||||
const isPayButtonEnabled = () => {
|
||||
if (values.paymentType === "due") return true;
|
||||
|
||||
|
|
@ -190,13 +192,17 @@ const SelectAmountScreen = () => {
|
|||
<KeyboardAvoidingView
|
||||
style={styles.container}
|
||||
behavior={Platform.OS === "ios" ? "padding" : "height"}
|
||||
keyboardVerticalOffset={Platform.OS === "ios" ? 0 : 20}
|
||||
// Improved keyboard offset
|
||||
keyboardVerticalOffset={Platform.OS === "ios" ? 88 : 0}
|
||||
>
|
||||
<Header title="Select Amount" showBackButton={true} />
|
||||
|
||||
<ScrollView
|
||||
style={styles.content}
|
||||
showsVerticalScrollIndicator={false}
|
||||
// Add keyboard dismiss on scroll
|
||||
keyboardShouldPersistTaps="handled"
|
||||
contentContainerStyle={styles.scrollContent}
|
||||
>
|
||||
<View style={styles.selectAmountContainer}>
|
||||
<TouchableOpacity
|
||||
|
|
@ -262,7 +268,14 @@ const SelectAmountScreen = () => {
|
|||
]}
|
||||
value={values.customAmount}
|
||||
onChangeText={(text) => {
|
||||
handleChange("customAmount")(text);
|
||||
const numericText = text.replace(/[^0-9.]/g, "");
|
||||
const parts = numericText.split(".");
|
||||
const formattedText =
|
||||
parts.length > 2
|
||||
? parts[0] + "." + parts.slice(1).join("")
|
||||
: numericText;
|
||||
|
||||
setFieldValue("customAmount", formattedText, true);
|
||||
setFieldValue("paymentType", "custom");
|
||||
}}
|
||||
onBlur={handleBlur("customAmount")}
|
||||
|
|
@ -270,6 +283,8 @@ const SelectAmountScreen = () => {
|
|||
placeholderTextColor="#94A3B8"
|
||||
keyboardType="numeric"
|
||||
onFocus={() => setFieldValue("paymentType", "custom")}
|
||||
// Add return key handling
|
||||
returnKeyType="done"
|
||||
/>
|
||||
<View style={styles.helperContainer}>
|
||||
<Text
|
||||
|
|
@ -345,6 +360,10 @@ const styles = StyleSheet.create({
|
|||
flex: 1,
|
||||
paddingHorizontal: 16,
|
||||
},
|
||||
// Add scroll content style for better keyboard handling
|
||||
scrollContent: {
|
||||
paddingBottom: 20,
|
||||
},
|
||||
selectAmountContainer: {
|
||||
paddingTop: 16,
|
||||
gap: 16,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,8 @@
|
|||
<svg width="81" height="81" viewBox="0 0 81 81" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<mask id="mask0_709_4789" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="81" height="81">
|
||||
<rect x="0.09375" y="0.123047" width="80" height="80" fill="#D9D9D9"/>
|
||||
</mask>
|
||||
<g mask="url(#mask0_709_4789)">
|
||||
<path d="M40.0931 44.7897L49.7598 54.4564C50.3709 55.0675 51.1487 55.3731 52.0931 55.3731C53.0375 55.3731 53.8153 55.0675 54.4264 54.4564C55.0375 53.8453 55.3431 53.0675 55.3431 52.1231C55.3431 51.1786 55.0375 50.4008 54.4264 49.7897L44.7598 40.1231L54.4264 30.4564C55.0375 29.8453 55.3431 29.0675 55.3431 28.1231C55.3431 27.1786 55.0375 26.4008 54.4264 25.7897C53.8153 25.1786 53.0375 24.8731 52.0931 24.8731C51.1487 24.8731 50.3709 25.1786 49.7598 25.7897L40.0931 35.4564L30.4264 25.7897C29.8153 25.1786 29.0375 24.8731 28.0931 24.8731C27.1487 24.8731 26.3709 25.1786 25.7598 25.7897C25.1487 26.4008 24.8431 27.1786 24.8431 28.1231C24.8431 29.0675 25.1487 29.8453 25.7598 30.4564L35.4264 40.1231L25.7598 49.7897C25.1487 50.4008 24.8431 51.1786 24.8431 52.1231C24.8431 53.0675 25.1487 53.8453 25.7598 54.4564C26.3709 55.0675 27.1487 55.3731 28.0931 55.3731C29.0375 55.3731 29.8153 55.0675 30.4264 54.4564L40.0931 44.7897ZM40.0931 73.4564C35.482 73.4564 31.1487 72.5814 27.0931 70.8314C23.0375 69.0814 19.5098 66.7064 16.5098 63.7064C13.5098 60.7064 11.1348 57.1786 9.38477 53.1231C7.63477 49.0675 6.75977 44.7342 6.75977 40.1231C6.75977 35.512 7.63477 31.1786 9.38477 27.1231C11.1348 23.0675 13.5098 19.5397 16.5098 16.5397C19.5098 13.5397 23.0375 11.1647 27.0931 9.41473C31.1487 7.66473 35.482 6.78973 40.0931 6.78973C44.7042 6.78973 49.0375 7.66473 53.0931 9.41473C57.1487 11.1647 60.6764 13.5397 63.6764 16.5397C66.6764 19.5397 69.0514 23.0675 70.8014 27.1231C72.5514 31.1786 73.4264 35.512 73.4264 40.1231C73.4264 44.7342 72.5514 49.0675 70.8014 53.1231C69.0514 57.1786 66.6764 60.7064 63.6764 63.7064C60.6764 66.7064 57.1487 69.0814 53.0931 70.8314C49.0375 72.5814 44.7042 73.4564 40.0931 73.4564Z" fill="#D51D10"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.0 KiB |
|
|
@ -0,0 +1,8 @@
|
|||
<svg width="80" height="81" viewBox="0 0 80 81" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<mask id="mask0_709_4861" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="80" height="81">
|
||||
<rect y="0.123047" width="80" height="80" fill="#D9D9D9"/>
|
||||
</mask>
|
||||
<g mask="url(#mask0_709_4861)">
|
||||
<path d="M39.9993 56.7897C40.9438 56.7897 41.7355 56.4703 42.3744 55.8314C43.0132 55.1925 43.3327 54.4008 43.3327 53.4564C43.3327 52.512 43.0132 51.7203 42.3744 51.0814C41.7355 50.4425 40.9438 50.1231 39.9993 50.1231C39.0549 50.1231 38.2632 50.4425 37.6243 51.0814C36.9855 51.7203 36.666 52.512 36.666 53.4564C36.666 54.4008 36.9855 55.1925 37.6243 55.8314C38.2632 56.4703 39.0549 56.7897 39.9993 56.7897ZM39.9993 43.4564C40.9438 43.4564 41.7355 43.137 42.3744 42.4981C43.0132 41.8592 43.3327 41.0675 43.3327 40.1231V26.7897C43.3327 25.8453 43.0132 25.0536 42.3744 24.4147C41.7355 23.7758 40.9438 23.4564 39.9993 23.4564C39.0549 23.4564 38.2632 23.7758 37.6243 24.4147C36.9855 25.0536 36.666 25.8453 36.666 26.7897V40.1231C36.666 41.0675 36.9855 41.8592 37.6243 42.4981C38.2632 43.137 39.0549 43.4564 39.9993 43.4564ZM39.9993 73.4564C35.3882 73.4564 31.0549 72.5814 26.9993 70.8314C22.9438 69.0814 19.416 66.7064 16.416 63.7064C13.416 60.7064 11.041 57.1786 9.29102 53.1231C7.54102 49.0675 6.66602 44.7342 6.66602 40.1231C6.66602 35.512 7.54102 31.1786 9.29102 27.1231C11.041 23.0675 13.416 19.5397 16.416 16.5397C19.416 13.5397 22.9438 11.1647 26.9993 9.41473C31.0549 7.66473 35.3882 6.78973 39.9993 6.78973C44.6105 6.78973 48.9438 7.66473 52.9994 9.41473C57.0549 11.1647 60.5827 13.5397 63.5827 16.5397C66.5827 19.5397 68.9577 23.0675 70.7077 27.1231C72.4577 31.1786 73.3327 35.512 73.3327 40.1231C73.3327 44.7342 72.4577 49.0675 70.7077 53.1231C68.9577 57.1786 66.5827 60.7064 63.5827 63.7064C60.5827 66.7064 57.0549 69.0814 52.9994 70.8314C48.9438 72.5814 44.6105 73.4564 39.9993 73.4564Z" fill="#FF7B00"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.9 KiB |
|
|
@ -15,9 +15,11 @@ export default ({
|
|||
method,
|
||||
status,
|
||||
}: PaymentHistoryCardProps) => {
|
||||
//failure, confirmed, pending
|
||||
console.log(status, "payment Status");
|
||||
const getStatusStyle = (status: string) => {
|
||||
switch (status.toLowerCase()) {
|
||||
case "failed":
|
||||
case "failure":
|
||||
return {
|
||||
backgroundColor: "#FDE8E7",
|
||||
color: "#D51C10",
|
||||
|
|
@ -27,7 +29,7 @@ export default ({
|
|||
backgroundColor: "#FFF0E3",
|
||||
color: "#8E4400",
|
||||
};
|
||||
case "completed":
|
||||
case "confirmed":
|
||||
return {
|
||||
backgroundColor: "#E8F5E8",
|
||||
color: "#2D7D32",
|
||||
|
|
@ -52,7 +54,7 @@ export default ({
|
|||
<Text style={styles.paymentDetails}>
|
||||
{time} • {method}
|
||||
</Text>
|
||||
{status && (
|
||||
{status && status !== "confirmed" && (
|
||||
<View
|
||||
style={[
|
||||
styles.statusBadge,
|
||||
|
|
@ -124,7 +126,6 @@ const styles = StyleSheet.create({
|
|||
marginTop: 2, // slight vertical offset from Figma y=2
|
||||
},
|
||||
statusBadge: {
|
||||
width: 63,
|
||||
height: 20,
|
||||
borderRadius: 4,
|
||||
justifyContent: "center",
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ const Header: React.FC<HeaderProps> = ({
|
|||
};
|
||||
|
||||
return (
|
||||
<View style={[styles.container, { paddingTop: insets.top }]}>
|
||||
<View style={[styles.container, { marginTop: insets.top }]}>
|
||||
<View style={styles.leftContainer}>
|
||||
{showBackButton && (
|
||||
<TouchableOpacity style={styles.backButton} onPress={handleBack}>
|
||||
|
|
|
|||
|
|
@ -10,10 +10,15 @@ import { MaterialIcons } from "@expo/vector-icons";
|
|||
|
||||
interface PaymentDueCardProps {
|
||||
label: string;
|
||||
amount: string;
|
||||
amount: number;
|
||||
onPress: () => void;
|
||||
}
|
||||
|
||||
const formatAmount = (amount: number | null) => {
|
||||
if (!amount) return "₹0";
|
||||
return `₹${amount.toLocaleString("en-IN")}`;
|
||||
};
|
||||
|
||||
const PaymentDueCard: React.FC<PaymentDueCardProps> = ({
|
||||
label,
|
||||
amount,
|
||||
|
|
@ -27,7 +32,7 @@ const PaymentDueCard: React.FC<PaymentDueCardProps> = ({
|
|||
</View>
|
||||
<View style={styles.textGroup}>
|
||||
<Text style={styles.label}>{label}</Text>
|
||||
<Text style={styles.amount}>{amount}</Text>
|
||||
<Text style={styles.amount}>{formatAmount(amount)}</Text>
|
||||
</View>
|
||||
</View>
|
||||
<TouchableOpacity
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import {
|
|||
} from "react-native";
|
||||
import Feather from "../../assets/icons/chevron_rightside.svg";
|
||||
import { ALERT_STYLES } from "@/constants/config";
|
||||
import { Href, useRouter } from "expo-router";
|
||||
|
||||
type AlertType = "info" | "warning" | "danger";
|
||||
|
||||
|
|
@ -15,19 +16,30 @@ interface AlertCardProps {
|
|||
type: AlertType;
|
||||
message: string;
|
||||
subMessage?: string;
|
||||
redirectPath: Href;
|
||||
}
|
||||
|
||||
const AlertCard: React.FC<AlertCardProps> = ({ type, message, subMessage }) => {
|
||||
const AlertCard: React.FC<AlertCardProps> = ({
|
||||
type,
|
||||
message,
|
||||
subMessage,
|
||||
redirectPath,
|
||||
}) => {
|
||||
const style = ALERT_STYLES[type];
|
||||
|
||||
const router = useRouter();
|
||||
const containerStyle: ViewStyle[] = [
|
||||
styles.container,
|
||||
{ backgroundColor: style.backgroundColor },
|
||||
];
|
||||
|
||||
function handlePress() {
|
||||
router.push(redirectPath);
|
||||
}
|
||||
return (
|
||||
<TouchableOpacity
|
||||
style={[containerStyle, { backgroundColor: style.backgroundColor }]}
|
||||
activeOpacity={0.9}
|
||||
onPress={() => handlePress()}
|
||||
>
|
||||
<View style={styles.leftContent}>
|
||||
<style.icon />
|
||||
|
|
|
|||
|
|
@ -187,4 +187,5 @@ export const payments = {
|
|||
REGISTER_TRANSACTION_EMIT_EVENT_NAME: "register-transaction",
|
||||
REGISTER_TRANSACTION_LISTEN_EVENT_NAME: "registration-ack",
|
||||
PAYMENT_CONFIRMATION_EVENT_NAME: "register-transaction",
|
||||
EMI_WARNING_DAYS_THRESHOLD: 14,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ interface PaymentOrder {
|
|||
id?: number; // optional if not always present
|
||||
expiry_date?: string;
|
||||
order_id: string;
|
||||
ppayment_link?: string;
|
||||
payment_link?: string;
|
||||
qr_code_url?: string;
|
||||
status: string; // "confirmed", "pending", etc.
|
||||
transaction_id?: string;
|
||||
|
|
|
|||
|
|
@ -33,18 +33,55 @@ interface Battery {
|
|||
charger_uid: string;
|
||||
}
|
||||
|
||||
interface PaymentSummary {
|
||||
due_amount: number;
|
||||
daysLeftToPayEmi: number;
|
||||
daysLeftToPayEmiText: string;
|
||||
regularServiceDueInDaysText: string;
|
||||
}
|
||||
|
||||
interface UserState {
|
||||
data: UserData | null;
|
||||
paymentSummary: PaymentSummary | null;
|
||||
loading: boolean;
|
||||
error: string | null;
|
||||
}
|
||||
|
||||
const initialState: UserState = {
|
||||
data: null,
|
||||
paymentSummary: null,
|
||||
loading: false,
|
||||
error: null,
|
||||
};
|
||||
|
||||
export const getPaymentSummary = createAsyncThunk<PaymentSummary>(
|
||||
"user/getPaymentSummary",
|
||||
async (_, { rejectWithValue }) => {
|
||||
try {
|
||||
console.log("Fetching payment summary from API...");
|
||||
const response = await api.get(`${BASE_URL}/api/v1/payments-summary`);
|
||||
if (response.data.success) {
|
||||
console.log("Payment summary fetched:", response.data);
|
||||
return {
|
||||
due_amount: response.data.due_amount,
|
||||
daysLeftToPayEmi: response.data.daysLeftToPayEmi,
|
||||
daysLeftToPayEmiText: response.data.daysLeftToPayEmiText,
|
||||
regularServiceDueInDaysText:
|
||||
response.data.regularServiceDueInDaysText || "",
|
||||
};
|
||||
} else {
|
||||
return rejectWithValue("Failed to fetch payment summary");
|
||||
}
|
||||
} catch (error: any) {
|
||||
const message =
|
||||
error.response?.data?.message ||
|
||||
error.message ||
|
||||
"Something went wrong";
|
||||
return rejectWithValue(message);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// 🔁 Thunk to get user details
|
||||
export const getUserDetails = createAsyncThunk<UserData>(
|
||||
"user/getUserDetails",
|
||||
|
|
@ -79,6 +116,7 @@ const userSlice = createSlice({
|
|||
state.data = null;
|
||||
state.error = null;
|
||||
state.loading = false;
|
||||
state.paymentSummary = null;
|
||||
},
|
||||
setUserData: (state, action: PayloadAction<Partial<UserData>>) => {
|
||||
if (state.data) {
|
||||
|
|
@ -107,6 +145,20 @@ const userSlice = createSlice({
|
|||
.addCase(getUserDetails.rejected, (state, action) => {
|
||||
state.loading = false;
|
||||
state.error = action.payload as string;
|
||||
})
|
||||
|
||||
// Payment summary
|
||||
.addCase(getPaymentSummary.pending, (state) => {
|
||||
state.loading = true;
|
||||
state.error = null;
|
||||
})
|
||||
.addCase(getPaymentSummary.fulfilled, (state, action) => {
|
||||
state.loading = false;
|
||||
state.paymentSummary = action.payload;
|
||||
})
|
||||
.addCase(getPaymentSummary.rejected, (state, action) => {
|
||||
state.loading = false;
|
||||
state.error = action.payload as string;
|
||||
});
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -0,0 +1,6 @@
|
|||
export const displayValue = (value: any, formatter?: (val: any) => string) => {
|
||||
if (value === null || value === undefined || value === "") {
|
||||
return "--";
|
||||
}
|
||||
return formatter ? formatter(value) : value;
|
||||
};
|
||||
Loading…
Reference in New Issue