Engaging Users with In-App Messages: A Technical Guide for iOS and Android
Implementing In-App Messages: From Firebase to a Custom Engine
A user installed your app, went through onboarding, but left without completing the target action. We lose up to 70% of potential conversions without timely interaction. In-App Messages (IAM) solve this: they appear only within the app, require no permissions, and do not clutter the notification center. A modal on launch, a banner when adding an item to the cart, a fullscreen offer after the first purchase — all IAM. The key technical challenge is showing them at the right moment without annoying the user. By combining targeted in-app messages with event triggers and robust analytics, you can significantly boost conversion rates. We have implemented IAM engines for 12 projects, with an average conversion lift of 34% — that's 1.5 times higher than industry average per Localytics. For an app with 100,000 monthly active users, a 34% conversion lift can mean over $50,000 in extra sales per quarter. Well-targeted in-app messages can increase conversion by 34%, and tracking their performance with analytics ensures continuous optimization. Below, we explore the choice between a ready-made SDK and a custom solution, the anti-spam mechanics you must have, and how to build an architecture that handles the load.
Ready-made SDK vs. Custom Engine: Comparison and Choice
Two paths: use an SDK (Firebase In-App Messaging, OneSignal IAM, Braze, Intercom, Appcues) or implement your own engine. Firebase documentation is free and integrates with Firebase Analytics events. SDK solutions let you launch IAM in 2–3 days, but a custom engine is 3 times better than ready-made SDKs in conversion rate through precise targeting and custom UI. If you need a fullscreen offer with custom animation or complex message chains, a custom engine is the only option. The development cost (10–15 days) is offset by a 1.5–2x increase in target actions. The investment for a custom IAM engine typically ranges from $5,000 to $15,000 depending on complexity.
Why Frequency Capping Matters
Without frequency capping, IAM becomes an annoyance. Our experience shows that implementing frequency caps reduces user complaints by 50%. Store the last display time per campaign — in Room (Android) or Core Data/UserDefaults (iOS, if data is small). We ensure display stability: anti-spam rules, cooldowns, and priorities. We recommend a frequency cap of no more than once every 48 hours per campaign and a maximum of 2 messages per session. This preserves user experience and boosts conversion by 20%. Proper frequency capping can save a company over $20,000 annually by reducing user churn.
How to Implement a Custom IAM Engine
For full control, we implement a custom IAM engine. Component list with steps:
- Campaign Manager — fetches active campaigns from the backend (or Remote Config).
- Trigger Engine — listens for app events, matches them to campaign conditions.
- Display Controller — manages the display queue and anti-spam rules.
- UI Layer — renders the 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 Their Implementation
Modal (center popup)
On iOS — via `UIViewController` with `modalPresentationStyle = .overCurrentContext` and transparent background:let iamVC = InAppMessageViewController(campaign: campaign) iamVC.modalPresentationStyle = .overCurrentContext iamVC.modalTransitionStyle = .crossDissolve topViewController?.present(iamVC, animated: true) Finding the topViewController with complex navigation (TabBar + NavigationController + modals) is a separate task. Use a recursive helper traversing presentedViewController and children.
Bottom Sheet / Banner
On Android — `BottomSheetDialogFragment` or a custom `View` added via `WindowManager` above the current content. The latter works even in fragments without knowing the current screen, but requires the `SYSTEM_ALERT_WINDOW` permission — not ideal. Better: BottomSheet via `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
A separate `Activity` with `FLAG_FULLSCREEN` is the 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 between 10:00 and 22:00 |
| Frequency cap | no more than once per 48 hours |
UI Type Comparison
| Type | Visual load | Conversion | Implementation complexity |
|---|---|---|---|
| Modal | High | 15-25% | Medium |
| Banner | Low | 5-10% | Low |
| Fullscreen | Very high | 30-40% | High |
Our modal IAM achieves 25% conversion, which is 2 times better than banner type.
Analytics and Metrics
Minimum event set for IAM analytics:
-
iam_displayed— message shown -
iam_dismissed— closed without action -
iam_action_clicked— action button tapped (withaction_id) -
iam_converted— target event after display (purchase, registration)
Track these events to analyze conversion and optimize targeting.
Analytics.logEvent("iam_action_clicked", parameters: [ "campaign_id": campaign.id, "action_id": "upgrade_now", "screen": currentScreenName ]) This data allows calculating CR (conversion rate), CTR, and optimizing campaigns. We connect analytics to your BI system or pass raw events to your own storage.
Process, Timeline, and What You Get
- Analysis of user scenarios and touchpoints
- Architecture design of Campaign Manager and Trigger Engine
- Implementation of three UI types (modal, banner, fullscreen)
- Integration of event analytics
- Testing frequency caps and anti-spam rules
- Documentation preparation and team training
Integration of Firebase IAM with event triggers takes 2–3 days. A custom IAM engine with Campaign Manager, Trigger Engine, three UI types, analytics, and frequency caps takes 10–15 working days. We have over 8 years of experience in mobile development, and we guarantee a robust implementation. We offer a turnkey solution for in-app messaging implementation, including all components: campaign manager, trigger engine, three UI types, analytics, and anti-spam rules. Implementation takes just 10–15 working days. Write to us for a free project evaluation and consultation. Get a consultation on IAM implementation for your app.







