Add Select Amount and Pay EMI screens
parent
9d5869c3c2
commit
d12efb4fd5
|
|
@ -39,7 +39,6 @@ export default function TabLayout() {
|
||||||
initSocket();
|
initSocket();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to fetch user details", error);
|
console.error("Failed to fetch user details", error);
|
||||||
logout();
|
|
||||||
showSnackbar("Failed to fetch user details", "error");
|
showSnackbar("Failed to fetch user details", "error");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -18,9 +18,10 @@ import CustomerCareIcon from "../../assets/icons/customer-care.svg";
|
||||||
import api from "@/services/axiosClient";
|
import api from "@/services/axiosClient";
|
||||||
import PaymentHistoryCard from "@/components/Payments/PaymentHistoryCard";
|
import PaymentHistoryCard from "@/components/Payments/PaymentHistoryCard";
|
||||||
import { BASE_URL } from "@/constants/config";
|
import { BASE_URL } from "@/constants/config";
|
||||||
|
import { useDispatch } from "react-redux";
|
||||||
|
import { setDueAmount, setMyPlan } from "@/store/paymentSlice";
|
||||||
|
|
||||||
// Type definitions
|
export interface MyPlan {
|
||||||
interface MyPlan {
|
|
||||||
no_of_emi: number;
|
no_of_emi: number;
|
||||||
total_amount: number;
|
total_amount: number;
|
||||||
down_payment: number;
|
down_payment: number;
|
||||||
|
|
@ -71,8 +72,7 @@ interface PaymentHistoryResponse {
|
||||||
|
|
||||||
const formatPaymentDate = (dateString: string) => {
|
const formatPaymentDate = (dateString: string) => {
|
||||||
try {
|
try {
|
||||||
// Parse the formatted date from API (e.g., "7 Aug 2025, 5:23:58 pm")
|
return dateString.split(",")[0];
|
||||||
return dateString.split(",")[0]; // Extract just the date part
|
|
||||||
} catch {
|
} catch {
|
||||||
return dateString;
|
return dateString;
|
||||||
}
|
}
|
||||||
|
|
@ -105,7 +105,7 @@ export default function PaymentsTabScreen() {
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
const [isSupportModalVisible, setIsSupportModalVisible] = useState(false);
|
const [isSupportModalVisible, setIsSupportModalVisible] = useState(false);
|
||||||
const [isEndReached, setIsEndReached] = useState(false);
|
const [isEndReached, setIsEndReached] = useState(false);
|
||||||
|
const dispatch = useDispatch();
|
||||||
//payment history states
|
//payment history states
|
||||||
const [paymentHistory, setPaymentHistory] = useState<PaymentHistoryItem[]>(
|
const [paymentHistory, setPaymentHistory] = useState<PaymentHistoryItem[]>(
|
||||||
[]
|
[]
|
||||||
|
|
@ -138,7 +138,12 @@ export default function PaymentsTabScreen() {
|
||||||
const result: EmiResponse = response.data;
|
const result: EmiResponse = response.data;
|
||||||
|
|
||||||
if (result.success && result.data.length > 0) {
|
if (result.success && result.data.length > 0) {
|
||||||
setEmiDetails(result.data[0]);
|
const details = result.data[0];
|
||||||
|
|
||||||
|
setEmiDetails(details);
|
||||||
|
|
||||||
|
dispatch(setDueAmount(details.due_amount));
|
||||||
|
dispatch(setMyPlan(details.myPlain));
|
||||||
} else {
|
} else {
|
||||||
showSnackbar("No EMI details found", "error");
|
showSnackbar("No EMI details found", "error");
|
||||||
}
|
}
|
||||||
|
|
@ -357,16 +362,20 @@ export default function PaymentsTabScreen() {
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Divider */}
|
|
||||||
<View style={styles.divider} />
|
<View style={styles.divider} />
|
||||||
|
|
||||||
{/* Action Buttons */}
|
|
||||||
<View style={styles.buttonContainer}>
|
<View style={styles.buttonContainer}>
|
||||||
<TouchableOpacity style={styles.primaryButton}>
|
<TouchableOpacity
|
||||||
|
style={styles.primaryButton}
|
||||||
|
onPress={() => router.push("/payments/selectAmount")}
|
||||||
|
>
|
||||||
<Text style={styles.primaryButtonText}>Pay EMI</Text>
|
<Text style={styles.primaryButtonText}>Pay EMI</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
|
|
||||||
<TouchableOpacity style={styles.tertiaryButton}>
|
<TouchableOpacity
|
||||||
|
style={styles.tertiaryButton}
|
||||||
|
onPress={() => router.push("/payments/myPlan")}
|
||||||
|
>
|
||||||
<Text style={styles.tertiaryButtonText}>View Plan</Text>
|
<Text style={styles.tertiaryButtonText}>View Plan</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
|
|
@ -445,8 +454,14 @@ export default function PaymentsTabScreen() {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const StatusBadge = ({ label, type }: { label: string; type: string }) => {
|
const StatusBadge = ({
|
||||||
if (!label) return null;
|
label,
|
||||||
|
type,
|
||||||
|
}: {
|
||||||
|
label: string | undefined;
|
||||||
|
type: string | undefined;
|
||||||
|
}) => {
|
||||||
|
if (!label || !type) return "--";
|
||||||
const getBadgeStyle = (type: string) => {
|
const getBadgeStyle = (type: string) => {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case "pending":
|
case "pending":
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,277 @@
|
||||||
|
import Header from "@/components/common/Header";
|
||||||
|
import ProgressCard from "@/components/common/ProgressCard";
|
||||||
|
import { RootState } from "@/store/rootReducer";
|
||||||
|
import React from "react";
|
||||||
|
import { View, Text, StyleSheet, TouchableOpacity } from "react-native";
|
||||||
|
import { useSelector } from "react-redux";
|
||||||
|
|
||||||
|
interface MyPlan {
|
||||||
|
planType?: string;
|
||||||
|
totalCost?: number;
|
||||||
|
downPayment?: number;
|
||||||
|
totalEmi?: number;
|
||||||
|
monthlyEmi?: number;
|
||||||
|
installmentsPaid?: number;
|
||||||
|
totalInstallments?: number;
|
||||||
|
emiPaidTillNow?: number;
|
||||||
|
totalAmountDue?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const MyPlanScreen: React.FC = () => {
|
||||||
|
const myPlan = useSelector((state: RootState) => state.payments.myPlan);
|
||||||
|
const dueAmount = useSelector(
|
||||||
|
(state: RootState) => state.payments.due_amount
|
||||||
|
);
|
||||||
|
|
||||||
|
// Helper function to format currency
|
||||||
|
const formatCurrency = (amount?: number | null): string => {
|
||||||
|
if (amount == null) return "---";
|
||||||
|
return `₹${amount.toLocaleString("en-IN")}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Helper function to display value or fallback
|
||||||
|
const displayValue = (value?: string | number | null): string => {
|
||||||
|
if (value == null || value === "") return "---";
|
||||||
|
return value.toString();
|
||||||
|
};
|
||||||
|
|
||||||
|
// Calculate progress percentage for EMI progress bar
|
||||||
|
const getProgressPercentage = (
|
||||||
|
firstValue: number,
|
||||||
|
secondValue: number
|
||||||
|
): number => {
|
||||||
|
if (!firstValue || !secondValue) return 0;
|
||||||
|
return (firstValue / secondValue) * 100;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={styles.container}>
|
||||||
|
<Header title="My Plan" showBackButton={true} />
|
||||||
|
|
||||||
|
<View style={styles.contentFrame}>
|
||||||
|
{/* Plan Details Card */}
|
||||||
|
<View style={styles.card}>
|
||||||
|
<Text style={styles.cardTitle}>Plan Details</Text>
|
||||||
|
<View style={styles.divider} />
|
||||||
|
|
||||||
|
<View style={styles.content}>
|
||||||
|
{/* Plan Type Row */}
|
||||||
|
<View style={styles.row}>
|
||||||
|
<Text style={styles.label}>Plan Type</Text>
|
||||||
|
<Text style={styles.value}>
|
||||||
|
{displayValue(myPlan?.no_of_emi)}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Total Cost Row */}
|
||||||
|
<View style={styles.row}>
|
||||||
|
<Text style={styles.label}>Total Cost</Text>
|
||||||
|
<Text style={styles.value}>
|
||||||
|
{formatCurrency(myPlan?.total_amount)}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Down Payment Row */}
|
||||||
|
<View style={styles.row}>
|
||||||
|
<Text style={styles.label}>Down Payment</Text>
|
||||||
|
<Text style={styles.value}>
|
||||||
|
{formatCurrency(myPlan?.down_payment)}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Total EMI Row */}
|
||||||
|
<View style={styles.row}>
|
||||||
|
<Text style={styles.label}>Total EMI</Text>
|
||||||
|
<Text style={styles.value}>
|
||||||
|
{formatCurrency(myPlan?.total_emi)}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Total Amount Due Card */}
|
||||||
|
<View style={styles.card}>
|
||||||
|
<Text style={styles.centerLabel}>Total Amount Due</Text>
|
||||||
|
<View style={styles.amountContainer}>
|
||||||
|
<Text style={styles.dueAmount}>{formatCurrency(dueAmount)}</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View style={styles.twoColumnContainer}>
|
||||||
|
{/* Monthly EMI Card */}
|
||||||
|
<View style={styles.halfCard}>
|
||||||
|
<Text style={styles.label}>Monthly EMI</Text>
|
||||||
|
<Text style={styles.value}>
|
||||||
|
{formatCurrency(myPlan?.total_emi)}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View style={styles.halfCard}>
|
||||||
|
<Text style={styles.label}>Installments Paid</Text>
|
||||||
|
<View style={styles.installmentRow}>
|
||||||
|
<Text style={styles.value}>
|
||||||
|
{displayValue(myPlan?.installment_paid)}
|
||||||
|
</Text>
|
||||||
|
<Text style={styles.installmentTotal}>
|
||||||
|
/ {displayValue(myPlan?.no_of_emi)}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<ProgressCard
|
||||||
|
title="EMI Paid Till Now"
|
||||||
|
firstText={formatCurrency(myPlan?.current_amount)}
|
||||||
|
secondText={formatCurrency(myPlan?.total_emi)}
|
||||||
|
percentage={getProgressPercentage(
|
||||||
|
myPlan?.current_amount || 0,
|
||||||
|
myPlan?.total_emi || 0
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TouchableOpacity style={styles.payButton}>
|
||||||
|
<Text style={styles.payButtonText}>Pay EMI</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
container: {
|
||||||
|
flex: 1,
|
||||||
|
backgroundColor: "#F3F5F8",
|
||||||
|
paddingTop: 16,
|
||||||
|
},
|
||||||
|
header: {
|
||||||
|
paddingHorizontal: 16,
|
||||||
|
paddingVertical: 16,
|
||||||
|
backgroundColor: "#F3F5F8",
|
||||||
|
},
|
||||||
|
headerTitle: {
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: "600",
|
||||||
|
color: "#252A34",
|
||||||
|
},
|
||||||
|
contentFrame: {
|
||||||
|
flex: 1,
|
||||||
|
paddingHorizontal: 16,
|
||||||
|
paddingBottom: 16,
|
||||||
|
gap: 16,
|
||||||
|
},
|
||||||
|
card: {
|
||||||
|
backgroundColor: "#FCFCFC",
|
||||||
|
borderRadius: 8,
|
||||||
|
padding: 12,
|
||||||
|
gap: 12,
|
||||||
|
},
|
||||||
|
cardTitle: {
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: "600",
|
||||||
|
color: "#252A34",
|
||||||
|
height: 20,
|
||||||
|
},
|
||||||
|
divider: {
|
||||||
|
height: 1,
|
||||||
|
backgroundColor: "#E5E9F0",
|
||||||
|
},
|
||||||
|
content: {
|
||||||
|
gap: 8,
|
||||||
|
},
|
||||||
|
row: {
|
||||||
|
flexDirection: "row",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
alignItems: "center",
|
||||||
|
height: 20,
|
||||||
|
},
|
||||||
|
label: {
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: "400",
|
||||||
|
color: "#252A34",
|
||||||
|
},
|
||||||
|
value: {
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: "600",
|
||||||
|
color: "#252A34",
|
||||||
|
},
|
||||||
|
centerLabel: {
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: "400",
|
||||||
|
color: "#252A34",
|
||||||
|
textAlign: "center",
|
||||||
|
height: 20,
|
||||||
|
},
|
||||||
|
amountContainer: {
|
||||||
|
height: 28,
|
||||||
|
justifyContent: "center",
|
||||||
|
alignItems: "center",
|
||||||
|
},
|
||||||
|
dueAmount: {
|
||||||
|
fontSize: 20,
|
||||||
|
fontWeight: "600",
|
||||||
|
color: "#252A34",
|
||||||
|
textAlign: "center",
|
||||||
|
},
|
||||||
|
twoColumnContainer: {
|
||||||
|
flexDirection: "row",
|
||||||
|
gap: 16,
|
||||||
|
},
|
||||||
|
halfCard: {
|
||||||
|
flex: 1,
|
||||||
|
backgroundColor: "#FCFCFC",
|
||||||
|
borderRadius: 8,
|
||||||
|
padding: 12,
|
||||||
|
gap: 8,
|
||||||
|
height: 72,
|
||||||
|
},
|
||||||
|
installmentRow: {
|
||||||
|
flexDirection: "row",
|
||||||
|
alignItems: "flex-end",
|
||||||
|
gap: 4,
|
||||||
|
},
|
||||||
|
installmentTotal: {
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: "500",
|
||||||
|
color: "#565F70",
|
||||||
|
},
|
||||||
|
progressAmountRow: {
|
||||||
|
flexDirection: "row",
|
||||||
|
alignItems: "flex-end",
|
||||||
|
gap: 4,
|
||||||
|
},
|
||||||
|
progressTotal: {
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: "500",
|
||||||
|
color: "#565F70",
|
||||||
|
},
|
||||||
|
progressBarContainer: {
|
||||||
|
width: "100%",
|
||||||
|
height: 8,
|
||||||
|
},
|
||||||
|
progressBarBackground: {
|
||||||
|
width: "100%",
|
||||||
|
height: 8,
|
||||||
|
backgroundColor: "#E5EAF0",
|
||||||
|
borderRadius: 10,
|
||||||
|
overflow: "hidden",
|
||||||
|
},
|
||||||
|
progressBarFill: {
|
||||||
|
height: "100%",
|
||||||
|
backgroundColor: "#4CAF50",
|
||||||
|
borderRadius: 10,
|
||||||
|
},
|
||||||
|
payButton: {
|
||||||
|
backgroundColor: "#4CAF50",
|
||||||
|
borderRadius: 8,
|
||||||
|
paddingVertical: 16,
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
},
|
||||||
|
payButtonText: {
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: "600",
|
||||||
|
color: "#FFFFFF",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default MyPlanScreen;
|
||||||
|
|
@ -0,0 +1,529 @@
|
||||||
|
import React, { useEffect } from "react";
|
||||||
|
import {
|
||||||
|
View,
|
||||||
|
Text,
|
||||||
|
TouchableOpacity,
|
||||||
|
StyleSheet,
|
||||||
|
SafeAreaView,
|
||||||
|
Alert,
|
||||||
|
Share,
|
||||||
|
Clipboard,
|
||||||
|
Platform,
|
||||||
|
Linking,
|
||||||
|
ActivityIndicator,
|
||||||
|
} from "react-native";
|
||||||
|
import { Ionicons } from "@expo/vector-icons";
|
||||||
|
import * as FileSystem from "expo-file-system";
|
||||||
|
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 { updatePaymentStatus } from "@/store/paymentSlice";
|
||||||
|
|
||||||
|
interface UpiPaymentScreenProps {
|
||||||
|
onBack?: () => void;
|
||||||
|
onPaymentSuccess?: () => void;
|
||||||
|
onPaymentFailure?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const UpiPaymentScreen: React.FC<UpiPaymentScreenProps> = ({
|
||||||
|
onBack,
|
||||||
|
onPaymentSuccess,
|
||||||
|
onPaymentFailure,
|
||||||
|
}) => {
|
||||||
|
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]);
|
||||||
|
|
||||||
|
// Show loading if no payment data
|
||||||
|
if (!paymentOrder) {
|
||||||
|
return (
|
||||||
|
<SafeAreaView style={styles.container}>
|
||||||
|
<View style={styles.loadingContainer}>
|
||||||
|
<ActivityIndicator size="large" color="#00876F" />
|
||||||
|
<Text style={styles.loadingText}>Loading payment details...</Text>
|
||||||
|
</View>
|
||||||
|
</SafeAreaView>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Format amount with currency symbol
|
||||||
|
const formatAmount = (amount: number): string => {
|
||||||
|
return `₹${amount.toLocaleString("en-IN")}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Extract numeric amount from the amount
|
||||||
|
const getNumericAmount = (): string => {
|
||||||
|
return paymentOrder.amount.toString();
|
||||||
|
};
|
||||||
|
|
||||||
|
// Check if payment is expired
|
||||||
|
const isPaymentExpired = (): boolean => {
|
||||||
|
const expiryDate = new Date(paymentOrder.expiry_date);
|
||||||
|
const currentDate = new Date();
|
||||||
|
return currentDate > expiryDate;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Get formatted expiry time
|
||||||
|
const getExpiryTime = (): string => {
|
||||||
|
const expiryDate = new Date(paymentOrder.expiry_date);
|
||||||
|
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<void> => {
|
||||||
|
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 }]
|
||||||
|
);
|
||||||
|
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.",
|
||||||
|
[
|
||||||
|
{
|
||||||
|
text: "Share Payment Details",
|
||||||
|
onPress: () => sharePaymentDetails(),
|
||||||
|
},
|
||||||
|
{ text: "Cancel", style: "cancel" },
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} 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" },
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const sharePaymentDetails = async (): Promise<void> => {
|
||||||
|
try {
|
||||||
|
const shareMessage =
|
||||||
|
`💰 Payment Request\n\n` +
|
||||||
|
`Amount: ${formatAmount(paymentOrder.amount)}\n` +
|
||||||
|
`UPI ID: ${paymentOrder.upi_handle}\n` +
|
||||||
|
`Order ID: ${paymentOrder.order_id}\n` +
|
||||||
|
`Transaction ID: ${paymentOrder.transaction_id}\n` +
|
||||||
|
`Expires: ${getExpiryTime()}\n\n` +
|
||||||
|
`UPI Link: ${getUpiUrl()}`;
|
||||||
|
|
||||||
|
await Share.share({
|
||||||
|
message: shareMessage,
|
||||||
|
title: "UPI Payment Details",
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
Alert.alert("Error", "Failed to share payment details");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
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> => {
|
||||||
|
try {
|
||||||
|
if (await Sharing.isAvailableAsync()) {
|
||||||
|
await Share.share({
|
||||||
|
message: `Pay ${formatAmount(
|
||||||
|
paymentOrder.amount
|
||||||
|
)} using this QR code`,
|
||||||
|
url: paymentOrder.qr_code_url,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
Alert.alert("Error", "Sharing is not available on this device");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
Alert.alert("Error", "Failed to share QR code");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const downloadQR = async (): Promise<void> => {
|
||||||
|
try {
|
||||||
|
const { status } = await MediaLibrary.requestPermissionsAsync();
|
||||||
|
if (status !== "granted") {
|
||||||
|
Alert.alert(
|
||||||
|
"Permission Required",
|
||||||
|
"Please grant permission to save images"
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fileUri =
|
||||||
|
FileSystem.documentDirectory + `qr-${paymentOrder.order_id}.png`;
|
||||||
|
const downloadResult = await FileSystem.downloadAsync(
|
||||||
|
paymentOrder.qr_code_url,
|
||||||
|
fileUri
|
||||||
|
);
|
||||||
|
|
||||||
|
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");
|
||||||
|
} else {
|
||||||
|
Alert.alert("Error", "Failed to download QR code");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
Alert.alert("Error", "Failed to download QR code");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getStatusColor = (status: string): string => {
|
||||||
|
switch (status) {
|
||||||
|
case "completed":
|
||||||
|
case "success":
|
||||||
|
return "#00876F";
|
||||||
|
case "failed":
|
||||||
|
return "#E74C3C";
|
||||||
|
case "processing":
|
||||||
|
return "#F39C12";
|
||||||
|
default:
|
||||||
|
return "#6C757D";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SafeAreaView style={styles.container}>
|
||||||
|
<View style={styles.header}>
|
||||||
|
<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.qrFrame}>
|
||||||
|
<View style={styles.amountSection}>
|
||||||
|
<Text style={styles.amountLabel}>Amount to be paid</Text>
|
||||||
|
<Text style={styles.amount}>
|
||||||
|
{formatAmount(paymentOrder.amount)}
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
{/* Payment Status */}
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
styles.statusBadge,
|
||||||
|
{ backgroundColor: getStatusColor(paymentOrder.status) },
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Text style={styles.statusText}>
|
||||||
|
{paymentOrder.status.toUpperCase()}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View style={styles.qrCodeContainer}>
|
||||||
|
<Image
|
||||||
|
source={{ uri: paymentOrder.qr_code_url }}
|
||||||
|
style={styles.qrCode}
|
||||||
|
contentFit="contain"
|
||||||
|
/>
|
||||||
|
</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}>
|
||||||
|
<TouchableOpacity onPress={shareQR} style={styles.secondaryButton}>
|
||||||
|
<Ionicons name="share-outline" size={20} color="#253342" />
|
||||||
|
<Text style={styles.secondaryButtonText}>Share QR</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
|
||||||
|
<TouchableOpacity
|
||||||
|
onPress={downloadQR}
|
||||||
|
style={styles.secondaryButton}
|
||||||
|
>
|
||||||
|
<Ionicons name="download-outline" size={20} color="#253342" />
|
||||||
|
<Text style={styles.secondaryButtonText}>Download QR</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<TouchableOpacity
|
||||||
|
onPress={payUsingUpiApp}
|
||||||
|
style={[
|
||||||
|
styles.primaryButton,
|
||||||
|
(isPaymentExpired() || paymentOrder.status === "completed") &&
|
||||||
|
styles.disabledButton,
|
||||||
|
]}
|
||||||
|
disabled={isPaymentExpired() || paymentOrder.status === "completed"}
|
||||||
|
>
|
||||||
|
<Text
|
||||||
|
style={[
|
||||||
|
styles.primaryButtonText,
|
||||||
|
(isPaymentExpired() || paymentOrder.status === "completed") &&
|
||||||
|
styles.disabledButtonText,
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
{paymentOrder.status === "completed"
|
||||||
|
? "Payment Completed"
|
||||||
|
: isPaymentExpired()
|
||||||
|
? "Payment Expired"
|
||||||
|
: "Pay using UPI app"}
|
||||||
|
</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
</SafeAreaView>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
container: {
|
||||||
|
flex: 1,
|
||||||
|
backgroundColor: "#F3F5F8",
|
||||||
|
},
|
||||||
|
loadingContainer: {
|
||||||
|
flex: 1,
|
||||||
|
justifyContent: "center",
|
||||||
|
alignItems: "center",
|
||||||
|
},
|
||||||
|
loadingText: {
|
||||||
|
marginTop: 16,
|
||||||
|
fontSize: 16,
|
||||||
|
color: "#253342",
|
||||||
|
},
|
||||||
|
header: {
|
||||||
|
flexDirection: "row",
|
||||||
|
alignItems: "center",
|
||||||
|
paddingHorizontal: 16,
|
||||||
|
paddingVertical: 12,
|
||||||
|
backgroundColor: "#FFFFFF",
|
||||||
|
},
|
||||||
|
backButton: {
|
||||||
|
padding: 8,
|
||||||
|
marginRight: 8,
|
||||||
|
},
|
||||||
|
headerTitle: {
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: "600",
|
||||||
|
color: "#253342",
|
||||||
|
},
|
||||||
|
content: {
|
||||||
|
flex: 1,
|
||||||
|
justifyContent: "space-between",
|
||||||
|
paddingHorizontal: 16,
|
||||||
|
paddingBottom: 16,
|
||||||
|
},
|
||||||
|
qrFrame: {
|
||||||
|
backgroundColor: "#FCFCFC",
|
||||||
|
borderRadius: 8,
|
||||||
|
padding: 16,
|
||||||
|
alignItems: "center",
|
||||||
|
marginTop: 16,
|
||||||
|
},
|
||||||
|
amountSection: {
|
||||||
|
alignItems: "center",
|
||||||
|
marginBottom: 16,
|
||||||
|
},
|
||||||
|
amountLabel: {
|
||||||
|
fontSize: 14,
|
||||||
|
color: "#253342",
|
||||||
|
textAlign: "center",
|
||||||
|
marginBottom: 4,
|
||||||
|
},
|
||||||
|
amount: {
|
||||||
|
fontSize: 20,
|
||||||
|
fontWeight: "600",
|
||||||
|
color: "#253342",
|
||||||
|
textAlign: "center",
|
||||||
|
marginBottom: 8,
|
||||||
|
},
|
||||||
|
statusBadge: {
|
||||||
|
paddingHorizontal: 12,
|
||||||
|
paddingVertical: 4,
|
||||||
|
borderRadius: 12,
|
||||||
|
},
|
||||||
|
statusText: {
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: "600",
|
||||||
|
color: "#FFFFFF",
|
||||||
|
},
|
||||||
|
qrCodeContainer: {
|
||||||
|
width: 248,
|
||||||
|
height: 248,
|
||||||
|
justifyContent: "center",
|
||||||
|
alignItems: "center",
|
||||||
|
marginBottom: 16,
|
||||||
|
},
|
||||||
|
qrCode: {
|
||||||
|
width: "100%",
|
||||||
|
height: "100%",
|
||||||
|
},
|
||||||
|
upiIdSection: {
|
||||||
|
flexDirection: "row",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
marginBottom: 12,
|
||||||
|
padding: 8,
|
||||||
|
backgroundColor: "#F3F5F8",
|
||||||
|
borderRadius: 4,
|
||||||
|
},
|
||||||
|
upiIdText: {
|
||||||
|
fontSize: 14,
|
||||||
|
color: "#253342",
|
||||||
|
marginRight: 8,
|
||||||
|
fontWeight: "500",
|
||||||
|
},
|
||||||
|
expirySection: {
|
||||||
|
alignItems: "center",
|
||||||
|
marginBottom: 16,
|
||||||
|
},
|
||||||
|
expiryLabel: {
|
||||||
|
fontSize: 12,
|
||||||
|
color: "#6C757D",
|
||||||
|
marginBottom: 2,
|
||||||
|
},
|
||||||
|
expiryTime: {
|
||||||
|
fontSize: 14,
|
||||||
|
color: "#253342",
|
||||||
|
fontWeight: "500",
|
||||||
|
},
|
||||||
|
buttonsContainer: {
|
||||||
|
flexDirection: "row",
|
||||||
|
gap: 8,
|
||||||
|
width: "100%",
|
||||||
|
},
|
||||||
|
secondaryButton: {
|
||||||
|
flex: 1,
|
||||||
|
flexDirection: "row",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
paddingVertical: 10,
|
||||||
|
paddingHorizontal: 16,
|
||||||
|
backgroundColor: "#F3F5F8",
|
||||||
|
borderColor: "#D8DDE7",
|
||||||
|
borderWidth: 1,
|
||||||
|
borderRadius: 4,
|
||||||
|
gap: 4,
|
||||||
|
},
|
||||||
|
secondaryButtonText: {
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: "500",
|
||||||
|
color: "#253342",
|
||||||
|
},
|
||||||
|
primaryButton: {
|
||||||
|
backgroundColor: "#00876F",
|
||||||
|
paddingVertical: 12,
|
||||||
|
paddingHorizontal: 16,
|
||||||
|
borderRadius: 4,
|
||||||
|
alignItems: "center",
|
||||||
|
},
|
||||||
|
primaryButtonText: {
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: "500",
|
||||||
|
color: "#FCFCFC",
|
||||||
|
},
|
||||||
|
disabledButton: {
|
||||||
|
backgroundColor: "#D8DDE7",
|
||||||
|
},
|
||||||
|
disabledButtonText: {
|
||||||
|
color: "#6C757D",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default UpiPaymentScreen;
|
||||||
|
|
@ -0,0 +1,433 @@
|
||||||
|
import React, { useState } from "react";
|
||||||
|
import {
|
||||||
|
View,
|
||||||
|
Text,
|
||||||
|
StyleSheet,
|
||||||
|
TouchableOpacity,
|
||||||
|
TextInput,
|
||||||
|
ScrollView,
|
||||||
|
Platform,
|
||||||
|
KeyboardAvoidingView,
|
||||||
|
} from "react-native";
|
||||||
|
import { useDispatch, useSelector } from "react-redux";
|
||||||
|
import { Formik } from "formik";
|
||||||
|
import * as Yup from "yup";
|
||||||
|
import Header from "../../components/common/Header";
|
||||||
|
import { RootState } from "@/store/rootReducer";
|
||||||
|
import { BASE_URL, payments } from "@/constants/config";
|
||||||
|
import api from "@/services/axiosClient";
|
||||||
|
import { setPaymentOrder } from "@/store/paymentSlice";
|
||||||
|
import { Overlay } from "@/components/common/Overlay";
|
||||||
|
import { useRouter } from "expo-router";
|
||||||
|
|
||||||
|
// Validation schema
|
||||||
|
const validationSchema = Yup.object().shape({
|
||||||
|
paymentType: Yup.string().required("Please select a payment option"),
|
||||||
|
customAmount: Yup.string().when("paymentType", {
|
||||||
|
is: "custom",
|
||||||
|
then: (schema) =>
|
||||||
|
schema
|
||||||
|
.required("Amount is required")
|
||||||
|
.test(
|
||||||
|
"min-amount",
|
||||||
|
`Minimum amount is ₹${payments.MIN_AMOUNT}`,
|
||||||
|
(value) => {
|
||||||
|
const numValue = parseFloat(value || "0");
|
||||||
|
return 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(),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const SelectAmountScreen = () => {
|
||||||
|
// Fetch due amount from Redux
|
||||||
|
const dueAmount = useSelector(
|
||||||
|
(state: RootState) => state.payments?.due_amount || 0
|
||||||
|
);
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
const [isFetching, setIsFetching] = useState<boolean>(false);
|
||||||
|
|
||||||
|
const quickAmounts = [50, 100, 500, 1000];
|
||||||
|
|
||||||
|
const initialValues = {
|
||||||
|
paymentType: "due",
|
||||||
|
customAmount: "",
|
||||||
|
};
|
||||||
|
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
const handleSubmit = async (values: any) => {
|
||||||
|
setIsFetching(true);
|
||||||
|
const paymentAmount =
|
||||||
|
values.paymentType === "due"
|
||||||
|
? dueAmount
|
||||||
|
: parseFloat(values.customAmount);
|
||||||
|
console.log(values, "values");
|
||||||
|
try {
|
||||||
|
const res = await api.post(`/api/v1/create-order`, {
|
||||||
|
amount: paymentAmount,
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(res, "response");
|
||||||
|
if (res.data && res.data.success) {
|
||||||
|
dispatch(setPaymentOrder(res.data.data));
|
||||||
|
router.push("/payments/payEmi");
|
||||||
|
} else {
|
||||||
|
throw new Error("Failed to create order");
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err, "Error in creating order.");
|
||||||
|
} finally {
|
||||||
|
setIsFetching(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("Payment Amount:", paymentAmount);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Formik
|
||||||
|
initialValues={initialValues}
|
||||||
|
validationSchema={validationSchema}
|
||||||
|
onSubmit={handleSubmit}
|
||||||
|
>
|
||||||
|
{({
|
||||||
|
values,
|
||||||
|
errors,
|
||||||
|
touched,
|
||||||
|
handleChange,
|
||||||
|
handleBlur,
|
||||||
|
handleSubmit,
|
||||||
|
setFieldValue,
|
||||||
|
isValid,
|
||||||
|
dirty,
|
||||||
|
}) => {
|
||||||
|
const handleQuickAmountPress = (amount: number) => {
|
||||||
|
const currentAmount = values.customAmount
|
||||||
|
? parseFloat(values.customAmount)
|
||||||
|
: 0;
|
||||||
|
const newAmount = currentAmount + amount;
|
||||||
|
setFieldValue("customAmount", newAmount.toString());
|
||||||
|
};
|
||||||
|
|
||||||
|
const getPaymentAmount = () => {
|
||||||
|
if (values.paymentType === "due") {
|
||||||
|
return dueAmount;
|
||||||
|
}
|
||||||
|
return values.customAmount ? parseFloat(values.customAmount) : 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
const isPayButtonEnabled = () => {
|
||||||
|
if (values.paymentType === "due") return true;
|
||||||
|
return isValid && dirty && values.customAmount;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<KeyboardAvoidingView
|
||||||
|
style={styles.container}
|
||||||
|
behavior={Platform.OS === "ios" ? "padding" : "height"}
|
||||||
|
keyboardVerticalOffset={Platform.OS === "ios" ? 0 : 20}
|
||||||
|
>
|
||||||
|
<Header title="Select Amount" showBackButton={true} />
|
||||||
|
|
||||||
|
<ScrollView
|
||||||
|
style={styles.content}
|
||||||
|
showsVerticalScrollIndicator={false}
|
||||||
|
>
|
||||||
|
<View style={styles.selectAmountContainer}>
|
||||||
|
<TouchableOpacity
|
||||||
|
style={[
|
||||||
|
styles.option,
|
||||||
|
values.paymentType === "due" && styles.selectedOption,
|
||||||
|
]}
|
||||||
|
onPress={() => setFieldValue("paymentType", "due")}
|
||||||
|
>
|
||||||
|
<View style={styles.radioContainer}>
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
styles.radioDot,
|
||||||
|
values.paymentType === "due" &&
|
||||||
|
styles.selectedRadioDot,
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
{values.paymentType === "due" && (
|
||||||
|
<View style={styles.radioInner} />
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
<Text style={styles.radioLabel}>Pay amount due</Text>
|
||||||
|
</View>
|
||||||
|
<Text style={styles.amountText}>
|
||||||
|
₹{dueAmount?.toFixed(2)}
|
||||||
|
</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
styles.customOption,
|
||||||
|
values.paymentType === "custom" && styles.selectedOption,
|
||||||
|
touched.customAmount &&
|
||||||
|
errors.customAmount &&
|
||||||
|
styles.errorOption,
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<TouchableOpacity
|
||||||
|
style={styles.radioContainer}
|
||||||
|
onPress={() => setFieldValue("paymentType", "custom")}
|
||||||
|
>
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
styles.radioDot,
|
||||||
|
values.paymentType === "custom" &&
|
||||||
|
styles.selectedRadioDot,
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
{values.paymentType === "custom" && (
|
||||||
|
<View style={styles.radioInner} />
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
<Text style={styles.radioLabel}>Enter custom amount</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
|
||||||
|
<View style={styles.inputContainer}>
|
||||||
|
<TextInput
|
||||||
|
style={[
|
||||||
|
styles.textInput,
|
||||||
|
touched.customAmount &&
|
||||||
|
errors.customAmount &&
|
||||||
|
styles.errorInput,
|
||||||
|
]}
|
||||||
|
value={values.customAmount}
|
||||||
|
onChangeText={(text) => {
|
||||||
|
handleChange("customAmount")(text);
|
||||||
|
setFieldValue("paymentType", "custom");
|
||||||
|
}}
|
||||||
|
onBlur={handleBlur("customAmount")}
|
||||||
|
placeholder="₹"
|
||||||
|
placeholderTextColor="#94A3B8"
|
||||||
|
keyboardType="numeric"
|
||||||
|
onFocus={() => setFieldValue("paymentType", "custom")}
|
||||||
|
/>
|
||||||
|
<View style={styles.helperContainer}>
|
||||||
|
<Text
|
||||||
|
style={[
|
||||||
|
styles.helperText,
|
||||||
|
touched.customAmount &&
|
||||||
|
errors.customAmount &&
|
||||||
|
styles.errorText,
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
{touched.customAmount && errors.customAmount
|
||||||
|
? errors.customAmount
|
||||||
|
: "Minimum: ₹200"}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View style={styles.chipsContainer}>
|
||||||
|
{quickAmounts.map((amount) => (
|
||||||
|
<TouchableOpacity
|
||||||
|
key={amount}
|
||||||
|
style={styles.chip}
|
||||||
|
onPress={() => handleQuickAmountPress(amount)}
|
||||||
|
>
|
||||||
|
<Text style={styles.chipText}>+₹{amount}</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* General form error */}
|
||||||
|
{touched.paymentType && errors.paymentType && (
|
||||||
|
<Text style={styles.generalErrorText}>
|
||||||
|
{errors.paymentType}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
</ScrollView>
|
||||||
|
|
||||||
|
{/* Pay Button */}
|
||||||
|
<View style={styles.buttonContainer}>
|
||||||
|
<TouchableOpacity
|
||||||
|
style={[
|
||||||
|
styles.payButton,
|
||||||
|
!isPayButtonEnabled() && styles.disabledButton,
|
||||||
|
]}
|
||||||
|
onPress={() => handleSubmit()}
|
||||||
|
disabled={!isPayButtonEnabled()}
|
||||||
|
>
|
||||||
|
{getPaymentAmount() < 200 ? (
|
||||||
|
<Text style={styles.payButtonText}>Select Amount</Text>
|
||||||
|
) : (
|
||||||
|
<Text style={styles.payButtonText}>
|
||||||
|
Pay ₹{getPaymentAmount().toFixed(2)}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
</KeyboardAvoidingView>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
</Formik>
|
||||||
|
<Overlay isUploading={isFetching} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
container: {
|
||||||
|
flex: 1,
|
||||||
|
backgroundColor: "#F3F4F6",
|
||||||
|
},
|
||||||
|
content: {
|
||||||
|
flex: 1,
|
||||||
|
paddingHorizontal: 16,
|
||||||
|
},
|
||||||
|
selectAmountContainer: {
|
||||||
|
paddingTop: 16,
|
||||||
|
gap: 16,
|
||||||
|
},
|
||||||
|
option: {
|
||||||
|
backgroundColor: "#FCFCFC",
|
||||||
|
borderRadius: 8,
|
||||||
|
padding: 16,
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: "transparent",
|
||||||
|
flexDirection: "row",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
minHeight: 56,
|
||||||
|
},
|
||||||
|
selectedOption: {
|
||||||
|
borderColor: "#009E71",
|
||||||
|
},
|
||||||
|
errorOption: {
|
||||||
|
borderColor: "#EF4444",
|
||||||
|
},
|
||||||
|
customOption: {
|
||||||
|
backgroundColor: "#FCFCFC",
|
||||||
|
borderRadius: 8,
|
||||||
|
padding: 16,
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: "transparent",
|
||||||
|
minHeight: 180,
|
||||||
|
},
|
||||||
|
radioContainer: {
|
||||||
|
flexDirection: "row",
|
||||||
|
alignItems: "center",
|
||||||
|
flex: 1,
|
||||||
|
marginBottom: 16,
|
||||||
|
},
|
||||||
|
radioDot: {
|
||||||
|
width: 18,
|
||||||
|
height: 18,
|
||||||
|
borderRadius: 9,
|
||||||
|
borderWidth: 2,
|
||||||
|
borderColor: "#D1D5DB",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
marginRight: 12,
|
||||||
|
},
|
||||||
|
selectedRadioDot: {
|
||||||
|
borderColor: "#009E71",
|
||||||
|
},
|
||||||
|
radioInner: {
|
||||||
|
width: 8,
|
||||||
|
height: 8,
|
||||||
|
borderRadius: 4,
|
||||||
|
backgroundColor: "#009E71",
|
||||||
|
},
|
||||||
|
radioLabel: {
|
||||||
|
fontSize: 14,
|
||||||
|
color: "#252936",
|
||||||
|
fontWeight: "400",
|
||||||
|
},
|
||||||
|
amountText: {
|
||||||
|
fontSize: 14,
|
||||||
|
color: "#252936",
|
||||||
|
fontWeight: "400",
|
||||||
|
},
|
||||||
|
inputContainer: {
|
||||||
|
marginBottom: 16,
|
||||||
|
},
|
||||||
|
textInput: {
|
||||||
|
backgroundColor: "#FFFFFF",
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: "#D8DDE7",
|
||||||
|
borderRadius: 4,
|
||||||
|
paddingHorizontal: 8,
|
||||||
|
paddingVertical: 10,
|
||||||
|
fontSize: 14,
|
||||||
|
color: "#252936",
|
||||||
|
height: 40,
|
||||||
|
},
|
||||||
|
errorInput: {
|
||||||
|
borderColor: "#EF4444",
|
||||||
|
},
|
||||||
|
helperContainer: {
|
||||||
|
marginTop: 4,
|
||||||
|
},
|
||||||
|
helperText: {
|
||||||
|
fontSize: 14,
|
||||||
|
color: "#565F70",
|
||||||
|
},
|
||||||
|
errorText: {
|
||||||
|
color: "#EF4444",
|
||||||
|
},
|
||||||
|
generalErrorText: {
|
||||||
|
fontSize: 14,
|
||||||
|
color: "#EF4444",
|
||||||
|
textAlign: "center",
|
||||||
|
marginTop: 8,
|
||||||
|
},
|
||||||
|
chipsContainer: {
|
||||||
|
flexDirection: "row",
|
||||||
|
gap: 8,
|
||||||
|
flexWrap: "wrap",
|
||||||
|
},
|
||||||
|
chip: {
|
||||||
|
backgroundColor: "#F3F4F6",
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: "#D8DDE7",
|
||||||
|
borderRadius: 4,
|
||||||
|
paddingHorizontal: 8,
|
||||||
|
paddingVertical: 4,
|
||||||
|
minWidth: 68,
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
height: 28,
|
||||||
|
},
|
||||||
|
chipText: {
|
||||||
|
fontSize: 14,
|
||||||
|
color: "#252936",
|
||||||
|
fontWeight: "500",
|
||||||
|
},
|
||||||
|
buttonContainer: {
|
||||||
|
padding: 16,
|
||||||
|
backgroundColor: "#F3F4F6",
|
||||||
|
},
|
||||||
|
payButton: {
|
||||||
|
backgroundColor: "#009E71",
|
||||||
|
borderRadius: 4,
|
||||||
|
paddingVertical: 8,
|
||||||
|
paddingHorizontal: 16,
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
height: 40,
|
||||||
|
},
|
||||||
|
disabledButton: {
|
||||||
|
backgroundColor: "#94A3B8",
|
||||||
|
},
|
||||||
|
payButtonText: {
|
||||||
|
fontSize: 14,
|
||||||
|
color: "#FCFCFC",
|
||||||
|
fontWeight: "500",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default SelectAmountScreen;
|
||||||
|
|
@ -0,0 +1,83 @@
|
||||||
|
import React from "react";
|
||||||
|
import { View, Text, TouchableOpacity, StyleSheet } from "react-native";
|
||||||
|
import { useNavigation } from "@react-navigation/native";
|
||||||
|
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||||
|
import ChevronLeft from "@/assets/icons/chevron_left.svg";
|
||||||
|
|
||||||
|
type HeaderProps = {
|
||||||
|
title: string;
|
||||||
|
showBackButton?: boolean;
|
||||||
|
onBackPress?: () => void;
|
||||||
|
rightComponent?: React.ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
const Header: React.FC<HeaderProps> = ({
|
||||||
|
title,
|
||||||
|
showBackButton = false,
|
||||||
|
onBackPress,
|
||||||
|
rightComponent,
|
||||||
|
}) => {
|
||||||
|
const navigation = useNavigation();
|
||||||
|
const insets = useSafeAreaInsets(); // get safe area insets
|
||||||
|
|
||||||
|
const handleBack = () => {
|
||||||
|
if (onBackPress) {
|
||||||
|
onBackPress();
|
||||||
|
} else {
|
||||||
|
navigation.goBack();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={[styles.container, { paddingTop: insets.top }]}>
|
||||||
|
{/* Left side */}
|
||||||
|
<View style={styles.leftContainer}>
|
||||||
|
{showBackButton && (
|
||||||
|
<TouchableOpacity style={styles.backButton} onPress={handleBack}>
|
||||||
|
<ChevronLeft width={24} height={24} />
|
||||||
|
</TouchableOpacity>
|
||||||
|
)}
|
||||||
|
<Text style={styles.title}>{title}</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Right side */}
|
||||||
|
{rightComponent && (
|
||||||
|
<View style={styles.rightContainer}>{rightComponent}</View>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Header;
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
container: {
|
||||||
|
width: "100%",
|
||||||
|
height: 56,
|
||||||
|
backgroundColor: "#F3F5F8",
|
||||||
|
flexDirection: "row",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
paddingHorizontal: 8,
|
||||||
|
},
|
||||||
|
leftContainer: {
|
||||||
|
flexDirection: "row",
|
||||||
|
alignItems: "center",
|
||||||
|
},
|
||||||
|
backButton: {
|
||||||
|
width: 40,
|
||||||
|
height: 40,
|
||||||
|
justifyContent: "center",
|
||||||
|
alignItems: "center",
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
marginLeft: 8,
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: "600",
|
||||||
|
color: "#252A34",
|
||||||
|
},
|
||||||
|
rightContainer: {
|
||||||
|
flexDirection: "row",
|
||||||
|
alignItems: "center",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,101 @@
|
||||||
|
import React from "react";
|
||||||
|
import { View, Text, StyleSheet, Pressable } from "react-native";
|
||||||
|
import ChevronRight from "../../assets/icons/chevron_rightside.svg";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
title: string;
|
||||||
|
firstText: string;
|
||||||
|
secondText?: string;
|
||||||
|
percentage: number;
|
||||||
|
onPress?: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const ProgressCard: React.FC<Props> = ({
|
||||||
|
title,
|
||||||
|
firstText,
|
||||||
|
secondText = "",
|
||||||
|
percentage,
|
||||||
|
onPress,
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<View style={styles.card}>
|
||||||
|
<View style={styles.topRow}>
|
||||||
|
<View style={styles.textColumn}>
|
||||||
|
<Text style={styles.title}>{title}</Text>
|
||||||
|
<Text style={styles.time}>
|
||||||
|
{firstText}
|
||||||
|
{secondText ? ` / ${secondText}` : ""}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
{onPress && (
|
||||||
|
<Pressable style={styles.iconButton} onPress={onPress}>
|
||||||
|
<ChevronRight />
|
||||||
|
</Pressable>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
<View style={styles.progressContainer}>
|
||||||
|
<View style={styles.progressBar}>
|
||||||
|
<View style={[styles.progressFill, { width: `${percentage}%` }]} />
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
card: {
|
||||||
|
backgroundColor: "#FCFCFC",
|
||||||
|
borderRadius: 10,
|
||||||
|
padding: 16,
|
||||||
|
width: "100%",
|
||||||
|
height: 96,
|
||||||
|
justifyContent: "space-between",
|
||||||
|
},
|
||||||
|
topRow: {
|
||||||
|
flexDirection: "row",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
alignItems: "flex-start",
|
||||||
|
},
|
||||||
|
textColumn: {
|
||||||
|
flexDirection: "column",
|
||||||
|
gap: 4,
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
fontSize: 12,
|
||||||
|
fontFamily: "Inter-Medium",
|
||||||
|
color: "#565E70",
|
||||||
|
lineHeight: 16,
|
||||||
|
},
|
||||||
|
time: {
|
||||||
|
fontSize: 14,
|
||||||
|
fontFamily: "Inter-SemiBold",
|
||||||
|
color: "#252A34",
|
||||||
|
lineHeight: 20,
|
||||||
|
},
|
||||||
|
iconButton: {
|
||||||
|
width: 40,
|
||||||
|
height: 40,
|
||||||
|
padding: 10,
|
||||||
|
justifyContent: "center",
|
||||||
|
alignItems: "center",
|
||||||
|
},
|
||||||
|
progressContainer: {
|
||||||
|
marginTop: 8,
|
||||||
|
width: "100%",
|
||||||
|
height: 8,
|
||||||
|
},
|
||||||
|
progressBar: {
|
||||||
|
width: "100%",
|
||||||
|
height: 8,
|
||||||
|
backgroundColor: "#E5E9F0",
|
||||||
|
borderRadius: 10,
|
||||||
|
overflow: "hidden",
|
||||||
|
},
|
||||||
|
progressFill: {
|
||||||
|
height: "100%",
|
||||||
|
backgroundColor: "#027A48",
|
||||||
|
borderRadius: 10,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default ProgressCard;
|
||||||
|
|
@ -13,6 +13,7 @@ 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 BASE_URL = "https://46fa2cacfc37.ngrok-free.app";
|
||||||
|
|
||||||
// const SERVER_URL =
|
// const SERVER_URL =
|
||||||
// "http://dev.vec-tr.ai:8089/?dashboardId=deviceDashboardSocket&assetId=V16000868651064644504";
|
// "http://dev.vec-tr.ai:8089/?dashboardId=deviceDashboardSocket&assetId=V16000868651064644504";
|
||||||
|
|
@ -176,3 +177,7 @@ export const issueConfig = [
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
export const payments = {
|
||||||
|
MIN_AMOUNT: 200,
|
||||||
|
};
|
||||||
|
|
|
||||||
|
|
@ -21,10 +21,13 @@
|
||||||
"expo-checkbox": "^4.1.4",
|
"expo-checkbox": "^4.1.4",
|
||||||
"expo-constants": "~17.1.6",
|
"expo-constants": "~17.1.6",
|
||||||
"expo-font": "~13.3.1",
|
"expo-font": "~13.3.1",
|
||||||
|
"expo-image": "^2.4.0",
|
||||||
"expo-image-picker": "~16.1.4",
|
"expo-image-picker": "~16.1.4",
|
||||||
"expo-linking": "~7.1.5",
|
"expo-linking": "~7.1.5",
|
||||||
|
"expo-media-library": "^17.1.7",
|
||||||
"expo-router": "~5.1.1",
|
"expo-router": "~5.1.1",
|
||||||
"expo-secure-store": "^14.2.3",
|
"expo-secure-store": "^14.2.3",
|
||||||
|
"expo-sharing": "^13.1.5",
|
||||||
"expo-splash-screen": "~0.30.9",
|
"expo-splash-screen": "~0.30.9",
|
||||||
"expo-status-bar": "~2.2.3",
|
"expo-status-bar": "~2.2.3",
|
||||||
"expo-system-ui": "~5.0.9",
|
"expo-system-ui": "~5.0.9",
|
||||||
|
|
@ -7095,6 +7098,23 @@
|
||||||
"react": "*"
|
"react": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/expo-image": {
|
||||||
|
"version": "2.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/expo-image/-/expo-image-2.4.0.tgz",
|
||||||
|
"integrity": "sha512-TQ/LvrtJ9JBr+Tf198CAqflxcvdhuj7P24n0LQ1jHaWIVA7Z+zYKbYHnSMPSDMul/y0U46Z5bFLbiZiSidgcNw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"expo": "*",
|
||||||
|
"react": "*",
|
||||||
|
"react-native": "*",
|
||||||
|
"react-native-web": "*"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"react-native-web": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/expo-image-loader": {
|
"node_modules/expo-image-loader": {
|
||||||
"version": "5.1.0",
|
"version": "5.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/expo-image-loader/-/expo-image-loader-5.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/expo-image-loader/-/expo-image-loader-5.1.0.tgz",
|
||||||
|
|
@ -7140,6 +7160,16 @@
|
||||||
"react-native": "*"
|
"react-native": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/expo-media-library": {
|
||||||
|
"version": "17.1.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/expo-media-library/-/expo-media-library-17.1.7.tgz",
|
||||||
|
"integrity": "sha512-hLCoMvlhjtt+iYxPe71P1F6t06mYGysuNOfjQzDbbf64PCkglCZJYmywPyUSV1V5Hu9DhRj//gEg+Ki+7VWXog==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"expo": "*",
|
||||||
|
"react-native": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/expo-modules-autolinking": {
|
"node_modules/expo-modules-autolinking": {
|
||||||
"version": "2.1.12",
|
"version": "2.1.12",
|
||||||
"resolved": "https://registry.npmjs.org/expo-modules-autolinking/-/expo-modules-autolinking-2.1.12.tgz",
|
"resolved": "https://registry.npmjs.org/expo-modules-autolinking/-/expo-modules-autolinking-2.1.12.tgz",
|
||||||
|
|
@ -7230,6 +7260,15 @@
|
||||||
"expo": "*"
|
"expo": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/expo-sharing": {
|
||||||
|
"version": "13.1.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/expo-sharing/-/expo-sharing-13.1.5.tgz",
|
||||||
|
"integrity": "sha512-X/5sAEiWXL2kdoGE3NO5KmbfcmaCWuWVZXHu8OQef7Yig4ZgHFkGD11HKJ5KqDrDg+SRZe4ISd6MxE7vGUgm4w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"expo": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/expo-splash-screen": {
|
"node_modules/expo-splash-screen": {
|
||||||
"version": "0.30.9",
|
"version": "0.30.9",
|
||||||
"resolved": "https://registry.npmjs.org/expo-splash-screen/-/expo-splash-screen-0.30.9.tgz",
|
"resolved": "https://registry.npmjs.org/expo-splash-screen/-/expo-splash-screen-0.30.9.tgz",
|
||||||
|
|
|
||||||
|
|
@ -26,10 +26,13 @@
|
||||||
"expo-checkbox": "^4.1.4",
|
"expo-checkbox": "^4.1.4",
|
||||||
"expo-constants": "~17.1.6",
|
"expo-constants": "~17.1.6",
|
||||||
"expo-font": "~13.3.1",
|
"expo-font": "~13.3.1",
|
||||||
|
"expo-image": "^2.4.0",
|
||||||
"expo-image-picker": "~16.1.4",
|
"expo-image-picker": "~16.1.4",
|
||||||
"expo-linking": "~7.1.5",
|
"expo-linking": "~7.1.5",
|
||||||
|
"expo-media-library": "^17.1.7",
|
||||||
"expo-router": "~5.1.1",
|
"expo-router": "~5.1.1",
|
||||||
"expo-secure-store": "^14.2.3",
|
"expo-secure-store": "^14.2.3",
|
||||||
|
"expo-sharing": "^13.1.5",
|
||||||
"expo-splash-screen": "~0.30.9",
|
"expo-splash-screen": "~0.30.9",
|
||||||
"expo-status-bar": "~2.2.3",
|
"expo-status-bar": "~2.2.3",
|
||||||
"expo-system-ui": "~5.0.9",
|
"expo-system-ui": "~5.0.9",
|
||||||
|
|
|
||||||
|
|
@ -11,18 +11,43 @@ const api = axios.create({
|
||||||
// Request interceptor to add auth token
|
// Request interceptor to add auth token
|
||||||
api.interceptors.request.use(async (config) => {
|
api.interceptors.request.use(async (config) => {
|
||||||
const token = await AsyncStorage.getItem(STORAGE_KEYS.AUTH_TOKEN);
|
const token = await AsyncStorage.getItem(STORAGE_KEYS.AUTH_TOKEN);
|
||||||
|
|
||||||
|
// Debug log for request
|
||||||
|
console.log("🚀 [API Request]");
|
||||||
|
console.log("➡️ Full URL:", `${config.baseURL || ""}${config.url}`);
|
||||||
|
console.log("➡️ Method:", config.method?.toUpperCase());
|
||||||
|
console.log("➡️ Headers (before token):", config.headers);
|
||||||
|
console.log("➡️ Body/Data:", config.data);
|
||||||
|
|
||||||
if (token) {
|
if (token) {
|
||||||
config.headers.Authorization = `Bearer ${token}`;
|
config.headers.Authorization = `Bearer ${token}`;
|
||||||
|
console.log("✅ Auth token attached");
|
||||||
|
} else {
|
||||||
|
console.log("⚠️ No auth token found");
|
||||||
}
|
}
|
||||||
|
|
||||||
return config;
|
return config;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Response interceptor to handle errors
|
// Response interceptor to handle errors
|
||||||
api.interceptors.response.use(
|
api.interceptors.response.use(
|
||||||
(response) => response,
|
(response) => {
|
||||||
|
// Debug log for successful response
|
||||||
|
console.log("✅ [API Response]");
|
||||||
|
console.log("⬅️ Status:", response.status);
|
||||||
|
console.log("⬅️ Data:", response.data);
|
||||||
|
return response;
|
||||||
|
},
|
||||||
async (error) => {
|
async (error) => {
|
||||||
const status = error.response?.status;
|
const status = error.response?.status;
|
||||||
//if token is expired or not present, clear it from storage
|
|
||||||
|
// Debug log for error
|
||||||
|
console.log("❌ [API Error]");
|
||||||
|
console.log("URL:", error.config?.url);
|
||||||
|
console.log("Status:", status);
|
||||||
|
console.log("Response data:", error.response?.data);
|
||||||
|
|
||||||
|
// If token is expired or not present
|
||||||
if (status === 401 || status === 403) {
|
if (status === 401 || status === 403) {
|
||||||
console.log("Token expired or not present");
|
console.log("Token expired or not present");
|
||||||
await AsyncStorage.removeItem(STORAGE_KEYS.AUTH_TOKEN);
|
await AsyncStorage.removeItem(STORAGE_KEYS.AUTH_TOKEN);
|
||||||
|
|
|
||||||
|
|
@ -1,152 +1,61 @@
|
||||||
import { BASE_URL } from "@/constants/config";
|
// store/emiSlice.ts
|
||||||
import api from "@/services/axiosClient";
|
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
|
||||||
import { toCamel } from "@/utils/Payments";
|
import { MyPlan } from "@/app/(tabs)/payments";
|
||||||
import { createSlice, PayloadAction, createAsyncThunk } from "@reduxjs/toolkit";
|
|
||||||
import { AxiosError } from "axios";
|
|
||||||
|
|
||||||
type LastEmiStatus = "pending" | "completed";
|
// Define the payment order interface based on your API response
|
||||||
|
interface PaymentOrder {
|
||||||
export interface MyPlan {
|
amount: number;
|
||||||
noOfEmi: number | null;
|
expiry_date: string;
|
||||||
totalAmount: number | null;
|
order_id: string;
|
||||||
downPayment: number | null;
|
ppayment_link: string;
|
||||||
emiAmount: number | null;
|
qr_code_url: string;
|
||||||
totalEmi: number | null;
|
status: string;
|
||||||
installmentPaid: number | null;
|
transaction_id: string;
|
||||||
currentAmount: number | null;
|
upi_handle: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface EmiDetails {
|
interface EmiState {
|
||||||
dueAmount: number | null;
|
due_amount: number | null;
|
||||||
totalAmountPaidInCurrentCycle: number | null;
|
myPlan: MyPlan | null;
|
||||||
dueDate: string | null;
|
paymentOrder: PaymentOrder | null;
|
||||||
status: LastEmiStatus | null;
|
|
||||||
advanceBalance: number | null;
|
|
||||||
pendingCycles: number | null;
|
|
||||||
totalPendingInstallments: number | null;
|
|
||||||
myPlan: MyPlan;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type LoadingState = "idle" | "pending" | "succeeded" | "failed";
|
const initialState: EmiState = {
|
||||||
|
due_amount: null,
|
||||||
export interface EmiDetailsState {
|
myPlan: null,
|
||||||
item?: EmiDetails;
|
paymentOrder: null,
|
||||||
loading: LoadingState;
|
|
||||||
error?: string;
|
|
||||||
lastFetchedAt?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
const initialState: EmiDetailsState = {
|
|
||||||
item: undefined,
|
|
||||||
loading: "idle",
|
|
||||||
error: undefined,
|
|
||||||
lastFetchedAt: undefined,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface MyPlanApi {
|
const emiSlice = createSlice({
|
||||||
no_of_emi: number | null;
|
name: "emi",
|
||||||
total_amount: number | null;
|
|
||||||
down_payment: number | null;
|
|
||||||
emi_amount: number | null;
|
|
||||||
total_emi: number | null;
|
|
||||||
installment_paid: number | null;
|
|
||||||
current_amount: number | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface EmiDetailsApi {
|
|
||||||
due_amount: number | null;
|
|
||||||
total_amount_paid_in_current_cycle: number | null;
|
|
||||||
due_date: string | null;
|
|
||||||
status: LastEmiStatus | null;
|
|
||||||
advance_balance: number | null;
|
|
||||||
pending_cycles: number | null;
|
|
||||||
total_pending_installments: number | null;
|
|
||||||
myPlain: MyPlanApi;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface EmiDetailsResponse {
|
|
||||||
success: boolean;
|
|
||||||
data: EmiDetailsApi[];
|
|
||||||
}
|
|
||||||
|
|
||||||
const mapEmiDetailsApiToModel = (apiObj: EmiDetailsApi): EmiDetails => ({
|
|
||||||
dueAmount: apiObj.due_amount,
|
|
||||||
totalAmountPaidInCurrentCycle: apiObj.total_amount_paid_in_current_cycle,
|
|
||||||
dueDate: apiObj.due_date,
|
|
||||||
status: apiObj.status,
|
|
||||||
advanceBalance: apiObj.advance_balance,
|
|
||||||
pendingCycles: apiObj.pending_cycles,
|
|
||||||
totalPendingInstallments: apiObj.total_pending_installments,
|
|
||||||
myPlan: {
|
|
||||||
noOfEmi: apiObj.myPlain.no_of_emi,
|
|
||||||
totalAmount: apiObj.myPlain.total_amount,
|
|
||||||
downPayment: apiObj.myPlain.down_payment,
|
|
||||||
emiAmount: apiObj.myPlain.emi_amount,
|
|
||||||
totalEmi: apiObj.myPlain.total_emi,
|
|
||||||
installmentPaid: apiObj.myPlain.installment_paid,
|
|
||||||
currentAmount: apiObj.myPlain.current_amount,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export const fetchEmiDetails = createAsyncThunk<
|
|
||||||
EmiDetails,
|
|
||||||
void,
|
|
||||||
{ rejectValue: string }
|
|
||||||
>("emiDetails/fetch", async (_: void, { rejectWithValue }) => {
|
|
||||||
try {
|
|
||||||
const res = await api.get(`${BASE_URL}}/api/v1/emi-details`, {
|
|
||||||
method: "GET",
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
});
|
|
||||||
|
|
||||||
const json = res.data;
|
|
||||||
|
|
||||||
const first = json?.data?.[0];
|
|
||||||
if (!json?.success || !first) {
|
|
||||||
return rejectWithValue("No EMI details found");
|
|
||||||
}
|
|
||||||
|
|
||||||
return mapEmiDetailsApiToModel(first);
|
|
||||||
} catch (e: any) {
|
|
||||||
const err = e as AxiosError<any>;
|
|
||||||
if (err.code === "ERR_CANCELED") return rejectWithValue("Request aborted");
|
|
||||||
const msg =
|
|
||||||
err.response?.data?.message || err.message || "Something went wrong";
|
|
||||||
return rejectWithValue(msg);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const emiDetailsSlice = createSlice({
|
|
||||||
name: "emiDetails",
|
|
||||||
initialState,
|
initialState,
|
||||||
reducers: {
|
reducers: {
|
||||||
clearEmiDetails(state) {
|
setDueAmount(state, action: PayloadAction<number | null>) {
|
||||||
state.item = undefined;
|
state.due_amount = action.payload;
|
||||||
state.error = undefined;
|
|
||||||
state.loading = "idle";
|
|
||||||
state.lastFetchedAt = undefined;
|
|
||||||
},
|
},
|
||||||
|
setMyPlan(state, action: PayloadAction<MyPlan | null>) {
|
||||||
|
state.myPlan = action.payload;
|
||||||
},
|
},
|
||||||
extraReducers: (builder) => {
|
setPaymentOrder(state, action: PayloadAction<PaymentOrder | null>) {
|
||||||
builder
|
state.paymentOrder = action.payload;
|
||||||
.addCase(fetchEmiDetails.pending, (state) => {
|
},
|
||||||
state.loading = "pending";
|
clearPaymentOrder(state) {
|
||||||
state.error = undefined;
|
state.paymentOrder = null;
|
||||||
})
|
},
|
||||||
.addCase(
|
updatePaymentStatus(state, action: PayloadAction<string>) {
|
||||||
fetchEmiDetails.fulfilled,
|
if (state.paymentOrder) {
|
||||||
(state, action: PayloadAction<EmiDetails>) => {
|
state.paymentOrder.status = action.payload;
|
||||||
state.loading = "succeeded";
|
|
||||||
state.item = action.payload;
|
|
||||||
state.lastFetchedAt = Date.now();
|
|
||||||
}
|
}
|
||||||
)
|
},
|
||||||
.addCase(fetchEmiDetails.rejected, (state, action) => {
|
|
||||||
state.loading = "failed";
|
|
||||||
state.error = action.payload as string | undefined;
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const { clearEmiDetails } = emiDetailsSlice.actions;
|
export const {
|
||||||
export default emiDetailsSlice.reducer;
|
setDueAmount,
|
||||||
|
setMyPlan,
|
||||||
|
setPaymentOrder,
|
||||||
|
clearPaymentOrder,
|
||||||
|
updatePaymentStatus,
|
||||||
|
} = emiSlice.actions;
|
||||||
|
|
||||||
|
export default emiSlice.reducer;
|
||||||
|
|
|
||||||
|
|
@ -51,6 +51,7 @@ export const getUserDetails = createAsyncThunk<UserData>(
|
||||||
async (_, { rejectWithValue }) => {
|
async (_, { rejectWithValue }) => {
|
||||||
try {
|
try {
|
||||||
console.log("Fetching user details from API...");
|
console.log("Fetching user details from API...");
|
||||||
|
console.log("BASE_URL", BASE_URL);
|
||||||
const response = await api.get(`${BASE_URL}/api/v1/get-user-details`);
|
const response = await api.get(`${BASE_URL}/api/v1/get-user-details`);
|
||||||
if (response.data.success) {
|
if (response.data.success) {
|
||||||
console.log("User details fetched successfully:", response.data.data);
|
console.log("User details fetched successfully:", response.data.data);
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue