はじめに
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.json
のexpo.ios.infoPlist
オブジェクトに追加することでカスタマイズできます。
デフォルトだと説明文が存在しない訳ではなく、英語、かつ目的を説明していない(Give ~ permissionという感じの)ため、App Storeに申請する際、下記の記事に書かれているようにリジェクトされてしまいます。
【React Native】【Expo】iOSのパーミッション要求ダイアログで審査リジェクトされた話とその対応
https://tech.maricuru.com/entry/2018/07/27/195921
私が過去リジェクトされた際には目的がどうのというより、言語を統一しなさいという感じだったと思うので、目的をちゃんと説明しつつアプリが対象としている言語で設定する必要があるようです。
フォトライブラリ(カメラロール)、カメラ、通知、位置情報のパーミッション設定をiOS/Androidの実機で確認してみたので、その流れを説明します。
画面を作成
このようにPermissionを確認するボタンだけを表示するような画面を作ります。
追加するモジュールはこのあたりです
- expo-image-picker
- expo-permissions
- expo-location
- expo-haptics これは全然関係ないけど使ってみたかったので。許可が得られた時に振動をつけてみます。
Permission取得の処理
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"
}}
>
👍
</Text>
) : null}
<Text
style={{
color: "#ffffff",
fontWeight: "bold"
}}
>
{title}
</Text>
</View>
</TouchableOpacity>
);
}
export default PermissionButton;
このように、指定したPermissionをaskするボタンをコンポーネント化しておきました。
画面を作成し、各機能を確認する処理を追加
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
の編集
{
"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.permissions
のVIBRATE
は、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はこんな感じです。
iOS
iOSの場合、ios.infoPlist
の内容はExpo Client上では反映されないため、スタンドアロンアプリとしてビルドしてみないと検証できません。
しかし、スタンドアロンアプリを実機で確認する場合、リリースビルドをApp StoreやTestFlight経由でインストールするしか方法がありません(少なくとも公式には)。
今回はTestFlightの内部テストで確認してみたいと思います。
途中までは通常のアプリ申請のフローと同じなので、
App Storeへの申請についての記事の
iOS用にビルド(Apple Developer Programにメンバーシップ登録後)
セクションから
アプリ(ipaファイル)をApp Store Connectにアップロード
セクションまでと同じ手順でビルドからアップロードまでを行います。
Transporterでアップロードする場合はこんな感じに。
そしてアップロードが完了した後、App Store ConnectのTestFlightタブを見るとこんな感じです。
何らかの処理をしているようなので、しばらく待ちます。数十分から一時間ほどかかります。。。
しばらくしてAppleからメールが届くかと思います。もう一度App Store Connectを見ると、
米国の輸出コンプライアンスへの確認が求められます。「!」アイコンから進み質問に答えてから、
テスターに自分を招待して、iOS端末にTestFlightをインストールして確認します。
各Permissionを確認してみます。
「"アプリ名"が〜を求めています
」という見出しは端末の言語設定によってiOSが固定で入れている文言です。
その下の部分に、app.json
で設定した説明文が表示されています。
通知
通知の場合は、もともとOSで決められたものが表示されます。
PUSH通知も問題なく届きました。
再確認の方法
iOSの許可ダイアログは一回しか表示されず、「設定」から各アプリ・各機能へのアクセスを不許可にしても再度表示することはできません。
再度ダイアログを表示させるには、「設定」→「一般」→「リセット」→「位置情報とプライバシーをリセット」で全てのアプリのプライバシー設定ごとリセットするか、アプリを再インストールするしか方法は無いようです。
TestFlightではアプリをアンインストールしてからこのようにすぐ再インストールすることが可能ですので、App Store経由の場合よりは比較的簡単にリセットできるかと思います。
Androidの場合は「設定」→「アプリと通知」→アプリを選択し、「権限」から各機能の許可を外すことができます。
その場合はiOSと違って、リクエストした際には再び許可ダイアログが表示されます。