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 }