LoginView.swift (13510B)
1 // 2 // LoginView.swift 3 // damus 4 // 5 // Created by William Casarin on 2022-05-22. 6 // 7 8 import SwiftUI 9 10 enum ParsedKey { 11 case pub(Pubkey) 12 case priv(Privkey) 13 case hex(String) 14 case nip05(String) 15 16 var is_pub: Bool { 17 if case .pub = self { 18 return true 19 } 20 21 if case .nip05 = self { 22 return true 23 } 24 return false 25 } 26 27 var is_hex: Bool { 28 if case .hex = self { 29 return true 30 } 31 return false 32 } 33 34 var is_priv: Bool { 35 if case .priv = self { 36 return true 37 } 38 return false 39 } 40 } 41 42 struct LoginView: View { 43 @State var key: String = "" 44 @State var is_pubkey: Bool = false 45 @State var error: String? = nil 46 @State private var credential_handler = CredentialHandler() 47 @State private var shouldSaveKey: Bool = true 48 var nav: NavigationCoordinator 49 50 func get_error(parsed_key: ParsedKey?) -> String? { 51 if self.error != nil { 52 return self.error 53 } 54 55 if !key.isEmpty && parsed_key == nil { 56 return LoginError.invalid_key.errorDescription 57 } 58 59 return nil 60 } 61 62 var body: some View { 63 ZStack(alignment: .top) { 64 VStack { 65 SignInHeader() 66 .padding(.top, 100) 67 68 SignInEntry(key: $key, shouldSaveKey: $shouldSaveKey) 69 70 let parsed = parse_key(key) 71 72 if parsed?.is_hex ?? false { 73 // convert to bech32 here 74 } 75 76 if let error = get_error(parsed_key: parsed) { 77 Text(error) 78 .foregroundColor(.red) 79 .padding() 80 } 81 82 if parsed?.is_pub ?? false { 83 Text("This is a public key, you will not be able to make notes or interact in any way. This is used for viewing accounts from their perspective.", comment: "Warning that the inputted account key is a public key and the result of what happens because of it.") 84 .foregroundColor(Color.orange) 85 .bold() 86 .fixedSize(horizontal: false, vertical: true) 87 } 88 89 if let p = parsed { 90 Button(action: { 91 Task { 92 do { 93 try await process_login(p, is_pubkey: is_pubkey, shouldSaveKey: shouldSaveKey) 94 } catch { 95 self.error = error.localizedDescription 96 } 97 } 98 }) { 99 HStack { 100 Text("Login", comment: "Button to log into account.") 101 .fontWeight(.semibold) 102 } 103 .frame(minWidth: 300, maxWidth: .infinity, maxHeight: 12, alignment: .center) 104 } 105 .buttonStyle(GradientButtonStyle()) 106 .padding(.top, 10) 107 } 108 109 CreateAccountPrompt(nav: nav) 110 .padding(.top, 10) 111 112 Spacer() 113 } 114 .padding() 115 } 116 .background(DamusBackground(maxHeight: 350), alignment: .top) 117 .onAppear { 118 credential_handler.check_credentials() 119 } 120 .navigationBarBackButtonHidden(true) 121 .navigationBarItems(leading: BackNav()) 122 } 123 } 124 125 extension View { 126 func nsecLoginStyle(key: String, title: String) -> some View { 127 self 128 .placeholder(when: key.isEmpty) { 129 Text(title).foregroundColor(.white.opacity(0.6)) 130 } 131 .padding(10) 132 .autocapitalization(.none) 133 .autocorrectionDisabled(true) 134 .textInputAutocapitalization(.never) 135 .font(.body.monospaced()) 136 .textContentType(.password) 137 } 138 } 139 140 func parse_key(_ thekey: String) -> ParsedKey? { 141 var key = thekey 142 if key.count > 0 && key.first! == "@" { 143 key = String(key.dropFirst()) 144 } 145 146 if hex_decode(key) != nil { 147 return .hex(key) 148 } 149 150 if (key.contains { $0 == "@" }) { 151 return .nip05(key) 152 } 153 154 if let bech_key = decode_bech32_key(key) { 155 switch bech_key { 156 case .pub(let pk): return .pub(pk) 157 case .sec(let sec): return .priv(sec) 158 } 159 } 160 161 return nil 162 } 163 164 enum LoginError: LocalizedError { 165 case invalid_key 166 case nip05_failed 167 168 var errorDescription: String? { 169 switch self { 170 case .invalid_key: 171 return NSLocalizedString("Invalid key", comment: "Error message indicating that an invalid account key was entered for login.") 172 case .nip05_failed: 173 return "Could not fetch pubkey" 174 } 175 } 176 } 177 178 func process_login(_ key: ParsedKey, is_pubkey: Bool, shouldSaveKey: Bool = true) async throws { 179 if shouldSaveKey { 180 switch key { 181 case .priv(let priv): 182 try handle_privkey(priv) 183 case .pub(let pub): 184 try clear_saved_privkey() 185 save_pubkey(pubkey: pub) 186 187 case .nip05(let id): 188 guard let nip05 = await get_nip05_pubkey(id: id) else { 189 throw LoginError.nip05_failed 190 } 191 192 // this is a weird way to login anyways 193 /* 194 var bootstrap_relays = load_bootstrap_relays(pubkey: nip05.pubkey) 195 for relay in nip05.relays { 196 if !(bootstrap_relays.contains { $0 == relay }) { 197 bootstrap_relays.append(relay) 198 } 199 } 200 */ 201 save_pubkey(pubkey: nip05.pubkey) 202 203 case .hex(let hexstr): 204 if is_pubkey, let pubkey = hex_decode_pubkey(hexstr) { 205 try clear_saved_privkey() 206 207 save_pubkey(pubkey: pubkey) 208 } else if let privkey = hex_decode_privkey(hexstr) { 209 try handle_privkey(privkey) 210 } 211 } 212 } 213 214 func handle_privkey(_ privkey: Privkey) throws { 215 try save_privkey(privkey: privkey) 216 217 guard let pk = privkey_to_pubkey(privkey: privkey) else { 218 throw LoginError.invalid_key 219 } 220 221 CredentialHandler().save_credential(pubkey: pk, privkey: privkey) 222 save_pubkey(pubkey: pk) 223 } 224 225 func handle_transient_privkey(_ key: ParsedKey) -> Keypair? { 226 if case let .priv(priv) = key, let pubkey = privkey_to_pubkey(privkey: priv) { 227 return Keypair(pubkey: pubkey, privkey: priv) 228 } 229 return nil 230 } 231 232 let keypair = shouldSaveKey ? get_saved_keypair() : handle_transient_privkey(key) 233 234 guard let keypair = keypair else { 235 return 236 } 237 238 await MainActor.run { 239 notify(.login(keypair)) 240 } 241 } 242 243 struct NIP05Result: Decodable { 244 let names: Dictionary<String, String> 245 let relays: Dictionary<String, [String]>? 246 } 247 248 struct NIP05User { 249 let pubkey: Pubkey 250 //let relays: [String] 251 } 252 253 func get_nip05_pubkey(id: String) async -> NIP05User? { 254 let parts = id.components(separatedBy: "@") 255 256 guard parts.count == 2 else { 257 return nil 258 } 259 260 let user = parts[0] 261 let host = parts[1] 262 263 guard let url = URL(string: "https://\(host)/.well-known/nostr.json?name=\(user)"), 264 let (data, _) = try? await URLSession.shared.data(for: URLRequest(url: url)), 265 let json: NIP05Result = decode_data(data), 266 let pubkey_hex = json.names[user], 267 let pubkey = hex_decode_pubkey(pubkey_hex) 268 else { 269 return nil 270 } 271 272 /* 273 var relays: [String] = [] 274 275 if let rs = json.relays, let rs = rs[pubkey] { 276 relays = rs 277 } 278 */ 279 280 return NIP05User(pubkey: pubkey/*, relays: relays*/) 281 } 282 283 struct KeyInput: View { 284 let title: String 285 let key: Binding<String> 286 let shouldSaveKey: Binding<Bool> 287 var privKeyFound: Binding<Bool> 288 @State private var is_secured: Bool = true 289 290 init(_ title: String, key: Binding<String>, shouldSaveKey: Binding<Bool>, privKeyFound: Binding<Bool>) { 291 self.title = title 292 self.key = key 293 self.shouldSaveKey = shouldSaveKey 294 self.privKeyFound = privKeyFound 295 } 296 297 var body: some View { 298 HStack { 299 Image(systemName: "doc.on.clipboard") 300 .foregroundColor(.gray) 301 .onTapGesture { 302 if let pastedkey = UIPasteboard.general.string { 303 self.key.wrappedValue = pastedkey 304 } 305 } 306 SignInScan(shouldSaveKey: shouldSaveKey, loginKey: key, privKeyFound: privKeyFound) 307 308 if is_secured { 309 SecureField("", text: key) 310 .nsecLoginStyle(key: key.wrappedValue, title: title) 311 } else { 312 TextField("", text: key) 313 .nsecLoginStyle(key: key.wrappedValue, title: title) 314 } 315 Image(systemName: "eye.slash") 316 .foregroundColor(.gray) 317 .onTapGesture { 318 is_secured.toggle() 319 } 320 } 321 .padding(.vertical, 2) 322 .padding(.horizontal, 10) 323 .overlay { 324 RoundedRectangle(cornerRadius: 12) 325 .stroke(.gray, lineWidth: 1) 326 } 327 } 328 } 329 330 struct SignInHeader: View { 331 var body: some View { 332 VStack { 333 Image("logo-nobg") 334 .resizable() 335 .frame(width: 56, height: 56, alignment: .center) 336 .shadow(color: DamusColors.purple, radius: 2) 337 .padding(.bottom) 338 339 Text("Sign in", comment: "Title of view to log into an account.") 340 .font(.system(size: 32, weight: .bold)) 341 .padding(.bottom, 5) 342 343 Text("Welcome to the social network you control", comment: "Welcome text") 344 .foregroundColor(Color("DamusMediumGrey")) 345 } 346 } 347 } 348 349 struct SignInEntry: View { 350 let key: Binding<String> 351 let shouldSaveKey: Binding<Bool> 352 @State private var privKeyFound: Bool = false 353 var body: some View { 354 VStack(alignment: .leading) { 355 Text("Enter your account key", comment: "Prompt for user to enter an account key to login.") 356 .fontWeight(.medium) 357 .padding(.top, 30) 358 359 KeyInput(NSLocalizedString("nsec1...", comment: "Prompt for user to enter in an account key to login. This text shows the characters the key could start with if it was a private key."), 360 key: key, 361 shouldSaveKey: shouldSaveKey, 362 privKeyFound: $privKeyFound) 363 if privKeyFound { 364 Toggle(NSLocalizedString("Save Key in Secure Keychain", comment: "Toggle to save private key to the Apple secure keychain."), isOn: shouldSaveKey) 365 } 366 } 367 } 368 } 369 370 struct SignInScan: View { 371 @State var showQR: Bool = false 372 @State var qrkey: ParsedKey? 373 @Binding var shouldSaveKey: Bool 374 @Binding var loginKey: String 375 @Binding var privKeyFound: Bool 376 let generator = UINotificationFeedbackGenerator() 377 378 var body: some View { 379 VStack { 380 Button(action: { showQR.toggle() }, label: { 381 Image(systemName: "qrcode.viewfinder")}) 382 .foregroundColor(.gray) 383 384 } 385 .sheet(isPresented: $showQR, onDismiss: { 386 if qrkey == nil { resetView() }} 387 ) { 388 QRScanNSECView(showQR: $showQR, 389 privKeyFound: $privKeyFound, 390 codeScannerCompletion: { scannerCompletion($0) }) 391 } 392 .onChange(of: showQR) { show in 393 if showQR { resetView() } 394 } 395 } 396 397 func handleQRString(_ string: String) { 398 qrkey = parse_key(string) 399 if let key = qrkey, key.is_priv { 400 loginKey = string 401 privKeyFound = true 402 shouldSaveKey = false 403 generator.notificationOccurred(.success) 404 } 405 } 406 407 func scannerCompletion(_ result: Result<ScanResult, ScanError>) { 408 switch result { 409 case .success(let success): 410 handleQRString(success.string) 411 case .failure: 412 return 413 } 414 } 415 416 func resetView() { 417 loginKey = "" 418 qrkey = nil 419 privKeyFound = false 420 shouldSaveKey = true 421 } 422 } 423 424 struct CreateAccountPrompt: View { 425 var nav: NavigationCoordinator 426 var body: some View { 427 HStack { 428 Text("New to Nostr?", comment: "Ask the user if they are new to Nostr") 429 .foregroundColor(Color("DamusMediumGrey")) 430 431 Button(NSLocalizedString("Create account", comment: "Button to navigate to create account view.")) { 432 nav.push(route: Route.CreateAccount) 433 } 434 435 Spacer() 436 } 437 } 438 } 439 440 struct LoginView_Previews: PreviewProvider { 441 static var previews: some View { 442 // let pubkey = "3efdaebb1d8923ebd99c9e7ace3b4194ab45512e2be79c1b7d68d9243e0d2681" 443 let pubkey = "npub18m76awca3y37hkvuneavuw6pjj4525fw90necxmadrvjg0sdy6qsngq955" 444 let bech32_pubkey = "KeyInput" 445 Group { 446 LoginView(key: pubkey, nav: .init()) 447 LoginView(key: bech32_pubkey, nav: .init()) 448 } 449 } 450 }