From 818e92f53c3d311248084fe5c956d2aad2f200b3 Mon Sep 17 00:00:00 2001 From: Sumeru Chatterjee Date: Tue, 24 Jun 2025 16:14:48 +0100 Subject: [PATCH 1/7] MOB-11622: Fix InApp messages not displaying in active window with iOS 13+ multi-window support - Add iOS 13+ multi-window scene detection to IterableUtil.rootViewController - Prioritize foregroundActive scenes and key windows for InApp display - Maintain backward compatibility with iOS 12 and single-window apps - Fixes Stage Manager and multi-window scenarios on iPad --- .../Internal/Utilities/IterableUtil.swift | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/swift-sdk/Internal/Utilities/IterableUtil.swift b/swift-sdk/Internal/Utilities/IterableUtil.swift index 7f1eaef42..7431821fd 100644 --- a/swift-sdk/Internal/Utilities/IterableUtil.swift +++ b/swift-sdk/Internal/Utilities/IterableUtil.swift @@ -8,6 +8,14 @@ import UIKit @objc final class IterableUtil: NSObject { static var rootViewController: UIViewController? { + // Try modern approach first for iOS 13+ multi-window support + if #available(iOS 13.0, *) { + if let activeViewController = getActiveWindowRootViewController() { + return activeViewController + } + } + + // Existing fallback chain - unchanged for backward compatibility if let rootViewController = AppExtensionHelper.application?.delegate?.window??.rootViewController { return rootViewController } else { @@ -15,6 +23,44 @@ import UIKit } } + @available(iOS 13.0, *) + private static func getActiveWindowRootViewController() -> UIViewController? { + guard let application = AppExtensionHelper.application else { return nil } + + // Find active scenes (foreground active takes priority) + let activeScenes = application.connectedScenes + .compactMap { $0 as? UIWindowScene } + .filter { $0.activationState == .foregroundActive } + + // Look for key window in active scenes first + for scene in activeScenes { + if let keyWindow = scene.windows.first(where: { $0.isKeyWindow }), + let rootVC = keyWindow.rootViewController { + return rootVC + } + } + + // Fallback to first window in first active scene + if let firstActiveScene = activeScenes.first, + let rootVC = firstActiveScene.windows.first?.rootViewController { + return rootVC + } + + // Final fallback: any foreground inactive scene with key window + let inactiveScenes = application.connectedScenes + .compactMap { $0 as? UIWindowScene } + .filter { $0.activationState == .foregroundInactive } + + for scene in inactiveScenes { + if let keyWindow = scene.windows.first(where: { $0.isKeyWindow }), + let rootVC = keyWindow.rootViewController { + return rootVC + } + } + + return nil + } + static func trim(string: String) -> String { string.trimmingCharacters(in: .whitespaces) } From bc23f41cfd716c58358dccb0aa1a9e49cf115c49 Mon Sep 17 00:00:00 2001 From: Sumeru Chatterjee Date: Wed, 2 Jul 2025 16:59:40 +0100 Subject: [PATCH 2/7] Add claude to gitignore --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index 6a43ee614..51fc14ca2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,8 @@ .DS_Store xcuserdata +.claude + .swiftpm/ *~ From 7603dc21d2a579393991ec29f9db5534b328c87d Mon Sep 17 00:00:00 2001 From: Sumeru Chatterjee Date: Wed, 2 Jul 2025 18:01:58 +0100 Subject: [PATCH 3/7] MOB-11622: Fix iOS 15+ keyWindow reliability issue - Use scene.keyWindow property on iOS 15+ instead of isKeyWindow filter - isKeyWindow can return false for all windows on iOS 15+ with Stage Manager - Maintain iOS 13-14 compatibility with existing isKeyWindow approach - Resolves window detection issues in multi-window scenarios --- .../Internal/Utilities/IterableUtil.swift | 39 ++++++++++++++----- 1 file changed, 29 insertions(+), 10 deletions(-) diff --git a/swift-sdk/Internal/Utilities/IterableUtil.swift b/swift-sdk/Internal/Utilities/IterableUtil.swift index 7431821fd..82a8d8a0e 100644 --- a/swift-sdk/Internal/Utilities/IterableUtil.swift +++ b/swift-sdk/Internal/Utilities/IterableUtil.swift @@ -32,11 +32,21 @@ import UIKit .compactMap { $0 as? UIWindowScene } .filter { $0.activationState == .foregroundActive } - // Look for key window in active scenes first - for scene in activeScenes { - if let keyWindow = scene.windows.first(where: { $0.isKeyWindow }), - let rootVC = keyWindow.rootViewController { - return rootVC + // iOS 15+: Use scene's keyWindow property (preferred approach) + if #available(iOS 15.0, *) { + for scene in activeScenes { + if let keyWindow = scene.keyWindow, + let rootVC = keyWindow.rootViewController { + return rootVC + } + } + } else { + // iOS 13-14: Fall back to isKeyWindow check + for scene in activeScenes { + if let keyWindow = scene.windows.first(where: { $0.isKeyWindow }), + let rootVC = keyWindow.rootViewController { + return rootVC + } } } @@ -46,15 +56,24 @@ import UIKit return rootVC } - // Final fallback: any foreground inactive scene with key window + // Final fallback: any foreground inactive scene let inactiveScenes = application.connectedScenes .compactMap { $0 as? UIWindowScene } .filter { $0.activationState == .foregroundInactive } - for scene in inactiveScenes { - if let keyWindow = scene.windows.first(where: { $0.isKeyWindow }), - let rootVC = keyWindow.rootViewController { - return rootVC + if #available(iOS 15.0, *) { + for scene in inactiveScenes { + if let keyWindow = scene.keyWindow, + let rootVC = keyWindow.rootViewController { + return rootVC + } + } + } else { + for scene in inactiveScenes { + if let keyWindow = scene.windows.first(where: { $0.isKeyWindow }), + let rootVC = keyWindow.rootViewController { + return rootVC + } } } From 3a0eeffb9f9c4ecb482a5110f18ecbc0890a69c4 Mon Sep 17 00:00:00 2001 From: Sumeru Chatterjee Date: Fri, 18 Jul 2025 16:28:31 +0100 Subject: [PATCH 4/7] Add debug loggimng --- .../Internal/Utilities/IterableUtil.swift | 37 ++++++++++++++++--- 1 file changed, 32 insertions(+), 5 deletions(-) diff --git a/swift-sdk/Internal/Utilities/IterableUtil.swift b/swift-sdk/Internal/Utilities/IterableUtil.swift index 82a8d8a0e..16767c576 100644 --- a/swift-sdk/Internal/Utilities/IterableUtil.swift +++ b/swift-sdk/Internal/Utilities/IterableUtil.swift @@ -25,27 +25,43 @@ import UIKit @available(iOS 13.0, *) private static func getActiveWindowRootViewController() -> UIViewController? { - guard let application = AppExtensionHelper.application else { return nil } + guard let application = AppExtensionHelper.application else { + ITBDebug("No application found in AppExtensionHelper") + return nil + } + + ITBDebug("Application has \(application.connectedScenes.count) connected scenes") // Find active scenes (foreground active takes priority) let activeScenes = application.connectedScenes .compactMap { $0 as? UIWindowScene } .filter { $0.activationState == .foregroundActive } + ITBDebug("Found \(activeScenes.count) foreground active scenes") + // iOS 15+: Use scene's keyWindow property (preferred approach) if #available(iOS 15.0, *) { - for scene in activeScenes { + for (index, scene) in activeScenes.enumerated() { + ITBDebug("Checking scene \(index): keyWindow exists = \(scene.keyWindow != nil)") if let keyWindow = scene.keyWindow, let rootVC = keyWindow.rootViewController { + ITBDebug("Found root view controller in keyWindow: \(String(describing: rootVC))") return rootVC + } else { + ITBDebug("Scene \(index): keyWindow or rootVC is nil") } } } else { // iOS 13-14: Fall back to isKeyWindow check - for scene in activeScenes { + for (index, scene) in activeScenes.enumerated() { + let keyWindows = scene.windows.filter { $0.isKeyWindow } + ITBDebug("Scene \(index): found \(keyWindows.count) key windows") if let keyWindow = scene.windows.first(where: { $0.isKeyWindow }), let rootVC = keyWindow.rootViewController { + ITBDebug("Found root view controller in keyWindow (iOS 13-14): \(String(describing: rootVC))") return rootVC + } else { + ITBDebug("Scene \(index): no keyWindow with rootVC found") } } } @@ -53,7 +69,10 @@ import UIKit // Fallback to first window in first active scene if let firstActiveScene = activeScenes.first, let rootVC = firstActiveScene.windows.first?.rootViewController { + ITBDebug("Fallback: Found root view controller in first window of first active scene: \(String(describing: rootVC))") return rootVC + } else { + ITBDebug("Fallback: No root view controller found in first window of first active scene") } // Final fallback: any foreground inactive scene @@ -61,22 +80,30 @@ import UIKit .compactMap { $0 as? UIWindowScene } .filter { $0.activationState == .foregroundInactive } + ITBDebug("Checking \(inactiveScenes.count) foreground inactive scenes as final fallback") + if #available(iOS 15.0, *) { - for scene in inactiveScenes { + for (index, scene) in inactiveScenes.enumerated() { + ITBDebug("Inactive scene \(index): keyWindow exists = \(scene.keyWindow != nil)") if let keyWindow = scene.keyWindow, let rootVC = keyWindow.rootViewController { + ITBDebug("Final fallback: Found root view controller in inactive scene keyWindow: \(String(describing: rootVC))") return rootVC } } } else { - for scene in inactiveScenes { + for (index, scene) in inactiveScenes.enumerated() { + let keyWindows = scene.windows.filter { $0.isKeyWindow } + ITBDebug("Inactive scene \(index): found \(keyWindows.count) key windows") if let keyWindow = scene.windows.first(where: { $0.isKeyWindow }), let rootVC = keyWindow.rootViewController { + ITBDebug("Final fallback: Found root view controller in inactive scene keyWindow (iOS 13-14): \(String(describing: rootVC))") return rootVC } } } + ITBDebug("No root view controller found in any scene") return nil } From 23467b3168a3084d5687dce8ac26be5dc77daa1a Mon Sep 17 00:00:00 2001 From: Sumeru Chatterjee Date: Tue, 22 Jul 2025 15:02:47 +0100 Subject: [PATCH 5/7] Compilation fix --- .../Internal/Utilities/IterableUtil.swift | 69 ------------------- 1 file changed, 69 deletions(-) diff --git a/swift-sdk/Internal/Utilities/IterableUtil.swift b/swift-sdk/Internal/Utilities/IterableUtil.swift index 2fef5a7b1..16767c576 100644 --- a/swift-sdk/Internal/Utilities/IterableUtil.swift +++ b/swift-sdk/Internal/Utilities/IterableUtil.swift @@ -191,73 +191,4 @@ import UIKit return true } } - - @available(iOS 13.0, *) - private static func getActiveWindowRootViewController() -> UIViewController? { - guard let application = AppExtensionHelper.application else { return nil } - - // Prioritize foregroundActive scenes for Stage Manager and multi-window scenarios - for scene in application.connectedScenes { - guard let windowScene = scene as? UIWindowScene, - windowScene.activationState == .foregroundActive else { continue } - - // Filter for visible, normal-level windows with valid rootViewControllers - let visibleWindows = windowScene.windows.filter { - $0.isHidden == false && - $0.windowLevel == .normal && - $0.rootViewController != nil - } - - // Try keyWindow first if available (iOS 15+) and in our visible set - if #available(iOS 15.0, *) { - if let keyWindow = windowScene.keyWindow, - visibleWindows.contains(keyWindow) { - return keyWindow.rootViewController - } - } - - // For iOS 13-14, find keyWindow manually in visible windows - for window in visibleWindows { - if window.isKeyWindow { - return window.rootViewController - } - } - - // Fallback to first visible, normal window - if let firstVisibleWindow = visibleWindows.first { - return firstVisibleWindow.rootViewController - } - } - - // Secondary fallback: any foregroundInactive scene (Stage Manager background) - for scene in application.connectedScenes { - guard let windowScene = scene as? UIWindowScene, - windowScene.activationState == .foregroundInactive else { continue } - - let visibleWindows = windowScene.windows.filter { - $0.isHidden == false && - $0.windowLevel == .normal && - $0.rootViewController != nil - } - - if #available(iOS 15.0, *) { - if let keyWindow = windowScene.keyWindow, - visibleWindows.contains(keyWindow) { - return keyWindow.rootViewController - } - } - - for window in visibleWindows { - if window.isKeyWindow { - return window.rootViewController - } - } - - if let firstVisibleWindow = visibleWindows.first { - return firstVisibleWindow.rootViewController - } - } - - return nil - } } From a44f38efe0103f963e386359a9691839a78e1fd7 Mon Sep 17 00:00:00 2001 From: Joao Dordio Date: Wed, 23 Jul 2025 14:17:42 +0100 Subject: [PATCH 6/7] =?UTF-8?q?=E2=9C=A8Added=20IterableInAppDisplayDelega?= =?UTF-8?q?te?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../DependencyContainerProtocol.swift | 1 + .../Internal/in-app/InAppDisplayer.swift | 35 ++++++++++++++---- swift-sdk/Internal/in-app/InAppManager.swift | 8 +++- .../Internal/in-app/InAppPresenter.swift | 37 +++++++++++++++---- swift-sdk/SDK/IterableConfig.swift | 25 +++++++++++++ tests/unit-tests/InAppPresenterTests.swift | 18 +++++---- 6 files changed, 101 insertions(+), 23 deletions(-) diff --git a/swift-sdk/Internal/Utilities/DependencyContainerProtocol.swift b/swift-sdk/Internal/Utilities/DependencyContainerProtocol.swift index f288e3170..c432ec399 100644 --- a/swift-sdk/Internal/Utilities/DependencyContainerProtocol.swift +++ b/swift-sdk/Internal/Utilities/DependencyContainerProtocol.swift @@ -39,6 +39,7 @@ extension DependencyContainerProtocol { displayer: inAppDisplayer, persister: inAppPersister, inAppDelegate: config.inAppDelegate, + inAppDisplayDelegate: config.inAppDisplayDelegate, urlDelegate: config.urlDelegate, customActionDelegate: config.customActionDelegate, urlOpener: urlOpener, diff --git a/swift-sdk/Internal/in-app/InAppDisplayer.swift b/swift-sdk/Internal/in-app/InAppDisplayer.swift index a3e4bc134..adafef9bd 100644 --- a/swift-sdk/Internal/in-app/InAppDisplayer.swift +++ b/swift-sdk/Internal/in-app/InAppDisplayer.swift @@ -44,12 +44,13 @@ class InAppDisplayer: InAppDisplayerProtocol { return .notShown("In-app notification is being presented.") } - guard let topViewController = getTopViewController() else { + // Initial basic checks - detailed checks will be done after delay + guard InAppDisplayer.getTopViewController() != nil else { return .notShown("No top view controller.") } - if topViewController is IterableHtmlMessageViewController { - return .notShown("Skipping the in-app notification. Another notification is already being displayed.") + guard let message = messageMetadata?.message else { + return .notShown("No message available for display validation.") } let parameters = IterableHtmlMessageViewController.Parameters(html: htmlString, @@ -58,11 +59,31 @@ class InAppDisplayer: InAppDisplayerProtocol { isModal: true) let htmlMessageVC = IterableHtmlMessageViewController.create(parameters: parameters, onClickCallback: onClickCallback) - topViewController.definesPresentationContext = true - htmlMessageVC.modalPresentationStyle = .overFullScreen - let presenter = InAppPresenter(topViewController: topViewController, htmlMessageViewController: htmlMessageVC) + // Create display validator that will be called after delay + let displayValidator: () -> Bool = { + // Check if another in-app is being presented + guard !InAppPresenter.isPresenting else { + return false + } + + // Check if we can get a top view controller + guard let topViewController = InAppDisplayer.getTopViewController() else { + return false + } + + // Check if another Iterable message is already displayed + guard !(topViewController is IterableHtmlMessageViewController) else { + return false + } + + return true + } + + let presenter = InAppPresenter(htmlMessageViewController: htmlMessageVC, + message: message, + displayValidator: displayValidator) presenter.show() return .shown @@ -81,7 +102,7 @@ class InAppDisplayer: InAppDisplayerProtocol { return topViewController is IterableHtmlMessageViewController } - private static func getTopViewController() -> UIViewController? { + static func getTopViewController() -> UIViewController? { guard let rootViewController = IterableUtil.rootViewController else { return nil } diff --git a/swift-sdk/Internal/in-app/InAppManager.swift b/swift-sdk/Internal/in-app/InAppManager.swift index c8cc328cd..5a3a52430 100644 --- a/swift-sdk/Internal/in-app/InAppManager.swift +++ b/swift-sdk/Internal/in-app/InAppManager.swift @@ -43,6 +43,7 @@ class InAppManager: NSObject, IterableInternalInAppManagerProtocol { displayer: InAppDisplayerProtocol, persister: InAppPersistenceProtocol, inAppDelegate: IterableInAppDelegate, + inAppDisplayDelegate: IterableInAppDisplayDelegate?, urlDelegate: IterableURLDelegate?, customActionDelegate: IterableCustomActionDelegate?, urlOpener: UrlOpenerProtocol, @@ -59,6 +60,7 @@ class InAppManager: NSObject, IterableInternalInAppManagerProtocol { self.displayer = displayer self.persister = persister self.inAppDelegate = inAppDelegate + self.inAppDisplayDelegate = inAppDisplayDelegate self.urlDelegate = urlDelegate self.customActionDelegate = customActionDelegate self.urlOpener = urlOpener @@ -555,6 +557,7 @@ class InAppManager: NSObject, IterableInternalInAppManagerProtocol { private let fetcher: InAppFetcherProtocol private let displayer: InAppDisplayerProtocol private let inAppDelegate: IterableInAppDelegate + private let inAppDisplayDelegate: IterableInAppDisplayDelegate? private let urlDelegate: IterableURLDelegate? private let customActionDelegate: IterableCustomActionDelegate? private let urlOpener: UrlOpenerProtocol @@ -654,7 +657,10 @@ extension InAppManager: InAppNotifiable { extension InAppManager: InAppDisplayChecker { func isOkToShowNow(message: IterableInAppMessage) -> Bool { - guard !isAutoDisplayPaused else { + // Check delegate first if available, otherwise fall back to isAutoDisplayPaused property + let autoDisplayPaused = inAppDisplayDelegate?.isAutoDisplayPaused?(for: message) ?? isAutoDisplayPaused + + guard !autoDisplayPaused else { ITBInfo("automatic in-app display has been paused") return false } diff --git a/swift-sdk/Internal/in-app/InAppPresenter.swift b/swift-sdk/Internal/in-app/InAppPresenter.swift index 03938e2bf..5d7c37854 100644 --- a/swift-sdk/Internal/in-app/InAppPresenter.swift +++ b/swift-sdk/Internal/in-app/InAppPresenter.swift @@ -8,16 +8,20 @@ class InAppPresenter { static var isPresenting = false private let maxDelay: TimeInterval - - private let topViewController: UIViewController private let htmlMessageViewController: IterableHtmlMessageViewController + private let message: IterableInAppMessage + private let displayValidator: () -> Bool private var delayTimer: Timer? - init(topViewController: UIViewController, htmlMessageViewController: IterableHtmlMessageViewController, maxDelay: TimeInterval = 0.75) { + init(htmlMessageViewController: IterableHtmlMessageViewController, + message: IterableInAppMessage, + displayValidator: @escaping () -> Bool, + maxDelay: TimeInterval = 0.75) { ITBInfo() - self.topViewController = topViewController self.htmlMessageViewController = htmlMessageViewController + self.message = message + self.displayValidator = displayValidator self.maxDelay = maxDelay // shouldn't be necessary, but in case there's some kind of race condition @@ -41,7 +45,7 @@ class InAppPresenter { ITBInfo("delayTimer called") self.delayTimer = nil - self.present() + self.presentAfterDelayValidation() } } } @@ -55,15 +59,34 @@ class InAppPresenter { delayTimer?.invalidate() delayTimer = nil - present() + presentAfterDelayValidation() } } - private func present() { + private func presentAfterDelayValidation() { ITBInfo() InAppPresenter.isPresenting = false + // Re-validate display conditions after delay + guard displayValidator() else { + ITBInfo("Display validation failed after delay - not showing message") + return + } + + // Get top view controller at presentation time, not at construction time + guard let topViewController = InAppDisplayer.getTopViewController() else { + ITBInfo("No top view controller available after delay") + return + } + + if topViewController is IterableHtmlMessageViewController { + ITBInfo("Another Iterable message is already being displayed") + return + } + + topViewController.definesPresentationContext = true + topViewController.present(htmlMessageViewController, animated: false) htmlMessageViewController.presenter = nil diff --git a/swift-sdk/SDK/IterableConfig.swift b/swift-sdk/SDK/IterableConfig.swift index 62f89c14e..4ff06ce51 100644 --- a/swift-sdk/SDK/IterableConfig.swift +++ b/swift-sdk/SDK/IterableConfig.swift @@ -4,6 +4,25 @@ import Foundation +/// Protocol to provide dynamic control over in-app message display timing. +/// This delegate allows the application to determine at the exact moment +/// whether an in-app message should be displayed, providing more granular +/// control than the static `isAutoDisplayPaused` property. +@objc public protocol IterableInAppDisplayDelegate { + + /// Called to determine if in-app messages should be automatically displayed at this moment. + /// This method is called just before an in-app message would be shown, allowing the app + /// to make a real-time decision based on current app state. + /// + /// - Parameter message: The in-app message that is about to be displayed + /// - Returns: `true` if automatic display should be paused (message will not show), `false` if display should proceed + /// + /// - Note: This method is called in addition to other display checks. If this returns `true`, + /// the message will not be shown regardless of other conditions. + /// - Note: If this delegate is not set, the default behavior uses the `isAutoDisplayPaused` property. + @objc optional func isAutoDisplayPaused(for message: IterableInAppMessage) -> Bool +} + public enum IterableAPIMobileFrameworkType: String, Codable { case flutter = "flutter" case reactNative = "reactnative" @@ -126,6 +145,12 @@ public class IterableConfig: NSObject { /// If more than 1 in-app is available, we show the first. public var inAppDelegate: IterableInAppDelegate = DefaultInAppDelegate() + /// Implement this protocol to provide dynamic control over when in-app messages can be displayed. + /// This delegate allows real-time decision making about display timing, providing more granular + /// control than the static `isAutoDisplayPaused` property. If not set, the SDK will fall back + /// to using the `isAutoDisplayPaused` property. + public var inAppDisplayDelegate: IterableInAppDisplayDelegate? + /// How many seconds to wait before showing the next in-app, if there are more than one present public var inAppDisplayInterval: Double = 30.0 diff --git a/tests/unit-tests/InAppPresenterTests.swift b/tests/unit-tests/InAppPresenterTests.swift index a152aab86..5e39d311c 100644 --- a/tests/unit-tests/InAppPresenterTests.swift +++ b/tests/unit-tests/InAppPresenterTests.swift @@ -12,8 +12,9 @@ class InAppPresenterTests: XCTestCase { func testInAppPresenterDelegateExistence() { let htmlMessageViewController = IterableHtmlMessageViewController.create(parameters: getEmptyParameters(), onClickCallback: nil) - let inAppPresenter = InAppPresenter(topViewController: UIViewController(), - htmlMessageViewController: htmlMessageViewController) + let inAppPresenter = InAppPresenter(htmlMessageViewController: htmlMessageViewController, + message: getEmptyInAppMessage(), + displayValidator: { true }) // a "no-op" to suppress warning _ = inAppPresenter.self @@ -22,19 +23,20 @@ class InAppPresenterTests: XCTestCase { } func testInAppPresenterIsPresentingOnInit() { - _ = InAppPresenter(topViewController: UIViewController(), - htmlMessageViewController: getEmptyHtmlMessageViewController()) + _ = InAppPresenter(htmlMessageViewController: getEmptyHtmlMessageViewController(), + message: getEmptyInAppMessage(), + displayValidator: { true }) XCTAssertFalse(InAppPresenter.isPresenting) } - func testInAppPresenterTimerFinished() { + func testInAppPresenterShowMethod() { let expectation1 = expectation(description: "delay timer executed") - let topViewController = UIViewController() let maxDelay = 0.75 - let inAppPresenter = InAppPresenter(topViewController: topViewController, - htmlMessageViewController: getEmptyHtmlMessageViewController(), + let inAppPresenter = InAppPresenter(htmlMessageViewController: getEmptyHtmlMessageViewController(), + message: getEmptyInAppMessage(), + displayValidator: { true }, maxDelay: maxDelay) inAppPresenter.show() From 13e727f956fe501a1e8bd6a06766d413f4025ee0 Mon Sep 17 00:00:00 2001 From: Joao Dordio Date: Thu, 7 Aug 2025 16:23:58 +0100 Subject: [PATCH 7/7] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Removed=20duplicated?= =?UTF-8?q?=20logic?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Internal/in-app/InAppDisplayer.swift | 23 +------------------ .../Internal/in-app/InAppPresenter.swift | 10 -------- tests/unit-tests/InAppPresenterTests.swift | 7 ++---- 3 files changed, 3 insertions(+), 37 deletions(-) diff --git a/swift-sdk/Internal/in-app/InAppDisplayer.swift b/swift-sdk/Internal/in-app/InAppDisplayer.swift index adafef9bd..f1803229e 100644 --- a/swift-sdk/Internal/in-app/InAppDisplayer.swift +++ b/swift-sdk/Internal/in-app/InAppDisplayer.swift @@ -61,29 +61,8 @@ class InAppDisplayer: InAppDisplayerProtocol { htmlMessageVC.modalPresentationStyle = .overFullScreen - // Create display validator that will be called after delay - let displayValidator: () -> Bool = { - // Check if another in-app is being presented - guard !InAppPresenter.isPresenting else { - return false - } - - // Check if we can get a top view controller - guard let topViewController = InAppDisplayer.getTopViewController() else { - return false - } - - // Check if another Iterable message is already displayed - guard !(topViewController is IterableHtmlMessageViewController) else { - return false - } - - return true - } + let presenter = InAppPresenter(htmlMessageViewController: htmlMessageVC, message: message) - let presenter = InAppPresenter(htmlMessageViewController: htmlMessageVC, - message: message, - displayValidator: displayValidator) presenter.show() return .shown diff --git a/swift-sdk/Internal/in-app/InAppPresenter.swift b/swift-sdk/Internal/in-app/InAppPresenter.swift index 5d7c37854..eec90b062 100644 --- a/swift-sdk/Internal/in-app/InAppPresenter.swift +++ b/swift-sdk/Internal/in-app/InAppPresenter.swift @@ -10,18 +10,15 @@ class InAppPresenter { private let maxDelay: TimeInterval private let htmlMessageViewController: IterableHtmlMessageViewController private let message: IterableInAppMessage - private let displayValidator: () -> Bool private var delayTimer: Timer? init(htmlMessageViewController: IterableHtmlMessageViewController, message: IterableInAppMessage, - displayValidator: @escaping () -> Bool, maxDelay: TimeInterval = 0.75) { ITBInfo() self.htmlMessageViewController = htmlMessageViewController self.message = message - self.displayValidator = displayValidator self.maxDelay = maxDelay // shouldn't be necessary, but in case there's some kind of race condition @@ -68,13 +65,6 @@ class InAppPresenter { InAppPresenter.isPresenting = false - // Re-validate display conditions after delay - guard displayValidator() else { - ITBInfo("Display validation failed after delay - not showing message") - return - } - - // Get top view controller at presentation time, not at construction time guard let topViewController = InAppDisplayer.getTopViewController() else { ITBInfo("No top view controller available after delay") return diff --git a/tests/unit-tests/InAppPresenterTests.swift b/tests/unit-tests/InAppPresenterTests.swift index 5e39d311c..cec8399a8 100644 --- a/tests/unit-tests/InAppPresenterTests.swift +++ b/tests/unit-tests/InAppPresenterTests.swift @@ -13,8 +13,7 @@ class InAppPresenterTests: XCTestCase { let htmlMessageViewController = IterableHtmlMessageViewController.create(parameters: getEmptyParameters(), onClickCallback: nil) let inAppPresenter = InAppPresenter(htmlMessageViewController: htmlMessageViewController, - message: getEmptyInAppMessage(), - displayValidator: { true }) + message: getEmptyInAppMessage()) // a "no-op" to suppress warning _ = inAppPresenter.self @@ -24,8 +23,7 @@ class InAppPresenterTests: XCTestCase { func testInAppPresenterIsPresentingOnInit() { _ = InAppPresenter(htmlMessageViewController: getEmptyHtmlMessageViewController(), - message: getEmptyInAppMessage(), - displayValidator: { true }) + message: getEmptyInAppMessage()) XCTAssertFalse(InAppPresenter.isPresenting) } @@ -36,7 +34,6 @@ class InAppPresenterTests: XCTestCase { let maxDelay = 0.75 let inAppPresenter = InAppPresenter(htmlMessageViewController: getEmptyHtmlMessageViewController(), message: getEmptyInAppMessage(), - displayValidator: { true }, maxDelay: maxDelay) inAppPresenter.show()