feat(ios): initial SilverApple Swift project scaffold
- App entry point, AppEnvironment dependency container - PassportAuthService: PKCE OAuth 2.0 against SilverSHELL Passport - TokenStore: Keychain-backed token storage - SilverAPIClient: MDM enrollment + privacy score API calls - MDMEnrollmentService: enrollment URL fetching + device listing - Onboarding coordinator with MDM, account setup, and hardening steps - Dashboard with privacy score ring and category breakdown - WelcomeView with Passport sign-in flow Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
115
SilverApple/Onboarding/Views/MDMEnrollmentView.swift
Normal file
115
SilverApple/Onboarding/Views/MDMEnrollmentView.swift
Normal file
@@ -0,0 +1,115 @@
|
||||
import SwiftUI
|
||||
import SafariServices
|
||||
|
||||
struct MDMEnrollmentView: View {
|
||||
var onComplete: () -> Void
|
||||
@EnvironmentObject var env: AppEnvironment
|
||||
@State private var isLoading = false
|
||||
@State private var showSafari = false
|
||||
@State private var enrollUrl: URL?
|
||||
@State private var error: String?
|
||||
@State private var didEnroll = false
|
||||
|
||||
var body: some View {
|
||||
ScrollView {
|
||||
VStack(spacing: 32) {
|
||||
Image(systemName: "iphone.badge.play")
|
||||
.font(.system(size: 56))
|
||||
.foregroundStyle(.accentColor)
|
||||
|
||||
VStack(spacing: 12) {
|
||||
Text("Device Management")
|
||||
.font(.title2.weight(.semibold))
|
||||
|
||||
Text("SilverApple uses Apple MDM to automatically configure your device — installing VPN, DNS, and account profiles without manual setup.")
|
||||
.font(.subheadline)
|
||||
.foregroundStyle(.secondary)
|
||||
.multilineTextAlignment(.center)
|
||||
|
||||
Text("You remain in control: you can remove MDM enrollment at any time in Settings → General → VPN & Device Management.")
|
||||
.font(.caption)
|
||||
.foregroundStyle(.tertiary)
|
||||
.multilineTextAlignment(.center)
|
||||
.padding(.top, 4)
|
||||
}
|
||||
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
FeatureRow(icon: "lock.shield", text: "Privacy hardening profiles")
|
||||
FeatureRow(icon: "network", text: "SilverVPN configuration")
|
||||
FeatureRow(icon: "globe", text: "Encrypted DNS via Technitium")
|
||||
FeatureRow(icon: "envelope", text: "Mailcow email account")
|
||||
FeatureRow(icon: "calendar", text: "CalDAV calendar sync")
|
||||
FeatureRow(icon: "person.crop.rectangle", text: "CardDAV contacts sync")
|
||||
}
|
||||
.padding()
|
||||
.background(.secondary.opacity(0.08), in: RoundedRectangle(cornerRadius: 12))
|
||||
|
||||
if let error {
|
||||
Text(error).font(.caption).foregroundStyle(.red).multilineTextAlignment(.center)
|
||||
}
|
||||
|
||||
VStack(spacing: 12) {
|
||||
Button {
|
||||
Task { await startEnrollment() }
|
||||
} label: {
|
||||
Group {
|
||||
if isLoading { ProgressView() }
|
||||
else { Label("Enroll this Device", systemImage: "checkmark.shield.fill") }
|
||||
}
|
||||
.frame(maxWidth: .infinity).padding()
|
||||
}
|
||||
.buttonStyle(.borderedProminent)
|
||||
.disabled(isLoading || didEnroll)
|
||||
|
||||
if didEnroll {
|
||||
Button("Continue →", action: onComplete)
|
||||
.buttonStyle(.bordered)
|
||||
}
|
||||
|
||||
Button("Skip for now") { onComplete() }
|
||||
.foregroundStyle(.secondary)
|
||||
.font(.caption)
|
||||
}
|
||||
}
|
||||
.padding(24)
|
||||
}
|
||||
.sheet(isPresented: $showSafari) {
|
||||
if let url = enrollUrl {
|
||||
SafariView(url: url)
|
||||
.ignoresSafeArea()
|
||||
.onDisappear { didEnroll = true }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func startEnrollment() async {
|
||||
isLoading = true; error = nil
|
||||
do {
|
||||
let response = try await env.apiClient.getEnrollmentToken()
|
||||
enrollUrl = URL(string: response.enrollmentUrl)
|
||||
showSafari = true
|
||||
} catch {
|
||||
self.error = error.localizedDescription
|
||||
}
|
||||
isLoading = false
|
||||
}
|
||||
}
|
||||
|
||||
struct FeatureRow: View {
|
||||
let icon: String
|
||||
let text: String
|
||||
var body: some View {
|
||||
Label(text, systemImage: icon)
|
||||
.font(.subheadline)
|
||||
.foregroundStyle(.primary)
|
||||
}
|
||||
}
|
||||
|
||||
/// UIViewControllerRepresentable wrapper for SFSafariViewController
|
||||
struct SafariView: UIViewControllerRepresentable {
|
||||
let url: URL
|
||||
func makeUIViewController(context: Context) -> SFSafariViewController {
|
||||
SFSafariViewController(url: url)
|
||||
}
|
||||
func updateUIViewController(_ vc: SFSafariViewController, context: Context) {}
|
||||
}
|
||||
Reference in New Issue
Block a user