"use client" import { useCallback, useEffect, useRef, useState } from "react" import { AlertCircle, Loader2, RefreshCw } from "lucide-react" import { useTranslations } from "next-intl" import { Button } from "@/components/ui/button" import { Dialog, DialogContent, DialogHeader, DialogTitle, } from "@/components/ui/dialog" import { weixinGetQrcode, weixinCheckQrcode } from "@/lib/api" /** Client-side QR code expiry (5 minutes). */ const QR_EXPIRY_MS = 5 * 60 * 1000 /** Show a warning after this many consecutive polling failures. */ const POLL_ERROR_WARN_THRESHOLD = 3 interface WeixinQrcodeDialogProps { open: boolean channelId: number onOpenChange: (open: boolean) => void onAuthSuccess: (channelId: number) => void } function WeixinQrcodeContent({ channelId, onAuthSuccess, onClose, }: { channelId: number onAuthSuccess: (channelId: number) => void onClose: () => void }) { const t = useTranslations("ChatChannelSettings") const [qrcodeImg, setQrcodeImg] = useState(null) const [qrcodeId, setQrcodeId] = useState(null) const [status, setStatus] = useState<"loading" | "waiting" | "expired">( "loading" ) const [error, setError] = useState(null) const [pollErrors, setPollErrors] = useState(0) const pollingRef = useRef | null>(null) const expiryRef = useRef | null>(null) // Stabilise callbacks via ref so the polling effect doesn't re-trigger const onAuthSuccessRef = useRef(onAuthSuccess) const onCloseRef = useRef(onClose) useEffect(() => { onAuthSuccessRef.current = onAuthSuccess onCloseRef.current = onClose }) const stopPolling = useCallback(() => { if (pollingRef.current) { clearInterval(pollingRef.current) pollingRef.current = null } if (expiryRef.current) { clearTimeout(expiryRef.current) expiryRef.current = null } }, []) const fetchQrcode = useCallback(async () => { setStatus("loading") setError(null) setQrcodeImg(null) setQrcodeId(null) setPollErrors(0) stopPolling() try { const result = await weixinGetQrcode() setQrcodeId(result.qrcode_id) if (result.qrcode_img_content) { const raw = result.qrcode_img_content const imgSrc = raw.startsWith("data:") ? raw : `data:image/png;base64,${raw}` setQrcodeImg(imgSrc) } setStatus("waiting") } catch (err) { const msg = err instanceof Error ? err.message : String(err) setError(msg) setStatus("expired") } }, [stopPolling]) // Fetch QR code on mount + cleanup on unmount useEffect(() => { // eslint-disable-next-line react-hooks/set-state-in-effect -- initial data fetch on mount fetchQrcode() return () => stopPolling() }, [fetchQrcode, stopPolling]) // Start polling + expiry timer when we have a qrcodeId useEffect(() => { if (!qrcodeId || status !== "waiting") return // Client-side expiry guard expiryRef.current = setTimeout(() => { stopPolling() setStatus("expired") }, QR_EXPIRY_MS) pollingRef.current = setInterval(async () => { try { const result = await weixinCheckQrcode(channelId, qrcodeId) setPollErrors(0) if (result.status === "confirmed") { stopPolling() onAuthSuccessRef.current(channelId) onCloseRef.current() } else if (result.status === "expired") { stopPolling() setStatus("expired") } } catch { setPollErrors((n) => n + 1) } }, 2000) return () => stopPolling() }, [qrcodeId, status, channelId, stopPolling]) return (

{t("weixinScanDescription")}

{status === "loading" && (
)} {status === "waiting" && qrcodeImg && ( <> {/* eslint-disable-next-line @next/next/no-img-element */} WeChat QR Code

{t("weixinWaitingScan")}

{pollErrors >= POLL_ERROR_WARN_THRESHOLD && (
{t("weixinPollError")}
)} )} {status === "expired" && ( <>

{t("weixinQrcodeExpired")}

)} {error && (
{error}
)}
) } export function WeixinQrcodeDialog({ open, channelId, onOpenChange, onAuthSuccess, }: WeixinQrcodeDialogProps) { const t = useTranslations("ChatChannelSettings") const handleClose = useCallback(() => onOpenChange(false), [onOpenChange]) return ( {t("weixinScanTitle")} {open && ( )} ) }