Explorar o código

Fix premium gating bugs and restrict scan to paid users.

Resolve stale bookmark recursion, scanner completion races, and entitlement timing issues; apply print/scan settings and tighten feature gates while making scan premium-only.

Co-authored-by: Cursor <cursoragent@cursor.com>
AhtashamShahzad1 hai 3 semanas
pai
achega
6d654db81d

+ 1 - 0
smart_printer/AppDelegate.swift

@@ -19,6 +19,7 @@ class AppDelegate: NSObject, NSApplicationDelegate {
         StoreManager.shared.start()
         PaywallConfigService.shared.start()
         AppRatingManager.shared.start()
+        AppSettings.syncLaunchAtLoginRegistration()
         resolveMainWindowController()
         configureMainWindow()
         configurePreferencesMenu()

+ 26 - 1
smart_printer/AppSettings.swift

@@ -1,4 +1,5 @@
 import Cocoa
+import ServiceManagement
 
 enum PaperSize: String, CaseIterable {
     case a4 = "A4"
@@ -50,7 +51,10 @@ enum AppSettings {
 
     static var launchAtLogin: Bool {
         get { defaults.bool(forKey: Key.launchAtLogin) }
-        set { defaults.set(newValue, forKey: Key.launchAtLogin) }
+        set {
+            defaults.set(newValue, forKey: Key.launchAtLogin)
+            applyLaunchAtLogin(newValue)
+        }
     }
 
     static var defaultPaperSize: PaperSize {
@@ -151,6 +155,27 @@ enum AppSettings {
         NotificationCenter.default.post(name: .appearanceDidChange, object: nil)
     }
 
+    static func syncLaunchAtLoginRegistration() {
+        guard #available(macOS 13.0, *) else { return }
+        let registered = SMAppService.mainApp.status == .enabled
+        if registered != launchAtLogin {
+            applyLaunchAtLogin(launchAtLogin)
+        }
+    }
+
+    private static func applyLaunchAtLogin(_ enabled: Bool) {
+        guard #available(macOS 13.0, *) else { return }
+        do {
+            if enabled {
+                try SMAppService.mainApp.register()
+            } else {
+                try SMAppService.mainApp.unregister()
+            }
+        } catch {
+            NSLog("Launch at login failed: \(error.localizedDescription)")
+        }
+    }
+
     static var appLanguage: String {
         get { defaults.string(forKey: Key.appLanguage) ?? "English" }
         set { defaults.set(newValue, forKey: Key.appLanguage) }

+ 1 - 1
smart_printer/DrawPrintView.swift

@@ -7,7 +7,7 @@ enum DrawPrintService {
     static func present(from window: NSWindow?) {
         let hostWindow = window ?? NSApp.keyWindow
         guard PremiumAccess.require(feature: .drawPrint, from: hostWindow) else { return }
-        guard let viewController = hostWindow?.contentViewController as? ViewController else { return }
+        guard let viewController = PremiumAccess.mainViewController(from: hostWindow) else { return }
         viewController.presentDrawPrint()
     }
 }

+ 2 - 2
smart_printer/FilePreviewView.swift

@@ -9,7 +9,7 @@ enum FilePreviewService {
         guard !urls.isEmpty else { return }
         let hostWindow = window ?? NSApp.keyWindow
         guard PremiumAccess.require(feature: .filePreview, from: hostWindow) else { return }
-        guard let viewController = hostWindow?.contentViewController as? ViewController else { return }
+        guard let viewController = PremiumAccess.mainViewController(from: hostWindow) else { return }
         viewController.presentFilePreview(urls: urls)
     }
 }
@@ -245,7 +245,7 @@ final class FilePreviewOverlayView: NSView, AppearanceRefreshable {
 
     private func printCurrentFile() {
         guard let url = currentURL() else { return }
-        PrintService.printFile(at: url, from: window)
+        guard PrintService.printFile(at: url, from: window) else { return }
 
         if currentIndex < urls.count - 1 {
             showFile(at: currentIndex + 1)

+ 0 - 45
smart_printer/FreeTierManager.swift

@@ -15,22 +15,6 @@ enum AppFeature: Sendable {
 
 @MainActor
 enum FreeTierManager {
-    static let dailyFreeScanLimit = 1
-
-    private enum Key {
-        static let scanDay = "freeTier.scanDay"
-        static let scanCount = "freeTier.scanCount"
-    }
-
-    private static let defaults = UserDefaults.standard
-    private static let dayFormatter: DateFormatter = {
-        let formatter = DateFormatter()
-        formatter.calendar = Calendar.current
-        formatter.locale = Locale(identifier: "en_US_POSIX")
-        formatter.dateFormat = "yyyy-MM-dd"
-        return formatter
-    }()
-
     static func canAccess(_ feature: AppFeature, isPremium: Bool) -> Bool {
         if isPremium { return true }
 
@@ -39,33 +23,4 @@ enum FreeTierManager {
             return false
         }
     }
-
-    static func remainingScansToday(isPremium: Bool) -> Int {
-        guard !isPremium else { return .max }
-        normalizeScanUsageForToday()
-        let used = defaults.integer(forKey: Key.scanCount)
-        return max(0, dailyFreeScanLimit - used)
-    }
-
-    static func recordScannerScanIfNeeded(isPremium: Bool) {
-        guard !isPremium else { return }
-        normalizeScanUsageForToday()
-        let used = defaults.integer(forKey: Key.scanCount)
-        defaults.set(used + 1, forKey: Key.scanCount)
-    }
-
-    static func scanLimitReachedMessage() -> (title: String, message: String) {
-        (
-            title: "Daily Free Scan Used",
-            message: "You've used today's free scan. Upgrade to Premium for unlimited scans, or try again tomorrow."
-        )
-    }
-
-    private static func normalizeScanUsageForToday() {
-        let today = dayFormatter.string(from: Date())
-        let storedDay = defaults.string(forKey: Key.scanDay)
-        guard storedDay != today else { return }
-        defaults.set(today, forKey: Key.scanDay)
-        defaults.set(0, forKey: Key.scanCount)
-    }
 }

+ 35 - 7
smart_printer/ImportedFilesStore.swift

@@ -6,7 +6,25 @@ enum ImportedFilesStore {
 
     static var fileURLs: [URL] {
         guard let bookmarks = defaults.array(forKey: bookmarksKey) as? [Data] else { return [] }
-        return bookmarks.compactMap { resolveBookmark($0) }
+        var staleIndices: [Int] = []
+        var urls: [URL] = []
+
+        for (index, data) in bookmarks.enumerated() {
+            switch resolveBookmarkData(data) {
+            case .valid(let url):
+                urls.append(url)
+            case .stale:
+                staleIndices.append(index)
+            case .invalid:
+                staleIndices.append(index)
+            }
+        }
+
+        if !staleIndices.isEmpty {
+            removeBookmarkData(at: staleIndices)
+        }
+
+        return urls
     }
 
     static func add(urls: [URL]) -> Int {
@@ -37,24 +55,34 @@ enum ImportedFilesStore {
         return added
     }
 
-    private static func resolveBookmark(_ data: Data) -> URL? {
+    private enum BookmarkResolution {
+        case valid(URL)
+        case stale
+        case invalid
+    }
+
+    private static func resolveBookmarkData(_ data: Data) -> BookmarkResolution {
         var isStale = false
         guard let url = try? URL(
             resolvingBookmarkData: data,
             options: .withSecurityScope,
             relativeTo: nil,
             bookmarkDataIsStale: &isStale
-        ) else { return nil }
+        ) else {
+            return .invalid
+        }
 
         if isStale {
-            remove(url)
+            return .stale
         }
-        return url
+        return .valid(url)
     }
 
-    private static func remove(_ url: URL) {
+    private static func removeBookmarkData(at indices: [Int]) {
         guard var bookmarks = defaults.array(forKey: bookmarksKey) as? [Data] else { return }
-        bookmarks.removeAll { resolveBookmark($0)?.path == url.path }
+        for index in indices.sorted(by: >) where bookmarks.indices.contains(index) {
+            bookmarks.remove(at: index)
+        }
         defaults.set(bookmarks, forKey: bookmarksKey)
     }
 }

+ 1 - 1
smart_printer/OCRFileView.swift

@@ -44,7 +44,7 @@ enum OCRFileService {
         let completion: (NSApplication.ModalResponse) -> Void = { response in
             guard response == .OK, let url = panel.url else { return }
             let hostWindow = window ?? NSApp.keyWindow
-            guard let viewController = hostWindow?.contentViewController as? ViewController else { return }
+            guard let viewController = PremiumAccess.mainViewController(from: hostWindow) else { return }
             viewController.presentOCRFile(imageURL: url)
         }
 

+ 15 - 2
smart_printer/PaywallView.swift

@@ -245,8 +245,9 @@ final class StoreManager {
         case .success(let verification):
             let transaction = try checkVerified(verification)
             await transaction.finish()
+            applyPremiumStatus(premiumAccessKind(for: transaction.productID))
             await refreshPremiumStatus()
-            return isPremium ? .purchased : .cancelled
+            return .purchased
         case .userCancelled:
             return .cancelled
         case .pending:
@@ -353,6 +354,16 @@ final class StoreManager {
         return .none
     }
 
+    private func premiumAccessKind(for productID: String) -> PremiumAccessKind {
+        if productID == StoreProductID.lifetime {
+            return .lifetime
+        }
+        if productID == StoreProductID.monthly || productID == StoreProductID.yearly {
+            return .subscription
+        }
+        return .none
+    }
+
     private func hasActiveEntitlement(for productID: String) async -> Bool {
         for await result in Transaction.currentEntitlements {
             guard let transaction = try? checkVerified(result) else { continue }
@@ -444,7 +455,9 @@ enum PremiumAccess {
     @MainActor
     static func canAccess(_ feature: AppFeature) -> Bool {
         let store = StoreManager.shared
-        guard !store.isResolvingEntitlements else { return false }
+        if store.isResolvingEntitlements {
+            return store.isPremium || FreeTierManager.canAccess(feature, isPremium: false)
+        }
         return FreeTierManager.canAccess(feature, isPremium: store.isPremium)
     }
 

+ 1 - 1
smart_printer/PhotoPreviewView.swift

@@ -7,7 +7,7 @@ enum PhotoPreviewService {
         guard !urls.isEmpty else { return }
         let hostWindow = window ?? NSApp.keyWindow
         guard PremiumAccess.require(feature: .photoPreview, from: hostWindow) else { return }
-        guard let viewController = hostWindow?.contentViewController as? ViewController else { return }
+        guard let viewController = PremiumAccess.mainViewController(from: hostWindow) else { return }
         viewController.presentPhotoPreview(urls: urls)
     }
 }

+ 64 - 24
smart_printer/PrintService.swift

@@ -5,12 +5,12 @@ import UniformTypeIdentifiers
 
 enum PrintService {
     @MainActor
-    private static func requirePremiumPrint(from window: NSWindow?) -> Bool {
-        PremiumAccess.require(feature: .printText, from: window ?? NSApp.keyWindow)
+    private static func requirePremium(_ feature: AppFeature, from window: NSWindow?) -> Bool {
+        PremiumAccess.require(feature: feature, from: window ?? NSApp.keyWindow)
     }
 
     static func printContacts(_ contacts: [PrintableContact], from window: NSWindow? = nil) {
-        guard requirePremiumPrint(from: window) else { return }
+        guard requirePremium(.printContacts, from: window) else { return }
         guard !contacts.isEmpty else {
             showNoContactsAlert()
             return
@@ -24,7 +24,7 @@ enum PrintService {
     }
 
     static func printCanvas(_ image: NSImage, from window: NSWindow? = nil) {
-        guard requirePremiumPrint(from: window) else { return }
+        guard requirePremium(.drawPrint, from: window) else { return }
         guard image.size.width > 0, image.size.height > 0 else {
             showEmptyCanvasAlert()
             return
@@ -33,7 +33,7 @@ enum PrintService {
     }
 
     static func printPhoto(_ image: NSImage, title: String = "Photo", from window: NSWindow? = nil) {
-        guard requirePremiumPrint(from: window) else { return }
+        guard requirePremium(.photoPreview, from: window) else { return }
         guard image.size.width > 0, image.size.height > 0 else {
             showPrintFailedAlert()
             return
@@ -41,7 +41,8 @@ enum PrintService {
         printImage(image, title: title, from: window)
     }
 
-    static func saveCanvasPDF(_ image: NSImage) {
+    static func saveCanvasPDF(_ image: NSImage, from window: NSWindow? = nil) {
+        guard requirePremium(.drawPrint, from: window) else { return }
         guard image.size.width > 0, image.size.height > 0 else {
             showEmptyCanvasAlert()
             return
@@ -67,7 +68,7 @@ enum PrintService {
     }
 
     static func printText(_ text: String, title: String = "Print Text", from window: NSWindow? = nil) {
-        guard requirePremiumPrint(from: window) else { return }
+        guard requirePremium(.printText, from: window) else { return }
         let trimmed = text.trimmingCharacters(in: .whitespacesAndNewlines)
         guard !trimmed.isEmpty else {
             showEmptyTextAlert()
@@ -85,6 +86,7 @@ enum PrintService {
         defaultName: String = "Scan",
         from window: NSWindow? = nil
     ) {
+        guard requirePremium(.scannerScan, from: window) else { return }
         guard hasPrintableContent(attributed) else {
             showEmptyTextAlert()
             return
@@ -114,7 +116,7 @@ enum PrintService {
         title: String = "Scan",
         from window: NSWindow? = nil
     ) {
-        guard requirePremiumPrint(from: window) else { return }
+        guard requirePremium(.scannerScan, from: window) else { return }
         guard hasPrintableContent(attributed) else {
             showEmptyTextAlert()
             return
@@ -131,6 +133,7 @@ enum PrintService {
         defaultName: String = "Scan",
         from window: NSWindow? = nil
     ) {
+        guard requirePremium(.scannerScan, from: window) else { return }
         guard !pages.isEmpty else {
             showEmptyTextAlert()
             return
@@ -160,7 +163,7 @@ enum PrintService {
         title: String = "Scan",
         from window: NSWindow? = nil
     ) {
-        guard requirePremiumPrint(from: window) else { return }
+        guard requirePremium(.scannerScan, from: window) else { return }
         guard !pages.isEmpty else {
             showEmptyTextAlert()
             return
@@ -177,6 +180,7 @@ enum PrintService {
         defaultName: String = "Scan",
         from window: NSWindow? = nil
     ) {
+        guard requirePremium(.printText, from: window) else { return }
         let trimmed = text.trimmingCharacters(in: .whitespacesAndNewlines)
         guard !trimmed.isEmpty else {
             showEmptyTextAlert()
@@ -203,7 +207,7 @@ enum PrintService {
     }
 
     static func print(urls: [URL], from window: NSWindow? = nil) {
-        guard requirePremiumPrint(from: window) else { return }
+        guard requirePremium(.filePreview, from: window) else { return }
         guard !urls.isEmpty else { return }
 
         let hostWindow = window ?? NSApp.keyWindow
@@ -216,14 +220,15 @@ enum PrintService {
         }
     }
 
-    static func printFile(at url: URL, from window: NSWindow? = nil) {
-        guard requirePremiumPrint(from: window) else { return }
+    @discardableResult
+    static func printFile(at url: URL, from window: NSWindow? = nil) -> Bool {
+        guard requirePremium(.filePreview, from: window) else { return false }
         let title = url.lastPathComponent
         guard let document = printablePDFDocument(from: url) else {
             showUnsupportedAlert(for: url)
-            return
+            return false
         }
-        printPDF(document, title: title, from: window ?? NSApp.keyWindow)
+        return printPDF(document, title: title, from: window ?? NSApp.keyWindow)
     }
 
     private static func contentType(for url: URL) -> UTType? {
@@ -280,10 +285,11 @@ enum PrintService {
         }
     }
 
-    private static func printPDF(_ document: PDFDocument, title: String, from window: NSWindow?) {
+    @discardableResult
+    private static func printPDF(_ document: PDFDocument, title: String, from window: NSWindow?) -> Bool {
         guard document.pageCount > 0 else {
             showPrintFailedAlert()
-            return
+            return false
         }
 
         let context = PrintOperationContext(document: document)
@@ -292,18 +298,20 @@ enum PrintService {
 
         let printInfo = configuredPrintInfo()
         let operation = NSPrintOperation(view: context.pdfView, printInfo: printInfo)
-        runPrintOperation(operation, title: title, from: window)
+        return runPrintOperation(operation, title: title, from: window)
     }
 
-    private static func printImage(_ image: NSImage, title: String, from window: NSWindow?) {
+    @discardableResult
+    private static func printImage(_ image: NSImage, title: String, from window: NSWindow?) -> Bool {
         guard let document = pdfDocument(from: image) else {
             showPrintFailedAlert()
-            return
+            return false
         }
-        printPDF(document, title: title, from: window)
+        return printPDF(document, title: title, from: window)
     }
 
-    private static func runPrintOperation(_ operation: NSPrintOperation, title: String, from window: NSWindow?) {
+    @discardableResult
+    private static func runPrintOperation(_ operation: NSPrintOperation, title: String, from window: NSWindow?) -> Bool {
         operation.showsPrintPanel = true
         operation.showsProgressPanel = true
         operation.jobTitle = title
@@ -320,16 +328,19 @@ enum PrintService {
                 didRun: #selector(PrintRatingCallback.printOperationDidRun(_:success:contextInfo:)),
                 contextInfo: nil
             )
+            let success = PrintRatingCallback.shared.lastSuccess
             PrintRatingCallback.shared.reset()
-        } else {
-            operation.run()
+            return success
         }
+
+        return operation.run()
     }
 
     private final class PrintRatingCallback: NSObject {
         static let shared = PrintRatingCallback()
 
         private var window: NSWindow?
+        private(set) var lastSuccess = false
 
         private override init() {
             super.init()
@@ -337,10 +348,12 @@ enum PrintService {
 
         func prepare(window: NSWindow?) {
             self.window = window
+            lastSuccess = false
         }
 
         func reset() {
             window = nil
+            lastSuccess = false
         }
 
         @objc func printOperationDidRun(
@@ -348,8 +361,9 @@ enum PrintService {
             success: Bool,
             contextInfo: UnsafeMutableRawPointer?
         ) {
+            lastSuccess = success
             let sourceWindow = window
-            reset()
+            window = nil
             guard success else { return }
             AppRatingManager.shared.recordSuccessfulPrint(from: sourceWindow)
         }
@@ -374,9 +388,35 @@ enum PrintService {
         printInfo.leftMargin = 36
         printInfo.rightMargin = 36
 
+        applyPrintSettings(to: printInfo)
+
         return printInfo
     }
 
+    private static func applyPrintSettings(to printInfo: NSPrintInfo) {
+        let settings = printInfo.dictionary()
+
+        let qualityValue: Int
+        switch AppSettings.printQuality {
+        case .draft: qualityValue = 0
+        case .normal: qualityValue = 1
+        case .best: qualityValue = 2
+        }
+        settings[NSPrintInfo.AttributeKey("NSPrintQuality")] = NSNumber(value: qualityValue)
+
+        if AppSettings.colorMode == .grayscale {
+            settings[NSPrintInfo.AttributeKey("NSPrintMode")] = "Gray"
+        } else {
+            settings.removeObject(forKey: NSPrintInfo.AttributeKey("NSPrintMode"))
+        }
+
+        if AppSettings.duplexEnabled {
+            settings[NSPrintInfo.AttributeKey("NSPrintDuplex")] = NSNumber(value: 1)
+        } else {
+            settings.removeObject(forKey: NSPrintInfo.AttributeKey("NSPrintDuplex"))
+        }
+    }
+
     private static func applyPrinter(to printInfo: NSPrintInfo) {
         let available = NSPrinter.printerNames
         guard !available.isEmpty else { return }

+ 33 - 5
smart_printer/ScannerService.swift

@@ -318,7 +318,10 @@ final class ScannerService: NSObject {
 
         if let error {
             state = .error(error.localizedDescription)
-            finishScan(with: .failure(error))
+            if scanCompletion != nil || shouldScanWhenReady {
+                shouldScanWhenReady = false
+                finishScan(with: .failure(error))
+            }
             return
         }
 
@@ -384,6 +387,13 @@ final class ScannerService: NSObject {
             isOpeningSession = false
             device.delegate = nil
             state = .error("Could not connect to the scanner. Select it again to retry.")
+            if scanCompletion != nil || shouldScanWhenReady {
+                shouldScanWhenReady = false
+                finishScan(with: .failure(ScanError.scanFailed))
+            } else {
+                shouldScanWhenReady = false
+                scanCompletion = nil
+            }
         }
     }
 
@@ -393,8 +403,16 @@ final class ScannerService: NSObject {
     }
 
     private func configureFunctionalUnit(on device: ICScannerDevice) {
+        paperSize = AppSettings.defaultPaperSize
+        resolution = AppSettings.scanResolution
+
         let unit = device.selectedFunctionalUnit
-        unit.pixelDataType = .RGB
+        switch AppSettings.colorMode {
+        case .color:
+            unit.pixelDataType = .RGB
+        case .grayscale:
+            unit.pixelDataType = .gray
+        }
         if let eightBit = ICScannerBitDepth(rawValue: 8) {
             unit.bitDepth = eightBit
         }
@@ -418,10 +436,18 @@ final class ScannerService: NSObject {
 
         device.transferMode = .fileBased
         device.documentName = "Scan"
-        device.documentUTI = "public.jpeg"
+        device.documentUTI = scanDocumentUTI
         device.downloadsDirectory = scanDownloadsDirectory()
     }
 
+    private var scanDocumentUTI: String {
+        switch AppSettings.scanFormat {
+        case .jpeg: return "public.jpeg"
+        case .png: return "public.png"
+        case .pdf: return "com.adobe.pdf"
+        }
+    }
+
     private func scanDownloadsDirectory() -> URL {
         let directory = FileManager.default.temporaryDirectory
             .appendingPathComponent("Scans", isDirectory: true)
@@ -438,8 +464,10 @@ final class ScannerService: NSObject {
     }
 
     private func isSharedDevice(_ device: ICDevice) -> Bool {
-        let locationBits = device.type.rawValue & ICDeviceLocationTypeMask.remote.rawValue
-        return locationBits != 0
+        if device.usbLocationID != 0 { return false }
+        let location = device.locationDescription?.lowercased() ?? ""
+        if location.contains("usb") { return false }
+        return true
     }
 
     private func categorize(_ device: ICScannerDevice, moreComing: Bool) {

+ 32 - 14
smart_printer/ViewController.swift

@@ -26,6 +26,9 @@ class ViewController: NSViewController {
     private var scanFileOverlay: ScanFileOverlayView?
     private var scannerOverlay: ScannerOverlayView?
     private var scannerDidComplete = false
+    private var scannerCancelled = false
+    private var scannerOutcomeDelivered = false
+    private var paywallReturnToHomeOnDismiss = true
 
     private var headerView: NSView!
     private var contentTopBelowHeader: NSLayoutConstraint!
@@ -87,12 +90,9 @@ class ViewController: NSViewController {
 
     private func navigateHomeAfterPremiumUnlock() {
         guard paywallOverlay != nil else { return }
+        paywallReturnToHomeOnDismiss = true
         paywallOverlay?.onDismiss = { [weak self] in
-            guard let self else { return }
-            self.setTrafficLightsHidden(false)
-            self.paywallOverlay = nil
-            self.sidebar.select(.home)
-            self.showDestination(.home)
+            self?.handlePaywallDismiss()
         }
         paywallOverlay?.dismiss()
     }
@@ -125,6 +125,8 @@ class ViewController: NSViewController {
         scanFileOverlay?.dismiss(animated: false)
 
         scannerDidComplete = false
+        scannerCancelled = false
+        scannerOutcomeDelivered = false
 
         if let existing = scannerOverlay, existing.superview != nil {
             configureScannerOverlay(existing, showScanPreview: showScanPreview, onComplete: onComplete)
@@ -148,15 +150,19 @@ class ViewController: NSViewController {
     ) {
         overlay.onDismiss = { [weak self] in
             guard let self else { return }
-            let cancelled = !self.scannerDidComplete && self.scannerOverlay?.hasResult != true
+            self.scannerCancelled = true
+            let cancelled = !self.scannerOutcomeDelivered
+                && !self.scannerDidComplete
+                && self.scannerOverlay?.hasResult != true
             self.scannerOverlay = nil
             if cancelled {
+                self.scannerOutcomeDelivered = true
                 onComplete(.failure(ScanError.cancelled))
             }
         }
         overlay.onScanComplete = { [weak self] image in
             Task { @MainActor [weak self] in
-                guard let self else { return }
+                guard let self, !self.scannerCancelled else { return }
                 let processed: NSImage
                 if let cgImage = image.cgImage(forProposedRect: nil, context: nil, hints: nil) {
                     processed = await ScanDocumentProcessor.process(cgImage) ?? image
@@ -164,11 +170,15 @@ class ViewController: NSViewController {
                     processed = image
                 }
 
+                guard !self.scannerCancelled else { return }
+
                 let displayImage = processed.normalizedForDisplay()
                 if showScanPreview {
                     overlay.updateScanPreview(displayImage)
                 } else {
+                    guard !self.scannerOutcomeDelivered else { return }
                     self.scannerDidComplete = true
+                    self.scannerOutcomeDelivered = true
                     onComplete(.success(displayImage))
                     overlay.dismiss()
                 }
@@ -207,6 +217,7 @@ class ViewController: NSViewController {
         contentContainer.isHidden = true
 
         if let overlay = printContactsOverlay {
+            contentContainer.isHidden = true
             overlay.present(in: mainContentView)
             return
         }
@@ -501,7 +512,11 @@ class ViewController: NSViewController {
     }
 
     func presentPaywall(returnToHomeOnDismiss: Bool = true) {
+        paywallReturnToHomeOnDismiss = returnToHomeOnDismiss
         if let paywallOverlay {
+            paywallOverlay.onDismiss = { [weak self] in
+                self?.handlePaywallDismiss()
+            }
             Task { @MainActor in
                 await StoreManager.shared.ensureEntitlementsResolved()
                 paywallOverlay.refreshStoreState()
@@ -511,18 +526,21 @@ class ViewController: NSViewController {
         setTrafficLightsHidden(true)
         let overlay = PaywallOverlayView()
         overlay.onDismiss = { [weak self] in
-            guard let self else { return }
-            self.setTrafficLightsHidden(false)
-            self.paywallOverlay = nil
-            if returnToHomeOnDismiss {
-                self.sidebar.select(.home)
-                self.showDestination(.home)
-            }
+            self?.handlePaywallDismiss()
         }
         overlay.present(in: view)
         paywallOverlay = overlay
     }
 
+    private func handlePaywallDismiss() {
+        setTrafficLightsHidden(false)
+        paywallOverlay = nil
+        if paywallReturnToHomeOnDismiss {
+            sidebar.select(.home)
+            showDestination(.home)
+        }
+    }
+
     private func guardPremiumFeature(_ feature: AppFeature) -> Bool {
         PremiumAccess.require(feature: feature, from: view.window)
     }