"use client"; import { useEffect } from "react"; import { useTranslation } from "react-i18next"; import { toast } from "sonner"; import { disconnectLotteryEcho, getLotteryEcho, } from "@/lib/lottery-echo"; import { parseJwtSessionVersion } from "@/lib/jwt-payload"; import { useActivePlayerCurrency } from "@/hooks/use-active-player-currency"; import { usePlayerSessionStore } from "@/stores/player-session-store"; type BalanceUpdateWsEvent = { player_id?: number; currency_code?: string; balance_minor?: number; change_minor?: number; change_formatted?: string; reason?: string; }; type SessionReplacedWsEvent = { player_id?: number; session_version?: number; }; const REASON_I18N_KEY: Record = { transfer_in: "wallet.wsReason.transferIn", transfer_out: "wallet.wsReason.transferOut", bet: "wallet.wsReason.bet", prize: "wallet.wsReason.prize", refund: "wallet.wsReason.refund", }; /** * 订阅 `player.{id}` 私有频道的 `balance.update`,刷新余额并 Toast。 */ export function usePlayerBalanceWs(): void { const { t } = useTranslation("player"); const playerId = usePlayerSessionStore((state) => state.profile?.id); const bearerToken = usePlayerSessionStore((state) => state.bearerToken); const { activeCurrency } = useActivePlayerCurrency(); useEffect(() => { if (!playerId || !bearerToken) { return; } const echo = getLotteryEcho(); if (!echo) { return; } const channelName = `player.${playerId}`; const channel = echo.private(channelName); const onBalanceUpdate = (evt: BalanceUpdateWsEvent): void => { const currency = evt.currency_code?.trim().toUpperCase(); if (currency && currency !== activeCurrency.toUpperCase()) { return; } window.dispatchEvent(new Event("lottery-wallet-refresh")); const changeLabel = typeof evt.change_formatted === "string" && evt.change_formatted !== "" ? evt.change_formatted : evt.change_minor != null ? String(evt.change_minor) : ""; const reasonKey = evt.reason && REASON_I18N_KEY[evt.reason] ? REASON_I18N_KEY[evt.reason] : "wallet.wsReason.unknown"; const reasonLabel = t(reasonKey); toast.message(t("wallet.wsBalanceUpdated", { change: changeLabel, reason: reasonLabel })); }; const onSessionReplaced = (evt: SessionReplacedWsEvent): void => { const nextVersion = evt.session_version; if ( evt.player_id !== playerId || typeof nextVersion !== "number" || !Number.isInteger(nextVersion) ) { return; } const state = usePlayerSessionStore.getState(); const currentVersion = parseJwtSessionVersion(state.bearerToken) ?? 0; if (nextVersion <= currentVersion) { return; } disconnectLotteryEcho(); state.clearBearerToken(); window.location.replace("/login?session=replaced"); }; channel.listen(".balance.update", onBalanceUpdate); channel.listen(".session.replaced", onSessionReplaced); return () => { channel.stopListening(".balance.update"); channel.stopListening(".session.replaced"); echo.leave(channelName); }; }, [activeCurrency, bearerToken, playerId, t]); }