Skip to content

Commit

Permalink
Provides a qr view to scan for nsec. Completes: #1291
Browse files Browse the repository at this point in the history
- Allow scanning of QR codes, and if detects a nsec, will provide it to the login prompt.
- If nsec is found, provides option to keep nsec in keychain; default is to not store
- User stays logged in until they logout, or app is force-quit if nsec is not stored.

damusApp.swift:
Obtains keypair from the notification generated to allow login.

LoginView.swift:
New views allowing for adding and logic handling the QR reader in QRScanNSECView.swift to enable QR scan for nsec.

QRScanNSECView.swift:
New view to scan for QR code. The sparkling magnifying glass is enable if the view calling the QR view changes the privKeyFound bound variable.

npub1el277q4kesp8vhs7rq6qkwnhpxfp345u7tnuxykwr67d9wg0wvyslam5n0

Signed-off-by: Jericho Hasselbush <[email protected]>
Signed-off-by: William Casarin <[email protected]>
  • Loading branch information
jerihass authored and jb55 committed Oct 1, 2023
1 parent a368a01 commit c10f612
Show file tree
Hide file tree
Showing 3 changed files with 252 additions and 35 deletions.
149 changes: 114 additions & 35 deletions damus/Views/LoginView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
//

import SwiftUI
import AudioToolbox // For haptic feedback on scanning of nsec qr code

enum ParsedKey {
case pub(Pubkey)
Expand All @@ -30,13 +31,21 @@ enum ParsedKey {
}
return false
}

var is_priv: Bool {
if case .priv = self {
return true
}
return false
}
}

struct LoginView: View {
@State var key: String = ""
@State var is_pubkey: Bool = false
@State var error: String? = nil
@State private var credential_handler = CredentialHandler()
@State private var shouldSaveKey: Bool = true
var nav: NavigationCoordinator

func get_error(parsed_key: ParsedKey?) -> String? {
Expand All @@ -57,7 +66,7 @@ struct LoginView: View {
SignInHeader()
.padding(.top, 100)

SignInEntry(key: $key)
SignInEntry(key: $key, shouldSaveKey: $shouldSaveKey)

let parsed = parse_key(key)

Expand All @@ -83,7 +92,7 @@ struct LoginView: View {
Button(action: {
Task {
do {
try await process_login(p, is_pubkey: is_pubkey)
try await process_login(p, is_pubkey: is_pubkey, shouldSaveKey: shouldSaveKey)
} catch {
self.error = error.localizedDescription
}
Expand Down Expand Up @@ -168,37 +177,39 @@ enum LoginError: LocalizedError {
}
}

func process_login(_ key: ParsedKey, is_pubkey: Bool) async throws {
switch key {
case .priv(let priv):
try handle_privkey(priv)
case .pub(let pub):
try clear_saved_privkey()
save_pubkey(pubkey: pub)

case .nip05(let id):
guard let nip05 = await get_nip05_pubkey(id: id) else {
throw LoginError.nip05_failed
}

// this is a weird way to login anyways
/*
var bootstrap_relays = load_bootstrap_relays(pubkey: nip05.pubkey)
for relay in nip05.relays {
if !(bootstrap_relays.contains { $0 == relay }) {
bootstrap_relays.append(relay)
}
}
*/
save_pubkey(pubkey: nip05.pubkey)

case .hex(let hexstr):
if is_pubkey, let pubkey = hex_decode_pubkey(hexstr) {
func process_login(_ key: ParsedKey, is_pubkey: Bool, shouldSaveKey: Bool = true) async throws {
if shouldSaveKey {
switch key {
case .priv(let priv):
try handle_privkey(priv)
case .pub(let pub):
try clear_saved_privkey()
save_pubkey(pubkey: pub)

case .nip05(let id):
guard let nip05 = await get_nip05_pubkey(id: id) else {
throw LoginError.nip05_failed
}

save_pubkey(pubkey: pubkey)
} else if let privkey = hex_decode_privkey(hexstr) {
try handle_privkey(privkey)
// this is a weird way to login anyways
/*
var bootstrap_relays = load_bootstrap_relays(pubkey: nip05.pubkey)
for relay in nip05.relays {
if !(bootstrap_relays.contains { $0 == relay }) {
bootstrap_relays.append(relay)
}
}
*/
save_pubkey(pubkey: nip05.pubkey)

case .hex(let hexstr):
if is_pubkey, let pubkey = hex_decode_pubkey(hexstr) {
try clear_saved_privkey()

save_pubkey(pubkey: pubkey)
} else if let privkey = hex_decode_privkey(hexstr) {
try handle_privkey(privkey)
}
}
}

Expand All @@ -213,7 +224,16 @@ func process_login(_ key: ParsedKey, is_pubkey: Bool) async throws {
save_pubkey(pubkey: pk)
}

guard let keypair = get_saved_keypair() else {
func handle_transient_privkey(_ key: ParsedKey) -> Keypair? {
if case let .priv(priv) = key, let pubkey = privkey_to_pubkey(privkey: priv) {
return Keypair(pubkey: pubkey, privkey: priv)
}
return nil
}

let keypair = shouldSaveKey ? get_saved_keypair() : handle_transient_privkey(key)

guard let keypair = keypair else {
return
}

Expand Down Expand Up @@ -265,11 +285,15 @@ func get_nip05_pubkey(id: String) async -> NIP05User? {
struct KeyInput: View {
let title: String
let key: Binding<String>
let shouldSaveKey: Binding<Bool>
var privKeyFound: Binding<Bool>
@State private var is_secured: Bool = true

init(_ title: String, key: Binding<String>) {
init(_ title: String, key: Binding<String>, shouldSaveKey: Binding<Bool>, privKeyFound: Binding<Bool>) {
self.title = title
self.key = key
self.shouldSaveKey = shouldSaveKey
self.privKeyFound = privKeyFound
}

var body: some View {
Expand All @@ -281,6 +305,8 @@ struct KeyInput: View {
self.key.wrappedValue = pastedkey
}
}
SignInScan(shouldSaveKey: shouldSaveKey, loginKey: key, privKeyFound: privKeyFound)

if is_secured {
SecureField("", text: key)
.nsecLoginStyle(key: key.wrappedValue, title: title)
Expand Down Expand Up @@ -323,16 +349,69 @@ struct SignInHeader: View {

struct SignInEntry: View {
let key: Binding<String>

let shouldSaveKey: Binding<Bool>
@State private var privKeyFound: Bool = false
var body: some View {
VStack(alignment: .leading) {
Text("Enter your account key", comment: "Prompt for user to enter an account key to login.")
.fontWeight(.medium)
.padding(.top, 30)

KeyInput(NSLocalizedString("nsec1...", comment: "Prompt for user to enter in an account key to login. This text shows the characters the key could start with if it was a private key."), key: key)
KeyInput(NSLocalizedString("nsec1...", comment: "Prompt for user to enter in an account key to login. This text shows the characters the key could start with if it was a private key."),
key: key,
shouldSaveKey: shouldSaveKey,
privKeyFound: $privKeyFound)
if privKeyFound {
Toggle("Save Key in Secure Keychain", isOn: shouldSaveKey)
}
}
}
}

struct SignInScan: View {
@State var showQR: Bool = false
@State var qrkey: ParsedKey?
@Binding var shouldSaveKey: Bool
@Binding var loginKey: String
@Binding var privKeyFound: Bool
var body: some View {
VStack {
Button(action: { showQR.toggle() }, label: {
Image(systemName: "qrcode.viewfinder")})
.foregroundColor(.gray)

}
.sheet(isPresented: $showQR, onDismiss: {
if qrkey == nil { resetView() }}
) {
QRScanNSECView(showQR: $showQR,
privKeyFound: $privKeyFound,
scannedTextHandler: { handleQRString($0) })
}
.onChange(of: showQR) { show in
if showQR { resetView() }
}
}

func handleQRString(_ string: String) {
qrkey = parse_key(string)
if let key = qrkey, key.is_priv {
loginKey = string
privKeyFound = true
shouldSaveKey = false
AudioServicesPlaySystemSound(SystemSoundID(kSystemSoundID_Vibrate))
// TODO: Do we want to keep haptic feedback
// This should use the single tap or something instead of this crazy
// vibrating thing.
}
}

func resetView() {
loginKey = ""
qrkey = nil
privKeyFound = false
shouldSaveKey = true
}
}

struct CreateAccountPrompt: View {
Expand Down
135 changes: 135 additions & 0 deletions damus/Views/QRScanNSECView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
//
// QRScanNSECView.swift
// damus
//
// Created by Jericho Hasselbush on 9/29/23.
//

import SwiftUI
import VisionKit

struct QRScanNSECView: View {
@Binding var showQR: Bool
@Binding var privKeyFound: Bool
var scannedTextHandler: (String) -> Void

var body: some View {
ZStack {
ZStack {
DamusGradient()
}
VStack {
Text("Scan Your Private Key QR", comment: "Text to prompt scanning a QR code of a user's privkey to login to their profile.")
.padding(.top, 50)
.font(.system(size: 24, weight: .heavy))

Spacer()

if QRViewController.scannerAvailable {
QRViewController(scannedTextHandler)
.scaledToFit()
.frame(width: 300, height: 300)
.cornerRadius(10)
.overlay(RoundedRectangle(cornerRadius: 10).stroke(DamusColors.white, lineWidth: 5.0))
.shadow(radius: 10)
} else {
ScannerEmptyView()
}

Button(action: { withAnimation(.bouncy(duration: 2.5, extraBounce: 2.5)) { showQR = false }}) {
VStack {
Image(systemName: privKeyFound ? "sparkle.magnifyingglass" : "magnifyingglass")
.font(privKeyFound ? .title : .title3)
}}
.padding(.top)
.buttonStyle(GradientButtonStyle())

Spacer()

Spacer()
}
}
}

func ScannerEmptyView() -> some View {
VStack {
if #available(iOS 17.0, macOS 14.0, *) {
ContentUnavailableView("No native support for QR Code Scanning", systemImage: "exclamationmark.triangle")
} else {
HStack {
Spacer()
VStack {
Image(systemName: "exclamationmark.triangle")
Text("No native support for QR Code Scanning", comment: "Device doesn't support scanning QR codes.")
.multilineTextAlignment(.center)
}
.font(.largeTitle)
.foregroundColor(.primary)
Spacer()
}
}
}

}
}

#Preview {
@State var showQR = true
@State var privKeyFound = false
@State var shouldSaveKey = true
return QRScanNSECView(showQR: $showQR,
privKeyFound: $privKeyFound,
scannedTextHandler: { _ in })
}


typealias RecognizedItemHandler = (String) -> Void

@MainActor
struct QRViewController: UIViewControllerRepresentable {
typealias UIViewControllerType = DataScannerViewController
var delegate: QRViewControllerDelegate

init(_ itemHandler: @escaping RecognizedItemHandler) {
self.delegate = QRViewControllerDelegate(itemHandler)
}

func makeUIViewController(context: Context) -> DataScannerViewController {
let controller = DataScannerViewController(recognizedDataTypes: [.barcode(symbologies: [.qr])],
qualityLevel: .accurate,
recognizesMultipleItems: false,
isHighFrameRateTrackingEnabled: true,
isPinchToZoomEnabled: true,
isGuidanceEnabled: true,
isHighlightingEnabled: true)
try? controller.startScanning()
controller.delegate = delegate
return controller
}

func updateUIViewController(_ uiViewController: DataScannerViewController, context: Context) {

}

static var scannerAvailable: Bool {
DataScannerViewController.isSupported &&
DataScannerViewController.isAvailable
}
}

class QRViewControllerDelegate: DataScannerViewControllerDelegate {
var handleItem: RecognizedItemHandler

init(_ handleItem: @escaping RecognizedItemHandler) {
self.handleItem = handleItem
}

func dataScanner(_ dataScanner: DataScannerViewController, didAdd addedItems: [RecognizedItem], allItems: [RecognizedItem]) {
guard let item = addedItems.first else { return }
if case let .barcode(barcode) = item {
let string = barcode.payloadStringValue ?? ""
self.handleItem(string)

}
}
}
3 changes: 3 additions & 0 deletions damus/damusApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ struct MainView: View {
.onReceive(handle_notify(.login)) { notif in
needs_setup = false
keypair = get_saved_keypair()
if keypair == nil, let tempkeypair = notif.to_full()?.to_keypair() {
keypair = tempkeypair
}
}
}
}
Expand Down

0 comments on commit c10f612

Please sign in to comment.