damus

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

SaveKeysView.swift (10682B)


      1 //
      2 //  SaveKeysView.swift
      3 //  damus
      4 //
      5 //  Created by William Casarin on 2022-05-21.
      6 //
      7 
      8 import SwiftUI
      9 import Security
     10 
     11 struct SaveKeysView: View {
     12     let account: CreateAccountModel
     13     let pool: RelayPool = RelayPool(ndb: Ndb()!)
     14     @State var pub_copied: Bool = false
     15     @State var priv_copied: Bool = false
     16     @State var loading: Bool = false
     17     @State var error: String? = nil
     18     
     19     @State private var credential_handler = CredentialHandler()
     20 
     21     @FocusState var pubkey_focused: Bool
     22     @FocusState var privkey_focused: Bool
     23     
     24     let first_contact_event: NdbNote?
     25     
     26     init(account: CreateAccountModel) {
     27         self.account = account
     28         self.first_contact_event = make_first_contact_event(keypair: account.keypair)
     29     }
     30     
     31     var body: some View {
     32         ZStack(alignment: .top) {
     33             VStack(alignment: .center) {
     34                 if account.rendered_name.isEmpty {
     35                     Text("Welcome!", comment: "Text to welcome user.")
     36                         .font(.title.bold())
     37                         .padding(.bottom, 10)
     38                 } else {
     39                     Text("Welcome, \(account.rendered_name)!", comment: "Text to welcome user.")
     40                         .font(.title.bold())
     41                         .padding(.bottom, 10)
     42                 }
     43                 
     44                 Text("Before we get started, you'll need to save your account info, otherwise you won't be able to login in the future if you ever uninstall Damus.", comment: "Reminder to user that they should save their account information.")
     45                     .padding(.bottom, 10)
     46                 
     47                 Text("Private Key", comment: "Label to indicate that the text below is the user's private key used by only the user themself as a secret to login to access their account.")
     48                     .font(.title2.bold())
     49                     .padding(.bottom, 10)
     50                 
     51                 Text("This is your secret account key. You need this to access your account. Don't share this with anyone! Save it in a password manager and keep it safe!", comment: "Label to describe that a private key is the user's secret account key and what they should do with it.")
     52                     .padding(.bottom, 10)
     53                 
     54                 SaveKeyView(text: account.privkey.nsec, textContentType: .newPassword, is_copied: $priv_copied, focus: $privkey_focused)
     55                     .padding(.bottom, 10)
     56 
     57                 if priv_copied {
     58                     if loading {
     59                         ProgressView()
     60                             .progressViewStyle(.circular)
     61                     } else if let err = error {
     62                         Text("Error: \(err)", comment: "Error message indicating why saving keys failed.")
     63                             .foregroundColor(.red)
     64 
     65                         Button(action: {
     66                             complete_account_creation(account)
     67                         }) {
     68                             HStack {
     69                                 Text("Retry", comment:  "Button to retry completing account creation after an error occurred.")
     70                                     .fontWeight(.semibold)
     71                             }
     72                             .frame(minWidth: 300, maxWidth: .infinity, maxHeight: 12, alignment: .center)
     73                         }
     74                         .buttonStyle(GradientButtonStyle())
     75                         .padding(.top, 20)
     76                     } else {
     77                         Button(action: {
     78                             complete_account_creation(account)
     79                         }) {
     80                             HStack {
     81                                 Text("Let's go!", comment:  "Button to complete account creation and start using the app.")
     82                                     .fontWeight(.semibold)
     83                             }
     84                             .frame(minWidth: 300, maxWidth: .infinity, maxHeight: 12, alignment: .center)
     85                         }
     86                         .buttonStyle(GradientButtonStyle())
     87                         .padding(.top, 20)
     88                     }
     89                 }
     90             }
     91             .padding(20)
     92         }
     93         .background(
     94             Image("eula-bg")
     95                 .resizable()
     96                 .blur(radius: 70)
     97                 .ignoresSafeArea(),
     98             alignment: .top
     99         )
    100         .navigationBarBackButtonHidden(true)
    101         .navigationBarItems(leading: BackNav())
    102         .onAppear {
    103             // Hack to force keyboard to show up for a short moment and then hiding it to register password autofill flow.
    104             pubkey_focused = true
    105             DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
    106                 pubkey_focused = false
    107             }
    108         }
    109     }
    110     
    111     func complete_account_creation(_ account: CreateAccountModel) {
    112         guard let first_contact_event else {
    113             error = NSLocalizedString("Could not create your initial contact list event. This is a software bug, please contact Damus support via support@damus.io or through our Nostr account for help.", comment: "Error message to the user indicating that the initial contact list failed to be created.")
    114             return
    115         }
    116         // Save contact list to storage right away so that we don't need to depend on the network to complete this important step
    117         self.save_to_storage(first_contact_event: first_contact_event, for: account)
    118         
    119         let bootstrap_relays = load_bootstrap_relays(pubkey: account.pubkey)
    120         for relay in bootstrap_relays {
    121             add_rw_relay(self.pool, relay)
    122         }
    123 
    124         self.pool.register_handler(sub_id: "signup", handler: handle_event)
    125         
    126         credential_handler.save_credential(pubkey: account.pubkey, privkey: account.privkey)
    127 
    128         self.loading = true
    129         
    130         self.pool.connect()
    131     }
    132     
    133     func save_to_storage(first_contact_event: NdbNote, for account: CreateAccountModel) {
    134         // Send to NostrDB so that we have a local copy in storage
    135         self.pool.send_raw_to_local_ndb(.typical(.event(first_contact_event)))
    136         
    137         // Save the ID to user settings so that we can easily find it later.
    138         let settings = UserSettingsStore.globally_load_for(pubkey: account.pubkey)
    139         settings.latest_contact_event_id_hex = first_contact_event.id.hex()
    140     }
    141 
    142     func handle_event(relay: RelayURL, ev: NostrConnectionEvent) {
    143         switch ev {
    144         case .ws_event(let wsev):
    145             switch wsev {
    146             case .connected:
    147                 let metadata = create_account_to_metadata(account)
    148                 
    149                 if let keypair = account.keypair.to_full(),
    150                    let metadata_ev = make_metadata_event(keypair: keypair, metadata: metadata) {
    151                     self.pool.send(.event(metadata_ev))
    152                 }
    153                 
    154                 if let first_contact_event {
    155                     self.pool.send(.event(first_contact_event))
    156                 }
    157                 
    158                 do {
    159                     try save_keypair(pubkey: account.pubkey, privkey: account.privkey)
    160                     notify(.login(account.keypair))
    161                 } catch {
    162                     self.error = "Failed to save keys"
    163                 }
    164                 
    165             case .error(let err):
    166                 self.loading = false
    167                 self.error = String(describing: err)
    168             default:
    169                 break
    170             }
    171         case .nostr_event(let resp):
    172             switch resp {
    173             case .notice(let msg):
    174                 // TODO handle message
    175                 self.loading = false
    176                 self.error = msg
    177                 print(msg)
    178             case .event:
    179                 print("event in signup?")
    180             case .eose:
    181                 break
    182             case .ok:
    183                 break
    184             case .auth:
    185                 break
    186             }
    187         }
    188     }
    189 }
    190 
    191 struct SaveKeyView: View {
    192     let text: String
    193     let textContentType: UITextContentType
    194     @Binding var is_copied: Bool
    195     var focus: FocusState<Bool>.Binding
    196     
    197     func copy_text() {
    198         UIPasteboard.general.string = text
    199         is_copied = true
    200     }
    201     
    202     var body: some View {
    203         HStack {
    204             Spacer()
    205             VStack {
    206                 spacerBlock(width: 0, height: 0)
    207                 Button(action: copy_text) {
    208                     Label("", image: is_copied ? "check-circle.fill" : "copy2")
    209                         .foregroundColor(is_copied ? .green : .gray)
    210                         .background {
    211                             if is_copied {
    212                                 Circle()
    213                                     .foregroundColor(.white)
    214                                     .frame(width: 25, height: 25, alignment: .center)
    215                                     .padding(.leading, -8)
    216                                     .padding(.top, 1)
    217                             } else {
    218                                 EmptyView()
    219                             }
    220                         }
    221                 }
    222             }
    223 
    224             TextField("", text: .constant(text))
    225                 .padding(5)
    226                 .background {
    227                     RoundedRectangle(cornerRadius: 4.0).opacity(0.1)
    228                 }
    229                 .textSelection(.enabled)
    230                 .font(.callout.monospaced())
    231                 .onTapGesture {
    232                     copy_text()
    233                     // Hack to force keyboard to hide. Showing keyboard on text field is necessary to register password autofill flow but the text itself should not be modified.
    234                     DispatchQueue.main.async {
    235                         end_editing()
    236                     }
    237                 }
    238                 .textContentType(textContentType)
    239                 .deleteDisabled(true)
    240                 .focused(focus)
    241             
    242             spacerBlock(width: 0, height: 0) /// set a 'width' > 0 here to vary key Text's aspect ratio
    243         }
    244     }
    245     
    246     @ViewBuilder private func spacerBlock(width: CGFloat, height: CGFloat) -> some View {
    247         Color.orange.opacity(1)
    248             .frame(width: width, height: height)
    249     }
    250 }
    251 
    252 struct SaveKeysView_Previews: PreviewProvider {
    253     static var previews: some View {
    254         let model = CreateAccountModel(real: "William", nick: "jb55", about: "I'm me")
    255         SaveKeysView(account: model)
    256     }
    257 }
    258 
    259 func create_account_to_metadata(_ model: CreateAccountModel) -> Profile {
    260     return Profile(name: model.nick_name, display_name: model.real_name, about: model.about, picture: model.profile_image?.absoluteString, banner: nil, website: nil, lud06: nil, lud16: nil, nip05: nil, damus_donation: nil)
    261 }