Compare commits

...

16 Commits

Author SHA1 Message Date
57ac1eb45e Notifikasi admin to user
Fix:
- android/app/src/main/AndroidManifest.xml
- app/(application)/(user)/job/(tabs)/_layout.tsx
- app/(application)/(user)/job/[id]/index.tsx
- app/(application)/(user)/notifications/index.tsx
- app/(application)/(user)/profile/[id]/index.tsx
- app/(application)/admin/job/[id]/[status]/index.tsx
- app/(application)/admin/notification/index.tsx
- app/+not-found.tsx
- ios/HIPMIBadungConnect.xcodeproj/project.pbxproj
- screens/Admin/Job/funUpdateStatus.ts
- screens/Home/bottomFeatureSection.tsx

### No Issue
2026-01-08 17:48:53 +08:00
145ad73616 Fix job notifikasi
###  No Issue
2026-01-08 10:12:53 +08:00
7c85e35c61 Note:
- Fitur notifikasi ke admin dari user baru
- Notifikasi ke user bahwa akunnya telah terverifikasi

Fix:
- app/(application)/(user)/notifications/index.tsx
- app/(application)/(user)/waiting-room.tsx
- app/(application)/admin/super-admin/[id]/index.tsx
- app/(application)/admin/user-access/[id]/index.tsx
- context/AuthContext.tsx
- screens/Home/tabsList.ts
- service/api-admin/api-admin-user-access.ts
- service/api-device-token.ts
- service/api-notifications.ts
- types/type-notification-category.ts

Add:
- lib/routeApp.ts

### No Issue
2026-01-06 12:27:30 +08:00
d098b8ca16 Setup notifikasi untuk android
Fix:
- modified:   service/api-notifications.ts
- modified:   types/type-notification-category.ts

### No Issue
2026-01-05 12:27:23 +08:00
73a473cdc7 Tambah fitur notifikasi untuk android
Add:
- android/app/src/main/res/xml/

Fix:
- android/app/src/main/AndroidManifest.xml
- android/build.gradle
- app/(application)/(user)/test-notifications.tsx
- bun.lock
- package.json

### No Issue
2026-01-02 17:43:23 +08:00
3f85f330d2 Background notifikasi berhasil dibuat
Add:
-components/Notification/BackgroundNotificationHandler.tsx

### No Issue
2025-12-24 17:43:53 +08:00
7743a2467c Fitur notifikasi dan foreground
Add:
- types/type-notification-category.ts

Fix:
- app/(application)/(user)/notifications/index.tsx
- app/(application)/(user)/test-notifications.tsx
- app/(application)/admin/notification/index.tsx
- components/Notification/NotificationInitializer.tsx
- hooks/use-notification-store.tsx
- service/api-notifications.ts
- utils/formatChatTime.ts

### No Issue
2025-12-24 15:29:58 +08:00
54611ef812 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
2025-12-23 17:45:30 +08:00
1503707eed Notifikasi terhubung ke DB
Add:
- components/Notification/
- hooks/use-notification-store.tsx.back

Fix:
- app.config.js
- app/(application)/(user)/_layout.tsx
- app/(application)/(user)/home.tsx
- app/(application)/(user)/test-notifications.tsx
- app/(application)/_layout.tsx
- app/_layout.tsx
- components/_Icon/IconComponent.tsx
- components/_Icon/IconPlus.tsx
- components/_ShareComponent/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
2025-12-19 17:54:49 +08:00
a01a9bd93f Filter console dan clean code
Add:
- service/api-device-token.ts

Fix:
- app/(application)/(user)/home.tsx
- app/(application)/(user)/test-notifications.tsx
- app/_layout.tsx
- components/_ShareComponent/NotificationInitializer.tsx
- context/AuthContext.tsx
- hooks/use-foreground-notifications.ts
- screens/Home/HeaderBell.tsx
- service/api-notifications.ts

### No Issue
2025-12-17 17:46:28 +08:00
05c1cac10f Penerapan ke database
Fix:
- android/app/src/main/AndroidManifest.xml
- app/(application)/(user)/home.tsx
- components/_ShareComponent/NotificationInitializer.tsx
- ios/HIPMIBadungConnect.xcodeproj/project.pbxproj
- ios/HIPMIBadungConnect/Info.plist
- service/api-notifications.ts

### No Issue
2025-12-16 17:47:50 +08:00
d27c01ed56 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
2025-12-15 17:46:05 +08:00
34680a4c38 test 2025-12-13 17:31:48 +08:00
43c8c105cf Notification firts commit
### No Issue
2025-12-12 17:50:49 +08:00
2c0198b1b7 Checkpoint QC Done
Cooming soon: Notification feature

### No Issue
2025-12-12 14:55:30 +08:00
573b525352 Fix QC ( Keano )
Fix:
- modified:   app/(application)/(user)/home.tsx
- modified:   app/(application)/(user)/voting/create.tsx
- modified:   components/DateInput/DataTimeAndroid.tsx
- modified:   components/DateInput/DateTimePickerCustom.tsx
- modified:   screens/Event/BoxPublishSection.tsx

### No Issue
2025-12-11 14:01:05 +08:00
63 changed files with 2382 additions and 377 deletions

View File

@@ -15,7 +15,11 @@
<data android:scheme="https"/>
</intent>
</queries>
<application android:name=".MainApplication" android:label="@string/app_name" android:icon="@mipmap/ic_launcher" android:roundIcon="@mipmap/ic_launcher_round" android:allowBackup="true" android:theme="@style/AppTheme" android:supportsRtl="true" android:enableOnBackInvokedCallback="false">
<application android:name=".MainApplication" android:label="@string/app_name" android:icon="@mipmap/ic_launcher" android:roundIcon="@mipmap/ic_launcher_round" android:allowBackup="true" android:theme="@style/AppTheme" android:supportsRtl="true" android:enableOnBackInvokedCallback="false" android:fullBackupContent="@xml/secure_store_backup_rules" android:dataExtractionRules="@xml/secure_store_data_extraction_rules">
<meta-data android:name="com.google.firebase.messaging.default_notification_color" android:resource="@color/notification_icon_color" tools:replace="android:resource"/>
<meta-data android:name="com.google.firebase.messaging.default_notification_icon" android:resource="@drawable/notification_icon"/>
<meta-data android:name="expo.modules.notifications.default_notification_color" android:resource="@color/notification_icon_color"/>
<meta-data android:name="expo.modules.notifications.default_notification_icon" android:resource="@drawable/notification_icon"/>
<meta-data android:name="expo.modules.updates.ENABLED" android:value="false"/>
<meta-data android:name="expo.modules.updates.EXPO_UPDATES_CHECK_ON_LAUNCH" android:value="ALWAYS"/>
<meta-data android:name="expo.modules.updates.EXPO_UPDATES_LAUNCH_WAIT_MS" android:value="0"/>

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 KiB

View File

@@ -3,4 +3,5 @@
<color name="iconBackground">#ffffff</color>
<color name="colorPrimary">#023c69</color>
<color name="colorPrimaryDark">#ffffff</color>
<color name="notification_icon_color">#ffffff</color>
</resources>

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<full-backup-content>
<exclude domain="sharedpref" path="SECURESTORE"/>
</full-backup-content>

View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<data-extraction-rules>
<cloud-backup>
<exclude domain="sharedpref" path="SECURESTORE"/>
</cloud-backup>
<device-transfer>
<exclude domain="sharedpref" path="SECURESTORE"/>
</device-transfer>
</data-extraction-rules>

View File

@@ -6,7 +6,7 @@ buildscript {
mavenCentral()
}
dependencies {
classpath 'com.google.gms:google-services:4.4.1'
classpath 'com.google.gms:google-services:4.4.1'
classpath('com.android.tools.build:gradle')
classpath('com.facebook.react:react-native-gradle-plugin')
classpath('org.jetbrains.kotlin:kotlin-gradle-plugin')

View File

@@ -14,12 +14,14 @@ export default {
ios: {
supportsTablet: true,
bundleIdentifier: "com.anonymous.hipmi-mobile",
googleServicesFile: "./ios/HIPMIBadungConnect/GoogleService-Info.plist",
infoPlist: {
ITSAppUsesNonExemptEncryption: false,
"NSLocationWhenInUseUsageDescription": "Aplikasi membutuhkan akses lokasi untuk menampilkan peta.",
NSLocationWhenInUseUsageDescription:
"Aplikasi membutuhkan akses lokasi untuk menampilkan peta.",
},
associatedDomains: ["applinks:cld-dkr-staging-hipmi.wibudev.com"],
buildNumber: "14",
buildNumber: "17",
},
android: {
@@ -56,7 +58,6 @@ export default {
plugins: [
"expo-router",
"expo-notifications",
"expo-web-browser",
[
"expo-splash-screen",
@@ -77,6 +78,15 @@ export default {
],
"expo-font",
"@rnmapbox/maps",
"@react-native-firebase/app",
[
"expo-notifications",
{
icon: "./assets/images/icon.png",
color: "#ffffff",
iosDisplayInForeground: true,
},
],
],
experiments: {

View File

@@ -1,4 +1,6 @@
import { BackButton } from "@/components";
import { IconPlus } from "@/components/_Icon";
import { IconDot } from "@/components/_Icon/IconComponent";
import LeftButtonCustom from "@/components/Button/BackButton";
import { MainColor } from "@/constants/color-palet";
import { ICON_SIZE_SMALL } from "@/constants/constans-value";
@@ -56,6 +58,12 @@ export default function UserLayout() {
options={{
title: "Notifikasi",
headerLeft: () => <BackButton />,
headerRight: () => (
<IconPlus
color={MainColor.yellow}
onPress={() => router.push("/test-notifications")}
/>
),
}}
/>
@@ -511,7 +519,8 @@ export default function UserLayout() {
name="job/(tabs)"
options={{
title: "Job Vacancy",
headerLeft: () => <BackButton path="/home" />,
// headerLeft: () => <BackButton path="/home" />,
// Note: headerLeft di pindahkan ke Tabs Layout
}}
/>
<Stack.Screen

View File

@@ -1,9 +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";
@@ -12,21 +14,23 @@ import { apiUser } from "@/service/api-client/api-user";
import { apiVersion } from "@/service/api-config";
import { Ionicons } from "@expo/vector-icons";
import { Redirect, router, Stack, useFocusEffect } from "expo-router";
import { useCallback, useEffect, useState } from "react";
import { useCallback, useState } from "react";
import { RefreshControl } from "react-native";
export default function Application() {
const { token, user, userData } = useAuth();
const [data, setData] = useState<any>();
const [refreshing, setRefreshing] = useState(false);
console.log("[User] >>", JSON.stringify(user?.id, null, 2));
const { syncUnreadCount } = useNotificationStore();
// ‼️ Untuk cek apakah: 1. user ada, 2. user punya profile, 3. accept temrs of forum nya ada atau tidak
useFocusEffect(
useCallback(() => {
onLoadData();
checkVersion();
userData(token as string);
syncUnreadCount()
}, [user?.id, token])
);
@@ -82,17 +86,7 @@ export default function Application() {
}}
/>
),
headerRight: () => (
<Ionicons
disabled={true}
name="notifications"
size={20}
color={MainColor.yellow}
onPress={() => {
router.push("/notifications");
}}
/>
),
headerRight: () => <HeaderBell />,
}}
/>
<ViewWrapper
@@ -109,6 +103,10 @@ export default function Application() {
}
>
<StackCustom>
{/* <ButtonCustom onPress={() => router.push("./test-notifications")}>
Test Notif
</ButtonCustom> */}
<Home_ImageSection />
<Home_FeatureSection />

View File

@@ -1,34 +1,72 @@
/* eslint-disable react-hooks/exhaustive-deps */
import { BackButton } from "@/components";
import { IconHome, IconStatus } from "@/components/_Icon";
import { TabsStyles } from "@/styles/tabs-styles";
import { Ionicons } from "@expo/vector-icons";
import { Tabs } from "expo-router";
import {
router,
Tabs,
useLocalSearchParams,
useNavigation
} from "expo-router";
import { useLayoutEffect } from "react";
export default function JobTabsLayout() {
const navigation = useNavigation();
const { from, category } = useLocalSearchParams<{
from?: string;
category?: string;
}>();
// Atur header secara dinamis
useLayoutEffect(() => {
navigation.setOptions({
headerLeft: () => (
<BackButton
onPress={() => {
if (from === "notifications") {
router.replace(`/notifications?category=${category}`);
} else {
if (from) {
router.replace(`/${from}` as any);
} else {
router.navigate("/home");
}
}
}}
/>
),
});
}, [from, router, navigation]);
return (
<Tabs screenOptions={TabsStyles}>
<Tabs.Screen
name="index"
options={{
title: "Beranda",
tabBarIcon: ({ color }) => <IconHome color={color} />,
}}
/>
<Tabs.Screen
name="status"
options={{
title: "Status",
tabBarIcon: ({ color }) => <IconStatus color={color} />,
}}
/>
<Tabs.Screen
name="archive"
options={{
title: "Arsip",
tabBarIcon: ({ color }) => (
<Ionicons size={20} name="archive" color={color} />
),
}}
/>
</Tabs>
<>
<Tabs screenOptions={TabsStyles}>
<Tabs.Screen
name="index"
options={{
title: "Beranda",
tabBarIcon: ({ color }) => <IconHome color={color} />,
}}
/>
<Tabs.Screen
name="status"
options={{
title: "Status",
tabBarIcon: ({ color }) => <IconStatus color={color} />,
}}
/>
<Tabs.Screen
name="archive"
options={{
title: "Arsip",
tabBarIcon: ({ color }) => (
<Ionicons size={20} name="archive" color={color} />
),
}}
/>
</Tabs>
</>
);
}

View File

@@ -9,14 +9,17 @@ import {
import { useAuth } from "@/hooks/use-auth";
import { dummyMasterStatus } from "@/lib/dummy-data/_master/status";
import { apiJobGetByStatus } from "@/service/api-client/api-job";
import { useFocusEffect } from "expo-router";
import { useFocusEffect, useLocalSearchParams } from "expo-router";
import _ from "lodash";
import { useCallback, useState } from "react";
export default function JobStatus() {
const { user } = useAuth();
const { status } = useLocalSearchParams<{ status?: string }>();
console.log("STATUS", status);
const [activeCategory, setActiveCategory] = useState<string | null>(
"publish"
status || "publish"
);
const [listData, setListData] = useState<any[]>([]);
const [isLoadList, setIsLoadList] = useState(false);
@@ -60,25 +63,29 @@ export default function JobStatus() {
);
return (
<ViewWrapper headerComponent={scrollComponent} hideFooter>
{isLoadList ? (
<LoaderCustom />
) : _.isEmpty(listData) ? (
<TextCustom align="center">Tidak ada data {activeCategory}</TextCustom>
) : (
listData.map((e, i) => (
<BaseBox
key={i}
paddingTop={20}
paddingBottom={20}
href={`/job/${e?.id}/${activeCategory}/detail`}
>
<TextCustom align="center" bold truncate size="large">
{e?.title}
</TextCustom>
</BaseBox>
))
)}
</ViewWrapper>
<>
<ViewWrapper headerComponent={scrollComponent} hideFooter>
{isLoadList ? (
<LoaderCustom />
) : _.isEmpty(listData) ? (
<TextCustom align="center">
Tidak ada data {activeCategory}
</TextCustom>
) : (
listData.map((e, i) => (
<BaseBox
key={i}
paddingTop={20}
paddingBottom={20}
href={`/job/${e?.id}/${activeCategory}/detail`}
>
<TextCustom align="center" bold truncate size="large">
{e?.title}
</TextCustom>
</BaseBox>
))
)}
</ViewWrapper>
</>
);
}

View File

@@ -74,7 +74,7 @@ export default function JobDetailStatus() {
<StackCustom gap={"xs"}>
{data &&
data?.catatan &&
(status === "draft" || status === "rejected") && (
(status === "draft" || status === "reject") && (
<ReportBox text={data?.catatan} />
)}

View File

@@ -25,6 +25,8 @@ export default function JobDetail() {
setIsLoading(true);
const response = await apiJobGetOne({ id: id as string });
console.log("DATA", JSON.stringify(response.data, null,2));
setData(response.data);
} catch (error) {
console.log("[ERROR]", error);

View File

@@ -19,7 +19,7 @@ import { useState } from "react";
import Toast from "react-native-toast-message";
export default function JobCreate() {
const nextUrl = "/(application)/(user)/job/(tabs)/status";
const nextUrl = "/(application)/(user)/job/(tabs)/status?status=review";
const { user } = useAuth();
const [isLoading, setIsLoading] = useState(false);
const [image, setImage] = useState<string | null>(null);

View File

@@ -1,79 +1,93 @@
/* eslint-disable react-hooks/exhaustive-deps */
import {
BaseBox,
Grid,
NewWrapper,
ScrollableCustom,
StackCustom,
TextCustom,
ViewWrapper,
} from "@/components";
import { MainColor } from "@/constants/color-palet";
import { useState } from "react";
import { View } from "react-native";
const categories = [
{ value: "all", label: "Semua" },
{ value: "event", label: "Event" },
{ value: "job", label: "Job" },
{ value: "voting", label: "Voting" },
{ value: "donasi", label: "Donasi" },
{ value: "investasi", label: "Investasi" },
{ value: "forum", label: "Forum" },
{ value: "collaboration", label: "Collaboration" },
];
import ListSkeletonComponent from "@/components/_ShareComponent/ListSkeletonComponent";
import NoDataText from "@/components/_ShareComponent/NoDataText";
import { AccentColor } from "@/constants/color-palet";
import { useAuth } from "@/hooks/use-auth";
import { useNotificationStore } from "@/hooks/use-notification-store";
import { apiGetNotificationsById } from "@/service/api-notifications";
import { listOfcategoriesAppNotification } from "@/types/type-notification-category";
import { formatChatTime } from "@/utils/formatChatTime";
import { router, useFocusEffect, useLocalSearchParams } from "expo-router";
import _ from "lodash";
import { useCallback, useState } from "react";
import { RefreshControl, View } from "react-native";
const selectedCategory = (value: string) => {
const category = categories.find((c) => c.value === value);
const category = listOfcategoriesAppNotification.find(
(c) => c.value === value
);
return category?.label;
};
const fixPath = ({
deepLink,
categoryApp,
}: {
deepLink: string;
categoryApp: string;
}) => {
if (categoryApp === "OTHER") {
return deepLink;
}
const separator = deepLink.includes("?") ? "&" : "?";
const fixedPath =
`${deepLink}${separator}from=notifications&category=${_.lowerCase(categoryApp)}`;
console.log("Fix Path", fixedPath);
return fixedPath;
};
const BoxNotification = ({
index,
data,
activeCategory,
}: {
index: number;
data: any;
activeCategory: string | null;
}) => {
// console.log("DATA NOTIFICATION", JSON.stringify(data, null, 2));
const { markAsRead } = useNotificationStore();
return (
<>
<BaseBox
onPress={() =>
console.log(
"Notification >",
selectedCategory(activeCategory as string)
)
}
backgroundColor={data.isRead ? AccentColor.darkblue : AccentColor.blue}
onPress={() => {
// console.log(
// "Notification >",
// selectedCategory(activeCategory as string)
// );
const newPath = fixPath({
deepLink: data.deepLink,
categoryApp: data.kategoriApp,
});
router.replace(newPath as any);
selectedCategory(activeCategory as string);
if (!data.isRead) {
markAsRead(data.id);
}
}}
>
<StackCustom>
<TextCustom bold>
# {selectedCategory(activeCategory as string)}
<TextCustom truncate={2} bold>
{data.title}
</TextCustom>
<View
style={{
borderBottomColor: MainColor.white_gray,
borderBottomWidth: 1,
}}
/>
<TextCustom truncate={2}>{data.pesan}</TextCustom>
<TextCustom truncate={2}>
Lorem ipsum dolor sit amet consectetur adipisicing elit. Sint odio
unde quidem voluptate quam culpa sequi molestias ipsa corrupti id,
soluta, nostrum adipisci similique, et illo asperiores deleniti eum
labore.
<TextCustom size="small" color="gray">
{formatChatTime(data.createdAt)}
</TextCustom>
<Grid>
<Grid.Col span={6}>
<TextCustom size="small" color="gray">
{index + 1} Agustus 2025
</TextCustom>
</Grid.Col>
<Grid.Col span={6} style={{ alignItems: "flex-end" }}>
<TextCustom size="small" color="gray">
Belum lihat
</TextCustom>
</Grid.Col>
</Grid>
</StackCustom>
</BaseBox>
</>
@@ -81,17 +95,57 @@ const BoxNotification = ({
};
export default function Notifications() {
const [activeCategory, setActiveCategory] = useState<string | null>("all");
const { user } = useAuth();
const { category } = useLocalSearchParams<{ category?: string }>();
const [activeCategory, setActiveCategory] = useState<string | null>(
category || "event"
);
const [listData, setListData] = useState<any[]>([]);
const [refreshing, setRefreshing] = useState(false);
const [loading, setLoading] = useState(false);
const handlePress = (item: any) => {
setActiveCategory(item.value);
// tambahkan logika lain seperti filter dsb.
};
useFocusEffect(
useCallback(() => {
fecthData();
}, [activeCategory])
);
const fecthData = async () => {
try {
setLoading(true);
const response = await apiGetNotificationsById({
id: user?.id as any,
category: activeCategory as any,
});
if (response.success) {
setListData(response.data);
} else {
setListData([]);
}
} catch (error) {
console.log("Error Notification", error);
} finally {
setLoading(false);
}
};
const onRefresh = () => {
setRefreshing(true);
fecthData();
setRefreshing(false);
};
return (
<ViewWrapper
<NewWrapper
headerComponent={
<ScrollableCustom
data={categories.map((e, i) => ({
data={listOfcategoriesAppNotification.map((e, i) => ({
id: i,
label: e.label,
value: e.value,
@@ -100,12 +154,21 @@ export default function Notifications() {
activeId={activeCategory as string}
/>
}
refreshControl={
<RefreshControl refreshing={refreshing} onRefresh={onRefresh} />
}
>
{Array.from({ length: 20 }).map((e, i) => (
<View key={i}>
<BoxNotification index={i} activeCategory={activeCategory as any} />
</View>
))}
</ViewWrapper>
{loading ? (
<ListSkeletonComponent />
) : _.isEmpty(listData) ? (
<NoDataText text="Belum ada notifikasi" />
) : (
listData.map((e, i) => (
<View key={i}>
<BoxNotification data={e} activeCategory={activeCategory as any} />
</View>
))
)}
</NewWrapper>
);
}

View File

@@ -139,7 +139,9 @@ const ButtonnDot = ({
isUserCheck: boolean;
logout: () => Promise<void>;
}) => {
const isId = id === undefined || id === null;
console.log("[ID] >>", id);
const isId = id === undefined || id === "undefined";
if (isId) {
return (

View File

@@ -0,0 +1,75 @@
import {
ButtonCustom,
NewWrapper,
StackCustom,
TextInputCustom,
} from "@/components";
import { useAuth } from "@/hooks/use-auth";
import { apiNotificationsSend } from "@/service/api-notifications";
import { useState } from "react";
import Toast from "react-native-toast-message";
export default function TestNotification() {
const { user } = useAuth();
const [data, setData] = useState("");
const [loading, setLoading] = useState(false);
const handleSubmit = async () => {
try {
console.log("[Data Dikirim]", data);
setLoading(true);
const response = await apiNotificationsSend({
data: {
title: "Test Notification !!",
body: data,
userLoginId: user?.id || "",
appId: "hipmi",
status: "publish",
kategoriApp: "JOB",
type: "announcement",
deepLink: "/job/cmhjz8u3h0005cfaxezyeilrr",
},
});
if (response.success) {
console.log("[RES SEND NOTIF]", JSON.stringify(response, null, 2));
Toast.show({
type: "success",
text1: "Notifikasi berhasil dikirim",
});
} else {
Toast.show({
type: "error",
text1: "Gagal mengirim notifikasi",
});
}
} catch (error) {
console.log("[ERROR SEND NOTIF]", error);
Toast.show({
type: "error",
text1: "Gagal mengirim notifikasi",
});
} finally {
setLoading(false);
}
};
return (
<>
<NewWrapper>
<StackCustom>
<TextInputCustom
required
label="Pesan"
placeholder="Masukkan pesan"
value={data}
onChangeText={(text) => setData(text)}
/>
<ButtonCustom onPress={handleSubmit} disabled={loading}>
Kirim
</ButtonCustom>
</StackCustom>
</NewWrapper>
</>
);
}

View File

@@ -110,7 +110,7 @@ export default function VotingCreate() {
<StackCustom gap={"xs"}>
<TextInputCustom
label="Judul Voting"
placeholder="MasukanJudul Voting"
placeholder="Masukan Judul Voting"
required
value={data.title}
onChangeText={(value: any) => setData({ ...data, title: value })}

View File

@@ -1,12 +1,10 @@
import {
AlertDefaultSystem,
BoxButtonOnFooter,
ButtonCenteredOnly,
ButtonCustom,
InformationBox,
NewWrapper,
StackCustom,
ViewWrapper,
StackCustom
} from "@/components";
import { ICON_SIZE_BUTTON } from "@/constants/constans-value";
import { useAuth } from "@/hooks/use-auth";

View File

@@ -1,15 +1,28 @@
import { BackButton } from "@/components";
import BackgroundNotificationHandler from "@/components/Notification/BackgroundNotificationHandler";
import NotificationInitializer from "@/components/Notification/NotificationInitializer";
import { NotificationProvider } from "@/hooks/use-notification-store";
import { HeaderStyles } from "@/styles/header-styles";
import { Stack } from "expo-router";
export default function ApplicationLayout() {
return (
<>
<NotificationProvider>
<NotificationInitializer />
<BackgroundNotificationHandler />
<ApplicationStack />
</NotificationProvider>
</>
);
}
function ApplicationStack() {
return (
<>
<Stack screenOptions={HeaderStyles}>
<Stack.Screen name="(user)" options={{ headerShown: false }} />
<Stack.Screen name="admin" options={{ headerShown: false }} />
{/* Take Picture */}
<Stack.Screen

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

@@ -15,6 +15,7 @@ import AdminButtonReject from "@/components/_ShareComponent/Admin/ButtonReject";
import AdminButtonReview from "@/components/_ShareComponent/Admin/ButtonReview";
import ReportBox from "@/components/Box/ReportBox";
import { MainColor } from "@/constants/color-palet";
import { useAuth } from "@/hooks/use-auth";
import funUpdateStatusJob from "@/screens/Admin/Job/funUpdateStatus";
import { apiAdminJobGetById } from "@/service/api-admin/api-admin-job";
import { router, useFocusEffect, useLocalSearchParams } from "expo-router";
@@ -23,8 +24,10 @@ import { useCallback, useState } from "react";
import Toast from "react-native-toast-message";
export default function AdminJobDetailStatus() {
const { user } = useAuth();
const { id, status } = useLocalSearchParams();
const [data, setData] = useState<any | null>(null);
const [isLoading, setIsLoading] = useState(false);
useFocusEffect(
useCallback(() => {
@@ -92,6 +95,9 @@ export default function AdminJobDetailStatus() {
const response = await funUpdateStatusJob({
id: id as string,
changeStatus,
data: {
senderId: user?.id as string,
},
});
if (!response.success) {
@@ -142,12 +148,15 @@ export default function AdminJobDetailStatus() {
</StackCustom>
</BaseBox>
{data && data?.catatan && (status === "reject" || status === "review") && (
<ReportBox text={data?.catatan}/>
)}
{data &&
data?.catatan &&
(status === "reject" || status === "review") && (
<ReportBox text={data?.catatan} />
)}
{status === "review" && (
<AdminButtonReview
isLoading={isLoading}
onPublish={() => {
AlertDefaultSystem({
title: "Publish",
@@ -156,6 +165,7 @@ export default function AdminJobDetailStatus() {
textRight: "Ya",
onPressRight: () => {
handleUpdate({ changeStatus: "publish" });
setIsLoading(true);
},
});
}}

View File

@@ -15,7 +15,10 @@ import Toast from "react-native-toast-message";
export default function AdminJobRejectInput() {
const { id, status } = useLocalSearchParams();
const [data, setData] = useState<any | null>(null);
const [data, setData] = useState({
catatan: "",
senderId: ""
});
const [isLoading, setIsLoading] = useState(false);
useFocusEffect(
@@ -48,7 +51,7 @@ export default function AdminJobRejectInput() {
const response = await funUpdateStatusJob({
id: id as string,
changeStatus,
data: data,
data: data ,
});
if (!response.success) {
@@ -102,8 +105,8 @@ export default function AdminJobRejectInput() {
headerComponent={<AdminBackButtonAntTitle title="Penolakan Job" />}
>
<TextAreaCustom
value={data}
onChangeText={setData}
value={data?.catatan}
onChangeText={(text) => setData({ ...data, catatan: text })}
placeholder="Masukan alasan"
required
showCount

View File

@@ -1,20 +1,159 @@
import { BackButton, TextCustom, ViewWrapper } from "@/components";
import { Stack } from "expo-router";
import {
BackButton,
BaseBox,
NewWrapper,
ScrollableCustom,
StackCustom,
TextCustom,
} from "@/components";
import { IconPlus } from "@/components/_Icon";
import ListSkeletonComponent from "@/components/_ShareComponent/ListSkeletonComponent";
import NoDataText from "@/components/_ShareComponent/NoDataText";
import { AccentColor, MainColor } from "@/constants/color-palet";
import { useAuth } from "@/hooks/use-auth";
import { useNotificationStore } from "@/hooks/use-notification-store";
import { apiGetNotificationsById } from "@/service/api-notifications";
import { listOfcategoriesAppNotification } from "@/types/type-notification-category";
import { formatChatTime } from "@/utils/formatChatTime";
import { router, Stack, useFocusEffect } from "expo-router";
import _ from "lodash";
import { useCallback, useState } from "react";
import { RefreshControl, View } from "react-native";
const selectedCategory = (value: string) => {
const category = listOfcategoriesAppNotification.find(
(c) => c.value === value
);
return category?.label;
};
const BoxNotification = ({
data,
activeCategory,
}: {
data: any;
activeCategory: string | null;
}) => {
const { markAsRead } = useNotificationStore();
return (
<>
<BaseBox
backgroundColor={data.isRead ? AccentColor.darkblue : AccentColor.blue}
onPress={() => {
console.log(
"Notification >",
selectedCategory(activeCategory as string)
);
router.push(data.deepLink);
markAsRead(data.id);
}}
>
<StackCustom>
<TextCustom truncate={2} bold>
{data.title}
</TextCustom>
<TextCustom truncate={2}>{data.pesan}</TextCustom>
<TextCustom size="small" color="gray">
{formatChatTime(data.createdAt)}
</TextCustom>
</StackCustom>
</BaseBox>
</>
);
};
export default function AdminNotification() {
const { user } = useAuth();
const [activeCategory, setActiveCategory] = useState<string | null>("event");
const [listData, setListData] = useState<any[]>([]);
const [refreshing, setRefreshing] = useState(false);
const [loading, setLoading] = useState(false);
const handlePress = (item: any) => {
setActiveCategory(item.value);
// tambahkan logika lain seperti filter dsb.
};
useFocusEffect(
useCallback(() => {
fecthData();
}, [activeCategory])
);
const fecthData = async () => {
try {
setLoading(true);
const response = await apiGetNotificationsById({
id: user?.id as any,
category: activeCategory as any,
});
if (response.success) {
setListData(response.data);
} else {
setListData([]);
}
} catch (error) {
console.log("Error Notification", error);
} finally {
setLoading(false);
}
};
const onRefresh = () => {
setRefreshing(true);
fecthData();
setRefreshing(false);
};
return (
<>
<Stack.Screen
options={{
title: "Admin Notifikasi",
headerLeft: () => <BackButton />,
headerRight: () => <></>,
headerRight: () => (
<IconPlus
color={MainColor.yellow}
onPress={() => router.push("/test-notifications")}
/>
),
}}
/>
<ViewWrapper>
<TextCustom>Notification</TextCustom>
</ViewWrapper>
<NewWrapper
headerComponent={
<ScrollableCustom
data={listOfcategoriesAppNotification.map((e, i) => ({
id: i,
label: e.label,
value: e.value,
}))}
onButtonPress={handlePress}
activeId={activeCategory as string}
/>
}
refreshControl={
<RefreshControl refreshing={refreshing} onRefresh={onRefresh} />
}
>
{loading ? (
<ListSkeletonComponent />
) : _.isEmpty(listData) ? (
<NoDataText text="Belum ada notifikasi" />
) : (
listData.map((e, i) => (
<View key={i}>
<BoxNotification
data={e}
activeCategory={activeCategory as any}
/>
</View>
))
)}
</NewWrapper>
</>
);
}

View File

@@ -48,6 +48,7 @@ export default function SuperAdminDetail() {
const response = await apiAdminUserAccessUpdateStatus({
id: id as string,
role: data?.masterUserRoleId === "2" ? "user" : "admin",
category: "role"
});
if (!response.success) {

View File

@@ -9,15 +9,21 @@ import {
} from "@/components";
import AdminBackButtonAntTitle from "@/components/_ShareComponent/Admin/BackButtonAntTitle";
import GridTwoView from "@/components/_ShareComponent/GridTwoView";
import { useAuth } from "@/hooks/use-auth";
import { routeUser } from "@/lib/routeApp";
import {
apiAdminUserAccessGetById,
apiAdminUserAccessUpdateStatus,
} from "@/service/api-admin/api-admin-user-access";
import {
apiNotificationsSendById
} from "@/service/api-notifications";
import { router, useFocusEffect, useLocalSearchParams } from "expo-router";
import { useCallback, useState } from "react";
import Toast from "react-native-toast-message";
export default function AdminUserAccessDetail() {
const { user } = useAuth();
const { id } = useLocalSearchParams();
const [data, setData] = useState<any | null>(null);
const [loadData, setLoadData] = useState(false);
@@ -33,6 +39,7 @@ export default function AdminUserAccessDetail() {
try {
setLoadData(true);
const response = await apiAdminUserAccessGetById({ id: id as string });
console.log("[DATA]", JSON.stringify(response.data, null, 2));
setData(response.data);
} catch (error) {
@@ -48,6 +55,7 @@ export default function AdminUserAccessDetail() {
const response = await apiAdminUserAccessUpdateStatus({
id: id as string,
active: !data?.active,
category: "access",
});
if (!response.success) {
@@ -61,6 +69,21 @@ export default function AdminUserAccessDetail() {
type: "success",
text1: "Update aktifasi berhasil ",
});
if (data.active === false) {
await apiNotificationsSendById({
data: {
title: "Akun anda telah diaktifkan",
body: "Selamat menjelajahi HIConnect",
userLoginId: user?.id || "",
kategoriApp: "OTHER",
type: "announcement",
deepLink: routeUser.home,
},
id: id as string,
});
}
router.back();
} catch (error) {
console.log("[ERROR UPDATE STATUS]", error);

View File

@@ -1,10 +1,19 @@
import { StackCustom, TextCustom, ViewWrapper } from "@/components";
import { BackButton, StackCustom, TextCustom, ViewWrapper } from "@/components";
import { Stack } from "expo-router";
export default function NotFoundScreen() {
return (
return (
<>
<Stack.Screen
options={{ headerShown: false, headerLeft: () => <BackButton /> }}
/>
<ViewWrapper>
<StackCustom align="center" gap={0} style={{justifyContent: "center", alignItems: "center", flex: 1}}>
<TextCustom size="large" bold style={{fontSize: 100}}>
<StackCustom
align="center"
gap={0}
style={{ justifyContent: "center", alignItems: "center", flex: 1 }}
>
<TextCustom size="large" bold style={{ fontSize: 100 }}>
404
</TextCustom>
<TextCustom size="large" bold>
@@ -12,5 +21,6 @@ export default function NotFoundScreen() {
</TextCustom>
</StackCustom>
</ViewWrapper>
);
}
</>
);
}

690
bun.lock

File diff suppressed because it is too large Load Diff

View File

@@ -12,10 +12,12 @@ const LeftButtonCustom = ({
path,
icon = "arrow-back",
iconCustom,
onPress,
}: {
path?: Href;
icon?: React.ReactNode | any;
iconCustom?: React.ReactNode;
onPress?: () => void;
}) => {
return (
<>
@@ -26,7 +28,7 @@ const LeftButtonCustom = ({
name={icon}
size={20}
color={MainColor.yellow}
onPress={() => (path ? router.replace(path) : router.back())}
onPress={() => (onPress ? onPress() : path ? router.replace(path) : router.back())}
/>
)}
</>

View File

@@ -53,6 +53,8 @@ const DateTimeInput_Android: React.FC<DateTimeInputProps> = ({
const [selectedDate, setSelectedDate] = useState<Date>(value as any);
const [selectedTime, setSelectedTime] = useState<Date>(value as any);
console.log("Date Android", value)
// Fungsi untuk menggabungkan tanggal dan waktu
const combineDateAndTime = useCallback((date: Date, time: Date): Date => {
const combined = new Date(date);

View File

@@ -23,6 +23,9 @@ const DateTimePickerCustom: React.FC<Props> = ({
minimumDate,
disabled = false,
}) => {
console.log("Date Android Comp", value)
return (
<>
{Platform.OS === "ios" ? (
@@ -47,6 +50,9 @@ const DateTimePickerCustom: React.FC<Props> = ({
maximumDate={maximumDate}
minimumDate={minimumDate}
disabled={disabled}
value={value as DateTimePickerEvent | Date | null | any}
/>
)}
</>

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

View File

@@ -0,0 +1,111 @@
// src/components/NotificationInitializer.tsx
import { useEffect } from "react";
import { useForegroundNotifications } from "@/hooks/use-foreground-notifications";
import { useNotificationStore } from "@/hooks/use-notification-store";
import type { FirebaseMessagingTypes } from "@react-native-firebase/messaging";
import { useAuth } from "@/hooks/use-auth";
import { Platform } from "react-native";
import * as Device from "expo-device";
import * as Application from "expo-application";
import { apiDeviceRegisterToken } from "@/service/api-device-token";
import messaging, {
isSupported,
requestPermission,
getToken,
AuthorizationStatus,
} from "@react-native-firebase/messaging";
// ✅ Modular imports (sesuai v22+)
export default function NotificationInitializer() {
// Setup handler notifikasi
const { user, logout } = useAuth(); // dari AuthContext
const { addNotification } = useNotificationStore();
// Ambil token FCM (opsional, hanya untuk log)
useEffect(() => {
if (!user) {
console.log("User not available, skipping token sync");
return;
}
const registerDeviceToken = async () => {
try {
// ✅ Dapatkan instance messaging
const messagingInstance = messaging();
// ✅ Gunakan instance sebagai argumen
const supported = await isSupported(messagingInstance);
if (!supported) {
console.log("‼️ FCM tidak didukung");
return;
}
const authStatus = await requestPermission(messagingInstance);
if (authStatus !== AuthorizationStatus.AUTHORIZED) {
console.warn("Izin telah ditolak");
return;
}
const fcmToken = await getToken(messagingInstance);
if (!fcmToken) {
logout();
return;
}
console.log("✅ FCM Token:", fcmToken);
const platform = Platform.OS; // "ios" | "android"
const model = Device.modelName || "unknown";
const appVersion =
(Application.nativeApplicationVersion || "unknown") +
"-" +
(Application.nativeBuildVersion || "unknown");
const deviceId =
Device.osInternalBuildId || Device.modelName || "unknown";
// Kirim ke backend
await apiDeviceRegisterToken({
data: {
fcmToken,
platform,
deviceId,
model,
appVersion,
userId: user?.id || "",
},
});
console.log("✅ Device token berhasil didaftarkan ke backend");
} catch (error) {
console.error("❌ Gagal mendaftarkan device token:", error);
}
};
registerDeviceToken();
}, [user?.id]);
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, type: "announcement" });
console.log("✅ Notifikasi ditambahkan ke state");
};
useForegroundNotifications(handleForegroundNotification);
return null; // komponen ini tidak merender apa-apa
}

View File

@@ -98,13 +98,14 @@ export const IconView = ({
);
};
export const IconDot = ({ size, color }: { size?: number; color?: string }) => {
export const IconDot = ({ size, color, onPress }: { size?: number; color?: string , onPress?: () => void}) => {
return (
<>
<Ionicons
name="ellipsis-vertical"
size={size || ICON_SIZE_MEDIUM}
color={color || MainColor.darkblue}
onPress={onPress}
/>
</>
);

View File

@@ -4,12 +4,21 @@ import { Octicons } from "@expo/vector-icons";
export { IconPlus };
function IconPlus({ color, size }: { color?: string; size?: number }) {
function IconPlus({
color,
size,
onPress,
}: {
color?: string;
size?: number;
onPress?: () => void;
}) {
return (
<Octicons
name="plus-circle"
size={size || ICON_SIZE_MEDIUM}
color={color || MainColor.white}
onPress={onPress}
/>
);
}

View File

@@ -4,8 +4,10 @@ import {
apiRegister,
apiValidationCode,
} from "@/service/api-config";
import { apiDeviceTokenDeleted } from "@/service/api-device-token";
import { IUser } from "@/types/User";
import AsyncStorage from "@react-native-async-storage/async-storage";
import * as Device from "expo-device";
import { router } from "expo-router";
import { createContext, useEffect, useState } from "react";
import Toast from "react-native-toast-message";
@@ -76,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({
@@ -104,18 +105,6 @@ export const AuthProvider = ({ children }: { children: React.ReactNode }) => {
}
};
// const loginWithNomor = async (nomor: string) => {
// setIsLoading(true);
// try {
// const response = await apiLogin({ nomor: nomor });
// await AsyncStorage.setItem("kode_otp", response.kodeId);
// } catch (error: any) {
// throw new Error(error.response?.data?.message || "Gagal kirim OTP");
// } finally {
// setIsLoading(false);
// }
// };
// --- 2. Validasi OTP & cek user ---
const validateOtp = async (nomor: string) => {
try {
@@ -139,13 +128,15 @@ export const AuthProvider = ({ children }: { children: React.ReactNode }) => {
await AsyncStorage.setItem("userData", JSON.stringify(dataUser));
if (response.active) {
if (response.roleId === "1") {
router.replace("/(application)/(user)/home");
return;
} else {
router.replace("/(application)/admin/dashboard");
return;
}
// if (response.roleId === "1") {
// router.replace("/(application)/(user)/home");
// return;
// } else {
// router.replace("/(application)/admin/dashboard");
// return;
// }
router.replace("/(application)/(user)/home");
return;
} else {
router.replace("/(application)/(user)/waiting-room");
return;
@@ -206,7 +197,6 @@ export const AuthProvider = ({ children }: { children: React.ReactNode }) => {
setIsLoading(true);
try {
const response = await apiRegister({ data: userData });
console.log("[REGISTER FETCH]", JSON.stringify(response, null, 2));
if (!response.success) {
Toast.show({
@@ -236,42 +226,7 @@ export const AuthProvider = ({ children }: { children: React.ReactNode }) => {
setIsLoading(false);
}
};
// const registerUser = async (userData: {
// username: string;
// nomor: string;
// termsOfServiceAccepted: boolean;
// }) => {
// setIsLoading(true);
// try {
// const response = await apiRegister({ data: userData });
// console.log("response", response);
// const { token } = response;
// if (!response.success) {
// Toast.show({
// type: "info",
// text1: "Info",
// text2: response.message,
// });
// return;
// }
// setToken(token);
// await AsyncStorage.setItem("authToken", token);
// Toast.show({
// type: "success",
// text1: "Sukses",
// text2: "Anda berhasil terdaftar",
// });
// router.replace("/(application)/(user)/waiting-room");
// return;
// } catch (error: any) {
// console.log("Error register", error);
// } finally {
// setIsLoading(false);
// }
// };
// --- 5. Logout ---
@@ -280,9 +235,13 @@ 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");
setIsLoading(false);
await apiDeviceTokenDeleted({ userId: user?.id as any, deviceId });
Toast.show({
type: "success",

View File

@@ -0,0 +1,27 @@
import { useEffect } from "react";
import {
getMessaging,
onMessage,
FirebaseMessagingTypes,
} from "@react-native-firebase/messaging";
// Gunakan tipe resmi dari library
type RemoteMessage = FirebaseMessagingTypes.RemoteMessage;
export function useForegroundNotifications(
onMessageReceived: (message: RemoteMessage) => void
) {
useEffect(() => {
const messaging = getMessaging();
const unsubscribe = onMessage(messaging, (remoteMessage) => {
console.log(
"🔔 Notifikasi diterima saat app aktif:",
JSON.stringify(remoteMessage, null, 2)
);
onMessageReceived(remoteMessage);
});
return unsubscribe;
}, [onMessageReceived]);
}

View File

@@ -0,0 +1,146 @@
// hooks/useNotificationStore.ts
import {
apiNotificationMarkAsRead,
apiNotificationUnreadCount,
} from "@/service/api-notifications";
import {
createContext,
ReactNode,
useContext,
useEffect,
useState,
} from "react";
import { useAuth } from "./use-auth";
type AppNotification = {
id: string;
title: string;
body: string;
data?: Record<string, string>;
isRead: boolean;
timestamp: number;
type: "announcement" | "trigger";
// untuk id dari setiap kategori app
appId?: string;
kategoriApp?:
| "JOB"
| "VOTING"
| "EVENT"
| "DONASI"
| "INVESTASI"
| "COLLABORATION"
| "FORUM"
| "ACCESS"; // Untuk trigger akses user;
};
type NotificationContextType = {
notifications: AppNotification[];
unreadCount: number;
addNotification: (
notif: Omit<AppNotification, "id" | "isRead" | "timestamp">
) => void;
markAsRead: (id: string) => void;
syncUnreadCount: () => Promise<void>;
};
const NotificationContext = createContext<NotificationContextType>({
notifications: [],
unreadCount: 0,
addNotification: () => {},
markAsRead: () => {},
syncUnreadCount: async () => {},
});
export const NotificationProvider = ({ children }: { children: ReactNode }) => {
const { user } = useAuth();
const [notifications, setNotifications] = useState<AppNotification[]>([]);
const [unreadCount, setUnreadCount] = useState(0);
console.log(
"🚀 Notifications Masuk:",
JSON.stringify(notifications, null, 2)
);
// Sync unread count dari backend saat provider di-mount
useEffect(() => {
fetchUnreadCount();
}, [user?.id]);
const fetchUnreadCount = async () => {
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);
setUnreadCount(result);
} catch (error) {
console.error("Gagal fetch unread count:", error);
}
};
const addNotification = (
notif: Omit<AppNotification, "id" | "isRead" | "timestamp">
) => {
setNotifications((prev) => [
{
...notif,
id: Date.now().toString(),
isRead: false,
timestamp: Date.now(),
},
...prev,
]);
setUnreadCount((prev) => prev + 1);
};
const markAsRead = async (id: string) => {
try {
const response = await apiNotificationMarkAsRead({ id });
console.log("🚀 Response Mark As Read:", response);
if (response.success) {
const cloneNotifications = [...notifications];
const index = cloneNotifications.findIndex((n) => n?.data?.id === id);
if (index !== -1) {
cloneNotifications[index].isRead = true;
setNotifications(cloneNotifications);
}
}
} catch (error) {
console.error("Gagal mark as read:", error);
}
};
const syncUnreadCount = async () => {
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);
}
};
return (
<NotificationContext.Provider
value={{
notifications,
addNotification,
markAsRead,
unreadCount,
syncUnreadCount,
}}
>
{children}
</NotificationContext.Provider>
);
};
export const useNotificationStore = () => useContext(NotificationContext);

View File

@@ -0,0 +1,113 @@
// hooks/useNotificationStore.ts
import { apiGetNotificationsById } from "@/service/api-notifications";
import { createContext, ReactNode, useContext, useState, useEffect } from "react";
import { useAuth } from "./use-auth";
type AppNotification = {
id: string;
title: string;
body: string;
data?: Record<string, string>;
isRead: boolean;
timestamp: number;
type: "notification" | "trigger";
appId?: string;
kategoriApp?:
| "JOB"
| "VOTING"
| "EVENT"
| "DONASI"
| "INVESTASI"
| "COLLABORATION"
| "FORUM"
| "ACCESS";
};
type NotificationContextType = {
notifications: AppNotification[];
unreadCount: number;
addNotification: (
notif: Omit<AppNotification, "id" | "isRead" | "timestamp">
) => void;
markAsRead: (id: string) => void;
syncUnreadCount: () => Promise<void>;
};
const NotificationContext = createContext<NotificationContextType>({
notifications: [],
unreadCount: 0,
addNotification: () => {},
markAsRead: () => {},
syncUnreadCount: async () => {},
});
export const NotificationProvider = ({ children }: { children: ReactNode }) => {
const {user} = useAuth()
const [notifications, setNotifications] = useState<AppNotification[]>([]);
const [unreadCount, setUnreadCount] = useState(0);
// 🔔 Sync unread count dari backend saat provider di-mount
useEffect(() => {
const fetchUnreadCount = async () => {
try {
const count = await apiGetNotificationsById({
id: user?.id as any,
category: "count-as-unread"
}); // ← harus return number
const result = count.data
setUnreadCount(result);
} catch (error) {
console.erro("⚠️ Gagal fetch unread count:", error);
}
};
fetchUnreadCount();
}, []);
const addNotification = (
notif: Omit<AppNotification, "id" | "isRead" | "timestamp">
) => {
setNotifications((prev) => [
{
...notif,
id: Date.now().toString(),
isRead: false,
timestamp: Date.now(),
},
...prev,
]);
// Tambahkan ke unread count (untuk notifikasi foreground)
setUnreadCount((prev) => prev + 1);
};
const markAsRead = (id: string) => {
setNotifications((prev) =>
prev.map((n) => (n.id === id ? { ...n, isRead: true } : n))
);
// Kurangi unread count
setUnreadCount((prev) => Math.max(0, prev - 1));
};
const syncUnreadCount = async () => {
try {
const count = await apiGetNotificationsById({
id: user?.id as any,
category: "count-as-unread"
}); // ← harus return number
const result = count.data
setUnreadCount(result);
} catch (error) {
console.warn("⚠️ Gagal sync unread count:", error);
}
};
return (
<NotificationContext.Provider
value={{ notifications, unreadCount, addNotification, markAsRead, syncUnreadCount }}
>
{children}
</NotificationContext.Provider>
);
};
export const useNotificationStore = () => useContext(NotificationContext);

File diff suppressed because one or more lines are too long

View File

@@ -1,4 +1,5 @@
import Expo
import FirebaseCore
import React
import ReactAppDependencyProvider
@@ -23,6 +24,9 @@ public class AppDelegate: ExpoAppDelegate {
#if os(iOS) || os(tvOS)
window = UIWindow(frame: UIScreen.main.bounds)
// @generated begin @react-native-firebase/app-didFinishLaunchingWithOptions - expo prebuild (DO NOT MODIFY) sync-10e8520570672fd76b2403b7e1e27f5198a6349a
FirebaseApp.configure()
// @generated end @react-native-firebase/app-didFinishLaunchingWithOptions
factory.startReactNative(
withModuleName: "main",
in: window,

View File

@@ -0,0 +1,30 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>API_KEY</key>
<string>AIzaSyAAvM0yBp5kCXa0hLAaNFZNqwkpBGYAWYs</string>
<key>GCM_SENDER_ID</key>
<string>608461535079</string>
<key>PLIST_VERSION</key>
<string>1</string>
<key>BUNDLE_ID</key>
<string>com.anonymous.hipmi-mobile</string>
<key>PROJECT_ID</key>
<string>hipmi-badung-connect</string>
<key>STORAGE_BUCKET</key>
<string>hipmi-badung-connect.firebasestorage.app</string>
<key>IS_ADS_ENABLED</key>
<false></false>
<key>IS_ANALYTICS_ENABLED</key>
<false></false>
<key>IS_APPINVITE_ENABLED</key>
<true></true>
<key>IS_GCM_ENABLED</key>
<true></true>
<key>IS_SIGNIN_ENABLED</key>
<true></true>
<key>GOOGLE_APP_ID</key>
<string>1:608461535079:ios:589f31b2b6b1068b6761c2</string>
</dict>
</plist>

View File

@@ -39,7 +39,7 @@
</dict>
</array>
<key>CFBundleVersion</key>
<string>14</string>
<string>17</string>
<key>ITSAppUsesNonExemptEncryption</key>
<false/>
<key>LSMinimumSystemVersion</key>
@@ -55,6 +55,8 @@
</dict>
<key>NSCameraUsageDescription</key>
<string>Allow $(PRODUCT_NAME) to access your camera</string>
<key>NSFaceIDUsageDescription</key>
<string>Allow $(PRODUCT_NAME) to access your Face ID biometric data.</string>
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
<string>Allow $(PRODUCT_NAME) to access your location</string>
<key>NSLocationAlwaysUsageDescription</key>

View File

@@ -20,6 +20,8 @@
<key>NSPrivacyAccessedAPITypeReasons</key>
<array>
<string>CA92.1</string>
<string>1C8F.1</string>
<string>C56D.1</string>
</array>
</dict>
<dict>

View File

@@ -1,3 +1,5 @@
use_modular_headers!
require File.join(File.dirname(`node --print "require.resolve('expo/package.json')"`), "scripts/autolinking")
require File.join(File.dirname(`node --print "require.resolve('react-native/package.json')"`), "scripts/react_native_pods")
@@ -33,13 +35,14 @@ target 'HIPMIBadungConnect' do
use_frameworks! :linkage => podfile_properties['ios.useFrameworks'].to_sym if podfile_properties['ios.useFrameworks']
use_frameworks! :linkage => ENV['USE_FRAMEWORKS'].to_sym if ENV['USE_FRAMEWORKS']
# @generated begin pre_installer - expo prebuild (DO NOT MODIFY) sync-c8812095000d6054b846ce74840f0ffb540c2757
# @generated begin pre_installer - expo prebuild (DO NOT MODIFY) sync-c8812095000d6054b846ce74840f0ffb540c2757
pre_install do |installer|
# @generated begin @rnmapbox/maps-pre_installer - expo prebuild (DO NOT MODIFY) sync-ea4905840bf9fcea0acc62e92aa2e784f9d760f8
# @generated begin @rnmapbox/maps-pre_installer - expo prebuild (DO NOT MODIFY) sync-ea4905840bf9fcea0acc62e92aa2e784f9d760f8
$RNMapboxMaps.pre_install(installer)
# @generated end @rnmapbox/maps-pre_installer
# @generated end @rnmapbox/maps-pre_installer
end
# @generated end pre_installer
# @generated end pre_installer
use_react_native!(
:path => config[:reactNativePath],
:hermes_enabled => podfile_properties['expo.jsEngine'] == nil || podfile_properties['expo.jsEngine'] == 'hermes',
@@ -48,10 +51,32 @@ target 'HIPMIBadungConnect' do
:privacy_file_aggregation_enabled => podfile_properties['apple.privacyManifestAggregationEnabled'] != 'false',
)
pod 'Firebase'
pod 'Firebase/Messaging'
# @generated begin post_installer - expo prebuild (DO NOT MODIFY) sync-4092f82b887b5b9edb84642c2a56984d69b9a403
post_install do |installer|
# @generated begin @rnmapbox/maps-post_installer - expo prebuild (DO NOT MODIFY) sync-c4e8f90e96f6b6c6ea9241dd7b52ab5f57f7bf36
# @generated begin @rnmapbox/maps-post_installer - expo prebuild (DO NOT MODIFY) sync-c4e8f90e96f6b6c6ea9241dd7b52ab5f57f7bf36
$RNMapboxMaps.post_install(installer)
# @generated end @rnmapbox/maps-post_installer
# @generated end @rnmapbox/maps-post_installer
# Fix all script phases with incorrect paths
installer.pods_project.targets.each do |target|
target.build_phases.each do |phase|
next unless phase.respond_to?(:shell_script)
# Fix duplicated path issue
if phase.shell_script.include?('with-environment.sh')
# Remove any existing path and use proper relative path
phase.shell_script = phase.shell_script.gsub(
%r{(/.*?/node_modules/react-native)+/scripts/xcode/with-environment.sh},
'${PODS_ROOT}/../../node_modules/react-native/scripts/xcode/with-environment.sh'
)
end
end
end
# Standard React Native post install
react_native_post_install(
installer,
config[:reactNativePath],
@@ -59,4 +84,6 @@ target 'HIPMIBadungConnect' do
:ccache_enabled => podfile_properties['apple.ccacheEnabled'] == 'true',
)
end
end
# @generated end post_installer
end

View File

@@ -239,6 +239,8 @@ PODS:
- ExpoModulesCore
- ExpoKeepAwake (15.0.7):
- ExpoModulesCore
- ExpoLinearGradient (15.0.7):
- ExpoModulesCore
- ExpoLinking (8.0.8):
- ExpoModulesCore
- ExpoLocation (19.0.7):
@@ -277,6 +279,113 @@ PODS:
- EXUpdatesInterface (2.0.0):
- ExpoModulesCore
- FBLazyVector (0.81.4)
- Firebase (12.6.0):
- Firebase/Core (= 12.6.0)
- Firebase/Core (12.6.0):
- Firebase/CoreOnly
- FirebaseAnalytics (~> 12.6.0)
- Firebase/CoreOnly (12.6.0):
- FirebaseCore (~> 12.6.0)
- Firebase/Messaging (12.6.0):
- Firebase/CoreOnly
- FirebaseMessaging (~> 12.6.0)
- FirebaseAnalytics (12.6.0):
- FirebaseAnalytics/Default (= 12.6.0)
- FirebaseCore (~> 12.6.0)
- FirebaseInstallations (~> 12.6.0)
- GoogleUtilities/AppDelegateSwizzler (~> 8.1)
- GoogleUtilities/MethodSwizzler (~> 8.1)
- GoogleUtilities/Network (~> 8.1)
- "GoogleUtilities/NSData+zlib (~> 8.1)"
- nanopb (~> 3.30910.0)
- FirebaseAnalytics/Default (12.6.0):
- FirebaseCore (~> 12.6.0)
- FirebaseInstallations (~> 12.6.0)
- GoogleAppMeasurement/Default (= 12.6.0)
- GoogleUtilities/AppDelegateSwizzler (~> 8.1)
- GoogleUtilities/MethodSwizzler (~> 8.1)
- GoogleUtilities/Network (~> 8.1)
- "GoogleUtilities/NSData+zlib (~> 8.1)"
- nanopb (~> 3.30910.0)
- FirebaseCore (12.6.0):
- FirebaseCoreInternal (~> 12.6.0)
- GoogleUtilities/Environment (~> 8.1)
- GoogleUtilities/Logger (~> 8.1)
- FirebaseCoreExtension (12.6.0):
- FirebaseCore (~> 12.6.0)
- FirebaseCoreInternal (12.6.0):
- "GoogleUtilities/NSData+zlib (~> 8.1)"
- FirebaseInstallations (12.6.0):
- FirebaseCore (~> 12.6.0)
- GoogleUtilities/Environment (~> 8.1)
- GoogleUtilities/UserDefaults (~> 8.1)
- PromisesObjC (~> 2.4)
- FirebaseMessaging (12.6.0):
- FirebaseCore (~> 12.6.0)
- FirebaseInstallations (~> 12.6.0)
- GoogleDataTransport (~> 10.1)
- GoogleUtilities/AppDelegateSwizzler (~> 8.1)
- GoogleUtilities/Environment (~> 8.1)
- GoogleUtilities/Reachability (~> 8.1)
- GoogleUtilities/UserDefaults (~> 8.1)
- nanopb (~> 3.30910.0)
- GoogleAdsOnDeviceConversion (3.2.0):
- GoogleUtilities/Environment (~> 8.1)
- GoogleUtilities/Logger (~> 8.1)
- GoogleUtilities/Network (~> 8.1)
- nanopb (~> 3.30910.0)
- GoogleAppMeasurement/Core (12.6.0):
- GoogleUtilities/AppDelegateSwizzler (~> 8.1)
- GoogleUtilities/MethodSwizzler (~> 8.1)
- GoogleUtilities/Network (~> 8.1)
- "GoogleUtilities/NSData+zlib (~> 8.1)"
- nanopb (~> 3.30910.0)
- GoogleAppMeasurement/Default (12.6.0):
- GoogleAdsOnDeviceConversion (~> 3.2.0)
- GoogleAppMeasurement/Core (= 12.6.0)
- GoogleAppMeasurement/IdentitySupport (= 12.6.0)
- GoogleUtilities/AppDelegateSwizzler (~> 8.1)
- GoogleUtilities/MethodSwizzler (~> 8.1)
- GoogleUtilities/Network (~> 8.1)
- "GoogleUtilities/NSData+zlib (~> 8.1)"
- nanopb (~> 3.30910.0)
- GoogleAppMeasurement/IdentitySupport (12.6.0):
- GoogleAppMeasurement/Core (= 12.6.0)
- GoogleUtilities/AppDelegateSwizzler (~> 8.1)
- GoogleUtilities/MethodSwizzler (~> 8.1)
- GoogleUtilities/Network (~> 8.1)
- "GoogleUtilities/NSData+zlib (~> 8.1)"
- nanopb (~> 3.30910.0)
- GoogleDataTransport (10.1.0):
- nanopb (~> 3.30910.0)
- PromisesObjC (~> 2.4)
- GoogleUtilities/AppDelegateSwizzler (8.1.0):
- GoogleUtilities/Environment
- GoogleUtilities/Logger
- GoogleUtilities/Network
- GoogleUtilities/Privacy
- GoogleUtilities/Environment (8.1.0):
- GoogleUtilities/Privacy
- GoogleUtilities/Logger (8.1.0):
- GoogleUtilities/Environment
- GoogleUtilities/Privacy
- GoogleUtilities/MethodSwizzler (8.1.0):
- GoogleUtilities/Logger
- GoogleUtilities/Privacy
- GoogleUtilities/Network (8.1.0):
- GoogleUtilities/Logger
- "GoogleUtilities/NSData+zlib"
- GoogleUtilities/Privacy
- GoogleUtilities/Reachability
- "GoogleUtilities/NSData+zlib (8.1.0)":
- GoogleUtilities/Privacy
- GoogleUtilities/Privacy (8.1.0)
- GoogleUtilities/Reachability (8.1.0):
- GoogleUtilities/Logger
- GoogleUtilities/Privacy
- GoogleUtilities/UserDefaults (8.1.0):
- GoogleUtilities/Logger
- GoogleUtilities/Privacy
- hermes-engine (0.81.4):
- hermes-engine/Pre-built (= 0.81.4)
- hermes-engine/Pre-built (0.81.4)
@@ -305,6 +414,12 @@ PODS:
- MapboxCommon (= 24.15.4)
- MapboxCoreMaps (= 11.15.4)
- Turf (= 4.0.0)
- nanopb (3.30910.0):
- nanopb/decode (= 3.30910.0)
- nanopb/encode (= 3.30910.0)
- nanopb/decode (3.30910.0)
- nanopb/encode (3.30910.0)
- PromisesObjC (2.4.0)
- RCTDeprecation (0.81.4)
- RCTRequired (0.81.4)
- RCTTypeSafety (0.81.4):
@@ -2138,6 +2253,14 @@ PODS:
- ReactCommon/turbomodule/core
- ReactNativeDependencies
- Yoga
- RNFBApp (23.7.0):
- Firebase/CoreOnly (= 12.6.0)
- React-Core
- RNFBMessaging (23.7.0):
- Firebase/Messaging (= 12.6.0)
- FirebaseCoreExtension
- React-Core
- RNFBApp
- RNGestureHandler (2.28.0):
- hermes-engine
- RCTRequired
@@ -2446,10 +2569,10 @@ PODS:
- ReactCommon/turbomodule/core
- ReactNativeDependencies
- Yoga
- SDWebImage (5.21.1):
- SDWebImage/Core (= 5.21.1)
- SDWebImage/Core (5.21.1)
- SDWebImageAVIFCoder (0.11.0):
- SDWebImage (5.21.3):
- SDWebImage/Core (= 5.21.3)
- SDWebImage/Core (5.21.3)
- SDWebImageAVIFCoder (0.11.1):
- libavif/core (>= 0.11.0)
- SDWebImage (~> 5.10)
- SDWebImageSVGCoder (1.7.0):
@@ -2489,6 +2612,7 @@ DEPENDENCIES:
- ExpoImage (from `../node_modules/expo-image/ios`)
- ExpoImagePicker (from `../node_modules/expo-image-picker/ios`)
- ExpoKeepAwake (from `../node_modules/expo-keep-awake/ios`)
- ExpoLinearGradient (from `../node_modules/expo-linear-gradient/ios`)
- ExpoLinking (from `../node_modules/expo-linking/ios`)
- ExpoLocation (from `../node_modules/expo-location/ios`)
- ExpoModulesCore (from `../node_modules/expo-modules-core`)
@@ -2498,6 +2622,8 @@ DEPENDENCIES:
- ExpoWebBrowser (from `../node_modules/expo-web-browser/ios`)
- EXUpdatesInterface (from `../node_modules/expo-updates-interface/ios`)
- FBLazyVector (from `../node_modules/react-native/Libraries/FBLazyVector`)
- Firebase
- Firebase/Messaging
- hermes-engine (from `../node_modules/react-native/sdks/hermes-engine/hermes-engine.podspec`)
- RCTDeprecation (from `../node_modules/react-native/ReactApple/Libraries/RCTFoundation/RCTDeprecation`)
- RCTRequired (from `../node_modules/react-native/Libraries/Required`)
@@ -2571,6 +2697,8 @@ DEPENDENCIES:
- ReactNativeDependencies (from `../node_modules/react-native/third-party-podspecs/ReactNativeDependencies.podspec`)
- "RNCAsyncStorage (from `../node_modules/@react-native-async-storage/async-storage`)"
- "RNDateTimePicker (from `../node_modules/@react-native-community/datetimepicker`)"
- "RNFBApp (from `../node_modules/@react-native-firebase/app`)"
- "RNFBMessaging (from `../node_modules/@react-native-firebase/messaging`)"
- RNGestureHandler (from `../node_modules/react-native-gesture-handler`)
- "rnmapbox-maps (from `../node_modules/@rnmapbox/maps`)"
- RNReanimated (from `../node_modules/react-native-reanimated`)
@@ -2582,12 +2710,25 @@ DEPENDENCIES:
SPEC REPOS:
trunk:
- Firebase
- FirebaseAnalytics
- FirebaseCore
- FirebaseCoreExtension
- FirebaseCoreInternal
- FirebaseInstallations
- FirebaseMessaging
- GoogleAdsOnDeviceConversion
- GoogleAppMeasurement
- GoogleDataTransport
- GoogleUtilities
- libavif
- libdav1d
- libwebp
- MapboxCommon
- MapboxCoreMaps
- MapboxMaps
- nanopb
- PromisesObjC
- SDWebImage
- SDWebImageAVIFCoder
- SDWebImageSVGCoder
@@ -2642,6 +2783,8 @@ EXTERNAL SOURCES:
:path: "../node_modules/expo-image-picker/ios"
ExpoKeepAwake:
:path: "../node_modules/expo-keep-awake/ios"
ExpoLinearGradient:
:path: "../node_modules/expo-linear-gradient/ios"
ExpoLinking:
:path: "../node_modules/expo-linking/ios"
ExpoLocation:
@@ -2805,6 +2948,10 @@ EXTERNAL SOURCES:
:path: "../node_modules/@react-native-async-storage/async-storage"
RNDateTimePicker:
:path: "../node_modules/@react-native-community/datetimepicker"
RNFBApp:
:path: "../node_modules/@react-native-firebase/app"
RNFBMessaging:
:path: "../node_modules/@react-native-firebase/messaging"
RNGestureHandler:
:path: "../node_modules/react-native-gesture-handler"
rnmapbox-maps:
@@ -2846,6 +2993,7 @@ SPEC CHECKSUMS:
ExpoImage: e88f500585913969b930e13a4be47277eb7c6de8
ExpoImagePicker: d251aab45a1b1857e4156fed88511b278b4eee1c
ExpoKeepAwake: 1a2e820692e933c94a565ec3fbbe38ac31658ffe
ExpoLinearGradient: a464898cb95153125e3b81894fd479bcb1c7dd27
ExpoLinking: f051f28e50ea9269ff539317c166adec81d9342d
ExpoLocation: 93d7faa0c2adbd5a04686af0c1a61bc6ed3ee2f7
ExpoModulesCore: 9281d8f1cda9d0c37dbce34c26014212b22eb8c0
@@ -2855,6 +3003,17 @@ SPEC CHECKSUMS:
ExpoWebBrowser: b973e1351fdcf5fec0c400997b1851f5a8219ec3
EXUpdatesInterface: 5adf50cb41e079c861da6d9b4b954c3db9a50734
FBLazyVector: 9e0cd874afd81d9a4d36679daca991b58b260d42
Firebase: a451a7b61536298fd5cbfe3a746fd40443a50679
FirebaseAnalytics: d0a97a0db6425e5a5d966340b87f92ca7b13a557
FirebaseCore: 0e38ad5d62d980a47a64b8e9301ffa311457be04
FirebaseCoreExtension: 032fd6f8509e591fda8cb76f6651f20d926b121f
FirebaseCoreInternal: 69bf1306a05b8ac43004f6cc1f804bb7b05b229e
FirebaseInstallations: 631b38da2e11a83daa4bfb482f79d286a5dfa7ad
FirebaseMessaging: a61bc42dcab3f7a346d94bbb54dab2c9435b18b2
GoogleAdsOnDeviceConversion: d68c69dd9581a0f5da02617b6f377e5be483970f
GoogleAppMeasurement: 3bf40aff49a601af5da1c3345702fcb4991d35ee
GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7
GoogleUtilities: 00c88b9a86066ef77f0da2fab05f65d7768ed8e1
hermes-engine: 35c763d57c9832d0eef764316ca1c4d043581394
libavif: 84bbb62fb232c3018d6f1bab79beea87e35de7b7
libdav1d: 23581a4d8ec811ff171ed5e2e05cd27bad64c39f
@@ -2862,6 +3021,8 @@ SPEC CHECKSUMS:
MapboxCommon: 975faa94b893bb64a1d28b09d9d6d820e1030a26
MapboxCoreMaps: 105af9894d850290fbb466e9f9a133f5d175abf1
MapboxMaps: e97e59d6ba48bb6f695a4c1dc2f174cb24743cd4
nanopb: fad817b59e0457d11a5dfbde799381cd727c1275
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
RCTDeprecation: 7487d6dda857ccd4cb3dd6ecfccdc3170e85dcbc
RCTRequired: 54128b7df8be566881d48c7234724a78cb9b6157
RCTTypeSafety: d2b07797a79e45d7b19e1cd2f53c79ab419fe217
@@ -2933,6 +3094,8 @@ SPEC CHECKSUMS:
ReactNativeDependencies: ed6d1e64802b150399f04f1d5728ec16b437251e
RNCAsyncStorage: 3a4f5e2777dae1688b781a487923a08569e27fe4
RNDateTimePicker: be0e44bcb9ed0607c7c5f47dbedd88cf091f6791
RNFBApp: 0c4cadae3900893d4631ea9a9effd14f853cd8e0
RNFBMessaging: 873220424f6f6aa5c787baf5c7099e1e2ba087c9
RNGestureHandler: 2914750df066d89bf9d8f48a10ad5f0051108ac3
rnmapbox-maps: 3c20ce786a7991498445c32de4fe4244e32aa0ee
RNReanimated: 8d3a14606ad49f022c17d9e12a2d339e9e5ad9b0
@@ -2940,14 +3103,14 @@ SPEC CHECKSUMS:
RNSVG: 31d6639663c249b7d5abc9728dde2041eb2a3c34
RNVectorIcons: 704d89d2f11886824e3bd5cbea34dd00aeade200
RNWorklets: 76fce72926e28e304afb44f0da23b2d24f2c1fa0
SDWebImage: f29024626962457f3470184232766516dee8dfea
SDWebImageAVIFCoder: 00310d246aab3232ce77f1d8f0076f8c4b021d90
SDWebImage: 16309af6d214ba3f77a7c6f6fdda888cb313a50a
SDWebImageAVIFCoder: afe194a084e851f70228e4be35ef651df0fc5c57
SDWebImageSVGCoder: 15a300a97ec1c8ac958f009c02220ac0402e936c
SDWebImageWebPCoder: e38c0a70396191361d60c092933e22c20d5b1380
Turf: c9eb11a65d96af58cac523460fd40fec5061b081
Yoga: 051f086b5ccf465ff2ed38a2cf5a558ae01aaaa1
ZXingObjC: 8898711ab495761b2dbbdec76d90164a6d7e14c5
PODFILE CHECKSUM: 961e5122c387eef49538723a9e3e7a469ca4144f
PODFILE CHECKSUM: 9c1ecbc7e57ca21dc7c93635b18e66f8f6d7bdd9
COCOAPODS: 1.16.2

9
lib/routeApp.ts Normal file
View File

@@ -0,0 +1,9 @@
export { routeAdmin, routeUser };
const routeAdmin = {
userAccess: ({ id }: { id: string }) => `/admin/user-access/${id}`,
};
const routeUser = {
home: `/(user)/home`,
};

View File

@@ -14,6 +14,8 @@
"@expo/vector-icons": "^15.0.2",
"@react-native-async-storage/async-storage": "2.2.0",
"@react-native-community/datetimepicker": "8.4.4",
"@react-native-firebase/app": "^23.7.0",
"@react-native-firebase/messaging": "^23.7.0",
"@react-navigation/bottom-tabs": "^7.3.10",
"@react-navigation/drawer": "^7.3.9",
"@react-navigation/elements": "^2.3.8",
@@ -72,6 +74,8 @@
},
"devDependencies": {
"@babel/core": "^7.25.2",
"@react-native-community/cli": "^20.0.2",
"@react-native/metro-config": "^0.83.1",
"@types/react": "~19.1.10",
"eslint": "^9.25.0",
"eslint-config-expo": "~10.0.0",

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

@@ -7,13 +7,17 @@ const funUpdateStatusJob = async ({
}: {
id: string;
changeStatus: "publish" | "review" | "reject";
data?: string;
data: {
catatan?: string;
senderId: string;
};
}) => {
try {
const fixData = data;
const response = await apiAdminJobUpdate({
id: id,
status: changeStatus as any,
data: data,
data: fixData as any,
});
return response;

View File

@@ -28,7 +28,7 @@ export default function Event_BoxPublishSection({
<AvatarUsernameAndOtherComponent
avatarHref={`/profile/${data?.Author?.Profile?.id}`}
name={data?.Author?.username || "-"}
rightComponent={rightComponentAvatar}
// rightComponent={rightComponentAvatar}
avatar={data?.Author?.Profile?.imageId || ""}
/>
<TextCustom truncate bold>

View File

@@ -0,0 +1,48 @@
// 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 { Text, View } from "react-native";
export default function HeaderBell() {
const { unreadCount } = useNotificationStore();
const { user } = useAuth();
const pathDetector =
user?.masterUserRoleId === "1" ? "/notifications" : "/admin/notification";
return (
<View style={{ position: "relative" }}>
<Ionicons
name="notifications"
size={20}
color={MainColor.yellow}
onPress={() => {
router.push(pathDetector);
}}
/>
{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

@@ -17,7 +17,12 @@ export default function Home_BottomFeatureSection() {
});
// console.log("[DATA JOB]", JSON.stringify(response.data, null, 2));
const result = response.data.slice(-2);
const result = response.data
.sort(
(a: any, b: any) =>
new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()
)
.slice(0, 2);
setListData(result);
} catch (error) {
console.log("[ERROR]", error);

View File

@@ -1,6 +1,13 @@
import { ITabs } from "@/components/_Interface/types";
import { Platform } from "react-native";
export const tabsHome: any = ({acceptedForumTermsAt, profileId}: {acceptedForumTermsAt: Date, profileId: string}) => [
export const tabsHome: any = ({
acceptedForumTermsAt,
profileId,
}: {
acceptedForumTermsAt: Date;
profileId: string;
}) => [
{
id: "forum",
icon: "chatbubble-ellipses-outline",
@@ -25,8 +32,8 @@ export const tabsHome: any = ({acceptedForumTermsAt, profileId}: {acceptedForumT
activeIcon: "map",
label: "Maps",
path: "/maps",
isActive: true,
disabled: false,
isActive: Platform.OS === "ios" ? true : false,
disabled: Platform.OS === "ios" ? false : true,
},
{
id: "profile",

View File

@@ -28,13 +28,15 @@ export const apiAdminUserAccessUpdateStatus = async ({
id,
active,
role,
category,
}: {
id: string;
active?: boolean;
role?: "user" | "admin" | "super_admin";
category: "access" | "role";
}) => {
try {
const response = await apiConfig.put(`/mobile/admin/user/${id}`, {
const response = await apiConfig.put(`/mobile/admin/user/${id}?category=${category}`, {
data: {
active,
role,

View File

@@ -0,0 +1,51 @@
import { apiConfig } from "./api-config";
type DeviceTokenData = {
fcmToken: string;
platform: string;
deviceId: string;
model: string;
appVersion: string;
userId: string;
};
export async function apiDeviceRegisterToken({
data,
}: {
data: DeviceTokenData;
}) {
try {
const response = await apiConfig.post(`/mobile/auth/device-tokens`, {
data: data,
});
return response.data;
} catch (error) {
console.error("Failed to register device token:", error);
throw error;
}
}
export async function apiDeviceTokenDeleted({ userId, deviceId }: { userId: string, deviceId: string }) {
try {
const response = await apiConfig.delete(
`/mobile/auth/device-tokens/${userId}?deviceId=${deviceId}`
);
return response.data;
} catch (error) {
console.error("Failed to delete device token:", error);
throw error;
}
}
export async function apiGetAllTokenDevice() {
try {
const response = await apiConfig.get(`/mobile/auth/device-tokens`);
return response.data;
} catch (error) {
console.error("Failed to delete device token:", error);
throw error;
}
}

View File

@@ -0,0 +1,88 @@
import {
NotificationProp,
TypeNotificationCategoryApp
} from "@/types/type-notification-category";
import { apiConfig } from "./api-config";
export async function apiNotificationsSend({
data,
}: {
data: NotificationProp;
}) {
try {
const response = await apiConfig.post(`/mobile/notification`, {
data: data,
});
return response.data;
} catch (error) {
throw error;
}
}
export async function apiNotificationsSendById({
data,
id,
}: {
data: NotificationProp;
id: string;
}) {
try {
const response = await apiConfig.post(`/mobile/notification/${id}`, {
data: data,
});
return response.data;
} catch (error) {
throw error;
}
}
export async function apiGetNotificationsById({
id,
category,
}: {
id: string;
category: TypeNotificationCategoryApp;
}) {
console.log("ID", id);
console.log("Category", category);
try {
const response = await apiConfig.get(
`/mobile/notification/${id}?category=${category}`
);
return response.data;
} catch (error) {
throw error;
}
}
export async function apiNotificationUnreadCount({
id,
role,
}: {
id: string;
role: "user" | "admin";
}) {
try {
const response = await apiConfig.get(
`/mobile/notification/${id}/unread-count?role=${role}`
);
console.log("Response Unread Count", response.data);
return response.data;
} catch (error) {
throw error;
}
}
export async function apiNotificationMarkAsRead({ id }: { id: string }) {
try {
const response = await apiConfig.put(`/mobile/notification/${id}`);
return response.data;
} catch (error) {
throw error;
}
}

View File

@@ -0,0 +1,35 @@
export type NotificationProp = {
title: TypeOfTilteCategoryApp | string
body: string;
userLoginId?: string;
appId?: string;
status?: string;
type?: "announcement" | "trigger";
deepLink?: string;
kategoriApp?: TypeNotificationCategoryApp
};
export type TypeNotificationCategoryApp =
| "EVENT"
| "JOB"
| "VOTING"
| "DONASI"
| "INVESTASI"
| "COLLABORATION"
| "FORUM"
| "OTHER";
export type TypeOfTilteCategoryApp = "Pendaftaran User Baru" | "Other"
export const listOfcategoriesAppNotification = [
{ value: "event", label: "Event" },
{ value: "job", label: "Job" },
{ value: "voting", label: "Voting" },
{ value: "donasi", label: "Donasi" },
{ value: "investasi", label: "Investasi" },
{ value: "forum", label: "Forum" },
{ value: "collaboration", label: "Collaboration" },
{ value: "other", label: "Lainnya" },
];

View File

@@ -17,7 +17,7 @@ export const formatChatTime = (date: string | Date): string => {
// Jika hari ini
if (messageDate.isSame(now, 'day')) {
return messageDate.format('HH.mm'); // contoh: "14.30"
return messageDate.format('HH:mm'); // contoh: "14.30"
}
// Jika kemarin
@@ -31,5 +31,5 @@ export const formatChatTime = (date: string | Date): string => {
}
// Lebih dari seminggu lalu → tampilkan tanggal
return messageDate.format('D MMM YYYY'); // contoh: "12 Mei 2024"
return messageDate.format('D MMM YYYY HH:mm'); // contoh: "12 Mei 2024 14:30"
};