diff --git a/android/app/build.gradle b/android/app/build.gradle
index 9ec188c7d98..bc2dcf954ff 100644
--- a/android/app/build.gradle
+++ b/android/app/build.gradle
@@ -93,7 +93,6 @@ android {
versionName "4.68.0"
vectorDrawables.useSupportLibrary = true
manifestPlaceholders = [BugsnagAPIKey: BugsnagAPIKey as String]
- missingDimensionStrategy "RNNotifications.reactNativeVersion", "reactNative60" // See note below!
resValue "string", "rn_config_reader_custom_package", "chat.rocket.reactnative"
}
@@ -144,7 +143,6 @@ dependencies {
implementation jscFlavor
}
- implementation project(':react-native-notifications')
implementation "com.google.firebase:firebase-messaging:23.3.1"
implementation project(':watermelondb-jsi')
diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index 552191d1316..4fb70ab1a89 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -75,6 +75,14 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/android/app/src/main/java/chat/rocket/reactnative/MainActivity.kt b/android/app/src/main/java/chat/rocket/reactnative/MainActivity.kt
index e2ed12f7eab..502ae26784f 100644
--- a/android/app/src/main/java/chat/rocket/reactnative/MainActivity.kt
+++ b/android/app/src/main/java/chat/rocket/reactnative/MainActivity.kt
@@ -7,8 +7,11 @@ import com.facebook.react.defaults.DefaultReactActivityDelegate
import android.os.Bundle
import com.zoontek.rnbootsplash.RNBootSplash
-import android.content.Intent;
-import android.content.res.Configuration;
+import android.content.Intent
+import android.content.res.Configuration
+import chat.rocket.reactnative.notification.VideoConfModule
+import chat.rocket.reactnative.notification.VideoConfNotification
+import com.google.gson.GsonBuilder
class MainActivity : ReactActivity() {
@@ -25,9 +28,60 @@ class MainActivity : ReactActivity() {
override fun createReactActivityDelegate(): ReactActivityDelegate =
DefaultReactActivityDelegate(this, mainComponentName, fabricEnabled)
- override fun onCreate(savedInstanceState: Bundle?) {
+ override fun onCreate(savedInstanceState: Bundle?) {
RNBootSplash.init(this, R.style.BootTheme)
super.onCreate(null)
+
+ // Handle video conf action from notification
+ intent?.let { handleVideoConfIntent(it) }
+ }
+
+ public override fun onNewIntent(intent: Intent) {
+ super.onNewIntent(intent)
+ // Handle video conf action when activity is already running
+ handleVideoConfIntent(intent)
+ }
+
+ private fun handleVideoConfIntent(intent: Intent) {
+ if (intent.getBooleanExtra("videoConfAction", false)) {
+ val notificationId = intent.getIntExtra("notificationId", 0)
+ val event = intent.getStringExtra("event") ?: return
+ val rid = intent.getStringExtra("rid") ?: ""
+ val callerId = intent.getStringExtra("callerId") ?: ""
+ val callerName = intent.getStringExtra("callerName") ?: ""
+ val host = intent.getStringExtra("host") ?: ""
+ val callId = intent.getStringExtra("callId") ?: ""
+
+ android.util.Log.d("RocketChat.MainActivity", "Handling video conf intent - event: $event, rid: $rid, host: $host, callId: $callId")
+
+ // Cancel the notification
+ if (notificationId != 0) {
+ VideoConfNotification.cancelById(this, notificationId)
+ }
+
+ // Store action for JS to pick up - include all required fields
+ val data = mapOf(
+ "notificationType" to "videoconf",
+ "rid" to rid,
+ "event" to event,
+ "host" to host,
+ "callId" to callId,
+ "caller" to mapOf(
+ "_id" to callerId,
+ "name" to callerName
+ )
+ )
+
+ val gson = GsonBuilder().create()
+ val jsonData = gson.toJson(data)
+
+ android.util.Log.d("RocketChat.MainActivity", "Storing video conf action: $jsonData")
+
+ VideoConfModule.storePendingAction(this, jsonData)
+
+ // Clear the video conf flag to prevent re-processing
+ intent.removeExtra("videoConfAction")
+ }
}
override fun invokeDefaultOnBackPressed() {
diff --git a/android/app/src/main/java/chat/rocket/reactnative/MainApplication.kt b/android/app/src/main/java/chat/rocket/reactnative/MainApplication.kt
index 0d07364246b..8b532b363bc 100644
--- a/android/app/src/main/java/chat/rocket/reactnative/MainApplication.kt
+++ b/android/app/src/main/java/chat/rocket/reactnative/MainApplication.kt
@@ -1,9 +1,7 @@
package chat.rocket.reactnative
import android.app.Application
-import android.content.Context
import android.content.res.Configuration
-import android.os.Bundle
import com.facebook.react.PackageList
import com.facebook.react.ReactApplication
import com.facebook.react.ReactHost
@@ -18,17 +16,25 @@ import com.facebook.react.defaults.DefaultReactNativeHost
import com.facebook.react.soloader.OpenSourceMergedSoMapping
import com.facebook.soloader.SoLoader
import com.nozbe.watermelondb.jsi.WatermelonDBJSIPackage;
-import com.wix.reactnativenotifications.core.AppLaunchHelper
-import com.wix.reactnativenotifications.core.AppLifecycleFacade
-import com.wix.reactnativenotifications.core.JsIOHelper
-import com.wix.reactnativenotifications.core.notification.INotificationsApplication
-import com.wix.reactnativenotifications.core.notification.IPushNotification
import com.bugsnag.android.Bugsnag
import expo.modules.ApplicationLifecycleDispatcher
import chat.rocket.reactnative.networking.SSLPinningTurboPackage;
import chat.rocket.reactnative.notification.CustomPushNotification;
+import chat.rocket.reactnative.notification.VideoConfTurboPackage
-open class MainApplication : Application(), ReactApplication, INotificationsApplication {
+/**
+ * Main Application class.
+ *
+ * NOTIFICATION ARCHITECTURE:
+ * - JS layer uses expo-notifications for token registration and event handling
+ * - Native layer uses RCFirebaseMessagingService + CustomPushNotification for:
+ * - FCM message handling
+ * - Notification display with MessagingStyle
+ * - E2E encrypted message decryption
+ * - Direct reply functionality
+ * - Message-id-only notification loading
+ */
+open class MainApplication : Application(), ReactApplication {
override val reactNativeHost: ReactNativeHost =
object : DefaultReactNativeHost(this) {
@@ -36,6 +42,7 @@ open class MainApplication : Application(), ReactApplication, INotificationsAppl
PackageList(this).packages.apply {
add(SSLPinningTurboPackage())
add(WatermelonDBJSIPackage())
+ add(VideoConfTurboPackage())
}
override fun getJSMainModuleName(): String = "index"
@@ -71,19 +78,4 @@ open class MainApplication : Application(), ReactApplication, INotificationsAppl
super.onConfigurationChanged(newConfig)
ApplicationLifecycleDispatcher.onConfigurationChanged(this, newConfig)
}
-
- override fun getPushNotification(
- context: Context,
- bundle: Bundle,
- defaultFacade: AppLifecycleFacade,
- defaultAppLaunchHelper: AppLaunchHelper
- ): IPushNotification {
- return CustomPushNotification(
- context,
- bundle,
- defaultFacade,
- defaultAppLaunchHelper,
- JsIOHelper()
- )
- }
}
diff --git a/android/app/src/main/java/chat/rocket/reactnative/notification/CustomPushNotification.java b/android/app/src/main/java/chat/rocket/reactnative/notification/CustomPushNotification.java
index 18f89fb0ca4..723ce880cb3 100644
--- a/android/app/src/main/java/chat/rocket/reactnative/notification/CustomPushNotification.java
+++ b/android/app/src/main/java/chat/rocket/reactnative/notification/CustomPushNotification.java
@@ -1,7 +1,5 @@
package chat.rocket.reactnative.notification;
-import static com.wix.reactnativenotifications.Defs.NOTIFICATION_RECEIVED_EVENT_NAME;
-
import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
@@ -25,10 +23,6 @@
import com.bumptech.glide.request.RequestOptions;
import com.facebook.react.bridge.ReactApplicationContext;
import com.google.gson.Gson;
-import com.wix.reactnativenotifications.core.AppLaunchHelper;
-import com.wix.reactnativenotifications.core.AppLifecycleFacade;
-import com.wix.reactnativenotifications.core.JsIOHelper;
-import com.wix.reactnativenotifications.core.notification.PushNotification;
import java.util.ArrayList;
import java.util.Date;
@@ -39,15 +33,16 @@
import java.util.concurrent.ExecutionException;
import chat.rocket.reactnative.BuildConfig;
+import chat.rocket.reactnative.MainActivity;
import chat.rocket.reactnative.R;
/**
* Custom push notification handler for Rocket.Chat.
*
* Handles standard push notifications and End-to-End encrypted (E2E) notifications.
- * For E2E notifications, waits for React Native initialization before decrypting and displaying.
+ * Provides MessagingStyle notifications, direct reply, and advanced processing.
*/
-public class CustomPushNotification extends PushNotification {
+public class CustomPushNotification {
private static final String TAG = "RocketChat.CustomPush";
private static final boolean ENABLE_VERBOSE_LOGS = BuildConfig.DEBUG;
@@ -59,14 +54,21 @@ public class CustomPushNotification extends PushNotification {
// Constants
public static final String KEY_REPLY = "KEY_REPLY";
public static final String NOTIFICATION_ID = "NOTIFICATION_ID";
+ private static final String CHANNEL_ID = "rocketchatrn_channel_01";
+ private static final String CHANNEL_NAME = "All";
// Instance fields
- final NotificationManager notificationManager;
+ private final Context mContext;
+ private Bundle mBundle;
+ private final NotificationManager notificationManager;
- public CustomPushNotification(Context context, Bundle bundle, AppLifecycleFacade appLifecycleFacade,
- AppLaunchHelper appLaunchHelper, JsIOHelper jsIoHelper) {
- super(context, bundle, appLifecycleFacade, appLaunchHelper, jsIoHelper);
- notificationManager = (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE);
+ public CustomPushNotification(Context context, Bundle bundle) {
+ this.mContext = context;
+ this.mBundle = bundle;
+ this.notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
+
+ // Ensure notification channel exists
+ createNotificationChannel();
}
/**
@@ -80,59 +82,66 @@ public static void setReactContext(ReactApplicationContext context) {
public static void clearMessages(int notId) {
notificationMessages.remove(Integer.toString(notId));
}
+
+ /**
+ * Check if React Native is initialized
+ */
+ private boolean isReactInitialized() {
+ return reactApplicationContext != null;
+ }
- @Override
- public void onReceived() throws InvalidNotificationException {
- Bundle bundle = mNotificationProps.asBundle();
- String notId = bundle.getString("notId");
+ public void onReceived() {
+ String notId = mBundle.getString("notId");
if (notId == null || notId.isEmpty()) {
- throw new InvalidNotificationException("Missing notification ID");
+ Log.w(TAG, "Missing notification ID, ignoring notification");
+ return;
}
try {
Integer.parseInt(notId);
} catch (NumberFormatException e) {
- throw new InvalidNotificationException("Invalid notification ID format: " + notId);
+ Log.w(TAG, "Invalid notification ID format: " + notId);
+ return;
}
// Check if React is ready - needed for MMKV access (avatars, encryption, message-id-only)
- if (!mAppLifecycleFacade.isReactInitialized()) {
- android.util.Log.w(TAG, "React not initialized yet, waiting before processing notification...");
+ if (!isReactInitialized()) {
+ Log.w(TAG, "React not initialized yet, waiting before processing notification...");
// Wait for React to initialize with timeout
new Thread(() -> {
int attempts = 0;
int maxAttempts = 50; // 5 seconds total (50 * 100ms)
- while (!mAppLifecycleFacade.isReactInitialized() && attempts < maxAttempts) {
+ while (!isReactInitialized() && attempts < maxAttempts) {
try {
Thread.sleep(100); // Wait 100ms
attempts++;
if (attempts % 10 == 0 && ENABLE_VERBOSE_LOGS) {
- android.util.Log.d(TAG, "Still waiting for React initialization... (" + (attempts * 100) + "ms elapsed)");
+ Log.d(TAG, "Still waiting for React initialization... (" + (attempts * 100) + "ms elapsed)");
}
} catch (InterruptedException e) {
- android.util.Log.e(TAG, "Wait interrupted", e);
+ Log.e(TAG, "Wait interrupted", e);
Thread.currentThread().interrupt();
return;
}
}
- if (mAppLifecycleFacade.isReactInitialized()) {
- android.util.Log.i(TAG, "React initialized after " + (attempts * 100) + "ms, proceeding with notification");
+ if (isReactInitialized()) {
+ Log.i(TAG, "React initialized after " + (attempts * 100) + "ms, proceeding with notification");
try {
handleNotification();
} catch (Exception e) {
- android.util.Log.e(TAG, "Failed to process notification after React initialization", e);
+ Log.e(TAG, "Failed to process notification after React initialization", e);
}
} else {
- android.util.Log.e(TAG, "Timeout waiting for React initialization after " + (maxAttempts * 100) + "ms, processing without MMKV");
+ Log.e(TAG, "Timeout waiting for React initialization after " + (maxAttempts * 100) + "ms, processing without MMKV");
try {
handleNotification();
} catch (Exception e) {
- android.util.Log.e(TAG, "Failed to process notification without React context", e);
+ Log.e(TAG, "Failed to process notification without React context", e);
}
}
}).start();
@@ -141,23 +150,21 @@ public void onReceived() throws InvalidNotificationException {
}
if (ENABLE_VERBOSE_LOGS) {
- android.util.Log.d(TAG, "React already initialized, proceeding with notification");
+ Log.d(TAG, "React already initialized, proceeding with notification");
}
try {
handleNotification();
} catch (Exception e) {
- android.util.Log.e(TAG, "Failed to process notification on main thread", e);
- throw new InvalidNotificationException("Notification processing failed: " + e.getMessage());
+ Log.e(TAG, "Failed to process notification on main thread", e);
}
}
private void handleNotification() {
- Bundle received = mNotificationProps.asBundle();
- Ejson receivedEjson = safeFromJson(received.getString("ejson", "{}"), Ejson.class);
+ Ejson receivedEjson = safeFromJson(mBundle.getString("ejson", "{}"), Ejson.class);
if (receivedEjson != null && receivedEjson.notificationType != null && receivedEjson.notificationType.equals("message-id-only")) {
- android.util.Log.d(TAG, "Detected message-id-only notification, will fetch full content from server");
+ Log.d(TAG, "Detected message-id-only notification, will fetch full content from server");
loadNotificationAndProcess(receivedEjson);
return; // Exit early, notification will be processed in callback
}
@@ -171,30 +178,19 @@ private void loadNotificationAndProcess(Ejson ejson) {
@Override
public void call(@Nullable Bundle bundle) {
if (bundle != null) {
- android.util.Log.d(TAG, "Successfully loaded notification content from server, updating notification props");
+ Log.d(TAG, "Successfully loaded notification content from server, updating notification props");
if (ENABLE_VERBOSE_LOGS) {
- // BEFORE createProps
- android.util.Log.d(TAG, "[BEFORE createProps] bundle.notificationLoaded=" + bundle.getBoolean("notificationLoaded", false));
- android.util.Log.d(TAG, "[BEFORE createProps] bundle.title=" + (bundle.getString("title") != null ? "[present]" : "[null]"));
- android.util.Log.d(TAG, "[BEFORE createProps] bundle.message length=" + (bundle.getString("message") != null ? bundle.getString("message").length() : 0));
- android.util.Log.d(TAG, "[BEFORE createProps] bundle has ejson=" + (bundle.getString("ejson") != null));
+ Log.d(TAG, "[BEFORE update] bundle.notificationLoaded=" + bundle.getBoolean("notificationLoaded", false));
+ Log.d(TAG, "[BEFORE update] bundle.title=" + (bundle.getString("title") != null ? "[present]" : "[null]"));
+ Log.d(TAG, "[BEFORE update] bundle.message length=" + (bundle.getString("message") != null ? bundle.getString("message").length() : 0));
}
synchronized(CustomPushNotification.this) {
- mNotificationProps = createProps(bundle);
- }
-
- if (ENABLE_VERBOSE_LOGS) {
- // AFTER createProps - verify it worked
- Bundle verifyBundle = mNotificationProps.asBundle();
- android.util.Log.d(TAG, "[AFTER createProps] mNotificationProps.notificationLoaded=" + verifyBundle.getBoolean("notificationLoaded", false));
- android.util.Log.d(TAG, "[AFTER createProps] mNotificationProps.title=" + (verifyBundle.getString("title") != null ? "[present]" : "[null]"));
- android.util.Log.d(TAG, "[AFTER createProps] mNotificationProps.message length=" + (verifyBundle.getString("message") != null ? verifyBundle.getString("message").length() : 0));
- android.util.Log.d(TAG, "[AFTER createProps] mNotificationProps has ejson=" + (verifyBundle.getString("ejson") != null));
+ mBundle = bundle;
}
} else {
- android.util.Log.w(TAG, "Failed to load notification content from server, will display placeholder notification");
+ Log.w(TAG, "Failed to load notification content from server, will display placeholder notification");
}
processNotification();
@@ -203,28 +199,26 @@ public void call(@Nullable Bundle bundle) {
}
private void processNotification() {
- // We should re-read these values since that can be changed by notificationLoad
- Bundle bundle = mNotificationProps.asBundle();
- Ejson loadedEjson = safeFromJson(bundle.getString("ejson", "{}"), Ejson.class);
- String notId = bundle.getString("notId", "1");
+ Ejson loadedEjson = safeFromJson(mBundle.getString("ejson", "{}"), Ejson.class);
+ String notId = mBundle.getString("notId", "1");
if (ENABLE_VERBOSE_LOGS) {
- android.util.Log.d(TAG, "[processNotification] notId=" + notId);
- android.util.Log.d(TAG, "[processNotification] bundle.notificationLoaded=" + bundle.getBoolean("notificationLoaded", false));
- android.util.Log.d(TAG, "[processNotification] bundle.title=" + (bundle.getString("title") != null ? "[present]" : "[null]"));
- android.util.Log.d(TAG, "[processNotification] bundle.message length=" + (bundle.getString("message") != null ? bundle.getString("message").length() : 0));
- android.util.Log.d(TAG, "[processNotification] loadedEjson.notificationType=" + (loadedEjson != null ? loadedEjson.notificationType : "null"));
- android.util.Log.d(TAG, "[processNotification] loadedEjson.sender=" + (loadedEjson != null && loadedEjson.sender != null ? loadedEjson.sender.username : "null"));
+ Log.d(TAG, "[processNotification] notId=" + notId);
+ Log.d(TAG, "[processNotification] bundle.notificationLoaded=" + mBundle.getBoolean("notificationLoaded", false));
+ Log.d(TAG, "[processNotification] bundle.title=" + (mBundle.getString("title") != null ? "[present]" : "[null]"));
+ Log.d(TAG, "[processNotification] bundle.message length=" + (mBundle.getString("message") != null ? mBundle.getString("message").length() : 0));
+ Log.d(TAG, "[processNotification] loadedEjson.notificationType=" + (loadedEjson != null ? loadedEjson.notificationType : "null"));
+ Log.d(TAG, "[processNotification] loadedEjson.sender=" + (loadedEjson != null && loadedEjson.sender != null ? loadedEjson.sender.username : "null"));
}
// Handle E2E encrypted notifications
if (isE2ENotification(loadedEjson)) {
- handleE2ENotification(bundle, loadedEjson, notId);
+ handleE2ENotification(mBundle, loadedEjson, notId);
return; // E2E processor will handle showing the notification
}
// Handle regular (non-E2E) notifications
- showNotification(bundle, loadedEjson, notId);
+ showNotification(mBundle, loadedEjson, notId);
}
/**
@@ -245,8 +239,7 @@ private void handleE2ENotification(Bundle bundle, Ejson ejson, String notId) {
if (decrypted != null) {
bundle.putString("message", decrypted);
- mNotificationProps = createProps(bundle);
- bundle = mNotificationProps.asBundle();
+ mBundle = bundle;
ejson = safeFromJson(bundle.getString("ejson", "{}"), Ejson.class);
showNotification(bundle, ejson, notId);
} else {
@@ -266,11 +259,9 @@ private void handleE2ENotification(Bundle bundle, Ejson ejson, String notId) {
new E2ENotificationProcessor.NotificationCallback() {
@Override
public void onDecryptionComplete(Bundle decryptedBundle, Ejson decryptedEjson, String notificationId) {
- // Update props and show notification
- mNotificationProps = createProps(decryptedBundle);
- Bundle finalBundle = mNotificationProps.asBundle();
- Ejson finalEjson = safeFromJson(finalBundle.getString("ejson", "{}"), Ejson.class);
- showNotification(finalBundle, finalEjson, notificationId);
+ mBundle = decryptedBundle;
+ Ejson finalEjson = safeFromJson(decryptedBundle.getString("ejson", "{}"), Ejson.class);
+ showNotification(decryptedBundle, finalEjson, notificationId);
}
@Override
@@ -308,134 +299,174 @@ private void showNotification(Bundle bundle, Ejson ejson, String notId) {
String avatarUri = ejson != null ? ejson.getAvatarUri() : null;
if (ENABLE_VERBOSE_LOGS) {
- android.util.Log.d(TAG, "[showNotification] avatarUri=" + (avatarUri != null ? "[present]" : "[null]"));
+ Log.d(TAG, "[showNotification] avatarUri=" + (avatarUri != null ? "[present]" : "[null]"));
}
bundle.putString("avatarUri", avatarUri);
// Handle special notification types
- if (ejson != null && ejson.notificationType instanceof String &&
- ejson.notificationType.equals("videoconf")) {
- notifyReceivedToJS();
+ if (ejson != null && "videoconf".equals(ejson.notificationType)) {
+ handleVideoConfNotification(bundle, ejson);
+ return;
} else {
// Show regular notification
if (ENABLE_VERBOSE_LOGS) {
- android.util.Log.d(TAG, "[Before add to notificationMessages] notId=" + notId + ", bundle.message length=" + (bundle.getString("message") != null ? bundle.getString("message").length() : 0) + ", bundle.notificationLoaded=" + bundle.getBoolean("notificationLoaded", false));
+ Log.d(TAG, "[Before add to notificationMessages] notId=" + notId + ", bundle.message length=" + (bundle.getString("message") != null ? bundle.getString("message").length() : 0) + ", bundle.notificationLoaded=" + bundle.getBoolean("notificationLoaded", false));
}
notificationMessages.get(notId).add(bundle);
if (ENABLE_VERBOSE_LOGS) {
- android.util.Log.d(TAG, "[After add] notificationMessages[" + notId + "].size=" + notificationMessages.get(notId).size());
+ Log.d(TAG, "[After add] notificationMessages[" + notId + "].size=" + notificationMessages.get(notId).size());
}
postNotification(Integer.parseInt(notId));
- notifyReceivedToJS();
}
}
- @Override
- public void onOpened() {
- Bundle bundle = mNotificationProps.asBundle();
- final String notId = bundle.getString("notId", "1");
- notificationMessages.remove(notId);
- digestNotification();
+ /**
+ * Handles video conference notifications.
+ * Shows incoming call notification or cancels existing one based on status.
+ */
+ private void handleVideoConfNotification(Bundle bundle, Ejson ejson) {
+ VideoConfNotification videoConf = new VideoConfNotification(mContext);
+
+ Integer status = ejson.status;
+ String rid = ejson.rid;
+ // Video conf uses 'caller' field, regular messages use 'sender'
+ String callerId = "";
+ if (ejson.caller != null && ejson.caller._id != null) {
+ callerId = ejson.caller._id;
+ } else if (ejson.sender != null && ejson.sender._id != null) {
+ callerId = ejson.sender._id;
+ }
+
+ Log.d(TAG, "Video conf notification - status: " + status + ", rid: " + rid);
+
+ if (status == null || status == 0) {
+ // Incoming call - show notification
+ videoConf.showIncomingCall(bundle, ejson);
+ } else if (status == 4) {
+ // Call cancelled/ended - dismiss notification
+ videoConf.cancelCall(rid, callerId);
+ } else {
+ Log.d(TAG, "Unknown video conf status: " + status);
+ }
}
- @Override
- protected Notification.Builder getNotificationBuilder(PendingIntent intent) {
- final Notification.Builder notification = new Notification.Builder(mContext);
+ private void postNotification(int notificationId) {
+ Notification.Builder notification = buildNotification(notificationId);
+ if (notification != null && notificationManager != null) {
+ notificationManager.notify(notificationId, notification.build());
+ }
+ }
+
+ private void createNotificationChannel() {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ NotificationChannel channel = new NotificationChannel(
+ CHANNEL_ID,
+ CHANNEL_NAME,
+ NotificationManager.IMPORTANCE_HIGH
+ );
+ if (notificationManager != null) {
+ notificationManager.createNotificationChannel(channel);
+ }
+ }
+ }
- Bundle bundle = mNotificationProps.asBundle();
- String notId = bundle.getString("notId", "1");
- String title = bundle.getString("title");
- String message = bundle.getString("message");
- Boolean notificationLoaded = bundle.getBoolean("notificationLoaded", false);
- Ejson ejson = safeFromJson(bundle.getString("ejson", "{}"), Ejson.class);
+ private Notification.Builder buildNotification(int notificationId) {
+ String notId = Integer.toString(notificationId);
+ String title = mBundle.getString("title");
+ String message = mBundle.getString("message");
+ Boolean notificationLoaded = mBundle.getBoolean("notificationLoaded", false);
+ Ejson ejson = safeFromJson(mBundle.getString("ejson", "{}"), Ejson.class);
if (ENABLE_VERBOSE_LOGS) {
- android.util.Log.d(TAG, "[getNotificationBuilder] notId=" + notId);
- android.util.Log.d(TAG, "[getNotificationBuilder] notificationLoaded=" + notificationLoaded);
- android.util.Log.d(TAG, "[getNotificationBuilder] title=" + (title != null ? "[present]" : "[null]"));
- android.util.Log.d(TAG, "[getNotificationBuilder] message length=" + (message != null ? message.length() : 0));
- android.util.Log.d(TAG, "[getNotificationBuilder] ejson=" + (ejson != null ? "present" : "null"));
- android.util.Log.d(TAG, "[getNotificationBuilder] ejson.notificationType=" + (ejson != null ? ejson.notificationType : "null"));
- android.util.Log.d(TAG, "[getNotificationBuilder] ejson.sender=" + (ejson != null && ejson.sender != null ? ejson.sender.username : "null"));
+ Log.d(TAG, "[buildNotification] notId=" + notId);
+ Log.d(TAG, "[buildNotification] notificationLoaded=" + notificationLoaded);
+ Log.d(TAG, "[buildNotification] title=" + (title != null ? "[present]" : "[null]"));
+ Log.d(TAG, "[buildNotification] message length=" + (message != null ? message.length() : 0));
+ }
+
+ // Create pending intent to open the app
+ Intent intent = new Intent(mContext, MainActivity.class);
+ intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
+ intent.putExtras(mBundle);
+
+ PendingIntent pendingIntent;
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
+ pendingIntent = PendingIntent.getActivity(mContext, notificationId, intent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
+ } else {
+ pendingIntent = PendingIntent.getActivity(mContext, notificationId, intent, PendingIntent.FLAG_UPDATE_CURRENT);
+ }
+
+ Notification.Builder notification;
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ notification = new Notification.Builder(mContext, CHANNEL_ID);
+ } else {
+ notification = new Notification.Builder(mContext);
}
notification
.setContentTitle(title)
.setContentText(message)
- .setContentIntent(intent)
+ .setContentIntent(pendingIntent)
.setPriority(Notification.PRIORITY_HIGH)
.setDefaults(Notification.DEFAULT_ALL)
.setAutoCancel(true);
- Integer notificationId = Integer.parseInt(notId);
notificationColor(notification);
- notificationChannel(notification);
- notificationIcons(notification, bundle);
+ notificationIcons(notification, mBundle);
notificationDismiss(notification, notificationId);
// if notificationType is null (RC < 3.5) or notificationType is different of message-id-only or notification was loaded successfully
if (ejson == null || ejson.notificationType == null || !ejson.notificationType.equals("message-id-only") || notificationLoaded) {
- android.util.Log.i(TAG, "[getNotificationBuilder] ✅ Rendering FULL notification style (ejson=" + (ejson != null) + ", notificationType=" + (ejson != null ? ejson.notificationType : "null") + ", notificationLoaded=" + notificationLoaded + ")");
- notificationStyle(notification, notificationId, bundle);
- notificationReply(notification, notificationId, bundle);
-
- // message couldn't be loaded from server (Fallback notification)
+ Log.i(TAG, "[buildNotification] ✅ Rendering FULL notification style");
+ notificationStyle(notification, notificationId, mBundle);
+ notificationReply(notification, notificationId, mBundle);
} else {
- android.util.Log.w(TAG, "[getNotificationBuilder] ⚠️ Rendering FALLBACK notification (ejson=" + (ejson != null) + ", notificationType=" + (ejson != null ? ejson.notificationType : "null") + ", notificationLoaded=" + notificationLoaded + ")");
- // iterate over the current notification ids to dismiss fallback notifications from same server
- for (Map.Entry> bundleList : notificationMessages.entrySet()) {
- // iterate over the notifications with this id (same host + rid)
- Iterator iterator = bundleList.getValue().iterator();
- while (iterator.hasNext()) {
- Bundle not = (Bundle) iterator.next();
- // get the notification info
- Ejson notEjson = safeFromJson(not.getString("ejson", "{}"), Ejson.class);
- // if already has a notification from same server
- if (ejson != null && notEjson != null && ejson.serverURL().equals(notEjson.serverURL())) {
- String id = not.getString("notId");
- // cancel this notification
- if (notificationManager != null) {
- try {
- notificationManager.cancel(Integer.parseInt(id));
- if (ENABLE_VERBOSE_LOGS) {
- android.util.Log.d(TAG, "Cancelled previous fallback notification from same server");
- }
- } catch (NumberFormatException e) {
- android.util.Log.e(TAG, "Invalid notification ID for cancel: " + id, e);
+ Log.w(TAG, "[buildNotification] ⚠️ Rendering FALLBACK notification");
+ // Cancel previous fallback notifications from same server
+ cancelPreviousFallbackNotifications(ejson);
+ }
+
+ return notification;
+ }
+
+ private void cancelPreviousFallbackNotifications(Ejson ejson) {
+ for (Map.Entry> bundleList : notificationMessages.entrySet()) {
+ Iterator iterator = bundleList.getValue().iterator();
+ while (iterator.hasNext()) {
+ Bundle not = iterator.next();
+ Ejson notEjson = safeFromJson(not.getString("ejson", "{}"), Ejson.class);
+ if (ejson != null && notEjson != null && ejson.serverURL().equals(notEjson.serverURL())) {
+ String id = not.getString("notId");
+ if (notificationManager != null && id != null) {
+ try {
+ notificationManager.cancel(Integer.parseInt(id));
+ if (ENABLE_VERBOSE_LOGS) {
+ Log.d(TAG, "Cancelled previous fallback notification from same server");
}
+ } catch (NumberFormatException e) {
+ Log.e(TAG, "Invalid notification ID for cancel: " + id, e);
}
}
}
}
}
-
- return notification;
- }
-
- private void notifyReceivedToJS() {
- boolean isReactInitialized = mAppLifecycleFacade.isReactInitialized();
- if (isReactInitialized) {
- mJsIOHelper.sendEventToJS(NOTIFICATION_RECEIVED_EVENT_NAME, mNotificationProps.asBundle(), mAppLifecycleFacade.getRunningReactContext());
- }
}
private Bitmap getAvatar(String uri) {
if (uri == null || uri.isEmpty()) {
if (ENABLE_VERBOSE_LOGS) {
- android.util.Log.w(TAG, "getAvatar called with null/empty URI");
+ Log.w(TAG, "getAvatar called with null/empty URI");
}
return largeIcon();
}
- // Sanitize URL for logging (remove query params with tokens)
- String sanitizedUri = uri;
- int queryStart = uri.indexOf("?");
- if (queryStart != -1) {
- sanitizedUri = uri.substring(0, queryStart) + "?[auth_params]";
- }
-
if (ENABLE_VERBOSE_LOGS) {
- android.util.Log.d(TAG, "Fetching avatar from: " + sanitizedUri);
+ String sanitizedUri = uri;
+ int queryStart = uri.indexOf("?");
+ if (queryStart != -1) {
+ sanitizedUri = uri.substring(0, queryStart) + "?[auth_params]";
+ }
+ Log.d(TAG, "Fetching avatar from: " + sanitizedUri);
}
try {
@@ -446,16 +477,9 @@ private Bitmap getAvatar(String uri) {
.submit(100, 100)
.get();
- if (avatar != null) {
- if (ENABLE_VERBOSE_LOGS) {
- android.util.Log.d(TAG, "Successfully loaded avatar");
- }
- } else {
- android.util.Log.w(TAG, "Avatar loaded but is null");
- }
return avatar != null ? avatar : largeIcon();
} catch (final ExecutionException | InterruptedException e) {
- android.util.Log.e(TAG, "Failed to fetch avatar: " + e.getMessage(), e);
+ Log.e(TAG, "Failed to fetch avatar: " + e.getMessage(), e);
return largeIcon();
}
}
@@ -464,16 +488,13 @@ private Bitmap largeIcon() {
final Resources res = mContext.getResources();
String packageName = mContext.getPackageName();
int largeIconResId = res.getIdentifier("ic_notification", "drawable", packageName);
- Bitmap largeIconBitmap = BitmapFactory.decodeResource(res, largeIconResId);
- return largeIconBitmap;
+ return BitmapFactory.decodeResource(res, largeIconResId);
}
private void notificationIcons(Notification.Builder notification, Bundle bundle) {
final Resources res = mContext.getResources();
String packageName = mContext.getPackageName();
-
int smallIconResId = res.getIdentifier("ic_notification", "drawable", packageName);
-
Ejson ejson = safeFromJson(bundle.getString("ejson", "{}"), Ejson.class);
notification.setSmallIcon(smallIconResId);
@@ -486,28 +507,6 @@ private void notificationIcons(Notification.Builder notification, Bundle bundle)
}
}
- private void notificationChannel(Notification.Builder notification) {
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
- String CHANNEL_ID = "rocketchatrn_channel_01";
- String CHANNEL_NAME = "All";
-
- // User-visible importance level: Urgent - Makes a sound and appears as a heads-up notification
- // https://developer.android.com/training/notify-user/channels#importance
- NotificationChannel channel = new NotificationChannel(CHANNEL_ID,
- CHANNEL_NAME,
- NotificationManager.IMPORTANCE_HIGH);
-
- final NotificationManager notificationManager = (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE);
- if (notificationManager != null) {
- notificationManager.createNotificationChannel(channel);
- } else {
- android.util.Log.e(TAG, "NotificationManager is null, cannot create notification channel");
- }
-
- notification.setChannelId(CHANNEL_ID);
- }
- }
-
private String extractMessage(String message, Ejson ejson) {
if (message == null) {
return "";
@@ -515,7 +514,7 @@ private String extractMessage(String message, Ejson ejson) {
if (ejson != null && ejson.type != null && !ejson.type.equals("d")) {
int pos = message.indexOf(":");
int start = pos == -1 ? 0 : pos + 2;
- return message.substring(start, message.length());
+ return message.substring(start);
}
return message;
}
@@ -530,25 +529,17 @@ private void notificationStyle(Notification.Builder notification, int notId, Bun
List bundles = notificationMessages.get(Integer.toString(notId));
if (ENABLE_VERBOSE_LOGS) {
- android.util.Log.d(TAG, "[notificationStyle] notId=" + notId + ", bundles=" + (bundles != null ? bundles.size() : "null"));
- if (bundles != null && bundles.size() > 0) {
- Bundle firstBundle = bundles.get(0);
- android.util.Log.d(TAG, "[notificationStyle] first bundle.message length=" + (firstBundle.getString("message") != null ? firstBundle.getString("message").length() : 0));
- android.util.Log.d(TAG, "[notificationStyle] first bundle.notificationLoaded=" + firstBundle.getBoolean("notificationLoaded", false));
- }
+ Log.d(TAG, "[notificationStyle] notId=" + notId + ", bundles=" + (bundles != null ? bundles.size() : "null"));
}
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
Notification.InboxStyle messageStyle = new Notification.InboxStyle();
if (bundles != null) {
- for (int i = 0; i < bundles.size(); i++) {
- Bundle data = bundles.get(i);
+ for (Bundle data : bundles) {
String message = data.getString("message");
-
messageStyle.addLine(message);
}
}
-
notification.setStyle(messageStyle);
} else {
Notification.MessagingStyle messageStyle;
@@ -567,9 +558,7 @@ private void notificationStyle(Notification.Builder notification, int notId, Bun
messageStyle.setConversationTitle(title);
if (bundles != null) {
- for (int i = 0; i < bundles.size(); i++) {
- Bundle data = bundles.get(i);
-
+ for (Bundle data : bundles) {
long timestamp = data.getLong("time");
String message = data.getString("message");
String senderId = data.getString("senderId");
@@ -582,18 +571,16 @@ private void notificationStyle(Notification.Builder notification, int notId, Bun
messageStyle.addMessage(m, timestamp, senderName);
} else {
Bitmap avatar = getAvatar(avatarUri);
-
String senderName = ejson != null ? ejson.senderName : "Unknown";
- Person.Builder sender = new Person.Builder()
+ Person.Builder senderBuilder = new Person.Builder()
.setKey(senderId)
.setName(senderName);
if (avatar != null) {
- sender.setIcon(Icon.createWithBitmap(avatar));
+ senderBuilder.setIcon(Icon.createWithBitmap(avatar));
}
- Person person = sender.build();
-
+ Person person = senderBuilder.build();
messageStyle.addMessage(m, timestamp, person);
}
}
@@ -630,8 +617,7 @@ private void notificationReply(Notification.Builder notification, int notificati
.setLabel(label)
.build();
- CharSequence title = label;
- Notification.Action replyAction = new Notification.Action.Builder(smallIconResId, title, replyPendingIntent)
+ Notification.Action replyAction = new Notification.Action.Builder(smallIconResId, label, replyPendingIntent)
.addRemoteInput(remoteInput)
.setAllowGeneratedReplies(true)
.build();
@@ -657,11 +643,6 @@ private void notificationLoad(Ejson ejson, Callback callback) {
/**
* Safely parses JSON string to object with error handling.
- *
- * @param json JSON string to parse
- * @param classOfT Target class type
- * @param Type parameter
- * @return Parsed object, or null if parsing fails
*/
private static T safeFromJson(String json, Class classOfT) {
if (json == null || json.trim().isEmpty() || json.equals("{}")) {
diff --git a/android/app/src/main/java/chat/rocket/reactnative/notification/Ejson.java b/android/app/src/main/java/chat/rocket/reactnative/notification/Ejson.java
index c12017dd0bc..64f3c3bac59 100644
--- a/android/app/src/main/java/chat/rocket/reactnative/notification/Ejson.java
+++ b/android/app/src/main/java/chat/rocket/reactnative/notification/Ejson.java
@@ -7,8 +7,6 @@
import com.ammarahmed.mmkv.SecureKeystore;
import com.tencent.mmkv.MMKV;
-import com.wix.reactnativenotifications.core.AppLifecycleFacade;
-import com.wix.reactnativenotifications.core.AppLifecycleFacadeHolder;
import java.math.BigInteger;
@@ -37,11 +35,14 @@ public class Ejson {
String rid;
String type;
Sender sender;
+ Caller caller; // For video conf notifications
String messageId;
+ String callId; // For video conf notifications
String notificationType;
String messageType;
String senderName;
String msg;
+ Integer status; // For video conf: 0=incoming, 4=cancelled
String tmid;
@@ -73,19 +74,9 @@ private synchronized void ensureMMKVInitialized() {
initializationAttempted = true;
- // Try to get ReactApplicationContext from available sources
+ // Get ReactApplicationContext from CustomPushNotification
if (this.reactContext == null) {
- AppLifecycleFacade facade = AppLifecycleFacadeHolder.get();
- if (facade != null) {
- Object runningContext = facade.getRunningReactContext();
- if (runningContext instanceof ReactApplicationContext) {
- this.reactContext = (ReactApplicationContext) runningContext;
- }
- }
-
- if (this.reactContext == null) {
- this.reactContext = CustomPushNotification.reactApplicationContext;
- }
+ this.reactContext = CustomPushNotification.reactApplicationContext;
}
// Initialize MMKV if context is available
@@ -238,6 +229,11 @@ static class Sender {
String name;
}
+ static class Caller {
+ String _id;
+ String name;
+ }
+
static class Content {
String algorithm;
String ciphertext;
diff --git a/android/app/src/main/java/chat/rocket/reactnative/notification/Encryption.java b/android/app/src/main/java/chat/rocket/reactnative/notification/Encryption.java
index b9fffc4bba7..f0fd33cea8f 100644
--- a/android/app/src/main/java/chat/rocket/reactnative/notification/Encryption.java
+++ b/android/app/src/main/java/chat/rocket/reactnative/notification/Encryption.java
@@ -8,8 +8,6 @@
import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.WritableMap;
-import com.wix.reactnativenotifications.core.AppLifecycleFacade;
-import com.wix.reactnativenotifications.core.AppLifecycleFacadeHolder;
import com.google.gson.Gson;
import com.google.gson.JsonObject;
import chat.rocket.mobilecrypto.algorithms.AESCrypto;
@@ -319,10 +317,8 @@ public String decryptMessage(final Ejson ejson, final Context context) {
if (context instanceof ReactApplicationContext) {
this.reactContext = (ReactApplicationContext) context;
} else {
- AppLifecycleFacade facade = AppLifecycleFacadeHolder.get();
- if (facade != null && facade.getRunningReactContext() instanceof ReactApplicationContext) {
- this.reactContext = (ReactApplicationContext) facade.getRunningReactContext();
- }
+ // Fallback to CustomPushNotification's static context
+ this.reactContext = CustomPushNotification.reactApplicationContext;
}
if (this.reactContext == null) {
@@ -370,10 +366,8 @@ public String decryptMessage(final Ejson ejson, final Context context) {
public EncryptionContent encryptMessageContent(final String message, final String id, final Ejson ejson) {
try {
- AppLifecycleFacade facade = AppLifecycleFacadeHolder.get();
- if (facade != null && facade.getRunningReactContext() instanceof ReactApplicationContext) {
- this.reactContext = (ReactApplicationContext) facade.getRunningReactContext();
- }
+ // Get ReactApplicationContext from CustomPushNotification
+ this.reactContext = CustomPushNotification.reactApplicationContext;
// Use reactContext for database access
if (this.reactContext == null) {
diff --git a/android/app/src/main/java/chat/rocket/reactnative/notification/LoadNotification.java b/android/app/src/main/java/chat/rocket/reactnative/notification/LoadNotification.java
index 6b6158393fc..eb528df4f3b 100644
--- a/android/app/src/main/java/chat/rocket/reactnative/notification/LoadNotification.java
+++ b/android/app/src/main/java/chat/rocket/reactnative/notification/LoadNotification.java
@@ -127,6 +127,7 @@ public void load(ReactApplicationContext reactApplicationContext, final Ejson ej
Request request = new Request.Builder()
.header("x-user-id", userId)
.header("x-auth-token", userToken)
+ .header("User-Agent", NotificationHelper.getUserAgent())
.url(urlBuilder.addQueryParameter("id", messageId).build())
.build();
diff --git a/android/app/src/main/java/chat/rocket/reactnative/notification/NativeVideoConfSpec.kt b/android/app/src/main/java/chat/rocket/reactnative/notification/NativeVideoConfSpec.kt
new file mode 100644
index 00000000000..8eaea08cfd4
--- /dev/null
+++ b/android/app/src/main/java/chat/rocket/reactnative/notification/NativeVideoConfSpec.kt
@@ -0,0 +1,23 @@
+package chat.rocket.reactnative.notification
+
+import com.facebook.react.bridge.Promise
+import com.facebook.react.bridge.ReactApplicationContext
+import com.facebook.react.bridge.ReactContextBaseJavaModule
+import com.facebook.react.bridge.ReactMethod
+import com.facebook.react.turbomodule.core.interfaces.TurboModule
+
+abstract class NativeVideoConfSpec(reactContext: ReactApplicationContext) :
+ ReactContextBaseJavaModule(reactContext), TurboModule {
+
+ companion object {
+ const val NAME = "VideoConfModule"
+ }
+
+ override fun getName(): String = NAME
+
+ @ReactMethod
+ abstract fun getPendingAction(promise: Promise)
+
+ @ReactMethod
+ abstract fun clearPendingAction()
+}
diff --git a/android/app/src/main/java/chat/rocket/reactnative/notification/NotificationHelper.java b/android/app/src/main/java/chat/rocket/reactnative/notification/NotificationHelper.java
index ac2f1256301..3b53a0be419 100644
--- a/android/app/src/main/java/chat/rocket/reactnative/notification/NotificationHelper.java
+++ b/android/app/src/main/java/chat/rocket/reactnative/notification/NotificationHelper.java
@@ -1,5 +1,7 @@
package chat.rocket.reactnative.notification;
+import android.os.Build;
+
import chat.rocket.reactnative.BuildConfig;
/**
@@ -23,5 +25,17 @@ public static String sanitizeUrl(String url) {
}
return "[workspace]";
}
+
+ /**
+ * Get the User-Agent string for API requests
+ * Format: RC Mobile; android {systemVersion}; v{appVersion} ({buildNumber})
+ * @return User-Agent string
+ */
+ public static String getUserAgent() {
+ String systemVersion = Build.VERSION.RELEASE;
+ String appVersion = BuildConfig.VERSION_NAME;
+ int buildNumber = BuildConfig.VERSION_CODE;
+ return String.format("RC Mobile; android %s; v%s (%d)", systemVersion, appVersion, buildNumber);
+ }
}
diff --git a/android/app/src/main/java/chat/rocket/reactnative/notification/RCFirebaseMessagingService.kt b/android/app/src/main/java/chat/rocket/reactnative/notification/RCFirebaseMessagingService.kt
new file mode 100644
index 00000000000..c8aba96e6b9
--- /dev/null
+++ b/android/app/src/main/java/chat/rocket/reactnative/notification/RCFirebaseMessagingService.kt
@@ -0,0 +1,51 @@
+package chat.rocket.reactnative.notification
+
+import android.os.Bundle
+import android.util.Log
+import com.google.firebase.messaging.FirebaseMessagingService
+import com.google.firebase.messaging.RemoteMessage
+
+/**
+ * Custom Firebase Messaging Service for Rocket.Chat.
+ *
+ * Handles incoming FCM messages and routes them to CustomPushNotification
+ * for advanced processing (E2E decryption, MessagingStyle, direct reply, etc.)
+ */
+class RCFirebaseMessagingService : FirebaseMessagingService() {
+
+ companion object {
+ private const val TAG = "RocketChat.FCM"
+ }
+
+ override fun onMessageReceived(remoteMessage: RemoteMessage) {
+ Log.d(TAG, "FCM message received from: ${remoteMessage.from}")
+
+ val data = remoteMessage.data
+ if (data.isEmpty()) {
+ Log.w(TAG, "FCM message has no data payload, ignoring")
+ return
+ }
+
+ // Convert FCM data to Bundle for processing
+ val bundle = Bundle().apply {
+ data.forEach { (key, value) ->
+ putString(key, value)
+ }
+ }
+
+ // Process the notification
+ try {
+ val notification = CustomPushNotification(this, bundle)
+ notification.onReceived()
+ } catch (e: Exception) {
+ Log.e(TAG, "Error processing FCM message", e)
+ }
+ }
+
+ override fun onNewToken(token: String) {
+ Log.d(TAG, "FCM token refreshed")
+ // Token handling is done by expo-notifications JS layer
+ // which uses getDevicePushTokenAsync()
+ super.onNewToken(token)
+ }
+}
diff --git a/android/app/src/main/java/chat/rocket/reactnative/notification/ReplyBroadcast.java b/android/app/src/main/java/chat/rocket/reactnative/notification/ReplyBroadcast.java
index 428332c3bf3..345596cfd98 100644
--- a/android/app/src/main/java/chat/rocket/reactnative/notification/ReplyBroadcast.java
+++ b/android/app/src/main/java/chat/rocket/reactnative/notification/ReplyBroadcast.java
@@ -27,8 +27,6 @@
import okhttp3.RequestBody;
import okhttp3.Response;
-import com.wix.reactnativenotifications.core.NotificationIntentAdapter;
-
public class ReplyBroadcast extends BroadcastReceiver {
private Context mContext;
private Bundle bundle;
@@ -43,7 +41,11 @@ public void onReceive(Context context, Intent intent) {
}
mContext = context;
- bundle = NotificationIntentAdapter.extractPendingNotificationDataFromIntent(intent);
+ // Extract bundle directly from intent extras
+ bundle = intent.getBundleExtra("pushNotification");
+ if (bundle == null) {
+ bundle = intent.getExtras();
+ }
notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
String notId = bundle.getString("notId");
@@ -79,6 +81,7 @@ protected void replyToMessage(final Ejson ejson, final int notId, final CharSequ
Request request = new Request.Builder()
.header("x-auth-token", ejson.token())
.header("x-user-id", ejson.userId())
+ .header("User-Agent", NotificationHelper.getUserAgent())
.url(String.format("%s/api/v1/chat.sendMessage", serverURL))
.post(body)
.build();
diff --git a/android/app/src/main/java/chat/rocket/reactnative/notification/VideoConfBroadcast.kt b/android/app/src/main/java/chat/rocket/reactnative/notification/VideoConfBroadcast.kt
new file mode 100644
index 00000000000..d5aa014fe8c
--- /dev/null
+++ b/android/app/src/main/java/chat/rocket/reactnative/notification/VideoConfBroadcast.kt
@@ -0,0 +1,73 @@
+package chat.rocket.reactnative.notification
+
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import android.util.Log
+import chat.rocket.reactnative.MainActivity
+import com.google.gson.GsonBuilder
+
+/**
+ * Handles video conference notification actions (accept/decline).
+ * Stores the action for the JS layer to process when the app opens.
+ */
+class VideoConfBroadcast : BroadcastReceiver() {
+
+ companion object {
+ private const val TAG = "RocketChat.VideoConf"
+ }
+
+ override fun onReceive(context: Context, intent: Intent) {
+ val action = intent.action
+ val extras = intent.extras
+
+ if (action == null || extras == null) {
+ Log.w(TAG, "Received broadcast with null action or extras")
+ return
+ }
+
+ Log.d(TAG, "Received video conf action: $action")
+
+ val event = when (action) {
+ VideoConfNotification.ACTION_ACCEPT -> "accept"
+ VideoConfNotification.ACTION_DECLINE -> "decline"
+ else -> {
+ Log.w(TAG, "Unknown action: $action")
+ return
+ }
+ }
+
+ // Cancel the notification
+ val notificationId = extras.getInt("notificationId", 0)
+ if (notificationId != 0) {
+ VideoConfNotification.cancelById(context, notificationId)
+ }
+
+ // Build data for JS layer
+ val data = mapOf(
+ "notificationType" to (extras.getString("notificationType") ?: "videoconf"),
+ "rid" to (extras.getString("rid") ?: ""),
+ "event" to event,
+ "caller" to mapOf(
+ "_id" to (extras.getString("callerId") ?: ""),
+ "name" to (extras.getString("callerName") ?: "")
+ )
+ )
+
+ // Store action for the JS layer to pick up
+ val gson = GsonBuilder().create()
+ val jsonData = gson.toJson(data)
+
+ VideoConfModule.storePendingAction(context, jsonData)
+
+ Log.d(TAG, "Stored video conf action: $event for rid: ${extras.getString("rid")}")
+
+ // Launch the app
+ val launchIntent = Intent(context, MainActivity::class.java).apply {
+ flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP
+ putExtras(extras)
+ putExtra("event", event)
+ }
+ context.startActivity(launchIntent)
+ }
+}
diff --git a/android/app/src/main/java/chat/rocket/reactnative/notification/VideoConfModule.kt b/android/app/src/main/java/chat/rocket/reactnative/notification/VideoConfModule.kt
new file mode 100644
index 00000000000..87059ec0cc7
--- /dev/null
+++ b/android/app/src/main/java/chat/rocket/reactnative/notification/VideoConfModule.kt
@@ -0,0 +1,66 @@
+package chat.rocket.reactnative.notification
+
+import android.content.Context
+import com.facebook.react.bridge.Promise
+import com.facebook.react.bridge.ReactApplicationContext
+import com.facebook.react.bridge.ReactMethod
+
+/**
+ * Native module to expose video conference notification actions to JavaScript.
+ * Used to retrieve pending video conf actions when the app opens.
+ */
+class VideoConfModule(reactContext: ReactApplicationContext) : NativeVideoConfSpec(reactContext) {
+
+ companion object {
+ private const val PREFS_NAME = "RocketChatPrefs"
+ private const val KEY_VIDEO_CONF_ACTION = "videoConfAction"
+
+ /**
+ * Stores a video conference action.
+ * Called from native code when user interacts with video conf notification.
+ */
+ @JvmStatic
+ fun storePendingAction(context: Context, actionJson: String) {
+ context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
+ .edit()
+ .putString(KEY_VIDEO_CONF_ACTION, actionJson)
+ .apply()
+ }
+ }
+
+ /**
+ * Gets any pending video conference action.
+ * Returns null if no pending action.
+ */
+ @ReactMethod
+ override fun getPendingAction(promise: Promise) {
+ try {
+ val prefs = reactApplicationContext.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
+ val action = prefs.getString(KEY_VIDEO_CONF_ACTION, null)
+
+ // Clear the action after reading
+ action?.let {
+ prefs.edit().remove(KEY_VIDEO_CONF_ACTION).apply()
+ }
+
+ promise.resolve(action)
+ } catch (e: Exception) {
+ promise.reject("ERROR", e.message)
+ }
+ }
+
+ /**
+ * Clears any pending video conference action.
+ */
+ @ReactMethod
+ override fun clearPendingAction() {
+ try {
+ reactApplicationContext.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
+ .edit()
+ .remove(KEY_VIDEO_CONF_ACTION)
+ .apply()
+ } catch (e: Exception) {
+ // Ignore errors
+ }
+ }
+}
diff --git a/android/app/src/main/java/chat/rocket/reactnative/notification/VideoConfNotification.kt b/android/app/src/main/java/chat/rocket/reactnative/notification/VideoConfNotification.kt
new file mode 100644
index 00000000000..b783e33e1b0
--- /dev/null
+++ b/android/app/src/main/java/chat/rocket/reactnative/notification/VideoConfNotification.kt
@@ -0,0 +1,210 @@
+package chat.rocket.reactnative.notification
+
+import android.app.Notification
+import android.app.NotificationChannel
+import android.app.NotificationManager
+import android.app.PendingIntent
+import android.content.Context
+import android.content.Intent
+import android.media.AudioAttributes
+import android.media.RingtoneManager
+import android.os.Build
+import android.os.Bundle
+import android.util.Log
+import androidx.core.app.NotificationCompat
+import chat.rocket.reactnative.MainActivity
+
+/**
+ * Handles video conference call notifications with call-style UI.
+ * Displays incoming call notifications with Accept/Decline actions.
+ */
+class VideoConfNotification(private val context: Context) {
+
+ companion object {
+ private const val TAG = "RocketChat.VideoConf"
+
+ const val CHANNEL_ID = "video-conf-call"
+ const val CHANNEL_NAME = "Video Calls"
+
+ const val ACTION_ACCEPT = "chat.rocket.reactnative.ACTION_VIDEO_CONF_ACCEPT"
+ const val ACTION_DECLINE = "chat.rocket.reactnative.ACTION_VIDEO_CONF_DECLINE"
+ const val EXTRA_NOTIFICATION_DATA = "notification_data"
+
+ /**
+ * Cancels a video call notification by notification ID.
+ */
+ @JvmStatic
+ fun cancelById(context: Context, notificationId: Int) {
+ val manager = context.getSystemService(Context.NOTIFICATION_SERVICE) as? NotificationManager
+ manager?.cancel(notificationId)
+ Log.d(TAG, "Video call notification cancelled with ID: $notificationId")
+ }
+ }
+
+ private val notificationManager: NotificationManager? =
+ context.getSystemService(Context.NOTIFICATION_SERVICE) as? NotificationManager
+
+ init {
+ createNotificationChannel()
+ }
+
+ /**
+ * Creates the notification channel for video calls with high importance and ringtone sound.
+ */
+ private fun createNotificationChannel() {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ val channel = NotificationChannel(
+ CHANNEL_ID,
+ CHANNEL_NAME,
+ NotificationManager.IMPORTANCE_HIGH
+ ).apply {
+ description = "Incoming video conference calls"
+ enableLights(true)
+ enableVibration(true)
+ lockscreenVisibility = Notification.VISIBILITY_PUBLIC
+
+ // Set ringtone sound
+ val ringtoneUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_RINGTONE)
+ val audioAttributes = AudioAttributes.Builder()
+ .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
+ .setUsage(AudioAttributes.USAGE_NOTIFICATION_RINGTONE)
+ .build()
+ setSound(ringtoneUri, audioAttributes)
+ }
+
+ notificationManager?.createNotificationChannel(channel)
+ }
+ }
+
+ /**
+ * Displays an incoming video call notification.
+ *
+ * @param bundle The notification data bundle
+ * @param ejson The parsed notification payload
+ */
+ fun showIncomingCall(bundle: Bundle, ejson: Ejson) {
+ val rid = ejson.rid
+ // Video conf uses 'caller' field, regular messages use 'sender'
+ val callerId: String
+ val callerName: String
+
+ if (ejson.caller != null) {
+ callerId = ejson.caller._id ?: ""
+ callerName = ejson.caller.name ?: "Unknown"
+ } else if (ejson.sender != null) {
+ // Fallback to sender if caller is not present
+ callerId = ejson.sender._id ?: ""
+ callerName = ejson.sender.name ?: ejson.senderName ?: "Unknown"
+ } else {
+ callerId = ""
+ callerName = "Unknown"
+ }
+
+ // Generate unique notification ID from rid + callerId
+ val notificationIdStr = (rid + callerId).replace(Regex("[^A-Za-z0-9]"), "")
+ val notificationId = notificationIdStr.hashCode()
+
+ Log.d(TAG, "Showing incoming call notification from: $callerName")
+
+ // Create intent data for actions - include all required fields for JS
+ val intentData = Bundle().apply {
+ putString("rid", rid ?: "")
+ putString("notificationType", "videoconf")
+ putString("callerId", callerId)
+ putString("callerName", callerName)
+ putString("host", ejson.host ?: "")
+ putString("callId", ejson.callId ?: "")
+ putString("ejson", bundle.getString("ejson", "{}"))
+ putInt("notificationId", notificationId)
+ }
+
+ Log.d(TAG, "Intent data - rid: $rid, callerId: $callerId, host: ${ejson.host}, callId: ${ejson.callId}")
+
+ // Full screen intent - opens app when notification is tapped
+ val fullScreenIntent = Intent(context, MainActivity::class.java).apply {
+ flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP
+ putExtras(intentData)
+ putExtra("event", "default")
+ }
+
+ val fullScreenPendingIntent = createPendingIntent(notificationId, fullScreenIntent)
+
+ // Accept action - directly opens MainActivity (Android 12+ blocks trampoline pattern)
+ val acceptIntent = Intent(context, MainActivity::class.java).apply {
+ flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP
+ putExtras(intentData)
+ putExtra("event", "accept")
+ putExtra("videoConfAction", true)
+ action = "${ACTION_ACCEPT}_$notificationId" // Unique action to differentiate intents
+ }
+
+ val acceptPendingIntent = createPendingIntent(notificationId + 1, acceptIntent)
+
+ // Decline action - directly opens MainActivity
+ val declineIntent = Intent(context, MainActivity::class.java).apply {
+ flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP
+ putExtras(intentData)
+ putExtra("event", "decline")
+ putExtra("videoConfAction", true)
+ action = "${ACTION_DECLINE}_$notificationId" // Unique action to differentiate intents
+ }
+
+ val declinePendingIntent = createPendingIntent(notificationId + 2, declineIntent)
+
+ // Get icons
+ val packageName = context.packageName
+ val smallIconResId = context.resources.getIdentifier("ic_notification", "drawable", packageName)
+
+ // Build notification
+ val builder = NotificationCompat.Builder(context, CHANNEL_ID).apply {
+ setSmallIcon(smallIconResId)
+ setContentTitle("Incoming call")
+ setContentText("Video call from $callerName")
+ priority = NotificationCompat.PRIORITY_HIGH
+ setCategory(NotificationCompat.CATEGORY_CALL)
+ setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
+ setAutoCancel(false)
+ setOngoing(true)
+ setFullScreenIntent(fullScreenPendingIntent, true)
+ setContentIntent(fullScreenPendingIntent)
+ addAction(0, "Decline", declinePendingIntent)
+ addAction(0, "Accept", acceptPendingIntent)
+ }
+
+ // Set sound for pre-O devices
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
+ val ringtoneUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_RINGTONE)
+ builder.setSound(ringtoneUri)
+ }
+
+ // Show notification
+ notificationManager?.notify(notificationId, builder.build())
+ Log.d(TAG, "Video call notification displayed with ID: $notificationId")
+ }
+
+ /**
+ * Creates a PendingIntent with appropriate flags for the Android version.
+ */
+ private fun createPendingIntent(requestCode: Int, intent: Intent): PendingIntent {
+ val flags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
+ PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
+ } else {
+ PendingIntent.FLAG_UPDATE_CURRENT
+ }
+ return PendingIntent.getActivity(context, requestCode, intent, flags)
+ }
+
+ /**
+ * Cancels a video call notification.
+ *
+ * @param rid The room ID
+ * @param callerId The caller's user ID
+ */
+ fun cancelCall(rid: String, callerId: String) {
+ val notificationIdStr = (rid + callerId).replace(Regex("[^A-Za-z0-9]"), "")
+ val notificationId = notificationIdStr.hashCode()
+
+ notificationManager?.cancel(notificationId)
+ Log.d(TAG, "Video call notification cancelled with ID: $notificationId")
+ }
+}
diff --git a/android/app/src/main/java/chat/rocket/reactnative/notification/VideoConfTurboPackage.kt b/android/app/src/main/java/chat/rocket/reactnative/notification/VideoConfTurboPackage.kt
new file mode 100644
index 00000000000..42e3f3eb211
--- /dev/null
+++ b/android/app/src/main/java/chat/rocket/reactnative/notification/VideoConfTurboPackage.kt
@@ -0,0 +1,37 @@
+package chat.rocket.reactnative.notification
+
+import com.facebook.react.TurboReactPackage
+import com.facebook.react.bridge.NativeModule
+import com.facebook.react.bridge.ReactApplicationContext
+import com.facebook.react.module.model.ReactModuleInfo
+import com.facebook.react.module.model.ReactModuleInfoProvider
+
+/**
+ * React Native TurboModule package for video conference notification module.
+ */
+class VideoConfTurboPackage : TurboReactPackage() {
+
+ override fun getModule(name: String, reactContext: ReactApplicationContext): NativeModule? {
+ return if (name == NativeVideoConfSpec.NAME) {
+ VideoConfModule(reactContext)
+ } else {
+ null
+ }
+ }
+
+ override fun getReactModuleInfoProvider(): ReactModuleInfoProvider {
+ return ReactModuleInfoProvider {
+ mapOf(
+ NativeVideoConfSpec.NAME to ReactModuleInfo(
+ NativeVideoConfSpec.NAME,
+ NativeVideoConfSpec.NAME,
+ false, // canOverrideExistingModule
+ false, // needsEagerInit
+ false, // hasConstants
+ false, // isCxxModule
+ true // isTurboModule
+ )
+ )
+ }
+ }
+}
diff --git a/app/index.tsx b/app/index.tsx
index 3e87e3bcaa0..6b8cd73920f 100644
--- a/app/index.tsx
+++ b/app/index.tsx
@@ -138,7 +138,10 @@ export default class Root extends React.Component<{}, IState> {
return;
}
- await getInitialNotification();
+ const handledVideoConf = await getInitialNotification();
+ if (handledVideoConf) {
+ return;
+ }
// Open app from deep linking
const deepLinking = await Linking.getInitialURL();
diff --git a/app/lib/native/NativeVideoConfAndroid.ts b/app/lib/native/NativeVideoConfAndroid.ts
new file mode 100644
index 00000000000..90346d97cb8
--- /dev/null
+++ b/app/lib/native/NativeVideoConfAndroid.ts
@@ -0,0 +1,9 @@
+import type { TurboModule } from 'react-native';
+import { TurboModuleRegistry } from 'react-native';
+
+export interface Spec extends TurboModule {
+ getPendingAction(): Promise;
+ clearPendingAction(): void;
+}
+
+export default TurboModuleRegistry.get('VideoConfModule');
diff --git a/app/lib/notifications/index.ts b/app/lib/notifications/index.ts
index 1d7ad538494..65adab40d62 100644
--- a/app/lib/notifications/index.ts
+++ b/app/lib/notifications/index.ts
@@ -17,39 +17,47 @@ interface IEjson {
export const onNotification = (push: INotification): void => {
const identifier = String(push?.payload?.action?.identifier);
+
+ // Handle video conf notification actions (Accept/Decline buttons)
if (identifier === 'ACCEPT_ACTION' || identifier === 'DECLINE_ACTION') {
- if (push?.payload && push?.payload?.ejson) {
- const notification = EJSON.parse(push?.payload?.ejson);
+ if (push?.payload?.ejson) {
+ const notification = EJSON.parse(push.payload.ejson);
store.dispatch(deepLinkingClickCallPush({ ...notification, event: identifier === 'ACCEPT_ACTION' ? 'accept' : 'decline' }));
return;
}
}
- if (push?.payload) {
- try {
- const notification = push?.payload;
- if (notification.ejson) {
- const { rid, name, sender, type, host, messageId }: IEjson = EJSON.parse(notification.ejson);
- const types: Record = {
- c: 'channel',
- d: 'direct',
- p: 'group',
- l: 'channels'
- };
- let roomName = type === SubscriptionType.DIRECT ? sender.username : name;
- if (type === SubscriptionType.OMNICHANNEL) {
- roomName = sender.name;
- }
+ if (push?.payload?.ejson) {
+ try {
+ const notification = EJSON.parse(push.payload.ejson);
- const params = {
- host,
- rid,
- messageId,
- path: `${types[type]}/${roomName}`
- };
- store.dispatch(deepLinkingOpen(params));
+ // Handle video conf notification tap (default action) - treat as accept
+ if (notification?.notificationType === 'videoconf') {
+ store.dispatch(deepLinkingClickCallPush({ ...notification, event: 'accept' }));
return;
}
+
+ // Handle regular message notifications
+ const { rid, name, sender, type, host, messageId }: IEjson = notification;
+ const types: Record = {
+ c: 'channel',
+ d: 'direct',
+ p: 'group',
+ l: 'channels'
+ };
+ let roomName = type === SubscriptionType.DIRECT ? sender.username : name;
+ if (type === SubscriptionType.OMNICHANNEL) {
+ roomName = sender.name;
+ }
+
+ const params = {
+ host,
+ rid,
+ messageId,
+ path: `${types[type]}/${roomName}`
+ };
+ store.dispatch(deepLinkingOpen(params));
+ return;
} catch (e) {
console.warn(e);
}
@@ -58,12 +66,14 @@ export const onNotification = (push: INotification): void => {
};
export const getDeviceToken = (): string => deviceToken;
-export const setBadgeCount = (count?: number): void => setNotificationsBadgeCount(count);
-export const removeNotificationsAndBadge = () => {
- removeAllNotifications();
- setBadgeCount();
+export const setBadgeCount = (count?: number): void => {
+ setNotificationsBadgeCount(count);
+};
+export const removeNotificationsAndBadge = async (): Promise => {
+ await removeAllNotifications();
+ await setNotificationsBadgeCount();
};
-export const initializePushNotifications = (): Promise | undefined => {
- setBadgeCount();
+export const initializePushNotifications = async (): Promise => {
+ await setNotificationsBadgeCount();
return pushNotificationConfigure(onNotification);
};
diff --git a/app/lib/notifications/push.ts b/app/lib/notifications/push.ts
index 639e83733ef..9dc160ff2e3 100644
--- a/app/lib/notifications/push.ts
+++ b/app/lib/notifications/push.ts
@@ -1,13 +1,6 @@
-import {
- Notifications,
- type Registered,
- type RegistrationError,
- type NotificationCompletion,
- type Notification,
- NotificationAction,
- NotificationCategory
-} from 'react-native-notifications';
-import { PermissionsAndroid, Platform } from 'react-native';
+import * as Notifications from 'expo-notifications';
+import * as Device from 'expo-device';
+import { Platform } from 'react-native';
import { type INotification } from '../../definitions';
import { isIOS } from '../methods/helpers';
@@ -16,18 +9,154 @@ import I18n from '../../i18n';
export let deviceToken = '';
-export const setNotificationsBadgeCount = (count = 0): void => {
- if (isIOS) {
- Notifications.ios.setBadgeCount(count);
+export const setNotificationsBadgeCount = async (count = 0): Promise => {
+ try {
+ await Notifications.setBadgeCountAsync(count);
+ } catch (e) {
+ console.log('Failed to set badge count:', e);
}
};
-export const removeAllNotifications = (): void => {
- Notifications.removeAllDeliveredNotifications();
+export const removeAllNotifications = async (): Promise => {
+ try {
+ await Notifications.dismissAllNotificationsAsync();
+ } catch (e) {
+ console.log('Failed to dismiss notifications:', e);
+ }
};
let configured = false;
+/**
+ * Transform expo-notifications response to the INotification format expected by the app
+ */
+const transformNotificationResponse = (response: Notifications.NotificationResponse): INotification => {
+ const { notification, actionIdentifier, userText } = response;
+ const { trigger, content } = notification.request;
+
+ // Get the raw data from the notification
+ let payload: Record = {};
+
+ if (trigger && 'type' in trigger && trigger.type === 'push') {
+ if (Platform.OS === 'android' && 'remoteMessage' in trigger && trigger.remoteMessage) {
+ // Android: data comes from remoteMessage.data
+ payload = trigger.remoteMessage.data || {};
+ } else if (Platform.OS === 'ios' && 'payload' in trigger && trigger.payload) {
+ // iOS: data comes from payload (userInfo)
+ payload = trigger.payload as Record;
+ }
+ }
+
+ // Fallback to content.data if trigger data is not available
+ if (Object.keys(payload).length === 0 && content.data) {
+ payload = content.data as Record;
+ }
+
+ // Add action identifier if it's a specific action (not default tap)
+ if (actionIdentifier && actionIdentifier !== Notifications.DEFAULT_ACTION_IDENTIFIER) {
+ payload.action = { identifier: actionIdentifier };
+ if (userText) {
+ payload.action.userText = userText;
+ }
+ }
+
+ return {
+ payload: {
+ message: content.body || payload.message || '',
+ style: payload.style || '',
+ ejson: payload.ejson || '',
+ collapse_key: payload.collapse_key || '',
+ notId: payload.notId || notification.request.identifier || '',
+ msgcnt: payload.msgcnt || '',
+ title: content.title || payload.title || '',
+ from: payload.from || '',
+ image: payload.image || '',
+ soundname: payload.soundname || '',
+ action: payload.action
+ },
+ identifier: notification.request.identifier
+ };
+};
+
+/**
+ * Set up notification categories for iOS (actions like Reply, Accept, Decline)
+ */
+const setupNotificationCategories = async (): Promise => {
+ if (!isIOS) {
+ return;
+ }
+
+ try {
+ // Message category with Reply action
+ await Notifications.setNotificationCategoryAsync('MESSAGE', [
+ {
+ identifier: 'REPLY_ACTION',
+ buttonTitle: I18n.t('Reply'),
+ textInput: {
+ submitButtonTitle: I18n.t('Reply'),
+ placeholder: I18n.t('Type_message')
+ },
+ options: {
+ opensAppToForeground: false
+ }
+ }
+ ]);
+
+ // Video conference category with Accept/Decline actions
+ await Notifications.setNotificationCategoryAsync('VIDEOCONF', [
+ {
+ identifier: 'ACCEPT_ACTION',
+ buttonTitle: I18n.t('accept'),
+ options: {
+ opensAppToForeground: true
+ }
+ },
+ {
+ identifier: 'DECLINE_ACTION',
+ buttonTitle: I18n.t('decline'),
+ options: {
+ opensAppToForeground: true
+ }
+ }
+ ]);
+ } catch (e) {
+ console.log('Failed to set notification categories:', e);
+ }
+};
+
+/**
+ * Request notification permissions and register for push notifications
+ */
+const registerForPushNotifications = async (): Promise => {
+ if (!Device.isDevice) {
+ console.log('Push notifications require a physical device');
+ return null;
+ }
+
+ try {
+ // Check and request permissions
+ const { status: existingStatus } = await Notifications.getPermissionsAsync();
+ let finalStatus = existingStatus;
+
+ if (existingStatus !== 'granted') {
+ const { status } = await Notifications.requestPermissionsAsync();
+ finalStatus = status;
+ }
+
+ if (finalStatus !== 'granted') {
+ console.log('Failed to get push notification permissions');
+ return null;
+ }
+
+ // Get the device push token (FCM for Android, APNs for iOS)
+ const tokenData = await Notifications.getDevicePushTokenAsync();
+ return tokenData.data;
+ } catch (e) {
+ console.log('Error registering for push notifications:', e);
+ return null;
+ }
+};
+
export const pushNotificationConfigure = (onNotification: (notification: INotification) => void): Promise => {
if (configured) {
return Promise.resolve({ configured: true });
@@ -35,50 +164,37 @@ export const pushNotificationConfigure = (onNotification: (notification: INotifi
configured = true;
- if (isIOS) {
- // init
- Notifications.ios.registerRemoteNotifications();
-
- const notificationAction = new NotificationAction('REPLY_ACTION', 'background', I18n.t('Reply'), true, {
- buttonTitle: I18n.t('Reply'),
- placeholder: I18n.t('Type_message')
- });
- const acceptAction = new NotificationAction('ACCEPT_ACTION', 'foreground', I18n.t('accept'), true);
- const rejectAction = new NotificationAction('DECLINE_ACTION', 'foreground', I18n.t('decline'), true);
-
- const notificationCategory = new NotificationCategory('MESSAGE', [notificationAction]);
- const videoConfCategory = new NotificationCategory('VIDEOCONF', [acceptAction, rejectAction]);
-
- Notifications.setCategories([videoConfCategory, notificationCategory]);
- } else if (Platform.OS === 'android' && Platform.constants.Version >= 33) {
- // @ts-ignore
- PermissionsAndroid.request('android.permission.POST_NOTIFICATIONS').then(permissionStatus => {
- if (permissionStatus === 'granted') {
- Notifications.registerRemoteNotifications();
- } else {
- // TODO: Ask user to enable notifications
- }
- });
- } else {
- Notifications.registerRemoteNotifications();
- }
+ // Set up how notifications should be handled when the app is in foreground
+ Notifications.setNotificationHandler({
+ handleNotification: () =>
+ Promise.resolve({
+ shouldShowAlert: false,
+ shouldPlaySound: false,
+ shouldSetBadge: false,
+ shouldShowBanner: false,
+ shouldShowList: false
+ })
+ });
+
+ // Set up notification categories for iOS
+ setupNotificationCategories();
- Notifications.events().registerRemoteNotificationsRegistered((event: Registered) => {
- deviceToken = event.deviceToken;
+ // Register for push notifications and get token
+ registerForPushNotifications().then(token => {
+ if (token) {
+ deviceToken = token;
+ }
});
- Notifications.events().registerRemoteNotificationsRegistrationFailed((event: RegistrationError) => {
- // TODO: Handle error
- console.log(event);
+ // Listen for token updates
+ Notifications.addPushTokenListener(tokenData => {
+ deviceToken = tokenData.data;
});
- Notifications.events().registerNotificationReceivedForeground(
- (notification: Notification, completion: (response: NotificationCompletion) => void) => {
- completion({ alert: false, sound: false, badge: false });
- }
- );
+ // Listen for notification responses (when user taps on notification)
+ Notifications.addNotificationResponseReceivedListener(response => {
+ const notification = transformNotificationResponse(response);
- Notifications.events().registerNotificationOpened((notification: Notification, completion: () => void) => {
if (isIOS) {
const { background } = reduxStore.getState().app;
if (background) {
@@ -87,14 +203,13 @@ export const pushNotificationConfigure = (onNotification: (notification: INotifi
} else {
onNotification(notification);
}
- completion();
});
- Notifications.events().registerNotificationReceivedBackground(
- (notification: Notification, completion: (response: any) => void) => {
- completion({ alert: true, sound: true, badge: false });
- }
- );
+ // Get initial notification (app was opened by tapping a notification)
+ const lastResponse = Notifications.getLastNotificationResponse();
+ if (lastResponse) {
+ return Promise.resolve(transformNotificationResponse(lastResponse));
+ }
- return Notifications.getInitialNotification();
+ return Promise.resolve(null);
};
diff --git a/app/lib/notifications/videoConf/backgroundNotificationHandler.ts b/app/lib/notifications/videoConf/backgroundNotificationHandler.ts
deleted file mode 100644
index ca33cc13787..00000000000
--- a/app/lib/notifications/videoConf/backgroundNotificationHandler.ts
+++ /dev/null
@@ -1,125 +0,0 @@
-import notifee, { AndroidCategory, AndroidFlags, AndroidImportance, AndroidVisibility, type Event } from '@notifee/react-native';
-import { getMessaging as messaging } from '@react-native-firebase/messaging';
-import AsyncStorage from '@react-native-async-storage/async-storage';
-import ejson from 'ejson';
-
-import { deepLinkingClickCallPush } from '../../../actions/deepLinking';
-import i18n from '../../../i18n';
-import { colors } from '../../constants/colors';
-import { store } from '../../store/auxStore';
-
-const VIDEO_CONF_CHANNEL = 'video-conf-call';
-const VIDEO_CONF_TYPE = 'videoconf';
-
-interface Caller {
- _id?: string;
- name?: string;
-}
-
-interface NotificationData {
- notificationType?: string;
- status?: number;
- rid?: string;
- caller?: Caller;
-}
-
-const createChannel = () =>
- notifee.createChannel({
- id: VIDEO_CONF_CHANNEL,
- name: 'Video Call',
- lights: true,
- vibration: true,
- importance: AndroidImportance.HIGH,
- sound: 'ringtone'
- });
-
-const handleBackgroundEvent = async (event: Event) => {
- const { pressAction, notification } = event.detail;
- const notificationData = notification?.data;
- if (
- typeof notificationData?.caller === 'object' &&
- (notificationData.caller as Caller)?._id &&
- (event.type === 1 || event.type === 2)
- ) {
- if (store?.getState()?.app.ready) {
- store.dispatch(deepLinkingClickCallPush({ ...notificationData, event: pressAction?.id }));
- } else {
- AsyncStorage.setItem('pushNotification', JSON.stringify({ ...notificationData, event: pressAction?.id }));
- }
- await notifee.cancelNotification(
- `${notificationData.rid}${(notificationData.caller as Caller)._id}`.replace(/[^A-Za-z0-9]/g, '')
- );
- }
-};
-
-const backgroundNotificationHandler = () => {
- notifee.onBackgroundEvent(handleBackgroundEvent);
-};
-
-const displayVideoConferenceNotification = async (notification: NotificationData) => {
- const id = `${notification.rid}${notification.caller?._id}`.replace(/[^A-Za-z0-9]/g, '');
- const actions = [
- {
- title: i18n.t('accept'),
- pressAction: {
- id: 'accept',
- launchActivity: 'default'
- }
- },
- {
- title: i18n.t('decline'),
- pressAction: {
- id: 'decline',
- launchActivity: 'default'
- }
- }
- ];
-
- await notifee.displayNotification({
- id,
- title: i18n.t('conference_call'),
- body: `${i18n.t('Incoming_call_from')} ${notification.caller?.name}`,
- data: notification as { [key: string]: string | number | object },
- android: {
- channelId: VIDEO_CONF_CHANNEL,
- category: AndroidCategory.CALL,
- visibility: AndroidVisibility.PUBLIC,
- importance: AndroidImportance.HIGH,
- smallIcon: 'ic_notification',
- color: colors.light.badgeBackgroundLevel4,
- actions,
- lightUpScreen: true,
- loopSound: true,
- sound: 'ringtone',
- autoCancel: false,
- ongoing: true,
- pressAction: {
- id: 'default',
- launchActivity: 'default'
- },
- flags: [AndroidFlags.FLAG_NO_CLEAR]
- }
- });
-};
-
-const setBackgroundNotificationHandler = () => {
- createChannel();
- messaging().setBackgroundMessageHandler(async message => {
- if (message?.data?.ejson) {
- const notification: NotificationData = ejson.parse(message?.data?.ejson as string);
- if (notification?.notificationType === VIDEO_CONF_TYPE) {
- if (notification.status === 0) {
- await displayVideoConferenceNotification(notification);
- } else if (notification.status === 4) {
- const id = `${notification.rid}${notification.caller?._id}`.replace(/[^A-Za-z0-9]/g, '');
- await notifee.cancelNotification(id);
- }
- }
- }
-
- return null;
- });
-};
-
-setBackgroundNotificationHandler();
-backgroundNotificationHandler();
diff --git a/app/lib/notifications/videoConf/getInitialNotification.ts b/app/lib/notifications/videoConf/getInitialNotification.ts
index 5e48a842ce1..25acc7cfe79 100644
--- a/app/lib/notifications/videoConf/getInitialNotification.ts
+++ b/app/lib/notifications/videoConf/getInitialNotification.ts
@@ -1,15 +1,62 @@
+import * as Notifications from 'expo-notifications';
+import EJSON from 'ejson';
+import { Platform } from 'react-native';
+
import { deepLinkingClickCallPush } from '../../../actions/deepLinking';
-import { isAndroid } from '../../methods/helpers';
import { store } from '../../store/auxStore';
+import NativeVideoConfModule from '../../native/NativeVideoConfAndroid';
-export const getInitialNotification = async (): Promise => {
- if (isAndroid) {
- const notifee = require('@notifee/react-native').default;
- const initialNotification = await notifee.getInitialNotification();
- if (initialNotification?.notification?.data?.notificationType === 'videoconf') {
- store.dispatch(
- deepLinkingClickCallPush({ ...initialNotification?.notification?.data, event: initialNotification?.pressAction?.id })
- );
+/**
+ * Check for pending video conference actions from native notification handling.
+ * @returns true if a video conf action was found and dispatched, false otherwise
+ */
+export const getInitialNotification = async (): Promise => {
+ // Android: Check native module for pending action
+ if (Platform.OS === 'android' && NativeVideoConfModule) {
+ try {
+ const pendingAction = await NativeVideoConfModule.getPendingAction();
+ if (pendingAction) {
+ const data = JSON.parse(pendingAction);
+ if (data?.notificationType === 'videoconf') {
+ store.dispatch(deepLinkingClickCallPush(data));
+ return true;
+ }
+ }
+ } catch (error) {
+ console.log('Error getting video conf initial notification:', error);
}
}
+
+ // iOS: Check expo-notifications for last response with video conf action
+ if (Platform.OS === 'ios') {
+ try {
+ const lastResponse = await Notifications.getLastNotificationResponseAsync();
+ if (lastResponse) {
+ const { actionIdentifier, notification } = lastResponse;
+ const { trigger } = notification.request;
+ let payload: Record = {};
+
+ if (trigger && 'type' in trigger && trigger.type === 'push' && 'payload' in trigger && trigger.payload) {
+ payload = trigger.payload as Record;
+ }
+
+ if (payload.ejson) {
+ const ejsonData = EJSON.parse(payload.ejson);
+ if (ejsonData?.notificationType === 'videoconf') {
+ // Accept/Decline actions or default tap (treat as accept)
+ let event = 'accept';
+ if (actionIdentifier === 'DECLINE_ACTION') {
+ event = 'decline';
+ }
+ store.dispatch(deepLinkingClickCallPush({ ...ejsonData, event }));
+ return true;
+ }
+ }
+ }
+ } catch (error) {
+ console.log('Error getting iOS video conf initial notification:', error);
+ }
+ }
+
+ return false;
};
diff --git a/app/sagas/deepLinking.js b/app/sagas/deepLinking.js
index abc28aba567..7afcadc7b92 100644
--- a/app/sagas/deepLinking.js
+++ b/app/sagas/deepLinking.js
@@ -223,7 +223,7 @@ const handleNavigateCallRoom = function* handleNavigateCallRoom({ params }) {
if (room) {
const isMasterDetail = yield select(state => state.app.isMasterDetail);
yield navigateToRoom({ item: room, isMasterDetail });
- const uid = params.caller._id;
+ const uid = params.caller?._id;
const { rid, callId, event } = params;
if (event === 'accept') {
yield call(notifyUser, `${uid}/video-conference`, {
@@ -246,6 +246,10 @@ const handleNavigateCallRoom = function* handleNavigateCallRoom({ params }) {
const handleClickCallPush = function* handleClickCallPush({ params }) {
let { host } = params;
+ if (!host) {
+ return;
+ }
+
if (host.slice(-1) === '/') {
host = host.slice(0, host.length - 1);
}
diff --git a/app/sagas/troubleshootingNotification.ts b/app/sagas/troubleshootingNotification.ts
index 0717c083f19..193568890e9 100644
--- a/app/sagas/troubleshootingNotification.ts
+++ b/app/sagas/troubleshootingNotification.ts
@@ -1,6 +1,6 @@
import { type Action } from 'redux';
import { call, takeLatest, put } from 'typed-redux-saga';
-import notifee, { AuthorizationStatus } from '@notifee/react-native';
+import * as Notifications from 'expo-notifications';
import { TROUBLESHOOTING_NOTIFICATION } from '../actions/actionsTypes';
import { setTroubleshootingNotification } from '../actions/troubleshootingNotification';
@@ -19,8 +19,8 @@ function* init() {
let defaultPushGateway = false;
let pushGatewayEnabled = false;
try {
- const { authorizationStatus } = yield* call(notifee.getNotificationSettings);
- deviceNotificationEnabled = authorizationStatus > AuthorizationStatus.DENIED;
+ const { status } = yield* call(Notifications.getPermissionsAsync);
+ deviceNotificationEnabled = status === 'granted';
} catch (e) {
log(e);
}
diff --git a/app/views/JitsiMeetView/index.tsx b/app/views/JitsiMeetView/index.tsx
index 67544b8de63..fe86c05d1c7 100644
--- a/app/views/JitsiMeetView/index.tsx
+++ b/app/views/JitsiMeetView/index.tsx
@@ -2,7 +2,7 @@ import CookieManager from '@react-native-cookies/cookies';
import { type RouteProp, useNavigation, useRoute } from '@react-navigation/native';
import { activateKeepAwake, deactivateKeepAwake } from 'expo-keep-awake';
import React, { useCallback, useEffect, useState } from 'react';
-import { ActivityIndicator, BackHandler, Linking, SafeAreaView, StyleSheet, View } from 'react-native';
+import { ActivityIndicator, Linking, SafeAreaView, StyleSheet, View } from 'react-native';
import WebView, { type WebViewNavigation } from 'react-native-webview';
import { userAgent } from '../../lib/constants/userAgent';
@@ -52,8 +52,8 @@ const JitsiMeetView = (): React.ReactElement => {
await Linking.openURL(`org.jitsi.meet://${callUrl}`);
goBack();
} catch (error) {
- // As the jitsi app was not opened, disable the backhandler on android
- BackHandler.addEventListener('hardwareBackPress', () => true);
+ // Jitsi app not installed - will use WebView instead
+ // No need to block back button as WebView handles navigation
}
}, [goBack, url]);
diff --git a/app/views/PushTroubleshootView/components/DeviceNotificationSettings.tsx b/app/views/PushTroubleshootView/components/DeviceNotificationSettings.tsx
index 9e77e43c47b..0adf5702b9d 100644
--- a/app/views/PushTroubleshootView/components/DeviceNotificationSettings.tsx
+++ b/app/views/PushTroubleshootView/components/DeviceNotificationSettings.tsx
@@ -1,4 +1,3 @@
-import notifee from '@notifee/react-native';
import React from 'react';
import { Linking } from 'react-native';
@@ -19,7 +18,7 @@ export default function DeviceNotificationSettings(): React.ReactElement {
if (isIOS) {
Linking.openURL('app-settings:');
} else {
- notifee.openNotificationSettings();
+ Linking.openSettings();
}
};
diff --git a/index.js b/index.js
index 4844e1da5dd..778e46478d1 100644
--- a/index.js
+++ b/index.js
@@ -22,9 +22,8 @@ if (process.env.USE_STORYBOOK) {
LogBox.ignoreAllLogs();
- if (isAndroid) {
- require('./app/lib/notifications/videoConf/backgroundNotificationHandler');
- }
+ // Note: Android video conference notifications are now handled natively
+ // in RCFirebaseMessagingService -> CustomPushNotification -> VideoConfNotification
AppRegistry.registerComponent(appName, () => require('./app/index').default);
}
diff --git a/ios/AppDelegate.swift b/ios/AppDelegate.swift
index 386a6feffd6..d5697908b38 100644
--- a/ios/AppDelegate.swift
+++ b/ios/AppDelegate.swift
@@ -27,8 +27,7 @@ public class AppDelegate: ExpoAppDelegate {
MMKV.initialize(rootDir: nil, groupDir: groupDir, logLevel: .debug)
}
- // Initialize notifications
- RNNotifications.startMonitorNotifications()
+ // Configure reply notification handler (integrates with expo-notifications)
ReplyNotification.configure()
let delegate = ReactNativeDelegate()
@@ -63,19 +62,6 @@ public class AppDelegate: ExpoAppDelegate {
return result
}
- // Remote Notification handling
- public override func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
- RNNotifications.didRegisterForRemoteNotifications(withDeviceToken: deviceToken)
- }
-
- public override func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) {
- RNNotifications.didFailToRegisterForRemoteNotificationsWithError(error)
- }
-
- public override func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable : Any], fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {
- RNNotifications.didReceiveBackgroundNotification(userInfo, withCompletionHandler: completionHandler)
- }
-
// Linking API
public override func application(
_ app: UIApplication,
diff --git a/ios/NotificationService/NotificationService.swift b/ios/NotificationService/NotificationService.swift
index 1d18a413e39..57cf88cf17b 100644
--- a/ios/NotificationService/NotificationService.swift
+++ b/ios/NotificationService/NotificationService.swift
@@ -13,11 +13,18 @@ class NotificationService: UNNotificationServiceExtension {
if let bestAttemptContent = bestAttemptContent {
let ejson = (bestAttemptContent.userInfo["ejson"] as? String ?? "").data(using: .utf8)!
guard let data = try? (JSONDecoder().decode(Payload.self, from: ejson)) else {
+ contentHandler(bestAttemptContent)
return
}
rocketchat = RocketChat(server: data.host.removeTrailingSlash())
+ // Handle video conference notifications
+ if data.notificationType == .videoconf {
+ self.processVideoConf(payload: data, request: request)
+ return
+ }
+
// If the notification has the content on the payload, show it
if data.notificationType != .messageIdOnly {
self.processPayload(payload: data)
@@ -35,17 +42,49 @@ class NotificationService: UNNotificationServiceExtension {
}
// Request the content from server
- self.rocketchat?.getPushWithId(data.messageId) { notification in
- if let notification = notification {
- self.bestAttemptContent?.title = notification.title
- self.bestAttemptContent?.body = notification.text
- self.processPayload(payload: notification.payload)
+ if let messageId = data.messageId {
+ self.rocketchat?.getPushWithId(messageId) { notification in
+ if let notification = notification {
+ self.bestAttemptContent?.title = notification.title
+ self.bestAttemptContent?.body = notification.text
+ self.processPayload(payload: notification.payload)
+ }
}
}
}
}
}
+ func processVideoConf(payload: Payload, request: UNNotificationRequest) {
+ guard let bestAttemptContent = bestAttemptContent else {
+ return
+ }
+
+ // Status 4 means call cancelled/ended - remove any existing notification
+ if payload.status == 4 {
+ if let rid = payload.rid, let callerId = payload.caller?._id {
+ let notificationId = "\(rid)\(callerId)".replacingOccurrences(of: "[^A-Za-z0-9]", with: "", options: .regularExpression)
+ UNUserNotificationCenter.current().removeDeliveredNotifications(withIdentifiers: [notificationId])
+ }
+ // Don't show anything for cancelled calls
+ contentHandler?(UNNotificationContent())
+ return
+ }
+
+ // Status 0 (or nil) means incoming call - show notification with actions
+ let callerName = payload.caller?.name ?? "Unknown"
+
+ bestAttemptContent.title = NSLocalizedString("Video Call", comment: "")
+ bestAttemptContent.body = String(format: NSLocalizedString("Incoming call from %@", comment: ""), callerName)
+ bestAttemptContent.categoryIdentifier = "VIDEOCONF"
+ bestAttemptContent.sound = UNNotificationSound(named: UNNotificationSoundName("ringtone.mp3"))
+ if #available(iOS 15.0, *) {
+ bestAttemptContent.interruptionLevel = .timeSensitive
+ }
+
+ contentHandler?(bestAttemptContent)
+ }
+
func processPayload(payload: Payload) {
// If is a encrypted message
if payload.messageType == .e2e {
diff --git a/ios/Podfile b/ios/Podfile
index 693b2016291..9fe51d0a279 100644
--- a/ios/Podfile
+++ b/ios/Podfile
@@ -36,8 +36,7 @@ end
$static_framework = [
'WatermelonDB',
'simdjson',
- 'react-native-mmkv-storage',
- 'react-native-notifications'
+ 'react-native-mmkv-storage'
]
pre_install do |installer|
Pod::Installer::Xcode::TargetValidator.send(:define_method, :verify_no_static_framework_transitive_dependencies) {}
diff --git a/ios/Podfile.lock b/ios/Podfile.lock
index b443f1d65a8..0ab9d11f83b 100644
--- a/ios/Podfile.lock
+++ b/ios/Podfile.lock
@@ -27,11 +27,15 @@ PODS:
- BVLinearGradient (2.6.2):
- React-Core
- DoubleConversion (1.1.6)
+ - EXApplication (7.0.8):
+ - ExpoModulesCore
- EXAV (15.1.3):
- ExpoModulesCore
- ReactCommon/turbomodule/core
- EXConstants (17.1.5):
- ExpoModulesCore
+ - EXNotifications (0.32.14):
+ - ExpoModulesCore
- Expo (53.0.7):
- DoubleConversion
- ExpoModulesCore
@@ -67,6 +71,8 @@ PODS:
- ExpoModulesCore
- ZXingObjC/OneD
- ZXingObjC/PDF417
+ - ExpoDevice (8.0.10):
+ - ExpoModulesCore
- ExpoDocumentPicker (13.1.4):
- ExpoModulesCore
- ExpoFileSystem (18.1.7):
@@ -1713,8 +1719,6 @@ PODS:
- Yoga
- react-native-netinfo (11.3.1):
- React-Core
- - react-native-notifications (5.1.0):
- - React-Core
- react-native-restart (0.0.22):
- React-Core
- react-native-safe-area-context (5.4.0):
@@ -2385,11 +2389,6 @@ PODS:
- React-Core
- RNLocalize (2.1.1):
- React-Core
- - RNNotifee (7.8.2):
- - React-Core
- - RNNotifee/NotifeeCore (= 7.8.2)
- - RNNotifee/NotifeeCore (7.8.2):
- - React-Core
- RNReanimated (3.17.1):
- DoubleConversion
- glog
@@ -2643,12 +2642,15 @@ DEPENDENCIES:
- "BugsnagReactNative (from `../node_modules/@bugsnag/react-native`)"
- BVLinearGradient (from `../node_modules/react-native-linear-gradient`)
- DoubleConversion (from `../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec`)
+ - EXApplication (from `../node_modules/expo-application/ios`)
- EXAV (from `../node_modules/expo-av/ios`)
- EXConstants (from `../node_modules/expo-constants/ios`)
+ - EXNotifications (from `../node_modules/expo-notifications/ios`)
- Expo (from `../node_modules/expo`)
- ExpoAppleAuthentication (from `../node_modules/expo-apple-authentication/ios`)
- ExpoAsset (from `../node_modules/expo-asset/ios`)
- ExpoCamera (from `../node_modules/expo-camera/ios`)
+ - ExpoDevice (from `../node_modules/expo-device/ios`)
- ExpoDocumentPicker (from `../node_modules/expo-document-picker/ios`)
- ExpoFileSystem (from `../node_modules/expo-file-system/ios`)
- ExpoFont (from `../node_modules/expo-font/ios`)
@@ -2706,7 +2708,6 @@ DEPENDENCIES:
- react-native-keyboard-controller (from `../node_modules/react-native-keyboard-controller`)
- react-native-mmkv-storage (from `../node_modules/react-native-mmkv-storage`)
- "react-native-netinfo (from `../node_modules/@react-native-community/netinfo`)"
- - react-native-notifications (from `../node_modules/react-native-notifications`)
- react-native-restart (from `../node_modules/react-native-restart`)
- react-native-safe-area-context (from `../node_modules/react-native-safe-area-context`)
- "react-native-slider (from `../node_modules/@react-native-community/slider`)"
@@ -2758,7 +2759,6 @@ DEPENDENCIES:
- RNImageCropPicker (from `../node_modules/react-native-image-crop-picker`)
- RNKeychain (from `../node_modules/react-native-keychain`)
- RNLocalize (from `../node_modules/react-native-localize`)
- - "RNNotifee (from `../node_modules/@notifee/react-native`)"
- RNReanimated (from `../node_modules/react-native-reanimated`)
- RNScreens (from `../node_modules/react-native-screens`)
- RNSVG (from `../node_modules/react-native-svg`)
@@ -2805,10 +2805,14 @@ EXTERNAL SOURCES:
:path: "../node_modules/react-native-linear-gradient"
DoubleConversion:
:podspec: "../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec"
+ EXApplication:
+ :path: "../node_modules/expo-application/ios"
EXAV:
:path: "../node_modules/expo-av/ios"
EXConstants:
:path: "../node_modules/expo-constants/ios"
+ EXNotifications:
+ :path: "../node_modules/expo-notifications/ios"
Expo:
:path: "../node_modules/expo"
ExpoAppleAuthentication:
@@ -2817,6 +2821,8 @@ EXTERNAL SOURCES:
:path: "../node_modules/expo-asset/ios"
ExpoCamera:
:path: "../node_modules/expo-camera/ios"
+ ExpoDevice:
+ :path: "../node_modules/expo-device/ios"
ExpoDocumentPicker:
:path: "../node_modules/expo-document-picker/ios"
ExpoFileSystem:
@@ -2928,8 +2934,6 @@ EXTERNAL SOURCES:
:path: "../node_modules/react-native-mmkv-storage"
react-native-netinfo:
:path: "../node_modules/@react-native-community/netinfo"
- react-native-notifications:
- :path: "../node_modules/react-native-notifications"
react-native-restart:
:path: "../node_modules/react-native-restart"
react-native-safe-area-context:
@@ -3032,8 +3036,6 @@ EXTERNAL SOURCES:
:path: "../node_modules/react-native-keychain"
RNLocalize:
:path: "../node_modules/react-native-localize"
- RNNotifee:
- :path: "../node_modules/@notifee/react-native"
RNReanimated:
:path: "../node_modules/react-native-reanimated"
RNScreens:
@@ -3052,12 +3054,15 @@ SPEC CHECKSUMS:
BugsnagReactNative: 8150cc1facb5c69c7a5d27d614fc50b4ed03c2b8
BVLinearGradient: 7815a70ab485b7b155186dd0cc836363e0288cad
DoubleConversion: cb417026b2400c8f53ae97020b2be961b59470cb
+ EXApplication: 1e98d4b1dccdf30627f92917f4b2c5a53c330e5f
EXAV: 90c33266835bf7e61dc0731fa8d82833d22d286f
EXConstants: a1112af878fddfe6acc0399473ac56a07ced0f47
+ EXNotifications: 52c982ff8aa4ed971534e27f20dbf532aa4082b1
Expo: bb70dfd014457bcca19f33e8c783afdc18308434
ExpoAppleAuthentication: b589b71be6bb817decf8f35e92c6281365140289
ExpoAsset: 3bc9adb7dbbf27ae82c18ca97eb988a3ae7e73b1
ExpoCamera: 105a9a963c443a3e112c51dd81290d81cd8da94a
+ ExpoDevice: 6327c3c200816795708885adf540d26ecab83d1a
ExpoDocumentPicker: 344f16224e6a8a088f2693667a8b713160f8f57b
ExpoFileSystem: 175267faf2b38511b01ac110243b13754dac57d3
ExpoFont: abbb91a911eb961652c2b0a22eef801860425ed6
@@ -3133,7 +3138,6 @@ SPEC CHECKSUMS:
react-native-keyboard-controller: 9ec7ee23328c30251a399cffd8b54324a00343bf
react-native-mmkv-storage: 9afc38c25213482f668c80bf2f0a50f75dda1777
react-native-netinfo: 2e3c27627db7d49ba412bfab25834e679db41e21
- react-native-notifications: 3bafa1237ae8a47569a84801f17d80242fe9f6a5
react-native-restart: f6f591aeb40194c41b9b5013901f00e6cf7d0f29
react-native-safe-area-context: 5928d84c879db2f9eb6969ca70e68f58623dbf25
react-native-slider: 605e731593322c4bb2eb48d7d64e2e4dbf7cbd77
@@ -3185,7 +3189,6 @@ SPEC CHECKSUMS:
RNImageCropPicker: b219389d3a300679b396e81d501e8c8169ffa3c0
RNKeychain: bbe2f6d5cc008920324acb49ef86ccc03d3b38e4
RNLocalize: ca86348d88b9a89da0e700af58d428ab3f343c4e
- RNNotifee: 8768d065bf1e2f9f8f347b4bd79147431c7eacd6
RNReanimated: f52ccd5ceea2bae48d7421eec89b3f0c10d7b642
RNScreens: b13e4c45f0406f33986a39c0d8da0324bff94435
RNSVG: 680e961f640e381aab730a04b2371969686ed9f7
@@ -3200,6 +3203,6 @@ SPEC CHECKSUMS:
Yoga: dfabf1234ccd5ac41d1b1d43179f024366ae9831
ZXingObjC: 8898711ab495761b2dbbdec76d90164a6d7e14c5
-PODFILE CHECKSUM: 4c73563b34520b90c036817cdb9ccf65fea5f5c5
+PODFILE CHECKSUM: 80f90fcad8e4b1f21b36f1dbbae085fa0ddff08c
COCOAPODS: 1.15.2
diff --git a/ios/ReplyNotification.swift b/ios/ReplyNotification.swift
index cffb486528e..3d3a2de7403 100644
--- a/ios/ReplyNotification.swift
+++ b/ios/ReplyNotification.swift
@@ -9,49 +9,110 @@
import Foundation
import UserNotifications
+/// Handles direct reply from iOS notifications.
+/// Intercepts REPLY_ACTION responses and sends messages natively,
+/// while forwarding all other notification events to expo-notifications.
@objc(ReplyNotification)
-class ReplyNotification: RNNotificationEventHandler {
- private static let dispatchOnce: Void = {
- let instance: AnyClass! = object_getClass(ReplyNotification())
- let originalMethod = class_getInstanceMethod(instance, #selector(didReceive))
- let swizzledMethod = class_getInstanceMethod(instance, #selector(replyNotification_didReceiveNotificationResponse))
- if let originalMethod = originalMethod, let swizzledMethod = swizzledMethod {
- method_exchangeImplementations(originalMethod, swizzledMethod)
- }
- }()
+class ReplyNotification: NSObject, UNUserNotificationCenterDelegate {
+ private static var shared: ReplyNotification?
+ private weak var originalDelegate: UNUserNotificationCenterDelegate?
@objc
public static func configure() {
- _ = self.dispatchOnce
+ let instance = ReplyNotification()
+ shared = instance
+
+ // Store the original delegate (expo-notifications) and set ourselves as the delegate
+ let center = UNUserNotificationCenter.current()
+ instance.originalDelegate = center.delegate
+ center.delegate = instance
}
- @objc
- func replyNotification_didReceiveNotificationResponse(_ response: UNNotificationResponse, completionHandler: @escaping(() -> Void)) {
+ // MARK: - UNUserNotificationCenterDelegate
+
+ func userNotificationCenter(
+ _ center: UNUserNotificationCenter,
+ didReceive response: UNNotificationResponse,
+ withCompletionHandler completionHandler: @escaping () -> Void
+ ) {
+ // Handle REPLY_ACTION natively
if response.actionIdentifier == "REPLY_ACTION" {
- if let notification = RCTConvert.unNotificationPayload(response.notification) {
- if let data = (notification["ejson"] as? String)?.data(using: .utf8) {
- if let payload = try? JSONDecoder().decode(Payload.self, from: data), let rid = payload.rid {
- if let msg = (response as? UNTextInputNotificationResponse)?.userText {
- let rocketchat = RocketChat(server: payload.host.removeTrailingSlash())
- let backgroundTask = UIApplication.shared.beginBackgroundTask(expirationHandler: nil)
-
- rocketchat.sendMessage(rid: rid, message: msg, threadIdentifier: payload.tmid) { response in
- guard let response = response, response.success else {
- let content = UNMutableNotificationContent()
- content.body = "Failed to reply message."
- let request = UNNotificationRequest(identifier: "replyFailure", content: content, trigger: nil)
- UNUserNotificationCenter.current().add(request, withCompletionHandler: nil)
- return
- }
- UIApplication.shared.endBackgroundTask(backgroundTask)
- }
- }
- }
+ handleReplyAction(response: response, completionHandler: completionHandler)
+ return
+ }
+
+ // Forward to original delegate (expo-notifications)
+ if let originalDelegate = originalDelegate {
+ originalDelegate.userNotificationCenter?(center, didReceive: response, withCompletionHandler: completionHandler)
+ } else {
+ completionHandler()
+ }
+ }
+
+ func userNotificationCenter(
+ _ center: UNUserNotificationCenter,
+ willPresent notification: UNNotification,
+ withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void
+ ) {
+ // Forward to original delegate (expo-notifications)
+ if let originalDelegate = originalDelegate {
+ originalDelegate.userNotificationCenter?(center, willPresent: notification, withCompletionHandler: completionHandler)
+ } else {
+ completionHandler([])
+ }
+ }
+
+ func userNotificationCenter(
+ _ center: UNUserNotificationCenter,
+ openSettingsFor notification: UNNotification?
+ ) {
+ // Forward to original delegate (expo-notifications)
+ if let originalDelegate = originalDelegate {
+ if #available(iOS 12.0, *) {
+ originalDelegate.userNotificationCenter?(center, openSettingsFor: notification)
+ }
+ }
+ }
+
+ // MARK: - Reply Handling
+
+ private func handleReplyAction(response: UNNotificationResponse, completionHandler: @escaping () -> Void) {
+ guard let textResponse = response as? UNTextInputNotificationResponse else {
+ completionHandler()
+ return
+ }
+
+ let userInfo = response.notification.request.content.userInfo
+
+ guard let ejsonString = userInfo["ejson"] as? String,
+ let ejsonData = ejsonString.data(using: .utf8),
+ let payload = try? JSONDecoder().decode(Payload.self, from: ejsonData),
+ let rid = payload.rid else {
+ completionHandler()
+ return
+ }
+
+ let message = textResponse.userText
+ let rocketchat = RocketChat(server: payload.host.removeTrailingSlash())
+ let backgroundTask = UIApplication.shared.beginBackgroundTask(expirationHandler: nil)
+
+ rocketchat.sendMessage(rid: rid, message: message, threadIdentifier: payload.tmid) { response in
+ // Ensure we're on the main thread for UI operations
+ DispatchQueue.main.async {
+ defer {
+ UIApplication.shared.endBackgroundTask(backgroundTask)
+ completionHandler()
+ }
+
+ guard let response = response, response.success else {
+ // Show failure notification
+ let content = UNMutableNotificationContent()
+ content.body = "Failed to reply message."
+ let request = UNNotificationRequest(identifier: "replyFailure", content: content, trigger: nil)
+ UNUserNotificationCenter.current().add(request, withCompletionHandler: nil)
+ return
}
}
- } else {
- let body = RNNotificationParser.parseNotificationResponse(response)
- RNEventEmitter.sendEvent(RNNotificationOpened, body: body)
}
}
}
diff --git a/ios/RocketChatRN-Bridging-Header.h b/ios/RocketChatRN-Bridging-Header.h
index 84cdd29326b..528f9e1aa39 100644
--- a/ios/RocketChatRN-Bridging-Header.h
+++ b/ios/RocketChatRN-Bridging-Header.h
@@ -4,12 +4,6 @@
#import
#import
-#import
-#import
-#import
-#import
-#import
-#import
#import
#import
#import
diff --git a/ios/RocketChatRN.xcodeproj/project.pbxproj b/ios/RocketChatRN.xcodeproj/project.pbxproj
index 7f679cbc227..80fbac43faf 100644
--- a/ios/RocketChatRN.xcodeproj/project.pbxproj
+++ b/ios/RocketChatRN.xcodeproj/project.pbxproj
@@ -1540,8 +1540,11 @@
inputPaths = (
"${PODS_ROOT}/Target Support Files/Pods-defaults-RocketChatRN/Pods-defaults-RocketChatRN-resources.sh",
"${PODS_CONFIGURATION_BUILD_DIR}/BugsnagReactNative/Bugsnag.bundle",
+ "${PODS_CONFIGURATION_BUILD_DIR}/EXApplication/ExpoApplication_privacy.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/EXConstants/EXConstants.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/EXConstants/ExpoConstants_privacy.bundle",
+ "${PODS_CONFIGURATION_BUILD_DIR}/EXNotifications/ExpoNotifications_privacy.bundle",
+ "${PODS_CONFIGURATION_BUILD_DIR}/ExpoDevice/ExpoDevice_privacy.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/ExpoFileSystem/ExpoFileSystem_privacy.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/ExpoSystemUI/ExpoSystemUI_privacy.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/FirebaseCore/FirebaseCore_Privacy.bundle",
@@ -1570,8 +1573,11 @@
name = "[CP] Copy Pods Resources";
outputPaths = (
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/Bugsnag.bundle",
+ "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoApplication_privacy.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/EXConstants.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoConstants_privacy.bundle",
+ "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoNotifications_privacy.bundle",
+ "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoDevice_privacy.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoFileSystem_privacy.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoSystemUI_privacy.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/FirebaseCore_Privacy.bundle",
@@ -1627,8 +1633,11 @@
inputPaths = (
"${PODS_ROOT}/Target Support Files/Pods-defaults-Rocket.Chat/Pods-defaults-Rocket.Chat-resources.sh",
"${PODS_CONFIGURATION_BUILD_DIR}/BugsnagReactNative/Bugsnag.bundle",
+ "${PODS_CONFIGURATION_BUILD_DIR}/EXApplication/ExpoApplication_privacy.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/EXConstants/EXConstants.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/EXConstants/ExpoConstants_privacy.bundle",
+ "${PODS_CONFIGURATION_BUILD_DIR}/EXNotifications/ExpoNotifications_privacy.bundle",
+ "${PODS_CONFIGURATION_BUILD_DIR}/ExpoDevice/ExpoDevice_privacy.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/ExpoFileSystem/ExpoFileSystem_privacy.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/ExpoSystemUI/ExpoSystemUI_privacy.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/FirebaseCore/FirebaseCore_Privacy.bundle",
@@ -1657,8 +1666,11 @@
name = "[CP] Copy Pods Resources";
outputPaths = (
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/Bugsnag.bundle",
+ "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoApplication_privacy.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/EXConstants.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoConstants_privacy.bundle",
+ "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoNotifications_privacy.bundle",
+ "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoDevice_privacy.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoFileSystem_privacy.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoSystemUI_privacy.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/FirebaseCore_Privacy.bundle",
@@ -1958,8 +1970,11 @@
inputPaths = (
"${PODS_ROOT}/Target Support Files/Pods-defaults-NotificationService/Pods-defaults-NotificationService-resources.sh",
"${PODS_CONFIGURATION_BUILD_DIR}/BugsnagReactNative/Bugsnag.bundle",
+ "${PODS_CONFIGURATION_BUILD_DIR}/EXApplication/ExpoApplication_privacy.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/EXConstants/EXConstants.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/EXConstants/ExpoConstants_privacy.bundle",
+ "${PODS_CONFIGURATION_BUILD_DIR}/EXNotifications/ExpoNotifications_privacy.bundle",
+ "${PODS_CONFIGURATION_BUILD_DIR}/ExpoDevice/ExpoDevice_privacy.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/ExpoFileSystem/ExpoFileSystem_privacy.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/ExpoSystemUI/ExpoSystemUI_privacy.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/FirebaseCore/FirebaseCore_Privacy.bundle",
@@ -1988,8 +2003,11 @@
name = "[CP] Copy Pods Resources";
outputPaths = (
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/Bugsnag.bundle",
+ "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoApplication_privacy.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/EXConstants.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoConstants_privacy.bundle",
+ "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoNotifications_privacy.bundle",
+ "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoDevice_privacy.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoFileSystem_privacy.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoSystemUI_privacy.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/FirebaseCore_Privacy.bundle",
diff --git a/ios/Shared/Models/NotificationType.swift b/ios/Shared/Models/NotificationType.swift
index 95e41bed78c..0e769aa0910 100644
--- a/ios/Shared/Models/NotificationType.swift
+++ b/ios/Shared/Models/NotificationType.swift
@@ -11,4 +11,5 @@ import Foundation
enum NotificationType: String, Codable {
case message = "message"
case messageIdOnly = "message-id-only"
+ case videoconf = "videoconf"
}
diff --git a/ios/Shared/Models/Payload.swift b/ios/Shared/Models/Payload.swift
index 124ccf612e6..2a91f3a1b38 100644
--- a/ios/Shared/Models/Payload.swift
+++ b/ios/Shared/Models/Payload.swift
@@ -8,12 +8,17 @@
import Foundation
+struct Caller: Codable {
+ let _id: String?
+ let name: String?
+}
+
struct Payload: Codable {
let host: String
let rid: String?
let type: RoomType?
let sender: Sender?
- let messageId: String
+ let messageId: String?
let notificationType: NotificationType?
let name: String?
let messageType: MessageType?
@@ -21,4 +26,9 @@ struct Payload: Codable {
let senderName: String?
let tmid: String?
let content: EncryptedContent?
+
+ // Video conference fields
+ let caller: Caller?
+ let callId: String?
+ let status: Int?
}
diff --git a/ios/Shared/RocketChat/API/Request.swift b/ios/Shared/RocketChat/API/Request.swift
index a1f6a728c57..97c54bf8574 100644
--- a/ios/Shared/RocketChat/API/Request.swift
+++ b/ios/Shared/RocketChat/API/Request.swift
@@ -55,6 +55,7 @@ extension Request {
request.httpMethod = method.rawValue
request.httpBody = body()
request.addValue(contentType, forHTTPHeaderField: "Content-Type")
+ request.addValue(userAgent, forHTTPHeaderField: "User-Agent")
if let userId = api.credentials?.userId {
request.addValue(userId, forHTTPHeaderField: "x-user-id")
@@ -69,4 +70,14 @@ extension Request {
return request
}
+
+ private var userAgent: String {
+ let osVersion = ProcessInfo.processInfo.operatingSystemVersion
+ let systemVersion = "\(osVersion.majorVersion).\(osVersion.minorVersion)"
+
+ let appVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "unknown"
+ let buildNumber = Bundle.main.infoDictionary?["CFBundleVersion"] as? String ?? "unknown"
+
+ return "RC Mobile; ios \(systemVersion); v\(appVersion) (\(buildNumber))"
+ }
}
diff --git a/ios/fastlane/Fastfile b/ios/fastlane/Fastfile
index f8df411c23d..318f743be2b 100644
--- a/ios/fastlane/Fastfile
+++ b/ios/fastlane/Fastfile
@@ -122,7 +122,7 @@ platform :ios do
build_path: "./fastlane/build",
configuration: "Release",
derived_data_path: "./fastlane/derived_data",
- xcargs: "-parallelizeTargets -jobs 4"
+ xcargs: "-parallelizeTargets -jobs 4 ARCHS=arm64"
)
end
diff --git a/jest.setup.js b/jest.setup.js
index 6374b2beb62..774f02d2527 100644
--- a/jest.setup.js
+++ b/jest.setup.js
@@ -136,18 +136,23 @@ jest.mock('@react-navigation/native', () => {
};
});
-jest.mock('react-native-notifications', () => ({
- Notifications: {
- getInitialNotification: jest.fn(() => Promise.resolve()),
- registerRemoteNotifications: jest.fn(),
- events: () => ({
- registerRemoteNotificationsRegistered: jest.fn(),
- registerRemoteNotificationsRegistrationFailed: jest.fn(),
- registerNotificationReceivedForeground: jest.fn(),
- registerNotificationReceivedBackground: jest.fn(),
- registerNotificationOpened: jest.fn()
- })
- }
+jest.mock('expo-notifications', () => ({
+ getDevicePushTokenAsync: jest.fn(() => Promise.resolve({ data: 'mock-token' })),
+ getPermissionsAsync: jest.fn(() => Promise.resolve({ status: 'granted' })),
+ requestPermissionsAsync: jest.fn(() => Promise.resolve({ status: 'granted' })),
+ setBadgeCountAsync: jest.fn(() => Promise.resolve(true)),
+ dismissAllNotificationsAsync: jest.fn(() => Promise.resolve()),
+ setNotificationHandler: jest.fn(),
+ setNotificationCategoryAsync: jest.fn(() => Promise.resolve()),
+ addNotificationReceivedListener: jest.fn(() => ({ remove: jest.fn() })),
+ addNotificationResponseReceivedListener: jest.fn(() => ({ remove: jest.fn() })),
+ addPushTokenListener: jest.fn(() => ({ remove: jest.fn() })),
+ getLastNotificationResponse: jest.fn(() => null),
+ DEFAULT_ACTION_IDENTIFIER: 'expo.modules.notifications.actions.DEFAULT'
+}));
+
+jest.mock('expo-device', () => ({
+ isDevice: true
}));
jest.mock('@discord/bottom-sheet', () => {
diff --git a/package.json b/package.json
index 18036f1628e..489c43244bc 100644
--- a/package.json
+++ b/package.json
@@ -28,7 +28,6 @@
"@discord/bottom-sheet": "bluesky-social/react-native-bottom-sheet",
"@expo/vector-icons": "^14.1.0",
"@hookform/resolvers": "^2.9.10",
- "@notifee/react-native": "7.8.2",
"@nozbe/watermelondb": "^0.28.1-0",
"@react-native-async-storage/async-storage": "^1.22.3",
"@react-native-camera-roll/camera-roll": "^7.10.0",
@@ -41,7 +40,6 @@
"@react-native-firebase/analytics": "^21.12.2",
"@react-native-firebase/app": "^21.12.2",
"@react-native-firebase/crashlytics": "^21.12.2",
- "@react-native-firebase/messaging": "^21.12.2",
"@react-native-masked-view/masked-view": "^0.3.1",
"@react-native-picker/picker": "^2.11.0",
"@react-native/codegen": "^0.80.0",
@@ -63,6 +61,7 @@
"expo-apple-authentication": "7.2.3",
"expo-av": "15.1.3",
"expo-camera": "16.1.5",
+ "expo-device": "^8.0.10",
"expo-document-picker": "13.1.4",
"expo-file-system": "18.1.7",
"expo-haptics": "14.1.3",
@@ -70,6 +69,7 @@
"expo-keep-awake": "14.1.3",
"expo-local-authentication": "16.0.3",
"expo-navigation-bar": "^4.2.4",
+ "expo-notifications": "^0.32.14",
"expo-status-bar": "^2.2.3",
"expo-system-ui": "^5.0.7",
"expo-video-thumbnails": "9.1.2",
@@ -108,7 +108,6 @@
"react-native-mime-types": "2.3.0",
"react-native-mmkv-storage": "^12.0.0",
"react-native-modal": "13.0.1",
- "react-native-notifications": "5.1.0",
"react-native-notifier": "1.6.1",
"react-native-picker-select": "9.0.1",
"react-native-platform-touchable": "1.1.1",
diff --git a/patches/@notifee+react-native+7.8.2.patch b/patches/@notifee+react-native+7.8.2.patch
deleted file mode 100644
index cdb17b7853a..00000000000
--- a/patches/@notifee+react-native+7.8.2.patch
+++ /dev/null
@@ -1,41 +0,0 @@
---- a/node_modules/@notifee/react-native/android/src/main/java/io/invertase/notifee/NotifeeApiModule.java
-+++ b/node_modules/@notifee/react-native/android/src/main/java/io/invertase/notifee/NotifeeApiModule.java
-@@ -238,7 +238,7 @@ public class NotifeeApiModule extends ReactContextBaseJavaModule implements Perm
- @ReactMethod
- public void requestPermission(Promise promise) {
- // For Android 12 and below, we return the notification settings
-- if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
-+ if (Build.VERSION.SDK_INT < 33) {
- Notifee.getInstance()
- .getNotificationSettings(
- (e, aBundle) -> NotifeeReactUtils.promiseResolver(promise, e, aBundle));
-@@ -265,7 +265,7 @@ public class NotifeeApiModule extends ReactContextBaseJavaModule implements Perm
- (e, aBundle) -> NotifeeReactUtils.promiseResolver(promise, e, aBundle));
-
- activity.requestPermissions(
-- new String[] {Manifest.permission.POST_NOTIFICATIONS},
-+ new String[] {"android.permission.POST_NOTIFICATIONS"},
- Notifee.REQUEST_CODE_NOTIFICATION_PERMISSION,
- this);
- }
-diff --git a/node_modules/@notifee/react-native/ios/NotifeeCore/NotifeeCore+UNUserNotificationCenter.m b/node_modules/@notifee/react-native/ios/NotifeeCore/NotifeeCore+UNUserNotificationCenter.m
-index cf8020d..3a1e080 100644
---- a/node_modules/@notifee/react-native/ios/NotifeeCore/NotifeeCore+UNUserNotificationCenter.m
-+++ b/node_modules/@notifee/react-native/ios/NotifeeCore/NotifeeCore+UNUserNotificationCenter.m
-@@ -179,11 +179,11 @@ - (void)userNotificationCenter:(UNUserNotificationCenter *)center
-
- _notificationOpenedAppID = notifeeNotification[@"id"];
-
-- // handle notification outside of notifee
-- if (notifeeNotification == nil) {
-- notifeeNotification =
-- [NotifeeCoreUtil parseUNNotificationRequest:response.notification.request];
-- }
-+ // disable notifee handler on ios devices
-+ // if (notifeeNotification == nil) {
-+ // notifeeNotification =
-+ // [NotifeeCoreUtil parseUNNotificationRequest:response.notification.request];
-+ // }
-
- if (notifeeNotification != nil) {
- if ([response.actionIdentifier isEqualToString:UNNotificationDismissActionIdentifier]) {
diff --git a/patches/@react-native-firebase+messaging+21.12.2.patch b/patches/@react-native-firebase+messaging+21.12.2.patch
deleted file mode 100644
index 34905325f15..00000000000
--- a/patches/@react-native-firebase+messaging+21.12.2.patch
+++ /dev/null
@@ -1,19 +0,0 @@
-diff --git a/node_modules/@react-native-firebase/messaging/android/src/main/AndroidManifest.xml b/node_modules/@react-native-firebase/messaging/android/src/main/AndroidManifest.xml
-index 2f6741b..d4f4abb 100644
---- a/node_modules/@react-native-firebase/messaging/android/src/main/AndroidManifest.xml
-+++ b/node_modules/@react-native-firebase/messaging/android/src/main/AndroidManifest.xml
-@@ -9,12 +9,12 @@
-
-
--
-
-
-
--
-+ -->
-
--@import UserNotifications;
-+#import
-
- @interface RCTConvert (UIMutableUserNotificationAction)
- + (UIMutableUserNotificationAction *)UIMutableUserNotificationAction:(id)json;
-diff --git a/node_modules/react-native-notifications/lib/ios/RNNotificationCenter.h b/node_modules/react-native-notifications/lib/ios/RNNotificationCenter.h
-index 4bc5292..71df0bc 100644
---- a/node_modules/react-native-notifications/lib/ios/RNNotificationCenter.h
-+++ b/node_modules/react-native-notifications/lib/ios/RNNotificationCenter.h
-@@ -4,7 +4,7 @@ typedef void (^RCTPromiseResolveBlock)(id result);
- typedef void (^RCTResponseSenderBlock)(NSArray *response);
- typedef void (^RCTPromiseRejectBlock)(NSString *code, NSString *message, NSError *error);
-
--@import UserNotifications;
-+#import
-
- @interface RNNotificationCenter : NSObject
-
-diff --git a/node_modules/react-native-notifications/lib/ios/RNNotificationCenter.m b/node_modules/react-native-notifications/lib/ios/RNNotificationCenter.m
-index afd5c73..ec4dd85 100644
---- a/node_modules/react-native-notifications/lib/ios/RNNotificationCenter.m
-+++ b/node_modules/react-native-notifications/lib/ios/RNNotificationCenter.m
-@@ -48,14 +48,15 @@
- }
-
- - (void)setCategories:(NSArray *)json {
-- NSMutableSet* categories = nil;
-+ NSMutableSet* categories = [NSMutableSet new];
-
-- if ([json count] > 0) {
-- categories = [NSMutableSet new];
-- for (NSDictionary* categoryJson in json) {
-- [categories addObject:[RCTConvert UNMutableUserNotificationCategory:categoryJson]];
-+ for (NSDictionary* categoryJson in json) {
-+ UNNotificationCategory *category = [RCTConvert UNMutableUserNotificationCategory:categoryJson];
-+ if (category) {
-+ [categories addObject:category];
- }
- }
-+
- [[UNUserNotificationCenter currentNotificationCenter] setNotificationCategories:categories];
- }
-
-diff --git a/node_modules/react-native-notifications/lib/ios/RNNotificationCenterListener.h b/node_modules/react-native-notifications/lib/ios/RNNotificationCenterListener.h
-index 77a67dd..eaf0043 100644
---- a/node_modules/react-native-notifications/lib/ios/RNNotificationCenterListener.h
-+++ b/node_modules/react-native-notifications/lib/ios/RNNotificationCenterListener.h
-@@ -1,5 +1,5 @@
- #import
--@import UserNotifications;
-+#import
- #import "RNNotificationEventHandler.h"
-
- @interface RNNotificationCenterListener : NSObject
-diff --git a/node_modules/react-native-notifications/lib/ios/RNNotificationEventHandler.h b/node_modules/react-native-notifications/lib/ios/RNNotificationEventHandler.h
-index a07c6e9..8e3ca6a 100644
---- a/node_modules/react-native-notifications/lib/ios/RNNotificationEventHandler.h
-+++ b/node_modules/react-native-notifications/lib/ios/RNNotificationEventHandler.h
-@@ -1,5 +1,5 @@
- #import
--@import UserNotifications;
-+#import
- #import "RNNotificationsStore.h"
- #import "RNEventEmitter.h"
-
-diff --git a/node_modules/react-native-notifications/lib/ios/RNNotificationParser.h b/node_modules/react-native-notifications/lib/ios/RNNotificationParser.h
-index 7aa2bfb..c1c019c 100644
---- a/node_modules/react-native-notifications/lib/ios/RNNotificationParser.h
-+++ b/node_modules/react-native-notifications/lib/ios/RNNotificationParser.h
-@@ -1,5 +1,5 @@
- #import
--@import UserNotifications;
-+#import
-
- @interface RNNotificationParser : NSObject
-
-diff --git a/node_modules/react-native-notifications/lib/ios/RNNotificationParser.m b/node_modules/react-native-notifications/lib/ios/RNNotificationParser.m
-index 62f3043..840acda 100644
---- a/node_modules/react-native-notifications/lib/ios/RNNotificationParser.m
-+++ b/node_modules/react-native-notifications/lib/ios/RNNotificationParser.m
-@@ -12,7 +12,18 @@
- }
-
- + (NSDictionary *)parseNotificationResponse:(UNNotificationResponse *)response {
-- NSDictionary* responseDict = @{@"notification": [RCTConvert UNNotificationPayload:response.notification], @"identifier": response.notification.request.identifier, @"action": [self parseNotificationResponseAction:response]};
-+ NSMutableDictionary *notificationPayload = [[RCTConvert UNNotificationPayload:response.notification] mutableCopy];
-+
-+ NSDictionary *responseAction = [self parseNotificationResponseAction:response];
-+
-+ if (responseAction != nil) {
-+ [notificationPayload setObject:responseAction forKey:@"action"];
-+ }
-+
-+ NSDictionary *responseDict = @{
-+ @"notification": [notificationPayload copy],
-+ @"identifier": response.notification.request.identifier
-+ };
-
- return responseDict;
- }
-diff --git a/node_modules/react-native-notifications/lib/ios/RNNotificationsStore.h b/node_modules/react-native-notifications/lib/ios/RNNotificationsStore.h
-index 4f8a171..7e4f9ca 100644
---- a/node_modules/react-native-notifications/lib/ios/RNNotificationsStore.h
-+++ b/node_modules/react-native-notifications/lib/ios/RNNotificationsStore.h
-@@ -1,6 +1,6 @@
- #import
- #import
--@import UserNotifications;
-+#import
-
- @interface RNNotificationsStore : NSObject
-
diff --git a/react-native.config.js b/react-native.config.js
index 685aec05f4b..5af09eb7525 100644
--- a/react-native.config.js
+++ b/react-native.config.js
@@ -1,9 +1,3 @@
module.exports = {
- dependencies: {
- '@react-native-firebase/messaging': {
- platforms: {
- ios: null
- }
- }
- }
+ dependencies: {}
};
diff --git a/yarn.lock b/yarn.lock
index 42818383dc4..a9dc56dfc17 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2750,6 +2750,26 @@
xcode "^3.0.1"
xml2js "0.6.0"
+"@expo/config-plugins@~54.0.3":
+ version "54.0.3"
+ resolved "https://registry.yarnpkg.com/@expo/config-plugins/-/config-plugins-54.0.3.tgz#2b9ffd68a48e3b51299cdbe3ee777b9f5163fc03"
+ integrity sha512-tBIUZIxLQfCu5jmqTO+UOeeDUGIB0BbK6xTMkPRObAXRQeTLPPfokZRCo818d2owd+Bcmq1wBaDz0VY3g+glfw==
+ dependencies:
+ "@expo/config-types" "^54.0.9"
+ "@expo/json-file" "~10.0.7"
+ "@expo/plist" "^0.4.7"
+ "@expo/sdk-runtime-versions" "^1.0.0"
+ chalk "^4.1.2"
+ debug "^4.3.5"
+ getenv "^2.0.0"
+ glob "^13.0.0"
+ resolve-from "^5.0.0"
+ semver "^7.5.4"
+ slash "^3.0.0"
+ slugify "^1.6.6"
+ xcode "^3.0.1"
+ xml2js "0.6.0"
+
"@expo/config-types@^53.0.3":
version "53.0.3"
resolved "https://registry.yarnpkg.com/@expo/config-types/-/config-types-53.0.3.tgz#d083d9b095972e89eee96c41d085feb5b92d2749"
@@ -2760,6 +2780,11 @@
resolved "https://registry.yarnpkg.com/@expo/config-types/-/config-types-53.0.4.tgz#fe64fac734531ae883d18529b32586c23ffb1ceb"
integrity sha512-0s+9vFx83WIToEr0Iwy4CcmiUXa5BgwBmEjylBB2eojX5XAMm9mJvw9KpjAb8m7zq2G0Q6bRbeufkzgbipuNQg==
+"@expo/config-types@^54.0.9":
+ version "54.0.9"
+ resolved "https://registry.yarnpkg.com/@expo/config-types/-/config-types-54.0.9.tgz#b9279c47fe249b774fbd3358b6abddea08f1bcec"
+ integrity sha512-Llf4jwcrAnrxgE5WCdAOxtMf8FGwS4Sk0SSgI0NnIaSyCnmOCAm80GPFvsK778Oj19Ub4tSyzdqufPyeQPksWw==
+
"@expo/config@~11.0.7", "@expo/config@~11.0.8":
version "11.0.8"
resolved "https://registry.yarnpkg.com/@expo/config/-/config-11.0.8.tgz#658538d4321cf6edf6741f8b8506fda0046d5e94"
@@ -2798,6 +2823,25 @@
slugify "^1.3.4"
sucrase "3.35.0"
+"@expo/config@~12.0.11":
+ version "12.0.11"
+ resolved "https://registry.yarnpkg.com/@expo/config/-/config-12.0.11.tgz#b9f1cecd6bac4c2101bb8489b918556dc100434f"
+ integrity sha512-bGKNCbHirwgFlcOJHXpsAStQvM0nU3cmiobK0o07UkTfcUxl9q9lOQQh2eoMGqpm6Vs1IcwBpYye6thC3Nri/w==
+ dependencies:
+ "@babel/code-frame" "~7.10.4"
+ "@expo/config-plugins" "~54.0.3"
+ "@expo/config-types" "^54.0.9"
+ "@expo/json-file" "^10.0.7"
+ deepmerge "^4.3.1"
+ getenv "^2.0.0"
+ glob "^13.0.0"
+ require-from-string "^2.0.2"
+ resolve-from "^5.0.0"
+ resolve-workspace-root "^2.0.0"
+ semver "^7.6.0"
+ slugify "^1.3.4"
+ sucrase "~3.35.1"
+
"@expo/devcert@^1.1.2":
version "1.1.4"
resolved "https://registry.yarnpkg.com/@expo/devcert/-/devcert-1.1.4.tgz#d98807802a541847cc42791a606bfdc26e641277"
@@ -2827,6 +2871,17 @@
dotenv-expand "~11.0.6"
getenv "^1.0.0"
+"@expo/env@~2.0.8":
+ version "2.0.8"
+ resolved "https://registry.yarnpkg.com/@expo/env/-/env-2.0.8.tgz#2aea906eed3d297b2e19608dc1a800fba0a3fe03"
+ integrity sha512-5VQD6GT8HIMRaSaB5JFtOXuvfDVU80YtZIuUT/GDhUF782usIXY13Tn3IdDz1Tm/lqA9qnRZQ1BF4t7LlvdJPA==
+ dependencies:
+ chalk "^4.0.0"
+ debug "^4.3.4"
+ dotenv "~16.4.5"
+ dotenv-expand "~11.0.6"
+ getenv "^2.0.0"
+
"@expo/fingerprint@0.12.4":
version "0.12.4"
resolved "https://registry.yarnpkg.com/@expo/fingerprint/-/fingerprint-0.12.4.tgz#d4cc4de50e7b6d4e03b0d38850d1e4a136b74c8c"
@@ -2858,6 +2913,30 @@
temp-dir "~2.0.0"
unique-string "~2.0.0"
+"@expo/image-utils@^0.8.8":
+ version "0.8.8"
+ resolved "https://registry.yarnpkg.com/@expo/image-utils/-/image-utils-0.8.8.tgz#db5d460fd0c7101b10e9d027ffbe42f9cf115248"
+ integrity sha512-HHHaG4J4nKjTtVa1GG9PCh763xlETScfEyNxxOvfTRr8IKPJckjTyqSLEtdJoFNJ1vqiABEjW7tqGhqGibZLeA==
+ dependencies:
+ "@expo/spawn-async" "^1.7.2"
+ chalk "^4.0.0"
+ getenv "^2.0.0"
+ jimp-compact "0.16.1"
+ parse-png "^2.1.0"
+ resolve-from "^5.0.0"
+ resolve-global "^1.0.0"
+ semver "^7.6.0"
+ temp-dir "~2.0.0"
+ unique-string "~2.0.0"
+
+"@expo/json-file@^10.0.7", "@expo/json-file@~10.0.7":
+ version "10.0.8"
+ resolved "https://registry.yarnpkg.com/@expo/json-file/-/json-file-10.0.8.tgz#05e524d1ecc0011db0a6d66b525ea2f58cfe6d43"
+ integrity sha512-9LOTh1PgKizD1VXfGQ88LtDH0lRwq9lsTb4aichWTWSWqy3Ugfkhfm3BhzBIkJJfQQ5iJu3m/BoRlEIjoCGcnQ==
+ dependencies:
+ "@babel/code-frame" "~7.10.4"
+ json5 "^2.2.3"
+
"@expo/json-file@^9.1.4", "@expo/json-file@~9.1.4":
version "9.1.4"
resolved "https://registry.yarnpkg.com/@expo/json-file/-/json-file-9.1.4.tgz#e719d092c08afb3234643f9285e57c6a24989327"
@@ -2920,6 +2999,15 @@
base64-js "^1.2.3"
xmlbuilder "^15.1.1"
+"@expo/plist@^0.4.7":
+ version "0.4.8"
+ resolved "https://registry.yarnpkg.com/@expo/plist/-/plist-0.4.8.tgz#e014511a4a5008cf2b832b91caa8e9f2704127cc"
+ integrity sha512-pfNtErGGzzRwHP+5+RqswzPDKkZrx+Cli0mzjQaus1ZWFsog5ibL+nVT3NcporW51o8ggnt7x813vtRbPiyOrQ==
+ dependencies:
+ "@xmldom/xmldom" "^0.8.8"
+ base64-js "^1.2.3"
+ xmlbuilder "^15.1.1"
+
"@expo/prebuild-config@^9.0.5":
version "9.0.5"
resolved "https://registry.yarnpkg.com/@expo/prebuild-config/-/prebuild-config-9.0.5.tgz#b8b864b5e19489a1f66442ae30d5d7295f658297"
@@ -3433,6 +3521,23 @@
resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-2.0.2.tgz#d9fae00a2d5cb40f92cfe64b47ad749fbc38f917"
integrity sha512-6EwiSjwWYP7pTckG6I5eyFANjPhmPjUX9JRLUSfNPC7FX7zK9gyZAfUEaECL6ALTpGX5AjnBq3C9XmVWPitNpw==
+"@ide/backoff@^1.0.0":
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/@ide/backoff/-/backoff-1.0.0.tgz#466842c25bd4a4833e0642fab41ccff064010176"
+ integrity sha512-F0YfUDjvT+Mtt/R4xdl2X0EYCHMMiJqNLdxHD++jDT5ydEFIyqbCHh51Qx2E211dgZprPKhV7sHmnXKpLuvc5g==
+
+"@isaacs/balanced-match@^4.0.1":
+ version "4.0.1"
+ resolved "https://registry.yarnpkg.com/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz#3081dadbc3460661b751e7591d7faea5df39dd29"
+ integrity sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==
+
+"@isaacs/brace-expansion@^5.0.0":
+ version "5.0.0"
+ resolved "https://registry.yarnpkg.com/@isaacs/brace-expansion/-/brace-expansion-5.0.0.tgz#4b3dabab7d8e75a429414a96bd67bf4c1d13e0f3"
+ integrity sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==
+ dependencies:
+ "@isaacs/balanced-match" "^4.0.1"
+
"@isaacs/cliui@^8.0.2":
version "8.0.2"
resolved "https://registry.yarnpkg.com/@isaacs/cliui/-/cliui-8.0.2.tgz#b37667b7bc181c168782259bab42474fbf52b550"
@@ -3825,11 +3930,6 @@
"@nodelib/fs.scandir" "2.1.5"
fastq "^1.6.0"
-"@notifee/react-native@7.8.2":
- version "7.8.2"
- resolved "https://registry.yarnpkg.com/@notifee/react-native/-/react-native-7.8.2.tgz#72d3199ae830b4128ddaef3c1c2f11604759c9c4"
- integrity sha512-VG4IkWJIlOKqXwa3aExC3WFCVCGCC9BA55Ivg0SMRfEs+ruvYy/zlLANcrVGiPtgkUEryXDhA8SXx9+JcO8oLA==
-
"@nozbe/simdjson@3.9.4":
version "3.9.4"
resolved "https://registry.yarnpkg.com/@nozbe/simdjson/-/simdjson-3.9.4.tgz#64bb522c54cd22e40ff4a64d8f8e8e9285b123b6"
@@ -4281,11 +4381,6 @@
dependencies:
stacktrace-js "^2.0.2"
-"@react-native-firebase/messaging@^21.12.2":
- version "21.12.2"
- resolved "https://registry.yarnpkg.com/@react-native-firebase/messaging/-/messaging-21.12.2.tgz#9d8ec5dd21c562a6b5c5d3699fadde419c9f2a75"
- integrity sha512-t/MQgqclINESO/1yCbgXTHJxAdxIzAnjtgTw6bj/po/1JRGroT+HG3VDpep9a3Z35S4f6eF90AE5zZzcKu8IjQ==
-
"@react-native-masked-view/masked-view@^0.3.1":
version "0.3.1"
resolved "https://registry.yarnpkg.com/@react-native-masked-view/masked-view/-/masked-view-0.3.1.tgz#5bd76f17004a6ccbcec03856893777ee91f23d29"
@@ -6123,6 +6218,17 @@ asap@~2.0.6:
resolved "https://registry.yarnpkg.com/asap/-/asap-2.0.6.tgz#e50347611d7e690943208bbdafebcbc2fb866d46"
integrity sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==
+assert@^2.0.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/assert/-/assert-2.1.0.tgz#6d92a238d05dc02e7427c881fb8be81c8448b2dd"
+ integrity sha512-eLHpSK/Y4nhMJ07gDaAzoX/XAKS8PSaojml3M0DM4JpV1LAi5JOJ/p6H/XWrl8L+DzVEvVCW1z3vWAaB9oTsQw==
+ dependencies:
+ call-bind "^1.0.2"
+ is-nan "^1.3.2"
+ object-is "^1.1.5"
+ object.assign "^4.1.4"
+ util "^0.12.5"
+
assertion-error@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/assertion-error/-/assertion-error-2.0.1.tgz#f641a196b335690b1070bf00b6e7593fec190bf7"
@@ -6406,6 +6512,11 @@ babel-preset-jest@^29.6.3:
babel-plugin-jest-hoist "^29.6.3"
babel-preset-current-node-syntax "^1.0.0"
+badgin@^1.1.5:
+ version "1.2.3"
+ resolved "https://registry.yarnpkg.com/badgin/-/badgin-1.2.3.tgz#994b5f519827d7d5422224825b2c8faea2bc43ad"
+ integrity sha512-NQGA7LcfCpSzIbGRbkgjgdWkjy7HI+Th5VLxTJfW5EeaAf3fnS+xWQaQOCYiny+q6QSvxqoSO04vCx+4u++EJw==
+
balanced-match@^1.0.0:
version "1.0.2"
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee"
@@ -6595,6 +6706,16 @@ call-bind-apply-helpers@^1.0.0, call-bind-apply-helpers@^1.0.1, call-bind-apply-
es-errors "^1.3.0"
function-bind "^1.1.2"
+call-bind@^1.0.0, call-bind@^1.0.8:
+ version "1.0.8"
+ resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.8.tgz#0736a9660f537e3388826f440d5ec45f744eaa4c"
+ integrity sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==
+ dependencies:
+ call-bind-apply-helpers "^1.0.0"
+ es-define-property "^1.0.0"
+ get-intrinsic "^1.2.4"
+ set-function-length "^1.2.2"
+
call-bind@^1.0.2, call-bind@^1.0.5, call-bind@^1.0.6, call-bind@^1.0.7:
version "1.0.7"
resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.7.tgz#06016599c40c56498c18769d2730be242b6fa3b9"
@@ -6606,16 +6727,6 @@ call-bind@^1.0.2, call-bind@^1.0.5, call-bind@^1.0.6, call-bind@^1.0.7:
get-intrinsic "^1.2.4"
set-function-length "^1.2.1"
-call-bind@^1.0.8:
- version "1.0.8"
- resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.8.tgz#0736a9660f537e3388826f440d5ec45f744eaa4c"
- integrity sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==
- dependencies:
- call-bind-apply-helpers "^1.0.0"
- es-define-property "^1.0.0"
- get-intrinsic "^1.2.4"
- set-function-length "^1.2.2"
-
call-bound@^1.0.2, call-bound@^1.0.3, call-bound@^1.0.4:
version "1.0.4"
resolved "https://registry.yarnpkg.com/call-bound/-/call-bound-1.0.4.tgz#238de935d2a2a692928c538c7ccfa91067fd062a"
@@ -8322,6 +8433,11 @@ expo-apple-authentication@7.2.3:
resolved "https://registry.yarnpkg.com/expo-apple-authentication/-/expo-apple-authentication-7.2.3.tgz#524817c1b2c0b165343039d183ee91c4674be5fe"
integrity sha512-2izNn8qhUUM/gXMxA2byOn4AymUpmhaZlnGZy1vpndT0dMXd3T/Wk8j67rEB0+JhQY11iEQGXBG8cfro7LV0dA==
+expo-application@~7.0.8:
+ version "7.0.8"
+ resolved "https://registry.yarnpkg.com/expo-application/-/expo-application-7.0.8.tgz#320af0d6c39b331456d3bc833b25763c702d23db"
+ integrity sha512-qFGyxk7VJbrNOQWBbE09XUuGuvkOgFS9QfToaK2FdagM2aQ+x3CvGV2DuVgl/l4ZxPgIf3b/MNh9xHpwSwn74Q==
+
expo-asset@~11.1.5:
version "11.1.5"
resolved "https://registry.yarnpkg.com/expo-asset/-/expo-asset-11.1.5.tgz#5cad3d781c9d0edec31b9b3adbba574eb4d5dd3e"
@@ -8350,6 +8466,21 @@ expo-constants@~17.1.5:
"@expo/config" "~11.0.7"
"@expo/env" "~1.0.5"
+expo-constants@~18.0.11:
+ version "18.0.11"
+ resolved "https://registry.yarnpkg.com/expo-constants/-/expo-constants-18.0.11.tgz#522680c14f0a54cfd968d581961848c6397d44ae"
+ integrity sha512-xnfrfZ7lHjb+03skhmDSYeFF7OU2K3Xn/lAeP+7RhkV2xp2f5RCKtOUYajCnYeZesvMrsUxOsbGOP2JXSOH3NA==
+ dependencies:
+ "@expo/config" "~12.0.11"
+ "@expo/env" "~2.0.8"
+
+expo-device@^8.0.10:
+ version "8.0.10"
+ resolved "https://registry.yarnpkg.com/expo-device/-/expo-device-8.0.10.tgz#88be854d6de5568392ed814b44dad0e19d1d50f8"
+ integrity sha512-jd5BxjaF7382JkDMaC+P04aXXknB2UhWaVx5WiQKA05ugm/8GH5uaz9P9ckWdMKZGQVVEOC8MHaUADoT26KmFA==
+ dependencies:
+ ua-parser-js "^0.7.33"
+
expo-document-picker@13.1.4:
version "13.1.4"
resolved "https://registry.yarnpkg.com/expo-document-picker/-/expo-document-picker-13.1.4.tgz#f78a91e31dfac8ff26ea065bdc015ce4938eb1cc"
@@ -8429,6 +8560,19 @@ expo-navigation-bar@^4.2.4:
react-native-edge-to-edge "1.6.0"
react-native-is-edge-to-edge "^1.1.6"
+expo-notifications@^0.32.14:
+ version "0.32.14"
+ resolved "https://registry.yarnpkg.com/expo-notifications/-/expo-notifications-0.32.14.tgz#8f2161207e81272aac2fe4f1981b2ff7c8ef5529"
+ integrity sha512-IRxzsd94+c1sim7R9OWdICPINmL4iwsLWcG3n6FKgzZal2ZZbBym2/m/k5yv3NQORUpytqB373WBJDZvaPCtgw==
+ dependencies:
+ "@expo/image-utils" "^0.8.8"
+ "@ide/backoff" "^1.0.0"
+ abort-controller "^3.0.0"
+ assert "^2.0.0"
+ badgin "^1.1.5"
+ expo-application "~7.0.8"
+ expo-constants "~18.0.11"
+
expo-status-bar@^2.2.3:
version "2.2.3"
resolved "https://registry.yarnpkg.com/expo-status-bar/-/expo-status-bar-2.2.3.tgz#09385a866732328e0af3b4588c4f349a15fd7cd0"
@@ -8936,6 +9080,11 @@ getenv@^1.0.0:
resolved "https://registry.yarnpkg.com/getenv/-/getenv-1.0.0.tgz#874f2e7544fbca53c7a4738f37de8605c3fcfc31"
integrity sha512-7yetJWqbS9sbn0vIfliPsFgoXMKn/YMF+Wuiog97x+urnSRRRZ7xB+uVkwGKzRgq9CDFfMQnE9ruL5DHv9c6Xg==
+getenv@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/getenv/-/getenv-2.0.0.tgz#b1698c7b0f29588f4577d06c42c73a5b475c69e0"
+ integrity sha512-VilgtJj/ALgGY77fiLam5iD336eSWi96Q15JSAG1zi8NRBysm3LXKdGnHb4m5cuyxvOLQQKWpBZAT6ni4FI2iQ==
+
github-from-package@0.0.0:
version "0.0.0"
resolved "https://registry.yarnpkg.com/github-from-package/-/github-from-package-0.0.0.tgz#97fb5d96bfde8973313f20e8288ef9a167fa64ce"
@@ -8967,6 +9116,15 @@ glob@^10.3.10, glob@^10.4.2:
package-json-from-dist "^1.0.0"
path-scurry "^1.11.1"
+glob@^13.0.0:
+ version "13.0.0"
+ resolved "https://registry.yarnpkg.com/glob/-/glob-13.0.0.tgz#9d9233a4a274fc28ef7adce5508b7ef6237a1be3"
+ integrity sha512-tvZgpqk6fz4BaNZ66ZsRaZnbHvP/jG3uKJvAZOwEVUL4RTA5nJeeLYfyN9/VA8NX/V3IBG+hkeuGpKjvELkVhA==
+ dependencies:
+ minimatch "^10.1.1"
+ minipass "^7.1.2"
+ path-scurry "^2.0.0"
+
glob@^7.1.1, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6:
version "7.2.3"
resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b"
@@ -8979,6 +9137,13 @@ glob@^7.1.1, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6:
once "^1.3.0"
path-is-absolute "^1.0.0"
+global-dirs@^0.1.1:
+ version "0.1.1"
+ resolved "https://registry.yarnpkg.com/global-dirs/-/global-dirs-0.1.1.tgz#b319c0dd4607f353f3be9cca4c72fc148c49f445"
+ integrity sha512-NknMLn7F2J7aflwFOlGdNIuCDpN3VGoSoB+aap3KABFWbHVn1TCgFC+np23J8W2BiZbjfEw3BFBycSMv1AFblg==
+ dependencies:
+ ini "^1.3.4"
+
globals@^11.1.0:
version "11.12.0"
resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e"
@@ -9384,7 +9549,7 @@ inherits@2, inherits@2.0.4, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.3:
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
-ini@~1.3.0:
+ini@^1.3.4, ini@~1.3.0:
version "1.3.8"
resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.8.tgz#a29da425b48806f34767a4efce397269af28432c"
integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==
@@ -9562,6 +9727,14 @@ is-map@^2.0.3:
resolved "https://registry.yarnpkg.com/is-map/-/is-map-2.0.3.tgz#ede96b7fe1e270b3c4465e3a465658764926d62e"
integrity sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==
+is-nan@^1.3.2:
+ version "1.3.2"
+ resolved "https://registry.yarnpkg.com/is-nan/-/is-nan-1.3.2.tgz#043a54adea31748b55b6cd4e09aadafa69bd9e1d"
+ integrity sha512-E+zBKpQ2t6MEo1VsonYmluk9NxGrbzpeeLC2xIViuO2EjU2xsXsBPwTr3Ykv9l08UYEVEdWeRZNouaZqF6RN0w==
+ dependencies:
+ call-bind "^1.0.0"
+ define-properties "^1.1.3"
+
is-negative-zero@^2.0.3:
version "2.0.3"
resolved "https://registry.yarnpkg.com/is-negative-zero/-/is-negative-zero-2.0.3.tgz#ced903a027aca6381b777a5743069d7376a49747"
@@ -10784,6 +10957,11 @@ lru-cache@^10.2.0:
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.2.0.tgz#0bd445ca57363465900f4d1f9bd8db343a4d95c3"
integrity sha512-2bIM8x+VAf6JT4bKAljS1qUWgMsqZRPGJS6FSahIMPVvctcNhyVp7AJu7quxOW9jwkryBReKZY5tY5JYv2n/7Q==
+lru-cache@^11.0.0:
+ version "11.2.4"
+ resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-11.2.4.tgz#ecb523ebb0e6f4d837c807ad1abaea8e0619770d"
+ integrity sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg==
+
lru-cache@^4.1.1:
version "4.1.5"
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.1.5.tgz#8bbe50ea85bed59bc9e33dcab8235ee9bcf443cd"
@@ -11352,6 +11530,13 @@ minimatch@9.0.3:
dependencies:
brace-expansion "^2.0.1"
+minimatch@^10.1.1:
+ version "10.1.1"
+ resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-10.1.1.tgz#e6e61b9b0c1dcab116b5a7d1458e8b6ae9e73a55"
+ integrity sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ==
+ dependencies:
+ "@isaacs/brace-expansion" "^5.0.0"
+
minimatch@^3.0.2, minimatch@^3.0.4, minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2:
version "3.1.2"
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b"
@@ -11639,6 +11824,14 @@ object-inspect@^1.13.1:
resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.13.1.tgz#b96c6109324ccfef6b12216a956ca4dc2ff94bc2"
integrity sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==
+object-is@^1.1.5:
+ version "1.1.6"
+ resolved "https://registry.yarnpkg.com/object-is/-/object-is-1.1.6.tgz#1a6a53aed2dd8f7e6775ff870bea58545956ab07"
+ integrity sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q==
+ dependencies:
+ call-bind "^1.0.7"
+ define-properties "^1.2.1"
+
object-keys@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e"
@@ -11995,6 +12188,14 @@ path-scurry@^1.11.1:
lru-cache "^10.2.0"
minipass "^5.0.0 || ^6.0.2 || ^7.0.0"
+path-scurry@^2.0.0:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/path-scurry/-/path-scurry-2.0.1.tgz#4b6572376cfd8b811fca9cd1f5c24b3cbac0fe10"
+ integrity sha512-oWyT4gICAu+kaA7QWk/jvCHWarMKNs6pXOGWKDTr7cw4IGcUbW+PeTfbaQiLGheFRpjo6O9J0PmyMfQPjH71oA==
+ dependencies:
+ lru-cache "^11.0.0"
+ minipass "^7.1.2"
+
path-type@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b"
@@ -12567,11 +12768,6 @@ react-native-modal@13.0.1:
prop-types "^15.6.2"
react-native-animatable "1.3.3"
-react-native-notifications@5.1.0:
- version "5.1.0"
- resolved "https://registry.yarnpkg.com/react-native-notifications/-/react-native-notifications-5.1.0.tgz#8cba105fd57ab9d5df9d27284acf1e2b4f3d7ea3"
- integrity sha512-laqDSDlCvEASmJR6cXpqaryK855ejQd07vrfYERzhv68YDOoSkKy/URExRP4vAfAOVqHhix80tLbNUcfvZk2VQ==
-
react-native-notifier@1.6.1:
version "1.6.1"
resolved "https://registry.yarnpkg.com/react-native-notifier/-/react-native-notifier-1.6.1.tgz#eec07bdebed6c22cd22f5167555b7762e4119552"
@@ -13105,6 +13301,13 @@ resolve-from@^5.0.0:
resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-5.0.0.tgz#c35225843df8f776df21c57557bc087e9dfdfc69"
integrity sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==
+resolve-global@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/resolve-global/-/resolve-global-1.0.0.tgz#a2a79df4af2ca3f49bf77ef9ddacd322dad19255"
+ integrity sha512-zFa12V4OLtT5XUX/Q4VLvTfBf+Ok0SPc1FNGM/z9ctUdiU618qwKpWnd0CHs3+RqROfyEg/DhuHbMWYqcgljEw==
+ dependencies:
+ global-dirs "^0.1.1"
+
resolve-pkg-maps@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz#616b3dc2c57056b5588c31cdf4b3d64db133720f"
@@ -14039,6 +14242,19 @@ sucrase@3.35.0:
pirates "^4.0.1"
ts-interface-checker "^0.1.9"
+sucrase@~3.35.1:
+ version "3.35.1"
+ resolved "https://registry.yarnpkg.com/sucrase/-/sucrase-3.35.1.tgz#4619ea50393fe8bd0ae5071c26abd9b2e346bfe1"
+ integrity sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==
+ dependencies:
+ "@jridgewell/gen-mapping" "^0.3.2"
+ commander "^4.0.0"
+ lines-and-columns "^1.1.6"
+ mz "^2.7.0"
+ pirates "^4.0.1"
+ tinyglobby "^0.2.11"
+ ts-interface-checker "^0.1.9"
+
sudo-prompt@^8.2.0:
version "8.2.5"
resolved "https://registry.yarnpkg.com/sudo-prompt/-/sudo-prompt-8.2.5.tgz#cc5ef3769a134bb94b24a631cc09628d4d53603e"
@@ -14242,7 +14458,7 @@ tiny-invariant@^1.3.3:
resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.3.3.tgz#46680b7a873a0d5d10005995eb90a70d74d60127"
integrity sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==
-tinyglobby@^0.2.14:
+tinyglobby@^0.2.11, tinyglobby@^0.2.14:
version "0.2.15"
resolved "https://registry.yarnpkg.com/tinyglobby/-/tinyglobby-0.2.15.tgz#e228dd1e638cea993d2fdb4fcd2d4602a79951c2"
integrity sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==
@@ -14535,7 +14751,7 @@ typical@^5.2.0:
resolved "https://registry.yarnpkg.com/typical/-/typical-5.2.0.tgz#4daaac4f2b5315460804f0acf6cb69c52bb93066"
integrity sha512-dvdQgNDNJo+8B2uBQoqdb11eUCE1JQXhvjC/CZtgvZseVd5TYMXnq0+vuUemXbd/Se29cTaUuPX3YIc2xgbvIg==
-ua-parser-js@1.0.2:
+ua-parser-js@1.0.2, ua-parser-js@^0.7.33:
version "1.0.2"
resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-1.0.2.tgz#e2976c34dbfb30b15d2c300b2a53eac87c57a775"
integrity sha512-00y/AXhx0/SsnI51fTc0rLRmafiGOM4/O+ny10Ps7f+j/b8p/ZY11ytMgznXkOVo4GQ+KwQG5UQLkLGirsACRg==
@@ -14729,7 +14945,7 @@ util-deprecate@^1.0.1, util-deprecate@~1.0.1:
resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==
-util@^0.12.4:
+util@^0.12.4, util@^0.12.5:
version "0.12.5"
resolved "https://registry.yarnpkg.com/util/-/util-0.12.5.tgz#5f17a6059b73db61a875668781a1c2b136bd6fbc"
integrity sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==