react-native-permissions icon indicating copy to clipboard operation
react-native-permissions copied to clipboard

Request permission is not resolving in Android 16 if the status is never_ask_again

Open kanthivel opened this issue 3 months ago • 7 comments

Before submitting a new issue

  • [x] I tested using the latest version of the library, as the bug might be already fixed.
  • [x] I tested using a supported version of react native.
  • [x] I checked for possible duplicate issues, with possible answers.

Bug summary

On Android 16, when using react-native-permissions (latest version) to check or request permissions, the promise does not resolve if the permission is in a blocked state.

  • Works fine for granted and denied states.

  • Hangs (no resolution) for blocked.

  • Issue reproduced with all permissions (check with ACCESS_FINE_LOCATION and CAMERA)

On Android 16, app must be moved foreground -> background → foreground to trigger callback in never_ask_again case

Android version: 16 Build number: BP41.250822.010

Library version

5.4.2

Environment info

System:
  OS: macOS 15.7
  CPU: (14) arm64 Apple M4 Pro
  Memory: 740.94 MB / 48.00 GB
  Shell:
    version: "5.9"
    path: /bin/zsh
Binaries:
  Node:
    version: 22.19.0
    path: /var/folders/hv/4xscjh9n527786cfrr112q_80000gn/T/yarn--1758535287705-0.9384704201405711/node
  Yarn:
    version: 1.22.22
    path: /var/folders/hv/4xscjh9n527786cfrr112q_80000gn/T/yarn--1758535287705-0.9384704201405711/yarn
  npm:
    version: 10.9.3
    path: /opt/homebrew/opt/node@22/bin/npm
  Watchman:
    version: 2025.09.15.00
    path: /opt/homebrew/bin/watchman
Managers:
  CocoaPods:
    version: 1.16.2
    path: /opt/homebrew/bin/pod
SDKs:
  iOS SDK:
    Platforms:
      - DriverKit 24.2
      - iOS 18.2
      - macOS 15.2
      - tvOS 18.2
      - visionOS 2.2
      - watchOS 11.2
  Android SDK:
    API Levels:
      - "35"
      - "36"
    Build Tools:
      - 35.0.0
      - 35.0.1
      - 36.0.0
    System Images:
      - android-35 | Pre-Release 16 KB Page Size Google Play ARM 64 v8a
      - android-36 | Google Play ARM 64 v8a
    Android NDK: Not Found
IDEs:
  Android Studio: 2024.3 AI-243.26053.27.2432.13536105
  Xcode:
    version: 16.2/16C5032a
    path: /usr/bin/xcodebuild
Languages:
  Java:
    version: 17.0.16
    path: /opt/homebrew/opt/openjdk@17/bin/javac
  Ruby:
    version: 2.6.10
    path: /usr/bin/ruby
npmPackages:
  "@react-native-community/cli":
    installed: 20.0.0
    wanted: 20.0.0
  react:
    installed: 19.1.0
    wanted: 19.1.0
  react-native:
    installed: 0.81.4
    wanted: 0.81.4
  react-native-macos: Not Found
npmGlobalPackages:
  "*react-native*": Not Found
Android:
  hermesEnabled: true
  newArchEnabled: true
iOS:
  hermesEnabled: true
  newArchEnabled: false

Steps to reproduce

Install react-native-permissions (latest).

Block location permission manually in app settings.

Run the following code:

import { request, PERMISSIONS } from "react-native-permissions";

const test = async () => { const result = await request(PERMISSIONS.ANDROID.ACCESS_FINE_LOCATION); console.log("result:", result); // never resolves if blocked };

Reproducible sample code

import React, { useState } from "react";
import { View, Text, Button, StyleSheet } from "react-native";
import RNPermissions, { PERMISSIONS, PermissionStatus } from "react-native-permissions";

const App = () => {
  const [status, setStatus] = useState<PermissionStatus | string>("Unknown");

  const requestLocationPermissionLib = async () => {
    try {
        const result = await RNPermissions.request(
          PERMISSIONS.ANDROID.ACCESS_FINE_LOCATION,
          {
            title: "Location Permission",
            message: "This app needs access to your location.",
            buttonNegative: "Cancel",
            buttonPositive: "OK",
          }
        );
        setStatus(result);
      } catch (err) {
      console.warn(err);
      setStatus("error");
    }
  };

  const checkLocationPermissionLib = async () => {
    try {
        const result = await RNPermissions.check(
          PERMISSIONS.ANDROID.ACCESS_FINE_LOCATION
        );
        setStatus(result);
      } catch (err) {
      console.warn(err);
      setStatus("error");
    }
  };

  return (
    <View style={styles.container}>
      <Text style={styles.title}>Location Permission Example</Text>
      <Text style={styles.status}>Current Status: {status}</Text>

      <View style={styles.buttonContainer}>
        <Button title="Check Permission lib" onPress={checkLocationPermissionLib} />
      </View>
      <View style={styles.buttonContainer}>
        <Button title="Request Permission lib" onPress={requestLocationPermissionLib} />
      </View>
    </View>
  );
};

const styles = StyleSheet.create({
  container: { flex: 1, justifyContent: "center", alignItems: "center", padding: 16, backgroundColor: "white" },
  title: { fontSize: 20, fontWeight: "bold", marginBottom: 20 },
  status: { fontSize: 16, marginBottom: 20 },
  buttonContainer: { marginVertical: 8, width: "80%" },
});

export default App;

kanthivel avatar Sep 22 '25 10:09 kanthivel

facebook/react-native#53887

kanthivel avatar Sep 23 '25 10:09 kanthivel

Getting the same on Android 16

dgreasi avatar Oct 01 '25 07:10 dgreasi

Same. It has to be Android 16 QPR1.

felipebaisi avatar Oct 01 '25 16:10 felipebaisi

Same on Android 16, react native 0.79.6

Brma1048 avatar Oct 08 '25 07:10 Brma1048

Same on Android 15, react native 0.80.1

AF-appointhq avatar Oct 09 '25 07:10 AF-appointhq

I think we need to wait for this fix https://github.com/facebook/react-native/pull/53898

Brma1048 avatar Oct 09 '25 08:10 Brma1048

FYI @kanthivel. This is my implementation for addressing the permission issue on Android.

Context

I encountered a “Not resolved” state when users denied the two system dialogs triggered by requestMultiple(). After the first two denials, the system dialog stops appearing entirely.

Implementation

public static async location(): Promise<void | boolean> {
  const checkSystemPermissions =
    await this.handleCheckSystemPermissionSetting([
      ActionLocation.UNIVERSAL_LOCATION,
    ]);

  if (checkSystemPermissions.length > 0) {
    this.handleOpenPermissionModalMultiple(checkSystemPermissions);
  } else {
    if (Platform.OS === 'ios') {
      return this._fnBatchImplIOS([
        {
          permissionId: PERMISSIONS.IOS.LOCATION_WHEN_IN_USE,
        },
      ]);
    } else if (Platform.OS === 'android') {
      return this._fnBatchImplAndroid([
        {
          permissionId: PermissionsAndroid.PERMISSIONS.ACCESS_COARSE_LOCATION,
          apiLevel: 23,
        },
        {
          permissionId: PermissionsAndroid.PERMISSIONS.ACCESS_FINE_LOCATION,
          apiLevel: 23,
        },
      ]);
    }
  }
}
private static async _fnBatchImplAndroid(
  batch: PermissionsBatch_ANDROID,
): Promise<void | boolean> {
  if (Platform.OS !== 'android') {
    return;
  }

  return new Promise((resolve, reject) => {
    PermissionsAndroid.requestMultiple(batch.map(i => i.permissionId)).then(
      (_result: { [key: string]: PermissionStatus }) => {
        const platformContants = (Platform as PlatformAndroidStatic).constants;
        const deniedPermissions: PermissionRejectError[] = [];

        const _fnCheckPermission = (permission: string): null | string => {
          if (_result[permission] === 'granted') {
            return null;
          }
          const rejectObject: PermissionRejectError = {
            os: Platform.OS,
            permissionType: this.mapPermissionToActionLocation(permission),
            reason: _result[permission],
            settingLevel: 'app',
            apiLevel: platformContants.Version,
          };
          deniedPermissions.push(rejectObject);
          return _result[permission];
        };

        for (const item of batch) {
          if (platformContants.Version >= item.apiLevel) {
            _fnCheckPermission(item.permissionId);
          }
        }

        if (deniedPermissions.length > 0) {
          reject(deniedPermissions); // Reject with all denied permissions
        } else {
          resolve(true);
        }
      },
    );
  });
}

Final Approach

After detecting that the user has denied the permission twice—and the system dialog will no longer appear—I switch to using check() to determine the permission state. If the permission is still denied and the system dialog has already appeared twice, the app displays our custom permission modal instead.

This results in a significantly faster, more predictable, and more consistent permission flow.

NedTran avatar Nov 21 '25 10:11 NedTran