Forráskód Böngészése

Add StoreKit 2 purchases to the paywall with premium gating.

Wire up monthly, yearly, and lifetime plans with restore support, live pricing, and a local StoreKit configuration for testing.

Co-authored-by: Cursor <cursoragent@cursor.com>
AhtashamShahzad1 1 hónapja
szülő
commit
91fdff8b17

+ 163 - 0
Products.storekit

@@ -0,0 +1,163 @@
+{
+  "appPolicies" : {
+    "eula" : "",
+    "policies" : [
+      {
+        "locale" : "en_US",
+        "policyText" : "",
+        "policyURL" : ""
+      }
+    ]
+  },
+  "identifier" : "F3A1B2C4",
+  "nonRenewingSubscriptions" : [
+
+  ],
+  "products" : [
+    {
+      "displayPrice" : "99.99",
+      "familyShareable" : false,
+      "internalID" : "6738291001",
+      "localizations" : [
+        {
+          "description" : "Lifetime access to all premium features.",
+          "displayName" : "Lifetime Premium",
+          "locale" : "en_US"
+        }
+      ],
+      "productID" : "MQL-DEV.smart-printer.premium.lifetime",
+      "referenceName" : "Lifetime Premium",
+      "type" : "NonConsumable"
+    }
+  ],
+  "settings" : {
+    "_failTransactionsEnabled" : false,
+    "_storeKitErrors" : [
+      {
+        "current" : null,
+        "enabled" : false,
+        "name" : "Load Products"
+      },
+      {
+        "current" : null,
+        "enabled" : false,
+        "name" : "Purchase"
+      },
+      {
+        "current" : null,
+        "enabled" : false,
+        "name" : "Verification"
+      },
+      {
+        "current" : null,
+        "enabled" : false,
+        "name" : "App Store Sync"
+      },
+      {
+        "current" : null,
+        "enabled" : false,
+        "name" : "Subscription Status"
+      },
+      {
+        "current" : null,
+        "enabled" : false,
+        "name" : "App Transaction"
+      },
+      {
+        "current" : null,
+        "enabled" : false,
+        "name" : "Manage Subscriptions Sheet"
+      },
+      {
+        "current" : null,
+        "enabled" : false,
+        "name" : "Refund Request Sheet"
+      },
+      {
+        "current" : null,
+        "enabled" : false,
+        "name" : "Offer Code Redeem Sheet"
+      }
+    ]
+  },
+  "subscriptionGroups" : [
+    {
+      "id" : "21502901",
+      "localizations" : [
+        {
+          "description" : "Premium subscription for Smart Printer.",
+          "displayName" : "Premium",
+          "locale" : "en_US"
+        }
+      ],
+      "name" : "Premium",
+      "subscriptions" : [
+        {
+          "adHocOffers" : [
+
+          ],
+          "codeOffers" : [
+
+          ],
+          "displayPrice" : "4.99",
+          "familyShareable" : false,
+          "groupNumber" : 1,
+          "internalID" : "6738291002",
+          "introductoryOffer" : null,
+          "localizations" : [
+            {
+              "description" : "Monthly access to all premium features.",
+              "displayName" : "Monthly Premium",
+              "locale" : "en_US"
+            }
+          ],
+          "productID" : "MQL-DEV.smart-printer.premium.monthly",
+          "recurringSubscriptionPeriod" : "P1M",
+          "referenceName" : "Monthly Premium",
+          "subscriptionGroupID" : "21502901",
+          "type" : "RecurringSubscription",
+          "winbackOffers" : [
+
+          ]
+        },
+        {
+          "adHocOffers" : [
+
+          ],
+          "codeOffers" : [
+
+          ],
+          "displayPrice" : "29.99",
+          "familyShareable" : false,
+          "groupNumber" : 1,
+          "internalID" : "6738291003",
+          "introductoryOffer" : {
+            "internalID" : "6738291004",
+            "numberOfPeriods" : 1,
+            "paymentMode" : "free",
+            "subscriptionPeriod" : "P1W"
+          },
+          "localizations" : [
+            {
+              "description" : "Yearly access to all premium features with a free trial.",
+              "displayName" : "Yearly Premium",
+              "locale" : "en_US"
+            }
+          ],
+          "productID" : "MQL-DEV.smart-printer.premium.yearly",
+          "recurringSubscriptionPeriod" : "P1Y",
+          "referenceName" : "Yearly Premium",
+          "subscriptionGroupID" : "21502901",
+          "type" : "RecurringSubscription",
+          "winbackOffers" : [
+
+          ]
+        }
+      ]
+    }
+  ],
+  "version" : {
+    "major" : 4,
+    "minor" : 0
+  }
+}

+ 4 - 0
smart_printer.xcodeproj/project.pbxproj

@@ -8,6 +8,7 @@
 
 /* Begin PBXFileReference section */
 		272FF22A2FD19A2200A87B72 /* smart_printer.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = smart_printer.app; sourceTree = BUILT_PRODUCTS_DIR; };
+		272FF22E2FD19A2200A87B72 /* Products.storekit */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = Products.storekit; sourceTree = "<group>"; };
 /* End PBXFileReference section */
 
 /* Begin PBXFileSystemSynchronizedRootGroup section */
@@ -32,6 +33,7 @@
 		272FF2212FD19A2200A87B72 = {
 			isa = PBXGroup;
 			children = (
+				272FF22E2FD19A2200A87B72 /* Products.storekit */,
 				272FF22C2FD19A2200A87B72 /* smart_printer */,
 				272FF22B2FD19A2200A87B72 /* Products */,
 			);
@@ -248,6 +250,7 @@
 			buildSettings = {
 				ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
 				ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
+				CODE_SIGN_ENTITLEMENTS = smart_printer/smart_printer.entitlements;
 				CODE_SIGN_STYLE = Automatic;
 				COMBINE_HIDPI_IMAGES = YES;
 				CURRENT_PROJECT_VERSION = 1;
@@ -279,6 +282,7 @@
 			buildSettings = {
 				ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
 				ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
+				CODE_SIGN_ENTITLEMENTS = smart_printer/smart_printer.entitlements;
 				CODE_SIGN_STYLE = Automatic;
 				COMBINE_HIDPI_IMAGES = YES;
 				CURRENT_PROJECT_VERSION = 1;

+ 82 - 0
smart_printer.xcodeproj/xcshareddata/xcschemes/smart_printer.xcscheme

@@ -0,0 +1,82 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Scheme
+   LastUpgradeVersion = "2640"
+   version = "1.7">
+   <BuildAction
+      parallelizeBuildables = "YES"
+      buildImplicitDependencies = "YES"
+      buildArchitectures = "Automatic">
+      <BuildActionEntries>
+         <BuildActionEntry
+            buildForTesting = "YES"
+            buildForRunning = "YES"
+            buildForProfiling = "YES"
+            buildForArchiving = "YES"
+            buildForAnalyzing = "YES">
+            <BuildableReference
+               BuildableIdentifier = "primary"
+               BlueprintIdentifier = "272FF2292FD19A2200A87B72"
+               BuildableName = "smart_printer.app"
+               BlueprintName = "smart_printer"
+               ReferencedContainer = "container:smart_printer.xcodeproj">
+            </BuildableReference>
+         </BuildActionEntry>
+      </BuildActionEntries>
+   </BuildAction>
+   <TestAction
+      buildConfiguration = "Debug"
+      selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
+      selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
+      shouldUseLaunchSchemeArgsEnv = "YES"
+      shouldAutocreateTestPlan = "YES">
+   </TestAction>
+   <LaunchAction
+      buildConfiguration = "Debug"
+      selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
+      selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
+      launchStyle = "0"
+      useCustomWorkingDirectory = "NO"
+      ignoresPersistentStateOnLaunch = "NO"
+      debugDocumentVersioning = "YES"
+      debugServiceExtension = "internal"
+      allowLocationSimulation = "YES"
+      queueDebuggingEnableBacktraceRecording = "Yes">
+      <BuildableProductRunnable
+         runnableDebuggingMode = "0">
+         <BuildableReference
+            BuildableIdentifier = "primary"
+            BlueprintIdentifier = "272FF2292FD19A2200A87B72"
+            BuildableName = "smart_printer.app"
+            BlueprintName = "smart_printer"
+            ReferencedContainer = "container:smart_printer.xcodeproj">
+         </BuildableReference>
+      </BuildableProductRunnable>
+      <StoreKitConfigurationFileReference
+         identifier = "../../Products.storekit">
+      </StoreKitConfigurationFileReference>
+   </LaunchAction>
+   <ProfileAction
+      buildConfiguration = "Release"
+      shouldUseLaunchSchemeArgsEnv = "YES"
+      savedToolIdentifier = ""
+      useCustomWorkingDirectory = "NO"
+      debugDocumentVersioning = "YES">
+      <BuildableProductRunnable
+         runnableDebuggingMode = "0">
+         <BuildableReference
+            BuildableIdentifier = "primary"
+            BlueprintIdentifier = "272FF2292FD19A2200A87B72"
+            BuildableName = "smart_printer.app"
+            BlueprintName = "smart_printer"
+            ReferencedContainer = "container:smart_printer.xcodeproj">
+         </BuildableReference>
+      </BuildableProductRunnable>
+   </ProfileAction>
+   <AnalyzeAction
+      buildConfiguration = "Debug">
+   </AnalyzeAction>
+   <ArchiveAction
+      buildConfiguration = "Release"
+      revealArchiveInOrganizer = "YES">
+   </ArchiveAction>
+</Scheme>

+ 1 - 0
smart_printer/AppDelegate.swift

@@ -15,6 +15,7 @@ class AppDelegate: NSObject, NSApplicationDelegate {
     }
 
     func applicationDidFinishLaunching(_ notification: Notification) {
+        StoreManager.shared.start()
         resolveMainWindowController()
         configureMainWindow()
         configurePreferencesMenu()

+ 334 - 11
smart_printer/PaywallView.swift

@@ -1,4 +1,5 @@
 import Cocoa
+import StoreKit
 
 // MARK: - Plan Model
 
@@ -7,6 +8,14 @@ enum PaywallPlan: CaseIterable {
     case yearly
     case lifetime
 
+    var productID: String {
+        switch self {
+        case .monthly: StoreProductID.monthly
+        case .yearly: StoreProductID.yearly
+        case .lifetime: StoreProductID.lifetime
+        }
+    }
+
     var title: String {
         switch self {
         case .monthly: "Monthly"
@@ -38,6 +47,246 @@ enum PaywallPlan: CaseIterable {
         case .lifetime: "Buy Lifetime Access"
         }
     }
+
+    func localizedPrice(from product: Product?) -> String {
+        product?.displayPrice ?? price
+    }
+
+    func localizedSubtitle(from product: Product?) -> String {
+        guard let product else { return subtitle }
+
+        switch self {
+        case .monthly:
+            return "\(product.displayPrice) / month, cancel anytime"
+        case .yearly:
+            if product.subscription?.introductoryOffer != nil {
+                return "Eligible new subscribers get 7 days free, then \(product.displayPrice) / year"
+            }
+            return "\(product.displayPrice) / year, cancel anytime"
+        case .lifetime:
+            return "\(product.displayPrice) once, lifetime access"
+        }
+    }
+
+    func localizedCTATitle(from product: Product?) -> String {
+        guard let product else { return ctaTitle }
+
+        switch self {
+        case .monthly:
+            return "Subscribe for \(product.displayPrice) / Month"
+        case .yearly:
+            if product.subscription?.introductoryOffer != nil {
+                return "Start 7-Day Free Trial"
+            }
+            return "Subscribe for \(product.displayPrice) / Year"
+        case .lifetime:
+            return "Buy Lifetime Access for \(product.displayPrice)"
+        }
+    }
+}
+
+// MARK: - StoreKit
+
+enum StoreProductID {
+    static let monthly = "MQL-DEV.smart-printer.premium.monthly"
+    static let yearly = "MQL-DEV.smart-printer.premium.yearly"
+    static let lifetime = "MQL-DEV.smart-printer.premium.lifetime"
+
+    static let all: Set<String> = [monthly, yearly, lifetime]
+}
+
+enum StoreError: LocalizedError {
+    case productNotFound
+    case failedVerification
+
+    var errorDescription: String? {
+        switch self {
+        case .productNotFound:
+            "The selected plan is not available right now. Please try again later."
+        case .failedVerification:
+            "We couldn't verify your purchase. Please contact support."
+        }
+    }
+}
+
+@MainActor
+final class StoreManager {
+    static let shared = StoreManager()
+
+    private(set) var products: [Product] = []
+    private(set) var isPremium = false
+    private(set) var isLoadingProducts = false
+    private(set) var isPurchasing = false
+
+    private var transactionListener: Task<Void, Never>?
+    private var hasStarted = false
+
+    private init() {}
+
+    func start() {
+        guard !hasStarted else { return }
+        hasStarted = true
+
+        transactionListener = Task { [weak self] in
+            for await update in Transaction.updates {
+                await self?.handleTransactionUpdate(update)
+            }
+        }
+
+        Task {
+            await loadProducts()
+            await refreshPremiumStatus()
+        }
+    }
+
+    func product(for plan: PaywallPlan) -> Product? {
+        products.first { $0.id == plan.productID }
+    }
+
+    func loadProducts() async {
+        isLoadingProducts = true
+        postStoreStateDidChange()
+        defer {
+            isLoadingProducts = false
+            postStoreStateDidChange()
+        }
+
+        do {
+            products = try await Product.products(for: StoreProductID.all)
+                .sorted { lhs, rhs in
+                    productSortOrder(for: lhs.id) < productSortOrder(for: rhs.id)
+                }
+            NotificationCenter.default.post(name: .storeProductsDidUpdate, object: nil)
+        } catch {
+            NSLog("Failed to load products: \(error.localizedDescription)")
+        }
+    }
+
+    @discardableResult
+    func purchase(plan: PaywallPlan) async throws -> Bool {
+        if products.isEmpty {
+            await loadProducts()
+        }
+
+        guard let product = product(for: plan) else {
+            throw StoreError.productNotFound
+        }
+
+        isPurchasing = true
+        postStoreStateDidChange()
+        defer {
+            isPurchasing = false
+            postStoreStateDidChange()
+        }
+
+        let result = try await product.purchase()
+
+        switch result {
+        case .success(let verification):
+            let transaction = try checkVerified(verification)
+            await transaction.finish()
+            await refreshPremiumStatus()
+            return isPremium
+        case .userCancelled, .pending:
+            return false
+        @unknown default:
+            return false
+        }
+    }
+
+    @discardableResult
+    func restorePurchases() async throws -> Bool {
+        isPurchasing = true
+        postStoreStateDidChange()
+        defer {
+            isPurchasing = false
+            postStoreStateDidChange()
+        }
+
+        try await AppStore.sync()
+        await refreshPremiumStatus()
+        return isPremium
+    }
+
+    func showAlert(title: String, message: String, on window: NSWindow?) {
+        let alert = NSAlert()
+        alert.messageText = title
+        alert.informativeText = message
+        alert.alertStyle = .informational
+        alert.addButton(withTitle: "OK")
+        if let window {
+            alert.beginSheetModal(for: window)
+        } else {
+            alert.runModal()
+        }
+    }
+
+    func showPurchaseError(_ error: Error, on window: NSWindow?) {
+        if let storeError = error as? StoreError {
+            showAlert(title: "Purchase Failed", message: storeError.localizedDescription, on: window)
+            return
+        }
+
+        if let storeKitError = error as? StoreKitError, case .userCancelled = storeKitError {
+            return
+        }
+
+        showAlert(title: "Purchase Failed", message: error.localizedDescription, on: window)
+    }
+
+    private func handleTransactionUpdate(_ update: VerificationResult<Transaction>) async {
+        do {
+            let transaction = try checkVerified(update)
+            await transaction.finish()
+            await refreshPremiumStatus()
+        } catch {
+            NSLog("Transaction verification failed: \(error.localizedDescription)")
+        }
+    }
+
+    private func refreshPremiumStatus() async {
+        var hasPremium = false
+
+        for await result in Transaction.currentEntitlements {
+            guard let transaction = try? checkVerified(result) else { continue }
+            if StoreProductID.all.contains(transaction.productID) {
+                hasPremium = true
+                break
+            }
+        }
+
+        guard hasPremium != isPremium else { return }
+        isPremium = hasPremium
+        NotificationCenter.default.post(name: .premiumStatusDidChange, object: nil)
+    }
+
+    private func checkVerified<T>(_ result: VerificationResult<T>) throws -> T {
+        switch result {
+        case .unverified:
+            throw StoreError.failedVerification
+        case .verified(let safe):
+            return safe
+        }
+    }
+
+    private func productSortOrder(for productID: String) -> Int {
+        switch productID {
+        case StoreProductID.monthly: 0
+        case StoreProductID.yearly: 1
+        case StoreProductID.lifetime: 2
+        default: 99
+        }
+    }
+
+    private func postStoreStateDidChange() {
+        NotificationCenter.default.post(name: .storeStateDidChange, object: nil)
+    }
+}
+
+extension Notification.Name {
+    static let premiumStatusDidChange = Notification.Name("premiumStatusDidChange")
+    static let storeProductsDidUpdate = Notification.Name("storeProductsDidUpdate")
+    static let storeStateDidChange = Notification.Name("storeStateDidChange")
 }
 
 // MARK: - Left Panel
@@ -275,6 +524,11 @@ private final class PaywallPlanCard: NSControl, AppearanceRefreshable {
     @available(*, unavailable)
     required init?(coder: NSCoder) { nil }
 
+    func updateDisplay(product: Product?) {
+        subtitleLabel.stringValue = plan.localizedSubtitle(from: product)
+        priceLabel.stringValue = plan.localizedPrice(from: product)
+    }
+
     func refreshAppearance() {
         updateAppearance()
         subtitleLabel.refreshThemeLabelColor()
@@ -474,8 +728,7 @@ private final class PaywallCTAButton: NSButton, AppearanceRefreshable {
 
 final class PaywallView: NSView, AppearanceRefreshable {
     var onClose: (() -> Void)?
-    var onPurchase: ((PaywallPlan) -> Void)?
-    var onRestore: (() -> Void)?
+    var onPurchaseSucceeded: (() -> Void)?
 
     private var selectedPlan: PaywallPlan = .yearly
     private var planCards: [PaywallPlan: PaywallPlanCard] = [:]
@@ -484,6 +737,7 @@ final class PaywallView: NSView, AppearanceRefreshable {
     private var rightTitle: NSTextField!
     private var rightSubtitle: NSTextField!
     private var trustStack: NSStackView!
+    private var storeObservers: [NSObjectProtocol] = []
 
     init() {
         super.init(frame: .zero)
@@ -491,9 +745,46 @@ final class PaywallView: NSView, AppearanceRefreshable {
         wantsLayer = true
         layer?.cornerRadius = 0
         setup()
+        observeStoreUpdates()
+        refreshProductDisplay()
         refreshAppearance()
     }
 
+    deinit {
+        storeObservers.forEach { NotificationCenter.default.removeObserver($0) }
+    }
+
+    private func observeStoreUpdates() {
+        let center = NotificationCenter.default
+        storeObservers = [
+            center.addObserver(forName: .storeProductsDidUpdate, object: nil, queue: .main) { [weak self] _ in
+                self?.refreshProductDisplay()
+            },
+            center.addObserver(forName: .storeStateDidChange, object: nil, queue: .main) { [weak self] _ in
+                self?.refreshPurchaseState()
+            },
+            center.addObserver(forName: .premiumStatusDidChange, object: nil, queue: .main) { [weak self] _ in
+                self?.refreshPurchaseState()
+            },
+        ]
+    }
+
+    private func refreshProductDisplay() {
+        let store = StoreManager.shared
+        for (plan, card) in planCards {
+            card.updateDisplay(product: store.product(for: plan))
+        }
+        ctaButton.title = selectedPlan.localizedCTATitle(from: store.product(for: selectedPlan))
+        refreshPurchaseState()
+    }
+
+    private func refreshPurchaseState() {
+        let store = StoreManager.shared
+        let isBusy = store.isPurchasing || store.isLoadingProducts
+        ctaButton.isEnabled = !isBusy
+        ctaButton.alphaValue = isBusy ? 0.65 : 1
+    }
+
     func refreshAppearance() {
         layer?.backgroundColor = AppTheme.paywallBackground.cgColor
         leftPanelTitle?.refreshThemeLabelColor()
@@ -607,7 +898,7 @@ final class PaywallView: NSView, AppearanceRefreshable {
             plansStack.addArrangedSubview(card)
         }
 
-        ctaButton.title = selectedPlan.ctaTitle
+        ctaButton.title = selectedPlan.localizedCTATitle(from: StoreManager.shared.product(for: selectedPlan))
         ctaButton.target = self
         ctaButton.action = #selector(purchaseTapped)
         ctaButton.translatesAutoresizingMaskIntoConstraints = false
@@ -756,15 +1047,45 @@ final class PaywallView: NSView, AppearanceRefreshable {
         for (key, card) in planCards {
             card.isChosen = key == plan
         }
-        ctaButton.title = plan.ctaTitle
+        ctaButton.title = plan.localizedCTATitle(from: StoreManager.shared.product(for: plan))
     }
 
     @objc private func purchaseTapped() {
-        onPurchase?(selectedPlan)
+        Task { @MainActor in
+            refreshPurchaseState()
+            do {
+                let succeeded = try await StoreManager.shared.purchase(plan: selectedPlan)
+                refreshPurchaseState()
+                if succeeded {
+                    onPurchaseSucceeded?()
+                }
+            } catch {
+                refreshPurchaseState()
+                StoreManager.shared.showPurchaseError(error, on: window)
+            }
+        }
     }
 
     @objc private func restoreTapped() {
-        onRestore?()
+        Task { @MainActor in
+            refreshPurchaseState()
+            do {
+                let restored = try await StoreManager.shared.restorePurchases()
+                refreshPurchaseState()
+                if restored {
+                    onPurchaseSucceeded?()
+                } else {
+                    StoreManager.shared.showAlert(
+                        title: "No Purchases Found",
+                        message: "We couldn't find any previous purchases for this Apple ID.",
+                        on: window
+                    )
+                }
+            } catch {
+                refreshPurchaseState()
+                StoreManager.shared.showPurchaseError(error, on: window)
+            }
+        }
     }
 
     @objc private func continueWithFreePlanTapped() {
@@ -815,11 +1136,8 @@ final class PaywallOverlayView: NSView, AppearanceRefreshable {
 
         paywallView.translatesAutoresizingMaskIntoConstraints = false
         paywallView.onClose = { [weak self] in self?.dismiss() }
-        paywallView.onPurchase = { plan in
-            NSLog("Purchase tapped: \(plan.title)")
-        }
-        paywallView.onRestore = {
-            NSLog("Restore purchases tapped")
+        paywallView.onPurchaseSucceeded = { [weak self] in
+            self?.dismiss()
         }
 
         addSubview(blurView)
@@ -871,6 +1189,11 @@ final class PaywallOverlayView: NSView, AppearanceRefreshable {
             ])
         }
         alphaValue = 0
+        Task { @MainActor in
+            if StoreManager.shared.products.isEmpty {
+                await StoreManager.shared.loadProducts()
+            }
+        }
         NSAnimationContext.runAnimationGroup { context in
             context.duration = 0.2
             animator().alphaValue = 1

+ 13 - 1
smart_printer/ViewController.swift

@@ -43,6 +43,12 @@ class ViewController: NSViewController {
             name: .appearanceDidChange,
             object: nil
         )
+        NotificationCenter.default.addObserver(
+            self,
+            selector: #selector(premiumStatusDidChange),
+            name: .premiumStatusDidChange,
+            object: nil
+        )
     }
 
     deinit {
@@ -57,6 +63,12 @@ class ViewController: NSViewController {
         refreshAppearance()
     }
 
+    @objc private func premiumStatusDidChange() {
+        if StoreManager.shared.isPremium, paywallOverlay != nil {
+            dismissPaywall()
+        }
+    }
+
     private func refreshAppearance() {
         view.layer?.backgroundColor = AppTheme.background.cgColor
         mainContentView?.layer?.backgroundColor = AppTheme.background.cgColor
@@ -153,7 +165,7 @@ class ViewController: NSViewController {
         contentTopBelowHeader.isActive = !isSettings
         contentTopBelowWindow.isActive = isSettings
 
-        if destination == .scanAndHome {
+        if destination == .scanAndHome, !StoreManager.shared.isPremium {
             presentPaywall()
         } else {
             dismissPaywall()

+ 12 - 0
smart_printer/smart_printer.entitlements

@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+	<key>com.apple.security.app-sandbox</key>
+	<true/>
+	<key>com.apple.security.files.user-selected.read-only</key>
+	<true/>
+	<key>com.apple.security.network.client</key>
+	<true/>
+</dict>
+</plist>