540 lines
16 KiB
TypeScript
540 lines
16 KiB
TypeScript
import React, { useEffect, useState } from "react";
|
|
import {
|
|
View,
|
|
Text,
|
|
StyleSheet,
|
|
TouchableOpacity,
|
|
TextInput,
|
|
ScrollView,
|
|
Keyboard,
|
|
} 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 { useFocusEffect, useRouter } from "expo-router";
|
|
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 SelectAmountScreen = () => {
|
|
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(t("payment.amount-is-required"))
|
|
.test("valid-number", "Please enter a valid amount", (value) => {
|
|
const numValue = parseFloat(value);
|
|
return !isNaN(numValue) && numValue > 0;
|
|
})
|
|
.test(
|
|
"min-amount",
|
|
`${t("payment.minimum")}: ₹${payments.MIN_AMOUNT}`,
|
|
(value) => {
|
|
const numValue = parseFloat(value);
|
|
return !isNaN(numValue) && numValue >= payments.MIN_AMOUNT;
|
|
}
|
|
),
|
|
otherwise: (schema) => schema.notRequired(),
|
|
}),
|
|
});
|
|
|
|
const dueAmount = useSelector(
|
|
(state: RootState) => state.payments?.due_amount || 0
|
|
);
|
|
|
|
const { showSnackbar } = useSnackbar();
|
|
|
|
const router = useRouter();
|
|
const [isFetching, setIsFetching] = useState<boolean>(false);
|
|
|
|
const { registerTransaction } = useSocket();
|
|
|
|
const quickAmounts = [50, 100, 500, 1000];
|
|
|
|
const initialValues = {
|
|
paymentType: "due",
|
|
customAmount: "",
|
|
};
|
|
|
|
const existingPaymentOrder = useSelector(
|
|
(state: RootState) => state.payments?.paymentOrder
|
|
);
|
|
|
|
const dispatch = useDispatch();
|
|
|
|
const insets = useSafeAreaInsets();
|
|
|
|
const [keyboardVisible, setKeyboardVisible] = useState(false);
|
|
|
|
useEffect(() => {
|
|
const showSub = Keyboard.addListener("keyboardDidShow", () =>
|
|
setKeyboardVisible(true)
|
|
);
|
|
const hideSub = Keyboard.addListener("keyboardDidHide", () =>
|
|
setKeyboardVisible(false)
|
|
);
|
|
|
|
return () => {
|
|
showSub.remove();
|
|
hideSub.remove();
|
|
};
|
|
}, []);
|
|
|
|
const { t } = useTranslation();
|
|
|
|
useFocusEffect(
|
|
React.useCallback(() => {
|
|
console.log(
|
|
"SelectAmountScreen focused - clearing paymentOrder and remounting"
|
|
);
|
|
// Clear the payment order
|
|
dispatch(setPaymentOrder(null));
|
|
setIsFetching(false);
|
|
}, [dispatch])
|
|
);
|
|
|
|
const handleSubmit = async (values: any) => {
|
|
setIsFetching(true);
|
|
const paymentAmount =
|
|
values.paymentType === "due"
|
|
? dueAmount
|
|
: parseFloat(values.customAmount);
|
|
|
|
try {
|
|
let orderData = existingPaymentOrder;
|
|
|
|
if (
|
|
existingPaymentOrder &&
|
|
existingPaymentOrder.amount === paymentAmount
|
|
) {
|
|
console.log(
|
|
"Order for current amount already exists, using existing order"
|
|
);
|
|
orderData = existingPaymentOrder;
|
|
} else {
|
|
console.log("Creating new order for amount:", paymentAmount);
|
|
const res = await api.post(`/api/v1/create-order`, {
|
|
amount: paymentAmount,
|
|
});
|
|
|
|
console.log(res.data, "response from select amount");
|
|
|
|
if (res.data && res.data.success) {
|
|
orderData = res.data.data;
|
|
dispatch(setPaymentOrder(orderData));
|
|
} else {
|
|
throw new Error("Failed to create order");
|
|
}
|
|
}
|
|
|
|
try {
|
|
await registerTransaction(orderData?.transaction_id);
|
|
console.log("Transaction registered successfully");
|
|
|
|
router.push("/payments/payEmi");
|
|
} catch (socketError) {
|
|
console.error("Socket connection failed:", socketError);
|
|
throw socketError;
|
|
}
|
|
} catch (err) {
|
|
console.error(err, "Error in creating order.");
|
|
showSnackbar(`${t("common.something-went-wrong")}`, "error");
|
|
} finally {
|
|
setIsFetching(false);
|
|
}
|
|
console.log("Payment Amount:", paymentAmount);
|
|
};
|
|
|
|
return (
|
|
<>
|
|
<Formik
|
|
initialValues={initialValues}
|
|
validationSchema={validationSchema}
|
|
onSubmit={handleSubmit}
|
|
// Add validateOnChange to make validation more responsive
|
|
validateOnChange={true}
|
|
validateOnBlur={true}
|
|
>
|
|
{({
|
|
values,
|
|
errors,
|
|
touched,
|
|
handleBlur,
|
|
handleSubmit,
|
|
setFieldValue,
|
|
}) => {
|
|
const handleQuickAmountPress = (amount: number) => {
|
|
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());
|
|
};
|
|
|
|
const handleCustomAmountChange = (text: string) => {
|
|
let numericText = text.replace(/[^0-9.]/g, "");
|
|
const parts = numericText.split(".");
|
|
|
|
// Only allow one decimal
|
|
if (parts.length > 2) {
|
|
numericText = parts[0] + "." + parts[1];
|
|
}
|
|
|
|
// Only allow two digits after decimal
|
|
if (parts[1]?.length > 2) {
|
|
numericText = parts[0] + "." + parts[1].slice(0, 2);
|
|
}
|
|
|
|
const numValue = parseFloat(numericText);
|
|
|
|
// If over max, don't update field value — just return early
|
|
if (!isNaN(numValue) && numValue > payments.MAX_AMOUNT) {
|
|
return;
|
|
}
|
|
|
|
setFieldValue("customAmount", numericText, true);
|
|
setFieldValue("paymentType", "custom");
|
|
};
|
|
|
|
const getPaymentAmount = () => {
|
|
if (values.paymentType === "due") {
|
|
return dueAmount;
|
|
}
|
|
return values.customAmount ? parseFloat(values.customAmount) : 0;
|
|
};
|
|
|
|
const paymentAmount = getPaymentAmount() || 0;
|
|
|
|
const isButtonEnabled =
|
|
values.paymentType === "due" ||
|
|
(values.paymentType === "custom" &&
|
|
!isNaN(paymentAmount) &&
|
|
paymentAmount >= payments.MIN_AMOUNT);
|
|
|
|
return (
|
|
<View style={styles.container}>
|
|
<Header
|
|
title={t("payment.select-amount")}
|
|
showBackButton={true}
|
|
/>
|
|
<ScrollView
|
|
style={styles.content}
|
|
showsVerticalScrollIndicator={false}
|
|
contentContainerStyle={styles.scrollContent}
|
|
>
|
|
<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}>
|
|
{t("payment.pay-amount-due")}
|
|
</Text>
|
|
</View>
|
|
<Text style={styles.amountText}>
|
|
{displayValue(dueAmount, formatCurrency)}
|
|
</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}>
|
|
{t("payment.enter-custom-amount")}
|
|
</Text>
|
|
</TouchableOpacity>
|
|
|
|
<View style={styles.inputContainer}>
|
|
<TextInput
|
|
style={[
|
|
styles.textInput,
|
|
touched.customAmount &&
|
|
errors.customAmount &&
|
|
styles.errorInput,
|
|
]}
|
|
value={values.customAmount}
|
|
onChangeText={handleCustomAmountChange}
|
|
onBlur={handleBlur("customAmount")}
|
|
placeholder="₹"
|
|
placeholderTextColor="#94A3B8"
|
|
keyboardType="numeric"
|
|
onFocus={() => setFieldValue("paymentType", "custom")}
|
|
returnKeyType="done"
|
|
/>
|
|
<View style={styles.helperContainer}>
|
|
<Text
|
|
style={[
|
|
styles.helperText,
|
|
touched.customAmount &&
|
|
errors.customAmount &&
|
|
styles.errorText,
|
|
]}
|
|
>
|
|
{touched.customAmount && errors.customAmount
|
|
? errors.customAmount
|
|
: `${t("payment.minimum")}: ₹${
|
|
payments.MIN_AMOUNT
|
|
}`}
|
|
</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>
|
|
<View style={[styles.buttonContainer]}>
|
|
<TouchableOpacity
|
|
style={[
|
|
styles.payButton,
|
|
!isButtonEnabled && styles.disabledButton,
|
|
]}
|
|
onPress={() => handleSubmit()}
|
|
disabled={!isButtonEnabled}
|
|
>
|
|
{getPaymentAmount() < payments.MIN_AMOUNT ? (
|
|
<Text style={styles.payButtonText}>
|
|
{t("payment.select-amount")}
|
|
</Text>
|
|
) : (
|
|
<Text style={styles.payButtonText}>
|
|
{`${t("payment.pay")}`}{" "}
|
|
{displayValue(getPaymentAmount(), formatCurrency)}
|
|
</Text>
|
|
)}
|
|
</TouchableOpacity>
|
|
</View>
|
|
</ScrollView>
|
|
</View>
|
|
);
|
|
}}
|
|
</Formik>
|
|
<Overlay isUploading={isFetching} />
|
|
</>
|
|
);
|
|
};
|
|
|
|
const styles = StyleSheet.create({
|
|
container: {
|
|
flex: 1,
|
|
backgroundColor: "#F3F4F6",
|
|
},
|
|
content: {
|
|
flex: 1,
|
|
paddingHorizontal: 16,
|
|
},
|
|
// Add scroll content style for better keyboard handling
|
|
scrollContent: {
|
|
paddingBottom: 20,
|
|
},
|
|
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;
|