Background notifikasi berhasil dibuat
Add: -components/Notification/BackgroundNotificationHandler.tsx ### No Issue
This commit is contained in:
@@ -103,9 +103,9 @@ export default function Application() {
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<StackCustom>
|
<StackCustom>
|
||||||
<ButtonCustom onPress={() => router.push("./test-notifications")}>
|
{/* <ButtonCustom onPress={() => router.push("./test-notifications")}>
|
||||||
Test Notif
|
Test Notif
|
||||||
</ButtonCustom>
|
</ButtonCustom> */}
|
||||||
|
|
||||||
<Home_ImageSection />
|
<Home_ImageSection />
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { BackButton } from "@/components";
|
import { BackButton } from "@/components";
|
||||||
|
import BackgroundNotificationHandler from "@/components/Notification/BackgroundNotificationHandler";
|
||||||
import NotificationInitializer from "@/components/Notification/NotificationInitializer";
|
import NotificationInitializer from "@/components/Notification/NotificationInitializer";
|
||||||
import { NotificationProvider } from "@/hooks/use-notification-store";
|
import { NotificationProvider } from "@/hooks/use-notification-store";
|
||||||
import { HeaderStyles } from "@/styles/header-styles";
|
import { HeaderStyles } from "@/styles/header-styles";
|
||||||
@@ -9,6 +10,7 @@ export default function ApplicationLayout() {
|
|||||||
<>
|
<>
|
||||||
<NotificationProvider>
|
<NotificationProvider>
|
||||||
<NotificationInitializer />
|
<NotificationInitializer />
|
||||||
|
<BackgroundNotificationHandler />
|
||||||
<ApplicationStack />
|
<ApplicationStack />
|
||||||
</NotificationProvider>
|
</NotificationProvider>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -6,7 +6,8 @@ import {
|
|||||||
StackCustom,
|
StackCustom,
|
||||||
TextCustom,
|
TextCustom,
|
||||||
} from "@/components";
|
} from "@/components";
|
||||||
import { AccentColor } from "@/constants/color-palet";
|
import { IconPlus } from "@/components/_Icon";
|
||||||
|
import { AccentColor, MainColor } from "@/constants/color-palet";
|
||||||
import { useAuth } from "@/hooks/use-auth";
|
import { useAuth } from "@/hooks/use-auth";
|
||||||
import { useNotificationStore } from "@/hooks/use-notification-store";
|
import { useNotificationStore } from "@/hooks/use-notification-store";
|
||||||
import { apiGetNotificationsById } from "@/service/api-notifications";
|
import { apiGetNotificationsById } from "@/service/api-notifications";
|
||||||
@@ -17,7 +18,9 @@ import { useCallback, useState } from "react";
|
|||||||
import { RefreshControl, View } from "react-native";
|
import { RefreshControl, View } from "react-native";
|
||||||
|
|
||||||
const selectedCategory = (value: string) => {
|
const selectedCategory = (value: string) => {
|
||||||
const category = listOfcategoriesAppNotification.find((c) => c.value === value);
|
const category = listOfcategoriesAppNotification.find(
|
||||||
|
(c) => c.value === value
|
||||||
|
);
|
||||||
return category?.label;
|
return category?.label;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -104,7 +107,12 @@ export default function AdminNotification() {
|
|||||||
options={{
|
options={{
|
||||||
title: "Admin Notifikasi",
|
title: "Admin Notifikasi",
|
||||||
headerLeft: () => <BackButton />,
|
headerLeft: () => <BackButton />,
|
||||||
headerRight: () => <></>,
|
headerRight: () => (
|
||||||
|
<IconPlus
|
||||||
|
color={MainColor.yellow}
|
||||||
|
onPress={() => router.push("/test-notifications")}
|
||||||
|
/>
|
||||||
|
),
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
140
components/Notification/BackgroundNotificationHandler.tsx
Normal file
140
components/Notification/BackgroundNotificationHandler.tsx
Normal file
@@ -0,0 +1,140 @@
|
|||||||
|
// src/components/BackgroundNotificationHandler.tsx
|
||||||
|
import { useNotificationStore } from "@/hooks/use-notification-store";
|
||||||
|
import AsyncStorage from "@react-native-async-storage/async-storage";
|
||||||
|
import {
|
||||||
|
FirebaseMessagingTypes,
|
||||||
|
getInitialNotification,
|
||||||
|
getMessaging,
|
||||||
|
onNotificationOpenedApp,
|
||||||
|
} from "@react-native-firebase/messaging";
|
||||||
|
import { router } from "expo-router";
|
||||||
|
import { useEffect, useRef } from "react";
|
||||||
|
|
||||||
|
const HANDLED_NOTIFICATIONS_KEY = "handled_notifications";
|
||||||
|
|
||||||
|
export default function BackgroundNotificationHandler() {
|
||||||
|
const { addNotification, markAsRead } = useNotificationStore();
|
||||||
|
const messaging = getMessaging();
|
||||||
|
const unsubscribeRef = useRef<(() => void) | null>(null); // 🔑 cegah duplikasi
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const init = async () => {
|
||||||
|
// 1. Handle (cold start)
|
||||||
|
const initialNotification = await getInitialNotification(messaging);
|
||||||
|
if (initialNotification) {
|
||||||
|
handleNotification(initialNotification);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Handle background
|
||||||
|
if (unsubscribeRef.current) {
|
||||||
|
unsubscribeRef.current();
|
||||||
|
}
|
||||||
|
|
||||||
|
const unsubscribe = onNotificationOpenedApp(
|
||||||
|
messaging,
|
||||||
|
(remoteMessage) => {
|
||||||
|
handleNotification(remoteMessage);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
unsubscribeRef.current = unsubscribe;
|
||||||
|
};
|
||||||
|
|
||||||
|
init();
|
||||||
|
|
||||||
|
// Cleanup saat komponen unmount
|
||||||
|
return () => {
|
||||||
|
if (unsubscribeRef.current) {
|
||||||
|
unsubscribeRef.current();
|
||||||
|
unsubscribeRef.current = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [addNotification, messaging]);
|
||||||
|
|
||||||
|
const isNotificationHandled = async (
|
||||||
|
notificationId: string
|
||||||
|
): Promise<boolean> => {
|
||||||
|
const handled = await AsyncStorage.getItem(HANDLED_NOTIFICATIONS_KEY);
|
||||||
|
const ids = handled ? JSON.parse(handled) : [];
|
||||||
|
return ids.includes(notificationId);
|
||||||
|
};
|
||||||
|
|
||||||
|
const markNotificationAsHandled = async (notificationId: string) => {
|
||||||
|
const handled = await AsyncStorage.getItem(HANDLED_NOTIFICATIONS_KEY);
|
||||||
|
const ids = handled ? JSON.parse(handled) : [];
|
||||||
|
if (!ids.includes(notificationId)) {
|
||||||
|
ids.push(notificationId);
|
||||||
|
// Simpan maksimal 50 ID terakhir untuk hindari memori bocor
|
||||||
|
await AsyncStorage.setItem(
|
||||||
|
HANDLED_NOTIFICATIONS_KEY,
|
||||||
|
JSON.stringify(ids.slice(-50))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleNotification = async (
|
||||||
|
remoteMessage: FirebaseMessagingTypes.RemoteMessage
|
||||||
|
) => {
|
||||||
|
const { notification, data } = remoteMessage;
|
||||||
|
if (!notification?.title) return;
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
"🚀 Notification received:",
|
||||||
|
JSON.stringify(remoteMessage, null, 2)
|
||||||
|
);
|
||||||
|
|
||||||
|
const notificationId = data?.id;
|
||||||
|
if (!notificationId || typeof notificationId !== "string") {
|
||||||
|
console.warn("Notification missing notificationId, skipping navigation");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ Cek apakah sudah pernah ditangani
|
||||||
|
if (await isNotificationHandled(notificationId)) {
|
||||||
|
console.log("Notification already handled, skipping:", notificationId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ Tandai sebagai ditangani
|
||||||
|
await markNotificationAsHandled(notificationId);
|
||||||
|
|
||||||
|
// ✅ Normalisasi deepLink: pastikan string
|
||||||
|
let deepLink: string | undefined;
|
||||||
|
if (data?.deepLink) {
|
||||||
|
if (typeof data.deepLink === "string") {
|
||||||
|
deepLink = data.deepLink;
|
||||||
|
} else {
|
||||||
|
// Jika object (jarang), coba string-kan
|
||||||
|
deepLink = JSON.stringify(data.deepLink);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tambahkan ke UI state (agar muncul di daftar notifikasi & badge)
|
||||||
|
addNotification({
|
||||||
|
title: notification.title,
|
||||||
|
body: notification.body || "",
|
||||||
|
type: "announcement",
|
||||||
|
data: data as Record<string, string>, // aman karena di-normalisasi di useNotificationStore
|
||||||
|
});
|
||||||
|
|
||||||
|
markAsRead(data?.id as any);
|
||||||
|
|
||||||
|
// Navigasi
|
||||||
|
if (
|
||||||
|
data?.deepLink &&
|
||||||
|
typeof data.deepLink === "string" &&
|
||||||
|
data.deepLink.startsWith("/")
|
||||||
|
) {
|
||||||
|
setTimeout(() => {
|
||||||
|
try {
|
||||||
|
router.push(data.deepLink as any);
|
||||||
|
} catch (error) {
|
||||||
|
console.warn("Navigation failed:", error);
|
||||||
|
}
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user