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:
File diff suppressed because it is too large
Load Diff
@@ -42,9 +42,49 @@ final class LocationManager: NSObject {
|
|||||||
return last
|
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
|
return try await withCheckedThrowingContinuation { continuation in
|
||||||
self.locationContinuation = continuation
|
self.locationContinuation = continuation
|
||||||
self.manager.requestLocation()
|
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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -69,6 +69,10 @@ final class WeatherManager {
|
|||||||
AnalyticsManager.shared.track(.weatherFetched)
|
AnalyticsManager.shared.track(.weatherFetched)
|
||||||
|
|
||||||
Self.logger.info("Weather saved for \(date)")
|
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 {
|
} catch {
|
||||||
Self.logger.error("Weather fetch failed: \(error.localizedDescription)")
|
Self.logger.error("Weather fetch failed: \(error.localizedDescription)")
|
||||||
AnalyticsManager.shared.track(.weatherFetchFailed(error: error.localizedDescription))
|
AnalyticsManager.shared.track(.weatherFetchFailed(error: error.localizedDescription))
|
||||||
@@ -79,6 +83,14 @@ final class WeatherManager {
|
|||||||
// MARK: - Retry Queue
|
// MARK: - Retry Queue
|
||||||
|
|
||||||
func retryPendingWeatherFetches() async {
|
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()
|
let dates = getRetryQueue()
|
||||||
guard !dates.isEmpty else { return }
|
guard !dates.isEmpty else { return }
|
||||||
|
|
||||||
@@ -138,11 +150,14 @@ final class WeatherManager {
|
|||||||
|
|
||||||
enum WeatherError: LocalizedError {
|
enum WeatherError: LocalizedError {
|
||||||
case noDataAvailable
|
case noDataAvailable
|
||||||
|
case locationDenied
|
||||||
|
|
||||||
var errorDescription: String? {
|
var errorDescription: String? {
|
||||||
switch self {
|
switch self {
|
||||||
case .noDataAvailable:
|
case .noDataAvailable:
|
||||||
return "No weather data available for the requested date"
|
return "No weather data available for the requested date"
|
||||||
|
case .locationDenied:
|
||||||
|
return "Location permission denied — cannot fetch weather"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ struct SettingsContentView: View {
|
|||||||
@AppStorage(UserDefaultsStore.Keys.hapticFeedbackEnabled.rawValue, store: GroupUserDefaults.groupDefaults) private var hapticFeedbackEnabled = true
|
@AppStorage(UserDefaultsStore.Keys.hapticFeedbackEnabled.rawValue, store: GroupUserDefaults.groupDefaults) private var hapticFeedbackEnabled = true
|
||||||
@AppStorage(UserDefaultsStore.Keys.weatherEnabled.rawValue, store: GroupUserDefaults.groupDefaults) private var weatherEnabled = true
|
@AppStorage(UserDefaultsStore.Keys.weatherEnabled.rawValue, store: GroupUserDefaults.groupDefaults) private var weatherEnabled = true
|
||||||
@State private var showWeatherSubscriptionStore = false
|
@State private var showWeatherSubscriptionStore = false
|
||||||
|
@State private var showLocationDeniedAlert = false
|
||||||
|
|
||||||
private var textColor: Color { theme.currentTheme.labelColor }
|
private var textColor: Color { theme.currentTheme.labelColor }
|
||||||
|
|
||||||
@@ -995,6 +996,15 @@ struct SettingsContentView: View {
|
|||||||
|
|
||||||
if newValue {
|
if newValue {
|
||||||
LocationManager.shared.requestAuthorization()
|
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))
|
AnalyticsManager.shared.track(.weatherToggled(enabled: newValue))
|
||||||
@@ -1026,6 +1036,19 @@ struct SettingsContentView: View {
|
|||||||
.sheet(isPresented: $showWeatherSubscriptionStore) {
|
.sheet(isPresented: $showWeatherSubscriptionStore) {
|
||||||
ReflectSubscriptionStoreView(source: "settings")
|
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
|
// 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.hapticFeedbackEnabled.rawValue, store: GroupUserDefaults.groupDefaults) private var hapticFeedbackEnabled = true
|
||||||
@AppStorage(UserDefaultsStore.Keys.weatherEnabled.rawValue, store: GroupUserDefaults.groupDefaults) private var weatherEnabled = true
|
@AppStorage(UserDefaultsStore.Keys.weatherEnabled.rawValue, store: GroupUserDefaults.groupDefaults) private var weatherEnabled = true
|
||||||
@State private var showWeatherSubscriptionStore = false
|
@State private var showWeatherSubscriptionStore = false
|
||||||
|
@State private var showLocationDeniedAlert = false
|
||||||
|
|
||||||
private var textColor: Color { theme.currentTheme.labelColor }
|
private var textColor: Color { theme.currentTheme.labelColor }
|
||||||
|
|
||||||
@@ -1665,6 +1689,15 @@ struct SettingsView: View {
|
|||||||
|
|
||||||
if newValue {
|
if newValue {
|
||||||
LocationManager.shared.requestAuthorization()
|
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))
|
AnalyticsManager.shared.track(.weatherToggled(enabled: newValue))
|
||||||
@@ -1692,6 +1725,19 @@ struct SettingsView: View {
|
|||||||
.sheet(isPresented: $showWeatherSubscriptionStore) {
|
.sheet(isPresented: $showWeatherSubscriptionStore) {
|
||||||
ReflectSubscriptionStoreView(source: "settings")
|
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
|
// MARK: - Export Data Button
|
||||||
|
|||||||
Reference in New Issue
Block a user