In-App Messages
Display targeted messages while users are in your app — Modal, Banner, Fullscreen, Card. Triggers, targeting, button actions, analytics. SDK integration on every platform.
Display targeted messages to users while they're actively using your app. Perfect for onboarding, feature announcements, promotions, and contextual tips.
Key Features#
Multiple Message Types
Modal, Banner, Fullscreen, and Card formats
Smart Triggers
On app open, custom events, or session start, within an optional start/end date window
User Targeting
Target by segments, user IDs, or properties
Built-in Analytics
Track impressions, clicks, and conversions
Message Types#
Modal
Centered popup dialog that requires user action. Best for important announcements or confirmations.
Banner
Top or bottom bar that doesn't interrupt the user flow. Good for tips and non-urgent messages.
Fullscreen
Full screen takeover for maximum impact. Ideal for onboarding or major announcements.
Card
Floating card notification that appears in corner. Perfect for contextual tips.
Using the Dashboard#
- In the Abrova Console sidebar, open AbrovaPush → In-App
- Click Create Message and choose your message type
- Configure Trigger — Select when the message should appear (on app open, event, etc.)
- Add Content — Set title, body, image, and action buttons
- Activate — Save as draft or activate immediately
Trigger Types#
| Trigger | Description | Use Case |
|---|---|---|
| on_app_open | Shows when user opens the app | Announcements, promotions |
| on_event | Shows when specific event is tracked | Feature discovery, tips |
| on_session_start | Shows at the start of each session | Daily tips, streaks |
| scheduled | Not fired automatically by the SDKs: fetch it and call showInAppMessage(id); limit the window with startDate/endDate | Time-sensitive offers |
| manual | Shown only when your app calls showInAppMessage(id) | Custom logic |
Flutter#
Required: wrap your app with AbrovaPushInAppOverlay. The Flutter plugin forwards every in-app message to Dart; the native SDK does not render its own UI. Without this widget, messages fetch and match but nothing displays.
import 'package:abrova_push/abrova_push.dart';
MaterialApp(
builder: (context, child) => AbrovaPushInAppOverlay(
onButtonTap: (context, message, button) {
if (button.action == InAppButtonAction.deepLink && button.value != null) {
Navigator.pushNamed(context, button.value!);
}
},
child: child ?? const SizedBox.shrink(),
),
home: const HomePage(),
);The overlay renders modal, banner, fullscreen, and card messages out of the box, records impressions/clicks/dismisses, respects Directionality (RTL), Theme.of(context).brightness (dark mode), and safe areas.
Customize styling with AbrovaPushInAppTheme:
AbrovaPushInAppOverlay(
theme: const AbrovaPushInAppTheme(
modalBorderRadius: 24,
modalMaxWidth: 360,
bannerBackgroundColor: Color(0xFF1E1E1E),
bannerTextColor: Colors.white,
animationDuration: Duration(milliseconds: 260),
),
child: child!,
);Fully replace a renderer with AbrovaPushInAppBuilders (any type left null falls back to the default):
AbrovaPushInAppOverlay(
builders: AbrovaPushInAppBuilders(
banner: (context, message, content, actions) => MyBanner(
title: content.title,
body: content.body,
onTap: () => actions.onButtonTap(content.buttons.first),
onClose: actions.onDismiss,
),
),
child: child!,
);// 1. Initialize AbrovaPush (only apiKey required)
await AbrovaPush.init(AbrovaPushConfig(
apiKey: 'ab_live_your_api_key',
));
// 2. Register device (required for in-app messages to work)
await AbrovaPush.register();
// 3. Trigger in-app messages based on your trigger type
// For "on_app_open" trigger:
await AbrovaPush.triggerInAppOnAppOpen();
// For "on_event" trigger (e.g., after purchase):
await AbrovaPush.triggerInAppEvent('purchase_completed',
properties: {'amount': 99.99});
// For "on_session_start" trigger:
await AbrovaPush.triggerInAppOnSessionStart();
// Or manually fetch and show a specific message:
final messages = await AbrovaPush.fetchInAppMessages();
if (messages.isNotEmpty) {
await AbrovaPush.showInAppMessage(messages.first.id);
}
// Dismiss current message programmatically:
await AbrovaPush.dismissInAppMessage();Android#
The SDK shows messages with its built-in UI when the current Activity is set with AbrovaPush.setCurrentActivity(this) (call it in onResume()) and no InAppMessageCallback is set. Setting a callback hands rendering to you: the SDK then records the impression and calls onMessageReady, but shows nothing (this is how the Flutter and React Native wrappers take over). onButtonClicked and onMessageDismissed are called only by the built-in UI, which does not run while a callback is set.
import ir.abrova.push.sdk.AbrovaPush
import ir.abrova.push.sdk.inapp.*
// Built-in UI: tell the SDK which Activity to draw on (in onResume)
AbrovaPush.setCurrentActivity(this)
// Optional, only if you render messages yourself (disables the built-in UI)
AbrovaPush.setInAppMessageCallback(object : InAppMessageCallback {
override fun onMessageReady(message: InAppMessage) {
// Render the message with your own UI
Log.d("InApp", "Message ready: ${message.id}")
}
override fun onButtonClicked(message: InAppMessage, button: InAppButton) {
// Handle button click
when (button.action) {
InAppButtonAction.DEEP_LINK -> {
// Navigate to deep link
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(button.value))
startActivity(intent)
}
InAppButtonAction.URL -> {
// Open URL in browser
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(button.value))
startActivity(intent)
}
InAppButtonAction.CUSTOM -> {
// Handle custom action
handleCustomAction(button.value)
}
else -> {} // Dismiss handled automatically
}
}
override fun onMessageDismissed(message: InAppMessage) {
Log.d("InApp", "Message dismissed: ${message.id}")
}
override fun onError(error: String) {
Log.e("InApp", "In-app error: $error")
}
})
// Trigger in-app messages
AbrovaPush.triggerInAppOnAppOpen()
AbrovaPush.triggerInAppEvent("viewed_product")
AbrovaPush.triggerInAppOnSessionStart()
// Or manually fetch and show messages
AbrovaPush.fetchInAppMessages { messages ->
if (messages.isNotEmpty()) {
AbrovaPush.showInAppMessage(messages.first().id)
}
}
// Dismiss current message
AbrovaPush.dismissInAppMessage()iOS#
As on Android, the built-in UI needs a view controller to present from (AbrovaPush.shared.setCurrentViewController(_:)) and runs only while no InAppMessageCallback is set. With a callback set, the SDK records the impression and calls inAppMessageReady but shows nothing; inAppMessageButtonClicked and inAppMessageDismissed come only from the built-in UI.
import AbrovaPushSDK
// Built-in UI: give the SDK a view controller to present from
AbrovaPush.shared.setCurrentViewController(viewController)
// Optional, only if you render messages yourself (disables the built-in UI)
class MyInAppHandler: InAppMessageCallback {
func inAppMessageReady(_ message: InAppMessage) {
print("Message ready: \(message.id)")
}
func inAppMessageButtonClicked(_ message: InAppMessage, button: InAppButton) {
switch button.action {
case .deepLink:
if let urlString = button.value, let url = URL(string: urlString) {
UIApplication.shared.open(url)
}
case .url:
if let urlString = button.value, let url = URL(string: urlString) {
UIApplication.shared.open(url)
}
case .custom:
handleCustomAction(button.value)
default:
break // Dismiss handled automatically
}
}
func inAppMessageDismissed(_ message: InAppMessage) {
print("Message dismissed: \(message.id)")
}
func inAppMessageError(_ error: String) {
print("In-app error: \(error)")
}
}
// Register the callback. It is held weakly, so keep a strong reference.
let inAppHandler = MyInAppHandler()
AbrovaPush.shared.setInAppMessageCallback(inAppHandler)
// Trigger in-app messages
AbrovaPush.shared.triggerInAppOnAppOpen()
AbrovaPush.shared.triggerInAppEvent("viewed_product")
AbrovaPush.shared.triggerInAppOnSessionStart()
// Or manually fetch and show messages
AbrovaPush.shared.fetchInAppMessages { messages in
if let first = messages.first {
AbrovaPush.shared.showInAppMessage(first.id)
}
}
// Dismiss current message
AbrovaPush.shared.dismissInAppMessage()React Native#
import AbrovaPush, { InAppButtonAction } from 'abrova-push-react-native';
// The React Native wrapper has no built-in in-app UI yet: when a trigger matches,
// the native SDK records the impression and emits onInAppMessageReady, and your
// app renders the message. onInAppButtonClick / onInAppMessageDismissed come only
// from the native SDKs' built-in UI, which the wrapper turns off, so they do not fire.
// To fetch messages and emit one manually:
const messages = await AbrovaPush.fetchInAppMessages();
if (messages.length > 0) {
await AbrovaPush.showInAppMessage(messages[0].id);
}
// Render the message yourself when it is ready
AbrovaPush.onInAppMessageReady((message) => {
console.log('Message ready:', message.id);
// e.g. show your own modal from message.content; for its buttons:
// if (button.action === InAppButtonAction.DEEP_LINK && button.value) {
// navigation.navigate(button.value);
// }
});
// Trigger messages
await AbrovaPush.triggerInAppOnAppOpen();
await AbrovaPush.triggerInAppEvent('viewed_product');
await AbrovaPush.triggerInAppOnSessionStart();Web#
Requires @abrova/push-web 0.1.7 or newer, and a registered device. In-app messages are on by default and render in a built-in shadow-DOM UI.
import AbrovaPush from '@abrova/push-web';
const abrovaPush = new AbrovaPush({
apiKey: 'ab_live_your_api_key',
inApp: {
// enabled: true, // default
// display: 'auto', // 'manual' = render nothing, only fire onMessageReady
// autoTrigger: false, // true = fire on_session_start + on_app_open on page load
// bannerPosition: 'top',
},
});
await abrovaPush.register({ userId: 'user_123' });
// Trigger messages
await abrovaPush.inApp.triggerOnAppOpen();
await abrovaPush.inApp.triggerEvent('viewed_product', { productId: 'prod_123' });
await abrovaPush.inApp.triggerSessionStart();
// Or fetch and show one manually
const messages = await abrovaPush.inApp.fetchMessages();
if (messages.length > 0) {
await abrovaPush.inApp.showMessage(messages[0].id);
}
// Listen to interactions (each returns an unsubscribe function)
abrovaPush.inApp.onMessageReady((message) => console.log('Ready:', message.id));
abrovaPush.inApp.onButtonClicked((message, button) => {
// url / deep_link buttons are opened in a new tab by the SDK;
// custom buttons are left to your app
if (button.action === 'custom') {
handleCustomAction(button.value);
}
});
abrovaPush.inApp.onDismissed((message) => console.log('Dismissed:', message.id));
// Close the message on screen
abrovaPush.inApp.dismissCurrentMessage();Configuration Options#
| Option | Type | Description |
|---|---|---|
| maxImpressions | number | Maximum times to show this message to each device, 1 to 100 (default: 1) |
| delaySeconds | number | Wait time after trigger before showing message (default: 0) |
| minSessionCount | number | Only show to users with at least this many sessions (engagement targeting) |
| priority | number | Higher priority messages display first when multiple match (default: 0) |
| startDate | date | Message becomes active on this date (scheduling) |
| endDate | date | Message stops showing after this date (scheduling) |
| segmentId | string | Show only to devices in this saved segment (mutually exclusive with targetUserIds) |
| targetUserIds | string[] | Show only to specific user IDs (mutually exclusive with segmentId) |
| content.backgroundColor | string | Custom background color (hex, e.g., #FFFFFF) |
| content.textColor | string | Custom text color (hex, e.g., #000000) |
Audience targeting is mutually exclusive. A message reaches its audience one of three ways:
- Everyone — set neither
segmentIdnortargetUserIds(default). - A segment — set
segmentIdto a saved segment UUID. Segment membership is checked on every fetch and matches the Console preview. - Explicit user list — set
targetUserIdsto an array of user IDs (as set via SDKsetUserId()).
Setting both segmentId and targetUserIds is rejected by the API — the dashboard's Audience picker enforces the same rule visually.
Button Actions#
Configure what happens when users tap buttons on your in-app messages.
| Action | Value | Description |
|---|---|---|
| dismiss | - | Simply closes the message |
| deep_link | myapp://screen/123 | Opens an in-app screen via deep link |
| url | https://example.com | Opens a URL in the browser |
| custom | any_custom_value | Custom action handled by your app code via callback |
Handling Custom Actions
In Flutter, handle them in the overlay's onButtonTap (the overlay renders the message, so the native onInAppButtonClick callback does not fire for it):
AbrovaPushInAppOverlay(
onButtonTap: (context, message, button) {
if (button.action == InAppButtonAction.custom) {
switch (button.value) {
case 'share_promo':
Share.share('Use code SUMMER20 for 20% off!');
break;
case 'rate_app':
StoreRedirect.redirect(
androidAppId: 'com.yourapp',
iOSAppId: '123456789'
);
break;
case 'start_subscription':
Navigator.pushNamed(context, '/subscription');
break;
case 'play_video':
showVideoPlayer(videoUrl: 'https://...');
break;
}
}
},
child: child!,
);Common Use Cases#
Onboarding
Welcome new users and guide them through your app.
- • Type: Fullscreen
- • Trigger: on_app_open
- • minSessionCount: 0 (no minimum, so new users qualify)
- • maxImpressions: 1
Feature Announcement
Introduce new features to existing users.
- • Type: Modal
- • Trigger: on_app_open
- • minSessionCount: 5 (engaged users)
- • delaySeconds: 3
Contextual Tip
Show tips when users visit specific screens.
- • Type: Banner or Card
- • Trigger: on_event
- • Event: viewed_settings
- • maxImpressions: 2
Time-Limited Promotion
Run limited-time offers with scheduling.
- • Type: Modal
- • Trigger: on_app_open
- • startDate/endDate: Campaign dates
- • maxImpressions: 3
Post-Purchase Upsell
Cross-sell after a successful purchase.
- • Type: Card
- • Trigger: on_event
- • Event: purchase_completed
- • delaySeconds: 2
VIP User Message
Target specific high-value users.
- • Type: Modal
- • Trigger: on_app_open
- • targetUserIds: [user1, user2]
- • priority: 10 (show first)
Best Practices#
Do
- • Keep messages concise and actionable
- • Use appropriate message type for context
- • Set frequency caps to avoid fatigue
- • A/B test different message variations
- • Target messages to relevant user segments
- • Use scheduling for time-sensitive campaigns
- • Add delay to avoid interrupting app launch
- • Use minSessionCount to target engaged users
Don't
- • Show too many messages per session
- • Use fullscreen for minor updates
- • Interrupt critical user flows
- • Forget to add a dismiss option
- • Use aggressive or misleading CTAs
- • Set maxImpressions too high (causes fatigue)
- • Show messages immediately on app open (use delay)
- • Target new users with advanced feature tips
Analytics & Metrics#
Track the performance of your in-app messages with built-in analytics.
| Metric | Description | Tracked Automatically |
|---|---|---|
| Impressions | Number of times the message was displayed | Yes |
| Clicks | Number of button clicks on the message | Yes |
| Dismissals | Number of times users dismissed the message | Yes |
| Click Rate | Clicks / Impressions × 100% | Yes |