Skip to content

PhoneBookUtil

This document describes how the contact sync pipeline works in Nursery App and why each step exists.

  1. Keep an up-to-date snapshot of the user’s phonebook on device.
  2. Upload created/updated contacts to the backend on a weekly cadence.
  3. Avoid UI disruption — all work happens in the background after login once permissions are granted.
login_success() / login_check_success()
enforcePermissionsAfterLogin()
↓ (permissions granted)
phonebook_sync()
local sync every login → weekly upload

PhoneBookUtilClass (www/scripts/native.js)

Section titled “PhoneBookUtilClass (www/scripts/native.js)”
FunctionPurposeNotes / Usage
init()One-time setup (rate limits, listeners).Call after deviceready.
getAllContacts()Fetches every contact from device.Requests permission via ensureContactsPermission(). Returns normalized structures.
searchContacts(filter)Filtered contact lookup.Same permission requirement as getAllContacts().
_simpleHash, normalizeContact, hashContactInternal helpers to create deterministic hashes per contact/phone entry.Used to detect changes between syncs.
_normalizeCountryName, _resolveCountryFromContact, _mapDeviceContact, _buildEntryId, _getCountryDialCode, _stripCountryCode, _formatContactForStorageInternal formatting helpers.Not meant to be called directly; they shape the payload stored/uploaded.
_getDb, _idbGet, _idbSet, loadSnapshot, saveSnapshot, _loadMeta, _saveMetaIndexedDB access for snapshots/meta (global hash & last sync timestamp).Provide persistence across launches.
computeDiff(oldSnap, newSnap, allContacts)Given two snapshots and full contact map, returns { createdOrUpdated, deleted }.Upload path only cares about createdOrUpdated.
setUploadHandler(fn)Registers the function that receives batches to send to the backend.Called from phonebook_sync() before upload.
syncContacts({ force, userId, systemId })Builds new snapshot and diff, respecting weekly rate limit unless force=true. Returns { createdOrUpdated, deleted, skipped, reason }.Used every login to refresh snapshot.
syncAndUpload({ batchSize, force, userId, systemId, diff })Runs syncContacts (unless diff provided), then sends createdOrUpdated batches through the registered upload handler.We now pass the diff from syncContacts when upload is due, avoiding double work.

phonebook_sync() (www/scripts/functions.js)

Section titled “phonebook_sync() (www/scripts/functions.js)”
StepDescription
Upload handler registrationCalls PhoneBookUtil.setUploadHandler with the backend payload format.
Forced local syncPhoneBookUtil.syncContacts({ force: true, ... }) every login to keep snapshots updated.
Weekly upload gatingReads phonebook_last_upload from localStorage; if 7+ days elapsed, calls syncAndUpload({ diff }) and updates the timestamp.
LoggingUses console.log for success/skips; errors are caught and logged without user-facing UI.
FunctionPurpose / Usage
enforcePermissionsAfterLogin()Checks Notifications, Camera, Contacts, and Location permissions. Navigates to track only if all granted. Returns true/false so callers know whether post-login work (e.g., phonebook_sync()) can run.
login_success()After manual login, sets session info, awaits enforcePermissionsAfterLogin(), and then—when permissions OK—triggers phonebook_sync().
login_check_success()Same as above, but for the saved-session path (check_login). Ensures auto-login also runs the sync pipeline when permissions are satisfied.
// Called after login_success/login_check_success
const permissionsOk = await enforcePermissionsAfterLogin();
if (permissionsOk) {
try {
await phonebook_sync(); // runs local sync every time, uploads weekly
} catch (err) {
console.error("Optional phonebook sync failure:", err);
}
}

Under the hood phonebook_sync():

  1. Registers the upload handler (only once per login).
  2. Calls PhoneBookUtil.syncContacts({ force: true, userId, systemId }).
  3. If 7+ days since the last upload: PhoneBookUtil.syncAndUpload({ diff }).
  4. Logs outcome; never blocks navigation.
  • Local sync every login ensures the app’s snapshot reflects the latest on-device contacts even if we don’t upload yet.
  • Weekly upload balances freshness vs. server load; the backend sees at most one upload per week per user.
  • Reusing the diff prevents double work when the upload is due immediately after a sync.
  • Silent operation (console logs only) keeps the UX clean.
  • Any error inside phonebook_sync() is caught, logged, and does not block navigation.
  • If the user uninstalls/reinstalls, localStorage is empty; the first login triggers an immediate upload because phonebook_last_upload is missing.
  • Surface sync/upload status in diagnostics UI if needed.
  • Backend-driven upload cadence or configuration.
  • More granular batch retries when the server rejects certain contacts.

FunctionImplementation notes
enforcePermissionsAfterLogin()Promise.all on checkNotificationPermission(), checkCameraPermission(), checkContactsPermission(), checkLocationPermission(). Navigates to track on success; otherwise routes to permission page. Returns true only if navigation to track happened.
login_success()Stores user/session data, then calls enforcePermissionsAfterLogin(). If true, triggers phonebook_sync() (wrapped in try/catch).
login_check_success()Same flow for the auto-login path (when check_login returns true). Ensures returning users also run the sync pipeline.
  1. Upload handler: PhoneBookUtil.setUploadHandler(batch => { ... }) formats data for backend.
  2. Forced local sync: const diff = await PhoneBookUtil.syncContacts({ force: true, userId, systemId });
  3. Weekly upload decision:
    • Read const lastUploadAt = Number(localStorage.getItem("phonebook_last_upload") || 0);
    • If stale → call PhoneBookUtil.syncAndUpload({ diff, force: false, ... }) and store new timestamp.
  4. Logging: console.log results; errors caught so login isn’t blocked.
  • Rate limiting: MIN_SYNC_INTERVAL_MS (7 days) enforced inside syncContacts() unless force=true.
  • Snapshot storage: contactsSnapshot holds entry hashes; contactsMeta stores globalHash & lastSyncAt.
  • Diff algorithm: builds newSnap map keyed by entryId (contact ID + phone digits). Compares hashes with oldSnap to detect creations/updates/deletions.
  • Upload pipeline: syncAndUpload() slices createdOrUpdated into batches, calls the registered upload handler, and pauses between batches (BATCH_DELAY_MS).

Together these pieces let us sync locally every login, upload changes weekly, and reuse the same diff to avoid redundant work.

How does PhoneBookUtil work and what’s stored in IndexedDB?

Section titled “How does PhoneBookUtil work and what’s stored in IndexedDB?”
  • Answer
    • Splitting (for backend payload):
      const countryCode = this._getCountryDialCode(baseContact.country) || "+91";
      const phoneNumber = this._stripCountryCode(phoneValue, countryCode);
      _getCountryDialCode uses a normalized country name and a big COUNTRY_CODE_MAP.
      _stripCountryCode removes the country digits from the full phone digits if they match.
    • Formatted contact (used for upload handler and diff):
      {
      user_id,
      uuid,
      system_id,
      phone_number, // stripped
      first_name,
      middle_name,
      last_name,
      full_name,
      country_code, // "+91" style
      type
      }
    • IndexedDB storage:
      • newSnap[entryId] = { hash, lastSyncedAt } is saved as contactsSnapshot.
      • formattedContacts[entryId] = formatted is only in memory, used to compute diffs and feed the upload handler.
      • So IndexedDB stores only hashes + timestamps, not full contact records.

How is the dashboard phone book stripping the phone number and formatting display?

Section titled “How is the dashboard phone book stripping the phone number and formatting display?”
  • Answer
    1. Yes, when building the backend payload in phonebook_sync, the code uses:

      const payload = batch.map(({ contact }) => ({
      phone_number: contact.phone_number, // already stripped
      country_code: contact.country_code, // "+91"
      ...
      }));

      Those come from _formatContactForStorage, which uses _getCountryDialCode + _stripCountryCode.

    2. For the dashboard UI, we implemented the same behavior inline:

      const normalizeCountryName = (country = "") => {
      return country
      .trim()
      .toLowerCase()
      .replace(/[^a-z\s]/g, "")
      .replace(/\s+/g, " ")
      .trim();
      };
      const normalizedCountry = normalizeCountryName(contact.country);
      const countryCodeMap = PhoneBookUtil.COUNTRY_CODE_MAP || {};
      const countryCode = countryCodeMap[normalizedCountry] || "+91";
      const stripCountryCode = (phoneNumber, countryCode) => {
      if (!phoneNumber) return "";
      const digits = phoneNumber.replace(/\D+/g, "");
      if (!digits) return "";
      const countryDigits = (countryCode || "").replace(/\D+/g, "");
      if (countryDigits && digits.startsWith(countryDigits)) {
      return digits.slice(countryDigits.length);
      }
      return digits;
      };
      const phoneNumber = stripCountryCode(rawPhoneNumber, countryCode);

      And we feed that stripped phoneNumber into the phone-book renderer. The name entry in phoneBookData uses font_weight: 700 so it appears bold.

    3. Yes, the stripping logic used for display matches the backend’s logic semantically (normalize country → find dial code → strip that code from the digits).


9. How often do we sync and upload? What exactly is IndexedDB used for?

Section titled “9. How often do we sync and upload? What exactly is IndexedDB used for?”
  • Answers
    1. Local sync frequency
      phonebook_sync(true) calls PhoneBookUtil.syncContacts({ force: true, userId, systemId }).
      Because force: true, every call runs a full local sync, regardless of the internal 7‑day interval. You are calling this:

      • After login (auto & manual).
      • From some loaders (e.g., loader_track), as a background task.
        So sync happens on each trigger (e.g. login/app open), not just daily.
    2. Upload frequency
      Upload is gated by a 7‑day interval:

      const UPLOAD_INTERVAL_MS = 7 * 24 * 60 * 60 * 1000;
      const LAST_UPLOAD_KEY = "phonebook_last_upload";
      const dueForUpload = forceUpload || !lastUploadAt || now - lastUploadAt >= UPLOAD_INTERVAL_MS;

      If dueForUpload is false, sync still runs but no upload occurs.

    3. What IndexedDB stores
      IndexedDB (PhoneBookDB) stores:

      • contactsSnapshot = { [entryId]: { hash, lastSyncedAt } }
      • contactsMeta = { globalHash, lastSyncAt }
        It does not store the full contact objects; formattedContacts stay in memory only.
        That’s why dashboard-phone-book must call getAllContacts() from the plugin instead of querying IndexedDB for full contact data.
    4. Which stripping / normalization is used where?

      • Backend payloads use the private methods on PhoneBookUtil:
        • _normalizeCountryName, _getCountryDialCode, _stripCountryCode, _formatContactForStorage.
      • Dashboard UI can’t call those private methods directly, so we duplicated the same logic inline in loader_dashboard_phone_book, using PhoneBookUtil.COUNTRY_CODE_MAP for consistency.
    5. Is this a good approach? Suggestions (senior dev view)
      Pros:

      • Clear separation: PhoneBookUtil handles permission, normalization, hashing, upload.
      • Weekly upload reduces backend load and respects privacy/bandwidth.
      • Hash-based diff avoids re‑uploading unchanged contacts.
      • Local IndexedDB snapshot is small (hashes only).

      Cons / improvements:

      • Logic duplication: phone normalization and stripping is duplicated in UI code. Better to expose public helpers like:
        PhoneBookUtil.getDialCode(country)
        PhoneBookUtil.stripCountryCode(phoneNumber, countryCode)
        and use them everywhere.
      • No cached full contacts: UI always hits the plugin. If the phone book is large, that may be slow and requires permission every time. For a smoother UX, you could:
        • Persist formattedContacts into a separate IndexedDB store (e.g. contactsStore).
        • Add PhoneBookUtil.getCachedContacts() to read them for UI.
        • Use a pattern: show cached list immediately, then refresh via plugin in the background.
      • Sync triggering: it’s called from multiple places. Centralizing sync triggers (e.g., on app start/resume, and on explicit user actions) can reduce complexity.
      • Privacy/security: if you do choose to cache full contacts:
        • Treat them as PII: clear on logout, document usage, possibly encrypt at rest, and give users control (e.g., “clear phonebook cache”).

      Net:
      Your current approach (hash-only snapshot + plugin for UI) is safe and simple. I’d keep it unless you have strong UX requirements for offline/instant phone-book views. If you move to caching full contacts, do it with:

      • Shared helper functions (no duplication),
      • A dedicated IndexedDB store,
      • Clear lifecycle and privacy rules.

10. Is it wise to store many full contacts in IndexedDB?

Section titled “10. Is it wise to store many full contacts in IndexedDB?”
  • Question
    “Is it wise to store so many full contacts in indexed db?”

  • Answer
    It’s technically fine but needs careful trade-offs:

    • Pros:
      • Faster UI (no plugin call each time).
      • Offline access to the phone book.
    • Cons:
      • Storage: thousands of contacts can be several MB; IndexedDB can handle it, but you must be aware of quotas and older devices.
      • Privacy & security: storing full names + phone numbers increases the impact of any compromise; you must handle logout/clear, permission revocation, and possibly regulatory expectations.
      • Complexity: you need a clear cache lifecycle (when to refresh, when to wipe, what happens if permission is revoked).

    As a rule:

    • If your UX is acceptable with live plugin reads, not caching full contacts is safer and simpler.
    • If you need fast, offline phone-book behavior, then caching is justified — but treat contacts as sensitive data: minimize fields, clear on logout, and keep all contact-related logic in a well-encapsulated module (PhoneBookUtil + a dedicated cache helper).