import { IconCheck, IconLoader2, IconQrcode, IconRefresh, IconX, } from "@tabler/icons-react" import { useCallback, useEffect, useRef, useState } from "react" import { useTranslation } from "react-i18next" import type { ChannelConfig } from "@/api/channels" import { pollWeixinFlow, startWeixinFlow } from "@/api/channels" import { type ArrayFieldFlusher, ChannelArrayListField, } from "@/components/channels/channel-array-list-field" import { asStringArray, parseAllowFromInput, } from "@/components/channels/channel-array-utils" import { Field } from "@/components/shared-form" import { Button } from "@/components/ui/button" import { Card, CardContent, CardDescription, CardHeader, CardTitle, } from "@/components/ui/card" import { Input } from "@/components/ui/input" type BindingState = | "idle" | "loading" | "waiting" | "scaned" | "confirmed" | "expired" | "error" interface WeixinFormProps { config: ChannelConfig onChange: (key: string, value: unknown) => void isEdit: boolean onBindSuccess?: () => void registerArrayFieldFlusher?: ( fieldPath: string, flusher: ArrayFieldFlusher | null, ) => void arrayFieldResetVersion?: number } function asString(value: unknown): string { return typeof value === "string" ? value : "" } export function WeixinForm({ config, onChange, isEdit, onBindSuccess, registerArrayFieldFlusher, arrayFieldResetVersion, }: WeixinFormProps) { const { t } = useTranslation() const [bindState, setBindState] = useState("idle") const [qrDataURI, setQrDataURI] = useState(null) const [accountID, setAccountID] = useState(null) const [errorMsg, setErrorMsg] = useState("") const pollTimerRef = useRef | null>(null) const pollGenerationRef = useRef(0) const isBound = isEdit && asString(config.account_id) !== "" const existingAccountID = asString(config.account_id) const stopPolling = useCallback(() => { pollGenerationRef.current += 1 if (pollTimerRef.current !== null) { clearInterval(pollTimerRef.current) pollTimerRef.current = null } }, []) useEffect(() => () => stopPolling(), [stopPolling]) useEffect(() => { if (!existingAccountID) return stopPolling() setAccountID(existingAccountID) setBindState("confirmed") setErrorMsg("") }, [existingAccountID, stopPolling]) const startPolling = useCallback( (id: string) => { stopPolling() const generation = pollGenerationRef.current let inFlight = false pollTimerRef.current = setInterval(async () => { if (inFlight) return inFlight = true try { const resp = await pollWeixinFlow(id) if (generation !== pollGenerationRef.current) { return } if (resp.status === "scaned") { setBindState("scaned") } else if (resp.status === "confirmed") { stopPolling() setAccountID(resp.account_id ?? existingAccountID ?? null) setBindState("confirmed") onBindSuccess?.() } else if (resp.status === "expired") { stopPolling() setBindState("expired") } else if (resp.status === "error") { stopPolling() setBindState("error") setErrorMsg(resp.error ?? t("channels.weixin.errorGeneric")) } } catch { // transient network error — keep polling } finally { inFlight = false } }, 2000) }, [existingAccountID, stopPolling, onBindSuccess, t], ) const handleBind = async () => { setBindState("loading") setErrorMsg("") setQrDataURI(null) stopPolling() try { const resp = await startWeixinFlow() setQrDataURI(resp.qr_data_uri ?? null) setBindState("waiting") startPolling(resp.flow_id) } catch (e) { setBindState("error") setErrorMsg( e instanceof Error ? e.message : t("channels.weixin.errorGeneric"), ) } } const handleRebind = () => { stopPolling() setBindState("idle") setQrDataURI(null) setAccountID(null) setErrorMsg("") void handleBind() } const renderBindSection = () => { if (bindState === "idle") { if (isBound) { return (
{t("channels.weixin.bound")}
{existingAccountID && (

{existingAccountID}

)}
) } return (

{t("channels.weixin.notBound")}

) } if (bindState === "loading") { return (

{t("channels.weixin.generating")}

) } if (bindState === "waiting" || bindState === "scaned") { return (
{qrDataURI ? ( WeChat QR Code ) : (
)} {bindState === "scaned" ? (
{t("channels.weixin.scanned")}
) : (

{t("channels.weixin.scanHint")}

)}
) } if (bindState === "confirmed") { return (

{t("channels.weixin.bound")}

{accountID && (

{accountID}

)}
) } if (bindState === "expired") { return (

{t("channels.weixin.expired")}

) } if (bindState === "error") { return (

{errorMsg || t("channels.weixin.errorGeneric")}

) } return null } return (
{t("channels.weixin.bindTitle")} {t("channels.weixin.bindDesc")} {renderBindSection()} onChange("allow_from", value)} placeholder={t("channels.field.allowFromPlaceholder")} parser={parseAllowFromInput} fieldPath="allow_from" registerFlusher={registerArrayFieldFlusher} resetVersion={arrayFieldResetVersion} /> onChange("proxy", e.target.value)} placeholder="http://localhost:7890" />
) }