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 }