2026-04-17 22:08:27 +02:00
|
|
|
import SwiftUI
|
|
|
|
|
|
2026-04-18 01:05:22 +02:00
|
|
|
private let loginAccent = AppTheme.accent
|
2026-04-17 22:08:27 +02:00
|
|
|
|
|
|
|
|
struct LoginRootView: View {
|
|
|
|
|
@ObservedObject var model: AppViewModel
|
|
|
|
|
@Environment(\.horizontalSizeClass) private var horizontalSizeClass
|
|
|
|
|
|
|
|
|
|
var body: some View {
|
2026-04-18 01:05:22 +02:00
|
|
|
AppScrollScreen(compactLayout: compactLayout) {
|
|
|
|
|
LoginHeroPanel(model: model, compactLayout: compactLayout)
|
|
|
|
|
PairingConsoleCard(model: model, compactLayout: compactLayout)
|
2026-04-17 22:08:27 +02:00
|
|
|
}
|
|
|
|
|
.sheet(isPresented: $model.isScannerPresented) {
|
|
|
|
|
QRScannerSheet(
|
2026-04-18 01:05:22 +02:00
|
|
|
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",
|
2026-04-17 22:08:27 +02:00
|
|
|
onCodeScanned: { payload in
|
2026-04-18 01:05:22 +02:00
|
|
|
model.manualPairingPayload = payload
|
2026-04-17 22:08:27 +02:00
|
|
|
Task {
|
2026-04-18 01:05:22 +02:00
|
|
|
await model.signIn(with: payload, transport: .qr)
|
2026-04-17 22:08:27 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private var compactLayout: Bool {
|
|
|
|
|
#if os(iOS)
|
|
|
|
|
horizontalSizeClass == .compact
|
|
|
|
|
#else
|
|
|
|
|
false
|
|
|
|
|
#endif
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private struct LoginHeroPanel: View {
|
|
|
|
|
@ObservedObject var model: AppViewModel
|
|
|
|
|
let compactLayout: Bool
|
|
|
|
|
|
|
|
|
|
var body: some View {
|
2026-04-18 01:05:22 +02:00
|
|
|
AppPanel(compactLayout: compactLayout, radius: AppLayout.largeCardRadius) {
|
|
|
|
|
AppBadge(title: "Secure passport setup", tone: loginAccent)
|
2026-04-17 22:08:27 +02:00
|
|
|
|
2026-04-18 01:05:22 +02:00
|
|
|
Text("Turn this device into a passport for your idp.global identity")
|
|
|
|
|
.font(.system(size: compactLayout ? 28 : 36, weight: .bold, design: .rounded))
|
|
|
|
|
.lineLimit(3)
|
2026-04-17 22:08:27 +02:00
|
|
|
|
2026-04-18 01:05:22 +02:00
|
|
|
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)
|
2026-04-17 22:08:27 +02:00
|
|
|
|
2026-04-18 01:05:22 +02:00
|
|
|
Divider()
|
2026-04-17 22:08:27 +02:00
|
|
|
|
2026-04-18 01:05:22 +02:00
|
|
|
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)
|
2026-04-17 22:08:27 +02:00
|
|
|
}
|
|
|
|
|
}
|
2026-04-18 01:05:22 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
}
|
2026-04-17 22:08:27 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private struct PairingConsoleCard: View {
|
|
|
|
|
@ObservedObject var model: AppViewModel
|
|
|
|
|
let compactLayout: Bool
|
|
|
|
|
|
|
|
|
|
var body: some View {
|
2026-04-18 01:05:22 +02:00
|
|
|
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...")
|
2026-04-17 22:08:27 +02:00
|
|
|
.foregroundStyle(.secondary)
|
|
|
|
|
}
|
2026-04-18 01:05:22 +02:00
|
|
|
}
|
2026-04-17 22:08:27 +02:00
|
|
|
|
2026-04-18 01:05:22 +02:00
|
|
|
Text("NFC, QR, and OTP proof methods become available after this passport is active.")
|
|
|
|
|
.font(.footnote)
|
|
|
|
|
.foregroundStyle(.secondary)
|
2026-04-17 22:08:27 +02:00
|
|
|
|
2026-04-18 01:05:22 +02:00
|
|
|
if compactLayout {
|
|
|
|
|
VStack(spacing: 12) {
|
|
|
|
|
primaryButtons
|
|
|
|
|
secondaryButtons
|
2026-04-17 22:08:27 +02:00
|
|
|
}
|
2026-04-18 01:05:22 +02:00
|
|
|
} else {
|
|
|
|
|
VStack(spacing: 12) {
|
|
|
|
|
HStack(spacing: 12) {
|
|
|
|
|
primaryButtons
|
2026-04-17 22:08:27 +02:00
|
|
|
}
|
2026-04-18 01:05:22 +02:00
|
|
|
|
|
|
|
|
secondaryButtons
|
2026-04-17 22:08:27 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@ViewBuilder
|
|
|
|
|
private var primaryButtons: some View {
|
|
|
|
|
Button {
|
|
|
|
|
model.isScannerPresented = true
|
|
|
|
|
} label: {
|
2026-04-18 01:05:22 +02:00
|
|
|
Label("Scan QR", systemImage: "qrcode.viewfinder")
|
|
|
|
|
.frame(maxWidth: .infinity)
|
2026-04-17 22:08:27 +02:00
|
|
|
}
|
|
|
|
|
.buttonStyle(.borderedProminent)
|
2026-04-18 01:05:22 +02:00
|
|
|
.controlSize(.large)
|
|
|
|
|
}
|
2026-04-17 22:08:27 +02:00
|
|
|
|
2026-04-18 01:05:22 +02:00
|
|
|
@ViewBuilder
|
|
|
|
|
private var secondaryButtons: some View {
|
|
|
|
|
if compactLayout {
|
|
|
|
|
VStack(spacing: 12) {
|
|
|
|
|
usePayloadButton
|
|
|
|
|
previewPayloadButton
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
HStack(spacing: 12) {
|
|
|
|
|
usePayloadButton
|
|
|
|
|
previewPayloadButton
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private var usePayloadButton: some View {
|
2026-04-17 22:08:27 +02:00
|
|
|
Button {
|
|
|
|
|
Task {
|
2026-04-18 01:05:22 +02:00
|
|
|
await model.signInWithManualPayload()
|
2026-04-17 22:08:27 +02:00
|
|
|
}
|
|
|
|
|
} label: {
|
|
|
|
|
if model.isAuthenticating {
|
|
|
|
|
ProgressView()
|
2026-04-18 01:05:22 +02:00
|
|
|
.frame(maxWidth: .infinity)
|
2026-04-17 22:08:27 +02:00
|
|
|
} else {
|
2026-04-18 01:05:22 +02:00
|
|
|
Label("Link with payload", systemImage: "arrow.right.circle")
|
|
|
|
|
.frame(maxWidth: .infinity)
|
2026-04-17 22:08:27 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
.buttonStyle(.bordered)
|
2026-04-18 01:05:22 +02:00
|
|
|
.controlSize(.large)
|
2026-04-17 22:08:27 +02:00
|
|
|
.disabled(model.isAuthenticating)
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-18 01:05:22 +02:00
|
|
|
private var previewPayloadButton: some View {
|
2026-04-17 22:08:27 +02:00
|
|
|
Button {
|
|
|
|
|
Task {
|
2026-04-18 01:05:22 +02:00
|
|
|
await model.signInWithSuggestedPayload()
|
2026-04-17 22:08:27 +02:00
|
|
|
}
|
|
|
|
|
} label: {
|
2026-04-18 01:05:22 +02:00
|
|
|
Label("Use preview passport", systemImage: "wand.and.stars")
|
|
|
|
|
.frame(maxWidth: .infinity)
|
2026-04-17 22:08:27 +02:00
|
|
|
}
|
|
|
|
|
.buttonStyle(.bordered)
|
2026-04-18 01:05:22 +02:00
|
|
|
.controlSize(.large)
|
2026-04-17 22:08:27 +02:00
|
|
|
}
|
|
|
|
|
}
|