From ebd30d1c9d76f5178bfeb25390abbd85868a46b4 Mon Sep 17 00:00:00 2001
From: Bruno Muniz Azevedo Filho <bruno@elixxir.io>
Date: Wed, 23 Nov 2022 00:31:08 -0300
Subject: [PATCH] trying to setup pulse

---
 .../NotificationService.swift                 |  92 +++++++-
 Package.swift                                 |  23 +-
 Sources/AppFeature/AppDelegate.swift          | 219 ++++++++++++++++-
 ...cyRegistrator.swift => Dependencies.swift} |  15 ++
 .../AppFeature/PushNotificationRouter.swift   |  57 +++++
 Sources/AppFeature/PushRouter.swift           |  57 -----
 Sources/LaunchFeature/LaunchController.swift  |  13 +-
 Sources/LaunchFeature/LaunchViewModel.swift   |  45 ++--
 .../PushNotificationRouter.swift}             |  10 +-
 Sources/PushFeature/ContentsBuilder.swift     |  23 --
 Sources/PushFeature/MockPushHandler.swift     |  39 ----
 Sources/PushFeature/Push.swift                |   7 -
 Sources/PushFeature/PushExtractor.swift       |  42 ----
 Sources/PushFeature/PushHandler.swift         | 220 ------------------
 Sources/PushFeature/PushHandling.swift        |  69 ------
 .../xcshareddata/swiftpm/Package.resolved     |   9 +
 16 files changed, 435 insertions(+), 505 deletions(-)
 rename Sources/AppFeature/{DependencyRegistrator.swift => Dependencies.swift} (87%)
 create mode 100644 Sources/AppFeature/PushNotificationRouter.swift
 delete mode 100644 Sources/AppFeature/PushRouter.swift
 rename Sources/{PushFeature/PushRouter.swift => LaunchFeature/PushNotificationRouter.swift} (62%)
 delete mode 100644 Sources/PushFeature/ContentsBuilder.swift
 delete mode 100644 Sources/PushFeature/MockPushHandler.swift
 delete mode 100644 Sources/PushFeature/Push.swift
 delete mode 100644 Sources/PushFeature/PushExtractor.swift
 delete mode 100644 Sources/PushFeature/PushHandler.swift
 delete mode 100644 Sources/PushFeature/PushHandling.swift

diff --git a/App/NotificationExtension/NotificationService.swift b/App/NotificationExtension/NotificationService.swift
index 4444b66a..d9657fb8 100644
--- a/App/NotificationExtension/NotificationService.swift
+++ b/App/NotificationExtension/NotificationService.swift
@@ -1,13 +1,97 @@
-import PushFeature
+import XXModels
+import XXClient
+import XXDatabase
+import ReportingFeature
+import XXMessengerClient
 import UserNotifications
 
 final class NotificationService: UNNotificationServiceExtension {
-  private let pushHandler = PushHandler()
-
   override func didReceive(
     _ request: UNNotificationRequest,
     withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void
   ) {
-    pushHandler.handlePush(request, contentHandler)
+    guard let defaults = UserDefaults(suiteName: "group.elixxir.messenger") else { return }
+
+    var environment = MessengerEnvironment.live()
+    environment.serviceList = .userDefaults(key: "preImage", userDefaults: defaults)
+    let messenger = Messenger.live(environment)
+    let userInfo = request.content.userInfo
+    let dbPath = FileManager.default
+      .containerURL(forSecurityApplicationGroupIdentifier: "group.elixxir.messenger")!
+      .appendingPathComponent("xxm_databasse")
+      .appendingPathExtension("sqlite").path
+
+    guard let csv = userInfo["notificationData"] as? String,
+          let reports = try? messenger.getNotificationReports(notificationCSV: csv) else { return }
+    reports
+      .filter { $0.forMe }
+      .filter { $0.type != .silent }
+      .filter { $0.type != .default }
+      .compactMap {
+        let content = UNMutableNotificationContent()
+        content.badge = 1
+        content.sound = .default
+        content.threadIdentifier = "new_message_identifier"
+        content.userInfo["type"] = $0.type.rawValue
+        content.userInfo["source"] = $0.source
+        content.body = getBodyForUnknownWith(type: $0.type)
+
+        guard let db = try? Database.onDisk(path: dbPath),
+              let contact = try? db.fetchContacts(.init(id: [$0.source])).first else {
+          return content
+        }
+        if ReportingStatus.live().isEnabled(), (contact.isBlocked || contact.isBanned) {
+          return nil
+        }
+        if let showSender = defaults.value(forKey: "isShowingUsernames") as? Bool, showSender == true {
+          let name = (contact.nickname ?? contact.username) ?? ""
+          content.body = getBodyFor(name: name, with: $0.type)
+        }
+        return content
+      }.forEach {
+        contentHandler($0)
+      }
+  }
+
+  private func getBodyForUnknownWith(type: NotificationReport.ReportType) -> String {
+    switch type {
+    case .`default`, .silent:
+      fatalError()
+    case .request:
+      return "Request received"
+    case .reset:
+      return "One of your contacts has restored their account"
+    case .confirm:
+      return "Request accepted"
+    case .e2e:
+      return "New private message"
+    case .group:
+      return "New group message"
+    case .endFT:
+      return "New media received"
+    case .groupRQ:
+      return "Group request received"
+    }
+  }
+
+  private func getBodyFor(name: String, with type: NotificationReport.ReportType) -> String {
+    switch type {
+    case .silent, .`default`:
+      fatalError()
+    case .e2e:
+      return String(format: "%@ sent you a private message", name)
+    case .reset:
+      return String(format: "%@ restored their account", name)
+    case .endFT:
+      return String(format: "%@ sent you a file", name)
+    case .group:
+      return String(format: "%@ sent you a group message", name)
+    case .groupRQ:
+      return String(format: "%@ sent you a group request", name)
+    case .confirm:
+      return String(format: "%@ confirmed your contact request", name)
+    case .request:
+      return String(format: "%@ sent you a contact request", name)
+    }
   }
 }
diff --git a/Package.swift b/Package.swift
index 7bf71c7d..155b1dd4 100644
--- a/Package.swift
+++ b/Package.swift
@@ -18,7 +18,6 @@ let package = Package(
     .library(name: "ScanFeature", targets: ["ScanFeature"]),
     .library(name: "MenuFeature", targets: ["MenuFeature"]),
     .library(name: "ChatFeature", targets: ["ChatFeature"]),
-    .library(name: "PushFeature", targets: ["PushFeature"]),
     .library(name: "CrashReport", targets: ["CrashReport"]),
     .library(name: "UpdateErrors", targets: ["UpdateErrors"]),
     .library(name: "CheckVersion", targets: ["CheckVersion"]),
@@ -121,6 +120,10 @@ let package = Package(
       url: "https://github.com/apple/swift-log.git",
       .upToNextMajor(from: "1.4.4")
     ),
+    .package(
+      url: "https://github.com/kean/Pulse.git",
+      .upToNextMajor(from: "2.1.3")
+    ),
     .package(
       url: "https://github.com/pointfreeco/xctest-dynamic-overlay.git",
       .upToNextMajor(from: "0.3.3")
@@ -136,7 +139,6 @@ let package = Package(
         .target(name: "ChatFeature"),
         .target(name: "MenuFeature"),
         .target(name: "CrashReport"),
-        .target(name: "PushFeature"),
         .target(name: "TermsFeature"),
         .target(name: "BackupFeature"),
         .target(name: "SearchFeature"),
@@ -154,6 +156,8 @@ let package = Package(
         .target(name: "CreateGroupFeature"),
         .target(name: "ContactListFeature"),
         .target(name: "RequestPermissionFeature"),
+        .product(name: "PulseUI", package: "Pulse"), // TO REMOVE
+        .product(name: "PulseLogHandler", package: "Pulse"), // TO REMOVE
       ]
     ),
     .testTarget(
@@ -270,17 +274,6 @@ let package = Package(
         ),
       ]
     ),
-    .target(
-      name: "PushFeature",
-      dependencies: [
-        .target(name: "AppCore"),
-        .target(name: "Defaults"),
-        .target(name: "ReportingFeature"),
-        .product(name: "XXDatabase", package: "client-ios-db"),
-        .product(name: "XXClient", package: "elixxir-dapps-sdk-swift"),
-        .product(name: "XXMessengerClient", package: "elixxir-dapps-sdk-swift"),
-      ]
-    ),
     .target(
       name: "Keychain",
       dependencies: [
@@ -392,7 +385,6 @@ let package = Package(
       name: "SearchFeature",
       dependencies: [
         .target(name: "Shared"),
-        .target(name: "PushFeature"),
         .target(name: "ContactFeature"),
         .target(name: "CountryListFeature"),
         .product(name: "Retry", package: "Retry"),
@@ -404,7 +396,6 @@ let package = Package(
       dependencies: [
         .target(name: "Shared"),
         .target(name: "Defaults"),
-        .target(name: "PushFeature"),
         .target(name: "UpdateErrors"),
         .target(name: "CheckVersion"),
         .target(name: "BackupFeature"),
@@ -525,7 +516,6 @@ let package = Package(
         .target(name: "Defaults"),
         .target(name: "Keychain"),
         .target(name: "InputField"),
-        .target(name: "PushFeature"),
         .target(name: "DrawerFeature"),
         .target(name: "AppNavigation"),
         .target(name: "CountryListFeature"),
@@ -611,7 +601,6 @@ let package = Package(
         .target(name: "Defaults"),
         .target(name: "Keychain"),
         .target(name: "InputField"),
-        .target(name: "PushFeature"),
         .target(name: "MenuFeature"),
         .target(name: "CrashReport"),
         .target(name: "DrawerFeature"),
diff --git a/Sources/AppFeature/AppDelegate.swift b/Sources/AppFeature/AppDelegate.swift
index cb3dce57..3f9cc3ec 100644
--- a/Sources/AppFeature/AppDelegate.swift
+++ b/Sources/AppFeature/AppDelegate.swift
@@ -1,23 +1,64 @@
 import UIKit
 import AppCore
 import Defaults
+import XXClient
+import Dependencies
 import LaunchFeature
+import XXMessengerClient
+
+// MARK: - TO REMOVE FROM PRODUCTION:
+import Logging
+import PulseUI
+import AppNavigation
+import PulseLogHandler
+// MARK: -
 
 public class AppDelegate: UIResponder, UIApplicationDelegate {
-  public var coverView: UIView?
   public var window: UIWindow?
+  private var coverView: UIView?
+  private var backgroundTimer: Timer?
+  private var backgroundTask: UIBackgroundTaskIdentifier?
+
+  @Dependency(\.app.log) var logger
+  @Dependency(\.navigator) var navigator
+  @Dependency(\.app.messenger) var messenger
+  @Dependency(\.pushNotificationRouter) var pushNotificationRouter
 
   @KeyObject(.hideAppList, defaultValue: false) var shouldHideAppInAppList
+  @KeyObject(.pushNotifications, defaultValue: false) var isPushNotificationsEnabled
 
   public func application(
     _ application: UIApplication,
     didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
   ) -> Bool {
+    UNUserNotificationCenter.current().delegate = self
 
     let navController = UINavigationController(rootViewController: LaunchController())
     window = UIWindow(frame: UIScreen.main.bounds)
     window?.rootViewController = RootViewController(navController)
     window?.makeKeyAndVisible()
+
+    pushNotificationRouter.set(.live(navigationController: navController))
+
+    // MARK: - TO REMOVE FROM PRODUCTION:
+    LoggingSystem.bootstrap(PersistentLogHandler.init)
+
+    NotificationCenter.default.addObserver(
+      forName: UIApplication.userDidTakeScreenshotNotification,
+      object: nil,
+      queue: OperationQueue.main
+    ) { [weak self] _ in
+      guard let self else { return }
+      let pulseViewController = PulseUI.MainViewController(store: .shared)
+      self.navigator.perform(
+        PresentModal(
+          pulseViewController,
+          from: navController.topViewController!
+        )
+      )
+    }
+    // MARK: -
+
     return true
   }
 
@@ -34,4 +75,180 @@ public class AppDelegate: UIResponder, UIApplicationDelegate {
     application.applicationIconBadgeNumber = 0
     coverView?.removeFromSuperview()
   }
+
+  public func applicationWillEnterForeground(_ application: UIApplication) {
+    resumeMessenger(application)
+  }
+
+  public func applicationDidEnterBackground(_ application: UIApplication) {
+    stopMessenger(application)
+  }
+
+  public func application(
+    application: UIApplication,
+    shouldAllowExtensionPointIdentifier identifier: String
+  ) -> Bool {
+    if identifier == UIApplication.ExtensionPointIdentifier.keyboard.rawValue {
+      return false /// Disable custom keyboards
+    }
+    return true
+  }
+
+  public func application(
+    _ application: UIApplication,
+    continue userActivity: NSUserActivity,
+    restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void
+  ) -> Bool {
+    guard userActivity.activityType == NSUserActivityTypeBrowsingWeb,
+          let incomingURL = userActivity.webpageURL,
+          let username = getUsernameFromInvitationDeepLink(incomingURL),
+          let router = pushNotificationRouter.get() else {
+      return false
+    }
+
+    router.navigateTo(.search(username: username), {})
+    return true
+  }
+}
+
+extension AppDelegate: UNUserNotificationCenterDelegate {
+  public func application(
+    _ application: UIApplication,
+    didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data
+  ) {
+    if messenger.isConnected() {
+      do {
+        try messenger.registerForNotifications(token: deviceToken)
+        isPushNotificationsEnabled = true
+      } catch {
+        isPushNotificationsEnabled = false
+        logger(.error(error as NSError))
+        print(error.localizedDescription)
+      }
+    }
+  }
+
+  public func userNotificationCenter(
+    _ center: UNUserNotificationCenter,
+    didReceive response: UNNotificationResponse,
+    withCompletionHandler completionHandler: @escaping () -> Void
+  ) {
+    let userInfo = response.notification.request.content.userInfo
+    guard let string = userInfo["type"] as? String,
+          let type = NotificationReport.ReportType(rawValue: string) else {
+      completionHandler()
+      return
+    }
+    var route: PushNotificationRouter.Route?
+    switch type {
+    case .e2e, .group:
+      guard let source = userInfo["source"] as? Data else {
+        completionHandler()
+        return
+      }
+      if type == .e2e {
+        route = .contactChat(id: source)
+      } else {
+        route = .groupChat(id: source)
+      }
+    default:
+      break
+    }
+
+    if let route, let router = pushNotificationRouter.get() {
+      router.navigateTo(route, completionHandler)
+    }
+  }
+
+  public func application(
+    _ application: UIApplication,
+    didReceiveRemoteNotification notification: [AnyHashable: Any],
+    fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void
+  ) {
+    if application.applicationState == .background,
+       let csv = notification["notificationData"] as? String,
+       let reports = try? messenger.getNotificationReports(notificationCSV: csv) {
+      reports
+        .filter { $0.forMe }
+        .filter { $0.type != .silent }
+        .filter { $0.type != .default }
+        .map {
+          let content = UNMutableNotificationContent()
+          content.badge = 1
+          content.body = ""
+          content.sound = .default
+          content.userInfo["source"] = $0.source
+          content.userInfo["type"] = $0.type.rawValue
+          content.threadIdentifier = "new_message_identifier"
+          return content
+        }.map {
+          UNNotificationRequest(
+            identifier: Bundle.main.bundleIdentifier!,
+            content: $0,
+            trigger: UNTimeIntervalNotificationTrigger(
+              timeInterval: 1,
+              repeats: false
+            )
+          )
+        }.forEach {
+          UNUserNotificationCenter.current().add($0) { error in
+            error == nil ? completionHandler(.newData) : completionHandler(.failed)
+          }
+        }
+    } else {
+      completionHandler(.noData)
+    }
+  }
+}
+
+extension AppDelegate {
+  private func resumeMessenger(_ application: UIApplication) {
+    backgroundTimer?.invalidate()
+    backgroundTimer = nil
+    if let backgroundTask {
+      application.endBackgroundTask(backgroundTask)
+    }
+    do {
+      if messenger.isLoaded() {
+        try messenger.start()
+      }
+    } catch {
+      logger(.error(error as NSError))
+      print(error.localizedDescription)
+    }
+  }
+
+  private func stopMessenger(_ application: UIApplication) {
+    guard messenger.isLoaded() else { return }
+
+    backgroundTask = application.beginBackgroundTask(withName: "STOPPING_NETWORK")
+    backgroundTimer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { [weak self] timer in
+      guard let self else { return }
+
+      if application.backgroundTimeRemaining <= 5 {
+        do {
+          self.backgroundTimer?.invalidate()
+          try self.messenger.stop()
+        } catch {
+          self.logger(.error(error as NSError))
+          print(error.localizedDescription)
+        }
+        if let backgroundTask = self.backgroundTask {
+          application.endBackgroundTask(backgroundTask)
+        }
+      }
+    }
+  }
+}
+
+func getUsernameFromInvitationDeepLink(_ url: URL) -> String? {
+  if let components = URLComponents(url: url, resolvingAgainstBaseURL: false),
+     components.scheme == "https",
+     components.host == "elixxir.io",
+     components.path == "/connect",
+     let queryItem = components.queryItems?.first(where: { $0.name == "username" }),
+     let username = queryItem.value {
+    return username
+  }
+  return nil
 }
diff --git a/Sources/AppFeature/DependencyRegistrator.swift b/Sources/AppFeature/Dependencies.swift
similarity index 87%
rename from Sources/AppFeature/DependencyRegistrator.swift
rename to Sources/AppFeature/Dependencies.swift
index 3504173f..16f2bf78 100644
--- a/Sources/AppFeature/DependencyRegistrator.swift
+++ b/Sources/AppFeature/Dependencies.swift
@@ -131,3 +131,18 @@ extension NavigatorKey: DependencyKey {
     )
   )
 }
+
+import LaunchFeature
+import XXMessengerClient
+
+private enum PushNotificationRouterKey: DependencyKey {
+  static var liveValue = Stored<PushNotificationRouter?>.inMemory()
+  static var testValue = Stored<PushNotificationRouter?>.unimplemented()
+}
+
+extension DependencyValues {
+  public var pushNotificationRouter: Stored<PushNotificationRouter?> {
+    get { self[PushNotificationRouterKey.self] }
+    set { self[PushNotificationRouterKey.self] = newValue }
+  }
+}
diff --git a/Sources/AppFeature/PushNotificationRouter.swift b/Sources/AppFeature/PushNotificationRouter.swift
new file mode 100644
index 00000000..0ff84204
--- /dev/null
+++ b/Sources/AppFeature/PushNotificationRouter.swift
@@ -0,0 +1,57 @@
+import UIKit
+import Dependencies
+import AppNavigation
+
+import ChatFeature
+import LaunchFeature
+import SearchFeature
+import ChatListFeature
+import RequestsFeature
+
+extension PushNotificationRouter {
+  public static func live(navigationController: UINavigationController) -> PushNotificationRouter {
+    PushNotificationRouter { route, completion in
+      @Dependency(\.navigator) var navigator
+      @Dependency(\.app.dbManager) var dbManager
+
+      if let launchController = navigationController.viewControllers.last as? LaunchController {
+        launchController.pendingPushNotificationRoute = route
+      } else {
+        switch route {
+        case .requests:
+          if !(navigationController.viewControllers.last is RequestsContainerController) {
+            navigator.perform(PresentRequests(on: navigationController))
+          }
+
+        case .search(username: let username):
+          if !(navigationController.viewControllers.last is SearchContainerController) {
+            navigator.perform(PresentSearch(
+              searching: username,
+              fromOnboarding: true,
+              on: navigationController,
+              animated: true
+            ))
+          } else {
+            (navigationController.viewControllers.last as? SearchContainerController)?
+              .startSearchingFor(username)
+          }
+
+        case .contactChat(id: let id):
+          if let contact = try? dbManager.getDB().fetchContacts(.init(id: [id])).first {
+            navigator.perform(SetStack([
+              ChatListController(), SingleChatController(contact)
+            ], on: navigationController))
+          }
+
+        case .groupChat(id: let id):
+          if let groupInfo = try? dbManager.getDB().fetchGroupInfos(.init(groupId: id)).first {
+            navigator.perform(SetStack([
+              ChatListController(), GroupChatController(groupInfo)
+            ], on: navigationController))
+          }
+        }
+      }
+      completion()
+    }
+  }
+}
diff --git a/Sources/AppFeature/PushRouter.swift b/Sources/AppFeature/PushRouter.swift
deleted file mode 100644
index f36d1e8a..00000000
--- a/Sources/AppFeature/PushRouter.swift
+++ /dev/null
@@ -1,57 +0,0 @@
-import UIKit
-import XXModels
-import PushFeature
-import ChatFeature
-import SearchFeature
-import LaunchFeature
-import ChatListFeature
-import RequestsFeature
-import XXMessengerClient
-
-extension PushRouter {
-  static func live(navigationController: UINavigationController) -> PushRouter {
-    fatalError()
-//    PushRouter { route, completion in
-//      if let launchController = navigationController.viewControllers.last as? LaunchController {
-//        launchController.pendingPushRoute = route
-//      } else {
-//        switch route {
-//        case .requests:
-//          if !(navigationController.viewControllers.last is RequestsContainerController) {
-//            navigationController.setViewControllers([RequestsContainerController()], animated: true)
-//          }
-//        case .search(username: let username):
-//          if let messenger = try? DI.Container.shared.resolve() as Messenger,
-//             let _ = try? messenger.ud.get()?.getContact() {
-//            if !(navigationController.viewControllers.last is SearchContainerController) {
-//              navigationController.setViewControllers([
-//                ChatListController(),
-//                SearchContainerController(username)
-//              ], animated: true)
-//            } else {
-//              (navigationController.viewControllers.last as? SearchContainerController)?.startSearchingFor(username)
-//            }
-//          }
-//        case .contactChat(id: let id):
-//          if let database: Database = try? DI.Container.shared.resolve(),
-//             let contact = try? dbManager.getDB().fetchContacts(.init(id: [id])).first {
-//            navigationController.setViewControllers([
-//              ChatListController(),
-//              SingleChatController(contact)
-//            ], animated: true)
-//          }
-//        case .groupChat(id: let id):
-//          if let database: Database = try? DI.Container.shared.resolve(),
-//             let info = try? dbManager.getDB().fetchGroupInfos(.init(groupId: id)).first {
-//            navigationController.setViewControllers([
-//              ChatListController(),
-//              GroupChatController(info)
-//            ], animated: true)
-//          }
-//        }
-//      }
-//
-//      completion()
-//    }
-  }
-}
diff --git a/Sources/LaunchFeature/LaunchController.swift b/Sources/LaunchFeature/LaunchController.swift
index 32e9b9b6..d6d1780b 100644
--- a/Sources/LaunchFeature/LaunchController.swift
+++ b/Sources/LaunchFeature/LaunchController.swift
@@ -1,7 +1,6 @@
 import UIKit
 import Shared
 import Combine
-import PushFeature
 import Dependencies
 import AppResources
 import DrawerFeature
@@ -15,6 +14,8 @@ public final class LaunchController: UIViewController {
   private var cancellables = Set<AnyCancellable>()
   private var drawerCancellables = Set<AnyCancellable>()
 
+  public var pendingPushNotificationRoute: PushNotificationRouter.Route?
+
   public override func loadView() {
     view = screenView
   }
@@ -31,10 +32,10 @@ public final class LaunchController: UIViewController {
             navigator.perform(PresentTermsAndConditions(replacing: true, on: navigationController!))
             return
           }
-//          if let route = pendingPushRoute {
-//            hasPendingPushRoute(route)
-//            return
-//          }
+          if let route = pendingPushNotificationRoute {
+            hasPendingPushRoute(route)
+            return
+          }
           navigator.perform(PresentChatList(on: navigationController!))
           return
         }
@@ -50,7 +51,7 @@ public final class LaunchController: UIViewController {
     viewModel.startLaunch()
   }
 
-  private func hasPendingPushRoute(_ route: PushRouter.Route) {
+  private func hasPendingPushRoute(_ route: PushNotificationRouter.Route) {
     switch route {
     case .requests:
       navigator.perform(PresentRequests(on: navigationController!))
diff --git a/Sources/LaunchFeature/LaunchViewModel.swift b/Sources/LaunchFeature/LaunchViewModel.swift
index 68587689..bbb68e5d 100644
--- a/Sources/LaunchFeature/LaunchViewModel.swift
+++ b/Sources/LaunchFeature/LaunchViewModel.swift
@@ -26,6 +26,8 @@ import XXLegacyDatabaseMigrator
 
 import class XXClient.Cancellable
 
+import PulseLogHandler
+
 final class LaunchViewModel {
   struct UpdateModel {
     let content: String
@@ -42,16 +44,15 @@ final class LaunchViewModel {
     var shouldPushOnboarding = false
   }
 
+  @Dependency(\.app.log) var log
   @Dependency(\.app.bgQueue) var bgQueue
   @Dependency(\.permissions) var permissions
   @Dependency(\.app.messenger) var messenger
   @Dependency(\.app.dbManager) var dbManager
-  @Dependency(\.keychain) var keychainManager
   @Dependency(\.updateErrors) var updateErrors
   @Dependency(\.app.hudManager) var hudManager
   @Dependency(\.checkVersion) var checkVersion
   @Dependency(\.dummyTraffic) var dummyTraffic
-  @Dependency(\.backupService) var backupService
   @Dependency(\.app.toastManager) var toastManager
   @Dependency(\.fetchBannedList) var fetchBannedList
   @Dependency(\.reportingStatus) var reportingStatus
@@ -166,17 +167,20 @@ final class LaunchViewModel {
 
 extension LaunchViewModel {
   func setupMessenger() throws {
-    authHandlerCancellable = authHandler {
-      print($0.localizedDescription)
+    _ = try messenger.setLogLevel(.trace)
+    messenger.startLogging()
+
+    authHandlerCancellable = authHandler { [weak self] in
+      self?.log(.error($0 as NSError))
     }
-    backupHandlerCancellable = backupHandler {
-      print($0.localizedDescription)
+    backupHandlerCancellable = backupHandler { [weak self] in
+      self?.log(.error($0 as NSError))
     }
-    receiveFileHandlerCancellable = receiveFileHandler {
-      print($0.localizedDescription)
+    receiveFileHandlerCancellable = receiveFileHandler { [weak self] in
+      self?.log(.error($0 as NSError))
     }
-    messageListenerHandlerCancellable = messageListener {
-      print($0.localizedDescription)
+    messageListenerHandlerCancellable = messageListener { [weak self] in
+      self?.log(.error($0 as NSError))
     }
 
     if messenger.isLoaded() == false {
@@ -217,18 +221,18 @@ extension LaunchViewModel {
       try? messenger.resumeBackup()
     }
 
-    groupRequestCancellable = groupRequest {
-      print($0)
+    groupRequestCancellable = groupRequest { [weak self] in
+      self?.log(.error($0 as NSError))
     }
 
-    groupMessageHandlerCancellable = groupMessageHandler {
-      print($0)
+    groupMessageHandlerCancellable = groupMessageHandler { [weak self] in
+      self?.log(.error($0 as NSError))
     }
 
     try messenger.startGroupChat()
 
-    try messenger.trackServices {
-      print($0.localizedDescription)
+    try messenger.trackServices { [weak self] in
+      self?.log(.error($0 as NSError))
     }
 
     try messenger.startFileTransfer()
@@ -239,10 +243,19 @@ extension LaunchViewModel {
         self.networkMonitor.update($0)
       }
     )
+
+    try failPendingProcessesFromLastSession()
   }
 }
 
 extension LaunchViewModel {
+  func failPendingProcessesFromLastSession() throws {
+    try dbManager.getDB().bulkUpdateMessages(
+      .init(status: [.sending]),
+      .init(status: .sendingFailed)
+    )
+  }
+
   func updateBannedList(completion: @escaping () -> Void) {
     fetchBannedList { result in
       switch result {
diff --git a/Sources/PushFeature/PushRouter.swift b/Sources/LaunchFeature/PushNotificationRouter.swift
similarity index 62%
rename from Sources/PushFeature/PushRouter.swift
rename to Sources/LaunchFeature/PushNotificationRouter.swift
index 942f747e..45b7850a 100644
--- a/Sources/PushFeature/PushRouter.swift
+++ b/Sources/LaunchFeature/PushNotificationRouter.swift
@@ -1,6 +1,7 @@
 import Foundation
+import XCTestDynamicOverlay
 
-public struct PushRouter {
+public struct PushNotificationRouter {
   public typealias NavigateTo = (Route, @escaping () -> Void) -> Void
 
   public enum Route {
@@ -17,7 +18,8 @@ public struct PushRouter {
   }
 }
 
-public extension PushRouter {
-  static let noop = PushRouter { _, _ in }
+public extension PushNotificationRouter {
+  static let unimplemented = PushNotificationRouter(
+    navigateTo: XCTUnimplemented("\(Self.self)")
+  )
 }
-
diff --git a/Sources/PushFeature/ContentsBuilder.swift b/Sources/PushFeature/ContentsBuilder.swift
deleted file mode 100644
index 9aa75041..00000000
--- a/Sources/PushFeature/ContentsBuilder.swift
+++ /dev/null
@@ -1,23 +0,0 @@
-import UserNotifications
-
-public struct ContentsBuilder {
-    enum Constants {
-        static let threadIdentifier = "new_message_identifier"
-    }
-
-    public var build: (String, Push) -> UNMutableNotificationContent
-}
-
-public extension ContentsBuilder {
-    static let live = ContentsBuilder { title, push in
-        let content = UNMutableNotificationContent()
-        content.badge = 1
-        content.body = title
-        content.title = title
-        content.sound = .default
-        content.userInfo["source"] = push.source
-        content.userInfo["type"] = push.type.rawValue
-        content.threadIdentifier = Constants.threadIdentifier
-        return content
-    }
-}
diff --git a/Sources/PushFeature/MockPushHandler.swift b/Sources/PushFeature/MockPushHandler.swift
deleted file mode 100644
index 385175c3..00000000
--- a/Sources/PushFeature/MockPushHandler.swift
+++ /dev/null
@@ -1,39 +0,0 @@
-import UIKit
-
-public struct MockPushHandler: PushHandling {
-  public init() {}
-  
-  public func registerToken(_ token: Data) {
-    // TODO
-  }
-  
-  public func requestAuthorization(
-    _ completion: @escaping (Result<Bool, Error>) -> Void
-  ) {
-    completion(.success(true))
-  }
-  
-  public func handlePush(
-    _ notification: [AnyHashable : Any],
-    _ completion: @escaping (UIBackgroundFetchResult) -> Void
-  ) {
-    completion(.noData)
-  }
-  
-  public func handlePush(
-    _ request: UNNotificationRequest,
-    _ completion: @escaping (UNNotificationContent) -> Void
-  ) {
-    let content = UNMutableNotificationContent()
-    content.title = String(describing: Self.self)
-    completion(content)
-  }
-  
-  public func handleAction(
-    _ router: PushRouter,
-    _ userInfo: [AnyHashable : Any],
-    _ completion: @escaping () -> Void
-  ) {
-    completion()
-  }
-}
diff --git a/Sources/PushFeature/Push.swift b/Sources/PushFeature/Push.swift
deleted file mode 100644
index a5ce5151..00000000
--- a/Sources/PushFeature/Push.swift
+++ /dev/null
@@ -1,7 +0,0 @@
-import XXClient
-import Foundation
-
-public struct Push {
-  public let type: NotificationReport.ReportType
-  public let source: Data
-}
diff --git a/Sources/PushFeature/PushExtractor.swift b/Sources/PushFeature/PushExtractor.swift
deleted file mode 100644
index ee584586..00000000
--- a/Sources/PushFeature/PushExtractor.swift
+++ /dev/null
@@ -1,42 +0,0 @@
-import XXModels
-import XXClient
-import Foundation
-import XXMessengerClient
-
-public struct PushExtractor {
-  enum Constants {
-    static let preImage = "preImage"
-    static let appGroup = "group.elixxir.messenger"
-    static let notificationData = "notificationData"
-  }
-  
-  public var extractFrom: ([AnyHashable: Any]) -> Result<[Push]?, Error>
-}
-
-public extension PushExtractor {
-  static let live = PushExtractor { dictionary in
-    var environment: MessengerEnvironment = .live()
-    environment.ndfEnvironment = .mainnet
-    environment.serviceList = .userDefaults(
-      key: "preImage",
-      userDefaults: UserDefaults(suiteName: "group.elixxir.messenger")!
-    )
-    let messenger = Messenger.live(environment)
-    guard let csv = dictionary[Constants.notificationData] as? String,
-          let defaults = UserDefaults(suiteName: Constants.appGroup) else {
-      return .success(nil)
-    }
-    do {
-      let reports = try messenger.getNotificationReports(notificationCSV: csv)
-      return .success(
-        reports
-          .filter { $0.forMe }
-          .filter { $0.type != .silent }
-          .filter { $0.type != .default }
-          .map { Push(type: $0.type, source: $0.source) }
-      )
-    } catch {
-      return .failure(error)
-    }
-  }
-}
diff --git a/Sources/PushFeature/PushHandler.swift b/Sources/PushFeature/PushHandler.swift
deleted file mode 100644
index 86bb7a29..00000000
--- a/Sources/PushFeature/PushHandler.swift
+++ /dev/null
@@ -1,220 +0,0 @@
-import UIKit
-import AppCore
-import Defaults
-import XXClient
-import XXModels
-import XXDatabase
-import ReportingFeature
-import XXMessengerClient
-import ComposableArchitecture
-
-public final class PushHandler: PushHandling {
-  private enum Constants {
-    static let appGroup = "group.elixxir.messenger"
-    static let usernamesSetting = "isShowingUsernames"
-  }
-
-  @Dependency(\.app.messenger) var messenger: Messenger
-
-  @KeyObject(.pushNotifications, defaultValue: false) var isPushEnabled: Bool
-
-  let requestAuth: RequestAuth
-  public static let defaultRequestAuth = UNUserNotificationCenter.current().requestAuthorization
-  public typealias RequestAuth = (UNAuthorizationOptions, @escaping (Bool, Error?) -> Void) -> Void
-
-  public var pushExtractor: PushExtractor
-  public var contentsBuilder: ContentsBuilder
-  public var applicationState: () -> UIApplication.State
-
-  public init(
-    requestAuth: @escaping RequestAuth = defaultRequestAuth,
-    pushExtractor: PushExtractor = .live,
-    contentsBuilder: ContentsBuilder = .live,
-    applicationState: @escaping () -> UIApplication.State = { UIApplication.shared.applicationState }
-  ) {
-    self.requestAuth = requestAuth
-    self.pushExtractor = pushExtractor
-    self.contentsBuilder = contentsBuilder
-    self.applicationState = applicationState
-  }
-
-  public func registerToken(_ token: Data) {
-    do {
-      try RegisterForNotifications.live(
-        e2eId: messenger.e2e.get()!.getId(),
-        token: token.map { String(format: "%02hhx", $0) }.joined()
-      )
-    } catch {
-      print(error.localizedDescription)
-      isPushEnabled = false
-    }
-  }
-
-  public func requestAuthorization(
-    _ completion: @escaping (Result<Bool, Error>) -> Void
-  ) {
-    let options: UNAuthorizationOptions = [.alert, .sound, .badge]
-
-    requestAuth(options) { granted, error in
-      guard let error = error else {
-        completion(.success(granted))
-        return
-      }
-
-      completion(.failure(error))
-    }
-  }
-
-  public func handlePush(
-    _ userInfo: [AnyHashable: Any],
-    _ completion: @escaping (UIBackgroundFetchResult) -> Void
-  ) {
-    do {
-      guard
-        let pushes = try pushExtractor.extractFrom(userInfo).get(),
-        applicationState() == .background,
-        pushes.isEmpty == false
-      else {
-        completion(.noData)
-        return
-      }
-
-      let content = contentsBuilder.build("New Messages Available", pushes.first!)
-      let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 1, repeats: false)
-      let request = UNNotificationRequest(identifier: Bundle.main.bundleIdentifier!, content: content, trigger: trigger)
-
-      UNUserNotificationCenter.current().add(request) { error in
-        if error == nil {
-          completion(.newData)
-        } else {
-          completion(.failed)
-        }
-      }
-    } catch {
-      completion(.failed)
-    }
-  }
-
-  public func handlePush(
-    _ request: UNNotificationRequest,
-    _ completion: @escaping (UNNotificationContent) -> Void
-  ) {
-    guard let pushes = try? pushExtractor.extractFrom(request.content.userInfo).get(), !pushes.isEmpty,
-          let defaults = UserDefaults(suiteName: Constants.appGroup) else { return }
-
-    let dbPath = FileManager.default
-      .containerURL(forSecurityApplicationGroupIdentifier: "group.elixxir.messenger")!
-      .appendingPathComponent("xxm_database")
-      .appendingPathExtension("sqlite").path
-
-    let tuples: [(String, Push)] = pushes.compactMap {
-      guard let dbManager = try? Database.onDisk(path: dbPath),
-            let contact = try? dbManager.fetchContacts(.init(id: [$0.source])).first else {
-        return (getStringForUnknown(type: $0.type), $0)
-      }
-
-      if ReportingStatus.live().isEnabled(), (contact.isBlocked || contact.isBanned) {
-        return nil
-      }
-
-      if let showSender = defaults.value(forKey: Constants.usernamesSetting) as? Bool, showSender == true {
-        let name = (contact.nickname ?? contact.username) ?? ""
-        return (getStringForKnown(name: name, type: $0.type), $0)
-      } else {
-        return (getStringForUnknown(type: $0.type), $0)
-      }
-    }
-
-    tuples
-      .map(contentsBuilder.build)
-      .forEach { completion($0) }
-  }
-
-  public func handleAction(
-    _ router: PushRouter,
-    _ userInfo: [AnyHashable : Any],
-    _ completion: @escaping () -> Void
-  ) {
-    guard let typeString = userInfo["type"] as? String,
-          let type = NotificationReport.ReportType.init(rawValue: typeString) else {
-      completion()
-      return
-    }
-
-    let route: PushRouter.Route
-
-    switch type {
-    case .e2e:
-      guard let source = userInfo["source"] as? Data else {
-        completion()
-        return
-      }
-
-      route = .contactChat(id: source)
-
-    case .group:
-      guard let source = userInfo["source"] as? Data else {
-        completion()
-        return
-      }
-
-      route = .groupChat(id: source)
-
-    case .request, .groupRQ:
-      route = .requests
-
-    case .silent, .`default`:
-      fatalError("Silent/Default push types should be filtered at this point")
-
-    case .reset, .endFT, .confirm:
-      route = .requests
-    }
-
-    router.navigateTo(route, completion)
-  }
-}
-
-private func getStringForUnknown(type: NotificationReport.ReportType) -> String {
-  switch type {
-  case .`default`, .silent:
-    return ""
-  case .request:
-    return "Request received"
-  case .reset:
-    return "One of your contacts has restored their account"
-  case .confirm:
-    return "Request accepted"
-  case .e2e:
-    return "New private message"
-  case .group:
-    return "New group message"
-  case .endFT:
-    return "New media received"
-  case .groupRQ:
-    return "Group request received"
-  }
-}
-
-private func getStringForKnown(
-  name: String,
-  type: NotificationReport.ReportType
-) -> String {
-  switch type {
-  case .silent, .`default`:
-    return ""
-  case .e2e:
-    return String(format: "%@ sent you a private message", name)
-  case .reset:
-    return String(format: "%@ restored their account", name)
-  case .endFT:
-    return String(format: "%@ sent you a file", name)
-  case .group:
-    return String(format: "%@ sent you a group message", name)
-  case .groupRQ:
-    return String(format: "%@ sent you a group request", name)
-  case .confirm:
-    return String(format: "%@ confirmed your contact request", name)
-  case .request:
-    return String(format: "%@ sent you a contact request", name)
-  }
-}
diff --git a/Sources/PushFeature/PushHandling.swift b/Sources/PushFeature/PushHandling.swift
deleted file mode 100644
index 1fe3a41b..00000000
--- a/Sources/PushFeature/PushHandling.swift
+++ /dev/null
@@ -1,69 +0,0 @@
-import UIKit
-
-public protocol PushHandling {
-  /// Submits the APNS token to a 3rd-party service.
-  /// This should be called whenever the user accepts
-  /// receiving remote push notifications.
-  ///
-  /// - Parameters:
-  ///   - token: The APNS provided token
-  ///
-  func registerToken(
-    _ token: Data
-  )
-
-  /// Prompts a system alert to the user requesting
-  /// permission for receiving remote push notifications
-  ///
-  /// - Parameters:
-  ///   - completion: Async result closure containing the user reponse
-  ///
-  func requestAuthorization(
-    _ completion: @escaping (Result<Bool, Error>) -> Void
-  )
-
-  /// Evaluates if the notification should be displayed or not
-  /// and if yes, how should it look like.
-  ///
-  /// - Note: This function should be called by the main app target
-  /// - Warning: The notifications should only appear if the app is in background
-  ///
-  /// - Parameters:
-  ///   - userInfo: Dictionary contaning the payload of the remote push
-  ///   - completion: Async closure containing the operation chosed
-  ///
-  func handlePush(
-    _ userInfo: [AnyHashable: Any],
-    _ completion: @escaping (UIBackgroundFetchResult) -> Void
-  )
-
-  /// Evaluates if the notification should be displayed or not
-  ///  and if yes, how it should look like and who is it from
-  ///
-  /// - Note: This function should be called by the `NotificationExtension`
-  ///
-  /// - Parameters:
-  ///   - request: The notification request that arrived for the `NotificationExtension`
-  ///   - completion: Async closure containing the operation chosed
-  ///
-  func handlePush(
-    _ request: UNNotificationRequest,
-    _ completion: @escaping (UNNotificationContent) -> Void
-  )
-
-  /// Deeplinks to any UI flow set within the notification.
-  /// It can get called either when the user starts the app
-  /// from a notification or when the user has the app in
-  /// background and resumes the app by tapping on a push
-  ///
-  /// - Parameters:
-  ///   - router: Router instance that will decide the correct UI flow
-  ///   - userInfo: Dictionary contaning the payload of the notification
-  ///   - completion: Async empty closure
-  ///
-  func handleAction(
-    _ router: PushRouter,
-    _ userInfo: [AnyHashable: Any],
-    _ completion: @escaping () -> Void
-  )
-}
diff --git a/client-ios.xcworkspace/xcshareddata/swiftpm/Package.resolved b/client-ios.xcworkspace/xcshareddata/swiftpm/Package.resolved
index a54280f2..97e0e358 100644
--- a/client-ios.xcworkspace/xcshareddata/swiftpm/Package.resolved
+++ b/client-ios.xcworkspace/xcshareddata/swiftpm/Package.resolved
@@ -243,6 +243,15 @@
         "version" : "2.1.1"
       }
     },
+    {
+      "identity" : "pulse",
+      "kind" : "remoteSourceControl",
+      "location" : "https://github.com/kean/Pulse.git",
+      "state" : {
+        "revision" : "6b682c529d98a38e6fdffee2a8bfa40c8de30821",
+        "version" : "2.1.3"
+      }
+    },
     {
       "identity" : "quick",
       "kind" : "remoteSourceControl",
-- 
GitLab