Flutter Permissions
The Flutter SDK exposes one six-bucket permission model on iOS and Android. Use these two calls:
| Call | What it does |
|---|---|
DoorstepAI.checkPermissions() | Reads all six buckets. Never prompts. |
DoorstepAI.requestPermissions([options]) | Requests exactly the optional whitelist and resolves after the native prompt sequence settles. |
Read the detailed statuses map for decisions. The coarse permissions map is convenient for UI,
but intentionally collapses native distinctions.
The plugin cannot create iOS usage-description text or restore a permission removed from the
Android merged manifest. Add the declarations in
Installation → Platform-Specific Configuration. A
missing declaration is reported as notDeclared.
Check current state
static Future<PermissionStatus> checkPermissions()
checkPermissions() always reports the wrapper's six canonical buckets; it does not take a
whitelist and never prompts.
final result = await DoorstepAI.checkPermissions();
final always = result.statuses[DoorstepPermissionType.locationAlways];
if (always?.state == DoorstepPermissionState.whenInUseOnly) {
// Foreground-only: locked-phone tracking and route geofencing are not ready.
}
PermissionStatus
class PermissionStatus {
final Map<DoorstepPermissionType, PermissionState> permissions;
final Map<DoorstepPermissionType, DoorstepPermissionStatus> statuses;
}
class DoorstepPermissionStatus {
final DoorstepPermissionState state;
final bool canRequest;
final String osStatus;
}
| Field | Meaning |
|---|---|
statuses | Native detailed verdicts. Branch on state. |
permissions | Coarse Flutter summary: granted, denied, prompt, or promptWithRationale. |
osStatus | Free-form native diagnostic evidence. Never branch on it. |
canRequest | Whether the default native request policy can currently show a prompt for that bucket. |
On iOS, nearbyWifiDevices and notifications are reported as unavailable; they remain in the
map so shared Flutter UI does not need a different key set per platform.
The coarse map is derived mechanically:
| Detailed condition | Coarse state |
|---|---|
granted | granted |
notDetermined | prompt |
Any other state with canRequest: true | promptWithRationale |
Any other state with canRequest: false | denied |
Request permissions
static Future<PermissionRequestResult> requestPermissions([
PermissionRequestOptions options = const PermissionRequestOptions(),
])
class PermissionRequestOptions {
final List<DoorstepPermissionType>? permissions;
final bool force; // iOS only; default false
}
Omit the optional positional argument to request every bucket, pass a list to request exactly that whitelist, or pass an empty list to request nothing:
await DoorstepAI.requestPermissions();
await DoorstepAI.requestPermissions(
const PermissionRequestOptions(
permissions: [DoorstepPermissionType.locationWhenInUse],
),
);
await DoorstepAI.requestPermissions(
const PermissionRequestOptions(permissions: []),
);
The future resolves after native permission handling settles. A second request while one is active
throws DoorstepAIException with the native busy error instead of interleaving prompts.
PermissionRequestResult
class PermissionRequestResult extends PermissionStatus {
final List<DoorstepPermissionType> requested;
final List<DoorstepPermissionType> alreadyDetermined;
final List<DoorstepPermissionType> unavailable;
final Map<DoorstepPermissionType, String> deferred;
}
| Field | Meaning |
|---|---|
requested | Buckets for which a prompt was actually raised. Empty means no prompt appeared. |
alreadyDetermined | Buckets native left alone because they were already settled. On Android this is the already-granted set; re-askable denials can be requested again. |
unavailable | Buckets absent on this device/OS or missing from the host build. |
deferred | Buckets deliberately not requested, with the native reason. |
statuses | State after the request sequence settled. |
There is no didRequest property on the Flutter result. Use result.requested.isNotEmpty.
Permission buckets
enum DoorstepPermissionType {
locationWhenInUse,
locationAlways,
motion,
bluetooth,
nearbyWifiDevices,
notifications,
}
| Bucket | Android | iOS |
|---|---|---|
locationWhenInUse | Fine + coarse location | When In Use location |
locationAlways | Background location | Always location |
motion | Activity recognition | Motion & Fitness |
bluetooth | BLE scan | Bluetooth |
nearbyWifiDevices | Nearby Wi-Fi (API 33+) | unavailable |
notifications | Tracking notification (API 33+) | unavailable |
The SDK also keeps these deprecated source aliases for older Flutter applications:
DoorstepPermissionType.location→locationWhenInUseDoorstepPermissionType.backgroundLocation→locationAlwaysDoorstepPermissionType.activity→motion
They are static aliases, not additional enum values. New code should use the six canonical names.
Permission states
DoorstepPermissionState mirrors the native SDKs exactly:
| State | Meaning | What to do |
|---|---|---|
granted | Held now. | Continue. |
notDetermined | Never asked; a prompt can be shown. | Ask from a user-initiated flow. |
whenInUseOnly | Foreground location is held, background is not. Only used for locationAlways. | Explain the background need, then run the separate Always phase. Never treat it as granted. |
denied | Refused but re-askable. Android only. | Show a rationale, then re-ask. |
permanentlyDenied | Only Settings can change it. | Open the app's system Settings page. |
restricted | Device policy, MDM, or Screen Time blocks it. | Explain the device restriction. |
unavailable | The OS version or hardware does not expose it. | Nothing to request. |
notDeclared | The host build lacks an Android manifest entry or iOS usage-description key. | Fix and rebuild the host app. |
The coarse PermissionState mapping has four values: granted, denied, prompt, and
promptWithRationale (wire value prompt-with-rationale). Use the detailed state whenever
background authorization, permanent denial, or host configuration matters.
Recommended staged flow
Request foreground and background location as separate user-visible stages:
// Stage 1: the driver taps "Start shift".
final foreground = await DoorstepAI.requestPermissions(
const PermissionRequestOptions(
permissions: [DoorstepPermissionType.locationWhenInUse],
),
);
// Stage 2: after your own explanation of background tracking.
if (foreground
.statuses[DoorstepPermissionType.locationWhenInUse]
?.state ==
DoorstepPermissionState.granted) {
final background =
await DoorstepAI.requestBackgroundLocationPermission();
if (background.statuses[DoorstepPermissionType.locationAlways]?.state !=
DoorstepPermissionState.granted) {
// Show Settings guidance or continue in foreground-only mode.
}
}
// Stage 3: optional collection buckets when their purpose is visible.
await DoorstepAI.requestPermissions(
const PermissionRequestOptions(
permissions: [
DoorstepPermissionType.motion,
DoorstepPermissionType.bluetooth,
],
),
);
The convenience method is available on both platforms:
static Future<PermissionRequestResult>
requestBackgroundLocationPermission({bool force = false})
It requests only locationAlways. On Android, foreground location must already be granted and the
OS may open the "Allow all the time" Settings flow. On iOS, force: true overrides native's
default onlyIfNotDetermined policy; use that only from an explicit Always-upgrade action, never
at launch.
whenInUseOnly is not background-readyIt means foreground location can work, but locked-phone collection and route geofencing cannot be
relied on. Gate those features on locationAlways == granted.
DoorstepAiView and automatic prompting
Mounting DoorstepAiView automatically calls init, setApiKey, requestPermissions(), and then
requestBackgroundLocationPermission() when foreground location succeeds. Use it for the default
bootstrap. Use manual initialization plus the staged calls above when permission prompts must be
tied to explicit onboarding or rationale screens.
Android-only helpers
These calls expose the native Android flat permission-list compatibility API:
DoorstepAI.getRequiredPermissions({bool includeBluetooth = true})
DoorstepAI.hasRequiredPermissions({bool includeBluetooth = false})
DoorstepAI.hasBackgroundLocationPermission()
They throw DoorstepAIException on iOS; the plugin does not fabricate [] or true. Prefer the
typed status/request API for portable code. The Flutter Android plugin receives and forwards its
own permission results, so the host does not need to override onRequestPermissionsResult.
Troubleshooting
A bucket reports notDeclared
The user did not deny anything. Add the matching Android <uses-permission> entry or iOS
NS…UsageDescription key from Installation, rebuild,
and reinstall the host app.
The background request does not show a prompt
Inspect result.deferred[DoorstepPermissionType.locationAlways]. Common causes are missing
foreground location, an Android request that was not isolated from other buckets, or an iOS choice
that is already answered and must now be changed in Settings.
Background tracking or route geofencing stops
Check the detailed locationAlways state. whenInUseOnly is insufficient. On Android also confirm
the SDK's foreground-service notification is present; set manualForeground: true only when the
host already owns a foreground service.
An Android-only helper fails on iOS
Expected: those helpers have no iOS native counterpart and throw. Use checkPermissions() or the
typed request methods instead.
Next steps
- Usage & Integration — initialize the SDK and wire the delivery lifecycle.
- Installation — add the native declarations behind these buckets.
- Examples — complete Flutter flows and troubleshooting.