Переглянути джерело

Redesign Premium paywall for desktop layouts and parent-sized windows.

PremiumFeaturesView now uses a two-column layout on wider surfaces (features plus sidebar plans) with a compact single-column fallback. Paywall host windows derive their size from the parent content area with sane min/size caps, resize with the hosting view, and set a sensible minimum frame.

Co-authored-by: Cursor <cursoragent@cursor.com>
huzaifahayat12 2 місяців тому
батько
коміт
7b69552d2d

+ 3 - 1
google_apps/AppDelegate.swift

@@ -64,7 +64,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSWindowDelegate {
             return
         }
 
-        let paywallSize = NSSize(width: 540, height: 720)
+        let paywallSize = PremiumPaywallWindowSizing.contentSize(forParentWindow: parent)
         let paywallWindow = NSWindow(
             contentRect: NSRect(origin: .zero, size: paywallSize),
             styleMask: [.borderless, .resizable],
@@ -78,6 +78,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSWindowDelegate {
         paywallWindow.isOpaque = false
         paywallWindow.backgroundColor = .clear
         paywallWindow.level = .floating
+        paywallWindow.minSize = NSSize(width: 560, height: 480)
 
         let hosting = NSHostingController(
             rootView: PremiumFeaturesView(onDismissFromAppKitHost: { [weak self] in
@@ -86,6 +87,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSWindowDelegate {
             .environmentObject(PremiumStore.shared)
         )
         hosting.view.frame = NSRect(origin: .zero, size: paywallSize)
+        hosting.view.autoresizingMask = [.width, .height]
         paywallWindow.contentViewController = hosting
 
         if let layer = paywallWindow.contentView?.layer {

+ 3 - 1
google_apps/InAppBrowserWindow.swift

@@ -730,7 +730,7 @@ final class InAppBrowserWindowController: NSWindowController, NSWindowDelegate {
             return
         }
 
-        let paywallSize = NSSize(width: 540, height: 720)
+        let paywallSize = PremiumPaywallWindowSizing.contentSize(forParentWindow: parentWindow)
         let paywallWindow = NSWindow(
             contentRect: NSRect(origin: .zero, size: paywallSize),
             styleMask: [.borderless, .resizable],
@@ -743,6 +743,7 @@ final class InAppBrowserWindowController: NSWindowController, NSWindowDelegate {
         paywallWindow.hasShadow = true
         paywallWindow.isOpaque = false
         paywallWindow.backgroundColor = .clear
+        paywallWindow.minSize = NSSize(width: 560, height: 480)
 
         let hosting = NSHostingController(
             rootView: PremiumFeaturesView(onDismissFromAppKitHost: { [weak self] in
@@ -751,6 +752,7 @@ final class InAppBrowserWindowController: NSWindowController, NSWindowDelegate {
             .environmentObject(PremiumStore.shared)
         )
         hosting.view.frame = NSRect(origin: .zero, size: paywallSize)
+        hosting.view.autoresizingMask = [.width, .height]
         paywallWindow.contentViewController = hosting
         configurePaywallWindowRoundedCorners(paywallWindow)
 

+ 342 - 218
google_apps/PremiumFeaturesView.swift

@@ -71,203 +71,310 @@ struct PremiumFeaturesView: View {
             )
             .ignoresSafeArea()
 
-            ScrollView {
-                VStack(alignment: .leading, spacing: 14) {
-                    HStack {
-                        Button(action: { dismissPremiumUI() }) {
-                            Image(systemName: "xmark")
-                                .font(.system(size: 13, weight: .semibold))
-                                .foregroundStyle(primaryTextColor)
-                        }
-                        .buttonStyle(.plain)
-
-                        Spacer()
-
-                        Button {
-                            Task { await premiumStore.restorePurchases() }
-                        } label: {
-                            HStack(spacing: 6) {
-                                if premiumStore.restoreInProgress {
-                                    ProgressView()
-                                        .controlSize(.mini)
-                                }
-                                Text(premiumStore.restoreInProgress ? "Restoring..." : "Restore")
-                                    .font(.system(size: 11, weight: .semibold))
-                            }
-                            .foregroundStyle(primaryTextColor)
-                            .padding(.horizontal, 8)
-                            .padding(.vertical, 3)
-                            .background(
-                                RoundedRectangle(cornerRadius: 7, style: .continuous)
-                                    .fill(chipFillColor)
-                            )
-                        }
-                        .buttonStyle(.plain)
-                        .disabled(premiumStore.purchaseInProgress || premiumStore.restoreInProgress)
-                    }
-
-                    Text("Upgrade to unlock all Premium Features")
-                        .font(.system(size: 20, weight: .bold))
-                        .foregroundStyle(primaryTextColor)
-                        .lineSpacing(1)
+            GeometryReader { geo in
+                let useWideLayout = geo.size.width >= 620
+                VStack(spacing: 0) {
+                    paywallTopBar
+                        .padding(.horizontal, useWideLayout ? 22 : 16)
+                        .padding(.vertical, 12)
 
-                    if let err = premiumStore.loadError {
-                        Text(err)
-                            .font(.system(size: 11, weight: .medium))
-                            .foregroundStyle(.orange.opacity(0.95))
+                    if useWideLayout {
+                        desktopSplitContent
+                            .frame(maxWidth: .infinity, maxHeight: .infinity)
+                    } else {
+                        compactScrollContent
+                            .frame(maxWidth: .infinity, maxHeight: .infinity)
                     }
+                }
+            }
+        }
+        .task {
+            await premiumStore.loadProducts()
+        }
+        .alert("Payment Successful", isPresented: $showingSuccessAlert) {
+            Button("Done") { dismissPremiumUI() }
+        } message: {
+            Text("Your \(selectedPlan == .perpetual ? "Lifetime" : "Yearly") plan has been activated.")
+        }
+    }
 
-                    if let err = premiumStore.purchaseError {
-                        Text(err)
-                            .font(.system(size: 11, weight: .medium))
-                            .foregroundStyle(.orange.opacity(0.95))
-                    }
+    private var paywallTopBar: some View {
+        HStack {
+            Button(action: { dismissPremiumUI() }) {
+                Image(systemName: "xmark")
+                    .font(.system(size: 13, weight: .semibold))
+                    .foregroundStyle(primaryTextColor)
+            }
+            .buttonStyle(.plain)
 
-                    if let restoreMessage = premiumStore.restoreMessage {
-                        Text(restoreMessage)
-                            .font(.system(size: 11, weight: .medium))
-                            .foregroundStyle(.green.opacity(0.95))
-                    }
+            Spacer()
 
-                    LazyVGrid(
-                        columns: [GridItem(.flexible(), spacing: 10), GridItem(.flexible(), spacing: 10)],
-                        spacing: 10
-                    ) {
-                        ForEach(Array(featureRows.enumerated()), id: \.offset) { index, feature in
-                            featureCard(title: feature.0, subtitle: feature.1, color: cardColors[index % cardColors.count])
-                        }
+            Button {
+                Task { await premiumStore.restorePurchases() }
+            } label: {
+                HStack(spacing: 6) {
+                    if premiumStore.restoreInProgress {
+                        ProgressView()
+                            .controlSize(.mini)
                     }
+                    Text(premiumStore.restoreInProgress ? "Restoring…" : "Restore")
+                        .font(.system(size: 12, weight: .semibold))
+                }
+                .foregroundStyle(primaryTextColor)
+                .padding(.horizontal, 11)
+                .padding(.vertical, 5)
+                .background(
+                    RoundedRectangle(cornerRadius: 8, style: .continuous)
+                        .fill(chipFillColor)
+                )
+            }
+            .buttonStyle(.plain)
+            .disabled(premiumStore.purchaseInProgress || premiumStore.restoreInProgress)
+        }
+    }
 
+    private var desktopSplitContent: some View {
+        HStack(alignment: .top, spacing: 0) {
+            ScrollView {
+                VStack(alignment: .leading, spacing: 18) {
+                    headlineBlock(useLargeType: true)
+                    messagesBlock
+                    featureGrid(desktop: true)
                     if productsUnavailable {
-                        VStack(alignment: .leading, spacing: 10) {
-                            Text("Store products not loaded")
-                                .font(.system(size: 14, weight: .bold))
-                                .foregroundStyle(primaryTextColor)
-                            Text(
-                                "Prices and checkout need product metadata from App Store Connect, or a StoreKit Configuration file when running from Xcode. Product IDs must match exactly."
-                            )
-                            .font(.system(size: 11, weight: .medium))
-                            .foregroundStyle(secondaryTextColor)
-                            .fixedSize(horizontal: false, vertical: true)
-                            Button {
-                                Task { await premiumStore.loadProducts() }
-                            } label: {
-                                Text("Retry")
-                                    .font(.system(size: 13, weight: .semibold))
-                                    .foregroundStyle(.white)
-                                    .frame(maxWidth: .infinity)
-                                    .padding(.vertical, 10)
-                                    .background(RoundedRectangle(cornerRadius: 10, style: .continuous).fill(Color.orange.opacity(0.85)))
-                            }
-                            .buttonStyle(.plain)
-                        }
-                        .padding(14)
-                        .frame(maxWidth: .infinity, alignment: .leading)
-                        .background(
-                            RoundedRectangle(cornerRadius: 14, style: .continuous)
-                                .fill(Color.orange.opacity(0.18))
-                        )
-                        .overlay(
-                            RoundedRectangle(cornerRadius: 14, style: .continuous)
-                                .stroke(Color.orange.opacity(0.45), lineWidth: 1)
-                        )
+                        productsUnavailableBanner
                     }
+                }
+                .padding(.leading, 22)
+                .padding(.trailing, 14)
+                .padding(.bottom, 22)
+            }
+            .frame(maxWidth: .infinity, maxHeight: .infinity)
 
-                    priceCard(
-                        title: "Lifetime",
-                        price: priceLine(for: .perpetual),
-                        subtitle: "Own the software for lifetime",
-                        badgeText: "One-time payment",
-                        isSelected: selectedPlan == .perpetual
-                    )
-                    .onTapGesture {
-                        selectedPlan = .perpetual
-                    }
+            Rectangle()
+                .fill(paywallDividerColor)
+                .frame(width: 1)
+                .padding(.vertical, 12)
 
-                    priceCard(
-                        title: "Yearly",
-                        price: priceLine(for: .yearly),
-                        subtitle: "Billed annually",
-                        badgeText: nil,
-                        isSelected: selectedPlan == .yearly
-                    )
-                    .onTapGesture {
-                        selectedPlan = .yearly
-                    }
+            ZStack(alignment: .topLeading) {
+                Rectangle()
+                    .fill(colorScheme == .dark ? Color.white.opacity(0.06) : Color.black.opacity(0.035))
 
-                    Button(action: {
-                        guard let product = selectedProduct else { return }
-                        Task {
-                            let ok = await premiumStore.purchase(product)
-                            if ok {
-                                showingSuccessAlert = true
-                            }
-                        }
-                    }) {
-                        HStack {
-                            if premiumStore.purchaseInProgress {
-                                ProgressView()
-                                    .controlSize(.small)
-                                    .tint(.white)
-                            }
-                            Text("Continue")
-                                .font(.system(size: 16, weight: .bold))
-                        }
-                        .foregroundStyle(.white)
-                        .frame(maxWidth: .infinity)
-                        .padding(.vertical, 12)
-                        .background(
-                            RoundedRectangle(cornerRadius: 16, style: .continuous)
-                                .fill(continueEnabled ? Color.blue.opacity(0.9) : Color.gray.opacity(0.55))
-                        )
-                    }
-                    .buttonStyle(.plain)
-                    .disabled(!continueEnabled)
-                    .padding(.top, 6)
-
-                    HStack {
-                        Spacer()
-                        Button("Privacy Policy") {
-                            openExternalPage(ExternalPage.privacy)
-                        }
-                        .buttonStyle(.plain)
-                        .font(.system(size: 11, weight: .medium))
-                        .foregroundStyle(secondaryTextColor)
-                        
-                        Spacer()
-                        
-                        Button("Support") {
-                            openExternalPage(ExternalPage.support)
-                        }
-                        .buttonStyle(.plain)
-                        .font(.system(size: 11, weight: .medium))
-                        .foregroundStyle(secondaryTextColor)
-                        
-                        Spacer()
-                        
-                        Button("Terms of Service") {
-                            openExternalPage(ExternalPage.terms)
-                        }
-                        .buttonStyle(.plain)
-                        .font(.system(size: 11, weight: .medium))
-                        .foregroundStyle(secondaryTextColor)
-                        Spacer()
-                    }
-                    .padding(.top, 2)
+                VStack(alignment: .leading, spacing: 18) {
+                    Text("Choose a plan")
+                        .font(.system(size: 15, weight: .semibold))
+                        .foregroundStyle(primaryTextColor)
+
+                    planCardsStack(compactPricing: false)
+
+                    continuePurchaseButton
+
+                    sidebarFooterLinks
                 }
-                .padding(16)
+                .padding(.leading, 18)
+                .padding(.trailing, 22)
+                .padding(.bottom, 22)
             }
+            .frame(width: 288)
+            .frame(maxHeight: .infinity, alignment: .top)
         }
-        .task {
-            await premiumStore.loadProducts()
+        .frame(maxHeight: .infinity)
+    }
+
+    private var compactScrollContent: some View {
+        ScrollView {
+            VStack(alignment: .leading, spacing: 16) {
+                headlineBlock(useLargeType: false)
+                messagesBlock
+                featureGrid(desktop: false)
+                if productsUnavailable {
+                    productsUnavailableBanner
+                }
+                planCardsStack(compactPricing: true)
+                continuePurchaseButton
+                compactFooterLinks
+            }
+            .padding(.horizontal, 16)
+            .padding(.bottom, 20)
         }
-        .alert("Payment Successful", isPresented: $showingSuccessAlert) {
-            Button("Done") { dismissPremiumUI() }
-        } message: {
-            Text("Your \(selectedPlan == .perpetual ? "Lifetime" : "Yearly") plan has been activated.")
+    }
+
+    private func headlineBlock(useLargeType: Bool) -> some View {
+        VStack(alignment: .leading, spacing: 6) {
+            Text("Premium")
+                .font(.system(size: useLargeType ? 28 : 22, weight: .bold))
+                .foregroundStyle(primaryTextColor)
+            Text("Unlock every feature across the launcher, widgets, and in‑app browser.")
+                .font(.system(size: useLargeType ? 14 : 13, weight: .medium))
+                .foregroundStyle(secondaryTextColor)
+                .fixedSize(horizontal: false, vertical: true)
+        }
+    }
+
+    private var messagesBlock: some View {
+        VStack(alignment: .leading, spacing: 6) {
+            if let err = premiumStore.loadError {
+                Text(err)
+                    .font(.system(size: 12, weight: .medium))
+                    .foregroundStyle(.orange.opacity(0.95))
+            }
+            if let err = premiumStore.purchaseError {
+                Text(err)
+                    .font(.system(size: 12, weight: .medium))
+                    .foregroundStyle(.orange.opacity(0.95))
+            }
+            if let restoreMessage = premiumStore.restoreMessage {
+                Text(restoreMessage)
+                    .font(.system(size: 12, weight: .medium))
+                    .foregroundStyle(.green.opacity(0.95))
+            }
+        }
+    }
+
+    private var productsUnavailableBanner: some View {
+        VStack(alignment: .leading, spacing: 10) {
+            Text("Store products not loaded")
+                .font(.system(size: 14, weight: .bold))
+                .foregroundStyle(primaryTextColor)
+            Text(
+                "Prices and checkout need product metadata from App Store Connect, or a StoreKit Configuration file when running from Xcode. Product IDs must match exactly."
+            )
+            .font(.system(size: 12, weight: .medium))
+            .foregroundStyle(secondaryTextColor)
+            .fixedSize(horizontal: false, vertical: true)
+            Button {
+                Task { await premiumStore.loadProducts() }
+            } label: {
+                Text("Retry")
+                    .font(.system(size: 13, weight: .semibold))
+                    .foregroundStyle(.white)
+                    .frame(maxWidth: .infinity)
+                    .padding(.vertical, 10)
+                    .background(RoundedRectangle(cornerRadius: 10, style: .continuous).fill(Color.orange.opacity(0.85)))
+            }
+            .buttonStyle(.plain)
+        }
+        .padding(14)
+        .frame(maxWidth: .infinity, alignment: .leading)
+        .background(
+            RoundedRectangle(cornerRadius: 14, style: .continuous)
+                .fill(Color.orange.opacity(0.18))
+        )
+        .overlay(
+            RoundedRectangle(cornerRadius: 14, style: .continuous)
+                .stroke(Color.orange.opacity(0.45), lineWidth: 1)
+        )
+    }
+
+    private func featureGrid(desktop: Bool) -> some View {
+        let spacing: CGFloat = desktop ? 14 : 10
+        let columns = [
+            GridItem(.flexible(), spacing: spacing),
+            GridItem(.flexible(), spacing: spacing),
+        ]
+        return LazyVGrid(columns: columns, spacing: desktop ? 12 : 10) {
+            ForEach(Array(featureRows.enumerated()), id: \.offset) { index, feature in
+                featureCard(
+                    title: feature.0,
+                    subtitle: feature.1,
+                    color: cardColors[index % cardColors.count],
+                    desktop: desktop
+                )
+            }
         }
     }
 
+    private func planCardsStack(compactPricing: Bool) -> some View {
+        VStack(spacing: 10) {
+            priceCard(
+                title: "Lifetime",
+                price: priceLine(for: .perpetual),
+                subtitle: "Pay once — no subscription.",
+                badgeText: "One-time payment",
+                isSelected: selectedPlan == .perpetual,
+                compact: compactPricing
+            )
+            .onTapGesture { selectedPlan = .perpetual }
+
+            priceCard(
+                title: "Yearly",
+                price: priceLine(for: .yearly),
+                subtitle: "Billed annually. Cancel anytime.",
+                badgeText: nil,
+                isSelected: selectedPlan == .yearly,
+                compact: compactPricing
+            )
+            .onTapGesture { selectedPlan = .yearly }
+        }
+    }
+
+    private var continuePurchaseButton: some View {
+        Button(action: {
+            guard let product = selectedProduct else { return }
+            Task {
+                let ok = await premiumStore.purchase(product)
+                if ok {
+                    showingSuccessAlert = true
+                }
+            }
+        }) {
+            HStack {
+                if premiumStore.purchaseInProgress {
+                    ProgressView()
+                        .controlSize(.small)
+                        .tint(.white)
+                }
+                Text("Continue")
+                    .font(.system(size: 15, weight: .bold))
+            }
+            .foregroundStyle(.white)
+            .frame(maxWidth: .infinity)
+            .padding(.vertical, 12)
+            .background(
+                RoundedRectangle(cornerRadius: 12, style: .continuous)
+                    .fill(continueEnabled ? Color.blue.opacity(0.9) : Color.gray.opacity(0.55))
+            )
+        }
+        .buttonStyle(.plain)
+        .disabled(!continueEnabled)
+        .padding(.top, 2)
+    }
+
+    private var sidebarFooterLinks: some View {
+        VStack(alignment: .leading, spacing: 8) {
+            linkButton("Privacy Policy", ExternalPage.privacy)
+            linkButton("Support", ExternalPage.support)
+            linkButton("Terms of Service", ExternalPage.terms)
+        }
+        .frame(maxWidth: .infinity, alignment: .leading)
+        .padding(.top, 4)
+    }
+
+    private var compactFooterLinks: some View {
+        HStack {
+            Spacer()
+            linkButton("Privacy Policy", ExternalPage.privacy)
+            Spacer()
+            linkButton("Support", ExternalPage.support)
+            Spacer()
+            linkButton("Terms", ExternalPage.terms)
+            Spacer()
+        }
+        .padding(.top, 4)
+    }
+
+    private func linkButton(_ title: String, _ url: String) -> some View {
+        Button(title) {
+            openExternalPage(url)
+        }
+        .buttonStyle(.plain)
+        .font(.system(size: 11.5, weight: .medium))
+        .foregroundStyle(secondaryTextColor)
+    }
+
+    private var paywallDividerColor: Color {
+        colorScheme == .dark ? Color.white.opacity(0.12) : Color.black.opacity(0.08)
+    }
+
     private var productsUnavailable: Bool {
         premiumStore.products.isEmpty && !premiumStore.isLoadingProducts
     }
@@ -308,89 +415,106 @@ struct PremiumFeaturesView: View {
         colorScheme == .dark ? .white.opacity(0.16) : .black.opacity(0.08)
     }
 
-    private func featureCard(title: String, subtitle: String, color: Color) -> some View {
-        HStack(alignment: .top, spacing: 10) {
-            Circle()
-                .fill(color.opacity(0.95))
-                .frame(width: 28, height: 28)
-                .overlay(
-                    Image(systemName: "sparkles")
-                        .font(.system(size: 11, weight: .semibold))
-                        .foregroundStyle(.white)
-                )
+    private func featureCard(title: String, subtitle: String, color: Color, desktop: Bool) -> some View {
+        HStack(alignment: .top, spacing: desktop ? 12 : 10) {
+            Image(systemName: "checkmark.circle.fill")
+                .font(.system(size: desktop ? 20 : 16, weight: .semibold))
+                .foregroundStyle(color)
+                .frame(width: desktop ? 24 : 22, alignment: .center)
 
-            VStack(alignment: .leading, spacing: 3) {
+            VStack(alignment: .leading, spacing: 4) {
                 Text(title)
-                    .font(.system(size: 11.5, weight: .bold))
+                    .font(.system(size: desktop ? 13 : 11.5, weight: .semibold))
                     .foregroundStyle(primaryTextColor)
-                    .lineLimit(1)
+                    .lineLimit(2)
                 Text(subtitle)
-                    .font(.system(size: 9.5, weight: .medium))
+                    .font(.system(size: desktop ? 11.5 : 9.5, weight: .medium))
                     .foregroundStyle(secondaryTextColor)
-                    .lineLimit(2)
+                    .lineLimit(desktop ? 4 : 2)
             }
             Spacer(minLength: 0)
         }
-        .padding(10)
-        .frame(maxWidth: .infinity, minHeight: 66, alignment: .leading)
+        .padding(desktop ? 12 : 10)
+        .frame(maxWidth: .infinity, minHeight: desktop ? 74 : 66, alignment: .leading)
         .background(
-            RoundedRectangle(cornerRadius: 12, style: .continuous)
-                .fill(colorScheme == .dark ? Color.white.opacity(0.14) : Color.black.opacity(0.05))
+            RoundedRectangle(cornerRadius: desktop ? 12 : 12, style: .continuous)
+                .fill(colorScheme == .dark ? Color.white.opacity(0.08) : Color.black.opacity(0.05))
         )
     }
 
-    private func priceCard(title: String, price: String, subtitle: String, badgeText: String?, isSelected: Bool) -> some View {
+    private func priceCard(title: String, price: String, subtitle: String, badgeText: String?, isSelected: Bool, compact: Bool) -> some View {
         let cardStroke: Color = {
             if isSelected { return Color.blue.opacity(0.95) }
-            return colorScheme == .dark ? Color.white.opacity(0.4) : Color.black.opacity(0.18)
+            return colorScheme == .dark ? Color.white.opacity(0.35) : Color.black.opacity(0.16)
         }()
         let cardFill: Color = {
-            if isSelected { return Color.blue.opacity(colorScheme == .dark ? 0.12 : 0.10) }
-            return colorScheme == .dark ? Color.white.opacity(0.05) : Color.black.opacity(0.025)
+            if isSelected { return Color.blue.opacity(colorScheme == .dark ? 0.14 : 0.11) }
+            return colorScheme == .dark ? Color.white.opacity(0.055) : Color.black.opacity(0.03)
         }()
-        let cardStrokeWidth: CGFloat = isSelected ? 2 : 1.5
+        let cardStrokeWidth: CGFloat = isSelected ? 2.25 : 1
+
+        let titleSize: CGFloat = compact ? 13 : 15
+        let subtitleSize: CGFloat = compact ? 10 : 12
+        let priceSize: CGFloat = compact ? 15 : 18
+        let badgeSize: CGFloat = compact ? 9 : 10
+        let vPad: CGFloat = compact ? 11 : 13
 
         return HStack(alignment: .center) {
-            VStack(alignment: .leading, spacing: 5) {
+            VStack(alignment: .leading, spacing: compact ? 3 : 5) {
                 Text(title)
-                    .font(.system(size: 12, weight: .bold))
+                    .font(.system(size: titleSize, weight: .bold))
                     .foregroundStyle(primaryTextColor)
                 Text(subtitle)
-                    .font(.system(size: 7.5, weight: .medium))
+                    .font(.system(size: subtitleSize, weight: .medium))
                     .foregroundStyle(secondaryTextColor)
+                    .fixedSize(horizontal: false, vertical: true)
             }
 
-            Spacer()
+            Spacer(minLength: 8)
 
-            VStack(alignment: .trailing, spacing: 7) {
+            VStack(alignment: .trailing, spacing: compact ? 5 : 6) {
                 Text(price)
-                    .font(.system(size: 10, weight: .bold))
+                    .font(.system(size: priceSize, weight: .bold))
                     .foregroundStyle(primaryTextColor)
-                    .minimumScaleFactor(0.7)
+                    .minimumScaleFactor(0.65)
                     .lineLimit(1)
                 if let badgeText {
                     Text(badgeText)
-                        .font(.system(size: 7.5, weight: .bold))
+                        .font(.system(size: badgeSize, weight: .bold))
                         .foregroundStyle(.white)
-                        .padding(.horizontal, 8)
-                        .padding(.vertical, 4)
+                        .padding(.horizontal, compact ? 7 : 9)
+                        .padding(.vertical, compact ? 3 : 5)
                         .background(
-                            RoundedRectangle(cornerRadius: 7, style: .continuous)
+                            RoundedRectangle(cornerRadius: 8, style: .continuous)
                                 .fill(Color.blue.opacity(0.95))
                         )
                 }
             }
         }
-        .padding(10)
+        .padding(.horizontal, compact ? 12 : 14)
+        .padding(.vertical, vPad)
         .background(
-            RoundedRectangle(cornerRadius: 14, style: .continuous)
+            RoundedRectangle(cornerRadius: compact ? 12 : 14, style: .continuous)
                 .stroke(cardStroke, lineWidth: cardStrokeWidth)
                 .background(
-                    RoundedRectangle(cornerRadius: 14, style: .continuous)
+                    RoundedRectangle(cornerRadius: compact ? 12 : 14, style: .continuous)
                         .fill(cardFill)
                 )
         )
-        .contentShape(RoundedRectangle(cornerRadius: 14, style: .continuous))
+        .contentShape(RoundedRectangle(cornerRadius: compact ? 12 : 14, style: .continuous))
+    }
+}
+
+enum PremiumPaywallWindowSizing {
+    /// Sizes the paywall to sit inside the parent window (launcher default content ~980×700, browser ~1200×800).
+    static func contentSize(forParentWindow parent: NSWindow) -> NSSize {
+        let inset: CGFloat = 48
+        let content = parent.contentRect(forFrameRect: parent.frame)
+        let usableW = content.width - inset * 2
+        let usableH = content.height - inset * 2
+        let w = min(1000, max(600, usableW))
+        let h = min(720, max(520, usableH))
+        return NSSize(width: floor(w), height: floor(h))
     }
 }
 
@@ -398,6 +522,6 @@ struct PremiumFeaturesView: View {
 #Preview {
     PremiumFeaturesView()
         .environmentObject(PremiumStore.shared)
-        .frame(width: 560, height: 690)
+        .frame(width: 920, height: 620)
 }
 #endif