rns-nativecall 0.0.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 +192 -0
- package/android/src/main/java/com/rnsnativecall/CallModule.kt +117 -0
- package/android/src/main/java/com/rnsnativecall/CallPackage.kt +17 -0
- package/android/src/main/java/com/rnsnativecall/MyCallConnection.kt +100 -0
- package/android/src/main/java/com/rnsnativecall/MyConnectionService.kt +70 -0
- package/app.plugin.js +6 -0
- package/index.d.ts +50 -0
- package/index.js +102 -0
- package/ios/CallModule.h +7 -0
- package/ios/CallModule.m +95 -0
- package/package.json +48 -0
- package/rns-nativecall.podspec +18 -0
package/README.md
ADDED
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
# RNS Native Call
|
|
2
|
+
|
|
3
|
+
RNS Native Call is a React Native package that provides fully native VoIP call handling on Android and iOS. It supports self-managed calls, custom ringtone handling, video calls, and allows dismissing native call UI programmatically. Fully TypeScript-ready.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
* Display incoming calls with native UI.
|
|
10
|
+
* Support for video and audio calls.
|
|
11
|
+
* Self-managed calls on Android (prevents polluting the device call log).
|
|
12
|
+
* Prevents iOS call log from being saved using `includesCallsInRecents = NO`.
|
|
13
|
+
* Control ringtone per call.
|
|
14
|
+
* Subscribe to call events (accepted, rejected, failed).
|
|
15
|
+
* Cross-platform (Android & iOS) ready.
|
|
16
|
+
* TypeScript types included.
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## Installation
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
npm install rns-nativecall
|
|
24
|
+
# or
|
|
25
|
+
yarn add rns-nativecall
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## Expo Integration
|
|
31
|
+
|
|
32
|
+
If you are using **Expo managed workflow**, the package provides a plugin that automatically modifies the `AndroidManifest.xml` for required permissions and services.
|
|
33
|
+
|
|
34
|
+
In your `app.json` / `app.config.js`:
|
|
35
|
+
|
|
36
|
+
```js
|
|
37
|
+
import withRaiidrVoip from 'rns-nativecall/withRaiidrVoip';
|
|
38
|
+
|
|
39
|
+
export default {
|
|
40
|
+
expo: {
|
|
41
|
+
name: 'YourApp',
|
|
42
|
+
slug: 'your-app',
|
|
43
|
+
plugins: [
|
|
44
|
+
withRaiidrVoip
|
|
45
|
+
],
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
> The plugin ensures the following Android permissions are added automatically:
|
|
51
|
+
>
|
|
52
|
+
> ```xml
|
|
53
|
+
> <uses-permission android:name="android.permission.READ_PHONE_NUMBERS"/>
|
|
54
|
+
> <uses-permission android:name="android.permission.CALL_PHONE"/>
|
|
55
|
+
> ```
|
|
56
|
+
>
|
|
57
|
+
> It also registers the `MyConnectionService` for Telecom integration:
|
|
58
|
+
>
|
|
59
|
+
> ```xml
|
|
60
|
+
> <service
|
|
61
|
+
> android:name="com.rnsnativecall.MyConnectionService"
|
|
62
|
+
> android:permission="android.permission.BIND_CONNECTION_SERVICE"
|
|
63
|
+
> android:exported="true">
|
|
64
|
+
> <intent-filter>
|
|
65
|
+
> <action android:name="android.telecom.ConnectionService"/>
|
|
66
|
+
> </intent-filter>
|
|
67
|
+
> </service>
|
|
68
|
+
> ```
|
|
69
|
+
|
|
70
|
+
---
|
|
71
|
+
|
|
72
|
+
## iOS Setup
|
|
73
|
+
|
|
74
|
+
1. Make sure you have the pod installed:
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
cd ios
|
|
78
|
+
pod install
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
2. CallKit is automatically used, and call log entries are prevented using:
|
|
82
|
+
|
|
83
|
+
```objc
|
|
84
|
+
config.includesCallsInRecents = NO;
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
> The native iOS module handles showing the call UI and managing call events.
|
|
88
|
+
|
|
89
|
+
---
|
|
90
|
+
|
|
91
|
+
## Usage
|
|
92
|
+
|
|
93
|
+
```ts
|
|
94
|
+
import CallHandler from 'rns-nativecall';
|
|
95
|
+
|
|
96
|
+
// Subscribe to call events
|
|
97
|
+
const unsubscribe = CallHandler.subscribe(
|
|
98
|
+
(data) => console.log('Call Accepted', data),
|
|
99
|
+
(data) => console.log('Call Rejected', data),
|
|
100
|
+
(data) => console.log('Call Failed', data)
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
// Display an incoming call
|
|
104
|
+
await CallHandler.displayCall(
|
|
105
|
+
'uuid-string',
|
|
106
|
+
'+1234567890',
|
|
107
|
+
'John Doe',
|
|
108
|
+
true, // hasVideo
|
|
109
|
+
true // shouldRing
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
// Dismiss call UI programmatically
|
|
113
|
+
CallHandler.destroyNativeCallUI('uuid-string');
|
|
114
|
+
|
|
115
|
+
// Remove event listeners when done
|
|
116
|
+
unsubscribe();
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
---
|
|
120
|
+
|
|
121
|
+
## API
|
|
122
|
+
|
|
123
|
+
### `displayCall(uuid, number, name, hasVideo?, shouldRing?) => Promise<boolean>`
|
|
124
|
+
|
|
125
|
+
Displays native incoming call UI.
|
|
126
|
+
|
|
127
|
+
* **uuid**: Unique call identifier.
|
|
128
|
+
* **number**: Caller number.
|
|
129
|
+
* **name**: Caller display name.
|
|
130
|
+
* **hasVideo**: Boolean, true for video call.
|
|
131
|
+
* **shouldRing**: Boolean, true to play native ringtone.
|
|
132
|
+
|
|
133
|
+
Returns a promise that resolves to `true` if UI was successfully displayed.
|
|
134
|
+
|
|
135
|
+
### `destroyNativeCallUI(uuid)`
|
|
136
|
+
|
|
137
|
+
Dismisses the native call UI.
|
|
138
|
+
|
|
139
|
+
* **uuid**: Call identifier.
|
|
140
|
+
|
|
141
|
+
### `subscribe(onAccept, onReject, onFailed) => () => void`
|
|
142
|
+
|
|
143
|
+
Subscribe to native call events.
|
|
144
|
+
|
|
145
|
+
* **onAccept**: Callback for accepted calls.
|
|
146
|
+
* **onReject**: Callback for rejected calls.
|
|
147
|
+
* **onFailed**: Optional callback for failed calls.
|
|
148
|
+
|
|
149
|
+
Returns a function to unsubscribe all listeners.
|
|
150
|
+
|
|
151
|
+
---
|
|
152
|
+
|
|
153
|
+
## Permissions
|
|
154
|
+
|
|
155
|
+
### Android
|
|
156
|
+
|
|
157
|
+
* `READ_PHONE_NUMBERS`
|
|
158
|
+
* `CALL_PHONE`
|
|
159
|
+
|
|
160
|
+
The Expo plugin ensures these are automatically added to `AndroidManifest.xml`.
|
|
161
|
+
|
|
162
|
+
### iOS
|
|
163
|
+
|
|
164
|
+
* Uses CallKit (`CXProvider`) internally.
|
|
165
|
+
* Prevents call log pollution using `includesCallsInRecents = NO`.
|
|
166
|
+
|
|
167
|
+
---
|
|
168
|
+
|
|
169
|
+
## TypeScript Support
|
|
170
|
+
|
|
171
|
+
Types are included. Example:
|
|
172
|
+
|
|
173
|
+
```ts
|
|
174
|
+
import CallHandler, { CallData, CallAcceptedCallback } from 'rns-nativecall';
|
|
175
|
+
|
|
176
|
+
const onAccept: CallAcceptedCallback = (data: CallData) => {
|
|
177
|
+
console.log('Accepted:', data.callUUID);
|
|
178
|
+
};
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
---
|
|
182
|
+
|
|
183
|
+
## Notes
|
|
184
|
+
|
|
185
|
+
* Android uses `Connection.CAPABILITY_SELF_MANAGED` to prevent calls from showing in the system call log.
|
|
186
|
+
* iOS uses `CXCallEndedReasonRemoteEnded` for clean UI dismissal without showing failed call overlays.
|
|
187
|
+
|
|
188
|
+
---
|
|
189
|
+
|
|
190
|
+
## License
|
|
191
|
+
|
|
192
|
+
MIT
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
///Users/bush/Desktop/Apps/Raiidr/package/android/src/main/java/com/rnsnativecall/CallModule.kt
|
|
2
|
+
package com.rnsnativecall
|
|
3
|
+
|
|
4
|
+
import android.content.ComponentName
|
|
5
|
+
import android.content.Context
|
|
6
|
+
import android.content.Intent
|
|
7
|
+
import android.net.Uri
|
|
8
|
+
import android.os.Bundle
|
|
9
|
+
import android.telecom.PhoneAccount
|
|
10
|
+
import android.telecom.PhoneAccountHandle
|
|
11
|
+
import android.telecom.TelecomManager
|
|
12
|
+
import android.telecom.DisconnectCause
|
|
13
|
+
import com.facebook.react.bridge.*
|
|
14
|
+
import com.facebook.react.modules.core.DeviceEventManagerModule
|
|
15
|
+
|
|
16
|
+
class CallModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaModule(reactContext) {
|
|
17
|
+
|
|
18
|
+
init {
|
|
19
|
+
instance = this
|
|
20
|
+
registerPhoneAccount()
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
override fun getName() = "CallModule"
|
|
24
|
+
|
|
25
|
+
private fun registerPhoneAccount() {
|
|
26
|
+
val telecomManager = reactApplicationContext.getSystemService(Context.TELECOM_SERVICE) as TelecomManager
|
|
27
|
+
val componentName = ComponentName(reactApplicationContext, MyConnectionService::class.java)
|
|
28
|
+
val phoneAccountHandle = PhoneAccountHandle(componentName, "RaiidrVoip")
|
|
29
|
+
|
|
30
|
+
// Combine all critical capabilities into one mask this is important!!!
|
|
31
|
+
val capabilities = PhoneAccount.CAPABILITY_VIDEO_CALLING or
|
|
32
|
+
PhoneAccount.CAPABILITY_SUPPORTS_VIDEO_CALLING
|
|
33
|
+
|
|
34
|
+
val phoneAccount = PhoneAccount.builder(phoneAccountHandle, "Raiidr")
|
|
35
|
+
.setCapabilities(capabilities)
|
|
36
|
+
.setCapabilities(PhoneAccount.CAPABILITY_SELF_MANAGED)
|
|
37
|
+
.setCapabilities(PhoneAccount.CAPABILITY_CALL_PROVIDER)
|
|
38
|
+
.build()
|
|
39
|
+
|
|
40
|
+
telecomManager.registerPhoneAccount(phoneAccount)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
@ReactMethod
|
|
44
|
+
fun displayIncomingCall(
|
|
45
|
+
uuid: String,
|
|
46
|
+
number: String,
|
|
47
|
+
name: String,
|
|
48
|
+
hasVideo: Boolean,
|
|
49
|
+
playRing: Boolean,
|
|
50
|
+
promise: Promise
|
|
51
|
+
) {
|
|
52
|
+
val telecomManager = reactApplicationContext.getSystemService(Context.TELECOM_SERVICE) as TelecomManager
|
|
53
|
+
val componentName = ComponentName(reactApplicationContext, MyConnectionService::class.java)
|
|
54
|
+
val phoneAccountHandle = PhoneAccountHandle(componentName, "RaiidrVoip")
|
|
55
|
+
|
|
56
|
+
val extras = Bundle().apply {
|
|
57
|
+
putParcelable(
|
|
58
|
+
TelecomManager.EXTRA_INCOMING_CALL_ADDRESS,
|
|
59
|
+
Uri.fromParts("sip", number, null)
|
|
60
|
+
)
|
|
61
|
+
putString(TelecomManager.EXTRA_CALL_SUBJECT, name)
|
|
62
|
+
putString("EXTRA_CALL_UUID", uuid)
|
|
63
|
+
putBoolean("EXTRA_PLAY_RING", playRing)
|
|
64
|
+
putBoolean(TelecomManager.EXTRA_START_CALL_WITH_VIDEO_STATE, hasVideo)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
try {
|
|
68
|
+
telecomManager.addNewIncomingCall(phoneAccountHandle, extras)
|
|
69
|
+
promise.resolve(true)
|
|
70
|
+
} catch (e: Exception) {
|
|
71
|
+
promise.reject("CALL_ERROR", e.message)
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
@ReactMethod
|
|
76
|
+
fun endNativeCall(uuid: String) {
|
|
77
|
+
val connection = MyConnectionService.getConnection(uuid)
|
|
78
|
+
connection?.let {
|
|
79
|
+
it.setDisconnected(DisconnectCause(DisconnectCause.LOCAL))
|
|
80
|
+
it.destroy()
|
|
81
|
+
MyConnectionService.removeConnection(uuid)
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
@ReactMethod
|
|
86
|
+
fun checkTelecomPermissions(promise: Promise) {
|
|
87
|
+
val telecomManager = reactApplicationContext.getSystemService(Context.TELECOM_SERVICE) as TelecomManager
|
|
88
|
+
val componentName = ComponentName(reactApplicationContext, MyConnectionService::class.java)
|
|
89
|
+
val phoneAccountHandle = PhoneAccountHandle(componentName, "RaiidrVoip")
|
|
90
|
+
|
|
91
|
+
val account = telecomManager.getPhoneAccount(phoneAccountHandle)
|
|
92
|
+
if (account != null && account.isEnabled) {
|
|
93
|
+
promise.resolve(true)
|
|
94
|
+
} else {
|
|
95
|
+
val intent = Intent(TelecomManager.ACTION_CHANGE_PHONE_ACCOUNTS)
|
|
96
|
+
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
|
97
|
+
reactApplicationContext.startActivity(intent)
|
|
98
|
+
promise.resolve(false)
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
@ReactMethod fun addListener(eventName: String) {}
|
|
103
|
+
@ReactMethod fun removeListeners(count: Int) {}
|
|
104
|
+
|
|
105
|
+
companion object {
|
|
106
|
+
private var instance: CallModule? = null
|
|
107
|
+
|
|
108
|
+
fun sendEventToJS(eventName: String, uuid: String?) {
|
|
109
|
+
val params = Arguments.createMap().apply {
|
|
110
|
+
putString("callUUID", uuid)
|
|
111
|
+
}
|
|
112
|
+
instance?.reactApplicationContext
|
|
113
|
+
?.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
|
|
114
|
+
?.emit(eventName, params)
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/////Users/bush/Desktop/Apps/Raiidr/package/android/src/main/java/com/rnsnativecall/CallPackage.kt
|
|
2
|
+
package com.rnsnativecall
|
|
3
|
+
|
|
4
|
+
import com.facebook.react.ReactPackage
|
|
5
|
+
import com.facebook.react.bridge.NativeModule
|
|
6
|
+
import com.facebook.react.bridge.ReactApplicationContext
|
|
7
|
+
import com.facebook.react.uimanager.ViewManager
|
|
8
|
+
|
|
9
|
+
class CallPackage : ReactPackage {
|
|
10
|
+
override fun createNativeModules(reactContext: ReactApplicationContext): List<NativeModule> {
|
|
11
|
+
return listOf(CallModule(reactContext))
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
override fun createViewManagers(reactContext: ReactApplicationContext): List<ViewManager<*, *>> {
|
|
15
|
+
return emptyList()
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/////Users/bush/Desktop/Apps/Raiidr/package/android/src/main/java/com/rnsnativecall/MyCallConnection.kt
|
|
2
|
+
package com.rnsnativecall
|
|
3
|
+
|
|
4
|
+
import android.content.Context
|
|
5
|
+
import android.content.Intent
|
|
6
|
+
import android.media.AudioAttributes
|
|
7
|
+
import android.media.MediaPlayer
|
|
8
|
+
import android.media.RingtoneManager
|
|
9
|
+
import android.telecom.Connection
|
|
10
|
+
import android.telecom.DisconnectCause
|
|
11
|
+
|
|
12
|
+
class MyCallConnection(
|
|
13
|
+
private val context: Context,
|
|
14
|
+
private val callUUID: String?,
|
|
15
|
+
private val playRing: Boolean,
|
|
16
|
+
private val onAcceptCallback: (String?) -> Unit,
|
|
17
|
+
private val onRejectCallback: (String?) -> Unit
|
|
18
|
+
) : Connection() {
|
|
19
|
+
|
|
20
|
+
private var mediaPlayer: MediaPlayer? = null
|
|
21
|
+
|
|
22
|
+
init {
|
|
23
|
+
connectionProperties = PROPERTY_SELF_MANAGED
|
|
24
|
+
audioModeIsVoip = true
|
|
25
|
+
|
|
26
|
+
if (playRing) {
|
|
27
|
+
startRingtone()
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
private fun startRingtone() {
|
|
32
|
+
val uri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_RINGTONE)
|
|
33
|
+
|
|
34
|
+
mediaPlayer = MediaPlayer().apply {
|
|
35
|
+
setDataSource(context, uri)
|
|
36
|
+
setAudioAttributes(
|
|
37
|
+
AudioAttributes.Builder()
|
|
38
|
+
.setUsage(AudioAttributes.USAGE_NOTIFICATION_RINGTONE)
|
|
39
|
+
.setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
|
|
40
|
+
.build()
|
|
41
|
+
)
|
|
42
|
+
isLooping = true
|
|
43
|
+
prepare()
|
|
44
|
+
start()
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
private fun stopRingtone() {
|
|
49
|
+
mediaPlayer?.stop()
|
|
50
|
+
mediaPlayer?.release()
|
|
51
|
+
mediaPlayer = null
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
private fun cleanUp() {
|
|
55
|
+
stopRingtone()
|
|
56
|
+
callUUID?.let { MyConnectionService.removeConnection(it) }
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
override fun onAnswer() {
|
|
60
|
+
stopRingtone()
|
|
61
|
+
|
|
62
|
+
// Bring app to foreground
|
|
63
|
+
val launchIntent = context.packageManager.getLaunchIntentForPackage(context.packageName)
|
|
64
|
+
launchIntent?.apply {
|
|
65
|
+
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_SINGLE_TOP)
|
|
66
|
+
context.startActivity(this)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Notify JS
|
|
70
|
+
onAcceptCallback(callUUID)
|
|
71
|
+
|
|
72
|
+
// 🔥 IMMEDIATELY detach from Telecom
|
|
73
|
+
setDisconnected(DisconnectCause(DisconnectCause.CANCELED))
|
|
74
|
+
cleanUp()
|
|
75
|
+
destroy()
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
override fun onReject() {
|
|
79
|
+
stopRingtone()
|
|
80
|
+
setDisconnected(DisconnectCause(DisconnectCause.REJECTED))
|
|
81
|
+
cleanUp()
|
|
82
|
+
destroy()
|
|
83
|
+
onRejectCallback(callUUID)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
override fun onDisconnect() {
|
|
87
|
+
stopRingtone()
|
|
88
|
+
setDisconnected(DisconnectCause(DisconnectCause.LOCAL))
|
|
89
|
+
cleanUp()
|
|
90
|
+
destroy()
|
|
91
|
+
onRejectCallback(callUUID)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
override fun onAbort() {
|
|
95
|
+
stopRingtone()
|
|
96
|
+
setDisconnected(DisconnectCause(DisconnectCause.CANCELED))
|
|
97
|
+
cleanUp()
|
|
98
|
+
destroy()
|
|
99
|
+
}
|
|
100
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/////Users/bush/Desktop/Apps/Raiidr/package/android/src/main/java/com/rnsnativecall/MyConnectionService.kt
|
|
2
|
+
package com.rnsnativecall
|
|
3
|
+
|
|
4
|
+
import android.telecom.Connection
|
|
5
|
+
import android.telecom.ConnectionRequest
|
|
6
|
+
import android.telecom.ConnectionService
|
|
7
|
+
import android.telecom.PhoneAccountHandle
|
|
8
|
+
import android.telecom.TelecomManager
|
|
9
|
+
import java.util.concurrent.ConcurrentHashMap
|
|
10
|
+
|
|
11
|
+
class MyConnectionService : ConnectionService() {
|
|
12
|
+
|
|
13
|
+
companion object {
|
|
14
|
+
private val activeConnections = ConcurrentHashMap<String, MyCallConnection>()
|
|
15
|
+
|
|
16
|
+
fun getConnection(uuid: String) = activeConnections[uuid]
|
|
17
|
+
fun addConnection(uuid: String, connection: MyCallConnection) {
|
|
18
|
+
activeConnections[uuid] = connection
|
|
19
|
+
}
|
|
20
|
+
fun removeConnection(uuid: String) {
|
|
21
|
+
activeConnections.remove(uuid)
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
override fun onCreateIncomingConnection(
|
|
26
|
+
connectionManagerPhoneAccount: PhoneAccountHandle?,
|
|
27
|
+
request: ConnectionRequest?
|
|
28
|
+
): Connection {
|
|
29
|
+
|
|
30
|
+
// 1️⃣ Read extras FIRST
|
|
31
|
+
val extras = request?.extras
|
|
32
|
+
val callUUID = extras?.getString("EXTRA_CALL_UUID")
|
|
33
|
+
val playRing = extras?.getBoolean("EXTRA_PLAY_RING", true) ?: true
|
|
34
|
+
val callerName = extras?.getString(TelecomManager.EXTRA_CALL_SUBJECT)
|
|
35
|
+
|
|
36
|
+
// 2️⃣ Create connection
|
|
37
|
+
val connection = MyCallConnection(
|
|
38
|
+
context = applicationContext,
|
|
39
|
+
callUUID = callUUID,
|
|
40
|
+
playRing = playRing,
|
|
41
|
+
onAcceptCallback = { uuid ->
|
|
42
|
+
CallModule.sendEventToJS("onCallAccepted", uuid)
|
|
43
|
+
},
|
|
44
|
+
onRejectCallback = { uuid ->
|
|
45
|
+
CallModule.sendEventToJS("onCallRejected", uuid)
|
|
46
|
+
}
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
// 3️⃣ Set caller display name (THIS FIXES MISALIGNMENT)
|
|
50
|
+
callerName?.let {
|
|
51
|
+
connection.setCallerDisplayName(
|
|
52
|
+
it,
|
|
53
|
+
TelecomManager.PRESENTATION_ALLOWED
|
|
54
|
+
)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// 4️⃣ Standard Telecom setup
|
|
58
|
+
connection.connectionCapabilities =
|
|
59
|
+
Connection.CAPABILITY_MUTE or Connection.CAPABILITY_SUPPORT_HOLD
|
|
60
|
+
|
|
61
|
+
connection.setAddress(request?.address, TelecomManager.PRESENTATION_ALLOWED)
|
|
62
|
+
connection.setInitializing()
|
|
63
|
+
connection.setRinging()
|
|
64
|
+
|
|
65
|
+
// 5️⃣ Track connection
|
|
66
|
+
callUUID?.let { addConnection(it, connection) }
|
|
67
|
+
|
|
68
|
+
return connection
|
|
69
|
+
}
|
|
70
|
+
}
|
package/app.plugin.js
ADDED
package/index.d.ts
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
///Users/bush/Desktop/Apps/Raiidr/package/index.d.ts
|
|
2
|
+
export interface CallData {
|
|
3
|
+
callUUID: string;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export type CallAcceptedCallback = (data: CallData) => void;
|
|
7
|
+
export type CallRejectedCallback = (data: CallData) => void;
|
|
8
|
+
export type CallFailedCallback = (data: CallData) => void;
|
|
9
|
+
|
|
10
|
+
export interface CallHandlerType {
|
|
11
|
+
/**
|
|
12
|
+
* Display an incoming call UI.
|
|
13
|
+
* @param uuid Unique call identifier
|
|
14
|
+
* @param number Caller number
|
|
15
|
+
* @param name Caller display name
|
|
16
|
+
* @param hasVideo True if video call
|
|
17
|
+
* @param shouldRing True to play native ringtone
|
|
18
|
+
* @returns Promise resolving to true if successfully displayed
|
|
19
|
+
*/
|
|
20
|
+
displayCall(
|
|
21
|
+
uuid: string,
|
|
22
|
+
number: string,
|
|
23
|
+
name: string,
|
|
24
|
+
hasVideo?: boolean,
|
|
25
|
+
shouldRing?: boolean
|
|
26
|
+
): Promise<boolean>;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Dismiss native call UI
|
|
30
|
+
* @param uuid Call identifier
|
|
31
|
+
*/
|
|
32
|
+
destroyNativeCallUI(uuid: string): void;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Subscribe to call events
|
|
36
|
+
* @param onAccept Callback for accepted calls
|
|
37
|
+
* @param onReject Callback for rejected calls
|
|
38
|
+
* @param onFailed Optional callback for failed calls
|
|
39
|
+
* @returns Function to unsubscribe all listeners
|
|
40
|
+
*/
|
|
41
|
+
subscribe(
|
|
42
|
+
onAccept: CallAcceptedCallback,
|
|
43
|
+
onReject: CallRejectedCallback,
|
|
44
|
+
onFailed?: CallFailedCallback
|
|
45
|
+
): () => void;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
declare const CallHandler: CallHandlerType;
|
|
49
|
+
|
|
50
|
+
export default CallHandler;
|
package/index.js
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
///Users/bush/Desktop/Apps/Raiidr/package/index.js
|
|
2
|
+
import {
|
|
3
|
+
NativeModules,
|
|
4
|
+
NativeEventEmitter,
|
|
5
|
+
PermissionsAndroid,
|
|
6
|
+
Platform,
|
|
7
|
+
Linking,
|
|
8
|
+
Alert
|
|
9
|
+
} from 'react-native';
|
|
10
|
+
|
|
11
|
+
const { CallModule } = NativeModules;
|
|
12
|
+
const callEventEmitter = CallModule ? new NativeEventEmitter(CallModule) : null;
|
|
13
|
+
|
|
14
|
+
const REQUIRED_PERMISSIONS = Platform.OS === 'android' ? [
|
|
15
|
+
PermissionsAndroid.PERMISSIONS.READ_PHONE_NUMBERS,
|
|
16
|
+
PermissionsAndroid.PERMISSIONS.CALL_PHONE,
|
|
17
|
+
] : [];
|
|
18
|
+
|
|
19
|
+
export async function ensureAndroidPermissions() {
|
|
20
|
+
if (Platform.OS !== 'android') return true;
|
|
21
|
+
const result = await PermissionsAndroid.requestMultiple(REQUIRED_PERMISSIONS);
|
|
22
|
+
const deniedPermissions = Object.entries(result)
|
|
23
|
+
.filter(([_, status]) => status !== PermissionsAndroid.RESULTS.GRANTED)
|
|
24
|
+
.map(([perm, _]) => perm);
|
|
25
|
+
|
|
26
|
+
if (deniedPermissions.length > 0) {
|
|
27
|
+
Alert.alert(
|
|
28
|
+
'Permissions Required',
|
|
29
|
+
`The following permissions are still missing: \n${deniedPermissions.join('\n')}`,
|
|
30
|
+
[{ text: 'Open Settings', onPress: () => Linking.openSettings() }]
|
|
31
|
+
);
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export const CallHandler = {
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* @param {string} uuid
|
|
41
|
+
* @param {string} number
|
|
42
|
+
* @param {string} name
|
|
43
|
+
* @param {boolean} hasVideo
|
|
44
|
+
* @param {boolean} shouldRing - NEW: Controls native ringtone/vibration
|
|
45
|
+
*/
|
|
46
|
+
displayCall: async (uuid, number, name, hasVideo = false, shouldRing = true) => {
|
|
47
|
+
if (!CallModule) {
|
|
48
|
+
console.log("CallModule is null. Check native linking.");
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// 1. Android Specific Checks
|
|
53
|
+
if (Platform.OS === 'android') {
|
|
54
|
+
const hasPerms = await ensureAndroidPermissions();
|
|
55
|
+
if (!hasPerms) return false;
|
|
56
|
+
|
|
57
|
+
const isAccountEnabled = await CallModule.checkTelecomPermissions();
|
|
58
|
+
if (!isAccountEnabled) return false;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const cleanUuid = uuid.toLowerCase().trim();
|
|
62
|
+
|
|
63
|
+
// 2. Cross-platform execution
|
|
64
|
+
try {
|
|
65
|
+
// We now pass 5 arguments to the native side
|
|
66
|
+
// uuid, number, name, hasVideo, shouldRing
|
|
67
|
+
const success = await CallModule.displayIncomingCall(
|
|
68
|
+
cleanUuid,
|
|
69
|
+
number,
|
|
70
|
+
name,
|
|
71
|
+
hasVideo,
|
|
72
|
+
shouldRing
|
|
73
|
+
);
|
|
74
|
+
return success;
|
|
75
|
+
} catch (e) {
|
|
76
|
+
console.log("Native Call Error:", e);
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
},
|
|
80
|
+
|
|
81
|
+
destroyNativeCallUI: (uuid) => {
|
|
82
|
+
if (CallModule && CallModule.endNativeCall) {
|
|
83
|
+
CallModule.endNativeCall(uuid.toLowerCase());
|
|
84
|
+
}
|
|
85
|
+
},
|
|
86
|
+
|
|
87
|
+
subscribe: (onAccept, onReject, onFailed) => {
|
|
88
|
+
if (!callEventEmitter) {
|
|
89
|
+
return () => { };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const acceptSub = callEventEmitter.addListener('onCallAccepted', (data) => onAccept(data));
|
|
93
|
+
const rejectSub = callEventEmitter.addListener('onCallRejected', (data) => onReject(data));
|
|
94
|
+
const failSub = onFailed ? callEventEmitter.addListener('onCallFailed', (data) => onFailed(data)) : null;
|
|
95
|
+
|
|
96
|
+
return () => {
|
|
97
|
+
acceptSub.remove();
|
|
98
|
+
rejectSub.remove();
|
|
99
|
+
if (failSub) failSub.remove();
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
};
|
package/ios/CallModule.h
ADDED
package/ios/CallModule.m
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
#import "CallModule.h"
|
|
2
|
+
|
|
3
|
+
@implementation CallModule
|
|
4
|
+
|
|
5
|
+
RCT_EXPORT_MODULE();
|
|
6
|
+
|
|
7
|
+
- (NSArray<NSString *> *)supportedEvents {
|
|
8
|
+
return @[ @"onCallAccepted", @"onCallRejected", @"onCallFailed" ];
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
- (instancetype)init {
|
|
12
|
+
self = [super init];
|
|
13
|
+
if (self) {
|
|
14
|
+
#pragma clang diagnostic push
|
|
15
|
+
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
|
|
16
|
+
CXProviderConfiguration *config = [[CXProviderConfiguration alloc] initWithLocalizedName:@"Raiidr"];
|
|
17
|
+
#pragma clang diagnostic pop
|
|
18
|
+
|
|
19
|
+
config.supportsVideo = YES;
|
|
20
|
+
config.maximumCallGroups = 1;
|
|
21
|
+
config.maximumCallsPerCallGroup = 1;
|
|
22
|
+
config.supportedHandleTypes = [NSSet setWithObject:@(CXHandleTypeGeneric)];
|
|
23
|
+
config.includesCallsInRecents = NO; // <-- prevent log
|
|
24
|
+
|
|
25
|
+
// If you had a custom file like "Ringtone.aif" in your project,
|
|
26
|
+
// you would set it here:
|
|
27
|
+
// config.ringtoneSound = @"Ringtone.aif";
|
|
28
|
+
|
|
29
|
+
self.provider = [[CXProvider alloc] initWithConfiguration:config];
|
|
30
|
+
[self.provider setDelegate:self queue:nil];
|
|
31
|
+
}
|
|
32
|
+
return self;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Updated with showRing (BOOL)
|
|
36
|
+
RCT_EXPORT_METHOD(displayIncomingCall:(NSString *)uuidString
|
|
37
|
+
number:(NSString *)number
|
|
38
|
+
name:(NSString *)name
|
|
39
|
+
hasVideo:(BOOL)hasVideo
|
|
40
|
+
showRing:(BOOL)showRing
|
|
41
|
+
resolve:(RCTPromiseResolveBlock)resolve
|
|
42
|
+
reject:(RCTPromiseRejectBlock)reject)
|
|
43
|
+
{
|
|
44
|
+
NSUUID *uuid = [[NSUUID alloc] initWithUUIDString:uuidString];
|
|
45
|
+
if (!uuid) {
|
|
46
|
+
reject(@"INVALID_UUID", @"The provided UUID string is invalid", nil);
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
CXCallUpdate *update = [[CXCallUpdate alloc] init];
|
|
51
|
+
update.remoteHandle = [[CXHandle alloc] initWithType:CXHandleTypeGeneric value:name];
|
|
52
|
+
update.hasVideo = hasVideo;
|
|
53
|
+
|
|
54
|
+
// Note: CallKit handles ringing based on the CXProviderConfiguration.
|
|
55
|
+
// iOS doesn't allow toggling 'ring' per-call via reportNewIncomingCall.
|
|
56
|
+
// However, including it here ensures your JS bridge call matches.
|
|
57
|
+
|
|
58
|
+
[self.provider reportNewIncomingCallWithUUID:uuid
|
|
59
|
+
update:update
|
|
60
|
+
completion:^(NSError *_Nullable error) {
|
|
61
|
+
if (error) {
|
|
62
|
+
reject(@"CALL_ERROR", error.localizedDescription, error);
|
|
63
|
+
} else {
|
|
64
|
+
resolve(@YES);
|
|
65
|
+
}
|
|
66
|
+
}];
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
RCT_EXPORT_METHOD(endNativeCall:(NSString *)uuidString) {
|
|
70
|
+
NSUUID *uuid = [[NSUUID alloc] initWithUUIDString:uuidString];
|
|
71
|
+
if (uuid) {
|
|
72
|
+
[self.provider reportCallWithUUID:uuid
|
|
73
|
+
endedAtDate:[NSDate date]
|
|
74
|
+
reason:CXCallEndedReasonRemoteEnded];
|
|
75
|
+
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// MARK: - CXProviderDelegate
|
|
80
|
+
|
|
81
|
+
- (void)provider:(CXProvider *)provider performAnswerCallAction:(CXAnswerCallAction *)action {
|
|
82
|
+
[action fulfill];
|
|
83
|
+
[self sendEventWithName:@"onCallAccepted"
|
|
84
|
+
body:@{@"callUUID" : [action.callUUID.UUIDString lowercaseString]}];
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
- (void)provider:(CXProvider *)provider performEndCallAction:(CXEndCallAction *)action {
|
|
88
|
+
[action fulfill];
|
|
89
|
+
[self sendEventWithName:@"onCallRejected"
|
|
90
|
+
body:@{@"callUUID" : [action.callUUID.UUIDString lowercaseString]}];
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
- (void)providerDidReset:(CXProvider *)provider { }
|
|
94
|
+
|
|
95
|
+
@end
|
package/package.json
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "rns-nativecall",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Raiidr nativecall component with native Android/iOS for handling native call ui, when app is not open or open.",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"types": "index.d.ts",
|
|
7
|
+
"react-native": "index.js",
|
|
8
|
+
"scripts": {
|
|
9
|
+
"p": "npm publish --access public"
|
|
10
|
+
},
|
|
11
|
+
"expo": {
|
|
12
|
+
"plugins": [
|
|
13
|
+
"./app.plugin.js"
|
|
14
|
+
]
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"react-native",
|
|
18
|
+
"nativecall",
|
|
19
|
+
"call",
|
|
20
|
+
"native",
|
|
21
|
+
"call keep",
|
|
22
|
+
"android",
|
|
23
|
+
"expo",
|
|
24
|
+
"incognito",
|
|
25
|
+
"dialog suppression",
|
|
26
|
+
"keyboard stability",
|
|
27
|
+
"lifecycle events"
|
|
28
|
+
],
|
|
29
|
+
"files": [
|
|
30
|
+
"android",
|
|
31
|
+
"ios",
|
|
32
|
+
"app.plugin.js",
|
|
33
|
+
"index.d.ts",
|
|
34
|
+
"index.js",
|
|
35
|
+
"package.json",
|
|
36
|
+
"react-native.config",
|
|
37
|
+
"README.md",
|
|
38
|
+
"rns-nativecall.podspec",
|
|
39
|
+
"withRaiidrVoip"
|
|
40
|
+
],
|
|
41
|
+
"peerDependencies": {
|
|
42
|
+
"react-native": "*",
|
|
43
|
+
"expo": "*"
|
|
44
|
+
},
|
|
45
|
+
"dependencies": {
|
|
46
|
+
"@expo/config-plugins": "~10.1.1"
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
require "json"
|
|
2
|
+
|
|
3
|
+
package = JSON.parse(File.read(File.join(__dir__, "package.json")))
|
|
4
|
+
|
|
5
|
+
Pod::Spec.new do |s|
|
|
6
|
+
s.name = package["name"]
|
|
7
|
+
s.version = package["version"]
|
|
8
|
+
s.summary = package["description"] || "Custom native call for React Native"
|
|
9
|
+
s.license = package["license"] || "MIT"
|
|
10
|
+
s.author = package["author"] || "Unknown"
|
|
11
|
+
s.homepage = package["homepage"] || "https://github.com/your/repo"
|
|
12
|
+
s.source = { :git => "https://github.com/raiidr/rns-nativecall.git", :tag => "#{s.version}" }
|
|
13
|
+
|
|
14
|
+
s.platforms = { :ios => "11.0" }
|
|
15
|
+
s.source_files = "ios/**/*.{h,m}"
|
|
16
|
+
|
|
17
|
+
s.dependency "React-Core"
|
|
18
|
+
end
|