PageSense Android SDK — React Native Bridge Integration Guide

PageSense Android SDK — React Native Bridge Integration Guide

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:
  • initialiseSDK
  • activateExperimentByContext
  • getVariationNameByContext
  • trackGoalByContext

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.


IdeaReact 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
  • Android Studio
  • Android SDK
  • Gradle
  • Java Development Kit (JDK)
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:
  1. gradle
  2. dependencyResolutionManagement {
  3.     repositories {
  4.         google()
  5.         mavenCentral()
  6.         maven {
  7.             url "https://maven.zohodl.com/"
  8.         }
  9.     }
  10. }
In some project configurations, the repository may also be added in the project-level build.gradle:
  1. gradle
  2. repositories {
  3.     google()
  4.     mavenCentral()
  5.     maven {
  6.         url "https://maven.zohodl.com/"
  7.     }
  8. }
Step 2 — Add the PageSense Android SDK Dependency  

Add the dependency to android/app/build.gradle:
  1. gradle
  2. dependencies {
  3.     implementation("com.facebook.react:react-android")
  4.     implementation("com.zoho.pagesense:pagesense:1.1.3")
  5. }

Android React Native Libraries  

The following React Native libraries are required and are automatically included as part of the Android build configuration:
  • react-android — Core React Native runtime library for Android.
  • hermes-android (optional) — JavaScript engine for improved performance.
The following React Native bridge libraries must be available for implementing native modules:
  • ReactContextBaseJavaModule
  • ReactMethod
  • Promise
  • ReadableMap
  • ReactPackage
  • ReactApplicationContext

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:
  1. /ProjectRoot/android/app/src/main/java/com/sampleapp/PageSenseSDKModule.kt
  2. kotlin
  3. package com.sampleapp
  4. import com.facebook.react.bridge.*
  5. import com.facebook.react.module.annotations.ReactModule
  6. import com.zoho.pagesense.android.abtesting.PageSenseClient
  7. import com.zoho.pagesense.android.abtesting.PageSenseClientBuilder
  8. import com.zoho.pagesense.android.abtesting.PageSenseSDKOptions
  9. import com.zoho.pagesense.android.abtesting.data.PageSenseUserContext
  10. import com.zoho.pagesense.android.network.ProjectSettingsCallBack
  11. import com.zoho.pagesense.android.logging.LogLevel
  12. import com.zoho.pagesense.android.abtesting.constants.GoalProperty
  13. @ReactModule(name = PageSenseSDKModule.NAME)
  14. class PageSenseSDKModule(private val reactContext: ReactApplicationContext) :
  15.     ReactContextBaseJavaModule(reactContext) {
  16.     companion object {
  17.         const val NAME = "PageSenseSDKModule"
  18.         private const val LOG_TAG = "[PageSenseSDK]"
  19.     }
  20.     private var client: PageSenseClient? = null
  21.     private var isInitialized = false
  22.     override fun getName(): String {
  23.         return NAME
  24.     }
  25.     @ReactMethod
  26.     fun initialiseSDK(
  27.         accountId: String,
  28.         sdkKey: String,
  29.         projectName: String,
  30.         promise: Promise
  31.     ) {
  32.         if (isInitialized) {
  33.             println("$LOG_TAG SDK already initialized")
  34.             promise.resolve(true)
  35.             return
  36.         }
  37.         if (!validateString(accountId, "accountId") ||
  38.             !validateString(sdkKey, "sdkKey") ||
  39.             !validateString(projectName, "projectName")) {
  40.             promise.resolve(false)
  41.             return
  42.         }
  43.         val pageSenseSDKOptions = PageSenseSDKOptions()
  44.         pageSenseSDKOptions.logLevel = LogLevel.DEBUG
  45.         pageSenseSDKOptions.pollingInterval = 5
  46.         PageSenseClientBuilder.createNewPageSenseClient(
  47.             accountId,
  48.             sdkKey,
  49.             projectName,
  50.             pageSenseSDKOptions,
  51.             object : ProjectSettingsCallBack {
  52.                 override fun onFailure(message: String?, code: Int?) {
  53.                     promise.resolve(false)
  54.                 }
  55.                 override fun onSuccess(data: PageSenseClient?) {
  56.                     if (data != null) {
  57.                         client = data
  58.                         isInitialized = true
  59.                         promise.resolve(true)
  60.                     } else {
  61.                         promise.resolve(false)
  62.                     }
  63.                 }
  64.             }
  65.         )
  66.     }
  67.     @ReactMethod
  68.     fun activateExperimentByContext(
  69.         experimentName: String,
  70.         userContextMap: ReadableMap,
  71.         promise: Promise
  72.     ) {
  73.         val clientInstance = client
  74.         if (clientInstance == null) { promise.resolve(null); return }
  75.         if (!validateString(experimentName, "experimentName")) { promise.resolve(null); return }
  76.         val userContext = validateAndBuildUserContext(userContextMap)
  77.         if (userContext == null) { promise.resolve(null); return }
  78.         try {
  79.             promise.resolve(clientInstance.activateExperiment(experimentName, userContext))
  80.         } catch (e: Exception) { promise.resolve(null) }
  81.     }
  82.     @ReactMethod
  83.     fun getVariationNameByContext(
  84.         experimentName: String,
  85.         userContextMap: ReadableMap,
  86.         promise: Promise
  87.     ) {
  88.         val clientInstance = client
  89.         if (clientInstance == null) { promise.resolve(null); return }
  90.         if (!validateString(experimentName, "experimentName")) { promise.resolve(null); return }
  91.         val userContext = validateAndBuildUserContext(userContextMap)
  92.         if (userContext == null) { promise.resolve(null); return }
  93.         try {
  94.             promise.resolve(clientInstance.getVariationName(experimentName, userContext))
  95.         } catch (e: Exception) { promise.resolve(null) }
  96.     }
  97.     @ReactMethod
  98.     fun trackGoalByContext(goalName: String, userContextMap: ReadableMap) {
  99.         val clientInstance = client ?: return
  100.         if (!validateString(goalName, "goalName")) return
  101.         val userContext = validateAndBuildUserContext(userContextMap) ?: return
  102.         try { clientInstance.trackGoal(goalName, userContext) } catch (e: Exception) { }
  103.     }
  104.     @ReactMethod
  105.     fun trackGoalWithPropertiesByContext(
  106.         goalName: String,
  107.         userContextMap: ReadableMap,
  108.         goalPropertiesMap: ReadableMap
  109.     ) {
  110.         val clientInstance = client ?: return
  111.         if (!validateString(goalName, "goalName")) return
  112.         val userContext = validateAndBuildUserContext(userContextMap) ?: return
  113.         val goalProperties = validateAndConvertGoalPropertiesNonEmpty(goalPropertiesMap) ?: return
  114.         try { clientInstance.trackGoal(goalName, userContext, goalProperties) } catch (e: Exception) { }
  115.     }
  116.     private fun validateString(param: String, paramName: String): Boolean {
  117.         if (param.trim().isEmpty()) {
  118.             println("$LOG_TAG Invalid $paramName: must be a non-empty string")
  119.             return false
  120.         }
  121.         return true
  122.     }
  123.     private fun validateAndBuildUserContext(userContextMap: ReadableMap): PageSenseUserContext? {
  124.         if (!userContextMap.hasKey("userId")) return null
  125.         val userId = userContextMap.getString("userId")
  126.         if (userId.isNullOrBlank()) return null
  127.         val userAttributes = HashMap<String, String>()
  128.         if (userContextMap.hasKey("userAttributes") && !userContextMap.isNull("userAttributes")) {
  129.             val rawAttrs = userContextMap.getMap("userAttributes") ?: return null
  130.             val iterator = rawAttrs.keySetIterator()
  131.             while (iterator.hasNextKey()) {
  132.                 val key = iterator.nextKey()
  133.                 if (key.trim().isEmpty()) return null
  134.                 if (rawAttrs.getType(key) != ReadableType.String) return null
  135.                 userAttributes[key] = rawAttrs.getString(key) ?: return null
  136.             }
  137.         }
  138.         return PageSenseUserContext(userId, userAttributes)
  139.     }
  140.     private fun validateAndConvertGoalPropertiesNonEmpty(
  141.         goalPropertiesMap: ReadableMap
  142.     ): HashMap<GoalProperty, String>? {
  143.         val result = HashMap<GoalProperty, String>()
  144.         val iterator = goalPropertiesMap.keySetIterator()
  145.         if (!iterator.hasNextKey()) return null
  146.         val freshIterator = goalPropertiesMap.keySetIterator()
  147.         while (freshIterator.hasNextKey()) {
  148.             val key = freshIterator.nextKey()
  149.             if (key.trim().isEmpty()) return null
  150.             if (goalPropertiesMap.getType(key) != ReadableType.String) return null
  151.             val value = goalPropertiesMap.getString(key) ?: return null
  152.             val goalProperty = when (key.lowercase()) {
  153.                 GoalProperty.REVENUE.value -> GoalProperty.REVENUE
  154.                 else -> return null
  155.             }
  156.             result[goalProperty] = value
  157.         }
  158.         return result
  159.     }
  160. }
Part 2 — React Native Package  

Create PageSenseSDKPackage.kt in the same directory:
  1. /ProjectRoot/android/app/src/main/java/com/sampleapp/PageSenseSDKPackage.kt
  2. kotlin
  3. package com.sampleapp
  4. import com.facebook.react.ReactPackage
  5. import com.facebook.react.bridge.NativeModule
  6. import com.facebook.react.bridge.ReactApplicationContext
  7. import com.facebook.react.uimanager.ViewManager
  8. class PageSenseSDKPackage : ReactPackage {
  9.     override fun createNativeModules(
  10.         reactContext: ReactApplicationContext
  11.     ): List<NativeModule> {
  12.         return listOf(PageSenseSDKModule(reactContext))
  13.     }
  14.     override fun createViewManagers(
  15.         reactContext: ReactApplicationContext
  16.     ): List<ViewManager<*, *>> {
  17.         return emptyList()
  18.     }
  19. }
  20. Part 3 — Package Registration  
  21. Register the package in MainApplication.kt:
  22. kotlin
  23. package com.sampleapp
  24. import android.app.Application
  25. import com.facebook.react.PackageList
  26. import com.facebook.react.ReactApplication
  27. import com.facebook.react.ReactHost
  28. import com.facebook.react.ReactNativeApplicationEntryPoint.loadReactNative
  29. import com.facebook.react.defaults.DefaultReactHost.getDefaultReactHost
  30. import com.sampleapp.PageSenseSDKPackage
  31. class MainApplication : Application(), ReactApplication {
  32.     override val reactHost: ReactHost by lazy {
  33.         getDefaultReactHost(
  34.             context = applicationContext,
  35.             packageList =
  36.                 PackageList(this).packages.apply {
  37.                     add(PageSenseSDKPackage())
  38.                 },
  39.         )
  40.     }
  41.     override fun onCreate() {
  42.         super.onCreate()
  43.         loadReactNative(this)
  44.     }
  45. }
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!