Skip to content

Commit

Permalink
[MOB-9446] Enhance push notification state tracking in SDKs (#881)
Browse files Browse the repository at this point in the history
Co-authored-by: Megha Pithadiya <[email protected]>
Co-authored-by: Joao Dordio <[email protected]>
Co-authored-by: Evan Greer <[email protected]>
Co-authored-by: Sumeru Chatterjee <[email protected]>
  • Loading branch information
5 people authored Jan 14, 2025
1 parent 75193e8 commit 6f00e45
Show file tree
Hide file tree
Showing 10 changed files with 159 additions and 14 deletions.
4 changes: 4 additions & 0 deletions swift-sdk.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
00B6FACE210E88ED007535CF /* prod-1.mobileprovision in Resources */ = {isa = PBXBuildFile; fileRef = 00B6FACD210E874D007535CF /* prod-1.mobileprovision */; };
00B6FAD1210E8D90007535CF /* dev-1.mobileprovision in Resources */ = {isa = PBXBuildFile; fileRef = 00B6FAD0210E8D90007535CF /* dev-1.mobileprovision */; };
00CB31B621096129004ACDEC /* TestUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 00CB31B4210960C4004ACDEC /* TestUtils.swift */; };
092D01942D3038F600E3066A /* NotificationObserverTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 092D01932D3038F600E3066A /* NotificationObserverTests.swift */; };
1CBFFE1A2A97AEEF00ED57EE /* EmbeddedManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1CBFFE162A97AEEE00ED57EE /* EmbeddedManagerTests.swift */; };
1CBFFE1B2A97AEEF00ED57EE /* EmbeddedMessagingProcessorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1CBFFE172A97AEEE00ED57EE /* EmbeddedMessagingProcessorTests.swift */; };
1CBFFE1C2A97AEEF00ED57EE /* EmbeddedSessionManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1CBFFE182A97AEEE00ED57EE /* EmbeddedSessionManagerTests.swift */; };
Expand Down Expand Up @@ -543,6 +544,7 @@
00B6FACD210E874D007535CF /* prod-1.mobileprovision */ = {isa = PBXFileReference; lastKnownFileType = file; path = "prod-1.mobileprovision"; sourceTree = "<group>"; };
00B6FAD0210E8D90007535CF /* dev-1.mobileprovision */ = {isa = PBXFileReference; lastKnownFileType = file; path = "dev-1.mobileprovision"; sourceTree = "<group>"; };
00CB31B4210960C4004ACDEC /* TestUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestUtils.swift; sourceTree = "<group>"; };
092D01932D3038F600E3066A /* NotificationObserverTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationObserverTests.swift; sourceTree = "<group>"; };
1CBFFE162A97AEEE00ED57EE /* EmbeddedManagerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EmbeddedManagerTests.swift; sourceTree = "<group>"; };
1CBFFE172A97AEEE00ED57EE /* EmbeddedMessagingProcessorTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EmbeddedMessagingProcessorTests.swift; sourceTree = "<group>"; };
1CBFFE182A97AEEE00ED57EE /* EmbeddedSessionManagerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EmbeddedSessionManagerTests.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -936,6 +938,7 @@
552A0AA9280E249C00A80963 /* notification-tests */ = {
isa = PBXGroup;
children = (
092D01932D3038F600E3066A /* NotificationObserverTests.swift */,
55B37FC32297135F0042F13A /* NotificationMetadataTests.swift */,
AC2C667F20D31B1F00D46CC9 /* NotificationResponseTests.swift */,
);
Expand Down Expand Up @@ -2186,6 +2189,7 @@
5588DFD128C0465E000697D7 /* MockAPNSTypeChecker.swift in Sources */,
00B6FACC210E8484007535CF /* APNSTypeCheckerTests.swift in Sources */,
AC8F35A2239806B500302994 /* InboxViewControllerViewModelTests.swift in Sources */,
092D01942D3038F600E3066A /* NotificationObserverTests.swift in Sources */,
AC995F9A2166EEB50099A184 /* CommonMocks.swift in Sources */,
5588DFE128C046B7000697D7 /* MockLocalStorage.swift in Sources */,
1CBFFE1B2A97AEEF00ED57EE /* EmbeddedMessagingProcessorTests.swift in Sources */,
Expand Down
4 changes: 3 additions & 1 deletion swift-sdk/Core/Constants.swift
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,9 @@ enum Const {
static let deviceId = "itbl_device_id"
static let sdkVersion = "itbl_sdk_version"
static let offlineMode = "itbl_offline_mode"

static let isNotificationsEnabled = "itbl_isNotificationsEnabled"
static let hasStoredNotificationSetting = "itbl_hasStoredNotificationSetting"

static let attributionInfoExpiration = 24
}

Expand Down
56 changes: 53 additions & 3 deletions swift-sdk/Internal/InternalIterableAPI.swift
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,7 @@ final class InternalIterableAPI: NSObject, PushTrackerProtocol, AuthProvider {

// MARK: - API Request Calls

func register(token: Data,
func register(token: String,
onSuccess: OnSuccessHandler? = nil,
onFailure: OnFailureHandler? = nil) {
guard let appName = pushIntegrationName else {
Expand All @@ -187,8 +187,8 @@ final class InternalIterableAPI: NSObject, PushTrackerProtocol, AuthProvider {
return
}

hexToken = token.hexString()
let registerTokenInfo = RegisterTokenInfo(hexToken: token.hexString(),
hexToken = token
let registerTokenInfo = RegisterTokenInfo(hexToken: token,
appName: appName,
pushServicePlatform: config.pushPlatform,
apnsType: dependencyContainer.apnsTypeChecker.apnsType,
Expand All @@ -208,6 +208,12 @@ final class InternalIterableAPI: NSObject, PushTrackerProtocol, AuthProvider {
)
}

func register(token: Data,
onSuccess: OnSuccessHandler? = nil,
onFailure: OnFailureHandler? = nil) {
register(token: token.hexString(), onSuccess: onSuccess, onFailure: onFailure)
}

@discardableResult
func disableDeviceForCurrentUser(withOnSuccess onSuccess: OnSuccessHandler? = nil,
onFailure: OnFailureHandler? = nil) -> Pending<SendRequestValue, SendRequestError> {
Expand All @@ -216,12 +222,18 @@ final class InternalIterableAPI: NSObject, PushTrackerProtocol, AuthProvider {
onFailure?(errorMessage, nil)
return SendRequestError.createErroredFuture(reason: errorMessage)
}

guard userId != nil || email != nil else {
let errorMessage = "either userId or email must be present"
onFailure?(errorMessage, nil)
return SendRequestError.createErroredFuture(reason: errorMessage)
}

// We need to call register token here so that we can trigger the device registration
// with the updated notification settings

register(token: hexToken)

return requestHandler.disableDeviceForCurrentUser(hexToken: hexToken, withOnSuccess: onSuccess, onFailure: onFailure)
}

Expand Down Expand Up @@ -500,6 +512,8 @@ final class InternalIterableAPI: NSObject, PushTrackerProtocol, AuthProvider {
private var _userId: String?
private var _successCallback: OnSuccessHandler? = nil
private var _failureCallback: OnFailureHandler? = nil

private let notificationCenter: NotificationCenterProtocol


/// the hex representation of this device token
Expand Down Expand Up @@ -666,6 +680,7 @@ final class InternalIterableAPI: NSObject, PushTrackerProtocol, AuthProvider {
localStorage = dependencyContainer.localStorage
inAppDisplayer = dependencyContainer.inAppDisplayer
urlOpener = dependencyContainer.urlOpener
notificationCenter = dependencyContainer.notificationCenter
deepLinkManager = DeepLinkManager(redirectNetworkSessionProvider: dependencyContainer)
}

Expand Down Expand Up @@ -698,10 +713,44 @@ final class InternalIterableAPI: NSObject, PushTrackerProtocol, AuthProvider {
requestHandler.start()

checkRemoteConfiguration()

addForegroundObservers()

return inAppManager.start()
}

private func addForegroundObservers() {
notificationCenter.addObserver(self,
selector: #selector(onAppDidBecomeActiveNotification(notification:)),
name: UIApplication.didBecomeActiveNotification,
object: nil)
}

@objc private func onAppDidBecomeActiveNotification(notification: Notification) {
guard config.autoPushRegistration else { return }

notificationStateProvider.isNotificationsEnabled { [weak self] systemEnabled in
guard let self = self else { return }

let storedEnabled = self.localStorage.isNotificationsEnabled
let hasStoredPermission = self.localStorage.hasStoredNotificationSetting

if self.isEitherUserIdOrEmailSet() {
if hasStoredPermission && (storedEnabled != systemEnabled) {
if !systemEnabled {
self.disableDeviceForCurrentUser()
} else {
self.notificationStateProvider.registerForRemoteNotifications()
}
}

// Always store the current state
self.localStorage.isNotificationsEnabled = systemEnabled
self.localStorage.hasStoredNotificationSetting = true
}
}
}

private func handle(launchOptions: [UIApplication.LaunchOptionsKey: Any]?) {
guard let launchOptions = launchOptions else {
return
Expand Down Expand Up @@ -772,6 +821,7 @@ final class InternalIterableAPI: NSObject, PushTrackerProtocol, AuthProvider {

deinit {
ITBInfo()
notificationCenter.removeObserver(self)
requestHandler.stop()
}
}
20 changes: 19 additions & 1 deletion swift-sdk/Internal/IterableUserDefaults.swift
Original file line number Diff line number Diff line change
Expand Up @@ -64,12 +64,28 @@ class IterableUserDefaults {

var offlineMode: Bool {
get {
return bool(withKey: .offlineMode)
bool(withKey: .offlineMode)
} set {
save(bool: newValue, withKey: .offlineMode)
}
}

var isNotificationsEnabled: Bool {
get {
bool(withKey: .isNotificationsEnabled)
} set {
save(bool: newValue, withKey: .isNotificationsEnabled)
}
}

var hasStoredNotificationSetting: Bool {
get {
bool(withKey: .hasStoredNotificationSetting)
} set {
save(bool: newValue, withKey: .hasStoredNotificationSetting)
}
}

func getAttributionInfo(currentDate: Date) -> IterableAttributionInfo? {
(try? codable(withKey: .attributionInfo, currentDate: currentDate)) ?? nil
}
Expand Down Expand Up @@ -196,6 +212,8 @@ class IterableUserDefaults {
static let deviceId = UserDefaultsKey(value: Const.UserDefault.deviceId)
static let sdkVersion = UserDefaultsKey(value: Const.UserDefault.sdkVersion)
static let offlineMode = UserDefaultsKey(value: Const.UserDefault.offlineMode)
static let isNotificationsEnabled = UserDefaultsKey(value: Const.UserDefault.isNotificationsEnabled)
static let hasStoredNotificationSetting = UserDefaultsKey(value: Const.UserDefault.hasStoredNotificationSetting)
}

private struct Envelope: Codable {
Expand Down
16 changes: 16 additions & 0 deletions swift-sdk/Internal/Utilities/LocalStorage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,22 @@ struct LocalStorage: LocalStorageProtocol {
}
}

var isNotificationsEnabled: Bool {
get {
iterableUserDefaults.isNotificationsEnabled
} set {
iterableUserDefaults.isNotificationsEnabled = newValue
}
}

var hasStoredNotificationSetting: Bool {
get {
iterableUserDefaults.hasStoredNotificationSetting
} set {
iterableUserDefaults.hasStoredNotificationSetting = newValue
}
}

func getAttributionInfo(currentDate: Date) -> IterableAttributionInfo? {
iterableUserDefaults.getAttributionInfo(currentDate: currentDate)
}
Expand Down
4 changes: 4 additions & 0 deletions swift-sdk/Internal/Utilities/LocalStorageProtocol.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ protocol LocalStorageProtocol {

var offlineMode: Bool { get set }

var isNotificationsEnabled: Bool { get set }

var hasStoredNotificationSetting: Bool { get set }

func getAttributionInfo(currentDate: Date) -> IterableAttributionInfo?

func save(attributionInfo: IterableAttributionInfo?, withExpiration expiration: Date?)
Expand Down
4 changes: 4 additions & 0 deletions tests/common/MockLocalStorage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ class MockLocalStorage: LocalStorageProtocol {

var offlineMode: Bool = false

var isNotificationsEnabled: Bool = false

var hasStoredNotificationSetting: Bool = false

func getAttributionInfo(currentDate: Date) -> IterableAttributionInfo? {
guard !MockLocalStorage.isExpired(expiration: attributionInfoExpiration, currentDate: currentDate) else {
return nil
Expand Down
1 change: 1 addition & 0 deletions tests/unit-tests/AutoRegistrationTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ class AutoRegistrationTests: XCTestCase {

func testCallDisableAndEnable() {
let expectation1 = expectation(description: "call register device API")
expectation1.expectedFulfillmentCount = 2
let expectation2 = expectation(description: "call registerForRemoteNotifications twice")
expectation2.expectedFulfillmentCount = 2
let expectation3 = expectation(description: "call disable on [email protected]")
Expand Down
18 changes: 9 additions & 9 deletions tests/unit-tests/Mocks.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,19 +10,19 @@ import XCTest

// Note: This is used only by swift tests. So can't put this in Common
class MockNotificationStateProvider: NotificationStateProviderProtocol {
func isNotificationsEnabled(withCallback callback: @escaping (Bool) -> Void) {
callback(enabled)
}

func registerForRemoteNotifications() {
expectation?.fulfill()
}
var enabled: Bool
private let expectation: XCTestExpectation?

init(enabled: Bool, expectation: XCTestExpectation? = nil) {
self.enabled = enabled
self.expectation = expectation
}

private let enabled: Bool
private let expectation: XCTestExpectation?
func isNotificationsEnabled(withCallback callback: @escaping (Bool) -> Void) {
callback(enabled)
}

func registerForRemoteNotifications() {
expectation?.fulfill()
}
}
46 changes: 46 additions & 0 deletions tests/unit-tests/NotificationObserverTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import XCTest
@testable import IterableSDK

class NotificationObserverTests: XCTestCase {
private var internalAPI: InternalIterableAPI!
private var mockNotificationStateProvider: MockNotificationStateProvider!
private var mockLocalStorage: MockLocalStorage!
private var mockNotificationCenter: MockNotificationCenter!

override func setUp() {
super.setUp()

mockNotificationStateProvider = MockNotificationStateProvider(enabled: false)
mockLocalStorage = MockLocalStorage()
mockNotificationCenter = MockNotificationCenter()

let config = IterableConfig()
internalAPI = InternalIterableAPI.initializeForTesting(
config: config,
notificationStateProvider: mockNotificationStateProvider,
localStorage: mockLocalStorage,
notificationCenter: mockNotificationCenter
)
}

func testNotificationStateChangeUpdatesStorage() {
// Arrange
internalAPI.email = "[email protected]"

mockLocalStorage.isNotificationsEnabled = false
mockNotificationStateProvider.enabled = true

// Act
mockNotificationCenter.post(name: UIApplication.didBecomeActiveNotification, object: nil, userInfo: nil)

// Small delay to allow async operation to complete
let expectation = XCTestExpectation(description: "Wait for state update")
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
expectation.fulfill()
}
wait(for: [expectation], timeout: 1.0)

// Assert
XCTAssertTrue(mockLocalStorage.isNotificationsEnabled)
}
}

0 comments on commit 6f00e45

Please sign in to comment.