- App.tsx: full navigation (Auth stack + Main tabs with 5 screens) - Auth: LoginScreen, RegisterScreen, ForgotPasswordScreen - HomeScreen: dashboard with IoT metrics, weather widget, alerts, quick actions, sensors - MapScreen: interactive map with layer toggles (6 layers) - MarketplaceScreen: categories (6), products (5), search - ChatScreen: AI chat with quick prompts (4), bot responses - ProfileScreen: user info, stats, menu (9 items), logout - AlertsScreen: alert list with severity, acknowledge - SensorsScreen: sensor list with type filters (6 types), search - ZonesScreen: zone cards with stats - SettingsScreen: language picker (FR/EN/ES/DE), privacy, about - Stores: iotStore (sensors, zones, alerts), notificationStore, uiStore + i18n - Hooks: useSensors, useAlerts, useNotifications, useLocation - Components: Card, Button, LoadingSpinner, ErrorBoundary, Header - Services: iotService, notificationService (with axios API client) - Utils: formatters (temp, AQI, noise, dates), validators (email, password, IBAN) - Theme: colors.ts with full design system (Blue Ocean palette) - Ditto: fixed MongoDB connection, new JWT secrets, official gateway image
628 lines
24 KiB
Objective-C
628 lines
24 KiB
Objective-C
// Copyright 2016-present 650 Industries. All rights reserved.
|
|
|
|
#import <EXLocation/EXLocation.h>
|
|
#import <EXLocation/EXLocationDelegate.h>
|
|
#import <EXLocation/EXLocationTaskConsumer.h>
|
|
#import <EXLocation/EXGeofencingTaskConsumer.h>
|
|
#import <EXLocation/EXLocationPermissionRequester.h>
|
|
#import <EXLocation/EXForegroundPermissionRequester.h>
|
|
#import <EXLocation/EXBackgroundLocationPermissionRequester.h>
|
|
|
|
#import <CoreLocation/CLLocationManager.h>
|
|
#import <CoreLocation/CLLocationManagerDelegate.h>
|
|
#import <CoreLocation/CLHeading.h>
|
|
#import <CoreLocation/CLGeocoder.h>
|
|
#import <CoreLocation/CLPlacemark.h>
|
|
#import <CoreLocation/CLError.h>
|
|
#import <CoreLocation/CLCircularRegion.h>
|
|
|
|
#import <ExpoModulesCore/EXEventEmitterService.h>
|
|
|
|
#import <ExpoModulesCore/EXPermissionsInterface.h>
|
|
#import <ExpoModulesCore/EXPermissionsMethodsDelegate.h>
|
|
#import <ExpoModulesCore/EXTaskManagerInterface.h>
|
|
|
|
NS_ASSUME_NONNULL_BEGIN
|
|
|
|
NSString * const EXLocationChangedEventName = @"Expo.locationChanged";
|
|
NSString * const EXHeadingChangedEventName = @"Expo.headingChanged";
|
|
|
|
@interface EXLocation ()
|
|
|
|
@property (nonatomic, strong) NSMutableDictionary<NSNumber *, EXLocationDelegate*> *delegates;
|
|
@property (nonatomic, strong) NSMutableSet<EXLocationDelegate *> *retainedDelegates;
|
|
@property (nonatomic, weak) id<EXEventEmitterService> eventEmitter;
|
|
@property (nonatomic, weak) id<EXPermissionsInterface> permissionsManager;
|
|
@property (nonatomic, weak) id<EXTaskManagerInterface> tasksManager;
|
|
|
|
@end
|
|
|
|
@implementation EXLocation
|
|
|
|
EX_EXPORT_MODULE(ExpoLocation);
|
|
|
|
- (instancetype)init
|
|
{
|
|
if (self = [super init]) {
|
|
_delegates = [NSMutableDictionary dictionary];
|
|
_retainedDelegates = [NSMutableSet set];
|
|
}
|
|
return self;
|
|
}
|
|
|
|
- (void)setModuleRegistry:(EXModuleRegistry *)moduleRegistry
|
|
{
|
|
_eventEmitter = [moduleRegistry getModuleImplementingProtocol:@protocol(EXEventEmitterService)];
|
|
_tasksManager = [moduleRegistry getModuleImplementingProtocol:@protocol(EXTaskManagerInterface)];
|
|
|
|
_permissionsManager = [moduleRegistry getModuleImplementingProtocol:@protocol(EXPermissionsInterface)];
|
|
[EXPermissionsMethodsDelegate registerRequesters:@[
|
|
[EXLocationPermissionRequester new],
|
|
[EXForegroundPermissionRequester new],
|
|
[EXBackgroundLocationPermissionRequester new]
|
|
] withPermissionsManager:_permissionsManager];
|
|
}
|
|
|
|
- (dispatch_queue_t)methodQueue
|
|
{
|
|
// Location managers must be created on the main thread
|
|
return dispatch_get_main_queue();
|
|
}
|
|
|
|
# pragma mark - EXEventEmitter
|
|
|
|
- (NSArray<NSString *> *)supportedEvents
|
|
{
|
|
return @[EXLocationChangedEventName, EXHeadingChangedEventName];
|
|
}
|
|
|
|
- (void)startObserving {}
|
|
- (void)stopObserving {}
|
|
|
|
# pragma mark - Exported methods
|
|
|
|
EX_EXPORT_METHOD_AS(getProviderStatusAsync,
|
|
resolver:(EXPromiseResolveBlock)resolve
|
|
rejecter:(EXPromiseRejectBlock)reject)
|
|
{
|
|
resolve(@{
|
|
@"locationServicesEnabled": @([CLLocationManager locationServicesEnabled]),
|
|
@"backgroundModeEnabled": @([_tasksManager hasBackgroundModeEnabled:@"location"]),
|
|
});
|
|
}
|
|
|
|
|
|
EX_EXPORT_METHOD_AS(getCurrentPositionAsync,
|
|
options:(NSDictionary *)options
|
|
resolver:(EXPromiseResolveBlock)resolve
|
|
rejecter:(EXPromiseRejectBlock)reject)
|
|
{
|
|
if (![self checkForegroundPermissions:reject]) {
|
|
return;
|
|
}
|
|
|
|
CLLocationManager *locMgr = [self locationManagerWithOptions:options];
|
|
|
|
__weak typeof(self) weakSelf = self;
|
|
__block EXLocationDelegate *delegate;
|
|
|
|
delegate = [[EXLocationDelegate alloc] initWithId:nil withLocMgr:locMgr onUpdateLocations:^(NSArray<CLLocation *> * _Nonnull locations) {
|
|
if (delegate != nil) {
|
|
if (locations.lastObject != nil) {
|
|
resolve([EXLocation exportLocation:locations.lastObject]);
|
|
} else {
|
|
reject(@"E_LOCATION_NOT_FOUND", @"Current location not found.", nil);
|
|
}
|
|
[weakSelf.retainedDelegates removeObject:delegate];
|
|
delegate = nil;
|
|
}
|
|
} onUpdateHeadings:nil onError:^(NSError *error) {
|
|
reject(@"E_LOCATION_UNAVAILABLE", [@"Cannot obtain current location: " stringByAppendingString:error.description], nil);
|
|
}];
|
|
|
|
// retain location manager delegate so it will not dealloc until onUpdateLocations gets called
|
|
[_retainedDelegates addObject:delegate];
|
|
|
|
locMgr.delegate = delegate;
|
|
[locMgr requestLocation];
|
|
}
|
|
|
|
EX_EXPORT_METHOD_AS(watchPositionImplAsync,
|
|
watchId:(nonnull NSNumber *)watchId
|
|
options:(NSDictionary *)options
|
|
resolver:(EXPromiseResolveBlock)resolve
|
|
rejecter:(EXPromiseRejectBlock)reject)
|
|
{
|
|
if (![self checkForegroundPermissions:reject]) {
|
|
return;
|
|
}
|
|
|
|
__weak typeof(self) weakSelf = self;
|
|
CLLocationManager *locMgr = [self locationManagerWithOptions:options];
|
|
|
|
EXLocationDelegate *delegate = [[EXLocationDelegate alloc] initWithId:watchId withLocMgr:locMgr onUpdateLocations:^(NSArray<CLLocation *> *locations) {
|
|
if (locations.lastObject != nil && weakSelf != nil) {
|
|
__strong typeof(weakSelf) strongSelf = weakSelf;
|
|
|
|
CLLocation *loc = locations.lastObject;
|
|
NSDictionary *body = @{
|
|
@"watchId": watchId,
|
|
@"location": [EXLocation exportLocation:loc],
|
|
};
|
|
|
|
[strongSelf->_eventEmitter sendEventWithName:EXLocationChangedEventName body:body];
|
|
}
|
|
} onUpdateHeadings:nil onError:^(NSError *error) {
|
|
// TODO: report errors
|
|
// (ben) error could be (among other things):
|
|
// - kCLErrorDenied - we should use the same UNAUTHORIZED behavior as elsewhere
|
|
// - kCLErrorLocationUnknown - we can actually ignore this error and keep tracking
|
|
// location (I think -- my knowledge might be a few months out of date)
|
|
}];
|
|
|
|
_delegates[delegate.watchId] = delegate;
|
|
locMgr.delegate = delegate;
|
|
[locMgr startUpdatingLocation];
|
|
resolve([NSNull null]);
|
|
}
|
|
|
|
EX_EXPORT_METHOD_AS(getLastKnownPositionAsync,
|
|
getLastKnownPositionWithOptions:(NSDictionary *)options
|
|
resolve:(EXPromiseResolveBlock)resolve
|
|
rejecter:(EXPromiseRejectBlock)reject)
|
|
{
|
|
if (![self checkForegroundPermissions:reject]) {
|
|
return;
|
|
}
|
|
CLLocation *location = [[self locationManagerWithOptions:nil] location];
|
|
|
|
if ([self.class isLocation:location validWithOptions:options]) {
|
|
resolve([EXLocation exportLocation:location]);
|
|
} else {
|
|
resolve([NSNull null]);
|
|
}
|
|
}
|
|
|
|
// Watch method for getting compass updates
|
|
EX_EXPORT_METHOD_AS(watchDeviceHeading,
|
|
watchHeadingWithWatchId:(nonnull NSNumber *)watchId
|
|
resolve:(EXPromiseResolveBlock)resolve
|
|
reject:(EXPromiseRejectBlock)reject) {
|
|
if (![self checkForegroundPermissions:reject]) {
|
|
return;
|
|
}
|
|
|
|
__weak typeof(self) weakSelf = self;
|
|
CLLocationManager *locMgr = [[CLLocationManager alloc] init];
|
|
|
|
locMgr.distanceFilter = kCLDistanceFilterNone;
|
|
locMgr.desiredAccuracy = kCLLocationAccuracyBest;
|
|
locMgr.allowsBackgroundLocationUpdates = NO;
|
|
|
|
EXLocationDelegate *delegate = [[EXLocationDelegate alloc] initWithId:watchId withLocMgr:locMgr onUpdateLocations: nil onUpdateHeadings:^(CLHeading *newHeading) {
|
|
if (newHeading != nil && weakSelf != nil) {
|
|
__strong typeof(weakSelf) strongSelf = weakSelf;
|
|
NSNumber *accuracy;
|
|
|
|
// Convert iOS heading accuracy to Android system
|
|
// 3: high accuracy, 2: medium, 1: low, 0: none
|
|
if (newHeading.headingAccuracy > 50 || newHeading.headingAccuracy < 0) {
|
|
accuracy = @(0);
|
|
} else if (newHeading.headingAccuracy > 35) {
|
|
accuracy = @(1);
|
|
} else if (newHeading.headingAccuracy > 20) {
|
|
accuracy = @(2);
|
|
} else {
|
|
accuracy = @(3);
|
|
}
|
|
NSDictionary *body = @{@"watchId": watchId,
|
|
@"heading": @{
|
|
@"trueHeading": @(newHeading.trueHeading),
|
|
@"magHeading": @(newHeading.magneticHeading),
|
|
@"accuracy": accuracy,
|
|
},
|
|
};
|
|
[strongSelf->_eventEmitter sendEventWithName:EXHeadingChangedEventName body:body];
|
|
}
|
|
} onError:^(NSError *error) {
|
|
// Error getting updates
|
|
}];
|
|
|
|
_delegates[delegate.watchId] = delegate;
|
|
locMgr.delegate = delegate;
|
|
[locMgr startUpdatingHeading];
|
|
resolve([NSNull null]);
|
|
}
|
|
|
|
EX_EXPORT_METHOD_AS(removeWatchAsync,
|
|
watchId:(nonnull NSNumber *)watchId
|
|
resolver:(EXPromiseResolveBlock)resolve
|
|
rejecter:(EXPromiseRejectBlock)reject)
|
|
{
|
|
EXLocationDelegate *delegate = _delegates[watchId];
|
|
|
|
if (delegate) {
|
|
// Unsuscribe from both location and heading updates
|
|
[delegate.locMgr stopUpdatingLocation];
|
|
[delegate.locMgr stopUpdatingHeading];
|
|
delegate.locMgr.delegate = nil;
|
|
[_delegates removeObjectForKey:watchId];
|
|
}
|
|
resolve([NSNull null]);
|
|
}
|
|
|
|
EX_EXPORT_METHOD_AS(geocodeAsync,
|
|
address:(nonnull NSString *)address
|
|
resolver:(EXPromiseResolveBlock)resolve
|
|
rejecter:(EXPromiseRejectBlock)reject)
|
|
{
|
|
CLGeocoder *geocoder = [[CLGeocoder alloc] init];
|
|
|
|
[geocoder geocodeAddressString:address completionHandler:^(NSArray* placemarks, NSError* error){
|
|
if (!error) {
|
|
NSMutableArray *results = [NSMutableArray arrayWithCapacity:placemarks.count];
|
|
for (CLPlacemark* placemark in placemarks) {
|
|
CLLocation *location = placemark.location;
|
|
[results addObject:@{
|
|
@"latitude": @(location.coordinate.latitude),
|
|
@"longitude": @(location.coordinate.longitude),
|
|
@"altitude": @(location.altitude),
|
|
@"accuracy": @(location.horizontalAccuracy),
|
|
}];
|
|
}
|
|
resolve(results);
|
|
} else if (error.code == kCLErrorGeocodeFoundNoResult || error.code == kCLErrorGeocodeFoundPartialResult) {
|
|
resolve(@[]);
|
|
} else if (error.code == kCLErrorNetwork) {
|
|
reject(@"E_RATE_EXCEEDED", @"Rate limit exceeded - too many requests", error);
|
|
} else {
|
|
reject(@"E_GEOCODING_FAILED", @"Error while geocoding an address", error);
|
|
}
|
|
}];
|
|
}
|
|
|
|
EX_EXPORT_METHOD_AS(reverseGeocodeAsync,
|
|
locationMap:(nonnull NSDictionary *)locationMap
|
|
resolver:(EXPromiseResolveBlock)resolve
|
|
rejecter:(EXPromiseRejectBlock)reject)
|
|
{
|
|
CLGeocoder *geocoder = [[CLGeocoder alloc] init];
|
|
CLLocation *location = [[CLLocation alloc] initWithLatitude:[locationMap[@"latitude"] floatValue] longitude:[locationMap[@"longitude"] floatValue]];
|
|
|
|
[geocoder reverseGeocodeLocation:location completionHandler:^(NSArray* placemarks, NSError* error){
|
|
if (!error) {
|
|
NSMutableArray *results = [NSMutableArray arrayWithCapacity:placemarks.count];
|
|
for (CLPlacemark* placemark in placemarks) {
|
|
NSDictionary *address = @{
|
|
@"city": UMNullIfNil(placemark.locality),
|
|
@"district": UMNullIfNil(placemark.subLocality),
|
|
@"streetNumber": UMNullIfNil(placemark.subThoroughfare),
|
|
@"street": EXNullIfNil(placemark.thoroughfare),
|
|
@"region": EXNullIfNil(placemark.administrativeArea),
|
|
@"subregion": EXNullIfNil(placemark.subAdministrativeArea),
|
|
@"country": EXNullIfNil(placemark.country),
|
|
@"postalCode": EXNullIfNil(placemark.postalCode),
|
|
@"name": EXNullIfNil(placemark.name),
|
|
@"isoCountryCode": EXNullIfNil(placemark.ISOcountryCode),
|
|
@"timezone": EXNullIfNil(placemark.timeZone.name),
|
|
};
|
|
[results addObject:address];
|
|
}
|
|
resolve(results);
|
|
} else if (error.code == kCLErrorGeocodeFoundNoResult || error.code == kCLErrorGeocodeFoundPartialResult) {
|
|
resolve(@[]);
|
|
} else if (error.code == kCLErrorNetwork) {
|
|
reject(@"E_RATE_EXCEEDED", @"Rate limit exceeded - too many requests", error);
|
|
} else {
|
|
reject(@"E_REVGEOCODING_FAILED", @"Error while reverse-geocoding a location", error);
|
|
}
|
|
}];
|
|
}
|
|
|
|
EX_EXPORT_METHOD_AS(getPermissionsAsync,
|
|
getPermissionsAsync:(EXPromiseResolveBlock)resolve
|
|
rejecter:(EXPromiseRejectBlock)reject)
|
|
{
|
|
[EXPermissionsMethodsDelegate getPermissionWithPermissionsManager:_permissionsManager
|
|
withRequester:[EXLocationPermissionRequester class]
|
|
resolve:resolve
|
|
reject:reject];
|
|
}
|
|
|
|
EX_EXPORT_METHOD_AS(requestPermissionsAsync,
|
|
requestPermissionsAsync:(EXPromiseResolveBlock)resolve
|
|
rejecter:(EXPromiseRejectBlock)reject)
|
|
{
|
|
[EXPermissionsMethodsDelegate askForPermissionWithPermissionsManager:_permissionsManager
|
|
withRequester:[EXLocationPermissionRequester class]
|
|
resolve:resolve
|
|
reject:reject];
|
|
}
|
|
|
|
EX_EXPORT_METHOD_AS(getForegroundPermissionsAsync,
|
|
getForegroundPermissionsAsync:(EXPromiseResolveBlock)resolve
|
|
rejecter:(EXPromiseRejectBlock)reject)
|
|
{
|
|
[EXPermissionsMethodsDelegate getPermissionWithPermissionsManager:_permissionsManager
|
|
withRequester:[EXForegroundPermissionRequester class]
|
|
resolve:resolve
|
|
reject:reject];
|
|
}
|
|
|
|
EX_EXPORT_METHOD_AS(requestForegroundPermissionsAsync,
|
|
requestForegroundPermissionsAsync:(EXPromiseResolveBlock)resolve
|
|
rejecter:(EXPromiseRejectBlock)reject)
|
|
{
|
|
[EXPermissionsMethodsDelegate askForPermissionWithPermissionsManager:_permissionsManager
|
|
withRequester:[EXForegroundPermissionRequester class]
|
|
resolve:resolve
|
|
reject:reject];
|
|
}
|
|
|
|
EX_EXPORT_METHOD_AS(getBackgroundPermissionsAsync,
|
|
getBackgroundPermissionsAsync:(EXPromiseResolveBlock)resolve
|
|
rejecter:(EXPromiseRejectBlock)reject)
|
|
{
|
|
[EXPermissionsMethodsDelegate getPermissionWithPermissionsManager:_permissionsManager
|
|
withRequester:[EXBackgroundLocationPermissionRequester class]
|
|
resolve:resolve
|
|
reject:reject];
|
|
}
|
|
|
|
EX_EXPORT_METHOD_AS(requestBackgroundPermissionsAsync,
|
|
requestBackgroundPermissionsAsync:(EXPromiseResolveBlock)resolve
|
|
rejecter:(EXPromiseRejectBlock)reject)
|
|
{
|
|
[EXPermissionsMethodsDelegate askForPermissionWithPermissionsManager:_permissionsManager
|
|
withRequester:[EXBackgroundLocationPermissionRequester class]
|
|
resolve:resolve
|
|
reject:reject];
|
|
}
|
|
|
|
EX_EXPORT_METHOD_AS(hasServicesEnabledAsync,
|
|
hasServicesEnabled:(EXPromiseResolveBlock)resolve
|
|
reject:(EXPromiseRejectBlock)reject)
|
|
{
|
|
BOOL servicesEnabled = [CLLocationManager locationServicesEnabled];
|
|
resolve(@(servicesEnabled));
|
|
}
|
|
|
|
# pragma mark - Background location
|
|
|
|
EX_EXPORT_METHOD_AS(startLocationUpdatesAsync,
|
|
startLocationUpdatesForTaskWithName:(nonnull NSString *)taskName
|
|
withOptions:(nonnull NSDictionary *)options
|
|
resolve:(EXPromiseResolveBlock)resolve
|
|
reject:(EXPromiseRejectBlock)reject)
|
|
{
|
|
// There are two ways of starting this service.
|
|
// 1. As a background location service, this requires the background location permission.
|
|
// 2. As a user-initiated foreground service, this does NOT require the background location permission.
|
|
// Unfortunately, we cannot distinguish between those cases.
|
|
// So we only check foreground permission which needs to be granted in both cases.
|
|
if (![self checkForegroundPermissions:reject] || ![self checkTaskManagerExists:reject] || ![self checkBackgroundServices:reject]) {
|
|
return;
|
|
}
|
|
if (![CLLocationManager significantLocationChangeMonitoringAvailable]) {
|
|
return reject(@"E_SIGNIFICANT_CHANGES_UNAVAILABLE", @"Significant location changes monitoring is not available.", nil);
|
|
}
|
|
|
|
@try {
|
|
[_tasksManager registerTaskWithName:taskName consumer:[EXLocationTaskConsumer class] options:options];
|
|
}
|
|
@catch (NSException *e) {
|
|
return reject(e.name, e.reason, nil);
|
|
}
|
|
resolve(nil);
|
|
}
|
|
|
|
EX_EXPORT_METHOD_AS(stopLocationUpdatesAsync,
|
|
stopLocationUpdatesForTaskWithName:(NSString *)taskName
|
|
resolve:(EXPromiseResolveBlock)resolve
|
|
reject:(EXPromiseRejectBlock)reject)
|
|
{
|
|
if (![self checkTaskManagerExists:reject]) {
|
|
return;
|
|
}
|
|
|
|
@try {
|
|
[_tasksManager unregisterTaskWithName:taskName consumerClass:[EXLocationTaskConsumer class]];
|
|
} @catch (NSException *e) {
|
|
return reject(e.name, e.reason, nil);
|
|
}
|
|
resolve(nil);
|
|
}
|
|
|
|
EX_EXPORT_METHOD_AS(hasStartedLocationUpdatesAsync,
|
|
hasStartedLocationUpdatesForTaskWithName:(nonnull NSString *)taskName
|
|
resolve:(EXPromiseResolveBlock)resolve
|
|
reject:(EXPromiseRejectBlock)reject)
|
|
{
|
|
if (![self checkTaskManagerExists:reject]) {
|
|
return;
|
|
}
|
|
|
|
resolve(@([_tasksManager taskWithName:taskName hasConsumerOfClass:[EXLocationTaskConsumer class]]));
|
|
}
|
|
|
|
# pragma mark - Geofencing
|
|
|
|
EX_EXPORT_METHOD_AS(startGeofencingAsync,
|
|
startGeofencingWithTaskName:(nonnull NSString *)taskName
|
|
withOptions:(nonnull NSDictionary *)options
|
|
resolve:(EXPromiseResolveBlock)resolve
|
|
reject:(EXPromiseRejectBlock)reject)
|
|
{
|
|
if (![self checkBackgroundPermissions:reject] || ![self checkTaskManagerExists:reject]) {
|
|
return;
|
|
}
|
|
if (![CLLocationManager isMonitoringAvailableForClass:[CLCircularRegion class]]) {
|
|
return reject(@"E_GEOFENCING_UNAVAILABLE", @"Geofencing is not available", nil);
|
|
}
|
|
|
|
@try {
|
|
[_tasksManager registerTaskWithName:taskName consumer:[EXGeofencingTaskConsumer class] options:options];
|
|
} @catch (NSException *e) {
|
|
return reject(e.name, e.reason, nil);
|
|
}
|
|
resolve(nil);
|
|
}
|
|
|
|
EX_EXPORT_METHOD_AS(stopGeofencingAsync,
|
|
stopGeofencingWithTaskName:(nonnull NSString *)taskName
|
|
resolve:(EXPromiseResolveBlock)resolve
|
|
reject:(EXPromiseRejectBlock)reject)
|
|
{
|
|
if (![self checkTaskManagerExists:reject]) {
|
|
return;
|
|
}
|
|
|
|
@try {
|
|
[_tasksManager unregisterTaskWithName:taskName consumerClass:[EXGeofencingTaskConsumer class]];
|
|
} @catch (NSException *e) {
|
|
return reject(e.name, e.reason, nil);
|
|
}
|
|
resolve(nil);
|
|
}
|
|
|
|
EX_EXPORT_METHOD_AS(hasStartedGeofencingAsync,
|
|
hasStartedGeofencingForTaskWithName:(NSString *)taskName
|
|
resolve:(EXPromiseResolveBlock)resolve
|
|
reject:(EXPromiseRejectBlock)reject)
|
|
{
|
|
if (![self checkTaskManagerExists:reject]) {
|
|
return;
|
|
}
|
|
|
|
resolve(@([_tasksManager taskWithName:taskName hasConsumerOfClass:[EXGeofencingTaskConsumer class]]));
|
|
}
|
|
|
|
# pragma mark - helpers
|
|
|
|
- (CLLocationManager *)locationManagerWithOptions:(nullable NSDictionary *)options
|
|
{
|
|
CLLocationManager *locMgr = [[CLLocationManager alloc] init];
|
|
locMgr.allowsBackgroundLocationUpdates = NO;
|
|
|
|
if (options) {
|
|
locMgr.distanceFilter = options[@"distanceInterval"] ? [options[@"distanceInterval"] doubleValue] ?: kCLDistanceFilterNone : kCLLocationAccuracyHundredMeters;
|
|
|
|
if (options[@"accuracy"]) {
|
|
EXLocationAccuracy accuracy = [options[@"accuracy"] unsignedIntegerValue] ?: EXLocationAccuracyBalanced;
|
|
locMgr.desiredAccuracy = [self.class CLLocationAccuracyFromOption:accuracy];
|
|
}
|
|
}
|
|
return locMgr;
|
|
}
|
|
|
|
- (BOOL)checkForegroundPermissions:(EXPromiseRejectBlock)reject
|
|
{
|
|
if (![CLLocationManager locationServicesEnabled]) {
|
|
reject(@"E_LOCATION_SERVICES_DISABLED", @"Location services are disabled", nil);
|
|
return NO;
|
|
}
|
|
if (![_permissionsManager hasGrantedPermissionUsingRequesterClass:[EXForegroundPermissionRequester class]]) {
|
|
reject(@"E_NO_PERMISSIONS", @"LOCATION_FOREGROUND permission is required to do this operation.", nil);
|
|
return NO;
|
|
}
|
|
return YES;
|
|
}
|
|
|
|
- (BOOL)checkBackgroundPermissions:(EXPromiseRejectBlock)reject
|
|
{
|
|
if (![CLLocationManager locationServicesEnabled]) {
|
|
reject(@"E_LOCATION_SERVICES_DISABLED", @"Location services are disabled", nil);
|
|
return NO;
|
|
}
|
|
if (![_permissionsManager hasGrantedPermissionUsingRequesterClass:[EXBackgroundLocationPermissionRequester class]]) {
|
|
reject(@"E_NO_PERMISSIONS", @"LOCATION_BACKGROUND permission is required to do this operation.", nil);
|
|
return NO;
|
|
}
|
|
return YES;
|
|
}
|
|
|
|
- (BOOL)checkTaskManagerExists:(EXPromiseRejectBlock)reject
|
|
{
|
|
if (_tasksManager == nil) {
|
|
reject(@"E_TASKMANAGER_NOT_FOUND", @"`expo-task-manager` module is required to use background services.", nil);
|
|
return NO;
|
|
}
|
|
return YES;
|
|
}
|
|
|
|
- (BOOL)checkBackgroundServices:(EXPromiseRejectBlock)reject
|
|
{
|
|
if (![_tasksManager hasBackgroundModeEnabled:@"location"]) {
|
|
reject(@"E_BACKGROUND_SERVICES_DISABLED", @"Background Location has not been configured. To enable it, add `location` to `UIBackgroundModes` in Info.plist file.", nil);
|
|
return NO;
|
|
}
|
|
return YES;
|
|
}
|
|
|
|
# pragma mark - static helpers
|
|
|
|
+ (NSDictionary *)exportLocation:(CLLocation *)location
|
|
{
|
|
return @{
|
|
@"coords": @{
|
|
@"latitude": @(location.coordinate.latitude),
|
|
@"longitude": @(location.coordinate.longitude),
|
|
@"altitude": @(location.altitude),
|
|
@"accuracy": @(location.horizontalAccuracy),
|
|
@"altitudeAccuracy": @(location.verticalAccuracy),
|
|
@"heading": @(location.course),
|
|
@"speed": @(location.speed),
|
|
},
|
|
@"timestamp": @([location.timestamp timeIntervalSince1970] * 1000),
|
|
};
|
|
}
|
|
|
|
+ (CLLocationAccuracy)CLLocationAccuracyFromOption:(EXLocationAccuracy)accuracy
|
|
{
|
|
switch (accuracy) {
|
|
case EXLocationAccuracyLowest:
|
|
return kCLLocationAccuracyThreeKilometers;
|
|
case EXLocationAccuracyLow:
|
|
return kCLLocationAccuracyKilometer;
|
|
case EXLocationAccuracyBalanced:
|
|
return kCLLocationAccuracyHundredMeters;
|
|
case EXLocationAccuracyHigh:
|
|
return kCLLocationAccuracyNearestTenMeters;
|
|
case EXLocationAccuracyHighest:
|
|
return kCLLocationAccuracyBest;
|
|
case EXLocationAccuracyBestForNavigation:
|
|
return kCLLocationAccuracyBestForNavigation;
|
|
default:
|
|
return kCLLocationAccuracyHundredMeters;
|
|
}
|
|
}
|
|
|
|
+ (CLActivityType)CLActivityTypeFromOption:(NSInteger)activityType
|
|
{
|
|
if (activityType >= CLActivityTypeOther && activityType <= CLActivityTypeOtherNavigation) {
|
|
return activityType;
|
|
}
|
|
if (@available(iOS 12.0, *)) {
|
|
if (activityType == CLActivityTypeAirborne) {
|
|
return activityType;
|
|
}
|
|
}
|
|
return CLActivityTypeOther;
|
|
}
|
|
|
|
+ (BOOL)isLocation:(nullable CLLocation *)location validWithOptions:(nullable NSDictionary *)options
|
|
{
|
|
if (location == nil) {
|
|
return NO;
|
|
}
|
|
NSTimeInterval maxAge = options[@"maxAge"] ? [options[@"maxAge"] doubleValue] : DBL_MAX;
|
|
CLLocationAccuracy requiredAccuracy = options[@"requiredAccuracy"] ? [options[@"requiredAccuracy"] doubleValue] : DBL_MAX;
|
|
NSTimeInterval timeDiff = -location.timestamp.timeIntervalSinceNow;
|
|
|
|
return location != nil && timeDiff * 1000 <= maxAge && location.horizontalAccuracy <= requiredAccuracy;
|
|
}
|
|
|
|
@end
|
|
|
|
NS_ASSUME_NONNULL_END
|