lnlink

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

commit b8b6c1e4448354a060a4abb0cfefd24dbacb2d7d
parent 4515b95d7cc856f939ae921f6d4d6a2e3d6b7c6c
Author: William Casarin <jb55@jb55.com>
Date:   Tue, 29 Mar 2022 17:30:14 -0700

ReceiveView: custom description and amount

Signed-off-by: William Casarin <jb55@jb55.com>

Diffstat:
Mlightninglink/Views/AmountInputView.swift | 62++++++++++++++++++++++++++++++++++++++++++--------------------
Mlightninglink/Views/ContentView.swift | 2+-
Mlightninglink/Views/PayView.swift | 55++++++++++++++++++++++++++++---------------------------
Mlightninglink/Views/ReceiveView.swift | 124++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----------------
4 files changed, 168 insertions(+), 75 deletions(-)

diff --git a/lightninglink/Views/AmountInputView.swift b/lightninglink/Views/AmountInputView.swift @@ -43,38 +43,40 @@ func get_preferred_denominations() -> [Denomination] { return [.bitcoin(btc_pref), .fiat(fiat_pref)] } +struct ParsedAmount { + let msats_str: String? + let msats: Int64? + + static var empty: ParsedAmount { + ParsedAmount(msats_str: nil, msats: nil) + } +} + struct AmountInput: View { @State var amount_msat: Int64? = nil let text: Binding<String> - let rate: ExchangeRate? - let onReceive: (String) -> Int64? + let placeholder: String + let onReceive: (ParsedAmount) -> () var body: some View { VStack { - Form { - HStack(alignment: .lastTextBaseline) { - TextField("10,000", text: self.text) - .font(.title) - .keyboardType(.numberPad) - .multilineTextAlignment(.trailing) - .onReceive(Just(self.text)) { - amount_msat = onReceive($0.wrappedValue) - } - Text("sats") - } + HStack(alignment: .lastTextBaseline) { + TextField(placeholder, text: self.text) + .font(.title) + .keyboardType(.numberPad) + .multilineTextAlignment(.trailing) + .onReceive(Just(self.text)) { + onReceive(parse_msat_input($0.wrappedValue)) + } + Text("sats") } - .frame(height: 100) - if let msats = amount_msat { - if let rate = self.rate { - Text("about \(sats_to_fiat(msats: msats, xr: rate))") - .foregroundColor(.gray) - } - } } } } + + func sats_to_fiat(msats: Int64, xr: ExchangeRate) -> String { let btc = Double(msats) / Double(100_000_000_000) let rate = xr.rate * btc @@ -82,3 +84,23 @@ func sats_to_fiat(msats: Int64, xr: ExchangeRate) -> String { } +func parse_msat_input(_ new_val: String) -> ParsedAmount { + if new_val == "" { + return ParsedAmount(msats_str: "", msats: nil) + } + + let ok = new_val.allSatisfy { $0 == "," || ($0 >= "0" && $0 <= "9") } + if ok { + let num_fmt = NumberFormatter() + num_fmt.numberStyle = .decimal + + let filtered = new_val.filter { $0 >= "0" && $0 <= "9" } + let sats = Int64(filtered) ?? 0 + let msats = sats * 1000 + let ret = num_fmt.string(from: NSNumber(value: sats)) ?? new_val + return ParsedAmount(msats_str: ret, msats: msats) + } + + return .empty +} + diff --git a/lightninglink/Views/ContentView.swift b/lightninglink/Views/ContentView.swift @@ -219,7 +219,7 @@ struct ContentView: View { } case .receive: - ReceiveView(lnlink: lnlink) + ReceiveView(lnlink: lnlink, rate: self.rate) case .pay(let decode): PayView(decode: decode, lnlink: self.lnlink, rate: self.rate) diff --git a/lightninglink/Views/PayView.swift b/lightninglink/Views/PayView.swift @@ -79,6 +79,8 @@ public enum PayState { case invoice_request(RequestInvoice) } +let default_placeholder = "10,000" + struct PayView: View { let init_decode_type: DecodeType let lnlink: LNLink @@ -97,6 +99,7 @@ struct PayView: View { @State var paying: Bool = false @Environment(\.presentationMode) var presentationMode + @Environment (\.colorScheme) var colorScheme: ColorScheme init(decode: DecodeType, lnlink: LNLink, rate: ExchangeRate?) { self.init_decode_type = decode @@ -232,28 +235,6 @@ struct PayView: View { return self.paying || (self.error == nil && is_ready(self.state) == nil) } - func handle_custom_receive(_ new_val: String) -> Int64? { - if new_val == "" { - self.custom_amount_input = "" - return nil - } - - let ok = new_val.allSatisfy { $0 == "," || ($0 >= "0" && $0 <= "9") } - if ok { - let num_fmt = NumberFormatter() - num_fmt.numberStyle = .decimal - - let filtered = new_val.filter { $0 >= "0" && $0 <= "9" } - let sats = Int64(filtered) ?? 0 - let msats = sats * 1000 - self.custom_amount_input = num_fmt.string(from: NSNumber(value: sats)) ?? new_val - self.custom_amount_msats = msats - return msats - } - - return nil - } - func tip_percent(_ tip: TipSelection) { if tip == self.current_tip { self.current_tip = .none @@ -336,8 +317,14 @@ struct PayView: View { Text("\(render_amount_msats(amt))") .font(.title) } else { - AmountInput(text: $custom_amount_input, rate: rate) { - let msats = handle_custom_receive($0) + AmountInput(text: $custom_amount_input, placeholder: default_placeholder) { result in + if let str = result.msats_str { + self.custom_amount_input = str + } + if let msats = result.msats { + self.custom_amount_msats = msats + } + if self.custom_amount_input != "" { if self.custom_amount_msats < min_amt { self.error = "Amount not allowed, must be higher than \(render_amount_msats(min_amt))" @@ -349,7 +336,6 @@ struct PayView: View { } } } - return msats } } @@ -359,15 +345,30 @@ struct PayView: View { Text("\(render_amount_msats(amt))") .font(.title) } else { - AmountInput(text: $custom_amount_input, rate: rate) { - handle_custom_receive($0) + Form { + AmountInput(text: $custom_amount_input, placeholder: default_placeholder) { parsed in + if let str = parsed.msats_str { + self.custom_amount_input = str + } + if let msats = parsed.msats { + self.custom_amount_msats = msats + } + } } + .frame(height: 100) } case .amount(let amt): Text("\(render_amount_msats(amt))") .font(.title) } + + if self.custom_amount_input != "", let msats = self.custom_amount_msats { + if let rate = self.rate { + Text("\(sats_to_fiat(msats: msats, xr: rate))") + .foregroundColor(.gray) + } + } } } diff --git a/lightninglink/Views/ReceiveView.swift b/lightninglink/Views/ReceiveView.swift @@ -8,16 +8,32 @@ import SwiftUI import AVFoundation import CoreImage.CIFilterBuiltins +import Combine + +struct QRData { + let img: Image + let data: String +} struct ReceiveView: View { @State private var loading: Bool = true - @State private var qr: Image? = nil - @State private var qr_data: String? = nil + @State private var qr_data: QRData? = nil + @State private var description: String = "" + @State private var amount: Int64? = nil + @State private var amount_str: String = "" + @State private var making: Bool = false + @FocusState private var is_kb_focused: Bool let lnlink: LNLink + let rate: ExchangeRate? @Environment(\.presentationMode) var presentationMode + var form: some View { + ProgressView() + .progressViewStyle(.circular) + } + var body: some View { VStack { Text("Receive payment") @@ -25,40 +41,78 @@ struct ReceiveView: View { Spacer() - if let qr = self.qr { - qr - .resizable() - .scaledToFit() - .frame(width: 300, height: 300) - .onTapGesture { - AudioServicesPlaySystemSound(SystemSoundID(kSystemSoundID_Vibrate)) - UIPasteboard.general.string = self.qr_data - } + if let qr = self.qr_data { + qrcode_view(qr) } else { - ProgressView() - .progressViewStyle(.circular) + if making { + ProgressView() + .progressViewStyle(.circular) + } else { + Form { + TextField("Description", text: $description) + .font(.body) + .focused($is_kb_focused) + + Section { + AmountInput(text: $amount_str, placeholder: "any") { parsed in + if let str = parsed.msats_str { + self.amount_str = str + } + if let msats = parsed.msats { + self.amount = msats + } + } + .focused($is_kb_focused) + + } + + } + .frame(height: 200) + + if self.amount_str != "", let msats = self.amount { + if let rate = self.rate { + Text("\(sats_to_fiat(msats: msats, xr: rate))") + .foregroundColor(.gray) + } + } + + } } Spacer() HStack { - Button("Back") { - dismiss() + if !is_kb_focused { + Button("Back") { + dismiss() + } + } + Spacer() + + if !self.making && self.qr_data == nil { + Button("Receive") { + self.making = true + make_invoice(lnlink: lnlink, expiry: "12h", description: self.description, amount: self.amount) { res in + self.making = false + switch res { + case .failure: + break + case .success(let invres): + let img = generate_qr(from: invres.bolt11) + self.qr_data = QRData(img: img, data: invres.bolt11) + } + } + } + .font(.title) + } } } .padding() - .onAppear() { - make_invoice(lnlink: lnlink, expiry: "12h") { res in - switch res { - case .failure: - break - case .success(let invres): - self.qr = generate_qr(from: invres.bolt11) - self.qr_data = invres.bolt11 - } - } + .onTapGesture { + UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), + to: nil, from: nil, for: nil) } } @@ -85,7 +139,7 @@ func generate_qr(from string: String) -> Image { return Image(uiImage: uiimg) } -func make_invoice(lnlink: LNLink, expiry: String, callback: @escaping (RequestRes<InvoiceRes>) -> ()) { +func make_invoice(lnlink: LNLink, expiry: String, description: String?, amount: Int64?, callback: @escaping (RequestRes<InvoiceRes>) -> ()) { let ln = LNSocket() ln.genkey() @@ -93,8 +147,24 @@ func make_invoice(lnlink: LNLink, expiry: String, callback: @escaping (RequestRe return } + DispatchQueue.global(qos: .background).async { - let res = rpc_invoice(ln: ln, token: lnlink.token, amount: .any, description: "lnlink invoice", expiry: "12h") + var amt: InvoiceAmount = .any + if let a = amount { + amt = .amount(a) + } + let res = rpc_invoice(ln: ln, token: lnlink.token, amount: amt, description: description ?? "lnlink invoice", expiry: "12h") callback(res) } } + +func qrcode_view(_ qrd: QRData) -> some View { + qrd.img + .resizable() + .scaledToFit() + .frame(width: 300, height: 300) + .onTapGesture { + AudioServicesPlaySystemSound(SystemSoundID(kSystemSoundID_Vibrate)) + UIPasteboard.general.string = qrd.data + } +}