index.js 6.4KB

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