React Native Permissions
The React Native SDK exposes the native iOS and Android permission engines through one typed API. There are two primary calls:
| Call | What it does |
|---|---|
DoorstepAI.checkPermissions(permissions?) | Reads current state. Never prompts. |
DoorstepAI.requestPermissions(options?) | Requests exactly the whitelisted buckets and resolves after the prompt sequence settles. |
Use the detailed statuses map for decisions. The legacy permissions map is retained only for
applications written against the older three-state wrapper API.
The wrapper cannot add iOS usage descriptions or replace Android manifest entries at runtime. Add
the declarations in Installation → Platform-Specific Configuration
before requesting a bucket. A missing declaration is reported as notDeclared.
Check current state
static checkPermissions(
permissions?: DoorstepPermissionInput[]
): Promise<PermissionStatus>
static allPermissionBuckets(): DoorstepPermission[]
static checkPermissionsJson(
permissions?: DoorstepPermissionInput[]
): Promise<string>
Omit permissions to inspect every bucket available on the current platform, or pass a whitelist
to inspect only those buckets. Platform-only names with no counterpart are returned in
unsupported rather than silently discarded.
const { statuses, unsupported } = await DoorstepAI.checkPermissions([
'locationWhenInUse',
'locationAlways',
'motion',
'bluetooth',
'notifications',
]);
if (statuses.locationAlways?.state === 'whenInUseOnly') {
// Foreground location is held; background tracking and route geofencing are not ready.
}
console.log('Not available on this platform:', unsupported);
console.log(await DoorstepAI.checkPermissionsJson());
allPermissionBuckets() is synchronous and returns the wrapper's bucket names, not live device
state:
- iOS:
locationWhenInUse,locationAlways,motionFitness,bluetooth - Android:
locationWhenInUse,locationAlways,activityRecognition,bluetoothScan,nearbyWifiDevices,notifications
PermissionStatus
interface PermissionStatus {
statuses: Partial<Record<DoorstepPermission, DoorstepPermissionStatus>>;
permissions: Record<'location' | 'motion' | 'bluetooth', 'granted' | 'denied' | 'prompt'>;
unsupported: string[];
}
| Field | Meaning |
|---|---|
statuses | Native detailed verdicts keyed by the canonical bucket name. Branch on this. |
permissions | Deprecated three-key compatibility view. It collapses distinctions such as whenInUseOnly, notDeclared, and permanent denial. |
unsupported | Input names that have no bucket on the current platform, such as notifications on iOS. |
Each DoorstepPermissionStatus contains permission, state, osStatus, and canRequest.
osStatus is free-form diagnostic evidence; never branch on it.
The legacy three-key map is intentionally compatibility-only and has historical behavior:
| Detailed condition | Legacy value |
|---|---|
notDetermined | prompt |
whenInUseOnly or unavailable | granted |
| Any other state | denied |
On Android, its motion key is always granted because IMU collection itself needs no runtime
grant; inspect statuses.activityRecognition for the step/activity bucket. These lossy mappings
are why new integrations must branch on statuses.
Request permissions
static requestPermissions(
options?: DoorstepPermissionInput[] | PermissionRequestOptions
): Promise<PermissionRequestResult>
interface PermissionRequestOptions {
permissions?: DoorstepPermissionInput[];
onlyIfNotDetermined?: boolean; // iOS only; default true
timeoutSeconds?: number; // default 120
}
Both call shapes are supported:
await DoorstepAI.requestPermissions(['locationWhenInUse']);
await DoorstepAI.requestPermissions({
permissions: ['motion', 'bluetooth'],
timeoutSeconds: 120,
});
await DoorstepAI.requestPermissions(); // every bucket on this platform
await DoorstepAI.requestPermissions([]); // request nothing
The promise resolves after the user answers or the wait times out. A second request while one
is pending does not interleave prompts: Android rejects with E_PERMISSION_REQUEST_IN_FLIGHT; iOS
returns the requested buckets in deferred with an explanation.
PermissionRequestResult
interface PermissionRequestResult extends PermissionStatus {
requested: DoorstepPermission[];
alreadyDetermined: DoorstepPermission[];
unavailable: DoorstepPermission[];
deferred: Partial<Record<DoorstepPermission, string>>;
didRequest: boolean;
androidPermissions?: string[];
}
| Field | Meaning |
|---|---|
requested | Buckets for which a prompt was actually raised. |
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. |
didRequest | true if at least one prompt was raised. |
androidPermissions | Android-only diagnostic list of manifest permission strings handed to the OS. |
If didRequest is false, no prompt appeared. Inspect alreadyDetermined, unavailable, and
deferred; do not leave your UI waiting for a callback that will never arrive.
Permission buckets
DoorstepPermission is the union of both native SDKs' names:
| Intent | iOS output name | Android output name |
|---|---|---|
| Foreground location | locationWhenInUse | locationWhenInUse |
| Background / Always location | locationAlways | locationAlways |
| Motion authorization | motionFitness | activityRecognition |
| BLE scan authorization | bluetooth | bluetoothScan |
| Nearby Wi-Fi | unsupported | nearbyWifiDevices |
| Tracking notification | unsupported | notifications |
The input whitelist also accepts these compatibility aliases:
locationexpands to both location buckets.motionandactivityselectmotionFitnesson iOS oractivityRecognitionon Android.motionFitnessandactivityRecognitiontranslate to the platform counterpart.bluetoothandbluetoothScantranslate to the platform counterpart.
Use the explicit locationWhenInUse and locationAlways names in new code. The coarse
location alias expands to both, and the native planner will defer the Always request when it must
be a separate phase.
Permission states
Both native SDKs use the same eight state names:
| 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 is missing an Android manifest entry or iOS usage-description key. | Fix and rebuild the host app. |
whenInUseOnly is not background-readyIt means location can work while the app is in use, but locked-phone collection and route
geofencing cannot be relied on. Check statuses.locationAlways.state === 'granted' before enabling
background-dependent workflows.
Recommended staged flow
Keep foreground and background location in separate user-visible stages:
// Stage 1: the driver taps "Start shift".
const foreground = await DoorstepAI.requestPermissions([
'locationWhenInUse',
]);
// Stage 2: after your own explanation of background tracking.
if (foreground.statuses.locationWhenInUse?.state === 'granted') {
const background = await DoorstepAI.requestPermissions([
'locationAlways',
]);
if (background.statuses.locationAlways?.state !== 'granted') {
// Show Settings guidance or continue in foreground-only mode.
}
}
// Stage 3: request optional collection buckets when their purpose is visible.
await DoorstepAI.requestPermissions(['motion', 'bluetooth']);
Android 11+ refuses a background-location request bundled with the foreground request, so
locationAlways must be requested alone after foreground location is granted. On iOS, a first
Always request commonly settles at whenInUseOnly; iOS controls when it offers the later Always
upgrade, and the default requester does not repeatedly re-escalate an answered choice.
RootDoorstepAI and automatic prompting
RootDoorstepAI calls init, setApiKey, and the permission flow when it mounts. Its relevant
props are:
{
permissions?: DoorstepPermissionInput[];
requestBluetooth?: boolean; // deprecated compatibility prop
requestBackgroundLocation?: boolean; // default true
}
Use the component for the default bootstrap. Use the methods above instead when your product needs permission prompts tied to explicit onboarding actions or separate rationale screens.
Android-only helpers
These methods reject on iOS because there is no native counterpart:
DoorstepAI.getRequiredPermissions(includeBluetooth?): Promise<string[]>
DoorstepAI.hasRequiredPermissions(includeBluetooth?): Promise<boolean>
DoorstepAI.hasBackgroundLocationPermission(): Promise<boolean>
DoorstepAI.requestBackgroundLocationPermission(): Promise<boolean>
requestBackgroundLocationPermission() is equivalent to requesting only locationAlways on
Android and resolves true only when a request was issued. For portable code, prefer
requestPermissions(['locationAlways']), which works on both platforms and returns the complete
result model.
Forwarding Android results is optional but improves permanent-denial classification:
await DoorstepAI.notePermissionRequestResult(permissions, grantResults);
The same method is a no-op on iOS. Native Android hosts can instead forward directly from
MainActivity.onRequestPermissionsResult to the Kotlin SDK's
DoorstepAI.notePermissionRequestResult(...).
Troubleshooting
A bucket is in unsupported
The current platform has no counterpart. nearbyWifiDevices and notifications are expected to
be unsupported on iOS. This differs from unavailable, where the platform has a bucket but the
current OS version or hardware cannot provide it.
A bucket reports notDeclared
The user did not deny anything. Add the Android <uses-permission> entry or iOS
NS…UsageDescription key from Installation, rebuild,
and reinstall the host app.
Background tracking or route geofencing stops
Check statuses.locationAlways.state. whenInUseOnly is insufficient. On Android also confirm a
foreground service is running; set manualForeground: true only when the host already owns one.
The background request does not show a prompt
Read deferred.locationAlways. The usual causes are missing foreground location, attempting to
bundle Always with another askable Android bucket, or an iOS choice that is already answered and
must now be changed in Settings.
Next steps
- Usage & Integration — initialize the SDK and wire the delivery lifecycle.
- Installation — add the native declarations behind these buckets.
- Examples & Troubleshooting — complete React Native flows.