diff --git a/damus.xcodeproj/project.pbxproj b/damus.xcodeproj/project.pbxproj index b0a32f18..c1646163 100644 --- a/damus.xcodeproj/project.pbxproj +++ b/damus.xcodeproj/project.pbxproj @@ -227,9 +227,6 @@ 4C7D09622A098D0E00943473 /* WalletConnect.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C7D09612A098D0E00943473 /* WalletConnect.swift */; }; 4C7D09662A0AE62100943473 /* AlbyButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C7D09652A0AE62100943473 /* AlbyButton.swift */; }; 4C7D09682A0AE9B200943473 /* NWCScannerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C7D09672A0AE9B200943473 /* NWCScannerView.swift */; }; - 4C7D096D2A0AEA0400943473 /* CodeScanner.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C7D096A2A0AEA0400943473 /* CodeScanner.swift */; }; - 4C7D096E2A0AEA0400943473 /* ScannerCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C7D096B2A0AEA0400943473 /* ScannerCoordinator.swift */; }; - 4C7D096F2A0AEA0400943473 /* ScannerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C7D096C2A0AEA0400943473 /* ScannerViewController.swift */; }; 4C7D09722A0AEF5E00943473 /* DamusGradient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C7D09712A0AEF5E00943473 /* DamusGradient.swift */; }; 4C7D09742A0AEF9000943473 /* AlbyGradient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C7D09732A0AEF9000943473 /* AlbyGradient.swift */; }; 4C7D09762A0AF19E00943473 /* FillAndStroke.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C7D09752A0AF19E00943473 /* FillAndStroke.swift */; }; @@ -579,6 +576,8 @@ D703D7B72C67118F00A400EA /* StringUtil.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A8CC6CB2A2CFEF900940F5F /* StringUtil.swift */; }; D703D7B82C6711A000A400EA /* NativeObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C32B9462A9AD44700DC3548 /* NativeObject.swift */; }; D70A3B172B02DCE5008BD568 /* NotificationFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = D70A3B162B02DCE5008BD568 /* NotificationFormatter.swift */; }; + D70D90982CDED61800CD0534 /* CodeScanner in Frameworks */ = {isa = PBXBuildFile; productRef = D70D90972CDED61800CD0534 /* CodeScanner */; }; + D70D909C2CDED7B200CD0534 /* CodeScanner in Frameworks */ = {isa = PBXBuildFile; productRef = D70D909B2CDED7B200CD0534 /* CodeScanner */; }; D7100C562B76F8E600C59298 /* PurpleViewPrimitives.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7100C552B76F8E600C59298 /* PurpleViewPrimitives.swift */; }; D7100C582B76FC8400C59298 /* MarketingContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7100C572B76FC8400C59298 /* MarketingContentView.swift */; }; D7100C5A2B76FD5100C59298 /* LogoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7100C592B76FD5100C59298 /* LogoView.swift */; }; @@ -774,9 +773,6 @@ D73E5ECA2C6A97F4007EB227 /* OnboardingSuggestionsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F71694E92A662232001F4053 /* OnboardingSuggestionsView.swift */; }; D73E5ECB2C6A97F4007EB227 /* SuggestedUserView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F71694F12A67314D001F4053 /* SuggestedUserView.swift */; }; D73E5ECC2C6A97F4007EB227 /* SuggestedUsersViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F71694EB2A662292001F4053 /* SuggestedUsersViewModel.swift */; }; - D73E5ECE2C6A97F4007EB227 /* CodeScanner.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C7D096A2A0AEA0400943473 /* CodeScanner.swift */; }; - D73E5ECF2C6A97F4007EB227 /* ScannerCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C7D096B2A0AEA0400943473 /* ScannerCoordinator.swift */; }; - D73E5ED02C6A97F4007EB227 /* ScannerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C7D096C2A0AEA0400943473 /* ScannerViewController.swift */; }; D73E5ED22C6A97F4007EB227 /* WalletView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C7D095D2A098C5D00943473 /* WalletView.swift */; }; D73E5ED32C6A97F4007EB227 /* NWCScannerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C7D09672A0AE9B200943473 /* NWCScannerView.swift */; }; D73E5ED42C6A97F4007EB227 /* FriendsButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C8D1A6E29F31E5000ACDF75 /* FriendsButton.swift */; }; @@ -1659,9 +1655,6 @@ 4C7D09612A098D0E00943473 /* WalletConnect.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WalletConnect.swift; sourceTree = ""; }; 4C7D09652A0AE62100943473 /* AlbyButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlbyButton.swift; sourceTree = ""; }; 4C7D09672A0AE9B200943473 /* NWCScannerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NWCScannerView.swift; sourceTree = ""; }; - 4C7D096A2A0AEA0400943473 /* CodeScanner.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CodeScanner.swift; sourceTree = ""; }; - 4C7D096B2A0AEA0400943473 /* ScannerCoordinator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ScannerCoordinator.swift; sourceTree = ""; }; - 4C7D096C2A0AEA0400943473 /* ScannerViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ScannerViewController.swift; sourceTree = ""; }; 4C7D09712A0AEF5E00943473 /* DamusGradient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusGradient.swift; sourceTree = ""; }; 4C7D09732A0AEF9000943473 /* AlbyGradient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlbyGradient.swift; sourceTree = ""; }; 4C7D09752A0AF19E00943473 /* FillAndStroke.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FillAndStroke.swift; sourceTree = ""; }; @@ -2021,6 +2014,7 @@ files = ( 4C06670428FC7EC500038D2A /* Kingfisher in Frameworks */, 3A0A30BB2C21397A00F8C9BC /* EmojiPicker in Frameworks */, + D70D90982CDED61800CD0534 /* CodeScanner in Frameworks */, D78DB8592C1CE9CA00F0AB12 /* SwipeActions in Frameworks */, 4C649881286E0EE300EAE2B3 /* secp256k1 in Frameworks */, 4C27C9322A64766F007DBC75 /* MarkdownUI in Frameworks */, @@ -2052,6 +2046,7 @@ D73E5F762C6A997E007EB227 /* EmojiPicker in Frameworks */, D703D7192C66E47100A400EA /* UniformTypeIdentifiers.framework in Frameworks */, D703D7492C6709B100A400EA /* secp256k1 in Frameworks */, + D70D909C2CDED7B200CD0534 /* CodeScanner in Frameworks */, D73E5F9B2C6AA8B0007EB227 /* Kingfisher in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -2564,7 +2559,6 @@ BA3759952ABCCF360018D73B /* Camera */, F71694E82A66221E001F4053 /* Onboarding */, 4C190F232A547D1700027FD5 /* NostrScript */, - 4C7D09692A0AEA0400943473 /* CodeScanner */, 4C7D095A2A098C5C00943473 /* Wallet */, 4C8D1A6D29F31E4100ACDF75 /* Buttons */, 4C1A9A2829DDF53B00516EAC /* Video */, @@ -2672,16 +2666,6 @@ path = Wallet; sourceTree = ""; }; - 4C7D09692A0AEA0400943473 /* CodeScanner */ = { - isa = PBXGroup; - children = ( - 4C7D096A2A0AEA0400943473 /* CodeScanner.swift */, - 4C7D096B2A0AEA0400943473 /* ScannerCoordinator.swift */, - 4C7D096C2A0AEA0400943473 /* ScannerViewController.swift */, - ); - path = CodeScanner; - sourceTree = ""; - }; 4C7D09702A0AEF4C00943473 /* Gradients */ = { isa = PBXGroup; children = ( @@ -3463,6 +3447,7 @@ 4C27C9312A64766F007DBC75 /* MarkdownUI */, 3A0A30BA2C21397A00F8C9BC /* EmojiPicker */, D78DB8582C1CE9CA00F0AB12 /* SwipeActions */, + D70D90972CDED61800CD0534 /* CodeScanner */, ); productName = damus; productReference = 4CE6DEE327F7A08100C66700 /* damus.app */; @@ -3519,6 +3504,7 @@ buildRules = ( ); dependencies = ( + D70D909A2CDED78400CD0534 /* PBXTargetDependency */, D703D7AD2C670FA700A400EA /* PBXTargetDependency */, ); name = HighlighterActionExtension; @@ -3528,6 +3514,7 @@ D73E5F752C6A997E007EB227 /* EmojiPicker */, D73E5F9A2C6AA8B0007EB227 /* Kingfisher */, D73E5F9C2C6AA8E3007EB227 /* SwipeActions */, + D70D909B2CDED7B200CD0534 /* CodeScanner */, ); productName = "highlighter action extension"; productReference = D703D7172C66E47100A400EA /* HighlighterActionExtension.appex */; @@ -3632,6 +3619,7 @@ D7A343EC2AD0D77C00CED48B /* XCRemoteSwiftPackageReference "swift-snapshot-testing" */, 3A0A30B92C21397A00F8C9BC /* XCRemoteSwiftPackageReference "EmojiPicker" */, D78DB8572C1CE9CA00F0AB12 /* XCRemoteSwiftPackageReference "SwipeActions" */, + D70D90962CDED61800CD0534 /* XCRemoteSwiftPackageReference "CodeScanner" */, ); productRefGroup = 4CE6DEE427F7A08100C66700 /* Products */; projectDirPath = ""; @@ -3851,7 +3839,6 @@ 4C75EFA627FF87A20006080F /* Nostr.swift in Sources */, 4CA927672A290F8B0098A105 /* RelativeTime.swift in Sources */, 4CB883A62975F83C00DC99E7 /* LNUrlPayRequest.swift in Sources */, - 4C7D096D2A0AEA0400943473 /* CodeScanner.swift in Sources */, D7CB5D4B2B11721600AD4105 /* ZapType.swift in Sources */, 4CE4F9DE2852768D00C00DD9 /* ConfigView.swift in Sources */, 3A48E7B029DFBE9D006E787E /* MutedThreadsManager.swift in Sources */, @@ -3888,7 +3875,6 @@ 4C3EA66828FF5F9900C48A62 /* hex.c in Sources */, E9E4ED0B295867B900DD7078 /* ThreadView.swift in Sources */, 4CD348EF29C3659D00497EB2 /* ImageUploadModel.swift in Sources */, - 4C7D096E2A0AEA0400943473 /* ScannerCoordinator.swift in Sources */, 4C3BEFDC281DCE6100B3DE84 /* Liked.swift in Sources */, 4CF0ABE7298444FD00D66079 /* EventMutingContainerView.swift in Sources */, 9C83F89329A937B900136C08 /* TextViewWrapper.swift in Sources */, @@ -4087,7 +4073,6 @@ 4CA927612A290E340098A105 /* EventShell.swift in Sources */, 4C363AA428296DEE006E126D /* SearchModel.swift in Sources */, 4C8D00CC29DF92DF0036AF10 /* Hashtags.swift in Sources */, - 4C7D096F2A0AEA0400943473 /* ScannerViewController.swift in Sources */, 4CEE2AF3280B25C500AB5EEF /* ProfilePicView.swift in Sources */, 4CC7AAF6297F1A6A00430951 /* EventBody.swift in Sources */, D76556D62B1E6C08001B0CCC /* DamusPurpleWelcomeView.swift in Sources */, @@ -4451,9 +4436,6 @@ D73E5ECA2C6A97F4007EB227 /* OnboardingSuggestionsView.swift in Sources */, D73E5ECB2C6A97F4007EB227 /* SuggestedUserView.swift in Sources */, D73E5ECC2C6A97F4007EB227 /* SuggestedUsersViewModel.swift in Sources */, - D73E5ECE2C6A97F4007EB227 /* CodeScanner.swift in Sources */, - D73E5ECF2C6A97F4007EB227 /* ScannerCoordinator.swift in Sources */, - D73E5ED02C6A97F4007EB227 /* ScannerViewController.swift in Sources */, D73E5ED22C6A97F4007EB227 /* WalletView.swift in Sources */, D73E5ED32C6A97F4007EB227 /* NWCScannerView.swift in Sources */, D73E5ED42C6A97F4007EB227 /* FriendsButton.swift in Sources */, @@ -4916,6 +4898,10 @@ isa = PBXTargetDependency; productRef = D703D7AC2C670FA700A400EA /* MarkdownUI */; }; + D70D909A2CDED78400CD0534 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + productRef = D70D90992CDED78400CD0534 /* CodeScanner */; + }; D79C4C1A2AFEB061003A41B4 /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = D79C4C132AFEB061003A41B4 /* DamusNotificationService */; @@ -5580,6 +5566,14 @@ minimumVersion = 0.2.26; }; }; + D70D90962CDED61800CD0534 /* XCRemoteSwiftPackageReference "CodeScanner" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/twostraws/CodeScanner.git"; + requirement = { + kind = revision; + revision = 9fa582f4b36c69c2a55bff5fb3377eb170ae273c; + }; + }; D78DB8572C1CE9CA00F0AB12 /* XCRemoteSwiftPackageReference "SwipeActions" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/damus-io/SwipeActions.git"; @@ -5634,6 +5628,21 @@ package = 4C27C9302A64766F007DBC75 /* XCRemoteSwiftPackageReference "swift-markdown-ui" */; productName = MarkdownUI; }; + D70D90972CDED61800CD0534 /* CodeScanner */ = { + isa = XCSwiftPackageProductDependency; + package = D70D90962CDED61800CD0534 /* XCRemoteSwiftPackageReference "CodeScanner" */; + productName = CodeScanner; + }; + D70D90992CDED78400CD0534 /* CodeScanner */ = { + isa = XCSwiftPackageProductDependency; + package = D70D90962CDED61800CD0534 /* XCRemoteSwiftPackageReference "CodeScanner" */; + productName = CodeScanner; + }; + D70D909B2CDED7B200CD0534 /* CodeScanner */ = { + isa = XCSwiftPackageProductDependency; + package = D70D90962CDED61800CD0534 /* XCRemoteSwiftPackageReference "CodeScanner" */; + productName = CodeScanner; + }; D73E5F752C6A997E007EB227 /* EmojiPicker */ = { isa = XCSwiftPackageProductDependency; package = 3A0A30B92C21397A00F8C9BC /* XCRemoteSwiftPackageReference "EmojiPicker" */; diff --git a/damus.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/damus.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 7cdc452f..e64ab159 100644 --- a/damus.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/damus.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,6 +1,14 @@ { - "originHash" : "1b14e62192b3fa4b04a57cb4601d175b325dc16cb5f22c4c8eb975a675328637", + "originHash" : "534c8e58993919d5ead25ceb4788c8e492c86bc2cf5833dc651ae60a0f30169c", "pins" : [ + { + "identity" : "codescanner", + "kind" : "remoteSourceControl", + "location" : "https://github.com/twostraws/CodeScanner.git", + "state" : { + "revision" : "9fa582f4b36c69c2a55bff5fb3377eb170ae273c" + } + }, { "identity" : "emojikit", "kind" : "remoteSourceControl", diff --git a/damus/Views/CodeScanner/CodeScanner.swift b/damus/Views/CodeScanner/CodeScanner.swift deleted file mode 100644 index ea849533..00000000 --- a/damus/Views/CodeScanner/CodeScanner.swift +++ /dev/null @@ -1,114 +0,0 @@ -// -// CodeScanner.swift -// https://github.com/twostraws/CodeScanner -// -// Created by Paul Hudson on 14/12/2021. -// Copyright © 2021 Paul Hudson. All rights reserved. -// - -import AVFoundation -import SwiftUI - -/// An enum describing the ways CodeScannerView can hit scanning problems. -public enum ScanError: Error { - /// The camera could not be accessed. - case badInput - - /// The camera was not capable of scanning the requested codes. - case badOutput - - /// Initialization failed. - case initError(_ error: Error) -} - -/// The result from a successful scan: the string that was scanned, and also the type of data that was found. -/// The type is useful for times when you've asked to scan several different code types at the same time, because -/// it will report the exact code type that was found. -public struct ScanResult { - /// The contents of the code. - public let string: String - - /// The type of code that was matched. - public let type: AVMetadataObject.ObjectType -} - -/// The operating mode for CodeScannerView. -public enum ScanMode { - /// Scan exactly one code, then stop. - case once - - /// Scan each code no more than once. - case oncePerCode - - /// Keep scanning all codes until dismissed. - case continuous -} - -/// A SwiftUI view that is able to scan barcodes, QR codes, and more, and send back what was found. -/// To use, set `codeTypes` to be an array of things to scan for, e.g. `[.qr]`, and set `completion` to -/// a closure that will be called when scanning has finished. This will be sent the string that was detected or a `ScanError`. -/// For testing inside the simulator, set the `simulatedData` property to some test data you want to send back. -public struct CodeScannerView: UIViewControllerRepresentable { - - public let codeTypes: [AVMetadataObject.ObjectType] - public let scanMode: ScanMode - public let scanInterval: Double - public let showViewfinder: Bool - public var simulatedData = "" - public var shouldVibrateOnSuccess: Bool - public var isTorchOn: Bool - public var isGalleryPresented: Binding - public var videoCaptureDevice: AVCaptureDevice? - public var completion: (Result) -> Void - - public init( - codeTypes: [AVMetadataObject.ObjectType], - scanMode: ScanMode = .once, - scanInterval: Double = 2.0, - showViewfinder: Bool = false, - simulatedData: String = "", - shouldVibrateOnSuccess: Bool = true, - isTorchOn: Bool = false, - isGalleryPresented: Binding = .constant(false), - videoCaptureDevice: AVCaptureDevice? = AVCaptureDevice.default(for: .video), - completion: @escaping (Result) -> Void - ) { - self.codeTypes = codeTypes - self.scanMode = scanMode - self.showViewfinder = showViewfinder - self.scanInterval = scanInterval - self.simulatedData = simulatedData - self.shouldVibrateOnSuccess = shouldVibrateOnSuccess - self.isTorchOn = isTorchOn - self.isGalleryPresented = isGalleryPresented - self.videoCaptureDevice = videoCaptureDevice - self.completion = completion - } - - public func makeCoordinator() -> ScannerCoordinator { - ScannerCoordinator(parent: self) - } - - public func makeUIViewController(context: Context) -> ScannerViewController { - let viewController = ScannerViewController(showViewfinder: showViewfinder) - viewController.delegate = context.coordinator - return viewController - } - - public func updateUIViewController(_ uiViewController: ScannerViewController, context: Context) { - uiViewController.updateViewController( - isTorchOn: isTorchOn, - isGalleryPresented: isGalleryPresented.wrappedValue - ) - } - -} - -@available(macCatalyst 14.0, *) -struct CodeScannerView_Previews: PreviewProvider { - static var previews: some View { - CodeScannerView(codeTypes: [.qr]) { result in - // do nothing - } - } -} diff --git a/damus/Views/CodeScanner/ScannerCoordinator.swift b/damus/Views/CodeScanner/ScannerCoordinator.swift deleted file mode 100644 index 5891cfe3..00000000 --- a/damus/Views/CodeScanner/ScannerCoordinator.swift +++ /dev/null @@ -1,75 +0,0 @@ -// -// CodeScanner.swift -// https://github.com/twostraws/CodeScanner -// -// Created by Paul Hudson on 14/12/2021. -// Copyright © 2021 Paul Hudson. All rights reserved. -// - -import AVFoundation -import SwiftUI - -extension CodeScannerView { - @available(macCatalyst 14.0, *) - public class ScannerCoordinator: NSObject, AVCaptureMetadataOutputObjectsDelegate { - var parent: CodeScannerView - var codesFound = Set() - var didFinishScanning = false - var lastTime = Date(timeIntervalSince1970: 0) - - init(parent: CodeScannerView) { - self.parent = parent - } - - public func reset() { - codesFound.removeAll() - didFinishScanning = false - lastTime = Date(timeIntervalSince1970: 0) - } - - public func metadataOutput(_ output: AVCaptureMetadataOutput, didOutput metadataObjects: [AVMetadataObject], from connection: AVCaptureConnection) { - if let metadataObject = metadataObjects.first { - guard let readableObject = metadataObject as? AVMetadataMachineReadableCodeObject else { return } - guard let stringValue = readableObject.stringValue else { return } - guard didFinishScanning == false else { return } - let result = ScanResult(string: stringValue, type: readableObject.type) - - switch parent.scanMode { - case .once: - found(result) - // make sure we only trigger scan once per use - didFinishScanning = true - - case .oncePerCode: - if !codesFound.contains(stringValue) { - codesFound.insert(stringValue) - found(result) - } - - case .continuous: - if isPastScanInterval() { - found(result) - } - } - } - } - - func isPastScanInterval() -> Bool { - Date().timeIntervalSince(lastTime) >= parent.scanInterval - } - - func found(_ result: ScanResult) { - lastTime = Date() - - if parent.shouldVibrateOnSuccess { - AudioServicesPlaySystemSound(SystemSoundID(kSystemSoundID_Vibrate)) - } - - parent.completion(.success(result)) - } - - func didFail(reason: ScanError) { - parent.completion(.failure(reason)) - } - } -} diff --git a/damus/Views/CodeScanner/ScannerViewController.swift b/damus/Views/CodeScanner/ScannerViewController.swift deleted file mode 100644 index c37b8e57..00000000 --- a/damus/Views/CodeScanner/ScannerViewController.swift +++ /dev/null @@ -1,296 +0,0 @@ -// -// CodeScanner.swift -// https://github.com/twostraws/CodeScanner -// -// Created by Paul Hudson on 14/12/2021. -// Copyright © 2021 Paul Hudson. All rights reserved. -// - -import AVFoundation -import UIKit - -extension CodeScannerView { - - @available(macCatalyst 14.0, *) - public class ScannerViewController: UIViewController, UIImagePickerControllerDelegate, UINavigationControllerDelegate { - - var delegate: ScannerCoordinator? - private let showViewfinder: Bool - - private var isGalleryShowing: Bool = false { - didSet { - // Update binding - if delegate?.parent.isGalleryPresented.wrappedValue != isGalleryShowing { - delegate?.parent.isGalleryPresented.wrappedValue = isGalleryShowing - } - } - } - - public init(showViewfinder: Bool = false) { - self.showViewfinder = showViewfinder - super.init(nibName: nil, bundle: nil) - } - - required init?(coder: NSCoder) { - self.showViewfinder = false - super.init(coder: coder) - } - - func openGallery() { - isGalleryShowing = true - let imagePicker = UIImagePickerController() - imagePicker.delegate = self - present(imagePicker, animated: true, completion: nil) - } - - @objc func openGalleryFromButton(_ sender: UIButton) { - openGallery() - } - - public func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any]) { - isGalleryShowing = false - - if let qrcodeImg = info[.originalImage] as? UIImage { - let detector = CIDetector(ofType: CIDetectorTypeQRCode, context: nil, options: [CIDetectorAccuracy: CIDetectorAccuracyHigh])! - let ciImage = CIImage(image:qrcodeImg)! - var qrCodeLink = "" - - let features = detector.features(in: ciImage) - - for feature in features as! [CIQRCodeFeature] { - qrCodeLink += feature.messageString! - } - - if qrCodeLink == "" { - delegate?.didFail(reason: .badOutput) - } else { - let result = ScanResult(string: qrCodeLink, type: .qr) - delegate?.found(result) - } - } else { - print("Something went wrong") - } - - dismiss(animated: true, completion: nil) - } - - public func imagePickerControllerDidCancel(_ picker: UIImagePickerController) { - isGalleryShowing = false - } - - #if targetEnvironment(simulator) - override public func loadView() { - view = UIView() - view.isUserInteractionEnabled = true - - let label = UILabel() - label.translatesAutoresizingMaskIntoConstraints = false - label.numberOfLines = 0 - label.text = "You're running in the simulator, which means the camera isn't available. Tap anywhere to send back some simulated data." - label.textAlignment = .center - - let button = UIButton() - button.translatesAutoresizingMaskIntoConstraints = false - button.setTitle("Select a custom image", for: .normal) - button.setTitleColor(UIColor.systemBlue, for: .normal) - button.setTitleColor(UIColor.gray, for: .highlighted) - button.addTarget(self, action: #selector(openGalleryFromButton), for: .touchUpInside) - - let stackView = UIStackView() - stackView.translatesAutoresizingMaskIntoConstraints = false - stackView.axis = .vertical - stackView.spacing = 50 - stackView.addArrangedSubview(label) - stackView.addArrangedSubview(button) - - view.addSubview(stackView) - - NSLayoutConstraint.activate([ - button.heightAnchor.constraint(equalToConstant: 50), - stackView.leadingAnchor.constraint(equalTo: view.layoutMarginsGuide.leadingAnchor), - stackView.trailingAnchor.constraint(equalTo: view.layoutMarginsGuide.trailingAnchor), - stackView.centerYAnchor.constraint(equalTo: view.centerYAnchor) - ]) - } - - override public func touchesBegan(_ touches: Set, with event: UIEvent?) { - guard let simulatedData = delegate?.parent.simulatedData else { - print("Simulated Data Not Provided!") - return - } - - // Send back their simulated data, as if it was one of the types they were scanning for - let result = ScanResult(string: simulatedData, type: delegate?.parent.codeTypes.first ?? .qr) - delegate?.found(result) - } - - #else - - var captureSession: AVCaptureSession! - var previewLayer: AVCaptureVideoPreviewLayer! - let fallbackVideoCaptureDevice = AVCaptureDevice.default(for: .video) - - private lazy var viewFinder: UIImageView? = { - guard let image = UIImage(named: "viewfinder", in: .main, with: nil) else { - return nil - } - - let imageView = UIImageView(image: image) - imageView.translatesAutoresizingMaskIntoConstraints = false - return imageView - }() - - override public func viewDidLoad() { - super.viewDidLoad() - - NotificationCenter.default.addObserver(self, - selector: #selector(updateOrientation), - name: Notification.Name("UIDeviceOrientationDidChangeNotification"), - object: nil) - - view.backgroundColor = UIColor.black - captureSession = AVCaptureSession() - - guard let videoCaptureDevice = delegate?.parent.videoCaptureDevice ?? fallbackVideoCaptureDevice else { - return - } - - let videoInput: AVCaptureDeviceInput - - do { - videoInput = try AVCaptureDeviceInput(device: videoCaptureDevice) - } catch { - delegate?.didFail(reason: .initError(error)) - return - } - - if (captureSession.canAddInput(videoInput)) { - captureSession.addInput(videoInput) - } else { - delegate?.didFail(reason: .badInput) - return - } - - let metadataOutput = AVCaptureMetadataOutput() - - if (captureSession.canAddOutput(metadataOutput)) { - captureSession.addOutput(metadataOutput) - - metadataOutput.setMetadataObjectsDelegate(delegate, queue: DispatchQueue.main) - metadataOutput.metadataObjectTypes = delegate?.parent.codeTypes - } else { - delegate?.didFail(reason: .badOutput) - return - } - - if previewLayer == nil { - previewLayer = AVCaptureVideoPreviewLayer(session: captureSession) - } - - previewLayer.frame = view.layer.bounds - previewLayer.videoGravity = .resizeAspectFill - view.layer.addSublayer(previewLayer) - addviewfinder() - - delegate?.reset() - - if (captureSession?.isRunning == false) { - DispatchQueue.global(qos: .userInitiated).async { - self.captureSession.startRunning() - } - } - } - - override public func viewWillLayoutSubviews() { - previewLayer?.frame = view.layer.bounds - } - - @objc func updateOrientation() { - guard let orientation = view.window?.windowScene?.interfaceOrientation else { return } - guard let connection = captureSession.connections.last, connection.isVideoOrientationSupported else { return } - connection.videoOrientation = AVCaptureVideoOrientation(rawValue: orientation.rawValue) ?? .portrait - } - - override public func viewDidAppear(_ animated: Bool) { - super.viewDidAppear(animated) - updateOrientation() - } - - private func addviewfinder() { - guard showViewfinder, let imageView = viewFinder else { return } - - view.addSubview(imageView) - - NSLayoutConstraint.activate([ - imageView.centerYAnchor.constraint(equalTo: view.centerYAnchor), - imageView.centerXAnchor.constraint(equalTo: view.centerXAnchor), - imageView.widthAnchor.constraint(equalToConstant: 200), - imageView.heightAnchor.constraint(equalToConstant: 200), - ]) - } - - override public func viewDidDisappear(_ animated: Bool) { - super.viewDidDisappear(animated) - - if (captureSession?.isRunning == true) { - DispatchQueue.global(qos: .userInitiated).async { - self.captureSession.stopRunning() - } - } - - NotificationCenter.default.removeObserver(self) - } - - override public var prefersStatusBarHidden: Bool { - true - } - - override public var supportedInterfaceOrientations: UIInterfaceOrientationMask { - .all - } - - /** Touch the screen for autofocus */ - public override func touchesBegan(_ touches: Set, with event: UIEvent?) { - guard touches.first?.view == view, - let touchPoint = touches.first, - let device = delegate?.parent.videoCaptureDevice ?? fallbackVideoCaptureDevice - else { return } - - let videoView = view - let screenSize = videoView!.bounds.size - let xPoint = touchPoint.location(in: videoView).y / screenSize.height - let yPoint = 1.0 - touchPoint.location(in: videoView).x / screenSize.width - let focusPoint = CGPoint(x: xPoint, y: yPoint) - - do { - try device.lockForConfiguration() - } catch { - return - } - - // Focus to the correct point, make continiuous focus and exposure so the point stays sharp when moving the device closer - device.focusPointOfInterest = focusPoint - device.focusMode = .continuousAutoFocus - device.exposurePointOfInterest = focusPoint - device.exposureMode = AVCaptureDevice.ExposureMode.continuousAutoExposure - device.unlockForConfiguration() - } - - #endif - - func updateViewController(isTorchOn: Bool, isGalleryPresented: Bool) { - if let backCamera = AVCaptureDevice.default(for: AVMediaType.video), - backCamera.hasTorch - { - try? backCamera.lockForConfiguration() - backCamera.torchMode = isTorchOn ? .on : .off - backCamera.unlockForConfiguration() - } - - if isGalleryPresented && !isGalleryShowing { - openGallery() - } - } - - } -} diff --git a/damus/Views/LoginView.swift b/damus/Views/LoginView.swift index 2b9ec1f8..3f263664 100644 --- a/damus/Views/LoginView.swift +++ b/damus/Views/LoginView.swift @@ -5,6 +5,7 @@ // Created by William Casarin on 2022-05-22. // +import CodeScanner import SwiftUI enum ParsedKey { diff --git a/damus/Views/ProfileActionSheetView.swift b/damus/Views/ProfileActionSheetView.swift index c9748b0d..ad6a4cb2 100644 --- a/damus/Views/ProfileActionSheetView.swift +++ b/damus/Views/ProfileActionSheetView.swift @@ -19,9 +19,12 @@ struct ProfileActionSheetView: View { @Environment(\.colorScheme) var colorScheme @Environment(\.presentationMode) var presentationMode - init(damus_state: DamusState, pubkey: Pubkey) { + var navigationHandler: (() -> Void)? + + init(damus_state: DamusState, pubkey: Pubkey, onNavigate navigationHandler: (() -> Void)? = nil) { self.damus_state = damus_state self._profile = StateObject(wrappedValue: ProfileModel(pubkey: pubkey, damus: damus_state)) + self.navigationHandler = navigationHandler } func imageBorderColor() -> Color { @@ -37,6 +40,12 @@ struct ProfileActionSheetView: View { return self.profile_data()?.profile } + func navigate(route: Route) { + damus_state.nav.push(route: route) + self.navigationHandler?() + dismiss() + } + var followButton: some View { return ProfileActionSheetFollowButton( target: .pubkey(self.profile.pubkey), @@ -65,8 +74,7 @@ struct ProfileActionSheetView: View { return VStack(alignment: .center, spacing: 10) { Button( action: { - damus_state.nav.push(route: Route.DMChat(dms: dm_model)) - dismiss() + self.navigate(route: Route.DMChat(dms: dm_model)) }, label: { Image("messages") @@ -126,8 +134,7 @@ struct ProfileActionSheetView: View { Button( action: { - damus_state.nav.push(route: Route.ProfileByKey(pubkey: profile.pubkey)) - dismiss() + self.navigate(route: Route.ProfileByKey(pubkey: profile.pubkey)) }, label: { HStack { diff --git a/damus/Views/QRCodeView.swift b/damus/Views/QRCodeView.swift index e968d7ef..69799fb2 100644 --- a/damus/Views/QRCodeView.swift +++ b/damus/Views/QRCodeView.swift @@ -7,61 +7,16 @@ import SwiftUI import CoreImage.CIFilterBuiltins +import CodeScanner -struct ProfileScanResult: Equatable { - let pubkey: Pubkey - - init?(hex: String) { - guard let pk = hex_decode(hex).map({ bytes in Pubkey(Data(bytes)) }) else { - return nil - } - - self.pubkey = pk - } - - init?(string: String) { - var str = string - guard str.count != 0 else { - return nil - } - - if str.hasPrefix("nostr:") { - str.removeFirst("nostr:".count) - } - - if let decoded = hex_decode(str), - str.count == 64 - { - self.pubkey = Pubkey(Data(decoded)) - return - } - - if str.starts(with: "npub"), - let b32 = try? bech32_decode(str) - { - self.pubkey = Pubkey(b32.data) - return - } - - return nil - } -} struct QRCodeView: View { let damus_state: DamusState @State var pubkey: Pubkey - @Environment(\.presentationMode) var presentationMode + @Environment(\.dismiss) var dismiss @State private var selectedTab = 0 - @State var scanResult: ProfileScanResult? = nil - @State var profile: Profile? = nil - @State var error: String? = nil - @State private var outerTrimEnd: CGFloat = 0 - - var animationDuration: Double = 0.5 - - let generator = UIImpactFeedbackGenerator(style: .light) @ViewBuilder func navImage(systemImage: String) -> some View { @@ -73,7 +28,7 @@ struct QRCodeView: View { var navBackButton: some View { Button { - presentationMode.wrappedValue.dismiss() + dismiss() } label: { navImage(systemImage: "chevron.left") } @@ -98,7 +53,7 @@ struct QRCodeView: View { TabView(selection: $selectedTab) { QRView .tag(0) - QRCameraView() + self.qrCameraView .tag(1) } .tabViewStyle(PageTabViewStyle(indexDisplayMode: .never)) @@ -182,35 +137,8 @@ struct QRCodeView: View { } } - func QRCameraView() -> some View { - return VStack(alignment: .center) { - Text("Scan a user's pubkey", comment: "Text to prompt scanning a QR code of a user's pubkey to open their profile.") - .padding(.top, 50) - .font(.system(size: 24, weight: .heavy)) - .foregroundColor(.white) - - Spacer() - - CodeScannerView(codeTypes: [.qr], scanMode: .continuous, simulatedData: "npub1k92qsr95jcumkpu6dffurkvwwycwa2euvx4fthv78ru7gqqz0nrs2ngfwd", shouldVibrateOnSuccess: false) { result in - switch result { - case .success(let success): - handleProfileScan(success.string) - case .failure(let failure): - self.error = failure.localizedDescription - } - } - .scaledToFit() - .frame(maxWidth: 300, maxHeight: 300) - .cornerRadius(10) - .overlay(RoundedRectangle(cornerRadius: 10).stroke(DamusColors.white, lineWidth: 5.0).scaledToFit()) - .overlay(RoundedRectangle(cornerRadius: 10).trim(from: 0.0, to: outerTrimEnd).stroke(DamusColors.black, lineWidth: 5.5) - .rotationEffect(.degrees(-90)).scaledToFit()) - .shadow(radius: 10) - - Spacer() - - Spacer() - + var qrCameraView: some View { + QRCameraView(damusState: damus_state, bottomContent: { Button(action: { selectedTab = 0 }) { @@ -218,65 +146,11 @@ struct QRCodeView: View { Text("View QR Code", comment: "Button to switch to view users QR Code") .fontWeight(.semibold) } - .frame( maxWidth: .infinity, maxHeight: 12, alignment: .center) + .frame(maxWidth: .infinity, maxHeight: 12, alignment: .center) } .buttonStyle(GradientButtonStyle()) .padding(50) - } - } - - func handleProfileScan(_ scanned_str: String) { - guard let result = ProfileScanResult(string: scanned_str) else { - self.error = "Invalid profile QR" - return - } - - self.error = nil - - guard result != self.scanResult else { - return - } - - generator.impactOccurred() - cameraAnimate { - scanResult = result - - find_event(state: damus_state, query: .profile(pubkey: result.pubkey)) { res in - guard let res else { - error = "Profile not found" - return - } - - switch res { - case .invalid_profile: - error = "Profile was found but was corrupt." - - case .profile: - show_profile_after_delay() - - case .event: - print("invalid search result") - } - - } - } - } - - func show_profile_after_delay() { - DispatchQueue.main.asyncAfter(deadline: .now() + animationDuration) { - if let scanResult { - damus_state.nav.push(route: Route.ProfileByKey(pubkey: scanResult.pubkey)) - presentationMode.wrappedValue.dismiss() - } - } - } - - func cameraAnimate(completion: @escaping () -> Void) { - outerTrimEnd = 0.0 - withAnimation(.easeInOut(duration: animationDuration)) { - outerTrimEnd = 1.05 // Set to 1.05 instead of 1.0 since sometimes `completion()` runs before the value reaches 1.0. This ensures the animation is done. - } - completion() + }, dismiss: dismiss) } func generateQRCode(pubkey: String) -> UIImage { @@ -299,6 +173,295 @@ struct QRCodeView: View { } } +/// A view that scans for pubkeys/npub QR codes and displays a profile when needed. +/// +/// ## Implementation notes: +/// +/// - Marked as `fileprivate` since it is a relatively niche view, but can be made public with some adaptation if reuse is needed +/// - The main state is tracked by a single enum, to ensure mutual exclusion of states (only one of the states can be active at a time), and that the info for each state is there when needed — both enforced at compile-time +fileprivate struct QRCameraView: View { + + // MARK: Input parameters + + var damusState: DamusState + /// A custom view to display on the bottom of the camera view + var bottomContent: () -> Content + var dismiss: DismissAction + + + // MARK: State properties + + /// The main state of this view. + @State var scannerState: ScannerState = .scanning { + didSet { + switch (oldValue, scannerState) { + case (.scanning, .scanSuccessful), (.incompatibleQRCodeFound, .scanSuccessful): + generator.impactOccurred() // Haptic feedback upon a successful scan + default: + break + } + } + } + + + // MARK: Helper properties and objects + + let generator = UIImpactFeedbackGenerator(style: .light) + /// A timer that ticks every second. + /// We need this to dismiss the incompatible QR code message automatically once the user is no longer pointing the camera at it + let timer = Timer.publish(every: 1.0, on: .main, in: .common).autoconnect() + + /// This is used to create a nice border animation when a scan is successful + /// + /// Computed property to simplify state management + var outerTrimEnd: CGFloat { + switch scannerState { + case .scanning, .error, .incompatibleQRCodeFound: + return 0.0 + case .scanSuccessful: + return 1.0 + } + } + + /// A computed binding that indicates if there is an error to be displayed. + /// + /// This property is computed based on the main state `scannerState`, and is used to manage the error sheet without adding any extra state variables + var errorBinding: Binding { + Binding( + get: { + guard case .error(let error) = scannerState else { return nil } + return error + }, + set: { newError in + guard let newError else { + self.scannerState = .scanning + return + } + self.scannerState = .error(newError) + }) + } + + /// A computed binding that indicates if there is a profile scan result to be displayed + /// + /// This property is computed based on the main state `scannerState`, and is used to manage the profile sheet without adding any extra state variables + var profileScanResultBinding: Binding { + Binding( + get: { + guard case .scanSuccessful(result: let scanResult) = scannerState else { return nil } + return scanResult + }, + set: { newProfileScanResult in + guard let newProfileScanResult else { + self.scannerState = .scanning + return + } + self.scannerState = .scanSuccessful(result: newProfileScanResult) + }) + } + + + // MARK: View layouts + + var body: some View { + VStack(alignment: .center) { + Text("Scan a user's pubkey", comment: "Text to prompt scanning a QR code of a user's pubkey to open their profile.") + .padding(.top, 50) + .font(.system(size: 24, weight: .heavy)) + .foregroundColor(.white) + + Spacer() + + CodeScannerView(codeTypes: [.qr], scanMode: .continuous, scanInterval: 1, showViewfinder: true, simulatedData: "npub1k92qsr95jcumkpu6dffurkvwwycwa2euvx4fthv78ru7gqqz0nrs2ngfwd", shouldVibrateOnSuccess: false) { result in + self.handleNewProfileScanInfo(result) + } + .scaledToFit() + .frame(maxWidth: 300, maxHeight: 300) + .cornerRadius(10) + .overlay(RoundedRectangle(cornerRadius: 10).stroke(DamusColors.white, lineWidth: 5.0).scaledToFit()) + .overlay(RoundedRectangle(cornerRadius: 10).trim(from: 0.0, to: outerTrimEnd).stroke(DamusColors.black, lineWidth: 5.5) + .rotationEffect(.degrees(-90)).scaledToFit()) + .shadow(radius: 10) + + Spacer() + + self.hintMessage + + Spacer() + + self.bottomContent() + } + // Show an error sheet if we are on an error state + .sheet(item: self.errorBinding, content: { error in + self.errorSheet(error: error) + }) + // Show the profile sheet if we have successfully scanned + .sheet(item: self.profileScanResultBinding, content: { scanResult in + ProfileActionSheetView(damus_state: self.damusState, pubkey: scanResult.pubkey, onNavigate: { + dismiss() + }) + .tint(DamusColors.adaptableBlack) + .presentationDetents([.large]) + }) + // Dismiss an incompatible QR code message automatically after a second or two of pointing it elsewhere. + .onReceive(timer) { _ in + switch self.scannerState { + case .incompatibleQRCodeFound(scannedAt: let date): + if abs(date.timeIntervalSinceNow) > 1.5 { + self.scannerState = .scanning + } + default: + break + } + } + } + + var hintMessage: some View { + HStack { + switch self.scannerState { + case .scanning: + Text("Point your camera to a QR code…", comment: "Text on QR code camera view instructing user to point to QR code") + case .incompatibleQRCodeFound: + Text("Sorry, this QR code looks incompatible with Damus. Please try another one.", comment: "Text on QR code camera view telling the user a QR is incompatible") + case .scanSuccessful: + Text("Found profile!", comment: "Text on QR code camera view telling user that profile scan was successful.") + case .error: + Text("Error, please try again", comment: "Text on QR code camera view indicating an error") + } + } + .foregroundColor(.white) + .padding() + } + + func errorSheet(error: ScannerError) -> some View { + VStack(spacing: 10) { + Image(systemName: "exclamationmark.circle.fill") + Text("Error", comment: "Headline label for an error sheet on the QR code scanner") + .font(.headline) + Text(error.localizedDescription) + } + .presentationDetents([.medium]) + .tint(DamusColors.adaptableBlack) + } + + + // MARK: Scanning and state management logic + + /// A base handler anytime the scanner sends new info, + /// + /// Behavior depends on the current state. In some states we completely ignore new scanner info (e.g. when looking at a profile) + /// This function mutates our state + func handleNewProfileScanInfo(_ scanInfo: Result) { + switch scannerState { + case .scanning, .incompatibleQRCodeFound: + withAnimation { + self.scannerState = self.processScanAndComputeNextState(scanInfo) + } + case .scanSuccessful, .error: + return // We don't want new scan results to pop-up while in these states + } + } + + /// Processes a QR code scan, and computes the next state to be applied to the view + func processScanAndComputeNextState(_ scanInfo: Result) -> ScannerState { + switch scanInfo { + case .success(let successfulScan): + guard let result = ProfileScanResult(string: successfulScan.string) else { + return .incompatibleQRCodeFound(scannedAt: Date.now) + } + return .scanSuccessful(result: result) + case .failure(let error): + return .error(.scanError(error)) + } + } + + // MARK: Helper types + + /// A custom type for `QRCameraView` to track the state of the scanner. + /// + /// This is done to avoid having multiple independent variables to track the state, which increases the chance of state inconsistency. + /// By using this we guarantee at compile-time that we will always be in one state at a time, and that the state is coherent/consistent/clear. + enum ScannerState { + /// Camera is on and actively scanning new QR codes + case scanning + /// Scan and decoding was successful. Show profile. + case scanSuccessful(result: ProfileScanResult) + /// Tell the user they scanned a QR code that is incompatible + case incompatibleQRCodeFound(scannedAt: Date) + /// There was an error. Display a human readable and actionable message + case error(ScannerError) + } + + /// Represents an error in this view, to be displayed to the user + /// + /// **Implementation notes:** + /// 1. This is identifiable because it that is needed for the error sheet view + /// 2. Currently there is only one error type (`ScanError`), but this is still used to allow us to customize it and add future error types outside the scanner. + enum ScannerError: Error, Identifiable { + case scanError(ScanError) + + var localizedDescription: String { + switch self { + case .scanError(let scanError): + switch scanError { + case .badInput: + NSLocalizedString("The camera could not be accessed.", comment: "Camera's bad input error label") + case .badOutput: + NSLocalizedString("The camera was not capable of scanning the requested codes.", comment: "Camera's bad output error label") + case .initError(_): + NSLocalizedString("There was an unexpected error in initializing the camera.", comment: "Camera's initialization error label") + case .permissionDenied: + NSLocalizedString("Camera's permission was denied. You can change this in iOS settings.", comment: "Camera's permission denied error label") + } + } + } + var id: String { return self.localizedDescription } + } + + /// A struct that holds results of a profile scan + struct ProfileScanResult: Equatable, Identifiable { + var id: Pubkey { return self.pubkey } + let pubkey: Pubkey + + init?(hex: String) { + guard let pk = hex_decode(hex).map({ bytes in Pubkey(Data(bytes)) }) else { + return nil + } + + self.pubkey = pk + } + + init?(string: String) { + var str = string.trimmingCharacters(in: ["\n", "\t", " "]) + guard str.count != 0 else { + return nil + } + + if str.hasPrefix("nostr:") { + str.removeFirst("nostr:".count) + } + + if let decoded = hex_decode(str), + str.count == 64 + { + self.pubkey = Pubkey(Data(decoded)) + return + } + + if str.starts(with: "npub"), + let b32 = try? bech32_decode(str) + { + self.pubkey = Pubkey(b32.data) + return + } + + return nil + } + } +} + + +// MARK: - Previews + struct QRCodeView_Previews: PreviewProvider { static var previews: some View { QRCodeView(damus_state: test_damus_state, pubkey: test_note.pubkey) diff --git a/damus/Views/QRScanNSECView.swift b/damus/Views/QRScanNSECView.swift index eddca242..d806de4f 100644 --- a/damus/Views/QRScanNSECView.swift +++ b/damus/Views/QRScanNSECView.swift @@ -5,6 +5,7 @@ // Created by Jericho Hasselbush on 9/29/23. // +import CodeScanner import SwiftUI import VisionKit diff --git a/damus/Views/Wallet/NWCScannerView.swift b/damus/Views/Wallet/NWCScannerView.swift index 433a1f17..421dbd2a 100644 --- a/damus/Views/Wallet/NWCScannerView.swift +++ b/damus/Views/Wallet/NWCScannerView.swift @@ -6,6 +6,7 @@ // import SwiftUI +import CodeScanner enum WalletScanResult: Equatable { static func == (lhs: WalletScanResult, rhs: WalletScanResult) -> Bool {