Skip to content

feat: Location enrichment#570

Merged
mahmoud-elmorabea merged 19 commits intomainfrom
feat/location-module
Mar 11, 2026
Merged

feat: Location enrichment#570
mahmoud-elmorabea merged 19 commits intomainfrom
feat/location-module

Conversation

@Shahroz16
Copy link
Contributor

@Shahroz16 Shahroz16 commented Mar 9, 2026

Summary

Adds support for the Customer.io Location module in the React Native SDK.

  • New CustomerIOLocation class with setLastKnownLocation() and requestLocationUpdate() APIs
  • CioLocationTrackingMode enum (Off, Manual, OnAppStart) for configuration
  • Location config support in CioConfig via location.trackingMode
  • Full TurboModule implementation (Android Kotlin + iOS Swift/ObjC++ bridge)

Optional dependency

Location is fully opt-in. Customers who don't need it pay no cost:

  • iOS: Add the location subspec in Podfile (pod 'customerio-reactnative/location'). Uses a Swift compiler flag (-DCIO_LOCATION_ENABLED) set by the subspec — deterministic, no #if canImport ambiguity.
  • Android: Add customerio_location_enabled=true to gradle.properties. Controls a BuildConfig flag that gates all location code at build time. When disabled, R8 eliminates dead code entirely. No reflection.

Note

Medium Risk
Adds a new optional native Location module and build-time gating on both Android and iOS, touching SDK initialization and dependency configuration. Risk is mainly around build configuration/packaging (R8, BuildConfig flags, CocoaPods subspecs) and ensuring apps not opting in don’t crash or bloat.

Overview
Adds opt-in Location enrichment support to the React Native SDK: CioConfig gains location.trackingMode plus a new CustomerIO.location API (setLastKnownLocation, requestLocationUpdate) and CioLocationTrackingMode enum.

Implements a new Location TurboModule bridge (Android NativeLocationModule + iOS NativeLocation with ObjC++ TurboModule wrapper) and wires it into SDK initialization only when enabled.

Introduces platform-specific opt-in wiring: Android uses customerio_location_enabled -> BuildConfig.CIO_LOCATION_ENABLED to conditionally include io.customer.android:location (with consumer-rules.pro to avoid R8 missing-class failures), and iOS adds a location subspec that pulls CustomerIO/Location and sets -DCIO_LOCATION_ENABLED. The example app is updated to enable location and request permissions, and Android SDK dependency is bumped to 4.17.0.

Written by Cursor Bugbot for commit 2256101. This will update automatically on new commits. Configure here.

Shahroz16 and others added 5 commits March 9, 2026 01:21
Integrate native Android and iOS location modules into the React Native
SDK. Adds TurboModule spec, native bridges, TypeScript API, config types,
and dependency declarations for the location feature.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Location is now opt-in: iOS uses a CocoaPods subspec with #if canImport
guards, Android uses compileOnly with reflection-based class loading.
Customers who don't need location no longer pull in the dependency.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ional location

iOS: Replace #if canImport(CioLocation) with #if CIO_LOCATION_ENABLED
compiler flag set via podspec subspec's pod_target_xcconfig. This is
deterministic regardless of CocoaPods linking mode.

Android: Replace reflection-based class loading with BuildConfig flag
controlled by customerio_location_enabled gradle property. Dependency is
conditionally included as implementation or compileOnly based on the
property. No reflection needed, no ProGuard concerns.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Fix critical bug: NSClassFromString used wrong class name
  (@"NativeLocation" instead of @"NativeCustomerIOLocation"), causing
  iOS location to silently no-op even when enabled
- Gate NativeLocationModule registration in getReactModuleInfoProvider
  on BuildConfig.CIO_LOCATION_ENABLED to avoid unnecessary module info
- Consolidate duplicate cioLocationEnabled definition into ext property
- Add NativeLocationSpec implements clause for compile-time type safety

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@Shahroz16 Shahroz16 requested a review from a team as a code owner March 9, 2026 06:18
@github-actions
Copy link
Contributor

github-actions bot commented Mar 9, 2026

Sample app builds 📱

Below you will find the list of the latest versions of the sample apps. It's recommended to always download the latest builds of the sample apps to accurately test the pull request.


  • iOS APN: 570.19.0 (29553867)

Shahroz16 and others added 2 commits March 9, 2026 02:25
rootProject.hasProperty() returns true even for
customerio_location_enabled=false. Use findProperty().toBoolean()
to correctly respect the property's actual value.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
TurboModuleRegistry.getEnforcing() runs at import time and throws if
the module isn't in the info provider. The module must always be
registered; getModule() already returns null when location is disabled.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
if (project.cioLocationEnabled) {
implementation "io.customer.android:location:$cioAndroidSDKVersion"
} else {
compileOnly "io.customer.android:location:$cioAndroidSDKVersion"
Copy link
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
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
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
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.


- (void)setLastKnownLocation:(double)latitude
longitude:(double)longitude {
if (!_swiftBridge) return;
Copy link
Contributor

Choose a reason for hiding this comment

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

There is a helper assertBridgeAvailable method for this in case you want to utilize it

Copy link
Contributor Author

Choose a reason for hiding this comment

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

assertBridgeAvailable calls RCT_ASSERT_NOT_NIL which crashes when the bridge is nil. That's correct
for push/in-app since they're always required — if the bridge is missing, something is broken.

For location, the bridge being nil is expected when the customer hasn't installed the location
subspec. Using assertBridgeAvailable would crash every app that doesn't opt into location. The if
(!_swiftBridge) return; pattern is the right one here — it silently no-ops, which is the intended
behavior for an optional module.

Copy link
Contributor

Choose a reason for hiding this comment

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

I'm okay with the current implementation. But just for clarity, RCT_ASSERT_NOT_NIL uses NSAssert to help with debugging. And these checks are generally in places where the code should never be called in the first place. If it is called, not having the bridge and failing silently could lead to unexpected behavior, no?


let sdkConfigBuilder = try SDKConfigBuilder.create(from: config)

#if CIO_LOCATION_ENABLED
Copy link
Contributor

Choose a reason for hiding this comment

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

How do customers set this? In their Podfile?

Shahroz16 and others added 2 commits March 9, 2026 18:08
getEnforcing throws at import time when the native module returns null,
crashing the SDK for every user who hasn't enabled location. Switch to
get() and silently no-op when the module is unavailable.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Without this, R8 minification fails on release builds when location is
disabled because NativeLocationModule class references io.customer.location.*
classes that are not on the runtime classpath with compileOnly.

Also bumps iOS native SDK to 4.3.0.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link

@cursor cursor bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.

Copy link
Contributor

@mrehan27 mrehan27 left a comment

Choose a reason for hiding this comment

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

Looks good overall. The only concern is that we probably shouldn't push all module initialization onto main thread, we can continue discussing this on slack.

// Location is an optional module — NativeModule may be null when location is not enabled.
// Methods silently no-op when the native module is unavailable.
const withNativeModule = (fn: (native: CodegenSpec) => void): void => {
if (NativeModule) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Shouldn't we log a warning here instead? These methods shouldn't be called if location module is disabled, and if they are called, location should probably be enabled.

Copy link
Contributor

Choose a reason for hiding this comment

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

They shouldn't be called if the module isn't added but they will still be part of the API, so they can be called, that's why we are protecting against it

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
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? 🤔

Shahroz16 and others added 6 commits March 10, 2026 11:41
When a developer calls location APIs without enabling the native module,
the calls silently no-op. This adds a one-time dev warning with
actionable setup instructions for both iOS and Android.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@mahmoud-elmorabea mahmoud-elmorabea self-assigned this Mar 11, 2026
@mahmoud-elmorabea mahmoud-elmorabea merged commit b225608 into main Mar 11, 2026
16 checks passed
@mahmoud-elmorabea mahmoud-elmorabea deleted the feat/location-module branch March 11, 2026 12:52
github-actions bot pushed a commit that referenced this pull request Mar 11, 2026
## [6.3.0](6.2.0...6.3.0) (2026-03-11)

### Features

* Location enrichment ([#570](#570)) ([b225608](b225608))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants