damus

nostr ios client
git clone git://jb55.com/damus
Log | Files | Refs | README | LICENSE

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 }