This commit is contained in:
@@ -12,64 +12,34 @@ struct LoginRootView: View {
|
||||
MacPairingView(model: model)
|
||||
#else
|
||||
NavigationStack {
|
||||
ZStack(alignment: .top) {
|
||||
LiveQRScannerView { payload in
|
||||
PairingWelcomeView(
|
||||
model: model,
|
||||
onScanRequested: {
|
||||
model.isScannerPresented = true
|
||||
},
|
||||
onNFCRequested: {
|
||||
isNFCSheetPresented = true
|
||||
}
|
||||
)
|
||||
.fullScreenCover(isPresented: $model.isScannerPresented) {
|
||||
QRScannerSheet(
|
||||
title: "Scan pairing QR",
|
||||
description: "Open pairing in your idp.global web session, then scan the QR shown there to link this iPhone.",
|
||||
navigationTitle: "Link Passport"
|
||||
) { payload in
|
||||
model.manualPairingPayload = payload
|
||||
Task {
|
||||
await model.signIn(with: payload, transport: .qr)
|
||||
}
|
||||
}
|
||||
.ignoresSafeArea()
|
||||
|
||||
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
|
||||
NFCSheet(
|
||||
title: "Hold near pairing tag",
|
||||
message: "Use a supported idp.global NFC tag to link this iPhone and attach a signed location proof.",
|
||||
actionTitle: "Link device"
|
||||
) { request in
|
||||
await model.signIn(with: request)
|
||||
}
|
||||
}
|
||||
@@ -77,6 +47,163 @@ struct LoginRootView: View {
|
||||
}
|
||||
}
|
||||
|
||||
#if !os(macOS)
|
||||
private struct PairingWelcomeView: View {
|
||||
@ObservedObject var model: AppViewModel
|
||||
let onScanRequested: () -> Void
|
||||
let onNFCRequested: () -> Void
|
||||
|
||||
var body: some View {
|
||||
AppScrollScreen(compactLayout: true, bottomPadding: 150) {
|
||||
AppPanel(compactLayout: true, radius: 30) {
|
||||
ShadcnBadge(
|
||||
title: "Passport setup",
|
||||
tone: .accent,
|
||||
leading: Image(systemName: "shield.lefthalf.filled")
|
||||
)
|
||||
|
||||
HStack(alignment: .top, spacing: 16) {
|
||||
ZStack {
|
||||
RoundedRectangle(cornerRadius: 20, style: .continuous)
|
||||
.fill(IdP.tint.opacity(0.14))
|
||||
.frame(width: 72, height: 72)
|
||||
|
||||
Image(systemName: "qrcode.viewfinder")
|
||||
.font(.system(size: 30, weight: .semibold))
|
||||
.foregroundStyle(IdP.tint)
|
||||
}
|
||||
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
Text("Link this iPhone to your idp.global account")
|
||||
.font(.title2.weight(.bold))
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
|
||||
Text("Start pairing in your browser, then scan the fresh QR on the next screen. This device becomes your passport for approvals, alerts, and identity proofs.")
|
||||
.font(.subheadline)
|
||||
.foregroundStyle(Color.idpMutedForeground)
|
||||
}
|
||||
}
|
||||
|
||||
Text("No camera? The scanner screen also lets you paste a pairing link manually.")
|
||||
.font(.footnote)
|
||||
.foregroundStyle(Color.idpMutedForeground)
|
||||
}
|
||||
|
||||
AppSectionCard(
|
||||
title: "How pairing works",
|
||||
subtitle: "The QR is created by the web session you want to trust.",
|
||||
compactLayout: true
|
||||
) {
|
||||
VStack(spacing: 14) {
|
||||
PairingStepRow(
|
||||
number: 1,
|
||||
title: "Start from the web",
|
||||
message: "Open idp.global in your browser and begin a new device pairing flow."
|
||||
)
|
||||
|
||||
PairingStepRow(
|
||||
number: 2,
|
||||
title: "Scan the QR",
|
||||
message: "Use the camera on the next screen to scan the pairing QR shown by that browser session."
|
||||
)
|
||||
|
||||
PairingStepRow(
|
||||
number: 3,
|
||||
title: "Approve future sign-ins",
|
||||
message: "Once linked, this iPhone can receive approval requests and security alerts."
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
AppSectionCard(
|
||||
title: "Before you scan",
|
||||
subtitle: "A few quick checks help the link complete cleanly.",
|
||||
compactLayout: true
|
||||
) {
|
||||
VStack(alignment: .leading, spacing: 10) {
|
||||
PairingNoteRow(icon: "safari", text: "Keep the pairing page open in your browser until this phone confirms the link.")
|
||||
PairingNoteRow(icon: "camera.fill", text: "Grant camera access when prompted so the QR can be read directly.")
|
||||
PairingNoteRow(icon: "wave.3.right", text: "If your organization uses NFC tags, you can link with NFC instead of QR.")
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationTitle("Welcome")
|
||||
.idpInlineNavigationTitle()
|
||||
.safeAreaInset(edge: .bottom) {
|
||||
VStack(spacing: 10) {
|
||||
Button(action: onScanRequested) {
|
||||
HStack(spacing: 10) {
|
||||
if model.isAuthenticating {
|
||||
ProgressView()
|
||||
.progressViewStyle(.circular)
|
||||
.tint(Color.idpPrimaryForeground)
|
||||
} else {
|
||||
Image(systemName: "camera.viewfinder")
|
||||
}
|
||||
|
||||
Text(model.isAuthenticating ? "Linking device..." : "Scan pairing QR")
|
||||
}
|
||||
}
|
||||
.buttonStyle(PrimaryActionStyle())
|
||||
.disabled(model.isAuthenticating)
|
||||
|
||||
Button(action: onNFCRequested) {
|
||||
Label("Use NFC tag instead", systemImage: "wave.3.right")
|
||||
}
|
||||
.buttonStyle(SecondaryActionStyle())
|
||||
.disabled(model.isAuthenticating)
|
||||
}
|
||||
.padding(.horizontal, 16)
|
||||
.padding(.top, 10)
|
||||
.padding(.bottom, 14)
|
||||
.background(.regularMaterial)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private struct PairingStepRow: View {
|
||||
let number: Int
|
||||
let title: String
|
||||
let message: String
|
||||
|
||||
var body: some View {
|
||||
HStack(alignment: .top, spacing: 12) {
|
||||
Text("\(number)")
|
||||
.font(.subheadline.weight(.bold))
|
||||
.frame(width: 28, height: 28)
|
||||
.foregroundStyle(IdP.tint)
|
||||
.background(IdP.tint.opacity(0.12), in: Circle())
|
||||
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text(title)
|
||||
.font(.subheadline.weight(.semibold))
|
||||
Text(message)
|
||||
.font(.footnote)
|
||||
.foregroundStyle(Color.idpMutedForeground)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private struct PairingNoteRow: View {
|
||||
let icon: String
|
||||
let text: String
|
||||
|
||||
var body: some View {
|
||||
HStack(alignment: .top, spacing: 10) {
|
||||
Image(systemName: icon)
|
||||
.foregroundStyle(IdP.tint)
|
||||
.frame(width: 18, height: 18)
|
||||
|
||||
Text(text)
|
||||
.font(.footnote)
|
||||
.foregroundStyle(Color.idpMutedForeground)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
#if os(macOS)
|
||||
private struct MacPairingView: View {
|
||||
@ObservedObject var model: AppViewModel
|
||||
|
||||
Reference in New Issue
Block a user