Quellcode durchsuchen

Fix paywall trial badge rules and remove launch-time App Store sync.

Stop calling AppStore.sync on relaunch to avoid the Xcode Apple ID prompt, and show the 3-day trial badge only for first-time users while hiding it after purchase or refund. Reconcile purchase history with StoreKit so sandbox transaction deletes reset eligibility without restoring the badge for refunded customers.

Co-authored-by: Cursor <cursoragent@cursor.com>
AhtashamShahzad1 vor 3 Wochen
Ursprung
Commit
5fc12348a6

+ 31 - 24
App AI for Reddit/Managers/SubscriptionManager.swift

@@ -102,21 +102,14 @@ final class SubscriptionManager: ObservableObject {
         Task { await listenForTransactionUpdates() }
 
         initialEntitlementTask = Task {
+            // Local StoreKit entitlements only on launch. AppStore.sync() prompts for
+            // Apple ID and fires on every relaunch when purchase flags are cached.
             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
-                }
+            if launchedWithCachedPremium {
                 launchedWithCachedPremium = false
-                await refreshPremiumAccess()
+                hasResolvedPremiumStatus = true
+                persistPremiumCache()
             }
 
             await loadProducts(presentLoadingUI: false)
@@ -491,21 +484,26 @@ final class SubscriptionManager: ObservableObject {
     }
 
     private func markAsHavingPurchasedPremium() {
-        guard !hasEverPurchasedPremium else { return }
+        let wasAlreadyMarked = hasEverPurchasedPremium
         hasEverPurchasedPremium = true
         trialDisplayByPlan = [:]
         UserDefaults.standard.set(true, forKey: Self.cachedHasEverPurchasedKey)
-        AIFreeUsageManager.shared.forfeitFreeUses()
+        if !wasAlreadyMarked {
+            AIFreeUsageManager.shared.forfeitFreeUses()
+        }
     }
 
-    /// Aligns local purchase flags with StoreKit history.
-    /// Purchase history is sticky: once a user has bought, they keep that status even if
-    /// StoreKit history is cleared (e.g. sandbox transaction delete).
+    /// Syncs `hasEverPurchasedPremium` with StoreKit history.
+    /// - Refund: revoked transactions remain in history → flag stays `true` → no trial badge.
+    /// - Sandbox reset: no transactions → flag clears → trial badge returns for testing.
     private func syncPurchaseHistory() async {
+        var foundPurchase = false
+
         for await result in Transaction.all {
             guard case .verified(let transaction) = result else { continue }
             guard SubscriptionProductID.all.contains(transaction.productID) else { continue }
 
+            foundPurchase = true
             if !hasEverPurchasedPremium {
                 hasEverPurchasedPremium = true
                 UserDefaults.standard.set(true, forKey: Self.cachedHasEverPurchasedKey)
@@ -513,9 +511,15 @@ final class SubscriptionManager: ObservableObject {
             }
             return
         }
+
+        if !foundPurchase, hasEverPurchasedPremium {
+            hasEverPurchasedPremium = false
+            UserDefaults.standard.set(false, forKey: Self.cachedHasEverPurchasedKey)
+        }
     }
 
     private func refreshTrialOffers() async {
+        // Trial badge is first-time users only — never after a purchase or refund.
         guard isEligibleForIntroTrial else {
             trialDisplayByPlan = [:]
             return
@@ -534,15 +538,18 @@ final class SubscriptionManager: ObservableObject {
     }
 
     private func resolveTrialDisplay(for plan: PaywallPlan) async -> PaywallTrialDisplay? {
-        guard plan == PaywallConfigService.shared.config.trialEligiblePlan else { return nil }
-        guard let product = product(for: plan),
-              let subscription = product.subscription else {
-            return nil
+        let config = PaywallConfigService.shared.config
+        guard plan == config.trialEligiblePlan else { return nil }
+        guard let fallback = config.trial.fallbackDuration else { return nil }
+
+        if let product = product(for: plan),
+           let subscription = product.subscription,
+           await subscription.isEligibleForIntroOffer,
+           let offer = subscription.introductoryOffer {
+            return PaywallTrialDisplay.from(offer: offer, config: config)
         }
-        guard await subscription.isEligibleForIntroOffer else { return nil }
 
-        guard let offer = subscription.introductoryOffer else { return nil }
-        return PaywallTrialDisplay.from(offer: offer, config: PaywallConfigService.shared.config)
+        return PaywallTrialDisplay.from(fallback: fallback, config: config)
     }
 
     private func persistPremiumCache() {

+ 9 - 0
App AI for Reddit/Views/PaywallView.swift

@@ -86,12 +86,21 @@ struct PaywallView: View {
             viewModel.syncSelectedPlan(with: availablePlans)
         }
         .onChange(of: subscriptions.hasEverPurchasedPremium) { _, _ in
+            Task { await subscriptions.refreshTrialEligibility() }
             viewModel.syncSelectedPlan(with: availablePlans)
             if let eligiblePlan = config.trialEligiblePlan,
                subscriptions.trialDisplay(for: eligiblePlan) != nil {
                 viewModel.selectPlan(eligiblePlan)
             }
         }
+        .onChange(of: subscriptions.hasPremiumAccess) { _, _ in
+            Task { await subscriptions.refreshTrialEligibility() }
+        }
+        .onChange(of: subscriptions.hasAllProductsLoaded) { _, loaded in
+            if loaded {
+                Task { await subscriptions.refreshTrialEligibility() }
+            }
+        }
         .onChange(of: subscriptions.trialDisplayByPlan) { _, _ in
             if let eligiblePlan = config.trialEligiblePlan,
                subscriptions.trialDisplay(for: eligiblePlan) != nil {