This commit is contained in:
@@ -0,0 +1,188 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user