- context/
- hook/
- types/

### No Issue
This commit is contained in:
2025-08-19 15:25:07 +08:00
parent 1da4b00c2f
commit e1039a5744
10 changed files with 169 additions and 16 deletions

View File

@@ -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 (
<>
<SafeAreaProvider>
<AppRoot />
<AppRoot />
{/* <AuthProvider>
</AuthProvider> */}
</SafeAreaProvider>
<Toast />
</>

View File

@@ -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=="],

92
context/AuthContext.tsx Normal file
View File

@@ -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<void>;
// validateOtp: (nomor: string, otp: string) => Promise<void>;
// logout: () => Promise<void>;
// registerUser: (userData: {
// username: string;
// nomor: string;
// }) => Promise<void>;
};
// --- Create Context ---
export const AuthContext = createContext<AuthContextType | undefined>(
undefined
);
export const AuthProvider = ({ children }: { children: React.ReactNode }) => {
const [user, setUser] = useState<IUser | null>(null);
const [token, setToken] = useState<string | null>(null);
const [isLoading, setIsLoading] = useState<boolean>(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 (
<>
<AuthContext.Provider
value={{
user,
token,
isLoading,
isAuthenticated,
isAdmin,
isUserActive,
loginWithNomor,
// validateOtp,
// logout,
// registerUser,
}}
>
{children}
</AuthContext.Provider>
</>
);
};

10
hook/use-auth.ts Normal file
View File

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

View File

@@ -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",

View File

@@ -129,8 +129,7 @@ export default function LoginView() {
Login
</ButtonCustom>
<Spacing />
{/* <ButtonCustom onPress={() => router.navigate("/admin/investment")}>
{/* <ButtonCustom onPress={() => router.navigate("/waiting-room")}>
Admin ( Delete Soon )
</ButtonCustom> */}
</View>

View File

@@ -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<string>("");
const [inputOtp, setInputOtp] = useState<string>("");
const [nomor, setNomor] = useState<string>("");
const [userNumber, setUserNumber] = useState<string>("");
const [loading, setLoading] = useState<boolean>(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() {
<Text style={GStyles.authTitle}>Verifikasi KOde OTP</Text>
<Spacing height={30} />
<Text style={GStyles.textLabel}>Masukan 4 digit kode otp</Text>
<Text style={GStyles.textLabel}>Yang di kirim ke +{nomor}</Text>
<Text style={GStyles.textLabel}>
Yang di kirim ke +{userNumber}
</Text>
<Spacing height={30} />
<OtpInput
disabled={codeOtp === ""}

View File

@@ -1,9 +1,10 @@
import axios, { AxiosInstance } from "axios";
import Constants from "expo-constants";
const API_BASE_URL = Constants.expoConfig?.extra?.API_BASE_URL;
const api: AxiosInstance = axios.create({
// const API_BASE_URL = process.env.API_BASE_URL
export const apiClient: AxiosInstance = axios.create({
baseURL: API_BASE_URL,
timeout: 10000,
headers: {
@@ -11,26 +12,40 @@ const api: AxiosInstance = axios.create({
},
});
// apiClient.interceptors.request.use(
// (config) => {
// 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;

17
types/User.ts Normal file
View File

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

5
types/env.d.ts vendored Normal file
View File

@@ -0,0 +1,5 @@
declare namespace NodeJS {
interface ProcessEnv {
API_BASE_URL?: string;
}
}