diff --git a/app/(tabs)/_layout.tsx b/app/(tabs)/_layout.tsx
index 0d4a668..d0aa448 100644
--- a/app/(tabs)/_layout.tsx
+++ b/app/(tabs)/_layout.tsx
@@ -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) {
diff --git a/app/(tabs)/index.tsx b/app/(tabs)/index.tsx
index c05760f..0be4815 100644
--- a/app/(tabs)/index.tsx
+++ b/app/(tabs)/index.tsx
@@ -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: () => (
- handleManualRefresh()}>
-
+
+
+
+
{
- console.log("Support Pressed");
- setIsSupportModalVisible(true);
- }}
+ onPress={() => setIsSupportModalVisible(true)}
>
- {
router.push("/user/profile");
}}
>
router.push("/user/profile")}
textSize={20}
boxSize={40}
/>
-
+
),
});
- }, [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 (
-
+ {daysLeftToPayEmi && daysLeftToPayEmiText && daysLeftToPayEmi > 0 && (
+ = payments.EMI_WARNING_DAYS_THRESHOLD
+ ? "warning"
+ : "danger"
+ }
+ message={daysLeftToPayEmiText}
+ subMessage="Pay now"
+ redirectPath="/(tabs)/payments"
+ />
+ )}
+
+ {regularServiceDueInDaysText && (
+
+ )}
+
@@ -178,13 +254,15 @@ export default function HomeScreen() {
unit="km"
/>
- {
- alert("le is number pe bhej de 8685846459");
- }}
- />
+ {due_amount && (
+ {
+ router.push("/payments/selectAmount");
+ }}
+ />
+ )}
{loading ? (
diff --git a/app/(tabs)/payments.tsx b/app/(tabs)/payments.tsx
index 9823c2f..54c5e89 100644
--- a/app/(tabs)/payments.tsx
+++ b/app/(tabs)/payments.tsx
@@ -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(
[]
);
+ 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,37 +141,79 @@ export default function PaymentsTabScreen() {
const model = vehicle?.model ?? "---";
const chasisNumber = vehicle?.chasis_number ?? "---";
- // Fetch EMI details
- useEffect(() => {
- const fetchEmiDetails = async () => {
- try {
- setIsLoading(true);
+ const fetchEmiDetails = async () => {
+ try {
+ setIsLoading(true);
+ setEmiDetails(null);
- const response = await api.get(`/api/v1/emi-details`);
- const result: EmiResponse = response.data;
+ const response = await api.get(`/api/v1/emi-details`);
+ const result: EmiResponse = response.data;
- if (result.success && result.data.length > 0) {
- const details = result.data[0];
+ if (result.success && result.data.length > 0) {
+ const details = result.data[0];
- setEmiDetails(details);
+ setEmiDetails(details);
- dispatch(setDueAmount(details.due_amount));
- dispatch(setMyPlan(details.myPlain));
- } else {
- showSnackbar("No EMI details found", "error");
- }
- } catch (err) {
- console.error("Error fetching EMI details:", err);
- const errorMessage =
- err instanceof Error ? err.message : "Something went wrong";
- showSnackbar(errorMessage, "error");
- } finally {
- setIsLoading(false);
+ dispatch(setDueAmount(details.due_amount));
+ dispatch(setMyPlan(details.myPlain));
+ } else {
+ showSnackbar("No EMI details found", "error");
}
- };
+ } catch (err) {
+ console.error("Error fetching EMI details:", err);
+ const errorMessage =
+ err instanceof Error ? err.message : "Something went wrong";
+ showSnackbar(errorMessage, "error");
+ } finally {
+ setIsLoading(false);
+ }
+ };
- fetchEmiDetails();
- }, []);
+ 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: () => (
+ handleRefresh()} disabled={refreshing}>
+
+
+
+
{
@@ -184,7 +244,7 @@ export default function PaymentsTabScreen() {
setIsSupportModalVisible(true);
}}
>
-
+
{
@@ -201,7 +261,7 @@ export default function PaymentsTabScreen() {
),
});
- }, [navigation, data, model, chasisNumber]);
+ }, [navigation, data, model, chasisNumber, refreshing]);
// Format currency
const formatCurrency = (amount: number) => {
@@ -307,150 +367,154 @@ export default function PaymentsTabScreen() {
setIsEndReached(isAtBottom);
};
- useEffect(() => {
- fetchPaymentHistory(1, false);
- }, []);
-
return (
-
- {/* Last EMI Details Card */}
-
- {/* Header */}
-
- Last EMI Details
- {getCurrentMonthYear()}
-
+ <>
+
+ {/* Last EMI Details Card */}
+
+ {/* Header */}
+
+ Last EMI Details
+ {getCurrentMonthYear()}
+
- {/* Divider */}
-
+ {/* Divider */}
+
- {/* EMI Details Content */}
- {true && (
+ {/* EMI Details Content */}
Amount Due
- {emiDetails?.due_amount
- ? formatCurrency(emiDetails.due_amount)
- : "---"}
+ {displayValue(emiDetails?.due_amount, formatCurrency)}
Amount Paid
- {emiDetails?.total_amount_paid_in_current_cycle &&
- formatCurrency(emiDetails.total_amount_paid_in_current_cycle)}
+ {displayValue(
+ emiDetails?.total_amount_paid_in_current_cycle,
+ formatCurrency
+ )}
Due Date
- {emiDetails?.due_date && emiDetails.due_date}
+ {displayValue(emiDetails?.due_date)}
Payment Status
-
+ {emiDetails?.status ? (
+
+ ) : (
+ --
+ )}
- )}
-
+
-
- router.push("/payments/selectAmount")}
- >
- Pay EMI
-
+
+ router.push("/payments/selectAmount")}
+ >
+ Pay EMI
+
- router.push("/payments/myPlan")}
- >
- View Plan
-
+ router.push("/payments/myPlan")}
+ >
+ View Plan
+
+
-
-
- Payment History
+
+ Payment History
-
- {isHistoryLoading ? (
-
-
-
- ) : (
- <>
- {/* Show initial payments or all payments based on showFullHistory */}
- {(showFullHistory
- ? paymentHistory
- : paymentHistory.slice(0, 3)
- ).map((payment) => (
-
- ))}
+
+ {isHistoryLoading ? (
+
+
+
+ ) : (
+ <>
+ {/* Show initial payments or all payments based on showFullHistory */}
+ {(showFullHistory
+ ? paymentHistory
+ : paymentHistory.slice(0, 3)
+ ).map((payment) => (
+
+ ))}
- {!showFullHistory && paymentHistory.length > 2 && (
-
- View all
- ›
-
- )}
+ {!showFullHistory && paymentHistory.length > 2 && (
+
+ View all
+ ›
+
+ )}
- {isLoadingMore && (
-
-
-
-
-
- )}
-
- {showFullHistory &&
- !hasMorePages &&
- paymentHistory.length > 0 && (
-
-
- No more payments to show
+ {isLoadingMore && (
+
+
+
)}
- {/* Empty state */}
- {paymentHistory.length === 0 && !isHistoryLoading && (
-
-
- No payment history found
-
-
- )}
- >
- )}
+ {showFullHistory &&
+ !hasMorePages &&
+ paymentHistory.length > 0 && (
+
+
+ No more payments to show
+
+
+ )}
+
+ {/* Empty state */}
+ {paymentHistory.length === 0 && !isHistoryLoading && (
+
+
+ No payment history found
+
+
+ )}
+ >
+ )}
+
-
-
+
+ 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",
diff --git a/app/payments/Confirmation.tsx b/app/payments/Confirmation.tsx
index 3fc5dbf..028adb3 100644
--- a/app/payments/Confirmation.tsx
+++ b/app/payments/Confirmation.tsx
@@ -138,6 +138,7 @@ const styles = StyleSheet.create({
paddingHorizontal: 16,
paddingBottom: 16,
justifyContent: "space-between",
+ marginTop: 8,
},
qrFrame: {
backgroundColor: "#fcfcfc",
diff --git a/app/payments/TransactionDetails.tsx b/app/payments/TransactionDetails.tsx
new file mode 100644
index 0000000..608313f
--- /dev/null
+++ b/app/payments/TransactionDetails.tsx
@@ -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(null);
+ const [isLoading, setIsLoading] = useState(true);
+
+ useLayoutEffect(() => {
+ navigation.setOptions({
+ headerStyle: {
+ backgroundColor: "#F3F5F8",
+ },
+ headerTitle: "Transaction Detail",
+ headerTitleStyle: {
+ fontSize: 18,
+ fontWeight: "600",
+ color: "#111827",
+ },
+ headerLeft: () => (
+ router.back()}
+ style={styles.backButton}
+ >
+ ‹
+
+ ),
+ });
+ }, [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 ;
+ } else if (status.toLowerCase() === "pending") {
+ return ;
+ } else {
+ return ;
+ }
+ };
+
+ 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 (
+
+
+
+
+
+ );
+ }
+
+ if (!transactionData) {
+ return (
+
+
+ Transaction not found
+
+
+ );
+ }
+
+ return (
+
+
+ {/* Status Icon */}
+
+ {getStatusIcon(transactionData.status)}
+
+
+ {/* Amount */}
+
+ {formatCurrency(transactionData.amount)}
+
+
+ {/* Status Text */}
+
+ {getStatusText(transactionData.status)}
+
+
+ {/* Date Time */}
+
+ {formatDateTime(transactionData.transaction_date)}
+
+
+ {/* Transaction Details Card */}
+
+ Transaction Details
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+const DetailRow = ({
+ label,
+ value,
+ isLast = false,
+}: {
+ label: string;
+ value: string;
+ isLast?: boolean;
+}) => (
+
+ {label}
+ {value}
+
+);
+
+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",
+ },
+});
diff --git a/app/payments/myPlan.tsx b/app/payments/myPlan.tsx
index 6bd3313..0f84159 100644
--- a/app/payments/myPlan.tsx
+++ b/app/payments/myPlan.tsx
@@ -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 = () => {
Monthly EMI
- {formatCurrency(myPlan?.total_emi)}
+ {formatCurrency(myPlan?.emi_amount)}
@@ -129,7 +132,10 @@ const MyPlanScreen: React.FC = () => {
)}
/>
-
+ router.push("/payments/selectAmount")}
+ >
Pay EMI
@@ -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",
diff --git a/app/payments/payEmi.tsx b/app/payments/payEmi.tsx
index 1d58361..4a9fa5c 100644
--- a/app/payments/payEmi.tsx
+++ b/app/payments/payEmi.tsx
@@ -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");
}
};
diff --git a/app/payments/selectAmount.tsx b/app/payments/selectAmount.tsx
index 4f6ab45..24ddd95 100644
--- a/app/payments/selectAmount.tsx
+++ b/app/payments/selectAmount.tsx
@@ -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 = () => {
{
]}
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"
/>
+
+
+
+
+
+
+
diff --git a/assets/icons/pending.svg b/assets/icons/pending.svg
new file mode 100644
index 0000000..568d61c
--- /dev/null
+++ b/assets/icons/pending.svg
@@ -0,0 +1,8 @@
+
diff --git a/components/Payments/PaymentHistoryCard.tsx b/components/Payments/PaymentHistoryCard.tsx
index ec9b74a..af57a17 100644
--- a/components/Payments/PaymentHistoryCard.tsx
+++ b/components/Payments/PaymentHistoryCard.tsx
@@ -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 ({
{time} • {method}
- {status && (
+ {status && status !== "confirmed" && (
= ({
};
return (
-
+
{showBackButton && (
diff --git a/components/home/PaymentDueCard.tsx b/components/home/PaymentDueCard.tsx
index f9baa01..911dd8b 100644
--- a/components/home/PaymentDueCard.tsx
+++ b/components/home/PaymentDueCard.tsx
@@ -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 = ({
label,
amount,
@@ -27,7 +32,7 @@ const PaymentDueCard: React.FC = ({
{label}
- {amount}
+ {formatAmount(amount)}
= ({ type, message, subMessage }) => {
+const AlertCard: React.FC = ({
+ 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 (
handlePress()}
>
diff --git a/constants/config.ts b/constants/config.ts
index 4ecbe30..92c5943 100644
--- a/constants/config.ts
+++ b/constants/config.ts
@@ -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,
};
diff --git a/store/paymentSlice.ts b/store/paymentSlice.ts
index 9991d75..f0859e4 100644
--- a/store/paymentSlice.ts
+++ b/store/paymentSlice.ts
@@ -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;
diff --git a/store/userSlice.ts b/store/userSlice.ts
index 187fcb1..80aa4b4 100644
--- a/store/userSlice.ts
+++ b/store/userSlice.ts
@@ -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(
+ "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(
"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>) => {
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;
});
},
});
diff --git a/utils/Common.ts b/utils/Common.ts
new file mode 100644
index 0000000..cd4bc31
--- /dev/null
+++ b/utils/Common.ts
@@ -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;
+};