Browse Source

Harden paywall for production release readiness.

Remove the free-tier reset from Release builds, align fallback pricing and trial copy with StoreKit, add Restore Purchases to Settings, use bundled paywall config in production, and show plan-appropriate payment notes.

Co-authored-by: Cursor <cursoragent@cursor.com>
AhtashamShahzad1 3 weeks ago
parent
commit
e5715781fc

+ 4 - 0
smart_printer/PaywallConfig.swift

@@ -104,6 +104,8 @@ struct PaywallConfig: Codable, Sendable {
         let purchaseFailedTitle: String
         let restoreNotFoundTitle: String
         let restoreNotFoundMessage: String
+        let restoreSuccessTitle: String
+        let restoreSuccessMessage: String
         let pendingPurchaseTitle: String
         let pendingPurchaseMessage: String
         let alertOK: String
@@ -148,6 +150,7 @@ struct PaywallConfig: Codable, Sendable {
     let lifetimeDisclosure: String
     let loadingDisclosure: String
     let securePaymentNote: String
+    let lifetimeSecurePaymentNote: String
     let closeButtonHelp: String
 }
 
@@ -175,6 +178,7 @@ extension PaywallConfig {
             lifetimeDisclosure: lifetimeDisclosure,
             loadingDisclosure: loadingDisclosure,
             securePaymentNote: securePaymentNote,
+            lifetimeSecurePaymentNote: lifetimeSecurePaymentNote,
             closeButtonHelp: closeButtonHelp
         )
     }

+ 2 - 0
smart_printer/PaywallConfigService.swift

@@ -18,7 +18,9 @@ final class PaywallConfigService {
     func start() {
         guard !hasStarted else { return }
         hasStarted = true
+        #if DEBUG
         Task { await refreshFromRemote() }
+        #endif
     }
 
     func refreshFromRemote() async {

+ 13 - 2
smart_printer/PaywallView.swift

@@ -1640,7 +1640,7 @@ final class PaywallView: NSView, AppearanceRefreshable {
         privacyLink.updateTitle(config.footer.privacyPolicy)
         termsLink.updateTitle(config.footer.termsOfService)
         supportLink.updateTitle(config.footer.support)
-        securePaymentLabel.stringValue = config.securePaymentNote
+        refreshSecurePaymentNote()
     }
 
     private func syncFeatureRows(with features: [String]) {
@@ -1699,9 +1699,17 @@ final class PaywallView: NSView, AppearanceRefreshable {
         refreshTrustItemsForSelectedPlan()
         refreshProductsErrorState()
         refreshPurchaseState()
+        refreshSecurePaymentNote()
         selectTrialEligiblePlanIfAvailable()
     }
 
+    private func refreshSecurePaymentNote() {
+        let config = paywallConfig
+        securePaymentLabel.stringValue = selectedPlan == .lifetime
+            ? config.lifetimeSecurePaymentNote
+            : config.securePaymentNote
+    }
+
     private func refreshProductsErrorState() {
         let store = StoreManager.shared
         let hasLoadFailure = store.productLoadError != nil && !store.hasAllProductsLoaded
@@ -1964,8 +1972,8 @@ final class PaywallView: NSView, AppearanceRefreshable {
         securePaymentLabel.font = AppTheme.regularFont(size: 10)
         securePaymentLabel.textColor = AppTheme.textSecondary
         securePaymentLabel.alignment = .center
-        securePaymentLabel.stringValue = config.securePaymentNote
         securePaymentLabel.translatesAutoresizingMaskIntoConstraints = false
+        refreshSecurePaymentNote()
 
         let trustRow = makeTrustRow()
         let footerLinks = makeFooterLinks()
@@ -2203,6 +2211,7 @@ final class PaywallView: NSView, AppearanceRefreshable {
         )
         refreshTrustItemsForSelectedPlan()
         refreshPurchaseState()
+        refreshSecurePaymentNote()
     }
 
     private func retryProductLoad() {
@@ -2444,7 +2453,9 @@ final class PaywallOverlayView: NSView, AppearanceRefreshable {
         }
 
         Task { @MainActor in
+            #if DEBUG
             await PaywallConfigService.shared.refreshFromRemote()
+            #endif
         }
     }
 

+ 29 - 1
smart_printer/SettingsView.swift

@@ -123,12 +123,19 @@ final class SettingsView: NSView, AppearanceRefreshable {
             symbolName: "moon.fill",
             title: "Dark Mode",
             isOn: AppSettings.darkModeEnabled,
-            isLast: !showsFreeTierReset
+            isLast: {
+                #if DEBUG
+                !showsFreeTierReset
+                #else
+                true
+                #endif
+            }()
         ) { enabled in
             AppSettings.darkModeEnabled = enabled
             AppSettings.applyAppearance()
         })
 
+        #if DEBUG
         if showsFreeTierReset {
             card.addRow(SettingsActionRow(
                 symbolName: "arrow.counterclockwise",
@@ -138,13 +145,16 @@ final class SettingsView: NSView, AppearanceRefreshable {
                 FreeTierManager.reset()
             })
         }
+        #endif
 
         return card
     }
 
+    #if DEBUG
     private var showsFreeTierReset: Bool {
         !StoreManager.shared.isPremium
     }
+    #endif
 
     private func makeAboutCard() -> SettingsGroupCard {
         let card = SettingsGroupCard()
@@ -165,6 +175,24 @@ final class SettingsView: NSView, AppearanceRefreshable {
             })
         }
 
+        let config = PaywallConfigService.shared.config
+        rows.append(SettingsActionRow(symbolName: "arrow.clockwise", title: config.footer.restorePurchase) {
+            Task { @MainActor in
+                let store = StoreManager.shared
+                store.clearPurchaseError()
+                let restored = await store.restorePurchases()
+                if restored {
+                    store.showAlert(
+                        title: config.messages.restoreSuccessTitle,
+                        message: config.messages.restoreSuccessMessage,
+                        on: NSApp.keyWindow
+                    )
+                } else if let error = store.purchaseError {
+                    store.showPurchaseError(error, on: NSApp.keyWindow)
+                }
+            }
+        })
+
         rows.append(SettingsActionRow(symbolName: "link", title: "Website") {
             NSWorkspace.shared.open(URL(string: "https://sites.google.com/view/smartprinterappmacos/home")!)
         })

+ 6 - 3
smart_printer/paywall.json

@@ -70,7 +70,7 @@
       "badge": "Best Value",
       "subtitleTemplate": "{price} once, lifetime access",
       "ctaTemplate": "Buy Lifetime Access for {price}",
-      "fallbackPrice": "$49.99",
+      "fallbackPrice": "$99.99",
       "priceSuffix": null,
       "fallbackBillingDescription": "One-time payment of {price}",
       "features": [
@@ -99,8 +99,8 @@
     "badgeTemplate": "{duration} Free Trial",
     "ctaTemplate": "Start {duration} Free Trial",
     "fallbackDuration": {
-      "count": 3,
-      "unit": "day"
+      "count": 1,
+      "unit": "week"
     }
   },
   "duration": {
@@ -121,6 +121,8 @@
     "purchaseFailedTitle": "Purchase Failed",
     "restoreNotFoundTitle": "No Purchases Found",
     "restoreNotFoundMessage": "We couldn't find any previous purchases for this Apple ID.",
+    "restoreSuccessTitle": "Purchases Restored",
+    "restoreSuccessMessage": "Your premium access has been restored.",
     "pendingPurchaseTitle": "Purchase Pending",
     "pendingPurchaseMessage": "Your purchase is waiting for approval. You'll get access once it's approved.",
     "alertOK": "OK",
@@ -150,5 +152,6 @@
   "lifetimeDisclosure": "One-time purchase. No subscription or recurring charges.",
   "loadingDisclosure": "Payment will be charged to your Apple ID. Subscription automatically renews unless canceled at least 24 hours before the end of the current period.",
   "securePaymentNote": "Secure payment. Cancel anytime.",
+  "lifetimeSecurePaymentNote": "Secure payment. One-time purchase.",
   "closeButtonHelp": "Close"
 }