Platform setup
Beekon declares the permissions it needs in its own manifest/Info.plist and merges them into your app at build time — but you still request the dangerous permissions at runtime and provide the OS-level declarations. Pick your platform; the tabs stay synced.
The install + first-fix flow is on the Quickstart; this page is the OS plumbing.
The .aar declares its permissions in its own manifest; they merge into your app at build. You request them at runtime and provide a NotificationConfig for the foreground service.
Dependency
Section titled “Dependency”android { defaultConfig { minSdk = 26 // Beekon's minimum }}
dependencies { implementation("io.github.beekonlabs:beekon:0.4.0")}JDK 17 to build. explicitApi() is enforced — Kotlin 2.x is recommended but your app’s version needn’t match.
Permissions
Section titled “Permissions”Add these to app/src/main/AndroidManifest.xml if you want to see them in your merged manifest:
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /><uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" /><uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" /><uses-permission android:name="android.permission.FOREGROUND_SERVICE" /><uses-permission android:name="android.permission.FOREGROUND_SERVICE_LOCATION" /><uses-permission android:name="android.permission.POST_NOTIFICATIONS" />FOREGROUND_SERVICE_LOCATION is required from Android 14 (API 34). POST_NOTIFICATIONS is required from Android 13 (API 33) for the foreground-service notification to be visible.
Runtime permission flow
Section titled “Runtime permission flow”Android forces a three-step sequence — you can’t ask for background location without first having foreground location, and notifications need their own grant on Android 13+.
- Request
ACCESS_FINE_LOCATION+ACCESS_COARSE_LOCATIONtogether (single dialog). - On API 29+, request
ACCESS_BACKGROUND_LOCATIONseparately. This always opens Settings on API 30+. - On API 33+, request
POST_NOTIFICATIONS.
The SDK’s requestPermission(...) / requestNextNeededPermission(...) perform this sequence correctly — pairing FINE + COARSE, requesting background only after foreground, and never firing a prompt the OS would silently swallow. If you’d rather drive it yourself, the Compose sample at beekon-android/sample ships a LocationPermissions composable that walks through this exact sequence — copy from there if you don’t have a permissions helper.
Foreground-service notification
Section titled “Foreground-service notification”BeekonConfig.notification is required — Android shows a notification while location is captured in the background. Its three fields are documented on Configuration. The channel id and notification id are SDK-internal constants.
BeekonKit is a Swift package distributed as a signed binary .xcframework. iOS 13 is the minimum; on iOS 17+ the SDK runs its modern stack — CLLocationUpdate.liveUpdates plus CLBackgroundActivitySession, with CLServiceSession(authorization: .always) on iOS 18+ — and falls back to the classic Core Location + CLRegion engines on iOS 13–16.
Add the package
Section titled “Add the package”In Xcode: File → Add Package Dependencies… then paste:
https://github.com/beekonlabs/beekon-ios-binary.gitSelect version 0.4.0 and add BeekonKit to your app target.
Info.plist
Section titled “Info.plist”Add usage descriptions and the location background mode. The strings appear in the system permission dialog — write them in your app’s voice.
<key>NSLocationWhenInUseUsageDescription</key><string>Used to show your live position in the app.</string>
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key><string>Used to keep tracking your trips when the app is in the background.</string>
<key>UIBackgroundModes</key><array> <string>location</string></array>fetch and processing are not required for v1 — only location.
Authorization flow
Section titled “Authorization flow”Beekon never prompts implicitly — nothing in configure(...)/start()/init ever pops a dialog. You drive the ask, whether through the SDK’s own request API (recommended), your own CLLocationManager calls, or a permission library. You must reach Always authorization before background tracking will work.
- Request
whenInUsefirst — required before you can ever ask foralways. - After the user grants
whenInUseand uses the feature once, requestalways. iOS denies silently if you ask too aggressively. - Call
Beekon.shared.start()once authorization isalways.
The SDK’s requestPermission(_:) performs exactly this two-step, one-shot Always upgrade correctly on your behalf. If you’d rather drive Core Location yourself, the sample at beekon-ios/Sample/LocationPermissionManager.swift shows the canonical two-step prompt:
private let manager = CLLocationManager()
func requestWhenInUse() { manager.requestWhenInUseAuthorization() }func requestAlways() { manager.requestAlwaysAuthorization() }Wire these to two buttons in onboarding rather than firing both at once.
beekon_flutter bridges straight to the Android .aar and BeekonKit on iOS, so background behaviour, gating, and persistence are exactly what the native tabs describe.
Install
Section titled “Install”dependencies: beekon_flutter: ^0.4.0flutter pub getDart SDK ^3.9.0, Flutter >=3.32.0. Android minSdk 26, iOS 13 — the same floors as the native SDKs.
OS configuration
Section titled “OS configuration”Request ACCESS_FINE_LOCATION (and POST_NOTIFICATIONS on Android 13+) at runtime before start(). Background tracking additionally needs ACCESS_BACKGROUND_LOCATION, requested separately after foreground location is granted. Add the iOS Info.plist keys and the location background mode — see the Android and iOS tabs above for the exact declarations.
The iOS side is SwiftPM-only — there is no podspec. On Flutter older than 3.44, enable Swift Package Manager once:
flutter config --enable-swift-package-manager@wayq/beekon-rn is a New Architecture TurboModule that bridges JS to the native SDKs. It owns no location logic.
Install
Section titled “Install”npm install @wayq/beekon-rncd ios && pod installOS configuration
Section titled “OS configuration”Declare permissions and request them at runtime — the mechanics match the native SDKs. See the Android tab for the manifest block and the iOS tab for the Info.plist keys and background mode.
License (optional)
Section titled “License (optional)”A license key is optional and purely observational — an absent key just means Beekon runs in evaluation mode, fully functional. If you’ve been issued a production token, supply it in configure(...) and read the status; it never blocks tracking. See Licensing for the full flow.
The permission doctor
Section titled “The permission doctor”You request the OS permissions — but which ones depends on your config. getRequiredPermissions() answers that: it returns the exact set the current config needs, each tagged required or recommended and marked whether the live OS grant already covers it. Call it after configure(...) so the result reflects the active gate; before that it returns a conservative location-only list. In Cloud mode the config is server-owned and unknown pre-start, so it returns the same conservative list.
It’s read-only and never prompts — it tells you what’s needed; Requesting permissions below is how you ask. Each entry is a PermissionRequirement:
| Field | Type | Meaning |
|---|---|---|
permission | enum | location, backgroundLocation, activityRecognition, or notifications (normalized, not raw OS constants). |
importance | enum | required (tracking can’t start, or an enabled feature is dead without it) or recommended (a feature silently degrades; tracking continues). |
satisfied | bool | Whether the live OS grant/capability already covers it. |
rationale | string | Human-readable: why it’s needed / what degrades without it. |
location and backgroundLocation are always present; activityRecognition appears when the config can use motion detection; notifications appears only on Android 13+ (it backs the foreground-service notification and per-geofence notifications) and never on iOS. Platform asymmetry on satisfied: on Android it’s the runtime grant; on iOS — which has no runtime motion-permission API — activityRecognition reflects the NSMotionUsageDescription plist key plus device capability.
// synchronous; call after configure()Beekon.getRequiredPermissions().forEach { r -> Log.d("beekon", "${r.permission} (${r.importance}) satisfied=${r.satisfied}: ${r.rationale}")}// r.permission: BeekonPermission.Location / .BackgroundLocation / .ActivityRecognition / .Notifications// r.importance: PermissionImportance.Required / .Recommended// actor-isolated; call after configure(_:)for r in await Beekon.shared.getRequiredPermissions() { print("\(r.permission) (\(r.importance)) satisfied=\(r.satisfied): \(r.rationale)")}// r.permission: .location / .backgroundLocation / .activityRecognition (.notifications never on iOS)// r.importance: .required / .recommendedfinal reqs = await Beekon.instance.getRequiredPermissions();for (final r in reqs) { print('${r.permission} (${r.importance}) satisfied=${r.satisfied}: ${r.rationale}');}const reqs = await Beekon.getRequiredPermissions();reqs.forEach((r) => { console.log(r.permission, r.importance, r.satisfied, r.rationale);});Requesting permissions
Section titled “Requesting permissions”The doctor tells you what’s needed. This is how you ask — the explicit, app-initiated request layer that performs each OS permission ask correctly on each platform, exactly once per app decision to ask.
The policy is unchanged: Beekon never prompts implicitly. Nothing in configure(...), start(), or init ever raises a dialog, and there is no auto-firing chain — every prompt is one your code deliberately triggers, in context, behind your own rationale UI. requestPermission(...) is that explicit ask; it complements the read-only doctor rather than replacing it. Third-party permission libraries (permission_handler, react-native-permissions, …) remain fully supported — drive them off getRequiredPermissions() instead if you prefer.
Three methods:
| Method | What it does |
|---|---|
requestPermission(permission) | Request one permission. Resolves to a PermissionRequestResult. Performs the platform-correct ask (see the platform notes below) and never fires a prompt the OS would silently swallow. |
requestNextNeededPermission() | Derives the doctor list and requests the first unsatisfied required permission in canonical order, then returns its result — or null/nil when nothing is needed. The idiomatic one call per onboarding screen. It walks required entries only; recommended ones stay deliberate app choices you surface from the doctor list. |
openSettings(for: permission) | Opens the closest reachable OS settings screen — the only path once an outcome is requiresSettings. |
There’s no auto-chain by design: requestNextNeededPermission() advances one step per call. Ask, show your rationale for the next one, ask again on the next tap — one prompt per screen.
The onboarding flow
Section titled “The onboarding flow”Read getRequiredPermissions() to render what’s needed and why, then call requestNextNeededPermission() in-context to ask for the first thing still missing. Repeat per screen until it returns null/nil, then start().
// suspend; needs a foreground Activity. Call from a coroutine.when (val r = Beekon.requestNextNeededPermission(activity)) { null -> Beekon.start() // nothing left to ask — good to go else -> when (r.outcome) { PermissionRequestOutcome.RequiresSettings -> Beekon.openSettings(activity, BeekonPermission.Location) else -> { /* re-render your rationale; ask again on the next tap */ } }}
// Or request one explicitly:val res = Beekon.requestPermission(activity, BeekonPermission.BackgroundLocation)// res.outcome, res.prerequisite (when Blocked), res.reason (when NotAttempted), res.status// actor method; the Core Location work hops to the main actor internally.if let r = await Beekon.shared.requestNextNeededPermission() { switch r.outcome { case .requiresSettings: Beekon.openSettings(for: .location) default: break // re-show your rationale; ask again on the next tap }} else { await Beekon.shared.start() // nothing needed}
// Or request one explicitly:let res = await Beekon.shared.requestPermission(.notifications)// res.outcome, res.prerequisite (when .blocked), res.reason (when .notAttempted), res.statusfinal r = await Beekon.instance.requestNextNeededPermission();if (r == null) { await Beekon.instance.start(); // nothing left to ask} else if (r.outcome == PermissionRequestOutcome.requiresSettings) { await Beekon.instance.openSettings(BeekonPermission.location);}
// Or request one explicitly:final res = await Beekon.instance.requestPermission(BeekonPermission.backgroundLocation);const r = await Beekon.requestNextNeededPermission();if (r === null) { await Beekon.start(); // nothing left to ask} else if (r.outcome === 'requiresSettings') { await Beekon.openSettings('location');}
// Or request one explicitly:const res = await Beekon.requestPermission('backgroundLocation');The result
Section titled “The result”Every request resolves to a flat PermissionRequestResult: an outcome, an optional prerequisite (a BeekonPermission, set only when outcome == blocked), an optional reason string (set only when outcome == notAttempted), and a refreshed PermissionStatus snapshot.
outcome | Meaning |
|---|---|
granted | The grant now covers the request. Includes iOS provisional Always and one-time grants — neither platform exposes durability, so it’s documented as unknowable. |
denied | The system prompt ran; the grant is still absent; a future prompt may remain possible. |
requiresSettings | A further prompt is impossible — openSettings(...) is the only path left. |
alreadySatisfied | No-op; the grant already covered it. |
blocked | A precondition guard fired; prerequisite names the permission to request first (e.g. background location before foreground). |
notAttempted | The SDK declined to fire a doomed prompt; reason says why — missing_manifest_entry, missing_plist_key, unsupported, or not_foreground. |
PermissionRequestOutcome is a host-facing enum and may gain values in a minor release — switch with an else/default branch.
Platform notes
Section titled “Platform notes”- Requests need a foreground
Activity— off the foreground you getnotAttemptedwith reasonnot_foreground. - The permission must be declared in the merged manifest or the request resolves
notAttempted(missing_manifest_entry). NotablyACCESS_BACKGROUND_LOCATIONis not in Beekon’s library manifest — your app must add it (see Permissions above). - Location requests pair FINE + COARSE in a single ask automatically.
- Background must come after foreground. A background-location request while foreground location is still ungranted returns
outcome = blocked,prerequisite = location(Android 11+ silently denies a combined ask with no UI). Request foreground first, then background. notificationsis runtime-requestable only on Android 13+ (API 33) targeting SDK 33+; otherwisenotAttempted(unsupported).
- Always is a one-shot upgrade. The When-In-Use → Always prompt fires at most once; once consumed, further Always requests are silent no-ops and resolve
requiresSettings.requestPermission(.backgroundLocation)handles this correctly. - Info.plist purpose strings are required. Without the matching
NSLocation…UsageDescriptionkey (orNSMotionUsageDescriptionfor activity recognition) the request resolvesnotAttempted(missing_plist_key). See Info.plist above. requestPermission(.notifications)usesUNUserNotificationCenterand pre-checks a prior denial.Beekon.requestNotificationAuthorization()is deprecated in favor ofrequestPermission(.notifications)and will be removed before 1.0.- There is no runtime motion-permission API, so
requestPermission(.activityRecognition)triggers the one-shotCMMotionActivityManagerprompt.
The bridge forwards to the native SDKs, so the Android and iOS platform notes above apply verbatim — add ACCESS_BACKGROUND_LOCATION to your Android manifest and the iOS Info.plist purpose strings, request background only after foreground, and expect the iOS one-shot Always upgrade. Result fields (outcome, prerequisite, reason, status) cross the bridge unchanged.
The TurboModule forwards to the native SDKs, so the Android and iOS platform notes above apply verbatim. outcome and reason arrive as string literals ('granted', 'requiresSettings', 'missing_plist_key', …); decode defensively and tolerate unknown tokens. react-native-permissions remains fully supported if you’d rather drive the asks yourself off getRequiredPermissions().
- Configuration — every field and enum.
- Background execution — what’s actually keeping your app alive.
- Lifecycle & states — the state machine driving
state. - Licensing — supply a key and read its status (optional).