# In-app messages

> Show in-app messages from the Grovs dashboard on your website, hold them back on some pages, and react to clicks and answers.

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

In-app messages are dashboard campaigns: a banner, modal, full-page cover, question or your own HTML. The SDK downloads live campaigns, picks one in the browser for each app open, screen view or event, and draws it over your page. 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/web/quick-start), campaigns show. Use `Grovs.inApp` to keep messages off some pages, pause them during a flow, or react to what the visitor did.

Requires Web SDK **2.1** or later. Messages are on by default, but nothing shows until a campaign targets web visitors. They replace the messages inbox removed in 2.1.

## How messages reach the page

- **Sync.** After `configure`, when the visitor returns to the tab (visible or focused) if the last sync is over a minute old, and every 15 minutes while the page is open.
- **Triggers.** App open (a new session), a screen view (automatic route changes or `trackScreenView`), or a custom event from `track`, optionally filtered by its properties.
- **Decision.** Schedule, conditions and frequency caps are checked in the browser. If several campaigns match one event, the highest priority shows.
- **One at a time.** While a message is on screen, the newest highest priority match waits. It shows once the screen is free, if it still qualifies.

Messages render in a shadow root, so CSS doesn't leak either way. They don't show during server rendering, while consent is pending under `requireConsent`, or while the SDK is disabled.

**App version conditions** compare against the `appVersion` passed to `configure`. Without one, those conditions fail.

## React to messages

```javascript
import Grovs from "grovs";

Grovs.inApp.setCallbacks({
  // Return false to stop a message. It gains no view, and nothing else shows for that event.
  beforeShow: (campaignId) => !checkout.isOpen,
  onShown: (campaignId) => {},
  // buttonId is null for a click on the message itself. payload is set for a key-value action.
  onClicked: (campaignId, buttonId, payload) => {
    if (payload?.page) router.push(payload.page);
  },
  // reason: close_button, backdrop, app or orientation
  onDismissed: (campaignId, reason) => {},
  // Called once an answered question has closed. value is a number, or the option's id for a choice.
  onAnswered: (campaignId, value, comment) => {},
});
```

Each `setCallbacks` call replaces the previous callbacks. It and every other `Grovs.inApp` method work before `configure`, and settings carry over when `configure` runs again.

| Callback                                   | When it's called                                                                                                             |
| ------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------- |
| `beforeShow(campaignId)`                   | Before a message shows. Return `false` to stop it.                                                                           |
| `onShown(campaignId)`                      | The message is on screen and counted as viewed.                                                                              |
| `onClicked(campaignId, buttonId, payload)` | A button or the message was clicked. `payload` carries the pairs of a key-value action.                                      |
| `onDismissed(campaignId, reason)`          | The message closed without a click. `orientation` means the window changed to a disallowed shape; not a dashboard dismissal. |
| `onAnswered(campaignId, value, comment)`   | A question was answered and the message closed. `comment` is `null` unless one was sent.                                     |

## Button actions

| Action                                                  | What the SDK does                                                           |
| ------------------------------------------------------- | --------------------------------------------------------------------------- |
| Close                                                   | Closes the message.                                                         |
| Open URL                                                | Reports the click, then navigates the current tab to the URL.               |
| Key-value                                               | Closes the message and passes the pairs to `onClicked` as `payload`.        |
| Request push permission, request app rating, open store | Not available on the web. Campaigns that use them aren't shown in browsers. |

## Hold messages back

```javascript
Grovs.inApp.pause();                       // hold messages; the newest highest priority one waits
Grovs.inApp.resume();                      // show the one that waited, if it still qualifies
Grovs.inApp.discard(true);                 // drop the waiting one and stay paused; true also closes the one on screen
Grovs.inApp.setExcludedScreens(["Checkout", "Sign in"]);  // no message on these screens
```

Excluded screens match tracked screen names (from `trackScreenView` or automatic route tracking). Messages wait until the visitor leaves.

## Questions

A message can ask for a star rating (1 to 5) or one choice from a list. One click answers. A comment box (up to 500 characters) and a thank-you text can follow. An answered campaign isn't shown again in that browser. The answer is an event (`in_app_answered`) that can trigger another campaign.

## Custom HTML messages

The HTML template shows your page in a sandboxed `srcdoc` iframe (`sandbox="allow-scripts"`, no same-origin access). It can't read your page, cookies or storage. The SDK injects `window.grovs` before your scripts run:

| Member                              | Description                                                                                                                                                                                               |
| ----------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `grovs.close()`                     | Closes the message.                                                                                                                                                                                       |
| `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 click or key press inside the message.                                                                                              |
| `grovs.setAttribute(key, value)`    | Sets a user attribute. `value` is a string, number, boolean or `null`.                                                                                                                                    |
| `grovs.requestPushPermission()`     | Resolves `"unsupported"` in browsers.                                                                                                                                                                     |
| `grovs.user`, `grovs.event`         | Read-only copies of the user's attributes and the triggering event's properties.                                                                                                                          |
| `grovs.capabilities.pushPermission` | Always `"unsupported"` in browsers.                                                                                                                                                                       |

Without script, use `data-grovs-action="close"`, `data-grovs-action="open"` with `data-grovs-url`, or `data-grovs-action="track"` with `data-grovs-event`. Plain `<a href>` links go through `grovs.open`; `#fragment` links scroll inside the message.

**Personalization.** `{{user.first_name}}` and `{{event.plan}}` are filled 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.

**Size.** Full-page cover, modal or banner. A banner takes up to 30% of screen height, or 45% on short screens (500px or less, like a sideways phone). Escape closes a dismissible modal or full-page message, even with focus inside the frame. It doesn't close banners.

### Content-Security-Policy

The iframe inherits your page's Content-Security-Policy; the SDK can't override it. A CSP must allow:

- **`script-src`:** inline scripts via `'unsafe-inline'`, with no nonce, hash or `'strict-dynamic'` in `script-src` (or `default-src` if there's no `script-src`). Browsers ignore `'unsafe-inline'` when any of those is present, so such policies can't run HTML messages at all.
- **`img-src`, `media-src`, `font-src`:** the hosts for the message's pictures, video and fonts, for example your CDN or `https:`. `img-src` also covers built-in template pictures.
- **`style-src 'unsafe-inline'`:** if the message uses `<style>` or `style=""`.

```http
Content-Security-Policy: script-src 'self' 'unsafe-inline'; img-src 'self' https:; style-src 'self' 'unsafe-inline'
```

A blocked message isn't ready within 5 seconds. The SDK skips it and reports `GrovsError.inAppDisplayFailed` (5) to `onError` once, naming the CSP.

Built-in templates need no script or style exception and work under any policy, including nonce-, hash- and `'strict-dynamic'`-based ones.

## Test messages

**Send test** on a dashboard campaign sends it to every device of one person, matched by `Grovs.setUserIdentifier`. The browser shows it at its next sync, ignoring triggers, conditions and limits, once the screen is free and messages aren't paused. Switch tabs and come back after a minute to trigger that sync.

A test message isn't counted or reported. A test using an action browsers can't perform is skipped.

## Orientation

A campaign targets portrait, landscape or both. A window wider than tall is landscape. The message shows only in that shape and closes if it changes, reported to `onDismissed` with reason `orientation`.

## Debugging

```javascript
Grovs.setDebugLevel("info");
Grovs.inApp.setDebug(true);
```

`setDebug(true)` logs, at info level, each campaign considered for an event and why it was skipped.

## Reserved event names

The SDK reports message views, clicks, dismissals and answers as events. `track()` and `grovs.track` 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 such as `app_open`, `screen_view`, `install` and `view`.

## See also

- [Smart banners](https://docs.grovs.io/docs/sdk/web/smart-banners): the "Open in app" banner for mobile browsers
- [Events & screen tracking](https://docs.grovs.io/docs/sdk/web/analytics): the events and screens that trigger messages
- [API reference](https://docs.grovs.io/docs/sdk/web/api-reference#in-app-messages)
- [Upgrading to 2.1](https://docs.grovs.io/docs/upgrade-guides/web-2-1)
