import SwiftUI struct InboxListView: View { @ObservedObject var model: AppViewModel @Binding var selectedRequestID: ApprovalRequest.ID? @Binding var searchText: String @Binding var isSearchPresented: Bool var usesSelection = false private var filteredRequests: [ApprovalRequest] { guard !searchText.isEmpty else { return model.requests } return model.requests.filter { $0.inboxTitle.localizedCaseInsensitiveContains(searchText) || $0.source.localizedCaseInsensitiveContains(searchText) || $0.subtitle.localizedCaseInsensitiveContains(searchText) } } private var recentRequests: [ApprovalRequest] { filteredRequests.filter { Date.now.timeIntervalSince($0.createdAt) <= 60 * 30 } } private var earlierRequests: [ApprovalRequest] { filteredRequests.filter { Date.now.timeIntervalSince($0.createdAt) > 60 * 30 } } private var highlightedRequestID: ApprovalRequest.ID? { filteredRequests.first?.id } var body: some View { List { if filteredRequests.isEmpty { EmptyPaneView( title: "No sign-in requests", message: "New approval requests will appear here as soon as a relying party asks for proof.", systemImage: "tray" ) .listRowBackground(Color.clear) } else { ForEach(recentRequests) { request in row(for: request, compact: false) .transition(.move(edge: .top).combined(with: .opacity)) } if !earlierRequests.isEmpty { Section { ForEach(earlierRequests) { request in row(for: request, compact: true) .transition(.move(edge: .top).combined(with: .opacity)) } } header: { Text("Earlier today") .textCase(nil) } } } } .listStyle(.plain) .navigationTitle("Inbox") .animation(.spring(response: 0.35, dampingFraction: 0.88), value: filteredRequests.map(\.id)) .idpSearchable(text: $searchText, isPresented: $isSearchPresented) } @ViewBuilder private func row(for request: ApprovalRequest, compact: Bool) -> some View { if usesSelection { Button { selectedRequestID = request.id Haptics.selection() } label: { ApprovalRow( request: request, handle: model.profile?.handle ?? "@you", compact: compact, highlighted: highlightedRequestID == request.id ) } .buttonStyle(.plain) .listRowInsets(EdgeInsets(top: 4, leading: 8, bottom: 4, trailing: 8)) .listRowSeparator(.hidden) .listRowBackground(Color.clear) } else { NavigationLink(value: request.id) { ApprovalRow( request: request, handle: model.profile?.handle ?? "@you", compact: compact, highlighted: highlightedRequestID == request.id ) } .buttonStyle(.plain) .listRowInsets(EdgeInsets(top: 4, leading: 8, bottom: 4, trailing: 8)) .listRowSeparator(.hidden) .listRowBackground(Color.clear) } } } struct NotificationCenterView: View { @ObservedObject var model: AppViewModel private var groupedNotifications: [(String, [AppNotification])] { let calendar = Calendar.current let groups = Dictionary(grouping: model.notifications) { calendar.startOfDay(for: $0.sentAt) } return groups .keys .sorted(by: >) .map { day in (sectionTitle(for: day), groups[day]?.sorted(by: { $0.sentAt > $1.sentAt }) ?? []) } } var body: some View { Group { if model.notifications.isEmpty { EmptyPaneView( title: "All clear", message: "You'll see new sign-in requests here.", systemImage: "shield" ) } else { List { if model.notificationPermission == .unknown || model.notificationPermission == .denied { NotificationPermissionCard(model: model) .listRowInsets(EdgeInsets(top: 12, leading: 12, bottom: 12, trailing: 12)) .listRowSeparator(.hidden) .listRowBackground(Color.clear) } ForEach(groupedNotifications, id: \.0) { section in Section { ForEach(section.1) { notification in Button { guard notification.isUnread else { return } Task { await model.markNotificationRead(notification) } } label: { NotificationEventRow(notification: notification) } .buttonStyle(.plain) } } header: { Text(section.0) .textCase(nil) } } } .listStyle(.plain) } } .navigationTitle("Notifications") } private func sectionTitle(for date: Date) -> String { if Calendar.current.isDateInToday(date) { return "Today" } if Calendar.current.isDateInYesterday(date) { return "Yesterday" } return date.formatted(.dateTime.month(.wide).day()) } } struct DevicesView: View { @ObservedObject var model: AppViewModel @State private var isPairingCodePresented = false private var devices: [DevicePresentation] { guard let session else { return [] } let current = DevicePresentation( name: session.deviceName, systemImage: symbolName(for: session.deviceName), lastSeen: .now, isCurrent: true, isTrusted: true ) let others = [ DevicePresentation(name: "Phil's iPad Pro", systemImage: "ipad", lastSeen: .now.addingTimeInterval(-60 * 18), isCurrent: false, isTrusted: true), DevicePresentation(name: "Berlin MacBook Pro", systemImage: "laptopcomputer", lastSeen: .now.addingTimeInterval(-60 * 74), isCurrent: false, isTrusted: true), DevicePresentation(name: "Apple Watch", systemImage: "applewatch", lastSeen: .now.addingTimeInterval(-60 * 180), isCurrent: false, isTrusted: false) ] let count = max((model.profile?.deviceCount ?? 1) - 1, 0) return [current] + Array(others.prefix(count)) } private var session: AuthSession? { model.session } var body: some View { Form { Section("This device") { if let current = devices.first { DeviceItemRow(device: current) } } Section("Other devices ยท \(max(devices.count - 1, 0))") { ForEach(Array(devices.dropFirst())) { device in DeviceItemRow(device: device) } } Section { VStack(spacing: 12) { Button("Pair another device") { isPairingCodePresented = true } .buttonStyle(PrimaryActionStyle()) Button("Sign out everywhere") { model.signOut() } .buttonStyle(DestructiveStyle()) } .padding(.vertical, 6) } } .navigationTitle("Devices") .sheet(isPresented: $isPairingCodePresented) { if let session { OneTimePasscodeSheet(session: session) } } } private func symbolName(for deviceName: String) -> String { let lowercased = deviceName.lowercased() if lowercased.contains("ipad") { return "ipad" } if lowercased.contains("watch") { return "applewatch" } if lowercased.contains("mac") || lowercased.contains("safari") { return "laptopcomputer" } return "iphone" } } struct IdentityView: View { @ObservedObject var model: AppViewModel var body: some View { Form { if let profile = model.profile { Section("Identity") { LabeledContent("Name", value: profile.name) LabeledContent("Handle", value: profile.handle) LabeledContent("Organization", value: profile.organization) } Section("Recovery") { Text(profile.recoverySummary) .font(.body) } } if let session = model.session { Section("Session") { LabeledContent("Device", value: session.deviceName) LabeledContent("Linked", value: session.pairedAt.formatted(date: .abbreviated, time: .shortened)) LabeledContent("Origin", value: session.originHost) LabeledContent("Transport", value: session.pairingTransport.title) } Section("Pairing payload") { Text(session.pairingCode) .font(.footnote.monospaced()) .textSelection(.enabled) } } } .navigationTitle("Identity") } } struct SettingsView: View { @ObservedObject var model: AppViewModel var body: some View { Form { Section("Alerts") { LabeledContent("Notifications", value: model.notificationPermission.title) Button("Enable Notifications") { Task { await model.requestNotificationAccess() } } .buttonStyle(SecondaryActionStyle()) Button("Send Test Notification") { Task { await model.sendTestNotification() } } .buttonStyle(SecondaryActionStyle()) } Section("Demo") { Button("Simulate Incoming Request") { Task { await model.simulateIncomingRequest() } } .buttonStyle(PrimaryActionStyle()) Button("Refresh") { Task { await model.refreshDashboard() } } .buttonStyle(SecondaryActionStyle()) } } .navigationTitle("Settings") } } enum PreviewFixtures { static let profile = MemberProfile( name: "Jurgen Meyer", handle: "@jurgen", organization: "idp.global", deviceCount: 4, recoverySummary: "Recovery kit healthy with 2 of 3 backup paths verified." ) static let session = AuthSession( deviceName: "iPhone 17 Pro", originHost: "github.com", pairedAt: .now.addingTimeInterval(-60 * 90), tokenPreview: "berlin", pairingCode: "idp.global://pair?token=swiftapp-demo-berlin&origin=github.com&device=iPhone%2017%20Pro", pairingTransport: .preview ) static let requests: [ApprovalRequest] = [ ApprovalRequest( title: "Prove identity for GitHub", subtitle: "GitHub is asking for a routine sign-in confirmation.", source: "github.com", createdAt: .now.addingTimeInterval(-60 * 4), kind: .signIn, risk: .routine, scopes: ["email", "profile", "session:read"], status: .pending ), ApprovalRequest( title: "Prove identity for workspace", subtitle: "Your secure workspace needs a stronger proof before unlocking.", source: "workspace.idp.global", createdAt: .now.addingTimeInterval(-60 * 42), kind: .elevatedAction, risk: .elevated, scopes: ["profile", "device", "location"], status: .pending ), ApprovalRequest( title: "CLI session", subtitle: "A CLI login was completed earlier today.", source: "cli.idp.global", createdAt: .now.addingTimeInterval(-60 * 120), kind: .signIn, risk: .routine, scopes: ["profile"], status: .approved ) ] static let notifications: [AppNotification] = [ AppNotification( title: "GitHub sign-in approved", message: "Your latest sign-in request for github.com was approved.", sentAt: .now.addingTimeInterval(-60 * 9), kind: .approval, isUnread: true ), AppNotification( title: "Recovery check passed", message: "Backup recovery channels were verified in the last 24 hours.", sentAt: .now.addingTimeInterval(-60 * 110), kind: .system, isUnread: false ), AppNotification( title: "Session expired", message: "A pending workstation approval expired before it could be completed.", sentAt: .now.addingTimeInterval(-60 * 1_500), kind: .security, isUnread: false ) ] @MainActor static func model() -> AppViewModel { let model = AppViewModel( service: MockIDPService.shared, notificationCoordinator: PreviewNotificationCoordinator(), appStateStore: PreviewStateStore(), launchArguments: [] ) model.session = session model.profile = profile model.requests = requests model.notifications = notifications model.selectedSection = .inbox model.manualPairingPayload = session.pairingCode model.suggestedPairingPayload = session.pairingCode model.notificationPermission = .allowed return model } } private struct PreviewNotificationCoordinator: NotificationCoordinating { func authorizationStatus() async -> NotificationPermissionState { .allowed } func requestAuthorization() async throws -> NotificationPermissionState { .allowed } func scheduleTestNotification(title: String, body: String) async throws {} } private struct PreviewStateStore: AppStateStoring { func load() -> PersistedAppState? { nil } func save(_ state: PersistedAppState) {} func clear() {} } #Preview("Inbox Light") { NavigationStack { InboxPreviewHost() } } #Preview("Inbox Dark") { NavigationStack { InboxPreviewHost() } .preferredColorScheme(.dark) } @MainActor private struct InboxPreviewHost: View { @State private var selectedRequestID = PreviewFixtures.requests.first?.id @State private var searchText = "" @State private var isSearchPresented = false @State private var model = PreviewFixtures.model() var body: some View { InboxListView( model: model, selectedRequestID: $selectedRequestID, searchText: $searchText, isSearchPresented: $isSearchPresented ) } }