import SwiftUI private enum DebugSupervisionMode: String, CaseIterable, Identifiable { case none case forceSupervised case forceUnsupervised var id: String { rawValue } var title: String { switch self { case .none: return "Kein Mode-Change" case .forceSupervised: return "Supervised setzen" case .forceUnsupervised: return "Unsupervised setzen" } } } struct WelcomeView: View { @Environment(WizardModel.self) private var model @State private var detecting = false @State private var error: String? @State private var pollTask: Task? @State private var resetRunning = false @State private var resetStatus: String? @State private var resetAll = true @State private var resetEnrollmentProfile = true @State private var resetLockProfile = true @State private var resetApp = true @State private var supervisionMode: DebugSupervisionMode = .none var body: some View { VStack(spacing: 24) { Image(systemName: "iphone.gen3") .font(.system(size: 80)) .foregroundStyle(.tint) Text("iPhone via USB verbinden") .font(.title) .bold() Text("Stecke dein iPhone per USB-C-Kabel an deinen Mac. Falls ein „Diesem Computer vertrauen?\"-Dialog erscheint, tippe auf **Vertrauen** + gib deinen iPhone-Code ein.") .multilineTextAlignment(.center) .foregroundStyle(.secondary) .padding(.horizontal, 40) if let device = model.device { deviceCard(device) } else if detecting { ProgressView("Suche iPhone …") .progressViewStyle(.circular) } else if let error { VStack(spacing: 8) { Image(systemName: "exclamationmark.triangle") .foregroundStyle(.orange) Text(error).font(.callout).foregroundStyle(.secondary).multilineTextAlignment(.center) } .padding() .background(Color.orange.opacity(0.1)) .cornerRadius(8) } HStack(spacing: 12) { Button("Erneut suchen") { startDetection() } .buttonStyle(.bordered) .disabled(detecting) Button(nextButtonLabel) { handleNext() } .buttonStyle(.borderedProminent) .disabled(model.device == nil) } resetSection } .padding(40) .onAppear { startDetection() } .onDisappear { pollTask?.cancel() } } private var resetSection: some View { VStack(alignment: .leading, spacing: 8) { Divider() Text("Interner Test-Reset") .font(.headline) Text("Wähle gezielt, was entfernt werden soll. Optional kann zusätzlich supervised/unsupervised für Tests gesetzt werden.") .font(.callout) .foregroundStyle(.secondary) Toggle("Alles entfernen (Profile + App)", isOn: $resetAll) .toggleStyle(.checkbox) .onChange(of: resetAll) { _, newValue in if newValue { resetEnrollmentProfile = true resetLockProfile = true resetApp = true } } Group { Toggle("MDM Enrollment-Profil löschen", isOn: $resetEnrollmentProfile) .toggleStyle(.checkbox) Toggle("Lock-Profil löschen", isOn: $resetLockProfile) .toggleStyle(.checkbox) Toggle("ReBreak-App löschen", isOn: $resetApp) .toggleStyle(.checkbox) } .disabled(resetAll) Picker("Test-Mode", selection: $supervisionMode) { ForEach(DebugSupervisionMode.allCases) { mode in Text(mode.title).tag(mode) } } .pickerStyle(.segmented) if let resetStatus { Text(resetStatus) .font(.caption) .foregroundStyle(.secondary) } HStack(spacing: 10) { if resetRunning { ProgressView() .controlSize(.small) } Button("Debug-Reset ausführen") { startDebugReset() } .buttonStyle(.bordered) .disabled(model.device == nil || resetRunning || detecting) } } .frame(maxWidth: 520, alignment: .leading) } private var nextButtonLabel: String { if model.device?.isFullyBound == true { return "Weiter → Schutz aktivieren" } if model.device?.isOwnedByReBreak == true { return "Weiter → MDM neu enrollen" } return "Weiter" } private func handleNext() { // Smart-Resume mit echter Validation: // - isFullyBound (supervised + recent MDM-ack) → skip zu Configure // - isOwnedByReBreak aber MDM-channel tot → skip zu Enroll // - sonst normaler Wizard-Flow if model.device?.isFullyBound == true { model.goTo(.configure) } else if model.device?.isOwnedByReBreak == true { model.goTo(.enroll) } else { model.advance() } } private func deviceCard(_ d: DeviceState) -> some View { VStack(alignment: .leading, spacing: 8) { HStack { Image(systemName: "checkmark.circle.fill") .foregroundStyle(.green) Text(d.deviceName).font(.headline) } Text("\(d.displayModel) · iOS \(d.productVersion)") .font(.callout).foregroundStyle(.secondary) Text("UDID: \(d.udid)") .font(.system(.caption, design: .monospaced)) .foregroundStyle(.tertiary) .lineLimit(1) .truncationMode(.middle) if d.isFullyBound { HStack(spacing: 6) { Image(systemName: "checkmark.shield.fill") .foregroundStyle(.green) Text("Vollständig durch **ReBreak** gebunden") .font(.callout) .foregroundStyle(.green) } .padding(.top, 4) if let ack = d.enrollmentStatus?.lastAckAt { Text("Letzter MDM-Check-In: \(ack.formatted(date: .abbreviated, time: .shortened))") .font(.caption) .foregroundStyle(.secondary) } Text("Wir überspringen Supervise + Enroll und gehen direkt zum Configure-Step.") .font(.caption) .foregroundStyle(.secondary) } else if d.isOwnedByReBreak && !d.hasEnrollmentProfile { HStack(spacing: 6) { Image(systemName: "exclamationmark.shield") .foregroundStyle(.orange) Text("Supervised, **aber Enrollment-Profil fehlt**") .font(.callout) .foregroundStyle(.orange) } .padding(.top, 4) Text("iPhone braucht MDM-Profil-Installation (Step 4).") .font(.caption) .foregroundStyle(.secondary) } else if d.isOwnedByReBreak { HStack(spacing: 6) { Image(systemName: "shield.lefthalf.filled") .foregroundStyle(.orange) Text("Supervised by ReBreak, **MDM-Kanal stumm**") .font(.callout) .foregroundStyle(.orange) } .padding(.top, 4) if let ack = d.enrollmentStatus?.lastAckAt { Text("Letzter Check-In: \(ack.formatted(date: .abbreviated, time: .shortened)) — älter als 30min") .font(.caption) .foregroundStyle(.secondary) } else { Text("Kein MDM-Check-In aufgezeichnet. iPhone neu enrollen.") .font(.caption) .foregroundStyle(.secondary) } } else if d.isSupervised == true, let org = d.supervisorOrgName { HStack(spacing: 6) { Image(systemName: "exclamationmark.shield") .foregroundStyle(.orange) Text("Supervised by „\(org)\" (nicht ReBreak)") .font(.callout) .foregroundStyle(.orange) } .padding(.top, 4) Text("Wir überschreiben das beim Supervise-Step.") .font(.caption) .foregroundStyle(.secondary) } } .padding() .frame(maxWidth: 400, alignment: .leading) .background(Color.green.opacity(0.08)) .cornerRadius(8) } private func startDetection() { pollTask?.cancel() detecting = true error = nil pollTask = Task { do { var device = try await DeviceDetector.detect() // Smart-Resume Layer 1: supervised + by ReBreak? let status = await DeviceDetector.readSupervisionStatus() device.isSupervised = status.isSupervised device.supervisorOrgName = status.organizationName device.isFmiOn = status.findMyEnabled // Smart-Resume Layer 2: MDM-Channel real lebendig? // Auch wenn supervised, kann re-supervise das MDM-Enrollment // gekillt haben. Daher DB-Real-Check + cfgutil-Ground-Truth. if let enrollment = try? await MDMStatus.query(udid: device.udid) { device.enrollmentStatus = enrollment device.isEnrolled = enrollment.isEnrolled } // Smart-Resume Layer 3: ist das Enrollment-Profil REAL auf iPhone? // (cfgutil-Liste — User kann Profil manuell entfernt haben, // NanoMDM-DB merkt das erst beim nächsten APNs-Cycle.) device.installedProfileIDs = await DeviceDetector.installedProfileIDs() device.installedAppBundleIDs = await DeviceDetector.installedAppBundleIDs() await MainActor.run { model.device = device detecting = false } } catch { await MainActor.run { self.error = error.localizedDescription self.detecting = false } } } } private func startDebugReset() { guard model.device != nil else { resetStatus = "Kein iPhone erkannt." return } resetRunning = true resetStatus = "Führe Debug-Reset aus …" Task { do { var changes: [String] = [] let removeEnrollment = resetAll || resetEnrollmentProfile let removeLock = resetAll || resetLockProfile let removeApp = resetAll || resetApp let installedProfileIDs = await DeviceDetector.installedProfileIDs() var profileIDs: [String] = [] if removeEnrollment, installedProfileIDs.contains(DeviceState.enrollmentProfileID) { profileIDs.append(DeviceState.enrollmentProfileID) } if removeLock, installedProfileIDs.contains(DeviceState.lockProfileID) { profileIDs.append(DeviceState.lockProfileID) } if !profileIDs.isEmpty { try await DeviceDetector.removeProfiles(identifiers: profileIDs) changes.append("Profile gelöscht: \(profileIDs.joined(separator: ", "))") } if removeApp { try await DeviceDetector.removeApp(bundleID: "org.rebreak.app") changes.append("App gelöscht: org.rebreak.app") } switch supervisionMode { case .forceSupervised: _ = try await SuperviseRunner.supervise(verbose: false) { _ in } changes.append("Mode gesetzt: supervised") case .forceUnsupervised: _ = try await SuperviseRunner.unsupervise { _ in } changes.append("Mode gesetzt: unsupervised") case .none: break } let nowInstalledProfiles = await DeviceDetector.installedProfileIDs() let nowApps = await DeviceDetector.installedAppBundleIDs() let status = await DeviceDetector.readSupervisionStatus() await MainActor.run { if changes.isEmpty { resetStatus = "Keine Aktion gewählt." } else { resetStatus = "✓ \(changes.joined(separator: " · "))" } if var device = model.device { device.installedProfileIDs = nowInstalledProfiles device.installedAppBundleIDs = nowApps device.isSupervised = status.isSupervised device.supervisorOrgName = status.organizationName device.isFmiOn = status.findMyEnabled device.isEnrolled = nowInstalledProfiles.contains(DeviceState.enrollmentProfileID) if !nowApps.contains("org.rebreak.app") { device.isManaged = false } if !nowInstalledProfiles.contains(DeviceState.lockProfileID) { device.isFilterActive = false } model.device = device } resetRunning = false } } catch { await MainActor.run { resetStatus = "✗ Reset fehlgeschlagen: \(error.localizedDescription)" resetRunning = false } } } } }