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 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 | Android: Launches full-screen Activity. iOS: Reports incoming call to CallKit. |
137
- | **checkCallValidity(uuid)** | All | Android only: Returns {isValid, isCanceled} to prevent ghost/canceled calls. |
138
- | **checkCallStatus(uuid)** | All | Android only: Returns {isCanceled, isActive, shouldDisplay} for UI syncing. |
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, uuid)
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 = "CALL_CHANNEL_V15_URGENT"
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 name = data["name"] ?: "Incoming Call"
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 ringtoneUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_RINGTONE)
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
- Intent(context, NotificationOverlayActivity::class.java).apply {
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 fullScreenPendingIntent = PendingIntent.getActivity(context, notificationId, overlayIntent, pendingFlags)
52
-
53
- val answerIntent =
54
- Intent(context, CallActionReceiver::class.java).apply {
55
- action = "ACTION_ANSWER"
56
- putExtra("EXTRA_CALL_UUID", uuid)
57
- data.forEach { (k, v) -> putExtra(k, v) }
58
- }
59
- val answerPendingIntent = PendingIntent.getBroadcast(context, notificationId + 1, answerIntent, pendingFlags)
60
-
61
- val rejectIntent =
62
- Intent(context, CallActionReceiver::class.java).apply {
63
- action = "ACTION_REJECT"
64
- putExtra("EXTRA_CALL_UUID", uuid)
65
- data.forEach { (k, v) -> putExtra(k, v) }
66
- }
67
- val rejectPendingIntent = PendingIntent.getBroadcast(context, notificationId + 2, rejectIntent, pendingFlags)
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
- ringtoneUri,
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
- notificationManager.createNotificationChannel(channel)
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
- val builder =
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(context.applicationInfo.icon)
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(fullScreenPendingIntent, true)
104
- .setStyle(NotificationCompat.CallStyle.forIncomingCall(caller, rejectPendingIntent, answerPendingIntent))
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
- // 1. Start the LOUD UI Service (Google allows this for PHONE_CALL)
112
- val uiIntent = Intent(context, CallUiForegroundService::class.java)
113
- ContextCompat.startForegroundService(context, uiIntent)
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
- val manager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
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
- val manager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
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; // Track state for handoff vs hangup
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: - Unified Export Method
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
- // REAL HANG UP
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
- // HANDOFF (Hide the CallKit banner/UI)
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
- for (CXCall *call in callObserver.calls) {
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.uuid.UUIDString isEqualToString:uuid.UUIDString]) {
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rns-nativecall",
3
- "version": "1.2.2",
3
+ "version": "1.2.4",
4
4
  "description": "High-performance React Native module for handling native VoIP call UI on Android and iOS.",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",
@@ -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
- withIosAppDelegateMod, // <--- ADDED THIS HERE
228
+ withNotificationColor,
229
+ withIosAppDelegateMod,
214
230
  [withIosConfig, props]
215
231
  ]);
216
232
  };
@@ -1,4 +0,0 @@
1
- <shape xmlns:android="http://schemas.android.com/apk/res/android"
2
- android:shape="oval">
3
- <solid android:color="#333333" />
4
- </shape>
@@ -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>