@cap-kit/people
v8.0.1
Published
Unified, high-performance contact management for Capacitor with zero-permission picking and capability-based access.
Readme
Install
pnpm add @cap-kit/people
# or
npm install @cap-kit/people
# or
yarn add @cap-kit/people
# then run:
npx cap syncApple Privacy Manifest
Apple mandates that app developers specify approved reasons for API usage to enhance user privacy.
This plugin includes a skeleton PrivacyInfo.xcprivacy file located in ios/Sources/PeoplePlugin/PrivacyInfo.xcprivacy.
You must populate this file if your plugin uses any Required Reason APIs.
Example: User Defaults
If your plugin uses UserDefaults, you must declare it in the manifest:
<dict>
<key>NSPrivacyAccessedAPIType</key>
<string>NSPrivacyAccessedAPICategoryUserDefaults</string>
<key>NSPrivacyAccessedAPITypeReasons</key>
<array>
<string>CA92.1</string>
</array>
</dict>
For detailed steps, please see the Capacitor Docs.
Configuration
Configuration options for the People plugin.
| Prop | Type | Description | Default | Since |
| -------------------- | -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------ | ----- |
| verboseLogging | boolean | Enables verbose native logging. When enabled, additional debug information is printed to the native console (Logcat on Android, Xcode on iOS). This option affects native logging behavior only and has no impact on the JavaScript API. | false | 8.0.0 |
Examples
In capacitor.config.json:
{
"plugins": {
"People": {
"verboseLogging": true
}
}
}In capacitor.config.ts:
/// <reference types="@cap-kit/people" />
import { CapacitorConfig } from '@capacitor/cli';
const config: CapacitorConfig = {
plugins: {
People: {
verboseLogging: true,
},
},
};
export default config;Permissions
Android
This plugin requires the following permissions be added to your AndroidManifest.xml:
<uses-permission android:name="android.permission.READ_CONTACTS" />
<uses-permission android:name="android.permission.WRITE_CONTACTS" />
Read about Setting Permissions in the Android Guide for more information on setting Android permissions.
iOS
To use the plugin on iOS, you need to add the following keys to your Info.plist file:
Contacts
NSContactsUsageDescription- Privacy - Contacts Usage Description
Read about Configuring Info.plist in the iOS Guide for more information on setting iOS permissions in Xcode.
Web
On the Web platform, only the zero-permission contact picker is supported via the Contact Picker API when available.
All systemic access operations (getContacts, getContact, searchPeople, CRUD operations, group management, and peopleChange listeners) are not implemented on Web and will reject as unimplemented.
✅ Correct Usage
All People plugin APIs are based on Promise and follow the standard Capacitor v8 reject paradigm. Use try / catch to handle native cancellations and errors.
import { People, PeopleErrorCode } from '@cap-kit/people';
try {
const { contact } = await People.pickContact({
projection: ['name', 'phones', 'emails'],
});
console.log('Picked contact:', contact);
} catch (err: any) {
if (err.code === PeopleErrorCode.CANCELLED) {
// User canceled selection
console.log('Picker cancelled');
} else {
console.error('Error:', err.message);
}
}❌ Incorrect Usage
Do not use checks based on the success property in the result, as the methods reject the Promise on error.
// ❌ DO NOT DO THIS
const result = await People.pickContact();
if (result.success) { ... }Error Handling
All People plugin methods can reject the Promise if they fail. It is recommended to handle standardized error codes using PeopleErrorCode.
Error Codes
All error codes are standardized and exposed via PeopleErrorCode:
UNAVAILABLE– Feature not available or OS limitationCANCELLED– User cancelled an interactive flow (e.g., contact picker)PERMISSION_DENIED– Permission denied or restrictedINIT_FAILED– Internal initialization or processing failureINVALID_INPUT– Invalid, missing, or malformed inputUNKNOWN_TYPE– Invalid or unsupported projection/type
These codes are consistent across iOS, Android, and Web.
Example
import { People, PeopleErrorCode } from '@cap-kit/people';
try {
const { contact } = await People.pickContact();
console.log('Contact selected:', contact);
} catch (err: any) {
switch (err.code) {
case PeopleErrorCode.CANCELLED:
// The user canceled the selection
console.log('Picker cancelled by user');
break;
case PeopleErrorCode.UNAVAILABLE:
// The user canceled the selection or the picker is not available
console.log('Picker unavailable or cancelled by user');
break;
case PeopleErrorCode.PERMISSION_DENIED:
// User has denied access to contacts (for methods that require permissions)
console.error('Permission to access contacts was denied');
break;
case PeopleErrorCode.INIT_FAILED:
// Internal error while processing native data
console.error('Native initialization or processing failure');
break;
default:
// Generic or unexpected error
console.error('An unexpected error occurred:', err.message);
break;
}
}API
createContact(...)updateContact(...)deleteContact(...)mergeContacts(...)listGroups()createGroup(...)deleteGroup(...)addPeopleToGroup(...)removePeopleFromGroup(...)checkPermissions()requestPermissions(...)pickContact(...)getContacts(...)getContact(...)getCapabilities()getPluginVersion()searchPeople(...)addListener('peopleChange', ...)addListener(string, ...)removeAllListeners()- Interfaces
- Type Aliases
Capacitor People plugin interface.
- This interface defines the contract between the JavaScript layer and the native implementations (Android and iOS).
createContact(...)
createContact(options: CreateContactOptions) => Promise<CreateContactResult>[CRUD] Creates a new contact in the device's address book.
- @throws {PeopleError} PERMISSION_DENIED if contacts permission is missing.
| Param | Type |
| ------------- | --------------------------------------------------------------------- |
| options | CreateContactOptions |
Returns: Promise<CreateContactResult>
Since: 8.0.0
Example
import { People } from '@cap-kit/people';
try {
const { contact } = await People.createContact({
contact: {
name: { given: 'John', family: 'Appleseed' },
emails: [{ address: '[email protected]', label: 'work' }],
},
});
console.log('Created contact ID:', contact.id);
} catch (error) {
console.error('Failed to create contact:', error.code);
}updateContact(...)
updateContact(options: UpdateContactOptions) => Promise<UpdateContactResult>[CRUD] Updates an existing contact using a patch-based approach.
- @throws {PeopleError} PERMISSION_DENIED if permission is missing.
| Param | Type |
| ------------- | --------------------------------------------------------------------- |
| options | UpdateContactOptions |
Returns: Promise<UpdateContactResult>
Since: 8.0.0
Example
import { People } from '@cap-kit/people';
try {
const { contact } = await People.updateContact({
contactId: 'some-contact-id',
contact: {
organization: { company: 'New Company Inc.' },
},
});
console.log('Updated contact company:', contact.organization?.company);
} catch (error) {
console.error('Update failed:', error.message);
}deleteContact(...)
deleteContact(options: DeleteContactOptions) => Promise<void>[CRUD] Deletes a contact from the device's address book. Only contacts owned by the app can be deleted.
- @throws {PeopleError} UNAVAILABLE if deletion fails or contact is not app-owned.
| Param | Type |
| ------------- | --------------------------------------------------------------------- |
| options | DeleteContactOptions |
Since: 8.0.0
Example
import { People } from '@cap-kit/people';
await People.deleteContact({ contactId: 'contact-id-to-delete' });mergeContacts(...)
mergeContacts(options: MergeContactsOptions) => Promise<MergeContactsResult>[CRUD] Merges a source contact into a destination contact. The source contact is deleted after the merge.
- @throws {PeopleError} PERMISSION_DENIED if permission is missing.
| Param | Type |
| ------------- | --------------------------------------------------------------------- |
| options | MergeContactsOptions |
Returns: Promise<MergeContactsResult>
Since: 8.0.0
Example
import { People } from '@cap-kit/people';
const { contact } = await People.mergeContacts({
sourceContactId: 'duplicate-contact-id',
destinationContactId: 'main-contact-id',
});
console.log('Final contact state:', contact);listGroups()
listGroups() => Promise<ListGroupsResult>[GROUPS] Lists all available contact groups.
Returns: Promise<ListGroupsResult>
Since: 8.0.0
Example
const { groups } = await People.listGroups();createGroup(...)
createGroup(options: CreateGroupOptions) => Promise<CreateGroupResult>[GROUPS] Creates a new contact group.
- @example
const { group } = await People.createGroup({ name: 'Family' });| Param | Type |
| ------------- | ----------------------------------------------------------------- |
| options | CreateGroupOptions |
Returns: Promise<CreateGroupResult>
Since: 8.0.0
deleteGroup(...)
deleteGroup(options: DeleteGroupOptions) => Promise<void>[GROUPS] Deletes a contact group.
| Param | Type |
| ------------- | ----------------------------------------------------------------- |
| options | DeleteGroupOptions |
Since: 8.0.0
Example
import { People } from '@cap-kit/people';
await People.deleteGroup({ groupId: 'group-id-to-delete' });addPeopleToGroup(...)
addPeopleToGroup(options: AddPeopleToGroupOptions) => Promise<void>[GROUPS] Adds contacts to a group.
| Param | Type |
| ------------- | --------------------------------------------------------------------------- |
| options | AddPeopleToGroupOptions |
Since: 8.0.0
Example
import { People } from '@cap-kit/people';
await People.addPeopleToGroup({
groupId: 'group-id',
contactIds: ['contact-id-1', 'contact-id-2'],
});removePeopleFromGroup(...)
removePeopleFromGroup(options: RemovePeopleFromGroupOptions) => Promise<void>[GROUPS] Removes contacts from a group.
| Param | Type |
| ------------- | ------------------------------------------------------------------------------------- |
| options | RemovePeopleFromGroupOptions |
Since: 8.0.0
Example
import { People } from '@cap-kit/people';
await People.removePeopleFromGroup({
groupId: 'group-id',
contactIds: ['contact-id-1'],
});checkPermissions()
checkPermissions() => Promise<PeoplePluginPermissions>Check the status of permissions.
Returns: Promise<PeoplePluginPermissions>
Since: 8.0.0
Example
import { People } from '@cap-kit/people';
const permissions = await People.checkPermissions();
console.log(permissions.contacts); // Output: 'granted' | 'denied' | 'prompt'requestPermissions(...)
requestPermissions(permissions?: { permissions: 'contacts'[]; } | undefined) => Promise<PeoplePluginPermissions>Request permissions.
| Param | Type | Description |
| ----------------- | ------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- |
| permissions | { permissions: 'contacts'[]; } | - An optional object specifying which permissions to request. If not provided, all permissions defined in the plugin will be requested. |
Returns: Promise<PeoplePluginPermissions>
Since: 8.0.0
Example
import { People } from '@cap-kit/people';
const permissions = await People.requestPermissions();
// OR
// const permissions = await People.requestPermissions({ permissions: ['contacts'] });
console.log(permissions.contacts); // Output: 'granted' | 'denied'pickContact(...)
pickContact(options?: { projection?: PeopleProjection[] | undefined; } | undefined) => Promise<PickContactResult>[ZERO-PERMISSION] Launches the native OS contact picker UI. This method does NOT require any entries in AndroidManifest.xml or Info.plist as the user explicitly selects the data via the system UI.
| Param | Type |
| ------------- | ------------------------------------------------- |
| options | { projection?: PeopleProjection[]; } |
Returns: Promise<PickContactResult>
Since: 8.0.0
Example
try {
const { contact } = await People.pickContact({
projection: ['name', 'phones', 'emails']
});
console.log('User selected:', contact);
} catch (error) {
if (error.code === 'CANCELLED') {
console.log('User cancelled the picker.');
}
}getContacts(...)
getContacts(options?: GetContactsOptions | undefined) => Promise<GetContactsResult>[SYSTEMIC-ACCESS]
Queries the entire contact database with specific projection and pagination.
REQUIRES 'contacts' permission.
Use includeTotal only when needed: computing totalCount may require scanning/counting across the full contacts set and can be expensive on large address books. Default is false.
| Param | Type |
| ------------- | ----------------------------------------------------------------- |
| options | GetContactsOptions |
Returns: Promise<GetContactsResult>
Since: 8.0.0
Example
const result = await People.getContacts({
projection: ['name', 'phones'],
limit: 20,
offset: 0
});getContact(...)
getContact(options: { id: string; projection?: PeopleProjection[]; }) => Promise<{ contact: UnifiedContact; }>Retrieves a single contact by ID.
- @throws {PeopleError} UNAVAILABLE if contact is not found.
| Param | Type |
| ------------- | ------------------------------------------------------------- |
| options | { id: string; projection?: PeopleProjection[]; } |
Returns: Promise<{ contact: UnifiedContact; }>
Since: 8.0.0
Example
import { People } from '@cap-kit/people';
const { contact } = await People.getContact({ id: 'contact-id', projection: ['name', 'emails'] });
console.log('Contact details:', contact);getCapabilities()
getCapabilities() => Promise<PeopleCapabilities>Returns what this device/implementation is capable of. Useful for UI adaptation (e.g. hiding "Edit" buttons).
Returns: Promise<PeopleCapabilities>
Since: 8.0.0
Example
import { People } from '@cap-kit/people';
const capabilities = await People.getCapabilities();
console.log('Can Read Contacts:', capabilities.canRead);
console.log('Can Write Contacts:', capabilities.canWrite);getPluginVersion()
getPluginVersion() => Promise<PluginVersionResult>Returns the native plugin version.
The returned version corresponds to the native implementation bundled with the application.
Returns: Promise<PluginVersionResult>
Since: 8.0.0
Example
const { version } = await People.getPluginVersion();searchPeople(...)
searchPeople(options: { query: string; projection?: PeopleProjection[]; limit?: number; }) => Promise<GetContactsResult>[SYSTEMIC-ACCESS] Searches the database with projection. REQUIRES 'contacts' permission.
| Param | Type |
| ------------- | -------------------------------------------------------------------------------- |
| options | { query: string; projection?: PeopleProjection[]; limit?: number; } |
Returns: Promise<GetContactsResult>
Since: 8.0.0
Example
import { People } from '@cap-kit/people';
const result = await People.searchPeople({ query: 'John', projection: ['name', 'phones'], limit: 10 });
console.log('Fetched contacts:', result.contacts);addListener('peopleChange', ...)
addListener(eventName: 'peopleChange', listenerFunc: (payload: PeopleChangeEvent) => void) => Promise<PluginListenerHandle>Listen for changes in the system address book. REQUIRES 'contacts' permission.
- @returns A promise that resolves to a handle to remove the listener.
| Param | Type |
| ------------------ | ------------------------------------------------------------------------------------- |
| eventName | 'peopleChange' |
| listenerFunc | (payload: PeopleChangeEvent) => void |
Returns: Promise<PluginListenerHandle>
Since: 8.0.0
Example
import { People } from '@cap-kit/people';
const handle = await People.addListener('peopleChange', (event) => {
console.log('People change detected:', event.type);
});
// To remove the listener later:
// await handle.remove();addListener(string, ...)
addListener(eventName: string, listenerFunc: (...args: unknown[]) => void) => Promise<PluginListenerHandle>Registers a listener for plugin events using a generic event name.
Prefer the typed peopleChange overload for full payload type safety.
| Param | Type |
| ------------------ | -------------------------------------------- |
| eventName | string |
| listenerFunc | (...args: unknown[]) => void |
Returns: Promise<PluginListenerHandle>
Since: 8.0.0
removeAllListeners()
removeAllListeners() => Promise<void>Removes all registered listeners for this plugin.
Since: 8.0.0
Example
import { People } from '@cap-kit/people';
await People.removeAllListeners();Interfaces
CreateContactResult
Result returned by createContact.
| Prop | Type | Description |
| ------------- | --------------------------------------------------------- | --------------------------------------------------------------------- |
| contact | UnifiedContact | The full contact object as it was saved, including its new unique ID. |
UnifiedContact
Represents a Unified Person in the directory. Maps to CNContact (iOS) and Aggregated Contact (Android).
| Prop | Type | Description |
| ------------------ | --------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------ |
| id | string | The platform-specific unique identifier (UUID or Long) |
| name | { display: string; given?: string; middle?: string; family?: string; prefix?: string; suffix?: string; } | The unified display name |
| organization | { company?: string; title?: string; department?: string; } | |
| birthday | { year?: number; month: number; day: number; } | |
| phones | PhoneNumber[] | |
| emails | EmailAddress[] | |
| addresses | PostalAddress[] | |
| urls | string[] | |
| note | string | |
| image | string | Base64 thumbnail string (iOS only, only if projected). |
PhoneNumber
Phone number representation.
| Prop | Type | Description |
| ---------------- | ------------------- | ------------------------------------------------- |
| label | string | Normalized label (e.g., 'mobile', 'home', 'work') |
| number | string | The raw input string |
| normalized | string | E.164 formatted number (if parsing succeeded) |
EmailAddress
Email address representation.
| Prop | Type |
| ------------- | ------------------- |
| label | string |
| address | string |
PostalAddress
Postal address representation.
| Prop | Type |
| --------------- | ------------------- |
| label | string |
| formatted | string |
| street | string |
| city | string |
| region | string |
| postcode | string |
| country | string |
CreateContactOptions
Options for creating a new contact. The contact data is provided as a partial UnifiedContact.
| Prop | Type | Description |
| ------------- | ------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------- |
| contact | Partial<Omit<UnifiedContact, 'id'>> | The contact data to be saved. At least one writable field (e.g., name, email) must be provided. |
UpdateContactResult
Result returned by updateContact.
| Prop | Type | Description |
| ------------- | --------------------------------------------------------- | ---------------------------------------------------------- |
| contact | UnifiedContact | The full contact object after the update has been applied. |
UpdateContactOptions
Options for updating an existing contact. This operation performs a patch, not a full replacement.
| Prop | Type | Description |
| --------------- | ------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------- |
| contactId | string | The unique identifier of the contact to update. |
| contact | Partial<Omit<UnifiedContact, 'id'>> | An object containing the fields to be updated. Only the provided fields will be modified. |
DeleteContactOptions
Options for deleting a contact.
| Prop | Type | Description |
| --------------- | ------------------- | ----------------------------------------------- |
| contactId | string | The unique identifier of the contact to delete. |
MergeContactsResult
Result returned by mergeContacts.
| Prop | Type | Description |
| ------------- | --------------------------------------------------------- | ----------------------------------------------------- |
| contact | UnifiedContact | The state of the destination contact after the merge. |
MergeContactsOptions
Options for merging two contacts.
| Prop | Type | Description |
| -------------------------- | ------------------- | ---------------------------------------------------------------- |
| sourceContactId | string | The identifier of the contact that will be subsumed and deleted. |
| destinationContactId | string | The identifier of the contact that will be kept and updated. |
ListGroupsResult
Result returned by listGroups.
| Prop | Type | Description |
| ------------ | -------------------- | --------------------------------------------- |
| groups | Group[] | An array of groups found in the address book. |
Group
Represents a group in the address book. A group can be system-generated (e.g., "All Contacts") or user-created.
| Prop | Type | Description |
| -------------- | -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| id | string | A unique identifier for the group. This ID is stable and can be used for subsequent operations. |
| name | string | The display name of the group. e.g., "Family", "Work", "Book Club" |
| source | string | The source or account where the group originates. On iOS, this could be "iCloud" or "Local". On Android, this corresponds to the account name (e.g., "[email protected]"). For logical groups, this will be 'local'. |
| readOnly | boolean | Indicates if the group is read-only. System groups are typically read-only and cannot be deleted or renamed. |
CreateGroupResult
Result returned by createGroup.
| Prop | Type | Description |
| ----------- | --------------------------------------- | ------------------------ |
| group | Group | The newly created group. |
CreateGroupOptions
Options for creating a new group.
| Prop | Type | Description |
| ---------- | ------------------- | --------------------------- |
| name | string | The name for the new group. |
DeleteGroupOptions
Options for deleting a group.
| Prop | Type | Description |
| ------------- | ------------------- | --------------------------------------------- |
| groupId | string | The unique identifier of the group to delete. |
AddPeopleToGroupOptions
Options for adding people to a group.
| Prop | Type | Description |
| ---------------- | --------------------- | ---------------------------------------------------- |
| groupId | string | The unique identifier of the group. |
| contactIds | string[] | An array of contact identifiers to add to the group. |
RemovePeopleFromGroupOptions
Options for removing people from a group.
| Prop | Type | Description |
| ---------------- | --------------------- | --------------------------------------------------------- |
| groupId | string | The unique identifier of the group. |
| contactIds | string[] | An array of contact identifiers to remove from the group. |
PeoplePluginPermissions
Permissions status interface.
| Prop | Type |
| -------------- | ----------------------------------------------------------- |
| contacts | PermissionState |
PickContactResult
Result returned by pickContact(). Now strictly returns the contact object on success.
| Prop | Type |
| ------------- | --------------------------------------------------------- |
| contact | UnifiedContact |
GetContactsResult
Result returned by getContacts().
| Prop | Type | Description |
| ---------------- | ----------------------------- | ---------------------------------------------- |
| contacts | UnifiedContact[] | |
| totalCount | number | Total count in the DB (permissions permitting) |
GetContactsOptions
Options for querying contacts.
| Prop | Type | Description | Default |
| ------------------ | ------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ | --------------------- |
| projection | PeopleProjection[] | Array of fields to fetch. MISSING fields will not be read from DB (Performance). | ['name'] |
| limit | number | Max number of records to return | |
| offset | number | Skip count (implement pagination via cursor usually better, but offset for now) | |
| includeTotal | boolean | Whether to compute totalCount across the full contacts set. This may require scanning/counting the full address book and can be expensive on large datasets. | false |
PeopleCapabilities
Capabilities of the People plugin on this device/implementation.
| Prop | Type |
| --------------------- | -------------------- |
| canRead | boolean |
| canWrite | boolean |
| canObserve | boolean |
| canManageGroups | boolean |
| canPickContact | boolean |
PluginVersionResult
Result object returned by the getPluginVersion() method.
| Prop | Type | Description |
| ------------- | ------------------- | --------------------------------- |
| version | string | The native plugin version string. |
PluginListenerHandle
| Prop | Type |
| ------------ | ----------------------------------------- |
| remove | () => Promise<void> |
PeopleChangeEvent
Event emitted when changes are detected in the device's address book.
| Prop | Type | Description |
| ---------- | ------------------------------------------------------------- | ------------------------------------------------------------ |
| ids | string[] | Array of affected contact IDs (always present, may be empty) |
| type | PeopleChangeType | The type of change detected |
Type Aliases
PermissionState
'prompt' | 'prompt-with-rationale' | 'granted' | 'denied'
PeopleProjection
Supported fields for the Projection Engine.
Requesting only what you need reduces memory usage by O(N).
image projection support is iOS-only. On Android and Web, requesting image is rejected with UNKNOWN_TYPE and message Unsupported projection field: image.
'name' | 'organization' | 'birthday' | 'phones' | 'emails' | 'addresses' | 'urls' | 'image' | 'note'
PeopleChangeType
Payload delivered to listeners registered for "peopleChange".
ids: always present, contains changed contact IDs (may be empty).type: one of 'insert' | 'update' | 'delete' (default 'update'). Current native implementations emitupdate.
'insert' | 'update' | 'delete'
Contributing
Contributions are welcome! Please read the contributing guide before submitting a pull request.
License
MIT
