Fix notifikasi system

Add:
- screens/Admin/AdminNotificationBell.tsx

Fix:
- app.config.js
- app/(application)/(user)/home.tsx
- app/(application)/(user)/test-notifications.tsx
- app/(application)/admin/_layout.tsx
- app/_layout.tsx
- components/Notification/NotificationInitializer.tsx
- context/AuthContext.tsx
- hooks/use-notification-store.tsx
- ios/HIPMIBadungConnect/Info.plist
- screens/Home/HeaderBell.tsx
- service/api-device-token.ts
- service/api-notifications.ts

### No Issue
This commit is contained in:
2025-12-23 17:45:30 +08:00
parent 1503707eed
commit 54611ef812
13 changed files with 103 additions and 44 deletions

View File

@@ -21,7 +21,7 @@ export default {
"Aplikasi membutuhkan akses lokasi untuk menampilkan peta.",
},
associatedDomains: ["applinks:cld-dkr-staging-hipmi.wibudev.com"],
buildNumber: "16",
buildNumber: "17",
},
android: {

View File

@@ -3,6 +3,7 @@
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";
@@ -20,12 +21,16 @@ export default function Application() {
const { token, user, userData } = useAuth();
const [data, setData] = useState<any>();
const [refreshing, setRefreshing] = useState(false);
const { syncUnreadCount } = useNotificationStore();
useFocusEffect(
useCallback(() => {
onLoadData();
checkVersion();
userData(token as string);
syncUnreadCount()
}, [user?.id, token])
);
@@ -98,9 +103,9 @@ export default function Application() {
}
>
<StackCustom>
{/* <ButtonCustom onPress={() => router.push("./test-notifications")}>
<ButtonCustom onPress={() => router.push("./test-notifications")}>
Test Notif
</ButtonCustom> */}
</ButtonCustom>
<Home_ImageSection />

View File

@@ -5,36 +5,26 @@ import {
TextInputCustom,
} from "@/components";
import { useAuth } from "@/hooks/use-auth";
import { apiGetAllTokenDevice } from "@/service/api-device-token";
import { apiNotificationsSend } from "@/service/api-notifications";
import { useEffect, useState } from "react";
import { useState } from "react";
import Toast from "react-native-toast-message";
export default function TestNotification() {
const { user } = useAuth();
const [data, setData] = useState("");
useEffect(() => {
// fecthData();
}, []);
const fecthData = async () => {
const response = await apiGetAllTokenDevice();
console.log(
"[RES GET ALL TOKEN DEVICE]",
JSON.stringify(response.data, null, 2)
);
};
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,
userLoginId: user?.id || "",
appId: "hipmi",
status: "publish",
kategoriApp: "EVENT",
type: "announcement",
deepLink: "event/23189913801",
},
});

View File

@@ -15,6 +15,7 @@ import {
ICON_SIZE_XLARGE,
} from "@/constants/constans-value";
import { useAuth } from "@/hooks/use-auth";
import AdminNotificationBell from "@/screens/Admin/AdminNotificationBell";
import {
adminListMenu,
superAdminListMenu,
@@ -192,11 +193,12 @@ export default function AdminLayout() {
label: "Notifikasi",
value: "notification",
icon: (
<Ionicons
name="notifications"
size={ICON_SIZE_SMALL}
color={MainColor.white}
/>
// <Ionicons
// name="notifications"
// size={ICON_SIZE_SMALL}
// color={MainColor.white}
// />
<AdminNotificationBell/>
),
path: "/admin/notification",
},

View File

@@ -1,6 +1,4 @@
import NotificationInitializer from "@/components/Notification/NotificationInitializer";
import { AuthProvider } from "@/context/AuthContext";
import { NotificationProvider } from "@/hooks/use-notification-store";
import AppRoot from "@/screens/RootLayout/AppRoot";
import "react-native-gesture-handler";
import { SafeAreaProvider } from "react-native-safe-area-context";

View File

@@ -39,7 +39,7 @@ export default function NotificationInitializer() {
if (!supported) {
console.log("‼️ FCM tidak didukung");
return;
};
}
const authStatus = await requestPermission(messagingInstance);
if (authStatus !== AuthorizationStatus.AUTHORIZED) {
@@ -62,7 +62,7 @@ export default function NotificationInitializer() {
"-" +
(Application.nativeBuildVersion || "unknown");
const deviceId =
Device.osInternalBuildId || Device.modelName + "-" + Date.now();
Device.osInternalBuildId || Device.modelName || "unknown";
// Kirim ke backend
await apiDeviceRegisterToken({
@@ -101,7 +101,7 @@ export default function NotificationInitializer() {
}
console.log("📥 Menambahkan ke store:", { title, body, safeData });
addNotification({ title, body, data: safeData , type: "notification", });
addNotification({ title, body, data: safeData, type: "notification" });
console.log("✅ Notifikasi ditambahkan ke state");
};

View File

@@ -10,6 +10,7 @@ import AsyncStorage from "@react-native-async-storage/async-storage";
import { router } from "expo-router";
import { createContext, useEffect, useState } from "react";
import Toast from "react-native-toast-message";
import * as Device from "expo-device";
// --- Types ---
type AuthContextType = {
@@ -77,7 +78,6 @@ export const AuthProvider = ({ children }: { children: React.ReactNode }) => {
const response = await apiLogin({ nomor: nomor });
console.log("[RESPONSE AUTH]", JSON.stringify(response));
if (response.success) {
console.log("[Keluar provider]", nomor);
Toast.show({
@@ -148,7 +148,7 @@ export const AuthProvider = ({ children }: { children: React.ReactNode }) => {
// return;
// }
router.replace("/(application)/(user)/home");
return
return;
} else {
router.replace("/(application)/(user)/waiting-room");
return;
@@ -283,9 +283,12 @@ export const AuthProvider = ({ children }: { children: React.ReactNode }) => {
setIsLoading(true);
setToken(null);
setUser(null);
const deviceId = Device.osInternalBuildId || Device.modelName || "unknown";
await AsyncStorage.removeItem("authToken");
await AsyncStorage.removeItem("userData");
await apiDeviceTokenDeleted({userId: user?.id as any})
await apiDeviceTokenDeleted({ userId: user?.id as any, deviceId });
Toast.show({
type: "success",

View File

@@ -70,6 +70,7 @@ export const NotificationProvider = ({ children }: { children: ReactNode }) => {
try {
const count = await apiNotificationUnreadCount({
id: user?.id as any,
role: user?.masterUserRoleId as any
}); // ← harus return number
const result = count.data;
console.log("📖 Unread count:", result);
@@ -105,8 +106,10 @@ export const NotificationProvider = ({ children }: { children: ReactNode }) => {
try {
const count = await apiNotificationUnreadCount({
id: user?.id as any,
role: user?.masterUserRoleId as any,
}); // ← harus return number
const result = count.data;
console.log("📖 Unread count sync:", result);
setUnreadCount(result);
} catch (error) {
console.warn("⚠️ Gagal sync unread count:", error);

View File

@@ -39,7 +39,7 @@
</dict>
</array>
<key>CFBundleVersion</key>
<string>16</string>
<string>17</string>
<key>ITSAppUsesNonExemptEncryption</key>
<false/>
<key>LSMinimumSystemVersion</key>

View File

@@ -0,0 +1,41 @@
// components/HeaderBell.tsx
import { MainColor } from "@/constants/color-palet";
import { ICON_SIZE_SMALL } from "@/constants/constans-value";
import { useNotificationStore } from "@/hooks/use-notification-store";
import { Ionicons } from "@expo/vector-icons";
import { router } from "expo-router";
import { Text, View } from "react-native";
export default function AdminNotificationBell() {
const { unreadCount } = useNotificationStore();
return (
<View style={{ position: "relative" }}>
<Ionicons
name="notifications"
size={ICON_SIZE_SMALL}
color={MainColor.white}
/>
{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

@@ -1,14 +1,17 @@
// components/HeaderBell.tsx
import { MainColor } from "@/constants/color-palet";
import { useAuth } from "@/hooks/use-auth";
import { useNotificationStore } from "@/hooks/use-notification-store";
import { Ionicons } from "@expo/vector-icons";
import { router } from "expo-router";
import { useEffect } from "react";
import { Text, View } from "react-native";
export default function HeaderBell() {
const { notifications , unreadCount} = useNotificationStore();
// console.log("NOTIF:", JSON.stringify(notifications, null, 2));
const { unreadCount } = useNotificationStore();
const { user } = useAuth();
const pathDetector =
user?.masterUserRoleId === "1" ? "/notifications" : "/admin/notification";
return (
<View style={{ position: "relative" }}>
@@ -17,7 +20,7 @@ export default function HeaderBell() {
size={20}
color={MainColor.yellow}
onPress={() => {
router.push("/notifications");
router.push(pathDetector);
}}
/>
{unreadCount > 0 && (

View File

@@ -26,10 +26,10 @@ export async function apiDeviceRegisterToken({
}
}
export async function apiDeviceTokenDeleted({ userId }: { userId: string }) {
export async function apiDeviceTokenDeleted({ userId, deviceId }: { userId: string, deviceId: string }) {
try {
const response = await apiConfig.delete(
`/mobile/auth/device-tokens/${userId}`
`/mobile/auth/device-tokens/${userId}?deviceId=${deviceId}`
);
console.log("Device token deleted:", response.data);
return response.data;

View File

@@ -1,10 +1,22 @@
import { apiConfig } from "./api-config";
type NotificationProp = {
fcmToken: string;
title: string;
body: Object;
userLoginId?: string;
body: string;
userLoginId: string;
appId?: string;
status?: string;
type?: "announcement" | "trigger";
deepLink?: string;
kategoriApp?:
| "JOB"
| "VOTING"
| "EVENT"
| "DONASI"
| "INVESTASI"
| "COLLABORATION"
| "FORUM"
| "ACCESS"; // Untuk trigger akses user;
};
export async function apiNotificationsSend({
@@ -44,11 +56,13 @@ export async function apiGetNotificationsById({
}
}
export async function apiNotificationUnreadCount({ id }: { id: string }) {
export async function apiNotificationUnreadCount({ id, role }: { id: string, role: "user" | "admin" }) {
try {
const response = await apiConfig.get(
`/mobile/notification/${id}/unread-count`
`/mobile/notification/${id}/unread-count?role=${role}`
);
console.log("Response Unread Count", response.data);
return response.data;
} catch (error) {
throw error;