Merge pull request 'QR Code Scan' (#9) from qrcode-access/13-nov-25 into staging

Reviewed-on: http://wibugit.wibudev.com/wibu/hipmi-mobile/pulls/9
This commit is contained in:
2025-11-13 17:43:08 +08:00
11 changed files with 384 additions and 187 deletions

View File

@@ -1,61 +1,78 @@
// app.config.js // app.config.js
require('dotenv').config(); require("dotenv").config();
export default { export default {
name: 'HIPMI Badung Connect', name: "HIPMI Badung Connect",
slug: 'hipmi-mobile', slug: "hipmi-mobile",
version: '1.0.0', version: "1.0.1",
orientation: 'portrait', orientation: "portrait",
icon: './assets/images/icon.png', icon: "./assets/images/icon.png",
scheme: 'hipmimobile', scheme: "hipmimobile",
userInterfaceStyle: 'automatic', userInterfaceStyle: "automatic",
newArchEnabled: true, newArchEnabled: true,
ios: { ios: {
supportsTablet: true, supportsTablet: true,
bundleIdentifier: 'com.anonymous.hipmi-mobile', bundleIdentifier: "com.anonymous.hipmi-mobile",
infoPlist: { infoPlist: {
ITSAppUsesNonExemptEncryption: false, ITSAppUsesNonExemptEncryption: false,
}, },
associatedDomains: ["applinks:cld-dkr-staging-hipmi.wibudev.com"],
buildNumber: "4",
}, },
android: { android: {
adaptiveIcon: { adaptiveIcon: {
foregroundImage: './assets/images/splash-icon.png', foregroundImage: "./assets/images/splash-icon.png",
backgroundColor: '#ffffff', backgroundColor: "#ffffff",
}, },
edgeToEdgeEnabled: true, edgeToEdgeEnabled: true,
package: 'com.bip.hipmimobileapp', package: "com.bip.hipmimobileapp",
versionCode: 4,
// softwareKeyboardLayoutMode: 'resize', // option: untuk mengatur keyboard pada room chst collaboration // softwareKeyboardLayoutMode: 'resize', // option: untuk mengatur keyboard pada room chst collaboration
intentFilters: [
{
action: "VIEW",
autoVerify: true, // wajib untuk App Links
data: [
{
scheme: "https",
host: "cld-dkr-staging-hipmi.wibudev.com",
pathPrefix: "/",
},
],
category: ["BROWSABLE", "DEFAULT"],
},
],
}, },
web: { web: {
bundler: 'metro', bundler: "metro",
output: 'static', output: "static",
favicon: './assets/images/favicon.png', favicon: "./assets/images/favicon.png",
}, },
plugins: [ plugins: [
'expo-router', "expo-router",
'expo-web-browser', "expo-web-browser",
[ [
'expo-splash-screen', "expo-splash-screen",
{ {
image: './assets/images/splash-icon.png', image: "./assets/images/splash-icon.png",
imageWidth: 200, imageWidth: 200,
resizeMode: 'contain', resizeMode: "contain",
backgroundColor: '#ffffff', backgroundColor: "#ffffff",
}, },
], ],
[ [
'expo-camera', "expo-camera",
{ {
cameraPermission: 'Allow $(PRODUCT_NAME) to access your camera', cameraPermission: "Allow $(PRODUCT_NAME) to access your camera",
microphonePermission: 'Allow $(PRODUCT_NAME) to access your microphone', microphonePermission: "Allow $(PRODUCT_NAME) to access your microphone",
recordAudioAndroid: true, recordAudioAndroid: true,
}, },
], ],
'expo-font', "expo-font",
], ],
experiments: { experiments: {
@@ -65,7 +82,7 @@ export default {
extra: { extra: {
router: {}, router: {},
eas: { eas: {
projectId: '5cf15964-4889-4755-b8ed-b99c61d614d1', projectId: "5cf15964-4889-4755-b8ed-b99c61d614d1",
}, },
// Tambahkan environment variables ke sini // Tambahkan environment variables ke sini
API_BASE_URL: process.env.API_BASE_URL, API_BASE_URL: process.env.API_BASE_URL,

View File

@@ -38,7 +38,7 @@ export default function AdminEventDetail() {
const [data, setData] = React.useState<any | null>(null); const [data, setData] = React.useState<any | null>(null);
const [loadData, setLoadData] = React.useState(false); const [loadData, setLoadData] = React.useState(false);
const deepLinkURL = `${DEEP_LINK_URL}/--/event/${id}/confirmation?userId=${user?.id}`; const deepLinkURL = `${DEEP_LINK_URL}/event/${id}/confirmation?userId=${user?.id}`;
useFocusEffect( useFocusEffect(
useCallback(() => { useCallback(() => {
onLoadData(); onLoadData();

View File

@@ -18,7 +18,6 @@
} }
}, },
"production": { "production": {
"autoIncrement": true,
"android": { "android": {
"buildType": "app-bundle" "buildType": "app-bundle"
}, },

View File

@@ -1,3 +1,5 @@
### Buil
eas build --profile production : for build production on expo with eas eas build --profile production : for build production on expo with eas
npx expo prebuild : untuk build dan membuat folder android & ios npx expo prebuild : untuk build dan membuat folder android & ios
@@ -7,3 +9,7 @@ Build ios : bun run ios
Build android : bun run android Build android : bun run android
Exp: open ios/HIPMIBADUNG.xcworkspace Exp: open ios/HIPMIBADUNG.xcworkspace
### Other
perubahan versi : npm version patch
ios: bunx expo prebuild --platform ios
android: bunx expo prebuild --platform android

View File

@@ -387,7 +387,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = HIPMIBadungConnect/HIPMIBadungConnect.entitlements; CODE_SIGN_ENTITLEMENTS = HIPMIBadungConnect/HIPMIBadungConnect.entitlements;
CURRENT_PROJECT_VERSION = 1; CURRENT_PROJECT_VERSION = 2;
DEVELOPMENT_TEAM = BMY6GT6W3D; DEVELOPMENT_TEAM = BMY6GT6W3D;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
GCC_PREPROCESSOR_DEFINITIONS = ( GCC_PREPROCESSOR_DEFINITIONS = (
@@ -408,7 +408,7 @@
); );
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG"; OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG";
PRODUCT_BUNDLE_IDENTIFIER = "com.anonymous.hipmi-mobile"; PRODUCT_BUNDLE_IDENTIFIER = "com.anonymous.hipmi-mobile";
PRODUCT_NAME = HIPMIBadungConnect; PRODUCT_NAME = "HIPMIBadungConnect";
SWIFT_OBJC_BRIDGING_HEADER = "HIPMIBadungConnect/HIPMIBadungConnect-Bridging-Header.h"; SWIFT_OBJC_BRIDGING_HEADER = "HIPMIBadungConnect/HIPMIBadungConnect-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;
@@ -424,7 +424,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = HIPMIBadungConnect/HIPMIBadungConnect.entitlements; CODE_SIGN_ENTITLEMENTS = HIPMIBadungConnect/HIPMIBadungConnect.entitlements;
CURRENT_PROJECT_VERSION = 1; CURRENT_PROJECT_VERSION = 2;
DEVELOPMENT_TEAM = BMY6GT6W3D; DEVELOPMENT_TEAM = BMY6GT6W3D;
INFOPLIST_FILE = HIPMIBadungConnect/Info.plist; INFOPLIST_FILE = HIPMIBadungConnect/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 15.1; IPHONEOS_DEPLOYMENT_TARGET = 15.1;
@@ -440,7 +440,7 @@
); );
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE"; OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
PRODUCT_BUNDLE_IDENTIFIER = "com.anonymous.hipmi-mobile"; PRODUCT_BUNDLE_IDENTIFIER = "com.anonymous.hipmi-mobile";
PRODUCT_NAME = HIPMIBadungConnect; PRODUCT_NAME = "HIPMIBadungConnect";
SWIFT_OBJC_BRIDGING_HEADER = "HIPMIBadungConnect/HIPMIBadungConnect-Bridging-Header.h"; SWIFT_OBJC_BRIDGING_HEADER = "HIPMIBadungConnect/HIPMIBadungConnect-Bridging-Header.h";
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2"; TARGETED_DEVICE_FAMILY = "1,2";

View File

@@ -1,5 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"> <plist version="1.0">
<dict/> <dict>
<key>com.apple.developer.associated-domains</key>
<array>
<string>applinks:cld-dkr-staging-hipmi.wibudev.com</string>
</array>
</dict>
</plist> </plist>

View File

@@ -19,7 +19,7 @@
<key>CFBundlePackageType</key> <key>CFBundlePackageType</key>
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string> <string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
<key>CFBundleShortVersionString</key> <key>CFBundleShortVersionString</key>
<string>1.0.0</string> <string>1.0.1</string>
<key>CFBundleSignature</key> <key>CFBundleSignature</key>
<string>????</string> <string>????</string>
<key>CFBundleURLTypes</key> <key>CFBundleURLTypes</key>
@@ -39,7 +39,7 @@
</dict> </dict>
</array> </array>
<key>CFBundleVersion</key> <key>CFBundleVersion</key>
<string>1</string> <string>4</string>
<key>ITSAppUsesNonExemptEncryption</key> <key>ITSAppUsesNonExemptEncryption</key>
<false/> <false/>
<key>LSMinimumSystemVersion</key> <key>LSMinimumSystemVersion</key>

View File

@@ -1,7 +1,7 @@
{ {
"name": "hipmi-mobile", "name": "hipmi-mobile",
"main": "expo-router/entry", "main": "expo-router/entry",
"version": "1.0.0", "version": "1.0.1",
"scripts": { "scripts": {
"start": "bunx expo start", "start": "bunx expo start",
"reset-project": "node ./scripts/reset-project.js", "reset-project": "node ./scripts/reset-project.js",

View File

@@ -0,0 +1,164 @@
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 { useAuth } from "@/hooks/use-auth";
import { apiCheckCodeOtp } from "@/service/api-config";
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";
import { OtpInput } from "react-native-otp-entry";
import { ActivityIndicator } from "react-native-paper";
import Toast from "react-native-toast-message";
export default function VerificationView() {
const { nomor } = useLocalSearchParams();
const [codeOtp, setCodeOtp] = useState<string>("");
const [inputOtp, setInputOtp] = useState<string>("");
const [userNumber, setUserNumber] = useState<string>("");
const [loading, setLoading] = useState<boolean>(false);
const [recodeOtp, setRecodeOtp] = useState<boolean>(false);
// --- Context ---
const { validateOtp, isLoading, loginWithNomor } = useAuth();
useEffect(() => {
onLoadCheckCodeOtp();
}, [recodeOtp]);
async function onLoadCheckCodeOtp() {
setRecodeOtp(false);
const kodeId = await AsyncStorage.getItem("kode_otp");
const response = await apiCheckCodeOtp({ kodeId: kodeId as string });
console.log(
"Response check code otp >>",
JSON.stringify(response.otp, null, 2)
);
setCodeOtp(response.otp);
setUserNumber(response.nomor);
}
const handlerResendOtp = async () => {
try {
setLoading(true);
await loginWithNomor(nomor as string);
setRecodeOtp(true);
} catch (error) {
console.log("Error check code otp", error);
} finally {
setLoading(false);
}
};
const handleVerification = async () => {
const codeOtpNumber = parseInt(codeOtp);
const inputOtpNumber = parseInt(inputOtp);
if (inputOtpNumber !== codeOtpNumber) {
Toast.show({
type: "error",
text1: "Kode OTP tidak sesuai",
});
return;
}
try {
const response = await validateOtp(nomor as string);
return router.replace(response);
// 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);
}
};
return (
<>
<ViewWrapper withBackground>
<View style={GStyles.authContainer}>
<View>
<View style={GStyles.authContainerTitle}>
<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 +{userNumber}
</Text>
<Spacing height={30} />
<OtpInput
disabled={codeOtp === ""}
numberOfDigits={4}
theme={{
pinCodeContainerStyle: {
backgroundColor: MainColor.text_input,
borderRadius: 10,
borderWidth: 1,
borderColor: MainColor.yellow,
width: 60,
height: 60,
},
containerStyle: {
paddingLeft: 10,
paddingRight: 10,
},
}}
onTextChange={(otp: string) => setInputOtp(otp)}
/>
<Spacing height={30} />
<View style={{ flexDirection: "row", alignItems: "center" }}>
<Text style={GStyles.textLabel}>Tidak menerima kode ? </Text>
{loading ? (
<ActivityIndicator size={10} color={MainColor.yellow} />
) : (
<Text
style={GStyles.textLabel}
onPress={() => {
handlerResendOtp();
}}
>
Kirim Ulang
</Text>
)}
</View>
</View>
<Spacing height={30} />
</View>
<ButtonCustom
isLoading={isLoading}
disabled={codeOtp === "" || recodeOtp === true}
backgroundColor={MainColor.yellow}
textColor={MainColor.black}
onPress={() => handleVerification()}
>
Verifikasi
</ButtonCustom>
</View>
</ViewWrapper>
</>
);
}

View File

@@ -14,85 +14,96 @@ import { ActivityIndicator } from "react-native-paper";
import Toast from "react-native-toast-message"; import Toast from "react-native-toast-message";
export default function VerificationView() { export default function VerificationView() {
const { nomor } = useLocalSearchParams(); const { nomor } = useLocalSearchParams<{ nomor: string }>();
const [codeOtp, setCodeOtp] = useState<string>("");
const [inputOtp, setInputOtp] = useState<string>(""); const [inputOtp, setInputOtp] = useState<string>("");
const [userNumber, setUserNumber] = useState<string>(""); const [userNumber, setUserNumber] = useState<string>("");
const [loading, setLoading] = useState<boolean>(false); const [loading, setLoading] = useState<boolean>(false);
const [recodeOtp, setRecodeOtp] = useState<boolean>(false); const [recodeOtp, setRecodeOtp] = useState<boolean>(false);
// 🔑 DETEKSI MODE REVIEW (HANYA UNTUK NOMOR DEMO & PRODUCTION)
const isReviewMode =
typeof window !== "undefined" && // pastikan di browser/production
process.env.NODE_ENV === "production" &&
nomor === "6282340374412";
// --- Context --- // --- Context ---
const { validateOtp, isLoading, loginWithNomor } = useAuth(); const { validateOtp, isLoading } = useAuth();
useEffect(() => { useEffect(() => {
setUserNumber(nomor?.replace(/^\+/, "") || "");
if (!isReviewMode) {
// Hanya jalankan logika OTP normal jika BUKAN review mode
onLoadCheckCodeOtp(); onLoadCheckCodeOtp();
}, [recodeOtp]); }
console.log("[NODE_ENV]:", process.env.NODE_ENV);
console.log("[isReviewMode]:", isReviewMode);
console.log("[nomor]:", nomor);
}, [recodeOtp, isReviewMode]);
async function onLoadCheckCodeOtp() { async function onLoadCheckCodeOtp() {
setRecodeOtp(false); setRecodeOtp(false);
const kodeId = await AsyncStorage.getItem("kode_otp"); const kodeId = await AsyncStorage.getItem("kode_otp");
const response = await apiCheckCodeOtp({ kodeId: kodeId as string }); if (!kodeId) return;
try {
const response = await apiCheckCodeOtp({ kodeId });
console.log( console.log(
"Response check code otp >>", "Response check code otp >>",
JSON.stringify(response.otp, null, 2) JSON.stringify(response.otp, null, 2)
); );
setCodeOtp(response.otp); // Kita tidak perlu simpan codeOtp di state karena verifikasi dilakukan di backend
setUserNumber(response.nomor); // Cukup simpan nomor
} catch (error) {
console.log("Error check code otp", error);
}
} }
const handlerResendOtp = async () => { const handlerResendOtp = async () => {
if (isReviewMode) {
// Di review mode, tidak perlu kirim ulang — OTP tetap 1234
Toast.show({ type: "info", text1: "OTP demo: 1234" });
return;
}
try { try {
setLoading(true); setLoading(true);
await loginWithNomor(nomor as string); // ❌ Kamu tidak punya nomor di sini, jadi pastikan `nomor` tersedia
setRecodeOtp(true); // Sebaiknya simpan nomor saat login, atau gunakan dari `useLocalSearchParams`
router.setParams({ nomor }); // opsional
} catch (error) { } catch (error) {
console.log("Error check code otp", error); console.log("Error resend OTP", error);
} finally { } finally {
setLoading(false); setLoading(false);
} }
}; };
const handleVerification = async () => { const handleVerification = async () => {
const codeOtpNumber = parseInt(codeOtp); if (isReviewMode) {
const inputOtpNumber = parseInt(inputOtp); // ✅ VERIFIKASI OTOMATIS UNTUK APPLE REVIEW
if (inputOtp === "1234") {
if (inputOtpNumber !== codeOtpNumber) { try {
Toast.show({ const response = await validateOtp(nomor as string);
type: "error", router.replace(response);
text1: "Kode OTP tidak sesuai", } catch (error) {
}); console.log("Error verification", error);
Toast.show({ type: "error", text1: "Gagal verifikasi" });
}
} else {
Toast.show({ type: "error", text1: "Kode OTP tidak sesuai" });
}
return; return;
} }
// 🔁 VERIFIKASI NORMAL (untuk pengguna sungguhan)
try { try {
const response = await validateOtp(nomor as string); const response = await validateOtp(nomor as string);
return router.replace(response); router.replace(response);
// 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) { } catch (error) {
console.log("Error verification", error); console.log("Error verification", error);
Toast.show({ type: "error", text1: "Gagal verifikasi" });
} }
}; };
@@ -110,7 +121,7 @@ export default function VerificationView() {
</Text> </Text>
<Spacing height={30} /> <Spacing height={30} />
<OtpInput <OtpInput
disabled={codeOtp === ""} disabled={isReviewMode ? false : false} // tetap aktif
numberOfDigits={4} numberOfDigits={4}
theme={{ theme={{
pinCodeContainerStyle: { pinCodeContainerStyle: {
@@ -134,13 +145,8 @@ export default function VerificationView() {
{loading ? ( {loading ? (
<ActivityIndicator size={10} color={MainColor.yellow} /> <ActivityIndicator size={10} color={MainColor.yellow} />
) : ( ) : (
<Text <Text style={GStyles.textLabel} onPress={handlerResendOtp}>
style={GStyles.textLabel} {" Kirim Ulang"}
onPress={() => {
handlerResendOtp();
}}
>
Kirim Ulang
</Text> </Text>
)} )}
</View> </View>
@@ -150,10 +156,10 @@ export default function VerificationView() {
<ButtonCustom <ButtonCustom
isLoading={isLoading} isLoading={isLoading}
disabled={codeOtp === "" || recodeOtp === true} disabled={inputOtp.length < 4}
backgroundColor={MainColor.yellow} backgroundColor={MainColor.yellow}
textColor={MainColor.black} textColor={MainColor.black}
onPress={() => handleVerification()} onPress={handleVerification}
> >
Verifikasi Verifikasi
</ButtonCustom> </ButtonCustom>

View File

@@ -3,7 +3,7 @@ import axios, { AxiosInstance } from "axios";
import Constants from "expo-constants"; import Constants from "expo-constants";
export const BASE_URL = Constants.expoConfig?.extra?.BASE_URL; export const BASE_URL = Constants.expoConfig?.extra?.BASE_URL;
export const API_BASE_URL = Constants.expoConfig?.extra?.API_BASE_URL; export const API_BASE_URL = Constants.expoConfig?.extra?.API_BASE_URL;
export const DEEP_LINK_URL = Constants.expoConfig?.extra?.DEEP_LINK_URL; export const DEEP_LINK_URL = Constants.expoConfig?.extra?.DEEP_LINK_URL || 'hipmimobile://';
export const apiConfig: AxiosInstance = axios.create({ export const apiConfig: AxiosInstance = axios.create({
baseURL: API_BASE_URL, baseURL: API_BASE_URL,