From 1203631d6e8bbf4cf7f33d8274896e7d8b8d88d5 Mon Sep 17 00:00:00 2001 From: Jullian Mercier <31648126+jullianm@users.noreply.github.com> Date: Wed, 11 Dec 2024 10:00:52 +0100 Subject: [PATCH 01/17] create Assembly, add DI mechanism, create Notification components and implementation --- .../FeatureConfigEvent.swift | 2 +- .../FederationEvent/FederationEvent.swift | 2 +- .../UpdateEvent/TeamEvent/TeamEvent.swift | 2 +- .../UpdateEvent/UserEvent/UserEvent.swift | 2 +- .../project.pbxproj | 52 ------ WireDomain/Sources/WireDomain/Assembly.swift | 121 +++++++++++++ .../DependencyInjection/Injector.swift | 94 ++++++++++ .../DependencyInjection/ServiceEntry.swift | 43 +++++ .../DependencyInjection/ServiceKey.swift | 35 ++++ .../NotificationPayload.swift | 55 ++++++ .../NotificationService.swift | 169 ++++++++++++++++++ .../NotificationSession.swift | 95 ++++++++++ .../Repositories/Team/TeamLocalStore.swift | 9 +- .../UpdateEvents/UpdateEventsRepository.swift | 23 ++- .../Repositories/User/UserLocalStore.swift | 16 ++ .../Repositories/User/UserRepository.swift | 9 + 16 files changed, 664 insertions(+), 65 deletions(-) create mode 100644 WireDomain/Sources/WireDomain/Assembly.swift create mode 100644 WireDomain/Sources/WireDomain/DependencyInjection/Injector.swift create mode 100644 WireDomain/Sources/WireDomain/DependencyInjection/ServiceEntry.swift create mode 100644 WireDomain/Sources/WireDomain/DependencyInjection/ServiceKey.swift create mode 100644 WireDomain/Sources/WireDomain/NotificationService/NotificationPayload.swift create mode 100644 WireDomain/Sources/WireDomain/NotificationService/NotificationService.swift create mode 100644 WireDomain/Sources/WireDomain/NotificationService/NotificationSession.swift diff --git a/WireAPI/Sources/WireAPI/Models/UpdateEvent/FeatureConfigEvent/FeatureConfigEvent.swift b/WireAPI/Sources/WireAPI/Models/UpdateEvent/FeatureConfigEvent/FeatureConfigEvent.swift index c302acc1290..9f02fe39746 100644 --- a/WireAPI/Sources/WireAPI/Models/UpdateEvent/FeatureConfigEvent/FeatureConfigEvent.swift +++ b/WireAPI/Sources/WireAPI/Models/UpdateEvent/FeatureConfigEvent/FeatureConfigEvent.swift @@ -20,7 +20,7 @@ import Foundation /// An event concerning feature configs. -public enum FeatureConfigEvent: Equatable, Codable { +public enum FeatureConfigEvent: Equatable, Codable, Sendable { /// A feature config was updated. diff --git a/WireAPI/Sources/WireAPI/Models/UpdateEvent/FederationEvent/FederationEvent.swift b/WireAPI/Sources/WireAPI/Models/UpdateEvent/FederationEvent/FederationEvent.swift index 2ed12d2ddad..b11b05b9617 100644 --- a/WireAPI/Sources/WireAPI/Models/UpdateEvent/FederationEvent/FederationEvent.swift +++ b/WireAPI/Sources/WireAPI/Models/UpdateEvent/FederationEvent/FederationEvent.swift @@ -20,7 +20,7 @@ import Foundation /// An event concerning federation between domains. -public enum FederationEvent: Equatable, Codable { +public enum FederationEvent: Equatable, Codable, Sendable { /// Two or more other domains have stopped federating /// with each other. diff --git a/WireAPI/Sources/WireAPI/Models/UpdateEvent/TeamEvent/TeamEvent.swift b/WireAPI/Sources/WireAPI/Models/UpdateEvent/TeamEvent/TeamEvent.swift index 72b1bd8f22f..56fd42b861f 100644 --- a/WireAPI/Sources/WireAPI/Models/UpdateEvent/TeamEvent/TeamEvent.swift +++ b/WireAPI/Sources/WireAPI/Models/UpdateEvent/TeamEvent/TeamEvent.swift @@ -20,7 +20,7 @@ import Foundation /// An event concerning teams. -public enum TeamEvent: Equatable, Codable { +public enum TeamEvent: Equatable, Codable, Sendable { /// The self team was deleted. diff --git a/WireAPI/Sources/WireAPI/Models/UpdateEvent/UserEvent/UserEvent.swift b/WireAPI/Sources/WireAPI/Models/UpdateEvent/UserEvent/UserEvent.swift index e0fc32be2e5..b9b53ad49f4 100644 --- a/WireAPI/Sources/WireAPI/Models/UpdateEvent/UserEvent/UserEvent.swift +++ b/WireAPI/Sources/WireAPI/Models/UpdateEvent/UserEvent/UserEvent.swift @@ -20,7 +20,7 @@ import Foundation /// An event concerning users. -public enum UserEvent: Equatable, Codable { +public enum UserEvent: Equatable, Codable, Sendable { /// The self user has added a new client. diff --git a/WireDomain/Project/WireDomain Project.xcodeproj/project.pbxproj b/WireDomain/Project/WireDomain Project.xcodeproj/project.pbxproj index 7131ad431ef..2ecd2fd9fcd 100644 --- a/WireDomain/Project/WireDomain Project.xcodeproj/project.pbxproj +++ b/WireDomain/Project/WireDomain Project.xcodeproj/project.pbxproj @@ -365,58 +365,6 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - C9D57B232CF75B3700012A0E /* FeatureConfigRepositoryTests.swift in Sources */, - C9F691292C9B164A008CC41F /* UserPushRemoveEventProcessorTests.swift in Sources */, - C96B757B2CDDEDC4003A85EB /* ConversationLabelsLocalStoreTests.swift in Sources */, - C9D5804A2CFF637600012A0E /* MessageLocalStoreTests.swift in Sources */, - C96B75292CDB7688003A85EB /* ConversationMemberUpdateEventProcessorTests.swift in Sources */, - C96B752A2CDB7688003A85EB /* ConversationMemberJoinEventTests.swift in Sources */, - C96B752B2CDB7688003A85EB /* ConversationProtocolUpdateEventProcessorTests.swift in Sources */, - C96B752C2CDB7688003A85EB /* ConversationAccessUpdateEventProcessorTests.swift in Sources */, - C96B755F2CDBCD24003A85EB /* UserLocalStoreTests.swift in Sources */, - C96B752D2CDB7688003A85EB /* ConversationCreateEventProcessorTests.swift in Sources */, - C96B752E2CDB7688003A85EB /* ConversationReceiptModeUpdateEventProcessorTests.swift in Sources */, - C96B752F2CDB7688003A85EB /* ConversationMemberLeaveEventTests.swift in Sources */, - C93961922C91B12800EA971A /* TestError.swift in Sources */, - EEC410262C60D48900E89394 /* SyncManagerTests.swift in Sources */, - C9C8FDD32C9DBE0E00702B91 /* UserLegalHoldDisableEventProcessorTests.swift in Sources */, - EE57A7032C2994420096F242 /* UpdateEventsRepositoryTests.swift in Sources */, - C98433F12CCBC251009723D4 /* MessageRepositoryTests.swift in Sources */, - C9C8FDD02C9DBE0E00702B91 /* TeamMemberLeaveEventProcessorTests.swift in Sources */, - C97C01A82CB813C9000683C5 /* UserClientsRepositoryTests.swift in Sources */, - C9841A1F2CA309310062A834 /* MockFeatureConfigRepositoryProtocol.swift in Sources */, - C97C015C2CB40038000683C5 /* UserPropertiesSetEventProcessorTests.swift in Sources */, - CB7979162C738547006FBA58 /* TestSetup.swift in Sources */, - 162356502C2B223100C6666C /* UserRepositoryTests.swift in Sources */, - C96B75452CDBA0A9003A85EB /* ConversationDeleteEventProcessorTests.swift in Sources */, - EE3F97542C2ADC4C00668DF1 /* ProteusMessageDecryptorTests.swift in Sources */, - C9D57A932CF7556800012A0E /* ConversationRepositoryTests.swift in Sources */, - EE57A70B2C2A8BAA0096F242 /* UpdateEventDecryptorTests.swift in Sources */, - C9E8A3AE2C73878B0093DD5C /* ConnectionsRepositoryTests.swift in Sources */, - C97C01BB2CBE5E65000683C5 /* UserUpdateEventProcessorTests.swift in Sources */, - C9C1024E2CE7984300EA273F /* OneOnOneResolverTests.swift in Sources */, - C97C013D2CAD7D69000683C5 /* UserPropertiesDeleteEventProcessorTests.swift in Sources */, - C9D574582CEB9FB400012A0E /* ConversationRenameEventProcessorTests.swift in Sources */, - C96B75612CDCAFC6003A85EB /* ConnectionsLocalStoreTests.swift in Sources */, - C9C8FDD22C9DBE0E00702B91 /* UserClientAddEventProcessorTests.swift in Sources */, - C96B756D2CDCFA36003A85EB /* FeatureConfigLocalStoreTests.swift in Sources */, - C9C8FDD12C9DBE0E00702B91 /* TeamMemberUpdateEventProcessorTests.swift in Sources */, - C9C1024C2CE7935600EA273F /* UserConnectionEventProcessorTests.swift in Sources */, - C97C01542CB04626000683C5 /* UserDeleteEventProcessorTests.swift in Sources */, - C96B75672CDCD3BC003A85EB /* UserClientsLocalStoreTests.swift in Sources */, - C9C8FDCE2C9DBE0E00702B91 /* FeatureConfigUpdateEventProcessorTests.swift in Sources */, - C9C8FDCF2C9DBE0E00702B91 /* TeamDeleteEventProcessorTests.swift in Sources */, - C97C01592CB40010000683C5 /* FederationConnectionRemovedEventProcessorTests.swift in Sources */, - C9D580482CFF57D500012A0E /* ConversationMLSWelcomeEventProcessorTests.swift in Sources */, - C97C01AC2CB92D47000683C5 /* UserLegalholdEnableEventProcessorTests.swift in Sources */, - C96B75652CDCC85B003A85EB /* TeamLocalStoreTests.swift in Sources */, - C96B75702CDD0000003A85EB /* MockFeatureConfigLocalStoreProtocol.swift in Sources */, - C97C015A2CB40010000683C5 /* FederationDeleteEventProcessorTests.swift in Sources */, - C9C8FDD42C9DBE0E00702B91 /* UserLegalholdRequestEventProcessorTests.swift in Sources */, - C96B755D2CDBB176003A85EB /* ConversationLocalStoreTests.swift in Sources */, - C96B75772CDD154C003A85EB /* UpdateEventsLocalStoreTests.swift in Sources */, - 017F679C2C20801800B6E02D /* TeamRepositoryTests.swift in Sources */, - C9E8A3B72C749F2A0093DD5C /* ConversationLabelsRepositoryTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/WireDomain/Sources/WireDomain/Assembly.swift b/WireDomain/Sources/WireDomain/Assembly.swift new file mode 100644 index 00000000000..1e257dbe91d --- /dev/null +++ b/WireDomain/Sources/WireDomain/Assembly.swift @@ -0,0 +1,121 @@ +// +// Wire +// Copyright (C) 2024 Wire Swiss GmbH +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see http://www.gnu.org/licenses/. +// + +import WireDataModel +import WireAPI +import WireFoundation + +public final class Assembly { + + private let userID: UUID + private let clientID: String + private let context: NSManagedObjectContext + private let sharedUserDefaults: UserDefaults + private let proteusService: any ProteusServiceInterface + private let apiService: any APIServiceProtocol + private let apiVersion: WireAPI.APIVersion + private let pushChannel: any PushChannelProtocol + private let cookieStorage: ZMPersistentCookieStorage + + init( + userID: UUID, + clientID: String, + context: NSManagedObjectContext, + sharedUserDefaults: UserDefaults, + proteusService: any ProteusServiceInterface, + apiService: any APIServiceProtocol, + apiVersion: WireAPI.APIVersion, + pushChannel: any PushChannelProtocol, + cookieStorage: ZMPersistentCookieStorage + ) { + self.userID = userID + self.clientID = clientID + self.context = context + self.sharedUserDefaults = sharedUserDefaults + self.proteusService = proteusService + self.apiService = apiService + self.apiVersion = apiVersion + self.pushChannel = pushChannel + self.cookieStorage = cookieStorage + + self.registerDomainDependencies() + } + + // MARK: - API Init + + private lazy var updateEventsAPI = UpdateEventsAPIBuilder( + apiService: apiService + ).makeAPI(for: apiVersion) + + private lazy var updateEventDecryptor = UpdateEventDecryptor( + proteusService: proteusService, + context: context + ) + + // MARK: - Repositories and local stores Init + + // TODO: [WPB-14606] when related PR merged, initialize and expose Domain components required by SyncManager (`UpdateEventsRepository`, `TeamRepository` etc), and register the dependencies. + + private lazy var userLocalStore = UserLocalStore(context: context) + + private lazy var updateEventsLocalStore = UpdateEventsLocalStore( + context: context, + userID: userID, + sharedUserDefaults: sharedUserDefaults + ) + +} + +extension Assembly { + + /// Register domain dependencies so they can automatically be resolved later on. + /// + /// - warning: Only meant to be used internally by WireDomain components. + + private func registerDomainDependencies() { + let injector = Injector.shared + + injector.register(ProteusServiceInterface.self) { + self.proteusService + } + + injector.register(UserLocalStoreProtocol.self) { + self.userLocalStore + } + + injector.register(UpdateEventsAPI.self) { + self.updateEventsAPI + } + + injector.register(UpdateEventDecryptorProtocol.self) { + self.updateEventDecryptor + } + + injector.register(UpdateEventsLocalStoreProtocol.self) { + self.updateEventsLocalStore + } + + injector.register(ZMPersistentCookieStorage.self) { + self.cookieStorage + } + } +} + + + + diff --git a/WireDomain/Sources/WireDomain/DependencyInjection/Injector.swift b/WireDomain/Sources/WireDomain/DependencyInjection/Injector.swift new file mode 100644 index 00000000000..333c7367ca5 --- /dev/null +++ b/WireDomain/Sources/WireDomain/DependencyInjection/Injector.swift @@ -0,0 +1,94 @@ +// +// Injector.swift +// +// +// Created by Jullian Mercier on 06/05/2022. +// + +import Foundation + +protocol OptionalProtocol { + static var wrappedType: Any.Type { get } +} + +extension Optional: OptionalProtocol { + public static var wrappedType: Any.Type { + Wrapped.self + } +} + +final class Injector { + static private(set) public var shared = Injector() + private var services: [ServiceKey: ServiceEntryProtocol] = [:] + + // MARK: - Object lifecycle + + private init() {} + + + // MARK: - Register + + @discardableResult + func register(_ serviceType: Service.Type, factory: @escaping () -> Service) -> ServiceEntry { + return _register(serviceType, factory: factory) + } + + @discardableResult + func register(_ serviceType: Service.Type, factory: @escaping (Arg1) -> Service) -> ServiceEntry { + return _register(serviceType, factory: factory) + } + + private func _register(_ serviceType: Service.Type, factory: @escaping (Arguments) -> Any) -> ServiceEntry { + let key = ServiceKey(serviceType: Service.self, argumentsType: Arguments.self) + let entry = ServiceEntry( + serviceType: serviceType, + argumentsType: Arguments.self, + factory: factory + ) + services[key] = entry + + return entry + } + + + // MARK: - Resolve + + func resolve() -> Service { + typealias FactoryType = ((Void)) -> Any + return _genericResolve(serviceType: Service.self) { (factory: FactoryType) in factory(()) } + } + + func resolve(argument: Arg1) -> Service { + typealias FactoryType = ((Arg1)) -> Any + return _genericResolve(serviceType: Service.self) { (factory: FactoryType) in factory((argument)) } + } + + private func _genericResolve(serviceType: Service.Type, invoker: @escaping ((Arguments) -> Any) -> Any) -> Service { + var resolvedInstance: Service? + var type: Any.Type + + if let optionalType = Service.self as? OptionalProtocol.Type { + type = optionalType.wrappedType + } else { + type = Service.self + } + + let key = ServiceKey(serviceType: type, argumentsType: Arguments.self) + + if let entry = services[key] { + resolvedInstance = resolve(entry: entry, invoker: invoker) + } + + if let resolvedInstance = resolvedInstance { + return resolvedInstance + } else { + fatalError("You need to register concrete type for \(Service.self)") + } + } + + private func resolve(entry: ServiceEntryProtocol, invoker: (Factory) -> Any) -> Service? { + let resolvedInstance = invoker(entry.factory as! Factory) + return resolvedInstance as? Service + } +} + diff --git a/WireDomain/Sources/WireDomain/DependencyInjection/ServiceEntry.swift b/WireDomain/Sources/WireDomain/DependencyInjection/ServiceEntry.swift new file mode 100644 index 00000000000..50b5fef05ef --- /dev/null +++ b/WireDomain/Sources/WireDomain/DependencyInjection/ServiceEntry.swift @@ -0,0 +1,43 @@ +// +// Wire +// Copyright (C) 2024 Wire Swiss GmbH +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see http://www.gnu.org/licenses/. +// + +import Foundation + +typealias FunctionType = Any + +protocol ServiceEntryProtocol: AnyObject { + var factory: FunctionType { get } + var serviceType: Any.Type { get } +} + +final class ServiceEntry: ServiceEntryProtocol { + + // MARK: - Properties + + let serviceType: Any.Type + let argumentsType: Any.Type + let factory: FunctionType + + // MARK: - Object lifecycle + + init(serviceType: Service.Type, argumentsType: Any.Type, factory: FunctionType) { + self.serviceType = serviceType + self.argumentsType = argumentsType + self.factory = factory + } +} diff --git a/WireDomain/Sources/WireDomain/DependencyInjection/ServiceKey.swift b/WireDomain/Sources/WireDomain/DependencyInjection/ServiceKey.swift new file mode 100644 index 00000000000..ff9e950cccb --- /dev/null +++ b/WireDomain/Sources/WireDomain/DependencyInjection/ServiceKey.swift @@ -0,0 +1,35 @@ +// +// Wire +// Copyright (C) 2024 Wire Swiss GmbH +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see http://www.gnu.org/licenses/. +// + +import Foundation + +struct ServiceKey { + let serviceType: Any.Type + let argumentsType: Any.Type +} + +extension ServiceKey: Hashable { + public func hash(into hasher: inout Hasher) { + ObjectIdentifier(serviceType).hash(into: &hasher) + ObjectIdentifier(argumentsType).hash(into: &hasher) + } + + static func == (lhs: ServiceKey, rhs: ServiceKey) -> Bool { + return lhs.serviceType == rhs.serviceType && lhs.argumentsType == rhs.argumentsType + } +} diff --git a/WireDomain/Sources/WireDomain/NotificationService/NotificationPayload.swift b/WireDomain/Sources/WireDomain/NotificationService/NotificationPayload.swift new file mode 100644 index 00000000000..d8f3a2ece75 --- /dev/null +++ b/WireDomain/Sources/WireDomain/NotificationService/NotificationPayload.swift @@ -0,0 +1,55 @@ +// +// Wire +// Copyright (C) 2024 Wire Swiss GmbH +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see http://www.gnu.org/licenses/. +// + +import Foundation + +struct NotificationPayload { + let userID: UUID + let eventID: UUID + + enum Failure: Error { + case missingUserID + case missingEventID + } + + enum Key: String { + case data + case user + case id + } + + init(userInfo: [AnyHashable : Any]) throws { + guard + let data = userInfo[Key.data.rawValue] as? [String: Any], + let userIDString = data[Key.user.rawValue] as? String, + let userID = UUID(uuidString: userIDString) + else { + throw Failure.missingUserID + } + + guard let innerData = data[Key.data.rawValue] as? [AnyHashable: Any], + let eventIDString = innerData[Key.id.rawValue] as? String, + let eventID = UUID(uuidString: eventIDString) + else { + throw Failure.missingEventID + } + + self.userID = userID + self.eventID = eventID + } +} diff --git a/WireDomain/Sources/WireDomain/NotificationService/NotificationService.swift b/WireDomain/Sources/WireDomain/NotificationService/NotificationService.swift new file mode 100644 index 00000000000..7617adf60d2 --- /dev/null +++ b/WireDomain/Sources/WireDomain/NotificationService/NotificationService.swift @@ -0,0 +1,169 @@ +// +// Wire +// Copyright (C) 2024 Wire Swiss GmbH +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see http://www.gnu.org/licenses/. +// + +import UserNotifications +import WireDataModel +import WireLogging + +final class NotificationService: UNNotificationServiceExtension { + + // MARK: - Failure + + enum Failure: Error { + case missingSelfClientID + } + + // MARK: - Properties + + private let injector = Injector.shared + private let logger = WireLogger.notifications + private var notificationSession: NotificationSession? + private var contentHandler: ((UNNotificationContent) -> Void)? + + // MARK: - Object lifecycle + + override init() { + logger.info("initializing new legacy notification service") + super.init() + } + + // MARK: - Notifications + + override func didReceive( + _ request: UNNotificationRequest, + withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void + ) { + let cookieStorage: ZMPersistentCookieStorage = injector.resolve() + let isAuthenticated = cookieStorage.isAuthenticated + + guard isAuthenticated else { + logger.error( + "Not displaying notification because app is not authenticated" + ) + + return finishWithEmptyNotification() + } + + self.contentHandler = contentHandler + + Task { + do { + let notificationUserInfo = request.content.userInfo + + let notification = try NotificationPayload( + userInfo: notificationUserInfo + ) + + notificationSession = try await createNotificationSession( + userID: notification.userID + ) + + try await notificationSession?.processPushNotification( + eventID: notification.eventID + ) + + } catch { + logError(error) + finishWithEmptyNotification() + } + } + } + + override func serviceExtensionTimeWillExpire() { + logger.warn("legacy service extension will expire") + finishWithEmptyNotification() + } + + private func createNotificationSession( + userID: UUID + ) async throws -> NotificationSession { + let userLocalStore: UserLocalStoreProtocol = injector.resolve() + let selfUserInfo = await userLocalStore.selfUserInfo() + + guard let selfClientID = selfUserInfo.clientId else { + throw Failure.missingSelfClientID + } + + let updateEventsRepository = UpdateEventsRepository( + userID: userID, + selfClientID: selfClientID, + updateEventsAPI: injector.resolve(), + pushChannel: injector.resolve(), + updateEventDecryptor: injector.resolve(), + updateEventsLocalStore: injector.resolve() + ) + + let notificationSession = NotificationSession( + userID: userID, + updateEventsRepository: updateEventsRepository + ) + + return notificationSession + } + + private func finishWithEmptyNotification() { + logger.info("finishing without showing notification") + let emptyNotification = UNNotificationContent() + contentHandler?(emptyNotification) + terminate() + } + + private func logError(_ error: any Error) { + switch error { + case let sessionError as NotificationSession.Failure: + switch sessionError { + case .unableToPullPendingEvents(let error): + logger.error( + "failed to process notification: could not pull pending events: \(error.localizedDescription)" + ) + } + + case let payloadError as NotificationPayload.Failure: + switch payloadError { + case .missingUserID: + logger.error( + "failed to decode notification payload: missing user ID" + ) + + case .missingEventID: + logger.error( + "failed to decode notification payload: missing event ID" + ) + } + + case let serviceError as NotificationService.Failure: + switch serviceError { + case .missingSelfClientID: + logger.error( + "failed to create notification session: missing self client ID" + ) + } + + default: + logger.error( + "failed to process notification: \(error.localizedDescription)" + ) + } + } + + private func terminate() { + // Content handler should only be consumed once. + contentHandler = nil + notificationSession = nil + } +} diff --git a/WireDomain/Sources/WireDomain/NotificationService/NotificationSession.swift b/WireDomain/Sources/WireDomain/NotificationService/NotificationSession.swift new file mode 100644 index 00000000000..cf9f85325b0 --- /dev/null +++ b/WireDomain/Sources/WireDomain/NotificationService/NotificationSession.swift @@ -0,0 +1,95 @@ +// +// Wire +// Copyright (C) 2024 Wire Swiss GmbH +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see http://www.gnu.org/licenses/. +// + +import WireAPI +import WireDataModel +import Combine + +final class NotificationSession { + + // MARK: - Failure + + enum Failure: Error { + case unableToPullPendingEvents(Error) + } + + // MARK: - Properties + + private let updateEventsRepository: any UpdateEventsRepositoryProtocol + private let userID: UUID + private var subscription: AnyCancellable? + + // MARK: - Object lifecycle + + init( + userID: UUID, + updateEventsRepository: any UpdateEventsRepositoryProtocol + ) { + self.updateEventsRepository = updateEventsRepository + self.userID = userID + + // Generate notification for current pending events as we receive them. + subscription = updateEventsRepository.observePendingEvents() + .sink { [weak self] updateEvents in + self?.generateNotification(for: updateEvents) + } + } + + deinit { + subscription = nil + } + + // MARK: - Notifications + + func processPushNotification( + eventID: UUID + ) async throws { + let newEventID = eventID + let lastEventId = updateEventsRepository.fetchLastEventEnvelopeID() + + if lastEventId == nil { + updateEventsRepository.storeLastEventEnvelopeID(newEventID) + } + + do { + try await updateEventsRepository.pullPendingEvents() + } catch { + throw Failure.unableToPullPendingEvents(error) + } + } + + private func generateNotification(for events: [UpdateEvent]) { + // TODO: [WPB-11175] - Generate UNNotificationContent from update events + for event in events { + switch event { + case .conversation(let conversationEvent): + break + case .featureConfig(let featureConfigEvent): + break + case .federation(let federationEvent): + break + case .user(let userEvent): + break + case .team(let teamEvent): + break + case .unknown(let eventType): + break + } + } + } +} diff --git a/WireDomain/Sources/WireDomain/Repositories/Team/TeamLocalStore.swift b/WireDomain/Sources/WireDomain/Repositories/Team/TeamLocalStore.swift index add4f74b6fa..532cd75ecc2 100644 --- a/WireDomain/Sources/WireDomain/Repositories/Team/TeamLocalStore.swift +++ b/WireDomain/Sources/WireDomain/Repositories/Team/TeamLocalStore.swift @@ -306,14 +306,7 @@ public final class TeamLocalStore: TeamLocalStoreProtocol { } public func selfUserInfo() async -> (id: UUID, clientId: String?) { - let selfUser = await userLocalStore.fetchSelfUser() - - return await context.perform { - ( - id: selfUser.remoteIdentifier, - clientId: selfUser.selfClient()?.remoteIdentifier - ) - } + await userLocalStore.selfUserInfo() } } diff --git a/WireDomain/Sources/WireDomain/Repositories/UpdateEvents/UpdateEventsRepository.swift b/WireDomain/Sources/WireDomain/Repositories/UpdateEvents/UpdateEventsRepository.swift index 73ec07cf81e..6096c57498a 100644 --- a/WireDomain/Sources/WireDomain/Repositories/UpdateEvents/UpdateEventsRepository.swift +++ b/WireDomain/Sources/WireDomain/Repositories/UpdateEvents/UpdateEventsRepository.swift @@ -21,10 +21,13 @@ import WireAPI import WireDataModel import WireFoundation import WireLogging +import Combine // sourcery: AutoMockable /// Access update events. protocol UpdateEventsRepositoryProtocol { + + func observePendingEvents() -> AnyPublisher<[UpdateEvent], Never> /// Pull pending events from the server, decrypt if needed, and store locally. /// @@ -77,6 +80,12 @@ protocol UpdateEventsRepositoryProtocol { /// - Parameter id: The id to store. func storeLastEventEnvelopeID(_ id: UUID) + + /// Fetches the last event envelope id. + /// + /// - Returns: The last envelope id if any. + + func fetchLastEventEnvelopeID() -> UUID? /// Pulls the last event envelope id and stores it locally. @@ -96,6 +105,7 @@ final class UpdateEventsRepository: UpdateEventsRepositoryProtocol { private let updateEventsLocalStore: any UpdateEventsLocalStoreProtocol private let encoder = JSONEncoder() private let decoder = JSONDecoder() + private let onDecryptedEvents = PassthroughSubject<[UpdateEvent], Never>() // MARK: - Object lifecycle @@ -116,6 +126,10 @@ final class UpdateEventsRepository: UpdateEventsRepositoryProtocol { } // MARK: - Pull pending events + + func observePendingEvents() -> AnyPublisher<[UpdateEvent], Never> { + onDecryptedEvents.eraseToAnyPublisher() + } func pullPendingEvents() async throws { WireLogger.sync.debug("pulling pending events") @@ -149,7 +163,8 @@ final class UpdateEventsRepository: UpdateEventsRepositoryProtocol { // We can only decrypt once so store the decrypted events for later retrieval. var decryptedEnvelope = envelope - decryptedEnvelope.events = try await updateEventDecryptor.decryptEvents(in: envelope) + let decryptedEvents = try await updateEventDecryptor.decryptEvents(in: envelope) + decryptedEnvelope.events = decryptedEvents WireLogger.sync.debug( "persisting envelope (\(count) of \(batchCount)", @@ -162,6 +177,8 @@ final class UpdateEventsRepository: UpdateEventsRepositoryProtocol { decryptedEnvelopeData, index: currentIndex ) + + onDecryptedEvents.send(decryptedEvents) currentIndex += 1 @@ -230,6 +247,10 @@ final class UpdateEventsRepository: UpdateEventsRepositoryProtocol { func stopReceivingLiveEvents() async { pushChannel.close() } + + func fetchLastEventEnvelopeID() -> UUID? { + updateEventsLocalStore.lastEventID() + } func storeLastEventEnvelopeID(_ id: UUID) { WireLogger.sync.debug( diff --git a/WireDomain/Sources/WireDomain/Repositories/User/UserLocalStore.swift b/WireDomain/Sources/WireDomain/Repositories/User/UserLocalStore.swift index 3ecf82ca186..b3cb9dc0057 100644 --- a/WireDomain/Sources/WireDomain/Repositories/User/UserLocalStore.swift +++ b/WireDomain/Sources/WireDomain/Repositories/User/UserLocalStore.swift @@ -127,6 +127,11 @@ public protocol UserLocalStoreProtocol { /// - returns: A list of users' qualified IDs. func fetchAllUserIDsWithOneOnOneConversation() async throws -> [WireDataModel.QualifiedID] + + /// Fetches self user info : user ID and client ID. + /// - returns: the user ID and the client ID. + + func selfUserInfo() async -> (id: UUID, clientId: String?) } public final class UserLocalStore: UserLocalStoreProtocol { @@ -378,4 +383,15 @@ public final class UserLocalStore: UserLocalStoreProtocol { user.isPendingMetadataRefresh = false } } + + public func selfUserInfo() async -> (id: UUID, clientId: String?) { + let selfUser = await fetchSelfUser() + + return await context.perform { + ( + id: selfUser.remoteIdentifier, + clientId: selfUser.selfClient()?.remoteIdentifier + ) + } + } } diff --git a/WireDomain/Sources/WireDomain/Repositories/User/UserRepository.swift b/WireDomain/Sources/WireDomain/Repositories/User/UserRepository.swift index 2a1427d61f6..71b4f1abd95 100644 --- a/WireDomain/Sources/WireDomain/Repositories/User/UserRepository.swift +++ b/WireDomain/Sources/WireDomain/Repositories/User/UserRepository.swift @@ -158,6 +158,11 @@ public protocol UserRepositoryProtocol { /// - returns: A list of users' qualified IDs. func fetchAllUserIDsWithOneOnOneConversation() async throws -> [WireDataModel.QualifiedID] + + /// Fetches self user info : user ID and client ID. + /// - returns: the user ID and the client ID. + + func selfUserInfo() async -> (id: UUID, clientId: String?) } @@ -366,4 +371,8 @@ public final class UserRepository: UserRepositoryProtocol { return isSelfUser } + + public func selfUserInfo() async -> (id: UUID, clientId: String?) { + await userLocalStore.selfUserInfo() + } } From 35ffc9df1f9e7b3563a481028b72bf48e14fef3e Mon Sep 17 00:00:00 2001 From: Jullian Mercier <31648126+jullianm@users.noreply.github.com> Date: Wed, 11 Dec 2024 12:15:37 +0100 Subject: [PATCH 02/17] dependency injection: add register / resolve methods for multiple arguments --- .../DependencyInjection/Injector.swift | 44 ++++++++++++++++++- 1 file changed, 42 insertions(+), 2 deletions(-) diff --git a/WireDomain/Sources/WireDomain/DependencyInjection/Injector.swift b/WireDomain/Sources/WireDomain/DependencyInjection/Injector.swift index 333c7367ca5..910abedddca 100644 --- a/WireDomain/Sources/WireDomain/DependencyInjection/Injector.swift +++ b/WireDomain/Sources/WireDomain/DependencyInjection/Injector.swift @@ -18,7 +18,7 @@ extension Optional: OptionalProtocol { } final class Injector { - static private(set) public var shared = Injector() + nonisolated(unsafe) static private(set) public var shared = Injector() private var services: [ServiceKey: ServiceEntryProtocol] = [:] // MARK: - Object lifecycle @@ -38,6 +38,26 @@ final class Injector { return _register(serviceType, factory: factory) } + @discardableResult + func register(_ serviceType: Service.Type, factory: @escaping (Arg1, Arg2) -> Service) -> ServiceEntry { + return _register(serviceType, factory: factory) + } + + @discardableResult + func register(_ serviceType: Service.Type, factory: @escaping (Arg1, Arg2, Arg3) -> Service) -> ServiceEntry { + return _register(serviceType, factory: factory) + } + + @discardableResult + func register(_ serviceType: Service.Type, factory: @escaping (Arg1, Arg2, Arg3, Arg4) -> Service) -> ServiceEntry { + return _register(serviceType, factory: factory) + } + + @discardableResult + func register(_ serviceType: Service.Type, factory: @escaping (Arg1, Arg2, Arg3, Arg4, Arg5) -> Service) -> ServiceEntry { + return _register(serviceType, factory: factory) + } + private func _register(_ serviceType: Service.Type, factory: @escaping (Arguments) -> Any) -> ServiceEntry { let key = ServiceKey(serviceType: Service.self, argumentsType: Arguments.self) let entry = ServiceEntry( @@ -63,7 +83,27 @@ final class Injector { return _genericResolve(serviceType: Service.self) { (factory: FactoryType) in factory((argument)) } } - private func _genericResolve(serviceType: Service.Type, invoker: @escaping ((Arguments) -> Any) -> Any) -> Service { + func resolve(arguments arg1: Arg1, _ arg2: Arg2) -> Service { + typealias FactoryType = ((Arg1, Arg2)) -> Any + return _genericResolve(serviceType: Service.self) { (factory: FactoryType) in factory((arg1, arg2)) } + } + + func resolve(arguments arg1: Arg1, _ arg2: Arg2, _ arg3: Arg3) -> Service { + typealias FactoryType = ((Arg1, Arg2, Arg3)) -> Any + return _genericResolve(serviceType: Service.self) { (factory: FactoryType) in factory((arg1, arg2, arg3)) } + } + + func resolve(arguments arg1: Arg1, _ arg2: Arg2, _ arg3: Arg3, _ arg4: Arg4) -> Service { + typealias FactoryType = ((Arg1, Arg2, Arg3, Arg4)) -> Any + return _genericResolve(serviceType: Service.self) { (factory: FactoryType) in factory((arg1, arg2, arg3, arg4)) } + } + + func resolve(arguments arg1: Arg1, _ arg2: Arg2, _ arg3: Arg3, _ arg4: Arg4, _ arg5: Arg5) -> Service { + typealias FactoryType = ((Arg1, Arg2, Arg3, Arg4, Arg5)) -> Any + return _genericResolve(serviceType: Service.self) { (factory: FactoryType) in factory((arg1, arg2, arg3, arg4, arg5)) } + } + + func _genericResolve(serviceType: Service.Type, invoker: @escaping ((Arguments) -> Any) -> Any) -> Service { var resolvedInstance: Service? var type: Any.Type From 634572873d3401bab3644fcfa9f84c99b748054d Mon Sep 17 00:00:00 2001 From: Jullian Mercier <31648126+jullianm@users.noreply.github.com> Date: Thu, 12 Dec 2024 17:46:11 +0100 Subject: [PATCH 03/17] add injector tests, add callback when notification content received, clean up code --- WireDomain/Sources/WireDomain/Assembly.swift | 31 ++- .../Dependency Injection/Injector.swift | 187 ++++++++++++++++ .../ServiceEntry.swift | 0 .../ServiceKey.swift | 0 .../DependencyInjection/Injector.swift | 134 ------------ .../NotificationPayload.swift | 1 + .../NotificationService.swift | 30 ++- .../NotificationSession.swift | 22 +- .../UpdateEvents/UpdateEventsRepository.swift | 4 +- .../generated/AutoMockable.generated.swift | 180 ++++------------ .../Dependency Injection/InjectorTests.swift | 93 ++++++++ .../Mock/MockUpdateEventsRepository.swift | 202 ++++++++++++++++++ 12 files changed, 565 insertions(+), 319 deletions(-) create mode 100644 WireDomain/Sources/WireDomain/Dependency Injection/Injector.swift rename WireDomain/Sources/WireDomain/{DependencyInjection => Dependency Injection}/ServiceEntry.swift (100%) rename WireDomain/Sources/WireDomain/{DependencyInjection => Dependency Injection}/ServiceKey.swift (100%) delete mode 100644 WireDomain/Sources/WireDomain/DependencyInjection/Injector.swift create mode 100644 WireDomain/Tests/WireDomainTests/Dependency Injection/InjectorTests.swift create mode 100644 WireDomain/Tests/WireDomainTests/Repositories/Mock/MockUpdateEventsRepository.swift diff --git a/WireDomain/Sources/WireDomain/Assembly.swift b/WireDomain/Sources/WireDomain/Assembly.swift index 1e257dbe91d..cbf548b24a2 100644 --- a/WireDomain/Sources/WireDomain/Assembly.swift +++ b/WireDomain/Sources/WireDomain/Assembly.swift @@ -53,7 +53,7 @@ public final class Assembly { self.pushChannel = pushChannel self.cookieStorage = cookieStorage - self.registerDomainDependencies() + self.registerNotificationServiceDependencies() } // MARK: - API Init @@ -69,8 +69,6 @@ public final class Assembly { // MARK: - Repositories and local stores Init - // TODO: [WPB-14606] when related PR merged, initialize and expose Domain components required by SyncManager (`UpdateEventsRepository`, `TeamRepository` etc), and register the dependencies. - private lazy var userLocalStore = UserLocalStore(context: context) private lazy var updateEventsLocalStore = UpdateEventsLocalStore( @@ -83,34 +81,31 @@ public final class Assembly { extension Assembly { - /// Register domain dependencies so they can automatically be resolved later on. - /// - /// - warning: Only meant to be used internally by WireDomain components. + /// Register some domain dependencies to be resolved by the `NotificationService`. + /// Since `NotificationService` is not initializable, the injector provides a lightweight dependency injection mechanism to retrieve some already initialized dependencies that the notification service requires. - private func registerDomainDependencies() { - let injector = Injector.shared - - injector.register(ProteusServiceInterface.self) { - self.proteusService - } - - injector.register(UserLocalStoreProtocol.self) { + private func registerNotificationServiceDependencies() { + Injector.register(UserLocalStoreProtocol.self) { self.userLocalStore } - injector.register(UpdateEventsAPI.self) { + Injector.register(UpdateEventsAPI.self) { self.updateEventsAPI } - injector.register(UpdateEventDecryptorProtocol.self) { + Injector.register(PushChannelProtocol.self) { + self.pushChannel + } + + Injector.register(UpdateEventDecryptorProtocol.self) { self.updateEventDecryptor } - injector.register(UpdateEventsLocalStoreProtocol.self) { + Injector.register(UpdateEventsLocalStoreProtocol.self) { self.updateEventsLocalStore } - injector.register(ZMPersistentCookieStorage.self) { + Injector.register(ZMPersistentCookieStorage.self) { self.cookieStorage } } diff --git a/WireDomain/Sources/WireDomain/Dependency Injection/Injector.swift b/WireDomain/Sources/WireDomain/Dependency Injection/Injector.swift new file mode 100644 index 00000000000..e43b9622b72 --- /dev/null +++ b/WireDomain/Sources/WireDomain/Dependency Injection/Injector.swift @@ -0,0 +1,187 @@ +// +// Injector.swift +// +// +// Created by Jullian Mercier on 06/05/2022. +// + +import Foundation + +protocol OptionalProtocol { + static var wrappedType: Any.Type { get } +} + +extension Optional: OptionalProtocol { + public static var wrappedType: Any.Type { + Wrapped.self + } +} + +final class Injector { + nonisolated(unsafe) private static var services: [ServiceKey: ServiceEntryProtocol] = [:] + + // MARK: - Register + + static func register( + _ serviceType: Service.Type, + factory: @escaping () -> Service + ) { + _register(serviceType, factory: factory) + } + + static func register( + _ serviceType: Service.Type, + factory: @escaping (Arg1) -> Service + ) { + _register(serviceType, factory: factory) + } + + static func register( + _ serviceType: Service.Type, + factory: @escaping (Arg1, Arg2) -> Service + ) { + _register(serviceType, factory: factory) + } + + static func register( + _ serviceType: Service.Type, + factory: @escaping (Arg1, Arg2, Arg3) -> Service + ) { + _register(serviceType, factory: factory) + } + + static func register( + _ serviceType: Service.Type, + factory: @escaping (Arg1, Arg2, Arg3, Arg4) -> Service + ) { + _register(serviceType, factory: factory) + } + + static func register( + _ serviceType: Service.Type, + factory: @escaping (Arg1, Arg2, Arg3, Arg4, Arg5) -> Service + ) { + _register(serviceType, factory: factory) + } + + static func register( + _ serviceType: Service.Type, + factory: @escaping (Arg1, Arg2, Arg3, Arg4, Arg5, Arg6) -> Service + ) { + _register(serviceType, factory: factory) + } + + private static func _register( + _ serviceType: Service.Type, + factory: @escaping (Arguments) -> Any + ) { + + let key = ServiceKey(serviceType: Service.self, argumentsType: Arguments.self) + + let entry = ServiceEntry( + serviceType: serviceType, + argumentsType: Arguments.self, + factory: factory + ) + services[key] = entry + } + + + // MARK: - Resolve + + static func resolve() -> Service { + typealias FactoryType = ((Void)) -> Any + return _genericResolve(serviceType: Service.self) { (factory: FactoryType) in + factory(()) + } + } + + static func resolve( + argument: Arg1 + ) -> Service { + typealias FactoryType = ((Arg1)) -> Any + return _genericResolve(serviceType: Service.self) { (factory: FactoryType) in + factory((argument)) + } + } + + static func resolve( + arguments arg1: Arg1, _ arg2: Arg2 + ) -> Service { + typealias FactoryType = ((Arg1, Arg2)) -> Any + return _genericResolve(serviceType: Service.self) { (factory: FactoryType) in + factory((arg1, arg2)) + } + } + + static func resolve( + arguments arg1: Arg1, _ arg2: Arg2, _ arg3: Arg3 + ) -> Service { + typealias FactoryType = ((Arg1, Arg2, Arg3)) -> Any + return _genericResolve(serviceType: Service.self) { (factory: FactoryType) in + factory((arg1, arg2, arg3)) + } + } + + static func resolve( + arguments arg1: Arg1, _ arg2: Arg2, _ arg3: Arg3, _ arg4: Arg4 + ) -> Service { + typealias FactoryType = ((Arg1, Arg2, Arg3, Arg4)) -> Any + return _genericResolve(serviceType: Service.self) { (factory: FactoryType) in + factory((arg1, arg2, arg3, arg4)) + } + } + + static func resolve( + arguments arg1: Arg1, _ arg2: Arg2, _ arg3: Arg3, _ arg4: Arg4, _ arg5: Arg5 + ) -> Service { + typealias FactoryType = ((Arg1, Arg2, Arg3, Arg4, Arg5)) -> Any + return _genericResolve(serviceType: Service.self) { (factory: FactoryType) in + factory((arg1, arg2, arg3, arg4, arg5)) + } + } + + static func resolve( + arguments arg1: Arg1, _ arg2: Arg2, _ arg3: Arg3, _ arg4: Arg4, _ arg5: Arg5, _ arg6: Arg6 + ) -> Service { + typealias FactoryType = ((Arg1, Arg2, Arg3, Arg4, Arg5, Arg6)) -> Any + return _genericResolve(serviceType: Service.self) { (factory: FactoryType) in + factory((arg1, arg2, arg3, arg4, arg5, arg6)) + } + } + + static func _genericResolve( + serviceType: Service.Type, + invoker: @escaping ((Arguments) -> Any) -> Any + ) -> Service { + var resolvedInstance: Service? + var type: Any.Type + + if let optionalType = Service.self as? OptionalProtocol.Type { + type = optionalType.wrappedType + } else { + type = Service.self + } + + let key = ServiceKey(serviceType: type, argumentsType: Arguments.self) + + if let entry = services[key] { + resolvedInstance = resolve(entry: entry, invoker: invoker) + } + + if let resolvedInstance = resolvedInstance { + return resolvedInstance + } else { + fatalError("You need to register concrete type for \(Service.self)") + } + } + + private static func resolve( + entry: ServiceEntryProtocol, + invoker: (Factory) -> Any + ) -> Service? { + let resolvedInstance = invoker(entry.factory as! Factory) + return resolvedInstance as? Service + } +} + diff --git a/WireDomain/Sources/WireDomain/DependencyInjection/ServiceEntry.swift b/WireDomain/Sources/WireDomain/Dependency Injection/ServiceEntry.swift similarity index 100% rename from WireDomain/Sources/WireDomain/DependencyInjection/ServiceEntry.swift rename to WireDomain/Sources/WireDomain/Dependency Injection/ServiceEntry.swift diff --git a/WireDomain/Sources/WireDomain/DependencyInjection/ServiceKey.swift b/WireDomain/Sources/WireDomain/Dependency Injection/ServiceKey.swift similarity index 100% rename from WireDomain/Sources/WireDomain/DependencyInjection/ServiceKey.swift rename to WireDomain/Sources/WireDomain/Dependency Injection/ServiceKey.swift diff --git a/WireDomain/Sources/WireDomain/DependencyInjection/Injector.swift b/WireDomain/Sources/WireDomain/DependencyInjection/Injector.swift deleted file mode 100644 index 910abedddca..00000000000 --- a/WireDomain/Sources/WireDomain/DependencyInjection/Injector.swift +++ /dev/null @@ -1,134 +0,0 @@ -// -// Injector.swift -// -// -// Created by Jullian Mercier on 06/05/2022. -// - -import Foundation - -protocol OptionalProtocol { - static var wrappedType: Any.Type { get } -} - -extension Optional: OptionalProtocol { - public static var wrappedType: Any.Type { - Wrapped.self - } -} - -final class Injector { - nonisolated(unsafe) static private(set) public var shared = Injector() - private var services: [ServiceKey: ServiceEntryProtocol] = [:] - - // MARK: - Object lifecycle - - private init() {} - - - // MARK: - Register - - @discardableResult - func register(_ serviceType: Service.Type, factory: @escaping () -> Service) -> ServiceEntry { - return _register(serviceType, factory: factory) - } - - @discardableResult - func register(_ serviceType: Service.Type, factory: @escaping (Arg1) -> Service) -> ServiceEntry { - return _register(serviceType, factory: factory) - } - - @discardableResult - func register(_ serviceType: Service.Type, factory: @escaping (Arg1, Arg2) -> Service) -> ServiceEntry { - return _register(serviceType, factory: factory) - } - - @discardableResult - func register(_ serviceType: Service.Type, factory: @escaping (Arg1, Arg2, Arg3) -> Service) -> ServiceEntry { - return _register(serviceType, factory: factory) - } - - @discardableResult - func register(_ serviceType: Service.Type, factory: @escaping (Arg1, Arg2, Arg3, Arg4) -> Service) -> ServiceEntry { - return _register(serviceType, factory: factory) - } - - @discardableResult - func register(_ serviceType: Service.Type, factory: @escaping (Arg1, Arg2, Arg3, Arg4, Arg5) -> Service) -> ServiceEntry { - return _register(serviceType, factory: factory) - } - - private func _register(_ serviceType: Service.Type, factory: @escaping (Arguments) -> Any) -> ServiceEntry { - let key = ServiceKey(serviceType: Service.self, argumentsType: Arguments.self) - let entry = ServiceEntry( - serviceType: serviceType, - argumentsType: Arguments.self, - factory: factory - ) - services[key] = entry - - return entry - } - - - // MARK: - Resolve - - func resolve() -> Service { - typealias FactoryType = ((Void)) -> Any - return _genericResolve(serviceType: Service.self) { (factory: FactoryType) in factory(()) } - } - - func resolve(argument: Arg1) -> Service { - typealias FactoryType = ((Arg1)) -> Any - return _genericResolve(serviceType: Service.self) { (factory: FactoryType) in factory((argument)) } - } - - func resolve(arguments arg1: Arg1, _ arg2: Arg2) -> Service { - typealias FactoryType = ((Arg1, Arg2)) -> Any - return _genericResolve(serviceType: Service.self) { (factory: FactoryType) in factory((arg1, arg2)) } - } - - func resolve(arguments arg1: Arg1, _ arg2: Arg2, _ arg3: Arg3) -> Service { - typealias FactoryType = ((Arg1, Arg2, Arg3)) -> Any - return _genericResolve(serviceType: Service.self) { (factory: FactoryType) in factory((arg1, arg2, arg3)) } - } - - func resolve(arguments arg1: Arg1, _ arg2: Arg2, _ arg3: Arg3, _ arg4: Arg4) -> Service { - typealias FactoryType = ((Arg1, Arg2, Arg3, Arg4)) -> Any - return _genericResolve(serviceType: Service.self) { (factory: FactoryType) in factory((arg1, arg2, arg3, arg4)) } - } - - func resolve(arguments arg1: Arg1, _ arg2: Arg2, _ arg3: Arg3, _ arg4: Arg4, _ arg5: Arg5) -> Service { - typealias FactoryType = ((Arg1, Arg2, Arg3, Arg4, Arg5)) -> Any - return _genericResolve(serviceType: Service.self) { (factory: FactoryType) in factory((arg1, arg2, arg3, arg4, arg5)) } - } - - func _genericResolve(serviceType: Service.Type, invoker: @escaping ((Arguments) -> Any) -> Any) -> Service { - var resolvedInstance: Service? - var type: Any.Type - - if let optionalType = Service.self as? OptionalProtocol.Type { - type = optionalType.wrappedType - } else { - type = Service.self - } - - let key = ServiceKey(serviceType: type, argumentsType: Arguments.self) - - if let entry = services[key] { - resolvedInstance = resolve(entry: entry, invoker: invoker) - } - - if let resolvedInstance = resolvedInstance { - return resolvedInstance - } else { - fatalError("You need to register concrete type for \(Service.self)") - } - } - - private func resolve(entry: ServiceEntryProtocol, invoker: (Factory) -> Any) -> Service? { - let resolvedInstance = invoker(entry.factory as! Factory) - return resolvedInstance as? Service - } -} - diff --git a/WireDomain/Sources/WireDomain/NotificationService/NotificationPayload.swift b/WireDomain/Sources/WireDomain/NotificationService/NotificationPayload.swift index d8f3a2ece75..d303532cb66 100644 --- a/WireDomain/Sources/WireDomain/NotificationService/NotificationPayload.swift +++ b/WireDomain/Sources/WireDomain/NotificationService/NotificationPayload.swift @@ -18,6 +18,7 @@ import Foundation +/// Push notification payload struct NotificationPayload { let userID: UUID let eventID: UUID diff --git a/WireDomain/Sources/WireDomain/NotificationService/NotificationService.swift b/WireDomain/Sources/WireDomain/NotificationService/NotificationService.swift index 7617adf60d2..cdead2fab69 100644 --- a/WireDomain/Sources/WireDomain/NotificationService/NotificationService.swift +++ b/WireDomain/Sources/WireDomain/NotificationService/NotificationService.swift @@ -20,6 +20,7 @@ import UserNotifications import WireDataModel import WireLogging +/// Receives push notifications, process them and generate new notifications from the pending events. final class NotificationService: UNNotificationServiceExtension { // MARK: - Failure @@ -30,10 +31,10 @@ final class NotificationService: UNNotificationServiceExtension { // MARK: - Properties - private let injector = Injector.shared private let logger = WireLogger.notifications private var notificationSession: NotificationSession? private var contentHandler: ((UNNotificationContent) -> Void)? + private var ongoingTask: Task? // MARK: - Object lifecycle @@ -48,7 +49,8 @@ final class NotificationService: UNNotificationServiceExtension { _ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void ) { - let cookieStorage: ZMPersistentCookieStorage = injector.resolve() + ongoingTask?.cancel() + let cookieStorage: ZMPersistentCookieStorage = Injector.resolve() let isAuthenticated = cookieStorage.isAuthenticated guard isAuthenticated else { @@ -61,7 +63,7 @@ final class NotificationService: UNNotificationServiceExtension { self.contentHandler = contentHandler - Task { + ongoingTask = Task { do { let notificationUserInfo = request.content.userInfo @@ -71,7 +73,7 @@ final class NotificationService: UNNotificationServiceExtension { notificationSession = try await createNotificationSession( userID: notification.userID - ) + ) try await notificationSession?.processPushNotification( eventID: notification.eventID @@ -92,7 +94,7 @@ final class NotificationService: UNNotificationServiceExtension { private func createNotificationSession( userID: UUID ) async throws -> NotificationSession { - let userLocalStore: UserLocalStoreProtocol = injector.resolve() + let userLocalStore: UserLocalStoreProtocol = Injector.resolve() let selfUserInfo = await userLocalStore.selfUserInfo() guard let selfClientID = selfUserInfo.clientId else { @@ -102,20 +104,25 @@ final class NotificationService: UNNotificationServiceExtension { let updateEventsRepository = UpdateEventsRepository( userID: userID, selfClientID: selfClientID, - updateEventsAPI: injector.resolve(), - pushChannel: injector.resolve(), - updateEventDecryptor: injector.resolve(), - updateEventsLocalStore: injector.resolve() + updateEventsAPI: Injector.resolve(), // these were already initialized in the assembly, resolving them + pushChannel: Injector.resolve(), + updateEventDecryptor: Injector.resolve(), + updateEventsLocalStore: Injector.resolve() ) let notificationSession = NotificationSession( - userID: userID, updateEventsRepository: updateEventsRepository - ) + ) { [weak self] notificationContent in + self?.finishWithNotification(content: notificationContent) + } return notificationSession } + private func finishWithNotification(content: UNNotificationContent) { + // TODO: [WPB-11175] + } + private func finishWithEmptyNotification() { logger.info("finishing without showing notification") let emptyNotification = UNNotificationContent() @@ -165,5 +172,6 @@ final class NotificationService: UNNotificationServiceExtension { // Content handler should only be consumed once. contentHandler = nil notificationSession = nil + ongoingTask = nil } } diff --git a/WireDomain/Sources/WireDomain/NotificationService/NotificationSession.swift b/WireDomain/Sources/WireDomain/NotificationService/NotificationSession.swift index cf9f85325b0..180be379f18 100644 --- a/WireDomain/Sources/WireDomain/NotificationService/NotificationSession.swift +++ b/WireDomain/Sources/WireDomain/NotificationService/NotificationSession.swift @@ -31,26 +31,22 @@ final class NotificationSession { // MARK: - Properties private let updateEventsRepository: any UpdateEventsRepositoryProtocol - private let userID: UUID private var subscription: AnyCancellable? // MARK: - Object lifecycle init( - userID: UUID, - updateEventsRepository: any UpdateEventsRepositoryProtocol + updateEventsRepository: any UpdateEventsRepositoryProtocol, + onNotificationContent: @escaping (UNNotificationContent) -> Void ) { self.updateEventsRepository = updateEventsRepository - self.userID = userID - - // Generate notification for current pending events as we receive them. - subscription = updateEventsRepository.observePendingEvents() - .sink { [weak self] updateEvents in - self?.generateNotification(for: updateEvents) - } + self.subscription = self.updateEventsRepository.observePendingEvents() + .map(generateNotificationContent) + .sink(receiveValue: onNotificationContent) } deinit { + subscription?.cancel() subscription = nil } @@ -73,7 +69,9 @@ final class NotificationSession { } } - private func generateNotification(for events: [UpdateEvent]) { + private func generateNotificationContent( + for events: [UpdateEvent] + ) -> UNNotificationContent { // TODO: [WPB-11175] - Generate UNNotificationContent from update events for event in events { switch event { @@ -91,5 +89,7 @@ final class NotificationSession { break } } + + return UNNotificationContent() } } diff --git a/WireDomain/Sources/WireDomain/Repositories/UpdateEvents/UpdateEventsRepository.swift b/WireDomain/Sources/WireDomain/Repositories/UpdateEvents/UpdateEventsRepository.swift index 6096c57498a..1c9be739521 100644 --- a/WireDomain/Sources/WireDomain/Repositories/UpdateEvents/UpdateEventsRepository.swift +++ b/WireDomain/Sources/WireDomain/Repositories/UpdateEvents/UpdateEventsRepository.swift @@ -23,10 +23,12 @@ import WireFoundation import WireLogging import Combine -// sourcery: AutoMockable /// Access update events. protocol UpdateEventsRepositoryProtocol { + /// Observe a pending events stream. + /// - Returns: A publisher of update events. + func observePendingEvents() -> AnyPublisher<[UpdateEvent], Never> /// Pull pending events from the server, decrypt if needed, and store locally. diff --git a/WireDomain/Sources/WireDomainSupport/Sourcery/generated/AutoMockable.generated.swift b/WireDomain/Sources/WireDomainSupport/Sourcery/generated/AutoMockable.generated.swift index 9452a00a877..caa99bc8c1f 100644 --- a/WireDomain/Sources/WireDomainSupport/Sourcery/generated/AutoMockable.generated.swift +++ b/WireDomain/Sources/WireDomainSupport/Sourcery/generated/AutoMockable.generated.swift @@ -1746,150 +1746,6 @@ class MockUpdateEventsLocalStoreProtocol: UpdateEventsLocalStoreProtocol { } -class MockUpdateEventsRepositoryProtocol: UpdateEventsRepositoryProtocol { - - // MARK: - Life cycle - - - - // MARK: - pullPendingEvents - - var pullPendingEvents_Invocations: [Void] = [] - var pullPendingEvents_MockError: Error? - var pullPendingEvents_MockMethod: (() async throws -> Void)? - - func pullPendingEvents() async throws { - pullPendingEvents_Invocations.append(()) - - if let error = pullPendingEvents_MockError { - throw error - } - - guard let mock = pullPendingEvents_MockMethod else { - fatalError("no mock for `pullPendingEvents`") - } - - try await mock() - } - - // MARK: - fetchNextPendingEvents - - var fetchNextPendingEventsLimit_Invocations: [UInt] = [] - var fetchNextPendingEventsLimit_MockError: Error? - var fetchNextPendingEventsLimit_MockMethod: ((UInt) async throws -> [UpdateEventEnvelope])? - var fetchNextPendingEventsLimit_MockValue: [UpdateEventEnvelope]? - - func fetchNextPendingEvents(limit: UInt) async throws -> [UpdateEventEnvelope] { - fetchNextPendingEventsLimit_Invocations.append(limit) - - if let error = fetchNextPendingEventsLimit_MockError { - throw error - } - - if let mock = fetchNextPendingEventsLimit_MockMethod { - return try await mock(limit) - } else if let mock = fetchNextPendingEventsLimit_MockValue { - return mock - } else { - fatalError("no mock for `fetchNextPendingEventsLimit`") - } - } - - // MARK: - deleteNextPendingEvents - - var deleteNextPendingEventsLimit_Invocations: [UInt] = [] - var deleteNextPendingEventsLimit_MockError: Error? - var deleteNextPendingEventsLimit_MockMethod: ((UInt) async throws -> Void)? - - func deleteNextPendingEvents(limit: UInt) async throws { - deleteNextPendingEventsLimit_Invocations.append(limit) - - if let error = deleteNextPendingEventsLimit_MockError { - throw error - } - - guard let mock = deleteNextPendingEventsLimit_MockMethod else { - fatalError("no mock for `deleteNextPendingEventsLimit`") - } - - try await mock(limit) - } - - // MARK: - startBufferingLiveEvents - - var startBufferingLiveEvents_Invocations: [Void] = [] - var startBufferingLiveEvents_MockError: Error? - var startBufferingLiveEvents_MockMethod: (() async throws -> AsyncThrowingStream)? - var startBufferingLiveEvents_MockValue: AsyncThrowingStream? - - func startBufferingLiveEvents() async throws -> AsyncThrowingStream { - startBufferingLiveEvents_Invocations.append(()) - - if let error = startBufferingLiveEvents_MockError { - throw error - } - - if let mock = startBufferingLiveEvents_MockMethod { - return try await mock() - } else if let mock = startBufferingLiveEvents_MockValue { - return mock - } else { - fatalError("no mock for `startBufferingLiveEvents`") - } - } - - // MARK: - stopReceivingLiveEvents - - var stopReceivingLiveEvents_Invocations: [Void] = [] - var stopReceivingLiveEvents_MockMethod: (() async -> Void)? - - func stopReceivingLiveEvents() async { - stopReceivingLiveEvents_Invocations.append(()) - - guard let mock = stopReceivingLiveEvents_MockMethod else { - fatalError("no mock for `stopReceivingLiveEvents`") - } - - await mock() - } - - // MARK: - storeLastEventEnvelopeID - - var storeLastEventEnvelopeID_Invocations: [UUID] = [] - var storeLastEventEnvelopeID_MockMethod: ((UUID) -> Void)? - - func storeLastEventEnvelopeID(_ id: UUID) { - storeLastEventEnvelopeID_Invocations.append(id) - - guard let mock = storeLastEventEnvelopeID_MockMethod else { - fatalError("no mock for `storeLastEventEnvelopeID`") - } - - mock(id) - } - - // MARK: - pullLastEventID - - var pullLastEventID_Invocations: [Void] = [] - var pullLastEventID_MockError: Error? - var pullLastEventID_MockMethod: (() async throws -> Void)? - - func pullLastEventID() async throws { - pullLastEventID_Invocations.append(()) - - if let error = pullLastEventID_MockError { - throw error - } - - guard let mock = pullLastEventID_MockMethod else { - fatalError("no mock for `pullLastEventID`") - } - - try await mock() - } - -} - public class MockUserClientsLocalStoreProtocol: UserClientsLocalStoreProtocol { // MARK: - Life cycle @@ -2361,6 +2217,24 @@ public class MockUserLocalStoreProtocol: UserLocalStoreProtocol { } } + // MARK: - selfUserInfo + + public var selfUserInfo_Invocations: [Void] = [] + public var selfUserInfo_MockMethod: (() async -> (id: UUID, clientId: String?))? + public var selfUserInfo_MockValue: (id: UUID, clientId: String?)? + + public func selfUserInfo() async -> (id: UUID, clientId: String?) { + selfUserInfo_Invocations.append(()) + + if let mock = selfUserInfo_MockMethod { + return await mock() + } else if let mock = selfUserInfo_MockValue { + return mock + } else { + fatalError("no mock for `selfUserInfo`") + } + } + } public class MockUserRepositoryProtocol: UserRepositoryProtocol { @@ -2670,6 +2544,24 @@ public class MockUserRepositoryProtocol: UserRepositoryProtocol { } } + // MARK: - selfUserInfo + + public var selfUserInfo_Invocations: [Void] = [] + public var selfUserInfo_MockMethod: (() async -> (id: UUID, clientId: String?))? + public var selfUserInfo_MockValue: (id: UUID, clientId: String?)? + + public func selfUserInfo() async -> (id: UUID, clientId: String?) { + selfUserInfo_Invocations.append(()) + + if let mock = selfUserInfo_MockMethod { + return await mock() + } else if let mock = selfUserInfo_MockValue { + return mock + } else { + fatalError("no mock for `selfUserInfo`") + } + } + } // swiftlint:enable variable_name diff --git a/WireDomain/Tests/WireDomainTests/Dependency Injection/InjectorTests.swift b/WireDomain/Tests/WireDomainTests/Dependency Injection/InjectorTests.swift new file mode 100644 index 00000000000..f44e4fb95a6 --- /dev/null +++ b/WireDomain/Tests/WireDomainTests/Dependency Injection/InjectorTests.swift @@ -0,0 +1,93 @@ +// +// Wire +// Copyright (C) 2024 Wire Swiss GmbH +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see http://www.gnu.org/licenses/. +// + +import XCTest +import WireDataModel +import WireDataModelSupport +import WireAPISupport +import WireAPI +@testable import WireDomain +@testable import WireDomainSupport + +final class InjectorTests: XCTestCase { + + private var stack: CoreDataStack! + private var coreDataStackHelper: CoreDataStackHelper! + + var context: NSManagedObjectContext { + stack.syncContext + } + + override func setUp() async throws { + coreDataStackHelper = CoreDataStackHelper() + stack = try await coreDataStackHelper.createStack() + } + + override func tearDown() async throws { + stack = nil + try coreDataStackHelper.cleanupDirectory() + coreDataStackHelper = nil + } + + func testRegisterAlreadyInitialized_Service_It_Resolves_The_Service() throws { + + // Given, an already initialized service + let updateEventsRepository = UpdateEventsRepository( + userID: .mockID1, + selfClientID: UUID.mockID2.uuidString, + updateEventsAPI: MockUpdateEventsAPI(), + pushChannel: MockPushChannelProtocol(), + updateEventDecryptor: MockUpdateEventDecryptorProtocol(), + updateEventsLocalStore: MockUpdateEventsLocalStoreProtocol() + ) + + // When, registering the service + Injector.register(UpdateEventsRepositoryProtocol.self) { + updateEventsRepository + } + + // Then, the instance is resolved + let _: UpdateEventsRepositoryProtocol = Injector.resolve() + } + + func testRegisterService_It_Resolves_The_Service_On_The_Fly() throws { + + // Given, a registered service not yet initialized + Injector.register(UpdateEventsRepositoryProtocol.self) { userID, selfClientID, updateEventsAPI, pushChannel, updateEventDecryptor, updateEventsLocalStore in + UpdateEventsRepository( + userID: userID, + selfClientID: selfClientID, + updateEventsAPI: updateEventsAPI, + pushChannel: pushChannel, + updateEventDecryptor: updateEventDecryptor, + updateEventsLocalStore: updateEventsLocalStore + ) + } + + // When, setting up the service dependencies + let mockUpdateEventsAPI: UpdateEventsAPI = MockUpdateEventsAPI() + let mockPushChannel: PushChannelProtocol = MockPushChannelProtocol() + let mockUpdateEventDecryptor: UpdateEventDecryptorProtocol = MockUpdateEventDecryptorProtocol() + let mockUpdateEventsLocalStore: UpdateEventsLocalStoreProtocol = MockUpdateEventsLocalStoreProtocol() + + // Then, it resolves the service on the fly with the provided dependencies + let _: UpdateEventsRepositoryProtocol = Injector.resolve( + arguments: UUID.mockID1, UUID.mockID2.uuidString, mockUpdateEventsAPI, mockPushChannel, mockUpdateEventDecryptor, mockUpdateEventsLocalStore + ) + } +} diff --git a/WireDomain/Tests/WireDomainTests/Repositories/Mock/MockUpdateEventsRepository.swift b/WireDomain/Tests/WireDomainTests/Repositories/Mock/MockUpdateEventsRepository.swift new file mode 100644 index 00000000000..cbd112db151 --- /dev/null +++ b/WireDomain/Tests/WireDomainTests/Repositories/Mock/MockUpdateEventsRepository.swift @@ -0,0 +1,202 @@ +// +// Wire +// Copyright (C) 2024 Wire Swiss GmbH +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see http://www.gnu.org/licenses/. +// + +import Combine +import WireAPI +import Foundation +@testable import WireDomain + +class MockUpdateEventsRepositoryProtocol: UpdateEventsRepositoryProtocol { + + // MARK: - Life cycle + + + + // MARK: - observePendingEvents + + var observePendingEvents_Invocations: [Void] = [] + var observePendingEvents_MockMethod: (() -> AnyPublisher<[UpdateEvent], Never>)? + var observePendingEvents_MockValue: AnyPublisher<[UpdateEvent], Never>? + + func observePendingEvents() -> AnyPublisher<[UpdateEvent], Never> { + observePendingEvents_Invocations.append(()) + + if let mock = observePendingEvents_MockMethod { + return mock() + } else if let mock = observePendingEvents_MockValue { + return mock + } else { + fatalError("no mock for `observePendingEvents`") + } + } + + // MARK: - pullPendingEvents + + var pullPendingEvents_Invocations: [Void] = [] + var pullPendingEvents_MockError: Error? + var pullPendingEvents_MockMethod: (() async throws -> Void)? + + func pullPendingEvents() async throws { + pullPendingEvents_Invocations.append(()) + + if let error = pullPendingEvents_MockError { + throw error + } + + guard let mock = pullPendingEvents_MockMethod else { + fatalError("no mock for `pullPendingEvents`") + } + + try await mock() + } + + // MARK: - fetchNextPendingEvents + + var fetchNextPendingEventsLimit_Invocations: [UInt] = [] + var fetchNextPendingEventsLimit_MockError: Error? + var fetchNextPendingEventsLimit_MockMethod: ((UInt) async throws -> [UpdateEventEnvelope])? + var fetchNextPendingEventsLimit_MockValue: [UpdateEventEnvelope]? + + func fetchNextPendingEvents(limit: UInt) async throws -> [UpdateEventEnvelope] { + fetchNextPendingEventsLimit_Invocations.append(limit) + + if let error = fetchNextPendingEventsLimit_MockError { + throw error + } + + if let mock = fetchNextPendingEventsLimit_MockMethod { + return try await mock(limit) + } else if let mock = fetchNextPendingEventsLimit_MockValue { + return mock + } else { + fatalError("no mock for `fetchNextPendingEventsLimit`") + } + } + + // MARK: - deleteNextPendingEvents + + var deleteNextPendingEventsLimit_Invocations: [UInt] = [] + var deleteNextPendingEventsLimit_MockError: Error? + var deleteNextPendingEventsLimit_MockMethod: ((UInt) async throws -> Void)? + + func deleteNextPendingEvents(limit: UInt) async throws { + deleteNextPendingEventsLimit_Invocations.append(limit) + + if let error = deleteNextPendingEventsLimit_MockError { + throw error + } + + guard let mock = deleteNextPendingEventsLimit_MockMethod else { + fatalError("no mock for `deleteNextPendingEventsLimit`") + } + + try await mock(limit) + } + + // MARK: - startBufferingLiveEvents + + var startBufferingLiveEvents_Invocations: [Void] = [] + var startBufferingLiveEvents_MockError: Error? + var startBufferingLiveEvents_MockMethod: (() async throws -> AsyncThrowingStream)? + var startBufferingLiveEvents_MockValue: AsyncThrowingStream? + + func startBufferingLiveEvents() async throws -> AsyncThrowingStream { + startBufferingLiveEvents_Invocations.append(()) + + if let error = startBufferingLiveEvents_MockError { + throw error + } + + if let mock = startBufferingLiveEvents_MockMethod { + return try await mock() + } else if let mock = startBufferingLiveEvents_MockValue { + return mock + } else { + fatalError("no mock for `startBufferingLiveEvents`") + } + } + + // MARK: - stopReceivingLiveEvents + + var stopReceivingLiveEvents_Invocations: [Void] = [] + var stopReceivingLiveEvents_MockMethod: (() async -> Void)? + + func stopReceivingLiveEvents() async { + stopReceivingLiveEvents_Invocations.append(()) + + guard let mock = stopReceivingLiveEvents_MockMethod else { + fatalError("no mock for `stopReceivingLiveEvents`") + } + + await mock() + } + + // MARK: - storeLastEventEnvelopeID + + var storeLastEventEnvelopeID_Invocations: [UUID] = [] + var storeLastEventEnvelopeID_MockMethod: ((UUID) -> Void)? + + func storeLastEventEnvelopeID(_ id: UUID) { + storeLastEventEnvelopeID_Invocations.append(id) + + guard let mock = storeLastEventEnvelopeID_MockMethod else { + fatalError("no mock for `storeLastEventEnvelopeID`") + } + + mock(id) + } + + // MARK: - fetchLastEventEnvelopeID + + var fetchLastEventEnvelopeID_Invocations: [Void] = [] + var fetchLastEventEnvelopeID_MockMethod: (() -> UUID?)? + var fetchLastEventEnvelopeID_MockValue: UUID?? + + func fetchLastEventEnvelopeID() -> UUID? { + fetchLastEventEnvelopeID_Invocations.append(()) + + if let mock = fetchLastEventEnvelopeID_MockMethod { + return mock() + } else if let mock = fetchLastEventEnvelopeID_MockValue { + return mock + } else { + fatalError("no mock for `fetchLastEventEnvelopeID`") + } + } + + // MARK: - pullLastEventID + + var pullLastEventID_Invocations: [Void] = [] + var pullLastEventID_MockError: Error? + var pullLastEventID_MockMethod: (() async throws -> Void)? + + func pullLastEventID() async throws { + pullLastEventID_Invocations.append(()) + + if let error = pullLastEventID_MockError { + throw error + } + + guard let mock = pullLastEventID_MockMethod else { + fatalError("no mock for `pullLastEventID`") + } + + try await mock() + } + +} From c43bad5efbdda5d7c2924d2a40fc4e52d44e1266 Mon Sep 17 00:00:00 2001 From: Jullian Mercier <31648126+jullianm@users.noreply.github.com> Date: Fri, 13 Dec 2024 10:52:43 +0100 Subject: [PATCH 04/17] add UTs --- .../NotificationService.swift | 5 +- .../NotificationSession.swift | 9 +- .../NotificationSessionTests.swift | 114 ++++++++++++++++++ 3 files changed, 122 insertions(+), 6 deletions(-) create mode 100644 WireDomain/Tests/WireDomainTests/Notifications/NotificationSessionTests.swift diff --git a/WireDomain/Sources/WireDomain/NotificationService/NotificationService.swift b/WireDomain/Sources/WireDomain/NotificationService/NotificationService.swift index cdead2fab69..dbec3ea03f1 100644 --- a/WireDomain/Sources/WireDomain/NotificationService/NotificationService.swift +++ b/WireDomain/Sources/WireDomain/NotificationService/NotificationService.swift @@ -20,7 +20,7 @@ import UserNotifications import WireDataModel import WireLogging -/// Receives push notifications, process them and generate new notifications from the pending events. +/// Receives push notifications, process the pending events through the `NotificationSession` to generate a notification content based on these events. final class NotificationService: UNNotificationServiceExtension { // MARK: - Failure @@ -104,7 +104,8 @@ final class NotificationService: UNNotificationServiceExtension { let updateEventsRepository = UpdateEventsRepository( userID: userID, selfClientID: selfClientID, - updateEventsAPI: Injector.resolve(), // these were already initialized in the assembly, resolving them + // these were already initialized, resolving them + updateEventsAPI: Injector.resolve(), pushChannel: Injector.resolve(), updateEventDecryptor: Injector.resolve(), updateEventsLocalStore: Injector.resolve() diff --git a/WireDomain/Sources/WireDomain/NotificationService/NotificationSession.swift b/WireDomain/Sources/WireDomain/NotificationService/NotificationSession.swift index 180be379f18..5ec241418af 100644 --- a/WireDomain/Sources/WireDomain/NotificationService/NotificationSession.swift +++ b/WireDomain/Sources/WireDomain/NotificationService/NotificationSession.swift @@ -20,6 +20,7 @@ import WireAPI import WireDataModel import Combine +/// Observes pending events, process them and generates new notifications content. final class NotificationSession { // MARK: - Failure @@ -37,10 +38,10 @@ final class NotificationSession { init( updateEventsRepository: any UpdateEventsRepositoryProtocol, - onNotificationContent: @escaping (UNNotificationContent) -> Void + onNotificationContent: @escaping (UNMutableNotificationContent) -> Void ) { self.updateEventsRepository = updateEventsRepository - self.subscription = self.updateEventsRepository.observePendingEvents() + self.subscription = updateEventsRepository.observePendingEvents() .map(generateNotificationContent) .sink(receiveValue: onNotificationContent) } @@ -71,7 +72,7 @@ final class NotificationSession { private func generateNotificationContent( for events: [UpdateEvent] - ) -> UNNotificationContent { + ) -> UNMutableNotificationContent { // TODO: [WPB-11175] - Generate UNNotificationContent from update events for event in events { switch event { @@ -90,6 +91,6 @@ final class NotificationSession { } } - return UNNotificationContent() + return UNMutableNotificationContent() } } diff --git a/WireDomain/Tests/WireDomainTests/Notifications/NotificationSessionTests.swift b/WireDomain/Tests/WireDomainTests/Notifications/NotificationSessionTests.swift new file mode 100644 index 00000000000..a5a065e0d7c --- /dev/null +++ b/WireDomain/Tests/WireDomainTests/Notifications/NotificationSessionTests.swift @@ -0,0 +1,114 @@ +// +// Wire +// Copyright (C) 2024 Wire Swiss GmbH +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see http://www.gnu.org/licenses/. +// + +import XCTest +@testable import WireAPI +import WireAPISupport +@testable import WireDomainSupport +@testable import WireDomain + +final class NotificationSessionTests: XCTestCase { + private var sut: NotificationSession! + + override func tearDown() async throws { + sut = nil + } + + func testNotificationSession_It_Triggers_Callback_When_Pulling_Pending_Events() async throws { + + // Given + + let expectation = XCTestExpectation() + var count = 0 + + let updateEventsAPI = MockUpdateEventsAPI() + updateEventsAPI.getUpdateEventsSelfClientIDSinceEventID_MockValue = .init(fetchPage: { _ in + // 3 events batches + .init( + element: [Scaffolding.updateEventEnvelope], + hasMore: count < 2, + nextStart: .init() + ) + }) + + let updateEventDecryptor = MockUpdateEventDecryptorProtocol() + updateEventDecryptor.decryptEventsIn_MockValue = [ + Scaffolding.mlsMessageUpdateEvent, + Scaffolding.proteusMessageUpdateEvent + ] + + let updateEventsLocalStore = MockUpdateEventsLocalStoreProtocol() + updateEventsLocalStore.lastEventID_MockValue = .mockID1 + updateEventsLocalStore.indexOfLastEventEnvelope_MockValue = 1 + updateEventsLocalStore.persistEventEnvelopeIndex_MockMethod = { _, _ in } + updateEventsLocalStore.storeLastEventIDId_MockMethod = { _ in } + + let updateEventsRepository = UpdateEventsRepository( + userID: .mockID1, + selfClientID: UUID.mockID2.uuidString, + updateEventsAPI: updateEventsAPI, + pushChannel: MockPushChannelProtocol(), + updateEventDecryptor: updateEventDecryptor, + updateEventsLocalStore: updateEventsLocalStore + ) + + sut = NotificationSession( + updateEventsRepository: updateEventsRepository, + onNotificationContent: { _ in + count += 1 + // Then, callback should be called 3 times + if count == 2 { expectation.fulfill() } + } + ) + + // When + + try await updateEventsRepository.pullPendingEvents() + + await fulfillment(of: [expectation]) + + } + + enum Scaffolding { + static let updateEventEnvelope = UpdateEventEnvelope( + id: .mockID1, + events: [mlsMessageUpdateEvent, proteusMessageUpdateEvent], + isTransient: false + ) + + static let mlsMessageUpdateEvent: UpdateEvent = .conversation(.mlsMessageAdd(mlsMessageAddEvent)) + + static let proteusMessageUpdateEvent: UpdateEvent = .conversation(.proteusMessageAdd(proteusMessageAddEvent)) + + static let mlsMessageAddEvent = ConversationMLSMessageAddEvent( + conversationID: ConversationID(uuid: .mockID1, domain: ""), + senderID: UserID(uuid: .mockID2, domain: ""), + subconversation: "subconversation", + message: "message" + ) + static let proteusMessageAddEvent = ConversationProteusMessageAddEvent( + conversationID: ConversationID(uuid: .mockID1, domain: ""), + senderID: UserID(uuid: .mockID2, domain: ""), + timestamp: .now, + message: .ciphertext("foo"), + externalData: .ciphertext("bar"), + messageSenderClientID: "abc123", + messageRecipientClientID: "def456" + ) + } +} From 06193e3f783a212ffeb9e07424c78748a59b44b0 Mon Sep 17 00:00:00 2001 From: Jullian Mercier <31648126+jullianm@users.noreply.github.com> Date: Fri, 13 Dec 2024 12:29:40 +0100 Subject: [PATCH 05/17] add missing logic to collect all events batches before processing them, fix UT --- .../NotificationService/NotificationSession.swift | 2 ++ .../UpdateEvents/UpdateEventsRepository.swift | 6 +++++- .../Notifications/NotificationSessionTests.swift | 13 ++++++++----- 3 files changed, 15 insertions(+), 6 deletions(-) diff --git a/WireDomain/Sources/WireDomain/NotificationService/NotificationSession.swift b/WireDomain/Sources/WireDomain/NotificationService/NotificationSession.swift index 5ec241418af..da16de32acb 100644 --- a/WireDomain/Sources/WireDomain/NotificationService/NotificationSession.swift +++ b/WireDomain/Sources/WireDomain/NotificationService/NotificationSession.swift @@ -42,6 +42,8 @@ final class NotificationSession { ) { self.updateEventsRepository = updateEventsRepository self.subscription = updateEventsRepository.observePendingEvents() + .collect() // Collects all the events batches. + .map { $0.flatMap { $0 } } .map(generateNotificationContent) .sink(receiveValue: onNotificationContent) } diff --git a/WireDomain/Sources/WireDomain/Repositories/UpdateEvents/UpdateEventsRepository.swift b/WireDomain/Sources/WireDomain/Repositories/UpdateEvents/UpdateEventsRepository.swift index 1c9be739521..4543255abba 100644 --- a/WireDomain/Sources/WireDomain/Repositories/UpdateEvents/UpdateEventsRepository.swift +++ b/WireDomain/Sources/WireDomain/Repositories/UpdateEvents/UpdateEventsRepository.swift @@ -107,7 +107,7 @@ final class UpdateEventsRepository: UpdateEventsRepositoryProtocol { private let updateEventsLocalStore: any UpdateEventsLocalStoreProtocol private let encoder = JSONEncoder() private let decoder = JSONDecoder() - private let onDecryptedEvents = PassthroughSubject<[UpdateEvent], Never>() + private var onDecryptedEvents = PassthroughSubject<[UpdateEvent], Never>() // MARK: - Object lifecycle @@ -191,6 +191,10 @@ final class UpdateEventsRepository: UpdateEventsRepositoryProtocol { } } } + + // All events batches are now fetched. + onDecryptedEvents.send(completion: .finished) + onDecryptedEvents = .init() } func pullLastEventID() async throws { diff --git a/WireDomain/Tests/WireDomainTests/Notifications/NotificationSessionTests.swift b/WireDomain/Tests/WireDomainTests/Notifications/NotificationSessionTests.swift index a5a065e0d7c..d1a01da319b 100644 --- a/WireDomain/Tests/WireDomainTests/Notifications/NotificationSessionTests.swift +++ b/WireDomain/Tests/WireDomainTests/Notifications/NotificationSessionTests.swift @@ -38,10 +38,14 @@ final class NotificationSessionTests: XCTestCase { let updateEventsAPI = MockUpdateEventsAPI() updateEventsAPI.getUpdateEventsSelfClientIDSinceEventID_MockValue = .init(fetchPage: { _ in + if count < 3 { + count += 1 + } + // 3 events batches - .init( + return .init( element: [Scaffolding.updateEventEnvelope], - hasMore: count < 2, + hasMore: count < 3, nextStart: .init() ) }) @@ -70,9 +74,8 @@ final class NotificationSessionTests: XCTestCase { sut = NotificationSession( updateEventsRepository: updateEventsRepository, onNotificationContent: { _ in - count += 1 - // Then, callback should be called 3 times - if count == 2 { expectation.fulfill() } + // Then, all 3 events batches have been received + expectation.fulfill() } ) From d23a682d630d0bf5d219b174ca0431c6cd7ed0bd Mon Sep 17 00:00:00 2001 From: Jullian Mercier <31648126+jullianm@users.noreply.github.com> Date: Fri, 13 Dec 2024 12:31:43 +0100 Subject: [PATCH 06/17] fix file header --- .../Dependency Injection/Injector.swift | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/WireDomain/Sources/WireDomain/Dependency Injection/Injector.swift b/WireDomain/Sources/WireDomain/Dependency Injection/Injector.swift index e43b9622b72..624c3025e4f 100644 --- a/WireDomain/Sources/WireDomain/Dependency Injection/Injector.swift +++ b/WireDomain/Sources/WireDomain/Dependency Injection/Injector.swift @@ -1,8 +1,19 @@ // -// Injector.swift +// Wire +// Copyright (C) 2024 Wire Swiss GmbH // +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. // -// Created by Jullian Mercier on 06/05/2022. +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see http://www.gnu.org/licenses/. // import Foundation From 438d5c8cfbba0d5e900b5596698d6e2eb672a4a8 Mon Sep 17 00:00:00 2001 From: Jullian Mercier <31648126+jullianm@users.noreply.github.com> Date: Wed, 18 Dec 2024 17:46:00 +0100 Subject: [PATCH 07/17] lint and format --- WireDomain/Sources/WireDomain/Assembly.swift | 43 ++++++------ .../Dependency Injection/Injector.swift | 70 +++++++++---------- .../Dependency Injection/ServiceEntry.swift | 8 +-- .../Dependency Injection/ServiceKey.swift | 4 +- .../NotificationPayload.swift | 12 ++-- .../NotificationService.swift | 69 +++++++++--------- .../NotificationSession.swift | 40 +++++------ .../UpdateEvents/UpdateEventsRepository.swift | 16 ++--- .../Repositories/User/UserLocalStore.swift | 4 +- .../Repositories/User/UserRepository.swift | 4 +- .../Dependency Injection/InjectorTests.swift | 53 +++++++------- .../NotificationSessionTests.swift | 48 ++++++------- .../Mock/MockUpdateEventsRepository.swift | 4 +- .../Tests/Source/Mocks/MockAPIService.swift | 2 +- 14 files changed, 186 insertions(+), 191 deletions(-) diff --git a/WireDomain/Sources/WireDomain/Assembly.swift b/WireDomain/Sources/WireDomain/Assembly.swift index cbf548b24a2..345df3c4fc7 100644 --- a/WireDomain/Sources/WireDomain/Assembly.swift +++ b/WireDomain/Sources/WireDomain/Assembly.swift @@ -16,12 +16,12 @@ // along with this program. If not, see http://www.gnu.org/licenses/. // -import WireDataModel import WireAPI +import WireDataModel import WireFoundation public final class Assembly { - + private let userID: UUID private let clientID: String private let context: NSManagedObjectContext @@ -31,7 +31,7 @@ public final class Assembly { private let apiVersion: WireAPI.APIVersion private let pushChannel: any PushChannelProtocol private let cookieStorage: ZMPersistentCookieStorage - + init( userID: UUID, clientID: String, @@ -52,65 +52,62 @@ public final class Assembly { self.apiVersion = apiVersion self.pushChannel = pushChannel self.cookieStorage = cookieStorage - - self.registerNotificationServiceDependencies() + + registerNotificationServiceDependencies() } - + // MARK: - API Init - + private lazy var updateEventsAPI = UpdateEventsAPIBuilder( apiService: apiService ).makeAPI(for: apiVersion) - + private lazy var updateEventDecryptor = UpdateEventDecryptor( proteusService: proteusService, context: context ) - + // MARK: - Repositories and local stores Init - + private lazy var userLocalStore = UserLocalStore(context: context) - + private lazy var updateEventsLocalStore = UpdateEventsLocalStore( context: context, userID: userID, sharedUserDefaults: sharedUserDefaults ) - + } extension Assembly { - + /// Register some domain dependencies to be resolved by the `NotificationService`. - /// Since `NotificationService` is not initializable, the injector provides a lightweight dependency injection mechanism to retrieve some already initialized dependencies that the notification service requires. + /// Since `NotificationService` is not initializable, the injector provides a lightweight dependency injection + /// mechanism to retrieve some already initialized dependencies that the notification service requires. private func registerNotificationServiceDependencies() { Injector.register(UserLocalStoreProtocol.self) { self.userLocalStore } - + Injector.register(UpdateEventsAPI.self) { self.updateEventsAPI } - + Injector.register(PushChannelProtocol.self) { self.pushChannel } - + Injector.register(UpdateEventDecryptorProtocol.self) { self.updateEventDecryptor } - + Injector.register(UpdateEventsLocalStoreProtocol.self) { self.updateEventsLocalStore } - + Injector.register(ZMPersistentCookieStorage.self) { self.cookieStorage } } } - - - - diff --git a/WireDomain/Sources/WireDomain/Dependency Injection/Injector.swift b/WireDomain/Sources/WireDomain/Dependency Injection/Injector.swift index 624c3025e4f..3c50935fc70 100644 --- a/WireDomain/Sources/WireDomain/Dependency Injection/Injector.swift +++ b/WireDomain/Sources/WireDomain/Dependency Injection/Injector.swift @@ -24,71 +24,71 @@ protocol OptionalProtocol { extension Optional: OptionalProtocol { public static var wrappedType: Any.Type { - Wrapped.self - } + Wrapped.self + } } -final class Injector { - nonisolated(unsafe) private static var services: [ServiceKey: ServiceEntryProtocol] = [:] - +enum Injector { + private nonisolated(unsafe) static var services: [ServiceKey: ServiceEntryProtocol] = [:] + // MARK: - Register - + static func register( _ serviceType: Service.Type, factory: @escaping () -> Service ) { _register(serviceType, factory: factory) } - + static func register( _ serviceType: Service.Type, factory: @escaping (Arg1) -> Service ) { _register(serviceType, factory: factory) } - + static func register( _ serviceType: Service.Type, factory: @escaping (Arg1, Arg2) -> Service ) { _register(serviceType, factory: factory) } - + static func register( _ serviceType: Service.Type, factory: @escaping (Arg1, Arg2, Arg3) -> Service ) { _register(serviceType, factory: factory) } - + static func register( _ serviceType: Service.Type, factory: @escaping (Arg1, Arg2, Arg3, Arg4) -> Service ) { _register(serviceType, factory: factory) } - + static func register( _ serviceType: Service.Type, factory: @escaping (Arg1, Arg2, Arg3, Arg4, Arg5) -> Service ) { _register(serviceType, factory: factory) } - + static func register( _ serviceType: Service.Type, factory: @escaping (Arg1, Arg2, Arg3, Arg4, Arg5, Arg6) -> Service ) { _register(serviceType, factory: factory) } - + private static func _register( _ serviceType: Service.Type, factory: @escaping (Arguments) -> Any ) { - + let key = ServiceKey(serviceType: Service.self, argumentsType: Arguments.self) - + let entry = ServiceEntry( serviceType: serviceType, argumentsType: Arguments.self, @@ -96,26 +96,25 @@ final class Injector { ) services[key] = entry } - - + // MARK: - Resolve - + static func resolve() -> Service { - typealias FactoryType = ((Void)) -> Any + typealias FactoryType = () -> Any return _genericResolve(serviceType: Service.self) { (factory: FactoryType) in factory(()) } } - + static func resolve( argument: Arg1 ) -> Service { - typealias FactoryType = ((Arg1)) -> Any + typealias FactoryType = (Arg1) -> Any return _genericResolve(serviceType: Service.self) { (factory: FactoryType) in - factory((argument)) + factory(argument) } } - + static func resolve( arguments arg1: Arg1, _ arg2: Arg2 ) -> Service { @@ -124,7 +123,7 @@ final class Injector { factory((arg1, arg2)) } } - + static func resolve( arguments arg1: Arg1, _ arg2: Arg2, _ arg3: Arg3 ) -> Service { @@ -133,7 +132,7 @@ final class Injector { factory((arg1, arg2, arg3)) } } - + static func resolve( arguments arg1: Arg1, _ arg2: Arg2, _ arg3: Arg3, _ arg4: Arg4 ) -> Service { @@ -142,7 +141,7 @@ final class Injector { factory((arg1, arg2, arg3, arg4)) } } - + static func resolve( arguments arg1: Arg1, _ arg2: Arg2, _ arg3: Arg3, _ arg4: Arg4, _ arg5: Arg5 ) -> Service { @@ -151,7 +150,7 @@ final class Injector { factory((arg1, arg2, arg3, arg4, arg5)) } } - + static func resolve( arguments arg1: Arg1, _ arg2: Arg2, _ arg3: Arg3, _ arg4: Arg4, _ arg5: Arg5, _ arg6: Arg6 ) -> Service { @@ -160,33 +159,31 @@ final class Injector { factory((arg1, arg2, arg3, arg4, arg5, arg6)) } } - + static func _genericResolve( serviceType: Service.Type, invoker: @escaping ((Arguments) -> Any) -> Any ) -> Service { var resolvedInstance: Service? - var type: Any.Type - - if let optionalType = Service.self as? OptionalProtocol.Type { - type = optionalType.wrappedType + var type: Any.Type = if let optionalType = Service.self as? OptionalProtocol.Type { + optionalType.wrappedType } else { - type = Service.self + Service.self } - + let key = ServiceKey(serviceType: type, argumentsType: Arguments.self) if let entry = services[key] { resolvedInstance = resolve(entry: entry, invoker: invoker) } - if let resolvedInstance = resolvedInstance { + if let resolvedInstance { return resolvedInstance } else { fatalError("You need to register concrete type for \(Service.self)") } } - + private static func resolve( entry: ServiceEntryProtocol, invoker: (Factory) -> Any @@ -195,4 +192,3 @@ final class Injector { return resolvedInstance as? Service } } - diff --git a/WireDomain/Sources/WireDomain/Dependency Injection/ServiceEntry.swift b/WireDomain/Sources/WireDomain/Dependency Injection/ServiceEntry.swift index 50b5fef05ef..ef9c8948ab5 100644 --- a/WireDomain/Sources/WireDomain/Dependency Injection/ServiceEntry.swift +++ b/WireDomain/Sources/WireDomain/Dependency Injection/ServiceEntry.swift @@ -26,15 +26,15 @@ protocol ServiceEntryProtocol: AnyObject { } final class ServiceEntry: ServiceEntryProtocol { - + // MARK: - Properties - + let serviceType: Any.Type let argumentsType: Any.Type let factory: FunctionType - + // MARK: - Object lifecycle - + init(serviceType: Service.Type, argumentsType: Any.Type, factory: FunctionType) { self.serviceType = serviceType self.argumentsType = argumentsType diff --git a/WireDomain/Sources/WireDomain/Dependency Injection/ServiceKey.swift b/WireDomain/Sources/WireDomain/Dependency Injection/ServiceKey.swift index ff9e950cccb..83a975353b1 100644 --- a/WireDomain/Sources/WireDomain/Dependency Injection/ServiceKey.swift +++ b/WireDomain/Sources/WireDomain/Dependency Injection/ServiceKey.swift @@ -28,8 +28,8 @@ extension ServiceKey: Hashable { ObjectIdentifier(serviceType).hash(into: &hasher) ObjectIdentifier(argumentsType).hash(into: &hasher) } - + static func == (lhs: ServiceKey, rhs: ServiceKey) -> Bool { - return lhs.serviceType == rhs.serviceType && lhs.argumentsType == rhs.argumentsType + lhs.serviceType == rhs.serviceType && lhs.argumentsType == rhs.argumentsType } } diff --git a/WireDomain/Sources/WireDomain/NotificationService/NotificationPayload.swift b/WireDomain/Sources/WireDomain/NotificationService/NotificationPayload.swift index d303532cb66..58039355596 100644 --- a/WireDomain/Sources/WireDomain/NotificationService/NotificationPayload.swift +++ b/WireDomain/Sources/WireDomain/NotificationService/NotificationPayload.swift @@ -22,19 +22,19 @@ import Foundation struct NotificationPayload { let userID: UUID let eventID: UUID - + enum Failure: Error { case missingUserID case missingEventID } - + enum Key: String { case data case user case id } - - init(userInfo: [AnyHashable : Any]) throws { + + init(userInfo: [AnyHashable: Any]) throws { guard let data = userInfo[Key.data.rawValue] as? [String: Any], let userIDString = data[Key.user.rawValue] as? String, @@ -42,14 +42,14 @@ struct NotificationPayload { else { throw Failure.missingUserID } - + guard let innerData = data[Key.data.rawValue] as? [AnyHashable: Any], let eventIDString = innerData[Key.id.rawValue] as? String, let eventID = UUID(uuidString: eventIDString) else { throw Failure.missingEventID } - + self.userID = userID self.eventID = eventID } diff --git a/WireDomain/Sources/WireDomain/NotificationService/NotificationService.swift b/WireDomain/Sources/WireDomain/NotificationService/NotificationService.swift index dbec3ea03f1..b1b6f0b3f19 100644 --- a/WireDomain/Sources/WireDomain/NotificationService/NotificationService.swift +++ b/WireDomain/Sources/WireDomain/NotificationService/NotificationService.swift @@ -20,31 +20,32 @@ import UserNotifications import WireDataModel import WireLogging -/// Receives push notifications, process the pending events through the `NotificationSession` to generate a notification content based on these events. +/// Receives push notifications, process the pending events through the `NotificationSession` to generate a notification +/// content based on these events. final class NotificationService: UNNotificationServiceExtension { - + // MARK: - Failure - + enum Failure: Error { case missingSelfClientID } - + // MARK: - Properties - + private let logger = WireLogger.notifications private var notificationSession: NotificationSession? private var contentHandler: ((UNNotificationContent) -> Void)? private var ongoingTask: Task? - + // MARK: - Object lifecycle - + override init() { logger.info("initializing new legacy notification service") super.init() } - + // MARK: - Notifications - + override func didReceive( _ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void @@ -52,55 +53,55 @@ final class NotificationService: UNNotificationServiceExtension { ongoingTask?.cancel() let cookieStorage: ZMPersistentCookieStorage = Injector.resolve() let isAuthenticated = cookieStorage.isAuthenticated - + guard isAuthenticated else { logger.error( "Not displaying notification because app is not authenticated" ) - + return finishWithEmptyNotification() } - + self.contentHandler = contentHandler - + ongoingTask = Task { do { let notificationUserInfo = request.content.userInfo - + let notification = try NotificationPayload( userInfo: notificationUserInfo ) - + notificationSession = try await createNotificationSession( userID: notification.userID - ) - + ) + try await notificationSession?.processPushNotification( eventID: notification.eventID ) - + } catch { logError(error) finishWithEmptyNotification() } } } - + override func serviceExtensionTimeWillExpire() { logger.warn("legacy service extension will expire") finishWithEmptyNotification() } - + private func createNotificationSession( userID: UUID ) async throws -> NotificationSession { let userLocalStore: UserLocalStoreProtocol = Injector.resolve() let selfUserInfo = await userLocalStore.selfUserInfo() - + guard let selfClientID = selfUserInfo.clientId else { throw Failure.missingSelfClientID } - + let updateEventsRepository = UpdateEventsRepository( userID: userID, selfClientID: selfClientID, @@ -110,50 +111,48 @@ final class NotificationService: UNNotificationServiceExtension { updateEventDecryptor: Injector.resolve(), updateEventsLocalStore: Injector.resolve() ) - - let notificationSession = NotificationSession( + + return NotificationSession( updateEventsRepository: updateEventsRepository ) { [weak self] notificationContent in self?.finishWithNotification(content: notificationContent) } - - return notificationSession } - + private func finishWithNotification(content: UNNotificationContent) { // TODO: [WPB-11175] } - + private func finishWithEmptyNotification() { logger.info("finishing without showing notification") let emptyNotification = UNNotificationContent() contentHandler?(emptyNotification) terminate() } - + private func logError(_ error: any Error) { switch error { case let sessionError as NotificationSession.Failure: switch sessionError { - case .unableToPullPendingEvents(let error): + case let .unableToPullPendingEvents(error): logger.error( "failed to process notification: could not pull pending events: \(error.localizedDescription)" ) } - + case let payloadError as NotificationPayload.Failure: switch payloadError { case .missingUserID: logger.error( "failed to decode notification payload: missing user ID" ) - + case .missingEventID: logger.error( "failed to decode notification payload: missing event ID" ) } - + case let serviceError as NotificationService.Failure: switch serviceError { case .missingSelfClientID: @@ -161,14 +160,14 @@ final class NotificationService: UNNotificationServiceExtension { "failed to create notification session: missing self client ID" ) } - + default: logger.error( "failed to process notification: \(error.localizedDescription)" ) } } - + private func terminate() { // Content handler should only be consumed once. contentHandler = nil diff --git a/WireDomain/Sources/WireDomain/NotificationService/NotificationSession.swift b/WireDomain/Sources/WireDomain/NotificationService/NotificationSession.swift index da16de32acb..5d1874ce303 100644 --- a/WireDomain/Sources/WireDomain/NotificationService/NotificationSession.swift +++ b/WireDomain/Sources/WireDomain/NotificationService/NotificationSession.swift @@ -16,26 +16,26 @@ // along with this program. If not, see http://www.gnu.org/licenses/. // +import Combine import WireAPI import WireDataModel -import Combine /// Observes pending events, process them and generates new notifications content. final class NotificationSession { - + // MARK: - Failure - + enum Failure: Error { case unableToPullPendingEvents(Error) } - + // MARK: - Properties - + private let updateEventsRepository: any UpdateEventsRepositoryProtocol private var subscription: AnyCancellable? - + // MARK: - Object lifecycle - + init( updateEventsRepository: any UpdateEventsRepositoryProtocol, onNotificationContent: @escaping (UNMutableNotificationContent) -> Void @@ -47,52 +47,52 @@ final class NotificationSession { .map(generateNotificationContent) .sink(receiveValue: onNotificationContent) } - + deinit { subscription?.cancel() subscription = nil } - + // MARK: - Notifications - + func processPushNotification( eventID: UUID ) async throws { let newEventID = eventID let lastEventId = updateEventsRepository.fetchLastEventEnvelopeID() - + if lastEventId == nil { updateEventsRepository.storeLastEventEnvelopeID(newEventID) } - + do { try await updateEventsRepository.pullPendingEvents() } catch { throw Failure.unableToPullPendingEvents(error) } } - + private func generateNotificationContent( for events: [UpdateEvent] ) -> UNMutableNotificationContent { // TODO: [WPB-11175] - Generate UNNotificationContent from update events for event in events { switch event { - case .conversation(let conversationEvent): + case let .conversation(conversationEvent): break - case .featureConfig(let featureConfigEvent): + case let .featureConfig(featureConfigEvent): break - case .federation(let federationEvent): + case let .federation(federationEvent): break - case .user(let userEvent): + case let .user(userEvent): break - case .team(let teamEvent): + case let .team(teamEvent): break - case .unknown(let eventType): + case let .unknown(eventType): break } } - + return UNMutableNotificationContent() } } diff --git a/WireDomain/Sources/WireDomain/Repositories/UpdateEvents/UpdateEventsRepository.swift b/WireDomain/Sources/WireDomain/Repositories/UpdateEvents/UpdateEventsRepository.swift index 4543255abba..c38e50b5d17 100644 --- a/WireDomain/Sources/WireDomain/Repositories/UpdateEvents/UpdateEventsRepository.swift +++ b/WireDomain/Sources/WireDomain/Repositories/UpdateEvents/UpdateEventsRepository.swift @@ -16,16 +16,16 @@ // along with this program. If not, see http://www.gnu.org/licenses/. // +import Combine import Foundation import WireAPI import WireDataModel import WireFoundation import WireLogging -import Combine /// Access update events. protocol UpdateEventsRepositoryProtocol { - + /// Observe a pending events stream. /// - Returns: A publisher of update events. @@ -82,11 +82,11 @@ protocol UpdateEventsRepositoryProtocol { /// - Parameter id: The id to store. func storeLastEventEnvelopeID(_ id: UUID) - + /// Fetches the last event envelope id. /// /// - Returns: The last envelope id if any. - + func fetchLastEventEnvelopeID() -> UUID? /// Pulls the last event envelope id and stores it locally. @@ -128,7 +128,7 @@ final class UpdateEventsRepository: UpdateEventsRepositoryProtocol { } // MARK: - Pull pending events - + func observePendingEvents() -> AnyPublisher<[UpdateEvent], Never> { onDecryptedEvents.eraseToAnyPublisher() } @@ -179,7 +179,7 @@ final class UpdateEventsRepository: UpdateEventsRepositoryProtocol { decryptedEnvelopeData, index: currentIndex ) - + onDecryptedEvents.send(decryptedEvents) currentIndex += 1 @@ -191,7 +191,7 @@ final class UpdateEventsRepository: UpdateEventsRepositoryProtocol { } } } - + // All events batches are now fetched. onDecryptedEvents.send(completion: .finished) onDecryptedEvents = .init() @@ -253,7 +253,7 @@ final class UpdateEventsRepository: UpdateEventsRepositoryProtocol { func stopReceivingLiveEvents() async { pushChannel.close() } - + func fetchLastEventEnvelopeID() -> UUID? { updateEventsLocalStore.lastEventID() } diff --git a/WireDomain/Sources/WireDomain/Repositories/User/UserLocalStore.swift b/WireDomain/Sources/WireDomain/Repositories/User/UserLocalStore.swift index b3cb9dc0057..79c40b2ccd3 100644 --- a/WireDomain/Sources/WireDomain/Repositories/User/UserLocalStore.swift +++ b/WireDomain/Sources/WireDomain/Repositories/User/UserLocalStore.swift @@ -127,7 +127,7 @@ public protocol UserLocalStoreProtocol { /// - returns: A list of users' qualified IDs. func fetchAllUserIDsWithOneOnOneConversation() async throws -> [WireDataModel.QualifiedID] - + /// Fetches self user info : user ID and client ID. /// - returns: the user ID and the client ID. @@ -383,7 +383,7 @@ public final class UserLocalStore: UserLocalStoreProtocol { user.isPendingMetadataRefresh = false } } - + public func selfUserInfo() async -> (id: UUID, clientId: String?) { let selfUser = await fetchSelfUser() diff --git a/WireDomain/Sources/WireDomain/Repositories/User/UserRepository.swift b/WireDomain/Sources/WireDomain/Repositories/User/UserRepository.swift index 71b4f1abd95..540aff29897 100644 --- a/WireDomain/Sources/WireDomain/Repositories/User/UserRepository.swift +++ b/WireDomain/Sources/WireDomain/Repositories/User/UserRepository.swift @@ -158,7 +158,7 @@ public protocol UserRepositoryProtocol { /// - returns: A list of users' qualified IDs. func fetchAllUserIDsWithOneOnOneConversation() async throws -> [WireDataModel.QualifiedID] - + /// Fetches self user info : user ID and client ID. /// - returns: the user ID and the client ID. @@ -371,7 +371,7 @@ public final class UserRepository: UserRepositoryProtocol { return isSelfUser } - + public func selfUserInfo() async -> (id: UUID, clientId: String?) { await userLocalStore.selfUserInfo() } diff --git a/WireDomain/Tests/WireDomainTests/Dependency Injection/InjectorTests.swift b/WireDomain/Tests/WireDomainTests/Dependency Injection/InjectorTests.swift index f44e4fb95a6..ce4d8be51b8 100644 --- a/WireDomain/Tests/WireDomainTests/Dependency Injection/InjectorTests.swift +++ b/WireDomain/Tests/WireDomainTests/Dependency Injection/InjectorTests.swift @@ -16,16 +16,16 @@ // along with this program. If not, see http://www.gnu.org/licenses/. // -import XCTest +import WireAPI +import WireAPISupport import WireDataModel import WireDataModelSupport -import WireAPISupport -import WireAPI +import XCTest @testable import WireDomain @testable import WireDomainSupport final class InjectorTests: XCTestCase { - + private var stack: CoreDataStack! private var coreDataStackHelper: CoreDataStackHelper! @@ -37,15 +37,15 @@ final class InjectorTests: XCTestCase { coreDataStackHelper = CoreDataStackHelper() stack = try await coreDataStackHelper.createStack() } - + override func tearDown() async throws { stack = nil try coreDataStackHelper.cleanupDirectory() coreDataStackHelper = nil } - + func testRegisterAlreadyInitialized_Service_It_Resolves_The_Service() throws { - + // Given, an already initialized service let updateEventsRepository = UpdateEventsRepository( userID: .mockID1, @@ -55,39 +55,44 @@ final class InjectorTests: XCTestCase { updateEventDecryptor: MockUpdateEventDecryptorProtocol(), updateEventsLocalStore: MockUpdateEventsLocalStoreProtocol() ) - + // When, registering the service Injector.register(UpdateEventsRepositoryProtocol.self) { updateEventsRepository } - + // Then, the instance is resolved let _: UpdateEventsRepositoryProtocol = Injector.resolve() } - + func testRegisterService_It_Resolves_The_Service_On_The_Fly() throws { - + // Given, a registered service not yet initialized - Injector.register(UpdateEventsRepositoryProtocol.self) { userID, selfClientID, updateEventsAPI, pushChannel, updateEventDecryptor, updateEventsLocalStore in - UpdateEventsRepository( - userID: userID, - selfClientID: selfClientID, - updateEventsAPI: updateEventsAPI, - pushChannel: pushChannel, - updateEventDecryptor: updateEventDecryptor, - updateEventsLocalStore: updateEventsLocalStore - ) - } - + Injector + .register( + UpdateEventsRepositoryProtocol + .self + ) { userID, selfClientID, updateEventsAPI, pushChannel, updateEventDecryptor, updateEventsLocalStore in + UpdateEventsRepository( + userID: userID, + selfClientID: selfClientID, + updateEventsAPI: updateEventsAPI, + pushChannel: pushChannel, + updateEventDecryptor: updateEventDecryptor, + updateEventsLocalStore: updateEventsLocalStore + ) + } + // When, setting up the service dependencies let mockUpdateEventsAPI: UpdateEventsAPI = MockUpdateEventsAPI() let mockPushChannel: PushChannelProtocol = MockPushChannelProtocol() let mockUpdateEventDecryptor: UpdateEventDecryptorProtocol = MockUpdateEventDecryptorProtocol() let mockUpdateEventsLocalStore: UpdateEventsLocalStoreProtocol = MockUpdateEventsLocalStoreProtocol() - + // Then, it resolves the service on the fly with the provided dependencies let _: UpdateEventsRepositoryProtocol = Injector.resolve( - arguments: UUID.mockID1, UUID.mockID2.uuidString, mockUpdateEventsAPI, mockPushChannel, mockUpdateEventDecryptor, mockUpdateEventsLocalStore + arguments: UUID.mockID1, UUID.mockID2.uuidString, mockUpdateEventsAPI, mockPushChannel, + mockUpdateEventDecryptor, mockUpdateEventsLocalStore ) } } diff --git a/WireDomain/Tests/WireDomainTests/Notifications/NotificationSessionTests.swift b/WireDomain/Tests/WireDomainTests/Notifications/NotificationSessionTests.swift index d1a01da319b..21699b7c96d 100644 --- a/WireDomain/Tests/WireDomainTests/Notifications/NotificationSessionTests.swift +++ b/WireDomain/Tests/WireDomainTests/Notifications/NotificationSessionTests.swift @@ -16,11 +16,11 @@ // along with this program. If not, see http://www.gnu.org/licenses/. // +import WireAPISupport import XCTest @testable import WireAPI -import WireAPISupport -@testable import WireDomainSupport @testable import WireDomain +@testable import WireDomainSupport final class NotificationSessionTests: XCTestCase { private var sut: NotificationSession! @@ -28,40 +28,40 @@ final class NotificationSessionTests: XCTestCase { override func tearDown() async throws { sut = nil } - + func testNotificationSession_It_Triggers_Callback_When_Pulling_Pending_Events() async throws { - + // Given - + let expectation = XCTestExpectation() var count = 0 - + let updateEventsAPI = MockUpdateEventsAPI() updateEventsAPI.getUpdateEventsSelfClientIDSinceEventID_MockValue = .init(fetchPage: { _ in if count < 3 { count += 1 } - + // 3 events batches - return .init( - element: [Scaffolding.updateEventEnvelope], - hasMore: count < 3, - nextStart: .init() - ) + return .init( + element: [Scaffolding.updateEventEnvelope], + hasMore: count < 3, + nextStart: .init() + ) }) - + let updateEventDecryptor = MockUpdateEventDecryptorProtocol() updateEventDecryptor.decryptEventsIn_MockValue = [ Scaffolding.mlsMessageUpdateEvent, Scaffolding.proteusMessageUpdateEvent ] - + let updateEventsLocalStore = MockUpdateEventsLocalStoreProtocol() updateEventsLocalStore.lastEventID_MockValue = .mockID1 updateEventsLocalStore.indexOfLastEventEnvelope_MockValue = 1 updateEventsLocalStore.persistEventEnvelopeIndex_MockMethod = { _, _ in } updateEventsLocalStore.storeLastEventIDId_MockMethod = { _ in } - + let updateEventsRepository = UpdateEventsRepository( userID: .mockID1, selfClientID: UUID.mockID2.uuidString, @@ -70,7 +70,7 @@ final class NotificationSessionTests: XCTestCase { updateEventDecryptor: updateEventDecryptor, updateEventsLocalStore: updateEventsLocalStore ) - + sut = NotificationSession( updateEventsRepository: updateEventsRepository, onNotificationContent: { _ in @@ -78,26 +78,26 @@ final class NotificationSessionTests: XCTestCase { expectation.fulfill() } ) - + // When - + try await updateEventsRepository.pullPendingEvents() - + await fulfillment(of: [expectation]) - + } - + enum Scaffolding { static let updateEventEnvelope = UpdateEventEnvelope( id: .mockID1, events: [mlsMessageUpdateEvent, proteusMessageUpdateEvent], isTransient: false ) - + static let mlsMessageUpdateEvent: UpdateEvent = .conversation(.mlsMessageAdd(mlsMessageAddEvent)) - + static let proteusMessageUpdateEvent: UpdateEvent = .conversation(.proteusMessageAdd(proteusMessageAddEvent)) - + static let mlsMessageAddEvent = ConversationMLSMessageAddEvent( conversationID: ConversationID(uuid: .mockID1, domain: ""), senderID: UserID(uuid: .mockID2, domain: ""), diff --git a/WireDomain/Tests/WireDomainTests/Repositories/Mock/MockUpdateEventsRepository.swift b/WireDomain/Tests/WireDomainTests/Repositories/Mock/MockUpdateEventsRepository.swift index cbd112db151..4cc7aa6ea62 100644 --- a/WireDomain/Tests/WireDomainTests/Repositories/Mock/MockUpdateEventsRepository.swift +++ b/WireDomain/Tests/WireDomainTests/Repositories/Mock/MockUpdateEventsRepository.swift @@ -17,16 +17,14 @@ // import Combine -import WireAPI import Foundation +import WireAPI @testable import WireDomain class MockUpdateEventsRepositoryProtocol: UpdateEventsRepositoryProtocol { // MARK: - Life cycle - - // MARK: - observePendingEvents var observePendingEvents_Invocations: [Void] = [] diff --git a/wire-ios-sync-engine/Tests/Source/Mocks/MockAPIService.swift b/wire-ios-sync-engine/Tests/Source/Mocks/MockAPIService.swift index 502da268f27..3e2298d4447 100644 --- a/wire-ios-sync-engine/Tests/Source/Mocks/MockAPIService.swift +++ b/wire-ios-sync-engine/Tests/Source/Mocks/MockAPIService.swift @@ -32,6 +32,6 @@ public class MockAPIService: APIServiceProtocol { _ request: URLRequest, requiringAccessToken: Bool ) async throws -> (Data, HTTPURLResponse) { - return try await requestHandler?(request, requiringAccessToken) ?? (Data(), HTTPURLResponse()) + try await requestHandler?(request, requiringAccessToken) ?? (Data(), HTTPURLResponse()) } } From e36dd0becf06b8ea7021bebce7f1d2f60b90384e Mon Sep 17 00:00:00 2001 From: Jullian Mercier <31648126+jullianm@users.noreply.github.com> Date: Fri, 3 Jan 2025 15:40:52 +0100 Subject: [PATCH 08/17] lint and format --- WireDomain/Sources/WireDomain/Assembly.swift | 2 +- .../Sources/WireDomain/Dependency Injection/Injector.swift | 2 +- .../Sources/WireDomain/Dependency Injection/ServiceEntry.swift | 2 +- .../Sources/WireDomain/Dependency Injection/ServiceKey.swift | 2 +- .../WireDomain/NotificationService/NotificationPayload.swift | 2 +- .../WireDomain/NotificationService/NotificationService.swift | 2 +- .../WireDomain/NotificationService/NotificationSession.swift | 2 +- .../WireDomainTests/Dependency Injection/InjectorTests.swift | 2 +- .../Notifications/NotificationSessionTests.swift | 2 +- .../Repositories/Mock/MockUpdateEventsRepository.swift | 2 +- 10 files changed, 10 insertions(+), 10 deletions(-) diff --git a/WireDomain/Sources/WireDomain/Assembly.swift b/WireDomain/Sources/WireDomain/Assembly.swift index 345df3c4fc7..12e19d08146 100644 --- a/WireDomain/Sources/WireDomain/Assembly.swift +++ b/WireDomain/Sources/WireDomain/Assembly.swift @@ -1,6 +1,6 @@ // // Wire -// Copyright (C) 2024 Wire Swiss GmbH +// Copyright (C) 2025 Wire Swiss GmbH // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by diff --git a/WireDomain/Sources/WireDomain/Dependency Injection/Injector.swift b/WireDomain/Sources/WireDomain/Dependency Injection/Injector.swift index 3c50935fc70..ab35a944bb2 100644 --- a/WireDomain/Sources/WireDomain/Dependency Injection/Injector.swift +++ b/WireDomain/Sources/WireDomain/Dependency Injection/Injector.swift @@ -1,6 +1,6 @@ // // Wire -// Copyright (C) 2024 Wire Swiss GmbH +// Copyright (C) 2025 Wire Swiss GmbH // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by diff --git a/WireDomain/Sources/WireDomain/Dependency Injection/ServiceEntry.swift b/WireDomain/Sources/WireDomain/Dependency Injection/ServiceEntry.swift index ef9c8948ab5..cc0f12587f9 100644 --- a/WireDomain/Sources/WireDomain/Dependency Injection/ServiceEntry.swift +++ b/WireDomain/Sources/WireDomain/Dependency Injection/ServiceEntry.swift @@ -1,6 +1,6 @@ // // Wire -// Copyright (C) 2024 Wire Swiss GmbH +// Copyright (C) 2025 Wire Swiss GmbH // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by diff --git a/WireDomain/Sources/WireDomain/Dependency Injection/ServiceKey.swift b/WireDomain/Sources/WireDomain/Dependency Injection/ServiceKey.swift index 83a975353b1..9f8e969881c 100644 --- a/WireDomain/Sources/WireDomain/Dependency Injection/ServiceKey.swift +++ b/WireDomain/Sources/WireDomain/Dependency Injection/ServiceKey.swift @@ -1,6 +1,6 @@ // // Wire -// Copyright (C) 2024 Wire Swiss GmbH +// Copyright (C) 2025 Wire Swiss GmbH // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by diff --git a/WireDomain/Sources/WireDomain/NotificationService/NotificationPayload.swift b/WireDomain/Sources/WireDomain/NotificationService/NotificationPayload.swift index 58039355596..3abe5a22beb 100644 --- a/WireDomain/Sources/WireDomain/NotificationService/NotificationPayload.swift +++ b/WireDomain/Sources/WireDomain/NotificationService/NotificationPayload.swift @@ -1,6 +1,6 @@ // // Wire -// Copyright (C) 2024 Wire Swiss GmbH +// Copyright (C) 2025 Wire Swiss GmbH // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by diff --git a/WireDomain/Sources/WireDomain/NotificationService/NotificationService.swift b/WireDomain/Sources/WireDomain/NotificationService/NotificationService.swift index b1b6f0b3f19..bb681a87818 100644 --- a/WireDomain/Sources/WireDomain/NotificationService/NotificationService.swift +++ b/WireDomain/Sources/WireDomain/NotificationService/NotificationService.swift @@ -1,6 +1,6 @@ // // Wire -// Copyright (C) 2024 Wire Swiss GmbH +// Copyright (C) 2025 Wire Swiss GmbH // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by diff --git a/WireDomain/Sources/WireDomain/NotificationService/NotificationSession.swift b/WireDomain/Sources/WireDomain/NotificationService/NotificationSession.swift index 5d1874ce303..7760f8c505d 100644 --- a/WireDomain/Sources/WireDomain/NotificationService/NotificationSession.swift +++ b/WireDomain/Sources/WireDomain/NotificationService/NotificationSession.swift @@ -1,6 +1,6 @@ // // Wire -// Copyright (C) 2024 Wire Swiss GmbH +// Copyright (C) 2025 Wire Swiss GmbH // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by diff --git a/WireDomain/Tests/WireDomainTests/Dependency Injection/InjectorTests.swift b/WireDomain/Tests/WireDomainTests/Dependency Injection/InjectorTests.swift index ce4d8be51b8..546f866a602 100644 --- a/WireDomain/Tests/WireDomainTests/Dependency Injection/InjectorTests.swift +++ b/WireDomain/Tests/WireDomainTests/Dependency Injection/InjectorTests.swift @@ -1,6 +1,6 @@ // // Wire -// Copyright (C) 2024 Wire Swiss GmbH +// Copyright (C) 2025 Wire Swiss GmbH // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by diff --git a/WireDomain/Tests/WireDomainTests/Notifications/NotificationSessionTests.swift b/WireDomain/Tests/WireDomainTests/Notifications/NotificationSessionTests.swift index 21699b7c96d..9e6260bc077 100644 --- a/WireDomain/Tests/WireDomainTests/Notifications/NotificationSessionTests.swift +++ b/WireDomain/Tests/WireDomainTests/Notifications/NotificationSessionTests.swift @@ -1,6 +1,6 @@ // // Wire -// Copyright (C) 2024 Wire Swiss GmbH +// Copyright (C) 2025 Wire Swiss GmbH // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by diff --git a/WireDomain/Tests/WireDomainTests/Repositories/Mock/MockUpdateEventsRepository.swift b/WireDomain/Tests/WireDomainTests/Repositories/Mock/MockUpdateEventsRepository.swift index 4cc7aa6ea62..c483ad7ef71 100644 --- a/WireDomain/Tests/WireDomainTests/Repositories/Mock/MockUpdateEventsRepository.swift +++ b/WireDomain/Tests/WireDomainTests/Repositories/Mock/MockUpdateEventsRepository.swift @@ -1,6 +1,6 @@ // // Wire -// Copyright (C) 2024 Wire Swiss GmbH +// Copyright (C) 2025 Wire Swiss GmbH // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by From 56aa464ffc10b6833db2607aa182fca8edabe0ac Mon Sep 17 00:00:00 2001 From: Jullian Mercier <31648126+jullianm@users.noreply.github.com> Date: Fri, 3 Jan 2025 17:04:56 +0100 Subject: [PATCH 09/17] Update WireDomain/Sources/WireDomain/NotificationService/NotificationService.swift MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: François Benaiteau --- .../WireDomain/NotificationService/NotificationService.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WireDomain/Sources/WireDomain/NotificationService/NotificationService.swift b/WireDomain/Sources/WireDomain/NotificationService/NotificationService.swift index bb681a87818..b1c55568683 100644 --- a/WireDomain/Sources/WireDomain/NotificationService/NotificationService.swift +++ b/WireDomain/Sources/WireDomain/NotificationService/NotificationService.swift @@ -40,7 +40,7 @@ final class NotificationService: UNNotificationServiceExtension { // MARK: - Object lifecycle override init() { - logger.info("initializing new legacy notification service") + logger.info("initializing notification service") super.init() } From e8c164f899036e8a46a2d1fb0b9d83d9c679ce9c Mon Sep 17 00:00:00 2001 From: Jullian Mercier <31648126+jullianm@users.noreply.github.com> Date: Fri, 3 Jan 2025 17:05:05 +0100 Subject: [PATCH 10/17] Update WireDomain/Sources/WireDomain/NotificationService/NotificationService.swift MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: François Benaiteau --- .../WireDomain/NotificationService/NotificationService.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WireDomain/Sources/WireDomain/NotificationService/NotificationService.swift b/WireDomain/Sources/WireDomain/NotificationService/NotificationService.swift index b1c55568683..363b745ab90 100644 --- a/WireDomain/Sources/WireDomain/NotificationService/NotificationService.swift +++ b/WireDomain/Sources/WireDomain/NotificationService/NotificationService.swift @@ -35,7 +35,7 @@ final class NotificationService: UNNotificationServiceExtension { private let logger = WireLogger.notifications private var notificationSession: NotificationSession? private var contentHandler: ((UNNotificationContent) -> Void)? - private var ongoingTask: Task? + private var onGoingTask: Task? // MARK: - Object lifecycle From 081543b94d5fdb95594fde4a41a83242323e8606 Mon Sep 17 00:00:00 2001 From: Jullian Mercier <31648126+jullianm@users.noreply.github.com> Date: Fri, 3 Jan 2025 17:05:27 +0100 Subject: [PATCH 11/17] Update WireDomain/Sources/WireDomain/NotificationService/NotificationService.swift MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: François Benaiteau --- .../WireDomain/NotificationService/NotificationService.swift | 3 +++ 1 file changed, 3 insertions(+) diff --git a/WireDomain/Sources/WireDomain/NotificationService/NotificationService.swift b/WireDomain/Sources/WireDomain/NotificationService/NotificationService.swift index 363b745ab90..33948ff91d0 100644 --- a/WireDomain/Sources/WireDomain/NotificationService/NotificationService.swift +++ b/WireDomain/Sources/WireDomain/NotificationService/NotificationService.swift @@ -126,6 +126,9 @@ final class NotificationService: UNNotificationServiceExtension { private func finishWithEmptyNotification() { logger.info("finishing without showing notification") let emptyNotification = UNNotificationContent() + // With the "filtering" entitlement, we can tell iOS to not display a user notification by + // passing empty content to the content handler. + // See https://developer.apple.com/documentation/bundleresources/entitlements/com_apple_developer_usernotifications_filtering contentHandler?(emptyNotification) terminate() } From a2e720ea75ab21d0ea4ca67a3974ff0c7ee8c7e1 Mon Sep 17 00:00:00 2001 From: Jullian Mercier <31648126+jullianm@users.noreply.github.com> Date: Fri, 3 Jan 2025 17:05:39 +0100 Subject: [PATCH 12/17] Update WireDomain/Sources/WireDomain/NotificationService/NotificationSession.swift MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: François Benaiteau --- .../WireDomain/NotificationService/NotificationSession.swift | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/WireDomain/Sources/WireDomain/NotificationService/NotificationSession.swift b/WireDomain/Sources/WireDomain/NotificationService/NotificationSession.swift index 7760f8c505d..ed5cc8fa16c 100644 --- a/WireDomain/Sources/WireDomain/NotificationService/NotificationSession.swift +++ b/WireDomain/Sources/WireDomain/NotificationService/NotificationSession.swift @@ -56,9 +56,8 @@ final class NotificationSession { // MARK: - Notifications func processPushNotification( - eventID: UUID + eventID newEventID: UUID ) async throws { - let newEventID = eventID let lastEventId = updateEventsRepository.fetchLastEventEnvelopeID() if lastEventId == nil { From 4ad3c726c1f6069ae938e9d8ffba2ede32a4a0fa Mon Sep 17 00:00:00 2001 From: Jullian Mercier <31648126+jullianm@users.noreply.github.com> Date: Fri, 3 Jan 2025 17:08:21 +0100 Subject: [PATCH 13/17] clean up code --- WireDomain/Sources/WireDomain/Assembly.swift | 9 --------- .../WireDomain/Dependency Injection/Injector.swift | 2 +- .../NotificationService/NotificationService.swift | 6 +++--- 3 files changed, 4 insertions(+), 13 deletions(-) diff --git a/WireDomain/Sources/WireDomain/Assembly.swift b/WireDomain/Sources/WireDomain/Assembly.swift index 12e19d08146..7f0c8d6d4b4 100644 --- a/WireDomain/Sources/WireDomain/Assembly.swift +++ b/WireDomain/Sources/WireDomain/Assembly.swift @@ -62,11 +62,6 @@ public final class Assembly { apiService: apiService ).makeAPI(for: apiVersion) - private lazy var updateEventDecryptor = UpdateEventDecryptor( - proteusService: proteusService, - context: context - ) - // MARK: - Repositories and local stores Init private lazy var userLocalStore = UserLocalStore(context: context) @@ -98,10 +93,6 @@ extension Assembly { self.pushChannel } - Injector.register(UpdateEventDecryptorProtocol.self) { - self.updateEventDecryptor - } - Injector.register(UpdateEventsLocalStoreProtocol.self) { self.updateEventsLocalStore } diff --git a/WireDomain/Sources/WireDomain/Dependency Injection/Injector.swift b/WireDomain/Sources/WireDomain/Dependency Injection/Injector.swift index ab35a944bb2..afaa2c94837 100644 --- a/WireDomain/Sources/WireDomain/Dependency Injection/Injector.swift +++ b/WireDomain/Sources/WireDomain/Dependency Injection/Injector.swift @@ -100,7 +100,7 @@ enum Injector { // MARK: - Resolve static func resolve() -> Service { - typealias FactoryType = () -> Any + typealias FactoryType = (()) -> Any return _genericResolve(serviceType: Service.self) { (factory: FactoryType) in factory(()) } diff --git a/WireDomain/Sources/WireDomain/NotificationService/NotificationService.swift b/WireDomain/Sources/WireDomain/NotificationService/NotificationService.swift index 33948ff91d0..0e68158f8fc 100644 --- a/WireDomain/Sources/WireDomain/NotificationService/NotificationService.swift +++ b/WireDomain/Sources/WireDomain/NotificationService/NotificationService.swift @@ -50,7 +50,7 @@ final class NotificationService: UNNotificationServiceExtension { _ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void ) { - ongoingTask?.cancel() + onGoingTask?.cancel() let cookieStorage: ZMPersistentCookieStorage = Injector.resolve() let isAuthenticated = cookieStorage.isAuthenticated @@ -64,7 +64,7 @@ final class NotificationService: UNNotificationServiceExtension { self.contentHandler = contentHandler - ongoingTask = Task { + onGoingTask = Task { do { let notificationUserInfo = request.content.userInfo @@ -175,6 +175,6 @@ final class NotificationService: UNNotificationServiceExtension { // Content handler should only be consumed once. contentHandler = nil notificationSession = nil - ongoingTask = nil + onGoingTask = nil } } From 8d3a51059027d6dfd35123cb0c5d180a73b1e166 Mon Sep 17 00:00:00 2001 From: Jullian Mercier <31648126+jullianm@users.noreply.github.com> Date: Fri, 3 Jan 2025 17:10:41 +0100 Subject: [PATCH 14/17] fix UT --- .../Notifications/NotificationSessionTests.swift | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/WireDomain/Tests/WireDomainTests/Notifications/NotificationSessionTests.swift b/WireDomain/Tests/WireDomainTests/Notifications/NotificationSessionTests.swift index 9e6260bc077..9ea0db6e0a3 100644 --- a/WireDomain/Tests/WireDomainTests/Notifications/NotificationSessionTests.swift +++ b/WireDomain/Tests/WireDomainTests/Notifications/NotificationSessionTests.swift @@ -102,14 +102,15 @@ final class NotificationSessionTests: XCTestCase { conversationID: ConversationID(uuid: .mockID1, domain: ""), senderID: UserID(uuid: .mockID2, domain: ""), subconversation: "subconversation", - message: "message" + message: "message", + timestamp: .now ) static let proteusMessageAddEvent = ConversationProteusMessageAddEvent( conversationID: ConversationID(uuid: .mockID1, domain: ""), senderID: UserID(uuid: .mockID2, domain: ""), timestamp: .now, - message: .ciphertext("foo"), - externalData: .ciphertext("bar"), + message: .init(encryptedMessage: "foo"), + externalData: .init(encryptedMessage: "bar"), messageSenderClientID: "abc123", messageRecipientClientID: "def456" ) From dba88d3859c3cd24f3db591318fdc1672289a609 Mon Sep 17 00:00:00 2001 From: Jullian Mercier <31648126+jullianm@users.noreply.github.com> Date: Fri, 3 Jan 2025 17:12:36 +0100 Subject: [PATCH 15/17] add comment to mocks --- .../Repositories/Mock/MockFeatureConfigRepositoryProtocol.swift | 2 +- .../Repositories/Mock/MockUpdateEventsRepository.swift | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/WireDomain/Tests/WireDomainTests/Repositories/Mock/MockFeatureConfigRepositoryProtocol.swift b/WireDomain/Tests/WireDomainTests/Repositories/Mock/MockFeatureConfigRepositoryProtocol.swift index 5532bdb02fe..a9c04974782 100644 --- a/WireDomain/Tests/WireDomainTests/Repositories/Mock/MockFeatureConfigRepositoryProtocol.swift +++ b/WireDomain/Tests/WireDomainTests/Repositories/Mock/MockFeatureConfigRepositoryProtocol.swift @@ -22,7 +22,7 @@ import WireDataModel @testable import WireDomain -/// Since stencil does not handle generics properly, we need to create this mock manually. +/// Since stencil does not handle generics and Combine code properly, we need to create this mock manually. class MockFeatureConfigRepositoryProtocol: FeatureConfigRepositoryProtocol { // MARK: - Life cycle diff --git a/WireDomain/Tests/WireDomainTests/Repositories/Mock/MockUpdateEventsRepository.swift b/WireDomain/Tests/WireDomainTests/Repositories/Mock/MockUpdateEventsRepository.swift index c483ad7ef71..58a9fdb58d8 100644 --- a/WireDomain/Tests/WireDomainTests/Repositories/Mock/MockUpdateEventsRepository.swift +++ b/WireDomain/Tests/WireDomainTests/Repositories/Mock/MockUpdateEventsRepository.swift @@ -21,6 +21,7 @@ import Foundation import WireAPI @testable import WireDomain +/// Since stencil does not handle Combine code properly, we need to create this mock manually. class MockUpdateEventsRepositoryProtocol: UpdateEventsRepositoryProtocol { // MARK: - Life cycle From 9378165166e7ee51ddd0724e5749f04302a6207b Mon Sep 17 00:00:00 2001 From: Jullian Mercier <31648126+jullianm@users.noreply.github.com> Date: Mon, 6 Jan 2025 16:41:45 +0100 Subject: [PATCH 16/17] inject BackendEnvironmentProvider, use cookie storage related to notification user ID --- WireDomain/Sources/WireDomain/Assembly.swift | 14 +++---- .../Dependency Injection/Injector.swift | 2 +- .../NotificationService.swift | 38 ++++++++++++------- 3 files changed, 32 insertions(+), 22 deletions(-) diff --git a/WireDomain/Sources/WireDomain/Assembly.swift b/WireDomain/Sources/WireDomain/Assembly.swift index 7f0c8d6d4b4..ba1887382ea 100644 --- a/WireDomain/Sources/WireDomain/Assembly.swift +++ b/WireDomain/Sources/WireDomain/Assembly.swift @@ -30,7 +30,7 @@ public final class Assembly { private let apiService: any APIServiceProtocol private let apiVersion: WireAPI.APIVersion private let pushChannel: any PushChannelProtocol - private let cookieStorage: ZMPersistentCookieStorage + private let backendEnvironmentProvider: BackendEnvironmentProvider init( userID: UUID, @@ -41,7 +41,7 @@ public final class Assembly { apiService: any APIServiceProtocol, apiVersion: WireAPI.APIVersion, pushChannel: any PushChannelProtocol, - cookieStorage: ZMPersistentCookieStorage + backendEnvironmentProvider: BackendEnvironmentProvider ) { self.userID = userID self.clientID = clientID @@ -51,7 +51,7 @@ public final class Assembly { self.apiService = apiService self.apiVersion = apiVersion self.pushChannel = pushChannel - self.cookieStorage = cookieStorage + self.backendEnvironmentProvider = backendEnvironmentProvider registerNotificationServiceDependencies() } @@ -92,13 +92,13 @@ extension Assembly { Injector.register(PushChannelProtocol.self) { self.pushChannel } + + Injector.register(BackendEnvironmentProvider.self) { + self.backendEnvironmentProvider + } Injector.register(UpdateEventsLocalStoreProtocol.self) { self.updateEventsLocalStore } - - Injector.register(ZMPersistentCookieStorage.self) { - self.cookieStorage - } } } diff --git a/WireDomain/Sources/WireDomain/Dependency Injection/Injector.swift b/WireDomain/Sources/WireDomain/Dependency Injection/Injector.swift index afaa2c94837..5fc364f2b69 100644 --- a/WireDomain/Sources/WireDomain/Dependency Injection/Injector.swift +++ b/WireDomain/Sources/WireDomain/Dependency Injection/Injector.swift @@ -165,7 +165,7 @@ enum Injector { invoker: @escaping ((Arguments) -> Any) -> Any ) -> Service { var resolvedInstance: Service? - var type: Any.Type = if let optionalType = Service.self as? OptionalProtocol.Type { + let type: Any.Type = if let optionalType = Service.self as? OptionalProtocol.Type { optionalType.wrappedType } else { Service.self diff --git a/WireDomain/Sources/WireDomain/NotificationService/NotificationService.swift b/WireDomain/Sources/WireDomain/NotificationService/NotificationService.swift index 0e68158f8fc..788ab28c0f5 100644 --- a/WireDomain/Sources/WireDomain/NotificationService/NotificationService.swift +++ b/WireDomain/Sources/WireDomain/NotificationService/NotificationService.swift @@ -28,6 +28,7 @@ final class NotificationService: UNNotificationServiceExtension { enum Failure: Error { case missingSelfClientID + case notAuthenticated } // MARK: - Properties @@ -51,17 +52,6 @@ final class NotificationService: UNNotificationServiceExtension { withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void ) { onGoingTask?.cancel() - let cookieStorage: ZMPersistentCookieStorage = Injector.resolve() - let isAuthenticated = cookieStorage.isAuthenticated - - guard isAuthenticated else { - logger.error( - "Not displaying notification because app is not authenticated" - ) - - return finishWithEmptyNotification() - } - self.contentHandler = contentHandler onGoingTask = Task { @@ -97,6 +87,19 @@ final class NotificationService: UNNotificationServiceExtension { ) async throws -> NotificationSession { let userLocalStore: UserLocalStoreProtocol = Injector.resolve() let selfUserInfo = await userLocalStore.selfUserInfo() + let environment: BackendEnvironmentProvider = Injector.resolve() + + let cookieStorage = ZMPersistentCookieStorage( + forServerName: environment.backendURL.host!, + userIdentifier: userID, + useCache: false + ) + + let isAuthenticated = cookieStorage.isAuthenticated + + guard isAuthenticated else { + throw Failure.notAuthenticated + } guard let selfClientID = selfUserInfo.clientId else { throw Failure.missingSelfClientID @@ -126,9 +129,10 @@ final class NotificationService: UNNotificationServiceExtension { private func finishWithEmptyNotification() { logger.info("finishing without showing notification") let emptyNotification = UNNotificationContent() - // With the "filtering" entitlement, we can tell iOS to not display a user notification by - // passing empty content to the content handler. - // See https://developer.apple.com/documentation/bundleresources/entitlements/com_apple_developer_usernotifications_filtering + + // With the "filtering" entitlement, we can tell iOS to not display a user notification by + // passing empty content to the content handler. + // See https://developer.apple.com/documentation/bundleresources/entitlements/com_apple_developer_usernotifications_filtering contentHandler?(emptyNotification) terminate() } @@ -162,6 +166,12 @@ final class NotificationService: UNNotificationServiceExtension { logger.error( "failed to create notification session: missing self client ID" ) + + case .notAuthenticated: + logger.error( + "Not displaying notification because app is not authenticated" + ) + } default: From 29d578487a812c42a7b333024438e2469e41bf7f Mon Sep 17 00:00:00 2001 From: Jullian Mercier <31648126+jullianm@users.noreply.github.com> Date: Mon, 6 Jan 2025 16:42:17 +0100 Subject: [PATCH 17/17] lint and format --- WireDomain/Sources/WireDomain/Assembly.swift | 2 +- .../NotificationService/NotificationService.swift | 11 +++++------ 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/WireDomain/Sources/WireDomain/Assembly.swift b/WireDomain/Sources/WireDomain/Assembly.swift index ba1887382ea..cd3f14414b8 100644 --- a/WireDomain/Sources/WireDomain/Assembly.swift +++ b/WireDomain/Sources/WireDomain/Assembly.swift @@ -92,7 +92,7 @@ extension Assembly { Injector.register(PushChannelProtocol.self) { self.pushChannel } - + Injector.register(BackendEnvironmentProvider.self) { self.backendEnvironmentProvider } diff --git a/WireDomain/Sources/WireDomain/NotificationService/NotificationService.swift b/WireDomain/Sources/WireDomain/NotificationService/NotificationService.swift index 788ab28c0f5..5b4b2dbbe71 100644 --- a/WireDomain/Sources/WireDomain/NotificationService/NotificationService.swift +++ b/WireDomain/Sources/WireDomain/NotificationService/NotificationService.swift @@ -88,15 +88,15 @@ final class NotificationService: UNNotificationServiceExtension { let userLocalStore: UserLocalStoreProtocol = Injector.resolve() let selfUserInfo = await userLocalStore.selfUserInfo() let environment: BackendEnvironmentProvider = Injector.resolve() - + let cookieStorage = ZMPersistentCookieStorage( forServerName: environment.backendURL.host!, userIdentifier: userID, useCache: false ) - + let isAuthenticated = cookieStorage.isAuthenticated - + guard isAuthenticated else { throw Failure.notAuthenticated } @@ -129,7 +129,7 @@ final class NotificationService: UNNotificationServiceExtension { private func finishWithEmptyNotification() { logger.info("finishing without showing notification") let emptyNotification = UNNotificationContent() - + // With the "filtering" entitlement, we can tell iOS to not display a user notification by // passing empty content to the content handler. // See https://developer.apple.com/documentation/bundleresources/entitlements/com_apple_developer_usernotifications_filtering @@ -166,12 +166,11 @@ final class NotificationService: UNNotificationServiceExtension { logger.error( "failed to create notification session: missing self client ID" ) - + case .notAuthenticated: logger.error( "Not displaying notification because app is not authenticated" ) - } default: