@poolse/react-native-calls 0.1.2 → 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/android/build.gradle +7 -0
- package/android/consumer-rules.pro +11 -0
- package/android/src/main/AndroidManifest.xml +2 -0
- package/android/src/main/java/dev/poolse/callcore/CallCoreRuntime.kt +245 -110
- package/android/src/main/java/dev/poolse/callcore/CallMachine.kt +113 -18
- package/android/src/main/java/dev/poolse/callcore/Ports.kt +17 -2
- package/android/src/main/java/dev/poolse/callcore/android/AndroidRingSurface.kt +74 -12
- package/android/src/main/java/dev/poolse/callcore/android/CallActionReceiver.kt +4 -3
- package/android/src/main/java/dev/poolse/callcore/android/CallCore.kt +253 -26
- package/android/src/main/java/dev/poolse/callcore/android/CallForegroundService.kt +228 -47
- package/android/src/main/java/dev/poolse/callcore/android/CallHttpClient.kt +97 -46
- package/android/src/main/java/dev/poolse/callcore/android/CallMessagingService.kt +59 -13
- package/android/src/main/java/dev/poolse/callcore/android/IncomingCallActivity.kt +106 -8
- package/android/src/main/java/expo/modules/poolsecalls/PoolseCallsModule.kt +75 -17
- package/android/src/main/livekit/java/dev/poolse/callcore/livekit/LiveKitMediaPort.kt +190 -16
- package/android/src/test/java/dev/poolse/callcore/android/CallHttpClientTest.kt +61 -0
- package/dist/index.cjs +10 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +29 -3
- package/dist/index.d.ts +29 -3
- package/dist/index.js +6 -1
- package/dist/index.js.map +1 -1
- package/package.json +2 -1
- package/src/index.ts +39 -2
package/dist/index.cjs
CHANGED
|
@@ -28,18 +28,28 @@ var hangUp = () => native?.hangUp();
|
|
|
28
28
|
var setMuted = (muted) => native?.setMuted(muted);
|
|
29
29
|
var setCamera = (on) => native?.setCamera(on);
|
|
30
30
|
var switchCamera = () => native?.switchCamera();
|
|
31
|
+
var setSpeaker = (on) => native?.setSpeaker(on);
|
|
32
|
+
var onRingEnded = (callId, reason) => native?.onRingEnded(callId, reason);
|
|
33
|
+
var onCallFinished = (callId, reason) => native?.onCallFinished(callId, reason ?? null);
|
|
34
|
+
var canUseFullScreenIntent = () => native?.canUseFullScreenIntent() ?? true;
|
|
35
|
+
var openFullScreenIntentSettings = () => native?.openFullScreenIntentSettings();
|
|
31
36
|
|
|
32
37
|
exports.answer = answer;
|
|
38
|
+
exports.canUseFullScreenIntent = canUseFullScreenIntent;
|
|
33
39
|
exports.configure = configure;
|
|
34
40
|
exports.decline = decline;
|
|
35
41
|
exports.getCallState = getCallState;
|
|
36
42
|
exports.hangUp = hangUp;
|
|
37
43
|
exports.join = join;
|
|
38
44
|
exports.nativeCallsAvailable = nativeCallsAvailable;
|
|
45
|
+
exports.onCallFinished = onCallFinished;
|
|
39
46
|
exports.onCallState = onCallState;
|
|
47
|
+
exports.onRingEnded = onRingEnded;
|
|
48
|
+
exports.openFullScreenIntentSettings = openFullScreenIntentSettings;
|
|
40
49
|
exports.place = place;
|
|
41
50
|
exports.setCamera = setCamera;
|
|
42
51
|
exports.setMuted = setMuted;
|
|
52
|
+
exports.setSpeaker = setSpeaker;
|
|
43
53
|
exports.switchCamera = switchCamera;
|
|
44
54
|
//# sourceMappingURL=index.cjs.map
|
|
45
55
|
//# sourceMappingURL=index.cjs.map
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts"],"names":["requireOptionalNativeModule"],"mappings":";;;;;
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"names":["requireOptionalNativeModule"],"mappings":";;;;;AA+EA,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;AAEzC,IAAM,UAAA,GAAa,CAAC,EAAA,KAAsB,MAAA,EAAQ,WAAW,EAAE;AAM/D,IAAM,cAAc,CAAC,MAAA,EAAgB,WAC1C,MAAA,EAAQ,WAAA,CAAY,QAAQ,MAAM;AAQ7B,IAAM,cAAA,GAAiB,CAAC,MAAA,EAAgB,MAAA,KAC7C,QAAQ,cAAA,CAAe,MAAA,EAAQ,UAAU,IAAI;AAMxC,IAAM,sBAAA,GAAyB,MAAe,MAAA,EAAQ,sBAAA,EAAuB,IAAK;AAGlF,IAAM,4BAAA,GAA+B,MAAY,MAAA,EAAQ,4BAAA","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/**\n * A call's complete state. `null` means idle — no call. After a call\n * ends the snapshot stays `phase: 'ended'` (with `reason`) for ~2.5 s,\n * then becomes `null`.\n */\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 /** REMOTE participants only; `userId` is the LiveKit identity. */\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 setSpeaker(on: boolean): void;\n onRingEnded(callId: string, reason: string): void;\n onCallFinished(callId: string, reason: string | null): void;\n canUseFullScreenIntent(): boolean;\n openFullScreenIntentSettings(): 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 and before configure. */\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/** Route call audio to the loudspeaker (`true`) or back to earpiece / headset (`false`). */\nexport const setSpeaker = (on: boolean): void => native?.setSpeaker(on);\n\n/**\n * Socket forwarding for an open app: a `call:ring_ended` seen on the\n * socket. Idempotent with the push path — the core dedupes.\n */\nexport const onRingEnded = (callId: string, reason: string): void =>\n native?.onRingEnded(callId, reason);\n\n/**\n * Socket forwarding: `call:finished`. Pass the payload's `reason`\n * (`finished` | `declined_all` | `no_answer` | `cancelled`) so an\n * unanswered caller sees \"Declined\"/\"No answer\", not \"Call ended\".\n * Idempotent with the `finished` push.\n */\nexport const onCallFinished = (callId: string, reason?: string): void =>\n native?.onCallFinished(callId, reason ?? null);\n\n/**\n * Android 14+: whether the app may show the full-screen ring over the\n * lock screen. `true` on older Android and where the module is absent.\n */\nexport const canUseFullScreenIntent = (): boolean => native?.canUseFullScreenIntent() ?? true;\n\n/** Open the system screen where the user grants full-screen intents (falls back to notification settings). */\nexport const openFullScreenIntentSettings = (): void => native?.openFullScreenIntentSettings();\n"]}
|
package/dist/index.d.cts
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
/**
|
|
1
|
+
/**
|
|
2
|
+
* A call's complete state. `null` means idle — no call. After a call
|
|
3
|
+
* ends the snapshot stays `phase: 'ended'` (with `reason`) for ~2.5 s,
|
|
4
|
+
* then becomes `null`.
|
|
5
|
+
*/
|
|
2
6
|
interface Snapshot {
|
|
3
7
|
cv: 1;
|
|
4
8
|
callId: string;
|
|
@@ -8,6 +12,7 @@ interface Snapshot {
|
|
|
8
12
|
media: 'audio' | 'video';
|
|
9
13
|
phase: 'ringing_in' | 'ringing_out' | 'connecting' | 'active' | 'reconnecting' | 'ended';
|
|
10
14
|
reason?: 'left' | 'finished' | 'declined_all' | 'busy_all' | 'no_answer' | 'cancelled' | 'connection_lost' | 'failed';
|
|
15
|
+
/** REMOTE participants only; `userId` is the LiveKit identity. */
|
|
11
16
|
participants: Array<{
|
|
12
17
|
userId: string;
|
|
13
18
|
externalId: string;
|
|
@@ -34,7 +39,7 @@ interface PlaceOptions {
|
|
|
34
39
|
declare const nativeCallsAvailable: boolean;
|
|
35
40
|
/** Configure once, before any call can arrive. No secrets — just the API origin. */
|
|
36
41
|
declare function configure(apiBaseUrl: string, options?: ConfigureOptions): Promise<void>;
|
|
37
|
-
/** The current snapshot — safe to read at any moment, including cold answer. */
|
|
42
|
+
/** The current snapshot — safe to read at any moment, including cold answer and before configure. */
|
|
38
43
|
declare function getCallState(): Snapshot | null;
|
|
39
44
|
type Unsubscribe = () => void;
|
|
40
45
|
/** Subscribe to call state. Fires a full snapshot on every change. */
|
|
@@ -47,5 +52,26 @@ declare const hangUp: () => void;
|
|
|
47
52
|
declare const setMuted: (muted: boolean) => void;
|
|
48
53
|
declare const setCamera: (on: boolean) => void;
|
|
49
54
|
declare const switchCamera: () => void;
|
|
55
|
+
/** Route call audio to the loudspeaker (`true`) or back to earpiece / headset (`false`). */
|
|
56
|
+
declare const setSpeaker: (on: boolean) => void;
|
|
57
|
+
/**
|
|
58
|
+
* Socket forwarding for an open app: a `call:ring_ended` seen on the
|
|
59
|
+
* socket. Idempotent with the push path — the core dedupes.
|
|
60
|
+
*/
|
|
61
|
+
declare const onRingEnded: (callId: string, reason: string) => void;
|
|
62
|
+
/**
|
|
63
|
+
* Socket forwarding: `call:finished`. Pass the payload's `reason`
|
|
64
|
+
* (`finished` | `declined_all` | `no_answer` | `cancelled`) so an
|
|
65
|
+
* unanswered caller sees "Declined"/"No answer", not "Call ended".
|
|
66
|
+
* Idempotent with the `finished` push.
|
|
67
|
+
*/
|
|
68
|
+
declare const onCallFinished: (callId: string, reason?: string) => void;
|
|
69
|
+
/**
|
|
70
|
+
* Android 14+: whether the app may show the full-screen ring over the
|
|
71
|
+
* lock screen. `true` on older Android and where the module is absent.
|
|
72
|
+
*/
|
|
73
|
+
declare const canUseFullScreenIntent: () => boolean;
|
|
74
|
+
/** Open the system screen where the user grants full-screen intents (falls back to notification settings). */
|
|
75
|
+
declare const openFullScreenIntentSettings: () => void;
|
|
50
76
|
|
|
51
|
-
export { type ConfigureOptions, type PlaceOptions, type Snapshot, type Unsubscribe, answer, configure, decline, getCallState, hangUp, join, nativeCallsAvailable, onCallState, place, setCamera, setMuted, switchCamera };
|
|
77
|
+
export { type ConfigureOptions, type PlaceOptions, type Snapshot, type Unsubscribe, answer, canUseFullScreenIntent, configure, decline, getCallState, hangUp, join, nativeCallsAvailable, onCallFinished, onCallState, onRingEnded, openFullScreenIntentSettings, place, setCamera, setMuted, setSpeaker, switchCamera };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
/**
|
|
1
|
+
/**
|
|
2
|
+
* A call's complete state. `null` means idle — no call. After a call
|
|
3
|
+
* ends the snapshot stays `phase: 'ended'` (with `reason`) for ~2.5 s,
|
|
4
|
+
* then becomes `null`.
|
|
5
|
+
*/
|
|
2
6
|
interface Snapshot {
|
|
3
7
|
cv: 1;
|
|
4
8
|
callId: string;
|
|
@@ -8,6 +12,7 @@ interface Snapshot {
|
|
|
8
12
|
media: 'audio' | 'video';
|
|
9
13
|
phase: 'ringing_in' | 'ringing_out' | 'connecting' | 'active' | 'reconnecting' | 'ended';
|
|
10
14
|
reason?: 'left' | 'finished' | 'declined_all' | 'busy_all' | 'no_answer' | 'cancelled' | 'connection_lost' | 'failed';
|
|
15
|
+
/** REMOTE participants only; `userId` is the LiveKit identity. */
|
|
11
16
|
participants: Array<{
|
|
12
17
|
userId: string;
|
|
13
18
|
externalId: string;
|
|
@@ -34,7 +39,7 @@ interface PlaceOptions {
|
|
|
34
39
|
declare const nativeCallsAvailable: boolean;
|
|
35
40
|
/** Configure once, before any call can arrive. No secrets — just the API origin. */
|
|
36
41
|
declare function configure(apiBaseUrl: string, options?: ConfigureOptions): Promise<void>;
|
|
37
|
-
/** The current snapshot — safe to read at any moment, including cold answer. */
|
|
42
|
+
/** The current snapshot — safe to read at any moment, including cold answer and before configure. */
|
|
38
43
|
declare function getCallState(): Snapshot | null;
|
|
39
44
|
type Unsubscribe = () => void;
|
|
40
45
|
/** Subscribe to call state. Fires a full snapshot on every change. */
|
|
@@ -47,5 +52,26 @@ declare const hangUp: () => void;
|
|
|
47
52
|
declare const setMuted: (muted: boolean) => void;
|
|
48
53
|
declare const setCamera: (on: boolean) => void;
|
|
49
54
|
declare const switchCamera: () => void;
|
|
55
|
+
/** Route call audio to the loudspeaker (`true`) or back to earpiece / headset (`false`). */
|
|
56
|
+
declare const setSpeaker: (on: boolean) => void;
|
|
57
|
+
/**
|
|
58
|
+
* Socket forwarding for an open app: a `call:ring_ended` seen on the
|
|
59
|
+
* socket. Idempotent with the push path — the core dedupes.
|
|
60
|
+
*/
|
|
61
|
+
declare const onRingEnded: (callId: string, reason: string) => void;
|
|
62
|
+
/**
|
|
63
|
+
* Socket forwarding: `call:finished`. Pass the payload's `reason`
|
|
64
|
+
* (`finished` | `declined_all` | `no_answer` | `cancelled`) so an
|
|
65
|
+
* unanswered caller sees "Declined"/"No answer", not "Call ended".
|
|
66
|
+
* Idempotent with the `finished` push.
|
|
67
|
+
*/
|
|
68
|
+
declare const onCallFinished: (callId: string, reason?: string) => void;
|
|
69
|
+
/**
|
|
70
|
+
* Android 14+: whether the app may show the full-screen ring over the
|
|
71
|
+
* lock screen. `true` on older Android and where the module is absent.
|
|
72
|
+
*/
|
|
73
|
+
declare const canUseFullScreenIntent: () => boolean;
|
|
74
|
+
/** Open the system screen where the user grants full-screen intents (falls back to notification settings). */
|
|
75
|
+
declare const openFullScreenIntentSettings: () => void;
|
|
50
76
|
|
|
51
|
-
export { type ConfigureOptions, type PlaceOptions, type Snapshot, type Unsubscribe, answer, configure, decline, getCallState, hangUp, join, nativeCallsAvailable, onCallState, place, setCamera, setMuted, switchCamera };
|
|
77
|
+
export { type ConfigureOptions, type PlaceOptions, type Snapshot, type Unsubscribe, answer, canUseFullScreenIntent, configure, decline, getCallState, hangUp, join, nativeCallsAvailable, onCallFinished, onCallState, onRingEnded, openFullScreenIntentSettings, place, setCamera, setMuted, setSpeaker, switchCamera };
|
package/dist/index.js
CHANGED
|
@@ -26,7 +26,12 @@ var hangUp = () => native?.hangUp();
|
|
|
26
26
|
var setMuted = (muted) => native?.setMuted(muted);
|
|
27
27
|
var setCamera = (on) => native?.setCamera(on);
|
|
28
28
|
var switchCamera = () => native?.switchCamera();
|
|
29
|
+
var setSpeaker = (on) => native?.setSpeaker(on);
|
|
30
|
+
var onRingEnded = (callId, reason) => native?.onRingEnded(callId, reason);
|
|
31
|
+
var onCallFinished = (callId, reason) => native?.onCallFinished(callId, reason ?? null);
|
|
32
|
+
var canUseFullScreenIntent = () => native?.canUseFullScreenIntent() ?? true;
|
|
33
|
+
var openFullScreenIntentSettings = () => native?.openFullScreenIntentSettings();
|
|
29
34
|
|
|
30
|
-
export { answer, configure, decline, getCallState, hangUp, join, nativeCallsAvailable, onCallState, place, setCamera, setMuted, switchCamera };
|
|
35
|
+
export { answer, canUseFullScreenIntent, configure, decline, getCallState, hangUp, join, nativeCallsAvailable, onCallFinished, onCallState, onRingEnded, openFullScreenIntentSettings, place, setCamera, setMuted, setSpeaker, switchCamera };
|
|
31
36
|
//# sourceMappingURL=index.js.map
|
|
32
37
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts"],"names":[],"mappings":";;;
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"names":[],"mappings":";;;AA+EA,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;AAEzC,IAAM,UAAA,GAAa,CAAC,EAAA,KAAsB,MAAA,EAAQ,WAAW,EAAE;AAM/D,IAAM,cAAc,CAAC,MAAA,EAAgB,WAC1C,MAAA,EAAQ,WAAA,CAAY,QAAQ,MAAM;AAQ7B,IAAM,cAAA,GAAiB,CAAC,MAAA,EAAgB,MAAA,KAC7C,QAAQ,cAAA,CAAe,MAAA,EAAQ,UAAU,IAAI;AAMxC,IAAM,sBAAA,GAAyB,MAAe,MAAA,EAAQ,sBAAA,EAAuB,IAAK;AAGlF,IAAM,4BAAA,GAA+B,MAAY,MAAA,EAAQ,4BAAA","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/**\n * A call's complete state. `null` means idle — no call. After a call\n * ends the snapshot stays `phase: 'ended'` (with `reason`) for ~2.5 s,\n * then becomes `null`.\n */\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 /** REMOTE participants only; `userId` is the LiveKit identity. */\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 setSpeaker(on: boolean): void;\n onRingEnded(callId: string, reason: string): void;\n onCallFinished(callId: string, reason: string | null): void;\n canUseFullScreenIntent(): boolean;\n openFullScreenIntentSettings(): 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 and before configure. */\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/** Route call audio to the loudspeaker (`true`) or back to earpiece / headset (`false`). */\nexport const setSpeaker = (on: boolean): void => native?.setSpeaker(on);\n\n/**\n * Socket forwarding for an open app: a `call:ring_ended` seen on the\n * socket. Idempotent with the push path — the core dedupes.\n */\nexport const onRingEnded = (callId: string, reason: string): void =>\n native?.onRingEnded(callId, reason);\n\n/**\n * Socket forwarding: `call:finished`. Pass the payload's `reason`\n * (`finished` | `declined_all` | `no_answer` | `cancelled`) so an\n * unanswered caller sees \"Declined\"/\"No answer\", not \"Call ended\".\n * Idempotent with the `finished` push.\n */\nexport const onCallFinished = (callId: string, reason?: string): void =>\n native?.onCallFinished(callId, reason ?? null);\n\n/**\n * Android 14+: whether the app may show the full-screen ring over the\n * lock screen. `true` on older Android and where the module is absent.\n */\nexport const canUseFullScreenIntent = (): boolean => native?.canUseFullScreenIntent() ?? true;\n\n/** Open the system screen where the user grants full-screen intents (falls back to notification settings). */\nexport const openFullScreenIntentSettings = (): void => native?.openFullScreenIntentSettings();\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@poolse/react-native-calls",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
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
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
"src",
|
|
21
21
|
"dist",
|
|
22
22
|
"android/build.gradle",
|
|
23
|
+
"android/consumer-rules.pro",
|
|
23
24
|
"android/src",
|
|
24
25
|
"expo-module.config.json",
|
|
25
26
|
"README.md"
|
package/src/index.ts
CHANGED
|
@@ -8,7 +8,11 @@
|
|
|
8
8
|
// so a host can render against this unconditionally.
|
|
9
9
|
import { requireOptionalNativeModule } from 'expo-modules-core';
|
|
10
10
|
|
|
11
|
-
/**
|
|
11
|
+
/**
|
|
12
|
+
* A call's complete state. `null` means idle — no call. After a call
|
|
13
|
+
* ends the snapshot stays `phase: 'ended'` (with `reason`) for ~2.5 s,
|
|
14
|
+
* then becomes `null`.
|
|
15
|
+
*/
|
|
12
16
|
export interface Snapshot {
|
|
13
17
|
cv: 1;
|
|
14
18
|
callId: string;
|
|
@@ -26,6 +30,7 @@ export interface Snapshot {
|
|
|
26
30
|
| 'cancelled'
|
|
27
31
|
| 'connection_lost'
|
|
28
32
|
| 'failed';
|
|
33
|
+
/** REMOTE participants only; `userId` is the LiveKit identity. */
|
|
29
34
|
participants: Array<{
|
|
30
35
|
userId: string;
|
|
31
36
|
externalId: string;
|
|
@@ -64,6 +69,11 @@ interface PoolseCallsNative {
|
|
|
64
69
|
setMuted(muted: boolean): void;
|
|
65
70
|
setCamera(on: boolean): void;
|
|
66
71
|
switchCamera(): void;
|
|
72
|
+
setSpeaker(on: boolean): void;
|
|
73
|
+
onRingEnded(callId: string, reason: string): void;
|
|
74
|
+
onCallFinished(callId: string, reason: string | null): void;
|
|
75
|
+
canUseFullScreenIntent(): boolean;
|
|
76
|
+
openFullScreenIntentSettings(): void;
|
|
67
77
|
addListener(event: 'onCallState', cb: (payload: NativeSnapshot) => void): { remove(): void };
|
|
68
78
|
}
|
|
69
79
|
|
|
@@ -77,7 +87,7 @@ export async function configure(apiBaseUrl: string, options?: ConfigureOptions):
|
|
|
77
87
|
await native?.configure(apiBaseUrl, options ?? null);
|
|
78
88
|
}
|
|
79
89
|
|
|
80
|
-
/** The current snapshot — safe to read at any moment, including cold answer. */
|
|
90
|
+
/** The current snapshot — safe to read at any moment, including cold answer and before configure. */
|
|
81
91
|
export function getCallState(): Snapshot | null {
|
|
82
92
|
return native?.getCallState().snapshot ?? null;
|
|
83
93
|
}
|
|
@@ -109,3 +119,30 @@ export const hangUp = (): void => native?.hangUp();
|
|
|
109
119
|
export const setMuted = (muted: boolean): void => native?.setMuted(muted);
|
|
110
120
|
export const setCamera = (on: boolean): void => native?.setCamera(on);
|
|
111
121
|
export const switchCamera = (): void => native?.switchCamera();
|
|
122
|
+
/** Route call audio to the loudspeaker (`true`) or back to earpiece / headset (`false`). */
|
|
123
|
+
export const setSpeaker = (on: boolean): void => native?.setSpeaker(on);
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Socket forwarding for an open app: a `call:ring_ended` seen on the
|
|
127
|
+
* socket. Idempotent with the push path — the core dedupes.
|
|
128
|
+
*/
|
|
129
|
+
export const onRingEnded = (callId: string, reason: string): void =>
|
|
130
|
+
native?.onRingEnded(callId, reason);
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Socket forwarding: `call:finished`. Pass the payload's `reason`
|
|
134
|
+
* (`finished` | `declined_all` | `no_answer` | `cancelled`) so an
|
|
135
|
+
* unanswered caller sees "Declined"/"No answer", not "Call ended".
|
|
136
|
+
* Idempotent with the `finished` push.
|
|
137
|
+
*/
|
|
138
|
+
export const onCallFinished = (callId: string, reason?: string): void =>
|
|
139
|
+
native?.onCallFinished(callId, reason ?? null);
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Android 14+: whether the app may show the full-screen ring over the
|
|
143
|
+
* lock screen. `true` on older Android and where the module is absent.
|
|
144
|
+
*/
|
|
145
|
+
export const canUseFullScreenIntent = (): boolean => native?.canUseFullScreenIntent() ?? true;
|
|
146
|
+
|
|
147
|
+
/** Open the system screen where the user grants full-screen intents (falls back to notification settings). */
|
|
148
|
+
export const openFullScreenIntentSettings = (): void => native?.openFullScreenIntentSettings();
|