Add offline resilience to subscription status checks
Cache subscription expiration date so premium access survives offline launches and transient StoreKit failures. Restores cached state synchronously on init to eliminate loading flash. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -130,6 +130,7 @@ class IAPManager: ObservableObject {
|
||||
// MARK: - Initialization
|
||||
|
||||
init() {
|
||||
restoreCachedSubscriptionState()
|
||||
updateListenerTask = listenForTransactions()
|
||||
|
||||
Task {
|
||||
@@ -167,12 +168,25 @@ class IAPManager: ObservableObject {
|
||||
let hasActiveSubscription = await checkForActiveSubscription()
|
||||
|
||||
if hasActiveSubscription {
|
||||
// State already set in checkForActiveSubscription
|
||||
// State already set in checkForActiveSubscription — cache it
|
||||
if case .subscribed(let expiration, _) = state {
|
||||
cacheSubscriptionExpiration(expiration)
|
||||
}
|
||||
syncSubscriptionStatusToUserDefaults()
|
||||
return
|
||||
}
|
||||
|
||||
// No active subscription - check trial status
|
||||
// Live check found no active subscription.
|
||||
// Before downgrading, check if cached expiration is still valid (covers offline/transient failures).
|
||||
let cachedExpiration = GroupUserDefaults.groupDefaults.object(forKey: UserDefaultsStore.Keys.cachedSubscriptionExpiration.rawValue) as? Date
|
||||
if let expiration = cachedExpiration, expiration > Date() {
|
||||
state = .subscribed(expirationDate: expiration, willAutoRenew: false)
|
||||
syncSubscriptionStatusToUserDefaults()
|
||||
return
|
||||
}
|
||||
|
||||
// Subscription genuinely gone — clear cache and fall back to trial
|
||||
cacheSubscriptionExpiration(nil)
|
||||
updateTrialState()
|
||||
}
|
||||
|
||||
@@ -182,6 +196,27 @@ class IAPManager: ObservableObject {
|
||||
GroupUserDefaults.groupDefaults.set(accessValue, forKey: UserDefaultsStore.Keys.hasActiveSubscription.rawValue)
|
||||
}
|
||||
|
||||
/// Cache subscription state so premium access survives offline/failed checks
|
||||
private func cacheSubscriptionExpiration(_ date: Date?) {
|
||||
GroupUserDefaults.groupDefaults.set(date, forKey: UserDefaultsStore.Keys.cachedSubscriptionExpiration.rawValue)
|
||||
}
|
||||
|
||||
/// Restore cached subscription state on launch (before async check completes)
|
||||
private func restoreCachedSubscriptionState() {
|
||||
let hasActive = GroupUserDefaults.groupDefaults.bool(forKey: UserDefaultsStore.Keys.hasActiveSubscription.rawValue)
|
||||
guard hasActive else { return }
|
||||
|
||||
let cachedExpiration = GroupUserDefaults.groupDefaults.object(forKey: UserDefaultsStore.Keys.cachedSubscriptionExpiration.rawValue) as? Date
|
||||
|
||||
// If we have a cached expiration and it's still in the future, restore subscribed state
|
||||
if let expiration = cachedExpiration, expiration > Date() {
|
||||
state = .subscribed(expirationDate: expiration, willAutoRenew: false)
|
||||
} else if cachedExpiration == nil && hasActive {
|
||||
// Had access but no expiration cached (e.g., upgraded from older version) — trust it
|
||||
state = .subscribed(expirationDate: nil, willAutoRenew: false)
|
||||
}
|
||||
}
|
||||
|
||||
/// Restore purchases
|
||||
func restore() async {
|
||||
do {
|
||||
|
||||
Reference in New Issue
Block a user