lnlink

iOS app for connecting to lightning nodes
git clone git://jb55.com/lnlink
Log | Files | Refs | Submodules | README | LICENSE

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 }