Fix location/weather error handling and complete localization

Add authorization pre-check and 15s timeout to LocationManager to
prevent hanging continuations. WeatherManager now skips retry queue
when location permission is denied. Settings weather toggle shows
alert directing users to Settings when location is denied. Fill
remaining 32 untranslated strings to reach 100% localization.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Trey t
2026-03-11 17:37:28 -05:00
parent 07b03fc8a1
commit 78d09803c3
4 changed files with 15813 additions and 14382 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -42,9 +42,49 @@ final class LocationManager: NSObject {
return last
}
// Pre-check authorization before requesting location
switch manager.authorizationStatus {
case .denied, .restricted:
throw LocationError.permissionDenied
case .notDetermined:
manager.requestWhenInUseAuthorization()
// Wait briefly for authorization response
try await Task.sleep(for: .seconds(1))
if manager.authorizationStatus == .denied || manager.authorizationStatus == .restricted {
throw LocationError.permissionDenied
}
default:
break
}
// Request location with a 15-second timeout
return try await withCheckedThrowingContinuation { continuation in
self.locationContinuation = continuation
self.manager.requestLocation()
// Schedule timeout safe because everything is @MainActor (no race)
Task { @MainActor in
try? await Task.sleep(for: .seconds(15))
guard self.locationContinuation != nil else { return }
self.locationContinuation?.resume(throwing: LocationError.timeout)
self.locationContinuation = nil
}
}
}
}
// MARK: - Errors
enum LocationError: LocalizedError {
case permissionDenied
case timeout
var errorDescription: String? {
switch self {
case .permissionDenied:
return "Location permission denied"
case .timeout:
return "Location request timed out"
}
}
}

View File

@@ -69,6 +69,10 @@ final class WeatherManager {
AnalyticsManager.shared.track(.weatherFetched)
Self.logger.info("Weather saved for \(date)")
} catch let error as LocationManager.LocationError where error == .permissionDenied {
Self.logger.warning("Weather fetch skipped: location permission denied")
AnalyticsManager.shared.track(.weatherFetchFailed(error: "location_permission_denied"))
// Don't add to retry queue retrying won't help until user grants permission
} catch {
Self.logger.error("Weather fetch failed: \(error.localizedDescription)")
AnalyticsManager.shared.track(.weatherFetchFailed(error: error.localizedDescription))
@@ -79,6 +83,14 @@ final class WeatherManager {
// MARK: - Retry Queue
func retryPendingWeatherFetches() async {
// Bail early if location permission is denied retrying will always fail
let status = LocationManager.shared.authorizationStatus
if status == .denied || status == .restricted {
Self.logger.info("Clearing weather retry queue — location permission denied")
saveRetryQueue([])
return
}
let dates = getRetryQueue()
guard !dates.isEmpty else { return }
@@ -138,11 +150,14 @@ final class WeatherManager {
enum WeatherError: LocalizedError {
case noDataAvailable
case locationDenied
var errorDescription: String? {
switch self {
case .noDataAvailable:
return "No weather data available for the requested date"
case .locationDenied:
return "Location permission denied — cannot fetch weather"
}
}
}

View File

@@ -41,6 +41,7 @@ struct SettingsContentView: View {
@AppStorage(UserDefaultsStore.Keys.hapticFeedbackEnabled.rawValue, store: GroupUserDefaults.groupDefaults) private var hapticFeedbackEnabled = true
@AppStorage(UserDefaultsStore.Keys.weatherEnabled.rawValue, store: GroupUserDefaults.groupDefaults) private var weatherEnabled = true
@State private var showWeatherSubscriptionStore = false
@State private var showLocationDeniedAlert = false
private var textColor: Color { theme.currentTheme.labelColor }
@@ -995,6 +996,15 @@ struct SettingsContentView: View {
if newValue {
LocationManager.shared.requestAuthorization()
// Check if permission was denied after a brief delay
Task {
try? await Task.sleep(for: .seconds(1))
let status = LocationManager.shared.authorizationStatus
if status == .denied || status == .restricted {
weatherEnabled = false
showLocationDeniedAlert = true
}
}
}
AnalyticsManager.shared.track(.weatherToggled(enabled: newValue))
@@ -1026,6 +1036,19 @@ struct SettingsContentView: View {
.sheet(isPresented: $showWeatherSubscriptionStore) {
ReflectSubscriptionStoreView(source: "settings")
}
.alert(
String(localized: "Location Access Required"),
isPresented: $showLocationDeniedAlert
) {
Button(String(localized: "Open Settings")) {
if let url = URL(string: UIApplication.openSettingsURLString) {
UIApplication.shared.open(url)
}
}
Button(String(localized: "Cancel"), role: .cancel) {}
} message: {
Text("Reflect needs location access to show weather. You can enable it in Settings.")
}
}
// MARK: - Export Data Button
@@ -1311,6 +1334,7 @@ struct SettingsView: View {
@AppStorage(UserDefaultsStore.Keys.hapticFeedbackEnabled.rawValue, store: GroupUserDefaults.groupDefaults) private var hapticFeedbackEnabled = true
@AppStorage(UserDefaultsStore.Keys.weatherEnabled.rawValue, store: GroupUserDefaults.groupDefaults) private var weatherEnabled = true
@State private var showWeatherSubscriptionStore = false
@State private var showLocationDeniedAlert = false
private var textColor: Color { theme.currentTheme.labelColor }
@@ -1665,6 +1689,15 @@ struct SettingsView: View {
if newValue {
LocationManager.shared.requestAuthorization()
// Check if permission was denied after a brief delay
Task {
try? await Task.sleep(for: .seconds(1))
let status = LocationManager.shared.authorizationStatus
if status == .denied || status == .restricted {
weatherEnabled = false
showLocationDeniedAlert = true
}
}
}
AnalyticsManager.shared.track(.weatherToggled(enabled: newValue))
@@ -1692,6 +1725,19 @@ struct SettingsView: View {
.sheet(isPresented: $showWeatherSubscriptionStore) {
ReflectSubscriptionStoreView(source: "settings")
}
.alert(
String(localized: "Location Access Required"),
isPresented: $showLocationDeniedAlert
) {
Button(String(localized: "Open Settings")) {
if let url = URL(string: UIApplication.openSettingsURLString) {
UIApplication.shared.open(url)
}
}
Button(String(localized: "Cancel"), role: .cancel) {}
} message: {
Text("Reflect needs location access to show weather. You can enable it in Settings.")
}
}
// MARK: - Export Data Button