@dialstack/sdk-native 0.1.0 → 0.2.0

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,25 @@ 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
+ /** Set by the owner to surface a late mic failure, since `Call` has no error event. */
650
+ private reportCaptureFailure;
651
+ onCaptureFailure(report: (e: unknown) => void): void;
594
652
  whenLocalMediaReady(): Promise<void>;
595
653
  private acquireLocalMedia;
596
654
  /** The selected mic, or null for the OS default. */
@@ -607,6 +665,7 @@ declare class Call {
607
665
  setAudioOutputDevice(deviceId: string | null): void;
608
666
  private watchCaptureTrack;
609
667
  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;
@@ -1095,37 +1228,19 @@ interface AppearanceOptions$1 {
1095
1228
  * the `appearance` prop flows through identically on both platforms.
1096
1229
  */
1097
1230
 
1098
- /** Flat color set the Softphone paints from, after appearance is resolved. */
1099
1231
  interface SoftphonePalette {
1100
- /** Component background. */
1101
1232
  background: string;
1102
- /** Dial-pad key / control surface fill. */
1103
1233
  surface: string;
1104
- /** Surface fill while pressed/active. */
1105
1234
  surfaceActive: string;
1106
- /** Primary text (digits, destination, peer name). */
1107
1235
  text: string;
1108
- /** Secondary text (letter sub-labels, call state, hints). */
1109
1236
  textSecondary: string;
1110
- /** Hairline borders/dividers. */
1111
1237
  border: string;
1112
- /** Accent (links, focus ring, keypad/secondary control highlight). */
1113
1238
  accent: string;
1114
- /** Call / Answer (green) action color. */
1115
1239
  success: string;
1116
- /** Hang up / Decline (red) action color. */
1117
1240
  danger: string;
1118
- /** Warning / caution (amber) — used by the E911 banner. */
1119
1241
  warning: string;
1120
- /** Foreground used on top of accent/success/danger fills. */
1121
1242
  onAccent: string;
1122
1243
  }
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
1244
  declare function resolveSoftphonePalette(appearance?: AppearanceOptions$1): SoftphonePalette;
1130
1245
 
1131
1246
  /**
@@ -1180,6 +1295,9 @@ declare const en: {
1180
1295
  addCall: string;
1181
1296
  addCallPlaceholder: string;
1182
1297
  addCallSend: string;
1298
+ merge: string;
1299
+ split: string;
1300
+ conferenceLabel: string;
1183
1301
  cancel: string;
1184
1302
  stateTrying: string;
1185
1303
  stateRinging: string;
@@ -2031,6 +2149,7 @@ declare const en: {
2031
2149
  perUser: string;
2032
2150
  perPhoneNumber: string;
2033
2151
  perVoiceAiLocation: string;
2152
+ voiceAiPerLocationNote: string;
2034
2153
  perMonth: string;
2035
2154
  rateNotSet: string;
2036
2155
  pendingRate: string;
@@ -2179,6 +2298,19 @@ interface SoftphoneContextBase {
2179
2298
  completeAttendedTransfer: () => void;
2180
2299
  cancelAttendedTransfer: () => void;
2181
2300
  placeCall: (destination: string) => Promise<void>;
2301
+ /** The calls bridged into a local three-way conference (empty when not merged). */
2302
+ mergedCalls: Call[];
2303
+ isMerged: boolean;
2304
+ /** While merged, every remote party mixed — what the local user hears. */
2305
+ conferenceAudio: MediaStream | null;
2306
+ /** Whether merging is possible: two connected calls, no transfer, WebAudio present. */
2307
+ canMerge: boolean;
2308
+ mergeCalls: () => void;
2309
+ splitMerge: () => void;
2310
+ /** Hang up every leg of the conference (the merged UI shows one Hang up). */
2311
+ hangupConference: () => void;
2312
+ /** Hold or resume every leg of the conference together. */
2313
+ holdConference: (held: boolean) => void;
2182
2314
  emergency: UseEmergencyBinding;
2183
2315
  emergencyManagedByHost: boolean;
2184
2316
  lastError: {
@@ -2193,52 +2325,476 @@ interface SoftphoneContextBase {
2193
2325
  }
2194
2326
 
2195
2327
  /**
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.
2328
+ * Its own module so `createPhone()` can use it without importing the provider
2329
+ * (and React with it): a push-woken headless runtime has no renderer, yet it
2330
+ * needs the same header or the ingress 403s its handshake.
2331
+ */
2332
+ declare const nativeSignalingSocket: SignalingSocketFactory;
2333
+
2334
+ type AudioOutputKind = 'earpiece' | 'speaker' | 'wired' | 'bluetooth' | 'car' | 'airplay' | 'other';
2335
+ interface AudioOutput {
2336
+ id: string;
2337
+ kind: AudioOutputKind;
2338
+ name: string;
2339
+ }
2340
+ /**
2341
+ * Call-audio routing, implemented by a host that owns call audio
2342
+ * (`callAudio="host"`) over whatever its OS integration exposes.
2343
+ *
2344
+ * `current()` must be what the OS reports, never what was last requested: the
2345
+ * route also changes from the CallKit/Telecom UI, a headset, or a car, and the
2346
+ * softphone shows only this. It must return the same object until the route
2347
+ * changes.
2348
+ */
2349
+ interface AudioOutputController {
2350
+ current(): AudioOutput | null;
2351
+ onChange(listener: (output: AudioOutput | null) => void): () => void;
2352
+ setSpeaker(on: boolean): Promise<void>;
2353
+ }
2354
+ interface UseAudioOutput {
2355
+ supported: boolean;
2356
+ current: AudioOutput | null;
2357
+ speakerOn: boolean;
2358
+ toggleSpeaker: () => void;
2359
+ }
2360
+
2361
+ type CallAudioOwner = 'sdk' | 'host';
2362
+
2363
+ /**
2364
+ * The OS call surface (CallKit on iOS, Telecom on Android) as the bridge needs
2365
+ * it — shaped by the bridge, not by any library. See the contract suite for
2366
+ * what an adapter must guarantee.
2367
+ */
2368
+ /** OS-side call identity: the UUID CallKit/Telecom track the call under. */
2369
+ type OsSessionId = string;
2370
+ interface OsIncomingCall {
2371
+ callId: string;
2372
+ /** Caller handle as the OS should display/dial it. */
2373
+ from: string;
2374
+ displayName?: string | null;
2375
+ }
2376
+ interface OsOutgoingCall {
2377
+ /** The recipient handle the OS should display/dial. */
2378
+ to: string;
2379
+ displayName?: string | null;
2380
+ }
2381
+ /**
2382
+ * Why the call ended, in the OS's vocabulary. `answeredElsewhere` /
2383
+ * `declinedElsewhere` make the OS dismiss the ring silently instead of logging
2384
+ * a missed call.
2385
+ */
2386
+ type OsEndReason = 'remoteEnded' | 'unanswered' | 'answeredElsewhere' | 'declinedElsewhere' | 'failed' | 'unknown';
2387
+ /** Where the OS thinks a session is. `connecting` is answered but not yet reported connected. */
2388
+ type OsSessionStatus = 'ringing' | 'connecting' | 'connected' | 'ended';
2389
+ interface OsActiveSession {
2390
+ sessionId: OsSessionId;
2391
+ callId: string | null;
2392
+ /**
2393
+ * The OS's own view, for reconciling against the SDK call. Optional: an
2394
+ * adapter that can't read it leaves it out and the bridge doesn't reconcile
2395
+ * that field.
2396
+ */
2397
+ status?: OsSessionStatus;
2398
+ held?: boolean;
2399
+ }
2400
+ interface OsCallEvents {
2401
+ /**
2402
+ * A call was reported to the OS — by native code before JS existed, or by
2403
+ * `reportIncoming`. `callId` is the call_id the session was created with (a
2404
+ * wake push's, or the one passed to `reportIncoming`): the bridge pairs the
2405
+ * session with its call by it, and ends a session that has none.
2406
+ */
2407
+ incomingReported: (e: OsActiveSession) => void;
2408
+ /** The OS wants to PLACE a call — the user dialled from the platform's own UI
2409
+ * (Recents, Siri, dialer intent), not from the app. */
2410
+ callIntent: (e: {
2411
+ handle: string;
2412
+ }) => void;
2413
+ answer: (e: {
2414
+ sessionId: OsSessionId;
2415
+ }) => void;
2416
+ end: (e: {
2417
+ sessionId: OsSessionId;
2418
+ }) => void;
2419
+ setHeld: (e: {
2420
+ sessionId: OsSessionId;
2421
+ held: boolean;
2422
+ }) => void;
2423
+ setMuted: (e: {
2424
+ sessionId: OsSessionId;
2425
+ muted: boolean;
2426
+ }) => void;
2427
+ dtmf: (e: {
2428
+ sessionId: OsSessionId;
2429
+ digits: string;
2430
+ }) => void;
2431
+ /** iOS only in practice: CallKit gates audio, so WebRTC audio starts here. */
2432
+ audioSessionActivated: (e: {
2433
+ sessionId: OsSessionId | null;
2434
+ }) => void;
2435
+ }
2436
+ type OsCallEventName = keyof OsCallEvents;
2437
+ interface OsCallAdapter {
2438
+ /** Resolves to the OS-assigned session id (the OS assigns, never the caller). */
2439
+ reportIncoming(call: OsIncomingCall): Promise<OsSessionId>;
2440
+ /**
2441
+ * Report an outbound call so the OS holds a live session — on Android that
2442
+ * starts the foreground service keeping the process and call alive when
2443
+ * backgrounded, which an outbound call otherwise lacks.
2444
+ */
2445
+ reportOutgoing(call: OsOutgoingCall): Promise<OsSessionId>;
2446
+ /** The call the OS is showing as incoming/dialling is now connected. */
2447
+ reportConnected(sessionId: OsSessionId): Promise<void>;
2448
+ /** Must not re-emit `end` — the bridge is telling the OS, not the other way round. */
2449
+ reportEnded(sessionId: OsSessionId, reason: OsEndReason): Promise<void>;
2450
+ /** Remote-initiated state only; the OS already knows about its own actions. */
2451
+ setHeld(sessionId: OsSessionId, held: boolean): Promise<void>;
2452
+ setMuted(sessionId: OsSessionId, muted: boolean): Promise<void>;
2453
+ /** The session the OS currently treats as active, if any. */
2454
+ getActiveSession(): Promise<OsActiveSession | null>;
2455
+ /**
2456
+ * The app's own Answer/End buttons go through the OS too, so every answer —
2457
+ * lock screen, notification, car, watch, in-app — arrives as the same
2458
+ * `answer`/`end` event and there is one code path, not two.
2459
+ */
2460
+ answer(sessionId: OsSessionId): Promise<void>;
2461
+ end(sessionId: OsSessionId): Promise<void>;
2462
+ on<K extends OsCallEventName>(event: K, listener: OsCallEvents[K]): () => void;
2463
+ }
2464
+
2465
+ /**
2466
+ * Where the bridge schedules its deadlines and retries. The JS timers are the
2467
+ * default, but React Native on Android pauses them in an app that is
2468
+ * backgrounded without being killed, even while a headless task runs. A wake
2469
+ * whose call never arrives (answered or cancelled before the app re-registered)
2470
+ * then waits on a deadline that never fires, and the OS keeps ringing. An
2471
+ * integration that can schedule natively passes timers here.
2472
+ */
2473
+ interface BridgeTimers {
2474
+ setTimeout(fn: () => void, ms: number): unknown;
2475
+ clearTimeout(handle: unknown): void;
2476
+ }
2477
+
2478
+ /**
2479
+ * "Keep this JS runtime alive" as a promise the platform can await.
2480
+ *
2481
+ * Android's HeadlessJsTaskService tears the runtime down the moment the task
2482
+ * promise settles, so a wake holds this from the instant the task starts until
2483
+ * live calls hit zero. What the hold actually covers is the gap before the call
2484
+ * library's foreground service exists — once that is up it holds the process,
2485
+ * and a ceiling reached mid-call ends only this task, not the call (measured: a
2486
+ * 60s ring survived the then-45s ceiling untouched).
2487
+ *
2488
+ * So the ceiling is a bound on a wake that never delivers, not a correctness
2489
+ * guarantee. It is set to the same 300s as the max park/ring, the service's task
2490
+ * timeout, the OS answer deadline and the library's incoming-call timeout purely
2491
+ * so the numbers don't contradict each other; keep them aligned.
2201
2492
  */
2493
+ declare class RuntimeHold {
2494
+ private readonly ceilingMs;
2495
+ private readonly timers;
2496
+ private release;
2497
+ private waiting;
2498
+ constructor(ceilingMs?: number, timers?: BridgeTimers);
2499
+ acquire(): Promise<void>;
2500
+ releaseNow(): void;
2501
+ /** Non-null while a hold is in flight — the reliable "a wake is mid-flight" signal. */
2502
+ pending(): Promise<void> | null;
2503
+ private settle;
2504
+ }
2505
+
2506
+ /**
2507
+ * What the bridge needs from a call. Overloads rather than a generic `on`: a
2508
+ * generic over a narrower event map is not assignable from the SDK's wider one.
2509
+ */
2510
+ interface BridgeCall {
2511
+ readonly id: string;
2512
+ readonly from: string;
2513
+ readonly fromName: string | null;
2514
+ readonly state: CallState;
2515
+ readonly isMuted: boolean;
2516
+ answer(): void;
2517
+ reject(reason?: 'busy' | 'decline'): void;
2518
+ hangup(): void;
2519
+ hold(): void;
2520
+ resume(): void;
2521
+ mute(): void;
2522
+ unmute(): void;
2523
+ sendDtmf(digits: string): void;
2524
+ on(event: 'answered' | 'resumed', handler: () => void): void;
2525
+ on(event: 'held', handler: (by: HeldBy) => void): void;
2526
+ on(event: 'ended', handler: (reason: CallEndReason) => void): void;
2527
+ off(event: 'held' | 'resumed' | 'ended', handler: (...args: never[]) => void): void;
2528
+ }
2529
+ interface BridgePhoneError {
2530
+ code?: string;
2531
+ message?: string;
2532
+ fatal?: boolean;
2533
+ callId?: string | null;
2534
+ }
2535
+ /** What the bridge needs from the phone — `DialStackPhone` satisfies it. */
2536
+ interface BridgePhone {
2537
+ readonly isConnected: boolean;
2538
+ connect(): Promise<void>;
2539
+ disconnect(): void;
2540
+ setToken(token: string): void;
2541
+ /** Place an outbound call. The bridge reports it to the OS around this. */
2542
+ call(destination: string): Promise<BridgeCall>;
2543
+ on(event: 'connected' | 'reconnected', handler: () => void): void;
2544
+ on(event: 'disconnected', handler: (error?: unknown) => void): void;
2545
+ on(event: 'reconnecting', handler: (attempt: number, delayMs: number) => void): void;
2546
+ on(event: 'incoming', handler: (call: BridgeCall) => void): void;
2547
+ on(event: 'error', handler: (err: BridgePhoneError) => void): void;
2548
+ }
2549
+ type AppLifecycleState = 'active' | 'background' | 'inactive' | 'unknown' | 'extension';
2550
+ /** Foreground/background as a port so the bridge has no react-native import. */
2551
+ interface AppLifecycle {
2552
+ onChange(listener: (state: AppLifecycleState) => void): () => void;
2553
+ }
2554
+ interface NativeCallBridgeOptions {
2555
+ phone: BridgePhone;
2556
+ os: OsCallAdapter;
2557
+ lifecycle?: AppLifecycle;
2558
+ hold?: RuntimeHold;
2559
+ log?: (message: string) => void;
2560
+ /** From the OS report until `connect()` resolves: a socket that never comes up. */
2561
+ registrationDeadlineMs?: number;
2562
+ /**
2563
+ * From `connect()` resolving until `incoming`. The proxy resumes a parked INVITE
2564
+ * on REGISTER or within its 1s poll, so after this nothing is coming.
2565
+ */
2566
+ deliveryDeadlineMs?: number;
2567
+ /** An OS answer held for a call that has not arrived yet is stale past this. */
2568
+ answerTtlMs?: number;
2569
+ /**
2570
+ * How many calls the integration puts on the OS call screen at once (default 1).
2571
+ * A second concurrent call past the cap is NOT reported to the OS but still
2572
+ * rings and is answerable in-app; it just gets no OS session. Calls pair with
2573
+ * sessions by call_id, so any value is safe; raising it is a product choice
2574
+ * (OS call-waiting) and needs an adapter whose library holds several sessions.
2575
+ */
2576
+ maxOsCalls?: number;
2577
+ /** Defaults to the JS timers; see `BridgeTimers` for when to pass native ones. */
2578
+ timers?: BridgeTimers;
2579
+ now?: () => number;
2580
+ }
2581
+ declare function toOsEndReason(reason: CallEndReason): OsEndReason;
2582
+ /**
2583
+ * The bidirectional bridge between the OS call surface and the SDK (OS↔SDK
2584
+ * answer/end/mute/hold/DTMF and lifecycle), and the only thing that subscribes to
2585
+ * either. Works with no renderer: a push-woken headless runtime starts it like
2586
+ * the app window does.
2587
+ *
2588
+ * ## Pairing a session with a call
2589
+ *
2590
+ * A call has one `call_id` everywhere: on the socket, in the wake webhook and on
2591
+ * the INVITE a wake resumes. The OS mints its own session id at setup, and the
2592
+ * bridge links it to that `call_id` (`ids`) the moment it knows both: when it
2593
+ * reports a call itself, when an outbound call is placed, and when the OS
2594
+ * reports a wake push's session (`incomingReported`, carrying the `call_id` the
2595
+ * push delivered). Every lookup goes through that link; none guesses by
2596
+ * position. A second session for a `call_id` already linked is the same call
2597
+ * shown twice, and is ended.
2598
+ *
2599
+ * The contract this puts on the integrator: the wake push carries the
2600
+ * webhook's `call_id`, and the OS adapter reports it on `incomingReported`.
2601
+ */
2602
+ declare class NativeCallBridge {
2603
+ readonly hold: RuntimeHold;
2604
+ private readonly timers;
2605
+ private readonly phone;
2606
+ private readonly os;
2607
+ private readonly lifecycle;
2608
+ private readonly log;
2609
+ private readonly registrationDeadlineMs;
2610
+ private readonly deliveryDeadlineMs;
2611
+ private readonly answerTtlMs;
2612
+ private readonly maxOsCalls;
2613
+ private readonly now;
2614
+ private started;
2615
+ private appState;
2616
+ private reconnecting;
2617
+ private readonly ids;
2618
+ private readonly calls;
2619
+ private readonly callErrorWaiters;
2620
+ private readonly ending;
2621
+ private readonly changingHold;
2622
+ private reconcileTimer;
2623
+ /** Outbound calls still being placed, so not yet in `calls`. */
2624
+ private placingOutbound;
2625
+ /** Calls this app placed. For these `answered` means the far end picked up. */
2626
+ private readonly outboundCalls;
2627
+ /** OS sessions created by a native wake report, before their call arrives on the socket. */
2628
+ private readonly wakeSessions;
2629
+ private readonly reportedToOs;
2630
+ private readonly connectedReported;
2631
+ /**
2632
+ * An OS answer/decline that landed before the SDK had the call. On a wake the
2633
+ * user taps within ~200ms while connect() takes seconds, so it's applied on arrival.
2634
+ */
2635
+ private readonly pendingAnswer;
2636
+ private readonly pendingDecline;
2637
+ private readonly answerDeadlines;
2638
+ private readonly suppressHoldEcho;
2639
+ private readonly wakeDeadlines;
2640
+ private readonly reportingCalls;
2641
+ private readonly traceLines;
2642
+ private traceSnapshot;
2643
+ private readonly traceListeners;
2644
+ constructor(options: NativeCallBridgeOptions);
2645
+ /** Idempotent. Call from the app entry on every path (window and headless). */
2646
+ start(): void;
2647
+ /**
2648
+ * Resolving means the SIP AOR is REGISTERed, so a parked INVITE can reach us.
2649
+ * The phone joins a connect already in flight, so this never starts a second
2650
+ * one. Never `reconnect()` here — that disposes live calls.
2651
+ *
2652
+ * `forWake` is the corpse-proofing path: when the OS freezes a backgrounded app
2653
+ * it can kill the WebSocket with no close frame reaching JS, so no `disconnected`
2654
+ * fires and `isConnected` still reads "connected" over a dead socket. Trusting them never re-REGISTERs the AOR and the parked call is never
2655
+ * re-forked. So a wake with NO live call sheds whatever socket state we hold and
2656
+ * reconnects for real. A wake WITH a live call skips this: that call proves the
2657
+ * socket and a teardown would drop it. A connect still in flight is joined, not
2658
+ * shed: it has no stale socket to distrust, and its own timeouts bound it.
2659
+ */
2660
+ ensureConnected(forWake?: boolean): Promise<void>;
2661
+ /** In-app Answer, routed through the OS so it converges on the one answer path. */
2662
+ answer(callId: string): void;
2663
+ /** In-app End/Decline, via the same convergence. */
2664
+ end(callId: string): void;
2665
+ /**
2666
+ * Place an outbound call, reported to the OS FIRST so on Android the foreground
2667
+ * service starts and the call survives backgrounding mid-dial (otherwise the OS
2668
+ * reaps the process and drops the call). If placement fails we end the session
2669
+ * we opened; from then on it rides the same `watchCall` lifecycle.
2670
+ *
2671
+ * An OS-initiated dial (`callIntent`) comes through here too, so both
2672
+ * directions converge on one path.
2673
+ */
2674
+ call(destination: string, displayName?: string | null): Promise<BridgeCall>;
2675
+ /** Ring buffer for a debug view; one-way, `useSyncExternalStore`-shaped. */
2676
+ getTrace(): readonly string[];
2677
+ subscribeTrace(listener: () => void): () => void;
2678
+ private subscribePhone;
2679
+ private onIncoming;
2680
+ private applyHeld;
2681
+ private reportIncoming;
2682
+ private endDuplicateSession;
2683
+ /**
2684
+ * An OS answer we could not act on yet. The OS holds its answer request open
2685
+ * meanwhile — CallKit keeps the incoming UI up, Telecom leaves the call in
2686
+ * CONNECTING — so if no call ever arrives to consume it, say so rather than
2687
+ * leaving the phone ringing until the adapter's own far longer deadline.
2688
+ *
2689
+ * Handle-stored and cleared by `forgetSession`, like the wake deadlines. It
2690
+ * has to be: session ids are recycled, so a timer left armed past its own
2691
+ * session fires against a LATER tap on the same id and reports a live call
2692
+ * failed. `armedAt` is the second half of that — it pins the timer to the one
2693
+ * tap it was armed for even if the map was refilled meanwhile.
2694
+ */
2695
+ private armAnswerDeadline;
2696
+ private dropHeldAnswer;
2697
+ private answerCall;
2698
+ private reportConnectedReliably;
2699
+ private watchCall;
2700
+ private reportHold;
2701
+ /**
2702
+ * Hang up, and make sure the call ends. The SDK sends one frame; this retries
2703
+ * one the server never answers and, once the attempts run out, ends the call
2704
+ * here so the OS and the app don't keep a call nothing can reach.
2705
+ *
2706
+ * An error reply means the server has no such call, and the SDK ends it. A
2707
+ * closed socket ends it too, through the lost session.
2708
+ */
2709
+ private endCall;
2710
+ /**
2711
+ * Hold or resume, confirmed. Resolves once the call's held/resumed arrives.
2712
+ *
2713
+ * The far end refusing on state (`invalid_message`) or a closed socket rejects
2714
+ * at once and leaves the call alone. A failure on the server's side (far end
2715
+ * unreachable) or no reply is retried; once the attempts run out the in-dialog
2716
+ * route is dead and every other action would fail the same way, so the call is
2717
+ * ended and the promise rejects.
2718
+ */
2719
+ private changeHold;
2720
+ private confirmHold;
2721
+ private attempt;
2722
+ /** The pause after a failed attempt; none after the last, which has nothing to wait for. */
2723
+ private backoffAfter;
2724
+ private abandonCall;
2725
+ private teardownCall;
2726
+ private reportEndedReliably;
2727
+ private subscribeOs;
2728
+ private onIncomingReported;
2729
+ private armWakeDeadline;
2730
+ private forgetSession;
2731
+ private wakeInFlight;
2732
+ /** How many OS-reported calls are still live — the OS's session count. */
2733
+ private liveOsCallCount;
2734
+ private linkOsSession;
2735
+ /**
2736
+ * Check, shortly after a change, that the OS shows what the call is.
2737
+ *
2738
+ * Every OS report above is retried when it fails, but a report the OS takes
2739
+ * and then doesn't act on goes unseen: iOS accepts a connected report for an
2740
+ * incoming call and keeps it ringing. So compare what the OS says it shows
2741
+ * with the call, and correct the OS; the call's state is the server's, and
2742
+ * the OS follows it.
2743
+ *
2744
+ * Only the session the OS calls active is read, since that is all the port
2745
+ * offers. It is the one on screen, and with the default of one OS call it is
2746
+ * every session there is.
2747
+ */
2748
+ private scheduleReconcile;
2749
+ private reconcile;
2750
+ private reconcileConnected;
2751
+ private reconcileHeld;
2752
+ private callForSession;
2753
+ private subscribeLifecycle;
2754
+ private dropRegistrationIfIdle;
2755
+ private trace;
2756
+ }
2757
+
2758
+ /** SoftphoneProvider (React Native), same API as the web provider. */
2202
2759
 
2203
2760
  type AppearanceOptions = Parameters<typeof resolveSoftphonePalette>[0];
2204
2761
  type ConnectionState = SoftphoneConnectionState;
2205
2762
  interface SoftphoneProviderProps {
2763
+ /**
2764
+ * Adopt an existing phone instead of constructing one from `token`, for a
2765
+ * native call surface where a call can exist before any phone does. Neither
2766
+ * connected nor disconnected by the provider.
2767
+ */
2768
+ existingPhone?: DialStackPhone | null;
2206
2769
  /** WebRTC user session token. */
2207
2770
  token: string;
2208
2771
  /**
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.
2772
+ * Persistence for the selected E911 address id. REQUIRED on RN: the SDK takes
2773
+ * no persistence dependency of its own. A synchronous in-memory adapter is
2774
+ * valid if you don't need persistence.
2216
2775
  */
2217
2776
  storage: PlatformStorage;
2218
2777
  /** API base URL (defaults to the SDK's production endpoint). */
2219
2778
  apiBaseUrl?: string;
2220
2779
  /**
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`.
2780
+ * Called shortly before the session token expires. Return a fresh token; the
2781
+ * SDK delivers it in-band with no reconnect. On reject the SDK keeps the
2782
+ * still-valid connection and surfaces an `error`.
2225
2783
  */
2226
2784
  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. */
2785
+ /** E911 address id to present on connect; when supplied the host manages E911
2786
+ * and the built-in prompt is disabled. */
2229
2787
  emergencyAddressId?: string;
2230
2788
  /**
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.
2789
+ * Device-location source for the E911 form's "Use my current location" action;
2790
+ * the host owns the permission prompt, geolocation, and reverse-geocoding.
2235
2791
  */
2236
2792
  locationProvider?: () => Promise<EmergencyAddressInput>;
2237
2793
  /** Connect automatically once mounted (default: true). */
2238
2794
  autoConnect?: boolean;
2239
- /** Theming — the shared appearance surface (same as the web softphone). */
2795
+ /** Theming — the shared appearance surface. */
2240
2796
  appearance?: AppearanceOptions;
2241
- /** Locale for UI strings (defaults to English), same surface as the web softphone. */
2797
+ /** Locale for UI strings (defaults to English). */
2242
2798
  locale?: Locale;
2243
2799
  /** Default country for number formatting. */
2244
2800
  defaultCountry?: CountryCode;
@@ -2246,6 +2802,9 @@ interface SoftphoneProviderProps {
2246
2802
  state: ConnectionState;
2247
2803
  }) => void;
2248
2804
  onIncomingCall?: (event: {
2805
+ /** Handle for `callActionsFor()` / `answerCall()`, and what a host bridging
2806
+ * to a native call UI binds its OS session to. */
2807
+ callId: string;
2249
2808
  from: string;
2250
2809
  fromName: string | null;
2251
2810
  }) => void;
@@ -2260,17 +2819,39 @@ interface SoftphoneProviderProps {
2260
2819
  code: string;
2261
2820
  message: string;
2262
2821
  }) => void;
2822
+ /**
2823
+ * The call bridge, when this app reports calls to the OS. Outbound placement
2824
+ * then goes through it, so the OS learns about the call BEFORE it is dialled —
2825
+ * on Android that report is what starts the foreground service, without which
2826
+ * backgrounding mid-dial lets the OS reap the process and drop the call.
2827
+ * Answering in-app goes through it too, so the OS answers before the call
2828
+ * starts media (on iOS that is what gives the call its audio).
2829
+ */
2830
+ bridge?: Pick<NativeCallBridge, 'call' | 'answer'>;
2831
+ /**
2832
+ * Who owns call audio: ringing, outbound ringback, the audio session and the
2833
+ * proximity sensor. `'sdk'` (the default): the provider does all of it.
2834
+ * `'host'`: the provider does none of it, for an app that reports calls to
2835
+ * CallKit/Telecom itself, which ring the phone and own the session. Doing any
2836
+ * of it on top of the OS breaks capture. Always `'host'` with a `bridge`.
2837
+ */
2838
+ callAudio?: CallAudioOwner;
2839
+ /**
2840
+ * The output route, for a host that owns call audio: without it the in-call
2841
+ * speaker button is disabled, since only the OS integration can move the
2842
+ * route. Ignored unless call audio is `'host'`.
2843
+ */
2844
+ audioOutput?: AudioOutputController;
2263
2845
  children: React.ReactNode;
2264
2846
  }
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
- */
2847
+ /** The native softphone context: the shared base plus the native-only
2848
+ * `locationProvider`. */
2269
2849
  interface SoftphoneContextValue extends SoftphoneContextBase {
2270
2850
  /** Host-supplied device-location source for the E911 form, or undefined. */
2271
2851
  locationProvider: (() => Promise<EmergencyAddressInput>) | undefined;
2852
+ audio: UseAudioOutput;
2272
2853
  }
2273
- declare function SoftphoneProvider({ token, storage, apiBaseUrl, onTokenExpiring, emergencyAddressId, locationProvider, autoConnect, appearance, locale, defaultCountry, onConnectionStateChange, onIncomingCall, onCallStarted, onCallEnded, onError, children, }: SoftphoneProviderProps): React.JSX.Element;
2854
+ 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
2855
  /** Access the full softphone context. Throws when used outside the provider. */
2275
2856
  declare function useSoftphone(): SoftphoneContextValue;
2276
2857
  /** The active foreground call + its actions (build-your-own convenience). */
@@ -2278,6 +2859,8 @@ declare function useActiveCall(): {
2278
2859
  activeCall: Call | null;
2279
2860
  actions: UseCallActions;
2280
2861
  };
2862
+ /** The call-audio output route and the speaker toggle. */
2863
+ declare function useAudioOutput(): UseAudioOutput;
2281
2864
  /** The currently-ringing inbound call, or null. */
2282
2865
  declare function useIncomingCall(): Call | null;
2283
2866
 
@@ -2317,10 +2900,10 @@ declare function Softphone({ autoFocusDestination }: SoftphoneProps): React.JSX.
2317
2900
  */
2318
2901
 
2319
2902
  interface DialPadProps {
2320
- /** Autofocus the destination field on mount. */
2321
2903
  autoFocusDestination?: boolean;
2904
+ compact?: boolean;
2322
2905
  }
2323
- declare function DialPad({ autoFocusDestination }: DialPadProps): React.JSX.Element;
2906
+ declare function DialPad({ autoFocusDestination, compact, }: DialPadProps): React.JSX.Element;
2324
2907
 
2325
2908
  /**
2326
2909
  * IncomingCall / IncomingStack (RN) — the ringing inbound UI.
@@ -2363,5 +2946,38 @@ declare function OngoingCall(): React.JSX.Element | null;
2363
2946
 
2364
2947
  declare function EmergencyBanner(): React.JSX.Element | null;
2365
2948
 
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 };
2949
+ type CreatePhoneOptions = Omit<PhoneOptions, 'createSignalingSocket' | 'deferInboundCapture'>;
2950
+ /**
2951
+ * The process's one phone.
2952
+ *
2953
+ * Module scope is the correct scope, not a shortcut: a push wake and the UI run
2954
+ * in the same JS runtime but enter from different places (a headless task, the
2955
+ * app window), so this is a plain shared reference. Two phones would register
2956
+ * the same SIP AOR twice and the call would go to whichever registered last
2957
+ * while the UI watched the other — a silent failure, so a second construction
2958
+ * fails loudly instead. Construct eagerly at app entry: `new DialStackPhone()`
2959
+ * does no I/O (only `connect()` does).
2960
+ */
2961
+ declare function createPhone(options: CreatePhoneOptions): DialStackPhone;
2962
+ declare function getPhone(): DialStackPhone | null;
2963
+ /** For consumers that must not be the one to create it (the UI). */
2964
+ declare function requirePhone(): DialStackPhone;
2965
+
2966
+ declare const appLifecycle: AppLifecycle;
2967
+ declare const DEFAULT_WAKE_TASK = "DialStackWake";
2968
+ /**
2969
+ * Android's wake: the adapter's native push service reports the call to Telecom
2970
+ * with no JS, and something must then start a runtime so the SDK can
2971
+ * re-REGISTER and receive the parked INVITE. That is this headless task; the
2972
+ * app's HeadlessJsTaskService names it (`taskName`).
2973
+ *
2974
+ * iOS has no equivalent — PushKit launches the process and React Native starts
2975
+ * normally, so the app entry's `bridge.start()` is the whole wake path.
2976
+ *
2977
+ * A single file with a runtime branch rather than `.android.ts`/`.ios.ts`: the
2978
+ * package ships as one bundle, so Metro's platform extensions cannot apply.
2979
+ */
2980
+ declare function registerWakeTask(bridge: NativeCallBridge, taskName?: string): void;
2981
+
2982
+ export { Call, DEFAULT_WAKE_TASK, DialPad, EmergencyBanner, IncomingCall, NativeCallBridge, OngoingCall, RuntimeHold, Softphone, SoftphoneProvider, appLifecycle, createPhone, getPhone, nativeSignalingSocket, registerWakeTask, requirePhone, toOsEndReason, useActiveCall, useAudioOutput, useIncomingCall, useSoftphone };
2983
+ 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 };