react-native-international-phone-number
Version:
International mobile phone input component with mask for React Native
673 lines (625 loc) • 20.3 kB
JavaScript
/* eslint-disable react-native/no-inline-styles */
/* eslint-disable react-hooks/exhaustive-deps */
import React, {
useEffect,
useState,
useRef,
forwardRef,
useImperativeHandle,
} from 'react';
import {
View,
Text,
TouchableOpacity,
TextInput,
Platform,
} from 'react-native';
import CountrySelect, {
getAllCountries,
getCountriesByName,
getCountriesByCallingCode,
getCountryByCca2,
} from 'react-native-country-select';
import parsePhoneNumber, {
formatIncompletePhoneNumber,
Metadata,
} from 'libphonenumber-js';
import getCountryByPhoneNumber from './utils/getCountryByPhoneNumber';
import { getPhoneNumberLength } from './utils/getPhoneNumberLength';
import isValidPhoneNumber from './utils/isValidPhoneNumber';
import {
getCountriesButtonAccessibilityHint,
getCountriesButtonAccessibilityLabel,
getPhoneNumberInputAccessibilityHint,
getPhoneNumberInputAccessibilityLabel,
getPhoneNumberInputPlaceholder,
} from './utils/getTranslations';
import {
getCaretStyle,
getContainerStyle,
getDividerStyle,
getFlagContainerStyle,
getFlagStyle,
getFlagTextStyle,
getInputStyle,
} from './utils/getStyles';
const PhoneInput = forwardRef(
(
{
theme,
language,
placeholder,
phoneInputPlaceholderTextColor,
phoneInputSelectionColor,
phoneInputStyles,
modalStyles,
disabled,
modalDisabled,
defaultCountry,
defaultValue,
onChangePhoneNumber,
selectedCountry,
onChangeSelectedCountry,
customMask,
visibleCountries,
hiddenCountries,
popularCountries,
customCaret,
rtl,
isFullScreen = false,
modalType = Platform.OS === 'web' ? 'popup' : 'bottomSheet',
minBottomsheetHeight,
maxBottomsheetHeight,
initialBottomsheetHeight,
modalDragHandleIndicatorComponent,
modalSearchInputPlaceholderTextColor,
modalSearchInputPlaceholder,
modalSearchInputSelectionColor,
modalPopularCountriesTitle,
modalAllCountriesTitle,
modalSectionTitleComponent,
modalCountryItemComponent,
modalCloseButtonComponent,
modalSectionTitleDisabled,
modalNotFoundCountryMessage,
disabledModalBackdropPress,
removedModalBackdrop,
onModalBackdropPress,
onModalRequestClose,
showModalAlphabetFilter = true,
showModalSearchInput,
showModalCloseButton,
showModalScrollIndicator,
allowFontScaling = true,
customFlag,
accessibilityLabelPhoneInput,
accessibilityHintPhoneInput,
accessibilityLabelCountriesButton,
accessibilityHintCountriesButton,
accessibilityLabelBackdrop,
accessibilityHintBackdrop,
accessibilityLabelCloseButton,
accessibilityHintCloseButton,
accessibilityLabelSearchInput,
accessibilityHintSearchInput,
accessibilityLabelCountriesList,
accessibilityHintCountriesList,
accessibilityLabelCountryItem,
accessibilityHintCountryItem,
accessibilityLabelAlphabetFilter,
accessibilityHintAlphabetFilter,
accessibilityLabelAlphabetLetter,
accessibilityHintAlphabetLetter,
...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);
useImperativeHandle(
ref,
() => ({
focus: () => textInputRef.current?.focus(),
blur: () => textInputRef.current?.blur(),
clear: () => textInputRef.current?.clear(),
isFocused: () => textInputRef.current?.isFocused(),
setNativeProps: (nativeProps) =>
textInputRef.current?.setNativeProps(nativeProps),
measure: (callback) =>
textInputRef.current?.measure(callback),
measureInWindow: (callback) =>
textInputRef.current?.measureInWindow(callback),
measureLayout: (relativeToNativeNode, onSuccess, onFail) =>
textInputRef.current?.measureLayout(
relativeToNativeNode,
onSuccess,
onFail
),
// Custom methods and properties
getValue: () => inputValue?.replace(/\D/g, ''),
value: inputValue?.replace(/\D/g, ''),
getValueFormatted: () => inputValue,
valueFormatted: inputValue,
getFullPhoneNumber: () =>
`${countryValue?.idd?.root || ''} ${inputValue || ''}`,
fullPhoneNumber: `${countryValue?.idd?.root || ''} ${
inputValue || ''
}`,
phoneNumberLength: getPhoneNumberLength(
countryValue,
inputValue
),
getSelectedCountry: () => countryValue,
selectedCountry: countryValue,
isValid: isValidPhoneNumber(inputValue, countryValue),
props: {
theme,
language,
placeholder,
phoneInputPlaceholderTextColor,
phoneInputSelectionColor,
phoneInputStyles,
modalStyles,
disabled,
modalDisabled,
defaultCountry,
defaultValue,
onChangePhoneNumber,
selectedCountry,
onChangeSelectedCountry,
customMask,
visibleCountries,
hiddenCountries,
popularCountries,
customCaret,
rtl,
isFullScreen,
modalType,
minBottomsheetHeight,
maxBottomsheetHeight,
initialBottomsheetHeight,
modalSearchInputPlaceholderTextColor,
modalSearchInputPlaceholder,
modalSearchInputSelectionColor,
modalPopularCountriesTitle,
modalAllCountriesTitle,
modalSectionTitleComponent,
modalCountryItemComponent,
modalCloseButtonComponent,
modalSectionTitleDisabled,
modalNotFoundCountryMessage,
disabledModalBackdropPress,
removedModalBackdrop,
onModalBackdropPress,
onModalRequestClose,
showModalSearchInput,
showModalCloseButton,
showModalScrollIndicator,
customFlag,
accessibilityLabelPhoneInput,
accessibilityHintPhoneInput,
accessibilityLabelCountriesButton,
accessibilityHintCountriesButton,
accessibilityLabelBackdrop,
accessibilityHintBackdrop,
accessibilityLabelCloseButton,
accessibilityHintCloseButton,
accessibilityLabelSearchInput,
accessibilityHintSearchInput,
accessibilityLabelCountriesList,
accessibilityHintCountriesList,
accessibilityLabelCountryItem,
accessibilityHintCountryItem,
allowFontScaling,
value: inputValue,
...rest,
},
}),
[inputValue, countryValue]
);
function onSelect(country) {
setShow(false);
setInputValue('');
setCountryValue(country);
if (onChangePhoneNumber) {
onChangePhoneNumber('');
}
if (onChangeSelectedCountry) {
onChangeSelectedCountry(country);
}
}
function formatPhoneNumberWithCustomMask(phoneNumber) {
if (!customMask || !phoneNumber) {
return phoneNumber;
}
const numbers = phoneNumber.replace(/\D/g, '');
let result = '';
let numberIndex = 0;
for (
let i = 0;
i < customMask.length && numberIndex < numbers.length;
i++
) {
if (customMask[i] === '#') {
result += numbers[numberIndex];
numberIndex++;
} else {
result += customMask[i];
}
}
setInputValue(result);
if (onChangePhoneNumber) {
onChangePhoneNumber(result);
}
}
function formatPhoneNumber(phoneNumber, callingCode) {
try {
let formattedNumber = '';
const metadata = new Metadata();
metadata.selectNumberingPlan(countryValue?.cca2);
const possibleLengths = countryValue
? metadata.possibleLengths()
: [];
let validCallingCode = callingCode
? callingCode
: countryValue?.idd?.root;
const res = formatIncompletePhoneNumber(
`${validCallingCode}${phoneNumber}`
);
formattedNumber = res;
if (res.startsWith(0)) {
formattedNumber = parsePhoneNumber(res)?.formatNational();
} else {
if (
validCallingCode &&
res &&
res.startsWith(validCallingCode)
) {
formattedNumber = res
.substring(validCallingCode.length)
.trim();
}
}
const possibleLength = formattedNumber.startsWith(0)
? possibleLengths.slice(-1)[0] + 1
: possibleLengths.slice(-1)[0];
if (
formattedNumber?.replace(/\D/g, '')?.length > possibleLength
) {
return;
}
setInputValue(formattedNumber);
if (onChangePhoneNumber) {
onChangePhoneNumber(formattedNumber);
}
} catch {
setInputValue(phoneNumber);
if (onChangePhoneNumber) {
onChangePhoneNumber(phoneNumber);
}
}
}
function onChangeText(phoneNumber, callingCode) {
if (phoneNumber.includes('+')) {
const matchingCountry = getCountryByPhoneNumber(phoneNumber);
if (matchingCountry) {
setDefaultCca2(matchingCountry.cca2);
setCountryValue(matchingCountry);
if (onChangeSelectedCountry) {
onChangeSelectedCountry(matchingCountry);
}
onChangeText(
phoneNumber.replace(matchingCountry?.idd?.root, ''),
null
);
}
return;
}
if (customMask) {
return formatPhoneNumberWithCustomMask(phoneNumber);
}
formatPhoneNumber(phoneNumber, callingCode);
}
useEffect(() => {
if (!countryValue && !defaultCountry) {
const country = getCountryByCca2('BR');
setCountryValue(country);
if (onChangeSelectedCountry) {
onChangeSelectedCountry(country);
}
}
}, []);
useEffect(() => {
if (defaultCountry) {
const c = getCountryByCca2(defaultCountry);
setCountryValue(c);
if (onChangeSelectedCountry) {
onChangeSelectedCountry(c);
}
}
}, [defaultCountry]);
useEffect(() => {
if (defaultValue) {
const matchingCountry = getCountryByPhoneNumber(defaultValue);
if (matchingCountry) {
setDefaultCca2(matchingCountry.cca2);
setCountryValue(matchingCountry);
if (onChangeSelectedCountry) {
onChangeSelectedCountry(matchingCountry);
}
} else {
// 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
) {
const callingCode = countryValue?.idd?.root;
let phoneNumber = defaultValue;
onChangeText(phoneNumber, callingCode);
}
}, [countryValue]);
useEffect(() => {
if (typeof rest.value === 'string') {
setInputValue(rest.value);
}
if (selectedCountry) {
setCountryValue(selectedCountry);
}
}, [selectedCountry, rest.value]);
{
// Create a separate constant for each part of the component
const touchableStart = (
<>
{(customFlag &&
countryValue &&
customFlag(countryValue)) || (
<Text
style={getFlagStyle(phoneInputStyles?.flag)}
allowFontScaling={allowFontScaling}
>
{countryValue?.flag || countryValue?.cca2}
</Text>
)}
{(customCaret && 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
)}
allowFontScaling={allowFontScaling}
>
{countryValue?.idd?.root}
</Text>
);
const touchablePart = (
<TouchableOpacity
testID="countryPickerFlagContainerButton"
accessibillityRole="button"
accessibilityLabel={
accessibilityLabelCountriesButton ||
getCountriesButtonAccessibilityLabel(language || 'eng')
}
accessibilityHint={
accessibilityHintCountriesButton ||
getCountriesButtonAccessibilityHint(language || 'eng')
}
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 || 'eng')
}
placeholderTextColor={
phoneInputPlaceholderTextColor ||
(theme === 'dark' ? '#CCCCCC' : '#AAAAAA')
}
selectionColor={
phoneInputSelectionColor ||
(theme === 'dark'
? 'rgba(255,255,255, .4)'
: 'rgba(0 ,0 ,0 , .4)')
}
editable={!disabled}
value={inputValue}
onChangeText={onChangeText}
keyboardType="number-pad"
ref={textInputRef}
testID="countryPickerPhoneInput"
accessibillityRole="input"
accessibilityLabel={
accessibilityLabelPhoneInput ||
getPhoneNumberInputAccessibilityLabel(language || 'eng')
}
accessibilityHint={
accessibilityHintPhoneInput ||
getPhoneNumberInputAccessibilityHint(language || 'eng')
}
allowFontScaling={allowFontScaling}
{...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 ? (
<>
<CountrySelect
visible={show}
onClose={() => setShow(false)}
onSelect={onSelect}
theme={theme}
language={language}
searchPlaceholder={modalSearchInputPlaceholder}
searchPlaceholderTextColor={
modalSearchInputPlaceholderTextColor
}
hiddenCountries={
hiddenCountries
? ['HM', 'AQ', ...hiddenCountries]
: ['HM', 'AQ']
}
visibleCountries={visibleCountries}
popularCountries={popularCountries}
onBackdropPress={(closeModal) =>
onModalBackdropPress
? onModalBackdropPress(closeModal)
: setShow(false)
}
onRequestClose={() =>
onModalRequestClose
? onModalRequestClose()
: setShow(false)
}
countrySelectStyle={modalStyles}
isFullScreen={isFullScreen}
modalType={modalType}
minBottomsheetHeight={minBottomsheetHeight}
maxBottomsheetHeight={maxBottomsheetHeight}
initialBottomsheetHeight={initialBottomsheetHeight}
allCountriesTitle={modalAllCountriesTitle}
popularCountriesTitle={modalPopularCountriesTitle}
sectionTitleComponent={
modalSectionTitleDisabled
? () => ''
: modalSectionTitleComponent
}
countryItemComponent={modalCountryItemComponent}
modalDragHandleIndicatorComponent={
modalDragHandleIndicatorComponent
}
showCloseButton={showModalCloseButton}
closeButtonComponent={modalCloseButtonComponent}
disabledBackdropPress={disabledModalBackdropPress}
removedBackdrop={removedModalBackdrop}
showSearchInput={showModalSearchInput}
showAlphabetFilter={showModalAlphabetFilter}
countryNotFoundMessage={modalNotFoundCountryMessage}
customFlag={customFlag}
accessibilityLabelBackdrop={
accessibilityLabelBackdrop
}
accessibilityHintBackdrop={accessibilityHintBackdrop}
accessibilityLabelCloseButton={
accessibilityLabelCloseButton
}
accessibilityHintCloseButton={
accessibilityHintCloseButton
}
accessibilityLabelSearchInput={
accessibilityLabelSearchInput
}
accessibilityHintSearchInput={
accessibilityHintSearchInput
}
accessibilityLabelCountriesList={
accessibilityLabelCountriesList
}
accessibilityHintCountriesList={
accessibilityHintCountriesList
}
accessibilityLabelCountryItem={
accessibilityLabelCountryItem
}
accessibilityHintCountryItem={
accessibilityHintCountryItem
}
accessibilityLabelAlphabetFilter={
accessibilityLabelAlphabetFilter
}
accessibilityHintAlphabetFilter={
accessibilityHintAlphabetFilter
}
accessibilityLabelAlphabetLetter={
accessibilityLabelAlphabetLetter
}
accessibilityHintAlphabetLetter={
accessibilityHintAlphabetLetter
}
allowFontScaling={allowFontScaling}
/>
</>
) : null}
</>
);
}
}
);
export default PhoneInput;
export {
getAllCountries,
getCountryByPhoneNumber,
getCountryByCca2,
getCountriesByCallingCode,
getCountriesByName,
isValidPhoneNumber,
getPhoneNumberLength,
};