ソースを参照

Refine settings upgrade flow for premium users.

Show Upgrade only for premium non-lifetime accounts, route it into a lifetime-focused paywall state, and surface the current active plan so users can continue to upgrade.

Made-with: Cursor
huzaifahayat12 1 週間 前
コミット
d993315608
共有1 個のファイルを変更した60 個の追加4 個の削除を含む
  1. 60 4
      meetings_app/ViewController.swift

+ 60 - 4
meetings_app/ViewController.swift

@@ -30,6 +30,7 @@ private enum SettingsAction: Int {
30 30
     case support = 2
31 31
     case moreApps = 3
32 32
     case shareApp = 4
33
+    case upgrade = 5
33 34
 }
34 35
 
35 36
 private enum PremiumPlan: Int {
@@ -84,6 +85,13 @@ private final class StoreKitCoordinator {
84 85
     var onEntitlementsChanged: ((Bool) -> Void)?
85 86
 
86 87
     var hasPremiumAccess: Bool { !activeEntitlementProductIDs.isEmpty }
88
+    var hasLifetimeAccess: Bool { activeEntitlementProductIDs.contains(PremiumStoreProduct.lifetime) }
89
+    var activeNonLifetimePlan: PremiumPlan? {
90
+        activeEntitlementProductIDs
91
+            .compactMap { PremiumStoreProduct.plan(for: $0) }
92
+            .filter { $0 != .lifetime }
93
+            .max(by: { $0.rawValue < $1.rawValue })
94
+    }
87 95
 
88 96
     private var transactionUpdatesTask: Task<Void, Never>?
89 97
 
@@ -266,6 +274,7 @@ final class ViewController: NSViewController {
266 274
     private var paywallPriceLabels: [PremiumPlan: NSTextField] = [:]
267 275
     private var paywallSubtitleLabels: [PremiumPlan: NSTextField] = [:]
268 276
     private var paywallContinueEnabled = true
277
+    private var paywallUpgradeFlowEnabled = false
269 278
     private var hasCompletedInitialStoreKitSync = false
270 279
     private var hasPresentedLaunchPaywall = false
271 280
     private var hasViewAppearedOnce = false
@@ -316,10 +325,12 @@ final class ViewController: NSViewController {
316 325
         let popover = NSPopover()
317 326
         popover.behavior = .transient
318 327
         popover.animates = true
328
+        let showUpgradeInSettings = storeKitCoordinator.hasPremiumAccess && !storeKitCoordinator.hasLifetimeAccess
319 329
         popover.contentViewController = SettingsMenuViewController(
320 330
             palette: palette,
321 331
             typography: typography,
322 332
             darkModeEnabled: darkModeEnabled,
333
+            showUpgradeInSettings: showUpgradeInSettings,
323 334
             onToggleDarkMode: { [weak self] enabled in
324 335
                 self?.setDarkMode(enabled)
325 336
             },
@@ -737,6 +748,10 @@ private extension ViewController {
737 748
             NSPasteboard.general.clearContents()
738 749
             NSPasteboard.general.setString(urlString, forType: .string)
739 750
             showSimpleAlert(title: "Share App", message: "Link copied to clipboard:\n\(urlString)")
751
+        case .upgrade:
752
+            settingsPopover?.performClose(nil)
753
+            settingsPopover = nil
754
+            showPaywall(upgradeFlow: true, preferredPlan: .lifetime)
740 755
         }
741 756
     }
742 757
 
@@ -748,8 +763,13 @@ private extension ViewController {
748 763
         alert.runModal()
749 764
     }
750 765
 
751
-    private func showPaywall() {
766
+    private func showPaywall(upgradeFlow: Bool = false, preferredPlan: PremiumPlan? = nil) {
767
+        paywallUpgradeFlowEnabled = upgradeFlow
768
+        if let preferredPlan {
769
+            selectedPremiumPlan = preferredPlan
770
+        }
752 771
         if let existing = paywallWindow {
772
+            refreshPaywallStoreUI()
753 773
             animatePaywallPresentation(existing)
754 774
             existing.makeKeyAndOrderFront(nil)
755 775
             NSApp.activate(ignoringOtherApps: true)
@@ -866,6 +886,16 @@ private extension ViewController {
866 886
 
867 887
     private func paywallOfferText(for plan: PremiumPlan) -> String {
868 888
         if storeKitCoordinator.hasPremiumAccess {
889
+            if storeKitCoordinator.hasLifetimeAccess {
890
+                return "Lifetime premium is active on this Apple ID."
891
+            }
892
+            if paywallUpgradeFlowEnabled {
893
+                let currentPlanName = storeKitCoordinator.activeNonLifetimePlan?.displayName ?? "Premium"
894
+                if plan == .lifetime {
895
+                    return "Current plan: \(currentPlanName). Tap Continue to upgrade to Lifetime."
896
+                }
897
+                return "Current plan: \(currentPlanName). Select Lifetime to upgrade."
898
+            }
869 899
             return "Premium is active on this Apple ID."
870 900
         }
871 901
         let productID = PremiumStoreProduct.productID(for: plan)
@@ -1052,10 +1082,20 @@ private extension ViewController {
1052 1082
             paywallContinueButton?.alphaValue = 0.75
1053 1083
             return
1054 1084
         }
1055
-        if storeKitCoordinator.hasPremiumAccess {
1085
+        if storeKitCoordinator.hasLifetimeAccess {
1056 1086
             paywallContinueEnabled = false
1057 1087
             paywallContinueLabel?.stringValue = "Premium Active"
1058 1088
             paywallContinueButton?.alphaValue = 0.75
1089
+        } else if paywallUpgradeFlowEnabled && storeKitCoordinator.hasPremiumAccess {
1090
+            if selectedPremiumPlan == .lifetime {
1091
+                paywallContinueEnabled = true
1092
+                paywallContinueLabel?.stringValue = "Continue"
1093
+                paywallContinueButton?.alphaValue = 1.0
1094
+            } else {
1095
+                paywallContinueEnabled = false
1096
+                paywallContinueLabel?.stringValue = "Select Lifetime to Upgrade"
1097
+                paywallContinueButton?.alphaValue = 0.75
1098
+            }
1059 1099
         } else {
1060 1100
             paywallContinueEnabled = true
1061 1101
             paywallContinueLabel?.stringValue = "Continue"
@@ -2736,6 +2776,17 @@ private extension ViewController {
2736 2776
     }
2737 2777
 }
2738 2778
 
2779
+private extension PremiumPlan {
2780
+    var displayName: String {
2781
+        switch self {
2782
+        case .weekly: return "Weekly"
2783
+        case .monthly: return "Monthly"
2784
+        case .yearly: return "Yearly"
2785
+        case .lifetime: return "Lifetime"
2786
+        }
2787
+    }
2788
+}
2789
+
2739 2790
 extension ViewController: NSTextFieldDelegate {
2740 2791
     func control(_ control: NSControl, textView: NSTextView, doCommandBy commandSelector: Selector) -> Bool {
2741 2792
         if control === browseAddressField, commandSelector == #selector(NSResponder.insertNewline(_:)) {
@@ -2751,6 +2802,7 @@ extension ViewController: NSWindowDelegate {
2751 2802
         guard let closingWindow = notification.object as? NSWindow else { return }
2752 2803
         if closingWindow === paywallWindow {
2753 2804
             paywallWindow = nil
2805
+            paywallUpgradeFlowEnabled = false
2754 2806
         }
2755 2807
     }
2756 2808
 }
@@ -3258,6 +3310,7 @@ private final class SettingsMenuViewController: NSViewController {
3258 3310
         palette: Palette,
3259 3311
         typography: Typography,
3260 3312
         darkModeEnabled: Bool,
3313
+        showUpgradeInSettings: Bool,
3261 3314
         onToggleDarkMode: @escaping (Bool) -> Void,
3262 3315
         onAction: @escaping (SettingsAction) -> Void
3263 3316
     ) {
@@ -3266,7 +3319,7 @@ private final class SettingsMenuViewController: NSViewController {
3266 3319
         self.onToggleDarkMode = onToggleDarkMode
3267 3320
         self.onAction = onAction
3268 3321
         super.init(nibName: nil, bundle: nil)
3269
-        self.view = makeView(darkModeEnabled: darkModeEnabled)
3322
+        self.view = makeView(darkModeEnabled: darkModeEnabled, showUpgradeInSettings: showUpgradeInSettings)
3270 3323
     }
3271 3324
 
3272 3325
     @available(*, unavailable)
@@ -3278,7 +3331,7 @@ private final class SettingsMenuViewController: NSViewController {
3278 3331
         darkToggle?.state = enabled ? .on : .off
3279 3332
     }
3280 3333
 
3281
-    private func makeView(darkModeEnabled: Bool) -> NSView {
3334
+    private func makeView(darkModeEnabled: Bool, showUpgradeInSettings: Bool) -> NSView {
3282 3335
         let root = NSView()
3283 3336
         root.translatesAutoresizingMaskIntoConstraints = false
3284 3337
 
@@ -3312,6 +3365,9 @@ private final class SettingsMenuViewController: NSViewController {
3312 3365
         stack.addArrangedSubview(settingsActionRow(icon: "💬", title: "Support", action: .support))
3313 3366
         stack.addArrangedSubview(settingsActionRow(icon: "⋯", title: "More Apps", action: .moreApps))
3314 3367
         stack.addArrangedSubview(settingsActionRow(icon: "⤴︎", title: "Share App", action: .shareApp))
3368
+        if showUpgradeInSettings {
3369
+            stack.addArrangedSubview(settingsActionRow(icon: "⬆︎", title: "Upgrade", action: .upgrade))
3370
+        }
3315 3371
 
3316 3372
         for v in stack.arrangedSubviews {
3317 3373
             v.widthAnchor.constraint(equalTo: stack.widthAnchor).isActive = true