# In-app messages

> Show in-app messages from the Grovs dashboard in your Android app, hold them back on some screens, and react to taps and answers.

Source: https://docs.grovs.io/docs/sdk/android/in-app-messages

In-app messages are dashboard campaigns: a banner, modal, full-screen cover, system alert, question or your own HTML. The SDK downloads live campaigns, picks one on the device for each app open, screen view or event, and draws it in its own window above the current activity. To create campaigns, see [In-app messages](https://docs.grovs.io/docs/engagement/in-app-messages).

No code is required. Once the SDK is [configured](https://docs.grovs.io/docs/sdk/android/quick-start), campaigns show. Use the calls below to keep messages off some screens, pause them during a flow, or react to what the user did.

## How messages reach the device

- **Sync.** Once the SDK authenticates, each time the app comes to the foreground, and at least every 15 minutes while it's in use. When the event batch reports that campaigns changed, the device syncs within 10 seconds. Dashboard changes apply at the next sync.
- **Triggers.** App open (once per session), a screen view (automatic, `trackScreenView` or `trackNavigation`), or a custom event from `track`, optionally filtered by its properties.
- **Decision.** Schedule, conditions and frequency caps are checked on the device. If several campaigns match one event, the highest priority shows.
- **One at a time.** While a message is on screen, the highest priority match waits. It shows once the screen is free, if it still qualifies.
- **Offline.** Synced messages still show. After 48 hours without a sync, nothing shows until a sync succeeds. After 14 days, saved campaigns are deleted.

Messages show only while the SDK is enabled and authenticated, the app is in the foreground and an activity is visible. A message that can't be drawn (no visible activity, say) calls [`onInAppMessageFailed`](#react-to-messages) and is tried again at its next trigger. `setSDK(false)` closes the message on screen.

## React to messages

Pass a `GrovsInAppListener` to `Grovs.setInAppListener`. Every method has a default, so override only what you need. Callbacks run on the main thread.

```kotlin
import io.grovs.Grovs
import io.grovs.GrovsInAppListener

class MedlyApp : Application() {
    override fun onCreate() {
        super.onCreate()
        Grovs.configure(this, "your-api-key", useTestEnvironment = false)

        Grovs.setInAppListener(object : GrovsInAppListener {
            // Return false to stop this message. It isn't counted as seen, and nothing else shows for this event.
            override fun shouldShowInAppMessage(campaignId: String): Boolean = !CheckoutFlow.isActive

            override fun onInAppMessageShown(campaignId: String) {}

            // buttonId is null for a tap on the message itself. payload is set for a key-value action.
            override fun onInAppMessageClicked(campaignId: String, buttonId: String?, payload: Map<String, Any?>?) {
                (payload?.get("screen") as? String)?.let { Router.open(it) }
            }

            // reason: close_button, backdrop, swipe, app or orientation
            override fun onInAppMessageDismissed(campaignId: String, reason: String) {}

            // Called once an answered question has closed, in place of the dismissal.
            override fun onInAppMessageAnswered(campaignId: String, value: Any, comment: String?) {}

            override fun onInAppMessageFailed(campaignId: String, message: String) {}
        })
    }
}
```

The SDK holds the listener **strongly**. Call `Grovs.setInAppListener(null)` to release it.

From Java, extend `GrovsInAppListenerAdapter`. Kotlin interface defaults aren't Java defaults on the SDK's JVM target:

```java
Grovs.Companion.setInAppListener(new GrovsInAppListenerAdapter() {
    @Override
    public void onInAppMessageShown(@NonNull String campaignId) {
        Log.d("Grovs", "Shown " + campaignId);
    }
});
```

| Method                                                 | When it's called                                                                                                                                                                                                                                                       |
| ------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `shouldShowInAppMessage(campaignId)`                   | Before a message shows. Return `false` to stop it.                                                                                                                                                                                                                     |
| `onInAppMessageShown(campaignId)`                      | The message is on screen and counted as viewed.                                                                                                                                                                                                                        |
| `onInAppMessageClicked(campaignId, buttonId, payload)` | A button or the message was tapped. `payload` carries the pairs of a key-value action.                                                                                                                                                                                 |
| `onInAppMessageDismissed(campaignId, reason)`          | The message closed without a click. `reason` is `close_button` (close button, back gesture, or the alert's Close), `backdrop`, `swipe`, `app` (closed by the SDK or your app) or `orientation` (device turned to a disallowed orientation; not a dashboard dismissal). |
| `onInAppMessageAnswered(campaignId, value, comment)`   | A question was answered and the message closed. `value` is an `Int` (1 to 5) for stars, or the chosen option's ID (`String`) for a choice. `comment` is `null` unless one was sent.                                                                                    |
| `onInAppMessageFailed(campaignId, message)`            | The message couldn't be drawn. It isn't counted and is tried again at its next trigger.                                                                                                                                                                                |

## Button actions

Each button, and the message itself, has one action. The SDK performs it, reports the click and calls `onInAppMessageClicked`.

| Action                  | What the SDK does                                                                                                                                                                                                                                                                                                                       |
| ----------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Close                   | Closes the message.                                                                                                                                                                                                                                                                                                                     |
| Open URL                | Opens the URL with an `ACTION_VIEW` intent.                                                                                                                                                                                                                                                                                             |
| Key-value               | Closes the message and passes the pairs to `onInAppMessageClicked` as `payload`. Route in your own code.                                                                                                                                                                                                                                |
| Request push permission | Shows the system prompt on Android 13 and later. If the user declined for good, or notifications are off for the app, opens your app's notification settings. Skips people who already allowed notifications. Needs `POST_NOTIFICATIONS` in your manifest on Android 13 and later; without it, campaigns with this action aren't shown. |
| Request app rating      | Starts the [Google Play In-App Review](#app-ratings) flow. Play decides whether the dialog appears. Without the Play review library, campaigns with this action aren't shown.                                                                                                                                                           |
| Open store              | Opens your app's Play Store page if the project has one configured. Otherwise campaigns with this action aren't shown.                                                                                                                                                                                                                  |

## Hold messages back

```kotlin
// Pause during checkout or a video. The highest priority message waits.
Grovs.pauseInAppMessages()

// Show messages again, starting with the one that waited, if it still qualifies.
Grovs.resumeInAppMessages()

// Drop waiting and delayed messages and stay paused. Pass true to also close the one on screen.
Grovs.discardInAppMessages(dismissCurrent = true)

// No message shows on these screens; messages wait until the user leaves them.
Grovs.setInAppExcludedScreens(listOf("CheckoutActivity", "PaymentFragment"))
```

`discardInAppMessages()` with no argument uses `dismissCurrent = false`, from Kotlin or Java.

Excluded screens match tracked screen names: the name passed to `trackScreenView`, or the one automatic tracking reported (the class's simple name, or its alias from `setScreenAliases`). Case is ignored. These calls, and the listener, work before `configure` and survive a second `configure` call.

## Questions

A message can ask for a star rating (1 to 5) or one choice from a list. One tap answers.

- A comment box (up to 500 characters, with **Send** and **Skip**) and a thank-you text, shown for 2 seconds, can follow.
- A high enough star rating can be followed by the Play review dialog, if your app has the [Play review library](#app-ratings).
- An answered campaign isn't shown again on that device.

The answer is reported on tap, so it counts even if the app closes during the comment. `onInAppMessageAnswered` runs once the message closes. The answer is also an event (`in_app_answered`) that can trigger another campaign.

## App ratings

Star questions and the "Rate the app" action use Google Play In-App Review. The SDK doesn't bring it in. Add it to your app to enable them:

```groovy
implementation("com.google.android.play:review-ktx:2.0.2")
```

Without it, campaigns with a "Rate the app" action are skipped and the review step after a high star rating is left out. Play shows the review dialog only for apps installed from Play, and limits how often it appears.

## Custom HTML messages

The HTML template shows your page in a WebView. The SDK injects `window.grovs` before your scripts run:

| Member                              | Description                                                                                                                                                                                               |
| ----------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `grovs.close()`                     | Closes the message (reported as `close_button`).                                                                                                                                                          |
| `grovs.track(name, properties)`     | Tracks a custom event with `campaign_id` added. The message stays open. The first call counts as the message's click. Names are 1 to 100 characters and can't be [reserved names](#reserved-event-names). |
| `grovs.open(url)`                   | Opens a link and closes the message. Only works within 2 seconds of a tap inside the message.                                                                                                             |
| `grovs.setAttribute(key, value)`    | Sets a user attribute. `value` is a string, number, boolean or `null`.                                                                                                                                    |
| `grovs.requestPushPermission()`     | Shows the push permission prompt. Resolves `"granted"`, `"denied"` or `"unsupported"`. Never opens the settings.                                                                                          |
| `grovs.user`, `grovs.event`         | Read-only copies of the user's attributes and the triggering event's properties.                                                                                                                          |
| `grovs.capabilities.pushPermission` | `"granted"`, `"denied"`, `"prompt"` or `"unsupported"` (no `POST_NOTIFICATIONS` declared on Android 13 and later).                                                                                        |

Or use data attributes, no script needed:

```html
<button data-grovs-action="close">Not now</button>
<button data-grovs-action="open" data-grovs-url="https://medly.sqd.link/refill">Refill now</button>
<button data-grovs-action="track" data-grovs-event="refill_interest">Tell me more</button>
```

Plain `<a href>` links go through `grovs.open`. A `#fragment` link scrolls the page.

**Personalization.** Write `{{user.first_name}}` or `{{event.plan}}` in text and in `alt`, `title`, `value`, `placeholder`, `aria-*` and `data-*` attributes. Add a fallback with `{{user.first_name | default: "there"}}`. If a field has no value and no fallback, the message isn't shown.

**Links.** `javascript:`, `vbscript:`, `data:`, `blob:`, `file:`, `about:`, `intent:` and plain `http:` links are refused. Use `https:`, `mailto:`, `tel:`, `sms:` or your app's own scheme.

**Layout.** Full screen, modal, or a top or bottom banner. The SDK sizes the message to the page's reported height and keeps it hidden until the page is ready. A page that isn't ready within 5 seconds isn't shown.

**WebView.** The page runs with JavaScript on, DOM storage, file access and third-party cookies off, and no navigation away from it. An HTML message whose script loops forever keeps the app's shared WebView renderer busy after the message closes.

## Test messages

**Send test** on a dashboard campaign sends it to every device of one person, matched by `Grovs.identifier`. The device shows it at its next sync, ignoring triggers, conditions and limits, once the screen is free and messages aren't paused. A test waits up to 10 minutes on the server. To see it sooner, send the app to the background and open it again.

A test message isn't counted or reported. Events it tracks and attributes it sets are logged, not sent. If its pictures fail to load after 10 tries, the test is dropped with a log line.

## Orientation, activities and appearance

A campaign targets portrait, landscape or both. It shows only in that orientation and closes if the device turns, with reason `orientation`.

The message follows the visible activity. If the activity stops, the window goes with it and the same message, at the same step, is drawn over the next activity that resumes.

Cards without their own background follow the app's light or dark theme. The SDK's labels (`grovs_in_app_close`, `grovs_in_app_open`, `grovs_in_app_continue`, `grovs_in_app_send`, `grovs_in_app_skip`, `grovs_in_app_stars`) are English string resources. Override them in your app's `res/values-*` folders to translate them.

## Debugging

```kotlin
Grovs.setDebug(LogLevel.INFO)
Grovs.setInAppDebugLogging(true)
```

`setInAppDebugLogging(true)` writes to Logcat at INFO under the tag `GrovsInApp`, whatever the SDK log level. For each event it logs the campaigns considered and why each was skipped: outside its schedule, failed condition, frequency cap reached, lower priority, already answered, pictures not loaded yet, a personalization field with no value, an unsafe link, not synced for 48 hours, or unsupported by this SDK version.

## Reserved event names

The SDK reports message views, clicks, dismissals, answers and comments as events. `track` and `grovs.track` in HTML messages refuse these names:

`in_app_viewed`, `in_app_clicked`, `in_app_dismissed`, `in_app_answered`, `in_app_commented`, `in_app_held`, `push_open`, `push_delivered`, `push_held`, `journey_entered`, `journey_held`, `journey_path`, plus the system names `view`, `open`, `install`, `reinstall`, `app_open`, `time_spent`, `reactivation`, `user_referred`, `custom` and `screen_view`.

## See also

- [Push notifications](https://docs.grovs.io/docs/sdk/android/push-notifications): receive push campaigns
- [Events & screen tracking](https://docs.grovs.io/docs/sdk/android/analytics): the events and screens that trigger messages
- [API reference](https://docs.grovs.io/docs/sdk/android/api-reference#in-app-messages): every in-app call and type
- [Upgrading to 3.1](https://docs.grovs.io/docs/upgrade-guides/android-3-1)
