@@ -481,7 +481,9 @@ struct CraftWebView: UIViewRepresentable {
481481 // Location
482482 private var locationManager : CLLocationManager ?
483483 private var singleLocationCallbackId : String ?
484+ private var singleLocationTimeoutWorkItem : DispatchWorkItem ?
484485 private var locationPermissionCallbackId : String ?
486+ private var locationPermissionRequiresAlways = false
485487 private var isWatchingLocation = false
486488 private var isRecordingLocation = false
487489 private var isLocationRecordingPaused = false
@@ -764,7 +766,7 @@ struct CraftWebView: UIViewRepresentable {
764766 // Geolocation
765767 case " getCurrentPosition " :
766768 if config. enableGeolocation {
767- getCurrentPosition ( callbackId: callbackId)
769+ getCurrentPosition ( body : body , callbackId: callbackId)
768770 } else {
769771 rejectCallback ( callbackId, error: " Geolocation is disabled " , code: " CAPABILITY_DISABLED " )
770772 }
@@ -2056,12 +2058,44 @@ struct CraftWebView: UIViewRepresentable {
20562058
20572059 // Geolocation
20582060 geolocation: {
2059- getCurrentPosition: function() {
2061+ getCurrentPosition: function(options) {
2062+ options = options || {};
20602063 var self = window.craft;
20612064 var id = 'cb_' + (++self._callbackId);
2062- window.webkit.messageHandlers.craft.postMessage({action: 'getCurrentPosition', callbackId: id});
2065+ var requestedTimeout = Number(options.timeout);
2066+ var timeoutMs = Number.isFinite(requestedTimeout) && requestedTimeout >= 0
2067+ ? requestedTimeout
2068+ : 30000;
20632069 return new Promise(function(resolve, reject) {
2064- self._callbacks[id] = {resolve: resolve, reject: reject};
2070+ var timeout;
2071+ self._callbacks[id] = {
2072+ resolve: function(value) { clearTimeout(timeout); resolve(value); },
2073+ reject: function(error) { clearTimeout(timeout); reject(error); }
2074+ };
2075+ timeout = setTimeout(function() {
2076+ if (!self._callbacks[id]) return;
2077+ delete self._callbacks[id];
2078+ var error = new Error('Location request timed out after ' + timeoutMs + 'ms');
2079+ error.name = 'GeolocationPositionError';
2080+ error.code = 3;
2081+ error.bridge = true;
2082+ reject(error);
2083+ }, timeoutMs);
2084+ try {
2085+ window.webkit.messageHandlers.craft.postMessage({
2086+ action: 'getCurrentPosition',
2087+ callbackId: id,
2088+ enableHighAccuracy: options.enableHighAccuracy === true,
2089+ timeout: timeoutMs,
2090+ maximumAge: Number.isFinite(Number(options.maximumAge))
2091+ ? Math.max(0, Number(options.maximumAge))
2092+ : 0
2093+ });
2094+ } catch (error) {
2095+ clearTimeout(timeout);
2096+ delete self._callbacks[id];
2097+ reject(error);
2098+ }
20652099 });
20662100 },
20672101 watchPosition: function(callback) {
@@ -3313,11 +3347,27 @@ struct CraftWebView: UIViewRepresentable {
33133347 }
33143348 switch permission {
33153349 case " location " , " locationAlways " :
3350+ guard let manager = locationManager else {
3351+ rejectCallback ( callbackId, error: " Geolocation is disabled " , code: " CAPABILITY_DISABLED " )
3352+ return
3353+ }
3354+ manager. delegate = self
3355+ let requiresAlways = permission == " locationAlways " || config. enableBackgroundLocation
3356+ let status = manager. authorizationStatus
3357+ let alreadyGranted = status == . authorizedAlways || ( !requiresAlways && status == . authorizedWhenInUse)
3358+ if alreadyGranted || status == . denied || status == . restricted {
3359+ resolveCallback ( callbackId, result: permissionStatus ( alreadyGranted, denied: status == . denied, restricted: status == . restricted) )
3360+ return
3361+ }
3362+ if let pendingCallbackId = locationPermissionCallbackId {
3363+ rejectCallback ( pendingCallbackId, error: " A newer location permission request replaced this request " , code: " REQUEST_REPLACED " )
3364+ }
33163365 locationPermissionCallbackId = callbackId
3317- if permission == " locationAlways " || config. enableBackgroundLocation {
3318- locationManager? . requestAlwaysAuthorization ( )
3366+ locationPermissionRequiresAlways = requiresAlways
3367+ if requiresAlways {
3368+ manager. requestAlwaysAuthorization ( )
33193369 } else {
3320- locationManager ? . requestWhenInUseAuthorization ( )
3370+ manager . requestWhenInUseAuthorization ( )
33213371 }
33223372 case " camera " :
33233373 AVCaptureDevice . requestAccess ( for: . video) { granted in
@@ -3363,11 +3413,59 @@ struct CraftWebView: UIViewRepresentable {
33633413 }
33643414
33653415 // MARK: - Geolocation
3366- private func getCurrentPosition( callbackId: String ? ) {
3367- locationManager? . delegate = self
3416+ private func getCurrentPosition( body: [ String : Any ] , callbackId: String ? ) {
3417+ guard let manager = locationManager else {
3418+ rejectCallback ( callbackId, error: " Geolocation is disabled " , code: " CAPABILITY_DISABLED " )
3419+ return
3420+ }
3421+ manager. delegate = self
3422+ manager. desiredAccuracy = body [ " enableHighAccuracy " ] as? Bool == true
3423+ ? kCLLocationAccuracyBest
3424+ : kCLLocationAccuracyHundredMeters
3425+
3426+ if let pendingCallbackId = singleLocationCallbackId {
3427+ finishSingleLocationRequest ( )
3428+ rejectCallback ( pendingCallbackId, error: " A newer location request replaced this request " , code: " POSITION_UNAVAILABLE " )
3429+ }
33683430 singleLocationCallbackId = callbackId
3431+ let maximumAge = max ( 0 , ( body [ " maximumAge " ] as? NSNumber ) ? . doubleValue ?? 0 )
3432+ if maximumAge > 0 ,
3433+ let cachedLocation = manager. location,
3434+ Date ( ) . timeIntervalSince ( cachedLocation. timestamp) * 1000 <= maximumAge {
3435+ finishSingleLocationRequest ( )
3436+ resolveCallback ( callbackId, result: locationData ( cachedLocation) )
3437+ return
3438+ }
3439+
3440+ let timeoutMs = max ( 0 , ( body [ " timeout " ] as? NSNumber ) ? . doubleValue ?? 30_000 )
3441+ let timeoutWorkItem = DispatchWorkItem { [ weak self] in
3442+ guard let self, self . singleLocationCallbackId == callbackId else { return }
3443+ self . finishSingleLocationRequest ( )
3444+ self . rejectCallback ( callbackId, error: " Location request timed out " , code: " LOCATION_TIMEOUT " )
3445+ }
3446+ singleLocationTimeoutWorkItem = timeoutWorkItem
3447+ DispatchQueue . main. asyncAfter ( deadline: . now( ) + . milliseconds( Int ( min ( timeoutMs + 100 , Double ( Int . max) ) ) ) , execute: timeoutWorkItem)
33693448 requestLocationAuthorization ( )
3370- locationManager? . requestLocation ( )
3449+ manager. requestLocation ( )
3450+ }
3451+
3452+ private func finishSingleLocationRequest( ) {
3453+ singleLocationTimeoutWorkItem? . cancel ( )
3454+ singleLocationTimeoutWorkItem = nil
3455+ singleLocationCallbackId = nil
3456+ }
3457+
3458+ private func locationData( _ location: CLLocation ) -> [ String : Any ] {
3459+ [
3460+ " latitude " : location. coordinate. latitude,
3461+ " longitude " : location. coordinate. longitude,
3462+ " altitude " : location. altitude,
3463+ " accuracy " : location. horizontalAccuracy,
3464+ " altitudeAccuracy " : location. verticalAccuracy,
3465+ " heading " : location. course,
3466+ " speed " : location. speed,
3467+ " timestamp " : location. timestamp. timeIntervalSince1970 * 1000
3468+ ]
33713469 }
33723470
33733471 private func watchPosition( callbackId: String ? ) {
@@ -3555,20 +3653,11 @@ struct CraftWebView: UIViewRepresentable {
35553653
35563654 func locationManager( _ manager: CLLocationManager , didUpdateLocations locations: [ CLLocation ] ) {
35573655 guard let location = locations. last else { return }
3558- let data : [ String : Any ] = [
3559- " latitude " : location. coordinate. latitude,
3560- " longitude " : location. coordinate. longitude,
3561- " altitude " : location. altitude,
3562- " accuracy " : location. horizontalAccuracy,
3563- " altitudeAccuracy " : location. verticalAccuracy,
3564- " heading " : location. course,
3565- " speed " : location. speed,
3566- " timestamp " : location. timestamp. timeIntervalSince1970 * 1000
3567- ]
3656+ let data = locationData ( location)
35683657
35693658 if let callbackId = singleLocationCallbackId {
3659+ finishSingleLocationRequest ( )
35703660 resolveCallback ( callbackId, result: data)
3571- singleLocationCallbackId = nil
35723661 }
35733662
35743663 appendRecordedLocation ( data)
@@ -3578,18 +3667,21 @@ struct CraftWebView: UIViewRepresentable {
35783667 }
35793668
35803669 func locationManager( _ manager: CLLocationManager , didFailWithError error: Error ) {
3581- rejectCallback ( singleLocationCallbackId, error: error. localizedDescription)
3582- singleLocationCallbackId = nil
3670+ let callbackId = singleLocationCallbackId
3671+ finishSingleLocationRequest ( )
3672+ rejectCallback ( callbackId, error: error. localizedDescription, code: " POSITION_UNAVAILABLE " )
35833673 sendToWeb ( " craftLocationError " , data: [ " message " : error. localizedDescription] )
35843674 }
35853675
35863676 func locationManagerDidChangeAuthorization( _ manager: CLLocationManager ) {
35873677 guard let callbackId = locationPermissionCallbackId else { return }
35883678 let status = manager. authorizationStatus
35893679 if status == . notDetermined { return }
3590- let granted = status == . authorizedAlways || status == . authorizedWhenInUse
3680+ if locationPermissionRequiresAlways && status == . authorizedWhenInUse { return }
3681+ let granted = status == . authorizedAlways || ( !locationPermissionRequiresAlways && status == . authorizedWhenInUse)
35913682 resolveCallback ( callbackId, result: permissionStatus ( granted, denied: status == . denied, restricted: status == . restricted) )
35923683 locationPermissionCallbackId = nil
3684+ locationPermissionRequiresAlways = false
35933685 }
35943686
35953687 // MARK: - Memory Usage (for Profiling)
0 commit comments