PhoneBookUtil
PhoneBook Sync & Upload
Section titled “PhoneBook Sync & Upload”This document describes how the contact sync pipeline works in Nursery App and why each step exists.
- Keep an up-to-date snapshot of the user’s phonebook on device.
- Upload created/updated contacts to the backend on a weekly cadence.
- Avoid UI disruption — all work happens in the background after login once permissions are granted.
High-Level Flow
Section titled “High-Level Flow”login_success() / login_check_success() ↓enforcePermissionsAfterLogin() ↓ (permissions granted)phonebook_sync() ↓local sync every login → weekly uploadAPI Surface
Section titled “API Surface”PhoneBookUtilClass (www/scripts/native.js)
Section titled “PhoneBookUtilClass (www/scripts/native.js)”| Function | Purpose | Notes / 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, hashContact | Internal helpers to create deterministic hashes per contact/phone entry. | Used to detect changes between syncs. |
_normalizeCountryName, _resolveCountryFromContact, _mapDeviceContact, _buildEntryId, _getCountryDialCode, _stripCountryCode, _formatContactForStorage | Internal formatting helpers. | Not meant to be called directly; they shape the payload stored/uploaded. |
_getDb, _idbGet, _idbSet, loadSnapshot, saveSnapshot, _loadMeta, _saveMeta | IndexedDB 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)”| Step | Description |
|---|---|
| Upload handler registration | Calls PhoneBookUtil.setUploadHandler with the backend payload format. |
| Forced local sync | PhoneBookUtil.syncContacts({ force: true, ... }) every login to keep snapshots updated. |
| Weekly upload gating | Reads phonebook_last_upload from localStorage; if 7+ days elapsed, calls syncAndUpload({ diff }) and updates the timestamp. |
| Logging | Uses console.log for success/skips; errors are caught and logged without user-facing UI. |
Permission Enforcement (functions.js)
Section titled “Permission Enforcement (functions.js)”| Function | Purpose / 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. |
Example Usage
Section titled “Example Usage”// Called after login_success/login_check_successconst 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():
- Registers the upload handler (only once per login).
- Calls
PhoneBookUtil.syncContacts({ force: true, userId, systemId }). - If 7+ days since the last upload:
PhoneBookUtil.syncAndUpload({ diff }). - Logs outcome; never blocks navigation.
Why this split?
Section titled “Why this split?”- 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.
Failure Handling
Section titled “Failure Handling”- Any error inside
phonebook_sync()is caught, logged, and does not block navigation. - If the user uninstalls/reinstalls,
localStorageis empty; the first login triggers an immediate upload becausephonebook_last_uploadis missing.
Future Enhancements
Section titled “Future Enhancements”- 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.
Implementation Deep Dive
Section titled “Implementation Deep Dive”Permission Gate (functions.js)
Section titled “Permission Gate (functions.js)”| Function | Implementation 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. |
phonebook_sync() internals
Section titled “phonebook_sync() internals”- Upload handler:
PhoneBookUtil.setUploadHandler(batch => { ... })formats data for backend. - Forced local sync:
const diff = await PhoneBookUtil.syncContacts({ force: true, userId, systemId }); - 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.
- Read
- Logging:
console.logresults; errors caught so login isn’t blocked.
PhoneBookUtilClass highlights
Section titled “PhoneBookUtilClass highlights”- Rate limiting:
MIN_SYNC_INTERVAL_MS(7 days) enforced insidesyncContacts()unlessforce=true. - Snapshot storage:
contactsSnapshotholds entry hashes;contactsMetastoresglobalHash&lastSyncAt. - Diff algorithm: builds
newSnapmap keyed byentryId(contact ID + phone digits). Compares hashes witholdSnapto detect creations/updates/deletions. - Upload pipeline:
syncAndUpload()slicescreatedOrUpdatedinto 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.
Phone Book & Contacts – Q&A Log
Section titled “Phone Book & Contacts – Q&A Log”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);
_getCountryDialCodeuses a normalized country name and a bigCOUNTRY_CODE_MAP.
_stripCountryCoderemoves 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, // strippedfirst_name,middle_name,last_name,full_name,country_code, // "+91" styletype}
- IndexedDB storage:
newSnap[entryId] = { hash, lastSyncedAt }is saved ascontactsSnapshot.formattedContacts[entryId] = formattedis only in memory, used to compute diffs and feed the upload handler.- So IndexedDB stores only hashes + timestamps, not full contact records.
- Splitting (for backend payload):
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
-
Yes, when building the backend payload in
phonebook_sync, the code uses:const payload = batch.map(({ contact }) => ({phone_number: contact.phone_number, // already strippedcountry_code: contact.country_code, // "+91"...}));Those come from
_formatContactForStorage, which uses_getCountryDialCode+_stripCountryCode. -
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
phoneNumberinto the phone-book renderer. The name entry inphoneBookDatausesfont_weight: 700so it appears bold. -
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
-
Local sync frequency
phonebook_sync(true)callsPhoneBookUtil.syncContacts({ force: true, userId, systemId }).
Becauseforce: 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.
-
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
dueForUploadis false, sync still runs but no upload occurs. -
What IndexedDB stores
IndexedDB (PhoneBookDB) stores:contactsSnapshot={ [entryId]: { hash, lastSyncedAt } }contactsMeta={ globalHash, lastSyncAt }
It does not store the full contact objects;formattedContactsstay in memory only.
That’s whydashboard-phone-bookmust callgetAllContacts()from the plugin instead of querying IndexedDB for full contact data.
-
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, usingPhoneBookUtil.COUNTRY_CODE_MAPfor consistency.
- Backend payloads use the private methods on
-
Is this a good approach? Suggestions (senior dev view)
Pros:- Clear separation:
PhoneBookUtilhandles 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:
and use them everywhere.PhoneBookUtil.getDialCode(country)PhoneBookUtil.stripCountryCode(phoneNumber, countryCode)
- 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
formattedContactsinto 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.
- Persist
- 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.
- Clear separation:
-
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).
- Pros: