Skip to content

Commit eab7521

Browse files
evantk91megha-iterablejoaodordioEvan Greersumeruchat
committed
[MOB-9446] Enhance push notification state tracking in SDKs (#881)
Co-authored-by: Megha Pithadiya <megha.pithadiya@iterable.com> Co-authored-by: Joao Dordio <joaodordio@icloud.com> Co-authored-by: Evan Greer <evan.greer@evan.greer> Co-authored-by: Sumeru Chatterjee <sumeru.chatterjee@iterable.com>
1 parent 25c9ca0 commit eab7521

10 files changed

+159
-14
lines changed

swift-sdk.xcodeproj/project.pbxproj

+4
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
00B6FACE210E88ED007535CF /* prod-1.mobileprovision in Resources */ = {isa = PBXBuildFile; fileRef = 00B6FACD210E874D007535CF /* prod-1.mobileprovision */; };
1212
00B6FAD1210E8D90007535CF /* dev-1.mobileprovision in Resources */ = {isa = PBXBuildFile; fileRef = 00B6FAD0210E8D90007535CF /* dev-1.mobileprovision */; };
1313
00CB31B621096129004ACDEC /* TestUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 00CB31B4210960C4004ACDEC /* TestUtils.swift */; };
14+
092D01942D3038F600E3066A /* NotificationObserverTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 092D01932D3038F600E3066A /* NotificationObserverTests.swift */; };
1415
1CBFFE1A2A97AEEF00ED57EE /* EmbeddedManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1CBFFE162A97AEEE00ED57EE /* EmbeddedManagerTests.swift */; };
1516
1CBFFE1B2A97AEEF00ED57EE /* EmbeddedMessagingProcessorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1CBFFE172A97AEEE00ED57EE /* EmbeddedMessagingProcessorTests.swift */; };
1617
1CBFFE1C2A97AEEF00ED57EE /* EmbeddedSessionManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1CBFFE182A97AEEE00ED57EE /* EmbeddedSessionManagerTests.swift */; };
@@ -543,6 +544,7 @@
543544
00B6FACD210E874D007535CF /* prod-1.mobileprovision */ = {isa = PBXFileReference; lastKnownFileType = file; path = "prod-1.mobileprovision"; sourceTree = "<group>"; };
544545
00B6FAD0210E8D90007535CF /* dev-1.mobileprovision */ = {isa = PBXFileReference; lastKnownFileType = file; path = "dev-1.mobileprovision"; sourceTree = "<group>"; };
545546
00CB31B4210960C4004ACDEC /* TestUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestUtils.swift; sourceTree = "<group>"; };
547+
092D01932D3038F600E3066A /* NotificationObserverTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationObserverTests.swift; sourceTree = "<group>"; };
546548
1CBFFE162A97AEEE00ED57EE /* EmbeddedManagerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EmbeddedManagerTests.swift; sourceTree = "<group>"; };
547549
1CBFFE172A97AEEE00ED57EE /* EmbeddedMessagingProcessorTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EmbeddedMessagingProcessorTests.swift; sourceTree = "<group>"; };
548550
1CBFFE182A97AEEE00ED57EE /* EmbeddedSessionManagerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EmbeddedSessionManagerTests.swift; sourceTree = "<group>"; };
@@ -936,6 +938,7 @@
936938
552A0AA9280E249C00A80963 /* notification-tests */ = {
937939
isa = PBXGroup;
938940
children = (
941+
092D01932D3038F600E3066A /* NotificationObserverTests.swift */,
939942
55B37FC32297135F0042F13A /* NotificationMetadataTests.swift */,
940943
AC2C667F20D31B1F00D46CC9 /* NotificationResponseTests.swift */,
941944
);
@@ -2186,6 +2189,7 @@
21862189
5588DFD128C0465E000697D7 /* MockAPNSTypeChecker.swift in Sources */,
21872190
00B6FACC210E8484007535CF /* APNSTypeCheckerTests.swift in Sources */,
21882191
AC8F35A2239806B500302994 /* InboxViewControllerViewModelTests.swift in Sources */,
2192+
092D01942D3038F600E3066A /* NotificationObserverTests.swift in Sources */,
21892193
AC995F9A2166EEB50099A184 /* CommonMocks.swift in Sources */,
21902194
5588DFE128C046B7000697D7 /* MockLocalStorage.swift in Sources */,
21912195
1CBFFE1B2A97AEEF00ED57EE /* EmbeddedMessagingProcessorTests.swift in Sources */,

swift-sdk/Core/Constants.swift

+3-1
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,9 @@ enum Const {
5757
static let deviceId = "itbl_device_id"
5858
static let sdkVersion = "itbl_sdk_version"
5959
static let offlineMode = "itbl_offline_mode"
60-
60+
static let isNotificationsEnabled = "itbl_isNotificationsEnabled"
61+
static let hasStoredNotificationSetting = "itbl_hasStoredNotificationSetting"
62+
6163
static let attributionInfoExpiration = 24
6264
}
6365

swift-sdk/Internal/InternalIterableAPI.swift

+53-3
Original file line numberDiff line numberDiff line change
@@ -176,7 +176,7 @@ final class InternalIterableAPI: NSObject, PushTrackerProtocol, AuthProvider {
176176

177177
// MARK: - API Request Calls
178178

179-
func register(token: Data,
179+
func register(token: String,
180180
onSuccess: OnSuccessHandler? = nil,
181181
onFailure: OnFailureHandler? = nil) {
182182
guard let appName = pushIntegrationName else {
@@ -187,8 +187,8 @@ final class InternalIterableAPI: NSObject, PushTrackerProtocol, AuthProvider {
187187
return
188188
}
189189

190-
hexToken = token.hexString()
191-
let registerTokenInfo = RegisterTokenInfo(hexToken: token.hexString(),
190+
hexToken = token
191+
let registerTokenInfo = RegisterTokenInfo(hexToken: token,
192192
appName: appName,
193193
pushServicePlatform: config.pushPlatform,
194194
apnsType: dependencyContainer.apnsTypeChecker.apnsType,
@@ -208,6 +208,12 @@ final class InternalIterableAPI: NSObject, PushTrackerProtocol, AuthProvider {
208208
)
209209
}
210210

211+
func register(token: Data,
212+
onSuccess: OnSuccessHandler? = nil,
213+
onFailure: OnFailureHandler? = nil) {
214+
register(token: token.hexString(), onSuccess: onSuccess, onFailure: onFailure)
215+
}
216+
211217
@discardableResult
212218
func disableDeviceForCurrentUser(withOnSuccess onSuccess: OnSuccessHandler? = nil,
213219
onFailure: OnFailureHandler? = nil) -> Pending<SendRequestValue, SendRequestError> {
@@ -216,12 +222,18 @@ final class InternalIterableAPI: NSObject, PushTrackerProtocol, AuthProvider {
216222
onFailure?(errorMessage, nil)
217223
return SendRequestError.createErroredFuture(reason: errorMessage)
218224
}
225+
219226
guard userId != nil || email != nil else {
220227
let errorMessage = "either userId or email must be present"
221228
onFailure?(errorMessage, nil)
222229
return SendRequestError.createErroredFuture(reason: errorMessage)
223230
}
224231

232+
// We need to call register token here so that we can trigger the device registration
233+
// with the updated notification settings
234+
235+
register(token: hexToken)
236+
225237
return requestHandler.disableDeviceForCurrentUser(hexToken: hexToken, withOnSuccess: onSuccess, onFailure: onFailure)
226238
}
227239

@@ -500,6 +512,8 @@ final class InternalIterableAPI: NSObject, PushTrackerProtocol, AuthProvider {
500512
private var _userId: String?
501513
private var _successCallback: OnSuccessHandler? = nil
502514
private var _failureCallback: OnFailureHandler? = nil
515+
516+
private let notificationCenter: NotificationCenterProtocol
503517

504518

505519
/// the hex representation of this device token
@@ -666,6 +680,7 @@ final class InternalIterableAPI: NSObject, PushTrackerProtocol, AuthProvider {
666680
localStorage = dependencyContainer.localStorage
667681
inAppDisplayer = dependencyContainer.inAppDisplayer
668682
urlOpener = dependencyContainer.urlOpener
683+
notificationCenter = dependencyContainer.notificationCenter
669684
deepLinkManager = DeepLinkManager(redirectNetworkSessionProvider: dependencyContainer)
670685
}
671686

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

700715
checkRemoteConfiguration()
716+
717+
addForegroundObservers()
701718

702719
return inAppManager.start()
703720
}
704721

722+
private func addForegroundObservers() {
723+
notificationCenter.addObserver(self,
724+
selector: #selector(onAppDidBecomeActiveNotification(notification:)),
725+
name: UIApplication.didBecomeActiveNotification,
726+
object: nil)
727+
}
728+
729+
@objc private func onAppDidBecomeActiveNotification(notification: Notification) {
730+
guard config.autoPushRegistration else { return }
731+
732+
notificationStateProvider.isNotificationsEnabled { [weak self] systemEnabled in
733+
guard let self = self else { return }
734+
735+
let storedEnabled = self.localStorage.isNotificationsEnabled
736+
let hasStoredPermission = self.localStorage.hasStoredNotificationSetting
737+
738+
if self.isEitherUserIdOrEmailSet() {
739+
if hasStoredPermission && (storedEnabled != systemEnabled) {
740+
if !systemEnabled {
741+
self.disableDeviceForCurrentUser()
742+
} else {
743+
self.notificationStateProvider.registerForRemoteNotifications()
744+
}
745+
}
746+
747+
// Always store the current state
748+
self.localStorage.isNotificationsEnabled = systemEnabled
749+
self.localStorage.hasStoredNotificationSetting = true
750+
}
751+
}
752+
}
753+
705754
private func handle(launchOptions: [UIApplication.LaunchOptionsKey: Any]?) {
706755
guard let launchOptions = launchOptions else {
707756
return
@@ -772,6 +821,7 @@ final class InternalIterableAPI: NSObject, PushTrackerProtocol, AuthProvider {
772821

773822
deinit {
774823
ITBInfo()
824+
notificationCenter.removeObserver(self)
775825
requestHandler.stop()
776826
}
777827
}

swift-sdk/Internal/IterableUserDefaults.swift

+19-1
Original file line numberDiff line numberDiff line change
@@ -64,12 +64,28 @@ class IterableUserDefaults {
6464

6565
var offlineMode: Bool {
6666
get {
67-
return bool(withKey: .offlineMode)
67+
bool(withKey: .offlineMode)
6868
} set {
6969
save(bool: newValue, withKey: .offlineMode)
7070
}
7171
}
7272

73+
var isNotificationsEnabled: Bool {
74+
get {
75+
bool(withKey: .isNotificationsEnabled)
76+
} set {
77+
save(bool: newValue, withKey: .isNotificationsEnabled)
78+
}
79+
}
80+
81+
var hasStoredNotificationSetting: Bool {
82+
get {
83+
bool(withKey: .hasStoredNotificationSetting)
84+
} set {
85+
save(bool: newValue, withKey: .hasStoredNotificationSetting)
86+
}
87+
}
88+
7389
func getAttributionInfo(currentDate: Date) -> IterableAttributionInfo? {
7490
(try? codable(withKey: .attributionInfo, currentDate: currentDate)) ?? nil
7591
}
@@ -196,6 +212,8 @@ class IterableUserDefaults {
196212
static let deviceId = UserDefaultsKey(value: Const.UserDefault.deviceId)
197213
static let sdkVersion = UserDefaultsKey(value: Const.UserDefault.sdkVersion)
198214
static let offlineMode = UserDefaultsKey(value: Const.UserDefault.offlineMode)
215+
static let isNotificationsEnabled = UserDefaultsKey(value: Const.UserDefault.isNotificationsEnabled)
216+
static let hasStoredNotificationSetting = UserDefaultsKey(value: Const.UserDefault.hasStoredNotificationSetting)
199217
}
200218

201219
private struct Envelope: Codable {

swift-sdk/Internal/Utilities/LocalStorage.swift

+16
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,22 @@ struct LocalStorage: LocalStorageProtocol {
6767
}
6868
}
6969

70+
var isNotificationsEnabled: Bool {
71+
get {
72+
iterableUserDefaults.isNotificationsEnabled
73+
} set {
74+
iterableUserDefaults.isNotificationsEnabled = newValue
75+
}
76+
}
77+
78+
var hasStoredNotificationSetting: Bool {
79+
get {
80+
iterableUserDefaults.hasStoredNotificationSetting
81+
} set {
82+
iterableUserDefaults.hasStoredNotificationSetting = newValue
83+
}
84+
}
85+
7086
func getAttributionInfo(currentDate: Date) -> IterableAttributionInfo? {
7187
iterableUserDefaults.getAttributionInfo(currentDate: currentDate)
7288
}

swift-sdk/Internal/Utilities/LocalStorageProtocol.swift

+4
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@ protocol LocalStorageProtocol {
1919

2020
var offlineMode: Bool { get set }
2121

22+
var isNotificationsEnabled: Bool { get set }
23+
24+
var hasStoredNotificationSetting: Bool { get set }
25+
2226
func getAttributionInfo(currentDate: Date) -> IterableAttributionInfo?
2327

2428
func save(attributionInfo: IterableAttributionInfo?, withExpiration expiration: Date?)

tests/common/MockLocalStorage.swift

+4
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,10 @@ class MockLocalStorage: LocalStorageProtocol {
2121

2222
var offlineMode: Bool = false
2323

24+
var isNotificationsEnabled: Bool = false
25+
26+
var hasStoredNotificationSetting: Bool = false
27+
2428
func getAttributionInfo(currentDate: Date) -> IterableAttributionInfo? {
2529
guard !MockLocalStorage.isExpired(expiration: attributionInfoExpiration, currentDate: currentDate) else {
2630
return nil

tests/unit-tests/AutoRegistrationTests.swift

+1
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ class AutoRegistrationTests: XCTestCase {
2020

2121
func testCallDisableAndEnable() {
2222
let expectation1 = expectation(description: "call register device API")
23+
expectation1.expectedFulfillmentCount = 2
2324
let expectation2 = expectation(description: "call registerForRemoteNotifications twice")
2425
expectation2.expectedFulfillmentCount = 2
2526
let expectation3 = expectation(description: "call disable on user1@example.com")

tests/unit-tests/Mocks.swift

+9-9
Original file line numberDiff line numberDiff line change
@@ -10,19 +10,19 @@ import XCTest
1010

1111
// Note: This is used only by swift tests. So can't put this in Common
1212
class MockNotificationStateProvider: NotificationStateProviderProtocol {
13-
func isNotificationsEnabled(withCallback callback: @escaping (Bool) -> Void) {
14-
callback(enabled)
15-
}
16-
17-
func registerForRemoteNotifications() {
18-
expectation?.fulfill()
19-
}
13+
var enabled: Bool
14+
private let expectation: XCTestExpectation?
2015

2116
init(enabled: Bool, expectation: XCTestExpectation? = nil) {
2217
self.enabled = enabled
2318
self.expectation = expectation
2419
}
2520

26-
private let enabled: Bool
27-
private let expectation: XCTestExpectation?
21+
func isNotificationsEnabled(withCallback callback: @escaping (Bool) -> Void) {
22+
callback(enabled)
23+
}
24+
25+
func registerForRemoteNotifications() {
26+
expectation?.fulfill()
27+
}
2828
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import XCTest
2+
@testable import IterableSDK
3+
4+
class NotificationObserverTests: XCTestCase {
5+
private var internalAPI: InternalIterableAPI!
6+
private var mockNotificationStateProvider: MockNotificationStateProvider!
7+
private var mockLocalStorage: MockLocalStorage!
8+
private var mockNotificationCenter: MockNotificationCenter!
9+
10+
override func setUp() {
11+
super.setUp()
12+
13+
mockNotificationStateProvider = MockNotificationStateProvider(enabled: false)
14+
mockLocalStorage = MockLocalStorage()
15+
mockNotificationCenter = MockNotificationCenter()
16+
17+
let config = IterableConfig()
18+
internalAPI = InternalIterableAPI.initializeForTesting(
19+
config: config,
20+
notificationStateProvider: mockNotificationStateProvider,
21+
localStorage: mockLocalStorage,
22+
notificationCenter: mockNotificationCenter
23+
)
24+
}
25+
26+
func testNotificationStateChangeUpdatesStorage() {
27+
// Arrange
28+
internalAPI.email = "johnappleseed@iterable.com"
29+
30+
mockLocalStorage.isNotificationsEnabled = false
31+
mockNotificationStateProvider.enabled = true
32+
33+
// Act
34+
mockNotificationCenter.post(name: UIApplication.didBecomeActiveNotification, object: nil, userInfo: nil)
35+
36+
// Small delay to allow async operation to complete
37+
let expectation = XCTestExpectation(description: "Wait for state update")
38+
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
39+
expectation.fulfill()
40+
}
41+
wait(for: [expectation], timeout: 1.0)
42+
43+
// Assert
44+
XCTAssertTrue(mockLocalStorage.isNotificationsEnabled)
45+
}
46+
}

0 commit comments

Comments
 (0)