110807d239
- Eating out: tap any empty slot → "Mark as eating out"; shows teal indicator, skipped by auto-assign, counts as complete, tap again to unmark - Rule violations panel: access via wand long-press context menu when conflicts exist; shows all isRuleOverridden slots with Fix (clear) or Ignore (acknowledge) actions - Firebase Crashlytics integrated: CrashlyticsService + dSYM upload build phase, isPremium property tracked per session - PremiumSyncService: extracted premium state machine, StoreKit Transaction.updates listener, isPremium no longer synced via iCloud to avoid stale state - StoreManager: analytics on purchase/restore, bundle ID fallback for product ID lookup - Search bar contrast bug fixed: TextField now has explicit foreground color for dark mode - WelcomeStepView: redesigned onboarding welcome screen with week preview - Version bump: 1.0.5 build 22 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
37 lines
1.5 KiB
Swift
37 lines
1.5 KiB
Swift
import Foundation
|
|
|
|
/// Encapsulates the premium status sync state machine.
|
|
/// Extracted from ContentView to be independently testable.
|
|
@MainActor
|
|
final class PremiumSyncService {
|
|
|
|
/// Injectable for testing — defaults to real StoreKit check.
|
|
var hasActiveSubscription: () async -> Bool = {
|
|
await StoreManager.hasActiveSubscription()
|
|
}
|
|
|
|
/// Determine whether the user should be premium after a sync.
|
|
///
|
|
/// Rules:
|
|
/// - Active subscription found → always return `true` (upgrade).
|
|
/// - No subscription, currently free → return `false`.
|
|
/// - No subscription, currently premium, **cold launch** → return `false`
|
|
/// (subscription likely expired between sessions).
|
|
/// - No subscription, currently premium, **foreground activation** → return `true`
|
|
/// (StoreKit `currentEntitlements` can return empty for a few seconds immediately
|
|
/// after a purchase while the receipt propagates; never downgrade mid-session).
|
|
func sync(currentIsPremium: Bool, isColdLaunch: Bool) async -> Bool {
|
|
let active = await hasActiveSubscription()
|
|
switch (active, currentIsPremium, isColdLaunch) {
|
|
case (true, _, _):
|
|
return true
|
|
case (false, false, _):
|
|
return false
|
|
case (false, true, true):
|
|
return false // Subscription expired → downgrade on cold launch
|
|
case (false, true, false):
|
|
return true // Keep premium on foreground (timing protection)
|
|
}
|
|
}
|
|
}
|