migration
This commit is contained in:
193
PediFoods/Services/LocationService.swift
Normal file
193
PediFoods/Services/LocationService.swift
Normal file
@@ -0,0 +1,193 @@
|
||||
import Foundation
|
||||
|
||||
#if os(iOS)
|
||||
import CoreLocation
|
||||
|
||||
/// The subset of `CLLocationManager` `LocationService` needs — lets tests
|
||||
/// substitute a fake instead of touching real hardware/OS permission state.
|
||||
@MainActor
|
||||
protocol LocationManaging: AnyObject {
|
||||
var locationManagingDelegate: CLLocationManagerDelegate? { get set }
|
||||
var desiredAccuracy: CLLocationAccuracy { get set }
|
||||
var authorizationStatus: CLAuthorizationStatus { get }
|
||||
var location: CLLocation? { get }
|
||||
func requestWhenInUseAuthorization()
|
||||
func requestLocation()
|
||||
}
|
||||
|
||||
extension CLLocationManager: LocationManaging {
|
||||
var locationManagingDelegate: CLLocationManagerDelegate? {
|
||||
get { delegate }
|
||||
set { delegate = newValue }
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
@MainActor
|
||||
final class LocationService: NSObject {
|
||||
typealias LocationResult = Result<(Double, Double), LocationError>
|
||||
static let shared = LocationService()
|
||||
|
||||
#if os(iOS)
|
||||
enum LocationError: Error {
|
||||
case servicesDisabled
|
||||
case denied
|
||||
case unavailable
|
||||
}
|
||||
#else
|
||||
enum LocationError: Error {
|
||||
case denied
|
||||
case unavailable
|
||||
}
|
||||
#endif
|
||||
|
||||
#if os(iOS)
|
||||
private let manager: LocationManaging
|
||||
private var completion: ((LocationResult) -> Void)?
|
||||
#endif
|
||||
|
||||
#if os(iOS)
|
||||
init(manager: LocationManaging = CLLocationManager()) {
|
||||
self.manager = manager
|
||||
super.init()
|
||||
manager.locationManagingDelegate = self
|
||||
manager.desiredAccuracy = kCLLocationAccuracyHundredMeters
|
||||
}
|
||||
#else
|
||||
override init() {
|
||||
super.init()
|
||||
}
|
||||
#endif
|
||||
|
||||
func requestLocation(_ completion: @escaping (LocationResult) -> Void) {
|
||||
#if os(iOS)
|
||||
self.completion = completion
|
||||
handleAuthorizationStatus(manager.authorizationStatus)
|
||||
#else
|
||||
let defaults = UserDefaults.standard
|
||||
if defaults.bool(forKey: "location_permission_denied") {
|
||||
completion(.failure(.denied))
|
||||
return
|
||||
}
|
||||
|
||||
guard let latRaw = defaults.string(forKey: "last_location_lat"),
|
||||
let lngRaw = defaults.string(forKey: "last_location_lng"),
|
||||
let lat = Double(latRaw),
|
||||
let lng = Double(lngRaw) else {
|
||||
completion(.failure(.unavailable))
|
||||
return
|
||||
}
|
||||
completion(.success((lat, lng)))
|
||||
#endif
|
||||
}
|
||||
|
||||
func cachedLocation() -> (Double, Double)? {
|
||||
#if os(iOS)
|
||||
guard let location = manager.location else {
|
||||
return nil
|
||||
}
|
||||
return (location.coordinate.latitude, location.coordinate.longitude)
|
||||
#else
|
||||
let defaults = UserDefaults.standard
|
||||
guard let latRaw = defaults.string(forKey: "last_location_lat"),
|
||||
let lngRaw = defaults.string(forKey: "last_location_lng"),
|
||||
let lat = Double(latRaw),
|
||||
let lng = Double(lngRaw) else {
|
||||
return nil
|
||||
}
|
||||
return (lat, lng)
|
||||
#endif
|
||||
}
|
||||
|
||||
/// Forward-geocodes a street address into coordinates. Used as a fallback
|
||||
/// when a saved CustomerAddress has no lat/long (e.g. the CEP lookup at
|
||||
/// creation time didn't return coordinates) — without this, delivery fee
|
||||
/// validation silently can't distinguish that address from any other.
|
||||
static func geocodeAddress(
|
||||
street: String?,
|
||||
number: String?,
|
||||
neighborhood: String?,
|
||||
city: String?,
|
||||
state: String?,
|
||||
zip: String?
|
||||
) async -> (Double, Double)? {
|
||||
#if os(iOS)
|
||||
let parts = [street, number, neighborhood, city, state, zip]
|
||||
.compactMap { $0?.trimmingCharacters(in: .whitespacesAndNewlines) }
|
||||
.filter { $0.isEmpty == false }
|
||||
guard parts.isEmpty == false else { return nil }
|
||||
let fullAddress = parts.joined(separator: ", ")
|
||||
|
||||
return await withCheckedContinuation { continuation in
|
||||
CLGeocoder().geocodeAddressString(fullAddress) { placemarks, error in
|
||||
guard error == nil, let coordinate = placemarks?.first?.location?.coordinate else {
|
||||
continuation.resume(returning: nil)
|
||||
return
|
||||
}
|
||||
continuation.resume(returning: (coordinate.latitude, coordinate.longitude))
|
||||
}
|
||||
}
|
||||
#else
|
||||
return nil
|
||||
#endif
|
||||
}
|
||||
|
||||
func requestLocationAsync(timeoutSeconds: TimeInterval = 8) async -> (Double, Double)? {
|
||||
await withCheckedContinuation { continuation in
|
||||
var hasResumed = false
|
||||
|
||||
func resumeOnce(_ value: (Double, Double)?) {
|
||||
guard hasResumed == false else { return }
|
||||
hasResumed = true
|
||||
continuation.resume(returning: value)
|
||||
}
|
||||
|
||||
requestLocation { result in
|
||||
switch result {
|
||||
case .success(let coordinate):
|
||||
resumeOnce(coordinate)
|
||||
case .failure:
|
||||
resumeOnce(nil)
|
||||
}
|
||||
}
|
||||
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + timeoutSeconds) {
|
||||
resumeOnce(nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#if os(iOS)
|
||||
extension LocationService: @preconcurrency CLLocationManagerDelegate {
|
||||
func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
|
||||
handleAuthorizationStatus(manager.authorizationStatus)
|
||||
}
|
||||
|
||||
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
|
||||
guard let location = locations.first else { return }
|
||||
completion?(.success((location.coordinate.latitude, location.coordinate.longitude)))
|
||||
completion = nil
|
||||
}
|
||||
|
||||
func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
|
||||
completion?(.failure(.unavailable))
|
||||
completion = nil
|
||||
}
|
||||
|
||||
private func handleAuthorizationStatus(_ status: CLAuthorizationStatus) {
|
||||
switch status {
|
||||
case .notDetermined:
|
||||
manager.requestWhenInUseAuthorization()
|
||||
case .authorizedAlways, .authorizedWhenInUse:
|
||||
manager.requestLocation()
|
||||
case .denied, .restricted:
|
||||
completion?(.failure(.denied))
|
||||
completion = nil
|
||||
@unknown default:
|
||||
completion?(.failure(.unavailable))
|
||||
completion = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
Reference in New Issue
Block a user