diff --git a/Examples/UIExplorer/GeoLocationExample.js b/Examples/UIExplorer/GeoLocationExample.js
new file mode 100644
index 000000000..1ab5f290c
--- /dev/null
+++ b/Examples/UIExplorer/GeoLocationExample.js
@@ -0,0 +1,72 @@
+/**
+ * Copyright 2004-present Facebook. All Rights Reserved.
+ *
+ * @providesModule GeoLocationExample
+ */
+/* eslint no-console: 0 */
+'use strict';
+
+
+var React = require('react-native');
+var {
+ StyleSheet,
+ Text,
+ View,
+} = React;
+
+exports.framework = 'React';
+exports.title = 'GeoLocation';
+exports.description = 'Examples of using the GeoLocation API.';
+
+exports.examples = [
+ {
+ title: 'navigator.geolocation',
+ render: function() {
+ return ;
+ },
+ }
+];
+
+var GeoLocationExample = React.createClass({
+ getInitialState: function() {
+ return {
+ initialPosition: 'unknown',
+ lastPosition: 'unknown',
+ };
+ },
+
+ componentDidMount: function() {
+ navigator.geolocation.getCurrentPosition(
+ (initialPosition) => this.setState({initialPosition}),
+ (error) => console.error(error)
+ );
+ this.watchID = navigator.geolocation.watchPosition((lastPosition) => {
+ this.setState({lastPosition});
+ });
+ },
+
+ componentWillUnmount: function() {
+ navigator.geolocation.clearWatch(this.watchID);
+ },
+
+ render: function() {
+ return (
+
+
+ Initial position:
+ {JSON.stringify(this.state.initialPosition)}
+
+
+ Current position:
+ {JSON.stringify(this.state.lastPosition)}
+
+
+ );
+ }
+});
+
+var styles = StyleSheet.create({
+ title: {
+ fontWeight: 'bold',
+ },
+});
diff --git a/Examples/UIExplorer/Info.plist b/Examples/UIExplorer/Info.plist
index 9a7ca7e3c..245054621 100644
--- a/Examples/UIExplorer/Info.plist
+++ b/Examples/UIExplorer/Info.plist
@@ -34,6 +34,8 @@
UIInterfaceOrientationLandscapeLeft
UIInterfaceOrientationLandscapeRight
+ NSLocationWhenInUseUsageDescription
+ You need to add NSLocationWhenInUseUsageDescription key in Info.plist to enable geolocation, otherwise it is going to *fail silently*!
UIViewControllerBasedStatusBarAppearance
diff --git a/Examples/UIExplorer/UIExplorerList.js b/Examples/UIExplorer/UIExplorerList.js
index 56734c70f..f6d55db36 100644
--- a/Examples/UIExplorer/UIExplorerList.js
+++ b/Examples/UIExplorer/UIExplorerList.js
@@ -31,6 +31,7 @@ var EXAMPLES = [
require('./TouchableExample'),
require('./ActivityIndicatorExample'),
require('./ScrollViewExample'),
+ require('./GeoLocationExample'),
];
var UIExplorerList = React.createClass({
diff --git a/Libraries/GeoLocation/GeoLocation.js b/Libraries/GeoLocation/GeoLocation.js
new file mode 100644
index 000000000..589931ee9
--- /dev/null
+++ b/Libraries/GeoLocation/GeoLocation.js
@@ -0,0 +1,98 @@
+/**
+ * Copyright 2004-present Facebook. All Rights Reserved.
+ *
+ * @providesModule GeoLocation
+ */
+'use strict';
+
+var RCTDeviceEventEmitter = require('RCTDeviceEventEmitter');
+var RCTLocationObserver = require('NativeModules').RCTLocationObserver;
+
+var invariant = require('invariant');
+var logError = require('logError');
+var warning = require('warning');
+
+var subscriptions = [];
+
+var updatesEnabled = false;
+
+var ensureObserving = function() {
+ if (!updatesEnabled) {
+ RCTLocationObserver.startObserving();
+ updatesEnabled = true;
+ }
+};
+
+/**
+ * /!\ ATTENTION /!\
+ * You need to add NSLocationWhenInUseUsageDescription key
+ * in Info.plist to enable geolocation, otherwise it's going
+ * to *fail silently*!
+ * \!/ \!/
+ *
+ * GeoLocation follows the MDN specification:
+ * https://developer.mozilla.org/en-US/docs/Web/API/Geolocation
+ */
+class GeoLocation {
+ static getCurrentPosition(geo_success, geo_error, geo_options) {
+ invariant(
+ typeof geo_success === 'function',
+ 'Must provide a valid geo_success callback.'
+ );
+ if (geo_options) {
+ warning('geo_options are not yet supported.');
+ }
+ ensureObserving();
+ RCTLocationObserver.getCurrentPosition(
+ geo_success,
+ geo_error || logError
+ );
+ }
+ static watchPosition(callback) {
+ ensureObserving();
+ var watchID = subscriptions.length;
+ subscriptions.push(
+ RCTDeviceEventEmitter.addListener(
+ 'geoLocationDidChange',
+ callback
+ )
+ );
+ return watchID;
+ }
+ static clearWatch(watchID) {
+ var sub = subscriptions[watchID];
+ if (!sub) {
+ // Silently exit when the watchID is invalid or already cleared
+ // This is consistent with timers
+ return;
+ }
+ sub.remove();
+ subscriptions[watchID] = undefined;
+ var noWatchers = true;
+ for (var ii = 0; ii < subscriptions.length; ii++) {
+ if (subscriptions[ii]) {
+ noWatchers = false; // still valid subscriptions
+ }
+ }
+ if (noWatchers) {
+ GeoLocation.stopObserving();
+ }
+ }
+ static stopObserving() {
+ if (updatesEnabled) {
+ RCTLocationObserver.stopObserving();
+ updatesEnabled = false;
+ for (var ii = 0; ii < subscriptions.length; ii++) {
+ if (subscriptions[ii]) {
+ warning('Called stopObserving with existing subscriptions.');
+ subscriptions[ii].remove();
+ }
+ }
+ subscriptions = [];
+ } else {
+ warning('Tried to stop observing when not observing.');
+ }
+ }
+}
+
+module.exports = GeoLocation;
diff --git a/Libraries/JavaScriptAppEngine/Initialization/InitializeJavaScriptAppEngine.js b/Libraries/JavaScriptAppEngine/Initialization/InitializeJavaScriptAppEngine.js
index b529460ee..883a85e03 100644
--- a/Libraries/JavaScriptAppEngine/Initialization/InitializeJavaScriptAppEngine.js
+++ b/Libraries/JavaScriptAppEngine/Initialization/InitializeJavaScriptAppEngine.js
@@ -139,9 +139,15 @@ function setupXHR() {
GLOBAL.fetch = require('fetch');
}
+function setupGeolocation() {
+ GLOBAL.navigator = GLOBAL.navigator || {};
+ GLOBAL.navigator.geolocation = require('GeoLocation');
+}
+
setupRedBoxErrorHandler();
setupDocumentShim();
setupTimers();
setupAlert();
setupPromise();
setupXHR();
+setupGeolocation();
diff --git a/ReactKit/Modules/RCTLocationObserver.h b/ReactKit/Modules/RCTLocationObserver.h
new file mode 100644
index 000000000..ad3ba2ce2
--- /dev/null
+++ b/ReactKit/Modules/RCTLocationObserver.h
@@ -0,0 +1,7 @@
+// Copyright 2004-present Facebook. All Rights Reserved.
+
+#import "RCTBridgeModule.h"
+
+@interface RCTLocationObserver : NSObject
+
+@end
diff --git a/ReactKit/Modules/RCTLocationObserver.m b/ReactKit/Modules/RCTLocationObserver.m
new file mode 100644
index 000000000..315adc842
--- /dev/null
+++ b/ReactKit/Modules/RCTLocationObserver.m
@@ -0,0 +1,182 @@
+// Copyright 2004-present Facebook. All Rights Reserved.
+
+#import "RCTLocationObserver.h"
+
+#import
+#import
+
+#import "RCTAssert.h"
+#import "RCTBridge.h"
+#import "RCTEventDispatcher.h"
+#import "RCTLog.h"
+
+// TODO (#5906496): Shouldn't these be configurable?
+const CLLocationAccuracy RCTLocationAccuracy = 500.0; // meters
+
+@interface RCTPendingLocationRequest : NSObject
+
+@property (nonatomic, copy) RCTResponseSenderBlock successBlock;
+@property (nonatomic, copy) RCTResponseSenderBlock errorBlock;
+
+@end
+
+@implementation RCTPendingLocationRequest @end
+
+@interface RCTLocationObserver ()
+
+@end
+
+@implementation RCTLocationObserver
+{
+ CLLocationManager *_locationManager;
+ RCTEventDispatcher *_eventDispatcher;
+ NSDictionary *_lastLocationEvent;
+ NSMutableDictionary *_pendingRequests;
+}
+
+#pragma mark - Lifecycle
+
+- (instancetype)initWithBridge:(RCTBridge *)bridge
+{
+ if (self = [super init]) {
+ _eventDispatcher = bridge.eventDispatcher;
+ _pendingRequests = [[NSMutableDictionary alloc] init];
+ }
+ return self;
+}
+
+- (void)dealloc
+{
+ [_locationManager stopUpdatingLocation];
+}
+
+#pragma mark - Public API
+
+- (void)startObserving
+{
+ RCT_EXPORT();
+
+ dispatch_async(dispatch_get_main_queue(), ^{
+
+ // Create the location manager if this object does not
+ // already have one, and it must be created and accessed
+ // on the main thread
+ if (nil == _locationManager) {
+ _locationManager = [[CLLocationManager alloc] init];
+ }
+
+ _locationManager.delegate = self;
+ _locationManager.desiredAccuracy = RCTLocationAccuracy;
+
+ // Set a movement threshold for new events.
+ _locationManager.distanceFilter = RCTLocationAccuracy; // meters
+
+ if([_locationManager respondsToSelector:@selector(requestWhenInUseAuthorization)]) {
+ [_locationManager requestWhenInUseAuthorization];
+ }
+
+ [_locationManager startUpdatingLocation];
+
+ });
+}
+
+- (void)stopObserving
+{
+ RCT_EXPORT();
+
+ dispatch_async(dispatch_get_main_queue(), ^{
+ [_locationManager stopUpdatingLocation];
+ _lastLocationEvent = nil;
+ });
+}
+
+#pragma mark - CLLocationManagerDelegate
+
+- (void)locationManager:(CLLocationManager *)manager didUpdateLocations:(NSArray *)locations
+{
+ CLLocation *loc = [locations lastObject];
+ NSDictionary *event = @{
+ @"coords": @{
+ @"latitude": @(loc.coordinate.latitude),
+ @"longitude": @(loc.coordinate.longitude),
+ @"altitude": @(loc.altitude),
+ @"accuracy": @(RCTLocationAccuracy),
+ @"altitudeAccuracy": @(RCTLocationAccuracy),
+ @"heading": @(loc.course),
+ @"speed": @(loc.speed),
+ },
+ @"timestamp": @(CACurrentMediaTime())
+ };
+ [_eventDispatcher sendDeviceEventWithName:@"geoLocationDidChange" body:event];
+ NSArray *pendingRequestsCopy;
+
+ // TODO (#5906496): is this locking neccessary? If so, use something better than @synchronize
+ @synchronized(self) {
+
+ pendingRequestsCopy = [_pendingRequests allValues];
+ [_pendingRequests removeAllObjects];
+
+ _lastLocationEvent = event;
+ }
+
+ for (RCTPendingLocationRequest *request in pendingRequestsCopy) {
+ if (request.successBlock) {
+ request.successBlock(@[event]);
+ }
+ }
+}
+
+- (void)locationManager:(CLLocationManager *)manager didFailWithError:(NSError *)error
+{
+ NSArray *pendingRequestsCopy;
+
+ // TODO (#5906496): is this locking neccessary? If so, use something better than @synchronize
+ @synchronized(self) {
+ pendingRequestsCopy = [_pendingRequests allValues];
+ [_pendingRequests removeAllObjects];
+ }
+
+ NSString *errorMsg = @"User denied location service or location service not available.";
+ for (RCTPendingLocationRequest *request in pendingRequestsCopy) {
+ if (request.errorBlock) {
+ request.errorBlock(@[errorMsg]);
+ }
+ }
+}
+
+- (void)getCurrentPosition:(RCTResponseSenderBlock)geoSuccess withErrorCallback:(RCTResponseSenderBlock)geoError
+{
+ RCT_EXPORT();
+
+ NSDictionary *lastLocationCopy;
+ // TODO (#5906496): is this locking neccessary? If so, use something better than @synchronize
+ @synchronized(self) {
+ if (![CLLocationManager locationServicesEnabled] || [CLLocationManager authorizationStatus] == kCLAuthorizationStatusDenied) {
+ if (geoError) {
+ NSString *errorMsg = @"User denied location service or location service not available.";
+ geoError(@[errorMsg]);
+ return;
+ }
+ }
+
+ // If a request for the current position comes in before the OS has informed us, we wait for the first
+ // OS event and then call our callbacks. This obviates the need for handling of the otherwise
+ // common failure case of requesting the geolocation until it succeeds, assuming we would have
+ // instead returned an error if it wasn't yet available.
+ if (!_lastLocationEvent) {
+ NSInteger requestID = [_pendingRequests count];
+ RCTPendingLocationRequest *request = [[RCTPendingLocationRequest alloc] init];
+ request.successBlock = geoSuccess;
+ request.errorBlock = geoError;
+ _pendingRequests[@(requestID)] = request;
+ return;
+ } else {
+ lastLocationCopy = [_lastLocationEvent copy];
+ }
+ }
+ if (geoSuccess) {
+ geoSuccess(@[lastLocationCopy]);
+ }
+}
+
+@end
diff --git a/ReactKit/ReactKit.xcodeproj/project.pbxproj b/ReactKit/ReactKit.xcodeproj/project.pbxproj
index 3067cd8e4..721556306 100644
--- a/ReactKit/ReactKit.xcodeproj/project.pbxproj
+++ b/ReactKit/ReactKit.xcodeproj/project.pbxproj
@@ -32,6 +32,7 @@
13E067561A70F44B002CDEE1 /* RCTViewManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 13E0674E1A70F44B002CDEE1 /* RCTViewManager.m */; };
13E067571A70F44B002CDEE1 /* RCTView.m in Sources */ = {isa = PBXBuildFile; fileRef = 13E067501A70F44B002CDEE1 /* RCTView.m */; };
13E067591A70F44B002CDEE1 /* UIView+ReactKit.m in Sources */ = {isa = PBXBuildFile; fileRef = 13E067541A70F44B002CDEE1 /* UIView+ReactKit.m */; };
+ 5F5F0D991A9E456B001279FA /* RCTLocationObserver.m in Sources */ = {isa = PBXBuildFile; fileRef = 5F5F0D981A9E456B001279FA /* RCTLocationObserver.m */; };
830A229E1A66C68A008503DA /* RCTRootView.m in Sources */ = {isa = PBXBuildFile; fileRef = 830A229D1A66C68A008503DA /* RCTRootView.m */; };
830BA4551A8E3BDA00D53203 /* RCTCache.m in Sources */ = {isa = PBXBuildFile; fileRef = 830BA4541A8E3BDA00D53203 /* RCTCache.m */; };
832348161A77A5AA00B55238 /* Layout.c in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FC71A68125100A75B9A /* Layout.c */; };
@@ -113,6 +114,8 @@
13E067541A70F44B002CDEE1 /* UIView+ReactKit.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "UIView+ReactKit.m"; sourceTree = ""; };
13ED13891A80C9D40050A8F9 /* RCTPointerEvents.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTPointerEvents.h; sourceTree = ""; };
13EFFCCF1A98E6FE002607DC /* RCTJSMethodRegistrar.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTJSMethodRegistrar.h; sourceTree = ""; };
+ 5F5F0D971A9E456B001279FA /* RCTLocationObserver.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTLocationObserver.h; sourceTree = ""; };
+ 5F5F0D981A9E456B001279FA /* RCTLocationObserver.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTLocationObserver.m; sourceTree = ""; };
830213F31A654E0800B993E6 /* RCTBridgeModule.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = RCTBridgeModule.h; sourceTree = ""; };
830A229C1A66C68A008503DA /* RCTRootView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTRootView.h; sourceTree = ""; };
830A229D1A66C68A008503DA /* RCTRootView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTRootView.m; sourceTree = ""; };
@@ -177,6 +180,8 @@
13B07FE01A69315300A75B9A /* Modules */ = {
isa = PBXGroup;
children = (
+ 5F5F0D971A9E456B001279FA /* RCTLocationObserver.h */,
+ 5F5F0D981A9E456B001279FA /* RCTLocationObserver.m */,
13B07FE71A69327A00A75B9A /* RCTAlertManager.h */,
13B07FE81A69327A00A75B9A /* RCTAlertManager.m */,
13B07FE91A69327A00A75B9A /* RCTExceptionsManager.h */,
@@ -376,6 +381,7 @@
13B0801E1A69489C00A75B9A /* RCTTextField.m in Sources */,
13B07FEF1A69327A00A75B9A /* RCTAlertManager.m in Sources */,
83CBBACC1A6023D300E9B192 /* RCTConvert.m in Sources */,
+ 5F5F0D991A9E456B001279FA /* RCTLocationObserver.m in Sources */,
830A229E1A66C68A008503DA /* RCTRootView.m in Sources */,
1302F0FD1A78550100EBEF02 /* RCTStaticImage.m in Sources */,
13B07FF01A69327A00A75B9A /* RCTExceptionsManager.m in Sources */,