QrCodeModal.tsx 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203
  1. import { useMemo, useState } from 'react';
  2. import { useTranslation } from 'react-i18next';
  3. import { Collapse, Modal } from 'antd';
  4. import type { CollapseProps } from 'antd';
  5. import { Protocols } from '@/schemas/primitives';
  6. import {
  7. genAllLinks,
  8. genWireguardConfigs,
  9. genWireguardLinks,
  10. isPostQuantumLink,
  11. preferPublicHost,
  12. } from '@/lib/xray/inbound-link';
  13. import { inboundFromDb, type DbInboundLike } from '@/lib/xray/inbound-from-db';
  14. import QrPanel from './QrPanel';
  15. import type { SubSettings } from '../useInbounds';
  16. interface ClientSetting {
  17. email?: string;
  18. subId?: string;
  19. [k: string]: unknown;
  20. }
  21. interface QrCodeModalProps {
  22. open: boolean;
  23. onClose: () => void;
  24. dbInbound: (DbInboundLike & { remark?: string }) | null;
  25. client?: ClientSetting | null;
  26. nodeAddress?: string;
  27. subSettings?: SubSettings;
  28. }
  29. interface QrItem {
  30. key: string;
  31. header: string;
  32. value: string;
  33. downloadName?: string;
  34. showQr?: boolean;
  35. }
  36. export default function QrCodeModal({
  37. open,
  38. onClose,
  39. dbInbound,
  40. client = null,
  41. nodeAddress = '',
  42. subSettings,
  43. }: QrCodeModalProps) {
  44. const { t } = useTranslation();
  45. const [links, setLinks] = useState<{ remark?: string; link: string }[]>([]);
  46. const [wireguardConfigs, setWireguardConfigs] = useState<string[]>([]);
  47. const [wireguardLinks, setWireguardLinks] = useState<string[]>([]);
  48. const [subLink, setSubLink] = useState('');
  49. const [subJsonLink, setSubJsonLink] = useState('');
  50. const [activeKey, setActiveKey] = useState<string[]>([]);
  51. // Building the links is a pure function of the props, so it runs during
  52. // render; an effect would paint the previous inbound's QR first.
  53. const [syncedProps, setSyncedProps] = useState<{
  54. dbInbound: typeof dbInbound;
  55. client: typeof client;
  56. nodeAddress: typeof nodeAddress;
  57. subSettings: typeof subSettings;
  58. } | null>(null);
  59. if (
  60. open &&
  61. dbInbound &&
  62. (syncedProps === null ||
  63. syncedProps.dbInbound !== dbInbound ||
  64. syncedProps.client !== client ||
  65. syncedProps.nodeAddress !== nodeAddress ||
  66. syncedProps.subSettings !== subSettings)
  67. ) {
  68. setSyncedProps({ dbInbound, client, nodeAddress, subSettings });
  69. const inbound = inboundFromDb(dbInbound);
  70. const fallbackHostname = preferPublicHost(
  71. window.location.hostname,
  72. subSettings?.publicHost ?? '',
  73. );
  74. if (inbound.protocol === Protocols.WIREGUARD) {
  75. const peerRemark = client?.email
  76. ? `${dbInbound.remark}-${client.email}`
  77. : dbInbound.remark || '';
  78. setWireguardConfigs(
  79. genWireguardConfigs({
  80. inbound,
  81. remark: peerRemark,
  82. hostOverride: nodeAddress,
  83. fallbackHostname,
  84. }).split('\r\n'),
  85. );
  86. setWireguardLinks(
  87. genWireguardLinks({
  88. inbound,
  89. remark: peerRemark,
  90. hostOverride: nodeAddress,
  91. fallbackHostname,
  92. }).split('\r\n'),
  93. );
  94. setLinks([]);
  95. } else {
  96. setLinks(
  97. genAllLinks({
  98. inbound,
  99. remark: dbInbound.remark || '',
  100. client: client ?? {},
  101. hostOverride: nodeAddress,
  102. fallbackHostname,
  103. }),
  104. );
  105. setWireguardConfigs([]);
  106. setWireguardLinks([]);
  107. }
  108. const subId = client?.subId;
  109. let nextSub = '';
  110. let nextSubJson = '';
  111. if (subSettings?.enable && subId) {
  112. nextSub = (subSettings.subURI || '') + subId;
  113. nextSubJson = subSettings.subJsonEnable ? (subSettings.subJsonURI || '') + subId : '';
  114. }
  115. setSubLink(nextSub);
  116. setSubJsonLink(nextSubJson);
  117. }
  118. const qrItems = useMemo<QrItem[]>(() => {
  119. const items: QrItem[] = [];
  120. if (subLink) {
  121. items.push({ key: 'sub', header: t('subscription.title'), value: subLink });
  122. }
  123. if (subJsonLink) {
  124. items.push({
  125. key: 'sub-json',
  126. header: `${t('subscription.title')} (JSON)`,
  127. value: subJsonLink,
  128. });
  129. }
  130. links.forEach((link, idx) => {
  131. items.push({ key: `l${idx}`, header: link.remark || `Link ${idx + 1}`, value: link.link });
  132. });
  133. wireguardConfigs.forEach((cfg, idx) => {
  134. items.push({
  135. key: `wc${idx}`,
  136. header: `Peer ${idx + 1} config`,
  137. value: cfg,
  138. downloadName: `peer-${idx + 1}.conf`,
  139. });
  140. if (wireguardLinks[idx]) {
  141. items.push({
  142. key: `wl${idx}`,
  143. header: `Peer ${idx + 1} link`,
  144. value: wireguardLinks[idx],
  145. showQr: false,
  146. });
  147. }
  148. });
  149. return items;
  150. }, [subLink, subJsonLink, links, wireguardConfigs, wireguardLinks, t]);
  151. const collapseItems: CollapseProps['items'] = useMemo(
  152. () =>
  153. qrItems.map((item) => ({
  154. key: item.key,
  155. label: item.header,
  156. children: (
  157. <QrPanel
  158. value={item.value}
  159. remark={item.header}
  160. downloadName={item.downloadName || ''}
  161. showQr={item.showQr !== false && !isPostQuantumLink(item.value)}
  162. />
  163. ),
  164. })),
  165. [qrItems],
  166. );
  167. const firstKey = open && qrItems.length > 0 ? qrItems[0].key : null;
  168. const [syncedFirstKey, setSyncedFirstKey] = useState<string | null>(null);
  169. if (firstKey !== syncedFirstKey) {
  170. setSyncedFirstKey(firstKey);
  171. setActiveKey(firstKey ? [firstKey] : []);
  172. }
  173. return (
  174. <Modal
  175. open={open}
  176. onCancel={onClose}
  177. title={t('qrCode')}
  178. footer={null}
  179. width={420}
  180. destroyOnHidden
  181. >
  182. {dbInbound && collapseItems && collapseItems.length > 0 && (
  183. <Collapse
  184. ghost
  185. activeKey={activeKey}
  186. onChange={(keys) => setActiveKey(typeof keys === 'string' ? [keys] : (keys as string[]))}
  187. items={collapseItems}
  188. />
  189. )}
  190. </Modal>
  191. );
  192. }