Alaznah

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

PathBest forWhat you mount
A — CallingScreenDemos, Quick Start, simplest appDialer + incoming + active in one component
B — CallingUIReal apps (chat, contacts)Your UI + Alaznah overlay for ringing / in-call
C — Custom screensFull brand controlHooks + your layouts (optional screen pieces)

Imports

ts
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)

tsx
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.


Use this when you already have chat / contacts / tabs and only want Alaznah to own the ringing + in-call screens.

Why this path

You keepAlaznah shows
Login, chat list, chat screen, dial buttonsIncoming call UI
Your navigation (AppNavigator)Outgoing “connecting…” UI
Your peer userId / display name logicActive 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)

text
CallingProvider          ← token + signaling connection
├── AppNavigator         ← your Home / Chat / tabs
└── CallingUI            ← only when a call is ringing, connecting, or active

Wrong: 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

tsx
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:

tsx
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: CallingUI from @alaznah/calling
  • client prop is required — get it from useCallingClient()
  • 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:

tsx
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:

  1. Same project / signaling
  2. That user’s own token and matching config.userId
  3. Same tree: CallingProvider + CallingUI

When you place a call, the peer sees the incoming UI from CallingUIswipe up to accept — Incoming Calls.

What CallingUI shows (by state)

SituationWhat user sees
No callNothing (your chat UI only)
You dialed outOutgoing / connecting → active (ActiveCallScreen)
Someone calls youIncoming (IncomingCallScreen, swipe-up accept)
Call connectedActive controls (mute, speaker, camera, end)
Call endedOverlay closes → back to your screen

Common mistakes

MistakeResultFix
Provider only on one screenCalling hooks must be used within CallingProviderWrap navigator at root
Forgot CallingUIstartCall logs connecting but no UIMount overlay under provider
CallingUI from @alaznah/calling/ui (legacy)Still worksPrefer @alaznah/calling
Same JWT / same userId on both phonesCall never rings the other sideMint per user
ready === false ignoredSilent no-op if you return earlyWait for useCallingReady() or show “connecting…”

Optional next


Path C — Custom UI (headless)

Drive everything from hooks:

tsx
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:

tsx
import { IncomingCallScreen, ActiveCallScreen, defaultCallingTheme } from '@alaznah/calling/ui';

Prefer CallingUI when possible so native incoming suppress / drain stay consistent.

Hooks

ts
const client = useCallingClient();
const ready = useCallingReady();     // wait before startCall
const call = useCall();              // active / focused call
const incoming = useIncomingCall();  // ringing inbound, if any

Use useCall() — there is no useActiveCall.


Incoming behavior (built-in)

Default accept UX:

  • Decline — tap red
  • Acceptswipe up on green (tap does not accept)
  • Video may show local preview + join-with-video

Details: Incoming Calls.


Video views (custom layouts)

tsx
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

  1. CallingProvider above any calling hook / UI
  2. Mount CallingScreen or CallingUI (or custom host)
  3. userId matches token; peer uses a different userId
  4. requestCallPermissions('audio' | 'video') before dial
  5. Import CallingUI from @alaznah/calling