Overhaul native approval UX and add widget surfaces
CI / test (push) Has been cancelled

Bring the SwiftUI app in line with the Apple-native mock and keep pending approvals actionable from Live Activities and watch complications.
This commit is contained in:
2026-04-19 16:29:13 +02:00
parent a6939453f8
commit 61a0cc1f7d
63 changed files with 3496 additions and 1769 deletions
+94 -161
View File
@@ -1,193 +1,126 @@
import SwiftUI
private let loginAccent = AppTheme.accent
struct LoginRootView: View {
@ObservedObject var model: AppViewModel
@Environment(\.horizontalSizeClass) private var horizontalSizeClass
#if !os(macOS)
@State private var isNFCSheetPresented = false
#endif
var body: some View {
AppScrollScreen(compactLayout: compactLayout) {
LoginHeroPanel(model: model, compactLayout: compactLayout)
PairingConsoleCard(model: model, compactLayout: compactLayout)
}
.sheet(isPresented: $model.isScannerPresented) {
QRScannerSheet(
seededPayload: model.suggestedPairingPayload,
title: "Scan linking QR",
description: "Use the camera to scan the QR code from the web flow that activates this device as your passport.",
navigationTitle: "Scan Linking QR",
onCodeScanned: { payload in
#if os(macOS)
MacPairingView(model: model)
#else
NavigationStack {
ZStack(alignment: .top) {
LiveQRScannerView { payload in
model.manualPairingPayload = payload
Task {
await model.signIn(with: payload, transport: .qr)
}
}
)
}
}
.ignoresSafeArea()
private var compactLayout: Bool {
#if os(iOS)
horizontalSizeClass == .compact
#else
false
VStack(spacing: 0) {
IdPGlassCapsule {
VStack(alignment: .leading, spacing: 6) {
Text("Scan a pairing code")
.font(.headline)
Text("Turn this iPhone into your idp.global passport with QR or NFC.")
.font(.subheadline)
.foregroundStyle(.secondary)
}
.frame(maxWidth: .infinity, alignment: .leading)
}
.padding(.horizontal, 16)
.padding(.top, 12)
Spacer()
Button {
isNFCSheetPresented = true
} label: {
IdPGlassCapsule {
HStack(spacing: 10) {
Image(systemName: "wave.3.right")
.foregroundStyle(IdP.tint)
Text("Hold near NFC tag")
.font(.headline)
.foregroundStyle(.primary)
}
}
}
.buttonStyle(.plain)
.padding(.horizontal, 16)
.padding(.bottom, 24)
}
}
.toolbar {
ToolbarItem(placement: .topBarTrailing) {
Button("Use demo payload") {
Task {
await model.signInWithSuggestedPayload()
}
}
.font(.footnote)
.disabled(model.isAuthenticating || model.suggestedPairingPayload.isEmpty)
}
}
}
.sheet(isPresented: $isNFCSheetPresented) {
NFCSheet(actionTitle: "Approve") { request in
await model.signIn(with: request)
}
}
#endif
}
}
private struct LoginHeroPanel: View {
#if os(macOS)
private struct MacPairingView: View {
@ObservedObject var model: AppViewModel
let compactLayout: Bool
var body: some View {
AppPanel(compactLayout: compactLayout, radius: AppLayout.largeCardRadius) {
AppBadge(title: "Secure passport setup", tone: loginAccent)
VStack(alignment: .leading, spacing: 18) {
HStack(spacing: 12) {
Image(systemName: "shield.lefthalf.filled")
.font(.title2)
.foregroundStyle(IdP.tint)
Text("Turn this device into a passport for your idp.global identity")
.font(.system(size: compactLayout ? 28 : 36, weight: .bold, design: .rounded))
.lineLimit(3)
Text("Scan a linking QR code or paste a payload to activate this device as your passport for identity proofs and security alerts.")
.font(.subheadline)
.foregroundStyle(.secondary)
Divider()
VStack(alignment: .leading, spacing: 14) {
LoginFeatureRow(icon: "qrcode.viewfinder", title: "Scan a QR code from the web flow")
LoginFeatureRow(icon: "doc.text.viewfinder", title: "Paste a payload when you already have one")
LoginFeatureRow(icon: "iphone.gen3", title: "Handle identity checks and alerts here")
}
if model.isBootstrapping {
ProgressView("Preparing preview passport...")
.tint(loginAccent)
}
}
}
}
private struct LoginFeatureRow: View {
let icon: String
let title: String
var body: some View {
HStack(alignment: .center, spacing: 12) {
Image(systemName: icon)
.font(.subheadline.weight(.semibold))
.foregroundStyle(loginAccent)
.frame(width: 28, height: 28)
Text(title)
.font(.headline)
Spacer(minLength: 0)
}
}
}
private struct PairingConsoleCard: View {
@ObservedObject var model: AppViewModel
let compactLayout: Bool
var body: some View {
AppSectionCard(title: "Set up passport", compactLayout: compactLayout) {
VStack(alignment: .leading, spacing: 8) {
Text("Link payload")
.font(.subheadline.weight(.semibold))
AppTextEditorField(
text: $model.manualPairingPayload,
minHeight: compactLayout ? 132 : 150
)
}
if model.isAuthenticating {
HStack(spacing: 10) {
ProgressView()
Text("Activating this passport...")
VStack(alignment: .leading, spacing: 2) {
Text("Set up idp.global")
.font(.headline)
Text("Use the demo payload or paste a pairing link.")
.font(.subheadline)
.foregroundStyle(.secondary)
}
}
Text("NFC, QR, and OTP proof methods become available after this passport is active.")
.font(.footnote)
.foregroundStyle(.secondary)
TextEditor(text: $model.manualPairingPayload)
.font(.footnote.monospaced())
.scrollContentBackground(.hidden)
.frame(minHeight: 140)
.padding(10)
.background(Color.idpSecondaryGroupedBackground, in: RoundedRectangle(cornerRadius: IdP.cardRadius, style: .continuous))
if compactLayout {
VStack(spacing: 12) {
primaryButtons
secondaryButtons
}
} else {
VStack(spacing: 12) {
HStack(spacing: 12) {
primaryButtons
VStack(spacing: 10) {
Button("Use demo payload") {
Task {
await model.signInWithSuggestedPayload()
}
secondaryButtons
}
}
}
}
.buttonStyle(PrimaryActionStyle())
@ViewBuilder
private var primaryButtons: some View {
Button {
model.isScannerPresented = true
} label: {
Label("Scan QR", systemImage: "qrcode.viewfinder")
.frame(maxWidth: .infinity)
}
.buttonStyle(.borderedProminent)
.controlSize(.large)
}
@ViewBuilder
private var secondaryButtons: some View {
if compactLayout {
VStack(spacing: 12) {
usePayloadButton
previewPayloadButton
}
} else {
HStack(spacing: 12) {
usePayloadButton
previewPayloadButton
Button("Link with payload") {
Task {
await model.signInWithManualPayload()
}
}
.buttonStyle(SecondaryActionStyle())
}
}
}
private var usePayloadButton: some View {
Button {
Task {
await model.signInWithManualPayload()
}
} label: {
if model.isAuthenticating {
ProgressView()
.frame(maxWidth: .infinity)
} else {
Label("Link with payload", systemImage: "arrow.right.circle")
.frame(maxWidth: .infinity)
}
}
.buttonStyle(.bordered)
.controlSize(.large)
.disabled(model.isAuthenticating)
}
private var previewPayloadButton: some View {
Button {
Task {
await model.signInWithSuggestedPayload()
}
} label: {
Label("Use preview passport", systemImage: "wand.and.stars")
.frame(maxWidth: .infinity)
}
.buttonStyle(.bordered)
.controlSize(.large)
.padding(20)
}
}
#endif