Help us understand the problem. What is going on with this article?

ExpoのPermissions周りを実機検証してみる

はじめに

Expoでの開発に限らず、iOSアプリでは一部のプライバシーに関わる機能を利用する際、その目的を説明する文言を設定する必要があります。設定した説明文は、ユーザーに権限を確認するためのダイアログに表示されます。

設定が必要な機能については下記のような記事が参考になりました。

iOS のユーザデータにアクセスするための Info.plist への許可設定まとめ
http://neos21.hatenablog.com/entry/2018/06/18/080000

[iOS 10] 各種ユーザーデータへアクセスする目的を記述することが必須になるようです
https://dev.classmethod.jp/smartphone/iphone/ios10-privacy-data-purpose-description/

Expoにおいてこの説明文(UsageDescription)はapp.jsonexpo.ios.infoPlistオブジェクトに追加することでカスタマイズできます。
デフォルトだと説明文が存在しない訳ではなく、英語、かつ目的を説明していない(Give ~ permissionという感じの)ため、App Storeに申請する際、下記の記事に書かれているようにリジェクトされてしまいます。

【React Native】【Expo】iOSのパーミッション要求ダイアログで審査リジェクトされた話とその対応
https://tech.maricuru.com/entry/2018/07/27/195921

私が過去リジェクトされた際には目的がどうのというより、言語を統一しなさいという感じだったと思うので、目的をちゃんと説明しつつアプリが対象としている言語で設定する必要があるようです。

フォトライブラリ(カメラロール)、カメラ、通知、位置情報のパーミッション設定をiOS/Androidの実機で確認してみたので、その流れを説明します。

画面を作成

このようにPermissionを確認するボタンだけを表示するような画面を作ります。
PNGイメージ 15.png

追加するモジュールはこのあたりです

  • expo-image-picker
  • expo-permissions
  • expo-location
  • expo-haptics これは全然関係ないけど使ってみたかったので。許可が得られた時に振動をつけてみます。

Permission取得の処理

PermissionButton.js
import React, { useState } from "react";
import {
  TouchableOpacity,
  Text,
  View,
  ActivityIndicator,
  Alert
} from "react-native";
import * as Haptics from "expo-haptics";
import { Linking } from "expo";
import * as Permissions from "expo-permissions";

function PermissionButton(props) {
  const { type, title } = props;
  const [isLoading, setLoading] = useState(false);
  const [isGranted, setGranted] = useState(false);

  return (
    <TouchableOpacity
      style={{
        width: "100%",
        height: 50,
        marginVertical: 12
      }}
      disabled={isLoading}
      onPress={async () => {
        if (isGranted) {
          props.onPress && props.onPress();
          return false;
        }
        setLoading(true);
        const { status } = await Permissions.askAsync(Permissions[type]);
        setLoading(false);
        if (status === "granted") {
          setGranted(true);
          await Haptics.notificationAsync(
            Haptics.NotificationFeedbackType.Success
          );
          setTimeout(props.onPress, 500);
        } else {
          // ユーザーが意図的にPermissionを不許可にしている場合
          // アラートを表示して設定画面に移動する
          Alert.alert(
            `${title}が無効になっています`,
            "設定画面へ移動しますか?",
            [
              {
                text: "キャンセル",
                style: "cancel"
              },
              {
                text: "設定する",
                onPress: () => {
                  Linking.openURL("app-settings:");
                }
              }
            ]
          );
        }
        console.log(type, status);
      }}
    >
      <View
        style={[
          props.style,
          {
            position: "relative",
            width: "100%",
            height: "100%",
            justifyContent: "center",
            alignItems: "center",
            backgroundColor: isGranted ? "#3cbe8d" : "#3cb3ff",
            borderRadius: 25
          }
        ]}
      >
        {isLoading ? (
          <ActivityIndicator
            color="#ffffff"
            style={{
              position: "absolute",
              width: "100%",
              height: "100%",
              top: 0,
              left: 15,
              justifyContent: "center",
              alignItems: "flex-start"
            }}
          />
        ) : null}
        {isGranted ? (
          <Text
            style={{
              position: "absolute",
              width: "100%",
              height: "100%",
              lineHeight: 50,
              top: -2,
              left: 15,
              justifyContent: "center",
              alignItems: "flex-start"
            }}
          >
            &#x1f44d;
          </Text>
        ) : null}
        <Text
          style={{
            color: "#ffffff",
            fontWeight: "bold"
          }}
        >
          {title}
        </Text>
      </View>
    </TouchableOpacity>
  );
}

export default PermissionButton;

このように、指定したPermissionをaskするボタンをコンポーネント化しておきました。

画面を作成し、各機能を確認する処理を追加

App.js
import React from "react";
import { Text, View, Alert } from "react-native";
import PermissionButton from "./PermissionButton";
import * as ImagePicker from "expo-image-picker";
import * as Location from "expo-location";
import { Notifications } from "expo";

export default function App() {
  return (
    <View
      style={{
        flex: 1,
        padding: 32,
        backgroundColor: "#fff",
        alignItems: "center",
        justifyContent: "center"
      }}
    >
      <Text
        style={{
          fontSize: 24,
          marginBottom: 16,
          fontWeight: "bold"
        }}
      >
        パーミッション確認
      </Text>
      <PermissionButton
        type="CAMERA_ROLL"
        title="フォトライブラリ(カメラロール)"
        onPress={async () => {
          try {
            const result = await ImagePicker.launchImageLibraryAsync();
            console.log(result);
          } catch (error) {
            Alert.alert(error.message);
          }
        }}
      />
      <PermissionButton
        type="CAMERA"
        title="カメラ"
        onPress={async () => {
          try {
            const result = await ImagePicker.launchCameraAsync();
            console.log(result);
          } catch (error) {
            Alert.alert(error.message);
          }
        }}
      />
      <PermissionButton
        type="NOTIFICATIONS"
        title="PUSH通知"
        onPress={async () => {
          try {
            const token = await Notifications.getExpoPushTokenAsync();
            console.log("token", token);
            const res = await fetch("https://expo.io/--/api/v2/push/send", {
              method: "POST",
              headers: {
                Accept: "application/json",
                "Content-type": "application/json"
              },
              body: JSON.stringify({
                to: token,
                title: "通知テスト",
                body: "ここにbodyが入ります",
                _displayInForeground: true
              })
            });
            console.log(res);
          } catch (error) {
            Alert.alert(error.message);
          }
        }}
      />
      <PermissionButton
        type="LOCATION"
        title="位置情報"
        onPress={async () => {
          try {
            const { coords } = await Location.getCurrentPositionAsync();
            Alert.alert(
              "位置情報の取得に成功しました",
              `緯度:${coords.latitude}\n経度:${coords.longitude}`
            );
          } catch (error) {
            Alert.alert(error.message);
          }
        }}
      />
    </View>
  );
}

このように画面にボタンを配置して、各機能を確認するだけの処理を実装します。
通知に関しては送信が確認できればいいので、ExpoのAPIを直接叩いています。その際、iOSでもアプリ起動時に通知が表示されるように_displayInForeground: trueをオプションに指定しておきます。

app.jsonの編集

app.json
{
  "expo": {
    "name": "許可テスト",
    "slug": "permission-test",
    "privacy": "public",
    "sdkVersion": "36.0.0",
    "platforms": [
      "ios",
      "android",
      "web"
    ],
    "version": "1.0.0",
    "orientation": "portrait",
    "icon": "./assets/icon.png",
    "splash": {
      "image": "./assets/splash.png",
      "resizeMode": "contain",
      "backgroundColor": "#3cb3ff"
    },
    "updates": {
      "fallbackToCacheTimeout": 0
    },
    "assetBundlePatterns": [
      "**/*"
    ],
    "ios": {
      "supportsTablet": true,
      "bundleIdentifier": "バンドル名",
      "infoPlist": {
        "NSCameraUsageDescription": "[テスト文言]写真をアップロードするためにカメラを使用します",
        "NSPhotoLibraryUsageDescription": "[テスト文言]アカウント画像をアップロードするためにフォトライブラリを使用します",
        "NSLocationAlwaysUsageDescription": "[テスト文言]位置情報をアップロードします",
        "NSLocationUsageDescription": "[テスト文言]位置情報をアップロードします",
        "NSLocationWhenInUseUsageDescription": "[テスト文言]位置情報をアップロードします"
      }
    },
    "android": {
      "package": "パッケージ名",
      "permissions": [
        "ACCESS_COARSE_LOCATION",
        "ACCESS_FINE_LOCATION",
        "CAMERA",
        "READ_EXTERNAL_STORAGE",
        "WRITE_EXTERNAL_STORAGE",
        "VIBRATE",
      ]
    },
    "notification": {
      "icon": "./assets/notificationicon.png",
      "color": "#3cb3ff"
    }
  }
}

app.jsonはこんな感じで設定しました。
expo.ios.infoPlistに各UsageDescriptionを、expo.android.permissionsには必要なだけの機能を記載します。
一応expo.notificationでAndroid用の通知アイコンも設定しておきました。(詳しくはこちらの記事
expo.android.permissionsVIBRATEは、expo-hapticsを使うので入れています。

検証

Android

実機での検証はAndroidの方が圧倒的に楽なので先にやってみます。
Androidの場合も6.0から一部機能でダイアログが表示されますが、この文言は設定できないので、
適切なexpo.android.permissionsがあれば申請の際に気にする必要はありません。
Expo Clientでの確認で十分な気もしつつ、念の為スタンドアロンアプリとしてビルドし実機で確認します。

ビルド
$ expo build:android

ビルド後に表示されたURLからダウンロードし、
端末を繋げてからAndroid SDKのadb installコマンドでAPKファイルのパスを指定してインストールします。

インストール
$ adb install permission-test-xxxxxx.apk

各Permissionを確認してみます。

カメラ
android

フォトライブラリ
android

通知
通知の許可に関してはダイアログが出ず通りました。
問題なく送信され、通知アイコンの設定も上手くいきました。色とアイコン(鍵)の部分が独自デザインです。
android

位置情報
android

以上、Androidはこんな感じです。

iOS

iOSの場合、ios.infoPlistの内容はExpo Client上では反映されないため、スタンドアロンアプリとしてビルドしてみないと検証できません。
しかし、スタンドアロンアプリを実機で確認する場合、リリースビルドをApp StoreやTestFlight経由でインストールするしか方法がありません(少なくとも公式には)。

今回はTestFlightの内部テストで確認してみたいと思います。

途中までは通常のアプリ申請のフローと同じなので、
App Storeへの申請についての記事
iOS用にビルド(Apple Developer Programにメンバーシップ登録後)
セクションから

アプリ(ipaファイル)をApp Store Connectにアップロード
セクションまでと同じ手順でビルドからアップロードまでを行います。

Transporterでアップロードする場合はこんな感じに。
空白_Skitch_キャンバス.jpg

そしてアップロードが完了した後、App Store ConnectのTestFlightタブを見るとこんな感じです。
App_Store_Connect_2.jpg

何らかの処理をしているようなので、しばらく待ちます。数十分から一時間ほどかかります。。。
しばらくしてAppleからメールが届くかと思います。もう一度App Store Connectを見ると、

App_Store_Connect3.jpg

米国の輸出コンプライアンスへの確認が求められます。「!」アイコンから進み質問に答えてから、

App_Store_Connect4.jpg
App_Store_Connect5.jpg

テスターに自分を招待して、iOS端末にTestFlightをインストールして確認します。

PNGイメージ 14.png

各Permissionを確認してみます。

フォトライブラリ
PNGイメージ 24.png

"アプリ名"が〜を求めています」という見出しは端末の言語設定によってiOSが固定で入れている文言です。
その下の部分に、app.jsonで設定した説明文が表示されています。

カメラ
PNGイメージ 19.png
こちらもOK。

通知
PNGイメージ 21.png
PNGイメージ_22.jpg
通知の場合は、もともとOSで決められたものが表示されます。
PUSH通知も問題なく届きました。

位置情報
PNGイメージ 23.png
設定した説明文が表示されました。

再確認の方法

iOSの許可ダイアログは一回しか表示されず、「設定」から各アプリ・各機能へのアクセスを不許可にしても再度表示することはできません。
再度ダイアログを表示させるには、「設定」→「一般」→「リセット」→「位置情報とプライバシーをリセット」で全てのアプリのプライバシー設定ごとリセットするか、アプリを再インストールするしか方法は無いようです。

TestFlightではアプリをアンインストールしてからこのようにすぐ再インストールすることが可能ですので、App Store経由の場合よりは比較的簡単にリセットできるかと思います。
PNGイメージ 25.png

Androidの場合は「設定」→「アプリと通知」→アプリを選択し、「権限」から各機能の許可を外すことができます。
その場合はiOSと違って、リクエストした際には再び許可ダイアログが表示されます。
android

mildsummer
ReactNative周りの記事が多めです。
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした