import Combine
import CoreNFC
import GBGBridge
import SwiftUI
// MARK: - App Entry Point
@main
struct AdvancedApp: App {
var body: some Scene {
WindowGroup {
JourneyLauncherView()
}
}
}
// MARK: - Journey Configuration
/// Centralized configuration for the identity verification journey.
struct JourneyConfig {
let url: URL
let requiredCapabilities: [String]
let hostVersion: String
static let passportVerification = JourneyConfig(
url: URL(string: "https://journey.example.com/passport")!,
requiredCapabilities: ["camera.document", "nfc.read"],
hostVersion: "1.0.0"
)
static let documentOnly = JourneyConfig(
url: URL(string: "https://journey.example.com/document")!,
requiredCapabilities: ["camera.document"],
hostVersion: "1.0.0"
)
}
// MARK: - Journey Launcher (Pre-launch Validation)
struct JourneyLauncherView: View {
@State private var selectedJourney: JourneyConfig = .passportVerification
@State private var showJourney = false
@State private var showCapabilityWarning = false
private let deviceCapabilities = DeviceCapabilities.detect()
var body: some View {
NavigationStack {
VStack(spacing: 24) {
Text("Identity Verification")
.font(.title2)
.fontWeight(.semibold)
// Journey selection
VStack(spacing: 12) {
JourneyOptionButton(
title: "Passport Verification",
subtitle: "Document + NFC chip read",
requirements: ["camera.document", "nfc.read"],
deviceCapabilities: deviceCapabilities
) {
selectedJourney = .passportVerification
attemptLaunch()
}
JourneyOptionButton(
title: "Document Verification",
subtitle: "Document capture only",
requirements: ["camera.document"],
deviceCapabilities: deviceCapabilities
) {
selectedJourney = .documentOnly
attemptLaunch()
}
}
.padding(.horizontal)
// Device capability summary
DeviceCapabilitySummary(capabilities: deviceCapabilities)
Spacer()
}
.padding(.top, 32)
.navigationTitle("Verification")
.navigationBarTitleDisplayMode(.inline)
.fullScreenCover(isPresented: $showJourney) {
JourneyContainerView(config: selectedJourney)
}
.alert("Capability Warning", isPresented: $showCapabilityWarning) {
Button("Continue Anyway") {
showJourney = true
}
Button("Cancel", role: .cancel) {}
} message: {
let missing = missingCapabilities(for: selectedJourney)
Text("This journey requires \(missing.joined(separator: ", ")) which \(missing.count == 1 ? "is" : "are") not available on this device. The journey may not complete successfully.")
}
}
}
private func attemptLaunch() {
let missing = missingCapabilities(for: selectedJourney)
if missing.isEmpty {
showJourney = true
} else {
showCapabilityWarning = true
}
}
private func missingCapabilities(for journey: JourneyConfig) -> [String] {
journey.requiredCapabilities.filter { capability in
deviceCapabilities.isSupported(capability) != true
}
}
}
// MARK: - Device Capabilities Detection
struct DeviceCapabilities {
let nfcAvailable: Bool
let cameraResult: CameraDetector.Result
static func detect() -> DeviceCapabilities {
DeviceCapabilities(
nfcAvailable: NFCTagReaderSession.readingAvailable,
cameraResult: CameraDetector.check()
)
}
func isSupported(_ capability: String) -> Bool {
switch capability {
case "camera.document": return cameraResult.hardwareAvailable
case "nfc.read": return nfcAvailable
default: return false
}
}
}
// MARK: - Journey Container (Full Integration)
struct JourneyContainerView: View {
let config: JourneyConfig
@StateObject private var host: BridgeHost
@StateObject private var coordinator = JourneyCoordinator()
@Environment(\.dismiss) private var dismiss
@State private var journeyState: JourneyState = .loading
@State private var showDocumentCamera = false
init(config: JourneyConfig) {
self.config = config
_host = StateObject(wrappedValue: BridgeHost(hostVersion: config.hostVersion))
}
var body: some View {
NavigationStack {
ZStack {
// Journey WebView
BridgeWebView(url: config.url, host: host)
.opacity(journeyState == .active ? 1 : 0)
// Loading overlay
if journeyState == .loading {
ProgressView("Loading verification...")
}
// Error overlay
if case .error(let message) = journeyState {
JourneyErrorView(message: message) {
journeyState = .loading
// WebView will reload via URL change mechanism
}
}
// Completion overlay
if case .completed(let result) = journeyState {
JourneyCompleteView(result: result) {
dismiss()
}
}
}
.navigationTitle("Verification")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Button("Cancel") {
host.send(event: "journey.cancel", data: [
"reason": .string("user_dismissed")
])
dismiss()
}
}
}
.onAppear { setupBridge() }
.onReceive(NotificationCenter.default.publisher(for: UIApplication.willResignActiveNotification)) { _ in
host.send(event: "host.background", data: [:])
}
.onReceive(NotificationCenter.default.publisher(for: UIApplication.didBecomeActiveNotification)) { _ in
host.send(event: "host.foreground", data: [:])
}
}
}
private func setupBridge() {
// Set delegate
host.delegate = coordinator
coordinator.onStateChange = { [weak coordinator] newState in
Task { @MainActor in
journeyState = newState
}
}
let capabilities = DeviceCapabilities.detect()
// Set up typed slot for document capture
if capabilities.cameraResult.hardwareAvailable {
host.documentCapture.handler = { [weak host] request in
guard let host else { return .cancelled(reason: "Host deallocated") }
return await host.documentCapture.awaitCompletion()
}
host.documentCapture.permissionState = capabilities.cameraResult.permissionState
}
// Register NFC as a custom capability
if capabilities.nfcAvailable {
host.registerCustomCapability("nfc.read", version: "1.0") { request, responder in
guard NFCTagReaderSession.readingAvailable else {
responder.respond(
status: .unsupported,
data: nil,
error: BridgeErrorPayload(
code: "NFC_NOT_AVAILABLE",
message: "NFC reading is not available on this device.",
recoverable: false
)
)
return
}
// In a real app, start NFC reader session and read the chip
responder.respond(
status: .success,
data: [
"chipRead": .bool(true),
"dataGroups": .array([.string("DG1"), .string("DG2")])
],
error: nil
)
}
}
// Mark as active after a brief delay (allows WebView to initialize)
Task {
try? await Task.sleep(nanoseconds: 500_000_000)
await MainActor.run {
if journeyState == .loading {
journeyState = .active
}
}
}
}
}
// MARK: - Journey State
enum JourneyState: Equatable {
case loading
case active
case completed(JourneyResult)
case error(String)
static func == (lhs: JourneyState, rhs: JourneyState) -> Bool {
switch (lhs, rhs) {
case (.loading, .loading), (.active, .active): return true
case (.completed(let a), .completed(let b)): return a.status == b.status
case (.error(let a), .error(let b)): return a == b
default: return false
}
}
}
struct JourneyResult {
let status: String
let referenceId: String?
}
// MARK: - Journey Coordinator (Delegate)
@MainActor
final class JourneyCoordinator: ObservableObject, BridgeHostDelegate {
var onStateChange: ((JourneyState) -> Void)?
func bridgeHost(_ host: BridgeHost, didReceive message: BridgeMessage) {
// Monitor for journey lifecycle events
switch message.payload.action {
case "journey.started":
onStateChange?(.active)
case "journey.complete":
let status = extractString(from: message.payload.data, key: "status") ?? "unknown"
let refId = extractString(from: message.payload.data, key: "referenceId")
onStateChange?(.completed(JourneyResult(status: status, referenceId: refId)))
case "journey.error":
let errorMsg = extractString(from: message.payload.data, key: "message") ?? "An error occurred"
onStateChange?(.error(errorMsg))
default:
break
}
}
func bridgeHost(_ host: BridgeHost, unhandledRequest message: BridgeMessage) {
// Respond to unknown requests with unsupported
host.respond(
to: message.correlationId,
status: .unsupported,
error: BridgeErrorPayload(
code: "UNSUPPORTED_ACTION",
message: "Action '\(message.payload.action)' is not supported by this host",
recoverable: false
)
)
}
private func extractString(from data: [String: JSONValue]?, key: String) -> String? {
guard let value = data?[key], case .string(let str) = value else { return nil }
return str
}
}
// MARK: - Supporting Views
struct JourneyOptionButton: View {
let title: String
let subtitle: String
let requirements: [String]
let deviceCapabilities: DeviceCapabilities
let action: () -> Void
private var allRequirementsMet: Bool {
requirements.allSatisfy { deviceCapabilities.isSupported($0) }
}
var body: some View {
Button(action: action) {
HStack {
VStack(alignment: .leading, spacing: 4) {
Text(title)
.font(.headline)
.foregroundColor(.primary)
Text(subtitle)
.font(.caption)
.foregroundColor(.secondary)
}
Spacer()
if allRequirementsMet {
Image(systemName: "checkmark.circle.fill")
.foregroundColor(.green)
} else {
Image(systemName: "exclamationmark.triangle.fill")
.foregroundColor(.orange)
}
}
.padding()
.background(Color(.secondarySystemGroupedBackground))
.cornerRadius(12)
}
}
}
struct DeviceCapabilitySummary: View {
let capabilities: DeviceCapabilities
var body: some View {
VStack(alignment: .leading, spacing: 8) {
Text("Device Capabilities")
.font(.caption)
.fontWeight(.semibold)
.foregroundColor(.secondary)
HStack(spacing: 16) {
CapabilityBadge(name: "Camera", available: capabilities.cameraAvailable)
CapabilityBadge(name: "NFC", available: capabilities.nfcAvailable)
}
}
.padding(.horizontal)
}
}
struct CapabilityBadge: View {
let name: String
let available: Bool
var body: some View {
HStack(spacing: 4) {
Image(systemName: available ? "checkmark.circle.fill" : "xmark.circle.fill")
.foregroundColor(available ? .green : .red)
.font(.caption)
Text(name)
.font(.caption)
.foregroundColor(.primary)
}
.padding(.horizontal, 10)
.padding(.vertical, 4)
.background(
Capsule()
.fill(available ? Color.green.opacity(0.1) : Color.red.opacity(0.1))
)
}
}
struct JourneyErrorView: View {
let message: String
let onRetry: () -> Void
var body: some View {
VStack(spacing: 16) {
Image(systemName: "exclamationmark.triangle")
.font(.system(size: 48))
.foregroundColor(.orange)
Text("Verification Error")
.font(.title3)
.fontWeight(.semibold)
Text(message)
.multilineTextAlignment(.center)
.foregroundColor(.secondary)
Button("Retry") {
onRetry()
}
.buttonStyle(.borderedProminent)
}
.padding()
}
}
struct JourneyCompleteView: View {
let result: JourneyResult
let onDismiss: () -> Void
var body: some View {
VStack(spacing: 16) {
Image(systemName: result.status == "success" ? "checkmark.circle.fill" : "xmark.circle.fill")
.font(.system(size: 64))
.foregroundColor(result.status == "success" ? .green : .red)
Text(result.status == "success" ? "Verification Complete" : "Verification Failed")
.font(.title3)
.fontWeight(.semibold)
if let refId = result.referenceId {
Text("Reference: \(refId)")
.font(.caption)
.foregroundColor(.secondary)
}
Button("Done") {
onDismiss()
}
.buttonStyle(.borderedProminent)
}
.padding()
}
}
// Note: CameraDetector (from GBGBridge) handles AVFoundation imports internally