feat: Permissions API

This commit is contained in:
Nathan Walker
2022-02-22 16:29:53 -08:00
parent 534486816a
commit 0def9137ed
13 changed files with 1336 additions and 26 deletions

View File

@@ -0,0 +1,8 @@
export enum PermissionStatus {
authorized = 'authorized',
denied = 'denied',
limited = 'limited',
restricted = 'restricted',
undetermined = 'undetermined',
never_ask_again = 'never_ask_again',
}

View File

@@ -0,0 +1,419 @@
import { Trace } from '../trace';
import { AndroidActivityRequestPermissionsEventData, AndroidApplication, android as androidApp } from '../application';
import { getBoolean, setBoolean } from '../application-settings';
import { PermissionStatus } from './common';
import { PermissionCheckOptions, PermissionsType, PermissionRationale, PermissionRequestOptions } from '.';
export * from './common';
let ANDROID_SDK = -1;
function getAndroidSDK() {
if (ANDROID_SDK === -1) {
ANDROID_SDK = android.os.Build.VERSION.SDK_INT;
}
return ANDROID_SDK;
}
const MARSHMALLOW = 23;
const ANDROIDQ = 29;
const ANDROIDS = 31;
const NativePermissionsTypes: PermissionsType[] = ['location', 'camera', 'mediaLocation', 'microphone', 'contacts', 'event', 'storage', 'photo', 'callPhone', 'readSms', 'receiveSms', 'bluetoothScan', 'bluetoothConnect', 'bluetooth'];
type NativePermissionsNames = typeof NativePermissionsTypes; // type Names = readonly ['Mike', 'Jeff', 'Ben']
type NativePermissions = NativePermissionsNames[number];
function getNativePermissions(permission: NativePermissions, options?) {
switch (permission) {
case 'location': {
const result = [android.Manifest.permission.ACCESS_FINE_LOCATION, android.Manifest.permission.ACCESS_COARSE_LOCATION];
if (getAndroidSDK() >= ANDROIDQ) {
const type = typeof options === 'string' ? options : options && options.type;
if (type === 'always') {
result.push(android.Manifest.permission.ACCESS_BACKGROUND_LOCATION);
}
}
return result;
}
case 'camera': {
return [android.Manifest.permission.CAMERA];
}
case 'mediaLocation': {
if (getAndroidSDK() >= ANDROIDQ) {
return [android.Manifest.permission.ACCESS_MEDIA_LOCATION];
}
break;
}
case 'microphone': {
return [android.Manifest.permission.RECORD_AUDIO];
}
case 'contacts': {
return [android.Manifest.permission.READ_CONTACTS];
}
case 'event': {
return [android.Manifest.permission.READ_CALENDAR];
}
case 'storage': {
return [android.Manifest.permission.WRITE_EXTERNAL_STORAGE, android.Manifest.permission.READ_EXTERNAL_STORAGE];
}
case 'photo': {
return [android.Manifest.permission.WRITE_EXTERNAL_STORAGE];
}
case 'callPhone': {
return [android.Manifest.permission.CALL_PHONE];
}
case 'readSms': {
return [android.Manifest.permission.READ_SMS];
}
case 'receiveSms': {
return [android.Manifest.permission.RECEIVE_SMS];
}
case 'bluetoothScan': {
if (getAndroidSDK() >= ANDROIDS) {
return [android.Manifest.permission.BLUETOOTH_SCAN];
}
break;
}
case 'bluetoothConnect': {
if (getAndroidSDK() >= ANDROIDS) {
return [android.Manifest.permission.BLUETOOTH_CONNECT];
}
break;
}
case 'bluetooth': {
if (getAndroidSDK() >= ANDROIDS) {
return [android.Manifest.permission.BLUETOOTH_ADVERTISE];
}
break;
}
}
return [];
}
const STORAGE_KEY = '@NSPermissions:didAskPermission:';
const setDidAskOnce = (permission: string) => Promise.resolve().then(() => setBoolean(STORAGE_KEY + permission, true));
const getDidAskOnce = (permission: string) => Promise.resolve(!!getBoolean(STORAGE_KEY + permission));
namespace PermissionsAndroid {
/**
* A list of specified "dangerous" permissions that require prompting the user
*/
// export const PERMISSIONS = {
// READ_CALENDAR: 'android.permission.READ_CALENDAR',
// WRITE_CALENDAR: 'android.permission.WRITE_CALENDAR',
// CAMERA: 'android.permission.CAMERA',
// READ_CONTACTS: 'android.permission.READ_CONTACTS',
// WRITE_CONTACTS: 'android.permission.WRITE_CONTACTS',
// GET_ACCOUNTS: 'android.permission.GET_ACCOUNTS',
// ACCESS_FINE_LOCATION: 'android.permission.ACCESS_FINE_LOCATION',
// ACCESS_COARSE_LOCATION: 'android.permission.ACCESS_COARSE_LOCATION',
// RECORD_AUDIO: 'android.permission.RECORD_AUDIO',
// READ_PHONE_STATE: 'android.permission.READ_PHONE_STATE',
// CALL_PHONE: 'android.permission.CALL_PHONE',
// READ_CALL_LOG: 'android.permission.READ_CALL_LOG',
// WRITE_CALL_LOG: 'android.permission.WRITE_CALL_LOG',
// ADD_VOICEMAIL: 'com.android.voicemail.permission.ADD_VOICEMAIL',
// USE_SIP: 'android.permission.USE_SIP',
// PROCESS_OUTGOING_CALLS: 'android.permission.PROCESS_OUTGOING_CALLS',
// BODY_SENSORS: 'android.permission.BODY_SENSORS',
// SEND_SMS: 'android.permission.SEND_SMS',
// RECEIVE_SMS: 'android.permission.RECEIVE_SMS',
// READ_SMS: 'android.permission.READ_SMS',
// RECEIVE_WAP_PUSH: 'android.permission.RECEIVE_WAP_PUSH',
// RECEIVE_MMS: 'android.permission.RECEIVE_MMS',
// READ_EXTERNAL_STORAGE: 'android.permission.READ_EXTERNAL_STORAGE',
// WRITE_EXTERNAL_STORAGE: 'android.permission.WRITE_EXTERNAL_STORAGE'
// };
export const RESULTS: { GRANTED: PermissionStatus; DENIED: PermissionStatus; NEVER_ASK_AGAIN: PermissionStatus } = {
GRANTED: PermissionStatus.authorized,
DENIED: PermissionStatus.denied,
NEVER_ASK_AGAIN: PermissionStatus.never_ask_again,
};
/**
* Returns a promise resolving to a boolean value as to whether the specified
* permissions has been granted
*
* See https://facebook.github.io/react-native/docs/permissionsandroid.html#check
*/
export async function check(permission: string | string[]) {
const context: android.content.Context = androidApp.foregroundActivity || androidApp.startActivity;
let result = true;
const granted = android.content.pm.PackageManager.PERMISSION_GRANTED;
if (!Array.isArray(permission)) {
permission = [permission];
}
if (getAndroidSDK() < MARSHMALLOW) {
permission.forEach((p) => (result = result && context.checkPermission(p, android.os.Process.myPid(), android.os.Process.myUid()) === granted));
} else {
permission.forEach((p) => (result = result && context.checkSelfPermission(p) === granted));
}
return result;
}
/**
* Prompts the user to enable a permission and returns a promise resolving to a
* string value indicating whether the user allowed or denied the request
*
* See https://facebook.github.io/react-native/docs/permissionsandroid.html#request
*/
export async function request(permission: string, rationale?: PermissionRationale): Promise<PermissionStatus> {
// if (rationale) {
// const shouldShowRationale = await shouldShowRequestPermissionRationale(permission);
// if (shouldShowRationale) {
// return new Promise((resolve, reject) => {
// NativeModules.DialogManagerAndroid.showAlert(rationale, () => reject(new Error('Error showing rationale')), () => resolve(requestPermission(permission)));
// });
// }
// }
return requestPermission(permission);
}
/**
* Prompts the user to enable multiple permissions in the same dialog and
* returns an object with the permissions as keys and strings as values
* indicating whether the user allowed or denied the request
*
* See https://facebook.github.io/react-native/docs/permissionsandroid.html#requestmultiple
*/
export function requestMultiple(permissions: string[]): Promise<{ [permission: string]: PermissionStatus }> {
return requestMultiplePermissions(permissions);
}
}
// PermissionsAndroid = new PermissionsAndroid();
let mRequestCode = 0;
function requestPermission(permission: string): Promise<PermissionStatus> {
const activity: android.app.Activity = androidApp.foregroundActivity || androidApp.startActivity;
if (getAndroidSDK() < MARSHMALLOW) {
return Promise.resolve(activity.checkPermission(permission, android.os.Process.myPid(), android.os.Process.myUid()) === android.content.pm.PackageManager.PERMISSION_GRANTED ? PermissionsAndroid.RESULTS.GRANTED : PermissionsAndroid.RESULTS.DENIED);
}
if (activity.checkSelfPermission(permission) === android.content.pm.PackageManager.PERMISSION_GRANTED) {
return Promise.resolve(PermissionsAndroid.RESULTS.GRANTED);
}
return new Promise((resolve, reject) => {
try {
const requestCode = mRequestCode++;
activity.requestPermissions([permission], requestCode);
androidApp.on(AndroidApplication.activityRequestPermissionsEvent, (args: AndroidActivityRequestPermissionsEventData) => {
if (args.requestCode === requestCode) {
if (args.grantResults.length > 0) {
if (args.grantResults.length > 0 && args.grantResults[0] === android.content.pm.PackageManager.PERMISSION_GRANTED) {
resolve(PermissionsAndroid.RESULTS.GRANTED);
} else {
if (activity.shouldShowRequestPermissionRationale(permission)) {
resolve(PermissionsAndroid.RESULTS.DENIED);
} else {
resolve(PermissionsAndroid.RESULTS.NEVER_ASK_AGAIN);
}
}
} else {
// it is possible that the permissions request interaction with the user is interrupted. In this case you will receive empty permissions and results arrays which should be treated as a cancellation.
reject();
}
}
});
} catch (e) {
reject(e);
}
});
}
async function requestMultiplePermissions(permissions: string[]): Promise<{ [permission: string]: PermissionStatus }> {
const grantedPermissions = {};
const permissionsToCheck = [];
let checkedPermissionsCount = 0;
if (Trace.isEnabled()) {
Trace.write(`requestMultiplePermissions ${permissions}`, Trace.categories.Permissions, Trace.messageType.info);
}
const context: android.content.Context = androidApp.foregroundActivity || androidApp.startActivity;
for (let i = 0; i < permissions.length; i++) {
const perm = permissions[i];
if (getAndroidSDK() < MARSHMALLOW) {
grantedPermissions[perm] = context.checkPermission(perm, android.os.Process.myPid(), android.os.Process.myUid()) === android.content.pm.PackageManager.PERMISSION_GRANTED ? PermissionsAndroid.RESULTS.GRANTED : PermissionsAndroid.RESULTS.DENIED;
checkedPermissionsCount++;
} else if (context.checkSelfPermission(perm) === android.content.pm.PackageManager.PERMISSION_GRANTED) {
grantedPermissions[perm] = PermissionsAndroid.RESULTS.GRANTED;
checkedPermissionsCount++;
} else {
permissionsToCheck.push(perm);
}
}
if (permissions.length === checkedPermissionsCount) {
return grantedPermissions;
}
const activity: android.app.Activity = androidApp.foregroundActivity || androidApp.startActivity;
return new Promise((resolve, reject) => {
try {
const requestCode = mRequestCode++;
activity.requestPermissions(permissionsToCheck, requestCode);
androidApp.on(AndroidApplication.activityRequestPermissionsEvent, (args: AndroidActivityRequestPermissionsEventData) => {
if (args.requestCode === requestCode) {
const results = args.grantResults;
for (let j = 0; j < permissionsToCheck.length; j++) {
const permission = permissionsToCheck[j];
if (results.length > j && results[j] === android.content.pm.PackageManager.PERMISSION_GRANTED) {
grantedPermissions[permission] = PermissionsAndroid.RESULTS.GRANTED;
} else {
if (activity.shouldShowRequestPermissionRationale(permission)) {
grantedPermissions[permission] = PermissionsAndroid.RESULTS.DENIED;
} else {
grantedPermissions[permission] = PermissionsAndroid.RESULTS.NEVER_ASK_AGAIN;
}
}
}
resolve(grantedPermissions);
}
// if (args.grantResults.length > 0 && args.grantResults[0] === android.content.pm.PackageManager.PERMISSION_GRANTED) {
// resolve(PermissionStatus.GRANTED);
// } else {
// if (activity.shouldShowRequestPermissionRationale(permission)) {
// resolve(PermissionStatus.DENIED);
// } else {
// resolve(PermissionStatus.NEVER_ASK_AGAIN);
// }
// }
});
} catch (e) {
reject(e);
}
});
}
function shouldShowRequestPermissionRationale(permission: string | string[]) {
if (getAndroidSDK() < MARSHMALLOW) {
return Promise.resolve(false);
}
const activity: android.app.Activity = androidApp.foregroundActivity || androidApp.startActivity;
try {
if (Array.isArray(permission)) {
return Promise.resolve(permission.reduce((accu, p) => accu && activity.shouldShowRequestPermissionRationale(p), true));
}
return Promise.resolve(activity.shouldShowRequestPermissionRationale(permission));
} catch (e) {
return Promise.reject(e);
}
}
export class Permissions {
static canOpenSettings() {
return Promise.resolve(true);
}
static openSettings() {
const activity = androidApp.foregroundActivity || androidApp.startActivity;
return new Promise<void>((resolve, reject) => {
const onActivityResultHandler = (data) => {
if (data.requestCode === 5140) {
androidApp.off(AndroidApplication.activityResultEvent, onActivityResultHandler);
resolve();
}
};
androidApp.on(AndroidApplication.activityResultEvent, onActivityResultHandler);
const intent = new android.content.Intent(android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
intent.setData(android.net.Uri.parse('package:' + activity.getPackageName()));
activity.startActivityForResult(intent, 5140);
});
}
static getTypes() {
return NativePermissionsTypes;
}
static async check(permission: PermissionsType, options?: PermissionCheckOptions): Promise<[PermissionStatus, boolean]> {
if (Trace.isEnabled()) {
Trace.write(`check ${permission}`, Trace.categories.Permissions, Trace.messageType.info);
}
const perms: string | string[] = getNativePermissions(permission, options);
if (!perms) {
if (Trace.isEnabled()) {
Trace.write(`${permission} is not a valid permission type on Android version`, Trace.categories.Permissions, Trace.messageType.warn);
}
return [PermissionStatus.authorized, true];
}
const isAuthorized = await PermissionsAndroid.check(perms);
if (isAuthorized) {
if (getAndroidSDK() >= ANDROIDQ && permission === 'location') {
const type = typeof options === 'string' ? options : options && options.type;
if (type === 'always') {
const backAuthorized = await PermissionsAndroid.check(android.Manifest.permission.ACCESS_BACKGROUND_LOCATION);
return [PermissionStatus.authorized, backAuthorized];
}
}
return [PermissionStatus.authorized, true];
}
return getDidAskOnce(permission).then((didAsk) => {
if (didAsk) {
return shouldShowRequestPermissionRationale(perms).then((shouldShow) => [shouldShow ? PermissionStatus.denied : PermissionStatus.restricted, true]);
}
return [PermissionStatus.undetermined, true];
});
}
static request(permission: PermissionsType | PermissionsType[] | string[], options?: PermissionRequestOptions): Promise<[PermissionStatus, boolean] | { [permission: string]: PermissionStatus }> {
if (Trace.isEnabled()) {
Trace.write(`request ${permission}`, Trace.categories.Permissions, Trace.messageType.info);
}
let types: string[] = [];
if (Array.isArray(permission)) {
permission.forEach((s) => {
if (s.startsWith('android.permission.')) {
types.push(s);
} else {
types.push(...getNativePermissions(s as PermissionsType, options));
}
});
} else {
if (permission.startsWith('android.permission.')) {
types.push(permission);
} else {
types = getNativePermissions(permission, options);
}
}
if (types.length === 0) {
return Promise.resolve([PermissionStatus.authorized, true]);
}
const rationale = typeof options === 'string' ? undefined : options && options.rationale;
if (types.length > 1) {
return requestMultiplePermissions(types);
}
return PermissionsAndroid.request(types[0], rationale).then((result) => {
// PermissionsAndroid.request() to native module resolves to boolean
// rather than string if running on OS version prior to Android M
if (typeof result === 'boolean') {
return [result ? PermissionStatus.authorized : PermissionStatus.denied, true];
}
if (Array.isArray(permission)) {
return Promise.all(permission.map(setDidAskOnce)).then(() => [result as PermissionStatus, true]);
}
return setDidAskOnce(permission).then(() => [result as PermissionStatus, true]);
});
}
static checkMultiple(permissions: PermissionsType[]) {
if (Trace.isEnabled()) {
Trace.write(`checkMultiple ${permissions}`, Trace.categories.Permissions, Trace.messageType.info);
}
return Promise.all(permissions.map((permission) => this.check(permission))).then((result) =>
result.reduce((acc, value, index) => {
const name = permissions[index];
acc[name] = value;
return acc;
}, {})
);
}
}

21
packages/core/permissions/index.d.ts vendored Normal file
View File

@@ -0,0 +1,21 @@
export * from './common';
export type PermissionsType = 'location' | 'camera' | 'microphone' | 'photo' | 'contacts' | 'event' | 'reminder' | 'bluetooth' | 'bluetoothConnect' | 'bluetoothScan' | 'notification' | 'backgroundRefresh' | 'speechRecognition' | 'mediaLocation' | 'mediaLibrary' | 'motion' | 'storage' | 'callPhone' | 'readSms' | 'receiveSms';
export interface PermissionRationale {
title: string;
message: string;
buttonPositive?: string;
buttonNegative?: string;
buttonNeutral?: string;
}
export type PermissionCheckOptions = string | { type: string };
export type PermissionRequestOptions = string | { type: string; rationale?: PermissionRationale };
export type PermissionResult<T> = T extends any[] ? { [k: string]: Status } : [Status, boolean];
export class Permissions {
static canOpenSettings(): Promise<boolean>;
static openSettings(): Promise<boolean>;
static getTypes(): Permissions[];
static check<T = Permissions>(permission: T, options?: CheckOptions): Promise<PermissionResult<T>>;
static request<T = Permissions | Permissions[] | string[]>(permission: T, options?: RequestOptions): Promise<PermissionResult<T>>;
static checkMultiple<T = Permissions[]>(permissions: T): Promise<PermissionResult<T>>;
}

View File

@@ -0,0 +1,788 @@
import { Device } from '../platform';
import { Trace } from '../trace';
import { PermissionStatus } from './common';
import { PermissionCheckOptions, PermissionsType, PermissionRequestOptions } from '.';
export * from './common';
export namespace PermissionsIOS {
namespace NSPLocation {
let status: PermissionStatus = PermissionStatus.undetermined;
function getStatusFromCLAuthorizationStatus(lStatus: CLAuthorizationStatus, type?: string): [PermissionStatus, boolean] {
let always = false;
switch (lStatus) {
case CLAuthorizationStatus.kCLAuthorizationStatusAuthorizedAlways:
always = true;
status = PermissionStatus.authorized;
break;
case CLAuthorizationStatus.kCLAuthorizationStatusAuthorizedWhenInUse:
status = PermissionStatus.authorized;
break;
case CLAuthorizationStatus.kCLAuthorizationStatusDenied:
status = PermissionStatus.denied;
break;
case CLAuthorizationStatus.kCLAuthorizationStatusRestricted:
status = PermissionStatus.restricted;
break;
default:
status = PermissionStatus.undetermined;
}
if (Trace.isEnabled()) {
Trace.write(`NSPLocation getStatusFromCLAuthorizationStatus ${status}`, Trace.categories.Permissions, Trace.messageType.info);
}
return [status, always];
}
export function getStatusForType(type?: string): [PermissionStatus, boolean] {
const status2 = CLLocationManager.authorizationStatus();
return getStatusFromCLAuthorizationStatus(status2, type);
}
let locationManager: CLLocationManager;
let locationManagerDelegate: CLLocationManagerDelegateImpl;
export type SubCLLocationManagerDelegate = Partial<CLLocationManagerDelegate>;
@NativeClass
export class CLLocationManagerDelegateImpl extends NSObject implements CLLocationManagerDelegate {
public static ObjCProtocols = [CLLocationManagerDelegate];
private subDelegates: SubCLLocationManagerDelegate[];
public addSubDelegate(delegate: SubCLLocationManagerDelegate) {
if (!this.subDelegates) {
this.subDelegates = [];
}
const index = this.subDelegates.indexOf(delegate);
if (index === -1) {
this.subDelegates.push(delegate);
}
}
public removeSubDelegate(delegate: SubCLLocationManagerDelegate) {
const index = this.subDelegates.indexOf(delegate);
if (index !== -1) {
this.subDelegates.splice(index, 1);
}
}
static new(): CLLocationManagerDelegateImpl {
return super.new() as CLLocationManagerDelegateImpl;
}
public initDelegate() {
this.subDelegates = [];
return this;
}
locationManagerDidChangeAuthorizationStatus(manager: CLLocationManager, status: CLAuthorizationStatus) {
this.subDelegates &&
this.subDelegates.forEach((d) => {
if (d.locationManagerDidChangeAuthorizationStatus) {
d.locationManagerDidChangeAuthorizationStatus(manager, status);
}
});
}
// locationManagerDidFailWithError(manager: CLLocationManager, error: NSError) {
// this.subDelegates &&
// this.subDelegates.forEach(d => {
// if (d.locationManagerDidFailWithError) {
// d.locationManagerDidFailWithError(manager, error);
// }
// });
// }
}
export function request(type): Promise<[PermissionStatus, boolean]> {
const status = getStatusForType(type);
if (Trace.isEnabled()) {
Trace.write(`NSPLocation request ${type}`, Trace.categories.Permissions, Trace.messageType.info);
}
if (status[0] === PermissionStatus.undetermined) {
return new Promise((resolve, reject) => {
if (!locationManager) {
locationManager = CLLocationManager.new();
}
if (!locationManagerDelegate) {
locationManagerDelegate = CLLocationManagerDelegateImpl.new().initDelegate();
locationManager.delegate = locationManagerDelegate;
}
const subD = {
locationManagerDidChangeAuthorizationStatus: (manager, status: CLAuthorizationStatus) => {
if (Trace.isEnabled()) {
Trace.write(`locationManagerDidChangeAuthorizationStatus ${status}`, Trace.categories.Permissions, Trace.messageType.info);
}
if (status !== CLAuthorizationStatus.kCLAuthorizationStatusNotDetermined) {
if (locationManagerDelegate) {
locationManagerDelegate.removeSubDelegate(subD);
locationManagerDelegate = null;
}
if (locationManager) {
locationManager.delegate = null;
locationManager = null;
}
const rStatus = getStatusFromCLAuthorizationStatus(status, type);
resolve(rStatus);
// } else {
// reject('kCLAuthorizationStatusNotDetermined');
}
},
};
locationManagerDelegate.addSubDelegate(subD);
try {
if (Trace.isEnabled()) {
Trace.write(`NSPLocation requestAuthorization ${type}`, Trace.categories.Permissions, Trace.messageType.info);
}
if (type === 'always') {
locationManager.requestAlwaysAuthorization();
} else {
locationManager.requestWhenInUseAuthorization();
}
} catch (e) {
reject(e);
if (locationManagerDelegate) {
locationManagerDelegate.removeSubDelegate(subD);
locationManagerDelegate = null;
}
if (locationManager) {
locationManager.delegate = null;
locationManager = null;
}
}
});
} else {
// if (CLLocationManager.authorizationStatus() === CLAuthorizationStatus.kCLAuthorizationStatusAuthorizedWhenInUse && type === 'always') {
// return Promise.resolve(PermissionStatus.denied);
// } else {
return Promise.resolve(status);
// }
}
}
}
namespace NSPBluetooth {
let status: PermissionStatus = PermissionStatus.undetermined;
export function getStatus(): [PermissionStatus, boolean] {
const status2 = CBPeripheralManager.authorizationStatus();
switch (status2) {
case CBPeripheralManagerAuthorizationStatus.Authorized:
status = PermissionStatus.authorized;
break;
case CBPeripheralManagerAuthorizationStatus.Denied:
status = PermissionStatus.denied;
break;
case CBPeripheralManagerAuthorizationStatus.Restricted:
status = PermissionStatus.restricted;
break;
default:
status = PermissionStatus.undetermined;
}
return [status, true];
}
export type SubCBPeripheralManagerDelegate = Partial<CBPeripheralManagerDelegate>;
@NativeClass
export class CBPeripheralManagerDelegateImpl extends NSObject implements CBPeripheralManagerDelegate {
public static ObjCProtocols = [CBPeripheralManagerDelegate];
private subDelegates: SubCBPeripheralManagerDelegate[];
public addSubDelegate(delegate: SubCBPeripheralManagerDelegate) {
const index = this.subDelegates.indexOf(delegate);
if (index === -1) {
this.subDelegates.push(delegate);
}
}
public removeSubDelegate(delegate: SubCBPeripheralManagerDelegate) {
const index = this.subDelegates.indexOf(delegate);
if (index !== -1) {
this.subDelegates.splice(index, 1);
}
}
static new(): CBPeripheralManagerDelegateImpl {
return super.new() as CBPeripheralManagerDelegateImpl;
}
public initDelegate(): CBPeripheralManagerDelegateImpl {
this.subDelegates = [];
return this;
}
peripheralManagerDidUpdateState(peripheralManager) {
this.subDelegates.forEach((d) => {
if (d.peripheralManagerDidUpdateState) {
d.peripheralManagerDidUpdateState(peripheralManager);
}
});
}
}
let peripheralManager: CBPeripheralManager;
export function request(): Promise<[PermissionStatus, boolean]> {
const status = getStatus();
if (status[0] === PermissionStatus.undetermined) {
return new Promise((resolve, reject) => {
if (!peripheralManager) {
peripheralManager = CBPeripheralManager.new();
peripheralManager.delegate = CBPeripheralManagerDelegateImpl.new().initDelegate();
}
const subD = {
peripheralManagerDidUpdateState: (peripheralManager) => {
if (peripheralManager) {
peripheralManager.stopAdvertising();
(peripheralManager.delegate as CBPeripheralManagerDelegateImpl).removeSubDelegate(subD);
peripheralManager.delegate = null;
peripheralManager = null;
}
// for some reason, checking permission right away returns denied. need to wait a tiny bit
setTimeout(() => {
resolve(getStatus());
}, 100);
},
};
(peripheralManager.delegate as CBPeripheralManagerDelegateImpl).addSubDelegate(subD);
try {
peripheralManager.startAdvertising(null);
} catch (e) {
reject(e);
}
});
} else {
return Promise.resolve(status);
}
}
}
namespace NSPAudioVideo {
let status: PermissionStatus = PermissionStatus.undetermined;
function typeFromString(value: string) {
if (value === 'audio') {
return AVMediaTypeAudio;
} else {
return AVMediaTypeVideo;
}
}
export function getStatus(type?: string): [PermissionStatus, boolean] {
const videoStatus = AVCaptureDevice.authorizationStatusForMediaType(typeFromString(type));
switch (videoStatus) {
case AVAuthorizationStatus.Authorized:
status = PermissionStatus.authorized;
break;
case AVAuthorizationStatus.Denied:
status = PermissionStatus.denied;
break;
case AVAuthorizationStatus.Restricted:
status = PermissionStatus.restricted;
break;
default:
status = PermissionStatus.undetermined;
}
return [status, true];
}
export function request(type): Promise<[PermissionStatus, boolean]> {
return new Promise((resolve, reject) => {
AVCaptureDevice.requestAccessForMediaTypeCompletionHandler(typeFromString(type), (granted) => resolve(getStatus(type)));
});
}
}
namespace NSPSpeechRecognition {
let status: PermissionStatus = PermissionStatus.undetermined;
export function getStatus(): [PermissionStatus, boolean] {
const speechStatus = SFSpeechRecognizer.authorizationStatus();
switch (speechStatus) {
case SFSpeechRecognizerAuthorizationStatus.Authorized:
status = PermissionStatus.authorized;
break;
case SFSpeechRecognizerAuthorizationStatus.Denied:
status = PermissionStatus.denied;
break;
case SFSpeechRecognizerAuthorizationStatus.Restricted:
status = PermissionStatus.restricted;
break;
default:
status = PermissionStatus.undetermined;
}
return [status, true];
}
export function request(): Promise<[PermissionStatus, boolean]> {
return new Promise((resolve) => {
SFSpeechRecognizer.requestAuthorization(() => resolve(getStatus()));
});
}
}
namespace NSPPhoto {
let status: PermissionStatus = PermissionStatus.undetermined;
export function getStatus(): [PermissionStatus, boolean] {
let photoStatus: PHAuthorizationStatus;
if (parseFloat(Device.osVersion) >= 14) {
photoStatus = PHPhotoLibrary.authorizationStatusForAccessLevel(PHAccessLevel.ReadWrite);
} else {
photoStatus = PHPhotoLibrary.authorizationStatus();
}
switch (photoStatus) {
case PHAuthorizationStatus.Authorized:
status = PermissionStatus.authorized;
break;
case PHAuthorizationStatus.Denied:
status = PermissionStatus.denied;
break;
case PHAuthorizationStatus.Restricted:
status = PermissionStatus.restricted;
break;
default:
status = PermissionStatus.undetermined;
}
return [status, true];
}
export function request(): Promise<[PermissionStatus, boolean]> {
return new Promise((resolve) => {
PHPhotoLibrary.requestAuthorization(() => resolve(getStatus()));
});
}
}
namespace NSPMotion {
let status: PermissionStatus = PermissionStatus.undetermined;
export function getStatus(): [PermissionStatus, boolean] {
if (status === PermissionStatus.undetermined) {
const cmStatus = CMMotionActivityManager.authorizationStatus as any as CMAuthorizationStatus;
switch (cmStatus) {
case CMAuthorizationStatus.Authorized:
status = PermissionStatus.authorized;
break;
case CMAuthorizationStatus.Denied:
status = PermissionStatus.denied;
break;
case CMAuthorizationStatus.Restricted:
status = PermissionStatus.restricted;
break;
}
}
return [status, true];
}
export function request(): Promise<[PermissionStatus, boolean]> {
if (status === PermissionStatus.undetermined) {
return new Promise((resolve) => {
let activityManager = CMMotionActivityManager.new();
let motionActivityQueue = NSOperationQueue.new();
if (Trace.isEnabled()) {
Trace.write(`NSPMotion request ${status}`, Trace.categories.Permissions, Trace.messageType.info);
}
activityManager.queryActivityStartingFromDateToDateToQueueWithHandler(NSDate.distantPast, new Date(), motionActivityQueue, (activities, error) => {
if (error) {
status = PermissionStatus.denied;
} else if (activities || !error) {
status = PermissionStatus.authorized;
}
if (Trace.isEnabled()) {
Trace.write(`NSPMotion got response ${status}`, Trace.categories.Permissions, Trace.messageType.info);
}
resolve([status, true]);
activityManager = null;
motionActivityQueue = null;
});
});
} else {
return Promise.resolve([status, true]);
}
}
}
namespace NSPMediaLibrary {
let status: PermissionStatus = PermissionStatus.undetermined;
export function getStatus(): [PermissionStatus, boolean] {
const mediaStatus = MPMediaLibrary.authorizationStatus();
switch (mediaStatus) {
case MPMediaLibraryAuthorizationStatus.Authorized:
status = PermissionStatus.authorized;
break;
case MPMediaLibraryAuthorizationStatus.Denied:
status = PermissionStatus.denied;
break;
case MPMediaLibraryAuthorizationStatus.Restricted:
status = PermissionStatus.restricted;
break;
default:
status = PermissionStatus.undetermined;
}
return [status, true];
}
export function request(): Promise<[PermissionStatus, boolean]> {
return new Promise((resolve) => {
MPMediaLibrary.requestAuthorization(() => resolve(getStatus()));
});
}
}
namespace NSPNotification {
let status: PermissionStatus = PermissionStatus.undetermined;
const NSPDidAskForNotification = 'NSPDidAskForNotification';
export async function getStatus(): Promise<[PermissionStatus, boolean]> {
const didAskForPermission = NSUserDefaults.standardUserDefaults.boolForKey(NSPDidAskForNotification);
let isEnabled = false;
const osVersion = parseFloat(Device.osVersion);
if (osVersion >= 10) {
isEnabled = (await new Promise<UNNotificationSettings>((resolve) => UNUserNotificationCenter.currentNotificationCenter().getNotificationSettingsWithCompletionHandler(resolve))) !== (UNAuthorizationOptionNone as any);
} else {
isEnabled = UIApplication.sharedApplication.currentUserNotificationSettings.types !== UIUserNotificationType.None;
}
if (isEnabled) {
status = PermissionStatus.authorized;
} else {
status = didAskForPermission ? PermissionStatus.denied : PermissionStatus.undetermined;
}
return [status, true];
}
export function request(types: UIUserNotificationType | UNAuthorizationOptions): Promise<[PermissionStatus, boolean]> {
const status = getStatus();
if (status[0] === PermissionStatus.undetermined) {
return new Promise((resolve, reject) => {
const observer = function () {
resolve(getStatus());
NSNotificationCenter.defaultCenter.removeObserver(observer);
};
NSNotificationCenter.defaultCenter.addObserverForNameObjectQueueUsingBlock(UIApplicationDidBecomeActiveNotification, null, null, observer);
const osVersion = parseFloat(Device.osVersion);
if (osVersion >= 10) {
UNUserNotificationCenter.currentNotificationCenter().requestAuthorizationWithOptionsCompletionHandler(types as UNAuthorizationOptions, (p1: boolean, error: NSError) => {
if (error) {
reject(error);
} else {
UIApplication.sharedApplication.registerForRemoteNotifications();
NSUserDefaults.standardUserDefaults.setBoolForKey(true, NSPDidAskForNotification);
NSUserDefaults.standardUserDefaults.synchronize();
}
});
} else {
const settings = UIUserNotificationSettings.settingsForTypesCategories(types as UIUserNotificationType, null);
UIApplication.sharedApplication.registerUserNotificationSettings(settings);
UIApplication.sharedApplication.registerForRemoteNotifications();
NSUserDefaults.standardUserDefaults.setBoolForKey(true, NSPDidAskForNotification);
NSUserDefaults.standardUserDefaults.synchronize();
}
});
} else {
return Promise.resolve(status);
}
}
}
namespace NSPContacts {
let status: PermissionStatus = PermissionStatus.undetermined;
export function getStatus(): [PermissionStatus, boolean] {
const contactStatus = CNContactStore.authorizationStatusForEntityType(CNEntityType.Contacts);
switch (contactStatus) {
case CNAuthorizationStatus.Authorized:
status = PermissionStatus.authorized;
break;
case CNAuthorizationStatus.Denied:
status = PermissionStatus.denied;
break;
case CNAuthorizationStatus.Restricted:
status = PermissionStatus.restricted;
break;
default:
status = PermissionStatus.undetermined;
}
return [status, true];
}
export function request(): Promise<[PermissionStatus, boolean]> {
return new Promise((resolve) => {
const contactStore = CNContactStore.new();
contactStore.requestAccessForEntityTypeCompletionHandler(CNEntityType.Contacts, () => resolve(getStatus()));
});
}
}
namespace NSPBackgroundRefresh {
let status: PermissionStatus = PermissionStatus.undetermined;
export function getStatus(): [PermissionStatus, boolean] {
const refreshStatus = UIApplication.sharedApplication.backgroundRefreshStatus;
switch (refreshStatus) {
case UIBackgroundRefreshStatus.Available:
status = PermissionStatus.authorized;
break;
case UIBackgroundRefreshStatus.Denied:
status = PermissionStatus.denied;
break;
case UIBackgroundRefreshStatus.Restricted:
status = PermissionStatus.restricted;
break;
default:
status = PermissionStatus.undetermined;
}
return [status, true];
}
export function request(): Promise<PermissionStatus> {
return new Promise((resolve) => {
const contactStore = CNContactStore.new();
contactStore.requestAccessForEntityTypeCompletionHandler(CNEntityType.Contacts, () => resolve(getStatus()[0]));
});
}
}
namespace NSPEvent {
let status: PermissionStatus = PermissionStatus.undetermined;
function typeFromString(value: string) {
if (value === 'reminder') {
return EKEntityType.Reminder;
} else {
return EKEntityType.Event;
}
}
export function getStatus(type?: string): [PermissionStatus, boolean] {
const eventStatus = EKEventStore.authorizationStatusForEntityType(typeFromString(type));
switch (eventStatus) {
case EKAuthorizationStatus.Authorized:
status = PermissionStatus.authorized;
break;
case EKAuthorizationStatus.Denied:
status = PermissionStatus.denied;
break;
case EKAuthorizationStatus.Restricted:
status = PermissionStatus.restricted;
break;
default:
status = PermissionStatus.undetermined;
}
return [status, true];
}
export function request(type?: string): Promise<[PermissionStatus, boolean]> {
return new Promise((resolve) => {
const aStore = EKEventStore.new();
aStore.requestAccessToEntityTypeCompletion(typeFromString(type), () => resolve(getStatus(type)));
});
}
}
export enum NSType {
Location = 'location',
Camera = 'camera',
Microphone = 'microphone',
Photo = 'photo',
Contacts = 'contacts',
Event = 'event',
Reminder = 'reminder',
Bluetooth = 'bluetooth',
Notification = 'notification',
BackgroundRefresh = 'backgroundRefresh',
NSPTypeSpeechRecognition = 'speechRecognition',
MediaLibrary = 'mediaLibrary',
Motion = 'motion',
}
export function openSettings() {
return new Promise((resolve, reject) => {
const center = NSNotificationCenter.defaultCenter;
const observer = function (notif) {
resolve(true);
center.removeObserver(observer);
};
center.addObserverForNameObjectQueueUsingBlock(UIApplicationDidBecomeActiveNotification, null, null, observer);
UIApplication.sharedApplication.openURL(NSURL.URLWithString(UIApplicationOpenSettingsURLString));
});
}
export function canOpenSettings() {
return Promise.resolve(UIApplicationOpenSettingsURLString !== null);
}
export async function getPermissionStatus(type, json): Promise<[PermissionStatus, boolean]> {
let status: [PermissionStatus, boolean];
if (Trace.isEnabled()) {
Trace.write(`getPermissionStatus ${type}`, Trace.categories.Permissions, Trace.messageType.info);
}
switch (type) {
case NSType.Location: {
// NSString *locationPermissionType = [RCTConvert NSString:json];
status = NSPLocation.getStatusForType(json);
break;
}
case NSType.Camera:
status = NSPAudioVideo.getStatus('video');
break;
case NSType.Microphone:
status = NSPAudioVideo.getStatus('audio');
break;
case NSType.Photo:
status = NSPPhoto.getStatus();
break;
case NSType.Contacts:
status = NSPContacts.getStatus();
break;
case NSType.Event:
status = NSPEvent.getStatus('event');
break;
case NSType.Reminder:
status = NSPEvent.getStatus('reminder');
break;
case NSType.Bluetooth:
status = NSPBluetooth.getStatus();
break;
case NSType.Notification:
status = await NSPNotification.getStatus();
break;
case NSType.BackgroundRefresh:
status = NSPBackgroundRefresh.getStatus();
break;
case NSType.NSPTypeSpeechRecognition:
status = NSPSpeechRecognition.getStatus();
break;
case NSType.MediaLibrary:
status = NSPMediaLibrary.getStatus();
break;
case NSType.Motion:
status = NSPMotion.getStatus();
break;
default:
break;
}
return status;
}
export function requestPermission(type, json): Promise<[PermissionStatus, boolean]> {
if (Trace.isEnabled()) {
Trace.write(`requestPermission ${type}`, Trace.categories.Permissions, Trace.messageType.info);
}
switch (type) {
case NSType.Location:
return NSPLocation.request(json);
case NSType.Camera:
return NSPAudioVideo.request('video');
case NSType.Microphone:
return NSPAudioVideo.request('audio');
case NSType.Photo:
return NSPPhoto.request();
case NSType.Contacts:
return NSPContacts.request();
case NSType.Event:
return NSPEvent.request('event');
case NSType.Reminder:
return NSPEvent.request('reminder');
case NSType.Bluetooth:
return NSPBluetooth.request();
case NSType.Notification:
let types: UIUserNotificationType;
const typeStrings: string[] = json;
const osVersion = parseFloat(Device.osVersion);
if (osVersion >= 10) {
if (typeStrings.indexOf('alert') !== -1) {
types = types | UNAuthorizationOptions.Alert;
}
if (typeStrings.indexOf('badge') !== -1) {
types = types | UNAuthorizationOptions.Badge;
}
if (typeStrings.indexOf('sound') !== -1) {
types = types | UNAuthorizationOptions.Sound;
}
if (typeStrings.indexOf('providesAppNotificationSettings') !== -1 && parseFloat(Device.osVersion) >= 12) {
types = types | UNAuthorizationOptions.ProvidesAppNotificationSettings;
}
} else {
if (typeStrings.indexOf('alert') !== -1) {
types = types | UIUserNotificationType.Alert;
}
if (typeStrings.indexOf('badge') !== -1) {
types = types | UIUserNotificationType.Badge;
}
if (typeStrings.indexOf('sound') !== -1) {
types = types | UIUserNotificationType.Sound;
}
}
return NSPNotification.request(types);
case NSType.NSPTypeSpeechRecognition:
return NSPSpeechRecognition.request();
case NSType.MediaLibrary:
return NSPMediaLibrary.request();
case NSType.Motion:
return NSPMotion.request();
default:
return Promise.reject('unknown');
}
}
}
const DEFAULTS = {
location: 'whenInUse',
notification: ['alert', 'badge', 'sound'],
};
type IOSPermissionTypes = `${PermissionsIOS.NSType}`;
const permissionTypes = Object.values(PermissionsIOS.NSType) as IOSPermissionTypes[];
type SingleResult = [PermissionStatus, boolean];
interface MultipleResult {
[k: string]: PermissionStatus;
}
type Result<T> = T extends any[] ? MultipleResult : SingleResult;
export class Permissions {
static canOpenSettings() {
return PermissionsIOS.canOpenSettings();
}
static openSettings() {
return PermissionsIOS.openSettings();
}
static getTypes() {
return permissionTypes;
}
static check(permission: IOSPermissionTypes, options?: PermissionCheckOptions): Promise<SingleResult> {
if (Trace.isEnabled()) {
Trace.write(`check ${permission}`, Trace.categories.Permissions, Trace.messageType.info);
}
if (permissionTypes.indexOf(permission) === -1) {
if (Trace.isEnabled()) {
Trace.write(`'${permission}' is not a valid permission type on iOS`, Trace.categories.Permissions, Trace.messageType.warn);
}
return Promise.resolve([PermissionStatus.authorized, true]);
}
let type: PermissionCheckOptions;
if (typeof options === 'string') {
type = options;
} else if (options && options.type) {
type = options.type;
}
return PermissionsIOS.getPermissionStatus(permission, type || DEFAULTS[permission]);
}
static async request<T extends IOSPermissionTypes | IOSPermissionTypes[]>(permission: T, options?: PermissionRequestOptions): Promise<Result<T>> {
if (Trace.isEnabled()) {
Trace.write(`request ${permission}`, Trace.categories.Permissions, Trace.messageType.info);
}
if (Array.isArray(permission)) {
const grantedPermissions: Result<IOSPermissionTypes[]> = {};
for (let index = 0; index < permission.length; index++) {
const res = await Permissions.request(permission[index], options);
grantedPermissions[permission[index]] = res[0];
}
return Promise.resolve(grantedPermissions as Result<T>);
}
if (permissionTypes.indexOf(permission) === -1) {
if (Trace.isEnabled()) {
Trace.write(`'${permission}' is not a valid permission type on iOS`, Trace.categories.Permissions, Trace.messageType.warn);
}
return Promise.resolve([PermissionStatus.authorized, true] as Result<T>);
}
//@ts-ignore
if (permission === 'backgroundRefresh') {
throw new Error('You cannot request backgroundRefresh');
}
let type: PermissionRequestOptions;
if (typeof options === 'string') {
type = options;
} else if (options && options.type) {
type = options.type;
}
//@ts-ignore
return PermissionsIOS.requestPermission(permission, type || DEFAULTS[permission]);
}
static checkMultiple(permissions: PermissionsType[]) {
return Promise.all(permissions.map((permission) => Permissions.check(<any>permission))).then((result) =>
result.reduce((acc, value, index) => {
const name = permissions[index];
acc[name] = value;
return acc;
}, {})
);
}
}