Skip to content

fix(Android): Push notification on killed apps and deep link#6888

Merged
diegolmello merged 9 commits intodevelopfrom
fix.android-e2ee-killed-app
Jan 8, 2026
Merged

fix(Android): Push notification on killed apps and deep link#6888
diegolmello merged 9 commits intodevelopfrom
fix.android-e2ee-killed-app

Conversation

@diegolmello
Copy link
Copy Markdown
Member

@diegolmello diegolmello commented Jan 6, 2026

Proposed changes

This PR fixes a critical issue where the Android app was being killed when handling End-to-End Encrypted (E2E) push notifications. The root cause was that the notification processing logic was waiting for React Native initialization and using React context dependencies, which could cause the app to crash or be terminated by the system.

The solution simplifies the notification handling architecture by:

  1. Removing React Native dependencies from E2E notification decryption: E2E notifications now decrypt immediately using the native Android Context, eliminating the need to wait for React Native initialization. This makes notifications more responsive and prevents crashes.

  2. Eliminating E2ENotificationProcessor: Removed the separate E2ENotificationProcessor class that was creating unnecessary complexity and React context dependencies. All notification processing is now handled directly in CustomPushNotification.

  3. Implementing TurboModule for notification data: Added a new PushNotificationModule TurboModule to properly handle notification Intent data when the app opens from a notification tap, ensuring data is correctly passed to the JavaScript layer.

  4. Improving notification handling flow: Enhanced the notification processing pipeline to handle both regular and E2E notifications more reliably, with better error handling and logging.

Issue(s)

https://rocketchat.atlassian.net/browse/CORE-1645

How to test or reproduce

  1. Test E2E notification handling:

    • Enable E2E encryption on a room
    • Send an encrypted message to the Android app while the app is in the background or killed
    • Verify the notification appears correctly with the decrypted message content
    • Verify the app does not crash or get killed by the system
  2. Test notification tap handling:

    • Receive a push notification (both regular and E2E)
    • Tap on the notification to open the app
    • Verify the app navigates to the correct room/message
    • Verify notification data is correctly processed
  3. Test app state scenarios:

    • Test notifications when app is in foreground
    • Test notifications when app is in background
    • Test notifications when app is completely killed
    • Verify notifications work correctly in all scenarios
  4. Test video conf:

    • Enter a DM from other client
    • Start a videoconf call
    • Accept and decline should work

Screenshots

Types of changes

  • Bugfix (non-breaking change which fixes an issue)
  • Improvement (non-breaking change which improves a current function)
  • New feature (non-breaking change which adds functionality)
  • Documentation update (if none of the other choices apply)

Checklist

  • I have read the CONTRIBUTING doc
  • I have signed the CLA
  • Lint and unit tests pass locally with my changes
  • I have added tests that prove my fix is effective or that my feature works (if applicable)
  • I have added necessary documentation (if applicable)
  • Any dependent changes have been merged and published in downstream modules

Further comments

Technical Details

Key Changes:

  • Deleted: E2ENotificationProcessor.java - Removed unnecessary React context dependency
  • Modified: CustomPushNotification.java - Simplified to decrypt E2E notifications immediately using Android Context instead of waiting for React Native
  • Added: PushNotificationModule.kt - New TurboModule to handle notification Intent data
  • Added: NativePushNotificationSpec.kt and PushNotificationTurboPackage.kt - TurboModule infrastructure
  • Modified: MainActivity.kt and MainApplication.kt - Integrated new notification handling
  • Modified: app/lib/notifications/push.ts and related files - Updated to check for pending notifications

Why this approach:
The previous implementation tried to use React Native context and initialization checks, which could fail when the app was in certain states (killed, background, etc.). By moving E2E decryption to the native layer using Android Context (which is always available), we eliminate these dependencies and make the notification handling more reliable. MMKV is initialized at app startup, so all notification types can work without React Native being ready.

Performance Impact:

  • E2E notifications are now processed faster since they don't wait for React Native initialization
  • Reduced memory overhead by removing the separate E2ENotificationProcessor class
  • More reliable notification handling reduces crashes and app terminations

Summary by CodeRabbit

  • Bug Fixes

    • Notifications now process immediately without waiting for framework readiness, improving delivery reliability.
    • Improved handling for encrypted notifications with a clearer fallback display when decryption fails.
  • New Features

    • Android: added native support to store and retrieve pending notification taps for delivery to the app.
  • Improvements

    • Unified notification tap handling and enhanced logging; app checks pending notifications when returning to foreground.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Jan 6, 2026

Warning

Rate limit exceeded

@diegolmello has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 14 minutes and 20 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

📥 Commits

Reviewing files that changed from the base of the PR and between 7455557 and 820f18d.

📒 Files selected for processing (13)
  • android/app/src/main/java/chat/rocket/reactnative/MainActivity.kt
  • android/app/src/main/java/chat/rocket/reactnative/MainApplication.kt
  • android/app/src/main/java/chat/rocket/reactnative/notification/CustomPushNotification.java
  • android/app/src/main/java/chat/rocket/reactnative/notification/E2ENotificationProcessor.java
  • android/app/src/main/java/chat/rocket/reactnative/notification/Ejson.java
  • android/app/src/main/java/chat/rocket/reactnative/notification/NativePushNotificationSpec.kt
  • android/app/src/main/java/chat/rocket/reactnative/notification/NotificationIntentHandler.kt
  • android/app/src/main/java/chat/rocket/reactnative/notification/PushNotificationModule.kt
  • android/app/src/main/java/chat/rocket/reactnative/notification/PushNotificationTurboPackage.kt
  • app/lib/native/NativePushNotificationAndroid.ts
  • app/lib/notifications/index.ts
  • app/lib/notifications/push.ts
  • app/sagas/state.js

Walkthrough

This PR removes React-context–dependent deferred E2E push processing and the E2ENotificationProcessor; adds a TurboModule push bridge, persistent pending-notification storage, and Android-side immediate decryption/notification handling, plus JS hooks to read and clear stored pending notifications.

Changes

Cohort / File(s) Summary
Application startup
android/app/src/main/java/chat/rocket/reactnative/MainApplication.kt
Removed ReactInstanceEventListener wiring for CustomPushNotification; registered PushNotificationTurboPackage.
Activity → intent handling
android/app/src/main/java/chat/rocket/reactnative/MainActivity.kt
android/app/src/main/java/chat/rocket/reactnative/notification/NotificationIntentHandler.kt
Replaced video-conf-specific intent logic with NotificationIntentHandler.handleIntent(...) that routes video-conf or generic notification payloads and persists pending action/notification.
Immediate notification logic
android/app/src/main/java/chat/rocket/reactnative/notification/CustomPushNotification.java
Removed ReactApplicationContext dependency, deferred/wait logic and E2E async processor usage; now decrypts/processes notifications immediately using Android context and synchronized bundle visibility.
Removed async processor
android/app/src/main/java/chat/rocket/reactnative/notification/E2ENotificationProcessor.java
Deleted class and its interfaces, callbacks, polling/timeout, and background decryption orchestration.
TurboModule spec & package
android/app/src/main/java/chat/rocket/reactnative/notification/NativePushNotificationSpec.kt
android/app/src/main/java/chat/rocket/reactnative/notification/PushNotificationTurboPackage.kt
Added TurboModule spec and TurboReactPackage exposing PushNotificationModule as a turbo module.
Native module implementation
android/app/src/main/java/chat/rocket/reactnative/notification/PushNotificationModule.kt
New Kotlin native module exposing getPendingNotification and clearPendingNotification; includes storePendingNotification to persist JSON in SharedPreferences.
Ejson cleanup
android/app/src/main/java/chat/rocket/reactnative/notification/Ejson.java
Removed unused RNCallback and React Callback import.
JS/TS bridge
app/lib/native/NativePushNotificationAndroid.ts
Added TurboModule TypeScript interface and default export for PushNotificationModule.
Notification flow (JS)
app/lib/notifications/index.ts
app/lib/notifications/push.ts
Added checkPendingNotification() and Android-native pending-notification retrieval in push flow; improved logging and error handling; prefer native pending notification over expo fallback.
Saga integration
app/sagas/state.js
On app foreground, asynchronously call checkPendingNotification() (non-blocking) to handle stored pending notifications.

Sequence Diagram(s)

sequenceDiagram
    participant System as System / Notification Intent
    participant Activity as MainActivity
    participant Handler as NotificationIntentHandler
    participant Native as PushNotificationModule (Kotlin)
    participant Store as SharedPreferences
    participant JS as React Native JS

    rect rgb(230,245,255)
    Note over System,Store: User taps notification → store pending payload
    System->>Activity: deliver Intent (onCreate/onNewIntent)
    Activity->>Handler: handleIntent(context, intent)
    Handler->>Native: PushNotificationModule.storePendingNotification(json)
    Native->>Store: save "pendingNotification"
    end

    rect rgb(230,255,230)
    Note over JS,Store: App foreground/startup reads pending payload
    JS->>Native: getPendingNotification()
    Native->>Store: read & clear "pendingNotification"
    Store-->>Native: pending JSON or null
    Native-->>JS: resolve(pending JSON or null)
    JS->>JS: parse JSON -> onNotification(dispatch)
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • OtavioStasiak

"I hopped through code with nimble paws,
I stored a ping and trimmed old claws,
No waiting now — taps land straight,
SharedPrefs keeps what I create,
Hop! 🐰"

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 39.13% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly describes the main fix: resolving push notification handling for killed Android apps and deep linking. It directly corresponds to the PR's core objective of removing React Native dependencies from E2E notification decryption and implementing TurboModule-based notification data handling.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix.android-e2ee-killed-app

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@diegolmello diegolmello had a problem deploying to official_android_build January 6, 2026 21:03 — with GitHub Actions Error
@diegolmello diegolmello temporarily deployed to experimental_android_build January 6, 2026 21:03 — with GitHub Actions Inactive
@diegolmello diegolmello had a problem deploying to experimental_ios_build January 6, 2026 21:03 — with GitHub Actions Error
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI Agents
In
@android/app/src/main/java/chat/rocket/reactnative/notification/CustomPushNotification.java:
- Around line 168-181: handleE2ENotification updates shared state unsafely and
reparses Ejson unnecessarily; modify it to synchronize updates to mBundle the
same way loadNotificationAndProcess does (use the same lock or synchronized
block), stop re-parsing Ejson (use the existing ejson parameter instead of
calling safeFromJson again), and on decryption failure call showNotification
with the original bundle or a fallback notification (or at minimum record a
visible failure) so users see something when Encryption.shared.decryptMessage
returns null; update references: handleE2ENotification, mBundle,
loadNotificationAndProcess, Ejson, showNotification,
Encryption.shared.decryptMessage, and the ConcurrentHashMap usage accordingly.
🧹 Nitpick comments (1)
android/app/src/main/java/chat/rocket/reactnative/notification/CustomPushNotification.java (1)

92-92: Consider adding more context to the error message.

The generic "Failed to process notification" message provides less debugging context than a more specific message would. Consider including details about the notification type or processing stage that failed.

💡 Example with more context
-            Log.e(TAG, "Failed to process notification", e);
+            Log.e(TAG, "Failed to process notification (notId: " + notId + ")", e);
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Jira integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between f7aef7f and 737d488.

📒 Files selected for processing (4)
  • android/app/src/main/java/chat/rocket/reactnative/MainApplication.kt
  • android/app/src/main/java/chat/rocket/reactnative/notification/CustomPushNotification.java
  • android/app/src/main/java/chat/rocket/reactnative/notification/E2ENotificationProcessor.java
  • android/app/src/main/java/chat/rocket/reactnative/notification/Ejson.java
💤 Files with no reviewable changes (3)
  • android/app/src/main/java/chat/rocket/reactnative/notification/Ejson.java
  • android/app/src/main/java/chat/rocket/reactnative/notification/E2ENotificationProcessor.java
  • android/app/src/main/java/chat/rocket/reactnative/MainApplication.kt
🧰 Additional context used
🧬 Code graph analysis (1)
android/app/src/main/java/chat/rocket/reactnative/notification/CustomPushNotification.java (1)
android/app/src/main/java/chat/rocket/reactnative/notification/Encryption.java (1)
  • Encryption (105-511)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: ESLint and Test / run-eslint-and-test
🔇 Additional comments (1)
android/app/src/main/java/chat/rocket/reactnative/notification/CustomPushNotification.java (1)

87-89: LGTM! Clear documentation of the architectural change.

The updated comments accurately reflect the shift from async React-context-dependent processing to immediate processing using MMKV and Android Context.

@diegolmello diegolmello had a problem deploying to upload_experimental_android January 6, 2026 21:32 — with GitHub Actions Error
@github-actions
Copy link
Copy Markdown

github-actions bot commented Jan 6, 2026

@diegolmello diegolmello had a problem deploying to experimental_ios_build January 7, 2026 16:34 — with GitHub Actions Error
@diegolmello diegolmello temporarily deployed to experimental_android_build January 7, 2026 16:34 — with GitHub Actions Inactive
@diegolmello diegolmello had a problem deploying to official_android_build January 7, 2026 16:34 — with GitHub Actions Error
@diegolmello diegolmello had a problem deploying to upload_experimental_android January 7, 2026 17:13 — with GitHub Actions Error
@github-actions
Copy link
Copy Markdown

github-actions bot commented Jan 7, 2026

@diegolmello diegolmello changed the title fix(Android): Push notification on killed apps fix(Android): Push notification on killed apps and deep link Jan 7, 2026
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In @app/lib/notifications/push.ts:
- Around line 246-253: The console.log in push.ts prints a snippet of
notification.payload.message which may contain user PII; remove the message
substring from the log or replace it with a non-PII indicator (e.g., a boolean
like hasMessage or just messageLength) so you never log user content. Locate the
log that references notification.payload.message (the transformed notification
dump) and change the message field to either undefined/omitted, hasMessage:
!!notification.payload?.message, or messageLength:
notification.payload?.message?.length to preserve debug signal without exposing
PII.
🧹 Nitpick comments (6)
android/app/src/main/java/chat/rocket/reactnative/notification/CustomPushNotification.java (1)

321-332: Inconsistent debug log gating.

Lines 321-332 add extensive debug logging in buildNotification(), but these logs are not gated by ENABLE_VERBOSE_LOGS like similar debug statements elsewhere in the method (lines 308-314). This creates inconsistent logging behavior and potential log spam in production builds.

🔧 Proposed fix to gate debug logs consistently
-        // Log intent extras for debugging
-        Log.d(TAG, "[buildNotification] Creating intent with extras:");
-        Log.d(TAG, "[buildNotification]   - ejson: " + (mBundle.getString("ejson") != null ? "[present, length=" + mBundle.getString("ejson").length() + "]" : "[null]"));
-        Log.d(TAG, "[buildNotification]   - rid: " + (ejson != null && ejson.rid != null ? ejson.rid : "[null]"));
-        Log.d(TAG, "[buildNotification]   - type: " + (ejson != null && ejson.type != null ? ejson.type : "[null]"));
-        Log.d(TAG, "[buildNotification]   - host: " + (ejson != null && ejson.host != null ? "[present]" : "[null]"));
-        Log.d(TAG, "[buildNotification]   - messageId: " + (ejson != null && ejson.messageId != null ? ejson.messageId : "[null]"));
-        Log.d(TAG, "[buildNotification]   - notificationType: " + (ejson != null && ejson.notificationType != null ? ejson.notificationType : "[null]"));
-        Log.d(TAG, "[buildNotification]   - notId: " + mBundle.getString("notId"));
-        Log.d(TAG, "[buildNotification]   - title: " + (title != null ? "[present]" : "[null]"));
-        Log.d(TAG, "[buildNotification]   - message: " + (message != null ? "[present, length=" + message.length() + "]" : "[null]"));
+        if (ENABLE_VERBOSE_LOGS) {
+            // Log intent extras for debugging
+            Log.d(TAG, "[buildNotification] Creating intent with extras:");
+            Log.d(TAG, "[buildNotification]   - ejson: " + (mBundle.getString("ejson") != null ? "[present, length=" + mBundle.getString("ejson").length() + "]" : "[null]"));
+            Log.d(TAG, "[buildNotification]   - rid: " + (ejson != null && ejson.rid != null ? ejson.rid : "[null]"));
+            Log.d(TAG, "[buildNotification]   - type: " + (ejson != null && ejson.type != null ? ejson.type : "[null]"));
+            Log.d(TAG, "[buildNotification]   - host: " + (ejson != null && ejson.host != null ? "[present]" : "[null]"));
+            Log.d(TAG, "[buildNotification]   - messageId: " + (ejson != null && ejson.messageId != null ? ejson.messageId : "[null]"));
+            Log.d(TAG, "[buildNotification]   - notificationType: " + (ejson != null && ejson.notificationType != null ? ejson.notificationType : "[null]"));
+            Log.d(TAG, "[buildNotification]   - notId: " + mBundle.getString("notId"));
+            Log.d(TAG, "[buildNotification]   - title: " + (title != null ? "[present]" : "[null]"));
+            Log.d(TAG, "[buildNotification]   - message: " + (message != null ? "[present, length=" + message.length() + "]" : "[null]"));
+        }
app/sagas/state.js (1)

32-35: Consider yielding the pending notification check for better error handling.

The current implementation fires checkPendingNotification() asynchronously without yielding, which means:

  • The saga continues immediately without waiting for completion
  • Errors are only logged, not handled by the saga's error boundaries
  • The operation races with setUserPresenceOnline()

While the fire-and-forget approach prevents blocking, it reduces visibility into failures and makes testing harder.

💡 Alternative: Yield the call for saga-managed execution
-		// Check for pending notification when app comes to foreground (Android - notification tap while in background)
-		checkPendingNotification().catch((e) => {
-			log('[state.js] Error checking pending notification:', e);
-		});
+		// Check for pending notification when app comes to foreground (Android - notification tap while in background)
+		try {
+			yield checkPendingNotification();
+		} catch (e) {
+			log('[state.js] Error checking pending notification:', e);
+		}

This approach:

  • Maintains saga control flow and error boundaries
  • Still doesn't block setUserPresenceOnline() (saga effects are sequential but the check should be fast)
  • Improves testability

If the fire-and-forget behavior is intentional (to avoid any delay), consider documenting why.

app/lib/notifications/push.ts (2)

280-295: Extract shared notification transformation logic to reduce duplication.

This payload transformation to INotification format is duplicated in app/lib/notifications/index.ts (lines 109-124). Consider extracting a shared helper function to avoid maintaining the same mapping logic in two places.

♻️ Suggested helper function

Create a shared helper in a common location:

// e.g., in push.ts or a separate helper file
export const transformNativePayloadToNotification = (data: Record<string, any>): INotification => ({
  payload: {
    message: data.message || '',
    style: data.style || '',
    ejson: data.ejson || '',
    collapse_key: data.collapse_key || '',
    notId: data.notId || '',
    msgcnt: data.msgcnt || '',
    title: data.title || '',
    from: data.from || '',
    image: data.image || '',
    soundname: data.soundname || '',
    action: data.action
  },
  identifier: data.notId || ''
});

Then use it in both files:

- const transformed: INotification = {
-   payload: {
-     message: notificationData.message || '',
-     // ... all the fields
-   },
-   identifier: notificationData.notId || ''
- };
+ const transformed = transformNativePayloadToNotification(notificationData);

309-325: Consider simplifying the promise chain for clarity.

The double .then().catch() chain works correctly, but could be clearer. The second .catch() at line 322 only handles errors from the fallback logic (lines 314-320), while the first .catch() (line 305) handles native module errors. This structure is correct but slightly confusing to read.

♻️ Optional: Use async/await for cleaner flow
// Alternative using async/await (would need to wrap in async IIFE)
try {
  const pendingNotification = await NativePushNotificationModule.getPendingNotification();
  if (pendingNotification) {
    const notificationData = JSON.parse(pendingNotification);
    return transformNativePayloadToNotification(notificationData);
  }
} catch (e) {
  console.error('[push.ts] Error getting pending notification:', e);
}

// Fallback to expo-notifications
const lastResponse = Notifications.getLastNotificationResponse();
return lastResponse ? transformNotificationResponse(lastResponse) : null;
app/lib/notifications/index.ts (1)

103-104: Consider using static import for consistency with push.ts.

The dynamic require() here contrasts with the static import in push.ts (line 10). Since the function is already guarded by Platform.OS === 'android', a static import at the top of the file with the same guard pattern would be more consistent and bundler-friendly.

♻️ Use static import pattern
+ import NativePushNotificationModule from '../native/NativePushNotificationAndroid';

  export const checkPendingNotification = async (): Promise<void> => {
    if (Platform.OS === 'android') {
      try {
-       const NativePushNotificationModule = require('../native/NativePushNotificationAndroid').default;
        if (NativePushNotificationModule) {
android/app/src/main/java/chat/rocket/reactnative/notification/PushNotificationModule.kt (1)

55-65: Consider adding debug logging for error visibility.

The empty catch block silently swallows exceptions. While acceptable for cleanup operations, adding a log statement would aid debugging if issues arise.

♻️ Add error logging
  @ReactMethod
  override fun clearPendingNotification() {
      try {
          reactApplicationContext.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
              .edit()
              .remove(KEY_PENDING_NOTIFICATION)
              .apply()
      } catch (e: Exception) {
-         // Ignore errors
+         android.util.Log.w("PushNotificationModule", "Failed to clear pending notification", e)
      }
  }
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Jira integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 344334e and 3fd087e.

📒 Files selected for processing (10)
  • android/app/src/main/java/chat/rocket/reactnative/MainActivity.kt
  • android/app/src/main/java/chat/rocket/reactnative/MainApplication.kt
  • android/app/src/main/java/chat/rocket/reactnative/notification/CustomPushNotification.java
  • android/app/src/main/java/chat/rocket/reactnative/notification/NativePushNotificationSpec.kt
  • android/app/src/main/java/chat/rocket/reactnative/notification/PushNotificationModule.kt
  • android/app/src/main/java/chat/rocket/reactnative/notification/PushNotificationTurboPackage.kt
  • app/lib/native/NativePushNotificationAndroid.ts
  • app/lib/notifications/index.ts
  • app/lib/notifications/push.ts
  • app/sagas/state.js
🚧 Files skipped from review as they are similar to previous changes (1)
  • android/app/src/main/java/chat/rocket/reactnative/MainApplication.kt
🧰 Additional context used
🧬 Code graph analysis (3)
app/sagas/state.js (1)
app/lib/notifications/index.ts (1)
  • checkPendingNotification (100-135)
app/lib/notifications/push.ts (2)
app/lib/notifications/index.ts (1)
  • onNotification (19-81)
app/definitions/INotification.ts (1)
  • INotification (1-16)
app/lib/notifications/index.ts (1)
app/definitions/INotification.ts (1)
  • INotification (1-16)
🔇 Additional comments (13)
android/app/src/main/java/chat/rocket/reactnative/notification/CustomPushNotification.java (3)

87-93: LGTM! Immediate notification processing without React Native dependency.

The updated comments and immediate processing approach correctly reflects the architectural change to process notifications without waiting for React Native initialization. MMKV initialization at app startup enables this direct path.


168-189: Good: Thread safety and fallback notification added.

The previous review concerns have been addressed:

  1. ✅ Synchronization added when updating mBundle (lines 175-177, 184-186)
  2. ✅ Fallback notification now shown when decryption fails (lines 180-188)
  3. ✅ No redundant ejson re-parsing

The fallback with "Encrypted message" placeholder ensures users know a message arrived even when decryption fails.


212-217: LGTM! Consistent synchronization pattern.

The synchronized block before buildNotification() ensures all bundle modifications (time, username, senderId, avatarUri, and any prior changes) are visible to the notification builder, maintaining consistency with the synchronization patterns used elsewhere in the file.

app/sagas/state.js (1)

10-10: LGTM! Import added for pending notification handling.

The import is correctly sourced from the notifications module.

android/app/src/main/java/chat/rocket/reactnative/MainActivity.kt (3)

14-14: LGTM! Import added for notification handling.

The import is correctly added to support the new notification intent handling flow.


38-41: LGTM! Notification intent handling added on activity creation.

The call to handleNotificationIntent correctly processes notification data when the app is launched from a killed state via a notification tap.


43-52: LGTM! Intent handling for running activity.

The addition of setIntent(intent) at line 45 is essential to update the activity's intent reference, and the subsequent calls to both video conf and regular notification handlers ensure all notification types are processed when the activity receives a new intent while already running.

android/app/src/main/java/chat/rocket/reactnative/notification/PushNotificationTurboPackage.kt (1)

1-38: LGTM! Standard TurboModule package implementation.

The PushNotificationTurboPackage correctly implements the TurboReactPackage pattern:

  • Module instantiation is properly gated by name matching (line 15)
  • Module metadata is correctly configured with isTurboModule=true and appropriate defaults for other flags
  • The package properly bridges the native module to React Native's TurboModule infrastructure
app/lib/native/NativePushNotificationAndroid.ts (1)

1-9: LGTM! Clean TurboModule TypeScript bridge.

The TypeScript bridge correctly defines the native module interface:

  • Extends TurboModule for proper type integration
  • Method signatures match the native Kotlin implementation (getPendingNotification returns Promise<string | null>, clearPendingNotification returns void)
  • Module name 'PushNotificationModule' is consistent with NativePushNotificationSpec.NAME in the Kotlin code
  • Properly uses TurboModuleRegistry.get<Spec> for type-safe module access
android/app/src/main/java/chat/rocket/reactnative/notification/NativePushNotificationSpec.kt (1)

9-23: LGTM! Clean TurboModule spec definition.

The abstract class correctly defines the contract for the push notification module with proper use of companion object for the shared NAME constant and appropriate @ReactMethod annotations on abstract methods.

android/app/src/main/java/chat/rocket/reactnative/notification/PushNotificationModule.kt (2)

14-28: LGTM! Clean SharedPreferences-based storage implementation.

The companion object pattern with @JvmStatic is correct for calling from Java/Kotlin code (MainActivity). Using apply() for async writes is appropriate for non-critical persistence. The PREFS_NAME and key naming are clear.


36-50: Race condition concern is not applicable to the actual call patterns in this codebase.

getPendingNotification() is called exactly once during app initialization via pushNotificationConfigure() (line 272 in push.ts), not in rapid succession. The scenario described in the review—multiple rapid calls causing a race condition before the async apply() completes—does not occur. Additionally, apply() is the correct Android practice for background preference writes in this context.

The PREFS_NAME sharing between PushNotificationModule and VideoConfModule is intentional and safe; both modules use distinct keys (KEY_PENDING_NOTIFICATION vs KEY_VIDEO_CONF_ACTION) within the same preferences file.

app/lib/notifications/index.ts (1)

100-135: The native module's getPendingNotification() already clears the stored notification after reading it (see PushNotificationModule.kt lines 43-46), so no explicit clearPendingNotification() call is needed. The notification is automatically removed from SharedPreferences during the getPendingNotification() read operation itself, preventing duplicate processing regardless of which code path reads it first.

Likely an incorrect or invalid review comment.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In
@android/app/src/main/java/chat/rocket/reactnative/notification/NotificationIntentHandler.kt:
- Around line 43-44: The handler currently returns true silently when event is
null; update the intent handling in NotificationIntentHandler (the block where
notificationId and event are read, and where videoConfAction is checked) to log
a warning/error when event == null (include notificationId and any relevant
intent extras) before returning, so malformed video conference notifications are
recorded for debugging; ensure you reference the same variables
(`notificationId`, `event`, `videoConfAction`) and keep the original control
flow (returning true) after logging.
🧹 Nitpick comments (3)
app/lib/notifications/push.ts (1)

218-274: Consider simplifying the promise chain structure.

The promise chain has a nested .then().catch().then().catch() pattern that's harder to follow. The second .catch() on line 270 is likely unreachable since getLastNotificationResponse() is synchronous and line 257's .then() doesn't throw.

The transformation logic correctly maps all INotification fields per the interface definition.

♻️ Suggested simplification using async/await
-	// First check native module for stored notification data (Android - when notification was created natively)
-	if (Platform.OS === 'android' && NativePushNotificationModule) {
-		return NativePushNotificationModule.getPendingNotification()
-			.then(pendingNotification => {
-				if (pendingNotification) {
-					try {
-						// Parse the stored notification data
-						const notificationData = JSON.parse(pendingNotification);
-
-						// Transform to INotification format
-						const transformed: INotification = {
-							payload: {
-								message: notificationData.message || '',
-								style: notificationData.style || '',
-								ejson: notificationData.ejson || '',
-								collapse_key: notificationData.collapse_key || '',
-								notId: notificationData.notId || '',
-								msgcnt: notificationData.msgcnt || '',
-								title: notificationData.title || '',
-								from: notificationData.from || '',
-								image: notificationData.image || '',
-								soundname: notificationData.soundname || '',
-								action: notificationData.action
-							},
-							identifier: notificationData.notId || ''
-						};
-
-						return transformed;
-					} catch (parseError) {
-						console.error('[push.ts] Error parsing notification data:', parseError);
-						return null;
-					}
-				}
-				return null;
-			})
-			.catch(e => {
-				console.error('[push.ts] Error getting pending notification from native module:', e);
-				return null;
-			})
-			.then(nativeNotification => {
-				if (nativeNotification) {
-					return nativeNotification;
-				}
-
-				// Fallback to expo-notifications (for iOS or if native module doesn't have data)
-				const lastResponse = Notifications.getLastNotificationResponse();
-				if (lastResponse) {
-					return transformNotificationResponse(lastResponse);
-				}
-
-				return null;
-			})
-			.catch(e => {
-				console.error('[push.ts] Error in promise chain:', e);
-				return null;
-			});
-	}
+	// First check native module for stored notification data (Android - when notification was created natively)
+	if (Platform.OS === 'android' && NativePushNotificationModule) {
+		return (async () => {
+			try {
+				const pendingNotification = await NativePushNotificationModule.getPendingNotification();
+				if (pendingNotification) {
+					try {
+						const notificationData = JSON.parse(pendingNotification);
+						return {
+							payload: {
+								message: notificationData.message || '',
+								style: notificationData.style || '',
+								ejson: notificationData.ejson || '',
+								collapse_key: notificationData.collapse_key || '',
+								notId: notificationData.notId || '',
+								msgcnt: notificationData.msgcnt || '',
+								title: notificationData.title || '',
+								from: notificationData.from || '',
+								image: notificationData.image || '',
+								soundname: notificationData.soundname || '',
+								action: notificationData.action
+							},
+							identifier: notificationData.notId || ''
+						} as INotification;
+					} catch (parseError) {
+						console.error('[push.ts] Error parsing notification data:', parseError);
+					}
+				}
+			} catch (e) {
+				console.error('[push.ts] Error getting pending notification from native module:', e);
+			}
+			// Fallback to expo-notifications
+			const lastResponse = Notifications.getLastNotificationResponse();
+			return lastResponse ? transformNotificationResponse(lastResponse) : null;
+		})();
+	}
android/app/src/main/java/chat/rocket/reactnative/notification/PushNotificationModule.kt (1)

46-61: Potential race condition: clearing before resolving the promise.

The notification is cleared (line 54) before the promise is resolved (line 57). If the JS side encounters an error after receiving the notification but before processing it, the data is lost. Consider clearing only after successful JS-side processing, or document this as intentional "at-most-once" delivery.

Additionally, using apply() for the remove (async) while using commit() for store (sync) creates an inconsistency. If the clear fails silently, duplicate processing could occur on next app launch.

♻️ Consider returning the value and letting JS trigger clear explicitly

If at-most-once semantics are acceptable, the current approach is fine. Otherwise, keep clearPendingNotification() as the explicit cleanup call from JS after successful processing:

     @ReactMethod
     override fun getPendingNotification(promise: Promise) {
         try {
             val prefs = reactApplicationContext.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
             val notification = prefs.getString(KEY_PENDING_NOTIFICATION, null)
 
-            // Clear the notification after reading
-            notification?.let {
-                prefs.edit().remove(KEY_PENDING_NOTIFICATION).apply()
-            }
+            // Note: Caller should invoke clearPendingNotification() after successful processing
 
             promise.resolve(notification)
         } catch (e: Exception) {
             promise.reject("ERROR", e.message)
         }
     }
android/app/src/main/java/chat/rocket/reactnative/notification/NotificationIntentHandler.kt (1)

70-71: Consider reusing Gson instance.

Two GsonBuilder().create() calls create separate Gson instances. While this is fine for infrequent notification handling, a single shared instance in the companion object would be slightly more efficient.

♻️ Optional: Share Gson instance
 companion object {
     private const val TAG = "RocketChat.NotificationIntentHandler"
+    private val gson = GsonBuilder().create()
 
     // ... in handleVideoConfIntent:
-    val gson = GsonBuilder().create()
-    val jsonData = gson.toJson(data)
+    val jsonData = gson.toJson(data)
 
     // ... in handleNotificationIntent:
-    val gson = GsonBuilder().create()
-    val jsonData = gson.toJson(notificationData)
+    val jsonData = gson.toJson(notificationData)

Also applies to: 139-140

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Jira integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 3fd087e and 7455557.

📒 Files selected for processing (5)
  • android/app/src/main/java/chat/rocket/reactnative/MainActivity.kt
  • android/app/src/main/java/chat/rocket/reactnative/notification/CustomPushNotification.java
  • android/app/src/main/java/chat/rocket/reactnative/notification/NotificationIntentHandler.kt
  • android/app/src/main/java/chat/rocket/reactnative/notification/PushNotificationModule.kt
  • app/lib/notifications/push.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • android/app/src/main/java/chat/rocket/reactnative/MainActivity.kt
🧰 Additional context used
🧬 Code graph analysis (1)
app/lib/notifications/push.ts (1)
app/definitions/INotification.ts (1)
  • INotification (1-16)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: ESLint and Test / run-eslint-and-test
  • GitHub Check: format
🔇 Additional comments (6)
android/app/src/main/java/chat/rocket/reactnative/notification/PushNotificationModule.kt (1)

23-39: Good use of synchronous commit() with error handling.

Using commit() instead of apply() here ensures storage errors are caught and propagated. The explicit logging and re-throwing pattern is appropriate for this critical path.

android/app/src/main/java/chat/rocket/reactnative/notification/NotificationIntentHandler.kt (2)

121-131: The toString() heuristic is fragile but acceptable.

The check !stringValue.contains("@") could theoretically filter out legitimate data if a non-String type's toString() happened to contain "@". However, this is unlikely in practice since standard types (String, Int, etc.) are handled explicitly above. The warning log provides visibility into what's being skipped.


95-136: Good defensive handling of Intent extras.

The per-key try-catch (lines 102-135) ensures a problematic extra doesn't break the entire notification processing. The explicit type whitelist for primitives is safer than attempting to serialize unknown types.

app/lib/notifications/push.ts (1)

10-10: Import path and module exports are valid.

The NativePushNotificationAndroid.ts module exists at the expected path and correctly exports the TurboModule interface with the required methods: getPendingNotification() and clearPendingNotification().

android/app/src/main/java/chat/rocket/reactnative/notification/CustomPushNotification.java (2)

87-94: LGTM!

The comments clearly document the architectural change, and the simplified error message appropriately reflects that React Native initialization is no longer a factor in notification processing.


212-216: LGTM!

Good addition to ensure visibility of all bundle modifications before buildNotification() reads mBundle. This synchronized block is consistent with the other synchronization points in the class.

@diegolmello diegolmello had a problem deploying to upload_experimental_android January 7, 2026 19:59 — with GitHub Actions Error
@github-actions
Copy link
Copy Markdown

github-actions bot commented Jan 7, 2026

Copy link
Copy Markdown
Contributor

@OtavioStasiak OtavioStasiak left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good to me!

…nitialization checks. E2E notifications now decrypt immediately using Android Context, improving responsiveness.
diegolmello and others added 8 commits January 8, 2026 17:02
…2ENotificationProcessor. Simplifies notification management by eliminating unnecessary React context dependencies.
- Added PushNotificationModule to manage notification data from taps.
- Integrated notification handling in MainActivity for both new intents and existing activity.
- Created NativePushNotificationSpec and PushNotificationTurboPackage for TurboModule support.
- Updated notification processing in the app to check for pending notifications when coming to the foreground.
- Enhanced logging for better debugging of notification data.
…management in PushNotificationModule

- Updated MainActivity to filter non-serializable types from notification extras, improving JSON serialization reliability.
- Added error handling for processing notification extras and storing pending notifications.
- Modified PushNotificationModule to use synchronous commit for storing notifications, ensuring error detection during storage.
…tionIntentHandler

- Consolidated notification intent processing into a new NotificationIntentHandler class for better organization and maintainability.
- Updated MainActivity to utilize the new handler for both initial and new intents, streamlining the notification handling logic.
- Removed redundant code related to video conference and regular notification handling from MainActivity.
@diegolmello diegolmello force-pushed the fix.android-e2ee-killed-app branch from 7455557 to 820f18d Compare January 8, 2026 20:02
@diegolmello diegolmello merged commit 1fe0bf7 into develop Jan 8, 2026
5 of 6 checks passed
@diegolmello diegolmello deleted the fix.android-e2ee-killed-app branch January 8, 2026 20:03
@diegolmello diegolmello had a problem deploying to experimental_ios_build January 8, 2026 20:05 — with GitHub Actions Failure
@diegolmello diegolmello had a problem deploying to experimental_android_build January 8, 2026 20:05 — with GitHub Actions Failure
@diegolmello diegolmello had a problem deploying to official_android_build January 8, 2026 20:05 — with GitHub Actions Failure
@coderabbitai coderabbitai bot mentioned this pull request Jan 20, 2026
10 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants