rns-nativecall 0.6.2 → 0.6.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.
@@ -0,0 +1,55 @@
1
+ package com.rnsnativecall
2
+
3
+ import android.app.Activity
4
+ import android.app.KeyguardManager
5
+ import android.content.Context
6
+ import android.content.Intent
7
+ import android.os.Build
8
+ import android.os.Bundle
9
+ import android.view.WindowManager
10
+
11
+ class UnlockPromptActivity : Activity() {
12
+ override fun onCreate(savedInstanceState: Bundle?) {
13
+ super.onCreate(savedInstanceState)
14
+
15
+ // Ensure this activity shows over the lockscreen to trigger the prompt
16
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {
17
+ setShowWhenLocked(true)
18
+ setTurnScreenOn(true)
19
+ } else {
20
+ window.addFlags(
21
+ WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED or
22
+ WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON
23
+ )
24
+ }
25
+
26
+ val km = getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager
27
+
28
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
29
+ km.requestDismissKeyguard(this, object : KeyguardManager.KeyguardDismissCallback() {
30
+ override fun onDismissSucceeded() {
31
+ super.onDismissSucceeded()
32
+ // Unlock successful, launch the main app
33
+ val launchIntent = packageManager.getLaunchIntentForPackage(packageName)
34
+ startActivity(launchIntent)
35
+ finish()
36
+ }
37
+
38
+ override fun onDismissCancelled() {
39
+ super.onDismissCancelled()
40
+ finish()
41
+ }
42
+
43
+ override fun onDismissError() {
44
+ super.onDismissError()
45
+ finish()
46
+ }
47
+ })
48
+ } else {
49
+ // Fallback for older Android versions
50
+ val launchIntent = packageManager.getLaunchIntentForPackage(packageName)
51
+ startActivity(launchIntent)
52
+ finish()
53
+ }
54
+ }
55
+ }
package/app.plugin.js CHANGED
@@ -1,4 +1,4 @@
1
- ///app.plugin.js
1
+ ////Users/bush/Desktop/Apps/Raiidr/package/app.plugin.js
2
2
  const withNativeCallVoip = require('./withNativeCallVoip');
3
3
 
4
4
  module.exports = function (config) {
package/index.d.ts CHANGED
@@ -2,50 +2,37 @@ export interface CallData {
2
2
  callUuid: string;
3
3
  name?: string;
4
4
  callType?: 'audio' | 'video';
5
- [key: string]: any; // To allow for custom FCM payload data
5
+ isBusySignal?: boolean;
6
+ [key: string]: any;
6
7
  }
7
8
 
8
- export type CallAcceptedCallback = (data: CallData) => void;
9
- export type CallRejectedCallback = (data: CallData) => void;
10
- export type CallFailedCallback = (data: any) => void;
11
-
12
- /**
13
- * Manually request/check Android permissions for Telecom.
14
- */
15
- export function ensureAndroidPermissions(): Promise<boolean>;
9
+ export type CallEventType = 'INCOMING_CALL' | 'BUSY';
16
10
 
17
11
  export interface CallHandlerType {
18
12
  /**
19
- * Display the Sticky Pill notification UI.
20
- * @param uuid Unique call identifier
21
- * @param name Caller display name
22
- * @param callType 'audio' or 'video'
13
+ * Registers the Headless JS task.
14
+ * Handles Busy signals and Call Validity automatically.
23
15
  */
16
+ registerHeadlessTask(
17
+ callback: (data: CallData, eventType: CallEventType) => Promise<void>
18
+ ): void;
19
+
24
20
  displayCall(
25
21
  uuid: string,
26
22
  name: string,
27
23
  callType: 'audio' | 'video',
28
24
  ): Promise<boolean>;
29
25
 
30
- /**
31
- * Dismiss the native call UI (Sticky Pill).
32
- */
33
26
  destroyNativeCallUI(uuid: string): void;
34
27
 
28
+ getInitialCallData(): Promise<any | null>;
35
29
 
36
- getInitialCallData(): Promise<CallData | null>;
37
- /**
38
- * Subscribe to call events. Automatically checks for cold-start data
39
- * if the app was opened via the Answer button.
40
- * @returns Function to unsubscribe
41
- */
42
30
  subscribe(
43
- onAccept: CallAcceptedCallback,
44
- onReject: CallRejectedCallback,
45
- onFailed?: CallFailedCallback
31
+ onAccept: (data: CallData) => void,
32
+ onReject: (data: CallData) => void,
33
+ onFailed?: (data: any) => void
46
34
  ): () => void;
47
35
  }
48
36
 
49
37
  declare const CallHandler: CallHandlerType;
50
-
51
38
  export default CallHandler;
package/index.js CHANGED
@@ -1,13 +1,48 @@
1
1
  import {
2
2
  NativeModules,
3
3
  NativeEventEmitter,
4
+ AppRegistry,
4
5
  } from 'react-native';
5
6
 
6
7
  const { CallModule } = NativeModules;
7
-
8
8
  const callEventEmitter = CallModule ? new NativeEventEmitter(CallModule) : null;
9
9
 
10
10
  export const CallHandler = {
11
+ /**
12
+ * INTERNAL GATEKEEPER: Moves headless logic into the package.
13
+ * The dev just passes their "NativeCall" function.
14
+ */
15
+ registerHeadlessTask: (onAction) => {
16
+ AppRegistry.registerHeadlessTask('ColdStartCallTask', () => async (data) => {
17
+ const { callUuid, isBusySignal } = data;
18
+ const uuid = callUuid?.toLowerCase().trim();
19
+
20
+ try {
21
+ // 1. Handle Busy Signal (Second Caller)
22
+ if (isBusySignal) {
23
+ console.log(`[RNSNativeCall] System busy. Informing app to send busy signal for: ${uuid}`);
24
+ if (onAction) await onAction(data, 'BUSY');
25
+ return;
26
+ }
27
+
28
+ // 2. Gatekeeping: Check Validity with Native logic
29
+ const status = await CallModule.checkCallValidity(uuid);
30
+ if (!status.isValid) {
31
+ console.log(`[RNSNativeCall] Aborting task: ${uuid} no longer valid or canceled.`);
32
+ if (onAction) await onAction(data, 'ABORTED_CALL');
33
+ return;
34
+ }
35
+
36
+ // 3. Valid Incoming Call: Pass to Developer's UI logic
37
+ if (onAction) {
38
+ await onAction(data, 'INCOMING_CALL');
39
+ }
40
+ } catch (error) {
41
+ console.error('[RNSNativeCall] Headless Task Error:', error);
42
+ }
43
+ });
44
+ },
45
+
11
46
  displayCall: async (uuid, name, callType = "audio") => {
12
47
  if (!CallModule) return false;
13
48
  return await CallModule.displayIncomingCall(uuid.toLowerCase().trim(), name, callType);
@@ -19,10 +54,6 @@ export const CallHandler = {
19
54
  }
20
55
  },
21
56
 
22
- /**
23
- * Manually check for cold-start data (App killed -> Answered).
24
- * Call this in your App.js useEffect.
25
- */
26
57
  getInitialCallData: async () => {
27
58
  if (!CallModule?.getInitialCallData) return null;
28
59
  return await CallModule.getInitialCallData();
@@ -40,11 +71,6 @@ export const CallHandler = {
40
71
  subs.push(callEventEmitter.addListener('onCallFailed', onFailed));
41
72
  }
42
73
 
43
- // Auto-check on subscribe for convenience
44
- CallHandler.getInitialCallData().then((data) => {
45
- if (data) onAccept(data);
46
- });
47
-
48
74
  return () => subs.forEach(s => s.remove());
49
75
  }
50
76
  };
package/package.json CHANGED
@@ -1,37 +1,28 @@
1
1
  {
2
2
  "name": "rns-nativecall",
3
- "version": "0.6.2",
4
- "description": "RNS nativecall component with native Android/iOS for handling native call ui, when app is not open or open.",
3
+ "version": "0.6.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",
7
7
  "homepage": "https://github.com/raiidr/rns-nativecall",
8
8
  "license": "MIT",
9
- "author": {
10
- "name": "Your Name",
11
- "email": "your.email@example.com"
12
- },
9
+ "author": "Your Name <your.email@example.com>",
13
10
  "repository": {
14
11
  "type": "git",
15
- "url": "https://github.com/raiidr/rns-nativecall.git"
12
+ "url": "git+https://github.com/raiidr/rns-nativecall.git"
16
13
  },
17
- "react-native": "index.js",
18
14
  "scripts": {
19
15
  "p": "npm publish --access public"
20
16
  },
21
- "expo": {
22
- "autolink": true,
23
- "plugins": [
24
- "./app.plugin.js"
25
- ]
26
- },
27
17
  "keywords": [
28
18
  "react-native",
29
19
  "nativecall",
30
- "call",
31
- "native",
32
- "call keep",
20
+ "voip",
21
+ "callkit",
33
22
  "android",
34
- "expo"
23
+ "ios",
24
+ "expo",
25
+ "config-plugin"
35
26
  ],
36
27
  "files": [
37
28
  "android",
@@ -39,18 +30,15 @@
39
30
  "app.plugin.js",
40
31
  "index.d.ts",
41
32
  "index.js",
42
- "package.json",
43
33
  "react-native.config.js",
44
- "README.md",
45
34
  "rns-nativecall.podspec",
46
35
  "withNativeCallVoip.js"
47
36
  ],
48
37
  "peerDependencies": {
49
- "expo": "*",
50
- "react-native": "*"
38
+ "react-native": ">=0.60.0",
39
+ "expo": ">=45.0.0"
51
40
  },
52
41
  "dependencies": {
53
- "@expo/config-plugins": "~10.1.1",
54
- "rns-nativecall": "^0.5.9"
42
+ "@expo/config-plugins": "^9.0.0"
55
43
  }
56
44
  }
@@ -10,7 +10,7 @@ Pod::Spec.new do |s|
10
10
  s.license = package["license"]
11
11
  s.authors = package["author"]
12
12
  s.platforms = { :ios => "10.0" }
13
- s.source = { :git => "https://github.com/your-repo.git", :tag => "#{s.version}" }
13
+ s.source = { :git => "https://github.com/raiidr.git", :tag => "#{s.version}" }
14
14
 
15
15
  s.source_files = "ios/**/*.{h,m,mm}"
16
16
 
@@ -5,133 +5,147 @@ function withMainActivityDataFix(config) {
5
5
  return withMainActivity(config, (config) => {
6
6
  let contents = config.modResults.contents;
7
7
 
8
- const imports = [
9
- 'import android.view.WindowManager',
10
- 'import android.os.Build',
11
- 'import android.os.Bundle',
12
- 'import android.content.Intent',
13
- 'import android.content.Context' // Added: Required for KEYGUARD_SERVICE
14
- ];
15
-
16
- imports.forEach(imp => {
17
- if (!contents.includes(imp)) {
18
- contents = contents.replace(/package .*/, (match) => `${match}\n${imp}`);
19
- }
20
- });
21
-
22
- const wakeLogic = `super.onCreate(savedInstanceState)
23
- if (intent?.getBooleanExtra("navigatingToCall", false) == true) {
24
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {
25
- setShowWhenLocked(true)
26
- setTurnScreenOn(true)
27
- } else {
28
- window.addFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED or WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON)
8
+ // Ensure imports exist
9
+ if (!contents.includes('import android.content.Intent')) {
10
+ contents = contents.replace(/package .*/, (match) => `${match}\n\nimport android.content.Intent`);
29
11
  }
30
-
31
- val keyguardManager = getSystemService(Context.KEYGUARD_SERVICE) as? android.app.KeyguardManager
32
- keyguardManager?.requestDismissKeyguard(this, null)
33
- }`;
34
-
35
- if (!contents.includes('setShowWhenLocked')) {
36
- contents = contents.replace(/super\.onCreate\(.*\)/, wakeLogic);
12
+ if (!contents.includes('import android.os.Bundle')) {
13
+ contents = contents.replace(/package .*/, (match) => `${match}\n\nimport android.os.Bundle`);
37
14
  }
38
15
 
39
- if (!contents.includes('override fun onNewIntent')) {
40
- const onNewIntentCode = `
16
+ const onNewIntentCode = `
41
17
  override fun onNewIntent(intent: Intent) {
42
18
  super.onNewIntent(intent)
43
19
  setIntent(intent)
44
- }\n`;
20
+
21
+ // Only fire JS event if the user actually pressed "Answer"
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
+ if (dataMap.isNotEmpty() && isAnswerAction) {
30
+ com.rnsnativecall.CallModule.setPendingCallData(dataMap)
31
+ com.rnsnativecall.CallModule.sendEventToJS("onCallAccepted", dataMap)
32
+ }
33
+ }
34
+ `;
35
+
36
+ const onCreateCode = `
37
+ override fun onCreate(savedInstanceState: Bundle?) {
38
+ super.onCreate(savedInstanceState)
39
+
40
+ val isAnswerAction = intent.action == "com.rnsnativecall.ACTION_ANSWER"
41
+
42
+ // Logic for Cold Start (App was dead, user answered)
43
+ if (isAnswerAction) {
44
+ val dataMap = mutableMapOf<String, String>()
45
+ intent.extras?.keySet()?.forEach { key ->
46
+ dataMap[key] = intent.extras?.get(key)?.toString() ?: ""
47
+ }
48
+ com.rnsnativecall.CallModule.setPendingCallData(dataMap)
49
+ }
50
+
51
+ // Move to back if it's a background wake (FCM) and NOT an answer click
52
+ if (intent.getBooleanExtra("background_wake", false) && !isAnswerAction) {
53
+ moveTaskToBack(true)
54
+ }
55
+ }
56
+ `;
57
+
58
+ // Inject codes
59
+ if (!contents.includes('override fun onNewIntent')) {
45
60
  const lastBraceIndex = contents.lastIndexOf('}');
46
61
  contents = contents.slice(0, lastBraceIndex) + onNewIntentCode + contents.slice(lastBraceIndex);
47
62
  }
63
+ if (!contents.includes('override fun onCreate')) {
64
+ const lastBraceIndex = contents.lastIndexOf('}');
65
+ contents = contents.slice(0, lastBraceIndex) + onCreateCode + contents.slice(lastBraceIndex);
66
+ }
48
67
 
49
68
  config.modResults.contents = contents;
50
69
  return config;
51
70
  });
52
71
  }
53
-
54
72
  /** 2. ANDROID MANIFEST CONFIG **/
55
73
  function withAndroidConfig(config) {
56
74
  return withAndroidManifest(config, (config) => {
57
- const androidManifest = config.modResults.manifest;
58
- const mainApplication = androidManifest.application[0];
59
-
60
- // Permissions
75
+ const manifest = config.modResults;
76
+ const application = manifest.manifest.application[0];
61
77
  const permissions = [
62
78
  'android.permission.USE_FULL_SCREEN_INTENT',
63
79
  'android.permission.VIBRATE',
64
80
  'android.permission.FOREGROUND_SERVICE',
65
81
  'android.permission.FOREGROUND_SERVICE_PHONE_CALL',
66
82
  'android.permission.POST_NOTIFICATIONS',
83
+ 'android.permission.SYSTEM_ALERT_WINDOW',
67
84
  'android.permission.WAKE_LOCK',
68
- 'android.permission.MANAGE_OWN_CALLS',
69
- 'android.permission.DISABLE_KEYGUARD',
70
- 'android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS',
71
- 'android.permission.RECEIVE_BOOT_COMPLETED'
85
+ 'android.permission.DISABLE_KEYGUARD'
72
86
  ];
73
-
74
- androidManifest['uses-permission'] = androidManifest['uses-permission'] || [];
87
+ manifest.manifest['uses-permission'] = manifest.manifest['uses-permission'] || [];
75
88
  permissions.forEach((perm) => {
76
- if (!androidManifest['uses-permission'].some((p) => p.$['android:name'] === perm)) {
77
- androidManifest['uses-permission'].push({ $: { 'android:name': perm } });
89
+ if (!manifest.manifest['uses-permission'].some((p) => p.$['android:name'] === perm)) {
90
+ manifest.manifest['uses-permission'].push({ $: { 'android:name': perm } });
78
91
  }
79
92
  });
80
-
81
- // Activity Registration: AcceptCallActivity
82
- mainApplication.activity = mainApplication.activity || [];
83
- if (!mainApplication.activity.some(a => a.$['android:name'] === 'com.rnsnativecall.AcceptCallActivity')) {
84
- mainApplication.activity.push({
93
+ application.activity = application.activity || [];
94
+ if (!application.activity.some(a => a.$['android:name'] === 'com.rnsnativecall.AcceptCallActivity')) {
95
+ application.activity.push({
85
96
  $: {
86
97
  'android:name': 'com.rnsnativecall.AcceptCallActivity',
87
98
  'android:theme': '@android:style/Theme.Translucent.NoTitleBar',
88
- 'android:exported': 'true', // CHANGE TO TRUE
99
+ 'android:excludeFromRecents': 'true',
100
+ 'android:noHistory': 'true',
101
+ 'android:exported': 'false',
102
+ 'android:launchMode': 'singleInstance',
89
103
  'android:showWhenLocked': 'true',
90
- 'android:turnScreenOn': 'true',
91
- 'android:launchMode': 'singleInstance' // Add this to prevent multiple instances
104
+ 'android:turnScreenOn': 'true'
92
105
  }
93
106
  });
94
107
  }
95
-
96
- // Receiver Registration: CallActionReceiver
97
- mainApplication.receiver = mainApplication.receiver || [];
98
- if (!mainApplication.receiver.some(r => r.$['android:name'] === 'com.rnsnativecall.CallActionReceiver')) {
99
- mainApplication.receiver.push({
108
+ if (!application.activity.some(a => a.$['android:name'] === 'com.rnsnativecall.UnlockPromptActivity')) {
109
+ application.activity.push({
100
110
  $: {
101
- 'android:name': 'com.rnsnativecall.CallActionReceiver',
102
- 'android:exported': 'false'
111
+ 'android:name': 'com.rnsnativecall.UnlockPromptActivity',
112
+ 'android:theme': '@android:style/Theme.Translucent.NoTitleBar',
113
+ 'android:excludeFromRecents': 'true',
114
+ 'android:noHistory': 'true',
115
+ 'android:exported': 'false',
116
+ 'android:launchMode': 'singleInstance',
117
+ 'android:showWhenLocked': 'true',
118
+ 'android:turnScreenOn': 'true'
103
119
  }
104
120
  });
105
121
  }
106
-
107
- mainApplication.service = mainApplication.service || [];
108
-
109
- // Messaging Service
110
- if (!mainApplication.service.some(s => s.$['android:name'] === 'com.rnsnativecall.CallMessagingService')) {
111
- mainApplication.service.push({
112
- $: { 'android:name': 'com.rnsnativecall.CallMessagingService', 'android:exported': 'false' },
122
+ application.service = application.service || [];
123
+ const firebaseServiceName = 'com.rnsnativecall.CallMessagingService';
124
+ if (!application.service.some(s => s.$['android:name'] === firebaseServiceName)) {
125
+ application.service.push({
126
+ $: { 'android:name': firebaseServiceName, 'android:exported': 'false' },
113
127
  'intent-filter': [{ action: [{ $: { 'android:name': 'com.google.firebase.MESSAGING_EVENT' } }] }]
114
128
  });
115
129
  }
116
-
117
- // Headless Task Service (Android 14)
118
- const headlessName = 'com.rnsnativecall.CallHeadlessTask';
119
- const headlessIndex = mainApplication.service.findIndex(s => s.$['android:name'] === headlessName);
120
- const headlessEntry = {
121
- $: {
122
- 'android:name': headlessName,
123
- 'android:exported': 'false',
124
- 'android:foregroundServiceType': 'phoneCall'
125
- }
126
- };
127
-
128
- if (headlessIndex > -1) mainApplication.service[headlessIndex] = headlessEntry;
129
- else mainApplication.service.push(headlessEntry);
130
+ const headlessServiceName = 'com.rnsnativecall.CallHeadlessTask';
131
+ if (!application.service.some(s => s.$['android:name'] === headlessServiceName)) {
132
+ application.service.push({
133
+ $: { 'android:name': headlessServiceName, 'android:exported': 'false' }
134
+ });
135
+ }
136
+ application.receiver = application.receiver || [];
137
+ const receiverName = 'com.rnsnativecall.CallActionReceiver';
138
+ if (!application.receiver.some(r => r.$['android:name'] === receiverName)) {
139
+ application.receiver.push({
140
+ $: { 'android:name': receiverName, 'android:exported': 'false' }
141
+ });
142
+ }
130
143
 
131
144
  return config;
132
145
  });
133
146
  }
134
147
 
148
+ /** 3. IOS CONFIG **/
135
149
  function withIosConfig(config) {
136
150
  return withInfoPlist(config, (config) => {
137
151
  const infoPlist = config.modResults;