Files
FamilyMealPlanner/MealMood/Services/PremiumSyncService.swift
T
alexandrev-tibco 110807d239 1.0.5: Eating out slots, rule violations panel, Crashlytics, search bar fix
- 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>
2026-04-27 09:48:20 +02:00

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)
}
}
}