Files
lotteryFront/src/hooks/use-pending-wallet-reconcile.ts
kang b819894e75 feat: 增强 iframe 通信机制与通知处理功能
实现 resolvePostMessageTargetOrigin,优化 iframe 消息通信中的目标来源(origin)解析与校验。
更新 IframeBridge:支持定期刷新允许的来源列表,并优化消息事件管理机制。
重构 usePendingWalletReconcile:优化待对账通知的获取与缓存逻辑,提升性能与用户体验。
增强 NotificationsScreen:新增待对账通知内容,并优化界面展示效果。
更新英文、尼泊尔语与中文语言包,新增待对账通知相关翻译文案。
2026-06-01 13:38:30 +08:00

158 lines
4.7 KiB
TypeScript

"use client";
import { useCallback, useEffect, useState } from "react";
import { getWalletLogs } from "@/api/wallet";
import { usePlayerSessionStore } from "@/stores/player-session-store";
import type { WalletPendingTransfer } from "@/types/api/wallet-logs";
export const WALLET_LOGS_REFRESH_EVENT = "lottery:wallet-logs-refreshed";
const WALLET_NOTIFICATION_READ_KEY = "lottery:wallet-notification-read-transfer-nos";
let pendingReconcileInFlight: Promise<WalletPendingTransfer[]> | null = null;
let pendingReconcileCache: WalletPendingTransfer[] | null = null;
let pendingReconcileFetchedAtMs = 0;
const PENDING_RECONCILE_CACHE_TTL_MS = 5_000;
async function fetchPendingWalletReconcile(): Promise<WalletPendingTransfer[]> {
const now = Date.now();
if (
pendingReconcileCache !== null &&
now - pendingReconcileFetchedAtMs < PENDING_RECONCILE_CACHE_TTL_MS
) {
return pendingReconcileCache;
}
if (pendingReconcileInFlight) {
return pendingReconcileInFlight;
}
pendingReconcileInFlight = getWalletLogs({ page: 1, size: 1 })
.then((data) => {
pendingReconcileCache = data.pending_reconcile ?? [];
pendingReconcileFetchedAtMs = Date.now();
return pendingReconcileCache;
})
.catch(() => [])
.finally(() => {
pendingReconcileInFlight = null;
});
return pendingReconcileInFlight;
}
type WalletLogsRefreshDetail = {
pending?: WalletPendingTransfer[];
};
export function usePendingWalletReconcile(): {
pending: WalletPendingTransfer[];
unreadPending: WalletPendingTransfer[];
unreadCount: number;
hasPending: boolean;
hasUnread: boolean;
loading: boolean;
refresh: () => Promise<void>;
markAsRead: (transferNo: string) => void;
markAllAsRead: () => void;
} {
const bearerToken = usePlayerSessionStore((s) => s.bearerToken);
const [pending, setPending] = useState<WalletPendingTransfer[]>([]);
const [readTransferNos, setReadTransferNos] = useState<Set<string>>(() => {
if (typeof window === "undefined") return new Set();
try {
const raw = window.localStorage.getItem(WALLET_NOTIFICATION_READ_KEY);
if (!raw) return new Set();
const parsed = JSON.parse(raw);
if (!Array.isArray(parsed)) return new Set();
return new Set(
parsed.filter((value): value is string => typeof value === "string" && value.trim() !== ""),
);
} catch {
return new Set();
}
});
const [loading, setLoading] = useState(false);
useEffect(() => {
if (typeof window === "undefined") return;
try {
window.localStorage.setItem(WALLET_NOTIFICATION_READ_KEY, JSON.stringify(Array.from(readTransferNos)));
} catch {
// ignore storage quota or privacy mode errors
}
}, [readTransferNos]);
const refresh = useCallback(async (): Promise<void> => {
if (!bearerToken?.trim()) {
setPending([]);
pendingReconcileCache = null;
pendingReconcileFetchedAtMs = 0;
return;
}
setLoading(true);
try {
const nextPending = await fetchPendingWalletReconcile();
setPending(nextPending);
} finally {
setLoading(false);
}
}, [bearerToken]);
useEffect(() => {
queueMicrotask(() => {
void refresh();
});
function onWalletLogsRefreshed(event: Event): void {
const detail = (event as CustomEvent<WalletLogsRefreshDetail>).detail;
if (Array.isArray(detail?.pending)) {
setPending(detail.pending);
return;
}
void refresh();
}
window.addEventListener(WALLET_LOGS_REFRESH_EVENT, onWalletLogsRefreshed);
return () => window.removeEventListener(WALLET_LOGS_REFRESH_EVENT, onWalletLogsRefreshed);
}, [refresh]);
const markAsRead = useCallback((transferNo: string): void => {
const normalized = transferNo.trim();
if (!normalized) return;
setReadTransferNos((prev) => {
if (prev.has(normalized)) return prev;
const next = new Set(prev);
next.add(normalized);
return next;
});
}, []);
const markAllAsRead = useCallback((): void => {
if (pending.length === 0) return;
setReadTransferNos(new Set(pending.map((item) => item.transfer_no)));
}, [pending]);
const unreadPending = pending.filter((item) => !readTransferNos.has(item.transfer_no));
return {
pending,
unreadPending,
unreadCount: unreadPending.length,
hasPending: pending.length > 0,
hasUnread: unreadPending.length > 0,
loading,
refresh,
markAsRead,
markAllAsRead,
};
}
export function dispatchWalletLogsRefresh(pending: WalletPendingTransfer[]): void {
if (typeof window === "undefined") return;
window.dispatchEvent(
new CustomEvent<WalletLogsRefreshDetail>(WALLET_LOGS_REFRESH_EVENT, {
detail: { pending },
}),
);
}