Choose CallingScreen, CallingUI, or custom screens for Alaznah Calling.
Call UI
Built-in UI for 1:1 voice and video. You always need CallingProvider. Placing a call with startCall alone does not show a screen — mount CallingScreen or CallingUI (or your own UI).
Component catalog & prop tables: UI Components · Props.
Themes & slots: Customization.
Choose a path
| Path | Best for | What you mount |
|---|---|---|
A — CallingScreen | Demos, Quick Start, simplest app | Dialer + incoming + active in one component |
B — CallingUI | Real apps (chat, contacts) | Your UI + Alaznah overlay for ringing / in-call |
| C — Custom screens | Full brand control | Hooks + your layouts (optional screen pieces) |
Imports
import {
CallingProvider,
CallingScreen,
CallingUI,
useCallingClient,
useCall,
useIncomingCall,
useCallingReady,
LocalVideoView,
RemoteVideoView,
requestCallPermissions,
} from '@alaznah/calling';
import {
IncomingCallScreen,
ActiveCallScreen,
CallControls,
} from '@alaznah/calling/ui';CallingUI comes from @alaznah/calling. Lower-level screens / theme helpers come from @alaznah/calling/ui.
Path A — CallingScreen (fastest)
import { CallingProvider, CallingScreen } from '@alaznah/calling';
export default function App() {
return (
<CallingProvider
config={{
userId: 'alice',
getAuthToken: fetchCallingToken,
}}
>
<CallingScreen userId="alice" defaultPeerId="bob" />
</CallingProvider>
);
}Includes: simple dialer, outbound ringing, incoming (swipe-up accept), active call, end.
Optional props: pushToken, pushPlatform, onError, onLogout — see Props.
Use this to verify signaling + media. For a production chat app, switch to Path B.
Path B — Your app + CallingUI overlay (recommended)
Use this when you already have chat / contacts / tabs and only want Alaznah to own the ringing + in-call screens.
Why this path
| You keep | Alaznah shows |
|---|---|
| Login, chat list, chat screen, dial buttons | Incoming call UI |
Your navigation (AppNavigator) | Outgoing “connecting…” UI |
Your peer userId / display name logic | Active call controls (mute, speaker, end, video) |
CallingUI is an overlay: when there is no call, it returns null so your screens stay fully visible underneath.
Architecture (mental model)
CallingProvider ← token + signaling connection
├── AppNavigator ← your Home / Chat / tabs
└── CallingUI ← only when a call is ringing, connecting, or activeWrong: wrap only one screen (e.g. Home) in CallingProvider — navigating to Chat breaks hooks (must be used within CallingProvider).
Correct: put the provider at the root of the authenticated tree (after login).
Step 1 — Provider at the root
import {
CallingProvider,
DEFAULT_HOSTED_SIGNALING_URL,
} from '@alaznah/calling';
const config = {
userId: currentUser.id, // must match JWT / mint userId
getAuthToken: async () => {
// return a fresh Alaznah calling token from YOUR backend
return fetchCallingToken();
},
signalingUrl: DEFAULT_HOSTED_SIGNALING_URL, // or omit for hosted default
};
export default function App() {
if (!isLoggedIn) return <LoginScreen />;
return (
<CallingProvider config={config}>
<AppNavigator />
<CallingOverlay />
</CallingProvider>
);
}getAuthToken / tokens: Authentication.
Step 2 — Mount CallingUI once
Use a small child component so hooks run inside the provider:
import { CallingUI, useCallingClient } from '@alaznah/calling';
function CallingOverlay() {
const client = useCallingClient();
return (
<CallingUI
client={client}
onError={(e) => console.warn('calling', e)}
// optional: theme / backgroundColor / slots — see Customization
/>
);
}- Import:
CallingUIfrom@alaznah/calling clientprop is required — get it fromuseCallingClient()- Mount one overlay for the whole app (do not remount it on every screen)
Step 3 — Place a call from Chat / Contacts
On the dial button: request permissions, then startCall. CallingUI opens the screens automatically:
import {
useCallingClient,
useCallingReady,
requestCallPermissions,
} from '@alaznah/calling';
function ChatHeader({ otherUserId }: { otherUserId: string }) {
const client = useCallingClient();
const ready = useCallingReady();
const onAudio = async () => {
if (!ready) return; // still connecting to signaling
await requestCallPermissions('audio');
await client.startCall({
calleeId: otherUserId, // peer's Alaznah userId
mediaType: 'audio',
});
};
const onVideo = async () => {
if (!ready) return;
await requestCallPermissions('video');
await client.startCall({
calleeId: otherUserId,
mediaType: 'video',
});
};
return (
<>
<Button title="Audio" onPress={() => void onAudio()} disabled={!ready} />
<Button title="Video" onPress={() => void onVideo()} disabled={!ready} />
</>
);
}Permissions: Permissions.
Step 4 — Other device (incoming)
On the peer device as well:
- Same project / signaling
- That user’s own token and matching
config.userId - Same tree:
CallingProvider+CallingUI
When you place a call, the peer sees the incoming UI from CallingUI → swipe up to accept — Incoming Calls.
What CallingUI shows (by state)
| Situation | What user sees |
|---|---|
| No call | Nothing (your chat UI only) |
| You dialed out | Outgoing / connecting → active (ActiveCallScreen) |
| Someone calls you | Incoming (IncomingCallScreen, swipe-up accept) |
| Call connected | Active controls (mute, speaker, camera, end) |
| Call ended | Overlay closes → back to your screen |
Common mistakes
| Mistake | Result | Fix |
|---|---|---|
| Provider only on one screen | Calling hooks must be used within CallingProvider | Wrap navigator at root |
Forgot CallingUI | startCall logs connecting but no UI | Mount overlay under provider |
CallingUI from @alaznah/calling/ui (legacy) | Still works | Prefer @alaznah/calling |
Same JWT / same userId on both phones | Call never rings the other side | Mint per user |
ready === false ignored | Silent no-op if you return early | Wait for useCallingReady() or show “connecting…” |
Optional next
- Theme / slots on
CallingUI→ Customization - Full prop list → UI Components · Props
- Background / kill-state ring → Push Notifications
Path C — Custom UI (headless)
Drive everything from hooks:
function MyCallHost() {
const client = useCallingClient();
const call = useCall();
const incoming = useIncomingCall();
if (incoming) {
return (
<MyIncoming
call={incoming}
onAccept={() => void client.accept(incoming.callId)}
onReject={() => void client.reject(incoming.callId, 'declined')}
/>
);
}
if (call) {
return (
<MyActive
call={call}
onMute={(m) => void client.setMuted(m)}
onEnd={() => void client.end(call.callId)}
/>
);
}
return null; // dialer is elsewhere in your app
}You can still reuse pieces:
import { IncomingCallScreen, ActiveCallScreen, defaultCallingTheme } from '@alaznah/calling/ui';Prefer CallingUI when possible so native incoming suppress / drain stay consistent.
Hooks
const client = useCallingClient();
const ready = useCallingReady(); // wait before startCall
const call = useCall(); // active / focused call
const incoming = useIncomingCall(); // ringing inbound, if anyUse useCall() — there is no useActiveCall.
Incoming behavior (built-in)
Default accept UX:
- Decline — tap red
- Accept — swipe up on green (tap does not accept)
- Video may show local preview + join-with-video
Details: Incoming Calls.
Video views (custom layouts)
import { LocalVideoView, RemoteVideoView, useCall } from '@alaznah/calling';
function Videos() {
const call = useCall();
if (!call) return null;
return (
<>
<RemoteVideoView stream={call.remoteStream} objectFit="cover" />
<LocalVideoView stream={call.localStream} mirror objectFit="cover" />
</>
);
}CallingUI / ActiveCallScreen already use these for the default video layout.
Checklist
CallingProviderabove any calling hook / UI- Mount
CallingScreenorCallingUI(or custom host) userIdmatches token; peer uses a differentuserIdrequestCallPermissions('audio' | 'video')before dial- Import
CallingUIfrom@alaznah/calling
Related
- UI Components — full component guide
- Customization — theme, slots, headless
- Incoming Calls — swipe-up + native wake
- Permissions · Quick Start