diff --git a/README.md b/README.md index 6a7c353..c27152e 100644 --- a/README.md +++ b/README.md @@ -57,21 +57,8 @@ connectivityStatusSubscription.remove() ``` ### Enable services -```js -import ConnectivityManager from 'react-native-connectivity-status' - -// Ask user to turn on Location Services -ConnectivityManager.enableLocation() - -// Ask user to turn on Bluetooth -ConnectivityManager.enableBluetooth() -``` - -**ATTENTION:** On `iOS` the `enableLocation` method won't ask the user for `Location Permissions` but will redirect to -`iOS` location settings screen to allow the user to enable `Location Services`. -In case `Location Services` are active but `Location Permissions` have yet to be asked to the user the method call will throw an error. -**ATTENTION:** Starting from `iOS 11` redirect to specific Settings page has been disabled, so the `enableLocation` method will only redirect to the `Settings` main screen. +**NOTE:** Due to possible app rejection from Apple (caused by illegal usage of private URL Scheme "prefs:root" or "App-Prefs:root"), methods for enabling bluetooth and location services have been removed from this module. --- Made with :sparkles: & :heart: by [Mattia Panzeri](https://github.com/panz3r) and [contributors](https://github.com/nearit/react-native-connectivity-status/graphs/contributors) \ No newline at end of file diff --git a/android/build.gradle b/android/build.gradle index 7be9042..3ab67ae 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -1,17 +1,19 @@ buildscript { repositories { + google() jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:1.3.1' - classpath 'com.google.gms:google-services:3.1.1' + classpath 'com.android.tools.build:gradle:3.1.4' + classpath 'com.google.gms:google-services:4.0.1' } } allprojects { repositories { + google() jcenter() maven { url "https://maven.google.com" @@ -22,12 +24,12 @@ allprojects { apply plugin: 'com.android.library' android { - compileSdkVersion 23 - buildToolsVersion "23.0.3" + compileSdkVersion 27 + buildToolsVersion "27.0.3" defaultConfig { minSdkVersion 16 - targetSdkVersion 22 + targetSdkVersion 27 versionCode 1 versionName "1.0" } @@ -37,11 +39,12 @@ android { } repositories { + google() + jcenter() mavenCentral() } dependencies { - compile 'com.facebook.react:react-native:+' - compile 'com.google.android.gms:play-services-base:11.8.0' - compile 'com.google.android.gms:play-services-location:11.8.0' + implementation 'com.facebook.react:react-native:+' + implementation 'com.google.android.gms:play-services-location:15.0.1' } diff --git a/android/src/main/java/com/nearit/connectivity/RNConnectivityStatusModule.java b/android/src/main/java/com/nearit/connectivity/RNConnectivityStatusModule.java index 3bb2605..fe3abee 100644 --- a/android/src/main/java/com/nearit/connectivity/RNConnectivityStatusModule.java +++ b/android/src/main/java/com/nearit/connectivity/RNConnectivityStatusModule.java @@ -1,4 +1,3 @@ - package com.nearit.connectivity; import android.bluetooth.BluetoothAdapter; @@ -6,11 +5,9 @@ import android.content.Context; import android.content.Intent; import android.content.IntentFilter; -import android.content.IntentSender; +import android.content.pm.PackageManager; import android.location.LocationManager; -import android.os.Bundle; -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; +import android.support.v4.content.ContextCompat; import com.facebook.react.bridge.Promise; import com.facebook.react.bridge.ReactApplicationContext; @@ -19,196 +16,142 @@ import com.facebook.react.bridge.WritableMap; import com.facebook.react.bridge.WritableNativeMap; import com.facebook.react.modules.core.DeviceEventManagerModule; -import com.google.android.gms.common.ConnectionResult; -import com.google.android.gms.common.api.GoogleApiClient; -import com.google.android.gms.common.api.PendingResult; -import com.google.android.gms.common.api.ResultCallback; -import com.google.android.gms.common.api.Status; -import com.google.android.gms.location.LocationRequest; -import com.google.android.gms.location.LocationServices; -import com.google.android.gms.location.LocationSettingsRequest; -import com.google.android.gms.location.LocationSettingsResult; -import com.google.android.gms.location.LocationSettingsStatusCodes; - -public class RNConnectivityStatusModule extends ReactContextBaseJavaModule implements GoogleApiClient.ConnectionCallbacks, GoogleApiClient.OnConnectionFailedListener { - - private final ReactApplicationContext reactContext; - - private final static String RN_CONNECTIVITY_STATUS_TOPIC = "RNConnectivityStatus"; - private final static String EVENT_TYPE = "eventType"; - private final static String EVENT_STATUS = "status"; - - private GoogleApiClient mGoogleApiClient; - private static final int NEAR_BLUETOOTH_SETTINGS_CODE = 4000; - private static final int NEAR_LOCATION_SETTINGS_CODE = 5000; - - private final BroadcastReceiver mBtReceiver = new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent intent) { - final String action = intent.getAction(); - - if (action != null && action.equals(BluetoothAdapter.ACTION_STATE_CHANGED)) { - final int state = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, - BluetoothAdapter.ERROR); - boolean active = false; - switch (state) { - case BluetoothAdapter.STATE_OFF: - active = false; - break; - case BluetoothAdapter.STATE_ON: - active = true; - break; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +public class RNConnectivityStatusModule extends ReactContextBaseJavaModule { + + private final ReactApplicationContext reactContext; + + private final static String RN_CONNECTIVITY_STATUS_TOPIC = "RNConnectivityStatus"; + private final static String EVENT_TYPE = "eventType"; + private final static String EVENT_STATUS = "status"; + + // Location permission status + private static final String PERMISSION_LOCATION_GRANTED = "Location.Permission.Granted.Always"; + private static final String PERMISSION_LOCATION_DENIED = "Location.Permission.Denied"; + + private final BroadcastReceiver mBtReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + final String action = intent.getAction(); + + if (action != null && action.equals(BluetoothAdapter.ACTION_STATE_CHANGED)) { + final int state = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, + BluetoothAdapter.ERROR); + boolean active = false; + switch (state) { + case BluetoothAdapter.STATE_OFF: + active = false; + break; + case BluetoothAdapter.STATE_ON: + active = true; + break; + } + + final WritableMap eventMap = new WritableNativeMap(); + eventMap.putString(EVENT_TYPE, "bluetooth"); + eventMap.putBoolean(EVENT_STATUS, active); + getReactApplicationContext().getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class).emit(RN_CONNECTIVITY_STATUS_TOPIC, eventMap); + } } + }; + + private final BroadcastReceiver mLocationReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + final boolean locationEnabled = intent.getAction() != null + && intent.getAction().matches(LocationManager.PROVIDERS_CHANGED_ACTION) + && checkLocationServices(); + + final WritableMap eventMap = new WritableNativeMap(); + eventMap.putString(EVENT_TYPE, "location"); + eventMap.putBoolean(EVENT_STATUS, locationEnabled); + getReactApplicationContext().getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class).emit(RN_CONNECTIVITY_STATUS_TOPIC, eventMap); + } + }; - final WritableMap eventMap = new WritableNativeMap(); - eventMap.putString(EVENT_TYPE, "bluetooth"); - eventMap.putBoolean(EVENT_STATUS, active); - getReactApplicationContext().getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class).emit(RN_CONNECTIVITY_STATUS_TOPIC, eventMap); - } + public RNConnectivityStatusModule(ReactApplicationContext reactContext) { + super(reactContext); + this.reactContext = reactContext; } - }; - private final BroadcastReceiver mLocationReceiver = new BroadcastReceiver() { @Override - public void onReceive(Context context, Intent intent) { - final boolean locationEnabled = intent.getAction() != null - && intent.getAction().matches(LocationManager.PROVIDERS_CHANGED_ACTION) - && checkLocation(); - - final WritableMap eventMap = new WritableNativeMap(); - eventMap.putString(EVENT_TYPE, "location"); - eventMap.putBoolean(EVENT_STATUS, locationEnabled); - getReactApplicationContext().getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class).emit(RN_CONNECTIVITY_STATUS_TOPIC, eventMap); + public String getName() { + return "RNConnectivityStatus"; } - }; - - public RNConnectivityStatusModule(ReactApplicationContext reactContext) { - super(reactContext); - this.reactContext = reactContext; - } - - @Override - public String getName() { - return "RNConnectivityStatus"; - } - - @Override - public void initialize() { - super.initialize(); - - final IntentFilter btFilter = new IntentFilter(); - btFilter.addAction(BluetoothAdapter.ACTION_STATE_CHANGED); - reactContext.getApplicationContext().registerReceiver(mBtReceiver, btFilter); - - final IntentFilter locationFilter = new IntentFilter(); - locationFilter.addAction(LocationManager.PROVIDERS_CHANGED_ACTION); - reactContext.getApplicationContext().registerReceiver(mLocationReceiver, locationFilter); - } - - @ReactMethod - public void isBluetoothEnabled(final Promise promise) { - try { - promise.resolve(checkBluetooth()); - } catch (Exception e) { - promise.reject("BLE_CHECK_ERROR", e.getMessage()); + + @javax.annotation.Nullable + @Override + public Map getConstants() { + return Collections.unmodifiableMap(new HashMap() { + { + put("LocationGrantedAlways", PERMISSION_LOCATION_GRANTED); + put("LocationDenied", PERMISSION_LOCATION_DENIED); + } + }); } - } - - /** - * Asks to enable bluetooth - */ - @ReactMethod - public void enableBluetooth(final Promise promise) { - try { - if (!checkBluetooth()) { - Intent enableBtIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE); - getReactApplicationContext().startActivityForResult(enableBtIntent, NEAR_BLUETOOTH_SETTINGS_CODE, new Bundle()); - } - - promise.resolve(true); - } catch (Exception e) { - promise.reject("BLE_ACTIVATION_ERROR", e.getMessage()); + + @Override + public void initialize() { + super.initialize(); + + final IntentFilter btFilter = new IntentFilter(); + btFilter.addAction(BluetoothAdapter.ACTION_STATE_CHANGED); + reactContext.getApplicationContext().registerReceiver(mBtReceiver, btFilter); + + final IntentFilter locationFilter = new IntentFilter(); + locationFilter.addAction(LocationManager.PROVIDERS_CHANGED_ACTION); + reactContext.getApplicationContext().registerReceiver(mLocationReceiver, locationFilter); } - } - - @ReactMethod - public void isLocationEnabled(final Promise promise) { - try { - promise.resolve(checkLocation()); - } catch (Exception e) { - promise.reject("LOCATION_CHECK_ERROR", e.getMessage()); + + @ReactMethod + public void isBluetoothEnabled(final Promise promise) { + try { + promise.resolve(checkBluetooth()); + } catch (Exception e) { + promise.reject("BLE_CHECK_ERROR", e.getMessage()); + } } - } - - /** - * Asks to enable location services. - */ - @ReactMethod - public void enableLocation(final Promise promise) { - mGoogleApiClient = new GoogleApiClient.Builder(getReactApplicationContext()) - .addApi(LocationServices.API) - .addConnectionCallbacks(this) - .addOnConnectionFailedListener(this) - .build(); - - mGoogleApiClient.connect(); - - promise.resolve(true); - } - - /** - * GoogleApiClient interfaces - */ - @Override - public void onConnected(@Nullable Bundle bundle) { - final LocationRequest locationRequest = LocationRequest.create() - .setPriority(LocationRequest.PRIORITY_LOW_POWER); - - final LocationSettingsRequest.Builder builder = new LocationSettingsRequest.Builder() - .addLocationRequest(locationRequest) - .setNeedBle(true); - - final PendingResult result = LocationServices.SettingsApi.checkLocationSettings(mGoogleApiClient, builder.build()); - - result.setResultCallback(new ResultCallback() { - @Override - public void onResult(@NonNull LocationSettingsResult locationSettingsResult) { - final Status status = locationSettingsResult.getStatus(); - if (status.getStatusCode() == LocationSettingsStatusCodes.RESOLUTION_REQUIRED) { - try { - status.startResolutionForResult(getCurrentActivity(), NEAR_LOCATION_SETTINGS_CODE); - } catch (IntentSender.SendIntentException e) { - e.printStackTrace(); - } + + @ReactMethod + public void areLocationServicesEnabled(final Promise promise) { + try { + promise.resolve(checkLocationServices()); + } catch (Exception e) { + promise.reject("LOCATION_CHECK_ERROR", e.getMessage()); + } + } + + @ReactMethod + public void isLocationPermissionGranted(final Promise promise) { + try { + if (checkLocationPermission()) { + promise.resolve(PERMISSION_LOCATION_GRANTED); + } else { + promise.resolve(PERMISSION_LOCATION_DENIED); + } + } catch (Exception e) { + promise.reject("LOCATION_PERMISSION_CHECK_ERROR", e.getMessage()); } - } - }); - } - - @Override - public void onConnectionSuspended(int i) { - } - - @Override - public void onConnectionFailed(@NonNull ConnectionResult connectionResult) { - } - - /** - * Private methods - */ - private boolean checkBluetooth() { - return BluetoothAdapter.getDefaultAdapter() != null && BluetoothAdapter.getDefaultAdapter().isEnabled(); - } - - private boolean checkLocation() { - boolean locationEnabled = false; - - final LocationManager lm = (LocationManager) getReactApplicationContext().getSystemService(Context.LOCATION_SERVICE); - if (lm != null) { - locationEnabled = lm.isProviderEnabled(LocationManager.GPS_PROVIDER); - locationEnabled |= lm.isProviderEnabled(LocationManager.NETWORK_PROVIDER); } - return locationEnabled; - } + /** + * Private methods + */ + private boolean checkBluetooth() { + return BluetoothAdapter.getDefaultAdapter() != null && BluetoothAdapter.getDefaultAdapter().isEnabled(); + } + + private boolean checkLocationServices() { + final LocationManager locationManager = (LocationManager) getReactApplicationContext().getSystemService(Context.LOCATION_SERVICE); + return (locationManager != null && locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER)) + | (locationManager != null && locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER)); + } + + private boolean checkLocationPermission() { + return ContextCompat.checkSelfPermission(getReactApplicationContext(), android.Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED; + } } \ No newline at end of file diff --git a/android/src/main/java/com/nearit/connectivity/RNConnectivityStatusPackage.java b/android/src/main/java/com/nearit/connectivity/RNConnectivityStatusPackage.java index bed01e1..68b2fb9 100644 --- a/android/src/main/java/com/nearit/connectivity/RNConnectivityStatusPackage.java +++ b/android/src/main/java/com/nearit/connectivity/RNConnectivityStatusPackage.java @@ -1,7 +1,5 @@ - package com.nearit.connectivity; -import java.util.Arrays; import java.util.Collections; import java.util.List; @@ -10,10 +8,12 @@ import com.facebook.react.bridge.ReactApplicationContext; import com.facebook.react.uimanager.ViewManager; import com.facebook.react.bridge.JavaScriptModule; + public class RNConnectivityStatusPackage implements ReactPackage { + @Override public List createNativeModules(ReactApplicationContext reactContext) { - return Arrays.asList(new RNConnectivityStatusModule(reactContext)); + return Collections.singletonList(new RNConnectivityStatusModule(reactContext)); } // Deprecated from RN 0.47 @@ -25,4 +25,5 @@ public List> createJSModules() { public List createViewManagers(ReactApplicationContext reactContext) { return Collections.emptyList(); } + } \ No newline at end of file diff --git a/index.js b/index.js index 3a5cb0e..6b084ab 100644 --- a/index.js +++ b/index.js @@ -14,15 +14,12 @@ export default class ConnectivityManager { return RNConnectivityStatus.isBluetoothEnabled() } - static enableBluetooth () { - return RNConnectivityStatus.enableBluetooth() + static areLocationServicesEnabled () { + return RNConnectivityStatus.areLocationServicesEnabled() } - static isLocationEnabled () { - return RNConnectivityStatus.isLocationEnabled() + static isLocationPermissionGranted () { + return RNConnectivityStatus.isLocationPermissionGranted() } - static enableLocation () { - return RNConnectivityStatus.enableLocation() - } } diff --git a/ios/RNConnectivityStatus.h b/ios/RNConnectivityStatus.h index adc6645..0ec5bed 100644 --- a/ios/RNConnectivityStatus.h +++ b/ios/RNConnectivityStatus.h @@ -2,6 +2,7 @@ // RNConnectivityStatus.h // // Created by Mattia Panzeri on 10/10/2017. +// Latest changes by Federico Boschini on 08/29/2018 // Copyright © 2017 Near Srl. All rights reserved. // @@ -18,4 +19,10 @@ #define EVENT_TYPE @"eventType" #define EVENT_STATUS @"status" +typedef NS_ENUM(NSInteger, LocationPermissionState) { + LocationPermissionOff, + LocationPermissionWhenInUse, + LocationPermissionAlways +}; + @end diff --git a/ios/RNConnectivityStatus.m b/ios/RNConnectivityStatus.m index 1b5f2b8..58b752c 100644 --- a/ios/RNConnectivityStatus.m +++ b/ios/RNConnectivityStatus.m @@ -2,6 +2,7 @@ // RNConnectivityStatus.m // // Created by Mattia Panzeri on 10/10/2017. +// Latest changes by Federico Boschini on 08/29/2018 // Copyright © 2017 Near Srl. All rights reserved. // @@ -10,6 +11,11 @@ #define SYSTEM_VERSION_GREATER_THAN_OR_EQUAL_TO(v) ([[[UIDevice currentDevice] systemVersion] compare:v options:NSNumericSearch] != NSOrderedAscending) +// Location permission status +NSString* const RNCS_PERMISSION_LOCATION_GRANTED_ALWAYS = @"Location.Permission.Granted.Always"; +NSString* const RNCS_PERMISSION_LOCATION_GRANTED_WHEN_IN_USE = @"Location.Permission.Granted.WhenInUse"; +NSString* const RNCS_PERMISSION_LOCATION_DENIED = @"Location.Permission.Denied"; + @implementation RNConnectivityStatus { bool hasListeners; CLLocationManager *locationManager; @@ -50,6 +56,17 @@ - (instancetype) init return self; } +- (NSDictionary *)constantsToExport +{ + return @{ + @"Permissions": @{ + @"LocationGrantedAlways": RNCS_PERMISSION_LOCATION_GRANTED_ALWAYS, + @"LocationGrantedWhenInUse": RNCS_PERMISSION_LOCATION_GRANTED_ALWAYS, + @"LocationDenied": RNCS_PERMISSION_LOCATION_DENIED + } + }; +} + // MARK: RCTEventEmitter - (NSArray *)supportedEvents @@ -92,16 +109,8 @@ - (void)sendActiveState:(BOOL)state forType:(NSString* _Nonnull)eventType { - (BOOL)isBluetoothActiveState:(CBManagerState)bluetoothState { switch (bluetoothState) { - case CBManagerStateUnknown: - case CBManagerStateResetting: - case CBManagerStateUnsupported: - case CBManagerStateUnauthorized: - case CBManagerStatePoweredOff: - return NO; - case CBManagerStatePoweredOn: return YES; - default: return NO; } @@ -114,19 +123,6 @@ - (BOOL)isBluetoothActiveState:(CBManagerState)bluetoothState { resolve(@(btIsActive)); } -RCT_EXPORT_METHOD(enableBluetooth:(RCTPromiseResolveBlock) resolve - rejecter:(RCTPromiseRejectBlock) reject) -{ - NSLog(@"iOS: trying to open Bluetooth settings"); - if (SYSTEM_VERSION_GREATER_THAN_OR_EQUAL_TO(@"10.0")) { - [[UIApplication sharedApplication] openURL:[NSURL URLWithString:@"App-Prefs:root=Bluetooth"]]; - } else { - [[UIApplication sharedApplication] openURL:[NSURL URLWithString:@"prefs:root=Bluetooth"]]; - } - - resolve(@(YES)); -} - // MARK: CBCentralManager Delegate - (void)centralManagerDidUpdateState:(CBCentralManager *)central { @@ -138,53 +134,41 @@ - (void)centralManagerDidUpdateState:(CBCentralManager *)central { // MARK: Location Permissions -- (BOOL)isLocationActiveState { - switch (CLLocationManager.authorizationStatus) { - case kCLAuthorizationStatusNotDetermined: - case kCLAuthorizationStatusRestricted: - case kCLAuthorizationStatusDenied: - return NO; - - case kCLAuthorizationStatusAuthorizedWhenInUse: - case kCLAuthorizationStatusAuthorizedAlways: - return YES; - - default: - return NO; - } +- (LocationPermissionState)isLocationPermissionGranted { + switch (CLLocationManager.authorizationStatus) { + case kCLAuthorizationStatusAuthorizedWhenInUse: + return LocationPermissionWhenInUse; + case kCLAuthorizationStatusAuthorizedAlways: + return LocationPermissionAlways; + default: + return LocationPermissionOff; + } } -// MARK: Location Services - -RCT_EXPORT_METHOD(isLocationEnabled:(RCTPromiseResolveBlock) resolve - rejecter:(RCTPromiseRejectBlock) reject) -{ - resolve(@(CLLocationManager.locationServicesEnabled)); +RCT_EXPORT_METHOD(isLocationPermissionGranted:(RCTPromiseResolveBlock) resolve + rejecter:(RCTPromiseRejectBlock) reject) { + LocationPermissionState state = [self isLocationPermissionGranted]; + switch (state) { + case LocationPermissionWhenInUse: + resolve(RNCS_PERMISSION_LOCATION_GRANTED_WHEN_IN_USE); + break; + case LocationPermissionAlways: + resolve(RNCS_PERMISSION_LOCATION_GRANTED_ALWAYS); + break; + case LocationPermissionOff: + resolve(RNCS_PERMISSION_LOCATION_DENIED); + break; + default: + reject(@"LOCATION_PERMISSION_CHECK_ERROR", @"Can't check location permission", nil); + break; + } } -RCT_EXPORT_METHOD(enableLocation:(RCTPromiseResolveBlock) resolve - reject:(RCTPromiseRejectBlock) reject) -{ - NSURL* settingsUrl; - if (!CLLocationManager.locationServicesEnabled) { - if (SYSTEM_VERSION_GREATER_THAN_OR_EQUAL_TO(@"10.0")) { - settingsUrl = [NSURL URLWithString:@"App-Prefs:root=Privacy&path=LOCATION"]; - } else { - settingsUrl = [NSURL URLWithString:@"prefs:root=LOCATION_SERVICES"]; - } - } else if (CLLocationManager.authorizationStatus != kCLAuthorizationStatusNotDetermined) { - settingsUrl = [NSURL URLWithString:UIApplicationOpenSettingsURLString]; - } else { - NSLog(@"iOS: Location permission request needed"); - reject(@"ERR_ENABLE_LOCATION", @"Location Services are enabled but Location permission request is required", nil); - } +// MARK: Location Services - if (settingsUrl != nil) { - NSLog(@"iOS: trying to open Location settings at %@", settingsUrl); - [[UIApplication sharedApplication] openURL:settingsUrl options:@{} completionHandler:^(BOOL success) { - resolve(@(success)); - }]; - } +RCT_EXPORT_METHOD(areLocationServicesEnabled:(RCTPromiseResolveBlock) resolve + rejecter:(RCTPromiseRejectBlock) reject) { + resolve(@(CLLocationManager.locationServicesEnabled)); } // MARK: CLLocationManagerDelegate diff --git a/package.json b/package.json index cbae4ee..2b7ec91 100644 --- a/package.json +++ b/package.json @@ -1,12 +1,15 @@ { "name": "react-native-connectivity-status", - "version": "1.4.0", + "version": "1.5.0", "description": "A ReactNative module to check Bluetooth and Location status on Android and iOS", "author": { "name": "Mattia Panzeri", "email": "mattia.panzeri93@gmail.com", "url": "https://github.com/panz3r" }, + "contributors": [ + "Federico Boschini " + ], "license": "MIT", "repository": { "type": "git",