index.js 6.3KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234
  1. // @flow
  2. import {
  3. AsyncStorage,
  4. NativeModules,
  5. PermissionsAndroid,
  6. Platform,
  7. // $FlowFixMe
  8. } from "react-native";
  9. const { RNPermissions } = NativeModules;
  10. export const ANDROID_PERMISSIONS = {
  11. ...PermissionsAndroid.PERMISSIONS,
  12. // Dangerous permissions not included in PermissionsAndroid (which might be unavailable)
  13. ACCEPT_HANDOVER: "android.permission.ACCEPT_HANDOVER",
  14. // @TODO Add more of them (https://developer.android.com/reference/android/Manifest.permission)
  15. };
  16. // function keyMirror<O: {}>(obj: O): $ObjMapi<O, <K>(k: K) => K> {
  17. // return Object.keys(obj).reduce((acc, key) => ({ ...acc, [key]: key }), {});
  18. // }
  19. export const IOS_PERMISSIONS = {
  20. BLUETOOTH_PERIPHERAL: "BLUETOOTH_PERIPHERAL",
  21. CALENDARS: "CALENDARS",
  22. CAMERA: "CAMERA",
  23. CONTACTS: "CONTACTS",
  24. FACE_ID: "FACE_ID",
  25. LOCATION_ALWAYS: "LOCATION_ALWAYS",
  26. LOCATION_WHEN_IN_USE: "LOCATION_WHEN_IN_USE",
  27. MEDIA_LIBRARY: "MEDIA_LIBRARY",
  28. MICROPHONE: "MICROPHONE",
  29. MOTION: "MOTION",
  30. NOTIFICATIONS: "NOTIFICATIONS",
  31. PHOTO_LIBRARY: "PHOTO_LIBRARY",
  32. REMINDERS: "REMINDERS",
  33. SIRI: "SIRI",
  34. SPEECH_RECOGNITION: "SPEECH_RECOGNITION",
  35. STOREKIT: "STOREKIT",
  36. };
  37. export const RESULTS = {
  38. GRANTED: "granted",
  39. DENIED: "denied",
  40. NEVER_ASK_AGAIN: "never_ask_again",
  41. UNAVAILABLE: "unavailable",
  42. };
  43. export type Permission =
  44. | $Keys<typeof ANDROID_PERMISSIONS>
  45. | $Keys<typeof IOS_PERMISSIONS>;
  46. export type PermissionStatus = $Values<typeof RESULTS>;
  47. export type Rationale = {|
  48. title: string,
  49. message: string,
  50. buttonPositive: string,
  51. buttonNegative?: string,
  52. buttonNeutral?: string,
  53. |};
  54. export type NotificationOption =
  55. | "badge"
  56. | "sound"
  57. | "alert"
  58. | "carPlay"
  59. | "criticalAlert"
  60. | "provisional";
  61. export type RequestConfig = {
  62. notificationOptions?: NotificationOption[],
  63. rationale?: Rationale,
  64. };
  65. const platformPermissions = Object.values(
  66. Platform.OS === "ios" ? IOS_PERMISSIONS : ANDROID_PERMISSIONS,
  67. );
  68. function assertValidPermission(permission: string) {
  69. if (!platformPermissions.includes(permission)) {
  70. const bulletsList = `• ${platformPermissions.join("\n• ")}`;
  71. const alertSentence = `Invalid ${
  72. Platform.OS
  73. } permission "${permission}". Must be one of:\n\n`;
  74. throw new Error(`${alertSentence}${bulletsList}`);
  75. }
  76. }
  77. function getUnavailablePermissions(permissions: string[]) {
  78. return Promise.all(
  79. permissions.map(p => RNPermissions.isPermissionAvailable(p)),
  80. ).then(availabilities =>
  81. availabilities.reduce((acc, available, i) => {
  82. return !available
  83. ? { ...acc, [permissions[i]]: RESULTS.UNAVAILABLE }
  84. : acc;
  85. }, {}),
  86. );
  87. }
  88. const requestedKey = "@RNPermissions:requested";
  89. async function getRequestedPermissions() {
  90. const requested = await AsyncStorage.getItem(requestedKey);
  91. return requested ? JSON.parse(requested) : [];
  92. }
  93. async function setRequestedPermissions(permissions: string[]) {
  94. const requested = await getRequestedPermissions();
  95. const dedup = [...new Set([...requested, ...permissions])];
  96. return AsyncStorage.setItem(requestedKey, JSON.stringify(dedup));
  97. }
  98. async function internalCheck(
  99. permission: Permission,
  100. ): Promise<PermissionStatus> {
  101. if (Platform.OS !== "android") {
  102. return RNPermissions.check(permission);
  103. }
  104. if (!(await RNPermissions.isPermissionAvailable(permission))) {
  105. return RESULTS.UNAVAILABLE;
  106. }
  107. if (await PermissionsAndroid.check(permission)) {
  108. return RESULTS.GRANTED;
  109. }
  110. if (!(await getRequestedPermissions()).includes(permission)) {
  111. return RESULTS.DENIED;
  112. }
  113. return (await NativeModules.PermissionsAndroid.shouldShowRequestPermissionRationale(
  114. permission,
  115. ))
  116. ? RESULTS.DENIED
  117. : RESULTS.NEVER_ASK_AGAIN;
  118. }
  119. async function internalRequest(
  120. permission: string,
  121. config: RequestConfig = {},
  122. ): Promise<PermissionStatus> {
  123. const { notificationOptions, rationale } = config;
  124. if (Platform.OS !== "android") {
  125. return RNPermissions.request(permission, { notificationOptions });
  126. }
  127. if (!(await RNPermissions.isPermissionAvailable(permission))) {
  128. return RESULTS.UNAVAILABLE;
  129. }
  130. const status = await PermissionsAndroid.request(permission, rationale);
  131. return setRequestedPermissions([permission]).then(() => status);
  132. }
  133. async function internalCheckMultiple(
  134. permissions: Permission[],
  135. ): Promise<{ [permission: Permission]: PermissionStatus }> {
  136. let available = permissions;
  137. let result = {};
  138. if (Platform.OS === "android") {
  139. result = await getUnavailablePermissions(permissions);
  140. const unavailable = Object.keys(result);
  141. available = permissions.filter(p => !unavailable.includes(p));
  142. }
  143. return Promise.all(available.map(p => internalCheck(p)))
  144. .then(statuses =>
  145. statuses.reduce((acc, status, i) => {
  146. return { ...acc, [available[i]]: status };
  147. }, {}),
  148. )
  149. .then(statuses => ({ ...result, ...statuses }));
  150. }
  151. async function internalRequestMultiple(
  152. permissions: Permission[],
  153. ): Promise<{ [permission: Permission]: PermissionStatus }> {
  154. if (Platform.OS !== "android") {
  155. const result = {};
  156. for (let i = 0; i < permissions.length; i++) {
  157. const permission = permissions[i];
  158. result[permission] = await internalRequest(permission); // once at the time
  159. }
  160. return result;
  161. }
  162. const result = await getUnavailablePermissions(permissions);
  163. const unavailable = Object.keys(result);
  164. const statuses = await PermissionsAndroid.requestMultiple(
  165. permissions.filter(p => !unavailable.includes(p)),
  166. ).then(statuses => ({ ...result, ...statuses }));
  167. return setRequestedPermissions(permissions).then(() => statuses);
  168. }
  169. export function openSettings(): Promise<boolean> {
  170. return RNPermissions.openSettings();
  171. }
  172. export function check(permission: Permission): Promise<PermissionStatus> {
  173. assertValidPermission(permission);
  174. return internalCheck(permission);
  175. }
  176. export function checkMultiple(
  177. permissions: Permission[],
  178. ): Promise<{ [permission: Permission]: PermissionStatus }> {
  179. permissions.forEach(assertValidPermission);
  180. return internalCheckMultiple(permissions);
  181. }
  182. export function request(
  183. permission: string,
  184. config: RequestConfig = {},
  185. ): Promise<PermissionStatus> {
  186. assertValidPermission(permission);
  187. return internalRequest(permission, config);
  188. }
  189. export function requestMultiple(
  190. permissions: Permission[],
  191. ): Promise<{ [permission: Permission]: PermissionStatus }> {
  192. permissions.forEach(assertValidPermission);
  193. return internalRequestMultiple(permissions);
  194. }