//
// PermissionsStepView.swift
// Whispera
//
// Created by Varkhuman Mac on 7/4/25.
//
import SwiftUI
import AVFoundation
struct PermissionsStepView: View {
@Binding var hasPermissions: Bool
@Bindable var audioManager: AudioManager
@ObservedObject var globalShortcutManager: GlobalShortcutManager
@State private var hasMicrophonePermission = false
@State private var permissionCheckTimer: Timer?
@State private var accessibilityCheckTimer: Timer?
var body: some View {
VStack(spacing: 24) {
VStack(spacing: 16) {
Image(systemName: "lock.shield.fill")
.font(.system(size: 48))
.foregroundColor(.orange)
Text("Permissions Required")
.font(.system(.title, design: .rounded, weight: .semibold))
Text("Whispera needs accessibility permissions to work with global keyboard shortcuts.")
.font(.body)
.foregroundColor(.secondary)
.multilineTextAlignment(.center)
}
VStack(spacing: 16) {
PermissionRowView(
icon: "key.fill",
title: "Accessibility Access",
description: "Required for global keyboard shortcuts",
isGranted: hasPermissions
)
if !hasPermissions {
Button {
globalShortcutManager.requestAccessibilityPermissions()
startPermissionChecking()
} label: {
Text("Grant Accessibility Access")
}
}
PermissionRowView(
icon: "mic.fill",
title: "Microphone Access",
description: "Required for voice recording",
isGranted: hasMicrophonePermission
)
if !hasMicrophonePermission {
Button {
requestMicrophonePermission()
} label: {
Text("Grant Microphone Access")
}
}
}
if hasPermissions && hasMicrophonePermission {
HStack(spacing: 8) {
Image(systemName: "checkmark.circle.fill")
.foregroundColor(.green)
Text("All permissions granted! You're ready to continue.")
.font(.subheadline)
.foregroundColor(.green)
}
.padding()
.background(.green.opacity(0.1), in: RoundedRectangle(cornerRadius: 8))
} else {
VStack(spacing: 12) {
if !hasPermissions {
Text("After clicking \"Grant Permissions\", you'll see a system dialog.")
.font(.subheadline)
.foregroundColor(.secondary)
Text("Go to System Settings > Privacy & Security > Accessibility and enable Whispera.")
.font(.subheadline)
.foregroundColor(.secondary)
.multilineTextAlignment(.center)
}
if !hasMicrophonePermission {
VStack(spacing: 8) {
Text("Microphone access will be requested when you first try to record.")
.font(.subheadline)
.foregroundColor(.secondary)
.multilineTextAlignment(.center)
Text("If Whispera doesn't appear in Microphone settings, try recording first to trigger the permission request.")
.font(.caption)
.foregroundColor(.orange)
.multilineTextAlignment(.center)
}
}
}
.padding()
.background(.orange.opacity(0.1), in: RoundedRectangle(cornerRadius: 8))
}
}
.onAppear {
checkMicrophonePermission()
checkAccessibilityPermission()
startContinuousPermissionChecking()
}
.onDisappear {
stopPermissionChecking()
stopContinuousPermissionChecking()
}
}
private func checkMicrophonePermission() {
hasMicrophonePermission = AVCaptureDevice.authorizationStatus(for: .audio) == .authorized
}
private func checkAccessibilityPermission() {
let newValue = AXIsProcessTrusted()
if newValue != hasPermissions {
hasPermissions = newValue
}
}
private func startPermissionChecking() {
// Check immediately
checkAccessibilityPermission()
checkMicrophonePermission()
// Then check every 0.5 seconds for changes
permissionCheckTimer?.invalidate()
permissionCheckTimer = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true) { _ in
checkAccessibilityPermission()
checkMicrophonePermission()
// Stop checking once both permissions are granted
if hasPermissions && hasMicrophonePermission {
stopPermissionChecking()
}
}
}
private func stopPermissionChecking() {
permissionCheckTimer?.invalidate()
permissionCheckTimer = nil
}
private func startContinuousPermissionChecking() {
// Start a timer that continuously checks for permission changes
accessibilityCheckTimer?.invalidate()
accessibilityCheckTimer = Timer.scheduledTimer(withTimeInterval: 0.3, repeats: true) { _ in
checkAccessibilityPermission()
checkMicrophonePermission()
}
}
private func stopContinuousPermissionChecking() {
accessibilityCheckTimer?.invalidate()
accessibilityCheckTimer = nil
}
private func requestMicrophonePermission() {
requestMicrophonePermissionFromUser { granted in
if granted {
self.checkMicrophonePermission()
}
}
}
private func openMicrophoneSettings() {
if let url = URL(string: "x-apple.systempreferences:com.apple.preference.security?Privacy_Microphone") {
NSWorkspace.shared.open(url)
}
}
private func requestMicrophonePermissionFromUser(completion: @escaping (Bool) -> Void) {
switch AVCaptureDevice.authorizationStatus(for: .audio) {
case .authorized:
print("authorized")
completion(true)
case .notDetermined:
print("notDetermined")
AVCaptureDevice.requestAccess(for: .audio) { granted in
DispatchQueue.main.async {
completion(granted)
}
}
case .denied, .restricted:
print("denied")
openMicrophoneSettings()
completion(false)
@unknown default:
print("unknown")
completion(false)
}
}
}