rns-nativecall 0.6.5 → 0.6.7

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.
@@ -13,6 +13,7 @@ class AcceptCallActivity : Activity() {
13
13
  override fun onCreate(savedInstanceState: Bundle?) {
14
14
  super.onCreate(savedInstanceState)
15
15
 
16
+ // Ensure we show over the lockscreen
16
17
  window.addFlags(
17
18
  WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED or
18
19
  WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON or
@@ -34,36 +35,30 @@ class AcceptCallActivity : Activity() {
34
35
  val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
35
36
  uuid?.let { notificationManager.cancel(it.hashCode()) }
36
37
 
37
- // WE STOP SENDING THE JS EVENT HERE.
38
- // Instead, we pass the intent to MainActivity with a specific ACTION.
38
+ val dataMap = mutableMapOf<String, String>()
39
+ extras?.keySet()?.forEach { key ->
40
+ extras.get(key)?.let { dataMap[key] = it.toString() }
41
+ }
42
+
43
+ // 1. Set the data for JS (Cold start support)
44
+ CallModule.setPendingCallData("onCallAccepted_pending", dataMap)
45
+
46
+ // 2. Fire event immediately if JS is alive
47
+ if (CallModule.isReady()) {
48
+ CallModule.sendEventToJS("onCallAccepted", dataMap)
49
+ }
50
+
51
+ // 3. Bring the Main App to the front
39
52
  openMainApp(extras)
40
53
  finish()
41
54
  }
42
55
 
43
56
  private fun openMainApp(extras: Bundle?) {
44
- try {
45
- // Get the actual MainActivity class name (e.g., com.yourapp.MainActivity)
46
- val mainActivityClassName = "${packageName}.MainActivity"
47
-
48
- val intent = Intent().apply {
49
- setClassName(packageName, mainActivityClassName)
50
- action = "com.rnsnativecall.ACTION_ANSWER"
51
- addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_SINGLE_TOP)
52
-
53
- // Ensure extras are carried over
54
- extras?.let { putExtras(it) }
55
- }
56
-
57
- startActivity(intent)
58
- } catch (e: Exception) {
59
- // Fallback: If explicit mapping fails, try the launch intent but force the action
60
- val launchIntent = packageManager.getLaunchIntentForPackage(packageName)
61
- launchIntent?.apply {
62
- action = "com.rnsnativecall.ACTION_ANSWER"
63
- addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_SINGLE_TOP)
64
- extras?.let { putExtras(it) }
65
- startActivity(this)
66
- }
57
+ val launchIntent = packageManager.getLaunchIntentForPackage(packageName)
58
+ launchIntent?.apply {
59
+ addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_SINGLE_TOP)
60
+ putExtras(extras ?: Bundle())
61
+ startActivity(this)
67
62
  }
68
63
  }
69
64
  }
@@ -0,0 +1,70 @@
1
+ package com.rnsnativecall
2
+
3
+ import android.app.*
4
+ import android.content.Context
5
+ import android.content.Intent
6
+ import android.os.Build
7
+ import android.os.IBinder
8
+ import androidx.core.app.NotificationCompat
9
+ import com.facebook.react.HeadlessJsTaskService
10
+
11
+ class CallForegroundService : Service() {
12
+
13
+ companion object {
14
+ private const val NOTIFICATION_ID = 101
15
+ private const val CHANNEL_ID = "incoming_call_service"
16
+
17
+ fun stop(context: Context) {
18
+ val intent = Intent(context, CallForegroundService::class.java)
19
+ context.stopService(intent)
20
+ }
21
+ }
22
+
23
+ override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
24
+ val data = intent?.extras
25
+ val name = data?.getString("name") ?: "Someone"
26
+
27
+ createNotificationChannel()
28
+
29
+ // Create a notification that shows "Connecting..."
30
+ val notification = NotificationCompat.Builder(this, CHANNEL_ID)
31
+ .setContentTitle(name)
32
+ .setContentText("Connecting...")
33
+ .setSmallIcon(applicationInfo.icon)
34
+ .setCategory(NotificationCompat.CATEGORY_CALL)
35
+ .setPriority(NotificationCompat.PRIORITY_LOW) // Keep it subtle until UI pops
36
+ .setOngoing(true)
37
+ .build()
38
+
39
+ // Start Foreground immediately to satisfy OS requirements
40
+ startForeground(NOTIFICATION_ID, notification)
41
+
42
+ // Launch the Headless Task while under the protection of this service
43
+ val headlessIntent = Intent(this, CallHeadlessTask::class.java).apply {
44
+ data?.let { putExtras(it) }
45
+ }
46
+
47
+ try {
48
+ this.startService(headlessIntent)
49
+ HeadlessJsTaskService.acquireWakeLockNow(this)
50
+ } catch (e: Exception) {
51
+ e.printStackTrace()
52
+ }
53
+
54
+ return START_NOT_STICKY
55
+ }
56
+
57
+ private fun createNotificationChannel() {
58
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
59
+ val channel = NotificationChannel(
60
+ CHANNEL_ID,
61
+ "Call Service",
62
+ NotificationManager.IMPORTANCE_LOW
63
+ )
64
+ val manager = getSystemService(NotificationManager::class.java)
65
+ manager.createNotificationChannel(channel)
66
+ }
67
+ }
68
+
69
+ override fun onBind(intent: Intent?): IBinder? = null
70
+ }
@@ -61,17 +61,30 @@ if (!CallState.setCurrent(uuid)) {
61
61
  // }
62
62
  // ContextCompat.startForegroundService(context, serviceIntent)
63
63
 
64
- val headlessIntent = Intent(context, CallHeadlessTask::class.java).apply {
65
- putExtras(Bundle().apply { data.forEach { (k, v) -> putString(k, v) } })
66
- }
67
- try {
68
- context.startService(headlessIntent)
69
- HeadlessJsTaskService.acquireWakeLockNow(context)
70
- } catch (e: Exception) {
71
- e.printStackTrace()
72
- }
64
+ // val headlessIntent = Intent(context, CallHeadlessTask::class.java).apply {
65
+ // putExtras(Bundle().apply { data.forEach { (k, v) -> putString(k, v) } })
66
+ // }
67
+ // try {
68
+ // context.startService(headlessIntent)
69
+ // HeadlessJsTaskService.acquireWakeLockNow(context)
70
+ // } catch (e: Exception) {
71
+ // e.printStackTrace()
72
+ // }
73
73
 
74
74
 
75
+ // Background → start foreground service (which in turn starts headless)
76
+ val serviceIntent = Intent(context, CallForegroundService::class.java).apply {
77
+ putExtras(Bundle().apply {
78
+ data.forEach { (k, v) -> putString(k, v) }
79
+ })
80
+ }
81
+
82
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
83
+ context.startForegroundService(serviceIntent)
84
+ } else {
85
+ context.startService(serviceIntent)
86
+ }
87
+
75
88
  }
76
89
  }
77
90
 
@@ -4,6 +4,7 @@ import android.app.NotificationManager
4
4
  import android.content.Context
5
5
  import com.facebook.react.bridge.*
6
6
  import com.facebook.react.modules.core.DeviceEventManagerModule
7
+ import android.content.Intent
7
8
 
8
9
  class CallModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaModule(reactContext) {
9
10
 
@@ -66,6 +67,18 @@ class CallModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaMo
66
67
  promise.resolve(map)
67
68
  }
68
69
 
70
+ @ReactMethod
71
+ fun stopForegroundService(promise: Promise) {
72
+ try {
73
+ // Using the current react context to stop the service
74
+ val intent = Intent(reactApplicationContext, CallForegroundService::class.java)
75
+ reactApplicationContext.stopService(intent)
76
+ promise.resolve(true)
77
+ } catch (e: Exception) {
78
+ promise.reject("SERVICE_STOP_ERROR", e.message)
79
+ }
80
+ }
81
+
69
82
  /**
70
83
  * General Status Check:
71
84
  * Useful for checking if the UI is still relevant.
package/index.d.ts CHANGED
@@ -25,6 +25,8 @@ export interface CallHandlerType {
25
25
 
26
26
  destroyNativeCallUI(uuid: string): void;
27
27
 
28
+ stopForegroundService(): Promise<void>;
29
+
28
30
  getInitialCallData(): Promise<any | null>;
29
31
 
30
32
  subscribe(
package/index.js CHANGED
@@ -37,6 +37,7 @@ export const CallHandler = {
37
37
  if (onAction) {
38
38
  await onAction(data, 'INCOMING_CALL');
39
39
  }
40
+
40
41
  } catch (error) {
41
42
  console.error('[RNSNativeCall] Headless Task Error:', error);
42
43
  }
@@ -48,6 +49,10 @@ export const CallHandler = {
48
49
  return await CallModule.displayIncomingCall(uuid.toLowerCase().trim(), name, callType);
49
50
  },
50
51
 
52
+ stopForegroundService: async () => {
53
+ await CallModule.stopForegroundService(true)
54
+ },
55
+
51
56
  destroyNativeCallUI: (uuid) => {
52
57
  if (CallModule?.endNativeCall) {
53
58
  CallModule.endNativeCall(uuid.toLowerCase().trim());
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rns-nativecall",
3
- "version": "0.6.5",
3
+ "version": "0.6.7",
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,32 +5,15 @@ function withMainActivityDataFix(config) {
5
5
  return withMainActivity(config, (config) => {
6
6
  let contents = config.modResults.contents;
7
7
 
8
- // Ensure imports exist
8
+ // Ensure Intent import for Kotlin
9
9
  if (!contents.includes('import android.content.Intent')) {
10
10
  contents = contents.replace(/package .*/, (match) => `${match}\n\nimport android.content.Intent`);
11
11
  }
12
- if (!contents.includes('import android.os.Bundle')) {
13
- contents = contents.replace(/package .*/, (match) => `${match}\n\nimport android.os.Bundle`);
14
- }
15
12
 
16
13
  const onNewIntentCode = `
17
14
  override fun onNewIntent(intent: Intent) {
18
15
  super.onNewIntent(intent)
19
16
  setIntent(intent)
20
-
21
- // Check for the specific Answer Action
22
- val isAnswerAction = intent.action == "com.rnsnativecall.ACTION_ANSWER"
23
-
24
- val dataMap = mutableMapOf<String, String>()
25
- intent.extras?.keySet()?.forEach { key ->
26
- dataMap[key] = intent.extras?.get(key)?.toString() ?: ""
27
- }
28
-
29
- // FIRE if it's the answer action, even if extras are slim
30
- if (isAnswerAction) {
31
- com.rnsnativecall.CallModule.setPendingCallData("onCallAccepted_pending", dataMap)
32
- com.rnsnativecall.CallModule.sendEventToJS("onCallAccepted", dataMap)
33
- }
34
17
  }
35
18
  `;
36
19
 
@@ -38,25 +21,12 @@ function withMainActivityDataFix(config) {
38
21
  override fun onCreate(savedInstanceState: Bundle?) {
39
22
  super.onCreate(savedInstanceState)
40
23
 
41
- val isAnswerAction = intent.action == "com.rnsnativecall.ACTION_ANSWER"
42
-
43
- // Logic for Cold Start (App was dead, user answered)
44
- if (isAnswerAction) {
45
- val dataMap = mutableMapOf<String, String>()
46
- intent.extras?.keySet()?.forEach { key ->
47
- dataMap[key] = intent.extras?.get(key)?.toString() ?: ""
48
- }
49
- com.rnsnativecall.CallModule.setPendingCallData(dataMap)
50
- }
51
-
52
- // Move to back if it's a background wake (FCM) and NOT an answer click
53
- if (intent.getBooleanExtra("background_wake", false) && !isAnswerAction) {
24
+ // If background wake from FCM, move to back to let LockScreen UI show
25
+ if (intent.getBooleanExtra("background_wake", false)) {
54
26
  moveTaskToBack(true)
55
27
  }
56
28
  }
57
29
  `;
58
-
59
- // Inject codes
60
30
  if (!contents.includes('override fun onNewIntent')) {
61
31
  const lastBraceIndex = contents.lastIndexOf('}');
62
32
  contents = contents.slice(0, lastBraceIndex) + onNewIntentCode + contents.slice(lastBraceIndex);
@@ -70,11 +40,14 @@ function withMainActivityDataFix(config) {
70
40
  return config;
71
41
  });
72
42
  }
43
+
73
44
  /** 2. ANDROID MANIFEST CONFIG **/
74
45
  function withAndroidConfig(config) {
75
46
  return withAndroidManifest(config, (config) => {
76
47
  const manifest = config.modResults;
77
48
  const application = manifest.manifest.application[0];
49
+
50
+ // Comprehensive list for VoIP & Foreground Services
78
51
  const permissions = [
79
52
  'android.permission.USE_FULL_SCREEN_INTENT',
80
53
  'android.permission.VIBRATE',
@@ -83,15 +56,19 @@ function withAndroidConfig(config) {
83
56
  'android.permission.POST_NOTIFICATIONS',
84
57
  'android.permission.SYSTEM_ALERT_WINDOW',
85
58
  'android.permission.WAKE_LOCK',
86
- 'android.permission.DISABLE_KEYGUARD'
59
+ 'android.permission.DISABLE_KEYGUARD',
60
+ 'android.permission.MANAGE_OWN_CALLS'
87
61
  ];
62
+
88
63
  manifest.manifest['uses-permission'] = manifest.manifest['uses-permission'] || [];
89
64
  permissions.forEach((perm) => {
90
65
  if (!manifest.manifest['uses-permission'].some((p) => p.$['android:name'] === perm)) {
91
66
  manifest.manifest['uses-permission'].push({ $: { 'android:name': perm } });
92
67
  }
93
68
  });
69
+
94
70
  application.activity = application.activity || [];
71
+ // AcceptCallActivity registration
95
72
  if (!application.activity.some(a => a.$['android:name'] === 'com.rnsnativecall.AcceptCallActivity')) {
96
73
  application.activity.push({
97
74
  $: {
@@ -106,21 +83,10 @@ function withAndroidConfig(config) {
106
83
  }
107
84
  });
108
85
  }
109
- if (!application.activity.some(a => a.$['android:name'] === 'com.rnsnativecall.UnlockPromptActivity')) {
110
- application.activity.push({
111
- $: {
112
- 'android:name': 'com.rnsnativecall.UnlockPromptActivity',
113
- 'android:theme': '@android:style/Theme.Translucent.NoTitleBar',
114
- 'android:excludeFromRecents': 'true',
115
- 'android:noHistory': 'true',
116
- 'android:exported': 'false',
117
- 'android:launchMode': 'singleInstance',
118
- 'android:showWhenLocked': 'true',
119
- 'android:turnScreenOn': 'true'
120
- }
121
- });
122
- }
86
+
123
87
  application.service = application.service || [];
88
+
89
+ // 1. Firebase Messaging Service
124
90
  const firebaseServiceName = 'com.rnsnativecall.CallMessagingService';
125
91
  if (!application.service.some(s => s.$['android:name'] === firebaseServiceName)) {
126
92
  application.service.push({
@@ -128,12 +94,27 @@ function withAndroidConfig(config) {
128
94
  'intent-filter': [{ action: [{ $: { 'android:name': 'com.google.firebase.MESSAGING_EVENT' } }] }]
129
95
  });
130
96
  }
97
+
98
+ // 2. Headless Task Service
131
99
  const headlessServiceName = 'com.rnsnativecall.CallHeadlessTask';
132
100
  if (!application.service.some(s => s.$['android:name'] === headlessServiceName)) {
133
101
  application.service.push({
134
102
  $: { 'android:name': headlessServiceName, 'android:exported': 'false' }
135
103
  });
136
104
  }
105
+
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({
110
+ $: {
111
+ 'android:name': foregroundServiceName,
112
+ 'android:foregroundServiceType': 'phoneCall',
113
+ 'android:exported': 'false'
114
+ }
115
+ });
116
+ }
117
+
137
118
  application.receiver = application.receiver || [];
138
119
  const receiverName = 'com.rnsnativecall.CallActionReceiver';
139
120
  if (!application.receiver.some(r => r.$['android:name'] === receiverName)) {