Skip to main content

Capacitor Permissions

The Capacitor SDK exposes one six-bucket permission model on iOS and Android:

CallWhat it does
DoorstepAIDropoffSDK.checkPermissions()Reads all six buckets. Never prompts.
DoorstepAIDropoffSDK.requestPermissions(options?)Requests exactly the optional whitelist and resolves after native permission handling settles.
DoorstepAIDropoffSDK.requestBackgroundLocationPermission(options?)Requests only background / Always location as the second location phase.

Use the detailed statuses map for decisions. The Capacitor-shaped permissions map is useful for UI, but deliberately collapses some 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. A missing declaration is reported as notDeclared.


Check current state

checkPermissions(): Promise<PermissionStatus>

The call always reports the six canonical buckets on both platforms and never prompts.

const { statuses, permissions } =
await DoorstepAIDropoffSDK.checkPermissions();

if (statuses.locationAlways.state === 'whenInUseOnly') {
// Foreground-only: locked-phone tracking and route geofencing are not ready.
}

console.log(permissions.locationWhenInUse); // coarse Capacitor state

PermissionStatus

interface PermissionStatus {
permissions: Record<
DoorstepPermissionType | 'location',
PermissionState
>;
statuses: Record<
DoorstepPermissionType,
DoorstepPermissionStatus
>;
}
FieldMeaning
statusesNative detailed verdicts for all six buckets. Branch on state.
permissionsCoarse Capacitor summary: granted, denied, prompt, or prompt-with-rationale.
permissions.locationCompatibility mirror of permissions.locationWhenInUse.

Each DoorstepPermissionStatus contains state, canRequest, and osStatus. osStatus is free-form native evidence for logs; never branch on it.

On iOS, nearbyWifiDevices and notifications report unavailable. They remain present so shared web UI does not need a different key set for each native platform.


Request permissions

requestPermissions(
options?: PermissionRequestOptions
): Promise<PermissionRequestResult>

interface PermissionRequestOptions {
permissions?: DoorstepPermissionTypeInput[];
force?: boolean; // iOS only
}

Omit permissions to request all six buckets, pass a list to request exactly that whitelist, or pass an empty list to request nothing:

await DoorstepAIDropoffSDK.requestPermissions();

await DoorstepAIDropoffSDK.requestPermissions({
permissions: ['locationWhenInUse'],
});

await DoorstepAIDropoffSDK.requestPermissions({ permissions: [] });

The promise resolves after the user answers or native handling settles. A second request while one is active rejects with PERMISSION_REQUEST_BUSY; an unknown bucket rejects with BAD_ARGS rather than silently requesting the wrong permission.

PermissionRequestResult

interface PermissionRequestResult extends PermissionStatus {
requested: DoorstepPermissionType[];
alreadyDetermined: DoorstepPermissionType[];
unavailable: DoorstepPermissionType[];
deferred: Partial<Record<DoorstepPermissionType, string>>;
}
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 the 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 field on the Capacitor result. Use result.requested.length > 0.


Permission buckets

type 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 input whitelist also accepts older and native-specific spellings:

  • locationlocationWhenInUse
  • backgroundLocationlocationAlways
  • activity, activityRecognition, or motionFitnessmotion
  • bluetoothScanbluetooth

Output keys are always canonical. New code should use the six names in DoorstepPermissionType.


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.Deep-link to the app's Settings screen.
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 is:

Detailed conditionCoarse state
grantedgranted
notDeterminedprompt
Any other state with canRequest: trueprompt-with-rationale
Any other state with canRequest: falsedenied

Background location

requestBackgroundLocationPermission(options?: {
force?: boolean; // iOS only
}): Promise<PermissionRequestResult>

This is requestPermissions with a fixed ['locationAlways'] whitelist. Call it only after foreground location is granted:

const foreground = await DoorstepAIDropoffSDK.requestPermissions({
permissions: ['locationWhenInUse'],
});

if (foreground.statuses.locationWhenInUse.state === 'granted') {
const background =
await DoorstepAIDropoffSDK.requestBackgroundLocationPermission();

if (background.statuses.locationAlways.state !== 'granted') {
// Show Settings guidance or continue in foreground-only mode.
}
}
  • Android 11+ refuses ACCESS_BACKGROUND_LOCATION when it is bundled with foreground location. Native therefore requests it alone and defers it while foreground location is missing.
  • iOS commonly answers a first Always request with When In Use authorization. The default policy does not repeatedly re-escalate an answered choice. { force: true } overrides that policy; use it 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'.


Recommended staged flow

Ask only when the purpose is visible to the driver:

// 1. Start shift: foreground location.
const foreground = await DoorstepAIDropoffSDK.requestPermissions({
permissions: ['locationWhenInUse'],
});

// 2. A later rationale screen: background / Always location.
if (foreground.statuses.locationWhenInUse.state === 'granted') {
await DoorstepAIDropoffSDK.requestBackgroundLocationPermission();
}

// 3. Optional collection buckets when your UI explains them.
await DoorstepAIDropoffSDK.requestPermissions({
permissions: ['motion', 'bluetooth'],
});

Do not request every bucket during app startup merely because the no-argument overload allows it. The whitelist exists so your onboarding can match each prompt to a visible feature or explanation.


Android permanent-denial evidence

Android cannot reliably distinguish a cancelled dialog from a permanent denial using status reads alone. The plugin remains conservative and reports re-askable denied unless it receives a real grant result. If your UI needs reliable permanentlyDenied classification, forward the host activity callback:

MainActivity.kt
import ai.doorstep.dropoffsdk.capacitor.DoorstepAIDropoffSDKPlugin

class MainActivity : BridgeActivity() {
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<out String>,
grantResults: IntArray
) {
DoorstepAIDropoffSDKPlugin.notePermissionRequestResult(
this,
permissions,
grantResults
)
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
}
}

An empty grantResults is treated as cancellation, not denial.


Troubleshooting

A bucket reports notDeclared

The user did not refuse anything. Add the corresponding Android <uses-permission> entry or iOS NS…UsageDescription key from Installation, then rebuild, sync, and reinstall the native app.

The background request does not show a prompt

Inspect result.deferred.locationAlways. Common causes are missing foreground location or an iOS choice that is already answered and must now be changed in Settings.

Background tracking or route geofencing stops

Check statuses.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.

Calls fail on web or during SSR

The plugin is native-only. Guard with Capacitor.isNativePlatform() and call it after Capacitor boots. Next.js calls belong in a 'use client' module after mount.

Next steps

  • Usage & Integration — initialize the plugin and wire the delivery lifecycle.
  • Installation — add the native declarations behind these buckets.
  • Examples — complete Capacitor flows and troubleshooting.