Skip to content
Snippets Groups Projects
PushHandler.swift 5.17 KiB
Newer Older
Ahmed Shehata's avatar
Ahmed Shehata committed
import UIKit
import Models
import Defaults
import Integration
import DependencyInjection

public final class PushHandler: PushHandling {
    private enum Constants {
        static let appGroup = "group.elixxir.messenger"
        static let usernamesSetting = "isShowingUsernames"
    }

    @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 {
            let session = try DependencyInjection.Container.shared.resolve() as SessionType
            try session.registerNotifications(token)
        } catch {
            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
        }

        guard let showSender = defaults.value(forKey: Constants.usernamesSetting) as? Bool, showSender == true else {
            pushes.map { ($0.type.unknownSenderContent!, $0) }
                .map(contentsBuilder.build)
                .forEach { completion($0) }
            return
        }

        let dbManager = GRDBDatabaseManager()
        try? dbManager.setup()

        let tuples: [(String, Push)] = pushes.compactMap {
            guard let userId = $0.source, let contact: Contact = try? dbManager.fetch(.withUserId(userId)).first else {
                return ($0.type.unknownSenderContent!, $0)
            }

            let name = contact.nickname ?? contact.username
            return ($0.type.knownSenderContent(name)!, $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 = PushType(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)
    }
}