Activation Android SDK 1.1.0
1.1.0 is a large release. It introduces a first-class theming system with dark-theme support, per-screen label customization, a Missed Earnings flow in the offer wall, and a reworked rewards and reward-currency model with precise reward amounts.
A newer release is available — Activation Android SDK 1.1.1. It builds on the theming system below with finer-grained color roles and per-screen color overrides.
Several of these updates change the public API and require code changes when upgrading from 1.0.0.
This release contains breaking changes. Code written for 1.0.0 will not compile until updated. See Breaking changes and the Migration checklist below.
Installation
Bump the BOM to 1.1.0. You still depend only on activation-bom and activation; everything else is pulled in automatically.
dependencies {
implementation(platform("com.actualplatform:activation-bom:1.1.0"))
implementation("com.actualplatform:activation")
}
The SDK ships its own consumer ProGuard/R8 rules and is safe under R8 full mode. If you previously added broad keep rules for Activation classes as a workaround, you can remove them.
What's new
- Theming system. Style every SDK screen from one place via
Activation.theme(colors, typography, shapes, and icons), with automatic dark-theme support. - Per-screen labels. Customize on-screen text (such as offer-wall labels) via
Activation.appearance. - Reward-currency display options. Control how reward amounts render (as an image, a currency code, or a name; leading or trailing; whole or decimal rounding) through new options on
RewardCurrency, plus a preciseRewardPointreward type. - Missed Earnings. Shoppers can add items a scan missed, directly within the offer flow. No integrator setup is required; the flow is built into the offer wall. See Missed Earnings for the user-facing experience.
- New supporting modules. Camera and barcode modules underpin the updated scanning and Missed Earnings experiences. They are included automatically, with no separate setup or dependencies required.
Breaking changes
At a glance:
| Area | What changed | Migration |
|---|---|---|
| Reward-currency config | The four rewardCurrency* / rewardIcon properties were removed | Set the single rewardCurrency = RewardCurrency.default(...) |
RewardCurrency | Built via a factory; fields renamed; new display options | Use RewardCurrency.default(...); rename fields |
| Reward amounts | amount changed Float → RewardPoint; new blinkReceiptId | Read reward.amount.value; use reward.blinkReceiptId |
ScanReward | reward changed Int? → RewardPoint? | Wrap values in RewardPoint(...) |
ScanReceiptResult | Now requires isValidReceipt and media | Pass both to the constructor |
| Offer wall | OffersWallStyle removed; OffersWallFlow callbacks renamed | Drop style; rename callbacks; theme globally |
| Theming | The old theme symbols were removed | Use Activation.theme = ActivationTheme(...) |
Reward-currency configuration consolidated
The four loose reward-currency properties on ActivationClient were removed in favor of a single, validated rewardCurrency value.
Why: the separate fields could drift out of sync and had no room for display options. One RewardCurrency gives a single source of truth with validation.
- Kotlin
- Java
// Before (1.0.0)
ActivationClient.instance.apply {
rewardCurrencyName = "Coins"
rewardCurrencyPerDollar = 100.0
rewardPayoutPercentage = 0.6
rewardIcon = coinIconBytes
}
// After (1.1.0)
ActivationClient.instance.rewardCurrency = RewardCurrency.default(
currencyName = "Coins",
currencyPerDollar = 100.0,
userPayoutPercentage = 0.6,
)
// The reward icon is no longer set here; currency imagery is supplied
// through the theming system. See "Theming reintroduced" below.
// Before (1.0.0)
ActivationClient client = ActivationClient.getInstance();
client.setRewardCurrencyName("Coins");
client.setRewardCurrencyPerDollar(100.0);
client.setRewardPayoutPercentage(0.6);
client.setRewardIcon(coinIconBytes);
// After (1.1.0)
// default(...) has no Java-visible default arguments, so pass every parameter.
ActivationClient.getInstance().setRewardCurrency(
RewardCurrency.Companion.default(
"Coins", // currencyName
null, // currencyCode
RewardCurrencyCodePosition.Leading, // currencyCodePosition
100.0, // currencyPerDollar
0.6, // userPayoutPercentage
RewardCurrencyLabelStyle.CurrencyImage, // label style
RewardCurrencyMessagingTextStyle.CurrencyName, // messaging style
RewardCurrencyRounding.Whole, // rounding
RewardCurrencyImageLocation.Companion.getALL() // image locations
)
);
RewardCurrency is now built via a factory
RewardCurrency is no longer constructed directly; build it with RewardCurrency.default(...). Several fields were renamed, and new options control how reward amounts are displayed.
The factory also normalizes input: currencyName is trimmed to a maximum of 8 characters, and userPayoutPercentage is clamped to the range 0.4–1.0.
// Before (1.0.0)
val currency = RewardCurrency(
name = "Coins",
rewardCurrencyPerDollar = 100.0,
rewardPayoutPercentage = 0.6,
icon = coinIconBytes,
)
// After (1.1.0)
val currency = RewardCurrency.default(
currencyName = "Coins",
currencyPerDollar = 100.0,
userPayoutPercentage = 0.6,
rewardCurrencyLabelStyle = RewardCurrencyLabelStyle.CurrencyImage,
rewardRounding = RewardCurrencyRounding.Whole,
)
Field renames:
| 1.0.0 | 1.1.0 |
|---|---|
name | currencyName |
rewardCurrencyPerDollar | currencyPerDollar |
rewardPayoutPercentage | userPayoutPercentage |
icon | removed; supply imagery through theming |
New display options (all in com.actualplatform.activation):
| Option | Values |
|---|---|
RewardCurrencyLabelStyle | CurrencyImage, CurrencyCode, CurrencyName |
RewardCurrencyMessagingTextStyle | CurrencyName, CurrencyCode, NoAmount |
RewardCurrencyCodePosition | Leading, Trailing |
RewardCurrencyRounding | Whole, Decimal |
RewardCurrencyImageLocation | OfferWallItem, ReceiptTotalReward, ReceiptBoost, ReceiptTask (plus ALL) |
From Java, build the value with RewardCurrency.Companion.default(...), passing all parameters as shown in the previous section.
Reward amounts are now typed, and carry a receipt ID
Reward amount changed from Float to the precise RewardPoint type, and each reward now carries the blinkReceiptId of the receipt it came from.
Why: RewardPoint (a wrapper over Double) avoids the precision loss of Float, and blinkReceiptId lets you reconcile a reward against the scan that produced it.
lifecycleScope.launch {
ActivationClient.instance.rewards.collect { reward ->
// Before (1.0.0): amount was a Float
// val points: Float = reward.amount
// After (1.1.0): amount is a RewardPoint; unwrap with .value
val points: Double = reward.amount.value
val receiptId: String = reward.blinkReceiptId // new in 1.1.0
when (reward) {
is Rewards.Boost -> credit(receiptId, points, type = "boost")
is Rewards.Promotion -> credit(receiptId, points, type = "promo")
is Rewards.ScanFinished -> credit(receiptId, points, type = "scan")
}
}
}
RewardPoint is a Kotlin value class; its accessors are name-mangled and not practical to read from Java. Handle the rewards flow in Kotlin (or expose a small Kotlin shim that returns a plain double).
ScanReward now takes a RewardPoint?
ScanReward.reward changed from Int? to RewardPoint?, for consistency with the reward type above.
// Before (1.0.0)
ScanReward(reward = 100) // Int?
// After (1.1.0)
ScanReward(reward = RewardPoint(100.0)) // RewardPoint?
ScanReceiptResult requires isValidReceipt and media
In 1.0.0 the result you returned from onScanReceipt carried only scanResults. In 1.1.0 it requires two more values, both of which your scan flow already produces:
isValidReceipt(Boolean, required): whether the captured frames represent a valid receipt. This is the validity signal from your camera scan flow's receipt-frame validator, the same scan pass that buildsscanResults. Passtruewhen the scan captured a usable receipt andfalseotherwise. The SDK uses it to gate the receipt-validity checks it runs during post-scan processing.media(Media, required): the captured receipt image file(s), which receipt validation uses. Wrap the absolute image paths inMedia(filePaths = ...).
- Kotlin
- Java
// Before (1.0.0)
ScanReceiptResult(scanResults = scanResults)
// After (1.1.0)
ScanReceiptResult(
scanResults = scanResults,
isValidReceipt = isValidReceipt, // from your scan's receipt-frame validator
media = Media(filePaths = capturedImagePaths), // receipt image paths
)
// After (1.1.0)
new ScanReceiptResult(scanResults, isValidReceipt, new Media(capturedImagePaths));
Media.filePaths must be absolute filesystem paths to readable image files. Content URIs (content://...), file:// URLs, and asset/resource identifiers are not supported. Keep the files readable until the receipt has been processed.
Offer wall styling removed and callbacks renamed
OffersWallStyle was removed, so OffersWallFlow no longer takes a style parameter; styling is now applied globally through the theming and appearance systems. Its result callbacks were also renamed.
// Before (1.0.0)
OffersWallFlow(
style = OffersWallStyle.default(wallTitle = { Text("Earn Rewards") }),
onScanReceipt = { /* … */ },
onScanSuccess = { /* … */ },
onError = { e -> /* … */ },
onClose = { /* … */ },
onNavigationStateChange = { state -> /* … */ },
)
// After (1.1.0): no `style`; callbacks renamed; visuals/labels are global
Activation.theme = ActivationTheme(/* colors, typography, icons */)
Activation.appearance = ActivationAppearance(/* per-screen labels */)
OffersWallFlow(
onScanReceipt = { /* … */ },
onContinue = { /* … */ }, // was onScanSuccess
onException = { e -> /* … */ }, // was onError
onDismiss = { /* … */ }, // was onClose
onNavigationStateChanged = { state -> /* … */ }, // was onNavigationStateChange
)
Callback renames:
| 1.0.0 | 1.1.0 |
|---|---|
onScanSuccess | onContinue |
onError | onException |
onClose | onDismiss |
onNavigationStateChange | onNavigationStateChanged |
OffersWallFlow is a @Composable function and cannot be called from Java. Host it from a Kotlin composable or ComponentActivity.
Theming reintroduced as a first-class system
1.1.0 adds a supported theming API. Configure it once via the new top-level Activation object (typically in Application.onCreate()), and it applies to every SDK screen, including dark mode. Currency and UI imagery (previously set through ActivationClient.rewardIcon) is now supplied through the theme's icons.
import com.actualplatform.activation.Activation
import com.actualplatform.activation.theming.ActivationTheme
import com.actualplatform.activation.theming.ActivationColorDefaults
import com.actualplatform.activation.theming.ActivationIcon
Activation.theme = ActivationTheme(
lightColors = ActivationColorDefaults.lightColors(primary = 0xFF7C3AED),
darkColors = ActivationColorDefaults.darkColors(primary = 0xFF9F67FF),
icons = ActivationTheme.Icons(
rewardIcon = ActivationIcon.Bytes(coinIconBytes), // was ActivationClient.rewardIcon
scanIcon = ActivationIcon.Bytes(scanIconBytes),
),
)
Every theme token has a built-in default, so override only what you need. Icons accept ActivationIcon.Bytes, ActivationIcon.Url, or ActivationIcon.Vector. The theming types come in transitively through activation, so no extra dependency is required.
The theme is configured from Kotlin. The Activation object's setters are reachable from Java (Activation.INSTANCE.setTheme(...)), but constructing a theme is far simpler in Kotlin.
Migration checklist
- Replace the four
ActivationClientreward properties with a singlerewardCurrency = RewardCurrency.default(...). - Replace every
RewardCurrency(...)constructor call withRewardCurrency.default(...); renamename → currencyName,rewardCurrencyPerDollar → currencyPerDollar,rewardPayoutPercentage → userPayoutPercentage; removeiconusage. - In the rewards flow, read
reward.amount.value(aDouble) and the newreward.blinkReceiptId. - Wrap reward values in
RewardPoint(...)when buildingScanReward. - Add
isValidReceiptandmedia = Media(filePaths = ...)to everyScanReceiptResult(...). - Remove the
style/OffersWallStyleargument fromOffersWallFlow(...)and rename the callbacks (onContinue,onException,onDismiss,onNavigationStateChanged). - Configure
Activation.theme(and optionallyActivation.appearance) once at startup; supply currency imagery through the theme instead ofrewardIcon.
Removed and replaced
| Removed in 1.1.0 | Replacement |
|---|---|
ActivationClient.rewardCurrencyName / rewardCurrencyPerDollar / rewardPayoutPercentage / rewardIcon | ActivationClient.rewardCurrency: RewardCurrency |
RewardCurrency(...) public constructor | RewardCurrency.default(...) |
Rewards.* with a Float amount and no receipt ID | Rewards.* with amount: RewardPoint and blinkReceiptId |
ScanReward(Int?) | ScanReward(RewardPoint?) |
ScanReceiptResult(ScanResults) | ScanReceiptResult(ScanResults, isValidReceipt, Media) |
OffersWallStyle and OffersWallFlow(style = ...) | Global Activation.theme / Activation.appearance |
| Old theme symbols | Activation.theme = ActivationTheme(...) |
Testing after migration
- Compile clean. A full rebuild surfaces every break above; resolve them all before runtime testing.
- Rewards. Confirm your rewards collector reads
amount.valuecorrectly and thatblinkReceiptIdreaches your backend; verifyBoost,Promotion, andScanFinishedare each handled. - Scan round-trip. Confirm
onScanReceiptreturns aScanReceiptResultwith non-emptyMedia.filePathsand the correctisValidReceipt. - Reward-currency display. Smoke-test each label style, messaging style, and rounding mode you configure, and check image placement against your chosen image locations.
- Theming. Validate your colors, typography, and icons render correctly in both light and dark mode, and that
Activation.appearancelabels replace your oldOffersWallStyletitles. - Offer wall. Exercise the full flow (
onContinue,onException,onDismiss, navigation-state changes).
Java interop: ActivationClient configuration with plain types and enums is Java-friendly. Reward handling and all UI/theming are Kotlin-first: RewardPoint is a value class, and the offer wall and theme builders are Compose APIs.