|
|
@@ -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 {
|