Native mobile app: CRM Caller ID and post-call logging for inbound/outbound phone calls #23479
Angel90000
started this conversation in
Ideas
Replies: 1 comment
|
can i do this? |
0 replies
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
I'd like Twenty to identify inbound phone calls against CRM records on a salesperson's phone, and to capture the call afterwards as an activity on the record. HubSpot's mobile app does this today and it's the single feature I miss most when moving a team onto Twenty.
Two capabilities, both of which need a native mobile client:
Out of scope: the rest of the mobile app surface (record browsing, editing, views). This assumes a native shell exists or is being built, so #6273 is a dependency. If you'd rather have it in smaller pieces, it splits cleanly into
mobile: caller ID directory syncandmobile: post-call logging.Also out of scope: VoIP/softphone calling inside Twenty, i.e. the Twilio and Zoom Phone style integrations discussed in #16735. This is about the calls people already make from their handset.
Worth saying why this is different from most mobile requests: it can't be built into the responsive web app at all. Only a native app can register with CallKit on iOS or the telephony broadcast/screening APIs on Android. So it's one of the few features that justifies a native client on its own merits, and it removes the main reason reps end up duplicating prospects into their personal address book.
Current behavior
Twenty has no native iOS/Android client. Mobile access is the responsive web app, which the OS never consults during an incoming call.
So when someone already in the workspace calls a user's phone, the dialler shows a bare number. The user either recognises it, declines and searches Twenty manually, or saves the contact into their personal phone address book, which puts CRM data outside the workspace and outside its permission model.
Calls are also never captured. To record that a call happened, the user opens Twenty later on desktop, finds the record, and writes a Note by hand. In practice most calls never get logged, so the timeline systematically under-represents actual activity.
Two structural gaps underneath this:
Person.phonesis a compositePHONESfield (primaryPhoneNumber,primaryPhoneCallingCode,primaryPhoneCountryCode,additionalPhones), stored per record and not normalised to a single indexed E.164 string. There's no efficient "who owns this number?" query.Callconcept in the data model. Standard objects are People, Companies, Opportunities, Notes, Tasks, Messages, Calendar Events and Attachments. A phone call has nowhere canonical to live.Expected behavior
How HubSpot does it
I dug into HubSpot's implementation before writing this, because the platform constraints shape the design more than I expected and they aren't obvious up front.
On iOS, HubSpot registers a CallKit Call Directory extension. Per Apple's CallKit documentation, when a call comes in the system first checks the user's own contacts, and only consults the app's Call Directory extension if there's no match. The important part: the extension is invoked when the system launches it, not per call, so all identification data has to be handed over in advance. A per-call network lookup is simply not possible. HubSpot formats its stored contacts and pushes them to the system each time the app starts. The consequence is that iOS is limited to first name, last name and company name, and a missed call leaves that label in the native call log.
On Android there's no such constraint. HubSpot uses a
BroadcastReceiveron phone-state changes and draws its own overlay, so it can show first name, last name, company name, deal name and deal amount. Their engineering blog mentions they originally hit the backend on every incoming call, found it slow and unreliable, and moved to an on-device cache synced daily, falling back to the backend only on a cache miss. Missed calls generate a notification with "view contact" and "call back".Common to both: contacts auto-sync every 24 hours, with a manual sync button under Settings → Calling → Caller ID. Matching runs off their Phone number and Mobile number property types. If the person is already in the phone's own address book, the OS contact wins. They document that it works best under roughly 10,000 contacts, and that numbers should be stored as
+<country code><number>with no spaces or parentheses. On iOS the user also has to enable the app under Settings → Apps → Phone → Call Blocking & Identification. After a call, a "Log calls" screen (Android and Xiaomi only) lets the user log it with an outcome, notes, or a follow-up task.The iOS/Android asymmetry is inherent to the platforms rather than a HubSpot shortcoming, so any implementation here will hit the same split.
Proposed behavior in Twenty
Enabling. Settings → Calling → Caller ID in the mobile app, with toggles for Caller ID and Log calls, a manual "Sync now" action, and a line showing last sync time and number of records synced. iOS additionally needs to point the user at Settings → Apps → Phone → Call Blocking & Identification, ideally with a deep link. Android requests
READ_PHONE_STATE,READ_CALL_LOG,ANSWER_PHONE_CALLSand overlay permission, each with an in-app explanation shown before the system dialog.Directory sync. On login, on app foreground, and on a 24-hour background schedule, the app pulls a compact caller-ID directory scoped to what the signed-in workspace member is permitted to see. Delta sync via an
updatedSincecursor, full rebuild on first run or workspace switch. Numbers normalised to E.164 server-side using the record's calling code, falling back to the workspace default. The directory stays on-device, and the phone's own address book is never read or uploaded.Incoming call, iOS. The Call Directory extension has already published the number-to-label mapping, so the dialler shows
Jane Doe · Acme Incwith no network round trip. The label is a single string, so the format has to be{firstName} {lastName} · {companyName}, truncated to fit. A missed call keeps that label in the iOS call log.Incoming call, Android. An overlay above the dialler showing name, company, and the primary open opportunity with its amount, plus an "Open in Twenty" action. On a cache miss with connectivity, fall back to a live server lookup; otherwise show nothing and let the OS behave normally. Missed calls produce a notification with "View record" and "Call back".
After the call. If Log calls is on and the number matched a record, a post-call screen offering an outcome (connected / no answer / left voicemail / wrong number / busy), duration prefilled, an optional note body, and an optional follow-up task with a due date. Confirming writes a call activity linked to the matched Person, and to their Company and open Opportunity where applicable. Dismissing writes nothing, and the screen should never block the dialler. Outbound calls placed from the phone to a known record get the same prompt.
Unmatched numbers. The OS behaves exactly as it does today. The post-call screen could optionally offer "Create person in Twenty" with the number prefilled, though that's fine as a follow-up.
Technical inputs
Suggestions rather than prescriptions, since you'll know better where things belong in the codebase.
Data model
The first thing to settle is how a call is represented. Options as I see them: a new standard
callobject (direction,outcome,startedAt,durationSeconds,phoneNumber,body, relations to Person/Company/Opportunity/WorkspaceMember); atimelineActivityof typecallwith no first-class object; or leaving it to a custom object via the SDK'sdefineObject. I'd argue for the first, because the same object is what any future Twilio, Zoom Phone or Quo integration would want to write into (#16735), so building it now avoids a migration later. This is the main product decision blocking the rest of the ticket.Second, a normalised phone index. Some E.164 representation alongside the composite
PHONESfield, either a generated column on the workspace schema or a lookup table maintained on write, with a btree index. Plus a secondary index on the last 7–9 digits for suffix matching when a caller's number arrives without a country code. Without this, both the sync endpoint and the server-side fallback lookup are full scans.Backend
GET /rest/mobile/caller-id-directory?updatedSince=<ISO8601>&cursor=<...>, returning a minimal payload per entry:{ e164, firstName, lastName, companyName, opportunityName, opportunityAmount, recordId, objectNameSingular, updatedAt }.syncTokenso clients can resume.GET /rest/mobile/caller-id-lookup?e164=<...>, rate-limited.Mobile client
packages/twenty-mobile(React Native + Expo, TypeScript), reusingtwenty-sharedand the generated GraphQL types so field changes propagate. Server URL supplied by the user at login so self-hosted instances work, which is non-negotiable for this project's user base.expo-sqlite, tablecaller_id_directory, unique index one164and an index on the digit suffix. Nothing stored beyond what the directory payload contains.expo-background-taskon iOS and WorkManager on Android, manual sync from settings. Surface the last-sync timestamp and entry count prominently — HubSpot users have complained for years about silent sync failures, and it seems worth designing against that from the start.iOS specifics
CXCallDirectoryProviderin a Call Directory app extension.CXCallDirectoryManager.reloadExtension(withIdentifier:).addIdentificationEntry/removeIdentificationEntryinbeginRequest) over full rebuilds, which get slow at scale.Android specifics
CallScreeningService(API 29+) where available, falling back to aBroadcastReceiveronandroid.intent.action.PHONE_STATE.SYSTEM_ALERT_WINDOW, or a full-screen intent notification where overlay permission is denied.READ_CALL_LOGandANSWER_PHONE_CALLSare sensitive permissions requiring a declaration form and a demo video. Better to know that before submission than during.Matching logic
libphonenumber(libphonenumber-json the JS side), using the device SIM region first and the workspace default calling code as a fallback.primaryPhoneNumberandadditionalPhones, on People and on Companies.Documentation
A docs page covering enabling per platform, sync behaviour, matching rules, the "store numbers as
+15551234567" guidance, and what data lives on the device.Notes
Happy to write the flows up in more detail, mock the screens, or contribute to the implementation if the direction is accepted.
One piece of prior art worth a look: TwentyMobile (AGPL-3.0), an independent native client that came out of #6273. It doesn't implement caller ID, but the app-shell groundwork is there.
All reactions