diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index a43b8d0..01b4438 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -1,6 +1,4 @@ - + @@ -17,36 +15,13 @@ xmlns:tools="http://schemas.android.com/tools"> - - - - - - - + + + + + + + diff --git a/app/(application)/(user)/home.tsx b/app/(application)/(user)/home.tsx index 800890c..95cf893 100644 --- a/app/(application)/(user)/home.tsx +++ b/app/(application)/(user)/home.tsx @@ -1,6 +1,6 @@ /* eslint-disable @typescript-eslint/no-unused-vars */ /* eslint-disable react-hooks/exhaustive-deps */ -import { ButtonCustom, StackCustom, ViewWrapper } from "@/components"; +import { StackCustom, ViewWrapper } from "@/components"; import { MainColor } from "@/constants/color-palet"; import { useAuth } from "@/hooks/use-auth"; import { useNotificationStore } from "@/hooks/use-notification-store"; @@ -23,14 +23,12 @@ export default function Application() { const [refreshing, setRefreshing] = useState(false); const { syncUnreadCount } = useNotificationStore(); - - useFocusEffect( useCallback(() => { onLoadData(); checkVersion(); userData(token as string); - syncUnreadCount() + syncUnreadCount(); }, [user?.id, token]) ); @@ -56,10 +54,10 @@ export default function Application() { setRefreshing(false); }, []); - if (user && user?.termsOfServiceAccepted === false) { - console.log("User is not accept term service"); - return ; - } + // if (user && user?.termsOfServiceAccepted === false) { + // console.log("User is not accept term service"); + // return ; + // } if (data && data?.active === false) { console.log("User is not active"); diff --git a/app/(application)/(user)/job/(tabs)/_layout.tsx b/app/(application)/(user)/job/(tabs)/_layout.tsx index 48846bb..6a63193 100644 --- a/app/(application)/(user)/job/(tabs)/_layout.tsx +++ b/app/(application)/(user)/job/(tabs)/_layout.tsx @@ -1,13 +1,13 @@ +/* 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 { - Stack, + router, Tabs, useLocalSearchParams, - router, - useNavigation, + useNavigation } from "expo-router"; import { useLayoutEffect } from "react"; @@ -31,7 +31,7 @@ export default function JobTabsLayout() { if (from) { router.replace(`/${from}` as any); } else { - router.back(); + router.navigate("/home"); } } }} diff --git a/app/(application)/(user)/job/[id]/index.tsx b/app/(application)/(user)/job/[id]/index.tsx index 43356d7..a4183a1 100644 --- a/app/(application)/(user)/job/[id]/index.tsx +++ b/app/(application)/(user)/job/[id]/index.tsx @@ -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); diff --git a/app/(application)/(user)/notifications/index.tsx b/app/(application)/(user)/notifications/index.tsx index 3eba978..15be826 100644 --- a/app/(application)/(user)/notifications/index.tsx +++ b/app/(application)/(user)/notifications/index.tsx @@ -1,3 +1,4 @@ +/* eslint-disable react-hooks/exhaustive-deps */ import { BaseBox, NewWrapper, @@ -32,10 +33,18 @@ const fixPath = ({ deepLink: string; categoryApp: string; }) => { - const fixPath = - deepLink + "&from=notifications&category=" + _.lowerCase(categoryApp); + if (categoryApp === "OTHER") { + return deepLink; + } - return fixPath; + const separator = deepLink.includes("?") ? "&" : "?"; + + const fixedPath = + `${deepLink}${separator}from=notifications&category=${_.lowerCase(categoryApp)}`; + + console.log("Fix Path", fixedPath); + + return fixedPath; }; const BoxNotification = ({ @@ -114,7 +123,6 @@ export default function Notifications() { category: activeCategory as any, }); - console.log("Response Notification", JSON.stringify(response, null, 2)); if (response.success) { setListData(response.data); } else { diff --git a/app/(application)/(user)/profile/[id]/index.tsx b/app/(application)/(user)/profile/[id]/index.tsx index 8af3a02..01e8856 100644 --- a/app/(application)/(user)/profile/[id]/index.tsx +++ b/app/(application)/(user)/profile/[id]/index.tsx @@ -139,7 +139,9 @@ const ButtonnDot = ({ isUserCheck: boolean; logout: () => Promise; }) => { - const isId = id === undefined || id === null; + console.log("[ID] >>", id); + + const isId = id === undefined || id === "undefined"; if (isId) { return ( diff --git a/app/(application)/admin/job/[id]/[status]/index.tsx b/app/(application)/admin/job/[id]/[status]/index.tsx index 1345932..d1a6d0a 100644 --- a/app/(application)/admin/job/[id]/[status]/index.tsx +++ b/app/(application)/admin/job/[id]/[status]/index.tsx @@ -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(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() { - {data && data?.catatan && (status === "reject" || status === "review") && ( - - )} + {data && + data?.catatan && + (status === "reject" || status === "review") && ( + + )} {status === "review" && ( { AlertDefaultSystem({ title: "Publish", @@ -156,6 +165,7 @@ export default function AdminJobDetailStatus() { textRight: "Ya", onPressRight: () => { handleUpdate({ changeStatus: "publish" }); + setIsLoading(true); }, }); }} diff --git a/app/(application)/admin/notification/index.tsx b/app/(application)/admin/notification/index.tsx index 0ff1992..9dc0a75 100644 --- a/app/(application)/admin/notification/index.tsx +++ b/app/(application)/admin/notification/index.tsx @@ -89,7 +89,7 @@ export default function AdminNotification() { id: user?.id as any, category: activeCategory as any, }); - // console.log("Response Notification", JSON.stringify(response, null, 2)); + if (response.success) { setListData(response.data); } else { diff --git a/app/+not-found.tsx b/app/+not-found.tsx index 297838a..8356007 100644 --- a/app/+not-found.tsx +++ b/app/+not-found.tsx @@ -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 ( + <> + }} + /> - - + + 404 @@ -12,5 +21,6 @@ export default function NotFoundScreen() { - ); -} \ No newline at end of file + + ); +} diff --git a/app/eula.tsx b/app/eula.tsx new file mode 100644 index 0000000..953b70e --- /dev/null +++ b/app/eula.tsx @@ -0,0 +1,9 @@ +import EULAView from "@/screens/Authentication/EULAView"; + +export default function EULA() { + return ( + <> + + + ); +} diff --git a/context/AuthContext.tsx b/context/AuthContext.tsx index 5d87fdd..416c5c7 100644 --- a/context/AuthContext.tsx +++ b/context/AuthContext.tsx @@ -2,6 +2,7 @@ import { apiConfig, apiLogin, apiRegister, + apiUpdatedTermCondition, apiValidationCode, } from "@/service/api-config"; import { apiDeviceTokenDeleted } from "@/service/api-device-token"; @@ -29,6 +30,7 @@ type AuthContextType = { termsOfServiceAccepted: boolean; }) => Promise; userData: (token: string) => Promise; + acceptedTerms: (nomor: string) => Promise; }; // --- Create Context --- @@ -74,28 +76,32 @@ export const AuthProvider = ({ children }: { children: React.ReactNode }) => { const loginWithNomor = async (nomor: string) => { setIsLoading(true); try { - console.log("[Masuk provider]", nomor); const response = await apiLogin({ nomor: nomor }); - console.log("[RESPONSE AUTH]", JSON.stringify(response)); + console.log("[RESPONSE AUTH]", JSON.stringify(response, null, 2)); if (response.success) { - console.log("[Keluar provider]", nomor); - Toast.show({ - type: "success", - text1: "Sukses", - text2: "Kode OTP berhasil dikirim", - }); + if (response.isAcceptTerms) { + Toast.show({ + type: "success", + text1: "Sukses", + text2: "Kode OTP berhasil dikirim", + }); - await AsyncStorage.setItem("kode_otp", response.kodeId); - router.push(`/verification?nomor=${nomor}`); - return; + await AsyncStorage.setItem("kode_otp", response.kodeId); + router.push(`/verification?nomor=${nomor}`); + return; + } else { + router.push(`/eula?nomor=${nomor}`); + return; + } } else { - router.push(`/register?nomor=${nomor}`); - Toast.show({ - type: "info", - text1: "Info", - text2: "Silahkan mendaftar", - }); + router.push(`/eula?nomor=${nomor}`); + + // Toast.show({ + // type: "info", + // text1: "Info", + // text2: "Silahkan mendaftar", + // }); return; } } catch (error: any) { @@ -226,7 +232,6 @@ export const AuthProvider = ({ children }: { children: React.ReactNode }) => { setIsLoading(false); } }; - // --- 5. Logout --- @@ -256,6 +261,31 @@ export const AuthProvider = ({ children }: { children: React.ReactNode }) => { } }; + const acceptedTerms = async (nomor: string) => { + try { + setIsLoading(true); + const response = await apiUpdatedTermCondition({ nomor: nomor }); + + if (response.success) { + router.replace(`/verification?nomor=${nomor}`); + } else { + if (response.status === 404) { + router.replace(`/register?nomor=${nomor}`); + } else { + Toast.show({ + type: "error", + text1: "Error", + text2: response.message, + }); + } + } + } catch (error) { + console.log("Error accept terms", error); + } finally { + setIsLoading(false); + } + }; + return ( <> { logout, registerUser, userData, + acceptedTerms, }} > {children} diff --git a/hooks/use-foreground-notifications.ts b/hooks/use-foreground-notifications.ts index f35e162..14bd6c8 100644 --- a/hooks/use-foreground-notifications.ts +++ b/hooks/use-foreground-notifications.ts @@ -4,6 +4,7 @@ import { onMessage, FirebaseMessagingTypes, } from "@react-native-firebase/messaging"; +import { useAuth } from "./use-auth"; // Gunakan tipe resmi dari library type RemoteMessage = FirebaseMessagingTypes.RemoteMessage; @@ -11,17 +12,26 @@ type RemoteMessage = FirebaseMessagingTypes.RemoteMessage; export function useForegroundNotifications( onMessageReceived: (message: RemoteMessage) => void ) { + const { user } = useAuth(); + useEffect(() => { const messaging = getMessaging(); const unsubscribe = onMessage(messaging, (remoteMessage) => { + const data = remoteMessage.data; + // console.log("DATA NOTIFIKASI DARI SERVER", data) + if (data?.recipientId && data?.recipientId !== user?.id) { + console.log("📵 Notification untuk user lain", data); + return; + } + console.log( "🔔 Notifikasi diterima saat app aktif:", - JSON.stringify(remoteMessage, null, 2) + JSON.stringify(data, null, 2) ); onMessageReceived(remoteMessage); }); return unsubscribe; - }, [onMessageReceived]); -} \ No newline at end of file + }, [user?.id, onMessageReceived]); +} diff --git a/ios/HIPMIBadungConnect.xcodeproj/project.pbxproj b/ios/HIPMIBadungConnect.xcodeproj/project.pbxproj index 09b1789..fb1ba31 100644 --- a/ios/HIPMIBadungConnect.xcodeproj/project.pbxproj +++ b/ios/HIPMIBadungConnect.xcodeproj/project.pbxproj @@ -458,7 +458,7 @@ ); OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG"; PRODUCT_BUNDLE_IDENTIFIER = "com.anonymous.hipmi-mobile"; - PRODUCT_NAME = "HIPMIBadungConnect"; + PRODUCT_NAME = HIPMIBadungConnect; SWIFT_OBJC_BRIDGING_HEADER = "HIPMIBadungConnect/HIPMIBadungConnect-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; @@ -490,7 +490,7 @@ ); OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE"; PRODUCT_BUNDLE_IDENTIFIER = "com.anonymous.hipmi-mobile"; - PRODUCT_NAME = "HIPMIBadungConnect"; + PRODUCT_NAME = HIPMIBadungConnect; SWIFT_OBJC_BRIDGING_HEADER = "HIPMIBadungConnect/HIPMIBadungConnect-Bridging-Header.h"; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; diff --git a/screens/Admin/Job/funUpdateStatus.ts b/screens/Admin/Job/funUpdateStatus.ts index 5e82cc3..2f07cb0 100644 --- a/screens/Admin/Job/funUpdateStatus.ts +++ b/screens/Admin/Job/funUpdateStatus.ts @@ -7,13 +7,17 @@ const funUpdateStatusJob = async ({ }: { id: string; changeStatus: "publish" | "review" | "reject"; - data?: any; + 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; diff --git a/screens/Authentication/EULAView.tsx b/screens/Authentication/EULAView.tsx new file mode 100644 index 0000000..58b7ebd --- /dev/null +++ b/screens/Authentication/EULAView.tsx @@ -0,0 +1,278 @@ +// app/syarat-dan-ketentuan.tsx +import { + View, + Text, + ScrollView, + TouchableOpacity, + StyleSheet, +} from "react-native"; +import { useState, useRef } from "react"; +import { useLocalSearchParams, useRouter } from "expo-router"; +import { SafeAreaView } from "react-native-safe-area-context"; +import { AccentColor, MainColor } from "@/constants/color-palet"; +import { useAuth } from "@/hooks/use-auth"; + +// Ganti dengan API call ke backend Anda +// const acceptEula = async (): Promise => { +// try { +// const response = await fetch("/api/user/update-eula", { +// method: "PATCH", +// headers: { "Content-Type": "application/json" }, +// credentials: "include", +// body: JSON.stringify({ +// eulaAcceptedAt: new Date().toISOString(), +// eulaVersion: "2026-01-v1", // sesuaikan versi Anda +// }), +// }); +// return response.ok; +// } catch (error) { +// console.error("Gagal menyimpan persetujuan EULA:", error); +// return false; +// } +// }; + +export default function EULAView() { + const { acceptedTerms } = useAuth(); + const { nomor } = useLocalSearchParams(); + const [isLoading, setIsLoading] = useState(false); + const [isAtBottom, setIsAtBottom] = useState(false); + const scrollViewRef = useRef(null); + + const handleScroll = (event: any) => { + const { layoutMeasurement, contentOffset, contentSize } = event.nativeEvent; + const paddingToBottom = 20; + const isCloseToBottom = + layoutMeasurement.height + contentOffset.y >= + contentSize.height - paddingToBottom; + setIsAtBottom(isCloseToBottom); + }; + + const handleAccept = async () => { + try { + if (!isAtBottom) return; + + setIsLoading(true); + await acceptedTerms(nomor as string); + } catch (error) { + console.log("Error accept terms", error); + } finally { + setIsLoading(false); + } + }; + + return ( + + + Syarat & Ketentuan Penggunaan HIPMI Badung Connect + + + + + Dengan menggunakan aplikasi{" "} + HIPMI Badung Connect (“Aplikasi”), + Anda setuju untuk mematuhi dan terikat oleh syarat dan ketentuan + berikut. Jika Anda tidak setuju dengan ketentuan ini, harap jangan + gunakan Aplikasi. + + + 1. Definisi + + HIPMI Badung Connect adalah platform + digital resmi untuk anggota Himpunan Pengusaha Muda Indonesia (HIPMI) + Kabupaten Badung, yang bertujuan memfasilitasi jaringan, kolaborasi, + dan pertumbuhan bisnis para pengusaha muda. + + + 2. Larangan Konten Tidak Pantas + + Anda dilarang keras memposting, + mengirim, membagikan, atau mengunggah konten apa pun yang mengandung: + + + + • Ujaran kebencian, diskriminasi, atau konten SARA (Suku, Agama, + Ras, Antar-golongan) + + + • Pornografi, konten seksual eksplisit, atau gambar tidak senonoh + + + • Ancaman, pelecehan, bullying, atau perilaku melecehkan + + + • Informasi palsu, hoaks, spam, atau konten menyesatkan + + + • Konten ilegal, melanggar hukum, atau melanggar hak kekayaan + intelektual pihak lain + + + • Promosi narkoba, perjudian, atau aktivitas ilegal lainnya + + + + 3. Tanggung Jawab Pengguna + + Anda bertanggung jawab penuh atas setiap konten yang Anda unggah atau + bagikan melalui fitur-fitur berikut: + + + • Profil (bio, foto, portofolio) + • Forum diskusi + • Chat pribadi atau grup + + • Lowongan kerja, investasi, dan donasi + + + + Konten yang melanggar ketentuan ini dapat dihapus kapan saja tanpa + pemberitahuan. + + + 4. Tindakan terhadap Pelanggaran + + Jika kami menerima laporan atau menemukan konten yang melanggar + ketentuan ini, kami akan: + + + + • Segera menghapus konten tersebut + + + • Memberikan peringatan atau memblokir akun pengguna + + + • Dalam kasus berat, melaporkan ke pihak berwajib sesuai hukum yang + berlaku + + + + Tim kami berkomitmen untuk menanggapi laporan konten tidak pantas{" "} + dalam waktu 24 jam. + + + 5. Mekanisme Pelaporan + + Anda dapat melaporkan konten atau pengguna yang mencurigakan melalui: + + + + • Tombol “Laporkan” di setiap + posting forum atau pesan chat + + + • Tombol “Blokir Pengguna” di + profil pengguna + + + + Setiap laporan akan ditangani secara rahasia dan segera. + + + 6. Perubahan Ketentuan + + Kami berhak memperbarui Syarat & Ketentuan ini sewaktu-waktu. Versi + terbaru akan dipublikasikan di halaman ini dengan tanggal revisi yang + diperbarui. + + + 7. Kontak + + Jika Anda memiliki pertanyaan tentang ketentuan ini, silakan hubungi + kami di:{"\n"} + + bip.baliinteraktifperkasa@gmail.com + + + + + © 2026 Bali Interaktif Perkasa. All rights reserved. + + + + + + {isLoading ? "Menyimpan..." : "Saya Setuju"} + + + + ); +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + backgroundColor: MainColor.darkblue, + padding: 16, + }, + title: { + fontSize: 20, + fontWeight: "bold", + textAlign: "center", + marginBottom: 16, + color: MainColor.white, + }, + scrollView: { + flex: 1, + marginBottom: 20, + }, + scrollContent: { + paddingBottom: 30, + }, + heading: { + fontSize: 16, + fontWeight: "600", + marginTop: 16, + marginBottom: 8, + color: MainColor.white, + }, + paragraph: { + fontSize: 14, + lineHeight: 22, + color: MainColor.white, + marginBottom: 12, + }, + bold: { + fontWeight: "600", + }, + list: { + marginLeft: 8, + marginBottom: 12, + }, + listItem: { + fontSize: 14, + lineHeight: 22, + color: MainColor.white, + marginBottom: 6, + }, + footer: { + fontSize: 12, + color: MainColor.white, + textAlign: "center", + marginTop: 20, + paddingTop: 10, + borderTopWidth: 2, + borderTopColor: AccentColor.blue, + }, + button: { + backgroundColor: MainColor.yellow, + paddingVertical: 14, + borderRadius: 8, + alignItems: "center", + }, + buttonText: { + color: "#fff", + fontSize: 16, + fontWeight: "600", + }, +}); diff --git a/screens/Authentication/RegisterView.tsx b/screens/Authentication/RegisterView.tsx index 214a63b..bc9fbe7 100644 --- a/screens/Authentication/RegisterView.tsx +++ b/screens/Authentication/RegisterView.tsx @@ -1,4 +1,3 @@ -import { CheckboxCustom } from "@/components"; import Spacing from "@/components/_ShareComponent/Spacing"; import ViewWrapper from "@/components/_ShareComponent/ViewWrapper"; import ButtonCustom from "@/components/Button/ButtonCustom"; @@ -7,7 +6,6 @@ import { MainColor } from "@/constants/color-palet"; import { useAuth } from "@/hooks/use-auth"; import { BASE_URL } from "@/service/api-config"; import { GStyles } from "@/styles/global-styles"; -import { openBrowser } from "@/utils/openBrower"; import { MaterialCommunityIcons } from "@expo/vector-icons"; import { useLocalSearchParams } from "expo-router"; import { useState } from "react"; @@ -17,7 +15,7 @@ import Toast from "react-native-toast-message"; export default function RegisterView() { const { nomor } = useLocalSearchParams(); const [username, setUsername] = useState(""); - const [term, setTerm] = useState(false); + // const [term, setTerm] = useState(false); const url = BASE_URL; const { registerUser, isLoading } = useAuth(); @@ -65,7 +63,7 @@ export default function RegisterView() { await registerUser({ nomor: nomor as string, username: usernameLower, - termsOfServiceAccepted: term, + termsOfServiceAccepted: true, }); } @@ -106,12 +104,12 @@ export default function RegisterView() { flexDirection: "row", alignItems: "center", marginTop: 16, - marginBottom: 16, + // marginBottom: 16, }} > - setTerm(!term)} /> + {/* setTerm(!term)} /> */} - + {/* Saya setuju dengan{" "} {" "} yang melarang konten tidak pantas dan perilaku merugikan. - + */} diff --git a/screens/Home/bottomFeatureSection.tsx b/screens/Home/bottomFeatureSection.tsx index b952055..d01e602 100644 --- a/screens/Home/bottomFeatureSection.tsx +++ b/screens/Home/bottomFeatureSection.tsx @@ -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); diff --git a/screens/RootLayout/AppRoot.tsx b/screens/RootLayout/AppRoot.tsx index e513fbf..203d43c 100644 --- a/screens/RootLayout/AppRoot.tsx +++ b/screens/RootLayout/AppRoot.tsx @@ -15,6 +15,7 @@ export default function AppRoot() { name="index" options={{ title: "", headerBackVisible: false }} /> +