rns-nativecall 1.2.2 → 1.2.3
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/CallModule.kt +21 -0
- package/android/src/main/java/com/rnsnativecall/NativeCallManager.kt +114 -5
- 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/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
|
|
@@ -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,7 @@ 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_V0_URGENT"
|
|
18
18
|
private var currentCallData: Map<String, String>? = null
|
|
19
19
|
|
|
20
20
|
@JvmStatic internal var pendingCallNotification: Notification? = null
|
|
@@ -28,10 +28,13 @@ object NativeCallManager {
|
|
|
28
28
|
data: Map<String, String>,
|
|
29
29
|
) {
|
|
30
30
|
Handler(Looper.getMainLooper()).post {
|
|
31
|
-
val uuid = data["callUuid"] ?: return@post
|
|
32
31
|
this.currentCallData = data
|
|
33
|
-
val
|
|
32
|
+
val uuid = data["callUuid"] ?: return@post
|
|
33
|
+
val name = data["name"] ?: "Someone"
|
|
34
|
+
val callType = data["callType"] ?: "audio"
|
|
35
|
+
val isVideo = callType.equals("video", ignoreCase = true)
|
|
34
36
|
val notificationId = uuid.hashCode()
|
|
37
|
+
|
|
35
38
|
val ringtoneUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_RINGTONE)
|
|
36
39
|
|
|
37
40
|
val pendingFlags =
|
|
@@ -86,6 +89,29 @@ object NativeCallManager {
|
|
|
86
89
|
notificationManager.createNotificationChannel(channel)
|
|
87
90
|
}
|
|
88
91
|
|
|
92
|
+
val iconId =
|
|
93
|
+
context.resources
|
|
94
|
+
.getIdentifier("notification_icon", "drawable", context.packageName)
|
|
95
|
+
.let { id ->
|
|
96
|
+
if (id != 0) {
|
|
97
|
+
id // Found the one you saw in the folder!
|
|
98
|
+
} else {
|
|
99
|
+
// Fallback chain if notification_icon is missing
|
|
100
|
+
val shellId = context.resources.getIdentifier("shell_notification_icon", "drawable", context.packageName)
|
|
101
|
+
if (shellId != 0) shellId else context.applicationInfo.icon
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
val colorId = context.resources.getIdentifier("notification_icon_color", "color", context.packageName)
|
|
106
|
+
|
|
107
|
+
val iconColor =
|
|
108
|
+
if (colorId != 0) {
|
|
109
|
+
ContextCompat.getColor(context, colorId)
|
|
110
|
+
} else {
|
|
111
|
+
// Fallback to a default blue or grey if they didn't provide a color
|
|
112
|
+
Color.parseColor("#ffffff")
|
|
113
|
+
}
|
|
114
|
+
|
|
89
115
|
val caller =
|
|
90
116
|
Person
|
|
91
117
|
.Builder()
|
|
@@ -93,15 +119,38 @@ object NativeCallManager {
|
|
|
93
119
|
.setImportant(true)
|
|
94
120
|
.build()
|
|
95
121
|
|
|
122
|
+
val incomingCallTemplate =
|
|
123
|
+
NotificationCompat.CallStyle.forIncomingCall(
|
|
124
|
+
caller,
|
|
125
|
+
rejectPendingIntent,
|
|
126
|
+
answerPendingIntent,
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
if (isVideo) {
|
|
130
|
+
try {
|
|
131
|
+
incomingCallTemplate.setIsVideo(true)
|
|
132
|
+
} catch (e: Exception) {
|
|
133
|
+
// Some versions of Android may throw here; ignore
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
96
137
|
val builder =
|
|
97
138
|
NotificationCompat
|
|
98
139
|
.Builder(context, channelId)
|
|
99
|
-
.setSmallIcon(
|
|
140
|
+
.setSmallIcon(iconId)
|
|
141
|
+
// 1. FOR ANDROID 8/9: They prioritize ContentTitle over the Style's internal logic.
|
|
142
|
+
// We include the name here so it's impossible to miss.
|
|
143
|
+
.setContentTitle("Incoming $callType Call from $name")
|
|
144
|
+
// 2. FOR ALL VERSIONS: This helps fill the "Status" line below the name.
|
|
145
|
+
.setContentText("Incoming $callType Call")
|
|
146
|
+
// 3. FOR ANDROID 12+: This places the text in the header next to the App Name.
|
|
147
|
+
.setSubText("Incoming $callType Call")
|
|
148
|
+
.setColor(iconColor)
|
|
100
149
|
.setPriority(NotificationCompat.PRIORITY_MAX)
|
|
101
150
|
.setCategory(NotificationCompat.CATEGORY_CALL)
|
|
102
151
|
.setOngoing(true)
|
|
103
152
|
.setFullScreenIntent(fullScreenPendingIntent, true)
|
|
104
|
-
.setStyle(
|
|
153
|
+
.setStyle(incomingCallTemplate)
|
|
105
154
|
.setForegroundServiceBehavior(NotificationCompat.FOREGROUND_SERVICE_IMMEDIATE)
|
|
106
155
|
|
|
107
156
|
val notification = builder.build()
|
|
@@ -125,6 +174,66 @@ object NativeCallManager {
|
|
|
125
174
|
manager.cancel(uuid.hashCode())
|
|
126
175
|
}
|
|
127
176
|
|
|
177
|
+
fun showMissedCallNotification(
|
|
178
|
+
context: Context,
|
|
179
|
+
data: Map<String, String>,
|
|
180
|
+
) {
|
|
181
|
+
val uuid = data["callUuid"] ?: return
|
|
182
|
+
val name = data["name"] ?: "Unknown"
|
|
183
|
+
val callType = data["callType"] ?: "video"
|
|
184
|
+
val channelId = "missed_calls"
|
|
185
|
+
|
|
186
|
+
val notificationManager =
|
|
187
|
+
context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
|
188
|
+
|
|
189
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
190
|
+
val channel =
|
|
191
|
+
NotificationChannel(
|
|
192
|
+
channelId,
|
|
193
|
+
"Missed Calls",
|
|
194
|
+
NotificationManager.IMPORTANCE_DEFAULT,
|
|
195
|
+
).apply { description = "Missed call notifications" }
|
|
196
|
+
notificationManager.createNotificationChannel(channel)
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Use your custom notification icon if available, fallback to system missed call icon
|
|
200
|
+
val iconResId =
|
|
201
|
+
context.resources
|
|
202
|
+
.getIdentifier("notification_icon", "drawable", context.packageName)
|
|
203
|
+
.takeIf { it != 0 } ?: android.R.drawable.sym_call_missed
|
|
204
|
+
|
|
205
|
+
val appName = context.applicationInfo.loadLabel(context.packageManager).toString()
|
|
206
|
+
|
|
207
|
+
val launchIntent = context.packageManager.getLaunchIntentForPackage(context.packageName)
|
|
208
|
+
launchIntent?.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_SINGLE_TOP)
|
|
209
|
+
|
|
210
|
+
val pendingIntent =
|
|
211
|
+
PendingIntent.getActivity(
|
|
212
|
+
context,
|
|
213
|
+
uuid.hashCode(),
|
|
214
|
+
launchIntent,
|
|
215
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
|
216
|
+
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
|
|
217
|
+
} else {
|
|
218
|
+
PendingIntent.FLAG_UPDATE_CURRENT
|
|
219
|
+
},
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
val builder =
|
|
223
|
+
NotificationCompat
|
|
224
|
+
.Builder(context, channelId)
|
|
225
|
+
.setSmallIcon(iconResId)
|
|
226
|
+
.setContentTitle("Missed $callType call")
|
|
227
|
+
.setContentText("You missed a call from $name")
|
|
228
|
+
.setPriority(NotificationCompat.PRIORITY_HIGH)
|
|
229
|
+
.setAutoCancel(true)
|
|
230
|
+
.setCategory(NotificationCompat.CATEGORY_MISSED_CALL)
|
|
231
|
+
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
|
|
232
|
+
.setContentIntent(pendingIntent)
|
|
233
|
+
|
|
234
|
+
notificationManager.notify(uuid.hashCode(), builder.build())
|
|
235
|
+
}
|
|
236
|
+
|
|
128
237
|
fun dismissIncomingCall(
|
|
129
238
|
context: Context,
|
|
130
239
|
uuid: String?,
|
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
|
+
* Displays the full-screen Native Incoming Call UI.
|
|
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
|
};
|