From 8d7653eb2af7cb94c4edfbfa9214c6847cc409b2 Mon Sep 17 00:00:00 2001 From: Muhammad Rehan Date: Thu, 25 Jun 2026 18:13:48 +0500 Subject: [PATCH] chore: register geofence module with native SDKs Wires the geofence module into the native SDK builders during initialize() on Android and iOS. Geofence runs automatically once registered and depends on the location module, which is registered (with the app's config or defaults) whenever geofence is enabled. iOS guards against a Flutter-null 'geofence' key (NSNull) by casting to a dictionary, so geofence/location are only registered when a GeofenceConfig is actually provided. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../customer/customer_io/CustomerIOPlugin.kt | 31 ++++++++++++++++--- .../geofence/CustomerIOGeofence.kt | 29 +++++++++++++++++ .../customer_io/CustomerIOPlugin.swift | 25 +++++++++++++-- 3 files changed, 78 insertions(+), 7 deletions(-) create mode 100644 android/src/main/kotlin/io/customer/customer_io/geofence/CustomerIOGeofence.kt diff --git a/android/src/main/kotlin/io/customer/customer_io/CustomerIOPlugin.kt b/android/src/main/kotlin/io/customer/customer_io/CustomerIOPlugin.kt index 9daacd5..7f05b29 100644 --- a/android/src/main/kotlin/io/customer/customer_io/CustomerIOPlugin.kt +++ b/android/src/main/kotlin/io/customer/customer_io/CustomerIOPlugin.kt @@ -6,6 +6,7 @@ import androidx.annotation.NonNull import io.customer.customer_io.bridge.NativeModuleBridge import io.customer.customer_io.bridge.nativeMapArgs import io.customer.customer_io.bridge.nativeNoArgs +import io.customer.customer_io.geofence.CustomerIOGeofence import io.customer.customer_io.location.CustomerIOLocation import io.customer.customer_io.messaginginapp.CustomerIOInAppMessaging import io.customer.customer_io.messagingpush.CustomerIOPushMessaging @@ -54,10 +55,15 @@ class CustomerIOPlugin : FlutterPlugin, MethodCallHandler, ActivityAware { modules = buildList { add(CustomerIOPushMessaging(flutterPluginBinding)) add(CustomerIOInAppMessaging(flutterPluginBinding)) - // Location module is optional - enabled via customerio_location_enabled gradle property + // Location module is optional - enabled via customerio_location_enabled gradle + // property. CIO_LOCATION_ENABLED also covers geofence (geofence implies location). if (BuildConfig.CIO_LOCATION_ENABLED) { add(CustomerIOLocation(flutterPluginBinding)) } + // Geofence module is optional - enabled via customerio_geofence_enabled gradle property + if (BuildConfig.CIO_GEOFENCE_ENABLED) { + add(CustomerIOGeofence(flutterPluginBinding)) + } } // Attach modules to engine @@ -231,15 +237,32 @@ class CustomerIOPlugin : FlutterPlugin, MethodCallHandler, ActivityAware { ) } } - // Configure location module based on config provided by customer app - args.getAs>(key = "location")?.let { locationConfig -> + // Configure location module. Geofence implies location, so register location + // (with the app's config if given, otherwise defaults) whenever location or + // geofence is configured, since geofence relies on its location fixes. The build + // flags guard each block so the optional bridge classes (which reference native + // artifacts that are only linked when enabled) are never touched otherwise. + val locationConfig = args.getAs>(key = "location") + val geofenceConfig = args.getAs>(key = "geofence") + if (BuildConfig.CIO_LOCATION_ENABLED && (locationConfig != null || geofenceConfig != null)) { modules.filterIsInstance().forEach { it.configureModule( builder = this, - config = locationConfig, + config = locationConfig ?: emptyMap(), ) } } + // Configure geofence module (runs automatically once registered) + if (BuildConfig.CIO_GEOFENCE_ENABLED) { + geofenceConfig?.let { config -> + modules.filterIsInstance().forEach { + it.configureModule( + builder = this, + config = config, + ) + } + } + } }.build() logger.info("Customer.io instance initialized successfully from app") diff --git a/android/src/main/kotlin/io/customer/customer_io/geofence/CustomerIOGeofence.kt b/android/src/main/kotlin/io/customer/customer_io/geofence/CustomerIOGeofence.kt new file mode 100644 index 0000000..80799ad --- /dev/null +++ b/android/src/main/kotlin/io/customer/customer_io/geofence/CustomerIOGeofence.kt @@ -0,0 +1,29 @@ +package io.customer.customer_io.geofence + +import io.customer.customer_io.bridge.NativeModuleBridge +import io.customer.geofence.GeofenceModuleConfig +import io.customer.geofence.ModuleGeofence +import io.customer.sdk.CustomerIOBuilder +import io.flutter.embedding.engine.plugins.FlutterPlugin +import io.flutter.plugin.common.MethodChannel + +/** + * Flutter bridge for the geofence module. Geofence has no Flutter-facing methods — + * it runs automatically once registered — so this only wires the native module into + * the SDK builder. The reference to [ModuleGeofence] is isolated here so it is loaded + * only when the geofence dependency is bundled. + * + * Geofence depends on the location module; registration of location is handled by the + * plugin when geofence is configured. + */ +internal class CustomerIOGeofence( + pluginBinding: FlutterPlugin.FlutterPluginBinding, +) : NativeModuleBridge { + override val moduleName: String = "Geofence" + override val flutterCommunicationChannel: MethodChannel = + MethodChannel(pluginBinding.binaryMessenger, "customer_io_geofence") + + override fun configureModule(builder: CustomerIOBuilder, config: Map) { + builder.addCustomerIOModule(ModuleGeofence(GeofenceModuleConfig.Builder().build())) + } +} diff --git a/ios/customer_io/Sources/customer_io/CustomerIOPlugin.swift b/ios/customer_io/Sources/customer_io/CustomerIOPlugin.swift index d8483d7..d743084 100644 --- a/ios/customer_io/Sources/customer_io/CustomerIOPlugin.swift +++ b/ios/customer_io/Sources/customer_io/CustomerIOPlugin.swift @@ -6,6 +6,9 @@ import UIKit #if canImport(CioLocation) import CioLocation #endif +#if canImport(CioLocationGeofence) +import CioLocationGeofence +#endif public class CustomerIOPlugin: NSObject, FlutterPlugin { private var methodChannel: FlutterMethodChannel! @@ -167,9 +170,18 @@ public class CustomerIOPlugin: NSObject, FlutterPlugin { let sdkConfigBuilder = try SDKConfigBuilder.create(from: params) #if canImport(CioLocation) - // Add location module to config builder if location config is provided - if let locationConfig = params["location"] as? [String: AnyHashable] { - let trackingModeValue = locationConfig["trackingMode"] as? String + let locationConfig = params["location"] as? [String: AnyHashable] + #if canImport(CioLocationGeofence) + let geofenceConfigured = params["geofence"] as? [String: AnyHashable] != nil + #else + let geofenceConfigured = false + #endif + + // Add location module when location or geofence is configured. Geofence implies + // location: it relies on the location module's fixes, so register location (with + // the app's config if given, otherwise defaults) whenever geofence is enabled. + if locationConfig != nil || geofenceConfigured { + let trackingModeValue = locationConfig?["trackingMode"] as? String let mode: LocationTrackingMode switch trackingModeValue?.uppercased() { case "OFF": @@ -181,6 +193,13 @@ public class CustomerIOPlugin: NSObject, FlutterPlugin { } _ = sdkConfigBuilder.addModule(LocationModule(config: LocationConfig(mode: mode))) } + + #if canImport(CioLocationGeofence) + // Geofence runs automatically once registered; relies on the location module above. + if geofenceConfigured { + _ = sdkConfigBuilder.addModule(GeofenceModule()) + } + #endif #endif CustomerIO.initialize(withConfig: sdkConfigBuilder.build())