Refocus app around identity proof flows

This commit is contained in:
2026-04-18 01:05:22 +02:00
parent d195037eb6
commit ea6b45388f
45 changed files with 2784 additions and 3159 deletions
+102 -28
View File
@@ -3,8 +3,8 @@ import Foundation
@MainActor
final class AppViewModel: ObservableObject {
@Published var suggestedQRCodePayload = ""
@Published var manualQRCodePayload = ""
@Published var suggestedPairingPayload = ""
@Published var manualPairingPayload = ""
@Published var session: AuthSession?
@Published var profile: MemberProfile?
@Published var requests: [ApprovalRequest] = []
@@ -13,11 +13,11 @@ final class AppViewModel: ObservableObject {
@Published var selectedSection: AppSection = .overview
@Published var isBootstrapping = false
@Published var isAuthenticating = false
@Published var isIdentifying = false
@Published var isRefreshing = false
@Published var isNotificationCenterPresented = false
@Published var activeRequestID: ApprovalRequest.ID?
@Published var isScannerPresented = false
@Published var bannerMessage: String?
@Published var errorMessage: String?
private var hasBootstrapped = false
@@ -84,13 +84,13 @@ final class AppViewModel: ObservableObject {
do {
let bootstrap = try await service.bootstrap()
suggestedQRCodePayload = bootstrap.suggestedQRCodePayload
manualQRCodePayload = bootstrap.suggestedQRCodePayload
suggestedPairingPayload = bootstrap.suggestedPairingPayload
manualPairingPayload = bootstrap.suggestedPairingPayload
notificationPermission = await notificationCoordinator.authorizationStatus()
if launchArguments.contains("--mock-auto-pair"),
session == nil {
await signIn(with: bootstrap.suggestedQRCodePayload)
await signIn(with: bootstrap.suggestedPairingPayload, transport: .preview)
if let preferredLaunchSection {
selectedSection = preferredLaunchSection
@@ -101,32 +101,52 @@ final class AppViewModel: ObservableObject {
}
}
func signInWithManualCode() async {
await signIn(with: manualQRCodePayload)
func signInWithManualPayload() async {
await signIn(with: manualPairingPayload, transport: .manual)
}
func signInWithSuggestedCode() async {
manualQRCodePayload = suggestedQRCodePayload
await signIn(with: suggestedQRCodePayload)
func signInWithSuggestedPayload() async {
manualPairingPayload = suggestedPairingPayload
await signIn(with: suggestedPairingPayload, transport: .preview)
}
func signIn(with payload: String) async {
let trimmed = payload.trimmingCharacters(in: .whitespacesAndNewlines)
func signIn(
with payload: String,
transport: PairingTransport = .manual,
signedGPSPosition: SignedGPSPosition? = nil
) async {
await signIn(
with: PairingAuthenticationRequest(
pairingPayload: payload,
transport: transport,
signedGPSPosition: signedGPSPosition
)
)
}
func signIn(with request: PairingAuthenticationRequest) async {
let trimmed = request.pairingPayload.trimmingCharacters(in: .whitespacesAndNewlines)
guard !trimmed.isEmpty else {
errorMessage = "Paste or scan a QR payload first."
errorMessage = "Paste or scan a pairing payload first."
return
}
let normalizedRequest = PairingAuthenticationRequest(
pairingPayload: trimmed,
transport: request.transport,
signedGPSPosition: request.signedGPSPosition
)
isAuthenticating = true
defer { isAuthenticating = false }
do {
let result = try await service.signIn(withQRCode: trimmed)
let result = try await service.signIn(with: normalizedRequest)
session = result.session
apply(snapshot: result.snapshot)
notificationPermission = await notificationCoordinator.authorizationStatus()
selectedSection = .overview
bannerMessage = "Paired with \(result.session.deviceName)."
errorMessage = nil
isScannerPresented = false
} catch let error as AppError {
errorMessage = error.errorDescription
@@ -135,6 +155,60 @@ final class AppViewModel: ObservableObject {
}
}
func identifyWithNFC(_ request: PairingAuthenticationRequest) async {
guard session != nil else {
errorMessage = "Set up this passport before proving your identity with NFC."
return
}
await submitIdentityProof(
payload: request.pairingPayload,
transport: .nfc,
signedGPSPosition: request.signedGPSPosition
)
}
func identifyWithPayload(_ payload: String, transport: PairingTransport = .qr) async {
guard session != nil else {
errorMessage = "Set up this passport before proving your identity."
return
}
await submitIdentityProof(payload: payload, transport: transport)
}
private func submitIdentityProof(
payload: String,
transport: PairingTransport,
signedGPSPosition: SignedGPSPosition? = nil
) async {
let trimmed = payload.trimmingCharacters(in: .whitespacesAndNewlines)
guard !trimmed.isEmpty else {
errorMessage = "The provided idp.global payload was empty."
return
}
let normalizedRequest = PairingAuthenticationRequest(
pairingPayload: trimmed,
transport: transport,
signedGPSPosition: signedGPSPosition
)
isIdentifying = true
defer { isIdentifying = false }
do {
let snapshot = try await service.identify(with: normalizedRequest)
apply(snapshot: snapshot)
errorMessage = nil
isScannerPresented = false
} catch let error as AppError {
errorMessage = error.errorDescription
} catch {
errorMessage = "Unable to complete identity proof."
}
}
func refreshDashboard() async {
guard session != nil else { return }
@@ -144,6 +218,7 @@ final class AppViewModel: ObservableObject {
do {
let snapshot = try await service.refreshDashboard()
apply(snapshot: snapshot)
errorMessage = nil
} catch {
errorMessage = "Unable to refresh the dashboard."
}
@@ -164,18 +239,16 @@ final class AppViewModel: ObservableObject {
let snapshot = try await service.simulateIncomingRequest()
apply(snapshot: snapshot)
selectedSection = .requests
bannerMessage = "A new mock approval request arrived."
errorMessage = nil
} catch {
errorMessage = "Unable to seed a new request right now."
errorMessage = "Unable to create a mock identity check right now."
}
}
func requestNotificationAccess() async {
do {
notificationPermission = try await notificationCoordinator.requestAuthorization()
if notificationPermission == .allowed || notificationPermission == .provisional {
bannerMessage = "Notifications are ready on this device."
}
errorMessage = nil
} catch {
errorMessage = "Unable to update notification permission."
}
@@ -184,11 +257,11 @@ final class AppViewModel: ObservableObject {
func sendTestNotification() async {
do {
try await notificationCoordinator.scheduleTestNotification(
title: "idp.global approval pending",
body: "A mock request is waiting for approval in the app."
title: "idp.global identity proof requested",
body: "A mock identity proof request is waiting in the app."
)
bannerMessage = "A local test notification will appear in a few seconds."
notificationPermission = await notificationCoordinator.authorizationStatus()
errorMessage = nil
} catch {
errorMessage = "Unable to schedule a test notification."
}
@@ -198,6 +271,7 @@ final class AppViewModel: ObservableObject {
do {
let snapshot = try await service.markNotificationRead(id: notification.id)
apply(snapshot: snapshot)
errorMessage = nil
} catch {
errorMessage = "Unable to update the notification."
}
@@ -209,8 +283,8 @@ final class AppViewModel: ObservableObject {
requests = []
notifications = []
selectedSection = .overview
bannerMessage = nil
manualQRCodePayload = suggestedQRCodePayload
manualPairingPayload = suggestedPairingPayload
errorMessage = nil
}
private func mutateRequest(_ request: ApprovalRequest, approve: Bool) async {
@@ -224,9 +298,9 @@ final class AppViewModel: ObservableObject {
? try await service.approveRequest(id: request.id)
: try await service.rejectRequest(id: request.id)
apply(snapshot: snapshot)
bannerMessage = approve ? "Request approved for \(request.source)." : "Request rejected for \(request.source)."
errorMessage = nil
} catch {
errorMessage = "Unable to update the request."
errorMessage = "Unable to update the identity check."
}
}