React Nativeアプリでカメラや位置情報へアクセスするには、OSレベルのパーミッション(権限)取得が必須です。iOS/Androidで仕組みが異なるうえ、OSバージョンごとの仕様変更も頻繁に発生するため、パーミッション管理はモバイル開発で最もつまずきやすい領域の1つです。

react-native-permissionsは、iOS・Android・Windowsのパーミッション操作を統一APIで扱えるライブラリです。GitHub Star数は4,300超、npm週間ダウンロード数は約50万件に達しており、React Nativeにおけるパーミッション管理のデファクトスタンダードとなっています。

react-native-permissionsの基本概念

ライブラリの役割と位置づけ

React Native本体にもAndroid向けのPermissionsAndroid APIが用意されていますが、iOS向けの公式パーミッションAPIは存在しません。react-native-permissionsはこのギャップを埋め、クロスプラットフォームで一貫した権限管理を実現します。

項目
最新安定版5.5.0(2026年2月リリース)
対応プラットフォームiOS 13.4以上 / Android 6.0以上 / Windows
対応React Native0.76.0以上(v5.0.0時点では0.73.0以上)
ライセンスMIT

パーミッションステータスの5状態

react-native-permissionsでは、すべてのパーミッションが以下の5つのステータスで表現されます。

ステータス意味具体例
UNAVAILABLEデバイスがその機能に非対応カメラ非搭載のデバイスでカメラ権限を確認
DENIED未リクエスト、またはリクエスト可能な拒否状態初回起動時やユーザーが「許可しない」を選択(再リクエスト可能)
GRANTEDユーザーが許可済み正常に機能を利用できる状態
LIMITED制限付きの許可iOS 14以降のフォトライブラリ「選択した写真」やiOS 18以降の連絡先
BLOCKED永続的に拒否され、システム設定からのみ変更可能Androidで「今後表示しない」にチェック後の拒否、iOSで一度拒否した状態

Androidの重要な仕様: check()checkMultiple()BLOCKEDを返しません。BLOCKEDかどうかを判定するにはrequest()の呼び出しが必要です。

インストールとプラットフォーム別セットアップ

パッケージのインストール

npm install react-native-permissions
# または
yarn add react-native-permissions

iOS側の設定

iOSでは、使用するパーミッションごとにネイティブハンドラをPodfileで選択的に組み込む仕組みになっています。未使用のパーミッション関連コードがバイナリに含まれるとApp Storeの審査でリジェクトされるため、この設定は非常に重要です。

Podfileの編集:

# Podfile先頭にヘルパー関数を追加
def node_require(script)
  require Pod::Executable.execute_command('node', ['-p',
    "require.resolve('#{script}', {paths: [process.argv[1]]})",
    __dir__]).strip
end

node_require('react-native/scripts/react_native_pods.rb')
node_require('react-native-permissions/scripts/setup.rb')

# 使用するパーミッションだけを指定(不要なものは削除)
setup_permissions([
  'Camera',
  'LocationWhenInUse',
  'Microphone',
  'Notifications',
  'PhotoLibrary',
])

setup.rbは指定されたパーミッションのみをRNPermissions.podspecに組み込むスクリプトです。必要なものだけを列挙してください。

Pod Install の実行:

cd ios && pod install

Info.plistへの使用理由の追加:

各パーミッションに対応するNS*UsageDescriptionキーをInfo.plistへ追記します。

<key>NSCameraUsageDescription</key>
<string>プロフィール写真の撮影とQRコード読み取りに使用します</string>
<key>NSLocationWhenInUseUsageDescription</key>
<string>近くの店舗検索と配達時間の算出に使用します</string>
<key>NSMicrophoneUsageDescription</key>
<string>音声メッセージの録音とビデオ通話に使用します</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>投稿やメッセージに添付する画像の選択に使用します</string>

Appleの審査では「なぜそのデータが必要か」を具体的に記述することが求められます。「カメラを使用するため」のような曖昧な文言はリジェクト対象となるため、必ず具体的なユースケースを含めてください。

Android側の設定

AndroidではAndroidManifest.xmlに使用するパーミッションを宣言するだけで完了します。iOSのようなPodfile設定は不要です。

<manifest xmlns:android="http://schemas.android.com/apk/res/android">
  <uses-permission android:name="android.permission.CAMERA" />
  <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
  <uses-permission android:name="android.permission.RECORD_AUDIO" />
  <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />

  <!-- Android 13以降のメディアアクセス -->
  <uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
  <uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />

  <!-- Android 14以降の選択的アクセス -->
  <uses-permission android:name="android.permission.READ_MEDIA_VISUAL_USER_SELECTED" />
</manifest>

Expo環境での設定

Expo(Managed Workflow)を使用している場合は、Config Pluginで設定できます。

app.json での設定例:

{
  "expo": {
    "plugins": [
      [
        "react-native-permissions",
        {
          "iosPermissions": ["Camera", "Microphone", "PhotoLibrary"]
        }
      ]
    ],
    "ios": {
      "infoPlist": {
        "NSCameraUsageDescription": "写真撮影に使用します",
        "NSMicrophoneUsageDescription": "音声録音に使用します",
        "NSPhotoLibraryUsageDescription": "画像選択に使用します"
      }
    },
    "android": {
      "permissions": [
        "android.permission.CAMERA",
        "android.permission.RECORD_AUDIO"
      ]
    }
  }
}

設定後にnpx expo prebuildを実行すると、ネイティブファイルが自動生成されます。

主要APIリファレンス

check — 現在のステータス確認

import { check, PERMISSIONS, RESULTS } from 'react-native-permissions';
import { Platform } from 'react-native';

const permission = Platform.select({
  ios: PERMISSIONS.IOS.CAMERA,
  android: PERMISSIONS.ANDROID.CAMERA,
})!;

const status = await check(permission);
// => 'unavailable' | 'denied' | 'granted' | 'limited' | 'blocked'

check()はダイアログを表示せず、現在の状態を取得するだけです。初回レンダリング時やUIの切り替え判定に使用します。

request — パーミッションのリクエスト

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

const status = await request(
  Platform.OS === 'ios'
    ? PERMISSIONS.IOS.CAMERA
    : PERMISSIONS.ANDROID.CAMERA
);

if (status === RESULTS.GRANTED) {
  // カメラ機能を有効化
}

request()はOSのパーミッションダイアログを表示し、ユーザーの応答結果を返します。Androidではrationale引数を渡すことで、システムダイアログの前に説明アラートを表示できます。

// Android向けの説明ダイアログ付きリクエスト
const status = await request(
  PERMISSIONS.ANDROID.CAMERA,
  {
    title: 'カメラへのアクセス',
    message: 'プロフィール写真の撮影にカメラを使用します',
    buttonPositive: '許可する',
    buttonNegative: 'キャンセル',
  }
);

checkMultiple / requestMultiple — 複数パーミッションの一括処理

import { requestMultiple, PERMISSIONS } from 'react-native-permissions';

const statuses = await requestMultiple([
  PERMISSIONS.ANDROID.CAMERA,
  PERMISSIONS.ANDROID.RECORD_AUDIO,
  PERMISSIONS.ANDROID.READ_MEDIA_IMAGES,
]);

console.log(statuses[PERMISSIONS.ANDROID.CAMERA]);      // => 'granted'
console.log(statuses[PERMISSIONS.ANDROID.RECORD_AUDIO]); // => 'denied'

ビデオ通話やメディア投稿など、複数権限が同時に必要な機能で使用します。

checkNotifications / requestNotifications — 通知パーミッション

通知は他のパーミッションと異なる専用APIで管理します。

import { checkNotifications, requestNotifications } from 'react-native-permissions';

// ステータスと詳細設定を取得
const { status, settings } = await checkNotifications();

// iOSではアラート・バッジ・サウンドの個別指定が可能
const result = await requestNotifications(['alert', 'badge', 'sound']);

openSettings — 設定画面への誘導

ユーザーが権限をブロックした場合、アプリ側からは再リクエストできません。openSettings()でOS設定画面を開き、手動で有効化してもらう必要があります。

import { openSettings } from 'react-native-permissions';

// アプリ設定画面を開く
await openSettings();

// 特定の設定画面を開く(通知設定など)
await openSettings('notifications');

iOS / Androidのパーミッションフロー比較

iOS/Androidではパーミッションの状態遷移が大きく異なります。この違いを理解しておかないと、プラットフォームごとに異なる不具合が発生します。

iOSのフロー

アプリ起動
  └─ check() で確認
       ├─ UNAVAILABLE → 機能非対応(ハードウェア不在など)
       ├─ DENIED → 未リクエスト状態。request() 可能
       │    └─ request() 実行
       │         ├─ GRANTED → 許可された
       │         ├─ LIMITED → 制限付き許可(写真の部分アクセスなど)
       │         └─ BLOCKED → 拒否された(以降はシステム設定からのみ変更可能)
       ├─ GRANTED → 許可済み
       ├─ LIMITED → 制限付き許可
       └─ BLOCKED → 設定画面で変更が必要

iOSではシステムダイアログはパーミッションの種類ごとに1回しか表示されません。ユーザーが拒否すると、以降はopenSettings()で設定画面へ誘導するしか方法がありません。

Androidのフロー

アプリ起動
  └─ check() で確認
       ├─ UNAVAILABLE → 機能非対応
       ├─ DENIED → 未リクエストまたは再リクエスト可能
       │    └─ request() 実行
       │         ├─ GRANTED → 許可された
       │         ├─ DENIED → 拒否された(再リクエスト可能)
       │         └─ BLOCKED → 「今後表示しない」で拒否された
       └─ GRANTED → 許可済み

Androidでは拒否されても再リクエストが可能ですが、ユーザーが「今後表示しない」をチェックして拒否するとBLOCKEDになります。また、check()BLOCKEDを返さないため、BLOCKEDかどうかはrequest()の結果で判定する点に注意が必要です。

プラットフォーム差異の早見表

挙動iOSAndroid
ダイアログ表示回数1回のみ複数回可能(「今後表示しない」選択まで)
check()BLOCKEDを返すか返す返さない
LIMITEDステータスあり(写真・連絡先)あり(Android 14以降の写真選択)
rationale引数無視されるシステムダイアログ前に説明アラートを表示
ワンタイムパーミッションなしAndroid 11以降で対応

実践的なカスタムHookの設計

パーミッション処理をコンポーネントから分離し、再利用可能なHookとして設計すると保守性が大きく向上します。

汎用パーミッションHook

import { useState, useEffect, useCallback } from 'react';
import {
  check,
  request,
  openSettings,
  Permission,
  PermissionStatus,
  RESULTS,
} from 'react-native-permissions';
import { AppState, AppStateStatus } from 'react-native';

interface UsePermissionReturn {
  status: PermissionStatus | null;
  isGranted: boolean;
  isBlocked: boolean;
  requestPermission: () => Promise<PermissionStatus>;
  goToSettings: () => Promise<void>;
}

export function usePermission(permission: Permission): UsePermissionReturn {
  const [status, setStatus] = useState<PermissionStatus | null>(null);

  const checkPermission = useCallback(async () => {
    const result = await check(permission);
    setStatus(result);
    return result;
  }, [permission]);

  const requestPermission = useCallback(async () => {
    const result = await request(permission);
    setStatus(result);
    return result;
  }, [permission]);

  const goToSettings = useCallback(async () => {
    await openSettings();
  }, []);

  // 初回マウント時にステータスを確認
  useEffect(() => {
    checkPermission();
  }, [checkPermission]);

  // 設定画面から戻ったときにステータスを再確認
  useEffect(() => {
    let currentState = AppState.currentState;
    const subscription = AppState.addEventListener(
      'change',
      (nextState: AppStateStatus) => {
        if (
          currentState.match(/inactive|background/) &&
          nextState === 'active'
        ) {
          checkPermission();
        }
        currentState = nextState;
      },
    );
    return () => subscription.remove();
  }, [checkPermission]);

  return {
    status,
    isGranted: status === RESULTS.GRANTED || status === RESULTS.LIMITED,
    isBlocked: status === RESULTS.BLOCKED,
    requestPermission,
    goToSettings,
  };
}

このHookのポイントはAppStateのリスナーです。ユーザーがopenSettings()で設定画面に遷移し、権限を変更してアプリに戻った際に、ステータスを自動的に再取得します。

カメラ機能での使用例

import React from 'react';
import { View, Text, Button, Alert, Platform } from 'react-native';
import { PERMISSIONS, RESULTS } from 'react-native-permissions';
import { usePermission } from './hooks/usePermission';

const cameraPermission = Platform.select({
  ios: PERMISSIONS.IOS.CAMERA,
  android: PERMISSIONS.ANDROID.CAMERA,
})!;

export function CameraScreen() {
  const { status, isGranted, isBlocked, requestPermission, goToSettings } =
    usePermission(cameraPermission);

  const handlePress = async () => {
    if (isGranted) {
      // カメラを起動
      return;
    }

    const result = await requestPermission();

    if (result === RESULTS.BLOCKED) {
      Alert.alert(
        'カメラへのアクセスが必要です',
        '設定画面からカメラへのアクセスを許可してください。',
        [
          { text: 'キャンセル', style: 'cancel' },
          { text: '設定を開く', onPress: goToSettings },
        ],
      );
    }
  };

  if (status === null) {
    return null; // ステータス確認中
  }

  return (
    <View>
      {isGranted ? (
        <Text>カメラが利用可能です</Text>
      ) : (
        <Button title="カメラを使用する" onPress={handlePress} />
      )}
    </View>
  );
}

複数パーミッションを一括管理するHook

ビデオ通話のようにカメラ・マイクを同時に必要とする場合は、複数パーミッションをまとめて管理するHookが便利です。

import { useState, useCallback, useEffect } from 'react';
import {
  checkMultiple,
  requestMultiple,
  Permission,
  PermissionStatus,
  RESULTS,
} from 'react-native-permissions';
import { AppState, AppStateStatus } from 'react-native';

export function useMultiplePermissions(permissions: Permission[]) {
  const [statuses, setStatuses] = useState<Record<string, PermissionStatus>>({});

  const checkAll = useCallback(async () => {
    const results = await checkMultiple(permissions);
    setStatuses(results);
    return results;
  }, [permissions]);

  const requestAll = useCallback(async () => {
    const results = await requestMultiple(permissions);
    setStatuses(results);
    return results;
  }, [permissions]);

  const allGranted = permissions.every(
    (p) => statuses[p] === RESULTS.GRANTED || statuses[p] === RESULTS.LIMITED,
  );

  const blockedPermissions = permissions.filter(
    (p) => statuses[p] === RESULTS.BLOCKED,
  );

  useEffect(() => {
    checkAll();
  }, [checkAll]);

  useEffect(() => {
    let currentState = AppState.currentState;
    const subscription = AppState.addEventListener(
      'change',
      (nextState: AppStateStatus) => {
        if (currentState.match(/inactive|background/) && nextState === 'active') {
          checkAll();
        }
        currentState = nextState;
      },
    );
    return () => subscription.remove();
  }, [checkAll]);

  return { statuses, checkAll, requestAll, allGranted, blockedPermissions };
}

Android 13以降の細分化メディアパーミッション

Android 13(API 33)で従来のREAD_EXTERNAL_STORAGEが廃止され、メディア種別ごとのパーミッションに分割されました。さらにAndroid 14(API 34)では「選択した写真のみ」のアクセスが追加されています。

バージョン別の対応パーミッション

Androidバージョンパーミッション動作
12以前(API 32以下)READ_EXTERNAL_STORAGE全メディアへのフルアクセス
13(API 33)READ_MEDIA_IMAGES / READ_MEDIA_VIDEO / READ_MEDIA_AUDIOメディア種別ごとのアクセス
14以降(API 34以上)READ_MEDIA_VISUAL_USER_SELECTEDユーザーが選択した写真・動画のみ

Androidバージョン別のパーミッション処理

import { Platform } from 'react-native';
import { requestMultiple, PERMISSIONS, RESULTS } from 'react-native-permissions';

async function requestMediaPermissions() {
  if (Platform.OS !== 'android') {
    // iOS の場合
    return request(PERMISSIONS.IOS.PHOTO_LIBRARY);
  }

  const apiLevel = Platform.Version;

  if (apiLevel >= 34) {
    // Android 14以降:選択的アクセス対応
    const statuses = await requestMultiple([
      PERMISSIONS.ANDROID.READ_MEDIA_IMAGES,
      PERMISSIONS.ANDROID.READ_MEDIA_VIDEO,
      PERMISSIONS.ANDROID.READ_MEDIA_VISUAL_USER_SELECTED,
    ]);
    return statuses;
  }

  if (apiLevel >= 33) {
    // Android 13:メディア種別ごとのパーミッション
    const statuses = await requestMultiple([
      PERMISSIONS.ANDROID.READ_MEDIA_IMAGES,
      PERMISSIONS.ANDROID.READ_MEDIA_VIDEO,
    ]);
    return statuses;
  }

  // Android 12以前
  return request(PERMISSIONS.ANDROID.READ_EXTERNAL_STORAGE);
}

Android 14でREAD_MEDIA_VISUAL_USER_SELECTEDを導入しないと、互換モードで動作します。互換モードではREAD_MEDIA_IMAGES/READ_MEDIA_VIDEOがワンタイムパーミッション(セッション限定の一時的許可)として扱われ、アプリを再起動するたびにユーザーが再許可する必要が生じます。

対応パーミッション一覧

iOS(20種類)

定数Info.plistキー用途
APP_TRACKING_TRANSPARENCYNSUserTrackingUsageDescription広告トラッキング(ATT)
BLUETOOTHNSBluetoothAlwaysUsageDescriptionBluetooth機器接続
CALENDARSNSCalendarsUsageDescriptionカレンダー読み書き
CALENDARS_WRITE_ONLYNSCalendarsWriteOnlyAccessUsageDescriptionカレンダー書き込みのみ
CAMERANSCameraUsageDescriptionカメラ撮影
CONTACTSNSContactsUsageDescription連絡先アクセス
FACE_IDNSFaceIDUsageDescriptionFace ID認証
LOCATION_WHEN_IN_USENSLocationWhenInUseUsageDescriptionフォアグラウンド位置情報
LOCATION_ALWAYSNSLocationAlwaysAndWhenInUseUsageDescriptionバックグラウンド位置情報
MEDIA_LIBRARYNSAppleMusicUsageDescriptionApple Music/メディアライブラリ
MICROPHONENSMicrophoneUsageDescriptionマイク録音
MOTIONNSMotionUsageDescriptionモーション・フィットネスデータ
PHOTO_LIBRARYNSPhotoLibraryUsageDescriptionフォトライブラリ読み取り
PHOTO_LIBRARY_ADD_ONLYNSPhotoLibraryAddUsageDescriptionフォトライブラリ追加のみ
REMINDERSNSRemindersUsageDescriptionリマインダーアクセス
SIRINSSiriUsageDescriptionSiri連携
SPEECH_RECOGNITIONNSSpeechRecognitionUsageDescription音声認識

Android(主要なもの)

定数AndroidManifestの宣言用途
CAMERAandroid.permission.CAMERAカメラ撮影
RECORD_AUDIOandroid.permission.RECORD_AUDIOマイク録音
ACCESS_FINE_LOCATIONandroid.permission.ACCESS_FINE_LOCATION精密位置情報
ACCESS_COARSE_LOCATIONandroid.permission.ACCESS_COARSE_LOCATIONおおまかな位置情報
ACCESS_BACKGROUND_LOCATIONandroid.permission.ACCESS_BACKGROUND_LOCATIONバックグラウンド位置情報
READ_MEDIA_IMAGESandroid.permission.READ_MEDIA_IMAGES画像読み取り(API 33以降)
READ_MEDIA_VIDEOandroid.permission.READ_MEDIA_VIDEO動画読み取り(API 33以降)
READ_MEDIA_AUDIOandroid.permission.READ_MEDIA_AUDIO音声読み取り(API 33以降)
READ_MEDIA_VISUAL_USER_SELECTED同名選択的メディアアクセス(API 34以降)
POST_NOTIFICATIONSandroid.permission.POST_NOTIFICATIONS通知表示(API 33以降)
BLUETOOTH_CONNECTandroid.permission.BLUETOOTH_CONNECTBluetooth接続
BLUETOOTH_SCANandroid.permission.BLUETOOTH_SCANBluetoothスキャン
READ_CONTACTSandroid.permission.READ_CONTACTS連絡先読み取り
READ_CALENDARandroid.permission.READ_CALENDARカレンダー読み取り

Androidでは全44種類のパーミッション定数が用意されています。完全な一覧は公式リポジトリのREADMEで確認できます。

パーミッションUXのベストプラクティス

パーミッション取得に成功するかどうかは、技術実装だけでなくUX設計に大きく依存します。米国のUX調査機関Nielsen Norman Groupの研究によると、パーミッションリクエストのタイミングと説明の質が許可率を大幅に左右します。

原則1:機能の文脈でリクエストする

アプリ起動直後にすべてのパーミッションをまとめて要求するのは避けてください。ユーザーがその機能を実際に使おうとしたタイミングでリクエストすると、許可率が大幅に向上します。

  • カメラアイコンをタップしたとき → カメラパーミッションを要求
  • 「現在地から探す」ボタンを押したとき → 位置情報パーミッションを要求
  • 音声メッセージ録音を開始するとき → マイクパーミッションを要求

例外として、ナビゲーションアプリの位置情報のようにアプリのコア機能に不可欠なパーミッションは、オンボーディング時にリクエストしても問題ありません。

原則2:事前説明画面(プリパーミッション)を挟む

iOSではシステムダイアログがパーミッションの種類ごとに1回しか表示されないため、ユーザーがそこで拒否するとアプリ側から再リクエストできません。この制約に対処する手法が「ダブルダイアログパターン」です。

[1] アプリ独自の説明画面(理由と利点を提示)
    ├─ 「許可する」 → [2] OSのシステムダイアログを表示
    └─ 「あとで」  → システムダイアログは表示しない(権限を温存)

説明画面では「なぜ必要か」「ユーザーにとってどんな利点があるか」を1〜2文で伝えます。Tan et al.の研究(出典: Nielsen Norman Groupが引用)によると、最も説得力のある説明文と最も弱い説明文の間で許可率に81%の差が生じたと報告されています。説明文の内容がユーザーの判断に直結するため、具体的な利点を記載することが重要です。

原則3:拒否後もアプリを使える状態を維持する

パーミッションが拒否されてもアプリ全体がフリーズしたり、エラー画面で止まったりしてはなりません。該当機能だけを無効化し、他の機能は通常通り利用できる状態を維持してください。

// パーミッションがBLOCKEDの場合のフォールバック表示例
function PhotoPicker({ permission }: { permission: PermissionStatus }) {
  if (permission === RESULTS.BLOCKED) {
    return (
      <View style={styles.fallback}>
        <Text>写真へのアクセスが許可されていません</Text>
        <Button
          title="設定画面を開く"
          onPress={() => openSettings()}
        />
      </View>
    );
  }

  // 通常のフォトピッカーUI
  return <PhotoGrid />;
}

原則4:しつこく再リクエストしない

一度拒否されたパーミッションを、画面遷移のたびに何度もリクエストするのはユーザー体験を著しく損ないます。再リクエストは、ユーザーが該当機能を明示的に操作したときだけに限定してください。

Jestでのパーミッションテスト

react-native-permissionsはビルトインのモックモジュールを提供しており、ユニットテストが容易に書けます。

モックのセットアップ

jest.setup.js:

jest.mock('react-native-permissions', () =>
  require('react-native-permissions/mock')
);

jest.config.js:

module.exports = {
  setupFiles: ['<rootDir>/jest.setup.js'],
};

ステータス別のテスト記述例

import { check, request, RESULTS } from 'react-native-permissions';
import { renderHook, act } from '@testing-library/react-hooks';
import { usePermission } from './usePermission';

describe('usePermission', () => {
  beforeEach(() => {
    jest.clearAllMocks();
  });

  it('初期状態でGRANTEDならisGrantedがtrueになる', async () => {
    (check as jest.Mock).mockResolvedValue(RESULTS.GRANTED);

    const { result, waitForNextUpdate } = renderHook(() =>
      usePermission('ios.permission.CAMERA')
    );

    await waitForNextUpdate();
    expect(result.current.isGranted).toBe(true);
  });

  it('DENIEDからrequestでGRANTEDに遷移する', async () => {
    (check as jest.Mock).mockResolvedValue(RESULTS.DENIED);
    (request as jest.Mock).mockResolvedValue(RESULTS.GRANTED);

    const { result, waitForNextUpdate } = renderHook(() =>
      usePermission('ios.permission.CAMERA')
    );

    await waitForNextUpdate();
    expect(result.current.isGranted).toBe(false);

    await act(async () => {
      await result.current.requestPermission();
    });

    expect(result.current.isGranted).toBe(true);
  });

  it('BLOCKEDの場合isBlockedがtrueになる', async () => {
    (check as jest.Mock).mockResolvedValue(RESULTS.BLOCKED);

    const { result, waitForNextUpdate } = renderHook(() =>
      usePermission('ios.permission.CAMERA')
    );

    await waitForNextUpdate();
    expect(result.current.isBlocked).toBe(true);
  });
});

E2Eテスト(Detox)でのパーミッション操作

Detoxを使ったE2Eテストでは、iOSはアプリ起動時の設定、AndroidはADBコマンドでパーミッション状態を制御します。

// iOS: アプリ起動時にパーミッションを事前設定
await device.launchApp({
  permissions: { camera: 'YES', location: 'always' },
});

// Android: ADBでパーミッションを付与/取り消し
execSync(
  `adb shell pm grant com.example.app android.permission.CAMERA`
);
execSync(
  `adb shell pm revoke com.example.app android.permission.CAMERA`
);

App Store / Google Playの審査で押さえるべきポイント

iOS: Guideline 5.1.1(Privacy)対策

App Store審査で最も頻出するリジェクト理由の1つがGuideline 5.1.1です。対策として以下を確認してください。

  • すべてのパーミッションにInfo.plistの使用理由を記載する: 具体的なユースケースを含めた1〜2文の説明を書く
  • 対応言語すべてにローカライズする: Usage Descriptionは端末の言語設定で表示されるため、多言語対応が必要
  • 未使用のパーミッション関連コードを含めない: Podfileのsetup_permissionsで必要なものだけを指定する
  • サードパーティSDKのパーミッション利用を把握する: Firebase、広告SDK等が暗黙的に要求するパーミッションもInfo.plistへの記載が必要
  • App Privacyの質問票を正確に記入する: App Store Connectの「Appのプライバシー」セクションで、収集するデータの種類と用途を正しく申告する

Android: Google Playポリシー対策

  • 不要なパーミッションをManifestから削除する: 使わないパーミッションが宣言されていると審査で指摘される可能性がある
  • READ_EXTERNAL_STORAGEをAPI 33以降で使用しない: READ_MEDIA_*系に移行する
  • バックグラウンド位置情報の正当性を示す: ACCESS_BACKGROUND_LOCATIONを使う場合、Google Playの審査フォームで具体的な利用理由を提出する必要がある

よくあるトラブルと解決策

1. iOSでcheck()がDENIEDを返し続ける

Podfileで対象パーミッションのsetup_permissionsへの追加を忘れているケースが最も多い原因です。pod install後にXcodeのBuild PhasesでPermissionハンドラが組み込まれているか確認してください。

2. AndroidのcheckでBLOCKEDが検出できない

仕様上、Androidのcheck()checkMultiple()BLOCKEDを返しません。BLOCKEDかどうかを判定するにはrequest()を呼ぶ必要があります。

// Androidでの正しいBLOCKED判定パターン
const status = await request(PERMISSIONS.ANDROID.CAMERA);
if (status === RESULTS.BLOCKED) {
  // 設定画面へ誘導
}

3. 設定画面から戻ってもUIが更新されない

openSettings()で設定画面に遷移後、アプリに戻ったときにパーミッション状態を再チェックしていないことが原因です。前述のカスタムHookのようにAppStateの変化を監視し、activeに戻ったタイミングでcheck()を再実行してください。

4. Android 13でストレージパーミッションが取得できない

Android 13以降ではREAD_EXTERNAL_STORAGEが非推奨です。メディアの種類に応じたREAD_MEDIA_IMAGESREAD_MEDIA_VIDEOREAD_MEDIA_AUDIOに移行してください。AndroidManifest.xmlでの宣言も変更する必要があります。

5. Expo prebuild後にパーミッションが動作しない

Config Pluginの設定後はnpx expo prebuild --cleanで一度クリーンビルドを実行してください。キャッシュされた古いネイティブコードが原因で設定が反映されないことがあります。

v5系のアップグレード時の注意点

react-native-permissions v4以前からv5系にアップグレードする際の主な変更点です。

変更内容詳細
最低動作要件の引き上げXcode 16、iOS 13.4以上、Android 6.0以上、React Native 0.73以上
AndroidネイティブコードのKotlin移行Java → Kotlinに移行。android/build.gradleの設定確認が必要
requestNotificationsrationale引数追加Android向けに説明ダイアログ表示のオプションが追加
iOS 18の連絡先LIMITED対応PERMISSIONS.IOS.CONTACTSLIMITEDを返す可能性がある
位置情報のエスカレーションiOSでLOCATION_WHEN_IN_USELOCATION_ALWAYSの段階的リクエストに対応

まとめ

react-native-permissionsを使うことで、iOS/Androidの複雑なパーミッション仕様を統一APIで管理できます。

実装時に特に意識すべきポイントは以下の3つです。

  1. プラットフォーム差異の把握: iOSはダイアログが1回限り、Androidはcheck()BLOCKEDを検出できないなど、同じAPIでも動作が異なる
  2. ユーザー体験の設計: 機能の文脈でリクエストする、事前説明画面を挟む、拒否後のフォールバックUIを用意する
  3. OSバージョン対応: Android 13以降のメディアパーミッション細分化やiOS 14以降のLIMITEDステータスなど、継続的なキャッチアップが求められる

パーミッション管理はアプリの信頼性とストア審査の合否に直結する重要な領域です。カスタムHookによる設計パターンやJestモックによるテストを活用し、堅牢な権限管理を実現してください。