Преглед на файлове

Fix lifetime and yearly premium access detection after purchase.

Grant premium immediately from verified transactions, add lifetime entitlement checks via Transaction.latest, and align restore failures with one-time purchases.

Co-authored-by: Cursor <cursoragent@cursor.com>
AhtashamShahzad1 преди 1 месец
родител
ревизия
2c76525c73
променени са 1 файла, в които са добавени 97 реда и са изтрити 18 реда
  1. 97 18
      Reddit App/Managers/SubscriptionManager.swift

+ 97 - 18
Reddit App/Managers/SubscriptionManager.swift

@@ -10,6 +10,7 @@ enum PurchaseError: Equatable {
     case purchaseNotVerified
     case purchasePending
     case noActiveSubscriptions
+    case noActivePurchases
     case generic
 
     var message: String {
@@ -26,6 +27,8 @@ enum PurchaseError: Equatable {
             "This purchase is waiting for approval (for example, Ask to Buy)."
         case .noActiveSubscriptions:
             "No active subscriptions were found for this Apple ID."
+        case .noActivePurchases:
+            "No premium purchases were found for this Apple ID."
         case .generic:
             "Something went wrong with your subscription. Please try again."
         }
@@ -43,8 +46,13 @@ final class SubscriptionManager: ObservableObject {
     @Published private(set) var purchasingPlan: PaywallPlan?
     @Published private(set) var hasPremiumAccess = false
     @Published private(set) var hasResolvedPremiumStatus = false
+    @Published private(set) var activePremiumPlan: PaywallPlan?
     @Published var purchaseError: PurchaseError?
 
+    var hasLifetimeAccess: Bool {
+        activePremiumPlan == .lifetime
+    }
+
     var hasAllProductsLoaded: Bool {
         SubscriptionProductID.all.allSatisfy { productsByID[$0] != nil }
     }
@@ -150,6 +158,11 @@ final class SubscriptionManager: ObservableObject {
                     purchaseError = .purchaseNotVerified
                     return false
                 }
+
+                if transactionGrantsPremium(transaction) {
+                    applyPremiumAccess(from: transaction)
+                }
+
                 await transaction.finish()
                 await refreshPremiumAccess()
                 return hasPremiumAccess
@@ -178,7 +191,7 @@ final class SubscriptionManager: ObservableObject {
             try await AppStore.sync()
             await refreshPremiumAccess()
             if !hasPremiumAccess {
-                purchaseError = .noActiveSubscriptions
+                purchaseError = .noActivePurchases
             }
         } catch {
             purchaseError = .generic
@@ -240,30 +253,90 @@ final class SubscriptionManager: ObservableObject {
     }
 
     private func refreshPremiumAccess() async {
-        var hasPremium = false
+        var resolvedPlan: PaywallPlan?
 
-        for await result in Transaction.currentEntitlements {
-            guard case .verified(let transaction) = result else { continue }
-            guard SubscriptionProductID.all.contains(transaction.productID) else { continue }
-            guard transaction.revocationDate == nil else { continue }
+        if let lifetimePlan = await activeLifetimePlan() {
+            resolvedPlan = lifetimePlan
+        }
 
-            if let expirationDate = transaction.expirationDate, expirationDate < Date() {
-                continue
+        if resolvedPlan == nil {
+            for await result in Transaction.currentEntitlements {
+                guard case .verified(let transaction) = result else { continue }
+                guard let plan = planGrantingPremium(from: transaction) else { continue }
+                resolvedPlan = preferredPlan(resolvedPlan, plan)
             }
-
-            hasPremium = true
-            break
         }
 
-        if !hasPremium {
-            hasPremium = await hasActiveSubscriptionStatus()
+        if resolvedPlan == nil || resolvedPlan != .lifetime {
+            if let subscriptionPlan = await activeSubscriptionPlan() {
+                resolvedPlan = preferredPlan(resolvedPlan, subscriptionPlan)
+            }
         }
 
-        hasPremiumAccess = hasPremium
+        activePremiumPlan = resolvedPlan
+        hasPremiumAccess = resolvedPlan != nil
         hasResolvedPremiumStatus = true
     }
 
-    private func hasActiveSubscriptionStatus() async -> Bool {
+    private func applyPremiumAccess(from transaction: StoreKit.Transaction) {
+        guard let plan = planGrantingPremium(from: transaction) else { return }
+        activePremiumPlan = preferredPlan(activePremiumPlan, plan)
+        hasPremiumAccess = true
+        hasResolvedPremiumStatus = true
+    }
+
+    private func planGrantingPremium(from transaction: StoreKit.Transaction) -> PaywallPlan? {
+        guard transactionGrantsPremium(transaction) else { return nil }
+        return SubscriptionProductID.plan(for: transaction.productID)
+    }
+
+    private func transactionGrantsPremium(_ transaction: StoreKit.Transaction) -> Bool {
+        guard SubscriptionProductID.all.contains(transaction.productID) else { return false }
+        guard transaction.revocationDate == nil else { return false }
+        if let expirationDate = transaction.expirationDate, expirationDate < Date() {
+            return false
+        }
+        return true
+    }
+
+    private func preferredPlan(_ current: PaywallPlan?, _ candidate: PaywallPlan) -> PaywallPlan {
+        if current == .lifetime || candidate == .lifetime {
+            return .lifetime
+        }
+        if let current {
+            if current == .yearly || candidate == .yearly {
+                return .yearly
+            }
+            return .monthly
+        }
+        return candidate
+    }
+
+    private func activeLifetimePlan() async -> PaywallPlan? {
+        guard let result = await Transaction.latest(for: SubscriptionProductID.lifetime) else {
+            return nil
+        }
+        guard case .verified(let transaction) = result else { return nil }
+        return planGrantingPremium(from: transaction)
+    }
+
+    private func activeSubscriptionPlan() async -> PaywallPlan? {
+        var resolvedPlan: PaywallPlan?
+
+        for await result in Transaction.currentEntitlements {
+            guard case .verified(let transaction) = result else { continue }
+            guard let plan = planGrantingPremium(from: transaction), plan != .lifetime else { continue }
+            resolvedPlan = preferredPlan(resolvedPlan, plan)
+        }
+
+        if resolvedPlan == nil, await hasActiveSubscriptionStatus() {
+            resolvedPlan = await subscriptionPlanFromProductStatus()
+        }
+
+        return resolvedPlan
+    }
+
+    private func subscriptionPlanFromProductStatus() async -> PaywallPlan? {
         if productsByID.isEmpty {
             do {
                 let loaded = try await Product.products(for: SubscriptionProductID.all)
@@ -271,12 +344,14 @@ final class SubscriptionManager: ObservableObject {
                 for product in loaded { map[product.id] = product }
                 productsByID = map
             } catch {
-                return false
+                return nil
             }
         }
 
         for product in productsByID.values {
             guard let subscription = product.subscription else { continue }
+            guard let plan = SubscriptionProductID.plan(for: product.id), plan != .lifetime else { continue }
+
             do {
                 let statuses = try await subscription.status
                 let isActive = statuses.contains { status in
@@ -289,13 +364,17 @@ final class SubscriptionManager: ObservableObject {
                         return false
                     }
                 }
-                if isActive { return true }
+                if isActive { return plan }
             } catch {
                 continue
             }
         }
 
-        return false
+        return nil
+    }
+
+    private func hasActiveSubscriptionStatus() async -> Bool {
+        await subscriptionPlanFromProductStatus() != nil
     }
 
     private func listenForTransactionUpdates() async {