Quellcode durchsuchen

Improve premium subscription UI for active subscribers.

Show the sidebar pro card after purchase, let premium users close the paywall, and route manage-subscription actions to the paywall or Apple subscriptions.

Co-authored-by: Cursor <cursoragent@cursor.com>
AhtashamShahzad1 vor 1 Monat
Ursprung
Commit
b4d77a92e6

+ 2 - 2
gramora.xcodeproj/project.pbxproj

@@ -269,7 +269,7 @@
 				SWIFT_APPROACHABLE_CONCURRENCY = YES;
 				SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor;
 				SWIFT_EMIT_LOC_STRINGS = YES;
-				SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
+				SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = NO;
 				SWIFT_VERSION = 5.0;
 			};
 			name = Debug;
@@ -300,7 +300,7 @@
 				SWIFT_APPROACHABLE_CONCURRENCY = YES;
 				SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor;
 				SWIFT_EMIT_LOC_STRINGS = YES;
-				SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
+				SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = NO;
 				SWIFT_VERSION = 5.0;
 			};
 			name = Release;

+ 6 - 0
gramora/Managers/SubscriptionManager.swift

@@ -1,3 +1,4 @@
+import AppKit
 import Combine
 import Foundation
 import StoreKit
@@ -182,6 +183,11 @@ final class SubscriptionManager: ObservableObject {
         }
     }
 
+    func openSubscriptionManagement() {
+        guard let url = URL(string: "https://apps.apple.com/account/subscriptions") else { return }
+        NSWorkspace.shared.open(url)
+    }
+
     func restorePurchases() async {
         purchaseError = nil
         do {

+ 10 - 4
gramora/Views/Components/PremiumCardView.swift

@@ -1,7 +1,9 @@
 import SwiftUI
 
 struct PremiumCardView: View {
+    let isPremium: Bool
     let onUpgradeTapped: () -> Void
+    let onManageSubscriptionTapped: () -> Void
     @State private var isUpgradeHovered = false
 
     var body: some View {
@@ -11,20 +13,24 @@ struct PremiumCardView: View {
                     .font(.system(size: 14))
                     .foregroundStyle(Color(red: 0.95, green: 0.75, blue: 0.20))
 
-                Text("Unlock Premium")
+                Text(isPremium ? "Gramora Pro" : "Unlock Premium")
                     .font(.system(size: 13, weight: .semibold))
                     .foregroundStyle(AppTheme.textPrimary)
             }
 
-            Text("Get unlimited checks, advanced features and an ad-free experience.")
+            Text(
+                isPremium
+                    ? "You have unlimited checks, advanced features and an ad-free experience."
+                    : "Get unlimited checks, advanced features and an ad-free experience."
+            )
                 .font(.system(size: 11))
                 .foregroundStyle(AppTheme.textSecondary)
                 .lineSpacing(2)
                 .fixedSize(horizontal: false, vertical: true)
 
-            Button(action: onUpgradeTapped) {
+            Button(action: isPremium ? onManageSubscriptionTapped : onUpgradeTapped) {
                 HStack(spacing: 6) {
-                    Text("Upgrade Now")
+                    Text(isPremium ? "Manage Subscription" : "Upgrade Now")
                         .font(.system(size: 12, weight: .semibold))
 
                     Image(systemName: "arrow.up.right")

+ 2 - 1
gramora/Views/MainView.swift

@@ -14,7 +14,8 @@ struct MainView: View {
                     SidebarView(
                         selectedDestination: $viewModel.selectedDestination,
                         isPremium: subscriptions.hasPremiumAccess,
-                        onUpgradeTapped: viewModel.showPaywall
+                        onUpgradeTapped: viewModel.showPaywall,
+                        onManageSubscriptionTapped: viewModel.showPaywall
                     )
                     .frame(maxHeight: .infinity)
 

+ 49 - 1
gramora/Views/PaywallView.swift

@@ -14,6 +14,10 @@ struct PaywallView: View {
         subscriptions.purchasingPlan != nil
     }
 
+    private var isPremium: Bool {
+        subscriptions.hasPremiumAccess
+    }
+
     var body: some View {
         GeometryReader { proxy in
             let horizontalInset = min(max(proxy.size.width * 0.03, 12), 34)
@@ -47,6 +51,10 @@ struct PaywallView: View {
                 .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center)
                 .background(Color.white.ignoresSafeArea())
 
+                if isPremium {
+                    closeButton
+                }
+
                 if subscriptions.isLoadingProducts && !subscriptions.hasAllProductsLoaded {
                     Color.white.opacity(0.65)
                         .ignoresSafeArea()
@@ -74,6 +82,13 @@ struct PaywallView: View {
         }
     }
 
+    private var closeButton: some View {
+        PaywallCloseButton(action: onClose)
+            .padding(.top, 12)
+            .padding(.trailing, 20)
+            .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topTrailing)
+    }
+
     private var titleSection: some View {
         VStack(spacing: 4) {
             HStack(spacing: 10) {
@@ -193,7 +208,10 @@ struct PaywallView: View {
             .multilineTextAlignment(.center)
 
             HStack(spacing: 0) {
-                PaywallFooterLinkView(title: "Continue with free plan", action: onClose)
+                PaywallFooterLinkView(
+                    title: isPremium ? "Manage Subscription" : "Continue with free plan",
+                    action: isPremium ? subscriptions.openSubscriptionManagement : onClose
+                )
                 PaywallFooterLinkView(title: "Restore Purchases") {
                     Task {
                         await subscriptions.restorePurchases()
@@ -215,6 +233,36 @@ struct PaywallView: View {
 
 }
 
+private struct PaywallCloseButton: View {
+    let action: () -> Void
+    @State private var isHovered = false
+
+    var body: some View {
+        Button(action: action) {
+            Image(systemName: "xmark")
+                .font(.system(size: 13, weight: .medium))
+                .foregroundStyle(isHovered ? AppTheme.toolbarForegroundHover : AppTheme.textSecondary)
+                .frame(width: 32, height: 32)
+                .background(isHovered ? AppTheme.toolbarBackgroundHover : Color.white)
+                .clipShape(RoundedRectangle(cornerRadius: 8))
+                .overlay(
+                    RoundedRectangle(cornerRadius: 8)
+                        .stroke(isHovered ? AppTheme.toolbarBorderHover : AppTheme.border, lineWidth: 1)
+                )
+                .shadow(
+                    color: isHovered ? AppTheme.toolbarShadowHover : AppTheme.toolbarShadow,
+                    radius: isHovered ? 4 : 2,
+                    y: isHovered ? 2 : 1
+                )
+                .scaleEffect(isHovered ? 1.02 : 1.0)
+                .animation(.easeOut(duration: 0.15), value: isHovered)
+        }
+        .buttonStyle(.plain)
+        .onHover { isHovered = $0 }
+        .accessibilityLabel("Close")
+    }
+}
+
 private struct PaywallFeatureBox: View {
     let feature: PaywallFeature
     let minHeight: CGFloat

+ 8 - 5
gramora/Views/SidebarView.swift

@@ -4,6 +4,7 @@ struct SidebarView: View {
     @Binding var selectedDestination: NavigationDestination
     let isPremium: Bool
     let onUpgradeTapped: () -> Void
+    let onManageSubscriptionTapped: () -> Void
 
     var body: some View {
         VStack(alignment: .leading, spacing: 0) {
@@ -26,11 +27,13 @@ struct SidebarView: View {
 
             Spacer(minLength: 12)
 
-            if !isPremium {
-                PremiumCardView(onUpgradeTapped: onUpgradeTapped)
-                    .padding(.horizontal, 16)
-                    .padding(.bottom, 20)
-            }
+            PremiumCardView(
+                isPremium: isPremium,
+                onUpgradeTapped: onUpgradeTapped,
+                onManageSubscriptionTapped: onManageSubscriptionTapped
+            )
+            .padding(.horizontal, 16)
+            .padding(.bottom, 20)
         }
         .frame(width: AppTheme.sidebarWidth, alignment: .top)
         .frame(maxHeight: .infinity)