Handle actions
This page implements GronaHost, the interface 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 interface
Section titled “The interface”| Method | Called when | Return |
|---|---|---|
product(productId) |
The SDK needs a product’s name, price, image or stock state, for copy or to check an action | A GronaProduct, or null when the product is unknown |
add(lines, 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 on a substitute decision |
A GronaAddResult |
navigate(route, source) |
The shopper tapped a Grona call to action that opens a screen | true when a screen opened, false otherwise |
The SDK calls these methods on the main thread 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 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 a fast in-memory lookup with no side effects.
A complete host
Section titled “A complete host”package com.example.acmemarket
import ai.grona.sdk.GronaAddResultimport ai.grona.sdk.GronaHostimport ai.grona.sdk.GronaLineimport ai.grona.sdk.GronaProductimport ai.grona.sdk.GronaRouteimport ai.grona.sdk.GronaSource
class AcmeGronaHost(private val app: AcmeMarketApp) : GronaHost {
override fun product(productId: String): GronaProduct? { val p = app.catalog.find(productId) ?: return null // in memory, no I/O return GronaProduct.Builder(p.id, p.name, p.priceMinor) .wasPriceMinor(p.wasPriceMinor) .imageUri(p.imageUrl) .inStock(p.inStock) .build() }
override fun add(lines: List<GronaLine>, source: GronaSource): GronaAddResult { val added = lines.filter { app.cart.add(it.productId, it.qty) } return when { added.isEmpty() -> GronaAddResult.REJECTED added.size < lines.size -> GronaAddResult.PARTIAL else -> GronaAddResult.ACCEPTED } }
override fun replace(productId: String, with: GronaLine, source: GronaSource): GronaAddResult { if (!app.cart.contains(productId)) return GronaAddResult.REJECTED val ok = app.cart.replace(productId, with.productId, with.qty) return if (ok) GronaAddResult.ACCEPTED else GronaAddResult.REJECTED }
override fun navigate(route: GronaRoute, source: GronaSource): Boolean { val id = route.id return when (route.type) { GronaRoute.HOME -> app.router.openHome() GronaRoute.DEALS -> app.router.openDeals() GronaRoute.CART -> app.router.openCart() GronaRoute.CHECKOUT -> app.router.openCheckout() GronaRoute.PRODUCT -> id != null && app.router.openProduct(id) GronaRoute.CATEGORY -> id != null && app.router.openCategory(id) else -> false // a route type from a newer configuration } }}Here catalog, cart and router stand for your app’s own classes. Each router call returns true when it opened a screen. The cart changes go through your normal cart code, so your cart listener pushes the new basket to Grona as it does for any other change.
Product lookups
Section titled “Product lookups”product() must be a fast in-memory lookup. The SDK calls it on the main thread to check actions and to fill copy, so do not read the disk or the network inside it. Return null for products you do not know.
GronaProduct.Builder |
Default | Meaning |
|---|---|---|
Builder(productId, name, priceMinor) |
Required | The product id, the display name and the price in minor units, so 349 is 3.49. |
wasPriceMinor(price) |
null |
A previous price, for strike-through copy. |
imageUri(uri) |
null |
The product image. |
inStock(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:
GronaOptions.Builder("acme-market") .maxAddQuantity(3) .maxAddLines(4) // ... .build()Routes
Section titled “Routes”GronaRoute is a typed destination. Grona never passes raw links, so your own router decides what each route opens.
| Constant | type |
id |
|---|---|---|
GronaRoute.HOME |
home |
null |
GronaRoute.PRODUCT |
product |
A product id, such as sku_10432 |
GronaRoute.CATEGORY |
category |
A category id, such as dairy |
GronaRoute.CART |
cart |
null |
GronaRoute.CHECKOUT |
checkout |
null |
GronaRoute.DEALS |
deals |
null |
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.