Handle actions
This page implements GronaHost, the protocol Grona uses to act inside your app. When a shopper taps a Grona call to action, or your code calls an action on a decision, the SDK checks the request and then calls your host. Your cart, your catalogue and your router do the work, so prices, stock rules and navigation stay yours.
The protocol
Section titled “The protocol”GronaHost is a main-actor protocol, and the SDK calls its methods on the main actor only. It calls add, replace and navigate only from a shopper’s tap or from your own call on a decision, never on a timer or on its own. It calls product(id:) whenever it resolves content for a slot or a decision. That can follow a basket change, a grant, a refresh or a timer, so keep product(id:) a fast in-memory lookup with no side effects.
| Method | Called when | Return |
|---|---|---|
product(id:) |
The SDK resolves content for a slot or a decision, or checks an action, and needs a product’s name, price, image or stock state | A GronaProduct, or nil when the product is unknown |
add(_:source:) |
The shopper tapped an add on a Grona banner, or your code called add or addAll on a decision |
A GronaAddResult |
replace(productID:with:source:) |
Your code called replace(with:) on a substitute decision |
A GronaAddResult |
navigate(to:source:) |
The shopper tapped a Grona call to action that opens a screen | true when a screen opened, false otherwise |
A complete host
Section titled “A complete host”import Grona
@MainActorfinal class AcmeGronaHost: GronaHost { private let services: AcmeServices
init(services: AcmeServices) { self.services = services }
private var catalog: AcmeCatalog { services.catalog } private var cart: AcmeCart { services.cart } private var router: AcmeRouter { services.router }
func product(id: String) -> GronaProduct? { guard let item = catalog.item(id: id) else { return nil } // in memory, no I/O return GronaProduct( id: item.id, name: item.name, priceMinor: item.priceMinor, wasPriceMinor: item.wasPriceMinor, imageURL: item.imageURL, inStock: item.inStock ) }
func add(_ lines: [GronaLine], source: GronaSource) -> GronaAddResult { let added = lines.filter { cart.add(productID: $0.productID, quantity: $0.quantity) } if added.isEmpty { return .rejected } return added.count < lines.count ? .partial : .accepted }
func replace(productID: String, with line: GronaLine, source: GronaSource) -> GronaAddResult { guard cart.contains(productID: productID) else { return .rejected } let replaced = cart.replace(productID: productID, with: line.productID, quantity: line.quantity) return replaced ? .accepted : .rejected }
func navigate(to route: GronaRoute, source: GronaSource) -> Bool { switch route.type { case .home: return router.openHome() case .deals: return router.openDeals() case .cart: return router.openCart() case .checkout: return router.openCheckout() case .product: guard let id = route.id else { return false } return router.openProduct(id) case .category: guard let id = route.id else { return false } return router.openCategory(id) default: return false // a route type from a newer configuration } }}Here AcmeServices, AcmeCatalog, AcmeCart and AcmeRouter stand for your app’s own types. Pass AcmeGronaHost(services: .shared) to Grona.start, as Initialise the SDK shows. Each router call returns true when it opened a screen. The cart changes go through your normal cart code, so your cart’s change notification pushes the new basket to Grona as it does for any other change.
Product lookups
Section titled “Product lookups”product(id:) must be a fast in-memory lookup with no side effects. The SDK calls it on the main actor whenever it resolves content or checks an action, including after a basket change, a grant, a refresh or a timer, so do not read the disk or the network inside it. Return nil for products you do not know.
GronaProduct parameter |
Default | Meaning |
|---|---|---|
id |
Required | The product id, such as sku_10432. |
name |
Required | The display name. |
priceMinor |
Required | The price in minor units, so 349 is 3.49. |
wasPriceMinor |
nil |
A previous price, for strike-through copy. |
imageURL |
nil |
The product image. |
inStock |
true |
Whether the product can be added now. |
Prices, totals and product names in Grona copy always come from this data, never from the configuration. A product your host does not know, or reports out of stock, is dropped from decisions and from add actions.
Add results
Section titled “Add results”GronaAddResult |
Return it when |
|---|---|
.accepted |
Every line was added, or the replacement was made. |
.partial |
Some lines were added and some were not. |
.rejected |
Nothing changed. |
The SDK logs the result with the action, so return what actually happened in your cart.
Add caps
Section titled “Add caps”Two options bound what one action may add. Both default to 5 and accept 1 to 20.
| Option | Limit |
|---|---|
maxAddQuantity |
The highest quantity of one product in one action. |
maxAddLines |
The most lines in one action. |
Before calling add, the SDK keeps only lines with a quantity from 1 to maxAddQuantity, a product your host knows and reports in stock, and no repeated product. It keeps at most maxAddLines of them. If no line is left, the action returns .rejected and your host is not called. A replacement uses the cart line’s quantity, capped at maxAddQuantity.
Set the caps in GronaOptions if your cart has its own limits:
let options = try GronaOptions( workspaceKey: "acme-market", configKeys: ["acme-market-2026-10": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE..."], maxAddQuantity: 3, maxAddLines: 4)Routes
Section titled “Routes”GronaRoute is a typed destination. Grona never passes raw links, so your own router decides what each route opens. route.type is a GronaRouteType, a struct rather than an enum, so a switch over it needs a default case for types from newer configurations.
route.type |
Raw value | route.id |
|---|---|---|
.home |
home |
nil |
.product |
product |
A product id, such as sku_10432 |
.category |
category |
A category id, such as dairy |
.cart |
cart |
nil |
.checkout |
checkout |
nil |
.deals |
deals |
nil |
Return false for a route type you do not recognise, an id you cannot find, or a screen you cannot open right now, such as checkout with an empty cart. Return true only when a screen actually opened.
Sources
Section titled “Sources”Every call carries a GronaSource that says which campaign the action came from.
| Property | Meaning |
|---|---|
placement |
The placement, such as home_top |
campaignID |
The campaign |
variantID |
The variant within the campaign |
You do not need it to complete the action. Use it if your own analytics should record where an add came from.
Next steps
Section titled “Next steps”- Track events from your analytics layer.
- Connect offers and grants.