import SwiftUI extension ApprovalRequest { var appDisplayName: String { source .replacingOccurrences(of: "auth.", with: "") .replacingOccurrences(of: ".idp.global", with: ".idp.global") } var inboxTitle: String { "Sign in to \(appDisplayName)" } var locationSummary: String { "Berlin, DE" } var deviceSummary: String { switch kind { case .signIn: "Safari on Berlin iPhone" case .accessGrant: "Chrome on iPad Pro" case .elevatedAction: "Berlin MacBook Pro" } } var networkSummary: String { switch kind { case .signIn: "Home Wi-Fi" case .accessGrant: "Shared office Wi-Fi" case .elevatedAction: "Ethernet" } } var ipSummary: String { risk == .elevated ? "84.187.12.44" : "84.187.12.36" } var trustColor: Color { switch (status, risk) { case (.rejected, _): .red case (.approved, _), (_, .routine): .green case (.pending, .elevated): .yellow } } var trustExplanation: String { switch (status, risk) { case (.approved, _): "This proof came from a signed device session that matches your usual sign-in pattern." case (.rejected, _): "This request was denied, so no data will be shared unless a new sign-in is started." case (.pending, .routine): "The origin and device pattern look familiar for this account." case (.pending, .elevated): "The request is valid, but it is asking for a stronger proof than usual." } } var expiresAt: Date { createdAt.addingTimeInterval(risk == .elevated ? 180 : 300) } } private enum NotificationPresentationStatus { case approved case denied case expired var title: String { switch self { case .approved: "Approved" case .denied: "Denied" case .expired: "Expired" } } var color: Color { switch self { case .approved: .green case .denied: .red case .expired: .secondary } } } extension AppNotification { fileprivate var presentationStatus: NotificationPresentationStatus { let haystack = "\(title) \(message)".lowercased() if haystack.contains("declined") || haystack.contains("denied") { return .denied } if haystack.contains("expired") || haystack.contains("quiet hours") { return .expired } return .approved } } struct StatusPill: View { let title: String let color: Color var body: some View { Text(title) .font(.caption.weight(.semibold)) .padding(.horizontal, 10) .padding(.vertical, 5) .background(color.opacity(0.12), in: Capsule(style: .continuous)) .foregroundStyle(color) } } struct TimeChip: View { let date: Date var compact = false var body: some View { Text(date, format: .dateTime.hour().minute()) .font(compact ? .caption2.weight(.medium) : .caption.weight(.medium)) .monospacedDigit() .padding(.horizontal, compact ? 8 : 10) .padding(.vertical, compact ? 4 : 6) .background(Color.idpTertiaryFill, in: Capsule(style: .continuous)) .foregroundStyle(.secondary) } } struct ApprovalRow: View { let request: ApprovalRequest let handle: String var compact = false var highlighted = false var body: some View { HStack(spacing: 12) { MonogramAvatar(title: request.appDisplayName, size: compact ? 32 : 40) VStack(alignment: .leading, spacing: 4) { Text(request.inboxTitle) .font(compact ? .subheadline.weight(.semibold) : .headline) .foregroundStyle(.primary) .lineLimit(2) Text("as \(handle) ยท \(request.locationSummary)") .font(compact ? .caption : .subheadline) .foregroundStyle(.secondary) .lineLimit(1) } Spacer(minLength: 8) HStack(spacing: 10) { TimeChip(date: request.createdAt, compact: compact) Image(systemName: "chevron.right") .font(.caption.weight(.semibold)) .foregroundStyle(.tertiary) } } .padding(.vertical, compact ? 6 : 10) .padding(.horizontal, 12) .background( RoundedRectangle(cornerRadius: 18, style: .continuous) .fill(highlighted ? IdP.tint.opacity(0.06) : Color.clear) ) .overlay( RoundedRectangle(cornerRadius: 18, style: .continuous) .stroke(highlighted ? IdP.tint : Color.clear, lineWidth: highlighted ? 1.5 : 0) ) .contentShape(Rectangle()) .accessibilityElement(children: .combine) .accessibilityLabel("\(request.inboxTitle), \(request.locationSummary), \(request.createdAt.formatted(date: .omitted, time: .shortened))") } } struct NotificationEventRow: View { let notification: AppNotification var body: some View { HStack(alignment: .top, spacing: 12) { MonogramAvatar(title: notification.title, size: 40, tint: notification.presentationStatus.color) VStack(alignment: .leading, spacing: 5) { Text(notification.title) .font(.headline) .lineLimit(2) Text(notification.message) .font(.subheadline) .foregroundStyle(.secondary) .lineLimit(2) } Spacer(minLength: 8) StatusPill(title: notification.presentationStatus.title, color: notification.presentationStatus.color) } .padding(.vertical, 8) .accessibilityElement(children: .combine) } } struct NotificationPermissionCard: View { @ObservedObject var model: AppViewModel var body: some View { VStack(alignment: .leading, spacing: 14) { Label("Allow sign-in alerts", systemImage: model.notificationPermission.systemImage) .font(.headline) Text(model.notificationPermission.summary) .font(.subheadline) .foregroundStyle(.secondary) VStack(spacing: 10) { Button("Enable Notifications") { Task { await model.requestNotificationAccess() } } .buttonStyle(PrimaryActionStyle()) Button("Send Test Alert") { Task { await model.sendTestNotification() } } .buttonStyle(SecondaryActionStyle()) } } .approvalCard() } } struct DevicePresentation: Identifiable, Hashable { let id: UUID let name: String let systemImage: String let lastSeen: Date let isCurrent: Bool let isTrusted: Bool init( id: UUID = UUID(), name: String, systemImage: String, lastSeen: Date, isCurrent: Bool, isTrusted: Bool ) { self.id = id self.name = name self.systemImage = systemImage self.lastSeen = lastSeen self.isCurrent = isCurrent self.isTrusted = isTrusted } } struct DeviceItemRow: View { let device: DevicePresentation var body: some View { HStack(spacing: 12) { Image(systemName: device.systemImage) .font(.headline) .foregroundStyle(IdP.tint) .frame(width: 28) VStack(alignment: .leading, spacing: 3) { Text(device.name) .font(.body.weight(.medium)) Text(device.isCurrent ? "This device" : "Seen \(device.lastSeen, style: .relative)") .font(.subheadline) .foregroundStyle(.secondary) } Spacer(minLength: 8) StatusDot(color: device.isTrusted ? .green : .yellow) Image(systemName: "chevron.right") .font(.caption.weight(.semibold)) .foregroundStyle(.tertiary) } .deviceRowStyle() .accessibilityElement(children: .combine) } } struct TrustSignalBanner: View { let request: ApprovalRequest var body: some View { HStack(alignment: .top, spacing: 12) { Image(systemName: symbolName) .font(.headline) .foregroundStyle(request.trustColor) VStack(alignment: .leading, spacing: 4) { Text(request.trustHeadline) .font(.subheadline.weight(.semibold)) Text(request.trustExplanation) .font(.subheadline) .foregroundStyle(.secondary) } } .padding(.vertical, 8) } private var symbolName: String { switch request.trustColor { case .green: return "checkmark.shield.fill" case .yellow: return "exclamationmark.triangle.fill" default: return "xmark.shield.fill" } } } struct EmptyPaneView: View { let title: String let message: String let systemImage: String var body: some View { ContentUnavailableView { Label(title, systemImage: systemImage) } description: { Text(message) } .frame(maxWidth: .infinity, maxHeight: .infinity) } }