From e1039a57446a26a2a4dcd3e6374f91b3a3fc5519 Mon Sep 17 00:00:00 2001 From: Bagasbanuna02 Date: Tue, 19 Aug 2025 15:25:07 +0800 Subject: [PATCH] Add: - context/ - hook/ - types/ ### No Issue --- app/_layout.tsx | 5 +- bun.lock | 7 ++ context/AuthContext.tsx | 92 +++++++++++++++++++++ hook/use-auth.ts | 10 +++ package.json | 1 + screens/Authentication/LoginView.tsx | 3 +- screens/Authentication/VerificationView.tsx | 16 ++-- service/api.ts | 29 +++++-- types/User.ts | 17 ++++ types/env.d.ts | 5 ++ 10 files changed, 169 insertions(+), 16 deletions(-) create mode 100644 context/AuthContext.tsx create mode 100644 hook/use-auth.ts create mode 100644 types/User.ts create mode 100644 types/env.d.ts diff --git a/app/_layout.tsx b/app/_layout.tsx index 1190c39..4e00346 100644 --- a/app/_layout.tsx +++ b/app/_layout.tsx @@ -1,3 +1,4 @@ +import { AuthProvider } from "@/context/AuthContext"; import AppRoot from "@/screens/RootLayout/AppRoot"; import "react-native-gesture-handler"; import { SafeAreaProvider } from "react-native-safe-area-context"; @@ -7,7 +8,9 @@ export default function RootLayout() { return ( <> - + + {/* + */} diff --git a/bun.lock b/bun.lock index d38c6a5..21a1fe4 100644 --- a/bun.lock +++ b/bun.lock @@ -5,6 +5,7 @@ "name": "hipmi-mobile", "dependencies": { "@expo/vector-icons": "^14.1.0", + "@react-native-async-storage/async-storage": "2.1.2", "@react-native-community/datetimepicker": "8.4.1", "@react-navigation/bottom-tabs": "^7.4.2", "@react-navigation/drawer": "^7.5.2", @@ -484,6 +485,8 @@ "@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.0", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-ujc+V6r0HNDviYqIK3rW4ffgYiZ8g5DEHrGJVk4x7kTlLXRDILnKX9vAUYeIsLOoDpDJ0ujpqMkjH4w2ofuo6w=="], + "@react-native-async-storage/async-storage": ["@react-native-async-storage/async-storage@2.1.2", "", { "dependencies": { "merge-options": "^3.0.4" }, "peerDependencies": { "react-native": "^0.0.0-0 || >=0.65 <1.0" } }, "sha512-dvlNq4AlGWC+ehtH12p65+17V0Dx7IecOWl6WanF2ja38O1Dcjjvn7jVzkUHJ5oWkQBlyASurTPlTHgKXyYiow=="], + "@react-native-community/datetimepicker": ["@react-native-community/datetimepicker@8.4.1", "", { "dependencies": { "invariant": "^2.2.4" }, "peerDependencies": { "expo": ">=52.0.0", "react": "*", "react-native": "*", "react-native-windows": "*" }, "optionalPeers": ["expo", "react-native-windows"] }, "sha512-DrK+CUS5fZnz8dhzBezirkzQTcNDdaXer3oDLh0z4nc2tbdIdnzwvXCvi8IEOIvleoc9L95xS5tKUl0/Xv71Mg=="], "@react-native/assets-registry": ["@react-native/assets-registry@0.79.5", "", {}, "sha512-N4Kt1cKxO5zgM/BLiyzuuDNquZPiIgfktEQ6TqJ/4nKA8zr4e8KJgU6Tb2eleihDO4E24HmkvGc73naybKRz/w=="], @@ -1366,6 +1369,8 @@ "is-number-object": ["is-number-object@1.1.1", "", { "dependencies": { "call-bound": "^1.0.3", "has-tostringtag": "^1.0.2" } }, "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw=="], + "is-plain-obj": ["is-plain-obj@2.1.0", "", {}, "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA=="], + "is-potential-custom-element-name": ["is-potential-custom-element-name@1.0.1", "", {}, "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ=="], "is-regex": ["is-regex@1.2.1", "", { "dependencies": { "call-bound": "^1.0.2", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g=="], @@ -1566,6 +1571,8 @@ "memoize-one": ["memoize-one@5.2.1", "", {}, "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q=="], + "merge-options": ["merge-options@3.0.4", "", { "dependencies": { "is-plain-obj": "^2.1.0" } }, "sha512-2Sug1+knBjkaMsMgf1ctR1Ujx+Ayku4EdJN4Z+C2+JzoeF7A3OZ9KM2GY0CpQS51NR61LTurMJrRKPhSs3ZRTQ=="], + "merge-stream": ["merge-stream@2.0.0", "", {}, "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w=="], "merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="], diff --git a/context/AuthContext.tsx b/context/AuthContext.tsx new file mode 100644 index 0000000..c087bf7 --- /dev/null +++ b/context/AuthContext.tsx @@ -0,0 +1,92 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +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"; + +// --- Types --- +type AuthContextType = { + user: IUser | null; + token: string | null; + isLoading: boolean; + isAuthenticated: boolean; + isAdmin: boolean; + isUserActive: boolean; + loginWithNomor: (nomor: string) => Promise; + // validateOtp: (nomor: string, otp: string) => Promise; + // logout: () => Promise; + // registerUser: (userData: { + // username: string; + // nomor: string; + // }) => Promise; +}; + +// --- Create Context --- +export const AuthContext = createContext( + undefined +); + +export const AuthProvider = ({ children }: { children: React.ReactNode }) => { + const [user, setUser] = useState(null); + const [token, setToken] = useState(null); + const [isLoading, setIsLoading] = useState(true); + + const isAuthenticated = !!user; + const isAdmin = user?.MasterUserRole?.name === "Admin"; + const isUserActive = user?.active === true; + + // --- Load session from AsyncStorage on app start --- + useEffect(() => { + const loadSession = async () => { + try { + const storedToken = await AsyncStorage.getItem("authToken"); + const storedUser = await AsyncStorage.getItem("userData"); + + if (storedToken && storedUser) { + setToken(storedToken); + setUser(JSON.parse(storedUser)); + } + } catch (error) { + console.error("Failed to load session", error); + } finally { + setIsLoading(false); + } + }; + + loadSession(); + }, []); + + // --- 1. Kirim nomor → dapat OTP --- + const loginWithNomor = async (nomor: string) => { + setIsLoading(true); + try { + const response = await apiLogin({ nomor: nomor }); + console.log("Response provider login", response); + } catch (error: any) { + throw new Error(error.response?.data?.message || "Gagal kirim OTP"); + } finally { + setIsLoading(false); + } + }; + + return ( + <> + + {children} + + + ); +}; diff --git a/hook/use-auth.ts b/hook/use-auth.ts new file mode 100644 index 0000000..1f9545c --- /dev/null +++ b/hook/use-auth.ts @@ -0,0 +1,10 @@ +import { AuthContext } from "@/context/AuthContext"; +import { useContext } from "react"; + +export const useAuth = () => { + const context = useContext(AuthContext); + if (!context) { + throw new Error("useAuth must be used within an AuthProvider"); + } + return context; +}; diff --git a/package.json b/package.json index 6c73bec..b816ef5 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ }, "dependencies": { "@expo/vector-icons": "^14.1.0", + "@react-native-async-storage/async-storage": "2.1.2", "@react-native-community/datetimepicker": "8.4.1", "@react-navigation/bottom-tabs": "^7.4.2", "@react-navigation/drawer": "^7.5.2", diff --git a/screens/Authentication/LoginView.tsx b/screens/Authentication/LoginView.tsx index 087c090..81cef56 100644 --- a/screens/Authentication/LoginView.tsx +++ b/screens/Authentication/LoginView.tsx @@ -129,8 +129,7 @@ export default function LoginView() { Login - - {/* router.navigate("/admin/investment")}> + {/* router.navigate("/waiting-room")}> Admin ( Delete Soon ) */} diff --git a/screens/Authentication/VerificationView.tsx b/screens/Authentication/VerificationView.tsx index 20533d7..b185027 100644 --- a/screens/Authentication/VerificationView.tsx +++ b/screens/Authentication/VerificationView.tsx @@ -11,10 +11,12 @@ import { OtpInput } from "react-native-otp-entry"; import Toast from "react-native-toast-message"; export default function VerificationView() { - const { kodeId } = useLocalSearchParams(); + const { kodeId, nomor } = useLocalSearchParams(); + console.log("nomor", nomor); + const [codeOtp, setCodeOtp] = useState(""); const [inputOtp, setInputOtp] = useState(""); - const [nomor, setNomor] = useState(""); + const [userNumber, setUserNumber] = useState(""); const [loading, setLoading] = useState(false); useEffect(() => { @@ -25,7 +27,7 @@ export default function VerificationView() { const response = await apiCheckCodeOtp({ kodeId: kodeId }); console.log("response ", JSON.stringify(response, null, 2)); setCodeOtp(response.otp); - setNomor(response.nomor); + setUserNumber(response.nomor); } const handleVerification = async () => { @@ -47,7 +49,7 @@ export default function VerificationView() { try { setLoading(true); - const response = await apiValidationCode({ nomor: nomor }); + const response = await apiValidationCode({ nomor: userNumber }); console.log("response ", JSON.stringify(response, null, 2)); if (response.success) { @@ -61,7 +63,7 @@ export default function VerificationView() { router.replace("/(application)/(user)/waiting-room"); } } else { - router.replace(`/register?nomor=${nomor}`); + router.replace(`/register?nomor=${userNumber}`); } } catch (error) { console.log("Error verification", error); @@ -79,7 +81,9 @@ export default function VerificationView() { Verifikasi KOde OTP Masukan 4 digit kode otp - Yang di kirim ke +{nomor} + + Yang di kirim ke +{userNumber} + { +// const token = localStorage.getItem("token"); +// if (token) { +// config.headers.Authorization = `Bearer ${token}`; +// } +// return config; +// }, +// (error) => { +// return Promise.reject(error); +// } +// ); + export async function apiVersion() { console.log("API_BASE_URL", API_BASE_URL); - const response = await api.get("/version"); + const response = await apiClient.get("/version"); + console.log("Response version", response.data); return response.data; } export async function apiLogin({ nomor }: { nomor: string }) { - const response = await api.post("/auth/login", { + const response = await apiClient.post("/auth/login", { nomor: nomor, }); return response.data; } export async function apiCheckCodeOtp({ kodeId }: { kodeId: string }) { - const response = await api.get(`/auth/check/${kodeId}`); + const response = await apiClient.get(`/auth/check/${kodeId}`); return response.data; } export async function apiValidationCode({ nomor }: { nomor: string }) { - const response = await api.post(`/auth/validasi`, { + const response = await apiClient.post(`/auth/validasi`, { nomor: nomor, }); return response.data; @@ -41,7 +56,7 @@ export async function apiRegister({ }: { data: { nomor: string; username: string }; }) { - const response = await api.post(`/auth/register`, { + const response = await apiClient.post(`/auth/register`, { data: data, }); return response.data; diff --git a/types/User.ts b/types/User.ts new file mode 100644 index 0000000..b3f564f --- /dev/null +++ b/types/User.ts @@ -0,0 +1,17 @@ +export type TUser = "User" | "Admin" | "Super Admin"; + +export interface IMasterUserRole { + id: string; + name: TUser; +} + +export interface IUser { + id: string; + username: string; + nomor: string; + active: boolean; + createdAt: string | null; + updatedAt: string | null; + masterUserRoleId: string; + MasterUserRole: IMasterUserRole; +} diff --git a/types/env.d.ts b/types/env.d.ts new file mode 100644 index 0000000..60533f6 --- /dev/null +++ b/types/env.d.ts @@ -0,0 +1,5 @@ +declare namespace NodeJS { + interface ProcessEnv { + API_BASE_URL?: string; + } +}