Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Example/Example/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}
}
}
Expand Down
5 changes: 2 additions & 3 deletions Mindbox/CoreController/CoreController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
21 changes: 19 additions & 2 deletions Mindbox/Mindbox.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}

/**
Expand Down
23 changes: 21 additions & 2 deletions MindboxTests/Mock/MockUNAuthorizationStatusProvider.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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))
}
}
75 changes: 54 additions & 21 deletions MindboxTests/Versioning/VersioningTestCase.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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<MobileApplicationInfoUpdated>(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)
}
}

Expand Down Expand Up @@ -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<MobileApplicationInfoUpdated>(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)
}
}
}