Overhaul native approval UX and add widget surfaces
Some checks failed
CI / test (push) Has been cancelled

Bring the SwiftUI app in line with the Apple-native mock and keep pending approvals actionable from Live Activities and watch complications.
This commit is contained in:
2026-04-19 16:29:13 +02:00
parent a6939453f8
commit 61a0cc1f7d
63 changed files with 3496 additions and 1769 deletions

View File

@@ -12,6 +12,8 @@ protocol IDPServicing {
}
actor MockIDPService: IDPServicing {
static let shared = MockIDPService()
private let profile = MemberProfile(
name: "Phil Kunz",
handle: "phil@idp.global",
@@ -20,15 +22,24 @@ actor MockIDPService: IDPServicing {
recoverySummary: "Recovery kit healthy with 2 of 3 backup paths verified."
)
private let appStateStore: AppStateStoring
private var requests: [ApprovalRequest] = []
private var notifications: [AppNotification] = []
init() {
requests = Self.seedRequests()
notifications = Self.seedNotifications()
init(appStateStore: AppStateStoring = UserDefaultsAppStateStore()) {
self.appStateStore = appStateStore
if let state = appStateStore.load() {
requests = state.requests.sorted { $0.createdAt > $1.createdAt }
notifications = state.notifications.sorted { $0.sentAt > $1.sentAt }
} else {
requests = Self.seedRequests()
notifications = Self.seedNotifications()
}
}
func bootstrap() async throws -> BootstrapContext {
restoreSharedState()
try await Task.sleep(for: .milliseconds(120))
return BootstrapContext(
suggestedPairingPayload: "idp.global://pair?token=swiftapp-demo-berlin&origin=code.foss.global&device=Safari%20on%20Berlin%20MBP"
@@ -36,6 +47,7 @@ actor MockIDPService: IDPServicing {
}
func signIn(with request: PairingAuthenticationRequest) async throws -> SignInResult {
restoreSharedState()
try await Task.sleep(for: .milliseconds(260))
try validateSignedGPSPosition(in: request)
@@ -51,6 +63,8 @@ actor MockIDPService: IDPServicing {
at: 0
)
persistSharedStateIfAvailable()
return SignInResult(
session: session,
snapshot: snapshot()
@@ -58,6 +72,7 @@ actor MockIDPService: IDPServicing {
}
func identify(with request: PairingAuthenticationRequest) async throws -> DashboardSnapshot {
restoreSharedState()
try await Task.sleep(for: .milliseconds(180))
try validateSignedGPSPosition(in: request)
@@ -73,15 +88,19 @@ actor MockIDPService: IDPServicing {
at: 0
)
persistSharedStateIfAvailable()
return snapshot()
}
func refreshDashboard() async throws -> DashboardSnapshot {
restoreSharedState()
try await Task.sleep(for: .milliseconds(180))
return snapshot()
}
func approveRequest(id: UUID) async throws -> DashboardSnapshot {
restoreSharedState()
try await Task.sleep(for: .milliseconds(150))
guard let index = requests.firstIndex(where: { $0.id == id }) else {
@@ -100,10 +119,13 @@ actor MockIDPService: IDPServicing {
at: 0
)
persistSharedStateIfAvailable()
return snapshot()
}
func rejectRequest(id: UUID) async throws -> DashboardSnapshot {
restoreSharedState()
try await Task.sleep(for: .milliseconds(150))
guard let index = requests.firstIndex(where: { $0.id == id }) else {
@@ -122,10 +144,13 @@ actor MockIDPService: IDPServicing {
at: 0
)
persistSharedStateIfAvailable()
return snapshot()
}
func simulateIncomingRequest() async throws -> DashboardSnapshot {
restoreSharedState()
try await Task.sleep(for: .milliseconds(120))
let syntheticRequest = ApprovalRequest(
@@ -151,10 +176,13 @@ actor MockIDPService: IDPServicing {
at: 0
)
persistSharedStateIfAvailable()
return snapshot()
}
func markNotificationRead(id: UUID) async throws -> DashboardSnapshot {
restoreSharedState()
try await Task.sleep(for: .milliseconds(80))
guard let index = notifications.firstIndex(where: { $0.id == id }) else {
@@ -162,6 +190,7 @@ actor MockIDPService: IDPServicing {
}
notifications[index].isUnread = false
persistSharedStateIfAvailable()
return snapshot()
}
@@ -227,6 +256,30 @@ actor MockIDPService: IDPServicing {
return "An identity proof was completed for \(context.deviceName) on \(context.originHost)."
}
private func restoreSharedState() {
guard let state = appStateStore.load() else {
requests = Self.seedRequests()
notifications = Self.seedNotifications()
return
}
requests = state.requests.sorted { $0.createdAt > $1.createdAt }
notifications = state.notifications.sorted { $0.sentAt > $1.sentAt }
}
private func persistSharedStateIfAvailable() {
guard let state = appStateStore.load() else { return }
appStateStore.save(
PersistedAppState(
session: state.session,
profile: state.profile,
requests: requests,
notifications: notifications
)
)
}
private static func seedRequests() -> [ApprovalRequest] {
[
ApprovalRequest(