Compare commits

...

4 Commits

Author SHA1 Message Date
1da4b00c2f reinstall bun.lock
### No Issue
2025-08-19 11:44:18 +08:00
a4825343ba API
Add: service api
-  service/
- app.config.js
- app.json.backup

Package:
- react-native-dotenv
- expo-module-scripts

### No Issue
2025-08-19 11:07:42 +08:00
0b6c360500 API
Add: Screen root
2025-08-18 11:45:06 +08:00
6f5d04e73f API
Add:
- lib/api.ts

### No Issue
2025-08-15 17:39:32 +08:00
16 changed files with 5981 additions and 183 deletions

2
.gitignore vendored
View File

@@ -38,3 +38,5 @@ yarn-error.*
app-example
.qodo
.env

71
app.config.js Normal file
View File

@@ -0,0 +1,71 @@
// app.config.js
require('dotenv').config();
export default {
name: 'HIPMI BADUNG',
slug: 'hipmi-mobile',
version: '1.0.0',
orientation: 'portrait',
icon: './assets/images/icon.png',
scheme: 'hipmimobile',
userInterfaceStyle: 'automatic',
newArchEnabled: true,
ios: {
supportsTablet: true,
bundleIdentifier: 'com.anonymous.hipmi-mobile',
infoPlist: {
ITSAppUsesNonExemptEncryption: false,
},
},
android: {
adaptiveIcon: {
foregroundImage: './assets/images/splash-icon.png',
backgroundColor: '#ffffff',
},
edgeToEdgeEnabled: true,
package: 'com.bip.hipmimobileapp',
},
web: {
bundler: 'metro',
output: 'static',
favicon: './assets/images/favicon.png',
},
plugins: [
'expo-router',
[
'expo-splash-screen',
{
image: './assets/images/splash-icon.png',
imageWidth: 200,
resizeMode: 'contain',
backgroundColor: '#ffffff',
},
],
[
'expo-camera',
{
cameraPermission: 'Allow $(PRODUCT_NAME) to access your camera',
microphonePermission: 'Allow $(PRODUCT_NAME) to access your microphone',
recordAudioAndroid: true,
},
],
'expo-font',
],
experiments: {
typedRoutes: true,
},
extra: {
router: {},
eas: {
projectId: '5cf15964-4889-4755-b8ed-b99c61d614d1',
},
// Tambahkan environment variables ke sini
API_BASE_URL: process.env.API_BASE_URL,
},
};

View File

@@ -33,6 +33,14 @@ export default function UserLayout() {
}}
/>
<Stack.Screen
name="waiting-room"
options={{
title: "Waiting Room",
headerBackVisible: false,
}}
/>
{/* ========== Profile Section ========= */}
<Stack.Screen
name="profile"

View File

@@ -0,0 +1,13 @@
import { InformationBox, ViewWrapper } from "@/components";
export default function WaitingRoom() {
return (
<>
<ViewWrapper>
<InformationBox
text="Permohonan akses Anda sedang dalam proses verifikasi oleh admin. Harap tunggu, Anda akan menerima pemberitahuan melalui Whatsapp setelah disetujui."
/>
</ViewWrapper>
</>
);
}

View File

@@ -1,5 +1,4 @@
import { MainColor } from "@/constants/color-palet";
import { Stack } from "expo-router";
import AppRoot from "@/screens/RootLayout/AppRoot";
import "react-native-gesture-handler";
import { SafeAreaProvider } from "react-native-safe-area-context";
import Toast from "react-native-toast-message";
@@ -8,28 +7,7 @@ export default function RootLayout() {
return (
<>
<SafeAreaProvider>
<Stack
screenOptions={{
headerStyle: { backgroundColor: MainColor.darkblue },
headerTitleStyle: { color: MainColor.yellow, fontWeight: "bold" },
headerTitleAlign: "center",
}}
>
<Stack.Screen
name="index"
options={{ title: "", headerBackVisible: false }}
/>
<Stack.Screen name="+not-found" options={{ title: "" }} />
<Stack.Screen
name="verification"
options={{ title: "", headerBackVisible: false }}
/>
<Stack.Screen
name="register"
options={{ title: "", headerBackVisible: false }}
/>
<Stack.Screen name="(application)" options={{ headerShown: false }} />
</Stack>
<AppRoot />
</SafeAreaProvider>
<Toast />
</>

889
bun.lock

File diff suppressed because it is too large Load Diff

View File

@@ -6,6 +6,7 @@ import { radiusMap } from "@/constants/radius-value";
import { MainColor } from "@/constants/color-palet";
import { stylesButton } from "./buttonCustomStyles";
import { Href, router } from "expo-router";
import { ActivityIndicator } from "react-native-paper";
// Import radiusMap
@@ -23,6 +24,7 @@ interface ButtonProps {
disabled?: boolean;
iconLeft?: React.ReactNode;
style?: StyleProp<ViewStyle>;
isLoading?: boolean;
}
const ButtonCustom: React.FC<ButtonProps> = ({
@@ -36,6 +38,7 @@ const ButtonCustom: React.FC<ButtonProps> = ({
disabled = false,
iconLeft,
style,
isLoading = false,
}) => {
return (
<TouchableOpacity
@@ -59,9 +62,13 @@ const ButtonCustom: React.FC<ButtonProps> = ({
>
{/* Render icon jika tersedia */}
{iconLeft && iconLeft}
<Text style={[stylesButton.buttonText, { color: textColor }]}>
{children || title}
</Text>
{isLoading ? (
<ActivityIndicator size={18} color={MainColor.darkblue} />
) : (
<Text style={[stylesButton.buttonText, { color: textColor }]}>
{children || title}
</Text>
)}
</TouchableOpacity>
);
};

23
lib/api.ts Normal file
View File

@@ -0,0 +1,23 @@
const API_BASE = (path: string) => {
// const url = "https://stg-hipmi.wibudev.com/";
const url = "http://172.20.173.254:3000/";
return `${url}/${path}`;
};
export async function apiVersion() {
const response = await fetch(API_BASE("api/version"));
const data = await response.json();
return data;
}
export async function apiLoginBack({ nomor }: { nomor: string }) {
const response = await fetch(API_BASE("api/auth/login"), {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ nomor: nomor }),
});
const data = await response.json();
return data;
}

4796
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -20,6 +20,7 @@
"@react-navigation/native-stack": "^7.3.21",
"@types/lodash": "^4.17.20",
"@types/react-native-vector-icons": "^6.4.18",
"axios": "^1.11.0",
"dayjs": "^1.11.13",
"expo": "53.0.17",
"expo-blur": "~14.1.5",
@@ -44,6 +45,7 @@
"react": "19.0.0",
"react-dom": "19.0.0",
"react-native": "0.79.5",
"react-native-dotenv": "^3.4.11",
"react-native-gesture-handler": "~2.24.0",
"react-native-international-phone-number": "^0.9.3",
"react-native-maps": "1.20.1",
@@ -65,6 +67,7 @@
"@types/react": "~19.0.10",
"eslint": "^9.25.0",
"eslint-config-expo": "~9.2.0",
"expo-module-scripts": "^4.1.10",
"typescript": "~5.8.3"
},
"private": true

View File

@@ -2,15 +2,28 @@ 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 { apiLogin, apiVersion } from "@/service/api";
import { GStyles } from "@/styles/global-styles";
import { router } from "expo-router";
import { useState } from "react";
import { useEffect, useState } from "react";
import { Text, View } from "react-native";
import PhoneInput, { ICountry } from "react-native-international-phone-number";
import Toast from "react-native-toast-message";
export default function LoginView() {
const [version, setVersion] = useState<string>("");
const [selectedCountry, setSelectedCountry] = useState<null | ICountry>(null);
const [inputValue, setInputValue] = useState<string>("");
const [loading, setLoading] = useState<boolean>(false);
useEffect(() => {
onLoadVersion();
}, []);
async function onLoadVersion() {
const res = await apiVersion();
setVersion(res.data);
}
function handleInputValue(phoneNumber: string) {
setInputValue(phoneNumber);
@@ -20,19 +33,59 @@ export default function LoginView() {
setSelectedCountry(country);
}
function handleLogin() {
async function validateData() {
if (inputValue.length === 0) {
return Toast.show({
type: "error",
text1: "Masukan nomor anda",
});
}
if (selectedCountry === null) {
return Toast.show({
type: "error",
text1: "Pilih negara",
});
}
if (inputValue.length < 9) {
return Toast.show({
type: "error",
text1: "Nomor tidak valid",
});
}
return true;
}
async function handleLogin() {
const isValid = await validateData();
if (!isValid) return;
const callingCode = selectedCountry?.callingCode.replace(/^\+/, "") || "";
const fixNumber = callingCode + inputValue;
// console.log("fixNumber", fixNumber);
const fixNumber = inputValue.replace(/\s+/g, "");
const realNumber = callingCode + fixNumber;
const randomAlfabet = Math.random().toString(36).substring(2, 8);
const randomNumber = Math.floor(Math.random() * 1000000);
const id = randomAlfabet + randomNumber + fixNumber;
console.log("login user id :", id);
setLoading(true);
const response = await apiLogin({ nomor: realNumber });
router.navigate("/verification");
// router.replace("/(application)/coba");
// router.navigate("/admin/dashboard")
if (response.success) {
Toast.show({
type: "success",
text1: "Sukses",
text2: "Kode OTP berhasil dikirim",
});
router.navigate(`/verification?kodeId=${response.kodeId}`);
setLoading(false);
// router.replace("/(application)/coba");
} else {
Toast.show({
type: "error",
text1: "Error",
text2: response.message,
});
setLoading(false);
}
}
return (
@@ -49,15 +102,15 @@ export default function LoginView() {
<Text
style={{
position: "absolute",
bottom: 30,
right: 20,
bottom: 35,
right: 50,
fontSize: 10,
fontWeight: "thin",
fontStyle: "italic",
color: MainColor.white_gray,
}}
>
powered by muku.id
{version} | powered by muku.id
</Text>
</View>
@@ -72,8 +125,9 @@ export default function LoginView() {
<Spacing />
<ButtonCustom onPress={handleLogin}>Login</ButtonCustom>
<ButtonCustom onPress={handleLogin} isLoading={loading}>
Login
</ButtonCustom>
<Spacing />
{/* <ButtonCustom onPress={() => router.navigate("/admin/investment")}>

View File

@@ -5,16 +5,71 @@ import TextInputCustom from "@/components/TextInput/TextInputCustom";
import { MainColor } from "@/constants/color-palet";
import { GStyles } from "@/styles/global-styles";
import { MaterialCommunityIcons } from "@expo/vector-icons";
import { router } from "expo-router";
import { router, useLocalSearchParams } from "expo-router";
import { Text, View } from "react-native";
import { useState } from "react";
import { apiRegister } from "@/service/api";
import Toast from "react-native-toast-message";
export default function RegisterView() {
const [username, setUsername] = useState("Bagas Banuna");
const handleRegister = () => {
console.log("Success register", username);
router.push("/(application)/(user)/home");
const { nomor } = useLocalSearchParams();
const [username, setUsername] = useState("");
const [loading, setLoading] = useState(false);
const validasiData = () => {
if (!nomor) {
Toast.show({
type: "error",
text1: "Gagal",
text2: "Nomor tidak ditemukan",
});
return false;
}
if (!username) {
Toast.show({
type: "error",
text1: "Gagal",
text2: "Username tidak boleh kosong",
});
return false;
}
return true;
};
async function handleRegister() {
const isValid = validasiData();
if (!isValid) return;
const data = {
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);
}
}
return (
<>
<ViewWrapper withBackground>
@@ -33,7 +88,7 @@ export default function RegisterView() {
<Text style={GStyles.textLabel}>
Anda akan terdaftar dengan nomor
</Text>
<Text style={GStyles.textLabel}>+6282xxxxxxxxx</Text>
<Text style={GStyles.textLabel}>+{nomor}</Text>
<Spacing />
</View>
<TextInputCustom
@@ -42,17 +97,9 @@ export default function RegisterView() {
onChangeText={(text) => setUsername(text)}
/>
<ButtonCustom onPress={handleRegister}>Daftar</ButtonCustom>
{/* <Spacing />
<ButtonCustom
title="Coba"
backgroundColor={MainColor.yellow}
textColor={MainColor.black}
onPress={() => {
console.log("Home clicked");
router.push("/(application)/coba");
}}
/> */}
<ButtonCustom isLoading={loading} onPress={handleRegister}>
Daftar
</ButtonCustom>
</View>
</View>
</ViewWrapper>

View File

@@ -2,16 +2,74 @@ 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 { GStyles } from "@/styles/global-styles";
import { router } from "expo-router";
import { router, useLocalSearchParams } from "expo-router";
import { useEffect, useState } from "react";
import { Text, View } from "react-native";
import { OtpInput } from "react-native-otp-entry";
import Toast from "react-native-toast-message";
export default function VerificationView() {
const handleVerification = () => {
console.log("Verification clicked");
router.push("/register");
const { kodeId } = useLocalSearchParams();
const [codeOtp, setCodeOtp] = useState<string>("");
const [inputOtp, setInputOtp] = useState<string>("");
const [nomor, setNomor] = useState<string>("");
const [loading, setLoading] = useState<boolean>(false);
useEffect(() => {
onLoadCheckCodeOtp(kodeId as string);
}, [kodeId]);
async function onLoadCheckCodeOtp(kodeId: string) {
const response = await apiCheckCodeOtp({ kodeId: kodeId });
console.log("response ", JSON.stringify(response, null, 2));
setCodeOtp(response.otp);
setNomor(response.nomor);
}
const handleVerification = async () => {
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",
});
return;
}
try {
setLoading(true);
const response = await apiValidationCode({ nomor: nomor });
console.log("response ", JSON.stringify(response, null, 2));
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=${nomor}`);
}
} catch (error) {
console.log("Error verification", error);
} finally {
setLoading(false);
}
};
return (
<>
<ViewWrapper withBackground>
@@ -21,11 +79,10 @@ 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 +6282xxxxxxxxx
</Text>
<Text style={GStyles.textLabel}>Yang di kirim ke +{nomor}</Text>
<Spacing height={30} />
<OtpInput
disabled={codeOtp === ""}
numberOfDigits={4}
theme={{
pinCodeContainerStyle: {
@@ -41,6 +98,7 @@ export default function VerificationView() {
paddingRight: 10,
},
}}
onTextChange={(otp: string) => setInputOtp(otp)}
/>
<Spacing height={30} />
<Text style={GStyles.textLabel}>
@@ -52,6 +110,8 @@ export default function VerificationView() {
</View>
<ButtonCustom
isLoading={loading}
disabled={codeOtp === ""}
backgroundColor={MainColor.yellow}
textColor={MainColor.black}
onPress={() => handleVerification()}

View File

@@ -0,0 +1,31 @@
import { MainColor } from "@/constants/color-palet";
import { Stack } from "expo-router";
export default function AppRoot() {
return (
<>
<Stack
screenOptions={{
headerStyle: { backgroundColor: MainColor.darkblue },
headerTitleStyle: { color: MainColor.yellow, fontWeight: "bold" },
headerTitleAlign: "center",
}}
>
<Stack.Screen
name="index"
options={{ title: "", headerBackVisible: false }}
/>
<Stack.Screen name="+not-found" options={{ title: "" }} />
<Stack.Screen
name="verification"
options={{ title: "", headerBackVisible: false }}
/>
<Stack.Screen
name="register"
options={{ title: "", headerBackVisible: false }}
/>
<Stack.Screen name="(application)" options={{ headerShown: false }} />
</Stack>
</>
);
}

48
service/api.ts Normal file
View File

@@ -0,0 +1,48 @@
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({
baseURL: API_BASE_URL,
timeout: 10000,
headers: {
"Content-Type": "application/json",
},
});
export async function apiVersion() {
console.log("API_BASE_URL", API_BASE_URL);
const response = await api.get("/version");
return response.data;
}
export async function apiLogin({ nomor }: { nomor: string }) {
const response = await api.post("/auth/login", {
nomor: nomor,
});
return response.data;
}
export async function apiCheckCodeOtp({ kodeId }: { kodeId: string }) {
const response = await api.get(`/auth/check/${kodeId}`);
return response.data;
}
export async function apiValidationCode({ nomor }: { nomor: string }) {
const response = await api.post(`/auth/validasi`, {
nomor: nomor,
});
return response.data;
}
export async function apiRegister({
data,
}: {
data: { nomor: string; username: string };
}) {
const response = await api.post(`/auth/register`, {
data: data,
});
return response.data;
}