lnlink

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

ContentView.swift (9138B)


      1 //
      2 //  ContentView.swift
      3 //  lightninglink
      4 //
      5 //  Created by William Casarin on 2022-01-07.
      6 //
      7 
      8 import SwiftUI
      9 import AVFoundation
     10 
     11 extension Notification.Name {
     12     static var sentPayment: Notification.Name {
     13         return Notification.Name("did send payment")
     14     }
     15 
     16     static var reset: Notification.Name {
     17         return Notification.Name("reset lnlink")
     18     }
     19 
     20     static var donate: Notification.Name {
     21         return Notification.Name("donate")
     22     }
     23 }
     24 
     25 enum ActiveAlert: Identifiable {
     26     var id: String {
     27         switch self {
     28         case .pay:
     29             return "pay"
     30         }
     31     }
     32 
     33     case pay(LNScanResult)
     34 }
     35 
     36 public enum ActiveSheet: Identifiable {
     37     public var id: String {
     38         switch self {
     39         case .receive:
     40             return "receive"
     41         case .auth:
     42             return "auth"
     43         case .qr:
     44             return "qrcode"
     45         case .pay:
     46             return "paysheet"
     47         }
     48     }
     49 
     50     case qr
     51     case receive
     52     case pay(DecodeType)
     53     case auth(LNUrlAuth)
     54 }
     55 
     56 struct Funds {
     57     public var onchain_sats: Int64
     58     public var channel_sats: Int64
     59 
     60     public static var empty = Funds(onchain_sats: 0, channel_sats: 0)
     61 
     62     public static func from_listfunds(fs: ListFunds) -> Funds {
     63         var onchain_sats: Int64 = 0
     64         var channel_sats: Int64 = 0
     65         //var our_sats: Int64 = 0
     66 
     67         let channels = fs.channels ?? []
     68         let outputs = fs.outputs ?? []
     69 
     70         for channel in channels {
     71             //our_sats += channel.our_amount_msat
     72             channel_sats += channel.our_amount_msat.msat / 1000
     73         }
     74 
     75         for output in outputs {
     76             onchain_sats += output.amount_msat.msat / 1000
     77         }
     78 
     79         return Funds(onchain_sats: onchain_sats, channel_sats: channel_sats)
     80     }
     81 }
     82 
     83 let SCAN_TYPES: [AVMetadataObject.ObjectType] = [.qr]
     84 
     85 struct ContentView: View {
     86     @State private var active_sheet: ActiveSheet? = nil
     87     @State private var active_alert: ActiveAlert? = nil
     88     @State private var has_alert: Bool = false
     89     @State private var last_pay: Pay?
     90     @State private var funds: Funds = .empty
     91     @State private var is_reset: Bool = false
     92     @State private var scan_invoice: String? = nil
     93     @State private var rate: ExchangeRate?
     94 
     95     private let dashboard: Dashboard
     96     private let lnlink: LNLink
     97     private let init_scan_invoice: String?
     98 
     99     init(dashboard: Dashboard, lnlink: LNLink, scan_invoice: String?) {
    100         self.dashboard = dashboard
    101         self.init_scan_invoice = scan_invoice
    102         self.lnlink = lnlink
    103     }
    104 
    105     func refresh_funds() {
    106         let ln = LNSocket()
    107         guard ln.connect_and_init(node_id: self.lnlink.node_id, host: self.lnlink.host) else {
    108             return
    109         }
    110         let funds = fetch_funds(ln: ln, token: lnlink.token)
    111         self.funds = Funds.from_listfunds(fs: funds)
    112     }
    113 
    114     func format_last_pay() -> String {
    115         guard let pay = last_pay else {
    116             return ""
    117         }
    118 
    119         let fee = pay.amount_sent_msat.msat - pay.amount_msat.msat
    120         return "-\(render_amount_msats(pay.amount_msat.msat)) (\(render_amount_msats(fee)) fee)"
    121     }
    122 
    123     func receive_pay() {
    124         self.active_sheet = .receive
    125     }
    126 
    127     func check_pay() {
    128         guard let decode = get_clipboard_invoice() else {
    129             self.active_sheet = .qr
    130             self.has_alert = false
    131             return
    132         }
    133 
    134         self.active_sheet = nil
    135         self.active_alert = .pay(decode)
    136         self.has_alert = true
    137     }
    138 
    139     func main_content() -> some View {
    140         NavigationView {
    141         VStack(alignment: .hcentered) {
    142             VStack{
    143                 HStack {
    144                     VStack {
    145                     Text(self.dashboard.info.alias)
    146                         .font(.title)
    147                     }
    148 
    149                     Spacer()
    150 
    151                     NavigationLink(destination: SettingsView()) {
    152                         Label("", systemImage: "gear")
    153                             .font(.system(size: 24))
    154                             .foregroundColor(.gray)
    155                     }
    156                 }
    157 
    158                 HStack {
    159                     Text("\(self.dashboard.info.fees_collected_msat.msat / 1000) sats earned")
    160                         .font(.footnote)
    161                         .foregroundColor(.gray)
    162 
    163                     Spacer()
    164                 }
    165             }
    166 
    167             Spacer()
    168             Text("\(format_last_pay())")
    169                 .foregroundColor(Color.red)
    170 
    171             amount_view(self.funds.channel_sats * 1000, rate: self.rate)
    172 
    173             if self.funds.onchain_sats != 0 {
    174                 Text("\(self.funds.onchain_sats) onchain")
    175                     .foregroundColor(.gray)
    176             }
    177 
    178             Spacer()
    179 
    180             HStack {
    181                 Button(action: receive_pay) {
    182                     Label("", systemImage: "arrow.down.circle")
    183                 }
    184                 .font(.largeTitle)
    185 
    186                 Spacer()
    187 
    188                 Button(action: check_pay) {
    189                     Label("", systemImage: "qrcode.viewfinder")
    190                 }
    191                 .font(.largeTitle)
    192             }
    193         }
    194         .padding()
    195         .alert("Use invoice in clipboard?", isPresented: $has_alert, presenting: active_alert) { alert in
    196             Button("Use QR") {
    197                 self.has_alert = false
    198                 self.active_sheet = .qr
    199             }
    200             Button("Yes") {
    201                 self.has_alert = false
    202                 self.active_alert = nil
    203                 switch alert {
    204                 case .pay(let scanres):
    205                     handle_scan_result(scanres)
    206                 }
    207             }
    208         }
    209         .sheet(item: $active_sheet) { sheet in
    210             switch sheet {
    211             case .auth(let auth):
    212                 AuthView(auth: auth, lnlink: lnlink)
    213                 
    214             case .qr:
    215                 CodeScannerView(codeTypes: SCAN_TYPES) { res in
    216                     switch res {
    217                     case .success(let scan_res):
    218                         handle_scan(scan_res.string)
    219 
    220                     case .failure:
    221                         self.active_sheet = nil
    222                         return
    223                     }
    224 
    225                 }
    226 
    227             case .receive:
    228                 ReceiveView(rate: $rate, lnlink: lnlink)
    229 
    230             case .pay(let decode):
    231                 PayView(decode: decode, lnlink: self.lnlink, rate: self.rate)
    232             }
    233         }
    234         .onReceive(NotificationCenter.default.publisher(for: .sentPayment)) { payment in
    235             last_pay = payment.object as? Pay
    236             self.active_sheet = nil
    237             refresh_funds()
    238         }
    239         .onReceive(NotificationCenter.default.publisher(for: .reset)) { _ in
    240             self.is_reset = true
    241         }
    242         .onReceive(NotificationCenter.default.publisher(for: .donate)) { _ in
    243             let offer: DecodeType = .offer("lno1pfsycnjvd9hxkgrfwvsxvun9v5s8xmmxw3mkzun9yysyyateypkk2grpyrcflrd6ypek7gzfyp3kzm3qvdhkuarfde6k2grd0ysxzmrrda5x7mrfwdkj6en4v4kx2epqvdhkg6twvusxzerkv4h8gatjv4eju9q2d3hxc6twdvhxzursrcs08sggen2ndwzjdpqlpfw9sgfth8n9sjs7kjfssrnurnp5lqk66u0sgr32zxwrh0kmxnvmt5hyn0my534209573mp9ck5ekvywvugm5x3kq8ztex8yumafeft0arh6dke04jqgckmdzekqxegxzhecl23lurrj")
    244             self.active_sheet = .pay(offer)
    245         }
    246         .onOpenURL() { url in
    247             handle_scan(url.absoluteString)
    248         }
    249         .onAppear() {
    250             get_exchange_rate(for_cur: .USD) {
    251                 self.rate = $0
    252             }
    253             refresh_funds()
    254             if init_scan_invoice != nil {
    255                 handle_scan(init_scan_invoice!)
    256                 scan_invoice = nil
    257             }
    258         }
    259         .navigationBarTitle("", displayMode: .inline)
    260         .navigationBarHidden(true)
    261 
    262         }
    263 
    264     }
    265 
    266     func handle_scan_result(_ scanres: LNScanResult) {
    267         switch scanres {
    268         case .lightning(let decode):
    269             self.active_sheet = .pay(decode)
    270         case .lnlink:
    271             print("got a lnlink, not an invoice")
    272             // TODO: report that this is an lnlink, not an invoice
    273         case .lnurl(let lnurl):
    274             let decode: DecodeType = .lnurl(lnurl)
    275             if let auth = is_lnurl_auth(lnurl) {
    276                 self.active_sheet = .auth(auth)
    277             } else {
    278                 self.active_sheet = .pay(decode)
    279             }
    280         }
    281     }
    282 
    283     func handle_scan(_ str: String) {
    284         switch handle_qrcode(str) {
    285         case .left(let err):
    286             print("scan error: \(err)")
    287         case .right(let scanres):
    288             handle_scan_result(scanres)
    289         }
    290     }
    291 
    292     var body: some View {
    293         if is_reset {
    294             SetupView()
    295         } else {
    296             main_content()
    297         }
    298     }
    299 }
    300 
    301 /*
    302 struct ContentView_Previews: PreviewProvider {
    303     static var previews: some View {
    304         Group {
    305             ContentView(info: .empty, lnlink: ln, token: "", funds: .empty)
    306         }
    307     }
    308 }
    309  */
    310 
    311 
    312 func get_clipboard_invoice() -> LNScanResult? {
    313     guard let inv = UIPasteboard.general.string else {
    314         return nil
    315     }
    316 
    317     switch handle_qrcode(inv) {
    318     case .left:
    319         return nil
    320     case .right(let scanres):
    321         return scanres
    322     }
    323 }