lnlink

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

commit 6d3cf24117a5b718596267b6a371c1622a549117
parent 4e58d38101f11328a4e7cdec05a7541d7a0ccaf7
Author: William Casarin <jb55@jb55.com>
Date:   Sat,  5 Feb 2022 10:23:35 -0800

pay qr code scanner

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

Diffstat:
Mlightninglink.xcodeproj/project.pbxproj | 8++++++++
Mlightninglink/ContentView.swift | 9+++++++++
Alightninglink/Info.plist | 8++++++++
Alightninglink/QRScan.swift | 122+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
4 files changed, 147 insertions(+), 0 deletions(-)

diff --git a/lightninglink.xcodeproj/project.pbxproj b/lightninglink.xcodeproj/project.pbxproj @@ -7,6 +7,7 @@ objects = { /* Begin PBXBuildFile section */ + 4C0359FB27AEE86600FF92CE /* QRScan.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C0359FA27AEE86600FF92CE /* QRScan.swift */; }; 4C641D192788FF2F002A36C9 /* lightninglinkApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C641D182788FF2F002A36C9 /* lightninglinkApp.swift */; }; 4C641D1B2788FF2F002A36C9 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C641D1A2788FF2F002A36C9 /* ContentView.swift */; }; 4C641D1D2788FF30002A36C9 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 4C641D1C2788FF30002A36C9 /* Assets.xcassets */; }; @@ -41,6 +42,8 @@ /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ + 4C0359FA27AEE86600FF92CE /* QRScan.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QRScan.swift; sourceTree = "<group>"; }; + 4C0359FE27AEEE8500FF92CE /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = "<group>"; }; 4C641D152788FF2F002A36C9 /* lightninglink.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = lightninglink.app; sourceTree = BUILT_PRODUCTS_DIR; }; 4C641D182788FF2F002A36C9 /* lightninglinkApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = lightninglinkApp.swift; sourceTree = "<group>"; }; 4C641D1A2788FF2F002A36C9 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; }; @@ -117,12 +120,14 @@ 4C641D172788FF2F002A36C9 /* lightninglink */ = { isa = PBXGroup; children = ( + 4C0359FE27AEEE8500FF92CE /* Info.plist */, 4C873FD427A6EF3F008C972C /* LNSocket.swift */, 4C641D182788FF2F002A36C9 /* lightninglinkApp.swift */, 4C641D1A2788FF2F002A36C9 /* ContentView.swift */, 4C641D1C2788FF30002A36C9 /* Assets.xcassets */, 4C641D1E2788FF30002A36C9 /* Preview Content */, 4C873FD627A6F1F5008C972C /* RPC.swift */, + 4C0359FA27AEE86600FF92CE /* QRScan.swift */, ); path = lightninglink; sourceTree = "<group>"; @@ -310,6 +315,7 @@ 4C641D492789083E002A36C9 /* lightninglink.c in Sources */, 4C641D192788FF2F002A36C9 /* lightninglinkApp.swift in Sources */, 4C873FD727A6F1F5008C972C /* RPC.swift in Sources */, + 4C0359FB27AEE86600FF92CE /* QRScan.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -488,6 +494,7 @@ DEVELOPMENT_TEAM = XK7H4JAB3D; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = lightninglink/Info.plist; INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UILaunchScreen_Generation = YES; @@ -524,6 +531,7 @@ DEVELOPMENT_TEAM = XK7H4JAB3D; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = lightninglink/Info.plist; INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UILaunchScreen_Generation = YES; diff --git a/lightninglink/ContentView.swift b/lightninglink/ContentView.swift @@ -9,6 +9,7 @@ import SwiftUI struct ContentView: View { @State private var info: GetInfo + @State private var showingQRScanner = false init(info: GetInfo) { self.info = info @@ -17,10 +18,18 @@ struct ContentView: View { var body: some View { let _self = self VStack { + Button("Pay") { + showingQRScanner = true + } Text(self.info.alias) Text("\(self.info.num_active_channels) active channels") Text("\(self.info.msatoshi_fees_collected / 1000) sats collected in fees") } + .sheet(isPresented: $showingQRScanner) { + QRScanner() { code in + print(code) + } + } } } diff --git a/lightninglink/Info.plist b/lightninglink/Info.plist @@ -0,0 +1,8 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> +<plist version="1.0"> +<dict> + <key>NSCameraUsageDescription</key> + <string>Scanning connection string qr codes</string> +</dict> +</plist> diff --git a/lightninglink/QRScan.swift b/lightninglink/QRScan.swift @@ -0,0 +1,122 @@ +// +// QRScan.swift +// lightninglink +// +// Created by William Casarin on 2022-02-05. +// + +import AVFoundation +import UIKit +import SwiftUI + +struct QRScanner: UIViewControllerRepresentable { + var found: (String) -> Void + + init(found: @escaping (String) -> Void) { + self.found = found + } + + func makeUIViewController(context: Context) -> some UIViewController { + return ScannerViewController().onScan(self.found) + } + + func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) { + } +} + +class ScannerViewController: UIViewController, AVCaptureMetadataOutputObjectsDelegate { + var captureSession: AVCaptureSession! + var previewLayer: AVCaptureVideoPreviewLayer! + var completion: ((String) -> Void)? = nil + + public func onScan(_ found: @escaping (String) -> Void) -> ScannerViewController { + self.completion = found + return self + } + + override func viewDidLoad() { + super.viewDidLoad() + + view.backgroundColor = UIColor.black + captureSession = AVCaptureSession() + + guard let videoCaptureDevice = AVCaptureDevice.default(for: .video) else { return } + let videoInput: AVCaptureDeviceInput + + do { + videoInput = try AVCaptureDeviceInput(device: videoCaptureDevice) + } catch { + return + } + + if (captureSession.canAddInput(videoInput)) { + captureSession.addInput(videoInput) + } else { + failed() + return + } + + let metadataOutput = AVCaptureMetadataOutput() + + if (captureSession.canAddOutput(metadataOutput)) { + captureSession.addOutput(metadataOutput) + + metadataOutput.setMetadataObjectsDelegate(self, queue: DispatchQueue.main) + metadataOutput.metadataObjectTypes = [.qr] + } else { + failed() + return + } + + previewLayer = AVCaptureVideoPreviewLayer(session: captureSession) + previewLayer.frame = view.layer.bounds + previewLayer.videoGravity = .resizeAspectFill + view.layer.addSublayer(previewLayer) + + captureSession.startRunning() + } + + func failed() { + let ac = UIAlertController(title: "Scanning not supported", message: "Your device does not support scanning a code from an item. Please use a device with a camera.", preferredStyle: .alert) + ac.addAction(UIAlertAction(title: "OK", style: .default)) + present(ac, animated: true) + captureSession = nil + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + + if (captureSession?.isRunning == false) { + captureSession.startRunning() + } + } + + override func viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated) + + if (captureSession?.isRunning == true) { + captureSession.stopRunning() + } + } + + func metadataOutput(_ output: AVCaptureMetadataOutput, didOutput metadataObjects: [AVMetadataObject], from connection: AVCaptureConnection) { + captureSession.stopRunning() + + if let metadataObject = metadataObjects.first { + guard let readableObject = metadataObject as? AVMetadataMachineReadableCodeObject else { return } + guard let stringValue = readableObject.stringValue else { return } + AudioServicesPlaySystemSound(SystemSoundID(kSystemSoundID_Vibrate)) + completion?(stringValue) + } + + dismiss(animated: true) + } + + override var prefersStatusBarHidden: Bool { + return true + } + + override var supportedInterfaceOrientations: UIInterfaceOrientationMask { + return .portrait + } +}