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