commit dce24dd0f721a807e39281ce122d0001782141c6
parent 8b307f8a359f39bc58b78d874c8a09e8d3460a0c
Author: William Casarin <jb55@jb55.com>
Date: Fri, 25 Feb 2022 11:22:39 -0800
detect invoices in clipboard
Signed-off-by: William Casarin <jb55@jb55.com>
Diffstat:
4 files changed, 154 insertions(+), 42 deletions(-)
diff --git a/lightninglink/ContentView.swift b/lightninglink/ContentView.swift
@@ -13,6 +13,17 @@ extension Notification.Name {
}
}
+enum ActiveAlert: Identifiable {
+ var id: String {
+ switch self {
+ case .pay:
+ return "pay"
+ }
+ }
+
+ case pay(InvoiceAmount, String)
+}
+
enum ActiveSheet: Identifiable {
var id: String {
switch self {
@@ -24,7 +35,7 @@ enum ActiveSheet: Identifiable {
}
case qr
- case pay(Int64, String)
+ case pay(InvoiceAmount, String)
}
struct Funds {
@@ -51,7 +62,9 @@ struct Funds {
struct ContentView: View {
@State private var info: GetInfo
- @State private var activeSheet: ActiveSheet?
+ @State private var active_sheet: ActiveSheet?
+ @State private var active_alert: ActiveAlert?
+ @State private var has_alert: Bool
@State private var last_pay: Pay?
@State private var funds: Funds
@@ -60,6 +73,7 @@ struct ContentView: View {
init(info: GetInfo, lnlink: LNLink, funds: ListFunds) {
self.info = info
self.lnlink = lnlink
+ self.has_alert = false
self.funds = Funds.from_listfunds(fs: funds)
}
@@ -86,6 +100,17 @@ struct ContentView: View {
return "-\(pay.msatoshi) msats (\(pay.msatoshi_sent) msats sent)"
}
+ func check_pay() {
+ guard let (amt, inv) = get_clipboard_invoice() else {
+ self.active_sheet = .qr
+ self.has_alert = false
+ return
+ }
+
+ self.has_alert = true
+ self.active_alert = .pay(amt, inv)
+ }
+
var body: some View {
VStack {
Group {
@@ -106,13 +131,29 @@ struct ContentView: View {
Spacer()
HStack {
Spacer()
- Button("Pay",
- action: { self.activeSheet = .qr })
+ Button("Pay", action: check_pay)
.font(.title)
.padding()
}
}
- .sheet(item: $activeSheet) { sheet in
+ .alert("Invoice found in clipboard", isPresented: $has_alert, presenting: active_alert, actions: { alert in
+ Button("Cancel") {
+ self.has_alert = false
+ }
+ Button("Use QR Instead") {
+ self.active_sheet = .qr
+ }
+ Button("Yes") {
+ self.active_alert = nil
+ switch alert {
+ case .pay(let amt, let inv):
+ self.active_sheet = .pay(amt, inv)
+ }
+ }
+ }, message: { alert in
+ Text("There is an invoice in your clipboard, should we use that for payment?")
+ })
+ .sheet(item: $active_sheet) { sheet in
switch sheet {
case .qr:
QRScanner() { code in
@@ -125,15 +166,16 @@ struct ContentView: View {
guard let parsed = m_parsed else {
return
}
- self.activeSheet = .pay(parsed, invstr)
+ self.active_sheet = .pay(parsed, invstr)
}
case .pay(let amt, let raw):
PayView(invoice_str: raw, amount: amt, lnlink: self.lnlink)
}
}
- .onReceive(NotificationCenter.default.publisher(for: .sentPayment)) { payment in
+ .onReceive(NotificationCenter.default.publisher(for: .sentPayment)) { payment in
last_pay = payment.object as! Pay
+ self.active_sheet = nil
refresh_funds()
}
}
@@ -148,3 +190,16 @@ struct ContentView_Previews: PreviewProvider {
}
}
*/
+
+
+func get_clipboard_invoice() -> (InvoiceAmount, String)? {
+ guard let inv = UIPasteboard.general.string else {
+ return nil
+ }
+
+ guard let amt = parseInvoiceAmount(inv) else {
+ return nil
+ }
+
+ return (amt, inv)
+}
diff --git a/lightninglink/Invoice.swift b/lightninglink/Invoice.swift
@@ -8,8 +8,16 @@
import Foundation
-public func parseInvoiceAmount(_ inv: String) -> Int64?
+public enum InvoiceAmount {
+ case amount(Int64)
+ case any
+}
+
+// this is just a quick stopgap before we have full invoice parsing
+public func parseInvoiceAmount(_ invoice: String) -> InvoiceAmount?
{
+ let inv = invoice.lowercased()
+
if !inv.starts(with: "lnbc") {
return nil
}
@@ -17,6 +25,7 @@ public func parseInvoiceAmount(_ inv: String) -> Int64?
var ind = 4
var num: String = ""
var scale: Character = Character("p")
+ var sep: Character
// number part
while true {
@@ -28,8 +37,15 @@ public func parseInvoiceAmount(_ inv: String) -> Int64?
} else {
let start_ind = inv.index(inv.startIndex, offsetBy: 4)
let end_ind = inv.index(inv.startIndex, offsetBy: ind - 1)
+
scale = inv[inv.index(inv.startIndex, offsetBy: ind - 1)]
+ sep = inv[inv.index(inv.startIndex, offsetBy: ind)]
num = String(inv[start_ind..<end_ind])
+
+ if sep != "1" {
+ return .any
+ }
+
break
}
}
@@ -43,10 +59,10 @@ public func parseInvoiceAmount(_ inv: String) -> Int64?
}
switch scale {
- case "m": return Int64(n * 100000000);
- case "u": return Int64(n * 100000);
- case "n": return Int64(n * 100);
- case "p": return Int64(n * 1);
+ case "m": return .amount(Int64(n * 100000000));
+ case "u": return .amount(Int64(n * 100000));
+ case "n": return .amount(Int64(n * 100));
+ case "p": return .amount(Int64(n * 1));
default: return nil
}
}
diff --git a/lightninglink/PayView.swift b/lightninglink/PayView.swift
@@ -7,24 +7,18 @@
import SwiftUI
-func render_amount(_ amount: Int64) -> String {
- if amount < 1000 {
- return "\(amount) msats"
- }
-
- return "\(amount / 1000) sats"
-}
struct PayView: View {
var invoice_str: String
- var amount: Int64
+ var amount: InvoiceAmount
var lnlink: LNLink
+
@State var pay_result: Pay?
@State var error: String?
@Environment(\.presentationMode) var presentationMode
- init(invoice_str: String, amount: Int64, lnlink: LNLink) {
+ init(invoice_str: String, amount: InvoiceAmount, lnlink: LNLink) {
self.invoice_str = invoice_str
self.amount = amount
self.lnlink = lnlink
@@ -53,7 +47,7 @@ struct PayView: View {
.font(.largeTitle)
Spacer()
Text("Pay")
- Text("\(render_amount(self.amount))?")
+ Text("\(render_amount(self.amount))")
.font(.title)
Text("\(self.error ?? "")")
Spacer()
@@ -66,32 +60,18 @@ struct PayView: View {
Spacer()
Button("Confirm") {
- // do a fresh connection for each payment
- let ln = LNSocket()
-
- guard ln.connect_and_init(node_id: self.lnlink.node_id, host: self.lnlink.host) else {
- self.error = "Failed to connect, please try again!"
- return
- }
-
- let res = rpc_pay(
- ln: ln,
- token: lnlink.token,
- bolt11: self.invoice_str,
- amount_msat: nil)
+ let res = confirm_payment(bolt11: self.invoice_str, lnlink: self.lnlink)
switch res {
- case .failure(let req_err):
- // handle error
- self.error = req_err.description
+ case .left(let err):
+ self.error = err
- case .success(let pay):
- self.error = nil
+ case .right(let pay):
print(pay)
self.dismiss()
NotificationCenter.default.post(name: .sentPayment, object: pay)
}
- }
+ }
.font(.title)
}
}
@@ -110,3 +90,50 @@ struct PayView_Previews: PreviewProvider {
*/
+
+public enum Either<L, R> {
+ case left(L)
+ case right(R)
+}
+
+func confirm_payment(bolt11: String, lnlink: LNLink) -> Either<String, Pay> {
+ // do a fresh connection for each payment
+ let ln = LNSocket()
+
+ guard ln.connect_and_init(node_id: lnlink.node_id, host: lnlink.host) else {
+ return .left("Failed to connect, please try again!")
+ }
+
+ let res = rpc_pay(
+ ln: ln,
+ token: lnlink.token,
+ bolt11: bolt11,
+ amount_msat: nil)
+
+ switch res {
+ case .failure(let req_err):
+ // handle error
+ return .left(req_err.description)
+
+ case .success(let pay):
+ return .right(pay)
+ }
+}
+
+
+func render_amount(_ amt: InvoiceAmount) -> String {
+ switch amt {
+ case .any:
+ return "Enter amount"
+ case .amount(let amt):
+ return "\(render_amount_msats(amt))?"
+ }
+}
+
+func render_amount_msats(_ amount: Int64) -> String {
+ if amount < 1000 {
+ return "\(amount) msats"
+ }
+
+ return "\(amount / 1000) sats"
+}
diff --git a/lightninglinkTests/lightninglinkTests.swift b/lightninglinkTests/lightninglinkTests.swift
@@ -12,13 +12,27 @@ class lightninglinkTests: XCTestCase {
override func setUpWithError() throws {
// Put setup code here. This method is called before the invocation of each test method in the class.
+ XCTAssert(false)
}
override func tearDownWithError() throws {
// Put teardown code here. This method is called after the invocation of each test method in the class.
}
- func testExample() throws {
+ func testAnyAmountParsesOk() throws {
+ let inv = "lnbc1p3psxjypp5335lq3qyr4vaexez53yxac5jfatdavwyq5eskkkvnrx6yw9j75vsdqvw3jhxarpdeusxqyjw5qcqpjsp5z65t0t70q4e6yp0t2rcajwslkz6uqmaw2eu5s3fkdfgaf5sdm7vsrzjqv7cv43pj3u8qy38rxwt6mm8qv6u34qg4y4w3zuk93yafhqws0sz2z2z0yqq40qqqqqqqqlgqqqqqeqqjq9qyyssqd432fhw3shf0l3zy0l3ku3xv8re6lhaayeyr8u0ayfcy46348vrzjsa46j7prz70l34wklyennpk7dzsw8eqacde74z92jylvevvdhgpzcxhyn"
+
+ let mamt = parseInvoiceAmount(inv)
+
+ XCTAssert(mamt != nil)
+ let amt = mamt!
+
+ switch amt {
+ case .amount(let _):
+ XCTAssert(false)
+ case .any:
+ XCTAssert(true)
+ }
// This is an example of a functional test case.
// Use XCTAssert and related functions to verify your tests produce the correct results.
}