rns-nativecall 1.2.2 → 1.2.4
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 +7 -6
- package/android/src/main/java/com/rnsnativecall/CallMessagingService.kt +1 -58
- package/android/src/main/java/com/rnsnativecall/CallModule.kt +21 -0
- package/android/src/main/java/com/rnsnativecall/NativeCallManager.kt +132 -51
- package/index.d.ts +9 -0
- package/index.js +5 -0
- package/ios/CallModule.m +14 -64
- package/package.json +1 -1
- package/withNativeCallVoip.js +18 -2
- package/android/src/main/res/drawable/circle_background.xml +0 -4
- package/android/src/main/res/drawable/ic_call_answer_white.xml +0 -9
- package/android/src/main/res/drawable/ic_call_end_white.xml +0 -9
- package/android/src/main/res/layout/activity_incoming_call.xml +0 -139
package/README.md
CHANGED
|
@@ -133,16 +133,17 @@ export default function App() {
|
|
|
133
133
|
| Method | Platform | Description |
|
|
134
134
|
| :--- | :--- | :--- |
|
|
135
135
|
| **registerHeadlessTask(callback)** | All | Registers the background task. `eventType` is 'INCOMING_CALL', 'BUSY', or 'ABORTED_CALL'. |
|
|
136
|
-
| **displayCall(uuid, name, type)** | All |
|
|
137
|
-
| **checkCallValidity(uuid)** | All |
|
|
138
|
-
| **checkCallStatus(uuid)** | All |
|
|
136
|
+
| **displayCall(uuid, name, type)** | All | Launches full-screen Activity. iOS: Reports incoming call to CallKit. |
|
|
137
|
+
| **checkCallValidity(uuid)** | All | Returns {isValid, isCanceled} to prevent ghost/canceled calls. |
|
|
138
|
+
| **checkCallStatus(uuid)** | All | Returns {isCanceled, isActive, shouldDisplay} for UI syncing. |
|
|
139
|
+
| **showMissedCall(uuid, name, type)** | All | Shows miss call on the deivce notification tray|
|
|
140
|
+
| **destroyNativeCallUI(uuid)** | All | Stops ringtone/Activity. iOS: Ends CallKit (Handoff vs Hangup logic). |
|
|
141
|
+
| **getInitialCallData()** | All | Retrieves the call payload if the app was cold-started via a notification Answer. |
|
|
142
|
+
| **subscribe(onAccept, onReject, onFailed)** | All | Listens for Answer/Decline button presses and system-level bridge errors. |
|
|
139
143
|
| **checkOverlayPermission()** | Android | Android only: Returns true if app can draw over other apps while unlocked. |
|
|
140
144
|
| **checkFullScreenPermission()** | Android | Android 14+: Checks if app can trigger full-screen intents on lockscreen. |
|
|
141
145
|
| **requestOverlayPermission()** | Android | Android only: Navigates user to "Draw over other apps" system settings. |
|
|
142
146
|
| **requestFullScreenSettings()** | Android | Android 14+: Navigates user to "Full Screen Intent" system settings. |
|
|
143
|
-
| **destroyNativeCallUI(uuid)** | All | Android: Stops ringtone/Activity. iOS: Ends CallKit (Handoff vs Hangup logic). |
|
|
144
|
-
| **getInitialCallData()** | All | Retrieves the call payload if the app was cold-started via a notification Answer. |
|
|
145
|
-
| **subscribe(onAccept, onReject, onFailed)** | All | Listens for Answer/Decline button presses and system-level bridge errors. |
|
|
146
147
|
---
|
|
147
148
|
|
|
148
149
|
# Implementation Notes
|
|
@@ -32,7 +32,7 @@ class CallMessagingService : FirebaseMessagingService() {
|
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
NativeCallManager.dismissIncomingCall(context, uuid)
|
|
35
|
-
showMissedCallNotification(context, data
|
|
35
|
+
NativeCallManager.showMissedCallNotification(context, data)
|
|
36
36
|
return
|
|
37
37
|
}
|
|
38
38
|
|
|
@@ -73,63 +73,6 @@ class CallMessagingService : FirebaseMessagingService() {
|
|
|
73
73
|
}
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
-
private fun showMissedCallNotification(
|
|
77
|
-
context: Context,
|
|
78
|
-
data: Map<String, String>,
|
|
79
|
-
uuid: String,
|
|
80
|
-
) {
|
|
81
|
-
val name = data["name"] ?: "Unknown"
|
|
82
|
-
val callType = data["callType"] ?: "video"
|
|
83
|
-
val channelId = "missed_calls"
|
|
84
|
-
|
|
85
|
-
val notificationManager =
|
|
86
|
-
context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
|
87
|
-
|
|
88
|
-
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
89
|
-
val channel =
|
|
90
|
-
NotificationChannel(
|
|
91
|
-
channelId,
|
|
92
|
-
"Missed Calls",
|
|
93
|
-
NotificationManager.IMPORTANCE_DEFAULT,
|
|
94
|
-
).apply { description = "Missed call notifications" }
|
|
95
|
-
notificationManager.createNotificationChannel(channel)
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
val iconResId =
|
|
99
|
-
context.resources
|
|
100
|
-
.getIdentifier("ic_missed_call", "drawable", context.packageName)
|
|
101
|
-
.takeIf { it != 0 } ?: android.R.drawable.sym_call_missed
|
|
102
|
-
val appName = context.applicationInfo.loadLabel(context.packageManager).toString()
|
|
103
|
-
|
|
104
|
-
val launchIntent = context.packageManager.getLaunchIntentForPackage(context.packageName)
|
|
105
|
-
launchIntent?.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_SINGLE_TOP)
|
|
106
|
-
val pendingIntent =
|
|
107
|
-
PendingIntent.getActivity(
|
|
108
|
-
context,
|
|
109
|
-
uuid.hashCode(),
|
|
110
|
-
launchIntent,
|
|
111
|
-
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
|
112
|
-
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
|
|
113
|
-
} else {
|
|
114
|
-
PendingIntent.FLAG_UPDATE_CURRENT
|
|
115
|
-
},
|
|
116
|
-
)
|
|
117
|
-
|
|
118
|
-
val builder =
|
|
119
|
-
NotificationCompat
|
|
120
|
-
.Builder(context, channelId)
|
|
121
|
-
.setSmallIcon(iconResId)
|
|
122
|
-
.setContentTitle("$appName • Missed $callType call")
|
|
123
|
-
.setContentText("You missed a call from $name")
|
|
124
|
-
.setPriority(NotificationCompat.PRIORITY_HIGH)
|
|
125
|
-
.setAutoCancel(true)
|
|
126
|
-
.setCategory(NotificationCompat.CATEGORY_MISSED_CALL)
|
|
127
|
-
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
|
|
128
|
-
.setContentIntent(pendingIntent)
|
|
129
|
-
|
|
130
|
-
notificationManager.notify(uuid.hashCode(), builder.build())
|
|
131
|
-
}
|
|
132
|
-
|
|
133
76
|
private fun isAppInForeground(context: Context): Boolean {
|
|
134
77
|
val activityManager =
|
|
135
78
|
context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
|
|
@@ -162,6 +162,27 @@ class CallModule(
|
|
|
162
162
|
promise.resolve(map)
|
|
163
163
|
}
|
|
164
164
|
|
|
165
|
+
@ReactMethod
|
|
166
|
+
fun showMissedCall(
|
|
167
|
+
uuid: String,
|
|
168
|
+
name: String,
|
|
169
|
+
callType: String,
|
|
170
|
+
promise: Promise,
|
|
171
|
+
) {
|
|
172
|
+
try {
|
|
173
|
+
val data =
|
|
174
|
+
mapOf(
|
|
175
|
+
"callUuid" to uuid,
|
|
176
|
+
"name" to name,
|
|
177
|
+
"callType" to callType,
|
|
178
|
+
)
|
|
179
|
+
NativeCallManager.showMissedCallNotification(reactApplicationContext, data)
|
|
180
|
+
promise.resolve(true)
|
|
181
|
+
} catch (e: Exception) {
|
|
182
|
+
promise.reject("MISSED_CALL_ERROR", e.message)
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
165
186
|
@ReactMethod
|
|
166
187
|
fun endNativeCall(uuid: String) {
|
|
167
188
|
NativeCallManager.dismissIncomingCall(reactApplicationContext, uuid)
|
|
@@ -14,7 +14,9 @@ import androidx.core.app.Person
|
|
|
14
14
|
import androidx.core.content.ContextCompat
|
|
15
15
|
|
|
16
16
|
object NativeCallManager {
|
|
17
|
-
const val channelId = "
|
|
17
|
+
const val channelId = "CALL_CHANNEL_URGENT_V2"
|
|
18
|
+
private const val MISSED_CHANNEL_ID = "missed_calls"
|
|
19
|
+
|
|
18
20
|
private var currentCallData: Map<String, String>? = null
|
|
19
21
|
|
|
20
22
|
@JvmStatic internal var pendingCallNotification: Notification? = null
|
|
@@ -23,51 +25,73 @@ object NativeCallManager {
|
|
|
23
25
|
|
|
24
26
|
fun getCurrentCallData(): Map<String, String>? = currentCallData
|
|
25
27
|
|
|
28
|
+
// --- REUSABLE HELPERS ---
|
|
29
|
+
|
|
30
|
+
private fun getPendingIntentFlags(): Int =
|
|
31
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
|
32
|
+
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
|
|
33
|
+
} else {
|
|
34
|
+
PendingIntent.FLAG_UPDATE_CURRENT
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
private fun getResourceColor(context: Context): Int {
|
|
38
|
+
val colorId = context.resources.getIdentifier("notification_icon_color", "color", context.packageName)
|
|
39
|
+
return if (colorId != 0) ContextCompat.getColor(context, colorId) else Color.parseColor("#ffffff")
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
private fun createCallIntent(
|
|
43
|
+
context: Context,
|
|
44
|
+
targetClass: Class<*>,
|
|
45
|
+
actionName: String?,
|
|
46
|
+
uuid: String,
|
|
47
|
+
data: Map<String, String>,
|
|
48
|
+
): Intent =
|
|
49
|
+
Intent(context, targetClass).apply {
|
|
50
|
+
action = actionName
|
|
51
|
+
putExtra("EXTRA_CALL_UUID", uuid)
|
|
52
|
+
data.forEach { (k, v) -> putExtra(k, v) }
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
private fun getNotificationManager(context: Context) = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
|
56
|
+
|
|
57
|
+
// --- MAIN LOGIC ---
|
|
58
|
+
|
|
26
59
|
fun handleIncomingPush(
|
|
27
60
|
context: Context,
|
|
28
61
|
data: Map<String, String>,
|
|
29
62
|
) {
|
|
30
63
|
Handler(Looper.getMainLooper()).post {
|
|
31
|
-
val uuid = data["callUuid"] ?: return@post
|
|
32
64
|
this.currentCallData = data
|
|
33
|
-
val
|
|
65
|
+
val uuid = data["callUuid"] ?: return@post
|
|
66
|
+
val name = data["name"] ?: "Someone"
|
|
67
|
+
val callType = data["callType"] ?: "audio"
|
|
34
68
|
val notificationId = uuid.hashCode()
|
|
35
|
-
val
|
|
36
|
-
|
|
37
|
-
val pendingFlags =
|
|
38
|
-
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
|
39
|
-
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
|
|
40
|
-
} else {
|
|
41
|
-
PendingIntent.FLAG_UPDATE_CURRENT
|
|
42
|
-
}
|
|
69
|
+
val flags = getPendingIntentFlags()
|
|
43
70
|
|
|
44
|
-
// Intents
|
|
71
|
+
// 1. Setup Intents using helper
|
|
45
72
|
val overlayIntent =
|
|
46
|
-
|
|
47
|
-
putExtra("EXTRA_CALL_UUID", uuid)
|
|
48
|
-
data.forEach { (k, v) -> putExtra(k, v) }
|
|
73
|
+
createCallIntent(context, NotificationOverlayActivity::class.java, null, uuid, data).apply {
|
|
49
74
|
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_NO_USER_ACTION)
|
|
50
75
|
}
|
|
51
|
-
val
|
|
52
|
-
|
|
53
|
-
val
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
val
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
// Channel
|
|
70
|
-
val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
|
76
|
+
val fullScreenPI = PendingIntent.getActivity(context, notificationId, overlayIntent, flags)
|
|
77
|
+
|
|
78
|
+
val answerPI =
|
|
79
|
+
PendingIntent.getBroadcast(
|
|
80
|
+
context,
|
|
81
|
+
notificationId + 1,
|
|
82
|
+
createCallIntent(context, CallActionReceiver::class.java, "ACTION_ANSWER", uuid, data),
|
|
83
|
+
flags,
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
val rejectPI =
|
|
87
|
+
PendingIntent.getBroadcast(
|
|
88
|
+
context,
|
|
89
|
+
notificationId + 2,
|
|
90
|
+
createCallIntent(context, CallActionReceiver::class.java, "ACTION_REJECT", uuid, data),
|
|
91
|
+
flags,
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
// 2. Setup Urgent Channel
|
|
71
95
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
72
96
|
val channel =
|
|
73
97
|
NotificationChannel(channelId, "Incoming Calls", NotificationManager.IMPORTANCE_HIGH).apply {
|
|
@@ -75,7 +99,7 @@ object NativeCallManager {
|
|
|
75
99
|
enableVibration(true)
|
|
76
100
|
setBypassDnd(true)
|
|
77
101
|
setSound(
|
|
78
|
-
|
|
102
|
+
RingtoneManager.getDefaultUri(RingtoneManager.TYPE_RINGTONE),
|
|
79
103
|
AudioAttributes
|
|
80
104
|
.Builder()
|
|
81
105
|
.setUsage(AudioAttributes.USAGE_NOTIFICATION_RINGTONE)
|
|
@@ -83,46 +107,104 @@ object NativeCallManager {
|
|
|
83
107
|
.build(),
|
|
84
108
|
)
|
|
85
109
|
}
|
|
86
|
-
|
|
110
|
+
getNotificationManager(context).createNotificationChannel(channel)
|
|
87
111
|
}
|
|
88
112
|
|
|
113
|
+
// 3. Resources & Style
|
|
114
|
+
val iconId =
|
|
115
|
+
context.resources
|
|
116
|
+
.getIdentifier("notification_icon", "drawable", context.packageName)
|
|
117
|
+
.let { if (it != 0) it else context.applicationInfo.icon }
|
|
118
|
+
|
|
89
119
|
val caller =
|
|
90
120
|
Person
|
|
91
121
|
.Builder()
|
|
92
122
|
.setName(name)
|
|
93
123
|
.setImportant(true)
|
|
94
124
|
.build()
|
|
125
|
+
val incomingCallStyle = NotificationCompat.CallStyle.forIncomingCall(caller, rejectPI, answerPI)
|
|
95
126
|
|
|
96
|
-
|
|
127
|
+
if (callType.equals("video", ignoreCase = true)) {
|
|
128
|
+
try {
|
|
129
|
+
incomingCallStyle.setIsVideo(true)
|
|
130
|
+
} catch (e: Exception) {
|
|
131
|
+
// No-op
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
// 4. Build Notification
|
|
135
|
+
val notification =
|
|
97
136
|
NotificationCompat
|
|
98
137
|
.Builder(context, channelId)
|
|
99
|
-
.setSmallIcon(
|
|
138
|
+
.setSmallIcon(iconId)
|
|
139
|
+
.setContentTitle("Incoming $callType Call from $name")
|
|
140
|
+
.setContentText("Incoming $callType Call")
|
|
141
|
+
.setSubText("Incoming $callType Call")
|
|
142
|
+
.setColor(getResourceColor(context))
|
|
100
143
|
.setPriority(NotificationCompat.PRIORITY_MAX)
|
|
101
144
|
.setCategory(NotificationCompat.CATEGORY_CALL)
|
|
102
145
|
.setOngoing(true)
|
|
103
|
-
.setFullScreenIntent(
|
|
104
|
-
.setStyle(
|
|
146
|
+
.setFullScreenIntent(fullScreenPI, true)
|
|
147
|
+
.setStyle(incomingCallStyle)
|
|
105
148
|
.setForegroundServiceBehavior(NotificationCompat.FOREGROUND_SERVICE_IMMEDIATE)
|
|
149
|
+
.build()
|
|
106
150
|
|
|
107
|
-
val notification = builder.build()
|
|
108
151
|
pendingCallNotification = notification
|
|
109
152
|
pendingNotificationId = notificationId
|
|
110
153
|
|
|
111
|
-
//
|
|
112
|
-
|
|
113
|
-
ContextCompat.startForegroundService(context,
|
|
114
|
-
|
|
115
|
-
// 2. Stop the SILENT Wake service (which no longer needs DATA_SYNC)
|
|
154
|
+
// 5. Execution
|
|
155
|
+
getNotificationManager(context).notify(notificationId, notification)
|
|
156
|
+
ContextCompat.startForegroundService(context, Intent(context, CallUiForegroundService::class.java))
|
|
116
157
|
CallForegroundService.stop(context)
|
|
117
158
|
}
|
|
118
159
|
}
|
|
119
160
|
|
|
161
|
+
fun showMissedCallNotification(
|
|
162
|
+
context: Context,
|
|
163
|
+
data: Map<String, String>,
|
|
164
|
+
) {
|
|
165
|
+
val uuid = data["callUuid"] ?: return
|
|
166
|
+
val name = data["name"] ?: "Unknown"
|
|
167
|
+
val callType = data["callType"] ?: "video"
|
|
168
|
+
|
|
169
|
+
// Setup Missed Channel
|
|
170
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
171
|
+
val channel = NotificationChannel(MISSED_CHANNEL_ID, "Missed Calls", NotificationManager.IMPORTANCE_DEFAULT)
|
|
172
|
+
getNotificationManager(context).createNotificationChannel(channel)
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
val iconResId =
|
|
176
|
+
context.resources
|
|
177
|
+
.getIdentifier("ic_missed_call", "drawable", context.packageName)
|
|
178
|
+
.let { if (it != 0) it else android.R.drawable.sym_call_missed }
|
|
179
|
+
|
|
180
|
+
val launchIntent =
|
|
181
|
+
context.packageManager.getLaunchIntentForPackage(context.packageName)?.apply {
|
|
182
|
+
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_SINGLE_TOP)
|
|
183
|
+
}
|
|
184
|
+
val pendingIntent = PendingIntent.getActivity(context, uuid.hashCode(), launchIntent, getPendingIntentFlags())
|
|
185
|
+
|
|
186
|
+
val notification =
|
|
187
|
+
NotificationCompat
|
|
188
|
+
.Builder(context, MISSED_CHANNEL_ID)
|
|
189
|
+
.setSmallIcon(iconResId)
|
|
190
|
+
.setContentTitle(context.applicationInfo.loadLabel(context.packageManager).toString())
|
|
191
|
+
.setContentText("You missed a $callType call from $name")
|
|
192
|
+
.setPriority(NotificationCompat.PRIORITY_HIGH)
|
|
193
|
+
.setColor(getResourceColor(context))
|
|
194
|
+
.setAutoCancel(true)
|
|
195
|
+
.setCategory(NotificationCompat.CATEGORY_MISSED_CALL)
|
|
196
|
+
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
|
|
197
|
+
.setContentIntent(pendingIntent)
|
|
198
|
+
.build()
|
|
199
|
+
|
|
200
|
+
getNotificationManager(context).notify(uuid.hashCode() + 10000, notification)
|
|
201
|
+
}
|
|
202
|
+
|
|
120
203
|
fun refreshNotificationOnly(
|
|
121
204
|
context: Context,
|
|
122
205
|
uuid: String,
|
|
123
206
|
) {
|
|
124
|
-
|
|
125
|
-
manager.cancel(uuid.hashCode())
|
|
207
|
+
getNotificationManager(context).cancel(uuid.hashCode())
|
|
126
208
|
}
|
|
127
209
|
|
|
128
210
|
fun dismissIncomingCall(
|
|
@@ -132,8 +214,7 @@ object NativeCallManager {
|
|
|
132
214
|
pendingCallNotification = null
|
|
133
215
|
pendingNotificationId = null
|
|
134
216
|
currentCallData = null
|
|
135
|
-
|
|
136
|
-
uuid?.let { manager.cancel(it.hashCode()) }
|
|
217
|
+
uuid?.let { getNotificationManager(context).cancel(it.hashCode()) }
|
|
137
218
|
context.stopService(Intent(context, CallUiForegroundService::class.java))
|
|
138
219
|
}
|
|
139
220
|
}
|
package/index.d.ts
CHANGED
|
@@ -62,6 +62,15 @@ export interface CallHandlerType {
|
|
|
62
62
|
callType?: 'audio' | 'video',
|
|
63
63
|
): Promise<boolean>;
|
|
64
64
|
|
|
65
|
+
/**
|
|
66
|
+
* Show showMissedCall on the device tray.
|
|
67
|
+
*/
|
|
68
|
+
showMissedCall(
|
|
69
|
+
uuid: string,
|
|
70
|
+
name: string,
|
|
71
|
+
callType?: 'audio' | 'video',
|
|
72
|
+
): Promise<boolean>;
|
|
73
|
+
|
|
65
74
|
/** * Checks if a call is still valid and has not been canceled.
|
|
66
75
|
*/
|
|
67
76
|
checkCallValidity(uuid: string): Promise<ValidityStatus>;
|
package/index.js
CHANGED
|
@@ -78,6 +78,11 @@ export const CallHandler = {
|
|
|
78
78
|
return await CallModule.checkCallStatus(uuid.toLowerCase().trim());
|
|
79
79
|
},
|
|
80
80
|
|
|
81
|
+
showMissedCall: async (uuid, name, callType) => {
|
|
82
|
+
if (!CallModule?.showMissedCall) return false;
|
|
83
|
+
return await CallModule.showMissedCall(uuid.toLowerCase().trim(), name, callType);
|
|
84
|
+
},
|
|
85
|
+
|
|
81
86
|
//--------------------------------------------------------------------------------------
|
|
82
87
|
|
|
83
88
|
requestOverlayPermission: async () => {
|
package/ios/CallModule.m
CHANGED
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
@interface CallModule ()
|
|
5
5
|
@property (nonatomic, strong) NSString *pendingCallUuid;
|
|
6
6
|
@property (nonatomic, strong) NSMutableDictionary *pendingEvents;
|
|
7
|
-
@property (nonatomic, assign) BOOL isCallActive;
|
|
7
|
+
@property (nonatomic, assign) BOOL isCallActive;
|
|
8
|
+
@property (nonatomic, strong) CXCallObserver *callObserver;
|
|
8
9
|
@end
|
|
9
10
|
|
|
10
11
|
@implementation CallModule
|
|
@@ -21,9 +22,10 @@ RCT_EXPORT_MODULE();
|
|
|
21
22
|
self = [super init];
|
|
22
23
|
if (self) {
|
|
23
24
|
self.pendingEvents = [NSMutableDictionary new];
|
|
25
|
+
self.callObserver = [[CXCallObserver alloc] init];
|
|
26
|
+
|
|
24
27
|
NSString *appName = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleDisplayName"] ?: [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleName"];
|
|
25
28
|
|
|
26
|
-
// Corrected initialization for modern iOS
|
|
27
29
|
CXProviderConfiguration *config = [[CXProviderConfiguration alloc] initWithLocalizedName:appName];
|
|
28
30
|
config.supportsVideo = YES;
|
|
29
31
|
config.maximumCallGroups = 1;
|
|
@@ -38,65 +40,47 @@ RCT_EXPORT_MODULE();
|
|
|
38
40
|
return self;
|
|
39
41
|
}
|
|
40
42
|
|
|
41
|
-
/**
|
|
42
|
-
* Modern helper to find the active UIWindow without using deprecated .keyWindow or .windows
|
|
43
|
-
*/
|
|
44
43
|
- (UIWindow *)getActiveWindow {
|
|
45
44
|
if (@available(iOS 13.0, *)) {
|
|
46
45
|
NSSet<UIScene *> *scenes = [[UIApplication sharedApplication] connectedScenes];
|
|
47
46
|
for (UIScene *scene in scenes) {
|
|
48
|
-
// We look for a foreground active window scene
|
|
49
47
|
if (scene.activationState == UISceneActivationStateForegroundActive &&
|
|
50
48
|
[scene isKindOfClass:[UIWindowScene class]]) {
|
|
51
49
|
UIWindowScene *windowScene = (UIWindowScene *)scene;
|
|
52
|
-
|
|
53
|
-
// On iOS 15+, we use windowScene.keyWindow if available,
|
|
54
|
-
// otherwise iterate the windows array of that scene
|
|
55
50
|
if (@available(iOS 15.0, *)) {
|
|
56
51
|
if (windowScene.keyWindow) return windowScene.keyWindow;
|
|
57
52
|
}
|
|
58
|
-
|
|
59
53
|
for (UIWindow *window in windowScene.windows) {
|
|
60
54
|
if (window.isKeyWindow) return window;
|
|
61
55
|
}
|
|
62
56
|
}
|
|
63
57
|
}
|
|
64
58
|
}
|
|
65
|
-
|
|
66
|
-
// Fallback for older devices or edge cases where scene is not yet active
|
|
67
59
|
#pragma clang diagnostic push
|
|
68
60
|
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
|
|
69
61
|
return [UIApplication sharedApplication].keyWindow;
|
|
70
62
|
#pragma clang diagnostic pop
|
|
71
63
|
}
|
|
72
64
|
|
|
73
|
-
// MARK: -
|
|
65
|
+
// MARK: - Exported Methods
|
|
74
66
|
|
|
75
67
|
RCT_EXPORT_METHOD(endNativeCall:(NSString *)uuidString) {
|
|
76
68
|
NSUUID *uuid = [[NSUUID alloc] initWithUUIDString:uuidString];
|
|
77
69
|
if (!uuid) return;
|
|
78
|
-
|
|
70
|
+
|
|
79
71
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
80
72
|
if (self.isCallActive) {
|
|
81
|
-
|
|
82
|
-
[self.provider reportCallWithUUID:uuid
|
|
83
|
-
endedAtDate:[NSDate date]
|
|
84
|
-
reason:CXCallEndedReasonRemoteEnded];
|
|
73
|
+
[self.provider reportCallWithUUID:uuid endedAtDate:[NSDate date] reason:CXCallEndedReasonRemoteEnded];
|
|
85
74
|
self.isCallActive = NO;
|
|
86
75
|
} else {
|
|
87
|
-
|
|
88
|
-
[self.provider reportCallWithUUID:uuid
|
|
89
|
-
endedAtDate:[NSDate date]
|
|
90
|
-
reason:CXCallEndedReasonAnsweredElsewhere];
|
|
76
|
+
[self.provider reportCallWithUUID:uuid endedAtDate:[NSDate date] reason:CXCallEndedReasonAnsweredElsewhere];
|
|
91
77
|
self.isCallActive = YES;
|
|
92
78
|
}
|
|
93
79
|
self.currentCallUUID = nil;
|
|
94
80
|
self.pendingCallUuid = nil;
|
|
95
|
-
});
|
|
81
|
+
}); // <--- FIXED: Added the missing ');' here
|
|
96
82
|
}
|
|
97
83
|
|
|
98
|
-
// MARK: - Core Logic
|
|
99
|
-
|
|
100
84
|
RCT_EXPORT_METHOD(displayIncomingCall:(NSString *)uuidString
|
|
101
85
|
name:(NSString *)name
|
|
102
86
|
callType:(NSString *)callType
|
|
@@ -128,15 +112,6 @@ RCT_EXPORT_METHOD(displayIncomingCall:(NSString *)uuidString
|
|
|
128
112
|
}];
|
|
129
113
|
}
|
|
130
114
|
|
|
131
|
-
RCT_EXPORT_METHOD(getInitialCallData:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject) {
|
|
132
|
-
NSMutableDictionary *result = [NSMutableDictionary new];
|
|
133
|
-
if (self.pendingCallUuid) {
|
|
134
|
-
result[@"default"] = @{@"callUuid": self.pendingCallUuid};
|
|
135
|
-
self.pendingCallUuid = nil;
|
|
136
|
-
}
|
|
137
|
-
resolve(result.count > 0 ? result : [NSNull null]);
|
|
138
|
-
}
|
|
139
|
-
|
|
140
115
|
RCT_EXPORT_METHOD(checkCallValidity:(NSString *)uuidString
|
|
141
116
|
resolve:(RCTPromiseResolveBlock)resolve
|
|
142
117
|
reject:(RCTPromiseRejectBlock)reject)
|
|
@@ -147,12 +122,9 @@ RCT_EXPORT_METHOD(checkCallValidity:(NSString *)uuidString
|
|
|
147
122
|
return;
|
|
148
123
|
}
|
|
149
124
|
|
|
150
|
-
// On iOS, we check if the call is currently known to the system observer
|
|
151
|
-
CXCallObserver *callObserver = [[CXCallObserver alloc] init];
|
|
152
125
|
BOOL found = NO;
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
if ([call.uuid.UUIDString isEqualToString:uuid.UUIDString]) {
|
|
126
|
+
for (CXCall *call in self.callObserver.calls) {
|
|
127
|
+
if ([call.UUID.UUIDString.lowercaseString isEqualToString:uuidString.lowercaseString]) {
|
|
156
128
|
if (!call.hasEnded) {
|
|
157
129
|
found = YES;
|
|
158
130
|
break;
|
|
@@ -160,8 +132,6 @@ RCT_EXPORT_METHOD(checkCallValidity:(NSString *)uuidString
|
|
|
160
132
|
}
|
|
161
133
|
}
|
|
162
134
|
|
|
163
|
-
// isValid: True if CallKit is currently tracking this call
|
|
164
|
-
// isCanceled: False if the call is active and valid
|
|
165
135
|
resolve(@{
|
|
166
136
|
@"isValid": @(found),
|
|
167
137
|
@"isCanceled": @(!found)
|
|
@@ -172,22 +142,11 @@ RCT_EXPORT_METHOD(checkCallStatus:(NSString *)uuidString
|
|
|
172
142
|
resolve:(RCTPromiseResolveBlock)resolve
|
|
173
143
|
reject:(RCTPromiseRejectBlock)reject)
|
|
174
144
|
{
|
|
175
|
-
NSUUID *uuid = [[NSUUID alloc] initWithUUIDString:uuidString];
|
|
176
|
-
if (!uuid) {
|
|
177
|
-
resolve(@{
|
|
178
|
-
@"isCanceled": @YES,
|
|
179
|
-
@"isActive": @NO,
|
|
180
|
-
@"shouldDisplay": @NO
|
|
181
|
-
});
|
|
182
|
-
return;
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
CXCallObserver *callObserver = [[CXCallObserver alloc] init];
|
|
186
145
|
BOOL isActive = NO;
|
|
187
146
|
BOOL isCanceled = YES;
|
|
188
147
|
|
|
189
|
-
for (CXCall *call in callObserver.calls) {
|
|
190
|
-
if ([call.
|
|
148
|
+
for (CXCall *call in self.callObserver.calls) {
|
|
149
|
+
if ([call.UUID.UUIDString.lowercaseString isEqualToString:uuidString.lowercaseString]) {
|
|
191
150
|
if (!call.hasEnded) {
|
|
192
151
|
isActive = YES;
|
|
193
152
|
isCanceled = NO;
|
|
@@ -206,22 +165,13 @@ RCT_EXPORT_METHOD(checkCallStatus:(NSString *)uuidString
|
|
|
206
165
|
// MARK: - CXProviderDelegate
|
|
207
166
|
|
|
208
167
|
- (void)provider:(CXProvider *)provider performAnswerCallAction:(CXAnswerCallAction *)action {
|
|
209
|
-
AVAudioSession *session = [AVAudioSession sharedInstance];
|
|
210
|
-
[session setCategory:AVAudioSessionCategoryPlayAndRecord mode:AVAudioSessionModeVoiceChat options:AVAudioSessionCategoryOptionAllowBluetoothHFP | AVAudioSessionCategoryOptionDefaultToSpeaker error:nil];
|
|
211
|
-
[session setActive:YES error:nil];
|
|
212
|
-
|
|
213
168
|
[action fulfill];
|
|
214
|
-
|
|
215
169
|
NSString *uuidStr = [action.callUUID.UUIDString lowercaseString];
|
|
216
170
|
self.pendingCallUuid = uuidStr;
|
|
217
171
|
|
|
218
172
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
219
|
-
// Use our non-deprecated helper to bring app to front
|
|
220
173
|
UIWindow *window = [self getActiveWindow];
|
|
221
|
-
if (window)
|
|
222
|
-
[window makeKeyAndVisible];
|
|
223
|
-
}
|
|
224
|
-
|
|
174
|
+
if (window) [window makeKeyAndVisible];
|
|
225
175
|
[self sendEventWithName:@"onCallAccepted" body:@{@"callUuid": uuidStr}];
|
|
226
176
|
});
|
|
227
177
|
}
|
package/package.json
CHANGED
package/withNativeCallVoip.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const { withAndroidManifest, withInfoPlist, withPlugins, withMainActivity, withAppDelegate } = require('@expo/config-plugins');
|
|
1
|
+
const { withAndroidStyles, AndroidConfig, withAndroidManifest, withInfoPlist, withPlugins, withMainActivity, withAppDelegate } = require('@expo/config-plugins');
|
|
2
2
|
|
|
3
3
|
function withMainActivityDataFix(config) {
|
|
4
4
|
return withMainActivity(config, (config) => {
|
|
@@ -69,6 +69,21 @@ function withMainActivityDataFix(config) {
|
|
|
69
69
|
return config;
|
|
70
70
|
});
|
|
71
71
|
}
|
|
72
|
+
function withNotificationColor(config) {
|
|
73
|
+
// 1. Find the color in app.json (fallback to #218aff)
|
|
74
|
+
const notificationPlugin = config.plugins?.find(p => Array.isArray(p) && p[0] === 'expo-notifications');
|
|
75
|
+
const iconColor = notificationPlugin?.[1]?.color || '#218aff';
|
|
76
|
+
|
|
77
|
+
return withAndroidStyles(config, (config) => {
|
|
78
|
+
config.modResults = AndroidConfig.Styles.assignStylesValue(config.modResults, {
|
|
79
|
+
name: 'notification_icon_color',
|
|
80
|
+
value: iconColor,
|
|
81
|
+
parent: 'AppTheme', // or your primary theme
|
|
82
|
+
type: 'color',
|
|
83
|
+
});
|
|
84
|
+
return config;
|
|
85
|
+
});
|
|
86
|
+
}
|
|
72
87
|
/** 2. ANDROID MANIFEST CONFIG **/
|
|
73
88
|
function withAndroidConfig(config) {
|
|
74
89
|
return withAndroidManifest(config, (config) => {
|
|
@@ -210,7 +225,8 @@ module.exports = (config, props) => {
|
|
|
210
225
|
return withPlugins(config, [
|
|
211
226
|
withAndroidConfig,
|
|
212
227
|
withMainActivityDataFix,
|
|
213
|
-
|
|
228
|
+
withNotificationColor,
|
|
229
|
+
withIosAppDelegateMod,
|
|
214
230
|
[withIosConfig, props]
|
|
215
231
|
]);
|
|
216
232
|
};
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
2
|
-
android:width="24dp"
|
|
3
|
-
android:height="24dp"
|
|
4
|
-
android:viewportWidth="24"
|
|
5
|
-
android:viewportHeight="24">
|
|
6
|
-
<path
|
|
7
|
-
android:fillColor="#FFFFFF"
|
|
8
|
-
android:pathData="M20,15.5c-1.25,0 -2.45,-0.2 -3.57,-0.57a1.02,1.02 0,0 0,-1.02 0.24l-2.2,2.2a15.05,15.05 0,0 1,-6.59 -6.59l2.2,-2.2a1.02,1.02 0,0 0,0.24 -1.02A11.36,11.36 0,0 1,8.5 4c0,-0.55 -0.45,-1 -1,-1H4c-0.55,0 -1,0.45 -1,1c0,9.39 7.61,17 17,17c0.55,0 1,-0.45 1,-1v-3.5c0,-0.55 -0.45,-1 -1,-1z" />
|
|
9
|
-
</vector>
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
2
|
-
android:width="24dp"
|
|
3
|
-
android:height="24dp"
|
|
4
|
-
android:viewportWidth="24"
|
|
5
|
-
android:viewportHeight="24">
|
|
6
|
-
<path
|
|
7
|
-
android:fillColor="#FFFFFF"
|
|
8
|
-
android:pathData="M12,9c-1.6,0 -3.15,0.25 -4.6,0.72v3.1c0,0.39 -0.23,0.74 -0.56,0.9 -0.98,0.49 -1.65,1.45 -1.65,2.58c0,1.65 1.34,3 3,3h2v-3H8.1c0.01,-0.99 0.87,-1.8 1.85,-1.8c0.51,0 1,-0.21 1.35,-0.56l1.8,-1.8C13.04,9.3 12.55,9 12,9z" />
|
|
9
|
-
</vector>
|
|
@@ -1,139 +0,0 @@
|
|
|
1
|
-
<?xml version="1.0" encoding="utf-8"?>
|
|
2
|
-
<androidx.constraintlayout.widget.ConstraintLayout
|
|
3
|
-
xmlns:android="http://schemas.android.com/apk/res/android"
|
|
4
|
-
xmlns:app="http://schemas.android.com/apk/res-auto"
|
|
5
|
-
xmlns:tools="http://schemas.android.com/tools"
|
|
6
|
-
android:layout_width="match_parent"
|
|
7
|
-
android:layout_height="match_parent"
|
|
8
|
-
android:background="#212121">
|
|
9
|
-
|
|
10
|
-
<!-- Profile Section -->
|
|
11
|
-
<ImageView
|
|
12
|
-
android:id="@+id/profileImage"
|
|
13
|
-
android:layout_width="200dp"
|
|
14
|
-
android:layout_height="200dp"
|
|
15
|
-
android:scaleType="centerCrop"
|
|
16
|
-
android:background="@drawable/circle_background"
|
|
17
|
-
app:layout_constraintTop_toTopOf="parent"
|
|
18
|
-
app:layout_constraintStart_toStartOf="parent"
|
|
19
|
-
app:layout_constraintEnd_toEndOf="parent"
|
|
20
|
-
app:layout_constraintBottom_toTopOf="@id/usernameText"
|
|
21
|
-
app:layout_constraintVertical_chainStyle="packed"
|
|
22
|
-
app:layout_constraintVertical_bias="0.35"
|
|
23
|
-
tools:src="@drawable/ic_profile_placeholder" />
|
|
24
|
-
|
|
25
|
-
<!-- Optional: Blur effect for discreet mode (apply programmatically) -->
|
|
26
|
-
<!-- You can use RenderScript or BlurView library for real blur -->
|
|
27
|
-
|
|
28
|
-
<TextView
|
|
29
|
-
android:id="@+id/usernameText"
|
|
30
|
-
android:layout_width="wrap_content"
|
|
31
|
-
android:layout_height="wrap_content"
|
|
32
|
-
android:text="John Doe"
|
|
33
|
-
android:textColor="#FFFFFF"
|
|
34
|
-
android:textSize="32sp"
|
|
35
|
-
android:fontFamily="sans-serif-medium"
|
|
36
|
-
android:layout_marginTop="20dp"
|
|
37
|
-
app:layout_constraintTop_toBottomOf="@id/profileImage"
|
|
38
|
-
app:layout_constraintStart_toStartOf="parent"
|
|
39
|
-
app:layout_constraintEnd_toEndOf="parent"
|
|
40
|
-
tools:text="John Doe" />
|
|
41
|
-
|
|
42
|
-
<TextView
|
|
43
|
-
android:id="@+id/callStatusText"
|
|
44
|
-
android:layout_width="wrap_content"
|
|
45
|
-
android:layout_height="wrap_content"
|
|
46
|
-
android:text="Incoming Video Call..."
|
|
47
|
-
android:textColor="#FFFFFF"
|
|
48
|
-
android:textSize="18sp"
|
|
49
|
-
android:alpha="0.8"
|
|
50
|
-
android:layout_marginTop="10dp"
|
|
51
|
-
app:layout_constraintTop_toBottomOf="@id/usernameText"
|
|
52
|
-
app:layout_constraintStart_toStartOf="parent"
|
|
53
|
-
app:layout_constraintEnd_toEndOf="parent" />
|
|
54
|
-
|
|
55
|
-
<!-- Action Buttons Container -->
|
|
56
|
-
<LinearLayout
|
|
57
|
-
android:id="@+id/buttonContainer"
|
|
58
|
-
android:layout_width="match_parent"
|
|
59
|
-
android:layout_height="wrap_content"
|
|
60
|
-
android:orientation="horizontal"
|
|
61
|
-
android:gravity="center"
|
|
62
|
-
android:paddingBottom="100dp"
|
|
63
|
-
app:layout_constraintBottom_toBottomOf="parent"
|
|
64
|
-
app:layout_constraintStart_toStartOf="parent"
|
|
65
|
-
app:layout_constraintEnd_toEndOf="parent">
|
|
66
|
-
|
|
67
|
-
<!-- Decline Button -->
|
|
68
|
-
<LinearLayout
|
|
69
|
-
android:layout_width="wrap_content"
|
|
70
|
-
android:layout_height="wrap_content"
|
|
71
|
-
android:orientation="vertical"
|
|
72
|
-
android:gravity="center"
|
|
73
|
-
android:layout_marginEnd="60dp">
|
|
74
|
-
|
|
75
|
-
<androidx.cardview.widget.CardView
|
|
76
|
-
android:layout_width="70dp"
|
|
77
|
-
android:layout_height="70dp"
|
|
78
|
-
app:cardCornerRadius="35dp"
|
|
79
|
-
app:cardElevation="8dp"
|
|
80
|
-
app:cardBackgroundColor="#FF3B30">
|
|
81
|
-
|
|
82
|
-
<ImageView
|
|
83
|
-
android:layout_width="match_parent"
|
|
84
|
-
android:layout_height="match_parent"
|
|
85
|
-
android:src="@drawable/ic_call_end_white"
|
|
86
|
-
android:padding="18dp"
|
|
87
|
-
android:tint="#FFFFFF" />
|
|
88
|
-
|
|
89
|
-
</androidx.cardview.widget.CardView>
|
|
90
|
-
|
|
91
|
-
<TextView
|
|
92
|
-
android:layout_width="wrap_content"
|
|
93
|
-
android:layout_height="wrap_content"
|
|
94
|
-
android:text="Decline"
|
|
95
|
-
android:textColor="#FFFFFF"
|
|
96
|
-
android:textSize="16sp"
|
|
97
|
-
android:fontFamily="sans-serif-medium"
|
|
98
|
-
android:layout_marginTop="10dp" />
|
|
99
|
-
|
|
100
|
-
</LinearLayout>
|
|
101
|
-
|
|
102
|
-
<!-- Accept Button -->
|
|
103
|
-
<LinearLayout
|
|
104
|
-
android:layout_width="wrap_content"
|
|
105
|
-
android:layout_height="wrap_content"
|
|
106
|
-
android:orientation="vertical"
|
|
107
|
-
android:gravity="center"
|
|
108
|
-
android:layout_marginStart="60dp">
|
|
109
|
-
|
|
110
|
-
<androidx.cardview.widget.CardView
|
|
111
|
-
android:layout_width="70dp"
|
|
112
|
-
android:layout_height="70dp"
|
|
113
|
-
app:cardCornerRadius="35dp"
|
|
114
|
-
app:cardElevation="8dp"
|
|
115
|
-
app:cardBackgroundColor="#4CD964">
|
|
116
|
-
|
|
117
|
-
<ImageView
|
|
118
|
-
android:layout_width="match_parent"
|
|
119
|
-
android:layout_height="match_parent"
|
|
120
|
-
android:src="@drawable/ic_call_answer_white"
|
|
121
|
-
android:padding="18dp"
|
|
122
|
-
android:tint="#FFFFFF" />
|
|
123
|
-
|
|
124
|
-
</androidx.cardview.widget.CardView>
|
|
125
|
-
|
|
126
|
-
<TextView
|
|
127
|
-
android:layout_width="wrap_content"
|
|
128
|
-
android:layout_height="wrap_content"
|
|
129
|
-
android:text="Accept"
|
|
130
|
-
android:textColor="#FFFFFF"
|
|
131
|
-
android:textSize="16sp"
|
|
132
|
-
android:fontFamily="sans-serif-medium"
|
|
133
|
-
android:layout_marginTop="10dp" />
|
|
134
|
-
|
|
135
|
-
</LinearLayout>
|
|
136
|
-
|
|
137
|
-
</LinearLayout>
|
|
138
|
-
|
|
139
|
-
</androidx.constraintlayout.widget.ConstraintLayout>
|