Skip to main content

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.

tip

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.

warning

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.

build.gradle.kts
dependencies {
implementation(platform("com.actualplatform:activation-bom:1.1.0"))
implementation("com.actualplatform:activation")
}
tip

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 precise RewardPoint reward 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:

AreaWhat changedMigration
Reward-currency configThe four rewardCurrency* / rewardIcon properties were removedSet the single rewardCurrency = RewardCurrency.default(...)
RewardCurrencyBuilt via a factory; fields renamed; new display optionsUse RewardCurrency.default(...); rename fields
Reward amountsamount changed FloatRewardPoint; new blinkReceiptIdRead reward.amount.value; use reward.blinkReceiptId
ScanRewardreward changed Int?RewardPoint?Wrap values in RewardPoint(...)
ScanReceiptResultNow requires isValidReceipt and mediaPass both to the constructor
Offer wallOffersWallStyle removed; OffersWallFlow callbacks renamedDrop style; rename callbacks; theme globally
ThemingThe old theme symbols were removedUse 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.

// 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.

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.41.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.01.1.0
namecurrencyName
rewardCurrencyPerDollarcurrencyPerDollar
rewardPayoutPercentageuserPayoutPercentage
iconremoved; supply imagery through theming

New display options (all in com.actualplatform.activation):

OptionValues
RewardCurrencyLabelStyleCurrencyImage, CurrencyCode, CurrencyName
RewardCurrencyMessagingTextStyleCurrencyName, CurrencyCode, NoAmount
RewardCurrencyCodePositionLeading, Trailing
RewardCurrencyRoundingWhole, Decimal
RewardCurrencyImageLocationOfferWallItem, ReceiptTotalReward, ReceiptBoost, ReceiptTask (plus ALL)
note

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")
}
}
}
note

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 builds scanResults. Pass true when the scan captured a usable receipt and false otherwise. 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 in Media(filePaths = ...).
// 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
)
note

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.01.1.0
onScanSuccessonContinue
onErroronException
onCloseonDismiss
onNavigationStateChangeonNavigationStateChanged
note

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.

note

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 ActivationClient reward properties with a single rewardCurrency = RewardCurrency.default(...).
  • Replace every RewardCurrency(...) constructor call with RewardCurrency.default(...); rename name → currencyName, rewardCurrencyPerDollar → currencyPerDollar, rewardPayoutPercentage → userPayoutPercentage; remove icon usage.
  • In the rewards flow, read reward.amount.value (a Double) and the new reward.blinkReceiptId.
  • Wrap reward values in RewardPoint(...) when building ScanReward.
  • Add isValidReceipt and media = Media(filePaths = ...) to every ScanReceiptResult(...).
  • Remove the style / OffersWallStyle argument from OffersWallFlow(...) and rename the callbacks (onContinue, onException, onDismiss, onNavigationStateChanged).
  • Configure Activation.theme (and optionally Activation.appearance) once at startup; supply currency imagery through the theme instead of rewardIcon.

Removed and replaced

Removed in 1.1.0Replacement
ActivationClient.rewardCurrencyName / rewardCurrencyPerDollar / rewardPayoutPercentage / rewardIconActivationClient.rewardCurrency: RewardCurrency
RewardCurrency(...) public constructorRewardCurrency.default(...)
Rewards.* with a Float amount and no receipt IDRewards.* 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 symbolsActivation.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.value correctly and that blinkReceiptId reaches your backend; verify Boost, Promotion, and ScanFinished are each handled.
  • Scan round-trip. Confirm onScanReceipt returns a ScanReceiptResult with non-empty Media.filePaths and the correct isValidReceipt.
  • 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.appearance labels replace your old OffersWallStyle titles.
  • Offer wall. Exercise the full flow (onContinue, onException, onDismiss, navigation-state changes).
note

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.