Overview
This guide explains how to integrate the PageSense Android SDK into a React Native application using the React Native Native Bridge. The bridge enables React Native JavaScript code to invoke Android SDK functions and leverage FullStack experimentation capabilities within the mobile application.
By the end of this guide, your React Native application will be able to call the following PageSense SDK functions directly from JavaScript:
What is the React Native Native Bridge
The React Native Native Bridge is the communication layer between the JavaScript runtime and native Android code. It allows JavaScript to invoke native functions, access platform-specific SDK features, and receive responses from native modules.
Since JavaScript cannot directly interact with Kotlin or Java, the bridge acts as the intermediary. Native methods are implemented in Kotlin and exposed to React Native through the React Native bridge API.
React Native UI
↓
React Native Wrapper (TypeScript/JavaScript)
↓
React Native Native Bridge
↓
PageSenseSDKPackage (Module Registration)
↓
PageSenseSDKModule (Kotlin Native Module)
↓
PageSense Android SDK
React Native applications primarily execute JavaScript, whereas the PageSense Android SDK is implemented in Kotlin. The native module exposes PageSense SDK methods to the React Native JavaScript layer. Once registered with the React Native runtime, the module becomes accessible through React Native's NativeModules API.
Prerequisites
Ensure the following tools are installed before integrating the SDK.
Development Environment
Node.js
npm
Watchman
React Native CLI
Android Development Tools
bash
node -v
npm -v
npx react-native doctor
Installing the PageSense Android SDK
The PageSense Android SDK is distributed through the Zoho Maven Repository.
Step 1 — Add the Zoho Maven Repository
In modern React Native projects (Gradle 7+), add the repository in settings.gradle:
- gradle
- dependencyResolutionManagement {
- repositories {
- google()
- mavenCentral()
- maven {
- url "https://maven.zohodl.com/"
- }
- }
- }
In some project configurations, the repository may also be added in the project-level build.gradle:
- gradle
- repositories {
- google()
- mavenCentral()
- maven {
- url "https://maven.zohodl.com/"
- }
- }
Step 2 — Add the PageSense Android SDK Dependency
Add the dependency to android/app/build.gradle:
- gradle
- dependencies {
- implementation("com.facebook.react:react-android")
- implementation("com.zoho.pagesense:pagesense:1.1.3")
- }
Android React Native Libraries
The following React Native libraries are required and are automatically included as part of the Android build configuration:
The following React Native bridge libraries must be available for implementing native modules:
Creating the Native Bridge Module
The Android native bridge is implemented in three parts: the Kotlin native module, the React Native package, and package registration.
Part 1 — Kotlin Native Module
Create PageSenseSDKModule.kt inside the Android application Java source package:
- /ProjectRoot/android/app/src/main/java/com/sampleapp/PageSenseSDKModule.kt
- kotlin
- package com.sampleapp
- import com.facebook.react.bridge.*
- import com.facebook.react.module.annotations.ReactModule
- import com.zoho.pagesense.android.abtesting.PageSenseClient
- import com.zoho.pagesense.android.abtesting.PageSenseClientBuilder
- import com.zoho.pagesense.android.abtesting.PageSenseSDKOptions
- import com.zoho.pagesense.android.abtesting.data.PageSenseUserContext
- import com.zoho.pagesense.android.network.ProjectSettingsCallBack
- import com.zoho.pagesense.android.logging.LogLevel
- import com.zoho.pagesense.android.abtesting.constants.GoalProperty
- @ReactModule(name = PageSenseSDKModule.NAME)
- class PageSenseSDKModule(private val reactContext: ReactApplicationContext) :
- ReactContextBaseJavaModule(reactContext) {
- companion object {
- const val NAME = "PageSenseSDKModule"
- private const val LOG_TAG = "[PageSenseSDK]"
- }
- private var client: PageSenseClient? = null
- private var isInitialized = false
- override fun getName(): String {
- return NAME
- }
- @ReactMethod
- fun initialiseSDK(
- accountId: String,
- sdkKey: String,
- projectName: String,
- promise: Promise
- ) {
- if (isInitialized) {
- println("$LOG_TAG SDK already initialized")
- promise.resolve(true)
- return
- }
- if (!validateString(accountId, "accountId") ||
- !validateString(sdkKey, "sdkKey") ||
- !validateString(projectName, "projectName")) {
- promise.resolve(false)
- return
- }
- val pageSenseSDKOptions = PageSenseSDKOptions()
- pageSenseSDKOptions.logLevel = LogLevel.DEBUG
- pageSenseSDKOptions.pollingInterval = 5
- PageSenseClientBuilder.createNewPageSenseClient(
- accountId,
- sdkKey,
- projectName,
- pageSenseSDKOptions,
- object : ProjectSettingsCallBack {
- override fun onFailure(message: String?, code: Int?) {
- promise.resolve(false)
- }
- override fun onSuccess(data: PageSenseClient?) {
- if (data != null) {
- client = data
- isInitialized = true
- promise.resolve(true)
- } else {
- promise.resolve(false)
- }
- }
- }
- )
- }
- @ReactMethod
- fun activateExperimentByContext(
- experimentName: String,
- userContextMap: ReadableMap,
- promise: Promise
- ) {
- val clientInstance = client
- if (clientInstance == null) { promise.resolve(null); return }
- if (!validateString(experimentName, "experimentName")) { promise.resolve(null); return }
- val userContext = validateAndBuildUserContext(userContextMap)
- if (userContext == null) { promise.resolve(null); return }
- try {
- promise.resolve(clientInstance.activateExperiment(experimentName, userContext))
- } catch (e: Exception) { promise.resolve(null) }
- }
- @ReactMethod
- fun getVariationNameByContext(
- experimentName: String,
- userContextMap: ReadableMap,
- promise: Promise
- ) {
- val clientInstance = client
- if (clientInstance == null) { promise.resolve(null); return }
- if (!validateString(experimentName, "experimentName")) { promise.resolve(null); return }
- val userContext = validateAndBuildUserContext(userContextMap)
- if (userContext == null) { promise.resolve(null); return }
- try {
- promise.resolve(clientInstance.getVariationName(experimentName, userContext))
- } catch (e: Exception) { promise.resolve(null) }
- }
- @ReactMethod
- fun trackGoalByContext(goalName: String, userContextMap: ReadableMap) {
- val clientInstance = client ?: return
- if (!validateString(goalName, "goalName")) return
- val userContext = validateAndBuildUserContext(userContextMap) ?: return
- try { clientInstance.trackGoal(goalName, userContext) } catch (e: Exception) { }
- }
- @ReactMethod
- fun trackGoalWithPropertiesByContext(
- goalName: String,
- userContextMap: ReadableMap,
- goalPropertiesMap: ReadableMap
- ) {
- val clientInstance = client ?: return
- if (!validateString(goalName, "goalName")) return
- val userContext = validateAndBuildUserContext(userContextMap) ?: return
- val goalProperties = validateAndConvertGoalPropertiesNonEmpty(goalPropertiesMap) ?: return
- try { clientInstance.trackGoal(goalName, userContext, goalProperties) } catch (e: Exception) { }
- }
- private fun validateString(param: String, paramName: String): Boolean {
- if (param.trim().isEmpty()) {
- println("$LOG_TAG Invalid $paramName: must be a non-empty string")
- return false
- }
- return true
- }
- private fun validateAndBuildUserContext(userContextMap: ReadableMap): PageSenseUserContext? {
- if (!userContextMap.hasKey("userId")) return null
- val userId = userContextMap.getString("userId")
- if (userId.isNullOrBlank()) return null
- val userAttributes = HashMap<String, String>()
- if (userContextMap.hasKey("userAttributes") && !userContextMap.isNull("userAttributes")) {
- val rawAttrs = userContextMap.getMap("userAttributes") ?: return null
- val iterator = rawAttrs.keySetIterator()
- while (iterator.hasNextKey()) {
- val key = iterator.nextKey()
- if (key.trim().isEmpty()) return null
- if (rawAttrs.getType(key) != ReadableType.String) return null
- userAttributes[key] = rawAttrs.getString(key) ?: return null
- }
- }
- return PageSenseUserContext(userId, userAttributes)
- }
- private fun validateAndConvertGoalPropertiesNonEmpty(
- goalPropertiesMap: ReadableMap
- ): HashMap<GoalProperty, String>? {
- val result = HashMap<GoalProperty, String>()
- val iterator = goalPropertiesMap.keySetIterator()
- if (!iterator.hasNextKey()) return null
- val freshIterator = goalPropertiesMap.keySetIterator()
- while (freshIterator.hasNextKey()) {
- val key = freshIterator.nextKey()
- if (key.trim().isEmpty()) return null
- if (goalPropertiesMap.getType(key) != ReadableType.String) return null
- val value = goalPropertiesMap.getString(key) ?: return null
- val goalProperty = when (key.lowercase()) {
- GoalProperty.REVENUE.value -> GoalProperty.REVENUE
- else -> return null
- }
- result[goalProperty] = value
- }
- return result
- }
- }
Part 2 — React Native Package
Create PageSenseSDKPackage.kt in the same directory:
- /ProjectRoot/android/app/src/main/java/com/sampleapp/PageSenseSDKPackage.kt
- kotlin
- package com.sampleapp
- import com.facebook.react.ReactPackage
- import com.facebook.react.bridge.NativeModule
- import com.facebook.react.bridge.ReactApplicationContext
- import com.facebook.react.uimanager.ViewManager
- class PageSenseSDKPackage : ReactPackage {
- override fun createNativeModules(
- reactContext: ReactApplicationContext
- ): List<NativeModule> {
- return listOf(PageSenseSDKModule(reactContext))
- }
- override fun createViewManagers(
- reactContext: ReactApplicationContext
- ): List<ViewManager<*, *>> {
- return emptyList()
- }
- }
- Part 3 — Package Registration
- Register the package in MainApplication.kt:
- kotlin
- package com.sampleapp
- import android.app.Application
- import com.facebook.react.PackageList
- import com.facebook.react.ReactApplication
- import com.facebook.react.ReactHost
- import com.facebook.react.ReactNativeApplicationEntryPoint.loadReactNative
- import com.facebook.react.defaults.DefaultReactHost.getDefaultReactHost
- import com.sampleapp.PageSenseSDKPackage
- class MainApplication : Application(), ReactApplication {
- override val reactHost: ReactHost by lazy {
- getDefaultReactHost(
- context = applicationContext,
- packageList =
- PackageList(this).packages.apply {
- add(PageSenseSDKPackage())
- },
- )
- }
- override fun onCreate() {
- super.onCreate()
- loadReactNative(this)
- }
- }
GoalProperty Mapping
On Android, the PageSense SDK accepts goal properties as HashMap<GoalProperty, String>, where GoalProperty is a constant class defined in com.zoho.pagesense.android.abtesting.constants. The React Native bridge receives goal properties from JavaScript as a ReadableMap of string key/value pairs and maps each string key to its corresponding GoalProperty constant.
JavaScript Key | String Value | Android GoalProperty Constant |
GoalProperty.REVENUE | "revenue" | GoalProperty.REVENUE |
Best Practices
Initialise the SDK only once per application lifecycle for a project.
Initialise the SDK when the application starts so experiments are available immediately.
Do not block UI rendering if SDK initialisation fails. Always use a fallback UI.
Keep the PageSenseClient instance alive for the entire application session.
Use a consistent user ID across the application session to ensure proper experiment tracking.
Build the PageSenseUserContext once per user session and reuse it across SDK calls.
When tracking Revenue Goals, always pass the revenue value in cents as a string. Multiply the dollar amount by 100 and round to the nearest integer.
Use the GoalProperty constants exported by the JavaScript wrapper instead of hard-coded strings.
Always handle null variations by falling back to a safe default UI.
Trigger goals after the specific user action or event completes successfully.
For all new integrations, use the context-based SDK functions (activateExperimentByContext, getVariationNameByContext, trackGoalByContext).
We’ve
designed this documentation to guide you every step of the way. If you
need further assistance or have any questions, don’t hesitate to contact
us at
support@zohopagesense.com - we’re always here to help!