189 lines
7.2 KiB
Swift
189 lines
7.2 KiB
Swift
import SwiftUI
|
|
|
|
struct RequestDetailSheet: View {
|
|
let request: ApprovalRequest
|
|
@ObservedObject var model: AppViewModel
|
|
|
|
@Environment(\.dismiss) private var dismiss
|
|
|
|
var body: some View {
|
|
NavigationStack {
|
|
AppScrollScreen(
|
|
compactLayout: true,
|
|
bottomPadding: AppLayout.compactBottomDockPadding
|
|
) {
|
|
RequestDetailHero(request: request)
|
|
|
|
AppSectionCard(title: "Summary", compactLayout: true) {
|
|
AppKeyValue(label: "Source", value: request.source)
|
|
AppKeyValue(label: "Requested", value: request.createdAt.formatted(date: .abbreviated, time: .shortened))
|
|
AppKeyValue(label: "Risk", value: request.risk.summary)
|
|
AppKeyValue(label: "Type", value: request.kind.title)
|
|
}
|
|
|
|
AppSectionCard(title: "Proof details", compactLayout: true) {
|
|
if request.scopes.isEmpty {
|
|
Text("No explicit proof details were provided by the mock backend.")
|
|
.foregroundStyle(.secondary)
|
|
} else {
|
|
Text(request.scopes.joined(separator: "\n"))
|
|
.font(.body.monospaced())
|
|
.foregroundStyle(.secondary)
|
|
}
|
|
}
|
|
|
|
AppSectionCard(title: "Guidance", compactLayout: true) {
|
|
Text(request.trustDetail)
|
|
.foregroundStyle(.secondary)
|
|
|
|
Text(request.risk.guidance)
|
|
.font(.headline)
|
|
}
|
|
|
|
if request.status == .pending {
|
|
AppSectionCard(title: "Actions", compactLayout: true) {
|
|
VStack(spacing: 12) {
|
|
Button {
|
|
Task {
|
|
await model.approve(request)
|
|
dismiss()
|
|
}
|
|
} label: {
|
|
if model.activeRequestID == request.id {
|
|
ProgressView()
|
|
} else {
|
|
Label("Verify identity", systemImage: "checkmark.circle.fill")
|
|
.frame(maxWidth: .infinity)
|
|
}
|
|
}
|
|
.buttonStyle(.borderedProminent)
|
|
.disabled(model.activeRequestID == request.id)
|
|
|
|
Button(role: .destructive) {
|
|
Task {
|
|
await model.reject(request)
|
|
dismiss()
|
|
}
|
|
} label: {
|
|
Label("Decline", systemImage: "xmark.circle.fill")
|
|
.frame(maxWidth: .infinity)
|
|
}
|
|
.buttonStyle(.bordered)
|
|
.disabled(model.activeRequestID == request.id)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
.navigationTitle("Review Proof")
|
|
.inlineNavigationTitleOnIOS()
|
|
.toolbar {
|
|
ToolbarItem(placement: .cancellationAction) {
|
|
Button("Close") {
|
|
dismiss()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private struct RequestDetailHero: View {
|
|
let request: ApprovalRequest
|
|
|
|
private var accent: Color {
|
|
switch request.status {
|
|
case .approved:
|
|
.green
|
|
case .rejected:
|
|
.red
|
|
case .pending:
|
|
request.risk == .routine ? dashboardAccent : .orange
|
|
}
|
|
}
|
|
|
|
var body: some View {
|
|
AppPanel(compactLayout: true, radius: AppLayout.largeCardRadius) {
|
|
AppBadge(title: request.kind.title, tone: accent)
|
|
|
|
Text(request.title)
|
|
.font(.system(size: 30, weight: .bold, design: .rounded))
|
|
.lineLimit(3)
|
|
|
|
Text(request.subtitle)
|
|
.foregroundStyle(.secondary)
|
|
|
|
HStack(spacing: 8) {
|
|
AppStatusTag(title: request.status.title, tone: accent)
|
|
AppStatusTag(title: request.risk.title, tone: request.risk == .routine ? dashboardAccent : .orange)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
struct OneTimePasscodeSheet: View {
|
|
let session: AuthSession
|
|
|
|
@Environment(\.dismiss) private var dismiss
|
|
@Environment(\.horizontalSizeClass) private var horizontalSizeClass
|
|
|
|
var body: some View {
|
|
NavigationStack {
|
|
TimelineView(.periodic(from: .now, by: 1)) { context in
|
|
let code = OneTimePasscodeGenerator.code(for: session.pairingCode, at: context.date)
|
|
let secondsRemaining = OneTimePasscodeGenerator.renewalCountdown(at: context.date)
|
|
|
|
AppScrollScreen(compactLayout: compactLayout) {
|
|
AppPanel(compactLayout: compactLayout, radius: AppLayout.largeCardRadius) {
|
|
AppBadge(title: "One-time passcode", tone: dashboardGold)
|
|
|
|
Text("OTP")
|
|
.font(.system(size: compactLayout ? 32 : 40, weight: .bold, design: .rounded))
|
|
|
|
Text("Share this code only with the site or device asking you to prove that it is really you.")
|
|
.font(.subheadline)
|
|
.foregroundStyle(.secondary)
|
|
|
|
Text(code)
|
|
.font(.system(size: compactLayout ? 42 : 54, weight: .bold, design: .rounded).monospacedDigit())
|
|
.tracking(compactLayout ? 4 : 6)
|
|
.frame(maxWidth: .infinity)
|
|
.padding(.vertical, compactLayout ? 16 : 20)
|
|
.background(AppTheme.mutedFill, in: RoundedRectangle(cornerRadius: 24, style: .continuous))
|
|
.overlay(
|
|
RoundedRectangle(cornerRadius: 24, style: .continuous)
|
|
.stroke(AppTheme.border, lineWidth: 1)
|
|
)
|
|
|
|
HStack(spacing: 8) {
|
|
AppStatusTag(title: "Renews in \(secondsRemaining)s", tone: dashboardGold)
|
|
AppStatusTag(title: session.originHost, tone: dashboardAccent)
|
|
}
|
|
|
|
Divider()
|
|
|
|
AppKeyValue(label: "Client", value: session.deviceName)
|
|
AppKeyValue(label: "Linked", value: session.pairedAt.formatted(date: .abbreviated, time: .shortened))
|
|
}
|
|
}
|
|
}
|
|
.navigationTitle("OTP")
|
|
.inlineNavigationTitleOnIOS()
|
|
.toolbar {
|
|
ToolbarItem(placement: .cancellationAction) {
|
|
Button("Close") {
|
|
dismiss()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private var compactLayout: Bool {
|
|
#if os(iOS)
|
|
horizontalSizeClass == .compact
|
|
#else
|
|
false
|
|
#endif
|
|
}
|
|
}
|