201 lines
8.1 KiB
Swift
201 lines
8.1 KiB
Swift
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<Void, Never>?
|
|
|
|
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
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|