Prechádzať zdrojové kódy

Fix premium subscribers seeing upgrade prompts after launch.

Preserve cached entitlements while StoreKit reconciles on startup so returning subscribers keep premium access and the AI tools reflect the correct subscription state.

Co-authored-by: Cursor <cursoragent@cursor.com>
AhtashamShahzad1 3 týždňov pred
rodič
commit
b9f13f5ade

+ 54 - 6
App AI for Reddit/Managers/SubscriptionManager.swift

@@ -73,6 +73,7 @@ final class SubscriptionManager: ObservableObject {
     private var initialEntitlementTask: Task<Void, Never>?
     private var entitlementRefreshTask: Task<Void, Never>?
     private var hasStarted = false
+    private var launchedWithCachedPremium = false
     private var cancellables = Set<AnyCancellable>()
     private var lastBackgroundEntitlementRefresh: Date?
 
@@ -84,6 +85,7 @@ final class SubscriptionManager: ObservableObject {
 
         // Optimistic restore so returning subscribers aren't paywalled while StoreKit resolves.
         if UserDefaults.standard.bool(forKey: Self.cachedPremiumKey) {
+            launchedWithCachedPremium = true
             hasPremiumAccess = true
             if let planRaw = UserDefaults.standard.string(forKey: Self.cachedPremiumPlanKey),
                let plan = PaywallPlan(rawValue: planRaw) {
@@ -101,6 +103,22 @@ final class SubscriptionManager: ObservableObject {
 
         initialEntitlementTask = Task {
             await refreshPremiumAccess()
+
+            let shouldReconcileWithAppStore = launchedWithCachedPremium
+                || (!hasPremiumAccess && hasEverPurchasedPremium)
+
+            if shouldReconcileWithAppStore {
+                do {
+                    try await syncAppStoreWithTimeout(seconds: 15)
+                } catch {
+                    #if DEBUG
+                    print("[SubscriptionManager] App Store sync failed during premium reconciliation")
+                    #endif
+                }
+                launchedWithCachedPremium = false
+                await refreshPremiumAccess()
+            }
+
             await loadProducts(presentLoadingUI: false)
         }
     }
@@ -366,17 +384,31 @@ final class SubscriptionManager: ObservableObject {
             }
         }
 
+        if resolvedPlan == nil {
+            resolvedPlan = await activePlanFromLatestTransactions()
+        }
+
         let finalPlan = mergeTrustedPlan(resolvedPlan, trustedPlan)
-        activePremiumPlan = finalPlan
-        hasPremiumAccess = finalPlan != nil
 
-        if hasPremiumAccess {
+        if let finalPlan {
+            activePremiumPlan = finalPlan
+            hasPremiumAccess = true
+            launchedWithCachedPremium = false
             markAsHavingPurchasedPremium()
+        } else if launchedWithCachedPremium, activePremiumPlan != nil {
+            // StoreKit can lag behind on launch; keep the cached plan until reconciliation finishes.
+            hasPremiumAccess = true
+        } else {
+            activePremiumPlan = nil
+            hasPremiumAccess = false
         }
 
-        hasResolvedPremiumStatus = true
-        persistPremiumCache()
-        await syncPurchaseHistory()
+        let resolved = !launchedWithCachedPremium
+        if resolved {
+            hasResolvedPremiumStatus = true
+            persistPremiumCache()
+            await syncPurchaseHistory()
+        }
         await refreshTrialOffers()
     }
 
@@ -414,6 +446,7 @@ final class SubscriptionManager: ObservableObject {
     }
 
     private func applyTrustedPlan(_ plan: PaywallPlan) {
+        launchedWithCachedPremium = false
         activePremiumPlan = preferredPlan(activePremiumPlan, plan)
         hasPremiumAccess = true
         hasResolvedPremiumStatus = true
@@ -422,6 +455,7 @@ final class SubscriptionManager: ObservableObject {
     }
 
     private func revokePremiumAccess() {
+        launchedWithCachedPremium = false
         activePremiumPlan = nil
         hasPremiumAccess = false
         hasResolvedPremiumStatus = true
@@ -448,6 +482,7 @@ final class SubscriptionManager: ObservableObject {
 
     private func applyPremiumAccess(from transaction: StoreKit.Transaction) {
         guard let plan = planGrantingPremium(from: transaction) else { return }
+        launchedWithCachedPremium = false
         activePremiumPlan = preferredPlan(activePremiumPlan, plan)
         hasPremiumAccess = true
         hasResolvedPremiumStatus = true
@@ -561,6 +596,19 @@ final class SubscriptionManager: ObservableObject {
         return candidate
     }
 
+    private func activePlanFromLatestTransactions() async -> PaywallPlan? {
+        var resolvedPlan: PaywallPlan?
+
+        for productID in SubscriptionProductID.all {
+            guard let result = await Transaction.latest(for: productID) else { continue }
+            guard case .verified(let transaction) = result else { continue }
+            guard let plan = planGrantingPremium(from: transaction) else { continue }
+            resolvedPlan = preferredPlan(resolvedPlan, plan)
+        }
+
+        return resolvedPlan
+    }
+
     private func activeLifetimePlan() async -> PaywallPlan? {
         guard let result = await Transaction.latest(for: SubscriptionProductID.lifetime) else {
             return nil

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

@@ -35,7 +35,7 @@ struct FrontPageView: View {
             .fullScreenLayoutWhenNeeded()
             .environment(\.requirePremiumAccess, {
                 if subscriptions.isResolvingEntitlements {
-                    return false
+                    return subscriptions.hasPremiumAccess
                 }
                 if subscriptions.hasPremiumAccess { return true }
                 viewModel.showPaywall()

+ 22 - 1
App AI for Reddit/Views/RootView.swift

@@ -1,6 +1,7 @@
 import SwiftUI
 
 struct RootView: View {
+    @EnvironmentObject private var subscriptions: SubscriptionManager
     let session: AppSessionState
     @Bindable var splashViewModel: SplashViewModel
 
@@ -16,7 +17,27 @@ struct RootView: View {
         }
         .animation(.easeInOut(duration: 0.45), value: splashViewModel.isVisible)
         .task {
-            await splashViewModel.runIfNeeded()
+            async let splash: Void = splashViewModel.runIfNeeded()
+            async let entitlements: Void = subscriptions.ensureEntitlementsResolved()
+            _ = await (splash, entitlements)
+            syncToolSubscriptionState()
         }
     }
+
+    private func syncToolSubscriptionState() {
+        let hasPremium = subscriptions.hasPremiumAccess
+        let hasEverPurchased = subscriptions.hasEverPurchasedPremium
+        session.postGenerator.setSubscriptionState(
+            hasPremiumAccess: hasPremium,
+            hasEverPurchasedPremium: hasEverPurchased
+        )
+        session.titleOptimizer.setSubscriptionState(
+            hasPremiumAccess: hasPremium,
+            hasEverPurchasedPremium: hasEverPurchased
+        )
+        session.commentWriter.setSubscriptionState(
+            hasPremiumAccess: hasPremium,
+            hasEverPurchasedPremium: hasEverPurchased
+        )
+    }
 }