Skip to content

MOB-11657 Issues with timing of in app message display #925

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

Open
wants to merge 13 commits into
base: master
Choose a base branch
from
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ extension DependencyContainerProtocol {
displayer: inAppDisplayer,
persister: inAppPersister,
inAppDelegate: config.inAppDelegate,
inAppDisplayDelegate: config.inAppDisplayDelegate,
urlDelegate: config.urlDelegate,
customActionDelegate: config.customActionDelegate,
urlOpener: urlOpener,
Expand Down
14 changes: 7 additions & 7 deletions swift-sdk/Internal/in-app/InAppDisplayer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -58,11 +59,10 @@ 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)
let presenter = InAppPresenter(htmlMessageViewController: htmlMessageVC, message: message)

presenter.show()

return .shown
Expand All @@ -81,7 +81,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
}
Expand Down
8 changes: 7 additions & 1 deletion swift-sdk/Internal/in-app/InAppManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ class InAppManager: NSObject, IterableInternalInAppManagerProtocol {
displayer: InAppDisplayerProtocol,
persister: InAppPersistenceProtocol,
inAppDelegate: IterableInAppDelegate,
inAppDisplayDelegate: IterableInAppDisplayDelegate?,
urlDelegate: IterableURLDelegate?,
customActionDelegate: IterableCustomActionDelegate?,
urlOpener: UrlOpenerProtocol,
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
}
Expand Down
27 changes: 20 additions & 7 deletions swift-sdk/Internal/in-app/InAppPresenter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,17 @@ class InAppPresenter {
static var isPresenting = false

private let maxDelay: TimeInterval

private let topViewController: UIViewController
private let htmlMessageViewController: IterableHtmlMessageViewController
private let message: IterableInAppMessage
private var delayTimer: Timer?

init(topViewController: UIViewController, htmlMessageViewController: IterableHtmlMessageViewController, maxDelay: TimeInterval = 0.75) {
init(htmlMessageViewController: IterableHtmlMessageViewController,
message: IterableInAppMessage,
maxDelay: TimeInterval = 0.75) {
ITBInfo()

self.topViewController = topViewController
self.htmlMessageViewController = htmlMessageViewController
self.message = message
self.maxDelay = maxDelay

// shouldn't be necessary, but in case there's some kind of race condition
Expand All @@ -41,7 +42,7 @@ class InAppPresenter {
ITBInfo("delayTimer called")

self.delayTimer = nil
self.present()
self.presentAfterDelayValidation()
}
}
}
Expand All @@ -55,15 +56,27 @@ class InAppPresenter {
delayTimer?.invalidate()
delayTimer = nil

present()
presentAfterDelayValidation()
}
}

private func present() {
private func presentAfterDelayValidation() {
ITBInfo()

InAppPresenter.isPresenting = false

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
Expand Down
25 changes: 25 additions & 0 deletions swift-sdk/SDK/IterableConfig.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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

Expand Down
15 changes: 7 additions & 8 deletions tests/unit-tests/InAppPresenterTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ 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())

// a "no-op" to suppress warning
_ = inAppPresenter.self
Expand All @@ -22,19 +22,18 @@ class InAppPresenterTests: XCTestCase {
}

func testInAppPresenterIsPresentingOnInit() {
_ = InAppPresenter(topViewController: UIViewController(),
htmlMessageViewController: getEmptyHtmlMessageViewController())
_ = InAppPresenter(htmlMessageViewController: getEmptyHtmlMessageViewController(),
message: getEmptyInAppMessage())

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(),
maxDelay: maxDelay)

inAppPresenter.show()
Expand Down