import { useCallback, useEffect, useMemo, useState } from 'react'; import { Alert, Button, Collapse, Divider, Form, Input, message, Modal, Tag, } from 'antd'; import { ApiOutlined, SyncOutlined, DeleteOutlined, PlusOutlined } from '@ant-design/icons'; import { HttpUtil, SizeFormatter, ObjectUtil, Wireguard } from '@/utils'; import './WarpModal.css'; interface WarpModalProps { open: boolean; templateSettings: { outbounds?: { tag?: string }[] } | null; onClose: () => void; onAddOutbound: (outbound: Record) => void; onResetOutbound: (payload: { index: number; outbound: Record }) => void; onRemoveOutbound: (tag: string) => void; } interface WarpData { access_token?: string; device_id?: string; license_key?: string; private_key?: string; client_id?: string; } interface WarpConfig { name?: string; model?: string; enabled?: boolean; config?: { interface?: { addresses?: { v4?: string; v6?: string } }; peers?: { public_key?: string; endpoint?: { host?: string } }[]; }; account?: { account_type?: string; role?: string; premium_data?: number; quota?: number; usage?: number; }; } function addressesFor(addrs: { v4?: string; v6?: string }): string[] { const out: string[] = []; if (addrs.v4) out.push(`${addrs.v4}/32`); if (addrs.v6) out.push(`${addrs.v6}/128`); return out; } function reservedFor(clientId?: string): number[] { if (!clientId) return []; const decoded = atob(clientId); const out: number[] = []; for (let i = 0; i < decoded.length; i += 1) out.push(decoded.charCodeAt(i)); return out; } export default function WarpModal({ open, templateSettings, onClose, onAddOutbound, onResetOutbound, onRemoveOutbound, }: WarpModalProps) { const [loading, setLoading] = useState(false); const [warpData, setWarpData] = useState(null); const [warpConfig, setWarpConfig] = useState(null); const [warpPlus, setWarpPlus] = useState(''); const [licenseError, setLicenseError] = useState(''); const [stagedOutbound, setStagedOutbound] = useState | null>(null); const warpOutboundIndex = useMemo(() => { const list = templateSettings?.outbounds; if (!list) return -1; return list.findIndex((o) => o?.tag === 'warp'); }, [templateSettings?.outbounds]); const collectConfig = useCallback((data: WarpData | null, config: WarpConfig | null) => { const cfg = config?.config; if (!cfg?.peers?.length) return; const peer = cfg.peers[0]; setStagedOutbound({ tag: 'warp', protocol: 'wireguard', settings: { mtu: 1420, secretKey: data?.private_key, address: addressesFor(cfg.interface?.addresses || {}), reserved: reservedFor(data?.client_id), domainStrategy: 'ForceIP', peers: [{ publicKey: peer.public_key, endpoint: peer.endpoint?.host }], noKernelTun: false, }, }); }, []); const fetchData = useCallback(async () => { setLoading(true); try { const msg = await HttpUtil.post('/panel/xray/warp/data'); if (msg?.success) { const raw = msg.obj; setWarpData(raw && raw.length > 0 ? JSON.parse(raw) : null); } } finally { setLoading(false); } }, []); useEffect(() => { if (!open) return; setWarpConfig(null); setStagedOutbound(null); setLicenseError(''); fetchData(); }, [open, fetchData]); async function register() { setLoading(true); try { const keys = Wireguard.generateKeypair(); const msg = await HttpUtil.post('/panel/xray/warp/reg', keys); if (msg?.success) { const resp = JSON.parse(msg.obj); setWarpData(resp.data); setWarpConfig(resp.config); collectConfig(resp.data, resp.config); } } finally { setLoading(false); } } async function getConfig() { setLoading(true); try { const msg = await HttpUtil.post('/panel/xray/warp/config'); if (msg?.success) { const parsed = JSON.parse(msg.obj); setWarpConfig(parsed); collectConfig(warpData, parsed); } } finally { setLoading(false); } } async function updateLicense() { if (warpPlus.length < 26) return; setLoading(true); setLicenseError(''); try { const msg = await HttpUtil.post('/panel/xray/warp/license', { license: warpPlus }); if (msg?.success) { setWarpData(JSON.parse(msg.obj)); setWarpConfig(null); setWarpPlus(''); } else { setLicenseError(msg?.msg || 'Failed to set WARP license.'); } } finally { setLoading(false); } } async function delConfig() { setLoading(true); try { const msg = await HttpUtil.post('/panel/xray/warp/del'); if (msg?.success) { setWarpData(null); setWarpConfig(null); setStagedOutbound(null); onRemoveOutbound('warp'); onClose(); } } finally { setLoading(false); } } function addOutbound() { if (!stagedOutbound) { message.warning('Fetch the WARP config first.'); return; } onAddOutbound(stagedOutbound); onClose(); } function resetOutbound() { if (!stagedOutbound) return; onResetOutbound({ index: warpOutboundIndex, outbound: stagedOutbound }); onClose(); } const hasWarp = !ObjectUtil.isEmpty(warpData); const hasConfig = !ObjectUtil.isEmpty(warpConfig); return ( {!hasWarp ? ( ) : ( <>
Access token {warpData?.access_token}
Device ID {warpData?.device_id}
License key {warpData?.license_key}
Private key {warpData?.private_key}
Settings { setWarpPlus(e.target.value); setLicenseError(''); }} />
{licenseError && ( )}
), }, ]} /> Account info {hasConfig && ( <> {warpConfig?.account && ( <> {warpConfig.account.usage != null && ( )} )}
Device name {warpConfig?.name}
Device model {warpConfig?.model}
Device enabled {String(warpConfig?.enabled)}
Account type {warpConfig.account.account_type}
Role {warpConfig.account.role}
WARP+ data {SizeFormatter.sizeFormat(warpConfig.account.premium_data)}
Quota {SizeFormatter.sizeFormat(warpConfig.account.quota)}
Usage {SizeFormatter.sizeFormat(warpConfig.account.usage)}
Outbound status {warpOutboundIndex >= 0 ? ( <> Enabled ) : ( <> Disabled )} )} )}
); }