From 6e6838ea4431c12a8c550d68f45e29d6c443a32b Mon Sep 17 00:00:00 2001 From: Marco Scabbiolo Date: Mon, 8 Mar 2021 13:38:09 -0300 Subject: [PATCH 1/4] ios permission getter and listener --- README.md | 10 ++++ index.d.ts | 12 +++-- ios/RNFusedLocation.swift | 96 +++++++++++++++++++++---------------- ios/RNFusedLocationBridge.m | 9 ++++ js/Geolocation.js | 4 ++ js/Geolocation.native.js | 24 ++++++++++ 6 files changed, 111 insertions(+), 44 deletions(-) diff --git a/README.md b/README.md index b845bb3..0648a43 100644 --- a/README.md +++ b/README.md @@ -64,6 +64,16 @@ When promise resolves, returns the status of the authorization. - `denied` - Permission denied - `restricted` - Permission restricted +#### `async getCurrentAuthorization() (iOS only)` +Get the current location permission status for the app without prompting the user for it. +Possible return values are the same as for `requestAuthorization` and `null` can also be returned if no specific permission has been granted. + +#### `watchPermission(changedCallback) (iOS Only)` + +Subscribe to changes to the location permission. These can happen if the user changes them in the settings app while the app is running. `changedCallback` will be called with the authorization result (return values of `requestAuthorization`). + +**Returns**: A function to unsubscribe (remove the listener). If called `changedCallback` will stop receiving permission updates. + #### `getCurrentPosition(successCallback, ?errorCallback, ?options)` - **successCallback**: Invoked with latest location info. - **errorCallback**: Invoked whenever an error is encountered. diff --git a/index.d.ts b/index.d.ts index 9f6c4e3..5c4d32e 100644 --- a/index.d.ts +++ b/index.d.ts @@ -9,8 +9,7 @@ declare module 'react-native-geolocation-service' { | 'nearestTenMeters' | 'hundredMeters' | 'kilometer' - | 'threeKilometers' - | 'reduced'; + | 'threeKilometers'; export type AccuracyAndroid = | 'high' @@ -38,7 +37,6 @@ declare module 'react-native-geolocation-service' { interval?: number fastestInterval?: number useSignificantChanges?: boolean - showsBackgroundLocationIndicator?: boolean } export enum PositionError { @@ -71,10 +69,14 @@ declare module 'react-native-geolocation-service' { mocked?: boolean; } + type PermissionChangedCallback = (permission: AuthorizationResult) => void + type SuccessCallback = (position: GeoPosition) => void type ErrorCallback = (error: GeoError) => void + export function getCurrentAuthorization(): Promise + export function requestAuthorization( authorizationLevel: AuthorizationLevel ): Promise @@ -85,6 +87,10 @@ declare module 'react-native-geolocation-service' { options?: GeoOptions ): void + export function watchPermission( + changedCallback: PermissionChangedCallback + ): () => void + export function watchPosition( successCallback: SuccessCallback, errorCallback?: ErrorCallback, diff --git a/ios/RNFusedLocation.swift b/ios/RNFusedLocation.swift index 5538e1e..6b7826c 100644 --- a/ios/RNFusedLocation.swift +++ b/ios/RNFusedLocation.swift @@ -24,6 +24,7 @@ class RNFusedLocation: RCTEventEmitter { private var resolveAuthorizationStatus: RCTPromiseResolveBlock? = nil private var successCallback: RCTResponseSenderBlock? = nil private var errorCallback: RCTResponseSenderBlock? = nil + private var permissionListenerCount: Int = 0 override init() { super.init() @@ -43,6 +44,15 @@ class RNFusedLocation: RCTEventEmitter { locationManager.delegate = nil; } + + // MARK: Bridge Method + @objc(getCurrentAuthorization:reject:) + func getCurrentAuthorization( + resolve: @escaping RCTPromiseResolveBlock, + reject: @escaping RCTPromiseRejectBlock + ) -> Void { + resolve(getCurrentAuthorizationStatus()) + } // MARK: Bridge Method @objc func requestAuthorization( @@ -56,19 +66,10 @@ class RNFusedLocation: RCTEventEmitter { resolve(AuthorizationStatus.disabled.rawValue) return } - - switch CLLocationManager.authorizationStatus() { - case .authorizedWhenInUse, .authorizedAlways: - resolve(AuthorizationStatus.granted.rawValue) - return - case .denied: - resolve(AuthorizationStatus.denied.rawValue) - return - case .restricted: - resolve(AuthorizationStatus.restricted.rawValue) - return - default: - break + + if let currentStatus = getCurrentAuthorizationStatus() { + resolve(currentStatus) + return } resolveAuthorizationStatus = resolve @@ -104,7 +105,7 @@ class RNFusedLocation: RCTEventEmitter { locManager.delegate = self locManager.desiredAccuracy = getAccuracy(options) locManager.distanceFilter = distanceFilter - locManager.startUpdatingLocation() + locManager.requestLocation() self.successCallback = successCallback self.errorCallback = errorCallback @@ -127,15 +128,11 @@ class RNFusedLocation: RCTEventEmitter { @objc func startLocationUpdate(_ options: [String: Any]) -> Void { let distanceFilter = options["distanceFilter"] as? Double ?? DEFAULT_DISTANCE_FILTER let significantChanges = options["useSignificantChanges"] as? Bool ?? false - let showsBackgroundLocationIndicator = options["showsBackgroundLocationIndicator"] as? Bool ?? false locationManager.desiredAccuracy = getAccuracy(options) locationManager.distanceFilter = distanceFilter locationManager.allowsBackgroundLocationUpdates = shouldAllowBackgroundUpdate() locationManager.pausesLocationUpdatesAutomatically = false - if #available(iOS 11.0, *) { - locationManager.showsBackgroundLocationIndicator = showsBackgroundLocationIndicator - } significantChanges ? locationManager.startMonitoringSignificantLocationChanges() @@ -153,6 +150,16 @@ class RNFusedLocation: RCTEventEmitter { observing = false } + + // MARK: Bridge Method + @objc func permissionListenerAdded() -> Void { + permissionListenerCount += 1 + } + + // MARK: Bridge Method + @objc func permissionListenerRemoved() -> Void { + permissionListenerCount -= 1 + } @objc func timerFired(timer: Timer) -> Void { let data = timer.userInfo as! [String: Any] @@ -211,12 +218,6 @@ class RNFusedLocation: RCTEventEmitter { return kCLLocationAccuracyKilometer case "threeKilometers": return kCLLocationAccuracyThreeKilometers - case "reduced": - if #available(iOS 14.0, *) { - return kCLLocationAccuracyReduced - } else { - return kCLLocationAccuracyThreeKilometers - } default: return highAccuracy ? kCLLocationAccuracyBest : kCLLocationAccuracyHundredMeters } @@ -231,6 +232,23 @@ class RNFusedLocation: RCTEventEmitter { return false } + + private func getAuthorizationStatusString(_ authorizationStatus: CLAuthorizationStatus) -> String? { + switch authorizationStatus { + case .authorizedWhenInUse, .authorizedAlways: + return AuthorizationStatus.granted.rawValue + case .denied: + return AuthorizationStatus.denied.rawValue + case .restricted: + return AuthorizationStatus.restricted.rawValue + default: + return nil + } + } + + private func getCurrentAuthorizationStatus() -> String? { + return getAuthorizationStatusString(CLLocationManager.authorizationStatus()) + } private func generateErrorResponse(code: Int, message: String = "") -> [String: Any] { var msg: String = message @@ -268,7 +286,7 @@ extension RNFusedLocation { } override func supportedEvents() -> [String]! { - return ["geolocationDidChange", "geolocationError"] + return ["geolocationDidChange", "geolocationError", "geolocationPermissionDidChange"] } override func startObserving() -> Void { @@ -282,22 +300,20 @@ extension RNFusedLocation { extension RNFusedLocation: CLLocationManagerDelegate { func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) { - if status == .notDetermined || resolveAuthorizationStatus == nil { + if status == .notDetermined { return } + + let statusString = getAuthorizationStatusString(status) - switch status { - case .authorizedWhenInUse, .authorizedAlways: - resolveAuthorizationStatus?(AuthorizationStatus.granted.rawValue) - case .denied: - resolveAuthorizationStatus?(AuthorizationStatus.denied.rawValue) - case .restricted: - resolveAuthorizationStatus?(AuthorizationStatus.restricted.rawValue) - default: - break + if let resolve = resolveAuthorizationStatus { + resolve(statusString) + resolveAuthorizationStatus = nil + } + + if (permissionListenerCount > 0) { + sendEvent(withName: "geolocationPermissionDidChange", body: statusString) } - - resolveAuthorizationStatus = nil } func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) { @@ -326,11 +342,10 @@ extension RNFusedLocation: CLLocationManagerDelegate { successCallback!([locationData]) // Cleanup - manager.stopUpdatingLocation() - manager.delegate = nil timeoutTimer?.invalidate() successCallback = nil errorCallback = nil + manager.delegate = nil } func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) { @@ -367,10 +382,9 @@ extension RNFusedLocation: CLLocationManagerDelegate { errorCallback!([errorData]) // Cleanup - manager.stopUpdatingLocation() - manager.delegate = nil timeoutTimer?.invalidate() successCallback = nil errorCallback = nil + manager.delegate = nil } } diff --git a/ios/RNFusedLocationBridge.m b/ios/RNFusedLocationBridge.m index 9f619fb..6d10aba 100644 --- a/ios/RNFusedLocationBridge.m +++ b/ios/RNFusedLocationBridge.m @@ -3,6 +3,11 @@ @interface RCT_EXTERN_MODULE(RNFusedLocation, RCTEventEmitter) +RCT_EXTERN_METHOD( + getCurrentAuthorization:(RCTPromiseResolveBlock *)resolve + reject:(RCTPromiseRejectBlock)reject +) + RCT_EXTERN_METHOD( requestAuthorization:(NSString *)level resolve:(RCTPromiseResolveBlock)resolve @@ -15,6 +20,10 @@ @interface RCT_EXTERN_MODULE(RNFusedLocation, RCTEventEmitter) errorCallback:(RCTResponseSenderBlock)errorCallback ) +RCT_EXTERN_METHOD(permissionListenerAdded) + +RCT_EXTERN_METHOD(permissionListenerRemoved) + _RCT_EXTERN_REMAP_METHOD( startObserving, startLocationUpdate:(NSDictionary *)options, diff --git a/js/Geolocation.js b/js/Geolocation.js index 3fc7eba..456369b 100644 --- a/js/Geolocation.js +++ b/js/Geolocation.js @@ -20,6 +20,10 @@ const Geolocation = { navigator.geolocation.getCurrentPosition(success, error, options); }, + watchPermission: function () { + throw new Error('Method not supported by browser') + }, + watchPosition: function (success, error, options) { if (!success) { throw new Error('Must provide a success callback'); diff --git a/js/Geolocation.native.js b/js/Geolocation.native.js index 89ccb4f..5fcfea8 100644 --- a/js/Geolocation.native.js +++ b/js/Geolocation.native.js @@ -10,6 +10,14 @@ let updatesEnabled = false; const Geolocation = { setRNConfiguration: (config) => {}, // eslint-disable-line no-unused-vars + getCurrentAuthorization: async () => { + if (Platform.OS !== 'ios') { + Promise.reject('getCurrentAuthorization is only for iOS'); + } + + return RNFusedLocation.getCurrentAuthorization(); + }, + requestAuthorization: async (authorizationLevel) => { if (Platform.OS !== 'ios') { return Promise.reject('requestAuthorization is only for iOS'); @@ -33,6 +41,22 @@ const Geolocation = { RNFusedLocation.getCurrentPosition(options, success, error); }, + watchPermission: (changed) => { + if (Platform.OS !== 'ios') { + throw new Error('watchPermission is only for iOS'); + } + if (!changed) { + throw new Error('Must provide a changed callback'); + } + + const subscription = LocationEventEmitter.addListener('geolocationPermissionDidChange', changed); + RNFusedLocation.permissionListenerAdded(); + return () => { + RNFusedLocation.permissionListenerRemoved(); + subscription.remove(); + }; + }, + watchPosition: (success, error = null, options = {}) => { if (!success) { // eslint-disable-next-line no-console From 5f46d2bd12bbfb1db77cf7f54e26e3dfa7985ee2 Mon Sep 17 00:00:00 2001 From: Marco Scabbiolo Date: Mon, 8 Mar 2021 13:45:49 -0300 Subject: [PATCH 2/4] update to lastest master version --- index.d.ts | 4 +++- ios/RNFusedLocation.swift | 18 +++++++++++++++--- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/index.d.ts b/index.d.ts index 5c4d32e..a92f377 100644 --- a/index.d.ts +++ b/index.d.ts @@ -9,7 +9,8 @@ declare module 'react-native-geolocation-service' { | 'nearestTenMeters' | 'hundredMeters' | 'kilometer' - | 'threeKilometers'; + | 'threeKilometers' + | 'reduced'; export type AccuracyAndroid = | 'high' @@ -37,6 +38,7 @@ declare module 'react-native-geolocation-service' { interval?: number fastestInterval?: number useSignificantChanges?: boolean + showsBackgroundLocationIndicator?: boolean } export enum PositionError { diff --git a/ios/RNFusedLocation.swift b/ios/RNFusedLocation.swift index 6b7826c..1bc99ab 100644 --- a/ios/RNFusedLocation.swift +++ b/ios/RNFusedLocation.swift @@ -105,7 +105,7 @@ class RNFusedLocation: RCTEventEmitter { locManager.delegate = self locManager.desiredAccuracy = getAccuracy(options) locManager.distanceFilter = distanceFilter - locManager.requestLocation() + locManager.startUpdatingLocation() self.successCallback = successCallback self.errorCallback = errorCallback @@ -128,11 +128,15 @@ class RNFusedLocation: RCTEventEmitter { @objc func startLocationUpdate(_ options: [String: Any]) -> Void { let distanceFilter = options["distanceFilter"] as? Double ?? DEFAULT_DISTANCE_FILTER let significantChanges = options["useSignificantChanges"] as? Bool ?? false + let showsBackgroundLocationIndicator = options["showsBackgroundLocationIndicator"] as? Bool ?? false locationManager.desiredAccuracy = getAccuracy(options) locationManager.distanceFilter = distanceFilter locationManager.allowsBackgroundLocationUpdates = shouldAllowBackgroundUpdate() locationManager.pausesLocationUpdatesAutomatically = false + if #available(iOS 11.0, *) { + locationManager.showsBackgroundLocationIndicator = showsBackgroundLocationIndicator + } significantChanges ? locationManager.startMonitoringSignificantLocationChanges() @@ -218,6 +222,12 @@ class RNFusedLocation: RCTEventEmitter { return kCLLocationAccuracyKilometer case "threeKilometers": return kCLLocationAccuracyThreeKilometers + case "reduced": + if #available(iOS 14.0, *) { + return kCLLocationAccuracyReduced + } else { + return kCLLocationAccuracyThreeKilometers + } default: return highAccuracy ? kCLLocationAccuracyBest : kCLLocationAccuracyHundredMeters } @@ -342,10 +352,11 @@ extension RNFusedLocation: CLLocationManagerDelegate { successCallback!([locationData]) // Cleanup + manager.stopUpdatingLocation() + manager.delegate = nil timeoutTimer?.invalidate() successCallback = nil errorCallback = nil - manager.delegate = nil } func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) { @@ -382,9 +393,10 @@ extension RNFusedLocation: CLLocationManagerDelegate { errorCallback!([errorData]) // Cleanup + manager.stopUpdatingLocation() + manager.delegate = nil timeoutTimer?.invalidate() successCallback = nil errorCallback = nil - manager.delegate = nil } } From 7aba7e0bbe6cc533bdbc0de3c8314c0560386fbf Mon Sep 17 00:00:00 2001 From: Marco Scabbiolo Date: Mon, 8 Mar 2021 13:51:24 -0300 Subject: [PATCH 3/4] added getCurrentAuthorization mock in browser version --- js/Geolocation.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/js/Geolocation.js b/js/Geolocation.js index 456369b..5efd479 100644 --- a/js/Geolocation.js +++ b/js/Geolocation.js @@ -6,6 +6,10 @@ const Geolocation = { throw new Error('Method not supported by browser'); }, + getCurrentAuthorization: function () { + throw new Error('Method not supported by browser'); + }, + requestAuthorization: async function () { return Promise.reject('Method not supported by browser'); }, From ea6247284a6c0918ae4ed99aa990e08553b8cfb0 Mon Sep 17 00:00:00 2001 From: Marco Scabbiolo Date: Mon, 8 Mar 2021 13:53:46 -0300 Subject: [PATCH 4/4] fix getCurrentAuthorization async --- js/Geolocation.native.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/js/Geolocation.native.js b/js/Geolocation.native.js index 5fcfea8..9e558a4 100644 --- a/js/Geolocation.native.js +++ b/js/Geolocation.native.js @@ -10,9 +10,9 @@ let updatesEnabled = false; const Geolocation = { setRNConfiguration: (config) => {}, // eslint-disable-line no-unused-vars - getCurrentAuthorization: async () => { + getCurrentAuthorization: () => { if (Platform.OS !== 'ios') { - Promise.reject('getCurrentAuthorization is only for iOS'); + return Promise.reject('getCurrentAuthorization is only for iOS'); } return RNFusedLocation.getCurrentAuthorization();