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; +};