# grovs documentation > grovs is an open-source, self-hostable platform for mobile apps: deep links, analytics, push and in-app engagement, revenue and event data. --- ## Get started URL: https://docs.grovs.io/docs/getting-started Description: Set up a Grovs project, add the SDK, and open your first link. Grovs gives your app smart links, attribution, analytics, push and in-app messages, and revenue tracking from one SDK and one dashboard. Start with the quickstart: a new project to a link that opens your app. - [Quickstart](https://docs.grovs.io/docs/getting-started/quickstart): Create a project, add the SDK, and open your first link. About 30 minutes. - [Introduction](https://docs.grovs.io/docs/getting-started/introduction): What Grovs does, and how the five products fit together. - [Project setup](https://docs.grovs.io/docs/getting-started/project-setup): Redirect rules, your link domain, and social previews. - [Build with AI](https://docs.grovs.io/docs/ai): Let a coding agent add the SDK, or connect the MCP server. ## Products | Product | What it covers | | --------------------------------------------------- | ------------------------------------------------------------------------------------ | | [Links](https://docs.grovs.io/docs/links) | Smart links, custom domains, copy-to-clipboard, smart banners | | [Analytics](https://docs.grovs.io/docs/analytics) | Where users come from, what they do, dashboards, segments | | [Engagement](https://docs.grovs.io/docs/engagement) | Push campaigns, in-app messages, journeys, experiments | | [Revenue](https://docs.grovs.io/docs/revenue) | Purchases from the App Store, Google Play and your own payments, attributed to links | | [Data](https://docs.grovs.io/docs/data) | The event stream: analytics and purchase events for your own server | ## Platform quickstarts | Platform | Package manager | Version | Quickstart | | ------------ | -------------------------------- | ------- | ---------------------------------------------------------------------- | | iOS | Swift Package Manager, CocoaPods | 3.1.0 | [Get started](https://docs.grovs.io/docs/sdk/ios/quick-start) | | Android | Gradle | 3.1.0 | [Get started](https://docs.grovs.io/docs/sdk/android/quick-start) | | Flutter | pub.dev | 3.2.0 | [Get started](https://docs.grovs.io/docs/sdk/flutter/quick-start) | | React Native | npm, Yarn | 3.1.0 | [Get started](https://docs.grovs.io/docs/sdk/react-native/quick-start) | | Unity | Package Manager | 1.0.0 | [Get started](https://docs.grovs.io/docs/sdk/unity/quick-start) | | Web | npm | 2.1.0 | [Get started](https://docs.grovs.io/docs/sdk/web/quick-start) | ## Also in this section - [How-to guides](https://docs.grovs.io/docs/how-to-guides): find bundle IDs, fingerprints, associated domains and other setup values - [Migration guides](https://docs.grovs.io/docs/migration-guides): move from Branch, AppsFlyer or Firebase Dynamic Links - [Upgrade guides](https://docs.grovs.io/docs/upgrade-guides): what changes between SDK versions - [Troubleshooting](https://docs.grovs.io/docs/troubleshooting): links that don't open the app, deferred deep linking, configuration errors - [REST API](https://docs.grovs.io/docs/api-reference): create links, send pushes and export data from your server --- ## Introduction URL: https://docs.grovs.io/docs/getting-started/introduction Description: What Grovs does: links, analytics, engagement, revenue and data, from one SDK and one dashboard. Grovs is one SDK and one dashboard for growing a mobile app or website. A link brings someone to your app. The SDK tells you where they came from and what they do. Push and in-app messages bring them back. Grovs is open source, and runs on Grovs Cloud or your own servers. ## Five products **[Links](https://docs.grovs.io/docs/links).** One link that opens your app on iOS and Android, and a web page everywhere else. If the app isn't installed, the link goes to the store and the SDK delivers its data after install (deferred deep linking). Serve links from your own domain, and show a smart banner on your mobile website. **[Analytics](https://docs.grovs.io/docs/analytics).** Installs, reinstalls, opens and reactivations, traced to the link, campaign or ad that brought them. Custom events and screen views from the SDK, your own dashboards, and segments of users by what they did. **[Engagement](https://docs.grovs.io/docs/engagement).** Push campaigns and in-app messages built in the dashboard, journeys that send them in steps, and experiments that measure what works. **[Revenue](https://docs.grovs.io/docs/revenue).** Purchases from the App Store, Google Play and your own payments, validated and attributed to the links that brought the buyer. **[Data](https://docs.grovs.io/docs/data).** Analytics and purchase events, read by your own server from the event stream. Engagement, segments, revenue and the event stream run on Grovs Cloud and on self-hosted Enterprise. ## How it fits together 1. You [create a project](https://docs.grovs.io/docs/getting-started/quickstart) and set its [redirect rules](https://docs.grovs.io/docs/getting-started/project-setup). 2. You add the SDK to your app or site. It opens links, reports installs and events, and shows messages. 3. You create links in the dashboard, through the [REST API](https://docs.grovs.io/docs/api-reference), or in the app with the SDK. 4. You read the results in the dashboard and act on them with campaigns. ## Platform SDKs | Platform | Package manager | Version | Quickstart | | ------------ | -------------------------------- | ------- | ---------------------------------------------------------------------- | | iOS | Swift Package Manager, CocoaPods | 3.1.0 | [Get started](https://docs.grovs.io/docs/sdk/ios/quick-start) | | Android | Gradle | 3.1.0 | [Get started](https://docs.grovs.io/docs/sdk/android/quick-start) | | Flutter | pub.dev | 3.2.0 | [Get started](https://docs.grovs.io/docs/sdk/flutter/quick-start) | | React Native | npm, Yarn | 3.1.0 | [Get started](https://docs.grovs.io/docs/sdk/react-native/quick-start) | | Unity | Package Manager | 1.0.0 | [Get started](https://docs.grovs.io/docs/sdk/unity/quick-start) | | Web | npm | 2.1.0 | [Get started](https://docs.grovs.io/docs/sdk/web/quick-start) | Push campaigns and in-app messages are fullest on iOS; see [what each SDK supports](https://docs.grovs.io/docs/sdk). ## Build with AI [Connect the Grovs MCP server](https://docs.grovs.io/docs/ai/mcp/introduction) to Claude, Cursor, VS Code or ChatGPT to create links and read analytics from chat. Or give a coding agent [a prompt](https://docs.grovs.io/docs/ai#let-an-agent-integrate-the-sdk) that adds the SDK for you. ## Self-hosting - [Self-hosting](https://docs.grovs.io/docs/self-hosting): architecture and requirements - [Deployment](https://docs.grovs.io/docs/self-hosting/get-started/deployment): run the stack - [SDK configuration](https://docs.grovs.io/docs/self-hosting/configure/sdk-configuration): point the SDKs at your server ## Moving from another provider - [Migrate from Branch](https://docs.grovs.io/docs/migration-guides/branch) - [Migrate from AppsFlyer](https://docs.grovs.io/docs/migration-guides/appsflyer) - [Firebase Dynamic Links](https://docs.grovs.io/docs/migration-guides/firebase-dynamic-links) --- ## Quickstart URL: https://docs.grovs.io/docs/getting-started/quickstart Description: Integrate Grovs end to end in about 30 minutes. Create a project, install and initialize the SDK, handle a deep link, and test it with a real link from the dashboard. > **Note:** Using a coding agent? [Build with AI](https://docs.grovs.io/docs/ai#let-an-agent-integrate-the-sdk) has a prompt per platform that does steps 2 to 4 for you. New to Grovs? Read the [Introduction](https://docs.grovs.io/docs/getting-started/introduction). ## Guided setup in the dashboard Your first project opens a four-step guided setup that follows this page. It saves progress: reopen it from **Finish setup** under the project switcher. | Step | What you do | How it's checked | | -------------------- | --------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------ | | **Link domain** | Pick a Grovs subdomain, which is ready right away, or use your own domain (paid plans). | Saved when you continue. | | **Connect your app** | For each platform (iOS, Android, Web), register the app, add the SDK and initialize it, then run the app. | Live: see below. | | **Links** | Choose where links go on each platform when the app isn't installed, and the share preview. | Done once a fallback is set. | | **Try it** | Create your first link, scan its QR code with your phone, and open it. | Done once the link exists. It also shows **Opened on your phone**, live, once the link opens your app. | If someone else writes the code, **Send to your developer** invites them with a link to the connect step. ### Is my SDK connected? On **Run your app**, the last part of **Connect your app**, the dashboard shows **Waiting for your app to open**. It refreshes every few seconds until the SDK reports in, then shows **Connected** and how long ago. | Platform | Connected when | | ------------ | --------------------------------------------------------------------------------------------------------------------------------------------- | | iOS, Android | A device running the SDK was seen in the last 30 days. The app must start with the SDK configured; opening a link in a browser doesn't count. | | Web | The Web SDK authenticated from one of your registered domains. | The check covers production and test, so a debug build with `useTestEnvironment: true` counts. Once done, the step stays done. If it keeps waiting: 1. Check the API key is the project's key, and the environment flag matches the build. 2. Turn on debug logging (`Grovs.setDebug(level: .info)` on iOS) and look for an authentication error. 3. On the web, check the page's origin is registered exactly, including the port on `localhost`. See [Registering domains](https://docs.grovs.io/docs/how-to-guides/web/register-domains). 4. See [SDK configuration issues](https://docs.grovs.io/docs/troubleshooting/sdk-configuration-issues). The **Try it** check uses the newest open of that link: an app open, install, reinstall or reactivation through it on a device running the SDK. ### 1. Create a project in the Grovs dashboard Sign up at [app.grovs.io](https://app.grovs.io) and create a project. The [guided setup](#guided-setup-in-the-dashboard) starts on its own for your first project. Copy your key from **API Key** in the dashboard sidebar. Each environment (test, production) has its own key: copy the one you build against. Then set your [redirect rules, domain and social preview](https://docs.grovs.io/docs/getting-started/project-setup). Redirect rules need a **fallback URL** before they save. ### 2. Install the SDK **iOS** **Swift Package Manager** 1. In Xcode, go to **File → Add Package Dependencies…** 2. Enter the repository URL: `https://github.com/grovs-io/grovs-iOS.git` 3. Choose **Up to Next Major Version** starting at `3.1.0` 4. Click **Next**, then **Finish** **CocoaPods** Add to your `Podfile`, then run `pod install`: ```ruby pod 'Grovs', '~> 3.1' ``` **Android** Add the dependency to your app-level `build.gradle`: ```groovy dependencies { implementation("io.grovs:Grovs:3.1.0") } ``` **Flutter** Add to `pubspec.yaml`, then run `flutter pub get`: ```yaml dependencies: grovs_flutter_plugin: ^3.2.0 ``` **React Native** Install the wrapper package: ```bash # Using npm npm install react-native-grovs-wrapper # Using yarn yarn add react-native-grovs-wrapper ``` In a bare React Native project, add the Grovs Android SDK to `android/app/build.gradle` at the wrapper's version. The Expo config plugin does this for you: ```groovy dependencies { implementation 'io.grovs:Grovs:3.1.0' } ``` `pod install` adds the iOS SDK. **Web** ```bash npm install grovs --save ``` ### 3. Initialize the SDK **iOS** Initialize the SDK in your `AppDelegate`: ```swift title="AppDelegate.swift" {4} import Grovs func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { Grovs.configure(APIKey: "your-api-key", useTestEnvironment: false, delegate: self) { success in if success { print("Grovs SDK is ready") } } return true } ``` Without a `SceneDelegate`, forward these calls: ```swift title="AppDelegate.swift" func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool { return Grovs.handleAppDelegate(continue: userActivity, restorationHandler: restorationHandler) } func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool { return Grovs.handleAppDelegate(open: url, options: options) } ``` `SceneDelegate` apps forward different calls. See the [iOS Quick Start](https://docs.grovs.io/docs/sdk/ios/quick-start) for that variant, debug logging and user identity. **Android** Initialize Grovs in your `Application` class: ```kotlin title="MyApplication.kt" {4} class MyApplication : Application() { override fun onCreate() { super.onCreate() Grovs.configure(this, "your-api-key", useTestEnvironment = false) } } ``` Forward lifecycle events in your **launcher activity**: ```kotlin title="MainActivity.kt" class MainActivity : AppCompatActivity() { override fun onStart() { super.onStart() Grovs.onStart(this) } override fun onNewIntent(intent: Intent?) { super.onNewIntent(intent) Grovs.onNewIntent(intent, this) } } ``` Links also need intent filters on the launcher activity. Copy them from the [Android Quick Start](https://docs.grovs.io/docs/sdk/android/quick-start). **Flutter** Flutter is configured in native files; there's no `configure` call in Dart. On Android, add the key and environment inside `` in `AndroidManifest.xml`: ```xml title="AndroidManifest.xml" ``` On iOS, add them to `Info.plist`: ```xml title="Info.plist" GrovsApiKey YOUR_API_KEY GrovsUseTestEnvironment ``` Optional keys: a self-hosted base URL (`GrovsBaseURL` / `grovs_base_url`), extra clipboard link domains (`GrovsClipboardDomains` / `grovs_clipboard_domains`), and starting disabled until consent (`GrovsEnabled` / `grovs_enabled`). Set `FlutterDeepLinkingEnabled` (Info.plist) and `flutter_deeplinking_enabled` (Android manifest) to `false`, or Flutter's own deep linking also handles Grovs links. You also need Android intent filters, an iOS URL scheme and Associated Domains. See the [Flutter Quick Start](https://docs.grovs.io/docs/sdk/flutter/quick-start). **React Native** On Android, initialize in `MainApplication`: ```kotlin import io.grovs.Grovs import com.grovswrapper.GrovsConsent override fun onCreate() { super.onCreate() Grovs.configure( this, "your-api-key", useTestEnvironment = false, baseURL = null, autoTrackScreenViews = false, clipboardDomains = null, enabled = GrovsConsent.isEnabled(this) ) } ``` On iOS, initialize in `AppDelegate.swift`: ```swift import Grovs import react_native_grovs_wrapper override func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { Grovs.configure( APIKey: "your-api-key", useTestEnvironment: false, autoTrackScreenViews: false, clipboardDomains: nil, enabled: GrovsWrapperSwift.isSDKEnabled(), delegate: nil ) Grovs.setDebug(level: .info) return super.application(application, didFinishLaunchingWithOptions: launchOptions) } ``` - Pass `nil` as the delegate. The wrapper registers itself and forwards links to your JS listener. - Keep `autoTrackScreenViews` off. The native trackers only see the React Native host screen. - Pass the wrapper's stored consent value as `enabled`, so `setSDK(false)` survives a cold start. Both platforms need more native wiring: link forwarding, intent filters, Associated Domains. See the [React Native Quick Start](https://docs.grovs.io/docs/sdk/react-native/quick-start). With Expo and a development build, the config plugin automates it (Swift and Kotlin templates only). **Web** Import and configure the SDK: ```javascript import Grovs from 'grovs'; await Grovs.configure({ apiKey: 'your-api-key', testEnvironment: false, onDeeplink: (payload) => { // Called when the page was opened from a Grovs link console.log('Link payload:', payload); }, }); ``` > **Warning:** Match `useTestEnvironment` (`testEnvironment` on the web) to the key: **test key → `true`**, **production key → `false`**. A link from the other environment won't resolve its payload. ### 4. Handle a deep link Register a handler. The SDK passes it the link and its payload whenever a Grovs link opens your app. **iOS** Conform to `GrovsDelegate`: ```swift class YourViewController: UIViewController, GrovsDelegate { override func viewDidLoad() { super.viewDidLoad() Grovs.delegate = self } func grovsReceivedPayloadFromDeeplink(link: String?, payload: [String: Any]?, tracking: [String: Any]?) { // Route the user based on payload data if let screen = payload?["screen"] as? String { navigateTo(screen) } } } ``` More options (past payloads, link details) in [iOS Deep Linking](https://docs.grovs.io/docs/sdk/ios/deep-linking). **Android** Register a listener in your launcher activity: ```kotlin Grovs.setOnDeeplinkReceivedListener(this) { deeplinkDetails -> val link = deeplinkDetails.link val payload = deeplinkDetails.data val tracking = deeplinkDetails.tracking Log.d("Grovs", "Opened from: $link, payload: $payload") } ``` More options (Kotlin Flow, link details) in [Android Deep Linking](https://docs.grovs.io/docs/sdk/android/deep-linking). **Flutter** Subscribe to `onDeeplinkReceived` in a widget that stays mounted, such as your root widget. Links that arrive earlier, including the launch link, are buffered for the first listener: ```dart import 'dart:async'; import 'package:flutter/material.dart'; import 'package:grovs_flutter_plugin/grovs.dart'; import 'package:grovs_flutter_plugin/models/grovs_link.dart'; class _AppState extends State { final _grovs = Grovs(); StreamSubscription? _subscription; @override void initState() { super.initState(); _subscription = _grovs.onDeeplinkReceived.listen((details) { final link = details.link; final payload = details.data; debugPrint('Opened from: $link, payload: $payload'); // Route the user based on payload if (payload?['screen'] == 'product') { navigateToProduct(payload?['productId']); } }); } @override void dispose() { _subscription?.cancel(); super.dispose(); } } ``` More details in [Flutter Deep Linking](https://docs.grovs.io/docs/sdk/flutter/deep-linking). **React Native** Use the `onDeeplinkReceived` callback: ```typescript import Grovs from 'react-native-grovs-wrapper'; const listener = Grovs.onDeeplinkReceived((response) => { console.log('Link:', response.link); console.log('Data:', response.data); // Route the user based on payload if (response.data?.screen === 'product') { navigation.navigate('Product', { id: response.data.productId }); } }); // When you no longer need the listener listener.remove(); ``` Set it up early, in your root component or app entry point. See [React Native Deep Linking](https://docs.grovs.io/docs/sdk/react-native/deep-linking). **Web** The `onDeeplink` callback you passed to `configure` receives the link's payload: ```javascript await Grovs.configure({ apiKey: 'your-api-key', onDeeplink: (payload) => { if (payload.screen === 'product_detail') { showProduct(payload.productId); } }, }); ``` More details in [Web Deep Linking](https://docs.grovs.io/docs/sdk/web/deep-linking). ### 5. Create your first link and test 1. Select the environment (Production or Test) in the dashboard. 2. Go to **Links → Dynamic Links** and click **Add Link**. In the guided setup, the **Try it** step makes this link and shows its QR code. 3. Enter a **custom path** (for example `/summer-sale`) and a **Link Name** for internal use. 4. Add key-value pairs under **Data**, for example `screen: product` or `coupon: 25OFF`. 5. Click **Create**. Link types, social previews, redirect overrides and tracking parameters are in [How to Create a Link](https://docs.grovs.io/docs/links/create-link). Open the link on a device: | Device | Result | | ----------------- | ---------------------------------------------------------------------------------------------------------------------------------- | | App installed | The app opens and your handler receives the link and payload. | | App not installed | The store or fallback URL, per your redirect rules. After install, the payload still reaches your handler (deferred deep linking). | | Desktop | By default, a Grovs page with a QR code and download links. | ## Where to next - **Full platform guides:** [iOS](https://docs.grovs.io/docs/sdk/ios), [Android](https://docs.grovs.io/docs/sdk/android), [Flutter](https://docs.grovs.io/docs/sdk/flutter), [React Native](https://docs.grovs.io/docs/sdk/react-native), [Unity](https://docs.grovs.io/docs/sdk/unity), [Web](https://docs.grovs.io/docs/sdk/web) - **How-to guides:** [setup values and configuration](https://docs.grovs.io/docs/how-to-guides) - **Links:** [Create a link](https://docs.grovs.io/docs/links/create-link), [Project setup](https://docs.grovs.io/docs/getting-started/project-setup), [Smart banners](https://docs.grovs.io/docs/links/smart-banners) - **Engagement:** [Push campaigns](https://docs.grovs.io/docs/engagement/push-campaigns) and [in-app messages](https://docs.grovs.io/docs/engagement/in-app-messages) - **Build with AI:** [Agent prompts and the MCP server](https://docs.grovs.io/docs/ai) - **REST API:** [Generate and manage links server-side](https://docs.grovs.io/docs/api-reference) --- ## Project setup URL: https://docs.grovs.io/docs/getting-started/project-setup Description: Set your project's redirect rules, link domain and social preview in the Grovs dashboard. Set where a Grovs link sends people on each platform, your link domain, and the preview shown when a link is shared. All three live under **Links → Link Settings**. The [guided setup](https://docs.grovs.io/docs/getting-started/quickstart#guided-setup-in-the-dashboard) covers the same settings. > **Note:** Redirect rules apply to **every link** in the project, past and future, including SDK links. A single link can override them when you [create it](https://docs.grovs.io/docs/links/create-link). ## Redirect rules Open **Links → Link Settings → Redirect Rules**. ### 1. Set a fallback URL The **Fallback URL** is where a link goes when no other rule applies, usually your home page. It's required to save. ### 2. Choose what happens on iOS and Android For each platform, pick one: - **Redirect to Web** sends everyone on that platform to the URL you enter. Use it if you have no app on that platform yet. - **Redirect to App** opens your app when it's installed and passes the link's data to the SDK. Then choose: - **When app is not installed:** the **App Store** or **Google Play Store** listing, or a **Custom URL** such as a landing page. After install, the SDK still delivers the link's data (deferred deep linking). - **App preview page:** **Show Preview** (recommended) shows your app's name, icon and an open button before redirecting, which helps on browsers that block automatic redirects. **Skip Preview** redirects at once, but some browsers may not open the app. - **Social app browsers:** see [below](#social-app-browsers). Apple's and Android's link verification files are only served for a platform set to **Redirect to App**. ### 3. Choose what happens on desktop Under **Desktops & Laptops**, **Grovs Page** shows a QR code and download links for your app. **Custom URL** sends desktop visitors to a page you choose. ### 4. Save Click **Save**. Every link follows the new rules right away. ### Social app browsers Instagram, Facebook, TikTok and similar apps open links in their own browser, which often can't open your app. For each platform set to **Redirect to App**, choose: - **Open-in-app page:** show a one-tap page that opens your app. - **Redirect as usual:** treat them like any other browser. ## Domain Open **Links → Link Settings → Domain**. Each project gets a Grovs subdomain for its links. Pick a different one, or [use your own domain](https://docs.grovs.io/docs/links/custom-domains). > **Warning:** Changing the subdomain breaks every link on the old one. Released apps can't open links on the new one until you update their associated domains and intent filters. Avoid it while campaigns run. ## Social preview Open **Links → Link Settings → Social Preview**. Set the default image, title and description shown when a link is shared on social networks or in messaging apps. A single link can override them, for example for a seasonal campaign. ## Next steps - [Create a link](https://docs.grovs.io/docs/links/create-link) - [Use your own domain](https://docs.grovs.io/docs/links/custom-domains) - [Add the SDK](https://docs.grovs.io/docs/getting-started/quickstart) --- ## How-to guides URL: https://docs.grovs.io/docs/how-to-guides Description: Step-by-step instructions for common tasks and integrations with Grovs. Find setup values and finish platform configuration for the Grovs SDK. ## [iOS](https://docs.grovs.io/docs/how-to-guides/ios) - [How to Get the Apple App Prefix](https://docs.grovs.io/docs/how-to-guides/ios/apple-app-prefix): find your Apple App Prefix (Team ID) using the Apple Developer Portal or Xcode - [How to Get the Bundle Identifier](https://docs.grovs.io/docs/how-to-guides/ios/bundle-identifier): find the Bundle Identifier for your iOS application - [How to Integrate a CocoaPods Library](https://docs.grovs.io/docs/how-to-guides/ios/cocoapods): add the Grovs SDK to your iOS project using CocoaPods - [How to Add an Associated Domain](https://docs.grovs.io/docs/how-to-guides/ios/associated-domain): configure associated domains in Xcode for Universal Links - [How to Integrate a Swift Package Manager package](https://docs.grovs.io/docs/how-to-guides/ios/spm): add the Grovs SDK to your iOS project using Swift Package Manager - [How to Add a New URL Scheme](https://docs.grovs.io/docs/how-to-guides/ios/url-scheme): configure custom URL schemes for deep linking in your iOS app ## [Android](https://docs.grovs.io/docs/how-to-guides/android) - [How to Add a package with Gradle](https://docs.grovs.io/docs/how-to-guides/android/gradle): add the Grovs SDK dependency to your Android project using Gradle - [How to Get the Package Name](https://docs.grovs.io/docs/how-to-guides/android/package-name): find the package name (application ID) for your Android app - [How to Get the SHA-256 Fingerprint](https://docs.grovs.io/docs/how-to-guides/android/sha256-fingerprint): obtain SHA-256 fingerprints for debug and release signing certificates - [How to Add a New Intent Filter](https://docs.grovs.io/docs/how-to-guides/android/intent-filter): configure intent filters for deep linking in your Android app ## [Flutter](https://docs.grovs.io/docs/how-to-guides/flutter) - [Integrating a pub.dev Package](https://docs.grovs.io/docs/how-to-guides/flutter/pub-dev): add the Grovs SDK to your Flutter project from pub.dev ## [React Native](https://docs.grovs.io/docs/how-to-guides/react-native) - [Integrating an npm package](https://docs.grovs.io/docs/how-to-guides/react-native/npm): add the Grovs SDK to your React Native project using npm - [Integrating a Yarn Package](https://docs.grovs.io/docs/how-to-guides/react-native/yarn): add the Grovs SDK to your React Native project using yarn ## [Unity](https://docs.grovs.io/docs/how-to-guides/unity) - [How to Get the SHA-256 Fingerprint](https://docs.grovs.io/docs/how-to-guides/unity/sha256-fingerprint): read SHA-256 fingerprints from Unity keystores with keytool ## [Web](https://docs.grovs.io/docs/how-to-guides/web) - [How to Integrate an npm package](https://docs.grovs.io/docs/how-to-guides/web/npm): add the Grovs SDK to your web project using npm - [Registering Domains for SDK Integration](https://docs.grovs.io/docs/how-to-guides/web/register-domains): register all required domains for proper SDK functionality across environments --- ## iOS guides URL: https://docs.grovs.io/docs/how-to-guides/ios Description: Step-by-step guides for iOS SDK integration and configuration. Install the iOS SDK, find your app identifiers, and set up deep linking in Xcode. ## Installation - [How to Integrate a CocoaPods Library](https://docs.grovs.io/docs/how-to-guides/ios/cocoapods): add the Grovs SDK to your iOS project using CocoaPods - [How to Integrate a Swift Package Manager package](https://docs.grovs.io/docs/how-to-guides/ios/spm): add the Grovs SDK to your iOS project using Swift Package Manager ## App identifiers - [How to Get the Apple App Prefix](https://docs.grovs.io/docs/how-to-guides/ios/apple-app-prefix): find your Apple App Prefix (Team ID) using the Apple Developer Portal or Xcode - [How to Get the Bundle Identifier](https://docs.grovs.io/docs/how-to-guides/ios/bundle-identifier): find the Bundle Identifier for your iOS application ## Deep linking setup - [How to Add an Associated Domain](https://docs.grovs.io/docs/how-to-guides/ios/associated-domain): configure associated domains in Xcode for Universal Links - [How to Add a New URL Scheme](https://docs.grovs.io/docs/how-to-guides/ios/url-scheme): configure custom URL schemes for deep linking in your iOS app --- ## How to get the Apple App Prefix URL: https://docs.grovs.io/docs/how-to-guides/ios/apple-app-prefix Description: Find your Apple App Prefix (Team ID) using the Apple Developer Portal or Xcode. Grovs needs it to build your app ID (`TEAMID.bundle.identifier`) for Universal Links. **Developer Portal** ### 1. Sign in to the Apple Developer Portal Go to [developer.apple.com](https://developer.apple.com/) and sign in. ### 2. Open Certificates, Identifiers & Profiles Click your account name in the top right and choose **Certificates, Identifiers & Profiles**. ### 3. Open Identifiers Click **Identifiers** in the left sidebar. ### 4. Open any App ID Select any App ID. If you have none, create one first. ### 5. Copy the Prefix Copy the **Prefix** field. That's your Apple App Prefix (Team ID). **Xcode** ### 1. Select your app target Click the project file at the top of the Project Navigator, then select your app target. ### 2. Open Signing & Capabilities Open the **Signing & Capabilities** tab. ### 3. Copy the Team ID Hover over the team name in the **Team** dropdown. The tooltip shows the Team ID. > **Note:** The Team ID and Apple App Prefix are the same value. It's shared by every app in the developer account. --- ## How to get the bundle identifier URL: https://docs.grovs.io/docs/how-to-guides/ios/bundle-identifier Description: Find the Bundle Identifier for your iOS application. Grovs needs it to configure the SDK and Universal Links. **Xcode** ### 1. Select your app target Click the project file at the top of the Project Navigator, then select your app target. ### 2. Open the General tab Open the **General** tab. ### 3. Copy the bundle identifier Copy the **Bundle Identifier** field in the **Identity** section. **Info.plist** ### 1. Open Info.plist Open `Info.plist` from the Project Navigator. ### 2. Copy CFBundleIdentifier Copy the value of the `CFBundleIdentifier` key. **Swift Code** ### 1. Read Bundle.main.bundleIdentifier Print it at runtime. It returns an optional `String?`: ```swift let bundleIdentifier = Bundle.main.bundleIdentifier print("Bundle Identifier: \(String(describing: bundleIdentifier))") ``` > **Note:** Bundle Identifiers usually use reverse domain notation, such as `com.example.myapp`. --- ## How to integrate a CocoaPods library URL: https://docs.grovs.io/docs/how-to-guides/ios/cocoapods Description: Add the Grovs SDK to your iOS project using CocoaPods. If CocoaPods isn't installed, run `sudo gem install cocoapods` first. ### 1. Create a Podfile In the directory that holds your `.xcodeproj`, run: ```bash cd /path/to/your/project pod init ``` ### 2. Add the Grovs pod Add Grovs to your target in the `Podfile`. Replace `'YourApp'` with your target name. ```ruby target 'YourApp' do use_frameworks! pod 'Grovs', '~> 3.1' end ``` ### 3. Install the pod ```bash pod install ``` ### 4. Open the workspace Close the Xcode project, then open the `.xcworkspace`: ```bash open YourProject.xcworkspace ``` ### 5. Import Grovs ```swift import Grovs ``` > **Warning:** After installing pods, always open the `.xcworkspace`. Opening the `.xcodeproj` leaves the dependencies out. --- ## How to add an associated domain URL: https://docs.grovs.io/docs/how-to-guides/ios/associated-domain Description: Configure associated domains in Xcode for Universal Links. ### 1. Select your app target In Xcode, click your project file in the Project Navigator, then select your app target under **Targets**. ### 2. Open Signing & Capabilities Open the **Signing & Capabilities** tab. ### 3. Add the Associated Domains capability Click **+ Capability**, search for **Associated Domains** and double-click it. ### 4. Add each domain Under Associated Domains, click **+** and add: ```text applinks:yourdomain.com ``` Replace `yourdomain.com` with a domain from your Grovs dashboard. Repeat for each domain listed. ### 5. Test on a physical device Build and run on a physical device: Universal Links don't work in the Simulator. Tap a link on one of your domains, for example from Notes. > **Warning:** Add both domains from the Grovs dashboard, one for test and one for production. If links still open Safari, see [Deep link doesn't open the app](https://docs.grovs.io/docs/troubleshooting/deep-link-not-opening-app#ios-universal-links). --- ## How to integrate a Swift Package Manager package URL: https://docs.grovs.io/docs/how-to-guides/ios/spm Description: Add the Grovs SDK to your iOS project using Swift Package Manager. ### 1. Open Package Dependencies Select your project file at the top of the Project Navigator, then open the **Package Dependencies** tab. ### 2. Add the Grovs package Click **+** and enter the repository URL, then click **Next**: ```text https://github.com/grovs-io/grovs-iOS ``` ### 3. Choose a version Choose the latest version and click **Next**. ### 4. Add it to your target Select the Grovs package and click **Add Package**. Xcode resolves and downloads it. ### 5. Import Grovs ```swift import Grovs ``` To update later, right-click the package in the Project Navigator and choose **Update Package**. --- ## How to add a new URL scheme URL: https://docs.grovs.io/docs/how-to-guides/ios/url-scheme Description: Configure custom URL schemes for deep linking in your iOS app. Register the Grovs URL schemes so links can open your app when Universal Links can't. ### 1. Open the Info tab In Xcode, click your project file, select your app target under **Targets**, and open the **Info** tab. ### 2. Add a URL Type Under **URL Types**, click **+**. ### 3. Enter the scheme In **URL Schemes**, enter the scheme from your Grovs dashboard. Leave **Identifier** as the default or set it to your Bundle Identifier. ### 4. Set the Role to Editor Set **Role** to **Editor**. ### 5. Test in Safari Run the app on a device or the Simulator. In Safari's address bar, enter your scheme: ```text yoururlscheme:// ``` The app should open. > **Warning:** Add both URL schemes from the Grovs dashboard, one for test and one for production. The SDK needs both. --- ## Android guides URL: https://docs.grovs.io/docs/how-to-guides/android Description: Step-by-step guides for Android SDK integration and configuration. Install the Android SDK, find your app identifiers, and set up deep linking. ## Installation - [How to Add a package with Gradle](https://docs.grovs.io/docs/how-to-guides/android/gradle): add the Grovs SDK dependency to your Android project using Gradle ## App identifiers - [How to Get the Package Name](https://docs.grovs.io/docs/how-to-guides/android/package-name): find the package name (application ID) for your Android app - [How to Get the SHA-256 Fingerprint](https://docs.grovs.io/docs/how-to-guides/android/sha256-fingerprint): obtain SHA-256 fingerprints for debug and release signing certificates ## Deep linking setup - [How to Add a New Intent Filter](https://docs.grovs.io/docs/how-to-guides/android/intent-filter): configure intent filters for deep linking in your Android app --- ## How to add a package with Gradle URL: https://docs.grovs.io/docs/how-to-guides/android/gradle Description: Add the Grovs SDK dependency to your Android project using Gradle. ### 1. Open app/build.gradle In Android Studio, open your app module's `build.gradle`, usually `app/build.gradle`. ### 2. Add the dependency Add Grovs inside `dependencies`: ```groovy dependencies { // Other dependencies implementation("io.grovs:Grovs:3.1.0") } ``` Check the [Android SDK docs](https://docs.grovs.io/docs/sdk/android) for the latest version. ### 3. Sync Gradle Click **Sync Now** in the banner, or **Sync Project with Gradle Files** in the toolbar. ### 4. Verify the install After the sync, the Grovs package appears under **External Libraries** in the Project view. ### 5. Import Grovs **Kotlin** ```kotlin import io.grovs.Grovs ``` **Java** ```java import io.grovs.Grovs; ``` --- ## How to get the package name URL: https://docs.grovs.io/docs/how-to-guides/android/package-name Description: Find the package name (application ID) for your Android app. The package name is your app's `applicationId`. Grovs needs it to configure the SDK and App Links. ### 1. Open app/build.gradle In Android Studio, open the app module's `build.gradle`, also shown as `build.gradle (Module: app)`. ### 2. Copy the applicationId In `android` → `defaultConfig`, copy the `applicationId` value, here `com.example.yourapp`: ```groovy android { compileSdkVersion 30 defaultConfig { applicationId "com.example.yourapp" minSdkVersion 16 targetSdkVersion 30 versionCode 1 versionName "1.0" } } ``` Enter it in the Grovs dashboard exactly as written. --- ## How to get the SHA-256 fingerprint URL: https://docs.grovs.io/docs/how-to-guides/android/sha256-fingerprint Description: Obtain SHA-256 fingerprints for debug and release signing certificates. Grovs lists your signing certificate's SHA-256 fingerprint in `assetlinks.json`, so Android can verify App Links. > **Warning:** Add **both** your debug and release fingerprints to the Grovs dashboard. Deep linking fails in any build variant whose fingerprint is missing. ## Debug fingerprint **Android Studio** ### 1. Open the Gradle panel In Android Studio, click the **Gradle** tab on the right. ### 2. Run the signing report Open **YourApp → Tasks → android** and double-click `signingReport`. ### 3. Copy the fingerprint In the **Run** tab, copy the `SHA-256` line under the `debug` variant: ```text Variant: debug Config: debug Store: /path/to/your/debug.keystore Alias: your_alias SHA-256: 12:34:56:78:90:AB:CD:EF:12:34:... ``` **Command Line** ### 1. Go to your project ```bash cd /path/to/your/project ``` ### 2. Run the signing report ```bash # macOS / Linux ./gradlew signingReport # Windows gradlew signingReport ``` ### 3. Copy the fingerprint Copy the `SHA-256` line under the `debug` variant: ```text Variant: debug Config: debug Store: /path/to/your/debug.keystore Alias: your_alias SHA-256: 12:34:56:78:90:AB:CD:EF:12:34:... ``` ## Release fingerprint **Self-Managed Keystore** ### 1. Run the keytool command ```bash keytool -list -v \ -keystore /path/to/your/release.keystore \ -alias your_alias_name \ -storepass your_keystore_password \ -keypass your_key_password ``` > **Note:** If `keytool` isn't found, add the JDK `bin` directory to your PATH. ### 2. Copy the fingerprint Copy the `SHA-256` value into your Grovs dashboard. **Google Play App Signing** ### 1. Open the Play Console Sign in to the [Google Play Console](https://play.google.com/console) and select your app. ### 2. Open App integrity Go to **Setup → App integrity**. ### 3. Copy the fingerprint Copy the `SHA-256` under **App signing key certificate** into your Grovs dashboard. Google re-signs Play Store installs with this key, so your upload key's fingerprint isn't enough. --- ## How to add a new intent filter URL: https://docs.grovs.io/docs/how-to-guides/android/intent-filter Description: Configure intent filters for deep linking in your Android app. Add an intent filter so your app opens for the Grovs URL schemes. For `https` App Links, add the `autoVerify` filter from the [Android Quick Start](https://docs.grovs.io/docs/sdk/android/quick-start#3-add-intent-filters). ### 1. Open AndroidManifest.xml Open `app/src/main/AndroidManifest.xml`. ### 2. Add the intent filter Inside your main activity's `` tag, add an `` with the `VIEW` action, the `DEFAULT` and `BROWSABLE` categories, and your URL scheme: ```xml ``` Replace `myapp` with the URL scheme from your Grovs dashboard. ### 3. Add both schemes Add both schemes from the dashboard as separate `` elements in the same filter: ```xml ``` > **Warning:** Add both URL schemes from the Grovs dashboard. Test and production each need their own. ### 4. Test with adb Install the app on a device or emulator and run: ```bash adb shell am start -W -a android.intent.action.VIEW -d "myapp://example.com/path" ``` Use your dashboard scheme in the URL. The app should open and handle the link. --- ## Flutter guides URL: https://docs.grovs.io/docs/how-to-guides/flutter Description: Step-by-step guides for Flutter SDK integration and configuration. Install the Flutter SDK from pub.dev. ## Installation - [Integrating a pub.dev Package](https://docs.grovs.io/docs/how-to-guides/flutter/pub-dev): add the Grovs SDK to your Flutter project from pub.dev ## Native platform setup The Flutter SDK wraps the native iOS and Android SDKs, so their platform setup applies too: - [How to Add an Associated Domain](https://docs.grovs.io/docs/how-to-guides/ios/associated-domain): configure associated domains in Xcode for Universal Links - [How to Add a New URL Scheme](https://docs.grovs.io/docs/how-to-guides/ios/url-scheme): configure custom URL schemes for deep linking in your iOS app - [How to Add a New Intent Filter](https://docs.grovs.io/docs/how-to-guides/android/intent-filter): configure intent filters for deep linking in your Android app - [How to Get the SHA-256 Fingerprint](https://docs.grovs.io/docs/how-to-guides/android/sha256-fingerprint): obtain SHA-256 fingerprints for debug and release signing certificates --- ## Integrating a pub.dev package URL: https://docs.grovs.io/docs/how-to-guides/flutter/pub-dev Description: Add the Grovs SDK to your Flutter project from pub.dev. ### 1. Add the package ```bash flutter pub add grovs_flutter_plugin:^3.2.0 ``` This updates `pubspec.yaml` and fetches the package. To do it by hand, add the package under `dependencies` and run `flutter pub get`. The plugin needs Flutter 3.38.0 or later. ### 2. Check pubspec.yaml `grovs_flutter_plugin` should be listed under `dependencies`: ```yaml dependencies: flutter: sdk: flutter grovs_flutter_plugin: ^3.2.0 ``` ### 3. Import the package ```dart import 'package:grovs_flutter_plugin/grovs.dart'; import 'package:grovs_flutter_plugin/models/grovs_link.dart'; ``` The first import provides the `Grovs` class and the in-app message types. The second provides link and error types such as `GenerateLinkParams`, `DeeplinkDetails` and `GrovsException`. ### 4. Run the app **iOS** ```bash flutter run -d ios ``` **Android** ```bash flutter run -d android ``` **Any device** ```bash flutter run ``` Runs on the connected device or emulator. Grovs includes native code, so iOS and Android need platform setup too. See the [Flutter Quick Start](https://docs.grovs.io/docs/sdk/flutter/quick-start). --- ## React Native guides URL: https://docs.grovs.io/docs/how-to-guides/react-native Description: Step-by-step guides for React Native SDK integration and configuration. Install the React Native SDK with npm or Yarn. ## Installation - [Integrating an npm package](https://docs.grovs.io/docs/how-to-guides/react-native/npm): add the Grovs SDK to your React Native project using npm - [Integrating a Yarn Package](https://docs.grovs.io/docs/how-to-guides/react-native/yarn): add the Grovs SDK to your React Native project using yarn ## Native platform setup The React Native SDK wraps the native iOS and Android SDKs, so their platform setup applies too: - [How to Add an Associated Domain](https://docs.grovs.io/docs/how-to-guides/ios/associated-domain): configure associated domains in Xcode for Universal Links - [How to Add a New URL Scheme](https://docs.grovs.io/docs/how-to-guides/ios/url-scheme): configure custom URL schemes for deep linking in your iOS app - [How to Add a New Intent Filter](https://docs.grovs.io/docs/how-to-guides/android/intent-filter): configure intent filters for deep linking in your Android app - [How to Get the SHA-256 Fingerprint](https://docs.grovs.io/docs/how-to-guides/android/sha256-fingerprint): obtain SHA-256 fingerprints for debug and release signing certificates --- ## Integrating an npm package URL: https://docs.grovs.io/docs/how-to-guides/react-native/npm Description: Add the Grovs SDK to your React Native project using npm. The wrapper requires React Native 0.70 or later and is auto-linked. ### 1. Install the package ```bash npm install react-native-grovs-wrapper ``` ### 2. Add the Android SDK Add the native SDK to `android/app/build.gradle`, at the version the wrapper is built against (`3.1.0` for wrapper 3.1.0, `3.0.1` for wrapper 3.0.2): ```groovy implementation 'io.grovs:Grovs:3.1.0' ``` See [Android dependency](https://docs.grovs.io/docs/sdk/react-native/quick-start#android-dependency). ### 3. Install iOS pods This pulls in the iOS SDK through the wrapper's `Grovs ~> 3.1` dependency (`~> 3.0` in wrapper 3.0.x): ```bash cd ios && pod install ``` ### 4. Import the package ```javascript import Grovs from 'react-native-grovs-wrapper'; ``` ### 5. Run the app With a simulator, emulator or device connected: **iOS** ```bash npx react-native run-ios ``` **Android** ```bash npx react-native run-android ``` > **Note:** Expo projects skip the Gradle and pod steps. The config plugin adds the Gradle line, and `npx expo prebuild` installs the pods. --- ## Integrating a Yarn package URL: https://docs.grovs.io/docs/how-to-guides/react-native/yarn Description: Add the Grovs SDK to your React Native project using yarn. The wrapper requires React Native 0.70 or later and is auto-linked. ### 1. Install the package ```bash yarn add react-native-grovs-wrapper ``` ### 2. Add the Android SDK Add the native SDK to `android/app/build.gradle`, at the version the wrapper is built against (`3.1.0` for wrapper 3.1.0, `3.0.1` for wrapper 3.0.2): ```groovy implementation 'io.grovs:Grovs:3.1.0' ``` See [Android dependency](https://docs.grovs.io/docs/sdk/react-native/quick-start#android-dependency). ### 3. Install iOS pods This pulls in the iOS SDK through the wrapper's `Grovs ~> 3.1` dependency (`~> 3.0` in wrapper 3.0.x): ```bash cd ios && pod install ``` ### 4. Import the package ```javascript import Grovs from 'react-native-grovs-wrapper'; ``` ### 5. Run the app With a simulator, emulator or device connected: **iOS** ```bash npx react-native run-ios ``` **Android** ```bash npx react-native run-android ``` > **Note:** Expo projects skip the Gradle and pod steps. The config plugin adds the Gradle line, and `npx expo prebuild` installs the pods. --- ## Unity guides URL: https://docs.grovs.io/docs/how-to-guides/unity Description: Step-by-step guides for Grovs Unity SDK setup. Find the setup values the Grovs dashboard asks for when your game is built with Unity. ## Android - [How to Get the SHA-256 Fingerprint in Unity](https://docs.grovs.io/docs/how-to-guides/unity/sha256-fingerprint): fingerprints for the Unity debug keystore, your upload keystore and Play App Signing ## Native platform setup The Unity package writes the URL scheme, associated domains and intent filters into the build from **Project Settings → Grovs**, so you don't edit Xcode or the Android manifest by hand. These guides cover the values it needs: - [How to Get the Apple App Prefix](https://docs.grovs.io/docs/how-to-guides/ios/apple-app-prefix): the Team ID the Grovs dashboard asks for - [How to Get the Bundle Identifier](https://docs.grovs.io/docs/how-to-guides/ios/bundle-identifier): in Unity it's **Player Settings → Other Settings → Bundle Identifier** - [How to Get the Package Name](https://docs.grovs.io/docs/how-to-guides/android/package-name): in Unity it's **Player Settings → Other Settings → Package Name** --- ## How to get the SHA-256 fingerprint in Unity URL: https://docs.grovs.io/docs/how-to-guides/unity/sha256-fingerprint Description: Get the SHA-256 fingerprints of the keys that sign your Unity Android build, for Grovs App Links. Grovs lists your signing certificates' SHA-256 fingerprints in `assetlinks.json`, so Android can verify App Links. Unity doesn't run a Gradle project you can call `signingReport` on, so read the fingerprints with `keytool`. > **Warning:** Register **every** key that signs your game in the Grovs dashboard under **SDK Setup → Android**. App Links fail in any build whose key is missing, and the link opens the browser instead. > **Note:** `keytool` ships with the JDK. Unity's Android module installs one under its `OpenJDK` folder. If `keytool` isn't found, add that JDK's `bin` directory to your PATH. ## Debug keystore Unity signs builds with the Android debug keystore when **Player Settings → Publishing Settings** has no custom keystore: ```bash keytool -list -v -keystore ~/.android/debug.keystore -alias androiddebugkey -storepass android ``` Copy the `SHA-256` line. ## Upload keystore Use the keystore and alias set in **Player Settings → Publishing Settings**: ```bash keytool -list -v \ -keystore /path/to/user.keystore \ -alias your_alias_name ``` `keytool` asks for the keystore password. Copy the `SHA-256` line. ## Play App Signing key Apps installed from Google Play are signed with Google's app signing key, not your upload key. ### 1. Open the Play Console Sign in to the [Google Play Console](https://play.google.com/console) and select your app. ### 2. Open App integrity Go to **Setup → App integrity**. ### 3. Copy the fingerprint Copy the `SHA-256` under **App signing key certificate** into the Grovs dashboard. ## See also - [Unity quick start](https://docs.grovs.io/docs/sdk/unity/quick-start#dashboard-setup): the rest of the dashboard setup - [Deep link not opening the app](https://docs.grovs.io/docs/troubleshooting/deep-link-not-opening-app) --- ## Web guides URL: https://docs.grovs.io/docs/how-to-guides/web Description: Step-by-step guides for Web SDK integration and configuration. Install the Web SDK and register the domains it runs on. ## Installation - [How to Integrate an npm package](https://docs.grovs.io/docs/how-to-guides/web/npm): add the Grovs SDK to your web project using npm ## Configuration - [Registering Domains for SDK Integration](https://docs.grovs.io/docs/how-to-guides/web/register-domains): register all required domains for proper SDK functionality across environments --- ## How to integrate an npm package URL: https://docs.grovs.io/docs/how-to-guides/web/npm Description: Add the Grovs SDK to your web project using npm. ### 1. Create package.json Skip this for an existing project: ```bash npm init -y ``` ### 2. Install the package ```bash npm install grovs ``` ### 3. Import the package **CommonJS** ```javascript const { default: Grovs } = require('grovs'); ``` **ES Modules** ```javascript import Grovs from 'grovs'; ``` > **Note:** Bundlers such as Webpack, Vite and Parcel resolve the import at build time. ### 4. Configure the SDK ```javascript await Grovs.configure({ apiKey: 'your-api-key', // Additional configuration options }); ``` See the [Web Quick Start](https://docs.grovs.io/docs/sdk/web/quick-start) for every configuration option. --- ## Registering domains for SDK integration URL: https://docs.grovs.io/docs/how-to-guides/web/register-domains Description: Register all required domains for proper SDK functionality across environments. Register every origin the Web SDK runs on, including local development and staging. An origin is the scheme, host and port, such as `https://app.example.com` or `http://localhost:3000`. On every authentication, the backend compares the page's origin **exactly** against your project's linked domains. An unlisted origin is rejected with "This Web app is not configured". > **Warning:** Register localhost URLs too. The SDK fails on any origin you leave out. ### 1. List every origin List each environment where the site runs or is tested, with its scheme and port: ```text # Production https://www.example.com # Test https://test.example.com # Staging https://staging.example.com # Development http://localhost:8080 http://localhost:3000 ``` ### 2. Add each origin in the dashboard In your project's domain registration section, add each origin exactly as the browser reports it: same scheme, same host, and the port when it isn't the default. `https://example.com` and `https://www.example.com` are different origins, as are `http://localhost:3000` and `http://localhost:5173`. ### 3. Verify each origin Load the site in each environment with info logging on. `configure` should resolve `true`: ```javascript const ready = await Grovs.configure({ apiKey: 'your-api-key', debugLevel: 'info', onError: (code, message) => console.error(code, message), }); console.log('Grovs configured:', ready); ``` If it resolves `false` with an `authenticationFailed` error saying the web app is not configured, the message includes the exact origin the SDK sent. Copy it into the dashboard instead of retyping it. > **Note:** Register a new environment's origin before you deploy to it. --- ## Migration guides URL: https://docs.grovs.io/docs/migration-guides Description: Step-by-step guides for migrating to Grovs from other deep linking platforms. Move to Grovs from another deep linking platform. There are two kinds of migration; use either or both: - **Link migration (Branch and AppsFlyer).** Point your existing provider subdomain at Grovs so your old links keep working. The links themselves need no code change. - **SDK migration (Firebase Dynamic Links).** Replace the provider SDK in your app with the Grovs SDK. To move to a newer Grovs SDK major version, see [Upgrade Guides](https://docs.grovs.io/docs/upgrade-guides). ## Available guides - **[Migrate from Branch](https://docs.grovs.io/docs/migration-guides/branch):** move your existing Branch subdomain and links to Grovs. - **[Migrate from AppsFlyer](https://docs.grovs.io/docs/migration-guides/appsflyer):** move your existing AppsFlyer OneLink subdomain and links to Grovs. - **[Firebase Dynamic Links](https://docs.grovs.io/docs/migration-guides/firebase-dynamic-links):** migrate from Firebase Dynamic Links for iOS and Android. ## How link migration works For Branch and AppsFlyer, you attach your provider subdomain to Grovs. The first time someone opens a legacy link, Grovs looks it up with your provider credentials, recreates it and redirects. Later clicks come from the cache. ```mermaid flowchart TD A[Old link opened]:::evt --> C{Cached?}:::dec C -->|No| D[Look up via provider]:::proc --> E[Create and cache link]:::proc --> R[301 to branded link]:::done C -->|Yes| R ``` The cutover runs in phases: onboard, provision SSL, prepare the app, flip DNS. Traffic only moves when you change DNS. > **Warning:** Link migration from Branch or AppsFlyer requires an **active paid subscription**. Firebase SDK migration doesn't. --- ## Firebase Dynamic Links URL: https://docs.grovs.io/docs/migration-guides/firebase-dynamic-links Description: Migrate from Firebase Dynamic Links to Grovs for deep linking in your iOS and Android apps. Each guide maps Firebase calls to their Grovs equivalents: initialization, link handling, link generation and user identity. - [iOS](https://docs.grovs.io/docs/migration-guides/firebase-dynamic-links/ios): Swift Package Manager or CocoaPods, AppDelegate and SceneDelegate forwarding - [Android](https://docs.grovs.io/docs/migration-guides/firebase-dynamic-links/android): intent filters, lifecycle forwarding, listener or flow --- ## iOS URL: https://docs.grovs.io/docs/migration-guides/firebase-dynamic-links/ios Description: Migrate your iOS app from Firebase Dynamic Links to Grovs for deep linking, universal link handling, and more. Replace each Firebase Dynamic Links call in your iOS app with its Grovs equivalent. **Before you start:** you need a Grovs account and the Grovs SDK in your project. See the [iOS SDK guide](https://docs.grovs.io/docs/sdk/ios). ## Steps ### 1. Initialization #### Firebase Dynamic Links No explicit initialization for link handling, but Firebase itself must be initialized. #### Grovs Configure Grovs in `application:didFinishLaunchingWithOptions`, passing `yourDelegate` to handle deep links. ```swift import Grovs func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { Grovs.configure(APIKey: "your-api-key", useTestEnvironment: false, delegate: yourDelegate) Grovs.setDebug(level: .info) return true } ``` ### 2. Universal Links and URL Handling In **Xcode project > Target > Signing & Capabilities**, replace the Firebase associated domains with the ones from the [Grovs dashboard](https://app.grovs.io/login). Then forward incoming links from the AppDelegate or the SceneDelegate. #### AppDelegate Approach **Firebase Dynamic Links:** ```swift func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool { guard let incomingURL = userActivity.webpageURL else { return false } let handled = DynamicLinks.dynamicLinks().handleUniversalLink(incomingURL) { dynamicLink, error in // Handle dynamic link } return handled } func application(_ application: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any]) -> Bool { return DynamicLinks.dynamicLinks().handleUniversalLink(url) { dynamicLink, error in // Handle dynamic link } } ``` **Grovs:** ```swift func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool { return Grovs.handleAppDelegate(continue: userActivity, restorationHandler: restorationHandler) } func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any]) -> Bool { return Grovs.handleAppDelegate(open: url, options: options) } ``` #### SceneDelegate Approach Apps with a **Scene Delegate** (iOS 13+) forward Universal Links and URLs there: **Firebase Dynamic Links:** ```swift @available(iOS 13.0, *) func scene(_ scene: UIScene, continue userActivity: NSUserActivity) { guard let url = userActivity.webpageURL else { return } DynamicLinks.dynamicLinks().handleUniversalLink(url) { dynamicLink, error in // Handle dynamic link } } ``` **Grovs:** ```swift // Handle open URL contexts @available(iOS 13.0, *) func scene(_ scene: UIScene, openURLContexts URLContexts: Set) { Grovs.handleSceneDelegate(openURLContexts: URLContexts) } // Handle continue user activity func scene(_ scene: UIScene, continue userActivity: NSUserActivity) { Grovs.handleSceneDelegate(continue: userActivity) } // Handle scene delegate options @available(iOS 13.0, *) func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { Grovs.handleSceneDelegate(options: connectionOptions) } ``` ### 3. Generating Links #### Firebase Dynamic Links ```swift guard let link = URL(string: "https://www.example.com/") else { return } let dynamicLinkComponents = DynamicLinkComponents(link: link, domainURIPrefix: "https://example.page.link") // iOS parameters dynamicLinkComponents?.iOSParameters = DynamicLinkIOSParameters(bundleID: "com.example.ios") // Android parameters dynamicLinkComponents?.androidParameters = DynamicLinkAndroidParameters(packageName: "com.example.ios") // Social meta-tag parameters dynamicLinkComponents?.socialMetaTagParameters = DynamicLinkSocialMetaTagParameters() dynamicLinkComponents?.socialMetaTagParameters?.title = "Welcome to My App!" dynamicLinkComponents?.socialMetaTagParameters?.descriptionText = "Some social description." dynamicLinkComponents?.socialMetaTagParameters?.imageURL = URL(string: "https://www.example.com/images/social_preview.png") // Build the short link dynamicLinkComponents?.shorten(completion: { (shortURL, warnings, error) in if let error = error { // Error return } if let shortURL = shortURL { // Link created } }) ``` #### Grovs Platform parameters live in the [Grovs dashboard](https://app.grovs.io/login), so the call only sets the link's content. ```swift Grovs.generateLink(title: "Welcome to My App!", subtitle: "Some social description.", imageURL: "https://www.example.com/images/social_preview.png", data: nil, tags: nil) { url in // Link created } ``` ### 4. User Identification and Attributes #### Firebase Dynamic Links Firebase uses FirebaseAuth for identity. #### Grovs ```swift Grovs.userIdentifier = "user123" Grovs.userAttributes = ["name": "Alice Smith", "tier": "Premium"] ``` ## Running both during the transition Grovs works alongside Firebase and Firebase Dynamic Links. You can keep both integrations for a while to phase the switch. --- ## Android URL: https://docs.grovs.io/docs/migration-guides/firebase-dynamic-links/android Description: Migrate your Android app from Firebase Dynamic Links to Grovs for deep linking, link generation, and more. **Before you start:** you need a Grovs account and the Grovs SDK in your project. See the [Android SDK guide](https://docs.grovs.io/docs/sdk/android). ## Steps ### 1. Initialization #### Firebase Dynamic Links No explicit initialization for link handling, but Firebase itself must be initialized. #### Grovs Call `Grovs.configure()` in your Application class: ```kotlin import io.grovs.Grovs import io.grovs.model.LogLevel class MainApplication : Application() { override fun onCreate() { super.onCreate() Grovs.configure(application = this, apiKey = "your-api-key", useTestEnvironment = false) Grovs.setDebug(LogLevel.INFO) // Optional } } ``` ### 2. Universal Links and URL Handling #### Firebase Dynamic Links An intent filter in **AndroidManifest.xml** opens links: ```xml ``` Your activity reads the link: ```kotlin Firebase.dynamicLinks .getDynamicLink(intent) .addOnSuccessListener { pendingDynamicLinkData -> val deepLink: Uri? = pendingDynamicLinkData?.link // Handle the deep link } ``` #### Grovs Grovs uses similar intent filters. Take `your_app_scheme`, `your_app_host` and `your_app_test_host` from your [Grovs project dashboard](https://app.grovs.io/login). ```xml ``` Add Grovs handling in `onNewIntent()` and `onStart()`: ```kotlin override fun onNewIntent(intent: Intent?) { super.onNewIntent(intent) Grovs.onNewIntent(intent, this) } override fun onStart() { super.onStart() Grovs.onStart(this) } ``` Then listen for deep links with a listener: ```kotlin Grovs.setOnDeeplinkReceivedListener(this) { deeplinkDetails -> // Handle the deep link val link = deeplinkDetails.link val payload = deeplinkDetails.data } ``` Or with a flow: ```kotlin Grovs.Companion::openedLinkDetails.flow.collect { deeplinkDetails -> // Handle the deep link } ``` ### 3. Generating Links #### Firebase Dynamic Links ```kotlin val shortLinkTask = Firebase.dynamicLinks.shortLinkAsync { link = Uri.parse("https://www.example.com/") domainUriPrefix = "https://example.page.link" androidParameters { } iosParameters("com.example.ios") { } socialMetaTagParameters { title = "Welcome to My App!" description = "Some social description." imageUrl = Uri.parse("https://www.example.com/images/social_preview.png") } }.addOnSuccessListener { (shortLink, flowchartLink) -> // Link created }.addOnFailureListener { // Error } ``` #### Grovs Platform parameters live in the [Grovs dashboard](https://app.grovs.io/login), so the call only sets the link's content. ```kotlin Grovs.generateLink(title = "Welcome to My App!", subtitle = "Some social description.", imageURL = "https://www.example.com/images/social_preview.png", listener = { link, error -> link?.let { link -> // Link created } error?.let { error -> // Error } }) ``` Or using Kotlin coroutines: ```kotlin try { val link = Grovs.generateLink(title = "Welcome to My App!", subtitle = "Some social description.", imageURL = "https://www.example.com/images/social_preview.png") } catch (e: GrovsException) { // Handle generation error } ``` ### 4. User Identification and Attributes #### Firebase Dynamic Links Firebase uses FirebaseAuth for identity. #### Grovs Set identity and attributes directly: ```kotlin Grovs.identifier = "user123" Grovs.attributes = hashMapOf( "name" to "Alice Smith", "tier" to "Premium" ) ``` ## Running both during the transition Grovs works alongside Firebase and Firebase Dynamic Links. You can keep both integrations for a while to phase the switch. --- ## Migrate from Branch URL: https://docs.grovs.io/docs/migration-guides/branch Description: Move your existing Branch subdomain and links to Grovs without breaking old links. When someone opens an old Branch link, Grovs looks it up with your Branch key, recreates it as a Grovs link and redirects to your branded URL. After that first click, Grovs serves it directly. This moves existing links only. To create **new** links and capture attribution through Grovs, also add the [Grovs SDK](https://docs.grovs.io/docs/sdk). ## How it works ```mermaid flowchart TD A[Old Branch link opened]:::evt --> C{Cached?}:::dec C -->|No| D[Look up via Branch API]:::proc --> E[Create and cache link]:::proc --> R[301 to branded link]:::done C -->|Yes| R ``` Only the first visitor to an old link triggers the lookup. Later visits come from the cache, with no call to Branch. ## Before you start - **An active paid subscription.** Without one, the dashboard prompts you to upgrade under **Settings → Billing**. - **A custom Branch subdomain you control**, such as `links.acme.com`, with access to its DNS. Links on Branch's shared `*.app.link` domain **can't** be migrated: Grovs can only take over a hostname you control. - **Your Branch key.** See [Finding your Branch key](#finding-your-branch-key). - **An updated app build, shipped before you flip DNS.** > **Warning:** **Ship the new build before you flip DNS.** Grovs serves its own Apple App Site Association and Android assetlinks on the migrated host, replacing Branch's. If DNS moves before your app trusts the host, links open the browser instead of your app. ## Migrate your Branch hostname ### 1. Open Domain settings Go to **Links → Link Settings → Domain** and click **Migrate from another platform**. ### 2. Confirm the pre-flight checklist If your iOS or Android SDK is integrated, confirm each item before you can continue: - **iOS:** `applinks:old.acme.com` is in your Associated Domains entitlement, and the build is on the App Store. - **Android:** your `assetlinks.json` setup has the SHA-256 cert fingerprint, and the build is shipped. - Your users have had time to update. ### 3. Choose Branch and enter your key Select **Branch** and enter your **Branch key**, for example `key_live_...` or `key_test_...`. Grovs uses it to look up the original link when an old URL is opened. ### 4. Enter the old hostname Enter the subdomain you're moving, for example `old.acme.com`. Grovs shows where its DNS points today. Submit it **before** changing DNS: Grovs provisions SSL first, and traffic only moves once DNS points at Grovs. ### 5. Complete the DNS checklist Add each record at your DNS provider as it appears: 1. **Hostname ownership verified:** the ownership `TXT` record, if shown. 2. **SSL certificate issued:** the SSL validation `TXT` record(s). If an `_acme-challenge` record exists, replace its value or add another with the same name. 3. **CNAME pointing to Grovs:** the `CNAME`, once SSL is active. On Cloudflare, set the CNAME to **DNS only** (gray cloud), not **Proxied** (orange). A proxied record blocks the certificate and stops links resolving. Status refreshes as DNS updates. Click **Recheck** to poll sooner. SSL takes a few minutes to a few hours, and Grovs retries for up to 72 hours. If it doesn't issue, check the TXT record and start over to re-create the challenge. ### 6. Test credentials and flip DNS Click **Test** to confirm Grovs can reach Branch with your key: | Result | Action | | ------------------------------- | ----------------------------------------------------------------------------------------- | | **Credentials OK** | Ready. | | **Credentials invalid** | Rotate them in **Settings → Migration** before flipping DNS, or old links stop resolving. | | **Rate limited or unreachable** | Branch is temporarily unavailable. Try again shortly. | Once SSL is active and the test passes, point the old hostname's **CNAME** at Grovs. Traffic moves as DNS resolvers refresh. ## Finding your Branch key In the Branch dashboard, open **Account Settings → Branch Key** and copy the key. Use the `key_live_...` value for production. ## Managing and removing When migration is active, **Links → Link Settings → Domain** shows Branch, the migrated hostname, and a green **Active** badge with the last verification time. - **Rotate credentials:** if your Branch key changes, use **Rotate migration credentials** in **Settings → Migration**. Grovs verifies the new key with Branch before saving. - **Remove migration:** click **Remove migration** and type the hostname to confirm. This removes the migration source and the legacy hostname from Grovs. > **Warning:** Repoint or retire the hostname's DNS before removing the migration. If DNS still points at Grovs, old links stop resolving. Removal also clears cached mappings: recreated links keep working, but if you re-onboard, each old slug hits Branch again on first click. ## Lifecycle and billing If your subscription lapses, Grovs tears down the migrated hostname and clears the cached mappings. Recovery means re-onboarding with fresh credentials. Keep billing active for as long as you rely on migration. --- ## Migrate from AppsFlyer URL: https://docs.grovs.io/docs/migration-guides/appsflyer Description: Move your existing AppsFlyer OneLink subdomain and links to Grovs without breaking old links. When someone opens an old OneLink, Grovs looks it up with your AppsFlyer credentials, recreates it as a Grovs link and redirects to your branded URL. After that first click, Grovs serves it directly. This moves existing links only. To create **new** links and capture attribution through Grovs, also add the [Grovs SDK](https://docs.grovs.io/docs/sdk). ## How it works ```mermaid flowchart TD A[Old OneLink opened]:::evt --> C{Cached?}:::dec C -->|No| D[Look up via AppsFlyer API]:::proc --> E[Create and cache link]:::proc --> R[301 to branded link]:::done C -->|Yes| R ``` Only the first visitor to an old link triggers the lookup. Later visits come from the cache, with no call to AppsFlyer. ## Before you start - **An active paid subscription.** Without one, the dashboard prompts you to upgrade under **Settings → Billing**. - **A custom AppsFlyer subdomain you control**, such as `links.acme.com`, with access to its DNS. Links on AppsFlyer's shared `*.onelink.me` domain **can't** be migrated: Grovs can only take over a hostname you control. - **AppsFlyer OneLink data API access.** The lookup uses this API, a paid AppsFlyer entitlement. Without it, the API rejects the token. Check your AppsFlyer plan first. - **Your OneLink ID and API token.** See [Finding your AppsFlyer credentials](#finding-your-appsflyer-credentials). - **An updated app build, shipped before you flip DNS.** > **Warning:** **Ship the new build before you flip DNS.** Grovs serves its own Apple App Site Association and Android assetlinks on the migrated host, replacing AppsFlyer's. If DNS moves before your app trusts the host, links open the browser instead of your app. ## Migrate your AppsFlyer hostname ### 1. Open Domain settings Go to **Links → Link Settings → Domain** and click **Migrate from another platform**. ### 2. Confirm the pre-flight checklist If your iOS or Android SDK is integrated, confirm each item before you can continue: - **iOS:** `applinks:old.acme.com` is in your Associated Domains entitlement, and the build is on the App Store. - **Android:** your `assetlinks.json` setup has the SHA-256 cert fingerprint, and the build is shipped. - Your users have had time to update. ### 3. Choose AppsFlyer and enter your credentials Select **AppsFlyer** and enter: - **OneLink ID:** for example `abc123`. Grovs uses it to match incoming legacy links. - **API token:** the V2.0 token Grovs uses to read the original link details. ### 4. Enter the old hostname Enter the subdomain you're moving, for example `old.acme.com`. Grovs shows where its DNS points today. Submit it **before** changing DNS: Grovs provisions SSL first, and traffic only moves once DNS points at Grovs. ### 5. Complete the DNS checklist Add each record at your DNS provider as it appears: 1. **Hostname ownership verified:** the ownership `TXT` record, if shown. 2. **SSL certificate issued:** the SSL validation `TXT` record(s). If an `_acme-challenge` record exists, replace its value or add another with the same name. 3. **CNAME pointing to Grovs:** the `CNAME`, once SSL is active. On Cloudflare, set the CNAME to **DNS only** (gray cloud), not **Proxied** (orange). A proxied record blocks the certificate and stops links resolving. Status refreshes as DNS updates. Click **Recheck** to poll sooner. SSL takes a few minutes to a few hours, and Grovs retries for up to 72 hours. If it doesn't issue, check the TXT record and start over to re-create the challenge. ### 6. Test credentials and flip DNS Click **Test** to confirm Grovs can reach AppsFlyer with your credentials: | Result | Action | | ------------------------------- | ----------------------------------------------------------------------------------------- | | **Credentials OK** | Ready. | | **Credentials invalid** | Rotate them in **Settings → Migration** before flipping DNS, or old links stop resolving. | | **Rate limited or unreachable** | AppsFlyer is temporarily unavailable. Try again shortly. | Once SSL is active and the test passes, point the old hostname's **CNAME** at Grovs. Traffic moves as DNS resolvers refresh. ## Finding your AppsFlyer credentials - **OneLink ID:** in your OneLink template configuration. - **API token:** open your account menu, then **Security Center → Manage your AppsFlyer API tokens** (older dashboards: **Settings → API tokens**). Copy the **V2.0** token. ## Managing and removing When migration is active, **Links → Link Settings → Domain** shows AppsFlyer, the migrated hostname, and a green **Active** badge with the last verification time. - **Rotate credentials:** if your token changes, use **Rotate migration credentials** in **Settings → Migration**. Grovs verifies the new credentials with AppsFlyer before saving. - **Remove migration:** click **Remove migration** and type the hostname to confirm. This removes the migration source and the legacy hostname from Grovs. > **Warning:** Repoint or retire the hostname's DNS before removing the migration. If DNS still points at Grovs, old links stop resolving. Removal also clears cached mappings: recreated links keep working, but if you re-onboard, each old slug hits AppsFlyer again on first click. ## Lifecycle and billing If your subscription lapses, Grovs tears down the migrated hostname and clears the cached mappings. Recovery means re-onboarding with fresh credentials. Keep billing active for as long as you rely on migration. --- ## Upgrade guides URL: https://docs.grovs.io/docs/upgrade-guides Description: What changes between major versions of each Grovs SDK, and how to move an existing integration forward. Each guide covers one SDK version: what breaks, what behaves differently without a code change, what's new, and how to verify. Existing integrations keep compiling on every platform. The guides are mostly about behavior to decide on before you ship. | SDK | Current version | Guide | | ------------ | --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | iOS | 3.1.0 | [Upgrading from 3.0](https://docs.grovs.io/docs/upgrade-guides/ios-3-1), [Upgrading from 2.x](https://docs.grovs.io/docs/upgrade-guides/ios-3-0) | | Android | 3.1.0 | [Upgrading from 3.0](https://docs.grovs.io/docs/upgrade-guides/android-3-1), [Upgrading from 1.x](https://docs.grovs.io/docs/upgrade-guides/android-3-0) | | Flutter | 3.2.0 | [Upgrading from 3.0](https://docs.grovs.io/docs/upgrade-guides/flutter-3-2), [Upgrading from 1.x](https://docs.grovs.io/docs/upgrade-guides/flutter-3-0) | | React Native | 3.1.0 | [Upgrading from 3.0](https://docs.grovs.io/docs/upgrade-guides/react-native-3-1), [Upgrading from 1.x](https://docs.grovs.io/docs/upgrade-guides/react-native-3-0) | | Web | 2.1.0 | [Upgrading from 2.0](https://docs.grovs.io/docs/upgrade-guides/web-2-1), [Upgrading from 1.x](https://docs.grovs.io/docs/upgrade-guides/web-2-0) | The native and web releases share one feature set: custom events and screen tracking, consent gating, clipboard-assisted deferred deep linking on mobile, and per-link copy-to-clipboard flags. The Flutter, React Native and Unity wrappers bundle the native SDKs, so a wrapper upgrade brings the native behavior changes too: Flutter 3.2, React Native 3.1 and Unity 1.0 use native 3.1. [SDK Releases](https://docs.grovs.io/docs/sdk/releases) lists where each package publishes release notes. iOS 3.1, Android 3.1, Flutter 3.2 and React Native 3.1 add push campaign opens and in-app messages and remove the messages inbox. Web 2.1 adds in-app messages and the smart banner. > **Note:** Moving from another provider? See [Migration Guides](https://docs.grovs.io/docs/migration-guides). --- ## iOS SDK 3.1 URL: https://docs.grovs.io/docs/upgrade-guides/ios-3-1 Description: What changes when you move the Grovs iOS SDK from 3.0 to 3.1. 3.1.0 adds [push campaigns](https://docs.grovs.io/docs/sdk/ios/push-notifications) and [in-app messages](https://docs.grovs.io/docs/sdk/ios/in-app-messages), and removes the messages inbox. Most apps bump the version and fix what the compiler flags. Requirements are unchanged: **iOS 13.0**, Swift 5.9 (Xcode 15). ## Checklist 1. [Bump the dependency](#1-bump-the-version) to 3.1.0. 2. [Handle the new `GrovsError` case](#2-handle-grovserrorinappdisplayfailed) if you `switch` over it. 3. [Check your `GrovsDelegate` methods](#3-check-your-grovsdelegate-methods) for "nearly matches" warnings. 4. [Remove the messages inbox](#4-remove-the-messages-inbox) from your UI. 5. [Rename custom events that use a reserved name](#5-rename-reserved-event-names). 6. [Check the other behavior changes](#6-other-behavior-changes). 7. Optional: [set up push campaigns](https://docs.grovs.io/docs/sdk/ios/push-notifications) and [react to in-app messages](https://docs.grovs.io/docs/sdk/ios/in-app-messages). ## 1. Bump the version **Swift Package Manager:** set the Grovs package rule to **Up to Next Major Version** from `3.1.0`. In a `Package.swift`: ```swift .package(url: "https://github.com/grovs-io/grovs-iOS.git", from: "3.1.0") ``` **CocoaPods:** in your `Podfile`: ```ruby pod 'Grovs', '~> 3.1' ``` For campaign images in push notifications, add the new `GrovsNotificationService` product or pod to a Notification Service Extension target. See [Campaign images](https://docs.grovs.io/docs/sdk/ios/push-notifications#4-campaign-images-optional). ## 2. Handle `GrovsError.inAppDisplayFailed` New case `inAppDisplayFailed` (5): an in-app message couldn't be drawn. An exhaustive `switch` won't compile until you handle it: ```swift func grovsDidEncounterError(_ error: GrovsError, message: String) { switch error { case .authenticationFailed, .networkRequestFailed, .eventDispatchFailed, .linkGenerationFailed: Log.warning("grovs: \(message)") case .inAppDisplayFailed: Log.info("grovs in-app: \(message)") } } ``` ## 3. Check your `GrovsDelegate` methods `GrovsDelegate` and the new `GrovsInAppDelegate` are now `@objc` protocols with `@objc optional` methods, so Objective-C apps can adopt them. `GrovsError` is an `@objc` enum and `Grovs` an `NSObject`. Most apps stay source-compatible, but the Swift default implementation of `grovsDidEncounterError` is gone. A delegate method that doesn't match the signature exactly compiles with only a "nearly matches" warning and is never called. Build once and check those warnings. ## 4. Remove the messages inbox The inbox is gone on the device and the backend. In-app messages replace it. - `Grovs.displayMessagesViewController(completion:)` and `displayMessagesViewController(completion:onFailure:)` are deprecated and show nothing. They call `completion` once on the main thread and never call `onFailure`. - `Grovs.numberOfUnreadMessages(completion:)` is deprecated and always yields `0`. - The SDK no longer displays dashboard messages automatically, calls the inbox endpoints, or ships the inbox XIBs. - A push carrying only `grovs_notification_id` is no longer a Grovs push: `isGrovsNotification(userInfo:)` and the notification handlers return `false` for it. Remove any code that opened the inbox from such a push. Remove inbox buttons and unread badges. Campaigns built under **Engage** in the dashboard show on their own once the SDK is configured. ## 5. Rename reserved event names `track(_:)` now refuses the same names as the backend. Rename any custom event that uses one: `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`. ## 6. Other behavior changes - **Push opens.** Call `handleNotificationOpen(userInfo:)` only for a tap on a notification, never from `didReceiveRemoteNotification` or `willPresent`, or each delivery counts as an open. A tap on a push that carries a Grovs link counts once, as `push_open`: the link isn't also counted as a link open, and later events aren't attributed to it. - **Screen tracking.** A view controller class named exactly `ViewController` (or `Controller`) is now tracked under that name. Before, its cleaned name was empty and it was never tracked. - **Push token.** A push token set before authentication no longer clears the user's identifier and attributes on the server. - **Other apps' URL schemes.** `handleAppDelegate(open:options:)` returns `false` for a custom scheme other than your project's Grovs URL scheme, such as a sign-in callback, so a handler after Grovs in an `||` chain still gets it. - **Purchases.** `logInAppPurchase` and `logCustomPurchase` save the purchase before sending it, including before authentication. Once configured and enabled, a `false` completion means it's kept and retried, so don't log it again. - **Events before authentication.** `track` and `trackScreenView` calls made before authentication completes are stored on disk, not held in memory. - **Locked after reboot.** On a background launch before the first unlock, `configure` waits until the device is unlocked or the app becomes active. - **Privacy manifest.** It now declares the file timestamp reason (C617.1) for the SDK's disk cache. Review your App Store privacy details if you copy them from the manifest. - **CocoaPods.** `Grovs.podspec` compiles only the main SDK; the extension code ships in its own pod, `GrovsNotificationService`. ## Verify the upgrade 1. Build with no "nearly matches" warnings on your delegate methods. 2. Open a Grovs link and check that `grovsReceivedPayloadFromDeeplink` still receives it. 3. In the dashboard, create an in-app campaign triggered on app open, use **Send test** with your own user identifier, and bring the app to the foreground. The message shows at the next sync. 4. If you use push, send a campaign to your own user identifier and tap it. The open shows in the campaign report. Full list of changes: [iOS CHANGELOG](https://github.com/grovs-io/grovs-iOS/blob/main/CHANGELOG.md). --- ## iOS SDK 3.0 URL: https://docs.grovs.io/docs/upgrade-guides/ios-3-0 Description: What changes when you move the Grovs iOS SDK from 2.x to 3.0. 3.0.0 removes four public types that were SDK internals and changes one closure signature. Most apps bump the version, decide on automatic screen tracking, and check how they use `setSDK(enabled:)`. Requirements are unchanged: **iOS 13.0**, Swift 5.9 (Xcode 15). ## Checklist 1. [Bump the dependency](#1-bump-the-version) to 3.0.0. 2. [Decide on automatic screen tracking](#2-decide-on-automatic-screen-tracking). It's on by default. 3. [Remove references to types that are now internal](#3-remove-references-to-types-that-are-now-internal). 4. [Update `GrovsDeviceDataClosureClosure`](#4-update-grovsdevicedataclosureclosure-if-you-use-it) if you name it. 5. [Review your consent gating](#5-review-your-consent-gating): `setSDK(enabled: false)` now stops everything, and `configure` has an `enabled:` parameter. 6. [Check the other behavior changes](#6-check-the-other-behavior-changes): the clipboard paste prompt, links in messages, and your App Store privacy details. ## 1. Bump the version **Swift Package Manager:** in your project's **Package Dependencies**, select Grovs and set the rule to **Up to Next Major Version** from `3.0.0`. A rule pinned to 2.x won't pick up 3.0. In a `Package.swift`: ```swift .package(url: "https://github.com/grovs-io/grovs-iOS.git", from: "3.0.0") ``` **CocoaPods:** in your `Podfile`, then run `pod update Grovs`: ```ruby pod 'Grovs', '~> 3.0' ``` ## 2. Decide on automatic screen tracking > **Warning:** This changes behavior with no code change on your side. `autoTrackScreenViews` defaults to `true`. An app that only recompiles starts hooking `UIViewController.viewDidAppear` and sending a `screen_view` event for each screen. Keep it for screen analytics. [Events & Screen Tracking](https://docs.grovs.io/docs/sdk/ios/analytics#automatic-screen-tracking) explains how names are chosen and overridden. To opt out, pass `false`: ```swift Grovs.configure( APIKey: "your-api-key", useTestEnvironment: false, autoTrackScreenViews: false, delegate: self ) ``` Existing `configure` calls compile unchanged: every new parameter has a default. ## 3. Remove references to types that are now internal Four types public in 2.x are internal in 3.0. They were implementation details, but code that referenced or subclassed one won't build: | Type | Was | Replacement | | ------------- | --------------------------------------- | -------------------------------- | | `DataCache` | `open class`, plus `DataCache.instance` | None. Use your own cache. | | `BaseService` | `open class` | None. Use `URLSession` directly. | | `ImageFormat` | `public enum` | None. | | `JSONClosure` | `public typealias` | Define your own closure type. | Everything else public in 2.x is still public and source-compatible. ## 4. Update `GrovsDeviceDataClosureClosure` if you use it The typealias gained a leading `success` flag, so a transport failure differs from an empty result: ```swift // 2.x (_ dictionary: [String: Any]?, _ link: String?, _ tracking: [String: Any]?) -> Void // 3.0 (_ success: Bool, _ dictionary: [String: Any]?, _ link: String?, _ tracking: [String: Any]?) -> Void ``` No public method uses this type. If you declared a variable or parameter with it, add the leading parameter. ## 5. Review your consent gating **`setSDK(enabled: false)` now stops everything.** In 2.x it blocked link generation and deep link resolution but kept collecting lifecycle events. In 3.0, while disabled, the SDK doesn't authenticate, resolve the device, or send lifecycle events, custom events, screen views or purchases. Changes to `userIdentifier`, `userAttributes`, `pushToken` and screen aliases are held until you re-enable it. Queued events stay on disk. **Start disabled with `enabled:`.** Calling `setSDK(enabled: false)` right after `configure` is too late: configuring starts the first round of collection. If you gate on consent, pass it to `configure`: ```swift // 2.x — collection has already started when setSDK runs Grovs.configure(APIKey: "your-api-key", useTestEnvironment: false, delegate: self) if !hasConsent { Grovs.setSDK(enabled: false) } // 3.0 Grovs.configure(APIKey: "your-api-key", useTestEnvironment: false, enabled: hasConsent, delegate: self) ``` The value isn't persisted, so pass your stored consent on every launch. A disabled `configure` reports `false` without authenticating. If that completion feeds your error reporting, don't count it as a failure. See [Privacy & Consent](https://docs.grovs.io/docs/sdk/ios/privacy). ## 6. Check the other behavior changes **Clipboard-assisted deferred deep linking is on.** On the first launch after a fresh install, the SDK can read a Grovs link from the clipboard to attribute the install, and iOS may show its paste prompt. It happens only if the backend reports recent clicks on copy-to-clipboard links for your project (currently a 48-hour window) and the clipboard holds a web URL. If you never enable copy-to-clipboard for the project or its links, the prompt never appears. See [Clipboard-assisted deferred deep linking](https://docs.grovs.io/docs/sdk/ios/deep-linking#clipboard-assisted-deferred-deep-linking). **Links in messages are restricted.** Message content now opens only `http`, `https`, `mailto`, `tel`, and `sms` links. Other schemes are ignored. Switch links that use a custom scheme such as `yourapp://` to a universal link. **The SDK ships a privacy manifest.** `PrivacyInfo.xcprivacy` is bundled for Swift Package Manager and CocoaPods and merged into your app's privacy report. Check your App Store privacy details against [what the SDK collects](https://docs.grovs.io/docs/sdk/ios/privacy#what-the-sdk-collects). **Logging moved to `os_log`.** `setDebug(level:)` works as before. In release builds, message content is marked private and appears as `` in Console.app. **Device model is Apple's identifier.** The SDK sends the hardware identifier (for example `iPhone15,2`) instead of a mapped name, so new devices need no SDK update. **Queued events survive the upgrade.** 3.0 reads 2.x event archives, so nothing pending is lost. ## What's new in 3.0 Nothing here is required. - **[Custom events and screen tracking](https://docs.grovs.io/docs/sdk/ios/analytics)**: `track`, `trackScreenView`, global tags, screen aliases, `.grovsScreen()` for SwiftUI, and `GrovsScreenTracking` for UIKit. - **[Consent gating](https://docs.grovs.io/docs/sdk/ios/privacy)**: the `enabled:` parameter on `configure`, and a `setSDK(enabled:)` that stops all collection. - **[Error callbacks](https://docs.grovs.io/docs/sdk/ios/deep-linking#handling-sdk-errors)**: an optional `grovsDidEncounterError(_:message:)` on `GrovsDelegate`. It has a default implementation, so existing delegates need no change. - **[Clipboard-assisted deferred deep linking](https://docs.grovs.io/docs/sdk/ios/deep-linking#clipboard-assisted-deferred-deep-linking)**: deterministic install attribution on first launch, with `clipboardDomains` for custom link domains. - **[`copyToClipboardiOS` / `copyToClipboardAndroid`](https://docs.grovs.io/docs/sdk/ios/link-creation)**: per-link override of the project's copy-to-clipboard setting. - **`displayMessagesViewController(completion:onFailure:)`**: tells a dismissal apart from a presentation that never happened. The messages inbox was later removed in [3.1](https://docs.grovs.io/docs/upgrade-guides/ios-3-1). ## Verifying the upgrade Turn on info logging before `configure` and confirm the completion reports `true`: ```swift Grovs.setDebug(level: .info) Grovs.configure(APIKey: "your-api-key", useTestEnvironment: true, delegate: self) { success in print("Grovs configured: \(success)") } ``` Then check that deep links still reach `grovsReceivedPayloadFromDeeplink`. If you kept automatic screen tracking, check that screen views appear in the dashboard under names you recognize. Rename any that read like class names with [`setScreenAliases`](https://docs.grovs.io/docs/sdk/ios/analytics#screen-name-aliases). ## See also - [Quick Start](https://docs.grovs.io/docs/sdk/ios/quick-start): full configuration reference - [API Reference](https://docs.grovs.io/docs/sdk/ios/api-reference): every property, method, and type - [SDK Releases](https://docs.grovs.io/docs/sdk/releases): where release notes are published --- ## Android SDK 3.1 URL: https://docs.grovs.io/docs/upgrade-guides/android-3-1 Description: What changes when you move the Grovs Android SDK from 3.0 to 3.1. 3.1.0 adds [push campaigns](https://docs.grovs.io/docs/sdk/android/push-notifications) and [in-app messages](https://docs.grovs.io/docs/sdk/android/in-app-messages), and removes the messages inbox along with the dependencies it brought in. Most apps bump the version, add back any dependency they relied on, and fix what the compiler flags. Requirements are unchanged: **Android 5.0 (API 21)**, Kotlin 1.8 or later. ## Checklist 1. [Bump the dependency](#1-bump-the-version) to 3.1.0. 2. [Declare the libraries the SDK no longer brings in](#2-declare-libraries-the-sdk-no-longer-brings-in), if your app uses them. 3. [Remove the messages inbox](#3-remove-the-messages-inbox) from your UI. 4. [Update your own messaging service](#4-update-your-own-messaging-service), if you have one. 5. [Check how you handle push taps](#5-check-how-you-handle-push-taps). 6. [Rename custom events that use a reserved name](#6-rename-reserved-event-names). 7. [Check the other behavior changes](#7-other-behavior-changes). 8. Optional: [set up push campaigns](https://docs.grovs.io/docs/sdk/android/push-notifications), [react to in-app messages](https://docs.grovs.io/docs/sdk/android/in-app-messages#react-to-messages) and [enable app ratings](https://docs.grovs.io/docs/sdk/android/in-app-messages#app-ratings). ## 1. Bump the version In your app-level `build.gradle`: ```groovy dependencies { implementation("io.grovs:Grovs:3.1.0") } ``` The SDK now depends on `androidx.webkit:webkit`, for HTML in-app messages. ## 2. Declare libraries the SDK no longer brings in The SDK used these only for the inbox screens. If your app uses any of them, declare them yourself, or the build fails with missing classes or resources: - `com.google.android.material:material` - `androidx.constraintlayout:constraintlayout` - `androidx.navigation:navigation-fragment-ktx` - `androidx.navigation:navigation-ui-ktx` The SDK now depends on `androidx.navigation:navigation-runtime-ktx` instead, so `trackNavigation` still works. ## 3. Remove the messages inbox The inbox is gone on the device and the backend. In-app messages replace it. - `Grovs.displayMessagesFragment(onDismissed)` is deprecated and shows nothing. It calls `onDismissed` once on the main thread. - Both `Grovs.numberOfUnreadMessages` forms are deprecated and always yield `0`, the callback once on the main thread. They used to yield `null` when the SDK wasn't configured, was disabled or was offline. - `Grovs.setOnAutomaticNotificationsListener` and `GrovsNotificationsListener` are deprecated. The listener is never called. - The SDK no longer displays dashboard messages automatically or calls the inbox endpoints. Remove inbox buttons and unread badges. Campaigns built under **Engage** in the dashboard show on their own once the SDK is configured. These were public and are now removed. Code that referenced them won't compile: - `io.grovs.handlers.NotificationsManager`, and everything in `io.grovs.fragments`, `io.grovs.viewmodels`, `io.grovs.adapters` and `io.grovs.model.notifications`. - The generated `io.grovs.databinding` classes. - The extensions `Fragment.applySystemBarInsets`, `showProgressBar`, `hideProgressBar` and `NavController.safeNavigate`. - `ActivityProvider.requireNotificationsListener`. - The instance methods `displayMessagesFragment`, `numberOfUnreadMessages`, `setOnAutomaticNotificationsListener` and `requireNotificationsListener` on `Grovs` instances. The companion methods `Grovs.displayMessagesFragment` and the others stay. - The four inbox methods on `IGrovsService`. The inbox resources are removed too. If your app overrode or referenced them, delete those references: - The `Grovs*` text and button styles, except `GrovsInAppWindow`. - The strings `grovs_messages` and `grovs_messages_empty`. - The `grovs_*` colors, except `grovs_push_notification_icon_tint`. - The inbox layouts, navigation graph, slide animations and icons. ## 4. Update your own messaging service If your app declares its own `FirebaseMessagingService`, replace `handleGrovsNotification`, now deprecated, with `GrovsMessaging.handleMessage`. In 3.0 it only handled inbox pushes; `handleMessage` handles push campaigns. ```kotlin // 3.0 override fun onMessageReceived(message: RemoteMessage) { if (handleGrovsNotification(message)) return // your own messages } override fun onNewToken(token: String) { super.onNewToken(token) Grovs.pushToken = token } // 3.1 override fun onMessageReceived(message: RemoteMessage) { if (GrovsMessaging.handleMessage(this, message)) return // your own messages } override fun onNewToken(token: String) { GrovsMessaging.onNewToken(token) } ``` You can also extend `io.grovs.handlers.MessagingService` instead. Either way, remove the SDK's service declaration from your merged manifest with `tools:node="remove"`. See [If your app has its own FirebaseMessagingService](https://docs.grovs.io/docs/sdk/android/push-notifications#if-your-app-has-its-own-firebasemessagingservice). ## 5. Check how you handle push taps In 3.0 the SDK ignored Grovs push campaigns: Android showed background pushes, nothing showed foreground ones, and your app read the `grovs_*` intent extras itself. In 3.1: - **Taps reach your deep link listener.** The SDK reports the open and calls `GrovsDeeplinkListener` with the push data, merged over its link's data. If your app also routes from the intent extras, it routes twice. Remove your own routing for Grovs pushes. - **Foreground pushes are shown.** The SDK's messaging service, or `GrovsMessaging.handleMessage`, now shows Grovs pushes while the app is open, with their image. If your app showed them itself, remove that code, or set `GrovsMessaging.showForegroundNotifications = false`. - **Splash screens.** If your launcher starts another activity without its extras, call `Grovs.handleNotificationOpen(intent)` where the tap lands. A push is a Grovs push when its data has a non-blank `grovs_campaign_id`. A blank or whitespace-only ID is no longer treated as one. ## 6. Rename reserved event names `track` now refuses every name the backend reserves. Rename any custom event that uses one of these, newly refused in 3.1: `push_open`, `push_delivered`, `push_held`, `in_app_held`, `journey_entered`, `journey_held`, `journey_path`. ## 7. Other behavior changes - **In-app messages show without code.** Once your project has live in-app campaigns, they show in the app. Use [`pauseInAppMessages`](https://docs.grovs.io/docs/sdk/android/in-app-messages#hold-messages-back) or [`setInAppExcludedScreens`](https://docs.grovs.io/docs/sdk/android/in-app-messages#hold-messages-back) to keep them off flows like checkout. - **Notification permission.** The SDK doesn't declare `POST_NOTIFICATIONS`. On Android 13 and later, in-app campaigns with an "Ask for push permission" button aren't shown unless your app declares it. - **App ratings.** "Rate the app" campaigns need `com.google.android.play:review-ktx` in your app. Without it they're skipped. - **Values set before `configure`.** `identifier`, `attributes` and `pushToken` set before `configure` are kept and sent once the SDK authenticates. They used to be lost. `identifier` and `attributes` set this way still win over the values the server holds. - **Push token.** A token set before authentication no longer clears the user identifier and attributes on the server. Token refreshes reach Grovs without app code. - **Small icon meta-data.** `io.grovs.NotificationIconSmall` accepts `android:resource` as well as a drawable name in `android:value`. - **Screen tracking.** Screens whose class is named `NotificationsMainFragment`, `NotificationsListFragment`, `NotificationDetailsFragment` or `AutoDisplayedNotificationFragment` are now tracked like any other screen. - **Event properties.** Whole numbers in custom event properties are sent as whole numbers (`4`, not `4.0`). - **Custom service implementations.** `BatchEventsResponse` has a new `inApp` field, so its constructor and `copy` changed, and `IGrovsService` has new `inAppSync` and `pushOpen(campaignId, sessionId)` methods. This only affects code that constructs or implements these types. - **Data safety.** Push opens and in-app message interactions are now reported. Check your Google Play Data safety form against [what the SDK collects](https://docs.grovs.io/docs/sdk/android/privacy#what-the-sdk-collects). ## Verify the upgrade 1. Build with no missing-class or missing-resource errors. 2. Open a Grovs link and check that your `GrovsDeeplinkListener` still receives it. 3. In the dashboard, create an in-app campaign triggered on app open, use **Send test** with your own user identifier, and bring the app to the foreground. The message shows at the next sync. 4. If you use push, send a campaign to your own user identifier and tap it, once with the app in the background and once with it open. The open shows in the campaign report, and the listener gets the push data once per tap. Full list of changes: [Android CHANGELOG](https://github.com/grovs-io/grovs-Android/blob/main/CHANGELOG.md). --- ## Android SDK 3.0 URL: https://docs.grovs.io/docs/upgrade-guides/android-3-0 Description: What changes when you move the Grovs Android SDK from 1.x to 3.0. 3.0.0 keeps every public method and property from 1.1.1. Kotlin code using named arguments compiles unchanged. The one signature change: both `generateLink` overloads gained `copyToClipboardIos` and `copyToClipboardAndroid` before `tracking`. This breaks positional Kotlin calls and Java calls that passed `tracking`. The rest is behavior: automatic screen tracking is on by default, `setSDK(false)` stops all collection, and clipboard-assisted deferred deep linking runs on first launch. Most apps bump the version, decide on screen tracking, and check their consent gating. Requirements are unchanged: **Android 5.0 (API 21)**, Kotlin 1.8 or later. > **Note:** There is no 2.x. Android goes from 1.1.1 to 3.0.0 so all Grovs native SDKs share one major version and feature set. ## Checklist 1. [Bump the dependency](#1-bump-the-version) to 3.0.0. 2. [Decide on automatic screen tracking](#2-decide-on-automatic-screen-tracking): it is on by default. 3. [Review your consent gating](#3-review-your-consent-gating): `setSDK(false)` now stops everything, and `configure` has an `enabled` parameter. 4. [Fix `PaymentEventType` names](#4-check-paymenteventtype-names) if you followed older docs. 5. [Check the other behavior changes](#5-check-the-other-behavior-changes): the clipboard toast, event batching, link lookup retries, and your Data safety form. ## 1. Bump the version In your app-level `build.gradle`: ```groovy dependencies { implementation("io.grovs:Grovs:3.0.0") } ``` Then sync Gradle. The SDK adds `androidx.fragment:fragment-ktx`, already reachable transitively, and still declares only the `INTERNET` and `ACCESS_NETWORK_STATE` permissions. ## 2. Decide on automatic screen tracking > **Warning:** This changes behavior with no code change on your side. `autoTrackScreenViews` defaults to `true`. An app that only bumps the version starts reporting a `screen_view` event for every Activity and Fragment that resumes. Keep it for screen analytics. See [Events & Screen Tracking](https://docs.grovs.io/docs/sdk/android/analytics#automatic-screen-tracking) for what is and is not tracked (Fragments over their host Activity, no modals, no Compose) and how to name screens. To opt out, use the overload that takes `autoTrackScreenViews`: ```kotlin Grovs.configure(this, "your-api-key", useTestEnvironment = false, baseURL = null, autoTrackScreenViews = false) ``` The 1.1.1 `configure(application, apiKey, useTestEnvironment, baseURL)` overload still compiles. It now means `autoTrackScreenViews = true`. ## 3. Review your consent gating **`setSDK(false)` now stops everything.** In 1.1.1 it blocked link generation, link details and deep link resolution but kept collecting lifecycle events. In 3.0, while disabled, the SDK doesn't authenticate, resolve the device, read the clipboard, or send lifecycle events, custom events, screen views or purchases. Outstanding requests and retries are cancelled. Changes to `identifier`, `attributes`, `pushToken` and screen aliases are held until you re-enable it. Queued events stay on the device. **Start disabled with `enabled`.** Calling `setSDK(false)` right after `configure` is too late: configuring starts the first round of collection. If you gate on consent, pass it to `configure`: ```kotlin // 1.x — collection has already started when setSDK runs Grovs.configure(this, "your-api-key", useTestEnvironment = false) if (!hasConsent) { Grovs.setSDK(false) } // 3.0 Grovs.configure( this, "your-api-key", useTestEnvironment = false, baseURL = null, autoTrackScreenViews = true, clipboardDomains = null, enabled = hasConsent ) ``` The value isn't persisted, so pass your stored consent on every launch. The shorter `configure` overloads reset it to `true`. See [Privacy & Consent](https://docs.grovs.io/docs/sdk/android/privacy). **Enabling doesn't replay a launch link.** If a link opened the app while disabled, forward `onStart` or `onNewIntent` again after enabling. ## 4. Check `PaymentEventType` names The enum has always been `BUY`, `CANCEL` and `REFUND`. Older docs listed `CANCELLATION`, which never existed, so a working app has nothing to change. Use `PaymentEventType.CANCEL` for cancellations. ## 5. Check the other behavior changes **Clipboard-assisted deferred deep linking is on.** On first launch after install, the SDK reads the clipboard to attribute the install when fingerprint matching found nothing and the backend reports recent clicks on copy-to-clipboard links (currently a 48-hour window). On Android 12 and later the system shows its "pasted from your clipboard" toast once. If you never enable copy-to-clipboard for the project or its links, the clipboard is never read. See [Clipboard-assisted deferred deep linking](https://docs.grovs.io/docs/sdk/android/deep-linking#clipboard-assisted-deferred-deep-linking). **Events are batched.** Lifecycle events and custom events are sent through a batch endpoint, with custom events flushed every 30 seconds. Up to 1,000 events are kept for seven days. **Install attribution has a 25-second budget.** Events queued during a fresh install wait until the install-referrer lookup, fingerprint match and clipboard fallback resolve, or the budget runs out, so they're credited to the right link. **Link lookups recover from outages differently.** In 1.1.1 a failed lookup retried inside the call indefinitely, every 5 then 10 seconds. In 3.0 in-call retries are bounded. A tapped link whose lookup still failed is retried at the session level: after a widening wait, on foreground, and when the network returns. The listener fires once it succeeds. **Purchases carry a session.** Purchase events now include the analytics session they were logged in. **Update your Data safety form.** Screen views and custom events are new categories of collection. Check your Google Play Console declarations against [what the SDK collects](https://docs.grovs.io/docs/sdk/android/privacy#what-the-sdk-collects). ## What's new in 3.0 Nothing here is required. - **[Custom events and screen tracking](https://docs.grovs.io/docs/sdk/android/analytics)**: `track`, `trackScreenView`, `trackNavigation` for Jetpack Navigation, global tags, and screen aliases. - **[Consent gating](https://docs.grovs.io/docs/sdk/android/privacy)**: the `enabled` parameter on `configure`, and a `setSDK` that stops all collection. - **[Clipboard-assisted deferred deep linking](https://docs.grovs.io/docs/sdk/android/deep-linking#clipboard-assisted-deferred-deep-linking)**: deterministic install attribution on first launch, with `clipboardDomains` for custom link domains. - **[`copyToClipboardIos` / `copyToClipboardAndroid`](https://docs.grovs.io/docs/sdk/android/link-creation)**: per-link override of the project's copy-to-clipboard setting. ## Verifying the upgrade Turn on info logging before `configure` and filter Logcat by the message text `GROVS` (the tag is `Logger`): ```kotlin Grovs.setDebug(LogLevel.INFO) Grovs.configure(this, "your-api-key", useTestEnvironment = true) ``` Then check that deep links still reach your `GrovsDeeplinkListener`. If you kept automatic screen tracking, check that screen views appear in the dashboard under names you recognize. Use [`setScreenAliases`](https://docs.grovs.io/docs/sdk/android/analytics#screen-name-aliases) to rename any that read like class names. ## See also - [Quick Start](https://docs.grovs.io/docs/sdk/android/quick-start): full configuration reference - [API Reference](https://docs.grovs.io/docs/sdk/android/api-reference): every property, method, and type - [SDK Releases](https://docs.grovs.io/docs/sdk/releases): where release notes are published --- ## Web SDK 2.1 URL: https://docs.grovs.io/docs/upgrade-guides/web-2-1 Description: What changes when you move the Grovs Web SDK from 2.0 to 2.1. 2.1.0 adds [in-app messages](https://docs.grovs.io/docs/sdk/web/in-app-messages) and the [smart banner](https://docs.grovs.io/docs/sdk/web/smart-banners), and removes the messages inbox. Existing code keeps working. Both new features are on by default, so decide on them before you ship. ## Checklist 1. [Install 2.1.0](#1-install-210). 2. [Decide on the smart banner and in-app messages](#2-decide-on-the-smart-banner-and-in-app-messages): both are on by default. 3. [Remove the messages inbox](#3-remove-the-messages-inbox) from your UI. 4. [Handle the new `GrovsError` code](#4-handle-grovserrorinappdisplayfailed) if you switch over it. 5. [Rename custom events that use a reserved name](#5-rename-reserved-event-names). 6. [Check your Content-Security-Policy](#6-check-your-content-security-policy) if you send one. 7. [Check the other changes](#7-other-changes). ## 1. Install 2.1.0 ```bash npm install grovs@2.1.0 ``` ## 2. Decide on the smart banner and in-app messages Nothing shows until a banner or a campaign is configured in the dashboard. After that, they show without a code change. - **Smart banner.** Shown on iPhone, iPad and Android browsers only. To keep it off, pass `smartBanner: false` to `configure()`. Pass it in your own app's webviews too. If you keep it, call `Grovs.smartBanner.setLinkData()` on product pages so **Open in app** lands on the right screen, and offset fixed headers with `--grovs-smart-banner-top`. See [Smart banners](https://docs.grovs.io/docs/sdk/web/smart-banners). - **In-app messages.** To hold them back on some pages, use `Grovs.inApp.setExcludedScreens()` or `Grovs.inApp.pause()`. See [In-app messages](https://docs.grovs.io/docs/sdk/web/in-app-messages). The deprecated 1.x constructor (`new Grovs(apiKey, …)`) never shows the smart banner; use `Grovs.configure()` to get it. ## 3. Remove the messages inbox The inbox UI and automatic message display are gone on the device and the backend. The methods remain as no-ops that make no request: | Method | Now | | ---------------------------------- | ------------------------------------------ | | `Grovs.showMessagesList()` | does nothing | | `Grovs.displayAutomaticMessages()` | does nothing | | `Grovs.getMessages(page)` | resolves `[]` | | `Grovs.numberOfUnreadMessages()` | resolves `0` | | `Grovs.markMessageAsRead(id)` | resolves `true`, also before `configure()` | The v1 `showMessagesList()` does nothing, and `getMessages()`, `getNumberOfUnreadMessages()` and `markMessageAsRead()` call their success callback with `[]`, `0` and `true`. The `messagesTheme` option is accepted and ignored; the `MessagesTheme` and `GrovsMessage` types remain for existing imports. Remove inbox buttons and unread badges from your pages. ## 4. Handle `GrovsError.inAppDisplayFailed` New code `inAppDisplayFailed` (5): an in-app message couldn't be drawn. An exhaustive `switch`, such as one with a TypeScript `never` check, needs a case for it: ```typescript import Grovs, { GrovsError } from "grovs"; await Grovs.configure({ apiKey: "your-api-key", onError: (code, message) => { switch (code) { case GrovsError.inAppDisplayFailed: console.info("grovs in-app:", message); break; default: reportToSentry(message); } }, }); ``` ## 5. Rename reserved event names `track()` refuses every name the backend rejects. Rename any custom event that uses one: `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`. ## 6. Check your Content-Security-Policy HTML in-app messages run in a sandboxed iframe that inherits your page's CSP. They need inline scripts allowed in `script-src`, with no nonce, hash or `'strict-dynamic'`. A nonce-, hash- or `'strict-dynamic'`-based policy can't run them; the SDK then skips the message and reports `inAppDisplayFailed` once. The built-in templates work under any policy. See [Content-Security-Policy](https://docs.grovs.io/docs/sdk/web/in-app-messages#content-security-policy). ## 7. Other changes - **Bundle size.** About 15 KB more compressed, about 40 KB in all, because in-app messages and the smart banner ship in the one file. - **Same-settings `configure()`.** Calling `configure()` again with the same settings on an authenticated client keeps the client and takes the new callbacks: no re-authentication, no identity re-push, and the smart banner doesn't flicker or count a second view. - **SPA navigation.** Route tracking moved into a shared observer, so the banner follows route changes even with `autoTrackScreenViews: false`. Screen tracking behaves as before. - **New events.** `smart_banner_view`, `smart_banner_click` and `smart_banner_dismiss`, plus the in-app events above, appear in your analytics. ## Verify the upgrade 1. Open a Grovs link to your site and check that `onDeeplink` still receives the payload. 2. On a phone browser, open a page a dashboard banner targets and check the banner, then tap **Open in app**. 3. In the dashboard, create an in-app campaign for web, use **Send test** with your own user identifier, then switch away from the tab and back after a minute. The message shows at that sync. Full list of changes: [Web SDK CHANGELOG](https://github.com/grovs-io/grovs-js/blob/main/CHANGELOG.md) and [MIGRATION.md](https://github.com/grovs-io/grovs-js/blob/main/MIGRATION.md). --- ## Web SDK 2.0 URL: https://docs.grovs.io/docs/upgrade-guides/web-2-0 Description: What changes when you move the Grovs Web SDK from 1.x to 2.0. 2.0.0 is a TypeScript rewrite of the Web SDK that brings it to parity with the iOS and Android SDKs. **Your existing 1.x code keeps working**: the constructor and every callback method are preserved as a deprecated compatibility layer. Two accessors now return corrected values, and the SDK starts sending events that 1.x never sent. > **Note:** The Web SDK's current major version is 2.0.0, not 3.0.0. The 3.0 major used by the native SDKs is where the deprecated 1.x layer is scheduled for removal. ## Checklist 1. [Bump the dependency](#1-bump-the-version) to 2.0.0. 2. [Fix code that compensated for the swapped accessors](#2-check-useridentifier-and-userattributes). 3. [Expect a step change in your dashboards](#3-events-now-actually-fire). 4. [Move to the new API](#4-move-to-the-new-api) at your own pace. The 1.x surface is removed in 3.0. 5. [Check the other behavior changes](#5-check-the-other-behavior-changes). ## 1. Bump the version ```bash npm install grovs@^2.0.0 ``` The package now publishes only `dist/`, with ESM, CommonJS, and IIFE builds plus type declarations. If you imported anything from `grovs/src`, that path no longer exists. ## 2. Check `userIdentifier()` and `userAttributes()` > **Warning:** This is the only 1.x behavior deliberately not preserved. 1.x assigned the authenticate response backwards, putting the user identifier into the attributes slot and the attributes into the identifier slot. 2.0 fixes it: | Call | 1.x returned | 2.0 returns | | ------------------ | ----------------------- | ------------------- | | `userIdentifier()` | the user **attributes** | the user identifier | | `userAttributes()` | the user **identifier** | the user attributes | If you worked around this by reading `userAttributes()` for the identifier, **remove that workaround**. It still compiles and runs after upgrading, but returns the wrong value. ## 3. Events now actually fire 1.x shipped an event queue whose `addEvent()` had no callers, so the web reported zero installs, opens, and engagement time. 2.0 emits the full set: `install` and `reinstall`, `app_open`, `time_spent`, `reactivation`, plus your custom events. **Expect a step change in your dashboards on the day you upgrade.** The series was flat zero before and will not be afterwards. That is the defect being fixed, but it looks like an anomaly if nobody was told. ## 4. Move to the new API Constructing the default export returns the deprecated 1.x surface, so existing npm and CDN integrations (`new Grovs(...)` and `new Grovs.default(...)`) keep working without edits. The same class is also reachable as `Grovs.V1`. Each deprecated method logs one warning naming its replacement. ```javascript // 1.x — still works, deprecated import Grovs from "grovs"; const sdk = new Grovs("your-api-key", false, (data) => console.log(data)); await sdk.start(); // 2.0 import Grovs from "grovs"; await Grovs.configure({ apiKey: "your-api-key", testEnvironment: false, onDeeplink: (payload) => console.log("deep link", payload), onError: (code, message) => console.error("grovs error", code, message), }); ``` > **Warning:** The constructor and `Grovs.configure()` each build their own client. Using both on one page runs sessions, launch events, and timers twice. Pick one; the SDK warns if it sees both. ### Method mapping | 1.x | 2.0 | | ------------------------------------------------------ | -------------------------------------------------------------------------------- | | `new Grovs(key, testEnv, cb)` + `.start()` | `Grovs.configure({ apiKey, testEnvironment, onDeeplink })` | | `createLink(title, subtitle, imageURL, data, ok, err)` | `generateLink({ title, subtitle, imageURL, data })` → Promise | | `setUserIdentifier`, `setUserAttributes` | same signature, **corrected values** | | `userIdentifier()`, `userAttributes()` | `Grovs.userIdentifier`, `Grovs.userAttributes` (accessors), **corrected values** | | `authenticated()` | `isAuthenticated()` | | `getAllReceivedData()` | `allReceivedPayloadsSinceStartup()` | | `showMessagesList()` | unchanged, now returns a Promise | | `getMessages(page, ok, err)` | `getMessages(page)` → Promise | | `getNumberOfUnreadMessages(ok, err)` | `numberOfUnreadMessages()` → Promise | | `markMessageAsRead(msg, ok, err)` | `markMessageAsRead(id)` → Promise | ## 5. Check the other behavior changes **Errors reach your code.** Pass `onError` to `configure`. Every failure in 1.x was a bare `console.log`, so a broken install was undetectable. See [Handling SDK errors](https://docs.grovs.io/docs/sdk/web/deep-linking#handling-sdk-errors). **Automatic screen tracking is on.** `autoTrackScreenViews` defaults to `true`, so single-page-app route changes fire `screen_view` events. This applies to the 1.x constructor too, which cannot turn it off. Pass `autoTrackScreenViews: false` to `Grovs.configure()` to opt out, and use [aliases](https://docs.grovs.io/docs/sdk/web/analytics#screen-name-aliases) to collapse high-cardinality URLs. **`PROJECT-KEY` replaces `PROJECT_KEY`** on the wire. No action needed on Grovs cloud. If you self-host behind nginx, note that nginx drops underscored headers under its default configuration, so this removes a latent 403. **Importing during server rendering no longer throws.** Call `configure` on the client; a server-side call logs and reports through `onError` once per call. **Optional `cookieDomain`.** 1.x wrote a host-only cookie. Set `cookieDomain` if you need identity to span subdomains. A host-only cookie left by 1.x that shadows the new one is repaired on the next write. **Stored state is per project.** The queue, session, and counters are keyed by project and by `testEnvironment`. Launch counters written by 1.x are carried over, so a returning visitor is not reported as a reinstall; an unsent 1.x queue has no known owner and is dropped rather than sent under the current project. **The messages UI is redesigned.** It renders in a shadow root, no longer loads a third-party font, no longer paints its backdrop red, and renders titles as text rather than HTML. It was themeable through `messagesTheme`. The messages inbox was removed in [2.1](https://docs.grovs.io/docs/upgrade-guides/web-2-1); in-app messages replace it. **Purchases and custom redirects are now sent correctly.** 1.x sent both under field names the backend does not read, so they were silently dropped. ## What's new in 2.0 Nothing here is required. - **[Custom events and screen tracking](https://docs.grovs.io/docs/sdk/web/analytics)**: `track`, `trackScreenView`, `setGlobalTags`, `setScreenAliases`, `screenNameProvider`, and `flush`. - **[Consent gating](https://docs.grovs.io/docs/sdk/web/privacy)**: `requireConsent`, `grantConsent()`, and `reset()`. - **[The remaining `generateLink` parameters](https://docs.grovs.io/docs/sdk/web/link-creation)**: tags, custom redirects, preview and copy-to-clipboard flags, and UTM tracking, plus `linkDetails`. - **[`logCustomPurchase`](https://docs.grovs.io/docs/sdk/web/revenue)**: for Grovs Enterprise backends. - **A ` ``` Banners are on once `configure()` runs, with no other code. The v1 constructor (`new Grovs(apiKey, …)`) never shows banners. ### 3. Create a banner Click **Create your first banner**, or the create button above the list. The editor has five sections. ****Details**** - **Name**: for your team only, up to 100 characters. - **Platforms**: **iOS** (iPhone and iPad), **Android**, or both. ****Targeting**** Leave **Show on all pages** on, or turn it off and list paths: - **Only on pages**: one path per line, starting with `/`. `*` matches anything, including `/`. For Streamly: `/shows/*`. - **Except on pages**: optional, and wins over the list above. For example `/checkout*`. Up to 20 paths in each list, 256 characters each. See [how paths match](https://docs.grovs.io/docs/links/smart-banners/behaviour#which-pages-show-it). ****Content**** | Field | Limit | | --------------- | ------------------------------------------------------------------------------------------------------------ | | **Title** | Required, up to 80 characters. Shown on one line | | **Subtitle** | Optional, up to 120 characters. Shown on one line | | **Button text** | Required, up to 24 characters. Default `Open` | | **App icon** | Optional. A square PNG, JPEG or WebP, at least 180×180 px, up to 1 MB. Without one, the banner shows no icon | ****Style**** - **Position**: **Top** sits above the page and pushes it down. **Bottom** is fixed to the bottom of the screen, over the page. - **Colours**: **Background**, **Text**, **Button** and **Button text**, as six-digit hex colours such as `#4F46E5`. ****Behaviour**** - **When a visitor closes it**: how long to hide it after they tap ×. Default 7 days. - **When a visitor taps Open**: how long to hide it after they tap the button. Default 30 days. Choose **Until the next page**, 1 to 90 days, or **Custom…** up to 365 days. **Preview on** shows the banner on an iPhone in Safari and an Android phone in Chrome. ### 4. Publish Click **Publish** to make the banner live, or **Save draft** to finish later. A live banner reaches visitors within about 10 minutes; the list of banners is cached for that long. ### 5. Tell the banner what the page is about (recommended) By default the button uses the banner's default link, with no page data. To open the app on the current show, call `setLinkData` on each page: ```javascript Grovs.smartBanner.setLinkData({ data: { show_id: "4815" } }); ``` Your app receives `data` plus `page_path`, the page's path, through its normal deep link callback. Pass your own `page_path` to override it. In a single-page app, call it again after each route change. A call made before `configure()` is kept and applied once it runs. Values that are `undefined` or `null` are left out, so `{ show_id: show?.id }` is fine. | Rule | Limit | | ------------- | -------------------------------------------------------------------- | | Keys | Up to 20, each up to 64 characters | | Values | Strings up to 512 characters, numbers or booleans. No nested objects | | `page_path` | Starts with `/`, up to 512 characters | | Whole payload | Up to 4 KB | Data over a limit isn't sent: the banner keeps its default link and the SDK logs a warning. The same data always gets the same link, so busy pages don't create a new link per visit. Full API: [Web SDK smart banners](https://docs.grovs.io/docs/sdk/web/smart-banners). ## Make room for the banner A top banner pushes the page down; a bottom banner floats over it. If your site has its own fixed header or footer, offset it with the two CSS variables the SDK sets on ``: ```css .site-header { top: var(--grovs-smart-banner-top, 0); } .cookie-bar { bottom: var(--grovs-smart-banner-bottom, 0); } ``` `--grovs-smart-banner-top` follows the top banner's visible height and shrinks to 0 as the page scrolls it away. Both are removed when the banner goes. ## Control it from code ```javascript Grovs.smartBanner.hide(); // hide on this page only; not a dismissal Grovs.smartBanner.refresh(); // run the banner selection again Grovs.configure({ apiKey: "your-api-key", smartBanner: false }); // never show banners ``` Pass `smartBanner: false` inside your own app's web views, where the banner would otherwise show to people who already have the app. If you use `requireConsent`, the banner waits for `grantConsent()`. ## Manage banners The list shows each banner's **Priority**, **Status**, **Platforms**, **Position** and its last 30 days: **Views**, **Clicks**, **CTR**, **Dismisses**, **Dismiss rate** and **Installs**. - **Order.** When several banners match a page, the one highest in the list shows. Drag a banner, or use **Move to top** and **Move to bottom**. Clear the search and filters to reorder. - **Status.** **Publish** or **Resume** makes a banner live; **Pause** stops it. **Archive** moves it to the archived banners and out of the order; **Restore** brings it back paused, at the bottom. - ****Duplicate**** copies every setting and the icon into a new draft with its own default link. - ****Delete**** removes the banner. Links it already handed out keep working. ## Related - [How banners behave](https://docs.grovs.io/docs/links/smart-banners/behaviour) - [Smart banners in the Web SDK](https://docs.grovs.io/docs/sdk/web/smart-banners) --- ## How smart banners behave URL: https://docs.grovs.io/docs/links/smart-banners/behaviour Description: When a smart banner shows, which pages and devices it matches, how it hides after a close or a tap, what the button opens and what the stats count. ## When a banner shows The Web SDK shows a banner on a page when all of this holds: 1. `Grovs.configure()` has run, banners aren't turned off with `smartBanner: false`, and, under `requireConsent`, consent was granted. 2. The project has at least one live banner. 3. The visitor is on a phone or tablet the banner targets (see [Platforms](#platforms)). 4. The page matches the banner's pages (see below). 5. The visitor hasn't closed the banner or tapped its button recently (see [Hiding](#hiding-after-a-close-or-a-tap)). When several banners pass, the one highest in the dashboard list wins. Only one banner shows at a time, even if two copies of the SDK run on the page. ## Platforms | Visitor | Sees the banner | | --------------------------------------- | ----------------------------------- | | iPhone or iPad, in any browser | When the banner targets **iOS** | | Android phone or tablet, in any browser | When the banner targets **Android** | | Desktop browsers | Never | Detection uses the browser's user agent. iPads that present themselves as a Mac are recognised by their touch screen. The banner also shows inside web views, including your own app's. Pass `smartBanner: false` to `configure()` in your app's web views. Inside the built-in browsers of Instagram, Facebook and similar apps, the button may not be able to open your app. ## Which pages show it Paths in **Only on pages** and **Except on pages** are matched against the page's path: - Only the path counts. The query string and `#anchor` are ignored, so `/shows/4815?ref=mail` matches `/shows/*`. - `*` matches any run of characters, including `/`, so `/shows/*` matches `/shows/4815/episodes`. Everything else is literal. - Matching ignores letter case and a trailing `/`, and decodes the path, so `/Shows/` matches `/shows`. - With **Show on all pages**, every page matches. A page that matches **Except on pages** never shows the banner. **Hash-routed sites.** When the URL's hash starts with `#/` or `#!/`, as in `shop.com/#/product/42`, the hash is the path: `/product/42`. An app mounted under a path (`/app/#/product/42`) matches on the hash route alone, so write `/product/*`, not `/app/product/*`. Other hashes, such as `#reviews`, are plain anchors. **Single-page apps.** The SDK follows route changes on its own (`pushState`, `replaceState`, back and forward, hash changes), whatever your screen-tracking settings. A new path is a new page: the banner is chosen again, the link data you set is cleared, and a **Until the next page** hide ends. A change to only the query string or the anchor is the same page. ## What the button opens The button is a Grovs link: - **App installed:** the app opens and receives the link's data. - **Not installed:** the visitor goes to the App Store or Google Play. After install, your app receives the link's data on first open. On pages where you called `setLinkData()`, the link carries that data plus `page_path`; elsewhere it's the banner's default link, with no page data. A tap before the page's link has arrived uses the default link. The link opens in the same tab and never shows a preview page. Banner links don't credit a referrer, and they're kept out of your links list, per-link metrics and CSV exports. They still count in your project's totals. They can't be edited or deleted as ordinary links, and they keep working after the banner is deleted. Each banner hands out at most 10,000 different page links. After that, pages get the default link. ## Hiding after a close or a tap | The visitor | Hidden for | Default | | --------------- | ---------------------------- | ------- | | Taps × | **When a visitor closes it** | 7 days | | Taps the button | **When a visitor taps Open** | 30 days | **Until the next page** (0 days) hides it only until the visitor moves to another page. Hiding is per banner: closing one doesn't hide another that targets other pages. It's stored in the browser's local storage, so it applies to that browser and device only. Clearing site data brings the banner back. `Grovs.smartBanner.hide()` hides the banner on the current page without counting as a close. "Open" means the tap. The SDK can't tell whether the app actually opened. ## How it looks - **Top** banners are placed at the top of the page and push it down. They scroll away with the page. - **Bottom** banners are fixed to the bottom of the screen, above the page and clear of the home indicator. The banner is drawn in its own shadow DOM, so your site's CSS doesn't change it and its CSS doesn't leak into your page. It works under a Content-Security-Policy that restricts styles. Title and subtitle are each one line, cut short with an ellipsis when too long. Colours are the ones you set; the banner doesn't switch for dark mode. The button is an ordinary link, and the close button is labelled "Close" for screen readers. ## What's counted The SDK sends three events with the banner's id: | Event | When | | ---------------------- | -------------------------------------------- | | `smart_banner_view` | Once per page view per banner, when it shows | | `smart_banner_click` | A tap on the button | | `smart_banner_dismiss` | A tap on × | The dashboard shows each banner's last 30 days: | Stat | Means | | ------------------------------------ | -------------------------------------------------------------------------------------------------- | | **Views**, **Clicks**, **Dismisses** | Counts of the three events | | **CTR** | Clicks divided by views | | **Dismiss rate** | Dismisses divided by views | | **Installs** | Installs and reinstalls that came through the banner's links, the default link and every page link | ## Updates and failures The list of banners is cached for 10 minutes, on the server and in the visitor's browser tab, so a change in the dashboard reaches visitors within about 10 minutes. If a request fails, the SDK waits 60 seconds, or longer if the server asks it to, before trying again, across page loads in the same tab. A banner already showing stays. ## Related - [Set up smart banners](https://docs.grovs.io/docs/links/smart-banners/set-up) - [Smart banners in the Web SDK](https://docs.grovs.io/docs/sdk/web/smart-banners) --- ## Analytics URL: https://docs.grovs.io/docs/analytics Description: See where users come from, what they do and who they are: reports, custom dashboards, segments and usage. - [Reports and metrics](https://docs.grovs.io/docs/analytics/overview): What the Links, Campaigns, Visitors and Referrals pages show, and what each metric means. - [Dashboards](https://docs.grovs.io/docs/analytics/dashboards): The built-in boards on Home, and boards of your own with up to 20 widgets each. - [Segments](https://docs.grovs.io/docs/analytics/segments): Group users by events, devices, profile, acquisition and purchases, then count, export and target them. - [Monthly active users](https://docs.grovs.io/docs/analytics/maus): How Grovs counts MAUs for your plan. ## Where things are in the dashboard | Sidebar | Page | Covered in | | ------------- | -------------------------------- | --------------------------------------------------------------------------- | | **Home** | Built-in and custom boards | [Dashboards](https://docs.grovs.io/docs/analytics/dashboards) | | **Links** | **Dynamic Links**, **Campaigns** | [Reports and metrics](https://docs.grovs.io/docs/analytics/overview) | | **Audience** | **Visitors**, **Referrals** | [Reports and metrics](https://docs.grovs.io/docs/analytics/overview) | | **Audience** | **Segments** | [Segments](https://docs.grovs.io/docs/analytics/segments) | | **Analytics** | **Events** | [Reports and metrics](https://docs.grovs.io/docs/analytics/overview#events) | | **Analytics** | **Revenue** | [Revenue](https://docs.grovs.io/docs/revenue) | To send events to your own systems, see [Data](https://docs.grovs.io/docs/data). --- ## Reports and metrics URL: https://docs.grovs.io/docs/analytics/overview Description: What the Links, Campaigns, Visitors, Referrals and Events pages show, and what each metric means. Each report shows the same events from a different angle. For charts you design yourself, see [Dashboards](https://docs.grovs.io/docs/analytics/dashboards). ## Metrics | Metric | Meaning | | ------------- | ---------------------------------------------------------------------------------------------------------------- | | Views | A link was opened in a web browser. | | Opens | The app was opened as a direct result of a link. | | Installs | The app was installed after someone accessed a link. | | Reinstalls | Someone who had uninstalled the app installed it again through a link. | | Reactivations | Someone returned to the app after 7 or more days without opening it. | | Time spent | Total time spent in the app by the devices in the row. | | Revenue | Purchases attributed to the row, when revenue tracking is on. See [Revenue](https://docs.grovs.io/docs/revenue). | ## Links **Links → Dynamic Links** lists links created in the dashboard with views, opens, installs, reinstalls, reactivations, time spent and revenue. Links created through the SDK or the API are not listed. ## Campaigns **Links → Campaigns** totals the metrics of every link in a campaign: views, opens, installs, reinstalls, reactivations, app opens, users referred and revenue. **Users referred** counts installs that came directly from the campaign's referrals. ## Visitors **Audience → Visitors** shows one row per visitor, whichever link or campaign brought them. A visitor who clicks a link without the app gets a **view**. If they then install and open the app, they also get an **install** and an **open**. | Column | Meaning | | -------------- | ------------------------------------------------------------ | | SDK Identifier | The user identifier your app set through the SDK, if any. | | Platform | The visitor's platform. | | Views | Times the visitor opened a Grovs link in a browser. | | Opens | Times the visitor opened the app from a Grovs link. | | Installs | Whether the visitor installed the app (0 or 1). | | Reinstalls | Times the visitor reinstalled the app through Grovs links. | | Reactivations | Times the visitor came back after 7 or more days. | | Invited Users | Users who installed the app after this visitor invited them. | | Time spent | Total time the visitor spent in the app. | ## Referrals **Audience → Referrals** shows the people each visitor invited and what they did: views, opens, installs, reinstalls, reactivations, invited users and time spent. Use it to find referrers who bring in active users. To work with a group of users, build a [segment](https://docs.grovs.io/docs/analytics/segments). > **Note:** Referral tracking needs a **user identifier** set in the SDK, such as your database user ID. Without one, Grovs cannot tell who invited whom. ## Events **Analytics → Events** lists every event your app sends: link events, app opens, screen views, custom events and purchases. Filter by event fields and pick a date range. `contains` and `is not` filters, and free-text search, only reach the recent part of your plan's data retention. On an older range the page shows "Filters can't search this far back". Narrow the range or remove those filters. --- ## What are MAUs? URL: https://docs.grovs.io/docs/analytics/maus Description: How Grovs counts monthly active users, and what happens when you pass your plan's limit. A monthly active user (MAU) is a unique visitor that sends at least one event to Grovs in a calendar month. Visitors include people who open your links in a browser and devices running your app with the Grovs SDK. - The count covers the project's **production and test** environments together. - It resets to zero on the first day of each month. - See the current count under **Settings → Usage & Billing**. ## The free allowance Without a paid plan, a project can have up to **10,000 MAUs** a month. Grovs checks the count about every 10 minutes. Once a month passes the allowance, link redirects show a quota page instead of redirecting. They work again when the month resets or when you choose a paid plan. Paid plans include a larger allowance, shown under **Settings → Usage & Billing**. ## Self-hosted Self-hosted deployments do not enforce MAU limits. --- ## Dashboards URL: https://docs.grovs.io/docs/analytics/dashboards Description: Build your own boards of charts on the Home page, or copy a built-in one and change it. **Home** shows one board at a time: one of five built-in boards, or up to 10 of your own per project. A board is a grid of widgets that all follow the board's date range and filters. ## Built-in boards Pick a board from the board menu at the top of **Home**. | Board | Shows | | --------------- | ------------------------------- | | **Overview** | Everything at a glance | | **Acquisition** | Where new users come from | | **Engagement** | Who comes back and what they do | | **Revenue** | What users spend | | **Links** | How your links perform | Built-in boards cannot be edited. To change one, copy it. ## Create a board ### 1. Open the board menu On **Home**, click the board name at the top of the page. ### 2. Add a board Click **New board**, enter a name of up to 100 characters, and click **Save**. The board opens empty. At 10 boards, **New board** is disabled. ### 3. Add widgets Click **Add widget**, or pick a template under **Or start with a template**. See [Widgets](https://docs.grovs.io/docs/analytics/dashboards/widgets). ## Copy a built-in board ### 1. Open the built-in board Choose it from the board menu on **Home**. ### 2. Copy it Click **Copy to customise**. Name the copy under **Board name** and untick unwanted widgets under **Widgets to include**. ### 3. Create it Click **Create board**. The copy appears under **Your boards** and is fully editable. The built-in board is unchanged. ## Edit a board - **Move a widget**: drag it by its handle. - **Edit or delete a widget**: choose **Edit** or **Delete** from the widget's actions menu. - **Rename the board**: choose **Rename** from the board's actions menu. - **Delete the board**: choose **Delete dashboard** from the board's actions menu. This removes all its widgets and cannot be undone. ## Date range and filters The date range, filter bar and **Day** / **Week** / **Month** toggle at the top of **Home** apply to every widget. - A widget's own filters are added on top of the board's. - Timeline widgets use the board's day, week or month buckets. - Ready-made panels follow only the date range and platform filter. ## Who sees a board Boards belong to a project environment, so production and test have separate boards. Everyone on the project's team, admin or member, sees the same boards and can create, edit and delete them. There are no private boards. On plans with the [audit log](https://docs.grovs.io/docs/enterprise/audit-log), board creation, renames, deletions and every widget change are recorded. ## Limits | Limit | Value | | ------------------ | ----------------------------- | | Boards per project | 10, plus the built-in boards | | Widgets per board | 20 | | Board name | 100 characters | | Widget title | 100 characters | | Widget subtitle | 140 characters | | Filters per widget | 25 | | Values per filter | 50, each up to 200 characters | Each widget is also limited in how far back it can read. See [Date range limits](https://docs.grovs.io/docs/analytics/dashboards/widgets#date-range-limits). --- ## Widgets URL: https://docs.grovs.io/docs/analytics/dashboards/widgets Description: What a dashboard widget can chart: events, measures, breakdowns, filters, chart types and date range limits. A widget is one chart on a [board](https://docs.grovs.io/docs/analytics/dashboards). - **Metric** widgets chart one number: an event, how to count it, how to split it and what to filter. - **Library** widgets add a ready-made panel from the built-in boards. ## Add a widget ### 1. Open the widget dialog On one of your boards, click **Add widget**. ### 2. Name it Enter a **Title**, for example `Installs by country`. ### 3. Choose what to show On the **Metric** tab, under **Show**, pick an event and a **Measure**. ### 4. Filter and split it (optional) Add filters under **Filter**. Under **Break down by**, pick a dimension and how many top values to keep. ### 5. Pick the chart Choose a chart type, a size, and whether to compare with the previous period. The preview updates as you go. ### 6. Save Click **Add widget**. To change a widget, choose **Edit** from its actions menu, then click **Save widget**. ## What you can show **Show** combines an event with a measure. **Unique visitors** counts distinct visitors; **Unique devices** counts distinct devices. | Event | **Count** | **Unique visitors** / **Unique devices** | | ------------------------ | -------------------------------------------- | ---------------------------------------- | | **Any event** | All events, purchases included | Yes | | **Install** | App installs | Yes | | **App open** | App opens | Yes | | **Link view** | Link views | Yes | | **Link open** | App opens from a link | Yes | | **Reinstall** | Reinstalls | Yes | | **Referred user** | Users who installed through a referral | Yes | | **Link view (via link)** | Views that came through one of your links | Count only | | **Link-driven install** | Installs that came through one of your links | Count only | | A custom event name | That event | Yes | ## Break down by A breakdown splits the number by one dimension and keeps the top 1 to 50 values (10 by default). The rest are grouped as **Other**. Dimensions: **Country**, **Platform**, **Source**, **Campaign**, **Link**, **App version**, **OS**, **OS version**, **Device**, **City**, **Event name**, **Screen**, **UTM source**, **UTM medium**, **UTM campaign**, **Ads platform**, **Language**, and any event property. ## Filters Each filter has a field, an operator (`is`, `is not` or `contains`) and one or more values. A widget holds up to 25 filters, each with up to 50 values of up to 200 characters. Filterable fields match the events explorer: event type and name, screen, platform, app version, country, city, language, device model, OS and OS version, UTM source, medium and campaign, ads platform, source, link, campaign, user identifier, session, visitor, event properties and user attributes. ## Chart types and sizes | Chart | Best for | | ------------ | ----------------------------------------------------------- | | **Number** | One total, optionally against the previous period | | **Bar list** | Top values of a breakdown | | **Timeline** | Change over time, in the board's day, week or month buckets | | **Table** | A breakdown with exact numbers | Sizes are **Small**, **Medium** and **Full width**. **Compare** adds the previous period: a range of the same length ending the day before the board's start date. It cannot reach past your data retention. Timelines do not offer it. ## Ready-made panels The **Library** tab adds a panel from the built-in boards: **New users trend**, **Acquisition channels**, **Top links**, **Top campaigns**, **Acquisition**, **Engagement**, **Revenue**, **Retention**, **Acquisition KPIs**, **Engagement KPIs**, **Revenue KPIs**, **Link KPIs** and **Revenue trend**. Panels follow the board's date range and platform filter. Revenue panels need [revenue tracking](https://docs.grovs.io/docs/revenue). ## Date range limits How far back a widget can read depends on how Grovs answers it. | How the widget is answered | Longest date range | | --------------------------------------------------------------------------------------------- | -------------------------- | | From daily totals | Your plan's data retention | | From raw events, counting events | 366 days | | From raw events, counting unique visitors or devices, or with a `contains` or `is not` filter | 90 days | Simple widgets read daily totals, for example installs over time, by platform or by country. A widget reads raw events when it: - has a filter other than a single platform - counts **Any event**, a custom event or a link-only event - uses a less common breakdown `contains` and `is not` filters also only reach the recent part of your data retention. A widget over a limit shows an error instead of a chart, such as "Date range cannot exceed 90 days for this query". Narrow the board's date range or remove the filter. On self-hosted deployments, `ANALYTICS_MAX_AGGREGATE_RANGE_DAYS` (default 90) and `ANALYTICS_RAW_COUNTER_MAX_DAYS` (default 366) set the two raw limits. ## What widgets cannot do - Filter by a [segment](https://docs.grovs.io/docs/analytics/segments). - Chart revenue as a metric. Use the revenue panels instead. --- ## Segments URL: https://docs.grovs.io/docs/analytics/segments Description: Group your users by what they did, what they use, how they arrived and what they bought, then count, list, export and target them. A segment is a saved set of conditions, such as "iOS users who bought `pro_yearly` in the last 90 days". Grovs evaluates it on demand, so members change as people act. Use a segment as the audience of a push campaign, an in-app message or a journey. Segments live under **Audience → Segments**. - [Build a segment](https://docs.grovs.io/docs/analytics/segments/build-a-segment): Create a segment, combine groups and conditions, and read the live estimate. - [Condition types](https://docs.grovs.io/docs/analytics/segments/condition-types): Every condition, its fields and its limits. - [Identifier lists](https://docs.grovs.io/docs/analytics/segments/identifier-lists): Upload your own user IDs and match people in or out of the list. - [Members and exports](https://docs.grovs.io/docs/analytics/segments/members-and-exports): Counts, the member list, breakdowns and CSV exports. - [Size history](https://docs.grovs.io/docs/analytics/segments/size-history): How Grovs records a segment's size once a day. ## Requirements - Segments need the Enterprise edition and ClickHouse with reads and writes enabled. Grovs Cloud has both. - Purchase conditions need [revenue tracking](https://docs.grovs.io/docs/revenue). ## People and devices Every count has two numbers: **people** and **devices**. - A **person** is the user identifier your app sets through the SDK, exactly as sent. Matching is case- and whitespace-sensitive, so `UserA` and `usera` are two people. - A visitor with no identifier counts as its own person. - A matching person brings in **all** of their devices. - An identifier shared by more than 50 devices is treated as not set. This stops a shared or guest ID from merging unrelated people. Without a user identifier in the SDK, people and devices are close to the same number. ## Groups and conditions A definition has two levels: - **Conditions** sit inside a **group**. The group's operator, `and` or `or`, decides whether a person must match every condition or at least one. - **Groups** are combined by a top-level `and` or `or`. Groups cannot be nested. A definition holds 1 to 5 groups of 1 to 10 conditions each. ### "Did not" conditions A negative condition matches people who did **not** do something. Negative conditions are: - event or purchase conditions set to "did not" - `is not`, `is not set` and `is not in the uploaded list` operators A negative condition must sit in an `and` group with at least one positive condition, which says who to start from. Example: "opened the app in the last 30 days" and "did not buy in the last 30 days". Exception: a device condition in "current device" mode with `is not` counts as positive. It needs a known current value that differs, so it skips people with no device data. ## Time windows Event, device ("any device used" mode), acquisition and purchase conditions take a **time window**: - **Rolling**: the last N days. - **Fixed**: a date range from one day to another. It cannot start in the future. Windows are in UTC and aligned to whole days. "Last 7 days" starts at 00:00 UTC seven days ago. | Condition | Longest window | | ------------------------- | ------------------------------------ | | Event | 90 days | | Device, "any device used" | 90 days | | Acquisition | 365 days (default when you set none) | | Purchase | 365 days | On self-hosted deployments, `SEGMENT_EVENT_LOOKBACK_DAYS`, `SEGMENT_ACQUISITION_LOOKBACK_DAYS` and `SEGMENT_PURCHASE_LOOKBACK_DAYS` set these caps. ## Data retention Segments only read data your plan still keeps. - Saving a fixed window that starts before your retention cutoff is refused. - A saved fixed window that later ages past the cutoff keeps working. Counts carry an **incomplete** flag, and the dashboard shows "A condition looks further back than your data retention; counts exclude deleted data." - A "did not" window that reaches past retention is refused. Deleted data could otherwise turn a past buyer into a "did not buy" match. ## Freshness Counts are exact, not sampled. - Unfiltered conditions on a built-in event or on any activity read daily totals. These lag up to about 10 minutes for the current month and the two before it, and up to a day for older months. Today is always included. - Every other condition reads live events. - Opening a segment computes a result once and reuses it for about 5 minutes. Counts, members and breakdown on that page read the same result, so they agree. ## Who can use segments Anyone on the project's team, admin or member, can create, edit, archive, count and export segments. Archiving stops evaluation and hides the segment from pickers. Restore it from the **Archived** filter on the segments list. ## Automate it Create, estimate, count and archive segments and upload identifier lists from your server with the [Segments API](https://docs.grovs.io/docs/api-reference/segments). --- ## Build a segment URL: https://docs.grovs.io/docs/analytics/segments/build-a-segment Description: Create a segment in the dashboard, combine conditions and groups, and check its reach before you save. The example below builds a segment of iOS users who bought a yearly plan in the last 90 days, then adds a second group. ## Create the segment ### 1. Open Segments Select your project and environment, go to **Audience → Segments**, and click **New segment**. ### 2. Name it Enter a **Name** of up to 255 characters, for example `iOS yearly buyers`. ### 3. Add the first condition Click **Add condition**, choose **Device**, and set **Platform** is `iOS` on the **current device**. ### 4. Add a second condition to the same group Click **Add condition**, choose **Purchase**, and set **bought** `pro_yearly`, at least 1 time, in the last 90 days. ### 5. Check the reach The **Estimated reach** panel shows matching people and devices. It refreshes as you edit. ### 6. Save Click **Create**. The segment's page opens with its exact count. > **Note:** **Purchase** needs revenue tracking. Without it, the menu marks the condition **Needs revenue collection**. See [Revenue](https://docs.grovs.io/docs/revenue). ## Combine conditions The word between two conditions is the group's operator. Click it to switch between `and` (match every condition) and `or` (match at least one). One operator applies to the whole group. To mix both, add a group with **Add an AND group** or **Add an OR group**. The word between groups is the top-level operator; switch it the same way. Example: "iOS yearly buyers **or** people who raised `checkout` 3 times in the last 30 days" is two groups joined by `or`: | Group | Operator | Conditions | | ----- | -------- | ----------------------------------------------------------------------------------------------- | | 1 | and | Platform is iOS on the current device · bought `pro_yearly` at least 1 time in the last 90 days | | 2 | and | did `checkout` at least 3 times in the last 30 days | A definition holds up to 5 groups of up to 10 conditions each. The add buttons are disabled at a limit. ## Use "did not" Event and purchase conditions start with a switchable word: **did** or **did not**, **bought** or **did not buy**. Device, profile, identifier and acquisition conditions use `is not` and `is not set` instead. A "did not" condition needs a positive condition in the same `and` group: - did **App open** at least 1 time in the last 30 days - **and** **did not buy** in the last 30 days A segment with a "did not" condition alone in a group, or in an `or` group, cannot be saved. ## Read the estimate The estimate counts people and devices for the unsaved draft. - The first estimate can take a few seconds. It is cached for about a minute. - "The project is busy computing other segments. Try again in a moment." means other segments are computing. Retry after a few seconds. - "Segment is too heavy — narrow a window or remove a condition." means the segment is too expensive to compute. ## Edit, archive and restore - **Edit**: click **Edit** on the segment page. A new definition changes who matches from then on and starts a new [size history](https://docs.grovs.io/docs/analytics/segments/size-history) line. - **Archive**: click **Archive** and confirm. Archived segments are not evaluated and disappear from pickers. - **Restore**: choose **Archived** in the segments list's status filter, then click **Restore**. --- ## Condition types URL: https://docs.grovs.io/docs/analytics/segments/condition-types Description: Every segment condition, the fields and operators it accepts, and its limits. Each condition has a `type` plus fields that depend on it. The examples show each condition in a segment definition's JSON form. | Dashboard | `type` | Matches | | -------------------- | ------------- | --------------------------------------------------- | | **Did an event** | `event` | A built-in event, a custom event, or any activity | | **Device** | `device` | Device and app fields | | **Profile property** | `profile` | A property your app set on the user | | **First seen** | `first_seen` | When the person first appeared | | **Identifier** | `identifier` | Your own user ID, or an uploaded list | | **Acquired via** | `acquisition` | The install or reinstall that brought the person in | | **Purchase** | `purchase` | In-app purchases and subscriptions | ## Shared rules - **Values.** 1 to 100 per condition, each a non-empty string of up to 256 bytes. - **Operators on text.** `is` and `is_not` compare exact values. `contains` is a case-insensitive substring match. - **Windows.** See [Time windows](https://docs.grovs.io/docs/analytics/segments#time-windows). A window is `{"last_days": N}` or `{"from": "YYYY-MM-DD", "to": "YYYY-MM-DD"}`. - **Negative conditions.** Those marked "negative" below must sit in an `and` group with a positive condition. ## Did an event Matches people who raised an event at least `count` times in the window. ```json { "type": "event", "event_name": "checkout", "filters": [{ "field": "properties.plan", "op": "is", "value": "pro" }], "count": 3, "window": { "last_days": 30 } } ``` | Field | Required | Notes | | ------------ | ------------ | -------------------------------------------------------------------------------------------------------------------------------------------- | | `event_type` | one of three | A built-in event: `app_open`, `view`, `open`, `install`, `reinstall`, `time_spent`, `reactivation`, `user_referred`, `screen_view`, `custom` | | `event_name` | one of three | A custom event name, up to 256 bytes | | `any` | one of three | `true` matches any activity | | `count` | no | At least this many times. Default `1` | | `window` | yes | Up to 90 days | | `filters` | no | Up to 25. Each has `field`, `op` (`is`, `is_not`, `contains`) and `value` | | `negate` | no | `true` means "did not". Negative | Give exactly one of `event_type`, `event_name` or `any`. Filter fields: `screen_name`, `platform`, `app_version`, `country`, `city`, `device_model`, `os`, `os_version`, `language`, `tracking_source`, `tracking_medium`, `tracking_campaign`, `ads_platform`, or an event property as `properties.`. In the dashboard, click **where** on the condition to add one. Message events, such as `push_open` and `in_app_viewed`, do not count toward **Any activity** or **Any custom event**, because a message shown is not something the person did. Target a message event by its name instead. ## Device Matches on a device or app field. ```json { "type": "device", "field": "platform", "op": "is", "value": ["ios"], "mode": "current" } ``` | Field | Required | Notes | | -------- | ---------- | ------------------------------------------------------------------------------------ | | `field` | yes | `platform`, `os`, `os_version`, `app_version`, `device_model`, `language`, `country` | | `op` | yes | `is`, `is_not`, `contains` | | `value` | yes | 1 to 100 values | | `mode` | no | `current` (default) or `used` | | `window` | for `used` | Up to 90 days | The two modes: - ****current device**** (`current`): the latest value seen in the last 30 days. No window. `is_not` means "has a known current value that is not this one", so it counts as positive. - ****any device used**** (`used`): any event with that value in the window. `is_not` means "not seen with that value in the window", so people with no activity in the window also match. This `is_not` is negative. On self-hosted deployments, `SEGMENT_DEVICE_CURRENT_DAYS` sets the 30-day lookback of `current`. ## Profile property Matches the **current** value of a profile property your app set through the SDK, not its value at the time of an event. ```json { "type": "profile", "field": "properties.plan", "op": "is", "value": ["pro"] } ``` | Field | Required | Notes | | ------- | ------------------------------ | -------------------------------------------------- | | `field` | yes | `properties.` | | `op` | yes | `is`, `is_not`, `contains`, `is_set`, `is_not_set` | | `value` | not for `is_set`, `is_not_set` | 1 to 100 values | `is_not` and `is_not_set` are negative. A person with no profile also matches `is_not`. A dotted key such as `user.plan` and the nested path `user` › `plan` are the same property. ## First seen Matches the date of the person's earliest built-in event. ```json { "type": "first_seen", "op": "gte", "value": "2026-01-01" } ``` | Field | Required | Notes | | ------- | -------- | --------------------------------------------------- | | `op` | yes | `gte` (**on or after**) or `lte` (**on or before**) | | `value` | yes | An ISO date, `YYYY-MM-DD` | Only built-in events set first seen. A person whose only activity is custom events never matches. ## Identifier Matches the user identifier your app set through the SDK. ```json { "type": "identifier", "op": "is", "value": ["member-4471"] } ``` | `op` | Value | Meaning | | ------------- | --------------- | ---------------------------------------------------- | | `is_set` | none | The person has an identifier | | `is_not_set` | none | The person has no identifier. Negative | | `is` | 1 to 100 values | Exact match | | `contains` | 1 to 100 values | Case-insensitive substring | | `in_list` | none | The identifier is in the segment's uploaded list | | `not_in_list` | none | The identifier is not in the uploaded list. Negative | `in_list` and `not_in_list` only work on a saved segment that has an uploaded list. See [Identifier lists](https://docs.grovs.io/docs/analytics/segments/identifier-lists). ## Acquired via Matches people who had an install or reinstall from a link, campaign, source or medium in the window. ```json { "type": "acquisition", "field": "campaign_id", "op": "is", "value": [41], "window": { "last_days": 365 } } ``` | Field | Required | Notes | | -------- | -------- | ------------------------------------------------------------------------------------------------ | | `field` | yes | `link_id`, `campaign_id`, `tracking_source`, `tracking_medium` | | `op` | yes | `is` or `is_not` for `link_id` and `campaign_id`. `is`, `is_not` or `contains` for the other two | | `value` | yes | Positive integer IDs for `link_id` and `campaign_id`, text for the others. 1 to 100 values | | `window` | no | Up to 365 days. Without one, Grovs uses 365 days, or your retention if it is shorter | `is_not` is negative. ## Purchase Matches people with at least `count` purchases in the window, optionally above a spending total. ```json { "type": "purchase", "product_id": ["pro_yearly"], "purchase_type": ["subscription"], "count": 1, "min_total_usd_cents": 999, "window": { "last_days": 90 } } ``` | Field | Required | Notes | | --------------------- | -------- | --------------------------------------------------------------------------------- | | `product_id` | no | Product IDs. Omit for any product | | `purchase_type` | no | `subscription`, `one_time` | | `store_source` | no | The store. In the dashboard: **on the App Store**, **on Google Play** | | `count` | no | At least this many purchases. Default `1` | | `min_total_usd_cents` | no | Minimum total spend in US cents. Default `0`. In the dashboard: **totalling ≥ $** | | `window` | yes | Up to 365 days, on the purchase date | | `negate` | no | `true` means "did not buy". Negative | How purchases count: - One transaction counts once, from its `buy`. Renewals and repeat purchases are separate transactions. - A refunded transaction does not count, unless the refund was later reversed. - `quantity` raises the spend total. It does not multiply the count. - Totals use the purchase's US dollar value. Saving a segment with a purchase condition fails while [revenue tracking](https://docs.grovs.io/docs/revenue) is off. ## Limits | Limit | Value | | -------------------------------- | --------- | | Groups per definition | 5 | | Conditions per group | 10 | | Filters per event condition | 25 | | Values per condition or filter | 100 | | Value length | 256 bytes | | Identifiers per uploaded list | 200,000 | | Event and device windows | 90 days | | Acquisition and purchase windows | 365 days | Self-hosted deployments can change the window caps with environment variables. The other limits are fixed. --- ## Identifier lists URL: https://docs.grovs.io/docs/analytics/segments/identifier-lists Description: Upload your own user IDs to a segment and match people who are, or are not, in the list. An identifier list is a set of your own user IDs attached to one segment. Use it for people you know from your CRM or database: beta testers, a support cohort, customers on an old contract. The IDs must be the user identifiers your app sets through the SDK. Grovs stores them exactly as sent, and matching is case- and whitespace-sensitive. A list changes members only once a condition uses it: - **Identifier** › **is in the uploaded list** (`in_list`) keeps only people in the list. - **Identifier** › **is not in the uploaded list** (`not_in_list`) leaves them out. This is negative, so it needs a positive condition in the same `and` group. ## Upload a list ### 1. Save the segment first A list belongs to a saved segment. Create the segment, then open it from **Audience → Segments**. ### 2. Open the Identifier list tab Click **Identifier list**. ### 3. Add your identifiers Drop a CSV file on the panel or browse for one. To type them, click **Paste instead** and enter one identifier per line. ### 4. Publish Click **Publish list**. Grovs checks and stores the list, then shows how many identifiers it holds. ### 5. Use the list in a condition Click **Edit**, add an **Identifier** condition, choose **is in the uploaded list** or **is not in the uploaded list**, and save. ## File format - Grovs reads the **first column** of each CSV row. - A header row is optional. A row whose first cell is `identifier`, in any case, is skipped. - Empty cells are skipped and duplicates removed. - Invalid CSV is refused. ```csv identifier member-4471 member-9021 member-10388 ``` ## Limits | Limit | Value | | -------------------- | -------------- | | Identifiers per list | 1 to 200,000 | | Identifier length | 1 to 256 bytes | | Lists per segment | 1 | ## Replace a list Uploading again replaces the **whole** list. There is no append. While an upload runs, a second upload to the same segment is refused with "Another upload for this segment is still running. Try again in a minute." If an upload fails, the previous list stays. ## Search and download The **Identifier list** tab has a search box that shows up to 100 matches. For the full list, click **Download CSV**. The file has one `identifier` column and can be re-uploaded unchanged. ## Edits and exports - A [member export](https://docs.grovs.io/docs/analytics/segments/members-and-exports) uses the list as it was when the export started. - Estimating a draft edit of a saved segment uses that segment's list. - A never-saved draft cannot use `in_list` or `not_in_list`, because it has no list yet. --- ## Members and exports URL: https://docs.grovs.io/docs/analytics/segments/members-and-exports Description: See how many people a segment holds, browse and search its members, split it by a dimension, and export it as CSV. Open a segment from **Audience → Segments**. Its page has three tabs: **Overview**, **Members** and **Identifier list**. ## Count **Overview** shows an exact count of **people** and **devices**. See [People and devices](https://docs.grovs.io/docs/analytics/segments#people-and-devices). Grovs computes the segment when you open it and reuses the result for about 5 minutes. Count, member list and breakdown read the same result, so they agree. The first count after a definition change can take a few seconds. If part of a window falls outside your data retention, the count is marked incomplete and may be low. ## Members **Members** lists one row per device: **Identifier**, **Device** ID, **Kind** (`App` or `Web`) and **First seen**. A device without an identifier shows as `anonymous`. - Rows load in pages as you scroll. - **Search members** matches an identifier anywhere in the text, case-insensitive, or a device ID from the start. Searches are up to 100 characters. - The totals above the list stay unfiltered while you search. After a long time open, the list shows "This result has expired." Reload the segment. ## Breakdown The **Breakdown** panel on **Overview** splits members by one dimension. Each person lands in exactly one bucket, so buckets add up to the total. | Dimension | What each person gets | | ------------------------ | ----------------------------------------------------- | | **Platform** | The platform of their current device | | **Country** | The country of their current device | | **App version** | The app version they last used | | **Install source (UTM)** | The `utm_source` on the link that drove their install | | **Install campaign** | The campaign of the link that drove their install | | **First seen (week)** | The week they were first seen | | **Profile property** | The value of one profile property | It shows the top 50 values. The rest are grouped as **Other**, and people with no value as **Unknown**. ## Export to CSV ### 1. Start the export Click **Export CSV** on the segment page, or in a segment's actions menu in the list. ### 2. Wait for the email The export runs in the background. Grovs emails you when it is ready. The email has no file link. ### 3. Download In the **Exports** panel on the segment's **Overview** tab, click **Download**. The file has one row per device: | Column | Contents | | ---------------- | --------------------------------------------------------------------- | | `person_key` | `i:` for an identified person, `v:` otherwise | | `visitor_uuid` | The device's visitor ID | | `sdk_identifier` | The user identifier your app set, if any | | `sdk_attributes` | The user attributes your app set, as JSON | | `created_at` | When the visitor was created | - The export captures the definition and identifier list at start. Later edits do not change the file. - Each download link is signed and works for 5 minutes. Clicking **Download** again creates a new link. - Files are deleted 24 hours after the export finishes, downloaded or not. The panel then shows **Expired**. - If Grovs is busy, the export retries up to 3 times over the next half hour before showing **Failed**. - Values starting with `=`, `+`, `-` or `@` get a leading `'` so spreadsheets do not run them as formulas. > **Warning:** On self-hosted deployments, the download link points at your file storage. With a private S3 or MinIO endpoint, check that your team's browsers can reach it. --- ## Size history URL: https://docs.grovs.io/docs/analytics/segments/size-history Description: How Grovs records each segment's size once a day, and what resets the history. ## How it works - Every day from about 04:00 UTC, Grovs estimates the people and devices in each active segment. The run is spread out over time. - Each estimate is one point for that date, tagged with the definition it used. - Archived segments are not measured. - If a segment is too heavy to estimate, or the estimate fails, that day has no point. - A point is marked incomplete when part of a window was outside your data retention that day. ## Where you see it - **Segment page**: **Overview** charts people and devices over time. A new segment shows "Not enough history yet." - **Segments list**: **People** and **Devices** show the latest point if it is at most 7 days old and matches the current definition. Otherwise the list shows "Size not computed yet". Open the segment for an exact count. ## Editing resets the line Changing a segment's definition starts a new history line. Points for the old definition stay stored but are no longer shown. Renaming a segment or changing its description does not reset it. --- ## Engagement URL: https://docs.grovs.io/docs/engagement Description: Reach your users after the install: push campaigns, in-app messages, journeys and the experiments that measure them. Engagement tools message people who already have your app. Pick an audience from a [segment](https://docs.grovs.io/docs/analytics/segments) or a list of users, and every message reports back what happened. In the dashboard they live under **Engage**. - [Push campaigns](https://docs.grovs.io/docs/engagement/push-campaigns): Send a notification to a segment, a user list or single devices, now or scheduled. See who got it and who tapped it. - [In-app messages](https://docs.grovs.io/docs/engagement/in-app-messages): Show covers, pop-ups, header and footer banners, alerts or your own HTML in your app or site, the moment people do something. - [Journeys](https://docs.grovs.io/docs/engagement/journeys): Move people through waits, splits, pushes and in-app messages. They enter on an event or a segment and leave at a goal. - [Experiments](https://docs.grovs.io/docs/engagement/experiments): Compare variants, hold back a control group, and see whether a message changed what people do and what it earned. ## Before you start - **Install the SDK.** Messages reach devices through the Grovs SDK. Push campaigns need iOS SDK 3.1.0 or Android SDK 3.1.0 or later to report taps. See push notifications on [iOS](https://docs.grovs.io/docs/sdk/ios/push-notifications) and [Android](https://docs.grovs.io/docs/sdk/android/push-notifications). - **Identify your users.** Set a user identifier in the SDK to target people by your own user ID and recognise them across devices. - **Track events.** Segments, triggers and experiment goals are built from the events and screens your app reports. ## Settings shared by every message **Engage › Engagement Settings** holds the project-wide rules: | Setting | What it does | | ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | Control group | Holds back 0 to 20% of people from every in-app message, journey and dashboard push campaign. Server pushes still reach them. Experiment reports show them as their own group. | | Pushes per person per day | Caps pushes per person per UTC day, across every campaign and journey. Off by default. | | In-app messages | On/off switch and limits for in-app messages. | Anyone on the project's team can create, send and change messages and these settings. --- ## Push campaigns URL: https://docs.grovs.io/docs/engagement/push-campaigns Description: Send push notifications to a segment, a list of your users or single devices, from the dashboard or your server, and see what happened to each one. A push campaign is one notification sent to an audience you choose. Write it under **Engage › Push** or send it from your server. Grovs delivers it through Apple Push Notification service (APNs) and Firebase Cloud Messaging (FCM). Each campaign has a report: devices targeted, how many Apple and Google accepted, which failed and why, and who tapped. ## How it works 1. **Pick an audience.** A [segment](https://docs.grovs.io/docs/analytics/segments), a list of your user IDs, or a list of devices. 2. **Grovs freezes the audience** at send time, or at release for a scheduled campaign. It finds every device of every person and skips devices that can't get a push. 3. **Grovs delivers from its own servers.** iOS goes to APNs and Android to FCM, with the credentials you uploaded. Large campaigns go out at a steady pace, not all at once. 4. **The SDK reports taps.** On a tap, the SDK records the open and passes the campaign's deep link and data to your app, like any Grovs link. A tap counts once per device per campaign. It is also recorded as a `push_open` event in your analytics. ## Platforms | Platform | Status | | --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | iOS | Delivery, tap tracking, deep links and images. Tap tracking and images need iOS SDK 3.1.0 or later. | | Android | Delivery, tap tracking, deep links and images. Tap tracking needs Android SDK 3.1.0 or later. Background pushes are shown by Firebase, foreground ones by the SDK. | | Flutter | Tap tracking, deep links and images with plugin 3.2.0. See [Flutter push notifications](https://docs.grovs.io/docs/sdk/flutter/push-notifications). | | React Native | Tap tracking, deep links and iOS images with wrapper 3.1.0. See [React Native push notifications](https://docs.grovs.io/docs/sdk/react-native/push-notifications). | | Unity | Tap tracking and deep links with package 1.0.0. See [Unity push notifications](https://docs.grovs.io/docs/sdk/unity/push-notifications). | | Web and desktop | Not reached. Counted as **No channel**. | ## Prerequisites ### 1. Upload your push credentials In the dashboard, go to **Developers › SDK Setup**, **Push Notifications** step. - **iOS**: upload your APNs `.p8` key with **Upload certificate** and enter the **P8 APN Key ID**. Grovs uses the Apple App Prefix (your Team ID) and bundle ID from the **Register App** step. - **Android**: upload the **Firebase Service Account JSON Key** and enter the **Firebase Project ID**. A platform without credentials is skipped. Its devices fail with **Push not configured for this platform**. ### 2. Pass the push token from your app Register for notifications and give the token to the Grovs SDK. See [Push notifications on iOS](https://docs.grovs.io/docs/sdk/ios/push-notifications) or [Push notifications on Android](https://docs.grovs.io/docs/sdk/android/push-notifications). A device without a token counts as **No channel**. ### 3. Identify your users (optional) Set the user identifier in the SDK to target people by your own user ID. A push to a user ID reaches every device that user is signed in on. > **Warning:** The Grovs **test** environment sends through the APNs sandbox, **production** through production APNs. Xcode builds have sandbox tokens, so they only get pushes from the test environment. TestFlight and App Store builds get production pushes. ## Self-hosted Push campaigns ship in the Enterprise image only. The Community image has no **Push** section. The Enterprise stack runs an `rpush` process, next to the web app and workers, that holds the APNs and FCM connections. If it isn't running, campaigns stay in **Sending**, devices stay **Queued** and nothing is delivered. See [Configuration](https://docs.grovs.io/docs/self-hosting/configure/configuration) for the Enterprise image. ## Next steps - [Create a campaign](https://docs.grovs.io/docs/engagement/push-campaigns/create-a-campaign): Write the message, pick the audience and send now, later or in each user's local time. - [Delivery and reporting](https://docs.grovs.io/docs/engagement/push-campaigns/delivery-and-reporting): What each status and number in the report means. - [Send from your server](https://docs.grovs.io/docs/engagement/push-campaigns/send-from-your-server): Trigger transactional pushes with your project key. - [Limits and troubleshooting](https://docs.grovs.io/docs/engagement/push-campaigns/limits-and-troubleshooting): Sizes, rate limits, token handling and common failures. To send from your server, see the [Server push API reference](https://docs.grovs.io/docs/api-reference/server-push). --- ## Create a campaign URL: https://docs.grovs.io/docs/engagement/push-campaigns/create-a-campaign Description: Write a push, choose who gets it, and send it now, at a set time or at the same hour in each user's own timezone. Go to **Engage › Push** and click **New campaign**. The composer has four steps: **Message**, **Audience**, **Experiment** and **Delivery**. **Save draft** keeps your work; a draft stays editable until you send it. The example is Fitloop, a fictional fitness app, bringing back people who haven't logged a workout this week. ## 1. Message What people see on the lock screen, and where a tap takes them. The preview shows the notification on iOS and Android as you type. | Field | What to enter | Limits | | --------------- | --------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------ | | **Title** | The bold first line. | 255 characters. | | **Body** | The text under the title. | 1,000 characters. | | **Image** | Upload an image, or paste an `https://` URL. | PNG, JPEG or GIF. Uploads up to 1 MB. Around 1038 × 1038 px works well; Grovs doesn't resize it. | | **Deep link** | A Grovs link from this project. Search by name, title or path. | Optional. **No link** opens the app. | | **Custom data** | Key–value pairs delivered to your app with the push. | 2 KB in total. | | **Sound** | **Default sound**, **Silent**, or **Custom…** with a sound file bundled in your app, such as `chime.caf`. | 64 characters. On Android, the sound needs a notification channel that uses it. | The whole notification must fit Apple's 4 KB payload limit. A long body plus a long image URL or lots of custom data can exceed it. Grovs then refuses to save and shows the size. **On tap**, your app receives the link's data merged with your custom data, through the same callback as any Grovs deep link. Grovs adds `grovs_campaign_id`, plus `grovs_link` and `grovs_image` when set. These names are reserved. Keys that would break the APNs or FCM payload are refused, such as `aps`, `notification` and keys starting with `google` or `gcm`. See [What your app receives](https://docs.grovs.io/docs/sdk/ios/push-notifications#what-your-app-receives). > **Note:** iOS images need a Notification Service Extension in your app, or the push arrives without its image. See [Campaign images](https://docs.grovs.io/docs/sdk/ios/push-notifications#4-campaign-images-optional). Android shows images natively. ## 2. Audience Pick one of three tabs: | Tab | Sends to | Notes | | --------------- | ------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------- | | **Segment** | Every device of every person in a [segment](https://docs.grovs.io/docs/analytics/segments). | Evaluated at send time. Later segment edits don't change who got this campaign. | | **My user ids** | Every device of each user ID your app set with the SDK, up to 50 devices per user. | Up to 10,000 IDs. | | **Devices** | Single devices, by device ID. | Up to 10,000 IDs. | For the two lists, type or paste IDs one per line or comma-separated, upload a CSV, or use **Add from visitors** to search and add visitors. Duplicates are dropped. To send to one person, add only their user ID or device ID. **Estimated reach** updates after each saved change. Per platform, it shows the audience's devices, those with no way to get a push (**No channel**) and those on a platform you haven't set up. For a segment, the platform split is estimated from devices active in the last 30 days, and may be skipped for a very large segment. > **Note:** The audience is frozen when the campaign goes out. People who sign in or join the segment afterwards aren't added. ### Testing your campaign There is no test button. To check a campaign on your phone, send it with the **Devices** tab and your device ID, or **My user ids** and your user ID. Then click **Duplicate** on the report to make the real campaign. Xcode builds only get pushes from the Grovs test environment. ## 3. Experiment (optional) Measure what the push does. You can: - set a **goal**, such as Fitloop's `workout_logged` event within 1 day - hold back a **control group** that doesn't get the push - add up to two **variants** of the title and body in the Message step, send them to a **test share** first, then the winner to everyone else See [Experiments](https://docs.grovs.io/docs/engagement/experiments). ## 4. Delivery Choose **Send now** or **Schedule**. - **Send now** starts a few seconds after you confirm. - **Schedule** sends at a date and time up to 30 days ahead, read in your browser's timezone. You can cancel until it starts sending. ### Each user's local time Under **Schedule**, turn on **Deliver in each user's local time** to send at the same wall-clock time everywhere: 09:00 in Berlin, then 09:00 in New York. Grovs uses the timezone each device last reported. Devices without one use the timezone you pick below the switch. The first devices to reach 09:00 are in UTC+14, so delivery starts that early and spans about 26 hours. Schedule at least 14 hours minus your zone's UTC offset ahead: | Your timezone | Minimum lead time | | ------------------------------------ | ----------------- | | Europe/Berlin, winter (UTC+1) | 13 hours | | America/Los_Angeles, summer (UTC−7) | 21 hours | | Asia/Tokyo (UTC+9) | 5 hours | The composer shows the minimum when the time is too close. A device whose slot has already passed gets the push right away. Local-time delivery can't be combined with an experiment test share. ### Confirm Click **Send now** or **Schedule…**. The confirmation shows the title, audience, reach, experiment and time. Only drafts can be edited. A scheduled or sent campaign stays as it is and is never sent twice. To resend, with or without changes, use **Duplicate** to make a new draft. ## Cancel a campaign Cancel a draft, scheduled or sending campaign from its menu or the **Cancel** button on its report. - **Scheduled**: nothing is sent. - **Sending**: devices not yet handed to Apple or Google are dropped and counted as **Cancelled**. Batches already in flight still go out, at most about 1,000 devices per platform. - **Waiting for the winner**: people still waiting don't get the push. Cancel is unavailable for the few seconds a campaign is being evaluated. --- ## Delivery and reporting URL: https://docs.grovs.io/docs/engagement/push-campaigns/delivery-and-reporting Description: Campaign statuses, what targeted, delivered, opened and failed mean, and how Grovs keeps the numbers right. Open a campaign from **Engage › Push** to see its report. Numbers update live while it sends, and the counts are kept as long as the campaign exists. ## Campaign status | Status | Meaning | | ---------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- | | Draft | Being written. Only drafts can be edited. | | Scheduled | Waiting for its send time. Can be cancelled. | | Evaluating | Grovs is resolving the audience: seconds for a list, longer for a large segment. | | Sending | Pushes are being handed to Apple and Google. Can be cancelled. | | Sent | Every device handed off. The last pushes reach Apple and Google seconds later, and pushes they asked to retry keep going out after that. | | Waiting for the winner | Sent to the test share of an [experiment](https://docs.grovs.io/docs/engagement/experiments). The rest get the winning variant later. | | Failed | The audience couldn't be resolved. The report shows why. | | Cancelled | Stopped before every device got it. | Why a campaign fails: | Reason | What happened | What to do | | ----------------- | ------------------------------------------------------------------------------------- | ------------------------------- | | Segment busy | The segment couldn't be evaluated in ten attempts over about three hours. | Retry later with **Duplicate**. | | Segment invalid | The segment was archived, its definition no longer works, or its time window expired. | Fix the segment. | | Segment too heavy | The segment query was too expensive to run. | Narrow the segment. | | Evaluation error | Something else went wrong. | Contact support. | ## Reach **Reach** tracks every device in the audience: | Number | Meaning | | ---------------------- | ------------------------------------------------------------------------------------------------ | | Targeted | Devices selected for this push. Excludes the control group. | | No channel | No push token on file, or a web or desktop device. Skipped. | | Not found | A user ID or device ID in your list that Grovs doesn't know. | | Queued | Handed to the push service, no result yet. | | Delivered | Accepted by APNs or FCM. Not proof it was shown: the phone may be off or notifications disabled. | | Failed | Rejected by APNs or FCM, or not sent. See [Failures](#failures). | | Opened | Tapped, as reported by the SDK. | | Cancelled | Not sent because the campaign was cancelled first. | | Control group | Held back on purpose, for comparison. | | Waiting for the winner | Outside the test share. Sent once a variant is chosen. | The report also shows audience size in people and devices. A segment whose time window reaches past your data retention is marked incomplete. ### How opens are counted - An open is a tap on the notification reported by the SDK. Dismissing it or tapping a custom action button doesn't count. See [Push notifications on iOS](https://docs.grovs.io/docs/sdk/ios/push-notifications). - Each device counts once per campaign, however many taps or SDK retries. - The open is recorded as a `push_open` event with the campaign ID, visible in your analytics. - A tap on a push with a deep link isn't also counted as a link open, and later events from that device aren't attributed to the link. The push gets the credit, once. - Android devices don't report opens yet. ## Failures **Failures by reason** groups failed devices by Apple's or Google's answer. APNs reasons keep their name; FCM failures show the HTTP status. | Reason | Meaning | | ----------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `Unregistered` | App uninstalled or token expired. Grovs removes the token. | | `BadDeviceToken` | A malformed token, or one from the other APNs environment. See [Tokens](https://docs.grovs.io/docs/engagement/push-campaigns/limits-and-troubleshooting#how-grovs-handles-push-tokens). | | `PayloadTooLarge` | The notification exceeded the size limit. | | FCM `404` | Device not registered. Grovs removes the token. | | `no_app` | No push credentials for this platform. Add them in **Developers › SDK Setup**. | | `no_token` | The device lost its token between the audience freeze and delivery. | | `capped` | The person already hit the daily maximum. See [Daily cap](https://docs.grovs.io/docs/engagement/push-campaigns/limits-and-troubleshooting#daily-cap). | | `app_removed` | Your push credentials were replaced or removed while the push waited. | More reasons: [Limits and troubleshooting](https://docs.grovs.io/docs/engagement/push-campaigns/limits-and-troubleshooting#common-failures). ## Deliveries The **Deliveries** table lists every device with its status, platform, visitor, any error, and open time. Filter by status, for example to find every failed device. Per-device rows are kept for 30 days after the send. Report counts are kept for good. ## How the numbers stay accurate - **At least once.** Grovs marks a push queued and hands it off in one step, so a crash never sends a batch twice. If the push service stops after Apple or Google accepted a push but before saving that, the push is resent on restart. A rare duplicate is possible; a lost push is not. - **Retries.** APNs `429`, `500` and `503` are retried every 10 seconds until accepted. FCM `5xx` is retried with backoff, and FCM `429` up to 8 retries. - **Reconciliation.** Every minute, Grovs settles devices still **Queued** after 10 minutes from the push service's own record. If a result was lost, the report catches up on its own. --- ## Send from your server URL: https://docs.grovs.io/docs/engagement/push-campaigns/send-from-your-server Description: Trigger transactional pushes, such as order updates, from your backend with your project key. Send pushes from your backend when something happens in your system: an order shipped, a payment failed, a class is about to start. Each call creates a push campaign with the same delivery, report and tap tracking as a dashboard campaign. It shows in **Engage › Push** with the source `api`. Keep the project key on your server. Never put it in an app or a web page. ## Authenticate Send two headers with every request: | Header | Value | | ------------- | ----------------------------------------------------------------- | | `PROJECT-KEY` | Your project key, from **Developers › API Key** in the dashboard. | | `ENVIRONMENT` | `production` or `test`. The environment the push is sent from. | These are the same headers as [link generation](https://docs.grovs.io/docs/api-reference/links#generate-a-link). See [Authentication](https://docs.grovs.io/docs/api-reference/authentication). ## Send a push Medly, a fictional pharmacy app, tells a user their prescription is ready: ```bash curl -X POST https://sdk.sqd.link/api/v1/sdk/server/push \ -H "PROJECT-KEY: $GROVS_PROJECT_KEY" \ -H "ENVIRONMENT: production" \ -H "Content-Type: application/json" \ -d '{ "title": "Your prescription is ready", "body": "Pick it up at Medly Central today", "link_id": 4821, "data": { "order_id": "ord_8812" }, "audience": { "type": "identifiers", "recipients": ["user_1042"] } }' ``` On self-hosted Grovs, use your own SDK host, such as `https://sdk.grovs.example.com`. The response is `202 Accepted` with the campaign in status `evaluating`. Grovs resolves the audience and hands the pushes to Apple and Google within a couple of seconds. ### What you can send The body takes the same message and schedule fields as a dashboard campaign: | Field | Notes | | ---------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `title`, `body` | Required. Up to 255 and 1,000 characters. | | `name` | Optional label in the dashboard. Defaults to the title. | | `image_url` | Optional. An `https://` URL of a PNG, JPEG or GIF. Uploads aren't available here. | | `sound` | Optional. `default` or a sound file bundled in your app. | | `link_id` | Optional. The numeric ID of a Grovs link in this project. | | `data` | Optional. An object of custom keys, up to 2 KB. | | `audience` | Required. `identifiers` (your user IDs), `visitors` (visitor IDs, the UUIDs Grovs assigns) or `segment` with a `segment_id`. Lists hold up to 10,000 entries. | | `scheduled_at`, `local_time`, `timezone` | Optional. Schedule the push instead of sending now, as in [Delivery](https://docs.grovs.io/docs/engagement/push-campaigns/create-a-campaign#4-delivery). | Experiments (variants, control groups and goals) are dashboard-only. For the full request and response, see the [Server push API reference](https://docs.grovs.io/docs/api-reference/server-push). ## Check the result Save the campaign `id` from the response and read the live report: ```bash curl https://sdk.sqd.link/api/v1/sdk/server/push/k9Qx2 \ -H "PROJECT-KEY: $GROVS_PROJECT_KEY" \ -H "ENVIRONMENT: production" ``` The `report` object holds the same numbers as the dashboard report. See [Delivery and reporting](https://docs.grovs.io/docs/engagement/push-campaigns/delivery-and-reporting). ## How server pushes differ - **Not held back.** Server pushes are transactional. The project control group in **Engagement Settings** doesn't apply to them. - **Still capped.** The daily push cap, if set, counts them like any other push. - **Rate limited.** Per project per minute: up to 600 campaigns, 10 of them with a segment audience, and 100,000 listed recipients. Over a limit, the request fails with `429` and `error_code: "rate_limited"`. A refused request creates nothing and uses no budget, so retry after a short wait. ## Errors Every error has an `error` message. Plan and rate-limit errors also carry an `error_code`. | Status | Meaning | | ------ | ---------------------------------------------------------------------------------------------------------------------------------------------- | | `400` | Missing `PROJECT-KEY`, or `ENVIRONMENT` isn't `production` or `test`. | | `403` | `Invalid credentials`: the project key is wrong. Or, with `error_code: "enterprise_required"`, your Cloud plan doesn't include the server API. | | `404` | Push campaigns aren't available on this installation, or no campaign has that ID. | | `422` | A field is invalid. The message names it, for example `Link not found`, `Segment not found` or `data must be an object`. | | `429` | `rate_limited`: over one of the per-minute limits above. | --- ## Limits and troubleshooting URL: https://docs.grovs.io/docs/engagement/push-campaigns/limits-and-troubleshooting Description: Size and audience limits, delivery pace, how push tokens are kept and removed, and what to do when a push doesn't arrive. ## Limits | What | Limit | | ------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Title | 255 characters | | Body | 1,000 characters | | Whole notification | 4,096 bytes (Apple's limit), including image URL and custom data. Checked for every variant. | | Custom data | 2 KB | | Image | PNG, JPEG or GIF. Uploads up to 1 MB; URLs must be `https://`. | | Sound name | 64 characters | | User ID or device ID list | 10,000 entries, each up to 256 bytes | | Devices per user ID | 50 | | Schedule | Up to 30 days ahead | | Per-device delivery rows | Kept 30 days after the send. Report counts are kept for good. | | Server API | Per project per minute: 600 campaigns, 10 segment campaigns, 100,000 recipients. See [Send from your server](https://docs.grovs.io/docs/engagement/push-campaigns/send-from-your-server). | ### Daily cap **Pushes per person per day** in **Engage › Engagement Settings** caps pushes per person per UTC day, from 1 to 50. It covers every campaign, journey and server push in the project, and is off by default. A push over the cap isn't sent and fails with reason `capped`. The count is per device, so someone with two phones has a separate count on each. ## Delivery pace Grovs hands pushes to Apple and Google only a few seconds ahead of sending. That keeps **Cancel** accurate and the report near real time. A campaign to a million devices takes around 10 minutes. Campaigns sending at the same time, from any project, take turns of a few seconds each. A large campaign slows a small one but never blocks it. ## How Grovs handles push tokens The SDK sends your app's push token to Grovs with the device's details. Grovs keeps one token per device. - **Updates.** Pass the token every time your app gets one. A new token replaces the old one. - **Missing token.** A request that omits the token or sends `null` keeps the stored token. - **Dead tokens.** On Apple `Unregistered` or `BadDeviceToken`, or FCM `404` for an unregistered device, Grovs removes the token. The next campaign counts the device as **No channel** instead of failing again. Only the failed token is removed; a newer token registered meanwhile is kept. - **Test environment.** A `BadDeviceToken` from the APNs sandbox usually means a production token reached the Grovs test environment, so the test environment keeps the token. In production, `BadDeviceToken` removes it. ## Common failures | Reason in the report | Likely cause | Fix | | ---------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------- | | Push not configured for this platform (`no_app`) | No APNs key or Firebase credentials for that platform. | Add them in **Developers › SDK Setup**, iOS or Android, **Push Notifications** step. | | Device has no push token (`no_token`) | The token was removed between the audience freeze and delivery. | None; the device is skipped. | | `Unregistered` / FCM `404` | App uninstalled or token expired. | None; Grovs removes the token. | | `BadDeviceToken` | A token from the other APNs environment, often an Xcode build getting a production push. | Send to Xcode builds from the Grovs test environment, and to TestFlight and App Store builds from production. | | `InvalidProviderToken`, `ExpiredProviderToken`, `MissingProviderToken` | Apple can't verify your APNs key: wrong Key ID or Team ID, or a revoked key. | Upload a valid `.p8` key with the right Key ID, and check the Apple App Prefix (Team ID). | | `BadTopic`, `DeviceTokenNotForTopic`, `TopicDisallowed` | The token belongs to a different app or bundle ID than the one configured. | Check the bundle ID in **Developers › SDK Setup › iOS**. | | `PayloadTooLarge` | Over Apple's size limit. | Shorten the body, custom data or image URL. | | FCM `401` / `403` | Firebase rejected your credentials, or the sender isn't allowed for this token. | Upload a service account key from the same Firebase project as your app, and check the Firebase Project ID. | | `capped` | The person reached the daily cap. | Raise or turn off the cap in **Engagement Settings**. | | `app_removed` | Push credentials were changed or removed while the push waited. | Resend with **Duplicate** if needed. | ## Troubleshooting ### Delivered, but nothing shows on the phone **Delivered** means Apple or Google accepted the push and will deliver it when the phone is online. It still won't show if: - the person turned off notifications for your app - a Focus mode hides it - the app never asked for notification permission. On Android 13 and later, request `POST_NOTIFICATIONS`. ### The image is missing - On iOS, your app needs a Notification Service Extension. See [Campaign images](https://docs.grovs.io/docs/sdk/ios/push-notifications#4-campaign-images-optional). - Use PNG, JPEG or GIF. WebP and other formats are skipped and the push arrives without an image. - Image URLs must be `https://`. ### Opened stays at zero - Tap tracking needs iOS SDK 3.1.0 or later, and your app must pass taps to `Grovs.handleNotificationResponse`. See [Push notifications on iOS](https://docs.grovs.io/docs/sdk/ios/push-notifications). - On Android, tap tracking needs Android SDK 3.1.0 or later, with taps reaching `Grovs.onStart`/`onNewIntent` or `Grovs.handleNotificationOpen`. See [Push notifications on Android](https://docs.grovs.io/docs/sdk/android/push-notifications). - Only a tap on the notification counts, not a dismissal or a custom action button. ### Many devices count as No channel These devices have no push token, or are web or desktop. Check that your app registers for notifications and passes the token to the SDK on every launch, and that people allowed notifications. ### A campaign stays in Evaluating Large segments take longer. If too many segment queries are running, Grovs retries for about three hours, then fails the campaign with **Segment busy**. ### A campaign stays in Sending (self-hosted) The `rpush` process sends pushes to Apple and Google. If it isn't running, nothing leaves the queue. Check that the Enterprise stack's `rpush` service is up. --- ## In-app messages URL: https://docs.grovs.io/docs/engagement/in-app-messages Description: Show a banner, card, full-screen message or alert inside your app or website the moment something happens. In-app messages show inside your app or website while people use it: on app open, on a screen view, or when a tracked event happens. Use them for announcements, offers, update prompts, push permission primers, ratings and short surveys. Create them under **Engage** › **In-App**. The SDK downloads eligible messages ahead of time and decides on the device when to show one, so a message appears the instant its trigger fires, with no network call. - [Create a message](https://docs.grovs.io/docs/engagement/in-app-messages/create-a-message): Content, trigger, audience, limits, schedule and test sends. - [Surveys and ratings](https://docs.grovs.io/docs/engagement/in-app-messages/surveys-and-ratings): Ask for stars or a choice, collect comments, export the answers. - [Custom HTML](https://docs.grovs.io/docs/engagement/in-app-messages/custom-html): Your own page with video, audio and a JavaScript bridge. - [Links and in-app browsers](https://docs.grovs.io/docs/engagement/in-app-messages/in-app-browser): How links in messages open, and grovs links inside social app browsers. - [Reporting](https://docs.grovs.io/docs/engagement/in-app-messages/reporting): Views, clicks, dismissals and what counts as each. ## Formats Pick a template in the **Message** step. | Template | Looks like | Blocks the app | | --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | --------------------- | | **Cover** | Fills the whole screen | Yes | | **Interstitial** | A card in the middle of the screen | Yes | | **Half interstitial** | A smaller card in the middle | Yes | | **Header** | A banner at the top | No | | **Footer** | A banner at the bottom | No | | **Alert** | The system alert on iOS, a styled dialog on the web. Text only | Yes | | **Cover image** | Image only, full screen | Yes | | **Interstitial image** | Image only, centred | Yes | | **Half interstitial image** | Image only | Yes | | **Custom HTML** | Your own page, full screen, as a card or as a banner. See [Custom HTML](https://docs.grovs.io/docs/engagement/in-app-messages/custom-html) | Depends on the layout | Text templates have a title, a message, an optional image, up to two buttons and an optional tap action on the message. Image-only templates show one picture and act on a tap. Images are PNG, JPEG or GIF, up to 1 MB. You can add a separate landscape picture; otherwise the portrait one is used. Header and footer banners leave the app usable underneath. Every other template covers the app until closed. ### Alerts Alerts are for short messages: | Field | Limit | | ----------------- | -------------- | | Title | 80 characters | | Message | 300 characters | | Each button label | 30 characters | Alerts have no image, question or tap action. An alert with no buttons gets a Close button. An alert people can't dismiss needs at least one button. ### Orientation Set **Orientation** to **Any orientation**, **Portrait only** or **Landscape only**. A single-orientation message shows only while the screen is held that way. If the device turns while it's showing, it closes, and that close isn't counted as a dismissal. The SDK never rotates the screen. On the web, a window wider than it is tall counts as landscape, so most desktop browsers are landscape. ## How messages reach a device 1. **You launch.** A message for everyone goes live at once, or at its start time. A segment message first shows **Preparing audience** while grovs resolves the segment. 2. **The device syncs.** At every session start, the SDK fetches the messages this person may see and stores them with their images. 3. **A trigger fires.** The app opens, a screen is viewed or an event is tracked. The SDK checks every stored message's triggers, device conditions, schedule and limits, and shows the highest priority one. 4. **The SDK reports back.** Views, clicks and dismissals go out with the SDK's normal event batches. ### When a device syncs | Moment | iOS | Web | | ---------------------- | ------------------------------------------------------------------------------------------------ | ----------------------------------------- | | Session start | Yes. An "app is opened" trigger waits up to 2 seconds for this sync, then uses what's stored | Yes, on every page load | | Back in the foreground | Yes, unless it synced in the last minute | Yes, unless it synced in the last minute | | While in use | With the next event once 15 minutes have passed since the last sync | Every 15 minutes while the tab is visible | | After a change | Every event batch response says whether anything changed. If so, the SDK syncs within 10 seconds | Same | A launch, edit, pause or end reaches a device in use usually within a minute, and at most about 15 minutes on an idle device, and every other device the next time it opens. Start and end times are applied on the device, so scheduled messages start and stop on time without a new sync. A device that hasn't synced for 48 hours shows nothing until it syncs again. After 14 days without a sync, it deletes its stored messages. ### Segment audiences Segment membership is refreshed every hour. People who join see the message within about an hour, and people who leave stop seeing it within about an hour. The audience is fixed at launch. If you later edit the segment, the report says so. Use **Update audience** to switch the message to the segment as it is now. ## SDK requirements | Platform | Minimum | Set up | | ------------ | ---------------------- | ---------------------------------------------------------------------------------------------- | | iOS | iOS SDK 3.1.0 | [In-app messages on iOS](https://docs.grovs.io/docs/sdk/ios/in-app-messages) | | Web | Web SDK 2.1.0 | [In-app messages on the web](https://docs.grovs.io/docs/sdk/web/in-app-messages) | | Android | Android SDK 3.1.0 | [In-app messages on Android](https://docs.grovs.io/docs/sdk/android/in-app-messages) | | Flutter | Flutter plugin 3.2.0 | [In-app messages on Flutter](https://docs.grovs.io/docs/sdk/flutter/in-app-messages) | | React Native | React Native SDK 3.1.0 | [In-app messages on React Native](https://docs.grovs.io/docs/sdk/react-native/in-app-messages) | | Unity | Unity package 1.0.0 | [In-app messages on Unity](https://docs.grovs.io/docs/sdk/unity/in-app-messages) | Older SDKs and unsupported platforms show no messages and report nothing. **Ask for push permission**, **Ask for an app rating** and **Open the app's store page** only work in apps. Web visitors never see a message that uses one. ## Project switches and limits | What | Limit | | ------------------------------------- | -------------------------------------- | | Running messages per project | 50 | | Segments targeted at once per project | 20, shared with journeys | | Message size | 16 KB, not counting a custom HTML page | In **Engagement Settings**: - **In-app messages** turns every message in the project off at once. Messages keep their state, and devices in use stop showing them usually within a minute, and at most about 15 minutes on an idle device. - **Limit on in-app messages together** caps how many messages one device shows per visit and per day, across all messages. Default: 10 each. A message can opt out with **Not held back by the project limit**. - **Control group** holds back up to 20% of people from every in-app message, dashboard push and journey, to measure what messaging does overall. ## Automate it Create, test, launch, pause and end messages and read their reports from your server with the [In-app messages API](https://docs.grovs.io/docs/api-reference/in-app-messages). ## Related - [Push campaigns](https://docs.grovs.io/docs/engagement/push-campaigns) - [Journeys](https://docs.grovs.io/docs/engagement/journeys) - [Experiments](https://docs.grovs.io/docs/engagement/experiments) --- ## Create an in-app message URL: https://docs.grovs.io/docs/engagement/in-app-messages/create-a-message Description: Write the message, choose when it shows and who sees it, set how often, send a test, and launch. Build an in-app message from blank to launch. The example is Medly, a health app, showing a card to people who open their plan screen for the third time without subscribing. > **Note:** Your app or site needs [iOS SDK 3.1.0](https://docs.grovs.io/docs/sdk/ios/in-app-messages), [Android SDK 3.1.0](https://docs.grovs.io/docs/sdk/android/in-app-messages) or [Web SDK 2.1.0](https://docs.grovs.io/docs/sdk/web/in-app-messages) or later. See [SDK requirements](https://docs.grovs.io/docs/engagement/in-app-messages#sdk-requirements) for Flutter, React Native and Unity. Until a platform is connected, **In-App** shows **Connect a platform to show in-app messages** instead of the list. ### 1. Start a message Go to **Engage** › **In-App** and click **New message**. Choose **Blank message**, or **Start from a template** for a ready-made **Ask people to update**, **Rate the app** or **Quick question** message. The composer has six steps: **Message**, **Trigger**, **Audience**, **Experiment**, **Limits and schedule** and **Check and launch**. Drafts can be incomplete; **Save draft** works at any time. ### 2. Write the message In **Message**, pick a **Template** (see [Formats](https://docs.grovs.io/docs/engagement/in-app-messages#formats)) and fill in **Title**, **Message** and **Image**. Under **What people can do**, choose: - **Tap a button**: up to two buttons, plus an optional action for a tap on the message itself. - **Answer a question**: stars or a choice. See [Surveys and ratings](https://docs.grovs.io/docs/engagement/in-app-messages/surveys-and-ratings). Each button has a label and an action: | Action | Does | | ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | **Close the message** | Closes it | | **Open a link** | Opens a web or app link, such as `https://medly.app/plans` or `medly://plans`. See [Links and in-app browsers](https://docs.grovs.io/docs/engagement/in-app-messages/in-app-browser) | | **Send data to the app** | Passes up to 20 key-value pairs to your app's click callback | | **Ask for push permission** | Shows the system permission prompt. Apps only | | **Ask for an app rating** | Shows the system review prompt. Apps only | | **Open the app's store page** | Opens your app in the App Store or Google Play. Apps only | Messages with an app-only action never show on the web. A push permission message is skipped for people who already granted it. Set how the message closes: - **People can close it without answering**: close button, tap outside or swipe. Turn it off for something that must be acted on, such as a required update. The message then stays until a button or the message is tapped. - **Show a close button** - **Dim the app behind the message** Pick **Colours** from a theme or set your own, and an **Orientation** if the message only works one way. ### 3. Personalize the text **Insert a value** adds a person's attribute or a property of the triggering event, with a fallback: ```text Hi {{ user.first_name | default: "there" }} {{ event.plan_name | default: "Your plan" }} is one tap away ``` Fields work in the title, message, button labels and links. In a link, the value is URL-encoded and can't supply the scheme or host. If a field has no value and no fallback, the message isn't shown for that trigger. ### 4. Choose the trigger In **Trigger**, choose what shows the message: | Trigger | Fires | | ----------------------- | ------------------------------------------------------------------------------------------------------------------------- | | **The app is opened** | Once per visit (SDK session) | | **A screen is viewed** | When a screen or page is viewed: one by name, or any. Case-insensitive | | **An event is tracked** | When a custom event is tracked. Exact name match. Add up to five filters under **Only when the event has**; all must pass | **Add another trigger** allows up to five; any one fires the message. For Medly: **A screen is viewed**, screen `Plans`. Under **When to show it**, a **Delay** of a few seconds lets the screen settle. After the delay the message is re-checked and shown only if it still applies. A waiting message is dropped if the app goes to the background. ### 5. Choose the audience In **Audience**, choose: - **Everyone**: every visitor of the project. - **People in a segment**: refreshed hourly, so people become eligible within about an hour of joining. See [Segments](https://docs.grovs.io/docs/analytics/segments). Under **Conditions on the device (optional)**, add up to ten conditions. All must hold at trigger time. The device checks them itself, with no delay. | Condition | Checks | | ------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | **Platform** | iOS, Android or web | | **App version**, **OS version** | Compared as versions, so `2.10` is above `2.9`. On the web, OS version is empty and app version is what your site passes to the SDK | | **Language** | The device's main language, such as `en` | | **User attribute** | An attribute your app has set | | **Times an event happened** | How often this device tracked an event. Counting starts when a message naming the event first reaches the device; earlier history isn't seen. Use a segment for past behaviour | | **First seen** | Before or after a date, or in the last so many days | For Medly: **Times an event happened** `plans_viewed` is above 2, and **User attribute** `subscribed` is not `true`. ### 6. Measure it (optional) In **Experiment**, add a **Goal** (built-in event, custom event, screen or purchase, within 1, 3 or 7 days, or 30 days for purchases), a **Control group**, and up to two extra content variants. See [Experiments](https://docs.grovs.io/docs/engagement/experiments). The control group and number of variants are fixed at launch. ### 7. Set limits, priority and schedule In **Limits and schedule**, set **How often one person sees it**: at most so many times per visit, day, week or ever. Day and week allow up to 20. Leave a box empty for no limit. Visit, day and week limits are per device. The "ever" limit also holds across a person's devices and reinstalls, once they're identified. The project caps messages per device per visit and per day (10 each by default, in **Engagement Settings**). **Not held back by the project limit** exempts a message that must always show; it then doesn't count toward that limit either. **Priority** runs from **1, lowest** to **10, highest**. When several messages match, only the highest priority shows; on a tie, the older message wins. One message is on screen at a time, with at most one waiting. Under **When it runs**, set a start and an end, or **Until I end it**. These are one moment for everyone, entered in your time zone. **Only on certain days or hours** follows each device's clock, so 09:00 is local. A window such as 22:00 to 06:00 crosses midnight. ### 8. Send a test message Under **See it on a device**, enter a **User identifier** and click **Send test**. Save first: the test uses the last saved version. Use the identifier your app gives the SDK (`Grovs.userIdentifier` on iOS, `Grovs.setUserIdentifier()` on the web). The test goes to all of that person's devices, up to 50, and shows at each device's next sync within 30 minutes. It ignores triggers, conditions, limits and schedule, shows once, and isn't counted in the report. | Device | Shows the test | | -------- | --------------------------------------------------------------------------------------------------------------- | | iOS app | When the app returns to the foreground, or at its next sync | | Web page | On the next page load, when the tab comes back into view, or at the next 15-minute sync of an open, visible tab | The 30-minute window outlasts the web SDK's 15-minute sync, so an open tab still gets the test. After 30 minutes it expires; send it again. A test is refused if nobody in the project has that identifier, or the message isn't complete enough to launch. Browsers skip tests that use an app-only action. ### 9. Launch **Check and launch** summarizes the message in one sentence and lists anything to fix. Click **Launch**. - An **Everyone** message goes **Live**, or **Scheduled** if its start is in the future. - A segment message shows **Preparing audience** first. If the audience can't be resolved, it returns to **Draft** with the reason. ## After launch You can edit a live message's content, triggers, conditions, limits and schedule; devices pick up changes at their next sync. You can't change its template, audience, control group or number of variants. | Action | Does | | ---------------------- | ------------------------------------------------------------------------------------------------------------------------ | | **Pause** / **Resume** | Stops and restarts delivery. Devices in use stop usually within a minute, and at most about 15 minutes on an idle device | | **End** | Stops it for good. The report stays | | **Update audience** | Moves a segment message to the segment as it is now. The old audience is served until the new one is ready | | **Duplicate** | Copies it into a new draft | | **Delete** | Drafts only | People who answered a question or hit the "ever" limit won't see the message again. To ask again, duplicate it. ## Related - [In-app messages overview](https://docs.grovs.io/docs/engagement/in-app-messages) - [Reporting](https://docs.grovs.io/docs/engagement/in-app-messages/reporting) --- ## Surveys and ratings URL: https://docs.grovs.io/docs/engagement/in-app-messages/surveys-and-ratings Description: Ask for a star rating or a choice in an in-app message, follow up with a comment or a store review, and read or export the answers. An in-app message can ask one question in place of its buttons. Triggers, audience, limits, schedule and experiments work as usual. | Kind | People answer with | | ----------- | ------------------------------- | | **Stars** | 1 to 5 stars | | **Choices** | One of 2 to 5 options you write | After the answer, the message can ask for a comment, say thank you and, for stars, ask for an App Store review. ## Ask a question ### 1. Pick a template that can hold a question Questions work in **Cover**, **Interstitial**, **Half interstitial**, **Header** and **Footer**. A five-star footer interrupts far less than a full-screen message. **Alert**, image-only templates and **Custom HTML** can't ask questions. For a head start, use **New message** › **Start from a template** › **Rate the app** or **Quick question**. ### 2. Write the question In **Message**, under **What people can do**, choose **Answer a question**. The title and message are the question, such as "How was your first workout?". Choose the **Kind of question**. For **Choices**, add two to five options with **Add choice**. ### 3. Choose what happens after the answer Under **After the answer**: | Answer | Options | | ---------- | ----------------------------------------------------------------------- | | Low stars | **Nothing more**, or **Ask what could be better** | | High stars | **Nothing more**, **Ask what they like**, or **Ask for a store review** | | A choice | **Nothing more**, or **Ask a follow-up question** | You set where low stars end and high stars begin. An optional **Thank-you message** shows for two seconds before the message closes. ### 4. Choose whether it can be skipped With **People can close it without answering** on, closing before answering counts as a dismissal, and the message can show again within its limits. Off, the message stays until answered. ## What the person sees 1. **One tap answers.** Tapping a star or option records the answer at once, before anything else, so it's kept even if they close the app. 2. **A comment, if you asked for one.** A text box, up to 500 characters, with **Send** and **Skip**. **Send** records the comment immediately. Skipping or closing records nothing more. 3. **Thanks, if you wrote it,** for two seconds. 4. **The store review prompt,** for high stars with **Ask for a store review**. It appears after the message closes, via `SKStoreReviewController` on iOS and the Play In-App Review API on Android. The store decides how often it actually shows. Skipped on the web. On Android the app must include `com.google.android.play:review-ktx`, or the SDK skips campaigns with a review step. See [App ratings on Android](https://docs.grovs.io/docs/sdk/android/in-app-messages#app-ratings). > **Warning:** Apple's guidelines restrict prompting only happy users for reviews. The store step is off unless you choose it; decide whether it fits your app. ## People answer once A person who answered never sees that message again, on any device once they're identified. A second answer from the same person is refused. To ask again, duplicate the message and launch the copy. ## Follow up on an answer Each answer is also an `in_app_answered` event on the device, with `campaign_id`, `question_id`, `type` and `value` (the star count or the option's id). Trigger another message on it to follow up in the same session. For example, "Tell us more" for one or two stars: | Trigger | Filter | | ----------------------------------------- | --------------------------------------------------------------------- | | **An event is tracked** `in_app_answered` | `campaign_id` is your rating message's id, and `value` is less than 3 | Answers are also events in your analytics, so segments can filter on them. ## Read the answers Open the message's report and choose the **Answers** tab. - ****What people answered**** shows how many people picked each star or option and, for stars, the **out of 5 on average** score. - ****Comments**** lists comments newest first, 25 at a time. Filter with **All comments**, or **By stars** / **By answer**. On the **Results** tab, a question message shows **Answers** and **Answer rate** (answers divided by views) instead of clicks. If you remove an option after launch, its past answers keep their own row under the option's id, so counts still add up. ## Export the answers **Export** downloads a CSV of the period on screen, one row per answer: | Column | Holds | | ------------ | ------------------------------------------------ | | Date (local) | When they answered, in your computer's time zone | | Answer | The star count, or the option's text | | Comment | Their comment, if any | | User | Their user identifier, when your app set one | | Platform | `ios`, `android` or `web` | | Country | Where they answered from | | App version | The app version they used | An export holds at most the newest 10,000 answers, and a period is at most 92 days. For more, export shorter periods. ## Related - [Create an in-app message](https://docs.grovs.io/docs/engagement/in-app-messages/create-a-message) - [Reporting](https://docs.grovs.io/docs/engagement/in-app-messages/reporting) --- ## Custom HTML messages URL: https://docs.grovs.io/docs/engagement/in-app-messages/custom-html Description: Show your own HTML page as an in-app message, with video, audio and a small JavaScript bridge back to the SDK. The **Custom HTML** template shows a page you write, in a sandboxed web view on iOS and a sandboxed iframe on the web. Use it when built-in templates aren't enough: a video, a countdown, a three-slide tour or your own brand layout. Triggers, audience, limits, priority, schedule, experiments, journeys and test messages work as for any other template. > **Warning:** Custom HTML needs an app built with iOS SDK 3.1.0 or Android SDK 3.1.0 or later, or a site on Web SDK 2.1.0 or later. Older versions skip the message silently. ## Layouts The page's height sets the message's height, up to the layout's maximum. | Layout | How it shows | | ------------------------ | ------------------------------------------------------------------------------------------------------- | | **Full screen** | Fills the screen inside the safe areas. The reported height is ignored. | | **Card in the middle** | A card over a dimmed backdrop, at most 420 points wide and 90% of the screen tall. Taller pages scroll. | | **Banner at the top** | Full width, at most 30% of the screen tall. The app stays usable underneath. | | **Banner at the bottom** | As above, at the bottom. | On an iPhone in landscape (under 500 points tall), a banner can grow to 45% of the screen. The SDK always draws its own close button in the top-right corner, whatever the close-button setting, so a broken script can never trap anyone. Don't draw your own X. Making the message non-closable only disables the backdrop tap, swipe and Escape key; the SDK's close button stays. ## Build the page ### 1. Pick Custom HTML In the composer's **Message** step, set **Template** to **Custom HTML**. Pick a **Layout**, and an **Orientation** if the message only works one way. ### 2. Start from an example **Start from an example** offers eight working pages: **Promo video**, **Product launch**, **Push permission primer**, **Countdown offer**, **Feature tour**, **Feedback**, **Announcement** and **Win-back offer**. Or choose **Start blank**. Every example works on phone and desktop, in light and dark mode. Picking an example later replaces your HTML, and the code editor can't undo it. ### 3. Edit the text, pictures and buttons **Quick edit** turns every element marked `data-grovs-edit="Label"` into a form field: text, image, video or audio file, button label and action, or colour. Script-controlled elements are offered as text only. For anything else, switch to **Code**. The editor autocompletes `grovs.*`, the `data-grovs-*` attributes and `{{ user.x }}` fields. **Snippets** inserts a "No thanks" button, a tracked button, a link button, a push permission button, a sized video, editable text and personalized text. **How it works** opens a short reference. The editor shows the size against the 64 KB limit and runs the server's checks as you type. ### 4. Upload media Upload pictures, video and audio in the **Media** panel and copy each file's link into your HTML. Each file shows **Used in this message** or **Not used yet**. Files still referenced in the HTML can't be deleted. ### 5. Preview it **Preview** runs your page with the real bridge on **Phone**, **Phone landscape** or **Browser**, in light or dark. Sample values fill personalization fields. Each bridge call appears under **What the message did**, so you can check what buttons close, track and open. ## Limits | What | Limit | | ----------------- | --------------------------------------------------------------------------------- | | HTML size | 64 KB of UTF-8. Doesn't count toward the 16 KB limit for the rest of the message. | | Images | PNG, JPEG or GIF, up to 1 MB each | | Video | MP4, up to 20 MB each | | Audio | MP3 or M4A, up to 10 MB each | | Files per message | 30 files and 100 MB in total | Media downloads when the message shows, not at sync. Export videos with "fast start" (web-optimized) so playback starts before the whole file arrives. Duplicating a message copies only the files its HTML still uses. A message's files are deleted with it. ## Links and resources The server checks your HTML on save, and the SDK re-checks links on tap. **Links** (`` and `data-grovs-url`) may be: - `https://` with a host name - `mailto:`, `tel:` or `sms:` - a `#fragment`, which scrolls within the page - your app's own scheme, such as `medly://` or `instagram://` Refused: `http:`, `javascript:`, `vbscript:`, `data:`, `blob:`, `file:`, `about:`, `intent:`, and relative links (the page has no base address). **Resources** (`src`, `poster`, `srcset`, `` and `url()` in a `style` attribute) must be `https:`. Images may also be `data:image/…`. `` and `` aren't allowed. Give every `` and `