|
|
@@ -27,6 +27,7 @@ final class SubscriptionManager: ObservableObject {
|
|
|
|
|
|
private static let cachedPremiumKey = "SubscriptionManager.cachedIsPremium"
|
|
|
private static let cachedPremiumPlanKey = "SubscriptionManager.cachedPremiumPlan"
|
|
|
+ private static let cachedHasEverPurchasedKey = "SubscriptionManager.cachedHasEverPurchased"
|
|
|
|
|
|
@Published private(set) var productsByID: [String: Product] = [:]
|
|
|
@Published private(set) var isLoadingProducts = false
|
|
|
@@ -37,8 +38,16 @@ final class SubscriptionManager: ObservableObject {
|
|
|
@Published private(set) var activePremiumPlan: PaywallPlan?
|
|
|
@Published private(set) var productLoadError: String?
|
|
|
@Published private(set) var introOfferEligibleByProductID: [String: Bool] = [:]
|
|
|
+ @Published private(set) var hasEverPurchasedPremium = false
|
|
|
@Published var purchaseError: PurchaseError?
|
|
|
|
|
|
+ var availablePaywallPlans: [PaywallPlan] {
|
|
|
+ if hasEverPurchasedPremium {
|
|
|
+ return PaywallPlan.allCases.filter { $0 != .lifetime }
|
|
|
+ }
|
|
|
+ return PaywallPlan.allCases
|
|
|
+ }
|
|
|
+
|
|
|
var isResolvingEntitlements: Bool {
|
|
|
!hasResolvedPremiumStatus
|
|
|
}
|
|
|
@@ -58,14 +67,10 @@ 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)
|
|
|
+ hasEverPurchasedPremium = UserDefaults.standard.bool(forKey: Self.cachedHasEverPurchasedKey)
|
|
|
if let raw = UserDefaults.standard.string(forKey: Self.cachedPremiumPlanKey),
|
|
|
let plan = PaywallPlan(rawValue: raw) {
|
|
|
activePremiumPlan = plan
|
|
|
@@ -76,14 +81,6 @@ 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 {
|
|
|
@@ -102,7 +99,7 @@ final class SubscriptionManager: ObservableObject {
|
|
|
}
|
|
|
|
|
|
func eligibleIntroOffer(for product: Product) -> Product.SubscriptionOffer? {
|
|
|
- guard isFirstTimeAppUser,
|
|
|
+ guard !hasEverPurchasedPremium,
|
|
|
introOfferEligibleByProductID[product.id] == true,
|
|
|
let offer = product.subscription?.introductoryOffer,
|
|
|
offer.paymentMode == .freeTrial else {
|
|
|
@@ -145,7 +142,7 @@ final class SubscriptionManager: ObservableObject {
|
|
|
return plan.fallbackBillingDescription(config: config)
|
|
|
}
|
|
|
|
|
|
- if isFirstTimeAppUser,
|
|
|
+ if !hasEverPurchasedPremium,
|
|
|
let subscription = product.subscription,
|
|
|
let intro = subscription.introductoryOffer,
|
|
|
intro.paymentMode == .freeTrial,
|
|
|
@@ -168,6 +165,11 @@ final class SubscriptionManager: ObservableObject {
|
|
|
func purchase(_ plan: PaywallPlan) async -> Bool {
|
|
|
purchaseError = nil
|
|
|
|
|
|
+ guard availablePaywallPlans.contains(plan) else {
|
|
|
+ purchaseError = .planUnavailable
|
|
|
+ return false
|
|
|
+ }
|
|
|
+
|
|
|
var product = productsByID[plan.productID]
|
|
|
if product == nil {
|
|
|
await loadProducts(presentLoadingUI: false)
|
|
|
@@ -344,6 +346,7 @@ final class SubscriptionManager: ObservableObject {
|
|
|
hasPremiumAccess = finalPlan != nil
|
|
|
hasResolvedPremiumStatus = true
|
|
|
persistPremiumCache()
|
|
|
+ await refreshPurchaseHistory()
|
|
|
}
|
|
|
|
|
|
/// Re-reads entitlements from StoreKit, then syncs with the App Store when needed.
|
|
|
@@ -387,6 +390,7 @@ final class SubscriptionManager: ObservableObject {
|
|
|
activePremiumPlan = preferredPlan(activePremiumPlan, plan)
|
|
|
hasPremiumAccess = true
|
|
|
hasResolvedPremiumStatus = true
|
|
|
+ markAsHavingPurchasedPremium()
|
|
|
persistPremiumCache()
|
|
|
}
|
|
|
|
|
|
@@ -411,9 +415,27 @@ final class SubscriptionManager: ObservableObject {
|
|
|
activePremiumPlan = preferredPlan(activePremiumPlan, plan)
|
|
|
hasPremiumAccess = true
|
|
|
hasResolvedPremiumStatus = true
|
|
|
+ markAsHavingPurchasedPremium()
|
|
|
persistPremiumCache()
|
|
|
}
|
|
|
|
|
|
+ private func markAsHavingPurchasedPremium() {
|
|
|
+ guard !hasEverPurchasedPremium else { return }
|
|
|
+ hasEverPurchasedPremium = true
|
|
|
+ UserDefaults.standard.set(true, forKey: Self.cachedHasEverPurchasedKey)
|
|
|
+ }
|
|
|
+
|
|
|
+ private func refreshPurchaseHistory() async {
|
|
|
+ if hasEverPurchasedPremium { return }
|
|
|
+
|
|
|
+ for await result in Transaction.all {
|
|
|
+ guard case .verified(let transaction) = result else { continue }
|
|
|
+ guard SubscriptionProductID.all.contains(transaction.productID) else { continue }
|
|
|
+ markAsHavingPurchasedPremium()
|
|
|
+ return
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
private func persistPremiumCache() {
|
|
|
UserDefaults.standard.set(hasPremiumAccess, forKey: Self.cachedPremiumKey)
|
|
|
UserDefaults.standard.set(activePremiumPlan?.rawValue, forKey: Self.cachedPremiumPlanKey)
|