import Capacitor import IONGeolocationLib import UIKit import Combine @objc(GeolocationPlugin) public class GeolocationPlugin: CAPPlugin, CAPBridgedPlugin { public let identifier = "GeolocationPlugin" public let jsName = "Geolocation" public let pluginMethods: [CAPPluginMethod] = [ .init(name: "getCurrentPosition", returnType: CAPPluginReturnPromise), .init(name: "watchPosition", returnType: CAPPluginReturnCallback), .init(name: "clearWatch", returnType: CAPPluginReturnPromise), .init(name: "checkPermissions", returnType: CAPPluginReturnPromise), .init(name: "requestPermissions", returnType: CAPPluginReturnPromise) ] private var locationService: (any IONGLOCService)? private var cancellables = Set() private var locationCancellable: AnyCancellable? private var callbackManager: GeolocationCallbackManager? private var statusInitialized = false private var locationInitialized: Bool = false override public func load() { self.locationService = IONGLOCManagerWrapper() self.callbackManager = .init(capacitorBridge: bridge) NotificationCenter.default.addObserver( self, selector: #selector(appDidBecomeActive), name: UIApplication.didBecomeActiveNotification, object: nil ) } @objc private func appDidBecomeActive() { if let watchCallbacksEmpty = callbackManager?.watchCallbacks.isEmpty, !watchCallbacksEmpty { print("App became active. Restarting location monitoring for watch callbacks.") locationCancellable?.cancel() locationCancellable = nil locationInitialized = false locationService?.stopMonitoringLocation() locationService?.startMonitoringLocation() bindLocationPublisher() } } deinit { NotificationCenter.default.removeObserver(self) } @objc func getCurrentPosition(_ call: CAPPluginCall) { shouldSetupBindings() let enableHighAccuracy = call.getBool(Constants.Arguments.enableHighAccuracy, false) handleLocationRequest(enableHighAccuracy, call: call) } @objc func watchPosition(_ call: CAPPluginCall) { shouldSetupBindings() let enableHighAccuracy = call.getBool(Constants.Arguments.enableHighAccuracy, false) let watchUUID = call.callbackId handleLocationRequest(enableHighAccuracy, watchUUID: watchUUID, call: call) } @objc func clearWatch(_ call: CAPPluginCall) { shouldSetupBindings() guard let callbackId = call.getString(Constants.Arguments.id) else { callbackManager?.sendError(.inputArgumentsIssue(target: .clearWatch)) return } callbackManager?.clearWatchCallbackIfExists(callbackId) if (callbackManager?.watchCallbacks.isEmpty) ?? false { locationService?.stopMonitoringLocation() locationCancellable?.cancel() locationCancellable = nil locationInitialized = false } callbackManager?.sendSuccess(call) } @objc override public func checkPermissions(_ call: CAPPluginCall) { guard checkIfLocationServicesAreEnabled(call) else { return } let status = switch locationService?.authorisationStatus { case .restricted, .denied: Constants.AuthorisationStatus.Status.denied case .authorisedAlways, .authorisedWhenInUse: Constants.AuthorisationStatus.Status.granted default: Constants.AuthorisationStatus.Status.prompt } let callResultData = [ Constants.AuthorisationStatus.ResultKey.location: status, Constants.AuthorisationStatus.ResultKey.coarseLocation: status ] callbackManager?.sendSuccess(call, with: callResultData) } @objc override public func requestPermissions(_ call: CAPPluginCall) { guard checkIfLocationServicesAreEnabled(call) else { return } if locationService?.authorisationStatus == .notDetermined { shouldSetupBindings() callbackManager?.addRequestPermissionsCallback(capacitorCall: call) } else { checkPermissions(call) } } } private extension GeolocationPlugin { func shouldSetupBindings() { bindAuthorisationStatusPublisher() bindLocationPublisher() } func bindAuthorisationStatusPublisher() { guard !statusInitialized else { return } statusInitialized = true locationService?.authorisationStatusPublisher .sink(receiveValue: { [weak self] status in guard let self else { return } switch status { case .denied: self.onLocationPermissionNotGranted(error: .permissionDenied) case .notDetermined: self.requestLocationAuthorisation(type: .whenInUse) case .restricted: self.onLocationPermissionNotGranted(error: .permissionRestricted) case .authorisedAlways, .authorisedWhenInUse: self.onLocationPermissionGranted() @unknown default: break } }) .store(in: &cancellables) } func bindLocationPublisher() { guard !locationInitialized else { return } locationInitialized = true locationCancellable = locationService?.currentLocationPublisher .catch { [weak self] error -> AnyPublisher in print("An error was found while retrieving the location: \(error)") if case IONGLOCLocationError.locationUnavailable = error { print("Location unavailable (likely due to backgrounding). Keeping watch callbacks alive.") self?.callbackManager?.sendError(.positionUnavailable) return Empty() .eraseToAnyPublisher() } else { self?.callbackManager?.sendError(.positionUnavailable) return Empty() .eraseToAnyPublisher() } } .sink(receiveValue: { [weak self] position in self?.callbackManager?.sendSuccess(with: position) }) } func requestLocationAuthorisation(type requestType: IONGLOCAuthorisationRequestType) { DispatchQueue.global(qos: .background).async { guard self.checkIfLocationServicesAreEnabled() else { return } self.locationService?.requestAuthorisation(withType: requestType) } } func checkIfLocationServicesAreEnabled(_ call: CAPPluginCall? = nil) -> Bool { guard locationService?.areLocationServicesEnabled() == true else { call.map { callbackManager?.sendError($0, error: .locationServicesDisabled) } ?? callbackManager?.sendError(.locationServicesDisabled) return false } return true } func onLocationPermissionNotGranted(error: GeolocationError) { let shouldNotifyRequestPermissionsResult = callbackManager?.requestPermissionsCallbacks.isEmpty == false let shouldNotifyPermissionError = callbackManager?.locationCallbacks.isEmpty == false || callbackManager?.watchCallbacks.isEmpty == false if shouldNotifyRequestPermissionsResult { self.callbackManager?.sendRequestPermissionsSuccess(Constants.AuthorisationStatus.Status.denied) } if shouldNotifyPermissionError { self.callbackManager?.sendError(error) } } func onLocationPermissionGranted() { let shouldNotifyPermissionGranted = callbackManager?.requestPermissionsCallbacks.isEmpty == false // should request location if callbacks below exist and are not empty let shouldRequestCurrentPosition = callbackManager?.locationCallbacks.isEmpty == false let shouldRequestLocationMonitoring = callbackManager?.watchCallbacks.isEmpty == false if shouldNotifyPermissionGranted { callbackManager?.sendRequestPermissionsSuccess(Constants.AuthorisationStatus.Status.granted) } if shouldRequestCurrentPosition { locationService?.requestSingleLocation() } if shouldRequestLocationMonitoring { locationService?.startMonitoringLocation() } } func handleLocationRequest(_ enableHighAccuracy: Bool, watchUUID: String? = nil, call: CAPPluginCall) { bindLocationPublisher() let configurationModel = IONGLOCConfigurationModel(enableHighAccuracy: enableHighAccuracy) locationService?.updateConfiguration(configurationModel) if let watchUUID { callbackManager?.addWatchCallback(watchUUID, capacitorCall: call) } else { callbackManager?.addLocationCallback(capacitorCall: call) } switch locationService?.authorisationStatus { case .authorisedAlways, .authorisedWhenInUse: onLocationPermissionGranted() case .denied: callbackManager?.sendError(.permissionDenied) case .restricted: callbackManager?.sendError(.permissionRestricted) default: break } } }