实现 resolvePostMessageTargetOrigin,优化 iframe 消息通信中的目标来源(origin)解析与校验。 更新 IframeBridge:支持定期刷新允许的来源列表,并优化消息事件管理机制。 重构 usePendingWalletReconcile:优化待对账通知的获取与缓存逻辑,提升性能与用户体验。 增强 NotificationsScreen:新增待对账通知内容,并优化界面展示效果。 更新英文、尼泊尔语与中文语言包,新增待对账通知相关翻译文案。
158 lines
4.7 KiB
TypeScript
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 },
|
|
}),
|
|
);
|
|
}
|