Abrova
☰ Docs menu · In-app messages
Coming soon. Abrova Push is coming soon. This page describes how it will work; it can’t be used yet.

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.

Last updated: 27 Oct 2026

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#

  1. In the Abrova Console sidebar, open AbrovaPush → In-App
  2. Click Create Message and choose your message type
  3. Configure Trigger — Select when the message should appear (on app open, event, etc.)
  4. Add Content — Set title, body, image, and action buttons
  5. Activate — Save as draft or activate immediately

Trigger Types#

TriggerDescriptionUse Case
on_app_openShows when user opens the appAnnouncements, promotions
on_eventShows when specific event is trackedFeature discovery, tips
on_session_startShows at the start of each sessionDaily tips, streaks
scheduledNot fired automatically by the SDKs: fetch it and call showInAppMessage(id); limit the window with startDate/endDateTime-sensitive offers
manualShown 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.

dart
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:

dart
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):

dart
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!,
);
dart
// 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.

kotlin
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.

swift
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#

typescript
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.

typescript
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#

OptionTypeDescription
maxImpressionsnumberMaximum times to show this message to each device, 1 to 100 (default: 1)
delaySecondsnumberWait time after trigger before showing message (default: 0)
minSessionCountnumberOnly show to users with at least this many sessions (engagement targeting)
prioritynumberHigher priority messages display first when multiple match (default: 0)
startDatedateMessage becomes active on this date (scheduling)
endDatedateMessage stops showing after this date (scheduling)
segmentIdstringShow only to devices in this saved segment (mutually exclusive with targetUserIds)
targetUserIdsstring[]Show only to specific user IDs (mutually exclusive with segmentId)
content.backgroundColorstringCustom background color (hex, e.g., #FFFFFF)
content.textColorstringCustom text color (hex, e.g., #000000)
Note.

Audience targeting is mutually exclusive. A message reaches its audience one of three ways:

  1. Everyone — set neither segmentId nor targetUserIds (default).
  2. A segment — set segmentId to a saved segment UUID. Segment membership is checked on every fetch and matches the Console preview.
  3. Explicit user list — set targetUserIds to an array of user IDs (as set via SDK setUserId()).

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.

ActionValueDescription
dismiss-Simply closes the message
deep_linkmyapp://screen/123Opens an in-app screen via deep link
urlhttps://example.comOpens a URL in the browser
customany_custom_valueCustom 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):

dart
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.

MetricDescriptionTracked Automatically
ImpressionsNumber of times the message was displayedYes
ClicksNumber of button clicks on the messageYes
DismissalsNumber of times users dismissed the messageYes
Click RateClicks / Impressions × 100%Yes
Was this page helpful?
Report a problem