Add: service api
-  service/
- app.config.js
- app.json.backup

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

### No Issue
This commit is contained in:
2025-08-19 11:07:42 +08:00
parent 0b6c360500
commit a4825343ba
14 changed files with 5033 additions and 92 deletions

2
.gitignore vendored
View File

@@ -38,3 +38,5 @@ yarn-error.*
app-example app-example
.qodo .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,13 @@ export default function UserLayout() {
}} }}
/> />
<Stack.Screen
name="waiting-room"
options={{
title: "Waiting Room",
}}
/>
{/* ========== Profile Section ========= */} {/* ========== Profile Section ========= */}
<Stack.Screen <Stack.Screen
name="profile" 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

@@ -38,6 +38,7 @@
"react": "19.0.0", "react": "19.0.0",
"react-dom": "19.0.0", "react-dom": "19.0.0",
"react-native": "0.79.5", "react-native": "0.79.5",
"react-native-dotenv": "^3.4.11",
"react-native-gesture-handler": "~2.24.0", "react-native-gesture-handler": "~2.24.0",
"react-native-international-phone-number": "^0.9.3", "react-native-international-phone-number": "^0.9.3",
"react-native-maps": "1.20.1", "react-native-maps": "1.20.1",
@@ -1450,6 +1451,8 @@
"react-native-country-codes-picker": ["react-native-country-codes-picker@2.3.5", "", { "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-dDQhd0bVvlmgb84NPhTOmTk5UVYPHtk3lqZI+BPb61H1rC2IDrTvPWENg6u1DMGliqWHQDBYpeH37zvxxQL71w=="], "react-native-country-codes-picker": ["react-native-country-codes-picker@2.3.5", "", { "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-dDQhd0bVvlmgb84NPhTOmTk5UVYPHtk3lqZI+BPb61H1rC2IDrTvPWENg6u1DMGliqWHQDBYpeH37zvxxQL71w=="],
"react-native-dotenv": ["react-native-dotenv@3.4.11", "", { "dependencies": { "dotenv": "^16.4.5" }, "peerDependencies": { "@babel/runtime": "^7.20.6" } }, "sha512-6vnIE+WHABSeHCaYP6l3O1BOEhWxKH6nHAdV7n/wKn/sciZ64zPPp2NUdEUf1m7g4uuzlLbjgr+6uDt89q2DOg=="],
"react-native-drawer-layout": ["react-native-drawer-layout@4.1.11", "", { "dependencies": { "use-latest-callback": "^0.2.4" }, "peerDependencies": { "react": ">= 18.2.0", "react-native": "*", "react-native-gesture-handler": ">= 2.0.0", "react-native-reanimated": ">= 2.0.0" } }, "sha512-31gilubSKPLToy31/bb0hhgOOenHYJq4JC7g/JkIEqBqSWzoCgiOlccDHlBRG+MV37UtXZnJN2spj3VusdCd4A=="], "react-native-drawer-layout": ["react-native-drawer-layout@4.1.11", "", { "dependencies": { "use-latest-callback": "^0.2.4" }, "peerDependencies": { "react": ">= 18.2.0", "react-native": "*", "react-native-gesture-handler": ">= 2.0.0", "react-native-reanimated": ">= 2.0.0" } }, "sha512-31gilubSKPLToy31/bb0hhgOOenHYJq4JC7g/JkIEqBqSWzoCgiOlccDHlBRG+MV37UtXZnJN2spj3VusdCd4A=="],
"react-native-edge-to-edge": ["react-native-edge-to-edge@1.6.0", "", { "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-2WCNdE3Qd6Fwg9+4BpbATUxCLcouF6YRY7K+J36KJ4l3y+tWN6XCqAC4DuoGblAAbb2sLkhEDp4FOlbOIot2Og=="], "react-native-edge-to-edge": ["react-native-edge-to-edge@1.6.0", "", { "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-2WCNdE3Qd6Fwg9+4BpbATUxCLcouF6YRY7K+J36KJ4l3y+tWN6XCqAC4DuoGblAAbb2sLkhEDp4FOlbOIot2Og=="],

View File

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

View File

@@ -10,7 +10,7 @@ export async function apiVersion() {
return data; return data;
} }
export async function apiLogin({ nomor }: { nomor: string }) { export async function apiLoginBack({ nomor }: { nomor: string }) {
const response = await fetch(API_BASE("api/auth/login"), { const response = await fetch(API_BASE("api/auth/login"), {
method: "POST", method: "POST",
headers: { headers: {

4796
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -45,6 +45,7 @@
"react": "19.0.0", "react": "19.0.0",
"react-dom": "19.0.0", "react-dom": "19.0.0",
"react-native": "0.79.5", "react-native": "0.79.5",
"react-native-dotenv": "^3.4.11",
"react-native-gesture-handler": "~2.24.0", "react-native-gesture-handler": "~2.24.0",
"react-native-international-phone-number": "^0.9.3", "react-native-international-phone-number": "^0.9.3",
"react-native-maps": "1.20.1", "react-native-maps": "1.20.1",
@@ -66,6 +67,7 @@
"@types/react": "~19.0.10", "@types/react": "~19.0.10",
"eslint": "^9.25.0", "eslint": "^9.25.0",
"eslint-config-expo": "~9.2.0", "eslint-config-expo": "~9.2.0",
"expo-module-scripts": "^4.1.10",
"typescript": "~5.8.3" "typescript": "~5.8.3"
}, },
"private": true "private": true

View File

@@ -2,7 +2,7 @@ import ButtonCustom from "@/components/Button/ButtonCustom";
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 { MainColor } from "@/constants/color-palet"; import { MainColor } from "@/constants/color-palet";
import { apiLogin, apiVersion } from "@/lib/api"; import { apiLogin, apiVersion } from "@/service/api";
import { GStyles } from "@/styles/global-styles"; import { GStyles } from "@/styles/global-styles";
import { router } from "expo-router"; import { router } from "expo-router";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
@@ -14,6 +14,7 @@ export default function LoginView() {
const [version, setVersion] = useState<string>(""); const [version, setVersion] = useState<string>("");
const [selectedCountry, setSelectedCountry] = useState<null | ICountry>(null); const [selectedCountry, setSelectedCountry] = useState<null | ICountry>(null);
const [inputValue, setInputValue] = useState<string>(""); const [inputValue, setInputValue] = useState<string>("");
const [loading, setLoading] = useState<boolean>(false);
useEffect(() => { useEffect(() => {
onLoadVersion(); onLoadVersion();
@@ -65,15 +66,17 @@ export default function LoginView() {
const fixNumber = inputValue.replace(/\s+/g, ""); const fixNumber = inputValue.replace(/\s+/g, "");
const realNumber = callingCode + fixNumber; const realNumber = callingCode + fixNumber;
setLoading(true);
const response = await apiLogin({ nomor: realNumber }); const response = await apiLogin({ nomor: realNumber });
if (response.success) { if (response.success) {
Toast.show({ Toast.show({
type: "success", type: "success",
text1: "Success", text1: "Sukses",
text2: "Login berhasil", text2: "Kode OTP berhasil dikirim",
}); });
router.navigate(`/verification?kodeId=${response.kodeId}`); router.navigate(`/verification?kodeId=${response.kodeId}`);
setLoading(false);
// router.replace("/(application)/coba"); // router.replace("/(application)/coba");
} else { } else {
Toast.show({ Toast.show({
@@ -81,6 +84,7 @@ export default function LoginView() {
text1: "Error", text1: "Error",
text2: response.message, text2: response.message,
}); });
setLoading(false);
} }
} }
@@ -121,7 +125,9 @@ export default function LoginView() {
<Spacing /> <Spacing />
<ButtonCustom onPress={handleLogin}>Login</ButtonCustom> <ButtonCustom onPress={handleLogin} isLoading={loading}>
Login
</ButtonCustom>
<Spacing /> <Spacing />
{/* <ButtonCustom onPress={() => router.navigate("/admin/investment")}> {/* <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 { MainColor } from "@/constants/color-palet";
import { GStyles } from "@/styles/global-styles"; import { GStyles } from "@/styles/global-styles";
import { MaterialCommunityIcons } from "@expo/vector-icons"; import { MaterialCommunityIcons } from "@expo/vector-icons";
import { router } from "expo-router"; import { router, useLocalSearchParams } from "expo-router";
import { Text, View } from "react-native"; import { Text, View } from "react-native";
import { useState } from "react"; import { useState } from "react";
import { apiRegister } from "@/service/api";
import Toast from "react-native-toast-message";
export default function RegisterView() { export default function RegisterView() {
const [username, setUsername] = useState("Bagas Banuna"); const { nomor } = useLocalSearchParams();
const handleRegister = () => { const [username, setUsername] = useState("");
console.log("Success register", username); const [loading, setLoading] = useState(false);
router.push("/(application)/(user)/home");
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 ( return (
<> <>
<ViewWrapper withBackground> <ViewWrapper withBackground>
@@ -33,7 +88,7 @@ export default function RegisterView() {
<Text style={GStyles.textLabel}> <Text style={GStyles.textLabel}>
Anda akan terdaftar dengan nomor Anda akan terdaftar dengan nomor
</Text> </Text>
<Text style={GStyles.textLabel}>+6282xxxxxxxxx</Text> <Text style={GStyles.textLabel}>+{nomor}</Text>
<Spacing /> <Spacing />
</View> </View>
<TextInputCustom <TextInputCustom
@@ -42,17 +97,9 @@ export default function RegisterView() {
onChangeText={(text) => setUsername(text)} onChangeText={(text) => setUsername(text)}
/> />
<ButtonCustom onPress={handleRegister}>Daftar</ButtonCustom> <ButtonCustom isLoading={loading} onPress={handleRegister}>
{/* <Spacing /> Daftar
<ButtonCustom </ButtonCustom>
title="Coba"
backgroundColor={MainColor.yellow}
textColor={MainColor.black}
onPress={() => {
console.log("Home clicked");
router.push("/(application)/coba");
}}
/> */}
</View> </View>
</View> </View>
</ViewWrapper> </ViewWrapper>

View File

@@ -2,19 +2,74 @@ 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";
import { MainColor } from "@/constants/color-palet"; import { MainColor } from "@/constants/color-palet";
import { apiCheckCodeOtp, apiValidationCode } from "@/service/api";
import { GStyles } from "@/styles/global-styles"; import { GStyles } from "@/styles/global-styles";
import { router, useLocalSearchParams } from "expo-router"; import { router, useLocalSearchParams } from "expo-router";
import { useEffect, useState } from "react";
import { Text, View } from "react-native"; import { Text, View } from "react-native";
import { OtpInput } from "react-native-otp-entry"; import { OtpInput } from "react-native-otp-entry";
import Toast from "react-native-toast-message";
export default function VerificationView() { export default function VerificationView() {
const { kodeId } = useLocalSearchParams(); const { kodeId } = useLocalSearchParams();
console.log("kodeId ", kodeId); const [codeOtp, setCodeOtp] = useState<string>("");
const [inputOtp, setInputOtp] = useState<string>("");
const handleVerification = () => { const [nomor, setNomor] = useState<string>("");
console.log("Verification clicked"); const [loading, setLoading] = useState<boolean>(false);
router.push("/register");
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 ( return (
<> <>
<ViewWrapper withBackground> <ViewWrapper withBackground>
@@ -24,11 +79,10 @@ export default function VerificationView() {
<Text style={GStyles.authTitle}>Verifikasi KOde OTP</Text> <Text style={GStyles.authTitle}>Verifikasi KOde OTP</Text>
<Spacing height={30} /> <Spacing height={30} />
<Text style={GStyles.textLabel}>Masukan 4 digit kode otp</Text> <Text style={GStyles.textLabel}>Masukan 4 digit kode otp</Text>
<Text style={GStyles.textLabel}> <Text style={GStyles.textLabel}>Yang di kirim ke +{nomor}</Text>
Yang di kirim ke +6282xxxxxxxxx
</Text>
<Spacing height={30} /> <Spacing height={30} />
<OtpInput <OtpInput
disabled={codeOtp === ""}
numberOfDigits={4} numberOfDigits={4}
theme={{ theme={{
pinCodeContainerStyle: { pinCodeContainerStyle: {
@@ -44,6 +98,7 @@ export default function VerificationView() {
paddingRight: 10, paddingRight: 10,
}, },
}} }}
onTextChange={(otp: string) => setInputOtp(otp)}
/> />
<Spacing height={30} /> <Spacing height={30} />
<Text style={GStyles.textLabel}> <Text style={GStyles.textLabel}>
@@ -55,6 +110,8 @@ export default function VerificationView() {
</View> </View>
<ButtonCustom <ButtonCustom
isLoading={loading}
disabled={codeOtp === ""}
backgroundColor={MainColor.yellow} backgroundColor={MainColor.yellow}
textColor={MainColor.black} textColor={MainColor.black}
onPress={() => handleVerification()} onPress={() => handleVerification()}

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