Skip to content
The mobile SDKs are available on request. Email support@grona.ai to get access.

Preview links

This page lets testers open a campaign variant on their own device before it goes live. A preview link carries a one-time pairing code. The SDK exchanges it for a signed pin bound to that install, so the tester sees the variant and nobody else’s experience changes. Your app routes every incoming link to Grona.handle.

Preview links use your app’s own URL scheme with the host grona-preview. If your app handles Universal Links, the SDK also accepts the same link on your own domain, with the path /grona-preview.

Link Effect
acmemarket://grona-preview?code=<pairing code> Pairs this install with one campaign variant.
acmemarket://grona-preview?clear=1 Removes every preview pin on this device.
https://shop.example.com/grona-preview?code=<pairing code> The same pairing, delivered as a Universal Link. ?clear=1 works here too.

A code is 8 to 64 characters of letters, digits and hyphens. The SDK rejects a link with user info, a port, a fragment, an extra or repeated parameter, or a query longer than 256 characters. A custom-scheme link must have the host grona-preview and an empty path or /. An https link must have the path /grona-preview exactly.

  1. Register your URL scheme. In Xcode, open your app target’s Info tab and add a URL type with your scheme, or add it to Info.plist:

    Info.plist
    <key>CFBundleURLTypes</key>
    <array>
    <dict>
    <key>CFBundleURLName</key>
    <string>com.example.acmemarket</string>
    <key>CFBundleURLSchemes</key>
    <array>
    <string>acmemarket</string>
    </array>
    </dict>
    </array>
  2. Pass every incoming URL to Grona.handle before your own routing. Cover every place a link arrives, including links that launch the app:

    AcmeMarketApp.swift
    import Grona
    import SwiftUI
    @main
    struct AcmeMarketApp: App {
    var body: some Scene {
    WindowGroup {
    HomeView()
    .onOpenURL { url in
    if Grona.handle(url) { return } // a Grona preview link, accepted or not
    AcmeRouter.shared.open(url) // your own deep-link routing
    }
    }
    }
    }

    onOpenURL receives custom-scheme links and Universal Links, including the link that launched the app.

  3. Set a previewURL in your options. Grona provides it for your workspace. Without it the SDK ignores codes and can only clear pins.

Grona.handle is safe from any thread and returns at once; pairing runs in the background. It returns true for any Grona preview link, whether or not the SDK accepted it: a link whose host is grona-preview, or an https link whose path is /grona-preview. It returns false for every other link. A rejected link is logged through your GronaLogger.

Any app can register the same custom URL scheme, and iOS does not define which app opens a link when two apps claim it. A Universal Link opens only your app, because your own domain names it. If your app already handles Universal Links, you can deliver preview codes that way:

  1. Add the Associated Domains capability to your app target with applinks:shop.example.com.

  2. Add a component for the preview path to the apple-app-site-association file your domain serves:

    https://shop.example.com/.well-known/apple-app-site-association
    {
    "applinks": {
    "details": [
    {
    "appIDs": ["ABCDE12345.com.example.acmemarket"],
    "components": [
    { "/": "/grona-preview", "comment": "Grona preview links" }
    ]
    }
    ]
    }
    }
  3. Send testers the code on your domain: https://shop.example.com/grona-preview?code=<pairing code>. The handlers above already route it to Grona.handle. Grona’s dashboard shows the custom-scheme link, so build the https link yourself by replacing acmemarket://grona-preview with https://shop.example.com/grona-preview.

Apple caches apple-app-site-association files, so a new component can take time to reach devices. A Universal Link tapped in Safari on a page of the same domain opens in Safari, not in your app.

When a code arrives, the SDK posts the code, the install id and the workspace key over https to previewURL, and stores the signed pin it gets back.

  • Pairing is refused while consent is .denied, because the request carries the install id. Under .unknown and .granted it runs: pairing is a tester action, not analytics.
  • One exchange runs at a time. A link that arrives while one is in flight is ignored.
  • Any response other than a valid pin leaves existing pins unchanged.
  • Lifetime. A pin lasts until it expires (at most 24 hours), until a clear=1 link, until Grona.reset(), or until consent becomes .denied. It survives app restarts and is verified again on every load.
  • One per campaign. The newest pin for a campaign wins.
  • What it bypasses. The pinned campaign goes first in its placement and skips audience rules, holdouts, the traffic split and activation.
  • What it never bypasses. A campaign disabled in the configuration (enabled: false), an expired configuration, or validation. A customer-unit campaign, such as every offer, still shows only to an identified shopper.
  • Control variants. A pinned control variant renders nothing, as any control arm does.
  • Marking. SDK components draw a “Preview” chip, decisions carry isPreview == true, and every event carries a preview flag so analysis excludes it.
  • No grants. A pinned offer renders without a grant, but it never appears in Grona.activeTreatments(). Pins never produce a grant, so a preview can never change what an order costs.

Debug builds can also pin a variant locally from the debug view, without a link. Local pins are unsigned, last at most 24 hours, and are ignored in builds compiled without DEBUG.