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.
Files changed (172) hide show
  1. package/README.md +477 -0
  2. package/lib/commonjs/CountrySelector/CountrySelector.js +74 -0
  3. package/lib/commonjs/CountrySelector/CountrySelector.js.map +1 -0
  4. package/lib/commonjs/CountrySelector/CountrySelectorModal.js +267 -0
  5. package/lib/commonjs/CountrySelector/CountrySelectorModal.js.map +1 -0
  6. package/lib/commonjs/Keyboard/Keyboard.js +316 -0
  7. package/lib/commonjs/Keyboard/Keyboard.js.map +1 -0
  8. package/lib/commonjs/Keyboard/KeyboardToolbar.js +70 -0
  9. package/lib/commonjs/Keyboard/KeyboardToolbar.js.map +1 -0
  10. package/lib/commonjs/Keyboard/KeypadButton.js +66 -0
  11. package/lib/commonjs/Keyboard/KeypadButton.js.map +1 -0
  12. package/lib/commonjs/Keyboard/KeypadButtonContainer.js +65 -0
  13. package/lib/commonjs/Keyboard/KeypadButtonContainer.js.map +1 -0
  14. package/lib/commonjs/Keyboard/KeypadRow.js +34 -0
  15. package/lib/commonjs/Keyboard/KeypadRow.js.map +1 -0
  16. package/lib/commonjs/PhoneCountryInput/PhoneCountryInput.js +86 -0
  17. package/lib/commonjs/PhoneCountryInput/PhoneCountryInput.js.map +1 -0
  18. package/lib/commonjs/PhoneNumberField.js +36 -0
  19. package/lib/commonjs/PhoneNumberField.js.map +1 -0
  20. package/lib/commonjs/Styling/Colors.js +197 -0
  21. package/lib/commonjs/Styling/Colors.js.map +1 -0
  22. package/lib/commonjs/Styling/Sizing.js +111 -0
  23. package/lib/commonjs/Styling/Sizing.js.map +1 -0
  24. package/lib/commonjs/consts/KEYBOARD_LAYOUT.js +45 -0
  25. package/lib/commonjs/consts/KEYBOARD_LAYOUT.js.map +1 -0
  26. package/lib/commonjs/consts/regions.js +1503 -0
  27. package/lib/commonjs/consts/regions.js.map +1 -0
  28. package/lib/commonjs/enum/CountryIds.js +264 -0
  29. package/lib/commonjs/enum/CountryIds.js.map +1 -0
  30. package/lib/commonjs/hooks/UsePhoneFieldState.js +237 -0
  31. package/lib/commonjs/hooks/UsePhoneFieldState.js.map +1 -0
  32. package/lib/commonjs/index.js +56 -0
  33. package/lib/commonjs/index.js.map +1 -0
  34. package/lib/commonjs/package.json +1 -0
  35. package/lib/commonjs/utils/characterDeletion.js +20 -0
  36. package/lib/commonjs/utils/characterDeletion.js.map +1 -0
  37. package/lib/commonjs/utils/characterInsert.js +20 -0
  38. package/lib/commonjs/utils/characterInsert.js.map +1 -0
  39. package/lib/commonjs/utils/fromMaskedNumberToUnmaskedSelection.js +14 -0
  40. package/lib/commonjs/utils/fromMaskedNumberToUnmaskedSelection.js.map +1 -0
  41. package/lib/commonjs/utils/fromUnmaskedToMaskedPosition.js +20 -0
  42. package/lib/commonjs/utils/fromUnmaskedToMaskedPosition.js.map +1 -0
  43. package/lib/commonjs/utils/generateCountryCodeList.js +23 -0
  44. package/lib/commonjs/utils/generateCountryCodeList.js.map +1 -0
  45. package/lib/commonjs/utils/getDefaultRegion.js +33 -0
  46. package/lib/commonjs/utils/getDefaultRegion.js.map +1 -0
  47. package/lib/commonjs/utils/maskToPhoneNumber.js +23 -0
  48. package/lib/commonjs/utils/maskToPhoneNumber.js.map +1 -0
  49. package/lib/commonjs/utils/matchCountryCode.js +27 -0
  50. package/lib/commonjs/utils/matchCountryCode.js.map +1 -0
  51. package/lib/module/CountrySelector/CountrySelector.js +70 -0
  52. package/lib/module/CountrySelector/CountrySelector.js.map +1 -0
  53. package/lib/module/CountrySelector/CountrySelectorModal.js +262 -0
  54. package/lib/module/CountrySelector/CountrySelectorModal.js.map +1 -0
  55. package/lib/module/Keyboard/Keyboard.js +310 -0
  56. package/lib/module/Keyboard/Keyboard.js.map +1 -0
  57. package/lib/module/Keyboard/KeyboardToolbar.js +65 -0
  58. package/lib/module/Keyboard/KeyboardToolbar.js.map +1 -0
  59. package/lib/module/Keyboard/KeypadButton.js +61 -0
  60. package/lib/module/Keyboard/KeypadButton.js.map +1 -0
  61. package/lib/module/Keyboard/KeypadButtonContainer.js +59 -0
  62. package/lib/module/Keyboard/KeypadButtonContainer.js.map +1 -0
  63. package/lib/module/Keyboard/KeypadRow.js +30 -0
  64. package/lib/module/Keyboard/KeypadRow.js.map +1 -0
  65. package/lib/module/PhoneCountryInput/PhoneCountryInput.js +80 -0
  66. package/lib/module/PhoneCountryInput/PhoneCountryInput.js.map +1 -0
  67. package/lib/module/PhoneNumberField.js +31 -0
  68. package/lib/module/PhoneNumberField.js.map +1 -0
  69. package/lib/module/Styling/Colors.js +193 -0
  70. package/lib/module/Styling/Colors.js.map +1 -0
  71. package/lib/module/Styling/Sizing.js +107 -0
  72. package/lib/module/Styling/Sizing.js.map +1 -0
  73. package/lib/module/consts/KEYBOARD_LAYOUT.js +41 -0
  74. package/lib/module/consts/KEYBOARD_LAYOUT.js.map +1 -0
  75. package/lib/module/consts/regions.js +1498 -0
  76. package/lib/module/consts/regions.js.map +1 -0
  77. package/lib/module/enum/CountryIds.js +260 -0
  78. package/lib/module/enum/CountryIds.js.map +1 -0
  79. package/lib/module/hooks/UsePhoneFieldState.js +232 -0
  80. package/lib/module/hooks/UsePhoneFieldState.js.map +1 -0
  81. package/lib/module/index.js +16 -0
  82. package/lib/module/index.js.map +1 -0
  83. package/lib/module/package.json +1 -0
  84. package/lib/module/utils/characterDeletion.js +16 -0
  85. package/lib/module/utils/characterDeletion.js.map +1 -0
  86. package/lib/module/utils/characterInsert.js +16 -0
  87. package/lib/module/utils/characterInsert.js.map +1 -0
  88. package/lib/module/utils/fromMaskedNumberToUnmaskedSelection.js +10 -0
  89. package/lib/module/utils/fromMaskedNumberToUnmaskedSelection.js.map +1 -0
  90. package/lib/module/utils/fromUnmaskedToMaskedPosition.js +16 -0
  91. package/lib/module/utils/fromUnmaskedToMaskedPosition.js.map +1 -0
  92. package/lib/module/utils/generateCountryCodeList.js +19 -0
  93. package/lib/module/utils/generateCountryCodeList.js.map +1 -0
  94. package/lib/module/utils/getDefaultRegion.js +28 -0
  95. package/lib/module/utils/getDefaultRegion.js.map +1 -0
  96. package/lib/module/utils/maskToPhoneNumber.js +19 -0
  97. package/lib/module/utils/maskToPhoneNumber.js.map +1 -0
  98. package/lib/module/utils/matchCountryCode.js +23 -0
  99. package/lib/module/utils/matchCountryCode.js.map +1 -0
  100. package/lib/typescript/CountrySelector/CountrySelector.d.ts +19 -0
  101. package/lib/typescript/CountrySelector/CountrySelector.d.ts.map +1 -0
  102. package/lib/typescript/CountrySelector/CountrySelectorModal.d.ts +12 -0
  103. package/lib/typescript/CountrySelector/CountrySelectorModal.d.ts.map +1 -0
  104. package/lib/typescript/Keyboard/Keyboard.d.ts +25 -0
  105. package/lib/typescript/Keyboard/Keyboard.d.ts.map +1 -0
  106. package/lib/typescript/Keyboard/KeyboardToolbar.d.ts +9 -0
  107. package/lib/typescript/Keyboard/KeyboardToolbar.d.ts.map +1 -0
  108. package/lib/typescript/Keyboard/KeypadButton.d.ts +10 -0
  109. package/lib/typescript/Keyboard/KeypadButton.d.ts.map +1 -0
  110. package/lib/typescript/Keyboard/KeypadButtonContainer.d.ts +12 -0
  111. package/lib/typescript/Keyboard/KeypadButtonContainer.d.ts.map +1 -0
  112. package/lib/typescript/Keyboard/KeypadRow.d.ts +9 -0
  113. package/lib/typescript/Keyboard/KeypadRow.d.ts.map +1 -0
  114. package/lib/typescript/PhoneCountryInput/PhoneCountryInput.d.ts +16 -0
  115. package/lib/typescript/PhoneCountryInput/PhoneCountryInput.d.ts.map +1 -0
  116. package/lib/typescript/PhoneNumberField.d.ts +17 -0
  117. package/lib/typescript/PhoneNumberField.d.ts.map +1 -0
  118. package/lib/typescript/Styling/Colors.d.ts +189 -0
  119. package/lib/typescript/Styling/Colors.d.ts.map +1 -0
  120. package/lib/typescript/Styling/Sizing.d.ts +214 -0
  121. package/lib/typescript/Styling/Sizing.d.ts.map +1 -0
  122. package/lib/typescript/consts/KEYBOARD_LAYOUT.d.ts +18 -0
  123. package/lib/typescript/consts/KEYBOARD_LAYOUT.d.ts.map +1 -0
  124. package/lib/typescript/consts/regions.d.ts +10 -0
  125. package/lib/typescript/consts/regions.d.ts.map +1 -0
  126. package/lib/typescript/enum/CountryIds.d.ts +249 -0
  127. package/lib/typescript/enum/CountryIds.d.ts.map +1 -0
  128. package/lib/typescript/hooks/UsePhoneFieldState.d.ts +34 -0
  129. package/lib/typescript/hooks/UsePhoneFieldState.d.ts.map +1 -0
  130. package/lib/typescript/index.d.ts +15 -0
  131. package/lib/typescript/index.d.ts.map +1 -0
  132. package/lib/typescript/utils/characterDeletion.d.ts +3 -0
  133. package/lib/typescript/utils/characterDeletion.d.ts.map +1 -0
  134. package/lib/typescript/utils/characterInsert.d.ts +3 -0
  135. package/lib/typescript/utils/characterInsert.d.ts.map +1 -0
  136. package/lib/typescript/utils/fromMaskedNumberToUnmaskedSelection.d.ts +2 -0
  137. package/lib/typescript/utils/fromMaskedNumberToUnmaskedSelection.d.ts.map +1 -0
  138. package/lib/typescript/utils/fromUnmaskedToMaskedPosition.d.ts +2 -0
  139. package/lib/typescript/utils/fromUnmaskedToMaskedPosition.d.ts.map +1 -0
  140. package/lib/typescript/utils/generateCountryCodeList.d.ts +3 -0
  141. package/lib/typescript/utils/generateCountryCodeList.d.ts.map +1 -0
  142. package/lib/typescript/utils/getDefaultRegion.d.ts +4 -0
  143. package/lib/typescript/utils/getDefaultRegion.d.ts.map +1 -0
  144. package/lib/typescript/utils/maskToPhoneNumber.d.ts +2 -0
  145. package/lib/typescript/utils/maskToPhoneNumber.d.ts.map +1 -0
  146. package/lib/typescript/utils/matchCountryCode.d.ts +3 -0
  147. package/lib/typescript/utils/matchCountryCode.d.ts.map +1 -0
  148. package/package.json +92 -0
  149. package/src/CountrySelector/CountrySelector.tsx +77 -0
  150. package/src/CountrySelector/CountrySelectorModal.tsx +280 -0
  151. package/src/Keyboard/Keyboard.tsx +322 -0
  152. package/src/Keyboard/KeyboardToolbar.tsx +53 -0
  153. package/src/Keyboard/KeypadButton.tsx +58 -0
  154. package/src/Keyboard/KeypadButtonContainer.tsx +67 -0
  155. package/src/Keyboard/KeypadRow.tsx +29 -0
  156. package/src/PhoneCountryInput/PhoneCountryInput.tsx +98 -0
  157. package/src/PhoneNumberField.tsx +46 -0
  158. package/src/Styling/Colors.ts +206 -0
  159. package/src/Styling/Sizing.ts +110 -0
  160. package/src/consts/KEYBOARD_LAYOUT.ts +34 -0
  161. package/src/consts/regions.ts +268 -0
  162. package/src/enum/CountryIds.ts +256 -0
  163. package/src/hooks/UsePhoneFieldState.tsx +268 -0
  164. package/src/index.ts +27 -0
  165. package/src/utils/characterDeletion.ts +16 -0
  166. package/src/utils/characterInsert.ts +20 -0
  167. package/src/utils/fromMaskedNumberToUnmaskedSelection.ts +10 -0
  168. package/src/utils/fromUnmaskedToMaskedPosition.ts +13 -0
  169. package/src/utils/generateCountryCodeList.ts +22 -0
  170. package/src/utils/getDefaultRegion.ts +30 -0
  171. package/src/utils/maskToPhoneNumber.ts +30 -0
  172. 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
+ });