@kaisarsofi/react-native-phone-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 (91) hide show
  1. package/LICENSE +20 -0
  2. package/README.md +467 -0
  3. package/lib/commonjs/CountryPicker.js +669 -0
  4. package/lib/commonjs/CountryPicker.js.map +1 -0
  5. package/lib/commonjs/Flag.js +23 -0
  6. package/lib/commonjs/Flag.js.map +1 -0
  7. package/lib/commonjs/PhoneInput.js +252 -0
  8. package/lib/commonjs/PhoneInput.js.map +1 -0
  9. package/lib/commonjs/countries.js +75 -0
  10. package/lib/commonjs/countries.js.map +1 -0
  11. package/lib/commonjs/data/countries.json +1472 -0
  12. package/lib/commonjs/index.js +71 -0
  13. package/lib/commonjs/index.js.map +1 -0
  14. package/lib/commonjs/package.json +1 -0
  15. package/lib/commonjs/safeArea.js +44 -0
  16. package/lib/commonjs/safeArea.js.map +1 -0
  17. package/lib/commonjs/theme.js +85 -0
  18. package/lib/commonjs/theme.js.map +1 -0
  19. package/lib/commonjs/types.js +6 -0
  20. package/lib/commonjs/types.js.map +1 -0
  21. package/lib/commonjs/value.js +68 -0
  22. package/lib/commonjs/value.js.map +1 -0
  23. package/lib/module/CountryPicker.js +665 -0
  24. package/lib/module/CountryPicker.js.map +1 -0
  25. package/lib/module/Flag.js +19 -0
  26. package/lib/module/Flag.js.map +1 -0
  27. package/lib/module/PhoneInput.js +248 -0
  28. package/lib/module/PhoneInput.js.map +1 -0
  29. package/lib/module/countries.js +65 -0
  30. package/lib/module/countries.js.map +1 -0
  31. package/lib/module/data/countries.json +1472 -0
  32. package/lib/module/index.js +8 -0
  33. package/lib/module/index.js.map +1 -0
  34. package/lib/module/package.json +1 -0
  35. package/lib/module/safeArea.js +39 -0
  36. package/lib/module/safeArea.js.map +1 -0
  37. package/lib/module/theme.js +81 -0
  38. package/lib/module/theme.js.map +1 -0
  39. package/lib/module/types.js +4 -0
  40. package/lib/module/types.js.map +1 -0
  41. package/lib/module/value.js +62 -0
  42. package/lib/module/value.js.map +1 -0
  43. package/lib/typescript/commonjs/package.json +1 -0
  44. package/lib/typescript/commonjs/src/CountryPicker.d.ts +37 -0
  45. package/lib/typescript/commonjs/src/CountryPicker.d.ts.map +1 -0
  46. package/lib/typescript/commonjs/src/Flag.d.ts +10 -0
  47. package/lib/typescript/commonjs/src/Flag.d.ts.map +1 -0
  48. package/lib/typescript/commonjs/src/PhoneInput.d.ts +3 -0
  49. package/lib/typescript/commonjs/src/PhoneInput.d.ts.map +1 -0
  50. package/lib/typescript/commonjs/src/countries.d.ts +28 -0
  51. package/lib/typescript/commonjs/src/countries.d.ts.map +1 -0
  52. package/lib/typescript/commonjs/src/index.d.ts +9 -0
  53. package/lib/typescript/commonjs/src/index.d.ts.map +1 -0
  54. package/lib/typescript/commonjs/src/safeArea.d.ts +11 -0
  55. package/lib/typescript/commonjs/src/safeArea.d.ts.map +1 -0
  56. package/lib/typescript/commonjs/src/theme.d.ts +53 -0
  57. package/lib/typescript/commonjs/src/theme.d.ts.map +1 -0
  58. package/lib/typescript/commonjs/src/types.d.ts +211 -0
  59. package/lib/typescript/commonjs/src/types.d.ts.map +1 -0
  60. package/lib/typescript/commonjs/src/value.d.ts +22 -0
  61. package/lib/typescript/commonjs/src/value.d.ts.map +1 -0
  62. package/lib/typescript/module/package.json +1 -0
  63. package/lib/typescript/module/src/CountryPicker.d.ts +37 -0
  64. package/lib/typescript/module/src/CountryPicker.d.ts.map +1 -0
  65. package/lib/typescript/module/src/Flag.d.ts +10 -0
  66. package/lib/typescript/module/src/Flag.d.ts.map +1 -0
  67. package/lib/typescript/module/src/PhoneInput.d.ts +3 -0
  68. package/lib/typescript/module/src/PhoneInput.d.ts.map +1 -0
  69. package/lib/typescript/module/src/countries.d.ts +28 -0
  70. package/lib/typescript/module/src/countries.d.ts.map +1 -0
  71. package/lib/typescript/module/src/index.d.ts +9 -0
  72. package/lib/typescript/module/src/index.d.ts.map +1 -0
  73. package/lib/typescript/module/src/safeArea.d.ts +11 -0
  74. package/lib/typescript/module/src/safeArea.d.ts.map +1 -0
  75. package/lib/typescript/module/src/theme.d.ts +53 -0
  76. package/lib/typescript/module/src/theme.d.ts.map +1 -0
  77. package/lib/typescript/module/src/types.d.ts +211 -0
  78. package/lib/typescript/module/src/types.d.ts.map +1 -0
  79. package/lib/typescript/module/src/value.d.ts +22 -0
  80. package/lib/typescript/module/src/value.d.ts.map +1 -0
  81. package/package.json +215 -0
  82. package/src/CountryPicker.tsx +805 -0
  83. package/src/Flag.tsx +17 -0
  84. package/src/PhoneInput.tsx +363 -0
  85. package/src/countries.ts +95 -0
  86. package/src/data/countries.json +1472 -0
  87. package/src/index.tsx +21 -0
  88. package/src/safeArea.ts +50 -0
  89. package/src/theme.ts +112 -0
  90. package/src/types.ts +229 -0
  91. package/src/value.ts +70 -0
package/src/index.tsx ADDED
@@ -0,0 +1,21 @@
1
+ export {
2
+ COUNTRIES,
3
+ getCountryByCode,
4
+ groupCountriesByLetter,
5
+ sectionLetterFor,
6
+ } from './countries';
7
+ export type { Country, CountrySection } from './countries';
8
+ export { Flag } from './Flag';
9
+ export { PhoneInput } from './PhoneInput';
10
+ export { CountryPicker } from './CountryPicker';
11
+ export { LIGHT_PALETTE, DARK_PALETTE, usePhoneInputPalette } from './theme';
12
+ export type { PhoneInputPalette, PhoneInputColorScheme } from './theme';
13
+ export type {
14
+ PhoneInputProps,
15
+ PhoneInputRef,
16
+ PhoneInputValue,
17
+ PhoneInputTheme,
18
+ CountryPickerRenderItemInfo,
19
+ CountryDisplayMode,
20
+ CountryPickerStyles,
21
+ } from './types';
@@ -0,0 +1,50 @@
1
+ import { createContext, useContext, type Context } from 'react';
2
+
3
+ export interface EdgeInsets {
4
+ top: number;
5
+ bottom: number;
6
+ left: number;
7
+ right: number;
8
+ }
9
+
10
+ type SafeAreaModule = {
11
+ SafeAreaInsetsContext?: Context<EdgeInsets | null>;
12
+ };
13
+
14
+ /**
15
+ * `react-native-safe-area-context` is an *optional* peer: this library ships no
16
+ * native modules, so it is never installed on our account. The guarded require
17
+ * is what makes that safe — Metro registers a `require` inside a try/catch as
18
+ * an optional dependency and emits a runtime throw instead of failing the
19
+ * build (`allowOptionalDependencies`, enabled by default in both
20
+ * `@expo/metro-config` and `@react-native/metro-config`). The catch also covers
21
+ * bundlers that hand the ESM build a scope with no `require` at all.
22
+ */
23
+ let optionalModule: SafeAreaModule | null = null;
24
+ try {
25
+ optionalModule = require('react-native-safe-area-context') as SafeAreaModule;
26
+ } catch {
27
+ optionalModule = null;
28
+ }
29
+
30
+ const FallbackContext = createContext<EdgeInsets | null>(null);
31
+
32
+ /**
33
+ * Deliberately the *context* rather than `useSafeAreaInsets()`: that hook
34
+ * throws when no `SafeAreaProvider` is mounted above it, and plenty of apps
35
+ * carry the package transitively (React Navigation depends on it) without a
36
+ * provider in scope. Reading the context degrades to `null` instead.
37
+ *
38
+ * Resolved once at module scope so the identity is stable, which keeps the
39
+ * `useContext` call below an unconditional, rules-of-hooks-safe one.
40
+ */
41
+ const InsetsContext: Context<EdgeInsets | null> =
42
+ optionalModule?.SafeAreaInsetsContext ?? FallbackContext;
43
+
44
+ /** The ambient safe-area insets, or null when unavailable. */
45
+ export function useOptionalSafeAreaInsets(): EdgeInsets | null {
46
+ return useContext(InsetsContext);
47
+ }
48
+
49
+ /** Whether the optional peer resolved. Exported for diagnostics and tests. */
50
+ export const hasSafeAreaContext = optionalModule?.SafeAreaInsetsContext != null;
package/src/theme.ts ADDED
@@ -0,0 +1,112 @@
1
+ import { useColorScheme } from 'react-native';
2
+ import { useMemo } from 'react';
3
+
4
+ /** Which palette to render with. "system" follows the OS appearance. */
5
+ export type PhoneInputColorScheme = 'light' | 'dark' | 'system';
6
+
7
+ /**
8
+ * Every color the input and the country picker draw with. Both palettes are
9
+ * complete, so dark mode needs no configuration — pass nothing and the
10
+ * component follows the OS. Override individual tokens via the `palette`
11
+ * prop when you want the component to match your own design system.
12
+ */
13
+ /**
14
+ * Note the split with `theme`: `theme` styles the text field alone (its fill,
15
+ * its borders, its text), while this palette covers the field's neutral
16
+ * defaults *and* the whole picker sheet. Keeping them separate is what lets a
17
+ * field sitting on a colored background go fully custom without erasing the
18
+ * picker's separators or A-Z index along with it.
19
+ */
20
+ export interface PhoneInputPalette {
21
+ /** Picker page background */
22
+ background: string;
23
+ /** The text field's own fill */
24
+ inputBackground: string;
25
+ /** Raised surfaces: search box, section headers, close button, selected row */
26
+ surface: string;
27
+ /** Pressed state for those surfaces */
28
+ surfacePressed: string;
29
+ /** Hairline separators, the input border, the country/number divider */
30
+ border: string;
31
+ /** Primary text: the number, country names, the picker title */
32
+ text: string;
33
+ /** Secondary text: dial codes, section letters, the empty state */
34
+ textMuted: string;
35
+ placeholder: string;
36
+ /** Focus ring and the A–Z index */
37
+ accent: string;
38
+ /** Text drawn on top of `accent` */
39
+ accentContrast: string;
40
+ /** Border shown when the typed number is invalid */
41
+ danger: string;
42
+ /** A–Z index letter with no countries under it */
43
+ disabled: string;
44
+ /** The sheet's drag handle */
45
+ handle: string;
46
+ /** The letter bubble shown while scrubbing the A–Z index */
47
+ bubble: string;
48
+ bubbleText: string;
49
+ }
50
+
51
+ export const LIGHT_PALETTE: PhoneInputPalette = {
52
+ background: '#FFFFFF',
53
+ inputBackground: '#FFFFFF',
54
+ surface: '#F2F2F7',
55
+ surfacePressed: '#E5E5EA',
56
+ border: '#D1D1D6',
57
+ text: '#1C1C1E',
58
+ textMuted: '#8E8E93',
59
+ placeholder: '#8E8E93',
60
+ accent: '#007AFF',
61
+ accentContrast: '#FFFFFF',
62
+ danger: '#FF3B30',
63
+ disabled: '#C7C7CC',
64
+ handle: '#D1D1D6',
65
+ bubble: 'rgba(60,60,67,0.85)',
66
+ bubbleText: '#FFFFFF',
67
+ };
68
+
69
+ export const DARK_PALETTE: PhoneInputPalette = {
70
+ background: '#1C1C1E',
71
+ inputBackground: '#1C1C1E',
72
+ surface: '#2C2C2E',
73
+ surfacePressed: '#3A3A3C',
74
+ border: '#38383A',
75
+ text: '#FFFFFF',
76
+ textMuted: '#98989F',
77
+ placeholder: '#8E8E93',
78
+ accent: '#0A84FF',
79
+ accentContrast: '#FFFFFF',
80
+ danger: '#FF453A',
81
+ disabled: '#48484A',
82
+ handle: '#48484A',
83
+ bubble: 'rgba(120,120,128,0.9)',
84
+ bubbleText: '#FFFFFF',
85
+ };
86
+
87
+ /**
88
+ * Resolves the palette for the current appearance. `colorScheme` forces one;
89
+ * "system" (the default) follows the OS and re-renders when it changes.
90
+ */
91
+ export function usePhoneInputPalette(
92
+ colorScheme: PhoneInputColorScheme = 'system',
93
+ overrides?: Partial<PhoneInputPalette>
94
+ ): PhoneInputPalette {
95
+ const systemScheme = useColorScheme();
96
+ const resolved =
97
+ colorScheme === 'system' ? (systemScheme ?? 'light') : colorScheme;
98
+
99
+ // Keyed on the overrides' contents rather than their identity, because
100
+ // callers naturally write `palette={{ ... }}` inline. A new object every
101
+ // render would give the palette a new identity every render, and the picker
102
+ // memoizes ~20 style objects and its whole row list on that identity —
103
+ // serializing a handful of color strings is far cheaper than losing those.
104
+ const overridesKey = overrides ? JSON.stringify(overrides) : '';
105
+
106
+ return useMemo(() => {
107
+ const base = resolved === 'dark' ? DARK_PALETTE : LIGHT_PALETTE;
108
+ return { ...base, ...overrides };
109
+ // `overrides` is intentionally tracked through `overridesKey`.
110
+ // eslint-disable-next-line react-hooks/exhaustive-deps
111
+ }, [resolved, overridesKey]);
112
+ }
package/src/types.ts ADDED
@@ -0,0 +1,229 @@
1
+ import type { CountryCode } from 'libphonenumber-js';
2
+ import type {
3
+ StyleProp,
4
+ TextInputProps,
5
+ TextStyle,
6
+ ViewStyle,
7
+ } from 'react-native';
8
+ import type { Country } from './countries';
9
+ import type { PhoneInputColorScheme, PhoneInputPalette } from './theme';
10
+
11
+ export type { Country };
12
+ export type { PhoneInputColorScheme, PhoneInputPalette };
13
+
14
+ export interface PhoneInputValue {
15
+ /** Raw digits the user typed, national significant number, no dial code (e.g. "4155552671") */
16
+ nationalNumber: string;
17
+ /** ISO 3166-1 alpha-2 country selected (e.g. "US") */
18
+ country: CountryCode;
19
+ /**
20
+ * Calling code without "+" (e.g. "1"). For the 23 NANP territories that
21
+ * share "+1" (Jamaica, Bermuda, etc.) this includes the distinguishing area
22
+ * code (e.g. "1876") so the picker can tell them apart — use `e164`, not
23
+ * `dialCode` + `nationalNumber`, to reconstruct the full number.
24
+ */
25
+ dialCode: string;
26
+ /** Full E.164 formatted number if valid, e.g. "+14155552671" */
27
+ e164: string | null;
28
+ /**
29
+ * The number as an international string at every keystroke, valid or not
30
+ * (e.g. "+1415555" while typing). Equals `e164` once the number is valid,
31
+ * and "" while the field is empty. Built through libphonenumber-js, so
32
+ * unlike `dialCode` + `nationalNumber` it is always correct for the NANP
33
+ * territories that share "+1" — use this, not string concatenation, when
34
+ * you need a value to store on every change.
35
+ */
36
+ international: string;
37
+ /** Whether libphonenumber-js considers the number valid for the selected country */
38
+ isValid: boolean;
39
+ }
40
+
41
+ export interface PhoneInputRef {
42
+ focus: () => void;
43
+ blur: () => void;
44
+ isValid: () => boolean;
45
+ getValue: () => PhoneInputValue;
46
+ }
47
+
48
+ export interface CountryPickerRenderItemInfo {
49
+ item: Country;
50
+ isSelected: boolean;
51
+ onPress: () => void;
52
+ }
53
+
54
+ export interface PhoneInputTheme {
55
+ backgroundColor?: string;
56
+ borderColor?: string;
57
+ focusedBorderColor?: string;
58
+ errorBorderColor?: string;
59
+ textColor?: string;
60
+ placeholderColor?: string;
61
+ dialCodeColor?: string;
62
+ /** The rule between the country control and the number. Defaults to the
63
+ * palette's border color — set it explicitly when the field itself is
64
+ * borderless, so the divider does not disappear with the border. */
65
+ dividerColor?: string;
66
+ borderRadius?: number;
67
+ fontSize?: number;
68
+ }
69
+
70
+ /** What the country control (left of the input) shows. */
71
+ export type CountryDisplayMode = 'flag' | 'code' | 'both';
72
+
73
+ /**
74
+ * Per-element style overrides for the country picker modal, so every part of it
75
+ * can be fully re-skinned — not just colors via `theme`. Each slot is merged on
76
+ * top of the built-in style, so you only need to specify what you're changing.
77
+ */
78
+ export interface CountryPickerStyles {
79
+ /** The modal's root view */
80
+ container?: StyleProp<ViewStyle>;
81
+ /** The block holding the title row and search box, ruled off from the list */
82
+ headerSection?: StyleProp<ViewStyle>;
83
+ /** Row containing the title and close button */
84
+ header?: StyleProp<ViewStyle>;
85
+ title?: StyleProp<TextStyle>;
86
+ closeButton?: StyleProp<ViewStyle>;
87
+ closeButtonText?: StyleProp<TextStyle>;
88
+ /** The search field's box — fill, border, height */
89
+ searchContainer?: StyleProp<ViewStyle>;
90
+ /** The search field's text input */
91
+ search?: StyleProp<TextStyle>;
92
+ /** A single country row */
93
+ row?: StyleProp<ViewStyle>;
94
+ /** Merged on top of `row` when that row is the selected country */
95
+ rowSelected?: StyleProp<ViewStyle>;
96
+ flag?: StyleProp<TextStyle>;
97
+ name?: StyleProp<TextStyle>;
98
+ dialCode?: StyleProp<TextStyle>;
99
+ /** The "A", "B", "C"... letter header above each section */
100
+ sectionHeader?: StyleProp<ViewStyle>;
101
+ sectionHeaderText?: StyleProp<TextStyle>;
102
+ /** A single letter in the right-edge alphabet index */
103
+ sidebarLetter?: StyleProp<TextStyle>;
104
+ /** Merged on top of `sidebarLetter` for the currently-active letter */
105
+ sidebarLetterActive?: StyleProp<TextStyle>;
106
+ }
107
+
108
+ export interface PhoneInputProps extends Omit<
109
+ TextInputProps,
110
+ 'value' | 'onChangeText' | 'onChange' | 'style'
111
+ > {
112
+ /**
113
+ * ISO 3166-1 alpha-2 initial country, e.g. "US" (uncontrolled). Set this
114
+ * only when you want a *fixed* initial country regardless of the device —
115
+ * it always wins over locale detection. Leave it unset to prefer the
116
+ * device's locale region instead (see {@link fallbackCountry}).
117
+ */
118
+ defaultCountry?: CountryCode;
119
+ /**
120
+ * Initial country used only when `defaultCountry` is unset AND the
121
+ * device's locale region can't be resolved (or isn't in the current
122
+ * `countries`/`excludedCountries` list). Default: "US". Precedence for the
123
+ * initial country is: `defaultCountry` > device locale > `fallbackCountry`.
124
+ */
125
+ fallbackCountry?: CountryCode;
126
+ /** Controlled selected country. Falls back to internal state when omitted. */
127
+ country?: CountryCode;
128
+ /**
129
+ * Controlled value. Accepts either the national number on its own (digits,
130
+ * no dial code — e.g. "4155552671") or a full international number starting
131
+ * with "+" (e.g. "+14155552671"), in which case the selected country is
132
+ * derived from it. The international form lets a form store one string and
133
+ * hand it straight back, with no country state of its own; it is also what
134
+ * {@link onChangeInternational} emits. The selected country only changes
135
+ * when the calling code does, so picking a specific "+1" territory is not
136
+ * undone by the next keystroke.
137
+ */
138
+ value?: string;
139
+ /** Called with the composed value on every change */
140
+ onChangeText?: (value: PhoneInputValue) => void;
141
+ /**
142
+ * Called on every change with {@link PhoneInputValue.international} alone.
143
+ * Pairs with the international form of `value` so a form field can be wired
144
+ * up with just `value` and this callback.
145
+ */
146
+ onChangeInternational?: (value: string) => void;
147
+ /** Called only when the selected country changes */
148
+ onChangeCountry?: (country: Country) => void;
149
+ /** Restrict the picker to a subset of ISO codes */
150
+ countries?: CountryCode[];
151
+ /** Exclude specific ISO codes from the picker */
152
+ excludedCountries?: CountryCode[];
153
+ /** Disable the built-in libphonenumber-js validation styling/behavior */
154
+ disableValidation?: boolean;
155
+ /** Format the national number as the user types (default: true) */
156
+ autoFormat?: boolean;
157
+ /** Show a country control (flag/code) to the left of the input (default: true) */
158
+ showCountryPicker?: boolean;
159
+ /** What the country control shows: 'flag' | 'code' | 'both' (default: 'both') */
160
+ displayMode?: CountryDisplayMode;
161
+ /** Render a completely custom flag element instead of the built-in one */
162
+ renderFlag?: (country: Country) => React.ReactNode;
163
+ /** Render a fully custom row inside the country picker list */
164
+ renderCountryItem?: (info: CountryPickerRenderItemInfo) => React.ReactNode;
165
+ /** Placeholder for the picker search box */
166
+ searchPlaceholder?: string;
167
+ /** Hide the search box inside the country picker */
168
+ disableSearch?: boolean;
169
+ /**
170
+ * Group the picker list into A–Z sections with letter headers (default: true).
171
+ * Independent of {@link showAlphabetIndex} — either can be on without the other.
172
+ */
173
+ groupAlphabetically?: boolean;
174
+ /**
175
+ * Show an A–Z index sidebar on the right edge of the picker for tap/drag jumping
176
+ * (default: true). Works whether or not {@link groupAlphabetically} is on: with
177
+ * it off, the list stays flat but the index still jumps to the right country.
178
+ */
179
+ showAlphabetIndex?: boolean;
180
+ /**
181
+ * When the user types or pastes a number that starts with "+" and its calling
182
+ * code resolves to a country in the current list, auto-switch the selected
183
+ * country to match (e.g. pasting "+911234567890" selects India). Default: true.
184
+ */
185
+ autoDetectCountry?: boolean;
186
+
187
+ /** Style for the outer wrapper (equivalent to `containerStyle`, kept as the plain
188
+ * `style` prop so style-interop tooling like NativeWind's `cssInterop` — the same
189
+ * mechanism shadcn-style RN kits (e.g. react-native-reusables) rely on — can target
190
+ * it by the standard prop name). */
191
+ style?: StyleProp<ViewStyle>;
192
+ containerStyle?: StyleProp<ViewStyle>;
193
+ inputStyle?: StyleProp<TextStyle>;
194
+ dialCodeStyle?: StyleProp<TextStyle>;
195
+ flagStyle?: StyleProp<TextStyle>;
196
+ countryPickerButtonStyle?: StyleProp<ViewStyle>;
197
+ /** Per-element style overrides for the country picker modal (see {@link CountryPickerStyles}) */
198
+ pickerStyles?: CountryPickerStyles;
199
+
200
+ /** Visual theme tokens, merged under the style props above */
201
+ theme?: PhoneInputTheme;
202
+ /**
203
+ * Which palette to draw with. Defaults to "system", which follows the OS
204
+ * appearance — dark mode needs no setup.
205
+ */
206
+ colorScheme?: PhoneInputColorScheme;
207
+ /**
208
+ * Override individual palette tokens (separators, surfaces, muted text, the
209
+ * A-Z index, ...) on top of the resolved light/dark palette. This is the
210
+ * one knob for recoloring the whole component, picker included.
211
+ */
212
+ palette?: Partial<PhoneInputPalette>;
213
+
214
+ /**
215
+ * Safe-area padding for the picker sheet. Usually unnecessary — when
216
+ * `react-native-safe-area-context` is installed (an optional peer) the
217
+ * insets are detected automatically, and the defaults work without it
218
+ * either way. Any edge set here overrides the detected value for that edge
219
+ * only, so `{ top: 0 }` opts out of the top inset and keeps the rest.
220
+ */
221
+ pickerSafeAreaInsets?: {
222
+ top?: number;
223
+ bottom?: number;
224
+ left?: number;
225
+ right?: number;
226
+ };
227
+ /** Modal presentation style for the built-in picker (default: "pageSheet" on iOS, "fullScreen" elsewhere) */
228
+ pickerPresentationStyle?: 'pageSheet' | 'fullScreen' | 'formSheet';
229
+ }
package/src/value.ts ADDED
@@ -0,0 +1,70 @@
1
+ import {
2
+ AsYouType,
3
+ getCountryCallingCode,
4
+ isValidPhoneNumber,
5
+ parsePhoneNumberFromString,
6
+ } from 'libphonenumber-js';
7
+ import { getCountryByCallingCode, getCountryByCode } from './countries';
8
+ import type { Country } from './countries';
9
+ import type { PhoneInputValue } from './types';
10
+
11
+ /**
12
+ * The country's true calling code ("1" for Jamaica), as opposed to the
13
+ * extended `dialCode` ("1876") the picker shows to tell the NANP territories
14
+ * apart. Concatenating the latter with a national number double-counts the
15
+ * area code, so every international string is built from this instead.
16
+ */
17
+ export function callingCodeFor(country: Country): string {
18
+ try {
19
+ return getCountryCallingCode(country.code);
20
+ } catch {
21
+ return country.dialCode;
22
+ }
23
+ }
24
+
25
+ export function buildValue(
26
+ nationalNumber: string,
27
+ country: Country
28
+ ): PhoneInputValue {
29
+ const digits = nationalNumber.replace(/\D/g, '');
30
+ const valid = digits.length > 0 && isValidPhoneNumber(digits, country.code);
31
+ const e164 = valid
32
+ ? (parsePhoneNumberFromString(digits, country.code)?.number ?? null)
33
+ : null;
34
+ return {
35
+ nationalNumber: digits,
36
+ country: country.code,
37
+ dialCode: country.dialCode,
38
+ e164,
39
+ international: digits ? `+${callingCodeFor(country)}${digits}` : '',
40
+ isValid: valid,
41
+ };
42
+ }
43
+
44
+ /**
45
+ * Splits a controlled `value` into the national digits and, when the value is
46
+ * an international string, the country it implies. `current` is kept whenever
47
+ * its calling code still matches, so an explicit pick among the countries
48
+ * sharing a calling code (Jamaica vs the US on "+1") is not undone by the
49
+ * next keystroke.
50
+ */
51
+ export function readControlledValue(
52
+ raw: string,
53
+ current: Country
54
+ ): { national: string; country: Country } {
55
+ if (!raw.startsWith('+')) {
56
+ return { national: raw.replace(/\D/g, ''), country: current };
57
+ }
58
+ const formatter = new AsYouType();
59
+ formatter.input(raw);
60
+ const callingCode = formatter.getCallingCode();
61
+ const national = formatter.getNationalNumber() ?? '';
62
+
63
+ if (!callingCode || callingCode === callingCodeFor(current)) {
64
+ return { national, country: current };
65
+ }
66
+ const next =
67
+ getCountryByCode(formatter.getCountry()) ??
68
+ getCountryByCallingCode(callingCode);
69
+ return { national, country: next ?? current };
70
+ }