diff --git a/ElementX/Sources/Application/AppCoordinator.swift b/ElementX/Sources/Application/AppCoordinator.swift index c1319dda0..f9ae849df 100644 --- a/ElementX/Sources/Application/AppCoordinator.swift +++ b/ElementX/Sources/Application/AppCoordinator.swift @@ -302,7 +302,7 @@ class AppCoordinator: AppCoordinatorProtocol { .sink { [weak self] callback in switch callback { case .registeredNotifications(let deviceToken): - self?.notificationManager?.register(with: deviceToken, completion: nil) + Task { await self?.notificationManager?.register(with: deviceToken) } case .failedToRegisteredNotifications(let error): self?.notificationManager?.registrationFailed(with: error) } @@ -467,8 +467,8 @@ extension AppCoordinator: NotificationManagerDelegate { break default: // error or no room proxy - service.showLocalNotification(with: "⚠️ " + ElementL10n.dialogTitleError, - subtitle: ElementL10n.a11yErrorSomeMessageNotSent) + await service.showLocalNotification(with: "⚠️ " + ElementL10n.dialogTitleError, + subtitle: ElementL10n.a11yErrorSomeMessageNotSent) } } } diff --git a/ElementX/Sources/Services/Notification/Manager/MockNotificationManager.swift b/ElementX/Sources/Services/Notification/Manager/MockNotificationManager.swift index fc37f5ad2..7891e7665 100644 --- a/ElementX/Sources/Services/Notification/Manager/MockNotificationManager.swift +++ b/ElementX/Sources/Services/Notification/Manager/MockNotificationManager.swift @@ -29,7 +29,7 @@ class MockNotificationManager: NotificationManagerProtocol { delegate?.authorizationStatusUpdated(self, granted: false) } - func register(with deviceToken: Data, completion: ((Bool) -> Void)? = nil) { } + func register(with deviceToken: Data) async -> Bool { false } func registrationFailed(with error: Error) { } diff --git a/ElementX/Sources/Services/Notification/Manager/NotificationManager.swift b/ElementX/Sources/Services/Notification/Manager/NotificationManager.swift index bc7cfedf1..9348b636c 100644 --- a/ElementX/Sources/Services/Notification/Manager/NotificationManager.swift +++ b/ElementX/Sources/Services/Notification/Manager/NotificationManager.swift @@ -46,30 +46,26 @@ class NotificationManager: NSObject, NotificationManagerProtocol { options: []) notificationCenter.setNotificationCategories([replyCategory]) notificationCenter.delegate = self - notificationCenter.requestAuthorization(options: [.alert, .sound, .badge]) { [weak self] granted, error in - guard let self else { - return - } - if let error { - MXLog.debug("[NotificationManager] request authorization failed: \(error)") - } else { + Task { + do { + let granted = try await notificationCenter.requestAuthorization(options: [.alert, .sound, .badge]) MXLog.debug("[NotificationManager] permission granted: \(granted)") - DispatchQueue.main.async { + await MainActor.run { self.delegate?.authorizationStatusUpdated(self, granted: granted) } + } catch { + MXLog.debug("[NotificationManager] request authorization failed: \(error)") } } } - func register(with deviceToken: Data, completion: ((Bool) -> Void)? = nil) { - setPusher(with: deviceToken, - clientProxy: clientProxy, - completion: completion) + func register(with deviceToken: Data) async -> Bool { + await setPusher(with: deviceToken, clientProxy: clientProxy) } func registrationFailed(with error: Error) { } - func showLocalNotification(with title: String, subtitle: String?) { + func showLocalNotification(with title: String, subtitle: String?) async { let content = UNMutableNotificationContent() content.title = title if let subtitle { @@ -78,44 +74,39 @@ class NotificationManager: NSObject, NotificationManagerProtocol { let request = UNNotificationRequest(identifier: ProcessInfo.processInfo.globallyUniqueString, content: content, trigger: nil) - notificationCenter.add(request) { error in - if let error { - MXLog.debug("[NotificationManager] show local notification failed: \(error)") - } else { - MXLog.debug("[NotificationManager] show local notification succeeded") - } + do { + try await notificationCenter.add(request) + MXLog.debug("[NotificationManager] show local notification succeeded") + } catch { + MXLog.debug("[NotificationManager] show local notification failed: \(error)") } } - - private func setPusher(with deviceToken: Data, - clientProxy: ClientProxyProtocol, - completion: ((Bool) -> Void)?) { - Task { - do { - try await clientProxy.setPusher(pushkey: deviceToken.base64EncodedString(), - kind: .http, - appId: ServiceLocator.shared.settings.pusherAppId, - appDisplayName: "\(InfoPlistReader.target.bundleDisplayName) (iOS)", - deviceDisplayName: UIDevice.current.name, - profileTag: pusherProfileTag(), - lang: Bundle.preferredLanguages.first ?? "en", - url: ServiceLocator.shared.settings.pushGatewayBaseURL.absoluteString, - format: .eventIdOnly, - defaultPayload: [ - "aps": [ - "mutable-content": 1, - "alert": [ - "loc-key": "Notification", - "loc-args": [] - ] + + private func setPusher(with deviceToken: Data, clientProxy: ClientProxyProtocol) async -> Bool { + do { + try await clientProxy.setPusher(pushkey: deviceToken.base64EncodedString(), + kind: .http, + appId: ServiceLocator.shared.settings.pusherAppId, + appDisplayName: "\(InfoPlistReader.target.bundleDisplayName) (iOS)", + deviceDisplayName: UIDevice.current.name, + profileTag: pusherProfileTag(), + lang: Bundle.preferredLanguages.first ?? "en", + url: ServiceLocator.shared.settings.pushGatewayBaseURL.absoluteString, + format: .eventIdOnly, + defaultPayload: [ + "aps": [ + "mutable-content": 1, + "alert": [ + "loc-key": "Notification", + "loc-args": [] ] - ]) - MXLog.debug("[NotificationManager] set pusher succeeded") - completion?(true) - } catch { - MXLog.debug("[NotificationManager] set pusher failed: \(error)") - completion?(false) - } + ] + ]) + MXLog.debug("[NotificationManager] set pusher succeeded") + return true + } catch { + MXLog.debug("[NotificationManager] set pusher failed: \(error)") + return false } } diff --git a/ElementX/Sources/Services/Notification/Manager/NotificationManagerProtocol.swift b/ElementX/Sources/Services/Notification/Manager/NotificationManagerProtocol.swift index 602c37fd8..e93353c2e 100644 --- a/ElementX/Sources/Services/Notification/Manager/NotificationManagerProtocol.swift +++ b/ElementX/Sources/Services/Notification/Manager/NotificationManagerProtocol.swift @@ -36,7 +36,7 @@ protocol NotificationManagerProtocol { var delegate: NotificationManagerDelegate? { get set } func start() - func register(with deviceToken: Data, completion: ((Bool) -> Void)?) + func register(with deviceToken: Data) async -> Bool func registrationFailed(with error: Error) - func showLocalNotification(with title: String, subtitle: String?) + func showLocalNotification(with title: String, subtitle: String?) async } diff --git a/ElementX/Sources/Services/Notification/Manager/UserNotificationCenterProtocol.swift b/ElementX/Sources/Services/Notification/Manager/UserNotificationCenterProtocol.swift index e32266c67..ea42b1e26 100644 --- a/ElementX/Sources/Services/Notification/Manager/UserNotificationCenterProtocol.swift +++ b/ElementX/Sources/Services/Notification/Manager/UserNotificationCenterProtocol.swift @@ -19,7 +19,7 @@ import UserNotifications protocol UserNotificationCenterProtocol: AnyObject { var delegate: UNUserNotificationCenterDelegate? { get set } - func add(_ request: UNNotificationRequest, withCompletionHandler completionHandler: ((Error?) -> Void)?) - func requestAuthorization(options: UNAuthorizationOptions, completionHandler: @escaping (Bool, Error?) -> Void) + func add(_ request: UNNotificationRequest) async throws + func requestAuthorization(options: UNAuthorizationOptions) async throws -> Bool func setNotificationCategories(_ categories: Set) } diff --git a/UnitTests/Sources/NotificationManager/NotificationManagerTests.swift b/UnitTests/Sources/NotificationManager/NotificationManagerTests.swift index faf12246b..55809b3ee 100644 --- a/UnitTests/Sources/NotificationManager/NotificationManagerTests.swift +++ b/UnitTests/Sources/NotificationManager/NotificationManagerTests.swift @@ -33,48 +33,29 @@ final class NotificationManagerTests: XCTestCase { sut = NotificationManager(clientProxy: clientProxySpy, notificationCenter: notificationCenter) } - func test_whenRegistered_pusherIsCalled() throws { - let expectation = expectation(description: "Callback happened") - sut.register(with: Data(), completion: { _ in - expectation.fulfill() - }) - waitForExpectations(timeout: 0.5) + func test_whenRegistered_pusherIsCalled() async { + _ = await sut.register(with: Data()) XCTAssertTrue(clientProxySpy.setPusherCalled) } - func test_whenRegisteredSuccess_completionSuccessIsCalled() throws { - let expectation = expectation(description: "Callback happened") - var success = false - sut.register(with: Data(), completion: { s in - success = s - expectation.fulfill() - }) - waitForExpectations(timeout: 0.5) + func test_whenRegisteredSuccess_completionSuccessIsCalled() async throws { + let success = await sut.register(with: Data()) XCTAssertTrue(success) } - func test_whenRegisteredAndPusherThrowsError_completionFalseIsCalled() throws { - let expectation = expectation(description: "Callback happened") - var success = true + func test_whenRegisteredAndPusherThrowsError_completionFalseIsCalled() async throws { enum TestError: Error { case someError } clientProxySpy.setPusherErrorToThrow = TestError.someError - sut.register(with: Data(), completion: { s in - success = s - expectation.fulfill() - }) - waitForExpectations(timeout: 0.5) + let success = await sut.register(with: Data()) XCTAssertFalse(success) } - func test_whenRegistered_pusherIsCalledWithCorrectValues() throws { - let expectation = expectation(description: "Callback happened") + @MainActor + func test_whenRegistered_pusherIsCalledWithCorrectValues() async throws { let pushkeyData = Data("1234".utf8) - sut.register(with: pushkeyData, completion: { _ in - expectation.fulfill() - }) - waitForExpectations(timeout: 0.5) + _ = await sut.register(with: pushkeyData) XCTAssertEqual(clientProxySpy.setPusherPushkey, pushkeyData.base64EncodedString()) XCTAssertEqual(clientProxySpy.setPusherAppId, settings?.pusherAppId) XCTAssertEqual(clientProxySpy.setPusherKind, .http) @@ -98,28 +79,20 @@ final class NotificationManagerTests: XCTestCase { XCTAssertTrue(actualPayload.isEqual(to: defaultPayload)) } - func test_whenRegisteredAndPusherTagNotSetInSettings_tagGeneratedAndSavedInSettings() throws { - let expectation = expectation(description: "Callback happened") + func test_whenRegisteredAndPusherTagNotSetInSettings_tagGeneratedAndSavedInSettings() async throws { settings?.pusherProfileTag = nil - sut.register(with: Data(), completion: { _ in - expectation.fulfill() - }) - waitForExpectations(timeout: 0.5) + _ = await sut.register(with: Data()) XCTAssertNotNil(settings?.pusherProfileTag) } - func test_whenRegisteredAndPusherTagIsSetInSettings_tagNotGenerated() throws { - let expectation = expectation(description: "Callback happened") + func test_whenRegisteredAndPusherTagIsSetInSettings_tagNotGenerated() async throws { settings?.pusherProfileTag = "12345" - sut.register(with: Data(), completion: { _ in - expectation.fulfill() - }) - waitForExpectations(timeout: 0.5) + _ = await sut.register(with: Data()) XCTAssertEqual(settings?.pusherProfileTag, "12345") } - func test_whenShowLocalNotification_notificationRequestGetsAdded() throws { - sut.showLocalNotification(with: "Title", subtitle: "Subtitle") + func test_whenShowLocalNotification_notificationRequestGetsAdded() async throws { + await sut.showLocalNotification(with: "Title", subtitle: "Subtitle") let request = try XCTUnwrap(notificationCenter.addRequest) XCTAssertEqual(request.content.title, "Title") XCTAssertEqual(request.content.subtitle, "Subtitle") @@ -143,21 +116,18 @@ final class NotificationManagerTests: XCTestCase { XCTAssertTrue(delegate.isEqual(sut)) } - func test_whenStart_requestAuthorizationCalledWithCorrectParams() throws { + func test_whenStart_requestAuthorizationCalledWithCorrectParams() async throws { sut.start() + await Task.yield() XCTAssertEqual(notificationCenter.requestAuthorizationOptions, [.alert, .sound, .badge]) } - func test_whenStartAndAuthorizationGranted_delegateCalled() throws { + func test_whenStartAndAuthorizationGranted_delegateCalled() async throws { authorizationStatusWasGranted = false notificationCenter.requestAuthorizationGrantedReturnValue = true sut.delegate = self sut.start() - let expectation = expectation(description: "Wait for main thread") - DispatchQueue.main.async { - expectation.fulfill() - } - waitForExpectations(timeout: 1, handler: nil) + try await Task.sleep(for: .milliseconds(100)) XCTAssertTrue(authorizationStatusWasGranted) } diff --git a/UnitTests/Sources/NotificationManager/UserNotificationCenterSpy.swift b/UnitTests/Sources/NotificationManager/UserNotificationCenterSpy.swift index f4e972c64..cc103412b 100644 --- a/UnitTests/Sources/NotificationManager/UserNotificationCenterSpy.swift +++ b/UnitTests/Sources/NotificationManager/UserNotificationCenterSpy.swift @@ -21,16 +21,15 @@ class UserNotificationCenterSpy: UserNotificationCenterProtocol { weak var delegate: UNUserNotificationCenterDelegate? var addRequest: UNNotificationRequest? - func add(_ request: UNNotificationRequest, withCompletionHandler completionHandler: ((Error?) -> Void)?) { + func add(_ request: UNNotificationRequest) async throws { addRequest = request - completionHandler?(nil) } var requestAuthorizationOptions: UNAuthorizationOptions? var requestAuthorizationGrantedReturnValue = false - func requestAuthorization(options: UNAuthorizationOptions, completionHandler: @escaping (Bool, Error?) -> Void) { + func requestAuthorization(options: UNAuthorizationOptions) async throws -> Bool { requestAuthorizationOptions = options - completionHandler(requestAuthorizationGrantedReturnValue, nil) + return requestAuthorizationGrantedReturnValue } var notificationCategoriesValue: Set? diff --git a/changelog.d/pr-407.change b/changelog.d/pr-407.change new file mode 100644 index 000000000..38aff47e0 --- /dev/null +++ b/changelog.d/pr-407.change @@ -0,0 +1 @@ +Notification Manager: Replace completion handlers with async/await. \ No newline at end of file