import SwiftUI struct WelcomeView: View { @Environment(WizardModel.self) private var model @State private var detecting = false @State private var error: String? @State private var pollTask: Task? 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) } } .padding(40) .onAppear { startDetection() } .onDisappear { pollTask?.cancel() } } 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 } } } } }