diff --git a/geolocator_apple/darwin/geolocator_apple/Sources/geolocator_apple/Handlers/PermissionHandler.m b/geolocator_apple/darwin/geolocator_apple/Sources/geolocator_apple/Handlers/PermissionHandler.m index 265401aad..d72ea1538 100644 --- a/geolocator_apple/darwin/geolocator_apple/Sources/geolocator_apple/Handlers/PermissionHandler.m +++ b/geolocator_apple/darwin/geolocator_apple/Sources/geolocator_apple/Handlers/PermissionHandler.m @@ -8,12 +8,16 @@ #import "../include/geolocator_apple/Handlers/PermissionHandler.h" #import "../include/geolocator_apple/Constants/ErrorCodes.h" #import "../include/geolocator_apple/Utils/PermissionUtils.h" +#if TARGET_OS_IOS +#import +#endif @interface PermissionHandler() @property (strong, nonatomic) CLLocationManager *locationManager; @property (strong, nonatomic) PermissionConfirmation confirmationHandler; @property (strong, nonatomic) PermissionError errorHandler; +@property (assign, nonatomic) BOOL isWaitingForPermission; @end @@ -49,19 +53,31 @@ - (void) requestPermission:(PermissionConfirmation)confirmationHandler confirmationHandler(authorizationStatus); return; } - + if (self.confirmationHandler) { // Permission request is already running, return immediatly with error errorHandler(GeolocatorErrorPermissionRequestInProgress, @"A request for location permissions is already running, please wait for it to complete before doing another request."); return; } - + self.confirmationHandler = confirmationHandler; self.errorHandler = errorHandler; + self.isWaitingForPermission = YES; CLLocationManager *locationManager = [self getLocationManager]; locationManager.delegate = self; - + +#if TARGET_OS_IOS + // Register for app lifecycle notifications to handle dialog dismissal + // When user locks phone or backgrounds app while permission dialog is showing, + // iOS dismisses the dialog without calling the delegate. We need to detect this + // and resolve the Future to prevent it from hanging indefinitely. + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(applicationDidBecomeActive:) + name:UIApplicationDidBecomeActiveNotification + object:nil]; +#endif + #if TARGET_OS_OSX if ([[NSBundle mainBundle] objectForInfoDictionaryKey:@"NSLocationUsageDescription"] != nil) { if (@available(macOS 10.15, *)) { @@ -85,7 +101,7 @@ - (void) requestPermission:(PermissionConfirmation)confirmationHandler "add either NSLocationWhenInUseUsageDescription or " "NSLocationAlwaysUsageDescription to the app's Info.plist file on iOS. If running on macOS please add NSLocationUsageDescription to the app's Info.plist file."); } - + [self cleanUp]; return; } @@ -106,19 +122,69 @@ - (BOOL) containsLocationAlwaysDescription { - (void) locationManager:(CLLocationManager *)manager didChangeAuthorizationStatus:(CLAuthorizationStatus)status { if (status == kCLAuthorizationStatusNotDetermined) { + // Dialog is being shown or was just dismissed without user action. + // Don't resolve yet - wait for actual user decision or app lifecycle event. return; } - + + // User has made a decision, resolve the callback if (self.confirmationHandler) { self.confirmationHandler(status); } - + [self cleanUp]; } +#if TARGET_OS_IOS +/// Called when app returns to foreground after being backgrounded or device was unlocked. +/// This handles the case where iOS dismisses the permission dialog when the app goes to background. +/// Without this, the Future would hang indefinitely waiting for a delegate callback that never comes. +- (void)applicationDidBecomeActive:(NSNotification *)notification { + // Only process if we're actually waiting for a permission response + if (!self.isWaitingForPermission || !self.confirmationHandler) { + return; + } + + // Small delay to allow any pending authorization status changes to be processed + // The system may have queued the authorization callback before this notification + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ + // Double-check we're still waiting (delegate might have been called in the meantime) + if (!self.isWaitingForPermission || !self.confirmationHandler) { + return; + } + + // Check current authorization status + CLAuthorizationStatus currentStatus = [self checkPermission]; + + // If status is still NotDetermined, the dialog was dismissed without user action + // (e.g., user locked phone, switched apps, etc.) + // Resolve with current status to prevent Future from hanging + if (self.confirmationHandler) { + self.confirmationHandler(currentStatus); + } + + [self cleanUp]; + }); +} +#endif + - (void) cleanUp { +#if TARGET_OS_IOS + // Remove app lifecycle observer + [[NSNotificationCenter defaultCenter] removeObserver:self + name:UIApplicationDidBecomeActiveNotification + object:nil]; +#endif + self.isWaitingForPermission = NO; self.locationManager = nil; self.errorHandler = nil; self.confirmationHandler = nil; } + +- (void)dealloc { +#if TARGET_OS_IOS + [[NSNotificationCenter defaultCenter] removeObserver:self]; +#endif +} + @end