Skip to content

Commit 1b36d69

Browse files
authored
feat: Location enrichment (#316)
1 parent 82b7370 commit 1b36d69

29 files changed

+736
-22
lines changed

android/build.gradle

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,16 @@ rootProject.allprojects {
2424
apply plugin: 'com.android.library'
2525
apply plugin: 'kotlin-android'
2626

27+
def cioLocationEnabled = rootProject.findProperty("customerio_location_enabled")?.toBoolean() ?: false
28+
2729
android {
2830
namespace 'io.customer.customer_io'
2931
compileSdkVersion 36
3032

33+
buildFeatures {
34+
buildConfig true
35+
}
36+
3137
compileOptions {
3238
sourceCompatibility JavaVersion.VERSION_17
3339
targetCompatibility JavaVersion.VERSION_17
@@ -44,6 +50,8 @@ android {
4450

4551
defaultConfig {
4652
minSdkVersion 21
53+
buildConfigField "boolean", "CIO_LOCATION_ENABLED", "$cioLocationEnabled"
54+
consumerProguardFiles 'consumer-rules.pro'
4755
}
4856

4957
// instruct kotlin compiler to opt in to the APIs annotated with the kotlin.RequiresOptIn
@@ -59,10 +67,17 @@ android {
5967
dependencies {
6068
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
6169
// Customer.io SDK
62-
def cioVersion = "4.16.0"
70+
def cioVersion = "4.17.0"
6371
implementation "io.customer.android:datapipelines:$cioVersion"
6472
implementation "io.customer.android:messaging-push-fcm:$cioVersion"
6573
implementation "io.customer.android:messaging-in-app:$cioVersion"
74+
// Location module is optional - customers enable it by adding
75+
// customerio_location_enabled=true in their gradle.properties
76+
if (cioLocationEnabled) {
77+
implementation "io.customer.android:location:$cioVersion"
78+
} else {
79+
compileOnly "io.customer.android:location:$cioVersion"
80+
}
6681

6782
testImplementation 'junit:junit:4.13.2'
6883
testImplementation "org.jetbrains.kotlin:kotlin-test-junit:$kotlin_version"

android/consumer-rules.pro

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# Location module is an optional compileOnly dependency.
2+
# When not enabled, R8 must not fail on missing location classes.
3+
-dontwarn io.customer.location.**

android/src/main/kotlin/io/customer/customer_io/CustomerIOPlugin.kt

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import androidx.annotation.NonNull
66
import io.customer.customer_io.bridge.NativeModuleBridge
77
import io.customer.customer_io.bridge.nativeMapArgs
88
import io.customer.customer_io.bridge.nativeNoArgs
9+
import io.customer.customer_io.location.CustomerIOLocation
910
import io.customer.customer_io.messaginginapp.CustomerIOInAppMessaging
1011
import io.customer.customer_io.messagingpush.CustomerIOPushMessaging
1112
import io.customer.customer_io.utils.getAs
@@ -50,10 +51,14 @@ class CustomerIOPlugin : FlutterPlugin, MethodCallHandler, ActivityAware {
5051
flutterCommunicationChannel.setMethodCallHandler(this)
5152

5253
// Initialize modules
53-
modules = listOf(
54-
CustomerIOPushMessaging(flutterPluginBinding),
55-
CustomerIOInAppMessaging(flutterPluginBinding)
56-
)
54+
modules = buildList {
55+
add(CustomerIOPushMessaging(flutterPluginBinding))
56+
add(CustomerIOInAppMessaging(flutterPluginBinding))
57+
// Location module is optional - enabled via customerio_location_enabled gradle property
58+
if (BuildConfig.CIO_LOCATION_ENABLED) {
59+
add(CustomerIOLocation(flutterPluginBinding))
60+
}
61+
}
5762

5863
// Attach modules to engine
5964
modules.forEach {
@@ -226,6 +231,15 @@ class CustomerIOPlugin : FlutterPlugin, MethodCallHandler, ActivityAware {
226231
)
227232
}
228233
}
234+
// Configure location module based on config provided by customer app
235+
args.getAs<Map<String, Any>>(key = "location")?.let { locationConfig ->
236+
modules.filterIsInstance<CustomerIOLocation>().forEach {
237+
it.configureModule(
238+
builder = this,
239+
config = locationConfig,
240+
)
241+
}
242+
}
229243
}.build()
230244

231245
logger.info("Customer.io instance initialized successfully from app")
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
package io.customer.customer_io.location
2+
3+
import io.customer.customer_io.bridge.NativeModuleBridge
4+
import io.customer.customer_io.bridge.nativeMapArgs
5+
import io.customer.customer_io.bridge.nativeNoArgs
6+
import io.customer.customer_io.utils.getAs
7+
import io.customer.location.LocationModuleConfig
8+
import io.customer.location.LocationTrackingMode
9+
import io.customer.location.ModuleLocation
10+
import io.customer.sdk.CustomerIOBuilder
11+
import io.customer.sdk.core.di.SDKComponent
12+
import io.customer.sdk.core.util.Logger
13+
import io.flutter.embedding.engine.plugins.FlutterPlugin
14+
import io.flutter.plugin.common.MethodCall
15+
import io.flutter.plugin.common.MethodChannel
16+
17+
/**
18+
* Flutter module implementation for location module in native SDKs. All functionality
19+
* linked with the module should be placed here.
20+
*/
21+
internal class CustomerIOLocation(
22+
pluginBinding: FlutterPlugin.FlutterPluginBinding,
23+
) : NativeModuleBridge, MethodChannel.MethodCallHandler {
24+
override val moduleName: String = "Location"
25+
override val flutterCommunicationChannel: MethodChannel =
26+
MethodChannel(pluginBinding.binaryMessenger, "customer_io_location")
27+
private val logger: Logger = SDKComponent.logger
28+
29+
private fun getLocationServices() = runCatching {
30+
ModuleLocation.instance().locationServices
31+
}.onFailure {
32+
logger.error("Location module is not initialized. Ensure location config is provided during SDK initialization.")
33+
}.getOrNull()
34+
35+
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
36+
when (call.method) {
37+
"setLastKnownLocation" -> call.nativeMapArgs(result, ::setLastKnownLocation)
38+
"requestLocationUpdate" -> call.nativeNoArgs(result, ::requestLocationUpdate)
39+
else -> super.onMethodCall(call, result)
40+
}
41+
}
42+
43+
private fun setLastKnownLocation(params: Map<String, Any>) {
44+
val latitude = params.getAs<Double>("latitude")
45+
val longitude = params.getAs<Double>("longitude")
46+
47+
if (latitude == null || longitude == null) {
48+
logger.error("Latitude and longitude are required for setLastKnownLocation")
49+
return
50+
}
51+
52+
getLocationServices()?.setLastKnownLocation(latitude, longitude)
53+
}
54+
55+
private fun requestLocationUpdate() {
56+
getLocationServices()?.requestLocationUpdate()
57+
}
58+
59+
override fun configureModule(
60+
builder: CustomerIOBuilder,
61+
config: Map<String, Any>
62+
) {
63+
val trackingModeValue = config.getAs<String>("trackingMode")
64+
val trackingMode = trackingModeValue?.let { value ->
65+
runCatching { enumValueOf<LocationTrackingMode>(value) }.getOrNull()
66+
} ?: LocationTrackingMode.MANUAL
67+
68+
val module = ModuleLocation(
69+
LocationModuleConfig.Builder()
70+
.setLocationTrackingMode(trackingMode)
71+
.build()
72+
)
73+
builder.addCustomerIOModule(module)
74+
}
75+
}
Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1-
org.gradle.jvmargs=-Xmx1536M
1+
org.gradle.jvmargs=-Xmx4g
22
android.useAndroidX=true
33
android.enableJetifier=true
4+
customerio_location_enabled=true

apps/amiapp_flutter/ios/Podfile

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ target 'Runner' do
4444

4545
# Uncomment only 1 of the lines below to install a version of the iOS SDK
4646
pod 'customer_io/fcm', :path => '.symlinks/plugins/customer_io/ios' # install podspec bundled with the plugin
47+
pod 'customer_io/location', :path => '.symlinks/plugins/customer_io/ios' # install location subspec for testing
4748
# install_non_production_ios_sdk_local_path(local_path: '~/Development/customerio-ios/', is_app_extension: false, push_service: "fcm")
4849
# install_non_production_ios_sdk_git_branch(branch_name: 'feature/wrappers-inline-support', is_app_extension: false, push_service: "fcm")
4950

@@ -68,7 +69,9 @@ post_install do |installer|
6869
config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] ||= [
6970
'$(inherited)',
7071
## dart: PermissionGroup.notification
71-
'PERMISSION_NOTIFICATIONS=1'
72+
'PERMISSION_NOTIFICATIONS=1',
73+
## dart: PermissionGroup.location
74+
'PERMISSION_LOCATION=1'
7275
]
7376
end
7477
end

apps/amiapp_flutter/ios/Runner.xcodeproj/project.pbxproj

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
2121
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
2222
E377A53B8DA8666B0D7793F5 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 959D89812F5C216E228A95E4 /* Pods_Runner.framework */; };
23+
78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */ = {isa = PBXBuildFile; productRef = 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */; };
2324
/* End PBXBuildFile section */
2425

2526
/* Begin PBXContainerItemProxy section */
@@ -84,6 +85,7 @@
8485
EDFC0FD173F4E6F4048DC792 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<group>"; };
8586
F62EE4FB6B9A8D86AD03E7BC /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = "<group>"; };
8687
F682EA52D3CECA56F5B31766 /* Pods_NotificationServiceExtension.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_NotificationServiceExtension.framework; sourceTree = BUILT_PRODUCTS_DIR; };
88+
78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = FlutterGeneratedPluginSwiftPackage; path = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; sourceTree = "<group>"; };
8789
/* End PBXFileReference section */
8890

8991
/* Begin PBXFrameworksBuildPhase section */
@@ -99,6 +101,7 @@
99101
isa = PBXFrameworksBuildPhase;
100102
buildActionMask = 2147483647;
101103
files = (
104+
78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */,
102105
E377A53B8DA8666B0D7793F5 /* Pods_Runner.framework in Frameworks */,
103106
);
104107
runOnlyForDeploymentPostprocessing = 0;
@@ -131,6 +134,7 @@
131134
9740EEB11CF90186004384FC /* Flutter */ = {
132135
isa = PBXGroup;
133136
children = (
137+
78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */,
134138
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,
135139
9740EEB21CF90195004384FC /* Debug.xcconfig */,
136140
7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
@@ -209,6 +213,9 @@
209213
productType = "com.apple.product-type.app-extension";
210214
};
211215
97C146ED1CF9000F007C117D /* Runner */ = {
216+
packageProductDependencies = (
217+
78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */,
218+
);
212219
isa = PBXNativeTarget;
213220
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
214221
buildPhases = (
@@ -237,6 +244,9 @@
237244

238245
/* Begin PBXProject section */
239246
97C146E61CF9000F007C117D /* Project object */ = {
247+
packageReferences = (
248+
781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */,
249+
);
240250
isa = PBXProject;
241251
attributes = {
242252
BuildIndependentTargetsInParallel = YES;
@@ -856,6 +866,18 @@
856866
defaultConfigurationName = Release;
857867
};
858868
/* End XCConfigurationList section */
869+
/* Begin XCLocalSwiftPackageReference section */
870+
781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */ = {
871+
isa = XCLocalSwiftPackageReference;
872+
relativePath = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage;
873+
};
874+
/* End XCLocalSwiftPackageReference section */
875+
/* Begin XCSwiftPackageProductDependency section */
876+
78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */ = {
877+
isa = XCSwiftPackageProductDependency;
878+
productName = FlutterGeneratedPluginSwiftPackage;
879+
};
880+
/* End XCSwiftPackageProductDependency section */
859881
};
860882
rootObject = 97C146E61CF9000F007C117D /* Project object */;
861883
}

apps/amiapp_flutter/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,24 @@
55
<BuildAction
66
parallelizeBuildables = "YES"
77
buildImplicitDependencies = "YES">
8+
<PreActions>
9+
<ExecutionAction
10+
ActionType = "Xcode.IDEStandardExecutionActionsCore.ExecutionActionType.ShellScriptAction">
11+
<ActionContent
12+
title = "Run Prepare Flutter Framework Script"
13+
scriptText = "/bin/sh &quot;$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh&quot; prepare&#10;">
14+
<EnvironmentBuildable>
15+
<BuildableReference
16+
BuildableIdentifier = "primary"
17+
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
18+
BuildableName = "Runner.app"
19+
BlueprintName = "Runner"
20+
ReferencedContainer = "container:Runner.xcodeproj">
21+
</BuildableReference>
22+
</EnvironmentBuildable>
23+
</ActionContent>
24+
</ExecutionAction>
25+
</PreActions>
826
<BuildActionEntries>
927
<BuildActionEntry
1028
buildForTesting = "YES"

apps/amiapp_flutter/ios/Runner/Info.plist

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,5 +100,7 @@
100100
<false/>
101101
<key>PermissionGroupNotification</key>
102102
<string>Required to send push notifications</string>
103+
<key>NSLocationWhenInUseUsageDescription</key>
104+
<string>This app uses your location to test Customer.io location tracking features.</string>
103105
</dict>
104106
</plist>

apps/amiapp_flutter/lib/src/app.dart

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import 'screens/dashboard.dart';
1212
import 'screens/events.dart';
1313
import 'screens/inbox_messages.dart';
1414
import 'screens/inline_messages.dart';
15+
import 'screens/location.dart';
1516
import 'screens/login.dart';
1617
import 'screens/settings.dart';
1718
import 'theme/sizes.dart';
@@ -150,6 +151,11 @@ class _AmiAppState extends State<AmiApp> {
150151
path: Screen.inboxMessages.path,
151152
builder: (context, state) => const InboxMessagesScreen(),
152153
),
154+
GoRoute(
155+
name: Screen.locationTest.name,
156+
path: Screen.locationTest.path,
157+
builder: (context, state) => const LocationScreen(),
158+
),
153159
],
154160
),
155161
],

0 commit comments

Comments
 (0)