Skip to content

Commit 78ec607

Browse files
New config enum to deal with in-app layouts and how they should appear in the app. Changing default bahaviour to follow the app layout
1 parent 7b95ec1 commit 78ec607

4 files changed

Lines changed: 171 additions & 15 deletions

File tree

CHANGELOG.md

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,36 @@ All notable changes to this project will be documented in this file.
33
This project adheres to [Semantic Versioning](http://semver.org/).
44

55
## [Unreleased]
6-
### Fixed
7-
- In-app messages now match the host app's system bar behavior instead of overriding it. This fixes fullscreen in-apps drawing content behind the status bar.
6+
### Added
7+
- New `IterableInAppDisplayMode` enum to control how in-app messages interact with system bars. Configure via `IterableConfig.Builder.setInAppDisplayMode()`:
8+
- `FOLLOW_APP_LAYOUT` (default) — matches the host app's system bar configuration. No change needed for existing integrations.
9+
- `FORCE_EDGE_TO_EDGE` — forces in-app content to draw behind system bars with transparent status and navigation bars.
10+
- `FORCE_FULLSCREEN` — hides the status bar entirely for all in-app messages.
11+
- `FORCE_RESPECT_BOUNDS` — ensures in-app content never overlaps system bars, keeping UI elements like the close button always accessible.
12+
13+
### Changed
14+
- In-app messages now match the host app's system bar behavior by default. Previously, fullscreen in-apps would always draw content behind the status bar, which could cause UI elements like the close button to be obscured. The new default (`FOLLOW_APP_LAYOUT`) detects whether your app uses edge-to-edge and matches that configuration.
15+
16+
### Migration guide
17+
**No action required for most apps.** The new default `FOLLOW_APP_LAYOUT` automatically adapts to your app's layout.
18+
19+
If you relied on the previous behavior where fullscreen in-apps drew content behind the status bar, you can restore it explicitly:
20+
21+
```java
22+
// Restore previous behavior: in-app content draws behind system bars
23+
IterableConfig config = new IterableConfig.Builder()
24+
.setInAppDisplayMode(IterableInAppDisplayMode.FORCE_EDGE_TO_EDGE)
25+
.build();
26+
```
27+
28+
If you want to ensure the close button is always accessible regardless of app configuration:
29+
30+
```java
31+
// In-app content never goes behind system bars
32+
IterableConfig config = new IterableConfig.Builder()
33+
.setInAppDisplayMode(IterableInAppDisplayMode.FORCE_RESPECT_BOUNDS)
34+
.build();
35+
```
836

937
## [3.6.5]
1038
### Fixed

iterableapi/src/main/java/com/iterable/iterableapi/IterableConfig.java

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,12 @@ public class IterableConfig {
140140
@Nullable
141141
final IterableAPIMobileFrameworkInfo mobileFrameworkInfo;
142142

143+
/**
144+
* Controls how in-app messages interact with the system bars (status bar, navigation bar).
145+
* Defaults to {@link IterableInAppDisplayMode#FOLLOW_APP_LAYOUT}.
146+
*/
147+
final IterableInAppDisplayMode inAppDisplayMode;
148+
143149
/**
144150
* Base URL for Webview content loading. Specifically used to enable CORS for external resources.
145151
* If null or empty, defaults to empty string (original behavior with about:blank origin).
@@ -183,6 +189,7 @@ private IterableConfig(Builder builder) {
183189
decryptionFailureHandler = builder.decryptionFailureHandler;
184190
mobileFrameworkInfo = builder.mobileFrameworkInfo;
185191
webViewBaseUrl = builder.webViewBaseUrl;
192+
inAppDisplayMode = builder.inAppDisplayMode;
186193
}
187194

188195
public static class Builder {
@@ -211,6 +218,7 @@ public static class Builder {
211218
private IterableIdentityResolution identityResolution = new IterableIdentityResolution();
212219
private IterableUnknownUserHandler iterableUnknownUserHandler;
213220
private String webViewBaseUrl;
221+
private IterableInAppDisplayMode inAppDisplayMode = IterableInAppDisplayMode.FOLLOW_APP_LAYOUT;
214222

215223
public Builder() {}
216224

@@ -453,6 +461,17 @@ public Builder setMobileFrameworkInfo(@NonNull IterableAPIMobileFrameworkInfo mo
453461
return this;
454462
}
455463

464+
/**
465+
* Set how in-app messages interact with the system bars (status bar, navigation bar).
466+
* Defaults to {@link IterableInAppDisplayMode#FOLLOW_APP_LAYOUT}, which preserves existing behavior.
467+
* @param inAppDisplayMode the display mode for in-app messages
468+
*/
469+
@NonNull
470+
public Builder setInAppDisplayMode(@NonNull IterableInAppDisplayMode inAppDisplayMode) {
471+
this.inAppDisplayMode = inAppDisplayMode;
472+
return this;
473+
}
474+
456475
/**
457476
* Set the base URL for WebView content loading. Used to enable CORS for external resources.
458477
* If not set or null, defaults to empty string (original behavior with about:blank origin).
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
package com.iterable.iterableapi;
2+
3+
/**
4+
* Controls how in-app messages interact with the system bars (status bar, navigation bar).
5+
* <p>
6+
* This setting is configured via {@link IterableConfig.Builder#setInAppDisplayMode(IterableInAppDisplayMode)}
7+
* and applies globally to all in-app messages displayed by the SDK.
8+
*/
9+
public enum IterableInAppDisplayMode {
10+
11+
/**
12+
* Default. The in-app message follows the host app's current layout configuration.
13+
* If the app is edge-to-edge, the in-app will display edge-to-edge.
14+
* If the app respects system bar bounds, the in-app will too.
15+
*/
16+
FOLLOW_APP_LAYOUT,
17+
18+
/**
19+
* Forces in-app messages to display edge-to-edge, drawing content behind system bars.
20+
* The in-app content will extend behind the status bar and navigation bar.
21+
*/
22+
FORCE_EDGE_TO_EDGE,
23+
24+
/**
25+
* Forces in-app messages to display in fullscreen mode, hiding the status bar entirely.
26+
* Uses legacy FLAG_FULLSCREEN on API &lt; 30 and WindowInsetsController on API 30+.
27+
*/
28+
FORCE_FULLSCREEN,
29+
30+
/**
31+
* Forces in-app messages to respect system bar boundaries.
32+
* Content will never draw behind the status bar or navigation bar,
33+
* ensuring UI elements like the close button are always accessible.
34+
*/
35+
FORCE_RESPECT_BOUNDS
36+
}

iterableapi/src/main/java/com/iterable/iterableapi/IterableInAppFragmentHTMLNotification.java

Lines changed: 86 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
import androidx.core.view.ViewCompat;
3434
import androidx.core.view.WindowCompat;
3535
import androidx.core.view.WindowInsetsCompat;
36+
import androidx.core.view.WindowInsetsControllerCompat;
3637
import androidx.fragment.app.DialogFragment;
3738

3839
public class IterableInAppFragmentHTMLNotification extends DialogFragment implements IterableWebView.HTMLNotificationCallbacks {
@@ -76,6 +77,7 @@ public class IterableInAppFragmentHTMLNotification extends DialogFragment implem
7677
private double inAppBackgroundAlpha;
7778
private String inAppBackgroundColor;
7879
private boolean hostIsEdgeToEdge;
80+
private IterableInAppDisplayMode displayMode = IterableInAppDisplayMode.FOLLOW_APP_LAYOUT;
7981

8082
public static IterableInAppFragmentHTMLNotification createInstance(@NonNull String htmlString, boolean callbackOnCancel, @NonNull IterableHelper.IterableUrlCallback clickCallback, @NonNull IterableInAppLocation location, @NonNull String messageId, @NonNull Double backgroundAlpha, @NonNull Rect padding) {
8183
return IterableInAppFragmentHTMLNotification.createInstance(htmlString, callbackOnCancel, clickCallback, location, messageId, backgroundAlpha, padding, false, new IterableInAppMessage.InAppBgColor(null, 0.0f));
@@ -147,6 +149,7 @@ public void onCreate(@Nullable Bundle savedInstanceState) {
147149
}
148150

149151
notification = this;
152+
displayMode = resolveDisplayMode();
150153
}
151154

152155
@NonNull
@@ -175,7 +178,7 @@ public void onCancel(DialogInterface dialog) {
175178
}
176179

177180
hostIsEdgeToEdge = isHostActivityEdgeToEdge();
178-
configureSystemBarBehavior(dialog.getWindow());
181+
configureSystemBarsForMode(dialog.getWindow());
179182
return dialog;
180183
}
181184

@@ -284,7 +287,7 @@ public void run() {
284287
@Override
285288
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
286289
super.onViewCreated(view, savedInstanceState);
287-
if (getInAppLayout(insetPadding) != InAppLayout.FULLSCREEN && hostIsEdgeToEdge) {
290+
if (shouldApplySystemBarInsets()) {
288291
ViewCompat.setOnApplyWindowInsetsListener(view, (v, insets) -> {
289292
Insets sysBars = insets.getInsets(WindowInsetsCompat.Type.systemBars());
290293
v.setPadding(0, sysBars.top, 0, sysBars.bottom);
@@ -494,11 +497,7 @@ public void run() {
494497
}
495498
};
496499

497-
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
498-
webView.postOnAnimationDelayed(dismissWebViewRunnable, 400);
499-
} else {
500-
webView.postDelayed(dismissWebViewRunnable, 400);
501-
}
500+
webView.postOnAnimationDelayed(dismissWebViewRunnable, 400);
502501
}
503502

504503
private void processMessageRemoval() {
@@ -678,21 +677,94 @@ static int roundToNearest90Degrees(int orientation) {
678677
}
679678
}
680679

681-
private void configureSystemBarBehavior(Window window) {
680+
private IterableInAppDisplayMode resolveDisplayMode() {
681+
try {
682+
IterableConfig config = IterableApi.sharedInstance.config;
683+
if (config != null) {
684+
return config.inAppDisplayMode;
685+
}
686+
} catch (Exception e) {
687+
IterableLogger.w(TAG, "Could not resolve display mode from config, using default");
688+
}
689+
return IterableInAppDisplayMode.FOLLOW_APP_LAYOUT;
690+
}
691+
692+
@SuppressWarnings("deprecation")
693+
private void configureSystemBarsForMode(Window window) {
682694
if (window == null) return;
695+
InAppLayout layout = getInAppLayout(insetPadding);
696+
697+
switch (displayMode) {
698+
case FORCE_EDGE_TO_EDGE:
699+
applyEdgeToEdge(window);
700+
break;
701+
702+
case FORCE_FULLSCREEN:
703+
hideStatusBar(window);
704+
break;
705+
706+
case FORCE_RESPECT_BOUNDS:
707+
applyRespectBounds(window);
708+
break;
709+
710+
case FOLLOW_APP_LAYOUT:
711+
default:
712+
configureSystemBarsFollowingApp(window, layout);
713+
break;
714+
}
715+
}
716+
717+
private void applyEdgeToEdge(Window window) {
718+
WindowCompat.setDecorFitsSystemWindows(window, false);
719+
// On API 35+, system bars are transparent by default; these setters are no-ops
720+
if (Build.VERSION.SDK_INT < 35) {
721+
window.setStatusBarColor(Color.TRANSPARENT);
722+
window.setNavigationBarColor(Color.TRANSPARENT);
723+
}
724+
}
725+
726+
@SuppressWarnings("deprecation")
727+
private void hideStatusBar(Window window) {
728+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
729+
WindowCompat.setDecorFitsSystemWindows(window, false);
730+
WindowInsetsControllerCompat controller = WindowCompat.getInsetsController(window, window.getDecorView());
731+
controller.hide(WindowInsetsCompat.Type.statusBars());
732+
controller.setSystemBarsBehavior(WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE);
733+
} else {
734+
window.setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN);
735+
}
736+
}
737+
738+
private void applyRespectBounds(Window window) {
739+
WindowCompat.setDecorFitsSystemWindows(window, true);
740+
}
741+
742+
@SuppressWarnings("deprecation")
743+
private void configureSystemBarsFollowingApp(Window window, InAppLayout layout) {
683744
Activity activity = getActivity();
684745
if (activity == null || activity.getWindow() == null) return;
685746

686747
if (hostIsEdgeToEdge) {
687-
WindowCompat.setDecorFitsSystemWindows(window, false);
688-
window.setStatusBarColor(Color.TRANSPARENT);
689-
window.setNavigationBarColor(Color.TRANSPARENT);
748+
applyEdgeToEdge(window);
690749
} else {
691-
window.setStatusBarColor(activity.getWindow().getStatusBarColor());
692-
window.setNavigationBarColor(activity.getWindow().getNavigationBarColor());
750+
if (Build.VERSION.SDK_INT < 35) {
751+
window.setStatusBarColor(activity.getWindow().getStatusBarColor());
752+
window.setNavigationBarColor(activity.getWindow().getNavigationBarColor());
753+
}
693754
}
694755
}
695756

757+
private boolean shouldApplySystemBarInsets() {
758+
InAppLayout layout = getInAppLayout(insetPadding);
759+
760+
return switch (displayMode) {
761+
case FORCE_EDGE_TO_EDGE, FORCE_FULLSCREEN -> false;
762+
case FORCE_RESPECT_BOUNDS -> true;
763+
default ->
764+
layout != InAppLayout.FULLSCREEN && hostIsEdgeToEdge;
765+
};
766+
}
767+
696768
private boolean isHostActivityEdgeToEdge() {
697769
Activity activity = getActivity();
698770
if (activity == null || activity.getWindow() == null) return false;
@@ -708,6 +780,7 @@ private boolean isHostActivityEdgeToEdge() {
708780
return false;
709781
}
710782

783+
@SuppressWarnings("deprecation")
711784
private boolean hasEdgeToEdgeLegacyFlags(Activity activity) {
712785
int flags = activity.getWindow().getDecorView().getSystemUiVisibility();
713786
return (flags & View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN) != 0;

0 commit comments

Comments
 (0)