# In-app messages

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

Source: https://docs.grovs.io/docs/sdk/ios/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 your app. 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/ios/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.** At start, on return to foreground if the last sync is over a minute old, and every 15 minutes while the app is in use. Dashboard changes apply at the next sync.
- **Triggers.** App open, a screen view (automatic or `trackScreenView`), 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 newest 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.

Messages show only while the SDK is enabled and authenticated and the app is in the foreground. A message that can't be drawn (the app has no window, say) reports `GrovsError.inAppDisplayFailed` (5) to [`grovsDidEncounterError`](https://docs.grovs.io/docs/sdk/ios/api-reference#grovserror). It retries at its next trigger.

## React to messages

Set `Grovs.inAppDelegate` to an object adopting `GrovsInAppDelegate`. Every method is optional and runs on the main thread. The delegate is held weakly, so keep a strong reference.

```swift
import Grovs

final class MedlyInAppHandler: NSObject, GrovsInAppDelegate {

    // Return false to stop this message. It isn't counted as seen, and nothing else shows for this event.
    func grovsShouldShowInAppMessage(campaignID: String) -> Bool {
        return !CheckoutFlow.isActive
    }

    func grovsDidShowInAppMessage(campaignID: String) {}

    // buttonID is nil for a tap on the message itself. payload is set for a key-value action.
    func grovsDidTapInAppMessage(campaignID: String, buttonID: String?, payload: [String: Any]?) {
        if let screen = payload?["screen"] as? String {
            Router.shared.open(screen)
        }
    }

    // reason: close_button, backdrop, swipe, app or orientation
    func grovsDidDismissInAppMessage(campaignID: String, reason: String) {}

    // Called once an answered question has closed, in place of the dismissal.
    func grovsDidAnswerInAppMessage(campaignID: String, value: Any, comment: String?) {}
}

let inAppHandler = MedlyInAppHandler()
Grovs.inAppDelegate = inAppHandler
```

| Method                                                  | When it's called                                                                                                                                                                                                    |
| ------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `grovsShouldShowInAppMessage(campaignID:)`              | Before a message shows. Return `false` to stop it.                                                                                                                                                                  |
| `grovsDidShowInAppMessage(campaignID:)`                 | The message is on screen and counted as viewed.                                                                                                                                                                     |
| `grovsDidTapInAppMessage(campaignID:buttonID:payload:)` | A button or the message was tapped. `payload` carries the key-value pairs of a key-value action.                                                                                                                    |
| `grovsDidDismissInAppMessage(campaignID:reason:)`       | The message closed without a click. `reason` is `close_button`, `backdrop`, `swipe`, `app` (closed by the SDK or your app) or `orientation` (device turned to a disallowed orientation; not a dashboard dismissal). |
| `grovsDidAnswerInAppMessage(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 for a choice. `comment` is `nil` unless one was sent.                                             |

## Button actions

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

| Action                  | What the SDK does                                                                                                                                                                    |
| ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| Close                   | Closes the message.                                                                                                                                                                  |
| Open URL                | Opens the URL with `UIApplication.shared.open`.                                                                                                                                      |
| Key-value               | Closes the message and passes the pairs to `grovsDidTapInAppMessage` as `payload`. Route in your own code.                                                                           |
| Request push permission | Shows the system prompt and registers for remote notifications if allowed. If the person declined before, opens your app's settings. Skips people who already allowed notifications. |
| Request app rating      | Shows the system review prompt (`SKStoreReviewController`). Apple decides whether it appears.                                                                                        |
| Open store              | Opens your app's App Store page if the project has one configured. Otherwise campaigns with this action aren't shown.                                                                |

## Hold messages back

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

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

// Drop the waiting message 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(["Checkout", "Onboarding"])
```

Excluded screens match tracked screen names: the name passed to `trackScreenView`, or the one automatic tracking resolved. Case is ignored. Pause, exclusions and the delegate 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 can follow.
- A high enough star rating can be followed by the system review prompt.
- 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. `grovsDidAnswerInAppMessage` runs once the message closes. The answer is also an event (`in_app_answered`) that can trigger another campaign.

## Custom HTML messages

The HTML template shows your page in a web view. 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"`.                                                                                                                    |
| `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"`.                                                                                                                                                   |

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.

## Test messages

**Send test** on a dashboard campaign sends it to every device of one person, matched by `Grovs.userIdentifier`. The device shows it at its next sync, ignoring triggers, conditions and limits, once the screen is free and messages aren't paused. To see it sooner, background the app and return after a minute.

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

## Orientation and iPad

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

On iPad the SDK draws in the scene holding the key window. Orientation follows that window's shape, not the device: a split-screen window can be portrait on a sideways device. Closing the window closes its message.

Messages keep the status bar hidden when your app hides it and return the keyboard to your app after a comment. VoiceOver focus moves to the message and back when it closes. The VoiceOver escape gesture closes dismissible messages.

## Debugging

```swift
Grovs.setDebug(level: .info)
Grovs.setInAppDebugLogging(true)
```

`setInAppDebugLogging(true)` logs, at info level, each campaign considered for an event and why it was skipped: outside its schedule, failed condition, frequency cap reached, lower priority, not yet synced, or unsupported by this SDK version.

## Reserved event names

The SDK reports message views, clicks, dismissals and answers 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/ios/push-notifications): receive push campaigns
- [Events & screen tracking](https://docs.grovs.io/docs/sdk/ios/analytics): the events and screens that trigger messages
- [API reference](https://docs.grovs.io/docs/sdk/ios/api-reference#in-app-messages): every in-app call and type
- [Upgrading to 3.1](https://docs.grovs.io/docs/upgrade-guides/ios-3-1)
