rns-nativecall 0.1.6 → 0.1.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.
@@ -0,0 +1,38 @@
1
+ package com.rnsnativecall
2
+
3
+ import android.app.Activity
4
+ import android.app.NotificationManager
5
+ import android.content.Context
6
+ import android.content.Intent
7
+ import android.os.Bundle
8
+
9
+ class AcceptCallActivity : Activity() {
10
+ override fun onCreate(savedInstanceState: Bundle?) {
11
+ super.onCreate(savedInstanceState)
12
+
13
+ // 1. CLEAR THE PILL IMMEDIATELY
14
+ val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
15
+ notificationManager.cancel(101)
16
+
17
+ // 2. PREPARE DATA FOR JS
18
+ val dataMap = mutableMapOf<String, String>()
19
+ intent.extras?.keySet()?.forEach { key ->
20
+ intent.getStringExtra(key)?.let { dataMap[key] = it }
21
+ }
22
+
23
+ // 3. SEND EVENT TO REACT NATIVE
24
+ CallModule.sendEventToJS("onCallAccepted", dataMap)
25
+
26
+ // 4. OPEN THE MAIN APP
27
+ val launchIntent = packageManager.getLaunchIntentForPackage(packageName)
28
+ launchIntent?.apply {
29
+ addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_SINGLE_TOP)
30
+ putExtras(intent.extras ?: Bundle())
31
+ putExtra("navigatingToCall", true)
32
+ }
33
+ startActivity(launchIntent)
34
+
35
+ // 5. KILL THIS INVISIBLE WINDOW
36
+ finish()
37
+ }
38
+ }
@@ -0,0 +1,41 @@
1
+ package com.rnsnativecall
2
+
3
+ import android.content.BroadcastReceiver
4
+ import android.content.Context
5
+ import android.content.Intent
6
+ import android.app.NotificationManager
7
+ import android.os.Bundle
8
+
9
+ class CallActionReceiver : BroadcastReceiver() {
10
+ override fun onReceive(context: Context, intent: Intent) {
11
+ val uuid = intent.getStringExtra("EXTRA_CALL_UUID")
12
+
13
+ // 1. Clear the notification
14
+ val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
15
+ notificationManager.cancel(101)
16
+
17
+ if (intent.action == "ACTION_ACCEPT") {
18
+ // 2. Prepare the data for React Native
19
+ val dataMap = mutableMapOf<String, String>()
20
+ intent.extras?.keySet()?.forEach { key ->
21
+ intent.getStringExtra(key)?.let { dataMap[key] = it }
22
+ }
23
+
24
+ // 3. Send the event to JS if the app is already running in background
25
+ CallModule.sendEventToJS("onCallAccepted", dataMap)
26
+
27
+ // 4. Bring the app to foreground
28
+ val launchIntent = context.packageManager.getLaunchIntentForPackage(context.packageName)
29
+ launchIntent?.apply {
30
+ addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_SINGLE_TOP)
31
+ // Forward the extras so the App can read them on startup/resume
32
+ putExtras(intent.extras ?: Bundle())
33
+ putExtra("navigatingToCall", true)
34
+ }
35
+ context.startActivity(launchIntent)
36
+ } else {
37
+ // Logic for Reject
38
+ CallModule.sendEventToJS("onCallRejected", mapOf("callUUID" to uuid))
39
+ }
40
+ }
41
+ }
@@ -1,55 +1,89 @@
1
1
  package com.rnsnativecall
2
2
 
3
- import android.content.ComponentName
3
+ import android.app.NotificationChannel
4
+ import android.app.NotificationManager
5
+ import android.app.PendingIntent
4
6
  import android.content.Context
5
- import android.net.Uri
6
- import android.os.Bundle
7
- import android.telecom.PhoneAccountHandle
8
- import android.telecom.TelecomManager
9
- import android.telecom.VideoProfile
7
+ import android.content.Intent
8
+ import android.os.Build
9
+ import androidx.core.app.NotificationCompat
10
10
 
11
11
  object NativeCallManager {
12
12
 
13
- // Inside NativeCallManager.kt
14
- fun handleIncomingPush(context: Context, data: Map<String, String>) {
15
- val uuid = data["callId"] ?: ""
16
- val name = data["name"] ?: "Unknown"
17
-
18
- // Create the Intent for the FullScreen Activity
19
- val fullScreenIntent = Intent(context, IncomingCallActivity::class.java).apply {
20
- flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_NO_USER_ACTION
21
- putExtra("EXTRA_CALL_UUID", uuid)
22
- putExtra("EXTRA_CALLER_NAME", name)
23
- }
13
+ fun handleIncomingPush(context: Context, data: Map<String, String>) {
14
+ val uuid = data["callId"] ?: return
15
+ val name = data["name"] ?: "Incoming Call"
16
+ val callType = data["callType"] ?: "audio"
24
17
 
25
- val fullScreenPendingIntent = PendingIntent.getActivity(context, 0,
26
- fullScreenIntent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
27
-
28
- // Build the Notification
29
- val notificationBuilder = NotificationCompat.Builder(context, "CALL_CHANNEL_ID")
30
- .setSmallIcon(context.applicationInfo.icon)
31
- .setContentTitle("Incoming Call")
32
- .setContentText(name)
33
- .setPriority(NotificationCompat.PRIORITY_MAX)
34
- .setCategory(NotificationCompat.CATEGORY_CALL)
35
- .setFullScreenIntent(fullScreenPendingIntent, true) // High Priority HUN
36
- .setOngoing(true)
37
- .setAutoCancel(false)
38
-
39
- val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
40
-
41
- // Create Channel for Android O+
42
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
43
- val channel = NotificationChannel("CALL_CHANNEL_ID", "Incoming Calls", NotificationManager.IMPORTANCE_HIGH)
44
- notificationManager.createNotificationChannel(channel)
45
- }
18
+ val pendingFlags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
19
+ PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
20
+ } else {
21
+ PendingIntent.FLAG_UPDATE_CURRENT
22
+ }
46
23
 
47
- notificationManager.notify(101, notificationBuilder.build())
24
+ // 1. DUMMY INTENT: This is the key to making the Pill "Sticky".
25
+ // It prevents the notification from disappearing but doesn't open a screen.
26
+ val dummyIntent = PendingIntent.getActivity(
27
+ context,
28
+ 0,
29
+ Intent(),
30
+ pendingFlags
31
+ )
32
+
33
+ // 2. Accept Action
34
+ val acceptIntent = Intent(context, AcceptCallActivity::class.java).apply {
35
+ action = "ACTION_ACCEPT"
36
+ putExtra("EXTRA_CALL_UUID", uuid)
37
+ data.forEach { (key, value) -> putExtra(key, value) }
38
+ // Crucial for launching from a notification
39
+ flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP
48
40
  }
49
-
50
41
 
51
- private fun getPhoneAccountHandle(context: Context): PhoneAccountHandle {
52
- val componentName = ComponentName(context, MyConnectionService::class.java)
53
- return PhoneAccountHandle(componentName, "${context.packageName}.voip")
42
+ // Keep this as getActivity
43
+ val acceptPendingIntent = PendingIntent.getActivity(
44
+ context,
45
+ 1001,
46
+ acceptIntent,
47
+ pendingFlags
48
+ )
49
+ // 3. Reject Action
50
+ val rejectIntent = Intent(context, CallActionReceiver::class.java).apply {
51
+ action = "ACTION_REJECT"
52
+ putExtra("EXTRA_CALL_UUID", uuid)
53
+ }
54
+ val rejectPendingIntent = PendingIntent.getBroadcast(context, 1002, rejectIntent, pendingFlags)
55
+
56
+ // 4. Setup Channel (Ensure Importance is HIGH)
57
+ val channelId = "CALL_CHANNEL_ID"
58
+ val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
59
+
60
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
61
+ val channel = NotificationChannel(channelId, "Incoming Calls", NotificationManager.IMPORTANCE_HIGH).apply {
62
+ description = "Shows incoming call notifications"
63
+ enableVibration(true)
64
+ // Critical for bypassing "Do Not Disturb"
65
+ setBypassDnd(true)
66
+ lockscreenVisibility = NotificationCompat.VISIBILITY_PUBLIC
67
+ }
68
+ notificationManager.createNotificationChannel(channel)
69
+ }
70
+
71
+ // 5. Build the Notification
72
+ val builder = NotificationCompat.Builder(context, channelId)
73
+ .setSmallIcon(context.applicationInfo.icon)
74
+ .setContentTitle("Incoming $callType call")
75
+ .setContentText(name)
76
+ .setPriority(NotificationCompat.PRIORITY_MAX)
77
+ .setCategory(NotificationCompat.CATEGORY_CALL)
78
+ .setOngoing(true) // Prevents user from swiping it away
79
+ .setAutoCancel(false)
80
+
81
+ // This is what keeps the Heads-Up "Pill" visible indefinitely:
82
+ .setFullScreenIntent(dummyIntent, true)
83
+
84
+ .addAction(0, "Answer", acceptPendingIntent)
85
+ .addAction(0, "Decline", rejectPendingIntent)
86
+
87
+ notificationManager.notify(101, builder.build())
54
88
  }
55
89
  }
@@ -1,3 +1,12 @@
1
- <style name="CircleImage">
2
- <item name="cornerSize">50%</item>
3
- </style>
1
+ <?xml version="1.0" encoding="utf-8"?>
2
+ <resources>
3
+ <style name="CircleImage" parent="">
4
+ <item name="cornerSize">50%</item>
5
+ </style>
6
+
7
+ <style name="Theme.IncomingCall" parent="Theme.AppCompat.Light.NoActionBar">
8
+ <item name="android:windowBackground">@android:color/transparent</item>
9
+ <item name="android:windowIsTranslucent">true</item>
10
+ <item name="android:windowAnimationStyle">@android:style/Animation.Translucent</item>
11
+ </style>
12
+ </resources>
package/index.js CHANGED
@@ -38,10 +38,10 @@ export const CallHandler = {
38
38
  displayCall: async (uuid, number, name, hasVideo = false, shouldRing = true) => {
39
39
  if (!CallModule) return false;
40
40
 
41
- if (Platform.OS === 'android') {
42
- const hasPerms = await ensureAndroidPermissions();
43
- if (!hasPerms) return false;
44
- }
41
+ // if (Platform.OS === 'android') {
42
+ // const hasPerms = await ensureAndroidPermissions();
43
+ // if (!hasPerms) return false;
44
+ // }
45
45
 
46
46
  try {
47
47
  return await CallModule.displayIncomingCall(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rns-nativecall",
3
- "version": "0.1.6",
3
+ "version": "0.1.7",
4
4
  "description": "RNS nativecall component with native Android/iOS for handling native call ui, when app is not open or open.",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",
@@ -1,14 +1,11 @@
1
1
  const { withAndroidManifest, withInfoPlist, withPlugins } = require('@expo/config-plugins');
2
2
 
3
- /**
4
- * ANDROID CONFIGURATION
5
- */
6
3
  function withAndroidConfig(config) {
7
4
  return withAndroidManifest(config, (config) => {
8
5
  const manifest = config.modResults;
9
6
  const application = manifest.manifest.application[0];
10
7
 
11
- // 1. Unified Permissions List
8
+ // 1. Permissions
12
9
  const permissions = [
13
10
  'android.permission.READ_PHONE_NUMBERS',
14
11
  'android.permission.CALL_PHONE',
@@ -26,84 +23,73 @@ function withAndroidConfig(config) {
26
23
  }
27
24
  });
28
25
 
29
- // 2. Register IncomingCallActivity
26
+ // 2. Activities (UI Components)
30
27
  application.activity = application.activity || [];
31
- const activityName = 'com.rnsnativecall.IncomingCallActivity';
32
- if (!application.activity.some(a => a.$['android:name'] === activityName)) {
28
+
29
+ // IncomingCallActivity (Optional lock screen UI)
30
+ if (!application.activity.some(a => a.$['android:name'] === 'com.rnsnativecall.IncomingCallActivity')) {
33
31
  application.activity.push({
34
32
  $: {
35
- 'android:name': activityName,
33
+ 'android:name': 'com.rnsnativecall.IncomingCallActivity',
36
34
  'android:showOnLockScreen': 'true',
37
35
  'android:launchMode': 'singleInstance',
38
- 'android:excludeFromRecents': 'true',
39
- 'android:screenOrientation': 'portrait',
40
36
  'android:theme': '@style/Theme.AppCompat.Light.NoActionBar'
41
37
  }
42
38
  });
43
39
  }
44
40
 
45
- // 3. Services (ConnectionService & FCM)
46
- application.service = application.service || [];
41
+ // AcceptCallActivity (The "Trampoline" that fixes the Answer button)
42
+ if (!application.activity.some(a => a.$['android:name'] === 'com.rnsnativecall.AcceptCallActivity')) {
43
+ application.activity.push({
44
+ $: {
45
+ 'android:name': 'com.rnsnativecall.AcceptCallActivity',
46
+ 'android:theme': '@android:style/Theme.Translucent.NoTitleBar',
47
+ 'android:excludeFromRecents': 'true',
48
+ 'android:noHistory': 'true',
49
+ 'android:exported': 'false',
50
+ 'android:launchMode': 'singleInstance'
51
+ }
52
+ });
53
+ }
47
54
 
55
+ // 3. Services (FCM and Telecom)
56
+ application.service = application.service || [];
48
57
  const services = [
49
- {
50
- name: 'com.rnsnativecall.MyConnectionService',
51
- permission: 'android.permission.BIND_CONNECTION_SERVICE',
52
- action: 'android.telecom.ConnectionService',
53
- exported: 'true'
54
- },
55
- {
56
- name: 'com.rnsnativecall.CallMessagingService',
57
- action: 'com.google.firebase.MESSAGING_EVENT',
58
- exported: 'false'
59
- }
58
+ { name: 'com.rnsnativecall.MyConnectionService', permission: 'android.permission.BIND_CONNECTION_SERVICE', action: 'android.telecom.ConnectionService' },
59
+ { name: 'com.rnsnativecall.CallMessagingService', action: 'com.google.firebase.MESSAGING_EVENT' }
60
60
  ];
61
61
 
62
62
  services.forEach(svc => {
63
63
  if (!application.service.some(s => s.$['android:name'] === svc.name)) {
64
- const serviceObj = {
65
- $: { 'android:name': svc.name, 'android:exported': svc.exported },
64
+ application.service.push({
65
+ $: { 'android:name': svc.name, 'android:exported': svc.permission ? 'true' : 'false', 'android:permission': svc.permission },
66
66
  'intent-filter': [{ action: [{ $: { 'android:name': svc.action } }] }]
67
- };
68
- if (svc.permission) serviceObj.$['android:permission'] = svc.permission;
69
- application.service.push(serviceObj);
67
+ });
70
68
  }
71
69
  });
72
70
 
71
+ // 4. Receivers (The Decline Button)
72
+ application.receiver = application.receiver || [];
73
+ if (!application.receiver.some(r => r.$['android:name'] === 'com.rnsnativecall.CallActionReceiver')) {
74
+ application.receiver.push({
75
+ $: { 'android:name': 'com.rnsnativecall.CallActionReceiver', 'android:exported': 'false' }
76
+ });
77
+ }
78
+
73
79
  return config;
74
80
  });
75
81
  }
76
82
 
77
- /**
78
- * IOS CONFIGURATION
79
- */
83
+ /** IOS Config remains the same **/
80
84
  function withIosConfig(config) {
81
85
  return withInfoPlist(config, (config) => {
82
86
  const infoPlist = config.modResults;
83
-
84
- // 1. Add background modes
85
- if (!infoPlist.UIBackgroundModes) {
86
- infoPlist.UIBackgroundModes = [];
87
- }
88
-
89
- ['voip', 'audio'].forEach((mode) => {
90
- if (!infoPlist.UIBackgroundModes.includes(mode)) {
91
- infoPlist.UIBackgroundModes.push(mode);
92
- }
93
- });
94
-
95
- // 2. Dynamic Microphone Description
96
- const appName = config.name || 'this app';
97
- infoPlist.NSMicrophoneUsageDescription =
98
- infoPlist.NSMicrophoneUsageDescription || `Allow ${appName} to access your microphone for calls.`;
99
-
87
+ if (!infoPlist.UIBackgroundModes) infoPlist.UIBackgroundModes = [];
88
+ ['voip', 'audio'].forEach(mode => { if (!infoPlist.UIBackgroundModes.includes(mode)) infoPlist.UIBackgroundModes.push(mode); });
100
89
  return config;
101
90
  });
102
91
  }
103
92
 
104
93
  module.exports = (config) => {
105
- return withPlugins(config, [
106
- withAndroidConfig,
107
- withIosConfig,
108
- ]);
109
- };
94
+ return withPlugins(config, [withAndroidConfig, withIosConfig]);
95
+ };