Skip to main content

React Native Permissions

The React Native SDK exposes the native iOS and Android permission engines through one typed API. There are two primary calls:

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

Platform declarations still matter

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[];
}
FieldMeaning
statusesNative detailed verdicts keyed by the canonical bucket name. Branch on this.
permissionsDeprecated three-key compatibility view. It collapses distinctions such as whenInUseOnly, notDeclared, and permanent denial.
unsupportedInput 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 conditionLegacy value
notDeterminedprompt
whenInUseOnly or unavailablegranted
Any other statedenied

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[];
}
FieldMeaning
requestedBuckets for which a prompt was actually raised.
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.
didRequesttrue if at least one prompt was raised.
androidPermissionsAndroid-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:

IntentiOS output nameAndroid output name
Foreground locationlocationWhenInUselocationWhenInUse
Background / Always locationlocationAlwayslocationAlways
Motion authorizationmotionFitnessactivityRecognition
BLE scan authorizationbluetoothbluetoothScan
Nearby Wi-FiunsupportednearbyWifiDevices
Tracking notificationunsupportednotifications

The input whitelist also accepts these compatibility aliases:

  • location expands to both location buckets.
  • motion and activity select motionFitness on iOS or activityRecognition on Android.
  • motionFitness and activityRecognition translate to the platform counterpart.
  • bluetooth and bluetoothScan translate 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:

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 is missing an Android manifest entry or iOS usage-description key.Fix and rebuild the host app.
whenInUseOnly is not background-ready

It 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