feat: remove pending reconcile notifications section from wallet screen
Some checks failed
lotteryfront CI / build (push) Has been cancelled
Some checks failed
lotteryfront CI / build (push) Has been cancelled
- Deleted WalletPendingReconcileSection component and usePendingWalletReconcile hook - Removed pending reconcile notification helpers and translation key functions - Cleaned up wallet logs refresh event dispatching and pending reconcile cache logic - Simplified wallet screen section deep linking to only handle logs section
This commit is contained in:
@@ -1,153 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import { BellRing, CheckCheck } from "lucide-react";
|
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import { usePendingWalletReconcile } from "@/hooks/use-pending-wallet-reconcile";
|
|
||||||
import { formatPlayerInstant } from "@/lib/player-datetime";
|
|
||||||
import { formatMinorAsCurrency } from "@/lib/money";
|
|
||||||
import { isCreditFundingPlayer } from "@/lib/player-funding-mode";
|
|
||||||
import {
|
|
||||||
pendingReconcileDescriptionKey,
|
|
||||||
pendingReconcileTitleKey,
|
|
||||||
} from "@/lib/pending-reconcile-notification";
|
|
||||||
import { usePlayerSessionStore } from "@/stores/player-session-store";
|
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
|
|
||||||
type WalletPendingReconcileSectionProps = {
|
|
||||||
onViewLogs?: () => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
/** 钱包页内嵌:待对账提醒(原独立 /notifications 页) */
|
|
||||||
export function WalletPendingReconcileSection({
|
|
||||||
onViewLogs,
|
|
||||||
}: WalletPendingReconcileSectionProps) {
|
|
||||||
const { t } = useTranslation("player");
|
|
||||||
const creditMode = isCreditFundingPlayer(usePlayerSessionStore((state) => state.profile));
|
|
||||||
const { pending, unreadPending, unreadCount, loading, markAsRead, markAllAsRead } =
|
|
||||||
usePendingWalletReconcile();
|
|
||||||
const unreadSet = new Set(unreadPending.map((item) => item.transfer_no));
|
|
||||||
|
|
||||||
if (creditMode) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<section id="wallet-pending" className="scroll-mt-3 space-y-3">
|
|
||||||
<div className="flex items-center justify-between rounded-xl border border-[#dce7f7] bg-[#f8fbff] px-3 py-2.5">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<BellRing className="size-4 text-[#0b56b7]" aria-hidden />
|
|
||||||
<p className="text-sm font-semibold text-[#0b3f96]">
|
|
||||||
{t("notifications.title")}
|
|
||||||
{unreadCount > 0 ? (
|
|
||||||
<span className="ml-1.5 text-xs font-bold text-amber-700">
|
|
||||||
({t("notifications.unreadCount", { count: unreadCount })})
|
|
||||||
</span>
|
|
||||||
) : null}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
size="sm"
|
|
||||||
variant="ghost"
|
|
||||||
className="h-8 px-2 text-xs font-bold text-[#0b56b7] hover:bg-[#ebf2ff]"
|
|
||||||
onClick={markAllAsRead}
|
|
||||||
disabled={pending.length === 0 || unreadCount === 0}
|
|
||||||
>
|
|
||||||
<CheckCheck className="mr-1 size-3.5" aria-hidden />
|
|
||||||
{t("notifications.markAllRead")}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{loading && pending.length === 0 ? (
|
|
||||||
<div className="rounded-xl border border-[#dce7f7] bg-white px-4 py-6 text-center text-sm text-slate-500">
|
|
||||||
{t("actions.loading")}
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
{!loading && pending.length === 0 ? (
|
|
||||||
<div className="rounded-xl border border-dashed border-[#dce7f7] bg-white px-4 py-8 text-center">
|
|
||||||
<p className="text-sm text-slate-500">{t("notifications.empty")}</p>
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
{pending.length > 0 ? (
|
|
||||||
<ul className="space-y-2">
|
|
||||||
{pending.map((item) => {
|
|
||||||
const cardRead = !unreadSet.has(item.transfer_no);
|
|
||||||
return (
|
|
||||||
<li
|
|
||||||
key={item.transfer_no}
|
|
||||||
className={cn(
|
|
||||||
"rounded-xl border px-3 py-3 transition-colors",
|
|
||||||
cardRead
|
|
||||||
? "border-[#e4eaf4] bg-white"
|
|
||||||
: "border-amber-200 bg-amber-50/80",
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<div className="flex items-start justify-between gap-2">
|
|
||||||
<div className="min-w-0">
|
|
||||||
<p className="text-sm font-bold text-amber-900">
|
|
||||||
{t(pendingReconcileTitleKey(item.type))}
|
|
||||||
</p>
|
|
||||||
<p className="mt-0.5 text-xs text-slate-500">
|
|
||||||
{formatPlayerInstant(item.created_at)}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="flex shrink-0 flex-col items-end gap-1">
|
|
||||||
<span className="rounded-full bg-amber-100 px-2 py-0.5 text-[11px] font-bold text-amber-800">
|
|
||||||
{t("notifications.pendingBadge")}
|
|
||||||
</span>
|
|
||||||
<span
|
|
||||||
className={cn(
|
|
||||||
"text-[11px] font-semibold",
|
|
||||||
cardRead ? "text-slate-400" : "text-amber-700",
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{cardRead ? t("notifications.read") : t("notifications.unread")}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<p className="mt-2 text-xs leading-relaxed text-amber-950/85">
|
|
||||||
{t(pendingReconcileDescriptionKey(item.type))}
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p className="mt-2 text-sm text-slate-700">
|
|
||||||
<span className="text-xs font-medium text-slate-500">
|
|
||||||
{t("notifications.amountLabel")}{" "}
|
|
||||||
</span>
|
|
||||||
{formatMinorAsCurrency(item.amount, item.currency_code)}
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div className="mt-3 flex items-center gap-2">
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
size="sm"
|
|
||||||
variant="outline"
|
|
||||||
className="h-8 rounded-full border-[#dce7f7] px-3 text-xs font-semibold text-[#0b56b7]"
|
|
||||||
onClick={() => markAsRead(item.transfer_no)}
|
|
||||||
>
|
|
||||||
{t("notifications.markRead")}
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
size="sm"
|
|
||||||
className="h-8 rounded-full bg-[#07459f] px-3 text-xs font-semibold text-white hover:bg-[#063b88]"
|
|
||||||
onClick={() => {
|
|
||||||
markAsRead(item.transfer_no);
|
|
||||||
onViewLogs?.();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{t("notifications.viewLogs")}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</ul>
|
|
||||||
) : null}
|
|
||||||
</section>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -14,10 +14,8 @@ import {
|
|||||||
TransferInDialog,
|
TransferInDialog,
|
||||||
TransferOutDialog,
|
TransferOutDialog,
|
||||||
} from "@/features/wallet/wallet-transfer-dialogs";
|
} from "@/features/wallet/wallet-transfer-dialogs";
|
||||||
import { WalletPendingReconcileSection } from "@/features/wallet/wallet-pending-reconcile-section";
|
|
||||||
import { PlayerMoneyDisplay } from "@/components/player-money-display";
|
import { PlayerMoneyDisplay } from "@/components/player-money-display";
|
||||||
import { WalletLogsBlock } from "@/features/wallet/wallet-logs-block";
|
import { WalletLogsBlock } from "@/features/wallet/wallet-logs-block";
|
||||||
import { dispatchWalletLogsRefresh } from "@/hooks/use-pending-wallet-reconcile";
|
|
||||||
import { useActivePlayerCurrency } from "@/hooks/use-active-player-currency";
|
import { useActivePlayerCurrency } from "@/hooks/use-active-player-currency";
|
||||||
import { isCreditFundingPlayer } from "@/lib/player-funding-mode";
|
import { isCreditFundingPlayer } from "@/lib/player-funding-mode";
|
||||||
import { formatMinorAsCurrency } from "@/lib/money";
|
import { formatMinorAsCurrency } from "@/lib/money";
|
||||||
@@ -80,10 +78,7 @@ export function WalletScreen() {
|
|||||||
if (section !== "logs" && section !== "pending") return;
|
if (section !== "logs" && section !== "pending") return;
|
||||||
|
|
||||||
sectionDeepLinkHandledRef.current = true;
|
sectionDeepLinkHandledRef.current = true;
|
||||||
const targetId =
|
const targetId = "wallet-logs";
|
||||||
section === "logs" || (section === "pending" && isCreditPlayer)
|
|
||||||
? "wallet-logs"
|
|
||||||
: "wallet-pending";
|
|
||||||
const timer = window.setTimeout(() => {
|
const timer = window.setTimeout(() => {
|
||||||
scrollToWalletSection(targetId);
|
scrollToWalletSection(targetId);
|
||||||
router.replace("/wallet", { scroll: false });
|
router.replace("/wallet", { scroll: false });
|
||||||
@@ -103,7 +98,6 @@ export function WalletScreen() {
|
|||||||
? { ...nextLogs, items: [...current.items, ...nextLogs.items] }
|
? { ...nextLogs, items: [...current.items, ...nextLogs.items] }
|
||||||
: nextLogs,
|
: nextLogs,
|
||||||
);
|
);
|
||||||
dispatchWalletLogsRefresh(nextLogs.pending_reconcile ?? []);
|
|
||||||
return nextLogs;
|
return nextLogs;
|
||||||
}, [currency, filter]);
|
}, [currency, filter]);
|
||||||
|
|
||||||
@@ -126,7 +120,6 @@ export function WalletScreen() {
|
|||||||
if (cancelled) return;
|
if (cancelled) return;
|
||||||
setBalance(b);
|
setBalance(b);
|
||||||
setLogs(nextLogs);
|
setLogs(nextLogs);
|
||||||
dispatchWalletLogsRefresh(nextLogs.pending_reconcile ?? []);
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (!cancelled) {
|
if (!cancelled) {
|
||||||
setError(formatWalletClientError(e, t));
|
setError(formatWalletClientError(e, t));
|
||||||
@@ -169,7 +162,6 @@ export function WalletScreen() {
|
|||||||
});
|
});
|
||||||
if (!cancelled) {
|
if (!cancelled) {
|
||||||
setLogs(nextLogs);
|
setLogs(nextLogs);
|
||||||
dispatchWalletLogsRefresh(nextLogs.pending_reconcile ?? []);
|
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (!cancelled) {
|
if (!cancelled) {
|
||||||
@@ -340,10 +332,6 @@ export function WalletScreen() {
|
|||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
<WalletPendingReconcileSection
|
|
||||||
onViewLogs={() => scrollToWalletSection("wallet-logs")}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div id="wallet-logs" className="scroll-mt-3">
|
<div id="wallet-logs" className="scroll-mt-3">
|
||||||
<WalletLogsBlock
|
<WalletLogsBlock
|
||||||
creditMode={isCreditPlayer}
|
creditMode={isCreditPlayer}
|
||||||
|
|||||||
@@ -1,159 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import { useCallback, useEffect, useState } from "react";
|
|
||||||
|
|
||||||
import { getWalletLogs } from "@/api/wallet";
|
|
||||||
import { isCreditFundingPlayer } from "@/lib/player-funding-mode";
|
|
||||||
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 creditMode = isCreditFundingPlayer(usePlayerSessionStore((s) => s.profile));
|
|
||||||
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() || creditMode) {
|
|
||||||
setPending([]);
|
|
||||||
pendingReconcileCache = null;
|
|
||||||
pendingReconcileFetchedAtMs = 0;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setLoading(true);
|
|
||||||
try {
|
|
||||||
const nextPending = await fetchPendingWalletReconcile();
|
|
||||||
setPending(nextPending);
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
}, [bearerToken, creditMode]);
|
|
||||||
|
|
||||||
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 },
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
/** 待对账划转通知文案(与流水类型「转入/转出」区分,避免误解为已成功) */
|
|
||||||
|
|
||||||
export function pendingReconcileTitleKey(type: string): string {
|
|
||||||
return type === "transfer_out"
|
|
||||||
? "notifications.pendingTitle.transfer_out"
|
|
||||||
: "notifications.pendingTitle.transfer_in";
|
|
||||||
}
|
|
||||||
|
|
||||||
export function pendingReconcileDescriptionKey(type: string): string {
|
|
||||||
return type === "transfer_out"
|
|
||||||
? "notifications.pendingDescription.transfer_out"
|
|
||||||
: "notifications.pendingDescription.transfer_in";
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user