Implementing In-App Messages in Mobile Apps
In-App Messages — these are not push notifications. They display only when the app is open, don't require permissions, don't go to notification center. A modal window on app launch, a banner at the bottom when needed, fullscreen offer after first purchase — all of this is IAM. The main technical task: show them at the right moment without annoying the user.
Platform Solutions vs Custom Implementation
Two paths: use an SDK (OneSignal IAM, Firebase In-App Messaging, Braze, Intercom, Appcues) or implement your own mechanism. SDK — quick start but limited UI customization. Custom solution — full control over appearance and display logic.
Firebase In-App Messaging — free, integrates with Firebase Analytics events:
// iOS — automatically shows IAM when logging specified event
Analytics.logEvent("checkout_started", parameters: [
"cart_value": 450.0
])
// In Firebase Console: IAM trigger = event "checkout_started", condition cart_value > 200
// Android
FirebaseAnalytics.getInstance(context).logEvent("checkout_started") {
param("cart_value", 450.0)
}
Firebase IAM shows message automatically — the app doesn't need to do anything else. Limitation: UI is only edited through Firebase Console (templates), no full control over animations and styles.
Custom Implementation: Architecture
For full control, implement your own IAM engine. Components:
- Campaign Manager — fetches list of active campaigns from backend (or Remote Config).
- Trigger Engine — tracks app events, matches with campaign conditions.
- Display Controller — manages display queue, anti-spam rules.
- UI Layer — renders specific message type.
// Android — Display Controller
class InAppMessageController(
private val campaignRepo: CampaignRepository,
private val displayHistory: DisplayHistoryDao
) {
suspend fun onEvent(eventName: String, params: Map<String, Any> = emptyMap()) {
val campaigns = campaignRepo.getActiveCampaigns()
val eligible = campaigns.filter { campaign ->
campaign.trigger.eventName == eventName &&
matchesConditions(campaign, params) &&
!wasShownRecently(campaign.id)
}
// Show only one message at a time — priority by score
eligible.maxByOrNull { it.priority }?.let { campaign ->
displayHistory.record(campaign.id, System.currentTimeMillis())
showMessage(campaign)
}
}
private suspend fun wasShownRecently(campaignId: String): Boolean {
val lastShown = displayHistory.getLastShownTime(campaignId) ?: return false
val cooldownMs = 24 * 60 * 60 * 1000L // 24 hours
return System.currentTimeMillis() - lastShown < cooldownMs
}
}
UI Types and How to Display Them
Modal (central popup). On iOS — through UIViewController with modalPresentationStyle = .overCurrentContext and transparent background:
let iamVC = InAppMessageViewController(campaign: campaign)
iamVC.modalPresentationStyle = .overCurrentContext
iamVC.modalTransitionStyle = .crossDissolve
topViewController?.present(iamVC, animated: true)
Finding topViewController in complex navigation (TabBar + NavigationController + modals) — a separate task. Recursive helper through presentedViewController and children.
Bottom Sheet / Banner. On Android — BottomSheetDialogFragment or custom View added through WindowManager over current content. Second option works even in fragments without knowing the current screen, but requires SYSTEM_ALERT_WINDOW permission — undesirable.
Better — BottomSheet through supportFragmentManager:
class InAppBottomSheet : BottomSheetDialogFragment() {
// ... campaign data binding
override fun onCreateView(...) = InAppBottomSheetBinding.inflate(inflater).also {
binding = it
}.root
}
InAppBottomSheet.newInstance(campaign).show(supportFragmentManager, "iam_bottom_sheet")
Fullscreen. Separate Activity with FLAG_FULLSCREEN — most reliable on Android. On iOS — UIViewController with modalPresentationStyle = .fullScreen.
Targeting and Display Conditions
| Condition Type | Example |
|---|---|
| Trigger event | screen_viewed = "home", purchase_completed |
| Session count | session_count >= 3 |
| User attribute | subscription = "free", days_since_install >= 7 |
| Time window | only 10:00–22:00 |
| Frequency cap | no more than once per 48 hours |
Without frequency cap, IAM becomes annoying. Store last shown time of each campaign — in Room (Android) or Core Data / UserDefaults (iOS, if data is small).
Analytics
Minimal set of events for IAM analytics:
-
iam_displayed— message shown -
iam_dismissed— closed without action -
iam_action_clicked— action button clicked (withaction_id) -
iam_converted— target event after display (purchase, signup)
Analytics.logEvent("iam_action_clicked", parameters: [
"campaign_id": campaign.id,
"action_id": "upgrade_now",
"screen": currentScreenName
])
Timeframe
Firebase IAM integration with event triggers — 2–3 days. Custom IAM engine with Campaign Manager, Trigger Engine, three UI types (modal, banner, fullscreen), analytics and frequency caps — 10–15 working days.







