import UIKit import SwiftUI import Foundation enum WorkingCopyGuard { enum GuardError: LocalizedError { case notInstalled case apiKeyMissing case repoNameMissing(String) var errorDescription: String? { switch self { case .notInstalled: return "Working Copy is not installed." case .apiKeyMissing: return "Working Copy API key not set. Go to Settings → Working Copy." case .repoNameMissing(let p): return "No Working Copy repo name set for '\(p)'." } } var actionLabel: String { switch self { case .notInstalled: return "App Store" default: return "Open Settings" } } } static func validate(projectName: String, requiresApiKey: Bool = true) throws { guard UIApplication.shared.canOpenURL(URL(string: "working-copy://")!) else { throw GuardError.notInstalled } if requiresApiKey { let key = UserDefaults.standard.string(forKey: "workingCopyAPIKey") ?? "" guard !key.trimmingCharacters(in: .whitespaces).isEmpty else { throw GuardError.apiKeyMissing } } let repoName = UserDefaults.standard.string(forKey: "wc_repo_for_\(projectName)") ?? "" guard !repoName.trimmingCharacters(in: .whitespaces).isEmpty else { throw GuardError.repoNameMissing(projectName) } } } struct WorkingCopyErrorBanner: ViewModifier { @Binding var error: WorkingCopyGuard.GuardError? func body(content: Content) -> some View { content.overlay(alignment: .top) { if let err = error { HStack(spacing: 10) { Image(systemName: "exclamationmark.triangle.fill").foregroundStyle(.orange) Text(err.localizedDescription ?? "").font(.subheadline).lineLimit(2) Spacer() Button(err.actionLabel) { handleAction(for: err) }.buttonStyle(.bordered).controlSize(.small) Button { withAnimation { self.error = nil } } label: { Image(systemName: "xmark").font(.caption.bold()) } .buttonStyle(.plain).foregroundStyle(.secondary) } .padding(.horizontal, 14).padding(.vertical, 10) .background(.regularMaterial) .transition(.move(edge: .top).combined(with: .opacity)) } } .animation(.spring(response: 0.3), value: error != nil) } private func handleAction(for err: WorkingCopyGuard.GuardError) { switch err { case .notInstalled: UIApplication.shared.open(URL(string: "https://apps.apple.com/app/working-copy-git-client/id896694807")!) default: NotificationCenter.default.post(name: .openSettings, object: nil) } self.error = nil } } extension View { func workingCopyErrorBanner(_ error: Binding) -> some View { modifier(WorkingCopyErrorBanner(error: error)) } }