-
Notifications
You must be signed in to change notification settings - Fork 16
feat: Location enrichment #570
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 5 commits
ca41f6b
060ecad
9d6053d
9361227
9bc1de6
d3c40d3
1df0a4b
3f464a3
c93c4b6
28f561b
c2c52a2
b6ff4b2
b434f1d
b7404d6
04b0570
d0531fd
ca2bed7
3bc2d88
2256101
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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" | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The compileOnly + BuildConfig approach serves our exact use case: customers who don't enable
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Seems like when |
||
| } | ||
| } | ||
| 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 | ||
|
cursor[bot] marked this conversation as resolved.
|
||
|
|
||
| val module = ModuleLocation( | ||
| LocationModuleConfig.Builder() | ||
| .setLocationTrackingMode(trackingMode) | ||
| .build() | ||
| ) | ||
| builder.addCustomerIOModule(module) | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,6 +2,9 @@ import CioAnalytics | |
| import CioDataPipelines | ||
| import CioInternalCommon | ||
| import CioMessagingInApp | ||
| #if CIO_LOCATION_ENABLED | ||
| import CioLocation | ||
| #endif | ||
|
|
||
| @objc(NativeCustomerIO) | ||
| public class NativeCustomerIO: NSObject { | ||
|
|
@@ -47,6 +50,24 @@ public class NativeCustomerIO: NSObject { | |
| } | ||
|
|
||
| let sdkConfigBuilder = try SDKConfigBuilder.create(from: config) | ||
|
|
||
| #if CIO_LOCATION_ENABLED | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. How do customers set this? In their Podfile? |
||
| // Add location module to config builder if location config is provided | ||
| if let locationConfig = config["location"] as? [String: Any] { | ||
| let trackingModeValue = locationConfig["trackingMode"] as? String | ||
| let mode: LocationTrackingMode | ||
| switch trackingModeValue?.uppercased() { | ||
|
mahmoud-elmorabea marked this conversation as resolved.
Outdated
|
||
| case "OFF": | ||
| mode = .off | ||
| case "ON_APP_START": | ||
| mode = .onAppStart | ||
| default: | ||
| mode = .manual | ||
| } | ||
| _ = sdkConfigBuilder.addModule(LocationModule(config: LocationConfig(mode: mode))) | ||
| } | ||
| #endif | ||
|
|
||
| CustomerIO.initialize(withConfig: sdkConfigBuilder.build()) | ||
|
|
||
| do { | ||
|
|
@@ -58,6 +79,7 @@ public class NativeCustomerIO: NSObject { | |
| } catch { | ||
| logger.error("[InApp] Failed to initialize module with error: \(error)") | ||
| } | ||
|
|
||
| logger.debug( | ||
| "Customer.io SDK (\(packageSource ?? "") \(packageVersion ?? "")) initialized with config: \(config)" | ||
| ) | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,52 @@ | ||
| #import <React/RCTBridgeModule.h> | ||
| #import <RNCustomerIOSpec/RNCustomerIOSpec.h> | ||
|
|
||
| // Objective-C wrapper for new architecture TurboModule implementation | ||
| @interface RCTNativeCustomerIOLocation : NSObject <NativeCustomerIOLocationSpec> | ||
| // Bridge to Swift implementation for cross-language compatibility | ||
| @property(nonatomic, strong) id<NativeCustomerIOLocationSpec> swiftBridge; | ||
| @end | ||
|
|
||
| @implementation RCTNativeCustomerIOLocation | ||
|
|
||
| RCT_EXPORT_MODULE() | ||
|
|
||
| // Create TurboModule instance for new architecture JSI integration | ||
| - (std::shared_ptr<facebook::react::TurboModule>)getTurboModule: | ||
| (const facebook::react::ObjCTurboModule::InitParams &)params { | ||
| return std::make_shared<facebook::react::NativeCustomerIOLocationSpecJSI>(params); | ||
| } | ||
|
|
||
| - (instancetype)init { | ||
| if (self = [super init]) { | ||
| // Use runtime class lookup - NativeLocation class only exists when CioLocation subspec is installed | ||
| Class swiftClass = NSClassFromString(@"NativeCustomerIOLocation"); | ||
| if (swiftClass) { | ||
| _swiftBridge = [[swiftClass alloc] init]; | ||
| } | ||
| } | ||
| return self; | ||
| } | ||
|
|
||
| // Module initialization can happen on background thread | ||
| + (BOOL)requiresMainQueueSetup { | ||
| return NO; | ||
| } | ||
|
|
||
| - (void)setLastKnownLocation:(double)latitude | ||
| longitude:(double)longitude { | ||
| if (!_swiftBridge) return; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There is a helper
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
For location, the bridge being nil is expected when the customer hasn't installed the location
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm okay with the current implementation. But just for clarity, |
||
| [_swiftBridge setLastKnownLocation:latitude longitude:longitude]; | ||
| } | ||
|
|
||
| - (void)requestLocationUpdate { | ||
| if (!_swiftBridge) return; | ||
| [_swiftBridge requestLocationUpdate]; | ||
| } | ||
|
|
||
| // Export class factory function for React Native component registration | ||
| Class<RCTBridgeModule> NativeCustomerIOLocationCls(void) { | ||
| return RCTNativeCustomerIOLocation.class; | ||
| } | ||
|
|
||
| @end | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,19 @@ | ||
| #if CIO_LOCATION_ENABLED | ||
| import CioLocation | ||
| import CoreLocation | ||
|
|
||
| @objc(NativeCustomerIOLocation) | ||
| public class NativeLocation: NSObject { | ||
|
|
||
| @objc | ||
| func setLastKnownLocation(_ latitude: Double, longitude: Double) { | ||
| let location = CLLocation(latitude: latitude, longitude: longitude) | ||
| CustomerIO.location.setLastKnownLocation(location) | ||
| } | ||
|
|
||
| @objc | ||
| func requestLocationUpdate() { | ||
| CustomerIO.location.requestLocationUpdate() | ||
| } | ||
|
cursor[bot] marked this conversation as resolved.
|
||
| } | ||
| #endif | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,47 @@ | ||
| import { type TurboModule } from 'react-native'; | ||
| import { | ||
| default as NativeModule, | ||
| type Spec as CodegenSpec, | ||
| } from './specs/modules/NativeCustomerIOLocation'; | ||
| import { callNativeModule, ensureNativeModule } from './utils/native-bridge'; | ||
|
|
||
| /** | ||
| * Ensures all methods defined in codegen spec are implemented by the public module | ||
| * | ||
| * @internal | ||
| */ | ||
| interface NativeLocationSpec extends Omit<CodegenSpec, keyof TurboModule> {} | ||
|
|
||
| const nativeModule = ensureNativeModule(NativeModule); | ||
|
|
||
| const withNativeModule = <R>(fn: (native: CodegenSpec) => R): R => { | ||
| return callNativeModule(nativeModule, fn); | ||
| }; | ||
|
|
||
| /** @public */ | ||
| export class CustomerIOLocation implements NativeLocationSpec { | ||
| /** | ||
| * Sets the last known location from the host app's existing location system. | ||
| * Use this when your app already manages location and you want to send that data | ||
| * to Customer.io without the SDK managing location permissions directly. | ||
| * | ||
| * @param latitude - Latitude in degrees, must be between -90 and 90 | ||
| * @param longitude - Longitude in degrees, must be between -180 and 180 | ||
| */ | ||
| setLastKnownLocation(latitude: number, longitude: number): void { | ||
| return withNativeModule((native) => | ||
| native.setLastKnownLocation(latitude, longitude) | ||
| ); | ||
| } | ||
|
|
||
| /** | ||
| * Requests a single location update and sends the result to Customer.io. | ||
| * No-ops if location tracking is disabled or permission is not granted. | ||
| * | ||
| * The SDK does not request location permission. The host app must request | ||
| * runtime permissions and only call this when permission is granted. | ||
| */ | ||
| requestLocationUpdate(): void { | ||
| return withNativeModule((native) => native.requestLocationUpdate()); | ||
| } | ||
| } |
Uh oh!
There was an error while loading. Please reload this page.