| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203 |
- 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<string[]>([]);
- const [wireguardLinks, setWireguardLinks] = useState<string[]>([]);
- const [subLink, setSubLink] = useState('');
- const [subJsonLink, setSubJsonLink] = useState('');
- const [activeKey, setActiveKey] = useState<string[]>([]);
- // 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<QrItem[]>(() => {
- 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: (
- <QrPanel
- value={item.value}
- remark={item.header}
- downloadName={item.downloadName || ''}
- showQr={item.showQr !== false && !isPostQuantumLink(item.value)}
- />
- ),
- })),
- [qrItems],
- );
- const firstKey = open && qrItems.length > 0 ? qrItems[0].key : null;
- const [syncedFirstKey, setSyncedFirstKey] = useState<string | null>(null);
- if (firstKey !== syncedFirstKey) {
- setSyncedFirstKey(firstKey);
- setActiveKey(firstKey ? [firstKey] : []);
- }
- return (
- <Modal
- open={open}
- onCancel={onClose}
- title={t('qrCode')}
- footer={null}
- width={420}
- destroyOnHidden
- >
- {dbInbound && collapseItems && collapseItems.length > 0 && (
- <Collapse
- ghost
- activeKey={activeKey}
- onChange={(keys) => setActiveKey(typeof keys === 'string' ? [keys] : (keys as string[]))}
- items={collapseItems}
- />
- )}
- </Modal>
- );
- }
|