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/README.md ADDED
@@ -0,0 +1,477 @@
1
+ # react-native-phone-country-input
2
+
3
+ ![shows changing the phone number country code](./readme_assets/fast_changing_country_code.gif)
4
+
5
+ An international phone number input for React Native with country selection, per-country number masking, and a custom keyboard that avoids the native soft keyboard. Ideal for quick changing country codes, entering phone numbers or using bigger devices like tablets that do not have native support for phone number keyboards.
6
+
7
+ ## Features
8
+
9
+ - 270+ countries with flags, calling codes, and phone masks
10
+ - Custom keyboard — no system keyboard flicker or layout shift. Great for iPad or Android tablets.
11
+ - Automatic country detection from the device locale on mount
12
+ - Per-country number formatting (parentheses, dashes, spaces)
13
+ - Country selector modal with search and recently-used countries
14
+ - Copy / paste support with built-in feedback modals (customizable)
15
+ - Cursor position tracking inside masked input
16
+ - Filter countries via allow-list (whitelist) or deny-list (blacklist)
17
+ - Fully customizable: swap in your own `TextInput`, `Pressable`, `Modal`, or feedback modals
18
+ - Full TypeScript support
19
+
20
+ Tested on:
21
+ - Xiaomi Redmi Pad 2 9.7 (Android Tablet)
22
+ - Galaxy A06 5G (Android Phone)
23
+ - iPhone 16 Plus
24
+ - iPad Pro 12.9"
25
+
26
+
27
+
28
+ ### Country code modal (with search and history)
29
+ ![Using the Country code modal](./readme_assets/change_phone_number_fields.gif)
30
+
31
+ ### On Paste
32
+ ![What happens on paste](./readme_assets/onPaste.gif)
33
+
34
+ ### Custom Keyboard
35
+ ![Showing Keyboard](./readme_assets/keyboard_showcase.gif)
36
+
37
+ #### Keyboard on Android Tablet
38
+ ![](./readme_assets/android-tablet-l.gif)
39
+ ![](./readme_assets/android-tablet-p.gif)
40
+
41
+ #### Keyboard on iPad
42
+ ![](./readme_assets/ipad-l.gif)
43
+ ![](./readme_assets/ipad-p.gif)
44
+
45
+ ## Peer dependencies
46
+
47
+ Install these if they are not already in your project:
48
+
49
+ ```sh
50
+ npm install \
51
+ react-native-reanimated \
52
+ react-native-gesture-handler \
53
+ react-native-teleport \
54
+ expo-localization \
55
+ @react-native-async-storage/async-storage \
56
+ @react-native-clipboard/clipboard
57
+ ```
58
+
59
+ Follow each library's setup guide for any native configuration required.
60
+
61
+ ## Installation
62
+
63
+ ```sh
64
+ npm install react-native-phone-country-input
65
+ ```
66
+
67
+ ## Setup
68
+
69
+ Wrap your app (or the screen that uses this component) in `PortalProvider` and render a `PortalHost`. The custom keyboard and feedback modals render through a portal to escape z-index stacking.
70
+
71
+ ```tsx
72
+ import { PortalProvider, PortalHost } from 'react-native-teleport';
73
+ import { StyleSheet } from 'react-native';
74
+
75
+ export default function App() {
76
+ return (
77
+ <PortalProvider>
78
+ {/* your app content */}
79
+ <PortalHost style={StyleSheet.absoluteFillObject} name="ipad-keyboard" />
80
+ </PortalProvider>
81
+ );
82
+ }
83
+ ```
84
+
85
+ ## Quick start
86
+ `PhoneCountryInput` is the ready-to-use component. Drop it in and handle the result via `onOutcomeChange`.
87
+
88
+ > **See it in action:** [`App.tsx`](./App.tsx) in this repo is a working demo that uses `PhoneCountryInput` with custom-styled inputs, a country deny-list, and outcome display — a good starting point to copy from.
89
+
90
+ ```tsx
91
+ import { PhoneCountryInput, PhoneFieldOutcome } from 'react-native-phone-country-input';
92
+
93
+ export default function MyScreen() {
94
+ const handleChange = (outcome?: PhoneFieldOutcome) => {
95
+ if (outcome?.isValid) {
96
+ console.log('Phone:', outcome.phoneNumber); // digits only, no formatting
97
+ console.log('Country:', outcome.countryDetails?.name);
98
+ }
99
+ };
100
+
101
+ return <PhoneCountryInput onOutcomeChange={handleChange} />;
102
+ }
103
+ ```
104
+
105
+ ## `PhoneCountryInput` props
106
+
107
+ Extends all `TextInput` props plus:
108
+
109
+ | Prop | Type | Required | Description |
110
+ |------|------|----------|-------------|
111
+ | `onOutcomeChange` | `(outcome?: PhoneFieldOutcome) => void` | Yes | Called whenever the phone number or country changes |
112
+ | `allowedCountryCodes` | `CountryId[]` | No | Show only these countries in the selector |
113
+ | `disallowedCountryCodes` | `CountryId[]` | No | Hide these countries from the selector |
114
+ | `underlineInput` | `React.ComponentType` | No | Replace the default `TextInput` with your own styled component |
115
+ | `underlineButton` | `React.ComponentType` | No | Replace the country selector button with your own styled `Pressable` |
116
+ | `underlineModal` | `React.ComponentType` | No | Replace the country selector modal entirely |
117
+ | `underlinePasteErrorModal` | `React.ComponentType<PasteErrorModalProps>` | No | Replace the built-in "cannot paste" modal |
118
+ | `underlineCopySuccessModal` | `React.ComponentType<CopySuccessModalProps>` | No | Replace the built-in "copied" confirmation modal |
119
+ | `style` | `ViewStyle` | No | Style applied to the outer row container |
120
+
121
+ ### Custom styling example
122
+
123
+ ```tsx
124
+ import { TextInput, Pressable } from 'react-native';
125
+ import { PhoneCountryInput } from 'react-native-phone-country-input';
126
+
127
+ function StyledInput(props: React.ComponentProps<typeof TextInput>) {
128
+ return (
129
+ <TextInput
130
+ {...props}
131
+ style={[{ flex: 1, borderWidth: 1, borderColor: '#ccc', padding: 10, borderRadius: 8 }, props.style]}
132
+ />
133
+ );
134
+ }
135
+
136
+ function StyledButton(props: React.ComponentProps<typeof Pressable>) {
137
+ return (
138
+ <Pressable {...props} style={{ padding: 10, borderWidth: 1, borderColor: '#ccc', borderRadius: 8 }}>
139
+ {props.children}
140
+ </Pressable>
141
+ );
142
+ }
143
+
144
+ export default function MyScreen() {
145
+ return (
146
+ <PhoneCountryInput
147
+ underlineInput={StyledInput}
148
+ underlineButton={StyledButton}
149
+ onOutcomeChange={(outcome) => console.log(outcome)}
150
+ />
151
+ );
152
+ }
153
+ ```
154
+
155
+ ### Country filtering example
156
+
157
+ ```tsx
158
+ import { PhoneCountryInput, CountryId } from 'react-native-phone-country-input';
159
+
160
+ const ALLOWED = [CountryId.UNITED_STATES, CountryId.CANADA, CountryId.MEXICO];
161
+
162
+ <PhoneCountryInput
163
+ allowedCountryCodes={ALLOWED}
164
+ onOutcomeChange={(outcome) => console.log(outcome)}
165
+ />
166
+ ```
167
+
168
+ ---
169
+
170
+ ## Copy and paste feedback modals
171
+
172
+ When the user taps **Copy**, the keyboard shows a "Copied" confirmation modal. When the user taps **Paste** and the clipboard does not contain a recognisable phone number, a "Cannot Paste" error modal appears. Both modals have built-in defaults and can be replaced with your own UI.
173
+
174
+ ### Default behaviour
175
+
176
+ No configuration needed — the defaults work out of the box:
177
+
178
+
179
+ #### Copy success
180
+ a modal with the title "Copied" and the message "Phone number copied to clipboard."
181
+
182
+ ![](./readme_assets/copy_modal.png)
183
+
184
+ #### Paste error
185
+ a modal with the title "Cannot Paste" and the message "Clipboard does not contain a valid phone number."
186
+
187
+ ![Paste error](./readme_assets/cannot_paste.png)
188
+
189
+
190
+
191
+ ### Replacing the paste-error modal
192
+ Pass a component that accepts `PasteErrorModalProps` to `underlinePasteErrorModal`:
193
+
194
+ ```tsx
195
+ import { Modal, View, Text, Pressable } from 'react-native';
196
+ import { PhoneCountryInput, PasteErrorModalProps } from 'react-native-phone-country-input';
197
+
198
+ function MyPasteErrorModal({ isPasteErrorVisible, dismissPasteError }: PasteErrorModalProps) {
199
+ return (
200
+ <Modal visible={isPasteErrorVisible} transparent animationType="fade" onRequestClose={dismissPasteError}>
201
+ <Pressable style={{ flex: 1, backgroundColor: 'rgba(0,0,0,0.5)', justifyContent: 'center', alignItems: 'center' }} onPress={dismissPasteError}>
202
+ <View style={{ backgroundColor: '#fff', borderRadius: 12, padding: 24, width: 280 }}>
203
+ <Text style={{ fontSize: 16, fontWeight: '600', marginBottom: 8 }}>Invalid number</Text>
204
+ <Text style={{ color: '#666' }}>Paste a phone number with a country code (e.g. +1 555 123 4567).</Text>
205
+ <Pressable onPress={dismissPasteError} style={{ marginTop: 16, alignSelf: 'flex-end' }}>
206
+ <Text style={{ color: '#007AFF', fontWeight: '600' }}>Got it</Text>
207
+ </Pressable>
208
+ </View>
209
+ </Pressable>
210
+ </Modal>
211
+ );
212
+ }
213
+
214
+ <PhoneCountryInput
215
+ underlinePasteErrorModal={MyPasteErrorModal}
216
+ onOutcomeChange={(outcome) => console.log(outcome)}
217
+ />
218
+ ```
219
+
220
+ ### Replacing the copy-success modal
221
+
222
+ Pass a component that accepts `CopySuccessModalProps` to `underlineCopySuccessModal`:
223
+
224
+ ```tsx
225
+ import { Modal, View, Text, Pressable } from 'react-native';
226
+ import { PhoneCountryInput, CopySuccessModalProps } from 'react-native-phone-country-input';
227
+
228
+ function MyCopySuccessModal({ isCopySuccessVisible, dismissCopySuccess }: CopySuccessModalProps) {
229
+ return (
230
+ <Modal visible={isCopySuccessVisible} transparent animationType="fade" onRequestClose={dismissCopySuccess}>
231
+ <Pressable style={{ flex: 1, justifyContent: 'flex-end', padding: 16 }} onPress={dismissCopySuccess}>
232
+ <View style={{ backgroundColor: '#22c55e', borderRadius: 12, padding: 16, flexDirection: 'row', alignItems: 'center', gap: 12 }}>
233
+ <Text style={{ color: '#fff', fontSize: 15, fontWeight: '600' }}>✓ Copied to clipboard</Text>
234
+ </View>
235
+ </Pressable>
236
+ </Modal>
237
+ );
238
+ }
239
+
240
+ <PhoneCountryInput
241
+ underlineCopySuccessModal={MyCopySuccessModal}
242
+ onOutcomeChange={(outcome) => console.log(outcome)}
243
+ />
244
+ ```
245
+
246
+ ---
247
+
248
+ ## Building your own UI with individual pieces
249
+
250
+ Import the sub-components and `usePhoneFieldState` to compose a completely custom layout.
251
+
252
+ ```tsx
253
+ import {
254
+ usePhoneFieldState,
255
+ PhoneNumberField,
256
+ CountrySelector,
257
+ Keyboard,
258
+ PhoneFieldOutcome,
259
+ } from 'react-native-phone-country-input';
260
+ import { View } from 'react-native';
261
+
262
+ export default function CustomPhoneInput({ onResult }: { onResult: (o: PhoneFieldOutcome) => void }) {
263
+ const {
264
+ // State
265
+ outcome,
266
+ phoneNumber,
267
+ filteredCountryCodes,
268
+ cursorPosition,
269
+ isKeyboardOpen,
270
+ isCountrySelectorOpen,
271
+ // Handlers
272
+ onChangeText,
273
+ onChangeFlag,
274
+ onKeyPress,
275
+ onTextSelection,
276
+ onCopy,
277
+ onPaste,
278
+ openKeyboard,
279
+ closeKeyboard,
280
+ openCountrySelector,
281
+ closeCountrySelector,
282
+ } = usePhoneFieldState({
283
+ // optional: allowedCountryCodes, disallowedCountryCodes
284
+ });
285
+
286
+ return (
287
+ <>
288
+ <View style={{ flexDirection: 'row', gap: 8 }}>
289
+ <CountrySelector
290
+ value={outcome?.countryDetails ?? null}
291
+ filtedredCountryCodes={filteredCountryCodes}
292
+ onSelectCountry={onChangeFlag}
293
+ isOpen={isCountrySelectorOpen}
294
+ onOpenChange={(open) => (open ? openCountrySelector() : closeCountrySelector())}
295
+ />
296
+ <PhoneNumberField
297
+ style={{ flex: 1 }}
298
+ value={phoneNumber}
299
+ onChangeText={onChangeText}
300
+ onOpen={openKeyboard}
301
+ onTextSelection={onTextSelection}
302
+ selection={cursorPosition}
303
+ />
304
+ </View>
305
+ <Keyboard
306
+ value={phoneNumber}
307
+ isOpen={isKeyboardOpen}
308
+ onClose={closeKeyboard}
309
+ onKeyPress={onKeyPress}
310
+ onCopy={onCopy}
311
+ onPaste={onPaste}
312
+ />
313
+ </>
314
+ );
315
+ }
316
+ ```
317
+
318
+ ---
319
+
320
+ ## API reference
321
+
322
+ ### `usePhoneFieldState(params)`
323
+
324
+ The state management hook that powers `PhoneCountryInput`. Returns all state and handlers needed to wire up the sub-components independently.
325
+
326
+ **Params**
327
+
328
+ | Field | Type | Description |
329
+ |-------|------|-------------|
330
+ | `allowedCountryCodes` | `CountryId[]` | Show only these countries |
331
+ | `disallowedCountryCodes` | `CountryId[]` | Hide these countries |
332
+
333
+ **Returns**
334
+
335
+ | Field | Type | Description |
336
+ |-------|------|-------------|
337
+ | `outcome` | `PhoneFieldOutcome \| undefined` | Current validation result |
338
+ | `phoneNumber` | `string \| undefined` | Raw unmasked digits (no `+`) |
339
+ | `country` | `CountryCode \| undefined` | Currently selected country |
340
+ | `filteredCountryCodes` | `CountryCode[]` | Country list after filtering |
341
+ | `cursorPosition` | `{ start: number; end: number }` | Cursor position in the masked display string |
342
+ | `isKeyboardOpen` | `boolean` | Whether the custom keyboard is visible |
343
+ | `isCountrySelectorOpen` | `boolean` | Whether the country selector modal is open |
344
+ | `onChangeText` | `(value: string) => void` | Pass to `PhoneNumberField.onChangeText` |
345
+ | `onChangeFlag` | `(country: CountryCode) => void` | Pass to `CountrySelector.onSelectCountry` |
346
+ | `onKeyPress` | `(key: KEYPAD_KEY) => void` | Pass to `Keyboard.onKeyPress` |
347
+ | `onTextSelection` | `(e: NativeSyntheticEvent<...>) => void` | Pass to `PhoneNumberField.onTextSelection` |
348
+ | `onCopy` | `() => void` | Copies the current number to the clipboard |
349
+ | `onPaste` | `() => Promise<boolean>` | Pastes from clipboard — resolves `true` on success, `false` if the clipboard content is not a recognisable phone number |
350
+ | `onClearText` | `() => void` | Resets the field to the country code only |
351
+ | `openKeyboard` | `() => void` | — |
352
+ | `closeKeyboard` | `() => void` | — |
353
+ | `openCountrySelector` | `() => void` | — |
354
+ | `closeCountrySelector` | `() => void` | — |
355
+
356
+ ---
357
+
358
+ ### `PhoneNumberField`
359
+
360
+ The masked text input. Renders a `TextInput` (or your custom component) prefixed with `+`. Always passes `showSoftInputOnFocus={false}` — use `Keyboard` for input.
361
+
362
+ | Prop | Type | Description |
363
+ |------|------|-------------|
364
+ | `value` | `string` | Unmasked phone number (digits only, no `+`) |
365
+ | `onChangeText` | `(value: string) => void` | Text change handler |
366
+ | `onOpen` | `() => void` | Called when the field is pressed — open the keyboard here |
367
+ | `onTextSelection` | `(e) => void` | Selection change handler for cursor tracking |
368
+ | `selection` | `{ start: number; end: number }` | Controlled cursor position |
369
+ | `underlineInput` | `React.ComponentType` | Custom `TextInput` replacement |
370
+
371
+ ---
372
+
373
+ ### `CountrySelector`
374
+
375
+ The country flag button + selector modal. Supports both controlled (`isOpen`) and uncontrolled usage.
376
+
377
+ | Prop | Type | Description |
378
+ |------|------|-------------|
379
+ | `value` | `CountryCode \| null` | Currently selected country |
380
+ | `onSelectCountry` | `(country: CountryCode) => void` | Called when the user picks a country |
381
+ | `filtedredCountryCodes` | `CountryCode[]` | The country list to display |
382
+ | `isOpen` | `boolean` | Controlled open state |
383
+ | `onOpenChange` | `(open: boolean) => void` | Called when open state should change |
384
+ | `underlineButton` | `React.ComponentType` | Custom button replacing the flag+chevron |
385
+ | `underlineModal` | `React.ComponentType` | Custom modal replacing the built-in sheet |
386
+
387
+ ---
388
+
389
+ ### `Keyboard`
390
+
391
+ The custom phone keypad. Renders via a `PortalHost` and animates in/out with `react-native-reanimated`. Manages copy/paste feedback modals internally.
392
+
393
+ | Prop | Type | Description |
394
+ |------|------|-------------|
395
+ | `isOpen` | `boolean` | Whether the keyboard is visible |
396
+ | `onClose` | `() => void` | Called when the Done button is pressed |
397
+ | `onKeyPress` | `(key: KEYPAD_KEY) => void` | Called for every key tap |
398
+ | `value` | `string` | Current phone number string (shown in the toolbar) |
399
+ | `onCopy` | `() => void` | Called when the Copy button is pressed |
400
+ | `onPaste` | `() => Promise<boolean>` | Called when the Paste button is pressed — return `false` to trigger the paste-error modal |
401
+ | `underlinePasteErrorModal` | `React.ComponentType<PasteErrorModalProps>` | Replace the built-in "cannot paste" modal |
402
+ | `underlineCopySuccessModal` | `React.ComponentType<CopySuccessModalProps>` | Replace the built-in "copied" confirmation modal |
403
+
404
+ ---
405
+
406
+ ## Types
407
+
408
+ ### `PhoneFieldOutcome`
409
+
410
+ ```ts
411
+ interface PhoneFieldOutcome {
412
+ phoneNumber: string; // digits only, no formatting, no leading '+'
413
+ isValid: boolean; // true when digit count matches the country's mask
414
+ correctLength: number; // total character length when fully formatted (e.g. "+1 (555) 123-4567".length)
415
+ countryDetails: CountryCode | null;
416
+ }
417
+ ```
418
+
419
+ ### `PasteErrorModalProps`
420
+
421
+ Received by a custom paste-error modal component.
422
+
423
+ ```ts
424
+ interface PasteErrorModalProps extends React.ComponentProps<typeof Modal> {
425
+ isPasteErrorVisible: boolean; // whether the modal should be visible
426
+ dismissPasteError: () => void; // call this to close the modal
427
+ }
428
+ ```
429
+
430
+ ### `CopySuccessModalProps`
431
+
432
+ Received by a custom copy-success modal component.
433
+
434
+ ```ts
435
+ interface CopySuccessModalProps extends React.ComponentProps<typeof Modal> {
436
+ isCopySuccessVisible: boolean; // whether the modal should be visible
437
+ dismissCopySuccess: () => void; // call this to close the modal
438
+ }
439
+ ```
440
+
441
+ ### `CountryCode`
442
+
443
+ ```ts
444
+ interface CountryCode {
445
+ id: CountryId; // e.g. CountryId.UNITED_STATES
446
+ code: string; // e.g. '+1'
447
+ flag: string; // emoji flag, e.g. '🇺🇸'
448
+ name: string; // e.g. 'United States'
449
+ mask: string; // formatting template, e.g. ' (###) ###-####'
450
+ }
451
+ ```
452
+
453
+ ### `CountryId`
454
+
455
+ An enum with an entry for every supported country, e.g. `CountryId.UNITED_STATES`, `CountryId.UNITED_KINGDOM`, `CountryId.CANADA`. Import it to build allow/deny lists.
456
+
457
+ ```ts
458
+ import { CountryId } from 'react-native-phone-country-input';
459
+ ```
460
+
461
+ ---
462
+
463
+ ## Contributing
464
+
465
+ Pull requests are welcome. To work on the library locally, clone the repo and run the Expo demo app from the project root:
466
+
467
+ ```sh
468
+ npm install
469
+ npm start # starts the Expo dev server
470
+ npm run build # compiles the library to lib/
471
+ npm test # runs the unit test suite
472
+ ```
473
+
474
+ ## Inspired by
475
+
476
+ - [react-international-phone](https://github.com/ybrusentsov/react-international-phone)
477
+ - [react-native-phone-entry](https://github.com/anday013/react-native-phone-entry)
@@ -0,0 +1,74 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.CountrySelector = CountrySelector;
7
+ var _react = require("react");
8
+ var _reactNative = require("react-native");
9
+ var _CountrySelectorModal = require("./CountrySelectorModal");
10
+ var _vectorIcons = require("@expo/vector-icons");
11
+ var _jsxRuntime = require("react/jsx-runtime");
12
+ // button for opening the country selector modal
13
+
14
+ //
15
+
16
+ function CountrySelectorButton(props) {
17
+ const Button = (0, _react.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 _reactNative.Pressable;
24
+ }, [props.underlineButton]);
25
+ return (
26
+ /*#__PURE__*/
27
+ // This could be a simple button that, when pressed, opens the country selector modal
28
+ (0, _jsxRuntime.jsxs)(Button, {
29
+ ...props,
30
+ children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
31
+ children: props.value ? props.value.flag : '🏴‍☠️'
32
+ }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_vectorIcons.Feather, {
33
+ name: `chevron-${props.isOpen ? 'up' : 'down'}`,
34
+ size: 14,
35
+ color: "gray"
36
+ })]
37
+ })
38
+ );
39
+ }
40
+ function CountrySelector(props) {
41
+ const [internalValue, setInternalValue] = (0, _react.useState)(props.value);
42
+ const [internalIsOpen, setInternalIsOpen] = (0, _react.useState)(false);
43
+ const {
44
+ filtedredCountryCodes,
45
+ onSelectCountry,
46
+ onOpenChange
47
+ } = props;
48
+ const isControlled = props.isOpen !== undefined;
49
+ const effectiveIsOpen = isControlled ? props.isOpen ?? false : internalIsOpen;
50
+ const toggle = (0, _react.useCallback)(() => {
51
+ const next = !effectiveIsOpen;
52
+ if (!isControlled) setInternalIsOpen(next);
53
+ onOpenChange?.(next);
54
+ }, [effectiveIsOpen, isControlled, onOpenChange]);
55
+ (0, _react.useEffect)(() => {
56
+ setInternalValue(props.value);
57
+ }, [props.value]);
58
+ return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_jsxRuntime.Fragment, {
59
+ children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(CountrySelectorButton, {
60
+ ...props,
61
+ underlineButton: props.underlineButton,
62
+ value: internalValue,
63
+ isOpen: effectiveIsOpen,
64
+ onPress: toggle
65
+ }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_CountrySelectorModal.CountrySelectorModal, {
66
+ value: internalValue,
67
+ onSelectCountry: onSelectCountry ?? (() => {}),
68
+ UserCountryCodes: filtedredCountryCodes,
69
+ isOpen: effectiveIsOpen,
70
+ toggleModalVisablity: toggle
71
+ })]
72
+ });
73
+ }
74
+ //# sourceMappingURL=CountrySelector.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"names":["_react","require","_reactNative","_CountrySelectorModal","_vectorIcons","_jsxRuntime","CountrySelectorButton","props","Button","useMemo","underlineButton","console","debug","Pressable","jsxs","children","jsx","Text","value","flag","Feather","name","isOpen","size","color","CountrySelector","internalValue","setInternalValue","useState","internalIsOpen","setInternalIsOpen","filtedredCountryCodes","onSelectCountry","onOpenChange","isControlled","undefined","effectiveIsOpen","toggle","useCallback","next","useEffect","Fragment","onPress","CountrySelectorModal","UserCountryCodes","toggleModalVisablity"],"sourceRoot":"../../../src","sources":["CountrySelector/CountrySelector.tsx"],"mappings":"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","ignoreList":[]}