Skip to content
Shiny.NET

Call Directory

NuGet package Shiny.AppDeviceBridge.CallDirectory

The call directory bridge lets the page block calls and label callers from a list of numbers it owns. It’s built on Shiny.Mobile.CallDirectory, which stores the list in SQLite on the device. iOS loads it through a Call Directory extension that the build generates, with no Xcode project, and Android checks every incoming call against it.

Platform Backed by
iOS A Call Directory extension (CallKit)
Android 10+ CallScreeningService, with the app holding the call screening role
Mac Catalyst, macOS, Windows, Linux nothing: every route answers 501
Terminal window
dotnet add package Shiny.AppDeviceBridge.CallDirectory
builder.UseAppDeviceBridge(
bridge => bridge.AddCallDirectoryBridge(o => o.DefaultCountryCode = 1),
webApp => { /* … */ }
);

On iOS, the app’s project file also builds the extension:

<PropertyGroup>
<ShinyCallDirectoryExtension>true</ShinyCallDirectoryExtension>
<ShinyCallDirectoryAppGroup>group.com.acme.app</ShinyCallDirectoryAppGroup>
</PropertyGroup>

AddCallDirectoryBridge(o => …) takes Shiny’s CallDirectoryOptions: the default country code, and on Android whether to block every unlisted caller and whether to post a caller-ID notification. An app that already calls services.AddCallDirectory(…) keeps its registration. See Shiny.Mobile.CallDirectory.

import { CallDirectoryBridge } from "@shinyorg/appdevicebridge";
const calls = new CallDirectoryBridge();
const { phoneNumber } = await calls.normalize({ number: "+1 (800) 555-0100" }); // 18005550100
await calls.set({ entries: [
{ phoneNumber, label: "Acme Support", type: "Identify" },
{ phoneNumber: 18005550199, label: null, type: "Block" }
] });
const { result } = await calls.reload();
if (result === "ExtensionDisabled")
await calls.requestAccess(); // iOS: opens Settings

In Blazor, register AddCallDirectoryBridgeClient() and inject ICallDirectoryBridge.

An entry is { phoneNumber, label, type }. phoneNumber is the country code and digits with no + or formatting, such as 18005550100; POST calldirectory/normalize turns a number as people write it into that form. type is:

Type What happens
Block The call is blocked.
Identify The call rings with label as the caller ID. On Android, where another app can’t change the dialer’s caller ID, a notification shows the label while it rings.
Allow Android: the call always rings, even when unlisted callers are blocked. iOS has no allow list and stores it as Identify.

PUT calldirectory/entries adds or replaces entries, POST calldirectory/entries/remove removes numbers, POST calldirectory/entries/replace swaps the whole list in one transaction (for a sync from your server), and DELETE calldirectory/entries clears it.

Edits are stored straight away, but iOS only sees them when the extension loads the list, which POST calldirectory/reload asks for. Make a batch of edits, then reload once. The result says how the load went: Success, ExtensionDisabled (the user hasn’t turned it on), CurrentlyLoading, MaximumEntriesExceeded, and so on. Android reads the list on every call, so there a reload only reports whether screening is on.

iOS has no permission prompt: the user turns the app on under Settings › Apps › Phone › Call Blocking & Identification. POST calldirectory/access opens Settings, as close to that page as iOS allows, and answers the current state, so check GET calldirectory again when the app comes back to the foreground. On Android it asks for the call screening role.

GET/_bridge/calldirectory#

Whether the call directory is turned on, and how many numbers it holds.

C#ICallDirectoryBridge.GetStatusAsyncTSCallDirectoryBridge.getStatus
Response
200 CallDirectoryStatus
CallDirectoryStatus · 2 fields
accessAccessStateiOS: whether the user turned the extension on in Settings › Apps › Phone › Call Blocking & Identification. Android: whether the app holds the call screening role.
countnumberHow many entries are stored.
POST/_bridge/calldirectory/access#

iOS has no prompt: this opens Settings (best effort) and answers the current state, so check again when the app is back in the foreground. Android asks for the call screening role.

C#ICallDirectoryBridge.RequestAccessAsyncTSCallDirectoryBridge.requestAccess
Response
200 CallDirectoryAccessResult
CallDirectoryAccessResult · 1 field
accessAccessState
GET/_bridge/calldirectory/entries/{phoneNumber}#

The stored entry for a number. Fails with 404 when there is none.

C#ICallDirectoryBridge.GetEntryAsyncTSCallDirectoryBridge.getEntry
ParameterInType
phoneNumberpathnumberrequired
Response
200 CallDirectoryEntry
CallDirectoryEntry · 3 fields
phoneNumbernumberCountry code and digits, no + or formatting: 18005550100. normalize converts a number as people write it.
labelstring | nullThe caller ID shown for Identify and Allow.
typeCallDirectoryEntryType
Errors
404Not found
PUT/_bridge/calldirectory/entries#

Adds or replaces entries.

C#ICallDirectoryBridge.SetAsyncTSCallDirectoryBridge.set
Body
application/json · CallDirectoryEntriesRequest
CallDirectoryEntriesRequest · 1 field
entriesCallDirectoryEntry[]
Response
204 No content
POST/_bridge/calldirectory/entries/remove#

Removes entries. Numbers that are not stored are ignored.

C#ICallDirectoryBridge.RemoveAsyncTSCallDirectoryBridge.remove
Body
application/json · CallDirectoryRemoveRequest
CallDirectoryRemoveRequest · 1 field
phoneNumbersnumber[]
Response
204 No content
POST/_bridge/calldirectory/entries/replace#

Replaces the whole list in one transaction — for a sync from a server.

C#ICallDirectoryBridge.ReplaceAllAsyncTSCallDirectoryBridge.replaceAll
Body
application/json · CallDirectoryEntriesRequest
CallDirectoryEntriesRequest · 1 field
entriesCallDirectoryEntry[]
Response
204 No content
DELETE/_bridge/calldirectory/entries#

Removes every entry.

C#ICallDirectoryBridge.ClearAsyncTSCallDirectoryBridge.clear
Response
204 No content
POST/_bridge/calldirectory/reload#

iOS: has the system load the stored list, and answers how it went. Android reads the list live, so it only reports whether screening is on.

C#ICallDirectoryBridge.ReloadAsyncTSCallDirectoryBridge.reload
Response
200 CallDirectoryReload
CallDirectoryReload · 1 field
resultCallDirectoryReloadResult
POST/_bridge/calldirectory/normalize#

Turns a number as people write it into the form the directory stores: formatting and a leading + or 00 dropped, and a country code added when it has none. Fails with 400 when it is not a phone number.

C#ICallDirectoryBridge.NormalizeAsyncTSCallDirectoryBridge.normalize
Body
application/json · NormalizeNumberRequest
NormalizeNumberRequest · 2 fields
numberstringA number as people write it: +1 (800) 555-0100, 800-555-0100, 0044 20 7946 0000.
defaultCountryCodenumber | nullThe country code for a number written without one; the app's configured default when null.
Response
200 NormalizedNumber
NormalizedNumber · 1 field
phoneNumbernumber
Errors
400Bad request

JSON property names are camelCase and enums travel as their names. A ? marks a property the payload may omit;| null one that may be null.

CallDirectoryStatusobject#
accessAccessStateiOS: whether the user turned the extension on in Settings › Apps › Phone › Call Blocking & Identification. Android: whether the app holds the call screening role.
countnumberHow many entries are stored.
AccessStateenum#

Where a permission stands. Reported by every bridge that needs one; the names match Shiny's own.

"Unknown""NotSupported""NotSetup""Disabled""Restricted""Denied""Available"
CallDirectoryEntryobject#

A number in the call directory.

phoneNumbernumberCountry code and digits, no + or formatting: 18005550100. normalize converts a number as people write it.
labelstring | nullThe caller ID shown for Identify and Allow.
typeCallDirectoryEntryType
CallDirectoryEntryTypeenum#

What happens to calls from a number.

"Block""Identify""Allow"
CallDirectoryRemoveRequestobject#
phoneNumbersnumber[]
CallDirectoryReloadResultenum#

How a reload went.

"Success""NotSupported""ExtensionDisabled""ExtensionNotFound""CurrentlyLoading""MaximumEntriesExceeded""LoadingInterrupted""Failed"
NormalizeNumberRequestobject#
numberstringA number as people write it: +1 (800) 555-0100, 800-555-0100, 0044 20 7946 0000.
defaultCountryCode?number | nullThe country code for a number written without one; the app's configured default when null.
NormalizedNumberobject#
phoneNumbernumber

Blocking calls is a device-level effect, so the bridge is behind the bridge policy like every other: callers on this device only, plus the launch session. Entries are validated (a number of at most 15 digits, a label of at most 256 characters) before they reach the store. See Security.