676 lines
28 KiB
React
676 lines
28 KiB
React
import { Form, Formik } from "formik";
|
|
import { useEffect, useState } from "react";
|
|
import { useSelector } from "react-redux";
|
|
import * as Yup from "yup";
|
|
import usersService from "../../../services/UsersService";
|
|
import Icons from "../../Helpers/Icons";
|
|
import InputCom from "../../Helpers/Inputs/InputCom";
|
|
import LoadingSpinner from "../../Spinners/LoadingSpinner";
|
|
|
|
const validationSchema = Yup.object().shape({
|
|
cardNum: Yup.string()
|
|
.min(6, "not a card number")
|
|
.max(19, "16 chars max.") //16 chars + 3 spaces
|
|
.test("luhn-validation", "Invalid Card Number", (value) => {
|
|
const sanitizedNumber = value?.replace(/\D/g, "");
|
|
const digits = Array?.from(sanitizedNumber, Number);
|
|
|
|
for (let i = digits.length - 2; i >= 0; i -= 2) {
|
|
digits[i] *= 2;
|
|
if (digits[i] > 9) {
|
|
digits[i] -= 9;
|
|
}
|
|
}
|
|
|
|
const sum = digits.reduce((acc, digit) => acc + digit, 0);
|
|
|
|
return sum % 10 === 0;
|
|
})
|
|
.required("required"),
|
|
code: Yup.string()
|
|
.min(3, "3 chars min.")
|
|
.max(25, "25 chars max.")
|
|
.required("required"),
|
|
state: Yup.string()
|
|
.min(2, "2 chars min.")
|
|
.max(25, "25 chars max.")
|
|
.required("required"),
|
|
address: Yup.string()
|
|
.min(3, "3 chars min.")
|
|
.max(50, "50 chars max.")
|
|
.required("required"),
|
|
expirationYear: Yup.string()
|
|
.min(4, "4 chars min.")
|
|
.max(4, "4 chars max.")
|
|
.required("required"),
|
|
expirationMonth: Yup.string()
|
|
.min(1, "1 chars min.")
|
|
.max(2, "2 chars max.")
|
|
.required("required"),
|
|
cvv: Yup.string()
|
|
.min(3, "3 chars min.")
|
|
.max(4, "4 chars max.")
|
|
.required("required"),
|
|
});
|
|
|
|
const initialValues = {
|
|
cardNum: "",
|
|
code: "",
|
|
state: "",
|
|
address: "",
|
|
expirationYear: "",
|
|
expirationMonth: "",
|
|
cvv: "",
|
|
};
|
|
|
|
function AddFundDollars(props) {
|
|
let MaxNoOfCards = process.env.REACT_APP_MAX_CREDIT_CARDS; // HOLDS THE VALUE OF THE MAX NUMBER OF CARDS USER CAN ADD
|
|
|
|
let [loadingState, setLoadingState] = useState(false)
|
|
|
|
const apiCall = new usersService();
|
|
let countryWallet = props.walletItem.country;
|
|
const [selectedOption, setSelectedOption] = useState("previous");
|
|
const { userDetails } = useSelector((state) => state?.userDetails);
|
|
const [prevCardDetails, setPrevCardDetails] = useState({});
|
|
const [payListCards, setPayListCards] = useState({ loading: true, data: [] });
|
|
const [cardIcons, setCardIcons] = useState("atm-card");
|
|
const [prevCardError, setPrevCardError] = useState("");
|
|
|
|
const handleOptionChange = (event) => {
|
|
setSelectedOption(event.target.value);
|
|
};
|
|
|
|
const { firstname, lastname } = userDetails;
|
|
|
|
// Handling Card Icons
|
|
const handleCards = (event) => {
|
|
const { name, value } = event.target;
|
|
|
|
if (name == "cardNum") {
|
|
// Check if the first character is 4 or 5 and set the card icon accordingly
|
|
const cardIcon =
|
|
value.length > 0
|
|
? value[0] === "4"
|
|
? "visa-card"
|
|
: value[0] === "5"
|
|
? "master-card"
|
|
: "atm-card"
|
|
: "atm-card";
|
|
setCardIcons(cardIcon);
|
|
}
|
|
};
|
|
|
|
// Handling card change
|
|
const handleInputChange = (event) => {
|
|
const { name, value } = event.target;
|
|
|
|
setPrevCardDetails((prevState) => ({
|
|
...prevState,
|
|
[name]: value,
|
|
}));
|
|
};
|
|
|
|
// Handling card number grouping
|
|
const handleCardNumberChange = (value) => {
|
|
return value
|
|
?.replace(/\s/g, "") // Remove existing spaces
|
|
.match(/.{1,4}/g) // Group every four characters
|
|
?.join(" ");
|
|
};
|
|
|
|
// card slicer
|
|
const indexOfFirstItem = 0;
|
|
const indexOfLastItem =
|
|
indexOfFirstItem + Number(process.env.REACT_APP_ITEM_PER_PAGE);
|
|
const currentPreviousCards = payListCards?.data?.slice(
|
|
indexOfFirstItem,
|
|
indexOfLastItem
|
|
);
|
|
|
|
// Submission for both prev and new cards
|
|
const handleSubmit = async (values, helpers) => {
|
|
props.setInputError("");
|
|
|
|
if (!props.input || props.input === "0") {
|
|
props.setInputError("Please Enter Amount");
|
|
setTimeout(() => props.setInputError(""), 5000);
|
|
return;
|
|
}
|
|
|
|
if (isNaN(props.input)) {
|
|
props.setInputError("Amount must be a Number");
|
|
setTimeout(() => props.setInputError(""), 5000);
|
|
return;
|
|
}
|
|
|
|
if (Number(props.input) * 100 > Number(props.walletItem?.transfer_limit)) {
|
|
props.setInputError("Credit limit has been exceeded");
|
|
setTimeout(() => props.setInputError(""), 5000);
|
|
return;
|
|
}
|
|
|
|
let stateData = {
|
|
amount: Number(props.input) * 100,
|
|
currency: props.walletItem?.code,
|
|
};
|
|
|
|
if (selectedOption === "previous") {
|
|
// To check if card is empty
|
|
if (Object.keys(prevCardDetails).length === 0) {
|
|
setPrevCardError("No card selected!");
|
|
setTimeout(() => setPrevCardError(""), 5000);
|
|
return;
|
|
}
|
|
}
|
|
|
|
// props.setConfirmCredit((prev) => ({
|
|
// ...prev,
|
|
// show: { awaitConfirm: { loader: true } },
|
|
// }));
|
|
setLoadingState(true)
|
|
|
|
// Extracting card_uid from the previous card details
|
|
const paymentCardValue = prevCardDetails["payment-card"];
|
|
|
|
if (paymentCardValue) {
|
|
try {
|
|
const paymentCardObject = JSON.parse(paymentCardValue);
|
|
stateData = {
|
|
...stateData,
|
|
card_uid: paymentCardObject.card_uid,
|
|
};
|
|
} catch (error) {
|
|
console.error("Error parsing JSON:", error);
|
|
}
|
|
} else {
|
|
// For the new card details
|
|
stateData = {
|
|
...stateData,
|
|
card_uid: "",
|
|
};
|
|
}
|
|
|
|
try {
|
|
const res = await apiCall.getStartCredit(stateData);
|
|
if (res.data.internal_return < 0) {
|
|
props.setInputError("An Error Occurred");
|
|
throw new Error("An Error Occurred");
|
|
|
|
// use commented code when you when to display pop for failed start credit API
|
|
// props.setConfirmCredit((prev) => ({
|
|
// ...prev,
|
|
// show: {
|
|
// awaitConfirm: { loader: false, state: false },
|
|
// acceptConfirm: { loader: false, state: true },
|
|
// },
|
|
// data: {internal_return: -1}
|
|
// }));
|
|
setLoadingState(false)
|
|
return
|
|
}
|
|
|
|
const _response = res.data;
|
|
stateData.card =
|
|
selectedOption === "previous"
|
|
? prevCardDetails["payment-card"]
|
|
: { ...values, cvv: values.cvv };
|
|
stateData.cardType = selectedOption === "previous" ? "prev" : "new";
|
|
stateData = { ...stateData, ..._response };
|
|
|
|
setTimeout(() => {
|
|
setLoadingState(false)
|
|
props.setConfirmCredit({
|
|
show: {
|
|
awaitConfirm: { loader: false, state: true },
|
|
acceptConfirm: { loader: false, state: false },
|
|
},
|
|
data: stateData,
|
|
});
|
|
}, 1500);
|
|
} catch (error) {
|
|
setLoadingState(false)
|
|
props.setInputError(error.message);
|
|
setTimeout(() => props.setInputError(""), 5000);
|
|
props.setConfirmCredit((prev) => ({
|
|
...prev,
|
|
show: { awaitConfirm: { loader: false } },
|
|
}));
|
|
console.log(error);
|
|
}
|
|
};
|
|
|
|
useEffect(() => {
|
|
apiCall
|
|
.payListCard()
|
|
.then((res) => {
|
|
setPayListCards({ loading: false, data: res.data.result_list });
|
|
})
|
|
.catch((err) => {
|
|
console.log("PAYCARDLIST ERROR", err);
|
|
setPayListCards({ loading: false, data: [] });
|
|
});
|
|
}, []);
|
|
|
|
const handleClose = props.onClose;
|
|
|
|
return (
|
|
<>
|
|
<div className="w-full">
|
|
{/* switch button */}
|
|
<div className="flex">
|
|
<form className="add-fund-info flex items-center gap-3">
|
|
<h1 className="text-xl font-bold text-dark-gray dark:text-white tracking-tighter my-1">
|
|
{countryWallet == "US" && "Payment Method"}
|
|
</h1>
|
|
<div className="my-1 flex items-center gap-2">
|
|
<label
|
|
htmlFor="previous"
|
|
className="cursor-pointer flex items-center gap-1"
|
|
>
|
|
<input
|
|
type="radio"
|
|
id="previous"
|
|
value="previous"
|
|
name="card-option"
|
|
onChange={handleOptionChange}
|
|
checked={selectedOption === "previous"}
|
|
className={`p-2 text-lg font-bold text-slate-600 dark:text-white border pointer-events-none w-7 h-7 ${
|
|
selectedOption == "previous" ? "" : ""
|
|
} tracking-wide transition duration-200`}
|
|
/>
|
|
Previous Cards
|
|
</label>
|
|
<label
|
|
htmlFor="new"
|
|
className={`cursor-pointer flex items-center gap-1 ${
|
|
payListCards.data.length >= MaxNoOfCards
|
|
? "pointer-events-none"
|
|
: ""
|
|
}`}
|
|
>
|
|
<input
|
|
id="new"
|
|
type="radio"
|
|
name="card-option"
|
|
value="new"
|
|
onChange={handleOptionChange}
|
|
checked={selectedOption === "new"}
|
|
className={`p-2 text-lg font-bold text-slate-600 dark:text-white border pointer-events-none w-7 h-7 ${
|
|
selectedOption == "new" ? "" : ""
|
|
} tracking-wide transition duration-200`}
|
|
/>
|
|
Add New Card{" "}
|
|
{payListCards.data.length >= MaxNoOfCards && (
|
|
<span className="text-[14px] text-red-500">Max Reached</span>
|
|
)}
|
|
</label>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
<hr />
|
|
{/* END OF switch button */}
|
|
|
|
{/* previous selectedOption */}
|
|
{selectedOption === "previous" && (
|
|
<div className="p-4 previous-details w-full min-h-[16.5rem] flex flex-col">
|
|
{payListCards.loading ? (
|
|
<LoadingSpinner size="10" color="sky-blue" />
|
|
) : payListCards?.data?.length ? (
|
|
<select
|
|
className="my-3 w-full rounded-full p-2 outline-none text-base text-black dark:text-gray-100 bg-[#FAFAFA] dark:bg-[#11131F] border"
|
|
value={prevCardDetails["payment-card"]?.card_uid}
|
|
id="payment-card"
|
|
name="payment-card"
|
|
onChange={handleInputChange}
|
|
>
|
|
<option value="">Select a card</option>
|
|
{currentPreviousCards.map((item, index) => (
|
|
<option
|
|
key={index}
|
|
className={index !== 0 ? "border-t-2" : undefined}
|
|
value={JSON.stringify(item)}
|
|
title={`${item.description} Card\nBank **************${item.digits}`}
|
|
>
|
|
{/* <div className="my-2 flex items-center gap-5">
|
|
<div className="card-details">
|
|
<h1 className="text-lg font-bold text-dark-gray dark:text-white tracking-wide">
|
|
{item.description} Card
|
|
</h1>
|
|
<p className="text-base font-bold text-dark-gray dark:text-white tracking-wide">
|
|
Bank **************{item.digits}
|
|
</p>
|
|
</div>
|
|
</div> */}
|
|
{item.description} Card - Bank **************{item.digits}
|
|
</option>
|
|
))}
|
|
</select>
|
|
) : (
|
|
<div className="w-full flex flex-col items-center">
|
|
<p className="my-5 text-base font-bold text-dark-gray dark:text-white tracking-wide">
|
|
No Previous Card Found!
|
|
</p>
|
|
<button
|
|
onClick={() => setSelectedOption("new")}
|
|
type="button"
|
|
className="my-5 px-2 py-1 h-11 flex justify-center items-center btn-gradient text-base rounded-full text-white"
|
|
>
|
|
<span className="text-white">Add Card</span>
|
|
</button>
|
|
</div>
|
|
)}
|
|
<p className="text-base italic text-red-500 h-5">
|
|
{prevCardError && prevCardError}
|
|
</p>
|
|
</div>
|
|
)}
|
|
|
|
{selectedOption === "new" && (
|
|
<div className="new-details w-full max-h-[22rem]">
|
|
{payListCards.loading ? (
|
|
<div className="pt-10 flex w-full h-full justify-center items-center">
|
|
<LoadingSpinner size="10" color="sky-blue" />
|
|
</div>
|
|
) : payListCards.data.length < MaxNoOfCards ? (
|
|
<div className="w-full flex flex-col justify-between">
|
|
<Formik
|
|
initialValues={initialValues}
|
|
validationSchema={validationSchema}
|
|
onSubmit={handleSubmit}
|
|
>
|
|
{(props) => {
|
|
return (
|
|
<Form className="md:pl-8">
|
|
<div className="flex flex-col-reverse sm:flex-row">
|
|
<div className="flex-1 sm:mr-10">
|
|
<div className="fields w-full">
|
|
{/* Inputs */}
|
|
{/* Name */}
|
|
<div className="flex items-center field w-full my-2 flex-[0.4] gap-3">
|
|
<label className="input-label text-[#181c32] dark:text-white text-[13.975px] leading-[20.9625px] font-semibold flex items-center gap-1">
|
|
Name:
|
|
</label>
|
|
<p className="input-label text-[#181c32] dark:text-white text-[16px] leading-[20.9625px] font-semibold flex items-center gap-1">{`${firstname} ${lastname}`}</p>
|
|
</div>
|
|
|
|
<div className="flex items-center flex-1 gap-3 my-2">
|
|
{/* Card Number */}
|
|
<div className="field w-full flex-[0.6]">
|
|
<InputCom
|
|
fieldClass="px-6"
|
|
spanTag="*"
|
|
iconName={cardIcons}
|
|
label="Card Number"
|
|
type="text"
|
|
name="cardNum"
|
|
onInput={handleCards}
|
|
placeholder="Enter Card Number"
|
|
value={handleCardNumberChange(
|
|
props.values.cardNum
|
|
)}
|
|
inputHandler={props.handleChange}
|
|
blurHandler={props.handleBlur}
|
|
error={props.errors.cardNum}
|
|
/>
|
|
</div>
|
|
|
|
{/* Expire Year, Year */}
|
|
<div className="sm:grid gap-5 grid-cols-2 flex-[0.4]">
|
|
<div className="field w-full mb-6 xl:mb-0 col-span-1">
|
|
<div className="select-option">
|
|
<div
|
|
className={`flex items-center justify-between mb-2.5`}
|
|
>
|
|
<label
|
|
className="input-label text-[#181c32] dark:text-white text-[13.975px] leading-[20.9625px] font-semibold line-clamp-3 flex items-center"
|
|
htmlFor="expiration"
|
|
>
|
|
Exp Month{" "}
|
|
<span className="text-red-700 text-sm italic">
|
|
*
|
|
</span>
|
|
<span className="text-[12px] text-red-500 ml-1">
|
|
{props.errors.expirationMonth &&
|
|
"**"}
|
|
</span>
|
|
</label>
|
|
</div>
|
|
<div
|
|
className={`input-wrapper border border-[#f5f8fa] dark:border-[#5e6278] w-full rounded-full h-[42px] overflow-hidden relative font-medium leading-6 bg-clip-padding text-[#5e6278] dark:text-gray-100 bg-[#f5f8fa] dark:bg-[#5e6278] text-base`}
|
|
>
|
|
<select
|
|
className={`input-field placeholder:text-base text-dark-gray w-full h-full tracking-wide dark:bg-[#11131F] bg-[#fafafa] focus:ring-0 focus:outline-none`}
|
|
value={props.values.expirationMonth}
|
|
onChange={props.handleChange}
|
|
onBlur={props.handleBlur}
|
|
name="expirationMonth"
|
|
>
|
|
<option
|
|
value=""
|
|
className="text-dark-gray"
|
|
>
|
|
Month
|
|
</option>
|
|
{expireMonth?.length &&
|
|
expireMonth.map((item, index) => (
|
|
<option
|
|
key={index}
|
|
value={
|
|
Number(item.value) < 10
|
|
? "0" + item.value
|
|
: item.value
|
|
}
|
|
>
|
|
{item.name}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="field w-full mb-6 xl:mb-0 col-span-1">
|
|
<div className="select-option">
|
|
<div
|
|
className={`flex items-center justify-between mb-2.5`}
|
|
>
|
|
<label
|
|
className="input-label text-[#181c32] dark:text-white text-[13.975px] leading-[20.9625px] font-semibold flex items-center line-clamp-3"
|
|
htmlFor="expiration"
|
|
>
|
|
Exp Year{" "}
|
|
<span className="text-red-700 text-sm tracking-wide">
|
|
*
|
|
</span>
|
|
<span className="text-[12px] text-red-500 italic">
|
|
{props.errors.expirationYear &&
|
|
"**"}
|
|
</span>
|
|
</label>
|
|
</div>
|
|
<div
|
|
className={`input-wrapper border border-[#f5f8fa] dark:border-[#5e6278] w-full rounded-full h-[42px] overflow-hidden relative font-medium leading-6 bg-clip-padding text-[#5e6278] dark:text-gray-100 bg-[#f5f8fa] dark:bg-[#5e6278] text-base`}
|
|
>
|
|
<select
|
|
className={`input-field placeholder:text-base text-dark-gray w-full h-full tracking-wide dark:bg-[#11131F] bg-[#fafafa] focus:ring-0 focus:outline-none`}
|
|
value={props.values.expirationYear}
|
|
onChange={props.handleChange}
|
|
onBlur={props.handleBlur}
|
|
name="expirationYear"
|
|
>
|
|
<option
|
|
value=""
|
|
className="text-dark-gray"
|
|
>
|
|
Year
|
|
</option>
|
|
{expireYear?.length &&
|
|
expireYear.map((item, index) => (
|
|
<option key={index} value={item}>
|
|
{item}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Address and CVV */}
|
|
<div className="flex items-center flex-1 gap-3 my-2">
|
|
<div className="field w-full col-span-1 flex-[0.4]">
|
|
<InputCom
|
|
fieldClass="px-6"
|
|
spanTag="*"
|
|
iconName={cardIcons}
|
|
label="CVV"
|
|
type="text"
|
|
name="cvv"
|
|
placeholder="CVV"
|
|
maxLength={3}
|
|
value={props.values.cvv}
|
|
inputHandler={props.handleChange}
|
|
blurHandler={props.handleBlur}
|
|
error={props.errors.cvv}
|
|
/>
|
|
</div>
|
|
<div className="field w-full flex-[0.6]">
|
|
<InputCom
|
|
fieldClass="px-6"
|
|
spanTag="*"
|
|
label="Billing Address"
|
|
type="text"
|
|
name="address"
|
|
placeholder="Billing Address"
|
|
value={props.values.Address}
|
|
inputHandler={props.handleChange}
|
|
blurHandler={props.handleBlur}
|
|
error={props.errors.address}
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Postal Code and State */}
|
|
<div className="sm:grid gap-5 grid-cols-3 my-2">
|
|
<div className="field w-full xl:mb-0 col-span-1">
|
|
<InputCom
|
|
fieldClass="px-6"
|
|
spanTag="*"
|
|
label="Postal Code"
|
|
type="number"
|
|
name="code"
|
|
placeholder="Postal Code"
|
|
value={props.values.code}
|
|
maxLength={6}
|
|
inputHandler={props.handleChange}
|
|
blurHandler={props.handleBlur}
|
|
error={props.errors.code}
|
|
/>
|
|
</div>
|
|
<div className="field w-full col-span-1">
|
|
<InputCom
|
|
fieldClass="px-6"
|
|
spanTag="*"
|
|
label="State"
|
|
type="text"
|
|
name="state"
|
|
placeholder="State"
|
|
value={props.values.state.toUpperCase()}
|
|
inputHandler={props.handleChange}
|
|
blurHandler={props.handleBlur}
|
|
error={props.errors.state}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="add-fund-btn flex justify-end items-center gap-2 mt-4">
|
|
<button
|
|
className="px-4 py-1 h-11 max-w-[100px] w-full flex justify-center bg-[#f5a430] text-black items-center text-base rounded-full"
|
|
onClick={handleClose}
|
|
>
|
|
Cancel
|
|
</button>
|
|
<button
|
|
type="submit"
|
|
className="px-4 py-1 h-11 max-w-[115px] w-full flex justify-center items-center btn-gradient text-base rounded-full text-white"
|
|
>
|
|
{loadingState ? (
|
|
<LoadingSpinner size="6" color="sky-blue" />
|
|
) : (
|
|
<>
|
|
<span className="text-white">Continue</span>{" "}
|
|
<Icons name="chevron-right" />
|
|
</>
|
|
)}
|
|
</button>
|
|
</div>
|
|
</Form>
|
|
);
|
|
}}
|
|
</Formik>
|
|
</div>
|
|
) : null}
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{selectedOption == "previous" && (
|
|
<div className="md:py-8 add-fund-btn flex justify-end items-center gap-2 py-4">
|
|
<button
|
|
className="px-4 py-1 h-11 max-w-[100px] w-full flex justify-center bg-[#f5a430] text-black items-center text-base rounded-full"
|
|
onClick={props.onClose}
|
|
>
|
|
Cancel
|
|
</button>
|
|
<button
|
|
onClick={handleSubmit}
|
|
name="previous"
|
|
type="button"
|
|
className="px-4 py-1 h-11 max-w-[115px] w-full flex justify-center items-center btn-gradient text-base rounded-full text-white"
|
|
>
|
|
{loadingState ? (
|
|
<LoadingSpinner size="6" color="sky-blue" />
|
|
) : (
|
|
<span className="text-white">Continue</span>
|
|
)}
|
|
</button>
|
|
</div>
|
|
)}
|
|
</>
|
|
);
|
|
}
|
|
|
|
export default AddFundDollars;
|
|
|
|
// FORMS ARRAY OF EXPIRATION YEAR FOR CARD
|
|
const expireYear = [];
|
|
let currentYear = new Date().getFullYear();
|
|
for (let i = 0; i <= 6; i++) {
|
|
expireYear.push(currentYear + i);
|
|
}
|
|
|
|
// FORMS ARRAY OF EXPIRATION MONTH FOR CARD
|
|
let month = [
|
|
"January",
|
|
"February",
|
|
"March",
|
|
"April",
|
|
"May",
|
|
"June",
|
|
"July",
|
|
"August",
|
|
"September",
|
|
"October",
|
|
"November",
|
|
"December",
|
|
];
|
|
const expireMonth = [];
|
|
for (let i = 0; i < month.length; i++) {
|
|
expireMonth.push({ name: month[i], value: i + 1 });
|
|
}
|