import { useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { Collapse, Modal } from 'antd'; import type { CollapseProps } from 'antd'; import { Protocols } from '@/schemas/primitives'; import { genAllLinks, genWireguardConfigs, genWireguardLinks, isPostQuantumLink, preferPublicHost, } from '@/lib/xray/inbound-link'; import { inboundFromDb, type DbInboundLike } from '@/lib/xray/inbound-from-db'; import QrPanel from './QrPanel'; import type { SubSettings } from '../useInbounds'; interface ClientSetting { email?: string; subId?: string; [k: string]: unknown; } interface QrCodeModalProps { open: boolean; onClose: () => void; dbInbound: (DbInboundLike & { remark?: string }) | null; client?: ClientSetting | null; nodeAddress?: string; subSettings?: SubSettings; } interface QrItem { key: string; header: string; value: string; downloadName?: string; showQr?: boolean; } export default function QrCodeModal({ open, onClose, dbInbound, client = null, nodeAddress = '', subSettings, }: QrCodeModalProps) { const { t } = useTranslation(); const [links, setLinks] = useState<{ remark?: string; link: string }[]>([]); const [wireguardConfigs, setWireguardConfigs] = useState([]); const [wireguardLinks, setWireguardLinks] = useState([]); const [subLink, setSubLink] = useState(''); const [subJsonLink, setSubJsonLink] = useState(''); const [activeKey, setActiveKey] = useState([]); // Building the links is a pure function of the props, so it runs during // render; an effect would paint the previous inbound's QR first. const [syncedProps, setSyncedProps] = useState<{ dbInbound: typeof dbInbound; client: typeof client; nodeAddress: typeof nodeAddress; subSettings: typeof subSettings; } | null>(null); if ( open && dbInbound && (syncedProps === null || syncedProps.dbInbound !== dbInbound || syncedProps.client !== client || syncedProps.nodeAddress !== nodeAddress || syncedProps.subSettings !== subSettings) ) { setSyncedProps({ dbInbound, client, nodeAddress, subSettings }); const inbound = inboundFromDb(dbInbound); const fallbackHostname = preferPublicHost( window.location.hostname, subSettings?.publicHost ?? '', ); if (inbound.protocol === Protocols.WIREGUARD) { const peerRemark = client?.email ? `${dbInbound.remark}-${client.email}` : dbInbound.remark || ''; setWireguardConfigs( genWireguardConfigs({ inbound, remark: peerRemark, hostOverride: nodeAddress, fallbackHostname, }).split('\r\n'), ); setWireguardLinks( genWireguardLinks({ inbound, remark: peerRemark, hostOverride: nodeAddress, fallbackHostname, }).split('\r\n'), ); setLinks([]); } else { setLinks( genAllLinks({ inbound, remark: dbInbound.remark || '', client: client ?? {}, hostOverride: nodeAddress, fallbackHostname, }), ); setWireguardConfigs([]); setWireguardLinks([]); } const subId = client?.subId; let nextSub = ''; let nextSubJson = ''; if (subSettings?.enable && subId) { nextSub = (subSettings.subURI || '') + subId; nextSubJson = subSettings.subJsonEnable ? (subSettings.subJsonURI || '') + subId : ''; } setSubLink(nextSub); setSubJsonLink(nextSubJson); } const qrItems = useMemo(() => { const items: QrItem[] = []; if (subLink) { items.push({ key: 'sub', header: t('subscription.title'), value: subLink }); } if (subJsonLink) { items.push({ key: 'sub-json', header: `${t('subscription.title')} (JSON)`, value: subJsonLink, }); } links.forEach((link, idx) => { items.push({ key: `l${idx}`, header: link.remark || `Link ${idx + 1}`, value: link.link }); }); wireguardConfigs.forEach((cfg, idx) => { items.push({ key: `wc${idx}`, header: `Peer ${idx + 1} config`, value: cfg, downloadName: `peer-${idx + 1}.conf`, }); if (wireguardLinks[idx]) { items.push({ key: `wl${idx}`, header: `Peer ${idx + 1} link`, value: wireguardLinks[idx], showQr: false, }); } }); return items; }, [subLink, subJsonLink, links, wireguardConfigs, wireguardLinks, t]); const collapseItems: CollapseProps['items'] = useMemo( () => qrItems.map((item) => ({ key: item.key, label: item.header, children: ( ), })), [qrItems], ); const firstKey = open && qrItems.length > 0 ? qrItems[0].key : null; const [syncedFirstKey, setSyncedFirstKey] = useState(null); if (firstKey !== syncedFirstKey) { setSyncedFirstKey(firstKey); setActiveKey(firstKey ? [firstKey] : []); } return ( {dbInbound && collapseItems && collapseItems.length > 0 && ( setActiveKey(typeof keys === 'string' ? [keys] : (keys as string[]))} items={collapseItems} /> )} ); }