@dialstack/sdk-native 0.1.1 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -41,7 +41,7 @@ interface PaginatedList<P extends Page<unknown>> extends Promise<P> {
41
41
  * rather than depending on the ambient `lib.dom.d.ts` names directly.
42
42
  */
43
43
  type RTCPeerConnection = globalThis.RTCPeerConnection;
44
- type MediaStream = globalThis.MediaStream;
44
+ type MediaStream$1 = globalThis.MediaStream;
45
45
  type RTCIceServer$1 = globalThis.RTCIceServer;
46
46
  type RTCSessionDescriptionInit = globalThis.RTCSessionDescriptionInit;
47
47
  /**
@@ -81,7 +81,7 @@ interface Ringback {
81
81
  setSinkId?(deviceId: string | null): void;
82
82
  }
83
83
 
84
- type PhoneErrorCode = 'auth_failed' | 'auth_expired' | 'invalid_message' | 'call_failed' | 'call_not_found' | 'emergency_address_required' | 'session_limit' | 'session_replaced' | 'session_revoked' | 'rate_limited' | 'presence_unavailable' | 'reachability_unavailable' | 'internal_error' | 'going_away' | 'idle_timeout' | 'slow_consumer' | 'transport_closed' | 'ice_fetch_failed' | 'mic_permission_denied' | 'audio_device_unavailable' | 'audio_device_in_use';
84
+ type PhoneErrorCode = 'auth_failed' | 'auth_expired' | 'invalid_message' | 'call_failed' | 'call_not_found' | 'emergency_address_required' | 'session_limit' | 'session_replaced' | 'session_revoked' | 'rate_limited' | 'call_limit' | 'presence_unavailable' | 'reachability_unavailable' | 'internal_error' | 'going_away' | 'idle_timeout' | 'slow_consumer' | 'transport_closed' | 'ice_fetch_failed' | 'token_refresh_failed' | 'mic_permission_denied' | 'audio_device_unavailable' | 'audio_device_in_use';
85
85
  declare class PhoneError extends Error {
86
86
  readonly code: PhoneErrorCode;
87
87
  readonly callId: string | null;
@@ -161,6 +161,17 @@ interface PhoneOptions {
161
161
  * processed the socket's close), reconnecting if it's dead.
162
162
  */
163
163
  onAppResume?: AppResumeSubscribe;
164
+ /**
165
+ * Acquire an inbound call's microphone when it is ANSWERED rather than when it
166
+ * arrives. Set by platforms whose OS owns the call audio session: CallKit and
167
+ * Telecom activate it only on answer, and capturing before then fails — on
168
+ * Android the device is still in `MODE_RINGTONE`, where WebRTC cannot start its
169
+ * recorder, so it releases it and the answered call has no mic at all.
170
+ *
171
+ * Off by default: a browser can capture during ringing, and doing so surfaces a
172
+ * microphone problem while the user can still see it.
173
+ */
174
+ deferInboundCapture?: boolean;
164
175
  /**
165
176
  * Microphone `deviceId` for new calls; omitted means the OS default. Held for the
166
177
  * phone's lifetime and NOT persisted — remembering a device is the host's call, so
@@ -299,25 +310,32 @@ type ClientMessage = {
299
310
  sdp: string;
300
311
  } | {
301
312
  type: 'call.answer';
313
+ req_id?: string;
302
314
  call_id: string;
303
315
  } | {
304
316
  type: 'call.reject';
317
+ req_id?: string;
305
318
  call_id: string;
306
319
  reason?: RejectReason;
307
320
  } | {
308
321
  type: 'call.hangup';
322
+ req_id?: string;
309
323
  call_id: string;
310
324
  } | {
311
325
  type: 'call.hold';
326
+ req_id?: string;
312
327
  call_id: string;
313
328
  } | {
314
329
  type: 'call.resume';
330
+ req_id?: string;
315
331
  call_id: string;
316
332
  } | {
317
333
  type: 'call.mute';
334
+ req_id?: string;
318
335
  call_id: string;
319
336
  } | {
320
337
  type: 'call.transfer';
338
+ req_id?: string;
321
339
  call_id: string;
322
340
  destination: string;
323
341
  } | {
@@ -329,6 +347,7 @@ type ClientMessage = {
329
347
  sdp?: string;
330
348
  } | {
331
349
  type: 'call.unmute';
350
+ req_id?: string;
332
351
  call_id: string;
333
352
  } | {
334
353
  type: 'sdp.offer';
@@ -476,6 +495,7 @@ type TransportEvents = {
476
495
  error?: PhoneError;
477
496
  }) => void;
478
497
  reconnecting: (attempt: number, delayMs: number) => void;
498
+ wake: () => void;
479
499
  };
480
500
  declare class Transport {
481
501
  private ws;
@@ -507,6 +527,11 @@ declare class Transport {
507
527
  */
508
528
  trySend(msg: ClientMessage): void;
509
529
  close(): void;
530
+ /**
531
+ * Abandon the current socket without ending the transport: its close runs the
532
+ * normal backoff reconnect. For an attempt that must not authenticate.
533
+ */
534
+ dropSocket(): void;
510
535
  private openSocket;
511
536
  private scheduleReconnect;
512
537
  private onWake;
@@ -525,6 +550,16 @@ type CallEventMap = {
525
550
  audioInputLost: () => void;
526
551
  };
527
552
  type Listener$1<K extends keyof CallEventMap> = CallEventMap[K];
553
+ /**
554
+ * The slice of a local conference a merged `Call` needs. Declared structurally
555
+ * here (rather than importing `LocalConference`) so the core has no dependency
556
+ * on the mixer — `conference.ts` imports `Call`, and the reverse import would
557
+ * make that a cycle.
558
+ */
559
+ interface ConferenceSink {
560
+ /** Apply mute across the merged legs' mixed uplinks. */
561
+ setMuted(muted: boolean): void;
562
+ }
528
563
  interface CallInit {
529
564
  id: string;
530
565
  direction: CallDirection;
@@ -538,6 +573,8 @@ interface CallInit {
538
573
  ringback?: Ringback;
539
574
  audioInputDeviceId?: string;
540
575
  audioOutputDeviceId?: string;
576
+ deferInboundCapture?: boolean;
577
+ nextReqId: () => string;
541
578
  }
542
579
  declare class Call {
543
580
  id: string;
@@ -571,6 +608,9 @@ declare class Call {
571
608
  */
572
609
  get canSendDtmf(): boolean;
573
610
  private transport;
611
+ private readonly nextReqId;
612
+ private readonly issuedReqIds;
613
+ private readonly hangupReqIds;
574
614
  private readonly startConsult;
575
615
  private localStream;
576
616
  private remoteStream;
@@ -580,6 +620,7 @@ declare class Call {
580
620
  private listeners;
581
621
  private endedSettled;
582
622
  private pendingAnswerSdp;
623
+ private buildingAnswer;
583
624
  private answerSent;
584
625
  private wantsAnswer;
585
626
  private localMediaReady;
@@ -589,8 +630,26 @@ declare class Call {
589
630
  private readonly ringback;
590
631
  private remoteAudioFlowing;
591
632
  private audioInputDeviceId_;
633
+ private deferredAudioInput;
634
+ private conference;
592
635
  private switchChain;
636
+ private captureDeferred;
593
637
  constructor(init: CallInit);
638
+ /**
639
+ * Take the mic this call deliberately skipped on arrival, then produce the
640
+ * answer SDP from it.
641
+ *
642
+ * `buildAnswer` is what needs the track: setRemoteDescription created a
643
+ * track-less sender per m-line, and answering without attaching a real track
644
+ * yields `a=recvonly` — the far end hears nothing, which is the failure this
645
+ * whole path exists to avoid. It runs only once the offer has been set;
646
+ * otherwise prepareAnswerForOffer does it when the offer lands.
647
+ */
648
+ private acquireDeferredCapture;
649
+ private reportAnswerFailure;
650
+ /** Set by the owner to surface a late mic failure, since `Call` has no error event. */
651
+ private reportCaptureFailure;
652
+ onCaptureFailure(report: (e: unknown) => void): void;
594
653
  whenLocalMediaReady(): Promise<void>;
595
654
  private acquireLocalMedia;
596
655
  /** The selected mic, or null for the OS default. */
@@ -606,7 +665,7 @@ declare class Call {
606
665
  /** Route this call's ringback tone to `deviceId` (null = OS default). */
607
666
  setAudioOutputDevice(deviceId: string | null): void;
608
667
  private watchCaptureTrack;
609
- private get isFinished();
668
+ private issueReqId;
610
669
  on<K extends keyof CallEventMap>(event: K, handler: Listener$1<K>): void;
611
670
  off<K extends keyof CallEventMap>(event: K, handler?: Listener$1<K>): void;
612
671
  answer(): void;
@@ -614,9 +673,40 @@ declare class Call {
614
673
  reject(reason?: RejectReason): void;
615
674
  hangup(): void;
616
675
  hold(): void;
676
+ /**
677
+ * Hold or resume this leg as part of holding the whole conference.
678
+ *
679
+ * `hold()` refuses while merged because holding ONE party silences it to the
680
+ * other. Holding every leg together is the coherent version, so the caller
681
+ * that does that reaches the frames through here.
682
+ */
683
+ setConferenceHold(held: boolean): void;
684
+ /**
685
+ * Re-hold a call `resume()` ran on in this same tick. `hold()`'s
686
+ * `state !== 'active'` guard would swallow it, since the state only flips on
687
+ * the server echo. Only for that unwind; use `hold()` everywhere else.
688
+ */
689
+ holdAfterResume(): void;
617
690
  resume(): void;
618
691
  mute(): void;
619
692
  unmute(): void;
693
+ /**
694
+ * Mute has two layers, and a merged call needs both handled differently.
695
+ *
696
+ * Normally: flip the mic track (instant local feedback) AND send `call.mute`,
697
+ * which the server turns into a re-INVITE narrowing the leg's SDP direction to
698
+ * `recvonly` — defence in depth against a peer that ignores the local disable.
699
+ *
700
+ * While MERGED neither of those works:
701
+ * - the wire carries the conference's mixed track, not this call's mic track,
702
+ * so toggling `localStream` no longer reaches the far end (the mic is an
703
+ * input to the mixer now). The conference toggles the mixed uplinks instead.
704
+ * - the server frame must be withheld: `recvonly` would stop this leg's
705
+ * upstream media at the far end regardless of what the graph does, which is
706
+ * silent one-way audio for the other conference parties. Muting a merged
707
+ * call is a purely local mix decision.
708
+ */
709
+ private setMuted;
620
710
  sendDtmf(digits: string, duration?: number, interToneGap?: number): void;
621
711
  /**
622
712
  * Blind transfer: immediately redirect the remote party to `destination`.
@@ -640,10 +730,25 @@ declare class Call {
640
730
  */
641
731
  completeTransfer(): void;
642
732
  private assertTransferable;
643
- get remoteMediaStream(): MediaStream;
733
+ get remoteMediaStream(): MediaStream$1;
734
+ /**
735
+ * The local capture (mic) stream. A local conference mixes against this rather
736
+ * than opening a second `getUserMedia`, so every merged leg shares the one
737
+ * capture the call already owns.
738
+ */
739
+ get localMediaStream(): MediaStream$1;
740
+ /**
741
+ * Attach (or detach, with null) the local conference this call is merged into.
742
+ *
743
+ * While attached, the wire carries the conference's MIXED track rather than
744
+ * this call's mic track, which changes what mute has to do — see `mute()`.
745
+ * Set by the conference owner (the softphone's merge action), not by the core.
746
+ */
747
+ attachConference(conference: ConferenceSink | null): void;
644
748
  startOutbound(): Promise<RTCSessionDescriptionInit>;
645
749
  prepareAnswerForOffer(sdp: string): Promise<void>;
646
750
  private buildAnswer;
751
+ private buildAnswerOnce;
647
752
  acceptRemoteProvisionalAnswer(sdp: string): Promise<boolean>;
648
753
  acceptRemoteAnswer(sdp: string): Promise<void>;
649
754
  addRemoteCandidate(candidate: string, sdpMid: string | null, sdpMLineIndex: number | null): Promise<void>;
@@ -680,6 +785,11 @@ type Listener<K extends keyof PhoneEventMap> = PhoneEventMap[K];
680
785
  declare class DialStackPhone {
681
786
  isConnected: boolean;
682
787
  readonly activeCalls: Call[];
788
+ /**
789
+ * A `connect()` is in flight but not yet connected. Consult alongside
790
+ * `isConnected`; a concurrent `connect()` joins the one in flight.
791
+ */
792
+ get isConnecting(): boolean;
683
793
  private token;
684
794
  private apiBaseUrl;
685
795
  private signalingUrl;
@@ -689,8 +799,12 @@ declare class DialStackPhone {
689
799
  private ringback;
690
800
  private createSignalingSocket;
691
801
  private onAppResume;
802
+ private deferInboundCapture;
692
803
  private transport;
693
804
  private readonly handshake;
805
+ private connectInFlight;
806
+ private tokenMint;
807
+ private tokenMintedAt;
694
808
  private iceServers;
695
809
  private listeners;
696
810
  private pendingOutbound;
@@ -749,6 +863,14 @@ declare class DialStackPhone {
749
863
  */
750
864
  private syncDeviceChangeSubscription;
751
865
  connect(): Promise<void>;
866
+ private openSession;
867
+ private tokenNeedsRefresh;
868
+ private tokenMsLeft;
869
+ private freshToken;
870
+ private mintToken;
871
+ private authenticateAfterMint;
872
+ private sendAuthenticate;
873
+ private awaitReconnect;
752
874
  disconnect(): void;
753
875
  /**
754
876
  * Tear down and reconnect, re-running the `authenticate` handshake. The new
@@ -768,6 +890,8 @@ declare class DialStackPhone {
768
890
  call(destination: string): Promise<Call>;
769
891
  private startConsult;
770
892
  private placeOutbound;
893
+ private endCallsOfLostSession;
894
+ private track;
771
895
  getCall(callId: string): Call | undefined;
772
896
  /**
773
897
  * Subscribe to presence (BLF) for an explicit, bounded set of users. The
@@ -847,6 +971,15 @@ declare class DialStackPhone {
847
971
  */
848
972
  clearEmergencyAddressRegisteredIp(id: string): Promise<void>;
849
973
  private nextReqId;
974
+ /**
975
+ * Replace the token for the NEXT connect, off the connected path — for a host
976
+ * that mints its own tokens and must (re)connect with a fresh one (boot,
977
+ * push-wake), which `onTokenExpiring` (live-session only) can't cover. Refuses
978
+ * while connected/connecting: swapping there would desync the local token from
979
+ * the one the server enforces and the session would die silently at expiry — to
980
+ * swap a live session's token, `disconnect()` then `setToken()` before `connect()`.
981
+ */
982
+ setToken(token: string): void;
850
983
  private adoptToken;
851
984
  private scheduleTokenRefresh;
852
985
  private fireTokenRefresh;
@@ -855,6 +988,7 @@ declare class DialStackPhone {
855
988
  private fetchIceServers;
856
989
  private handleMessage;
857
990
  private handleIncoming;
991
+ private answerFailureReporter;
858
992
  private emit;
859
993
  }
860
994
 
@@ -1095,37 +1229,19 @@ interface AppearanceOptions$1 {
1095
1229
  * the `appearance` prop flows through identically on both platforms.
1096
1230
  */
1097
1231
 
1098
- /** Flat color set the Softphone paints from, after appearance is resolved. */
1099
1232
  interface SoftphonePalette {
1100
- /** Component background. */
1101
1233
  background: string;
1102
- /** Dial-pad key / control surface fill. */
1103
1234
  surface: string;
1104
- /** Surface fill while pressed/active. */
1105
1235
  surfaceActive: string;
1106
- /** Primary text (digits, destination, peer name). */
1107
1236
  text: string;
1108
- /** Secondary text (letter sub-labels, call state, hints). */
1109
1237
  textSecondary: string;
1110
- /** Hairline borders/dividers. */
1111
1238
  border: string;
1112
- /** Accent (links, focus ring, keypad/secondary control highlight). */
1113
1239
  accent: string;
1114
- /** Call / Answer (green) action color. */
1115
1240
  success: string;
1116
- /** Hang up / Decline (red) action color. */
1117
1241
  danger: string;
1118
- /** Warning / caution (amber) — used by the E911 banner. */
1119
1242
  warning: string;
1120
- /** Foreground used on top of accent/success/danger fills. */
1121
1243
  onAccent: string;
1122
1244
  }
1123
- /**
1124
- * Resolve the final palette from appearance: theme picks the base set, then any
1125
- * `appearance.variables` overrides (the same keys other SDK components honor)
1126
- * are layered on top. Unrecognized/absent variables fall back to the theme
1127
- * default, so a bare `{ theme: 'dark' }` and a fully-specified palette both work.
1128
- */
1129
1245
  declare function resolveSoftphonePalette(appearance?: AppearanceOptions$1): SoftphonePalette;
1130
1246
 
1131
1247
  /**
@@ -1180,6 +1296,9 @@ declare const en: {
1180
1296
  addCall: string;
1181
1297
  addCallPlaceholder: string;
1182
1298
  addCallSend: string;
1299
+ merge: string;
1300
+ split: string;
1301
+ conferenceLabel: string;
1183
1302
  cancel: string;
1184
1303
  stateTrying: string;
1185
1304
  stateRinging: string;
@@ -1618,9 +1737,16 @@ declare const en: {
1618
1737
  extensionPlaceholder: string;
1619
1738
  addUser: string;
1620
1739
  removeUser: string;
1740
+ removePhoneAccess: string;
1741
+ adminBadge: string;
1621
1742
  noUsers: string;
1622
1743
  atLeastOne: string;
1623
1744
  duplicateEmail: string;
1745
+ extensionRequired: string;
1746
+ extensionTaken: string;
1747
+ noPhoneService: string;
1748
+ givePhoneAccess: string;
1749
+ givingPhoneAccess: string;
1624
1750
  };
1625
1751
  location: {
1626
1752
  heading: string;
@@ -1893,6 +2019,7 @@ declare const en: {
1893
2019
  focDateTooSoon: string;
1894
2020
  focDateTooFar: string;
1895
2021
  focTimeRequired: string;
2022
+ focTimeOutOfWindow: string;
1896
2023
  billCopyRequired: string;
1897
2024
  signatureRequired: string;
1898
2025
  eligibilityError: string;
@@ -2031,6 +2158,7 @@ declare const en: {
2031
2158
  perUser: string;
2032
2159
  perPhoneNumber: string;
2033
2160
  perVoiceAiLocation: string;
2161
+ voiceAiPerLocationNote: string;
2034
2162
  perMonth: string;
2035
2163
  rateNotSet: string;
2036
2164
  pendingRate: string;
@@ -2179,6 +2307,19 @@ interface SoftphoneContextBase {
2179
2307
  completeAttendedTransfer: () => void;
2180
2308
  cancelAttendedTransfer: () => void;
2181
2309
  placeCall: (destination: string) => Promise<void>;
2310
+ /** The calls bridged into a local three-way conference (empty when not merged). */
2311
+ mergedCalls: Call[];
2312
+ isMerged: boolean;
2313
+ /** While merged, every remote party mixed — what the local user hears. */
2314
+ conferenceAudio: MediaStream | null;
2315
+ /** Whether merging is possible: two connected calls, no transfer, WebAudio present. */
2316
+ canMerge: boolean;
2317
+ mergeCalls: () => void;
2318
+ splitMerge: () => void;
2319
+ /** Hang up every leg of the conference (the merged UI shows one Hang up). */
2320
+ hangupConference: () => void;
2321
+ /** Hold or resume every leg of the conference together. */
2322
+ holdConference: (held: boolean) => void;
2182
2323
  emergency: UseEmergencyBinding;
2183
2324
  emergencyManagedByHost: boolean;
2184
2325
  lastError: {
@@ -2193,52 +2334,476 @@ interface SoftphoneContextBase {
2193
2334
  }
2194
2335
 
2195
2336
  /**
2196
- * SoftphoneProvider (React Native), same API as the web provider. Shared wiring
2197
- * lives in SoftphoneProviderBase (from `@dialstack/sdk-react/core`); this
2198
- * file adds only the native bits: the required `storage` adapter,
2199
- * `locationProvider`, `appearance`/`defaultCountry`, and the InCallManager audio
2200
- * session + ringtone.
2337
+ * Its own module so `createPhone()` can use it without importing the provider
2338
+ * (and React with it): a push-woken headless runtime has no renderer, yet it
2339
+ * needs the same header or the ingress 403s its handshake.
2340
+ */
2341
+ declare const nativeSignalingSocket: SignalingSocketFactory;
2342
+
2343
+ type AudioOutputKind = 'earpiece' | 'speaker' | 'wired' | 'bluetooth' | 'car' | 'airplay' | 'other';
2344
+ interface AudioOutput {
2345
+ id: string;
2346
+ kind: AudioOutputKind;
2347
+ name: string;
2348
+ }
2349
+ /**
2350
+ * Call-audio routing, implemented by a host that owns call audio
2351
+ * (`callAudio="host"`) over whatever its OS integration exposes.
2352
+ *
2353
+ * `current()` must be what the OS reports, never what was last requested: the
2354
+ * route also changes from the CallKit/Telecom UI, a headset, or a car, and the
2355
+ * softphone shows only this. It must return the same object until the route
2356
+ * changes.
2357
+ */
2358
+ interface AudioOutputController {
2359
+ current(): AudioOutput | null;
2360
+ onChange(listener: (output: AudioOutput | null) => void): () => void;
2361
+ setSpeaker(on: boolean): Promise<void>;
2362
+ }
2363
+ interface UseAudioOutput {
2364
+ supported: boolean;
2365
+ current: AudioOutput | null;
2366
+ speakerOn: boolean;
2367
+ toggleSpeaker: () => void;
2368
+ }
2369
+
2370
+ type CallAudioOwner = 'sdk' | 'host';
2371
+
2372
+ /**
2373
+ * The OS call surface (CallKit on iOS, Telecom on Android) as the bridge needs
2374
+ * it — shaped by the bridge, not by any library. See the contract suite for
2375
+ * what an adapter must guarantee.
2376
+ */
2377
+ /** OS-side call identity: the UUID CallKit/Telecom track the call under. */
2378
+ type OsSessionId = string;
2379
+ interface OsIncomingCall {
2380
+ callId: string;
2381
+ /** Caller handle as the OS should display/dial it. */
2382
+ from: string;
2383
+ displayName?: string | null;
2384
+ }
2385
+ interface OsOutgoingCall {
2386
+ /** The recipient handle the OS should display/dial. */
2387
+ to: string;
2388
+ displayName?: string | null;
2389
+ }
2390
+ /**
2391
+ * Why the call ended, in the OS's vocabulary. `answeredElsewhere` /
2392
+ * `declinedElsewhere` make the OS dismiss the ring silently instead of logging
2393
+ * a missed call.
2394
+ */
2395
+ type OsEndReason = 'remoteEnded' | 'unanswered' | 'answeredElsewhere' | 'declinedElsewhere' | 'failed' | 'unknown';
2396
+ /** Where the OS thinks a session is. `connecting` is answered but not yet reported connected. */
2397
+ type OsSessionStatus = 'ringing' | 'connecting' | 'connected' | 'ended';
2398
+ interface OsActiveSession {
2399
+ sessionId: OsSessionId;
2400
+ callId: string | null;
2401
+ /**
2402
+ * The OS's own view, for reconciling against the SDK call. Optional: an
2403
+ * adapter that can't read it leaves it out and the bridge doesn't reconcile
2404
+ * that field.
2405
+ */
2406
+ status?: OsSessionStatus;
2407
+ held?: boolean;
2408
+ }
2409
+ interface OsCallEvents {
2410
+ /**
2411
+ * A call was reported to the OS — by native code before JS existed, or by
2412
+ * `reportIncoming`. `callId` is the call_id the session was created with (a
2413
+ * wake push's, or the one passed to `reportIncoming`): the bridge pairs the
2414
+ * session with its call by it, and ends a session that has none.
2415
+ */
2416
+ incomingReported: (e: OsActiveSession) => void;
2417
+ /** The OS wants to PLACE a call — the user dialled from the platform's own UI
2418
+ * (Recents, Siri, dialer intent), not from the app. */
2419
+ callIntent: (e: {
2420
+ handle: string;
2421
+ }) => void;
2422
+ answer: (e: {
2423
+ sessionId: OsSessionId;
2424
+ }) => void;
2425
+ end: (e: {
2426
+ sessionId: OsSessionId;
2427
+ }) => void;
2428
+ setHeld: (e: {
2429
+ sessionId: OsSessionId;
2430
+ held: boolean;
2431
+ }) => void;
2432
+ setMuted: (e: {
2433
+ sessionId: OsSessionId;
2434
+ muted: boolean;
2435
+ }) => void;
2436
+ dtmf: (e: {
2437
+ sessionId: OsSessionId;
2438
+ digits: string;
2439
+ }) => void;
2440
+ /** iOS only in practice: CallKit gates audio, so WebRTC audio starts here. */
2441
+ audioSessionActivated: (e: {
2442
+ sessionId: OsSessionId | null;
2443
+ }) => void;
2444
+ }
2445
+ type OsCallEventName = keyof OsCallEvents;
2446
+ interface OsCallAdapter {
2447
+ /** Resolves to the OS-assigned session id (the OS assigns, never the caller). */
2448
+ reportIncoming(call: OsIncomingCall): Promise<OsSessionId>;
2449
+ /**
2450
+ * Report an outbound call so the OS holds a live session — on Android that
2451
+ * starts the foreground service keeping the process and call alive when
2452
+ * backgrounded, which an outbound call otherwise lacks.
2453
+ */
2454
+ reportOutgoing(call: OsOutgoingCall): Promise<OsSessionId>;
2455
+ /** The call the OS is showing as incoming/dialling is now connected. */
2456
+ reportConnected(sessionId: OsSessionId): Promise<void>;
2457
+ /** Must not re-emit `end` — the bridge is telling the OS, not the other way round. */
2458
+ reportEnded(sessionId: OsSessionId, reason: OsEndReason): Promise<void>;
2459
+ /** Remote-initiated state only; the OS already knows about its own actions. */
2460
+ setHeld(sessionId: OsSessionId, held: boolean): Promise<void>;
2461
+ setMuted(sessionId: OsSessionId, muted: boolean): Promise<void>;
2462
+ /** The session the OS currently treats as active, if any. */
2463
+ getActiveSession(): Promise<OsActiveSession | null>;
2464
+ /**
2465
+ * The app's own Answer/End buttons go through the OS too, so every answer —
2466
+ * lock screen, notification, car, watch, in-app — arrives as the same
2467
+ * `answer`/`end` event and there is one code path, not two.
2468
+ */
2469
+ answer(sessionId: OsSessionId): Promise<void>;
2470
+ end(sessionId: OsSessionId): Promise<void>;
2471
+ on<K extends OsCallEventName>(event: K, listener: OsCallEvents[K]): () => void;
2472
+ }
2473
+
2474
+ /**
2475
+ * Where the bridge schedules its deadlines and retries. The JS timers are the
2476
+ * default, but React Native on Android pauses them in an app that is
2477
+ * backgrounded without being killed, even while a headless task runs. A wake
2478
+ * whose call never arrives (answered or cancelled before the app re-registered)
2479
+ * then waits on a deadline that never fires, and the OS keeps ringing. An
2480
+ * integration that can schedule natively passes timers here.
2481
+ */
2482
+ interface BridgeTimers {
2483
+ setTimeout(fn: () => void, ms: number): unknown;
2484
+ clearTimeout(handle: unknown): void;
2485
+ }
2486
+
2487
+ /**
2488
+ * "Keep this JS runtime alive" as a promise the platform can await.
2489
+ *
2490
+ * Android's HeadlessJsTaskService tears the runtime down the moment the task
2491
+ * promise settles, so a wake holds this from the instant the task starts until
2492
+ * live calls hit zero. What the hold actually covers is the gap before the call
2493
+ * library's foreground service exists — once that is up it holds the process,
2494
+ * and a ceiling reached mid-call ends only this task, not the call (measured: a
2495
+ * 60s ring survived the then-45s ceiling untouched).
2496
+ *
2497
+ * So the ceiling is a bound on a wake that never delivers, not a correctness
2498
+ * guarantee. It is set to the same 300s as the max park/ring, the service's task
2499
+ * timeout, the OS answer deadline and the library's incoming-call timeout purely
2500
+ * so the numbers don't contradict each other; keep them aligned.
2201
2501
  */
2502
+ declare class RuntimeHold {
2503
+ private readonly ceilingMs;
2504
+ private readonly timers;
2505
+ private release;
2506
+ private waiting;
2507
+ constructor(ceilingMs?: number, timers?: BridgeTimers);
2508
+ acquire(): Promise<void>;
2509
+ releaseNow(): void;
2510
+ /** Non-null while a hold is in flight — the reliable "a wake is mid-flight" signal. */
2511
+ pending(): Promise<void> | null;
2512
+ private settle;
2513
+ }
2514
+
2515
+ /**
2516
+ * What the bridge needs from a call. Overloads rather than a generic `on`: a
2517
+ * generic over a narrower event map is not assignable from the SDK's wider one.
2518
+ */
2519
+ interface BridgeCall {
2520
+ readonly id: string;
2521
+ readonly from: string;
2522
+ readonly fromName: string | null;
2523
+ readonly state: CallState;
2524
+ readonly isMuted: boolean;
2525
+ answer(): void;
2526
+ reject(reason?: 'busy' | 'decline'): void;
2527
+ hangup(): void;
2528
+ hold(): void;
2529
+ resume(): void;
2530
+ mute(): void;
2531
+ unmute(): void;
2532
+ sendDtmf(digits: string): void;
2533
+ on(event: 'answered' | 'resumed', handler: () => void): void;
2534
+ on(event: 'held', handler: (by: HeldBy) => void): void;
2535
+ on(event: 'ended', handler: (reason: CallEndReason) => void): void;
2536
+ off(event: 'held' | 'resumed' | 'ended', handler: (...args: never[]) => void): void;
2537
+ }
2538
+ interface BridgePhoneError {
2539
+ code?: string;
2540
+ message?: string;
2541
+ fatal?: boolean;
2542
+ callId?: string | null;
2543
+ }
2544
+ /** What the bridge needs from the phone — `DialStackPhone` satisfies it. */
2545
+ interface BridgePhone {
2546
+ readonly isConnected: boolean;
2547
+ connect(): Promise<void>;
2548
+ disconnect(): void;
2549
+ setToken(token: string): void;
2550
+ /** Place an outbound call. The bridge reports it to the OS around this. */
2551
+ call(destination: string): Promise<BridgeCall>;
2552
+ on(event: 'connected' | 'reconnected', handler: () => void): void;
2553
+ on(event: 'disconnected', handler: (error?: unknown) => void): void;
2554
+ on(event: 'reconnecting', handler: (attempt: number, delayMs: number) => void): void;
2555
+ on(event: 'incoming', handler: (call: BridgeCall) => void): void;
2556
+ on(event: 'error', handler: (err: BridgePhoneError) => void): void;
2557
+ }
2558
+ type AppLifecycleState = 'active' | 'background' | 'inactive' | 'unknown' | 'extension';
2559
+ /** Foreground/background as a port so the bridge has no react-native import. */
2560
+ interface AppLifecycle {
2561
+ onChange(listener: (state: AppLifecycleState) => void): () => void;
2562
+ }
2563
+ interface NativeCallBridgeOptions {
2564
+ phone: BridgePhone;
2565
+ os: OsCallAdapter;
2566
+ lifecycle?: AppLifecycle;
2567
+ hold?: RuntimeHold;
2568
+ log?: (message: string) => void;
2569
+ /** From the OS report until `connect()` resolves: a socket that never comes up. */
2570
+ registrationDeadlineMs?: number;
2571
+ /**
2572
+ * From `connect()` resolving until `incoming`. The proxy resumes a parked INVITE
2573
+ * on REGISTER or within its 1s poll, so after this nothing is coming.
2574
+ */
2575
+ deliveryDeadlineMs?: number;
2576
+ /** An OS answer held for a call that has not arrived yet is stale past this. */
2577
+ answerTtlMs?: number;
2578
+ /**
2579
+ * How many calls the integration puts on the OS call screen at once (default 1).
2580
+ * A second concurrent call past the cap is NOT reported to the OS but still
2581
+ * rings and is answerable in-app; it just gets no OS session. Calls pair with
2582
+ * sessions by call_id, so any value is safe; raising it is a product choice
2583
+ * (OS call-waiting) and needs an adapter whose library holds several sessions.
2584
+ */
2585
+ maxOsCalls?: number;
2586
+ /** Defaults to the JS timers; see `BridgeTimers` for when to pass native ones. */
2587
+ timers?: BridgeTimers;
2588
+ now?: () => number;
2589
+ }
2590
+ declare function toOsEndReason(reason: CallEndReason): OsEndReason;
2591
+ /**
2592
+ * The bidirectional bridge between the OS call surface and the SDK (OS↔SDK
2593
+ * answer/end/mute/hold/DTMF and lifecycle), and the only thing that subscribes to
2594
+ * either. Works with no renderer: a push-woken headless runtime starts it like
2595
+ * the app window does.
2596
+ *
2597
+ * ## Pairing a session with a call
2598
+ *
2599
+ * A call has one `call_id` everywhere: on the socket, in the wake webhook and on
2600
+ * the INVITE a wake resumes. The OS mints its own session id at setup, and the
2601
+ * bridge links it to that `call_id` (`ids`) the moment it knows both: when it
2602
+ * reports a call itself, when an outbound call is placed, and when the OS
2603
+ * reports a wake push's session (`incomingReported`, carrying the `call_id` the
2604
+ * push delivered). Every lookup goes through that link; none guesses by
2605
+ * position. A second session for a `call_id` already linked is the same call
2606
+ * shown twice, and is ended.
2607
+ *
2608
+ * The contract this puts on the integrator: the wake push carries the
2609
+ * webhook's `call_id`, and the OS adapter reports it on `incomingReported`.
2610
+ */
2611
+ declare class NativeCallBridge {
2612
+ readonly hold: RuntimeHold;
2613
+ private readonly timers;
2614
+ private readonly phone;
2615
+ private readonly os;
2616
+ private readonly lifecycle;
2617
+ private readonly log;
2618
+ private readonly registrationDeadlineMs;
2619
+ private readonly deliveryDeadlineMs;
2620
+ private readonly answerTtlMs;
2621
+ private readonly maxOsCalls;
2622
+ private readonly now;
2623
+ private started;
2624
+ private appState;
2625
+ private reconnecting;
2626
+ private readonly ids;
2627
+ private readonly calls;
2628
+ private readonly callErrorWaiters;
2629
+ private readonly ending;
2630
+ private readonly changingHold;
2631
+ private reconcileTimer;
2632
+ /** Outbound calls still being placed, so not yet in `calls`. */
2633
+ private placingOutbound;
2634
+ /** Calls this app placed. For these `answered` means the far end picked up. */
2635
+ private readonly outboundCalls;
2636
+ /** OS sessions created by a native wake report, before their call arrives on the socket. */
2637
+ private readonly wakeSessions;
2638
+ private readonly reportedToOs;
2639
+ private readonly connectedReported;
2640
+ /**
2641
+ * An OS answer/decline that landed before the SDK had the call. On a wake the
2642
+ * user taps within ~200ms while connect() takes seconds, so it's applied on arrival.
2643
+ */
2644
+ private readonly pendingAnswer;
2645
+ private readonly pendingDecline;
2646
+ private readonly answerDeadlines;
2647
+ private readonly suppressHoldEcho;
2648
+ private readonly wakeDeadlines;
2649
+ private readonly reportingCalls;
2650
+ private readonly traceLines;
2651
+ private traceSnapshot;
2652
+ private readonly traceListeners;
2653
+ constructor(options: NativeCallBridgeOptions);
2654
+ /** Idempotent. Call from the app entry on every path (window and headless). */
2655
+ start(): void;
2656
+ /**
2657
+ * Resolving means the SIP AOR is REGISTERed, so a parked INVITE can reach us.
2658
+ * The phone joins a connect already in flight, so this never starts a second
2659
+ * one. Never `reconnect()` here — that disposes live calls.
2660
+ *
2661
+ * `forWake` is the corpse-proofing path: when the OS freezes a backgrounded app
2662
+ * it can kill the WebSocket with no close frame reaching JS, so no `disconnected`
2663
+ * fires and `isConnected` still reads "connected" over a dead socket. Trusting them never re-REGISTERs the AOR and the parked call is never
2664
+ * re-forked. So a wake with NO live call sheds whatever socket state we hold and
2665
+ * reconnects for real. A wake WITH a live call skips this: that call proves the
2666
+ * socket and a teardown would drop it. A connect still in flight is joined, not
2667
+ * shed: it has no stale socket to distrust, and its own timeouts bound it.
2668
+ */
2669
+ ensureConnected(forWake?: boolean): Promise<void>;
2670
+ /** In-app Answer, routed through the OS so it converges on the one answer path. */
2671
+ answer(callId: string): void;
2672
+ /** In-app End/Decline, via the same convergence. */
2673
+ end(callId: string): void;
2674
+ /**
2675
+ * Place an outbound call, reported to the OS FIRST so on Android the foreground
2676
+ * service starts and the call survives backgrounding mid-dial (otherwise the OS
2677
+ * reaps the process and drops the call). If placement fails we end the session
2678
+ * we opened; from then on it rides the same `watchCall` lifecycle.
2679
+ *
2680
+ * An OS-initiated dial (`callIntent`) comes through here too, so both
2681
+ * directions converge on one path.
2682
+ */
2683
+ call(destination: string, displayName?: string | null): Promise<BridgeCall>;
2684
+ /** Ring buffer for a debug view; one-way, `useSyncExternalStore`-shaped. */
2685
+ getTrace(): readonly string[];
2686
+ subscribeTrace(listener: () => void): () => void;
2687
+ private subscribePhone;
2688
+ private onIncoming;
2689
+ private applyHeld;
2690
+ private reportIncoming;
2691
+ private endDuplicateSession;
2692
+ /**
2693
+ * An OS answer we could not act on yet. The OS holds its answer request open
2694
+ * meanwhile — CallKit keeps the incoming UI up, Telecom leaves the call in
2695
+ * CONNECTING — so if no call ever arrives to consume it, say so rather than
2696
+ * leaving the phone ringing until the adapter's own far longer deadline.
2697
+ *
2698
+ * Handle-stored and cleared by `forgetSession`, like the wake deadlines. It
2699
+ * has to be: session ids are recycled, so a timer left armed past its own
2700
+ * session fires against a LATER tap on the same id and reports a live call
2701
+ * failed. `armedAt` is the second half of that — it pins the timer to the one
2702
+ * tap it was armed for even if the map was refilled meanwhile.
2703
+ */
2704
+ private armAnswerDeadline;
2705
+ private dropHeldAnswer;
2706
+ private answerCall;
2707
+ private reportConnectedReliably;
2708
+ private watchCall;
2709
+ private reportHold;
2710
+ /**
2711
+ * Hang up, and make sure the call ends. The SDK sends one frame; this retries
2712
+ * one the server never answers and, once the attempts run out, ends the call
2713
+ * here so the OS and the app don't keep a call nothing can reach.
2714
+ *
2715
+ * An error reply means the server has no such call, and the SDK ends it. A
2716
+ * closed socket ends it too, through the lost session.
2717
+ */
2718
+ private endCall;
2719
+ /**
2720
+ * Hold or resume, confirmed. Resolves once the call's held/resumed arrives.
2721
+ *
2722
+ * The far end refusing on state (`invalid_message`) or a closed socket rejects
2723
+ * at once and leaves the call alone. A failure on the server's side (far end
2724
+ * unreachable) or no reply is retried; once the attempts run out the in-dialog
2725
+ * route is dead and every other action would fail the same way, so the call is
2726
+ * ended and the promise rejects.
2727
+ */
2728
+ private changeHold;
2729
+ private confirmHold;
2730
+ private attempt;
2731
+ /** The pause after a failed attempt; none after the last, which has nothing to wait for. */
2732
+ private backoffAfter;
2733
+ private abandonCall;
2734
+ private teardownCall;
2735
+ private reportEndedReliably;
2736
+ private subscribeOs;
2737
+ private onIncomingReported;
2738
+ private armWakeDeadline;
2739
+ private forgetSession;
2740
+ private wakeInFlight;
2741
+ /** How many OS-reported calls are still live — the OS's session count. */
2742
+ private liveOsCallCount;
2743
+ private linkOsSession;
2744
+ /**
2745
+ * Check, shortly after a change, that the OS shows what the call is.
2746
+ *
2747
+ * Every OS report above is retried when it fails, but a report the OS takes
2748
+ * and then doesn't act on goes unseen: iOS accepts a connected report for an
2749
+ * incoming call and keeps it ringing. So compare what the OS says it shows
2750
+ * with the call, and correct the OS; the call's state is the server's, and
2751
+ * the OS follows it.
2752
+ *
2753
+ * Only the session the OS calls active is read, since that is all the port
2754
+ * offers. It is the one on screen, and with the default of one OS call it is
2755
+ * every session there is.
2756
+ */
2757
+ private scheduleReconcile;
2758
+ private reconcile;
2759
+ private reconcileConnected;
2760
+ private reconcileHeld;
2761
+ private callForSession;
2762
+ private subscribeLifecycle;
2763
+ private dropRegistrationIfIdle;
2764
+ private trace;
2765
+ }
2766
+
2767
+ /** SoftphoneProvider (React Native), same API as the web provider. */
2202
2768
 
2203
2769
  type AppearanceOptions = Parameters<typeof resolveSoftphonePalette>[0];
2204
2770
  type ConnectionState = SoftphoneConnectionState;
2205
2771
  interface SoftphoneProviderProps {
2772
+ /**
2773
+ * Adopt an existing phone instead of constructing one from `token`, for a
2774
+ * native call surface where a call can exist before any phone does. Neither
2775
+ * connected nor disconnected by the provider.
2776
+ */
2777
+ existingPhone?: DialStackPhone | null;
2206
2778
  /** WebRTC user session token. */
2207
2779
  token: string;
2208
2780
  /**
2209
- * Persistence for the selected E911 address id, so it survives app restarts.
2210
- * REQUIRED on React Native: the SDK takes no persistence dependency of its own
2211
- * (react-native-mmkv and AsyncStorage vary by version/architecture and can't be
2212
- * defaulted safely), so the host supplies a small `PlatformStorage` adapter —
2213
- * typically MMKV- or AsyncStorage-backed. See the example apps for reference
2214
- * adapters; a synchronous in-memory adapter is also valid if you don't need
2215
- * persistence. It only stores the E911 id, nothing sensitive.
2781
+ * Persistence for the selected E911 address id. REQUIRED on RN: the SDK takes
2782
+ * no persistence dependency of its own. A synchronous in-memory adapter is
2783
+ * valid if you don't need persistence.
2216
2784
  */
2217
2785
  storage: PlatformStorage;
2218
2786
  /** API base URL (defaults to the SDK's production endpoint). */
2219
2787
  apiBaseUrl?: string;
2220
2788
  /**
2221
- * Called shortly before the session token expires (about 60 seconds ahead).
2222
- * Return a fresh token minted by your backend; the SDK delivers it in-band over
2223
- * the existing connection — no reconnect and no call disruption. If it rejects,
2224
- * the SDK keeps the (still-valid) connection open and surfaces an `error`.
2789
+ * Called shortly before the session token expires. Return a fresh token; the
2790
+ * SDK delivers it in-band with no reconnect. On reject the SDK keeps the
2791
+ * still-valid connection and surfaces an `error`.
2225
2792
  */
2226
2793
  onTokenExpiring?: () => Promise<string>;
2227
- /** Emergency (E911) address id to present on connect; when supplied the host
2228
- * manages E911 and the built-in prompt is disabled. */
2794
+ /** E911 address id to present on connect; when supplied the host manages E911
2795
+ * and the built-in prompt is disabled. */
2229
2796
  emergencyAddressId?: string;
2230
2797
  /**
2231
- * Optional device-location source for the E911 form. When provided, the
2232
- * built-in emergency-address form shows a "Use my current location" action
2233
- * that calls this to prefill the address fields; the host owns the location
2234
- * permission prompt, geolocation, and reverse-geocoding. Omit for manual entry.
2798
+ * Device-location source for the E911 form's "Use my current location" action;
2799
+ * the host owns the permission prompt, geolocation, and reverse-geocoding.
2235
2800
  */
2236
2801
  locationProvider?: () => Promise<EmergencyAddressInput>;
2237
2802
  /** Connect automatically once mounted (default: true). */
2238
2803
  autoConnect?: boolean;
2239
- /** Theming — the shared appearance surface (same as the web softphone). */
2804
+ /** Theming — the shared appearance surface. */
2240
2805
  appearance?: AppearanceOptions;
2241
- /** Locale for UI strings (defaults to English), same surface as the web softphone. */
2806
+ /** Locale for UI strings (defaults to English). */
2242
2807
  locale?: Locale;
2243
2808
  /** Default country for number formatting. */
2244
2809
  defaultCountry?: CountryCode;
@@ -2246,6 +2811,9 @@ interface SoftphoneProviderProps {
2246
2811
  state: ConnectionState;
2247
2812
  }) => void;
2248
2813
  onIncomingCall?: (event: {
2814
+ /** Handle for `callActionsFor()` / `answerCall()`, and what a host bridging
2815
+ * to a native call UI binds its OS session to. */
2816
+ callId: string;
2249
2817
  from: string;
2250
2818
  fromName: string | null;
2251
2819
  }) => void;
@@ -2260,17 +2828,39 @@ interface SoftphoneProviderProps {
2260
2828
  code: string;
2261
2829
  message: string;
2262
2830
  }) => void;
2831
+ /**
2832
+ * The call bridge, when this app reports calls to the OS. Outbound placement
2833
+ * then goes through it, so the OS learns about the call BEFORE it is dialled —
2834
+ * on Android that report is what starts the foreground service, without which
2835
+ * backgrounding mid-dial lets the OS reap the process and drop the call.
2836
+ * Answering in-app goes through it too, so the OS answers before the call
2837
+ * starts media (on iOS that is what gives the call its audio).
2838
+ */
2839
+ bridge?: Pick<NativeCallBridge, 'call' | 'answer'>;
2840
+ /**
2841
+ * Who owns call audio: ringing, outbound ringback, the audio session and the
2842
+ * proximity sensor. `'sdk'` (the default): the provider does all of it.
2843
+ * `'host'`: the provider does none of it, for an app that reports calls to
2844
+ * CallKit/Telecom itself, which ring the phone and own the session. Doing any
2845
+ * of it on top of the OS breaks capture. Always `'host'` with a `bridge`.
2846
+ */
2847
+ callAudio?: CallAudioOwner;
2848
+ /**
2849
+ * The output route, for a host that owns call audio: without it the in-call
2850
+ * speaker button is disabled, since only the OS integration can move the
2851
+ * route. Ignored unless call audio is `'host'`.
2852
+ */
2853
+ audioOutput?: AudioOutputController;
2263
2854
  children: React.ReactNode;
2264
2855
  }
2265
- /**
2266
- * The native softphone context: the shared base plus the native-only
2267
- * `locationProvider`. (`palette` is on the base — computed identically to web.)
2268
- */
2856
+ /** The native softphone context: the shared base plus the native-only
2857
+ * `locationProvider`. */
2269
2858
  interface SoftphoneContextValue extends SoftphoneContextBase {
2270
2859
  /** Host-supplied device-location source for the E911 form, or undefined. */
2271
2860
  locationProvider: (() => Promise<EmergencyAddressInput>) | undefined;
2861
+ audio: UseAudioOutput;
2272
2862
  }
2273
- declare function SoftphoneProvider({ token, storage, apiBaseUrl, onTokenExpiring, emergencyAddressId, locationProvider, autoConnect, appearance, locale, defaultCountry, onConnectionStateChange, onIncomingCall, onCallStarted, onCallEnded, onError, children, }: SoftphoneProviderProps): React.JSX.Element;
2863
+ declare function SoftphoneProvider({ existingPhone, token, storage, apiBaseUrl, onTokenExpiring, emergencyAddressId, locationProvider, autoConnect, appearance, locale, defaultCountry, onConnectionStateChange, onIncomingCall, onCallStarted, onCallEnded, onError, bridge, callAudio: requestedCallAudio, audioOutput, children, }: SoftphoneProviderProps): React.JSX.Element;
2274
2864
  /** Access the full softphone context. Throws when used outside the provider. */
2275
2865
  declare function useSoftphone(): SoftphoneContextValue;
2276
2866
  /** The active foreground call + its actions (build-your-own convenience). */
@@ -2278,6 +2868,8 @@ declare function useActiveCall(): {
2278
2868
  activeCall: Call | null;
2279
2869
  actions: UseCallActions;
2280
2870
  };
2871
+ /** The call-audio output route and the speaker toggle. */
2872
+ declare function useAudioOutput(): UseAudioOutput;
2281
2873
  /** The currently-ringing inbound call, or null. */
2282
2874
  declare function useIncomingCall(): Call | null;
2283
2875
 
@@ -2317,10 +2909,10 @@ declare function Softphone({ autoFocusDestination }: SoftphoneProps): React.JSX.
2317
2909
  */
2318
2910
 
2319
2911
  interface DialPadProps {
2320
- /** Autofocus the destination field on mount. */
2321
2912
  autoFocusDestination?: boolean;
2913
+ compact?: boolean;
2322
2914
  }
2323
- declare function DialPad({ autoFocusDestination }: DialPadProps): React.JSX.Element;
2915
+ declare function DialPad({ autoFocusDestination, compact, }: DialPadProps): React.JSX.Element;
2324
2916
 
2325
2917
  /**
2326
2918
  * IncomingCall / IncomingStack (RN) — the ringing inbound UI.
@@ -2363,5 +2955,38 @@ declare function OngoingCall(): React.JSX.Element | null;
2363
2955
 
2364
2956
  declare function EmergencyBanner(): React.JSX.Element | null;
2365
2957
 
2366
- export { Call, DialPad, EmergencyBanner, IncomingCall, OngoingCall, Softphone, SoftphoneProvider, useActiveCall, useIncomingCall, useSoftphone };
2367
- export type { CallEndReason, CallState, ConnectionState, DialPadProps, EmergencyAddressInput, PlatformStorage, SoftphoneContextValue, SoftphoneProps, SoftphoneProviderProps, UseCallActions };
2958
+ type CreatePhoneOptions = Omit<PhoneOptions, 'createSignalingSocket' | 'deferInboundCapture'>;
2959
+ /**
2960
+ * The process's one phone.
2961
+ *
2962
+ * Module scope is the correct scope, not a shortcut: a push wake and the UI run
2963
+ * in the same JS runtime but enter from different places (a headless task, the
2964
+ * app window), so this is a plain shared reference. Two phones would register
2965
+ * the same SIP AOR twice and the call would go to whichever registered last
2966
+ * while the UI watched the other — a silent failure, so a second construction
2967
+ * fails loudly instead. Construct eagerly at app entry: `new DialStackPhone()`
2968
+ * does no I/O (only `connect()` does).
2969
+ */
2970
+ declare function createPhone(options: CreatePhoneOptions): DialStackPhone;
2971
+ declare function getPhone(): DialStackPhone | null;
2972
+ /** For consumers that must not be the one to create it (the UI). */
2973
+ declare function requirePhone(): DialStackPhone;
2974
+
2975
+ declare const appLifecycle: AppLifecycle;
2976
+ declare const DEFAULT_WAKE_TASK = "DialStackWake";
2977
+ /**
2978
+ * Android's wake: the adapter's native push service reports the call to Telecom
2979
+ * with no JS, and something must then start a runtime so the SDK can
2980
+ * re-REGISTER and receive the parked INVITE. That is this headless task; the
2981
+ * app's HeadlessJsTaskService names it (`taskName`).
2982
+ *
2983
+ * iOS has no equivalent — PushKit launches the process and React Native starts
2984
+ * normally, so the app entry's `bridge.start()` is the whole wake path.
2985
+ *
2986
+ * A single file with a runtime branch rather than `.android.ts`/`.ios.ts`: the
2987
+ * package ships as one bundle, so Metro's platform extensions cannot apply.
2988
+ */
2989
+ declare function registerWakeTask(bridge: NativeCallBridge, taskName?: string): void;
2990
+
2991
+ export { Call, DEFAULT_WAKE_TASK, DialPad, EmergencyBanner, IncomingCall, NativeCallBridge, OngoingCall, RuntimeHold, Softphone, SoftphoneProvider, appLifecycle, createPhone, getPhone, nativeSignalingSocket, registerWakeTask, requirePhone, toOsEndReason, useActiveCall, useAudioOutput, useIncomingCall, useSoftphone };
2992
+ export type { AppLifecycle, AppLifecycleState, AudioOutput, AudioOutputController, AudioOutputKind, BridgeCall, BridgePhone, BridgePhoneError, BridgeTimers, CallEndReason, CallState, ConnectionState, CreatePhoneOptions, DialPadProps, EmergencyAddressInput, NativeCallBridgeOptions, OsActiveSession, OsCallAdapter, OsCallEventName, OsCallEvents, OsEndReason, OsIncomingCall, OsOutgoingCall, OsSessionId, OsSessionStatus, PlatformStorage, SoftphoneContextValue, SoftphoneProps, SoftphoneProviderProps, UseAudioOutput, UseCallActions };