2026-04-17 22:08:27 +02:00
import SwiftUI
struct LoginRootView : View {
@ ObservedObject var model : AppViewModel
2026-04-19 16:29:13 +02:00
#if ! os ( macOS )
@ State private var isNFCSheetPresented = false
#endif
2026-04-17 22:08:27 +02:00
var body : some View {
2026-04-19 16:29:13 +02:00
#if os ( macOS )
MacPairingView ( model : model )
#else
NavigationStack {
2026-04-20 18:40:32 +00:00
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
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
}
}
2026-04-20 18:40:32 +00:00
}
}
. sheet ( isPresented : $ isNFCSheetPresented ) {
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 )
}
}
#endif
}
}
2026-04-19 16:29:13 +02:00
2026-04-20 18:40:32 +00:00
#if ! os ( macOS )
private struct PairingWelcomeView : View {
@ ObservedObject var model : AppViewModel
let onScanRequested : ( ) -> Void
let onNFCRequested : ( ) -> Void
2026-04-19 16:29:13 +02:00
2026-04-20 18:40:32 +00:00
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 )
2026-04-19 16:29:13 +02:00
}
2026-04-20 18:40:32 +00:00
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 )
2026-04-19 16:29:13 +02:00
}
2026-04-20 18:40:32 +00:00
}
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. "
)
2026-04-19 16:29:13 +02:00
}
2026-04-18 01:05:22 +02:00
}
2026-04-20 18:40:32 +00:00
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 " )
2026-04-19 16:29:13 +02:00
}
2026-04-20 18:40:32 +00:00
Text ( model . isAuthenticating ? " Linking device... " : " Scan pairing QR " )
2026-04-19 16:29:13 +02:00
}
}
2026-04-20 18:40:32 +00:00
. buttonStyle ( PrimaryActionStyle ( ) )
. disabled ( model . isAuthenticating )
Button ( action : onNFCRequested ) {
Label ( " Use NFC tag instead " , systemImage : " wave.3.right " )
}
. buttonStyle ( SecondaryActionStyle ( ) )
. disabled ( model . isAuthenticating )
2026-04-17 22:08:27 +02:00
}
2026-04-20 18:40:32 +00:00
. padding ( . horizontal , 16 )
. padding ( . top , 10 )
. padding ( . bottom , 14 )
. background ( . regularMaterial )
2026-04-17 22:08:27 +02:00
}
2026-04-20 18:40:32 +00:00
}
}
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 )
2026-04-19 16:29:13 +02:00
}
2026-04-18 01:05:22 +02:00
}
2026-04-17 22:08:27 +02:00
}
}
2026-04-20 18:40:32 +00:00
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
2026-04-19 16:29:13 +02:00
#if os ( macOS )
private struct MacPairingView : View {
2026-04-17 22:08:27 +02:00
@ ObservedObject var model : AppViewModel
var body : some View {
2026-04-19 16:29:13 +02:00
VStack ( alignment : . leading , spacing : 18 ) {
HStack ( spacing : 12 ) {
Image ( systemName : " shield.lefthalf.filled " )
. font ( . title2 )
. foregroundStyle ( IdP . tint )
VStack ( alignment : . leading , spacing : 2 ) {
Text ( " Set up idp.global " )
. font ( . headline )
Text ( " Use the demo payload or paste a pairing link. " )
. font ( . subheadline )
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-19 16:29:13 +02:00
TextEditor ( text : $ model . manualPairingPayload )
. font ( . footnote . monospaced ( ) )
. scrollContentBackground ( . hidden )
. frame ( minHeight : 140 )
. padding ( 10 )
. background ( Color . idpSecondaryGroupedBackground , in : RoundedRectangle ( cornerRadius : IdP . cardRadius , style : . continuous ) )
2026-04-17 22:08:27 +02:00
2026-04-19 16:29:13 +02:00
VStack ( spacing : 10 ) {
Button ( " Use demo payload " ) {
Task {
await model . signInWithSuggestedPayload ( )
2026-04-17 22:08:27 +02:00
}
}
2026-04-19 16:29:13 +02:00
. buttonStyle ( PrimaryActionStyle ( ) )
2026-04-18 01:05:22 +02:00
2026-04-19 16:29:13 +02:00
Button ( " Link with payload " ) {
Task {
await model . signInWithManualPayload ( )
}
}
. buttonStyle ( SecondaryActionStyle ( ) )
2026-04-17 22:08:27 +02:00
}
}
2026-04-19 16:29:13 +02:00
. padding ( 20 )
2026-04-17 22:08:27 +02:00
}
}
2026-04-19 16:29:13 +02:00
#endif