diff --git a/damus/Views/LoginView.swift b/damus/Views/LoginView.swift index d5901f222..6dafc5c0c 100644 --- a/damus/Views/LoginView.swift +++ b/damus/Views/LoginView.swift @@ -6,6 +6,7 @@ // import SwiftUI +import AudioToolbox // For haptic feedback on scanning of nsec qr code enum ParsedKey { case pub(Pubkey) @@ -30,6 +31,13 @@ enum ParsedKey { } return false } + + var is_priv: Bool { + if case .priv = self { + return true + } + return false + } } struct LoginView: View { @@ -37,6 +45,7 @@ struct LoginView: View { @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? { @@ -57,7 +66,7 @@ struct LoginView: View { SignInHeader() .padding(.top, 100) - SignInEntry(key: $key) + SignInEntry(key: $key, shouldSaveKey: $shouldSaveKey) let parsed = parse_key(key) @@ -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 } @@ -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) + } } } @@ -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 } @@ -265,11 +285,15 @@ func get_nip05_pubkey(id: String) async -> NIP05User? { struct KeyInput: View { let title: String let key: Binding + let shouldSaveKey: Binding + var privKeyFound: Binding @State private var is_secured: Bool = true - init(_ title: String, key: Binding) { + init(_ title: String, key: Binding, shouldSaveKey: Binding, privKeyFound: Binding) { self.title = title self.key = key + self.shouldSaveKey = shouldSaveKey + self.privKeyFound = privKeyFound } var body: some View { @@ -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) @@ -323,16 +349,69 @@ struct SignInHeader: View { struct SignInEntry: View { let key: Binding - + let shouldSaveKey: Binding + @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 { diff --git a/damus/Views/QRScanNSECView.swift b/damus/Views/QRScanNSECView.swift new file mode 100644 index 000000000..66e6de6ad --- /dev/null +++ b/damus/Views/QRScanNSECView.swift @@ -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) + + } + } +} diff --git a/damus/damusApp.swift b/damus/damusApp.swift index 7300b1b0e..a629200a9 100644 --- a/damus/damusApp.swift +++ b/damus/damusApp.swift @@ -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 + } } } }