Android Activation Integration
SDK Initialization
When the Activation SDK is on the classpath, BlinkReceiptSdk.initialize() automatically detects it and configures the ActivationClient with deviceId, clientUserId, and appBundleId. Set user identity in the onComplete callback.
Kotlin
import com.microblink.BlinkReceiptSdk
import com.actualplatform.activation.ActivationClient
import com.actualplatform.activation.RewardCurrency
import com.actualplatform.activation.RewardPoint
import com.actualplatform.activation.ScanReward
import okio.ByteString.Companion.encodeUtf8
BlinkReceiptSdk.initialize(context, object : InitializeCallback {
override fun onComplete() {
// ActivationClient is now configured with device identity.
// 1. Configure user identification (at least one required)
ActivationClient.instance.apply {
hashedEmail = userEmail.encodeUtf8().sha256().hex()
hashedPhone = userPhone.encodeUtf8().sha256().hex() // optional
}
// 2. Configure in-app currency
ActivationClient.instance.rewardCurrency = RewardCurrency.default(
currencyName = "Points",
userPayoutPercentage = 1.0, // 0–1 scale, clamped to 0.4…1.0
currencyPerDollar = 100.0,
)
// 3. Configure base scan reward
ActivationClient.instance.scanReward = ScanReward(reward = RewardPoint(10.0))
}
override fun onException(throwable: Throwable) {
Log.e("Init", "SDK init failed", throwable)
}
})
Java
BlinkReceiptSdk.initialize(context, new InitializeCallback() {
@Override
public void onComplete() {
ActivationClient client = ActivationClient.getInstance();
String hash = ByteString.encodeUtf8(email).sha256().hex();
client.setHashedEmail(hash);
}
@Override
public void onException(@NonNull Throwable throwable) {
Log.e("Init", "SDK init failed", throwable);
}
});
The SDK debounces property changes. Rapid sequential sets (e.g., the receipts SDK setting deviceId followed by the host app setting hashedEmail) coalesce into a single registration call.
At least one of hashedEmail or hashedPhone must be set before displaying the offer wall. The offer wall will not load promotions without a registered user.
Presenting the Offer Wall
The Offer Wall is the primary entry point for user engagement. There are two integration options depending on your app's navigation approach.
Option A: OffersWall (Recommended)
OffersWall renders the promotions offer wall as an embeddable composable that fits within your existing navigation stack. The host app retains full control over the navigation chrome (toolbar, back handling) and the scan experience.
When the user taps "Scan Receipt", the onScanReceipt callback fires and the host app launches its own camera flow. Set .activation(true) on ScanOptions to let the SDK handle the post-scan experience (loading screen, receipt summary, rewards) inside the camera activity.
import com.actualplatform.activation.OffersWall
@Composable
fun PromotionsScreen(onBack: () -> Unit) {
OffersWall(
onScanReceipt = {
// Launch your camera or scan flow here
},
onDismiss = onBack,
)
}
OffersWall Parameters
| Parameter | Type | Description |
|---|---|---|
modifier | Modifier | Applied to the root container. |
onScanReceipt | suspend () -> Unit | Called when the user taps "Scan Receipt". Launch your camera flow here. |
onDismiss | (() -> Unit) | Called on back navigation. When null, the SDK header is hidden. |
onNavigationStateChanged | (OffersWallNavigationState) -> Unit | Emits Browsing and Scanning states for analytics or UI coordination. |
You have an existing navigation stack and want to embed the offer wall as one destination, control the toolbar and back button, and use .activation(true) on ScanOptions.
Option B: OffersWallFlow
OffersWallFlow is a full-screen, self-contained composable that manages the entire promotions experience: offer wall, ads loading, and receipt summary. The SDK owns all internal navigation between these screens.
The key difference: onScanReceipt is a suspend function that must return a ScanReceiptResult (or null if cancelled). The SDK uses this result to drive the internal loading and receipt summary screens.
import com.actualplatform.activation.OffersWallFlow
import com.actualplatform.activation.ScanReceiptResult
@Composable
fun FullPromotionsFlow(onFinish: () -> Unit) {
OffersWallFlow(
onScanReceipt = {
launchCameraAndAwaitResult()
},
onContinue = onFinish,
onException = { exception: ActivationException -> Log.e("Activations", "${exception.message}") },
onDismiss = onFinish,
)
}
Additional OffersWallFlow Parameters
| Parameter | Type | Description |
|---|---|---|
onScanReceipt | suspend () -> ScanReceiptResult? | Must return scan results or null if cancelled. |
onContinue | () -> Unit | Called when the user taps "Continue" on the receipt summary screen. |
onException | (ActivationException) -> Unit | Called on errors with an ActivationException. |
You want a dedicated full-screen promotions experience with no host app navigation chrome. Do not set .activation(true) on ScanOptions with this option (OffersWallFlow handles post-scan itself).
Starting a Receipt Scan
When using OffersWall with the Paper Receipt Camera UI, enable the activation post-scan flow on ScanOptions:
val launcher = rememberLauncherForActivityResult(
contract = CameraRecognizerContract(),
) { result ->
when (result) {
is CameraRecognizerResults.Success -> {
scanResults = result.results
}
is CameraRecognizerResults.Exception -> {
Log.e("Scan", "Error: ${result.exception}")
}
CameraRecognizerResults.Cancelled -> {
// User cancelled the scan
}
}
}
// Build scan options with the activation post-scan flow enabled
val scanOptions = ScanOptions.newBuilder()
.activation(true) // Enables post-scan activation flow
.build()
// Launch the camera
launcher.launch(
CameraRecognizerOptions.Builder()
.options(scanOptions)
.characteristics(cameraCharacteristics)
.build()
)
When .activation(true) is set on ScanOptions, the camera fragment automatically:
- Scans the receipt
- Maps
ScanResultsto the Activation SDK model - Displays the ads loading screen with promotions
- Shows the receipt summary with matched offers and rewards
- Returns to the host app when the user taps "Continue"
If the Activation SDK is not present at runtime, the .activation(true) flag on ScanOptions is silently ignored and the camera returns results normally.