@nativetalkcommunications/react-native-call-sdk 0.1.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/LICENSE +21 -0
- package/NativetalkCallSdk.podspec +31 -0
- package/README.md +494 -0
- package/android/build.gradle +58 -0
- package/android/gradle.properties +2 -0
- package/android/src/main/AndroidManifest.xml +84 -0
- package/android/src/main/java/io/nativetalk/callsdk/BackgroundService.kt +149 -0
- package/android/src/main/java/io/nativetalk/callsdk/CallActionReceiver.kt +24 -0
- package/android/src/main/java/io/nativetalk/callsdk/CallService.kt +45 -0
- package/android/src/main/java/io/nativetalk/callsdk/Compatibility.kt +96 -0
- package/android/src/main/java/io/nativetalk/callsdk/CoreManager.kt +801 -0
- package/android/src/main/java/io/nativetalk/callsdk/NativetalkCallScreeningService.kt +105 -0
- package/android/src/main/java/io/nativetalk/callsdk/NativetalkCallSdkModule.kt +205 -0
- package/android/src/main/java/io/nativetalk/callsdk/NativetalkCallSdkPackage.kt +18 -0
- package/android/src/main/java/io/nativetalk/callsdk/TelephonyMonitor.kt +229 -0
- package/android/src/main/java/io/nativetalk/callsdk/Utils.kt +42 -0
- package/android/src/main/res/drawable/ic_nativetalk_call.xml +9 -0
- package/android/src/main/res/values/strings.xml +9 -0
- package/app.plugin.js +1 -0
- package/ios/NativetalkCallSdk-Bridging-Header.h +4 -0
- package/ios/NativetalkCallSdk.swift +738 -0
- package/ios/NativetalkCallSdkBridge.m +35 -0
- package/lib/commonjs/CallProvider.js +602 -0
- package/lib/commonjs/helpers.js +173 -0
- package/lib/commonjs/index.js +96 -0
- package/lib/commonjs/native.js +146 -0
- package/lib/commonjs/types.js +8 -0
- package/lib/commonjs/ui/Avatar.js +29 -0
- package/lib/commonjs/ui/Dialer.js +189 -0
- package/lib/commonjs/ui/IncomingCallView.js +128 -0
- package/lib/commonjs/ui/OutgoingCallView.js +117 -0
- package/lib/commonjs/ui/index.js +22 -0
- package/lib/commonjs/ui/theme.js +21 -0
- package/lib/module/CallProvider.js +573 -0
- package/lib/module/helpers.js +161 -0
- package/lib/module/index.js +57 -0
- package/lib/module/native.js +123 -0
- package/lib/module/types.js +7 -0
- package/lib/module/ui/Avatar.js +22 -0
- package/lib/module/ui/Dialer.js +162 -0
- package/lib/module/ui/IncomingCallView.js +101 -0
- package/lib/module/ui/OutgoingCallView.js +110 -0
- package/lib/module/ui/index.js +13 -0
- package/lib/module/ui/theme.js +17 -0
- package/lib/typescript/CallProvider.d.ts +46 -0
- package/lib/typescript/helpers.d.ts +52 -0
- package/lib/typescript/index.d.ts +77 -0
- package/lib/typescript/native.d.ts +53 -0
- package/lib/typescript/types.d.ts +155 -0
- package/lib/typescript/ui/Avatar.d.ts +13 -0
- package/lib/typescript/ui/Dialer.d.ts +29 -0
- package/lib/typescript/ui/IncomingCallView.d.ts +39 -0
- package/lib/typescript/ui/OutgoingCallView.d.ts +28 -0
- package/lib/typescript/ui/index.d.ts +13 -0
- package/lib/typescript/ui/theme.d.ts +20 -0
- package/linphonesw-pod/Sources/LinphoneSdkInfos.swift +4 -0
- package/linphonesw-pod/Sources/LinphoneWrapper.swift +42949 -0
- package/linphonesw-pod/linphonesw.podspec +46 -0
- package/package.json +90 -0
- package/plugin/build/index.js +12 -0
- package/plugin/build/withAndroid.js +78 -0
- package/plugin/build/withIos.js +66 -0
- package/src/CallProvider.tsx +675 -0
- package/src/helpers.ts +179 -0
- package/src/index.ts +84 -0
- package/src/native.ts +185 -0
- package/src/types.ts +202 -0
- package/src/ui/Avatar.tsx +46 -0
- package/src/ui/Dialer.tsx +248 -0
- package/src/ui/IncomingCallView.tsx +161 -0
- package/src/ui/OutgoingCallView.tsx +203 -0
- package/src/ui/index.ts +13 -0
- package/src/ui/theme.ts +36 -0
- package/ui/package.json +6 -0
package/src/helpers.ts
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure helpers used by the provider and UI components.
|
|
3
|
+
*
|
|
4
|
+
* Kept here (not in `native.ts` or `CallProvider.tsx`) so they can be
|
|
5
|
+
* unit-tested without touching React or native modules.
|
|
6
|
+
*/
|
|
7
|
+
import type { CallState, RegistrationState } from './types';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Strips `http(s)://` and trailing `/` from a tenant domain.
|
|
11
|
+
*
|
|
12
|
+
* Multi-tenant backends often hand the SDK an HTTP URL ("https://t1.example.com/")
|
|
13
|
+
* when what we need is a bare SIP host ("t1.example.com"). Doing this once
|
|
14
|
+
* here is friendlier than failing the registration with a cryptic error.
|
|
15
|
+
*/
|
|
16
|
+
export function formatTenantDomain(domain: string | undefined | null): string {
|
|
17
|
+
if (!domain) return '';
|
|
18
|
+
return domain.replace(/^https?:\/\//, '').replace(/\/$/, '');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Extract the user-part of `sip:user@domain` (or returns the input unchanged).
|
|
23
|
+
*
|
|
24
|
+
* The regex stops at `@`, `;` (URI params like `;transport=tcp`), and `>`
|
|
25
|
+
* (display-name angle brackets) so it handles all three common SIP URI
|
|
26
|
+
* formats without separate parsers.
|
|
27
|
+
*/
|
|
28
|
+
export function parseSipUser(sipUri: string = ''): string {
|
|
29
|
+
const m = /sip:([^@;>]+)/i.exec(sipUri);
|
|
30
|
+
return m?.[1] ?? sipUri.replace(/^sip:/i, '');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Allowlist anything dialable: digits plus the three special SIP keys.
|
|
34
|
+
// Notably DROPS letters — PSTN gateways reject them, and the dial-pad
|
|
35
|
+
// converts letters via the standard ABC/DEF mapping before getting here.
|
|
36
|
+
export function sanitizeDial(input: string = ''): string {
|
|
37
|
+
return input.replace(/[^\d+#*]/g, '');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Format seconds as `M:SS` or `H:MM:SS`. */
|
|
41
|
+
export function formatDuration(totalSeconds: number): string {
|
|
42
|
+
const s = Math.max(0, Math.floor(totalSeconds));
|
|
43
|
+
const h = Math.floor(s / 3600);
|
|
44
|
+
const m = Math.floor((s % 3600) / 60);
|
|
45
|
+
const sec = s % 60;
|
|
46
|
+
const pad = (n: number) => String(n).padStart(2, '0');
|
|
47
|
+
return h ? `${h}:${pad(m)}:${pad(sec)}` : `${m}:${pad(sec)}`;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Convert two-character initials for avatar placeholders. */
|
|
51
|
+
export function initialsFrom(name: string | undefined | null): string {
|
|
52
|
+
return (name || '??').slice(0, 2).toUpperCase();
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Linphone numeric-state → string-state mapping.
|
|
57
|
+
*
|
|
58
|
+
* The SDK's current native modules emit states as strings ("Connected"),
|
|
59
|
+
* but older Linphone bindings — and any third party that wires up the
|
|
60
|
+
* native module directly — may still send the raw enum ordinals. Keeping
|
|
61
|
+
* this map means a Linphone upgrade or a different binding doesn't break
|
|
62
|
+
* existing apps.
|
|
63
|
+
*/
|
|
64
|
+
const CALL_STATE_BY_INT: Record<number, CallState> = {
|
|
65
|
+
0: 'Idle',
|
|
66
|
+
1: 'IncomingReceived',
|
|
67
|
+
2: 'PushIncomingReceived',
|
|
68
|
+
3: 'OutgoingInit',
|
|
69
|
+
4: 'OutgoingProgress',
|
|
70
|
+
5: 'OutgoingRinging',
|
|
71
|
+
6: 'OutgoingEarlyMedia',
|
|
72
|
+
7: 'Connected',
|
|
73
|
+
8: 'StreamsRunning',
|
|
74
|
+
9: 'Pausing',
|
|
75
|
+
10: 'Paused',
|
|
76
|
+
11: 'Resuming',
|
|
77
|
+
12: 'Referred',
|
|
78
|
+
13: 'Error',
|
|
79
|
+
14: 'End',
|
|
80
|
+
15: 'PausedByRemote',
|
|
81
|
+
16: 'UpdatedByRemote',
|
|
82
|
+
17: 'IncomingEarlyMedia',
|
|
83
|
+
18: 'Updating',
|
|
84
|
+
19: 'Released',
|
|
85
|
+
20: 'EarlyUpdatedByRemote',
|
|
86
|
+
21: 'EarlyUpdating',
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const REG_STATE_BY_INT: Record<number, RegistrationState> = {
|
|
90
|
+
0: 'none',
|
|
91
|
+
1: 'progress',
|
|
92
|
+
2: 'ok',
|
|
93
|
+
3: 'cleared',
|
|
94
|
+
4: 'failed',
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
/** Normalise a raw call-state value (string or int) to its canonical string form. */
|
|
98
|
+
export function callStateName(raw: unknown): CallState {
|
|
99
|
+
if (typeof raw === 'number') return CALL_STATE_BY_INT[raw] ?? String(raw);
|
|
100
|
+
return String(raw ?? '') as CallState;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Normalise a raw registration-state value (string or int) to canonical lowercase string. */
|
|
104
|
+
export function regStateName(raw: unknown): RegistrationState {
|
|
105
|
+
if (typeof raw === 'number') {
|
|
106
|
+
return REG_STATE_BY_INT[raw] ?? 'unknown';
|
|
107
|
+
}
|
|
108
|
+
const s = String(raw ?? '').toLowerCase();
|
|
109
|
+
if (
|
|
110
|
+
s === 'none' ||
|
|
111
|
+
s === 'progress' ||
|
|
112
|
+
s === 'ok' ||
|
|
113
|
+
s === 'cleared' ||
|
|
114
|
+
s === 'failed'
|
|
115
|
+
) {
|
|
116
|
+
return s as RegistrationState;
|
|
117
|
+
}
|
|
118
|
+
return 'unknown';
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Map a raw call status to a user-friendly UI label.
|
|
123
|
+
*
|
|
124
|
+
* You typically don't need to call this directly — `useCall().callStatus`
|
|
125
|
+
* already returns the canonical state name, and the bundled
|
|
126
|
+
* `<OutgoingCallView>` does its own mapping. Use this when building a custom UI.
|
|
127
|
+
*/
|
|
128
|
+
export function callStatusLabel(status: CallState): string {
|
|
129
|
+
switch (status) {
|
|
130
|
+
case 'OutgoingInit':
|
|
131
|
+
case 'OutgoingProgress':
|
|
132
|
+
case 'OutgoingEarlyMedia':
|
|
133
|
+
return 'Calling…';
|
|
134
|
+
case 'OutgoingRinging':
|
|
135
|
+
return 'Ringing…';
|
|
136
|
+
case 'Connected':
|
|
137
|
+
case 'StreamsRunning':
|
|
138
|
+
return 'In progress';
|
|
139
|
+
case 'Pausing':
|
|
140
|
+
return 'Pausing…';
|
|
141
|
+
case 'Paused':
|
|
142
|
+
case 'PausedByRemote':
|
|
143
|
+
return 'On hold';
|
|
144
|
+
case 'Resuming':
|
|
145
|
+
return 'Resuming…';
|
|
146
|
+
case 'End':
|
|
147
|
+
case 'Released':
|
|
148
|
+
return 'Call ended';
|
|
149
|
+
case 'Error':
|
|
150
|
+
return 'Call failed';
|
|
151
|
+
case 'IncomingReceived':
|
|
152
|
+
case 'PushIncomingReceived':
|
|
153
|
+
case 'IncomingEarlyMedia':
|
|
154
|
+
return 'Incoming call';
|
|
155
|
+
default:
|
|
156
|
+
return status || 'Idle';
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Resolve a destination string to a SIP URI.
|
|
162
|
+
*
|
|
163
|
+
* Three-case dispatch in order of "most explicit wins":
|
|
164
|
+
* 1. `sip:100@gw.com` → already a URI, pass through unchanged.
|
|
165
|
+
* 2. `100@gw.com` → caller specified the gateway, just prefix `sip:`.
|
|
166
|
+
* 3. `100` (bare) → fill in the configured domain.
|
|
167
|
+
*
|
|
168
|
+
* This lets apps mix internal-extension dialling with PSTN gateway URIs
|
|
169
|
+
* without needing a separate API.
|
|
170
|
+
*/
|
|
171
|
+
export function destinationToSipUri(
|
|
172
|
+
destination: string,
|
|
173
|
+
domain: string
|
|
174
|
+
): string {
|
|
175
|
+
if (!destination) return '';
|
|
176
|
+
if (destination.startsWith('sip:')) return destination;
|
|
177
|
+
if (destination.includes('@')) return `sip:${destination}`;
|
|
178
|
+
return `sip:${destination}@${formatTenantDomain(domain)}`;
|
|
179
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Public entry point for `@nativetalkcommunications/react-native-call-sdk`.
|
|
3
|
+
*
|
|
4
|
+
* Most apps only need:
|
|
5
|
+
*
|
|
6
|
+
* ```tsx
|
|
7
|
+
* import { CallProvider, useCall } from '@nativetalkcommunications/react-native-call-sdk';
|
|
8
|
+
* ```
|
|
9
|
+
*
|
|
10
|
+
* Optional UI components are exported from the `/ui` sub-path:
|
|
11
|
+
*
|
|
12
|
+
* ```tsx
|
|
13
|
+
* import { Dialer, IncomingCallView, OutgoingCallView } from '@nativetalkcommunications/react-native-call-sdk/ui';
|
|
14
|
+
* ```
|
|
15
|
+
*
|
|
16
|
+
* Power-users can reach the raw native bridge:
|
|
17
|
+
*
|
|
18
|
+
* ```ts
|
|
19
|
+
* import { CallEngine } from '@nativetalkcommunications/react-native-call-sdk';
|
|
20
|
+
* CallEngine.call('sip:100@sip.example.com');
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
export { CallProvider, useCall, useCallSafe } from './CallProvider';
|
|
25
|
+
export type {
|
|
26
|
+
CallApi,
|
|
27
|
+
CallLogEntry,
|
|
28
|
+
CallProviderEvents,
|
|
29
|
+
CallProviderProps,
|
|
30
|
+
CallState,
|
|
31
|
+
DeclineReason,
|
|
32
|
+
IncomingCallInfo,
|
|
33
|
+
RegistrationEvent,
|
|
34
|
+
RegistrationState,
|
|
35
|
+
SipConfig,
|
|
36
|
+
SipTransport,
|
|
37
|
+
} from './types';
|
|
38
|
+
export {
|
|
39
|
+
callStateName,
|
|
40
|
+
callStatusLabel,
|
|
41
|
+
destinationToSipUri,
|
|
42
|
+
formatDuration,
|
|
43
|
+
formatTenantDomain,
|
|
44
|
+
initialsFrom,
|
|
45
|
+
parseSipUser,
|
|
46
|
+
regStateName,
|
|
47
|
+
sanitizeDial,
|
|
48
|
+
} from './helpers';
|
|
49
|
+
|
|
50
|
+
import * as Native from './native';
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Direct access to the native bridge.
|
|
54
|
+
*
|
|
55
|
+
* This is escape-hatch territory — prefer `useCall()` whenever possible. Use
|
|
56
|
+
* `CallEngine` only when you need to drive the SDK from outside React (e.g.
|
|
57
|
+
* a headless task) or to wire iOS VoIP push tokens.
|
|
58
|
+
*/
|
|
59
|
+
export const CallEngine = {
|
|
60
|
+
init: Native.init,
|
|
61
|
+
register: Native.register,
|
|
62
|
+
call: Native.call,
|
|
63
|
+
answer: Native.answer,
|
|
64
|
+
end: Native.end,
|
|
65
|
+
hangup: Native.end,
|
|
66
|
+
decline: Native.decline,
|
|
67
|
+
mute: Native.mute,
|
|
68
|
+
speaker: Native.speaker,
|
|
69
|
+
hold: Native.hold,
|
|
70
|
+
resume: Native.resume,
|
|
71
|
+
sendDtmf: Native.sendDtmf,
|
|
72
|
+
playKeyTone: Native.playKeyTone,
|
|
73
|
+
refreshRegisters: Native.refreshRegisters,
|
|
74
|
+
setRegisterEnabled: Native.setRegisterEnabled,
|
|
75
|
+
getRegistrationStatus: Native.getRegistrationStatus,
|
|
76
|
+
getCallLogs: Native.getCallLogs,
|
|
77
|
+
startNativeServices: Native.startNativeServices,
|
|
78
|
+
stopNativeServices: Native.stopNativeServices,
|
|
79
|
+
registerVoipToken: Native.registerVoipToken,
|
|
80
|
+
ensureMicPermission: Native.ensureMicPermission,
|
|
81
|
+
on: Native.on,
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
export { callEvents } from './native';
|
package/src/native.ts
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Thin bridge to the native `NativetalkCallSdk` module.
|
|
3
|
+
*
|
|
4
|
+
* This file is the only place that talks to `NativeModules` and the event
|
|
5
|
+
* emitter. Everything else in the SDK uses these exports so the JS layer
|
|
6
|
+
* stays testable.
|
|
7
|
+
*
|
|
8
|
+
* If you see "TurboModuleRegistry … was not found" at runtime, the native
|
|
9
|
+
* library isn't linked. See `docs/installation.md`.
|
|
10
|
+
*/
|
|
11
|
+
import {
|
|
12
|
+
NativeEventEmitter,
|
|
13
|
+
NativeModules,
|
|
14
|
+
PermissionsAndroid,
|
|
15
|
+
Platform,
|
|
16
|
+
} from 'react-native';
|
|
17
|
+
import type { CallLogEntry, RegistrationEvent, SipTransport } from './types';
|
|
18
|
+
|
|
19
|
+
const LINKING_ERROR =
|
|
20
|
+
`The package '@nativetalkcommunications/react-native-call-sdk' doesn't seem to be linked. Make sure:\n\n` +
|
|
21
|
+
`- you rebuilt the app after installing the package\n` +
|
|
22
|
+
`- you are not using Expo Go (use a dev client or bare workflow)\n` +
|
|
23
|
+
`- (iOS) you ran 'pod install' inside the ios/ directory\n`;
|
|
24
|
+
|
|
25
|
+
// Proxy fallback: if autolinking failed, every method access throws a helpful
|
|
26
|
+
// error instead of a cryptic "undefined is not a function". This means a
|
|
27
|
+
// misconfigured app crashes EARLY with a clear message, not deep inside JS.
|
|
28
|
+
const NativetalkCallSdk =
|
|
29
|
+
NativeModules.NativetalkCallSdk ??
|
|
30
|
+
new Proxy(
|
|
31
|
+
{},
|
|
32
|
+
{
|
|
33
|
+
get() {
|
|
34
|
+
throw new Error(LINKING_ERROR);
|
|
35
|
+
},
|
|
36
|
+
}
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
// Single shared emitter — re-creating it per subscription wastes native
|
|
40
|
+
// handlers and can drop events during the swap.
|
|
41
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
42
|
+
export const callEvents = new NativeEventEmitter(NativetalkCallSdk as any);
|
|
43
|
+
|
|
44
|
+
/** Request microphone permission on Android. Returns true if granted (or platform is iOS). */
|
|
45
|
+
export async function ensureMicPermission(): Promise<boolean> {
|
|
46
|
+
if (Platform.OS !== 'android') return true;
|
|
47
|
+
const granted = await PermissionsAndroid.request(
|
|
48
|
+
PermissionsAndroid.PERMISSIONS.RECORD_AUDIO,
|
|
49
|
+
{
|
|
50
|
+
title: 'Microphone Permission',
|
|
51
|
+
message: 'Microphone access is required to make and receive calls.',
|
|
52
|
+
buttonPositive: 'OK',
|
|
53
|
+
}
|
|
54
|
+
);
|
|
55
|
+
return granted === PermissionsAndroid.RESULTS.GRANTED;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// --- Core lifecycle ---
|
|
59
|
+
|
|
60
|
+
// Idempotent on the native side, safe to call multiple times.
|
|
61
|
+
export function init(cfg?: Record<string, unknown>): Promise<void> {
|
|
62
|
+
return NativetalkCallSdk.init(cfg ?? {});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Android-only: iOS uses CallKit + VoIP push instead of a background
|
|
66
|
+
// service. Guarding here keeps the JS API symmetric across platforms.
|
|
67
|
+
export function startNativeServices(): void {
|
|
68
|
+
if (Platform.OS !== 'android') return;
|
|
69
|
+
NativetalkCallSdk.startNativeServices();
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function stopNativeServices(logout = false): void {
|
|
73
|
+
if (Platform.OS !== 'android') return;
|
|
74
|
+
NativetalkCallSdk.stopNativeServices(logout);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// --- Registration ---
|
|
78
|
+
|
|
79
|
+
export interface RegisterArgs {
|
|
80
|
+
username: string;
|
|
81
|
+
password: string;
|
|
82
|
+
domain: string;
|
|
83
|
+
transport?: SipTransport;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function register(account: RegisterArgs): void {
|
|
87
|
+
NativetalkCallSdk.register(account);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Optional-chained: older native builds may not export this method, and
|
|
91
|
+
// we'd rather no-op than crash on a non-critical refresh.
|
|
92
|
+
export function refreshRegisters(): void {
|
|
93
|
+
NativetalkCallSdk.refreshRegisters?.();
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function setRegisterEnabled(enabled: boolean): void {
|
|
97
|
+
NativetalkCallSdk.setRegisterEnabled(enabled);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function getRegistrationStatus(): Promise<RegistrationEvent> {
|
|
101
|
+
return NativetalkCallSdk.getRegistrationStatus();
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// --- Call control ---
|
|
105
|
+
|
|
106
|
+
export function call(sipUri: string): void {
|
|
107
|
+
NativetalkCallSdk.call(sipUri);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function answer(): void {
|
|
111
|
+
NativetalkCallSdk.answer();
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function end(): void {
|
|
115
|
+
NativetalkCallSdk.end();
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function decline(reason: string = 'declined'): void {
|
|
119
|
+
NativetalkCallSdk.decline?.(reason);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function mute(on: boolean): void {
|
|
123
|
+
NativetalkCallSdk.mute(on);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function speaker(on: boolean): void {
|
|
127
|
+
NativetalkCallSdk.speaker(on);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function hold(): void {
|
|
131
|
+
NativetalkCallSdk.hold();
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function resume(): void {
|
|
135
|
+
NativetalkCallSdk.resume();
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function sendDtmf(digit: string): void {
|
|
139
|
+
NativetalkCallSdk.sendDtmf(digit);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export function playKeyTone(digit: string): void {
|
|
143
|
+
NativetalkCallSdk.playKeyTone(digit);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// --- Call logs ---
|
|
147
|
+
|
|
148
|
+
export function getCallLogs(): Promise<CallLogEntry[]> {
|
|
149
|
+
return NativetalkCallSdk.getCallLogs();
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// --- iOS push token (advanced; only used if you wire VoIP push manually) ---
|
|
153
|
+
|
|
154
|
+
// Call from the host AppDelegate's PushKit `didUpdate` handler so the token
|
|
155
|
+
// gets attached to the SIP account params. Android FCM uses a different flow.
|
|
156
|
+
export function registerVoipToken(hex: string): void {
|
|
157
|
+
if (Platform.OS !== 'ios') return;
|
|
158
|
+
NativetalkCallSdk.registerVoipToken?.(hex);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// --- Event subscriptions ---
|
|
162
|
+
//
|
|
163
|
+
// Typed wrappers around `callEvents.addListener` — using string-typed event
|
|
164
|
+
// names directly would lose autocomplete and let typos slip through. The
|
|
165
|
+
// `Sub` return shape mirrors NativeEventEmitter's so consumers can call
|
|
166
|
+
// `.remove()` without importing RN types.
|
|
167
|
+
type Listener<T> = (event: T) => void;
|
|
168
|
+
type Sub = { remove: () => void };
|
|
169
|
+
|
|
170
|
+
export const on = {
|
|
171
|
+
RegistrationChanged: (cb: Listener<any>): Sub =>
|
|
172
|
+
callEvents.addListener('RegistrationChanged', cb),
|
|
173
|
+
CallIncoming: (cb: Listener<any>): Sub =>
|
|
174
|
+
callEvents.addListener('CallIncoming', cb),
|
|
175
|
+
CallState: (cb: Listener<any>): Sub =>
|
|
176
|
+
callEvents.addListener('CallState', cb),
|
|
177
|
+
CallEnded: (cb: Listener<any>): Sub =>
|
|
178
|
+
callEvents.addListener('CallEnded', cb),
|
|
179
|
+
// The TM* events are Android-only (telephony observer for native GSM
|
|
180
|
+
// calls). They never fire on iOS but the subscription is harmless.
|
|
181
|
+
TMPhoneCallState: (cb: Listener<any>): Sub =>
|
|
182
|
+
callEvents.addListener('TMPhoneCallState', cb),
|
|
183
|
+
TMPhoneCallInfo: (cb: Listener<any>): Sub =>
|
|
184
|
+
callEvents.addListener('TMPhoneCallInfo', cb),
|
|
185
|
+
};
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Public types for the Nativetalk Call SDK.
|
|
3
|
+
*
|
|
4
|
+
* These describe the SIP account configuration, registration state, call
|
|
5
|
+
* lifecycle events, and the shape of `useCall()` and `<CallProvider>`.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/** Transport used for SIP signalling. Most providers support `tcp`; use `tls` for encrypted signalling. */
|
|
9
|
+
export type SipTransport = 'udp' | 'tcp' | 'tls';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* SIP account credentials.
|
|
13
|
+
*
|
|
14
|
+
* `domain` should be the SIP server hostname (and optionally `:port`). Do NOT
|
|
15
|
+
* include `sip:` or `http(s)://` — the SDK strips those automatically, but it's
|
|
16
|
+
* cleaner not to send them.
|
|
17
|
+
*/
|
|
18
|
+
export interface SipConfig {
|
|
19
|
+
username: string;
|
|
20
|
+
password: string;
|
|
21
|
+
domain: string;
|
|
22
|
+
transport?: SipTransport;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Registration lifecycle states reported by the underlying SIP stack. */
|
|
26
|
+
export type RegistrationState =
|
|
27
|
+
| 'none'
|
|
28
|
+
| 'progress'
|
|
29
|
+
| 'ok'
|
|
30
|
+
| 'cleared'
|
|
31
|
+
| 'failed'
|
|
32
|
+
| 'unknown';
|
|
33
|
+
|
|
34
|
+
/** Registration state event payload. */
|
|
35
|
+
export interface RegistrationEvent {
|
|
36
|
+
state: RegistrationState;
|
|
37
|
+
message?: string;
|
|
38
|
+
username?: string;
|
|
39
|
+
domain?: string;
|
|
40
|
+
displayName?: string;
|
|
41
|
+
/** Human-readable form of `state` (e.g. `"Ok"`, `"Failed"`). */
|
|
42
|
+
pretty?: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Linphone-derived call lifecycle states. Use the string forms in your UI.
|
|
47
|
+
*
|
|
48
|
+
* The numeric mapping is preserved internally for backwards compatibility with
|
|
49
|
+
* raw Linphone state values, but `useCall().callStatus` always returns the
|
|
50
|
+
* string form.
|
|
51
|
+
*/
|
|
52
|
+
export type CallState =
|
|
53
|
+
| 'Idle'
|
|
54
|
+
| 'IncomingReceived'
|
|
55
|
+
| 'PushIncomingReceived'
|
|
56
|
+
| 'OutgoingInit'
|
|
57
|
+
| 'OutgoingProgress'
|
|
58
|
+
| 'OutgoingRinging'
|
|
59
|
+
| 'OutgoingEarlyMedia'
|
|
60
|
+
| 'Connected'
|
|
61
|
+
| 'StreamsRunning'
|
|
62
|
+
| 'Pausing'
|
|
63
|
+
| 'Paused'
|
|
64
|
+
| 'Resuming'
|
|
65
|
+
| 'Referred'
|
|
66
|
+
| 'Error'
|
|
67
|
+
| 'End'
|
|
68
|
+
| 'PausedByRemote'
|
|
69
|
+
| 'UpdatedByRemote'
|
|
70
|
+
| 'IncomingEarlyMedia'
|
|
71
|
+
| 'Updating'
|
|
72
|
+
| 'Released'
|
|
73
|
+
| 'EarlyUpdatedByRemote'
|
|
74
|
+
| 'EarlyUpdating'
|
|
75
|
+
| string;
|
|
76
|
+
|
|
77
|
+
/** Reason passed to `decline()`. Maps to standard SIP response codes. */
|
|
78
|
+
export type DeclineReason =
|
|
79
|
+
| 'declined'
|
|
80
|
+
| 'busy'
|
|
81
|
+
| 'notacceptable'
|
|
82
|
+
| 'temporarilyunavailable';
|
|
83
|
+
|
|
84
|
+
/** Information about an incoming call, surfaced via `useCall().incomingInfo`. */
|
|
85
|
+
export interface IncomingCallInfo {
|
|
86
|
+
/** Best-effort display name: display-name → username → SIP user-part → `"Unknown"`. */
|
|
87
|
+
name: string;
|
|
88
|
+
/** Phone number or SIP identifier the call came from. */
|
|
89
|
+
phone: string;
|
|
90
|
+
/** Two-character initials for avatar placeholders. */
|
|
91
|
+
initials: string;
|
|
92
|
+
/** SIP call ID. May be missing on push-initiated calls before media flows. */
|
|
93
|
+
callId?: string;
|
|
94
|
+
/** Raw SIP URI of the remote party (`sip:user@domain`). */
|
|
95
|
+
uri?: string;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** A single call history entry, normalised across platforms. */
|
|
99
|
+
export interface CallLogEntry {
|
|
100
|
+
id: number;
|
|
101
|
+
/** ISO-8601 timestamp (UTC). */
|
|
102
|
+
call_start: string;
|
|
103
|
+
call_type: 'LOCAL' | 'DID' | 'STANDARD' | string;
|
|
104
|
+
caller_id: string;
|
|
105
|
+
call_direction: 'inbound' | 'outbound' | string;
|
|
106
|
+
called_number: string;
|
|
107
|
+
/** Platform-specific shape — Android returns `{ text, code }`, iOS returns a string. */
|
|
108
|
+
disposition:
|
|
109
|
+
| { text: string; code: number }
|
|
110
|
+
| string;
|
|
111
|
+
debit: string;
|
|
112
|
+
/** `MM:SS` or `HH:MM:SS`. */
|
|
113
|
+
duration: string;
|
|
114
|
+
destination: string;
|
|
115
|
+
sip_user: string;
|
|
116
|
+
created_at: string;
|
|
117
|
+
updated_at: string;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** Optional callbacks the host app can supply to `<CallProvider>`. */
|
|
121
|
+
export interface CallProviderEvents {
|
|
122
|
+
/** Fired when a new incoming call arrives. Use this to navigate to your incoming-call screen. */
|
|
123
|
+
onIncomingCall?: (info: IncomingCallInfo) => void;
|
|
124
|
+
/** Fired when an outgoing call has been initiated via `dial()`. */
|
|
125
|
+
onOutgoingCall?: (info: { phone: string; initials: string }) => void;
|
|
126
|
+
/** Fired when the active call ends (any reason). */
|
|
127
|
+
onCallEnded?: () => void;
|
|
128
|
+
/** Fired on every call state transition. */
|
|
129
|
+
onCallStateChanged?: (state: CallState) => void;
|
|
130
|
+
/** Fired on every registration state transition. */
|
|
131
|
+
onRegistrationStateChanged?: (event: RegistrationEvent) => void;
|
|
132
|
+
/** Fired when an error occurs (e.g. registration failure). */
|
|
133
|
+
onError?: (error: { code: string; message: string }) => void;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** Props for `<CallProvider>`. */
|
|
137
|
+
export interface CallProviderProps extends CallProviderEvents {
|
|
138
|
+
children: React.ReactNode;
|
|
139
|
+
/**
|
|
140
|
+
* SIP credentials. Pass `null` or omit while you load them from your backend —
|
|
141
|
+
* the SDK will idle until a valid config arrives.
|
|
142
|
+
*/
|
|
143
|
+
config?: SipConfig | null;
|
|
144
|
+
/**
|
|
145
|
+
* If true (default), the SDK calls `register()` automatically whenever
|
|
146
|
+
* `config` changes. Set to false if you want manual control via the
|
|
147
|
+
* `register()` action.
|
|
148
|
+
*/
|
|
149
|
+
autoRegister?: boolean;
|
|
150
|
+
/**
|
|
151
|
+
* If true (default on Android), the SDK starts the background/foreground
|
|
152
|
+
* service so calls can still arrive when the app is backgrounded. Has no
|
|
153
|
+
* effect on iOS — there CallKit + VoIP push handles backgrounding.
|
|
154
|
+
*/
|
|
155
|
+
enableNativeServices?: boolean;
|
|
156
|
+
/**
|
|
157
|
+
* If true, the SDK requests `RECORD_AUDIO` permission on Android during
|
|
158
|
+
* provider mount. Default true. Disable if you handle permissions yourself.
|
|
159
|
+
*/
|
|
160
|
+
requestMicPermission?: boolean;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** Public API returned by `useCall()`. */
|
|
164
|
+
export interface CallApi {
|
|
165
|
+
// --- Config ---
|
|
166
|
+
config: SipConfig | null;
|
|
167
|
+
|
|
168
|
+
// --- State ---
|
|
169
|
+
registration: RegistrationEvent | null;
|
|
170
|
+
callStatus: CallState;
|
|
171
|
+
durationSec: number;
|
|
172
|
+
formattedDuration: string;
|
|
173
|
+
incoming: boolean;
|
|
174
|
+
incomingInfo: IncomingCallInfo | null;
|
|
175
|
+
isMuted: boolean;
|
|
176
|
+
isSpeaker: boolean;
|
|
177
|
+
isHeld: boolean;
|
|
178
|
+
|
|
179
|
+
// --- Controls ---
|
|
180
|
+
dial: (destination: string) => Promise<void>;
|
|
181
|
+
answer: () => Promise<void>;
|
|
182
|
+
hangup: () => Promise<void>;
|
|
183
|
+
decline: (reason?: DeclineReason) => Promise<void>;
|
|
184
|
+
toggleMute: () => void;
|
|
185
|
+
toggleSpeaker: () => void;
|
|
186
|
+
toggleHold: () => void;
|
|
187
|
+
sendDtmf: (digit: string) => void;
|
|
188
|
+
playKeyTone: (digit: string) => void;
|
|
189
|
+
|
|
190
|
+
// --- Registration management ---
|
|
191
|
+
register: (config?: SipConfig) => Promise<void>;
|
|
192
|
+
refreshRegistration: () => Promise<void>;
|
|
193
|
+
unregister: () => Promise<void>;
|
|
194
|
+
|
|
195
|
+
// --- Call history ---
|
|
196
|
+
getCallLogs: () => Promise<CallLogEntry[]>;
|
|
197
|
+
getRegistrationStatus: () => Promise<RegistrationEvent>;
|
|
198
|
+
|
|
199
|
+
// --- Lifecycle (Android only — no-ops on iOS) ---
|
|
200
|
+
startNativeServices: () => void;
|
|
201
|
+
stopNativeServices: (logout?: boolean) => void;
|
|
202
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Text, View, StyleSheet, ViewStyle, TextStyle } from 'react-native';
|
|
3
|
+
|
|
4
|
+
interface AvatarProps {
|
|
5
|
+
initials: string;
|
|
6
|
+
size?: number;
|
|
7
|
+
background?: string;
|
|
8
|
+
color?: string;
|
|
9
|
+
style?: ViewStyle;
|
|
10
|
+
textStyle?: TextStyle;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** Tiny initials-circle used by the bundled call screens. */
|
|
14
|
+
export function Avatar({
|
|
15
|
+
initials,
|
|
16
|
+
size = 80,
|
|
17
|
+
background = '#EEF2FF',
|
|
18
|
+
color = '#2D6BFF',
|
|
19
|
+
style,
|
|
20
|
+
textStyle,
|
|
21
|
+
}: AvatarProps) {
|
|
22
|
+
return (
|
|
23
|
+
<View
|
|
24
|
+
style={[
|
|
25
|
+
styles.base,
|
|
26
|
+
{ width: size, height: size, borderRadius: size / 2, backgroundColor: background },
|
|
27
|
+
style,
|
|
28
|
+
]}
|
|
29
|
+
>
|
|
30
|
+
<Text
|
|
31
|
+
style={[
|
|
32
|
+
styles.text,
|
|
33
|
+
{ color, fontSize: Math.floor(size * 0.4) },
|
|
34
|
+
textStyle,
|
|
35
|
+
]}
|
|
36
|
+
>
|
|
37
|
+
{(initials || '??').slice(0, 2).toUpperCase()}
|
|
38
|
+
</Text>
|
|
39
|
+
</View>
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const styles = StyleSheet.create({
|
|
44
|
+
base: { alignItems: 'center', justifyContent: 'center' },
|
|
45
|
+
text: { fontWeight: '700' },
|
|
46
|
+
});
|