rns-nativecall 1.1.5 → 1.1.6

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
@@ -130,31 +130,32 @@ export default function App() {
130
130
  ### 📖 API Reference
131
131
  # rns-nativecall API Reference
132
132
 
133
- | Method | Platform | Description |
134
- |:-----------------------------|:------------|:--------------------------------------------------------------------------------------|
135
- | checkFullScreenPermission() | Android 14+ | Returns true if the app is allowed to show calls over the lockscreen (FSI). |
136
- | openFullScreenSettings() | Android 14+ | Deep-links the user to the specific system page to enable Full-Screen Intent. |
137
- | checkOverlayPermission() | Android | Returns true if the app can draw over other apps (Overlay/Draw over apps). |
138
- | requestOverlayPermission() | Android | Opens System Settings to let the user enable "Draw over other apps". |
139
- | displayCall(uuid, name, type)| All | Launches the native call UI (CallStyle on Android / CallKit on iOS). |
140
- | checkCallValidity(uuid) | All | Returns boolean values for `isValid` and `isCanceled` to prevent "ghost" calls. |
141
- | checkCallStatus(uuid) | All | Detailed check of a call's status; useful for determining if UI should remain active. |
142
- | reportRemoteEnded(uuid, res) | All | Notifies the native side that the caller hung up (prevents infinite ringing). |
143
- | destroyNativeCallUI(uuid) | All | Forcefully dismisses the native call interface and stops the ringtone immediately. |
144
- | getInitialCallData() | All | Returns call data if the app was launched by clicking "Answer" from a killed state. |
145
- | subscribe(onAccept, onReject)| All | Listens for native button presses (Answer/End/Failed) and returns a cleanup function. |
133
+ | Method | Platform | Description |
134
+ | :--- | :--- | :--- |
135
+ | **registerHeadlessTask(callback)** | Android | Registers background logic. `eventType` is `INCOMING_CALL` or `BUSY`. |
136
+ | **checkOverlayPermission()** | Android | Returns true if the app can draw over other apps (Required for Android Lockscreen). |
137
+ | **requestOverlayPermission()** | Android | Opens System Settings to let the user enable "Draw over other apps". |
138
+ | **displayCall(uuid, name, type)** | All | Shows the native call UI (Standard Notification on Android / CallKit on iOS). |
139
+ | **checkCallValidity(uuid)** | All | Returns boolean values for `isValid` and `isCanceled`. |
140
+ | **stopForegroundService()** | Android | Stops the ongoing service and clears the persistent notification/pill. |
141
+ | **destroyNativeCallUI(uuid)** | All | Dismisses the native call interface and stops the ringtone. |
142
+ | **getInitialCallData()** | All | Returns call data if the app was launched by clicking `Answer` from a killed state. |
143
+ | **subscribe(onAccept, onReject)** | All | Listens for native button presses (Answer/End). |
146
144
 
147
145
  ---
148
146
 
149
147
  # Implementation Notes
150
148
 
151
- 1. Android Overlay:
149
+ 1. Android Persistence:
150
+ Because this library uses a Foreground Service on Android, the notification will persist and show a "Call Pill" in the status bar. To remove this after the call ends or connects, you MUST call 'CallHandler.stopForegroundService()'.
151
+
152
+ 2. Android Overlay:
152
153
  For your React Native call screen to show up when the phone is locked, the user must grant the "Overlay Permission". Use 'checkOverlayPermission()' and 'requestOverlayPermission()' during your app's onboarding or call initiation.
153
154
 
154
- 2. iOS CallKit:
155
+ 3. iOS CallKit:
155
156
  On iOS, 'displayCall' uses the native system CallKit UI. This works automatically in the background and on the lockscreen without extra overlay permissions.
156
157
 
157
- 3. Single Call Gate:
158
+ 4. Single Call Gate:
158
159
  The library automatically prevents multiple overlapping native UIs. If a call is already active, subsequent calls will trigger the 'BUSY' event in your Headless Task.
159
160
  ---
160
161
 
@@ -61,6 +61,7 @@ class CallModule(
61
61
  }
62
62
  }
63
63
 
64
+ // Inside your CallModule class
64
65
  @ReactMethod
65
66
  fun checkOverlayPermission(promise: Promise) {
66
67
  if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
@@ -70,16 +71,6 @@ class CallModule(
70
71
  }
71
72
  }
72
73
 
73
- @ReactMethod
74
- fun reportRemoteEnded(
75
- uuid: String,
76
- reason: String,
77
- promise: Promise,
78
- ) {
79
- NativeCallManager.dismissIncomingCall(reactApplicationContext, uuid)
80
- promise.resolve(true)
81
- }
82
-
83
74
  @ReactMethod
84
75
  fun requestOverlayPermission() {
85
76
  if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
@@ -95,39 +86,10 @@ class CallModule(
95
86
  }
96
87
  }
97
88
 
98
- @ReactMethod
99
- fun checkFullScreenIntentPermission(promise: Promise) {
100
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
101
- val notificationManager = reactApplicationContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
102
- promise.resolve(notificationManager.canUseFullScreenIntent())
103
- } else {
104
- // Automatically true for Android 13 and below
105
- promise.resolve(true)
106
- }
107
- }
108
-
109
- @ReactMethod
110
- fun openFullScreenIntentSettings() {
111
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
112
- try {
113
- val intent =
114
- Intent(Settings.ACTION_MANAGE_APP_USE_FULL_SCREEN_INTENT).apply {
115
- data = Uri.fromParts("package", reactApplicationContext.packageName, null)
116
- addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
117
- }
118
- reactApplicationContext.startActivity(intent)
119
- } catch (e: Exception) {
120
- // Fallback for some OEM skins that might not support the direct intent
121
- val intent =
122
- Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
123
- data = Uri.fromParts("package", reactApplicationContext.packageName, null)
124
- addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
125
- }
126
- reactApplicationContext.startActivity(intent)
127
- }
128
- }
129
- }
130
-
89
+ /**
90
+ * Combined Validity Check:
91
+ * Used by Headless Task to see if it should proceed with the UI.
92
+ */
131
93
  @ReactMethod
132
94
  fun checkCallValidity(
133
95
  uuid: String,
@@ -145,6 +107,22 @@ class CallModule(
145
107
  promise.resolve(map)
146
108
  }
147
109
 
110
+ @ReactMethod
111
+ fun stopForegroundService(promise: Promise) {
112
+ try {
113
+ // Using the current react context to stop the service
114
+ val intent = Intent(reactApplicationContext, CallForegroundService::class.java)
115
+ reactApplicationContext.stopService(intent)
116
+ promise.resolve(true)
117
+ } catch (e: Exception) {
118
+ promise.reject("SERVICE_STOP_ERROR", e.message)
119
+ }
120
+ }
121
+
122
+ /**
123
+ * General Status Check:
124
+ * Useful for checking if the UI is still relevant.
125
+ */
148
126
  @ReactMethod
149
127
  fun checkCallStatus(
150
128
  uuid: String,
@@ -14,7 +14,7 @@ import androidx.core.app.Person
14
14
  import androidx.core.content.ContextCompat
15
15
 
16
16
  object NativeCallManager {
17
- const val channelId = "CALL_CHANNEL_V0_URGENT"
17
+ const val channelId = "CALL_CHANNEL_V15_URGENT"
18
18
  private var currentCallData: Map<String, String>? = null
19
19
 
20
20
  @JvmStatic internal var pendingCallNotification: Notification? = null
@@ -102,7 +102,6 @@ object NativeCallManager {
102
102
  .setOngoing(true)
103
103
  .setFullScreenIntent(fullScreenPendingIntent, true)
104
104
  .setStyle(NotificationCompat.CallStyle.forIncomingCall(caller, rejectPendingIntent, answerPendingIntent))
105
- .setAutoCancel(false)
106
105
  .setForegroundServiceBehavior(NotificationCompat.FOREGROUND_SERVICE_IMMEDIATE)
107
106
 
108
107
  val notification = builder.build()
@@ -118,14 +117,6 @@ object NativeCallManager {
118
117
  }
119
118
  }
120
119
 
121
- fun refreshNotificationOnly(
122
- context: Context,
123
- uuid: String,
124
- ) {
125
- val manager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
126
- manager.cancel(uuid.hashCode())
127
- }
128
-
129
120
  fun dismissIncomingCall(
130
121
  context: Context,
131
122
  uuid: String?,
package/index.d.ts CHANGED
@@ -48,7 +48,7 @@ export interface CallHandlerType {
48
48
  /**
49
49
  * Registers the background "Headless" task.
50
50
  * This is the bridge between a Native push notification and your React Native UI.
51
- * @param callback Async function receiving call data and the event type.
51
+ * * @param callback Async function receiving call data and the event type.
52
52
  */
53
53
  registerHeadlessTask(
54
54
  callback: (data: CallData, eventType: CallEventType) => Promise<void>
@@ -57,7 +57,7 @@ export interface CallHandlerType {
57
57
  /**
58
58
  * Displays the full-screen Native Incoming Call UI.
59
59
  * Usually called inside registerHeadlessTask after validating the call.
60
- * @param uuid The unique call ID.
60
+ * * @param uuid The unique call ID.
61
61
  * @param name Name to display on the call screen.
62
62
  * @param callType Defaults to 'audio'.
63
63
  * @returns Promise resolving to true if the UI was successfully launched.
@@ -65,7 +65,7 @@ export interface CallHandlerType {
65
65
  displayCall(
66
66
  uuid: string,
67
67
  name: string,
68
- callType?: 'audio' | 'video',
68
+ callType: 'audio' | 'video',
69
69
  ): Promise<boolean>;
70
70
 
71
71
  /** * Checks if a call is still valid and has not been canceled.
@@ -78,36 +78,12 @@ export interface CallHandlerType {
78
78
  */
79
79
  checkCallStatus(uuid: string): Promise<CallStatus>;
80
80
 
81
- /**
82
- * [Android 14+] Checks if the user has granted the special "Full Screen Intent" permission.
83
- * This is required for the call to show over the lockscreen.
84
- */
85
- checkFullScreenPermission(): Promise<boolean>;
81
+ requestOverlayPermission(): Promise<boolean>;
86
82
 
87
- /**
88
- * [Android 14+] Opens the specific system settings page for the user to
89
- * manually enable "Full Screen Intent" if it was revoked.
90
- */
91
- openFullScreenSettings(): void;
83
+ reportRemoteEnded(uuid: string, reason: number): void;
92
84
 
93
- /**
94
- * Checks if the app has permission to draw over other apps (Overlay).
95
- * Necessary for showing the "Pill" or answer buttons while the phone is unlocked.
96
- */
97
85
  checkOverlayPermission(): Promise<boolean>;
98
86
 
99
- /**
100
- * Opens system settings to request the "Draw over other apps" (Overlay) permission.
101
- */
102
- requestOverlayPermission(): void;
103
-
104
- /**
105
- * Reports to the native side that the remote party has terminated the call.
106
- * @param uuid The unique call ID.
107
- * @param reason Custom string reason (e.g., "RemoteEnded", "TimedOut").
108
- */
109
- reportRemoteEnded(uuid: string, reason?: string): Promise<void>;
110
-
111
87
  /**
112
88
  * Forcefully dismisses the native call UI, stops the ringtone, and clears state.
113
89
  * Use this when the call is hung up or timed out.
@@ -129,7 +105,7 @@ export interface CallHandlerType {
129
105
 
130
106
  /**
131
107
  * Subscribes to user interactions on the Native Call UI.
132
- * @param onAccept Callback when user presses the Answer button.
108
+ * * @param onAccept Callback when user presses the Answer button.
133
109
  * @param onReject Callback when user presses the Decline button.
134
110
  * @param onFailed Callback for system errors (optional).
135
111
  * @returns A cleanup function to unsubscribe.
package/index.js CHANGED
@@ -2,18 +2,12 @@ import {
2
2
  NativeModules,
3
3
  NativeEventEmitter,
4
4
  AppRegistry,
5
- Platform,
6
5
  } from 'react-native';
7
6
 
8
7
  const { CallModule } = NativeModules;
9
8
  const callEventEmitter = CallModule ? new NativeEventEmitter(CallModule) : null;
10
9
 
11
10
  export const CallHandler = {
12
- /**
13
- * REGISTER HEADLESS TASK
14
- * This is the "Engine" that listens for incoming push notifications
15
- * while the app is killed or in the background.
16
- */
17
11
  registerHeadlessTask: (onAction) => {
18
12
  AppRegistry.registerHeadlessTask('ColdStartCallTask', () => async (data) => {
19
13
  const { callUuid, isBusySignal } = data;
@@ -25,81 +19,64 @@ export const CallHandler = {
25
19
  return;
26
20
  }
27
21
 
28
- // Check if the call is still valid before showing UI
22
+ // CallModule.checkCallValidity is now accessible via the native bridge
29
23
  const status = await CallModule.checkCallValidity(uuid);
30
24
  if (!status.isValid) {
31
25
  if (onAction) await onAction(data, 'ABORTED_CALL');
32
26
  return;
33
27
  }
34
28
 
35
- if (onAction) await onAction(data, 'INCOMING_CALL');
29
+ if (onAction) {
30
+ await onAction(data, 'INCOMING_CALL');
31
+ }
36
32
  } catch (error) {
37
33
  console.error('[RNSNativeCall] Headless Task Error:', error);
38
34
  }
39
35
  });
40
36
  },
41
37
 
42
- // --- PERMISSION HEALTH CHECKS ---
43
-
44
- checkFullScreenPermission: async () => {
45
- if (Platform.OS !== 'android' || !CallModule?.checkFullScreenIntentPermission) return true;
46
- return await CallModule.checkFullScreenIntentPermission();
38
+ // --- Added missing bridge methods ---
39
+ checkCallValidity: async (uuid) => {
40
+ if (!CallModule?.checkCallValidity) return { isValid: false, isCanceled: true };
41
+ return await CallModule.checkCallValidity(uuid.toLowerCase().trim());
47
42
  },
48
43
 
49
- openFullScreenSettings: () => {
50
- if (Platform.OS === 'android' && CallModule?.openFullScreenIntentSettings) {
51
- CallModule.openFullScreenIntentSettings();
52
- }
44
+ checkCallStatus: async (uuid) => {
45
+ if (!CallModule?.checkCallStatus) return { isCanceled: true, isActive: false, shouldDisplay: false };
46
+ return await CallModule.checkCallStatus(uuid.toLowerCase().trim());
53
47
  },
48
+ // ------------------------------------
54
49
 
55
- checkOverlayPermission: async () => {
56
- if (Platform.OS !== 'android' || !CallModule?.checkOverlayPermission) return true;
57
- return await CallModule.checkOverlayPermission();
50
+ reportRemoteEnded: async (uuid, endReason) => {
51
+ if (!CallModule?.reportRemoteEnded) return;
52
+ await CallModule.reportRemoteEnded(uuid.toLowerCase().trim(), endReason);
58
53
  },
59
54
 
60
- requestOverlayPermission: () => {
61
- if (Platform.OS === 'android' && CallModule?.requestOverlayPermission) {
62
- CallModule.requestOverlayPermission();
63
- }
55
+ requestOverlayPermission: async () => {
56
+ if (!CallModule?.requestOverlayPermission) return false;
57
+ return await CallModule.requestOverlayPermission();
64
58
  },
65
59
 
66
- // --- CALL LIFECYCLE ACTIONS ---
60
+ checkOverlayPermission: async () => {
61
+ if (!CallModule?.checkOverlayPermission) return false;
62
+ return await CallModule.checkOverlayPermission();
63
+ },
67
64
 
68
65
  displayCall: async (uuid, name, callType = "audio") => {
69
66
  if (!CallModule) return false;
70
67
  return await CallModule.displayIncomingCall(uuid.toLowerCase().trim(), name, callType);
71
68
  },
72
69
 
73
- /** Stops the UI and the Foreground Service */
74
- destroyNativeCallUI: (uuid) => {
75
- if (CallModule?.endNativeCall) {
76
- CallModule.endNativeCall(uuid.toLowerCase().trim());
77
- }
78
- },
79
-
80
- /** Reports to the native side that the other person hung up */
81
- reportRemoteEnded: async (uuid, endReason = "RemoteEnded") => {
82
- if (CallModule?.reportRemoteEnded) {
83
- await CallModule.reportRemoteEnded(uuid.toLowerCase().trim(), endReason);
84
- }
85
- },
86
-
87
70
  stopForegroundService: async () => {
88
71
  if (CallModule?.stopForegroundService) {
89
72
  await CallModule.stopForegroundService();
90
73
  }
91
74
  },
92
75
 
93
- // --- STATE VERIFICATION ---
94
-
95
- checkCallValidity: async (uuid) => {
96
- if (!CallModule?.checkCallValidity) return { isValid: false, isCanceled: true };
97
- return await CallModule.checkCallValidity(uuid.toLowerCase().trim());
98
- },
99
-
100
- checkCallStatus: async (uuid) => {
101
- if (!CallModule?.checkCallStatus) return { isCanceled: true, isActive: false, shouldDisplay: false };
102
- return await CallModule.checkCallStatus(uuid.toLowerCase().trim());
76
+ destroyNativeCallUI: (uuid) => {
77
+ if (CallModule?.endNativeCall) {
78
+ CallModule.endNativeCall(uuid.toLowerCase().trim());
79
+ }
103
80
  },
104
81
 
105
82
  getInitialCallData: async () => {
@@ -107,8 +84,6 @@ export const CallHandler = {
107
84
  return await CallModule.getInitialCallData();
108
85
  },
109
86
 
110
- // --- EVENT SUBSCRIPTION ---
111
-
112
87
  subscribe: (onAccept, onReject, onFailed) => {
113
88
  if (!callEventEmitter) return () => { };
114
89
  const subs = [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rns-nativecall",
3
- "version": "1.1.5",
3
+ "version": "1.1.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",
@@ -1,6 +1,5 @@
1
1
  const { withAndroidManifest, withInfoPlist, withPlugins, withMainActivity, withAppDelegate } = require('@expo/config-plugins');
2
2
 
3
- /** 1. MAIN ACTIVITY MOD **/
4
3
  function withMainActivityDataFix(config) {
5
4
  return withMainActivity(config, (config) => {
6
5
  let contents = config.modResults.contents;
@@ -14,61 +13,62 @@ function withMainActivityDataFix(config) {
14
13
  'import android.Manifest'
15
14
  ];
16
15
 
17
- // 1. Add missing imports
16
+ // Add imports if they don't exist
18
17
  imports.forEach(imp => {
19
18
  if (!contents.includes(imp)) {
20
19
  contents = contents.replace(/package .*/, (match) => `${match}\n${imp}`);
21
20
  }
22
21
  });
23
22
 
24
- // 2. Inject VoIP logic into EXISTING onCreate
25
- const voipOnCreateLogic = `
23
+ const onCreateCode = `
24
+ override fun onCreate(savedInstanceState: Bundle?) {
25
+ super.onCreate(savedInstanceState)
26
+
27
+ // Request Notification Permissions for Android 13+ (Required for Pill UI)
26
28
  if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
27
- ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.POST_NOTIFICATIONS), 101)
29
+ ActivityCompat.requestPermissions(
30
+ this,
31
+ arrayOf(Manifest.permission.POST_NOTIFICATIONS),
32
+ 101
33
+ )
28
34
  }
29
35
 
30
36
  if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {
31
37
  setShowWhenLocked(true)
32
38
  setTurnScreenOn(true)
33
39
  } else {
34
- window.addFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED or WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON or WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
40
+ window.addFlags(
41
+ WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED or
42
+ WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON or
43
+ WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
44
+ )
35
45
  }
36
46
 
37
47
  if (intent.getBooleanExtra("background_wake", false)) {
38
48
  moveTaskToBack(true)
39
49
  }
40
- `;
41
-
42
- if (contents.includes('override fun onCreate')) {
43
- if (!contents.includes('setShowWhenLocked(true)')) {
44
- contents = contents.replace(
45
- /super\.onCreate\(.*\)/,
46
- (match) => `${match}\n${voipOnCreateLogic}`
47
- );
48
- }
49
- }
50
+ }`;
50
51
 
51
- // 3. Handle onNewIntent (Inject method if missing, or fix if exists)
52
- if (!contents.includes('override fun onNewIntent')) {
53
- const onNewIntentCode = `
52
+ const onNewIntentCode = `
54
53
  override fun onNewIntent(intent: Intent) {
55
54
  super.onNewIntent(intent)
56
55
  setIntent(intent)
57
- }
58
- `;
59
- contents = contents.replace(/class MainActivity\s*:\s*ReactActivity\(\)\s*\{/, (match) => `${match}${onNewIntentCode}`);
60
- } else if (!contents.includes('setIntent(intent)')) {
61
- contents = contents.replace(
62
- /super\.onNewIntent\(intent\)/,
63
- (match) => `super.onNewIntent(intent)\n setIntent(intent)`
64
- );
56
+ }`;
57
+
58
+ const classRegex = /class MainActivity\s*:\s*ReactActivity\(\)\s*\{/;
59
+
60
+ if (!contents.includes('override fun onCreate')) {
61
+ contents = contents.replace(classRegex, (match) => `${match}${onCreateCode}`);
62
+ }
63
+
64
+ if (!contents.includes('override fun onNewIntent')) {
65
+ contents = contents.replace(classRegex, (match) => `${match}${onNewIntentCode}`);
65
66
  }
66
67
 
67
68
  config.modResults.contents = contents;
68
69
  return config;
69
70
  });
70
71
  }
71
-
72
72
  /** 2. ANDROID MANIFEST CONFIG **/
73
73
  function withAndroidConfig(config) {
74
74
  return withAndroidManifest(config, (config) => {
@@ -149,37 +149,22 @@ function withAndroidConfig(config) {
149
149
  application.receiver.push({ $: { 'android:name': 'com.rnsnativecall.CallActionReceiver', 'android:exported': 'false' } });
150
150
  }
151
151
 
152
- // 2. ADDED UnlockReceiver with intent-filter
153
- if (!application.receiver.some(r => r.$['android:name'] === 'com.rnsnativecall.UnlockReceiver')) {
154
- application.receiver.push({
155
- $: {
156
- 'android:name': 'com.rnsnativecall.UnlockReceiver',
157
- 'android:exported': 'false'
158
- },
159
- 'intent-filter': [
160
- {
161
- action: [
162
- { $: { 'android:name': 'android.intent.action.USER_PRESENT' } }
163
- ]
164
- }
165
- ]
166
- });
167
- }
168
-
169
152
  return config;
170
153
  });
171
154
  }
172
-
173
- /** 2. IOS APP DELEGATE MOD **/
155
+ /** 2. IOS APP DELEGATE MOD (The fix for Lock Screen Answer) **/
174
156
  function withIosAppDelegateMod(config) {
175
157
  return withAppDelegate(config, (config) => {
176
158
  let contents = config.modResults.contents;
177
159
 
160
+ // 1. Surgical Import: Add 'import React' at the very top if missing
178
161
  if (!contents.includes('import React')) {
179
162
  contents = 'import React\n' + contents;
180
163
  }
181
164
 
165
+ // 2. Check for the continue userActivity method
182
166
  if (!contents.includes('continue userActivity')) {
167
+ // Method is missing, inject it before the final closing brace of the class
183
168
  const swiftLinkingCode = `
184
169
  // Universal Links
185
170
  public override func application(
@@ -191,8 +176,10 @@ function withIosAppDelegateMod(config) {
191
176
  return super.application(application, continue: userActivity, restorationHandler: restorationHandler) || result
192
177
  }
193
178
  `;
179
+ // This regex finds the last '}' in the file (closing the AppDelegate class)
194
180
  contents = contents.replace(/\n}\s*$/, `\n${swiftLinkingCode}\n}`);
195
181
  } else if (!contents.includes('RCTLinkingManager.application')) {
182
+ // Method exists but is missing our logic, inject it inside
196
183
  contents = contents.replace(
197
184
  /continue userActivity: NSUserActivity,[\s\S]*?\) -> Bool \{/,
198
185
  `continue userActivity: NSUserActivity, restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool {\n let result = RCTLinkingManager.application(application, continue: userActivity, restorationHandler: restorationHandler)`
@@ -203,8 +190,7 @@ function withIosAppDelegateMod(config) {
203
190
  return config;
204
191
  });
205
192
  }
206
-
207
- /** 3. IOS INFO.PLIST CONFIG **/
193
+ /** 3. IOS INFO.PLIST CONFIG (Existing) **/
208
194
  function withIosConfig(config, props = {}) {
209
195
  return withInfoPlist(config, (config) => {
210
196
  const infoPlist = config.modResults;
@@ -219,13 +205,12 @@ function withIosConfig(config, props = {}) {
219
205
  return config;
220
206
  });
221
207
  }
222
-
223
208
  // Main Plugin Entry
224
209
  module.exports = (config, props) => {
225
210
  return withPlugins(config, [
226
211
  withAndroidConfig,
227
212
  withMainActivityDataFix,
228
- withIosAppDelegateMod,
213
+ withIosAppDelegateMod, // <--- ADDED THIS HERE
229
214
  [withIosConfig, props]
230
215
  ]);
231
216
  };