diff --git a/astro.config.mjs b/astro.config.mjs
index bc5b58c46..5ec6c4e36 100644
--- a/astro.config.mjs
+++ b/astro.config.mjs
@@ -363,6 +363,11 @@ export default defineConfig({
description: 'in-app review prompt plugin for app store ratings',
paths: ['docs/plugins/in-app-review/**'],
},
+ {
+ label: 'Plugin Incoming Call Kit',
+ description: 'native incoming call UI plugin with iOS CallKit and Android full-screen notifications',
+ paths: ['docs/plugins/incoming-call-kit/**'],
+ },
{
label: 'Plugin Intent Launcher',
description: 'Android intent launcher plugin',
@@ -1089,6 +1094,16 @@ export default defineConfig({
],
collapsed: true,
},
+ {
+ label: 'Incoming Call Kit',
+ items: [
+ { label: 'Overview', link: '/docs/plugins/incoming-call-kit/' },
+ { label: 'Getting started', link: '/docs/plugins/incoming-call-kit/getting-started' },
+ { label: 'iOS', link: '/docs/plugins/incoming-call-kit/ios' },
+ { label: 'Android', link: '/docs/plugins/incoming-call-kit/android' },
+ ],
+ collapsed: true,
+ },
{
label: 'Intercom',
items: [
diff --git a/public/icons/plugins/incoming-call-kit.svg b/public/icons/plugins/incoming-call-kit.svg
new file mode 100644
index 000000000..e8b3c04a3
--- /dev/null
+++ b/public/icons/plugins/incoming-call-kit.svg
@@ -0,0 +1,6 @@
+
diff --git a/src/config/plugins.ts b/src/config/plugins.ts
index 7a3977619..ac619ec0d 100644
--- a/src/config/plugins.ts
+++ b/src/config/plugins.ts
@@ -484,6 +484,14 @@ export const actions = [
title: 'Text Interaction',
icon: 'DocumentText',
},
+ {
+ name: '@capgo/capacitor-incoming-call-kit',
+ author: 'github.com/Cap-go',
+ description: 'Present native incoming call UI with iOS CallKit and Android full-screen notifications',
+ href: 'https://github.com/Cap-go/capacitor-incoming-call-kit/',
+ title: 'Incoming Call Kit',
+ icon: 'Phone',
+ },
{
name: '@capgo/capacitor-twilio-voice',
author: 'github.com/Cap-go',
diff --git a/src/content/docs/docs/plugins/incoming-call-kit/android.mdx b/src/content/docs/docs/plugins/incoming-call-kit/android.mdx
new file mode 100644
index 000000000..c9b5659f0
--- /dev/null
+++ b/src/content/docs/docs/plugins/incoming-call-kit/android.mdx
@@ -0,0 +1,75 @@
+---
+title: Android
+description: Configure notifications, full-screen intents, and Android-specific incoming call behavior.
+sidebar:
+ order: 4
+---
+
+## How Android behavior works
+
+On Android, the plugin posts a high-priority incoming-call notification and can raise a full-screen activity when the platform and user settings allow it.
+
+The plugin manifest already includes:
+
+```xml
+
+
+```
+
+After installation, `bunx cap sync` is enough to merge that configuration into your host app.
+
+## Runtime permissions
+
+Call these methods during onboarding or before you rely on incoming-call presentation:
+
+```ts
+import { IncomingCallKit } from '@capgo/capacitor-incoming-call-kit';
+
+await IncomingCallKit.requestPermissions();
+await IncomingCallKit.requestFullScreenIntentPermission();
+```
+
+- `requestPermissions()` requests notification permission on Android 13 and later.
+- `requestFullScreenIntentPermission()` opens the Android 14 and later settings page for full-screen intents when needed.
+
+## Basic example
+
+```ts
+await IncomingCallKit.showIncomingCall({
+ callId: 'call-42',
+ callerName: 'Ada Lovelace',
+ appName: 'Capgo Phone',
+ timeoutMs: 45_000,
+ android: {
+ channelId: 'calls',
+ channelName: 'Incoming Calls',
+ showFullScreen: true,
+ isHighPriority: true,
+ accentColor: '#0F766E',
+ },
+});
+```
+
+## Android-specific options
+
+- `channelId`: identifier for the notification channel.
+- `channelName`: user-visible channel name.
+- `showFullScreen`: request the full-screen activity.
+- `isHighPriority`: keep the notification disruptive enough for ringing flows.
+- `accentColor`: tint compatible notification surfaces.
+- `ringtoneUri`: point at a custom Android ringtone resource or URI.
+
+## Behavior notes
+
+- Full-screen presentation is best-effort. If the device or user settings block it, Android still shows the incoming-call notification.
+- Timeout handling is also best-effort. The plugin tracks `timeoutMs` and emits `callTimedOut`, but your backend should still reconcile missed calls on its side.
+- Accept, decline, and end actions are emitted back through Capacitor listeners so your app can join or clean up the real call session.
+
+## Recommended production model
+
+Use Android push or your calling SDK for transport, then let this plugin handle the last mile of native ringing UI. Keep these responsibilities outside the plugin:
+
+- FCM registration and token management
+- Media session lifecycle
+- Backend call state
+- Retry and missed-call business logic
diff --git a/src/content/docs/docs/plugins/incoming-call-kit/getting-started.mdx b/src/content/docs/docs/plugins/incoming-call-kit/getting-started.mdx
new file mode 100644
index 000000000..85e5f57a3
--- /dev/null
+++ b/src/content/docs/docs/plugins/incoming-call-kit/getting-started.mdx
@@ -0,0 +1,109 @@
+---
+title: Getting Started
+description: Install and wire incoming call presentation in Capacitor with a small, transport-agnostic API.
+sidebar:
+ order: 2
+---
+
+## Install
+
+```bash
+bun add @capgo/capacitor-incoming-call-kit
+bunx cap sync
+```
+
+## Decide where the ring event comes from
+
+Before writing code, decide how your app learns that someone is calling:
+
+1. A foreground JavaScript event from your backend.
+2. An Android FCM notification or iOS PushKit/APNs flow.
+3. A vendor SDK such as Twilio, Stream, SIP, or your own native layer.
+
+When that ring event reaches your app, call `showIncomingCall()`. When the user accepts, join your real voice or video session from your own SDK or backend flow.
+
+## Minimal integration
+
+```ts
+import { IncomingCallKit } from '@capgo/capacitor-incoming-call-kit';
+
+await IncomingCallKit.requestPermissions();
+await IncomingCallKit.requestFullScreenIntentPermission();
+
+await IncomingCallKit.addListener('callAccepted', async ({ call }) => {
+ console.log('Accepted', call.callId, call.extra);
+ // Start or join your real call session here.
+});
+
+await IncomingCallKit.addListener('callDeclined', ({ call }) => {
+ console.log('Declined', call.callId);
+ // Tell your backend or SDK that the user declined.
+});
+
+await IncomingCallKit.addListener('callTimedOut', ({ call }) => {
+ console.log('Timed out', call.callId);
+ // Clear ringing state in your backend or SDK.
+});
+
+await IncomingCallKit.showIncomingCall({
+ callId: 'call-42',
+ callerName: 'Ada Lovelace',
+ handle: '+39 555 010 020',
+ appName: 'Capgo Phone',
+ hasVideo: true,
+ timeoutMs: 45_000,
+ extra: {
+ roomId: 'room-42',
+ callerUserId: 'user_ada',
+ },
+ android: {
+ channelId: 'calls',
+ channelName: 'Incoming Calls',
+ showFullScreen: true,
+ },
+ ios: {
+ handleType: 'phoneNumber',
+ },
+});
+```
+
+## Important options
+
+- `callId`: stable identifier you reuse later with `endCall()`.
+- `timeoutMs`: best-effort unanswered timeout.
+- `extra`: arbitrary JSON echoed back in all listener payloads.
+- `android.channelId` and `android.channelName`: tune the Android notification channel.
+- `android.showFullScreen`: request the Android full-screen incoming-call activity.
+- `ios.handleType`: choose `generic`, `phoneNumber`, or `emailAddress` for CallKit.
+
+## Managing active calls
+
+```ts
+const { calls } = await IncomingCallKit.getActiveCalls();
+
+await IncomingCallKit.endCall({
+ callId: 'call-42',
+ reason: 'remote-ended',
+});
+
+await IncomingCallKit.endAllCalls({
+ reason: 'session-reset',
+});
+```
+
+## Production architecture
+
+The safest model is:
+
+1. Your backend or calling SDK emits a ring event.
+2. Your app calls `showIncomingCall()`.
+3. The plugin shows native UI.
+4. `callAccepted` tells your app to join the actual room or VoIP session.
+5. `callDeclined`, `callEnded`, or `callTimedOut` tells your app to clean up remote state.
+
+Keep the plugin focused on native presentation. Keep call transport, authentication, and media session ownership in the system you already trust for those responsibilities.
+
+## Platform setup
+
+- Read the [iOS guide](/docs/plugins/incoming-call-kit/ios/) before wiring CallKit into a PushKit or APNs flow.
+- Read the [Android guide](/docs/plugins/incoming-call-kit/android/) before relying on full-screen intents on Android 14 and later.
diff --git a/src/content/docs/docs/plugins/incoming-call-kit/index.mdx b/src/content/docs/docs/plugins/incoming-call-kit/index.mdx
new file mode 100644
index 000000000..762d0e789
--- /dev/null
+++ b/src/content/docs/docs/plugins/incoming-call-kit/index.mdx
@@ -0,0 +1,68 @@
+---
+title: "@capgo/capacitor-incoming-call-kit"
+description: Native incoming call presentation for Capacitor with iOS CallKit and Android full-screen notifications.
+tableOfContents: false
+next: false
+prev: false
+sidebar:
+ order: 1
+ label: "Introduction"
+hero:
+ tagline: Present native incoming call UI in Capacitor with iOS CallKit and Android full-screen notifications, while keeping your push and media stack under your control.
+ image:
+ file: ~public/icons/plugins/incoming-call-kit.svg
+ actions:
+ - text: Get started
+ link: /docs/plugins/incoming-call-kit/getting-started/
+ icon: right-arrow
+ variant: primary
+ - text: GitHub
+ link: https://github.com/Cap-go/capacitor-incoming-call-kit/
+ icon: external
+ variant: minimal
+---
+
+import { Card, CardGrid } from '@astrojs/starlight/components';
+
+## Overview
+
+`@capgo/capacitor-incoming-call-kit` gives your Capacitor app the native ringing layer for incoming calls without forcing a specific VoIP vendor or push provider.
+
+Use it when you already have your own signaling flow, SIP stack, Twilio integration, Stream integration, FCM delivery, or PushKit delivery and you want the platform-native incoming call experience on top of that.
+
+
+
+ Report incoming calls to CallKit and react to accepted, declined, ended, and timed-out events.
+
+
+ Show a high-priority notification and raise a full-screen activity when Android allows it.
+
+
+ User actions are retained until the Capacitor bridge consumes them, which helps when JavaScript starts late.
+
+
+ Bring your own FCM, APNs, PushKit, SIP, or backend event flow. The plugin focuses on presentation, not transport.
+
+
+ Use one small API to show calls, end calls, inspect active calls, and listen for state changes.
+
+
+ Start with the [Getting started guide](/docs/plugins/incoming-call-kit/getting-started/), then read the [iOS guide](/docs/plugins/incoming-call-kit/ios/) and [Android guide](/docs/plugins/incoming-call-kit/android/).
+
+
+
+## What the plugin handles
+
+- Native incoming call presentation
+- Active call tracking
+- Accept, decline, end, and timeout events
+- Android notification and full-screen intent wiring
+- iOS CallKit reporting
+
+## What the plugin does not handle
+
+- FCM, APNs, or PushKit registration
+- Starting or managing the real audio or video session
+- Vendor-specific call logic from Twilio, Stream, Daily, Agora, SIP, or WebRTC SDKs
+
+If you need a complete communications stack, use this plugin as the native incoming-call surface and keep your transport and media session in your existing backend or SDK.
diff --git a/src/content/docs/docs/plugins/incoming-call-kit/ios.mdx b/src/content/docs/docs/plugins/incoming-call-kit/ios.mdx
new file mode 100644
index 000000000..cb59dc7f7
--- /dev/null
+++ b/src/content/docs/docs/plugins/incoming-call-kit/ios.mdx
@@ -0,0 +1,69 @@
+---
+title: iOS
+description: Configure CallKit behavior and understand production limits on iOS.
+sidebar:
+ order: 3
+---
+
+## How iOS behavior works
+
+On iOS, the plugin reports the incoming call to CallKit. That gives you the system incoming-call sheet and standardized call actions without building your own native UI.
+
+`requestPermissions()` resolves immediately on iOS because CallKit itself does not require a runtime permission dialog.
+
+## Basic example
+
+```ts
+import { IncomingCallKit } from '@capgo/capacitor-incoming-call-kit';
+
+await IncomingCallKit.showIncomingCall({
+ callId: 'call-42',
+ callerName: 'Ada Lovelace',
+ handle: '+1 555 010 020',
+ ios: {
+ handleType: 'phoneNumber',
+ supportsHolding: true,
+ supportsDTMF: false,
+ },
+});
+```
+
+## Handle types
+
+Use the `ios.handleType` option to control how CallKit formats the handle:
+
+- `generic` for app-specific identifiers
+- `phoneNumber` for real phone numbers
+- `emailAddress` for email-based identities
+
+## If you need real background incoming calls
+
+This plugin does not register PushKit or APNs for you.
+
+For true background or terminated-state ringing on iOS, your host app still needs the native Apple push setup that matches your transport strategy:
+
+1. Enable the Push Notifications capability when your transport uses Apple push delivery.
+2. Enable the Voice over IP background mode when your app uses a VoIP push flow.
+3. Deliver the incoming-call event to your app and invoke this plugin as soon as the Capacitor bridge is available.
+
+If your ring event exists only in JavaScript, you will get the best experience while the app is already running in the foreground.
+
+## Microphone and camera permissions
+
+CallKit does not replace your media SDK. If the real call session uses microphone or camera access, those usage descriptions still belong in your app:
+
+```xml
+NSMicrophoneUsageDescription
+This app uses the microphone for calls.
+NSCameraUsageDescription
+This app uses the camera for video calls.
+```
+
+Add only the keys your real calling flow needs.
+
+## What to keep in your app layer
+
+- PushKit and APNs registration
+- Authentication and token refresh
+- Joining the real room or VoIP session after `callAccepted`
+- Ending or reconciling remote call state when the plugin emits `callDeclined`, `callEnded`, or `callTimedOut`
diff --git a/src/content/docs/docs/plugins/index.mdx b/src/content/docs/docs/plugins/index.mdx
index f6bb69570..cbaae1b5d 100644
--- a/src/content/docs/docs/plugins/index.mdx
+++ b/src/content/docs/docs/plugins/index.mdx
@@ -243,6 +243,11 @@ The Updater plugin is the foundation of Capgo Cloud, enabling you to:
## 📞 Communication & Analytics
+ {
+ console.log('Accepted', call.callId);
+ // Join the real room or VoIP session here.
+});
+
+await IncomingCallKit.showIncomingCall({
+ callId: 'call-42',
+ callerName: 'Ada Lovelace',
+ handle: '+39 555 010 020',
+ appName: 'Capgo Phone',
+ hasVideo: true,
+ timeoutMs: 45_000,
+ extra: {
+ roomId: 'room-42',
+ },
+ android: {
+ channelId: 'calls',
+ channelName: 'Incoming Calls',
+ showFullScreen: true,
+ },
+ ios: {
+ handleType: 'phoneNumber',
+ },
+});
+```
+
+## Wire it to your real call flow
+
+The normal production pattern looks like this:
+
+1. Your backend or communications SDK emits a ring event.
+2. Your Capacitor app calls `showIncomingCall()`.
+3. The plugin presents native incoming-call UI.
+4. `callAccepted` tells your app to join the actual call.
+5. `callDeclined`, `callEnded`, or `callTimedOut` tells your app to clean up remote state.
+
+This separation is useful because it lets you swap transport or media providers later without rewriting the native ringing layer.
+
+## Platform notes
+
+### iOS
+
+- Uses CallKit for the system incoming-call sheet.
+- Does not register PushKit or APNs for you.
+- Real background ringing still depends on your host app's Apple push setup.
+
+### Android
+
+- Uses a high-priority notification and optional full-screen activity.
+- Requests notification permission on Android 13 and later.
+- Can open the Android 14 full-screen intent settings page when needed.
+
+## Useful API methods
+
+- `showIncomingCall()` to present the native UI.
+- `endCall()` to end a single tracked call.
+- `endAllCalls()` to clear all tracked calls.
+- `getActiveCalls()` to inspect what the native layer still considers active.
+
+## Learn more
+
+- Full documentation: [Incoming Call Kit docs](https://capgo.app/docs/plugins/incoming-call-kit/)
+- GitHub repository: [Cap-go/capacitor-incoming-call-kit](https://github.com/Cap-go/capacitor-incoming-call-kit)
+
+If your app already has transport and media solved, this plugin is the missing native presentation layer that makes incoming calls feel like a real mobile calling experience.