import SwiftUI
import PhotosUI
struct VerifyView: View {
@EnvironmentObject var proofViewModel: ProofViewModel
@EnvironmentObject var permissionService: PermissionService
@StateObject private var proofModeService = ProofModeService()
@Binding var selectedProof: Proof?
@State private var showingImagePicker = false
@State private var verificationResult: String?
@State private var showingVerificationResult = false
var body: some View {
VStack(spacing: 20) {
// Verify Files Section
VStack(alignment: .leading, spacing: 16) {
HStack {
Image(systemName: "doc.text.magnifyingglass")
.font(.title2)
.foregroundColor(.blue)
VStack(alignment: .leading) {
Text("Verify Files")
.font(.headline)
Text("Check photos for proof data")
.font(.caption)
.foregroundColor(.secondary)
}
Spacer()
}
Button(action: {
checkPhotoLibraryPermissionAndVerify()
}) {
HStack {
Image(systemName: "photo.on.rectangle")
.font(.title2)
Text("Choose Photo to Verify")
.font(.headline)
}
.frame(maxWidth: .infinity)
.padding()
.background(Color.orange)
.foregroundColor(.white)
.cornerRadius(12)
}
}
.padding()
.background(Color.gray.opacity(0.05))
.cornerRadius(12)
// Progress View for File Verification
if proofModeService.isVerifying {
VStack(spacing: 12) {
ProgressView(value: proofModeService.progress)
.progressViewStyle(LinearProgressViewStyle())
HStack {
ProgressView()
.scaleEffect(0.8)
Text(proofModeService.status.isEmpty ? "Verifying file..." : proofModeService.status)
.font(.caption)
.foregroundColor(.secondary)
}
}
.padding()
.background(Color.gray.opacity(0.1))
.cornerRadius(12)
}
Divider()
// Generated Proofs Section
VStack(alignment: .leading, spacing: 16) {
HStack {
Image(systemName: "shield.checkerboard")
.font(.title2)
.foregroundColor(.green)
VStack(alignment: .leading) {
Text("Generated Proofs")
.font(.headline)
Text("Verify proofs you've created")
.font(.caption)
.foregroundColor(.secondary)
}
Spacer()
}
// Show existing proofs
if proofViewModel.proofs.isEmpty {
VStack(spacing: 12) {
Image(systemName: "shield.slash")
.font(.system(size: 40))
.foregroundColor(.gray)
Text("No Generated Proofs")
.font(.subheadline)
.fontWeight(.medium)
Text("Generate proofs first to verify them here")
.font(.caption)
.foregroundColor(.secondary)
.multilineTextAlignment(.center)
}
.frame(maxWidth: .infinity)
.padding()
.background(Color.gray.opacity(0.05))
.cornerRadius(8)
} else {
ProofListView(selectedProof: $selectedProof)
.frame(maxHeight: 300)
}
}
.padding()
.background(Color.gray.opacity(0.05))
.cornerRadius(12)
Spacer()
}
.padding()
.sheet(isPresented: $showingImagePicker) {
ImagePicker(sourceType: .photoLibrary) { image in
verifySelectedImage(image)
}
}
.sheet(isPresented: $showingVerificationResult) {
NavigationStack {
FileVerificationResultView(result: verificationResult ?? "No result")
.navigationTitle("Verification Result")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button("Done") {
showingVerificationResult = false
}
}
}
}
}
}
private func checkPhotoLibraryPermissionAndVerify() {
permissionService.requestPhotoLibraryPermission { granted in
if granted {
showingImagePicker = true
}
}
}
private func verifySelectedImage(_ image: UIImage) {
// Create a temporary proof-like object for verification
guard let imageData = image.jpegData(compressionQuality: 1.0) else {
verificationResult = "Failed to process selected image"
showingVerificationResult = true
return
}
let tempProof = Proof(
fileName: "selected_image.jpg",
hash: proofModeService.calculateHash(for: imageData),
fileSize: Int64(imageData.count),
imageData: imageData
)
proofModeService.verifyProof(tempProof) { result in
switch result {
case .success(let output):
verificationResult = output
case .failure(let error):
verificationResult = "Verification failed: \(error.localizedDescription)"
}
showingVerificationResult = true
}
}
}
struct FileVerificationResultView: View {
let result: String
private var parsedResult: VerificationResult? {
guard let data = result.data(using: .utf8) else { return nil }
// Try to parse as JSON
if let jsonObject = try? JSONSerialization.jsonObject(with: data, options: []) {
// Check if it's an empty array
if let array = jsonObject as? [[String: Any]], array.isEmpty {
return VerificationResult(
isEmpty: true,
summary: "No verification data found",
details: []
)
}
// Parse the actual verification data
if let dict = jsonObject as? [String: Any] {
return parseVerificationDict(dict)
} else if let array = jsonObject as? [[String: Any]], let first = array.first {
return parseVerificationDict(first)
}
}
// If not JSON, return the raw text
return VerificationResult(
isEmpty: false,
summary: "Verification Complete",
details: [("Raw Result", result)]
)
}
private func parseVerificationDict(_ dict: [String: Any]) -> VerificationResult {
var details: [(String, String)] = []
var summary = "Verification Complete"
// Check for files
if let files = dict["files"] as? [[String: Any]] {
if files.isEmpty {
summary = "No proof files found"
} else {
summary = "Found \(files.count) file(s) with proof data"
for file in files {
if let name = file["name"] as? String {
var fileStatus = "✓ Verified"
if let integrity = file["integrity"] as? [String: Any],
let verified = integrity["verified"] as? Bool {
fileStatus = verified ? "✓ Verified" : "✗ Not Verified"
}
details.append((name, fileStatus))
}
}
}
}
// Check for integrity results
if let integrity = dict["integrity"] as? [String: Any] {
if let c2pa = integrity["c2pa"] as? [String: Any],
let verified = c2pa["verified"] as? Bool {
details.append(("C2PA", verified ? "✓ Verified" : "✗ Not Found"))
}
if let pgp = integrity["pgp"] as? [String: Any],
let verified = pgp["verified"] as? Bool {
details.append(("PGP Signature", verified ? "✓ Verified" : "✗ Not Found"))
}
if let ots = integrity["ots"] as? [String: Any],
let verified = ots["verified"] as? Bool {
details.append(("OpenTimestamps", verified ? "✓ Verified" : "✗ Not Found"))
}
}
// Check for errors
if let errors = dict["errors"] as? [[String: Any]], !errors.isEmpty {
summary = "Verification completed with \(errors.count) error(s)"
for error in errors {
if let name = error["name"] as? String,
let errorMsg = error["error"] as? String {
details.append((name, "Error: \(errorMsg)"))
}
}
}
return VerificationResult(
isEmpty: details.isEmpty,
summary: summary,
details: details
)
}
var body: some View {
ScrollView {
VStack(alignment: .leading, spacing: 16) {
titleView
if let parsed = parsedResult {
parsedResultView(parsed)
} else {
rawResultView
}
}
}
}
// MARK: - Sub-views
private var titleView: some View {
Text("Verification Results")
.font(.headline)
.padding(.horizontal)
}
@ViewBuilder
private func parsedResultView(_ parsed: VerificationResult) -> some View {
VStack(alignment: .leading, spacing: 16) {
summaryView(parsed)
if !parsed.details.isEmpty {
detailsView(parsed.details)
}
if parsed.isEmpty {
helpTextView
}
rawDataDisclosureView
copyButtonView
}
.padding()
}
private func summaryView(_ parsed: VerificationResult) -> some View {
HStack {
Image(systemName: parsed.isEmpty ? "exclamationmark.circle" : "checkmark.circle")
.foregroundColor(parsed.isEmpty ? .orange : .green)
Text(parsed.summary)
.font(.subheadline)
.fontWeight(.medium)
}
.padding()
.frame(maxWidth: .infinity, alignment: .leading)
.background(Color.gray.opacity(0.05))
.cornerRadius(8)
}
private func detailsView(_ details: [(String, String)]) -> some View {
VStack(alignment: .leading, spacing: 8) {
Text("Details")
.font(.caption)
.fontWeight(.semibold)
.foregroundColor(.secondary)
ForEach(Array(details.enumerated()), id: \.offset) { index, item in
detailRow(item: item, isLast: index == details.count - 1)
}
}
.padding()
.background(Color.gray.opacity(0.05))
.cornerRadius(8)
}
private func detailRow(item: (String, String), isLast: Bool) -> some View {
VStack(spacing: 0) {
HStack {
Text(item.0)
.font(.caption)
.foregroundColor(.primary)
Spacer()
Text(item.1)
.font(.caption)
.foregroundColor(detailColor(for: item.1))
}
.padding(.vertical, 4)
if !isLast {
Divider()
}
}
}
private func detailColor(for text: String) -> Color {
if text.contains("✓") {
return .green
} else if text.contains("✗") {
return .red
} else {
return .secondary
}
}
private var helpTextView: some View {
VStack(alignment: .leading, spacing: 8) {
Text("How to add ProofMode data:")
.font(.caption)
.fontWeight(.semibold)
Text("1. Go to the Generate tab")
Text("2. Select a photo from your library")
Text("3. Generate proof data for it")
Text("4. The proof files will be saved with the image")
}
.font(.caption)
.foregroundColor(.secondary)
.padding()
.background(Color.blue.opacity(0.05))
.cornerRadius(8)
}
private var rawDataDisclosureView: some View {
DisclosureGroup("View Raw Data") {
Text(result)
.font(.system(.caption2, design: .monospaced))
.textSelection(.enabled)
.frame(maxWidth: .infinity, alignment: .leading)
.padding()
.background(Color.gray.opacity(0.05))
.cornerRadius(8)
}
.font(.caption)
.foregroundColor(.secondary)
.padding(.top, 8)
}
private var copyButtonView: some View {
HStack {
Button("Copy Raw Data") {
UIPasteboard.general.string = result
}
.font(.caption)
.foregroundColor(.blue)
Spacer()
}
}
private var rawResultView: some View {
VStack(alignment: .leading, spacing: 12) {
Text(result)
.font(.system(.caption, design: .monospaced))
.textSelection(.enabled)
.frame(maxWidth: .infinity, alignment: .leading)
.padding()
.background(Color.gray.opacity(0.05))
.cornerRadius(8)
.overlay(
RoundedRectangle(cornerRadius: 8)
.stroke(Color.gray.opacity(0.3), lineWidth: 1)
)
HStack {
Button("Copy Results") {
UIPasteboard.general.string = result
}
.font(.caption)
.foregroundColor(.blue)
Spacer()
Text("Tap text to select")
.font(.caption2)
.foregroundColor(.secondary)
}
}
.padding()
}
}
// Helper struct for parsed verification results
private struct VerificationResult {
let isEmpty: Bool
let summary: String
let details: [(String, String)]
}
#Preview {
VerifyView(selectedProof: .constant(nil))
.environmentObject(ProofViewModel())
.environmentObject(PermissionService())
}