RTSDA-iOS/Managers/PermissionsManager.swift
2025-02-03 16:15:57 -05:00

265 lines
8.1 KiB
Swift

import Foundation
import EventKit
import CoreLocation
import Photos
import Contacts
import AVFoundation
@MainActor
class PermissionsManager: ObservableObject {
static let shared = PermissionsManager()
@Published var calendarAccess: Bool = false
@Published var locationAccess: Bool = false
@Published var cameraAccess: Bool = false
@Published var photosAccess: Bool = false
@Published var contactsAccess: Bool = false
@Published var microphoneAccess: Bool = false
private let locationManager = CLLocationManager()
private init() {
checkPermissions()
}
func checkPermissions() {
checkCalendarAccess()
checkLocationAccess()
checkCameraAccess()
checkPhotosAccess()
checkContactsAccess()
checkMicrophoneAccess()
}
// MARK: - Calendar
func checkCalendarAccess() {
if #available(iOS 17.0, *) {
let status = EKEventStore.authorizationStatus(for: .event)
calendarAccess = status == .fullAccess || status == .writeOnly
} else {
let status = EKEventStore.authorizationStatus(for: .event)
calendarAccess = status == .authorized
}
}
func requestCalendarAccess() async -> Bool {
let store = EKEventStore()
do {
if #available(iOS 17.0, *) {
// First try to get write-only access as it's less invasive
let writeGranted = try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<Bool, Error>) in
store.requestWriteOnlyAccessToEvents { granted, error in
if let error = error {
continuation.resume(throwing: error)
} else {
continuation.resume(returning: granted)
}
}
}
if writeGranted {
await MainActor.run {
calendarAccess = true
}
return true
}
// If write-only access fails or is denied, try for full access
let fullGranted = try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<Bool, Error>) in
store.requestFullAccessToEvents { granted, error in
if let error = error {
continuation.resume(throwing: error)
} else {
continuation.resume(returning: granted)
}
}
}
await MainActor.run {
calendarAccess = fullGranted
}
return fullGranted
} else {
// Pre-iOS 17 fallback
#if compiler(>=5.9)
let granted = try await store.requestAccess(to: .event)
#else
let granted = try await withCheckedThrowingContinuation { continuation in
store.requestAccess(to: .event) { granted, error in
if let error = error {
continuation.resume(throwing: error)
} else {
continuation.resume(returning: granted)
}
}
}
#endif
await MainActor.run {
calendarAccess = granted
}
return granted
}
} catch {
print("❌ Calendar access error: \(error)")
return false
}
}
// MARK: - Location
func checkLocationAccess() {
switch locationManager.authorizationStatus {
case .authorizedWhenInUse, .authorizedAlways:
locationAccess = true
default:
locationAccess = false
}
}
func requestLocationAccess() {
locationManager.requestWhenInUseAuthorization()
}
// MARK: - Camera
func checkCameraAccess() {
let status = AVCaptureDevice.authorizationStatus(for: .video)
cameraAccess = status == .authorized
}
func requestCameraAccess() async -> Bool {
let granted = await AVCaptureDevice.requestAccess(for: .video)
await MainActor.run {
cameraAccess = granted
}
return granted
}
// MARK: - Photos
func checkPhotosAccess() {
let status = PHPhotoLibrary.authorizationStatus()
photosAccess = status == .authorized || status == .limited
}
func requestPhotosAccess() async -> Bool {
let status = await PHPhotoLibrary.requestAuthorization(for: .readWrite)
await MainActor.run {
photosAccess = status == .authorized || status == .limited
}
return photosAccess
}
// MARK: - Contacts
func checkContactsAccess() {
let status = CNContactStore.authorizationStatus(for: .contacts)
contactsAccess = status == .authorized
}
func requestContactsAccess() async -> Bool {
let store = CNContactStore()
do {
let granted = try await store.requestAccess(for: .contacts)
await MainActor.run {
contactsAccess = granted
}
return granted
} catch {
print("❌ Contacts access error: \(error)")
return false
}
}
// MARK: - Microphone
func checkMicrophoneAccess() {
let status = AVCaptureDevice.authorizationStatus(for: .audio)
microphoneAccess = status == .authorized
}
func requestMicrophoneAccess() async -> Bool {
let granted = await AVCaptureDevice.requestAccess(for: .audio)
await MainActor.run {
microphoneAccess = granted
}
return granted
}
// MARK: - Helper Methods
func handleLimitedAccess(for feature: AppFeature) -> FeatureAvailability {
switch feature {
case .calendar:
if #available(iOS 17.0, *) {
let status = EKEventStore.authorizationStatus(for: .event)
switch status {
case .fullAccess:
return .full
case .writeOnly:
return .limited
default:
return .unavailable
}
} else {
let status = EKEventStore.authorizationStatus(for: .event)
if status == .authorized {
return .full
}
return .unavailable
}
case .location:
return locationAccess ? .full : .limited
case .camera:
return cameraAccess ? .full : .limited
case .photos:
// Photos is special - it can work with limited access
let status = PHPhotoLibrary.authorizationStatus()
switch status {
case .authorized:
return .full
case .limited:
return .limited
default:
return .unavailable
}
case .contacts:
return contactsAccess ? .full : .limited
case .microphone:
return microphoneAccess ? .full : .limited
}
}
}
enum AppFeature {
case calendar
case location
case camera
case photos
case contacts
case microphone
var displayName: String {
switch self {
case .calendar: return "Calendar"
case .location: return "Location"
case .camera: return "Camera"
case .photos: return "Photos"
case .contacts: return "Contacts"
case .microphone: return "Microphone"
}
}
}
enum FeatureAvailability {
case full
case limited
case unavailable
var description: String {
switch self {
case .full:
return "Full Access"
case .limited:
return "Limited Access"
case .unavailable:
return "Not Available"
}
}
}