Sfoglia il codice sorgente

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 mese fa
parent
commit
2c76525c73
1 ha cambiato i file con 97 aggiunte e 18 eliminazioni
  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 {