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.
Link format
Section titled “Link format”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.
Handle the link
Section titled “Handle the link”-
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> -
Pass every incoming URL to
Grona.handlebefore your own routing. Cover every place a link arrives, including links that launch the app:AcmeMarketApp.swift import Gronaimport SwiftUI@mainstruct AcmeMarketApp: App {var body: some Scene {WindowGroup {HomeView().onOpenURL { url inif Grona.handle(url) { return } // a Grona preview link, accepted or notAcmeRouter.shared.open(url) // your own deep-link routing}}}}onOpenURLreceives custom-scheme links and Universal Links, including the link that launched the app.SceneDelegate.swift import Gronaimport UIKitfinal class SceneDelegate: UIResponder, UIWindowSceneDelegate {var window: UIWindow?func scene(_ scene: UIScene,willConnectTo session: UISceneSession,options connectionOptions: UIScene.ConnectionOptions) {// ... set up the window, then handle a link that launched the appconnectionOptions.urlContexts.forEach { open($0.url) }connectionOptions.userActivities.compactMap(\.webpageURL).forEach { open($0) }}// Custom-scheme links while the app runsfunc scene(_ scene: UIScene, openURLContexts URLContexts: Set<UIOpenURLContext>) {URLContexts.forEach { open($0.url) }}// Universal Links while the app runsfunc scene(_ scene: UIScene, continue userActivity: NSUserActivity) {if let url = userActivity.webpageURL { open(url) }}private func open(_ url: URL) {if Grona.handle(url) { return } // a Grona preview link, accepted or notAcmeRouter.shared.open(url) // your own deep-link routing}}An app without scenes routes the same links from
application(_:open:options:)andapplication(_:continue:restorationHandler:)in its app delegate. -
Set a
previewURLin 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.
Use Universal Links for preview
Section titled “Use Universal Links for preview”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:
-
Add the Associated Domains capability to your app target with
applinks:shop.example.com. -
Add a component for the preview path to the
apple-app-site-associationfile 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" }]}]}} -
Send testers the code on your domain:
https://shop.example.com/grona-preview?code=<pairing code>. The handlers above already route it toGrona.handle. Grona’s dashboard shows the custom-scheme link, so build the https link yourself by replacingacmemarket://grona-previewwithhttps://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.
Pairing
Section titled “Pairing”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.unknownand.grantedit 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.
How a pin behaves
Section titled “How a pin behaves”- Lifetime. A pin lasts until it expires (at most 24 hours), until a
clear=1link, untilGrona.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.
Next steps
Section titled “Next steps”- Test and debug your integration.