diff --git a/app/(tabs)/index.tsx b/app/(tabs)/index.tsx index 394c1ad..c05760f 100644 --- a/app/(tabs)/index.tsx +++ b/app/(tabs)/index.tsx @@ -128,7 +128,7 @@ export default function HomeScreen() { }} > router.push("/user/profile")} textSize={20} boxSize={40} diff --git a/app/(tabs)/payments.tsx b/app/(tabs)/payments.tsx index 7737e47..ae937b1 100644 --- a/app/(tabs)/payments.tsx +++ b/app/(tabs)/payments.tsx @@ -20,6 +20,7 @@ import PaymentHistoryCard from "@/components/Payments/PaymentHistoryCard"; import { BASE_URL } from "@/constants/config"; import { useDispatch } from "react-redux"; import { setDueAmount, setMyPlan } from "@/store/paymentSlice"; +import { ActivityIndicator } from "react-native-paper"; export interface MyPlan { no_of_emi: number; @@ -71,6 +72,7 @@ interface PaymentHistoryResponse { } const formatPaymentDate = (dateString: string) => { + console.log(dateString.split(",")[0], "datestring"); try { return dateString.split(",")[0]; } catch { @@ -80,17 +82,21 @@ const formatPaymentDate = (dateString: string) => { // Format time for payment history const formatPaymentTime = (dateString: string) => { + console.log(dateString, "formattime"); try { - // Extract time and day info from the formatted date - const parts = dateString.split(","); - if (parts.length > 1) { - const timePart = parts[1].trim(); // "5:23:58 pm" - const date = new Date(dateString); - const dayName = date.toLocaleDateString("en-US", { weekday: "long" }); - return `${timePart} | ${dayName}`; + // 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; } - return dateString; - } catch { + + const dayName = parsedDate.toLocaleDateString("en-US", { weekday: "long" }); + return `${timePart.trim()} | ${dayName}`; + } catch (err) { return dateString; } }; @@ -309,7 +315,7 @@ export default function PaymentsTabScreen() { return ( - + Payment History {isHistoryLoading ? ( - Loading payment history... + ) : ( <> {/* Show initial payments or all payments based on showFullHistory */} {(showFullHistory ? paymentHistory - : paymentHistory.slice(0, 2) + : paymentHistory.slice(0, 3) ).map((payment) => ( ))} - {/* View All button */} {!showFullHistory && paymentHistory.length > 2 && ( )} - {/* Loading indicator when fetching more */} {isLoadingMore && ( - Loading more payments... + )} - {/* No more data message */} {showFullHistory && !hasMorePages && paymentHistory.length > 0 && ( @@ -649,23 +652,16 @@ const styles = StyleSheet.create({ borderRadius: 8, padding: 12, }, - paymentHistorySection: { - width: 328, - marginLeft: 16, - marginTop: 20, - // No height fixed here, flexible container - }, sectionTitle: { - fontFamily: "Inter-SemiBold", fontSize: 14, lineHeight: 20, - color: "#252A33", // rgb(37,42,51) from design + color: "#252A34", // rgb(37,42,51) from design marginBottom: 8, + fontWeight: "600", }, paymentHistoryContainer: { - width: 328, flexDirection: "column", - gap: 16, // for spacing between cards, might need polyfill or use margin + gap: 16, }, historyLoadingContainer: { height: 312, @@ -763,6 +759,7 @@ const styles = StyleSheet.create({ }, viewAllButton: { flexDirection: "row", + justifyContent: "center", alignItems: "center", paddingVertical: 8, paddingHorizontal: 16, diff --git a/app/_layout.tsx b/app/_layout.tsx index ae44bb9..01982b8 100644 --- a/app/_layout.tsx +++ b/app/_layout.tsx @@ -11,19 +11,22 @@ import * as SplashScreen from "expo-splash-screen"; import AsyncStorage from "@react-native-async-storage/async-storage"; import { STORAGE_KEYS } from "@/constants/config"; import { setIsLoggedIn } from "@/store/authSlice"; +import { SocketProvider } from "@/contexts/SocketContext"; SplashScreen.preventAutoHideAsync(); export default function RootLayout() { return ( - - - - - - - + + + + + + + + + ); } @@ -38,6 +41,7 @@ function SplashAndAuthRouter() { try { const token = await AsyncStorage.getItem(STORAGE_KEYS.AUTH_TOKEN); + console.log(token, "token of backend"); if (!token) { dispatch(setIsLoggedIn(false)); return; diff --git a/app/payments/Confirmation.tsx b/app/payments/Confirmation.tsx new file mode 100644 index 0000000..17e9921 --- /dev/null +++ b/app/payments/Confirmation.tsx @@ -0,0 +1,58 @@ +import React from "react"; +import { View, Text, StyleSheet, TouchableOpacity } from "react-native"; +import { useRouter } from "expo-router"; + +const PaymentConfirmationScreen = () => { + const router = useRouter(); + + return ( + + Payment Successful! + + Thank you for your payment. Your transaction has been completed. + + + router.push("/dashboard")} // Or wherever you want to go + > + Go to Dashboard + + + ); +}; + +export default PaymentConfirmationScreen; + +const styles = StyleSheet.create({ + container: { + flex: 1, + justifyContent: "center", + alignItems: "center", + padding: 24, + backgroundColor: "#f0f4f7", + }, + title: { + fontSize: 28, + fontWeight: "bold", + marginBottom: 12, + color: "#2e7d32", + }, + message: { + fontSize: 16, + textAlign: "center", + marginBottom: 30, + color: "#555", + }, + button: { + backgroundColor: "#2e7d32", + paddingVertical: 14, + paddingHorizontal: 30, + borderRadius: 8, + }, + buttonText: { + color: "white", + fontSize: 16, + fontWeight: "600", + }, +}); diff --git a/app/payments/payEmi.tsx b/app/payments/payEmi.tsx index 146e406..9cae28c 100644 --- a/app/payments/payEmi.tsx +++ b/app/payments/payEmi.tsx @@ -1,4 +1,4 @@ -import React, { useEffect } from "react"; +import React, { useEffect, useState } from "react"; import { View, Text, @@ -8,9 +8,9 @@ import { Alert, Share, Clipboard, - Platform, Linking, ActivityIndicator, + BackHandler, } from "react-native"; import { Ionicons } from "@expo/vector-icons"; import * as FileSystem from "expo-file-system"; @@ -18,37 +18,31 @@ import * as MediaLibrary from "expo-media-library"; import * as Sharing from "expo-sharing"; import { Image } from "expo-image"; import { useSelector, useDispatch } from "react-redux"; -import { RootState } from "@/store"; // Adjust path as needed +import { RootState } from "@/store"; import { updatePaymentStatus } from "@/store/paymentSlice"; +import Header from "@/components/common/Header"; +import ShareIcon from "@/assets/icons/share.svg"; +import { useSafeAreaInsets } from "react-native-safe-area-context"; +import { useSnackbar } from "@/contexts/Snackbar"; -interface UpiPaymentScreenProps { - onBack?: () => void; - onPaymentSuccess?: () => void; - onPaymentFailure?: () => void; -} +import DownloadIcon from "@/assets/icons/download.svg"; +import { payments } from "@/constants/config"; +import { useRouter } from "expo-router"; +import { useSocket } from "@/contexts/SocketContext"; -const UpiPaymentScreen: React.FC = ({ - onBack, - onPaymentSuccess, - onPaymentFailure, -}) => { +const UpiPaymentScreen = () => { const dispatch = useDispatch(); const paymentOrder = useSelector( (state: RootState) => state.payments.paymentOrder ); - useEffect(() => { - // Check if payment order exists - if (!paymentOrder) { - Alert.alert( - "Error", - "No payment order found. Please create a payment order first.", - [{ text: "OK", onPress: onBack }] - ); - } - }, [paymentOrder]); + const router = useRouter(); + const { onPaymentConfirmation, offPaymentConfirmation } = useSocket(); + const [isListening, setIsListening] = useState(false); - // Show loading if no payment data + const { showSnackbar } = useSnackbar(); + + const insets = useSafeAreaInsets(); if (!paymentOrder) { return ( @@ -60,6 +54,67 @@ const UpiPaymentScreen: React.FC = ({ ); } + useEffect(() => { + // Set up payment confirmation listener + const handlePaymentConfirmation = (data: any) => { + console.log("Payment confirmation received:", data); + + // Show success message + Alert.alert( + "Payment Successful!", + "Your payment has been confirmed successfully.", + [ + { + text: "Continue", + onPress: () => { + router.replace("/payments/Confirmation"); + }, + }, + ], + { cancelable: false } + ); + }; + + // Start listening for payment confirmation + onPaymentConfirmation(handlePaymentConfirmation); + setIsListening(true); + + // Handle Android back button + const backAction = () => { + Alert.alert( + "Cancel Payment?", + "Are you sure you want to cancel this payment?", + [ + { + text: "No", + onPress: () => null, + style: "cancel", + }, + { + text: "Yes", + onPress: () => { + offPaymentConfirmation(); + router.back(); + }, + }, + ] + ); + return true; + }; + + const backHandler = BackHandler.addEventListener( + "hardwareBackPress", + backAction + ); + + // Cleanup on unmount + return () => { + offPaymentConfirmation(); + setIsListening(false); + backHandler.remove(); + }; + }, [onPaymentConfirmation, offPaymentConfirmation, router]); + // Format amount with currency symbol const formatAmount = (amount: number): string => { return `₹${amount.toLocaleString("en-IN")}`; @@ -83,82 +138,33 @@ const UpiPaymentScreen: React.FC = ({ return expiryDate.toLocaleString("en-IN"); }; - // Generate UPI payment URL from the API response const getUpiUrl = (): string => { - // Use the UPI string from the API response const upiString = paymentOrder.ppayment_link; - // Extract the UPI URL from the deep link const upiMatch = upiString.match(/upi_string=([^&]+)/); if (upiMatch) { return decodeURIComponent(upiMatch[1]); } - - // Fallback: construct UPI URL manually return `upi://pay?pa=${paymentOrder.upi_handle}&am=${paymentOrder.amount}&cu=INR&tr=${paymentOrder.order_id}`; }; const payUsingUpiApp = async (): Promise => { try { - // Check if payment is expired if (isPaymentExpired()) { - Alert.alert( - "Payment Expired", - "This payment order has expired. Please create a new payment order.", - [{ text: "OK", onPress: onBack }] - ); - return; - } - - // Check if payment is already completed - if ( - paymentOrder.status === "completed" || - paymentOrder.status === "success" - ) { - Alert.alert( - "Payment Already Completed", - "This payment has already been completed.", - [{ text: "OK", onPress: onPaymentSuccess }] - ); + showSnackbar(payments.LINK_EXPIRED, "error"); return; } const upiUrl = getUpiUrl(); console.log("Opening UPI URL:", upiUrl); - // Update payment status to processing dispatch(updatePaymentStatus("processing")); - // Check if device can handle UPI URLs const canOpenUrl = await Linking.canOpenURL(upiUrl); if (canOpenUrl) { await Linking.openURL(upiUrl); - - // Show payment status dialog - Alert.alert( - "Payment Initiated", - "Payment has been initiated. Please complete the payment in your UPI app.", - [ - { - text: "Payment Completed", - onPress: () => { - dispatch(updatePaymentStatus("completed")); - onPaymentSuccess?.(); - }, - }, - { - text: "Payment Failed", - style: "cancel", - onPress: () => { - dispatch(updatePaymentStatus("failed")); - onPaymentFailure?.(); - }, - }, - ] - ); } else { - // Fallback: Show available UPI apps or general share Alert.alert( "UPI App Required", "Please install a UPI-enabled app like PhonePe, Paytm, Google Pay, or BHIM to make payments.", @@ -191,7 +197,7 @@ const UpiPaymentScreen: React.FC = ({ const sharePaymentDetails = async (): Promise => { try { const shareMessage = - `💰 Payment Request\n\n` + + `Payment Request\n\n` + `Amount: ${formatAmount(paymentOrder.amount)}\n` + `UPI ID: ${paymentOrder.upi_handle}\n` + `Order ID: ${paymentOrder.order_id}\n` + @@ -208,15 +214,6 @@ const UpiPaymentScreen: React.FC = ({ } }; - const copyUpiId = async (): Promise => { - try { - await Clipboard.setString(paymentOrder.upi_handle); - Alert.alert("Copied!", "UPI ID copied to clipboard"); - } catch (error) { - Alert.alert("Error", "Failed to copy UPI ID"); - } - }; - const shareQR = async (): Promise => { try { if (await Sharing.isAvailableAsync()) { @@ -280,12 +277,7 @@ const UpiPaymentScreen: React.FC = ({ return ( - - - - - Pay EMI - +
@@ -294,18 +286,6 @@ const UpiPaymentScreen: React.FC = ({ {formatAmount(paymentOrder.amount)} - - {/* Payment Status */} - - - {paymentOrder.status.toUpperCase()} - - @@ -316,21 +296,9 @@ const UpiPaymentScreen: React.FC = ({ /> - {/* UPI ID Section */} - - {paymentOrder.upi_handle} - - - - {/* Expiry Info */} - - Expires: - {getExpiryTime()} - - - + Share QR @@ -338,7 +306,7 @@ const UpiPaymentScreen: React.FC = ({ onPress={downloadQR} style={styles.secondaryButton} > - + Download QR @@ -346,26 +314,9 @@ const UpiPaymentScreen: React.FC = ({ - - {paymentOrder.status === "completed" - ? "Payment Completed" - : isPaymentExpired() - ? "Payment Expired" - : "Pay using UPI app"} - + Pay using UPI app @@ -444,11 +395,15 @@ const styles = StyleSheet.create({ color: "#FFFFFF", }, qrCodeContainer: { - width: 248, - height: 248, + width: 200, + height: 200, justifyContent: "center", alignItems: "center", marginBottom: 16, + borderWidth: 2, + borderColor: "#E5E9F0", + padding: 12, + borderRadius: 8, }, qrCode: { width: "100%", diff --git a/app/payments/selectAmount.tsx b/app/payments/selectAmount.tsx index cfc6201..cb86af6 100644 --- a/app/payments/selectAmount.tsx +++ b/app/payments/selectAmount.tsx @@ -8,6 +8,7 @@ import { ScrollView, Platform, KeyboardAvoidingView, + Alert, } from "react-native"; import { useDispatch, useSelector } from "react-redux"; import { Formik } from "formik"; @@ -19,6 +20,7 @@ import api from "@/services/axiosClient"; import { setPaymentOrder } from "@/store/paymentSlice"; import { Overlay } from "@/components/common/Overlay"; import { useRouter } from "expo-router"; +import { useSocket } from "@/contexts/SocketContext"; // Validation schema const validationSchema = Yup.object().shape({ @@ -28,18 +30,18 @@ const validationSchema = Yup.object().shape({ then: (schema) => schema .required("Amount is required") + .test("valid-number", "Please enter a valid amount", (value) => { + const numValue = parseFloat(value); + return !isNaN(numValue) && numValue > 0; + }) .test( "min-amount", `Minimum amount is ₹${payments.MIN_AMOUNT}`, (value) => { - const numValue = parseFloat(value || "0"); - return numValue >= payments.MIN_AMOUNT; + const numValue = parseFloat(value); + return !isNaN(numValue) && numValue >= payments.MIN_AMOUNT; } - ) - .test("valid-number", "Please enter a valid amount", (value) => { - const numValue = parseFloat(value || "0"); - return !isNaN(numValue) && numValue > 0; - }), + ), otherwise: (schema) => schema.notRequired(), }), }); @@ -53,6 +55,8 @@ const SelectAmountScreen = () => { const router = useRouter(); const [isFetching, setIsFetching] = useState(false); + const { registerTransaction } = useSocket(); + const quickAmounts = [50, 100, 500, 1000]; const initialValues = { @@ -74,10 +78,18 @@ const SelectAmountScreen = () => { amount: paymentAmount, }); - console.log(res, "response"); + console.log(res.data, "response from select amount"); if (res.data && res.data.success) { dispatch(setPaymentOrder(res.data.data)); - router.push("/payments/payEmi"); + try { + await registerTransaction(res.data.data.transaction_id); + console.log("Transaction registered successfully"); + + // Navigate to payment screen + router.push("/payments/payEmi"); + } catch (socketError) { + throw socketError; + } } else { throw new Error("Failed to create order"); } @@ -224,7 +236,7 @@ const SelectAmountScreen = () => { > {touched.customAmount && errors.customAmount ? errors.customAmount - : "Minimum: ₹200"} + : `Minimum: ₹${payments.MIN_AMOUNT}`} @@ -261,7 +273,7 @@ const SelectAmountScreen = () => { onPress={() => handleSubmit()} disabled={!isPayButtonEnabled()} > - {getPaymentAmount() < 200 ? ( + {getPaymentAmount() < payments.MIN_AMOUNT ? ( Select Amount ) : ( diff --git a/assets/icons/download.svg b/assets/icons/download.svg new file mode 100644 index 0000000..9e8933a --- /dev/null +++ b/assets/icons/download.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/assets/icons/share.svg b/assets/icons/share.svg new file mode 100644 index 0000000..52c8f95 --- /dev/null +++ b/assets/icons/share.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/components/Payments/PaymentHistoryCard.tsx b/components/Payments/PaymentHistoryCard.tsx index 405dd15..ec9b74a 100644 --- a/components/Payments/PaymentHistoryCard.tsx +++ b/components/Payments/PaymentHistoryCard.tsx @@ -17,7 +17,6 @@ export default ({ }: PaymentHistoryCardProps) => { const getStatusStyle = (status: string) => { switch (status.toLowerCase()) { - case "failure": case "failed": return { backgroundColor: "#FDE8E7", @@ -28,9 +27,7 @@ export default ({ backgroundColor: "#FFF0E3", color: "#8E4400", }; - case "confirmed": case "completed": - case "success": return { backgroundColor: "#E8F5E8", color: "#2D7D32", @@ -76,23 +73,20 @@ import { StyleSheet } from "react-native"; const styles = StyleSheet.create({ paymentCard: { - width: 328, height: 76, - backgroundColor: "rgba(252,252,252,1)", // #FCFCFC + width: "100%", + backgroundColor: "rgba(252,252,252,1)", borderRadius: 8, paddingVertical: 12, paddingHorizontal: 12, - justifyContent: "flex-start", - alignItems: "flex-start", + flexDirection: "column", + gap: "12", }, paymentCardTop: { - width: 304, height: 20, - marginLeft: 12, flexDirection: "row", - justifyContent: "flex-start", alignItems: "center", - gap: 4, // gap isn't officially supported in RN, so use marginRight on children instead + justifyContent: "space-between", }, paymentDate: { width: 231, @@ -110,17 +104,14 @@ const styles = StyleSheet.create({ fontSize: 14, lineHeight: 20, color: "rgba(37,42,52,1)", // #252A34 - textAlign: "left", + textAlign: "right", marginLeft: 4, // simulate gap }, paymentCardBottom: { - width: 304, height: 20, - marginLeft: 12, flexDirection: "row", - justifyContent: "flex-start", + justifyContent: "space-between", alignItems: "center", - gap: 4, // simulate gap with margin }, paymentDetails: { width: 237, @@ -146,7 +137,7 @@ const styles = StyleSheet.create({ fontFamily: "Inter-Medium", fontSize: 12, lineHeight: 16, - color: "#8E4400", // default, overridden by inline style from getStatusStyle + color: "#8E4400", textAlign: "center", }, }); diff --git a/components/common/Header.tsx b/components/common/Header.tsx index 0637b9f..e5141a0 100644 --- a/components/common/Header.tsx +++ b/components/common/Header.tsx @@ -30,7 +30,6 @@ const Header: React.FC = ({ return ( - {/* Left side */} {showBackButton && ( @@ -40,7 +39,6 @@ const Header: React.FC = ({ {title} - {/* Right side */} {rightComponent && ( {rightComponent} )} diff --git a/constants/config.ts b/constants/config.ts index 0ca52e3..561aa24 100644 --- a/constants/config.ts +++ b/constants/config.ts @@ -13,6 +13,8 @@ import type { BmsState } from "./types"; import { useTranslation } from "react-i18next"; export const BASE_URL = "https://dev-driver-saathi-api.vecmocon.com"; +export const PAYMENT_SOCKET_BASE_URL = + "wss://dev-driver-saathi-api.vecmocon.com"; // export const BASE_URL = "https://46fa2cacfc37.ngrok-free.app"; // const SERVER_URL = @@ -179,5 +181,6 @@ export const issueConfig = [ ]; export const payments = { - MIN_AMOUNT: 200, + MIN_AMOUNT: 1, + LINK_EXPIRED: "Payment link expired", }; diff --git a/contexts/SocketContext.tsx b/contexts/SocketContext.tsx new file mode 100644 index 0000000..fd3d834 --- /dev/null +++ b/contexts/SocketContext.tsx @@ -0,0 +1,106 @@ +// contexts/SocketContext.tsx +import React, { + createContext, + useContext, + useEffect, + useState, + ReactNode, +} from "react"; +import SocketService from "../services/paymentSocket"; + +interface SocketContextType { + isConnected: boolean; + registerTransaction: (transactionId: string) => Promise; + onPaymentConfirmation: (callback: (data: any) => void) => void; + offPaymentConfirmation: () => void; + disconnect: () => void; + connectSocket: () => void; +} + +const SocketContext = createContext(undefined); + +interface SocketProviderProps { + children: ReactNode; +} + +export const SocketProvider: React.FC = ({ children }) => { + const [isConnected, setIsConnected] = useState(false); + + // useEffect(() => { + // const initSocket = async () => { + // try { + // await SocketService.connect(); + // setIsConnected(true); + // } catch (error) { + // console.error("Failed to connect socket:", error); + // setIsConnected(false); + // } + // }; + + // initSocket(); + + // return () => { + // SocketService.disconnect(); + // setIsConnected(false); + // }; + // }, []); + + const connectSocket = async () => { + try { + await SocketService.connect(); + setIsConnected(true); + } catch (error) { + console.error("Failed to connect socket:", error); + setIsConnected(false); + throw error; + } + }; + + const registerTransaction = async (transactionId: string) => { + try { + if (!transactionId) { + throw new Error("Transaction Id missing in register transaction"); + } + if (!isConnected) { + await connectSocket(); + } + await SocketService.registerTransaction(transactionId); + } catch (err) { + throw err; + } + }; + + const onPaymentConfirmation = (callback: (data: any) => void) => { + SocketService.onPaymentConfirmation(callback); + }; + + const offPaymentConfirmation = () => { + SocketService.offPaymentConfirmation(); + }; + + return ( + { + SocketService.disconnect(); + setIsConnected(false); + }, + connectSocket, + }} + > + {children} + + ); +}; + +export const useSocket = (): SocketContextType => { + const context = useContext(SocketContext); + if (context === undefined) { + throw new Error("useSocket must be used within a SocketProvider"); + } + return context; +}; diff --git a/services/paymentSocket.ts b/services/paymentSocket.ts new file mode 100644 index 0000000..17f78b5 --- /dev/null +++ b/services/paymentSocket.ts @@ -0,0 +1,136 @@ +// services/socketService.ts +import { PAYMENT_SOCKET_BASE_URL, STORAGE_KEYS } from "@/constants/config"; +import AsyncStorage from "@react-native-async-storage/async-storage"; +import io, { Socket } from "socket.io-client"; + +class SocketService { + private socket: Socket | null = null; + private static instance: SocketService; + + private constructor() {} + + public static getInstance(): SocketService { + if (!SocketService.instance) { + SocketService.instance = new SocketService(); + } + return SocketService.instance; + } + + public async connect(): Promise { + try { + const token = await AsyncStorage.getItem(STORAGE_KEYS.AUTH_TOKEN); + + if (!token) { + throw new Error("No auth token found"); + } + + this.socket = io(PAYMENT_SOCKET_BASE_URL, { + transports: ["websocket"], + autoConnect: true, + reconnection: true, + reconnectionDelay: 1000, + reconnectionAttempts: 5, + timeout: 20000, + extraHeaders: { + Authorization: `Bearer ${token}`, + }, + }); + + return new Promise((resolve, reject) => { + this.socket!.on("connect", () => { + console.log("Socket connected:", this.socket?.id); + resolve(this.socket!); + }); + + this.socket!.on("connect_error", (error) => { + console.error("Socket connection error:", error); + reject(error); + }); + + this.socket!.on("disconnect", (reason) => { + console.log("Socket disconnected:", reason); + }); + }); + } catch (error) { + console.error("Failed to connect socket:", error); + return Promise.reject(error); + } + } + + public registerTransaction(transactionId: string): Promise { + return new Promise((resolve, reject) => { + const socket = this.socket; // Save reference to socket here + + if (!socket || !socket.connected) { + reject(new Error("Socket not connected")); + return; + } + + const timeoutMs = 5000; + + const timeoutId = setTimeout(() => { + socket.off("registration-ack", onRegistrationAck); + reject(new Error("Timeout: No registration-ack received")); + }, timeoutMs); + + const onRegistrationAck = (data: any) => { + console.log("inside onRegisterAck"); + if (data.transactionId === transactionId) { + clearTimeout(timeoutId); + // socket.off("registration-ack", onRegistrationAck); + + if (data.success) { + resolve(); + } else { + reject(new Error("Registration failed")); + } + } + }; + + socket.on("registration-ack", onRegistrationAck); + + socket.on("register-transaction", this.onPaymentConfirm); + socket.onAny((eventName, ...args) => { + console.log(eventName, "eventname", JSON.stringify(args), "✅✅✅✅✅"); + }); + + socket.emit("register-transaction", { transactionId }); + }); + } + + private onPaymentConfirm(eventname: string, ...args: any[]): void { + console.log(eventname, "eventName", JSON.stringify(args)); + } + + public onPaymentConfirmation(callback: (data: any) => void): void { + if (!this.socket) { + console.error("Socket not connected"); + return; + } + + this.socket.on("register-transaction", callback); + } + + public offPaymentConfirmation(): void { + if (this.socket) { + this.socket.off("payment-confirmation"); + } + } + + public disconnect(): void { + if (this.socket) { + this.socket.disconnect(); + this.socket = null; + } + } + + public isConnected(): boolean { + return this.socket?.connected || false; + } + + public getSocket(): Socket | null { + return this.socket; + } +} + +export default SocketService.getInstance(); diff --git a/services/socket.ts b/services/socket.ts index 7e63978..af5ca4e 100644 --- a/services/socket.ts +++ b/services/socket.ts @@ -103,8 +103,10 @@ export const connectSocket = () => { export const initSocket = async () => { try { + //get latest telemetry as fallback option token = await fetchToken(); controllingServer = await fetchControllingServer(token); + await fetchLatestTelemetry(token, controllingServer); connectSocket(); } catch (err) { console.log(err, ""); @@ -165,3 +167,71 @@ const handleSocketData = (data: any) => { store.dispatch(setTelemetryError("Data parsing failed")); } }; + +const fetchLatestTelemetry = async ( + token: string, + controllingServer: string +) => { + try { + const hardwareDeviceId = + store.getState().user.data?.batteries[0]?.device_id; + if (!hardwareDeviceId || !token || !controllingServer) + throw new Error("Missing hardwareDeviceId"); + + const response = await axios.post( + "https://vec-tr.ai/api/dashboard/getLastTelemetry", + { deviceId: hardwareDeviceId }, // body + { + headers: { + Authorization: `Bearer ${token}`, + controllingServer: controllingServer, + "Content-Type": "application/json", + }, + } + ); + + if (response.data?.success && response.data?.data?.length > 0) { + const telemetry = response.data.data[0]; + const SoH = + telemetry?.assetData?.[0]?.bms?.[0]?.bmsSpecific?.ivecSpecific?.soh ?? + null; + const SoC = telemetry?.assetData?.[0]?.bms?.[0]?.batterySoc ?? null; + const currentMode = + telemetry?.assetData?.[0]?.bms?.[0]?.bmsSpecific?.ivecSpecific + ?.ivecStatus?.currentMode ?? null; + + const gps = telemetry?.locationData?.gps ?? null; + const totalDistance = telemetry?.systemData?.odoMeter ?? null; + + const lat = gps?.[1]; + const lon = gps?.[2]; + + let bms_state: BmsState | null = null; + if (currentMode === 0) { + bms_state = 0; + } else if (currentMode === 1) { + bms_state = -1; + } else if (currentMode === 2) { + bms_state = 1; + } + + store.dispatch( + updateTelemetry({ + SoH, + SoC, + chargingState: bms_state, + lat, + lon, + loading: false, + error: null, + totalDistance, + }) + ); + } else { + store.dispatch(setTelemetryError("No telemetry data found")); + } + } catch (error) { + console.error("Error fetching latest telemetry:", error); + store.dispatch(setTelemetryError("Failed to fetch latest telemetry")); + } +}; diff --git a/store/telemetrySlice.ts b/store/telemetrySlice.ts index f8a347a..36cd0ef 100644 --- a/store/telemetrySlice.ts +++ b/store/telemetrySlice.ts @@ -32,14 +32,6 @@ export const telemetrySlice = createSlice({ }, setTelemetryLoading: (state) => { state.loading = true; - state.SoH = null; - state.SoC = null; - state.chargingState = null; - state.lat = null; - state.lon = null; - state.totalDistance = null; - state.loading = true; - state.error = null; }, setTelemetryError: (state, action: PayloadAction) => { state.loading = false;