Refactor: apply PhoneInputCustom to LoginView

Changes:
- Replace react-native-international-phone-number with PhoneInputCustom
- Remove dependency on ICountry, use CountryData from constants
- Add KeyboardAvoidingView for better iOS keyboard handling
- Improve validation with libphonenumber-js
- Add proper phone number formatting
- Update state management (inputValue → phoneNumber)

Features Applied:
 NO emoji flags - only calling codes (+62, +65, etc)
 Clean, professional UI
 Modal country picker with search
 Better keyboard handling on iOS
 Real-time validation with libphonenumber-js
 Auto-formatting for international numbers
 Reusable component

UI:
- Phone Input: [+62 ⌄ | 812-3456-7890]
- Country Picker: Modal with search
- Display: Country name + calling code only

Validation:
- Check phone number length
- Validate with libphonenumber-js
- Format to E.164 on login
- Error handling with Toast

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
This commit is contained in:
2026-03-26 11:33:18 +08:00
parent 0d2fef1878
commit 0cb734e790

View File

@@ -1,8 +1,9 @@
import { NewWrapper, ViewWrapper } from "@/components"; import { NewWrapper, PhoneInputCustom, ViewWrapper } from "@/components";
import ButtonCustom from "@/components/Button/ButtonCustom"; import ButtonCustom from "@/components/Button/ButtonCustom";
import ModalReactNative from "@/components/Modal/ModalReactNative"; import ModalReactNative from "@/components/Modal/ModalReactNative";
import Spacing from "@/components/_ShareComponent/Spacing"; import Spacing from "@/components/_ShareComponent/Spacing";
import { MainColor } from "@/constants/color-palet"; import { MainColor } from "@/constants/color-palet";
import { DEFAULT_COUNTRY, type CountryData } from "@/constants/countries";
import { useAuth } from "@/hooks/use-auth"; import { useAuth } from "@/hooks/use-auth";
import { apiVersion, BASE_URL } from "@/service/api-config"; import { apiVersion, BASE_URL } from "@/service/api-config";
import { GStyles } from "@/styles/global-styles"; import { GStyles } from "@/styles/global-styles";
@@ -10,16 +11,16 @@ import { openBrowser } from "@/utils/openBrower";
import versionBadge from "@/utils/viersionBadge"; import versionBadge from "@/utils/viersionBadge";
import { Redirect } from "expo-router"; import { Redirect } from "expo-router";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { RefreshControl, Text, View } from "react-native"; import { KeyboardAvoidingView, Platform, RefreshControl, Text, View } from "react-native";
import PhoneInput, { ICountry } from "react-native-international-phone-number"; import { parsePhoneNumber } from "libphonenumber-js";
import Toast from "react-native-toast-message"; import Toast from "react-native-toast-message";
import EULASection from "./EULASection"; import EULASection from "./EULASection";
export default function LoginView() { export default function LoginView() {
const url = BASE_URL; const url = BASE_URL;
const [version, setVersion] = useState<string>(""); const [version, setVersion] = useState<string>("");
const [selectedCountry, setSelectedCountry] = useState<null | ICountry>(null); const [selectedCountry, setSelectedCountry] = useState<CountryData>(DEFAULT_COUNTRY);
const [inputValue, setInputValue] = useState<string>(""); const [phoneNumber, setPhoneNumber] = useState<string>("");
const [loading, setLoading] = useState<boolean>(false); const [loading, setLoading] = useState<boolean>(false);
const [refreshing, setRefreshing] = useState<boolean>(false); const [refreshing, setRefreshing] = useState<boolean>(false);
const [modalVisible, setModalVisible] = useState(false); const [modalVisible, setModalVisible] = useState(false);
@@ -43,41 +44,43 @@ export default function LoginView() {
async function handleRefresh() { async function handleRefresh() {
setRefreshing(true); setRefreshing(true);
await onLoadVersion(); await onLoadVersion();
setInputValue(""); setPhoneNumber("");
setSelectedCountry(DEFAULT_COUNTRY);
setLoading(false); setLoading(false);
setRefreshing(false); setRefreshing(false);
} }
function handleInputValue(phoneNumber: string) {
setInputValue(phoneNumber);
}
function handleSelectedCountry(country: ICountry) {
setSelectedCountry(country);
}
async function validateData() { async function validateData() {
if (inputValue.length === 0) { if (phoneNumber.length === 0) {
return Toast.show({ return Toast.show({
type: "error", type: "error",
text1: "Masukan nomor anda", text1: "Masukan nomor anda",
}); });
} }
if (selectedCountry === null) { if (phoneNumber.length < 9) {
return Toast.show({
type: "error",
text1: "Pilih negara",
});
}
if (inputValue.length < 9) {
return Toast.show({ return Toast.show({
type: "error", type: "error",
text1: "Nomor tidak valid", text1: "Nomor tidak valid",
}); });
} }
// Validate with libphonenumber-js
try {
const parsedNumber = parsePhoneNumber(phoneNumber, selectedCountry.code);
if (!parsedNumber || !parsedNumber.isValid()) {
return Toast.show({
type: "error",
text1: "Nomor tidak valid",
});
}
} catch (error) {
return Toast.show({
type: "error",
text1: "Format nomor tidak valid",
});
}
return true; return true;
} }
@@ -85,8 +88,17 @@ export default function LoginView() {
const isValid = await validateData(); const isValid = await validateData();
if (!isValid) return; if (!isValid) return;
const callingCode = selectedCountry?.callingCode.replace(/^\+/, "") || ""; // Format phone number with country code
let fixNumber = inputValue.replace(/\s+/g, "").replace(/^0+/, ""); const callingCode = selectedCountry.callingCode;
let fixNumber = phoneNumber.replace(/\s+/g, "").replace(/^0+/, "");
// Remove country code if already present
if (fixNumber.startsWith(callingCode)) {
fixNumber = fixNumber.substring(callingCode.length);
}
// Remove leading zero
fixNumber = fixNumber.replace(/^0+/, "");
const realNumber = callingCode + fixNumber; const realNumber = callingCode + fixNumber;
@@ -128,13 +140,18 @@ export default function LoginView() {
} }
return ( return (
<KeyboardAvoidingView
behavior={Platform.OS === "ios" ? "padding" : "height"}
keyboardVerticalOffset={Platform.OS === "ios" ? 100 : 50}
style={{ flex: 1 }}
>
<ViewWrapper <ViewWrapper
withBackground withBackground
refreshControl={ refreshControl={
<RefreshControl refreshing={refreshing} onRefresh={handleRefresh} /> <RefreshControl refreshing={refreshing} onRefresh={handleRefresh} />
} }
> >
<View style={GStyles.authContainer}> <View style={[GStyles.authContainer, { paddingBottom: 40 }]}>
<View> <View>
<View style={GStyles.authContainerTitle}> <View style={GStyles.authContainerTitle}>
<Text style={GStyles.authSubTitle}>WELCOME TO</Text> <Text style={GStyles.authSubTitle}>WELCOME TO</Text>
@@ -158,12 +175,13 @@ export default function LoginView() {
</Text> </Text>
</View> </View>
<PhoneInput <Spacing height={20} />
value={inputValue}
onChangePhoneNumber={handleInputValue} <PhoneInputCustom
value={phoneNumber}
onChangePhoneNumber={setPhoneNumber}
selectedCountry={selectedCountry} selectedCountry={selectedCountry}
onChangeSelectedCountry={handleSelectedCountry} onChangeCountry={setSelectedCountry}
defaultCountry="ID"
placeholder="Masukkan nomor" placeholder="Masukkan nomor"
/> />
@@ -197,6 +215,7 @@ export default function LoginView() {
dan seluruh kebijakan privasi yang berlaku. dan seluruh kebijakan privasi yang berlaku.
</Text> </Text>
</View> </View>
</ViewWrapper>
<ModalReactNative isVisible={modalVisible}> <ModalReactNative isVisible={modalVisible}>
<EULASection <EULASection
@@ -205,6 +224,6 @@ export default function LoginView() {
setLoadingTerm={setLoadingTerm} setLoadingTerm={setLoadingTerm}
/> />
</ModalReactNative> </ModalReactNative>
</ViewWrapper> </KeyboardAvoidingView>
); );
} }