Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[MOB-9446] Enhance push notification state tracking in SDKs #881

Merged
merged 18 commits into from
Jan 14, 2025
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
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)
}
}
Loading