@poolse/react-native-calls 0.0.0-stage → 0.1.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/README.md +33 -2
- package/android/build.gradle +51 -0
- package/android/src/main/AndroidManifest.xml +40 -0
- package/android/src/main/java/dev/poolse/callcore/CallCoreRuntime.kt +257 -0
- package/android/src/main/java/dev/poolse/callcore/CallMachine.kt +292 -0
- package/android/src/main/java/dev/poolse/callcore/Ports.kt +94 -0
- package/android/src/main/java/dev/poolse/callcore/android/AndroidRingSurface.kt +183 -0
- package/android/src/main/java/dev/poolse/callcore/android/CallActionReceiver.kt +28 -0
- package/android/src/main/java/dev/poolse/callcore/android/CallCore.kt +236 -0
- package/android/src/main/java/dev/poolse/callcore/android/CallForegroundService.kt +80 -0
- package/android/src/main/java/dev/poolse/callcore/android/CallHttpClient.kt +151 -0
- package/android/src/main/java/dev/poolse/callcore/android/CallMessagingService.kt +99 -0
- package/android/src/main/java/dev/poolse/callcore/android/HandlerTimerPort.kt +42 -0
- package/android/src/main/java/dev/poolse/callcore/android/IncomingCallActivity.kt +209 -0
- package/android/src/main/java/dev/poolse/callcore/android/RingBundle.kt +42 -0
- package/android/src/main/java/dev/poolse/callcore/android/SnapshotJson.kt +57 -0
- package/android/src/main/java/expo/modules/poolsecalls/PoolseCallsModule.kt +67 -0
- package/android/src/main/livekit/java/dev/poolse/callcore/livekit/LiveKitMediaPort.kt +108 -0
- package/dist/index.cjs +45 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +51 -0
- package/dist/index.d.ts +51 -0
- package/dist/index.js +32 -0
- package/dist/index.js.map +1 -0
- package/expo-module.config.json +6 -0
- package/package.json +39 -4
- package/src/expo-modules-core.d.ts +11 -0
- package/src/index.ts +111 -0
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
package dev.poolse.callcore.livekit
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import dev.poolse.callcore.CallSessionBundle
|
|
5
|
+
import dev.poolse.callcore.MediaPort
|
|
6
|
+
import dev.poolse.callcore.android.MediaEvents
|
|
7
|
+
import dev.poolse.callcore.android.MediaParticipant
|
|
8
|
+
import io.livekit.android.LiveKit
|
|
9
|
+
import io.livekit.android.events.RoomEvent
|
|
10
|
+
import io.livekit.android.events.collect
|
|
11
|
+
import io.livekit.android.room.Room
|
|
12
|
+
import kotlinx.coroutines.CoroutineScope
|
|
13
|
+
import kotlinx.coroutines.Dispatchers
|
|
14
|
+
import kotlinx.coroutines.Job
|
|
15
|
+
import kotlinx.coroutines.SupervisorJob
|
|
16
|
+
import kotlinx.coroutines.cancel
|
|
17
|
+
import kotlinx.coroutines.launch
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* The shipped [MediaPort], over LiveKit's native Android SDK. Wire it
|
|
21
|
+
* with [factory] in [dev.poolse.callcore.android.CallCoreConfig].
|
|
22
|
+
*
|
|
23
|
+
* This is the one adapter that depends on an external SFU SDK, so it
|
|
24
|
+
* lives in its own source set (`src/main/livekit`) kept off the core
|
|
25
|
+
* library's classpath. Lifecycle and signaling are not its concern — it
|
|
26
|
+
* joins a room with the server-minted token, publishes the mic (and
|
|
27
|
+
* camera for video), and reports room transitions back through
|
|
28
|
+
* [MediaEvents]. The runtime decides everything else.
|
|
29
|
+
*/
|
|
30
|
+
class LiveKitMediaPort(
|
|
31
|
+
private val context: Context,
|
|
32
|
+
private val events: MediaEvents,
|
|
33
|
+
) : MediaPort {
|
|
34
|
+
|
|
35
|
+
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)
|
|
36
|
+
private var room: Room? = null
|
|
37
|
+
private var eventJob: Job? = null
|
|
38
|
+
|
|
39
|
+
override fun join(bundle: CallSessionBundle) {
|
|
40
|
+
leave()
|
|
41
|
+
val wantsVideo = bundle.media == "video"
|
|
42
|
+
val newRoom = LiveKit.create(appContext = context.applicationContext)
|
|
43
|
+
room = newRoom
|
|
44
|
+
eventJob = scope.launch { observe(newRoom) }
|
|
45
|
+
scope.launch {
|
|
46
|
+
runCatching {
|
|
47
|
+
newRoom.connect(bundle.livekitUrl, bundle.livekitToken)
|
|
48
|
+
newRoom.localParticipant.setMicrophoneEnabled(true)
|
|
49
|
+
if (wantsVideo) newRoom.localParticipant.setCameraEnabled(true)
|
|
50
|
+
}.onFailure { events.onFailed() }
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
override fun leave() {
|
|
55
|
+
eventJob?.cancel()
|
|
56
|
+
eventJob = null
|
|
57
|
+
room?.disconnect()
|
|
58
|
+
room = null
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
override fun setMuted(muted: Boolean) {
|
|
62
|
+
scope.launch { room?.localParticipant?.setMicrophoneEnabled(!muted) }
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
override fun setCamera(on: Boolean) {
|
|
66
|
+
scope.launch { room?.localParticipant?.setCameraEnabled(on) }
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
override fun switchCamera() {
|
|
70
|
+
// The host's VideoTrackView performs the actual flip; exposing the
|
|
71
|
+
// control here keeps the port surface complete.
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
private suspend fun observe(room: Room) {
|
|
75
|
+
room.events.collect { event ->
|
|
76
|
+
when (event) {
|
|
77
|
+
is RoomEvent.Connected -> events.onConnected()
|
|
78
|
+
is RoomEvent.Reconnecting, is RoomEvent.Disconnected -> events.onLost()
|
|
79
|
+
is RoomEvent.Reconnected -> events.onRecovered()
|
|
80
|
+
is RoomEvent.ParticipantConnected -> { events.onRemoteJoined(); emit(room) }
|
|
81
|
+
is RoomEvent.ParticipantDisconnected,
|
|
82
|
+
is RoomEvent.ActiveSpeakersChanged,
|
|
83
|
+
is RoomEvent.TrackMuted,
|
|
84
|
+
is RoomEvent.TrackUnmuted -> emit(room)
|
|
85
|
+
else -> Unit
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
private fun emit(room: Room) {
|
|
91
|
+
events.onParticipants(
|
|
92
|
+
room.remoteParticipants.values.map {
|
|
93
|
+
MediaParticipant(
|
|
94
|
+
identity = it.identity?.value ?: "",
|
|
95
|
+
speaking = it.isSpeaking,
|
|
96
|
+
muted = it.audioTrackPublications.firstOrNull()?.first?.muted ?: true,
|
|
97
|
+
)
|
|
98
|
+
},
|
|
99
|
+
)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
fun dispose() = scope.cancel()
|
|
103
|
+
|
|
104
|
+
companion object {
|
|
105
|
+
/** Pass as `mediaFactory` in CallCoreConfig. */
|
|
106
|
+
val factory: (Context, MediaEvents) -> MediaPort = { ctx, ev -> LiveKitMediaPort(ctx, ev) }
|
|
107
|
+
}
|
|
108
|
+
}
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var expoModulesCore = require('expo-modules-core');
|
|
4
|
+
|
|
5
|
+
// src/index.ts
|
|
6
|
+
var native = expoModulesCore.requireOptionalNativeModule("PoolseCalls");
|
|
7
|
+
var nativeCallsAvailable = native != null;
|
|
8
|
+
async function configure(apiBaseUrl, options) {
|
|
9
|
+
await native?.configure(apiBaseUrl, options ?? null);
|
|
10
|
+
}
|
|
11
|
+
function getCallState() {
|
|
12
|
+
return native?.getCallState().snapshot ?? null;
|
|
13
|
+
}
|
|
14
|
+
function onCallState(cb) {
|
|
15
|
+
if (!native) return () => void 0;
|
|
16
|
+
const sub = native.addListener("onCallState", (payload) => cb(payload.snapshot));
|
|
17
|
+
return () => sub.remove();
|
|
18
|
+
}
|
|
19
|
+
function place(conversationId, options) {
|
|
20
|
+
return native?.place(conversationId, options.media, options.sessionToken) ?? Promise.resolve();
|
|
21
|
+
}
|
|
22
|
+
function join(callId, conversationId, sessionToken) {
|
|
23
|
+
return native?.join(callId, conversationId, sessionToken) ?? Promise.resolve();
|
|
24
|
+
}
|
|
25
|
+
var answer = () => native?.answer();
|
|
26
|
+
var decline = () => native?.decline();
|
|
27
|
+
var hangUp = () => native?.hangUp();
|
|
28
|
+
var setMuted = (muted) => native?.setMuted(muted);
|
|
29
|
+
var setCamera = (on) => native?.setCamera(on);
|
|
30
|
+
var switchCamera = () => native?.switchCamera();
|
|
31
|
+
|
|
32
|
+
exports.answer = answer;
|
|
33
|
+
exports.configure = configure;
|
|
34
|
+
exports.decline = decline;
|
|
35
|
+
exports.getCallState = getCallState;
|
|
36
|
+
exports.hangUp = hangUp;
|
|
37
|
+
exports.join = join;
|
|
38
|
+
exports.nativeCallsAvailable = nativeCallsAvailable;
|
|
39
|
+
exports.onCallState = onCallState;
|
|
40
|
+
exports.place = place;
|
|
41
|
+
exports.setCamera = setCamera;
|
|
42
|
+
exports.setMuted = setMuted;
|
|
43
|
+
exports.switchCamera = switchCamera;
|
|
44
|
+
//# sourceMappingURL=index.cjs.map
|
|
45
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"names":["requireOptionalNativeModule"],"mappings":";;;;;AAqEA,IAAM,MAAA,GAASA,4CAA+C,aAAa,CAAA;AAGpE,IAAM,uBAAuB,MAAA,IAAU;AAG9C,eAAsB,SAAA,CAAU,YAAoB,OAAA,EAA2C;AAC7F,EAAA,MAAM,MAAA,EAAQ,SAAA,CAAU,UAAA,EAAY,OAAA,IAAW,IAAI,CAAA;AACrD;AAGO,SAAS,YAAA,GAAgC;AAC9C,EAAA,OAAO,MAAA,EAAQ,YAAA,EAAa,CAAE,QAAA,IAAY,IAAA;AAC5C;AAKO,SAAS,YAAY,EAAA,EAAsD;AAChF,EAAA,IAAI,CAAC,MAAA,EAAQ,OAAO,MAAM,MAAA;AAC1B,EAAA,MAAM,GAAA,GAAM,OAAO,WAAA,CAAY,aAAA,EAAe,CAAC,OAAA,KAAY,EAAA,CAAG,OAAA,CAAQ,QAAQ,CAAC,CAAA;AAC/E,EAAA,OAAO,MAAM,IAAI,MAAA,EAAO;AAC1B;AAEO,SAAS,KAAA,CAAM,gBAAwB,OAAA,EAAsC;AAClF,EAAA,OAAO,MAAA,EAAQ,MAAM,cAAA,EAAgB,OAAA,CAAQ,OAAO,OAAA,CAAQ,YAAY,CAAA,IAAK,OAAA,CAAQ,OAAA,EAAQ;AAC/F;AAEO,SAAS,IAAA,CACd,MAAA,EACA,cAAA,EACA,YAAA,EACe;AACf,EAAA,OAAO,QAAQ,IAAA,CAAK,MAAA,EAAQ,gBAAgB,YAAY,CAAA,IAAK,QAAQ,OAAA,EAAQ;AAC/E;AAEO,IAAM,MAAA,GAAS,MAAY,MAAA,EAAQ,MAAA;AACnC,IAAM,OAAA,GAAU,MAAY,MAAA,EAAQ,OAAA;AACpC,IAAM,MAAA,GAAS,MAAY,MAAA,EAAQ,MAAA;AACnC,IAAM,QAAA,GAAW,CAAC,KAAA,KAAyB,MAAA,EAAQ,SAAS,KAAK;AACjE,IAAM,SAAA,GAAY,CAAC,EAAA,KAAsB,MAAA,EAAQ,UAAU,EAAE;AAC7D,IAAM,YAAA,GAAe,MAAY,MAAA,EAAQ,YAAA","file":"index.cjs","sourcesContent":["// @poolse/react-native-calls — the React Native face of the native\n// call core (CONTRACT.md §8). A thin, typed surface over the native\n// module: commands in, one snapshot event out. All call logic lives in\n// the native cores; nothing here decides anything about a call.\n//\n// On a platform or build without the native module (Expo Go, web,\n// unlinked), every command is a safe no-op and the state stays idle —\n// so a host can render against this unconditionally.\nimport { requireOptionalNativeModule } from 'expo-modules-core';\n\n/** A call's complete state. `null` means idle — no call. */\nexport interface Snapshot {\n cv: 1;\n callId: string;\n conversationId: string;\n conversationName: string | null;\n direction: 'incoming' | 'outgoing' | 'joined';\n media: 'audio' | 'video';\n phase: 'ringing_in' | 'ringing_out' | 'connecting' | 'active' | 'reconnecting' | 'ended';\n reason?:\n | 'left'\n | 'finished'\n | 'declined_all'\n | 'busy_all'\n | 'no_answer'\n | 'cancelled'\n | 'connection_lost'\n | 'failed';\n participants: Array<{\n userId: string;\n externalId: string;\n speaking: boolean;\n muted: boolean;\n cameraOn: boolean;\n }>;\n audioRoute: 'earpiece' | 'speaker' | 'bluetooth' | 'headset';\n mutedSelf: boolean;\n cameraOn: boolean;\n}\n\nexport interface ConfigureOptions {\n /** Drawable resource NAME for the call notification's small icon. */\n smallIcon?: string;\n /** Raw resource NAME of the ringback tone (Android). */\n ringback?: string;\n}\n\nexport interface PlaceOptions {\n media: 'audio' | 'video';\n /** The user's session token — authorizes placing/joining (CONTRACT.md §3.2). */\n sessionToken: string;\n}\n\ntype NativeSnapshot = { snapshot: Snapshot | null };\n\ninterface PoolseCallsNative {\n configure(apiBaseUrl: string, options: ConfigureOptions | null): Promise<void>;\n getCallState(): NativeSnapshot;\n place(conversationId: string, media: string, sessionToken: string): Promise<void>;\n join(callId: string, conversationId: string | null, sessionToken: string): Promise<void>;\n answer(): void;\n decline(): void;\n hangUp(): void;\n setMuted(muted: boolean): void;\n setCamera(on: boolean): void;\n switchCamera(): void;\n addListener(event: 'onCallState', cb: (payload: NativeSnapshot) => void): { remove(): void };\n}\n\nconst native = requireOptionalNativeModule<PoolseCallsNative>('PoolseCalls');\n\n/** True when the native call core is present in this build. */\nexport const nativeCallsAvailable = native != null;\n\n/** Configure once, before any call can arrive. No secrets — just the API origin. */\nexport async function configure(apiBaseUrl: string, options?: ConfigureOptions): Promise<void> {\n await native?.configure(apiBaseUrl, options ?? null);\n}\n\n/** The current snapshot — safe to read at any moment, including cold answer. */\nexport function getCallState(): Snapshot | null {\n return native?.getCallState().snapshot ?? null;\n}\n\nexport type Unsubscribe = () => void;\n\n/** Subscribe to call state. Fires a full snapshot on every change. */\nexport function onCallState(cb: (snapshot: Snapshot | null) => void): Unsubscribe {\n if (!native) return () => undefined;\n const sub = native.addListener('onCallState', (payload) => cb(payload.snapshot));\n return () => sub.remove();\n}\n\nexport function place(conversationId: string, options: PlaceOptions): Promise<void> {\n return native?.place(conversationId, options.media, options.sessionToken) ?? Promise.resolve();\n}\n\nexport function join(\n callId: string,\n conversationId: string | null,\n sessionToken: string,\n): Promise<void> {\n return native?.join(callId, conversationId, sessionToken) ?? Promise.resolve();\n}\n\nexport const answer = (): void => native?.answer();\nexport const decline = (): void => native?.decline();\nexport const hangUp = (): void => native?.hangUp();\nexport const setMuted = (muted: boolean): void => native?.setMuted(muted);\nexport const setCamera = (on: boolean): void => native?.setCamera(on);\nexport const switchCamera = (): void => native?.switchCamera();\n"]}
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/** A call's complete state. `null` means idle — no call. */
|
|
2
|
+
interface Snapshot {
|
|
3
|
+
cv: 1;
|
|
4
|
+
callId: string;
|
|
5
|
+
conversationId: string;
|
|
6
|
+
conversationName: string | null;
|
|
7
|
+
direction: 'incoming' | 'outgoing' | 'joined';
|
|
8
|
+
media: 'audio' | 'video';
|
|
9
|
+
phase: 'ringing_in' | 'ringing_out' | 'connecting' | 'active' | 'reconnecting' | 'ended';
|
|
10
|
+
reason?: 'left' | 'finished' | 'declined_all' | 'busy_all' | 'no_answer' | 'cancelled' | 'connection_lost' | 'failed';
|
|
11
|
+
participants: Array<{
|
|
12
|
+
userId: string;
|
|
13
|
+
externalId: string;
|
|
14
|
+
speaking: boolean;
|
|
15
|
+
muted: boolean;
|
|
16
|
+
cameraOn: boolean;
|
|
17
|
+
}>;
|
|
18
|
+
audioRoute: 'earpiece' | 'speaker' | 'bluetooth' | 'headset';
|
|
19
|
+
mutedSelf: boolean;
|
|
20
|
+
cameraOn: boolean;
|
|
21
|
+
}
|
|
22
|
+
interface ConfigureOptions {
|
|
23
|
+
/** Drawable resource NAME for the call notification's small icon. */
|
|
24
|
+
smallIcon?: string;
|
|
25
|
+
/** Raw resource NAME of the ringback tone (Android). */
|
|
26
|
+
ringback?: string;
|
|
27
|
+
}
|
|
28
|
+
interface PlaceOptions {
|
|
29
|
+
media: 'audio' | 'video';
|
|
30
|
+
/** The user's session token — authorizes placing/joining (CONTRACT.md §3.2). */
|
|
31
|
+
sessionToken: string;
|
|
32
|
+
}
|
|
33
|
+
/** True when the native call core is present in this build. */
|
|
34
|
+
declare const nativeCallsAvailable: boolean;
|
|
35
|
+
/** Configure once, before any call can arrive. No secrets — just the API origin. */
|
|
36
|
+
declare function configure(apiBaseUrl: string, options?: ConfigureOptions): Promise<void>;
|
|
37
|
+
/** The current snapshot — safe to read at any moment, including cold answer. */
|
|
38
|
+
declare function getCallState(): Snapshot | null;
|
|
39
|
+
type Unsubscribe = () => void;
|
|
40
|
+
/** Subscribe to call state. Fires a full snapshot on every change. */
|
|
41
|
+
declare function onCallState(cb: (snapshot: Snapshot | null) => void): Unsubscribe;
|
|
42
|
+
declare function place(conversationId: string, options: PlaceOptions): Promise<void>;
|
|
43
|
+
declare function join(callId: string, conversationId: string | null, sessionToken: string): Promise<void>;
|
|
44
|
+
declare const answer: () => void;
|
|
45
|
+
declare const decline: () => void;
|
|
46
|
+
declare const hangUp: () => void;
|
|
47
|
+
declare const setMuted: (muted: boolean) => void;
|
|
48
|
+
declare const setCamera: (on: boolean) => void;
|
|
49
|
+
declare const switchCamera: () => void;
|
|
50
|
+
|
|
51
|
+
export { type ConfigureOptions, type PlaceOptions, type Snapshot, type Unsubscribe, answer, configure, decline, getCallState, hangUp, join, nativeCallsAvailable, onCallState, place, setCamera, setMuted, switchCamera };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/** A call's complete state. `null` means idle — no call. */
|
|
2
|
+
interface Snapshot {
|
|
3
|
+
cv: 1;
|
|
4
|
+
callId: string;
|
|
5
|
+
conversationId: string;
|
|
6
|
+
conversationName: string | null;
|
|
7
|
+
direction: 'incoming' | 'outgoing' | 'joined';
|
|
8
|
+
media: 'audio' | 'video';
|
|
9
|
+
phase: 'ringing_in' | 'ringing_out' | 'connecting' | 'active' | 'reconnecting' | 'ended';
|
|
10
|
+
reason?: 'left' | 'finished' | 'declined_all' | 'busy_all' | 'no_answer' | 'cancelled' | 'connection_lost' | 'failed';
|
|
11
|
+
participants: Array<{
|
|
12
|
+
userId: string;
|
|
13
|
+
externalId: string;
|
|
14
|
+
speaking: boolean;
|
|
15
|
+
muted: boolean;
|
|
16
|
+
cameraOn: boolean;
|
|
17
|
+
}>;
|
|
18
|
+
audioRoute: 'earpiece' | 'speaker' | 'bluetooth' | 'headset';
|
|
19
|
+
mutedSelf: boolean;
|
|
20
|
+
cameraOn: boolean;
|
|
21
|
+
}
|
|
22
|
+
interface ConfigureOptions {
|
|
23
|
+
/** Drawable resource NAME for the call notification's small icon. */
|
|
24
|
+
smallIcon?: string;
|
|
25
|
+
/** Raw resource NAME of the ringback tone (Android). */
|
|
26
|
+
ringback?: string;
|
|
27
|
+
}
|
|
28
|
+
interface PlaceOptions {
|
|
29
|
+
media: 'audio' | 'video';
|
|
30
|
+
/** The user's session token — authorizes placing/joining (CONTRACT.md §3.2). */
|
|
31
|
+
sessionToken: string;
|
|
32
|
+
}
|
|
33
|
+
/** True when the native call core is present in this build. */
|
|
34
|
+
declare const nativeCallsAvailable: boolean;
|
|
35
|
+
/** Configure once, before any call can arrive. No secrets — just the API origin. */
|
|
36
|
+
declare function configure(apiBaseUrl: string, options?: ConfigureOptions): Promise<void>;
|
|
37
|
+
/** The current snapshot — safe to read at any moment, including cold answer. */
|
|
38
|
+
declare function getCallState(): Snapshot | null;
|
|
39
|
+
type Unsubscribe = () => void;
|
|
40
|
+
/** Subscribe to call state. Fires a full snapshot on every change. */
|
|
41
|
+
declare function onCallState(cb: (snapshot: Snapshot | null) => void): Unsubscribe;
|
|
42
|
+
declare function place(conversationId: string, options: PlaceOptions): Promise<void>;
|
|
43
|
+
declare function join(callId: string, conversationId: string | null, sessionToken: string): Promise<void>;
|
|
44
|
+
declare const answer: () => void;
|
|
45
|
+
declare const decline: () => void;
|
|
46
|
+
declare const hangUp: () => void;
|
|
47
|
+
declare const setMuted: (muted: boolean) => void;
|
|
48
|
+
declare const setCamera: (on: boolean) => void;
|
|
49
|
+
declare const switchCamera: () => void;
|
|
50
|
+
|
|
51
|
+
export { type ConfigureOptions, type PlaceOptions, type Snapshot, type Unsubscribe, answer, configure, decline, getCallState, hangUp, join, nativeCallsAvailable, onCallState, place, setCamera, setMuted, switchCamera };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { requireOptionalNativeModule } from 'expo-modules-core';
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
var native = requireOptionalNativeModule("PoolseCalls");
|
|
5
|
+
var nativeCallsAvailable = native != null;
|
|
6
|
+
async function configure(apiBaseUrl, options) {
|
|
7
|
+
await native?.configure(apiBaseUrl, options ?? null);
|
|
8
|
+
}
|
|
9
|
+
function getCallState() {
|
|
10
|
+
return native?.getCallState().snapshot ?? null;
|
|
11
|
+
}
|
|
12
|
+
function onCallState(cb) {
|
|
13
|
+
if (!native) return () => void 0;
|
|
14
|
+
const sub = native.addListener("onCallState", (payload) => cb(payload.snapshot));
|
|
15
|
+
return () => sub.remove();
|
|
16
|
+
}
|
|
17
|
+
function place(conversationId, options) {
|
|
18
|
+
return native?.place(conversationId, options.media, options.sessionToken) ?? Promise.resolve();
|
|
19
|
+
}
|
|
20
|
+
function join(callId, conversationId, sessionToken) {
|
|
21
|
+
return native?.join(callId, conversationId, sessionToken) ?? Promise.resolve();
|
|
22
|
+
}
|
|
23
|
+
var answer = () => native?.answer();
|
|
24
|
+
var decline = () => native?.decline();
|
|
25
|
+
var hangUp = () => native?.hangUp();
|
|
26
|
+
var setMuted = (muted) => native?.setMuted(muted);
|
|
27
|
+
var setCamera = (on) => native?.setCamera(on);
|
|
28
|
+
var switchCamera = () => native?.switchCamera();
|
|
29
|
+
|
|
30
|
+
export { answer, configure, decline, getCallState, hangUp, join, nativeCallsAvailable, onCallState, place, setCamera, setMuted, switchCamera };
|
|
31
|
+
//# sourceMappingURL=index.js.map
|
|
32
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"names":[],"mappings":";;;AAqEA,IAAM,MAAA,GAAS,4BAA+C,aAAa,CAAA;AAGpE,IAAM,uBAAuB,MAAA,IAAU;AAG9C,eAAsB,SAAA,CAAU,YAAoB,OAAA,EAA2C;AAC7F,EAAA,MAAM,MAAA,EAAQ,SAAA,CAAU,UAAA,EAAY,OAAA,IAAW,IAAI,CAAA;AACrD;AAGO,SAAS,YAAA,GAAgC;AAC9C,EAAA,OAAO,MAAA,EAAQ,YAAA,EAAa,CAAE,QAAA,IAAY,IAAA;AAC5C;AAKO,SAAS,YAAY,EAAA,EAAsD;AAChF,EAAA,IAAI,CAAC,MAAA,EAAQ,OAAO,MAAM,MAAA;AAC1B,EAAA,MAAM,GAAA,GAAM,OAAO,WAAA,CAAY,aAAA,EAAe,CAAC,OAAA,KAAY,EAAA,CAAG,OAAA,CAAQ,QAAQ,CAAC,CAAA;AAC/E,EAAA,OAAO,MAAM,IAAI,MAAA,EAAO;AAC1B;AAEO,SAAS,KAAA,CAAM,gBAAwB,OAAA,EAAsC;AAClF,EAAA,OAAO,MAAA,EAAQ,MAAM,cAAA,EAAgB,OAAA,CAAQ,OAAO,OAAA,CAAQ,YAAY,CAAA,IAAK,OAAA,CAAQ,OAAA,EAAQ;AAC/F;AAEO,SAAS,IAAA,CACd,MAAA,EACA,cAAA,EACA,YAAA,EACe;AACf,EAAA,OAAO,QAAQ,IAAA,CAAK,MAAA,EAAQ,gBAAgB,YAAY,CAAA,IAAK,QAAQ,OAAA,EAAQ;AAC/E;AAEO,IAAM,MAAA,GAAS,MAAY,MAAA,EAAQ,MAAA;AACnC,IAAM,OAAA,GAAU,MAAY,MAAA,EAAQ,OAAA;AACpC,IAAM,MAAA,GAAS,MAAY,MAAA,EAAQ,MAAA;AACnC,IAAM,QAAA,GAAW,CAAC,KAAA,KAAyB,MAAA,EAAQ,SAAS,KAAK;AACjE,IAAM,SAAA,GAAY,CAAC,EAAA,KAAsB,MAAA,EAAQ,UAAU,EAAE;AAC7D,IAAM,YAAA,GAAe,MAAY,MAAA,EAAQ,YAAA","file":"index.js","sourcesContent":["// @poolse/react-native-calls — the React Native face of the native\n// call core (CONTRACT.md §8). A thin, typed surface over the native\n// module: commands in, one snapshot event out. All call logic lives in\n// the native cores; nothing here decides anything about a call.\n//\n// On a platform or build without the native module (Expo Go, web,\n// unlinked), every command is a safe no-op and the state stays idle —\n// so a host can render against this unconditionally.\nimport { requireOptionalNativeModule } from 'expo-modules-core';\n\n/** A call's complete state. `null` means idle — no call. */\nexport interface Snapshot {\n cv: 1;\n callId: string;\n conversationId: string;\n conversationName: string | null;\n direction: 'incoming' | 'outgoing' | 'joined';\n media: 'audio' | 'video';\n phase: 'ringing_in' | 'ringing_out' | 'connecting' | 'active' | 'reconnecting' | 'ended';\n reason?:\n | 'left'\n | 'finished'\n | 'declined_all'\n | 'busy_all'\n | 'no_answer'\n | 'cancelled'\n | 'connection_lost'\n | 'failed';\n participants: Array<{\n userId: string;\n externalId: string;\n speaking: boolean;\n muted: boolean;\n cameraOn: boolean;\n }>;\n audioRoute: 'earpiece' | 'speaker' | 'bluetooth' | 'headset';\n mutedSelf: boolean;\n cameraOn: boolean;\n}\n\nexport interface ConfigureOptions {\n /** Drawable resource NAME for the call notification's small icon. */\n smallIcon?: string;\n /** Raw resource NAME of the ringback tone (Android). */\n ringback?: string;\n}\n\nexport interface PlaceOptions {\n media: 'audio' | 'video';\n /** The user's session token — authorizes placing/joining (CONTRACT.md §3.2). */\n sessionToken: string;\n}\n\ntype NativeSnapshot = { snapshot: Snapshot | null };\n\ninterface PoolseCallsNative {\n configure(apiBaseUrl: string, options: ConfigureOptions | null): Promise<void>;\n getCallState(): NativeSnapshot;\n place(conversationId: string, media: string, sessionToken: string): Promise<void>;\n join(callId: string, conversationId: string | null, sessionToken: string): Promise<void>;\n answer(): void;\n decline(): void;\n hangUp(): void;\n setMuted(muted: boolean): void;\n setCamera(on: boolean): void;\n switchCamera(): void;\n addListener(event: 'onCallState', cb: (payload: NativeSnapshot) => void): { remove(): void };\n}\n\nconst native = requireOptionalNativeModule<PoolseCallsNative>('PoolseCalls');\n\n/** True when the native call core is present in this build. */\nexport const nativeCallsAvailable = native != null;\n\n/** Configure once, before any call can arrive. No secrets — just the API origin. */\nexport async function configure(apiBaseUrl: string, options?: ConfigureOptions): Promise<void> {\n await native?.configure(apiBaseUrl, options ?? null);\n}\n\n/** The current snapshot — safe to read at any moment, including cold answer. */\nexport function getCallState(): Snapshot | null {\n return native?.getCallState().snapshot ?? null;\n}\n\nexport type Unsubscribe = () => void;\n\n/** Subscribe to call state. Fires a full snapshot on every change. */\nexport function onCallState(cb: (snapshot: Snapshot | null) => void): Unsubscribe {\n if (!native) return () => undefined;\n const sub = native.addListener('onCallState', (payload) => cb(payload.snapshot));\n return () => sub.remove();\n}\n\nexport function place(conversationId: string, options: PlaceOptions): Promise<void> {\n return native?.place(conversationId, options.media, options.sessionToken) ?? Promise.resolve();\n}\n\nexport function join(\n callId: string,\n conversationId: string | null,\n sessionToken: string,\n): Promise<void> {\n return native?.join(callId, conversationId, sessionToken) ?? Promise.resolve();\n}\n\nexport const answer = (): void => native?.answer();\nexport const decline = (): void => native?.decline();\nexport const hangUp = (): void => native?.hangUp();\nexport const setMuted = (muted: boolean): void => native?.setMuted(muted);\nexport const setCamera = (on: boolean): void => native?.setCamera(on);\nexport const switchCamera = (): void => native?.switchCamera();\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,41 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@poolse/react-native-calls",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "Native call core for poolse — Kotlin/Swift calling engine with a thin React Native bridge. Ring, answer and carry audio with the app killed and the phone locked.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "./dist/index.cjs",
|
|
8
|
+
"module": "./dist/index.js",
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"react-native": "./src/index.ts",
|
|
11
|
+
"exports": {
|
|
12
|
+
".": {
|
|
13
|
+
"types": "./dist/index.d.ts",
|
|
14
|
+
"react-native": "./src/index.ts",
|
|
15
|
+
"import": "./dist/index.js",
|
|
16
|
+
"require": "./dist/index.cjs"
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
"files": [
|
|
20
|
+
"src",
|
|
21
|
+
"dist",
|
|
22
|
+
"android/build.gradle",
|
|
23
|
+
"android/src",
|
|
24
|
+
"expo-module.config.json",
|
|
25
|
+
"README.md"
|
|
26
|
+
],
|
|
27
|
+
"scripts": {
|
|
28
|
+
"build": "tsup",
|
|
29
|
+
"prepublishOnly": "tsup",
|
|
30
|
+
"typecheck": "tsc --noEmit"
|
|
31
|
+
},
|
|
32
|
+
"peerDependencies": {
|
|
33
|
+
"expo": "*",
|
|
34
|
+
"react-native": "*"
|
|
35
|
+
},
|
|
36
|
+
"devDependencies": {
|
|
37
|
+
"expo-modules-core": "*",
|
|
38
|
+
"tsup": "^8.3.5",
|
|
39
|
+
"typescript": "^5.6.0"
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// Minimal ambient declaration for the one `expo-modules-core` function
|
|
2
|
+
// this package calls at runtime. It lets the package type-check and emit
|
|
3
|
+
// its own `.d.ts` without `expo-modules-core`'s full types installed in
|
|
4
|
+
// the monorepo — the host app always supplies the real module at runtime.
|
|
5
|
+
//
|
|
6
|
+
// This shim lives only in the package source; consumers read the emitted
|
|
7
|
+
// `dist/index.d.ts`, which never references `expo-modules-core`, so it
|
|
8
|
+
// cannot collide with the app's own copy of the types.
|
|
9
|
+
declare module 'expo-modules-core' {
|
|
10
|
+
export function requireOptionalNativeModule<T>(name: string): T | null;
|
|
11
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
// @poolse/react-native-calls — the React Native face of the native
|
|
2
|
+
// call core (CONTRACT.md §8). A thin, typed surface over the native
|
|
3
|
+
// module: commands in, one snapshot event out. All call logic lives in
|
|
4
|
+
// the native cores; nothing here decides anything about a call.
|
|
5
|
+
//
|
|
6
|
+
// On a platform or build without the native module (Expo Go, web,
|
|
7
|
+
// unlinked), every command is a safe no-op and the state stays idle —
|
|
8
|
+
// so a host can render against this unconditionally.
|
|
9
|
+
import { requireOptionalNativeModule } from 'expo-modules-core';
|
|
10
|
+
|
|
11
|
+
/** A call's complete state. `null` means idle — no call. */
|
|
12
|
+
export interface Snapshot {
|
|
13
|
+
cv: 1;
|
|
14
|
+
callId: string;
|
|
15
|
+
conversationId: string;
|
|
16
|
+
conversationName: string | null;
|
|
17
|
+
direction: 'incoming' | 'outgoing' | 'joined';
|
|
18
|
+
media: 'audio' | 'video';
|
|
19
|
+
phase: 'ringing_in' | 'ringing_out' | 'connecting' | 'active' | 'reconnecting' | 'ended';
|
|
20
|
+
reason?:
|
|
21
|
+
| 'left'
|
|
22
|
+
| 'finished'
|
|
23
|
+
| 'declined_all'
|
|
24
|
+
| 'busy_all'
|
|
25
|
+
| 'no_answer'
|
|
26
|
+
| 'cancelled'
|
|
27
|
+
| 'connection_lost'
|
|
28
|
+
| 'failed';
|
|
29
|
+
participants: Array<{
|
|
30
|
+
userId: string;
|
|
31
|
+
externalId: string;
|
|
32
|
+
speaking: boolean;
|
|
33
|
+
muted: boolean;
|
|
34
|
+
cameraOn: boolean;
|
|
35
|
+
}>;
|
|
36
|
+
audioRoute: 'earpiece' | 'speaker' | 'bluetooth' | 'headset';
|
|
37
|
+
mutedSelf: boolean;
|
|
38
|
+
cameraOn: boolean;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface ConfigureOptions {
|
|
42
|
+
/** Drawable resource NAME for the call notification's small icon. */
|
|
43
|
+
smallIcon?: string;
|
|
44
|
+
/** Raw resource NAME of the ringback tone (Android). */
|
|
45
|
+
ringback?: string;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface PlaceOptions {
|
|
49
|
+
media: 'audio' | 'video';
|
|
50
|
+
/** The user's session token — authorizes placing/joining (CONTRACT.md §3.2). */
|
|
51
|
+
sessionToken: string;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
type NativeSnapshot = { snapshot: Snapshot | null };
|
|
55
|
+
|
|
56
|
+
interface PoolseCallsNative {
|
|
57
|
+
configure(apiBaseUrl: string, options: ConfigureOptions | null): Promise<void>;
|
|
58
|
+
getCallState(): NativeSnapshot;
|
|
59
|
+
place(conversationId: string, media: string, sessionToken: string): Promise<void>;
|
|
60
|
+
join(callId: string, conversationId: string | null, sessionToken: string): Promise<void>;
|
|
61
|
+
answer(): void;
|
|
62
|
+
decline(): void;
|
|
63
|
+
hangUp(): void;
|
|
64
|
+
setMuted(muted: boolean): void;
|
|
65
|
+
setCamera(on: boolean): void;
|
|
66
|
+
switchCamera(): void;
|
|
67
|
+
addListener(event: 'onCallState', cb: (payload: NativeSnapshot) => void): { remove(): void };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const native = requireOptionalNativeModule<PoolseCallsNative>('PoolseCalls');
|
|
71
|
+
|
|
72
|
+
/** True when the native call core is present in this build. */
|
|
73
|
+
export const nativeCallsAvailable = native != null;
|
|
74
|
+
|
|
75
|
+
/** Configure once, before any call can arrive. No secrets — just the API origin. */
|
|
76
|
+
export async function configure(apiBaseUrl: string, options?: ConfigureOptions): Promise<void> {
|
|
77
|
+
await native?.configure(apiBaseUrl, options ?? null);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** The current snapshot — safe to read at any moment, including cold answer. */
|
|
81
|
+
export function getCallState(): Snapshot | null {
|
|
82
|
+
return native?.getCallState().snapshot ?? null;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export type Unsubscribe = () => void;
|
|
86
|
+
|
|
87
|
+
/** Subscribe to call state. Fires a full snapshot on every change. */
|
|
88
|
+
export function onCallState(cb: (snapshot: Snapshot | null) => void): Unsubscribe {
|
|
89
|
+
if (!native) return () => undefined;
|
|
90
|
+
const sub = native.addListener('onCallState', (payload) => cb(payload.snapshot));
|
|
91
|
+
return () => sub.remove();
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function place(conversationId: string, options: PlaceOptions): Promise<void> {
|
|
95
|
+
return native?.place(conversationId, options.media, options.sessionToken) ?? Promise.resolve();
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function join(
|
|
99
|
+
callId: string,
|
|
100
|
+
conversationId: string | null,
|
|
101
|
+
sessionToken: string,
|
|
102
|
+
): Promise<void> {
|
|
103
|
+
return native?.join(callId, conversationId, sessionToken) ?? Promise.resolve();
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export const answer = (): void => native?.answer();
|
|
107
|
+
export const decline = (): void => native?.decline();
|
|
108
|
+
export const hangUp = (): void => native?.hangUp();
|
|
109
|
+
export const setMuted = (muted: boolean): void => native?.setMuted(muted);
|
|
110
|
+
export const setCamera = (on: boolean): void => native?.setCamera(on);
|
|
111
|
+
export const switchCamera = (): void => native?.switchCamera();
|