rns-nativecall 0.7.9 → 0.8.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.
@@ -0,0 +1,26 @@
1
+ package com.rnsnativecall
2
+
3
+ import android.app.Activity
4
+ import android.os.Build
5
+ import android.os.Bundle
6
+ import android.view.WindowManager
7
+
8
+ class NotificationOverlayActivity : Activity() {
9
+ override fun onCreate(savedInstanceState: Bundle?) {
10
+ super.onCreate(savedInstanceState)
11
+
12
+ // These flags allow the notification pill to show over the lockscreen
13
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {
14
+ setShowWhenLocked(true)
15
+ setTurnScreenOn(true)
16
+ } else {
17
+ @Suppress("DEPRECATION")
18
+ window.addFlags(
19
+ WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED or
20
+ WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON or
21
+ WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
22
+ )
23
+ }
24
+ finish()
25
+ }
26
+ }
@@ -0,0 +1,22 @@
1
+ package com.rnsnativecall
2
+
3
+ import android.content.BroadcastReceiver
4
+ import android.content.Context
5
+ import android.content.Intent
6
+
7
+ class UnlockReceiver : BroadcastReceiver() {
8
+ override fun onReceive(context: Context, intent: Intent) {
9
+ android.util.Log.d("UnlockReceiver", "Device Unlocked! Action: ${intent.action}")
10
+
11
+ if (intent.action == Intent.ACTION_USER_PRESENT) {
12
+ val activeData = NativeCallManager.getCurrentCallData()
13
+
14
+ if (activeData != null) {
15
+ android.util.Log.d("UnlockReceiver", "Re-triggering call for: ${activeData["name"]}")
16
+ NativeCallManager.handleIncomingPush(context, activeData)
17
+ } else {
18
+ android.util.Log.d("UnlockReceiver", "No active call data found to re-trigger.")
19
+ }
20
+ }
21
+ }
22
+ }
@@ -0,0 +1,4 @@
1
+ <shape xmlns:android="http://schemas.android.com/apk/res/android"
2
+ android:shape="oval">
3
+ <solid android:color="#333333" />
4
+ </shape>
@@ -0,0 +1,9 @@
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>
@@ -0,0 +1,9 @@
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>
@@ -0,0 +1,139 @@
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>
package/index.d.ts CHANGED
@@ -78,6 +78,12 @@ export interface CallHandlerType {
78
78
  */
79
79
  checkCallStatus(uuid: string): Promise<CallStatus>;
80
80
 
81
+ requestOverlayPermission(): Promise<boolean>;
82
+
83
+ reportRemoteEnded(uuid: string, reason: number): void;
84
+
85
+ checkOverlayPermission(): Promise<boolean>;
86
+
81
87
  /**
82
88
  * Forcefully dismisses the native call UI, stops the ringtone, and clears state.
83
89
  * Use this when the call is hung up or timed out.
package/index.js CHANGED
@@ -47,6 +47,21 @@ export const CallHandler = {
47
47
  },
48
48
  // ------------------------------------
49
49
 
50
+ reportRemoteEnded: async (uuid, endReason) => {
51
+ if (!CallModule?.reportRemoteEnded) return;
52
+ await CallModule.reportRemoteEnded(uuid.toLowerCase().trim(), endReason);
53
+ },
54
+
55
+ requestOverlayPermission: async () => {
56
+ if (!CallModule?.requestOverlayPermission) return false;
57
+ return await CallModule.requestOverlayPermission();
58
+ },
59
+
60
+ checkOverlayPermission: async () => {
61
+ if (!CallModule?.checkOverlayPermission) return false;
62
+ return await CallModule.checkOverlayPermission();
63
+ },
64
+
50
65
  displayCall: async (uuid, name, callType = "audio") => {
51
66
  if (!CallModule) return false;
52
67
  return await CallModule.displayIncomingCall(uuid.toLowerCase().trim(), name, callType);
package/ios/CallModule.m CHANGED
@@ -3,6 +3,7 @@
3
3
 
4
4
  @interface CallModule ()
5
5
  @property (nonatomic, strong) NSString *pendingCallUuid;
6
+ @property (nonatomic, strong) NSMutableDictionary *pendingEvents;
6
7
  @end
7
8
 
8
9
  @implementation CallModule
@@ -20,6 +21,7 @@ RCT_EXPORT_MODULE();
20
21
  - (instancetype)init {
21
22
  self = [super init];
22
23
  if (self) {
24
+ self.pendingEvents = [NSMutableDictionary new];
23
25
  NSString *appName = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleDisplayName"] ?: [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleName"];
24
26
 
25
27
  CXProviderConfiguration *config = [[CXProviderConfiguration alloc] initWithLocalizedName:appName];
@@ -35,6 +37,34 @@ RCT_EXPORT_MODULE();
35
37
  return self;
36
38
  }
37
39
 
40
+ // MARK: - New Parity Methods (Synced with Android)
41
+
42
+ RCT_EXPORT_METHOD(checkCallValidity:(NSString *)uuidString resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject) {
43
+ BOOL isCurrent = [uuidString isEqualToString:self.currentCallUUID.UUIDString];
44
+ resolve(@{
45
+ @"isValid": @(isCurrent),
46
+ @"isCanceled": @(!isCurrent)
47
+ });
48
+ }
49
+
50
+ RCT_EXPORT_METHOD(getInitialCallData:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject) {
51
+ NSMutableDictionary *result = [NSMutableDictionary new];
52
+
53
+ if (self.pendingCallUuid) {
54
+ result[@"default"] = @{@"callUuid": self.pendingCallUuid};
55
+ self.pendingCallUuid = nil;
56
+ }
57
+
58
+ if (self.pendingEvents.count > 0) {
59
+ [result addEntriesFromDictionary:self.pendingEvents];
60
+ [self.pendingEvents removeAllObjects];
61
+ }
62
+
63
+ resolve(result.count > 0 ? result : [NSNull null]);
64
+ }
65
+
66
+ // MARK: - Core Logic
67
+
38
68
  RCT_EXPORT_METHOD(displayIncomingCall:(NSString *)uuidString
39
69
  name:(NSString *)name
40
70
  callType:(NSString *)callType
@@ -50,9 +80,9 @@ RCT_EXPORT_METHOD(displayIncomingCall:(NSString *)uuidString
50
80
  self.currentCallUUID = uuid;
51
81
 
52
82
  AVAudioSession *session = [AVAudioSession sharedInstance];
53
- [session setCategory:AVAudioSessionCategoryPlayAndRecord
54
- mode:AVAudioSessionModeVoiceChat
55
- options:AVAudioSessionCategoryOptionAllowBluetooth | AVAudioSessionCategoryOptionDefaultToSpeaker
83
+ [session setCategory:AVAudioSessionCategoryPlayAndRecord
84
+ mode:AVAudioSessionModeVoiceChat
85
+ options:AVAudioSessionCategoryOptionAllowBluetoothHFP | AVAudioSessionCategoryOptionDefaultToSpeaker
56
86
  error:nil];
57
87
 
58
88
  CXCallUpdate *update = [[CXCallUpdate alloc] init];
@@ -68,38 +98,35 @@ RCT_EXPORT_METHOD(displayIncomingCall:(NSString *)uuidString
68
98
  }];
69
99
  }
70
100
 
71
- RCT_EXPORT_METHOD(endNativeCall:(NSString *)uuidString)
72
- {
101
+ // Handles programmatic ending (e.g., via FCM "CANCEL")
102
+ RCT_EXPORT_METHOD(reportRemoteEnded:(NSString *)uuidString reason:(NSInteger)reason) {
73
103
  NSUUID *uuid = [[NSUUID alloc] initWithUUIDString:uuidString];
74
104
  if (!uuid) return;
75
105
 
76
- [self.provider reportCallWithUUID:uuid
77
- endedAtDate:[NSDate date]
78
- reason:CXCallEndedReasonRemoteEnded];
79
- self.currentCallUUID = nil;
80
- self.pendingCallUuid = nil;
81
- }
82
-
83
- RCT_EXPORT_METHOD(getInitialCallData:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject) {
84
- if (self.pendingCallUuid) {
85
- resolve(@{@"callUuid": self.pendingCallUuid});
86
- self.pendingCallUuid = nil;
87
- } else {
88
- resolve([NSNull null]);
106
+ // Report to CallKit that the remote user ended the call
107
+ [self.provider reportCallWithUUID:uuid
108
+ endedAtDate:[NSDate date]
109
+ reason:(CXCallEndedReason)reason];
110
+
111
+ // Clear local tracking
112
+ if ([uuid isEqual:self.currentCallUUID]) {
113
+ self.currentCallUUID = nil;
114
+ self.pendingCallUuid = nil;
89
115
  }
90
116
  }
91
117
 
92
- RCT_EXPORT_METHOD(checkTelecomPermissions:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject) {
93
- resolve(@YES);
118
+ RCT_EXPORT_METHOD(endNativeCall:(NSString *)uuidString)
119
+ {
120
+ [self reportRemoteEnded:uuidString reason:CXCallEndedReasonRemoteEnded];
94
121
  }
95
122
 
96
123
  // MARK: - CXProviderDelegate
97
124
 
98
125
  - (void)provider:(CXProvider *)provider performAnswerCallAction:(CXAnswerCallAction *)action {
99
126
  AVAudioSession *session = [AVAudioSession sharedInstance];
100
- [session setCategory:AVAudioSessionCategoryPlayAndRecord
101
- mode:AVAudioSessionModeVoiceChat
102
- options:AVAudioSessionCategoryOptionAllowBluetooth | AVAudioSessionCategoryOptionDefaultToSpeaker
127
+ [session setCategory:AVAudioSessionCategoryPlayAndRecord
128
+ mode:AVAudioSessionModeVoiceChat
129
+ options:AVAudioSessionCategoryOptionAllowBluetoothHFP | AVAudioSessionCategoryOptionDefaultToSpeaker
103
130
  error:nil];
104
131
  [session setActive:YES error:nil];
105
132
 
@@ -108,9 +135,12 @@ RCT_EXPORT_METHOD(checkTelecomPermissions:(RCTPromiseResolveBlock)resolve reject
108
135
  NSString *uuidStr = [action.callUUID.UUIDString lowercaseString];
109
136
  self.pendingCallUuid = uuidStr;
110
137
 
111
- dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
112
- // NUDGE: Force the window to become visible.
113
- // This helps transition from "Ghost Mode" to "Active UI" once the user unlocks.
138
+ // Switch from system UI to App UI
139
+ dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
140
+ [self.provider reportCallWithUUID:action.callUUID
141
+ endedAtDate:[NSDate date]
142
+ reason:CXCallEndedReasonAnsweredElsewhere];
143
+
114
144
  dispatch_async(dispatch_get_main_queue(), ^{
115
145
  [[[UIApplication sharedApplication] keyWindow] makeKeyAndVisible];
116
146
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rns-nativecall",
3
- "version": "0.7.9",
3
+ "version": "0.8.1",
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",
@@ -5,35 +5,54 @@ function withMainActivityDataFix(config) {
5
5
  return withMainActivity(config, (config) => {
6
6
  let contents = config.modResults.contents;
7
7
 
8
- // Ensure Intent import for Kotlin
9
- if (!contents.includes('import android.content.Intent')) {
10
- contents = contents.replace(/package .*/, (match) => `${match}\n\nimport android.content.Intent`);
11
- }
8
+ const imports = [
9
+ 'import android.content.Intent',
10
+ 'import android.os.Bundle',
11
+ 'import android.view.WindowManager',
12
+ 'import android.os.Build'
13
+ ];
12
14
 
13
- const onNewIntentCode = `
14
- override fun onNewIntent(intent: Intent) {
15
- super.onNewIntent(intent)
16
- setIntent(intent)
17
- }
18
- `;
15
+ // Add imports if they don't exist
16
+ imports.forEach(imp => {
17
+ if (!contents.includes(imp)) {
18
+ contents = contents.replace(/package .*/, (match) => `${match}\n${imp}`);
19
+ }
20
+ });
19
21
 
20
22
  const onCreateCode = `
21
23
  override fun onCreate(savedInstanceState: Bundle?) {
22
24
  super.onCreate(savedInstanceState)
23
-
24
- // If background wake from FCM, move to back to let LockScreen UI show
25
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {
26
+ setShowWhenLocked(false)
27
+ setTurnScreenOn(true)
28
+ } else {
29
+ window.addFlags(
30
+ WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED or
31
+ WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON or
32
+ WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
33
+ )
34
+ }
25
35
  if (intent.getBooleanExtra("background_wake", false)) {
26
36
  moveTaskToBack(true)
27
37
  }
28
- }
29
- `;
30
- if (!contents.includes('override fun onNewIntent')) {
31
- const lastBraceIndex = contents.lastIndexOf('}');
32
- contents = contents.slice(0, lastBraceIndex) + onNewIntentCode + contents.slice(lastBraceIndex);
33
- }
38
+ }`;
39
+
40
+ const onNewIntentCode = `
41
+ override fun onNewIntent(intent: Intent) {
42
+ super.onNewIntent(intent)
43
+ setIntent(intent)
44
+ }`;
45
+
46
+ // Use a more flexible regex for the class definition
47
+ const classRegex = /class MainActivity\s*:\s*ReactActivity\(\)\s*\{/;
48
+
34
49
  if (!contents.includes('override fun onCreate')) {
35
- const lastBraceIndex = contents.lastIndexOf('}');
36
- contents = contents.slice(0, lastBraceIndex) + onCreateCode + contents.slice(lastBraceIndex);
50
+ contents = contents.replace(classRegex, (match) => `${match}${onCreateCode}`);
51
+ }
52
+
53
+ if (!contents.includes('override fun onNewIntent')) {
54
+ // Re-run match check because contents might have changed from onCreate injection
55
+ contents = contents.replace(classRegex, (match) => `${match}${onNewIntentCode}`);
37
56
  }
38
57
 
39
58
  config.modResults.contents = contents;
@@ -47,79 +66,115 @@ function withAndroidConfig(config) {
47
66
  const manifest = config.modResults;
48
67
  const application = manifest.manifest.application[0];
49
68
 
50
- // Comprehensive list for VoIP & Foreground Services
69
+ // Ensure MainActivity flags
70
+ const mainActivity = application.activity.find((a) => a.$['android:name'] === '.MainActivity');
71
+ if (mainActivity) {
72
+ mainActivity.$['android:launchMode'] = 'singleTop';
73
+ mainActivity.$['android:showWhenLocked'] = 'false';
74
+ mainActivity.$['android:turnScreenOn'] = 'true';
75
+ }
76
+
51
77
  const permissions = [
52
78
  'android.permission.USE_FULL_SCREEN_INTENT',
53
79
  'android.permission.VIBRATE',
54
80
  'android.permission.FOREGROUND_SERVICE',
55
81
  'android.permission.FOREGROUND_SERVICE_PHONE_CALL',
56
82
  'android.permission.POST_NOTIFICATIONS',
57
- 'android.permission.SYSTEM_ALERT_WINDOW',
58
83
  'android.permission.WAKE_LOCK',
59
84
  'android.permission.DISABLE_KEYGUARD',
60
- 'android.permission.MANAGE_OWN_CALLS'
85
+ 'android.permission.SYSTEM_ALERT_WINDOW'
61
86
  ];
62
87
 
88
+ // Initialize permissions array if missing
63
89
  manifest.manifest['uses-permission'] = manifest.manifest['uses-permission'] || [];
90
+
64
91
  permissions.forEach((perm) => {
65
92
  if (!manifest.manifest['uses-permission'].some((p) => p.$['android:name'] === perm)) {
66
93
  manifest.manifest['uses-permission'].push({ $: { 'android:name': perm } });
67
94
  }
68
95
  });
69
96
 
97
+ // Initialize components if missing
98
+ application.service = application.service || [];
70
99
  application.activity = application.activity || [];
71
- // AcceptCallActivity registration
72
- if (!application.activity.some(a => a.$['android:name'] === 'com.rnsnativecall.AcceptCallActivity')) {
73
- application.activity.push({
74
- $: {
75
- 'android:name': 'com.rnsnativecall.AcceptCallActivity',
76
- 'android:theme': '@android:style/Theme.Translucent.NoTitleBar',
77
- 'android:excludeFromRecents': 'true',
78
- 'android:noHistory': 'true',
79
- 'android:exported': 'false',
80
- 'android:launchMode': 'singleInstance',
81
- 'android:showWhenLocked': 'true',
82
- 'android:turnScreenOn': 'true'
83
- }
84
- });
85
- }
100
+ application.receiver = application.receiver || [];
86
101
 
87
- application.service = application.service || [];
102
+ // 1. Services
103
+ const services = [
104
+ { name: 'com.rnsnativecall.CallMessagingService', exported: 'false', filter: 'com.google.firebase.MESSAGING_EVENT' },
105
+ { name: 'com.rnsnativecall.CallForegroundService', type: 'phoneCall' },
106
+ { name: 'com.rnsnativecall.CallHeadlessTask' }
107
+ ];
88
108
 
89
- // 1. Firebase Messaging Service
90
- const firebaseServiceName = 'com.rnsnativecall.CallMessagingService';
91
- if (!application.service.some(s => s.$['android:name'] === firebaseServiceName)) {
92
- application.service.push({
93
- $: { 'android:name': firebaseServiceName, 'android:exported': 'false' },
94
- 'intent-filter': [{ action: [{ $: { 'android:name': 'com.google.firebase.MESSAGING_EVENT' } }] }]
95
- });
96
- }
109
+ services.forEach(svc => {
110
+ if (!application.service.some(s => s.$['android:name'] === svc.name)) {
111
+ const entry = { $: { 'android:name': svc.name, 'android:exported': svc.exported || 'false' } };
112
+ if (svc.type) entry.$['android:foregroundServiceType'] = svc.type;
113
+ if (svc.filter) {
114
+ entry['intent-filter'] = [{ action: [{ $: { 'android:name': svc.filter } }] }];
115
+ }
116
+ application.service.push(entry);
117
+ }
118
+ });
97
119
 
98
- // 2. Headless Task Service
99
- const headlessServiceName = 'com.rnsnativecall.CallHeadlessTask';
100
- if (!application.service.some(s => s.$['android:name'] === headlessServiceName)) {
101
- application.service.push({
102
- $: { 'android:name': headlessServiceName, 'android:exported': 'false' }
103
- });
104
- }
120
+ // 2. Activities (Trampoline & Overlay)
121
+ const activities = [
122
+ {
123
+ name: 'com.rnsnativecall.AcceptCallActivity',
124
+ theme: '@android:style/Theme.Translucent.NoTitleBar',
125
+ launchMode: 'singleInstance'
126
+ },
127
+ {
128
+ name: 'com.rnsnativecall.NotificationOverlayActivity',
129
+ theme: '@android:style/Theme.NoTitleBar.Fullscreen',
130
+ launchMode: 'singleInstance'
131
+ }
132
+ ];
133
+
134
+ activities.forEach(act => {
135
+ if (!application.activity.some(a => a.$['android:name'] === act.name)) {
136
+ application.activity.push({
137
+ $: {
138
+ 'android:name': act.name,
139
+ 'android:theme': act.theme,
140
+ 'android:exported': 'false',
141
+ 'android:showWhenLocked': 'true',
142
+ 'android:turnScreenOn': 'true',
143
+ 'android:excludeFromRecents': 'true',
144
+ 'android:noHistory': 'true',
145
+ 'android:launchMode': act.launchMode
146
+ }
147
+ });
148
+ }
149
+ });
105
150
 
106
- // 3. Foreground Service (The "Connecting..." spinner)
107
- const foregroundServiceName = 'com.rnsnativecall.CallForegroundService';
108
- if (!application.service.some(s => s.$['android:name'] === foregroundServiceName)) {
109
- application.service.push({
151
+ // 3. Receiver
152
+ if (!application.receiver.some(r => r.$['android:name'] === 'com.rnsnativecall.CallActionReceiver')) {
153
+ application.receiver.push({
110
154
  $: {
111
- 'android:name': foregroundServiceName,
112
- 'android:foregroundServiceType': 'phoneCall',
155
+ 'android:name': 'com.rnsnativecall.CallActionReceiver',
113
156
  'android:exported': 'false'
114
157
  }
115
158
  });
116
159
  }
117
160
 
118
- application.receiver = application.receiver || [];
119
- const receiverName = 'com.rnsnativecall.CallActionReceiver';
120
- if (!application.receiver.some(r => r.$['android:name'] === receiverName)) {
161
+ // ADD THIS: UnlockReceiver with USER_PRESENT filter
162
+ if (!application.receiver.some(r => r.$['android:name'] === 'com.rnsnativecall.UnlockReceiver')) {
121
163
  application.receiver.push({
122
- $: { 'android:name': receiverName, 'android:exported': 'false' }
164
+ $: {
165
+ 'android:name': 'com.rnsnativecall.UnlockReceiver',
166
+ 'android:exported': 'true',
167
+ 'android:enabled': 'true',
168
+ 'android:directBootAware': 'true', // Helps bypass some background restrictions
169
+ },
170
+ 'intent-filter': [
171
+ {
172
+ $: { 'android:priority': '1000' },
173
+ action: [
174
+ { $: { 'android:name': 'android.intent.action.USER_PRESENT' } }
175
+ ]
176
+ }
177
+ ]
123
178
  });
124
179
  }
125
180
 
@@ -132,13 +187,20 @@ function withIosConfig(config) {
132
187
  return withInfoPlist(config, (config) => {
133
188
  const infoPlist = config.modResults;
134
189
  if (!infoPlist.UIBackgroundModes) infoPlist.UIBackgroundModes = [];
135
- ['voip', 'audio'].forEach(mode => {
136
- if (!infoPlist.UIBackgroundModes.includes(mode)) infoPlist.UIBackgroundModes.push(mode);
190
+
191
+ ['voip', 'audio', 'remote-notification'].forEach(mode => {
192
+ if (!infoPlist.UIBackgroundModes.includes(mode)) {
193
+ infoPlist.UIBackgroundModes.push(mode);
194
+ }
137
195
  });
138
196
  return config;
139
197
  });
140
198
  }
141
199
 
142
200
  module.exports = (config) => {
143
- return withPlugins(config, [withAndroidConfig, withMainActivityDataFix, withIosConfig]);
144
- };
201
+ return withPlugins(config, [
202
+ withAndroidConfig,
203
+ withMainActivityDataFix,
204
+ withIosConfig
205
+ ]);
206
+ };