QRCodeView.swift (10252B)
1 // 2 // QRCodeView.swift 3 // damus 4 // 5 // Created by eric on 1/27/23. 6 // 7 8 import SwiftUI 9 import CoreImage.CIFilterBuiltins 10 11 struct ProfileScanResult: Equatable { 12 let pubkey: Pubkey 13 14 init?(hex: String) { 15 guard let pk = hex_decode(hex).map({ bytes in Pubkey(Data(bytes)) }) else { 16 return nil 17 } 18 19 self.pubkey = pk 20 } 21 22 init?(string: String) { 23 var str = string 24 guard str.count != 0 else { 25 return nil 26 } 27 28 if str.hasPrefix("nostr:") { 29 str.removeFirst("nostr:".count) 30 } 31 32 if let decoded = hex_decode(str), 33 str.count == 64 34 { 35 self.pubkey = Pubkey(Data(decoded)) 36 return 37 } 38 39 if str.starts(with: "npub"), 40 let b32 = try? bech32_decode(str) 41 { 42 self.pubkey = Pubkey(b32.data) 43 return 44 } 45 46 return nil 47 } 48 } 49 50 struct QRCodeView: View { 51 let damus_state: DamusState 52 @State var pubkey: Pubkey 53 54 @Environment(\.presentationMode) var presentationMode 55 56 @State private var selectedTab = 0 57 @State var scanResult: ProfileScanResult? = nil 58 @State var profile: Profile? = nil 59 @State var error: String? = nil 60 @State private var outerTrimEnd: CGFloat = 0 61 62 var animationDuration: Double = 0.5 63 64 let generator = UIImpactFeedbackGenerator(style: .light) 65 66 @ViewBuilder 67 func navImage(systemImage: String) -> some View { 68 Image(systemName: systemImage) 69 .frame(width: 33, height: 33) 70 .background(Color.black.opacity(0.6)) 71 .clipShape(Circle()) 72 } 73 74 var navBackButton: some View { 75 Button { 76 presentationMode.wrappedValue.dismiss() 77 } label: { 78 navImage(systemImage: "chevron.left") 79 } 80 } 81 82 var customNavbar: some View { 83 HStack { 84 navBackButton 85 Spacer() 86 } 87 .padding(.top, 5) 88 .padding(.horizontal) 89 .accentColor(DamusColors.white) 90 } 91 92 var body: some View { 93 NavigationView { 94 ZStack(alignment: .center) { 95 ZStack(alignment: .topLeading) { 96 DamusGradient() 97 } 98 TabView(selection: $selectedTab) { 99 QRView 100 .tag(0) 101 QRCameraView() 102 .tag(1) 103 } 104 .tabViewStyle(PageTabViewStyle(indexDisplayMode: .never)) 105 .onAppear { 106 UIScrollView.appearance().isScrollEnabled = false 107 } 108 .gesture( 109 DragGesture() 110 .onChanged { _ in } 111 ) 112 } 113 } 114 .navigationTitle("") 115 .navigationBarHidden(true) 116 .overlay(customNavbar, alignment: .top) 117 } 118 119 var QRView: some View { 120 VStack(alignment: .center) { 121 let profile_txn = damus_state.profiles.lookup(id: pubkey, txn_name: "qrview-profile") 122 let profile = profile_txn?.unsafeUnownedValue 123 let our_profile = profile_txn.flatMap({ ptxn in 124 damus_state.ndb.lookup_profile_with_txn(damus_state.pubkey, txn: ptxn)?.profile 125 }) 126 127 if our_profile?.picture != nil { 128 ProfilePicView(pubkey: pubkey, size: 90.0, highlight: .custom(DamusColors.white, 3.0), profiles: damus_state.profiles, disable_animation: damus_state.settings.disable_animation) 129 .padding(.top, 50) 130 } else { 131 Image(systemName: "person.fill") 132 .font(.system(size: 60)) 133 .padding(.top, 50) 134 } 135 136 if let display_name = profile?.display_name { 137 Text(display_name) 138 .font(.system(size: 24, weight: .heavy)) 139 .foregroundColor(.white) 140 } 141 if let name = profile?.name { 142 Text("@" + name) 143 .font(.body) 144 .foregroundColor(.white) 145 } 146 147 Spacer() 148 149 Image(uiImage: generateQRCode(pubkey: "nostr:" + pubkey.npub)) 150 .interpolation(.none) 151 .resizable() 152 .scaledToFit() 153 .frame(width: 300, height: 300) 154 .cornerRadius(10) 155 .overlay(RoundedRectangle(cornerRadius: 10) 156 .stroke(DamusColors.white, lineWidth: 5.0)) 157 .shadow(radius: 10) 158 159 Spacer() 160 161 Text("Follow me on Nostr", comment: "Text on QR code view to prompt viewer looking at screen to follow the user.") 162 .font(.system(size: 24, weight: .heavy)) 163 .padding(.top) 164 .foregroundColor(.white) 165 166 Text("Scan the code", comment: "Text on QR code view to prompt viewer to scan the QR code on screen with their device camera.") 167 .font(.system(size: 18, weight: .ultraLight)) 168 .foregroundColor(.white) 169 170 Spacer() 171 172 Button(action: { 173 selectedTab = 1 174 }) { 175 HStack { 176 Text("Scan Code", comment: "Button to switch to scan QR Code page.") 177 .fontWeight(.semibold) 178 } 179 .frame(minWidth: 300, maxWidth: .infinity, maxHeight: 12, alignment: .center) 180 } 181 .buttonStyle(GradientButtonStyle()) 182 .padding(50) 183 } 184 } 185 186 func QRCameraView() -> some View { 187 return VStack(alignment: .center) { 188 Text("Scan a user's pubkey", comment: "Text to prompt scanning a QR code of a user's pubkey to open their profile.") 189 .padding(.top, 50) 190 .font(.system(size: 24, weight: .heavy)) 191 .foregroundColor(.white) 192 193 Spacer() 194 195 CodeScannerView(codeTypes: [.qr], scanMode: .continuous, simulatedData: "npub1k92qsr95jcumkpu6dffurkvwwycwa2euvx4fthv78ru7gqqz0nrs2ngfwd", shouldVibrateOnSuccess: false) { result in 196 switch result { 197 case .success(let success): 198 handleProfileScan(success.string) 199 case .failure(let failure): 200 self.error = failure.localizedDescription 201 } 202 } 203 .scaledToFit() 204 .frame(width: 300, height: 300) 205 .cornerRadius(10) 206 .overlay(RoundedRectangle(cornerRadius: 10).stroke(DamusColors.white, lineWidth: 5.0)) 207 .overlay(RoundedRectangle(cornerRadius: 10).trim(from: 0.0, to: outerTrimEnd).stroke(DamusColors.black, lineWidth: 5.5) 208 .rotationEffect(.degrees(-90))) 209 .shadow(radius: 10) 210 211 Spacer() 212 213 Spacer() 214 215 Button(action: { 216 selectedTab = 0 217 }) { 218 HStack { 219 Text("View QR Code", comment: "Button to switch to view users QR Code") 220 .fontWeight(.semibold) 221 } 222 .frame( maxWidth: .infinity, maxHeight: 12, alignment: .center) 223 } 224 .buttonStyle(GradientButtonStyle()) 225 .padding(50) 226 } 227 } 228 229 func handleProfileScan(_ scanned_str: String) { 230 guard let result = ProfileScanResult(string: scanned_str) else { 231 self.error = "Invalid profile QR" 232 return 233 } 234 235 self.error = nil 236 237 guard result != self.scanResult else { 238 return 239 } 240 241 generator.impactOccurred() 242 cameraAnimate { 243 scanResult = result 244 245 find_event(state: damus_state, query: .profile(pubkey: result.pubkey)) { res in 246 guard let res else { 247 error = "Profile not found" 248 return 249 } 250 251 switch res { 252 case .invalid_profile: 253 error = "Profile was found but was corrupt." 254 255 case .profile: 256 show_profile_after_delay() 257 258 case .event: 259 print("invalid search result") 260 } 261 262 } 263 } 264 } 265 266 func show_profile_after_delay() { 267 DispatchQueue.main.asyncAfter(deadline: .now() + animationDuration) { 268 if let scanResult { 269 damus_state.nav.push(route: Route.ProfileByKey(pubkey: scanResult.pubkey)) 270 presentationMode.wrappedValue.dismiss() 271 } 272 } 273 } 274 275 func cameraAnimate(completion: @escaping () -> Void) { 276 outerTrimEnd = 0.0 277 withAnimation(.easeInOut(duration: animationDuration)) { 278 outerTrimEnd = 1.05 // Set to 1.05 instead of 1.0 since sometimes `completion()` runs before the value reaches 1.0. This ensures the animation is done. 279 } 280 completion() 281 } 282 283 func generateQRCode(pubkey: String) -> UIImage { 284 let data = pubkey.data(using: String.Encoding.ascii) 285 let qrFilter = CIFilter(name: "CIQRCodeGenerator") 286 qrFilter?.setValue(data, forKey: "inputMessage") 287 let qrImage = qrFilter?.outputImage 288 289 let colorInvertFilter = CIFilter(name: "CIColorInvert") 290 colorInvertFilter?.setValue(qrImage, forKey: "inputImage") 291 let outputInvertedImage = colorInvertFilter?.outputImage 292 293 let maskToAlphaFilter = CIFilter(name: "CIMaskToAlpha") 294 maskToAlphaFilter?.setValue(outputInvertedImage, forKey: "inputImage") 295 let outputCIImage = maskToAlphaFilter?.outputImage 296 297 let context = CIContext() 298 let cgImage = context.createCGImage(outputCIImage!, from: outputCIImage!.extent)! 299 return UIImage(cgImage: cgImage) 300 } 301 } 302 303 struct QRCodeView_Previews: PreviewProvider { 304 static var previews: some View { 305 QRCodeView(damus_state: test_damus_state, pubkey: test_note.pubkey) 306 } 307 } 308