SetupView.swift (7550B)
1 // 2 // SetupView.swift 3 // lightninglink 4 // 5 // Created by William Casarin on 2022-02-26. 6 // 7 8 import SwiftUI 9 import Foundation 10 11 public enum ActiveAuthSheet: Identifiable { 12 public var id: String { 13 switch self { 14 case .qr: 15 return "qrcode" 16 } 17 } 18 19 case qr 20 } 21 22 public enum SetupResult { 23 case connection_failed 24 case plugin_missing 25 case auth_invalid(String) 26 case success(GetInfo, ListFunds) 27 } 28 29 public enum SetupViewState { 30 case initial 31 case validating(LNLink) 32 case validated(LNLink) 33 } 34 35 func initial_state() -> SetupViewState { 36 let lnlink = load_lnlink() 37 if lnlink != nil { 38 return .validating(lnlink!) 39 } 40 41 return .initial 42 } 43 44 struct SetupView: View { 45 @State var active_sheet: ActiveAuthSheet? = nil 46 @State var state: SetupViewState = initial_state() 47 @State var error: String? = nil 48 @State var dashboard: Dashboard = .empty 49 @State var has_clipboard_lnlink: Bool = false 50 @State var scan_invoice: String? = nil 51 52 func perform_validation(_ lnlink: LNLink) { 53 DispatchQueue.global(qos: .background).async { 54 validate_connection(lnlink: lnlink) { res in 55 switch res { 56 case .connection_failed: 57 self.state = .initial 58 self.error = "Connection failed" 59 case .plugin_missing: 60 self.state = .initial 61 self.error = "Connected but could not retrieve data. Commando plugin missing?" 62 case .auth_invalid(let str): 63 self.state = .initial 64 self.error = str 65 case .success(let info, let funds): 66 save_lnlink(lnlink: lnlink) 67 self.dashboard = Dashboard(info: info, funds: funds) 68 self.state = .validated(lnlink) 69 self.error = nil 70 } 71 } 72 } 73 } 74 75 func setup_view() -> some View { 76 VStack { 77 Text("Connect") 78 .font(.largeTitle) 79 Text("Scan a LNLink QR Code to connect to your clightning node") 80 .multilineTextAlignment(.center) 81 .font(.subheadline) 82 .foregroundColor(.gray) 83 84 Spacer() 85 86 Button("Use LNLink from Clipboard") { 87 guard let lnlink = check_clipboard_lnlink() else { 88 self.error = "No clipboard lnlink found" 89 return 90 } 91 92 self.state = .validating(lnlink) 93 } 94 .foregroundColor(Color.blue) 95 .padding() 96 .background(Color(.secondarySystemBackground)) 97 .cornerRadius(16) 98 99 Button("Scan LNLink QR Code") { 100 self.active_sheet = .qr 101 } 102 .foregroundColor(Color.blue) 103 .padding() 104 .background(Color(.secondarySystemBackground)) 105 .cornerRadius(16) 106 107 if self.error != nil { 108 Text("Error: \(self.error!)") 109 .foregroundColor(Color.red) 110 } 111 112 Spacer() 113 114 Link("What the heck is LNLink?", destination: URL(string:"http://lnlink.app/qr")!) 115 } 116 .padding() 117 .onOpenURL() { url in 118 self.scan_invoice = url.absoluteString 119 } 120 .sheet(item: $active_sheet) { active_sheet in 121 switch active_sheet { 122 case .qr: 123 CodeScannerView(codeTypes: SCAN_TYPES) { code_res in 124 switch code_res { 125 case .success(let scan_res): 126 let auth_qr = scan_res.string 127 // auth_qr ~ lnlink:host:port?nodeid=nodeid&token=rune 128 let m_lnlink = parse_auth_qr(auth_qr) 129 130 switch m_lnlink { 131 case .left(let err): 132 self.error = err 133 case .right(let lnlink): 134 self.state = .validating(lnlink) 135 } 136 137 case .failure(let scan_err): 138 self.error = scan_err.localizedDescription 139 } 140 } 141 } 142 } 143 144 } 145 146 func validating_view(lnlink: LNLink) -> some View { 147 ProgressView() 148 .progressViewStyle(.circular) 149 .onAppear() { 150 self.perform_validation(lnlink) 151 } 152 .onOpenURL() { url in 153 self.scan_invoice = url.absoluteString 154 } 155 } 156 157 var body: some View { 158 Group { 159 switch self.state { 160 case .initial: 161 setup_view() 162 case .validating(let lnlink): 163 validating_view(lnlink: lnlink) 164 case .validated(let lnlink): 165 ContentView(dashboard: self.dashboard, lnlink: lnlink, scan_invoice: self.scan_invoice) 166 } 167 } 168 } 169 } 170 171 struct SetupView_Previews: PreviewProvider { 172 static var previews: some View { 173 SetupView() 174 } 175 } 176 177 func check_clipboard_lnlink() -> LNLink? { 178 guard UIPasteboard.general.hasStrings else { 179 return nil 180 } 181 182 guard let clip = UIPasteboard.general.string else { 183 return nil 184 } 185 186 let m_lnlink = parse_auth_qr(clip) 187 188 switch m_lnlink { 189 case .left: 190 return nil 191 case .right(let lnlink): 192 return lnlink 193 } 194 } 195 196 func get_qs_param(qs: URLComponents, param: String) -> String? { 197 return qs.queryItems?.first(where: { $0.name == param })?.value 198 } 199 200 201 func parse_auth_qr(_ qr: String) -> Either<String, LNLink> { 202 var auth_qr = qr 203 if auth_qr.hasPrefix("lnlink:") && !auth_qr.hasPrefix("lnlink://") { 204 auth_qr = qr.replacingOccurrences(of: "lnlink:", with: "lnlink://") 205 } 206 207 guard let url = URL(string: auth_qr) else { 208 return .left("Invalid url") 209 } 210 211 guard let nodeid = url.user else { 212 return .left("No nodeid found in auth qr") 213 } 214 215 guard var host = url.host else { 216 return .left("No hostname found in auth qr") 217 } 218 219 let port = url.port ?? 9735 220 host = host + ":\(port)" 221 222 guard let qs = URLComponents(string: auth_qr) else { 223 return .left("Invalid url querystring") 224 } 225 226 guard let token = get_qs_param(qs: qs, param: "token") else { 227 return .left("No token found in auth qr") 228 } 229 230 let lnlink = LNLink(token: token, host: host, node_id: nodeid) 231 return .right(lnlink) 232 } 233 234 235 func validate_connection(lnlink: LNLink, completion: @escaping (SetupResult) -> Void) { 236 let ln = LNSocket() 237 238 guard ln.connect_and_init(node_id: lnlink.node_id, host: lnlink.host) else { 239 completion(.connection_failed) 240 return 241 } 242 243 let res = rpc_getinfo(ln: ln, token: lnlink.token, timeout: 5000) 244 245 switch res { 246 case .failure(let rpc_err): 247 switch rpc_err.errorType { 248 case .timeout: 249 completion(.plugin_missing) 250 return 251 default: 252 break 253 } 254 255 completion(.auth_invalid(rpc_err.description)) 256 257 case .success(let getinfo): 258 let funds_res = rpc_listfunds(ln: ln, token: lnlink.token) 259 260 switch funds_res { 261 case .failure(let err): 262 print(err) 263 completion(.success(getinfo, .empty)) 264 case .success(let listfunds): 265 completion(.success(getinfo, listfunds)) 266 } 267 } 268 } 269 270 271 func handle_scan(_ str: String) { 272 273 }