Alaznah

End-to-end setup for ringing and accepting calls when the app is backgrounded or killed — Android (FCM) and iOS (PushKit VoIP + CallKit) configured separately.

Background Calling

Foreground calling works with Hosted Signaling alone. Background calling is what lets your app ring, show native incoming UI, and connect media when the callee’s app is in the background or force-quit.

This guide is split by platform — configure Android and iOS independently, then wire the shared JavaScript layer.

Background calling flow overview

What you need before you start

RequirementAndroidiOS
Physical device for kill-state testRequiredRequired
Push credentials in Alaznah ConsoleFCM server keyAPNs .p8 + Key ID + Team ID + Bundle ID
Host app push package@react-native-firebase/messaging (recommended)None — SDK owns PushKit
Manifest / capabilitiesSee Permissions → AndroidPush + Background Modes (voip, audio)
Display namesRequired: config.displayName + startCall({ calleeDisplayName })Same

Simulator limits: iOS Simulator cannot show CallKit incoming rings or AVKit PiP. Android emulators may not deliver FCM reliably when killed. Always validate on real hardware.


Step 1 — Configure push in Alaznah Console

Sign in at console.alaznah.com → select your project → Push credentials.

Console push configuration

Bind credentials to the same project you use when minting calling tokens. Mismatched project / bundle / package = silent push failure.

FieldPlatformWhere to get it
FCM server keyAndroidFirebase Console → Project settings → Cloud Messaging → Server key (legacy)
APNs Key IDiOSApple Developer → Keys → your APNs Auth Key
Team IDiOSApple Developer → Membership
Bundle IDiOSMust match your Xcode target exactly
.p8 private keyiOSDownload when creating the APNs Auth Key (one-time)

After saving, signaling can wake offline devices when an invite is sent. Until credentials are configured, kill-state incoming stays unreliable — see Troubleshooting.


Android — background & killed-state calling

Android background calling setup

1. Create a Firebase project

  1. Open Firebase ConsoleAdd project (or use an existing one).
  2. Add Android app with your application ID (com.example.app — same as applicationId in Gradle).
  3. Download google-services.json → place in android/app/google-services.json.
  4. Ensure the Firebase Android Gradle plugin is applied (standard React Native Firebase setup).

2. Upload FCM credentials to Alaznah

Copy the FCM server key from Firebase → Project settings → Cloud Messaging and paste it in the console Push credentials form (Step 1 above).

3. Add Android permissions

Merge these into your app AndroidManifest.xml (or use the copy-paste block in Permissions):

  • POST_NOTIFICATIONS (Android 13+)
  • USE_FULL_SCREEN_INTENT — full-screen incoming UI when locked
  • FOREGROUND_SERVICE + FOREGROUND_SERVICE_PHONE_CALL — ongoing call keep-alive
  • WAKE_LOCK, VIBRATE as needed

You do not declare a custom FCM BroadcastReceiver for Alaznah calls — the SDK merges that for you (next section).

4. Install React Native Firebase Messaging

bash
npm install @react-native-firebase/app @react-native-firebase/messaging

Rebuild the native app after install. The SDK forwards non-call FCM messages to React Native Firebase when that package is present; call invite / cancel payloads are handled natively first.

5. What @alaznah/calling merges automatically

After npm install @alaznah/calling, the library manifest merges into your app — no host Kotlin/Java required for standard RN apps:

Merged by SDKPurpose
CallMessagingReceiverHandles incoming_call / call_canceled when app is killed
Removes default RN Firebase messaging receiverAvoids duplicate notifications for call payloads
IncomingCallActivity + foreground servicesFull-screen incoming + ongoing call UI
PiP attrs on ${applicationId}.MainActivityVideo PiP on Home — Picture in Picture

Reference: Permissions → Android auto-merge.

6. Obtain the FCM token (JavaScript)

Request notification permission, then read the device token:

ts
import { getApp } from '@react-native-firebase/app';
import {
  getMessaging,
  getToken,
  onTokenRefresh,
  requestPermission,
} from '@react-native-firebase/messaging';
import { PermissionsAndroid, Platform } from 'react-native';

async function getAndroidFcmToken(): Promise<string | null> {
  if (Platform.OS !== 'android') return null;

  if (Platform.Version >= 33) {
    const granted = await PermissionsAndroid.request(
      PermissionsAndroid.PERMISSIONS.POST_NOTIFICATIONS,
    );
    if (granted !== PermissionsAndroid.RESULTS.GRANTED) return null;
  }

  const messaging = getMessaging(getApp());
  await requestPermission(messaging);
  return getToken(messaging);
}

Re-subscribe to onTokenRefresh and call registerPushToken again when the token rotates.

7. Register the token with Alaznah

After useCallingReady() is true and the user is connected:

ts
import { Platform } from 'react-native';

await client.registerPushToken(fcmToken, 'android');

If you use <CallingScreen pushToken={...} pushPlatform="android" />, token registration can be automatic — see Call UI.

8. Handle foreground FCM data (optional)

When the app is foreground, the SDK may already show in-app incoming UI via signaling. For data-only FCM messages that arrive while open, sync pending calls:

ts
import { handleBackgroundIncomingCall } from '@alaznah/calling';

// On FCM data message with type incoming_call or call_canceled:
await handleBackgroundIncomingCall({ data: message.data });
await client.syncPendingCalls();

Full wiring example: alaznah-examples/basic-callsrc/useFirebase.ts + App.tsx.

9. Android test checklist

StepExpected result
Install release/dev build on Device A and Device B with different userIdsBoth connect to signaling
Register FCM tokens (log token in dev)Console project has FCM key saved
Caller starts voice/video callCallee rings in foreground
Callee sends app to backgroundIncoming UI still appears
Callee force-quits the appFull-screen incoming intent on lock screen
Callee taps AcceptApp opens → call connects without duplicate JS incoming flash
Callee taps DeclineCall ends; no stuck notification

iOS — background & killed-state calling

iOS background calling setup

1. Enable capabilities in Apple Developer

For your App ID:

  1. Enable Push Notifications.
  2. Enable Background ModesVoice over IP and Audio (and Remote notifications if you also use standard APNs alerts).

Create an APNs Auth Key (.p8) if you do not have one. Note the Key ID and your Team ID.

2. Upload APNs credentials to Alaznah

In console Push credentials, enter Key ID, Team ID, Bundle ID, and paste the .p8 key (Step 1 above). Bundle ID must match Xcode exactly.

3. Configure Xcode

Target → Signing & Capabilities:

CapabilityRequired for
Push NotificationsAPNs delivery
Background Modes → Voice over IPPushKit wake when killed
Background Modes → AudioMedia + video PiP while backgrounded
Background Modes → Remote notificationsOptional; standard alerts

Add usage strings to Info.plist:

  • NSMicrophoneUsageDescription
  • NSCameraUsageDescription (video calls)

Expo: the @alaznah/calling config plugin injects audio, voip, and remote-notification — still verify after prebuild.

4. What @alaznah/calling bootstraps automatically

No custom AppDelegate CallKit / PushKit code in your host app. On pod load the SDK:

SDK native (automatic)Purpose
PushKit registryVoIP push token for kill-state wake
CallKit providerNative incoming call UI (Accept / Decline)
CallKit ↔ WebRTC audio bridgeMedia after accept
registerIosVoipToken / onIosVoipToken JS bridgeToken → your JS → registerPushToken

Important: FCM alone is not enough for iOS kill-state incoming. You must register the PushKit VoIP token, not the FCM token, with registerPushToken(..., 'ios').

5. Obtain the VoIP token (JavaScript)

ts
import {
  onIosVoipToken,
  registerIosVoipToken,
} from '@alaznah/calling';

// On app start (iOS only):
const voipToken = await registerIosVoipToken();

// Listen for rotation:
const unsubscribe = onIosVoipToken((token) => {
  void client.registerPushToken(token, 'ios');
});

Standard notification permission (requestPermission from Firebase or @react-native-community/push-notification-ios) is independent from VoIP — denying alert permission must not block CallKit incoming calls.

6. Register the token with Alaznah

ts
await client.registerPushToken(voipToken, 'ios');

Call again whenever onIosVoipToken fires or after reconnect.

7. App resume after native Accept / Decline

When the user acts on CallKit UI and JS becomes active:

ts
import { AppState } from 'react-native';

AppState.addEventListener('change', async (state) => {
  if (state !== 'active') return;
  await client.drainNativeIncomingAction();
  await client.syncPendingCalls();
});

drainNativeIncomingAction() prevents a duplicate JS incoming screen after the user already accepted on CallKit. syncPendingCalls() attaches any pending invite session.

CallingUI / CallingContext call drainNativeIncomingAction on mount — you still want the AppState hook if you have custom navigation.

8. iOS test checklist

StepExpected result
Test on physical iPhone (not Simulator)CallKit ring appears
Callee force-quits appVoIP push wakes device → CallKit incoming
Lock screen incomingCaller display name shown (not raw user id)
Accept from CallKitApp foregrounds → media connects
Decline from CallKitCall ends cleanly
Background video call → HomePiP on device (Simulator: use in-app minimize — Picture in Picture)

Shared JavaScript integration

Minimal pattern (adapt from basic-call App.tsx):

tsx
import { useEffect } from 'react';
import { AppState, Platform } from 'react-native';
import {
  CallingProvider,
  useCallingClient,
  useCallingReady,
} from '@alaznah/calling';

function PushRegistration({ fcmOrVoipToken }: { fcmOrVoipToken: string | null }) {
  const client = useCallingClient();
  const ready = useCallingReady();

  useEffect(() => {
    if (!ready || !fcmOrVoipToken) return;
    void client.registerPushToken(
      fcmOrVoipToken,
      Platform.OS === 'ios' ? 'ios' : 'android',
    );
  }, [client, ready, fcmOrVoipToken]);

  useEffect(() => {
    if (!ready) return;
    const sub = AppState.addEventListener('change', (state) => {
      if (state !== 'active') return;
      void client.drainNativeIncomingAction();
      void client.syncPendingCalls();
    });
    return () => sub.remove();
  }, [client, ready]);

  return null;
}

End-to-end flow (both platforms)

  1. Caller invokes startCall({ calleeId, calleeDisplayName, … }).
  2. Signaling delivers the invite over WebSocket if callee is online.
  3. If callee is offline / killed, signaling sends FCM (Android) or APNs VoIP (iOS).
  4. Native layer shows incoming UI with caller display name.
  5. Accept → JS resumes → drainNativeIncomingAction + syncPendingCalls → WebRTC media same as foreground.

Details on invite handling: Incoming Calls.


Troubleshooting

SymptomLikely causeFix
No ring when app killedConsole push not configuredUpload FCM / APNs in console; verify project matches token mint
Android: no full-screen incomingMissing USE_FULL_SCREEN_INTENT or OEM battery restrictionsPermissions manifest + disable aggressive battery saver for test
iOS: no ring on SimulatorExpected limitationTest on physical iPhone
iOS: no ring on deviceRegistered FCM token instead of VoIPUse registerIosVoipToken()registerPushToken(..., 'ios')
Duplicate incoming UI (native + JS)Missing drain on resumedrainNativeIncomingAction() on AppState active
Caller id / “Unknown caller”Missing display nameSet config.displayName; pass calleeDisplayName on outbound calls (both required)
Call works foreground onlyToken not registeredLog token; confirm registerPushToken after ready
Same user on both devicesSignaling rejects self-callUse two distinct userIds

More: Troubleshooting.