commit 439f9974c5e99a7b3626b9a3a02b1ffbf38e6792
parent cf243e39c928f37d59d6078da42d1caab3a597c3
Author: Jericho Hasselbush <jericho@sal-et-lucem.com>
Date: Wed, 11 Oct 2023 08:17:28 -0400
login: add nsec qr-scanning
- 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 enabled
if the view calling the QR view changes the privKeyFound bound variable.
Tipjar: npub1el277q4kesp8vhs7rq6qkwnhpxfp345u7tnuxykwr67d9wg0wvyslam5n0
Closes: https://github.com/damus-io/damus/issues/1291
Changelog-Added: Add QR scan nsec logins.
Signed-off-by: Jericho Hasselbush <jericho@sal-et-lucem.com>
Reviewed-by: William Casarin <jb55@jb55.com>
Signed-off-by: William Casarin <jb55@jb55.com>
Diffstat:
4 files changed, 194 insertions(+), 35 deletions(-)
diff --git a/damus.xcodeproj/project.pbxproj b/damus.xcodeproj/project.pbxproj
@@ -419,6 +419,7 @@
9609F058296E220800069BF3 /* BannerImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9609F057296E220800069BF3 /* BannerImageView.swift */; };
9C83F89329A937B900136C08 /* TextViewWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C83F89229A937B900136C08 /* TextViewWrapper.swift */; };
9CA876E229A00CEA0003B9A3 /* AttachMediaUtility.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9CA876E129A00CE90003B9A3 /* AttachMediaUtility.swift */; };
+ ADFE73552AD4793100EC7326 /* QRScanNSECView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADFE73542AD4793100EC7326 /* QRScanNSECView.swift */; };
BA37598A2ABCCDE40018D73B /* ImageResizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA3759892ABCCDE30018D73B /* ImageResizer.swift */; };
BA37598D2ABCCE500018D73B /* PhotoCaptureProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA37598B2ABCCE500018D73B /* PhotoCaptureProcessor.swift */; };
BA37598E2ABCCE500018D73B /* VideoCaptureProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA37598C2ABCCE500018D73B /* VideoCaptureProcessor.swift */; };
@@ -1113,6 +1114,7 @@
9609F057296E220800069BF3 /* BannerImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BannerImageView.swift; sourceTree = "<group>"; };
9C83F89229A937B900136C08 /* TextViewWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextViewWrapper.swift; sourceTree = "<group>"; };
9CA876E129A00CE90003B9A3 /* AttachMediaUtility.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttachMediaUtility.swift; sourceTree = "<group>"; };
+ ADFE73542AD4793100EC7326 /* QRScanNSECView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = QRScanNSECView.swift; sourceTree = "<group>"; };
BA3759892ABCCDE30018D73B /* ImageResizer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImageResizer.swift; sourceTree = "<group>"; };
BA37598B2ABCCE500018D73B /* PhotoCaptureProcessor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PhotoCaptureProcessor.swift; sourceTree = "<group>"; };
BA37598C2ABCCE500018D73B /* VideoCaptureProcessor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VideoCaptureProcessor.swift; sourceTree = "<group>"; };
@@ -1685,6 +1687,7 @@
4C3AC79E2833115300E1F516 /* FollowButtonView.swift */,
4C3AC79C2833036D00E1F516 /* FollowingView.swift */,
4C90BD17283A9EE5008EE7EF /* LoginView.swift */,
+ ADFE73542AD4793100EC7326 /* QRScanNSECView.swift */,
4C363A8D28236FE4006E126D /* NoteContentView.swift */,
4C75EFAC28049CFB0006080F /* PostButton.swift */,
4C75EFA327FA577B0006080F /* PostView.swift */,
@@ -2564,6 +2567,7 @@
4C4793042A993DC000489948 /* midl.c in Sources */,
4C4793012A993CDA00489948 /* mdb.c in Sources */,
4CE9FBBA2A6B3C63007E485C /* nostrdb.c in Sources */,
+ ADFE73552AD4793100EC7326 /* QRScanNSECView.swift in Sources */,
4C3AC79D2833036D00E1F516 /* FollowingView.swift in Sources */,
5CF72FC229B9142F00124A13 /* ShareAction.swift in Sources */,
4C32B9522A9AD44700DC3548 /* Message.swift in Sources */,
diff --git a/damus/Views/LoginView.swift b/damus/Views/LoginView.swift
@@ -30,6 +30,13 @@ enum ParsedKey {
}
return false
}
+
+ var is_priv: Bool {
+ if case .priv = self {
+ return true
+ }
+ return false
+ }
}
struct LoginView: View {
@@ -37,6 +44,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 +65,7 @@ struct LoginView: View {
SignInHeader()
.padding(.top, 100)
- SignInEntry(key: $key)
+ SignInEntry(key: $key, shouldSaveKey: $shouldSaveKey)
let parsed = parse_key(key)
@@ -83,7 +91,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 +176,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 +223,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 +284,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 {
@@ -281,6 +304,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,18 +348,79 @@ 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
+ let generator = UINotificationFeedbackGenerator()
+
+ 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,
+ codeScannerCompletion: { scannerCompletion($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
+ generator.notificationOccurred(.success)
+ }
+ }
+
+ func scannerCompletion(_ result: Result<ScanResult, ScanError>) {
+ switch result {
+ case .success(let success):
+ handleQRString(success.string)
+ case .failure:
+ return
+ }
+ }
+
+ func resetView() {
+ loginKey = ""
+ qrkey = nil
+ privKeyFound = false
+ shouldSaveKey = true
+ }
+}
+
struct CreateAccountPrompt: View {
var nav: NavigationCoordinator
var body: some View {
diff --git a/damus/Views/QRScanNSECView.swift b/damus/Views/QRScanNSECView.swift
@@ -0,0 +1,66 @@
+//
+// 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 codeScannerCompletion: (Result<ScanResult, ScanError>) -> 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()
+ CodeScannerView(codeTypes: [.qr],
+ scanMode: .continuous,
+ scanInterval: 2.0,
+ showViewfinder: false,
+ simulatedData: "",
+ shouldVibrateOnSuccess: false,
+ isTorchOn: false,
+ isGalleryPresented: .constant(false),
+ videoCaptureDevice: .default(for: .video),
+ completion: codeScannerCompletion)
+ .scaledToFit()
+ .frame(width: 300, height: 300)
+ .cornerRadius(10)
+ .overlay(RoundedRectangle(cornerRadius: 10).stroke(DamusColors.white, lineWidth: 5.0))
+ .shadow(radius: 10)
+
+ Button(action: { showQR = false }) {
+ VStack {
+ Image(systemName: privKeyFound ? "sparkle.magnifyingglass" : "magnifyingglass")
+ .font(privKeyFound ? .title : .title3)
+ }}
+ .padding(.top)
+ .buttonStyle(GradientButtonStyle())
+
+ Spacer()
+
+ Spacer()
+ }
+ }
+ }
+}
+
+#Preview {
+ @State var showQR = true
+ @State var privKeyFound = false
+ @State var shouldSaveKey = true
+ return QRScanNSECView(showQR: $showQR,
+ privKeyFound: $privKeyFound,
+ codeScannerCompletion: { _ in })
+}
diff --git 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
+ }
}
}
}