connect using socket for payment confirmation
parent
d12efb4fd5
commit
5b89bebe0b
|
|
@ -128,7 +128,7 @@ export default function HomeScreen() {
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<ProfileImage
|
<ProfileImage
|
||||||
username={data?.name || "User"}
|
username={data?.name || "Vec"}
|
||||||
onClick={() => router.push("/user/profile")}
|
onClick={() => router.push("/user/profile")}
|
||||||
textSize={20}
|
textSize={20}
|
||||||
boxSize={40}
|
boxSize={40}
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ import PaymentHistoryCard from "@/components/Payments/PaymentHistoryCard";
|
||||||
import { BASE_URL } from "@/constants/config";
|
import { BASE_URL } from "@/constants/config";
|
||||||
import { useDispatch } from "react-redux";
|
import { useDispatch } from "react-redux";
|
||||||
import { setDueAmount, setMyPlan } from "@/store/paymentSlice";
|
import { setDueAmount, setMyPlan } from "@/store/paymentSlice";
|
||||||
|
import { ActivityIndicator } from "react-native-paper";
|
||||||
|
|
||||||
export interface MyPlan {
|
export interface MyPlan {
|
||||||
no_of_emi: number;
|
no_of_emi: number;
|
||||||
|
|
@ -71,6 +72,7 @@ interface PaymentHistoryResponse {
|
||||||
}
|
}
|
||||||
|
|
||||||
const formatPaymentDate = (dateString: string) => {
|
const formatPaymentDate = (dateString: string) => {
|
||||||
|
console.log(dateString.split(",")[0], "datestring");
|
||||||
try {
|
try {
|
||||||
return dateString.split(",")[0];
|
return dateString.split(",")[0];
|
||||||
} catch {
|
} catch {
|
||||||
|
|
@ -80,17 +82,21 @@ const formatPaymentDate = (dateString: string) => {
|
||||||
|
|
||||||
// Format time for payment history
|
// Format time for payment history
|
||||||
const formatPaymentTime = (dateString: string) => {
|
const formatPaymentTime = (dateString: string) => {
|
||||||
|
console.log(dateString, "formattime");
|
||||||
try {
|
try {
|
||||||
// Extract time and day info from the formatted date
|
// Expected: "12 Aug 2025, 12:38:23 pm"
|
||||||
const parts = dateString.split(",");
|
const [datePart, timePart] = dateString.split(",");
|
||||||
if (parts.length > 1) {
|
|
||||||
const timePart = parts[1].trim(); // "5:23:58 pm"
|
// Ensure JS can parse (convert "12 Aug 2025 12:38:23 pm" to valid format)
|
||||||
const date = new Date(dateString);
|
const parsedDate = new Date(`${datePart.trim()} ${timePart.trim()}`);
|
||||||
const dayName = date.toLocaleDateString("en-US", { weekday: "long" });
|
|
||||||
return `${timePart} | ${dayName}`;
|
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;
|
return dateString;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
@ -309,7 +315,7 @@ export default function PaymentsTabScreen() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ScrollView
|
<ScrollView
|
||||||
style={styles.scrollContainer}
|
contentContainerStyle={{ paddingHorizontal: 16, paddingBottom: 125 }}
|
||||||
showsVerticalScrollIndicator={false}
|
showsVerticalScrollIndicator={false}
|
||||||
onScroll={handleScroll}
|
onScroll={handleScroll}
|
||||||
scrollEventThrottle={16}
|
scrollEventThrottle={16}
|
||||||
|
|
@ -381,20 +387,20 @@ export default function PaymentsTabScreen() {
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<View style={styles.paymentHistorySection}>
|
<View>
|
||||||
<Text style={styles.sectionTitle}>Payment History</Text>
|
<Text style={styles.sectionTitle}>Payment History</Text>
|
||||||
|
|
||||||
<View style={styles.paymentHistoryContainer}>
|
<View style={styles.paymentHistoryContainer}>
|
||||||
{isHistoryLoading ? (
|
{isHistoryLoading ? (
|
||||||
<View style={styles.historyLoadingContainer}>
|
<View style={styles.historyLoadingContainer}>
|
||||||
<Text style={styles.loadingText}>Loading payment history...</Text>
|
<ActivityIndicator color="#00BE88" />
|
||||||
</View>
|
</View>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
{/* Show initial payments or all payments based on showFullHistory */}
|
{/* Show initial payments or all payments based on showFullHistory */}
|
||||||
{(showFullHistory
|
{(showFullHistory
|
||||||
? paymentHistory
|
? paymentHistory
|
||||||
: paymentHistory.slice(0, 2)
|
: paymentHistory.slice(0, 3)
|
||||||
).map((payment) => (
|
).map((payment) => (
|
||||||
<PaymentHistoryCard
|
<PaymentHistoryCard
|
||||||
key={payment.id}
|
key={payment.id}
|
||||||
|
|
@ -406,7 +412,6 @@ export default function PaymentsTabScreen() {
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
{/* View All button */}
|
|
||||||
{!showFullHistory && paymentHistory.length > 2 && (
|
{!showFullHistory && paymentHistory.length > 2 && (
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={styles.viewAllButton}
|
style={styles.viewAllButton}
|
||||||
|
|
@ -417,16 +422,14 @@ export default function PaymentsTabScreen() {
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Loading indicator when fetching more */}
|
|
||||||
{isLoadingMore && (
|
{isLoadingMore && (
|
||||||
<View style={styles.loadingContainer}>
|
<View style={styles.loadingContainer}>
|
||||||
<Text style={styles.loadingText}>
|
<Text style={styles.loadingText}>
|
||||||
Loading more payments...
|
<ActivityIndicator color="#00BE88" />
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* No more data message */}
|
|
||||||
{showFullHistory &&
|
{showFullHistory &&
|
||||||
!hasMorePages &&
|
!hasMorePages &&
|
||||||
paymentHistory.length > 0 && (
|
paymentHistory.length > 0 && (
|
||||||
|
|
@ -649,23 +652,16 @@ const styles = StyleSheet.create({
|
||||||
borderRadius: 8,
|
borderRadius: 8,
|
||||||
padding: 12,
|
padding: 12,
|
||||||
},
|
},
|
||||||
paymentHistorySection: {
|
|
||||||
width: 328,
|
|
||||||
marginLeft: 16,
|
|
||||||
marginTop: 20,
|
|
||||||
// No height fixed here, flexible container
|
|
||||||
},
|
|
||||||
sectionTitle: {
|
sectionTitle: {
|
||||||
fontFamily: "Inter-SemiBold",
|
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
lineHeight: 20,
|
lineHeight: 20,
|
||||||
color: "#252A33", // rgb(37,42,51) from design
|
color: "#252A34", // rgb(37,42,51) from design
|
||||||
marginBottom: 8,
|
marginBottom: 8,
|
||||||
|
fontWeight: "600",
|
||||||
},
|
},
|
||||||
paymentHistoryContainer: {
|
paymentHistoryContainer: {
|
||||||
width: 328,
|
|
||||||
flexDirection: "column",
|
flexDirection: "column",
|
||||||
gap: 16, // for spacing between cards, might need polyfill or use margin
|
gap: 16,
|
||||||
},
|
},
|
||||||
historyLoadingContainer: {
|
historyLoadingContainer: {
|
||||||
height: 312,
|
height: 312,
|
||||||
|
|
@ -763,6 +759,7 @@ const styles = StyleSheet.create({
|
||||||
},
|
},
|
||||||
viewAllButton: {
|
viewAllButton: {
|
||||||
flexDirection: "row",
|
flexDirection: "row",
|
||||||
|
justifyContent: "center",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
paddingVertical: 8,
|
paddingVertical: 8,
|
||||||
paddingHorizontal: 16,
|
paddingHorizontal: 16,
|
||||||
|
|
|
||||||
|
|
@ -11,19 +11,22 @@ import * as SplashScreen from "expo-splash-screen";
|
||||||
import AsyncStorage from "@react-native-async-storage/async-storage";
|
import AsyncStorage from "@react-native-async-storage/async-storage";
|
||||||
import { STORAGE_KEYS } from "@/constants/config";
|
import { STORAGE_KEYS } from "@/constants/config";
|
||||||
import { setIsLoggedIn } from "@/store/authSlice";
|
import { setIsLoggedIn } from "@/store/authSlice";
|
||||||
|
import { SocketProvider } from "@/contexts/SocketContext";
|
||||||
|
|
||||||
SplashScreen.preventAutoHideAsync();
|
SplashScreen.preventAutoHideAsync();
|
||||||
|
|
||||||
export default function RootLayout() {
|
export default function RootLayout() {
|
||||||
return (
|
return (
|
||||||
<PaperProvider>
|
<PaperProvider>
|
||||||
<SnackbarProvider>
|
<SocketProvider>
|
||||||
<Provider store={store}>
|
<SnackbarProvider>
|
||||||
<I18nextProvider i18n={i18next}>
|
<Provider store={store}>
|
||||||
<SplashAndAuthRouter />
|
<I18nextProvider i18n={i18next}>
|
||||||
</I18nextProvider>
|
<SplashAndAuthRouter />
|
||||||
</Provider>
|
</I18nextProvider>
|
||||||
</SnackbarProvider>
|
</Provider>
|
||||||
|
</SnackbarProvider>
|
||||||
|
</SocketProvider>
|
||||||
</PaperProvider>
|
</PaperProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -38,6 +41,7 @@ function SplashAndAuthRouter() {
|
||||||
try {
|
try {
|
||||||
const token = await AsyncStorage.getItem(STORAGE_KEYS.AUTH_TOKEN);
|
const token = await AsyncStorage.getItem(STORAGE_KEYS.AUTH_TOKEN);
|
||||||
|
|
||||||
|
console.log(token, "token of backend");
|
||||||
if (!token) {
|
if (!token) {
|
||||||
dispatch(setIsLoggedIn(false));
|
dispatch(setIsLoggedIn(false));
|
||||||
return;
|
return;
|
||||||
|
|
|
||||||
|
|
@ -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 (
|
||||||
|
<View style={styles.container}>
|
||||||
|
<Text style={styles.title}>Payment Successful!</Text>
|
||||||
|
<Text style={styles.message}>
|
||||||
|
Thank you for your payment. Your transaction has been completed.
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<TouchableOpacity
|
||||||
|
style={styles.button}
|
||||||
|
// onPress={() => router.push("/dashboard")} // Or wherever you want to go
|
||||||
|
>
|
||||||
|
<Text style={styles.buttonText}>Go to Dashboard</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
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",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import React, { useEffect } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import {
|
import {
|
||||||
View,
|
View,
|
||||||
Text,
|
Text,
|
||||||
|
|
@ -8,9 +8,9 @@ import {
|
||||||
Alert,
|
Alert,
|
||||||
Share,
|
Share,
|
||||||
Clipboard,
|
Clipboard,
|
||||||
Platform,
|
|
||||||
Linking,
|
Linking,
|
||||||
ActivityIndicator,
|
ActivityIndicator,
|
||||||
|
BackHandler,
|
||||||
} from "react-native";
|
} from "react-native";
|
||||||
import { Ionicons } from "@expo/vector-icons";
|
import { Ionicons } from "@expo/vector-icons";
|
||||||
import * as FileSystem from "expo-file-system";
|
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 * as Sharing from "expo-sharing";
|
||||||
import { Image } from "expo-image";
|
import { Image } from "expo-image";
|
||||||
import { useSelector, useDispatch } from "react-redux";
|
import { useSelector, useDispatch } from "react-redux";
|
||||||
import { RootState } from "@/store"; // Adjust path as needed
|
import { RootState } from "@/store";
|
||||||
import { updatePaymentStatus } from "@/store/paymentSlice";
|
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 {
|
import DownloadIcon from "@/assets/icons/download.svg";
|
||||||
onBack?: () => void;
|
import { payments } from "@/constants/config";
|
||||||
onPaymentSuccess?: () => void;
|
import { useRouter } from "expo-router";
|
||||||
onPaymentFailure?: () => void;
|
import { useSocket } from "@/contexts/SocketContext";
|
||||||
}
|
|
||||||
|
|
||||||
const UpiPaymentScreen: React.FC<UpiPaymentScreenProps> = ({
|
const UpiPaymentScreen = () => {
|
||||||
onBack,
|
|
||||||
onPaymentSuccess,
|
|
||||||
onPaymentFailure,
|
|
||||||
}) => {
|
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const paymentOrder = useSelector(
|
const paymentOrder = useSelector(
|
||||||
(state: RootState) => state.payments.paymentOrder
|
(state: RootState) => state.payments.paymentOrder
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
const router = useRouter();
|
||||||
// Check if payment order exists
|
const { onPaymentConfirmation, offPaymentConfirmation } = useSocket();
|
||||||
if (!paymentOrder) {
|
const [isListening, setIsListening] = useState(false);
|
||||||
Alert.alert(
|
|
||||||
"Error",
|
|
||||||
"No payment order found. Please create a payment order first.",
|
|
||||||
[{ text: "OK", onPress: onBack }]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}, [paymentOrder]);
|
|
||||||
|
|
||||||
// Show loading if no payment data
|
const { showSnackbar } = useSnackbar();
|
||||||
|
|
||||||
|
const insets = useSafeAreaInsets();
|
||||||
if (!paymentOrder) {
|
if (!paymentOrder) {
|
||||||
return (
|
return (
|
||||||
<SafeAreaView style={styles.container}>
|
<SafeAreaView style={styles.container}>
|
||||||
|
|
@ -60,6 +54,67 @@ const UpiPaymentScreen: React.FC<UpiPaymentScreenProps> = ({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
// Format amount with currency symbol
|
||||||
const formatAmount = (amount: number): string => {
|
const formatAmount = (amount: number): string => {
|
||||||
return `₹${amount.toLocaleString("en-IN")}`;
|
return `₹${amount.toLocaleString("en-IN")}`;
|
||||||
|
|
@ -83,82 +138,33 @@ const UpiPaymentScreen: React.FC<UpiPaymentScreenProps> = ({
|
||||||
return expiryDate.toLocaleString("en-IN");
|
return expiryDate.toLocaleString("en-IN");
|
||||||
};
|
};
|
||||||
|
|
||||||
// Generate UPI payment URL from the API response
|
|
||||||
const getUpiUrl = (): string => {
|
const getUpiUrl = (): string => {
|
||||||
// Use the UPI string from the API response
|
|
||||||
const upiString = paymentOrder.ppayment_link;
|
const upiString = paymentOrder.ppayment_link;
|
||||||
|
|
||||||
// Extract the UPI URL from the deep link
|
|
||||||
const upiMatch = upiString.match(/upi_string=([^&]+)/);
|
const upiMatch = upiString.match(/upi_string=([^&]+)/);
|
||||||
if (upiMatch) {
|
if (upiMatch) {
|
||||||
return decodeURIComponent(upiMatch[1]);
|
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}`;
|
return `upi://pay?pa=${paymentOrder.upi_handle}&am=${paymentOrder.amount}&cu=INR&tr=${paymentOrder.order_id}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
const payUsingUpiApp = async (): Promise<void> => {
|
const payUsingUpiApp = async (): Promise<void> => {
|
||||||
try {
|
try {
|
||||||
// Check if payment is expired
|
|
||||||
if (isPaymentExpired()) {
|
if (isPaymentExpired()) {
|
||||||
Alert.alert(
|
showSnackbar(payments.LINK_EXPIRED, "error");
|
||||||
"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 }]
|
|
||||||
);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const upiUrl = getUpiUrl();
|
const upiUrl = getUpiUrl();
|
||||||
console.log("Opening UPI URL:", upiUrl);
|
console.log("Opening UPI URL:", upiUrl);
|
||||||
|
|
||||||
// Update payment status to processing
|
|
||||||
dispatch(updatePaymentStatus("processing"));
|
dispatch(updatePaymentStatus("processing"));
|
||||||
|
|
||||||
// Check if device can handle UPI URLs
|
|
||||||
const canOpenUrl = await Linking.canOpenURL(upiUrl);
|
const canOpenUrl = await Linking.canOpenURL(upiUrl);
|
||||||
|
|
||||||
if (canOpenUrl) {
|
if (canOpenUrl) {
|
||||||
await Linking.openURL(upiUrl);
|
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 {
|
} else {
|
||||||
// Fallback: Show available UPI apps or general share
|
|
||||||
Alert.alert(
|
Alert.alert(
|
||||||
"UPI App Required",
|
"UPI App Required",
|
||||||
"Please install a UPI-enabled app like PhonePe, Paytm, Google Pay, or BHIM to make payments.",
|
"Please install a UPI-enabled app like PhonePe, Paytm, Google Pay, or BHIM to make payments.",
|
||||||
|
|
@ -191,7 +197,7 @@ const UpiPaymentScreen: React.FC<UpiPaymentScreenProps> = ({
|
||||||
const sharePaymentDetails = async (): Promise<void> => {
|
const sharePaymentDetails = async (): Promise<void> => {
|
||||||
try {
|
try {
|
||||||
const shareMessage =
|
const shareMessage =
|
||||||
`💰 Payment Request\n\n` +
|
`Payment Request\n\n` +
|
||||||
`Amount: ${formatAmount(paymentOrder.amount)}\n` +
|
`Amount: ${formatAmount(paymentOrder.amount)}\n` +
|
||||||
`UPI ID: ${paymentOrder.upi_handle}\n` +
|
`UPI ID: ${paymentOrder.upi_handle}\n` +
|
||||||
`Order ID: ${paymentOrder.order_id}\n` +
|
`Order ID: ${paymentOrder.order_id}\n` +
|
||||||
|
|
@ -208,15 +214,6 @@ const UpiPaymentScreen: React.FC<UpiPaymentScreenProps> = ({
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const copyUpiId = async (): Promise<void> => {
|
|
||||||
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<void> => {
|
const shareQR = async (): Promise<void> => {
|
||||||
try {
|
try {
|
||||||
if (await Sharing.isAvailableAsync()) {
|
if (await Sharing.isAvailableAsync()) {
|
||||||
|
|
@ -280,12 +277,7 @@ const UpiPaymentScreen: React.FC<UpiPaymentScreenProps> = ({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SafeAreaView style={styles.container}>
|
<SafeAreaView style={styles.container}>
|
||||||
<View style={styles.header}>
|
<Header title="Pay EMI" showBackButton={true} />
|
||||||
<TouchableOpacity onPress={onBack} style={styles.backButton}>
|
|
||||||
<Ionicons name="chevron-back" size={24} color="#253342" />
|
|
||||||
</TouchableOpacity>
|
|
||||||
<Text style={styles.headerTitle}>Pay EMI</Text>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<View style={styles.content}>
|
<View style={styles.content}>
|
||||||
<View style={styles.qrFrame}>
|
<View style={styles.qrFrame}>
|
||||||
|
|
@ -294,18 +286,6 @@ const UpiPaymentScreen: React.FC<UpiPaymentScreenProps> = ({
|
||||||
<Text style={styles.amount}>
|
<Text style={styles.amount}>
|
||||||
{formatAmount(paymentOrder.amount)}
|
{formatAmount(paymentOrder.amount)}
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
{/* Payment Status */}
|
|
||||||
<View
|
|
||||||
style={[
|
|
||||||
styles.statusBadge,
|
|
||||||
{ backgroundColor: getStatusColor(paymentOrder.status) },
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<Text style={styles.statusText}>
|
|
||||||
{paymentOrder.status.toUpperCase()}
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<View style={styles.qrCodeContainer}>
|
<View style={styles.qrCodeContainer}>
|
||||||
|
|
@ -316,21 +296,9 @@ const UpiPaymentScreen: React.FC<UpiPaymentScreenProps> = ({
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* UPI ID Section */}
|
|
||||||
<TouchableOpacity onPress={copyUpiId} style={styles.upiIdSection}>
|
|
||||||
<Text style={styles.upiIdText}>{paymentOrder.upi_handle}</Text>
|
|
||||||
<Ionicons name="copy-outline" size={16} color="#253342" />
|
|
||||||
</TouchableOpacity>
|
|
||||||
|
|
||||||
{/* Expiry Info */}
|
|
||||||
<View style={styles.expirySection}>
|
|
||||||
<Text style={styles.expiryLabel}>Expires:</Text>
|
|
||||||
<Text style={styles.expiryTime}>{getExpiryTime()}</Text>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<View style={styles.buttonsContainer}>
|
<View style={styles.buttonsContainer}>
|
||||||
<TouchableOpacity onPress={shareQR} style={styles.secondaryButton}>
|
<TouchableOpacity onPress={shareQR} style={styles.secondaryButton}>
|
||||||
<Ionicons name="share-outline" size={20} color="#253342" />
|
<ShareIcon />
|
||||||
<Text style={styles.secondaryButtonText}>Share QR</Text>
|
<Text style={styles.secondaryButtonText}>Share QR</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
|
|
||||||
|
|
@ -338,7 +306,7 @@ const UpiPaymentScreen: React.FC<UpiPaymentScreenProps> = ({
|
||||||
onPress={downloadQR}
|
onPress={downloadQR}
|
||||||
style={styles.secondaryButton}
|
style={styles.secondaryButton}
|
||||||
>
|
>
|
||||||
<Ionicons name="download-outline" size={20} color="#253342" />
|
<DownloadIcon />
|
||||||
<Text style={styles.secondaryButtonText}>Download QR</Text>
|
<Text style={styles.secondaryButtonText}>Download QR</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
|
|
@ -346,26 +314,9 @@ const UpiPaymentScreen: React.FC<UpiPaymentScreenProps> = ({
|
||||||
|
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
onPress={payUsingUpiApp}
|
onPress={payUsingUpiApp}
|
||||||
style={[
|
style={[styles.primaryButton, { marginBottom: insets.bottom || 20 }]}
|
||||||
styles.primaryButton,
|
|
||||||
(isPaymentExpired() || paymentOrder.status === "completed") &&
|
|
||||||
styles.disabledButton,
|
|
||||||
]}
|
|
||||||
disabled={isPaymentExpired() || paymentOrder.status === "completed"}
|
|
||||||
>
|
>
|
||||||
<Text
|
<Text style={[styles.primaryButtonText]}>Pay using UPI app</Text>
|
||||||
style={[
|
|
||||||
styles.primaryButtonText,
|
|
||||||
(isPaymentExpired() || paymentOrder.status === "completed") &&
|
|
||||||
styles.disabledButtonText,
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
{paymentOrder.status === "completed"
|
|
||||||
? "Payment Completed"
|
|
||||||
: isPaymentExpired()
|
|
||||||
? "Payment Expired"
|
|
||||||
: "Pay using UPI app"}
|
|
||||||
</Text>
|
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
</SafeAreaView>
|
</SafeAreaView>
|
||||||
|
|
@ -444,11 +395,15 @@ const styles = StyleSheet.create({
|
||||||
color: "#FFFFFF",
|
color: "#FFFFFF",
|
||||||
},
|
},
|
||||||
qrCodeContainer: {
|
qrCodeContainer: {
|
||||||
width: 248,
|
width: 200,
|
||||||
height: 248,
|
height: 200,
|
||||||
justifyContent: "center",
|
justifyContent: "center",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
marginBottom: 16,
|
marginBottom: 16,
|
||||||
|
borderWidth: 2,
|
||||||
|
borderColor: "#E5E9F0",
|
||||||
|
padding: 12,
|
||||||
|
borderRadius: 8,
|
||||||
},
|
},
|
||||||
qrCode: {
|
qrCode: {
|
||||||
width: "100%",
|
width: "100%",
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ import {
|
||||||
ScrollView,
|
ScrollView,
|
||||||
Platform,
|
Platform,
|
||||||
KeyboardAvoidingView,
|
KeyboardAvoidingView,
|
||||||
|
Alert,
|
||||||
} from "react-native";
|
} from "react-native";
|
||||||
import { useDispatch, useSelector } from "react-redux";
|
import { useDispatch, useSelector } from "react-redux";
|
||||||
import { Formik } from "formik";
|
import { Formik } from "formik";
|
||||||
|
|
@ -19,6 +20,7 @@ import api from "@/services/axiosClient";
|
||||||
import { setPaymentOrder } from "@/store/paymentSlice";
|
import { setPaymentOrder } from "@/store/paymentSlice";
|
||||||
import { Overlay } from "@/components/common/Overlay";
|
import { Overlay } from "@/components/common/Overlay";
|
||||||
import { useRouter } from "expo-router";
|
import { useRouter } from "expo-router";
|
||||||
|
import { useSocket } from "@/contexts/SocketContext";
|
||||||
|
|
||||||
// Validation schema
|
// Validation schema
|
||||||
const validationSchema = Yup.object().shape({
|
const validationSchema = Yup.object().shape({
|
||||||
|
|
@ -28,18 +30,18 @@ const validationSchema = Yup.object().shape({
|
||||||
then: (schema) =>
|
then: (schema) =>
|
||||||
schema
|
schema
|
||||||
.required("Amount is required")
|
.required("Amount is required")
|
||||||
|
.test("valid-number", "Please enter a valid amount", (value) => {
|
||||||
|
const numValue = parseFloat(value);
|
||||||
|
return !isNaN(numValue) && numValue > 0;
|
||||||
|
})
|
||||||
.test(
|
.test(
|
||||||
"min-amount",
|
"min-amount",
|
||||||
`Minimum amount is ₹${payments.MIN_AMOUNT}`,
|
`Minimum amount is ₹${payments.MIN_AMOUNT}`,
|
||||||
(value) => {
|
(value) => {
|
||||||
const numValue = parseFloat(value || "0");
|
const numValue = parseFloat(value);
|
||||||
return numValue >= payments.MIN_AMOUNT;
|
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(),
|
otherwise: (schema) => schema.notRequired(),
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
@ -53,6 +55,8 @@ const SelectAmountScreen = () => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [isFetching, setIsFetching] = useState<boolean>(false);
|
const [isFetching, setIsFetching] = useState<boolean>(false);
|
||||||
|
|
||||||
|
const { registerTransaction } = useSocket();
|
||||||
|
|
||||||
const quickAmounts = [50, 100, 500, 1000];
|
const quickAmounts = [50, 100, 500, 1000];
|
||||||
|
|
||||||
const initialValues = {
|
const initialValues = {
|
||||||
|
|
@ -74,10 +78,18 @@ const SelectAmountScreen = () => {
|
||||||
amount: paymentAmount,
|
amount: paymentAmount,
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log(res, "response");
|
console.log(res.data, "response from select amount");
|
||||||
if (res.data && res.data.success) {
|
if (res.data && res.data.success) {
|
||||||
dispatch(setPaymentOrder(res.data.data));
|
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 {
|
} else {
|
||||||
throw new Error("Failed to create order");
|
throw new Error("Failed to create order");
|
||||||
}
|
}
|
||||||
|
|
@ -224,7 +236,7 @@ const SelectAmountScreen = () => {
|
||||||
>
|
>
|
||||||
{touched.customAmount && errors.customAmount
|
{touched.customAmount && errors.customAmount
|
||||||
? errors.customAmount
|
? errors.customAmount
|
||||||
: "Minimum: ₹200"}
|
: `Minimum: ₹${payments.MIN_AMOUNT}`}
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|
@ -261,7 +273,7 @@ const SelectAmountScreen = () => {
|
||||||
onPress={() => handleSubmit()}
|
onPress={() => handleSubmit()}
|
||||||
disabled={!isPayButtonEnabled()}
|
disabled={!isPayButtonEnabled()}
|
||||||
>
|
>
|
||||||
{getPaymentAmount() < 200 ? (
|
{getPaymentAmount() < payments.MIN_AMOUNT ? (
|
||||||
<Text style={styles.payButtonText}>Select Amount</Text>
|
<Text style={styles.payButtonText}>Select Amount</Text>
|
||||||
) : (
|
) : (
|
||||||
<Text style={styles.payButtonText}>
|
<Text style={styles.payButtonText}>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<mask id="mask0_746_3070" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="20" height="20">
|
||||||
|
<rect width="20" height="20" fill="#D9D9D9"/>
|
||||||
|
</mask>
|
||||||
|
<g mask="url(#mask0_746_3070)">
|
||||||
|
<path d="M10.0007 12.9792C9.88954 12.9792 9.78537 12.9618 9.68815 12.9271C9.59093 12.8924 9.50065 12.8333 9.41732 12.75L6.41732 9.75C6.25065 9.58333 6.17079 9.38888 6.17773 9.16666C6.18468 8.94444 6.26454 8.74999 6.41732 8.58333C6.58398 8.41666 6.7819 8.32986 7.01107 8.32291C7.24023 8.31597 7.43815 8.39583 7.60482 8.56249L9.16732 10.125V4.16666C9.16732 3.93055 9.24718 3.73263 9.4069 3.57291C9.56662 3.41319 9.76454 3.33333 10.0007 3.33333C10.2368 3.33333 10.4347 3.41319 10.5944 3.57291C10.7541 3.73263 10.834 3.93055 10.834 4.16666V10.125L12.3965 8.56249C12.5632 8.39583 12.7611 8.31597 12.9902 8.32291C13.2194 8.32986 13.4173 8.41666 13.584 8.58333C13.7368 8.74999 13.8166 8.94444 13.8236 9.16666C13.8305 9.38888 13.7507 9.58333 13.584 9.75L10.584 12.75C10.5007 12.8333 10.4104 12.8924 10.3132 12.9271C10.2159 12.9618 10.1118 12.9792 10.0007 12.9792ZM5.00065 16.6667C4.54232 16.6667 4.14996 16.5035 3.82357 16.1771C3.49718 15.8507 3.33398 15.4583 3.33398 15V13.3333C3.33398 13.0972 3.41385 12.8993 3.57357 12.7396C3.73329 12.5799 3.93121 12.5 4.16732 12.5C4.40343 12.5 4.60135 12.5799 4.76107 12.7396C4.92079 12.8993 5.00065 13.0972 5.00065 13.3333V15H15.0007V13.3333C15.0007 13.0972 15.0805 12.8993 15.2402 12.7396C15.4 12.5799 15.5979 12.5 15.834 12.5C16.0701 12.5 16.268 12.5799 16.4277 12.7396C16.5875 12.8993 16.6673 13.0972 16.6673 13.3333V15C16.6673 15.4583 16.5041 15.8507 16.1777 16.1771C15.8513 16.5035 15.459 16.6667 15.0007 16.6667H5.00065Z" fill="#717B8C"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.7 KiB |
|
|
@ -0,0 +1,8 @@
|
||||||
|
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<mask id="mask0_746_4324" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="20" height="20">
|
||||||
|
<rect width="20" height="20" fill="#D9D9D9"/>
|
||||||
|
</mask>
|
||||||
|
<g mask="url(#mask0_746_4324)">
|
||||||
|
<path d="M14.166 18.3333C13.4716 18.3333 12.8813 18.0903 12.3952 17.6042C11.9091 17.1181 11.666 16.5278 11.666 15.8333C11.666 15.75 11.6868 15.5556 11.7285 15.25L5.87435 11.8333C5.65213 12.0417 5.39518 12.2049 5.10352 12.3229C4.81185 12.441 4.49935 12.5 4.16602 12.5C3.47157 12.5 2.88129 12.257 2.39518 11.7708C1.90907 11.2847 1.66602 10.6945 1.66602 10C1.66602 9.30556 1.90907 8.71528 2.39518 8.22917C2.88129 7.74306 3.47157 7.50001 4.16602 7.50001C4.49935 7.50001 4.81185 7.55903 5.10352 7.67709C5.39518 7.79514 5.65213 7.95834 5.87435 8.16667L11.7285 4.75001C11.7007 4.65278 11.6834 4.55903 11.6764 4.46876C11.6695 4.37848 11.666 4.27778 11.666 4.16667C11.666 3.47223 11.9091 2.88195 12.3952 2.39584C12.8813 1.90973 13.4716 1.66667 14.166 1.66667C14.8605 1.66667 15.4507 1.90973 15.9368 2.39584C16.423 2.88195 16.666 3.47223 16.666 4.16667C16.666 4.86112 16.423 5.45139 15.9368 5.93751C15.4507 6.42362 14.8605 6.66667 14.166 6.66667C13.8327 6.66667 13.5202 6.60764 13.2285 6.48959C12.9368 6.37153 12.6799 6.20834 12.4577 6.00001L6.60352 9.41667C6.63129 9.51389 6.64865 9.60764 6.6556 9.69792C6.66254 9.7882 6.66602 9.88889 6.66602 10C6.66602 10.1111 6.66254 10.2118 6.6556 10.3021C6.64865 10.3924 6.63129 10.4861 6.60352 10.5833L12.4577 14C12.6799 13.7917 12.9368 13.6285 13.2285 13.5104C13.5202 13.3924 13.8327 13.3333 14.166 13.3333C14.8605 13.3333 15.4507 13.5764 15.9368 14.0625C16.423 14.5486 16.666 15.1389 16.666 15.8333C16.666 16.5278 16.423 17.1181 15.9368 17.6042C15.4507 18.0903 14.8605 18.3333 14.166 18.3333ZM14.166 16.6667C14.4021 16.6667 14.6 16.5868 14.7598 16.4271C14.9195 16.2674 14.9993 16.0695 14.9993 15.8333C14.9993 15.5972 14.9195 15.3993 14.7598 15.2396C14.6 15.0799 14.4021 15 14.166 15C13.9299 15 13.732 15.0799 13.5723 15.2396C13.4125 15.3993 13.3327 15.5972 13.3327 15.8333C13.3327 16.0695 13.4125 16.2674 13.5723 16.4271C13.732 16.5868 13.9299 16.6667 14.166 16.6667ZM4.16602 10.8333C4.40213 10.8333 4.60004 10.7535 4.75977 10.5938C4.91949 10.434 4.99935 10.2361 4.99935 10C4.99935 9.76389 4.91949 9.56598 4.75977 9.40626C4.60004 9.24653 4.40213 9.16667 4.16602 9.16667C3.9299 9.16667 3.73199 9.24653 3.57227 9.40626C3.41254 9.56598 3.33268 9.76389 3.33268 10C3.33268 10.2361 3.41254 10.434 3.57227 10.5938C3.73199 10.7535 3.9299 10.8333 4.16602 10.8333ZM14.166 5.00001C14.4021 5.00001 14.6 4.92014 14.7598 4.76042C14.9195 4.6007 14.9993 4.40278 14.9993 4.16667C14.9993 3.93056 14.9195 3.73264 14.7598 3.57292C14.6 3.4132 14.4021 3.33334 14.166 3.33334C13.9299 3.33334 13.732 3.4132 13.5723 3.57292C13.4125 3.73264 13.3327 3.93056 13.3327 4.16667C13.3327 4.40278 13.4125 4.6007 13.5723 4.76042C13.732 4.92014 13.9299 5.00001 14.166 5.00001Z" fill="#565F70"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 2.9 KiB |
|
|
@ -17,7 +17,6 @@ export default ({
|
||||||
}: PaymentHistoryCardProps) => {
|
}: PaymentHistoryCardProps) => {
|
||||||
const getStatusStyle = (status: string) => {
|
const getStatusStyle = (status: string) => {
|
||||||
switch (status.toLowerCase()) {
|
switch (status.toLowerCase()) {
|
||||||
case "failure":
|
|
||||||
case "failed":
|
case "failed":
|
||||||
return {
|
return {
|
||||||
backgroundColor: "#FDE8E7",
|
backgroundColor: "#FDE8E7",
|
||||||
|
|
@ -28,9 +27,7 @@ export default ({
|
||||||
backgroundColor: "#FFF0E3",
|
backgroundColor: "#FFF0E3",
|
||||||
color: "#8E4400",
|
color: "#8E4400",
|
||||||
};
|
};
|
||||||
case "confirmed":
|
|
||||||
case "completed":
|
case "completed":
|
||||||
case "success":
|
|
||||||
return {
|
return {
|
||||||
backgroundColor: "#E8F5E8",
|
backgroundColor: "#E8F5E8",
|
||||||
color: "#2D7D32",
|
color: "#2D7D32",
|
||||||
|
|
@ -76,23 +73,20 @@ import { StyleSheet } from "react-native";
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
paymentCard: {
|
paymentCard: {
|
||||||
width: 328,
|
|
||||||
height: 76,
|
height: 76,
|
||||||
backgroundColor: "rgba(252,252,252,1)", // #FCFCFC
|
width: "100%",
|
||||||
|
backgroundColor: "rgba(252,252,252,1)",
|
||||||
borderRadius: 8,
|
borderRadius: 8,
|
||||||
paddingVertical: 12,
|
paddingVertical: 12,
|
||||||
paddingHorizontal: 12,
|
paddingHorizontal: 12,
|
||||||
justifyContent: "flex-start",
|
flexDirection: "column",
|
||||||
alignItems: "flex-start",
|
gap: "12",
|
||||||
},
|
},
|
||||||
paymentCardTop: {
|
paymentCardTop: {
|
||||||
width: 304,
|
|
||||||
height: 20,
|
height: 20,
|
||||||
marginLeft: 12,
|
|
||||||
flexDirection: "row",
|
flexDirection: "row",
|
||||||
justifyContent: "flex-start",
|
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
gap: 4, // gap isn't officially supported in RN, so use marginRight on children instead
|
justifyContent: "space-between",
|
||||||
},
|
},
|
||||||
paymentDate: {
|
paymentDate: {
|
||||||
width: 231,
|
width: 231,
|
||||||
|
|
@ -110,17 +104,14 @@ const styles = StyleSheet.create({
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
lineHeight: 20,
|
lineHeight: 20,
|
||||||
color: "rgba(37,42,52,1)", // #252A34
|
color: "rgba(37,42,52,1)", // #252A34
|
||||||
textAlign: "left",
|
textAlign: "right",
|
||||||
marginLeft: 4, // simulate gap
|
marginLeft: 4, // simulate gap
|
||||||
},
|
},
|
||||||
paymentCardBottom: {
|
paymentCardBottom: {
|
||||||
width: 304,
|
|
||||||
height: 20,
|
height: 20,
|
||||||
marginLeft: 12,
|
|
||||||
flexDirection: "row",
|
flexDirection: "row",
|
||||||
justifyContent: "flex-start",
|
justifyContent: "space-between",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
gap: 4, // simulate gap with margin
|
|
||||||
},
|
},
|
||||||
paymentDetails: {
|
paymentDetails: {
|
||||||
width: 237,
|
width: 237,
|
||||||
|
|
@ -146,7 +137,7 @@ const styles = StyleSheet.create({
|
||||||
fontFamily: "Inter-Medium",
|
fontFamily: "Inter-Medium",
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
lineHeight: 16,
|
lineHeight: 16,
|
||||||
color: "#8E4400", // default, overridden by inline style from getStatusStyle
|
color: "#8E4400",
|
||||||
textAlign: "center",
|
textAlign: "center",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,6 @@ const Header: React.FC<HeaderProps> = ({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={[styles.container, { paddingTop: insets.top }]}>
|
<View style={[styles.container, { paddingTop: insets.top }]}>
|
||||||
{/* Left side */}
|
|
||||||
<View style={styles.leftContainer}>
|
<View style={styles.leftContainer}>
|
||||||
{showBackButton && (
|
{showBackButton && (
|
||||||
<TouchableOpacity style={styles.backButton} onPress={handleBack}>
|
<TouchableOpacity style={styles.backButton} onPress={handleBack}>
|
||||||
|
|
@ -40,7 +39,6 @@ const Header: React.FC<HeaderProps> = ({
|
||||||
<Text style={styles.title}>{title}</Text>
|
<Text style={styles.title}>{title}</Text>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* Right side */}
|
|
||||||
{rightComponent && (
|
{rightComponent && (
|
||||||
<View style={styles.rightContainer}>{rightComponent}</View>
|
<View style={styles.rightContainer}>{rightComponent}</View>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,8 @@ import type { BmsState } from "./types";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
export const BASE_URL = "https://dev-driver-saathi-api.vecmocon.com";
|
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";
|
// export const BASE_URL = "https://46fa2cacfc37.ngrok-free.app";
|
||||||
|
|
||||||
// const SERVER_URL =
|
// const SERVER_URL =
|
||||||
|
|
@ -179,5 +181,6 @@ export const issueConfig = [
|
||||||
];
|
];
|
||||||
|
|
||||||
export const payments = {
|
export const payments = {
|
||||||
MIN_AMOUNT: 200,
|
MIN_AMOUNT: 1,
|
||||||
|
LINK_EXPIRED: "Payment link expired",
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -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<void>;
|
||||||
|
onPaymentConfirmation: (callback: (data: any) => void) => void;
|
||||||
|
offPaymentConfirmation: () => void;
|
||||||
|
disconnect: () => void;
|
||||||
|
connectSocket: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const SocketContext = createContext<SocketContextType | undefined>(undefined);
|
||||||
|
|
||||||
|
interface SocketProviderProps {
|
||||||
|
children: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SocketProvider: React.FC<SocketProviderProps> = ({ 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 (
|
||||||
|
<SocketContext.Provider
|
||||||
|
value={{
|
||||||
|
isConnected,
|
||||||
|
registerTransaction,
|
||||||
|
onPaymentConfirmation,
|
||||||
|
offPaymentConfirmation,
|
||||||
|
disconnect: () => {
|
||||||
|
SocketService.disconnect();
|
||||||
|
setIsConnected(false);
|
||||||
|
},
|
||||||
|
connectSocket,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</SocketContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useSocket = (): SocketContextType => {
|
||||||
|
const context = useContext(SocketContext);
|
||||||
|
if (context === undefined) {
|
||||||
|
throw new Error("useSocket must be used within a SocketProvider");
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
};
|
||||||
|
|
@ -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<Socket> {
|
||||||
|
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<void> {
|
||||||
|
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();
|
||||||
|
|
@ -103,8 +103,10 @@ export const connectSocket = () => {
|
||||||
|
|
||||||
export const initSocket = async () => {
|
export const initSocket = async () => {
|
||||||
try {
|
try {
|
||||||
|
//get latest telemetry as fallback option
|
||||||
token = await fetchToken();
|
token = await fetchToken();
|
||||||
controllingServer = await fetchControllingServer(token);
|
controllingServer = await fetchControllingServer(token);
|
||||||
|
await fetchLatestTelemetry(token, controllingServer);
|
||||||
connectSocket();
|
connectSocket();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log(err, "");
|
console.log(err, "");
|
||||||
|
|
@ -165,3 +167,71 @@ const handleSocketData = (data: any) => {
|
||||||
store.dispatch(setTelemetryError("Data parsing failed"));
|
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"));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
|
||||||
|
|
@ -32,14 +32,6 @@ export const telemetrySlice = createSlice({
|
||||||
},
|
},
|
||||||
setTelemetryLoading: (state) => {
|
setTelemetryLoading: (state) => {
|
||||||
state.loading = true;
|
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<string>) => {
|
setTelemetryError: (state, action: PayloadAction<string>) => {
|
||||||
state.loading = false;
|
state.loading = false;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue