import Foundation import SwiftUI private let watchAccent = AppTheme.accent private let watchGold = AppTheme.warmAccent struct WatchRootView: View { @ObservedObject var model: AppViewModel var body: some View { NavigationStack { Group { if model.session == nil { WatchPairingView(model: model) } else { WatchDashboardView(model: model) } } .navigationBarTitleDisplayMode(.inline) } .tint(watchAccent) } } private struct WatchPairingView: View { @ObservedObject var model: AppViewModel var body: some View { ScrollView { VStack(alignment: .leading, spacing: 12) { VStack(alignment: .leading, spacing: 10) { AppBadge(title: "Preview passport", tone: watchAccent) Text("Prove identity from your wrist") .font(.title3.weight(.semibold)) .foregroundStyle(.white) Text("Link this watch to the preview passport so identity checks and alerts stay visible on your wrist.") .font(.footnote) .foregroundStyle(.white.opacity(0.72)) HStack(spacing: 8) { AppStatusTag(title: "Wrist-ready", tone: watchAccent) AppStatusTag(title: "Proof focus", tone: watchGold) } } .watchCard() if model.isBootstrapping { HStack(spacing: 8) { ProgressView() .tint(watchAccent) Text("Preparing preview passport...") .font(.footnote) .foregroundStyle(.white.opacity(0.72)) } .frame(maxWidth: .infinity, alignment: .leading) .watchCard() } Button { Task { await model.signInWithSuggestedPayload() } } label: { if model.isAuthenticating { ProgressView() .frame(maxWidth: .infinity) } else { Label("Link Preview Passport", systemImage: "applewatch") .frame(maxWidth: .infinity) } } .buttonStyle(.borderedProminent) .tint(watchAccent) .disabled(model.isBootstrapping || model.suggestedPairingPayload.isEmpty || model.isAuthenticating) VStack(alignment: .leading, spacing: 10) { Text("What this watch does") .font(.headline) .foregroundStyle(.white) WatchSetupFeatureRow( systemImage: "checkmark.shield", title: "Review identity checks", subtitle: "See pending proof prompts quickly." ) WatchSetupFeatureRow( systemImage: "bell.badge", title: "Surface important alerts", subtitle: "Keep passport activity visible at a glance." ) WatchSetupFeatureRow( systemImage: "iphone.radiowaves.left.and.right", title: "Stay in sync with the phone preview", subtitle: "Use the same mocked passport context." ) } .watchCard() } .padding(.horizontal, 8) .padding(.top, 6) .padding(.bottom, 20) } .background(Color.black.ignoresSafeArea()) .navigationTitle("Link Watch") } } private struct WatchSetupFeatureRow: View { let systemImage: String let title: String let subtitle: String var body: some View { HStack(alignment: .top, spacing: 10) { Image(systemName: systemImage) .font(.footnote.weight(.semibold)) .foregroundStyle(watchAccent) .frame(width: 18, height: 18) VStack(alignment: .leading, spacing: 2) { Text(title) .font(.footnote.weight(.semibold)) .foregroundStyle(.white) Text(subtitle) .font(.caption2) .foregroundStyle(.white.opacity(0.68)) } } } } private extension View { func watchCard() -> some View { padding(14) .frame(maxWidth: .infinity, alignment: .leading) .background(Color.white.opacity(0.08), in: RoundedRectangle(cornerRadius: 22, style: .continuous)) .overlay( RoundedRectangle(cornerRadius: 22, style: .continuous) .stroke(Color.white.opacity(0.10), lineWidth: 1) ) } } private struct WatchDashboardView: View { @ObservedObject var model: AppViewModel var body: some View { ScrollView { VStack(alignment: .leading, spacing: 12) { WatchPassportCard(model: model) .watchCard() WatchSectionHeader( title: "Pending", detail: model.pendingRequests.isEmpty ? nil : "\(model.pendingRequests.count)" ) if model.pendingRequests.isEmpty { VStack(alignment: .leading, spacing: 10) { Text("No checks waiting.") .font(.footnote.weight(.semibold)) .foregroundStyle(.white) Text("New identity checks will appear here when a site or device asks you to prove it is really you.") .font(.caption2) .foregroundStyle(.white.opacity(0.68)) Button("Seed Identity Check") { Task { await model.simulateIncomingRequest() } } .buttonStyle(.bordered) .tint(watchAccent) } .watchCard() } else { ForEach(model.pendingRequests) { request in NavigationLink { WatchRequestDetailView(model: model, requestID: request.id) } label: { WatchRequestRow(request: request) .watchCard() } .buttonStyle(.plain) } } WatchSectionHeader(title: "Activity") if model.notifications.isEmpty { VStack(alignment: .leading, spacing: 8) { Text("No recent alerts.") .font(.footnote.weight(.semibold)) .foregroundStyle(.white) Text("Passport activity and security events will show up here.") .font(.caption2) .foregroundStyle(.white.opacity(0.68)) } .watchCard() } else { ForEach(model.notifications.prefix(3)) { notification in NavigationLink { WatchNotificationDetailView(model: model, notificationID: notification.id) } label: { WatchNotificationRow(notification: notification) .watchCard() } .buttonStyle(.plain) } } WatchSectionHeader(title: "Actions") VStack(alignment: .leading, spacing: 10) { Button("Refresh") { Task { await model.refreshDashboard() } } .buttonStyle(.bordered) .tint(watchAccent) .disabled(model.isRefreshing) Button("Send Test Alert") { Task { await model.sendTestNotification() } } .buttonStyle(.bordered) if model.notificationPermission == .unknown || model.notificationPermission == .denied { Button("Enable Alerts") { Task { await model.requestNotificationAccess() } } .buttonStyle(.bordered) } Button("Sign Out", role: .destructive) { model.signOut() } .buttonStyle(.bordered) } .watchCard() if let profile = model.profile { WatchSectionHeader(title: "Identity") VStack(alignment: .leading, spacing: 8) { Text(profile.handle) .font(.footnote.weight(.semibold)) .foregroundStyle(.white) Text(profile.organization) .font(.caption2) .foregroundStyle(.white.opacity(0.68)) Text("Notifications: \(model.notificationPermission.title)") .font(.caption2) .foregroundStyle(.white.opacity(0.68)) } .watchCard() } } .padding(.horizontal, 8) .padding(.top, 12) .padding(.bottom, 20) } .background(Color.black.ignoresSafeArea()) .navigationTitle("Passport") .refreshable { await model.refreshDashboard() } } } private struct WatchSectionHeader: View { let title: String var detail: String? = nil var body: some View { HStack(alignment: .firstTextBaseline, spacing: 8) { Text(title) .font(.headline) .foregroundStyle(.white) if let detail, !detail.isEmpty { Text(detail) .font(.caption2.weight(.semibold)) .foregroundStyle(.white.opacity(0.58)) } } .padding(.horizontal, 2) } } private struct WatchPassportCard: View { @ObservedObject var model: AppViewModel var body: some View { VStack(alignment: .leading, spacing: 10) { AppBadge(title: "Passport active", tone: watchAccent) VStack(alignment: .leading, spacing: 2) { Text(model.profile?.name ?? "Preview Session") .font(.headline) .foregroundStyle(.white) Text(model.pairedDeviceSummary) .font(.footnote) .foregroundStyle(.white.opacity(0.72)) if let session = model.session { Text("Via \(session.pairingTransport.title)") .font(.caption2) .foregroundStyle(.white.opacity(0.58)) } } HStack(spacing: 8) { WatchMetricPill(title: "Pending", value: "\(model.pendingRequests.count)", accent: watchAccent) WatchMetricPill(title: "Unread", value: "\(model.unreadNotificationCount)", accent: watchGold) } } .padding(.vertical, 6) } } private struct WatchMetricPill: View { let title: String let value: String let accent: Color var body: some View { VStack(alignment: .leading, spacing: 2) { Text(value) .font(.headline.monospacedDigit()) .foregroundStyle(.white) Text(title) .font(.caption2) .foregroundStyle(.white.opacity(0.68)) } .padding(.horizontal, 10) .padding(.vertical, 8) .frame(maxWidth: .infinity, alignment: .leading) .background(accent.opacity(0.14), in: RoundedRectangle(cornerRadius: 14, style: .continuous)) } } private struct WatchRequestRow: View { let request: ApprovalRequest var body: some View { VStack(alignment: .leading, spacing: 4) { HStack(alignment: .top, spacing: 6) { Text(request.title) .font(.headline) .lineLimit(2) .foregroundStyle(.white) Spacer(minLength: 6) Image(systemName: request.risk == .elevated ? "exclamationmark.shield.fill" : "checkmark.shield.fill") .foregroundStyle(request.risk == .elevated ? .orange : watchAccent) } Text(request.source) .font(.footnote) .foregroundStyle(.white.opacity(0.72)) HStack(spacing: 8) { AppStatusTag(title: request.risk.title, tone: request.risk == .routine ? watchAccent : .orange) AppStatusTag(title: request.status.title, tone: request.status == .pending ? .orange : watchAccent) } Text(request.createdAt.watchRelativeString) .font(.caption2) .foregroundStyle(.white.opacity(0.58)) } } } private struct WatchNotificationRow: View { let notification: AppNotification var body: some View { VStack(alignment: .leading, spacing: 4) { HStack(alignment: .top, spacing: 6) { Text(notification.title) .font(.headline) .lineLimit(2) .foregroundStyle(.white) Spacer(minLength: 6) if notification.isUnread { Circle() .fill(watchAccent) .frame(width: 8, height: 8) } } Text(notification.message) .font(.footnote) .foregroundStyle(.white.opacity(0.72)) .lineLimit(2) Text(notification.sentAt.watchRelativeString) .font(.caption2) .foregroundStyle(.white.opacity(0.58)) } } } private struct WatchRequestDetailView: View { @ObservedObject var model: AppViewModel let requestID: ApprovalRequest.ID private var request: ApprovalRequest? { model.requests.first(where: { $0.id == requestID }) } var body: some View { Group { if let request { ScrollView { VStack(alignment: .leading, spacing: 12) { detailHeader( title: request.title, subtitle: request.source, badge: request.status.title ) Text(request.subtitle) .font(.footnote) .foregroundStyle(.secondary) VStack(alignment: .leading, spacing: 6) { Text("Trust Summary") .font(.headline) Text(request.trustHeadline) .font(.subheadline.weight(.semibold)) Text(request.trustDetail) .font(.footnote) .foregroundStyle(.secondary) Text(request.risk.guidance) .font(.footnote) .foregroundStyle(.secondary) } .padding(10) .background(.thinMaterial, in: RoundedRectangle(cornerRadius: 18, style: .continuous)) if !request.scopes.isEmpty { VStack(alignment: .leading, spacing: 8) { Text("Scopes") .font(.headline) ForEach(request.scopes, id: \.self) { scope in Label(scope, systemImage: "checkmark.seal.fill") .font(.footnote) } } } if request.status == .pending { if model.activeRequestID == request.id { ProgressView("Updating proof...") } else { Button("Verify") { Task { await model.approve(request) } } .buttonStyle(.borderedProminent) Button("Decline", role: .destructive) { Task { await model.reject(request) } } } } } .padding(.horizontal, 8) .padding(.bottom, 20) } } else { Text("This request is no longer available.") .foregroundStyle(.secondary) } } .navigationTitle("Identity Check") } private func detailHeader(title: String, subtitle: String, badge: String) -> some View { VStack(alignment: .leading, spacing: 6) { Text(title) .font(.headline) Text(subtitle) .font(.footnote) .foregroundStyle(.secondary) Text(badge) .font(.caption.weight(.semibold)) .padding(.horizontal, 8) .padding(.vertical, 4) .background(watchAccent.opacity(0.14), in: Capsule()) } } } private struct WatchNotificationDetailView: View { @ObservedObject var model: AppViewModel let notificationID: AppNotification.ID private var notification: AppNotification? { model.notifications.first(where: { $0.id == notificationID }) } var body: some View { Group { if let notification { ScrollView { VStack(alignment: .leading, spacing: 12) { VStack(alignment: .leading, spacing: 6) { Text(notification.title) .font(.headline) Text(notification.kind.title) .font(.footnote.weight(.semibold)) .foregroundStyle(watchAccent) Text(notification.sentAt.watchRelativeString) .font(.caption2) .foregroundStyle(.secondary) } Text(notification.message) .font(.footnote) .foregroundStyle(.secondary) VStack(alignment: .leading, spacing: 6) { Text("Alert posture") .font(.headline) Text(model.notificationPermission.summary) .font(.footnote) .foregroundStyle(.secondary) } .padding(10) .background(.thinMaterial, in: RoundedRectangle(cornerRadius: 18, style: .continuous)) if notification.isUnread { Button("Mark Read") { Task { await model.markNotificationRead(notification) } } } } .padding(.horizontal, 8) .padding(.bottom, 20) } } else { Text("This activity item has already been cleared.") .foregroundStyle(.secondary) } } .navigationTitle("Activity") } } private extension Date { var watchRelativeString: String { WatchFormatters.relative.localizedString(for: self, relativeTo: .now) } } private enum WatchFormatters { static let relative: RelativeDateTimeFormatter = { let formatter = RelativeDateTimeFormatter() formatter.unitsStyle = .abbreviated return formatter }() }