@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.
- package/LICENSE +20 -0
- package/README.md +467 -0
- package/lib/commonjs/CountryPicker.js +669 -0
- package/lib/commonjs/CountryPicker.js.map +1 -0
- package/lib/commonjs/Flag.js +23 -0
- package/lib/commonjs/Flag.js.map +1 -0
- package/lib/commonjs/PhoneInput.js +252 -0
- package/lib/commonjs/PhoneInput.js.map +1 -0
- package/lib/commonjs/countries.js +75 -0
- package/lib/commonjs/countries.js.map +1 -0
- package/lib/commonjs/data/countries.json +1472 -0
- package/lib/commonjs/index.js +71 -0
- package/lib/commonjs/index.js.map +1 -0
- package/lib/commonjs/package.json +1 -0
- package/lib/commonjs/safeArea.js +44 -0
- package/lib/commonjs/safeArea.js.map +1 -0
- package/lib/commonjs/theme.js +85 -0
- package/lib/commonjs/theme.js.map +1 -0
- package/lib/commonjs/types.js +6 -0
- package/lib/commonjs/types.js.map +1 -0
- package/lib/commonjs/value.js +68 -0
- package/lib/commonjs/value.js.map +1 -0
- package/lib/module/CountryPicker.js +665 -0
- package/lib/module/CountryPicker.js.map +1 -0
- package/lib/module/Flag.js +19 -0
- package/lib/module/Flag.js.map +1 -0
- package/lib/module/PhoneInput.js +248 -0
- package/lib/module/PhoneInput.js.map +1 -0
- package/lib/module/countries.js +65 -0
- package/lib/module/countries.js.map +1 -0
- package/lib/module/data/countries.json +1472 -0
- package/lib/module/index.js +8 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/module/safeArea.js +39 -0
- package/lib/module/safeArea.js.map +1 -0
- package/lib/module/theme.js +81 -0
- package/lib/module/theme.js.map +1 -0
- package/lib/module/types.js +4 -0
- package/lib/module/types.js.map +1 -0
- package/lib/module/value.js +62 -0
- package/lib/module/value.js.map +1 -0
- package/lib/typescript/commonjs/package.json +1 -0
- package/lib/typescript/commonjs/src/CountryPicker.d.ts +37 -0
- package/lib/typescript/commonjs/src/CountryPicker.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/Flag.d.ts +10 -0
- package/lib/typescript/commonjs/src/Flag.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/PhoneInput.d.ts +3 -0
- package/lib/typescript/commonjs/src/PhoneInput.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/countries.d.ts +28 -0
- package/lib/typescript/commonjs/src/countries.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/index.d.ts +9 -0
- package/lib/typescript/commonjs/src/index.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/safeArea.d.ts +11 -0
- package/lib/typescript/commonjs/src/safeArea.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/theme.d.ts +53 -0
- package/lib/typescript/commonjs/src/theme.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/types.d.ts +211 -0
- package/lib/typescript/commonjs/src/types.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/value.d.ts +22 -0
- package/lib/typescript/commonjs/src/value.d.ts.map +1 -0
- package/lib/typescript/module/package.json +1 -0
- package/lib/typescript/module/src/CountryPicker.d.ts +37 -0
- package/lib/typescript/module/src/CountryPicker.d.ts.map +1 -0
- package/lib/typescript/module/src/Flag.d.ts +10 -0
- package/lib/typescript/module/src/Flag.d.ts.map +1 -0
- package/lib/typescript/module/src/PhoneInput.d.ts +3 -0
- package/lib/typescript/module/src/PhoneInput.d.ts.map +1 -0
- package/lib/typescript/module/src/countries.d.ts +28 -0
- package/lib/typescript/module/src/countries.d.ts.map +1 -0
- package/lib/typescript/module/src/index.d.ts +9 -0
- package/lib/typescript/module/src/index.d.ts.map +1 -0
- package/lib/typescript/module/src/safeArea.d.ts +11 -0
- package/lib/typescript/module/src/safeArea.d.ts.map +1 -0
- package/lib/typescript/module/src/theme.d.ts +53 -0
- package/lib/typescript/module/src/theme.d.ts.map +1 -0
- package/lib/typescript/module/src/types.d.ts +211 -0
- package/lib/typescript/module/src/types.d.ts.map +1 -0
- package/lib/typescript/module/src/value.d.ts +22 -0
- package/lib/typescript/module/src/value.d.ts.map +1 -0
- package/package.json +215 -0
- package/src/CountryPicker.tsx +805 -0
- package/src/Flag.tsx +17 -0
- package/src/PhoneInput.tsx +363 -0
- package/src/countries.ts +95 -0
- package/src/data/countries.json +1472 -0
- package/src/index.tsx +21 -0
- package/src/safeArea.ts +50 -0
- package/src/theme.ts +112 -0
- package/src/types.ts +229 -0
- 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';
|
package/src/safeArea.ts
ADDED
|
@@ -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
|
+
}
|