import { useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { Collapse, Modal, Spin, Tag } from 'antd'; import { HttpUtil } from '@/utils'; import { isPostQuantumLink } from '@/lib/xray/inbound-link'; import { LinkTags, linkMetaText, parseLinkParts } from '@/lib/xray/link-label'; import { QrPanel } from '@/pages/inbounds/qr'; import type { ClientRecord, InboundOption } from '@/hooks/useClients'; import { buildWireguardClientConfig, findWireguardInbound, isWireguardClient, } from './wireguardConfig'; interface SubSettings { enable: boolean; subURI: string; subJsonURI: string; subJsonEnable: boolean; publicHost?: string; } interface ClientQrModalProps { open: boolean; client: ClientRecord | null; inboundsById: Record; subSettings?: SubSettings; onOpenChange: (open: boolean) => void; } interface ApiMsg { success?: boolean; obj?: T; } const DEFAULT_SUB: SubSettings = { enable: false, subURI: '', subJsonURI: '', subJsonEnable: false, publicHost: '', }; export default function ClientQrModal({ open, client, inboundsById, subSettings = DEFAULT_SUB, onOpenChange, }: ClientQrModalProps) { const { t } = useTranslation(); const [links, setLinks] = useState([]); const [loading, setLoading] = useState(false); const subId = client?.subId; const subEnabled = !!subSettings?.enable; const subLink = subId && subEnabled && subSettings?.subURI ? subSettings.subURI + subId : ''; const subJsonLink = subId && subEnabled && subSettings?.subJsonEnable && subSettings?.subJsonURI ? subSettings.subJsonURI + subId : ''; const wgInbound = useMemo( () => findWireguardInbound(client, inboundsById), [client, inboundsById], ); const wgConfigText = useMemo(() => { if (!client || !wgInbound || !isWireguardClient(client)) return ''; return buildWireguardClientConfig( client, wgInbound, window.location.hostname, subSettings?.publicHost ?? '', ); }, [client, wgInbound, subSettings?.publicHost]); const hasAnything = !!subLink || !!subJsonLink || !!wgConfigText || links.length > 0; // The reset runs during render so the effect only carries the request. const openSubId = open ? (client?.subId ?? '') : ''; const [syncedSubId, setSyncedSubId] = useState(openSubId); if (openSubId !== syncedSubId) { setSyncedSubId(openSubId); setLinks([]); setLoading(!!openSubId); } useEffect(() => { if (!open || !client?.subId) return; let cancelled = false; (async () => { try { const msg = (await HttpUtil.get( `/panel/api/clients/subLinks/${encodeURIComponent(client.subId!)}`, )) as ApiMsg; if (!cancelled) { setLinks(msg?.success && Array.isArray(msg.obj) ? msg.obj : []); } } finally { if (!cancelled) setLoading(false); } })(); return () => { cancelled = true; }; }, [open, client?.subId]); const [activeKey, setActiveKey] = useState([]); const items = useMemo(() => { const out: { key: string; label: React.ReactNode; children: React.ReactNode }[] = []; if (subLink) { out.push({ key: 'sub', label: t('subscription.title'), children: ( ), }); } if (subJsonLink) { out.push({ key: 'subJson', label: `${t('subscription.title')} (JSON)`, children: , }); } links.forEach((link, idx) => { const parts = parseLinkParts(link); const meta = parts ? linkMetaText(parts) : ''; const label: React.ReactNode = parts ? ( {meta && ({meta})} ) : ( `${t('pages.clients.link')} ${idx + 1}` ); out.push({ key: `l${idx}`, label, children: ( ), }); }); if (wgConfigText) { out.push({ key: 'wg-config', label: ( {t('pages.clients.wireguardConfig')} ), children: ( ), }); } return out; }, [subLink, subJsonLink, wgConfigText, links, client?.email, t]); // Expanding the first panel is a render-time adjustment, not a side effect. const firstKey = open && items.length > 0 ? items[0].key : null; const [syncedFirstKey, setSyncedFirstKey] = useState(null); if (firstKey !== syncedFirstKey) { setSyncedFirstKey(firstKey); setActiveKey(firstKey ? [firstKey] : []); } return ( onOpenChange(false)} > {!client?.subId && !loading && (
{t('pages.clients.noSubId')}
)} {client?.subId && !hasAnything && !loading && (
{t('pages.clients.noLinks')}
)} {hasAnything && ( setActiveKey(typeof keys === 'string' ? [keys] : (keys as string[])) } items={items} /> )}
); }