Skip to content

Commit affa6ac

Browse files
MOB-11657 Issues with timing of in app message display (#925)
Co-authored-by: Sumeru Chatterjee <[email protected]>
1 parent 52099a1 commit affa6ac

File tree

8 files changed

+89
-26
lines changed

8 files changed

+89
-26
lines changed

.github/workflows/build-and-test.yml

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,15 @@ jobs:
2929
- name: Get latest stable iOS version
3030
id: ios-version
3131
run: |
32-
LATEST_IOS=$(xcrun simctl list runtimes | grep "iOS 18" | grep -v "watchOS" | grep -v "beta" | grep -v "Beta" | tail -1 | sed 's/.*iOS \([0-9]*\.[0-9]*\).*/\1/')
32+
# First try to find iOS 18.x that's available
33+
LATEST_IOS=$(xcrun simctl list runtimes | grep "iOS 18" | grep -v "watchOS" | grep -v "beta" | grep -v "Beta" | grep -v "unavailable" | tail -1 | sed 's/.*iOS \([0-9]*\.[0-9]*\).*/\1/')
34+
35+
# If no iOS 18.x available, fall back to any available iOS version
36+
if [ -z "$LATEST_IOS" ]; then
37+
echo "No iOS 18.x available, falling back to latest available iOS version..."
38+
LATEST_IOS=$(xcrun simctl list runtimes | grep "iOS" | grep -v "watchOS" | grep -v "beta" | grep -v "Beta" | grep -v "unavailable" | tail -1 | sed 's/.*iOS \([0-9]*\.[0-9]*\).*/\1/')
39+
fi
40+
3341
echo "version=$LATEST_IOS" >> $GITHUB_OUTPUT
3442
echo "Latest stable iOS version: $LATEST_IOS"
3543

swift-sdk/Internal/Utilities/DependencyContainerProtocol.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ extension DependencyContainerProtocol {
3939
displayer: inAppDisplayer,
4040
persister: inAppPersister,
4141
inAppDelegate: config.inAppDelegate,
42+
inAppDisplayDelegate: config.inAppDisplayDelegate,
4243
urlDelegate: config.urlDelegate,
4344
customActionDelegate: config.customActionDelegate,
4445
urlOpener: urlOpener,

swift-sdk/Internal/in-app/InAppDisplayer.swift

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -44,12 +44,13 @@ class InAppDisplayer: InAppDisplayerProtocol {
4444
return .notShown("In-app notification is being presented.")
4545
}
4646

47-
guard let topViewController = getTopViewController() else {
47+
// Initial basic checks - detailed checks will be done after delay
48+
guard InAppDisplayer.getTopViewController() != nil else {
4849
return .notShown("No top view controller.")
4950
}
5051

51-
if topViewController is IterableHtmlMessageViewController {
52-
return .notShown("Skipping the in-app notification. Another notification is already being displayed.")
52+
guard let message = messageMetadata?.message else {
53+
return .notShown("No message available for display validation.")
5354
}
5455

5556
let parameters = IterableHtmlMessageViewController.Parameters(html: htmlString,
@@ -58,11 +59,10 @@ class InAppDisplayer: InAppDisplayerProtocol {
5859
isModal: true)
5960
let htmlMessageVC = IterableHtmlMessageViewController.create(parameters: parameters, onClickCallback: onClickCallback)
6061

61-
topViewController.definesPresentationContext = true
62-
6362
htmlMessageVC.modalPresentationStyle = .overFullScreen
6463

65-
let presenter = InAppPresenter(topViewController: topViewController, htmlMessageViewController: htmlMessageVC)
64+
let presenter = InAppPresenter(htmlMessageViewController: htmlMessageVC, message: message)
65+
6666
presenter.show()
6767

6868
return .shown
@@ -81,7 +81,7 @@ class InAppDisplayer: InAppDisplayerProtocol {
8181
return topViewController is IterableHtmlMessageViewController
8282
}
8383

84-
private static func getTopViewController() -> UIViewController? {
84+
static func getTopViewController() -> UIViewController? {
8585
guard let rootViewController = IterableUtil.rootViewController else {
8686
return nil
8787
}

swift-sdk/Internal/in-app/InAppManager.swift

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ class InAppManager: NSObject, IterableInternalInAppManagerProtocol {
4343
displayer: InAppDisplayerProtocol,
4444
persister: InAppPersistenceProtocol,
4545
inAppDelegate: IterableInAppDelegate,
46+
inAppDisplayDelegate: IterableInAppDisplayDelegate?,
4647
urlDelegate: IterableURLDelegate?,
4748
customActionDelegate: IterableCustomActionDelegate?,
4849
urlOpener: UrlOpenerProtocol,
@@ -59,6 +60,7 @@ class InAppManager: NSObject, IterableInternalInAppManagerProtocol {
5960
self.displayer = displayer
6061
self.persister = persister
6162
self.inAppDelegate = inAppDelegate
63+
self.inAppDisplayDelegate = inAppDisplayDelegate
6264
self.urlDelegate = urlDelegate
6365
self.customActionDelegate = customActionDelegate
6466
self.urlOpener = urlOpener
@@ -555,6 +557,7 @@ class InAppManager: NSObject, IterableInternalInAppManagerProtocol {
555557
private let fetcher: InAppFetcherProtocol
556558
private let displayer: InAppDisplayerProtocol
557559
private let inAppDelegate: IterableInAppDelegate
560+
private let inAppDisplayDelegate: IterableInAppDisplayDelegate?
558561
private let urlDelegate: IterableURLDelegate?
559562
private let customActionDelegate: IterableCustomActionDelegate?
560563
private let urlOpener: UrlOpenerProtocol
@@ -654,7 +657,10 @@ extension InAppManager: InAppNotifiable {
654657

655658
extension InAppManager: InAppDisplayChecker {
656659
func isOkToShowNow(message: IterableInAppMessage) -> Bool {
657-
guard !isAutoDisplayPaused else {
660+
// Check delegate first if available, otherwise fall back to isAutoDisplayPaused property
661+
let autoDisplayPaused = inAppDisplayDelegate?.isAutoDisplayPaused?(for: message) ?? isAutoDisplayPaused
662+
663+
guard !autoDisplayPaused else {
658664
ITBInfo("automatic in-app display has been paused")
659665
return false
660666
}

swift-sdk/Internal/in-app/InAppPresenter.swift

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,16 +8,17 @@ class InAppPresenter {
88
static var isPresenting = false
99

1010
private let maxDelay: TimeInterval
11-
12-
private let topViewController: UIViewController
1311
private let htmlMessageViewController: IterableHtmlMessageViewController
12+
private let message: IterableInAppMessage
1413
private var delayTimer: Timer?
1514

16-
init(topViewController: UIViewController, htmlMessageViewController: IterableHtmlMessageViewController, maxDelay: TimeInterval = 0.75) {
15+
init(htmlMessageViewController: IterableHtmlMessageViewController,
16+
message: IterableInAppMessage,
17+
maxDelay: TimeInterval = 0.75) {
1718
ITBInfo()
1819

19-
self.topViewController = topViewController
2020
self.htmlMessageViewController = htmlMessageViewController
21+
self.message = message
2122
self.maxDelay = maxDelay
2223

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

4344
self.delayTimer = nil
44-
self.present()
45+
self.presentAfterDelayValidation()
4546
}
4647
}
4748
}
@@ -55,15 +56,27 @@ class InAppPresenter {
5556
delayTimer?.invalidate()
5657
delayTimer = nil
5758

58-
present()
59+
presentAfterDelayValidation()
5960
}
6061
}
6162

62-
private func present() {
63+
private func presentAfterDelayValidation() {
6364
ITBInfo()
6465

6566
InAppPresenter.isPresenting = false
6667

68+
guard let topViewController = InAppDisplayer.getTopViewController() else {
69+
ITBInfo("No top view controller available after delay")
70+
return
71+
}
72+
73+
if topViewController is IterableHtmlMessageViewController {
74+
ITBInfo("Another Iterable message is already being displayed")
75+
return
76+
}
77+
78+
topViewController.definesPresentationContext = true
79+
6780
topViewController.present(htmlMessageViewController, animated: false)
6881

6982
htmlMessageViewController.presenter = nil

swift-sdk/SDK/IterableConfig.swift

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,25 @@
44

55
import Foundation
66

7+
/// Protocol to provide dynamic control over in-app message display timing.
8+
/// This delegate allows the application to determine at the exact moment
9+
/// whether an in-app message should be displayed, providing more granular
10+
/// control than the static `isAutoDisplayPaused` property.
11+
@objc public protocol IterableInAppDisplayDelegate {
12+
13+
/// Called to determine if in-app messages should be automatically displayed at this moment.
14+
/// This method is called just before an in-app message would be shown, allowing the app
15+
/// to make a real-time decision based on current app state.
16+
///
17+
/// - Parameter message: The in-app message that is about to be displayed
18+
/// - Returns: `true` if automatic display should be paused (message will not show), `false` if display should proceed
19+
///
20+
/// - Note: This method is called in addition to other display checks. If this returns `true`,
21+
/// the message will not be shown regardless of other conditions.
22+
/// - Note: If this delegate is not set, the default behavior uses the `isAutoDisplayPaused` property.
23+
@objc optional func isAutoDisplayPaused(for message: IterableInAppMessage) -> Bool
24+
}
25+
726
public enum IterableAPIMobileFrameworkType: String, Codable {
827
case flutter = "flutter"
928
case reactNative = "reactnative"
@@ -126,6 +145,12 @@ public class IterableConfig: NSObject {
126145
/// If more than 1 in-app is available, we show the first.
127146
public var inAppDelegate: IterableInAppDelegate = DefaultInAppDelegate()
128147

148+
/// Implement this protocol to provide dynamic control over when in-app messages can be displayed.
149+
/// This delegate allows real-time decision making about display timing, providing more granular
150+
/// control than the static `isAutoDisplayPaused` property. If not set, the SDK will fall back
151+
/// to using the `isAutoDisplayPaused` property.
152+
public var inAppDisplayDelegate: IterableInAppDisplayDelegate?
153+
129154
/// How many seconds to wait before showing the next in-app, if there are more than one present
130155
public var inAppDisplayInterval: Double = 30.0
131156

tests/endpoint-tests/scripts/run_test.sh

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,19 @@ sed -e "s/\(apiKey = \).*$/\1\"$api_key\"/" \
2020
-e "s/\(inAppCampaignId = \).*$/\1\NSNumber($in_app_campaign_id)/" \
2121
-e "s/\(inAppTemplateId = \).*$/\1\NSNumber($in_app_template_id)/" $e2e_folder/CI.swift.template > $e2e_folder/CI.swift
2222

23-
echo "Detecting latest stable iOS version..."
24-
LATEST_IOS=$(xcrun simctl list runtimes | grep "iOS 18" | grep -v "watchOS" | grep -v "beta" | grep -v "Beta" | tail -1 | sed 's/.*iOS \([0-9]*\.[0-9]*\).*/\1/')
23+
echo "Available runtimes:"
24+
xcrun simctl list runtimes
25+
26+
echo "Detecting latest available iOS version..."
27+
# First try to find iOS 18.x that's available
28+
LATEST_IOS=$(xcrun simctl list runtimes | grep "iOS 18" | grep -v "watchOS" | grep -v "beta" | grep -v "Beta" | grep -v "unavailable" | tail -1 | sed 's/.*iOS \([0-9]*\.[0-9]*\).*/\1/')
29+
30+
# If no iOS 18.x available, fall back to any available iOS version
31+
if [ -z "$LATEST_IOS" ]; then
32+
echo "No iOS 18.x available, falling back to latest available iOS version..."
33+
LATEST_IOS=$(xcrun simctl list runtimes | grep "iOS" | grep -v "watchOS" | grep -v "beta" | grep -v "Beta" | grep -v "unavailable" | tail -1 | sed 's/.*iOS \([0-9]*\.[0-9]*\).*/\1/')
34+
fi
35+
2536
echo "Using iOS version: $LATEST_IOS"
2637

2738
xcodebuild -project swift-sdk.xcodeproj \

tests/unit-tests/InAppPresenterTests.swift

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@ class InAppPresenterTests: XCTestCase {
1212
func testInAppPresenterDelegateExistence() {
1313
let htmlMessageViewController = IterableHtmlMessageViewController.create(parameters: getEmptyParameters(), onClickCallback: nil)
1414

15-
let inAppPresenter = InAppPresenter(topViewController: UIViewController(),
16-
htmlMessageViewController: htmlMessageViewController)
15+
let inAppPresenter = InAppPresenter(htmlMessageViewController: htmlMessageViewController,
16+
message: getEmptyInAppMessage())
1717

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

2424
func testInAppPresenterIsPresentingOnInit() {
25-
_ = InAppPresenter(topViewController: UIViewController(),
26-
htmlMessageViewController: getEmptyHtmlMessageViewController())
25+
_ = InAppPresenter(htmlMessageViewController: getEmptyHtmlMessageViewController(),
26+
message: getEmptyInAppMessage())
2727

2828
XCTAssertFalse(InAppPresenter.isPresenting)
2929
}
3030

31-
func testInAppPresenterTimerFinished() {
31+
func testInAppPresenterShowMethod() {
3232
let expectation1 = expectation(description: "delay timer executed")
3333

34-
let topViewController = UIViewController()
3534
let maxDelay = 0.75
36-
let inAppPresenter = InAppPresenter(topViewController: topViewController,
37-
htmlMessageViewController: getEmptyHtmlMessageViewController(),
35+
let inAppPresenter = InAppPresenter(htmlMessageViewController: getEmptyHtmlMessageViewController(),
36+
message: getEmptyInAppMessage(),
3837
maxDelay: maxDelay)
3938

4039
inAppPresenter.show()

0 commit comments

Comments
 (0)