A zero-permission contact picker for React Native. Lets the user pick a single contact (name + phone) via the native OS picker β without ever requesting READ_CONTACTS or Contacts authorization.
Look mom, no permission dialogs! π
Traditional library react-native-pick-contact
βββββββββββββββββββββ βββββββββββββββββββββββββ
1. User taps "Pick Contact" 1. User taps "Pick Contact"
2. π Permission dialog appears 2. π± Native picker opens instantly
3. π¬ User hesitates / denies 3. User picks a contact
4. ...or grants full address book 4. β
App receives name + phone
5. App reads entire contact list
6. App finds the one contact
Permissions: READ_CONTACTS Permissions: NONE
Data exposed: EVERYTHING Data exposed: 1 contact
Many popular contact libraries are broken on React Native 0.76+ because they rely on the legacy Bridge and haven't migrated to TurboModules.
react-native-pick-contact is built from the ground up for the New Architecture:
- TurboModule native module (C++ codegen on iOS, Java codegen on Android)
- Codegen type-safe specs β no manual bridging, no
NativeModules["..."]hacks - Works out of the box with React Native 0.76+ β just install and go
If you're migrating to the New Architecture and your current contact library broke, this is a drop-in replacement.
Most React Native contact libraries require the READ_CONTACTS permission, which gives your app access to the entire address book. This is:
- A privacy concern β users see a scary permission dialog and may deny it
- A security risk β your app has access to data it doesn't need
- An App Store / Play Store review flag β reviewers question why you need full contact access
react-native-pick-contact takes a different approach:
| Traditional Libraries | react-native-pick-contact | |
|---|---|---|
| Permissions | READ_CONTACTS (full address book) |
None |
| Data access | All contacts, all fields | One contact, name + phone only |
| User trust | Permission dialog before use | System picker, no dialog |
| Privacy | App can read contacts in background | Only what user explicitly picks |
- iOS: Uses
CNContactPickerViewControllerβ an out-of-process system UI. Your app never touches the Contacts database directly. - Android: Launches
ACTION_PICKonContactsContract.CommonDataKinds.Phone.CONTENT_URI. The system picker returns a single phone data row with a temporary URI permission scoped to that row, which already carries the name, number and photo. On API 37+ Android upgrades the same intent to the new Contact Picker UI automatically.
- React Native 0.76+ (New Architecture enabled)
- iOS 15.0+
- Android minSdk 24+
npm install react-native-pick-contactcd ios && pod installNo additional configuration needed. No Info.plist keys required.
No additional configuration needed. The library's manifest declares no permissions, so nothing is merged into your app manifest and installing it never adds a contacts permission to your Play Console data declaration.
import { pickContact } from 'react-native-pick-contact';
async function handlePickContact() {
const contact = await pickContact();
if (contact === null) {
// User cancelled the picker
return;
}
console.log(contact.name); // "John Appleseed"
console.log(contact.phone); // "+1 (555) 012-3456"
}function pickContact(): Promise<Contact | null>;Opens the native OS contact picker. Returns a Promise that resolves with:
- A
Contactobject if the user selected a contact nullif the user cancelled
type Contact = {
name: string; // Full display name
phone: string; // First phone number (formatted)
givenName?: string; // First/given name, if available
familyName?: string; // Last/family name, if available
email?: string; // First email address, if available
thumbnailUri?: string; // File URI to the contact's photo, if available
};name and phone are always present. The rest are best-effort and platform-dependent:
| Field | iOS | Android |
|---|---|---|
name |
β | β |
phone |
β | β |
thumbnailUri |
β | β |
givenName |
β | only if your app already holds READ_CONTACTS |
familyName |
β | only if your app already holds READ_CONTACTS |
email |
β | only if your app already holds READ_CONTACTS |
On Android the picker hands back a single phone data row. Structured name and email live on
other rows of the contact, which are unreachable without READ_CONTACTS β and this library never
asks for it. If your app already holds the permission for its own reasons, those three fields are
filled in; otherwise they are absent. No prompt is ever shown either way.
| Code | Description |
|---|---|
E_NO_ACTIVITY |
Android: no active Activity found |
E_NO_VIEW_CONTROLLER |
iOS: no root view controller found |
E_PICKER_BUSY |
Picker is already open (both platforms) |
E_LAUNCH_PICKER |
Failed to launch the system picker |
E_CONTACT_RESOLVE |
Failed to read data from the selected contact |
E_ACTIVITY_DESTROYED |
Android: Activity destroyed while picker was open |
E_MODULE_DEALLOCATED |
iOS: native module was deallocated mid-operation |
- On iOS, contacts without phone numbers are grayed out in the picker (cannot be selected).
- On Android, the picker lists phone entries, so a contact with no number cannot be selected either. A contact with several numbers shows one entry per number and the user picks the exact one.
- The
phonefield returns the number the user picked (iOS returns the contact's first number).
ActivityResultContracts.PickContact() picks against Contacts.CONTENT_URI and returns a
contact-level URI. Reading a phone number from it means appending the /data sub-path β and on
API 36 and below the picker's temporary grant does not cover sub-paths. The query comes back
empty on most devices (Pixel/AOSP, Samsung, OnePlus, Oppo), which is why versions up to 0.0.5 fell
back to a READ_CONTACTS runtime request on practically every launch
(#2).
Since 0.0.6 the library picks against Phone.CONTENT_URI instead. That returns a data-row URI
(content://com.android.contacts/data/<id>) whose single row already holds the number, the display
name and the photo URI. No sub-path, so the temporary grant is sufficient, no permission is
requested and none is declared. On API 37+ the platform upgrades the same ACTION_PICK intent to
the new Contact Picker UI, so no version branch is needed.
Android 11 (API 30) introduced package visibility filtering, which requires apps to declare <queries> in their manifest to interact with other apps. You do not need to add any <queries> tags for this library. The system contact picker is an OS-level component β Android launches it directly and grants a temporary URI permission for the selected contact. No inter-app resolution or manifest declarations are required.
The iOS Simulator ships with no contacts by default. The picker will open but display an empty list.
Fix: Import the test contacts file included in this repo by dragging test-contacts.vcf onto the Simulator window, or run:
xcrun simctl openurl booted "file://$(pwd)/test-contacts.vcf"You can also open the Contacts app in the Simulator and add contacts manually. Alternatively, test on a real device where your iCloud or local contacts are available.
This can happen if the root view controller is not fully presented yet (e.g., calling pickContact() during app launch). Wait until your screen is fully mounted before calling the function.
This occurs when pickContact() is called while no Activity is in the foreground (e.g., from a background task or headless JS). Ensure you only call it from a user-facing screen.
Found a bug? Have a feature idea? Open an issue or submit a PR β contributions are welcome!
The repo carries a standalone Gradle harness, so the Android unit tests (Robolectric) run without a host app:
npm run test:android # ./gradlew :react-native-pick-contact:test
npm run typescript # tsc --noEmit
npm run lintThe harness files (build.gradle, settings.gradle, gradlew, gradle/) are development-only and are not published to npm.
If this library saved you from dealing with READ_CONTACTS permissions, consider giving it a star on GitHub β it helps other developers discover the zero-permission approach.
MIT