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
This commit is contained in:
2025-12-15 17:46:05 +08:00
parent 34680a4c38
commit d27c01ed56
7 changed files with 226 additions and 101 deletions

View File

@@ -1,10 +1,11 @@
/* eslint-disable @typescript-eslint/no-unused-vars */ /* eslint-disable @typescript-eslint/no-unused-vars */
/* eslint-disable react-hooks/exhaustive-deps */ /* eslint-disable react-hooks/exhaustive-deps */
import { StackCustom, ViewWrapper } from "@/components"; import { ButtonCustom, StackCustom, ViewWrapper } from "@/components";
import { MainColor } from "@/constants/color-palet"; import { 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 Home_BottomFeatureSection from "@/screens/Home/bottomFeatureSection"; import Home_BottomFeatureSection from "@/screens/Home/bottomFeatureSection";
import HeaderBell from "@/screens/Home/HeaderBell";
import Home_ImageSection from "@/screens/Home/imageSection"; import Home_ImageSection from "@/screens/Home/imageSection";
import TabSection from "@/screens/Home/tabSection"; import TabSection from "@/screens/Home/tabSection";
import { tabsHome } from "@/screens/Home/tabsList"; import { tabsHome } from "@/screens/Home/tabsList";
@@ -22,12 +23,11 @@ export default function Application() {
const [refreshing, setRefreshing] = useState(false); const [refreshing, setRefreshing] = useState(false);
// console.log("[User] >>", JSON.stringify(user?.id, null, 2)); // console.log("[User] >>", JSON.stringify(user?.id, null, 2));
const { notifications } = useNotificationStore(); // const { notifications } = useNotificationStore();
const unreadCount = notifications.filter((n) => !n.read).length; // const unreadCount = notifications.filter((n) => !n.read).length;
// console.log("UNREAD", notifications)
console.log("UNREAD", unreadCount)
// ‼️ Untuk cek apakah: 1. user ada, 2. user punya profile, 3. accept temrs of forum nya ada atau tidak // ‼️ Untuk cek apakah: 1. user ada, 2. user punya profile, 3. accept temrs of forum nya ada atau tidak
useFocusEffect( useFocusEffect(
useCallback(() => { useCallback(() => {
onLoadData(); onLoadData();
@@ -88,46 +88,47 @@ export default function Application() {
}} }}
/> />
), ),
headerRight: () => { headerRight: () => <HeaderBell />,
return ( // headerRight: () => {
<View style={{ position: "relative" }}> // return (
<Ionicons // <View style={{ position: "relative" }}>
name="notifications" // <Ionicons
size={20} // name="notifications"
color={MainColor.yellow} // size={20}
onPress={() => { // color={MainColor.yellow}
router.push("/notifications"); // onPress={() => {
}} // router.push("/notifications");
/> // }}
{unreadCount > 0 && ( // />
<View // {unreadCount > 0 && (
style={{ // <View
position: "absolute", // style={{
top: -4, // position: "absolute",
right: -4, // top: -4,
backgroundColor: "red", // right: -4,
borderRadius: 8, // backgroundColor: "red",
minWidth: 16, // borderRadius: 8,
height: 16, // minWidth: 16,
justifyContent: "center", // height: 16,
alignItems: "center", // justifyContent: "center",
paddingHorizontal: 2, // alignItems: "center",
}} // paddingHorizontal: 2,
> // }}
<Text // >
style={{ // <Text
color: "white", // style={{
fontSize: 10, // color: "white",
fontWeight: "bold", // fontSize: 10,
}} // fontWeight: "bold",
> // }}
{unreadCount > 9 ? "9+" : unreadCount} // >
</Text> // {unreadCount > 9 ? "9+" : unreadCount}
</View> // </Text>
)} // </View>
</View> // )}
); // </View>
}, // );
// },
}} }}
/> />
<ViewWrapper <ViewWrapper
@@ -144,6 +145,8 @@ export default function Application() {
} }
> >
<StackCustom> <StackCustom>
<ButtonCustom onPress={() => router.push("./test-notifications")}>Test Notif</ButtonCustom>
<Home_ImageSection /> <Home_ImageSection />
<Home_FeatureSection /> <Home_FeatureSection />

View File

@@ -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 (
<>
<NewWrapper>
<StackCustom>
<TextInputCustom
required
label="Nama"
placeholder="Masukkan nama"
value={data}
onChangeText={(text) => setData(text)}
/>
<ButtonCustom
onPress={() => {
handleSubmit();
}}
>
Kirim
</ButtonCustom>
</StackCustom>
</NewWrapper>
</>
);
}

View File

@@ -1,69 +1,26 @@
import NotificationInitializer from "@/components/_ShareComponent/NotificationInitializer";
import { AuthProvider } from "@/context/AuthContext"; 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 { useForegroundNotifications } from "@/hooks/use-foreground-notifications";
import { import {
NotificationProvider, NotificationProvider,
useNotificationStore, useNotificationStore,
} from "@/hooks/use-notification-store"; } 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() { 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<string, string> = {};
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 ( return (
<> <>
<NotificationProvider> <NotificationProvider>
<SafeAreaProvider> <SafeAreaProvider>
<AuthProvider> <AuthProvider>
<NotificationInitializer />
<AppRoot /> <AppRoot />
</AuthProvider> </AuthProvider>
</SafeAreaProvider> </SafeAreaProvider>

View File

@@ -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<string, string> = {};
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
}

View File

@@ -134,10 +134,10 @@ export default function LoginView() {
if (token && token !== "" && isAdmin) { if (token && token !== "" && isAdmin) {
// Akan di aktifkan jika sudah losos review // Akan di aktifkan jika sudah losos review
return <Redirect href={"/(application)/admin/dashboard"} />; // return <Redirect href={"/(application)/admin/dashboard"} />;
// Sementara gunakan ini // Sementara gunakan ini
// return <Redirect href={"/(application)/(user)/home"} />; return <Redirect href={"/(application)/(user)/home"} />;
} }
return ( return (

View File

@@ -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 (
<View style={{ position: "relative" }}>
<Ionicons
name="notifications"
size={20}
color={MainColor.yellow}
onPress={() => {
router.push("/notifications");
}}
/>
{unreadCount > 0 && (
<View
style={{
position: "absolute",
top: -4,
right: -4,
backgroundColor: "red",
borderRadius: 8,
minWidth: 16,
height: 16,
justifyContent: "center",
alignItems: "center",
paddingHorizontal: 2,
}}
>
<Text style={{ color: "white", fontSize: 10, fontWeight: "bold" }}>
{unreadCount > 9 ? "9+" : unreadCount}
</Text>
</View>
)}
</View>
);
}

View File

@@ -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;
}
}