Capacitor Permissions
The Capacitor SDK exposes one six-bucket permission model on iOS and Android:
| Call | What 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.
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
>;
}
| Field | Meaning |
|---|---|
statuses | Native detailed verdicts for all six buckets. Branch on state. |
permissions | Coarse Capacitor summary: granted, denied, prompt, or prompt-with-rationale. |
permissions.location | Compatibility 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>>;
}
| 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 the 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 field on the Capacitor result. Use result.requested.length > 0.
Permission buckets
type 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 input whitelist also accepts older and native-specific spellings:
location→locationWhenInUsebackgroundLocation→locationAlwaysactivity,activityRecognition, ormotionFitness→motionbluetoothScan→bluetooth
Output keys are always canonical. New code should use the six names in
DoorstepPermissionType.
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. | Deep-link to the app's Settings screen. |
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 is:
| Detailed condition | Coarse state |
|---|---|
granted | granted |
notDetermined | prompt |
Any other state with canRequest: true | prompt-with-rationale |
Any other state with canRequest: false | denied |
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_LOCATIONwhen 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-readyIt 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:
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.