Repository: firebase/firebase-ios-sdk (language: cpp)

Issue:
open_notification event not being triggered when app is not in background.
<!-- DO NOT DELETE
validate_template=true
template_path=.github/ISSUE_TEMPLATE/bug_report.md
-->


### [REQUIRED] Step 1: Describe your environment

  * Xcode version: 12.5
  * Firebase SDK version: [8.0.0](url)
  * Installation method: SPM
  * Firebase Component: Messaging

### [REQUIRED] Step 2: Describe the problem

#### Steps to reproduce:

1. Force quit the app
2. Send a push notification to the app
3. Tap on the push notification
4. Observe Debug View

Expected: notification_open event gets triggered
Actual: No notification_event gets triggered


We are noticing that iOS is severely undercounting the push notification open rate. On Android, our open rate is around 12%. On iOS, our open rate is around 0.29%. However, evidence suggests that users ARE receiving push notifications and opening them on iOS (we see similar surges in traffic as Android), but just that the notification_open event is not getting triggered.

We have confirmed that swizzling is ON.

We have tested the following scenarios, each 5 times, and each reproduced 100%

- App is in the background, notification is received, notification_open is triggered
- App is closed, notification is received, notification_open is NOT triggered
- App is open in foreground, notification is received, notification_foreground is triggered correctly

We are monitoring the events via the DebugView.

Note that we recently updated to v8.0.0. However, we saw the same issue in the previous version of the SDK which was manually integrated.


Discussion / hints:
@khuffie Thank you for filing the issue. Please remind me again if you are using Unity/C++ or just iOS SDK?

If it's iOS SDK, do you have `- (void)userNotificationCenter:didReceiveNotificationResponse:withCompletionHandler:` implemented as mentioned in our [guide](https://firebase.google.com/docs/cloud-messaging/ios/receive#handle_alert_notifications).
@chliangGoogle we are using just the iOS SDK. We have a native Swift app.

And yep, we do implement that method as described.

Additional context: we send additional metadata with the push to deeplink users to specific sections of the app. Even in the cold start scenario, the deeplinking logic works as expected. It's just that the event isn't triggered so our open rate is completely off.

Fix patch:
diff --git a/FirebaseMessaging/Apps/AdvancedSample/Podfile b/FirebaseMessaging/Apps/AdvancedSample/Podfile
index 19bcaf5d765..c7b90e55c5d 100644
--- a/FirebaseMessaging/Apps/AdvancedSample/Podfile
+++ b/FirebaseMessaging/Apps/AdvancedSample/Podfile
@@ -13,6 +13,7 @@ end
 
 target 'AdvancedSample' do
   platform :ios, '13.0'
+  pod 'FirebaseAnalytics'
   shared_pods
 end
 
@@ -23,6 +24,7 @@ end
 
 target 'AppClips' do
   platform :ios, '13.0'
+  pod 'FirebaseAnalytics'
   shared_pods
 end
 
diff --git a/FirebaseMessaging/Apps/Sample/Podfile b/FirebaseMessaging/Apps/Sample/Podfile
index 3749aec605b..6b9f0a286f4 100644
--- a/FirebaseMessaging/Apps/Sample/Podfile
+++ b/FirebaseMessaging/Apps/Sample/Podfile
@@ -11,5 +11,6 @@ target 'Sample' do
   pod 'FirebaseMessaging', :path => '../../../'
   pod 'FirebaseCoreDiagnostics', :path => '../../../'
   pod 'FirebaseInstallations', :path => '../../../'
+  pod 'FirebaseAnalytics'
 
 end
diff --git a/FirebaseMessaging/Apps/Sample/Sample.xcodeproj/xcshareddata/xcschemes/Sample.xcscheme b/FirebaseMessaging/Apps/Sample/Sample.xcodeproj/xcshareddata/xcschemes/Sample.xcscheme
index 9e291084f87..a82d5a9c4d5 100644
--- a/FirebaseMessaging/Apps/Sample/Sample.xcodeproj/xcshareddata/xcschemes/Sample.xcscheme
+++ b/FirebaseMessaging/Apps/Sample/Sample.xcodeproj/xcshareddata/xcschemes/Sample.xcscheme
@@ -55,6 +55,10 @@
             argument = "-FIRDebugEnabled"
             isEnabled = "YES">
          </CommandLineArgument>
+         <CommandLineArgument
+            argument = "-FIRAnalyticsDebugEnabled "
+            isEnabled = "YES">
+         </CommandLineArgument>
       </CommandLineArguments>
    </LaunchAction>
    <ProfileAction
diff --git a/FirebaseMessaging/Apps/Shared/AppDelegate.swift b/FirebaseMessaging/Apps/Shared/AppDelegate.swift
index 441e6858087..d639dff7207 100644
--- a/FirebaseMessaging/Apps/Shared/AppDelegate.swift
+++ b/FirebaseMessaging/Apps/Shared/AppDelegate.swift
@@ -14,6 +14,7 @@
 
 import UIKit
 import FirebaseCore
+import FirebaseAnalytics
 
 @UIApplicationMain
 class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterDelegate {
@@ -21,6 +22,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
                    didFinishLaunchingWithOptions launchOptions: [UIApplication
                      .LaunchOptionsKey: Any]?) -> Bool {
     FirebaseApp.configure()
+    FirebaseAnalytics.Analytics.logEvent("test", parameters: nil)
 
     let center = UNUserNotificationCenter.current()
     center.delegate = self
@@ -42,14 +44,9 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
     completionHandler([.alert, .sound])
   }
 
-  // MARK: UISceneSession Lifecycle
-
-  func application(_ application: UIApplication,
-                   configurationForConnecting connectingSceneSession: UISceneSession,
-                   options: UIScene.ConnectionOptions) -> UISceneConfiguration {
-    // Called when a new scene session is being created.
-    // Use this method to select a configuration to create the new scene with.
-    return UISceneConfiguration(name: "Default Configuration",
-                                sessionRole: connectingSceneSession.role)
+  func userNotificationCenter(_ center: UNUserNotificationCenter,
+                              didReceive response: UNNotificationResponse,
+                              withCompletionHandler completionHandler: @escaping () -> Void) {
+    completionHandler()
   }
 }
diff --git a/FirebaseMessaging/CHANGELOG.md b/FirebaseMessaging/CHANGELOG.md
index dda2c675438..d8fe37262b9 100644
--- a/FirebaseMessaging/CHANGELOG.md
+++ b/FirebaseMessaging/CHANGELOG.md
@@ -1,3 +1,6 @@
+# 2021-05 -- v8.1.0
+- [fixed] Fixed an issue that notification open is not logged to Analytics correctly when app is completely shut off. (#7707, #8128).
+
 # 2021-04 -- v8.0.0
 - [changed] Remove the Instance ID dependency from Firebase Cloud Messaging. This is a breaking change for FCM users who use the deprecated Instance ID API to manage registration tokens. Users should migrate to FCM's token APIs by following the migration guide: https://firebase.google.com/docs/projects/manage-installations#fid-iid. (#7836)
 
diff --git a/FirebaseMessaging/Sources/FIRMessagingAnalytics.m b/FirebaseMessaging/Sources/FIRMessagingAnalytics.m
index d59c37d8789..af10b662cde 100644
--- a/FirebaseMessaging/Sources/FIRMessagingAnalytics.m
+++ b/FirebaseMessaging/Sources/FIRMessagingAnalytics.m
@@ -189,6 +189,7 @@ + (void)logUserPropertyForConversionTracking:(NSDictionary *)notification
 + (void)logMessage:(NSDictionary *)notification
        toAnalytics:(id<FIRAnalyticsInterop> _Nullable)analytics {
   // iOS only because Analytics doesn't support other platforms.
+
 #if TARGET_OS_IOS
   if (![self canLogNotification:notification]) {
     return;
@@ -201,9 +202,12 @@ + (void)logMessage:(NSDictionary *)notification
   UIApplicationState applicationState = application.applicationState;
   switch (applicationState) {
     case UIApplicationStateInactive:
-      // App was either in background(suspended) or inactive and user tapped on a display
-      // notification.
-      [self logOpenNotification:notification toAnalytics:analytics];
+      // App was in background and in transition to open when user tapped
+      // on a display notification.
+      // Needs to check notification is displayed.
+      if ([[self class] isDisplayNotification:notification]) {
+        [self logOpenNotification:notification toAnalytics:analytics];
+      }
       break;
 
     case UIApplicationStateActive:
@@ -212,11 +216,34 @@ + (void)logMessage:(NSDictionary *)notification
       break;
 
     default:
-      // Only a silent notification (i.e. 'content-available' is true) can be received while the app
-      // is in the background. These messages aren't loggable anyway.
+      // App was either in background state or in transition from closed
+      // to open.
+      // Needs to check notification is displayed.
+      if ([[self class] isDisplayNotification:notification]) {
+        [self logOpenNotification:notification toAnalytics:analytics];
+      }
       break;
   }
 #endif
 }
 
++ (BOOL)isDisplayNotification:(NSDictionary *)notification {
+  NSDictionary *aps = notification[kApsKey];
+  if (!aps || ![aps isKindOfClass:[NSDictionary class]]) {
+    return NO;
+  }
+  NSDictionary *alert = aps[kApsAlertKey];
+  if (!alert) {
+    return NO;
+  }
+  if ([alert isKindOfClass:[NSDictionary class]]) {
+    return alert.allKeys.count > 0;
+  }
+  // alert can be string sometimes (if only body is specified)
+  if ([alert isKindOfClass:[NSString class]]) {
+    return YES;
+  }
+  return NO;
+}
+
 @end
diff --git a/FirebaseMessaging/Tests/UnitTests/FIRMessagingAnalyticsTest.m b/FirebaseMessaging/Tests/UnitTests/FIRMessagingAnalyticsTest.m
index 5c6cb6227de..cca57385050 100644
--- a/FirebaseMessaging/Tests/UnitTests/FIRMessagingAnalyticsTest.m
+++ b/FirebaseMessaging/Tests/UnitTests/FIRMessagingAnalyticsTest.m
@@ -140,6 +140,7 @@ + (void)logEvent:(NSString *)event
     withNotification:(NSDictionary *)notification
          toAnalytics:(id<FIRAnalyticsInterop> _Nullable)analytics;
 ;
++ (BOOL)isDisplayNotification:(NSDictionary *)notification;
 
 @end
 
@@ -447,6 +448,42 @@ - (void)testLogOpenNotification {
                             toAnalytics:nil]);
 }
 
+- (void)testDisplayNotification {
+  NSDictionary *notification = @{
+    @"google.c.a.e" : @"1",
+  };
+  XCTAssertFalse([FIRMessagingAnalytics isDisplayNotification:notification]);
+
+  notification = @{
+    @"aps" : @{@"alert" : @"to check the reporting format"},
+  };
+  XCTAssertTrue([FIRMessagingAnalytics isDisplayNotification:notification]);
+
+  notification = @{
+    @"google.c.a.e" : @"1",
+    @"aps" : @{@"alert" : @{@"title" : @"Hello World"}},
+  };
+  XCTAssertTrue([FIRMessagingAnalytics isDisplayNotification:notification]);
+
+  notification = @{
+    @"google.c.a.e" : @"1",
+    @"aps" : @{@"alert" : @{@"body" : @"This is the body of notification."}},
+  };
+  XCTAssertTrue([FIRMessagingAnalytics isDisplayNotification:notification]);
+
+  notification = @{
+    @"google.c.a.e" : @"1",
+    @"aps" :
+        @{@"alert" : @{@"title" : @"Hello World", @"body" : @"This is the body of notification."}},
+  };
+  XCTAssertTrue([FIRMessagingAnalytics isDisplayNotification:notification]);
+
+  notification = @{
+    @"google.c.a.e" : @"1",
+    @"aps" : @{@"alert" : @{@"subtitle" : @"Hello World"}},
+  };
+  XCTAssertTrue([FIRMessagingAnalytics isDisplayNotification:notification]);
+}
 @end
 
 #endif


Test patch: