Просмотр исходного кода

In-app browser: premium gates and borderless paywall

Gate gesture mode, add-to-desktop, and add-to-status-bar behind Premium;
non-premium users see locked titlebar styling and the Premium paywall on tap.
Present paywall in a non-modal borderless child window so the browser stays
usable, with rounded corners to match sheet-based PremiumFeaturesView elsewhere.
PremiumFeaturesView gains AppKit dismiss support for the in-app window host.

Made-with: Cursor
huzaifahayat12 3 месяцев назад
Родитель
Сommit
67552e4320
2 измененных файлов с 134 добавлено и 12 удалено
  1. 121 10
      google_apps/InAppBrowserWindow.swift
  2. 13 2
      google_apps/PremiumFeaturesView.swift

+ 121 - 10
google_apps/InAppBrowserWindow.swift

@@ -1,4 +1,5 @@
 import AppKit
+import SwiftUI
 import WebKit
 
 /// Titlebar icon control: white/neutral tint (no accent blue), subtle hover and selected backgrounds.
@@ -6,6 +7,10 @@ private final class TitlebarIconButton: NSButton {
     private var isHovering = false
     private var hoverTrackingArea: NSTrackingArea?
     var isToggleActive = false
+    /// Looks disabled (gray) but keeps `isEnabled == true` so the control still receives clicks (e.g. paywall).
+    var lockedForPaywallAppearance = false {
+        didSet { refreshVisuals() }
+    }
 
     override func awakeFromNib() {
         super.awakeFromNib()
@@ -69,7 +74,7 @@ private final class TitlebarIconButton: NSButton {
 
     func refreshVisuals() {
         let white = NSColor.white
-        if !isEnabled {
+        if !isEnabled || lockedForPaywallAppearance {
             contentTintColor = white.withAlphaComponent(0.35)
             layer?.backgroundColor = NSColor.clear.cgColor
             return
@@ -159,6 +164,7 @@ final class InAppBrowserWindowController: NSWindowController, NSWindowDelegate {
     private var isGestureModeEnabled = true
 
     private var statusBarShortcutObserver: NSObjectProtocol?
+    private var premiumSheetWindow: NSWindow?
 
     private func setToggledStyle(_ button: TitlebarIconButton, isOn: Bool) {
         button.setToggleActive(isOn)
@@ -490,7 +496,15 @@ final class InAppBrowserWindowController: NSWindowController, NSWindowDelegate {
     }
 
     func windowWillClose(_ notification: Notification) {
-        onClose?()
+        guard let w = notification.object as? NSWindow else { return }
+        if w === premiumSheetWindow {
+            premiumSheetWindow = nil
+            updateToolbarState()
+            return
+        }
+        if w === window {
+            onClose?()
+        }
     }
 
     @objc private func goBack() {
@@ -521,10 +535,7 @@ final class InAppBrowserWindowController: NSWindowController, NSWindowDelegate {
 
     @objc private func toggleCurrentPageStatusBar() {
         guard PremiumStore.shared.isPremiumUnlocked else {
-            showAlert(
-                title: "Premium required",
-                message: "Update to Premium to add shortcuts to the menu bar."
-            )
+            presentPremiumPaywall()
             return
         }
         guard let app = resolvedLauncherAppForCurrentPage() else {
@@ -555,10 +566,7 @@ final class InAppBrowserWindowController: NSWindowController, NSWindowDelegate {
 
     @objc private func addCurrentPageToDesktop() {
         guard PremiumStore.shared.isPremiumUnlocked else {
-            showAlert(
-                title: "Premium required",
-                message: "Update to Premium to save desktop shortcuts."
-            )
+            presentPremiumPaywall()
             return
         }
         let url = currentPageURL()
@@ -606,6 +614,17 @@ final class InAppBrowserWindowController: NSWindowController, NSWindowDelegate {
 
     private func updateAddToStatusBarButtonAppearance() {
         let cfg = NSImage.SymbolConfiguration(pointSize: 12, weight: .semibold)
+        guard PremiumStore.shared.isPremiumUnlocked else {
+            addToStatusBarButton.isEnabled = true
+            addToStatusBarButton.toolTip = "Add to Status Bar — Premium"
+            let img = NSImage(systemSymbolName: "arrow.up.square", accessibilityDescription: "Add to Status Bar")?
+                .withSymbolConfiguration(cfg)
+            addToStatusBarButton.image = img
+            addToStatusBarButton.image?.isTemplate = true
+            setToggledStyle(addToStatusBarButton, isOn: false)
+            addToStatusBarButton.refreshVisuals()
+            return
+        }
         guard let app = resolvedLauncherAppForCurrentPage() else {
             addToStatusBarButton.isEnabled = false
             addToStatusBarButton.toolTip = "Add to Status Bar — open this site from the launcher first"
@@ -652,6 +671,10 @@ final class InAppBrowserWindowController: NSWindowController, NSWindowDelegate {
     }
 
     @objc private func toggleGestureMode() {
+        guard PremiumStore.shared.isPremiumUnlocked else {
+            presentPremiumPaywall()
+            return
+        }
         isGestureModeEnabled.toggle()
         webView.allowsBackForwardNavigationGestures = isGestureModeEnabled
         if let button = handButton {
@@ -664,6 +687,93 @@ final class InAppBrowserWindowController: NSWindowController, NSWindowDelegate {
         }
     }
 
+    /// Borderless `NSWindow` is square; SwiftUI `.sheet` gets rounded chrome automatically — match that look.
+    private func configurePaywallWindowRoundedCorners(_ win: NSWindow) {
+        guard let cv = win.contentView else { return }
+        cv.wantsLayer = true
+        let r: CGFloat = 16
+        cv.layer?.cornerRadius = r
+        if #available(macOS 11.0, *) {
+            cv.layer?.cornerCurve = .continuous
+        }
+        cv.layer?.masksToBounds = true
+    }
+
+    private func presentPremiumPaywall() {
+        guard let parentWindow = window else { return }
+        if let existing = premiumSheetWindow, existing.isVisible {
+            existing.makeKeyAndOrderFront(nil)
+            NSApp.activate(ignoringOtherApps: true)
+            return
+        }
+
+        let paywallSize = NSSize(width: 540, height: 720)
+        let paywallWindow = NSWindow(
+            contentRect: NSRect(origin: .zero, size: paywallSize),
+            styleMask: [.borderless, .resizable],
+            backing: .buffered,
+            defer: false
+        )
+        paywallWindow.isReleasedWhenClosed = false
+        paywallWindow.delegate = self
+        paywallWindow.isMovableByWindowBackground = true
+        paywallWindow.hasShadow = true
+        paywallWindow.isOpaque = false
+        paywallWindow.backgroundColor = .clear
+
+        let hosting = NSHostingController(
+            rootView: PremiumFeaturesView(onDismissFromAppKitHost: { [weak self] in
+                self?.closePremiumPaywallWindow()
+            })
+            .environmentObject(PremiumStore.shared)
+        )
+        hosting.view.frame = NSRect(origin: .zero, size: paywallSize)
+        paywallWindow.contentViewController = hosting
+        configurePaywallWindowRoundedCorners(paywallWindow)
+
+        let pf = parentWindow.frame
+        paywallWindow.setFrame(
+            NSRect(
+                x: pf.midX - paywallSize.width / 2,
+                y: pf.midY - paywallSize.height / 2,
+                width: paywallSize.width,
+                height: paywallSize.height
+            ),
+            display: false
+        )
+
+        premiumSheetWindow = paywallWindow
+        parentWindow.addChildWindow(paywallWindow, ordered: .above)
+        paywallWindow.makeKeyAndOrderFront(nil)
+        NSApp.activate(ignoringOtherApps: true)
+    }
+
+    private func closePremiumPaywallWindow() {
+        guard let w = premiumSheetWindow else { return }
+        w.parent?.removeChildWindow(w)
+        w.close()
+        // `premiumSheetWindow` and toolbar refresh happen in `windowWillClose`.
+    }
+
+    /// Non‑Premium: only gesture, status bar, and desktop look locked; they stay enabled so taps open the paywall.
+    private func applyPremiumLockedTitlebarAppearance() {
+        let locked = !PremiumStore.shared.isPremiumUnlocked
+        handButton.lockedForPaywallAppearance = locked
+        addToDesktopButton.lockedForPaywallAppearance = locked
+        addToStatusBarButton.lockedForPaywallAppearance = locked
+        handButton.alphaValue = 1
+        addToDesktopButton.alphaValue = 1
+        addToStatusBarButton.alphaValue = 1
+        if locked {
+            handButton.isEnabled = true
+            addToDesktopButton.isEnabled = true
+            addToStatusBarButton.isEnabled = true
+        }
+        handButton.refreshVisuals()
+        addToDesktopButton.refreshVisuals()
+        addToStatusBarButton.refreshVisuals()
+    }
+
     @objc private func openDownloads() {
         guard let downloads = FileManager.default.urls(for: .downloadsDirectory, in: .userDomainMask).first else {
             showAlert(title: "Unavailable", message: "Couldn’t find your Downloads folder.")
@@ -773,6 +883,7 @@ final class InAppBrowserWindowController: NSWindowController, NSWindowDelegate {
         // Sync center site name.
         updateSiteNameFromWebView()
         updateAddToStatusBarButtonAppearance()
+        applyPremiumLockedTitlebarAppearance()
     }
 
     private func showAlert(title: String, message: String) {

+ 13 - 2
google_apps/PremiumFeaturesView.swift

@@ -2,6 +2,9 @@ import StoreKit
 import SwiftUI
 
 struct PremiumFeaturesView: View {
+    /// When presented from AppKit (`NSWindow.beginSheet`), use this to dismiss; SwiftUI `dismiss` may not close the sheet.
+    var onDismissFromAppKitHost: (() -> Void)? = nil
+
     enum PlanOption: String {
         case perpetual
         case yearly
@@ -74,7 +77,7 @@ struct PremiumFeaturesView: View {
             ScrollView {
                 VStack(alignment: .leading, spacing: 14) {
                     HStack {
-                        Button(action: { dismiss() }) {
+                        Button(action: { dismissPremiumUI() }) {
                             Image(systemName: "xmark")
                                 .font(.system(size: 13, weight: .semibold))
                                 .foregroundStyle(.white.opacity(0.88))
@@ -237,7 +240,7 @@ struct PremiumFeaturesView: View {
             await premiumStore.loadProducts()
         }
         .alert("Payment Successful", isPresented: $showingSuccessAlert) {
-            Button("Done") { dismiss() }
+            Button("Done") { dismissPremiumUI() }
         } message: {
             Text("Your \(selectedPlan == .perpetual ? "Perpetual" : "Yearly") plan has been activated.")
         }
@@ -251,6 +254,14 @@ struct PremiumFeaturesView: View {
         selectedProduct != nil && !premiumStore.purchaseInProgress && !premiumStore.isLoadingProducts
     }
 
+    private func dismissPremiumUI() {
+        if let onDismissFromAppKitHost {
+            onDismissFromAppKitHost()
+        } else {
+            dismiss()
+        }
+    }
+
     private func featureCard(title: String, subtitle: String, color: Color) -> some View {
         HStack(alignment: .top, spacing: 10) {
             Circle()