Skip to content

Commit

Permalink
Added ability to restrict to read-only for calendar permission on And…
Browse files Browse the repository at this point in the history
…roid (#320)

* docs(example): remove use of non-standard `trash` utility

`rm -fr` is standard, trash is not

* feat(android): add readOnly option to calendar access

Defaults to backwards-compatible read/write, with the same
permission key used in shared preferences

If used it access a new shared preferences key, and restricts
permission usage to READ_CALENDAR, so AndroidManifest.xml can have
the WRITE_CALENDAR permission removed

Updated docs and example to explain + show it works

* Update CHANGELOG.md

Co-authored-by: Max Thirouin <git@moox.io>
  • Loading branch information
mikehardy and MoOx authored Aug 11, 2020
1 parent de6d65c commit 1bb027b
Show file tree
Hide file tree
Showing 9 changed files with 115 additions and 39 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Changelog for `react-native-calendar-events`

## Unreleased

- Added ability to restrict to read-only permission on Android [#320](https://github.com/wmcmahan/react-native-calendar-events/pull/320) by [@mikehardy](https://github.com/mikehardy)

## 2.0.1 - 2020-08-01

- Fixed TypeScript definition for missing `requestPermissions` [#316](https://github.com/wmcmahan/react-native-calendar-events/pull/316) by [@wmcmahan](https://github.com/wmcmahan)
Expand Down
17 changes: 15 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,9 +113,10 @@ import RNCalendarEvents from "react-native-calendar-events";
### `checkPermissions`

Get calendar authorization status.
You may check for the default read/write access with no argument, or read-only access on Android by passing boolean true. iOS is always read/write.

```javascript
RNCalendarEvents.checkPermissions();
RNCalendarEvents.checkPermissions((readOnly = false));
```

Returns: **Promise**
Expand All @@ -127,8 +128,20 @@ Returns: **Promise**

Request calendar authorization. Authorization must be granted before accessing calendar events.

Note that to restrict to read-only usage on Android (iOS is always read/write) you will need to alter the included Android permissions
as the AndroidManifest.xml is merged during the Android build.

You do that by altering your AndroidManifest.xml to "remove" the WRITE_CALENDAR permission with an entry like so:

```xml
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
>
<uses-permission tools:node="remove" android:name="android.permission.WRITE_CALENDAR" />
```

```javascript
RNCalendarEvents.requestPermissions();
RNCalendarEvents.requestPermissions((readOnly = false));
```

> Android note: This is necessary for targeted SDK of >=23.
Expand Down
60 changes: 38 additions & 22 deletions android/src/main/java/com/calendarevents/RNCalendarEvents.java
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ public String getName() {
}

//region Calendar Permissions
private void requestCalendarReadWritePermission(final Promise promise)
private void requestCalendarPermission(boolean readOnly, final Promise promise)
{
Activity currentActivity = getCurrentActivity();
if (currentActivity == null) {
Expand All @@ -73,10 +73,11 @@ private void requestCalendarReadWritePermission(final Promise promise)
PermissionAwareActivity activity = (PermissionAwareActivity)currentActivity;
PERMISSION_REQUEST_CODE++;
permissionsPromises.put(PERMISSION_REQUEST_CODE, promise);
activity.requestPermissions(new String[]{
Manifest.permission.WRITE_CALENDAR,
Manifest.permission.READ_CALENDAR
}, PERMISSION_REQUEST_CODE, this);
String[] permissions = new String[]{Manifest.permission.WRITE_CALENDAR, Manifest.permission.READ_CALENDAR};
if (readOnly == true) {
permissions = new String[]{Manifest.permission.READ_CALENDAR};
}
activity.requestPermissions(permissions, PERMISSION_REQUEST_CODE, this);
}

@Override
Expand All @@ -99,15 +100,19 @@ public boolean onRequestPermissionsResult(int requestCode, String permissions[],
return permissionsPromises.size() == 0;
}

private boolean haveCalendarReadWritePermissions() {
private boolean haveCalendarPermissions(boolean readOnly) {
int writePermission = ContextCompat.checkSelfPermission(reactContext, Manifest.permission.WRITE_CALENDAR);
int readPermission = ContextCompat.checkSelfPermission(reactContext, Manifest.permission.READ_CALENDAR);

if (readOnly) {
return readPermission == PackageManager.PERMISSION_GRANTED;
}

return writePermission == PackageManager.PERMISSION_GRANTED &&
readPermission == PackageManager.PERMISSION_GRANTED;
}

private boolean shouldShowRequestPermissionRationale() {
private boolean shouldShowRequestPermissionRationale(boolean readOnly) {
Activity currentActivity = getCurrentActivity();

if (currentActivity == null) {
Expand All @@ -121,6 +126,9 @@ private boolean shouldShowRequestPermissionRationale() {

PermissionAwareActivity activity = (PermissionAwareActivity)currentActivity;

if (readOnly) {
return activity.shouldShowRequestPermissionRationale(Manifest.permission.READ_CALENDAR);
}
return activity.shouldShowRequestPermissionRationale(Manifest.permission.WRITE_CALENDAR);
}

Expand Down Expand Up @@ -1203,40 +1211,48 @@ private WritableNativeArray serializeAttendeeCalendar(Cursor cursor) {
}
// endregion

private String getPermissionKey(boolean readOnly) {
String permissionKey = "permissionRequested"; // default to previous key for read/write, backwards-compatible
if (readOnly) {
permissionKey = "permissionRequestedRead"; // new key for read-only permission requests
}
return permissionKey;
}

//region React Native Methods
@ReactMethod
public void checkPermissions(Promise promise) {
public void checkPermissions(boolean readOnly, Promise promise) {
SharedPreferences sharedPreferences = reactContext.getSharedPreferences(RNC_PREFS, ReactContext.MODE_PRIVATE);
boolean permissionRequested = sharedPreferences.getBoolean("permissionRequested", false);
boolean permissionRequested = sharedPreferences.getBoolean(getPermissionKey(readOnly), false);

if (this.haveCalendarReadWritePermissions()) {
if (this.haveCalendarPermissions(readOnly)) {
promise.resolve("authorized");
} else if (!permissionRequested) {
promise.resolve("undetermined");
} else if(this.shouldShowRequestPermissionRationale()) {
} else if(this.shouldShowRequestPermissionRationale(readOnly)) {
promise.resolve("denied");
} else {
promise.resolve("restricted");
}
}

@ReactMethod
public void requestPermissions(Promise promise) {
public void requestPermissions(boolean readOnly, Promise promise) {
SharedPreferences sharedPreferences = reactContext.getSharedPreferences(RNC_PREFS, ReactContext.MODE_PRIVATE);
SharedPreferences.Editor editor = sharedPreferences.edit();
editor.putBoolean("permissionRequested", true);
editor.putBoolean(getPermissionKey(readOnly), true);
editor.apply();

if (this.haveCalendarReadWritePermissions()) {
if (this.haveCalendarPermissions(readOnly)) {
promise.resolve("authorized");
} else {
this.requestCalendarReadWritePermission(promise);
this.requestCalendarPermission(readOnly, promise);
}
}

@ReactMethod
public void findCalendars(final Promise promise) {
if (this.haveCalendarReadWritePermissions()) {
if (this.haveCalendarPermissions(true)) {
try {
Thread thread = new Thread(new Runnable(){
@Override
Expand All @@ -1256,7 +1272,7 @@ public void run() {

@ReactMethod
public void saveCalendar(final ReadableMap options, final Promise promise) {
if (!this.haveCalendarReadWritePermissions()) {
if (!this.haveCalendarPermissions(false)) {
promise.reject("save calendar error", "unauthorized to access calendar");
return;
}
Expand All @@ -1280,7 +1296,7 @@ public void run() {

@ReactMethod
public void removeCalendar(final String CalendarID, final Promise promise) {
if (this.haveCalendarReadWritePermissions()) {
if (this.haveCalendarPermissions(false)) {
try {
Thread thread = new Thread(new Runnable(){
@Override
Expand All @@ -1301,7 +1317,7 @@ public void run() {
}
@ReactMethod
public void saveEvent(final String title, final ReadableMap details, final ReadableMap options, final Promise promise) {
if (this.haveCalendarReadWritePermissions()) {
if (this.haveCalendarPermissions(false)) {
try {
Thread thread = new Thread(new Runnable(){
@Override
Expand Down Expand Up @@ -1331,7 +1347,7 @@ public void run() {
@ReactMethod
public void findAllEvents(final Dynamic startDate, final Dynamic endDate, final ReadableArray calendars, final Promise promise) {

if (this.haveCalendarReadWritePermissions()) {
if (this.haveCalendarPermissions(true)) {
try {
Thread thread = new Thread(new Runnable(){
@Override
Expand All @@ -1353,7 +1369,7 @@ public void run() {

@ReactMethod
public void findById(final String eventID, final Promise promise) {
if (this.haveCalendarReadWritePermissions()) {
if (this.haveCalendarPermissions(true)) {
try {
Thread thread = new Thread(new Runnable(){
@Override
Expand All @@ -1375,7 +1391,7 @@ public void run() {

@ReactMethod
public void removeEvent(final String eventID, final ReadableMap options, final Promise promise) {
if (this.haveCalendarReadWritePermissions()) {
if (this.haveCalendarPermissions(false)) {
try {
Thread thread = new Thread(new Runnable(){
@Override
Expand Down
37 changes: 36 additions & 1 deletion example/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
StatusBar,
Button,
Alert,
Platform,
} from 'react-native';
import {Header, Colors} from 'react-native/Libraries/NewAppScreen';
import RNCalendarEvents from 'react-native-calendar-events';
Expand All @@ -33,7 +34,7 @@ const App: () => React$Node = () => {
)}
<View style={styles.body}>
<View style={styles.sectionContainer}>
<Text style={styles.sectionTitle}>Auth</Text>
<Text style={styles.sectionTitle}>Read/Write Auth</Text>
<Text style={styles.sectionDescription}>
<Button
title="Request auth"
Expand Down Expand Up @@ -64,6 +65,40 @@ const App: () => React$Node = () => {
/>
</Text>
</View>
{Platform.OS === 'android' && (
<View style={styles.sectionContainer}>
<Text style={styles.sectionTitle}>Read-Only Auth</Text>
<Text style={styles.sectionDescription}>
<Button
title="Request auth"
onPress={() => {
RNCalendarEvents.requestPermissions(true).then(
(result) => {
Alert.alert('Read-only Auth requested', result);
},
(result) => {
console.error(result);
},
);
}}
/>
<Text>{'\n'}</Text>
<Button
title="Check auth"
onPress={() => {
RNCalendarEvents.checkPermissions(true).then(
(result) => {
Alert.alert('Read-only Auth check', result);
},
(result) => {
console.error(result);
},
);
}}
/>
</Text>
</View>
)}
<View style={styles.sectionContainer}>
<Text style={styles.sectionTitle}>Calendars</Text>
<Text style={styles.sectionDescription}>
Expand Down
4 changes: 2 additions & 2 deletions example/ios/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -296,7 +296,7 @@ PODS:
- React-Core (= 0.63.2)
- React-cxxreact (= 0.63.2)
- React-jsi (= 0.63.2)
- RNCalendarEvents (2.0.0):
- RNCalendarEvents (2.0.1):
- React
- Yoga (1.14.0)
- YogaKit (1.18.1):
Expand Down Expand Up @@ -460,7 +460,7 @@ SPEC CHECKSUMS:
React-RCTText: 1b6773e776e4b33f90468c20fe3b16ca3e224bb8
React-RCTVibration: 4d2e726957f4087449739b595f107c0d4b6c2d2d
ReactCommon: a0a1edbebcac5e91338371b72ffc66aa822792ce
RNCalendarEvents: c2c2e113f384bd193886bd529cf91f836b9e5888
RNCalendarEvents: d2029b1ec739b5ffc379bf72e597ec0b166bd8e6
Yoga: 7740b94929bbacbddda59bf115b5317e9a161598
YogaKit: f782866e155069a2cca2517aafea43200b01fd5a

Expand Down
4 changes: 2 additions & 2 deletions example/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@
"version": "0.0.1",
"private": true,
"scripts": {
"clean": "trash ./node_modules ./ios/Pods",
"clean": "rm -fr ./node_modules ./ios/Pods",
"reinstall": "yarn clean && yarn",
"ios:dependencies": "bundle install",
"ios:dependencies:install": "cd ios && bundle exec pod install --repo-update",
"clean-modules": "trash ./node_modules/react-native-calendar-events/{example,node_modules}",
"clean-modules": "rm -fr ./node_modules/react-native-calendar-events/{example,node_modules}",
"prepare": "yarn clean-modules && yarn ios:dependencies && yarn ios:dependencies:install",
"android": "react-native run-android",
"ios": "react-native run-ios",
Expand Down
8 changes: 4 additions & 4 deletions src/index.android.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@ import { NativeModules, processColor } from "react-native";
const RNCalendarEvents = NativeModules.RNCalendarEvents;

export default {
async checkPermissions() {
return RNCalendarEvents.checkPermissions();
async checkPermissions(readOnly = false) {
return RNCalendarEvents.checkPermissions(readOnly);
},
async requestPermissions() {
return RNCalendarEvents.requestPermissions();
async requestPermissions(readOnly = false) {
return RNCalendarEvents.requestPermissions(readOnly);
},

async fetchAllEvents(startDate, endDate, calendars = []) {
Expand Down
14 changes: 10 additions & 4 deletions src/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -165,10 +165,16 @@ export type CalendarAccountSourceAndroid =
};

export default class ReactNativeCalendarEvents {
/** Get calendar authorization status. */
static checkPermissions(): Promise<AuthorizationStatus>;
/** Request calendar authorization. Authorization must be granted before accessing calendar events. */
static requestPermissions(): Promise<AuthorizationStatus>;
/**
* Get calendar authorization status.
* @param readOnly - optional, default false, use true to check for calendar read only vs calendar read/write. Android-specific, iOS is always read/write
*/
static checkPermissions(readOnly?: boolean): Promise<AuthorizationStatus>;
/**
* Request calendar authorization. Authorization must be granted before accessing calendar events.
* @param readOnly - optional, default false, use true to check for calendar read only vs calendar read/write. Android-specific, iOS is always read/write
*/
static requestPermissions(readOnly?: boolean): Promise<AuthorizationStatus>;

/** Finds all the calendars on the device. */
static findCalendars(): Promise<Calendar[]>;
Expand Down
6 changes: 4 additions & 2 deletions src/index.ios.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@ import { NativeModules, processColor } from "react-native";
const RNCalendarEvents = NativeModules.RNCalendarEvents;

export default {
checkPermissions() {
checkPermissions(readOnly = false) {
// readOnly is ignored on iOS, the platform does not support it.
return RNCalendarEvents.checkPermissions();
},
requestPermissions() {
requestPermissions(readOnly = false) {
// readOnly is ignored on iOS, the platform does not support it.
return RNCalendarEvents.requestPermissions();
},

Expand Down

0 comments on commit 1bb027b

Please sign in to comment.