diff --git a/Configuration.storekit b/Configuration.storekit deleted file mode 100644 index a3e76a1..0000000 --- a/Configuration.storekit +++ /dev/null @@ -1,139 +0,0 @@ -{ - "appPolicies" : { - "eula" : "", - "policies" : [ - { - "locale" : "en_US", - "policyText" : "", - "policyURL" : "" - } - ] - }, - "identifier" : "00CCEDCC", - "nonRenewingSubscriptions" : [ - - ], - "products" : [ - - ], - "settings" : { - "_askToBuyEnabled" : false, - "_billingGracePeriodEnabled" : false, - "_billingIssuesEnabled" : false, - "_disableDialogs" : false, - "_failTransactionsEnabled" : false, - "_locale" : "en_US", - "_renewalBillingIssuesEnabled" : false, - "_storefront" : "USA", - "_storeKitErrors" : [ - { - "enabled" : false, - "name" : "Load Products" - }, - { - "enabled" : false, - "name" : "Purchase" - }, - { - "enabled" : false, - "name" : "Verification" - }, - { - "enabled" : false, - "name" : "App Store Sync" - }, - { - "enabled" : false, - "name" : "Subscription Status" - }, - { - "enabled" : false, - "name" : "App Transaction" - }, - { - "enabled" : false, - "name" : "Manage Subscriptions Sheet" - }, - { - "enabled" : false, - "name" : "Refund Request Sheet" - }, - { - "enabled" : false, - "name" : "Offer Code Redeem Sheet" - } - ], - "_timeRate" : 0 - }, - "subscriptionGroups" : [ - { - "id" : "21914363", - "localizations" : [ - - ], - "name" : "group1", - "subscriptions" : [ - { - "adHocOffers" : [ - - ], - "codeOffers" : [ - - ], - "displayPrice" : "0.99", - "familyShareable" : false, - "groupNumber" : 1, - "internalID" : "C011E06B", - "introductoryOffer" : null, - "localizations" : [ - { - "description" : "Flexible month-to-month billing", - "displayName" : "Monthly", - "locale" : "en_US" - } - ], - "productID" : "com.88oakapps.feels.IAP.subscriptions.monthly", - "recurringSubscriptionPeriod" : "P1M", - "referenceName" : "Monthly", - "subscriptionGroupID" : "21914363", - "type" : "RecurringSubscription", - "winbackOffers" : [ - - ] - }, - { - "adHocOffers" : [ - - ], - "codeOffers" : [ - - ], - "displayPrice" : "9.99", - "familyShareable" : false, - "groupNumber" : 1, - "internalID" : "32967821", - "introductoryOffer" : null, - "localizations" : [ - { - "description" : "Best value — save over 15%", - "displayName" : "Yearly", - "locale" : "en_US" - } - ], - "productID" : "com.88oakapps.feels.IAP.subscriptions.yearly", - "recurringSubscriptionPeriod" : "P1Y", - "referenceName" : "Yearly", - "subscriptionGroupID" : "21914363", - "type" : "RecurringSubscription", - "winbackOffers" : [ - - ] - } - ] - } - ], - "version" : { - "major" : 4, - "minor" : 0 - } -} diff --git a/Shared/IAPManager.swift b/Shared/IAPManager.swift index 60bf02b..8f1fd3a 100644 --- a/Shared/IAPManager.swift +++ b/Shared/IAPManager.swift @@ -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 { diff --git a/Shared/Models/UserDefaultsStore.swift b/Shared/Models/UserDefaultsStore.swift index 3ff2adb..4b64982 100644 --- a/Shared/Models/UserDefaultsStore.swift +++ b/Shared/Models/UserDefaultsStore.swift @@ -196,6 +196,7 @@ class UserDefaultsStore { case daysFilter case firstLaunchDate case hasActiveSubscription + case cachedSubscriptionExpiration case lastVotedDate case votingLayoutStyle case dayViewStyle