Add payment success screen

feature/app-setup
vinay kumar 2025-08-13 17:15:02 +05:30
parent 1ef44669e1
commit b648a485be
5 changed files with 264 additions and 82 deletions

View File

@ -1,23 +1,127 @@
import React from "react"; import React from "react";
import { View, Text, StyleSheet, TouchableOpacity } from "react-native"; import { View, Text, StyleSheet, TouchableOpacity } from "react-native";
import { useRouter } from "expo-router"; import { useRouter } from "expo-router";
import { useSelector } from "react-redux";
import { RootState } from "@/store/rootReducer";
import Header from "@/components/common/Header";
import CheckCircle from "@/assets/icons/check_circle.svg";
import { useSafeAreaInsets } from "react-native-safe-area-context";
const PaymentConfirmationScreen = () => { const PaymentConfirmationScreen = () => {
const router = useRouter(); const router = useRouter();
const { paymentOrder, due_amount } = useSelector(
(state: RootState) => state.payments
);
const insets = useSafeAreaInsets();
// Format amount with currency
const formatAmount = (amount: number | null) => {
if (!amount) return "₹0";
return `${amount.toLocaleString("en-IN")}`;
};
// Format date
const formatDate = (dateString?: string) => {
if (!dateString)
return new Date().toLocaleString("en-IN", {
day: "numeric",
month: "long",
year: "numeric",
hour: "numeric",
minute: "2-digit",
hour12: true,
weekday: "long",
});
return new Date(dateString).toLocaleString("en-IN", {
day: "numeric",
month: "long",
year: "numeric",
hour: "numeric",
minute: "2-digit",
hour12: true,
weekday: "long",
});
};
return ( return (
<View style={styles.container}> <View style={styles.container}>
<Text style={styles.title}>Payment Successful!</Text> <Header title="Payment Status" />
<Text style={styles.message}>
Thank you for your payment. Your transaction has been completed.
</Text>
<TouchableOpacity <View style={styles.contentFrame}>
style={styles.button} <View style={styles.qrFrame}>
// onPress={() => router.push("/dashboard")} // Or wherever you want to go <View style={styles.paymentStatusContainer}>
> <View style={styles.successIconContainer}>
<Text style={styles.buttonText}>Go to Dashboard</Text> <CheckCircle />
</TouchableOpacity> </View>
<Text style={styles.amountText}>
{formatAmount(paymentOrder?.amount || due_amount)}
</Text>
<View style={styles.statusContainer}>
<Text style={styles.statusText}>Payment successful</Text>
<Text style={styles.dateText}>
{formatDate(paymentOrder?.transaction_date)}
</Text>
</View>
</View>
<View style={styles.divider} />
<View style={styles.transactionContainer}>
<Text style={styles.sectionHeader}>Transaction Details</Text>
<View style={styles.detailRow}>
<Text style={styles.detailLabel}>Payment mode</Text>
<Text style={styles.detailValue}>
{paymentOrder?.payment_mode?.[0] || "UPI"}
</Text>
</View>
<View style={styles.detailRow}>
<Text style={styles.detailLabel}>Paid to</Text>
<Text style={styles.detailValue}>
{paymentOrder?.upi_handle || "randomupiid@vecpay"}
</Text>
</View>
<View style={styles.detailRow}>
<Text style={styles.detailLabel}>Paid by</Text>
<Text style={styles.detailValue}>
{paymentOrder?.paid_by_upi_handle || "amar.kesari@vecpay"}
</Text>
</View>
<View style={styles.detailRow}>
<Text style={styles.detailLabel}>Order ID</Text>
<Text style={styles.detailValue}>
{paymentOrder?.order_id || "1000516861984940"}
</Text>
</View>
<View style={styles.detailRow}>
<Text style={styles.detailLabel}>Transaction ID</Text>
<Text style={styles.detailValue}>
{paymentOrder?.transaction_id ||
paymentOrder?.transaction_order_id ||
"1000516861984940"}
</Text>
</View>
<View style={styles.detailRow}>
<Text style={styles.detailLabel}>RRN</Text>
<Text style={styles.detailValue}>
{paymentOrder?.payment_reference_id || "1000516861984940"}
</Text>
</View>
</View>
</View>
<TouchableOpacity
style={[styles.primaryButton, { marginBottom: insets.bottom }]}
onPress={() => router.replace("/(tabs)/payments")}
>
<Text style={styles.buttonText}>OK</Text>
</TouchableOpacity>
</View>
</View> </View>
); );
}; };
@ -27,32 +131,92 @@ export default PaymentConfirmationScreen;
const styles = StyleSheet.create({ const styles = StyleSheet.create({
container: { container: {
flex: 1, flex: 1,
backgroundColor: "#f3f4f6", // Light gray background
},
contentFrame: {
flex: 1,
paddingHorizontal: 16,
paddingBottom: 16,
justifyContent: "space-between",
},
qrFrame: {
backgroundColor: "#fcfcfc",
borderRadius: 8,
padding: 16,
paddingVertical: 32,
},
paymentStatusContainer: {
alignItems: "center",
},
successIconContainer: {
marginBottom: 16,
alignItems: "center",
},
successIcon: {
// Icon styling handled by Ionicons
},
amountText: {
fontSize: 20,
fontWeight: "600",
color: "#252a34",
textAlign: "center",
marginBottom: 16,
},
statusContainer: {
alignItems: "center",
gap: 4,
},
statusText: {
fontSize: 14,
color: "#252a34",
textAlign: "center",
},
dateText: {
fontSize: 14,
color: "#252a34",
textAlign: "center",
},
divider: {
height: 1,
backgroundColor: "#e5e9f0",
marginVertical: 24,
},
transactionContainer: {
gap: 8,
},
sectionHeader: {
fontSize: 14,
fontWeight: "600",
color: "#252a34",
},
detailRow: {
flexDirection: "row",
justifyContent: "space-between",
alignItems: "flex-start",
paddingVertical: 2,
},
detailLabel: {
fontSize: 14,
color: "#252a34",
flex: 1,
},
detailValue: {
fontSize: 14,
fontWeight: "600",
color: "#252a34",
textAlign: "left",
flex: 1,
},
primaryButton: {
backgroundColor: "#008761",
height: 40,
borderRadius: 4,
justifyContent: "center", justifyContent: "center",
alignItems: "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: { buttonText: {
color: "white", color: "#fcfcfc",
fontSize: 16, fontSize: 14,
fontWeight: "600", fontWeight: "500",
}, },
}); });

View File

@ -16,7 +16,7 @@ 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"; import { RootState } from "@/store";
import { updatePaymentStatus } from "@/store/paymentSlice"; import { setPaymentOrder, updatePaymentStatus } from "@/store/paymentSlice";
import Header from "@/components/common/Header"; import Header from "@/components/common/Header";
import ShareIcon from "@/assets/icons/share.svg"; import ShareIcon from "@/assets/icons/share.svg";
import { useSafeAreaInsets } from "react-native-safe-area-context"; import { useSafeAreaInsets } from "react-native-safe-area-context";
@ -34,83 +34,74 @@ const UpiPaymentScreen = () => {
); );
const router = useRouter(); const router = useRouter();
const { onPaymentConfirmation, offPaymentConfirmation } = useSocket(); const { onPaymentConfirmation, offPaymentConfirmation, disconnect } =
const [isListening, setIsListening] = useState(false); useSocket();
const { showSnackbar } = useSnackbar(); const { showSnackbar } = useSnackbar();
const insets = useSafeAreaInsets(); const insets = useSafeAreaInsets();
useEffect(() => { useEffect(() => {
let backPressCount = 0;
let backPressTimer: NodeJS.Timeout | null = null;
const handlePaymentConfirmation = (data: any) => { const handlePaymentConfirmation = (data: any) => {
console.log("Payment confirmation received:", data); console.log("Payment confirmation received:", data);
Alert.alert( dispatch(
"Payment Successful!", setPaymentOrder({
"Your payment has been confirmed successfully.", ...paymentOrder,
[ ...data,
{ })
text: "Continue",
onPress: () => {
router.replace("/payments/Confirmation");
},
},
],
{ cancelable: false }
); );
offPaymentConfirmation();
disconnect();
router.replace("/payments/Confirmation");
}; };
onPaymentConfirmation(handlePaymentConfirmation); onPaymentConfirmation(handlePaymentConfirmation);
setIsListening(true);
const backAction = () => { const backAction = () => {
Alert.alert( if (backPressCount === 0) {
"Cancel Payment?", backPressCount++;
"Are you sure you want to cancel this payment?", console.log("Press back again to cancel payment");
[ showSnackbar("Press back again to cancel payment", "success");
{
text: "No", backPressTimer = setTimeout(() => {
onPress: () => null, backPressCount = 0;
style: "cancel", }, 2000);
},
{ return true;
text: "Yes", } else {
onPress: () => { if (backPressTimer) clearTimeout(backPressTimer);
offPaymentConfirmation(); offPaymentConfirmation();
router.back(); disconnect();
}, router.back();
}, return true;
] }
);
return true;
}; };
const backHandler = BackHandler.addEventListener( const backHandler = BackHandler.addEventListener(
"hardwareBackPress", "hardwareBackPress",
backAction backAction
); );
// Cleanup on unmount
return () => { return () => {
offPaymentConfirmation(); offPaymentConfirmation();
setIsListening(false);
backHandler.remove(); backHandler.remove();
}; };
}, [onPaymentConfirmation, offPaymentConfirmation, router]); }, [onPaymentConfirmation, offPaymentConfirmation, router]);
// 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")}`;
}; };
// Check if payment is expired
const isPaymentExpired = (): boolean => { const isPaymentExpired = (): boolean => {
const expiryDate = new Date(paymentOrder.expiry_date); const expiryDate = new Date(paymentOrder.expiry_date);
const currentDate = new Date(); const currentDate = new Date();
return currentDate > expiryDate; return currentDate > expiryDate;
}; };
// Get formatted expiry time
const getExpiryTime = (): string => { const getExpiryTime = (): string => {
const expiryDate = new Date(paymentOrder.expiry_date); const expiryDate = new Date(paymentOrder.expiry_date);
return expiryDate.toLocaleString("en-IN"); return expiryDate.toLocaleString("en-IN");

View File

@ -140,6 +140,8 @@ const SelectAmountScreen = () => {
initialValues={initialValues} initialValues={initialValues}
validationSchema={validationSchema} validationSchema={validationSchema}
onSubmit={handleSubmit} onSubmit={handleSubmit}
validateOnChange={true}
validateOnBlur={true}
> >
{({ {({
values, values,
@ -169,7 +171,19 @@ const SelectAmountScreen = () => {
const isPayButtonEnabled = () => { const isPayButtonEnabled = () => {
if (values.paymentType === "due") return true; if (values.paymentType === "due") return true;
return isValid && dirty && values.customAmount;
// For custom amount, check if it's valid and meets minimum requirement
if (values.paymentType === "custom") {
const amount = parseFloat(values.customAmount);
return (
values.customAmount &&
!isNaN(amount) &&
amount >= payments.MIN_AMOUNT &&
!errors.customAmount
);
}
return false;
}; };
return ( return (

View File

@ -0,0 +1,8 @@
<svg width="80" height="80" viewBox="0 0 80 80" fill="none" xmlns="http://www.w3.org/2000/svg">
<mask id="mask0_709_4688" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="80" height="80">
<rect width="80" height="80" fill="#D9D9D9"/>
</mask>
<g mask="url(#mask0_709_4688)">
<path d="M35.3327 46L28.166 38.8334C27.5549 38.2222 26.7771 37.9167 25.8327 37.9167C24.8882 37.9167 24.1105 38.2222 23.4993 38.8334C22.8882 39.4445 22.5827 40.2222 22.5827 41.1667C22.5827 42.1111 22.8882 42.8889 23.4993 43.5L32.9993 53C33.666 53.6667 34.4438 54 35.3327 54C36.2216 54 36.9993 53.6667 37.666 53L56.4993 34.1667C57.1105 33.5556 57.416 32.7778 57.416 31.8334C57.416 30.8889 57.1105 30.1111 56.4993 29.5C55.8882 28.8889 55.1105 28.5834 54.166 28.5834C53.2216 28.5834 52.4438 28.8889 51.8327 29.5L35.3327 46ZM39.9993 73.3334C35.3882 73.3334 31.0549 72.4584 26.9993 70.7084C22.9438 68.9584 19.416 66.5834 16.416 63.5834C13.416 60.5834 11.041 57.0556 9.29102 53C7.54102 48.9445 6.66602 44.6111 6.66602 40C6.66602 35.3889 7.54102 31.0556 9.29102 27C11.041 22.9445 13.416 19.4167 16.416 16.4167C19.416 13.4167 22.9438 11.0417 26.9993 9.29169C31.0549 7.54169 35.3882 6.66669 39.9993 6.66669C44.6105 6.66669 48.9438 7.54169 52.9994 9.29169C57.0549 11.0417 60.5827 13.4167 63.5827 16.4167C66.5827 19.4167 68.9577 22.9445 70.7077 27C72.4577 31.0556 73.3327 35.3889 73.3327 40C73.3327 44.6111 72.4577 48.9445 70.7077 53C68.9577 57.0556 66.5827 60.5834 63.5827 63.5834C60.5827 66.5834 57.0549 68.9584 52.9994 70.7084C48.9438 72.4584 44.6105 73.3334 39.9993 73.3334Z" fill="#009E71"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@ -2,15 +2,20 @@
import { createSlice, PayloadAction } from "@reduxjs/toolkit"; import { createSlice, PayloadAction } from "@reduxjs/toolkit";
import { MyPlan } from "@/app/(tabs)/payments"; import { MyPlan } from "@/app/(tabs)/payments";
// Define the payment order interface based on your API response
interface PaymentOrder { interface PaymentOrder {
amount: number; amount: number; // kept as number for math
expiry_date: string; id?: number; // optional if not always present
expiry_date?: string;
order_id: string; order_id: string;
ppayment_link: string; ppayment_link?: string;
qr_code_url: string; qr_code_url?: string;
status: string; status: string; // "confirmed", "pending", etc.
transaction_id: string; transaction_id?: string;
transaction_order_id?: string;
transaction_date?: string;
payment_mode?: string[]; // e.g. ["UPI"]
payment_reference_id?: string;
paid_by_upi_handle?: string;
upi_handle: string; upi_handle: string;
} }