Files
lotteryFront/src/hooks/use-player-balance-ws.ts
wchino 827f369eb6
Some checks failed
lotteryfront CI / build (push) Has been cancelled
feat(player): improve wallet activity and session handling
2026-07-22 20:53:43 +08:00

114 lines
3.2 KiB
TypeScript

"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<string, string> = {
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]);
}