Redesign:
- Nach Login landet User direkt im neuen DeviceHubView statt
Auto-Mac-Registrierung. Hub zeigt: User-Email, X/5-Slot-Counter,
Liste aller registrierten Geraete + 'Geraet hinzufuegen' mit
iPhone/iPad vs Mac Wahl.
- Mac wird NUR registriert wenn User aktiv 'Mac' im Hub waehlt
(frueher: auto on app-start, frass Slot).
- iOS-Pfad: Hub -> Welcome/Preflight/Supervise/Enroll/Configure
-> Done -> 'Zurueck zur Geraete-Uebersicht'.
- Mac-Pfad: Hub -> MacRegistrationView (Register+DNS-Install)
-> 'Fertig -> Hub'.
- Wizard-Header hat jetzt Grid-Icon 'Zur Geraete-Uebersicht' als
Escape-Hatch jederzeit.
- Per-Device-Loeschung im Hub: Trash-Icon -> Confirm-Dialog
('Auf X muss Freigabe bestaetigt werden, 24h Cooldown') ->
request-release-Endpoint (existing infra).
- Device-Limit 3 -> 5 in backend (Staging-Testing + Legend-Wert
fuer spaeter).
- StepIndicator/Step-Counter: macRegistration zaehlt nicht im
iOS-Flow.
238 lines
7.5 KiB
Swift
238 lines
7.5 KiB
Swift
import Foundation
|
|
import Observation
|
|
|
|
enum DebugSupervisionMode: String, CaseIterable, Identifiable {
|
|
case none
|
|
case forceSupervised
|
|
case forceUnsupervised
|
|
|
|
var id: String { rawValue }
|
|
|
|
var title: String {
|
|
switch self {
|
|
case .none: return "Aus"
|
|
case .forceSupervised: return "Force Supervised"
|
|
case .forceUnsupervised: return "Force Unsupervised"
|
|
}
|
|
}
|
|
}
|
|
|
|
@MainActor
|
|
@Observable
|
|
final class WizardModel {
|
|
var step: WizardStep = .macRegistration
|
|
var device: DeviceState?
|
|
|
|
var supervisionLog: [String] = []
|
|
var supervisionRunning: Bool = false
|
|
var supervisionError: String?
|
|
|
|
var enrollmentLog: [String] = []
|
|
var enrollmentRunning: Bool = false
|
|
var enrollmentError: String?
|
|
|
|
var configureLog: [String] = []
|
|
var configureRunning: Bool = false
|
|
var configureError: String?
|
|
|
|
var showAdvancedLogs: Bool = false
|
|
|
|
var cooldownEndsAt: Date?
|
|
|
|
// Debug-Reset State
|
|
var supervisionMode: DebugSupervisionMode = .none
|
|
var resetRunning: Bool = false
|
|
var resetStatus: String?
|
|
var resetAll: Bool = true
|
|
var resetEnrollmentProfile: Bool = true
|
|
var resetLockProfile: Bool = true
|
|
var resetApp: Bool = true
|
|
|
|
// Auth + Magic State
|
|
var authSession: AuthSession?
|
|
var showingLogin: Bool = false
|
|
var showingHub: Bool = false
|
|
var showingManageBindings: Bool = false
|
|
var magicRegistration: MagicRegistration?
|
|
var registrationError: String?
|
|
|
|
init() {
|
|
// Load existing session from keychain
|
|
authSession = AuthService.shared.currentSession()
|
|
showingLogin = (authSession == nil)
|
|
// Nach Login direkt zum Hub statt Mac-Auto-Registrierung
|
|
showingHub = (authSession != nil)
|
|
}
|
|
|
|
func advance() {
|
|
if let next = WizardStep(rawValue: step.rawValue + 1) {
|
|
step = next
|
|
}
|
|
}
|
|
|
|
func goTo(_ s: WizardStep) {
|
|
step = s
|
|
}
|
|
|
|
// MARK: - Mac Registration
|
|
|
|
/// Registriert den aktuellen Mac im Backend.
|
|
/// Wirft MagicError.limitReached falls Device-Limit erreicht.
|
|
func registerMac() async throws {
|
|
registrationError = nil
|
|
|
|
do {
|
|
let macInfo = try MacDeviceDetector.detect()
|
|
|
|
let registration = try await MagicAPIClient.shared.register(
|
|
deviceId: macInfo.deviceId,
|
|
hostname: macInfo.hostname,
|
|
model: macInfo.model,
|
|
osVersion: macInfo.osVersion
|
|
)
|
|
|
|
magicRegistration = registration
|
|
|
|
} catch let error as MagicError {
|
|
// Bei limit_reached → öffne ManageBindingsView
|
|
if case .limitReached(_) = error {
|
|
showingManageBindings = true
|
|
}
|
|
registrationError = error.localizedDescription
|
|
throw error
|
|
} catch {
|
|
registrationError = error.localizedDescription
|
|
throw error
|
|
}
|
|
}
|
|
|
|
func handleLogin(session: AuthSession) {
|
|
authSession = session
|
|
showingLogin = false
|
|
showingHub = true
|
|
}
|
|
|
|
func handleLogout() async {
|
|
await AuthService.shared.signOut()
|
|
authSession = nil
|
|
showingLogin = true
|
|
showingHub = false
|
|
reset()
|
|
}
|
|
|
|
// MARK: - Hub Navigation
|
|
|
|
/// User wählt 'iOS-Gerät hinzufügen' im Hub.
|
|
func startIOSFlow() {
|
|
showingHub = false
|
|
step = .welcome
|
|
}
|
|
|
|
/// User wählt 'Mac schützen' im Hub.
|
|
func startMacFlow() {
|
|
showingHub = false
|
|
step = .macRegistration
|
|
}
|
|
|
|
/// Zurück zur Geräte-Übersicht.
|
|
func returnToHub() {
|
|
reset()
|
|
showingHub = true
|
|
}
|
|
|
|
func reset() {
|
|
step = .macRegistration
|
|
device = nil
|
|
supervisionLog = []
|
|
enrollmentLog = []
|
|
configureLog = []
|
|
supervisionError = nil
|
|
enrollmentError = nil
|
|
configureError = nil
|
|
showAdvancedLogs = false
|
|
cooldownEndsAt = nil
|
|
resetStatus = nil
|
|
magicRegistration = nil
|
|
registrationError = nil
|
|
}
|
|
|
|
func startDebugReset() {
|
|
guard 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 = self.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 }
|
|
self.device = device
|
|
}
|
|
|
|
resetRunning = false
|
|
}
|
|
} catch {
|
|
await MainActor.run {
|
|
resetStatus = "✗ Reset fehlgeschlagen: \(error.localizedDescription)"
|
|
resetRunning = false
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|