diff --git a/app/(tabs)/payments.tsx b/app/(tabs)/payments.tsx index c97a5ba..3b06388 100644 --- a/app/(tabs)/payments.tsx +++ b/app/(tabs)/payments.tsx @@ -38,6 +38,7 @@ import { displayValue } from "@/utils/Common"; import RefreshIcon from "@/assets/icons/refresh.svg"; import CustomerSupport from "@/components/home/CustomerSupportModal"; import { useTranslation } from "react-i18next"; +import { Image } from "expo-image"; export interface MyPlan { no_of_emi: number; @@ -231,8 +232,16 @@ export default function PaymentsTabScreen() { }, headerTitle: () => ( - {model} - {chasisNumber} + + + + + {model} + {chasisNumber} + ), headerRight: () => ( @@ -270,7 +279,6 @@ export default function PaymentsTabScreen() { // Format currency const formatCurrency = (amount: number) => { - console.log(amount, "amount format current"); return `₹${amount.toLocaleString()}`; }; @@ -590,6 +598,22 @@ const StatusBadge = ({ }; const styles = StyleSheet.create({ + logo: { + width: "100%", + height: "100%", + resizeMode: "contain", + }, + logoContainer: { + padding: 8, + borderRadius: 44 / 2, // make it perfectly round + borderWidth: 2, + borderColor: "#E5E9F0", + width: 44, + height: 44, + alignItems: "center", + justifyContent: "center", + overflow: "hidden", // ensures image doesn't overflow + }, loadingContainer: { padding: 16, alignItems: "center", @@ -611,8 +635,9 @@ const styles = StyleSheet.create({ width: "80%", }, headerTitleContainer: { - flexDirection: "column", + flexDirection: "row", backgroundColor: "#F3F5F8", + gap: 8, }, subtitle: { fontSize: 18, diff --git a/app/payments/Confirmation.tsx b/app/payments/Confirmation.tsx index a7a680f..6e38dff 100644 --- a/app/payments/Confirmation.tsx +++ b/app/payments/Confirmation.tsx @@ -9,6 +9,8 @@ import Pending from "@/assets/icons/pending.svg"; import Failed from "@/assets/icons/cancel.svg"; import { useSafeAreaInsets } from "react-native-safe-area-context"; import { useTranslation } from "react-i18next"; +import { payments } from "@/constants/config"; +import { formatDate } from "@/utils/Payments"; const PaymentConfirmationScreen = () => { const router = useRouter(); @@ -22,35 +24,11 @@ const PaymentConfirmationScreen = () => { const paymentStatus = paymentOrder?.status || "confirmed"; // Format amount with currency - const formatAmount = (amount: number | null) => { + const formatAmount = (amount: number | null | undefined) => { 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", - }); - }; - // Get icon and text based on status const getStatusDisplay = () => { switch (paymentStatus) { @@ -95,15 +73,15 @@ const PaymentConfirmationScreen = () => { + {statusDisplay.text} {statusDisplay.icon} - {formatAmount(paymentOrder?.amount || due_amount)} + {formatAmount(paymentOrder?.amount)} - + Payment to {payments.AMOUNT_PAID_TO} - {statusDisplay.text} {formatDate(paymentOrder?.transaction_date)} @@ -200,7 +178,6 @@ const styles = StyleSheet.create({ backgroundColor: "#fcfcfc", borderRadius: 8, padding: 16, - paddingVertical: 32, }, paymentStatusContainer: { alignItems: "center", @@ -220,6 +197,13 @@ const styles = StyleSheet.create({ successIcon: { // Icon styling handled by Ionicons }, + paidTo: { + fontSize: 12, + fontWeight: "700", + color: "#253342", + textAlign: "center", + lineHeight: 20, + }, amountText: { fontSize: 20, fontWeight: "600", diff --git a/app/payments/TransactionDetails.tsx b/app/payments/TransactionDetails.tsx index 021c600..81c302b 100644 --- a/app/payments/TransactionDetails.tsx +++ b/app/payments/TransactionDetails.tsx @@ -9,16 +9,15 @@ import { import { useNavigation, useRoute } from "@react-navigation/native"; import { useRouter } from "expo-router"; import api from "@/services/axiosClient"; -import { BASE_URL } from "@/constants/config"; +import { BASE_URL, payments } from "@/constants/config"; import { useSnackbar } from "@/contexts/Snackbar"; -import { SafeAreaView } from "react-native-safe-area-context"; -// Import your success/failure icons import SuccessIcon from "@/assets/icons/check_circle.svg"; import FailureIcon from "@/assets/icons/cancel.svg"; import PendingIcon from "@/assets/icons/pending.svg"; import Header from "@/components/common/Header"; import { useTranslation } from "react-i18next"; +import { formatDate } from "@/utils/Payments"; interface TransactionDetailData { id: number; @@ -152,18 +151,18 @@ export default function TransactionDetailScreen() { ) : ( + + {getStatusText(transactionData.status)} + {getStatusIcon(transactionData.status)} {formatCurrency(transactionData.amount)} - - - {getStatusText(transactionData.status)} - + Payment to {payments.AMOUNT_PAID_TO} - {formatDateTime(transactionData.transaction_date)} + {formatDate(transactionData?.transaction_date)} @@ -305,7 +304,6 @@ const styles = StyleSheet.create({ fontSize: 14, fontWeight: "400", color: "#252A34", - marginBottom: 4, }, dateTime: { fontSize: 14, diff --git a/app/payments/payEmi.tsx b/app/payments/payEmi.tsx index c5c7c9f..f55e0fe 100644 --- a/app/payments/payEmi.tsx +++ b/app/payments/payEmi.tsx @@ -27,6 +27,7 @@ import { payments } from "@/constants/config"; import { useFocusEffect, useRouter } from "expo-router"; import { useSocket } from "@/contexts/SocketContext"; import { useTranslation } from "react-i18next"; +import CancelPayment from "@/components/Payments/CancelPaymentModal"; const { height: screenHeight } = Dimensions.get("window"); @@ -34,9 +35,6 @@ const UpiPaymentScreen = () => { const dispatch = useDispatch(); //paymentorder.amount is undefined console.log("inside payemi ✅✅"); - useEffect(() => { - console.log("inside pay emi useeffect 🔥🔥🔥🔥🔥"); - }, []); const paymentOrder = useSelector( (state: RootState) => state.payments.paymentOrder ); @@ -46,36 +44,36 @@ const UpiPaymentScreen = () => { const { showSnackbar } = useSnackbar(); const insets = useSafeAreaInsets(); + const [isCancelModalVisible, setIsCancelModalVisible] = + useState(false); + + function toggleCancelModal() { + setIsCancelModalVisible(!isCancelModalVisible); + } + + function handleCancelPayment() { + offPaymentConfirmation(); + disconnect(); + router.back(); + } + useFocusEffect( React.useCallback(() => { - let backPressCount = 0; let backPressTimer: NodeJS.Timeout | null = null; const backAction = () => { - if (backPressCount === 0) { - backPressCount++; - showSnackbar("Press back again to cancel payment", "info"); - backPressTimer = setTimeout(() => { - backPressCount = 0; - }, 2000); - return true; - } else { - if (backPressTimer) clearTimeout(backPressTimer); - offPaymentConfirmation(); - disconnect(); - router.back(); - return true; - } + setIsCancelModalVisible(true); + return true; }; const backHandler = BackHandler.addEventListener( "hardwareBackPress", backAction ); - // ✅ Cleanup when screen loses focus + return () => { if (backPressTimer) clearTimeout(backPressTimer); backHandler.remove(); }; - }, [offPaymentConfirmation, disconnect, router]) + }, []) ); useFocusEffect( @@ -99,7 +97,7 @@ const UpiPaymentScreen = () => { ]) ); - const formatAmount = (amount: number): string => { + const formatAmount = (amount: number | null | undefined): string => { if (amount == null || amount == undefined) return `₹${0}`; return `₹${amount.toLocaleString("en-IN")}`; }; @@ -143,18 +141,28 @@ const UpiPaymentScreen = () => { const shareQR = async (): Promise => { try { + const fileUri = + FileSystem.documentDirectory + `qr-${paymentOrder?.order_id}.png`; + + // Download QR code image to local file + const downloadResult = await FileSystem.downloadAsync( + paymentOrder?.qr_code_url, + fileUri + ); + + // Share using expo-sharing if (await Sharing.isAvailableAsync()) { - await Share.share({ - message: `Pay ${formatAmount( - paymentOrder.amount - )} using this QR code`, - url: paymentOrder.qr_code_url, + await Sharing.shareAsync(downloadResult.uri, { + mimeType: "image/png", + dialogTitle: "Share QR Code", + UTI: "public.png", }); } else { - Alert.alert("Error", "Sharing is not available on this device"); + showSnackbar("Sharing is not available on this device.", "error"); } } catch (error) { - Alert.alert("Error", "Failed to share QR code"); + console.error("Error sharing QR code:", error); + showSnackbar("Failed to share QR code", "error"); } }; @@ -190,74 +198,82 @@ const UpiPaymentScreen = () => { const { t } = useTranslation(); return ( - -
- - - - - - {t("payment.amount-to-be-paid")} - - - {formatAmount(paymentOrder?.amount)} - - - - - - - - - - {t("payment.share-qr")} + <> + +
+ + + + + + {t("payment.amount-to-be-paid")} - + {payments.AMOUNT_PAID_TO} + + {formatAmount(paymentOrder?.amount)} + + + + + + + + + + {t("payment.share-qr")} + + + + + + {t("payment.download-qr")} + + + - - - {t("payment.download-qr")} + + {t("payment.pay-using-upi")} - - - {t("payment.pay-using-upi")} - - - - - - {t("payment.confirm-payment")} - - handlePaymentDone()} - style={styles.paymentDone} - > - {t("payment.payment-done")} - + + + {t("payment.confirm-payment")} + + handlePaymentDone()} + style={styles.paymentDone} + > + {t("payment.payment-done")} + + - - - + + + + ); }; @@ -305,6 +321,7 @@ const styles = StyleSheet.create({ borderRadius: 4, }, confirmTitle: { + padding: 2, fontWeight: "400", fontSize: 14, lineHeight: 14, @@ -346,6 +363,13 @@ const styles = StyleSheet.create({ textAlign: "center", marginBottom: 4, }, + paidTo: { + fontSize: 12, + fontWeight: "700", + color: "#253342", + textAlign: "center", + lineHeight: 20, + }, amount: { fontSize: 20, fontWeight: "600", diff --git a/app/payments/selectAmount.tsx b/app/payments/selectAmount.tsx index 9e5be81..80cb552 100644 --- a/app/payments/selectAmount.tsx +++ b/app/payments/selectAmount.tsx @@ -23,6 +23,7 @@ import { useSocket } from "@/contexts/SocketContext"; import { useSnackbar } from "@/contexts/Snackbar"; import { useSafeAreaInsets } from "react-native-safe-area-context"; import { useTranslation } from "react-i18next"; +import { displayValue, formatCurrency } from "@/utils/Common"; const validationSchema = Yup.object().shape({ paymentType: Yup.string().required("Please select a payment option"), @@ -169,34 +170,41 @@ const SelectAmountScreen = () => { values, errors, touched, - handleChange, handleBlur, handleSubmit, setFieldValue, - isValid, - validateField, - dirty, }) => { const handleQuickAmountPress = (amount: number) => { - const currentAmount = values.customAmount - ? parseFloat(values.customAmount) - : 0; - const newAmount = currentAmount + amount; + let newAmount = values.customAmount + ? parseFloat(values.customAmount) + amount + : amount; + + if (newAmount > payments.MAX_AMOUNT) { + newAmount = payments.MAX_AMOUNT; + } + + setFieldValue("paymentType", "custom"); setFieldValue("customAmount", newAmount.toString()); - validateField("customAmount"); }; const handleCustomAmountChange = (text: string) => { - const numericText = text.replace(/[^0-9.]/g, ""); + let numericText = text.replace(/[^0-9.]/g, ""); const parts = numericText.split("."); - const formattedText = - parts.length > 2 - ? parts[0] + "." + parts.slice(1).join("") - : numericText; - setFieldValue("customAmount", formattedText, true); + if (parts.length > 2) { + numericText = parts[0] + "." + parts[1]; + } + if (parts[1]?.length > 2) { + numericText = parts[0] + "." + parts[1].slice(0, 2); + } + + const numValue = parseFloat(numericText); + if (!isNaN(numValue) && numValue > payments.MAX_AMOUNT) { + numericText = payments.MAX_AMOUNT.toString(); + } + + setFieldValue("customAmount", numericText, true); setFieldValue("paymentType", "custom"); - validateField("customAmount"); // Trigger validation after change }; const getPaymentAmount = () => { @@ -214,34 +222,15 @@ const SelectAmountScreen = () => { !isNaN(paymentAmount) && paymentAmount >= payments.MIN_AMOUNT); - // Button text logic - let buttonText = ""; - if (values.paymentType === "due") { - buttonText = `Pay ₹${paymentAmount.toFixed(2)}`; - } else { - buttonText = - paymentAmount >= payments.MIN_AMOUNT - ? `Pay ₹${paymentAmount.toFixed(2)}` - : "Select Amount"; - } - return ( - +
- @@ -269,7 +258,7 @@ const SelectAmountScreen = () => { - ₹{dueAmount?.toFixed(2)} + {displayValue(dueAmount, formatCurrency)} @@ -317,7 +306,6 @@ const SelectAmountScreen = () => { placeholderTextColor="#94A3B8" keyboardType="numeric" onFocus={() => setFieldValue("paymentType", "custom")} - // Add return key handling returnKeyType="done" /> @@ -358,34 +346,28 @@ const SelectAmountScreen = () => { )} + + handleSubmit()} + disabled={!isButtonEnabled} + > + {getPaymentAmount() < payments.MIN_AMOUNT ? ( + + {t("payment.select-amount")} + + ) : ( + + Pay {displayValue(getPaymentAmount(), formatCurrency)} + + )} + + - - - handleSubmit()} - disabled={!isButtonEnabled} - > - {getPaymentAmount() < payments.MIN_AMOUNT ? ( - - {t("payment.select-amount")} - - ) : ( - - Pay ₹{getPaymentAmount().toFixed(2)} - - )} - - - + ); }} diff --git a/app/user/edit_name.tsx b/app/user/edit_name.tsx index 02e75f6..171b881 100644 --- a/app/user/edit_name.tsx +++ b/app/user/edit_name.tsx @@ -148,7 +148,6 @@ const styles = StyleSheet.create({ }, formContainer: { flex: 1, - justifyContent: "space-between", }, inputContainer: { marginBottom: 24, diff --git a/components/Payments/CancelPaymentModal.tsx b/components/Payments/CancelPaymentModal.tsx new file mode 100644 index 0000000..f09d25b --- /dev/null +++ b/components/Payments/CancelPaymentModal.tsx @@ -0,0 +1,91 @@ +import BottomSheetModal from "@/components/common/BottomSheetModal"; +import { useTranslation } from "react-i18next"; +import { StyleSheet, Text, TouchableOpacity, View } from "react-native"; + +interface CancelPaymentProps { + visible: boolean; + onClose: () => void; + onCancel: () => void; +} + +export default function CancelPayment({ + visible, + onClose, + onCancel, +}: CancelPaymentProps) { + const { t } = useTranslation(); + return ( + + + {t("payment.do-you-want-to-cancel-payment")} + + + + + {t("payment.cancel-payment")} + + + + + {t("payment.continue-payment")} + + + + + ); +} + +const styles = StyleSheet.create({ + buttonsContainer: { + flexDirection: "row", + justifyContent: "space-between", + paddingVertical: 8, + borderTopWidth: 1, + borderBottomWidth: 1, + borderColor: "#D8DDE7", + }, + bottomButtonText: { + textAlign: "center", + }, + button: { + width: "45%", + height: 44, + paddingHorizontal: 16, + paddingVertical: 8, + borderWidth: 1, + borderColor: "#E5E9F0", + borderRadius: 4, + backgroundColor: "#F3F5F8", + }, + row: { + flexDirection: "row", + gap: 16, + justifyContent: "space-between", + }, + secondaryButton: { + flexDirection: "row", + alignItems: "center", + justifyContent: "center", + paddingVertical: 8, + paddingHorizontal: 16, + height: 40, + borderRadius: 4, + backgroundColor: "#F3F5F8", + borderWidth: 1, + borderColor: "#D8DDE7", + width: 156, + gap: 8, + }, + fullButton: { + width: "100%", + }, + buttonText: { + fontSize: 14, + fontWeight: "500", + color: "#252A34", + }, +}); diff --git a/components/Payments/PaymentHistoryCard.tsx b/components/Payments/PaymentHistoryCard.tsx index 354999d..5b8a5d0 100644 --- a/components/Payments/PaymentHistoryCard.tsx +++ b/components/Payments/PaymentHistoryCard.tsx @@ -124,7 +124,6 @@ const styles = StyleSheet.create({ textAlign: "left", }, paymentAmount: { - width: 69, height: 20, fontFamily: "Inter-SemiBold", fontSize: 14, @@ -140,7 +139,6 @@ const styles = StyleSheet.create({ alignItems: "center", }, paymentDetails: { - width: 237, height: 16, fontFamily: "Inter-Regular", fontSize: 12, @@ -156,7 +154,6 @@ const styles = StyleSheet.create({ alignItems: "center", paddingHorizontal: 8, paddingVertical: 2, - marginLeft: 4, // simulate gap }, statusText: { fontFamily: "Inter-Medium", diff --git a/constants/config.ts b/constants/config.ts index e7a65cc..89f1b3d 100644 --- a/constants/config.ts +++ b/constants/config.ts @@ -185,11 +185,13 @@ export const issueConfig = [ ]; export const payments = { - MIN_AMOUNT: 200, + MIN_AMOUNT: 1, + MAX_AMOUNT: 500000, LINK_EXPIRED: "Payment link expired", SOCKET_CONNECTION_TIMEOUT_IN_SECS: 5, REGISTER_TRANSACTION_EMIT_EVENT_NAME: "register-transaction", REGISTER_TRANSACTION_LISTEN_EVENT_NAME: "registration-ack", PAYMENT_CONFIRMATION_EVENT_NAME: "register-transaction", EMI_WARNING_DAYS_THRESHOLD: 14, + AMOUNT_PAID_TO: "Nav Shakti Lithium Pvt. Ltd.", }; diff --git a/services/i18n/locals/en.json b/services/i18n/locals/en.json index 70122f8..475bc75 100644 --- a/services/i18n/locals/en.json +++ b/services/i18n/locals/en.json @@ -113,7 +113,10 @@ "pay-using-upi": "Pay using UPI App", "confirm-payment": "Confirm once your payment is completed", "payment-done": "Payment Done", - "view-plan": "View Plan" + "view-plan": "View Plan", + "cancel-payment": "Cancel Payment", + "continue-payment": "Continue Payment", + "do-you-want-to-cancel-payment": "Are you sure you want to cancel the payment?" }, "service": { "schedule-maintenance": "Schedule Maintenance", diff --git a/services/i18n/locals/hi.json b/services/i18n/locals/hi.json index fd6c4a6..478c81a 100644 --- a/services/i18n/locals/hi.json +++ b/services/i18n/locals/hi.json @@ -113,7 +113,10 @@ "amount-to-be-paid": "भुगतान की जाने वाली राशि", "pay-using-upi": "UPI ऐप का उपयोग करके भुगतान करें", "confirm-payment": "भुगतान पूरा होने पर पुष्टि करें।", - "payment-done": "भुगतान हो गया" + "payment-done": "भुगतान हो गया", + "cancel-payment": "भुगतान रद्द करें", + "continue-payment": "भुगतान जारी रखें", + "do-you-want-to-cancel-payment": "क्या आप वाकई भुगतान रद्द करना चाहते हैं?" }, "service": { "schedule-maintenance": "शेड्यूल मेंटेनेंस", diff --git a/utils/Common.ts b/utils/Common.ts index cd4bc31..54a2bfd 100644 --- a/utils/Common.ts +++ b/utils/Common.ts @@ -4,3 +4,7 @@ export const displayValue = (value: any, formatter?: (val: any) => string) => { } return formatter ? formatter(value) : value; }; + +export const formatCurrency = (amount: number) => { + return `₹${amount.toLocaleString()}`; +}; diff --git a/utils/Payments.ts b/utils/Payments.ts index e4e9503..ac7ea8c 100644 --- a/utils/Payments.ts +++ b/utils/Payments.ts @@ -10,3 +10,28 @@ export const toCamel = (obj: any): any => { } return obj; }; + +export const formatDate = (dateString?: string | null) => { + if (!dateString) { + return "--"; + } + const date = dateString ? new Date(dateString) : new Date(); + + const optionsDate = { + day: "numeric", + month: "long", + year: "numeric", + } as const; + const optionsTime = { + hour: "numeric", + minute: "2-digit", + hour12: true, + } as const; + const optionsWeekday = { weekday: "long" } as const; + + const formattedDate = date.toLocaleDateString("en-IN", optionsDate); + const formattedTime = date.toLocaleTimeString("en-IN", optionsTime); + const formattedWeekday = date.toLocaleDateString("en-IN", optionsWeekday); + + return `${formattedDate}, ${formattedTime}, ${formattedWeekday}`; +};