Some checks failed
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.
194 lines
5.9 KiB
Swift
194 lines
5.9 KiB
Swift
import SwiftUI
|
|
|
|
private let loginAccent = AppTheme.accent
|
|
|
|
struct LoginRootView: View {
|
|
@ObservedObject var model: AppViewModel
|
|
@Environment(\.horizontalSizeClass) private var horizontalSizeClass
|
|
|
|
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
|
|
model.manualPairingPayload = payload
|
|
Task {
|
|
await model.signIn(with: payload, transport: .qr)
|
|
}
|
|
}
|
|
)
|
|
}
|
|
}
|
|
|
|
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 {
|
|
AppPanel(compactLayout: compactLayout, radius: AppLayout.largeCardRadius) {
|
|
AppBadge(title: "Secure passport setup", tone: loginAccent)
|
|
|
|
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...")
|
|
.foregroundStyle(.secondary)
|
|
}
|
|
}
|
|
|
|
Text("NFC, QR, and OTP proof methods become available after this passport is active.")
|
|
.font(.footnote)
|
|
.foregroundStyle(.secondary)
|
|
|
|
if compactLayout {
|
|
VStack(spacing: 12) {
|
|
primaryButtons
|
|
secondaryButtons
|
|
}
|
|
} else {
|
|
VStack(spacing: 12) {
|
|
HStack(spacing: 12) {
|
|
primaryButtons
|
|
}
|
|
|
|
secondaryButtons
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
@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
|
|
}
|
|
}
|
|
}
|
|
|
|
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)
|
|
}
|
|
}
|