Naar de inhoud
Documentatie doorbladeren

De documentatie is alleen in het Engels geschreven. De rest van de site is ook in het Nederlands beschikbaar.

Besloten

Connected services and sync

The Services tab: connecting the phone's address book and Google Contacts, choosing circles, running a sync, resolving the reconciliation buckets, and cross-referencing an address book against Incico.

Bijgewerkt: 29 October 2026

A connected service is an address book that Incico writes contact cards into and reads contacts from. Everything happens on the device: the phone talks to its own address book or to Google with the user’s own token. The server only stores the service’s configuration and reconciliation state so a second device can show it.

Providers

ProviderLaunchMechanics
Device contactsyesiOS Contacts framework, Android ContactsContract; permission requested after an in-app explanation; Incico writes into its own contact group/account (“Incico”) so its entries are distinguishable and removable
Google ContactsyesOAuth 2.0 PKCE with a per-platform public client, scope contacts (read/write), People API with syncToken for incremental reads; Incico contacts carry a userDefined field incico=<profile_id> and are grouped under an “Incico” label
Microsoft / OutlookflagMicrosoft Graph Contacts.ReadWrite, delta queries; prepared, enabled by the questions page
Apple iCloudvia device contactsiCloud contacts are the device address book when iCloud is on; no separate provider

3.3 Services tab

Top bar: the profile switcher and Connect (Add service). Title “Services”. Mock: mobile/design/redesign/redesign-round-2.html (A110).

  1. Summary, shown while anything waits: one card across every service on this device, “Your address books are 3 changes behind”, a thin stacked bar (to add in primary, to update in orange, the rest fill), the legend “1 to add · 2 to update · 36 in sync” and Review and sync, which opens 3.3.1.3 over all of them.
  2. Connected: one row per service: the provider’s icon, its name (“Google”, “Phone contacts”), a status line (“3 waiting · yesterday”, “In sync · 10 min ago”) and the circle labels of the circles it syncs. A service connected on another device is dimmed with the reason “On another device · connect from there”; tapping it still opens its details, where Connect here lives.

When a service fails, an error box leads the tab instead of the summary: “Google stopped accepting changes”, “Google signed Incico out on 27 Oct. Sign in again so your Work and Sports club contacts stay current.” and Sign in to Google again; the row’s status line says “Signed out · 5 changes waiting” in danger. Empty state: “No connected services. Connect your address book.” with the two providers.

3.3.2 Add service (sheet)

Provider list with descriptions. Choosing one:

  1. Explain screen: what will be read and written, that nothing leaves the phone, and which OS permission or Google consent follows.
  2. Authorise: OS permission dialog (device) or the Google consent screen in a secure browser tab (Custom Tabs / ASWebAuthenticationSession). Failure or cancel returns to the explain screen with a clear message.
  3. Configure: label (pre-filled), Circles to sync (multi-select; contacts in these circles are written to the service), Allow Incico to delete (switch, default off: when off, contacts that leave the chosen circles or are removed are only reported in the delete bucket, never deleted).
  4. First sync → 3.3.1.1 Synchronising, then the service details.

Label uniqueness: a second Google account gets “Google (2)” automatically (v1 E2E behaviour).

3.3.1 Service details

Sections:

  1. Header: provider icon, label (tap to rename), account identity, connected-on device, last sync, overflow (Rename, Disconnect).
  2. Circles: multi-select checklist with dots and counts, “12 contacts in selected circles” (v2).
  3. Allow Incico to delete switch with the explanation.
  4. Sync now (primary) and Find people on Incico (cross-reference, secondary).
  5. Changes: the review list of 3.3.1.3 for this service, with the buckets that need nothing (Invited, In sync, Hidden) as one count line each under it. The buckets, v1’s seven plus v2’s states, unified:
BucketMeaningActions
To addIncico contact in a synced circle with no entry in the service, including one whose entry the person deleted from the address book by hand: the link is stale, the sync deletes it and writes the card againAdd (default on sync), Hide
To updateLinked entry whose synced_hash differs from the current snapshot hashUpdate (default on sync), Show diff
To deleteLinked entry whose contact left the synced circles or was removedDelete (only if allow-delete, else Unlink), Keep. Unlink and Keep both leave the entry in the address book as the person’s own: Incico forgets the link (the sync_links row is deleted), so the row leaves the list and later syncs do not touch the entry
To linkService contact that matches an Incico contact (by hashed e-mail/phone) but is not linkedLink (merges: Incico fields update the entry, other fields untouched) is the default on sync only when exactly one match is a connection, otherwise the user picks; Ignore
To inviteService contact matched to an Incico user who is not a connectionInvite (→ 3.1.2.1), Hide
InvitedInvitation pending(none)
In syncLinked, hashes equal(none)
HiddenUser chose to ignoreUnhide

Each bucket row shows the service contact’s name and the Incico contact’s name where relevant. Multi-select and bulk actions are available in every bucket.

  1. Disconnect (danger): asks whether to keep or remove the entries Incico created in the service (device and Google support removing the Incico group); revokes the Google token at Google; deletes the service’s row in sync_services and, by cascade, every row it owns in sync_links.

3.3.1.3 Review changes

One list (round 2, variant 1), reached from Review and sync on the tab or from a service’s details. Title “3 changes for Google” and the line “marieke@gmail.com · gets Work and Sports club”; from the tab with more than one service waiting, one such block per service.

  • A section per bucket that has an action (“To update · 2”, “To add · 1”, “To delete”, “To link”), with Select none / Select all on the right.
  • Each change is a row with a checkbox, ticked by default where the sync would apply it by default (step 5 below), the person’s name, the field, and the change as the old value struck through, an arrow and the new value (“+31 6 1111 2222 → +31 6 4455 6677”; “none → f.elamrani@acme.nl”); a new contact says “New in Google · 3 details”. The circle label sits at the end.
  • Footer: Not now and Apply 3 changes, the count following the ticks. Apply runs 3.3.1.1 with exactly the ticked changes; an unticked change stays in its bucket for next time.

3.3.1.1 Synchronising (full screen)

Title “Syncing Phone contacts”, determinate progress bar, step text (“Reading contacts 120 of 300”, “Matching”, “Writing 8 changes”), Cancel (safe at any point: writes are per contact, links recorded as they succeed). End states: Done with a summary (added, updated, deleted, linked, still to review), Cancelled, Failed with the reason and Retry. No advertising (v2 planned a full-screen ad here).

Sync algorithm per run:

  1. Refresh Incico data (delta fetch) so cards are current.
  2. Read the service (incremental via change token when available).
  3. Match unlinked service contacts to Incico connections by hashed e-mail and E.164 phone; fill To link.
  4. Compute buckets from links, snapshot hashes, circle membership and allow_delete.
  5. Apply defaults: add and update automatically; delete only when allow-delete is on; link only when the match is unique.
  6. Record synced_hash, external_id, last_sync_at; upload sync_links state to the server for the other devices’ benefit.

Writes map snapshot fields to the provider’s schema: names (given/family split heuristically from the default name unless the person shared a structured name), phones with type labels, e-mails with labels, postal addresses, birthday (with or without year; providers that require a year receive none and skip the field), photo (first shared photo, downscaled), and a note “Kept up to date by Incico”.

Background: a second scheduled job per platform runs the same algorithm every 15 minutes, without UI and applying only the automatic actions — on Android a periodic WorkManager request constrained to an unmetered network and a battery that is not low, on iOS a BGProcessingTaskRequest (nl.sefaja.incico.services) that requires network connectivity, because BGAppRefreshTask cannot express either constraint. It is a separate job from the 15-minute delta fetch of Offline behaviour, whose constraints are deliberately looser, and it stands down while a foreground sync is running or when the Incico-side cache is more than 24 hours old (A75). Badges update.

3.3.1.2 Cross-reference (Find people on Incico)

The same lookup also runs over the phone’s own address book as the In your phone segment of Find people (Contacts §3.1.2, A106), where it needs only the contacts permission, not a connected service. v2’s sequential search becomes one batched, server-side lookup:

  1. Read the service’s contacts (name, e-mails, phones).
  2. Hash every e-mail (lower-cased) and phone (E.164) on device, with the rules pinned by docs/vectors/search-tokens.json; send them in batches of at most 200 tokens to cross_reference(p_tokens text[]), which returns the token that matched beside the profile it belongs to, so the device can say which of its own contacts a hit was for. Thirty calls an hour, which is six thousand tokens: a larger address book is checked over more than one sitting, and the screen says so rather than failing. search_profiles is not used here — it is capped at twenty tokens and sixty calls an hour and deliberately never says which token matched, both of which are right for the Find people screen and wrong for an address book (A71).
  3. Show results as they arrive: Already connected (green), On Incico with an Invite button and a circle picker preset from a default the user chooses at the top (“Put people in: Friends”), Invited. Contacts with no match are counted (“276 not on Incico”) but not listed (v2 removed them after 2 seconds; v3 never shows them).
  4. Bulk: “Invite all shown” with the preset circle. Each invite opens no extra screen; a snackbar confirms with Undo (cancels the invitation).
  5. Progress and results persist in the local cache so leaving and returning resumes (v2’s AsyncStorage state); a Restart icon clears it.

Names are not used for cross-reference matching (v2 used name as the third step) because whole-word name search of an entire address book produces false positives and is expensive; name search remains available manually in Find people.

Rules

  • Service configuration is per profile: a managed profile can have its own device-contacts service (a child’s details written into the parent’s phone), but Google services belong to whoever authorised them.
  • Contacts written into a service are marked as Incico-managed; Incico never modifies fields it did not write. The marker is a field on the entry, and which field depends on what the platform lets an app own: on Android it is the entry’s note row, prefixed incico=, beside membership of the “Incico” group; on iOS it is a social-profile entry on the contact (CNSocialProfile on the service “Incico”, carrying the profile id) beside membership of the Incico CNGroup, because reading or writing a CNContact note needs the restricted com.apple.developer.contacts.notes entitlement that this app does not request. Google Contacts uses the userDefined field incico=<profile_id>. An update or a delete reads the marker back first and refuses an entry that does not carry one; the single exception is adopting an entry the cross-reference matched, which by definition carries somebody else’s data and no marker until that write puts one there (A76, as built 2026-09-15).
  • Reading the address book never uploads anything but hashes for cross-reference; the raw address book stays on the device.
  • Device-contacts reads are always full reads. Both platforms ignore any stored change token for the phone’s own address book and read all of it every time: the address book is local, so a full read costs nothing over the wire, and it is the only way the reconciliation can tell that an entry Incico wrote was deleted by hand (a delta says nothing about what it did not mention). Google Contacts keeps its syncToken, and reports removals as metadata.deleted (as built 2026-09-15).
  • The server holds two tables about a service and nothing else: sync_services (provider, label, the device that holds the credential, the circles it syncs, allow_delete, last sync) and sync_links (which address-book entry stands for which connection, and the snapshot hash Incico last wrote). Provider change tokens and OAuth tokens are never among them (A21); a connection that is removed leaves its sync_links row behind with connection_id emptied, which is what puts the entry in the delete bucket on every device.
  • A sync writes links by upsert on (service_id, external_id). A removed contact leaves its link row behind with connection_id emptied but its external_id intact, so when that person reconnects while the entry Incico wrote is still in the address book, the app updates the orphaned row’s connection_id instead of inserting beside it: a second insert with the same pair is refused by the unique constraint (23505).
  • Pending-sync badges are computed per service from the circles it syncs (v1 E2E step 8): a contact’s change badges only the services whose circles include that contact.
  • Tokens: Google refresh tokens live in the secure store keyed google_refresh_<service_id>; disconnecting revokes them at Google.