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

Initialise the SDK

This page starts the SDK when your app launches. You build a GronaOptions value, implement GronaHost so Grona can reach your cart and router, and call Grona.start once, from your SwiftUI App initialiser or your app delegate. Every other Grona call does nothing until start has run.

Grona provides the workspace key, the URLs and the signing keys for your workspace. The values below are Acme Market’s; replace them with yours.

  1. Add the signed bundled configuration that Grona exports for your workspace to your app target, named grona-config.json. Drag it into Xcode’s project navigator, tick your app target, and check that it is listed under Build Phases > Copy Bundle Resources. The SDK applies it at launch and whenever it is offline.

  2. Build the options and call Grona.start:

    AcmeMarketApp.swift
    import Grona
    import SwiftUI
    @main
    struct AcmeMarketApp: App {
    init() {
    do {
    let options = try GronaOptions(
    workspaceKey: "acme-market",
    configURL: URL(string: "https://api.grona.ai/v1/mobile/workspaces/acme-market/config"),
    bundledConfig: Bundle.main.url(forResource: "grona-config", withExtension: "json"),
    configKeys: ["acme-market-2026-10": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE..."],
    eventsURL: URL(string: "https://api.grona.ai/v1/mobile/events"),
    previewURL: URL(string: "https://api.grona.ai/v1/mobile/preview/pair")
    )
    Grona.start(options: options, host: AcmeGronaHost(services: .shared))
    } catch {
    assertionFailure("Invalid Grona options: \(error)")
    }
    }
    var body: some Scene {
    WindowGroup {
    HomeView()
    }
    }
    }
  3. Implement AcmeGronaHost (outline below), then pass the shopper’s consent, identity and basket as your app learns them.

  • Constant time. start queues its work off the main thread and returns. It does no disk or network work on the main thread.
  • Never throws. If start-up fails, the SDK logs through your GronaLogger and stays inactive. Your app carries on.
  • Once only. A second call is ignored, so you cannot change options after launch.
  • Your app only. In an app extension start does nothing and Grona stays inactive there.
  • Before start. Every other call does nothing. Grona.decision returns a subscription that never delivers. Grona.refresh(completion:) still calls its completion, and await Grona.refresh() still returns.
  • Concurrency. start and Grona.decision are main-actor APIs, and Swift checks at compile time that you call them on the main actor. Setters such as setBasket and identify are safe from any thread. The SDK calls your GronaHost on the main actor only.
  • In order. The SDK applies your calls in the order you make them from any one thread, including calls made right after start. So setConsent straight after start, or identify followed by setProfile, behaves as written.
  • Your host stays alive. The SDK keeps a strong reference to the host for the life of the process. Make it a long-lived object, not a view controller.
  • Before the first passcode. If iOS launches your app in the background after a restart, before the shopper has entered their passcode, the SDK’s files cannot be read yet. The SDK waits until they can be, rather than starting with a new install id.

At start the SDK applies the bundled configuration, then fetches the remote one. It never refreshes the configuration more often than every 300 seconds. For how configurations are signed and checked, see Signed configuration.

GronaOptions is a struct. Pass the parameters you need in this order; every parameter except workspaceKey and configKeys has a default.

Parameter Default Validation What it does
workspaceKey Required Matches [A-Za-z0-9._:-]{1,128} Your workspace key, for example acme-market. It is a public identifier, not a secret.
configURL nil https only Signed remote configuration. Without it the SDK runs on the bundled copy only.
bundledConfig nil File URL of the signed bundled configuration, applied at launch and offline, for example Bundle.main.url(forResource: "grona-config", withExtension: "json").
configKeys Required At least one Trusted configuration signing keys, as key id to public key: the base64 DER SubjectPublicKeyInfo of a P-256 key. Add the current and the next key.
grantKeys [:] Trusted grant signing keys, in the same format. Needed for offers.
eventsURL nil https only Events endpoint. Without it the SDK sends no events and keeps no event queue. Debug builds keep the last 200 events in memory for the debug view.
previewURL nil https only Preview pairing endpoint. Without it preview links can only clear pins.
activationSource nil A GronaActivationSource that can switch campaigns off. See Activation source.
grantSource nil A GronaGrantSource that fetches grants from your backend. See Offers and grants.
theme GronaTheme() Fonts, colours and corner radius for Grona components. See Theming.
logger nil Receives SDK diagnostics. Silent when nil. See Test and debug.
maxAddQuantity 5 1 to 20 Highest quantity one Grona action may add. See Handle actions.
maxAddLines 5 1 to 20 Most lines one Grona action may add.
sendPreConsentEvents false Send events buffered before consent once consent is granted. See Consent.
demoMode false Demos only, never in production. See Offers and grants.

GronaOptionsError has four cases:

Case Thrown when
.invalidWorkspaceKey The workspace key does not match [A-Za-z0-9._:-]{1,128}.
.insecureURL(URL) configURL, eventsURL or previewURL has a scheme other than https.
.missingConfigKey configKeys is empty.
.invalidAddCap maxAddQuantity or maxAddLines is outside 1 to 20.

GronaHost is how Grona works through your app’s own code. The SDK never reaches into your app any other way. It is a main-actor protocol with four methods:

AcmeGronaHost.swift
import Grona
@MainActor
final class AcmeGronaHost: GronaHost {
private let services: AcmeServices
init(services: AcmeServices) {
self.services = services
}
// A fast in-memory lookup in your catalogue; nil when the product is unknown
func product(id: String) -> GronaProduct? {
services.catalog.gronaProduct(id: id)
}
// Add lines to the cart after the shopper tapped a Grona call to action
func add(_ lines: [GronaLine], source: GronaSource) -> GronaAddResult {
services.cart.addFromGrona(lines)
}
// Swap one cart line, for example an out-of-stock item for its substitute
func replace(productID: String, with line: GronaLine, source: GronaSource) -> GronaAddResult {
services.cart.replaceFromGrona(productID: productID, with: line)
}
// Open the route with your own router; false when the route is unknown
func navigate(to route: GronaRoute, source: GronaSource) -> Bool {
services.router.open(route)
}
}

Here AcmeServices, catalog, cart and router stand for your app’s own types. Handle actions has a complete implementation and the rules for each return value.

An activation source lets an external campaign system switch Grona campaigns off. Each campaign can list external ids, such as CMP-1024. When your source reports which external ids are active, a campaign that lists ids stays active only while at least one of them is active. Activation can only switch campaigns off, never on, and campaigns without external ids ignore it.

AcmeActivationSource.swift
import Grona
struct AcmeActivationSource: GronaActivationSource {
let campaigns: AcmeCampaignClient
// Called off the main actor. After 10 seconds the SDK stops waiting and cancels the task.
func activeExternalIDs() async -> Set<String>? {
do {
return try await campaigns.fetchActiveIDs() // for example ["CMP-1024"]
} catch {
return nil // unknown: Grona does not narrow anything
}
}
}

Pass it as activationSource: AcmeActivationSource(campaigns: campaignClient) in your options.

  • Return nil when you do not know. Unknown never switches a campaign off.
  • The SDK polls the source every 5 minutes while the app is in the foreground. An answer is trusted for at most 10 minutes. A poll that fails, times out or finds the previous call still running makes activation unknown at once.
  • After 10 seconds the SDK stops waiting, treats the answer as unknown and cancels the task. Check for cancellation, or use APIs that do, such as URLSession’s async methods. The SDK calls the source again only once the earlier call has returned.
  • The protocol requires Sendable. The SDK calls it off the main actor, sometimes while your own code runs, so keep it thread-safe and away from your views.

For how activation fits the kill switch, see Campaign activation and kill switch.

The SDK refreshes on its own. When you need a refresh now, for example after a pull to refresh, call one of these:

Grona.refresh() // no completion signal
Grona.refresh {
// Main actor, called once, also on timeouts and failures
}

In async code, use the async form:

await Grona.refresh()

All three poll the activation source and ask for a configuration fetch, subject to the 300-second floor. The completion and async forms wait for an activation answer requested after the call (10-second timeout), then for the fetch and re-evaluation, before the completion runs or the async call returns. In an async context Swift chooses the async form for Grona.refresh(), so write await. To start a refresh from async code without waiting, use the completion form.

A campaign that was switched off stays off for the rest of the screen visit, even after it is switched back on. To show it at once, end the visit after the refresh and bind again: cancel the decision’s GronaSubscription, or set the slot’s screen to nil and back. See Screen visits for UIKit and SwiftUI code.