Skip to main content

Android React Native - Integration Guide

This section covers integrating the Activations SDK into a React Native application on Android. The Activations SDK is a native Android library built with Jetpack Compose. React Native integration requires a native module bridge to launch the offer wall and receive callbacks.

Additional Prerequisites

  • React Native 0.72+
  • Kotlin enabled in your Android project
  • Jetpack Compose dependencies (the Activations SDK bundles its own Compose runtime, but your app's Gradle must support it)

Additional Dependencies

In your React Native project's android/app/build.gradle, add the Compose dependencies alongside the Activations SDK dependencies listed above:

android {
buildFeatures {
compose true
}
composeOptions {
kotlinCompilerExtensionVersion = "1.5.14"
}
}

dependencies {
// Compose dependencies (if not already present)
implementation platform("androidx.compose:compose-bom:2025.01.01")
implementation "androidx.compose.material3:material3"
implementation "androidx.activity:activity-compose:1.10.1"
}

Initialize in Application Class

In your MainApplication.kt, initialize the SDK and configure the ActivationClient. User identity can be set here or later from JavaScript via the native module.

import com.microblink.BlinkReceiptSdk
import com.actualplatform.activation.ActivationClient
import okio.ByteString.Companion.encodeUtf8

class MainApplication : Application(), ReactApplication {

override fun onCreate() {
super.onCreate()

BlinkReceiptSdk.initialize(this, object : InitializeCallback {
override fun onComplete() {
// ActivationClient is automatically configured with deviceId
// Set user identity here or later from JavaScript via the native module
}

override fun onException(throwable: Throwable) {
Log.e("App", "SDK init failed", throwable)
}
})
}

override fun onTerminate() {
BlinkReceiptSdk.terminate()
ActivationClient.instance.close()
super.onTerminate()
}
}

Creating the Native Module

Create a native module that exposes the Activations SDK to JavaScript. This bridge handles user identity, configuration, launching the offer wall, and listening for rewards.

Create ActivationsModule.kt in android/app/src/main/java/com/yourapp/:

package com.yourapp

import android.app.Activity
import android.content.Intent
import com.actualplatform.activation.ActivationClient
import com.actualplatform.activation.networking.HttpEnvironment
import com.actualplatform.activation.networking.TestOptions
import com.facebook.react.bridge.*
import com.facebook.react.modules.core.DeviceEventManagerModule
import kotlinx.coroutines.*
import okio.ByteString.Companion.encodeUtf8

class ActivationsModule(reactContext: ReactApplicationContext) :
ReactContextBaseJavaModule(reactContext) {

private val scope = CoroutineScope(Dispatchers.Main + SupervisorJob())
private var rewardListenerActive = false

companion object {
const val NAME = "ActivationsModule"
const val REQUEST_CODE_ACTIVATIONS = 9001
}

override fun getName(): String = NAME

// --- User Identity ---
@ReactMethod
fun setUserIdentity(email: String?, phone: String?) {
ActivationClient.instance.apply {
hashedEmail = email?.takeIf { it.isNotEmpty() }
?.encodeUtf8()?.sha256()?.hex()
hashedPhone = phone?.takeIf { it.isNotEmpty() }
?.encodeUtf8()?.sha256()?.hex()
}
}

// --- Configuration ---
@ReactMethod
fun configure(config: ReadableMap) {
val client = ActivationClient.instance

if (config.hasKey("testAds")) {
val options = mutableSetOf<TestOptions>()
if (config.getBoolean("testAds")) options.add(TestOptions.Ads)
if (config.hasKey("testMode") && config.getBoolean("testMode")) {
options.add(TestOptions.Test)
}
client.testOptions = options
}

if (config.hasKey("rewardCurrencyName")) {
client.rewardCurrencyName = config.getString("rewardCurrencyName")
?: "Points"
}

if (config.hasKey("rewardPayoutPercentage")) {
client.rewardPayoutPercentage =
config.getDouble("rewardPayoutPercentage")
}
}

// --- Launch Offer Wall ---
@ReactMethod
fun showOffersWall(promise: Promise) {
val activity = currentActivity
if (activity == null) {
promise.reject("NO_ACTIVITY", "No current activity")
return
}

val intent = Intent(activity, ActivationsComposeActivity::class.java)
activity.startActivityForResult(intent, REQUEST_CODE_ACTIVATIONS)
promise.resolve(null)
}

// --- Reward Listener ---
@ReactMethod
fun startRewardListener() {
if (rewardListenerActive) return
rewardListenerActive = true

scope.launch {
ActivationClient.instance.rewards.collect { reward ->
val params = Arguments.createMap().apply {
putDouble("amount", reward.amount.toDouble())
putString("type", reward::class.simpleName ?: "Unknown")
}
sendEvent("onRewardEarned", params)
}
}
}

@ReactMethod
fun stopRewardListener() {
rewardListenerActive = false
scope.coroutineContext.cancelChildren()
}

// --- Event Emitter ---
private fun sendEvent(eventName: String, params: WritableMap) {
reactApplicationContext
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
.emit(eventName, params)
}

@ReactMethod
fun addListener(eventName: String) { /* Required for RN */ }

@ReactMethod
fun removeListeners(count: Int) { /* Required for RN */ }

override fun invalidate() {
scope.cancel()
super.invalidate()
}
}

Creating the Activations Activity

The offer wall requires a Compose-based Activity. Create a lightweight wrapper.

Create ActivationsComposeActivity.kt:

package com.yourapp

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.compose.material3.MaterialTheme
import com.actualplatform.activation.OffersWall
import com.microblink.ScanOptions
import com.microblink.camera.ui.CameraCharacteristics
import com.microblink.camera.ui.CameraRecognizerContract
import com.microblink.camera.ui.CameraRecognizerOptions
import com.microblink.camera.ui.CameraRecognizerResults

class ActivationsComposeActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MaterialTheme {
val launcher = rememberLauncherForActivityResult(
contract = CameraRecognizerContract(),
) { result ->
when (result) {
is CameraRecognizerResults.Success -> {
// Post-scan flow handled via activation(true)
}
is CameraRecognizerResults.Exception -> {
// Handle error
}
CameraRecognizerResults.Cancelled -> {
// User cancelled
}
}
}

OffersWall(
onScanReceipt = {
launcher.launch(
CameraRecognizerOptions.Builder()
.options(ScanOptions.newBuilder().build())
.characteristics(CameraCharacteristics.Builder().build())
.activation(true)
.build()
)
},
onDismiss = { finish() },
)
}
}
}
}

Register this activity in android/app/src/main/AndroidManifest.xml:

<activity
android:name=".ActivationsComposeActivity"
android:screenOrientation="portrait"
android:theme="@style/Theme.AppCompat.Light.NoActionBar" />

Registering the Native Module

Create ActivationsPackage.kt:

package com.yourapp

import com.facebook.react.ReactPackage
import com.facebook.react.bridge.NativeModule
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.uimanager.ViewManager

class ActivationsPackage : ReactPackage {
override fun createNativeModules(
reactContext: ReactApplicationContext
): List<NativeModule> {
return listOf(ActivationsModule(reactContext))
}

override fun createViewManagers(
reactContext: ReactApplicationContext
): List<ViewManager<*, *>> {
return emptyList()
}
}

Register the package in MainApplication:

override fun getPackages(): List<ReactPackage> {
val packages = PackageList(this).packages.toMutableList()
packages.add(ActivationsPackage())
return packages
}

JavaScript Bridge

Create ActivationsModule.ts in your React Native project:

import { NativeModules, NativeEventEmitter, Platform } from 'react-native';

const { ActivationsModule } = NativeModules;

interface ActivationsConfig {
environment?: 'production' | 'staging' | 'development';
testAds?: boolean;
testMode?: boolean;
rewardCurrencyName?: string;
rewardPayoutPercentage?: number;
}

interface RewardEvent {
amount: number;
type: 'ScanFinished' | 'Promotion' | 'Boost';
}

const eventEmitter = new NativeEventEmitter(ActivationsModule);

export const Activations = {
setUserIdentity: (email?: string, phone?: string) => {
ActivationsModule.setUserIdentity(email ?? null, phone ?? null);
},

configure: (config: ActivationsConfig) => {
ActivationsModule.configure(config);
},

showOffersWall: (): Promise<void> => {
return ActivationsModule.showOffersWall();
},

onRewardEarned: (callback: (event: RewardEvent) => void) => {
ActivationsModule.startRewardListener();
const subscription = eventEmitter.addListener('onRewardEarned', callback);
return () => {
subscription.remove();
ActivationsModule.stopRewardListener();
};
},
};

Usage Example

import React, { useEffect, useState } from 'react';
import { SafeAreaView, View, Button, Text, StyleSheet, Alert } from 'react-native';
import { Activations } from './ActivationsModule';

export default function App() {
const [rewards, setRewards] = useState<Array<{ amount: number; type: string }>>([]);
const [total, setTotal] = useState(0);

useEffect(() => {
// One-time setup
Activations.setUserIdentity('user@example.com', '+15551234567');
Activations.configure({
rewardCurrencyName: 'Points',
});

// Start listening for rewards
const unsubscribe = Activations.onRewardEarned((event) => {
setRewards((prev) => [...prev, event]);
setTotal((prev) => prev + event.amount);
});

return unsubscribe;
}, []);

const openPromotions = async () => {
try {
setRewards([]);
setTotal(0);
await Activations.showOffersWall();
} catch (error) {
Alert.alert('Error', String(error));
}
};

return (
<SafeAreaView>
<Text>Activations Demo</Text>
<Button title="Open Promotions" onPress={openPromotions} />
<View>
<Text>Total Rewards: {total.toFixed(2)} Points</Text>
{rewards.map((r, i) => (
<Text key={i}>{r.type}: +{r.amount.toFixed(2)}</Text>
))}
</View>
</SafeAreaView>
);
}

Camera Permission

The BlinkReceipt camera requires android.permission.CAMERA. Request this permission in your React Native code before launching the offer wall:

import { PermissionsAndroid } from 'react-native';

await PermissionsAndroid.request(
PermissionsAndroid.PERMISSIONS.CAMERA
);

React Native Troubleshooting

Compose Version Conflicts

If you see Compose compiler version errors, ensure your kotlinCompilerExtensionVersion matches your Kotlin version. The Activations SDK bundles its own Compose dependencies, which Gradle will resolve against your project's BOM.

Missing ActivationClient at Runtime

If the app crashes with ClassNotFoundException for ActivationClient, ensure:

  • The implementation dependency is in your app-level build.gradle (not compileOnly)
  • The Maven repository is correctly configured
  • Run ./gradlew app:dependencies to verify resolution

Reward Events Not Received

  • Ensure startRewardListener() is called before opening the offer wall
  • Check that the NativeEventEmitter subscription is active
  • Rewards are only emitted during an active scan session (after scanning a receipt)

React Native Testing Checklist

  • BlinkReceipt SDK initialized in MainApplication
  • Activations SDK detected automatically on classpath
  • Native module registered in MainApplication packages
  • ActivationsComposeActivity registered in AndroidManifest.xml
  • At least one user identifier set (hashed email or hashed phone)
  • Currency name and payout percentage configured
  • Reward events received in JavaScript via onRewardEarned
  • Offer Wall presenting correctly via showOffersWall()
  • Receipt scan completing and showing reward summary
  • Boost ads displaying (use testAds: true for test ad units)
  • Webhook endpoint receiving ReceiptProcessed and RewardUpdated events
  • Missed earnings correction flow accessible
  • Camera permission requested before launching offer wall
  • Debug/test options disabled for production build
  • app-ads.txt configured and publicly accessible (see GAM MCM Setup)