feat: permissions touchups

This commit is contained in:
Nathan Walker
2022-02-28 12:42:22 -08:00
parent cbacda3e9e
commit 31c82478b7
17 changed files with 304 additions and 72 deletions

View File

@@ -1,3 +1,180 @@
# permissions
## @nativescript/permissions
This library was generated with [Nx](https://nx.dev).
Android and iOS permission handling with NativeScript.
* `npm install @nativescript/permissions`
Inspired by [react-native-permissions](https://github.com/yonahforst/react-native-permissions)
## API
### Permissions statuses
Promises resolve into ```[status:Status, always:boolean]``` where status is one of these statuses:
| Return value | Notes |
| -------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `authorized` | User has authorized this permission |
| `denied` | User has denied this permission at least once. On iOS this means that the user will not be prompted again. Android users can be prompted multiple times until they select 'Never ask me again' |
| `limited` | **iOS** - this means the permission is granted but with limitations |
| `restricted` | **iOS** - this means user is not able to grant this permission, either because it's not supported by the device or because it has been blocked by parental controls. **Android** - this means that the user has selected 'Never ask me again' while denying permission |
| `undetermined` | User has not yet been prompted with a permission dialog |
### Supported permissions types
The current supported permissions are:
| | Type | iOS | Android |
| ------------------ | ------------------- | --- | ------- |
| Location | `location` | ✔ | ✔ |
| Camera | `camera` | ✔ | ✔ |
| Microphone | `microphone` | ✔ | ✔ |
| Photos | `photo` | ✔ | ✔ |
| Contacts | `contacts` | ✔ | ✔ |
| Events | `event` | ✔ | ✔ |
| Bluetooth | `bluetooth` | ✔ | ✔(api >= 31) |
| Reminders | `reminder` | ✔ | ❌ |
| Push Notifications | `notification` | ✔ | ❌ |
| Background Refresh | `backgroundRefresh` | ✔ | ❌ |
| Speech Recognition | `speechRecognition` | ✔ | ❌ |
| Media Library | `mediaLibrary` | ✔ | ❌ |
| Motion Activity | `motion` | ✔ | ❌ |
| Storage | `storage` | ❌️ | ✔ |
| Phone Call | `callPhone` | ❌️ | ✔ |
| Read SMS | `readSms` | ❌️ | ✔ |
| Receive SMS | `receiveSms` | ❌️ | ✔ |
| Media Location | `mediaLocation` | ❌️ | ✔(api >= 29) |
| Bluetooth Scan | `bluetoothScan` | ❌️ | ✔(api >= 31) |
| Bluetooth Connect | `bluetoothConnect` | ❌️ | ✔(api >= 31) |
### Methods
| Method Name | Arguments | Notes |
| ------------------- | --------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `check()` | `type` | - Returns a promise with the permission status. See iOS Notes for special cases |
| `request()` | `type` | - Accepts any permission type except `backgroundRefresh`. If the current status is `undetermined`, shows the permission dialog and returns a promise with the resulting status. Otherwise, immediately return a promise with the current status. See iOS Notes for special cases |
| `checkMultiple()` | `[types]` | - Accepts an array of permission types and returns a promise with an object mapping permission types to statuses |
| `getTypes()` | _none_ | - Returns an array of valid permission types |
| `openSettings()` | _none_ | - _(iOS only - 8.0 and later)_ Switches the user to the settings page of your app |
| `canOpenSettings()` | _none_ | - _(iOS only)_ Returns a boolean indicating if the device supports switching to the settings page |
### iOS Notes
* Permission type `bluetooth` represents the status of the
`CBPeripheralManager`. Don't use this if only need `CBCentralManager`
* Permission type `location` accepts a second parameter for `request()` and
`check()`; the second parameter is a string, either `always` or `whenInUse`
(default).
* Permission type `notification` accepts a second parameter for `request()`. The
second parameter is an array with the desired alert types. Any combination of
`alert`, `badge` and `sound` (default requests all three).
* iOS 12+: The second parameter also takes this type inside of the array `providesAppNotificationSettings`.
* If you are not requesting mediaLibrary then you can remove MediaPlayer.framework from the xcode project
```js
import { Permissions } from '@nativescript/permissions';
// example
Permissions.check('location', { type: 'always' }).then(response => {
this.setState({ locationPermission: response[0] })
})
Permissions.request('location', { type: 'always' }).then(response => {
this.setState({ locationPermission: response[0] })
})
Permissions.request('notification', { type: ['alert', 'badge'] }).then(
response => {
this.setState({ notificationPermission: response[0] })
},
)
```
* You cannot request microphone permissions on the simulator.
* With Xcode 8, you now need to add usage descriptions for each permission you
will request. Open Xcode ➜ `Info.plist` ➜ Add a key (starting with "Privacy -
...") with your kit specific permission.
Example: If you need Contacts permission you have to add the key `Privacy -
Contacts Usage Description`.
<img width="338" alt="3cde3b44-7ffd-11e6-918b-63888e33f983" src="https://cloud.githubusercontent.com/assets/1440796/18713019/271be540-8011-11e6-87fb-c3828c172dfc.png">
#### App Store submission disclaimer
If you need to submit you application to the AppStore, you need to add to your
`Info.plist` all `*UsageDescription` keys with a string value explaining to the
user how the app uses this data. **Even if you don't use them**.
So before submitting your app to the App Store, make sure that in your
`Info.plist` you have the following keys:
```xml
<key>NSBluetoothPeripheralUsageDescription</key>
<string>Some description</string>
<key>NSCalendarsUsageDescription</key>
<string>Some description</string>
<key>NSCameraUsageDescription</key>
<string>Some description</string>
<key>NSLocationWhenInUseUsageDescription</key>
<string>Some description</string>
<key>NSPhotoLibraryAddUsageDescription</key>
<string>Some description</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>Some description</string>
<key>NSSpeechRecognitionUsageDescription</key>
<string>Some description</string>
<key>NSAppleMusicUsageDescription</key>
<string>Some description</string>
<key>NSMotionUsageDescription</key>
<string>Some description</string>
```
This is required because during the phase of processing in the App Store
submission, the system detects that you app contains code to request the
permission `X` but don't have the `UsageDescription` key and then it rejects the
build.
> Please note that it will only be shown to the users the usage descriptions of
> the permissions you really require in your app.
You can find more information about this issue in #46.
### Android Notes
* `check` and `request` also allows you to directly pass android permission(s) as a value or an array. This would allow to request any new permission without a required update of this plugin
* All required permissions also need to be included in the `AndroidManifest.xml`
file before they can be requested. Otherwise `request()` will immediately
return `denied`.
* You can request write access to any of these types by also including the
appropriate write permission in the `AndroidManifest.xml` file. Read more
[here](https://developer.android.com/guide/topics/security/permissions.html#normal-dangerous).
* Permissions are automatically accepted for **targetSdkVersion < 23** but you
can still use `check()` to check if the user has disabled them from Settings.
You might need to elevate the **targetSdkVersion** version in your
`build.gradle`:
```groovy
android {
compileSdkVersion 23 // ← set at least 23
buildToolsVersion "23.0.1" // ← set at least 23.0.0
defaultConfig {
minSdkVersion 16
targetSdkVersion 23 // ← set at least 23
// ...
```
## Troubleshooting
#### Q: iOS - App crashes as soon as I request permission
> A: Starting with Xcode 8, you need to add permission descriptions. See iOS
> notes for more details. Thanks to [@jesperlndk](https://github.com/jesperlndk)
> for discovering this.
#### Q: iOS - App crashes when I change permission from settings
> A: This is normal. iOS restarts your app when your privacy settings change.
> Just google "iOS crash permission change"

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,422 @@
import { AndroidActivityRequestPermissionsEventData, AndroidApplication, Application, Trace, ApplicationSettings } from '@nativescript/core';
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;
function getAndroidActivity() {
return Application.android.foregroundActivity || Application.android.startActivity;
}
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(() => ApplicationSettings.setBoolean(STORAGE_KEY + permission, true));
const getDidAskOnce = (permission: string) => Promise.resolve(!!ApplicationSettings.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 = getAndroidActivity();
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 = getAndroidActivity();
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);
Application.android.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[], options?: PermissionRequestOptions): 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 = getAndroidActivity();
for (let i = 0; i < permissions.length; i++) {
const perm = permissions[i];
const nativePerm = getNativePermissions(perm as PermissionsType, options)[0];
if (getAndroidSDK() < MARSHMALLOW) {
grantedPermissions[perm] = context.checkPermission(nativePerm, 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(nativePerm) === 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 = getAndroidActivity();
return new Promise((resolve, reject) => {
try {
const requestCode = mRequestCode++;
activity.requestPermissions(permissionsToCheck, requestCode);
Application.android.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 = getAndroidActivity();
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 = getAndroidActivity();
return new Promise<void>((resolve, reject) => {
const onActivityResultHandler = (data) => {
if (data.requestCode === 5140) {
Application.android.off(AndroidApplication.activityResultEvent, onActivityResultHandler);
resolve();
}
};
Application.android.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, options);
}
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;
}, {})
);
}
}

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

@@ -0,0 +1,22 @@
import type { PermissionStatus } from './common';
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]: PermissionStatus } : [PermissionStatus, boolean];
export class Permissions {
static canOpenSettings(): Promise<boolean>;
static openSettings(): Promise<boolean>;
static getTypes(): Permissions[];
static check<T = Permissions>(permission: T, options?: PermissionCheckOptions): Promise<PermissionResult<T>>;
static request<T = Permissions | Permissions[] | string[]>(permission: T, options?: PermissionRequestOptions): Promise<PermissionResult<T>>;
static checkMultiple<T = Permissions[]>(permissions: T): Promise<PermissionResult<T>>;
}

View File

@@ -0,0 +1,787 @@
import { Device, Trace } from '@nativescript/core';
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;
}, {})
);
}
}

View File

@@ -1,7 +1,7 @@
{
"name": "@nativescript/permissions",
"version": "8.2.0",
"description": "A JavaScript library providing an easy to use api for interacting with iOS and Android permission handling.",
"description": "Android and iOS permission handling with NativeScript.",
"sideEffects": false,
"main": "index",
"types": "index.d.ts",

View File

@@ -1,8 +1,17 @@
{
"root": "packages/permissions",
"sourceRoot": "packages/permissions/src",
"sourceRoot": "packages/permissions",
"projectType": "library",
"targets": {
"build": {
"executor": "@nrwl/workspace:run-commands",
"outputs": ["dist/packages"],
"options": {
"commands": ["npx rimraf dist/packages/permissions", "./node_modules/.bin/tsc -p packages/permissions/tsconfig.lib.json", "cp packages/permissions/index.d.ts dist/packages/permissions", "cp packages/permissions/package.json dist/packages/permissions", "cp packages/permissions/README.md dist/packages/permissions", "cp LICENSE dist/packages/permissions"],
"cwd": ".",
"parallel": false
}
},
"lint": {
"executor": "@nrwl/linter:eslint",
"outputs": ["{options.outputFile}"],

3
packages/permissions/references.d.ts vendored Normal file
View File

@@ -0,0 +1,3 @@
/// <reference path="../types-ios/src/lib/ios.d.ts" />
/// <reference path="../types-android/src/lib/android-31.d.ts" />
/// <reference path="../global-types.d.ts" />

View File

@@ -1 +0,0 @@
export * from './lib/permissions';

View File

@@ -1,16 +1,11 @@
{
"extends": "../../tsconfig.base.json",
"files": [],
"include": [],
"references": [
{
"path": "./tsconfig.lib.json"
}
],
"compilerOptions": {
"forceConsistentCasingInFileNames": true,
"strict": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true
}
"baseUrl": ".",
"paths": {
"@nativescript/core": ["../core/index.ts"]
}
},
"include": ["**/*.ts", "./references.d.ts"],
"exclude": ["dist", "__tests__"]
}

View File

@@ -1,10 +1,27 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "../../dist/out-tsc",
"noEmitOnError": true,
"noEmitHelpers": true,
"declaration": true,
"types": []
"noImplicitAny": false,
"noImplicitUseStrict": true,
"removeComments": false,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"diagnostics": true,
"sourceMap": true,
"inlineSourceMap": false,
"baseUrl": ".",
"outDir": "../../dist",
"types": ["node"],
"plugins": [
{
"transform": "../webpack/transformers/ns-transform-native-classes.ts",
"type": "raw"
}
]
},
"include": ["**/*.ts"],
"exclude": ["**/*.spec.ts"]
"exclude": ["**/*.spec.ts", "**/*.test.ts", "dist", "__tests__"],
"include": ["**/*.ts", "./references.d.ts"]
}