Skip to main content

Flutter Permissions

The Flutter SDK exposes one six-bucket permission model on iOS and Android. Use these two calls:

CallWhat 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.

Platform declarations still matter

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;
}
FieldMeaning
statusesNative detailed verdicts. Branch on state.
permissionsCoarse Flutter summary: granted, denied, prompt, or promptWithRationale.
osStatusFree-form native diagnostic evidence. Never branch on it.
canRequestWhether 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 conditionCoarse state
grantedgranted
notDeterminedprompt
Any other state with canRequest: truepromptWithRationale
Any other state with canRequest: falsedenied

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;
}
FieldMeaning
requestedBuckets for which a prompt was actually raised. Empty means no prompt appeared.
alreadyDeterminedBuckets native left alone because they were already settled. On Android this is the already-granted set; re-askable denials can be requested again.
unavailableBuckets absent on this device/OS or missing from the host build.
deferredBuckets deliberately not requested, with the native reason.
statusesState 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,
}
BucketAndroidiOS
locationWhenInUseFine + coarse locationWhen In Use location
locationAlwaysBackground locationAlways location
motionActivity recognitionMotion & Fitness
bluetoothBLE scanBluetooth
nearbyWifiDevicesNearby Wi-Fi (API 33+)unavailable
notificationsTracking notification (API 33+)unavailable

The SDK also keeps these deprecated source aliases for older Flutter applications:

  • DoorstepPermissionType.locationlocationWhenInUse
  • DoorstepPermissionType.backgroundLocationlocationAlways
  • DoorstepPermissionType.activitymotion

They are static aliases, not additional enum values. New code should use the six canonical names.


Permission states

DoorstepPermissionState mirrors the native SDKs exactly:

StateMeaningWhat to do
grantedHeld now.Continue.
notDeterminedNever asked; a prompt can be shown.Ask from a user-initiated flow.
whenInUseOnlyForeground 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.
deniedRefused but re-askable. Android only.Show a rationale, then re-ask.
permanentlyDeniedOnly Settings can change it.Open the app's system Settings page.
restrictedDevice policy, MDM, or Screen Time blocks it.Explain the device restriction.
unavailableThe OS version or hardware does not expose it.Nothing to request.
notDeclaredThe 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-ready

It 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.