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

Limit paywall free-trial messaging to first-time app users.

Track first-session completion so returning users no longer see the yearly trial badge, CTA, or disclosure unless StoreKit and first-use eligibility both apply.

Co-authored-by: Cursor <cursoragent@cursor.com>
AhtashamShahzad1 3 тижнів тому
батько
коміт
7008d680df

+ 40 - 0
App AI for Reddit/Managers/AppUsageManager.swift

@@ -0,0 +1,40 @@
+import AppKit
+import Combine
+import Foundation
+
+@MainActor
+final class AppUsageManager {
+    static let shared = AppUsageManager()
+
+    static let didCompleteFirstSession = Notification.Name("AppUsageManager.didCompleteFirstSession")
+
+    private static let hasCompletedFirstSessionKey = "AppUsageManager.hasCompletedFirstSession"
+
+    private let defaults: UserDefaults
+    private var cancellables = Set<AnyCancellable>()
+
+    private init(defaults: UserDefaults = .standard) {
+        self.defaults = defaults
+    }
+
+    /// `true` until the user has finished their first app session.
+    var isFirstTimeUser: Bool {
+        !defaults.bool(forKey: Self.hasCompletedFirstSessionKey)
+    }
+
+    func start() {
+        guard cancellables.isEmpty else { return }
+
+        NotificationCenter.default.publisher(for: NSApplication.willResignActiveNotification)
+            .sink { [weak self] _ in
+                self?.markFirstSessionCompleted()
+            }
+            .store(in: &cancellables)
+    }
+
+    func markFirstSessionCompleted() {
+        guard isFirstTimeUser else { return }
+        defaults.set(true, forKey: Self.hasCompletedFirstSessionKey)
+        NotificationCenter.default.post(name: Self.didCompleteFirstSession, object: nil)
+    }
+}

+ 19 - 3
App AI for Reddit/Managers/SubscriptionManager.swift

@@ -58,6 +58,11 @@ final class SubscriptionManager: ObservableObject {
     private var inFlightProductLoad: Task<Void, Never>?
     private var initialEntitlementTask: Task<Void, Never>?
     private var hasStarted = false
+    private var cancellables = Set<AnyCancellable>()
+
+    var isFirstTimeAppUser: Bool {
+        AppUsageManager.shared.isFirstTimeUser
+    }
 
     init() {
         hasPremiumAccess = UserDefaults.standard.bool(forKey: Self.cachedPremiumKey)
@@ -71,6 +76,14 @@ final class SubscriptionManager: ObservableObject {
         guard !hasStarted else { return }
         hasStarted = true
 
+        AppUsageManager.shared.start()
+
+        NotificationCenter.default.publisher(for: AppUsageManager.didCompleteFirstSession)
+            .sink { [weak self] _ in
+                self?.objectWillChange.send()
+            }
+            .store(in: &cancellables)
+
         Task { await listenForTransactionUpdates() }
 
         initialEntitlementTask = Task {
@@ -89,7 +102,8 @@ final class SubscriptionManager: ObservableObject {
     }
 
     func eligibleIntroOffer(for product: Product) -> Product.SubscriptionOffer? {
-        guard introOfferEligibleByProductID[product.id] == true,
+        guard isFirstTimeAppUser,
+              introOfferEligibleByProductID[product.id] == true,
               let offer = product.subscription?.introductoryOffer,
               offer.paymentMode == .freeTrial else {
             return nil
@@ -131,9 +145,11 @@ final class SubscriptionManager: ObservableObject {
             return plan.fallbackBillingDescription(config: config)
         }
 
-        if let subscription = product.subscription,
+        if isFirstTimeAppUser,
+           let subscription = product.subscription,
            let intro = subscription.introductoryOffer,
-           intro.paymentMode == .freeTrial {
+           intro.paymentMode == .freeTrial,
+           introOfferEligibleByProductID[product.id] == true {
             let trialPeriod = formattedPeriod(intro.period)
             let billingPeriod = formattedSubscriptionPeriod(subscription.subscriptionPeriod)
             return "\(trialPeriod) Free Trial, then \(product.displayPrice) per \(billingPeriod)"

+ 1 - 0
App AI for Reddit/Views/FrontPageView.swift

@@ -44,6 +44,7 @@ struct FrontPageView: View {
 
             if viewModel.isPaywallPresented {
                 PaywallView(viewModel: paywallViewModel) {
+                    AppUsageManager.shared.markFirstSessionCompleted()
                     viewModel.dismissPaywall()
                 }
                 .frame(maxWidth: .infinity, maxHeight: .infinity)