npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@omarsdev/react-native-contacts

v1.3.1

Published

Access the device address book and track when contacts were last touched.

Readme

@omarsdev/react-native-contacts

Contacts at scale for React Native, with fast first-run paging and efficient “delta since last sync” on Android and iOS.

Why this library

  • Large address books without jank: fetch 10k+ contacts in pages (200–500) to keep UI responsive.
  • Real delta sync: only fetch contacts that changed since your last run.
  • Native tokens, minimal JS state: tokens are persisted natively to avoid storing massive lists in JS.
  • Android sorted by last updated: newest changes first for better UX.
  • iOS resilient strategy: uses Contacts change history when available, and a native fingerprint snapshot fallback when it isn’t.

Install

# with Yarn
yarn add @omarsdev/react-native-contacts

# with npm
npm install @omarsdev/react-native-contacts

Permissions

  • Android: request READ_CONTACTS at runtime before usage.

    • Optional (manifest) — if you prefer declaring in your app too:

      <!-- android/app/src/main/AndroidManifest.xml -->
      <manifest ...>
        <uses-permission android:name="android.permission.READ_CONTACTS" />
      </manifest>
    • Runtime request (JS):

      import { PermissionsAndroid, Platform } from 'react-native';
      
      export async function ensureContactsPermission() {
        if (Platform.OS !== 'android') return true;
        const res = await PermissionsAndroid.request(
          PermissionsAndroid.PERMISSIONS.READ_CONTACTS
        );
        return res === PermissionsAndroid.RESULTS.GRANTED;
      }
  • iOS: add usage description to your app Info.plist and rebuild pods.

    • Info.plist:
      <key>NSContactsUsageDescription</key>
      <string>This app needs access to your contacts to sync changes.</string>
    • iOS will show the permission prompt the first time you access contacts.

Type shapes

type Contact = {
  id: string;
  displayName: string;
  phoneNumbers: string[];
  givenName?: string | null;
  familyName?: string | null;
  // Android only; iOS sets null
  lastUpdatedAt?: number | null;
};

type PhoneNumberUpdate = {
  previous: string;
  current: string;
};

type PhoneNumberChanges = {
  created: string[];
  deleted: string[];
  updated: PhoneNumberUpdate[];
};

type ContactChange = Contact & {
  changeType: 'created' | 'updated' | 'deleted';
  isDeleted: boolean;
  phoneNumberChanges: PhoneNumberChanges;
  previous?: {
    displayName?: string | null;
    givenName?: string | null;
    familyName?: string | null;
    phoneNumbers: string[];
  } | null;
};

type UpdatedPageBase = {
  nextSince: string;
  totalContacts: number;
};

type UpdatedPage = UpdatedPageBase & {
  mode: 'delta' | 'full';
  items: ContactChange[];
};

API reference & examples

Types

| Type | Description | | ------------------------- | -------------------------------------------------------------------------------------------------------------------------------- | | type Contact | Normalised contact record returned by all APIs. Includes optional givenName, familyName, and lastUpdatedAt (Android only). | | type PhoneNumberUpdate | Represents an individual phone number that changed within a contact delta (previouscurrent). | | type PhoneNumberChanges | Buckets the numbers added/removed/updated in a ContactChange. Useful when reconciling diffs. | | type ContactChange | Extends Contact with delta metadata (changeType, isDeleted, phoneNumberChanges, and an optional previous snapshot). | | type UpdatedPage | Page returned by paging APIs (mode, items, nextSince, and totalContacts so you know the device book size). Items are always ContactChange[]. |

Functions (promise / async)

  • getById(id: string): Promise<Contact | null>

    • Look up a single contact by its native identifier. Resolves to null if the contact no longer exists or the identifier was empty.
    const maybeAlice = await getById('42');
    if (maybeAlice) {
      console.log('Found contact', maybeAlice.displayName);
    }
  • getPersistedSince(): Promise<string>

    • Reads the last token that was committed natively (empty string if nothing has been stored yet). Handy when resuming delta sync in a fresh JS session.
    const lastToken = await getPersistedSince();
    console.log('Native token:', lastToken);
  • commitPersisted(nextSince: string): Promise<void>

    • Persists the supplied token on the native side so subsequent delta calls start from that point. On iOS this also rebuilds the fingerprint snapshot.
    await commitPersisted(nextToken);
  • getAll(): Promise<Contact[]>

    • Convenience helper that returns the full native contact list in one call.
    const everyone = await getAll();
    console.log('Fetched contacts', everyone.length);
  • getUpdatedSincePaged(since: string, offset: number, limit: number): Promise<UpdatedPage>

    • Fetch a delta page using an explicit token. When native change tracking is available the result is { mode: 'delta' } with changed contacts and the next token. If the platform cannot supply a delta token—or you pass an empty token on first synchronisation—it returns { mode: 'full' } so you can re-sync ordinary contact pages (still respecting offset/limit). Token format differs by platform: Android always returns a millisecond timestamp, while iOS may return a base64-encoded CNChangeHistory token (e.g. YnBsaXN0…) or, when history is unavailable, a synthetic fp:<timestamp> token.
    const page = await getUpdatedSincePaged(lastToken, 0, 200);
    if (page.mode === 'full') {
      page.items.forEach((change) => console.log('Full contact', change.id));
    } else {
      page.items.forEach((change) => console.log(change.changeType, change.id));
    }
    console.log('Total contacts on device', page.totalContacts);
    • Need to walk every page? Use getUpdatedSincePaged.listen to stream until exhaustion (return false from the handler to stop early). pageSize is optional and defaults to 300.
    await getUpdatedSincePaged.listen(
      { since: lastToken, pageSize: 250 },
      async (page) => {
        console.log(
          `Page mode=${page.mode} size=${page.items.length} total=${page.totalContacts}`
        );
      }
    );
    • Streaming signature: getUpdatedSincePaged.listen(handler, options?) or getUpdatedSincePaged.listen(options, handler). The handler can be async and should return false to stop fetching. options accepts { since?: string; offset?: number; pageSize?: number }.

iOS tokens:

  • Real change-history tokens look like long base64 strings (YnBsaXN0MDD…).
  • Fallback fingerprints use the fp:<timestamp> format when history is disabled or unchanged.

Quick start

import {
  commitPersisted,
  getPersistedSince,
  getUpdatedSincePaged,
} from '@omarsdev/react-native-contacts';
import { ensureContactsPermission } from './permissions'; // from snippet above

// Delta or baseline sync (falls back to full pages when native tokens are unavailable)
if (await ensureContactsPermission()) {
  const persistedSince = await getPersistedSince();
  const pageSize = 300;
  let nextSince: string | undefined = persistedSince;
  let usedFullFallback = false;

  await getUpdatedSincePaged.listen(
    { since: persistedSince, pageSize },
    (page) => {
      if (page.nextSince) nextSince = page.nextSince;
      if (!page.items.length) return false;
      const label = page.mode === 'full' ? 'Contacts page' : 'Delta page';
      console.log(
        `${label}: ${page.items.length} items (total contacts ${page.totalContacts})`
      );
      if (page.mode === 'full') usedFullFallback = true;
      return page.items.length >= pageSize;
    }
  );

  if (nextSince && nextSince !== persistedSince) {
    await commitPersisted(nextSince);
  } else if (usedFullFallback && !nextSince) {
    console.log('Full snapshot processed; no token persisted yet.');
  }
}

Example scenarios

The example app in example/src/screens/ContactsDemoScreen.tsx walks through the most common flows. The snippets below highlight the key cases in isolation:

import {
  commitPersisted,
  getById,
  getPersistedSince,
  getUpdatedSincePaged,
} from '@omarsdev/react-native-contacts';
import type { Contact, ContactChange } from '@omarsdev/react-native-contacts';

// 1. Request permission on Android before touching contacts.
await ensureContactsPermission();

// 2. Pull the delta (or fallback full pages) since the last committed token and persist progress.
const persistedSince = await getPersistedSince();
const pageSize = 300;
let sessionToken = persistedSince;
let totalContacts: number | undefined;
const delta: ContactChange[] = [];
let fullFallback: Contact[] = [];

await getUpdatedSincePaged.listen(
  { since: persistedSince, pageSize },
  (page) => {
    if (page.nextSince) sessionToken = page.nextSince;
    if (!page.items.length) return false;
    totalContacts = page.totalContacts;
    if (page.mode === 'delta') {
      delta.push(...page.items);
    } else {
      fullFallback = fullFallback.concat(page.items);
    }
    return page.items.length >= pageSize;
  }
);

if (sessionToken && sessionToken !== persistedSince) {
  await commitPersisted(sessionToken);
}

console.log('Total contacts reported by native layer', totalContacts ?? 'unknown');

// 3. Full fallback pages can be handled like a baseline rebuild.
console.log('Full snapshot contacts (if fallback)', fullFallback.length);

// 4. Look up a single contact by identifier (helpful after any baseline rebuild).
const singleContact = await getById('12345'); // returns `null` if the contact was deleted

Platform details

  • Android
    • Uses ContactsContract.Contacts.CONTACT_LAST_UPDATED_TIMESTAMP for sorting and delta (timestamp filter).
    • lastUpdatedAt is set from this value.
  • iOS
    • Change history: uses CNContactStore change-history tokens when available.
    • Fingerprint fallback: when change history is unavailable or returns no events, a native snapshot (id → fingerprint of name + normalized numbers) detects adds/edits; snapshot updates on commitPersisted.
    • Synthetic tokens: when the system token doesn’t advance, we synthesize fp:<timestamp> to ensure forward progress.
    • Delta payloads include changeType, a previous snapshot (when available), and phoneNumberChanges summarising added, updated, and deleted numbers.

Recommended paging & usage

  • Page size 200–500 works well for large books.
  • Always commit a delta token after finishing a delta session.
  • First run: do a full fetch; then do a delta to seed/commit a token.

Build & development

  • Prereqs: Node 20+, Yarn 3 (Berry).
  • Install deps and build the lib:
    • yarn (at repo root)
    • yarn prepare (runs bob/codegen; generates lib/ and TS types)
  • Example app — Android:
    • cd example && yarn android
  • Example app — iOS:
    • cd example/ios && pod install
    • cd .. && yarn ios
  • Using in your app:
    • yarn add @omarsdev/react-native-contacts
    • iOS: cd ios && pod install
    • Rebuild the app

Why install

  • You need to sync very large contact sets without freezing the UI.
  • You want reliable “delta since last run” on both Android and iOS.
  • You don’t want to persist huge JS arrays; tokens are handled natively.
  • You want Android sorted by latest updates and an iOS strategy that works even when change history is unavailable.

License

MIT

Contributing

  • See CONTRIBUTING.md for development workflow and conventions.