react-native-international-phone-number
Version:
International mobile phone input component with mask for React Native
557 lines (516 loc) • 15.6 kB
JavaScript
import React, {
useEffect,
useState,
useRef,
forwardRef,
} from 'react';
import {
View,
Text,
TouchableOpacity,
TextInput,
} from 'react-native';
import {
CountryPicker,
CountryButton,
} from 'react-native-country-codes-picker';
import getInputMask from './utils/getInputMask';
import getAllCountries from './utils/getAllCountries';
import getCountriesByName from './utils/getCountriesByName';
import getCountriesByCallingCode from './utils/getCountriesByCallingCode';
import getCountryByCca2 from './utils/getCountryByCca2';
import getCountryByPhoneNumber from './utils/getCountryByPhoneNumber';
import isValidPhoneNumber from './utils/isValidPhoneNumber';
import {
getCountryNotFoundMessage,
getPhoneNumberInputPlaceholder,
getPopularCountriesSectionTitle,
getRestOfCountriesSectionTitle,
getSearchInputPlaceholder,
} from './utils/getPlaceholders';
import {
getCaretStyle,
getContainerStyle,
getCountryPickerStyle,
getDividerStyle,
getFlagContainerStyle,
getFlagStyle,
getFlagTextStyle,
getInputStyle,
getModalSectionTitleStyle,
} from './utils/getStyles';
const PhoneInput = forwardRef(
(
{
theme,
language,
placeholder,
placeholderTextColor,
selectionColor,
phoneInputStyles,
modalStyles,
disabled,
modalDisabled,
modalHeight,
defaultCountry,
defaultValue,
onChangePhoneNumber,
selectedCountry,
onChangeSelectedCountry,
customMask,
showOnly,
excludedCountries,
popularCountries,
popularCountriesSectionTitle,
restOfCountriesSectionTitle,
modalSectionTitleDisabled,
modalSearchInputPlaceholder,
modalSearchInputPlaceholderTextColor,
modalSearchInputSelectionColor,
modalNotFoundCountryMessage,
customCaret,
rtl,
allowZeroAfterCallingCode,
testID,
...rest
},
ref
) => {
const [show, setShow] = useState(false);
const [defaultCca2, setDefaultCca2] = useState('');
const [inputValue, setInputValue] = useState(null);
const [countryValue, setCountryValue] = useState(null);
const textInputRef = useRef(null);
const refBase = {
...textInputRef.current,
onFocus: () => textInputRef.current?.focus(),
focus: () => textInputRef.current?.focus(),
blur: () => textInputRef.current?.blur(),
clear: () => textInputRef.current?.clear(),
getValue: () => inputValue,
value: inputValue,
getFullPhoneNumber: () =>
`${countryValue?.callingCode} ${inputValue}`,
fullPhoneNumber: `${countryValue?.callingCode} ${inputValue}`,
getSelectedCountry: () => countryValue,
selectedCountry: countryValue,
isValid: isValidPhoneNumber(inputValue, selectedCountry),
props: {
theme,
language,
placeholder,
placeholderTextColor,
selectionColor,
phoneInputStyles,
modalStyles,
disabled,
modalDisabled,
modalHeight,
defaultCountry,
defaultValue,
onChangePhoneNumber,
selectedCountry,
onChangeSelectedCountry,
customMask,
showOnly,
excludedCountries,
popularCountries,
popularCountriesSectionTitle,
restOfCountriesSectionTitle,
modalSectionTitleDisabled,
modalSearchInputPlaceholder,
modalSearchInputPlaceholderTextColor,
modalSearchInputSelectionColor,
modalNotFoundCountryMessage,
customCaret,
rtl,
allowZeroAfterCallingCode,
testID,
...rest,
},
};
function updateRef(phoneNumber, country) {
if (ref) {
ref.current = {
...refBase,
getValue: () => phoneNumber,
value: phoneNumber,
getFullPhoneNumber: () =>
`${country?.callingCode} ${phoneNumber}`,
fullPhoneNumber: `${country?.callingCode} ${phoneNumber}`,
getSelectedCountry: () => country,
selectedCountry: country,
isValid: isValidPhoneNumber(phoneNumber, country),
props: {
...refBase.props,
value: phoneNumber,
selectedCountry: country,
},
};
}
}
function onSelect(country) {
setShow(false);
if (ref) {
setInputValue('');
} else {
onChangePhoneNumber('');
}
if (onChangeSelectedCountry || ref) {
const newValue = {
name: country.name,
cca2: country.code,
flag: country.flag,
callingCode: country.dial_code,
};
if (ref) {
setCountryValue(newValue);
updateRef('', newValue);
} else {
onChangeSelectedCountry(newValue);
}
}
}
function onChangeText(phoneNumber, callingCode) {
if (phoneNumber.includes('+')) {
const matchingCountry = getCountryByPhoneNumber(phoneNumber);
if (matchingCountry) {
setDefaultCca2(matchingCountry.cca2);
if (ref) {
setCountryValue(matchingCountry);
updateRef('', matchingCountry);
} else {
onChangeSelectedCountry(matchingCountry);
}
onChangeText(
phoneNumber.replace(matchingCountry.callingCode, ''),
null
);
}
return;
}
const res = getInputMask(
phoneNumber,
callingCode ? callingCode : countryValue?.callingCode,
countryValue?.cca2,
customMask ? customMask : null,
allowZeroAfterCallingCode === false ? false : true
);
if (ref) {
setInputValue(res);
updateRef(res, countryValue);
} else {
onChangePhoneNumber(res);
}
}
useEffect(() => {
if (!countryValue && !defaultCountry) {
const defaultCountry = getCountryByCca2('BR');
if (ref) {
setCountryValue(defaultCountry);
updateRef('', defaultCountry);
} else {
onChangeSelectedCountry(defaultCountry);
}
} else {
if (ref) {
updateRef('', countryValue);
}
}
}, []);
useEffect(() => {
if (defaultCountry) {
if (ref) {
setCountryValue(getCountryByCca2(defaultCountry));
updateRef('', getCountryByCca2(defaultCountry));
} else {
onChangeSelectedCountry(getCountryByCca2(defaultCountry));
}
}
}, [defaultCountry]);
useEffect(() => {
if (ref) {
setInputValue('');
} else {
onChangePhoneNumber('');
}
if (defaultValue) {
const matchingCountry = getCountryByPhoneNumber(defaultValue);
if (matchingCountry) {
setDefaultCca2(matchingCountry.cca2);
if (ref) {
setCountryValue(matchingCountry);
updateRef('', matchingCountry);
} else {
onChangeSelectedCountry(matchingCountry);
}
} else {
setDefaultCca2(null);
if (ref) {
setCountryValue(null);
updateRef('', null);
} else {
onChangeSelectedCountry(null);
}
onChangeText('', null);
console.warn(
"The default number provided (defaultValue) don't match with anyone country. Please, correct it to be shown in the input. For more information: https://github.com/AstrOOnauta/react-native-international-phone-number#intermediate-usage---typescript--default-phone-number-value"
);
}
}
}, [defaultValue]);
useEffect(() => {
if (
defaultValue &&
countryValue &&
countryValue.cca2 === defaultCca2 &&
!inputValue
) {
const callingCode = countryValue.callingCode;
let phoneNumber = defaultValue;
if (
callingCode === '+1' &&
countryValue.cca2 !== 'CA' &&
countryValue.cca2 !== 'US'
) {
phoneNumber = defaultValue
.replace(/\s/g, '')
.substring(
callingCode.length + 3,
defaultValue.replace(/\D/g, '').length +
callingCode.length
);
} else {
phoneNumber = defaultValue
.replace(/\s/g, '')
.substring(
callingCode.length,
defaultValue.replace(/\D/g, '').length +
callingCode.length
);
}
onChangeText(phoneNumber, callingCode);
}
}, [countryValue]);
useEffect(() => {
if (!ref) {
setInputValue(rest.value);
setCountryValue(selectedCountry);
}
}, [selectedCountry]);
if (
ref &&
(rest.value ||
onChangePhoneNumber ||
selectedCountry ||
onChangeSelectedCountry)
) {
throw new Error(
"Error: Don't use the useRef hook combined with the useState hook to manage the phoneNumber and selectedCountry values. Instead, choose to use just one of them (useRef or useState)."
);
} else {
// Create a separate constant for each part of the component
const touchableStart = (
<>
<Text
testID={`${testID}-flag`}
style={getFlagStyle(phoneInputStyles?.flag)}
>
{countryValue?.flag}
</Text>
{customCaret || (
<View style={phoneInputStyles?.caret}>
<View
style={{
flexDirection: 'row',
justifyContent: 'center',
paddingTop: 4,
}}
>
<View
style={getCaretStyle(
theme,
phoneInputStyles?.caret
)}
/>
</View>
</View>
)}
</>
);
const touchableMiddle = (
<View
style={getDividerStyle(theme, phoneInputStyles?.divider)}
/>
);
const touchableEnd = (
<Text
style={getFlagTextStyle(
theme,
phoneInputStyles?.callingCode
)}
>
{countryValue?.callingCode}
</Text>
);
const touchablePart = (
<TouchableOpacity
activeOpacity={disabled || modalDisabled ? 1 : 0.6}
onPress={() =>
disabled || modalDisabled ? null : setShow(true)
}
style={getFlagContainerStyle(
theme,
phoneInputStyles?.flagContainer
)}
>
{/* LTR Display */}
{!rtl && touchableStart}
{!rtl && touchableMiddle}
{!rtl && touchableEnd}
{/* RTL Display */}
{rtl && touchableEnd}
{rtl && touchableMiddle}
{rtl && touchableStart}
</TouchableOpacity>
);
const inputPart = (
<TextInput
style={getInputStyle(theme, phoneInputStyles?.input)}
placeholder={
placeholder === '' || placeholder
? placeholder
: getPhoneNumberInputPlaceholder(language || 'en')
}
placeholderTextColor={
placeholderTextColor ||
(theme === 'dark' ? '#CCCCCC' : '#AAAAAA')
}
selectionColor={
selectionColor ||
(theme === 'dark'
? 'rgba(255,255,255, .4)'
: 'rgba(0 ,0 ,0 , .4)')
}
editable={!disabled}
value={inputValue}
onChangeText={onChangeText}
keyboardType="numeric"
ref={textInputRef}
{...rest}
/>
);
return (
<>
<View
style={getContainerStyle(
theme,
phoneInputStyles?.container,
disabled
)}
>
{/* LTR Display */}
{!rtl && touchablePart}
{!rtl && inputPart}
{/* RTL Display */}
{rtl && inputPart}
{rtl && touchablePart}
</View>
{!disabled && !modalDisabled && show ? (
<CountryPicker
show={show}
lang={language}
inputPlaceholder={
modalSearchInputPlaceholder ||
getSearchInputPlaceholder(language || 'en')
}
inputPlaceholderTextColor={
modalSearchInputPlaceholderTextColor ||
(theme === 'dark' ? '#CCCCCC' : '#AAAAAA')
}
selectionColor={
modalSearchInputSelectionColor ||
(theme === 'dark'
? 'rgba(255,255,255, .4)'
: 'rgba(0 ,0 ,0 , .4)')
}
searchMessage={
modalNotFoundCountryMessage ||
getCountryNotFoundMessage(language || 'en')
}
enableModalAvoiding
style={getCountryPickerStyle(
theme,
modalHeight,
modalStyles
)}
pickerButtonOnPress={onSelect}
onBackdropPress={() => setShow(false)}
onRequestClose={() => setShow(false)}
showOnly={showOnly}
excludedCountries={excludedCountries}
popularCountries={popularCountries}
testID={testID}
ListHeaderComponent={({ countries, lang, onPress }) => {
return (
<View>
{!modalSectionTitleDisabled && (
<Text
style={getModalSectionTitleStyle(
theme,
modalStyles
)}
>
{popularCountriesSectionTitle ||
getPopularCountriesSectionTitle(
language || 'en'
)}
</Text>
)}
{countries.map((country, index) => {
return (
<CountryButton
key={index}
item={country}
name={country?.name?.[lang || 'en']}
onPress={() => onPress(country)}
style={getCountryPickerStyle(
theme,
modalHeight,
modalStyles
)}
/>
);
})}
{!modalSectionTitleDisabled && (
<Text
style={getModalSectionTitleStyle(
theme,
modalStyles
)}
>
{restOfCountriesSectionTitle ||
getRestOfCountriesSectionTitle(
language || 'en'
)}
</Text>
)}
</View>
);
}}
/>
) : null}
</>
);
}
}
);
export default PhoneInput;
export {
getAllCountries,
getCountryByPhoneNumber,
getCountryByCca2,
getCountriesByCallingCode,
getCountriesByName,
isValidPhoneNumber,
};