migration

This commit is contained in:
Daniel Arantes Loverde
2026-08-11 13:22:02 -03:00
parent c4d6998595
commit 7702836fe7
231 changed files with 4329 additions and 1958 deletions

View 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