@geofencekit/sdk
v0.2.1
Published
React Native geofencing SDK: server-managed fence sync, native iOS/Android region monitoring, offline event queue, local notifications
Maintainers
Readme
Geofence Mobile SDK — native core
A server-managed geofencing SDK whose entire logic lives in native Swift and Kotlin — no JavaScript, no cross-platform runtime baked in. Fence sync, OS region monitoring, the offline event queue, notifications, and config all run natively, so the same dependable engine can be consumed by a native app, a Flutter plugin, or a React Native module.
Any SDK-provided UI (permission priming, debug map) ships as SwiftUI / Jetpack Compose — never web views or JS components.
Why server-managed?
iOS caps monitored regions at 20 per app; Android at 100. The SDK registers only the nearest 18 fences (leaving iOS headroom) and re-syncs when the device moves — so the platform supports unlimited branches while staying inside OS limits.
Layout
Package.swift Swift Package manifest (pure-native iOS)
ios/Sources/GeofenceSDK/ Native iOS core — no React
├── GeofenceSDK.swift Public facade: configure / requestPermissions / start / stop
├── GeofenceConfig.swift · Models.swift
├── APIClient.swift URLSession client (fence sync, event upload)
├── EventStore.swift Offline queue (Application Support, batched)
├── BackgroundUploader.swift Background URLSession upload + BGTaskScheduler
├── FenceStore.swift UserDefaults: fences, config, cooldowns, anchor
├── LocationManager.swift CLLocationManager: permissions, regions, SLC
└── NotificationManager.swift UNUserNotificationCenter
android/src/main/java/com/geofencesdk/ Native Android core — no React
├── GeofenceSdk.kt Public facade (mirror of the Swift one)
├── GeofenceConfig / Models.kt
├── ApiClient.kt HttpURLConnection + org.json (no deps)
├── EventQueue.kt Offline queue (SharedPreferences, batched)
├── UploadWorker.kt WorkManager background upload (survives Doze / reboot)
├── FenceStore.kt Persisted fences, config, cooldowns, anchor
├── GeofenceManager.kt GeofencingClient + FusedLocation wrapper
├── GeofenceBroadcastReceiver.kt Headless triggers (app dead) → notify + record + upload
├── BootReceiver.kt Re-register fences on reboot / app update
└── NotificationHelper.kt NotificationManagerCompat
bindings/ Thin host wrappers (see bindings/README.md)
├── react-native/ RN module — thin delegate to the native core
└── flutter/ Flutter plugin (MethodChannel) — vendors the coresInstall
iOS — Swift Package Manager (recommended)
// Package.swift
dependencies: [
.package(url: "https://github.com/mohamedma872/geofence-mobile-sdk.git", from: "0.2.0")
]Add the location + background-mode keys to Info.plist
(NSLocationWhenInUseUsageDescription, NSLocationAlwaysAndWhenInUseUsageDescription,
UIBackgroundModes → location).
Android — Gradle (AAR via Maven / local module)
dependencies {
implementation "com.geofencekit:geofence-sdk:0.2.0"
implementation "com.google.android.gms:play-services-location:21.2.0"
}Declare ACCESS_FINE_LOCATION, ACCESS_BACKGROUND_LOCATION, POST_NOTIFICATIONS,
RECEIVE_BOOT_COMPLETED in the manifest and request the runtime permissions from
your Activity.
Use it — native
Swift
import GeofenceSDK
let sdk = GeofenceSDK.shared
sdk.configure(GeofenceConfig(baseURL: "https://api.example.com", apiKey: "sdk-key", userId: "u_1"))
let status = await sdk.requestPermissions() // two-step WhenInUse → Always
if status == .grantedAlways {
let fences = try await sdk.start() // sync + register + monitor
}Kotlin
val sdk = GeofenceSdk.getInstance(context)
sdk.configure(GeofenceConfig(baseUrl = "https://api.example.com", apiKey = "sdk-key", userId = "u_1"))
// Request ACCESS_FINE / BACKGROUND / POST_NOTIFICATIONS from your Activity, then:
if (sdk.permissionStatus() == PermissionStatus.GRANTED_ALWAYS) {
lifecycleScope.launch { val fences = sdk.start() }
}Runtime behaviour (identical on both platforms)
start()gets the current location, callsGET /v1/fences/nearest, registers the nearest fences with the OS, and begins movement monitoring.- On movement past the server's
refresh_after_m, the fence set is re-fetched and swapped. - On enter, a local notification fires immediately from cached campaign content — no network round-trip, works offline — with a per-fence cooldown (hours managed server-side, applied on device).
- Events queue offline-safe with idempotency keys and upload in batches; the server enforces the authoritative cooldown / quiet hours / windows.
- Server-managed rules (fence limit, re-sync distance, cooldown) arrive in the sync response and are applied on the next sync — no app release.
Android process-death resilience
GeofenceBroadcastReceiver fires even when the app process is dead: it notifies
from FenceStore-persisted content, records the event, and schedules a
WorkManager upload (network-constrained, exponential backoff) that runs when
connectivity returns — surviving app-death, Doze, and reboot. BootReceiver
re-registers all fences on BOOT_COMPLETED and app update — the two moments
Android silently drops geofences.
iOS background delivery
Uploads use a background URLSession, so iOS finishes the transfer
out-of-process even if the app is suspended or terminated, and relaunches the
app to deliver the result. Events are removed from the queue only once the
server confirms them (idempotent client_event_id makes a re-send harmless). A
BGTaskScheduler processing task provides a periodic drain when no location
event is relaunching the app.
Two of these require host wiring you add once. Call configure() early (in
didFinishLaunching) so the background session + BGTask handler are registered
before iOS delivers events:
// Info.plist
// <key>UIBackgroundModes</key><array><string>location</string><string>processing</string></array>
// <key>BGTaskSchedulerPermittedIdentifiers</key><array><string>com.geofencekit.upload.refresh</string></array>
// AppDelegate
func application(_ app: UIApplication,
handleEventsForBackgroundURLSession identifier: String,
completionHandler: @escaping () -> Void) {
GeofenceSDK.shared.handleBackgroundURLSession(identifier, completion: completionHandler)
}
// in application(_:didFinishLaunchingWithOptions:)
BGTaskScheduler.shared.register(
forTaskWithIdentifier: "com.geofencekit.upload.refresh", using: nil
) { task in
GeofenceSDK.shared.handleBGTask(task as! BGProcessingTask)
}Without the wiring it still works — uploads just fall back to the region /
movement wake windows (nothing is lost; delivery may be less timely). The
React Native host wires the same two hooks from a Swift AppDelegate.
Platform limits & quotas
The SDK is thin; these limits live on the platform and apply to every host.
| Limit | Default | Notes | | --- | --- | --- | | Branches per workspace | 100 | Platform admin can raise it per customer | | API keys — trial / production | 2 / 25 | Active keys a workspace can self-issue | | Trial | 30 days · 1,000 devices · 10,000 events/mo | All features during the trial | | Fences monitored per device | ~18 | Nearest fences, re-synced on movement (iOS caps at 20 regions) | | Zone types | Circle + polygon | Radius fence or arbitrary polygon (point-in-polygon) | | Analytics retention | 90 days | Auto-deleted after; export to CSV from the dashboard anytime |
Capabilities: native iOS (Swift) + Android (Kotlin) cores; React Native + Flutter bindings; enter / exit / dwell; circular + polygon zones; background operation surviving reboot & app update (Android); offline event queue with idempotency keys; server-managed nearest-N fence sync; instant offline notifications from cached campaign content; central config (cooldown, quiet hours, fence limit) managed from the dashboard and synced to devices.
Bindings
The native cores are the SDK. Hosts plug in through thin wrappers — see
bindings/README.md. Flutter uses a MethodChannel;
React Native uses a small native module. Neither contains business logic —
they only forward calls to the native facade.
