Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
ca41f6b
feat: add location tracking module support
Shahroz16 Mar 9, 2026
060ecad
chore: make location module an optional dependency
Shahroz16 Mar 9, 2026
9d6053d
refactor: use build flags instead of canImport and reflection for opt…
Shahroz16 Mar 9, 2026
9361227
fix: resolve simplify review findings for location module
Shahroz16 Mar 9, 2026
9bc1de6
style: fix pre-existing prettier formatting errors
Shahroz16 Mar 9, 2026
d3c40d3
fix: read gradle property value instead of just checking existence
Shahroz16 Mar 9, 2026
1df0a4b
fix: always register location module info to prevent import-time crash
Shahroz16 Mar 9, 2026
3f464a3
android version bump
Shahroz16 Mar 9, 2026
c93c4b6
fix: use TurboModuleRegistry.get for optional location module
Shahroz16 Mar 9, 2026
28f561b
fix: add R8 consumer rules for optional location compileOnly dependency
Shahroz16 Mar 10, 2026
c2c52a2
Fix wrapper layer for iOS
mahmoud-elmorabea Mar 10, 2026
b6ff4b2
Merge branch 'feat/location-module' of github.com:customerio/customer…
mahmoud-elmorabea Mar 10, 2026
b434f1d
Update public API baseline
mahmoud-elmorabea Mar 10, 2026
b7404d6
fix: log clear warning when location module is not enabled
Shahroz16 Mar 10, 2026
04b0570
Merge branch 'feat/location-module' of github.com:customerio/customer…
Shahroz16 Mar 10, 2026
d0531fd
style: fix prettier formatting in customerio-inapp.ts
Shahroz16 Mar 10, 2026
ca2bed7
Review comments
mahmoud-elmorabea Mar 10, 2026
3bc2d88
Bump ios sdk version
mahmoud-elmorabea Mar 11, 2026
2256101
chore: Build test screen for location module (#571)
mahmoud-elmorabea Mar 11, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,22 @@ def getExtOrIntegerDefault(name) {
return rootProject.ext.has(name) ? rootProject.ext.get(name) : (project.properties['customerio.reactnative.' + name]).toInteger()
}

ext.cioLocationEnabled = rootProject.findProperty("customerio_location_enabled")?.toBoolean() ?: false

android {
namespace = 'io.customer.reactnative.sdk'
compileSdkVersion getExtOrIntegerDefault('compileSdkVersion')
defaultConfig {
minSdkVersion getExtOrIntegerDefault('minSdkVersion')
targetSdkVersion getExtOrIntegerDefault('targetSdkVersion')
buildConfigField "boolean", "CIO_LOCATION_ENABLED", "${project.cioLocationEnabled}"
consumerProguardFiles 'consumer-rules.pro'
}

buildFeatures {
buildConfig true
}

buildTypes {
release {
minifyEnabled false
Expand Down
7 changes: 7 additions & 0 deletions android/cio-core.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,11 @@ dependencies {
api "io.customer.android:datapipelines:$cioAndroidSDKVersion"
api "io.customer.android:messaging-push-fcm:$cioAndroidSDKVersion"
api "io.customer.android:messaging-in-app:$cioAndroidSDKVersion"
// Location module is optional - customers enable it by adding
// customerio_location_enabled=true in their gradle.properties
if (project.cioLocationEnabled) {
implementation "io.customer.android:location:$cioAndroidSDKVersion"
} else {
compileOnly "io.customer.android:location:$cioAndroidSDKVersion"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a nice addition but it probably won't work properly since the code still references location classes. Ideally to solve this, we would need to completely remove location class references when the build flag is disabled to fully eliminate these challenges. So I think this is a bigger problem to solve. Unless we really need this for location right now, we should probably address it in a separate PR.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. Kotlin compiler eliminates dead code at compile time; When BuildConfig.CIO_LOCATION_ENABLED = false, the compiler strips the entire if block from bytecode. I decompiled the AAR and confirmed:
    NativeCustomerIOModule has zero references to any io.customer.location.* class in its constant pool.
    The getModule() path compiles down to just aconst_null (return null).
  2. Manifest permissions don't leak — Built the RN sample app with location disabled and checked the
    merged manifest. ACCESS_COARSE_LOCATION is completely absent. compileOnly means the location AAR's manifest is never fed to the manifest merger.
  3. App runs without crashes — Installed and launched on an emulator with location disabled. No
    NoClassDefFoundError or ClassNotFoundException.

The compileOnly + BuildConfig approach serves our exact use case: customers who don't enable
location don't get the permission in their app.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the detailed response. Can you check why the sample app build is failing during minifyReleaseWithR8 then?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like when R8 runs on a release build, it scans all classes and fails because the compileOnly location classes aren't on the runtime classpath. Adding rule would solve that.

}
}
3 changes: 3 additions & 0 deletions android/consumer-rules.pro
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Location module is an optional compileOnly dependency.
# When not enabled, R8 must not fail on missing location classes.
-dontwarn io.customer.location.**
2 changes: 1 addition & 1 deletion android/gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@ customerio.reactnative.kotlinVersion=2.1.20
customerio.reactnative.compileSdkVersion=36
customerio.reactnative.targetSdkVersion=36
customerio.reactnative.minSdkVersion=21
customerio.reactnative.cioSDKVersionAndroid=4.16.0
customerio.reactnative.cioSDKVersionAndroid=4.17.0
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.module.model.ReactModuleInfo
import com.facebook.react.module.model.ReactModuleInfoProvider
import com.facebook.react.uimanager.ViewManager
import io.customer.reactnative.sdk.location.NativeLocationModule
import io.customer.reactnative.sdk.logging.NativeCustomerIOLoggingModule
import io.customer.reactnative.sdk.messaginginapp.InlineInAppMessageViewManager
import io.customer.reactnative.sdk.messaginginapp.NativeMessagingInAppModule
Expand All @@ -32,6 +33,9 @@ class CustomerIOReactNativePackage : BaseReactPackage() {
InlineInAppMessageViewManager.NAME -> InlineInAppMessageViewManager()
NativeCustomerIOLoggingModule.NAME -> NativeCustomerIOLoggingModule(reactContext)
NativeCustomerIOModule.NAME -> NativeCustomerIOModule(reactContext = reactContext)
NativeLocationModule.NAME -> if (BuildConfig.CIO_LOCATION_ENABLED) {
NativeLocationModule(reactContext)
} else null
NativeMessagingInAppModule.NAME -> NativeMessagingInAppModule(reactContext)
NativeMessagingPushModule.NAME -> NativeMessagingPushModule(reactContext)
else -> assertNotNull<NativeModule>(value = null) { "Unknown module name: $name" }
Expand Down Expand Up @@ -61,10 +65,13 @@ class CustomerIOReactNativePackage : BaseReactPackage() {
override fun getReactModuleInfoProvider(): ReactModuleInfoProvider {
// List of all Fabric ViewManagers and TurboModules registered in this package.
// Used by React Native to identify and instantiate the modules.
// Location module is always registered so TurboModuleRegistry.getEnforcing()
// doesn't crash at import time. getModule() returns null when disabled.
val moduleNames: List<String> = listOf(
InlineInAppMessageViewManager.NAME,
NativeCustomerIOLoggingModule.NAME,
NativeCustomerIOModule.NAME,
NativeLocationModule.NAME,
NativeMessagingInAppModule.NAME,
NativeMessagingPushModule.NAME,
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import io.customer.datapipelines.config.ScreenView
import io.customer.reactnative.sdk.constant.Keys
import io.customer.reactnative.sdk.extension.getTypedValue
import io.customer.reactnative.sdk.extension.toMap
import io.customer.reactnative.sdk.location.NativeLocationModule
import io.customer.reactnative.sdk.messaginginapp.NativeMessagingInAppModule
import io.customer.reactnative.sdk.messagingpush.NativeMessagingPushModule
import io.customer.reactnative.sdk.util.assertNotNull
Expand Down Expand Up @@ -99,6 +100,15 @@ class NativeCustomerIOModule(
region = region
)
}
// Configure location module if enabled via gradle property
if (BuildConfig.CIO_LOCATION_ENABLED) {
packageConfig.getTypedValue<Map<String, Any>>(key = "location")?.let { locationConfig ->
NativeLocationModule.addNativeModuleFromConfig(
builder = this,
config = locationConfig
)
}
}
}.build()

logger.info("Customer.io instance initialized successfully from app")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package io.customer.reactnative.sdk.location

import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.module.annotations.ReactModule
import io.customer.location.LocationModuleConfig
import io.customer.location.LocationTrackingMode
import io.customer.location.ModuleLocation
import io.customer.reactnative.sdk.NativeCustomerIOLocationSpec
import io.customer.reactnative.sdk.extension.getTypedValue
import io.customer.sdk.CustomerIOBuilder
import io.customer.sdk.core.di.SDKComponent
import io.customer.sdk.core.util.Logger

/**
* React Native module implementation for Customer.io Location Native SDK
* using TurboModules with new architecture.
*/
@ReactModule(name = NativeLocationModule.NAME)
class NativeLocationModule(
private val reactContext: ReactApplicationContext,
) : NativeCustomerIOLocationSpec(reactContext) {
private val logger: Logger
get() = SDKComponent.logger

private fun getLocationServices() = runCatching {
ModuleLocation.instance().locationServices
}.onFailure {
logger.error("Location module is not initialized. Ensure location config is provided during SDK initialization.")
}.getOrNull()

override fun setLastKnownLocation(latitude: Double, longitude: Double) {
getLocationServices()?.setLastKnownLocation(latitude, longitude)
}

override fun requestLocationUpdate() {
getLocationServices()?.requestLocationUpdate()
}

companion object {
const val NAME = "NativeCustomerIOLocation"

/**
* Adds location module to native Android SDK based on the configuration provided by
* customer app.
*
* @param builder instance of CustomerIOBuilder to add location module.
* @param config configuration provided by customer app for location module.
*/
internal fun addNativeModuleFromConfig(
builder: CustomerIOBuilder,
config: Map<String, Any>
) {
val trackingModeValue = config.getTypedValue<String>("trackingMode")
val trackingMode = trackingModeValue?.let { value ->
runCatching { enumValueOf<LocationTrackingMode>(value) }.getOrNull()
} ?: LocationTrackingMode.MANUAL

val module = ModuleLocation(
LocationModuleConfig.Builder()
.setLocationTrackingMode(trackingMode)
.build()
)
builder.addCustomerIOModule(module)
}
}
}
24 changes: 22 additions & 2 deletions api-extractor-output/customerio-reactnative.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,18 @@ export type CioConfig = {
pushClickBehavior?: PushClickBehaviorAndroid;
};
};
location?: {
trackingMode?: CioLocationTrackingMode;
};
};

// @public
export enum CioLocationTrackingMode {
Manual = "MANUAL",
Off = "OFF",
OnAppStart = "ON_APP_START"
}

// @public
export enum CioLogLevel {
Debug = "debug",
Expand Down Expand Up @@ -74,20 +84,22 @@ export type CustomAttributes = Record<string, any>;
export class CustomerIO {
static readonly clearIdentify: () => Promise<any>;
static readonly deleteDeviceToken: () => Promise<void>;
static readonly identify: ({ userId, traits, }?: IdentifyParams) => Promise<any>;
static readonly identify: (input?: IdentifyParams) => Promise<any>;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did we really change this? 🤔

// (undocumented)
static readonly inAppMessaging: CustomerIOInAppMessaging;
static readonly initialize: (config: CioConfig) => Promise<void>;
// @deprecated
static readonly isInitialized: () => boolean;
// (undocumented)
static readonly location: CustomerIOLocation;
// (undocumented)
static readonly pushMessaging: CustomerIOPushMessaging;
static readonly registerDeviceToken: (token: string) => Promise<void>;
static readonly screen: (title: string, properties?: Record<string, any>) => Promise<any>;
static readonly setDeviceAttributes: (attributes: Record<string, any>) => Promise<any>;
static readonly setProfileAttributes: (attributes: Record<string, any>) => Promise<any>;
static readonly track: (name: string, properties?: Record<string, any>) => Promise<any>;
static readonly trackMetric: ({ deliveryID, deviceToken, event, }: {
static readonly trackMetric: (input: {
deliveryID: string;
deviceToken: string;
event: MetricEvent;
Expand All @@ -104,6 +116,14 @@ export class CustomerIOInAppMessaging implements NativeInAppSpec {
registerEventsListener(listener: (event: InAppMessageEvent) => void): EventSubscription;
}

// Warning: (ae-forgotten-export) The symbol "NativeLocationSpec" needs to be exported by the entry point index.d.ts
//
// @public (undocumented)
export class CustomerIOLocation implements NativeLocationSpec {
requestLocationUpdate(): void;
setLastKnownLocation(latitude: number, longitude: number): void;
}

// Warning: (ae-forgotten-export) The symbol "NativePushSpec" needs to be exported by the entry point index.d.ts
//
// @public (undocumented)
Expand Down
8 changes: 8 additions & 0 deletions customerio-reactnative.podspec
Original file line number Diff line number Diff line change
Expand Up @@ -49,4 +49,12 @@ Pod::Spec.new do |s|
# Customer.io Firebase Wrapper - provides Firebase integration
ss.dependency "CioFirebaseWrapper", package["cioiOSFirebaseWrapperSdkVersion"]
end

# Location module is optional - customers must opt in by adding this subspec.
s.subspec "location" do |ss|
ss.dependency "CustomerIO/Location", package["cioNativeiOSSdkVersion"]
ss.pod_target_xcconfig = {
'OTHER_SWIFT_FLAGS' => '$(inherited) -DCIO_LOCATION_ENABLED'
}
end
end
Loading
Loading