import SwiftUI struct MacRegistrationView: View { @Environment(WizardModel.self) private var model @State private var macInfo: MacDeviceInfo? @State private var isRegistering = false @State private var isInstallingProfile = false @State private var errorMessage: String? @State private var successMessage: String? @State private var profileInstalled = false @State private var checkingProfile = false @State private var isVerifying = false @State private var verifyStatus = "" @State private var backendActive = false var body: some View { VStack(spacing: 24) { Image(systemName: "desktopcomputer.and.arrow.down") .font(.system(size: 80)) .foregroundStyle(.blue) Text("Mac mit DNS-Schutz registrieren") .font(.title) .bold() Text("Dieser Mac wird als geschütztes Gerät registriert. ReBreak installiert ein DNS-Filter-Profil — Glücksspiel-Domains werden auf System-Ebene blockiert.") .multilineTextAlignment(.center) .foregroundStyle(.secondary) .padding(.horizontal, 40) if let info = macInfo { macInfoCard(info) } else { ProgressView("Lese Mac-Informationen...") .progressViewStyle(.circular) } if let error = errorMessage { errorCard(error) } if let success = successMessage { successCard(success) } if let registration = model.magicRegistration { VStack(spacing: 12) { HStack(spacing: 8) { Image(systemName: profileInstalled ? "checkmark.shield.fill" : "checkmark.circle.fill") .foregroundStyle(.green) Text(profileInstalled ? "Mac geschützt + registriert" : "Mac registriert") .font(.headline) .foregroundStyle(.green) } VStack(alignment: .leading, spacing: 6) { Text("✓ Device registriert: \(registration.deviceId.prefix(8))...") if profileInstalled { Text("✓ DNS-Filter-Profil installiert") } } .font(.caption) .foregroundStyle(.secondary) .frame(maxWidth: .infinity, alignment: .leading) } .padding() .background(Color.green.opacity(0.1)) .cornerRadius(8) .frame(maxWidth: 400) } if isVerifying { verifyingCard } HStack(spacing: 12) { Button("← Abbrechen") { model.returnToHub() } .buttonStyle(.bordered) .disabled(isRegistering || isInstallingProfile || isVerifying) if isVerifying { // Während der Verifikation kein Aktions-Button — der Flow läuft // automatisch durch und navigiert bei Erfolg zur Übersicht. EmptyView() } else if model.magicRegistration == nil { Button("Mac registrieren") { handleRegistration() } .buttonStyle(.borderedProminent) .disabled(isRegistering || macInfo == nil || isInstallingProfile) } else if !profileInstalled { Button("DNS-Schutz installieren") { handleProfileInstall() } .buttonStyle(.borderedProminent) .disabled(isInstallingProfile) } else { Button("✓ Fertig — zurück zur Übersicht") { model.returnToHub() } .buttonStyle(.borderedProminent) } if isRegistering || isInstallingProfile { ProgressView() .controlSize(.small) } } } .padding(40) .onAppear { loadMacInfo() checkProfileStatus() } } @ViewBuilder private func macInfoCard(_ info: MacDeviceInfo) -> some View { VStack(alignment: .leading, spacing: 8) { HStack { Image(systemName: "desktopcomputer") .foregroundStyle(.blue) Text(info.hostname) .font(.headline) } Text("\(info.model) · macOS \(info.osVersion)") .font(.callout) .foregroundStyle(.secondary) Text("Device-ID: \(info.deviceId.prefix(8))...\(info.deviceId.suffix(8))") .font(.system(.caption, design: .monospaced)) .foregroundStyle(.tertiary) } .padding() .frame(maxWidth: 400, alignment: .leading) .background(Color.blue.opacity(0.08)) .cornerRadius(8) } @ViewBuilder private func errorCard(_ error: String) -> some View { VStack(spacing: 8) { HStack(spacing: 8) { Image(systemName: "exclamationmark.triangle.fill") .foregroundStyle(.red) Text(error) .font(.callout) .foregroundStyle(.red) .multilineTextAlignment(.leading) } .frame(maxWidth: .infinity, alignment: .leading) } .padding() .background(Color.red.opacity(0.1)) .cornerRadius(8) .frame(maxWidth: 400) } private var verifyingCard: some View { HStack(spacing: 10) { ProgressView() .controlSize(.small) Text(verifyStatus.isEmpty ? "Wird geprüft…" : verifyStatus) .font(.callout) .foregroundStyle(.secondary) } .padding() .background(Color.blue.opacity(0.08)) .cornerRadius(8) .frame(maxWidth: 400) } @ViewBuilder private func successCard(_ message: String) -> some View { HStack(spacing: 8) { Image(systemName: "checkmark.circle.fill") .foregroundStyle(.green) Text(message) .font(.callout) .foregroundStyle(.green) } .padding() .background(Color.green.opacity(0.1)) .cornerRadius(8) .frame(maxWidth: 400) } private func loadMacInfo() { Task { do { let info = try MacDeviceDetector.detect() await MainActor.run { macInfo = info } } catch { await MainActor.run { errorMessage = "Mac-Info konnte nicht gelesen werden: \(error.localizedDescription)" } } } } private func checkProfileStatus() { Task { checkingProfile = true let installed = await MacProfileInstaller.isInstalled() await MainActor.run { profileInstalled = installed checkingProfile = false } } } private func handleRegistration() { Task { isRegistering = true errorMessage = nil successMessage = nil do { try await model.registerMac() await MainActor.run { isRegistering = false } // KEIN Auto-Profile-Install mehr — DNS-Schutz ist optional. // User entscheidet selbst via Button. } catch { await MainActor.run { isRegistering = false errorMessage = error.localizedDescription } } } } private func handleProfileInstall() { guard let registration = model.magicRegistration else { errorMessage = "Keine Registrierung vorhanden. Bitte zuerst registrieren." return } Task { isInstallingProfile = true errorMessage = nil // 1. Profil herunterladen + System Settings → Profile öffnen. do { try await MacProfileInstaller.downloadAndInstall(registration: registration) } catch { await MainActor.run { isInstallingProfile = false errorMessage = "Profil-Installation fehlgeschlagen: \(error.localizedDescription)" } return } await MainActor.run { isInstallingProfile = false isVerifying = true successMessage = "System Settings → Profile geöffnet. Bitte dort „Installieren“ klicken und Admin-Passwort eingeben." verifyStatus = "Warte auf Profil-Installation…" } // 2. Warten bis das Profil tatsächlich installiert ist (User klickt // in System Settings „Installieren" + gibt Admin-PW ein). let profileOK = await pollUntilProfileInstalled(timeoutSeconds: 180) guard profileOK else { await MainActor.run { isVerifying = false verifyStatus = "" errorMessage = "Profil noch nicht installiert. Bitte in System Settings → Profile auf „Installieren“ klicken und es erneut versuchen." } return } await MainActor.run { profileInstalled = true verifyStatus = "Prüfe Schutz-Status am Server…" } // 3. Nebenbei: serverseitigen Binding-Status bestätigen. let backendOK = await pollBackendActive(token: registration.dnsToken, attempts: 5) await MainActor.run { backendActive = backendOK isVerifying = false verifyStatus = "" } guard backendOK else { await MainActor.run { errorMessage = "Profil installiert, aber der Server bestätigt den Schutz noch nicht. Du kannst es später in der Übersicht prüfen." } return } // 4. Erfolg anzeigen, dann zurück zur Übersicht. await MainActor.run { successMessage = "✓ Mac geschützt — Glücksspiel-Domains werden jetzt blockiert." } try? await Task.sleep(nanoseconds: 1_800_000_000) await MainActor.run { model.returnToHub() } } } /// Pollt lokal `profiles show` bis das ReBreak-DNS-Profil erscheint (oder Timeout). private func pollUntilProfileInstalled(timeoutSeconds: Int) async -> Bool { let deadline = Date().addingTimeInterval(Double(timeoutSeconds)) while Date() < deadline { if await MacProfileInstaller.isInstalled() { return true } try? await Task.sleep(nanoseconds: 2_000_000_000) } return await MacProfileInstaller.isInstalled() } /// Pollt `/api/magic/status?token=` bis `active=true` (oder Versuche erschöpft). private func pollBackendActive(token: String, attempts: Int) async -> Bool { for attempt in 0..