react-native-phone-country-input 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +477 -0
- package/lib/commonjs/CountrySelector/CountrySelector.js +74 -0
- package/lib/commonjs/CountrySelector/CountrySelector.js.map +1 -0
- package/lib/commonjs/CountrySelector/CountrySelectorModal.js +267 -0
- package/lib/commonjs/CountrySelector/CountrySelectorModal.js.map +1 -0
- package/lib/commonjs/Keyboard/Keyboard.js +316 -0
- package/lib/commonjs/Keyboard/Keyboard.js.map +1 -0
- package/lib/commonjs/Keyboard/KeyboardToolbar.js +70 -0
- package/lib/commonjs/Keyboard/KeyboardToolbar.js.map +1 -0
- package/lib/commonjs/Keyboard/KeypadButton.js +66 -0
- package/lib/commonjs/Keyboard/KeypadButton.js.map +1 -0
- package/lib/commonjs/Keyboard/KeypadButtonContainer.js +65 -0
- package/lib/commonjs/Keyboard/KeypadButtonContainer.js.map +1 -0
- package/lib/commonjs/Keyboard/KeypadRow.js +34 -0
- package/lib/commonjs/Keyboard/KeypadRow.js.map +1 -0
- package/lib/commonjs/PhoneCountryInput/PhoneCountryInput.js +86 -0
- package/lib/commonjs/PhoneCountryInput/PhoneCountryInput.js.map +1 -0
- package/lib/commonjs/PhoneNumberField.js +36 -0
- package/lib/commonjs/PhoneNumberField.js.map +1 -0
- package/lib/commonjs/Styling/Colors.js +197 -0
- package/lib/commonjs/Styling/Colors.js.map +1 -0
- package/lib/commonjs/Styling/Sizing.js +111 -0
- package/lib/commonjs/Styling/Sizing.js.map +1 -0
- package/lib/commonjs/consts/KEYBOARD_LAYOUT.js +45 -0
- package/lib/commonjs/consts/KEYBOARD_LAYOUT.js.map +1 -0
- package/lib/commonjs/consts/regions.js +1503 -0
- package/lib/commonjs/consts/regions.js.map +1 -0
- package/lib/commonjs/enum/CountryIds.js +264 -0
- package/lib/commonjs/enum/CountryIds.js.map +1 -0
- package/lib/commonjs/hooks/UsePhoneFieldState.js +237 -0
- package/lib/commonjs/hooks/UsePhoneFieldState.js.map +1 -0
- package/lib/commonjs/index.js +56 -0
- package/lib/commonjs/index.js.map +1 -0
- package/lib/commonjs/package.json +1 -0
- package/lib/commonjs/utils/characterDeletion.js +20 -0
- package/lib/commonjs/utils/characterDeletion.js.map +1 -0
- package/lib/commonjs/utils/characterInsert.js +20 -0
- package/lib/commonjs/utils/characterInsert.js.map +1 -0
- package/lib/commonjs/utils/fromMaskedNumberToUnmaskedSelection.js +14 -0
- package/lib/commonjs/utils/fromMaskedNumberToUnmaskedSelection.js.map +1 -0
- package/lib/commonjs/utils/fromUnmaskedToMaskedPosition.js +20 -0
- package/lib/commonjs/utils/fromUnmaskedToMaskedPosition.js.map +1 -0
- package/lib/commonjs/utils/generateCountryCodeList.js +23 -0
- package/lib/commonjs/utils/generateCountryCodeList.js.map +1 -0
- package/lib/commonjs/utils/getDefaultRegion.js +33 -0
- package/lib/commonjs/utils/getDefaultRegion.js.map +1 -0
- package/lib/commonjs/utils/maskToPhoneNumber.js +23 -0
- package/lib/commonjs/utils/maskToPhoneNumber.js.map +1 -0
- package/lib/commonjs/utils/matchCountryCode.js +27 -0
- package/lib/commonjs/utils/matchCountryCode.js.map +1 -0
- package/lib/module/CountrySelector/CountrySelector.js +70 -0
- package/lib/module/CountrySelector/CountrySelector.js.map +1 -0
- package/lib/module/CountrySelector/CountrySelectorModal.js +262 -0
- package/lib/module/CountrySelector/CountrySelectorModal.js.map +1 -0
- package/lib/module/Keyboard/Keyboard.js +310 -0
- package/lib/module/Keyboard/Keyboard.js.map +1 -0
- package/lib/module/Keyboard/KeyboardToolbar.js +65 -0
- package/lib/module/Keyboard/KeyboardToolbar.js.map +1 -0
- package/lib/module/Keyboard/KeypadButton.js +61 -0
- package/lib/module/Keyboard/KeypadButton.js.map +1 -0
- package/lib/module/Keyboard/KeypadButtonContainer.js +59 -0
- package/lib/module/Keyboard/KeypadButtonContainer.js.map +1 -0
- package/lib/module/Keyboard/KeypadRow.js +30 -0
- package/lib/module/Keyboard/KeypadRow.js.map +1 -0
- package/lib/module/PhoneCountryInput/PhoneCountryInput.js +80 -0
- package/lib/module/PhoneCountryInput/PhoneCountryInput.js.map +1 -0
- package/lib/module/PhoneNumberField.js +31 -0
- package/lib/module/PhoneNumberField.js.map +1 -0
- package/lib/module/Styling/Colors.js +193 -0
- package/lib/module/Styling/Colors.js.map +1 -0
- package/lib/module/Styling/Sizing.js +107 -0
- package/lib/module/Styling/Sizing.js.map +1 -0
- package/lib/module/consts/KEYBOARD_LAYOUT.js +41 -0
- package/lib/module/consts/KEYBOARD_LAYOUT.js.map +1 -0
- package/lib/module/consts/regions.js +1498 -0
- package/lib/module/consts/regions.js.map +1 -0
- package/lib/module/enum/CountryIds.js +260 -0
- package/lib/module/enum/CountryIds.js.map +1 -0
- package/lib/module/hooks/UsePhoneFieldState.js +232 -0
- package/lib/module/hooks/UsePhoneFieldState.js.map +1 -0
- package/lib/module/index.js +16 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/module/utils/characterDeletion.js +16 -0
- package/lib/module/utils/characterDeletion.js.map +1 -0
- package/lib/module/utils/characterInsert.js +16 -0
- package/lib/module/utils/characterInsert.js.map +1 -0
- package/lib/module/utils/fromMaskedNumberToUnmaskedSelection.js +10 -0
- package/lib/module/utils/fromMaskedNumberToUnmaskedSelection.js.map +1 -0
- package/lib/module/utils/fromUnmaskedToMaskedPosition.js +16 -0
- package/lib/module/utils/fromUnmaskedToMaskedPosition.js.map +1 -0
- package/lib/module/utils/generateCountryCodeList.js +19 -0
- package/lib/module/utils/generateCountryCodeList.js.map +1 -0
- package/lib/module/utils/getDefaultRegion.js +28 -0
- package/lib/module/utils/getDefaultRegion.js.map +1 -0
- package/lib/module/utils/maskToPhoneNumber.js +19 -0
- package/lib/module/utils/maskToPhoneNumber.js.map +1 -0
- package/lib/module/utils/matchCountryCode.js +23 -0
- package/lib/module/utils/matchCountryCode.js.map +1 -0
- package/lib/typescript/CountrySelector/CountrySelector.d.ts +19 -0
- package/lib/typescript/CountrySelector/CountrySelector.d.ts.map +1 -0
- package/lib/typescript/CountrySelector/CountrySelectorModal.d.ts +12 -0
- package/lib/typescript/CountrySelector/CountrySelectorModal.d.ts.map +1 -0
- package/lib/typescript/Keyboard/Keyboard.d.ts +25 -0
- package/lib/typescript/Keyboard/Keyboard.d.ts.map +1 -0
- package/lib/typescript/Keyboard/KeyboardToolbar.d.ts +9 -0
- package/lib/typescript/Keyboard/KeyboardToolbar.d.ts.map +1 -0
- package/lib/typescript/Keyboard/KeypadButton.d.ts +10 -0
- package/lib/typescript/Keyboard/KeypadButton.d.ts.map +1 -0
- package/lib/typescript/Keyboard/KeypadButtonContainer.d.ts +12 -0
- package/lib/typescript/Keyboard/KeypadButtonContainer.d.ts.map +1 -0
- package/lib/typescript/Keyboard/KeypadRow.d.ts +9 -0
- package/lib/typescript/Keyboard/KeypadRow.d.ts.map +1 -0
- package/lib/typescript/PhoneCountryInput/PhoneCountryInput.d.ts +16 -0
- package/lib/typescript/PhoneCountryInput/PhoneCountryInput.d.ts.map +1 -0
- package/lib/typescript/PhoneNumberField.d.ts +17 -0
- package/lib/typescript/PhoneNumberField.d.ts.map +1 -0
- package/lib/typescript/Styling/Colors.d.ts +189 -0
- package/lib/typescript/Styling/Colors.d.ts.map +1 -0
- package/lib/typescript/Styling/Sizing.d.ts +214 -0
- package/lib/typescript/Styling/Sizing.d.ts.map +1 -0
- package/lib/typescript/consts/KEYBOARD_LAYOUT.d.ts +18 -0
- package/lib/typescript/consts/KEYBOARD_LAYOUT.d.ts.map +1 -0
- package/lib/typescript/consts/regions.d.ts +10 -0
- package/lib/typescript/consts/regions.d.ts.map +1 -0
- package/lib/typescript/enum/CountryIds.d.ts +249 -0
- package/lib/typescript/enum/CountryIds.d.ts.map +1 -0
- package/lib/typescript/hooks/UsePhoneFieldState.d.ts +34 -0
- package/lib/typescript/hooks/UsePhoneFieldState.d.ts.map +1 -0
- package/lib/typescript/index.d.ts +15 -0
- package/lib/typescript/index.d.ts.map +1 -0
- package/lib/typescript/utils/characterDeletion.d.ts +3 -0
- package/lib/typescript/utils/characterDeletion.d.ts.map +1 -0
- package/lib/typescript/utils/characterInsert.d.ts +3 -0
- package/lib/typescript/utils/characterInsert.d.ts.map +1 -0
- package/lib/typescript/utils/fromMaskedNumberToUnmaskedSelection.d.ts +2 -0
- package/lib/typescript/utils/fromMaskedNumberToUnmaskedSelection.d.ts.map +1 -0
- package/lib/typescript/utils/fromUnmaskedToMaskedPosition.d.ts +2 -0
- package/lib/typescript/utils/fromUnmaskedToMaskedPosition.d.ts.map +1 -0
- package/lib/typescript/utils/generateCountryCodeList.d.ts +3 -0
- package/lib/typescript/utils/generateCountryCodeList.d.ts.map +1 -0
- package/lib/typescript/utils/getDefaultRegion.d.ts +4 -0
- package/lib/typescript/utils/getDefaultRegion.d.ts.map +1 -0
- package/lib/typescript/utils/maskToPhoneNumber.d.ts +2 -0
- package/lib/typescript/utils/maskToPhoneNumber.d.ts.map +1 -0
- package/lib/typescript/utils/matchCountryCode.d.ts +3 -0
- package/lib/typescript/utils/matchCountryCode.d.ts.map +1 -0
- package/package.json +92 -0
- package/src/CountrySelector/CountrySelector.tsx +77 -0
- package/src/CountrySelector/CountrySelectorModal.tsx +280 -0
- package/src/Keyboard/Keyboard.tsx +322 -0
- package/src/Keyboard/KeyboardToolbar.tsx +53 -0
- package/src/Keyboard/KeypadButton.tsx +58 -0
- package/src/Keyboard/KeypadButtonContainer.tsx +67 -0
- package/src/Keyboard/KeypadRow.tsx +29 -0
- package/src/PhoneCountryInput/PhoneCountryInput.tsx +98 -0
- package/src/PhoneNumberField.tsx +46 -0
- package/src/Styling/Colors.ts +206 -0
- package/src/Styling/Sizing.ts +110 -0
- package/src/consts/KEYBOARD_LAYOUT.ts +34 -0
- package/src/consts/regions.ts +268 -0
- package/src/enum/CountryIds.ts +256 -0
- package/src/hooks/UsePhoneFieldState.tsx +268 -0
- package/src/index.ts +27 -0
- package/src/utils/characterDeletion.ts +16 -0
- package/src/utils/characterInsert.ts +20 -0
- package/src/utils/fromMaskedNumberToUnmaskedSelection.ts +10 -0
- package/src/utils/fromUnmaskedToMaskedPosition.ts +13 -0
- package/src/utils/generateCountryCodeList.ts +22 -0
- package/src/utils/getDefaultRegion.ts +30 -0
- package/src/utils/maskToPhoneNumber.ts +30 -0
- package/src/utils/matchCountryCode.ts +23 -0
package/package.json
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "react-native-phone-country-input",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "International phone number input for React Native with country selection, masking, and a custom keyboard",
|
|
5
|
+
"main": "lib/commonjs/index",
|
|
6
|
+
"module": "lib/module/index",
|
|
7
|
+
"types": "lib/typescript/index.d.ts",
|
|
8
|
+
"react-native": "src/index",
|
|
9
|
+
"source": "src/index",
|
|
10
|
+
"files": [
|
|
11
|
+
"src",
|
|
12
|
+
"lib",
|
|
13
|
+
"!**/*.test.js",
|
|
14
|
+
"!**/*.test.js.map",
|
|
15
|
+
"!**/*.test.ts",
|
|
16
|
+
"!**/*.test.tsx",
|
|
17
|
+
"!**/*.html"
|
|
18
|
+
],
|
|
19
|
+
"scripts": {
|
|
20
|
+
"android": "expo run:android",
|
|
21
|
+
"ios": "expo run:ios",
|
|
22
|
+
"start": "expo start",
|
|
23
|
+
"build": "bob build",
|
|
24
|
+
"prepare": "bob build",
|
|
25
|
+
"lint": "eslint \"**/*.{js,jsx,ts,tsx}\" && prettier -c \"**/*.{js,jsx,ts,tsx,json}\"",
|
|
26
|
+
"format": "eslint \"**/*.{js,jsx,ts,tsx}\" --fix && prettier \"**/*.{js,jsx,ts,tsx,json}\" --write",
|
|
27
|
+
"web": "expo start --web",
|
|
28
|
+
"expo:prebuild": "expo prebuild",
|
|
29
|
+
"test": "jest"
|
|
30
|
+
},
|
|
31
|
+
"peerDependencies": {
|
|
32
|
+
"@react-native-async-storage/async-storage": ">=2.0.0",
|
|
33
|
+
"@react-native-clipboard/clipboard": ">=1.0.0",
|
|
34
|
+
"expo-localization": ">=17.0.0",
|
|
35
|
+
"react": ">=18.0.0",
|
|
36
|
+
"react-native": ">=0.71.0",
|
|
37
|
+
"react-native-gesture-handler": ">=2.0.0",
|
|
38
|
+
"react-native-reanimated": ">=3.0.0",
|
|
39
|
+
"react-native-teleport": ">=1.0.0"
|
|
40
|
+
},
|
|
41
|
+
"devDependencies": {
|
|
42
|
+
"@babel/core": "^7.20.0",
|
|
43
|
+
"@expo/vector-icons": "^15.0.2",
|
|
44
|
+
"@react-native-async-storage/async-storage": "2.2.0",
|
|
45
|
+
"@react-native-clipboard/clipboard": "^1.16.3",
|
|
46
|
+
"@react-navigation/native": "^7.1.6",
|
|
47
|
+
"@react-navigation/stack": "^7.4.8",
|
|
48
|
+
"@types/jest": "^29.5.14",
|
|
49
|
+
"@types/react": "~19.1.10",
|
|
50
|
+
"eslint": "^9.25.1",
|
|
51
|
+
"eslint-config-expo": "~10.0.0",
|
|
52
|
+
"eslint-config-prettier": "^10.1.2",
|
|
53
|
+
"expo": "^54.0.0",
|
|
54
|
+
"expo-localization": "~17.0.9",
|
|
55
|
+
"expo-status-bar": "~3.0.8",
|
|
56
|
+
"jest": "^29.7.0",
|
|
57
|
+
"jest-expo": "^56.0.5",
|
|
58
|
+
"prettier": "^3.2.5",
|
|
59
|
+
"react": "19.1.0",
|
|
60
|
+
"react-native": "0.81.5",
|
|
61
|
+
"react-native-builder-bob": "^0.30.3",
|
|
62
|
+
"react-native-gesture-handler": "~2.28.0",
|
|
63
|
+
"react-native-reanimated": "~4.1.1",
|
|
64
|
+
"react-native-safe-area-context": "~5.6.0",
|
|
65
|
+
"react-native-screens": "~4.16.0",
|
|
66
|
+
"react-native-teleport": "^1.1.8",
|
|
67
|
+
"typescript": "~5.9.2"
|
|
68
|
+
},
|
|
69
|
+
"react-native-builder-bob": {
|
|
70
|
+
"source": "src",
|
|
71
|
+
"output": "lib",
|
|
72
|
+
"targets": [
|
|
73
|
+
"commonjs",
|
|
74
|
+
"module",
|
|
75
|
+
[
|
|
76
|
+
"typescript",
|
|
77
|
+
{
|
|
78
|
+
"project": "tsconfig.build.json"
|
|
79
|
+
}
|
|
80
|
+
]
|
|
81
|
+
]
|
|
82
|
+
},
|
|
83
|
+
"jest": {
|
|
84
|
+
"preset": "jest-expo",
|
|
85
|
+
"testMatch": [
|
|
86
|
+
"**/*.test.(ts|tsx)"
|
|
87
|
+
],
|
|
88
|
+
"transformIgnorePatterns": [
|
|
89
|
+
"node_modules/(?!((jest-)?react-native|@react-native(-community)?)|expo(nent)?|@expo(nent)?/.*|@expo-google-fonts/.*|react-navigation|@react-navigation/.*|@unimodules/.*|unimodules|sentry-expo|native-base|react-native-svg)"
|
|
90
|
+
]
|
|
91
|
+
}
|
|
92
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { useCallback, useEffect, useMemo, useState } from 'react';
|
|
2
|
+
import { CountryCode } from '../consts/regions';
|
|
3
|
+
import { Modal, Pressable, Text } from 'react-native';
|
|
4
|
+
import { CountrySelectorModal } from './CountrySelectorModal';
|
|
5
|
+
import { Feather } from '@expo/vector-icons';
|
|
6
|
+
|
|
7
|
+
// button for opening the country selector modal
|
|
8
|
+
interface CountrySelectorButtonProps extends React.ComponentProps<typeof Pressable> {
|
|
9
|
+
value: CountryCode | null;
|
|
10
|
+
underlineButton?: React.ComponentType<React.ComponentProps<typeof Pressable>> | null;
|
|
11
|
+
isOpen?: boolean;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
//
|
|
15
|
+
|
|
16
|
+
function CountrySelectorButton(props: CountrySelectorButtonProps) {
|
|
17
|
+
const Button = useMemo(() => {
|
|
18
|
+
if (props.underlineButton) {
|
|
19
|
+
console.debug('Using custom button for CountrySelectorButton');
|
|
20
|
+
return props.underlineButton;
|
|
21
|
+
}
|
|
22
|
+
console.debug('Using default Pressable for CountrySelectorButton');
|
|
23
|
+
return Pressable;
|
|
24
|
+
}, [props.underlineButton]);
|
|
25
|
+
return (
|
|
26
|
+
// This could be a simple button that, when pressed, opens the country selector modal
|
|
27
|
+
<Button {...props}>
|
|
28
|
+
<Text>{props.value ? props.value.flag : '🏴☠️'}</Text>
|
|
29
|
+
<Feather name={`chevron-${props.isOpen ? 'up' : 'down'}`} size={14} color="gray" />
|
|
30
|
+
</Button>
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
export interface CountrySelectorProps extends React.ComponentProps<typeof CountrySelectorButton> {
|
|
34
|
+
filtedredCountryCodes?: CountryCode[] | null;
|
|
35
|
+
onSelectCountry?: (country: CountryCode) => void;
|
|
36
|
+
value: CountryCode | null;
|
|
37
|
+
underlineButton?: React.ComponentType<React.ComponentProps<typeof Pressable>> | null;
|
|
38
|
+
underlineModal?: React.ComponentType<React.ComponentProps<typeof Modal>> | null;
|
|
39
|
+
onOpenChange?: (open: boolean) => void;
|
|
40
|
+
}
|
|
41
|
+
export function CountrySelector(props: CountrySelectorProps) {
|
|
42
|
+
const [internalValue, setInternalValue] = useState(props.value);
|
|
43
|
+
const [internalIsOpen, setInternalIsOpen] = useState(false);
|
|
44
|
+
const { filtedredCountryCodes, onSelectCountry, onOpenChange } = props;
|
|
45
|
+
|
|
46
|
+
const isControlled = props.isOpen !== undefined;
|
|
47
|
+
const effectiveIsOpen = isControlled ? (props.isOpen ?? false) : internalIsOpen;
|
|
48
|
+
|
|
49
|
+
const toggle = useCallback(() => {
|
|
50
|
+
const next = !effectiveIsOpen;
|
|
51
|
+
if (!isControlled) setInternalIsOpen(next);
|
|
52
|
+
onOpenChange?.(next);
|
|
53
|
+
}, [effectiveIsOpen, isControlled, onOpenChange]);
|
|
54
|
+
|
|
55
|
+
useEffect(() => {
|
|
56
|
+
setInternalValue(props.value);
|
|
57
|
+
}, [props.value]);
|
|
58
|
+
|
|
59
|
+
return (
|
|
60
|
+
<>
|
|
61
|
+
<CountrySelectorButton
|
|
62
|
+
{...props}
|
|
63
|
+
underlineButton={props.underlineButton}
|
|
64
|
+
value={internalValue}
|
|
65
|
+
isOpen={effectiveIsOpen}
|
|
66
|
+
onPress={toggle}
|
|
67
|
+
/>
|
|
68
|
+
<CountrySelectorModal
|
|
69
|
+
value={internalValue}
|
|
70
|
+
onSelectCountry={onSelectCountry ?? (() => {})}
|
|
71
|
+
UserCountryCodes={filtedredCountryCodes}
|
|
72
|
+
isOpen={effectiveIsOpen}
|
|
73
|
+
toggleModalVisablity={toggle}
|
|
74
|
+
/>
|
|
75
|
+
</>
|
|
76
|
+
);
|
|
77
|
+
}
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
import { useCallback, useEffect, useMemo, useState } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
Modal,
|
|
4
|
+
View,
|
|
5
|
+
Text,
|
|
6
|
+
StyleSheet,
|
|
7
|
+
useWindowDimensions,
|
|
8
|
+
TouchableWithoutFeedback,
|
|
9
|
+
TextInput,
|
|
10
|
+
SectionList,
|
|
11
|
+
Pressable,
|
|
12
|
+
} from 'react-native';
|
|
13
|
+
import AsyncStorage from '@react-native-async-storage/async-storage';
|
|
14
|
+
import { Feather } from '@expo/vector-icons';
|
|
15
|
+
import { CountryCode } from '../consts/regions';
|
|
16
|
+
import { getDefaultRegionWithNullableState } from '../utils/getDefaultRegion';
|
|
17
|
+
import {
|
|
18
|
+
spacing,
|
|
19
|
+
borderWidth as borders,
|
|
20
|
+
fontSize as fontSizes,
|
|
21
|
+
padding,
|
|
22
|
+
borderWidth,
|
|
23
|
+
radius,
|
|
24
|
+
gap,
|
|
25
|
+
SIDE_SCREEN_PADDING,
|
|
26
|
+
} from '../Styling/Sizing';
|
|
27
|
+
import { colors } from '../Styling/Colors';
|
|
28
|
+
|
|
29
|
+
export interface CountrySelectorModalProps extends React.ComponentProps<typeof Modal> {
|
|
30
|
+
underlineModal?: React.ComponentType<React.ComponentProps<typeof Modal>> | null;
|
|
31
|
+
value: CountryCode | null;
|
|
32
|
+
UserCountryCodes?: CountryCode[] | null;
|
|
33
|
+
onSelectCountry: (country: CountryCode) => void;
|
|
34
|
+
isOpen?: boolean;
|
|
35
|
+
toggleModalVisablity: () => void;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const CLICK_COUNTS_KEY = '@country_selector_click_counts';
|
|
39
|
+
const RECENT_SECTION_COUNT = 4;
|
|
40
|
+
|
|
41
|
+
export function CountrySelectorModal(props: CountrySelectorModalProps) {
|
|
42
|
+
const { height } = useWindowDimensions();
|
|
43
|
+
const [searchValue, setSearchValue] = useState('');
|
|
44
|
+
const [clickCounts, setClickCounts] = useState<Record<string, number>>({});
|
|
45
|
+
|
|
46
|
+
useEffect(() => {
|
|
47
|
+
AsyncStorage.getItem(CLICK_COUNTS_KEY).then((raw) => {
|
|
48
|
+
if (raw) setClickCounts(JSON.parse(raw));
|
|
49
|
+
});
|
|
50
|
+
}, []);
|
|
51
|
+
|
|
52
|
+
const recordClick = useCallback(
|
|
53
|
+
async (country: CountryCode) => {
|
|
54
|
+
const updated = { ...clickCounts, [country.id]: (clickCounts[country.id] ?? 0) + 1 };
|
|
55
|
+
setClickCounts(updated);
|
|
56
|
+
await AsyncStorage.setItem(CLICK_COUNTS_KEY, JSON.stringify(updated));
|
|
57
|
+
},
|
|
58
|
+
[clickCounts]
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
const recentSection = useMemo(() => {
|
|
62
|
+
const allCountries = props.UserCountryCodes ?? [];
|
|
63
|
+
const defaultCountry = getDefaultRegionWithNullableState(allCountries);
|
|
64
|
+
|
|
65
|
+
const topClicked = Object.entries(clickCounts)
|
|
66
|
+
.sort(([, a], [, b]) => b - a)
|
|
67
|
+
.slice(0, RECENT_SECTION_COUNT)
|
|
68
|
+
.map(([id]) => allCountries.find((c) => c.id === id))
|
|
69
|
+
.filter((c): c is CountryCode => c != null);
|
|
70
|
+
|
|
71
|
+
if (!defaultCountry && topClicked.length === 0) return null;
|
|
72
|
+
|
|
73
|
+
const data: CountryCode[] = [];
|
|
74
|
+
if (defaultCountry) data.push(defaultCountry);
|
|
75
|
+
for (const c of topClicked) {
|
|
76
|
+
if (!data.some((d) => d.id === c.id)) data.push(c);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return { title: 'Recent Countries', data };
|
|
80
|
+
}, [clickCounts, props.UserCountryCodes]);
|
|
81
|
+
|
|
82
|
+
const sections = useMemo(() => {
|
|
83
|
+
const sorted = [...(props.UserCountryCodes ?? [])].sort((a, b) => a.name.localeCompare(b.name));
|
|
84
|
+
const filtered =
|
|
85
|
+
searchValue.length < 1
|
|
86
|
+
? sorted
|
|
87
|
+
: sorted.filter(
|
|
88
|
+
({ name, code, id }) =>
|
|
89
|
+
name.toLowerCase().includes(searchValue.toLowerCase()) ||
|
|
90
|
+
code.includes(searchValue) ||
|
|
91
|
+
id.toLowerCase().includes(searchValue.toLowerCase())
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
const grouped = filtered.reduce<Record<string, CountryCode[]>>((acc, country) => {
|
|
95
|
+
let letter = country.name[0].toUpperCase();
|
|
96
|
+
if (letter === 'Å') {
|
|
97
|
+
letter = 'A';
|
|
98
|
+
}
|
|
99
|
+
if (!acc[letter]) acc[letter] = [];
|
|
100
|
+
acc[letter].push(country);
|
|
101
|
+
return acc;
|
|
102
|
+
}, {});
|
|
103
|
+
|
|
104
|
+
const alphaSections = Object.keys(grouped)
|
|
105
|
+
.sort()
|
|
106
|
+
.map((letter) => ({ title: letter, data: grouped[letter] }));
|
|
107
|
+
|
|
108
|
+
return alphaSections;
|
|
109
|
+
}, [props.UserCountryCodes, searchValue]);
|
|
110
|
+
|
|
111
|
+
const allSections = useMemo(() => {
|
|
112
|
+
if (searchValue.length > 0 || !recentSection) return sections;
|
|
113
|
+
return [recentSection, ...sections];
|
|
114
|
+
}, [recentSection, sections, searchValue]);
|
|
115
|
+
|
|
116
|
+
const UserModal = useMemo(() => {
|
|
117
|
+
if (props.underlineModal) {
|
|
118
|
+
console.debug('underlineModal');
|
|
119
|
+
return props.underlineModal;
|
|
120
|
+
}
|
|
121
|
+
console.debug('underlineModal - underfine');
|
|
122
|
+
return undefined;
|
|
123
|
+
}, [props.underlineModal]);
|
|
124
|
+
|
|
125
|
+
if (UserModal) {
|
|
126
|
+
return <UserModal {...props} />;
|
|
127
|
+
}
|
|
128
|
+
return (
|
|
129
|
+
<Modal animationType="slide" visible={props.isOpen} transparent>
|
|
130
|
+
<View style={styles.overlay}>
|
|
131
|
+
<TouchableWithoutFeedback
|
|
132
|
+
onPress={() => {
|
|
133
|
+
props.toggleModalVisablity();
|
|
134
|
+
setSearchValue('');
|
|
135
|
+
}}>
|
|
136
|
+
<View style={StyleSheet.absoluteFillObject} />
|
|
137
|
+
</TouchableWithoutFeedback>
|
|
138
|
+
<View style={[styles.container, { height: height / 2.5 }]}>
|
|
139
|
+
<View
|
|
140
|
+
style={{
|
|
141
|
+
backgroundColor: colors.white,
|
|
142
|
+
padding: padding[3],
|
|
143
|
+
borderTopLeftRadius: radius['3xl'],
|
|
144
|
+
borderTopRightRadius: radius['3xl'],
|
|
145
|
+
}}>
|
|
146
|
+
<View style={styles.searchContainer}>
|
|
147
|
+
<Feather name="search" size={16} color="gray" />
|
|
148
|
+
<TextInput
|
|
149
|
+
placeholder="Country name or code"
|
|
150
|
+
style={styles.searchInput}
|
|
151
|
+
value={searchValue}
|
|
152
|
+
onChangeText={(value) => {
|
|
153
|
+
setSearchValue(value);
|
|
154
|
+
}}
|
|
155
|
+
/>
|
|
156
|
+
</View>
|
|
157
|
+
</View>
|
|
158
|
+
<SectionList
|
|
159
|
+
ItemSeparatorComponent={() => <View style={styles.divider} />}
|
|
160
|
+
sections={allSections}
|
|
161
|
+
keyExtractor={(item, index) => `${item.id}-${index}`}
|
|
162
|
+
style={{
|
|
163
|
+
borderTopLeftRadius: radius['lg'],
|
|
164
|
+
borderTopRightRadius: radius['lg'],
|
|
165
|
+
}}
|
|
166
|
+
renderSectionHeader={({ section }) => (
|
|
167
|
+
<View style={styles.sectionHeader}>
|
|
168
|
+
<Text style={styles.sectionHeaderText}>{section.title}</Text>
|
|
169
|
+
</View>
|
|
170
|
+
)}
|
|
171
|
+
renderItem={({ item, index, section }) => {
|
|
172
|
+
const isSelected = item.id === props.value?.id;
|
|
173
|
+
const isFirst = index === 0;
|
|
174
|
+
const isLast = index === section.data.length - 1;
|
|
175
|
+
return (
|
|
176
|
+
<Pressable
|
|
177
|
+
onPress={() => {
|
|
178
|
+
recordClick(item);
|
|
179
|
+
props.onSelectCountry(item);
|
|
180
|
+
props.toggleModalVisablity();
|
|
181
|
+
setSearchValue('');
|
|
182
|
+
}}
|
|
183
|
+
style={{
|
|
184
|
+
padding: spacing[3],
|
|
185
|
+
gap: spacing[1],
|
|
186
|
+
marginHorizontal: SIDE_SCREEN_PADDING,
|
|
187
|
+
backgroundColor: isSelected ? colors.blue[50] : colors.white,
|
|
188
|
+
borderTopLeftRadius: isFirst ? radius.lg : 0,
|
|
189
|
+
borderTopRightRadius: isFirst ? radius.lg : 0,
|
|
190
|
+
borderBottomLeftRadius: isLast ? radius.lg : 0,
|
|
191
|
+
borderBottomRightRadius: isLast ? radius.lg : 0,
|
|
192
|
+
}}>
|
|
193
|
+
<View
|
|
194
|
+
style={{
|
|
195
|
+
flexDirection: 'row',
|
|
196
|
+
gap: spacing[1],
|
|
197
|
+
alignItems: 'center',
|
|
198
|
+
}}>
|
|
199
|
+
<Text>{item.flag}</Text>
|
|
200
|
+
<Text
|
|
201
|
+
style={[{ flexShrink: 1 }, isSelected ? { fontWeight: '600' } : undefined]}>
|
|
202
|
+
{item.name}
|
|
203
|
+
</Text>
|
|
204
|
+
<Text>({item.code})</Text>
|
|
205
|
+
{isSelected && (
|
|
206
|
+
<Feather
|
|
207
|
+
name="check"
|
|
208
|
+
size={14}
|
|
209
|
+
color={colors.blue[500]}
|
|
210
|
+
style={{ marginLeft: 'auto' }}
|
|
211
|
+
/>
|
|
212
|
+
)}
|
|
213
|
+
</View>
|
|
214
|
+
<View>
|
|
215
|
+
<Text
|
|
216
|
+
style={{
|
|
217
|
+
fontSize: fontSizes.xs,
|
|
218
|
+
color: colors.gray[400],
|
|
219
|
+
paddingHorizontal: padding[0.5],
|
|
220
|
+
}}>
|
|
221
|
+
{item.code} {item.mask.replace(/\#/g, '_')}
|
|
222
|
+
</Text>
|
|
223
|
+
</View>
|
|
224
|
+
</Pressable>
|
|
225
|
+
);
|
|
226
|
+
}}
|
|
227
|
+
/>
|
|
228
|
+
</View>
|
|
229
|
+
</View>
|
|
230
|
+
</Modal>
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const styles = StyleSheet.create({
|
|
235
|
+
overlay: {
|
|
236
|
+
flex: 1,
|
|
237
|
+
justifyContent: 'flex-end',
|
|
238
|
+
},
|
|
239
|
+
container: {
|
|
240
|
+
backgroundColor: colors.gray[200],
|
|
241
|
+
// gap: gap[4],
|
|
242
|
+
borderTopLeftRadius: radius['3xl'],
|
|
243
|
+
borderTopRightRadius: radius['3xl'],
|
|
244
|
+
shadowColor: '#000',
|
|
245
|
+
shadowOffset: { width: 0, height: -4 },
|
|
246
|
+
shadowOpacity: 0.25,
|
|
247
|
+
shadowRadius: 8,
|
|
248
|
+
},
|
|
249
|
+
searchContainer: {
|
|
250
|
+
height: 40,
|
|
251
|
+
flexDirection: 'row',
|
|
252
|
+
alignItems: 'center',
|
|
253
|
+
borderWidth: borders.DEFAULT,
|
|
254
|
+
borderColor: colors.gray[100],
|
|
255
|
+
paddingHorizontal: spacing[1],
|
|
256
|
+
backgroundColor: colors.gray[300],
|
|
257
|
+
borderRadius: radius['2xl'],
|
|
258
|
+
},
|
|
259
|
+
searchInput: {
|
|
260
|
+
flex: 1,
|
|
261
|
+
padding: spacing[1],
|
|
262
|
+
},
|
|
263
|
+
divider: {
|
|
264
|
+
height: borderWidth.DEFAULT,
|
|
265
|
+
backgroundColor: colors.gray[200],
|
|
266
|
+
marginHorizontal: spacing[1.5],
|
|
267
|
+
},
|
|
268
|
+
sectionHeader: {
|
|
269
|
+
backgroundColor: colors.gray[200],
|
|
270
|
+
paddingHorizontal: SIDE_SCREEN_PADDING,
|
|
271
|
+
paddingVertical: spacing[2],
|
|
272
|
+
},
|
|
273
|
+
sectionHeaderText: {
|
|
274
|
+
fontSize: fontSizes.xs,
|
|
275
|
+
fontWeight: '700',
|
|
276
|
+
color: colors.gray[500],
|
|
277
|
+
textTransform: 'uppercase',
|
|
278
|
+
letterSpacing: 0.8,
|
|
279
|
+
},
|
|
280
|
+
});
|