Files
QuickDrop/QuickDrop/Util/ErrorAlertHandler.swift
T

239 lines
7.6 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//
// ErrorAlertHandler.swift
// QuickDrop
//
// Created by Leon Böttger on 25.07.25.
//
import AudioToolbox
import Network
import StoreKit
import SwiftUI
import UserNotifications
#if os(macOS)
import BezelNotification
import Cocoa
#else
import LUI
#endif
class ErrorAlertHandler {
private init() {}
static let shared = ErrorAlertHandler()
private var isAlertShown = false
#if os(macOS)
private var firewallAlertWindow: NSWindow?
private var apIsolationAlertWindow: NSWindow?
private var networkFilterAlertWindow: NSWindow?
#endif
func showErrorAlert(for deviceName: String, error: Error) {
#if os(macOS)
NSApp.activate(ignoringOtherApps: true)
#endif
var description = ""
let fixInstructions = " " + "Error.FixInstructions".localized()
if let ne = (error as? NearbyError) {
switch ne {
case .inputOutput:
description = "I/O Error." + fixInstructions
case .protocolError(errorMessage: let errorMessage):
description = errorMessage + fixInstructions
case .packetFilterError:
#if os(macOS)
openAlert(type: .NetworkFilter)
return
#else
description = error.localizedDescription
#endif
case .firewallError:
#if os(macOS)
openAlert(type: .Firewall)
return
#else
description = error.localizedDescription
#endif
case .requiredFieldMissing(errorMessage: let errorMessage):
description = errorMessage + fixInstructions
case .ukey2:
description = "Error.Crypto".localized() + ": \(ne.localizedDescription)" + fixInstructions
case .canceled(reason: let reason):
if reason == .timedOut {
description = reason.localizedDescription() + fixInstructions
}
else {
description = reason.localizedDescription()
}
}
} else {
description = error.localizedDescription
}
// Prevent multiple alerts at the same time
if self.isAlertShown {
log("Skipping alert for error \(error.localizedDescription) because one is already shown")
return
}
else {
AudioManager.playErrorSound()
let title = String(format: "TransferError".localized(), arguments: [deviceName])
log("Showing alert with title: \"\(title)\" and description: \"\(description)\"")
log("Unsuccessful transmission. Already successful transmissions: \(Settings.shared.incomingTransmissionCount)")
#if os(macOS)
let primaryButtonTitle = "InformDeveloper".localized()
let secondaryButtonTitle = "CloseAlert".localized()
self.isAlertShown = true
let alert = NSAlert()
alert.alertStyle = .critical
alert.messageText = title
alert.informativeText = description
alert.addButton(withTitle: primaryButtonTitle)
alert.addButton(withTitle: secondaryButtonTitle)
let result = alert.runModal()
self.isAlertShown = false
if result == .alertFirstButtonReturn {
//log("Sending logging string")
//sendLoggingString()
}
#else
showAlert(title: title, message: description)
#endif
}
}
#if os(macOS)
func openAlert(type: AlertType) {
log("Opening Alert for \(type)")
AudioManager.playErrorSound()
DispatchQueue.main.async { [self] in
// Create an NSWindow to host the SwiftUI view
let alertWindow = NSWindow(
contentRect: NSRect(x: 0, y: 0, width: issueViewWidth, height: issueViewHeight),
styleMask: [.titled, .closable],
backing: .buffered,
defer: false
)
switch type {
case .ApIsolation:
apIsolationAlertWindow = alertWindow
alertWindow.contentView = NSHostingView(rootView: ApIsolationIssueView(closeView: { self.apIsolationAlertWindow?.close() }))
case .NetworkFilter:
networkFilterAlertWindow = alertWindow
alertWindow.contentView = NSHostingView(rootView: NetworkFilterIssueView())
case .Firewall:
firewallAlertWindow = alertWindow
alertWindow.contentView = NSHostingView(rootView: FirewallIssueView())
}
alertWindow.title = "QuickDrop"
alertWindow.center()
alertWindow.isReleasedWhenClosed = false
alertWindow.setFrameAutosaveName(type.rawValue)
NSApp.activate(ignoringOtherApps: true)
alertWindow.makeKeyAndOrderFront(nil)
alertWindow.level = .normal
}
}
func closeApIsolationAlert() {
log("Closing AP Isolation Alert")
DispatchQueue.main.async {
self.apIsolationAlertWindow?.close()
self.apIsolationAlertWindow = nil
}
}
#endif
}
#if os(macOS)
func sendLoggingString() {
// If we're in an extension, redirect to the main app
if Bundle.main.bundlePath.hasSuffix(".appex") {
log("sendLoggingString: in extension, redirecting to main app")
if let url = URL(string: "quickdrop://sendLog") {
NSWorkspace.shared.open(url)
}
return
}
// We're in the main app – do the actual sending
if let url = LogManager.sharedInstance.logFileURL {
let appVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "Unknown"
log("Sending logging string with file URL: \(url)")
log("Currently running apps: \(NSWorkspace.shared.runningApplications.map { $0.localizedName ?? "Unknown" })")
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3, execute: {
sendEmailWithAttachment(
fileURL: url,
recipients: ["quickdrop@leonboettger.com"],
subject: "QuickDrop \(appVersion) - \(getDeviceAndSystem())"
)
})
}
}
fileprivate func getDeviceAndSystem() -> String {
// Get Mac model identifier
var size: Int = 0
sysctlbyname("hw.model", nil, &size, nil, 0)
var modelBuffer = [CChar](repeating: 0, count: size)
sysctlbyname("hw.model", &modelBuffer, &size, nil, 0)
let model = String(cString: modelBuffer)
// Get macOS version
let version = ProcessInfo.processInfo.operatingSystemVersion
let versionString = "\(version.majorVersion).\(version.minorVersion).\(version.patchVersion)"
return "\(model) - \(versionString)"
}
func sendEmailWithAttachment(fileURL: URL, recipients: [String], subject: String) {
guard let emailService = NSSharingService(named: .composeEmail) else {
log("No email service available")
return
}
emailService.recipients = recipients
emailService.subject = subject
emailService.perform(withItems: [fileURL])
log("Email service opened with recipients: \(recipients) and subject: \(subject)")
}
func copyToClipboard(_ text: String) {
let pasteboard = NSPasteboard.general
pasteboard.clearContents()
pasteboard.setString(text, forType: .string)
}
#endif
enum AlertType: String {
case ApIsolation
case NetworkFilter
case Firewall
}