feat: add new feature and fix bugs
This commit is contained in:
@@ -18,6 +18,7 @@ import { Toaster } from "react-hot-toast";
|
|||||||
import { IsAuthenticated } from "./utils/auth";
|
import { IsAuthenticated } from "./utils/auth";
|
||||||
import { HelmetProvider } from "react-helmet-async";
|
import { HelmetProvider } from "react-helmet-async";
|
||||||
import InActivePayment from "./pages/inActivePayment";
|
import InActivePayment from "./pages/inActivePayment";
|
||||||
|
import AdvancedVerification from "./pages/advancedVerification";
|
||||||
import { useSelector } from "react-redux";
|
import { useSelector } from "react-redux";
|
||||||
import { RootState } from "./redux/store";
|
import { RootState } from "./redux/store";
|
||||||
import Page404 from "./pages/404";
|
import Page404 from "./pages/404";
|
||||||
@@ -72,6 +73,10 @@ const App = () => {
|
|||||||
isPaymentGateway ? <Navigate to="/" /> : <InActivePayment />
|
isPaymentGateway ? <Navigate to="/" /> : <InActivePayment />
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
<Route
|
||||||
|
path="/advanced-verification"
|
||||||
|
element={<AdvancedVerification />}
|
||||||
|
/>
|
||||||
<Route path="*" element={<Page404 />} />
|
<Route path="*" element={<Page404 />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
</HelmetProvider>
|
</HelmetProvider>
|
||||||
|
|||||||
@@ -35,10 +35,10 @@ export default function MuiModal({
|
|||||||
onClose={handleCloseCustom || handleClose}
|
onClose={handleCloseCustom || handleClose}
|
||||||
aria-labelledby="modal-modal-title"
|
aria-labelledby="modal-modal-title"
|
||||||
aria-describedby="modal-modal-description"
|
aria-describedby="modal-modal-description"
|
||||||
className="relative w-full h-full flex justify-center items-center !border-none !outline-none !ring-0"
|
className="relative w-full h-full flex justify-center items-center !border-none !outline-none !ring-0 overflow-y-auto p-4"
|
||||||
>
|
>
|
||||||
<motion.div
|
<motion.div
|
||||||
className="max-md:w-72 md:w-80 lg:w-96 min-h-80 shadow-xl shadow-[rgb(20,20,20)] bg-[rgb(20,20,20,0.99)] rounded-lg p-4 relative flex flex-col justify-between items-center gap-4"
|
className="max-md:w-72 md:w-80 lg:w-96 max-h-[90vh] shadow-xl shadow-[rgb(20,20,20)] bg-[rgb(20,20,20,0.99)] rounded-lg p-4 relative flex flex-col justify-between items-center gap-4 my-auto overflow-y-auto"
|
||||||
initial={{ y: "-100vw", opacity: 0 }}
|
initial={{ y: "-100vw", opacity: 0 }}
|
||||||
animate={{ y: 0, opacity: 1 }}
|
animate={{ y: 0, opacity: 1 }}
|
||||||
transition={{ duration: 0.5 }}
|
transition={{ duration: 0.5 }}
|
||||||
@@ -54,7 +54,7 @@ export default function MuiModal({
|
|||||||
<IoClose className="w-7 h-7" />
|
<IoClose className="w-7 h-7" />
|
||||||
</button>
|
</button>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
<motion.div className="w-full h-full flex flex-col justify-between text-center items-center">
|
<motion.div className="w-full flex-1 flex flex-col justify-between text-center items-center overflow-y-auto">
|
||||||
{children}
|
{children}
|
||||||
</motion.div>
|
</motion.div>
|
||||||
<motion.div
|
<motion.div
|
||||||
|
|||||||
@@ -5,12 +5,7 @@ const Dropdown = ({ isOpen, setIsOpen, children }: DropdownProps) => {
|
|||||||
const dropdownRef = useRef<HTMLDivElement>(null);
|
const dropdownRef = useRef<HTMLDivElement>(null);
|
||||||
const childrenArray = Children.toArray(children);
|
const childrenArray = Children.toArray(children);
|
||||||
|
|
||||||
if (childrenArray.length !== 2) {
|
useEffect(() => {
|
||||||
console.error("ِDropDown باید دقیقاً دو فرزند داشته باشد.");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const [ButtonChild, DropDownChild] = childrenArray;
|
|
||||||
const handleClickOutside = (event: MouseEvent) => {
|
const handleClickOutside = (event: MouseEvent) => {
|
||||||
if (
|
if (
|
||||||
dropdownRef.current &&
|
dropdownRef.current &&
|
||||||
@@ -20,12 +15,18 @@ const Dropdown = ({ isOpen, setIsOpen, children }: DropdownProps) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
document.addEventListener("mousedown", handleClickOutside);
|
document.addEventListener("mousedown", handleClickOutside);
|
||||||
return () => {
|
return () => {
|
||||||
document.removeEventListener("mousedown", handleClickOutside);
|
document.removeEventListener("mousedown", handleClickOutside);
|
||||||
};
|
};
|
||||||
}, []);
|
}, [setIsOpen]);
|
||||||
|
|
||||||
|
if (childrenArray.length !== 2) {
|
||||||
|
console.error("ِDropDown باید دقیقاً دو فرزند داشته باشد.");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [ButtonChild, DropDownChild] = childrenArray;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative inline-block text-left" ref={dropdownRef}>
|
<div className="relative inline-block text-left" ref={dropdownRef}>
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ import { ProgressSpaceProps } from "../../types/props";
|
|||||||
const ProgressSpace = ({ startSpace, endSpace }: ProgressSpaceProps) => {
|
const ProgressSpace = ({ startSpace, endSpace }: ProgressSpaceProps) => {
|
||||||
let startSpacePercentage;
|
let startSpacePercentage;
|
||||||
let remaining;
|
let remaining;
|
||||||
let startSpaceToNumber = Number(startSpace);
|
const startSpaceToNumber = Number(startSpace);
|
||||||
let endSpaceToNumber = Number(endSpace);
|
const endSpaceToNumber = Number(endSpace);
|
||||||
|
|
||||||
const isMaxSpace = startSpaceToNumber >= endSpaceToNumber;
|
const isMaxSpace = startSpaceToNumber >= endSpaceToNumber;
|
||||||
|
|
||||||
|
|||||||
@@ -14,16 +14,15 @@ const LandingLayout = () => {
|
|||||||
if (location.hash === "#shoppingConfig") {
|
if (location.hash === "#shoppingConfig") {
|
||||||
navigate("/shopping-config");
|
navigate("/shopping-config");
|
||||||
}
|
}
|
||||||
}, [location]);
|
}, [location, navigate]);
|
||||||
|
|
||||||
const { configs } = useParams<{ configs: string }>();
|
const { configs } = useParams<{ configs: string }>();
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
console.log(configs)
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (configs === "1" || configs === "2" || configs === "3") {
|
if (configs === "1" || configs === "2" || configs === "3") {
|
||||||
dispatch(setConfigId(configs))
|
dispatch(setConfigId(configs))
|
||||||
}
|
}
|
||||||
}, [configs]);
|
}, [configs, dispatch]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ const Layout = () => {
|
|||||||
dispatch(setShopId(shopId));
|
dispatch(setShopId(shopId));
|
||||||
navigate("/");
|
navigate("/");
|
||||||
}
|
}
|
||||||
}, [shopId, dispatch]);
|
}, [shopId, dispatch, navigate]);
|
||||||
|
|
||||||
// useEffect(() => {
|
// useEffect(() => {
|
||||||
// toast.custom(
|
// toast.custom(
|
||||||
|
|||||||
@@ -0,0 +1,91 @@
|
|||||||
|
import { BiArrowBack, BiShieldQuarter } from "react-icons/bi";
|
||||||
|
import { FaTelegram } from "react-icons/fa6";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
import Button from "../../../components/button";
|
||||||
|
import {
|
||||||
|
ADVANCED_VERIFICATION_PRICE_LIMIT,
|
||||||
|
ADVANCED_VERIFICATION_SUPPORT_URL,
|
||||||
|
buildAdvancedVerificationMessage,
|
||||||
|
formatToman,
|
||||||
|
} from "../constants";
|
||||||
|
|
||||||
|
interface AdvancedVerificationNoticeProps {
|
||||||
|
payablePrice?: number;
|
||||||
|
limit?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const AdvancedVerificationNotice = ({
|
||||||
|
payablePrice,
|
||||||
|
limit = ADVANCED_VERIFICATION_PRICE_LIMIT,
|
||||||
|
}: AdvancedVerificationNoticeProps) => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const hasOrderDetails = Boolean(payablePrice || limit);
|
||||||
|
const message = buildAdvancedVerificationMessage(limit);
|
||||||
|
const orderDetailsText = [
|
||||||
|
payablePrice ? `مبلغ سفارش شما ${formatToman(payablePrice)} است.` : "",
|
||||||
|
limit
|
||||||
|
? `خریدهای با مبلغ بیش از ${formatToman(limit)} نیاز به احراز هویت پیشرفته دارند.`
|
||||||
|
: "",
|
||||||
|
]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(" ");
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="w-full max-w-2xl glass p-5 md:p-7 rounded-lg border border-yellow-400/20 shadow-2xl shadow-yellow-950/20">
|
||||||
|
<div className="w-full flex flex-col items-center gap-6 text-center">
|
||||||
|
<div className="relative flex justify-center items-center">
|
||||||
|
<span className="absolute w-28 h-28 rounded-full bg-yellow-400/10" />
|
||||||
|
<span className="absolute w-20 h-20 rounded-full bg-yellow-400/15" />
|
||||||
|
<BiShieldQuarter className="relative w-20 h-20 text-yellow-300" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<p className="text-xs md:text-sm text-yellow-300">
|
||||||
|
نیاز به احراز هویت پیشرفته
|
||||||
|
</p>
|
||||||
|
<h1 className="text-xl md:text-2xl font-bold">
|
||||||
|
ادامه پرداخت این سفارش فعلا ممکن نیست
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p className="max-w-xl text-sm md:text-base text-gray-200 leading-8">
|
||||||
|
{message}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{hasOrderDetails && (
|
||||||
|
<div className="w-full rounded-md bg-gray-900/60 p-4 border border-white/5 text-sm text-start">
|
||||||
|
<p className="text-gray-400">جزئیات سفارش</p>
|
||||||
|
<p className="mt-2 text-yellow-300 font-bold leading-7">
|
||||||
|
{orderDetailsText}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="w-full flex flex-col sm:flex-row gap-3">
|
||||||
|
<Button extra="!flex-1 !bg-[#229ED9] !text-white hover:!bg-[#1b8bc0]">
|
||||||
|
<a
|
||||||
|
href={ADVANCED_VERIFICATION_SUPPORT_URL}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="w-full h-full flex justify-center items-center gap-2"
|
||||||
|
>
|
||||||
|
<FaTelegram className="w-6 h-6" />
|
||||||
|
<span>ارتباط با پشتیبانی تلگرام</span>
|
||||||
|
</a>
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
extra="!flex-1 !bg-gray-800 !text-white hover:!bg-gray-700"
|
||||||
|
onClick={() => navigate("/")}
|
||||||
|
>
|
||||||
|
<span className="w-full flex justify-center items-center gap-2">
|
||||||
|
<BiArrowBack className="w-5 h-5" />
|
||||||
|
بازگشت به فروشگاه
|
||||||
|
</span>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AdvancedVerificationNotice;
|
||||||
37
src/pages/advancedVerification/constants.ts
Normal file
37
src/pages/advancedVerification/constants.ts
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
export const ADVANCED_VERIFICATION_PRICE_LIMIT = 20000000;
|
||||||
|
export const ADVANCED_VERIFICATION_ROUTE = "/advanced-verification";
|
||||||
|
export const ADVANCED_VERIFICATION_SUPPORT_URL =
|
||||||
|
"https://t.me/PersianPingSupport";
|
||||||
|
|
||||||
|
const persianDigits: Record<string, string> = {
|
||||||
|
"0": "۰",
|
||||||
|
"1": "۱",
|
||||||
|
"2": "۲",
|
||||||
|
"3": "۳",
|
||||||
|
"4": "۴",
|
||||||
|
"5": "۵",
|
||||||
|
"6": "۶",
|
||||||
|
"7": "۷",
|
||||||
|
"8": "۸",
|
||||||
|
"9": "۹",
|
||||||
|
};
|
||||||
|
|
||||||
|
const toPersianDigits = (value: string): string => {
|
||||||
|
return value.replace(/\d/g, (digit) => persianDigits[digit]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const separateByThree = (num: number): string => {
|
||||||
|
return (num ?? 0).toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
|
||||||
|
};
|
||||||
|
|
||||||
|
export const formatToman = (amount: number): string => {
|
||||||
|
return `${toPersianDigits(separateByThree(amount))} تومان`;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const buildAdvancedVerificationMessage = (
|
||||||
|
limit = ADVANCED_VERIFICATION_PRICE_LIMIT
|
||||||
|
): string => {
|
||||||
|
return `بهدلیل برخی پروتکلهای امنیتی و بهمنظور جلوگیری از سوءاستفاده، برای خریدهای با مبلغ بیش از ${formatToman(
|
||||||
|
limit
|
||||||
|
)}، احراز هویت پیشرفته انجام میشود، لطفا برای ادامه فرایند با پشتیبانی تلگرام به نشانی ${ADVANCED_VERIFICATION_SUPPORT_URL} در ارتباط باشید.`;
|
||||||
|
};
|
||||||
35
src/pages/advancedVerification/index.tsx
Normal file
35
src/pages/advancedVerification/index.tsx
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import { Helmet } from "react-helmet-async";
|
||||||
|
import { useLocation } from "react-router-dom";
|
||||||
|
import Particle from "../../components/particle";
|
||||||
|
import AdvancedVerificationNotice from "./components/AdvancedVerificationNotice";
|
||||||
|
|
||||||
|
interface AdvancedVerificationLocationState {
|
||||||
|
payablePrice?: number;
|
||||||
|
limit?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const AdvancedVerification = () => {
|
||||||
|
const location = useLocation();
|
||||||
|
const state = (location.state || {}) as AdvancedVerificationLocationState;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Helmet>
|
||||||
|
<title>احراز هویت پیشرفته | پرشین پینگ</title>
|
||||||
|
<meta
|
||||||
|
name="description"
|
||||||
|
content="برای تکمیل خریدهای نیازمند احراز هویت پیشرفته با پشتیبانی پرشین پینگ در ارتباط باشید."
|
||||||
|
/>
|
||||||
|
</Helmet>
|
||||||
|
<Particle />
|
||||||
|
<div className="relative w-full min-h-screen bg-noisy flex justify-center items-center p-5">
|
||||||
|
<AdvancedVerificationNotice
|
||||||
|
payablePrice={state.payablePrice}
|
||||||
|
limit={state.limit}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AdvancedVerification;
|
||||||
@@ -16,8 +16,7 @@ import callof2 from "../../../../../assets/product-image/callof/cp-callofduty360
|
|||||||
import callof3 from "../../../../../assets/product-image/callof/cp-callofduty880.png";
|
import callof3 from "../../../../../assets/product-image/callof/cp-callofduty880.png";
|
||||||
import callof4 from "../../../../../assets/product-image/callof/cp-callofduty2400.png";
|
import callof4 from "../../../../../assets/product-image/callof/cp-callofduty2400.png";
|
||||||
|
|
||||||
const ListAllProduct = ({ activeId }: { activeId: number }) => {
|
const allProducts = [
|
||||||
const allProducts = [
|
|
||||||
[
|
[
|
||||||
{ id: 1, title: "پابجی 1", image: pubg1 },
|
{ id: 1, title: "پابجی 1", image: pubg1 },
|
||||||
{ id: 2, title: "پابجی 2", image: pubg2 },
|
{ id: 2, title: "پابجی 2", image: pubg2 },
|
||||||
@@ -37,7 +36,9 @@ const ListAllProduct = ({ activeId }: { activeId: number }) => {
|
|||||||
{ id: 3, title: "جم کلش 3", image: clash3 },
|
{ id: 3, title: "جم کلش 3", image: clash3 },
|
||||||
{ id: 4, title: "جم کلش 4", image: clash4 },
|
{ id: 4, title: "جم کلش 4", image: clash4 },
|
||||||
],
|
],
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const ListAllProduct = ({ activeId }: { activeId: number }) => {
|
||||||
const [product, setProduct] = useState(allProducts[activeId]);
|
const [product, setProduct] = useState(allProducts[activeId]);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setProduct(allProducts[activeId]);
|
setProduct(allProducts[activeId]);
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import ProfileDetails from "./section/ProfileDetails";
|
import ProfileDetails from "./section/ProfileDetails";
|
||||||
import ProductConfigList from "./section/ProductConfigList";
|
import ProductConfigList from "./section/ProductConfigList";
|
||||||
|
import TelegramBanner from "./section/TelegramBanner";
|
||||||
import { useQueryParams } from "../../hooks/useQueryParams";
|
import { useQueryParams } from "../../hooks/useQueryParams";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
@@ -17,23 +18,22 @@ const Profile = () => {
|
|||||||
const [message, setMessage] = useState<string>("");
|
const [message, setMessage] = useState<string>("");
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (success === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (success) {
|
if (success === "1") {
|
||||||
if(success === "1"){
|
setIsOpen(true);
|
||||||
setIsOpen(true)
|
setIsPayment(true);
|
||||||
setIsPayment(true)
|
setMessage("موفقیت آمیز");
|
||||||
setMessage("موفقیت آمیز")
|
} else if (success === "0") {
|
||||||
}else if(success === "0"){
|
setIsOpen(true);
|
||||||
setIsOpen(true)
|
setIsPayment(false);
|
||||||
setIsPayment(false)
|
setMessage("خطا");
|
||||||
setMessage("خطا")
|
|
||||||
}else{
|
|
||||||
toast.error("متاسفانه کد پرداخت به درستی دریافت نشد")
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
console.error("Track ID is missing or invalid.");
|
toast.error("متاسفانه کد پرداخت به درستی دریافت نشد");
|
||||||
}
|
}
|
||||||
}, []);
|
}, [success]);
|
||||||
|
|
||||||
const { data, isLoading, isFetching, error } = useQuery({
|
const { data, isLoading, isFetching, error } = useQuery({
|
||||||
queryKey: ["profile"],
|
queryKey: ["profile"],
|
||||||
@@ -58,6 +58,7 @@ const Profile = () => {
|
|||||||
isFetching={isFetching}
|
isFetching={isFetching}
|
||||||
error={error}
|
error={error}
|
||||||
/>
|
/>
|
||||||
|
<TelegramBanner />
|
||||||
<ProductConfigList
|
<ProductConfigList
|
||||||
data={data?.data}
|
data={data?.data}
|
||||||
userTitle={data?.data?.result?.user_profile?.name}
|
userTitle={data?.data?.result?.user_profile?.name}
|
||||||
|
|||||||
@@ -16,12 +16,15 @@ const ProductConfigList = ({
|
|||||||
error,
|
error,
|
||||||
userTitle,
|
userTitle,
|
||||||
}: ProfileDetailsProps) => {
|
}: ProfileDetailsProps) => {
|
||||||
|
const configs = data?.result?.configs;
|
||||||
|
const showInitialLoading = (isLoading || isFetching) && !data;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full h-full p-5 glass flex flex-col gap-3">
|
<div className="w-full h-full p-5 glass flex flex-col gap-3">
|
||||||
<ProfileImage />
|
<ProfileImage />
|
||||||
<div className="w-full h-full flex justify-center items-center">
|
<div className="w-full h-full flex justify-center items-center">
|
||||||
<Button extra="!py-3 max-md:!py-1 max-md:px-2 !bg-gray-800 !text-white">
|
<Button extra="!py-3 max-md:!py-1 max-md:px-2 !bg-gray-800 !text-white">
|
||||||
{isLoading || isFetching ? (
|
{showInitialLoading ? (
|
||||||
<LoaderIcon className="m-1.5" />
|
<LoaderIcon className="m-1.5" />
|
||||||
) : error ? (
|
) : error ? (
|
||||||
"error"
|
"error"
|
||||||
@@ -31,7 +34,7 @@ const ProductConfigList = ({
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{data?.result?.configs?.length !== 0 && (
|
{configs?.length !== 0 && (
|
||||||
<p className="max-md:text-lg text-xl font-bold text-center">
|
<p className="max-md:text-lg text-xl font-bold text-center">
|
||||||
لیست کانفیگ ها
|
لیست کانفیگ ها
|
||||||
</p>
|
</p>
|
||||||
@@ -40,7 +43,7 @@ const ProductConfigList = ({
|
|||||||
className="w-full h-full p-3 max-sm:px-0 flex flex-col gap-3
|
className="w-full h-full p-3 max-sm:px-0 flex flex-col gap-3
|
||||||
justify-center items-center"
|
justify-center items-center"
|
||||||
>
|
>
|
||||||
{isLoading || isFetching ? (
|
{showInitialLoading ? (
|
||||||
Array(3)
|
Array(3)
|
||||||
.fill(null)
|
.fill(null)
|
||||||
.map((_, i) => (
|
.map((_, i) => (
|
||||||
@@ -52,7 +55,7 @@ const ProductConfigList = ({
|
|||||||
<Alert type="danger" message="خطایی با این موضوع رخ داد: ">
|
<Alert type="danger" message="خطایی با این موضوع رخ داد: ">
|
||||||
{error.message}
|
{error.message}
|
||||||
</Alert>
|
</Alert>
|
||||||
) : data?.result?.configs?.length === 0 ? (
|
) : configs?.length === 0 ? (
|
||||||
<Link to={'/configs/2'} className="w-full h-full flex flex-col gap-3 justify-center items-center">
|
<Link to={'/configs/2'} className="w-full h-full flex flex-col gap-3 justify-center items-center">
|
||||||
<IoAlertCircleSharp className="max-md:w-[3.5rem] max-md:h-[3.5rem] w-[5rem] h-[5rem]" />
|
<IoAlertCircleSharp className="max-md:w-[3.5rem] max-md:h-[3.5rem] w-[5rem] h-[5rem]" />
|
||||||
<div
|
<div
|
||||||
@@ -66,19 +69,27 @@ const ProductConfigList = ({
|
|||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
) : (
|
) : (
|
||||||
data?.result?.configs?.map((item) => {
|
configs?.map((item, index) => {
|
||||||
return (
|
return (
|
||||||
<Fragment key={item.id}>
|
<Fragment key={`${item.id}-${index}`}>
|
||||||
<ConfigItem
|
<ConfigItem
|
||||||
id={String(item.id)}
|
id={String(item.id)}
|
||||||
|
orderId={item.order_id}
|
||||||
configName={item.title}
|
configName={item.title}
|
||||||
dateBuy={item.created_at || "تاریخ نامعتبر"}
|
dateBuy={item.created_at || "تاریخ نامعتبر"}
|
||||||
isDisabled={item.is_expired}
|
isDisabled={item.is_expired}
|
||||||
isShowTraffic={item.show_traffic}
|
isShowTraffic={item.show_traffic}
|
||||||
textCopy={item.url}
|
textCopy={item.url}
|
||||||
type={item.type}
|
type={item.type}
|
||||||
|
username={item.username}
|
||||||
categoryId={item.category_id}
|
categoryId={item.category_id}
|
||||||
useMaxTraffic={item.use_max_traffic}
|
useMaxTraffic={item.use_max_traffic}
|
||||||
|
primaryIpv4={item.primary_ipv4}
|
||||||
|
secondaryIpv4={item.secondary_ipv4}
|
||||||
|
primaryIpv6={item.primary_ipv6}
|
||||||
|
secondaryIpv6={item.secondary_ipv6}
|
||||||
|
dnsIpPending={item.dns_ip_pending}
|
||||||
|
dnsRegistrationExpired={item.dns_registration_expired}
|
||||||
/>
|
/>
|
||||||
</Fragment>
|
</Fragment>
|
||||||
);
|
);
|
||||||
|
|||||||
24
src/pages/profile/section/TelegramBanner.tsx
Normal file
24
src/pages/profile/section/TelegramBanner.tsx
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import { FaTelegram } from "react-icons/fa";
|
||||||
|
|
||||||
|
const TELEGRAM_CHANNEL = "https://t.me/PersianPing";
|
||||||
|
|
||||||
|
const TelegramBanner = () => {
|
||||||
|
return (
|
||||||
|
<a
|
||||||
|
href={TELEGRAM_CHANNEL}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="w-full flex items-center justify-between gap-3 glass rounded-xl px-4 py-3 text-sm md:text-base hover:brightness-110 transition-all"
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-2 text-[#29A8EB]">
|
||||||
|
<FaTelegram className="text-2xl md:text-3xl shrink-0" />
|
||||||
|
<span className="font-bold">عضویت در کانال تلگرام</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-gray-400 text-xs md:text-sm text-right">
|
||||||
|
برای دریافت آخرین اخبار و اطلاعیهها حتماً در کانال تلگرام ما عضو باشید.
|
||||||
|
</p>
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TelegramBanner;
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
import { IoCopy } from "react-icons/io5";
|
import { IoCopy, IoDownload } from "react-icons/io5";
|
||||||
import Button from "../../../../components/button";
|
import Button from "../../../../components/button";
|
||||||
import { ConfigItemProps } from "../../../../types/props";
|
import { ConfigItemProps } from "../../../../types/props";
|
||||||
import ModalConfigDetails from "./ModalConfigDetails";
|
import ModalConfigDetails from "./ModalConfigDetails";
|
||||||
|
import ModalDnsConfig from "./ModalDnsConfig";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import Dropdown, { DropdownItem } from "../../../../components/dropdown";
|
import Dropdown, { DropdownItem } from "../../../../components/dropdown";
|
||||||
import ProgressSpace from "../../../../components/progressSpace";
|
import ProgressSpace from "../../../../components/progressSpace";
|
||||||
@@ -12,9 +13,11 @@ import { setCategoryId } from "../../../../redux/slices/categoryId";
|
|||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import useAxios from "../../../../hooks/useAxios";
|
import useAxios from "../../../../hooks/useAxios";
|
||||||
import LoaderSpaceConfig from "./LoaderSpaceConfig";
|
import LoaderSpaceConfig from "./LoaderSpaceConfig";
|
||||||
|
import { downloadWireGuardConfig } from "../../utils/wireguardConfig";
|
||||||
|
|
||||||
const ConfigItem = ({
|
const ConfigItem = ({
|
||||||
id,
|
id,
|
||||||
|
orderId,
|
||||||
dateBuy,
|
dateBuy,
|
||||||
configName,
|
configName,
|
||||||
isDisabled,
|
isDisabled,
|
||||||
@@ -22,9 +25,17 @@ const ConfigItem = ({
|
|||||||
textCopy,
|
textCopy,
|
||||||
type,
|
type,
|
||||||
categoryId,
|
categoryId,
|
||||||
useMaxTraffic
|
useMaxTraffic,
|
||||||
|
username: configUsername,
|
||||||
|
primaryIpv4,
|
||||||
|
secondaryIpv4,
|
||||||
|
primaryIpv6,
|
||||||
|
secondaryIpv6,
|
||||||
|
dnsIpPending,
|
||||||
|
dnsRegistrationExpired,
|
||||||
}: ConfigItemProps) => {
|
}: ConfigItemProps) => {
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
const [isOpenDnsModal, setIsOpenDnsModal] = useState(false);
|
||||||
const [isOpenDropDown, setIsOpenDropDown] = useState(false);
|
const [isOpenDropDown, setIsOpenDropDown] = useState(false);
|
||||||
const [configId, setConfigId] = useState("");
|
const [configId, setConfigId] = useState("");
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
@@ -49,12 +60,24 @@ const ConfigItem = ({
|
|||||||
setIsOpen(true);
|
setIsOpen(true);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
const configType = type ?? "";
|
||||||
|
const isWireGuard = configType.toLowerCase() === "wireguard";
|
||||||
|
const isDns = configType.toLowerCase() === "dns";
|
||||||
|
const downloadWireGuardFile = () => {
|
||||||
|
if (!textCopy) {
|
||||||
|
toast.error("کانفیگ وایرگارد موجود نیست");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
downloadWireGuardConfig(textCopy, configUsername);
|
||||||
|
toast.success("فایل کانفیگ دانلود شد");
|
||||||
|
};
|
||||||
|
|
||||||
const [username, setUsername] = useState("");
|
const [username, setUsername] = useState("");
|
||||||
const [password, setPassword] = useState("");
|
const [password, setPassword] = useState("");
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (type === "gaming" && textCopy) {
|
if (configType === "gaming" && textCopy) {
|
||||||
const [user, pass] = textCopy.split(",");
|
const [user, pass] = textCopy.split(",");
|
||||||
setUsername(user);
|
setUsername(user);
|
||||||
setPassword(pass);
|
setPassword(pass);
|
||||||
@@ -62,7 +85,7 @@ const ConfigItem = ({
|
|||||||
setUsername("");
|
setUsername("");
|
||||||
setPassword("");
|
setPassword("");
|
||||||
}
|
}
|
||||||
}, [textCopy, type]);
|
}, [configType, textCopy]);
|
||||||
|
|
||||||
const queryUpdateConfig = useQuery({
|
const queryUpdateConfig = useQuery({
|
||||||
queryKey: ["updateConfig", isOpenDropDown || isOpen],
|
queryKey: ["updateConfig", isOpenDropDown || isOpen],
|
||||||
@@ -98,6 +121,7 @@ const ConfigItem = ({
|
|||||||
type={type}
|
type={type}
|
||||||
username={username}
|
username={username}
|
||||||
password={password}
|
password={password}
|
||||||
|
wireguardUsername={configUsername}
|
||||||
categoryId={categoryId}
|
categoryId={categoryId}
|
||||||
isLoadSpaceConfig={
|
isLoadSpaceConfig={
|
||||||
queryUpdateConfig.isLoading || queryUpdateConfig.isFetching
|
queryUpdateConfig.isLoading || queryUpdateConfig.isFetching
|
||||||
@@ -105,6 +129,20 @@ const ConfigItem = ({
|
|||||||
error={queryUpdateConfig?.error ? queryUpdateConfig?.error : null}
|
error={queryUpdateConfig?.error ? queryUpdateConfig?.error : null}
|
||||||
useMaxTraffic={useMaxTraffic}
|
useMaxTraffic={useMaxTraffic}
|
||||||
/>
|
/>
|
||||||
|
{isDns && (
|
||||||
|
<ModalDnsConfig
|
||||||
|
isOpen={isOpenDnsModal}
|
||||||
|
setIsOpen={setIsOpenDnsModal}
|
||||||
|
nameConfig={configName}
|
||||||
|
orderId={orderId}
|
||||||
|
primaryIpv4={primaryIpv4}
|
||||||
|
secondaryIpv4={secondaryIpv4}
|
||||||
|
primaryIpv6={primaryIpv6}
|
||||||
|
secondaryIpv6={secondaryIpv6}
|
||||||
|
dnsIpPending={dnsIpPending}
|
||||||
|
dnsRegistrationExpired={dnsRegistrationExpired}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<div className="relative w-full h-full rounded-md">
|
<div className="relative w-full h-full rounded-md">
|
||||||
{isDisabled || useMaxTraffic && (
|
{isDisabled || useMaxTraffic && (
|
||||||
<div className="absolute inset-0 flex items-center justify-center bg-opacity-50 rounded-md backdrop-blur-sm z-10">
|
<div className="absolute inset-0 flex items-center justify-center bg-opacity-50 rounded-md backdrop-blur-sm z-10">
|
||||||
@@ -129,7 +167,14 @@ const ConfigItem = ({
|
|||||||
{configName}
|
{configName}
|
||||||
</li>
|
</li>
|
||||||
<div className="max-md:hidden flex flex-row-reverse gap-2 justify-end items-center">
|
<div className="max-md:hidden flex flex-row-reverse gap-2 justify-end items-center">
|
||||||
{type === "gaming" ? (
|
{isDns ? (
|
||||||
|
<Button
|
||||||
|
onClick={() => setIsOpenDnsModal(true)}
|
||||||
|
extra="!m-0 !px-2 !py-1.5 shadow-md shadow-white"
|
||||||
|
>
|
||||||
|
مدیریت DNS
|
||||||
|
</Button>
|
||||||
|
) : configType === "gaming" ? (
|
||||||
<>
|
<>
|
||||||
<Button
|
<Button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@@ -152,6 +197,16 @@ const ConfigItem = ({
|
|||||||
<IoCopy />
|
<IoCopy />
|
||||||
</Button>
|
</Button>
|
||||||
</>
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{isWireGuard ? (
|
||||||
|
<Button
|
||||||
|
onClick={downloadWireGuardFile}
|
||||||
|
extra="!m-0 !px-2 !py-1.5 flex flex-row-reverse justify-center items-center gap-1 shadow-md shadow-white"
|
||||||
|
>
|
||||||
|
دانلود فایل
|
||||||
|
<IoDownload />
|
||||||
|
</Button>
|
||||||
) : (
|
) : (
|
||||||
<Button
|
<Button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@@ -164,6 +219,8 @@ const ConfigItem = ({
|
|||||||
<IoCopy />
|
<IoCopy />
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
<Button
|
<Button
|
||||||
extra="!m-0 !px-2 !py-1.5 shadow-md"
|
extra="!m-0 !px-2 !py-1.5 shadow-md"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@@ -206,16 +263,26 @@ const ConfigItem = ({
|
|||||||
</Dropdown>
|
</Dropdown>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="max-[400px]:w-full md:hidden flex flex-row-reverse gap-2 justify-end items-center">
|
<div className="w-full md:hidden flex flex-row-reverse gap-2 justify-end items-stretch">
|
||||||
|
{isDns && (
|
||||||
<Button
|
<Button
|
||||||
extra="!m-0 !px-2 max-[400px]:w-full !py-1 shadow-md !font-bold !text-black text-lg"
|
extra="!m-0 flex-1 !px-2 !py-2 shadow-md !font-bold text-sm whitespace-nowrap"
|
||||||
|
onClick={() => setIsOpenDnsModal(true)}
|
||||||
|
>
|
||||||
|
مدیریت DNS
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
{!isDns && (
|
||||||
|
<Button
|
||||||
|
extra="!m-0 flex-1 !px-2 !py-2 shadow-md !font-bold !text-black text-sm whitespace-nowrap"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setConfigId(id);
|
setConfigId(id);
|
||||||
setIsOpen(true);
|
setIsOpen(true);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
مشاهده مشخصات سرویس
|
مشخصات سرویس
|
||||||
</Button>
|
</Button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Dispatch, SetStateAction, useState } from "react";
|
import { Dispatch, SetStateAction, useState } from "react";
|
||||||
import MuiModal from "../../../../common/modal";
|
import MuiModal from "../../../../common/modal";
|
||||||
import Button from "../../../../components/button";
|
import Button from "../../../../components/button";
|
||||||
import { IoCopy } from "react-icons/io5";
|
import { IoCopy, IoDownload } from "react-icons/io5";
|
||||||
import Alert from "../../../../components/alert/Alert";
|
import Alert from "../../../../components/alert/Alert";
|
||||||
import ProgressSpace from "../../../../components/progressSpace";
|
import ProgressSpace from "../../../../components/progressSpace";
|
||||||
import toast from "react-hot-toast";
|
import toast from "react-hot-toast";
|
||||||
@@ -11,6 +11,7 @@ import { setCategoryId } from "../../../../redux/slices/categoryId";
|
|||||||
import Input from "../../../../components/input";
|
import Input from "../../../../components/input";
|
||||||
import LoaderSpaceConfig from "./LoaderSpaceConfig";
|
import LoaderSpaceConfig from "./LoaderSpaceConfig";
|
||||||
import { AxiosError } from "axios";
|
import { AxiosError } from "axios";
|
||||||
|
import { downloadWireGuardConfig } from "../../utils/wireguardConfig";
|
||||||
|
|
||||||
const ModalConfigDetails = ({
|
const ModalConfigDetails = ({
|
||||||
isOpen,
|
isOpen,
|
||||||
@@ -25,6 +26,7 @@ const ModalConfigDetails = ({
|
|||||||
type,
|
type,
|
||||||
username,
|
username,
|
||||||
password,
|
password,
|
||||||
|
wireguardUsername,
|
||||||
categoryId,
|
categoryId,
|
||||||
isLoadSpaceConfig,
|
isLoadSpaceConfig,
|
||||||
error,
|
error,
|
||||||
@@ -39,18 +41,20 @@ const ModalConfigDetails = ({
|
|||||||
textCopy: string;
|
textCopy: string;
|
||||||
maxTraffic: number | "unlimited";
|
maxTraffic: number | "unlimited";
|
||||||
traffic: number;
|
traffic: number;
|
||||||
type: string;
|
type: string | null;
|
||||||
username: string;
|
username: string;
|
||||||
password: string;
|
password: string;
|
||||||
|
wireguardUsername?: string | null;
|
||||||
categoryId: number;
|
categoryId: number;
|
||||||
isLoadSpaceConfig: boolean;
|
isLoadSpaceConfig: boolean;
|
||||||
error: AxiosError | any;
|
error: AxiosError | Error | null;
|
||||||
useMaxTraffic: boolean;
|
useMaxTraffic: boolean;
|
||||||
}) => {
|
}) => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const [_, setCopyText] = useState<string>("");
|
|
||||||
const [isCopyError, setIsCopyError] = useState(false);
|
const [isCopyError, setIsCopyError] = useState(false);
|
||||||
|
const configType = type ?? "";
|
||||||
|
const isWireGuard = configType.toLowerCase() === "wireguard";
|
||||||
|
|
||||||
const copyToClipboard = async (textToCopy: string) => {
|
const copyToClipboard = async (textToCopy: string) => {
|
||||||
try {
|
try {
|
||||||
@@ -62,6 +66,16 @@ const ModalConfigDetails = ({
|
|||||||
toast.error(`Failed to copy: , ${err}`);
|
toast.error(`Failed to copy: , ${err}`);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
const downloadWireGuardFile = () => {
|
||||||
|
if (!textCopy) {
|
||||||
|
toast.error("کانفیگ وایرگارد موجود نیست");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
downloadWireGuardConfig(textCopy, wireguardUsername);
|
||||||
|
toast.success("فایل کانفیگ دانلود شد");
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MuiModal
|
<MuiModal
|
||||||
isOpenModal={isOpen}
|
isOpenModal={isOpen}
|
||||||
@@ -89,7 +103,7 @@ const ModalConfigDetails = ({
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
{type === "gaming" ? (
|
{configType === "gaming" ? (
|
||||||
<>
|
<>
|
||||||
{isCopyError ? (
|
{isCopyError ? (
|
||||||
<div className="w-full h-full flex flex-col gap-2">
|
<div className="w-full h-full flex flex-col gap-2">
|
||||||
@@ -112,7 +126,6 @@ const ModalConfigDetails = ({
|
|||||||
<>
|
<>
|
||||||
<Button
|
<Button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setCopyText(password);
|
|
||||||
copyToClipboard(password);
|
copyToClipboard(password);
|
||||||
}}
|
}}
|
||||||
extra="!m-0 !px-2 !py-1.5 flex flex-row-reverse justify-center items-center gap-1 shadow-md shadow-white"
|
extra="!m-0 !px-2 !py-1.5 flex flex-row-reverse justify-center items-center gap-1 shadow-md shadow-white"
|
||||||
@@ -122,7 +135,6 @@ const ModalConfigDetails = ({
|
|||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setCopyText(username);
|
|
||||||
copyToClipboard(username);
|
copyToClipboard(username);
|
||||||
}}
|
}}
|
||||||
extra="!m-0 !px-2 !py-1.5 flex flex-row-reverse justify-center items-center gap-1 shadow-md shadow-white"
|
extra="!m-0 !px-2 !py-1.5 flex flex-row-reverse justify-center items-center gap-1 shadow-md shadow-white"
|
||||||
@@ -150,10 +162,19 @@ const ModalConfigDetails = ({
|
|||||||
></textarea>
|
></textarea>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{isWireGuard ? (
|
||||||
|
<Button
|
||||||
|
onClick={downloadWireGuardFile}
|
||||||
|
extra="!m-0 !px-2 !py-1.5 flex flex-row-reverse justify-center items-center gap-1 shadow-md shadow-white"
|
||||||
|
>
|
||||||
|
دانلود فایل
|
||||||
|
<IoDownload />
|
||||||
|
</Button>
|
||||||
) : (
|
) : (
|
||||||
<Button
|
<Button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setCopyText(textCopy);
|
|
||||||
copyToClipboard(textCopy);
|
copyToClipboard(textCopy);
|
||||||
}}
|
}}
|
||||||
extra="!m-0 !px-2 !py-1.5 flex flex-row-reverse justify-center items-center gap-1 shadow-md shadow-white"
|
extra="!m-0 !px-2 !py-1.5 flex flex-row-reverse justify-center items-center gap-1 shadow-md shadow-white"
|
||||||
@@ -164,6 +185,8 @@ const ModalConfigDetails = ({
|
|||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
<Button
|
<Button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (categoryId) {
|
if (categoryId) {
|
||||||
|
|||||||
239
src/pages/profile/section/components/ModalDnsConfig.tsx
Normal file
239
src/pages/profile/section/components/ModalDnsConfig.tsx
Normal file
@@ -0,0 +1,239 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
import toast from "react-hot-toast";
|
||||||
|
import { IoCopyOutline, IoWifiOutline } from "react-icons/io5";
|
||||||
|
import { MdOutlineDns } from "react-icons/md";
|
||||||
|
import { FaBolt } from "react-icons/fa6";
|
||||||
|
import { BsShieldExclamation } from "react-icons/bs";
|
||||||
|
import { useMutation } from "@tanstack/react-query";
|
||||||
|
import { AxiosError } from "axios";
|
||||||
|
import MuiModal from "../../../../common/modal";
|
||||||
|
import Button from "../../../../components/button";
|
||||||
|
import Input from "../../../../components/input";
|
||||||
|
import Alert from "../../../../components/alert/Alert";
|
||||||
|
import { ModalDnsConfigProps } from "../../../../types/props";
|
||||||
|
import useAxios from "../../../../hooks/useAxios";
|
||||||
|
import { queryClient } from "../../../../main";
|
||||||
|
|
||||||
|
const IP_ADDRESS_REGEX =
|
||||||
|
/^(((\d{1,3}\.){3}\d{1,3})|([0-9a-fA-F:]+:+[0-9a-fA-F:]+))$/;
|
||||||
|
|
||||||
|
const CLIENT_IP_ENDPOINTS = [
|
||||||
|
{
|
||||||
|
url: "https://api64.ipify.org?format=json",
|
||||||
|
parse: async (res: Response) => (await res.json())?.ip,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
url: "https://api.ipify.org?format=json",
|
||||||
|
parse: async (res: Response) => (await res.json())?.ip,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
url: "https://icanhazip.com",
|
||||||
|
parse: async (res: Response) => (await res.text()).trim(),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const getErrorMessage = (err: AxiosError) => {
|
||||||
|
const responseData = err.response?.data;
|
||||||
|
if (
|
||||||
|
responseData &&
|
||||||
|
typeof responseData === "object" &&
|
||||||
|
"alert" in responseData
|
||||||
|
) {
|
||||||
|
return (responseData as { alert: string }).alert;
|
||||||
|
}
|
||||||
|
return "یک خطای غیرمنتظره رخ داد";
|
||||||
|
};
|
||||||
|
|
||||||
|
const ModalDnsConfig = ({
|
||||||
|
isOpen,
|
||||||
|
setIsOpen,
|
||||||
|
nameConfig,
|
||||||
|
orderId,
|
||||||
|
primaryIpv4,
|
||||||
|
secondaryIpv4,
|
||||||
|
primaryIpv6,
|
||||||
|
secondaryIpv6,
|
||||||
|
dnsIpPending,
|
||||||
|
dnsRegistrationExpired,
|
||||||
|
}: ModalDnsConfigProps) => {
|
||||||
|
const axios = useAxios();
|
||||||
|
const [ip, setIp] = useState("");
|
||||||
|
const [isDetecting, setIsDetecting] = useState(false);
|
||||||
|
|
||||||
|
const copyToClipboard = async (label: string, textToCopy?: string | null) => {
|
||||||
|
if (!textToCopy) return;
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(textToCopy);
|
||||||
|
toast.success(`${label} کپی شد!`);
|
||||||
|
} catch (err) {
|
||||||
|
toast.error(`کپی انجام نشد: ${err}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const registerIpMutation = useMutation({
|
||||||
|
mutationKey: ["registerDnsIp", orderId],
|
||||||
|
mutationFn: (ipToRegister: string) => {
|
||||||
|
return axios.post(`/profile/dns/${orderId}/register-ip`, {
|
||||||
|
ip: ipToRegister,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["profile"] });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const registerIp = async (ipToRegister: string) => {
|
||||||
|
const normalizedIp = ipToRegister.trim();
|
||||||
|
if (!orderId) {
|
||||||
|
toast.error("شناسه سفارش نامعتبر است");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!IP_ADDRESS_REGEX.test(normalizedIp)) {
|
||||||
|
toast.error("آیپی وارد شده معتبر نیست");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setIp(normalizedIp);
|
||||||
|
try {
|
||||||
|
await toast.promise(registerIpMutation.mutateAsync(normalizedIp), {
|
||||||
|
loading: "درحال ذخیره آیپی...",
|
||||||
|
success: (res) => res?.data?.alert || "آیپی با موفقیت ذخیره شد",
|
||||||
|
error: (err: AxiosError) => getErrorMessage(err),
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAutoDetectIp = async () => {
|
||||||
|
setIsDetecting(true);
|
||||||
|
try {
|
||||||
|
for (const endpoint of CLIENT_IP_ENDPOINTS) {
|
||||||
|
try {
|
||||||
|
const res = await fetch(endpoint.url, { cache: "no-store" });
|
||||||
|
if (!res.ok) continue;
|
||||||
|
|
||||||
|
const detectedIp = await endpoint.parse(res);
|
||||||
|
if (detectedIp && IP_ADDRESS_REGEX.test(detectedIp)) {
|
||||||
|
setIp(detectedIp);
|
||||||
|
toast.success("آیپی کاربر دریافت شد");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
toast.error("دریافت آیپی خودکار ممکن نشد");
|
||||||
|
} catch (err) {
|
||||||
|
toast.error(`دریافت آیپی خودکار با خطا مواجه شد: ${err}`);
|
||||||
|
} finally {
|
||||||
|
setIsDetecting(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const ipRows: { label: string; badge: string; value?: string | null }[] = [
|
||||||
|
{ label: "DNS نسل 4", badge: "v4", value: primaryIpv4 },
|
||||||
|
{ label: "DNS نسل 4", badge: "v4", value: secondaryIpv4 },
|
||||||
|
{ label: "DNS نسل 6", badge: "v6", value: primaryIpv6 },
|
||||||
|
{ label: "DNS نسل 6", badge: "v6", value: secondaryIpv6 },
|
||||||
|
].filter((row) => row.value);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MuiModal isOpenModal={isOpen} setIsOpenModal={setIsOpen} nameService={nameConfig}>
|
||||||
|
<div className="w-full h-full flex flex-col gap-4 max-sm:text-xs max-md:text-sm text-base">
|
||||||
|
{dnsRegistrationExpired && (
|
||||||
|
<Alert type="danger" extra="!mb-0 !rounded-lg">
|
||||||
|
مهلت ثبت آیپی برای این سرویس به پایان رسیده است
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
{dnsIpPending && !dnsRegistrationExpired && (
|
||||||
|
<Alert type="warning" extra="!mb-0 !rounded-lg">
|
||||||
|
ثبت آیپی این سرویس در انتظار تایید است
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{ipRows.length > 0 && (
|
||||||
|
<div className="w-full flex flex-col gap-2">
|
||||||
|
<div className="flex flex-row-reverse items-center gap-1.5 text-secondTheme font-bold">
|
||||||
|
<MdOutlineDns className="w-4 h-4" />
|
||||||
|
<span>آدرسهای DNS</span>
|
||||||
|
</div>
|
||||||
|
<div className="w-full flex flex-col gap-2">
|
||||||
|
{ipRows.map((row, index) => (
|
||||||
|
<div
|
||||||
|
key={`${row.badge}-${row.value}-${index}`}
|
||||||
|
className="group w-full flex flex-col gap-1.5 bg-gray-900/60 border border-gray-700 hover:border-secondTheme/50 transition-colors rounded-lg p-2.5"
|
||||||
|
>
|
||||||
|
<div className="w-full flex flex-row-reverse items-center justify-between gap-2">
|
||||||
|
<div className="flex flex-row-reverse items-center gap-2 min-w-0">
|
||||||
|
<span className="shrink-0 text-[10px] md:text-xs font-bold px-2 py-0.5 rounded-full bg-secondTheme/10 text-secondTheme border border-secondTheme/30">
|
||||||
|
{row.badge}
|
||||||
|
</span>
|
||||||
|
<span className="text-gray-300 whitespace-nowrap">{row.label}</span>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={() => copyToClipboard(row.label, row.value)}
|
||||||
|
className="shrink-0 p-2 rounded-md bg-gray-800 text-gray-300 hover:bg-secondTheme hover:text-gray-900 transition-colors"
|
||||||
|
title="کپی"
|
||||||
|
>
|
||||||
|
<IoCopyOutline className="w-4 h-4" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="w-full text-start break-all font-mono text-secondTheme/90"
|
||||||
|
dir="ltr"
|
||||||
|
>
|
||||||
|
{row.value}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="w-full flex flex-row-reverse items-start gap-2 bg-yellow-500/10 border border-yellow-500/30 text-yellow-300 rounded-lg p-3">
|
||||||
|
<BsShieldExclamation className="w-8 h-8 md:w-5 md:h-5 shrink-0 mt-0.5" />
|
||||||
|
<span className="text-start leading-6">
|
||||||
|
در صورت روشن بودن فیلترشکن (VPN)، برای ثبت صحیح آیپی، لطفا ابتدا آن را خاموش کنید.
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="w-full flex flex-col gap-2.5 bg-gray-900/60 border border-gray-700 rounded-lg p-3">
|
||||||
|
<div className="flex flex-row-reverse items-center gap-1.5 text-secondTheme font-bold">
|
||||||
|
<IoWifiOutline className="w-4 h-4" />
|
||||||
|
<span>ثبت آیپی</span>
|
||||||
|
</div>
|
||||||
|
<div className="w-full flex flex-row-reverse gap-2">
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
dir="ltr"
|
||||||
|
placeholder="آیپی خود را وارد کنید"
|
||||||
|
value={ip}
|
||||||
|
onChange={(e) => setIp(e.target.value)}
|
||||||
|
extra="!text-start"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
onClick={() => registerIp(ip)}
|
||||||
|
disabled={registerIpMutation.isPending || !ip.trim()}
|
||||||
|
extra="!m-0 !px-3 shadow-md shrink-0 disabled:!opacity-50"
|
||||||
|
>
|
||||||
|
ذخیره آیپی
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
onClick={handleAutoDetectIp}
|
||||||
|
disabled={isDetecting || registerIpMutation.isPending}
|
||||||
|
extra="!m-0 !px-3 !bg-secondTheme !text-gray-900 font-bold shadow-md shadow-secondTheme/30 hover:!bg-secondTheme/80 flex flex-row-reverse justify-center items-center gap-2 disabled:!opacity-50"
|
||||||
|
>
|
||||||
|
<FaBolt className={`w-4 h-4 ${isDetecting ? "animate-pulse" : ""}`} />
|
||||||
|
{isDetecting ? "در حال تشخیص..." : "دریافت آیپی کاربر"}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</MuiModal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ModalDnsConfig;
|
||||||
71
src/pages/profile/utils/wireguardConfig.ts
Normal file
71
src/pages/profile/utils/wireguardConfig.ts
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
const WIREGUARD_FIELDS = [
|
||||||
|
"Address",
|
||||||
|
"PrivateKey",
|
||||||
|
"DNS",
|
||||||
|
"MTU",
|
||||||
|
"PublicKey",
|
||||||
|
"AllowedIPs",
|
||||||
|
"Endpoint",
|
||||||
|
];
|
||||||
|
|
||||||
|
export const formatWireGuardConfig = (config: string) => {
|
||||||
|
const fieldPattern = new RegExp(
|
||||||
|
`\\s+(${WIREGUARD_FIELDS.join("|")})\\s*=`,
|
||||||
|
"g"
|
||||||
|
);
|
||||||
|
|
||||||
|
const lines = config
|
||||||
|
.replace(/\r\n?/g, "\n")
|
||||||
|
.replace(/\s*(\[(?:Interface|Peer)\])\s*/g, "\n$1\n")
|
||||||
|
.replace(fieldPattern, "\n$1 =")
|
||||||
|
.split("\n")
|
||||||
|
.map((line) => line.trim())
|
||||||
|
.filter(Boolean);
|
||||||
|
|
||||||
|
const formattedLines = lines.reduce<string[]>((result, line) => {
|
||||||
|
if (line.startsWith("[") && result.length > 0) {
|
||||||
|
result.push("");
|
||||||
|
}
|
||||||
|
|
||||||
|
result.push(line);
|
||||||
|
return result;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return `${formattedLines.join("\n")}\n`;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getWireGuardConfigFileName = (
|
||||||
|
username: string | null | undefined
|
||||||
|
) => {
|
||||||
|
const randomTwoDigitNumber = Math.floor(Math.random() * 90) + 10;
|
||||||
|
const defaultName = `persianping${randomTwoDigitNumber}`;
|
||||||
|
const baseName = (username?.trim() || defaultName)
|
||||||
|
.trim()
|
||||||
|
.replace(/[\\/:*?"<>|]+/g, "-")
|
||||||
|
.replace(/\s+/g, "-");
|
||||||
|
|
||||||
|
const safeName = baseName || defaultName;
|
||||||
|
|
||||||
|
return safeName.toLowerCase().endsWith(".conf")
|
||||||
|
? safeName
|
||||||
|
: `${safeName}.conf`;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const downloadWireGuardConfig = (
|
||||||
|
config: string,
|
||||||
|
username: string | null | undefined
|
||||||
|
) => {
|
||||||
|
const fileName = getWireGuardConfigFileName(username);
|
||||||
|
const file = new Blob([formatWireGuardConfig(config)], {
|
||||||
|
type: "application/octet-stream",
|
||||||
|
});
|
||||||
|
const url = URL.createObjectURL(file);
|
||||||
|
const link = document.createElement("a");
|
||||||
|
|
||||||
|
link.href = url;
|
||||||
|
link.download = fileName;
|
||||||
|
document.body.appendChild(link);
|
||||||
|
link.click();
|
||||||
|
link.remove();
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
};
|
||||||
@@ -60,6 +60,14 @@ const ListRules = () => {
|
|||||||
نخواهد شد.
|
نخواهد شد.
|
||||||
</p>
|
</p>
|
||||||
</li>
|
</li>
|
||||||
|
<li className="flex flex-row mt-2 gap-2 w-full bg-gray-800 p-2 rounded-md items-center">
|
||||||
|
<img src={Warning} alt="warning" className="w-[2rem]" />
|
||||||
|
<p className="font-normal max-sm:text-xs max-md:text-sm md:text-lg text-start">
|
||||||
|
بهدلیل برخی پروتکلهای امنیتی و بهمنظور جلوگیری از سوءاستفاده،
|
||||||
|
برای خریدهای با مبلغ بیش از ۲۰ میلیون تومان، احراز هویت پیشرفته
|
||||||
|
انجام میشود.
|
||||||
|
</p>
|
||||||
|
</li>
|
||||||
<li className="flex flex-row mt-2 gap-2 w-full bg-gray-800 p-2 rounded-md items-center">
|
<li className="flex flex-row mt-2 gap-2 w-full bg-gray-800 p-2 rounded-md items-center">
|
||||||
<img src={Warning} alt="warning" className="w-[2rem]" />
|
<img src={Warning} alt="warning" className="w-[2rem]" />
|
||||||
<p className="font-normal max-sm:text-xs max-md:text-sm md:text-lg text-start">
|
<p className="font-normal max-sm:text-xs max-md:text-sm md:text-lg text-start">
|
||||||
|
|||||||
@@ -53,7 +53,10 @@ const ProductList = () => {
|
|||||||
activeId={activeId}
|
activeId={activeId}
|
||||||
setActiveId={setActiveId}
|
setActiveId={setActiveId}
|
||||||
/>
|
/>
|
||||||
<ListAllProduct activeId={activeId} />
|
<ListAllProduct
|
||||||
|
activeId={activeId}
|
||||||
|
categories={data?.data?.result?.categories}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -9,11 +9,25 @@ import { RootState } from "../../../../../redux/store";
|
|||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import toast from "react-hot-toast";
|
import toast from "react-hot-toast";
|
||||||
|
|
||||||
const ListAllProduct = ({ activeId }: { activeId: number }) => {
|
interface Category {
|
||||||
|
id: number | string;
|
||||||
|
label?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ListAllProduct = ({
|
||||||
|
activeId,
|
||||||
|
categories,
|
||||||
|
}: {
|
||||||
|
activeId: number;
|
||||||
|
categories?: Category[];
|
||||||
|
}) => {
|
||||||
const axios = useAxios();
|
const axios = useAxios();
|
||||||
const [selectedConfigId, setSelectedConfigId] = useState<number | null>(null);
|
const [selectedConfigId, setSelectedConfigId] = useState<number | null>(null);
|
||||||
const { token } = useSelector((state: RootState) => state.token);
|
const { token } = useSelector((state: RootState) => state.token);
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const isDnsCategory =
|
||||||
|
categories?.find((category) => Number(category.id) === activeId)?.label?.toLowerCase() ===
|
||||||
|
"dns";
|
||||||
const { data, error, isLoading, isFetching } = useQuery({
|
const { data, error, isLoading, isFetching } = useQuery({
|
||||||
queryKey: ["configs", activeId],
|
queryKey: ["configs", activeId],
|
||||||
queryFn: () => {
|
queryFn: () => {
|
||||||
@@ -70,6 +84,7 @@ const ListAllProduct = ({ activeId }: { activeId: number }) => {
|
|||||||
isOpen={selectedConfigId !== null}
|
isOpen={selectedConfigId !== null}
|
||||||
setIsOpen={(open) => { if (!open) setSelectedConfigId(null); }}
|
setIsOpen={(open) => { if (!open) setSelectedConfigId(null); }}
|
||||||
configId={selectedConfigId}
|
configId={selectedConfigId}
|
||||||
|
isDns={isDnsCategory}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -8,21 +8,33 @@ import toast from "react-hot-toast";
|
|||||||
import Alert from "../../../../../components/alert/Alert";
|
import Alert from "../../../../../components/alert/Alert";
|
||||||
import { useFormik } from "formik";
|
import { useFormik } from "formik";
|
||||||
import Input from "../../../../../components/input";
|
import Input from "../../../../../components/input";
|
||||||
import { BiMinus, BiPlus } from "react-icons/bi";
|
import { BiInfoCircle, BiMinus, BiPlus } from "react-icons/bi";
|
||||||
|
import ModalDnsServiceInfo from "./ModalDnsServiceInfo";
|
||||||
import { queryClient } from "../../../../../main";
|
import { queryClient } from "../../../../../main";
|
||||||
import { AxiosError, AxiosResponse } from "axios";
|
import { AxiosError, AxiosResponse } from "axios";
|
||||||
import { useCallback, useEffect, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import { debounce } from "@mui/material";
|
import { debounce } from "@mui/material";
|
||||||
import { useDispatch } from "react-redux";
|
import { useDispatch } from "react-redux";
|
||||||
import { setIsPaymentGateway } from "../../../../../redux/slices/isPaymentGateway";
|
import { setIsPaymentGateway } from "../../../../../redux/slices/isPaymentGateway";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
|
import {
|
||||||
|
ADVANCED_VERIFICATION_PRICE_LIMIT,
|
||||||
|
ADVANCED_VERIFICATION_ROUTE,
|
||||||
|
buildAdvancedVerificationMessage,
|
||||||
|
} from "../../../../advancedVerification/constants";
|
||||||
|
|
||||||
const ModalBuy = ({ isOpen, setIsOpen, configId }: ModalBuyProps) => {
|
const ModalBuy = ({ isOpen, setIsOpen, configId, isDns }: ModalBuyProps) => {
|
||||||
const axios = useAxios();
|
const axios = useAxios();
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const separateByThree = (num: number): string => {
|
const separateByThree = (num: number): string => {
|
||||||
return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
|
return (num ?? 0).toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
|
||||||
|
};
|
||||||
|
const toNumber = (value: unknown): number => {
|
||||||
|
const normalizedValue =
|
||||||
|
typeof value === "string" ? value.replace(/,/g, "") : value;
|
||||||
|
const parsedValue = Number(normalizedValue);
|
||||||
|
return Number.isFinite(parsedValue) ? parsedValue : 0;
|
||||||
};
|
};
|
||||||
const { data, error, isLoading, isFetching, refetch } = useQuery({
|
const { data, error, isLoading, isFetching, refetch } = useQuery({
|
||||||
queryKey: ["configSee", configId],
|
queryKey: ["configSee", configId],
|
||||||
@@ -32,6 +44,34 @@ const ModalBuy = ({ isOpen, setIsOpen, configId }: ModalBuyProps) => {
|
|||||||
enabled: isOpen,
|
enabled: isOpen,
|
||||||
});
|
});
|
||||||
const [price, setPrice] = useState(0);
|
const [price, setPrice] = useState(0);
|
||||||
|
const [isInfoOpen, setIsInfoOpen] = useState(false);
|
||||||
|
const payablePrice = toNumber(
|
||||||
|
price === 0 ? data?.data?.result?.price || 0 : price
|
||||||
|
);
|
||||||
|
const advancedVerificationLimit = toNumber(
|
||||||
|
data?.data?.result?.advanced_verification_limit ??
|
||||||
|
ADVANCED_VERIFICATION_PRICE_LIMIT
|
||||||
|
);
|
||||||
|
const advancedVerificationMessage =
|
||||||
|
buildAdvancedVerificationMessage(advancedVerificationLimit);
|
||||||
|
const advancedVerificationFromApi =
|
||||||
|
data?.data?.result?.requires_advanced_verification;
|
||||||
|
const hasAdvancedVerificationApiFlag =
|
||||||
|
advancedVerificationFromApi !== undefined &&
|
||||||
|
advancedVerificationFromApi !== null;
|
||||||
|
const requiresAdvancedVerification =
|
||||||
|
hasAdvancedVerificationApiFlag
|
||||||
|
? Boolean(advancedVerificationFromApi)
|
||||||
|
: payablePrice > advancedVerificationLimit;
|
||||||
|
const goToAdvancedVerification = () => {
|
||||||
|
setIsOpen(false);
|
||||||
|
navigate(ADVANCED_VERIFICATION_ROUTE, {
|
||||||
|
state: {
|
||||||
|
payablePrice,
|
||||||
|
limit: advancedVerificationLimit,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const formik = useFormik({
|
const formik = useFormik({
|
||||||
initialValues: {
|
initialValues: {
|
||||||
@@ -48,13 +88,15 @@ const ModalBuy = ({ isOpen, setIsOpen, configId }: ModalBuyProps) => {
|
|||||||
console.log(values);
|
console.log(values);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
const { setFieldValue } = formik;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isOpen) {
|
if (!isOpen) {
|
||||||
formik.setFieldValue("count", 1);
|
setFieldValue("count", 1);
|
||||||
formik.setFieldValue("discount", "");
|
setFieldValue("discount", "");
|
||||||
setPrice(0);
|
setPrice(0);
|
||||||
}
|
}
|
||||||
}, [isOpen]);
|
}, [isOpen, setFieldValue]);
|
||||||
const mutation = useMutation({
|
const mutation = useMutation({
|
||||||
mutationKey: ["buyConfig"],
|
mutationKey: ["buyConfig"],
|
||||||
mutationFn: (values: BuyConfigDataProps) => {
|
mutationFn: (values: BuyConfigDataProps) => {
|
||||||
@@ -81,7 +123,8 @@ const ModalBuy = ({ isOpen, setIsOpen, configId }: ModalBuyProps) => {
|
|||||||
return `${res.data.alert}`;
|
return `${res.data.alert}`;
|
||||||
},
|
},
|
||||||
error: (err: AxiosError) => {
|
error: (err: AxiosError) => {
|
||||||
formik.setFieldValue("discount", "");
|
setFieldValue("discount", "");
|
||||||
|
setPrice(0);
|
||||||
let errorMessage = "یک خطای غیرمنتظره رخ داد";
|
let errorMessage = "یک خطای غیرمنتظره رخ داد";
|
||||||
if (
|
if (
|
||||||
err.response?.data &&
|
err.response?.data &&
|
||||||
@@ -94,7 +137,8 @@ const ModalBuy = ({ isOpen, setIsOpen, configId }: ModalBuyProps) => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
const chnageCountDebouncedMutation = useCallback(
|
const chnageCountDebouncedMutation = useMemo(
|
||||||
|
() =>
|
||||||
debounce((values) => {
|
debounce((values) => {
|
||||||
toast.promise(mutation.mutateAsync(values), {
|
toast.promise(mutation.mutateAsync(values), {
|
||||||
loading: "درحال بررسی...",
|
loading: "درحال بررسی...",
|
||||||
@@ -103,7 +147,8 @@ const ModalBuy = ({ isOpen, setIsOpen, configId }: ModalBuyProps) => {
|
|||||||
return `${res.data.alert}`;
|
return `${res.data.alert}`;
|
||||||
},
|
},
|
||||||
error: (err: AxiosError) => {
|
error: (err: AxiosError) => {
|
||||||
formik.setFieldValue("discount", "");
|
setFieldValue("discount", "");
|
||||||
|
setPrice(0);
|
||||||
refetch();
|
refetch();
|
||||||
let errorMessage = "یک خطای غیرمنتظره رخ داد";
|
let errorMessage = "یک خطای غیرمنتظره رخ داد";
|
||||||
if (
|
if (
|
||||||
@@ -117,17 +162,17 @@ const ModalBuy = ({ isOpen, setIsOpen, configId }: ModalBuyProps) => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
}, 1000),
|
}, 1000),
|
||||||
[]
|
[mutation, refetch, setFieldValue]
|
||||||
);
|
);
|
||||||
const chnageCountHandler = (status: "inc" | "dec") => {
|
const chnageCountHandler = (status: "inc" | "dec") => {
|
||||||
if (status === "inc") {
|
if (status === "inc") {
|
||||||
formik.setFieldValue("count", formik.values.count + 1);
|
setFieldValue("count", formik.values.count + 1);
|
||||||
chnageCountDebouncedMutation({
|
chnageCountDebouncedMutation({
|
||||||
...formik.values,
|
...formik.values,
|
||||||
count: formik.values.count + 1,
|
count: formik.values.count + 1,
|
||||||
});
|
});
|
||||||
} else if (status === "dec") {
|
} else if (status === "dec") {
|
||||||
formik.setFieldValue("count", formik.values.count - 1);
|
setFieldValue("count", formik.values.count - 1);
|
||||||
chnageCountDebouncedMutation({
|
chnageCountDebouncedMutation({
|
||||||
...formik.values,
|
...formik.values,
|
||||||
count: formik.values.count - 1,
|
count: formik.values.count - 1,
|
||||||
@@ -136,6 +181,11 @@ const ModalBuy = ({ isOpen, setIsOpen, configId }: ModalBuyProps) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const submitHandler = () => {
|
const submitHandler = () => {
|
||||||
|
if (requiresAdvancedVerification) {
|
||||||
|
goToAdvancedVerification();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
console.log(formik.values);
|
console.log(formik.values);
|
||||||
toast.promise(mutation.mutateAsync({ ...formik.values, status: "buy" }), {
|
toast.promise(mutation.mutateAsync({ ...formik.values, status: "buy" }), {
|
||||||
loading: "درحال انتقال به درگاه پرداخت...",
|
loading: "درحال انتقال به درگاه پرداخت...",
|
||||||
@@ -144,7 +194,8 @@ const ModalBuy = ({ isOpen, setIsOpen, configId }: ModalBuyProps) => {
|
|||||||
return `${res.data.alert}`;
|
return `${res.data.alert}`;
|
||||||
},
|
},
|
||||||
error: (err: AxiosError) => {
|
error: (err: AxiosError) => {
|
||||||
formik.setFieldValue("discount", "");
|
setFieldValue("discount", "");
|
||||||
|
setPrice(0);
|
||||||
let errorMessage = "یک خطای غیرمنتظره رخ داد";
|
let errorMessage = "یک خطای غیرمنتظره رخ داد";
|
||||||
if (
|
if (
|
||||||
err.response?.data &&
|
err.response?.data &&
|
||||||
@@ -153,21 +204,51 @@ const ModalBuy = ({ isOpen, setIsOpen, configId }: ModalBuyProps) => {
|
|||||||
) {
|
) {
|
||||||
errorMessage = (err.response.data as { alert: string }).alert;
|
errorMessage = (err.response.data as { alert: string }).alert;
|
||||||
}
|
}
|
||||||
toast.error(`خطایی رخ داد : ${errorMessage}`);
|
|
||||||
|
const responseData = err.response?.data;
|
||||||
|
const isResponseObject =
|
||||||
|
responseData !== null && typeof responseData === "object";
|
||||||
|
const requiresAdvancedVerificationFromApi =
|
||||||
|
isResponseObject &&
|
||||||
|
(("requires_advanced_verification" in responseData &&
|
||||||
|
Boolean(
|
||||||
|
(responseData as { requires_advanced_verification?: boolean })
|
||||||
|
.requires_advanced_verification
|
||||||
|
)) ||
|
||||||
|
("code" in responseData &&
|
||||||
|
(responseData as { code?: string }).code ===
|
||||||
|
"ADVANCED_VERIFICATION_REQUIRED"));
|
||||||
|
const isAdvancedVerificationError =
|
||||||
|
requiresAdvancedVerificationFromApi ||
|
||||||
|
(err.response?.status === 422 &&
|
||||||
|
errorMessage.includes("احراز هویت پیشرفته"));
|
||||||
|
|
||||||
|
if (isAdvancedVerificationError) {
|
||||||
|
goToAdvancedVerification();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (err.response?.status === 500) {
|
||||||
dispatch(setIsPaymentGateway(false));
|
dispatch(setIsPaymentGateway(false));
|
||||||
navigate("/noactive-payment");
|
navigate("/noactive-payment");
|
||||||
|
}
|
||||||
|
|
||||||
return errorMessage;
|
return errorMessage;
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<>
|
||||||
|
<ModalDnsServiceInfo isOpen={isInfoOpen} setIsOpen={setIsInfoOpen} />
|
||||||
<MuiModal
|
<MuiModal
|
||||||
isOpenModal={isOpen}
|
isOpenModal={isOpen}
|
||||||
setIsOpenModal={setIsOpen}
|
setIsOpenModal={setIsOpen}
|
||||||
submitText={data?.data?.result?.max_config === 0 ? undefined : "خرید"}
|
submitText={data?.data?.result?.max_config === 0 ? undefined : "خرید"}
|
||||||
submitOnClick={submitHandler}
|
submitOnClick={submitHandler}
|
||||||
disabled={isLoading || isFetching}
|
disabled={
|
||||||
|
isLoading ||
|
||||||
|
isFetching
|
||||||
|
}
|
||||||
nameService={
|
nameService={
|
||||||
isLoading || isFetching
|
isLoading || isFetching
|
||||||
? "درحال بارگذاری..."
|
? "درحال بارگذاری..."
|
||||||
@@ -212,6 +293,15 @@ const ModalBuy = ({ isOpen, setIsOpen, configId }: ModalBuyProps) => {
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="w-full h-full flex flex-col gap-3 justify-between items-center text-sm md:text-xl">
|
<div className="w-full h-full flex flex-col gap-3 justify-between items-center text-sm md:text-xl">
|
||||||
|
{isDns && (
|
||||||
|
<Button
|
||||||
|
extra="!m-0 !w-full !py-1.5 !bg-gray-800 !text-secondTheme border border-secondTheme/40 flex flex-row-reverse justify-center items-center gap-2 shadow-md text-sm md:text-base"
|
||||||
|
onClick={() => setIsInfoOpen(true)}
|
||||||
|
>
|
||||||
|
<BiInfoCircle className="w-5 h-5" />
|
||||||
|
توضیحات سرویس
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
<Alert type="danger">
|
<Alert type="danger">
|
||||||
{" "}
|
{" "}
|
||||||
کاربر گرامی لطفا از خاموش بودن vpn خود اطمینان حاصل فرمایید تا در
|
کاربر گرامی لطفا از خاموش بودن vpn خود اطمینان حاصل فرمایید تا در
|
||||||
@@ -224,11 +314,15 @@ const ModalBuy = ({ isOpen, setIsOpen, configId }: ModalBuyProps) => {
|
|||||||
<h1 className="font-bold">
|
<h1 className="font-bold">
|
||||||
مبلغ قابل پرداخت :{" "}
|
مبلغ قابل پرداخت :{" "}
|
||||||
<span className="px-[2px] rounded-md bg-secondTheme text-gray-950">
|
<span className="px-[2px] rounded-md bg-secondTheme text-gray-950">
|
||||||
{separateByThree(
|
{separateByThree(payablePrice)}
|
||||||
price === 0 ? data?.data?.result?.price || 0 : price
|
|
||||||
)}
|
|
||||||
</span>
|
</span>
|
||||||
</h1>
|
</h1>
|
||||||
|
{requiresAdvancedVerification &&
|
||||||
|
data?.data?.result?.max_config !== 0 && (
|
||||||
|
<Alert type="warning" extra="!mb-0 text-start">
|
||||||
|
{advancedVerificationMessage}
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
{data?.data?.result?.max_config === 0 ? (
|
{data?.data?.result?.max_config === 0 ? (
|
||||||
<>
|
<>
|
||||||
<Alert type="info" extra="mt-5">
|
<Alert type="info" extra="mt-5">
|
||||||
@@ -238,7 +332,7 @@ const ModalBuy = ({ isOpen, setIsOpen, configId }: ModalBuyProps) => {
|
|||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<h3 className="text-xs md:text-sm text-gray-400">
|
<h3 className="text-xs md:text-sm text-gray-400 mt-2">
|
||||||
در صورت نداشتن کد تخفیف پرداخت رو بزنید
|
در صورت نداشتن کد تخفیف پرداخت رو بزنید
|
||||||
</h3>
|
</h3>
|
||||||
<form
|
<form
|
||||||
@@ -309,7 +403,11 @@ const ModalBuy = ({ isOpen, setIsOpen, configId }: ModalBuyProps) => {
|
|||||||
</div>
|
</div>
|
||||||
<div className="w-full h-full bg-gray-900/50 rounded-md shadow flex justify-between items-center p-3">
|
<div className="w-full h-full bg-gray-900/50 rounded-md shadow flex justify-between items-center p-3">
|
||||||
<p>محدودیت کاربر</p>
|
<p>محدودیت کاربر</p>
|
||||||
<p>{data?.data?.result?.limit ?? "بدون محدودیت کاربر"}</p>
|
{!data?.data?.result?.limit ? (
|
||||||
|
<p>نامحدود</p>
|
||||||
|
) : (
|
||||||
|
<p>{data?.data?.result?.limit}</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="w-full h-full bg-gray-900/50 rounded-md shadow flex justify-between items-center p-3">
|
<div className="w-full h-full bg-gray-900/50 rounded-md shadow flex justify-between items-center p-3">
|
||||||
<p>مدت زمان</p>
|
<p>مدت زمان</p>
|
||||||
@@ -318,8 +416,15 @@ const ModalBuy = ({ isOpen, setIsOpen, configId }: ModalBuyProps) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
{data?.data?.result?.max_config !== 0 && !isLoading && !isFetching && (
|
||||||
|
<p className="w-full text-center text-xs text-gray-500 mt-1">
|
||||||
|
با خرید، <span className="text-gray-400">قوانین و مقررات</span> را
|
||||||
|
میپذیرید.
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</MuiModal>
|
</MuiModal>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,142 @@
|
|||||||
|
import { Dispatch, SetStateAction } from "react";
|
||||||
|
import MuiModal from "../../../../../common/modal";
|
||||||
|
|
||||||
|
interface ModalDnsServiceInfoProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
setIsOpen: Dispatch<SetStateAction<boolean>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const GAMES = [
|
||||||
|
"Valorant",
|
||||||
|
"Call of Duty: Warzone",
|
||||||
|
"EA SPORTS FC 25",
|
||||||
|
"EA SPORTS FC 26",
|
||||||
|
"PUBG Mobile",
|
||||||
|
"PUBG: Battlegrounds (PC)",
|
||||||
|
"Call of Duty: Mobile (CODM)",
|
||||||
|
"Fortnite",
|
||||||
|
"League of Legends (LoL)",
|
||||||
|
"Clash Royale",
|
||||||
|
"Clash of Clans",
|
||||||
|
];
|
||||||
|
|
||||||
|
const SERVICES = [
|
||||||
|
"ChatGPT",
|
||||||
|
"Discord",
|
||||||
|
"Spotify",
|
||||||
|
"NVIDIA (App & Services)",
|
||||||
|
"GitHub، Docker، NPM، PyPI و بسیاری از ابزارها و سرویسهای توسعه (Developer Services)",
|
||||||
|
];
|
||||||
|
|
||||||
|
const STREAMING = ["YouTube", "Twitch", "Kick"];
|
||||||
|
|
||||||
|
const BENEFITS = [
|
||||||
|
"بدون نیاز به VPN",
|
||||||
|
"راهاندازی در کمتر از یک دقیقه",
|
||||||
|
"مناسب برای کامپیوتر، موبایل و کنسول بازی",
|
||||||
|
"پشتیبانی و بروزرسانی مداوم سرویسها",
|
||||||
|
];
|
||||||
|
|
||||||
|
const Section = ({
|
||||||
|
emoji,
|
||||||
|
title,
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
emoji: string;
|
||||||
|
title: string;
|
||||||
|
children: React.ReactNode;
|
||||||
|
}) => (
|
||||||
|
<div className="w-full flex flex-col gap-2 bg-gray-900/60 border border-gray-700 rounded-lg p-3">
|
||||||
|
<div className="flex flex-row-reverse items-center gap-1.5 text-secondTheme font-bold">
|
||||||
|
<span>{emoji}</span>
|
||||||
|
<span>{title}</span>
|
||||||
|
</div>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
const ModalDnsServiceInfo = ({ isOpen, setIsOpen }: ModalDnsServiceInfoProps) => {
|
||||||
|
return (
|
||||||
|
<MuiModal
|
||||||
|
isOpenModal={isOpen}
|
||||||
|
setIsOpenModal={setIsOpen}
|
||||||
|
title="توضیحات سرویس"
|
||||||
|
nameService="DNS"
|
||||||
|
>
|
||||||
|
<div className="w-full h-full flex flex-col gap-3 max-sm:text-xs max-md:text-sm text-base">
|
||||||
|
<Section emoji="🎮" title="بازیهای پشتیبانیشده">
|
||||||
|
<ul className="flex flex-col gap-1.5">
|
||||||
|
{GAMES.map((game) => (
|
||||||
|
<li
|
||||||
|
key={game}
|
||||||
|
className="flex flex-row-reverse items-center gap-2 text-gray-200"
|
||||||
|
>
|
||||||
|
<span>🔹</span>
|
||||||
|
<span dir="ltr" className="text-start">
|
||||||
|
{game}
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</Section>
|
||||||
|
|
||||||
|
<Section emoji="💻" title="سرویسها و پلتفرمها">
|
||||||
|
<ul className="flex flex-col gap-1.5">
|
||||||
|
{SERVICES.map((service) => (
|
||||||
|
<li
|
||||||
|
key={service}
|
||||||
|
className="flex flex-row-reverse items-start gap-2 text-gray-200"
|
||||||
|
>
|
||||||
|
<span className="shrink-0">✔️</span>
|
||||||
|
<span className="text-start leading-6">{service}</span>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</Section>
|
||||||
|
|
||||||
|
<Section emoji="📺" title="سرویسهای استریم">
|
||||||
|
<ul className="flex flex-col gap-1.5">
|
||||||
|
{STREAMING.map((item) => (
|
||||||
|
<li
|
||||||
|
key={item}
|
||||||
|
className="flex flex-row-reverse items-center justify-between gap-2 text-gray-200"
|
||||||
|
>
|
||||||
|
<div className="flex flex-row-reverse items-center gap-2">
|
||||||
|
<span>▶️</span>
|
||||||
|
<span dir="ltr">{item}</span>
|
||||||
|
</div>
|
||||||
|
<span className="text-[10px] md:text-xs text-yellow-300/90 whitespace-nowrap">
|
||||||
|
فقط روی اینترنت خانگی
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</Section>
|
||||||
|
|
||||||
|
<Section emoji="⚡️" title="مزایای سرویس">
|
||||||
|
<ul className="flex flex-col gap-1.5">
|
||||||
|
{BENEFITS.map((benefit) => (
|
||||||
|
<li
|
||||||
|
key={benefit}
|
||||||
|
className="flex flex-row-reverse items-center gap-2 text-gray-200"
|
||||||
|
>
|
||||||
|
<span>✅</span>
|
||||||
|
<span>{benefit}</span>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</Section>
|
||||||
|
|
||||||
|
<div className="w-full flex flex-row-reverse items-start gap-2 bg-secondTheme/10 border border-secondTheme/30 text-secondTheme rounded-lg p-3">
|
||||||
|
<span className="shrink-0">📌</span>
|
||||||
|
<span className="text-start leading-6">
|
||||||
|
بازی و سرویسهای جدید بهصورت مداوم به DNS اضافه میشوند و این
|
||||||
|
لیست همواره در حال بروزرسانی است.
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</MuiModal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ModalDnsServiceInfo;
|
||||||
36
src/types/props.d.ts
vendored
36
src/types/props.d.ts
vendored
@@ -121,14 +121,35 @@ export interface MuiModalProps {
|
|||||||
|
|
||||||
export interface ConfigItemProps {
|
export interface ConfigItemProps {
|
||||||
id: string;
|
id: string;
|
||||||
|
orderId?: number;
|
||||||
dateBuy: string;
|
dateBuy: string;
|
||||||
configName: string;
|
configName: string;
|
||||||
isDisabled: boolean;
|
isDisabled: boolean;
|
||||||
isShowTraffic: boolean;
|
isShowTraffic: boolean;
|
||||||
textCopy: string;
|
textCopy: string;
|
||||||
type: string;
|
type: string | null;
|
||||||
categoryId: number;
|
categoryId: number;
|
||||||
useMaxTraffic: boolean;
|
useMaxTraffic: boolean;
|
||||||
|
username?: string | null;
|
||||||
|
primaryIpv4?: string | null;
|
||||||
|
secondaryIpv4?: string | null;
|
||||||
|
primaryIpv6?: string | null;
|
||||||
|
secondaryIpv6?: string | null;
|
||||||
|
dnsIpPending?: boolean;
|
||||||
|
dnsRegistrationExpired?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ModalDnsConfigProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
setIsOpen: Dispatch<SetStateAction<boolean>>;
|
||||||
|
nameConfig: string;
|
||||||
|
orderId?: number;
|
||||||
|
primaryIpv4?: string | null;
|
||||||
|
secondaryIpv4?: string | null;
|
||||||
|
primaryIpv6?: string | null;
|
||||||
|
secondaryIpv6?: string | null;
|
||||||
|
dnsIpPending?: boolean;
|
||||||
|
dnsRegistrationExpired?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
// نوع Item برای آیتمهای داخل Dropdown
|
// نوع Item برای آیتمهای داخل Dropdown
|
||||||
@@ -178,6 +199,7 @@ interface ModalBuyProps {
|
|||||||
// unknown
|
// unknown
|
||||||
// >;
|
// >;
|
||||||
configId: number;
|
configId: number;
|
||||||
|
isDns?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface BuyConfigDataProps {
|
interface BuyConfigDataProps {
|
||||||
@@ -195,17 +217,25 @@ type UserProfile = {
|
|||||||
|
|
||||||
// تعریف تایپ پیکربندیها
|
// تعریف تایپ پیکربندیها
|
||||||
type Config = {
|
type Config = {
|
||||||
id: number;
|
id: number | string;
|
||||||
|
order_id?: number;
|
||||||
title: string;
|
title: string;
|
||||||
url: string;
|
url: string;
|
||||||
created_at: string | null;
|
created_at: string | null;
|
||||||
show_traffic: boolean;
|
show_traffic: boolean;
|
||||||
max_traffic: number;
|
max_traffic: number;
|
||||||
traffic: number;
|
traffic: number;
|
||||||
type: string;
|
type: string | null;
|
||||||
|
username?: string | null;
|
||||||
category_id: number;
|
category_id: number;
|
||||||
is_expired: boolean;
|
is_expired: boolean;
|
||||||
use_max_traffic: boolean;
|
use_max_traffic: boolean;
|
||||||
|
primary_ipv4?: string | null;
|
||||||
|
secondary_ipv4?: string | null;
|
||||||
|
primary_ipv6?: string | null;
|
||||||
|
secondary_ipv6?: string | null;
|
||||||
|
dns_ip_pending?: boolean;
|
||||||
|
dns_registration_expired?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
// تعریف تایپ دادههای برگشتی
|
// تعریف تایپ دادههای برگشتی
|
||||||
|
|||||||
Reference in New Issue
Block a user