BaaS_Driver_Android_App/app/payments/selectAmount.tsx

535 lines
15 KiB
TypeScript

import React, { useEffect, useState } from "react";
import {
View,
Text,
StyleSheet,
TouchableOpacity,
TextInput,
ScrollView,
KeyboardAvoidingView,
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 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("valid-number", "Please enter a valid amount", (value) => {
const numValue = parseFloat(value);
return !isNaN(numValue) && numValue > 0;
})
.test(
"min-amount",
`Minimum amount is ₹${payments.MIN_AMOUNT}`,
(value) => {
const numValue = parseFloat(value);
return !isNaN(numValue) && numValue >= payments.MIN_AMOUNT;
}
),
otherwise: (schema) => schema.notRequired(),
}),
});
const SelectAmountScreen = () => {
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("service.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(".");
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");
};
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}>
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;