From 21c6460220f7eedb8fb9f04dda42128e0de99675 Mon Sep 17 00:00:00 2001 From: Bagasbanuna02 Date: Thu, 21 Aug 2025 15:22:14 +0800 Subject: [PATCH] API Add: - hooks/ - ios.build.device : untuk mendownload di ios Fix: - service/api.t : mengatur api - context/AuthContext.tsx: Provider untuk access token ### No Issue --- .../(user)/profile/[id]/index.tsx | 4 + app/(application)/(user)/waiting-room.tsx | 85 ++++++++- app/(application)/admin/_layout.tsx | 30 +-- components/Button/ButtonCenteredOnly.tsx | 11 +- context/AuthContext.tsx | 178 ++++++++++++++++-- {hook => hooks}/use-auth.ts | 0 ios.build.device | 1 + screens/Authentication/LoginView.tsx | 31 ++- screens/Authentication/RegisterView.tsx | 42 ++--- screens/Authentication/VerificationView.tsx | 70 +++---- screens/Profile/menuDrawerSection.tsx | 16 +- service/api.ts | 54 ++---- 12 files changed, 380 insertions(+), 142 deletions(-) rename {hook => hooks}/use-auth.ts (100%) create mode 100644 ios.build.device diff --git a/app/(application)/(user)/profile/[id]/index.tsx b/app/(application)/(user)/profile/[id]/index.tsx index ba375dc..ab423ad 100644 --- a/app/(application)/(user)/profile/[id]/index.tsx +++ b/app/(application)/(user)/profile/[id]/index.tsx @@ -3,6 +3,7 @@ import AlertCustom from "@/components/Alert/AlertCustom"; import LeftButtonCustom from "@/components/Button/BackButton"; import DrawerCustom from "@/components/Drawer/DrawerCustom"; import { MainColor } from "@/constants/color-palet"; +import { useAuth } from "@/hooks/use-auth"; import { drawerItemsProfile } from "@/screens/Profile/ListPage"; import Profile_MenuDrawerSection from "@/screens/Profile/menuDrawerSection"; import ProfileSection from "@/screens/Profile/ProfileSection"; @@ -17,6 +18,8 @@ export default function Profile() { const [isDrawerOpen, setIsDrawerOpen] = useState(false); const [showLogoutAlert, setShowLogoutAlert] = useState(false); + const { logout } = useAuth(); + const openDrawer = () => { setIsDrawerOpen(true); }; @@ -65,6 +68,7 @@ export default function Profile() { drawerItems={drawerItemsProfile({ id: id as string })} setShowLogoutAlert={setShowLogoutAlert} setIsDrawerOpen={setIsDrawerOpen} + logout={logout} /> diff --git a/app/(application)/(user)/waiting-room.tsx b/app/(application)/(user)/waiting-room.tsx index 5b01dc9..8a1b33d 100644 --- a/app/(application)/(user)/waiting-room.tsx +++ b/app/(application)/(user)/waiting-room.tsx @@ -1,12 +1,87 @@ -import { InformationBox, ViewWrapper } from "@/components"; +import { + AlertDefaultSystem, + BoxButtonOnFooter, + ButtonCenteredOnly, + ButtonCustom, + InformationBox, + StackCustom, + ViewWrapper, +} from "@/components"; +import { ICON_SIZE_BUTTON } from "@/constants/constans-value"; +import { useAuth } from "@/hooks/use-auth"; +import { Ionicons } from "@expo/vector-icons"; +import { router } from "expo-router"; +import Toast from "react-native-toast-message"; export default function WaitingRoom() { + const { token, userData, isLoading, logout } = useAuth(); + + async function handleCheck() { + try { + const response = await userData(token as string); + console.log("response check", JSON.stringify(response, null, 2)); + if (response.active) { + Toast.show({ + type: "success", + text1: "Akun anda telah aktif", // text2: "Anda berhasil login", + }); + router.replace("/(application)/(user)/home"); + } else { + Toast.show({ + type: "error", + text1: "Akun anda belum aktif", + text2: "Silahkan hubungi admin", + }); + } + } catch (error) { + console.log("Error check", error); + } + } + + const logoutButton = () => { + return ( + <> + + + } + onPress={() => { + AlertDefaultSystem({ + title: "Keluar", + message: "Apakah anda yakin ingin keluar?", + textLeft: "Batal", + textRight: "Ya", + onPressRight: () => { + logout(); + }, + }) + }} + > + Keluar + + + + ); + }; + return ( <> - - + + + + { + handleCheck(); + }} + icon="refresh-ccw" + > + Check + + ); diff --git a/app/(application)/admin/_layout.tsx b/app/(application)/admin/_layout.tsx index 4ea3188..4709c5d 100644 --- a/app/(application)/admin/_layout.tsx +++ b/app/(application)/admin/_layout.tsx @@ -9,7 +9,12 @@ import { import DrawerAdmin from "@/components/Drawer/DrawerAdmin"; import NavbarMenu from "@/components/Drawer/NavbarMenu"; import { AccentColor, MainColor } from "@/constants/color-palet"; -import { ICON_SIZE_MEDIUM, ICON_SIZE_SMALL, ICON_SIZE_XLARGE } from "@/constants/constans-value"; +import { + ICON_SIZE_MEDIUM, + ICON_SIZE_SMALL, + ICON_SIZE_XLARGE, +} from "@/constants/constans-value"; +import { useAuth } from "@/hooks/use-auth"; import { adminListMenu } from "@/screens/Admin/listPageAdmin"; import { GStyles } from "@/styles/global-styles"; import { FontAwesome6, Ionicons } from "@expo/vector-icons"; @@ -19,6 +24,9 @@ import { useState } from "react"; export default function AdminLayout() { const [openDrawerNavbar, setOpenDrawerNavbar] = useState(false); const [openDrawerUser, setOpenDrawerUser] = useState(false); + + const { logout } = useAuth(); + return ( <> - - + + {/* ================== Collaboration End ================== */} {/* ================== Forum Start ================== */} - - + + - + {/* ================== Forum End ================== */} {/* ================== Voting Start ================== */} - + {/* ================== Voting End ================== */} {/* ================== Event Start ================== */} - - - + + + {/* */} {/* ================== Event End ================== */} @@ -223,7 +231,7 @@ export default function AdminLayout() { textLeft: "Batal", textRight: "Keluar", onPressRight: () => { - router.replace("/"); + logout(); }, }); } diff --git a/components/Button/ButtonCenteredOnly.tsx b/components/Button/ButtonCenteredOnly.tsx index 0b7b09c..c0a608e 100644 --- a/components/Button/ButtonCenteredOnly.tsx +++ b/components/Button/ButtonCenteredOnly.tsx @@ -9,17 +9,24 @@ interface ButtonCenteredOnlyProps { children?: React.ReactNode; icon?: "plus" | "upload" | string; onPress: () => void; + isLoading?: boolean; } export default function ButtonCenteredOnly({ onPress, children, - icon = "plus" + icon = "plus", + isLoading = false, }: ButtonCenteredOnlyProps) { return ( + } style={[GStyles.buttonCentered50Percent]} > diff --git a/context/AuthContext.tsx b/context/AuthContext.tsx index c087bf7..9226a82 100644 --- a/context/AuthContext.tsx +++ b/context/AuthContext.tsx @@ -1,8 +1,14 @@ -/* eslint-disable @typescript-eslint/no-unused-vars */ +import { + apiClient, + apiLogin, + apiRegister, + apiValidationCode, +} from "@/service/api"; import { IUser } from "@/types/User"; -import { createContext, useEffect, useState } from "react"; import AsyncStorage from "@react-native-async-storage/async-storage"; -import { apiClient, apiLogin } from "@/service/api"; +import { router } from "expo-router"; +import { createContext, useEffect, useState } from "react"; +import Toast from "react-native-toast-message"; // --- Types --- type AuthContextType = { @@ -13,12 +19,13 @@ type AuthContextType = { isAdmin: boolean; isUserActive: boolean; loginWithNomor: (nomor: string) => Promise; - // validateOtp: (nomor: string, otp: string) => Promise; - // logout: () => Promise; - // registerUser: (userData: { - // username: string; - // nomor: string; - // }) => Promise; + validateOtp: (nomor: string) => Promise; + logout: () => Promise; + registerUser: (userData: { + username: string; + nomor: string; + }) => Promise; + userData: (token: string) => Promise; }; // --- Create Context --- @@ -32,7 +39,7 @@ export const AuthProvider = ({ children }: { children: React.ReactNode }) => { const [isLoading, setIsLoading] = useState(true); const isAuthenticated = !!user; - const isAdmin = user?.MasterUserRole?.name === "Admin"; + const isAdmin = user?.masterUserRoleId !== "1"; const isUserActive = user?.active === true; // --- Load session from AsyncStorage on app start --- @@ -42,6 +49,10 @@ export const AuthProvider = ({ children }: { children: React.ReactNode }) => { const storedToken = await AsyncStorage.getItem("authToken"); const storedUser = await AsyncStorage.getItem("userData"); + if (storedToken) { + setToken(storedToken); + } + if (storedToken && storedUser) { setToken(storedToken); setUser(JSON.parse(storedUser)); @@ -61,7 +72,8 @@ export const AuthProvider = ({ children }: { children: React.ReactNode }) => { setIsLoading(true); try { const response = await apiLogin({ nomor: nomor }); - console.log("Response provider login", response); + console.log("Success login api", JSON.stringify(response, null, 2)); + await AsyncStorage.setItem("kode_otp", response.kodeId); } catch (error: any) { throw new Error(error.response?.data?.message || "Gagal kirim OTP"); } finally { @@ -69,6 +81,143 @@ export const AuthProvider = ({ children }: { children: React.ReactNode }) => { } }; + // --- 2. Validasi OTP & cek user --- + const validateOtp = async (nomor: string) => { + try { + setIsLoading(true); + const response = await apiValidationCode({ nomor: nomor }); + + const { token } = response; + if (response.success) { + setToken(token); + await AsyncStorage.setItem("authToken", token); + + const responseUser = await apiClient.get( + `/mobile/user?token=${token}`, + { + headers: { + Authorization: `Bearer ${token}`, + }, + } + ); + const dataUser = responseUser.data.data; + console.log("res validasi user :", JSON.stringify(dataUser, null, 2)); + + setUser(dataUser); + await AsyncStorage.setItem("userData", JSON.stringify(dataUser)); + + if (response.active) { + if (response.roleId === "1") { + return "/(application)/(user)/home"; + } else { + return "/(application)/admin/dashboard"; + } + } else { + return "/(application)/(user)/waiting-room"; + } + } else { + Toast.show({ + type: "info", + text1: "Anda belum terdaftar", + text2: "Silahkan daftar terlebih dahulu", + }); + return `/register?nomor=${nomor}`; + } + } catch (error: any) { + console.log("Error validasi otp >>", (error as Error).message || error); + throw new Error( + error.response?.data?.message || "OTP salah atau user tidak ditemukan" + ); + } finally { + setIsLoading(false); + } + }; + + // --- 3. Ambil data user --- + const userData = async (token: string) => { + try { + setIsLoading(true); + const response = await apiClient.get(`/mobile/user?token=${token}`, { + headers: { + Authorization: `Bearer ${token}`, + }, + }); + + const dataUser = response.data.data; + console.log("res validasi user :", JSON.stringify(dataUser, null, 2)); + + setUser(dataUser); + await AsyncStorage.setItem("userData", JSON.stringify(dataUser)); + return dataUser; + } catch (error: any) { + throw new Error( + error.response?.data?.message || "Gagal mengambil data user" + ); + } finally { + setIsLoading(false); + } + }; + + // --- 4. Register jika user belum ada --- + const registerUser = async (userData: { + username: string; + nomor: string; + }) => { + setIsLoading(true); + try { + const response = await apiRegister({ data: userData }); + console.log("Success register api", JSON.stringify(response, null, 2)); + + 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 --- + const logout = async () => { + try { + setIsLoading(true); + setToken(null); + setUser(null); + await AsyncStorage.removeItem("authToken"); + await AsyncStorage.removeItem("userData"); + setIsLoading(false); + + Toast.show({ + type: "success", + text1: "Logout berhasil", + text2: "Anda berhasil keluar dari akun.", + }); + router.replace("/"); + } catch (error) { + console.log("Logout error (optional):", error); + } finally { + setIsLoading(false); + } + }; + return ( <> { isAdmin, isUserActive, loginWithNomor, - // validateOtp, - // logout, - // registerUser, + validateOtp, + logout, + registerUser, + userData, }} > {children} diff --git a/hook/use-auth.ts b/hooks/use-auth.ts similarity index 100% rename from hook/use-auth.ts rename to hooks/use-auth.ts diff --git a/ios.build.device b/ios.build.device new file mode 100644 index 0000000..8b688e5 --- /dev/null +++ b/ios.build.device @@ -0,0 +1 @@ +npx expo run:ios --device \ No newline at end of file diff --git a/screens/Authentication/LoginView.tsx b/screens/Authentication/LoginView.tsx index 1f397c2..aba50c9 100644 --- a/screens/Authentication/LoginView.tsx +++ b/screens/Authentication/LoginView.tsx @@ -2,10 +2,10 @@ import ButtonCustom from "@/components/Button/ButtonCustom"; import Spacing from "@/components/_ShareComponent/Spacing"; import ViewWrapper from "@/components/_ShareComponent/ViewWrapper"; import { MainColor } from "@/constants/color-palet"; -import { useAuth } from "@/hook/use-auth"; -import { apiVersion } from "@/service/api"; +import { useAuth } from "@/hooks/use-auth"; +import { apiClient, apiVersion } from "@/service/api"; import { GStyles } from "@/styles/global-styles"; -import { router } from "expo-router"; +import { Redirect, router } from "expo-router"; import { useEffect, useState } from "react"; import { Text, View } from "react-native"; import PhoneInput, { ICountry } from "react-native-international-phone-number"; @@ -17,16 +17,25 @@ export default function LoginView() { const [inputValue, setInputValue] = useState(""); const [loading, setLoading] = useState(false); - const { loginWithNomor } = useAuth(); + const { loginWithNomor, token, isAdmin, isUserActive } = useAuth(); + + // console.log("Token state:", token ? "AVAILABLE" : "NOT AVAILABLE"); + // console.log("isAdmin state:", isAdmin); + // console.log("isUserActive state:", isUserActive); + // console.log("isAuthenticated state:", isAuthenticated); useEffect(() => { onLoadVersion(); }, []); async function onLoadVersion() { + // const token = await AsyncStorage.getItem("authToken"); + // console.log("Token Version:", token); const res = await apiVersion(); - console.log("Version", res.data); setVersion(res.data); + + const seasonKey = await apiClient.get("/mobile/season-key"); + console.log("seasonKey", seasonKey.data); } function handleInputValue(phoneNumber: string) { @@ -94,6 +103,18 @@ export default function LoginView() { } } + if (token && !isUserActive) { + return ; + } + + if (token && !isAdmin) { + return ; + } + + if (token && isAdmin) { + return ; + } + return ( diff --git a/screens/Authentication/RegisterView.tsx b/screens/Authentication/RegisterView.tsx index ecb980f..88ea6d0 100644 --- a/screens/Authentication/RegisterView.tsx +++ b/screens/Authentication/RegisterView.tsx @@ -3,18 +3,20 @@ import ViewWrapper from "@/components/_ShareComponent/ViewWrapper"; import ButtonCustom from "@/components/Button/ButtonCustom"; import TextInputCustom from "@/components/TextInput/TextInputCustom"; import { MainColor } from "@/constants/color-palet"; +import { useAuth } from "@/hooks/use-auth"; import { GStyles } from "@/styles/global-styles"; import { MaterialCommunityIcons } from "@expo/vector-icons"; -import { router, useLocalSearchParams } from "expo-router"; -import { Text, View } from "react-native"; +import { useLocalSearchParams } from "expo-router"; import { useState } from "react"; -import { apiRegister } from "@/service/api"; +import { Text, View } from "react-native"; import Toast from "react-native-toast-message"; export default function RegisterView() { const { nomor } = useLocalSearchParams(); const [username, setUsername] = useState(""); - const [loading, setLoading] = useState(false); + // const [loading, setLoading] = useState(false); + + const { registerUser, isLoading } = useAuth(); const validasiData = () => { if (!nomor) { @@ -39,35 +41,13 @@ export default function RegisterView() { async function handleRegister() { const isValid = validasiData(); if (!isValid) return; - const data = { + + const response = await registerUser({ nomor: nomor as string, username: username, - }; + }); - try { - setLoading(true); - const response = await apiRegister({ data }); - console.log("Success register", JSON.stringify(response, null, 2)); - - if (response.success) { - Toast.show({ - type: "success", - text1: "Sukses", - text2: "Anda berhasil terdaftar", - }); - router.replace("/(application)/(user)/waiting-room"); - } - - Toast.show({ - type: "info", - text1: "Info", - text2: response.message, - }); - } catch (error: any) { - console.log("Error register", error); - } finally { - setLoading(false); - } + console.log("Success register page", JSON.stringify(response, null, 2)); } return ( @@ -97,7 +77,7 @@ export default function RegisterView() { onChangeText={(text) => setUsername(text)} /> - + Daftar diff --git a/screens/Authentication/VerificationView.tsx b/screens/Authentication/VerificationView.tsx index b185027..7ec5aef 100644 --- a/screens/Authentication/VerificationView.tsx +++ b/screens/Authentication/VerificationView.tsx @@ -2,8 +2,10 @@ import Spacing from "@/components/_ShareComponent/Spacing"; import ViewWrapper from "@/components/_ShareComponent/ViewWrapper"; import ButtonCustom from "@/components/Button/ButtonCustom"; import { MainColor } from "@/constants/color-palet"; -import { apiCheckCodeOtp, apiValidationCode } from "@/service/api"; +import { useAuth } from "@/hooks/use-auth"; +import { apiCheckCodeOtp } from "@/service/api"; import { GStyles } from "@/styles/global-styles"; +import AsyncStorage from "@react-native-async-storage/async-storage"; import { router, useLocalSearchParams } from "expo-router"; import { useEffect, useState } from "react"; import { Text, View } from "react-native"; @@ -11,21 +13,23 @@ import { OtpInput } from "react-native-otp-entry"; import Toast from "react-native-toast-message"; export default function VerificationView() { - const { kodeId, nomor } = useLocalSearchParams(); - console.log("nomor", nomor); + const { nomor } = useLocalSearchParams(); const [codeOtp, setCodeOtp] = useState(""); const [inputOtp, setInputOtp] = useState(""); const [userNumber, setUserNumber] = useState(""); - const [loading, setLoading] = useState(false); + + // --- Context --- + const { validateOtp, isLoading } = useAuth(); useEffect(() => { - onLoadCheckCodeOtp(kodeId as string); - }, [kodeId]); + onLoadCheckCodeOtp(); + }, []); - async function onLoadCheckCodeOtp(kodeId: string) { - const response = await apiCheckCodeOtp({ kodeId: kodeId }); - console.log("response ", JSON.stringify(response, null, 2)); + async function onLoadCheckCodeOtp() { + const kodeId = await AsyncStorage.getItem("kode_otp"); + const response = await apiCheckCodeOtp({ kodeId: kodeId as string }); + console.log("response kode otp :", JSON.stringify(response.otp, null, 2)); setCodeOtp(response.otp); setUserNumber(response.nomor); } @@ -34,41 +38,41 @@ export default function VerificationView() { const codeOtpNumber = parseInt(codeOtp); const inputOtpNumber = parseInt(inputOtp); - console.log("codeOtpNumber ", codeOtpNumber, typeof codeOtpNumber); - console.log("inputOtpNumber ", inputOtpNumber, typeof inputOtpNumber); - if (inputOtpNumber !== codeOtpNumber) { Toast.show({ type: "error", - text1: "Gagal", - text2: "Kode OTP tidak sesuai", + text1: "Kode OTP tidak sesuai", }); return; } try { - setLoading(true); - const response = await apiValidationCode({ nomor: userNumber }); - console.log("response ", JSON.stringify(response, null, 2)); + const response = await validateOtp(nomor as string); + return router.replace(response); - if (response.success) { - if (response.active) { - if (response.roleId === "1") { - router.replace("/(application)/(user)/home"); - } else { - router.replace("/(application)/admin/dashboard"); - } - } else { - router.replace("/(application)/(user)/waiting-room"); - } - } else { - router.replace(`/register?nomor=${userNumber}`); - } + // if (response.success) { + // await userData(response.token); + + // if (response.active) { + // if (response.roleId === "1") { + // return "/(application)/(user)/home"; + // } else { + // return "/(application)/admin/dashboard"; + // } + // } else { + // return "/(application)/(user)/waiting-room"; + // } + // } else { + // Toast.show({ + // type: "info", + // text1: "Anda belum terdaftar", + // text2: "Silahkan daftar terlebih dahulu", + // }); + // return `/register?nomor=${nomor}`; + // } } catch (error) { console.log("Error verification", error); - } finally { - setLoading(false); } }; @@ -114,7 +118,7 @@ export default function VerificationView() { void; setIsDrawerOpen: (value: boolean) => void; + logout: () => Promise; }) { const handlePress = (item: IMenuDrawerItem) => { if (item.label === "Keluar") { // console.log("Logout clicked"); - setShowLogoutAlert(true); + // setShowLogoutAlert(true); + AlertDefaultSystem({ + title: "Apakah anda yakin ingin keluar?", + message: "Anda akan keluar dari akun ini", + textLeft: "Batal", + textRight: "Keluar", + onPressRight: () => { + logout(); + setIsDrawerOpen(false); + }, + onPressLeft: () => setIsDrawerOpen(false), + }); } else { console.log("PATH >> ", item.path); router.push(item.path as any); diff --git a/service/api.ts b/service/api.ts index b953f9a..bd01982 100644 --- a/service/api.ts +++ b/service/api.ts @@ -3,55 +3,29 @@ import axios, { AxiosInstance } from "axios"; import Constants from "expo-constants"; const API_BASE_URL = Constants.expoConfig?.extra?.API_BASE_URL; -// const API_BASE_URL = process.env.API_BASE_URL - export const apiClient: AxiosInstance = axios.create({ baseURL: API_BASE_URL, - timeout: 10000, - headers: { - "Content-Type": "application/json", - }, }); // Endpoint yang TIDAK butuh token -const PUBLIC_ROUTES = [ - // "/version", - "/auth/send-otp", - "/auth/verify-otp", - "/auth/register", - "/auth/logout", // opsional, tergantung kebutuhan -]; - -// apiClient.interceptors.request.use( -// (config) => { -// const token = AsyncStorage.getItem("authToken"); -// if (token) { -// config.headers.Authorization = `Bearer ${token}`; -// } -// return config; -// }, -// (error) => { -// return Promise.reject(error); -// } -// ); +// const PUBLIC_ROUTES = [ +// // "/version", +// "/auth/send-otp", +// "/auth/verify-otp", +// "/auth/register", +// "/auth/logout", // opsional, tergantung kebutuhan +// ]; apiClient.interceptors.request.use( - (config) => { - const token = AsyncStorage.getItem("authToken"); + async (config) => { + const token = await AsyncStorage.getItem("authToken"); if (token) { + // config.timeout = 10000; + config.headers["Content-Type"] = "application/json"; config.headers.Authorization = `Bearer ${token}`; } - // const isPublic = PUBLIC_ROUTES.some((route) => config.url?.includes(route)); - - // if (!isPublic) { - // const token = AsyncStorage.getItem("authToken"); - // if (token) { - // config.headers.Authorization = `Bearer ${token}`; - // } else { - // console.warn(`Token tidak ditemukan untuk endpoint: ${config.url}`); - // } - // } + // console.log("config", JSON.stringify(config, null, 2)); return config; }, (error) => { @@ -60,9 +34,9 @@ apiClient.interceptors.request.use( ); export async function apiVersion() { - console.log("API_BASE_URL", API_BASE_URL); + // console.log("API_BASE_URL", API_BASE_URL); const response = await apiClient.get("/version"); - console.log("Response version", response.data); + // console.log("Response version", JSON.stringify(response.data, null, 2)); return response.data; }