1// ContactCardView.swift
2// SophaxChat
3
4import SwiftUI
5import CoreImage.CIFilterBuiltins
6import SophaxChatCore
7
8struct ContactCardView: View {
9 @EnvironmentObject var appState: AppState
10 @Environment(\.dismiss) private var dismiss
11 #if !targetEnvironment(macCatalyst) && canImport(CoreNFC)
12 @StateObject private var nfc = NFCContactManager()
13 #endif
14
15 private var contactURL: URL? {
16 // Full URL with Tor onion address
17 if let peerID = appState.chatManager?.identity.publicIdentity.peerID,
18 let onion = appState.derivedOnionHostname {
19 let port = appState.tcpPort.isEmpty ? "25519" : appState.tcpPort
20 var comps = URLComponents()
21 comps.scheme = "sophaxchat"
22 comps.host = "add"
23 comps.queryItems = [
24 URLQueryItem(name: "id", value: peerID),
25 URLQueryItem(name: "onion", value: onion),
26 URLQueryItem(name: "port", value: port),
27 ]
28 return comps.url
29 }
30 // Fallback: local meet link (no Tor required)
31 return appState.generateInviteLink()
32 }
33
34 private var qrImage: Image? {
35 guard let url = contactURL else { return nil }
36 let context = CIContext()
37 let filter = CIFilter.qrCodeGenerator()
38 filter.message = Data(url.absoluteString.utf8)
39 filter.correctionLevel = "M"
40 guard let output = filter.outputImage else { return nil }
41 let scaled = output.transformed(by: CGAffineTransform(scaleX: 10, y: 10))
42 guard let cg = context.createCGImage(scaled, from: scaled.extent) else { return nil }
43 return Image(uiImage: UIImage(cgImage: cg))
44 }
45
46 var body: some View {
47 NavigationStack {
48 VStack(spacing: 0) {
49 Spacer()
50
51 // QR Code
52 Group {
53 if let qr = qrImage {
54 qr
55 .interpolation(.none)
56 .resizable()
57 .scaledToFit()
58 } else {
59 RoundedRectangle(cornerRadius: 4)
60 .fill(Color(.systemGray5))
61 .overlay {
62 Text("Complete setup first")
63 .font(.caption)
64 .foregroundStyle(.secondary)
65 }
66 }
67 }
68 .frame(width: 240, height: 240)
69 .padding(20)
70 .background(.white)
71 .clipShape(RoundedRectangle(cornerRadius: 24))
72 .shadow(color: .black.opacity(0.1), radius: 20, y: 8)
73
74 Spacer().frame(height: 32)
75
76 Text("Scan to add me")
77 .font(.title2.weight(.semibold))
78
79 Spacer().frame(height: 8)
80
81 Text("Open SophaxChat → tap \(Image(systemName: "plus")) → scan")
82 .font(.subheadline)
83 .foregroundStyle(.secondary)
84
85 Spacer().frame(height: 20)
86
87 // Invite code — for verbal recognition ("my code is SXPH-…")
88 HStack(spacing: 10) {
89 Text(appState.friendlyCode)
90 .font(.system(.callout, design: .monospaced).weight(.semibold))
91 .tracking(1.5)
92 .foregroundStyle(.primary)
93 Button {
94 UIPasteboard.general.string = appState.friendlyCode
95 UIImpactFeedbackGenerator(style: .light).impactOccurred()
96 } label: {
97 Image(systemName: "doc.on.doc")
98 .font(.caption)
99 .foregroundStyle(.secondary)
100 }
101 }
102 .padding(.horizontal, 24)
103 .padding(.vertical, 10)
104 .background(Color(.secondarySystemGroupedBackground))
105 .clipShape(RoundedRectangle(cornerRadius: 10))
106
107 Spacer()
108
109 // Share button
110 if let url = contactURL {
111 VStack(spacing: 12) {
112 #if !targetEnvironment(macCatalyst) && canImport(CoreNFC)
113 Button {
114 nfc.writeContact(url: url.absoluteString)
115 } label: {
116 Label("NFC Tap", systemImage: "wave.3.right")
117 .font(.body.weight(.semibold))
118 .frame(maxWidth: .infinity)
119 .padding(.vertical, 16)
120 .background(Color.accentColor)
121 .foregroundStyle(.white)
122 .clipShape(RoundedRectangle(cornerRadius: 14))
123 }
124 #endif
125 ShareLink(item: url) {
126 Text("Share Link")
127 .font(.body.weight(.medium))
128 .frame(maxWidth: .infinity)
129 .padding(.vertical, 14)
130 .background(Color(.secondarySystemGroupedBackground))
131 .foregroundStyle(.primary)
132 .clipShape(RoundedRectangle(cornerRadius: 14))
133 }
134 }
135 .padding(.horizontal, 32)
136 .padding(.bottom, 32)
137 }
138 }
139 .navigationBarTitleDisplayMode(.inline)
140 .toolbar {
141 ToolbarItem(placement: .confirmationAction) {
142 Button("Done") { dismiss() }
143 }
144 }
145 }
146 .presentationDetents([.medium])
147 }
148}
149
150#Preview {
151 ContactCardView().environmentObject(AppState())
152}