Quellcode durchsuchen

Require premium for print and scan, and harden print dialog handling.

Remove the free print-text and daily scan allowances so non-premium users see the paywall at scan/print entry points, while fixing print panel crashes by retaining print context and selecting a valid default printer.

Co-authored-by: Cursor <cursoragent@cursor.com>
AhtashamShahzad1 vor 3 Wochen
Ursprung
Commit
8751b42917

+ 1 - 5
smart_printer/FreeTierManager.swift

@@ -35,11 +35,7 @@ enum FreeTierManager {
         if isPremium { return true }
 
         switch feature {
-        case .printText:
-            return true
-        case .scannerScan:
-            return remainingScansToday(isPremium: false) > 0
-        case .scanImport, .printContacts, .drawPrint, .ocrFile, .photoPreview, .filePreview, .documentImport:
+        case .printText, .scannerScan, .scanImport, .printContacts, .drawPrint, .ocrFile, .photoPreview, .filePreview, .documentImport:
             return false
         }
     }

+ 1 - 8
smart_printer/PaywallView.swift

@@ -444,7 +444,7 @@ enum PremiumAccess {
     @MainActor
     static func canAccess(_ feature: AppFeature) -> Bool {
         let store = StoreManager.shared
-        guard !store.isResolvingEntitlements else { return feature == .printText }
+        guard !store.isResolvingEntitlements else { return false }
         return FreeTierManager.canAccess(feature, isPremium: store.isPremium)
     }
 
@@ -483,13 +483,6 @@ enum PremiumAccess {
             return
         }
 
-        if feature == .scannerScan,
-           !StoreManager.shared.isPremium,
-           FreeTierManager.remainingScansToday(isPremium: false) == 0 {
-            let alert = FreeTierManager.scanLimitReachedMessage()
-            StoreManager.shared.showAlert(title: alert.title, message: alert.message, on: window)
-        }
-
         guard let viewController = mainViewController(from: window) else { return }
         viewController.presentPaywall(returnToHomeOnDismiss: returnToHomeOnDismiss)
     }

+ 68 - 79
smart_printer/PrintService.swift

@@ -4,7 +4,13 @@ import PDFKit
 import UniformTypeIdentifiers
 
 enum PrintService {
+    @MainActor
+    private static func requirePremiumPrint(from window: NSWindow?) -> Bool {
+        PremiumAccess.require(feature: .printText, from: window ?? NSApp.keyWindow)
+    }
+
     static func printContacts(_ contacts: [PrintableContact], from window: NSWindow? = nil) {
+        guard requirePremiumPrint(from: window) else { return }
         guard !contacts.isEmpty else {
             showNoContactsAlert()
             return
@@ -18,6 +24,7 @@ enum PrintService {
     }
 
     static func printCanvas(_ image: NSImage, from window: NSWindow? = nil) {
+        guard requirePremiumPrint(from: window) else { return }
         guard image.size.width > 0, image.size.height > 0 else {
             showEmptyCanvasAlert()
             return
@@ -26,6 +33,7 @@ enum PrintService {
     }
 
     static func printPhoto(_ image: NSImage, title: String = "Photo", from window: NSWindow? = nil) {
+        guard requirePremiumPrint(from: window) else { return }
         guard image.size.width > 0, image.size.height > 0 else {
             showPrintFailedAlert()
             return
@@ -59,6 +67,7 @@ enum PrintService {
     }
 
     static func printText(_ text: String, title: String = "Print Text", from window: NSWindow? = nil) {
+        guard requirePremiumPrint(from: window) else { return }
         let trimmed = text.trimmingCharacters(in: .whitespacesAndNewlines)
         guard !trimmed.isEmpty else {
             showEmptyTextAlert()
@@ -105,6 +114,7 @@ enum PrintService {
         title: String = "Scan",
         from window: NSWindow? = nil
     ) {
+        guard requirePremiumPrint(from: window) else { return }
         guard hasPrintableContent(attributed) else {
             showEmptyTextAlert()
             return
@@ -150,6 +160,7 @@ enum PrintService {
         title: String = "Scan",
         from window: NSWindow? = nil
     ) {
+        guard requirePremiumPrint(from: window) else { return }
         guard !pages.isEmpty else {
             showEmptyTextAlert()
             return
@@ -192,6 +203,7 @@ enum PrintService {
     }
 
     static func print(urls: [URL], from window: NSWindow? = nil) {
+        guard requirePremiumPrint(from: window) else { return }
         guard !urls.isEmpty else { return }
 
         let hostWindow = window ?? NSApp.keyWindow
@@ -205,6 +217,7 @@ enum PrintService {
     }
 
     static func printFile(at url: URL, from window: NSWindow? = nil) {
+        guard requirePremiumPrint(from: window) else { return }
         let title = url.lastPathComponent
         guard let document = printablePDFDocument(from: url) else {
             showUnsupportedAlert(for: url)
@@ -253,22 +266,32 @@ enum PrintService {
         return nil
     }
 
+    private static var activePrintContext: PrintOperationContext?
+
+    private final class PrintOperationContext {
+        let document: PDFDocument
+        let pdfView: PDFView
+
+        init(document: PDFDocument) {
+            self.document = document
+            self.pdfView = PDFView(frame: NSRect(x: 0, y: 0, width: 612, height: 792))
+            self.pdfView.document = document
+            self.pdfView.autoScales = true
+        }
+    }
+
     private static func printPDF(_ document: PDFDocument, title: String, from window: NSWindow?) {
-        guard let data = document.dataRepresentation(),
-              let printable = PDFDocument(data: data) else {
+        guard document.pageCount > 0 else {
             showPrintFailedAlert()
             return
         }
 
+        let context = PrintOperationContext(document: document)
+        activePrintContext = context
+        defer { activePrintContext = nil }
+
         let printInfo = configuredPrintInfo()
-        guard let operation = printable.printOperation(
-            for: printInfo,
-            scalingMode: .pageScaleToFit,
-            autoRotate: false
-        ) else {
-            showPrintFailedAlert()
-            return
-        }
+        let operation = NSPrintOperation(view: context.pdfView, printInfo: printInfo)
         runPrintOperation(operation, title: title, from: window)
     }
 
@@ -282,108 +305,59 @@ enum PrintService {
 
     private static func runPrintOperation(_ operation: NSPrintOperation, title: String, from window: NSWindow?) {
         operation.showsPrintPanel = true
-        operation.showsProgressPanel = false
+        operation.showsProgressPanel = true
         operation.jobTitle = title
-        operation.printPanel.options.insert([
-            .showsCopies,
-            .showsPageRange,
-            .showsPaperSize,
-            .showsOrientation,
-            .showsScaling,
-        ])
 
         NSApp.activate(ignoringOtherApps: true)
         let parentWindow = window ?? NSApp.keyWindow ?? NSApp.mainWindow
         parentWindow?.makeKeyAndOrderFront(nil)
 
-        let observer = observePrintPanelAppearance(relativeTo: parentWindow)
-        defer { NotificationCenter.default.removeObserver(observer) }
-
         if let parentWindow {
-            let callback = PrintRatingCallback(window: parentWindow)
+            PrintRatingCallback.shared.prepare(window: parentWindow)
             operation.runModal(
                 for: parentWindow,
-                delegate: callback,
+                delegate: PrintRatingCallback.shared,
                 didRun: #selector(PrintRatingCallback.printOperationDidRun(_:success:contextInfo:)),
                 contextInfo: nil
             )
-            withExtendedLifetime(callback) {}
+            PrintRatingCallback.shared.reset()
         } else {
             operation.run()
         }
     }
 
     private final class PrintRatingCallback: NSObject {
-        private let window: NSWindow?
+        static let shared = PrintRatingCallback()
 
-        init(window: NSWindow?) {
+        private var window: NSWindow?
+
+        private override init() {
+            super.init()
+        }
+
+        func prepare(window: NSWindow?) {
             self.window = window
         }
 
+        func reset() {
+            window = nil
+        }
+
         @objc func printOperationDidRun(
             _ printOperation: NSPrintOperation,
             success: Bool,
             contextInfo: UnsafeMutableRawPointer?
         ) {
+            let sourceWindow = window
+            reset()
             guard success else { return }
-            Task { @MainActor in
-                AppRatingManager.shared.recordSuccessfulPrint(from: window)
-            }
-        }
-    }
-
-    private static func observePrintPanelAppearance(relativeTo parentWindow: NSWindow?) -> NSObjectProtocol {
-        NotificationCenter.default.addObserver(
-            forName: NSWindow.didBecomeKeyNotification,
-            object: nil,
-            queue: .main
-        ) { notification in
-            guard let panelWindow = notification.object as? NSWindow,
-                  isPrintPanelWindow(panelWindow) else { return }
-            configurePrintPanelWindow(panelWindow, relativeTo: parentWindow)
-            DispatchQueue.main.async {
-                configurePrintPanelWindow(panelWindow, relativeTo: parentWindow)
-            }
-        }
-    }
-
-    private static func configurePrintPanelWindow(_ panelWindow: NSWindow, relativeTo parentWindow: NSWindow?) {
-        hideTrafficLights(on: panelWindow)
-        if let parentWindow {
-            center(panelWindow, relativeTo: parentWindow)
-        }
-    }
-
-    private static func center(_ panelWindow: NSWindow, relativeTo parentWindow: NSWindow) {
-        let parentFrame = parentWindow.frame
-        var panelFrame = panelWindow.frame
-        panelFrame.origin.x = parentFrame.midX - panelFrame.width / 2
-        panelFrame.origin.y = parentFrame.midY - panelFrame.height / 2
-        panelWindow.setFrame(panelFrame, display: true)
-    }
-
-    private static func isPrintPanelWindow(_ window: NSWindow) -> Bool {
-        let className = String(describing: type(of: window))
-        if className.localizedCaseInsensitiveContains("printpanel") {
-            return true
-        }
-        return window.title == "Print"
-    }
-
-    private static func hideTrafficLights(on window: NSWindow) {
-        for buttonType: NSWindow.ButtonType in [.closeButton, .miniaturizeButton, .zoomButton] {
-            window.standardWindowButton(buttonType)?.isHidden = true
+            AppRatingManager.shared.recordSuccessfulPrint(from: sourceWindow)
         }
     }
 
     private static func configuredPrintInfo() -> NSPrintInfo {
         let printInfo = (NSPrintInfo.shared.copy() as? NSPrintInfo) ?? NSPrintInfo()
-
-        let printerName = AppSettings.effectiveDefaultPrinter
-        if let printer = NSPrinter(name: printerName),
-           NSPrinter.printerNames.contains(printerName) {
-            printInfo.printer = printer
-        }
+        applyPrinter(to: printInfo)
 
         switch AppSettings.defaultPaperSize {
         case .a4: printInfo.paperName = NSPrinter.PaperName("iso-a4")
@@ -403,6 +377,21 @@ enum PrintService {
         return printInfo
     }
 
+    private static func applyPrinter(to printInfo: NSPrintInfo) {
+        let available = NSPrinter.printerNames
+        guard !available.isEmpty else { return }
+
+        let preferred = AppSettings.effectiveDefaultPrinter
+        if available.contains(preferred), let printer = NSPrinter(name: preferred) {
+            printInfo.printer = printer
+            return
+        }
+
+        if let printer = NSPrinter(name: available[0]) {
+            printInfo.printer = printer
+        }
+    }
+
     private static func hasPrintableContent(_ attributed: NSAttributedString) -> Bool {
         if !attributed.string.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
             return true

+ 7 - 26
smart_printer/ScanFileView.swift

@@ -33,24 +33,12 @@ enum ScanError: LocalizedError {
 enum ScanFileService {
     static func present(from window: NSWindow?) {
         let hostWindow = window ?? NSApp.keyWindow
+        guard PremiumAccess.require(feature: .scannerScan, from: hostWindow) else { return }
 
-        if StoreManager.shared.isPremium {
-            showSourcePicker(from: hostWindow, allowsImport: true) { source in
-                guard let source else { return }
-                handleScanSource(source, from: hostWindow)
-            }
-            return
-        }
-
-        if FreeTierManager.remainingScansToday(isPremium: false) > 0 {
-            showSourcePicker(from: hostWindow, allowsImport: false) { source in
-                guard let source else { return }
-                handleScanSource(source, from: hostWindow)
-            }
-            return
+        showSourcePicker(from: hostWindow) { source in
+            guard let source else { return }
+            handleScanSource(source, from: hostWindow)
         }
-
-        guard PremiumAccess.require(feature: .scannerScan, from: hostWindow) else { return }
     }
 
     private static func handleScanSource(_ source: ScanSource, from window: NSWindow?) {
@@ -69,28 +57,21 @@ enum ScanFileService {
 
     private static func showSourcePicker(
         from window: NSWindow?,
-        allowsImport: Bool,
         completion: @escaping (ScanSource?) -> Void
     ) {
         let alert = NSAlert()
         alert.messageText = "Scan Document"
-        if allowsImport {
-            alert.informativeText = "Scan with a connected scanner, or import an image or PDF."
-        } else {
-            alert.informativeText = "Use your free daily scan with a connected scanner. Import and unlimited scans are available with Premium."
-        }
+        alert.informativeText = "Scan with a connected scanner, or import an image or PDF."
         alert.alertStyle = .informational
         alert.addButton(withTitle: "Use Scanner")
-        if allowsImport {
-            alert.addButton(withTitle: "Import File")
-        }
+        alert.addButton(withTitle: "Import File")
         alert.addButton(withTitle: "Cancel")
 
         let handler: (NSApplication.ModalResponse) -> Void = { response in
             switch response {
             case .alertFirstButtonReturn:
                 completion(.scanner)
-            case .alertSecondButtonReturn where allowsImport:
+            case .alertSecondButtonReturn:
                 completion(.importFile)
             default:
                 completion(nil)

+ 30 - 22
smart_printer/ViewController.swift

@@ -26,7 +26,6 @@ class ViewController: NSViewController {
     private var scanFileOverlay: ScanFileOverlayView?
     private var scannerOverlay: ScannerOverlayView?
     private var scannerDidComplete = false
-    private var didRecordFreeScanThisSession = false
 
     private var headerView: NSView!
     private var contentTopBelowHeader: NSLayoutConstraint!
@@ -126,7 +125,6 @@ class ViewController: NSViewController {
         scanFileOverlay?.dismiss(animated: false)
 
         scannerDidComplete = false
-        didRecordFreeScanThisSession = false
 
         if let existing = scannerOverlay, existing.superview != nil {
             configureScannerOverlay(existing, showScanPreview: showScanPreview, onComplete: onComplete)
@@ -168,18 +166,9 @@ class ViewController: NSViewController {
 
                 let displayImage = processed.normalizedForDisplay()
                 if showScanPreview {
-                    if !self.didRecordFreeScanThisSession {
-                        FreeTierManager.recordScannerScanIfNeeded(
-                            isPremium: StoreManager.shared.isPremium
-                        )
-                        self.didRecordFreeScanThisSession = true
-                    }
                     overlay.updateScanPreview(displayImage)
                 } else {
                     self.scannerDidComplete = true
-                    FreeTierManager.recordScannerScanIfNeeded(
-                        isPremium: StoreManager.shared.isPremium
-                    )
                     onComplete(.success(displayImage))
                     overlay.dismiss()
                 }
@@ -250,6 +239,7 @@ class ViewController: NSViewController {
     }
 
     func presentScanFile(pages: [ScanPage]) {
+        guard guardPremiumFeature(.scannerScan) else { return }
         photoPreviewOverlay?.dismiss(animated: false)
         filePreviewOverlay?.dismiss(animated: false)
         printTextOverlay?.dismiss(animated: false)
@@ -466,6 +456,35 @@ class ViewController: NSViewController {
     }
 
     private func showDestinationAfterEntitlementCheck(_ destination: SidebarDestination) {
+        if destination == .scanAndHome {
+            if StoreManager.shared.isPremium {
+                sidebar.select(.scan)
+                showDestinationAfterEntitlementCheck(.scan)
+            } else {
+                presentPaywall(returnToHomeOnDismiss: true)
+            }
+            return
+        }
+
+        if destination == .scan, !StoreManager.shared.isPremium {
+            sidebar.select(.home)
+            showDestinationContent(.home)
+            presentPaywall(returnToHomeOnDismiss: true)
+            return
+        }
+
+        if destination == .print, !StoreManager.shared.isPremium {
+            sidebar.select(.home)
+            showDestinationContent(.home)
+            presentPaywall(returnToHomeOnDismiss: true)
+            return
+        }
+
+        showDestinationContent(destination)
+        dismissPaywall()
+    }
+
+    private func showDestinationContent(_ destination: SidebarDestination) {
         dismissFeatureOverlays()
         contentContainer.isHidden = false
 
@@ -479,17 +498,6 @@ class ViewController: NSViewController {
         headerView.isHidden = isSettings
         contentTopBelowHeader.isActive = !isSettings
         contentTopBelowWindow.isActive = isSettings
-
-        if destination == .scanAndHome {
-            if StoreManager.shared.isPremium {
-                sidebar.select(.scan)
-                showDestinationAfterEntitlementCheck(.scan)
-            } else {
-                presentPaywall(returnToHomeOnDismiss: true)
-            }
-        } else {
-            dismissPaywall()
-        }
     }
 
     func presentPaywall(returnToHomeOnDismiss: Bool = true) {

+ 1 - 1
smart_printer/paywall.json

@@ -102,7 +102,7 @@
   "sidebar": {
     "unlockTitle": "Unlock Premium",
     "proTitle": "Smart Printer Pro",
-    "unlockDescription": "Free plan includes Print Text and 1 scan per day. Upgrade for unlimited access.",
+    "unlockDescription": "Upgrade to Premium for unlimited scans, printing, OCR, and all features.",
     "proDescription": "You have unlimited scans, printing, OCR and all premium features.",
     "upgradeAction": "Upgrade Now",
     "manageSubscriptionAction": "Manage Subscription",