From d27c01ed56579bf41a8cf6c9c8cb3d08b2d8b4a1 Mon Sep 17 00:00:00 2001 From: bagasbanuna Date: Mon, 15 Dec 2025 17:46:05 +0800 Subject: [PATCH] Percobaan notifikasi Add: - app/(application)/(user)/test-notifications.tsx - components/_ShareComponent/NotificationInitializer.tsx - screens/Home/HeaderBell.tsx - service/api-notifications.ts Fix: - app/(application)/(user)/home.tsx - app/_layout.tsx - screens/Authentication/LoginView.tsx ### No Issue --- app/(application)/(user)/home.tsx | 95 ++++++++++--------- .../(user)/test-notifications.tsx | 48 ++++++++++ app/_layout.tsx | 63 ++---------- .../NotificationInitializer.tsx | 49 ++++++++++ screens/Authentication/LoginView.tsx | 4 +- screens/Home/HeaderBell.tsx | 45 +++++++++ service/api-notifications.ts | 23 +++++ 7 files changed, 226 insertions(+), 101 deletions(-) create mode 100644 app/(application)/(user)/test-notifications.tsx create mode 100644 components/_ShareComponent/NotificationInitializer.tsx create mode 100644 screens/Home/HeaderBell.tsx create mode 100644 service/api-notifications.ts diff --git a/app/(application)/(user)/home.tsx b/app/(application)/(user)/home.tsx index 2136ee5..a176a08 100644 --- a/app/(application)/(user)/home.tsx +++ b/app/(application)/(user)/home.tsx @@ -1,10 +1,11 @@ /* eslint-disable @typescript-eslint/no-unused-vars */ /* eslint-disable react-hooks/exhaustive-deps */ -import { StackCustom, ViewWrapper } from "@/components"; +import { ButtonCustom, StackCustom, ViewWrapper } from "@/components"; import { MainColor } from "@/constants/color-palet"; import { useAuth } from "@/hooks/use-auth"; import { useNotificationStore } from "@/hooks/use-notification-store"; import Home_BottomFeatureSection from "@/screens/Home/bottomFeatureSection"; +import HeaderBell from "@/screens/Home/HeaderBell"; import Home_ImageSection from "@/screens/Home/imageSection"; import TabSection from "@/screens/Home/tabSection"; import { tabsHome } from "@/screens/Home/tabsList"; @@ -22,12 +23,11 @@ export default function Application() { const [refreshing, setRefreshing] = useState(false); // console.log("[User] >>", JSON.stringify(user?.id, null, 2)); - const { notifications } = useNotificationStore(); - const unreadCount = notifications.filter((n) => !n.read).length; - - console.log("UNREAD", unreadCount) - + // const { notifications } = useNotificationStore(); + // const unreadCount = notifications.filter((n) => !n.read).length; + // console.log("UNREAD", notifications) // ‼️ Untuk cek apakah: 1. user ada, 2. user punya profile, 3. accept temrs of forum nya ada atau tidak + useFocusEffect( useCallback(() => { onLoadData(); @@ -88,46 +88,47 @@ export default function Application() { }} /> ), - headerRight: () => { - return ( - - { - router.push("/notifications"); - }} - /> - {unreadCount > 0 && ( - - - {unreadCount > 9 ? "9+" : unreadCount} - - - )} - - ); - }, + headerRight: () => , + // headerRight: () => { + // return ( + // + // { + // router.push("/notifications"); + // }} + // /> + // {unreadCount > 0 && ( + // + // + // {unreadCount > 9 ? "9+" : unreadCount} + // + // + // )} + // + // ); + // }, }} /> + router.push("./test-notifications")}>Test Notif + diff --git a/app/(application)/(user)/test-notifications.tsx b/app/(application)/(user)/test-notifications.tsx new file mode 100644 index 0000000..c607948 --- /dev/null +++ b/app/(application)/(user)/test-notifications.tsx @@ -0,0 +1,48 @@ +import { + ButtonCustom, + NewWrapper, + StackCustom, + TextInputCustom, +} from "@/components"; +import { apiNotificationsSend } from "@/service/api-notifications"; +import { useState } from "react"; + +export default function TestNotification() { + const [data, setData] = useState(""); + const handleSubmit = async () => { + console.log("[Data Dikirim]", data); + const response = await apiNotificationsSend({ + data: { + fcmToken: + "cVmHm-3P4E-1vjt6AA9kSF:APA91bHTkHjGTLxrFsb6Le6bZmzboZhwMGYXU4p0FP9yEeXixLDXNKS4F5vLuZV3sRgSnjjQsPpLOgstVLHJB8VJTObctKLdN-CxAp4dnP7Jbc_mH53jWvs", + title: "Test dari Backend (App Router)!", + body: data, + }, + }); + + console.log("[RES SEND NOTIF]", JSON.stringify(response.data, null, 2)); + }; + + return ( + <> + + + setData(text)} + /> + { + handleSubmit(); + }} + > + Kirim + + + + + ); +} diff --git a/app/_layout.tsx b/app/_layout.tsx index e0f77ae..c568ec3 100644 --- a/app/_layout.tsx +++ b/app/_layout.tsx @@ -1,69 +1,26 @@ +import NotificationInitializer from "@/components/_ShareComponent/NotificationInitializer"; import { AuthProvider } from "@/context/AuthContext"; -import AppRoot from "@/screens/RootLayout/AppRoot"; -import { useEffect } from "react"; -import "react-native-gesture-handler"; -import { SafeAreaProvider } from "react-native-safe-area-context"; -import Toast from "react-native-toast-message"; -import messaging, { - FirebaseMessagingTypes, -} from "@react-native-firebase/messaging"; import { useForegroundNotifications } from "@/hooks/use-foreground-notifications"; import { NotificationProvider, useNotificationStore, } from "@/hooks/use-notification-store"; +import AppRoot from "@/screens/RootLayout/AppRoot"; +import messaging, { + FirebaseMessagingTypes, +} from "@react-native-firebase/messaging"; +import { useEffect } from "react"; +import "react-native-gesture-handler"; +import { SafeAreaProvider } from "react-native-safe-area-context"; +import Toast from "react-native-toast-message"; export default function RootLayout() { - useEffect(() => { - const testFCM = async () => { - if (!messaging().isSupported()) { - console.warn("Firebase Messaging not supported (e.g. Expo Go)"); - return; - } - - const authStatus = await messaging().requestPermission(); - if (authStatus !== messaging.AuthorizationStatus.AUTHORIZED) { - console.warn("Permission not granted"); - return; - } - - const token = await messaging().getToken(); - console.log("✅ FCM Token:", token); - }; - - testFCM(); - }, []); - - const { addNotification } = useNotificationStore(); - - const handleForegroundNotification = ( - message: FirebaseMessagingTypes.RemoteMessage - ) => { - const title = message.notification?.title || "Notifikasi"; - const body = message.notification?.body || ""; - const rawData = message.data || {}; - - const safeData: Record = {}; - for (const key in rawData) { - if (typeof rawData[key] === "string") { - safeData[key] = rawData[key] as string; - } else { - // Jika object/array/number → ubah ke JSON string - safeData[key] = JSON.stringify(rawData[key]); - } - } - - // ✅ Simpan ke state → akan trigger update UI (termasuk icon bell) - addNotification({ body, title, data: safeData }); - }; - - useForegroundNotifications(handleForegroundNotification); - return ( <> + diff --git a/components/_ShareComponent/NotificationInitializer.tsx b/components/_ShareComponent/NotificationInitializer.tsx new file mode 100644 index 0000000..62f0949 --- /dev/null +++ b/components/_ShareComponent/NotificationInitializer.tsx @@ -0,0 +1,49 @@ +// src/components/NotificationInitializer.tsx +import { useEffect } from "react"; +import messaging from "@react-native-firebase/messaging"; +import { useForegroundNotifications } from "@/hooks/use-foreground-notifications"; +import { + useNotificationStore, +} from "@/hooks/use-notification-store"; +import type { FirebaseMessagingTypes } from "@react-native-firebase/messaging"; + +export default function NotificationInitializer() { + // 1. Ambil token FCM (opsional, hanya untuk log) + useEffect(() => { + const getFCMToken = async () => { + if (!messaging().isSupported()) return; + const authStatus = await messaging().requestPermission(); + if (authStatus === messaging.AuthorizationStatus.AUTHORIZED) { + const token = await messaging().getToken(); + console.log("✅ FCM Token:", token); + } + }; + getFCMToken(); + }, []); + + // 2. Setup handler notifikasi + const { addNotification } = useNotificationStore(); + + const handleForegroundNotification = ( + message: FirebaseMessagingTypes.RemoteMessage + ) => { + const title = message.notification?.title || "Notifikasi"; + const body = message.notification?.body || ""; + const rawData = message.data || {}; + + const safeData: Record = {}; + for (const key in rawData) { + safeData[key] = typeof rawData[key] === "string" + ? rawData[key] + : JSON.stringify(rawData[key]); + } + + console.log("📥 Menambahkan ke store:", { title, body, safeData }); + addNotification({ title, body, data: safeData }); + console.log("✅ Notifikasi ditambahkan ke state"); + }; + + useForegroundNotifications(handleForegroundNotification); + + return null; // komponen ini tidak merender apa-apa +} \ No newline at end of file diff --git a/screens/Authentication/LoginView.tsx b/screens/Authentication/LoginView.tsx index 3aa2fff..c80833d 100644 --- a/screens/Authentication/LoginView.tsx +++ b/screens/Authentication/LoginView.tsx @@ -134,10 +134,10 @@ export default function LoginView() { if (token && token !== "" && isAdmin) { // Akan di aktifkan jika sudah losos review - return ; + // return ; // Sementara gunakan ini - // return ; + return ; } return ( diff --git a/screens/Home/HeaderBell.tsx b/screens/Home/HeaderBell.tsx new file mode 100644 index 0000000..a52a20e --- /dev/null +++ b/screens/Home/HeaderBell.tsx @@ -0,0 +1,45 @@ +// components/HeaderBell.tsx +import { Ionicons } from "@expo/vector-icons"; +import { View, Text } from "react-native"; +import { router } from "expo-router"; +import { useNotificationStore } from "@/hooks/use-notification-store"; +import { MainColor } from "@/constants/color-palet"; + +export default function HeaderBell() { + const { notifications } = useNotificationStore(); + const unreadCount = notifications.filter((n) => !n.read).length; + console.log("NOTIF:", JSON.stringify(notifications, null, 2)); + + return ( + + { + router.push("/notifications"); + }} + /> + {unreadCount > 0 && ( + + + {unreadCount > 9 ? "9+" : unreadCount} + + + )} + + ); +} diff --git a/service/api-notifications.ts b/service/api-notifications.ts new file mode 100644 index 0000000..85ff6a6 --- /dev/null +++ b/service/api-notifications.ts @@ -0,0 +1,23 @@ +import { apiConfig } from "./api-config"; + + +type NotificationProp = { + fcmToken: string + title: string, + body: Object +} + + +export async function apiNotificationsSend({ data }: { data: NotificationProp }) { + try { + const response = await apiConfig.post(`/mobile/notifications`, { + data: data, + }); + + console.log("Fecth Notif", response.data) + + return response.data; + } catch (error) { + throw error; + } +}