feat: add can_renew
This commit is contained in:
@@ -4,8 +4,9 @@ import Button from "../../../components/button";
|
|||||||
import { ProfileDetailsProps } from "../../../types/props";
|
import { ProfileDetailsProps } from "../../../types/props";
|
||||||
import ConfigItem from "./components/ConfigItem";
|
import ConfigItem from "./components/ConfigItem";
|
||||||
import ConfigItemLoading from "./components/ConfigItemLoading";
|
import ConfigItemLoading from "./components/ConfigItemLoading";
|
||||||
|
import ConfigFilterBar from "./components/ConfigFilterBar";
|
||||||
import ProfileImage from "./components/ProfileImage";
|
import ProfileImage from "./components/ProfileImage";
|
||||||
import { Fragment } from "react/jsx-runtime";
|
import { Fragment, useMemo, useState } from "react";
|
||||||
import { IoAlertCircleSharp } from "react-icons/io5";
|
import { IoAlertCircleSharp } from "react-icons/io5";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
|
|
||||||
@@ -19,6 +20,18 @@ const ProductConfigList = ({
|
|||||||
const configs = data?.result?.configs;
|
const configs = data?.result?.configs;
|
||||||
const showInitialLoading = (isLoading || isFetching) && !data;
|
const showInitialLoading = (isLoading || isFetching) && !data;
|
||||||
|
|
||||||
|
const [statusFilter, setStatusFilter] = useState<string>("all");
|
||||||
|
const [typeFilter, setTypeFilter] = useState<string>("all");
|
||||||
|
|
||||||
|
const filteredConfigs = useMemo(() => {
|
||||||
|
return configs?.filter((item) => {
|
||||||
|
const matchesStatus =
|
||||||
|
statusFilter === "all" || item.status === statusFilter;
|
||||||
|
const matchesType = typeFilter === "all" || item.type === typeFilter;
|
||||||
|
return matchesStatus && matchesType;
|
||||||
|
});
|
||||||
|
}, [configs, statusFilter, typeFilter]);
|
||||||
|
|
||||||
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 />
|
||||||
@@ -39,6 +52,17 @@ const ProductConfigList = ({
|
|||||||
لیست کانفیگ ها
|
لیست کانفیگ ها
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{!showInitialLoading && !error && configs && configs.length > 0 && (
|
||||||
|
<ConfigFilterBar
|
||||||
|
configs={configs}
|
||||||
|
statusFilter={statusFilter}
|
||||||
|
setStatusFilter={setStatusFilter}
|
||||||
|
typeFilter={typeFilter}
|
||||||
|
setTypeFilter={setTypeFilter}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
<div
|
<div
|
||||||
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"
|
||||||
@@ -68,8 +92,15 @@ const ProductConfigList = ({
|
|||||||
<Button>برو به فروشگاه</Button>
|
<Button>برو به فروشگاه</Button>
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
|
) : filteredConfigs?.length === 0 ? (
|
||||||
|
<div className="w-full h-full flex flex-col gap-3 justify-center items-center py-5">
|
||||||
|
<IoAlertCircleSharp className="max-md:w-[2.5rem] max-md:h-[2.5rem] w-[3.5rem] h-[3.5rem]" />
|
||||||
|
<p className="text-sm md:text-base">
|
||||||
|
با این فیلتر سرویسی پیدا نشد
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
) : (
|
) : (
|
||||||
configs?.map((item, index) => {
|
filteredConfigs?.map((item, index) => {
|
||||||
return (
|
return (
|
||||||
<Fragment key={`${item.id}-${index}`}>
|
<Fragment key={`${item.id}-${index}`}>
|
||||||
<ConfigItem
|
<ConfigItem
|
||||||
@@ -90,6 +121,8 @@ const ProductConfigList = ({
|
|||||||
secondaryIpv6={item.secondary_ipv6}
|
secondaryIpv6={item.secondary_ipv6}
|
||||||
dnsIpPending={item.dns_ip_pending}
|
dnsIpPending={item.dns_ip_pending}
|
||||||
dnsRegistrationExpired={item.dns_registration_expired}
|
dnsRegistrationExpired={item.dns_registration_expired}
|
||||||
|
dnsExpiresAt={item.dns_expires_at}
|
||||||
|
canRenew={item.can_renew}
|
||||||
/>
|
/>
|
||||||
</Fragment>
|
</Fragment>
|
||||||
);
|
);
|
||||||
|
|||||||
143
src/pages/profile/section/components/ConfigFilterBar.tsx
Normal file
143
src/pages/profile/section/components/ConfigFilterBar.tsx
Normal file
@@ -0,0 +1,143 @@
|
|||||||
|
import {
|
||||||
|
IoAppsOutline,
|
||||||
|
IoCheckmarkCircle,
|
||||||
|
IoCloseCircle,
|
||||||
|
IoTimeOutline,
|
||||||
|
} from "react-icons/io5";
|
||||||
|
import { Config } from "../../../../types/props";
|
||||||
|
|
||||||
|
const STATUS_META: Record<
|
||||||
|
string,
|
||||||
|
{ label: string; icon: React.ReactNode; activeClass: string }
|
||||||
|
> = {
|
||||||
|
all: {
|
||||||
|
label: "همه",
|
||||||
|
icon: <IoAppsOutline />,
|
||||||
|
activeClass: "!bg-secondTheme !text-gray-900",
|
||||||
|
},
|
||||||
|
active: {
|
||||||
|
label: "فعال",
|
||||||
|
icon: <IoCheckmarkCircle />,
|
||||||
|
activeClass: "!bg-green-600 !text-white",
|
||||||
|
},
|
||||||
|
expired: {
|
||||||
|
label: "منقضی شده",
|
||||||
|
icon: <IoCloseCircle />,
|
||||||
|
activeClass: "!bg-rose-600 !text-white",
|
||||||
|
},
|
||||||
|
pending: {
|
||||||
|
label: "در انتظار ثبت",
|
||||||
|
icon: <IoTimeOutline />,
|
||||||
|
activeClass: "!bg-yellow-400 !text-gray-900",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const TYPE_LABELS: Record<string, string> = {
|
||||||
|
eshtraki: "اشتراکی",
|
||||||
|
eshtraki_single: "اشتراک تکی",
|
||||||
|
wireguard: "وایرگارد",
|
||||||
|
dns: "DNS",
|
||||||
|
gaming: "گیمینگ",
|
||||||
|
};
|
||||||
|
|
||||||
|
const typeLabel = (type: string | null) => {
|
||||||
|
if (!type) return "سایر";
|
||||||
|
return TYPE_LABELS[type.toLowerCase()] || type;
|
||||||
|
};
|
||||||
|
|
||||||
|
interface ConfigFilterBarProps {
|
||||||
|
configs: Config[];
|
||||||
|
statusFilter: string;
|
||||||
|
setStatusFilter: (value: string) => void;
|
||||||
|
typeFilter: string;
|
||||||
|
setTypeFilter: (value: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ConfigFilterBar = ({
|
||||||
|
configs,
|
||||||
|
statusFilter,
|
||||||
|
setStatusFilter,
|
||||||
|
typeFilter,
|
||||||
|
setTypeFilter,
|
||||||
|
}: ConfigFilterBarProps) => {
|
||||||
|
const statusCounts = configs.reduce<Record<string, number>>((acc, item) => {
|
||||||
|
if (item.status) acc[item.status] = (acc[item.status] || 0) + 1;
|
||||||
|
return acc;
|
||||||
|
}, {});
|
||||||
|
const availableStatuses = Object.keys(statusCounts);
|
||||||
|
|
||||||
|
const typeCounts = configs.reduce<Record<string, number>>((acc, item) => {
|
||||||
|
if (item.type) acc[item.type] = (acc[item.type] || 0) + 1;
|
||||||
|
return acc;
|
||||||
|
}, {});
|
||||||
|
const availableTypes = Object.keys(typeCounts);
|
||||||
|
|
||||||
|
if (availableStatuses.length < 2 && availableTypes.length < 2) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="w-full flex flex-col gap-3 bg-gray-900/60 rounded-xl p-3 shadow-md shadow-gray-900">
|
||||||
|
{availableStatuses.length > 1 && (
|
||||||
|
<div className="w-full flex flex-wrap gap-2 justify-center">
|
||||||
|
{["all", ...availableStatuses].map((status) => {
|
||||||
|
const meta = STATUS_META[status] || {
|
||||||
|
label: status,
|
||||||
|
icon: null,
|
||||||
|
activeClass: "!bg-secondTheme !text-gray-900",
|
||||||
|
};
|
||||||
|
const isActive = statusFilter === status;
|
||||||
|
const count =
|
||||||
|
status === "all" ? configs.length : statusCounts[status];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={status}
|
||||||
|
type="button"
|
||||||
|
onClick={() => setStatusFilter(status)}
|
||||||
|
className={`flex items-center gap-1.5 px-3 py-1.5 rounded-full text-xs md:text-sm font-bold
|
||||||
|
transition-all duration-200 shadow-md
|
||||||
|
${isActive
|
||||||
|
? meta.activeClass
|
||||||
|
: "bg-gray-800 text-gray-300 hover:bg-gray-700"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{meta.icon}
|
||||||
|
{meta.label}
|
||||||
|
<span className="opacity-70">({count})</span>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{availableTypes.length > 1 && (
|
||||||
|
<div
|
||||||
|
className={`w-full flex flex-wrap gap-2 justify-center ${availableStatuses.length > 1 ? "pt-2 border-t border-gray-700" : ""
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{["all", ...availableTypes].map((type) => {
|
||||||
|
const isActive = typeFilter === type;
|
||||||
|
const count = type === "all" ? configs.length : typeCounts[type];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={type}
|
||||||
|
type="button"
|
||||||
|
onClick={() => setTypeFilter(type)}
|
||||||
|
className={`px-3 py-1 rounded-full text-[11px] md:text-xs font-bold transition-all duration-200
|
||||||
|
${isActive
|
||||||
|
? "!bg-secondTheme !text-gray-900 shadow-md"
|
||||||
|
: "bg-gray-800/70 text-gray-400 hover:bg-gray-700"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{type === "all" ? "همه سرویسها" : typeLabel(type)}
|
||||||
|
<span className="opacity-70"> ({count})</span>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ConfigFilterBar;
|
||||||
@@ -33,6 +33,8 @@ const ConfigItem = ({
|
|||||||
secondaryIpv6,
|
secondaryIpv6,
|
||||||
dnsIpPending,
|
dnsIpPending,
|
||||||
dnsRegistrationExpired,
|
dnsRegistrationExpired,
|
||||||
|
dnsExpiresAt,
|
||||||
|
canRenew,
|
||||||
}: ConfigItemProps) => {
|
}: ConfigItemProps) => {
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const [isOpenDnsModal, setIsOpenDnsModal] = useState(false);
|
const [isOpenDnsModal, setIsOpenDnsModal] = useState(false);
|
||||||
@@ -141,6 +143,8 @@ const ConfigItem = ({
|
|||||||
secondaryIpv6={secondaryIpv6}
|
secondaryIpv6={secondaryIpv6}
|
||||||
dnsIpPending={dnsIpPending}
|
dnsIpPending={dnsIpPending}
|
||||||
dnsRegistrationExpired={dnsRegistrationExpired}
|
dnsRegistrationExpired={dnsRegistrationExpired}
|
||||||
|
dnsExpiresAt={dnsExpiresAt}
|
||||||
|
canRenew={canRenew}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<div className="relative w-full h-full rounded-md">
|
<div className="relative w-full h-full rounded-md">
|
||||||
|
|||||||
@@ -14,23 +14,16 @@ import { ModalDnsConfigProps } from "../../../../types/props";
|
|||||||
import useAxios from "../../../../hooks/useAxios";
|
import useAxios from "../../../../hooks/useAxios";
|
||||||
import { queryClient } from "../../../../main";
|
import { queryClient } from "../../../../main";
|
||||||
|
|
||||||
const IP_ADDRESS_REGEX =
|
const IPV4_ADDRESS_REGEX = /^(\d{1,3}\.){3}\d{1,3}$/;
|
||||||
/^(((\d{1,3}\.){3}\d{1,3})|([0-9a-fA-F:]+:+[0-9a-fA-F:]+))$/;
|
|
||||||
|
|
||||||
const CLIENT_IP_ENDPOINTS = [
|
const isValidIpv4Address = (address: string) => {
|
||||||
{
|
if (!IPV4_ADDRESS_REGEX.test(address)) return false;
|
||||||
url: "https://api64.ipify.org?format=json",
|
|
||||||
parse: async (res: Response) => (await res.json())?.ip,
|
return address.split(".").every((part) => {
|
||||||
},
|
const value = Number(part);
|
||||||
{
|
return Number.isInteger(value) && value >= 0 && value <= 255;
|
||||||
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 getErrorMessage = (err: AxiosError) => {
|
||||||
const responseData = err.response?.data;
|
const responseData = err.response?.data;
|
||||||
@@ -55,11 +48,38 @@ const ModalDnsConfig = ({
|
|||||||
secondaryIpv6,
|
secondaryIpv6,
|
||||||
dnsIpPending,
|
dnsIpPending,
|
||||||
dnsRegistrationExpired,
|
dnsRegistrationExpired,
|
||||||
|
dnsExpiresAt,
|
||||||
|
canRenew,
|
||||||
}: ModalDnsConfigProps) => {
|
}: ModalDnsConfigProps) => {
|
||||||
const axios = useAxios();
|
const axios = useAxios();
|
||||||
const [ip, setIp] = useState("");
|
const [ip, setIp] = useState("");
|
||||||
const [isDetecting, setIsDetecting] = useState(false);
|
const [isDetecting, setIsDetecting] = useState(false);
|
||||||
|
|
||||||
|
const renewMutation = useMutation({
|
||||||
|
mutationKey: ["renewDnsOrder", orderId],
|
||||||
|
mutationFn: () => {
|
||||||
|
return axios.post(`/profile/dns/${orderId}/renew`);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleRenew = () => {
|
||||||
|
if (!orderId) {
|
||||||
|
toast.error("شناسه سفارش نامعتبر است");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
toast.promise(renewMutation.mutateAsync(), {
|
||||||
|
loading: "درحال انتقال به درگاه پرداخت...",
|
||||||
|
success: (res) => {
|
||||||
|
const paymentAction = res?.data?.result?.payment?.action;
|
||||||
|
if (paymentAction) {
|
||||||
|
window.location.href = paymentAction;
|
||||||
|
}
|
||||||
|
return res?.data?.alert || "به درگاه پرداخت منتقل میشوید";
|
||||||
|
},
|
||||||
|
error: (err: AxiosError) => getErrorMessage(err),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const copyToClipboard = async (label: string, textToCopy?: string | null) => {
|
const copyToClipboard = async (label: string, textToCopy?: string | null) => {
|
||||||
if (!textToCopy) return;
|
if (!textToCopy) return;
|
||||||
try {
|
try {
|
||||||
@@ -88,8 +108,8 @@ const ModalDnsConfig = ({
|
|||||||
toast.error("شناسه سفارش نامعتبر است");
|
toast.error("شناسه سفارش نامعتبر است");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!IP_ADDRESS_REGEX.test(normalizedIp)) {
|
if (!isValidIpv4Address(normalizedIp)) {
|
||||||
toast.error("آیپی وارد شده معتبر نیست");
|
toast.error("فقط آیپی نسخه ۴ معتبر است");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setIp(normalizedIp);
|
setIp(normalizedIp);
|
||||||
@@ -107,25 +127,22 @@ const ModalDnsConfig = ({
|
|||||||
const handleAutoDetectIp = async () => {
|
const handleAutoDetectIp = async () => {
|
||||||
setIsDetecting(true);
|
setIsDetecting(true);
|
||||||
try {
|
try {
|
||||||
for (const endpoint of CLIENT_IP_ENDPOINTS) {
|
const res = await axios.get("/user-ipv4");
|
||||||
try {
|
const detectedIp = res.data?.result?.ipv4 || res.data?.result?.ip;
|
||||||
const res = await fetch(endpoint.url, { cache: "no-store" });
|
|
||||||
if (!res.ok) continue;
|
|
||||||
|
|
||||||
const detectedIp = await endpoint.parse(res);
|
if (detectedIp && isValidIpv4Address(detectedIp)) {
|
||||||
if (detectedIp && IP_ADDRESS_REGEX.test(detectedIp)) {
|
setIp(detectedIp);
|
||||||
setIp(detectedIp);
|
toast.success(res.data?.alert || "آیپی نسخه ۴ کاربر دریافت شد");
|
||||||
toast.success("آیپی کاربر دریافت شد");
|
return;
|
||||||
return;
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
toast.error("دریافت آیپی خودکار ممکن نشد");
|
toast.error("دریافت آیپی نسخه ۴ خودکار ممکن نشد");
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
toast.error(`دریافت آیپی خودکار با خطا مواجه شد: ${err}`);
|
toast.error(
|
||||||
|
err instanceof Error
|
||||||
|
? `دریافت آیپی خودکار با خطا مواجه شد: ${err.message}`
|
||||||
|
: "دریافت آیپی خودکار با خطا مواجه شد"
|
||||||
|
);
|
||||||
} finally {
|
} finally {
|
||||||
setIsDetecting(false);
|
setIsDetecting(false);
|
||||||
}
|
}
|
||||||
@@ -143,7 +160,7 @@ const ModalDnsConfig = ({
|
|||||||
<div className="w-full h-full flex flex-col gap-4 max-sm:text-xs max-md:text-sm text-base">
|
<div className="w-full h-full flex flex-col gap-4 max-sm:text-xs max-md:text-sm text-base">
|
||||||
{dnsRegistrationExpired && (
|
{dnsRegistrationExpired && (
|
||||||
<Alert type="danger" extra="!mb-0 !rounded-lg">
|
<Alert type="danger" extra="!mb-0 !rounded-lg">
|
||||||
مهلت ثبت آیپی برای این سرویس به پایان رسیده است
|
مهلت این سرویس به پایان رسیده است. برای ادامه استفاده، سرویس را تمدید کنید
|
||||||
</Alert>
|
</Alert>
|
||||||
)}
|
)}
|
||||||
{dnsIpPending && !dnsRegistrationExpired && (
|
{dnsIpPending && !dnsRegistrationExpired && (
|
||||||
@@ -152,6 +169,32 @@ const ModalDnsConfig = ({
|
|||||||
</Alert>
|
</Alert>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{canRenew && (
|
||||||
|
<div className="w-full flex flex-row-reverse items-center justify-between gap-2 bg-gray-900/60 border border-gray-700 rounded-lg p-3">
|
||||||
|
<div className="text-start">
|
||||||
|
{dnsExpiresAt ? (
|
||||||
|
<span className="text-gray-300 text-xs md:text-sm">
|
||||||
|
مهلت سرویس تا{" "}
|
||||||
|
<span className="text-secondTheme font-bold" dir="ltr">
|
||||||
|
{new Date(dnsExpiresAt).toLocaleDateString("fa-IR")}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<span className="text-gray-300 text-xs md:text-sm">
|
||||||
|
تمدید سرویس DNS
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
onClick={handleRenew}
|
||||||
|
disabled={renewMutation.isPending}
|
||||||
|
extra="!m-0 !px-3 shadow-md shrink-0 !bg-secondTheme !text-gray-900 font-bold disabled:!opacity-50"
|
||||||
|
>
|
||||||
|
{renewMutation.isPending ? "درحال انتقال..." : "تمدید سرویس"}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{ipRows.length > 0 && (
|
{ipRows.length > 0 && (
|
||||||
<div className="w-full flex flex-col gap-2">
|
<div className="w-full flex flex-col gap-2">
|
||||||
<div className="flex flex-row-reverse items-center gap-1.5 text-secondTheme font-bold">
|
<div className="flex flex-row-reverse items-center gap-1.5 text-secondTheme font-bold">
|
||||||
|
|||||||
7
src/types/props.d.ts
vendored
7
src/types/props.d.ts
vendored
@@ -137,6 +137,8 @@ export interface ConfigItemProps {
|
|||||||
secondaryIpv6?: string | null;
|
secondaryIpv6?: string | null;
|
||||||
dnsIpPending?: boolean;
|
dnsIpPending?: boolean;
|
||||||
dnsRegistrationExpired?: boolean;
|
dnsRegistrationExpired?: boolean;
|
||||||
|
dnsExpiresAt?: string | null;
|
||||||
|
canRenew?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ModalDnsConfigProps {
|
export interface ModalDnsConfigProps {
|
||||||
@@ -150,6 +152,8 @@ export interface ModalDnsConfigProps {
|
|||||||
secondaryIpv6?: string | null;
|
secondaryIpv6?: string | null;
|
||||||
dnsIpPending?: boolean;
|
dnsIpPending?: boolean;
|
||||||
dnsRegistrationExpired?: boolean;
|
dnsRegistrationExpired?: boolean;
|
||||||
|
dnsExpiresAt?: string | null;
|
||||||
|
canRenew?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
// نوع Item برای آیتمهای داخل Dropdown
|
// نوع Item برای آیتمهای داخل Dropdown
|
||||||
@@ -229,6 +233,7 @@ type Config = {
|
|||||||
username?: string | null;
|
username?: string | null;
|
||||||
category_id: number;
|
category_id: number;
|
||||||
is_expired: boolean;
|
is_expired: boolean;
|
||||||
|
status?: "active" | "expired" | "pending";
|
||||||
use_max_traffic: boolean;
|
use_max_traffic: boolean;
|
||||||
primary_ipv4?: string | null;
|
primary_ipv4?: string | null;
|
||||||
secondary_ipv4?: string | null;
|
secondary_ipv4?: string | null;
|
||||||
@@ -236,6 +241,8 @@ type Config = {
|
|||||||
secondary_ipv6?: string | null;
|
secondary_ipv6?: string | null;
|
||||||
dns_ip_pending?: boolean;
|
dns_ip_pending?: boolean;
|
||||||
dns_registration_expired?: boolean;
|
dns_registration_expired?: boolean;
|
||||||
|
dns_expires_at?: string | null;
|
||||||
|
can_renew?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
// تعریف تایپ دادههای برگشتی
|
// تعریف تایپ دادههای برگشتی
|
||||||
|
|||||||
Reference in New Issue
Block a user