Compare commits

...

3 Commits

Author SHA1 Message Date
ea3fbdc541 Penambahan metode login dengan menerapkan EULA di awal
Add:
app/eula.tsx
screens/Authentication/EULAView.tsx

Fix:
- context/AuthContext.tsx
- screens/Authentication/RegisterView.tsx
- screens/RootLayout/AppRoot.tsx
- service/api-config.ts

### No Issue
2026-01-09 17:44:13 +08:00
33cd47aaed Fix mobile notification:
> - Bug penerima pesan 2 kali

Fix:
modified:   hooks/use-foreground-notifications.ts

### No Issue
2026-01-09 14:46:21 +08:00
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
19 changed files with 443 additions and 95 deletions

View File

@@ -1,6 +1,4 @@
<manifest <manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools">
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/> <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/> <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
<uses-permission android:name="android.permission.CAMERA"/> <uses-permission android:name="android.permission.CAMERA"/>
@@ -17,36 +15,13 @@ xmlns:tools="http://schemas.android.com/tools">
<data android:scheme="https"/> <data android:scheme="https"/>
</intent> </intent>
</queries> </queries>
<application <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">
android:name=".MainApplication" <meta-data android:name="com.google.firebase.messaging.default_notification_color" android:resource="@color/notification_icon_color" tools:replace="android:resource"/>
android:label="@string/app_name" <meta-data android:name="com.google.firebase.messaging.default_notification_icon" android:resource="@drawable/notification_icon"/>
android:icon="@mipmap/ic_launcher" <meta-data android:name="expo.modules.notifications.default_notification_color" android:resource="@color/notification_icon_color"/>
android:roundIcon="@mipmap/ic_launcher_round" <meta-data android:name="expo.modules.notifications.default_notification_icon" android:resource="@drawable/notification_icon"/>
android:allowBackup="true" <meta-data android:name="expo.modules.updates.ENABLED" android:value="false"/>
android:theme="@style/AppTheme" <meta-data android:name="expo.modules.updates.EXPO_UPDATES_CHECK_ON_LAUNCH" android:value="ALWAYS"/>
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"/> <meta-data android:name="expo.modules.updates.EXPO_UPDATES_LAUNCH_WAIT_MS" android:value="0"/>
<activity android:name=".MainActivity" android:configChanges="keyboard|keyboardHidden|orientation|screenSize|screenLayout|uiMode" android:launchMode="singleTask" android:windowSoftInputMode="adjustResize" android:theme="@style/Theme.App.SplashScreen" android:exported="true" android:screenOrientation="portrait"> <activity android:name=".MainActivity" android:configChanges="keyboard|keyboardHidden|orientation|screenSize|screenLayout|uiMode" android:launchMode="singleTask" android:windowSoftInputMode="adjustResize" android:theme="@style/Theme.App.SplashScreen" android:exported="true" android:screenOrientation="portrait">
<intent-filter> <intent-filter>

View File

@@ -1,6 +1,6 @@
/* eslint-disable @typescript-eslint/no-unused-vars */ /* eslint-disable @typescript-eslint/no-unused-vars */
/* eslint-disable react-hooks/exhaustive-deps */ /* eslint-disable react-hooks/exhaustive-deps */
import { ButtonCustom, StackCustom, ViewWrapper } from "@/components"; import { StackCustom, ViewWrapper } from "@/components";
import { MainColor } from "@/constants/color-palet"; import { MainColor } from "@/constants/color-palet";
import { useAuth } from "@/hooks/use-auth"; import { useAuth } from "@/hooks/use-auth";
import { useNotificationStore } from "@/hooks/use-notification-store"; import { useNotificationStore } from "@/hooks/use-notification-store";
@@ -23,14 +23,12 @@ export default function Application() {
const [refreshing, setRefreshing] = useState(false); const [refreshing, setRefreshing] = useState(false);
const { syncUnreadCount } = useNotificationStore(); const { syncUnreadCount } = useNotificationStore();
useFocusEffect( useFocusEffect(
useCallback(() => { useCallback(() => {
onLoadData(); onLoadData();
checkVersion(); checkVersion();
userData(token as string); userData(token as string);
syncUnreadCount() syncUnreadCount();
}, [user?.id, token]) }, [user?.id, token])
); );
@@ -56,10 +54,10 @@ export default function Application() {
setRefreshing(false); setRefreshing(false);
}, []); }, []);
if (user && user?.termsOfServiceAccepted === false) { // if (user && user?.termsOfServiceAccepted === false) {
console.log("User is not accept term service"); // console.log("User is not accept term service");
return <Redirect href={`/terms-agreement`} />; // return <Redirect href={`/terms-agreement`} />;
} // }
if (data && data?.active === false) { if (data && data?.active === false) {
console.log("User is not active"); console.log("User is not active");

View File

@@ -1,13 +1,13 @@
/* eslint-disable react-hooks/exhaustive-deps */
import { BackButton } from "@/components"; import { BackButton } from "@/components";
import { IconHome, IconStatus } from "@/components/_Icon"; import { IconHome, IconStatus } from "@/components/_Icon";
import { TabsStyles } from "@/styles/tabs-styles"; import { TabsStyles } from "@/styles/tabs-styles";
import { Ionicons } from "@expo/vector-icons"; import { Ionicons } from "@expo/vector-icons";
import { import {
Stack, router,
Tabs, Tabs,
useLocalSearchParams, useLocalSearchParams,
router, useNavigation
useNavigation,
} from "expo-router"; } from "expo-router";
import { useLayoutEffect } from "react"; import { useLayoutEffect } from "react";
@@ -31,7 +31,7 @@ export default function JobTabsLayout() {
if (from) { if (from) {
router.replace(`/${from}` as any); router.replace(`/${from}` as any);
} else { } else {
router.back(); router.navigate("/home");
} }
} }
}} }}

View File

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

View File

@@ -1,3 +1,4 @@
/* eslint-disable react-hooks/exhaustive-deps */
import { import {
BaseBox, BaseBox,
NewWrapper, NewWrapper,
@@ -32,10 +33,18 @@ const fixPath = ({
deepLink: string; deepLink: string;
categoryApp: string; categoryApp: string;
}) => { }) => {
const fixPath = if (categoryApp === "OTHER") {
deepLink + "&from=notifications&category=" + _.lowerCase(categoryApp); 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 = ({ const BoxNotification = ({
@@ -114,7 +123,6 @@ export default function Notifications() {
category: activeCategory as any, category: activeCategory as any,
}); });
console.log("Response Notification", JSON.stringify(response, null, 2));
if (response.success) { if (response.success) {
setListData(response.data); setListData(response.data);
} else { } else {

View File

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

View File

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

View File

@@ -89,7 +89,7 @@ export default function AdminNotification() {
id: user?.id as any, id: user?.id as any,
category: activeCategory as any, category: activeCategory as any,
}); });
// console.log("Response Notification", JSON.stringify(response, null, 2));
if (response.success) { if (response.success) {
setListData(response.data); setListData(response.data);
} else { } else {

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

9
app/eula.tsx Normal file
View File

@@ -0,0 +1,9 @@
import EULAView from "@/screens/Authentication/EULAView";
export default function EULA() {
return (
<>
<EULAView />
</>
);
}

View File

@@ -2,6 +2,7 @@ import {
apiConfig, apiConfig,
apiLogin, apiLogin,
apiRegister, apiRegister,
apiUpdatedTermCondition,
apiValidationCode, apiValidationCode,
} from "@/service/api-config"; } from "@/service/api-config";
import { apiDeviceTokenDeleted } from "@/service/api-device-token"; import { apiDeviceTokenDeleted } from "@/service/api-device-token";
@@ -29,6 +30,7 @@ type AuthContextType = {
termsOfServiceAccepted: boolean; termsOfServiceAccepted: boolean;
}) => Promise<void>; }) => Promise<void>;
userData: (token: string) => Promise<any>; userData: (token: string) => Promise<any>;
acceptedTerms: (nomor: string) => Promise<any>;
}; };
// --- Create Context --- // --- Create Context ---
@@ -74,28 +76,32 @@ export const AuthProvider = ({ children }: { children: React.ReactNode }) => {
const loginWithNomor = async (nomor: string) => { const loginWithNomor = async (nomor: string) => {
setIsLoading(true); setIsLoading(true);
try { try {
console.log("[Masuk provider]", nomor);
const response = await apiLogin({ nomor: 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) { if (response.success) {
console.log("[Keluar provider]", nomor); if (response.isAcceptTerms) {
Toast.show({ Toast.show({
type: "success", type: "success",
text1: "Sukses", text1: "Sukses",
text2: "Kode OTP berhasil dikirim", text2: "Kode OTP berhasil dikirim",
}); });
await AsyncStorage.setItem("kode_otp", response.kodeId); await AsyncStorage.setItem("kode_otp", response.kodeId);
router.push(`/verification?nomor=${nomor}`); router.push(`/verification?nomor=${nomor}`);
return; return;
} else {
router.push(`/eula?nomor=${nomor}`);
return;
}
} else { } else {
router.push(`/register?nomor=${nomor}`); router.push(`/eula?nomor=${nomor}`);
Toast.show({
type: "info", // Toast.show({
text1: "Info", // type: "info",
text2: "Silahkan mendaftar", // text1: "Info",
}); // text2: "Silahkan mendaftar",
// });
return; return;
} }
} catch (error: any) { } catch (error: any) {
@@ -226,7 +232,6 @@ export const AuthProvider = ({ children }: { children: React.ReactNode }) => {
setIsLoading(false); setIsLoading(false);
} }
}; };
// --- 5. Logout --- // --- 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 ( return (
<> <>
<AuthContext.Provider <AuthContext.Provider
@@ -271,6 +301,7 @@ export const AuthProvider = ({ children }: { children: React.ReactNode }) => {
logout, logout,
registerUser, registerUser,
userData, userData,
acceptedTerms,
}} }}
> >
{children} {children}

View File

@@ -4,6 +4,7 @@ import {
onMessage, onMessage,
FirebaseMessagingTypes, FirebaseMessagingTypes,
} from "@react-native-firebase/messaging"; } from "@react-native-firebase/messaging";
import { useAuth } from "./use-auth";
// Gunakan tipe resmi dari library // Gunakan tipe resmi dari library
type RemoteMessage = FirebaseMessagingTypes.RemoteMessage; type RemoteMessage = FirebaseMessagingTypes.RemoteMessage;
@@ -11,17 +12,26 @@ type RemoteMessage = FirebaseMessagingTypes.RemoteMessage;
export function useForegroundNotifications( export function useForegroundNotifications(
onMessageReceived: (message: RemoteMessage) => void onMessageReceived: (message: RemoteMessage) => void
) { ) {
const { user } = useAuth();
useEffect(() => { useEffect(() => {
const messaging = getMessaging(); const messaging = getMessaging();
const unsubscribe = onMessage(messaging, (remoteMessage) => { 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( console.log(
"🔔 Notifikasi diterima saat app aktif:", "🔔 Notifikasi diterima saat app aktif:",
JSON.stringify(remoteMessage, null, 2) JSON.stringify(data, null, 2)
); );
onMessageReceived(remoteMessage); onMessageReceived(remoteMessage);
}); });
return unsubscribe; return unsubscribe;
}, [onMessageReceived]); }, [user?.id, onMessageReceived]);
} }

View File

@@ -458,7 +458,7 @@
); );
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG"; OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG";
PRODUCT_BUNDLE_IDENTIFIER = "com.anonymous.hipmi-mobile"; PRODUCT_BUNDLE_IDENTIFIER = "com.anonymous.hipmi-mobile";
PRODUCT_NAME = "HIPMIBadungConnect"; PRODUCT_NAME = HIPMIBadungConnect;
SWIFT_OBJC_BRIDGING_HEADER = "HIPMIBadungConnect/HIPMIBadungConnect-Bridging-Header.h"; SWIFT_OBJC_BRIDGING_HEADER = "HIPMIBadungConnect/HIPMIBadungConnect-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;
@@ -490,7 +490,7 @@
); );
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE"; OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
PRODUCT_BUNDLE_IDENTIFIER = "com.anonymous.hipmi-mobile"; PRODUCT_BUNDLE_IDENTIFIER = "com.anonymous.hipmi-mobile";
PRODUCT_NAME = "HIPMIBadungConnect"; PRODUCT_NAME = HIPMIBadungConnect;
SWIFT_OBJC_BRIDGING_HEADER = "HIPMIBadungConnect/HIPMIBadungConnect-Bridging-Header.h"; SWIFT_OBJC_BRIDGING_HEADER = "HIPMIBadungConnect/HIPMIBadungConnect-Bridging-Header.h";
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2"; TARGETED_DEVICE_FAMILY = "1,2";

View File

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

View File

@@ -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<boolean> => {
// 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<boolean>(false);
const [isAtBottom, setIsAtBottom] = useState(false);
const scrollViewRef = useRef<ScrollView>(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 (
<SafeAreaView edges={["bottom"]} style={styles.container}>
<Text style={styles.title}>
Syarat & Ketentuan Penggunaan HIPMI Badung Connect
</Text>
<ScrollView
ref={scrollViewRef}
onScroll={handleScroll}
scrollEventThrottle={16}
style={styles.scrollView}
contentContainerStyle={styles.scrollContent}
>
<Text style={styles.paragraph}>
Dengan menggunakan aplikasi{" "}
<Text style={styles.bold}>HIPMI Badung Connect</Text> (Aplikasi),
Anda setuju untuk mematuhi dan terikat oleh syarat dan ketentuan
berikut. Jika Anda tidak setuju dengan ketentuan ini, harap jangan
gunakan Aplikasi.
</Text>
<Text style={styles.heading}>1. Definisi</Text>
<Text style={styles.paragraph}>
<Text style={styles.bold}>HIPMI Badung Connect</Text> adalah platform
digital resmi untuk anggota Himpunan Pengusaha Muda Indonesia (HIPMI)
Kabupaten Badung, yang bertujuan memfasilitasi jaringan, kolaborasi,
dan pertumbuhan bisnis para pengusaha muda.
</Text>
<Text style={styles.heading}>2. Larangan Konten Tidak Pantas</Text>
<Text style={styles.paragraph}>
Anda <Text style={styles.bold}>dilarang keras</Text> memposting,
mengirim, membagikan, atau mengunggah konten apa pun yang mengandung:
</Text>
<View style={styles.list}>
<Text style={styles.listItem}>
Ujaran kebencian, diskriminasi, atau konten SARA (Suku, Agama,
Ras, Antar-golongan)
</Text>
<Text style={styles.listItem}>
Pornografi, konten seksual eksplisit, atau gambar tidak senonoh
</Text>
<Text style={styles.listItem}>
Ancaman, pelecehan, bullying, atau perilaku melecehkan
</Text>
<Text style={styles.listItem}>
Informasi palsu, hoaks, spam, atau konten menyesatkan
</Text>
<Text style={styles.listItem}>
Konten ilegal, melanggar hukum, atau melanggar hak kekayaan
intelektual pihak lain
</Text>
<Text style={styles.listItem}>
Promosi narkoba, perjudian, atau aktivitas ilegal lainnya
</Text>
</View>
<Text style={styles.heading}>3. Tanggung Jawab Pengguna</Text>
<Text style={styles.paragraph}>
Anda bertanggung jawab penuh atas setiap konten yang Anda unggah atau
bagikan melalui fitur-fitur berikut:
</Text>
<View style={styles.list}>
<Text style={styles.listItem}> Profil (bio, foto, portofolio)</Text>
<Text style={styles.listItem}> Forum diskusi</Text>
<Text style={styles.listItem}> Chat pribadi atau grup</Text>
<Text style={styles.listItem}>
Lowongan kerja, investasi, dan donasi
</Text>
</View>
<Text style={styles.paragraph}>
Konten yang melanggar ketentuan ini dapat dihapus kapan saja tanpa
pemberitahuan.
</Text>
<Text style={styles.heading}>4. Tindakan terhadap Pelanggaran</Text>
<Text style={styles.paragraph}>
Jika kami menerima laporan atau menemukan konten yang melanggar
ketentuan ini, kami akan:
</Text>
<View style={styles.list}>
<Text style={styles.listItem}>
Segera menghapus konten tersebut
</Text>
<Text style={styles.listItem}>
Memberikan peringatan atau memblokir akun pengguna
</Text>
<Text style={styles.listItem}>
Dalam kasus berat, melaporkan ke pihak berwajib sesuai hukum yang
berlaku
</Text>
</View>
<Text style={styles.paragraph}>
Tim kami berkomitmen untuk menanggapi laporan konten tidak pantas{" "}
<Text style={styles.bold}>dalam waktu 24 jam</Text>.
</Text>
<Text style={styles.heading}>5. Mekanisme Pelaporan</Text>
<Text style={styles.paragraph}>
Anda dapat melaporkan konten atau pengguna yang mencurigakan melalui:
</Text>
<View style={styles.list}>
<Text style={styles.listItem}>
Tombol <Text style={styles.bold}>Laporkan</Text> di setiap
posting forum atau pesan chat
</Text>
<Text style={styles.listItem}>
Tombol <Text style={styles.bold}>Blokir Pengguna</Text> di
profil pengguna
</Text>
</View>
<Text style={styles.paragraph}>
Setiap laporan akan ditangani secara rahasia dan segera.
</Text>
<Text style={styles.heading}>6. Perubahan Ketentuan</Text>
<Text style={styles.paragraph}>
Kami berhak memperbarui Syarat & Ketentuan ini sewaktu-waktu. Versi
terbaru akan dipublikasikan di halaman ini dengan tanggal revisi yang
diperbarui.
</Text>
<Text style={styles.heading}>7. Kontak</Text>
<Text style={styles.paragraph}>
Jika Anda memiliki pertanyaan tentang ketentuan ini, silakan hubungi
kami di:{"\n"}
<Text style={[styles.bold, { color: "#1E90FF" }]}>
bip.baliinteraktifperkasa@gmail.com
</Text>
</Text>
<Text style={styles.footer}>
© 2026 Bali Interaktif Perkasa. All rights reserved.
</Text>
</ScrollView>
<TouchableOpacity
onPress={handleAccept}
disabled={!isAtBottom || isLoading}
style={[styles.button, { opacity: !isAtBottom || isLoading ? 0.6 : 1 }]}
>
<Text style={styles.buttonText}>
{isLoading ? "Menyimpan..." : "Saya Setuju"}
</Text>
</TouchableOpacity>
</SafeAreaView>
);
}
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",
},
});

View File

@@ -1,4 +1,3 @@
import { CheckboxCustom } from "@/components";
import Spacing from "@/components/_ShareComponent/Spacing"; import Spacing from "@/components/_ShareComponent/Spacing";
import ViewWrapper from "@/components/_ShareComponent/ViewWrapper"; import ViewWrapper from "@/components/_ShareComponent/ViewWrapper";
import ButtonCustom from "@/components/Button/ButtonCustom"; import ButtonCustom from "@/components/Button/ButtonCustom";
@@ -7,7 +6,6 @@ import { MainColor } from "@/constants/color-palet";
import { useAuth } from "@/hooks/use-auth"; import { useAuth } from "@/hooks/use-auth";
import { BASE_URL } from "@/service/api-config"; import { BASE_URL } from "@/service/api-config";
import { GStyles } from "@/styles/global-styles"; import { GStyles } from "@/styles/global-styles";
import { openBrowser } from "@/utils/openBrower";
import { MaterialCommunityIcons } from "@expo/vector-icons"; import { MaterialCommunityIcons } from "@expo/vector-icons";
import { useLocalSearchParams } from "expo-router"; import { useLocalSearchParams } from "expo-router";
import { useState } from "react"; import { useState } from "react";
@@ -17,7 +15,7 @@ import Toast from "react-native-toast-message";
export default function RegisterView() { export default function RegisterView() {
const { nomor } = useLocalSearchParams(); const { nomor } = useLocalSearchParams();
const [username, setUsername] = useState(""); const [username, setUsername] = useState("");
const [term, setTerm] = useState(false); // const [term, setTerm] = useState(false);
const url = BASE_URL; const url = BASE_URL;
const { registerUser, isLoading } = useAuth(); const { registerUser, isLoading } = useAuth();
@@ -65,7 +63,7 @@ export default function RegisterView() {
await registerUser({ await registerUser({
nomor: nomor as string, nomor: nomor as string,
username: usernameLower, username: usernameLower,
termsOfServiceAccepted: term, termsOfServiceAccepted: true,
}); });
} }
@@ -106,12 +104,12 @@ export default function RegisterView() {
flexDirection: "row", flexDirection: "row",
alignItems: "center", alignItems: "center",
marginTop: 16, marginTop: 16,
marginBottom: 16, // marginBottom: 16,
}} }}
> >
<CheckboxCustom value={term} onChange={() => setTerm(!term)} /> {/* <CheckboxCustom value={term} onChange={() => setTerm(!term)} /> */}
<Text style={GStyles.textLabel}> {/* <Text style={GStyles.textLabel}>
Saya setuju dengan{" "} Saya setuju dengan{" "}
<Text <Text
style={{ style={{
@@ -126,11 +124,11 @@ export default function RegisterView() {
Syarat & Ketentuan Syarat & Ketentuan
</Text>{" "} </Text>{" "}
yang melarang konten tidak pantas dan perilaku merugikan. yang melarang konten tidak pantas dan perilaku merugikan.
</Text> </Text> */}
</View> </View>
<ButtonCustom <ButtonCustom
disabled={!term} disabled={isLoading}
isLoading={isLoading} isLoading={isLoading}
onPress={handleRegister} onPress={handleRegister}
> >

View File

@@ -17,7 +17,12 @@ export default function Home_BottomFeatureSection() {
}); });
// console.log("[DATA JOB]", JSON.stringify(response.data, null, 2)); // 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); setListData(result);
} catch (error) { } catch (error) {
console.log("[ERROR]", error); console.log("[ERROR]", error);

View File

@@ -15,6 +15,7 @@ export default function AppRoot() {
name="index" name="index"
options={{ title: "", headerBackVisible: false }} options={{ title: "", headerBackVisible: false }}
/> />
<Stack.Screen name="eula" options={{ title: "Terms & Conditions", headerBackVisible: false }} />
<Stack.Screen name="+not-found" options={{ title: "" }} /> <Stack.Screen name="+not-found" options={{ title: "" }} />
<Stack.Screen <Stack.Screen
name="verification" name="verification"

View File

@@ -74,3 +74,10 @@ export async function apiAcceptTermService({
return response.data; return response.data;
} }
export async function apiUpdatedTermCondition({nomor}: {nomor: string}) {
const response = await apiConfig.post(`/auth/mobile-eula`, {
nomor: nomor,
});
return response.data;
}