diff --git a/Example/Example/AppDelegate.swift b/Example/Example/AppDelegate.swift index cea740d1..9c4ec6d5 100644 --- a/Example/Example/AppDelegate.swift +++ b/Example/Example/AppDelegate.swift @@ -76,7 +76,7 @@ final class AppDelegate: MindboxAppDelegate { if let error = error { print("NotificationsRequestAuthorization failed with error: \(error.localizedDescription)") } - Mindbox.shared.notificationsRequestAuthorization(granted: granted) + Mindbox.shared.refreshNotificationPermissionStatus() } } } diff --git a/Mindbox/CoreController/CoreController.swift b/Mindbox/CoreController/CoreController.swift index 371cc96d..cc569329 100644 --- a/Mindbox/CoreController/CoreController.swift +++ b/Mindbox/CoreController/CoreController.swift @@ -80,12 +80,11 @@ final class CoreController { } } - func checkNotificationStatus(granted: Bool? = nil, - completion: (() -> Void)? = nil) { + func checkNotificationStatus(completion: (() -> Void)? = nil) { controllerQueue.async { defer { DispatchQueue.main.async { completion?() } } - let isNotificationsEnabled = granted ?? self.notificationStatus() + let isNotificationsEnabled = self.notificationStatus() guard self.persistenceStorage.isNotificationsEnabled != isNotificationsEnabled else { return } diff --git a/Mindbox/Mindbox.swift b/Mindbox/Mindbox.swift index 255868c7..1b65b0c4 100644 --- a/Mindbox/Mindbox.swift +++ b/Mindbox/Mindbox.swift @@ -181,9 +181,26 @@ public class Mindbox: NSObject { } } - /// Use this method to notify Mindbox for notification request authorization changes. + /// Deprecated. Use ``Mindbox/Mindbox/refreshNotificationPermissionStatus()`` instead. + /// + /// This method is kept for backward compatibility. The `granted` argument is ignored. + /// The SDK reads the current system authorization status and, if it differs + /// from the last known value, sends an update to the backend. + @available(*, deprecated, message: "Use refreshNotificationPermissionStatus() instead.", renamed: "refreshNotificationPermissionStatus()") public func notificationsRequestAuthorization(granted: Bool) { - coreController?.checkNotificationStatus(granted: granted) + coreController?.checkNotificationStatus() + } + + /// Checks the current system authorization status for push notifications + /// and reports any changes to Mindbox. + /// + /// The SDK retrieves the current `UNAuthorizationStatus` from + /// `UNUserNotificationCenter`, compares it with the last known value, + /// and, if it has changed, sends the update to the backend. + /// + /// - Important: This method does **not** prompt the system permission alert. + public func refreshNotificationPermissionStatus() { + coreController?.checkNotificationStatus() } /** diff --git a/MindboxTests/Mock/MockUNAuthorizationStatusProvider.swift b/MindboxTests/Mock/MockUNAuthorizationStatusProvider.swift index b1db458e..951d38aa 100644 --- a/MindboxTests/Mock/MockUNAuthorizationStatusProvider.swift +++ b/MindboxTests/Mock/MockUNAuthorizationStatusProvider.swift @@ -7,10 +7,10 @@ // import Foundation -import UIKit +import UserNotifications @testable import Mindbox -class MockUNAuthorizationStatusProvider: UNAuthorizationStatusProviding { +final class MockUNAuthorizationStatusProvider: UNAuthorizationStatusProviding { func getStatus(result: @escaping (Bool) -> Void) { result(status.rawValue == UNAuthorizationStatus.authorized.rawValue) @@ -22,3 +22,22 @@ class MockUNAuthorizationStatusProvider: UNAuthorizationStatusProviding { self.status = status } } + +final class CyclicUNAuthorizationStatusProvider: UNAuthorizationStatusProviding { + private let sequence: [UNAuthorizationStatus] + private var index = 0 + + init(sequence: [UNAuthorizationStatus]) { + precondition(!sequence.isEmpty, "Sequence must not be empty") + self.sequence = sequence + } + + func getStatus(result: @escaping (Bool) -> Void) { + let current = sequence[index % sequence.count] + index += 1 + + var granted: [UNAuthorizationStatus] = [.authorized] + if #available(iOS 12.0, *) { granted.append(.provisional) } + result(granted.contains(current)) + } +} diff --git a/MindboxTests/Versioning/VersioningTestCase.swift b/MindboxTests/Versioning/VersioningTestCase.swift index 2906ae0f..bad38603 100644 --- a/MindboxTests/Versioning/VersioningTestCase.swift +++ b/MindboxTests/Versioning/VersioningTestCase.swift @@ -78,37 +78,33 @@ class VersioningTestCase: XCTestCase { } } - func testInfoUpdateVersioningByRequestAuthorization() { + @available(*, deprecated, message: "Suppress `deprecated` notificationsRequestAuthorization(granted:) warning") + func testInfoUpdateVersioningByDeprecatedMethod() { + setupCyclicStatusProvider() initConfiguration(delay: .default) self.guaranteedDeliveryManager.canScheduleOperations = false let infoUpdateLimit = 50 - makeMockSequentialCall(limit: infoUpdateLimit) { index in - Mindbox.shared.notificationsRequestAuthorization(granted: index % 2 == 0) + makeMockSequentialCall(limit: infoUpdateLimit) { _ in + Mindbox.shared.notificationsRequestAuthorization(granted: true) } - delay(of: .default) + verifyVersioning(limit: infoUpdateLimit) + } + + func testInfoUpdateVersioningByRefreshNotificationPermissionStatus() { + setupCyclicStatusProvider() + initConfiguration(delay: .default) - do { - let events = try self.databaseRepository.query(fetchLimit: infoUpdateLimit) - XCTAssertNotEqual(events.count, 0) - XCTAssertEqual(events.count, infoUpdateLimit) + self.guaranteedDeliveryManager.canScheduleOperations = false + let infoUpdateLimit = 50 - events.forEach({ - XCTAssertTrue($0.type == .infoUpdated) - }) - events - .sorted { $0.dateTimeOffset > $1.dateTimeOffset } - .compactMap { BodyDecoder(decodable: $0.body)?.body } - .enumerated() - .makeIterator() - .forEach { offset, element in - XCTAssertEqual(offset + 1, element.version, "Element version mismatch at offset \(offset + 1)") - } - } catch { - XCTFail(error.localizedDescription) + makeMockSequentialCall(limit: infoUpdateLimit) { _ in + Mindbox.shared.refreshNotificationPermissionStatus() } + + verifyVersioning(limit: infoUpdateLimit) } } @@ -227,4 +223,41 @@ private extension VersioningTestCase { wait(for: [delayExpectation], timeout: of.timeInterval * 2) } + + /// Sets up CyclicUNAuthorizationStatusProvider for versioning tests. + /// Must use .container scope (singleton) to preserve state between calls. + func setupCyclicStatusProvider() { + MBInject.container.register( + UNAuthorizationStatusProviding.self, + scope: .container + ) { + let seq: [UNAuthorizationStatus] = [.authorized, .denied] + return CyclicUNAuthorizationStatusProvider(sequence: seq) + } + } + + /// Verifies that events are properly versioned with sequential version numbers. + func verifyVersioning(limit: Int, file: StaticString = #file, line: UInt = #line) { + delay(of: .default) + + do { + let events = try self.databaseRepository.query(fetchLimit: limit) + XCTAssertNotEqual(events.count, 0, file: file, line: line) + XCTAssertEqual(events.count, limit, file: file, line: line) + + events.forEach({ + XCTAssertTrue($0.type == .infoUpdated, file: file, line: line) + }) + events + .sorted { $0.dateTimeOffset > $1.dateTimeOffset } + .compactMap { BodyDecoder(decodable: $0.body)?.body } + .enumerated() + .makeIterator() + .forEach { offset, element in + XCTAssertEqual(offset + 1, element.version, "Element version mismatch at offset \(offset + 1)", file: file, line: line) + } + } catch { + XCTFail(error.localizedDescription, file: file, line: line) + } + } }