a6939453f8
CI / test (push) Has been cancelled
Move the app payload under swift/ while keeping git, package.json, and .smartconfig.json at the repo root. This standardizes the Swift app setup so build, test, run, and watch workflows match the other repos.
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
|
|
}
|
|
}
|