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, genAmneziaWGPeerConfigs, genAmneziaWGPeerLinks, genWireguardPeerConfigs, genWireguardPeerLinks, isPostQuantumLink, preferPublicHost, } from '@/lib/xray/inbound-link'; import { inboundFromDb, type DbInboundLike } from '@/lib/xray/inbound-from-db'; import { withHostEndpoints } from '@/lib/hosts/host-link'; import type { HostRecord } from '@/schemas/api/host'; import QrPanel from './QrPanel'; import { peerConfFileName } from '../info/helpers'; import type { SubSettings } from '../useInbounds'; interface ClientSetting { email?: string; subId?: string; [k: string]: unknown; } interface QrCodeModalProps { open: boolean; onClose: () => void; dbInbound: (DbInboundLike & { id: number; remark?: string }) | null; client?: ClientSetting | null; nodeAddress?: string; subSettings?: SubSettings; hosts?: HostRecord[]; } interface QrItem { key: string; header: string; value: string; downloadName?: string; showQr?: boolean; } const EMPTY_HOSTS: HostRecord[] = []; export default function QrCodeModal({ open, onClose, dbInbound, client = null, nodeAddress = '', subSettings, hosts = EMPTY_HOSTS, }: QrCodeModalProps) { const { t } = useTranslation(); const [links, setLinks] = useState<{ remark?: string; link: string }[]>([]); const [wireguardConfigs, setWireguardConfigs] = useState([]); const [wireguardLinks, setWireguardLinks] = useState([]); const [amneziawgConfigs, setAmneziawgConfigs] = useState([]); const [amneziawgLinks, setAmneziawgLinks] = 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; hosts: typeof hosts; } | null>(null); if ( open && dbInbound && (syncedProps === null || syncedProps.dbInbound !== dbInbound || syncedProps.client !== client || syncedProps.nodeAddress !== nodeAddress || syncedProps.subSettings !== subSettings || syncedProps.hosts !== hosts) ) { setSyncedProps({ dbInbound, client, nodeAddress, subSettings, hosts }); const fallbackHostname = preferPublicHost( window.location.hostname, subSettings?.publicHost ?? '', ); const inbound = withHostEndpoints( inboundFromDb(dbInbound), dbInbound.id, hosts, nodeAddress, fallbackHostname, ); if (inbound.protocol === Protocols.WIREGUARD) { const peerRemark = client?.email ? `${dbInbound.remark}-${client.email}` : dbInbound.remark || ''; setWireguardConfigs( genWireguardPeerConfigs({ inbound, remark: peerRemark, hostOverride: nodeAddress, fallbackHostname, }), ); setWireguardLinks( genWireguardPeerLinks({ inbound, remark: peerRemark, hostOverride: nodeAddress, fallbackHostname, }), ); setAmneziawgConfigs([]); setAmneziawgLinks([]); setLinks([]); } else if (inbound.protocol === Protocols.AMNEZIAWG) { const peerRemark = client?.email ? `${dbInbound.remark}-${client.email}` : dbInbound.remark || ''; setAmneziawgConfigs( genAmneziaWGPeerConfigs({ inbound, remark: peerRemark, hostOverride: nodeAddress, fallbackHostname, }), ); setAmneziawgLinks( genAmneziaWGPeerLinks({ inbound, remark: peerRemark, hostOverride: nodeAddress, fallbackHostname, }), ); setWireguardConfigs([]); setWireguardLinks([]); setLinks([]); } else { setLinks( genAllLinks({ inbound, remark: dbInbound.remark || '', client: client ?? {}, hostOverride: nodeAddress, fallbackHostname, }), ); setWireguardConfigs([]); setWireguardLinks([]); setAmneziawgConfigs([]); setAmneziawgLinks([]); } 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 }); }); const pushTunnelPeers = (prefix: string, configs: string[][], peerLinks: string[][]) => { configs.forEach((peerConfigs, idx) => { peerConfigs.forEach((cfg, j) => { const multi = peerConfigs.length > 1 ? ` #${j + 1}` : ''; items.push({ key: `${prefix}c${idx}-${j}`, header: `Peer ${idx + 1} config${multi}`, value: cfg, downloadName: peerConfFileName(idx, j, peerConfigs.length), }); const link = peerLinks[idx]?.[j]; if (link) { items.push({ key: `${prefix}l${idx}-${j}`, header: `Peer ${idx + 1} link${multi}`, value: link, showQr: false, }); } }); }); }; pushTunnelPeers('w', wireguardConfigs, wireguardLinks); pushTunnelPeers('a', amneziawgConfigs, amneziawgLinks); return items; }, [ subLink, subJsonLink, links, wireguardConfigs, wireguardLinks, amneziawgConfigs, amneziawgLinks, 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} /> )} ); }