Add in-app + real-time notifications (SignalR/mock, Iran-friendly)

- AppNotification + OnlineService.onNotification (hub event + mock periodic) —
  no FCM/APNs (blocked in Iran); uses the existing realtime channel
- notification-store + pushNotification(); 🔔 bell with unread badge in TopBar,
  notifications screen, global toaster (plays notify sfx)
- Wired events: daily reward, post-match achievements, friend requests
- Closed-app push (Pushe/Najva/Chabok) noted as a later step (needs provider keys)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
soroush.asadi
2026-06-04 15:52:06 +03:30
parent e02d976dda
commit 2d2352dfe8
13 changed files with 291 additions and 3 deletions
+21
View File
@@ -11,12 +11,16 @@ import { MatchmakingScreen } from "@/components/screens/MatchmakingScreen";
import { LeaderboardScreen } from "@/components/screens/LeaderboardScreen";
import { ShopScreen } from "@/components/screens/ShopScreen";
import { ChatScreen } from "@/components/screens/ChatScreen";
import { NotificationsScreen } from "@/components/screens/NotificationsScreen";
import { AuthScreen } from "@/components/screens/AuthScreen";
import { DailyRewardModal } from "@/components/online/DailyRewardModal";
import { NotificationToaster } from "@/components/online/NotificationToaster";
import { CapacitorBack } from "@/components/CapacitorBack";
import { useSessionStore } from "@/lib/session-store";
import { useGameStore } from "@/lib/game-store";
import { useOnlineStore } from "@/lib/online-store";
import { useNotifStore, pushNotification } from "@/lib/notification-store";
import { getService } from "@/lib/online/service";
import { screenFromHash, useUIStore, type Screen } from "@/lib/ui-store";
/** Transient screens can't be restored without their state — fall back to home. */
@@ -43,6 +47,20 @@ export default function Page() {
useEffect(() => {
init();
useUIStore.getState().initHistory();
useNotifStore.getState().init();
// surface a daily-reward notification if it's available
getService()
.getDailyState()
.then((d) => {
if (d.available)
pushNotification({
kind: "daily",
titleFa: "پاداش روزانه آماده است",
titleEn: "Daily reward is ready",
icon: "🎁",
});
})
.catch(() => {});
const onPop = (e: PopStateEvent) => {
const raw = ((e.state?.screen as Screen) ?? screenFromHash());
@@ -56,6 +74,7 @@ export default function Page() {
<>
{renderScreen(screen)}
<DailyRewardModal />
<NotificationToaster />
<CapacitorBack />
{loading && null}
</>
@@ -84,6 +103,8 @@ function renderScreen(screen: string) {
return <ShopScreen />;
case "chat":
return <ChatScreen />;
case "notifications":
return <NotificationsScreen />;
default:
return <HomeScreen />;
}