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

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.

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.

AcmeGronaHost.kt
package com.example.acmemarket
import ai.grona.sdk.GronaAddResult
import ai.grona.sdk.GronaHost
import ai.grona.sdk.GronaLine
import ai.grona.sdk.GronaProduct
import ai.grona.sdk.GronaRoute
import 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() 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.

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.

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()

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.

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.